بعد Generative AI overview، نخطو خطوة أعمق في النماذج اللغوية: ما الذي تفعله بالضبط، وكيف تحسب الرمز التالي. هذا الدرس يضع الأساس الذي يقوم عليه كل درس لاحق في هذه الموجة.
ما هو Language Model؟
Language Model = نموذج يأخذ سياقًا نصّيًا ويُرجع توزيع احتمالي للرمز الذي يأتي بعده.
input (سياق): "العاصمة الرسمية لفرنسا هي"
output (distribution): {
"باريس": 0.62,
"مدينة": 0.08,
"لندن": 0.03,
...
}
من هذا التوزيع يُختار رمز واحد (ليس دائمًا الأعلى احتمالًا)، ويُضاف إلى السياق، وتتكرّر العملية:
خطوة 1: "العاصمة الرسمية لفرنسا هي" → P(.)
خطوة 2: "العاصمة الرسمية لفرنسا هي باريس" → P(.)
خطوة 3: "العاصمة الرسمية لفرنسا هي باريس ." → P(.)
... حتّى بلوغ حدّ الإيقاف.
هذه الحلقة تُسمّى autoregressive generation (توليد ذاتي الانحدار): كل رمز يُولَّد يعتمد على كل ما سبقه.
Tokens — لا كلمات
النموذج لا يرى "كلمات" كما يراها البشر. يرى رموزًا (tokens) ينتجها tokenizer خاصّ بالنموذج. راجع درس Tokenization في NLP للتفاصيل الكاملة حول الخوارزميات، لكن الفكرة الأساسية هنا:
النصّ البشري: "أحبّ البرمجة"
تمثيل رمزيّ محتمل: ["أحب", "ّ", "البر", "مجة"] # subwords
أو ["أحب", "البرمجة"] # whole words
أو ["أ", "ح", "ب", " ", "ا", ...] # characters
عدد الرموز يختلف عن عدد الكلمات — ولا توجد قاعدة عامة للتحويل بينهما. للتحقّق الدقيق، استخدم tokenizer الـ checkpoint الفعلية.
المفردات (Vocabulary)
Vocabulary = مجموعة كل الرموز التي يعرفها النموذج.
- حجم المفردات يختلف: من عشرات الآلاف (نماذج قديمة) إلى مئويات الآلاف (نماذج حديثة).
- كل رمز له معرّف عددي فريد (ID). النموذج يعمل على IDs، لا على نصوص.
- كل ID يُربط بـ embedding متعلَّم أثناء التدريب (ارجع لـ Embeddings في NLP).
vocab = {"<unk>": 0, "<pad>": 1, "<bos>": 2, ..., "مرحبا": 12345, ...}
من السياق إلى الاحتمالات — الخطوات
1. Tokenization
تحويل النصّ إلى IDs:
"العاصمة لفرنسا" → ["ال", "عاصمة", " ل", "فر", "نسا"] → [421, 9012, 18, 7531, 9012]
2. Embedding + Contextualization
كل ID يُحوَّل إلى متجه، ثم تمرّ هذه المتجهات عبر طبقات النموذج (Transformer blocks) فيتغيّر تمثيل كل رمز بناءً على سياقه (الدرس 68: معمارية Transformers يشرح هذه المعمارية).
3. Logits
في الموضع الأخير، يُنتج النموذج متجها بحجم المفردات، كل عنصر فيه = "نتيجة خام" (logit) لكل رمز ممكن:
import numpy as np
# مخرج وهمي بعد طبقة أخيرة (حجمه = حجم المفردات، مثلاً 50000)
logits = np.array([2.1, -0.5, 1.8, ..., 0.3])
vocab_size = len(logits)
4. Softmax — تحويل الدرجات إلى احتمالات
Softmax تأخذ logits وتحوّلها إلى توزيع احتمالي (كل القيم بين 0 و 1، ومجموعها 1):
def softmax(logits):
# لتفادي overflow نطرح max
shifted = logits - np.max(logits)
exp = np.exp(shifted)
return exp / np.sum(exp)
probs = softmax(logits)
# probs.sum() == 1.0, كل عنصر ∈ [0, 1]
logits → softmax → probabilities
[2.1, -0.5, 1.8] → [0.65, 0.05, 0.43 / sum]
↓
[0.57, 0.04, 0.39] # مجموعها 1
5. اختيار الرمز التالي
من التوزيع probs، يُختار رمز واحد. عدة استراتيجيات:
- Greedy: الأعلى احتمالًا (الأبسط، لكن قد يكون متكرّرًا).
- Sampling: عيّنة عشوائية من التوزيع.
- Top-k / Top-p: تقييد العيّنة بأعلى k رمز أو بتراكم احتمال p.
التفاصيل في درس Decoding و Sampling.
6. التكرار
الرمز المختار يُضاف إلى السياق، وتتكرّر العملية من جديد حتى بلوغ:
- رمز إيقاف خاص (
<eos>)، أو - حدّ أقصى لعدد الرموز المولَّدة (
max_new_tokens).
مثال تعليمي صغير — ليس LLM حقيقي
هذا المثال يوضّح الفكرة، لا يعطي نموذجًا حقيقيًا. يتعلّم من نصّ عربي صغير جدًا:
import re
from collections import defaultdict, Counter
# 1. corpus صغير جدًا (للتوضيح فقط)
corpus = [
"أحبّ البرمجة",
"أحبّ القراءة",
"أحبّ التعلّم",
]
def simple_tokenize(text):
return re.findall(r"\S+", text) # تجزئة بالمسافات (تعليمية فقط)
tokens_per_sentence = [simple_tokenize(s) for s in corpus]
# 2. تعلّم bigram distribution: P(الكلمة التالية | السابقة)
transitions = defaultdict(Counter)
for sent in tokens_per_sentence:
for prev, curr in zip(sent, sent[1:]):
transitions[prev][curr] += 1
# 3. P("التعلّم" | "أحبّ")
def prob(next_word, prev_word):
counter = transitions.get(prev_word)
if not counter:
return 0.0
total = sum(counter.values())
return counter[next_word] / total
print(prob("التعلّم", "أحبّ")) # ~0.33
print(prob("البرمجة", "أحبّ")) # ~0.33
print(prob("القراءة", "أحبّ")) # ~0.33
النتيجة: عند البدء بـ"أحبّ"، التوزيع الاحتمالي للكلمة التالية يقسّم الاحتمال بين "البرمجة"، "القراءة"، "التعلّم" (bigram model بدائية).
حدود المثال:
- لا يستخدم embeddings أو transformer.
- لا يفهم معانيًا، فقط تكرار أنماط إحصائية ضيّقة.
- corpus صغير جدًا يعطي توزيعات متفرّقة.
نماذج LLM الحقيقية تستخدم مليارات المعاملات وtrillions من الرموز في التدريب، فتتعلّم أنماطًا أعمق بكثير. لكن الفكرة الجوهرية واحدة: حساب P(التالي | السياق) ثم تكرار.
التدريب مقابل الاستدلال
التدريب (Training): يتعلّم النموذج معاملاته من corpus ضخم. تكلفة عالية، مرّة واحدة عادةً. لا تفعلها بنفسك للنماذج الكبيرة.
الاستدلال (Inference): تستخدم النموذج المدرَّب لتوليد مخرجات. تكلفة لكل طلب، تتكرّر مع كل استخدام.
Training : corpus → model parameters (تعديل أوزان)
Inference : context → trained model → output (الأوزان ثابتة)
درس 103: Pre-training و Instruction Tuning يفصّل مراحل التدريب.
أخطاء شائعة
- "الرمز التالي = الكلمة التالية": خطأ — راجع Tokenization درس 81.
- "Softmax = دالة تفعيل مثل ReLU": لا، ReLU تضيف عدم خطية. Softmax تحوّل logits إلى احتمالات (مهمة مختلفة).
- "Greedy يعطي أفضل نتيجة دائمًا": ليس بالضرورة. غالبًا يولّد نصًّا متكرّرًا. Sampling مع قيود أفضل في مهام إبداعية.
- "النموذج يختار الكلمة الأعلى احتمالًا دائمًا": ليس في النماذج الحديثة — يستخدم sampling مع temperature و top-p.
الخطوات التالية
- كيف تعمل النماذج اللغوية الكبيرة — كيف تتجمّع هذه الأفكار في ملايين المعاملات في بنية Transformer.
- Context Windows و Tokens — كيف يُدار الحجم المحدود للسياق.