Crispr / bert_model.py
santu0032's picture
Upload 9 files
930ed52 verified
Raw
History Blame Contribute Delete
15.4 kB
"""
BERT Model for CRISPR Off-Target Prediction
Built from scratch using the architecture from Figure S5
"""
import numpy as np
class Embedding:
"""Embedding layer to convert token IDs to vectors."""
def __init__(self, vocab_size, embed_dim):
"""
Initialize embedding layer.
Args:
vocab_size: Size of vocabulary
embed_dim: Dimension of embedding vectors
"""
self.vocab_size = vocab_size
self.embed_dim = embed_dim
# Initialize embedding matrix
self.embeddings = np.random.randn(vocab_size, embed_dim) * 0.01
def forward(self, x):
"""
Forward pass: look up embeddings for input IDs.
Args:
x: Input token IDs of shape (batch, seq_len)
Returns:
Embeddings of shape (batch, seq_len, embed_dim)
"""
return self.embeddings[x]
class LayerNorm:
"""
Layer Normalization.
Formula: y = (x - mean) / sqrt(var + eps) * gamma + beta
"""
def __init__(self, normalized_shape, eps=1e-6):
self.eps = eps
self.gamma = np.ones(normalized_shape)
self.beta = np.zeros(normalized_shape)
def forward(self, x):
"""Apply layer normalization."""
mean = np.mean(x, axis=-1, keepdims=True)
var = np.var(x, axis=-1, keepdims=True)
x_norm = (x - mean) / np.sqrt(var + self.eps)
return self.gamma * x_norm + self.beta
class MultiHeadAttention:
"""
Multi-Head Self-Attention mechanism.
Formulas:
Q = XW_q, K = XW_k, V = XW_v
Attention(Q,K,V) = softmax(QK^T / sqrt(d_k)) V
MultiHead = Concat(head_1, ..., head_h) W_o
"""
def __init__(self, embed_dim, num_heads):
"""
Initialize multi-head attention.
Args:
embed_dim: Embedding dimension
num_heads: Number of attention heads
"""
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
assert embed_dim % num_heads == 0, "embed_dim must be divisible by num_heads"
# Weight matrices for Q, K, V
self.W_q = np.random.randn(embed_dim, embed_dim) * 0.01
self.W_k = np.random.randn(embed_dim, embed_dim) * 0.01
self.W_v = np.random.randn(embed_dim, embed_dim) * 0.01
# Output projection
self.W_o = np.random.randn(embed_dim, embed_dim) * 0.01
def scaled_dot_product_attention(self, Q, K, V):
"""
Scaled dot-product attention.
Args:
Q, K, V: Query, Key, Value matrices
Returns:
Attention output
"""
# Calculate attention scores: QK^T / sqrt(d_k)
d_k = Q.shape[-1]
scores = Q @ K.transpose(0, 1, 3, 2) / np.sqrt(d_k)
# Apply softmax
scores_exp = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
attention_weights = scores_exp / np.sum(scores_exp, axis=-1, keepdims=True)
# Apply attention to values
output = attention_weights @ V
return output
def forward(self, x):
"""
Forward pass through multi-head attention.
Args:
x: Input of shape (batch, seq_len, embed_dim)
Returns:
Output of shape (batch, seq_len, embed_dim)
"""
batch_size, seq_len, _ = x.shape
# Linear projections
Q = x @ self.W_q # (batch, seq_len, embed_dim)
K = x @ self.W_k
V = x @ self.W_v
# Reshape for multi-head: (batch, num_heads, seq_len, head_dim)
Q = Q.reshape(batch_size, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
K = K.reshape(batch_size, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
V = V.reshape(batch_size, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
# Apply attention
attention_output = self.scaled_dot_product_attention(Q, K, V)
# Concatenate heads: (batch, seq_len, embed_dim)
attention_output = attention_output.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.embed_dim)
# Output projection
output = attention_output @ self.W_o
return output
class FeedForward:
"""
Position-wise Feed-Forward Network.
Formula: FFN(x) = max(0, xW1 + b1)W2 + b2
"""
def __init__(self, embed_dim, ff_dim):
"""
Initialize feed-forward network.
Args:
embed_dim: Embedding dimension
ff_dim: Hidden dimension of feed-forward network
"""
self.W1 = np.random.randn(embed_dim, ff_dim) * 0.01
self.b1 = np.zeros(ff_dim)
self.W2 = np.random.randn(ff_dim, embed_dim) * 0.01
self.b2 = np.zeros(embed_dim)
def gelu(self, x):
"""
GELU activation: 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
Approximation used in BERT
"""
return 0.5 * x * (1.0 + np.tanh(np.sqrt(2.0 / np.pi) * (x + 0.044715 * x**3)))
def forward(self, x):
"""Forward pass through feed-forward network."""
hidden = self.gelu(x @ self.W1 + self.b1)
output = hidden @ self.W2 + self.b2
return output
class TransformerBlock:
"""
Transformer encoder block.
Architecture:
x -> [Multi-Head Attention -> Add & Norm] -> [FFN -> Add & Norm] -> output
"""
def __init__(self, embed_dim, num_heads, ff_dim, dropout_rate=0.1):
self.attention = MultiHeadAttention(embed_dim, num_heads)
self.norm1 = LayerNorm(embed_dim)
self.ffn = FeedForward(embed_dim, ff_dim)
self.norm2 = LayerNorm(embed_dim)
self.dropout_rate = dropout_rate
def dropout(self, x, training=True):
"""Apply dropout."""
if training and self.dropout_rate > 0:
mask = np.random.binomial(1, 1 - self.dropout_rate, x.shape) / (1 - self.dropout_rate)
return x * mask
return x
def forward(self, x, training=True):
"""
Forward pass through transformer block.
Args:
x: Input of shape (batch, seq_len, embed_dim)
training: Whether in training mode
Returns:
Output of shape (batch, seq_len, embed_dim)
"""
# Multi-head attention with residual connection
attn_output = self.attention.forward(x)
attn_output = self.dropout(attn_output, training)
x = self.norm1.forward(x + attn_output)
# Feed-forward with residual connection
ffn_output = self.ffn.forward(x)
ffn_output = self.dropout(ffn_output, training)
x = self.norm2.forward(x + ffn_output)
return x
class BiGRU:
"""
Bidirectional GRU layer.
Same implementation as in CNN model.
"""
def __init__(self, input_size, hidden_size):
self.input_size = input_size
self.hidden_size = hidden_size
# Initialize weights for forward GRU
limit = np.sqrt(1.0 / hidden_size)
self.W_z_fwd = np.random.uniform(-limit, limit, (hidden_size, input_size))
self.U_z_fwd = np.random.uniform(-limit, limit, (hidden_size, hidden_size))
self.b_z_fwd = np.zeros(hidden_size)
self.W_r_fwd = np.random.uniform(-limit, limit, (hidden_size, input_size))
self.U_r_fwd = np.random.uniform(-limit, limit, (hidden_size, hidden_size))
self.b_r_fwd = np.zeros(hidden_size)
self.W_h_fwd = np.random.uniform(-limit, limit, (hidden_size, input_size))
self.U_h_fwd = np.random.uniform(-limit, limit, (hidden_size, hidden_size))
self.b_h_fwd = np.zeros(hidden_size)
# Initialize weights for backward GRU
self.W_z_bwd = np.random.uniform(-limit, limit, (hidden_size, input_size))
self.U_z_bwd = np.random.uniform(-limit, limit, (hidden_size, hidden_size))
self.b_z_bwd = np.zeros(hidden_size)
self.W_r_bwd = np.random.uniform(-limit, limit, (hidden_size, input_size))
self.U_r_bwd = np.random.uniform(-limit, limit, (hidden_size, hidden_size))
self.b_r_bwd = np.zeros(hidden_size)
self.W_h_bwd = np.random.uniform(-limit, limit, (hidden_size, input_size))
self.U_h_bwd = np.random.uniform(-limit, limit, (hidden_size, hidden_size))
self.b_h_bwd = np.zeros(hidden_size)
def sigmoid(self, x):
"""Sigmoid activation."""
return 1.0 / (1.0 + np.exp(-np.clip(x, -500, 500)))
def tanh(self, x):
"""Tanh activation."""
return np.tanh(np.clip(x, -500, 500))
def gru_cell_forward(self, x_t, h_prev, direction='fwd'):
"""Single GRU cell forward pass."""
if direction == 'fwd':
W_z, U_z, b_z = self.W_z_fwd, self.U_z_fwd, self.b_z_fwd
W_r, U_r, b_r = self.W_r_fwd, self.U_r_fwd, self.b_r_fwd
W_h, U_h, b_h = self.W_h_fwd, self.U_h_fwd, self.b_h_fwd
else:
W_z, U_z, b_z = self.W_z_bwd, self.U_z_bwd, self.b_z_bwd
W_r, U_r, b_r = self.W_r_bwd, self.U_r_bwd, self.b_r_bwd
W_h, U_h, b_h = self.W_h_bwd, self.U_h_bwd, self.b_h_bwd
# GRU formulas
z_t = self.sigmoid(W_z @ x_t + U_z @ h_prev + b_z)
r_t = self.sigmoid(W_r @ x_t + U_r @ h_prev + b_r)
h_tilde = self.tanh(W_h @ x_t + U_h @ (r_t * h_prev) + b_h)
h_t = (1 - z_t) * h_prev + z_t * h_tilde
return h_t
def forward(self, x):
"""Forward pass through BiGRU."""
batch_size, seq_len, _ = x.shape
# Initialize outputs
h_fwd = np.zeros((batch_size, seq_len, self.hidden_size))
h_bwd = np.zeros((batch_size, seq_len, self.hidden_size))
for b in range(batch_size):
# Forward direction
h_prev = np.zeros(self.hidden_size)
for t in range(seq_len):
h_prev = self.gru_cell_forward(x[b, t], h_prev, 'fwd')
h_fwd[b, t] = h_prev
# Backward direction
h_prev = np.zeros(self.hidden_size)
for t in range(seq_len - 1, -1, -1):
h_prev = self.gru_cell_forward(x[b, t], h_prev, 'bwd')
h_bwd[b, t] = h_prev
# Concatenate forward and backward
output = np.concatenate([h_fwd, h_bwd], axis=-1)
return output
class Dense:
"""Fully connected layer."""
def __init__(self, input_size, output_size, activation=None):
self.input_size = input_size
self.output_size = output_size
self.activation = activation
# Xavier initialization
limit = np.sqrt(6.0 / (input_size + output_size))
self.weights = np.random.uniform(-limit, limit, (input_size, output_size))
self.bias = np.zeros(output_size)
def forward(self, x):
"""Forward pass."""
output = x @ self.weights + self.bias
if self.activation == 'relu':
output = np.maximum(0, output)
elif self.activation == 'sigmoid':
output = 1.0 / (1.0 + np.exp(-np.clip(output, -500, 500)))
elif self.activation == 'softmax':
exp_x = np.exp(output - np.max(output, axis=-1, keepdims=True))
output = exp_x / np.sum(exp_x, axis=-1, keepdims=True)
return output
class Dropout:
"""Dropout layer."""
def __init__(self, rate=0.35):
self.rate = rate
def forward(self, x, training=True):
"""Apply dropout."""
if training and self.rate > 0:
mask = np.random.binomial(1, 1 - self.rate, x.shape) / (1 - self.rate)
return x * mask
return x
class BERTBranch:
"""
BERT Branch for CRISPR-BERT.
Architecture: Token/Position/Segment Embeddings → Transformer → Output (26, 80)
Note: Original uses BERT-base (768 hidden), but we project to 80 dimensions
"""
def __init__(self, vocab_size=28, embed_dim=256, num_heads=4, num_layers=2, ff_dim=1024, output_dim=80):
"""
Initialize BERT branch.
Args:
vocab_size: Size of token vocabulary (default: 28 for CRISPR tokens)
embed_dim: Embedding dimension (default: 256)
num_heads: Number of attention heads (default: 4)
num_layers: Number of transformer layers (default: 2)
ff_dim: Feed-forward dimension (default: 1024)
output_dim: Output dimension (default: 80)
"""
# Embedding layers
self.token_embedding = Embedding(vocab_size, embed_dim)
self.position_embedding = Embedding(26, embed_dim) # Max sequence length = 26
self.segment_embedding = Embedding(2, embed_dim) # Segment A/B (we use only A)
self.embed_norm = LayerNorm(embed_dim)
self.embed_dropout = Dropout(0.1)
# Transformer layers
self.transformer_layers = [
TransformerBlock(embed_dim, num_heads, ff_dim, dropout_rate=0.1)
for _ in range(num_layers)
]
# Projection layer to output dimension (embed_dim → 80)
self.projection = Dense(embed_dim, output_dim, activation=None)
def forward(self, token_ids, segment_ids, position_ids, training=True):
"""
Forward pass through BERT branch.
Args:
token_ids: Token IDs of shape (batch, 26)
segment_ids: Segment IDs of shape (batch, 26) - all zeros
position_ids: Position IDs of shape (batch, 26) - [0, 1, ..., 25]
training: Whether in training mode
Returns:
Output of shape (batch, 26, 80)
"""
batch_size = token_ids.shape[0]
# Embedding layer: sum of token, position, and segment embeddings
token_embeds = self.token_embedding.forward(token_ids)
position_embeds = self.position_embedding.forward(position_ids)
segment_embeds = self.segment_embedding.forward(segment_ids)
# Sum embeddings
embeddings = token_embeds + position_embeds + segment_embeds
# Apply layer norm and dropout
embeddings = self.embed_norm.forward(embeddings)
embeddings = self.embed_dropout.forward(embeddings, training)
# Pass through transformer layers
x = embeddings
for layer in self.transformer_layers:
x = layer.forward(x, training)
# Project to output dimension: (batch, 26, embed_dim) → (batch, 26, 80)
output = self.projection.forward(x)
return output