import torch
import numpy as np
from rf_diffusion.diffusion import get_beta_schedule, cosine_interp
from icecream import ic
torch.set_printoptions(sci_mode=False)
class DiscreteSeqDiffuser():
'''
Class to do discrete diffusion according to the method reported in [1]. This class will
yield a noised sequence and also will return the "true" probability distributions at
certain timesteps along the noising trajectory.
[1] Austin, Jacob, et al. "Structured denoising diffusion models in discrete
state-spaces." Advances in Neural Information Processing Systems 34 (2021):
17981-17993.
'''
def __init__(self,
T,
rate_matrix,
s_a0=None,
s_aT=None,
s_b0=None,
s_bT=None,
schedule_type='linear',
schedule_params={},
lamda=1,
K=20):
assert (s_a0 is not None and s_aT is not None) ^ (s_b0 is not None and s_bT is not None), \
f"SequenceDiffuser cannot use alpha and beta schedules at the same time, a_0: {s_a0} a_T: {s_aT} " + \
f"b_0: {s_b0} b_T: {s_bT}"
self.K = K
self.T = T
self.rate_matrix_type = rate_matrix
if rate_matrix in ['blosum']:
assert s_a0 is not None and s_aT is not None
self.seq_alpha_schedule, self.seq_alpha_bar_schedule = self.get_seq_alpha_schedule(
s_a0=s_a0,
s_aT=s_aT,
schedule_type=schedule_type,
**schedule_params)
self.singly_stochastic_B = self.rowwise_normalize( torch.exp(torch.Tensor(blosum62)) )
elif rate_matrix in ['uniform']:
assert s_b0 is not None and s_bT is not None
self.seq_beta_schedule, self.seq_alpha_schedule, self.seq_alpha_bar_schedule = self.get_seq_beta_schedule(
s_b0=s_b0,
s_bT=s_bT,
schedule_type=schedule_type,
**schedule_params)
else: raise NotImplementedError(f'Received invalid rate matrix for SequenceDiffuser. Rate matrix {rate_matrix} is not implemented.')
self.lamda = lamda
self.softmax = torch.nn.Softmax(dim=2)
self.rate_matrix = None
self.singly_stochastic = rate_matrix in ['blosum']
def continuous_seq(self):
'''
This type of sequence diffuser does not use a continuous sequence representation so return False.
'''
return False
def diffuse_sequence(self,
seq,
diffusion_mask=None,
t_list=None):
'''
Given a sequence, do discrete diffusion of the sequence and return the result at the
specified timepoints.
Args:
seq (torch.tensor [L], required): Torch tensor of true sequence to be noised. Integer sequence representation
diffusion_mask (torch.tensor [L], optional): Tensor of bools, True means NOT diffused at this residue, False means diffused
t_list (list, optional): If present, only return the diffused coordinates at timesteps t within the list
Returns:
diffused_seq (torch.tensor, [t,L])
true_seq (torch.tensor, [L])
'''
diffusion_mask = diffusion_mask or []
diffused_seq = self.apply_kernel_recursive(seq, diffusion_mask, t_list)
true_seq = seq.clone()
assert( torch.all( diffused_seq[:,diffusion_mask] == true_seq[None,diffusion_mask] ) )
return diffused_seq, true_seq
def rowwise_normalize(self, B):
''' Given a [K,K] matrix, normalize each of the rows by the elements contained in the rows.'''
return B / torch.sum(B, dim=1, keepdim=True)
def get_seq_alpha_schedule(self,
s_a0=0.1,
s_aT=10,
schedule_type='linear'):
'''
The noising schedule parameter alpha is defined differently in Austin et al. from Ho et al. We are going to call
the parameter from Austin et al. seq_alpha (or s_a, s_alpha, etc.) to avoid confusion.
'''
if schedule_type == 'linear':
seq_alpha_schedule = torch.linspace(s_a0, s_aT, self.T)
elif schedule_type == 'cosine':
seq_alpha_schedule = cosine_interp(self.T, s_aT, s_a0)
elif schedule_type == 'exponential':
seq_alpha_schedule = torch.tensor(np.exp(np.linspace(np.log(s_a0), np.log(s_aT), self.T)))
else: raise NotImplementedError(f'Received invalid schedule type for seq_alpha_schedule. Schedule {schedule_type} is not implemented.')
seq_alpha_bar_schedule = torch.cumsum(seq_alpha_schedule, dim=0)
return seq_alpha_schedule, seq_alpha_bar_schedule
def get_seq_beta_schedule(self,
s_b0=0.0001,
s_bT=0.1,
schedule_type='linear'):
if schedule_type == 'linear':
seq_beta_schedule = torch.linspace(s_b0, s_bT, self.T)
elif schedule_type == 'cosine':
seq_beta_schedule = cosine_interp(self.T, s_bT, s_b0)
else: raise NotImplementedError(f'Received invalid schedule type for seq_beta_schedule. Schedule {schedule_type} is not implemented.')
seq_alpha_schedule = torch.ones_like(seq_beta_schedule) - seq_beta_schedule
seq_alpha_bar_schedule = torch.cumprod(seq_alpha_schedule, dim=0)
return seq_beta_schedule, seq_alpha_schedule, seq_alpha_bar_schedule
def get_Qt(self, t_idx):
if self.rate_matrix_type == 'blosum':
if self.rate_matrix is None:
self.rate_matrix = self.singly_stochastic2rate_matrix( self.rowwise_normalize( torch.exp(torch.Tensor(blosum62)) ) )
s_alpha_t = self.seq_alpha_schedule[t_idx]
Qt = torch.matrix_exp(s_alpha_t*self.rate_matrix)
elif self.rate_matrix_type == 'uniform':
beta_t = self.seq_beta_schedule[t_idx]
Qt = ( beta_t / self.K ) * torch.ones(self.K,self.K)
Qt.fill_diagonal_( 1 - ((self.K-1)/self.K) * beta_t )
else: raise NotImplementedError(f'Sequence diffusion with rate matrix of type {self.rate_matrix_type} is not implemented')
return Qt
def get_bar_Qt(self, t_idx):
if self.rate_matrix_type == 'blosum':
if self.rate_matrix is None:
self.rate_matrix = self.singly_stochastic2rate_matrix( self.rowwise_normalize( torch.exp(torch.Tensor(blosum62)) ) )
s_alpha_bar_t = self.seq_alpha_bar_schedule[t_idx]
barQt = torch.matrix_exp(s_alpha_bar_t*self.rate_matrix)
elif self.rate_matrix_type == 'uniform':
s_alpha_bar_t = self.seq_alpha_bar_schedule[t_idx]
barQt = s_alpha_bar_t*torch.eye(self.K) + (1-s_alpha_bar_t)/self.K*torch.ones([self.K,self.K])
else: raise NotImplementedError(f'Sequence diffusion with rate matrix of type {self.rate_matrix_type} is not implemented')
return barQt
def apply_kernel(self, seq, t, diffusion_mask=None):
'''
Take the sequence at timestep = 0 and return the noised sequence at timestep = t
Args:
seq (torch.Tensor, [L]): Integer representation of sequence at timestep = 0
t (int): Timestep to sample
diffusion_mask (torch.Tensor, [L]): Mask
Returns:
out_seq (torch.Tensor, [L]): The noised sequence at timestep = t
'''
t_idx = t - 1
bar_Qt = self.get_bar_Qt(t_idx)
seq_probs_t = bar_Qt[seq]
out_seq = torch.multinomial(seq_probs_t, num_samples=1).squeeze()
if diffusion_mask is not None:
out_seq[diffusion_mask] = seq[diffusion_mask]
return out_seq
def apply_kernel_recursive(self, seq, diffusion_mask=None, t_list=None):
'''
Repeatedly apply self.apply_kernel T times and return all seqs
Args:
Returns:
seq_stack (torch.Tensor, [L,t]): A tensor with the integer representation of the noised sequence at each timestep. The order of dimensions
is defined like this to match the convention in Euclidean Diffuser
'''
if False:
cur_seq = torch.clone(seq)
for t in range(1,self.T+1):
cur_seq = self.apply_kernel(cur_seq,
t,
diffusion_mask)
cur_seq.append(cur_seq)
seq_stack = [self.apply_kernel(seq,t,diffusion_mask) for t in range(1,self.T+1)]
seq_stack = torch.stack(seq_stack, dim=0)
if t_list is not None:
t_idx = torch.tensor([t-1 for t in t_list])
assert(t_idx>=0).all(), 'detected timestep less than 1'
seq_stack = seq_stack[t_idx,:]
return seq_stack
def singly_stochastic2rate_matrix(self, A):
"""rate_matrix computes the rate matrix corresponding to a singly stochastic
matrix A.
Args:
A: single stochastic matrix (e.g. Blossom probabilities) [K, K]
Returns:
rate matrix of shape [K, K]
"""
R = torch.clone(A)
for i in range(A.shape[0]):
R[i,i] = -sum(R[i])
R[i,i] += A[i,i]
return R
def q_Xt1_given_Xt_and_X0(self, x_t, x_0, t):
"""q_Xt1_given_Xt_and_X0 computes conditional q(x_{t-1} | x_t, x_0).
Computation of this conditional follows equation 3 of [1].
For this, the transition matrices are defined via matrix
exponentials as described in Appendix A.4.2 of [1].
Args:
x_t, x_0: index of amino acid at times t and 0
t: time index (positive integer)
Reference:
[1] Austin, Jacob, et al. "Structured denoising diffusion models in discrete
state-spaces." Advances in Neural Information Processing Systems 34 (2021):
17981-17993.
"""
assert t > 0, "time step t-1=%d is undefined "%(t-1)
if t==1: return np.eye(self.K)[x_0]
t_idx = t-1
Qt = self.get_Qt(t_idx).to(x_t.device)
bar_Qt = self.get_bar_Qt(t_idx).to(x_t.device)
bar_Qt1 = self.get_bar_Qt(t_idx-1).to(x_t.device)
if self.singly_stochastic: qs = (Qt[:,x_t].transpose(1,0)*bar_Qt1[x_0])/bar_Qt[x_0, x_t][:,None]
else: qs = (Qt[x_t]*bar_Qt1[x_0])/bar_Qt[x_0, x_t][:,None]
return qs
def p_theta_Xt1_given_Xt_from_pX0(self, x_t, px_0, t):
"""p_theta_Xt1_given_Xt_from_pX0 computes model prediction of
p(X_{t-1}| X_t) from an output that may be understood as a prediction of
p(X_{0}|X_t).
Important: this function operates on one residue position at a time, not on the whole seqeunce
This implementation follows equation 4 of Austin.
Args:
x_t: index of amino acid at time t [1]
px_0: model prediction of probilities of x_0 [20]
t: time index (positive integer)
Returns:
predicted probabilities of x_{t-1} of shape [20]
"""
t_idx = t-1
Qt = self.get_Qt(t_idx).to(x_t.device)
bar_Qt1 = self.get_bar_Qt(t_idx-1).to(x_t.device)
def q_xt1_xt_given_x0(x_t1_, x_t_, x_0_):
return bar_Qt1[x_0_, x_t1_] * Qt[x_t1_, x_t_]
all_terms = torch.stack([
px_0i * torch.tensor([
q_xt1_xt_given_x0(x_t1_j, x_t, x_0_i)
for x_t1_j in range(self.K)
]).to(x_t.device)
for x_0_i, px_0i in enumerate(px_0)])
p_theta_x_t1 = torch.sum(all_terms, axis=0)
p_theta_x_t1 /= torch.sum(p_theta_x_t1)
return p_theta_x_t1
def KL(self, q, p):
"""KL computes the KL divergence for discrete PMFs q to p"""
return q.dot(torch.log(q) - torch.log(p))
def loss(self, x_t, x_0, p_logit_x_0, t, diffusion_mask):
'''
Compute the loss given the timestep
This implementation follows equations 1 and 5 of Austin.
Args:
x_t: index of amino acid at time t [B,L]
x_0: index of amino acid at time 0 [B,L]
p_logit_x_0: model prediction of logits of x_0 [B,L,20]
t: time index (positive integer)
'''
px_0 = self.softmax(p_logit_x_0)
return self.loss_prob(x_t, x_0, px_0, t, diffusion_mask)
def loss_prob(self, x_t, x_0, px_0, t, diffusion_mask):
assert(t>0), f'Received t <= 0 in seq diffuser loss function. t: {t}'
assert px_0.ndim == 3, f'{px_0.ndim} != 3. {px_0.shape}'
B, L, K = px_0.shape
assert K == self.K, f'{K} != {self.K}'
assert x_t.shape == (B,L), f'{x_t.shape} != {B,L}'
assert x_0.shape == (B,L), f'{x_0.shape} != {B,L}'
px_0 = px_0.squeeze(0)
x_t = x_t.squeeze(0)
x_0 = x_0.squeeze(0)
if t>1:
q_Xt1 = self.q_Xt1_given_Xt_and_X0(x_t, x_0, t)
p_theta_Xt1 = torch.stack([self.p_theta_Xt1_given_Xt_from_pX0(x_ti, px_0i, t) for x_ti, px_0i in zip(x_t, px_0)], dim=0).to(x_t.device)
loss_vb = torch.stack([self.KL(q_Xt1_, p_theta_Xt1_) for q_Xt1_, p_theta_Xt1_ in zip(q_Xt1, p_theta_Xt1)], dim=0)
elif t==1:
p_theta_Xt1 = torch.stack([self.p_theta_Xt1_given_Xt_from_pX0(x_ti, px_0i, t) for x_ti, px_0i in zip(x_t, px_0)], dim=0).to(x_t.device)
p_theta_X0 = torch.gather(p_theta_Xt1,1,x_0[:,None]).squeeze(1)
loss_vb = -1 * torch.log(p_theta_X0)
p_theta_Xt1 = torch.stack([self.p_theta_Xt1_given_Xt_from_pX0(x_ti, px_0i, t) for x_ti, px_0i in zip(x_t, px_0)], dim=0).to(x_t.device)
p_theta_X0 = torch.gather(px_0, 1, x_0[...,None]).squeeze(1)
loss_aux = -1 * torch.log(p_theta_X0)
loss = loss_vb + self.lamda * loss_aux
if diffusion_mask is None: diffusion_mask = torch.zeros_like(loss, dtype=torch.bool)
assert diffusion_mask.dtype == torch.bool
return torch.mean(loss[~diffusion_mask]), torch.mean(loss_aux[~diffusion_mask]), torch.mean(loss_vb[~diffusion_mask])
def sample_init(self, seq_t, seq_diffusion_mask):
'''
Function to generate an initial sequence while keeping a motif fixed
Args:
seq_t (torch.tensor): [L] Sequence with zeros everywhere except for fixed motif
seq_diffusion_mask (torch.tensor): [L]
Returns:
seq_T (torch.tensor): [L,22]
'''
sampled_seq = torch.randint(low=0, high=20, size=seq_t.shape)
seq_t[~seq_diffusion_mask] = sampled_seq[~seq_diffusion_mask]
seq_T = torch.nn.functional.one_hot(seq_t, num_classes=22).float()
return seq_T
def get_next_sequence(self, seq_t, pseq0, t, seq_diffusion_mask):
'''
Function to take the predicted sequence and get the sequence at t-1 to feed to the model
Args:
seq_t (torch.tensor): [L,K] One-hot encoding of sequence fed to model at timestep t
pseq0 (torch.tensor): [L,K] The model's prediction of sequence logits
t (int): The current timestep
seq_diffusion_mask (torch.tensor): [L] Whether each position is diffusing sequence or not
Returns:
seq_t_1 (torch.tensor): [L,K] One-hot encoding of sequence to feed to model at timestep t-1
'''
L = seq_t.shape[0]
int_seq_t = torch.argmax(seq_t, dim=-1)
seq_next_diff = []
for l in range(L):
seq_tl = int_seq_t[l]
pseq0_l = pseq0[l]
p_xt1 = self.p_theta_Xt1_given_Xt_from_pX0(seq_tl, pseq0_l, t)
p_xt1 = torch.clamp(p_xt1, min=0)
sampled_seq = torch.multinomial(p_xt1, 1)
seq_next_diff.append(sampled_seq)
seq_next_diff = torch.tensor(seq_next_diff)
seq_next_diff[seq_diffusion_mask] = int_seq_t[seq_diffusion_mask]
seq_t_1 = torch.nn.functional.one_hot(seq_next_diff, num_classes=self.K)
return seq_t_1
def get_pi(self, L):
'''Return the stationary distribution.'''
if self.rate_matrix_type == 'uniform':
return torch.nn.functional.one_hot(torch.randint(0, self.K, size=(L,)), num_classes=self.K)
raise NotImplementedError(f'rate_matrix_type {rate_matrix_type} not supported')
class ContinuousSeqDiffuser():
'''
Class to do analog bit diffusion according to the method reported in [1]. This class will
yield a noised sequence at certain timesteps along the noising trajectory.
[1] Chen, Ting, et al. "Analog Bits: Generating Discrete Data using Diffusion Models
ith Self-Conditioning." arXiv (2022)
'''
def __init__(self,
T,
s_b0,
s_bT,
schedule_type='linear',
schedule_params={},
loss_type='l2_loss'):
self.K = 20
self.bitscale = 1
self.T = T
self.loss_type = loss_type
self.beta_schedule, _, self.alphabar_schedule = get_beta_schedule(T, s_b0, s_bT, schedule_type, **schedule_params)
def continuous_seq(self):
'''
This type of sequence diffuser uses a continuous sequence representation so return True
'''
return True
def seq2bits(self,
seq,
K=None):
'''
Given a sequence in integer representation return the sequence as analog bit.
Args:
seq (torch.tensor [L], required): Torch tensor of integer sequence representation.
Returns:
seqbits (torch.tensor [L,K]): Torch tensor of sequence as analog bits [-1,1] at each position
'''
if K is None: K = self.K
seqbits = torch.nn.functional.one_hot(seq, num_classes=K).float()
seqbits = (seqbits * 2 - 1) * self.bitscale
return seqbits
def diffuse_sequence(self,
seq,
diffusion_mask=None,
t_list=None):
'''
Given a sequence, do bit diffusion of the sequence and return the result at the
specified timepoints.
Args:
seq (torch.tensor [L], required): Torch tensor of true sequence to be noised. Integer sequence representation
diffusion_mask (torch.tensor [L], optional): Tensor of bools, True means NOT diffused at this residue, False means diffused
t_list (list, optional): If present, only return the diffused coordinates at timesteps t within the list
Returns:
diffused_seq (torch.tensor [t,L,K] )
true_seq (torch.tensor [L,K] )
'''
true_seq = self.seq2bits(seq.clone())
diffused_seq = self.apply_kernel_recursive(seq, diffusion_mask, t_list)
if diffusion_mask is not None:
assert(torch.all( diffused_seq[:,diffusion_mask] == true_seq[None,diffusion_mask] ))
return diffused_seq, true_seq
def apply_kernel_recursive(self,
seq,
diffusion_mask,
t_list):
curr_seq = self.seq2bits( seq.clone() )
seq_stack = []
for t in range(1,self.T+1):
curr_seq = self.apply_kernel(curr_seq,
t,
diffusion_mask)
seq_stack.append(curr_seq)
seq_stack = torch.stack(seq_stack, dim=0)
if t_list is not None:
t_idx = torch.tensor([t-1 for t in t_list])
assert(t_idx>=0).all(), 'detected timestep less than 1'
seq_stack = seq_stack[t_idx]
return seq_stack
def apply_kernel(self,
seq,
t,
diffusion_mask=None,
var_scale=1):
'''
seq (torch.tensor, [L,K])
t (int)
diffusion_mask (torch.tensor, [L]) True means NOT diffused
'''
t_idx = t-1
L,K = seq.shape
b_t = self.beta_schedule[t_idx]
mean = torch.sqrt(1-b_t)*seq
var = torch.ones(L,K) * b_t * var_scale
sampled_seq = torch.normal(mean, torch.sqrt(var))
delta = sampled_seq - seq
if diffusion_mask is not None:
delta[diffusion_mask, ...] = 0
out_seq = seq + delta
return out_seq
def loss(self,
seq_true,
seq_pred,
diffusion_mask):
'''
Wrapper function to decide which loss to run
Args:
seq_true (torch.tensor, [L]): The true sequence as integers
seq_pred (torch.tensor, [L,K]): The predicted sequence in analog bit form
'''
if self.loss_type == 'l2_loss':
true_bits = self.seq2bits(seq_true, K=21)
return self.l2_loss(true_bits, seq_pred)
if self.loss_type == 'sigmoid':
true_bits = self.seq2bits(seq_true, K=21)
return self.sigmoid_loss(true_bits, seq_pred)
else:
raise NotImplementedError()
def l2_loss(self,
seq_true,
seq_pred):
'''
Loss described in Equation 2 of Chen et al.
This is an L2 norm of the distance of the true bits from the predicted bits
Args:
seq_true (torch.tensor, [L,K]): The true sequence in analog bit form
seq_pred (torch.tensor, [L,K]): The predicted sequence in analog bit form
'''
loss = torch.square( seq_true - seq_pred )
return torch.mean(loss)
def sigmoid_loss(self,
seq_true,
seq_pred):
'''
Loss described in B.2 of Chen et al.
This is the sigmoid cross entropy loss of the predicted bits versus the real bits
Args:
seq_true (torch.tensor, [L,K]): The true sequence in analog bit form
seq_pred (torch.tensor, [L,K]): The predicted sequence in analog bit form
'''
ic('Doing Sigmoid Seq loss')
loss = torch.log( torch.sigmoid(seq_true * seq_pred) )
return -1 * torch.mean(loss)
def cce_loss(self,
seq_true,
seq_pred):
'''
Loss described in B.3 of Chen et al.
'''
raise NotImplementedError()
def sample_init(self, seq_t, seq_diffusion_mask):
'''
Function to generate an initial sequence while keeping a motif fixed
Args:
seq_t (torch.tensor): [L] Sequence with zeros everywhere except for fixed motif
seq_diffusion_mask (torch.tensor): [L] True means NOT diffused
Returns:
seq_T (torch.tensor): [L,22]
'''
L = seq_t.shape[0]
mean = torch.zeros(L,20)
std = torch.full((L,20), self.bitscale)
sampled_seq = torch.normal(mean, std)
seq_T = self.seq2bits(seq_t.clone(), K=20)
seq_T[~seq_diffusion_mask] = sampled_seq[~seq_diffusion_mask]
zeros = torch.zeros(L,2)
seq_T = torch.cat((seq_T, zeros), dim=-1)
return seq_T
def get_seq_mu_xt_x0(self, seq_t, pseq_0, t_idx, eps=1e-6):
"""
Given xt, predicted x0 and the timestep t, give mu of x(t-1)
Assumes t_idx is 0 indexed
"""
sigma = ((1-self.alphabar_schedule[t_idx-1])/(1-self.alphabar_schedule[t_idx]))*self.beta_schedule[t_idx]*self.bitscale
a = ((torch.sqrt(self.alphabar_schedule[t_idx-1] + eps)*self.beta_schedule[t_idx])/(1-self.alphabar_schedule[t_idx]))*pseq_0
b = ((torch.sqrt(1-self.beta_schedule[t_idx] + eps)*(1-self.alphabar_schedule[t_idx-1]))/(1-self.alphabar_schedule[t_idx]))*seq_t
mu = a + b
return mu, sigma
def get_next_sequence(self, seq_t, pseq0, t, seq_diffusion_mask):
'''
Function to take predicted analog bits and get the sequence at t-1 to feed to the model
Args:
seq_t (torch.tensor): [L,K] The sequence bits fed to the model at timestep t
pseq0 (torch.tensor): [L,K] The t=0 sequence bits predicted by the model
t (int): The current timestep
seq_diffusion_mask (torch.tensor): [L] Mask of whether each position is diffusing sequence or not, True means NOT diffused
Returns:
seq_t_1 (torch.tensor): [L,K] The sequence bits to feed to the model at timestep t-1
'''
t_idx = t-1
if self.loss_type == 'sigmoid':
pseq0 = 2*torch.sigmoid(pseq0) - 1
mu, sigma = self.get_seq_mu_xt_x0(seq_t=seq_t, pseq_0=pseq0, t_idx=t_idx)
sampled_seq = torch.normal(mu, sigma)
delta = sampled_seq - seq_t
if seq_diffusion_mask is not None:
delta[seq_diffusion_mask,:] = 0
seq_t_1 = seq_t + delta
return seq_t_1