-
Notifications
You must be signed in to change notification settings - Fork 1
/
Alignment.py
executable file
·386 lines (250 loc) · 12.7 KB
/
Alignment.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
import numpy as np
from sklearn.cluster import KMeans
def get_centroids(X, nt):
"""
Get nt well-representative centroids from stacked feature matrix X
Parameters:
----------
X : 2-D concatenated feature array (Δ) with shape (D, p),
D = (Fold-1)/Fold * ((N_1*n_1) + ... + (N_m*n_m) + ... + (N_M*n_M))
nt : int, number of nodes in template graphs, number of nodes in resulting aligned graphs
or number of cluster centroids during K-Means clustering. It should be lower than
the size of the smallest graphs across different modalities.
Return:
-------
out : 2-D centroid array with shape (nt, p), each row is a cluster center
"""
clustering = KMeans(n_clusters=nt, n_init=10).fit(X)
L = clustering.labels_
Centroids = np.array([np.average(X[L == node_v], axis=0) for node_v in range(nt)])
return Centroids
def obtain_Corr_for_single_modality(partial_feats_m, Centroids, corr='hard'):
"""
Obtain correspondence matrices for graphs of a particular modality
Parameters:
----------
partial_feats_m : For DB-based alignment, 3-D DB array with shape (N_m, n_m, p),
only the first p columns of original "DB_m" are used. For GW-based alignment,
3-D GW array with shape (N_m, n_m, P), no partial use of features (i.e, p=P).
All columns of "GW_m" are used. Note that the term "partial" is related only to DB-based alignment.
Centroids : 2-D array with shape (nt, P), nt cluster centroids calculated from feature vectors
corr : 'hard' (default) or 'soft',
correspondence strategy used in feature extraction phase. If 'hard', correspondence of a node
in a graph is entirely reserved to the closest cluster centroid that is generated by KMeans clustering
(correspondence: 1) and the correspondences to the other centroids are set to 0. If 'soft',
unlike hard correspondence, correspondence of a node is distributed between cluster centroids based on
their euclidean distances to the node of interest such that all correspondences add up to 1.
In this case, correspondences are inversely proportional to pairwise distances.
Return:
-------
out : 3-D correspondence matrix with shape (N_m, n_m, nt) for modality-m
"""
N_m, n_m, p = partial_feats_m.shape
nt = len(Centroids)
Corr_m_shape = (N_m, n_m, nt)
Dist_m = np.zeros(Corr_m_shape, dtype=float)
# Calculating distance matrix first
# for each graph in modality-m
for i in range(N_m):
# for each node in "Graph_i"
for v in range(n_m):
# delta : each p-dimensional feature in "feat_i"
delta = partial_feats_m[i,v]
# for each template node (i.e, centroid) in T_p
for vt in range(nt):
centr = Centroids[vt]
dist = np.linalg.norm(delta-centr)
Dist_m[i,v,vt] = dist
# Hard Correspondence
if corr=='hard':
# print('Hard correspondence selected')
Corr_m = np.zeros(Corr_m_shape, dtype=float) # "int" casted to "float"
I = np.concatenate((np.array(list(np.ndindex(Corr_m_shape[:-1]))), Dist_m.argmin(axis=-1).reshape(-1,1)), axis=-1)
Corr_m[I[:,0], I[:,1], I[:,2]] = 1
# Soft Correspondence
elif corr=='soft':
# print('Soft correspondence selected')
Sim = np.exp(-Dist_m/p) # similarity
Corr_m = Sim / Sim.sum(axis=-1, keepdims=True) # normalized similarity, elements in each row should add up to 1
else:
raise ValueError('Invalid correspondence type (corr), choose "hard" or "soft"\n')
return Corr_m
def obtain_all_Corr(All_Features, Centroids, corr='hard'):
"""
Obtain all correspondence matrices for M modalities
Parameters:
----------
All_Features : list of M sets of feature matrices, each with shape (N_m, n_m, P)
Centroids : 2-D array with shape (nt, P), nt cluster centroids calculated from feature vectors
corr : 'hard' (default) or 'soft',
correspondence strategy used in feature extraction phase. If 'hard', correspondence of a node
in a graph is entirely reserved to the closest cluster centroid that is generated by KMeans clustering
(correspondence: 1) and the correspondences to the other centroids are set to 0. If 'soft',
unlike hard correspondence, correspondence of a node is distributed between cluster centroids based on
their euclidean distances to the node of interest such that all correspondences add up to 1.
In this case, correspondences are inversely proportional to pairwise distances.
Return:
-------
out : list of M 3-D correspondence matrix arrays for all modalities, each with shape (N_m, n_m, nt)
"""
M = len(All_Features)
nt, p = Centroids.shape
All_Corr = [obtain_Corr_for_single_modality(Features_m[:,:,:p], Centroids, corr) for Features_m in All_Features]
return All_Corr
def get_aligned_graphs(All_Graphs, All_Corr):
"""
Align multimodal connectomes using correspondence matrices of original graphs
Parameters:
----------
All_Graphs : list of M sets of graphs from M distinct modalities, each with shape (N_m, n_m, n_m)
All_Corr : list of M sets of correspondence matrices for all M modalities, each with shape (N_m, n_m, nt)
Return:
-------
out : list of M sets of aligned graphs, each with shape (N_m, nt, nt)
"""
All_Aligned_Graphs = []
for Graphs_m, Corr_m in zip(All_Graphs, All_Corr):
n_m, nt = Corr_m.shape[-2:]
reduction_ratio = (nt/n_m)**2
All_Aligned_Graphs.append( np.array([corr_i.T @ graph_i @ corr_i for graph_i, corr_i in zip(Graphs_m, Corr_m)]) * reduction_ratio )
return All_Aligned_Graphs
def average_aligned_graphs_across_p(List_p, M, P):
"""
Average a set of aligned graphs that are created with respect to
different p values. p is in range [1-P]
Parameters:
----------
List_p : list of list of all aligned modalities to be averaged
M : int, total number of modalities
P : int, default=3
maximum depth level at which graphs are almost completely covered during subgraph expansions.
Return:
-------
out : list of all averaged aligned modalities across p
"""
L = []
for m in range(M):
L.append(np.average([List_p[p][m] for p in range(P)], axis=0))
return L
def alignment_to_template(All_Graphs, All_Features, Tr_Ind, nt, feat_method='DB', corr='hard', seed=100):
"""
Get aligned multimodal connectomes via shared template
Parameters:
----------
All_Graphs : list of M sets of graphs from M distinct modalities, each with shape (N_m, n_m, n_m)
All_Features : list of M sets of feature matrices, each with shape (N_m, n_m, P)
Tr_Ind : list of M 1-D index arrays,
the m-th index array holds the indices of the training subjects in
All_Graphs[m] or All_Features[m] during particular fold
nt : int,
number of nodes in template graphs, number of nodes in resulting aligned graphs,
or number of cluster centroids during K-Means clustering. It should be lower than
the size of the smallest graphs across different modalities.
feat_method : 'DB' (default) or 'GW',
Type of extracted connectomic features.
If 'DB', depth-based vector representations are calculated from graphs using "subgraph expansions".
If 'GW', structural GraphWave node embeddings are extracted using "heat diffusion wavelets".
corr : 'hard' (default) or 'soft',
correspondence strategy used in feature extraction phase. If 'hard', correspondence of a node
in a graph is entirely reserved to the closest cluster centroid that is generated by KMeans clustering
(correspondence: 1) and the correspondences to the other centroids are set to 0. If 'soft',
unlike hard correspondence, correspondence of a node is distributed between cluster centroids based on
their euclidean distances to the node of interest such that all correspondences add up to 1.
In this case, correspondences are inversely proportional to pairwise distances.
seed : int (default: 100),
seed value to anchor randomization for multiple runs
Return:
-------
out : list of M sets of aligned graphs, each with shape (N_m, nt, nt)
"""
if len(All_Graphs) == len(All_Features):
M = len(All_Graphs)
P = All_Features[0].shape[-1]
else:
raise ValueError('Number of Modalities is different in input parameters (All_Graphs, All_Features, All_Labels)')
# Delta : tr_feats (all features concatenated vertically)
Delta = np.concatenate([Features_m[ind].reshape(-1,P) for Features_m, ind in zip(All_Features, Tr_Ind)], axis=0)
if feat_method == 'GW':
GW_Centroids = get_centroids(X=Delta, nt=nt)
All_Corr = obtain_all_Corr(All_Features, GW_Centroids, corr)
All_Aligned_Graphs = get_aligned_graphs(All_Graphs, All_Corr)
elif feat_method == 'DB':
List_p = []
for p in range(1, P+1):
DB_Centroids = get_centroids(X=Delta[:,:p], nt=nt)
All_Corr = obtain_all_Corr(All_Features, DB_Centroids, corr)
Aligned_Graphs_p = get_aligned_graphs(All_Graphs, All_Corr)
### <---IF NODE REARRANGEMENT IS NEEDED AFTER ALIGNMENT, HERE---> ###
List_p.append(Aligned_Graphs_p)
All_Aligned_Graphs = average_aligned_graphs_across_p(List_p, M, P)
else:
raise ValueError('Invalid "feat_method", it can be "GW" or "DB"\n')
return All_Aligned_Graphs
def Align_Multimodal_Connectomes(All_Graphs, All_Features, All_Labels, nt, feat_method='DB', corr='hard', Fold=5, seed=100):
"""
Align multimodal connectomes using DB or GW-based graph alignment to get fixed-sized connectomes
Parameters:
----------
All_Graphs : list of M sets of graphs from M distinct modalities, each with shape (N_m, n_m, n_m)
All_Features : list of M sets of feature matrices, each with shape (N_m, n_m, P)
All_Labels : list of M label arrays, each with length N_m
nt : int,
number of nodes in template graphs, number of nodes in resulting aligned graphs,
or number of cluster centroids during K-Means clustering. It should be lower than
the size of the smallest graphs across different modalities.
feat_method : 'DB' (default) or 'GW',
Type of extracted connectomic features.
If 'DB', depth-based vector representations are calculated from graphs using "subgraph expansions".
If 'GW', structural GraphWave node embeddings are extracted using "heat diffusion wavelets".
corr : 'hard' (default) or 'soft',
correspondence strategy used in feature extraction phase. If 'hard', correspondence of a node
in a graph is entirely reserved to the closest cluster centroid that is generated by KMeans clustering
(correspondence: 1) and the correspondences to the other centroids are set to 0. If 'soft',
unlike hard correspondence, correspondence of a node is distributed between cluster centroids based on
their euclidean distances to the node of interest such that all correspondences add up to 1.
In this case, correspondences are inversely proportional to pairwise distances.
Fold : int (default: 5),
number of fold to use in Cross-Validation
seed : int (default: 100),
seed value to anchor randomization for multiple runs
Return:
-------
out : list of M sets of aligned graphs, training indices and testing indices
"""
if len(All_Graphs) == len(All_Features) == len(All_Labels):
M = len(All_Graphs)
P = All_Features[0].shape[-1]
else:
raise ValueError('Number of Modalities is different in input parameters (All_Graphs, All_Features, All_Labels)')
# N_list : (M,)
# Indices : (M, ...)
# Train_Indices : (M, Fold, ...)
# Test_Indices : (M, Fold, ...)
N_list, n_list, Indices, Train_Indices, Test_Indices = [], [], [], [], []
# iterating over M modalities for index operations (train/test)
for m in range(M):
if not (len(All_Graphs[m]) == len(All_Features[m]) == len(All_Labels[m])):
raise ValueError(f'Sample sizes in modality-{m+1} are different in input parameters.')
N_m, n_m, _ = All_Features[m].shape
N_list.append(N_m)
n_list.append(n_m)
indices = np.arange(N_m)
np.random.seed(seed)
np.random.shuffle(indices)
Indices.append(indices)
test_indices = np.array_split(indices, Fold)
Test_Indices.append(test_indices)
train_indices = [indices[np.in1d(indices, ind, invert=True)] for ind in test_indices]
Train_Indices.append(train_indices)
if nt >= min(n_list):
raise ValueError("nt value should be smaller than the minimum connectomic resolution (n_m) of all modalities")
print("")
for f in range(Fold):
print(f"{feat_method} | nt={nt} | {corr} correspondence | Fold-{f+1}")
# Tr_Ind : (M, ...)
# Tst_Ind : (M, ...)
Tr_Ind = [Train_Indices[m][f] for m in range(M)]
Tst_Ind = [Test_Indices[m][f] for m in range(M)]
# All_Aligned_Graphs : [(N_1, nt, nt), (N_2, nt, nt), ..., (N_M, nt, nt)]
All_Aligned_Graphs = alignment_to_template(All_Graphs, All_Features, Tr_Ind, nt, feat_method, corr=corr, seed=seed)
yield All_Aligned_Graphs, Tr_Ind, Tst_Ind