python-som 0.1.3__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_som-0.1.3 → python_som-0.3.0}/.gitignore +9 -0
- python_som-0.3.0/CHANGELOG.md +179 -0
- python_som-0.3.0/PKG-INFO +174 -0
- python_som-0.3.0/README.md +125 -0
- python_som-0.3.0/docs/api.md +26 -0
- python_som-0.3.0/docs/assets/iris.png +0 -0
- python_som-0.3.0/docs/changelog.md +1 -0
- python_som-0.3.0/docs/getting-started.md +138 -0
- python_som-0.3.0/docs/index.md +1 -0
- python_som-0.3.0/docs/neighborhood-functions.md +135 -0
- python_som-0.3.0/docs/training-modes.md +90 -0
- python_som-0.3.0/examples/iris.py +84 -0
- python_som-0.3.0/pyproject.toml +169 -0
- python_som-0.3.0/src/python_som/__init__.py +58 -0
- python_som-0.3.0/src/python_som/_decay.py +68 -0
- python_som-0.3.0/src/python_som/_distance.py +26 -0
- python_som-0.3.0/src/python_som/_neighborhood.py +218 -0
- python_som-0.3.0/src/python_som/_som.py +649 -0
- python_som-0.3.0/src/python_som/py.typed +0 -0
- python_som-0.3.0/tests/__init__.py +5 -0
- python_som-0.3.0/tests/conftest.py +79 -0
- python_som-0.3.0/tests/test_analysis.py +134 -0
- python_som-0.3.0/tests/test_construction.py +148 -0
- python_som-0.3.0/tests/test_decay.py +80 -0
- python_som-0.3.0/tests/test_end_to_end.py +114 -0
- python_som-0.3.0/tests/test_io_types.py +80 -0
- python_som-0.3.0/tests/test_neighborhood.py +332 -0
- python_som-0.3.0/tests/test_packaging.py +103 -0
- python_som-0.3.0/tests/test_training.py +291 -0
- python_som-0.3.0/tests/test_weight_init.py +125 -0
- python_som-0.1.3/PKG-INFO +0 -147
- python_som-0.1.3/README.md +0 -121
- python_som-0.1.3/pyproject.toml +0 -39
- python_som-0.1.3/python_som/__init__.py +0 -825
- python_som-0.1.3/requirements.txt +0 -3
- python_som-0.1.3/test.py +0 -74
- python_som-0.1.3/test_iris.ipynb +0 -408
- python_som-0.1.3/test_output_iris.png +0 -0
- {python_som-0.1.3 → python_som-0.3.0}/LICENSE +0 -0
|
@@ -127,3 +127,12 @@ dmypy.json
|
|
|
127
127
|
|
|
128
128
|
# Pyre type checker
|
|
129
129
|
.pyre/
|
|
130
|
+
|
|
131
|
+
# .dump directory
|
|
132
|
+
/.dump
|
|
133
|
+
|
|
134
|
+
# Claude Code working files. CLAUDE.md holds this maintainer's agent conventions rather than
|
|
135
|
+
# repository policy, so it stays local instead of being imposed on contributors.
|
|
136
|
+
/CLAUDE.md
|
|
137
|
+
/CLAUDE.local.md
|
|
138
|
+
/.claude/
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
6
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-07-29
|
|
9
|
+
|
|
10
|
+
Modernizes the packaging and corrects four methodology defects. **Numerical results differ from
|
|
11
|
+
0.2.0.** The changes are listed with the passage of Kohonen (2013) each one follows.
|
|
12
|
+
|
|
13
|
+
### Changed, and results differ
|
|
14
|
+
|
|
15
|
+
- **Seeds no longer reproduce 0.1.x/0.2.0 results.** The random generator is now a per-instance
|
|
16
|
+
`np.random.Generator`. Previously the constructor called `np.random.seed`, which reseeded NumPy's
|
|
17
|
+
*global* generator as a side effect of building a SOM, disturbing the whole host program. The
|
|
18
|
+
side effect is gone, but `random_seed=42` now yields a different map.
|
|
19
|
+
|
|
20
|
+
To reproduce figures made with an earlier version, pin `python-som==0.2.0`.
|
|
21
|
+
|
|
22
|
+
- Batch training no longer destroys models whose neighborhood contains no data. `_train_batch`
|
|
23
|
+
built its result from a zeroed array and assigned it wholesale, so every model it did not touch
|
|
24
|
+
became the zero vector. On a 30x30 map with 20 samples and a small radius, that wiped 282 of 900
|
|
25
|
+
models in a single step, and drove the quantization error from 0.0 to 0.196. Untouched models now
|
|
26
|
+
keep their previous value, and the denominator is compared against a tolerance rather than exact
|
|
27
|
+
zero. (Kohonen Eq. 8.)
|
|
28
|
+
|
|
29
|
+
- Linear initialization now spans the principal-component plane. It used
|
|
30
|
+
`pca.explained_variance_`, the two eigen*values*, where it needed `pca.components_`, the
|
|
31
|
+
eigen*vectors*. Every model came out as a scalar broadcast across all features: a constant vector
|
|
32
|
+
on the all-ones diagonal, rank 1 rather than a plane. PCA is also now fitted on the raw data
|
|
33
|
+
instead of standardized data, so the models live in the same space as the inputs they are compared
|
|
34
|
+
against during training. (Kohonen Section 4.3.)
|
|
35
|
+
|
|
36
|
+
- `random` training now samples i.i.d. with replacement. It previously drew with
|
|
37
|
+
`replace=(n_iteration > len(data))`, so it was a random permutation whenever the iteration count
|
|
38
|
+
did not exceed the sample count, and independent draws only beyond it. The character of the
|
|
39
|
+
sampling therefore depended on how many iterations you asked for. It is now uniformly i.i.d.,
|
|
40
|
+
which is the stochastic approximation of Robbins and Monro (1951) that Kohonen cites in
|
|
41
|
+
Section 4.1.
|
|
42
|
+
|
|
43
|
+
- `sequential` training honours `n_iteration`. It iterated the dataset exactly once regardless
|
|
44
|
+
of the requested count, so asking for 500 iterations over 30 samples ran 30 steps while the decay
|
|
45
|
+
functions still used 500 as their horizon, leaving the learning rate almost unchanged. It now
|
|
46
|
+
cycles the dataset until the requested number of steps has run.
|
|
47
|
+
|
|
48
|
+
- The automatic map-size ratio uses `sqrt(lambda_1 / lambda_2)` rather than the raw eigenvalue
|
|
49
|
+
ratio, rounds instead of floor-dividing, and clamps to at least 1 so a side can no longer come out
|
|
50
|
+
as zero. Kohonen Section 3.5 asks for side lengths matching "the lengths of the two largest
|
|
51
|
+
principal components"; a component is a unit direction, so its length in the data is the standard
|
|
52
|
+
deviation. For Iris with `x=20`, `y` moves from 6 to 11.
|
|
53
|
+
|
|
54
|
+
- The neighborhood radius is floored at the new `min_neighborhood_radius` parameter, default
|
|
55
|
+
0.5. Kohonen Section 4.2: "the final value of sigma shall not go to zero, because otherwise the
|
|
56
|
+
process loses its ordering power. It should always remain, say, above half of the grid spacing."
|
|
57
|
+
This also stops `linear_decay` from reaching a division by zero at its horizon.
|
|
58
|
+
|
|
59
|
+
- `get_shape()` returns `tuple[int, int]` rather than NumPy integers, so
|
|
60
|
+
`plt.figure(figsize=som.get_shape())` works without the `float()` workaround the old example
|
|
61
|
+
needed.
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- `mexican_hat` is accepted as an alias for `mexicanhat`.
|
|
66
|
+
- A test suite, where there was none: 190 tests at 100% coverage, including one regression test
|
|
67
|
+
per defect above carrying its measured numbers, and property-based tests via Hypothesis over
|
|
68
|
+
generated grid sizes, radii and centres.
|
|
69
|
+
- Type annotations are now exported, via a `py.typed` marker. The library was already annotated,
|
|
70
|
+
but without the marker PEP 561 requires downstream type checkers to ignore it.
|
|
71
|
+
- Documentation site at <https://andremsouza.github.io/python-som/>, built with Material for
|
|
72
|
+
MkDocs, including the derivations behind the neighborhood functions.
|
|
73
|
+
- Continuous integration across Python 3.10 to 3.13, with ruff, mypy `--strict`, coverage
|
|
74
|
+
gating, and a build that installs the wheel into a clean environment and imports it.
|
|
75
|
+
- Releases publish through PyPI Trusted Publishing, so no long-lived API token exists.
|
|
76
|
+
|
|
77
|
+
### Performance
|
|
78
|
+
|
|
79
|
+
- Batch training is about 30x faster. The nested Python loop over every pair of nodes is
|
|
80
|
+
replaced by a NumPy contraction. Measured on a 20x20 map with 150 samples over 5 iterations: 1.89 s
|
|
81
|
+
to 0.06 s. A regression test asserts the new result matches the old formulation to 1e-12.
|
|
82
|
+
|
|
83
|
+
### Internal
|
|
84
|
+
|
|
85
|
+
- The package moves to a `src/` layout and splits into `_som`, `_neighborhood`, `_decay` and
|
|
86
|
+
`_distance`. `import python_som; python_som.SOM(...)` is unchanged, and the pre-0.3.0 private names
|
|
87
|
+
(`_asymptotic_decay`, `_euclidean_distance`, and the rest) still resolve.
|
|
88
|
+
- `test_iris.ipynb` is removed; its narrative moved into the documentation. `SOM_atualizado.py`, an
|
|
89
|
+
untracked experimental variant that the sdist had been sweeping up, is gone.
|
|
90
|
+
- `print` calls become `logging`; `verbose=True` still drives the tqdm progress bar.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- The `bubble` neighborhood is documented as using the Chebyshev metric, so its region is a
|
|
95
|
+
square rather than a disc. The behavior is unchanged and follows Vrieze's appendix pseudo-code
|
|
96
|
+
(`b = MAX(ABS(i - w_i), ABS(j - w_j))`), but it was previously unstated, and it means the bubble is
|
|
97
|
+
*not* isotropic under the Euclidean metric. The test suite shipped in 0.2.0 asserted that it was;
|
|
98
|
+
that assertion passed only because no counterexample exists on a grid that small. The smallest is
|
|
99
|
+
at radius `sqrt(50)`, where `(5, 5)` is inside a `sigma = 5` square and `(7, 1)` is outside.
|
|
100
|
+
|
|
101
|
+
## [0.2.0] - 2026-07-28
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- Mexican hat neighborhood function, selectable with `neighborhood_function='mexicanhat'`
|
|
106
|
+
(contributed by [@Arne49](https://github.com/Arne49) in
|
|
107
|
+
[#2](https://github.com/andremsouza/python-som/pull/2)).
|
|
108
|
+
|
|
109
|
+
Implemented as the isotropic 2-D form `(1 - u) * exp(-u)` over `u = sqdist(c, i) / (2 * sigma^2)`,
|
|
110
|
+
normalized so that `h(c, c) = 1`. It crosses zero at a radius of `sqrt(2) * sigma` and reaches its
|
|
111
|
+
minimum of `-exp(-2)` at a radius of `2 * sigma`.
|
|
112
|
+
|
|
113
|
+
Note that a neighborhood function must depend on the grid distance alone, not on the two axis
|
|
114
|
+
offsets separately, per `sqdist(c, i)` in Kohonen (2013) Eq. (5) and the "lateral distance"
|
|
115
|
+
abscissa of Vrieze (1995) Fig. 3. The gaussian happens to be separable into a product of per-axis
|
|
116
|
+
terms, but that is a property of the exponential rather than of neighborhood functions in general.
|
|
117
|
+
|
|
118
|
+
- `## Neighborhood functions` section in the README, comparing all three and documenting the
|
|
119
|
+
batch-mode restriction below.
|
|
120
|
+
- Test suite for the neighborhood functions, asserting closed-form analytic properties rather than
|
|
121
|
+
golden values.
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- Cyclic (toroidal) neighborhoods were only correct in one direction. The fold-back handled
|
|
126
|
+
offsets above `+length/2` but left those below `-length/2` alone, so a winner in the upper half of
|
|
127
|
+
an axis did not wrap. On a 10x10 toroidal map with sigma=1, a winner at row 0 gave `h[9] = 0.6065`
|
|
128
|
+
while a winner at row 9 gave `h[0] = 0.0` instead of the same 0.6065. Now uses the minimum-image
|
|
129
|
+
convention, which folds both tails.
|
|
130
|
+
|
|
131
|
+
This changes results for any map built with `cyclic_x=True` or `cyclic_y=True`.
|
|
132
|
+
|
|
133
|
+
- A neighborhood radius of zero divided by zero and produced `inf`/`nan` weights instead of raising.
|
|
134
|
+
`_gaussian` and `_mexicanhat` now reject a non-finite or non-positive radius; `_bubble` still
|
|
135
|
+
accepts a radius of zero, which selects the winner alone.
|
|
136
|
+
|
|
137
|
+
- An unknown `neighborhood_function` raised a bare `KeyError`. It now raises `ValueError` listing the
|
|
138
|
+
valid names, matching the behavior of `weight_initialization`.
|
|
139
|
+
|
|
140
|
+
### Changed
|
|
141
|
+
|
|
142
|
+
- `requires-python` is now `>=3.10`. The declared floor of `>=3.9` was never accurate: the module
|
|
143
|
+
uses PEP 604 unions (`int | None`) in runtime-evaluated annotations without
|
|
144
|
+
`from __future__ import annotations`, so importing it on Python 3.9 raises `TypeError`. Python 3.9
|
|
145
|
+
reached end of life in October 2025. Installers will now decline the package on 3.9 rather than
|
|
146
|
+
install something that cannot be imported.
|
|
147
|
+
|
|
148
|
+
- `train(mode='batch')` combined with `neighborhood_function='mexicanhat'` now raises `ValueError`.
|
|
149
|
+
The batch update of Kohonen (2013) Eq. (8) is a weighted mean whose denominator `sum_j n_j * h_ji`
|
|
150
|
+
is only well defined for a non-negative neighborhood function. The mexican hat is signed by
|
|
151
|
+
construction, so the denominator can vanish or turn negative: on a 12x12 grid, 49 of 144
|
|
152
|
+
denominators come out negative, which inverts or explodes the update. This is a property of the
|
|
153
|
+
function itself rather than of this implementation. Use `mode='random'` or `mode='sequential'`.
|
|
154
|
+
|
|
155
|
+
### Packaging
|
|
156
|
+
|
|
157
|
+
- Restored the `cli` extra (`pip install python-som[cli]`, which pulls in `tqdm` for training
|
|
158
|
+
progress bars). This extra shipped in 0.1.3 on PyPI but was never committed to the repository, so
|
|
159
|
+
releasing from the repository as-is would have silently removed it.
|
|
160
|
+
|
|
161
|
+
### Note on 0.1.3
|
|
162
|
+
|
|
163
|
+
There is no changelog entry for 0.1.3 because it was published to PyPI from an uncommitted working
|
|
164
|
+
tree and has no corresponding commit. Its `python_som/__init__.py` is byte-identical to the 0.1.2
|
|
165
|
+
tag; the only differences are the version string and the `cli` extra restored above.
|
|
166
|
+
|
|
167
|
+
## [0.1.3] - 2025-01-14
|
|
168
|
+
|
|
169
|
+
Published to PyPI only. See the note above.
|
|
170
|
+
|
|
171
|
+
## [0.1.2] - 2025-01-14
|
|
172
|
+
|
|
173
|
+
Earlier releases predate this changelog. See the
|
|
174
|
+
[commit history](https://github.com/andremsouza/python-som/commits/master) for details.
|
|
175
|
+
|
|
176
|
+
[0.3.0]: https://github.com/andremsouza/python-som/releases/tag/v0.3.0
|
|
177
|
+
[0.2.0]: https://github.com/andremsouza/python-som/releases/tag/v0.2.0
|
|
178
|
+
[0.1.3]: https://pypi.org/project/python-som/0.1.3/
|
|
179
|
+
[0.1.2]: https://pypi.org/project/python-som/0.1.2/
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-som
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python implementation of the Self-Organizing Map
|
|
5
|
+
Project-URL: Homepage, https://github.com/andremsouza/python-som
|
|
6
|
+
Project-URL: Documentation, https://andremsouza.github.io/python-som/
|
|
7
|
+
Project-URL: Changelog, https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/andremsouza/python-som/issues
|
|
9
|
+
Author-email: André Moreira Souza <msouza.andre@hotmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: pandas>=2.0
|
|
28
|
+
Requires-Dist: scikit-learn>=1.3
|
|
29
|
+
Provides-Extra: cli
|
|
30
|
+
Requires-Dist: tqdm>=4.66; extra == 'cli'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: hypothesis==6.163.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy==2.3.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pandas-stubs==3.0.3.260530; (python_version >= '3.11') and extra == 'dev'
|
|
35
|
+
Requires-Dist: pre-commit==4.6.1; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff==0.16.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: tomli==2.4.1; (python_version < '3.11') and extra == 'dev'
|
|
40
|
+
Requires-Dist: twine==7.0.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: types-tqdm==4.69.0.20260728; extra == 'dev'
|
|
42
|
+
Provides-Extra: docs
|
|
43
|
+
Requires-Dist: mkdocs-material==9.7.7; extra == 'docs'
|
|
44
|
+
Requires-Dist: mkdocstrings-python==2.0.5; extra == 'docs'
|
|
45
|
+
Provides-Extra: examples
|
|
46
|
+
Requires-Dist: matplotlib>=3.8; extra == 'examples'
|
|
47
|
+
Requires-Dist: seaborn>=0.13; extra == 'examples'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# python-som
|
|
51
|
+
|
|
52
|
+
[](https://github.com/andremsouza/python-som/actions/workflows/ci.yml)
|
|
53
|
+
[](https://pypi.org/project/python-som/)
|
|
54
|
+
[](https://pypi.org/project/python-som/)
|
|
55
|
+
[](https://github.com/andremsouza/python-som/blob/master/LICENSE)
|
|
56
|
+
|
|
57
|
+
Implementation of Kohonen's 2-D self-organizing map, built on NumPy, with scikit-learn for PCA and
|
|
58
|
+
standardization. Accepts NumPy arrays, pandas DataFrames and plain lists.
|
|
59
|
+
|
|
60
|
+
[Documentation](https://andremsouza.github.io/python-som/) ·
|
|
61
|
+
[Changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md)
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install python-som # requires Python 3.10+
|
|
67
|
+
pip install "python-som[cli]" # adds tqdm progress bars
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import numpy as np
|
|
74
|
+
import python_som
|
|
75
|
+
|
|
76
|
+
rng = np.random.default_rng(0)
|
|
77
|
+
data = rng.normal(size=(150, 4))
|
|
78
|
+
|
|
79
|
+
som = python_som.SOM(x=20, y=None, input_len=4, data=data, random_seed=42)
|
|
80
|
+
som.weight_initialization(mode="linear", data=data)
|
|
81
|
+
error = som.train(data, n_iteration=len(data), mode="batch")
|
|
82
|
+
|
|
83
|
+
umatrix = som.distance_matrix()
|
|
84
|
+
winner = som.winner(data[0])
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A full worked example with plots is in [examples/iris.py](https://github.com/andremsouza/python-som/blob/master/examples/iris.py) and in the
|
|
88
|
+
[getting-started guide](https://andremsouza.github.io/python-som/getting-started/).
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
|
|
94
|
+
* Stepwise and batch training
|
|
95
|
+
* Random, random-sampling and linear (PCA) weight initialization
|
|
96
|
+
* Automatic selection of the map size ratio, from PCA
|
|
97
|
+
* Cyclic arrays, for toroidal maps
|
|
98
|
+
* Gaussian, bubble and Mexican hat neighborhood functions
|
|
99
|
+
* Custom decay functions
|
|
100
|
+
* Visualization support: U-matrix, activation matrix
|
|
101
|
+
* Supervised labelling, via the label map
|
|
102
|
+
* Fully type-annotated, with a `py.typed` marker
|
|
103
|
+
|
|
104
|
+
## Neighborhood functions
|
|
105
|
+
|
|
106
|
+
All three are functions of the distance between two nodes in the grid, `sqdist(c, i)` in Eq. (5) of
|
|
107
|
+
Kohonen (2013).
|
|
108
|
+
|
|
109
|
+
| Name | Shape | Notes |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `'gaussian'` | `exp(-r² / 2σ²)` | Strictly positive, monotonically decreasing. The default. |
|
|
112
|
+
| `'bubble'` | `1` for `max(dx, dy) ≤ σ`, else `0` | The truncated inner lobe of the Mexican hat. Uses the Chebyshev metric, so the region is a square. |
|
|
113
|
+
| `'mexicanhat'` | `(1 - u)·exp(-u)`, `u = r² / 2σ²` | Excitatory near the winner, inhibitory beyond it. Zero at `r = √2·σ`, minimum `-e⁻²` at `r = 2σ`. |
|
|
114
|
+
|
|
115
|
+
The Mexican hat takes negative values, so it **cannot be used with `mode='batch'`**: the batch
|
|
116
|
+
update of Kohonen Eq. (8) is a weighted mean whose denominator is not sign-definite for a signed
|
|
117
|
+
neighborhood function. Use `mode='random'` or `mode='sequential'`; `mode='batch'` raises a
|
|
118
|
+
`ValueError`.
|
|
119
|
+
|
|
120
|
+
See [Neighborhood functions](https://andremsouza.github.io/python-som/neighborhood-functions/) for
|
|
121
|
+
the derivations, including why the Mexican hat is not an outer product of two 1-D wavelets.
|
|
122
|
+
|
|
123
|
+
## Upgrading
|
|
124
|
+
|
|
125
|
+
0.3.0 corrects several methodology defects, so numerical results are not comparable with earlier
|
|
126
|
+
versions. In particular **`random_seed` no longer reproduces pre-0.3.0 maps**: the generator is now
|
|
127
|
+
per-instance rather than a call to `np.random.seed` on NumPy's global state. To reproduce figures
|
|
128
|
+
made with an older version, pin `python-som==0.2.0`.
|
|
129
|
+
|
|
130
|
+
Each change and the passage of Kohonen (2013) behind it is in the
|
|
131
|
+
[changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md).
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv sync --all-extras
|
|
137
|
+
uv run pytest --cov # tests and coverage
|
|
138
|
+
uv run ruff check . # lint
|
|
139
|
+
uv run ruff format --check . # formatting
|
|
140
|
+
uv run mypy # type-check
|
|
141
|
+
uv run mkdocs serve # docs, locally
|
|
142
|
+
pre-commit install # optional, run the gates on commit
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
If you use the SonarQube for IDE (SonarLint) VS Code extension, it will also apply Sonar's Python
|
|
146
|
+
rules locally; the ruff configuration is set up to cover most of the same ground.
|
|
147
|
+
|
|
148
|
+
## References
|
|
149
|
+
|
|
150
|
+
Based on:
|
|
151
|
+
|
|
152
|
+
Teuvo Kohonen,
|
|
153
|
+
Essentials of the self-organizing map,
|
|
154
|
+
Neural Networks,
|
|
155
|
+
Volume 37,
|
|
156
|
+
2013,
|
|
157
|
+
Pages 52-65,
|
|
158
|
+
ISSN 0893-6080,
|
|
159
|
+
<https://doi.org/10.1016/j.neunet.2012.09.018>
|
|
160
|
+
|
|
161
|
+
The Mexican hat neighborhood follows the lateral-interaction formulation in:
|
|
162
|
+
|
|
163
|
+
O. J. Vrieze,
|
|
164
|
+
Kohonen network,
|
|
165
|
+
in: Artificial Neural Networks: An Introduction to ANN Theory and Practice,
|
|
166
|
+
Lecture Notes in Computer Science, Volume 931,
|
|
167
|
+
Springer, Berlin, Heidelberg,
|
|
168
|
+
1995,
|
|
169
|
+
Pages 83-100,
|
|
170
|
+
<https://doi.org/10.1007/BFb0027024>
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT. See [LICENSE](https://github.com/andremsouza/python-som/blob/master/LICENSE).
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# python-som
|
|
2
|
+
|
|
3
|
+
[](https://github.com/andremsouza/python-som/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/python-som/)
|
|
5
|
+
[](https://pypi.org/project/python-som/)
|
|
6
|
+
[](https://github.com/andremsouza/python-som/blob/master/LICENSE)
|
|
7
|
+
|
|
8
|
+
Implementation of Kohonen's 2-D self-organizing map, built on NumPy, with scikit-learn for PCA and
|
|
9
|
+
standardization. Accepts NumPy arrays, pandas DataFrames and plain lists.
|
|
10
|
+
|
|
11
|
+
[Documentation](https://andremsouza.github.io/python-som/) ·
|
|
12
|
+
[Changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install python-som # requires Python 3.10+
|
|
18
|
+
pip install "python-som[cli]" # adds tqdm progress bars
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import numpy as np
|
|
25
|
+
import python_som
|
|
26
|
+
|
|
27
|
+
rng = np.random.default_rng(0)
|
|
28
|
+
data = rng.normal(size=(150, 4))
|
|
29
|
+
|
|
30
|
+
som = python_som.SOM(x=20, y=None, input_len=4, data=data, random_seed=42)
|
|
31
|
+
som.weight_initialization(mode="linear", data=data)
|
|
32
|
+
error = som.train(data, n_iteration=len(data), mode="batch")
|
|
33
|
+
|
|
34
|
+
umatrix = som.distance_matrix()
|
|
35
|
+
winner = som.winner(data[0])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A full worked example with plots is in [examples/iris.py](https://github.com/andremsouza/python-som/blob/master/examples/iris.py) and in the
|
|
39
|
+
[getting-started guide](https://andremsouza.github.io/python-som/getting-started/).
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
* Stepwise and batch training
|
|
46
|
+
* Random, random-sampling and linear (PCA) weight initialization
|
|
47
|
+
* Automatic selection of the map size ratio, from PCA
|
|
48
|
+
* Cyclic arrays, for toroidal maps
|
|
49
|
+
* Gaussian, bubble and Mexican hat neighborhood functions
|
|
50
|
+
* Custom decay functions
|
|
51
|
+
* Visualization support: U-matrix, activation matrix
|
|
52
|
+
* Supervised labelling, via the label map
|
|
53
|
+
* Fully type-annotated, with a `py.typed` marker
|
|
54
|
+
|
|
55
|
+
## Neighborhood functions
|
|
56
|
+
|
|
57
|
+
All three are functions of the distance between two nodes in the grid, `sqdist(c, i)` in Eq. (5) of
|
|
58
|
+
Kohonen (2013).
|
|
59
|
+
|
|
60
|
+
| Name | Shape | Notes |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `'gaussian'` | `exp(-r² / 2σ²)` | Strictly positive, monotonically decreasing. The default. |
|
|
63
|
+
| `'bubble'` | `1` for `max(dx, dy) ≤ σ`, else `0` | The truncated inner lobe of the Mexican hat. Uses the Chebyshev metric, so the region is a square. |
|
|
64
|
+
| `'mexicanhat'` | `(1 - u)·exp(-u)`, `u = r² / 2σ²` | Excitatory near the winner, inhibitory beyond it. Zero at `r = √2·σ`, minimum `-e⁻²` at `r = 2σ`. |
|
|
65
|
+
|
|
66
|
+
The Mexican hat takes negative values, so it **cannot be used with `mode='batch'`**: the batch
|
|
67
|
+
update of Kohonen Eq. (8) is a weighted mean whose denominator is not sign-definite for a signed
|
|
68
|
+
neighborhood function. Use `mode='random'` or `mode='sequential'`; `mode='batch'` raises a
|
|
69
|
+
`ValueError`.
|
|
70
|
+
|
|
71
|
+
See [Neighborhood functions](https://andremsouza.github.io/python-som/neighborhood-functions/) for
|
|
72
|
+
the derivations, including why the Mexican hat is not an outer product of two 1-D wavelets.
|
|
73
|
+
|
|
74
|
+
## Upgrading
|
|
75
|
+
|
|
76
|
+
0.3.0 corrects several methodology defects, so numerical results are not comparable with earlier
|
|
77
|
+
versions. In particular **`random_seed` no longer reproduces pre-0.3.0 maps**: the generator is now
|
|
78
|
+
per-instance rather than a call to `np.random.seed` on NumPy's global state. To reproduce figures
|
|
79
|
+
made with an older version, pin `python-som==0.2.0`.
|
|
80
|
+
|
|
81
|
+
Each change and the passage of Kohonen (2013) behind it is in the
|
|
82
|
+
[changelog](https://github.com/andremsouza/python-som/blob/master/CHANGELOG.md).
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync --all-extras
|
|
88
|
+
uv run pytest --cov # tests and coverage
|
|
89
|
+
uv run ruff check . # lint
|
|
90
|
+
uv run ruff format --check . # formatting
|
|
91
|
+
uv run mypy # type-check
|
|
92
|
+
uv run mkdocs serve # docs, locally
|
|
93
|
+
pre-commit install # optional, run the gates on commit
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If you use the SonarQube for IDE (SonarLint) VS Code extension, it will also apply Sonar's Python
|
|
97
|
+
rules locally; the ruff configuration is set up to cover most of the same ground.
|
|
98
|
+
|
|
99
|
+
## References
|
|
100
|
+
|
|
101
|
+
Based on:
|
|
102
|
+
|
|
103
|
+
Teuvo Kohonen,
|
|
104
|
+
Essentials of the self-organizing map,
|
|
105
|
+
Neural Networks,
|
|
106
|
+
Volume 37,
|
|
107
|
+
2013,
|
|
108
|
+
Pages 52-65,
|
|
109
|
+
ISSN 0893-6080,
|
|
110
|
+
<https://doi.org/10.1016/j.neunet.2012.09.018>
|
|
111
|
+
|
|
112
|
+
The Mexican hat neighborhood follows the lateral-interaction formulation in:
|
|
113
|
+
|
|
114
|
+
O. J. Vrieze,
|
|
115
|
+
Kohonen network,
|
|
116
|
+
in: Artificial Neural Networks: An Introduction to ANN Theory and Practice,
|
|
117
|
+
Lecture Notes in Computer Science, Volume 931,
|
|
118
|
+
Springer, Berlin, Heidelberg,
|
|
119
|
+
1995,
|
|
120
|
+
Pages 83-100,
|
|
121
|
+
<https://doi.org/10.1007/BFb0027024>
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT. See [LICENSE](https://github.com/andremsouza/python-som/blob/master/LICENSE).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
::: python_som
|
|
4
|
+
options:
|
|
5
|
+
members:
|
|
6
|
+
- SOM
|
|
7
|
+
|
|
8
|
+
## Neighborhood functions
|
|
9
|
+
|
|
10
|
+
::: python_som._neighborhood
|
|
11
|
+
options:
|
|
12
|
+
members:
|
|
13
|
+
- gaussian
|
|
14
|
+
- bubble
|
|
15
|
+
- mexican_hat
|
|
16
|
+
- axis_offsets
|
|
17
|
+
- squared_grid_distance
|
|
18
|
+
- resolve
|
|
19
|
+
|
|
20
|
+
## Decay functions
|
|
21
|
+
|
|
22
|
+
::: python_som._decay
|
|
23
|
+
|
|
24
|
+
## Distance functions
|
|
25
|
+
|
|
26
|
+
::: python_som._distance
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--8<-- "CHANGELOG.md"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install python-som
|
|
7
|
+
|
|
8
|
+
# with tqdm progress bars during training
|
|
9
|
+
pip install "python-som[cli]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.10 or later.
|
|
13
|
+
|
|
14
|
+
## A complete example
|
|
15
|
+
|
|
16
|
+
This walks through the Iris dataset end to end. The runnable version is
|
|
17
|
+
[`examples/iris.py`](https://github.com/andremsouza/python-som/blob/master/examples/iris.py).
|
|
18
|
+
|
|
19
|
+
### Load the data
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np
|
|
23
|
+
import seaborn as sns
|
|
24
|
+
|
|
25
|
+
iris = sns.load_dataset("iris")
|
|
26
|
+
target = iris.iloc[:, -1].to_numpy()
|
|
27
|
+
features = iris.iloc[:, :-1].to_numpy()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Build the map
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import python_som
|
|
34
|
+
|
|
35
|
+
som = python_som.SOM(
|
|
36
|
+
x=20,
|
|
37
|
+
y=None, # chosen from the data by PCA
|
|
38
|
+
input_len=features.shape[1],
|
|
39
|
+
learning_rate=0.5,
|
|
40
|
+
neighborhood_radius=1.0,
|
|
41
|
+
neighborhood_function="gaussian",
|
|
42
|
+
cyclic_x=True,
|
|
43
|
+
cyclic_y=True,
|
|
44
|
+
data=features, # required when a dimension is None
|
|
45
|
+
random_seed=42,
|
|
46
|
+
)
|
|
47
|
+
print(som.get_shape())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Leaving one dimension as `None` asks the library to pick it. Kohonen Section 3.5 advises matching
|
|
51
|
+
the side lengths to *"the lengths of the two largest principal components"*; since a component is a
|
|
52
|
+
unit direction, its length in the data is the standard deviation, so the ratio is
|
|
53
|
+
$\sqrt{\lambda_1/\lambda_2}$.
|
|
54
|
+
|
|
55
|
+
### Initialize the models
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
som.weight_initialization(mode="linear", data=features)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Linear initialization is the one to prefer. Kohonen Section 4.3: *"much faster and convergence
|
|
62
|
+
follow if the initial values are selected as a regular, two-dimensional sequence of vectors taken
|
|
63
|
+
along a hyperplane spanned by the two largest principal components"*. It is also the only
|
|
64
|
+
deterministic initializer.
|
|
65
|
+
|
|
66
|
+
The alternatives are `mode="random"` (draws from a distribution) and `mode="sample"` (copies random
|
|
67
|
+
rows of your data).
|
|
68
|
+
|
|
69
|
+
### Train
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
error = som.train(features, n_iteration=len(features), mode="batch", verbose=True)
|
|
73
|
+
print(f"quantization error: {error:.4f}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See [Training modes](training-modes.md) for how the three modes differ and why batch is
|
|
77
|
+
recommended.
|
|
78
|
+
|
|
79
|
+
### Inspect the result
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
umatrix = som.distance_matrix().T # the U-matrix
|
|
83
|
+
activations = som.activation_matrix(features) # samples per node
|
|
84
|
+
labels = som.label_map(features, target) # label frequencies per node
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Plot it
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import matplotlib.pyplot as plt
|
|
91
|
+
|
|
92
|
+
plt.figure(figsize=som.get_shape())
|
|
93
|
+
plt.pcolor(umatrix, cmap="bone_r")
|
|
94
|
+
|
|
95
|
+
markers, colors = ["o", "s", "D"], ["C0", "C1", "C2"]
|
|
96
|
+
codes = {name: i for i, name in enumerate(np.unique(target))}
|
|
97
|
+
for sample, label in zip(features, target):
|
|
98
|
+
w = som.winner(sample)
|
|
99
|
+
plt.plot(
|
|
100
|
+
w[0] + 0.5,
|
|
101
|
+
w[1] + 0.5,
|
|
102
|
+
markers[codes[label]],
|
|
103
|
+
markerfacecolor="None",
|
|
104
|
+
markeredgecolor=colors[codes[label]],
|
|
105
|
+
markersize=12,
|
|
106
|
+
markeredgewidth=2,
|
|
107
|
+
)
|
|
108
|
+
plt.axis((0, som.get_shape()[0], 0, som.get_shape()[1]))
|
|
109
|
+
plt.savefig("iris.png")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+
Darker regions of the U-matrix mean neighbouring models are far apart, so they read as boundaries
|
|
115
|
+
between clusters.
|
|
116
|
+
|
|
117
|
+
## Progress output
|
|
118
|
+
|
|
119
|
+
`verbose=True` shows a tqdm progress bar when `python-som[cli]` is installed. It also logs on the
|
|
120
|
+
`python_som` logger at INFO level, which is off by default the way any library logger is; enable it
|
|
121
|
+
with:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import logging
|
|
125
|
+
|
|
126
|
+
logging.basicConfig(level=logging.INFO)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Reproducibility
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
som = python_som.SOM(x=10, y=10, input_len=4, random_seed=42)
|
|
133
|
+
print(som.get_random_seed())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Same seed, same map. The generator belongs to the instance, so building a SOM will not disturb
|
|
137
|
+
NumPy's global random state. Note that seeds do not reproduce across the 0.3.0 boundary; see the
|
|
138
|
+
warning in [Training modes](training-modes.md#reproducibility).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--8<-- "README.md"
|