كلاود
ذكاء اصطناعي

نماذج التضمين للغة العربية: أيها يفهم نصوصك فعلاً

كلاود|٢٤ أكتوبر ٢٠٢٦|6 دقائق قراءة|٩٨ مشاهدة
نماذج التضمين للغة العربية: أيها يفهم نصوصك فعلاً

لمعظم مشاريع RAG العربية ابدأ بـbge-m3 أو multilingual-e5-large بمتجه 1024 بُعداً، وجرّب Qwen3-Embedding إن كان عندك GPU. لا تثق بالترتيب العام، بل اختبر على 100 سؤال حقيقي من بياناتك وقِس Recall@5 قبل أن تقرر.

نموذج التضمين (embedding) هو الذي يحوّل النص إلى متجه أرقام، وعلى جودة هذا التحويل يقوم كل شيء بعده: البحث الدلالي، ونظام RAG، وتصنيف التذاكر، وكشف التكرار. إذا فشل النموذج في أن يفهم أن "وش أسوي إذا نسيت كلمة المرور" قريبة من "استعادة الحساب"، فلن يُنقذك أقوى نموذج لغوي بعده، لأنه سيستلم مقاطع خاطئة من الأساس. الجواب المباشر: لمعظم المشاريع العربية ابدأ بأحد نموذجين مفتوحين يعملان على سيرفرك، BAAI/bge-m3 أو intfloat/multilingual-e5-large، كلاهما يخرج متجهاً بـ1024 بُعداً ويتعامل مع الفصحى بشكل جيد ومع اللهجة السعودية بشكل مقبول. إذا كان عندك GPU فجرّب Qwen3-Embedding-0.6B لأنه أحدث وأقوى غالباً في النصوص الطويلة. إذا كانت الموارد محدودة جداً فـmultilingual-e5-small بـ384 بُعداً يستهلك ربع التخزين ويعمل على CPU بسرعة معقولة. لكن القاعدة الأهم: لا تختر من لوحة ترتيب عامة، بل خذ 100 سؤال حقيقي من موظفيك أو عملائك، وقِس كم مرة يظهر المقطع الصحيح ضمن أول 5 نتائج (Recall@5). هذا الاختبار يأخذ ساعتين ويوفّر عليك أشهراً من نتائج بحث ضعيفة.

لماذا العربية صعبة على نماذج التضمين

العربية لغة اشتقاقية، فالجذر "كتب" يعطي كتاب ومكتبة وكاتب ومكتوب واستكتاب، والنموذج الجيد يفهم القرابة بينها. النماذج التي دُرّبت أساساً على الإنجليزية تقطّع الكلمة العربية إلى أجزاء كثيرة بلا معنى، فتضيع العلاقات الصرفية. يضاف لذلك ثلاث مشكلات عملية: الكتابة بلا همزات أو بهمزات خاطئة (اسعار وأسعار)، والتاء المربوطة والهاء (فاتورة وفاتوره)، واللهجة التي تختلف عن الفصحى في المفردات نفسها ("أبغى" مقابل "أريد"، "وش" مقابل "ماذا"). نصوص الشركات السعودية خليط من الثلاثة: سياسات مكتوبة بالفصحى، وأسئلة موظفين بالعامية، ومصطلحات تقنية بالإنجليزية داخل الجملة العربية.

الحل الأول رخيص ومضمون: توحيد النص قبل التضمين، في الوثائق والأسئلة معاً بنفس الدالة. أزل التشكيل والتطويل، ووحّد أشكال الألف (أ إ آ إلى ا)، والألف المقصورة إلى ياء. في التجارب العملية على نصوص الدعم الفني ترفع هذه الخطوة وحدها Recall غالباً بنحو 3 إلى 6 نقاط مئوية حسب النموذج، وكلفتها سطور قليلة.

import re
def normalize_ar(t: str) -> str:
    t = re.sub(r"[\u064B-\u0652\u0640]", "", t)   # تشكيل وتطويل
    t = re.sub("[إأآ]", "ا", t)
    t = t.replace("ى", "ي")
    return t.strip()

مقارنة النماذج المفتوحة

الجدول التالي يلخص النماذج الأكثر استخداماً للعربية حالياً. السرعة تقريبية لمقاطع بطول 256 توكن على 4 أنوية CPU، وتختلف حسب المعالج وحجم الدفعة، فاعتبرها للمقارنة النسبية لا كرقم نهائي.

النموذجالأبعادالحجمأقصى طولCPU تقريباًملاحظة
multilingual-e5-small384118M512من 40 إلى 60 مقطعاً/ثخفيف، جودة أقل في اللهجة
multilingual-e5-large1024560M512من 6 إلى 10 مقاطع/ثيحتاج بادئة query: وpassage:
bge-m31024568M8192من 6 إلى 10 مقاطع/ثكثيف ومتناثر معاً، ممتاز للمستندات الطويلة
gte-multilingual-base768305M8192من 15 إلى 25 مقطعاً/ثتوازن جيد بين الحجم والجودة
Qwen3-Embedding-0.6B1024600M32Kمن 4 إلى 8 مقاطع/ثأداء قوي، يدعم تقليص الأبعاد

جميعها برخص تسمح بالاستخدام التجاري (MIT أو Apache 2.0). انتبه لنماذج تبدو مغرية في لوحات الترتيب لكن رخصتها غير تجارية، وراجع ملف الرخصة قبل بناء منتج عليها.

تفاصيل تُسقط النتائج إن أهملتها

نماذج e5 تتوقع البادئة "query: " قبل السؤال و"passage: " قبل المقطع، ونسيانها يخفض الجودة بشكل ملحوظ. Qwen3-Embedding يتوقع تعليمة قصيرة قبل السؤال مثل "Instruct: Given a question, retrieve relevant passages\nQuery: ". أما bge-m3 فلا يحتاج بادئة، ويعطيك مع المتجه الكثيف أوزاناً متناثرة تشبه BM25، والجمع بينهما مفيد جداً مع أسماء المنتجات وأرقام المواد في العقود.

حجم المتجه وأثره على التخزين

كل بُعد يُخزن كرقم float32 بأربعة بايت، فالحساب مباشر: مليون مقطع بـ1024 بُعداً تساوي 4GB للمتجهات وحدها، ومع فهرس HNSW في pgvector أو Qdrant أضف من 30% إلى 50% تقريباً. نفس المليون بـ384 بُعداً تساوي 1.5GB. الفرق يظهر في الذاكرة أكثر من القرص، لأن الفهرس يجب أن يبقى في RAM ليعطيك استجابة تحت 50ms.

عدد المقاطع384 بُعداً768 بُعداً1024 بُعداً
100 ألفنحو 150MBنحو 300MBنحو 400MB
مليوننحو 1.5GBنحو 3GBنحو 4GB
5 ملاييننحو 7.5GBنحو 15GBنحو 20GB

أغلب الشركات المتوسطة عندها من 50 إلى 300 ألف مقطع (سياسات، عقود، تذاكر سابقة، أسئلة شائعة)، فالمتجه بـ1024 بُعداً مناسب تماماً ولا داعي للتضحية بالجودة. إن احتجت تقليصاً لاحقاً فاستخدم التكميم إلى int8 الذي يخفض الحجم إلى الربع بخسارة جودة صغيرة غالباً، أو نموذجاً يدعم تقليص الأبعاد مثل Qwen3-Embedding.

CPU أم GPU

التضمين عمليتان مختلفتان في الحِمل. الأولى فهرسة أولية لكل الوثائق مرة واحدة، والثانية تضمين الأسئلة لحظة البحث. تضمين سؤال واحد بنموذج 1024 بُعداً على CPU يأخذ من 40 إلى 120 ميلي ثانية، وهذا مقبول لتطبيق داخلي. الفهرسة الأولية هي المكلفة: 200 ألف مقطع بسرعة 8 مقاطع في الثانية تعني نحو 7 ساعات على CPU، بينما على GPU متوسط تنتهي في أقل من 15 دقيقة.

الحل العملي لأغلب الشركات: اجعل الفهرسة الأولية تعمل ليلاً على خادم سحابي بموارد كافية، ثم حدّث الفهرس تدريجياً كلما أُضيفت وثيقة. باقة أعمال بـ4 أنوية و8GB بسعر 11,500 ريال سنوياً تكفي لتشغيل bge-m3 مع pgvector لنحو 300 ألف مقطع، وإذا زاد الحِمل تضيف 2GB RAM بـ600 ريال أو vCPU بـ500 ريال سنوياً حسب الحاجة. استخدم خادم استدلال مخصصاً مثل text-embeddings-inference من Hugging Face بدل تحميل النموذج داخل تطبيقك، لأنه يجمع الطلبات في دفعات ويضاعف الإنتاجية:

docker run -d --name tei -p 127.0.0.1:8081:80 \
  -v /srv/models:/data \
  ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 \
  --model-id BAAI/bge-m3 --max-batch-tokens 16384

اختبار 100 سؤال من بياناتك

هذا الجزء أهم من كل ما سبق. لوحات الترتيب العامة مثل MTEB تقيس نماذج على بيانات عامة، أما نصوصك فلها مصطلحاتها ولهجتها. خطوات الاختبار:

  1. اجمع 100 سؤال حقيقي من سجلات الدعم أو من موظفيك، نصفها بالعامية ونصفها بالفصحى، وبعضها يحتوي مصطلحاً إنجليزياً.
  2. لكل سؤال حدّد يدوياً رقم المقطع أو المقاطع الصحيحة التي تجيب عنه، وهذه أطول خطوة لكنها تأخذ نحو ساعة ونصف.
  3. ضمّن كل الوثائق بكل نموذج مرشح، ثم ابحث بكل سؤال واحسب Recall@5 وMRR.
from sentence_transformers import SentenceTransformer
import numpy as np

def recall_at_k(model_id, docs, qs, gold, k=5, qp="", dp=""):
    m = SentenceTransformer(model_id)
    D = m.encode([dp+normalize_ar(x) for x in docs], normalize_embeddings=True)
    Q = m.encode([qp+normalize_ar(x) for x in qs], normalize_embeddings=True)
    top = np.argsort(-Q @ D.T, axis=1)[:, :k]
    return np.mean([any(g in row for g in gold[i]) for i, row in enumerate(top)])

print(recall_at_k("intfloat/multilingual-e5-large", docs, qs, gold,
                  qp="query: ", dp="passage: "))
print(recall_at_k("BAAI/bge-m3", docs, qs, gold))

كقاعدة تقريبية: Recall@5 فوق 0.85 ممتاز لنظام RAG داخلي، ومن 0.70 إلى 0.85 مقبول مع إضافة reranker، وتحت 0.70 يعني أن المشكلة غالباً في تقطيع الوثائق لا في النموذج. جرّب مقاطع بطول من 300 إلى 500 كلمة مع تداخل 15% قبل أن تغيّر النموذج.

متى تضيف reranker

إذا وصلت Recall@5 إلى 0.75 مثلاً وظهر المقطع الصحيح غالباً في المرتبة 7 أو 12 بدل الأولى، فالحل ليس نموذج تضمين أكبر بل مرحلة ثانية تعيد الترتيب. الفكرة أن تسحب أول 30 مقطعاً من البحث المتجهي، ثم تمرر كل زوج (سؤال، مقطع) على نموذج cross-encoder مثل bge-reranker-v2-m3 الذي يقرأ السؤال والمقطع معاً ويعطي درجة صلة أدق. هذا النموذج متعدد اللغات ويدعم العربية، وحجمه نحو 568M معامل. الكلفة زمنية: إعادة ترتيب 30 مقطعاً على CPU تأخذ من 300 إلى 800 ميلي ثانية، وهذا مقبول لمساعد داخلي يجيب في ثانيتين أو ثلاث. النتيجة المعتادة قفزة واضحة في دقة المرتبة الأولى، وهي التي يعتمد عليها النموذج اللغوي حين يكتب الجواب.

لماذا يبقى التضمين على سيرفرك

التضمين يمر على كل وثيقة عندك، أي أن استخدام API خارجي يعني إرسال كامل أرشيف العقود والسياسات خارج المملكة. تشغيل النموذج محلياً يحل هذا، ويلغي كلفة كل طلب، ويعطيك زمن استجابة ثابتاً. قوالب استضافة الذكاء الاصطناعي تأتي بـDocker وJupyter جاهزين، فتبدأ الاختبار في نفس اليوم.

القرار في أربع خطوات

  1. ابدأ بـbge-m3 وmultilingual-e5-large، ووحّد النص العربي قبل التضمين.
  2. اختبر على 100 سؤال من بياناتك وقِس Recall@5، ولا تكتفِ بلوحة ترتيب عامة.
  3. اختر 1024 بُعداً ما دام عدد مقاطعك تحت مليون، واحسب الذاكرة بأربعة بايت لكل بُعد مع هامش 50% للفهرس.
  4. افصل خادم التضمين عن تطبيقك، وشغّل الفهرسة الأولية ليلاً، ثم راجع الأسعار والباقات لاختيار الحجم المناسب.
مشاركة