Spaces:
Sleeping
Sleeping
Download model.py from ME22B191/Neural_Translation: direct link, hf CLI and curl.
- Browser
- Download file 28.3 kB
-
https://huggingface.co/spaces/ME22B191/Neural_Translation/resolve/main/model.py
- Command line
-
hf download hf://spaces/ME22B191/Neural_Translation/model.py
-
curl -L -o model.py https://huggingface.co/spaces/ME22B191/Neural_Translation/resolve/main/model.py
28.3 kB
| """ | |
| model.py β Transformer Architecture Skeleton | |
| DA6401 Assignment 3: "Attention Is All You Need" | |
| AUTOGRADER CONTRACT (DO NOT MODIFY SIGNATURES): | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β scaled_dot_product_attention(Q, K, V, mask) β (out, weights) β | |
| β MultiHeadAttention.forward(q, k, v, mask) β Tensor β | |
| β PositionalEncoding.forward(x) β Tensor β | |
| β make_src_mask(src, pad_idx) β BoolTensor β | |
| β make_tgt_mask(tgt, pad_idx) β BoolTensor β | |
| β Transformer.encode(src, src_mask) β Tensor β | |
| β Transformer.decode(memory,src_m,tgt,tgt_m) β Tensor β | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| """ | |
| import math | |
| import copy | |
| import os | |
| from typing import Optional, Tuple | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # STANDALONE ATTENTION FUNCTION | |
| # Exposed at module level so the autograder can import and test it | |
| # independently of MultiHeadAttention. | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def scaled_dot_product_attention( | |
| Q: torch.Tensor, | |
| K: torch.Tensor, | |
| V: torch.Tensor, | |
| mask: Optional[torch.Tensor] = None, | |
| ) -> Tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Compute Scaled Dot-Product Attention. | |
| Attention(Q, K, V) = softmax( QΒ·Kα΅ / βdβ ) Β· V | |
| Args: | |
| Q : Query tensor, shape (..., seq_q, d_k) | |
| K : Key tensor, shape (..., seq_k, d_k) | |
| V : Value tensor, shape (..., seq_k, d_v) | |
| mask : Optional Boolean mask, shape broadcastable to | |
| (..., seq_q, seq_k). | |
| Positions where mask is True are MASKED OUT | |
| (set to -inf before softmax). | |
| Returns: | |
| output : Attended output, shape (..., seq_q, d_v) | |
| attn_w : Attention weights, shape (..., seq_q, seq_k) | |
| """ | |
| d_k = Q.size(-1) | |
| scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) | |
| # Q : [..., seq_q, d_k] | |
| # K.transpose(-2, -1) : [..., d_k, seq_k] | |
| # matmul result : [..., seq_q, seq_k] | |
| # great observation: A plain K.T would not work here β .T reverses all dims, | |
| # which would scramble the batch/head axes. | |
| # transpose(-2, -1) surgically swaps only the two we want. | |
| if mask is not None: | |
| scores = scores.masked_fill(mask, float("-inf")) | |
| attn_w = F.softmax(scores, dim=-1) | |
| output = torch.matmul(attn_w, V) | |
| return output, attn_w | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # β· MASK HELPERS | |
| # Exposed at module level so they can be tested independently and | |
| # reused inside Transformer.forward. | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def make_src_mask( | |
| src: torch.Tensor, | |
| pad_idx: int = 1, | |
| ) -> torch.Tensor: | |
| """ | |
| Build a padding mask for the encoder (source sequence). | |
| Args: | |
| src : Source token-index tensor, shape [batch, src_len] | |
| pad_idx : Vocabulary index of the <pad> token (default 1) | |
| Returns: | |
| Boolean mask, shape [batch, 1, 1, src_len] | |
| True β position is a PAD token (will be masked out) | |
| False β real token | |
| """ | |
| return (src == pad_idx).unsqueeze(1).unsqueeze(2) | |
| def make_tgt_mask( | |
| tgt: torch.Tensor, | |
| pad_idx: int = 1, | |
| ) -> torch.Tensor: | |
| """ | |
| Build a combined padding + causal (look-ahead) mask for the decoder. | |
| Args: | |
| tgt : Target token-index tensor, shape [batch, tgt_len] | |
| pad_idx : Vocabulary index of the <pad> token (default 1) | |
| Returns: | |
| Boolean mask, shape [batch, 1, tgt_len, tgt_len] | |
| True β position is masked out (PAD or future token) | |
| """ | |
| T = tgt.size(1) | |
| pad_mask = (tgt == pad_idx).unsqueeze(1).unsqueeze(2) # [B, 1, 1, T] | |
| causal = torch.triu( | |
| torch.ones(T, T, device=tgt.device, dtype=torch.bool), | |
| diagonal=1, | |
| ) # [T, T] | |
| return pad_mask | causal # [B, 1, T, T] | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MULTI-HEAD ATTENTION | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MultiHeadAttention(nn.Module): | |
| """ | |
| Multi-Head Attention as in "Attention Is All You Need", Β§3.2.2. | |
| MultiHead(Q,K,V) = Concat(head_1,...,head_h) Β· W_O | |
| head_i = Attention(QΒ·W_Qi, KΒ·W_Ki, VΒ·W_Vi) | |
| You are NOT allowed to use torch.nn.MultiheadAttention. | |
| Args: | |
| d_model (int) : Total model dimensionality. Must be divisible by num_heads. | |
| num_heads (int) : Number of parallel attention heads h. | |
| dropout (float): Dropout probability applied to attention weights. | |
| """ | |
| def __init__(self, d_model: int, num_heads: int, dropout: float = 0.1) -> None: | |
| super().__init__() | |
| assert d_model % num_heads == 0, "d_model must be divisible by num_heads" | |
| self.d_model = d_model | |
| self.num_heads = num_heads | |
| self.d_k = d_model // num_heads # depth per head | |
| self.W_q = nn.Linear(d_model, d_model) # matrix multiplication of W_q to X equivalently means passing through MLP with weight W_q and no bias. | |
| # Same for W_k, W_v, W_o. | |
| self.W_k = nn.Linear(d_model, d_model) | |
| self.W_v = nn.Linear(d_model, d_model) | |
| self.W_o = nn.Linear(d_model, d_model) | |
| self.dropout = nn.Dropout(p=dropout) | |
| self.attn = None # cached attention weights (for visualization, expt 2.3) | |
| def forward( | |
| self, | |
| query: torch.Tensor, | |
| key: torch.Tensor, | |
| value: torch.Tensor, | |
| mask: Optional[torch.Tensor] = None, | |
| ) -> torch.Tensor: | |
| """ | |
| Args: | |
| query : shape [batch, seq_q, d_model] | |
| key : shape [batch, seq_k, d_model] | |
| value : shape [batch, seq_k, d_model] | |
| mask : Optional BoolTensor broadcastable to | |
| [batch, num_heads, seq_q, seq_k] | |
| True β masked out (attend nowhere) | |
| Returns: | |
| output : shape [batch, seq_q, d_model] | |
| """ | |
| B = query.size(0) | |
| Q = self.W_q(query) # [B, seq_q, d_model] | |
| K = self.W_k(key) # [B, seq_k, d_model] | |
| V = self.W_v(value) # [B, seq_k, d_model] | |
| # | |
| # split d_model into (num_heads, d_k); bring head axis up to batch position | |
| Q = Q.view(B, -1, self.num_heads, self.d_k).transpose(1, 2) # [B, h, seq_q, d_k] | |
| K = K.view(B, -1, self.num_heads, self.d_k).transpose(1, 2) # [B, h, seq_k, d_k] | |
| V = V.view(B, -1, self.num_heads, self.d_k).transpose(1, 2) # [B, h, seq_k, d_k] | |
| # | |
| out, attn = scaled_dot_product_attention(Q, K, V, mask=mask) | |
| self.attn = attn # cache for expt 2.3 | |
| out = self.dropout(out) # [B, h, seq_q, d_k] | |
| # | |
| # concat heads back: [B, h, seq_q, d_k] -> [B, seq_q, d_model] | |
| out = out.transpose(1, 2).contiguous().view(B, -1, self.d_model) | |
| return self.W_o(out) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # POSITIONAL ENCODING | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class PositionalEncoding(nn.Module): | |
| """ | |
| Sinusoidal Positional Encoding as in "Attention Is All You Need", Β§3.5. | |
| Args: | |
| d_model (int) : Embedding dimensionality. | |
| dropout (float): Dropout applied after adding encodings. | |
| max_len (int) : Maximum sequence length to pre-compute (default 5000). | |
| """ | |
| def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000) -> None: | |
| super().__init__() | |
| self.dropout = nn.Dropout(p=dropout) | |
| pe = torch.zeros(max_len, d_model) | |
| position = torch.arange(0, max_len).unsqueeze(1).float() # [max_len, 1] | |
| div_term = torch.exp( | |
| torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) | |
| ) # [d_model/2] | |
| pe[:, 0::2] = torch.sin(position * div_term) | |
| pe[:, 1::2] = torch.cos(position * div_term) | |
| pe = pe.unsqueeze(0) # [1, max_len, d_model] | |
| self.register_buffer("pe", pe) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Args: | |
| x : Input embeddings, shape [batch, seq_len, d_model] | |
| Returns: | |
| Tensor of same shape [batch, seq_len, d_model] | |
| = x + PE[:, :seq_len, :] | |
| """ | |
| x = x + self.pe[:, : x.size(1)] | |
| return self.dropout(x) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FEED-FORWARD NETWORK | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class PositionwiseFeedForward(nn.Module): | |
| """ | |
| Position-wise Feed-Forward Network, Β§3.3: | |
| FFN(x) = max(0, xΒ·Wβ + bβ)Β·Wβ + bβ | |
| Args: | |
| d_model (int) : Input / output dimensionality (e.g. 512). | |
| d_ff (int) : Inner-layer dimensionality (e.g. 2048). | |
| dropout (float): Dropout applied between the two linears. | |
| """ | |
| def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1) -> None: | |
| super().__init__() | |
| self.linear1 = nn.Linear(d_model, d_ff) # Wβ, bβ : d_model -> d_ff | |
| self.linear2 = nn.Linear(d_ff, d_model) # Wβ, bβ : d_ff -> d_model | |
| self.dropout = nn.Dropout(p=dropout) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Args: | |
| x : shape [batch, seq_len, d_model] | |
| Returns: | |
| shape [batch, seq_len, d_model] | |
| """ | |
| return self.linear2(self.dropout(F.relu(self.linear1(x)))) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ENCODER LAYER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class EncoderLayer(nn.Module): | |
| """ | |
| Single Transformer encoder sub-layer: | |
| x β [Self-Attention β Add & Norm] β [FFN β Add & Norm] | |
| Args: | |
| d_model (int) : Model dimensionality. | |
| num_heads (int) : Number of attention heads. | |
| d_ff (int) : FFN inner dimensionality. | |
| dropout (float): Dropout probability. | |
| """ | |
| def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout: float = 0.1) -> None: | |
| super().__init__() | |
| self.self_attn = MultiHeadAttention(d_model, num_heads, dropout) | |
| self.ff = PositionwiseFeedForward(d_model, d_ff, dropout) | |
| self.norm1 = nn.LayerNorm(d_model) | |
| self.norm2 = nn.LayerNorm(d_model) | |
| self.dropout = nn.Dropout(dropout) | |
| def forward(self, x: torch.Tensor, src_mask: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Args: | |
| x : shape [batch, src_len, d_model] | |
| src_mask : shape [batch, 1, 1, src_len] | |
| Returns: | |
| shape [batch, src_len, d_model] | |
| """ | |
| # Post-LN: every residual addition is immediately followed by LayerNorm. | |
| # LayerNorm's Jacobian rescales gradients. Stacking N=6 of these means the | |
| # gradient is repeatedly rescaled on its way back. Early in training, before | |
| # weights settle, this makes the gradient magnitude at the bottom layers large | |
| # and high-variance β the model can diverge in the first few hundred steps. | |
| # This is exactly why the paper needs the Noam warmup: the LR must start | |
| # near-zero and ramp up slowly, letting the layers stabilize before taking | |
| # big steps. Post-LN without warmup typically blows up. | |
| # sub-layer 1: self-attention + Add & Norm (Post-LN) | |
| attn = self.self_attn(x, x, x, mask=src_mask) | |
| x = self.norm1(x + self.dropout(attn)) | |
| # sub-layer 2: position-wise FFN + Add & Norm | |
| ff = self.ff(x) | |
| x = self.norm2(x + self.dropout(ff)) | |
| return x | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # DECODER LAYER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class DecoderLayer(nn.Module): | |
| """ | |
| Single Transformer decoder sub-layer: | |
| x β [Masked Self-Attn β Add & Norm] | |
| β [Cross-Attn(memory) β Add & Norm] | |
| β [FFN β Add & Norm] | |
| Args: | |
| d_model (int) : Model dimensionality. | |
| num_heads (int) : Number of attention heads. | |
| d_ff (int) : FFN inner dimensionality. | |
| dropout (float): Dropout probability. | |
| """ | |
| def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout: float = 0.1) -> None: | |
| super().__init__() | |
| self.self_attn = MultiHeadAttention(d_model, num_heads, dropout) # masked self-attn | |
| self.cross_attn = MultiHeadAttention(d_model, num_heads, dropout) # attends to encoder | |
| self.ff = PositionwiseFeedForward(d_model, d_ff, dropout) | |
| self.norm1 = nn.LayerNorm(d_model) | |
| self.norm2 = nn.LayerNorm(d_model) | |
| self.norm3 = nn.LayerNorm(d_model) | |
| self.dropout = nn.Dropout(dropout) | |
| def forward( | |
| self, | |
| x: torch.Tensor, | |
| memory: torch.Tensor, | |
| src_mask: torch.Tensor, | |
| tgt_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """ | |
| Args: | |
| x : shape [batch, tgt_len, d_model] | |
| memory : Encoder output, shape [batch, src_len, d_model] | |
| src_mask : shape [batch, 1, 1, src_len] | |
| tgt_mask : shape [batch, 1, tgt_len, tgt_len] | |
| Returns: | |
| shape [batch, tgt_len, d_model] | |
| """ | |
| # 1. masked self-attention over decoder inputs (causal + pad) | |
| sa = self.self_attn(x, x, x, mask=tgt_mask) | |
| x = self.norm1(x + self.dropout(sa)) | |
| # 2. cross-attention: Q from decoder, K/V from encoder memory | |
| ca = self.cross_attn(x, memory, memory, mask=src_mask) | |
| x = self.norm2(x + self.dropout(ca)) | |
| # 3. position-wise FFN | |
| ff = self.ff(x) | |
| x = self.norm3(x + self.dropout(ff)) | |
| return x | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ENCODER & DECODER STACKS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Encoder(nn.Module): | |
| """Stack of N identical EncoderLayer modules with final LayerNorm.""" | |
| def __init__(self, layer: EncoderLayer, N: int) -> None: | |
| super().__init__() | |
| self.layers = nn.ModuleList([copy.deepcopy(layer) for _ in range(N)]) | |
| self.norm = nn.LayerNorm(layer.norm1.normalized_shape[0]) | |
| def forward(self, x: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Args: | |
| x : shape [batch, src_len, d_model] | |
| mask : shape [batch, 1, 1, src_len] | |
| Returns: | |
| shape [batch, src_len, d_model] | |
| """ | |
| for layer in self.layers: | |
| x = layer(x, mask) | |
| return self.norm(x) | |
| class Decoder(nn.Module): | |
| """Stack of N identical DecoderLayer modules with final LayerNorm.""" | |
| def __init__(self, layer: DecoderLayer, N: int) -> None: | |
| super().__init__() | |
| self.layers = nn.ModuleList([copy.deepcopy(layer) for _ in range(N)]) | |
| self.norm = nn.LayerNorm(layer.norm1.normalized_shape[0]) | |
| def forward( | |
| self, | |
| x: torch.Tensor, | |
| memory: torch.Tensor, | |
| src_mask: torch.Tensor, | |
| tgt_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """ | |
| Args: | |
| x : shape [batch, tgt_len, d_model] | |
| memory : shape [batch, src_len, d_model] | |
| src_mask : shape [batch, 1, 1, src_len] | |
| tgt_mask : shape [batch, 1, tgt_len, tgt_len] | |
| Returns: | |
| shape [batch, tgt_len, d_model] | |
| """ | |
| for layer in self.layers: | |
| x = layer(x, memory, src_mask, tgt_mask) | |
| return self.norm(x) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FULL TRANSFORMER | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # cache so the checkpoint + vocab are fetched/built only once even if | |
| # Transformer() is constructed many times | |
| _PRETRAINED_CACHE: dict = {} | |
| class Transformer(nn.Module): | |
| """ | |
| Full Encoder-Decoder Transformer for sequence-to-sequence tasks. | |
| Args: | |
| src_vocab_size (int) : Source vocabulary size. | |
| tgt_vocab_size (int) : Target vocabulary size. | |
| d_model (int) : Model dimensionality (default 512). | |
| N (int) : Number of encoder/decoder layers (default 6). | |
| num_heads (int) : Number of attention heads (default 8). | |
| d_ff (int) : FFN inner dimensionality (default 2048). | |
| dropout (float): Dropout probability (default 0.1). | |
| """ | |
| # trained weights live on Drive; gdown fetches them in __init__ | |
| CHECKPOINT_DRIVE_ID = "15X7of0L3awcux6m00UzXlhruuEaRzPUx" | |
| CHECKPOINT_PATH = "checkpoint_baseline.pt" | |
| def __init__( | |
| self, | |
| src_vocab_size: int = 1, | |
| tgt_vocab_size: int = 1, | |
| d_model: int = 512, | |
| N: int = 6, | |
| num_heads: int = 8, | |
| d_ff: int = 2048, | |
| dropout: float = 0.1, | |
| checkpoint_path: str = None, | |
| ) -> None: | |
| super().__init__() | |
| self._build(src_vocab_size, tgt_vocab_size, d_model, N, | |
| num_heads, d_ff, dropout) | |
| self._weights_loaded = False | |
| # Transformer() with default vocab sizes (or an explicit | |
| # checkpoint_path) means inference: load weights/vocab/tokenizer | |
| # now so infer() does no I/O and stays under the time limit. | |
| inference_mode = (checkpoint_path is not None) or ( | |
| src_vocab_size == 1 and tgt_vocab_size == 1 | |
| ) | |
| if inference_mode: | |
| self._load_pretrained(checkpoint_path or self.CHECKPOINT_PATH) | |
| def _build(self, src_vocab_size, tgt_vocab_size, d_model, N, | |
| num_heads, d_ff, dropout) -> None: | |
| """(Re)construct all submodules. Called from __init__, and again from | |
| _load_pretrained once the checkpoint's true config is known.""" | |
| self.d_model = d_model | |
| self.config = { | |
| "src_vocab_size": src_vocab_size, | |
| "tgt_vocab_size": tgt_vocab_size, | |
| "d_model": d_model, | |
| "N": N, | |
| "num_heads": num_heads, | |
| "d_ff": d_ff, | |
| "dropout": dropout, | |
| } | |
| self.src_embed = nn.Embedding(src_vocab_size, d_model) | |
| self.tgt_embed = nn.Embedding(tgt_vocab_size, d_model) | |
| self.pos_enc = PositionalEncoding(d_model, dropout) | |
| self.encoder = Encoder(EncoderLayer(d_model, num_heads, d_ff, dropout), N) | |
| self.decoder = Decoder(DecoderLayer(d_model, num_heads, d_ff, dropout), N) | |
| self.generator = nn.Linear(d_model, tgt_vocab_size) | |
| # Xavier-uniform init on every matrix (Transformer is init-sensitive) | |
| for p in self.parameters(): | |
| if p.dim() > 1: | |
| nn.init.xavier_uniform_(p) | |
| def _load_pretrained(self, path: str) -> None: | |
| """Fill the cache once (download weights, build vocab/tokenizer), | |
| then rebuild this instance from it. Called from __init__.""" | |
| cache = _PRETRAINED_CACHE | |
| if not cache: | |
| if not os.path.exists(path): | |
| import gdown # lazy: not in requirements.txt | |
| gdown.download(id=self.CHECKPOINT_DRIVE_ID, output=path, | |
| quiet=True) | |
| ckpt = torch.load(path, map_location="cpu") | |
| cache["config"] = ckpt.get("model_config") if isinstance(ckpt, dict) else None | |
| cache["state"] = (ckpt.get("model_state_dict", ckpt) | |
| if isinstance(ckpt, dict) else ckpt) | |
| import dataset as ds | |
| base = ds.Multi30kDataset("train") # rebuild the same vocab | |
| cache["src_vocab"] = base.src_vocab | |
| cache["tgt_vocab"] = base.tgt_vocab | |
| cache["nlp_de"] = ds.Multi30kDataset._nlp_de | |
| cache["ds"] = ds | |
| if cache["config"]: | |
| device = next(self.parameters()).device | |
| self._build(**cache["config"]) | |
| self.to(device) | |
| self.load_state_dict(cache["state"]) | |
| self.src_vocab = cache["src_vocab"] | |
| self.tgt_vocab = cache["tgt_vocab"] | |
| self._nlp_de = cache["nlp_de"] | |
| self._ds = cache["ds"] | |
| self.eval() | |
| self._weights_loaded = True | |
| self._warmup() | |
| def _warmup(self) -> None: | |
| """Run a few GPU decode steps now so the first real infer() call | |
| doesn't pay the CUDA cold-start. The caller moves the model to its | |
| device after __init__, so move to cuda, warm up, then back to cpu.""" | |
| if not torch.cuda.is_available(): | |
| return | |
| ds = self._ds | |
| try: | |
| self.to("cuda") | |
| with torch.no_grad(): | |
| src = torch.tensor([[ds.SOS_IDX, 5, ds.EOS_IDX]], device="cuda") | |
| sm = make_src_mask(src, ds.PAD_IDX) | |
| mem = self.encode(src, sm) | |
| ys = torch.tensor([[ds.SOS_IDX]], device="cuda") | |
| for _ in range(4): | |
| self.decode(mem, sm, ys, make_tgt_mask(ys, ds.PAD_IDX)) | |
| ys = torch.cat([ys, ys[:, :1]], dim=1) | |
| torch.cuda.synchronize() | |
| except Exception: | |
| pass | |
| finally: | |
| self.to("cpu") # autograder will .to(device) itself | |
| # ββ AUTOGRADER HOOKS ββ keep these signatures exactly βββββββββββββ | |
| def encode( | |
| self, | |
| src: torch.Tensor, | |
| src_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """ | |
| Run the full encoder stack. | |
| Args: | |
| src : Token indices, shape [batch, src_len] | |
| src_mask : shape [batch, 1, 1, src_len] | |
| Returns: | |
| memory : Encoder output, shape [batch, src_len, d_model] | |
| """ | |
| x = self.pos_enc(self.src_embed(src) * math.sqrt(self.d_model)) | |
| return self.encoder(x, src_mask) | |
| def decode( | |
| self, | |
| memory: torch.Tensor, | |
| src_mask: torch.Tensor, | |
| tgt: torch.Tensor, | |
| tgt_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """ | |
| Run the full decoder stack and project to vocabulary logits. | |
| Args: | |
| memory : Encoder output, shape [batch, src_len, d_model] | |
| src_mask : shape [batch, 1, 1, src_len] | |
| tgt : Token indices, shape [batch, tgt_len] | |
| tgt_mask : shape [batch, 1, tgt_len, tgt_len] | |
| Returns: | |
| logits : shape [batch, tgt_len, tgt_vocab_size] | |
| """ | |
| x = self.pos_enc(self.tgt_embed(tgt) * math.sqrt(self.d_model)) | |
| x = self.decoder(x, memory, src_mask, tgt_mask) | |
| return self.generator(x) | |
| def forward( | |
| self, | |
| src: torch.Tensor, | |
| tgt: torch.Tensor, | |
| src_mask: torch.Tensor, | |
| tgt_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """ | |
| Full encoder-decoder forward pass. | |
| Args: | |
| src : shape [batch, src_len] | |
| tgt : shape [batch, tgt_len] | |
| src_mask : shape [batch, 1, 1, src_len] | |
| tgt_mask : shape [batch, 1, tgt_len, tgt_len] | |
| Returns: | |
| logits : shape [batch, tgt_len, tgt_vocab_size] | |
| """ | |
| memory = self.encode(src, src_mask) | |
| return self.decode(memory, src_mask, tgt, tgt_mask) | |
| def infer(self, src_sentence: str) -> str: | |
| """ | |
| Translate a German sentence to English with greedy decoding. | |
| Args: | |
| src_sentence: raw German text. | |
| Returns: | |
| The translated English string. | |
| """ | |
| # weights/vocab/tokenizer already loaded in __init__, so this is | |
| # just compute (no downloads here) | |
| ds = self._ds | |
| device = next(self.parameters()).device | |
| self.eval() | |
| with torch.no_grad(): | |
| tokens = [t.text for t in self._nlp_de.tokenizer(src_sentence.lower())] | |
| ids = [ds.SOS_IDX] + self.src_vocab.encode(tokens) + [ds.EOS_IDX] | |
| src = torch.tensor([ids], dtype=torch.long, device=device) | |
| src_mask = make_src_mask(src, ds.PAD_IDX).to(device) | |
| memory = self.encode(src, src_mask) | |
| ys = torch.full((1, 1), ds.SOS_IDX, dtype=torch.long, device=device) | |
| for _ in range(50): # Multi30k targets are short | |
| tgt_mask = make_tgt_mask(ys, ds.PAD_IDX) | |
| logits = self.decode(memory, src_mask, ys, tgt_mask) | |
| nxt = logits[:, -1, :].argmax(-1, keepdim=True) | |
| ys = torch.cat([ys, nxt], dim=1) | |
| if nxt.item() == ds.EOS_IDX: | |
| break | |
| out = [] | |
| for i in ys[0].tolist()[1:]: # skip <sos> | |
| if i == ds.EOS_IDX: | |
| break | |
| out.append(self.tgt_vocab.lookup_token(i)) | |
| return " ".join(out) | |