الوحدة 4 — ترميز الموضع: المطلق ثمّ الدوّار
بعد الوحدتَين السابقتَين، نملك كتلة انتباه متعدّدة الرؤوس تعمل. ولكن لو أعطيتَ هذه الكتلة الجملة «القطّ يأكل السمكة» وأعدتَ ترتيبها إلى «السمكة تأكل القطّ»، فسوف تُعطيك النموذج بالضبط نفس المخرَج. الانتباه بطبيعته متبادل الترتيب (equivariant to permutation): يعامل التسلسل كمجموعة، لا كسلسلة. وهذا لا يصلح، بلا شكّ، للغة أو للسلاسل الزمنية.
الحلّ الذي طرحه Vaswani: نُدخل معلومة الموضع قبل الانتباه، في التمثيل نفسه. فيرى الانتباه جتونًا يقول ضمنيًّا «أنا في المرتبة الثالثة». هذه الوحدة تُعرّي الآليّة، وتُقدّم بديلها الحديث الذي أصبح افتراضًا في نماذج 2023-2026: ترميز الموضع الدوّار RoPE.
لماذا لا ي عرف الانتباه الترتيب
الحسابات التي شرحناها في الوحدة 2 صريحة في هذا الشأن. من أجل كلّ زوج جتونات ، نُحسب ، ثمّ نجمع بأوزان softmax. لا شيء في هذه الصيغة يعتمد على موقع أو في التسلسل. لو بدّلت أسطر مصفوفة وأسطر مصفوفة و بالتبديل نفسه، تحصل على نفس المصفوفة الناتجة مبدّلة الأسطر. أي أنّ الشبكة تُعامل التسلسل كـكيس من الجتونات — bag of tokens بالإنجليزيّة.
الشبكة التكرارية لم تكن تعاني هذه المشكلة لأنّ ترتيب الحساب نفسه يحمل الترتيب. أمّا هنا فقد فُقد هذا الملحق مجّانًا لصالح الموازاة. لا بدّ من إعادة إدخاله يدويًّا.
الترميز الجيبي المطلق
الحلّ الأوّل في ورقة 2017 كان صناعة متّجه موضع لكلّ موقع ثمّ جمعه إلى تمثيل الجتون:
والمتّجه بُني بصيغة تحليلية تعتمد على الدوال المثلّثية:
لماذا الجيب وجيب التمام بالتحديد؟ لثلاثة أسباب مترابطة. أوّلًا، القيم محصورة في ، فلا تُخلّ باستقرار الانتباه. ثانيًا، ترددات مختلفة على أبعاد مختلفة تعطي كلّ موقع بصمة فريدة. ثالثًا والأهمّ، الفرق بين موقعَين و يمكن التعبير عنه بواسطة دوران خطّي يعتمد على فقط: أي أنّ الشبكة تستطيع تعلّم علاقات نسبيّة (مثل «الجتون التالي») باستقلال عن الموقع المطلق.
خذ مثالًا: ، للموقع 0 نحصل على . للموقع 1 على . تظهر البصمة سريعًا.
البديل: مواضع مُتعلَّمة
طرحت أوراق لاحقة بديلًا أبسط: جدول مواضع مُتعلَّم، تمامًا كجدول تضمين الكلمات. nn.Embedding(max_len, d_model) يُعطي لكلّ موقع متّجهًا يُحدّده التدريب. الأداء على مقاييس التصنيف يكاد يتطابق مع الجيبي.
المفاضلة عمليّة:
- الجيبي: لا معاملات إضافيّة، يعمل خارج نطاق طول التدريب. جملة أطول ممّا رأت الشبكة أثناء التدريب تولّد ترميزات موضع صالحة (ولو أنّ التعميم يبقى محدودًا).
- المُتعلَّم: أخفّ من الجيبي عمليًّا، لكنّه يعجز أمام أطوال لم يرَها. الموقع 1024 غير موجود في الجدول إن كان التدريب على 512.
BERT مثلًا اعتمد المُتعلَّم بحدّ أقصى 512؛ ومن هنا شهرته بعجزه عن السياقات الطويلة. GPT-2 اعتمد أيضًا مُتعلَّمًا. أمّا Transformer الأصلي فاختار الجيبي بالتحديد لأنّه توقّع الحاجة إلى التوسيع.
الترميز الدوّار RoPE
في 2021 أَخرج Su وآخرون في ورقة RoFormer صيغة تركيبية جميلة، أصبحت افتراض النماذج الحديثة (LLaMA وMistral وGemma وQwen بأنسخة عديدة). الفكرة تقلب المسألة: بدلًا من جمع موضع إلى التمثيل، نُدخل الموضع داخل حساب الانتباه نفسه.
في RoPE، يُطبَّق على كلّ زوج مكوّنات من و دوران في المستوى بزاوية تتناسب مع الموضع. المذهل أنّ الجداء النقطي بين الملفوف بالدوران و الملفوف بالدوران يعطي:
أي أنّ الجداء يعتمد على الفرق فقط، لا على الموقع المطلق. هذا يعني أنّ الشبكة تكتسب طبيعيًّا خاصّية الترجمة الموضعيّة: علاقة بين جتونين تبقى نفسها إذا انزلق كلاهما بالمقدار نفسه في السياق.
عمليًّا، RoPE:
- لا يضيف معاملات، تمامًا كالجيبي.
- يُحقن داخل طبقة الانتباه، لا يُجمع في المدخل. النموذج يبقى عارفًا بالموقع في كلّ طبقة.
- يعمم على أطوال أكبر من التدريب بشكل جيّد نسبيًّا، وهو ما جعله مفتاح النماذج التي تدّعي 32k جتونًا وأكثر.
- توسّعات RoPE (YaRN، NTK-aware، Linear scaling) تُطيل السياق أبعد بضبط ترددات الدوران.
تنفيذ الجيبي ببايتورش
هذه الكتلة الثالثة في المشروع الموجّه:
import math
import torch
import torch.nn as nn
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, d_model: int, max_len: int = 5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# buffer وليس معامل: لا يُدرَّب لكنّه ينتقل مع النموذج إلى GPU
self.register_buffer("pe", pe.unsqueeze(0))
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: (B, n, d_model)
n = x.size(1)
return x + self.pe[:, :n, :]
if __name__ == "__main__":
pe = SinusoidalPositionalEncoding(d_model=64, max_len=100)
x = torch.randn(2, 20, 64)
y = pe(x)
print(y.shape) # torch.Size([2, 20, 64])
print((y - x)[0, 0, :4]) # القطعة الأولى من الموقع 0
نستعمل register_buffer عوض nn.Parameter، لأنّ هذه المصفوفة ليست قابلة للتعلّم. لكنّ Windows حال الفارق: تنتقل مع النموذج إلى المسرّع، وتُحفَظ في state_dict، لكنّها لا تحضر في optimizer.parameters().
تراكم أدلّة تجريبية منذ 2022 يُشير إلى أنّ RoPE أفضل من الجيبي على أغلب المهمّات، ومن المُتعلَّم على السياقات المتوسطة والطويلة. إن كنت تُطلق نموذجًا جديدًا اليوم، ابدأ بRoPE واحفظ الجيبي لدواعي التوافق مع نماذج قديمة، والمُتعلَّم لسيناريوهات هجينة نادرة. القاعدة تختلف عن 2018.
نصائح تنفيذ عملية
قبل أن تتّجه إلى الوحدة التالية، عدّة قواعد اختبار قابلة للتذكّر:
- إن كان طول تسلسل الاختبار أكبر من
max_len،peسترفع خطأ فهرسة. لا تنسَ ضبطه على أقصى طول متوقَّع. - جمعت الموضع إلى التمثيل، أم دُلّت الأبعاد الأولى منه بالموضع؟ الطريقة الأولى معتمدة في Vaswani، وهي الأشيع اليوم.
- في RoPE، لا يجب الجمع في المُدخَل. إن رأيت مستودعًا يفعل ذلك بجانب RoPE، فيغلب أنّه خطأ.
- ترميز الموضع يُطبَّق قبل الانتباه الذاتي في كلّ طبقة. في الترميز المتقاطع (وحدة 8)، فاكّ الترميز يستعمل ترميز موضعه، والمرمّز موضعه؛ وليست هناك حاجة لمشاركتهما.
في الخلاصة
- الانتباه يجهل الترتيب بطبيعته؛ لولا ترميز الموضع لتصرّفت الشبكة مع التسلسل ككيس من الجتونات.
- الترميز الجيبي يقدّم بصمة موضع تحليلية، بلا معاملات، وتُعمّم إلى أطوال لم تُشاهَد؛ الترميز المُتعلَّم أخفّ لكنّه ينهار على الأطوال الجديدة.
- RoPE يُدخل الموضع داخل الانتباه نفسه، ويُنتج طبيعيًّا اعتمادًا على الفرق ؛ وهو افتراض النماذج المفتوحة الرائجة اليوم.
- التنفيذ العملي بسيط:
register_bufferللجيبي، أو حقن دوران في لRoPE؛ الاختيار عمليّ وليس مبدئيًّا.
الوحدة التالية: نغلّف كتلة الانتباه بالوصلات المتبقّية والتسوية الطبقية، وهما ما يجعل تدريب Transformer ممكنًا على أعماق كبيرة.