| """ |
| 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 |
| |
| |
| 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" |
| |
| |
| 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 |
| |
| |
| 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 |
| """ |
| |
| d_k = Q.shape[-1] |
| scores = Q @ K.transpose(0, 1, 3, 2) / np.sqrt(d_k) |
| |
| |
| scores_exp = np.exp(scores - np.max(scores, axis=-1, keepdims=True)) |
| attention_weights = scores_exp / np.sum(scores_exp, axis=-1, keepdims=True) |
| |
| |
| 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 |
| |
| |
| Q = x @ self.W_q |
| K = x @ self.W_k |
| V = x @ self.W_v |
| |
| |
| 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) |
| |
| |
| attention_output = self.scaled_dot_product_attention(Q, K, V) |
| |
| |
| attention_output = attention_output.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.embed_dim) |
| |
| |
| 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) |
| """ |
| |
| attn_output = self.attention.forward(x) |
| attn_output = self.dropout(attn_output, training) |
| x = self.norm1.forward(x + attn_output) |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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): |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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) |
| """ |
| |
| self.token_embedding = Embedding(vocab_size, embed_dim) |
| self.position_embedding = Embedding(26, embed_dim) |
| self.segment_embedding = Embedding(2, embed_dim) |
| |
| self.embed_norm = LayerNorm(embed_dim) |
| self.embed_dropout = Dropout(0.1) |
| |
| |
| self.transformer_layers = [ |
| TransformerBlock(embed_dim, num_heads, ff_dim, dropout_rate=0.1) |
| for _ in range(num_layers) |
| ] |
| |
| |
| 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] |
| |
| |
| token_embeds = self.token_embedding.forward(token_ids) |
| position_embeds = self.position_embedding.forward(position_ids) |
| segment_embeds = self.segment_embedding.forward(segment_ids) |
| |
| |
| embeddings = token_embeds + position_embeds + segment_embeds |
| |
| |
| embeddings = self.embed_norm.forward(embeddings) |
| embeddings = self.embed_dropout.forward(embeddings, training) |
| |
| |
| x = embeddings |
| for layer in self.transformer_layers: |
| x = layer.forward(x, training) |
| |
| |
| output = self.projection.forward(x) |
| |
| return output |
|
|