fastersom 0.2.0__tar.gz → 0.3.0__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.
- {fastersom-0.2.0 → fastersom-0.3.0}/PKG-INFO +25 -13
- {fastersom-0.2.0 → fastersom-0.3.0}/README.md +24 -12
- {fastersom-0.2.0 → fastersom-0.3.0}/pyproject.toml +1 -1
- {fastersom-0.2.0 → fastersom-0.3.0}/pyproject.toml.orig +1 -1
- {fastersom-0.2.0 → fastersom-0.3.0}/src/fastersom/_som.py +60 -24
- {fastersom-0.2.0 → fastersom-0.3.0}/LICENSE +0 -0
- {fastersom-0.2.0 → fastersom-0.3.0}/src/fastersom/__init__.py +0 -0
- {fastersom-0.2.0 → fastersom-0.3.0}/src/fastersom/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastersom
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Fast batch self-organising maps (SOM) in PyTorch for GPU and CPU
|
|
5
5
|
Keywords: self-organizing map,som,kohonen,clustering,pytorch,gpu,spatial omics
|
|
6
6
|
Author: Darius Schaub
|
|
@@ -30,9 +30,9 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
A small, fast batch self-organising map (SOM) in PyTorch: a reimplementation of Kohonen's batch map with a
|
|
32
32
|
rectangular grid, gaussian neighbourhood and exponentially decaying neighbourhood width. Its defaults reproduce the
|
|
33
|
-
maps of [
|
|
34
|
-
MPS or CPU) and the per-epoch update uses per-node sums, so an epoch
|
|
35
|
-
product.
|
|
33
|
+
batch-trained maps of [XPySom](https://github.com/Manciukic/xpysom) with a gaussian neighbourhood of initial width
|
|
34
|
+
half the grid. Data stays on the device (CUDA, MPS or CPU) and the per-epoch update uses per-node sums, so an epoch
|
|
35
|
+
costs one BMU search plus a (nodes x nodes) matrix product.
|
|
36
36
|
|
|
37
37
|
## Install
|
|
38
38
|
|
|
@@ -72,21 +72,33 @@ Options:
|
|
|
72
72
|
(`SOM(init=som.weights_).fit(X)`); with `n_epochs=0`, `fit` keeps them unchanged. The sigma schedule restarts at
|
|
73
73
|
`sigma_initial`, which with the default largely retrains the map; to continue training instead, pass a small
|
|
74
74
|
`sigma_initial`, e.g. the previous map's `sigma_final`.
|
|
75
|
-
- `sigma_initial` and `sigma_final` set the neighbourhood width schedule; the defaults reproduce
|
|
76
|
-
- `batch_size` bounds memory (about `batch_size x n_nodes` floats) without changing the algorithm.
|
|
75
|
+
- `sigma_initial` and `sigma_final` set the neighbourhood width schedule; the defaults reproduce XPySom's.
|
|
76
|
+
- `batch_size` bounds memory (about `batch_size x n_nodes` floats) without changing the algorithm. On CUDA and MPS
|
|
77
|
+
it defaults to the largest batch that still helps speed and fits in `memory_fraction` (default 0.9 on CUDA, 0.5
|
|
78
|
+
on MPS) of the free GPU memory, so it rarely needs tuning.
|
|
77
79
|
|
|
78
80
|
Fitted maps pickle without device tensors and load on the configured device if available, else on the default one,
|
|
79
81
|
so a map fitted on a GPU loads on a CPU-only machine. `BatchSOM` is an alias for `SOM`.
|
|
80
82
|
|
|
81
83
|
## Benchmark
|
|
82
84
|
|
|
83
|
-
480,000 x 34 pixels, 30 x 30 nodes, 10 epochs,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
85
|
+
480,000 x 34 pixels, 30 x 30 nodes, 10 epochs, mean of 3 seeds; peak GPU memory includes the data
|
|
86
|
+
([`benchmarks/memory.py`](https://github.com/dschaub95/fastersom/blob/main/benchmarks/memory.py)).
|
|
87
|
+
|
|
88
|
+
| implementation | hardware | batch size | fit time | peak GPU memory | quantization error |
|
|
89
|
+
| ---------------------------------------------------- | ------------ | ----------------- | -------- | --------------- | ------------------ |
|
|
90
|
+
| fastersom | NVIDIA A100 | default (298,261) | 0.12 s | 1150 MiB | 0.168 |
|
|
91
|
+
| fastersom | NVIDIA A100 | 32,768 | 0.12 s | 204 MiB | 0.168 |
|
|
92
|
+
| fastersom | NVIDIA A100 | 8,096 | 0.20 s | 118 MiB | 0.168 |
|
|
93
|
+
| [XPySom](https://github.com/Manciukic/xpysom) (CuPy) | NVIDIA A100 | 8,096 | 0.46 s | 313 MiB | 0.169 |
|
|
94
|
+
| XPySom (CuPy) | NVIDIA A100 | 65,536 | 0.41 s | 2088 MiB | 0.169 |
|
|
95
|
+
| [torchsom](https://github.com/michelin/TorchSOM) | NVIDIA A100 | 1,024 | 21.7 s | 44 MiB | 0.175 |
|
|
96
|
+
| fastersom | 256-core CPU | default (8,192) | 1.8 s | - | 0.169 |
|
|
97
|
+
| XPySom (NumPy) | 256-core CPU | 8,096 | 40.6 s | - | 0.168 |
|
|
98
|
+
| torchsom | 256-core CPU | 1,024 | 39.2 s | - | 0.176 |
|
|
99
|
+
|
|
100
|
+
At equal GPU memory fastersom is about 4x faster than XPySom, and about 20x faster on the CPU. torchsom updates the map
|
|
101
|
+
per mini-batch, so it needs small batches and is far slower.
|
|
90
102
|
|
|
91
103
|
## Algorithm
|
|
92
104
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A small, fast batch self-organising map (SOM) in PyTorch: a reimplementation of Kohonen's batch map with a
|
|
4
4
|
rectangular grid, gaussian neighbourhood and exponentially decaying neighbourhood width. Its defaults reproduce the
|
|
5
|
-
maps of [
|
|
6
|
-
MPS or CPU) and the per-epoch update uses per-node sums, so an epoch
|
|
7
|
-
product.
|
|
5
|
+
batch-trained maps of [XPySom](https://github.com/Manciukic/xpysom) with a gaussian neighbourhood of initial width
|
|
6
|
+
half the grid. Data stays on the device (CUDA, MPS or CPU) and the per-epoch update uses per-node sums, so an epoch
|
|
7
|
+
costs one BMU search plus a (nodes x nodes) matrix product.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -44,21 +44,33 @@ Options:
|
|
|
44
44
|
(`SOM(init=som.weights_).fit(X)`); with `n_epochs=0`, `fit` keeps them unchanged. The sigma schedule restarts at
|
|
45
45
|
`sigma_initial`, which with the default largely retrains the map; to continue training instead, pass a small
|
|
46
46
|
`sigma_initial`, e.g. the previous map's `sigma_final`.
|
|
47
|
-
- `sigma_initial` and `sigma_final` set the neighbourhood width schedule; the defaults reproduce
|
|
48
|
-
- `batch_size` bounds memory (about `batch_size x n_nodes` floats) without changing the algorithm.
|
|
47
|
+
- `sigma_initial` and `sigma_final` set the neighbourhood width schedule; the defaults reproduce XPySom's.
|
|
48
|
+
- `batch_size` bounds memory (about `batch_size x n_nodes` floats) without changing the algorithm. On CUDA and MPS
|
|
49
|
+
it defaults to the largest batch that still helps speed and fits in `memory_fraction` (default 0.9 on CUDA, 0.5
|
|
50
|
+
on MPS) of the free GPU memory, so it rarely needs tuning.
|
|
49
51
|
|
|
50
52
|
Fitted maps pickle without device tensors and load on the configured device if available, else on the default one,
|
|
51
53
|
so a map fitted on a GPU loads on a CPU-only machine. `BatchSOM` is an alias for `SOM`.
|
|
52
54
|
|
|
53
55
|
## Benchmark
|
|
54
56
|
|
|
55
|
-
480,000 x 34 pixels, 30 x 30 nodes, 10 epochs,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
57
|
+
480,000 x 34 pixels, 30 x 30 nodes, 10 epochs, mean of 3 seeds; peak GPU memory includes the data
|
|
58
|
+
([`benchmarks/memory.py`](https://github.com/dschaub95/fastersom/blob/main/benchmarks/memory.py)).
|
|
59
|
+
|
|
60
|
+
| implementation | hardware | batch size | fit time | peak GPU memory | quantization error |
|
|
61
|
+
| ---------------------------------------------------- | ------------ | ----------------- | -------- | --------------- | ------------------ |
|
|
62
|
+
| fastersom | NVIDIA A100 | default (298,261) | 0.12 s | 1150 MiB | 0.168 |
|
|
63
|
+
| fastersom | NVIDIA A100 | 32,768 | 0.12 s | 204 MiB | 0.168 |
|
|
64
|
+
| fastersom | NVIDIA A100 | 8,096 | 0.20 s | 118 MiB | 0.168 |
|
|
65
|
+
| [XPySom](https://github.com/Manciukic/xpysom) (CuPy) | NVIDIA A100 | 8,096 | 0.46 s | 313 MiB | 0.169 |
|
|
66
|
+
| XPySom (CuPy) | NVIDIA A100 | 65,536 | 0.41 s | 2088 MiB | 0.169 |
|
|
67
|
+
| [torchsom](https://github.com/michelin/TorchSOM) | NVIDIA A100 | 1,024 | 21.7 s | 44 MiB | 0.175 |
|
|
68
|
+
| fastersom | 256-core CPU | default (8,192) | 1.8 s | - | 0.169 |
|
|
69
|
+
| XPySom (NumPy) | 256-core CPU | 8,096 | 40.6 s | - | 0.168 |
|
|
70
|
+
| torchsom | 256-core CPU | 1,024 | 39.2 s | - | 0.176 |
|
|
71
|
+
|
|
72
|
+
At equal GPU memory fastersom is about 4x faster than XPySom, and about 20x faster on the CPU. torchsom updates the map
|
|
73
|
+
per mini-batch, so it needs small batches and is far slower.
|
|
62
74
|
|
|
63
75
|
## Algorithm
|
|
64
76
|
|
|
@@ -51,8 +51,6 @@ def _is_available(device: torch.device) -> bool:
|
|
|
51
51
|
class SOM:
|
|
52
52
|
"""Batch self-organising map on a rectangular grid with a gaussian neighbourhood.
|
|
53
53
|
|
|
54
|
-
The defaults reproduce the maps of spatiomic's ``so.dimension.som``.
|
|
55
|
-
|
|
56
54
|
Args:
|
|
57
55
|
n_rows: Number of grid rows.
|
|
58
56
|
n_cols: Number of grid columns.
|
|
@@ -79,8 +77,11 @@ class SOM:
|
|
|
79
77
|
batch_size: Samples processed at once in BMU search, aggregation, predict and transform. Affects memory
|
|
80
78
|
(about batch_size x n_nodes floats) and speed only. Each epoch's update is the same up to float
|
|
81
79
|
summation order, but over several epochs such rounding differences (as between devices) can flip
|
|
82
|
-
near-tied BMUs, so maps agree in quality rather than bit for bit. Defaults to
|
|
83
|
-
|
|
80
|
+
near-tied BMUs, so maps agree in quality rather than bit for bit. Defaults to 8192 on CPU and on GPUs
|
|
81
|
+
to 2^28 / n_nodes (where speed levels off), fewer if that does not fit in memory_fraction of the free GPU
|
|
82
|
+
memory (on MPS, of the memory torch recommends; 65536 before torch 2.5).
|
|
83
|
+
memory_fraction: Fraction of the free GPU memory the default batch_size may use, in (0, 1]. Defaults to 0.9
|
|
84
|
+
on CUDA and 0.5 on MPS, whose memory the system and other apps share.
|
|
84
85
|
|
|
85
86
|
Attributes:
|
|
86
87
|
weights_: Node weights, shape (n_rows * n_cols, n_features), row-major (node = row * n_cols + col).
|
|
@@ -100,6 +101,7 @@ class SOM:
|
|
|
100
101
|
random_state: int | None = None,
|
|
101
102
|
device: str | torch.device | None = None,
|
|
102
103
|
batch_size: int | None = None,
|
|
104
|
+
memory_fraction: float | None = None,
|
|
103
105
|
):
|
|
104
106
|
self.n_rows = n_rows
|
|
105
107
|
self.n_cols = n_cols
|
|
@@ -111,6 +113,7 @@ class SOM:
|
|
|
111
113
|
self.random_state = random_state
|
|
112
114
|
self.device = device
|
|
113
115
|
self.batch_size = batch_size
|
|
116
|
+
self.memory_fraction = memory_fraction
|
|
114
117
|
|
|
115
118
|
@classmethod
|
|
116
119
|
def from_weights(cls, weights: ArrayLike | torch.Tensor, n_rows: int, n_cols: int, **kwargs) -> "SOM":
|
|
@@ -130,6 +133,7 @@ class SOM:
|
|
|
130
133
|
|
|
131
134
|
def __setstate__(self, state: dict) -> None:
|
|
132
135
|
"""Restores a pickled map on the configured device if available (cpu, cuda, mps), else the default one."""
|
|
136
|
+
state.setdefault("memory_fraction", None) # pickled before 0.3
|
|
133
137
|
self.__dict__.update(state)
|
|
134
138
|
if hasattr(self, "weights_"):
|
|
135
139
|
self._setup(fall_back=True)
|
|
@@ -145,24 +149,29 @@ class SOM:
|
|
|
145
149
|
raise ValueError(f"{name} must be an integer >= {minimum}, got {getattr(self, name)!r}")
|
|
146
150
|
if self.batch_size is not None and not _is_int(self.batch_size, 1):
|
|
147
151
|
raise ValueError(f"batch_size must be None or an integer >= 1, got {self.batch_size!r}")
|
|
152
|
+
if self.memory_fraction is not None and not 0 < self.memory_fraction <= 1:
|
|
153
|
+
raise ValueError(f"memory_fraction must be None or in (0, 1], got {self.memory_fraction!r}")
|
|
148
154
|
self._device = torch.device(self.device) if self.device is not None else _default_device()
|
|
149
155
|
if fall_back and not _is_available(self._device):
|
|
150
156
|
self._device = _default_device()
|
|
151
|
-
self._batch_size = self.batch_size or (8192 if self._device.type == "cpu" else 1 << 16)
|
|
152
157
|
self._sigma0 = ceil(min(self.n_rows, self.n_cols) / 2) / 2 if self.sigma_initial is None else self.sigma_initial
|
|
153
158
|
self._sigma1 = 0.1 * self._sigma0 if self.sigma_final is None else self.sigma_final
|
|
154
159
|
if not all(isfinite(sigma) and sigma > 0 for sigma in (self._sigma0, self._sigma1)):
|
|
155
160
|
raise ValueError("sigma_initial and sigma_final must be finite and > 0")
|
|
156
161
|
self._coords = torch.cartesian_prod(torch.arange(self.n_rows), torch.arange(self.n_cols))
|
|
157
162
|
self._coords = self._coords.to(self._device, torch.float32)
|
|
158
|
-
self._grid_sq_dist = torch.cdist(self._coords, self._coords) ** 2
|
|
159
163
|
|
|
160
164
|
def _as_tensor(self, X: ArrayLike | torch.Tensor) -> torch.Tensor:
|
|
161
|
-
|
|
162
|
-
X
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
if isinstance(X, torch.Tensor):
|
|
166
|
+
X = X.detach()
|
|
167
|
+
elif hasattr(X, "__dlpack__") and not isinstance(X, np.ndarray): # e.g. CuPy, without leaving the GPU
|
|
168
|
+
try:
|
|
169
|
+
X = torch.from_dlpack(X)
|
|
170
|
+
except RuntimeError: # torch cannot import negative strides
|
|
171
|
+
X = torch.from_dlpack(X.copy())
|
|
172
|
+
else: # numpy refuses to export read-only arrays through DLPack
|
|
173
|
+
X = np.ascontiguousarray(X)
|
|
174
|
+
return torch.as_tensor(X, dtype=torch.float32, device=self._device)
|
|
166
175
|
|
|
167
176
|
def _check_X(self, X: ArrayLike | torch.Tensor) -> torch.Tensor:
|
|
168
177
|
data = self._as_tensor(X)
|
|
@@ -171,10 +180,25 @@ class SOM:
|
|
|
171
180
|
data = data.reshape(-1, data.shape[-1])
|
|
172
181
|
if hasattr(self, "n_features_in_") and data.shape[1] != self.n_features_in_:
|
|
173
182
|
raise ValueError(f"X has {data.shape[1]} features, but SOM was fitted with {self.n_features_in_}")
|
|
174
|
-
if not torch.isfinite(data).all():
|
|
183
|
+
if not torch.isfinite(torch.stack(torch.aminmax(data))).all(): # unlike isfinite(data), without a copy of X
|
|
175
184
|
raise ValueError("X contains NaN or infinite values")
|
|
176
185
|
return data
|
|
177
186
|
|
|
187
|
+
def _batch_size_for(self, data: torch.Tensor) -> int:
|
|
188
|
+
if self.batch_size is not None:
|
|
189
|
+
return self.batch_size
|
|
190
|
+
if self._device.type == "cuda":
|
|
191
|
+
cached = torch.cuda.memory_reserved(self._device) - torch.cuda.memory_allocated(self._device)
|
|
192
|
+
free, fraction = torch.cuda.mem_get_info(self._device)[0] + cached, 0.9
|
|
193
|
+
elif self._device.type == "mps" and hasattr(torch.mps, "recommended_max_memory"): # torch >= 2.5
|
|
194
|
+
free, fraction = torch.mps.recommended_max_memory() - torch.mps.current_allocated_memory(), 0.5
|
|
195
|
+
else:
|
|
196
|
+
return 8192 if self._device.type == "cpu" else 1 << 16
|
|
197
|
+
fraction = fraction if self.memory_fraction is None else self.memory_fraction
|
|
198
|
+
n_nodes = self.n_rows * self.n_cols
|
|
199
|
+
# BMU search holds at most about 2 floats per sample and node, plus copies of the batch
|
|
200
|
+
return max(1, min((1 << 28) // n_nodes, int(fraction * free) // (8 * (n_nodes + data.shape[1]))))
|
|
201
|
+
|
|
178
202
|
def _check_fitted(self) -> None:
|
|
179
203
|
if not hasattr(self, "weights_"):
|
|
180
204
|
raise RuntimeError("This SOM instance is not fitted yet, call fit() first")
|
|
@@ -233,11 +257,14 @@ class SOM:
|
|
|
233
257
|
def _scores(self, batch: torch.Tensor, weights: torch.Tensor) -> torch.Tensor:
|
|
234
258
|
"""Per-node score to minimise.
|
|
235
259
|
|
|
236
|
-
||w||^2 - 2 x.w
|
|
237
|
-
|
|
260
|
+
||w - c||^2 - 2 (x - c).(w - c) with c the mean node (euclidean, ||x - c||^2 dropped; centring keeps a large
|
|
261
|
+
offset from cancelling), the L1 distance (manhattan) or -x.w / ||w|| of the (for correlation centred) vectors
|
|
262
|
+
(cosine, correlation).
|
|
238
263
|
"""
|
|
239
264
|
if self.metric == "euclidean":
|
|
240
|
-
|
|
265
|
+
centre = weights.mean(0)
|
|
266
|
+
weights = weights - centre
|
|
267
|
+
return torch.addmm((weights**2).sum(1), batch - centre, weights.T, alpha=-2)
|
|
241
268
|
if self.metric == "manhattan":
|
|
242
269
|
return torch.cdist(batch, weights, p=1)
|
|
243
270
|
weights = self._centre(weights)
|
|
@@ -248,14 +275,15 @@ class SOM:
|
|
|
248
275
|
"""Distances from the scores of any number of nodes per sample, for every metric but euclidean."""
|
|
249
276
|
if self.metric == "manhattan":
|
|
250
277
|
return scores
|
|
251
|
-
|
|
278
|
+
# in place, as transform's scores hold a batch x nodes matrix; callers pass scores they no longer need
|
|
279
|
+
return scores.div_(self._centre(batch).norm(dim=1, keepdim=True)).add_(1).clamp_(0, 2)
|
|
252
280
|
|
|
253
281
|
def _bmus(
|
|
254
282
|
self, data: torch.Tensor, weights: torch.Tensor, return_distance: bool = True
|
|
255
283
|
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
|
256
284
|
"""BMU and, if return_distance, distance to it per sample; -1 and NaN for samples without a BMU."""
|
|
257
285
|
bmus, distances = [], []
|
|
258
|
-
for batch in data.split(self.
|
|
286
|
+
for batch in data.split(self._batch_size_for(data)):
|
|
259
287
|
score, bmu = self._scores(batch, weights).min(1)
|
|
260
288
|
bmus.append(bmu)
|
|
261
289
|
if not return_distance:
|
|
@@ -277,8 +305,9 @@ class SOM:
|
|
|
277
305
|
"""Fits the map, starting from the initialisation given by init.
|
|
278
306
|
|
|
279
307
|
Args:
|
|
280
|
-
X: Samples, shape (n_samples, n_features) or (..., n_features); numpy array, array-like
|
|
281
|
-
Any real values are fine; data is copied to the device once and stays
|
|
308
|
+
X: Samples, shape (n_samples, n_features) or (..., n_features); numpy array, array-like, torch tensor
|
|
309
|
+
or DLPack array such as CuPy. Any real values are fine; data is copied to the device once and stays
|
|
310
|
+
there.
|
|
282
311
|
y: Ignored.
|
|
283
312
|
|
|
284
313
|
Returns:
|
|
@@ -289,19 +318,21 @@ class SOM:
|
|
|
289
318
|
self.__dict__.pop(attr, None)
|
|
290
319
|
data = self._check_X(X)
|
|
291
320
|
weights = self._init_weights(data)
|
|
292
|
-
n_nodes = weights.shape[0]
|
|
321
|
+
n_nodes, batch_size = weights.shape[0], self._batch_size_for(data)
|
|
293
322
|
for t in range(self.n_epochs):
|
|
294
323
|
sigma = self._sigma0 * (self._sigma1 / self._sigma0) ** (t / self.n_epochs)
|
|
295
324
|
sums, counts = torch.zeros_like(weights), weights.new_zeros(n_nodes)
|
|
296
|
-
for batch in data.split(
|
|
325
|
+
for batch in data.split(batch_size):
|
|
297
326
|
bmus = self._scores(batch, weights).argmin(1)
|
|
298
327
|
keep = self._has_bmu(batch) # samples without a BMU do not contribute
|
|
299
328
|
if keep is not None:
|
|
300
329
|
batch, bmus = batch[keep], bmus[keep]
|
|
301
330
|
sums.index_add_(0, bmus, batch)
|
|
302
331
|
counts += torch.bincount(bmus, minlength=n_nodes)
|
|
303
|
-
|
|
332
|
+
# in place, so large maps hold one nodes x nodes matrix at a time
|
|
333
|
+
h = torch.cdist(self._coords, self._coords).square_().mul_(-0.5 / sigma**2).exp_()
|
|
304
334
|
numerator, denominator = h @ sums, h @ counts
|
|
335
|
+
del h # before the next epoch's BMU search and neighbourhood matrix
|
|
305
336
|
weights = torch.where(denominator[:, None] > 0, numerator / denominator[:, None], weights)
|
|
306
337
|
self._set_weights(weights)
|
|
307
338
|
self.labels_ = self._bmus(data, weights, return_distance=False)[0].cpu().numpy()
|
|
@@ -352,7 +383,12 @@ class SOM:
|
|
|
352
383
|
"""
|
|
353
384
|
self._check_fitted()
|
|
354
385
|
data = self._check_X(X)
|
|
355
|
-
|
|
386
|
+
batch_size = self._batch_size_for(data)
|
|
387
|
+
# on the CPU, as it can exceed device memory
|
|
388
|
+
distances = torch.empty(len(data), len(self._weights), dtype=torch.float32, device="cpu")
|
|
389
|
+
for batch, rows in zip(data.split(batch_size), distances.split(batch_size), strict=True):
|
|
390
|
+
rows.copy_(self._distances(batch))
|
|
391
|
+
return distances.numpy()
|
|
356
392
|
|
|
357
393
|
def fit_transform(self, X: ArrayLike | torch.Tensor, y: object = None) -> np.ndarray:
|
|
358
394
|
"""Fits the map and returns transform(X)."""
|
|
@@ -376,7 +412,7 @@ class SOM:
|
|
|
376
412
|
nodes = torch.cat(
|
|
377
413
|
[
|
|
378
414
|
self._scores(batch, self._weights).topk(2, dim=1, largest=False).indices
|
|
379
|
-
for batch in data.split(self.
|
|
415
|
+
for batch in data.split(self._batch_size_for(data))
|
|
380
416
|
]
|
|
381
417
|
)
|
|
382
418
|
offset = (self._coords[nodes[:, 0]] - self._coords[nodes[:, 1]]).abs().amax(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|