ksom 0.9.1__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.
- ksom-0.9.1/.gitignore +2 -0
- ksom-0.9.1/LICENCE.txt +21 -0
- ksom-0.9.1/PKG-INFO +26 -0
- ksom-0.9.1/README.md +13 -0
- ksom-0.9.1/chica.jpg +0 -0
- ksom-0.9.1/pyproject.toml +22 -0
- ksom-0.9.1/requirements.txt +0 -0
- ksom-0.9.1/src/ksom/__init__.py +217 -0
- ksom-0.9.1/src/ksom/ksom.py +217 -0
- ksom-0.9.1/tests/test_cheese.py +114 -0
- ksom-0.9.1/tests/test_dist.py +30 -0
- ksom-0.9.1/tests/test_img.py +91 -0
- ksom-0.9.1/tests/test_nb_fct.py +62 -0
ksom-0.9.1/.gitignore
ADDED
ksom-0.9.1/LICENCE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mathieu d'Aquin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ksom-0.9.1/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ksom
|
|
3
|
+
Version: 0.9.1
|
|
4
|
+
Summary: Self Organising Maps in pytorch
|
|
5
|
+
Project-URL: Homepage, https://github.com/mdaquin/ksom
|
|
6
|
+
Project-URL: Issues, https://github.com/mdaquin/ksom/issues
|
|
7
|
+
Author-email: Mathieu d'Aquin <mathieu.daquin.w+ksom@gmail.com>
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# KSOM - Simple, but relatively efficient, pytorch-based self organising map
|
|
15
|
+
|
|
16
|
+
This is a simple implementation of self-organising map training in python, using pytorch for efficiency. Check ``test-img.py`` for a simple use case creating a square color map of an image. To use, try
|
|
17
|
+
```
|
|
18
|
+
pip install -r requirements.txt
|
|
19
|
+
```
|
|
20
|
+
and then
|
|
21
|
+
```
|
|
22
|
+
python test/test_img.py chica.jpg 6
|
|
23
|
+
```
|
|
24
|
+
This shoud show an updating 6x6 map of the colours in the ``chica.jpg`` image. Change the image or the size of the map by changing the corresponding parameters.
|
|
25
|
+
|
|
26
|
+
Another example is included in the ``test_cheese.py`` creating a map of cheeses based on various binary attributes.
|
ksom-0.9.1/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# KSOM - Simple, but relatively efficient, pytorch-based self organising map
|
|
2
|
+
|
|
3
|
+
This is a simple implementation of self-organising map training in python, using pytorch for efficiency. Check ``test-img.py`` for a simple use case creating a square color map of an image. To use, try
|
|
4
|
+
```
|
|
5
|
+
pip install -r requirements.txt
|
|
6
|
+
```
|
|
7
|
+
and then
|
|
8
|
+
```
|
|
9
|
+
python test/test_img.py chica.jpg 6
|
|
10
|
+
```
|
|
11
|
+
This shoud show an updating 6x6 map of the colours in the ``chica.jpg`` image. Change the image or the size of the map by changing the corresponding parameters.
|
|
12
|
+
|
|
13
|
+
Another example is included in the ``test_cheese.py`` creating a map of cheeses based on various binary attributes.
|
ksom-0.9.1/chica.jpg
ADDED
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ksom"
|
|
7
|
+
version = "0.9.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Mathieu d'Aquin", email="mathieu.daquin.w+ksom@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Self Organising Maps in pytorch"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/mdaquin/ksom"
|
|
22
|
+
Issues = "https://github.com/mdaquin/ksom/issues"
|
|
Binary file
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Self-organising map pytroch model.
|
|
2
|
+
# Author: Mathieu d'Aquin
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
from torch.nn import CosineSimilarity
|
|
6
|
+
from torchmetrics.functional import pairwise_cosine_similarity
|
|
7
|
+
import math
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
def euclidean_distance(x,y):
|
|
11
|
+
"""returns a distance matrix between the elements of
|
|
12
|
+
the tensor x and the ones of the tensor y"""
|
|
13
|
+
return torch.cdist(x,y,2)
|
|
14
|
+
|
|
15
|
+
def cosine_distance(x,y):
|
|
16
|
+
"""returns a distance matrix between the elements of
|
|
17
|
+
the tensor x and the ones of the tensor y"""
|
|
18
|
+
return 1 - pairwise_cosine_similarity(x,y)
|
|
19
|
+
|
|
20
|
+
# TODO: #13 this does not seem to work...
|
|
21
|
+
def nb_ricker(node, dims, coord, nb):
|
|
22
|
+
"""
|
|
23
|
+
Ricker wavelet (mexican hat) neighborhood weights between between node (x,y)
|
|
24
|
+
and all the coordinates in the tensor coord ([(x,y)]) assuming
|
|
25
|
+
it follow the dimensions in dims (height, width).
|
|
26
|
+
nb is the neighborhood radius (i.e. the distance after which
|
|
27
|
+
the function returns 0).
|
|
28
|
+
"""
|
|
29
|
+
# if nb < 1.0: nb = 1.0 # seem to make no difference...
|
|
30
|
+
nodes = node.repeat(dims[0]*dims[1], 1)
|
|
31
|
+
dist = torch.nn.functional.pairwise_distance(nodes, coord)
|
|
32
|
+
dist[int(node[0]*dims[0])+node[1]%dims[0]] = 0.0
|
|
33
|
+
FM = (math.sqrt(6)/(math.pi*(2*nb)))/math.sqrt(3)
|
|
34
|
+
fbit = (1-2*math.pi**2*FM**2*dist**2)
|
|
35
|
+
sbit = -math.pi**2*FM**2*dist**2
|
|
36
|
+
return fbit*torch.exp(sbit)
|
|
37
|
+
|
|
38
|
+
def nb_gaussian(node, dims, coord, nb):
|
|
39
|
+
"""
|
|
40
|
+
Gaussian neighborhood weights between between node (x,y)
|
|
41
|
+
and all the coordinates in the tensor coord ([(x,y)]) assuming
|
|
42
|
+
it follow the dimensions in dims (height, width).
|
|
43
|
+
nb is the neighborhood radius (i.e. the distance after which
|
|
44
|
+
the function returns 0).
|
|
45
|
+
"""
|
|
46
|
+
# exp(-(x/(nb/2))**2)
|
|
47
|
+
nodes = node.repeat(dims[0]*dims[1], 1)
|
|
48
|
+
dist = torch.nn.functional.pairwise_distance(nodes, coord)
|
|
49
|
+
dist[int(node[0]*dims[0])+node[1]%dims[0]] = 0.0
|
|
50
|
+
ret = torch.exp(-(dist/(nb/2))**2)
|
|
51
|
+
ret[ret < 0] = 0.0
|
|
52
|
+
return ret
|
|
53
|
+
|
|
54
|
+
def nb_linear_o(node1, node2, nb):
|
|
55
|
+
"""deprecated non-batch versoin of the linear
|
|
56
|
+
neighborhood function"""
|
|
57
|
+
if node1[0] == node2[0] and node1[1] == node2[1]: return 1.0
|
|
58
|
+
dist = euclidean_distance(node1.view(-1, 2).to(torch.float32),
|
|
59
|
+
node2.view(-1, 2).to(torch.float32))[0][0]
|
|
60
|
+
return max(0,1-(dist/nb))
|
|
61
|
+
|
|
62
|
+
def nb_linear(node, dims, coord, nb):
|
|
63
|
+
"""linear neighborhood distances between node (x,y)
|
|
64
|
+
and all the coordinates in the tensor coord ([(x,y)]) assuming
|
|
65
|
+
it follow the dimensions in dims (height, width).
|
|
66
|
+
nb is the neighborhood radius (i.e. the distance after which
|
|
67
|
+
the function returns 0)."""
|
|
68
|
+
nodes = node.repeat(dims[0]*dims[1], 1)
|
|
69
|
+
dist = torch.nn.functional.pairwise_distance(nodes, coord)
|
|
70
|
+
dist[int(node[0]*dims[0])+node[1]%dims[0]] = 0.0
|
|
71
|
+
ret = 1-(dist/nb)
|
|
72
|
+
ret[ret < 0] = 0.0
|
|
73
|
+
return ret
|
|
74
|
+
|
|
75
|
+
class SOM(torch.nn.Module):
|
|
76
|
+
"""
|
|
77
|
+
Model for a self-organising map (Kohonen map), which is trained by adding data points that will make the map adapt. The general principle for the training is that each point is matched to the closest unit (using the euclidean distance). The weights of this best matching unit (BMU) are then updated to move closer to the input data point (to a rate dependent on the learning rate, alpha). Neighboring units are also updated, to a lesser extent, depending on the neighborhood function and radius. The learning rate and the neighborhood radius decay over time (provided input points).
|
|
78
|
+
|
|
79
|
+
Parameters:
|
|
80
|
+
-----------
|
|
81
|
+
|
|
82
|
+
xs: vertical size of the map
|
|
83
|
+
|
|
84
|
+
ys: horizontal size of the map
|
|
85
|
+
|
|
86
|
+
dist (default: ksom.euclidean_distance): the fonction to identify the best matching unit (BMU). This takes two 2D vectors and returns a 2D matrix of the distances between them. Two options are available as part of ksom: euclidean_distance and consine_distance.
|
|
87
|
+
|
|
88
|
+
zero_init (default: False): Whether the map is initialised randomly, or by zero vectors.
|
|
89
|
+
|
|
90
|
+
alpha_init (default: 1e-3): initial value of learning rate.
|
|
91
|
+
|
|
92
|
+
alpha_drate (default: 1e-6): decay rate of the learning rate. The learning rate (alpha) decreases linearly based on this rate for each new data point, until it reaches the valyue of alpha_drate.
|
|
93
|
+
|
|
94
|
+
neightborgood_init (default: half of the smallest dimension of the map): the initial radius of the neighborhood.
|
|
95
|
+
|
|
96
|
+
neighborhood_fct (default: ksom.nb_gaussian, other values: nb_linear, nb_ricker): the function to get the neighborhood rate depending on the neighborhood rate and the euclidean distance between units.
|
|
97
|
+
|
|
98
|
+
neighborhood_drate (default: 1e-6): rate of decay of the neigtborhood radius. This will decrease linearly from neighborhood_init to neighborhood_drate depending on neighborhood_drate.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
--------
|
|
102
|
+
|
|
103
|
+
See https://github.com/mdaquin/KSOM/blob/main/test_img.py for an example of the use of KSOM to build a map of the pixel colors of an image.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, xs, ys, dim,
|
|
107
|
+
dist=euclidean_distance, zero_init=False, sample_init=None,
|
|
108
|
+
alpha_init=1e-2, alpha_drate=1e-6,
|
|
109
|
+
neighborhood_init=None, neighborhood_fct=nb_gaussian, neighborhood_drate=1e-6,
|
|
110
|
+
minval=None, maxval=None, device="cpu"):
|
|
111
|
+
if type(xs) != int or type(ys) != int or type(dim) != int: raise TypeError("size and dimension of SOM should be int")
|
|
112
|
+
if alpha_init <= alpha_drate: raise ValueError("Decay rate of learning rate (alpha_drate) should be smaller than initial value (alpha_init)")
|
|
113
|
+
if neighborhood_init is None: self.neighborhood_init = min(xs,ys)/2 # start with half the map
|
|
114
|
+
else: self.neighborhood_init = neighborhood_init
|
|
115
|
+
if self.neighborhood_init <= neighborhood_drate: raise ValueError("Neighborhood radius decay rate should (neighborhood_drate) should be smaller than initial value (neighborhood_init)")
|
|
116
|
+
super(SOM, self).__init__()
|
|
117
|
+
self.somap = torch.randn(xs*ys, dim).to(device)
|
|
118
|
+
if minval is not None and maxval is not None:
|
|
119
|
+
self.somap = (self.somap + minval) * (maxval - minval)
|
|
120
|
+
self.minval = minval
|
|
121
|
+
self.maxval = maxval
|
|
122
|
+
if zero_init: self.somap = torch.zeros((xs*ys, dim), dtype=torch.float).to(device)
|
|
123
|
+
if sample_init is not None:
|
|
124
|
+
if sample_init.shape == self.somap.shape: self.somap = sample_init
|
|
125
|
+
else: raise ValueError("Number of samples provided for initialisation should be the same as number of cells in map;")
|
|
126
|
+
self.xs = xs
|
|
127
|
+
self.ys = ys
|
|
128
|
+
self.dim = dim
|
|
129
|
+
self.dist = dist
|
|
130
|
+
self.step = 0
|
|
131
|
+
self.neighborhood_drate = neighborhood_drate
|
|
132
|
+
self.neighborhood_fct = neighborhood_fct
|
|
133
|
+
self.alpha_init = alpha_init
|
|
134
|
+
self.alpha_drate = alpha_drate
|
|
135
|
+
lx = torch.arange(xs).repeat(ys).view(-1, ys).T.reshape(xs*ys)
|
|
136
|
+
ly = torch.arange(ys).repeat(xs)
|
|
137
|
+
self.coord = torch.stack((lx,ly), -1).to(device)
|
|
138
|
+
|
|
139
|
+
def to(self, device):
|
|
140
|
+
super(SOM, self).to(device)
|
|
141
|
+
self.somap = self.somap.to(device)
|
|
142
|
+
self.coord = self.coord.to(device)
|
|
143
|
+
|
|
144
|
+
def forward(self, x):
|
|
145
|
+
"""
|
|
146
|
+
Identifies the best matching unit for the data points in x in the current map.
|
|
147
|
+
|
|
148
|
+
Parameter:
|
|
149
|
+
----------
|
|
150
|
+
|
|
151
|
+
x: 2D tensor (N, dim) corresponding to N data points of dimension dim.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
--------
|
|
155
|
+
|
|
156
|
+
Returns a tuple including the coordinates of the bmu in the current map and the distance matrix used to find it.
|
|
157
|
+
"""
|
|
158
|
+
if type(x) != torch.Tensor: raise TypeError("x should be a tensor of shape (N,dim)")
|
|
159
|
+
if len(x.size()) != 2: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
160
|
+
if x.size()[1] != self.dim: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
161
|
+
dists = self.dist(self.somap, x)
|
|
162
|
+
bmu_ind = dists.min(dim=0).indices
|
|
163
|
+
bmu_ind_x = (bmu_ind/self.xs).to(torch.int32)
|
|
164
|
+
bmu_ind_y = bmu_ind%self.xs
|
|
165
|
+
return torch.stack((bmu_ind_x, bmu_ind_y), -1), dists
|
|
166
|
+
|
|
167
|
+
def __1DIndexTo2DIndex(self, ind):
|
|
168
|
+
return torch.Tensor([int(ind/self.xs), ind%self.xs])
|
|
169
|
+
|
|
170
|
+
def add(self, x):
|
|
171
|
+
"""
|
|
172
|
+
Add the data points in x to the current map and update the map to those points (training).
|
|
173
|
+
|
|
174
|
+
Parameters:
|
|
175
|
+
-----------
|
|
176
|
+
|
|
177
|
+
x: 2D tensor (N, dim) corresponding to N data points of dimension dim.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
--------
|
|
181
|
+
|
|
182
|
+
Returns the euclidean distance of the SOM's matrix before and after training. Gives an indication of the impact of the added training points on the map.
|
|
183
|
+
"""
|
|
184
|
+
if type(x) != torch.Tensor: raise TypeError("x should be a tensor of shape (N,dim)")
|
|
185
|
+
if len(x.size()) != 2: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
186
|
+
if x.size()[1] != self.dim: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
187
|
+
prev_som = self.somap.clone().detach()
|
|
188
|
+
count = 0
|
|
189
|
+
for x_k in x:
|
|
190
|
+
if x_k.isnan().any() or x_k.isinf().any(): continue # do not try to add vector containing nans !
|
|
191
|
+
count+=1
|
|
192
|
+
# decreases linearly...
|
|
193
|
+
nb = max(self.neighborhood_drate, self.neighborhood_init - (self.step*self.neighborhood_drate))
|
|
194
|
+
alpha = max(self.alpha_drate, self.alpha_init - (self.step*self.alpha_drate))
|
|
195
|
+
self.step += 1
|
|
196
|
+
bmu = self(x_k.view(-1, x_k.size()[0]))[0][0]
|
|
197
|
+
theta = self.neighborhood_fct(bmu, (self.xs, self.ys), self.coord, nb)
|
|
198
|
+
ntheta = theta.view(-1, theta.size(0)).T
|
|
199
|
+
# TODO: print(ntheta) prob is that neg ones get to quickly neg and then go to infinity...
|
|
200
|
+
self.somap = self.somap + ntheta*(alpha*(x_k-self.somap))
|
|
201
|
+
if torch.isnan(self.somap).any() or torch.isinf(self.somap).any():
|
|
202
|
+
print("*** Nan! ***")
|
|
203
|
+
print("bmu", bmu)
|
|
204
|
+
print("theta", theta.min(), theta.max(), theta.mean(), theta.isnan().any())
|
|
205
|
+
print("ntheta", ntheta.min(), ntheta.max(), ntheta.mean(), ntheta.isnan().any())
|
|
206
|
+
print("alpha", alpha)
|
|
207
|
+
print("x_k", x_k.min(), x_k.max(), x_k.mean(), x_k.isnan().any())
|
|
208
|
+
# old non batch (slow) version
|
|
209
|
+
# batch here means calculating the whole map at once,
|
|
210
|
+
# not having a batch of values treated at once.
|
|
211
|
+
# for i, w_i in enumerate(self.somap):
|
|
212
|
+
# i2 = self.__1DIndexTo2DIndex(i)
|
|
213
|
+
# theta = self.neighborhood_fct(bmu, i2, nb)
|
|
214
|
+
# self.somap[i] = w_i + theta*alpha*(x_k-w_i)
|
|
215
|
+
# print("o nsomap", self.somap)
|
|
216
|
+
# wij' = wij + ( n_fct(bmu,ni,nb(s)) * alpha(s) * (x_k - wij) )
|
|
217
|
+
return float(torch.nn.functional.pairwise_distance(prev_som, self.somap).mean()), count
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Self-organising map pytroch model.
|
|
2
|
+
# Author: Mathieu d'Aquin
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
from torch.nn import CosineSimilarity
|
|
6
|
+
from torchmetrics.functional import pairwise_cosine_similarity
|
|
7
|
+
import math
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
def euclidean_distance(x,y):
|
|
11
|
+
"""returns a distance matrix between the elements of
|
|
12
|
+
the tensor x and the ones of the tensor y"""
|
|
13
|
+
return torch.cdist(x,y,2)
|
|
14
|
+
|
|
15
|
+
def cosine_distance(x,y):
|
|
16
|
+
"""returns a distance matrix between the elements of
|
|
17
|
+
the tensor x and the ones of the tensor y"""
|
|
18
|
+
return 1 - pairwise_cosine_similarity(x,y)
|
|
19
|
+
|
|
20
|
+
# TODO: #13 this does not seem to work...
|
|
21
|
+
def nb_ricker(node, dims, coord, nb):
|
|
22
|
+
"""
|
|
23
|
+
Ricker wavelet (mexican hat) neighborhood weights between between node (x,y)
|
|
24
|
+
and all the coordinates in the tensor coord ([(x,y)]) assuming
|
|
25
|
+
it follow the dimensions in dims (height, width).
|
|
26
|
+
nb is the neighborhood radius (i.e. the distance after which
|
|
27
|
+
the function returns 0).
|
|
28
|
+
"""
|
|
29
|
+
# if nb < 1.0: nb = 1.0 # seem to make no difference...
|
|
30
|
+
nodes = node.repeat(dims[0]*dims[1], 1)
|
|
31
|
+
dist = torch.nn.functional.pairwise_distance(nodes, coord)
|
|
32
|
+
dist[int(node[0]*dims[0])+node[1]%dims[0]] = 0.0
|
|
33
|
+
FM = (math.sqrt(6)/(math.pi*(2*nb)))/math.sqrt(3)
|
|
34
|
+
fbit = (1-2*math.pi**2*FM**2*dist**2)
|
|
35
|
+
sbit = -math.pi**2*FM**2*dist**2
|
|
36
|
+
return fbit*torch.exp(sbit)
|
|
37
|
+
|
|
38
|
+
def nb_gaussian(node, dims, coord, nb):
|
|
39
|
+
"""
|
|
40
|
+
Gaussian neighborhood weights between between node (x,y)
|
|
41
|
+
and all the coordinates in the tensor coord ([(x,y)]) assuming
|
|
42
|
+
it follow the dimensions in dims (height, width).
|
|
43
|
+
nb is the neighborhood radius (i.e. the distance after which
|
|
44
|
+
the function returns 0).
|
|
45
|
+
"""
|
|
46
|
+
# exp(-(x/(nb/2))**2)
|
|
47
|
+
nodes = node.repeat(dims[0]*dims[1], 1)
|
|
48
|
+
dist = torch.nn.functional.pairwise_distance(nodes, coord)
|
|
49
|
+
dist[int(node[0]*dims[0])+node[1]%dims[0]] = 0.0
|
|
50
|
+
ret = torch.exp(-(dist/(nb/2))**2)
|
|
51
|
+
ret[ret < 0] = 0.0
|
|
52
|
+
return ret
|
|
53
|
+
|
|
54
|
+
def nb_linear_o(node1, node2, nb):
|
|
55
|
+
"""deprecated non-batch versoin of the linear
|
|
56
|
+
neighborhood function"""
|
|
57
|
+
if node1[0] == node2[0] and node1[1] == node2[1]: return 1.0
|
|
58
|
+
dist = euclidean_distance(node1.view(-1, 2).to(torch.float32),
|
|
59
|
+
node2.view(-1, 2).to(torch.float32))[0][0]
|
|
60
|
+
return max(0,1-(dist/nb))
|
|
61
|
+
|
|
62
|
+
def nb_linear(node, dims, coord, nb):
|
|
63
|
+
"""linear neighborhood distances between node (x,y)
|
|
64
|
+
and all the coordinates in the tensor coord ([(x,y)]) assuming
|
|
65
|
+
it follow the dimensions in dims (height, width).
|
|
66
|
+
nb is the neighborhood radius (i.e. the distance after which
|
|
67
|
+
the function returns 0)."""
|
|
68
|
+
nodes = node.repeat(dims[0]*dims[1], 1)
|
|
69
|
+
dist = torch.nn.functional.pairwise_distance(nodes, coord)
|
|
70
|
+
dist[int(node[0]*dims[0])+node[1]%dims[0]] = 0.0
|
|
71
|
+
ret = 1-(dist/nb)
|
|
72
|
+
ret[ret < 0] = 0.0
|
|
73
|
+
return ret
|
|
74
|
+
|
|
75
|
+
class SOM(torch.nn.Module):
|
|
76
|
+
"""
|
|
77
|
+
Model for a self-organising map (Kohonen map), which is trained by adding data points that will make the map adapt. The general principle for the training is that each point is matched to the closest unit (using the euclidean distance). The weights of this best matching unit (BMU) are then updated to move closer to the input data point (to a rate dependent on the learning rate, alpha). Neighboring units are also updated, to a lesser extent, depending on the neighborhood function and radius. The learning rate and the neighborhood radius decay over time (provided input points).
|
|
78
|
+
|
|
79
|
+
Parameters:
|
|
80
|
+
-----------
|
|
81
|
+
|
|
82
|
+
xs: vertical size of the map
|
|
83
|
+
|
|
84
|
+
ys: horizontal size of the map
|
|
85
|
+
|
|
86
|
+
dist (default: ksom.euclidean_distance): the fonction to identify the best matching unit (BMU). This takes two 2D vectors and returns a 2D matrix of the distances between them. Two options are available as part of ksom: euclidean_distance and consine_distance.
|
|
87
|
+
|
|
88
|
+
zero_init (default: False): Whether the map is initialised randomly, or by zero vectors.
|
|
89
|
+
|
|
90
|
+
alpha_init (default: 1e-3): initial value of learning rate.
|
|
91
|
+
|
|
92
|
+
alpha_drate (default: 1e-6): decay rate of the learning rate. The learning rate (alpha) decreases linearly based on this rate for each new data point, until it reaches the valyue of alpha_drate.
|
|
93
|
+
|
|
94
|
+
neightborgood_init (default: half of the smallest dimension of the map): the initial radius of the neighborhood.
|
|
95
|
+
|
|
96
|
+
neighborhood_fct (default: ksom.nb_gaussian, other values: nb_linear, nb_ricker): the function to get the neighborhood rate depending on the neighborhood rate and the euclidean distance between units.
|
|
97
|
+
|
|
98
|
+
neighborhood_drate (default: 1e-6): rate of decay of the neigtborhood radius. This will decrease linearly from neighborhood_init to neighborhood_drate depending on neighborhood_drate.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
--------
|
|
102
|
+
|
|
103
|
+
See https://github.com/mdaquin/KSOM/blob/main/test_img.py for an example of the use of KSOM to build a map of the pixel colors of an image.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, xs, ys, dim,
|
|
107
|
+
dist=euclidean_distance, zero_init=False, sample_init=None,
|
|
108
|
+
alpha_init=1e-2, alpha_drate=1e-6,
|
|
109
|
+
neighborhood_init=None, neighborhood_fct=nb_gaussian, neighborhood_drate=1e-6,
|
|
110
|
+
minval=None, maxval=None, device="cpu"):
|
|
111
|
+
if type(xs) != int or type(ys) != int or type(dim) != int: raise TypeError("size and dimension of SOM should be int")
|
|
112
|
+
if alpha_init <= alpha_drate: raise ValueError("Decay rate of learning rate (alpha_drate) should be smaller than initial value (alpha_init)")
|
|
113
|
+
if neighborhood_init is None: self.neighborhood_init = min(xs,ys)/2 # start with half the map
|
|
114
|
+
else: self.neighborhood_init = neighborhood_init
|
|
115
|
+
if self.neighborhood_init <= neighborhood_drate: raise ValueError("Neighborhood radius decay rate should (neighborhood_drate) should be smaller than initial value (neighborhood_init)")
|
|
116
|
+
super(SOM, self).__init__()
|
|
117
|
+
self.somap = torch.randn(xs*ys, dim).to(device)
|
|
118
|
+
if minval is not None and maxval is not None:
|
|
119
|
+
self.somap = (self.somap + minval) * (maxval - minval)
|
|
120
|
+
self.minval = minval
|
|
121
|
+
self.maxval = maxval
|
|
122
|
+
if zero_init: self.somap = torch.zeros((xs*ys, dim), dtype=torch.float).to(device)
|
|
123
|
+
if sample_init is not None:
|
|
124
|
+
if sample_init.shape == self.somap.shape: self.somap = sample_init
|
|
125
|
+
else: raise ValueError("Number of samples provided for initialisation should be the same as number of cells in map;")
|
|
126
|
+
self.xs = xs
|
|
127
|
+
self.ys = ys
|
|
128
|
+
self.dim = dim
|
|
129
|
+
self.dist = dist
|
|
130
|
+
self.step = 0
|
|
131
|
+
self.neighborhood_drate = neighborhood_drate
|
|
132
|
+
self.neighborhood_fct = neighborhood_fct
|
|
133
|
+
self.alpha_init = alpha_init
|
|
134
|
+
self.alpha_drate = alpha_drate
|
|
135
|
+
lx = torch.arange(xs).repeat(ys).view(-1, ys).T.reshape(xs*ys)
|
|
136
|
+
ly = torch.arange(ys).repeat(xs)
|
|
137
|
+
self.coord = torch.stack((lx,ly), -1).to(device)
|
|
138
|
+
|
|
139
|
+
def to(self, device):
|
|
140
|
+
super(SOM, self).to(device)
|
|
141
|
+
self.somap = self.somap.to(device)
|
|
142
|
+
self.coord = self.coord.to(device)
|
|
143
|
+
|
|
144
|
+
def forward(self, x):
|
|
145
|
+
"""
|
|
146
|
+
Identifies the best matching unit for the data points in x in the current map.
|
|
147
|
+
|
|
148
|
+
Parameter:
|
|
149
|
+
----------
|
|
150
|
+
|
|
151
|
+
x: 2D tensor (N, dim) corresponding to N data points of dimension dim.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
--------
|
|
155
|
+
|
|
156
|
+
Returns a tuple including the coordinates of the bmu in the current map and the distance matrix used to find it.
|
|
157
|
+
"""
|
|
158
|
+
if type(x) != torch.Tensor: raise TypeError("x should be a tensor of shape (N,dim)")
|
|
159
|
+
if len(x.size()) != 2: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
160
|
+
if x.size()[1] != self.dim: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
161
|
+
dists = self.dist(self.somap, x)
|
|
162
|
+
bmu_ind = dists.min(dim=0).indices
|
|
163
|
+
bmu_ind_x = (bmu_ind/self.xs).to(torch.int32)
|
|
164
|
+
bmu_ind_y = bmu_ind%self.xs
|
|
165
|
+
return torch.stack((bmu_ind_x, bmu_ind_y), -1), dists
|
|
166
|
+
|
|
167
|
+
def __1DIndexTo2DIndex(self, ind):
|
|
168
|
+
return torch.Tensor([int(ind/self.xs), ind%self.xs])
|
|
169
|
+
|
|
170
|
+
def add(self, x):
|
|
171
|
+
"""
|
|
172
|
+
Add the data points in x to the current map and update the map to those points (training).
|
|
173
|
+
|
|
174
|
+
Parameters:
|
|
175
|
+
-----------
|
|
176
|
+
|
|
177
|
+
x: 2D tensor (N, dim) corresponding to N data points of dimension dim.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
--------
|
|
181
|
+
|
|
182
|
+
Returns the euclidean distance of the SOM's matrix before and after training. Gives an indication of the impact of the added training points on the map.
|
|
183
|
+
"""
|
|
184
|
+
if type(x) != torch.Tensor: raise TypeError("x should be a tensor of shape (N,dim)")
|
|
185
|
+
if len(x.size()) != 2: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
186
|
+
if x.size()[1] != self.dim: raise ValueError("x should be a tensor of shape (N,dim)")
|
|
187
|
+
prev_som = self.somap.clone().detach()
|
|
188
|
+
count = 0
|
|
189
|
+
for x_k in x:
|
|
190
|
+
if x_k.isnan().any() or x_k.isinf().any(): continue # do not try to add vector containing nans !
|
|
191
|
+
count+=1
|
|
192
|
+
# decreases linearly...
|
|
193
|
+
nb = max(self.neighborhood_drate, self.neighborhood_init - (self.step*self.neighborhood_drate))
|
|
194
|
+
alpha = max(self.alpha_drate, self.alpha_init - (self.step*self.alpha_drate))
|
|
195
|
+
self.step += 1
|
|
196
|
+
bmu = self(x_k.view(-1, x_k.size()[0]))[0][0]
|
|
197
|
+
theta = self.neighborhood_fct(bmu, (self.xs, self.ys), self.coord, nb)
|
|
198
|
+
ntheta = theta.view(-1, theta.size(0)).T
|
|
199
|
+
# TODO: print(ntheta) prob is that neg ones get to quickly neg and then go to infinity...
|
|
200
|
+
self.somap = self.somap + ntheta*(alpha*(x_k-self.somap))
|
|
201
|
+
if torch.isnan(self.somap).any() or torch.isinf(self.somap).any():
|
|
202
|
+
print("*** Nan! ***")
|
|
203
|
+
print("bmu", bmu)
|
|
204
|
+
print("theta", theta.min(), theta.max(), theta.mean(), theta.isnan().any())
|
|
205
|
+
print("ntheta", ntheta.min(), ntheta.max(), ntheta.mean(), ntheta.isnan().any())
|
|
206
|
+
print("alpha", alpha)
|
|
207
|
+
print("x_k", x_k.min(), x_k.max(), x_k.mean(), x_k.isnan().any())
|
|
208
|
+
# old non batch (slow) version
|
|
209
|
+
# batch here means calculating the whole map at once,
|
|
210
|
+
# not having a batch of values treated at once.
|
|
211
|
+
# for i, w_i in enumerate(self.somap):
|
|
212
|
+
# i2 = self.__1DIndexTo2DIndex(i)
|
|
213
|
+
# theta = self.neighborhood_fct(bmu, i2, nb)
|
|
214
|
+
# self.somap[i] = w_i + theta*alpha*(x_k-w_i)
|
|
215
|
+
# print("o nsomap", self.somap)
|
|
216
|
+
# wij' = wij + ( n_fct(bmu,ni,nb(s)) * alpha(s) * (x_k - wij) )
|
|
217
|
+
return float(torch.nn.functional.pairwise_distance(prev_som, self.somap).mean()), count
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pygame
|
|
5
|
+
from sklearn.decomposition import PCA
|
|
6
|
+
import ksom
|
|
7
|
+
import torch
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
def remspace(x): return x.replace(", ", ",") if type(x) == str else x
|
|
11
|
+
|
|
12
|
+
def onehotencode(df, col):
|
|
13
|
+
return pd.concat([df, df[col].apply(remspace).str.get_dummies(sep=",")], axis=1).drop(col, axis=1)
|
|
14
|
+
|
|
15
|
+
def findLabel(i, map, labels):
|
|
16
|
+
idx = abs(map.mean(dim=0)-map[i]).argmax()
|
|
17
|
+
lab = labels[idx]
|
|
18
|
+
if map.mean(dim=0)[idx] > map[i][idx]: lab = "not "+lab
|
|
19
|
+
return lab
|
|
20
|
+
|
|
21
|
+
def display(map, xoffset=0, labels=None, label_offset=0):
|
|
22
|
+
if map.shape[1] > 3:
|
|
23
|
+
pca = PCA(n_components=3)
|
|
24
|
+
somap = pca.fit_transform(map)
|
|
25
|
+
else: somap = map
|
|
26
|
+
for event in pygame.event.get():
|
|
27
|
+
if event.type == pygame.QUIT:
|
|
28
|
+
pygame.quit()
|
|
29
|
+
sys.exit()
|
|
30
|
+
unit = int(screen_size/smodel.xs)
|
|
31
|
+
for i,cs in enumerate(somap):
|
|
32
|
+
x = int(i/smodel.xs)
|
|
33
|
+
y = i%smodel.xs
|
|
34
|
+
x = (x*unit)+xoffset
|
|
35
|
+
y = y*unit
|
|
36
|
+
try :
|
|
37
|
+
color = (max(min(255, int(cs[0]*255)), 0),
|
|
38
|
+
max(min(255, int(cs[1]*255)), 0),
|
|
39
|
+
max(min(255, int(cs[2]*255)), 0))
|
|
40
|
+
except:
|
|
41
|
+
print(cs*255)
|
|
42
|
+
sys.exit(-1)
|
|
43
|
+
pygame.draw.rect(surface,
|
|
44
|
+
color,
|
|
45
|
+
pygame.Rect(x, y, unit, unit))
|
|
46
|
+
if labels is not None:
|
|
47
|
+
lab = findLabel(i, map, labels)
|
|
48
|
+
cp = surface.get_at((int(x+label_offset+unit/20),y+int(unit/5)))
|
|
49
|
+
cl = (200, 200, 200)
|
|
50
|
+
if cp[0] > 100 : cl = (0, 0, 0)
|
|
51
|
+
texts = font.render(lab, False, cl)
|
|
52
|
+
surface.blit(texts, (x+label_offset+unit/20,y+int(unit/5)))
|
|
53
|
+
|
|
54
|
+
pygame.display.flip()
|
|
55
|
+
pygame.display.update()
|
|
56
|
+
|
|
57
|
+
df = pd.read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/refs/heads/main/data/2024/2024-06-04/cheeses.csv", index_col=0)
|
|
58
|
+
df = df.drop(["url", "producers", "alt_spellings", "synonyms", "fat_content", "calcium_content", "vegetarian", "vegan"], axis=1)
|
|
59
|
+
print(df.columns)
|
|
60
|
+
df = onehotencode(df, "milk")
|
|
61
|
+
df = onehotencode(df, "country")
|
|
62
|
+
df = onehotencode(df, "region")
|
|
63
|
+
df = onehotencode(df, "family")
|
|
64
|
+
df = onehotencode(df, "type")
|
|
65
|
+
df = onehotencode(df, "texture")
|
|
66
|
+
df = onehotencode(df, "rind")
|
|
67
|
+
df = onehotencode(df, "color")
|
|
68
|
+
df = onehotencode(df, "flavor")
|
|
69
|
+
df = onehotencode(df, "aroma")
|
|
70
|
+
|
|
71
|
+
#print(df.aroma.apply(remspace).str.get_dummies(sep=","))
|
|
72
|
+
#df["vegetarian"] = df.vegetarian.apply(lambda x: int(x) if type(x)==bool else x)
|
|
73
|
+
#df["vegan"] = df.vegan.apply(lambda x: int(x) if type(x)==bool else x)
|
|
74
|
+
df = df.dropna()
|
|
75
|
+
|
|
76
|
+
screen_size=600 # size of screen
|
|
77
|
+
pygame.init()
|
|
78
|
+
surface = pygame.display.set_mode((screen_size*2,screen_size))
|
|
79
|
+
|
|
80
|
+
NBEPOCH = 10
|
|
81
|
+
BATCHSIZE = 100
|
|
82
|
+
SOMSIZE = 6
|
|
83
|
+
DIST = ksom.cosine_distance
|
|
84
|
+
|
|
85
|
+
pygame.font.init()
|
|
86
|
+
font = pygame.font.SysFont('Courrier',int((screen_size/6)/5))
|
|
87
|
+
|
|
88
|
+
smodel = ksom.SOM(SOMSIZE, SOMSIZE,
|
|
89
|
+
len(df.T),
|
|
90
|
+
zero_init=True,
|
|
91
|
+
# sample_init=torch.Tensor(df.sample(SOMSIZE*SOMSIZE).to_numpy()),
|
|
92
|
+
dist=DIST)
|
|
93
|
+
|
|
94
|
+
for epoch in range(NBEPOCH):
|
|
95
|
+
for b in range(math.ceil(len(df)/BATCHSIZE)):
|
|
96
|
+
dist,count = smodel.add(torch.Tensor(df.iloc[b*BATCHSIZE:(b+1)*BATCHSIZE].to_numpy()))
|
|
97
|
+
print(f"{epoch+1:02d}.{b:02d}: distance {dist:.4f} out of {count} objects")
|
|
98
|
+
freqmap = torch.zeros(SOMSIZE*SOMSIZE)
|
|
99
|
+
bmu,dists = smodel(torch.Tensor(df.to_numpy()))
|
|
100
|
+
for i in bmu: freqmap[i[0]*SOMSIZE+i[1]] += 1
|
|
101
|
+
freqmap = (freqmap - freqmap.min())/(freqmap.max()-freqmap.min())
|
|
102
|
+
freqmap = freqmap.view(len(freqmap), 1).repeat((1,3))
|
|
103
|
+
display(freqmap, xoffset=screen_size)
|
|
104
|
+
display(smodel.somap, labels=list(df.columns), label_offset=screen_size)
|
|
105
|
+
|
|
106
|
+
# continue to keep the display alive
|
|
107
|
+
while True:
|
|
108
|
+
for event in pygame.event.get():
|
|
109
|
+
if event.type == pygame.QUIT:
|
|
110
|
+
pygame.quit()
|
|
111
|
+
sys.exit()
|
|
112
|
+
time.sleep(0.1)
|
|
113
|
+
pygame.display.flip()
|
|
114
|
+
pygame.display.update()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from ksom import euclidean_distance, cosine_distance
|
|
3
|
+
from scipy.spatial import distance
|
|
4
|
+
|
|
5
|
+
som_size = 10
|
|
6
|
+
dim = 128
|
|
7
|
+
batch_size = 256
|
|
8
|
+
|
|
9
|
+
som = torch.randn(som_size*som_size, 128)
|
|
10
|
+
batch = torch.randn(batch_size, dim)
|
|
11
|
+
|
|
12
|
+
euc1 = euclidean_distance(batch, som)
|
|
13
|
+
euc2 = []
|
|
14
|
+
|
|
15
|
+
cos1 = cosine_distance(batch, som)
|
|
16
|
+
cos2 = []
|
|
17
|
+
|
|
18
|
+
for s in batch:
|
|
19
|
+
seuc = []
|
|
20
|
+
scos = []
|
|
21
|
+
for c in som:
|
|
22
|
+
seuc.append(distance.euclidean(s, c))
|
|
23
|
+
scos.append(distance.cosine(s, c))
|
|
24
|
+
euc2.append(torch.Tensor(seuc))
|
|
25
|
+
cos2.append(torch.Tensor(scos))
|
|
26
|
+
euc2 = torch.cat(euc2).view(batch_size, som_size*som_size)
|
|
27
|
+
cos2 = torch.cat(cos2).view(batch_size, som_size*som_size)
|
|
28
|
+
|
|
29
|
+
print("euclidean distance:", float(abs(euc1-euc2).mean()))
|
|
30
|
+
print("cosine distance:", float(abs(cos1-cos2).mean()))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if len(sys.argv) < 3:
|
|
4
|
+
print("No image or map size provided, default parameters (chica.jpg, 6) will be used.")
|
|
5
|
+
img = "chica.jpg"
|
|
6
|
+
som_size = 6
|
|
7
|
+
else:
|
|
8
|
+
img = sys.argv[1]
|
|
9
|
+
if not sys.argv[2].isnumeric():
|
|
10
|
+
print("Second argument should be a number.")
|
|
11
|
+
sys.exit(-1)
|
|
12
|
+
som_size = int(sys.argv[2])
|
|
13
|
+
disp = True
|
|
14
|
+
if len(sys.argv) >=4: disp = sys.argv[3] != "nodisplay"
|
|
15
|
+
|
|
16
|
+
from PIL import Image
|
|
17
|
+
from torchvision import transforms
|
|
18
|
+
from ksom import SOM, cosine_distance, nb_linear, nb_gaussian, nb_ricker
|
|
19
|
+
if disp: import pygame
|
|
20
|
+
import torch
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
# init display screen and function to display map
|
|
24
|
+
if disp:
|
|
25
|
+
screen_size=600 # size of screen
|
|
26
|
+
pygame.init()
|
|
27
|
+
surface = pygame.display.set_mode((screen_size,screen_size))
|
|
28
|
+
|
|
29
|
+
def display(smodel):
|
|
30
|
+
for event in pygame.event.get():
|
|
31
|
+
if event.type == pygame.QUIT:
|
|
32
|
+
pygame.quit()
|
|
33
|
+
sys.exit()
|
|
34
|
+
unit = int(screen_size/som_size)
|
|
35
|
+
for i,cs in enumerate(smodel.somap):
|
|
36
|
+
x = int(i/som_size)
|
|
37
|
+
y = i%som_size
|
|
38
|
+
x = x*unit
|
|
39
|
+
y = y*unit
|
|
40
|
+
try :
|
|
41
|
+
color = (max(min(255, int(cs[0]*255)), 0),
|
|
42
|
+
max(min(255, int(cs[1]*255)), 0),
|
|
43
|
+
max(min(255, int(cs[2]*255)), 0))
|
|
44
|
+
except:
|
|
45
|
+
print(cs*255)
|
|
46
|
+
sys.exit(-1)
|
|
47
|
+
pygame.draw.rect(surface,
|
|
48
|
+
color,
|
|
49
|
+
pygame.Rect(x, y, unit, unit))
|
|
50
|
+
pygame.display.flip()
|
|
51
|
+
pygame.display.update()
|
|
52
|
+
|
|
53
|
+
# open image, transform into tensor, and create shuffle index
|
|
54
|
+
im= Image.open(img)
|
|
55
|
+
x= transforms.ToTensor()(im)
|
|
56
|
+
x = x[:-1] if x.size(0) == 4 else x # remove alpha layer if there is one
|
|
57
|
+
x = x.view(-1, x.size()[1]*x.size()[2]).transpose(0,1)
|
|
58
|
+
perm = torch.randperm(x.size(0))
|
|
59
|
+
|
|
60
|
+
# init SOM model
|
|
61
|
+
samples = x[perm[-(som_size*som_size):]]
|
|
62
|
+
smodel = SOM(som_size, som_size, 3, sample_init=samples, # zero_init=False,
|
|
63
|
+
dist=cosine_distance,
|
|
64
|
+
alpha_init=0.01, alpha_drate=1e-7,
|
|
65
|
+
neighborhood_fct=nb_gaussian, neighborhood_init=som_size, neighborhood_drate=0.0001)
|
|
66
|
+
|
|
67
|
+
device = "cpu"
|
|
68
|
+
if torch.cuda.is_available():
|
|
69
|
+
device = "cuda:0"
|
|
70
|
+
x = x.to(device)
|
|
71
|
+
smodel.to(device)
|
|
72
|
+
print("Running on CUDA")
|
|
73
|
+
|
|
74
|
+
# train (1 pass through all the pixels) by batches of 1000 pixels
|
|
75
|
+
for i in range(int(x.size()[0]/1000)):
|
|
76
|
+
idx = perm[i*1000:(i+1)*1000]
|
|
77
|
+
time1 = time.time()
|
|
78
|
+
dist,count = smodel.add(x[idx])
|
|
79
|
+
print(f"{(i+1):06d}K - {dist:.4f} - {(time.time()-time1)*1000:05.2f}ms")
|
|
80
|
+
if disp: display(smodel)
|
|
81
|
+
|
|
82
|
+
# continue to keep the display alive
|
|
83
|
+
if display:
|
|
84
|
+
while True:
|
|
85
|
+
for event in pygame.event.get():
|
|
86
|
+
if event.type == pygame.QUIT:
|
|
87
|
+
pygame.quit()
|
|
88
|
+
sys.exit()
|
|
89
|
+
time.sleep(0.1)
|
|
90
|
+
pygame.display.flip()
|
|
91
|
+
pygame.display.update()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import torch
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
if len(sys.argv) < 4:
|
|
6
|
+
print("Provide the name of the neighborhood function to test and the size (N) of map (NxN) and radius of neighborhood function, using default params for now (ricker,20,6)")
|
|
7
|
+
sys.argv.append("ricker")
|
|
8
|
+
sys.argv.append("20")
|
|
9
|
+
sys.argv.append("6")
|
|
10
|
+
if not sys.argv[2].isnumeric():
|
|
11
|
+
print("Second argument should be a number.")
|
|
12
|
+
sys.exit(-1)
|
|
13
|
+
if not sys.argv[3].isnumeric():
|
|
14
|
+
print("Third argument should be a number.")
|
|
15
|
+
sys.exit(-1)
|
|
16
|
+
|
|
17
|
+
from ksom import SOM, nb_linear, nb_gaussian, nb_ricker
|
|
18
|
+
|
|
19
|
+
if sys.argv[1] == "linear":
|
|
20
|
+
fct = nb_linear
|
|
21
|
+
elif sys.argv[1] == "gaussian":
|
|
22
|
+
fct = nb_gaussian
|
|
23
|
+
elif sys.argv[1] == "ricker":
|
|
24
|
+
fct = nb_ricker
|
|
25
|
+
else:
|
|
26
|
+
print("Neighboorhood function should be linear, gaussian or ricker")
|
|
27
|
+
sys.exit(-1)
|
|
28
|
+
|
|
29
|
+
import pygame
|
|
30
|
+
|
|
31
|
+
# init display screen and function to display map
|
|
32
|
+
screen_size=600 # size of screen
|
|
33
|
+
pygame.init()
|
|
34
|
+
surface = pygame.display.set_mode((screen_size,screen_size))
|
|
35
|
+
|
|
36
|
+
def display(distmatrix):
|
|
37
|
+
unit = int(screen_size/som_size)
|
|
38
|
+
for i,cs in enumerate(distmatrix):
|
|
39
|
+
x = int(i/som_size)
|
|
40
|
+
y = i%som_size
|
|
41
|
+
x = x*unit
|
|
42
|
+
y = y*unit
|
|
43
|
+
color = (max(min(255, int(-cs*255)), 0),
|
|
44
|
+
max(min(255, int(cs*255)), 0),
|
|
45
|
+
max(min(255, int(cs*255)), 0))
|
|
46
|
+
pygame.draw.rect(surface,
|
|
47
|
+
color,
|
|
48
|
+
pygame.Rect(x, y, unit, unit))
|
|
49
|
+
pygame.display.flip()
|
|
50
|
+
|
|
51
|
+
# init SOM model
|
|
52
|
+
som_size = int(sys.argv[2]) # size of som (square, so som_size x som_size)
|
|
53
|
+
smodel = SOM(som_size, som_size, 3)
|
|
54
|
+
|
|
55
|
+
node = (int(som_size/2), int(som_size/2))
|
|
56
|
+
|
|
57
|
+
distmtx = fct(torch.tensor(node), (som_size, som_size), smodel.coord, int(sys.argv[3]))
|
|
58
|
+
display(distmtx)
|
|
59
|
+
|
|
60
|
+
# continue to keep the display alive
|
|
61
|
+
while True: time.sleep(10)
|
|
62
|
+
|