|
1 | 1 | import sys |
2 | 2 | import anndata as ad |
3 | 3 | import numpy as np |
| 4 | +from scipy.linalg import cho_factor, cho_solve |
4 | 5 | from scipy.sparse import csc_matrix |
5 | 6 | from sklearn.decomposition import TruncatedSVD |
6 | 7 | from sklearn.gaussian_process.kernels import RBF |
7 | | -from sklearn.kernel_ridge import KernelRidge |
8 | 8 | from threadpoolctl import threadpool_limits |
9 | 9 |
|
10 | 10 | ## VIASH START |
|
29 | 29 | if meta.get('cpus'): |
30 | 30 | threadpool_limits(meta['cpus']) |
31 | 31 |
|
| 32 | +# Workaround for sklearn's KernelRidge: this computes |
| 33 | +# KernelRidge(alpha=alpha, kernel=kernel).fit(X, y).predict(X_test), but solves the system itself. |
| 34 | +# |
| 35 | +# KernelRidge solves (K + alpha * I) w = y with scipy.linalg.solve(assume_a="pos"). In this image |
| 36 | +# (scipy 1.17 and 1.18 with OpenBLAS) that call raises a MemoryError or segfaults once a half of the |
| 37 | +# batches holds more than ~30-40k cells, no matter how much memory the job gets (200 GB was not enough |
| 38 | +# for 40k cells, which need ~13 GB). So the method failed on every dataset except bmmc_multiome. |
| 39 | +# |
| 40 | +# K + alpha * I is symmetric positive definite, so solve() does a Cholesky factorisation anyway. |
| 41 | +# cho_factor()/cho_solve() run that same factorisation directly and handle 46k cells in about a minute; |
| 42 | +# the predictions are identical to KernelRidge's. They also factorise K in place, whereas KernelRidge |
| 43 | +# copies it first, which saves one n x n float64 matrix of peak memory. |
| 44 | +# |
| 45 | +# Switch back to KernelRidge once scipy.linalg.solve() handles matrices of this size again. |
| 46 | +def kernel_ridge_predict(kernel, alpha, X, y, X_test): |
| 47 | + K = kernel(X) |
| 48 | + K.flat[::K.shape[0] + 1] += alpha |
| 49 | + # K is symmetric, so its transpose is a Fortran-ordered view LAPACK can factorise in place |
| 50 | + dual_coef = cho_solve(cho_factor(K.T, overwrite_a=True), y) |
| 51 | + return kernel(X_test, X) @ dual_coef |
| 52 | + |
32 | 53 |
|
33 | 54 | ## Removed PCA and normalization steps, as they arr already performed with the input data |
34 | 55 | print('Reading input files', flush=True) |
|
118 | 139 |
|
119 | 140 | print(batch, flush=True) |
120 | 141 | kernel = RBF(length_scale = scale) |
121 | | - krr = KernelRidge(alpha=alpha, kernel=kernel) |
122 | 142 | print('Fitting KRR ... ', flush=True) |
123 | | - krr.fit( |
| 143 | + y_pred_batch = kernel_ridge_predict( |
| 144 | + kernel, |
| 145 | + alpha, |
124 | 146 | train_norm[input_train_mod1.obs.batch.isin(batch)], |
125 | | - train_gs[input_train_mod2.obs.batch.isin(batch)] |
| 147 | + train_gs[input_train_mod2.obs.batch.isin(batch)], |
| 148 | + test_norm |
126 | 149 | ) |
127 | | - y_pred_batch = krr.predict(test_norm) |
128 | 150 | if embedder_mod2 is not None: |
129 | 151 | # map the predicted components back to the mod2 feature space |
130 | 152 | y_pred_batch = y_pred_batch @ embedder_mod2.components_ |
|
0 commit comments