umap-learn 0.5.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- umap/__init__.py +41 -0
- umap/aligned_umap.py +561 -0
- umap/distances.py +1300 -0
- umap/layouts.py +1079 -0
- umap/parametric_umap.py +1021 -0
- umap/plot.py +1625 -0
- umap/sparse.py +623 -0
- umap/spectral.py +556 -0
- umap/umap_.py +3556 -0
- umap/utils.py +220 -0
- umap/validation.py +86 -0
- umap_learn-0.5.6.dist-info/LICENSE.txt +29 -0
- umap_learn-0.5.6.dist-info/METADATA +564 -0
- umap_learn-0.5.6.dist-info/RECORD +16 -0
- umap_learn-0.5.6.dist-info/WHEEL +5 -0
- umap_learn-0.5.6.dist-info/top_level.txt +1 -0
umap/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from warnings import warn, catch_warnings, simplefilter
|
|
2
|
+
from .umap_ import UMAP
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
with catch_warnings():
|
|
6
|
+
simplefilter("ignore")
|
|
7
|
+
from .parametric_umap import ParametricUMAP
|
|
8
|
+
except ImportError:
|
|
9
|
+
warn(
|
|
10
|
+
"Tensorflow not installed; ParametricUMAP will be unavailable",
|
|
11
|
+
category=ImportWarning,
|
|
12
|
+
)
|
|
13
|
+
# Add a dummy class to raise an error
|
|
14
|
+
class ParametricUMAP(object):
|
|
15
|
+
def __init__(self, **kwds):
|
|
16
|
+
warn(
|
|
17
|
+
"""The umap.parametric_umap package requires Tensorflow > 2.0 to be installed.
|
|
18
|
+
You can install Tensorflow at https://www.tensorflow.org/install
|
|
19
|
+
|
|
20
|
+
or you can install the CPU version of Tensorflow using
|
|
21
|
+
|
|
22
|
+
pip install umap-learn[parametric_umap]
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
)
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"umap.parametric_umap requires Tensorflow >= 2.0"
|
|
28
|
+
) from None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
from .aligned_umap import AlignedUMAP
|
|
32
|
+
|
|
33
|
+
# Workaround: https://github.com/numba/numba/issues/3341
|
|
34
|
+
import numba
|
|
35
|
+
|
|
36
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
__version__ = version("umap-learn")
|
|
40
|
+
except PackageNotFoundError:
|
|
41
|
+
__version__ = "0.5-dev"
|
umap/aligned_umap.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numba
|
|
3
|
+
from sklearn.base import BaseEstimator
|
|
4
|
+
from sklearn.utils import check_array
|
|
5
|
+
|
|
6
|
+
from umap.sparse import arr_intersect as intersect1d
|
|
7
|
+
from umap.sparse import arr_union as union1d
|
|
8
|
+
from umap.umap_ import UMAP, make_epochs_per_sample
|
|
9
|
+
from umap.spectral import spectral_layout
|
|
10
|
+
from umap.layouts import optimize_layout_aligned_euclidean
|
|
11
|
+
|
|
12
|
+
INT32_MIN = np.iinfo(np.int32).min + 1
|
|
13
|
+
INT32_MAX = np.iinfo(np.int32).max - 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@numba.njit(parallel=True)
|
|
17
|
+
def in1d(arr, test_set):
|
|
18
|
+
test_set = set(test_set)
|
|
19
|
+
result = np.empty(arr.shape[0], dtype=np.bool_)
|
|
20
|
+
for i in numba.prange(arr.shape[0]):
|
|
21
|
+
if arr[i] in test_set:
|
|
22
|
+
result[i] = True
|
|
23
|
+
else:
|
|
24
|
+
result[i] = False
|
|
25
|
+
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def invert_dict(d):
|
|
30
|
+
return {value: key for key, value in d.items()}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@numba.njit()
|
|
34
|
+
def procrustes_align(embedding_base, embedding_to_align, anchors):
|
|
35
|
+
subset1 = embedding_base[anchors[0]]
|
|
36
|
+
subset2 = embedding_to_align[anchors[1]]
|
|
37
|
+
M = subset2.T @ subset1
|
|
38
|
+
U, S, V = np.linalg.svd(M)
|
|
39
|
+
R = U @ V
|
|
40
|
+
return embedding_to_align @ R
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def expand_relations(relation_dicts, window_size=3):
|
|
44
|
+
max_n_samples = (
|
|
45
|
+
max(
|
|
46
|
+
[max(d.keys()) for d in relation_dicts]
|
|
47
|
+
+ [max(d.values()) for d in relation_dicts]
|
|
48
|
+
)
|
|
49
|
+
+ 1
|
|
50
|
+
)
|
|
51
|
+
result = np.full(
|
|
52
|
+
(len(relation_dicts) + 1, 2 * window_size + 1, max_n_samples),
|
|
53
|
+
-1,
|
|
54
|
+
dtype=np.int32,
|
|
55
|
+
)
|
|
56
|
+
reverse_relation_dicts = [invert_dict(d) for d in relation_dicts]
|
|
57
|
+
for i in range(result.shape[0]):
|
|
58
|
+
for j in range(window_size):
|
|
59
|
+
result_index = (window_size) + (j + 1)
|
|
60
|
+
if i + j + 1 >= len(relation_dicts):
|
|
61
|
+
result[i, result_index] = np.full(max_n_samples, -1, dtype=np.int32)
|
|
62
|
+
else:
|
|
63
|
+
mapping = np.arange(max_n_samples)
|
|
64
|
+
for k in range(j + 1):
|
|
65
|
+
mapping = np.array(
|
|
66
|
+
[relation_dicts[i + k].get(n, -1) for n in mapping]
|
|
67
|
+
)
|
|
68
|
+
result[i, result_index] = mapping
|
|
69
|
+
|
|
70
|
+
for j in range(0, -window_size, -1):
|
|
71
|
+
result_index = (window_size) + (j - 1)
|
|
72
|
+
if i + j - 1 < 0:
|
|
73
|
+
result[i, result_index] = np.full(max_n_samples, -1, dtype=np.int32)
|
|
74
|
+
else:
|
|
75
|
+
mapping = np.arange(max_n_samples)
|
|
76
|
+
for k in range(0, j - 1, -1):
|
|
77
|
+
mapping = np.array(
|
|
78
|
+
[reverse_relation_dicts[i + k - 1].get(n, -1) for n in mapping]
|
|
79
|
+
)
|
|
80
|
+
result[i, result_index] = mapping
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@numba.njit()
|
|
86
|
+
def build_neighborhood_similarities(graphs_indptr, graphs_indices, relations):
|
|
87
|
+
result = np.zeros(relations.shape, dtype=np.float32)
|
|
88
|
+
center_index = (relations.shape[1] - 1) // 2
|
|
89
|
+
for i in range(relations.shape[0]):
|
|
90
|
+
base_graph_indptr = graphs_indptr[i]
|
|
91
|
+
base_graph_indices = graphs_indices[i]
|
|
92
|
+
for j in range(relations.shape[1]):
|
|
93
|
+
if i + j - center_index < 0 or i + j - center_index >= len(graphs_indptr):
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
comparison_graph_indptr = graphs_indptr[i + j - center_index]
|
|
97
|
+
comparison_graph_indices = graphs_indices[i + j - center_index]
|
|
98
|
+
for k in range(relations.shape[2]):
|
|
99
|
+
comparison_index = relations[i, j, k]
|
|
100
|
+
if comparison_index < 0:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
raw_base_graph_indices = base_graph_indices[
|
|
104
|
+
base_graph_indptr[k] : base_graph_indptr[k + 1]
|
|
105
|
+
].copy()
|
|
106
|
+
base_indices = relations[i, j][raw_base_graph_indices[
|
|
107
|
+
raw_base_graph_indices < relations.shape[2]]]
|
|
108
|
+
base_indices = base_indices[base_indices >= 0]
|
|
109
|
+
comparison_indices = comparison_graph_indices[
|
|
110
|
+
comparison_graph_indptr[comparison_index] : comparison_graph_indptr[
|
|
111
|
+
comparison_index + 1
|
|
112
|
+
]
|
|
113
|
+
]
|
|
114
|
+
comparison_indices = comparison_indices[
|
|
115
|
+
in1d(comparison_indices, relations[i, j])
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
intersection_size = intersect1d(base_indices, comparison_indices).shape[
|
|
119
|
+
0
|
|
120
|
+
]
|
|
121
|
+
union_size = union1d(base_indices, comparison_indices).shape[0]
|
|
122
|
+
|
|
123
|
+
if union_size > 0:
|
|
124
|
+
result[i, j, k] = intersection_size / union_size
|
|
125
|
+
else:
|
|
126
|
+
result[i, j, k] = 1.0
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_nth_item_or_val(iterable_or_val, n):
|
|
132
|
+
if iterable_or_val is None:
|
|
133
|
+
return None
|
|
134
|
+
if type(iterable_or_val) in (list, tuple, np.ndarray):
|
|
135
|
+
return iterable_or_val[n]
|
|
136
|
+
elif type(iterable_or_val) in (int, float, bool, None):
|
|
137
|
+
return iterable_or_val
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError("Unrecognized parameter type")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
PARAM_NAMES = (
|
|
143
|
+
"n_neighbors",
|
|
144
|
+
"n_components",
|
|
145
|
+
"metric",
|
|
146
|
+
"metric_kwds",
|
|
147
|
+
"n_epochs",
|
|
148
|
+
"learning_rate",
|
|
149
|
+
"init",
|
|
150
|
+
"min_dist",
|
|
151
|
+
"spread",
|
|
152
|
+
"set_op_mix_ratio",
|
|
153
|
+
"local_connectivity",
|
|
154
|
+
"repulsion_strength",
|
|
155
|
+
"negative_sample_rate",
|
|
156
|
+
"transform_queue_size",
|
|
157
|
+
"angular_rp_forest",
|
|
158
|
+
"target_n_neighbors",
|
|
159
|
+
"target_metric",
|
|
160
|
+
"target_metric_kwds",
|
|
161
|
+
"target_weight",
|
|
162
|
+
"unique",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def set_aligned_params(new_params, existing_params, n_models, param_names=PARAM_NAMES):
|
|
167
|
+
for param in param_names:
|
|
168
|
+
if param in new_params:
|
|
169
|
+
if isinstance(existing_params[param], list):
|
|
170
|
+
existing_params[param].append(new_params[param])
|
|
171
|
+
elif isinstance(existing_params[param], tuple):
|
|
172
|
+
existing_params[param] = existing_params[param] + \
|
|
173
|
+
(new_params[param],)
|
|
174
|
+
elif isinstance(existing_params[param], np.ndarray):
|
|
175
|
+
existing_params[param] = np.append(existing_params[param],
|
|
176
|
+
new_params[param])
|
|
177
|
+
else:
|
|
178
|
+
if new_params[param] != existing_params[param]:
|
|
179
|
+
existing_params[param] = (existing_params[param],) * n_models + (
|
|
180
|
+
new_params[param],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return existing_params
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@numba.njit()
|
|
187
|
+
def init_from_existing_internal(
|
|
188
|
+
previous_embedding, weights_indptr, weights_indices, weights_data, relation_dict
|
|
189
|
+
):
|
|
190
|
+
n_samples = weights_indptr.shape[0] - 1
|
|
191
|
+
n_features = previous_embedding.shape[1]
|
|
192
|
+
result = np.zeros((n_samples, n_features), dtype=np.float32)
|
|
193
|
+
|
|
194
|
+
for i in range(n_samples):
|
|
195
|
+
if i in relation_dict:
|
|
196
|
+
result[i] = previous_embedding[relation_dict[i]]
|
|
197
|
+
else:
|
|
198
|
+
normalisation = 0.0
|
|
199
|
+
for idx in range(weights_indptr[i], weights_indptr[i + 1]):
|
|
200
|
+
j = weights_indices[idx]
|
|
201
|
+
if j in relation_dict:
|
|
202
|
+
normalisation += weights_data[idx]
|
|
203
|
+
result[i] += (
|
|
204
|
+
weights_data[idx] * previous_embedding[relation_dict[j]]
|
|
205
|
+
)
|
|
206
|
+
if normalisation == 0:
|
|
207
|
+
result[i] = np.random.uniform(-10.0, 10.0, n_features)
|
|
208
|
+
else:
|
|
209
|
+
result[i] /= normalisation
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def init_from_existing(previous_embedding, graph, relations):
|
|
215
|
+
typed_relations = numba.typed.Dict.empty(numba.types.int32, numba.types.int32)
|
|
216
|
+
for key, val in relations.items():
|
|
217
|
+
typed_relations[np.int32(key)] = np.int32(val)
|
|
218
|
+
return init_from_existing_internal(
|
|
219
|
+
previous_embedding,
|
|
220
|
+
graph.indptr,
|
|
221
|
+
graph.indices,
|
|
222
|
+
graph.data,
|
|
223
|
+
typed_relations,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class AlignedUMAP(BaseEstimator):
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
n_neighbors=15,
|
|
231
|
+
n_components=2,
|
|
232
|
+
metric="euclidean",
|
|
233
|
+
metric_kwds=None,
|
|
234
|
+
n_epochs=None,
|
|
235
|
+
learning_rate=1.0,
|
|
236
|
+
init="spectral",
|
|
237
|
+
alignment_regularisation=1.0e-2,
|
|
238
|
+
alignment_window_size=3,
|
|
239
|
+
min_dist=0.1,
|
|
240
|
+
spread=1.0,
|
|
241
|
+
low_memory=False,
|
|
242
|
+
set_op_mix_ratio=1.0,
|
|
243
|
+
local_connectivity=1.0,
|
|
244
|
+
repulsion_strength=1.0,
|
|
245
|
+
negative_sample_rate=5,
|
|
246
|
+
transform_queue_size=4.0,
|
|
247
|
+
a=None,
|
|
248
|
+
b=None,
|
|
249
|
+
random_state=None,
|
|
250
|
+
angular_rp_forest=False,
|
|
251
|
+
target_n_neighbors=-1,
|
|
252
|
+
target_metric="categorical",
|
|
253
|
+
target_metric_kwds=None,
|
|
254
|
+
target_weight=0.5,
|
|
255
|
+
transform_seed=42,
|
|
256
|
+
force_approximation_algorithm=False,
|
|
257
|
+
verbose=False,
|
|
258
|
+
unique=False,
|
|
259
|
+
):
|
|
260
|
+
|
|
261
|
+
self.n_neighbors = n_neighbors
|
|
262
|
+
self.metric = metric
|
|
263
|
+
self.metric_kwds = metric_kwds
|
|
264
|
+
|
|
265
|
+
self.n_epochs = n_epochs
|
|
266
|
+
self.init = init
|
|
267
|
+
self.n_components = n_components
|
|
268
|
+
self.repulsion_strength = repulsion_strength
|
|
269
|
+
self.learning_rate = learning_rate
|
|
270
|
+
self.alignment_regularisation = alignment_regularisation
|
|
271
|
+
self.alignment_window_size = alignment_window_size
|
|
272
|
+
|
|
273
|
+
self.spread = spread
|
|
274
|
+
self.min_dist = min_dist
|
|
275
|
+
self.low_memory = low_memory
|
|
276
|
+
self.set_op_mix_ratio = set_op_mix_ratio
|
|
277
|
+
self.local_connectivity = local_connectivity
|
|
278
|
+
self.negative_sample_rate = negative_sample_rate
|
|
279
|
+
self.random_state = random_state
|
|
280
|
+
self.angular_rp_forest = angular_rp_forest
|
|
281
|
+
self.transform_queue_size = transform_queue_size
|
|
282
|
+
self.target_n_neighbors = target_n_neighbors
|
|
283
|
+
self.target_metric = target_metric
|
|
284
|
+
self.target_metric_kwds = target_metric_kwds
|
|
285
|
+
self.target_weight = target_weight
|
|
286
|
+
self.transform_seed = transform_seed
|
|
287
|
+
self.force_approximation_algorithm = force_approximation_algorithm
|
|
288
|
+
self.verbose = verbose
|
|
289
|
+
self.unique = unique
|
|
290
|
+
|
|
291
|
+
self.a = a
|
|
292
|
+
self.b = b
|
|
293
|
+
|
|
294
|
+
def fit(self, X, y=None, **fit_params):
|
|
295
|
+
if "relations" not in fit_params:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
"Aligned UMAP requires relations between data to be " "specified"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self.dict_relations_ = fit_params["relations"]
|
|
301
|
+
assert type(self.dict_relations_) in (list, tuple)
|
|
302
|
+
assert type(X) in (list, tuple, np.ndarray)
|
|
303
|
+
assert (len(X) - 1) == (len(self.dict_relations_))
|
|
304
|
+
|
|
305
|
+
if y is not None:
|
|
306
|
+
assert type(y) in (list, tuple, np.ndarray)
|
|
307
|
+
assert (len(y) - 1) == (len(self.dict_relations_))
|
|
308
|
+
else:
|
|
309
|
+
y = [None] * len(X)
|
|
310
|
+
|
|
311
|
+
# We need n_components to be constant or this won't work
|
|
312
|
+
if type(self.n_components) in (list, tuple, np.ndarray):
|
|
313
|
+
raise ValueError("n_components must be a single integer, and cannot vary")
|
|
314
|
+
|
|
315
|
+
self.n_models_ = len(X)
|
|
316
|
+
|
|
317
|
+
if self.n_epochs is None:
|
|
318
|
+
self.n_epochs = 200
|
|
319
|
+
|
|
320
|
+
n_epochs = self.n_epochs
|
|
321
|
+
|
|
322
|
+
self.mappers_ = [
|
|
323
|
+
UMAP(
|
|
324
|
+
n_neighbors=get_nth_item_or_val(self.n_neighbors, n),
|
|
325
|
+
min_dist=get_nth_item_or_val(self.min_dist, n),
|
|
326
|
+
n_epochs=get_nth_item_or_val(self.n_epochs, n),
|
|
327
|
+
repulsion_strength=get_nth_item_or_val(self.repulsion_strength, n),
|
|
328
|
+
learning_rate=get_nth_item_or_val(self.learning_rate, n),
|
|
329
|
+
init=self.init,
|
|
330
|
+
spread=get_nth_item_or_val(self.spread, n),
|
|
331
|
+
negative_sample_rate=get_nth_item_or_val(self.negative_sample_rate, n),
|
|
332
|
+
local_connectivity=get_nth_item_or_val(self.local_connectivity, n),
|
|
333
|
+
set_op_mix_ratio=get_nth_item_or_val(self.set_op_mix_ratio, n),
|
|
334
|
+
unique=get_nth_item_or_val(self.unique, n),
|
|
335
|
+
n_components=self.n_components,
|
|
336
|
+
metric=self.metric,
|
|
337
|
+
metric_kwds=self.metric_kwds,
|
|
338
|
+
low_memory=self.low_memory,
|
|
339
|
+
random_state=self.random_state,
|
|
340
|
+
angular_rp_forest=self.angular_rp_forest,
|
|
341
|
+
transform_queue_size=self.transform_queue_size,
|
|
342
|
+
target_n_neighbors=self.target_n_neighbors,
|
|
343
|
+
target_metric=self.target_metric,
|
|
344
|
+
target_metric_kwds=self.target_metric_kwds,
|
|
345
|
+
target_weight=self.target_weight,
|
|
346
|
+
transform_seed=self.transform_seed,
|
|
347
|
+
force_approximation_algorithm=self.force_approximation_algorithm,
|
|
348
|
+
verbose=self.verbose,
|
|
349
|
+
a=self.a,
|
|
350
|
+
b=self.b,
|
|
351
|
+
).fit(X[n], y[n])
|
|
352
|
+
for n in range(self.n_models_)
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
window_size = fit_params.get("window_size", self.alignment_window_size)
|
|
356
|
+
relations = expand_relations(self.dict_relations_, window_size)
|
|
357
|
+
|
|
358
|
+
indptr_list = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
359
|
+
indices_list = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
360
|
+
heads = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
361
|
+
tails = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
362
|
+
epochs_per_samples = numba.typed.List.empty_list(numba.types.float64[::1])
|
|
363
|
+
|
|
364
|
+
for mapper in self.mappers_:
|
|
365
|
+
indptr_list.append(mapper.graph_.indptr)
|
|
366
|
+
indices_list.append(mapper.graph_.indices)
|
|
367
|
+
heads.append(mapper.graph_.tocoo().row)
|
|
368
|
+
tails.append(mapper.graph_.tocoo().col)
|
|
369
|
+
epochs_per_samples.append(
|
|
370
|
+
make_epochs_per_sample(mapper.graph_.tocoo().data, n_epochs)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
rng_state_transform = np.random.RandomState(self.transform_seed)
|
|
374
|
+
regularisation_weights = build_neighborhood_similarities(
|
|
375
|
+
indptr_list,
|
|
376
|
+
indices_list,
|
|
377
|
+
relations,
|
|
378
|
+
)
|
|
379
|
+
first_init = spectral_layout(
|
|
380
|
+
self.mappers_[0]._raw_data,
|
|
381
|
+
self.mappers_[0].graph_,
|
|
382
|
+
self.n_components,
|
|
383
|
+
rng_state_transform,
|
|
384
|
+
)
|
|
385
|
+
expansion = 10.0 / np.abs(first_init).max()
|
|
386
|
+
first_embedding = (first_init * expansion).astype(
|
|
387
|
+
np.float32,
|
|
388
|
+
order="C",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
embeddings = numba.typed.List.empty_list(numba.types.float32[:, ::1])
|
|
392
|
+
embeddings.append(first_embedding)
|
|
393
|
+
for i in range(1, self.n_models_):
|
|
394
|
+
next_init = spectral_layout(
|
|
395
|
+
self.mappers_[i]._raw_data,
|
|
396
|
+
self.mappers_[i].graph_,
|
|
397
|
+
self.n_components,
|
|
398
|
+
rng_state_transform,
|
|
399
|
+
)
|
|
400
|
+
expansion = 10.0 / np.abs(next_init).max()
|
|
401
|
+
next_embedding = (next_init * expansion).astype(
|
|
402
|
+
np.float32,
|
|
403
|
+
order="C",
|
|
404
|
+
)
|
|
405
|
+
anchor_data = relations[i][window_size - 1]
|
|
406
|
+
left_anchors = anchor_data[anchor_data >= 0]
|
|
407
|
+
right_anchors = np.where(anchor_data >= 0)[0]
|
|
408
|
+
embeddings.append(
|
|
409
|
+
procrustes_align(
|
|
410
|
+
embeddings[-1],
|
|
411
|
+
next_embedding,
|
|
412
|
+
np.vstack([left_anchors, right_anchors]),
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
seed_triplet = rng_state_transform.randint(INT32_MIN, INT32_MAX, 3).astype(
|
|
417
|
+
np.int64
|
|
418
|
+
)
|
|
419
|
+
self.embeddings_ = optimize_layout_aligned_euclidean(
|
|
420
|
+
embeddings,
|
|
421
|
+
embeddings,
|
|
422
|
+
heads,
|
|
423
|
+
tails,
|
|
424
|
+
n_epochs,
|
|
425
|
+
epochs_per_samples,
|
|
426
|
+
regularisation_weights,
|
|
427
|
+
relations,
|
|
428
|
+
seed_triplet,
|
|
429
|
+
lambda_=self.alignment_regularisation,
|
|
430
|
+
move_other=True,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
for i, embedding in enumerate(self.embeddings_):
|
|
434
|
+
disconnected_vertices = (
|
|
435
|
+
np.array(self.mappers_[i].graph_.sum(axis=1)).flatten() == 0
|
|
436
|
+
)
|
|
437
|
+
embedding[disconnected_vertices] = np.full(self.n_components, np.nan)
|
|
438
|
+
|
|
439
|
+
return self
|
|
440
|
+
|
|
441
|
+
def fit_transform(self, X, y=None, **fit_params):
|
|
442
|
+
self.fit(X, y, **fit_params)
|
|
443
|
+
return self.embeddings_
|
|
444
|
+
|
|
445
|
+
def update(self, X, y=None, **fit_params):
|
|
446
|
+
if "relations" not in fit_params:
|
|
447
|
+
raise ValueError(
|
|
448
|
+
"Aligned UMAP requires relations between data to be " "specified"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
new_dict_relations = fit_params["relations"]
|
|
452
|
+
assert isinstance(new_dict_relations, dict)
|
|
453
|
+
|
|
454
|
+
X = check_array(X)
|
|
455
|
+
|
|
456
|
+
self.__dict__ = set_aligned_params(fit_params, self.__dict__, self.n_models_)
|
|
457
|
+
|
|
458
|
+
# We need n_components to be constant or this won't work
|
|
459
|
+
if type(self.n_components) in (list, tuple, np.ndarray):
|
|
460
|
+
raise ValueError("n_components must be a single integer, and cannot vary")
|
|
461
|
+
|
|
462
|
+
if self.n_epochs is None:
|
|
463
|
+
self.n_epochs = 200
|
|
464
|
+
|
|
465
|
+
n_epochs = self.n_epochs
|
|
466
|
+
|
|
467
|
+
new_mapper = UMAP(
|
|
468
|
+
n_neighbors=get_nth_item_or_val(self.n_neighbors, self.n_models_),
|
|
469
|
+
min_dist=get_nth_item_or_val(self.min_dist, self.n_models_),
|
|
470
|
+
n_epochs=get_nth_item_or_val(self.n_epochs, self.n_models_),
|
|
471
|
+
repulsion_strength=get_nth_item_or_val(
|
|
472
|
+
self.repulsion_strength, self.n_models_
|
|
473
|
+
),
|
|
474
|
+
learning_rate=get_nth_item_or_val(self.learning_rate, self.n_models_),
|
|
475
|
+
init=self.init,
|
|
476
|
+
spread=get_nth_item_or_val(self.spread, self.n_models_),
|
|
477
|
+
negative_sample_rate=get_nth_item_or_val(
|
|
478
|
+
self.negative_sample_rate, self.n_models_
|
|
479
|
+
),
|
|
480
|
+
local_connectivity=get_nth_item_or_val(
|
|
481
|
+
self.local_connectivity, self.n_models_
|
|
482
|
+
),
|
|
483
|
+
set_op_mix_ratio=get_nth_item_or_val(self.set_op_mix_ratio, self.n_models_),
|
|
484
|
+
unique=get_nth_item_or_val(self.unique, self.n_models_),
|
|
485
|
+
n_components=self.n_components,
|
|
486
|
+
metric=self.metric,
|
|
487
|
+
metric_kwds=self.metric_kwds,
|
|
488
|
+
low_memory=self.low_memory,
|
|
489
|
+
random_state=self.random_state,
|
|
490
|
+
angular_rp_forest=self.angular_rp_forest,
|
|
491
|
+
transform_queue_size=self.transform_queue_size,
|
|
492
|
+
target_n_neighbors=self.target_n_neighbors,
|
|
493
|
+
target_metric=self.target_metric,
|
|
494
|
+
target_metric_kwds=self.target_metric_kwds,
|
|
495
|
+
target_weight=self.target_weight,
|
|
496
|
+
transform_seed=self.transform_seed,
|
|
497
|
+
force_approximation_algorithm=self.force_approximation_algorithm,
|
|
498
|
+
verbose=self.verbose,
|
|
499
|
+
a=self.a,
|
|
500
|
+
b=self.b,
|
|
501
|
+
).fit(X, y)
|
|
502
|
+
|
|
503
|
+
self.n_models_ += 1
|
|
504
|
+
self.mappers_ += [new_mapper]
|
|
505
|
+
|
|
506
|
+
self.dict_relations_ += [new_dict_relations]
|
|
507
|
+
|
|
508
|
+
window_size = fit_params.get("window_size", self.alignment_window_size)
|
|
509
|
+
new_relations = expand_relations(self.dict_relations_, window_size)
|
|
510
|
+
|
|
511
|
+
indptr_list = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
512
|
+
indices_list = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
513
|
+
heads = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
514
|
+
tails = numba.typed.List.empty_list(numba.types.int32[::1])
|
|
515
|
+
epochs_per_samples = numba.typed.List.empty_list(numba.types.float64[::1])
|
|
516
|
+
|
|
517
|
+
for i, mapper in enumerate(self.mappers_):
|
|
518
|
+
indptr_list.append(mapper.graph_.indptr)
|
|
519
|
+
indices_list.append(mapper.graph_.indices)
|
|
520
|
+
heads.append(mapper.graph_.tocoo().row)
|
|
521
|
+
tails.append(mapper.graph_.tocoo().col)
|
|
522
|
+
if i == len(self.mappers_) - 1:
|
|
523
|
+
epochs_per_samples.append(
|
|
524
|
+
make_epochs_per_sample(mapper.graph_.tocoo().data, n_epochs)
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
epochs_per_samples.append(
|
|
528
|
+
np.full(mapper.embedding_.shape[0], n_epochs + 1, dtype=np.float64)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
new_regularisation_weights = build_neighborhood_similarities(
|
|
532
|
+
indptr_list,
|
|
533
|
+
indices_list,
|
|
534
|
+
new_relations,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# TODO: We can likely make this more efficient and not recompute each time
|
|
538
|
+
inv_dict_relations = invert_dict(new_dict_relations)
|
|
539
|
+
|
|
540
|
+
new_embedding = init_from_existing(
|
|
541
|
+
self.embeddings_[-1], new_mapper.graph_, inv_dict_relations
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
self.embeddings_.append(new_embedding)
|
|
545
|
+
|
|
546
|
+
rng_state_transform = np.random.RandomState(self.transform_seed)
|
|
547
|
+
seed_triplet = rng_state_transform.randint(INT32_MIN, INT32_MAX, 3).astype(
|
|
548
|
+
np.int64
|
|
549
|
+
)
|
|
550
|
+
self.embeddings_ = optimize_layout_aligned_euclidean(
|
|
551
|
+
self.embeddings_,
|
|
552
|
+
self.embeddings_,
|
|
553
|
+
heads,
|
|
554
|
+
tails,
|
|
555
|
+
n_epochs,
|
|
556
|
+
epochs_per_samples,
|
|
557
|
+
new_regularisation_weights,
|
|
558
|
+
new_relations,
|
|
559
|
+
seed_triplet,
|
|
560
|
+
lambda_=self.alignment_regularisation,
|
|
561
|
+
)
|