الأسبوع 4: توليد النصوص بالمعاينة العشوائية (Sampling)
🎯 أهداف التعلّم
بنهاية هذا الأسبوع ستكون قادرًا على:
- شرح الفرق بين اختيار الكلمة التالية الأرجح دائمًا والمعاينة (sampling) من التوزيع.
- استخدام
random.choicesلمعاينة كلمة تالية موزونة باحتمالات ثنائي الغرام. - كتابة دالة
generate_text()تُنتج جملة جديدة، كلمة تلو الأخرى، من جدول ثنائي الغرام. - جعل التوليد العشوائي قابلًا لإعادة الإنتاج بـ
random.seed، عندما يكون ذلك مفيدًا.
الدرس
طريقتان لاختيار الكلمة "التالية"
بمعطى probs_table["the"] = {"cat": 0.35, "dog": 0.3, "mouse": 0.1, "mat": 0.15, ...}، توجد استراتيجيتان مختلفتان لاختيار ما يأتي بعدها:
- جشعة (Greedy): خذ دائمًا الكلمة الوحيدة الأرجح (
"cat"، بنسبة 35%). هذه حتمية — نفس المدخل يُنتج دائمًا نفس المخرج — وتصبح متكررة بسرعة، بما أن الكلمة الأعلى احتمالًا بعد "cat" على الأرجح نفسها دائمًا أيضًا. - معاينة (Sampling): اختر كلمة عشوائيًا، لكن موزونة باحتمالها — بحيث تُختار "cat" حوالي 35% من الوقت، و"dog" حوالي 30%، وهكذا، مطابقة لرمي نرد موزون حيث لكل وجه مساحة مختلفة الحجم، تمامًا مثل المعاينة من أي توزيع منفصل .
يستخدم هذا الأسبوع المعاينة، لأنها ما يجعل النص المُولَّد يتنوّع من تشغيل لآخر — نفس السبب الذي يجعل نموذجًا لغويًا لا يعطيك نفس الرد بالضبط في كل مرة تسأله فيها نفس السؤال.
المعاينة بـ random.choices
تُنفّذ random.choices(population, weights) في بايثون هذا بالضبط: بمعطى قائمة نتائج ممكنة وقائمة أوزان مطابقة، تُعيد نتيجة واحدة مُختارة باحتمال متناسب مع وزنها.
import random
def sample_next(word, probs_table):
if word not in probs_table:
return None # لا استمرارية معروفة — طريق مسدود
next_words = list(probs_table[word].keys())
weights = list(probs_table[word].values())
return random.choices(next_words, weights=weights, k=1)[0]
sample_next("the", probs_table) # مثلًا "cat" — لكن ليس في كل مرة
لا تحتاج weights أن تجمع بالضبط إلى 1 كي تعمل random.choices بشكل صحيح (تُطبّعها داخليًا) — لكن أوزاننا تفعل ذلك بالفعل، بما أنها جاءت مباشرة من bigram_probabilities الأسبوع الماضي.
إمكانية إعادة الإنتاج بـ random.seed
العشوائية هي بالضبط ما نريده للتنوع، لكنها تجعل التصحيح ومشاركة النتائج غير عملي — "لقد ولّد جملة غريبة" من الصعب التحقيق فيها إن لم تستطع إعادة إنتاج نفس التشغيل بالضبط. تُثبّت random.seed(n) مولّد الأرقام العشوائية في بايثون عند نقطة بداية محددة، بحيث يصبح كل استدعاء لـ random.choices(...) بعدها حتميًا لذلك التشغيل:
random.seed(42)
print(generate_text(probs_table, "the")) # نفس المخرج دائمًا، كل مرة تشغّل فيها هذا
random.seed() # يُعيد العشوائية، عودة للسلوك العادي غير المتوقع
هذه تقنية عامة، ليست خاصة بالنماذج اللغوية — أي وقت تحتاج فيه "عشوائي، لكن قابل لإعادة الإنتاج للاختبار"، فإن random.seed هي الأداة.
توليد جملة كاملة
بدءًا من كلمة بذرة، عاين الكلمة التالية مرارًا وأدخلها مرة أخرى كـ"الكلمة السابقة" الجديدة — حلقة، تتوقف إما عند طول ثابت أو عندما تصل sample_next إلى طريق مسدود:
def generate_text(probs_table, start_word, max_words=10):
words = [start_word]
current = start_word
for _ in range(max_words - 1):
next_word = sample_next(current, probs_table)
if next_word is None:
break
words.append(next_word)
current = next_word
return " ".join(words)
generate_text(probs_table, "the")
# مثلًا "the cat sat on the rug" — ستختلف من تشغيل لآخر
هذا هو اللب التوليدي للنموذج اللغوي بأكمله: لا شيء سوى معاينة موزونة متكررة من جدول مبني من العدّ. إنه نموذج لغوي حقيقي وعامل (وإن كان ضعيفًا جدًا) — بنفس الفكرة حقًا لحلقة معاينة الرمز التالي داخل نماذج أكبر بكثير، فقط مُقدَّرة من عدّ 20 جملة بدلًا من مليارات المعاملات المدرَّبة على مجموعات بيانات ضخمة.
توليد عدة مرشّحات دفعة واحدة
بما أن كل استدعاء لـ generate_text عشوائي، فإن توليد عدة جمل ومراجعتها طريقة طبيعية لاستكشاف ما يستطيع النموذج إنتاجه — نفس نمط "عاين قليلًا، اختر الأفضل" المُستخدم عند العمل مع أي نظام توليدي:
def generate_many(probs_table, start_word, n=5, max_words=10):
return [generate_text(probs_table, start_word, max_words) for _ in range(n)]
for sentence in generate_many(probs_table, "the", n=5):
print(sentence)
⚠️ أخطاء شائعة
- نسيان فحص
Noneمنsample_next. لو لم تفحصgenerate_textبـif next_word is None: break، لكانت ستتعطل في المرة القادمة التي تحاول فيها البحث عنNoneكمفتاح فيprobs_table. - افتراض أن
random.choicesتحتاج أوزانًا تجمع بالضبط إلى 1. لا تحتاج ذلك — تُطبّع أي أوزان تُعطى لها. هذا مهم إن مررت عدّات خامًا بدلًا من احتمالات مُطبَّعة بالفعل. - نسيان أن
random.seed()(بلا وسيط) تُعيد العشوائية. بعد التصحيح ببذرة ثابتة، نسيان استدعاءrandom.seed()مجددًا يعني أن كل استدعاء "عشوائي" لاحق في تلك الجلسة يبقى حتميًا، مما قد يبدو كخطأ في كود غير مرتبط. - توقّع مخرجات صحيحة نحويًا تمامًا. ليس لدى نموذج ثنائي الغرام مفهوم لتطابق الفاعل والفعل، أو معنى على مستوى الجملة، أو أي شيء يتجاوز "ما يتبع عادة هذه الكلمة الواحدة" — هذا قيد حقيقي، وليس خطأً يجب إصلاحه، وهذا بالضبط ما تطلب منك الأسئلة السقراطية أدناه ملاحظته مباشرة.
🧩 تحديات
استدعِ generate_text خمس مرات بنفس start_word. هل المخرجات متطابقة؟ لماذا أو لماذا لا؟
استدعِ generate_text بـstart_word لا تظهر أبدًا كأول كلمة في ثنائي غرام في أي مكان بمجموعة النصوص (وجدت مرشحات لهذا في التحدي 3 الأسبوع الماضي). ماذا يحدث، ولماذا؟
عدّل generate_text بحيث تتوقف مبكرًا إن اختيرت نفس الكلمة 3 مرات متتالية (حماية بسيطة ضد الحلقات المتكررة). في أي ظروف لمجموعة النصوص تعتقد أن هذا قد يحدث فعليًا؟
اكتب sample_next_greedy(word, probs_table) تختار دائمًا الكلمة التالية الوحيدة الأرجح بدلًا من المعاينة العشوائية. شغّل generate_text (باستخدام هذه النسخة الجشعة) خمس مرات بنفس كلمة البداية — ماذا تلاحظ؟
استدعِ random.seed(1) قبل توليد جملة، سجّل النتيجة، ثم استدعِ random.seed(1) مجددًا قبل توليد جملة أخرى بنفس الوسائط. هل النتيجتان متطابقتان؟ لماذا؟
باستخدام generate_many، ولّد 20 جملة مرشحة من نفس كلمة البداية، ثم اطبع أيًا كانت الأطول (لديها أكثر الكلمات).
🤔 أسئلة سقراطية
- تُدخل المعاينة العشوائية عمدًا. هل يمكنك التفكير في حالة استخدام واقعية لنموذج لغوي تريد فيها فعليًا السلوك الجشع، دائمًا-نفس-الإجابة بدلًا من ذلك؟
- تُعيد
sample_nextقيمةNoneعندما لا يكون للكلمة الحالية استمرارية معروفة. أي تشبيه واقعي لهذا "الطريق المسدود" — هل هو أقرب لكون النموذج غير متأكد، أم لكون النموذج لم يرَ هذا الموقف حرفيًا من قبل؟ - يمكن لنموذج ثنائي الغرام هذا أن يولّد فقط متتاليات كلمات معقولة إحصائيًا بمعطى انتقالات كلمة واحدة — ليس لديه مفهوم لكون الجملة "منطقية" ككل. ولّد عدة جمل وابحث عن جمل غريبة أو بلا معنى نحويًا رغم أن كل زوج كلمات منفرد معقول محليًا. بماذا يوحي هذا عن حدود النظر إلى كلمة واحدة فقط للخلف؟
- تجعل
random.seedعملية عشوائية قابلة لإعادة الإنتاج للتصحيح، لكن التطبيقات الحقيقية للنماذج اللغوية (مثل روبوت محادثة) عادة لا تُثبّت البذرة. لماذا قد يكون عدم تثبيتها الخيار الصحيح هناك، رغم أن ذلك يجعل الأخطاء أصعب لإعادة الإنتاج؟
✅ اختبار الأسبوع
✅ اختبار الأسبوع
🎁 إضافي: معالجة الأخطاء لكلمة بداية سيئة
يُتاح بعد اجتيازك اختبار هذا الأسبوع.