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 +441 -250
- {python_som-0.0.1a1.dist-info → python_som-0.1.1.dist-info}/METADATA +13 -18
- python_som-0.1.1.dist-info/RECORD +5 -0
- {python_som-0.0.1a1.dist-info → python_som-0.1.1.dist-info}/WHEEL +1 -2
- python_som-0.0.1a1.dist-info/RECORD +0 -6
- python_som-0.0.1a1.dist-info/top_level.txt +0 -1
- {python_som-0.0.1a1.dist-info → python_som-0.1.1.dist-info/licenses}/LICENSE +0 -0
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
|
|
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:
|
|
63
|
-
"""
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
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
|
|
145
|
-
Defaults to _asymptotic_decay.
|
|
146
|
-
:param neighborhood_radius: float: Initial neighborhood radius for the training process.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
153
|
-
network.
|
|
154
|
-
May be a predefined one from this package, or a custom function, with the same
|
|
155
|
-
Defaults to _euclidean_distance.
|
|
156
|
-
:param cyclic_x: bool: Boolean value activate/deactivate cyclic arrays in the x direction,
|
|
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,
|
|
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(
|
|
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
|
|
172
|
-
|
|
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,
|
|
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(
|
|
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 =
|
|
241
|
+
self._learning_rate = float(learning_rate)
|
|
196
242
|
self._learning_rate_decay = learning_rate_decay
|
|
197
|
-
self._neighborhood_radius =
|
|
243
|
+
self._neighborhood_radius = float(neighborhood_radius)
|
|
198
244
|
self._neighborhood_radius_decay = neighborhood_radius_decay
|
|
199
245
|
self._neighborhood_function = {
|
|
200
|
-
|
|
201
|
-
|
|
246
|
+
"gaussian": self._gaussian,
|
|
247
|
+
"bubble": self._bubble,
|
|
248
|
+
}[neighborhood_function]
|
|
202
249
|
self._distance_function = distance_function
|
|
203
|
-
self._cyclic = (
|
|
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 =
|
|
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) ->
|
|
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 =
|
|
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
|
|
293
|
+
:param neighborhood_radius: float: New value for neighborhood_radius of a SOM instance.
|
|
247
294
|
"""
|
|
248
|
-
self._neighborhood_radius =
|
|
295
|
+
self._neighborhood_radius = float(neighborhood_radius)
|
|
249
296
|
|
|
250
|
-
def activate(self, x:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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,
|
|
298
|
-
|
|
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=[
|
|
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
|
-
|
|
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:
|
|
313
|
-
"""
|
|
314
|
-
|
|
315
|
-
have been assigned to
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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,
|
|
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
|
-
|
|
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 = {
|
|
469
|
+
n_iteration = {"random": 1000, "sequential": 1000, "batch": 10}[mode] * len(
|
|
470
|
+
data_array
|
|
471
|
+
)
|
|
401
472
|
|
|
402
473
|
if verbose:
|
|
403
|
-
print(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 "
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
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
|
|
493
|
-
For 'sample' and 'linear' modes, 'data': array-like must be provided
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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(
|
|
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(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
Random initialization method.
|
|
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
|
-
|
|
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 = {
|
|
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 =
|
|
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 "
|
|
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(
|
|
529
|
-
|
|
530
|
-
|
|
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.
|
|
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
|
-
|
|
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] =
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
578
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-som
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Python implementation of the Self-Organizing Map
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Author-email: msouza.andre@hotmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
|
|
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
|
-
|
|
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,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
|
|
File without changes
|