الوحدة 7 — فاكّ الترميز: GPT والتوليد
في الوحدة 6 رأينا أنّ المرمّز يقرأ التسلسل بأكمله في وقت واحد ويُنتج تمثيلات ثنائيّة الاتّجاه. فاكّ الترميز يقلب هذا الحيادَ عمدًا: إنّه يُدرَّب على مهمّة توقّع الجتون التالي، وهذا يفرض قيدًا حاسمًا — لا يجوز لأيّ جتون أن يرى المستقبل. هذا القيد يُفرَض بقناع بسيط اسمه القناع السببي، وهو الفرق الوحيد المعماري بين المرمّز وفاكّ الترميز.
من هذا القيد الصغير خرجت عائلة GPT كلّها، وخرجت بعدها كلّ نماذج التوليد التي نشاهدها اليوم. هذه الوحدة تشرح الميكانيك بيدنا، وتُبيّن كيف نسنقذ ميكانيكيّة التوليد.
القناع السببي: منع رؤية المستقبل
في الانتباه العادي، الجتون في الموقع ينظر إلى جتونات المواقع كلّها من 1 إلى . في فاكّ الترميز، لا يجوز له النظر إلّا إلى المواقع من 1 إلى . تقنيًّا، نُنشئ مصفوفة قناع مثلّثيّة حجمها :
ثمّ نطبّقها على الدرجات قبل softmax، لا بعده:
يجعل softmax ينتج 0 على المواقع الممنوعة، والباقي يُقسم مجموعه على 1. هذا هو كلّ الفرق بين المرمّز وفاكّ الترميز.
def causal_mask(size: int) -> torch.Tensor:
"""يُنتج قناعًا مثلّثيًّا سفليًّا (B=1, 1, size, size)."""
return torch.tril(torch.ones(size, size)).unsqueeze(0).unsqueeze(0)
if __name__ == "__main__":
m = causal_mask(5)
print(m.squeeze())
# tensor([[1., 0., 0., 0., 0.],
# [1., 1., 0., 0., 0.],
# [1., 1., 1., 0., 0.],
# [1., 1., 1., 1., 0.],
# [1., 1., 1., 1., 1.]])
نستعمل هذا القناع مع MultiHeadAttention من الوحدة 3، حيث ترى دالّة forward تنفيذ masked_fill(mask == 0, -inf).
إن نسيتَ القناع، لن يرفع بايتورش أيّ خطأ. سيتدرّب النموذج بشكل ممتاز على مقاييس التدريب — بل سيصل إلى خسائر منخفضة جدًّا، لأنّه ببساطة يغشّ: كلّ جتون يرى الجتون التالي مباشرة قبل أن يتوقّعه. ثمّ عند التوليد، لا يعرف النموذج ماذا يفعل لأنّه لم يتدرّب على مهمّة الاستدلال الحقيقيّة. عرض المشكلة: خسارة تدريب قريبة من الصفر، وتوليد كلام هراء. افحص القناع دائمًا بطباعة إحصاءاته قبل أوّل ركض تدريب.
مهمّة توقّع الجتون التالي
من أجل تسلسل جتونات ، يُدرَّب فاكّ الترميز على توقّع من ، لكلّ . أي أنّ خسارة واحدة تُوزّع على كلّ المواقع في التسلسل، وهذا ما يجعل التدريب فعّالًا بذاته.
الترتيب في الكود:
import torch
import torch.nn as nn
import torch.nn.functional as F
# نفترض التسلسل tokens من (B, n)
# tokens_in = tokens[:, :-1] المُدخَل: بلا آخر جتون
# tokens_out = tokens[:, 1:] الهدف: بلا أوّل جتون
def language_model_loss(logits: torch.Tensor, tokens: torch.Tensor) -> torch.Tensor:
logits_in = logits[:, :-1, :].contiguous() # (B, n-1, V)
targets = tokens[:, 1:].contiguous() # (B, n-1)
return F.cross_entropy(logits_in.view(-1, logits.size(-1)), targets.view(-1))