dspin 1.5.0__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.
- dspin/__init__.py +21 -0
- dspin/annotate.py +461 -0
- dspin/compute.py +1690 -0
- dspin/dspin.py +869 -0
- dspin/gpt.py +65 -0
- dspin/metadata_drug.py +13 -0
- dspin/plot.py +776 -0
- dspin-1.5.0.dist-info/METADATA +49 -0
- dspin-1.5.0.dist-info/RECORD +12 -0
- dspin-1.5.0.dist-info/WHEEL +5 -0
- dspin-1.5.0.dist-info/licenses/LICENSE +201 -0
- dspin-1.5.0.dist-info/top_level.txt +1 -0
dspin/compute.py
ADDED
|
@@ -0,0 +1,1690 @@
|
|
|
1
|
+
# -*-coding:utf-8 -*-
|
|
2
|
+
'''
|
|
3
|
+
@Time : 2023/09/14 21:51
|
|
4
|
+
@Author : Jialong Jiang, Yingying Gong
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
from scipy.linalg import orth
|
|
8
|
+
from collections import deque
|
|
9
|
+
import numba
|
|
10
|
+
from sklearn.preprocessing import normalize
|
|
11
|
+
from sklearn.decomposition import NMF, PCA
|
|
12
|
+
import numpy as np
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import scanpy as sc
|
|
15
|
+
import anndata as ad
|
|
16
|
+
import os
|
|
17
|
+
from sklearn.cluster import KMeans
|
|
18
|
+
import scipy.io as sio
|
|
19
|
+
from tqdm import tqdm
|
|
20
|
+
from typing import Tuple, List, Callable, Any, Dict
|
|
21
|
+
import warnings
|
|
22
|
+
from scipy.sparse import issparse
|
|
23
|
+
from joblib import Parallel, delayed, parallel_backend
|
|
24
|
+
from sklearn.cluster import AgglomerativeClustering
|
|
25
|
+
import leidenalg as la
|
|
26
|
+
import igraph as ig
|
|
27
|
+
import networkx as nx
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def category_balance_number(
|
|
31
|
+
total_sample_size: int,
|
|
32
|
+
cluster_count: List[int],
|
|
33
|
+
method: str,
|
|
34
|
+
maximum_sample_rate: float) -> np.array:
|
|
35
|
+
"""
|
|
36
|
+
Calculate the sampling number for each category based on the specified method.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
total_sample_size : int
|
|
41
|
+
The total number of samples.
|
|
42
|
+
cluster_count : List[int]
|
|
43
|
+
A list containing the count of elements in each cluster.
|
|
44
|
+
method : str
|
|
45
|
+
The method used for balancing the categories. Options are 'equal', 'proportional', or 'squareroot'.
|
|
46
|
+
maximum_sample_rate : float
|
|
47
|
+
The maximum sample rate allowed.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
np.array
|
|
52
|
+
An array containing the number of samples for each category computed using the given method.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Validate the method parameter
|
|
56
|
+
if method not in ['equal', 'proportional', 'squareroot']:
|
|
57
|
+
raise ValueError('method must be one of equal, proportional, squareroot')
|
|
58
|
+
|
|
59
|
+
# Calculate the sampling number based on the selected method.
|
|
60
|
+
if method == 'squareroot':
|
|
61
|
+
# For the 'squareroot' method, compute values based on the square root of the cluster counts.
|
|
62
|
+
esti_size = (np.sqrt(cluster_count) / np.sum(np.sqrt(cluster_count)) * total_sample_size).astype(int)
|
|
63
|
+
weight_fun = np.min([esti_size, maximum_sample_rate * np.array(cluster_count)], axis=0)
|
|
64
|
+
elif method == 'equal':
|
|
65
|
+
# For the 'equal' method, divide the total sample size equally among clusters.
|
|
66
|
+
esti_size = total_sample_size / len(cluster_count)
|
|
67
|
+
weight_fun = np.min([esti_size * np.ones(len(cluster_count)), maximum_sample_rate * np.array(cluster_count)], axis=0)
|
|
68
|
+
else:
|
|
69
|
+
# For the 'proportional' method, use the cluster counts directly as weights.
|
|
70
|
+
weight_fun = cluster_count
|
|
71
|
+
|
|
72
|
+
# Compute the final sampling number for each category.
|
|
73
|
+
sampling_number = (weight_fun / np.sum(weight_fun) * total_sample_size).astype(int)
|
|
74
|
+
return sampling_number
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def summary_components(all_components: np.array,
|
|
78
|
+
num_spin: int,
|
|
79
|
+
summary_method: str = 'kmeans',
|
|
80
|
+
figure_folder: str=None) -> List[np.array]:
|
|
81
|
+
"""
|
|
82
|
+
Summarize components using a clustering algorithm.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
all_components : np.array
|
|
87
|
+
A 2D array where each row represents a sample and each column represents a feature.
|
|
88
|
+
num_spin : int
|
|
89
|
+
The number of clusters.
|
|
90
|
+
summary_method : str, optional
|
|
91
|
+
The method used for summarizing the components. Options are 'kmeans' or 'leiden'. Default is 'kmeans'.
|
|
92
|
+
figure_folder : str, optional
|
|
93
|
+
Folder in which to save the figure.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
List[np.array]
|
|
98
|
+
A list of numpy arrays, each containing the indices of the genes that belong to a specific cluster.
|
|
99
|
+
"""
|
|
100
|
+
num_gene = all_components.shape[1]
|
|
101
|
+
|
|
102
|
+
if summary_method == 'kmeans':
|
|
103
|
+
# Fit KMeans clustering to the components and to their transposed version.
|
|
104
|
+
kmeans = KMeans(n_clusters=num_spin, random_state=0, n_init=50).fit(all_components)
|
|
105
|
+
|
|
106
|
+
# take the genes with larger weights for display only
|
|
107
|
+
gene_weight = np.sum(all_components ** 2, axis=0) ** 0.5
|
|
108
|
+
gene_sele_filt = gene_weight > np.mean(gene_weight)
|
|
109
|
+
all_components_sub = all_components[:, gene_sele_filt]
|
|
110
|
+
kmeans_gene = KMeans(n_clusters=min(2 * num_spin, all_components_sub.shape[1]), random_state=0, n_init=10).fit(all_components_sub.T)
|
|
111
|
+
|
|
112
|
+
# Initialize an array to store the average component for each cluster.
|
|
113
|
+
components_kmeans = np.zeros((num_spin, num_gene))
|
|
114
|
+
|
|
115
|
+
for ii in range(num_spin):
|
|
116
|
+
# Calculate the average component for the current cluster.
|
|
117
|
+
components_kmeans[ii] = np.mean(all_components[kmeans.labels_ == ii], axis=0)
|
|
118
|
+
|
|
119
|
+
# Normalize the computed components.
|
|
120
|
+
components_kmeans = normalize(components_kmeans, axis=1, norm='l2')
|
|
121
|
+
|
|
122
|
+
gene_groups_ind = []
|
|
123
|
+
for ii in range(num_spin):
|
|
124
|
+
# Determine gene cluster assignments based on the maximum component values.
|
|
125
|
+
gene_groups_ind.append(np.argmax(components_kmeans, axis=0) == ii)
|
|
126
|
+
|
|
127
|
+
sc.set_figure_params(figsize=(8, 4))
|
|
128
|
+
|
|
129
|
+
plt.subplot(1, 2, 1)
|
|
130
|
+
gene_order = np.argsort(kmeans_gene.labels_)
|
|
131
|
+
comp_order = np.argsort(kmeans.labels_)
|
|
132
|
+
plt.imshow(all_components_sub[comp_order, :][:, gene_order], aspect='auto', cmap='Blues', vmax=np.max(all_components) / 10, interpolation='none')
|
|
133
|
+
plt.title('All components')
|
|
134
|
+
|
|
135
|
+
plt.subplot(1, 2, 2)
|
|
136
|
+
plt.imshow(components_kmeans[:, gene_sele_filt][:, gene_order], aspect='auto', cmap='Blues', vmax=np.max(components_kmeans) / 10, interpolation='none')
|
|
137
|
+
plt.title('Kmeans components')
|
|
138
|
+
|
|
139
|
+
if figure_folder is not None:
|
|
140
|
+
plt.savefig(figure_folder + 'onmf_decomposition_summary.png', dpi=300, bbox_inches='tight')
|
|
141
|
+
plt.close()
|
|
142
|
+
|
|
143
|
+
elif summary_method == 'leiden':
|
|
144
|
+
|
|
145
|
+
consensus = np.einsum('ij,ik->jk', all_components, all_components) / all_components.shape[0]
|
|
146
|
+
consensus_filt = np.where(np.max(consensus, axis=0) > np.percentile(consensus.flatten(), 99))[0]
|
|
147
|
+
consensus_sub = consensus[:, consensus_filt][consensus_filt]
|
|
148
|
+
|
|
149
|
+
# Perform Agglomerative Clustering on consensus matrix
|
|
150
|
+
clusterer = AgglomerativeClustering(n_clusters=num_spin, metric='precomputed', linkage='complete')
|
|
151
|
+
cluster_labels = clusterer.fit_predict(np.sqrt(- np.log(consensus_sub)))
|
|
152
|
+
|
|
153
|
+
# Convert to graph and apply Leiden algorithm
|
|
154
|
+
G = nx.from_numpy_array(consensus)
|
|
155
|
+
G = ig.Graph.from_networkx(G)
|
|
156
|
+
np.random.seed(0)
|
|
157
|
+
|
|
158
|
+
optimiser = la.Optimiser()
|
|
159
|
+
membership = np.ones(consensus.shape[0]) * (1 + max(cluster_labels))
|
|
160
|
+
membership[consensus_filt] = cluster_labels
|
|
161
|
+
membership_fixed = np.zeros(consensus.shape[0])
|
|
162
|
+
membership_fixed[consensus_filt] = 1
|
|
163
|
+
|
|
164
|
+
membership = membership.astype(int)
|
|
165
|
+
membership_fixed = membership_fixed.astype(int)
|
|
166
|
+
|
|
167
|
+
partition = la.RBConfigurationVertexPartition(G, initial_membership=membership, weights='weight')
|
|
168
|
+
diff = optimiser.optimise_partition(partition, n_iterations=- 1, is_membership_fixed=list(membership_fixed))
|
|
169
|
+
|
|
170
|
+
gene_groups_ind = []
|
|
171
|
+
cluster_label_full = np.nan * np.ones(consensus.shape[0])
|
|
172
|
+
|
|
173
|
+
for ii, part in enumerate(partition):
|
|
174
|
+
gene_groups_ind.append(part)
|
|
175
|
+
cluster_label_full[part] = ii
|
|
176
|
+
|
|
177
|
+
gene_order = np.argsort(cluster_label_full)
|
|
178
|
+
gene_list_size = np.array([np.sum(cluster_label_full == ii) for ii in range(num_spin)])
|
|
179
|
+
|
|
180
|
+
plt.figure(figsize=[8, 8])
|
|
181
|
+
plt.imshow(consensus[gene_order][:, gene_order], cmap='Blues', vmin=0, vmax=np.percentile(consensus.flatten(), 98))
|
|
182
|
+
for ii in range(1, num_spin):
|
|
183
|
+
plt.axvline(np.sum(gene_list_size[:ii]) - 0.5, color='k')
|
|
184
|
+
plt.axhline(np.sum(gene_list_size[:ii]) - 0.5, color='k')
|
|
185
|
+
plt.grid()
|
|
186
|
+
|
|
187
|
+
if figure_folder is not None:
|
|
188
|
+
plt.savefig(figure_folder + 'onmf_decomposition_summary.png', dpi=300, bbox_inches='tight')
|
|
189
|
+
plt.close()
|
|
190
|
+
|
|
191
|
+
return gene_groups_ind
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def onmf(X: np.array, rank: int, max_iter: int = 500) -> Tuple[np.array, np.array]:
|
|
195
|
+
"""
|
|
196
|
+
Perform Orthogonal Non-Negative Matrix Factorization (ONMF) for a given rank.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
X : np.array
|
|
201
|
+
Input data matrix.
|
|
202
|
+
rank : int
|
|
203
|
+
Desired rank for factorization.
|
|
204
|
+
max_iter : int, optional
|
|
205
|
+
Maximum number of iterations. Default is 500.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
Tuple[np.array, np.array]
|
|
210
|
+
The factorized matrix S.T.
|
|
211
|
+
The matrix A.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
m, n = X.shape
|
|
215
|
+
|
|
216
|
+
# Initialize matrices A and S
|
|
217
|
+
A = np.random.rand(m, rank)
|
|
218
|
+
S = np.random.rand(rank, n)
|
|
219
|
+
S = np.abs(orth(S.T).T)
|
|
220
|
+
|
|
221
|
+
pbar = tqdm(total=max_iter, desc="Iteration Progress")
|
|
222
|
+
|
|
223
|
+
for itr in range(max_iter):
|
|
224
|
+
# Update A and S using multiplicative update rules
|
|
225
|
+
coef_A = X.dot(S.T) / A.dot(S.dot(S.T))
|
|
226
|
+
A = np.nan_to_num(A * coef_A) #, posinf=1e5)
|
|
227
|
+
|
|
228
|
+
AtX = A.T.dot(X)
|
|
229
|
+
coef_S = AtX / S.dot(AtX.T).dot(S)
|
|
230
|
+
S = np.nan_to_num(S * coef_S) #, posinf=1e5)
|
|
231
|
+
|
|
232
|
+
# pbar.update(1)
|
|
233
|
+
|
|
234
|
+
# Calculate the reconstruction error every 10 iterations and update the
|
|
235
|
+
# progress bar
|
|
236
|
+
if itr % 20 == 0:
|
|
237
|
+
error = np.linalg.norm(X - np.dot(A, S), 'fro')
|
|
238
|
+
self_product = S.dot(S.T)
|
|
239
|
+
orthogonal_error = np.linalg.norm(self_product - np.diag(np.diag(self_product)), 'fro') / np.linalg.norm(S, 'fro')
|
|
240
|
+
pbar.set_postfix({"Reconstruction error": f"{error:.2f}", "Orthogonal error": f"{orthogonal_error:.4f}"})
|
|
241
|
+
pbar.update(20)
|
|
242
|
+
|
|
243
|
+
pbar.close()
|
|
244
|
+
|
|
245
|
+
# Normalize the components
|
|
246
|
+
norm_fac = np.sqrt(np.diag(S.dot(S.T)))
|
|
247
|
+
S /= norm_fac.reshape(-1, 1)
|
|
248
|
+
A *= norm_fac.reshape(1, -1)
|
|
249
|
+
A *= np.sum(X * A.dot(S)) / np.sum((A.dot(S)) ** 2)
|
|
250
|
+
|
|
251
|
+
return S.T, A
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def compute_onmf(seed: int,
|
|
255
|
+
num_spin: int,
|
|
256
|
+
gene_matrix: np.array,
|
|
257
|
+
max_iter: int = 500) -> NMF:
|
|
258
|
+
"""
|
|
259
|
+
Compute the ONMF model for the given gene matrix.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
seed : int
|
|
264
|
+
Seed for random number generation.
|
|
265
|
+
num_spin : int
|
|
266
|
+
The number of desired components (clusters/spins).
|
|
267
|
+
gene_matrix : np.array
|
|
268
|
+
Matrix representing gene expression data. Typically, the matrix should be normalized by library size and log1-transformed. Normalizing each gene by its standard deviation is also recommended.
|
|
269
|
+
max_iter : int, optional
|
|
270
|
+
Maximum number of iterations. Default is 500.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
NMF
|
|
275
|
+
The NMF model with computed components.
|
|
276
|
+
"""
|
|
277
|
+
# Generate a random seed
|
|
278
|
+
np.random.seed(seed)
|
|
279
|
+
|
|
280
|
+
tqdm._instances.clear()
|
|
281
|
+
# Factorized Matrices
|
|
282
|
+
H, W = onmf(gene_matrix, num_spin, max_iter)
|
|
283
|
+
|
|
284
|
+
# Initialize the NMF model
|
|
285
|
+
nmf_model = NMF(n_components=num_spin, random_state=seed)
|
|
286
|
+
|
|
287
|
+
# Set the components and number of components
|
|
288
|
+
nmf_model.components_ = np.array(H).T
|
|
289
|
+
nmf_model.n_components_ = num_spin
|
|
290
|
+
|
|
291
|
+
return nmf_model
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def onmf_discretize(onmf_rep_ori: np.array,
|
|
295
|
+
num_init: int = 10,
|
|
296
|
+
fig_folder: str = None) -> np.array:
|
|
297
|
+
"""
|
|
298
|
+
Discretize the ONMF representation using KMeans clustering and visualize the sorted representations.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
onmf_rep_ori : np.array
|
|
303
|
+
Original ONMF representation.
|
|
304
|
+
fig_folder : str, optional
|
|
305
|
+
Folder in which to save the figure.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
np.array
|
|
310
|
+
Discretized ONMF representation.
|
|
311
|
+
"""
|
|
312
|
+
num_spin = onmf_rep_ori.shape[1]
|
|
313
|
+
sc.set_figure_params(figsize=[2, 2])
|
|
314
|
+
_, grid = sc.pl._tools._panel_grid(
|
|
315
|
+
0.3, 0.3, ncols=7, num_panels=min(
|
|
316
|
+
21, num_spin))
|
|
317
|
+
|
|
318
|
+
onmf_rep_tri = np.zeros(onmf_rep_ori.shape)
|
|
319
|
+
print('Processing gene or program expression levels...')
|
|
320
|
+
for ii in tqdm(range(num_spin)):
|
|
321
|
+
# Perform KMeans clustering for each spin/component
|
|
322
|
+
km_fit = KMeans(n_clusters=3, n_init=num_init).fit(onmf_rep_ori[:, ii].reshape(-1, 1))
|
|
323
|
+
|
|
324
|
+
onmf_rep_tri[:, ii] = (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[0]) * \
|
|
325
|
+
(-1) + (km_fit.labels_ == np.argsort(km_fit.cluster_centers_.reshape(-1))[2]) * 1
|
|
326
|
+
|
|
327
|
+
# Visualize the sorted representations
|
|
328
|
+
if ii < 21:
|
|
329
|
+
ax = plt.subplot(grid[ii])
|
|
330
|
+
plt.plot(np.sort(onmf_rep_ori[:, ii]))
|
|
331
|
+
plt.plot(np.sort(km_fit.cluster_centers_[km_fit.labels_].reshape(-1)))
|
|
332
|
+
|
|
333
|
+
# Save the visual representation
|
|
334
|
+
if fig_folder is not None:
|
|
335
|
+
print('Saving the example state partition figure to ' + fig_folder + 'onmf_discretize.png')
|
|
336
|
+
plt.savefig(f'{fig_folder}/onmf_discretize.png', bbox_inches='tight')
|
|
337
|
+
plt.close()
|
|
338
|
+
|
|
339
|
+
return onmf_rep_tri
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def compute_onmf_decomposition(gene_matrix_norm: np.ndarray,
|
|
343
|
+
num_onmf_components: int,
|
|
344
|
+
seed: int = 0,
|
|
345
|
+
params: dict = None,
|
|
346
|
+
save_path: str = None) -> None:
|
|
347
|
+
"""
|
|
348
|
+
Perform multiple oNMF decompositions on subsampled data to enhance stability.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
gene_matrix_norm : np.ndarray
|
|
353
|
+
Normalized gene expression matrix.
|
|
354
|
+
num_onmf_components : int
|
|
355
|
+
Number of oNMF components.
|
|
356
|
+
seed : int, optional
|
|
357
|
+
Random seed for reproducibility. Default is 0.
|
|
358
|
+
params : dict, optional
|
|
359
|
+
Additional parameters for oNMF. Default is None.
|
|
360
|
+
save_path : str, optional
|
|
361
|
+
Path to save the oNMF decomposition results. Default is None.
|
|
362
|
+
"""
|
|
363
|
+
np.random.seed(seed)
|
|
364
|
+
print(f'Computing oNMF decomposition of seed {seed}')
|
|
365
|
+
|
|
366
|
+
# Check if oNMF decomposition for the current iteration exists
|
|
367
|
+
if os.path.exists(save_path):
|
|
368
|
+
print("oNMF decomposition with seed {} already exists. Skipping...".format(seed))
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
# Compute current oNMF decomposition and save the result
|
|
372
|
+
current_onmf = compute_onmf(
|
|
373
|
+
seed, num_onmf_components, gene_matrix_norm, max_iter=params['onmf_epoch_number'])
|
|
374
|
+
np.save(save_path, current_onmf)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def subsample_normalize_gene_matrix(gene_matrix: np.ndarray,
|
|
378
|
+
cluster_label: np.ndarray,
|
|
379
|
+
num_subsample: int = 10000,
|
|
380
|
+
seed: int = 0,
|
|
381
|
+
params: dict = None) -> Tuple[np.ndarray, np.ndarray]:
|
|
382
|
+
"""
|
|
383
|
+
Subsample and balance the gene matrix based on desired sample size and standard deviation clipping.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
gene_matrix : np.ndarray
|
|
388
|
+
The gene expression matrix.
|
|
389
|
+
cluster_label : np.ndarray
|
|
390
|
+
Labels for clustering.
|
|
391
|
+
num_subsample : int, optional
|
|
392
|
+
Number of samples to subsample. Default is 10000.
|
|
393
|
+
seed : int, optional
|
|
394
|
+
Seed for reproducibility. Default is 0.
|
|
395
|
+
params : dict, optional
|
|
396
|
+
Additional parameters for subsampling and balancing. Default is None.
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
Tuple[np.ndarray, np.ndarray]
|
|
401
|
+
Tuple containing the standard deviation of the subsampled matrix and the normalized gene matrix.
|
|
402
|
+
"""
|
|
403
|
+
np.random.seed(seed)
|
|
404
|
+
|
|
405
|
+
num_cell, num_gene = gene_matrix.shape
|
|
406
|
+
|
|
407
|
+
method = params['balance_method']
|
|
408
|
+
maximum_sample_rate = params['max_sample_rate']
|
|
409
|
+
std_clip_percentile = params['std_clip_percentile']
|
|
410
|
+
min_cluster_size = params['min_cluster_size']
|
|
411
|
+
|
|
412
|
+
# If no balancing is required, simply subsample the matrix
|
|
413
|
+
if method is None:
|
|
414
|
+
gene_matrix_balanced = gene_matrix[np.random.choice(
|
|
415
|
+
num_cell, num_subsample, replace=False), :]
|
|
416
|
+
else:
|
|
417
|
+
cluster_list, cluster_count = np.unique(cluster_label, return_counts=True)
|
|
418
|
+
|
|
419
|
+
# Filter out clusters smaller than the minimum size
|
|
420
|
+
if np.any(cluster_count < min_cluster_size):
|
|
421
|
+
use_cluster = cluster_list[cluster_count >= min_cluster_size]
|
|
422
|
+
filt_ind = np.isin(cluster_label, use_cluster)
|
|
423
|
+
gene_matrix = gene_matrix[filt_ind, :]
|
|
424
|
+
cluster_label = cluster_label[filt_ind]
|
|
425
|
+
cluster_list, cluster_count = np.unique(cluster_label, return_counts=True)
|
|
426
|
+
|
|
427
|
+
warnings.warn("Clusters smaller than {} cells are filtered out.".format(min_cluster_size))
|
|
428
|
+
|
|
429
|
+
sampling_number = category_balance_number(num_subsample, cluster_count, method, maximum_sample_rate)
|
|
430
|
+
|
|
431
|
+
gene_matrix_balanced = np.zeros(
|
|
432
|
+
(np.sum(sampling_number), num_gene))
|
|
433
|
+
|
|
434
|
+
for ii in range(len(cluster_list)):
|
|
435
|
+
cur_num = sampling_number[ii]
|
|
436
|
+
cur_filt = cluster_label == cluster_list[ii]
|
|
437
|
+
sele_ind = np.random.choice(np.sum(cur_filt), cur_num)
|
|
438
|
+
strart_ind = np.sum(sampling_number[:ii])
|
|
439
|
+
end_ind = strart_ind + cur_num
|
|
440
|
+
|
|
441
|
+
if issparse(gene_matrix):
|
|
442
|
+
gene_matrix_balanced[strart_ind: end_ind, :] = gene_matrix[cur_filt, :][sele_ind, :].toarray()
|
|
443
|
+
else:
|
|
444
|
+
gene_matrix_balanced[strart_ind: end_ind, :] = gene_matrix[cur_filt, :][sele_ind, :]
|
|
445
|
+
|
|
446
|
+
std = gene_matrix_balanced.std(axis=0)
|
|
447
|
+
std_clipped = std.clip(np.percentile(std, std_clip_percentile), np.inf)
|
|
448
|
+
gene_matrix_balanced_normalized = gene_matrix_balanced / std_clipped
|
|
449
|
+
|
|
450
|
+
return std_clipped, gene_matrix_balanced_normalized
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def corr_mean(cur_data: np.array) -> np.array:
|
|
454
|
+
"""
|
|
455
|
+
Calculate the correlation matrix and mean of the given data.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
cur_data : np.array
|
|
460
|
+
Input data matrix.
|
|
461
|
+
|
|
462
|
+
Returns
|
|
463
|
+
-------
|
|
464
|
+
np.array
|
|
465
|
+
Array containing the correlation matrix and the mean of the data.
|
|
466
|
+
"""
|
|
467
|
+
rec_data = np.zeros(2, dtype=object)
|
|
468
|
+
rec_data[0] = cur_data.T.dot(cur_data) / cur_data.shape[0]
|
|
469
|
+
rec_data[1] = np.mean(cur_data, axis=0).reshape(-1, 1)
|
|
470
|
+
|
|
471
|
+
return rec_data
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def sample_corr_mean(samp_full: np.array, comp_bin: np.array) -> Tuple[np.array, np.array]:
|
|
475
|
+
"""
|
|
476
|
+
Calculate the correlation mean for each unique sample in samp_full.
|
|
477
|
+
|
|
478
|
+
Parameters
|
|
479
|
+
----------
|
|
480
|
+
samp_full : np.array
|
|
481
|
+
Array of samples.
|
|
482
|
+
comp_bin : np.array
|
|
483
|
+
Binary matrix representation of the samples.
|
|
484
|
+
|
|
485
|
+
Returns
|
|
486
|
+
-------
|
|
487
|
+
Tuple[np.array, np.array]
|
|
488
|
+
An array of correlation means for each unique sample.
|
|
489
|
+
An array of unique sample labels.
|
|
490
|
+
"""
|
|
491
|
+
samp_list = np.unique(samp_full)
|
|
492
|
+
raw_corr_data = np.zeros(len(samp_list), dtype=object)
|
|
493
|
+
|
|
494
|
+
# Calculating correlation mean for each unique sample
|
|
495
|
+
for ind, samp in enumerate(samp_list):
|
|
496
|
+
filt_ind = samp_full == samp
|
|
497
|
+
raw_corr_data[ind] = corr_mean(comp_bin[filt_ind, :])
|
|
498
|
+
|
|
499
|
+
return raw_corr_data, samp_list
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def sample_states(samp_full: np.array, onmf_rep_tri: np.array) -> Tuple[np.array, np.array]:
|
|
503
|
+
"""
|
|
504
|
+
Extract the state representations for each unique sample.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
samp_full : np.array
|
|
509
|
+
Array of sample labels.
|
|
510
|
+
onmf_rep_tri : np.array
|
|
511
|
+
Discretized ONMF representation.
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
Tuple[np.array, np.array]
|
|
516
|
+
An array of state representations (transposed) for each unique sample.
|
|
517
|
+
An array of unique sample labels.
|
|
518
|
+
"""
|
|
519
|
+
samp_list = np.unique(samp_full)
|
|
520
|
+
state_list = np.zeros(len(samp_list), dtype=object)
|
|
521
|
+
|
|
522
|
+
for ii, cur_samp in enumerate(samp_list):
|
|
523
|
+
cur_filt = samp_full == cur_samp
|
|
524
|
+
cur_state = onmf_rep_tri[cur_filt, :]
|
|
525
|
+
state_list[ii] = cur_state.T
|
|
526
|
+
|
|
527
|
+
return state_list, samp_list
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def para_moments_numpy(j_mat: np.array, h_vec: np.array) -> Tuple[np.array, np.array]:
|
|
531
|
+
"""
|
|
532
|
+
Calculate the correlation and mean parameters given a j matrix and an h vector.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
j_mat : np.array
|
|
537
|
+
Interaction matrix.
|
|
538
|
+
h_vec : np.array
|
|
539
|
+
External field vector.
|
|
540
|
+
|
|
541
|
+
Returns
|
|
542
|
+
-------
|
|
543
|
+
Tuple[np.array, np.array]
|
|
544
|
+
The correlation parameter.
|
|
545
|
+
The mean parameter.
|
|
546
|
+
"""
|
|
547
|
+
num_spin = j_mat.shape[0]
|
|
548
|
+
num_sample = 3 ** num_spin
|
|
549
|
+
sample_indices = np.indices((3,) * num_spin)
|
|
550
|
+
ordered_sample = (sample_indices - 1).reshape(num_spin, num_sample)
|
|
551
|
+
|
|
552
|
+
# Adding diagonal elements to themselves for calculation
|
|
553
|
+
j_mat = j_mat + np.diag(np.diag(j_mat))
|
|
554
|
+
ordered_energy = - (h_vec.T @ ordered_sample + \
|
|
555
|
+
np.sum((j_mat @ ordered_sample) * ordered_sample, axis=0) / 2)
|
|
556
|
+
ordered_exp = np.exp(-ordered_energy)
|
|
557
|
+
partition = np.sum(ordered_exp)
|
|
558
|
+
freq = ordered_exp / partition # Calculating the frequency
|
|
559
|
+
mean_para = np.sum(ordered_sample * freq.reshape(1, -1), axis=1)
|
|
560
|
+
|
|
561
|
+
corr_para = np.einsum(
|
|
562
|
+
'i,ji,ki->jk',
|
|
563
|
+
freq.flatten(),
|
|
564
|
+
ordered_sample,
|
|
565
|
+
ordered_sample)
|
|
566
|
+
|
|
567
|
+
return corr_para, mean_para
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@numba.njit(fastmath=True, cache=True)
|
|
571
|
+
def para_moments(j_mat: np.ndarray,
|
|
572
|
+
h_vec: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
573
|
+
"""
|
|
574
|
+
Calculate the correlation and mean parameters given an interaction
|
|
575
|
+
matrix and an external field vector, using a Numba‑accelerated kernel.
|
|
576
|
+
|
|
577
|
+
Parameters
|
|
578
|
+
----------
|
|
579
|
+
j_mat : np.ndarray
|
|
580
|
+
Interaction matrix.
|
|
581
|
+
h_vec : np.ndarray
|
|
582
|
+
External field vector.
|
|
583
|
+
|
|
584
|
+
Returns
|
|
585
|
+
-------
|
|
586
|
+
Tuple[np.ndarray, np.ndarray]
|
|
587
|
+
The correlation parameter array of shape (n, n).
|
|
588
|
+
The mean parameter array of shape (n,).
|
|
589
|
+
"""
|
|
590
|
+
num_spin: int = j_mat.shape[0]
|
|
591
|
+
|
|
592
|
+
# Copy so we can safely modify the diagonal in‑place
|
|
593
|
+
j_dbl = j_mat.copy()
|
|
594
|
+
for ii in range(num_spin):
|
|
595
|
+
j_dbl[ii, ii] += j_mat[ii, ii]
|
|
596
|
+
|
|
597
|
+
num_sample: int = 3 ** num_spin
|
|
598
|
+
|
|
599
|
+
partition: float = 0.0
|
|
600
|
+
mean_para = np.zeros(num_spin, dtype=np.float64)
|
|
601
|
+
corr_para = np.zeros((num_spin, num_spin), dtype=np.float64)
|
|
602
|
+
|
|
603
|
+
spins = np.empty(num_spin, dtype=np.int8)
|
|
604
|
+
|
|
605
|
+
for idx in range(num_sample):
|
|
606
|
+
# Decode `idx` → base‑3 spin configuration in {‑1, 0, 1}
|
|
607
|
+
tmp = idx
|
|
608
|
+
for s in range(num_spin - 1, -1, -1):
|
|
609
|
+
spins[s] = (tmp % 3) - 1
|
|
610
|
+
tmp //= 3
|
|
611
|
+
|
|
612
|
+
# Energy: E = −h·s − s'Js / 2
|
|
613
|
+
energy: float = 0.0
|
|
614
|
+
for ii in range(num_spin):
|
|
615
|
+
energy -= h_vec[ii] * spins[ii]
|
|
616
|
+
|
|
617
|
+
js_term: float = 0.0
|
|
618
|
+
for ii in range(num_spin):
|
|
619
|
+
dot: float = 0.0
|
|
620
|
+
for jj in range(num_spin):
|
|
621
|
+
dot += j_dbl[ii, jj] * spins[jj]
|
|
622
|
+
js_term += spins[ii] * dot
|
|
623
|
+
energy -= 0.5 * js_term
|
|
624
|
+
|
|
625
|
+
weight: float = np.exp(-energy)
|
|
626
|
+
|
|
627
|
+
partition += weight
|
|
628
|
+
for ii in range(num_spin):
|
|
629
|
+
mean_para[ii] += spins[ii] * weight
|
|
630
|
+
for jj in range(num_spin):
|
|
631
|
+
corr_para[ii, jj] += spins[ii] * spins[jj] * weight
|
|
632
|
+
|
|
633
|
+
mean_para /= partition
|
|
634
|
+
corr_para /= partition
|
|
635
|
+
|
|
636
|
+
return corr_para, mean_para
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@numba.njit(fastmath=True)
|
|
640
|
+
def pseudol_gradient(cur_j: np.ndarray,
|
|
641
|
+
cur_h: np.ndarray,
|
|
642
|
+
cur_state: np.ndarray,
|
|
643
|
+
directed: bool = False) -> Tuple[np.ndarray, np.ndarray]:
|
|
644
|
+
"""
|
|
645
|
+
Compute the pseudo-likelihood gradients of j and h.
|
|
646
|
+
|
|
647
|
+
Parameters
|
|
648
|
+
----------
|
|
649
|
+
cur_j : np.ndarray
|
|
650
|
+
Current j matrix.
|
|
651
|
+
cur_h : np.ndarray
|
|
652
|
+
Current h vector.
|
|
653
|
+
cur_state : np.ndarray
|
|
654
|
+
Current state matrix.
|
|
655
|
+
directed : bool, optional
|
|
656
|
+
Whether to compute directed gradients. Default is False.
|
|
657
|
+
|
|
658
|
+
Returns
|
|
659
|
+
-------
|
|
660
|
+
Tuple[np.array, np.array]
|
|
661
|
+
The gradient of j.
|
|
662
|
+
The gradient of h.
|
|
663
|
+
"""
|
|
664
|
+
num_spin, num_sample = cur_state.shape
|
|
665
|
+
sample_factor = 1.0 / num_sample
|
|
666
|
+
|
|
667
|
+
diag_j = np.empty(num_spin, dtype=cur_j.dtype)
|
|
668
|
+
for ii in range(num_spin):
|
|
669
|
+
diag_j[ii] = cur_j[ii, ii]
|
|
670
|
+
|
|
671
|
+
j_no_diag = cur_j.copy()
|
|
672
|
+
for ii in range(num_spin):
|
|
673
|
+
j_no_diag[ii, ii] = 0.0
|
|
674
|
+
|
|
675
|
+
eff_h = j_no_diag @ cur_state + cur_h
|
|
676
|
+
|
|
677
|
+
exp_plus = np.empty_like(eff_h)
|
|
678
|
+
exp_minus = np.empty_like(eff_h)
|
|
679
|
+
denom = np.empty_like(eff_h)
|
|
680
|
+
|
|
681
|
+
for ii in range(num_spin):
|
|
682
|
+
exp_plus[ii, :] = np.exp(diag_j[ii] + eff_h[ii, :])
|
|
683
|
+
exp_minus[ii, :] = np.exp(diag_j[ii] - eff_h[ii, :])
|
|
684
|
+
denom[ii, :] = exp_plus[ii, :] + exp_minus[ii, :] + 1.0
|
|
685
|
+
|
|
686
|
+
# Diagonal (self‑coupling) gradients
|
|
687
|
+
term_diag = cur_state * cur_state - (exp_plus + exp_minus) / denom
|
|
688
|
+
|
|
689
|
+
# Off‑diagonal and field gradients
|
|
690
|
+
h_eff_grad = cur_state - (exp_plus - exp_minus) / denom
|
|
691
|
+
cur_j_grad = (h_eff_grad @ cur_state.T) * sample_factor
|
|
692
|
+
for ii in range(num_spin):
|
|
693
|
+
cur_j_grad[ii, ii] = np.sum(term_diag[ii, :]) * sample_factor
|
|
694
|
+
|
|
695
|
+
cur_h_grad = np.zeros((num_spin, 1), dtype=cur_j.dtype)
|
|
696
|
+
for ii in range(num_spin):
|
|
697
|
+
cur_h_grad[ii, 0] = np.sum(h_eff_grad[ii, :]) * sample_factor
|
|
698
|
+
|
|
699
|
+
if not directed:
|
|
700
|
+
cur_j_grad = 0.5 * (cur_j_grad + cur_j_grad.T)
|
|
701
|
+
|
|
702
|
+
return - cur_j_grad, - cur_h_grad
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@numba.njit()
|
|
706
|
+
def samp_moments(j_mat: np.ndarray,
|
|
707
|
+
h_vec: np.ndarray,
|
|
708
|
+
sample_size: int,
|
|
709
|
+
mixing_time: int) -> Tuple[np.ndarray, np.ndarray]:
|
|
710
|
+
"""
|
|
711
|
+
Sample moments for the Markov Chain Monte Carlo (MCMC).
|
|
712
|
+
|
|
713
|
+
Parameters
|
|
714
|
+
----------
|
|
715
|
+
j_mat : np.ndarray
|
|
716
|
+
Interaction matrix.
|
|
717
|
+
h_vec : np.ndarray
|
|
718
|
+
External field vector.
|
|
719
|
+
sample_size : int
|
|
720
|
+
Size of the sample.
|
|
721
|
+
mixing_time : int
|
|
722
|
+
Mixing time for the MCMC.
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
Tuple[np.array, np.array]
|
|
727
|
+
The correlation parameter.
|
|
728
|
+
The mean parameter.
|
|
729
|
+
"""
|
|
730
|
+
sample_size = int(sample_size)
|
|
731
|
+
mixing_time = int(mixing_time)
|
|
732
|
+
|
|
733
|
+
num_spin = j_mat.shape[0]
|
|
734
|
+
rec_corr = np.zeros((num_spin, num_spin), dtype=np.float64)
|
|
735
|
+
rec_mean = np.zeros(num_spin, dtype=np.float64)
|
|
736
|
+
beta = 1.0
|
|
737
|
+
|
|
738
|
+
# Cast & make contiguous (Numba-safe)
|
|
739
|
+
j_mat = np.ascontiguousarray(j_mat.astype(np.float64))
|
|
740
|
+
h_vec = np.ascontiguousarray(h_vec.astype(np.float64)).reshape(-1)
|
|
741
|
+
|
|
742
|
+
# Spins in {-1,0,1}
|
|
743
|
+
cur_spin = np.sign(h_vec).astype(np.float64)
|
|
744
|
+
|
|
745
|
+
# Local field f = h + J @ s (update incrementally on changes)
|
|
746
|
+
f_vec = h_vec + j_mat.dot(cur_spin)
|
|
747
|
+
|
|
748
|
+
# Sampling schedule
|
|
749
|
+
tot_sampling = sample_size + mixing_time
|
|
750
|
+
|
|
751
|
+
# Pre-generate randomness (one uniform per site update)
|
|
752
|
+
rand_prob = np.random.rand(tot_sampling)
|
|
753
|
+
|
|
754
|
+
# Lazy accumulation buffers
|
|
755
|
+
last_outer = np.zeros((num_spin, num_spin), dtype=np.float64) # current s s^T
|
|
756
|
+
have_last_outer = False
|
|
757
|
+
recording_started = False
|
|
758
|
+
weight = 0 # how many identical samples to add for current last_outer
|
|
759
|
+
|
|
760
|
+
# MCMC with sweep updates + heat-bath conditionals
|
|
761
|
+
for ii in range(tot_sampling):
|
|
762
|
+
# sweep index (no random site index)
|
|
763
|
+
cur_ind = ii % num_spin
|
|
764
|
+
|
|
765
|
+
# current spin and local field excluding self-term
|
|
766
|
+
s_i = cur_spin[cur_ind]
|
|
767
|
+
Jii = j_mat[cur_ind, cur_ind]
|
|
768
|
+
hstar = f_vec[cur_ind] - Jii * s_i
|
|
769
|
+
|
|
770
|
+
# heat-bath weights for k in {+1, 0, -1}:
|
|
771
|
+
# log-weights: a = beta*( hstar + 0.5*Jii ), b = 0, c = beta*( -hstar + 0.5*Jii )
|
|
772
|
+
a = beta * (hstar + Jii)
|
|
773
|
+
b = 0.0
|
|
774
|
+
c = beta * (-hstar + Jii)
|
|
775
|
+
m = max(a, b, c)
|
|
776
|
+
pa = np.exp(a - m)
|
|
777
|
+
pb = np.exp(b - m) # == exp(-m)
|
|
778
|
+
pc = np.exp(c - m)
|
|
779
|
+
Z = pa + pb + pc
|
|
780
|
+
|
|
781
|
+
# sample new spin
|
|
782
|
+
u = rand_prob[ii]
|
|
783
|
+
thresh_a = pa / Z
|
|
784
|
+
thresh_ab = (pa + pb) / Z
|
|
785
|
+
if u < thresh_a:
|
|
786
|
+
new_spin = 1.0
|
|
787
|
+
elif u < thresh_ab:
|
|
788
|
+
new_spin = 0.0
|
|
789
|
+
else:
|
|
790
|
+
new_spin = - 1.0
|
|
791
|
+
|
|
792
|
+
# if spin changed, update cur_spin and cached field
|
|
793
|
+
changed = (new_spin != s_i)
|
|
794
|
+
if changed:
|
|
795
|
+
delta_s = new_spin - s_i
|
|
796
|
+
cur_spin[cur_ind] = new_spin
|
|
797
|
+
# incremental local-field update: f_k += J[k,i] * delta_s
|
|
798
|
+
for k in range(num_spin):
|
|
799
|
+
f_vec[k] += j_mat[k, cur_ind] * delta_s
|
|
800
|
+
|
|
801
|
+
# If we're recording and have accumulated weight, flush rec_corr BEFORE changing last_outer
|
|
802
|
+
if recording_started and weight > 0 and have_last_outer:
|
|
803
|
+
for k in range(num_spin):
|
|
804
|
+
row = rec_corr[k]
|
|
805
|
+
lo_row = last_outer[k]
|
|
806
|
+
for l in range(num_spin):
|
|
807
|
+
row[l] += weight * lo_row[l]
|
|
808
|
+
weight = 0
|
|
809
|
+
|
|
810
|
+
# Update last_outer incrementally if it exists
|
|
811
|
+
if recording_started and have_last_outer:
|
|
812
|
+
d = delta_s
|
|
813
|
+
i = cur_ind
|
|
814
|
+
# s's' = s s^T + d (e_i s'^T + s' e_i^T) - d^2 e_i e_i^T (uses updated cur_spin)
|
|
815
|
+
for j in range(num_spin):
|
|
816
|
+
last_outer[i, j] += d * cur_spin[j]
|
|
817
|
+
for j in range(num_spin):
|
|
818
|
+
last_outer[j, i] += d * cur_spin[j]
|
|
819
|
+
last_outer[i, i] -= d * d
|
|
820
|
+
|
|
821
|
+
# Record sample on schedule
|
|
822
|
+
if ii >= mixing_time:
|
|
823
|
+
# mean: O(N)
|
|
824
|
+
for k in range(num_spin):
|
|
825
|
+
rec_mean[k] += cur_spin[k]
|
|
826
|
+
|
|
827
|
+
# initialize last_outer once at the first record
|
|
828
|
+
if not recording_started:
|
|
829
|
+
for k in range(num_spin):
|
|
830
|
+
sk = cur_spin[k]
|
|
831
|
+
row = last_outer[k]
|
|
832
|
+
for l in range(num_spin):
|
|
833
|
+
row[l] = sk * cur_spin[l]
|
|
834
|
+
have_last_outer = True
|
|
835
|
+
recording_started = True
|
|
836
|
+
|
|
837
|
+
# lazily count how many times to add current outer
|
|
838
|
+
weight += 1
|
|
839
|
+
|
|
840
|
+
# Flush any remaining weight after the loop
|
|
841
|
+
if recording_started and weight > 0 and have_last_outer:
|
|
842
|
+
for k in range(num_spin):
|
|
843
|
+
row = rec_corr[k]
|
|
844
|
+
lo_row = last_outer[k]
|
|
845
|
+
for l in range(num_spin):
|
|
846
|
+
row[l] += weight * lo_row[l]
|
|
847
|
+
|
|
848
|
+
corr_para = rec_corr / sample_size
|
|
849
|
+
mean_para = rec_mean / sample_size
|
|
850
|
+
|
|
851
|
+
return corr_para, mean_para
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
@numba.njit()
|
|
855
|
+
def samp_mcmc_states(j_mat: np.ndarray,
|
|
856
|
+
h_vec: np.ndarray,
|
|
857
|
+
sample_size: int,
|
|
858
|
+
mixing_time: int,
|
|
859
|
+
samp_gap: int) -> np.ndarray:
|
|
860
|
+
"""
|
|
861
|
+
Sample states for the Markov Chain Monte Carlo (MCMC).
|
|
862
|
+
|
|
863
|
+
Parameters
|
|
864
|
+
----------
|
|
865
|
+
j_mat : np.ndarray
|
|
866
|
+
Interaction matrix.
|
|
867
|
+
h_vec : np.ndarray
|
|
868
|
+
External field vector.
|
|
869
|
+
sample_size : int
|
|
870
|
+
Size of the sample.
|
|
871
|
+
mixing_time : int
|
|
872
|
+
Mixing time for the MCMC.
|
|
873
|
+
samp_gap : int
|
|
874
|
+
Gap between samples.
|
|
875
|
+
|
|
876
|
+
Returns
|
|
877
|
+
-------
|
|
878
|
+
np.ndarray
|
|
879
|
+
The sampled states.
|
|
880
|
+
"""
|
|
881
|
+
sample_size = int(sample_size)
|
|
882
|
+
mixing_time = int(mixing_time)
|
|
883
|
+
samp_gap = int(samp_gap)
|
|
884
|
+
|
|
885
|
+
num_spin = j_mat.shape[0]
|
|
886
|
+
|
|
887
|
+
beta = 1.0
|
|
888
|
+
batch_count = 1
|
|
889
|
+
rec_sample = np.empty((num_spin, int(sample_size)), dtype=np.float64)
|
|
890
|
+
|
|
891
|
+
# Cast & make contiguous (Numba-safe)
|
|
892
|
+
j_mat = np.ascontiguousarray(j_mat.astype(np.float64))
|
|
893
|
+
# Accept both (n,) and (n,1) h_vec; flatten to 1D
|
|
894
|
+
h_vec = h_vec.reshape(h_vec.size)
|
|
895
|
+
h_vec = np.ascontiguousarray(h_vec.astype(np.float64))
|
|
896
|
+
|
|
897
|
+
# Spins in {-1,0,1}
|
|
898
|
+
cur_spin = (np.random.randint(0, 3, num_spin) - 1).astype(np.float64)
|
|
899
|
+
|
|
900
|
+
# Local field f = h + J @ s (update incrementally on changes)
|
|
901
|
+
f_vec = h_vec + j_mat.dot(cur_spin)
|
|
902
|
+
|
|
903
|
+
# Sampling schedule (avoid modulo inside loop)
|
|
904
|
+
rem = mixing_time % samp_gap
|
|
905
|
+
first_sample = mixing_time if rem == 0 else (mixing_time + (samp_gap - rem))
|
|
906
|
+
last_sample = first_sample + (sample_size - 1) * samp_gap
|
|
907
|
+
tot_sampling = last_sample + 1
|
|
908
|
+
next_sample = first_sample
|
|
909
|
+
|
|
910
|
+
# Pre-generate randomness: one uniform per single-site update
|
|
911
|
+
rand_prob = np.random.rand(tot_sampling)
|
|
912
|
+
|
|
913
|
+
# MCMC (Gibbs) with deterministic sweep order
|
|
914
|
+
for ii in range(tot_sampling):
|
|
915
|
+
cur_ind = ii % num_spin
|
|
916
|
+
|
|
917
|
+
# ---- Compute conditional for s_i given s_-i (PRE-UPDATE neighborhood) ----
|
|
918
|
+
s_i = cur_spin[cur_ind]
|
|
919
|
+
Jii = j_mat[cur_ind, cur_ind]
|
|
920
|
+
# h*_i = h_i + sum_{j!=i} J_ij s_j = f_i - J_ii * s_i (pre-update)
|
|
921
|
+
hstar = f_vec[cur_ind] - Jii * s_i
|
|
922
|
+
|
|
923
|
+
# Unnormalized log-weights for k in {+1, 0, -1}
|
|
924
|
+
# Matches the enumerator convention (diagonal contributes +J_ii*k^2 here)
|
|
925
|
+
a = beta * (hstar + Jii) # log w(+1)
|
|
926
|
+
b = 0.0 # log w(0)
|
|
927
|
+
c = beta * (-hstar + Jii) # log w(-1)
|
|
928
|
+
|
|
929
|
+
# Stable normalization via max-trick
|
|
930
|
+
m = max(a, b, c)
|
|
931
|
+
pa = np.exp(a - m)
|
|
932
|
+
pb = np.exp(b - m)
|
|
933
|
+
pc = np.exp(c - m)
|
|
934
|
+
Z = pa + pb + pc
|
|
935
|
+
|
|
936
|
+
# Sample new spin from the conditional (heat-bath)
|
|
937
|
+
u = rand_prob[ii]
|
|
938
|
+
t1 = pa / Z
|
|
939
|
+
t2 = (pa + pb) / Z
|
|
940
|
+
if u < t1:
|
|
941
|
+
new_spin = 1.0
|
|
942
|
+
elif u < t2:
|
|
943
|
+
new_spin = 0.0
|
|
944
|
+
else:
|
|
945
|
+
new_spin = -1.0
|
|
946
|
+
|
|
947
|
+
# If changed, update spin and cached local field
|
|
948
|
+
if new_spin != s_i:
|
|
949
|
+
delta_s = new_spin - s_i
|
|
950
|
+
cur_spin[cur_ind] = new_spin
|
|
951
|
+
# Incremental local-field update: f_k += J[k, i] * delta_s
|
|
952
|
+
for k in range(num_spin):
|
|
953
|
+
f_vec[k] += j_mat[k, cur_ind] * delta_s
|
|
954
|
+
|
|
955
|
+
# Record sample on schedule
|
|
956
|
+
if ii == next_sample:
|
|
957
|
+
for k in range(num_spin):
|
|
958
|
+
rec_sample[k, batch_count - 1] = cur_spin[k]
|
|
959
|
+
batch_count += 1
|
|
960
|
+
next_sample += samp_gap
|
|
961
|
+
|
|
962
|
+
return rec_sample
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _gradient_one_round(kk: int,
|
|
966
|
+
cur_j: np.ndarray,
|
|
967
|
+
cur_h: np.ndarray,
|
|
968
|
+
raw_data_k,
|
|
969
|
+
method: str,
|
|
970
|
+
train_dat: Dict[str, Any]):
|
|
971
|
+
"""Helper that compute gradient for a single round."""
|
|
972
|
+
if method == 'pseudo_likelihood':
|
|
973
|
+
j_grad, h_grad = pseudol_gradient(cur_j, cur_h[:, kk:kk + 1], raw_data_k, directed=train_dat['directed'])
|
|
974
|
+
h_grad = h_grad.flatten()
|
|
975
|
+
else:
|
|
976
|
+
if method == 'maximum_likelihood':
|
|
977
|
+
corr_para, mean_para = para_moments(cur_j, cur_h[:, kk])
|
|
978
|
+
elif method == 'mcmc_maximum_likelihood':
|
|
979
|
+
corr_para, mean_para = samp_moments(
|
|
980
|
+
cur_j, cur_h[:, kk], train_dat['mcmc_samplingsz'], train_dat['mcmc_samplingmix'])
|
|
981
|
+
j_grad = corr_para - raw_data_k[0]
|
|
982
|
+
h_grad = mean_para - raw_data_k[1].flatten()
|
|
983
|
+
|
|
984
|
+
return kk, j_grad, h_grad
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def compute_gradient(cur_j: np.ndarray,
|
|
988
|
+
cur_h: np.ndarray,
|
|
989
|
+
raw_data: List[Tuple[np.ndarray, np.ndarray]],
|
|
990
|
+
method: str, train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
|
|
991
|
+
"""
|
|
992
|
+
Compute the gradient based on the specified method.
|
|
993
|
+
|
|
994
|
+
Parameters
|
|
995
|
+
----------
|
|
996
|
+
cur_j : np.ndarray
|
|
997
|
+
Current j matrix.
|
|
998
|
+
cur_h : np.ndarray
|
|
999
|
+
Current h matrix.
|
|
1000
|
+
raw_data : List[Tuple[np.ndarray, np.ndarray]]
|
|
1001
|
+
Raw data used to calculate the gradient (each element is a tuple of correlation and mean).
|
|
1002
|
+
method : str
|
|
1003
|
+
The method to calculate the gradient; possible values are 'pseudo_likelihood',
|
|
1004
|
+
'maximum_likelihood', and 'mcmc_maximum_likelihood'.
|
|
1005
|
+
train_dat : Dict[str, Any]
|
|
1006
|
+
Training data and hyperparameters.
|
|
1007
|
+
|
|
1008
|
+
Returns
|
|
1009
|
+
-------
|
|
1010
|
+
Tuple[np.array, np.array]
|
|
1011
|
+
The gradient of j.
|
|
1012
|
+
The gradient of h.
|
|
1013
|
+
"""
|
|
1014
|
+
num_spin, num_round = cur_h.shape
|
|
1015
|
+
|
|
1016
|
+
rec_jgrad = np.zeros((num_spin, num_spin, num_round))
|
|
1017
|
+
rec_hgrad = np.zeros((num_spin, num_round))
|
|
1018
|
+
|
|
1019
|
+
if method == 'pseudo_likelihood':
|
|
1020
|
+
for kk in range(num_round):
|
|
1021
|
+
# Computing gradients using the specified method.
|
|
1022
|
+
j_grad, h_grad = pseudol_gradient(
|
|
1023
|
+
cur_j, cur_h[:, kk: kk + 1], raw_data[kk], directed=train_dat['directed'])
|
|
1024
|
+
h_grad = h_grad.flatten()
|
|
1025
|
+
|
|
1026
|
+
rec_jgrad[:, :, kk] = j_grad
|
|
1027
|
+
rec_hgrad[:, kk] = h_grad
|
|
1028
|
+
else:
|
|
1029
|
+
n_jobs = train_dat.get('max_workers', min(num_round, os.cpu_count()))
|
|
1030
|
+
chunk = train_dat.get('chunk_size', 1) # rounds per task
|
|
1031
|
+
backend = 'loky' # process‑based
|
|
1032
|
+
|
|
1033
|
+
# print(f'Processing with {n_jobs} workers...')
|
|
1034
|
+
with parallel_backend(backend, n_jobs=n_jobs):
|
|
1035
|
+
results = Parallel(batch_size=chunk)(
|
|
1036
|
+
delayed(_gradient_one_round)(kk, cur_j, cur_h, raw_data[kk], method, train_dat)
|
|
1037
|
+
for kk in range(num_round)
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
for kk, j_grad, h_grad in results:
|
|
1041
|
+
rec_jgrad[:, :, kk] = j_grad
|
|
1042
|
+
rec_hgrad[:, kk] = h_grad
|
|
1043
|
+
|
|
1044
|
+
return rec_jgrad, rec_hgrad
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def compute_gradient_para(cur_j: np.ndarray,
|
|
1048
|
+
cur_h: np.ndarray,
|
|
1049
|
+
raw_data: List[Tuple[np.ndarray, np.ndarray]],
|
|
1050
|
+
method: str, train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
|
|
1051
|
+
"""
|
|
1052
|
+
Compute the gradient based on the specified method with parallel processing.
|
|
1053
|
+
|
|
1054
|
+
Parameters
|
|
1055
|
+
----------
|
|
1056
|
+
cur_j : np.ndarray
|
|
1057
|
+
Current j matrix.
|
|
1058
|
+
cur_h : np.ndarray
|
|
1059
|
+
Current h matrix.
|
|
1060
|
+
raw_data : List[Tuple[np.ndarray, np.ndarray]]
|
|
1061
|
+
Raw data used to calculate the gradient (each element is a tuple of correlation and mean).
|
|
1062
|
+
method : str
|
|
1063
|
+
The method to calculate the gradient; possible values are 'pseudo_likelihood',
|
|
1064
|
+
'maximum_likelihood', and 'mcmc_maximum_likelihood'.
|
|
1065
|
+
train_dat : Dict[str, Any]
|
|
1066
|
+
Training data and hyperparameters.
|
|
1067
|
+
|
|
1068
|
+
Returns
|
|
1069
|
+
-------
|
|
1070
|
+
Tuple[np.array, np.array]
|
|
1071
|
+
The gradient of j.
|
|
1072
|
+
The gradient of h.
|
|
1073
|
+
"""
|
|
1074
|
+
num_spin, num_round = cur_h.shape
|
|
1075
|
+
rec_jgrad = np.zeros((num_spin, num_spin, num_round))
|
|
1076
|
+
rec_hgrad = np.zeros((num_spin, num_round))
|
|
1077
|
+
|
|
1078
|
+
n_jobs = train_dat.get('max_workers', min(num_round, os.cpu_count()))
|
|
1079
|
+
chunk = train_dat.get('chunk_size', 1) # rounds per task
|
|
1080
|
+
backend = 'loky' # process‑based
|
|
1081
|
+
|
|
1082
|
+
# print(f'Processing with {n_jobs} workers...')
|
|
1083
|
+
with parallel_backend(backend, n_jobs=n_jobs):
|
|
1084
|
+
results = Parallel(batch_size=chunk)(
|
|
1085
|
+
delayed(_gradient_one_round)(kk, cur_j, cur_h, raw_data[kk], method, train_dat)
|
|
1086
|
+
for kk in range(num_round)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
for kk, j_grad, h_grad in results:
|
|
1090
|
+
rec_jgrad[:, :, kk] = j_grad
|
|
1091
|
+
rec_hgrad[:, kk] = h_grad
|
|
1092
|
+
|
|
1093
|
+
return rec_jgrad, rec_hgrad
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def apply_regularization(rec_jgrad: np.ndarray,
|
|
1097
|
+
rec_hgrad: np.ndarray,
|
|
1098
|
+
cur_j: np.ndarray,
|
|
1099
|
+
cur_h: np.ndarray,
|
|
1100
|
+
train_dat: Dict[str, Any],
|
|
1101
|
+
print_regu: bool = False) -> Tuple[np.ndarray, np.ndarray]:
|
|
1102
|
+
"""
|
|
1103
|
+
Apply regularization to the gradients.
|
|
1104
|
+
|
|
1105
|
+
Parameters
|
|
1106
|
+
----------
|
|
1107
|
+
rec_jgrad : np.ndarray
|
|
1108
|
+
Gradients of j matrix.
|
|
1109
|
+
rec_hgrad : np.ndarray
|
|
1110
|
+
Gradients of h matrix.
|
|
1111
|
+
cur_j : np.ndarray
|
|
1112
|
+
Current j matrix.
|
|
1113
|
+
cur_h : np.ndarray
|
|
1114
|
+
Current h matrix.
|
|
1115
|
+
train_dat : Dict[str, Any]
|
|
1116
|
+
Training data and hyperparameters.
|
|
1117
|
+
print_regu : bool, optional
|
|
1118
|
+
If True, print regularization parameters. Default is False.
|
|
1119
|
+
|
|
1120
|
+
Returns
|
|
1121
|
+
-------
|
|
1122
|
+
Tuple[np.array, np.array]
|
|
1123
|
+
Regularized gradients of j.
|
|
1124
|
+
Regularized gradients of h.
|
|
1125
|
+
"""
|
|
1126
|
+
num_spin, num_round = cur_h.shape
|
|
1127
|
+
|
|
1128
|
+
lambda_l1_j, lambda_l1_h, lambda_l2_j, lambda_l2_h = (
|
|
1129
|
+
train_dat.get(
|
|
1130
|
+
key, 0) for key in [
|
|
1131
|
+
"lambda_l1_j", "lambda_l1_h", "lambda_l2_j", "lambda_l2_h"])
|
|
1132
|
+
|
|
1133
|
+
if lambda_l1_j > 0:
|
|
1134
|
+
rec_jgrad += lambda_l1_j * \
|
|
1135
|
+
(cur_j / 1e-3).clip(-1, 1).reshape(num_spin, num_spin, 1)
|
|
1136
|
+
if lambda_l1_h > 0:
|
|
1137
|
+
rec_hgrad += lambda_l1_h * (cur_h / 1e-3).clip(-1, 1)
|
|
1138
|
+
|
|
1139
|
+
if lambda_l2_j > 0:
|
|
1140
|
+
rec_jgrad += lambda_l2_j * cur_j.reshape(num_spin, num_spin, 1)
|
|
1141
|
+
if lambda_l2_h > 0:
|
|
1142
|
+
rec_hgrad += lambda_l2_h * cur_h
|
|
1143
|
+
|
|
1144
|
+
if 'if_control' in train_dat:
|
|
1145
|
+
if_control = train_dat['if_control']
|
|
1146
|
+
batch_index = train_dat['batch_index']
|
|
1147
|
+
h_rela = compute_relative_responses(cur_h, if_control, batch_index)
|
|
1148
|
+
|
|
1149
|
+
if 'lambda_l2_h_rela_prior' in train_dat:
|
|
1150
|
+
perturb_matrix = train_dat['perturb_matrix']
|
|
1151
|
+
rec_hgrad = average_batch_control(rec_hgrad, if_control, batch_index)
|
|
1152
|
+
rec_hgrad += train_dat['lambda_l2_h_rela_prior'] * (h_rela - perturb_matrix)
|
|
1153
|
+
|
|
1154
|
+
if 'lambda_l1_h_rela' in train_dat:
|
|
1155
|
+
rec_hgrad += train_dat['lambda_l1_h_rela'] * (h_rela / 1e-3).clip(-1, 1)
|
|
1156
|
+
|
|
1157
|
+
if 'lambda_l2_h_rela' in train_dat:
|
|
1158
|
+
rec_hgrad += train_dat['lambda_l2_h_rela'] * h_rela
|
|
1159
|
+
|
|
1160
|
+
if 'lambda_l2_j_prior' in train_dat:
|
|
1161
|
+
rec_jgrad += (train_dat['lambda_l2_j_prior'] * (cur_j - train_dat['j_prior']))[:, :, np.newaxis]
|
|
1162
|
+
|
|
1163
|
+
if 'lambda_l2_j_prior_mask' in train_dat:
|
|
1164
|
+
rec_jgrad += train_dat['lambda_l2_j_prior_mask'] * (cur_j * (train_dat['j_prior_mask'] == 0))[:, :, np.newaxis]
|
|
1165
|
+
|
|
1166
|
+
if 'lambda_l1_j_prior_mask' in train_dat:
|
|
1167
|
+
rec_jgrad += train_dat['lambda_l1_j_prior_mask'] * ((cur_j * (train_dat['j_prior_mask'] == 0))[:, :, np.newaxis] / 1e-3).clip(-1, 1)
|
|
1168
|
+
|
|
1169
|
+
if print_regu:
|
|
1170
|
+
print('Regularization parameters')
|
|
1171
|
+
all_regu_list = ['lambda_l1_j', 'lambda_l1_h', 'lambda_l2_j', 'lambda_l2_h', 'lambda_l2_j_prior', 'lambda_l2_j_prior_mask', 'lambda_l2_h_rela_prior', 'lambda_l1_h_rela', 'lambda_l2_h_rela', 'lambda_l1_j_prior_mask']
|
|
1172
|
+
all_values = [train_dat.get(key, 0) for key in all_regu_list]
|
|
1173
|
+
for key, value in zip(all_regu_list, all_values):
|
|
1174
|
+
if value > 0:
|
|
1175
|
+
print(f'{key}: {value}')
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
return rec_jgrad, rec_hgrad
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def update_adam(gradient: np.ndarray,
|
|
1182
|
+
m: np.ndarray,
|
|
1183
|
+
v: np.ndarray,
|
|
1184
|
+
counter: int,
|
|
1185
|
+
stepsz: float,
|
|
1186
|
+
beta1: float = 0.9,
|
|
1187
|
+
beta2: float = 0.999,
|
|
1188
|
+
epsilon: float = 1e-8) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
1189
|
+
"""
|
|
1190
|
+
Adam optimizer update rule.
|
|
1191
|
+
|
|
1192
|
+
Parameters
|
|
1193
|
+
----------
|
|
1194
|
+
gradient : np.ndarray
|
|
1195
|
+
The gradient of the objective function.
|
|
1196
|
+
m : np.ndarray
|
|
1197
|
+
1st moment vector (moving average of the gradients).
|
|
1198
|
+
v : np.ndarray
|
|
1199
|
+
2nd moment vector (moving average of the gradient squared).
|
|
1200
|
+
counter : int
|
|
1201
|
+
The current time step or epoch.
|
|
1202
|
+
stepsz : float
|
|
1203
|
+
Step size or learning rate.
|
|
1204
|
+
beta1 : float, optional
|
|
1205
|
+
The exponential decay rate for the 1st moment vector.
|
|
1206
|
+
beta2 : float, optional
|
|
1207
|
+
The exponential decay rate for the 2nd moment vector.
|
|
1208
|
+
epsilon : float, optional
|
|
1209
|
+
Small constant to prevent division by zero.
|
|
1210
|
+
|
|
1211
|
+
Returns
|
|
1212
|
+
-------
|
|
1213
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
1214
|
+
The update value.
|
|
1215
|
+
The updated first moment vector.
|
|
1216
|
+
The updated second moment vector.
|
|
1217
|
+
"""
|
|
1218
|
+
# Update biased first moment estimate and biased second raw moment estimate
|
|
1219
|
+
m = beta1 * m + (1 - beta1) * gradient
|
|
1220
|
+
v = beta2 * v + (1 - beta2) * (gradient ** 2)
|
|
1221
|
+
|
|
1222
|
+
# Compute bias-corrected first moment estimate and bias-corrected second
|
|
1223
|
+
# raw moment estimate
|
|
1224
|
+
m_hat = m / (1 - beta1 ** counter)
|
|
1225
|
+
v_hat = v / (1 - beta2 ** counter)
|
|
1226
|
+
|
|
1227
|
+
# Compute the update rule for parameters
|
|
1228
|
+
update = stepsz * m_hat / (np.sqrt(v_hat) + epsilon)
|
|
1229
|
+
|
|
1230
|
+
return update, m, v
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def learn_network_adam(raw_data: Any,
|
|
1234
|
+
method: str,
|
|
1235
|
+
train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray, Dict[str, Any]]:
|
|
1236
|
+
"""
|
|
1237
|
+
Train the network using the Adam optimizer.
|
|
1238
|
+
|
|
1239
|
+
Parameters
|
|
1240
|
+
----------
|
|
1241
|
+
raw_data : Any
|
|
1242
|
+
The input data.
|
|
1243
|
+
method : str
|
|
1244
|
+
The chosen method for training.
|
|
1245
|
+
train_dat : Dict[str, Any]
|
|
1246
|
+
Dictionary containing training data and hyperparameters.
|
|
1247
|
+
|
|
1248
|
+
Returns
|
|
1249
|
+
-------
|
|
1250
|
+
Tuple[np.ndarray, np.ndarray, dict]
|
|
1251
|
+
The trained j matrix.
|
|
1252
|
+
The trained h matrix.
|
|
1253
|
+
The training log.
|
|
1254
|
+
"""
|
|
1255
|
+
np.random.seed(train_dat['seed'])
|
|
1256
|
+
|
|
1257
|
+
# Retrieve training parameters and data
|
|
1258
|
+
num_spin, num_round = train_dat['cur_h'].shape
|
|
1259
|
+
num_epoch, stepsz, rec_gap = (
|
|
1260
|
+
train_dat.get(
|
|
1261
|
+
key, None) for key in [
|
|
1262
|
+
"num_epoch", "stepsz", "rec_gap"])
|
|
1263
|
+
list_step = np.arange(num_epoch, 0, - rec_gap)[::-1]
|
|
1264
|
+
cur_j, cur_h = (train_dat.get(key, None) for key in ["cur_j", "cur_h"])
|
|
1265
|
+
save_path = train_dat.get('save_path', None)
|
|
1266
|
+
backtrack_gap, backtrack_tol = (train_dat.get(key, None) for key in ["backtrack_gap", "backtrack_tol"])
|
|
1267
|
+
|
|
1268
|
+
# Initialize variables to store parameters, gradients, and other values
|
|
1269
|
+
# during training
|
|
1270
|
+
rec_jmat_all = np.zeros((num_epoch, num_spin, num_spin))
|
|
1271
|
+
rec_hvec_all = np.zeros((num_epoch, num_spin, num_round))
|
|
1272
|
+
rec_jgrad_sum_norm = np.inf * np.ones(num_epoch)
|
|
1273
|
+
mjj, vjj = np.zeros(cur_j.shape), np.zeros(cur_j.shape)
|
|
1274
|
+
mhh, vhh = np.zeros(cur_h.shape), np.zeros(cur_h.shape)
|
|
1275
|
+
log_adam_grad = {name: deque(maxlen=backtrack_gap)
|
|
1276
|
+
for name in ["mjj", "vjj", "mhh", "vhh"]}
|
|
1277
|
+
|
|
1278
|
+
apply_regularization(np.zeros((num_spin, num_spin, num_round)), np.zeros((num_spin, num_round)), cur_j, cur_h, train_dat, print_regu=True)
|
|
1279
|
+
|
|
1280
|
+
backtrack_counter = 0
|
|
1281
|
+
counter = 1
|
|
1282
|
+
|
|
1283
|
+
tqdm._instances.clear()
|
|
1284
|
+
pbar = tqdm(total=num_epoch)
|
|
1285
|
+
|
|
1286
|
+
while counter <= num_epoch:
|
|
1287
|
+
|
|
1288
|
+
# Compute gradient and apply regularization
|
|
1289
|
+
rec_jgrad, rec_hgrad = compute_gradient(
|
|
1290
|
+
cur_j, cur_h, raw_data, method, train_dat)
|
|
1291
|
+
rec_jgrad, rec_hgrad = apply_regularization(
|
|
1292
|
+
rec_jgrad, rec_hgrad, cur_j, cur_h, train_dat, print_regu=False)
|
|
1293
|
+
|
|
1294
|
+
# Update parameters using Adam optimizer
|
|
1295
|
+
rec_jgrad_sum = np.sum(rec_jgrad, axis=2)
|
|
1296
|
+
update, mjj, vjj = update_adam(
|
|
1297
|
+
rec_jgrad_sum, mjj, vjj, counter, stepsz)
|
|
1298
|
+
cur_j -= update
|
|
1299
|
+
update, mhh, vhh = update_adam(rec_hgrad, mhh, vhh, counter, stepsz)
|
|
1300
|
+
cur_h -= update
|
|
1301
|
+
|
|
1302
|
+
# Store updated parameters and gradients for later analysis or use
|
|
1303
|
+
rec_jmat_all[counter - 1, :, :] = cur_j
|
|
1304
|
+
rec_hvec_all[counter - 1, :, :] = cur_h
|
|
1305
|
+
rec_jgrad_sum_norm[counter - 1] = np.linalg.norm(rec_jgrad_sum)
|
|
1306
|
+
|
|
1307
|
+
# Log Adam gradients for backtracking
|
|
1308
|
+
for name, value in zip(["mjj", "vjj", "mhh", "vhh"], [
|
|
1309
|
+
mjj, vjj, mhh, vhh]):
|
|
1310
|
+
log_adam_grad[name].append(value)
|
|
1311
|
+
|
|
1312
|
+
# Save training log and print progress
|
|
1313
|
+
if counter in list_step:
|
|
1314
|
+
if train_dat['save_log']:
|
|
1315
|
+
sio.savemat(save_path + 'train_log.mat',
|
|
1316
|
+
{'list_step': list_step,
|
|
1317
|
+
'rec_hvec_all': rec_hvec_all,
|
|
1318
|
+
'rec_jmat_all': rec_jmat_all,
|
|
1319
|
+
'rec_jgrad_sum_norm': rec_jgrad_sum_norm})
|
|
1320
|
+
|
|
1321
|
+
pbar.update(rec_gap)
|
|
1322
|
+
pbar.set_postfix({"Network Gradient": f"{rec_jgrad_sum_norm[counter - 1]:.6f}"})
|
|
1323
|
+
|
|
1324
|
+
# Handle backtracking
|
|
1325
|
+
|
|
1326
|
+
if_backtrack = False
|
|
1327
|
+
|
|
1328
|
+
if (counter > backtrack_gap) and (rec_jgrad_sum_norm[counter - 1] > 1.5 * rec_jgrad_sum_norm[counter - 1 - backtrack_gap]):
|
|
1329
|
+
if_backtrack = True
|
|
1330
|
+
|
|
1331
|
+
if (counter > 2 * backtrack_gap) and (counter % backtrack_gap == 0):
|
|
1332
|
+
|
|
1333
|
+
est_cur_grad = rec_jgrad_sum_norm[counter - 1 - backtrack_gap: counter - 1].mean()
|
|
1334
|
+
est_prev_grad = rec_jgrad_sum_norm[counter - 1 - 2 * backtrack_gap: counter - 1 - backtrack_gap].mean()
|
|
1335
|
+
|
|
1336
|
+
if est_cur_grad > 1.05 * est_prev_grad:
|
|
1337
|
+
if_backtrack = True
|
|
1338
|
+
|
|
1339
|
+
if if_backtrack:
|
|
1340
|
+
print('Backtracking at epoch %d' % counter)
|
|
1341
|
+
backtrack_counter += 1
|
|
1342
|
+
mjj, vjj, mhh, vhh = [log_adam_grad[key][0]
|
|
1343
|
+
for key in ['mjj', 'vjj', 'mhh', 'vhh']]
|
|
1344
|
+
counter = counter - backtrack_gap
|
|
1345
|
+
stepsz = stepsz / 4
|
|
1346
|
+
if backtrack_counter > backtrack_tol:
|
|
1347
|
+
print(
|
|
1348
|
+
'Backtracking more than %d times, stop training.' %
|
|
1349
|
+
backtrack_tol)
|
|
1350
|
+
break
|
|
1351
|
+
else:
|
|
1352
|
+
counter += 1
|
|
1353
|
+
|
|
1354
|
+
pbar.close()
|
|
1355
|
+
|
|
1356
|
+
# Retrieve parameters corresponding to the minimum gradient norm found during training
|
|
1357
|
+
trace_epoch = 50 + num_epoch - counter
|
|
1358
|
+
pos = num_epoch - trace_epoch + np.argmin(rec_jgrad_sum_norm[- trace_epoch: ])
|
|
1359
|
+
# print(pos)
|
|
1360
|
+
cur_h = rec_hvec_all[pos, :, :]
|
|
1361
|
+
cur_j = rec_jmat_all[pos, :, :]
|
|
1362
|
+
|
|
1363
|
+
train_log = {}
|
|
1364
|
+
train_log['network_gradient'] = rec_jgrad_sum_norm[:counter]
|
|
1365
|
+
|
|
1366
|
+
return cur_j, cur_h, train_log
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def learn_program_regulators(gene_states: List[np.ndarray],
|
|
1370
|
+
program_states: List[np.ndarray],
|
|
1371
|
+
train_dat: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
1372
|
+
"""
|
|
1373
|
+
Discover regulators for gene programs via regression.
|
|
1374
|
+
|
|
1375
|
+
Parameters
|
|
1376
|
+
----------
|
|
1377
|
+
gene_states : List[np.ndarray]
|
|
1378
|
+
Array of gene states.
|
|
1379
|
+
program_states : List[np.ndarray]
|
|
1380
|
+
Array of program states.
|
|
1381
|
+
train_dat : Dict[str, Any]
|
|
1382
|
+
Dictionary of training parameters.
|
|
1383
|
+
|
|
1384
|
+
Returns
|
|
1385
|
+
-------
|
|
1386
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
1387
|
+
The learned interaction matrix of shape (n_spin, n_target).
|
|
1388
|
+
The learned self-interaction (regulator-specific bias) of shape (n_target,).
|
|
1389
|
+
The learned program activity bias of shape (n_target,).
|
|
1390
|
+
"""
|
|
1391
|
+
num_gene = gene_states[0].shape[0]
|
|
1392
|
+
num_program = program_states[0].shape[0]
|
|
1393
|
+
num_round = len(gene_states)
|
|
1394
|
+
|
|
1395
|
+
num_epoch, stepsz, rec_gap = (
|
|
1396
|
+
train_dat.get(key, None) for key in [
|
|
1397
|
+
"num_epoch", "stepsz", "rec_gap"])
|
|
1398
|
+
list_step = np.arange(num_epoch, 0, - rec_gap)[::-1]
|
|
1399
|
+
|
|
1400
|
+
save_path = train_dat.get('save_path', None)
|
|
1401
|
+
|
|
1402
|
+
backtrack_gap, backtrack_tol = (
|
|
1403
|
+
train_dat.get(key, None) for key in [
|
|
1404
|
+
"backtrack_gap", "backtrack_tol"])
|
|
1405
|
+
|
|
1406
|
+
lambda_l1_j, lambda_l2_j = (train_dat.get(key, 0) for key in ["lambda_l1_j", "lambda_l2_j"])
|
|
1407
|
+
|
|
1408
|
+
# Initialize parameters (using zeros)
|
|
1409
|
+
cur_interaction = np.zeros((num_gene, num_program))
|
|
1410
|
+
cur_selfj = np.zeros(num_program)
|
|
1411
|
+
cur_selfh = np.zeros(num_program)
|
|
1412
|
+
|
|
1413
|
+
# Compute sample weights based on the number of states (columns) in each gene_states sample
|
|
1414
|
+
state_sizes = np.array([state.shape[1] for state in gene_states])
|
|
1415
|
+
samp_weight = np.sqrt(state_sizes)
|
|
1416
|
+
samp_weight = samp_weight / np.sum(samp_weight)
|
|
1417
|
+
|
|
1418
|
+
# Preallocate gradient norm recording (optional)
|
|
1419
|
+
rec_interaction_all = np.zeros((num_epoch, num_gene, num_program))
|
|
1420
|
+
rec_selfj_all = np.zeros((num_epoch, num_program))
|
|
1421
|
+
rec_selfh_all = np.zeros((num_epoch, num_program))
|
|
1422
|
+
|
|
1423
|
+
rec_grad = np.zeros(num_epoch)
|
|
1424
|
+
|
|
1425
|
+
m_interaction, v_interaction = np.zeros_like(cur_interaction), np.zeros_like(cur_interaction)
|
|
1426
|
+
m_selfj, v_selfj = np.zeros_like(cur_selfj), np.zeros_like(cur_selfj)
|
|
1427
|
+
m_selfh, v_selfh = np.zeros_like(cur_selfh), np.zeros_like(cur_selfh)
|
|
1428
|
+
|
|
1429
|
+
# Create l1 regularization schedule
|
|
1430
|
+
l1_base_start = 0.02
|
|
1431
|
+
l1_base_end = 1e-3
|
|
1432
|
+
|
|
1433
|
+
part1 = np.full((num_epoch // 4 + 1,), l1_base_start)
|
|
1434
|
+
part2 = np.logspace(np.log10(l1_base_start), np.log10(l1_base_end), num=num_epoch // 2)
|
|
1435
|
+
part3 = np.full((num_epoch // 4 + 1,), l1_base_end)
|
|
1436
|
+
l1_base_array = np.concatenate([part1, part2, part3])[:num_epoch]
|
|
1437
|
+
|
|
1438
|
+
# Preallocate arrays to collect per-sample gradients
|
|
1439
|
+
rec_interaction_grad = np.zeros((num_round, num_gene, num_program))
|
|
1440
|
+
rec_selfj_grad = np.zeros((num_round, num_program))
|
|
1441
|
+
rec_selfh_grad = np.zeros((num_round, num_program))
|
|
1442
|
+
|
|
1443
|
+
log_adam_grad = {name: deque(maxlen=backtrack_gap)
|
|
1444
|
+
for name in ["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"]}
|
|
1445
|
+
|
|
1446
|
+
tqdm._instances.clear()
|
|
1447
|
+
pbar = tqdm(total=num_epoch)
|
|
1448
|
+
counter = 1
|
|
1449
|
+
backtrack_counter = 0
|
|
1450
|
+
|
|
1451
|
+
while counter <= num_epoch:
|
|
1452
|
+
|
|
1453
|
+
l1_base = l1_base_array[counter - 1]
|
|
1454
|
+
|
|
1455
|
+
for kk in range(num_round):
|
|
1456
|
+
cur_state = gene_states[kk]
|
|
1457
|
+
cur_target = program_states[kk]
|
|
1458
|
+
|
|
1459
|
+
effective_h = cur_state.T @ cur_interaction + cur_selfh.T
|
|
1460
|
+
|
|
1461
|
+
# j_sub is the bias for regulators (broadcasted as row vector)
|
|
1462
|
+
j_sub = cur_selfj.T # shape: (n_target,)
|
|
1463
|
+
|
|
1464
|
+
# Compute terms.
|
|
1465
|
+
# We follow MATLAB: term1 = exp(j_sub + effective_h)' so that term1 becomes (n_target, T)
|
|
1466
|
+
term1 = np.exp(j_sub + effective_h).T
|
|
1467
|
+
term2 = np.exp(j_sub - effective_h).T
|
|
1468
|
+
|
|
1469
|
+
# Compute gradients for the bias parameters per sample
|
|
1470
|
+
j_sub_grad = cur_target**2 - (term1 + term2) / (term1 + term2 + 1)
|
|
1471
|
+
h_eff_grad = cur_target - (term1 - term2) / (term1 + term2 + 1)
|
|
1472
|
+
|
|
1473
|
+
# Gradient for interaction parameters
|
|
1474
|
+
# cur_state: (n_spin, T), h_eff_grad.T: (T, n_target)
|
|
1475
|
+
j_off_sub_grad = cur_state @ h_eff_grad.T # shape: (n_spin, n_target)
|
|
1476
|
+
|
|
1477
|
+
# Average over the T (state) dimension
|
|
1478
|
+
rec_interaction_grad[kk] = j_off_sub_grad / cur_state.shape[1]
|
|
1479
|
+
rec_selfj_grad[kk] = np.mean(j_sub_grad, axis=1)
|
|
1480
|
+
rec_selfh_grad[kk] = np.mean(h_eff_grad, axis=1)
|
|
1481
|
+
|
|
1482
|
+
# Aggregate gradients across samples using sample weights
|
|
1483
|
+
interaction_grad = - np.sum(rec_interaction_grad * samp_weight[:, None, None], axis=0)
|
|
1484
|
+
selfj_grad = - np.sum(rec_selfj_grad * samp_weight[:, None], axis=0)
|
|
1485
|
+
selfh_grad = - np.sum(rec_selfh_grad * samp_weight[:, None], axis=0)
|
|
1486
|
+
|
|
1487
|
+
if lambda_l1_j > 0:
|
|
1488
|
+
interaction_grad += lambda_l1_j * (cur_interaction / l1_base).clip(- 1, 1)
|
|
1489
|
+
if lambda_l2_j > 0:
|
|
1490
|
+
interaction_grad += lambda_l2_j * cur_interaction
|
|
1491
|
+
|
|
1492
|
+
# Update parameters using the provided update_adam function
|
|
1493
|
+
update_val, m_interaction, v_interaction = update_adam(interaction_grad, m_interaction, v_interaction, counter, stepsz)
|
|
1494
|
+
cur_interaction = cur_interaction - update_val
|
|
1495
|
+
|
|
1496
|
+
update_val, m_selfj, v_selfj = update_adam(selfj_grad, m_selfj, v_selfj, counter, stepsz)
|
|
1497
|
+
cur_selfj = cur_selfj - update_val
|
|
1498
|
+
|
|
1499
|
+
update_val, m_selfh, v_selfh = update_adam(selfh_grad, m_selfh, v_selfh, counter, stepsz)
|
|
1500
|
+
cur_selfh = cur_selfh - update_val
|
|
1501
|
+
|
|
1502
|
+
rec_interaction_all[counter - 1, :, :] = cur_interaction
|
|
1503
|
+
rec_selfj_all[counter - 1, :] = cur_selfj
|
|
1504
|
+
rec_selfh_all[counter - 1, :] = cur_selfh
|
|
1505
|
+
|
|
1506
|
+
for name, value in zip(["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"], [m_interaction, v_interaction, m_selfj, v_selfj, m_selfh, v_selfh]):
|
|
1507
|
+
log_adam_grad[name].append(value)
|
|
1508
|
+
|
|
1509
|
+
# Record gradient norm (optional)
|
|
1510
|
+
rec_grad[counter - 1] = np.linalg.norm(interaction_grad)
|
|
1511
|
+
|
|
1512
|
+
if counter in list_step:
|
|
1513
|
+
pbar.update(rec_gap)
|
|
1514
|
+
pbar.set_postfix({"Network Gradient": f"{rec_grad[counter - 1]:.6f}"})
|
|
1515
|
+
|
|
1516
|
+
if_backtrack = False
|
|
1517
|
+
if (counter > backtrack_gap) and (rec_grad[counter - 1] > 1.5 * rec_grad[counter - 1 - backtrack_gap]):
|
|
1518
|
+
if_backtrack = True
|
|
1519
|
+
|
|
1520
|
+
if (counter > 2 * backtrack_gap) and (counter % backtrack_gap == 0):
|
|
1521
|
+
est_cur_grad = rec_grad[counter - 1 - backtrack_gap: counter - 1].mean()
|
|
1522
|
+
est_prev_grad = rec_grad[counter - 1 - 2 * backtrack_gap: counter - 1 - backtrack_gap].mean()
|
|
1523
|
+
|
|
1524
|
+
if est_cur_grad > 1.05 * est_prev_grad:
|
|
1525
|
+
if_backtrack = True
|
|
1526
|
+
|
|
1527
|
+
if if_backtrack:
|
|
1528
|
+
print('Backtracking at epoch %d' % counter)
|
|
1529
|
+
backtrack_counter += 1
|
|
1530
|
+
m_interaction, v_interaction, m_selfj, v_selfj, m_selfh, v_selfh = [log_adam_grad[key][0] for key in ["m_interaction", "v_interaction", "m_selfj", "v_selfj", "m_selfh", "v_selfh"]]
|
|
1531
|
+
counter = counter - backtrack_gap
|
|
1532
|
+
stepsz = stepsz / 4
|
|
1533
|
+
if backtrack_counter > backtrack_tol:
|
|
1534
|
+
print(f'Backtracking more than {backtrack_tol} times, stop training.')
|
|
1535
|
+
break
|
|
1536
|
+
else:
|
|
1537
|
+
counter += 1
|
|
1538
|
+
|
|
1539
|
+
pbar.close()
|
|
1540
|
+
|
|
1541
|
+
# Retrieve parameters corresponding to the minimum gradient norm found during training
|
|
1542
|
+
trace_epoch = 50 + num_epoch - counter
|
|
1543
|
+
pos = num_epoch - trace_epoch + np.argmin(rec_grad[- trace_epoch: ])
|
|
1544
|
+
cur_interaction = rec_interaction_all[pos, :, :]
|
|
1545
|
+
cur_selfj = rec_selfj_all[pos, :]
|
|
1546
|
+
cur_selfh = rec_selfh_all[pos, :]
|
|
1547
|
+
|
|
1548
|
+
return cur_interaction, cur_selfj, cur_selfh
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def compute_relative_responses(cur_h: np.ndarray,
|
|
1552
|
+
if_control: np.ndarray,
|
|
1553
|
+
batch_index: np.ndarray) -> np.ndarray:
|
|
1554
|
+
"""
|
|
1555
|
+
Compute the relative responses of samples by subtracting the average of control samples.
|
|
1556
|
+
This function calculates the relative responses for a set of samples by comparing each sample's response
|
|
1557
|
+
to the average response of control samples. The control samples can be specific to the batch a sample belongs to,
|
|
1558
|
+
or a global set of control samples if no control samples are present in the batch.
|
|
1559
|
+
|
|
1560
|
+
Parameters
|
|
1561
|
+
----------
|
|
1562
|
+
cur_h : np.ndarray
|
|
1563
|
+
A 2D array where each column represents a sample and each row represents a feature.
|
|
1564
|
+
if_control : np.ndarray
|
|
1565
|
+
A boolean array indicating which samples are control samples.
|
|
1566
|
+
batch_index : np.ndarray
|
|
1567
|
+
An array indicating the batch assignment for each sample.
|
|
1568
|
+
|
|
1569
|
+
Returns
|
|
1570
|
+
-------
|
|
1571
|
+
np.ndarray
|
|
1572
|
+
A 2D array of the same shape as `cur_h`, containing the relative responses for each sample.
|
|
1573
|
+
"""
|
|
1574
|
+
unique_batches = np.unique(batch_index)
|
|
1575
|
+
num_batches = len(unique_batches)
|
|
1576
|
+
|
|
1577
|
+
relative_h = np.zeros_like(cur_h)
|
|
1578
|
+
|
|
1579
|
+
if np.all(if_control == 0):
|
|
1580
|
+
all_controls_mean = np.mean(cur_h, axis=1)
|
|
1581
|
+
else:
|
|
1582
|
+
all_controls_mean = np.mean(cur_h[:, if_control], axis=1)
|
|
1583
|
+
|
|
1584
|
+
for current_batch in unique_batches:
|
|
1585
|
+
batch_samples_idx = np.where(batch_index == current_batch)[0]
|
|
1586
|
+
|
|
1587
|
+
if np.any(if_control[batch_samples_idx]):
|
|
1588
|
+
batch_controls_idx = batch_samples_idx[np.where(if_control[batch_samples_idx])[0]]
|
|
1589
|
+
batch_controls_mean = np.mean(cur_h[:, batch_controls_idx], axis=1)
|
|
1590
|
+
else:
|
|
1591
|
+
batch_controls_mean = all_controls_mean
|
|
1592
|
+
|
|
1593
|
+
for idx in batch_samples_idx:
|
|
1594
|
+
relative_h[:, idx] = cur_h[:, idx] - batch_controls_mean
|
|
1595
|
+
|
|
1596
|
+
return relative_h
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
def average_batch_control(hgrad_raw: np.ndarray,
|
|
1600
|
+
if_control: np.ndarray,
|
|
1601
|
+
batch_index: np.ndarray) -> np.ndarray:
|
|
1602
|
+
"""
|
|
1603
|
+
Assign the gradient of control samples as the average gradient of the samples in the same batch.
|
|
1604
|
+
|
|
1605
|
+
Parameters
|
|
1606
|
+
----------
|
|
1607
|
+
hgrad : np.ndarray
|
|
1608
|
+
The gradient of the h matrix.
|
|
1609
|
+
if_control : np.ndarray
|
|
1610
|
+
A boolean array indicating which samples are control samples.
|
|
1611
|
+
batch_index : np.ndarray
|
|
1612
|
+
An array indicating the batch assignment for each sample.
|
|
1613
|
+
"""
|
|
1614
|
+
|
|
1615
|
+
hgrad = hgrad_raw.copy()
|
|
1616
|
+
|
|
1617
|
+
unique_batches = np.unique(batch_index)
|
|
1618
|
+
num_batches = len(unique_batches)
|
|
1619
|
+
|
|
1620
|
+
for current_batch in unique_batches:
|
|
1621
|
+
batch_samples_idx = np.where(batch_index == current_batch)[0]
|
|
1622
|
+
|
|
1623
|
+
if np.any(if_control[batch_samples_idx]):
|
|
1624
|
+
if np.any(if_control[batch_samples_idx]):
|
|
1625
|
+
batch_controls_idx = batch_samples_idx[np.where(if_control[batch_samples_idx])[0]]
|
|
1626
|
+
batch_mean = np.mean(hgrad_raw[:, batch_samples_idx], axis=1)
|
|
1627
|
+
|
|
1628
|
+
hgrad[:, batch_controls_idx] = batch_mean[:, np.newaxis]
|
|
1629
|
+
|
|
1630
|
+
return hgrad
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def select_representative_sample(raw_data: List[Tuple[np.ndarray, np.ndarray]],
|
|
1634
|
+
num_select: int) -> List[int]:
|
|
1635
|
+
"""
|
|
1636
|
+
Select representative samples by performing KMeans clustering on the samples'
|
|
1637
|
+
correlation and mean vectors and selecting the sample closest to the cluster center.
|
|
1638
|
+
|
|
1639
|
+
Parameters
|
|
1640
|
+
----------
|
|
1641
|
+
raw_data : List[Tuple[np.ndarray, np.ndarray]]
|
|
1642
|
+
A list of tuples, each containing the correlation and mean vectors for a sample.
|
|
1643
|
+
num_select : int
|
|
1644
|
+
The number of representative samples to select.
|
|
1645
|
+
|
|
1646
|
+
Returns
|
|
1647
|
+
-------
|
|
1648
|
+
List[int]
|
|
1649
|
+
A list of indices of the selected representative samples.
|
|
1650
|
+
"""
|
|
1651
|
+
num_spin = raw_data[0][0].shape[1]
|
|
1652
|
+
num_samp = len(raw_data)
|
|
1653
|
+
raw_data_cat_vec = np.zeros([num_samp, int(num_spin * (num_spin + 3) / 2)])
|
|
1654
|
+
ind1, ind2 = np.triu_indices(num_spin)
|
|
1655
|
+
|
|
1656
|
+
for ii in range(num_samp):
|
|
1657
|
+
cur_corrmean = raw_data[ii]
|
|
1658
|
+
triu_vect = cur_corrmean[0][ind1, ind2] / np.sqrt((num_spin + 1) / 2)
|
|
1659
|
+
triu_vect = np.concatenate([triu_vect, cur_corrmean[1].flatten()])
|
|
1660
|
+
raw_data_cat_vec[ii, :] = triu_vect
|
|
1661
|
+
|
|
1662
|
+
pca_all = PCA().fit(raw_data_cat_vec)
|
|
1663
|
+
pca_rep = pca_all.transform(raw_data_cat_vec)
|
|
1664
|
+
kmeans = KMeans(n_clusters=num_select, n_init=200)
|
|
1665
|
+
kmeans.fit(raw_data_cat_vec)
|
|
1666
|
+
|
|
1667
|
+
use_data_list = []
|
|
1668
|
+
for ii in range(num_select):
|
|
1669
|
+
cur_data = np.where(kmeans.labels_ == ii)[0]
|
|
1670
|
+
|
|
1671
|
+
cur_center = kmeans.cluster_centers_[ii, :]
|
|
1672
|
+
data_dist = np.linalg.norm(raw_data_cat_vec[cur_data, :] - cur_center, axis=1)
|
|
1673
|
+
data_select = cur_data[np.argmin(data_dist)]
|
|
1674
|
+
|
|
1675
|
+
use_data_list.append(data_select)
|
|
1676
|
+
|
|
1677
|
+
sc.set_figure_params(figsize=[1, 1])
|
|
1678
|
+
fig, grid = sc.pl._tools._panel_grid(0.2, 0.2, ncols=3, num_panels=9)
|
|
1679
|
+
|
|
1680
|
+
for ii in range(3):
|
|
1681
|
+
for jj in range(ii + 1):
|
|
1682
|
+
ax = plt.subplot(grid[ii * 3 + jj])
|
|
1683
|
+
plt.scatter(pca_rep[use_data_list, ii + 1], pca_rep[use_data_list, jj], s=90, color='#aaaaaa')
|
|
1684
|
+
plt.scatter(pca_rep[:, ii + 1], pca_rep[:, jj], s=20, c=kmeans.labels_, cmap='tab20', alpha=0.5)
|
|
1685
|
+
plt.xlabel('PC' + str(ii + 2))
|
|
1686
|
+
plt.ylabel('PC' + str(jj + 1))
|
|
1687
|
+
plt.xticks([])
|
|
1688
|
+
plt.yticks([])
|
|
1689
|
+
|
|
1690
|
+
return use_data_list
|