python-som 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -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 Bubble neighborhood functions
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 Bubble neighborhood functions
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 'bubble'.
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
- self._neighborhood_function = {
245
+ neighborhood_functions = {
246
246
  "gaussian": self._gaussian,
247
247
  "bubble": self._bubble,
248
- }[neighborhood_function]
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
- # Calculate coefficient with sigma
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-som
3
- Version: 0.1.3
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,7 +16,7 @@ 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.9
19
+ Requires-Python: >=3.10
20
20
  Requires-Dist: numpy
21
21
  Requires-Dist: pandas
22
22
  Requires-Dist: scikit-learn
@@ -37,7 +37,7 @@ Most features were implemented using NumPy, with Scikit-learn for standardizatio
37
37
  * Linear weight initialization (with PCA)
38
38
  * Automatic selection of map size ratio (with PCA)
39
39
  * Support for cyclic arrays, for toroidal or spherical maps
40
- * Gaussian and Bubble neighborhood functions
40
+ * Gaussian, Bubble and Mexican hat neighborhood functions
41
41
  * Support for custom decay functions
42
42
  * Support for visualization (U-matrix, activation matrix)
43
43
  * Support for supervised learning (label map)
@@ -134,6 +134,22 @@ The following are lists of public methods and functions currently available in t
134
134
  * SOM.train
135
135
  * SOM.weight_initialization
136
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
+
137
153
  ## References
138
154
  This implemetation was based on the following paper, by Professor Teuvo Kohonen:
139
155
 
@@ -144,4 +160,15 @@ Volume 37,
144
160
  2013,
145
161
  Pages 52-65,
146
162
  ISSN 0893-6080,
147
- 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.
@@ -0,0 +1,5 @@
1
+ python_som/__init__.py,sha256=xG3Dq-6Wipv57lTsHzZWUC-UmYmtF5MP3BV5yC3_jkI,39881
2
+ python_som-0.2.0.dist-info/METADATA,sha256=CkwnAxayiH7G3lf1nbt6KMpvzl3d9lrqdxLKGS5M8BU,6542
3
+ python_som-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
4
+ python_som-0.2.0.dist-info/licenses/LICENSE,sha256=dMZQho8iIicWbAJQk1QaAXXvvJlaWSQ3a-9NkWxu_lc,1077
5
+ python_som-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,5 +0,0 @@
1
- python_som/__init__.py,sha256=MMEdaZxLE5MQsrmjYWhwQcC3Nj47QRLlPTtRiOJiSa8,33022
2
- python_som-0.1.3.dist-info/METADATA,sha256=ClgvuD9gQ1hoHuagaRpbM55vG6QLWm4jXDSBNkFpaUs,5084
3
- python_som-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
- python_som-0.1.3.dist-info/licenses/LICENSE,sha256=dMZQho8iIicWbAJQk1QaAXXvvJlaWSQ3a-9NkWxu_lc,1077
5
- python_som-0.1.3.dist-info/RECORD,,