الأسبوع 3: جداول احتمالات ثنائية الغرام
🎯 أهداف التعلّم
بنهاية هذا الأسبوع ستكون قادرًا على:
- استخراج ثنائيات الغرام (bigrams) (أزواج كلمات متتالية) من جمل مُجزَّأة.
- بناء قاموس متداخل
dict[str, dict[str, float]]يربط كل كلمة بتوزيع احتمالي على الكلمات التي تليها. - شرح، بمثال، لماذا تلتقط ثنائيات الغرام سياقًا لم يستطع النموذج أحادي الغرام التقاطه.
- التعامل مع مشكلة "السياق غير المرئي": ماذا تفعل عندما لا يكون لكلمة أي متابِعات معروفة إطلاقًا.
الدرس
ثنائيات الغرام: أزواج كلمات متتالية
ثنائي الغرام (bigram) هو زوج من كلمتين متتاليتين. للجملة المُجزَّأة ["the", "cat", "sat"]، ثنائيات الغرام هي ("the", "cat") و("cat", "sat") — زوج واحد لكل موضع متجاور:
def bigrams(tokens):
return [(tokens[i], tokens[i + 1]) for i in range(len(tokens) - 1)]
bigrams(["the", "cat", "sat"])
# [('the', 'cat'), ('cat', 'sat')]
هذا نفس نمط range(len(...) - 1) الذي يظهر كلما احتجت النظر إلى أزواج من الجيران في متتالية — -1 موجودة لأن الكلمة الأخيرة ليس لها كلمة بعدها لتُزاوَج معها. لجملة بها رمزًا، يوجد دائمًا بالضبط ثنائي غرام.
جدول احتمال شرطي
نُقدّر الآن — احتمال الكلمة التالية، بمعطى فقط الكلمة السابقة مباشرة. هذا نموذج ثنائي الغرام: لا يزال بسياق محدود (ذاكرة كلمة واحدة بالضبط)، لكن أكثر بشكل صارم من ذاكرة النموذج أحادي الغرام المعدومة.
بنية البيانات الطبيعية هي قاموس من قواميس: لكل كلمة ، قاموس متداخل يربط كل كلمة تالية ممكنة باحتمالها، مشروطًا بأن تُسبَق بـ :
def bigram_counts(tokenized_sentences):
table = {} # word -> {next_word: count}
for tokens in tokenized_sentences:
for first, second in bigrams(tokens):
if first not in table:
table[first] = {}
table[first][second] = table[first].get(second, 0) + 1
return table
def bigram_probabilities(counts_table):
probs_table = {}
for word, next_counts in counts_table.items():
total = sum(next_counts.values())
probs_table[word] = {w: c / total for w, c in next_counts.items()}
return probs_table
تُعيد bigram_probabilities استخدام نفس فكرة "العدّات ← القسمة على الإجمالي" من to_probabilities الأسبوع الماضي — الفرق الوحيد أنها تُطبَّق بشكل منفصل على صف كل كلمة الخاص بها في الجدول، بما أن لكل كلمة توزيعها الخاص على ما يتبعها. كل قاموس داخلي probs_table[word] يجمع إلى 1 بمفرده، نفس خاصية "المجموع يساوي 1" من الأسبوع الماضي، لكن توزيع واحد لكل كلمة بدلًا من توزيع واحد للمفردات كاملة.
probs_table["the"]
# {'cat': 0.35, 'dog': 0.3, 'mouse': 0.1, 'mat': 0.15, ...}
اقرأ probs_table["the"]["cat"] كـ : بمعطى أن الكلمة السابقة كانت "the"، ما احتمال أن تكون "cat" التالية؟
مشكلة السياق غير المرئي
الكلمة التي تظهر فقط في نهاية جملة لا تبدأ أبدًا ثنائي غرام، لذا هي غائبة ببساطة كمفتاح رئيسي في probs_table — لا يوجد صف لها إطلاقًا، بما أن bigram_counts تضيف مفتاحًا فقط للكلمات التي تظهر كالعنصر الأول في أي ثنائي غرام. هذا مهم جدًا للأسبوع 4، حيث ستحتاج فحص word in probs_table قبل البحث عن أي شيء، بالضبط نفس النمط الدفاعي لفحص وجود مفتاح قبل الفهرسة في قاموس عادي.
def next_word_distribution(word, probs_table):
if word not in probs_table:
return None # هذه الكلمة لا تبدأ أبدًا ثنائي غرام في مجموعة نصوصنا
return probs_table[word]
حالة "النموذج لم يرَ هذا الموقف حرفيًا أبدًا" هي قيد حقيقي لا مفر منه لأي منهج قائم على العدّ — لا يمكنه أبدًا قول شيء إلا عن أنماط لاحظها فعليًا في بيانات التدريب، موضوع سيعود صراحة في الأسبوع 4.
⚠️ أخطاء شائعة
- افتراض أن كل كلمة هي مفتاح رئيسي في
probs_table. فقط الكلمات التي تظهر كالعنصر الأول في ثنائي غرام واحد على الأقل تحصل على صف — انظر "مشكلة السياق غير المرئي" أعلاه. - الخلط بين
probs_table[word]وprobs_table[word][other_word]. الأولى توزيع كامل (قاموس)؛ الثانية احتمال واحد (عدد عشري). نسيان أيهما لديك يؤدي إلى أخطاءTypeErrorمحيّرة لاحقًا. - بناء جدول العدّات وجدول الاحتمالات في نفس المرور. إبقاء
bigram_countsوbigram_probabilitiesكدالتين منفصلتين (بدلًا من دمجهما) يعني أن لديك العدّات الخام متاحة لاحقًا — مفيد للتحقق المنطقي، ولتجربة توقيت الأسبوع 5، التي تهتم بخطوة العدّ تحديدًا.
🧩 تحديات
باستخدام مجموعة نصوص الأسبوع 1، احسب جدول احتمالات ثنائية الغرام. أيهما أرجح ليتبع "the" مباشرة: "cat" أم "dog"؟
أي كلمة في مجموعة النصوص تتبعها أكثر الكلمات الأخرى المختلفة (أي لديها أكبر قاموس داخلي في probs_table)؟
اختر كلمة تظهر فقط كآخر كلمة في جملة بمجموعة النصوص. هل هي مفتاح رئيسي في probs_table؟ لماذا أو لماذا لا، بمعطى كيفية تعريف bigrams()؟
عمّم bigrams(tokens) إلى دالة trigrams(tokens) تُعيد كل الثلاثيات المتتالية من الكلمات. كيف ستعمّمها أكثر إلى دالة ngrams(tokens, n)؟
استخدم next_word_distribution للبحث بأمان عن كلمة حددتها بالفعل في التحدي 3 كأنها لا تبدأ ثنائي غرام أبدًا. تأكد أنها تُعيد None بدلًا من التعطّل.
لكلمة تظهر في كل من counts أحادي الغرام في الأسبوع 2 وbigram_counts هذا الأسبوع، قارن عدّها أحادي الغرام بمجموع قيم صف ثنائي الغرام الخاص بها (sum(bigram_counts[word].values())). هل يجب أن يتطابقا؟ افحص بضع كلمات واشرح أي تباينات صغيرة تجدها.
🤔 أسئلة سقراطية
- ابحث عن
probs_table["the"]وقارنها بـprobsالإجمالية من الأسبوع الماضي. هل هما نفس التوزيع؟ بماذا يخبرك هذا عمّا إذا كانت "the" تغيّر ما هو مرجّح أن يأتي بعدها؟ - نموذج ثلاثي الغرام (يشترط الكلمتين السابقتين) يلتقط سياقًا أكثر من نموذج ثنائي الغرام. بمعطى صغر مجموعة نصوصنا (20 جملة)، ما المشكلة العملية التي تتوقع أن تواجهها عدّات ثلاثي الغرام والتي تتجنبها عدّات ثنائي الغرام غالبًا؟
- تبني
bigram_probabilitiesتوزيعًا احتماليًا كاملًا لكل كلمة في المفردات. إن كانت المفردات تحتوي كلمة فريدة، تقريبًا كم رقمًا يمكن أن يحتويه جدول ثنائي الغرام الكامل في أسوأ الحالات (كل كلمة تتبع كل كلمة أخرى مرة واحدة على الأقل)؟ بماذا يوحي هذا عن كيفية تضخم حجم الجدول مع حجم المفردات؟ - تعني مشكلة السياق غير المرئي أن نموذج ثنائي الغرام يمكن أن يكون صامتًا تمامًا عن كلمات لم يرَ أبدًا أنها تبدأ ثنائي غرام. هل يمكنك التفكير في طريقة تجعل النموذج دائمًا لديه شيء يقوله، حتى لكلمة غير مرئية — ربما بالرجوع لشيء من الأسبوع الماضي؟
- يطلب منك التحدي 6 مقارنة عدّات أحادي الغرام بعدّات ثنائي الغرام المجمّعة. لمعظم الكلمات تتطابق هذه، لكن آخر كلمة في جملة يُنقَص عدّها بشكل منهجي بواحد في نسخة ثنائي الغرام. لماذا واحد بالضبط، ولماذا الكلمة الأخيرة فقط؟