python-som 0.1.1__tar.gz → 0.1.2__tar.gz

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