انتقل إلى المحتوى الرئيسي

الوحدة 3 — الانتباه متعدّد الرؤوس

انتهت الوحدة السابقة إلى انتباه ذي رأس واحدة: يستقبل تسلسلًا، ويُنتج تمثيلًا جديدًا لكلّ جتون بالنظر إلى الآخرين. يعمل هذا الترتيب، لكنّه يعاني قيدًا ضمنيًّا: رأس واحدة تلتقط علاقة واحدة. رأس متخصّص في المطابقة النحوية لا يستطيع، في الوقت ذاته، أن يلتقط الاقتران الدلالي البعيد. والاختيار بين هذَين قرار خاسر.

الحلّ الذي طرحه Transformer ليس أن نُوسّع الرأس، بل أن نُشغّل عدّة رؤوس على التوازي بأبعاد أصغر لكلّ منها، ثمّ نُدمج مخرجاتها. هذا ما تُسمّيه الورقة الانتباه متعدّد الرؤوس، وهو ما سنبنيه في هذه الوحدة كلبنة ثانية في المشروع الموجّه.

الفكرة الحسابية: تقسيم البُعد لا زيادته

الحلّ الساذج سيكون تشغيل رأسَين بأبعاد d=512d = 512 لكلّ منهما. لكنّ ذلك يُضاعف كلفة المصفوفات WQ,WK,WVW_Q, W_K, W_V بعدد الرؤوس، ويفجّر الحسابات. الاختيار الأنيق للمعمارية أن نُقسّم البُعد الأصلي:

dk=dv=dmodelhd_k = d_v = \frac{d_{\text{model}}}{h}

حيث hh عدد الرؤوس. مثال قياسي: dmodel=512d_{\text{model}} = 512 وh=8h = 8، فتصبح لكلّ رأس dk=64d_k = 64. المخرجات جميعها لها مقاس 6464؛ يُصفّ الجدع بينها فيُنتج متّجهًا واحدًا مقاسه 8×64=5128 \times 64 = 512، أي بالضبط dmodeld_{\text{model}} من جديد. الكلفة الحسابية الإجمالية قريبة جدًّا من كلفة رأس واحدة بمقاس 512، لكنّ التنوّع مفتوح.

هذه الضربة الذكيّة تظهر جليّة في العدّ:

كمّيةرأس واحدة، d=512d = 512ثمان رؤوس، dk=64d_k = 64
معاملات WQ,WK,WVW_Q, W_K, W_V لرأس3×512×512=7864323 \times 512 \times 512 = 786\,4323×512×64=983043 \times 512 \times 64 = 98\,304
الإجمالي على كلّ الرؤوس786432786\,4328×98304=7864328 \times 98\,304 = 786\,432
إسقاط الإخراج WOW_O512×512=262144512 \times 512 = 262\,144512×512=262144512 \times 512 = 262\,144

الأعداد متطابقة، لكنّ الأولى تتعلّم علاقة واحدة، والثانية ثمانيًا. المشكلة الوحيدة المُنشأة إسقاط الإخراج WOW_O الذي يخلط ما تعلّمته الرؤوس. لكن نموذج بغير هذا الإسقاط يبقى ثمانية رؤوس متجاورة لا تلتقي، وهو ما يُفسد الغاية.

ما تتعلّمه الرؤوس عمليًّا

الأنماط التي تُظهرها خرائط الانتباه المُدرَّبة معروفة الآن بفضل أعمال Clark وآخرين على BERT (2019). لكنّها ليست مبرمجة: بل تنبثق من التدريب.

  • رأس التركيب النحوي: تُنتج وزنًا عاليًا بين الفعل وفاعله، أو بين الاسم وصفته.
  • رأس مرجع الضمير: تُنشئ رابطًا بين ضمير و مرجعه في الجملة السابقة.
  • رأس الجتون الحالي: تُركّز على الجتون نفسه، فتُشبه هوية طفيفة.
  • رأس الجتون التالي: تنقل معلومة الجتون التالي إلى الحالي، مما يُشبه شبكة تكرارية مصغّرة.
  • رؤوس بلا تفسير مبسّط: أغلبها. النموذج يوزّع مهامًا لا يمكن دائمًا تسميتها بلغة نحويّة.

هذا التنوّع هو الرد على السؤال «لماذا لا نكتفي برأس؟»: لأنّ اللغة تحوي عدّة علاقات متزامنة، وتخصيص رأس لكلّ نوع منها موازٍ يجعل الطبقة تُلخّص جانبًا أكثر ثراءً من التسلسل.

الحساب مصفوفيًّا

لنكتب الصيغة الكاملة. من أجل رأس ii:

headi=Attention(QWQ(i),KWK(i),VWV(i))\text{head}_i = \text{Attention}(QW_Q^{(i)}, KW_K^{(i)}, VW_V^{(i)})

ثمّ نجمع الرؤوس ونُطبّق الإسقاط النهائي:

MultiHead(Q,K,V)=Concat(head1,,headh)WO\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)\, W_O

هذه الصياغة عزيزة على القلب، لكنّ التنفيذ الفعّال يتحاشى الحلقة على الرؤوس. الحيلة أن نُنشئ Q,K,VQ, K, V بأبعاد (B,n,dmodel)(B, n, d_{\text{model}})، ثمّ نُعيد تشكيلها إلى (B,n,h,dk)(B, n, h, d_k) ونُبدّل المحاور لتصبح (B,h,n,dk)(B, h, n, d_k). عندئذ يُطبَّق ضرب المصفوفات على البُعدَين الأخيرين، وكلّ الرؤوس تُحسَب في ضربة واحدة.

التنفيذ ببايتورش

هذه هي الكتلة الثانية من مشروعنا الموجّه. سنمتنع عمدًا عن nn.MultiheadAttention لنُبيّن الميكانيك بالكامل، لكنّه ما يقوم به بايتورش خلف الكواليس:

import torch
import torch.nn as nn
import torch.nn.functional as F


class MultiHeadAttention(nn.Module):
def __init__(self, d_model: int, num_heads: int):
super().__init__()
assert d_model % num_heads == 0, "d_model يجب أن يقبل القسمة على num_heads"
self.d_model = d_model
self.num_heads = 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: torch.Tensor) -> torch.Tensor:
# (B, n, d_model) -> (B, h, n, d_k)
B, n, _ = x.shape
return x.view(B, n, self.num_heads, self.d_k).transpose(1, 2)

def _merge(self, x: torch.Tensor) -> torch.Tensor:
# (B, h, n, d_k) -> (B, n, d_model)
B, h, n, d_k = x.shape
return x.transpose(1, 2).contiguous().view(B, n, h * d_k)

def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
mask: torch.Tensor | None = None) -> torch.Tensor:
Q = self._split(self.W_q(q))
K = self._split(self.W_k(k))
V = self._split(self.W_v(v))

scores = Q @ K.transpose(-2, -1) / (self.d_k ** 0.5) # (B, h, n, n)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
weights = F.softmax(scores, dim=-1)

context = weights @ V # (B, h, n, d_k)
return self.W_o(self._merge(context))


if __name__ == "__main__":
torch.manual_seed(0)
mha = MultiHeadAttention(d_model=64, num_heads=8)
x = torch.randn(2, 10, 64) # حزمة 2، 10 جتونات، بُعد 64
y = mha(x, x, x) # انتباه ذاتي
print(y.shape) # torch.Size([2, 10, 64])

انتبه إلى ثلاث نقاط عمليّة. الأولى، أنّنا استقبلنا q,k,vq, k, v منفصلين، لأنّ الانتباه المتقاطع (وحدة 8) سيمرّر منفصلين أيضًا. أمّا في الانتباه الذاتي فنُمرّر نفس xx ثلاث مرّات. الثانية، أنّ القناع (وحدة 7) يُطبَّق على الدرجات لا على الأوزان، لأنّ تطبيقه بعد softmax لا يُعيد التوزيع إلى مجموع 1. الثالثة، أنّ contiguous() قبل view ضرورية بعد transpose، وإلّا رفعت view استثناء لأنّ الذاكرة لم تعد متعاقبة.

الكلفة في المعاملات وفي الحوسبة

لنُقيّم ما نُدفعه ثمنًا للتعدّد:

  • المعاملات: أربع مصفوفات dmodel×dmodeld_{\text{model}} \times d_{\text{model}}، أي 4dmodel24d_{\text{model}}^2. من أجل dmodel=512d_{\text{model}} = 512: 4×2621441,054 \times 262\,144 \approx 1{,}05 مليون معامل لكلّ كتلة انتباه.
  • الحوسبة: تقريبًا O(n2dmodel)O(n^2 d_{\text{model}}) أساسًا بسبب حساب الدرجات QKQ K^{\top}. سنعود إلى هذه الكلفة التربيعية في الوحدة 9 لأنّها العائق الأكبر أمام السياقات الطويلة.
  • الذاكرة: مصفوفة الأوزان (B,h,n,n)(B, h, n, n) هي المستهلك الرئيسي. حزمة 32، ثمانية رؤوس، 1024 جتون تستهلك وحدها 32 × 8 × 1024 × 1024 × 4 بايت 1,07\approx 1{,}07 جيغابايت في float32.
عدد الرؤوس ليس عبثيًّا

مضاعفة الرؤوس لا تُحسّن الأداء تلقائيًّا. في الأوراق الجادّة يُلاحَظ أنّ أكثر من ثمانية إلى ستّة عشر رأسًا يبدأ في تفتيت الأبعاد لدرجة يفشل معها كلّ رأس في التقاط شيء ذي معنى. وتظهر أوراق تشذيب (pruning) تجرّبت حذف حتى نصف الرؤوس المدرَّبة دون خسارة تُذكر، بل أحيانًا مع تحسّن طفيف. القاعدة العملية: اقتَنِ hh من مجموعة {4,8,12,16}\{4, 8, 12, 16\} حسب dmodeld_{\text{model}}، ولا تُضاعف دون قياس.

ما نُضيفه إلى المشروع الموجّه

نُثبّت MultiHeadAttention كوحدة قابلة لإعادة الاستعمال. ستحلّ محلّ SingleHeadAttention في كلّ الكتل اللاحقة، مع الحفاظ على الواجهة نفسها. وستُستدعى بثلاث صيغ مختلفة عبر الدورة:

  • انتباه ذاتي في المرمّز (الوحدة 6): mha(x, x, x).
  • انتباه ذاتي مقنَّع في فاكّ الترميز (الوحدة 7): mha(x, x, x, mask=causal_mask).
  • انتباه متقاطع بين المرمّز وفاكّ الترميز (الوحدة 8): mha(dec, enc, enc).

هذه الأشكال الثلاثة بنية واحدة بمدخلات مختلفة. وهذا التوحيد هو أحد أسباب أناقة Transformer: كلّ التنوّع الوظيفي يقع في من يتحدّث إلى من، لا في تعديل الآلية الأساسية.

في الخلاصة

  • الرأس الواحدة لا تلتقط إلّا نوعًا واحدًا من العلاقات؛ تعدّد الرؤوس يُوازِن عدّة أنواع بشكل موازٍ.
  • تقسيم البُعد dk=dmodel/hd_k = d_{\text{model}}/h يُبقي الكلفة الإجمالية شبه ثابتة مقارنة برأس واحدة كبيرة، ويفتح الباب لتنوّع الأدوار.
  • الإسقاط النهائي WOW_O ضروري لخلط مخرجات الرؤوس؛ بغيره تبقى الرؤوس معزولة.
  • التنفيذ الفعّال يُعيد تشكيل التنسور إلى (B,h,n,dk)(B, h, n, d_k) ليَحسب كلّ الرؤوس في ضربة واحدة، ويُعيدها في النهاية إلى (B,n,dmodel)(B, n, d_{\text{model}}) بعملية عكسية.

الوحدة التالية: نلاحظ أنّ الانتباه لا يعرف الترتيب، فنُدخل ترميز الموضع؛ ونرى لماذا أزاح الشكل الدوّار الشكل المطلق في النماذج الحديثة.