الترميز
درس 1 من 8
1الترميز
الصورة الكبيرةالنموذج لا يقرأ حروفًا ولا كلمات، بل أرقامًا. المُقطِّع (Tokenizer) هو من يقرّر كيف يُقسَم النص إلى قطع وكل قطعة إلى رقم، وهذا القرار يحدّد كم تدفع، وكم يتّسع السياق، ولماذا تكلّف الجملة العربية أكثر من ترجمتها الإنجليزية.
ماذا تشاهد في الرسم: شاهد الجملة العربية تتفتّت إلى 12 قطعة، أغلبها حروف مفردة، بينما صارت كل كلمة إنجليزية في المثال رمزًا واحدًا. هذا هو الفرق الذي تدفعه في السعر وفي مساحة السياق.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
النموذج لا يقرأ حروفًا ولا كلمات كاملة، بل قطعًا تسمّى رموزًا: كلمة شائعة رمز واحد، وكلمة نادرة تُقطّع إلى قطع أصغر. ولأن أغلب ما تعلّمت منه النماذج نصوص إنجليزية، تُقطّع الجملة العربية غالبًا إلى قطع أكثر، فتكلّف أكثر وتأخذ مساحة أكبر.
BPE يبدأ بالحروف ويدمج أكثر الأزواج تكرارًا حتى يبلغ حجم المفردات، وSentencePiece يعمل على النص الخام دون تقسيم مسبق بالمسافات. عدد الرموز يحدد التكلفة وطول السياق، والعربية بصرفها الغني تنتج رموزًا أكثر لكل كلمة في مفردات مدرّبة على الإنجليزية.
قبل أن تبدأ
🔢المصفوفات مع NumPy
تعرف أن النموذج يتعامل مع أرقام في مصفوفات، لا مع نص، فكل ما يدخله يجب أن يتحوّل إلى أعداد أولًا.
🐍عدّ التكرارات في بايثون
تستطيع استخدام Counter وzip لعدّ الأزواج المتجاورة في قائمة، وهذا كل ما تحتاجه لكتابة BPE بيدك.
الفكرة الجوهرية
في هذا الدرس ترى لماذا يُقطَّع النص إلى قطع، ثم تبني BPE بيدك، ثم تعدّ البايتات، وأخيرًا تفهم لماذا تكلّف العربية رموزًا أكثر.
1لماذا نقطّع النص
النموذج يحتاج مفردات ثابتة من الأرقام. التقطيع حرفًا حرفًا يعطي مفردات بالعشرات لكن تسلسلات طويلة جدًّا، والتقطيع كلمة كلمة يعطي تسلسلات قصيرة لكن مفردات بالملايين وكلمات مجهولة.
الحل في الوسط: قطع بين الحرف والكلمة. الشائع يصير رمزًا واحدًا، والنادر عدّة قطع، ولا يبقى شيء مجهول.
انظر إلى الرسم: المُقطِّع هو الخطوة الوسطى، وهو يُبنى مرة واحدة قبل التدريب ثم يُجمَّد مع النموذج. لذلك فإن قراره في تقطيع جملتك يحدّد كم تدفع.
2BPE: ادمج أكثر زوج تكرارًا
أشهر طريقة لبناء هذه القطع اسمها BPE. هي لا تأخذ القطع من قاموس، بل تكتشفها من النص نفسه: ما يتكرّر معًا كثيرًا يصير قطعة واحدة.
ابدأ بكل كلمة كحروف منفصلة مع علامة نهاية </w>. عدّ كل زوج متجاور، وادمج الأكثر تكرارًا في رمز واحد، وكرّر.
from collections import Counter
words = ["lower", "lowest", "newer", "newest", "wider", "widest"]
seqs = [list(w) + ["</w>"] for w in words]
def merge_once(seqs):
pairs = Counter()
for s in seqs:
pairs.update(zip(s, s[1:]))
(a, b), n = pairs.most_common(1)[0]
out = []
for s in seqs:
t, i = [], 0
while i < len(s):
if i + 1 < len(s) and s[i] == a and s[i + 1] == b:
t.append(a + b)
i += 2
else:
t.append(s[i])
i += 1
out.append(t)
return (a, b, n), out
for step in range(6):
(a, b, n), seqs = merge_once(seqs)
print(f"merge {step + 1}: {a!r} + {b!r} (seen {n} times)")
print(seqs[1])
print(seqs[4])
بعد ست عمليات دمج فقط صار lowest قطعتين: lowe وst</w>، لأن st في آخر الكلمة تكرّر ثلاث مرات. أما wider فما زالت حروفًا، لأن أزواجها لم تتكرّر بما يكفي. المُقطِّع الحقيقي يفعل هذا نفسه عشرات آلاف المرات على مليارات الكلمات، فتصير الكلمات الشائعة رمزًا واحدًا والنادرة عدّة قطع.
انظر إلى الرسم: الدمج الأول كان w مع e لأنه ظهر 4 مرات. ولاحظ أن الدمج قد يبني على دمج سابق، مثل st ثم st</w>.
3البايتات: الحرف العربي بايتان
قبل أي دمج، من أين يبدأ المُقطِّع؟ انظر إلى الرسم: ثلاث جمل بنفس المعنى تقريبًا، والعربية تبدأ من سلسلة أطول، والتشكيل يزيدها كثيرًا.
أغلب المُقطِّعات الحديثة تعمل على البايتات لا الحروف، حتى لا يوجد رمز مجهول أبدًا. والحرف العربي في UTF-8 بايتان، والإنجليزي بايت واحد.
samples = {
"English": "The model reads the text",
"Arabic": "النموذج يقرأ النص",
"Arabic with diacritics": "النَّموذَجُ يَقرَأُ النَّصَّ",
}
for name, text in samples.items():
print(f"{name:24} chars={len(text):3} utf8 bytes={len(text.encode('utf-8')):3}")
جملة عربية أقصر بالحروف أطول بالبايتات، والتشكيل يضاعفها تقريبًا. إن لم يرَ المُقطِّع نصًّا عربيًّا كافيًا أثناء بنائه، فلن يدمج هذه البايتات في قطع كبيرة، وستدفع ثمن كل بايت تقريبًا.
4لماذا تكلّف العربية أكثر
المُقطِّع يدمج أكثر ما يراه. إن بُني على نص معظمه إنجليزي، دمج قطعًا إنجليزية كبيرة وترك العربية قطعًا صغيرة.
الرقم الذي يقيس هذا هو الخصوبة: متوسط الرموز لكل كلمة. مُقطِّع جيد للإنجليزية يعطيها نحو 1.3، والمُقطِّعات القديمة قد تعطي العربية 3 أو 4.
ندرّب BPE صغيرًا على نص تسعة أعشاره إنجليزي وعُشره عربي، ثم نقطّع جملة من كل لغة بنفس المعنى تقريبًا.
from collections import Counter
def train_bpe(text, merges):
seqs = [list(w) + ["_"] for w in text.split()]
rules = []
for _ in range(merges):
pairs = Counter()
for s in seqs:
pairs.update(zip(s, s[1:]))
if not pairs:
break
(a, b), _ = pairs.most_common(1)[0]
rules.append((a, b))
seqs = [apply(s, a, b) for s in seqs]
return rules
def apply(s, a, b):
t, i = [], 0
while i < len(s):
if i + 1 < len(s) and s[i] == a and s[i + 1] == b:
t.append(a + b); i += 2
else:
t.append(s[i]); i += 1
return t
def tokenize(text, rules):
total = 0
for w in text.split():
s = list(w) + ["_"]
for a, b in rules:
s = apply(s, a, b)
total += len(s)
return total
english = "the student reads the lesson and the teacher reads the answer " * 20
arabic = "الطالب يقرأ الدرس والمعلم يقرأ الجواب " * 20
rules = train_bpe(english * 9 + arabic, merges=40) # trained mostly on English
test_en = "the teacher reads the lesson"
test_ar = "المعلم يقرأ الدرس"
print("English tokens:", tokenize(test_en, rules), "for", len(test_en.split()), "words")
print("Arabic tokens: ", tokenize(test_ar, rules), "for", len(test_ar.split()), "words")
خمس كلمات إنجليزية صارت خمسة رموز، كل كلمة رمز. وثلاث كلمات عربية صارت 12 رمزًا، أي أربعة لكل كلمة. هذا بالضبط ما يحدث في المُقطِّعات الكبيرة بنسب مختلفة: من يرى لغة أكثر يدمج قطعها أكثر.
النتيجة رموز أكثر لنفس المعنى: سعر أعلى، وسياق يمتلئ أسرع، وتوليد أبطأ.
كيف يعمل، خطوة بخطوة: كيف يُبنى مُقطِّع BPE
تبدأ بكل كلمة حروفًا أو بايتات منفصلة، مع علامة نهاية للكلمة مثل </w>.
تعدّ كل زوج متجاور في النص كله، مثل w وe اللذين ظهرا معًا 4 مرات.
تدمج أكثر زوج تكرارًا في رمز واحد، وتضيف هذا الدمج إلى قائمة القواعد.
تكرّر حتى يبلغ حجم المفردات الهدف، ثم تطبّق القواعد نفسها بالترتيب على أي نص جديد.
أعمق قليلًا
الرقم المفيد للمقارنة هو «الخصوبة» (Fertility): متوسط عدد الرموز لكل كلمة. مُقطِّع جيد للإنجليزية يعطيها نحو 1.3، والمُقطِّعات القديمة قد تعطي العربية 3 أو 4. النماذج الأحدث التي أضافت نصًّا عربيًّا كثيرًا لمفرداتها أقرب بكثير، ولهذا تختلف تكلفة نفس الطلب العربي بين مزوّد وآخر.
| الطريقة | حجم المفردات | طول التسلسل | الكلمة المجهولة |
|---|---|---|---|
| حروف | عشرات | طويل جدًّا | لا توجد |
| كلمات | مئات الآلاف | قصير | كثيرة |
| BPE على البايتات | 32 إلى 200 ألف | متوسط | لا توجد |
مثال محلول
عندك ثلاثة خيارات لتقسيم النص: حرفًا حرفًا (مفردات صغيرة لكن تسلسلات طويلة جدًّا)، أو كلمة كلمة (تسلسلات قصيرة لكن مفردات بالملايين وكلمات لم يرها النموذج قط)، أو قطعًا بين الاثنين. النماذج الحديثة كلها تختار الثالث، والطريقة الأشهر لبنائه اسمها BPE.
مثال: عدّ الرموز بمُقطِّع حقيقي
في مشروعك استخدم مُقطِّع النموذج نفسه. هذا يحتاج تثبيت transformers وتنزيل المُقطِّع، فهو للقراءة هنا.
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B")
for text in ["The model reads the text", "النموذج يقرأ النص"]:
ids = tok.encode(text)
print(len(text.split()), "words ->", len(ids), "tokens:", tok.convert_ids_to_tokens(ids))
جرّب نفس الجملتين مع مُقطِّعات عدّة نماذج، واحسب الخصوبة على مئة جملة عربية من مجالك لا على جملة واحدة. هذا الرقم يدخل مباشرة في تقدير التكلفة في درس «استدعاء النموذج».
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| الرمز يساوي كلمة. | الرمز قطعة قد تكون كلمة كاملة أو جزءًا منها أو حرفًا واحدًا. في المثال صارت «المعلم» ستة رموز. |
| التقطيع الأدقّ يعني فهمًا أفضل. | التقطيع الأدق يعني تسلسلًا أطول وتكلفة أعلى فقط. النموذج لا يفهم العربية أفضل لأنه يقطعها حروفًا. |
| يمكن تبديل مُقطِّع نموذج مدرّب بمُقطِّع أفضل للعربية. | كل رقم في المفردات مربوط بصف تعلّمه النموذج في طبقة التضمين، فتغيير المُقطِّع يحتاج إعادة تدريب تلك الطبقة على الأقل. |
جرّب بنفسك
نموذجان بنفس السعر لكل مليون رمز. الأول يقطّع جملك العربية بمتوسط 1.8 رمز للكلمة، والثاني بـ3.6. ماذا يعني هذا؟
السعر لكل رمز، فضعف الرموز ضعف السعر لنفس الكلام، ونفس نافذة السياق تتّسع لنصف الكلمات. التقطيع الأدق لا يعني فهمًا أفضل.
ماذا يدمج BPE في كل خطوة؟
هذا كل BPE: عدّ الأزواج المتجاورة، ادمج الأكثر، كرّر. ولهذا يعكس المُقطِّع تمامًا ما كان كثيرًا في النص الذي بُني منه.
بطاقات مراجعة سريعة
▁)، فتصلح للغات لا تفصل كلماتها بمسافات وتُعاد الجملة منها حرفيًّا.تحقق من فهمك
لماذا لا يحتوي المُقطِّع المبني على البايتات رمزًا «مجهولًا» أبدًا؟
لأن مفرداته تبدأ بكل البايتات الـ256 الممكنة، وأي نص بأي لغة أو رمز تعبيري هو في النهاية تسلسل بايتات. أسوأ حالة أن تُقطَّع كلمة نادرة إلى بايتاتها المنفردة: مكلف لكنه لا يُفقد شيئًا.
لماذا يكون عدد البايتات في «النموذج يقرأ النص» أكبر من عدد حروفها؟
لأن الحرف العربي يأخذ بايتين في UTF-8 والمسافة بايتًا واحدًا، فصارت 17 حرفًا 32 بايتًا. والتشكيل يضيف بايتين لكل حركة، فقفزت النسخة المشكولة إلى 54.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.