phyloK 1.0.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.
phyloK/LRMF.py ADDED
@@ -0,0 +1,171 @@
1
+ import numpy as np
2
+
3
+ def check_convergence(x_old, x_new, threshold=1e-6):
4
+ max_updt = np.linalg.norm(x_new - x_old, ord = np.inf)
5
+ w_max = np.linalg.norm(x_new, ord = np.inf)
6
+ if w_max == 0.0 or max_updt/w_max <= threshold:
7
+ return True
8
+
9
+ return False
10
+
11
+ def get_Options(X, nan_value = 0):
12
+ if np.isnan(nan_value):
13
+ Oi,Oj = np.where(~np.isnan(X))
14
+ else:
15
+ Oi,Oj = np.where(X != nan_value)
16
+
17
+ return np.c_[Oi, Oj]
18
+
19
+ def init_AB(k, shape, rng = np.random.default_rng(12038)):
20
+ a,b = shape
21
+
22
+ A = rng.normal(size = (a,k))
23
+ B = rng.normal(size = (k,b))
24
+ return A, B
25
+
26
+ def ALS_legacy(sample_train, sample_test,
27
+ k = 6, lam1 = 0.1, lam2 = 0.1,
28
+ get_errors = True, epsilon = 1e-6,
29
+ max_epochs = 1000,
30
+ rng = np.random.default_rng(12038)):
31
+
32
+ Oij_test = get_Options(sample_test)
33
+ Oij = get_Options(sample_train)
34
+
35
+ M_test = sample_test[Oij_test[:,0], Oij_test[:,1]]
36
+
37
+ Ik = np.eye(k)
38
+ A,B = init_AB(k, sample_train.shape, rng = rng)
39
+
40
+ all_errors = []
41
+ for _ in range(max_epochs):
42
+ Ac = np.copy(A)
43
+ Bc = np.copy(B)
44
+ # row and columns
45
+ # update
46
+ for i,j in Oij:
47
+ # i,j
48
+ Ok_ = Oij[ Oij[:,0] == i ]
49
+ Mkj = sample_train[ Ok_[:,0], Ok_[:,1] ]
50
+ Bj = B[:, Ok_[:,1] ]
51
+
52
+ # matrix form of
53
+ # update rule of A[i:]
54
+ A[i,:] = np.linalg.solve(Bj @ Bj.T + lam1*Ik, Bj @ Mkj.T).T
55
+
56
+ O_k = Oij[ Oij[:,1] == j ]
57
+ Mik = sample_train[ O_k[:,0], O_k[:,1] ]
58
+ Ai = A[ O_k[:,0],:]
59
+
60
+ # matrix form of
61
+ # update rule of B[:k]
62
+ B[:,j] = np.linalg.solve( Ai.T @ Ai + lam2*Ik, Ai.T @ Mik )
63
+
64
+ if get_errors:
65
+ # matrix form of
66
+ # error evaluation
67
+ AiBj = (A @ B)[Oij_test[:,0], Oij_test[:,1]]
68
+ # rmse's
69
+ myrmse = np.sqrt( np.mean( (M_test - AiBj)**2 ) )
70
+ # print("current RMSE: ", myrmse)
71
+ all_errors.append( myrmse)
72
+
73
+ if check_convergence(Ac, A, epsilon) and\
74
+ check_convergence(Bc, B, epsilon):
75
+ break
76
+
77
+ AiBj = (A @ B)[ Oij_test[:,0], Oij_test[:,1] ]
78
+ final_error = np.sqrt( np.mean( (M_test - AiBj)**2 ) )
79
+
80
+ return A, B, all_errors, final_error
81
+
82
+
83
+ def ALS(M_train,
84
+ k = 6, lam1 = 0.1, lam2 = 0.1,
85
+ epsilon = 1e-6,
86
+ max_epochs = 1000,
87
+ rng = np.random.default_rng(12038),
88
+ nan_value = 0):
89
+
90
+
91
+ Oij = get_Options(M_train, nan_value = nan_value)
92
+ Ik = np.eye(k)
93
+ A,B = init_AB(k, M_train.shape, rng = rng)
94
+
95
+ for _ in range(max_epochs):
96
+ Ac = np.copy(A)
97
+ Bc = np.copy(B)
98
+ # row and columns
99
+ # update
100
+ for i,j in Oij:
101
+ # i,j
102
+ Ok_ = Oij[ Oij[:,0] == i ]
103
+ Mkj = M_train[ Ok_[:,0], Ok_[:,1] ]
104
+ Bj = B[:, Ok_[:,1] ]
105
+
106
+ # matrix form of
107
+ # update rule of A[i:]
108
+ A[i,:] = np.linalg.solve(Bj @ Bj.T + lam1*Ik, Bj @ Mkj.T).T
109
+
110
+ O_k = Oij[ Oij[:,1] == j ]
111
+ Mik = M_train[ O_k[:,0], O_k[:,1] ]
112
+ Ai = A[ O_k[:,0],:]
113
+
114
+ # matrix form of
115
+ # update rule of B[:k]
116
+ B[:,j] = np.linalg.solve( Ai.T @ Ai + lam2*Ik, Ai.T @ Mik )
117
+
118
+ if check_convergence(Ac, A, epsilon) and\
119
+ check_convergence(Bc, B, epsilon):
120
+ break
121
+
122
+ return A, B
123
+
124
+ def score(A, B, M_test, nan_value = 0):
125
+ Oij_test = get_Options(M_test, nan_value = nan_value)
126
+ M_test = M_test[Oij_test[:,0], Oij_test[:,1]]
127
+ AiBj = (A @ B)[Oij_test[:,0], Oij_test[:,1]]
128
+ final_error = np.sqrt( np.mean( (M_test - AiBj)**2 ) )
129
+ return final_error
130
+
131
+
132
+ if __name__ == "__main__":
133
+
134
+ train_file = "../data/rate_train.csv"
135
+ test_file = "../data/rate_test.csv"
136
+
137
+ rng = np.random.default_rng(12038)
138
+
139
+ sample_train = np.loadtxt(train_file, delimiter=',')
140
+ sample_test = np.loadtxt(test_file , delimiter=',')
141
+
142
+ lam1 = 4
143
+ lam2 = 4
144
+ k = 5
145
+
146
+ (A,B) = ALS(sample_train,
147
+ k = k, lam1 = lam1, lam2 = lam2,
148
+ max_epochs = 100, epsilon = 1e-3,
149
+ rng = rng)
150
+
151
+ final_error = score(A, B, sample_test)
152
+ print("Final RMSE: ", final_error) # Final RMSE: 0.8042507213383732
153
+
154
+
155
+
156
+ # (A,B,_,_) = ALS_legacy(sample_train, sample_test,
157
+ # k = k, lam1 = lam1, lam2 = lam2,
158
+ # max_epochs = 100, epsilon = 1e-3,
159
+ # get_errors = False,rng = rng,)
160
+
161
+
162
+ # import matplotlib.pyplot as plt
163
+ # plt.plot(all_errors)
164
+ # plt.xlabel('Epoch')
165
+ # plt.ylabel('RMSE')
166
+ # plt.title('Convergence of ALS')
167
+ # plt.show()
168
+
169
+
170
+ # with open("./completed_matrix.csv", "w") as f:
171
+ # np.savetxt(f, A @ B, delimiter=",", fmt='%1.4f')
phyloK/RBF.py ADDED
@@ -0,0 +1,203 @@
1
+ import numpy as np
2
+
3
+
4
+ def distance_matrix(a, b, M=None):
5
+ """
6
+ l2 norm squared matrix
7
+
8
+ Time complexity:
9
+ If M is full matrix, then O(np^2 + n^2p). Assuming p < n, then O(n^2p).
10
+ If M is a diagonal or vector, then O(n^2p) regardless of the relationship between p and n.
11
+ If M is None, then O(n^2p) regardless of the relationship between p and n.
12
+
13
+ a: shape (m, p)
14
+ b: shape (n, p)
15
+ M: shape (p, p) or None
16
+
17
+ returns: shape (m, n)
18
+ """
19
+ # a = X_test[:2, :]
20
+ # b = X_train[:3, :]
21
+ # # M = np.array([[2, 0],
22
+ # # [0, 1]])
23
+ # M = np.diag(M)
24
+
25
+ # a = X_train
26
+ # b = X_test
27
+ # M = np.eye(a.shape[1])
28
+ if M is None:
29
+ # O(pn^2) for the whole matrix
30
+ return np.linalg.norm(a[:, None, :] - b[None, :, :], axis=-1)**2
31
+
32
+ else:
33
+ # total time:
34
+ # O(np + np + mnp + mn) = O(mnp) = O(n^2p) if m = n
35
+ if len(M.shape) == 1:
36
+ # O(np)
37
+ A = ((a[ :, None, :] * M) @ a[:, :, None]).reshape(a.shape[0])
38
+ # O(mp)
39
+ B = ((b[ :, None, :] * M) @ b[:, :, None]).reshape(b.shape[0])
40
+ # O(mpn + pn) = O(mnp)
41
+ C = (a * M) @ b.T
42
+ # O(mn)
43
+ return np.add.outer(A,B) - 2*C
44
+
45
+
46
+ else:
47
+ # total time:
48
+ # O(mn + mp^2 + np^2 + mpn + p^2n)
49
+ # = O((m + n)p^2 + mpn)
50
+ # if m = n:
51
+ # O(np^2 + n^2p) = O(npp + nnp) <= O(n^2p) if p < n
52
+
53
+ # O(np^2)
54
+ A = (a[ :, None, :] @ M @ a[:, :, None]).reshape(a.shape[0])
55
+ # O(mp^2)
56
+ B = (b[ :, None, :] @ M @ b[:, :, None]).reshape(b.shape[0])
57
+ # O(mpn + p^2n)
58
+ C = a @ M @ b.T
59
+ # O(mn)
60
+ return np.add.outer(A,B) - 2*C
61
+
62
+
63
+ def nan_sqdist(a, b, use_t=False, T=None, k = 3):
64
+ r"""
65
+ Assuming a and b have 0 where it is missing.
66
+
67
+ a**2 @ Mbf.T = [ a_1^2 [ | |
68
+ ... 0/1 ... 0/1
69
+ a_m^2 ] | | ]
70
+ b1 bn <- presence/absence in b
71
+ = [ || a^{1}_1 ||^2 ... || a^{n}_1 ||^2
72
+
73
+ || a^{1}_m ||^2 ... || a^{n}_m ||^2 ]
74
+ In the sum, this activates l2 norm for overlapped features.
75
+ The same applies to (b**2 @ Maf.T)
76
+
77
+ a @ b.T = [ a_1^T b_1 ... a_1^T b_n
78
+ ... ... ...
79
+ a_m^T b_1 ... a_m^T b_n ]
80
+
81
+ thus a**2 @ Mbf.T + (b**2 @ Maf.T).T - 2 * (a @ b.T):
82
+
83
+ = [ ||a^{1}_1||^2 -2a_1^T b_1 +||b^{1}_1||^2 ... ||a^{n}_1||^2 -2a_1^T b_n +||b^{1}_n||^2
84
+ ...
85
+ ||a^{1}_m||^2 -2a_m^T b_1 +||b^{m}_1||^2 ... ||a^{n}_m||^2 -2a_m^T b_n +||b^{m}_n||^2 ]
86
+
87
+
88
+ O(n^2p) time complexity.
89
+ """
90
+ p = a.shape[1]
91
+
92
+ Ma = ~np.isnan(a) # observed masks, shape (m, p), O(mp)
93
+ Mb = ~np.isnan(b) # shape (n, p), O(np)
94
+ a0 = np.where(Ma, a, 0.0) # shape (m, p) zero-filled data, O(mp)
95
+ b0 = np.where(Mb, b, 0.0) # shape (n, p) zero-filled data, O(np)
96
+
97
+ Maf = Ma.astype(a.dtype)
98
+ Mbf = Mb.astype(b.dtype)
99
+
100
+ n = Maf @ Mbf.T # overlap counts, (m, n), O(mpn)
101
+ # R^{m,p} R^{p,n} + (R^{n,p} R^{p,m}).T - 2 R^{m,p} R^{p,n} = R^{m,n}
102
+ # O(mpn + mpn + mnp) = O(mnp) = O(n^2p) if m = n
103
+ S = (a0**2) @ Mbf.T + ((b0**2) @ Maf.T).T - 2 * (a0 @ b0.T)
104
+
105
+ with np.errstate(divide="ignore", invalid="ignore"):
106
+ D = S * (p / n) # n == 0 -> inf or nan
107
+
108
+ D[n == 0] = np.nan
109
+
110
+ if sum(n == 0) > 0:
111
+ k_linear_interpolation(D, k=k, T=T, use_t=use_t)
112
+
113
+ return D
114
+
115
+ def k_linear_interpolation(D, k = 3, T = None, use_t = False):
116
+ r"""
117
+ D: distance matrix with NaN values, shape (m, n)
118
+ k: number of nearest neighbors to use for interpolation
119
+ T: distance matrix with NaN values, shape (m, n)
120
+ use_t: if True, use T for finding nearest neighbors instead of D
121
+
122
+
123
+ O(n^2) algorithm
124
+ The algorithm iterates over each row of the distance matrix D,
125
+ and for each row, it finds the k nearest neighbors that have non-NaN values.
126
+ It then replaces the NaN values in that row with the mean of the k
127
+ nearest neighbors' distances.
128
+ """
129
+ # k = 2
130
+ assert k > 0, "k must be a positive integer"
131
+
132
+ if use_t:
133
+ assert T is not None, "T must be provided when use_t is True"
134
+
135
+ m,n = D.shape
136
+ # O(n^2) for the whole matrix
137
+ for i in range(m):
138
+ # i = 0
139
+
140
+ full_idx = ~np.isnan(D[i,:]) # O(n)
141
+ if sum(full_idx) == n:
142
+ continue
143
+
144
+ # available neighbors
145
+ full_idx[i] = False
146
+
147
+ if sum(full_idx) == 0:
148
+ # this means that there exist a single x_ik value at the row i
149
+ # and there there no more values at column k.
150
+ raise ValueError("All neighbors are NaN for row {}".format(i))
151
+
152
+ if sum(full_idx) <= k:
153
+ k_neighs_idx = full_idx
154
+ else:
155
+ # Get the k nearest neighbors, O(n)
156
+ if use_t:
157
+ k_neighs_idx = np.argpartition(T[i, full_idx], k)[:k]
158
+
159
+ else:
160
+ k_neighs_idx = np.argpartition(D[i, full_idx], k)[:k]
161
+
162
+ # index with full values
163
+ full_idx[i] = True
164
+
165
+ D[i, ~full_idx] = np.mean(D[i, k_neighs_idx])
166
+
167
+ def RBF_kernel(a, b, gamma, M = None, nan_adjust = False, k = 3, T = None, use_t = False):
168
+ """
169
+ Radial Basis Function
170
+
171
+ Time complexity:
172
+ If M is full matrix, then O(np^2 + n^2p). Assuming p < n, then O(n^2p).
173
+ If M is a diagonal or vector, then O(n^2p) regardless of the relationship between p and n.
174
+ If M is None, then O(n^2p) regardless of the relationship between p and n.
175
+
176
+
177
+ a: shape (m, p)\\
178
+ b: shape (n, p)\\
179
+ M: shape (p, p) or None\\
180
+ nan_adjust: if True, then use nan_sqdist to compute the distance matrix, otherwise use distance_matrix\\
181
+ k: number of nearest neighbors to use for interpolation if nan_adjust is True\\
182
+ T: distance matrix with NaN values, shape (m, n)\\
183
+ use_t: if True, use T for finding nearest neighbors instead of D\\
184
+
185
+
186
+ returns: shape (m, n)
187
+ """
188
+ a = np.ascontiguousarray(a, dtype=np.float64)
189
+ b = np.ascontiguousarray(b, dtype=np.float64)
190
+ # data sim
191
+ if nan_adjust:
192
+ # O(n^2p) for the whole matrix and never uses M.
193
+ D = nan_sqdist(a, b, use_t=use_t, T=T, k=k)
194
+
195
+ else:
196
+ D = distance_matrix(a, b, M=M)
197
+ # print("D.shape", D.shape)
198
+ # print("D", D)
199
+ D = np.where(D < 1e-10, 0.0, D) # numerical stability
200
+ tmp_rbf = -gamma * D
201
+
202
+ np.exp(tmp_rbf, tmp_rbf) # RBF kernel. Inplace exponentiation
203
+ return tmp_rbf
phyloK/__init__.py ADDED
File without changes
phyloK/agop.py ADDED
@@ -0,0 +1,240 @@
1
+ import numpy as np
2
+
3
+
4
+ def nabla_kw(l, K, X, M, gamma, w, diag = False):
5
+ r"""
6
+ Derivative of the kernel matrix with respect to the input data X.
7
+ \partial k_{\ell, j} w_j / \partial x_{\ell}
8
+
9
+ total time complexity:
10
+ O(pnm + mp^2) = O( mp(n + p) ) if diag = False
11
+ O(pnm) O\W
12
+ """
13
+
14
+ # w = W * Du[l,:]
15
+
16
+ Dl = (X[l,:] - X).T #O(pn), R^{p,n}
17
+ Dl_Kl = Dl * K[l,:] #O(pn), R^{p,n}
18
+ Dl_kl_w = Dl_Kl @ w #O(pnm), R^{p,n} x R^{n,m} = R^{p,m}
19
+
20
+ if M is None:
21
+ return -2. * gamma * Dl_kl_w # O(pm), R^{p,m}
22
+
23
+ else:
24
+ if not diag:
25
+ return -2. * gamma * M @ Dl_kl_w # O(mp^2), R^{p,m}
26
+ else:
27
+ assert len(M.shape) == 1, "M should be a vector when diag is True"
28
+ b = -2. * gamma * M[:,None] * Dl_kl_w # O(pm), R^{p,m}
29
+ # b = -2. * gamma * M * Dl_kl_w # O(pm), R^{p,m} # needed it for obj agop
30
+ # print("b ", b)
31
+ return b
32
+
33
+ def agop(Du, mu, Vu, y, W, lam, X, K, M, gamma, diag = False):
34
+ """
35
+ Outer product gradient for recursive feature machines
36
+
37
+ Algorithm exact time complexity:
38
+ m be the number of outputs,
39
+ n be the number of samples, and
40
+ p be the number of features.
41
+
42
+ If diag = False:
43
+ O(nmp^2 + nmp(n + p)) = O(nmp(n + p)). Assuming p < n, then O(mpn^2).
44
+
45
+
46
+ If diag = True:
47
+ O(n^2mp). regardless of the relationship between p and n.
48
+
49
+ Du: Derivative of the estimator, shape (n, m)
50
+ mu: Estimator, shape (n, m)
51
+ Vu: Variance of the estimator, shape (n, m)
52
+ y: observed labels, shape (n, m)
53
+ W: weights, shape (n, m)
54
+ lam: regularization parameter
55
+ X: input data, shape (n, p)
56
+ K: kernel matrix, shape (n, n)
57
+ M: prev agop matrix, shape (p, p) or shape (p,) if diag = True or None if no previous agop matrix is provided
58
+ gamma: rbf kernel parameter
59
+ diag: if True, then M is a vector and the agop matrix is computed as
60
+ M_new = np.sum(nabla_f_l**2, axis=0) / n
61
+ otherwise, M is a matrix and the agop matrix is computed as
62
+ M_new = np.sum(np.outer(nabla_f_l, nabla_f_l), axis=0) / n
63
+
64
+
65
+ returns: M_new, shape (p, p)
66
+
67
+ reference:
68
+ Supplemntary material of the paper "Mechanism for feature learning in neural networks and backpropagation-free machine learning models"
69
+ """
70
+
71
+ # mu = lkrr.mu(self.f_curr)
72
+ # Vu = lkrr.Vu_func(self.f_curr)
73
+
74
+ # lam = lkrr.lam
75
+ # M = lkrr.M
76
+ # gamma = lkrr.gamma
77
+
78
+ # K = lkrr.K_train
79
+ # X = X_train
80
+ # W = lkrr.alpha[1:]
81
+ # Du = lkrr.Du
82
+ # y = lkrr.y
83
+ # diag = True
84
+
85
+ mn = y.shape
86
+ d1_array = False
87
+ if len(mn) == 1:
88
+ n,m = (mn[0], 1)
89
+ d1_array = True
90
+
91
+ else:
92
+ n,m = mn
93
+
94
+ p = X.shape[1]
95
+
96
+ if d1_array:
97
+ W = W[ :, np.newaxis]
98
+ Du = Du[:, np.newaxis]
99
+
100
+
101
+ G = np.zeros((p,m,n))
102
+ # O( nmp(n + p) ) or O( n^2mp ) if diag
103
+ for l in range(n):
104
+ # l = 0
105
+ # O( mp(n + p) ) or O( mnp ) if diag
106
+ G[:,:,l] = nabla_kw(l, K, X, M, gamma, W * Du[l,:], diag=diag)
107
+
108
+
109
+ G -= G.mean(axis = 2, keepdims = True)
110
+
111
+ if not diag:
112
+ M_new = np.zeros((p,p), dtype=np.float32)
113
+ # O(nmp^2)
114
+ for l in range(n):
115
+ # O(pm)
116
+ nabla_f_l = G[:,:,l] # R^{p,m}
117
+ # O(mp^2)
118
+ # M_new += np.outer(nabla_f_l, nabla_f_l)
119
+ M_new += nabla_f_l @ nabla_f_l.T
120
+ else:
121
+ M_new = np.zeros(p, dtype=np.float32)
122
+ # O(nmp)
123
+ for l in range(n):
124
+ # O(mp)
125
+ nabla_f_l = G[:,:,l]
126
+ # O(mp)
127
+ M_new += np.sum(nabla_f_l**2, axis = 1)
128
+
129
+ return M_new / n
130
+
131
+
132
+ def obj_agop(Du, mu, Vu, y, W, lam, X, K, M, gamma, diag = False):
133
+ """
134
+ Average gradient outer product from the objective function
135
+
136
+ Algorithm exact time complexity:
137
+ m be the number of outputs,
138
+ n be the number of samples, and
139
+ p be the number of features.
140
+
141
+ If diag = False:
142
+ O(nmp^2 + pmn^2 + np^2) = O(mnp^2 + mpn^2) = O(mnp(p + n)). Assuming p < n, then O(mpn^2).
143
+
144
+
145
+ If diag = True:
146
+ O(nm + mpn^2 + np) = O(mpn^2) regardless of the relationship between p and n.
147
+
148
+ Du: Derivative of the estimator, shape (n, m)
149
+ mu: Estimator, shape (n, m)
150
+ Vu: Variance of the estimator, shape (n, m)
151
+ y: observed labels, shape (n, m)
152
+ W: weights, shape (n, m)
153
+ lam: regularization parameter
154
+ X: input data, shape (n, p)
155
+ K: kernel matrix, shape (n, n)
156
+ M: prev agop matrix, shape (p, p) or shape (p,) if diag = True or None if no previous agop matrix is provided
157
+ gamma: rbf kernel parameter
158
+ diag: if True, then M is a vector and the agop matrix is computed as
159
+ M_new = np.sum(nabla_f_l**2, axis=0) / n
160
+ otherwise, M is a matrix and the agop matrix is computed as
161
+ M_new = np.sum(np.outer(nabla_f_l, nabla_f_l), axis=0) / n
162
+
163
+
164
+ returns: M_new, shape (p, p)
165
+
166
+ reference:
167
+ Supplemntary material of the paper "Mechanism for feature learning in neural networks and backpropagation-free machine learning models"
168
+ """
169
+ # f_curr = lkrr.f_curr
170
+ # Du = lkrr.Du
171
+ # mu = self.mu(self.f_curr)
172
+ # Vu = lkrr.Vu
173
+ # W = lkrr.alpha[1:]
174
+ # lam = lkrr.lam
175
+ # M = lkrr.M
176
+ # gamma = lkrr.gamma
177
+ # X = X_train
178
+ # K = lkrr.K_train
179
+ # self = lkrr
180
+ # y = lkrr.y
181
+ # diag = True
182
+ # y = np.random.normal(size = (10,1))
183
+
184
+ mn = y.shape
185
+ d1_array = False
186
+ if len(mn) == 1:
187
+ n,m = (mn[0], 1)
188
+ d1_array = True
189
+
190
+ else:
191
+ n,m = mn
192
+
193
+ # Du, mu = self.get_Du_mu_internal(f_curr) # O(nm)
194
+ # Vu = self.get_Vu_func(f_curr) # O(nm)
195
+ # yb = y.reshape(n, m, order='F') # O(nm)
196
+ e = y - mu # O(nm)
197
+ e /= Vu
198
+
199
+ if d1_array:
200
+ e = e[ :, np.newaxis]
201
+ W = W[ :, np.newaxis]
202
+ Du = Du[:, np.newaxis]
203
+
204
+ c1 = m*n
205
+ # O(nmp^2 + pmn^2) or O(mpn^2) if diag = True
206
+ G = np.zeros((n, X.shape[1]))
207
+ for l in range(n):
208
+ nabla_f_l = np.zeros(X.shape[1])
209
+ I_0 = np.ones(n)
210
+ I_0[l] = 0
211
+ # O(p^2m + pnm) or O(pnm) if diag = True
212
+ for j in range(m):
213
+ w1 = (-1./c1) * ( (W[:,j]*Du[l,j]*e[l,j]) + (W[l,j]*I_0*Du[:,j]*e[:,j]) ) # O(n)
214
+ w2 = (lam/2.) * W[l,j] * ( W[:,j] + I_0 * W[:,j] ) # O(n)
215
+ # O(p^2 + pn) or O(pn) if diag = True
216
+ tmp = nabla_kw(l, K, X, M, gamma, w1 + w2, diag=diag)
217
+ # print("tmp: ",tmp)
218
+ nabla_f_l += tmp
219
+
220
+ G[l,:] = nabla_f_l
221
+
222
+ G -= G.mean(axis=0)
223
+ if not diag:
224
+ M_new = np.zeros((X.shape[1], X.shape[1]), dtype=np.float32)
225
+ # O(np^2)
226
+ for l in range(n):
227
+ # O(p)
228
+ nabla_f_l = G[l,:]
229
+ # O(p^2)
230
+ M_new += np.outer(nabla_f_l, nabla_f_l)
231
+ else:
232
+ M_new = np.zeros(X.shape[1], dtype=np.float32)
233
+ # O(np)
234
+ for l in range(n):
235
+ # O(p)
236
+ nabla_f_l = G[l,:]
237
+ # O(p)
238
+ M_new += nabla_f_l**2
239
+
240
+ return M_new / n