الفصل 2: الانتباه الذاتي والانتباه متعدد الرؤوس
المقدمة
بناءً على فهمنا لآلية الانتباه، يستكشف هذا الفصل كيفية تطبيق الانتباه عندما ينتبه تسلسل إلى نفسه (الانتباه الذاتي) ولماذا يؤدي استخدام رؤوس متعددة للانتباه بشكل متوازٍ إلى تحسين قدرة النموذج بشكل كبير.
الانتباه الذاتي: جوهر المحولات
ما هو الانتباه الذاتي؟
في الانتباه الذاتي، ينتبه التسلسل إلى نفسه. يمكن لكل موضع في المدخلات أن ينتبه إلى كل موضع آخر في نفس المدخلات.
المدخلات: "القط جلس على السجادة"
الانتباه الذاتي: كل كلمة تنتبه إلى جميع الكلمات
├─ "ال" تنتبه إلى ["ال", "قط", "جلس", "على", "ال", "سجادة"]
├─ "قط" تنتبه إلى ["ال", "قط", "جلس", "على", "ال", "سجادة"]
├─ "جلس" تنتبه إلى ["ال", "قط", "جلس", "على", "ال", "سجادة"]
└─ ... وهكذا
لماذا الانتباه الذاتي؟
يمكن الانتباه الذاتي من:
- الاعتماد على المدى الطويل: اتصالات مباشرة بين أي موضعين
- المعالجة المتوازية: يتم حساب جميع المواضع في وقت واحد
- السياق الديناميكي: أوزان مختلفة لمدخلات مختلفة
الصيغة الرياضية
بالنسبة للانتباه الذاتي، تأتي Q و K و V جميعها من نفس المدخلات:
التنفيذ
import torch
import torch.nn as nn
import math
class SelfAttention(nn.Module):
"""
طبقة الانتباه الذاتي حيث تأتي Q و K و V جميعها من نفس المدخلات.
"""
def __init__(self, d_model, d_k=None):
super().__init__()
d_k = d_k or d_model
self.W_Q = nn.Linear(d_model, d_k, bias=False)
self.W_K = nn.Linear(d_model, d_k, bias=False)
self.W_V = nn.Linear(d_model, d_k, bias=False)
self.d_k = d_k
def forward(self, x, mask=None):
"""
x: موتر المدخلات (batch, seq_len, d_model)
mask: قناع الانتباه الاختياري
"""
# جميع الإسقاطات من نفس المدخلات
Q = self.W_Q(x)
K = self.W_K(x)
V = self.W_V(x)
# حساب درجات الانتباه
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention_weights = torch.softmax(scores, dim=-1)
output = torch.matmul(attention_weights, V)
return output, attention_weights
مقارنة أنواع الانتباه
الانتباه الذاتي مقابل الانتباه المتقاطع
| النوع | مصدر Q | مصدر K, V | حالة الاستخدام |
|---|---|---|---|
| الانتباه الذاتي | المدخلات X | المدخلات X | المشفر، المفكك |
| الانتباه المتقاطع | المفكك | المشفر | نماذج المشفر-المفكك |
مثال على الانتباه المتقاطع
class CrossAttention(nn.Module):
"""
الانتباه المتقاطع حيث تأتي Q من مصدر واحد، و K و V من مصدر آخر.
يستخدم في هياكل المشفر-المفكك.
"""
def __init__(self, d_model, d_k=None):
super().__init__()
d_k = d_k or d_model
self.W_Q = nn.Linear(d_model, d_k, bias=False)
self.W_K = nn.Linear(d_model, d_k, bias=False)
self.W_V = nn.Linear(d_model, d_k, bias=False)
self.d_k = d_k
def forward(self, query_input, key_value_input, mask=None):
"""
query_input: مصدر Q (مثل حالة المفكك)
key_value_input: مصدر K, V (مثل مخرجات المشفر)
"""
Q = self.W_Q(query_input)
K = self.W_K(key_value_input)
V = self.W_V(key_value_input)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention_weights = torch.softmax(scores, dim=-1)
output = torch.matmul(attention_weights, V)
return output, attention_weights
الانتباه متعدد الرؤوس
الدافع
يمكن لرأس انتباه واحد فقط التقاط نوع واحد من العلاقات. يقوم الانتباه متعدد الرؤوس بتشغيل عمليات الانتباه المتعددة بشكل متوازٍ، مما يسمح للنموذج بالانتباه بشكل مشترك إلى المعلومات من فضاءات تمثيل مختلفة.
الفكرة
يمكن للرؤوس المختلفة تعلم أنماط مختلفة:
- الرأس 1: العلاقات النحوية (الموضوع-الفعل)
- الرأس 2: العلاقات الموضعية (الكلمات القريبة)
- الرأس 3: العلاقات الدلالية (المرادفات)
- الرأس 4: الإحالة (الضمائر إلى الأسماء)
التعريف الرياضي
حيث كل رأس هو:
أبعاد المعلمات
بالنسبة لـ رؤوس مع بُعد النموذج :
- كل رأس له بُعد
- تبقى المعلمات الإجمالية تقريبًا كما هي في الانتباه ذو الرأس الواحد
# مثال: d_model = 512, h = 8
d_model = 512
num_heads = 8
d_k = d_model // num_heads # 64 لكل رأس
التنفيذ الكامل
class MultiHeadAttention(nn.Module):
"""
آلية الانتباه متعدد الرؤوس.
"""
def __init__(self, d_model, num_heads, dropout=0.1):
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
# الإسقاطات الخطية لـ Q, K, V (جميع الرؤوس مجتمعة)
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)
self.dropout = nn.Dropout(dropout)
def split_heads(self, x, batch_size):
"""
تقسيم البُعد الأخير إلى (num_heads, d_k).
تحويل إلى الشكل (batch, num_heads, seq_len, d_k)
"""
x = x.view(batch_size, -1, self.num_heads, self.d_k)
return x.transpose(1, 2)
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
# الإسقاطات الخطية
Q = self.W_Q(query)
K = self.W_K(key)
V = self.W_V(value)
# تقسيم إلى رؤوس
Q = self.split_heads(Q, batch_size) # (batch, heads, seq, d_k)
K = self.split_heads(K, batch_size)
V = self.split_heads(V, batch_size)
# الانتباه بالنقاط المتدرجة لجميع الرؤوس بشكل متوازٍ
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
# توسيع القناع لأبعاد الرؤوس
if mask.dim() == 3:
mask = mask.unsqueeze(1)
scores = scores.masked_fill(mask == 0, float('-inf'))
attention_weights = torch.softmax(scores, dim=-1)
attention_weights = self.dropout(attention_weights)
# تطبيق الانتباه على القيم
context = torch.matmul(attention_weights, V)
# دمج الرؤوس
context = context.transpose(1, 2).contiguous()
context = context.view(batch_size, -1, self.d_model)
# الإسقاط الخطي النهائي
output = self.W_O(context)
return output, attention_weights
استخدام الانتباه متعدد الرؤوس
# مثال على الاستخدام
d_model = 512
num_heads = 8
seq_len = 100
batch_size = 32
mha = MultiHeadAttention(d_model, num_heads)
# المدخلات (نفسها للانتباه الذاتي)
x = torch.randn(batch_size, seq_len, d_model)
# الانتباه الذاتي
output, attn_weights = mha(x, x, x)
print(f"شكل المخرجات: {output.shape}") # (32, 100, 512)
print(f"شكل الانتباه: {attn_weights.shape}") # (32, 8, 100, 100)
فوائد التوازي
لماذا تتدرب المحولات بسرعة
على عكس RNNs، يمكن حساب جميع المواضع في المحول في وقت واحد:
# RNN: تسلسلي (بطيء)
for t in range(seq_len):
h_t = rnn_cell(x[t], h_{t-1})
# المحول: متوازي (سريع)
output = multi_head_attention(x, x, x) # جميع المواضع دفعة واحدة
كفاءة وحدة معالجة الرسومات
الانتباه متعدد الرؤوس مناسب بشكل خاص لوحدة معالجة الرسومات:
# جميع الرؤوس محسوبة في عملية ضرب مصفوفة مجمعة واحدة
# الشكل: (batch, heads, seq, d_k) @ (batch, heads, d_k, seq)
# → (batch, heads, seq, seq)
scores = torch.matmul(Q, K.transpose(-2, -1)) # متوازي عبر جميع الرؤوس!
تصور الرؤوس المتعددة
def visualize_multi_head_attention(attention_weights, tokens, num_heads_to_show=4):
"""
تصور أنماط الانتباه من رؤوس متعددة.
attention_weights: (batch, heads, seq, seq)
"""
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, num_heads_to_show, figsize=(4*num_heads_to_show, 4))
for head_idx in range(num_heads_to_show):
ax = axes[head_idx]
attn = attention_weights[0, head_idx].detach().numpy() # العنصر الأول في الدفعة
im = ax.imshow(attn, cmap='Blues')
ax.set_xticks(range(len(tokens)))
ax.set_yticks(range(len(tokens)))
ax.set_xticklabels(tokens, rotation=45, ha='right')
ax.set_yticklabels(tokens)
ax.set_title(f'الرأس {head_idx + 1}')
plt.tight_layout()
plt.show()
أنماط الانتباه في الممارسة
أظهرت الأبحاث أن الرؤوس المختلفة تتعلم أنماطًا متخصصة:
| نوع النمط | ما يلتقطه | مثال |
|---|---|---|
| قطري | نفس الموضع / الهوية | الرمز ينتبه إلى نفسه |
| الرمز السابق | العلاقات التسلسلية | أنماط نموذج اللغة |
| الرمز الأول | التجميع العالمي | رمز CLS في BERT |
| نحوي | هيكل القواعد | توافق الموضوع-الفعل |
| دلالي | العلاقات المعنوية | المرادفات، الأضداد |
النقاط الرئيسية
- الانتباه الذاتي يسمح لكل موضع بالانتباه إلى جميع المواضع في نفس التسلسل
- الانتباه المتقاطع يمكن من الانتباه إلى تسلسل مختلف (المشفر-المفكك)
- الانتباه متعدد الرؤوس يشغل عمليات الانتباه المتعددة بشكل متوازٍ
- الرؤوس المختلفة تتعلم أنواعًا مختلفة من العلاقات
- التوازي يجعل المحولات فعالة على وحدات معالجة الرسومات
نظرة مستقبلية
في الفصل التالي، سنستكشف كيف تتعامل المحولات مع المعلومات الموضعية من خلال الترميز الموضعي، وكيف يتم تمثيل الرموز من خلال التضمينات.