python-som 0.1.2__tar.gz → 0.2.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.2 → python_som-0.2.0}/.gitignore +3 -0
- python_som-0.2.0/CHANGELOG.md +85 -0
- {python_som-0.1.2 → python_som-0.2.0}/PKG-INFO +33 -4
- {python_som-0.1.2 → python_som-0.2.0}/README.md +29 -2
- {python_som-0.1.2 → python_som-0.2.0}/pyproject.toml +8 -3
- {python_som-0.1.2 → python_som-0.2.0}/python_som/__init__.py +141 -22
- python_som-0.2.0/tests/test_neighborhood.py +290 -0
- {python_som-0.1.2 → python_som-0.2.0}/LICENSE +0 -0
- {python_som-0.1.2 → python_som-0.2.0}/requirements.txt +0 -0
- {python_som-0.1.2 → python_som-0.2.0}/test.py +0 -0
- {python_som-0.1.2 → python_som-0.2.0}/test_iris.ipynb +0 -0
- {python_som-0.1.2 → python_som-0.2.0}/test_output_iris.png +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
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.2.0] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Mexican hat neighborhood function, selectable with `neighborhood_function='mexicanhat'`
|
|
13
|
+
(contributed by [@Arne49](https://github.com/Arne49) in
|
|
14
|
+
[#2](https://github.com/andremsouza/python-som/pull/2)).
|
|
15
|
+
|
|
16
|
+
Implemented as the isotropic 2-D form `(1 - u) * exp(-u)` over `u = sqdist(c, i) / (2 * sigma^2)`,
|
|
17
|
+
normalized so that `h(c, c) = 1`. It crosses zero at a radius of `sqrt(2) * sigma` and reaches its
|
|
18
|
+
minimum of `-exp(-2)` at a radius of `2 * sigma`.
|
|
19
|
+
|
|
20
|
+
Note that a neighborhood function must depend on the grid distance alone, not on the two axis
|
|
21
|
+
offsets separately, per `sqdist(c, i)` in Kohonen (2013) Eq. (5) and the "lateral distance"
|
|
22
|
+
abscissa of Vrieze (1995) Fig. 3. The gaussian happens to be separable into a product of per-axis
|
|
23
|
+
terms, but that is a property of the exponential rather than of neighborhood functions in general.
|
|
24
|
+
|
|
25
|
+
- `## Neighborhood functions` section in the README, comparing all three and documenting the
|
|
26
|
+
batch-mode restriction below.
|
|
27
|
+
- Test suite for the neighborhood functions, asserting closed-form analytic properties rather than
|
|
28
|
+
golden values.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Cyclic (toroidal) neighborhoods were only correct in one direction.** The fold-back handled
|
|
33
|
+
offsets above `+length/2` but left those below `-length/2` alone, so a winner in the upper half of
|
|
34
|
+
an axis did not wrap. On a 10x10 toroidal map with sigma=1, a winner at row 0 gave `h[9] = 0.6065`
|
|
35
|
+
while a winner at row 9 gave `h[0] = 0.0` instead of the same 0.6065. Now uses the minimum-image
|
|
36
|
+
convention, which folds both tails.
|
|
37
|
+
|
|
38
|
+
This changes results for any map built with `cyclic_x=True` or `cyclic_y=True`.
|
|
39
|
+
|
|
40
|
+
- A neighborhood radius of zero divided by zero and produced `inf`/`nan` weights instead of raising.
|
|
41
|
+
`_gaussian` and `_mexicanhat` now reject a non-finite or non-positive radius; `_bubble` still
|
|
42
|
+
accepts a radius of zero, which selects the winner alone.
|
|
43
|
+
|
|
44
|
+
- An unknown `neighborhood_function` raised a bare `KeyError`. It now raises `ValueError` listing the
|
|
45
|
+
valid names, matching the behavior of `weight_initialization`.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **`requires-python` is now `>=3.10`.** The declared floor of `>=3.9` was never accurate: the module
|
|
50
|
+
uses PEP 604 unions (`int | None`) in runtime-evaluated annotations without
|
|
51
|
+
`from __future__ import annotations`, so importing it on Python 3.9 raises `TypeError`. Python 3.9
|
|
52
|
+
reached end of life in October 2025. Installers will now decline the package on 3.9 rather than
|
|
53
|
+
install something that cannot be imported.
|
|
54
|
+
|
|
55
|
+
- `train(mode='batch')` combined with `neighborhood_function='mexicanhat'` now raises `ValueError`.
|
|
56
|
+
The batch update of Kohonen (2013) Eq. (8) is a weighted mean whose denominator `sum_j n_j * h_ji`
|
|
57
|
+
is only well defined for a non-negative neighborhood function. The mexican hat is signed by
|
|
58
|
+
construction, so the denominator can vanish or turn negative: on a 12x12 grid, 49 of 144
|
|
59
|
+
denominators come out negative, which inverts or explodes the update. This is a property of the
|
|
60
|
+
function itself rather than of this implementation. Use `mode='random'` or `mode='sequential'`.
|
|
61
|
+
|
|
62
|
+
### Packaging
|
|
63
|
+
|
|
64
|
+
- Restored the `cli` extra (`pip install python-som[cli]`, which pulls in `tqdm` for training
|
|
65
|
+
progress bars). This extra shipped in 0.1.3 on PyPI but was never committed to the repository, so
|
|
66
|
+
releasing from the repository as-is would have silently removed it.
|
|
67
|
+
|
|
68
|
+
### Note on 0.1.3
|
|
69
|
+
|
|
70
|
+
There is no changelog entry for 0.1.3 because it was published to PyPI from an uncommitted working
|
|
71
|
+
tree and has no corresponding commit. Its `python_som/__init__.py` is byte-identical to the 0.1.2
|
|
72
|
+
tag; the only differences are the version string and the `cli` extra restored above.
|
|
73
|
+
|
|
74
|
+
## [0.1.3] - 2025-01-14
|
|
75
|
+
|
|
76
|
+
Published to PyPI only. See the note above.
|
|
77
|
+
|
|
78
|
+
## [0.1.2] - 2025-01-14
|
|
79
|
+
|
|
80
|
+
Earlier releases predate this changelog. See the
|
|
81
|
+
[commit history](https://github.com/andremsouza/python-som/commits/master) for details.
|
|
82
|
+
|
|
83
|
+
[0.2.0]: https://github.com/andremsouza/python-som/releases/tag/v0.2.0
|
|
84
|
+
[0.1.3]: https://pypi.org/project/python-som/0.1.3/
|
|
85
|
+
[0.1.2]: https://pypi.org/project/python-som/0.1.2/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-som
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python implementation of the Self-Organizing Map
|
|
5
5
|
Project-URL: Homepage, https://github.com/andremsouza/python-som
|
|
6
6
|
Project-URL: Issues, https://github.com/andremsouza/python-som/issues
|
|
@@ -16,10 +16,12 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: numpy
|
|
21
21
|
Requires-Dist: pandas
|
|
22
22
|
Requires-Dist: scikit-learn
|
|
23
|
+
Provides-Extra: cli
|
|
24
|
+
Requires-Dist: tqdm; extra == 'cli'
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
24
26
|
|
|
25
27
|
# python-som
|
|
@@ -35,7 +37,7 @@ Most features were implemented using NumPy, with Scikit-learn for standardizatio
|
|
|
35
37
|
* Linear weight initialization (with PCA)
|
|
36
38
|
* Automatic selection of map size ratio (with PCA)
|
|
37
39
|
* Support for cyclic arrays, for toroidal or spherical maps
|
|
38
|
-
* Gaussian and
|
|
40
|
+
* Gaussian, Bubble and Mexican hat neighborhood functions
|
|
39
41
|
* Support for custom decay functions
|
|
40
42
|
* Support for visualization (U-matrix, activation matrix)
|
|
41
43
|
* Support for supervised learning (label map)
|
|
@@ -132,6 +134,22 @@ The following are lists of public methods and functions currently available in t
|
|
|
132
134
|
* SOM.train
|
|
133
135
|
* SOM.weight_initialization
|
|
134
136
|
|
|
137
|
+
## Neighborhood functions
|
|
138
|
+
The `neighborhood_function` parameter selects how the winner's correction is spread over the grid.
|
|
139
|
+
All three are functions of the distance between two nodes in the grid, `sqdist(c, i)` in Eq. (5) of
|
|
140
|
+
Kohonen (2013):
|
|
141
|
+
|
|
142
|
+
| Name | Shape | Notes |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `'gaussian'` | `exp(-r² / 2σ²)` | Strictly positive and monotonically decreasing. The default. |
|
|
145
|
+
| `'bubble'` | `1` for `r ≤ σ`, else `0` | The truncated inner lobe of the mexican hat. Vrieze (1995) notes this flat choice is "just as effective and sometimes even better". |
|
|
146
|
+
| `'mexicanhat'` | `(1 - u)·exp(-u)`, `u = r² / 2σ²` | Excitatory near the winner, inhibitory beyond it. Crosses zero at `r = √2·σ`, reaches its minimum of `-e⁻² ≈ -0.135` at `r = 2σ`. |
|
|
147
|
+
|
|
148
|
+
The mexican hat takes negative values, so it **cannot be used with `mode='batch'`**: the batch update
|
|
149
|
+
of Kohonen (2013), Eq. (8), is a weighted mean whose denominator `Σⱼ nⱼ·hⱼᵢ` is not sign-definite for
|
|
150
|
+
a signed neighborhood function. Use `mode='random'` or `mode='sequential'` instead; passing
|
|
151
|
+
`mode='batch'` raises a `ValueError`.
|
|
152
|
+
|
|
135
153
|
## References
|
|
136
154
|
This implemetation was based on the following paper, by Professor Teuvo Kohonen:
|
|
137
155
|
|
|
@@ -142,4 +160,15 @@ Volume 37,
|
|
|
142
160
|
2013,
|
|
143
161
|
Pages 52-65,
|
|
144
162
|
ISSN 0893-6080,
|
|
145
|
-
https://doi.org/10.1016/j.neunet.2012.09.018.
|
|
163
|
+
https://doi.org/10.1016/j.neunet.2012.09.018.
|
|
164
|
+
|
|
165
|
+
The mexican hat neighborhood function follows the lateral-interaction formulation in:
|
|
166
|
+
|
|
167
|
+
O. J. Vrieze,
|
|
168
|
+
Kohonen network,
|
|
169
|
+
in: Artificial Neural Networks: An Introduction to ANN Theory and Practice,
|
|
170
|
+
Lecture Notes in Computer Science, Volume 931,
|
|
171
|
+
Springer, Berlin, Heidelberg,
|
|
172
|
+
1995,
|
|
173
|
+
Pages 83-100,
|
|
174
|
+
https://doi.org/10.1007/BFb0027024.
|
|
@@ -11,7 +11,7 @@ Most features were implemented using NumPy, with Scikit-learn for standardizatio
|
|
|
11
11
|
* Linear weight initialization (with PCA)
|
|
12
12
|
* Automatic selection of map size ratio (with PCA)
|
|
13
13
|
* Support for cyclic arrays, for toroidal or spherical maps
|
|
14
|
-
* Gaussian and
|
|
14
|
+
* Gaussian, Bubble and Mexican hat neighborhood functions
|
|
15
15
|
* Support for custom decay functions
|
|
16
16
|
* Support for visualization (U-matrix, activation matrix)
|
|
17
17
|
* Support for supervised learning (label map)
|
|
@@ -108,6 +108,22 @@ The following are lists of public methods and functions currently available in t
|
|
|
108
108
|
* SOM.train
|
|
109
109
|
* SOM.weight_initialization
|
|
110
110
|
|
|
111
|
+
## Neighborhood functions
|
|
112
|
+
The `neighborhood_function` parameter selects how the winner's correction is spread over the grid.
|
|
113
|
+
All three are functions of the distance between two nodes in the grid, `sqdist(c, i)` in Eq. (5) of
|
|
114
|
+
Kohonen (2013):
|
|
115
|
+
|
|
116
|
+
| Name | Shape | Notes |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `'gaussian'` | `exp(-r² / 2σ²)` | Strictly positive and monotonically decreasing. The default. |
|
|
119
|
+
| `'bubble'` | `1` for `r ≤ σ`, else `0` | The truncated inner lobe of the mexican hat. Vrieze (1995) notes this flat choice is "just as effective and sometimes even better". |
|
|
120
|
+
| `'mexicanhat'` | `(1 - u)·exp(-u)`, `u = r² / 2σ²` | Excitatory near the winner, inhibitory beyond it. Crosses zero at `r = √2·σ`, reaches its minimum of `-e⁻² ≈ -0.135` at `r = 2σ`. |
|
|
121
|
+
|
|
122
|
+
The mexican hat takes negative values, so it **cannot be used with `mode='batch'`**: the batch update
|
|
123
|
+
of Kohonen (2013), Eq. (8), is a weighted mean whose denominator `Σⱼ nⱼ·hⱼᵢ` is not sign-definite for
|
|
124
|
+
a signed neighborhood function. Use `mode='random'` or `mode='sequential'` instead; passing
|
|
125
|
+
`mode='batch'` raises a `ValueError`.
|
|
126
|
+
|
|
111
127
|
## References
|
|
112
128
|
This implemetation was based on the following paper, by Professor Teuvo Kohonen:
|
|
113
129
|
|
|
@@ -118,4 +134,15 @@ Volume 37,
|
|
|
118
134
|
2013,
|
|
119
135
|
Pages 52-65,
|
|
120
136
|
ISSN 0893-6080,
|
|
121
|
-
https://doi.org/10.1016/j.neunet.2012.09.018.
|
|
137
|
+
https://doi.org/10.1016/j.neunet.2012.09.018.
|
|
138
|
+
|
|
139
|
+
The mexican hat neighborhood function follows the lateral-interaction formulation in:
|
|
140
|
+
|
|
141
|
+
O. J. Vrieze,
|
|
142
|
+
Kohonen network,
|
|
143
|
+
in: Artificial Neural Networks: An Introduction to ANN Theory and Practice,
|
|
144
|
+
Lecture Notes in Computer Science, Volume 931,
|
|
145
|
+
Springer, Berlin, Heidelberg,
|
|
146
|
+
1995,
|
|
147
|
+
Pages 83-100,
|
|
148
|
+
https://doi.org/10.1007/BFb0027024.
|
|
@@ -3,13 +3,13 @@ requires = ["hatchling"]
|
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
[project]
|
|
5
5
|
name = "python-som"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.2.0"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name="André Moreira Souza", email="msouza.andre@hotmail.com" },
|
|
9
9
|
]
|
|
10
10
|
description = "Python implementation of the Self-Organizing Map"
|
|
11
11
|
readme = "README.md"
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 3 - Alpha",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
@@ -29,6 +29,11 @@ dependencies = [
|
|
|
29
29
|
"scikit-learn",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
cli = [
|
|
34
|
+
"tqdm",
|
|
35
|
+
]
|
|
36
|
+
|
|
32
37
|
[project.urls]
|
|
33
38
|
Homepage = "https://github.com/andremsouza/python-som"
|
|
34
|
-
Issues = "https://github.com/andremsouza/python-som/issues"
|
|
39
|
+
Issues = "https://github.com/andremsouza/python-som/issues"
|
|
@@ -9,7 +9,7 @@ Features:
|
|
|
9
9
|
- Linear weight initialization (with PCA)
|
|
10
10
|
- Automatic selection of map size ratio (with PCA)
|
|
11
11
|
- Support for cyclic arrays, for toroidal or spherical maps
|
|
12
|
-
- Gaussian and
|
|
12
|
+
- Gaussian, Bubble and Mexican Hat neighborhood functions
|
|
13
13
|
- Support for custom decay functions
|
|
14
14
|
- Support for visualization (U-matrix, activation matrix)
|
|
15
15
|
- Support for supervised learning (label map)
|
|
@@ -124,7 +124,7 @@ class SOM:
|
|
|
124
124
|
- Linear weight initialization (with PCA)
|
|
125
125
|
- Automatic selection of map size ratio (with PCA)
|
|
126
126
|
- Support for cyclic arrays, for toroidal or spherical maps
|
|
127
|
-
- Gaussian and
|
|
127
|
+
- Gaussian, Bubble and Mexican hat neighborhood functions
|
|
128
128
|
- Support for custom decay functions
|
|
129
129
|
- Support for visualization (U-matrix, activation matrix)
|
|
130
130
|
- Support for supervised learning (label map)
|
|
@@ -190,7 +190,7 @@ class SOM:
|
|
|
190
190
|
variable. May be a predefined one from this package, or a custom function, with the
|
|
191
191
|
same parameters and return type. Defaults to _asymptotic_decay
|
|
192
192
|
:param neighborhood_function: str: Neighborhood function name for the training process.
|
|
193
|
-
May be either 'gaussian' or '
|
|
193
|
+
May be either 'gaussian', 'bubble' or 'mexicanhat'.
|
|
194
194
|
:param distance_function: function: Function for calculating distances/dissimilarities
|
|
195
195
|
between models of the network.
|
|
196
196
|
May be a predefined one from this package, or a custom function, with the same
|
|
@@ -242,10 +242,19 @@ class SOM:
|
|
|
242
242
|
self._learning_rate_decay = learning_rate_decay
|
|
243
243
|
self._neighborhood_radius = float(neighborhood_radius)
|
|
244
244
|
self._neighborhood_radius_decay = neighborhood_radius_decay
|
|
245
|
-
|
|
245
|
+
neighborhood_functions = {
|
|
246
246
|
"gaussian": self._gaussian,
|
|
247
247
|
"bubble": self._bubble,
|
|
248
|
-
|
|
248
|
+
"mexicanhat": self._mexicanhat,
|
|
249
|
+
}
|
|
250
|
+
try:
|
|
251
|
+
self._neighborhood_function = neighborhood_functions[neighborhood_function]
|
|
252
|
+
except KeyError as exc:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"Invalid value for 'neighborhood_function' parameter. Value should be in "
|
|
255
|
+
+ str(list(neighborhood_functions.keys()))
|
|
256
|
+
) from exc
|
|
257
|
+
self._neighborhood_function_name = neighborhood_function
|
|
249
258
|
self._distance_function = distance_function
|
|
250
259
|
self._cyclic = (bool(cyclic_x), bool(cyclic_y))
|
|
251
260
|
self._neigx, self._neigy = np.arange(self._shape[0]), np.arange(self._shape[1])
|
|
@@ -484,6 +493,18 @@ class SOM:
|
|
|
484
493
|
elif mode == "sequential":
|
|
485
494
|
self._train_sequential(data_array, n_iteration, verbose)
|
|
486
495
|
elif mode == "batch":
|
|
496
|
+
if self._neighborhood_function_name == "mexicanhat":
|
|
497
|
+
# The batch update of Kohonen (2013), Eq. (8), is a weighted mean whose denominator
|
|
498
|
+
# is sum_j n_j * h_ji. That is only well defined for a non-negative neighborhood
|
|
499
|
+
# function. The mexican hat is signed by construction, so the denominator can vanish
|
|
500
|
+
# or turn negative, which makes the update blow up or invert the sign of the
|
|
501
|
+
# correction. Use a stepwise mode instead.
|
|
502
|
+
raise ValueError(
|
|
503
|
+
"The 'mexicanhat' neighborhood function cannot be used with the 'batch'"
|
|
504
|
+
" training mode: the weighted mean of Kohonen (2013), Eq. (8), is undefined"
|
|
505
|
+
" for a neighborhood function that takes negative values, as its denominator"
|
|
506
|
+
" is not sign-definite. Use mode='random' or mode='sequential' instead."
|
|
507
|
+
)
|
|
487
508
|
self._train_batch(data_array, n_iteration, verbose)
|
|
488
509
|
else:
|
|
489
510
|
# Invalid training mode value
|
|
@@ -641,7 +662,7 @@ class SOM:
|
|
|
641
662
|
def weight_initialization(
|
|
642
663
|
self,
|
|
643
664
|
mode: str = "random",
|
|
644
|
-
**kwargs: np.ndarray | pd.DataFrame | list | str | int
|
|
665
|
+
**kwargs: np.ndarray | pd.DataFrame | list | str | int,
|
|
645
666
|
) -> None:
|
|
646
667
|
"""Function for weight initialization of the self-organizing map.
|
|
647
668
|
|
|
@@ -758,28 +779,70 @@ class SOM:
|
|
|
758
779
|
)
|
|
759
780
|
self._weights = data_array[sample].reshape(self._weights.shape)
|
|
760
781
|
|
|
782
|
+
@staticmethod
|
|
783
|
+
def _axis_offsets(
|
|
784
|
+
coords: np.ndarray, center: int, length: float, cyclic: bool
|
|
785
|
+
) -> np.ndarray:
|
|
786
|
+
"""
|
|
787
|
+
Signed offsets from 'center' to each coordinate along one axis of the grid.
|
|
788
|
+
|
|
789
|
+
When the axis is cyclic, the minimum-image convention is applied, i.e., each offset is
|
|
790
|
+
folded into the interval [-length/2, length/2), so that the shortest way around the torus
|
|
791
|
+
is used. Both tails must be folded: an offset of -9 on an axis of length 10 represents a
|
|
792
|
+
distance of 1, not 9.
|
|
793
|
+
|
|
794
|
+
:param coords: np.ndarray: Coordinates along the axis, i.e., np.arange(length).
|
|
795
|
+
:param center: int: Coordinate of the center (winner node) along the axis.
|
|
796
|
+
:param length: float: Number of nodes along the axis.
|
|
797
|
+
:param cyclic: bool: Whether the axis wraps around.
|
|
798
|
+
:return: np.ndarray: Signed offsets from center to each coordinate.
|
|
799
|
+
"""
|
|
800
|
+
d = coords - center
|
|
801
|
+
if cyclic:
|
|
802
|
+
d = (d + length / 2) % length - length / 2
|
|
803
|
+
return d
|
|
804
|
+
|
|
805
|
+
def _sqdist(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
|
|
806
|
+
"""
|
|
807
|
+
Squared geometric distance from the center c to every node of the grid.
|
|
808
|
+
|
|
809
|
+
This is the sqdist(c, i) term of Kohonen (2013), Eq. (5). Every neighborhood function must
|
|
810
|
+
be a function of this scalar quantity: the neighborhood describes lateral interaction over
|
|
811
|
+
a metric space, so it depends on the distance between two nodes and not on the individual
|
|
812
|
+
axis offsets. Only the gaussian is separable into a product of per-axis terms, and that is
|
|
813
|
+
a property of the exponential rather than of neighborhood functions in general.
|
|
814
|
+
|
|
815
|
+
:param c: (int, int): Center coordinates.
|
|
816
|
+
:param sigma: float: Spread variable, validated here for all neighborhood functions.
|
|
817
|
+
:return: np.ndarray: Squared distances from c to each node, with the shape of the network.
|
|
818
|
+
:raises ValueError: If sigma is not strictly positive.
|
|
819
|
+
"""
|
|
820
|
+
if not np.isfinite(sigma) or sigma <= 0:
|
|
821
|
+
raise ValueError(
|
|
822
|
+
"The neighborhood radius 'sigma' must be a finite positive number, got "
|
|
823
|
+
+ str(sigma)
|
|
824
|
+
)
|
|
825
|
+
dx = self._axis_offsets(
|
|
826
|
+
self._neigx, c[0], float(self._shape[0]), self._cyclic[0]
|
|
827
|
+
)
|
|
828
|
+
dy = self._axis_offsets(
|
|
829
|
+
self._neigy, c[1], float(self._shape[1]), self._cyclic[1]
|
|
830
|
+
)
|
|
831
|
+
return np.add.outer(np.power(dx, 2), np.power(dy, 2))
|
|
832
|
+
|
|
761
833
|
def _gaussian(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
|
|
762
834
|
"""
|
|
763
835
|
Gaussian neighborhood function, centered in c. Has support for cyclic arrays.
|
|
764
836
|
|
|
837
|
+
Implements h_ci = exp(-sqdist(c, i) / (2 * sigma^2)), i.e., Eq. (5) of Kohonen (2013)
|
|
838
|
+
with the learning rate factored out, so that h_cc = 1.
|
|
839
|
+
|
|
765
840
|
:param c: (int, int): Center coordinates for gaussian function.
|
|
766
|
-
:param sigma: float: Spread variable for gaussian function.
|
|
841
|
+
:param sigma: float: Spread variable for gaussian function. Must be positive.
|
|
767
842
|
:return: np.ndarray: Gaussian, centered in c, over all the weights of the network.
|
|
843
|
+
:raises ValueError: If sigma is not strictly positive.
|
|
768
844
|
"""
|
|
769
|
-
|
|
770
|
-
d = 2 * sigma * sigma
|
|
771
|
-
# Calculate vertical and horizontal distances
|
|
772
|
-
dx = self._neigx - c[0]
|
|
773
|
-
dy = self._neigy - c[1]
|
|
774
|
-
# If using cyclic arrays, perform fold back distance
|
|
775
|
-
if self._cyclic[0]:
|
|
776
|
-
dx[dx > self._shape[0] / 2] -= self._shape[0]
|
|
777
|
-
if self._cyclic[1]:
|
|
778
|
-
dy[dy > self._shape[1] / 2] -= self._shape[1]
|
|
779
|
-
# Calculate gaussian centered in c
|
|
780
|
-
ax = np.exp(-np.power(dx, 2) / d)
|
|
781
|
-
ay = np.exp(-np.power(dy, 2) / d)
|
|
782
|
-
return np.outer(ax, ay)
|
|
845
|
+
return np.exp(-self._sqdist(c, sigma) / (2 * sigma * sigma))
|
|
783
846
|
|
|
784
847
|
def _bubble(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
|
|
785
848
|
"""
|
|
@@ -788,10 +851,26 @@ class SOM:
|
|
|
788
851
|
The neighbors of c are the nodes in the region of sigma positions in the vertical
|
|
789
852
|
and horizontal directions around c.
|
|
790
853
|
|
|
854
|
+
This is the truncated inner, excitatory lobe of the mexican hat lateral-interaction
|
|
855
|
+
function; cf. Vrieze (1995), p. 85: "In Kohonen networks usually only the inner stimulation
|
|
856
|
+
area is used, i.e., when a neuron i fires, a positive feedback takes place for all neurons
|
|
857
|
+
i', whose distance to i is smaller than some given number rho."
|
|
858
|
+
|
|
859
|
+
Unlike the other neighborhood functions, a radius of zero is admissible here: it selects
|
|
860
|
+
the winner alone, which is well defined, whereas for the gaussian and the mexican hat a
|
|
861
|
+
zero radius is a division by zero.
|
|
862
|
+
|
|
791
863
|
:param c: (int, int): Center coordinates for gaussian function.
|
|
792
|
-
:param sigma: float: Spread variable for gaussian function.
|
|
864
|
+
:param sigma: float: Spread variable for gaussian function. Must be finite and
|
|
865
|
+
non-negative.
|
|
793
866
|
:return: np.ndarray: Neighborhood matrix, centered in c.
|
|
867
|
+
:raises ValueError: If sigma is negative or not finite.
|
|
794
868
|
"""
|
|
869
|
+
if not np.isfinite(sigma) or sigma < 0:
|
|
870
|
+
raise ValueError(
|
|
871
|
+
"The neighborhood radius 'sigma' must be a finite non-negative number, got "
|
|
872
|
+
+ str(sigma)
|
|
873
|
+
)
|
|
795
874
|
# Convert sigma to integer
|
|
796
875
|
sigma = int(np.around(sigma))
|
|
797
876
|
|
|
@@ -813,6 +892,46 @@ class SOM:
|
|
|
813
892
|
|
|
814
893
|
return np.outer(ax, ay).astype(int)
|
|
815
894
|
|
|
895
|
+
def _mexicanhat(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
|
|
896
|
+
"""
|
|
897
|
+
Mexican Hat neighborhood function, centered in c. Has support for cyclic arrays.
|
|
898
|
+
|
|
899
|
+
Also known as the Ricker wavelet, or the Laplacian of Gaussian. This is the biologically
|
|
900
|
+
motivated lateral-interaction function: nodes near the winner are excited, nodes beyond a
|
|
901
|
+
certain distance are inhibited, and the inhibition vanishes as the distance grows further
|
|
902
|
+
(Vrieze, 1995, Fig. 3).
|
|
903
|
+
|
|
904
|
+
Implements the isotropic 2-D form as a function of u = sqdist(c, i) / (2 * sigma^2):
|
|
905
|
+
|
|
906
|
+
h_ci = (1 - u) * exp(-u)
|
|
907
|
+
|
|
908
|
+
normalized so that h_cc = 1. It crosses zero at a radius of sqrt(2) * sigma, reaches its
|
|
909
|
+
minimum of -exp(-2) ~= -0.135 at a radius of 2 * sigma, and decays to zero thereafter.
|
|
910
|
+
|
|
911
|
+
Note that this is *not* the outer product of two 1-D Ricker wavelets. Unlike the gaussian,
|
|
912
|
+
the Ricker wavelet is not multiplicatively separable, and such a product is not a function
|
|
913
|
+
of the distance between two nodes: it would be positive in the diagonal quadrants, where
|
|
914
|
+
both 1-D factors are negative, placing a spurious excitatory lobe exactly where the
|
|
915
|
+
function must inhibit. The neighborhood function must depend on the grid distance alone,
|
|
916
|
+
cf. sqdist(c, i) in Kohonen (2013), Eq. (5), and the "lateral distance" abscissa of
|
|
917
|
+
Vrieze (1995), Fig. 3.
|
|
918
|
+
|
|
919
|
+
The '_bubble' neighborhood function is the truncated inner (excitatory) lobe of this same
|
|
920
|
+
lateral-interaction function; cf. Vrieze (1995), p. 85.
|
|
921
|
+
|
|
922
|
+
:param c: (int, int): Center coordinates for mexican hat function.
|
|
923
|
+
:param sigma: float: Spread variable for mexican hat function. Must be positive.
|
|
924
|
+
:return: np.ndarray: Mexican Hat, centered in c, over all the weights of the network.
|
|
925
|
+
:raises ValueError: If sigma is not strictly positive.
|
|
926
|
+
|
|
927
|
+
References:
|
|
928
|
+
O. J. Vrieze, Kohonen network, in: Artificial Neural Networks: An Introduction to ANN
|
|
929
|
+
Theory and Practice, Lecture Notes in Computer Science, vol. 931, Springer, 1995,
|
|
930
|
+
pp. 83-100, https://doi.org/10.1007/BFb0027024.
|
|
931
|
+
"""
|
|
932
|
+
u = self._sqdist(c, sigma) / (2 * sigma * sigma)
|
|
933
|
+
return (1.0 - u) * np.exp(-u)
|
|
934
|
+
|
|
816
935
|
def _data_to_numpy(self, data: np.ndarray | pd.DataFrame | list) -> np.ndarray:
|
|
817
936
|
"""
|
|
818
937
|
Converts data to numpy array.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Analytic property tests for the neighborhood functions of the self-organizing map.
|
|
2
|
+
|
|
3
|
+
These assert closed-form properties rather than golden values, so they remain meaningful if the
|
|
4
|
+
implementation is rewritten. The central property is *isotropy*: a neighborhood function models
|
|
5
|
+
lateral interaction over the grid, so it must be a function of the distance between two nodes and
|
|
6
|
+
nothing else.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
Teuvo Kohonen, Essentials of the self-organizing map, Neural Networks 37 (2013) 52-65,
|
|
10
|
+
https://doi.org/10.1016/j.neunet.2012.09.018 -- Eq. (5), the neighborhood as a function of
|
|
11
|
+
sqdist(c, i).
|
|
12
|
+
|
|
13
|
+
O. J. Vrieze, Kohonen network, in: Artificial Neural Networks: An Introduction to ANN Theory and
|
|
14
|
+
Practice, LNCS 931, Springer, 1995, pp. 83-100, https://doi.org/10.1007/BFb0027024 -- Fig. 3, the
|
|
15
|
+
"Mexican-hat" function of lateral interaction plotted against a single "lateral distance" axis.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
import python_som
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_som(
|
|
25
|
+
x=21, y=21, cyclic_x=False, cyclic_y=False, neighborhood_function="gaussian"
|
|
26
|
+
):
|
|
27
|
+
"""Builds a SOM with a fixed seed; only the grid geometry matters for these tests."""
|
|
28
|
+
return python_som.SOM(
|
|
29
|
+
x=x,
|
|
30
|
+
y=y,
|
|
31
|
+
input_len=3,
|
|
32
|
+
neighborhood_function=neighborhood_function,
|
|
33
|
+
cyclic_x=cyclic_x,
|
|
34
|
+
cyclic_y=cyclic_y,
|
|
35
|
+
random_seed=0,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def grid_radius(shape, c):
|
|
40
|
+
"""Euclidean distance from c to every node of a non-cyclic grid."""
|
|
41
|
+
dx = np.arange(shape[0]) - c[0]
|
|
42
|
+
dy = np.arange(shape[1]) - c[1]
|
|
43
|
+
return np.sqrt(np.add.outer(dx**2, dy**2))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --------------------------------------------------------------------------------------------
|
|
47
|
+
# Properties shared by every neighborhood function
|
|
48
|
+
# --------------------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize("name", ["gaussian", "bubble", "mexicanhat"])
|
|
52
|
+
def test_shape_matches_the_grid(name):
|
|
53
|
+
som = make_som(neighborhood_function=name)
|
|
54
|
+
assert som._neighborhood_function((10, 10), 3.0).shape == (21, 21)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("name", ["gaussian", "bubble", "mexicanhat"])
|
|
58
|
+
def test_winner_has_maximum_response(name):
|
|
59
|
+
"""The winner is maximally excited: h(c, c) is the global maximum."""
|
|
60
|
+
som = make_som(neighborhood_function=name)
|
|
61
|
+
h = som._neighborhood_function((10, 10), 3.0)
|
|
62
|
+
assert h[10, 10] == pytest.approx(h.max())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("name", ["gaussian", "mexicanhat"])
|
|
66
|
+
def test_normalized_to_unity_at_the_winner(name):
|
|
67
|
+
som = make_som(neighborhood_function=name)
|
|
68
|
+
assert som._neighborhood_function((10, 10), 3.0)[10, 10] == pytest.approx(1.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("name", ["gaussian", "bubble", "mexicanhat"])
|
|
72
|
+
@pytest.mark.parametrize("sigma", [-1.0, -0.5, float("nan"), float("inf")])
|
|
73
|
+
def test_negative_or_non_finite_sigma_is_rejected(name, sigma):
|
|
74
|
+
"""A negative or non-finite radius is meaningless for any neighborhood function."""
|
|
75
|
+
som = make_som(neighborhood_function=name)
|
|
76
|
+
with pytest.raises(ValueError, match="sigma"):
|
|
77
|
+
som._neighborhood_function((10, 10), sigma)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parametrize("name", ["gaussian", "mexicanhat"])
|
|
81
|
+
def test_zero_sigma_is_rejected_where_it_divides(name):
|
|
82
|
+
"""sigma appears in a denominator, so zero must raise rather than yield inf/nan."""
|
|
83
|
+
som = make_som(neighborhood_function=name)
|
|
84
|
+
with pytest.raises(ValueError, match="sigma"):
|
|
85
|
+
som._neighborhood_function((10, 10), 0.0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_bubble_admits_a_zero_radius():
|
|
89
|
+
"""For the bubble a zero radius is well defined: it selects the winner alone."""
|
|
90
|
+
som = make_som(neighborhood_function="bubble")
|
|
91
|
+
h = som._bubble((10, 10), 0.0)
|
|
92
|
+
assert h[10, 10] == 1
|
|
93
|
+
assert h.sum() == 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.parametrize("name", ["gaussian", "bubble", "mexicanhat"])
|
|
97
|
+
def test_four_fold_symmetry_about_the_winner(name):
|
|
98
|
+
"""h is symmetric under reflection in both axes when the winner is centred."""
|
|
99
|
+
som = make_som(neighborhood_function=name)
|
|
100
|
+
h = som._neighborhood_function((10, 10), 3.0)
|
|
101
|
+
np.testing.assert_allclose(h, h[::-1, :])
|
|
102
|
+
np.testing.assert_allclose(h, h[:, ::-1])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.parametrize("name", ["gaussian", "bubble", "mexicanhat"])
|
|
106
|
+
def test_is_isotropic(name):
|
|
107
|
+
"""h depends only on the grid distance -- the property a separable product violates.
|
|
108
|
+
|
|
109
|
+
Kohonen (2013) Eq. (5) defines the neighborhood over sqdist(c, i); Vrieze (1995) Fig. 3 plots
|
|
110
|
+
it against a single "lateral distance" axis. Nodes equidistant from the winner must therefore
|
|
111
|
+
receive equal responses. An outer product of two 1-D profiles fails this for every profile
|
|
112
|
+
except the gaussian, which is separable only by accident of the exponential.
|
|
113
|
+
"""
|
|
114
|
+
som = make_som(neighborhood_function=name)
|
|
115
|
+
h = som._neighborhood_function((10, 10), 3.0)
|
|
116
|
+
r = np.round(grid_radius((21, 21), (10, 10)), 9)
|
|
117
|
+
for radius in np.unique(r):
|
|
118
|
+
responses = h[r == radius]
|
|
119
|
+
np.testing.assert_allclose(responses, responses[0], atol=1e-12)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------------------------
|
|
123
|
+
# Gaussian
|
|
124
|
+
# --------------------------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_gaussian_matches_its_closed_form():
|
|
128
|
+
som = make_som()
|
|
129
|
+
sigma = 3.0
|
|
130
|
+
h = som._gaussian((10, 10), sigma)
|
|
131
|
+
expected = np.exp(-(grid_radius((21, 21), (10, 10)) ** 2) / (2 * sigma**2))
|
|
132
|
+
np.testing.assert_allclose(h, expected)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_gaussian_is_strictly_positive_and_decreasing():
|
|
136
|
+
som = make_som()
|
|
137
|
+
h = som._gaussian((10, 10), 3.0)
|
|
138
|
+
assert (h > 0).all()
|
|
139
|
+
centre_row = h[10, 10:]
|
|
140
|
+
assert (np.diff(centre_row) < 0).all()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# --------------------------------------------------------------------------------------------
|
|
144
|
+
# Mexican hat -- the properties that define the shape
|
|
145
|
+
# --------------------------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_mexican_hat_crosses_zero_at_sqrt2_sigma():
|
|
149
|
+
"""(1 - u) exp(-u) with u = r^2/(2 sigma^2) vanishes at u = 1, i.e. r = sqrt(2) sigma."""
|
|
150
|
+
som = make_som(x=41, y=41)
|
|
151
|
+
sigma = 4.0
|
|
152
|
+
h = som._mexicanhat((20, 20), sigma)
|
|
153
|
+
r = grid_radius((41, 41), (20, 20))
|
|
154
|
+
inside = r < np.sqrt(2) * sigma - 1e-9
|
|
155
|
+
outside_but_near = (r > np.sqrt(2) * sigma + 1e-9) & (r < 4 * sigma)
|
|
156
|
+
assert (h[inside] > 0).all()
|
|
157
|
+
assert (h[outside_but_near] < 0).all()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_mexican_hat_minimum_is_minus_exp_minus_two_at_two_sigma():
|
|
161
|
+
"""d/du [(1-u) e^-u] = 0 at u = 2, giving h = -e^-2 at r = 2 sigma."""
|
|
162
|
+
som = make_som(x=41, y=41)
|
|
163
|
+
sigma = 4.0
|
|
164
|
+
h = som._mexicanhat((20, 20), sigma)
|
|
165
|
+
r = grid_radius((41, 41), (20, 20))
|
|
166
|
+
assert h.min() == pytest.approx(-np.exp(-2.0), rel=1e-9)
|
|
167
|
+
assert r.flat[h.argmin()] == pytest.approx(2 * sigma, rel=1e-9)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_mexican_hat_is_inhibitory_on_the_diagonal():
|
|
171
|
+
"""Regression for the separable outer product.
|
|
172
|
+
|
|
173
|
+
An outer product of two 1-D Ricker wavelets is positive wherever both factors are negative,
|
|
174
|
+
putting a spurious excitatory lobe on the diagonals. Measured on this exact grid, that
|
|
175
|
+
construction gives +0.165 at (c+2s, c+2s) where the isotropic form gives -0.055.
|
|
176
|
+
"""
|
|
177
|
+
som = make_som()
|
|
178
|
+
sigma = 3.0
|
|
179
|
+
h = som._mexicanhat((10, 10), sigma)
|
|
180
|
+
assert h[16, 16] < 0
|
|
181
|
+
assert h[16, 16] == pytest.approx(-0.054946916666, rel=1e-9)
|
|
182
|
+
assert h[19, 19] < 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_mexican_hat_has_no_positive_lobe_beyond_the_zero_crossing():
|
|
186
|
+
"""Excitation is confined to the inner disc; everything beyond it inhibits or vanishes."""
|
|
187
|
+
som = make_som(x=41, y=41)
|
|
188
|
+
sigma = 4.0
|
|
189
|
+
h = som._mexicanhat((20, 20), sigma)
|
|
190
|
+
r = grid_radius((41, 41), (20, 20))
|
|
191
|
+
assert h[r > np.sqrt(2) * sigma + 1e-9].max() <= 0.0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_mexican_hat_differs_from_the_separable_product():
|
|
195
|
+
"""Guards against a regression to np.outer of two 1-D wavelets."""
|
|
196
|
+
som = make_som()
|
|
197
|
+
sigma = 3.0
|
|
198
|
+
dx = np.arange(21) - 10
|
|
199
|
+
ax = (1 - dx**2 / sigma**2) * np.exp(-(dx**2) / (2 * sigma**2))
|
|
200
|
+
separable = np.outer(ax, ax)
|
|
201
|
+
h = som._mexicanhat((10, 10), sigma)
|
|
202
|
+
assert not np.allclose(h, separable)
|
|
203
|
+
# The decisive difference: on the diagonal the separable product is excitatory where the
|
|
204
|
+
# isotropic form inhibits, because there both of its 1-D factors are negative.
|
|
205
|
+
assert separable[16, 16] > 0 > h[16, 16]
|
|
206
|
+
assert separable[16, 16] == pytest.approx(0.164840749998, rel=1e-9)
|
|
207
|
+
# The separable product is also anisotropic: (10, 14) and (13, 13) are not equidistant
|
|
208
|
+
# in its value despite both lying at grid radius 4 and 3*sqrt(2) respectively.
|
|
209
|
+
assert separable[10, 6] != pytest.approx(separable[13, 13])
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# --------------------------------------------------------------------------------------------
|
|
213
|
+
# Bubble
|
|
214
|
+
# --------------------------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_bubble_is_the_indicator_of_a_neighbourhood_set():
|
|
218
|
+
"""Vrieze (1995) p. 85: N_i = {i' : d(i, i') <= rho}, the truncated inner lobe."""
|
|
219
|
+
som = make_som()
|
|
220
|
+
h = som._bubble((10, 10), 2.0)
|
|
221
|
+
assert set(np.unique(h)) <= {0, 1}
|
|
222
|
+
expected = np.zeros((21, 21), dtype=int)
|
|
223
|
+
expected[8:13, 8:13] = 1
|
|
224
|
+
np.testing.assert_array_equal(h, expected)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# --------------------------------------------------------------------------------------------
|
|
228
|
+
# Cyclic (toroidal) grids
|
|
229
|
+
# --------------------------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@pytest.mark.parametrize("name", ["gaussian", "mexicanhat"])
|
|
233
|
+
def test_cyclic_wraps_from_both_edges(name):
|
|
234
|
+
"""Regression for the one-sided fold-back.
|
|
235
|
+
|
|
236
|
+
The original fold only handled dx > shape/2, leaving offsets below -shape/2 unfolded. A winner
|
|
237
|
+
at row 0 wrapped correctly (h[9] = 0.6065) while a winner at row 9 did not (h[0] = 0.0).
|
|
238
|
+
"""
|
|
239
|
+
som = make_som(x=10, y=10, cyclic_x=True, cyclic_y=True, neighborhood_function=name)
|
|
240
|
+
low = som._neighborhood_function((0, 5), 1.0)[:, 5]
|
|
241
|
+
high = som._neighborhood_function((9, 5), 1.0)[:, 5]
|
|
242
|
+
assert low[9] == pytest.approx(low[1])
|
|
243
|
+
assert high[0] == pytest.approx(high[8])
|
|
244
|
+
np.testing.assert_allclose(high, np.roll(low, 9))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@pytest.mark.parametrize("name", ["gaussian", "mexicanhat"])
|
|
248
|
+
def test_cyclic_response_is_translation_invariant(name):
|
|
249
|
+
"""On a torus every node is equivalent, so h depends only on the offset from the winner."""
|
|
250
|
+
som = make_som(x=12, y=12, cyclic_x=True, cyclic_y=True, neighborhood_function=name)
|
|
251
|
+
reference = som._neighborhood_function((0, 0), 2.0)
|
|
252
|
+
for cx in range(12):
|
|
253
|
+
for cy in range(12):
|
|
254
|
+
shifted = som._neighborhood_function((cx, cy), 2.0)
|
|
255
|
+
np.testing.assert_allclose(
|
|
256
|
+
shifted, np.roll(reference, (cx, cy), axis=(0, 1))
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_non_cyclic_grid_does_not_wrap():
|
|
261
|
+
som = make_som(x=10, y=10)
|
|
262
|
+
h = som._gaussian((0, 5), 1.0)[:, 5]
|
|
263
|
+
assert h[9] < 1e-12
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# --------------------------------------------------------------------------------------------
|
|
267
|
+
# Selection and guards at the SOM level
|
|
268
|
+
# --------------------------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_unknown_neighborhood_function_raises_value_error():
|
|
272
|
+
with pytest.raises(ValueError, match="neighborhood_function"):
|
|
273
|
+
python_som.SOM(x=8, y=8, input_len=3, neighborhood_function="mexican_hat")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_batch_training_rejects_the_mexican_hat():
|
|
277
|
+
"""Kohonen Eq. (8) divides by sum_j n_j h_ji, which is not sign-definite for a signed h."""
|
|
278
|
+
rng = np.random.default_rng(0)
|
|
279
|
+
data = rng.normal(size=(30, 3))
|
|
280
|
+
som = make_som(x=12, y=12, neighborhood_function="mexicanhat")
|
|
281
|
+
with pytest.raises(ValueError, match="batch"):
|
|
282
|
+
som.train(data, n_iteration=1, mode="batch")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@pytest.mark.parametrize("mode", ["random", "sequential"])
|
|
286
|
+
def test_stepwise_training_accepts_the_mexican_hat(mode):
|
|
287
|
+
rng = np.random.default_rng(0)
|
|
288
|
+
som = make_som(x=12, y=12, neighborhood_function="mexicanhat")
|
|
289
|
+
som.train(rng.normal(size=(30, 3)), n_iteration=30, mode=mode)
|
|
290
|
+
assert np.isfinite(som.get_weights()).all()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|