1import torch
2import torch.nn as nn
3import torch.nn.functional as F
4
5
6class MultiHeadSelfAttention(nn.Module):
7 """The core mechanism from 'Attention Is All You Need'."""
8
9 def __init__(self, d_model: int = 512, n_heads: int = 8):
10 super().__init__()
11 assert d_model % n_heads == 0
12 self.d_k = d_model // n_heads # 64 per head, as in the paper
13 self.n_heads = n_heads
14
15 # Learned projections for Query, Key, Value (+ output mix)
16 self.w_q = nn.Linear(d_model, d_model)
17 self.w_k = nn.Linear(d_model, d_model)
18 self.w_v = nn.Linear(d_model, d_model)
19 self.w_o = nn.Linear(d_model, d_model)
20
21 def forward(self, x: torch.Tensor) -> torch.Tensor:
22 # x: (batch, seq_len, d_model)
23 B, T, C = x.shape
24
25 # 1. Project into Q, K, V and split into heads
26 # (batch, heads, seq_len, d_k)
27 q = self.w_q(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
28 k = self.w_k(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
29 v = self.w_v(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
30
31 # 2. Attention scores: how relevant is every word to every word?
32 # scaled by sqrt(d_k) so softmax stays in a healthy range
33 scores = (q @ k.transpose(-2, -1)) / (self.d_k ** 0.5)
34
35 # 3. Softmax -> attention weights that sum to 1 per word
36 weights = F.softmax(scores, dim=-1)
37
38 # 4. Weighted sum of values: each word gathers information
39 # from the words it attends to
40 context = weights @ v # (B, heads, T, d_k)
41
42 # 5. Merge heads back together and mix
43 context = context.transpose(1, 2).reshape(B, T, C)
44 return self.w_o(context)
45
46
47# --- Try it ---
48attn = MultiHeadSelfAttention(d_model=512, n_heads=8)
49sentence = torch.randn(1, 10, 512) # batch of 1, 10 tokens
50out = attn(sentence)
51print(out.shape) # torch.Size([1, 10, 512])