الوحدة 10 — تنفيذ كتلة Transformer كاملة
بعد تسع وحدات نظريّة وكتلًا مكتوبة بيدنا، وقت الجمع قد حان. هذه الوحدة تجمع كلّ ما بنيناه في نموذج seq2seq مكتمل، ثمّ تُدرّبه أوّلًا على مهمّة لعبة (النسخ المعكوس)، ثمّ على مهمّة أكثر واقعيّة: ترجمة تواريخ من صيغة إلى صيغة، مثلًا «3 مارس 2026» إلى «2026-03-03». وسنعرض خرائط الانتباه التي يتعلّمها لنرى، بأمّ العَين، ما تفعله الرؤوس.
المشروع الموجّه: تجميع نهائي
الكتل الجاهزة من الوحدات 2 إلى 8:
MultiHeadAttention(الوحدة 3)SinusoidalPositionalEncoding(الوحدة 4)FeedForwardوResidualBlock (الوحدة 5)EncoderLayer(الوحدة 5)Seq2SeqDecoderLayer(الوحدة 8)causal_mask(الوحدة 7)
نُركّبها في نموذج Transformer seq2seq كامل ونحفظه في ملفّ transformer.py. الكود بأكمله دون 200 سطر:
import math, torch, torch.nn as nn, torch.nn.functional as F
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.d_model, self.num_heads = d_model, num_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model, bias=False)
self.W_k = nn.Linear(d_model, d_model, bias=False)
self.W_v = nn.Linear(d_model, d_model, bias=False)
self.W_o = nn.Linear(d_model, d_model, bias=False)
def _split(self, x):
B, n, _ = x.shape
return x.view(B, n, self.num_heads, self.d_k).transpose(1, 2)
def _merge(self, x):
B, h, n, d_k = x.shape
return x.transpose(1, 2).contiguous().view(B, n, h * d_k)
def forward(self, q, k, v, mask=None):
Q, K, V = self._split(self.W_q(q)), self._split(self.W_k(k)), self._split(self.W_v(v))
scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
weights = F.softmax(scores, dim=-1)
return self.W_o(self._merge(weights @ V)), weights
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div)
pe[:, 1::2] = torch.cos(position * div)
self.register_buffer("pe", pe.unsqueeze(0))
def forward(self, x):
return x + self.pe[:, : x.size(1), :]
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout):
super().__init__()
self.fc1, self.fc2 = nn.Linear(d_model, d_ff), nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.fc2(self.dropout(F.gelu(self.fc1(x))))
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super().__init__()
self.attn = MultiHeadAttention(d_model, num_heads)
self.ff = FeedForward(d_model, d_ff, dropout)
self.n1, self.n2 = nn.LayerNorm(d_model), nn.LayerNorm(d_model)
self.drop = nn.Dropout(dropout)
def forward(self, x, mask=None):
a, _ = self.attn(self.n1(x), self.n1(x), self.n1(x), mask=mask)
x = x + self.drop(a)
return x + self.drop(self.ff(self.n2(x)))
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.ff = FeedForward(d_model, d_ff, dropout)
self.n1, self.n2, self.n3 = (nn.LayerNorm(d_model) for _ in range(3))
self.drop = nn.Dropout(dropout)
def forward(self, x, memory, causal, src_mask=None):
a, _ = self.self_attn(self.n1(x), self.n1(x), self.n1(x), mask=causal)
x = x + self.drop(a)
c, cross_w = self.cross_attn(self.n2(x), memory, memory, mask=src_mask)
x = x + self.drop(c)
x = x + self.drop(self.ff(self.n3(x)))
return x, cross_w
class Transformer(nn.Module):
def __init__(self, src_vocab, tgt_vocab, d_model=64, num_heads=4,
num_layers=2, d_ff=128, dropout=0.1, max_len=64):
super().__init__()
self.d_model = d_model
self.src_emb = nn.Embedding(src_vocab, d_model)
self.tgt_emb = nn.Embedding(tgt_vocab, d_model)
self.pos = SinusoidalPositionalEncoding(d_model, max_len)
self.enc = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)])
self.dec = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)])
self.norm_e = nn.LayerNorm(d_model)
self.norm_d = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, tgt_vocab, bias=False)
def encode(self, src):
x = self.pos(self.src_emb(src) * math.sqrt(self.d_model))
for layer in self.enc:
x = layer(x)
return self.norm_e(x)
def decode(self, tgt, memory):
n = tgt.size(1)
causal = torch.tril(torch.ones(n, n, device=tgt.device)).view(1, 1, n, n)
x = self.pos(self.tgt_emb(tgt) * math.sqrt(self.d_model))
cross_w = None
for layer in self.dec:
x, cross_w = layer(x, memory, causal)
return self.head(self.norm_d(x)), cross_w
def forward(self, src, tgt):
return self.decode(tgt, self.encode(src))
اختبار وحدة للأشكال
قبل أيّ تدريب، فحص الأشكال يمنع 90% من الأخطاء:
def test_shapes():
model = Transformer(src_vocab=20, tgt_vocab=15, d_model=64,
num_heads=4, num_layers=2, d_ff=128)
src = torch.randint(0, 20, (2, 10)) # حزمة 2، مصدر 10
tgt = torch.randint(0, 15, (2, 8)) # حزمة 2، هدف 8
logits, cross_w = model(src, tgt)
assert logits.shape == (2, 8, 15), f"logits: {logits.shape}"
assert cross_w.shape == (2, 4, 8, 10), f"cross: {cross_w.shape}"
print("جميع الأشكال صحيحة.")
if __name__ == "__main__":
test_shapes()
عند الركض نتحقّق: مخرجات ومصفوفة الانتباه المتقاطع . أيّ اختلاف يعني عطلًا في view أو transpose أو ضربًا خاطئًا.