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.
- {python_som-0.1.1 → python_som-0.1.2}/PKG-INFO +4 -1
- {python_som-0.1.1 → python_som-0.1.2}/pyproject.toml +6 -1
- python_som-0.1.1/SOM_atualizado.py +0 -758
- python_som-0.1.1/setup.cfg +0 -2
- python_som-0.1.1/setup.py +0 -38
- {python_som-0.1.1 → python_som-0.1.2}/.gitignore +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/LICENSE +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/README.md +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/python_som/__init__.py +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/requirements.txt +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/test.py +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/test_iris.ipynb +0 -0
- {python_som-0.1.1 → python_som-0.1.2}/test_output_iris.png +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-som
|
|
3
|
-
Version: 0.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.
|
|
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)
|
python_som-0.1.1/setup.cfg
DELETED
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
|
|
File without changes
|
|
File without changes
|