إنتقل إلى المحتوى الرئيسي

الأسبوع 2: ترددات الكلمات كتوزيع احتمالي منفصل

🎲 يبدو جدول عدّ الكلمات كسجل محاسبي. إنه في الحقيقة توزيع احتمالي متنكّر.

🎯 أهداف التعلّم

بنهاية هذا الأسبوع ستكون قادرًا على:

  • بناء جدول تردد كلمات (dict[str, int]) من مجموعة نصوص مُجزَّأة.
  • تحويل العدّات الخام إلى احتمالات، وشرح لماذا تُشكّل توزيعًا احتماليًا منفصلًا صحيحًا.
  • التفكير فيما يمكن وما لا يمكن لنموذج "أحادي الغرام" (unigram) هذا التنبؤ به.
  • التعرّف على شكل توزيع تردد الكلمات وفق قانون زيبف (Zipf، توزيع أُسّي).

الدرس

عدّ الكلمات

بمعطى قائمة جمل مُجزَّأة (من الأسبوع الماضي)، عدّ عدد مرات ظهور كل كلمة هو بالضبط نمط جدول التردد من الأسبوع 3 من المسار العادي لبايثون 101:

def count_words(tokenized_sentences):
counts = {}
for tokens in tokenized_sentences:
for word in tokens:
counts[word] = counts.get(word, 0) + 1
return counts

counts = count_words(tokenized)
counts["the"] # عدد مرات ظهور "the" عبر مجموعة النصوص كاملة

تمتلك المكتبة القياسية فعليًا أداة مبنية خصيصًا لهذا النمط بالتحديد، collections.Counter — تستحق المعرفة رغم أننا سنستمر بكتابتها يدويًا في هذا المسار للبقاء بلا اعتماديات:

from collections import Counter

counts = Counter(word for tokens in tokenized for word in tokens)
counts.most_common(5) # أكثر 5 أزواج (كلمة، عدّ) تكرارًا، مرتبة بالفعل

Counter هي فئة فرعية من dict، لذا كل ما تعرفه بالفعل عن القواميس (.get()، .items()، الانتماء بـ in) لا يزال ينطبق عليها — تضيف فقط بضع تسهيلات خاصة بالعدّ مثل .most_common().

من العدّات إلى الاحتمالات

يصبح جدول التردد توزيعًا احتماليًا بمجرد أن تقسم كل عدّ على إجمالي عدد ظهورات الكلمات. إن كانت c(w)c(w) هي عدّ الكلمة ww و N=wc(w)N = \sum_w c(w) هو إجمالي عدد الكلمات، فإن:

P(w)=c(w)NP(w) = \frac{c(w)}{N}
def to_probabilities(counts):
total = sum(counts.values())
return {word: count / total for word, count in counts.items()}

probs = to_probabilities(counts)
probs["the"] # مثلًا 0.18 — احتمال 18% أن تكون أي فتحة كلمة عشوائية هي "the"

خاصيتان تجعلان هذا توزيعًا احتماليًا حقيقيًا، لا مجرد "بعض الأرقام":

  1. كل P(w)0P(w) \ge 0 — لا يمكن أن يكون العدّ سالبًا أبدًا.
  2. wP(w)=1\sum_w P(w) = 1 — كل ظهور كلمة يُحتسب لكلمة واحدة بالضبط، بحيث تجمع احتمالات كل النتائج الممكنة إلى 1، نفس المتطلب لأي توزيع درسته في مقرر إحصاء.

يمكنك التحقق من الخاصية 2 مباشرة: sum(probs.values()) يجب أن تكون (قريبة جدًا من) 1.0 — "قريبة جدًا" بسبب عدم دقة الفاصلة العائمة من الأسبوع 1 من المسار العادي.

هذا نموذج أحادي الغرام (unigram)

تقدير P(w)P(w) بهذه الطريقة يتجاهل كل السياق — نفس الاحتمال لـ"the" سواء تبعت "sat on" أو بدأت جملة جديدة تمامًا. يُسمى هذا نموذج أحادي الغرام: ينظر إلى كلمة واحدة في كل مرة، بلا ذاكرة لما سبق. إنه نموذج لغوي حقيقي وصالح — لكنه ضعيف للغاية، بما أن اللغة الحقيقية معتمدة على السياق بشدة (تعني "bank" بعد "river" شيئًا مختلفًا عن بعد "money"). يُصلح الأسبوع القادم هذا، خطوة بخطوة، بالاشتراط على كلمة سابقة واحدة — نموذج ثنائي الغرام (bigram).

نمط سترى في كل مكان: قانون زيبف (Zipf's law)

رتّب عدّات كلماتك من الأكثر إلى الأقل تكرارًا وانظر إلى الشكل: حفنة من الكلمات (مثل "the"، "a"، "on") تستحوذ على حصة ضخمة من كل ظهورات الكلمات، بينما تظهر معظم كلمات المفردات مرة أو مرتين فقط. هذا ليس خاصًا بمجموعة نصوصنا الصغيرة — إنه نمط تجريبي قوي للغاية في النصوص الحقيقية يُسمى قانون زيبف: تردد الكلمة يتناسب تقريبًا عكسيًا مع رتبتها، c(w)1rank(w)c(w) \propto \frac{1}{\text{rank}(w)}. تظهر الكلمة الأكثر شيوعًا تقريبًا ضعف تكرار الكلمة الثانية الأكثر شيوعًا، وثلاثة أضعاف الثالثة، وهكذا. يمكنك رؤية نسخة تقريبية من هذا الشكل حتى في مجموعة نصوص من 20 جملة:

ranked = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
for rank, (word, count) in enumerate(ranked[:10], start=1):
print(rank, word, count)

تُرقّم enumerate(ranked[:10], start=1) أفضل 10 مُدخلات بدءًا من 1 بدلًا من 0 المعتادة في بايثون — مفيد كلما كانت "الرتبة" أو "الموضع" مقصودة بالمعنى اليومي الذي يبدأ بواحد.

⚠️ أخطاء شائعة

  • البحث عن كلمة مباشرة بدلًا من استخدام .get(). counts[word] + 1 عند أول ظهور لكلمة يرفع KeyError، لأن الكلمة ليست مفتاحًا بعد — تتعامل .get(word, 0) + 1 بأناقة مع حالة "لم نرَ هذا بعد".
  • نسيان أن المقام يتغير حسب مجموعة النصوص. probs["the"] من مجموعة نصوص وprobs["the"] من مجموعة نصوص أخرى أكبر ليستا قابلتين للمقارنة المباشرة عمومًا — كل منهما نسبية إلى إجمالي عدد كلمات مجموعتها NN.
  • الخلط بين "حجم المفردات" و"إجمالي عدد الكلمات". N=wc(w)N = \sum_w c(w) يعدّ كل ظهور (بالتكرارات)؛ حجم المفردات (الأسبوع 1) يعدّ فقط الكلمات الفريدة. تقسم to_probabilities على NN، وليس على حجم المفردات — الخلط بينهما يكسر خاصية "المجموع يساوي 1".

🧩 تحديات

بمعطى counts من count_words، جِد أكثر 5 كلمات تكرارًا في مجموعة النصوص.

تحقق من الخاصية 2 من الدرس: احسب sum(probs.values()) لمجموعة نصوصك وتأكد أنها (تقريبًا تمامًا) 1.0.

تُسمى الكلمات التي تظهر مرة واحدة فقط في مجموعة النصوص hapax legomena. جِدها كلها، واشرح الاحتمال الذي تُسنده to_probabilities لكل منها.

أي احتمال يُسنده هذا النموذج لكلمة لم تظهر أبدًا في مجموعة النصوص، مثل "giraffe"؟ ماذا يحدث فعليًا إن كتبت probs["giraffe"] مباشرة؟

أعد كتابة التحدي 1 (أكثر 5 كلمات تكرارًا) باستخدام collections.Counter و.most_common() بدلًا من sorted(). هل تحصل على نفس الإجابة؟

باستخدام قائمة ranked من مثال قانون زيبف، احسب count * rank لكل من أفضل 10 كلمات. يتوقع قانون زيبف أن يكون هذا الناتج ثابتًا تقريبًا. هل هذا صحيح لمجموعة نصوصنا الصغيرة؟

🤔 أسئلة سقراطية

  • يمنح النموذج أحادي الغرام "the" احتمالًا عاليًا وثابتًا في كل مكان في الجملة، حتى في البداية تمامًا. هل يتوافق هذا مع حدسك حول أي الكلمات تميل لبدء جملة إنجليزية مقابل الظهور في المنتصف؟ ما الناقص من هذا النموذج الذي قد يُصلح ذلك؟
  • جملتان مختلفتان، "the cat sat on the mat" و"mat the on sat cat the" (الكلمات مُبعثرة)، تُنتجان جدول تردد كلمات متطابقًا تمامًا. بماذا يخبرك هذا عن المعلومات التي يلتقطها النموذج أحادي الغرام وتلك التي لا يلتقطها؟
  • لماذا القسمة على إجمالي عدد الكلمات NN (كل ظهورات الكلمات) بدلًا من حجم المفردات (الكلمات الفريدة) عند حساب P(w)P(w)؟ ماذا سيحدث خطأً لخاصية "المجموع يساوي 1" لو قسمت على حجم المفردات بدلًا من ذلك؟
  • يظهر قانون زيبف في أكثر بكثير من مجرد ترددات الكلمات — أحجام سكان المدن، توزيع الثروة، وحركة مرور المواقع كلها تُظهر شكلًا مشابهًا "قلة ضخمة، كثرة صغيرة". لماذا قد تميل الظواهر القائمة على العدّ عمومًا لإنتاج هذا النمط، بدلًا من أن يكون كل شيء متساويًا تقريبًا؟
  • تحتوي مجموعة نصوصنا فقط على 20 جملة، لذا معظم الكلمات هي hapax legomena (تظهر مرة واحدة بالضبط). ماذا تعتقد يحدث لـنسبة hapax legomena بينما تنمو مجموعة النصوص لتصبح أكبر بكثير — هل تتقلص نحو الصفر، أم يوحي قانون زيبف بأنها تبقى مرتفعة بشكل مفاجئ حتى لمجموعات نصوص ضخمة؟

✅ اختبار الأسبوع

✅ اختبار الأسبوع

1. لتوزيع احتمالي صحيح على الكلمات، ما الذي يجب أن يساويه sum(probs.values()) (تقريبًا)؟
2. يُقدّر النموذج "أحادي الغرام" احتمال الكلمة بناءً على:
3. أي احتمال يُسنده النموذج أحادي الغرام لكلمة لم تُرَ أبدًا في مجموعة النصوص؟
4. تُستخدم counts.get(word, 0) + 1 بدلًا من counts[word] + 1 لأن:
5. أي علاقة يصف قانون زيبف بين رتبة الكلمة وتردّدها؟