كتلة المحوّل
درس 4 من 8
4كتلة المحوّل
الصورة الكبيرةالمحوّل ليس إلا كتلة واحدة مكرّرة عشرات المرات. كل كتلة فيها أربع قطع: انتباه برؤوس متعدّدة، وشبكة أمامية صغيرة، وتطبيع طبقي قبل كل منهما، ووصلة متبقية تجمع المدخل مع الناتج. افهم الكتلة، وفهمت البنية كلها.
ماذا تشاهد في الرسم: تابع الرمز وهو يمرّ بالمحطات الأربع: الانتباه وحده (المحاط بخط متقطّع) يخلط الرموز ببعضها، والباقي يعمل على كل رمز منفردًا. ولأن كل نصف يُضاف إلى x بدل أن يستبدله، يخرج الشكل كما دخل فتُكدّس الكتل بلا حدّ.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
كتلة المحوّل مثل محطة في خط تجميع تتكرر عشرات المرات. في كل محطة خطوتان: الكلمات تتشاور فيما بينها (الانتباه)، ثم تفكر كل كلمة وحدها فيما سمعته (التغذية الأمامية). وبين الخطوات طرق جانبية تحفظ ما كان قبلها حتى لا يضيع.
الكتلة: انتباه متعدد الرؤوس multi-head attention يرى أنماطًا مختلفة بالتوازي، ثم شبكة تغذية أمامية MLP لكل رمز. حول كل جزء وصلة متبقية x + f(x) وتطبيع طبقة LayerNorm، وهما ما يسمح بتكديس عشرات الكتل دون أن يتلاشى الميل.
قبل أن تبدأ
👀الانتباه
تستطيع حساب softmax(Q Kᵀ / √d) V بيدك على جملة صغيرة.
⚖️الطبقات والتطبيع
تعرف التطبيع بالدفعة، فتفهم لماذا يختلف عنه التطبيع الطبقي الذي يعمل على رمز واحد.
الفكرة الجوهرية
في هذا الدرس تبني الكتلة قطعة قطعة: تطبيع الطبقة، ثم الرؤوس المتعدّدة، ثم الشبكة الأمامية، ثم الوصلة المتبقية التي تجمعها كلها.
1تطبيع الطبقة
تطبيع الطبقة يأخذ متّجه كل رمز وحده ويجعل متوسطه صفرًا وانتشاره واحدًا. هكذا لا تنفجر الأرقام ولا تتلاشى وهي تعبر عشرات الكتل.
import numpy as np
np.set_printoptions(precision=2, suppress=True)
x = np.array([[2.0, 4.0, 6.0, 8.0],
[100.0, 101.0, 99.0, 100.0]]) # two tokens on very different scales
def layer_norm(x, eps=1e-5):
mean = x.mean(axis=-1, keepdims=True)
var = x.var(axis=-1, keepdims=True)
return (x - mean) / np.sqrt(var + eps)
y = layer_norm(x)
print(y)
print("mean per token:", y.mean(axis=-1) + 0.0)
print("std per token: ", y.std(axis=-1))
رمزان، واحد قيمه بين 2 و8 وآخر قرب 100. بعد التطبيع كلاهما بمتوسط 0 وانحراف 1، وبقي فقط شكل كل منهما: الأول متصاعد، والثاني فيه قمة وقاع. في nn.LayerNorm يوجد أيضًا معاملان متعلَّمان يعيدان تكبير الناتج وإزاحته إن احتاج النموذج.
2الرؤوس المتعدّدة
رأس واحد يعطي خليطًا واحدًا. حين نقسم العرض بين عدّة رؤوس، يتعلّم كل رأس نوعًا مختلفًا من العلاقات بنفس التكلفة تقريبًا.
الرؤوس المتعدّدة ليست مصفوفات منفصلة، بل إعادة تشكيل لنفس المصفوفة.
import numpy as np
T, d_model, heads = 5, 8, 2
d_head = d_model // heads
q = np.arange(T * d_model, dtype=float).reshape(T, d_model)
split = q.reshape(T, heads, d_head).transpose(1, 0, 2)
print("one sequence:", q.shape)
print("split into heads:", split.shape, "-> (heads, tokens, d_head)")
print("head 0 sees columns 0..3 of token 0:", split[0, 0])
print("head 1 sees columns 4..7 of token 0:", split[1, 0])
back = split.transpose(1, 0, 2).reshape(T, d_model)
print("joined back equals the original:", bool((back == q).all()))
reshape ثم transpose يحوّلان (tokens, d_model) إلى (heads, tokens, d_head)، فيحسب كل رأس انتباهه على ربع الأعمدة الخاص به، ثم نعكس العملية لنجمع النتائج. هذا السطر بالضبط موجود في كل تنفيذ للمحوّل، وأخطاء الترتيب فيه من أشهر أسباب نماذج «تتدرّب» ولا تتعلّم.
انظر إلى الرسم: كل رأس يرى نصف أعمدة كل رمز فقط، ويحسب انتباهه عليها، ثم تُضمّ النتائج إلى العرض الكامل.
3الشبكة الأمامية
بعد الانتباه تمرّ كل كلمة وحدها في شبكة صغيرة: طبقة توسّع العرض من d إلى 4d، ودالة تفعيل، وطبقة تعيده إلى d. الانتباه هو المكان الوحيد الذي تتبادل فيه الرموز المعلومات، والشبكة الأمامية تعالج كل رمز بما جمعه.
انظر إلى الرسم: الانتباه فيه 4 × d² معامل، والأمامية 8 × d². المجموع 12 × d²، وهي القاعدة التي تقدّر بها حجم أي محوّل.
4الوصلة المتبقية: x + f(x)
الكتلة لا تستبدل مدخلها، بل تضيف إليه تغييرًا. لذلك يعبر التدرّج عشرات الطبقات دون أن يتلاشى، والشكل يبقى كما هو فنكدّس أي عدد من الكتل.
كل القطع معًا: تطبيع ثم انتباه بأربعة رؤوس وقناع ثم جمع، وتطبيع ثم شبكة أمامية ثم جمع.
import numpy as np
rng = np.random.default_rng(1)
T, d, heads, d_ff = 6, 16, 4, 64
dh = d // heads
def layer_norm(x):
return (x - x.mean(-1, keepdims=True)) / np.sqrt(x.var(-1, keepdims=True) + 1e-5)
def softmax(z):
z = z - z.max(-1, keepdims=True)
e = np.exp(z)
return e / e.sum(-1, keepdims=True)
def init(*shape):
return rng.normal(0, 0.02, size=shape)
Wqkv, Wo = init(d, 3 * d), init(d, d)
W1, W2 = init(d, d_ff), init(d_ff, d)
mask = np.triu(np.ones((T, T), dtype=bool), 1)
def attention(x):
q, k, v = np.split(x @ Wqkv, 3, axis=-1)
q, k, v = (m.reshape(T, heads, dh).transpose(1, 0, 2) for m in (q, k, v))
s = q @ k.transpose(0, 2, 1) / np.sqrt(dh)
s[:, mask] = -np.inf
out = softmax(s) @ v # (heads, T, dh)
return out.transpose(1, 0, 2).reshape(T, d) @ Wo
def feed_forward(x):
return np.maximum(0, x @ W1) @ W2
def block(x):
x = x + attention(layer_norm(x)) # residual around attention
x = x + feed_forward(layer_norm(x)) # residual around the MLP
return x
x = rng.normal(size=(T, d))
y = block(x)
params = Wqkv.size + Wo.size + W1.size + W2.size
print("in:", x.shape, " out:", y.shape)
print("parameters in one block:", params)
print("how far the block moved x:", round(float(np.abs(y - x).mean() / np.abs(x).mean()), 3))
الشكل لا يتغيّر من (6, 16) إلى (6, 16)، ولهذا تستطيع تكديس أي عدد من الكتل. و3072 معاملًا لكتلة بعرض 16 تعني نحو 12 × d²، وهي القاعدة التي ستستخدمها لتقدير حجم أي محوّل. ولاحظ أن الكتلة حرّكت x بنحو 1% فقط: بأوزان ابتدائية صغيرة تكاد الكتلة تكون «لا شيء» بفضل الوصلة المتبقية، وهذا ما يجعل البداية مستقرّة.
f(x) بدل x = x + f(x). كتلة أو كتلتان تعملان، لكن نموذجًا عميقًا يتوقّف عن التعلّم لأن التدرّج يضيع في الطريق.كيف يعمل، خطوة بخطوة: كيف يمرّ رمز عبر كتلة واحدة
يُطبَّع متّجه الرمز إلى متوسط 0 وانحراف 1 عبر أبعاده.
يدخل الانتباه متعدّد الرؤوس، فيجمع معلومات من الرموز السابقة، ويُضاف الناتج إلى المدخل الأصلي.
يُطبَّع من جديد ويدخل الشبكة الأمامية التي توسّع العرض من d إلى 4d ثم تعيده إلى d، دون النظر إلى الرموز الأخرى.
يُضاف ناتج الشبكة الأمامية إليه مرة ثانية، ويخرج بنفس الشكل ليدخل الكتلة التالية.
أعمق قليلًا
ترتيب القطع مهم. المحوّل الأصلي وضع التطبيع بعد الجمع (Post-norm)، والنماذج الحديثة كلها تضعه قبل الانتباه والشبكة (Pre-norm)، لأن المسار المتبقي يبقى نظيفًا من المدخل إلى المخرج، فيصير تدريب النماذج العميقة أكثر استقرارًا ويحتاج تسخينًا أقل لمعدّل التعلّم.
مثال محلول
أبسط القطع أولًا: التطبيع الطبقي (Layer norm). يأخذ متّجه كل رمز على حدة ويجعل متوسطه صفرًا وانحرافه واحدًا، أيًّا كان مداه الأصلي. هذا يمنع الأرقام من الانفجار أو التلاشي وهي تمرّ عبر عشرات الكتل.
٢. الفرق عن تطبيع الدفعة
تطبيع الدفعة (درس الطبقات) يحسب المتوسط عبر أمثلة الدفعة، فيتغيّر سلوكه بين التدريب والاستخدام ويتأثّر بحجم الدفعة. التطبيع الطبقي يحسب عبر أبعاد الرمز الواحد، فكل رمز مستقل، وهذا ما يناسب تسلسلات بأطوال مختلفة.
مثال: الكتلة جاهزة في PyTorch
import torch
import torch.nn as nn
block = nn.TransformerEncoderLayer(d_model=512, nhead=8, dim_feedforward=2048,
batch_first=True, norm_first=True)
x = torch.randn(2, 10, 512)
mask = nn.Transformer.generate_square_subsequent_mask(10)
print(block(x, src_mask=mask, is_causal=True).shape) # torch.Size([2, 10, 512])
print(sum(p.numel() for p in block.parameters())) # 3,152,384
norm_first=True هو Pre-norm. والعدد 3,152,384 قريب من 12 × 512² = 3,145,728، والفرق هو الانحيازات ومعاملات التطبيع. في درس «بناء محوّل صغير» نكتب الكتلة بأنفسنا بدل هذه الجاهزة، حتى ترى كل سطر.
12 × عدد الكتل × d² تقريبًا، زائد جدول التضمين المفردات × d. نموذج بـ32 كتلة وعرض 4096 فيه نحو 6.4 مليار معامل في الكتل وحدها. جرّبها على أي نموذج تعرف حجمه.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| الرؤوس الإضافية تضيف معاملات انتباه. | الرؤوس تقسّم العرض نفسه: 8 رؤوس في عرض 512 تعني 64 لكل رأس، والمصفوفات بنفس الحجم. |
| الانتباه أكبر جزء في الكتلة. | الشبكة الأمامية تحمل نحو ثلثي معاملات الكتلة: في مثال NumPy أخذت 2,048 من 3,072. |
| الوصلة المتبقية تفصيل اختياري. | بدونها يمرّ التدرّج عبر عشرات التحويلات المتتالية فيتلاشى، ولا تكاد الطبقات الأولى تتعلّم. |
جرّب بنفسك
نموذج بعرض 512 و8 رؤوس. ما عرض كل رأس؟
512 ÷ 8 = 64. الرؤوس تقسّم العرض ولا تضاعفه، ولهذا لا تزيد الرؤوس الإضافية عدد معاملات الانتباه.
في أي قطعة من الكتلة تتبادل الرموز المعلومات فيما بينها؟
الشبكة الأمامية والتطبيع يعملان على كل رمز منفردًا، والوصلة المتبقية جمع للرمز مع نفسه. الانتباه وحده يخلط بين مواضع مختلفة.
بطاقات مراجعة سريعة
x = x + f(x): الكتلة تضيف تعديلًا إلى مدخلها بدل أن تستبدله، فيمرّ التدرّج عبر عشرات الطبقات دون أن يتلاشى.تحقق من فهمك
ماذا يحدث لتدريب محوّل من 48 كتلة إذا حذفت الوصلات المتبقية؟
يصير التدرّج مضطرًّا للمرور عبر 48 تحويلًا متتاليًا، فيتلاشى أو ينفجر، وتتوقّف الطبقات الأولى عن التعلّم عمليًّا. الوصلة المتبقية تعطيه «طريقًا سريعًا» مباشرًا من الخسارة إلى كل طبقة. هذه نفس فكرة ResNet في الرؤية.
لماذا تحرّك الكتلة في مثال NumPy المدخلَ بنحو 1% فقط؟
لأن الأوزان الأولية صغيرة (انحراف 0.02)، فما تضيفه الكتلة إلى x قريب من الصفر. بفضل الوصلة المتبقية تبدأ الكتلة شبه «لا شيء» وتتعلّم تدريجيًّا ما تضيفه.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي1
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.