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 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
+ )