Transformers and Modern NLP for Engineers

Move the learner into current AI engineering territory with strong transformer fundamentals.

Every frontier AI model - GPT-4, Claude, Gemini, LLaMA, Mistral, Stable Diffusion, Whisper - is built on the Transformer architecture. Understanding Transformers is not optional for an ML engineer in 2025. It is the prerequisite for everything: understanding LLM capabilities and failure modes, fine-tuning, prompt engineering, RAG, agents, and system design.

This module covers the Transformer architecture with the depth required to build, modify, and debug Transformer-based systems.

The Core Mechanism: Self-Attention

The Transformer's fundamental operation is self-attention: each token attends to every other token in the sequence, producing a context-weighted representation.

For a sequence of token embeddings packed into matrix XRn×dX \in \mathbb{R}^{n \times d}:

Attention(Q,K,V)=softmax ⁣(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^T}{\sqrt{d_k}}\right) V

Where Q=XWQQ = XW_Q, K=XWKK = XW_K, V=XWVV = XW_V are linear projections. The dk\sqrt{d_k} scaling prevents dot products from growing too large and pushing softmax into saturation.

python
import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadSelfAttention(nn.Module): def __init__(self, d_model: int, n_heads: int, dropout: float = 0.1): super().__init__() assert d_model % n_heads == 0 self.d_model = d_model self.n_heads = n_heads self.d_k = d_model // n_heads self.q_proj = nn.Linear(d_model, d_model) self.k_proj = nn.Linear(d_model, d_model) self.v_proj = nn.Linear(d_model, d_model) self.out_proj = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout) def forward(self, x: torch.Tensor, mask: torch.Tensor = None): B, T, C = x.shape Q = self.q_proj(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2) K = self.k_proj(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2) V = self.v_proj(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2) scores = (Q @ K.transpose(-2, -1)) / (self.d_k ** 0.5) if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf')) attn = self.dropout(F.softmax(scores, dim=-1)) out = (attn @ V).transpose(1, 2).contiguous().view(B, T, C) return self.out_proj(out)

Multiple heads allow the model to attend to different aspects of the input simultaneously - one head might attend to syntactic relationships, another to coreference, another to semantic similarity.

The Full Transformer Block

Each Transformer layer wraps attention with residual connections, layer normalization, and a position-wise feed-forward network:

python
class TransformerBlock(nn.Module): def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1): super().__init__() self.attn = MultiHeadSelfAttention(d_model, n_heads, dropout) self.ff = nn.Sequential( nn.Linear(d_model, d_ff), nn.GELU(), nn.Dropout(dropout), nn.Linear(d_ff, d_model), nn.Dropout(dropout), ) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) def forward(self, x: torch.Tensor, mask: torch.Tensor = None): x = x + self.attn(self.norm1(x), mask) # Pre-norm (modern standard) x = x + self.ff(self.norm2(x)) return x

Pre-norm (normalize before the sub-layer) vs. post-norm (normalize after): pre-norm produces more stable training gradients and is standard in all modern models (GPT, LLaMA, Mistral).

Positional Encoding

Self-attention is permutation-invariant - it has no notion of token order. Positional encoding adds position information to the token embeddings before the first layer.

Sinusoidal encoding (original Transformer): fixed patterns based on sine/cosine waves at different frequencies. Generalizes to longer sequences than seen during training.

Rotary Position Embedding (RoPE): encodes position in the rotation of Q and K vectors rather than additive offsets. Used in LLaMA, Mistral, GPT-NeoX. Enables better length generalization and relative position sensitivity.

python
# Learned positional embeddings (GPT-2 style) - simple and effective class GPTEmbeddings(nn.Module): def __init__(self, vocab_size: int, d_model: int, max_len: int = 2048, dropout: float = 0.1): super().__init__() self.token_emb = nn.Embedding(vocab_size, d_model) self.pos_emb = nn.Embedding(max_len, d_model) self.dropout = nn.Dropout(dropout) def forward(self, input_ids: torch.Tensor): T = input_ids.shape[1] positions = torch.arange(T, device=input_ids.device).unsqueeze(0) return self.dropout(self.token_emb(input_ids) + self.pos_emb(positions))

Using HuggingFace Transformers in Production

In practice, you use pretrained Transformer models rather than implementing your own. HuggingFace Transformers is the standard library:

python
from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch model_name = "distilbert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) texts = ["This product is amazing!", "The worst experience I have ever had."] inputs = tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits predictions = logits.argmax(dim=-1) print(predictions) # tensor([1, 0]) → positive, negative

Key AutoModel variants to know:

  • AutoModelForSequenceClassification - text classification, sentiment
  • AutoModelForTokenClassification - NER, POS tagging
  • AutoModelForQuestionAnswering - extractive QA
  • AutoModelForCausalLM - text generation (GPT-style)
  • AutoModelForSeq2SeqLM - summarization, translation (T5-style)

Tokenization: What Actually Happens to Your Text

python
tokenizer = AutoTokenizer.from_pretrained("gpt2") text = "The quick brown fox" tokens = tokenizer.encode(text) print(tokens) # [464, 2068, 7586, 21831] print(tokenizer.decode(tokens)) # "The quick brown fox" # BPE (Byte-Pair Encoding): "un##familiar" splits into ["un", "familiar"] # Rare words become multi-token sequences - "brewYourAgent" might be 4+ tokens # This affects context length accounting and cost estimation

Understanding tokenization matters for: counting context window usage, understanding why multi-lingual text is longer, debugging truncation bugs, and knowing why some code/math prompts consume unexpectedly many tokens.

Attention Patterns: What They Mean

Attention weights reveal what the model is "looking at" when computing each token's representation. Visualizable with:

python
from transformers import AutoModel import matplotlib.pyplot as plt outputs = model(**inputs, output_attentions=True) attn = outputs.attentions # tuple of (batch, heads, seq, seq) per layer # Plot layer 6, head 0 plt.imshow(attn[6][0, 0].detach().numpy(), cmap='viridis')

Common patterns: heads attending to previous tokens (causal LM), to [CLS] token (classification), to syntactically related words (NLP), to diagonal (position-local context).

Common Mistakes and Bad Instincts

Ignoring the attention_mask in batched inference. When padding sequences to the same length, you must pass attention_mask to tell the model which tokens are real vs. padding. Without it, the model attends to padding tokens, producing corrupted representations. HuggingFace tokenizers return this mask by default - do not drop it.

Using BERT for text generation. BERT is an encoder-only model trained with masked language modeling. It is excellent for classification, NER, and embeddings - not for generating text. GPT-style causal LMs (GPT-2, LLaMA) are the right choice for generation tasks.

Truncating prompts without thinking about what you cut. When input exceeds max_length, HuggingFace truncates from the end by default. For tasks where the answer is at the end of the document (e.g., summarization), this discards the most relevant content. Use truncation_side='left' or implement sliding window chunking.

Where to Go Next

  • llm-product-engineering: apply Transformer understanding to build reliable LLM-powered product features
  • retrieval-systems-vector-databases-and-rag: use Transformer encoders to produce embeddings for retrieval
  • fine-tuning-adaptation-and-when-not-to-fine-tune: fine-tune pretrained Transformers using LoRA and PEFT

Module 15 of 34 · Software Engineer to ML/AI Engineer

Related Posts

More posts

Fine-Tuning and Post-Training: LoRA, SFT, DPO, and Reasoning RL

What actually happens after pretraining, and when you should do any of it yourself. Parameter-efficient fine-tuning with LoRA, supervised fine-tuning data, preference optimization, and the reinforcement learning recipe behind reasoning models, with a decision framework and a project you can run on one GPU.

#fine-tuning#post-training#rl#reasoning-models#huggingface#llm

LLM Context Windows: What They Mean for System Design

Context window size shapes every architectural decision in LLM applications. This post covers how to reason about context allocation, the limits that still matter even with large windows, and the patterns that scale.

#llm#system-design#transformers

Common ML Architectures Reference: CNN, RNN, Transformer, MoE

A concise technical reference for the neural network architectures that power modern ML - what each one does, how it works, when to use it, and what to watch out for.

#cnn#reference#moe#deep-learning#rnn#transformer