python-som 0.0.1a1__py3-none-any.whl → 0.1.1__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,13 +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(a: Union[float, np.ndarray], b: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
63
- """
64
- 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.
65
100
 
66
101
  The parameters a and b may be n-dimensional, but their shapes must be capable of broadcasting
67
102
 
68
- 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.
69
105
 
70
106
  :param a: array-like: list or numpy array of values. a must not be a scalar value.
71
107
  :param b: array-like: list or numpy array of values. b must not be a scalar value.
@@ -75,9 +111,11 @@ def _euclidean_distance(a: Union[float, np.ndarray], b: Union[float, np.ndarray]
75
111
 
76
112
 
77
113
  class SOM:
78
- """
79
- Implementation of the 2D self-organizing map, with support for NumPy arrays and Pandas DataFrames.
80
- 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.
81
119
 
82
120
  Features:
83
121
  - Stepwise and batch training
@@ -101,25 +139,28 @@ class SOM:
101
139
  Pages 52-65,
102
140
  ISSN 0893-6080,
103
141
  https://doi.org/10.1016/j.neunet.2012.09.018.
104
-
105
142
  """
106
143
 
107
144
  def __init__(
108
- self,
109
- x: Union[int, None],
110
- y: Union[int, None],
111
- input_len: int,
112
- learning_rate: float = 0.5,
113
- learning_rate_decay: Callable[[float, int, int], float] = _asymptotic_decay,
114
- neighborhood_radius: float = 1.0,
115
- neighborhood_radius_decay: Callable[[float, int, int], float] = _asymptotic_decay,
116
- neighborhood_function: str = 'gaussian',
117
- distance_function: Callable[
118
- [Union[float, np.ndarray], Union[float, np.ndarray]], Union[float, np.ndarray]] = _euclidean_distance,
119
- cyclic_x: bool = False,
120
- cyclic_y: bool = False,
121
- random_seed: Union[int, None] = None,
122
- data: Union[np.ndarray, pd.DataFrame, list, None] = None,
145
+ self,
146
+ x: int | None,
147
+ y: int | None,
148
+ input_len: int,
149
+ learning_rate: float = 0.5,
150
+ learning_rate_decay: Callable[[float, int, int], float] = _asymptotic_decay,
151
+ neighborhood_radius: float = 1.0,
152
+ neighborhood_radius_decay: Callable[
153
+ [float, int, int], float
154
+ ] = _asymptotic_decay,
155
+ neighborhood_function: str = "gaussian",
156
+ distance_function: Callable[
157
+ [np.ndarray, np.ndarray],
158
+ np.ndarray,
159
+ ] = _euclidean_distance,
160
+ cyclic_x: bool = False,
161
+ cyclic_y: bool = False,
162
+ random_seed: int | None = None,
163
+ data: np.ndarray | pd.DataFrame | list | None = None,
123
164
  ) -> None:
124
165
  """
125
166
  Constructor for the self-organizing map class.
@@ -127,13 +168,13 @@ class SOM:
127
168
  :param x: int or NoneType: X dimension of the self-organizing map, i.e.,
128
169
  number of rows of the matrix of weights.
129
170
  x should be larger than 0.
130
- If x is None and 'data' is provided in kwargs, its value will be automatically selected using PCA of 'data'.
131
- 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.
132
173
  :param y: int or NoneType: Y dimension of the self-organizing map, i.e.,
133
174
  number of columns of the matrix of weights.
134
175
  y should be larger than 0.
135
- If y is None and 'data' is provided in kwargs, its value will be automatically selected using PCA of 'data'.
136
- 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.
137
178
  :param input_len: int: Number of features of the training dataset, i.e.,
138
179
  number of elements of each node of the network.
139
180
  :param learning_rate: float: Initial learning rate for the training process.
@@ -141,23 +182,24 @@ class SOM:
141
182
  Defaults to 0.5.
142
183
  Note: The value of the learning_rate is irrelevant for the 'batch' training mode.
143
184
  :param learning_rate_decay: function: Decay function for the learning_rate variable.
144
- May be a predefined one from this package, or a custom function, with the same parameters and return type.
145
- Defaults to _asymptotic_decay.
146
- :param neighborhood_radius: float: Initial neighborhood radius for the training process. Defaults to 1.
147
- :param neighborhood_radius_decay: function: Decay function for the neighborhood_radius variable.
148
- May be a predefined one from this package, or a custom function, with the same parameters and return type.
149
- 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
150
192
  :param neighborhood_function: str: Neighborhood function name for the training process.
151
193
  May be either 'gaussian' or 'bubble'.
152
- :param distance_function: function: Function for calculating distances/dissimilarities between models of the
153
- network.
154
- May be a predefined one from this package, or a custom function, with the same parameters and return type.
155
- Defaults to _euclidean_distance.
156
- :param cyclic_x: bool: Boolean value activate/deactivate cyclic arrays in the x direction, i.e,
157
- 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.
158
200
  Defaults to False.
159
- :param cyclic_y: bool: Boolean value activate/deactivate cyclic arrays in the y direction, i.e,
160
- 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.
161
203
  Defaults to False.
162
204
  :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
163
205
  :param data: array-like: dataset for performing PCA.
@@ -165,21 +207,25 @@ class SOM:
165
207
  """
166
208
  # Verifying map dimensions (initializing automatically, if a dataset is provided)
167
209
  if (x, y) == (None, None):
168
- raise ValueError('At least one of the dimensions (x, y) must be specified')
210
+ raise ValueError("At least one of the dimensions (x, y) must be specified")
169
211
  if x is None or y is None:
170
212
  # If a dataset was given through **kwargs, select missing dimension with PCA
171
- # The ratio of the (x, y) sizes will comply roughly with the ratio of the two largest principal components
172
- if 'data' is 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:
173
216
  raise ValueError(
174
- "If one of the dimensions is not specified, a dataset must be provided for automatic size "
175
- "initialization.")
217
+ "If one of the dimensions is not specified,"
218
+ "a dataset must be provided for automatic size initialization."
219
+ )
176
220
  # Convert data to numpy array
177
221
  if isinstance(data, pd.DataFrame):
178
222
  data_array = data.to_numpy()
179
223
  else:
180
224
  data_array = np.array(data)
181
225
  # Perform PCA w/ sklearn
182
- data_array = sklearn.preprocessing.StandardScaler().fit_transform(data_array)
226
+ data_array = sklearn.preprocessing.StandardScaler().fit_transform(
227
+ data_array
228
+ )
183
229
  pca = sklearn.decomposition.PCA(n_components=2)
184
230
  pca.fit(data_array)
185
231
  ratio = pca.explained_variance_[0] / pca.explained_variance_[1]
@@ -192,30 +238,31 @@ class SOM:
192
238
  # Initializing private variables
193
239
  self._shape = (np.uint(x), np.uint(y))
194
240
  self._input_len = np.uint(input_len)
195
- self._learning_rate = np.float(learning_rate)
241
+ self._learning_rate = float(learning_rate)
196
242
  self._learning_rate_decay = learning_rate_decay
197
- self._neighborhood_radius = np.float(neighborhood_radius)
243
+ self._neighborhood_radius = float(neighborhood_radius)
198
244
  self._neighborhood_radius_decay = neighborhood_radius_decay
199
245
  self._neighborhood_function = {
200
- 'gaussian': self._gaussian,
201
- 'bubble': self._bubble}[neighborhood_function]
246
+ "gaussian": self._gaussian,
247
+ "bubble": self._bubble,
248
+ }[neighborhood_function]
202
249
  self._distance_function = distance_function
203
- self._cyclic = (np.bool(cyclic_x), np.bool(cyclic_y))
250
+ self._cyclic = (bool(cyclic_x), bool(cyclic_y))
204
251
  self._neigx, self._neigy = np.arange(self._shape[0]), np.arange(self._shape[1])
205
252
 
206
253
  # Seed numpy random generator
207
254
  if random_seed is None:
208
- self._random_seed = np.random.randint(
209
- np.random.randint(np.iinfo(np.int32).max))
255
+ self._random_seed = np.random.randint(np.iinfo(np.int32).max)
210
256
  else:
211
- self._random_seed = np.int(random_seed)
257
+ self._random_seed = int(random_seed)
212
258
  np.random.seed(self._random_seed)
213
259
 
214
260
  # Random weight initialization
215
261
  self._weights = np.random.standard_normal(
216
- size=(self._shape[0], self._shape[1], self._input_len))
262
+ size=(self._shape[0], self._shape[1], self._input_len)
263
+ )
217
264
 
218
- def get_shape(self) -> Tuple[int, int]:
265
+ def get_shape(self) -> tuple[np.uint, np.uint]:
219
266
  """
220
267
  Gets the shape of the network.
221
268
 
@@ -237,17 +284,17 @@ class SOM:
237
284
 
238
285
  :param learning_rate: float: New value for learning_rate of an instance of the SOM.
239
286
  """
240
- self._learning_rate = np.float(learning_rate)
287
+ self._learning_rate = float(learning_rate)
241
288
 
242
289
  def set_neighborhood_radius(self, neighborhood_radius: float) -> None:
243
290
  """
244
291
  Sets the neighborhood_radius member of the SOM.
245
292
 
246
- :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.
247
294
  """
248
- self._neighborhood_radius = np.float(neighborhood_radius)
295
+ self._neighborhood_radius = float(neighborhood_radius)
249
296
 
250
- def activate(self, x: Union[np.ndarray, pd.DataFrame, list]) -> np.ndarray:
297
+ def activate(self, x: np.ndarray) -> np.ndarray:
251
298
  """
252
299
  Calculates distances between an instance x and the weights of the network.
253
300
 
@@ -256,7 +303,7 @@ class SOM:
256
303
  """
257
304
  return self._distance_function(x, self._weights)
258
305
 
259
- def winner(self, x: Union[np.ndarray, pd.DataFrame, list]) -> Union[Iterable, Tuple[int, int]]:
306
+ def winner(self, x: np.ndarray) -> tuple[int, ...]:
260
307
  """
261
308
  Calculates the best-matching unit of the network for an instance x
262
309
 
@@ -264,26 +311,31 @@ class SOM:
264
311
  :return: (int, int): Index of the best-matching unit of x.
265
312
  """
266
313
  activation_map = self.activate(x)
267
- 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
268
318
 
269
- def quantization(self, data: Union[np.ndarray, pd.DataFrame, list]) -> np.ndarray:
319
+ def quantization(self, data: np.ndarray | pd.DataFrame | list) -> np.ndarray:
270
320
  """
271
321
  Calculates distances from each instance of 'data' to each of the weights of the network.
272
322
 
273
323
  :param data: array-like: Dataset to be compared with the weights of the network.
324
+ Expected shape is (n_samples, n_features).
274
325
  :return: np.ndarray: array of lists of distances from each instance of the dataset
275
326
  to each weight of the network.
276
327
  """
277
328
  # Convert data to numpy array
278
- if isinstance(data, pd.DataFrame):
279
- data_array = data.to_numpy()
280
- else:
281
- data_array = np.array(data)
282
- return np.array([(self._distance_function(i, self._weights[self.winner(i)])) for i in data_array])
283
-
284
- def quantization_error(self, data: Union[np.ndarray, pd.DataFrame, list]) -> float:
285
- """
286
- Calculates average distance of the weights of the network to their assigned instances from data.
329
+ data_array = self._data_to_numpy(data)
330
+ return np.array(
331
+ [
332
+ (self._distance_function(i, self._weights[self.winner(i)]))
333
+ for i in data_array
334
+ ]
335
+ )
336
+
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.
287
339
  This error is a quality measure for the training process.
288
340
 
289
341
  :param data: array-like: Dataset to be compared with the weights of the network.
@@ -292,43 +344,52 @@ class SOM:
292
344
  quantization = self.quantization(data)
293
345
  return quantization.mean()
294
346
 
295
- def distance_matrix(self) -> np.ndarray:
347
+ def distance_matrix(self, normalize: bool = False) -> np.ndarray:
296
348
  """
297
- Calculates U-matrix of the current state of the network, i.e., the matrix of distances between each node and its
298
- 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
299
352
 
353
+ :param normalize: bool: Activate to normalize the U-matrix between 0 and 1.
354
+ Defaults to False.
300
355
  :return: np.ndarray: U-matrix of the current state of the network.
301
356
  """
302
357
  um = np.zeros(shape=self._shape)
303
- it = np.nditer(um, flags=['multi_index'])
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
+
304
366
  while not it.finished:
305
367
  update_matrix = self._bubble(it.multi_index, 1)
306
- um[it.multi_index] = np.sum(
307
- update_matrix * self._distance_function(self._weights[it.multi_index], self._weights))
368
+ um[it.multi_index] = np.sum(update_matrix * distances[it.multi_index])
308
369
  it.iternext()
309
- 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))
310
373
  return um
311
374
 
312
- def activation_matrix(self, data: Union[np.ndarray, pd.DataFrame, list]) -> np.ndarray:
313
- """
314
- Calculates the activation matrix of the network for a dataset, i.e., for each node, the count of instances that
315
- 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.
316
380
 
317
381
  :param data: array-like: Dataset to be compared with the weights of the network.
318
382
  :return: np.ndarray: Activation matrix.
319
383
  """
320
384
  # Convert data to numpy array
321
- if isinstance(data, pd.DataFrame):
322
- data_array = data.to_numpy()
323
- else:
324
- data_array = np.array(data)
385
+ data_array = self._data_to_numpy(data)
325
386
 
326
387
  activation_matrix = np.zeros(self._shape)
327
388
  for i in data_array:
328
389
  activation_matrix[self.winner(i)] += 1
329
390
  return activation_matrix
330
391
 
331
- def winner_map(self, data: Union[np.ndarray, pd.DataFrame, list]) -> dict:
392
+ def winner_map(self, data: np.ndarray | pd.DataFrame | list) -> dict:
332
393
  """
333
394
  Calculates, for each node (i, j) of the network,
334
395
  the list of all instances from 'data' that has been assigned to it.
@@ -337,44 +398,55 @@ class SOM:
337
398
  :return: dict: Winner map.
338
399
  """
339
400
  # Convert data to numpy array
340
- if isinstance(data, pd.DataFrame):
341
- data_array = data.to_numpy()
342
- else:
343
- data_array = np.array(data)
344
- winner_map = {(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)
404
+ }
345
405
  for i in data_array:
346
406
  winner_map[self.winner(i)].append(i)
347
407
  return winner_map
348
408
 
349
- def label_map(self, data: Union[np.ndarray, pd.DataFrame, list],
350
- labels: Union[np.ndarray, pd.DataFrame, list]) -> dict:
351
- """
352
- Calculates, for each node (i, j) of the network, the frequency of each label from 'labels' corresponding to its
353
- respective instance from 'data' that has been assigned to this node.
409
+ def label_map(
410
+ self,
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
+
354
419
  :param data: array-like: Dataset to be compared with the weights of the network.
355
420
  :param labels: array-like: Labels corresponding to the indices of 'data'.
356
421
  :return: dict: Label map.
357
422
  """
358
423
  # Convert data to numpy array
359
- if isinstance(data, pd.DataFrame):
360
- data_array = data.to_numpy()
361
- else:
362
- data_array = np.array(data)
424
+ data_array = self._data_to_numpy(data)
363
425
  # Convert labels to numpy array
364
426
  if isinstance(labels, pd.DataFrame):
365
427
  labels = labels.to_numpy()
366
428
  else:
367
429
  labels = np.array(labels)
368
430
 
369
- winner_map = {(i, j): [] for i in range(self._shape[0]) for j in range(self._shape[1])}
370
- for i in range(len(data_array)):
371
- winner_map[self.winner(data_array[i])].append(labels[i])
372
- for key in winner_map:
373
- winner_map[key] = Counter(winner_map[key])
374
- return winner_map
375
-
376
- def train(self, data: Union[np.ndarray, pd.DataFrame, list], n_iteration: Union[int, None] = None,
377
- mode: str = 'random', verbose: bool = False) -> float:
431
+ winner_map: dict[tuple[int, ...], list] = {
432
+ tuple(index): [] for index in np.ndindex(self._shape)
433
+ }
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
442
+
443
+ def train(
444
+ self,
445
+ data: np.ndarray | pd.DataFrame | list,
446
+ n_iteration: int | None = None,
447
+ mode: str = "random",
448
+ verbose: bool = False,
449
+ ) -> float:
378
450
  """
379
451
  Trains the self-organizing map, with the dataset 'data', and a certain number of iterations.
380
452
 
@@ -383,163 +455,268 @@ class SOM:
383
455
  If None, defaults to 1000 * len(data) for stepwise training modes,
384
456
  or 10 * len(data) for batch training mode.
385
457
  :param mode: str: Training mode name. May be either 'random', 'sequential', or 'batch'.
386
- For 'batch' mode, a much smaller number of iterations is needed, but a higher computation power is required
387
- 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.
388
460
  :param verbose: bool: Activate to print useful information to the terminal/console, e.g.,
389
461
  the progress of the training process
390
462
  :return: float: Quantization error after training
391
463
  """
392
464
  # Convert data to numpy array for training
393
- if isinstance(data, pd.DataFrame):
394
- data_array = data.to_numpy()
395
- else:
396
- data_array = np.array(data)
465
+ data_array = self._data_to_numpy(data)
397
466
 
398
467
  # If no number of iterations is given, select automatically
399
468
  if n_iteration is None:
400
- n_iteration = {'random': 1000, 'sequential': 1000, 'batch': 10}[mode] * len(data_array)
469
+ n_iteration = {"random": 1000, "sequential": 1000, "batch": 10}[mode] * len(
470
+ data_array
471
+ )
401
472
 
402
473
  if verbose:
403
- print("Training with", n_iteration,
404
- "iterations.\nTraining mode:", mode, sep=' ')
405
- if mode == 'random':
406
- # Random sampling from training dataset
407
- for it, i in enumerate(
408
- np.random.choice(len(data_array), size=n_iteration, replace=(n_iteration > len(data)))):
409
- # Calculating decaying alpha and sigma parameters for updating weights
410
- alpha = self._learning_rate_decay(self._learning_rate, it, n_iteration)
411
- sigma = self._neighborhood_radius_decay(
412
- self._neighborhood_radius, it, n_iteration)
413
-
414
- # Finding winner node (best-matching unit)
415
- winner = self.winner(data_array[i])
416
-
417
- # Updating weights, based on current neighborhood function
418
- self._weights += alpha * self._neighborhood_function(winner, sigma)[..., None] * (
419
- data_array[i] - self._weights)
420
-
421
- # Print progress, if verbose is activated
422
- if verbose:
423
- print("Iteration:", it, "/", n_iteration, sep=' ', end='\r', flush=True)
424
- elif mode == 'sequential':
425
- # Sequential sampling from training dataset
426
- for it, i in enumerate(data_array):
427
- # Calculating decaying alpha and sigma parameters for updating weights
428
- alpha = self._learning_rate_decay(self._learning_rate, it, n_iteration)
429
- sigma = self._neighborhood_radius_decay(
430
- self._neighborhood_radius, it, n_iteration)
431
-
432
- # Finding winner node (best-matching unit)
433
- winner = self.winner(i)
434
-
435
- # Updating weights, based on current neighborhood function
436
- self._weights += alpha * self._neighborhood_function(winner, sigma)[..., None] * (
437
- i - self._weights)
438
-
439
- # Print progress, if verbose is activated
440
- if verbose:
441
- print("Iteration:", it, "/", n_iteration, sep=' ', end='\r', flush=True)
442
- elif mode == 'batch':
443
- # Batch training
444
- for it in range(n_iteration):
445
- # Calculating decaying sigma
446
- sigma = self._neighborhood_radius_decay(
447
- self._neighborhood_radius, it, n_iteration)
448
-
449
- # For each node, create a list of instances associated to it
450
- winner_map = self.winner_map(data_array)
451
-
452
- # Calculate the weighted average of all instances in the neighborhood of each node
453
- new_weights = np.zeros(self._weights.shape)
454
- for i in winner_map.keys():
455
- neig = self._neighborhood_function(i, sigma)
456
- upper, bottom = np.zeros(self._input_len), 0.0
457
- for j in winner_map.keys():
458
- upper += neig[j] * np.sum(winner_map[j], axis=0)
459
- bottom += neig[j] * len(winner_map[j])
460
-
461
- # Only update if there is any instance associated with the winner node or its neighbors
462
- if bottom != 0:
463
- new_weights[i] = upper / bottom
464
-
465
- # Update all nodes concurrently
466
- self._weights = new_weights
467
-
468
- # Print progress, if verbose is activated
469
- if verbose:
470
- print("Iteration:", it, "/", n_iteration, sep=' ', end='\r', flush=True)
474
+ print(
475
+ "Training with",
476
+ n_iteration,
477
+ "iterations.\nTraining mode:",
478
+ mode,
479
+ sep=" ",
480
+ )
481
+
482
+ if mode == "random":
483
+ self._train_random(data_array, n_iteration, verbose)
484
+ elif mode == "sequential":
485
+ self._train_sequential(data_array, n_iteration, verbose)
486
+ elif mode == "batch":
487
+ self._train_batch(data_array, n_iteration, verbose)
471
488
  else:
472
489
  # Invalid training mode value
473
490
  raise ValueError(
474
- "Invalid value for 'mode' parameter. Value should be in " + str(['random', 'sequential', 'batch']))
491
+ "Invalid value for 'mode' parameter. Value should be in "
492
+ + str(["random", "sequential", "batch"])
493
+ )
475
494
 
476
495
  # Compute quantization error
477
496
  q_error = self.quantization_error(data_array)
478
497
  if verbose:
479
- print("Quantization error:", q_error, sep=' ')
498
+ print("Quantization error:", q_error, sep=" ")
480
499
  return q_error
481
500
 
482
- def weight_initialization(self, mode: str = 'random',
483
- **kwargs: Union[np.ndarray, pd.DataFrame, list, str, int]) -> None:
484
- """
485
- Function for weight initialization of the self-organizing map. Calls other methods for each initialization mode.
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
+
641
+ def weight_initialization(
642
+ self,
643
+ mode: str = "random",
644
+ **kwargs: np.ndarray | pd.DataFrame | list | str | int
645
+ ) -> None:
646
+ """Function for weight initialization of the self-organizing map.
647
+
648
+ Calls other methods for each initialization mode.
486
649
 
487
650
  :param mode: str: Initialization mode. May be either 'random', 'linear', or 'sample'.
488
651
  Note: Each initialization method may require multiple additional arguments in kwargs.
489
652
  :param kwargs:
490
- For 'random' initialization mode, 'sample_mode': str may be provided to determine the sampling mode.
491
- 'sample_mode' may be either 'standard_normal' (default) or 'uniform'.
492
- For 'random' and 'sample' modes, 'random_seed': int may be provided for the random value generator.
493
- For 'sample' and 'linear' modes, 'data': array-like must be provided for sampling/PCA.
494
- """
495
- modes = {'random': self._weight_initialization_random, 'linear': self._weight_initialization_linear,
496
- 'sample': self._weight_initialization_sample}
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.
658
+ """
659
+ modes: dict[str, Callable[..., None]] = {
660
+ "random": self._weight_initialization_random,
661
+ "linear": self._weight_initialization_linear,
662
+ "sample": self._weight_initialization_sample,
663
+ }
497
664
  try:
498
665
  modes[mode](**kwargs)
499
- except KeyError:
500
- raise ValueError("Invalid value for 'mode' parameter. Value should be in " + str(modes.keys()))
666
+ except KeyError as exc:
667
+ raise ValueError(
668
+ "Invalid value for 'mode' parameter. Value should be in "
669
+ + str(modes.keys())
670
+ ) from exc
501
671
 
502
- def _weight_initialization_random(self, sample_mode: str = 'standard_normal',
503
- random_seed: Union[int, None] = None) -> None:
504
- """
505
- Random initialization method. Assigns weights from a random distribution defined by 'sample_mode'.
672
+ def _weight_initialization_random(
673
+ self, sample_mode: str = "standard_normal", random_seed: int | None = None
674
+ ) -> None:
675
+ """Random initialization method.
506
676
 
507
- :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'.
508
681
  Defaults to 'standard_normal'.
509
682
  :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
510
683
  """
511
- sample_modes = {'uniform': np.random.random, 'standard_normal': np.random.standard_normal}
684
+ sample_modes = {
685
+ "uniform": np.random.random,
686
+ "standard_normal": np.random.standard_normal,
687
+ }
512
688
 
513
689
  # Seed numpy random generator
514
690
  if random_seed is None:
515
- random_seed = np.random.randint(
516
- np.random.randint(np.iinfo(np.int32).max))
691
+ random_seed = np.random.randint(np.iinfo(np.int32).max)
517
692
  else:
518
- random_seed = np.int(random_seed)
693
+ random_seed = int(random_seed)
519
694
  np.random.seed(random_seed)
520
695
 
521
696
  # Initialize weights randomly
522
697
  try:
523
698
  self._weights = sample_modes[sample_mode](size=self._weights.shape)
524
- except KeyError:
699
+ except KeyError as exc:
525
700
  raise ValueError(
526
- "Invalid value for 'sample_mode' parameter. Value should be in " + str(sample_modes.keys()))
701
+ "Invalid value for 'sample_mode' parameter. Value should be in "
702
+ + str(sample_modes.keys())
703
+ ) from exc
527
704
 
528
- def _weight_initialization_linear(self, data: Union[np.ndarray, pd.DataFrame, list]) -> None:
529
- """
530
- Linear initialization method. Assigns weights spanning the hyperplane formed by the two first principal
705
+ def _weight_initialization_linear(
706
+ self, data: np.ndarray | pd.DataFrame | list
707
+ ) -> None:
708
+ """Linear initialization method.
709
+
710
+ Assigns weights spanning the hyperplane formed by the two first principal
531
711
  components of 'data'.
532
712
 
533
- This is the recommended initialization method, as it may lead to a faster convergence. Unlike other
534
- 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.
535
715
 
536
716
  :param data: array-like: Dataset for weight initialization w/ PCA.
537
717
  """
538
718
  # Convert data to numpy array for training
539
- if isinstance(data, pd.DataFrame):
540
- data_array = data.to_numpy()
541
- else:
542
- data_array = np.array(data)
719
+ data_array = self._data_to_numpy(data)
543
720
 
544
721
  # Perform PCA w/ sklearn
545
722
  data_array = sklearn.preprocessing.StandardScaler().fit_transform(data_array)
@@ -548,37 +725,40 @@ class SOM:
548
725
  # Initialize weights spanning first 2 principal components of data
549
726
  for i, c1 in enumerate(np.linspace(-1, 1, num=self._shape[0])):
550
727
  for j, c2 in enumerate(np.linspace(-1, 1, num=self._shape[1])):
551
- self._weights[i, j] = c1 * pca.explained_variance_[0] + c2 * pca.explained_variance_[1]
552
-
553
- def _weight_initialization_sample(self, data: Union[np.ndarray, pd.DataFrame, list],
554
- random_seed: Union[int, None] = None) -> None:
555
- """
556
- Initialization method. Assigns weights to random samples from an input dataset.
728
+ self._weights[i, j] = (
729
+ c1 * pca.explained_variance_[0] + c2 * pca.explained_variance_[1]
730
+ )
731
+
732
+ def _weight_initialization_sample(
733
+ self,
734
+ data: np.ndarray | pd.DataFrame | list,
735
+ random_seed: int | None = None,
736
+ ) -> None:
737
+ """Initialization method. Assigns weights to random samples from an input dataset.
557
738
 
558
739
  :param data: Dataset for weight initialization/sampling.
559
740
  :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
560
741
  """
561
742
  # Seed numpy random generator
562
743
  if random_seed is None:
563
- random_seed = np.random.randint(
564
- np.random.randint(np.iinfo(np.int32).max))
744
+ random_seed = np.random.randint(np.iinfo(np.int32).max)
565
745
  else:
566
- random_seed = np.int(random_seed)
746
+ random_seed = int(random_seed)
567
747
  np.random.seed(random_seed)
568
748
 
569
749
  # Convert data to numpy array for training
570
- if isinstance(data, pd.DataFrame):
571
- data_array = data.to_numpy()
572
- else:
573
- data_array = np.array(data)
750
+ data_array = self._data_to_numpy(data)
574
751
 
575
752
  # Assign weights to random samples from dataset
576
- sample_size = self._shape[0] * self._shape[1]
577
- sample = np.random.choice(len(data_array), size=sample_size,
578
- replace=(sample_size > len(data_array)))
753
+ sample_size: np.uint = self._shape[0] * self._shape[1]
754
+ sample = np.random.choice(
755
+ len(data_array),
756
+ size=int(sample_size),
757
+ replace=bool(sample_size > len(data_array)),
758
+ )
579
759
  self._weights = data_array[sample].reshape(self._weights.shape)
580
760
 
581
- def _gaussian(self, c: Tuple[int, int], sigma: float) -> np.ndarray:
761
+ def _gaussian(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
582
762
  """
583
763
  Gaussian neighborhood function, centered in c. Has support for cyclic arrays.
584
764
 
@@ -601,12 +781,12 @@ class SOM:
601
781
  ay = np.exp(-np.power(dy, 2) / d)
602
782
  return np.outer(ax, ay)
603
783
 
604
- def _bubble(self, c: Tuple[int, int], sigma: float) -> np.ndarray:
784
+ def _bubble(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
605
785
  """
606
786
  Bubble neighborhood function, centered in c. Has support for cyclic arrays.
607
787
 
608
- The neighbors of c are the nodes in the region of sigma positions in the vertical and horizontal directions
609
- 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.
610
790
 
611
791
  :param c: (int, int): Center coordinates for gaussian function.
612
792
  :param sigma: float: Spread variable for gaussian function.
@@ -622,13 +802,24 @@ class SOM:
622
802
  # Calculate cyclic regions
623
803
  if self._cyclic[0]:
624
804
  if c[0] - sigma < 0:
625
- ax[int(c[0] - sigma):] = True
805
+ ax[int(c[0] - sigma) :] = True
626
806
  if c[0] + sigma >= self._shape[0]:
627
- ax[:int((c[0] + sigma) % self._shape[0] + 1)] = True
807
+ ax[: int((c[0] + sigma) % self._shape[0] + 1)] = True
628
808
  if self._cyclic[1]:
629
809
  if c[1] - sigma < 0:
630
- ay[int(c[1] - sigma):] = True
810
+ ay[int(c[1] - sigma) :] = True
631
811
  if c[1] + sigma >= self._shape[1]:
632
- ay[:int((c[1] + sigma) % self._shape[1] + 1)] = True
812
+ ay[: int((c[1] + sigma) % self._shape[1] + 1)] = True
633
813
 
634
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,23 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-som
3
- Version: 0.0.1a1
3
+ Version: 0.1.1
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
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
21
20
  Description-Content-Type: text/markdown
22
- License-File: LICENSE
23
- Requires-Dist: numpy
24
- Requires-Dist: pandas
25
- Requires-Dist: scikit-learn
26
21
 
27
22
  # python-som
28
23
 
@@ -144,4 +139,4 @@ Volume 37,
144
139
  2013,
145
140
  Pages 52-65,
146
141
  ISSN 0893-6080,
147
- https://doi.org/10.1016/j.neunet.2012.09.018.
142
+ 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.1.dist-info/METADATA,sha256=YyBET88cyw79ltVyrYUNXOagJpdjgaYaw8ii6RhcyLM,4957
3
+ python_som-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
+ python_som-0.1.1.dist-info/licenses/LICENSE,sha256=dMZQho8iIicWbAJQk1QaAXXvvJlaWSQ3a-9NkWxu_lc,1077
5
+ python_som-0.1.1.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=1s6ryY2Og0N9MnX98lk6TaCTogan6d3Dhwc3fX5R5gg,29200
2
- python_som-0.0.1a1.dist-info/LICENSE,sha256=dMZQho8iIicWbAJQk1QaAXXvvJlaWSQ3a-9NkWxu_lc,1077
3
- python_som-0.0.1a1.dist-info/METADATA,sha256=QGrBTJ2eWkQU_4TKxsGZGhw1qlVslZPwr3UF_Ok4lSw,5096
4
- python_som-0.0.1a1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
5
- python_som-0.0.1a1.dist-info/top_level.txt,sha256=__rpeARBlUvNEMA9BWjqS7nbMeb_3JUR_QQndHHo5Mk,11
6
- python_som-0.0.1a1.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- python_som