python-som 0.1.3__py3-none-any.whl → 0.3.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
@@ -1,825 +1,58 @@
1
- """This module contains the implementation of the 2D self-organizing map.
1
+ """Python implementation of Kohonen's 2-D self-organizing map.
2
2
 
3
- Most features were implemented using NumPy, with Scikit-learn for standardization and PCA.
3
+ The public surface is :class:`SOM`, plus the decay, distance and neighborhood functions that can be
4
+ passed to it or used directly.
4
5
 
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
6
+ >>> import numpy as np, python_som
7
+ >>> data = np.random.default_rng(0).normal(size=(150, 4))
8
+ >>> som = python_som.SOM(x=10, y=10, input_len=4, random_seed=0)
9
+ >>> som.weight_initialization(mode="linear", data=data)
10
+ >>> error = som.train(data, n_iteration=100, mode="batch")
17
11
 
18
12
  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.
13
+ Teuvo Kohonen, Essentials of the self-organizing map, Neural Networks 37 (2013) 52-65,
14
+ ISSN 0893-6080, https://doi.org/10.1016/j.neunet.2012.09.018
27
15
  """
28
16
 
29
- # %%
30
- from collections import Counter
31
- from typing import Callable
32
-
33
- import numpy as np
34
- import pandas as pd
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
45
-
46
-
47
- # %%
48
- def _asymptotic_decay(x: float, t: int, max_t: int) -> float:
49
- """
50
- Asymptotic decay function. Can be used for both the learning_rate or the neighborhood_radius.
51
-
52
- :param x: float: Initial x parameter
53
- :param t: int: Current iteration
54
- :param max_t: int: Maximum number of iterations
55
- :return: float: Current state of x after t iterations
56
- """
57
- return x / (1 + t / (max_t / 2))
58
-
59
-
60
- def _linear_decay(x: float, t: int, max_t: int) -> float:
61
- """
62
- Linear decay function. Can be used for both the learning_rate or the neighborhood_radius.
63
-
64
- :param x: float: Initial x parameter
65
- :param t: int: Current iteration
66
- :param max_t: int: Maximum number of iterations
67
- :return: float: Current state of x after t iterations
68
- """
69
- return x * (1.0 - t / max_t)
70
-
71
-
72
- def _exponential_decay(x: float, t: int, max_t: int, factor: float = 2.0) -> float:
73
- """
74
- Exponential decay function. Can be used for both the learning_rate or the neighborhood_radius.
75
-
76
- :param x: float: Initial x parameter
77
- :param t: int: Current iteration
78
- :param max_t: int: Maximum number of iterations
79
- :param factor: float: Exponential decay factor. Defaults to 2.0.
80
- :return: float: Current state of x after t iterations
81
- """
82
- return x * (1 - (factor / max_t)) ** t
83
-
84
-
85
- def _inverse_decay(x: float, t: int, max_t: int) -> float:
86
- """
87
- Inverse decay function. Can be used for both the learning_rate or the neighborhood_radius.
88
-
89
- :param x: float: Initial x parameter
90
- :param t: int: Current iteration
91
- :param max_t: int: Maximum number of iterations
92
- :return: float: Current state of x after t iterations
93
- """
94
- return (max_t / 100) * x / ((max_t / 100) + t)
95
-
96
-
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.
100
-
101
- The parameters a and b may be n-dimensional, but their shapes must be capable of broadcasting
102
-
103
- The shape of the output complies to the first n-1 dimensions of the parameter with the highest
104
- dimensionality.
105
-
106
- :param a: array-like: list or numpy array of values. a must not be a scalar value.
107
- :param b: array-like: list or numpy array of values. b must not be a scalar value.
108
- :return: array-like: An array of euclidean distances between a and b.
109
- """
110
- return np.linalg.norm(np.subtract(a, b), ord=2, axis=-1)
111
-
112
-
113
- class SOM:
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.
119
-
120
- Features:
121
- - Stepwise and batch training
122
- - Random weight initialization
123
- - Random sampling weight initialization
124
- - Linear weight initialization (with PCA)
125
- - Automatic selection of map size ratio (with PCA)
126
- - Support for cyclic arrays, for toroidal or spherical maps
127
- - Gaussian and Bubble neighborhood functions
128
- - Support for custom decay functions
129
- - Support for visualization (U-matrix, activation matrix)
130
- - Support for supervised learning (label map)
131
- - Support for NumPy arrays, Pandas DataFrames and regular lists of values
132
-
133
- Reference:
134
- Teuvo Kohonen,
135
- Essentials of the self-organizing map,
136
- Neural Networks,
137
- Volume 37,
138
- 2013,
139
- Pages 52-65,
140
- ISSN 0893-6080,
141
- https://doi.org/10.1016/j.neunet.2012.09.018.
142
- """
143
-
144
- def __init__(
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,
164
- ) -> None:
165
- """
166
- Constructor for the self-organizing map class.
167
-
168
- :param x: int or NoneType: X dimension of the self-organizing map, i.e.,
169
- number of rows of the matrix of weights.
170
- x should be larger than 0.
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.
173
- :param y: int or NoneType: Y dimension of the self-organizing map, i.e.,
174
- number of columns of the matrix of weights.
175
- y should be larger than 0.
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.
178
- :param input_len: int: Number of features of the training dataset, i.e.,
179
- number of elements of each node of the network.
180
- :param learning_rate: float: Initial learning rate for the training process.
181
- Should be a positive floating point value.
182
- Defaults to 0.5.
183
- Note: The value of the learning_rate is irrelevant for the 'batch' training mode.
184
- :param learning_rate_decay: function: Decay function for the learning_rate variable.
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
192
- :param neighborhood_function: str: Neighborhood function name for the training process.
193
- May be either 'gaussian' or 'bubble'.
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.
200
- Defaults to False.
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.
203
- Defaults to False.
204
- :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
205
- :param data: array-like: dataset for performing PCA.
206
- Required when either x or y is None, for determining map size.
207
- """
208
- # Verifying map dimensions (initializing automatically, if a dataset is provided)
209
- if (x, y) == (None, None):
210
- raise ValueError("At least one of the dimensions (x, y) must be specified")
211
- if x is None or y is None:
212
- # If a dataset was given through **kwargs, select missing dimension with PCA
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:
216
- raise ValueError(
217
- "If one of the dimensions is not specified,"
218
- "a dataset must be provided for automatic size initialization."
219
- )
220
- # Convert data to numpy array
221
- if isinstance(data, pd.DataFrame):
222
- data_array = data.to_numpy()
223
- else:
224
- data_array = np.array(data)
225
- # Perform PCA w/ sklearn
226
- data_array = sklearn.preprocessing.StandardScaler().fit_transform(
227
- data_array
228
- )
229
- pca = sklearn.decomposition.PCA(n_components=2)
230
- pca.fit(data_array)
231
- ratio = pca.explained_variance_[0] / pca.explained_variance_[1]
232
- # Update missing size variable
233
- if x is None:
234
- x = y // ratio
235
- if y is None:
236
- y = x // ratio
237
-
238
- # Initializing private variables
239
- self._shape = (np.uint(x), np.uint(y))
240
- self._input_len = np.uint(input_len)
241
- self._learning_rate = float(learning_rate)
242
- self._learning_rate_decay = learning_rate_decay
243
- self._neighborhood_radius = float(neighborhood_radius)
244
- self._neighborhood_radius_decay = neighborhood_radius_decay
245
- self._neighborhood_function = {
246
- "gaussian": self._gaussian,
247
- "bubble": self._bubble,
248
- }[neighborhood_function]
249
- self._distance_function = distance_function
250
- self._cyclic = (bool(cyclic_x), bool(cyclic_y))
251
- self._neigx, self._neigy = np.arange(self._shape[0]), np.arange(self._shape[1])
252
-
253
- # Seed numpy random generator
254
- if random_seed is None:
255
- self._random_seed = np.random.randint(np.iinfo(np.int32).max)
256
- else:
257
- self._random_seed = int(random_seed)
258
- np.random.seed(self._random_seed)
259
-
260
- # Random weight initialization
261
- self._weights = np.random.standard_normal(
262
- size=(self._shape[0], self._shape[1], self._input_len)
263
- )
264
-
265
- def get_shape(self) -> tuple[np.uint, np.uint]:
266
- """
267
- Gets the shape of the network.
268
-
269
- :return: tuple(int, int): Shape of the network.
270
- """
271
- return self._shape
272
-
273
- def get_weights(self) -> np.ndarray:
274
- """
275
- Gets the weight matrix of the network.
276
-
277
- :return: np.ndarray: Weight matrix of the network.
278
- """
279
- return self._weights
280
-
281
- def set_learning_rate(self, learning_rate: float) -> None:
282
- """
283
- Sets the learning_rate member of the SOM.
284
-
285
- :param learning_rate: float: New value for learning_rate of an instance of the SOM.
286
- """
287
- self._learning_rate = float(learning_rate)
288
-
289
- def set_neighborhood_radius(self, neighborhood_radius: float) -> None:
290
- """
291
- Sets the neighborhood_radius member of the SOM.
292
-
293
- :param neighborhood_radius: float: New value for neighborhood_radius of a SOM instance.
294
- """
295
- self._neighborhood_radius = float(neighborhood_radius)
296
-
297
- def activate(self, x: np.ndarray) -> np.ndarray:
298
- """
299
- Calculates distances between an instance x and the weights of the network.
300
-
301
- :param x: array-like: Instance to be compared with the weights of the network.
302
- :return: np.ndarray: Distances between x and each weight of the network.
303
- """
304
- return self._distance_function(x, self._weights)
305
-
306
- def winner(self, x: np.ndarray) -> tuple[int, ...]:
307
- """
308
- Calculates the best-matching unit of the network for an instance x
309
-
310
- :param x: array-like: Instance to be compared with the weights of the network.
311
- :return: (int, int): Index of the best-matching unit of x.
312
- """
313
- activation_map = self.activate(x)
314
- min_index = tuple(
315
- map(int, np.unravel_index(activation_map.argmin(), activation_map.shape))
316
- )
317
- return min_index
318
-
319
- def quantization(self, data: np.ndarray | pd.DataFrame | list) -> np.ndarray:
320
- """
321
- Calculates distances from each instance of 'data' to each of the weights of the network.
322
-
323
- :param data: array-like: Dataset to be compared with the weights of the network.
324
- Expected shape is (n_samples, n_features).
325
- :return: np.ndarray: array of lists of distances from each instance of the dataset
326
- to each weight of the network.
327
- """
328
- # Convert data to numpy array
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.
339
- This error is a quality measure for the training process.
340
-
341
- :param data: array-like: Dataset to be compared with the weights of the network.
342
- :return: float: Quantization error.
343
- """
344
- quantization = self.quantization(data)
345
- return quantization.mean()
346
-
347
- def distance_matrix(self, normalize: bool = False) -> np.ndarray:
348
- """
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
352
-
353
- :param normalize: bool: Activate to normalize the U-matrix between 0 and 1.
354
- Defaults to False.
355
- :return: np.ndarray: U-matrix of the current state of the network.
356
- """
357
- um = np.zeros(shape=self._shape)
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
-
366
- while not it.finished:
367
- update_matrix = self._bubble(it.multi_index, 1)
368
- um[it.multi_index] = np.sum(update_matrix * distances[it.multi_index])
369
- it.iternext()
370
- if normalize:
371
- # Normalize U-matrix between 0 and 1
372
- um = (um - np.min(um)) / (np.max(um) - np.min(um))
373
- return um
374
-
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.
380
-
381
- :param data: array-like: Dataset to be compared with the weights of the network.
382
- :return: np.ndarray: Activation matrix.
383
- """
384
- # Convert data to numpy array
385
- data_array = self._data_to_numpy(data)
386
-
387
- activation_matrix = np.zeros(self._shape)
388
- for i in data_array:
389
- activation_matrix[self.winner(i)] += 1
390
- return activation_matrix
391
-
392
- def winner_map(self, data: np.ndarray | pd.DataFrame | list) -> dict:
393
- """
394
- Calculates, for each node (i, j) of the network,
395
- the list of all instances from 'data' that has been assigned to it.
396
-
397
- :param data: array-like: Dataset to be compared with the weights of the network.
398
- :return: dict: Winner map.
399
- """
400
- # Convert data to numpy array
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
- }
405
- for i in data_array:
406
- winner_map[self.winner(i)].append(i)
407
- return winner_map
408
-
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
-
419
- :param data: array-like: Dataset to be compared with the weights of the network.
420
- :param labels: array-like: Labels corresponding to the indices of 'data'.
421
- :return: dict: Label map.
422
- """
423
- # Convert data to numpy array
424
- data_array = self._data_to_numpy(data)
425
- # Convert labels to numpy array
426
- if isinstance(labels, pd.DataFrame):
427
- labels = labels.to_numpy()
428
- else:
429
- labels = np.array(labels)
430
-
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:
450
- """
451
- Trains the self-organizing map, with the dataset 'data', and a certain number of iterations.
452
-
453
- :param data: array-like: Dataset for training.
454
- :param n_iteration: int or None: Number of iterations of training.
455
- If None, defaults to 1000 * len(data) for stepwise training modes,
456
- or 10 * len(data) for batch training mode.
457
- :param mode: str: Training mode name. May be either 'random', 'sequential', or 'batch'.
458
- For 'batch' mode, a much smaller number of iterations is needed,
459
- but a higher computation power is required for each individual iteration.
460
- :param verbose: bool: Activate to print useful information to the terminal/console, e.g.,
461
- the progress of the training process
462
- :return: float: Quantization error after training
463
- """
464
- # Convert data to numpy array for training
465
- data_array = self._data_to_numpy(data)
466
-
467
- # If no number of iterations is given, select automatically
468
- if n_iteration is None:
469
- n_iteration = {"random": 1000, "sequential": 1000, "batch": 10}[mode] * len(
470
- data_array
471
- )
472
-
473
- if verbose:
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)
488
- else:
489
- # Invalid training mode value
490
- raise ValueError(
491
- "Invalid value for 'mode' parameter. Value should be in "
492
- + str(["random", "sequential", "batch"])
493
- )
494
-
495
- # Compute quantization error
496
- q_error = self.quantization_error(data_array)
497
- if verbose:
498
- print("Quantization error:", q_error, sep=" ")
499
- return q_error
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
-
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.
649
-
650
- :param mode: str: Initialization mode. May be either 'random', 'linear', or 'sample'.
651
- Note: Each initialization method may require multiple additional arguments in kwargs.
652
- :param kwargs:
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
- }
664
- try:
665
- modes[mode](**kwargs)
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
671
-
672
- def _weight_initialization_random(
673
- self, sample_mode: str = "standard_normal", random_seed: int | None = None
674
- ) -> None:
675
- """Random initialization method.
676
-
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'.
681
- Defaults to 'standard_normal'.
682
- :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
683
- """
684
- sample_modes = {
685
- "uniform": np.random.random,
686
- "standard_normal": np.random.standard_normal,
687
- }
688
-
689
- # Seed numpy random generator
690
- if random_seed is None:
691
- random_seed = np.random.randint(np.iinfo(np.int32).max)
692
- else:
693
- random_seed = int(random_seed)
694
- np.random.seed(random_seed)
695
-
696
- # Initialize weights randomly
697
- try:
698
- self._weights = sample_modes[sample_mode](size=self._weights.shape)
699
- except KeyError as exc:
700
- raise ValueError(
701
- "Invalid value for 'sample_mode' parameter. Value should be in "
702
- + str(sample_modes.keys())
703
- ) from exc
704
-
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
711
- components of 'data'.
712
-
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.
715
-
716
- :param data: array-like: Dataset for weight initialization w/ PCA.
717
- """
718
- # Convert data to numpy array for training
719
- data_array = self._data_to_numpy(data)
720
-
721
- # Perform PCA w/ sklearn
722
- data_array = sklearn.preprocessing.StandardScaler().fit_transform(data_array)
723
- pca = sklearn.decomposition.PCA(n_components=2)
724
- pca.fit(data_array)
725
- # Initialize weights spanning first 2 principal components of data
726
- for i, c1 in enumerate(np.linspace(-1, 1, num=self._shape[0])):
727
- for j, c2 in enumerate(np.linspace(-1, 1, num=self._shape[1])):
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.
738
-
739
- :param data: Dataset for weight initialization/sampling.
740
- :param random_seed: int or None: Seed for NumPy random value generator. Defaults to None.
741
- """
742
- # Seed numpy random generator
743
- if random_seed is None:
744
- random_seed = np.random.randint(np.iinfo(np.int32).max)
745
- else:
746
- random_seed = int(random_seed)
747
- np.random.seed(random_seed)
748
-
749
- # Convert data to numpy array for training
750
- data_array = self._data_to_numpy(data)
751
-
752
- # Assign weights to random samples from dataset
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
- )
759
- self._weights = data_array[sample].reshape(self._weights.shape)
760
-
761
- def _gaussian(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
762
- """
763
- Gaussian neighborhood function, centered in c. Has support for cyclic arrays.
764
-
765
- :param c: (int, int): Center coordinates for gaussian function.
766
- :param sigma: float: Spread variable for gaussian function.
767
- :return: np.ndarray: Gaussian, centered in c, over all the weights of the network.
768
- """
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)
783
-
784
- def _bubble(self, c: tuple[int, ...], sigma: float) -> np.ndarray:
785
- """
786
- Bubble neighborhood function, centered in c. Has support for cyclic arrays.
787
-
788
- The neighbors of c are the nodes in the region of sigma positions in the vertical
789
- and horizontal directions around c.
790
-
791
- :param c: (int, int): Center coordinates for gaussian function.
792
- :param sigma: float: Spread variable for gaussian function.
793
- :return: np.ndarray: Neighborhood matrix, centered in c.
794
- """
795
- # Convert sigma to integer
796
- sigma = int(np.around(sigma))
797
-
798
- # Calculate vertical and horizontal regions
799
- ax = np.logical_and(self._neigx >= c[0] - sigma, self._neigx <= c[0] + sigma)
800
- ay = np.logical_and(self._neigy >= c[1] - sigma, self._neigy <= c[1] + sigma)
801
-
802
- # Calculate cyclic regions
803
- if self._cyclic[0]:
804
- if c[0] - sigma < 0:
805
- ax[int(c[0] - sigma) :] = True
806
- if c[0] + sigma >= self._shape[0]:
807
- ax[: int((c[0] + sigma) % self._shape[0] + 1)] = True
808
- if self._cyclic[1]:
809
- if c[1] - sigma < 0:
810
- ay[int(c[1] - sigma) :] = True
811
- if c[1] + sigma >= self._shape[1]:
812
- ay[: int((c[1] + sigma) % self._shape[1] + 1)] = True
813
-
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)
17
+ from __future__ import annotations
18
+
19
+ from ._decay import (
20
+ asymptotic_decay,
21
+ exponential_decay,
22
+ inverse_decay,
23
+ linear_decay,
24
+ )
25
+ from ._distance import euclidean_distance
26
+ from ._neighborhood import (
27
+ NEIGHBORHOOD_FUNCTIONS,
28
+ SIGNED_NEIGHBORHOODS,
29
+ bubble,
30
+ gaussian,
31
+ mexican_hat,
32
+ )
33
+ from ._som import SOM
34
+
35
+ __all__ = [
36
+ "NEIGHBORHOOD_FUNCTIONS",
37
+ "SIGNED_NEIGHBORHOODS",
38
+ "SOM",
39
+ "asymptotic_decay",
40
+ "bubble",
41
+ "euclidean_distance",
42
+ "exponential_decay",
43
+ "gaussian",
44
+ "inverse_decay",
45
+ "linear_decay",
46
+ "mexican_hat",
47
+ ]
48
+
49
+ __version__ = "0.3.0"
50
+
51
+ # Backwards-compatible aliases. Before 0.3.0 these functions lived in this module under
52
+ # underscore-prefixed names; they were private by convention but reachable, and the README listed
53
+ # them. Keep them working rather than breaking imports silently.
54
+ _asymptotic_decay = asymptotic_decay
55
+ _linear_decay = linear_decay
56
+ _exponential_decay = exponential_decay
57
+ _inverse_decay = inverse_decay
58
+ _euclidean_distance = euclidean_distance