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 +52 -819
- python_som/_decay.py +68 -0
- python_som/_distance.py +26 -0
- python_som/_neighborhood.py +218 -0
- python_som/_som.py +649 -0
- python_som/py.typed +0 -0
- python_som-0.3.0.dist-info/METADATA +174 -0
- python_som-0.3.0.dist-info/RECORD +10 -0
- {python_som-0.1.3.dist-info → python_som-0.3.0.dist-info}/WHEEL +1 -1
- python_som-0.1.3.dist-info/METADATA +0 -147
- python_som-0.1.3.dist-info/RECORD +0 -5
- {python_som-0.1.3.dist-info → python_som-0.3.0.dist-info}/licenses/LICENSE +0 -0
python_som/__init__.py
CHANGED
|
@@ -1,825 +1,58 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Python implementation of Kohonen's 2-D self-organizing map.
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
from
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|