python-som 0.0.1a2__py3-none-any.whl → 0.1.2__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
@@ -1,12 +1,47 @@
1
+ """This module contains the implementation of the 2D self-organizing map.
2
+
3
+ Most features were implemented using NumPy, with Scikit-learn for standardization and PCA.
4
+
5
+ Features:
6
+ - Stepwise and batch training
7
+ - Random weight initialization
8
+ - Random sampling weight initialization
9
+ - Linear weight initialization (with PCA)
10
+ - Automatic selection of map size ratio (with PCA)
11
+ - Support for cyclic arrays, for toroidal or spherical maps
12
+ - Gaussian and Bubble neighborhood functions
13
+ - Support for custom decay functions
14
+ - Support for visualization (U-matrix, activation matrix)
15
+ - Support for supervised learning (label map)
16
+ - Support for NumPy arrays, Pandas DataFrames and regular lists of values
17
+
18
+ Reference:
19
+ Teuvo Kohonen,
20
+ Essentials of the self-organizing map,
21
+ Neural Networks,
22
+ Volume 37,
23
+ 2013,
24
+ Pages 52-65,
25
+ ISSN 0893-6080,
26
+ https://doi.org/10.1016/j.neunet.2012.09.018.
27
+ """
28
+
1
29
  # %%
2
30
  from collections import Counter
3
- from typing import Union, Callable, Tuple, Iterable
31
+ from typing import Callable
4
32
 
5
33
  import numpy as np
6
34
  import pandas as pd
7
- import sklearn
8
- import sklearn.decomposition
9
- import sklearn.preprocessing
35
+ import sklearn # type: ignore
36
+ import sklearn.decomposition # type: ignore
37
+ import sklearn.preprocessing # type: ignore
38
+
39
+ try:
40
+ import tqdm
41
+
42
+ TQDM_AVAILABLE = True
43
+ except ImportError:
44
+ TQDM_AVAILABLE = False
10
45
 
11
46
 
12
47
  # %%
@@ -59,15 +94,14 @@ def _inverse_decay(x: float, t: int, max_t: int) -> float:
59
94
  return (max_t / 100) * x / ((max_t / 100) + t)
60
95
 
61
96
 
62
- def _euclidean_distance(
63
- a: Union[float, np.ndarray], b: Union[float, np.ndarray]
64
- ) -> Union[float, np.ndarray]:
65
- """
66
- This function calculates the euclidean distances between the elements of the last dimension of a and b.
97
+ def _euclidean_distance(a: np.ndarray, b: np.ndarray) -> np.ndarray:
98
+ """This function calculates the euclidean distances between the elements of the last dimension
99
+ of a and b.
67
100
 
68
101
  The parameters a and b may be n-dimensional, but their shapes must be capable of broadcasting
69
102
 
70
- The shape of the output complies to the first n-1 dimensions of the parameter with the highest dimensionality.
103
+ The shape of the output complies to the first n-1 dimensions of the parameter with the highest
104
+ dimensionality.
71
105
 
72
106
  :param a: array-like: list or numpy array of values. a must not be a scalar value.
73
107
  :param b: array-like: list or numpy array of values. b must not be a scalar value.
@@ -77,9 +111,11 @@ def _euclidean_distance(
77
111
 
78
112
 
79
113
  class SOM:
80
- """
81
- Implementation of the 2D self-organizing map, with support for NumPy arrays and Pandas DataFrames.
82
- Most features were implemented using NumPy, with Scikit-learn for standardization and PCA operations.
114
+ """Implementation of the 2D self-organizing map, with support for NumPy arrays and
115
+ Pandas DataFrames.
116
+
117
+ Most features were implemented using NumPy, with Scikit-learn for standardization and
118
+ PCA operations.
83
119
 
84
120
  Features:
85
121
  - Stepwise and batch training
@@ -103,13 +139,12 @@ class SOM:
103
139
  Pages 52-65,
104
140
  ISSN 0893-6080,
105
141
  https://doi.org/10.1016/j.neunet.2012.09.018.
106
-
107
142
  """
108
143
 
109
144
  def __init__(
110
145
  self,
111
- x: Union[int, None],
112
- y: Union[int, None],
146
+ x: int | None,
147
+ y: int | None,
113
148
  input_len: int,
114
149
  learning_rate: float = 0.5,
115
150
  learning_rate_decay: Callable[[float, int, int], float] = _asymptotic_decay,
@@ -119,13 +154,13 @@ class SOM:
119
154
  ] = _asymptotic_decay,
120
155
  neighborhood_function: str = "gaussian",
121
156
  distance_function: Callable[
122
- [Union[float, np.ndarray], Union[float, np.ndarray]],
123
- Union[float, np.ndarray],
157
+ [np.ndarray, np.ndarray],
158
+ np.ndarray,
124
159
  ] = _euclidean_distance,
125
160
  cyclic_x: bool = False,
126
161
  cyclic_y: bool = False,
127
- random_seed: Union[int, None] = None,
128
- data: Union[np.ndarray, pd.DataFrame, list, None] = None,
162
+ random_seed: int | None = None,
163
+ data: np.ndarray | pd.DataFrame | list | None = None,
129
164
  ) -> None:
130
165
  """
131
166
  Constructor for the self-organizing map class.
@@ -133,13 +168,13 @@ class SOM:
133
168
  :param x: int or NoneType: X dimension of the self-organizing map, i.e.,
134
169
  number of rows of the matrix of weights.
135
170
  x should be larger than 0.
136
- If x is None and 'data' is provided in kwargs, its value will be automatically selected using PCA of 'data'.
137
- Either x or y should be different than None.
171
+ If x is None and 'data' is provided in kwargs, its value will be automatically
172
+ selected using PCA of 'data'. Either x or y should be different than None.
138
173
  :param y: int or NoneType: Y dimension of the self-organizing map, i.e.,
139
174
  number of columns of the matrix of weights.
140
175
  y should be larger than 0.
141
- If y is None and 'data' is provided in kwargs, its value will be automatically selected using PCA of 'data'.
142
- Either x or y should be different than None.
176
+ If y is None and 'data' is provided in kwargs, its value will be automatically
177
+ selected using PCA of 'data'. Either x or y should be different than None.
143
178
  :param input_len: int: Number of features of the training dataset, i.e.,
144
179
  number of elements of each node of the network.
145
180
  :param learning_rate: float: Initial learning rate for the training process.
@@ -147,23 +182,24 @@ class SOM:
147
182
  Defaults to 0.5.
148
183
  Note: The value of the learning_rate is irrelevant for the 'batch' training mode.
149
184
  :param learning_rate_decay: function: Decay function for the learning_rate variable.
150
- May be a predefined one from this package, or a custom function, with the same parameters and return type.
151
- Defaults to _asymptotic_decay.
152
- :param neighborhood_radius: float: Initial neighborhood radius for the training process. Defaults to 1.
153
- :param neighborhood_radius_decay: function: Decay function for the neighborhood_radius variable.
154
- May be a predefined one from this package, or a custom function, with the same parameters and return type.
155
- Defaults to _asymptotic_decay
185
+ May be a predefined one from this package, or a custom function, with the same
186
+ parameters and return type. Defaults to _asymptotic_decay.
187
+ :param neighborhood_radius: float: Initial neighborhood radius for the training process.
188
+ Defaults to 1.
189
+ :param neighborhood_radius_decay: function: Decay function for the neighborhood_radius
190
+ variable. May be a predefined one from this package, or a custom function, with the
191
+ same parameters and return type. Defaults to _asymptotic_decay
156
192
  :param neighborhood_function: str: Neighborhood function name for the training process.
157
193
  May be either 'gaussian' or 'bubble'.
158
- :param distance_function: function: Function for calculating distances/dissimilarities between models of the
159
- network.
160
- May be a predefined one from this package, or a custom function, with the same parameters and return type.
161
- Defaults to _euclidean_distance.
162
- :param cyclic_x: bool: Boolean value activate/deactivate cyclic arrays in the x direction, i.e,
163
- between the first and last rows of the weight matrix.
194
+ :param distance_function: function: Function for calculating distances/dissimilarities
195
+ between models of the network.
196
+ May be a predefined one from this package, or a custom function, with the same
197
+ parameters and return type. Defaults to _euclidean_distance.
198
+ :param cyclic_x: bool: Boolean value activate/deactivate cyclic arrays in the x direction,
199
+ i.e, between the first and last rows of the weight matrix.
164
200
  Defaults to False.
165
- :param cyclic_y: bool: Boolean value activate/deactivate cyclic arrays in the y direction, i.e,
166
- between the first and last columns of the weight matrix.
201
+ :param cyclic_y: bool: Boolean value activate/deactivate cyclic arrays in the y direction,
202
+ i.e, between the first and last columns of the weight matrix.
167
203
  Defaults to False.
168
204
  :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
169
205
  :param data: array-like: dataset for performing PCA.
@@ -174,11 +210,12 @@ class SOM:
174
210
  raise ValueError("At least one of the dimensions (x, y) must be specified")
175
211
  if x is None or y is None:
176
212
  # If a dataset was given through **kwargs, select missing dimension with PCA
177
- # The ratio of the (x, y) sizes will comply roughly with the ratio of the two largest principal components
178
- if data == None:
213
+ # The ratio of the (x, y) sizes will comply roughly with the ratio of
214
+ # the two largest principal components
215
+ if data is None:
179
216
  raise ValueError(
180
- "If one of the dimensions is not specified, a dataset must be provided for automatic size "
181
- "initialization."
217
+ "If one of the dimensions is not specified,"
218
+ "a dataset must be provided for automatic size initialization."
182
219
  )
183
220
  # Convert data to numpy array
184
221
  if isinstance(data, pd.DataFrame):
@@ -201,25 +238,23 @@ class SOM:
201
238
  # Initializing private variables
202
239
  self._shape = (np.uint(x), np.uint(y))
203
240
  self._input_len = np.uint(input_len)
204
- self._learning_rate = np.float(learning_rate)
241
+ self._learning_rate = float(learning_rate)
205
242
  self._learning_rate_decay = learning_rate_decay
206
- self._neighborhood_radius = np.float(neighborhood_radius)
243
+ self._neighborhood_radius = float(neighborhood_radius)
207
244
  self._neighborhood_radius_decay = neighborhood_radius_decay
208
245
  self._neighborhood_function = {
209
246
  "gaussian": self._gaussian,
210
247
  "bubble": self._bubble,
211
248
  }[neighborhood_function]
212
249
  self._distance_function = distance_function
213
- self._cyclic = (np.bool(cyclic_x), np.bool(cyclic_y))
250
+ self._cyclic = (bool(cyclic_x), bool(cyclic_y))
214
251
  self._neigx, self._neigy = np.arange(self._shape[0]), np.arange(self._shape[1])
215
252
 
216
253
  # Seed numpy random generator
217
254
  if random_seed is None:
218
- self._random_seed = np.random.randint(
219
- np.random.randint(np.iinfo(np.int32).max)
220
- )
255
+ self._random_seed = np.random.randint(np.iinfo(np.int32).max)
221
256
  else:
222
- self._random_seed = np.int(random_seed)
257
+ self._random_seed = int(random_seed)
223
258
  np.random.seed(self._random_seed)
224
259
 
225
260
  # Random weight initialization
@@ -227,7 +262,7 @@ class SOM:
227
262
  size=(self._shape[0], self._shape[1], self._input_len)
228
263
  )
229
264
 
230
- def get_shape(self) -> Tuple[int, int]:
265
+ def get_shape(self) -> tuple[np.uint, np.uint]:
231
266
  """
232
267
  Gets the shape of the network.
233
268
 
@@ -249,17 +284,17 @@ class SOM:
249
284
 
250
285
  :param learning_rate: float: New value for learning_rate of an instance of the SOM.
251
286
  """
252
- self._learning_rate = np.float(learning_rate)
287
+ self._learning_rate = float(learning_rate)
253
288
 
254
289
  def set_neighborhood_radius(self, neighborhood_radius: float) -> None:
255
290
  """
256
291
  Sets the neighborhood_radius member of the SOM.
257
292
 
258
- :param neighborhood_radius: float: New value for neighborhood_radius of an instance of the SOM.
293
+ :param neighborhood_radius: float: New value for neighborhood_radius of a SOM instance.
259
294
  """
260
- self._neighborhood_radius = np.float(neighborhood_radius)
295
+ self._neighborhood_radius = float(neighborhood_radius)
261
296
 
262
- def activate(self, x: Union[np.ndarray, pd.DataFrame, list]) -> np.ndarray:
297
+ def activate(self, x: np.ndarray) -> np.ndarray:
263
298
  """
264
299
  Calculates distances between an instance x and the weights of the network.
265
300
 
@@ -268,9 +303,7 @@ class SOM:
268
303
  """
269
304
  return self._distance_function(x, self._weights)
270
305
 
271
- def winner(
272
- self, x: Union[np.ndarray, pd.DataFrame, list]
273
- ) -> Union[Iterable, Tuple[int, int]]:
306
+ def winner(self, x: np.ndarray) -> tuple[int, ...]:
274
307
  """
275
308
  Calculates the best-matching unit of the network for an instance x
276
309
 
@@ -278,21 +311,22 @@ class SOM:
278
311
  :return: (int, int): Index of the best-matching unit of x.
279
312
  """
280
313
  activation_map = self.activate(x)
281
- return np.unravel_index(activation_map.argmin(), activation_map.shape)
314
+ min_index = tuple(
315
+ map(int, np.unravel_index(activation_map.argmin(), activation_map.shape))
316
+ )
317
+ return min_index
282
318
 
283
- def quantization(self, data: Union[np.ndarray, pd.DataFrame, list]) -> np.ndarray:
319
+ def quantization(self, data: np.ndarray | pd.DataFrame | list) -> np.ndarray:
284
320
  """
285
321
  Calculates distances from each instance of 'data' to each of the weights of the network.
286
322
 
287
323
  :param data: array-like: Dataset to be compared with the weights of the network.
324
+ Expected shape is (n_samples, n_features).
288
325
  :return: np.ndarray: array of lists of distances from each instance of the dataset
289
326
  to each weight of the network.
290
327
  """
291
328
  # Convert data to numpy array
292
- if isinstance(data, pd.DataFrame):
293
- data_array = data.to_numpy()
294
- else:
295
- data_array = np.array(data)
329
+ data_array = self._data_to_numpy(data)
296
330
  return np.array(
297
331
  [
298
332
  (self._distance_function(i, self._weights[self.winner(i)]))
@@ -300,9 +334,8 @@ class SOM:
300
334
  ]
301
335
  )
302
336
 
303
- def quantization_error(self, data: Union[np.ndarray, pd.DataFrame, list]) -> float:
304
- """
305
- Calculates average distance of the weights of the network to their assigned instances from data.
337
+ def quantization_error(self, data: np.ndarray | pd.DataFrame | list) -> float:
338
+ """Calculates average distance of the weights of the network to their assigned instances.
306
339
  This error is a quality measure for the training process.
307
340
 
308
341
  :param data: array-like: Dataset to be compared with the weights of the network.
@@ -311,47 +344,52 @@ class SOM:
311
344
  quantization = self.quantization(data)
312
345
  return quantization.mean()
313
346
 
314
- def distance_matrix(self) -> np.ndarray:
347
+ def distance_matrix(self, normalize: bool = False) -> np.ndarray:
315
348
  """
316
- Calculates U-matrix of the current state of the network, i.e., the matrix of distances between each node and its
317
- neighbors. Has support for cyclic arrays
349
+ Calculates U-matrix of the current state of the network,
350
+ i.e., the matrix of distances between each node and its neighbors.
351
+ Has support for cyclic arrays
318
352
 
353
+ :param normalize: bool: Activate to normalize the U-matrix between 0 and 1.
354
+ Defaults to False.
319
355
  :return: np.ndarray: U-matrix of the current state of the network.
320
356
  """
321
357
  um = np.zeros(shape=self._shape)
322
358
  it = np.nditer(um, flags=["multi_index"])
359
+ distances = np.zeros(self._shape + self._shape)
360
+ for i in range(self._shape[0]):
361
+ for j in range(self._shape[1]):
362
+ distances[i, j] = self._distance_function(
363
+ self._weights[i, j], self._weights
364
+ )
365
+
323
366
  while not it.finished:
324
367
  update_matrix = self._bubble(it.multi_index, 1)
325
- um[it.multi_index] = np.sum(
326
- update_matrix
327
- * self._distance_function(self._weights[it.multi_index], self._weights)
328
- )
368
+ um[it.multi_index] = np.sum(update_matrix * distances[it.multi_index])
329
369
  it.iternext()
330
- um /= um.max(initial=0.0)
370
+ if normalize:
371
+ # Normalize U-matrix between 0 and 1
372
+ um = (um - np.min(um)) / (np.max(um) - np.min(um))
331
373
  return um
332
374
 
333
- def activation_matrix(
334
- self, data: Union[np.ndarray, pd.DataFrame, list]
335
- ) -> np.ndarray:
336
- """
337
- Calculates the activation matrix of the network for a dataset, i.e., for each node, the count of instances that
338
- have been assigned to it, in the current state.
375
+ def activation_matrix(self, data: np.ndarray | pd.DataFrame | list) -> np.ndarray:
376
+ """Calculates the activation matrix of the network for a dataset.
377
+
378
+ I.e., for each node, the count of instances that have been assigned to in,
379
+ in the current state.
339
380
 
340
381
  :param data: array-like: Dataset to be compared with the weights of the network.
341
382
  :return: np.ndarray: Activation matrix.
342
383
  """
343
384
  # Convert data to numpy array
344
- if isinstance(data, pd.DataFrame):
345
- data_array = data.to_numpy()
346
- else:
347
- data_array = np.array(data)
385
+ data_array = self._data_to_numpy(data)
348
386
 
349
387
  activation_matrix = np.zeros(self._shape)
350
388
  for i in data_array:
351
389
  activation_matrix[self.winner(i)] += 1
352
390
  return activation_matrix
353
391
 
354
- def winner_map(self, data: Union[np.ndarray, pd.DataFrame, list]) -> dict:
392
+ def winner_map(self, data: np.ndarray | pd.DataFrame | list) -> dict:
355
393
  """
356
394
  Calculates, for each node (i, j) of the network,
357
395
  the list of all instances from 'data' that has been assigned to it.
@@ -360,12 +398,9 @@ class SOM:
360
398
  :return: dict: Winner map.
361
399
  """
362
400
  # Convert data to numpy array
363
- if isinstance(data, pd.DataFrame):
364
- data_array = data.to_numpy()
365
- else:
366
- data_array = np.array(data)
367
- winner_map = {
368
- (i, j): [] for i in range(self._shape[0]) for j in range(self._shape[1])
401
+ data_array = self._data_to_numpy(data)
402
+ winner_map: dict[tuple[int, ...], list] = {
403
+ tuple(index): [] for index in np.ndindex(self._shape)
369
404
  }
370
405
  for i in data_array:
371
406
  winner_map[self.winner(i)].append(i)
@@ -373,40 +408,42 @@ class SOM:
373
408
 
374
409
  def label_map(
375
410
  self,
376
- data: Union[np.ndarray, pd.DataFrame, list],
377
- labels: Union[np.ndarray, pd.DataFrame, list],
378
- ) -> dict:
379
- """
380
- Calculates, for each node (i, j) of the network, the frequency of each label from 'labels' corresponding to its
381
- respective instance from 'data' that has been assigned to this node.
411
+ data: np.ndarray | pd.DataFrame | list,
412
+ labels: np.ndarray | pd.DataFrame | list,
413
+ ) -> dict[tuple[int, ...], Counter]:
414
+ """Calculates, for each node (i, j) of the network, the frequency of each label...
415
+
416
+ from 'labels' corresponding to its respective instance from 'data' that has been assigned
417
+ to this node.
418
+
382
419
  :param data: array-like: Dataset to be compared with the weights of the network.
383
420
  :param labels: array-like: Labels corresponding to the indices of 'data'.
384
421
  :return: dict: Label map.
385
422
  """
386
423
  # Convert data to numpy array
387
- if isinstance(data, pd.DataFrame):
388
- data_array = data.to_numpy()
389
- else:
390
- data_array = np.array(data)
424
+ data_array = self._data_to_numpy(data)
391
425
  # Convert labels to numpy array
392
426
  if isinstance(labels, pd.DataFrame):
393
427
  labels = labels.to_numpy()
394
428
  else:
395
429
  labels = np.array(labels)
396
430
 
397
- winner_map = {
398
- (i, j): [] for i in range(self._shape[0]) for j in range(self._shape[1])
431
+ winner_map: dict[tuple[int, ...], list] = {
432
+ tuple(index): [] for index in np.ndindex(self._shape)
399
433
  }
400
- for i in range(len(data_array)):
401
- winner_map[self.winner(data_array[i])].append(labels[i])
402
- for key in winner_map:
403
- winner_map[key] = Counter(winner_map[key])
404
- return winner_map
434
+ label_count_map: dict[tuple[int, ...], Counter] = {
435
+ tuple(index): Counter() for index in np.ndindex(self._shape)
436
+ }
437
+ for i, instance in enumerate(data_array):
438
+ winner = self.winner(instance)
439
+ winner_map[winner].append(labels[i])
440
+ label_count_map[winner].update([labels[i]])
441
+ return label_count_map
405
442
 
406
443
  def train(
407
444
  self,
408
- data: Union[np.ndarray, pd.DataFrame, list],
409
- n_iteration: Union[int, None] = None,
445
+ data: np.ndarray | pd.DataFrame | list,
446
+ n_iteration: int | None = None,
410
447
  mode: str = "random",
411
448
  verbose: bool = False,
412
449
  ) -> float:
@@ -418,17 +455,14 @@ class SOM:
418
455
  If None, defaults to 1000 * len(data) for stepwise training modes,
419
456
  or 10 * len(data) for batch training mode.
420
457
  :param mode: str: Training mode name. May be either 'random', 'sequential', or 'batch'.
421
- For 'batch' mode, a much smaller number of iterations is needed, but a higher computation power is required
422
- for each individual iteration.
458
+ For 'batch' mode, a much smaller number of iterations is needed,
459
+ but a higher computation power is required for each individual iteration.
423
460
  :param verbose: bool: Activate to print useful information to the terminal/console, e.g.,
424
461
  the progress of the training process
425
462
  :return: float: Quantization error after training
426
463
  """
427
464
  # Convert data to numpy array for training
428
- if isinstance(data, pd.DataFrame):
429
- data_array = data.to_numpy()
430
- else:
431
- data_array = np.array(data)
465
+ data_array = self._data_to_numpy(data)
432
466
 
433
467
  # If no number of iterations is given, select automatically
434
468
  if n_iteration is None:
@@ -444,108 +478,13 @@ class SOM:
444
478
  mode,
445
479
  sep=" ",
446
480
  )
447
- if mode == "random":
448
- # Random sampling from training dataset
449
- for it, i in enumerate(
450
- np.random.choice(
451
- len(data_array), size=n_iteration, replace=(n_iteration > len(data))
452
- )
453
- ):
454
- # Calculating decaying alpha and sigma parameters for updating weights
455
- alpha = self._learning_rate_decay(self._learning_rate, it, n_iteration)
456
- sigma = self._neighborhood_radius_decay(
457
- self._neighborhood_radius, it, n_iteration
458
- )
459
-
460
- # Finding winner node (best-matching unit)
461
- winner = self.winner(data_array[i])
462
-
463
- # Updating weights, based on current neighborhood function
464
- self._weights += (
465
- alpha
466
- * self._neighborhood_function(winner, sigma)[..., None]
467
- * (data_array[i] - self._weights)
468
- )
469
481
 
470
- # Print progress, if verbose is activated
471
- if verbose:
472
- print(
473
- "Iteration:",
474
- it,
475
- "/",
476
- n_iteration,
477
- sep=" ",
478
- end="\r",
479
- flush=True,
480
- )
482
+ if mode == "random":
483
+ self._train_random(data_array, n_iteration, verbose)
481
484
  elif mode == "sequential":
482
- # Sequential sampling from training dataset
483
- for it, i in enumerate(data_array):
484
- # Calculating decaying alpha and sigma parameters for updating weights
485
- alpha = self._learning_rate_decay(self._learning_rate, it, n_iteration)
486
- sigma = self._neighborhood_radius_decay(
487
- self._neighborhood_radius, it, n_iteration
488
- )
489
-
490
- # Finding winner node (best-matching unit)
491
- winner = self.winner(i)
492
-
493
- # Updating weights, based on current neighborhood function
494
- self._weights += (
495
- alpha
496
- * self._neighborhood_function(winner, sigma)[..., None]
497
- * (i - self._weights)
498
- )
499
-
500
- # Print progress, if verbose is activated
501
- if verbose:
502
- print(
503
- "Iteration:",
504
- it,
505
- "/",
506
- n_iteration,
507
- sep=" ",
508
- end="\r",
509
- flush=True,
510
- )
485
+ self._train_sequential(data_array, n_iteration, verbose)
511
486
  elif mode == "batch":
512
- # Batch training
513
- for it in range(n_iteration):
514
- # Calculating decaying sigma
515
- sigma = self._neighborhood_radius_decay(
516
- self._neighborhood_radius, it, n_iteration
517
- )
518
-
519
- # For each node, create a list of instances associated to it
520
- winner_map = self.winner_map(data_array)
521
-
522
- # Calculate the weighted average of all instances in the neighborhood of each node
523
- new_weights = np.zeros(self._weights.shape)
524
- for i in winner_map.keys():
525
- neig = self._neighborhood_function(i, sigma)
526
- upper, bottom = np.zeros(self._input_len), 0.0
527
- for j in winner_map.keys():
528
- upper += neig[j] * np.sum(winner_map[j], axis=0)
529
- bottom += neig[j] * len(winner_map[j])
530
-
531
- # Only update if there is any instance associated with the winner node or its neighbors
532
- if bottom != 0:
533
- new_weights[i] = upper / bottom
534
-
535
- # Update all nodes concurrently
536
- self._weights = new_weights
537
-
538
- # Print progress, if verbose is activated
539
- if verbose:
540
- print(
541
- "Iteration:",
542
- it,
543
- "/",
544
- n_iteration,
545
- sep=" ",
546
- end="\r",
547
- flush=True,
548
- )
487
+ self._train_batch(data_array, n_iteration, verbose)
549
488
  else:
550
489
  # Invalid training mode value
551
490
  raise ValueError(
@@ -559,42 +498,186 @@ class SOM:
559
498
  print("Quantization error:", q_error, sep=" ")
560
499
  return q_error
561
500
 
501
+ def _train_random(
502
+ self, data_array: np.ndarray, n_iteration: int, verbose: bool
503
+ ) -> None:
504
+ if TQDM_AVAILABLE and verbose:
505
+ iterator: enumerate | tqdm.tqdm = tqdm.tqdm(
506
+ np.random.choice(
507
+ len(data_array),
508
+ size=n_iteration,
509
+ replace=(n_iteration > len(data_array)),
510
+ ),
511
+ total=n_iteration,
512
+ desc="Training",
513
+ )
514
+ else:
515
+ iterator = enumerate(
516
+ np.random.choice(
517
+ len(data_array),
518
+ size=n_iteration,
519
+ replace=(n_iteration > len(data_array)),
520
+ )
521
+ )
522
+
523
+ for it, i in iterator: # type: ignore
524
+ # Calculating decaying alpha and sigma parameters for updating weights
525
+ alpha = self._learning_rate_decay(self._learning_rate, it, n_iteration)
526
+ sigma = self._neighborhood_radius_decay(
527
+ self._neighborhood_radius, it, n_iteration
528
+ )
529
+
530
+ # Finding winner node (best-matching unit)
531
+ winner = self.winner(data_array[i])
532
+
533
+ # Updating weights, based on current neighborhood function
534
+ self._weights += (
535
+ alpha
536
+ * self._neighborhood_function(winner, sigma)[..., None]
537
+ * (data_array[i] - self._weights)
538
+ )
539
+
540
+ # Print progress, if verbose is activated and tqdm is not available
541
+ if verbose and not TQDM_AVAILABLE:
542
+ print(
543
+ "Iteration:",
544
+ it,
545
+ "/",
546
+ n_iteration,
547
+ sep=" ",
548
+ end="\r",
549
+ flush=True,
550
+ )
551
+
552
+ def _train_sequential(
553
+ self, data_array: np.ndarray, n_iteration: int, verbose: bool
554
+ ) -> None:
555
+ if TQDM_AVAILABLE and verbose:
556
+ iterator: enumerate | tqdm.tqdm = tqdm.tqdm(
557
+ enumerate(data_array),
558
+ total=n_iteration,
559
+ desc="Training",
560
+ )
561
+ else:
562
+ iterator = enumerate(data_array)
563
+
564
+ for it, i in iterator: # type: ignore
565
+ # Calculating decaying alpha and sigma parameters for updating weights
566
+ alpha = self._learning_rate_decay(self._learning_rate, it, n_iteration)
567
+ sigma = self._neighborhood_radius_decay(
568
+ self._neighborhood_radius, it, n_iteration
569
+ )
570
+
571
+ # Finding winner node (best-matching unit)
572
+ winner = self.winner(i)
573
+
574
+ # Updating weights, based on current neighborhood function
575
+ self._weights += (
576
+ alpha
577
+ * self._neighborhood_function(winner, sigma)[..., None]
578
+ * (i - self._weights)
579
+ )
580
+
581
+ # Print progress, if verbose is activated and tqdm is not available
582
+ if verbose and not TQDM_AVAILABLE:
583
+ print(
584
+ "Iteration:",
585
+ it,
586
+ "/",
587
+ n_iteration,
588
+ sep=" ",
589
+ end="\r",
590
+ flush=True,
591
+ )
592
+
593
+ def _train_batch(
594
+ self, data_array: np.ndarray, n_iteration: int, verbose: bool
595
+ ) -> None:
596
+ if TQDM_AVAILABLE and verbose:
597
+ iterator: range | tqdm.tqdm = tqdm.tqdm(
598
+ range(n_iteration), total=n_iteration, desc="Training"
599
+ )
600
+ else:
601
+ iterator = range(n_iteration)
602
+
603
+ for it in iterator:
604
+ # Calculating decaying sigma
605
+ sigma = self._neighborhood_radius_decay(
606
+ self._neighborhood_radius, it, n_iteration
607
+ )
608
+
609
+ # For each node, create a list of instances associated to it
610
+ winner_map = self.winner_map(data_array)
611
+
612
+ # Calculate the weighted average of all instances in the neighborhood of each node
613
+ new_weights = np.zeros(self._weights.shape)
614
+ for i in winner_map.keys():
615
+ neig = self._neighborhood_function(i, sigma)
616
+ upper, bottom = np.zeros(self._input_len), 0.0
617
+ for j in winner_map.keys():
618
+ upper += neig[j] * np.sum(winner_map[j], axis=0)
619
+ bottom += neig[j] * len(winner_map[j])
620
+
621
+ # Only update if there is any instance associated with the winner node
622
+ # or its neighbors
623
+ if bottom != 0:
624
+ new_weights[i] = upper / bottom
625
+
626
+ # Update all nodes concurrently
627
+ self._weights = new_weights
628
+
629
+ # Print progress, if verbose is activated and tqdm is not available
630
+ if verbose and not TQDM_AVAILABLE:
631
+ print(
632
+ "Iteration:",
633
+ it,
634
+ "/",
635
+ n_iteration,
636
+ sep=" ",
637
+ end="\r",
638
+ flush=True,
639
+ )
640
+
562
641
  def weight_initialization(
563
642
  self,
564
643
  mode: str = "random",
565
- **kwargs: Union[np.ndarray, pd.DataFrame, list, str, int]
644
+ **kwargs: np.ndarray | pd.DataFrame | list | str | int
566
645
  ) -> None:
567
- """
568
- Function for weight initialization of the self-organizing map. Calls other methods for each initialization mode.
646
+ """Function for weight initialization of the self-organizing map.
647
+
648
+ Calls other methods for each initialization mode.
569
649
 
570
650
  :param mode: str: Initialization mode. May be either 'random', 'linear', or 'sample'.
571
651
  Note: Each initialization method may require multiple additional arguments in kwargs.
572
652
  :param kwargs:
573
- For 'random' initialization mode, 'sample_mode': str may be provided to determine the sampling mode.
574
- 'sample_mode' may be either 'standard_normal' (default) or 'uniform'.
575
- For 'random' and 'sample' modes, 'random_seed': int may be provided for the random value generator.
576
- For 'sample' and 'linear' modes, 'data': array-like must be provided for sampling/PCA.
653
+ For 'random' initialization mode, 'sample_mode': str may be provided to determine
654
+ the sampling mode. 'sample_mode' may be either 'standard_normal' (default) or 'uniform'
655
+ For 'random' and 'sample' modes, 'random_seed': int may be provided for the random
656
+ value generator. For 'sample' and 'linear' modes, 'data': array-like must be provided
657
+ for sampling/PCA.
577
658
  """
578
- modes = {
659
+ modes: dict[str, Callable[..., None]] = {
579
660
  "random": self._weight_initialization_random,
580
661
  "linear": self._weight_initialization_linear,
581
662
  "sample": self._weight_initialization_sample,
582
663
  }
583
664
  try:
584
665
  modes[mode](**kwargs)
585
- except KeyError:
666
+ except KeyError as exc:
586
667
  raise ValueError(
587
668
  "Invalid value for 'mode' parameter. Value should be in "
588
669
  + str(modes.keys())
589
- )
670
+ ) from exc
590
671
 
591
672
  def _weight_initialization_random(
592
- self, sample_mode: str = "standard_normal", random_seed: Union[int, None] = None
673
+ self, sample_mode: str = "standard_normal", random_seed: int | None = None
593
674
  ) -> None:
594
- """
595
- Random initialization method. Assigns weights from a random distribution defined by 'sample_mode'.
675
+ """Random initialization method.
596
676
 
597
- :param sample_mode: str: Distribution for random sampling. May be either 'uniform' or 'standard_normal'.
677
+ Assigns weights from a random distribution defined by 'sample_mode'.
678
+
679
+ :param sample_mode: str: Distribution for random sampling.
680
+ May be either 'uniform' or 'standard_normal'.
598
681
  Defaults to 'standard_normal'.
599
682
  :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
600
683
  """
@@ -605,37 +688,35 @@ class SOM:
605
688
 
606
689
  # Seed numpy random generator
607
690
  if random_seed is None:
608
- random_seed = np.random.randint(np.random.randint(np.iinfo(np.int32).max))
691
+ random_seed = np.random.randint(np.iinfo(np.int32).max)
609
692
  else:
610
- random_seed = np.int(random_seed)
693
+ random_seed = int(random_seed)
611
694
  np.random.seed(random_seed)
612
695
 
613
696
  # Initialize weights randomly
614
697
  try:
615
698
  self._weights = sample_modes[sample_mode](size=self._weights.shape)
616
- except KeyError:
699
+ except KeyError as exc:
617
700
  raise ValueError(
618
701
  "Invalid value for 'sample_mode' parameter. Value should be in "
619
702
  + str(sample_modes.keys())
620
- )
703
+ ) from exc
621
704
 
622
705
  def _weight_initialization_linear(
623
- self, data: Union[np.ndarray, pd.DataFrame, list]
706
+ self, data: np.ndarray | pd.DataFrame | list
624
707
  ) -> None:
625
- """
626
- Linear initialization method. Assigns weights spanning the hyperplane formed by the two first principal
708
+ """Linear initialization method.
709
+
710
+ Assigns weights spanning the hyperplane formed by the two first principal
627
711
  components of 'data'.
628
712
 
629
- This is the recommended initialization method, as it may lead to a faster convergence. Unlike other
630
- initialization modes, this method is deterministic based on the input dataset.
713
+ This is the recommended initialization method, as it may lead to a faster convergence.
714
+ Unlike other initialization modes, this method is deterministic based on the input dataset.
631
715
 
632
716
  :param data: array-like: Dataset for weight initialization w/ PCA.
633
717
  """
634
718
  # Convert data to numpy array for training
635
- if isinstance(data, pd.DataFrame):
636
- data_array = data.to_numpy()
637
- else:
638
- data_array = np.array(data)
719
+ data_array = self._data_to_numpy(data)
639
720
 
640
721
  # Perform PCA w/ sklearn
641
722
  data_array = sklearn.preprocessing.StandardScaler().fit_transform(data_array)
@@ -650,36 +731,34 @@ class SOM:
650
731
 
651
732
  def _weight_initialization_sample(
652
733
  self,
653
- data: Union[np.ndarray, pd.DataFrame, list],
654
- random_seed: Union[int, None] = None,
734
+ data: np.ndarray | pd.DataFrame | list,
735
+ random_seed: int | None = None,
655
736
  ) -> None:
656
- """
657
- Initialization method. Assigns weights to random samples from an input dataset.
737
+ """Initialization method. Assigns weights to random samples from an input dataset.
658
738
 
659
739
  :param data: Dataset for weight initialization/sampling.
660
740
  :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
661
741
  """
662
742
  # Seed numpy random generator
663
743
  if random_seed is None:
664
- random_seed = np.random.randint(np.random.randint(np.iinfo(np.int32).max))
744
+ random_seed = np.random.randint(np.iinfo(np.int32).max)
665
745
  else:
666
- random_seed = np.int(random_seed)
746
+ random_seed = int(random_seed)
667
747
  np.random.seed(random_seed)
668
748
 
669
749
  # Convert data to numpy array for training
670
- if isinstance(data, pd.DataFrame):
671
- data_array = data.to_numpy()
672
- else:
673
- data_array = np.array(data)
750
+ data_array = self._data_to_numpy(data)
674
751
 
675
752
  # Assign weights to random samples from dataset
676
- sample_size = self._shape[0] * self._shape[1]
753
+ sample_size: np.uint = self._shape[0] * self._shape[1]
677
754
  sample = np.random.choice(
678
- len(data_array), size=sample_size, replace=(sample_size > len(data_array))
755
+ len(data_array),
756
+ size=int(sample_size),
757
+ replace=bool(sample_size > len(data_array)),
679
758
  )
680
759
  self._weights = data_array[sample].reshape(self._weights.shape)
681
760
 
682
- def _gaussian(self, c: Tuple[int, int], sigma: float) -> np.ndarray:
761
+ def _gaussian(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
683
762
  """
684
763
  Gaussian neighborhood function, centered in c. Has support for cyclic arrays.
685
764
 
@@ -702,12 +781,12 @@ class SOM:
702
781
  ay = np.exp(-np.power(dy, 2) / d)
703
782
  return np.outer(ax, ay)
704
783
 
705
- def _bubble(self, c: Tuple[int, int], sigma: float) -> np.ndarray:
784
+ def _bubble(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
706
785
  """
707
786
  Bubble neighborhood function, centered in c. Has support for cyclic arrays.
708
787
 
709
- The neighbors of c are the nodes in the region of sigma positions in the vertical and horizontal directions
710
- around c.
788
+ The neighbors of c are the nodes in the region of sigma positions in the vertical
789
+ and horizontal directions around c.
711
790
 
712
791
  :param c: (int, int): Center coordinates for gaussian function.
713
792
  :param sigma: float: Spread variable for gaussian function.
@@ -733,3 +812,14 @@ class SOM:
733
812
  ay[: int((c[1] + sigma) % self._shape[1] + 1)] = True
734
813
 
735
814
  return np.outer(ax, ay).astype(int)
815
+
816
+ def _data_to_numpy(self, data: np.ndarray | pd.DataFrame | list) -> np.ndarray:
817
+ """
818
+ Converts data to numpy array.
819
+
820
+ :param data: array-like: Dataset for training.
821
+ :return: np.ndarray: Numpy array of the dataset.
822
+ """
823
+ if isinstance(data, pd.DataFrame):
824
+ return data.to_numpy()
825
+ return np.array(data)
@@ -1,28 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-som
3
- Version: 0.0.1a2
3
+ Version: 0.1.2
4
4
  Summary: Python implementation of the Self-Organizing Map
5
- Home-page: https://github.com/andremsouza/python-som
6
- Author: André Moreira Souza
7
- Author-email: msouza.andre@hotmail.com
8
- License: MIT
9
- Project-URL: Bug Reports, https://github.com/andremsouza/python-som/issues
10
- Project-URL: Source, https://github.com/andremsouza/python-som
11
- Keywords: som kohonen-map self-organizing-map machine-learning
5
+ Project-URL: Homepage, https://github.com/andremsouza/python-som
6
+ Project-URL: Issues, https://github.com/andremsouza/python-som/issues
7
+ Author-email: André Moreira Souza <msouza.andre@hotmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
12
10
  Classifier: Development Status :: 3 - Alpha
13
11
  Classifier: Intended Audience :: Developers
14
12
  Classifier: Intended Audience :: Science/Research
15
- Classifier: Topic :: Software Development :: Libraries
16
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
13
  Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
19
15
  Classifier: Programming Language :: Python :: 3
20
- Requires-Python: >=3
21
- Description-Content-Type: text/markdown
22
- License-File: LICENSE
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
23
20
  Requires-Dist: numpy
24
21
  Requires-Dist: pandas
25
22
  Requires-Dist: scikit-learn
23
+ Description-Content-Type: text/markdown
26
24
 
27
25
  # python-som
28
26
 
@@ -144,4 +142,4 @@ Volume 37,
144
142
  2013,
145
143
  Pages 52-65,
146
144
  ISSN 0893-6080,
147
- https://doi.org/10.1016/j.neunet.2012.09.018.
145
+ https://doi.org/10.1016/j.neunet.2012.09.018.
@@ -0,0 +1,5 @@
1
+ python_som/__init__.py,sha256=MMEdaZxLE5MQsrmjYWhwQcC3Nj47QRLlPTtRiOJiSa8,33022
2
+ python_som-0.1.2.dist-info/METADATA,sha256=qg5ea52OLyI6ovd-7q8Ph_2ecDOXdzEGbYYkFPRTwUY,5028
3
+ python_som-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
+ python_som-0.1.2.dist-info/licenses/LICENSE,sha256=dMZQho8iIicWbAJQk1QaAXXvvJlaWSQ3a-9NkWxu_lc,1077
5
+ python_som-0.1.2.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.1)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,6 +0,0 @@
1
- python_som/__init__.py,sha256=bKgFPV8FzkL2CQ09V86kywGyfXr-HvOD9ZT4VnWm-ck,30520
2
- python_som-0.0.1a2.dist-info/LICENSE,sha256=dMZQho8iIicWbAJQk1QaAXXvvJlaWSQ3a-9NkWxu_lc,1077
3
- python_som-0.0.1a2.dist-info/METADATA,sha256=4Ck4KJNCqIxRbjrgfLF1kX-efdQdWoZfegcxzRIF9-E,5096
4
- python_som-0.0.1a2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
5
- python_som-0.0.1a2.dist-info/top_level.txt,sha256=__rpeARBlUvNEMA9BWjqS7nbMeb_3JUR_QQndHHo5Mk,11
6
- python_som-0.0.1a2.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- python_som