| import numpy as np |
|
|
|
|
| def mse_loss_at_residue(V_hat, Y, residue_index): |
| |
| |
| d = residue_index |
| |
| return 2*((V_hat[d]@V_hat.T - Y[d])**2).sum() - (V_hat[d]@V_hat[d] - Y[d,d])**2 |
|
|
|
|
| def null_mse_loss_at_residue(Y, residue_index): |
| |
| d = residue_index |
| return 2*(Y[d]**2).sum() - Y[d,d]**2 |
|
|
|
|
| def mse_loss(V_hat, Y): |
| |
| return ((V_hat@V_hat.T - Y)**2).sum() |
|
|
|
|
| def nll_loss(V_hat, Y): |
| """We now interpret ys as probabilities. |
| |
| We want to minimise the negative log probability. |
| |
| NB Y must be symmetric and [0-1] |
| """ |
| |
| log_pij = (V_hat@V_hat.T) * np.log(Y) + (1-(V_hat@V_hat.T))*np.log(1-Y) |
| |
| |
| return -0.5*(log_pij.sum() + np.diag(log_pij).sum()) |
|
|
|
|
| def nll_loss_at_residue(V_hat, Y, residue_index): |
| |
| d = residue_index |
| |
| log_pdj = (V_hat[d]@V_hat.T) * np.log(Y[d]) + (1-(V_hat[d]@V_hat.T))*np.log(1-Y[d]) |
| return -log_pdj.sum() |
|
|
|
|
| def null_nll_loss_at_residue(Y, residue_index): |
| return - np.log(1-Y[residue_index]).sum() |
|
|
|
|
| def greedy_V(Y, N_iters=3, K_init=4, cost_type="mse"): |
| """ |
| Learn a binary matrix V, with at most one nonzero entry per row, to minimize |
| |
| || VV' - Y ||_2 |
| |
| This is done by initializing a V of all zeros, and then doing a greedy optimization. |
| V is initially D x K_init, where D is the number of residues (i.e., where Y is DxD). |
| K is learned automatically; different values of K_init will not change the result but |
| might be more or less efficient in terms of memory usage. The code keeps track of the |
| current number of nonzero columns of V, and when all the columns are full it adds extra |
| columns of zeros at the end. |
| |
| Each iteration sweeps through all residues (i.e. rows of V) once. |
| |
| The implementation relies on additivity of loss to reduce computational cost |
| by only computing required increments to the loss at each iteration. |
| |
| |
| INPUTS: |
| |
| Y: matrix of predictions with entries in [0, 1] |
| N_iters: number of iterations |
| K_init: initial number of columns of V, adjust this for tweaking performance |
| """ |
| Y = (Y + Y.T) / 2 |
| V_hat = np.zeros((Y.shape[0], K_init), dtype=np.uint8) |
| if cost_type == "mse": |
| loss = mse_loss(V_hat, Y) |
| elif cost_type == "nll": |
| loss = nll_loss(V_hat, Y) |
| else: |
| raise ValueError(cost_type) |
| K_max = K_init |
|
|
| for it in range(N_iters): |
| for d in range(V_hat.shape[0]): |
| |
| |
| |
| loss_minus_d = loss - mse_loss_at_residue(V_hat, Y, d) |
|
|
| |
| V_hat[d] *= 0 |
|
|
| |
| L0 = loss_minus_d + null_mse_loss_at_residue(Y, d) |
| L_opt = np.zeros(K_max) |
| for k in range(K_max): |
| |
| V_hat[d,k] = 1 |
| L_opt[k] = loss_minus_d + mse_loss_at_residue(V_hat, Y, d) |
| V_hat[d,k] = 0 |
|
|
| |
| z = np.argmin(L_opt) |
| if L_opt[z] < L0: |
| V_hat[d,z] = 1 |
| |
| |
| loss = loss_minus_d + mse_loss_at_residue(V_hat, Y, d) |
|
|
| if z == K_max-1: |
| |
| |
| |
| |
| V_hat = np.concatenate((V_hat, np.zeros_like(V_hat)), -1) |
| K_max = V_hat.shape[1] |
|
|
| |
| empty = V_hat.sum(0) == 0 |
| return V_hat[:,~empty], loss |
|
|