التضمينات
درس 2 من 8
2التضمينات
الصورة الكبيرةبعد التقطيع يصير كل رمز رقمًا، لكن الرقم 512 ليس «أقرب» إلى 513 في المعنى. التضمين (Embedding) يعطي كل رمز متّجهًا من مئات الأرقام، ويتعلّمها النموذج حتى تقترب الكلمات المتشابهة في الاستخدام. هذا الأساس الذي يقوم عليه البحث الدلالي وRAG كلّه.
ماذا تشاهد في الرسم: القوس الصغير بين «ملك» و«ملكة» هو سرّ التشابه: كلما ضاقت الزاوية اقترب الجيب تمام من 1. «خبز» على زاوية قائمة من «ملك»، فتشابههما صفر، وطول السهم لا يغيّر شيئًا من ذلك.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
التضمين يعطي كل كلمة عنوانًا على خريطة معنى. الكلمات المتقاربة في المعنى تسكن في أحياء متجاورة: «ملك» قرب «ملكة»، و«قط» قرب «كلب». فتستطيع أن تقيس قرب كلمتين بقياس المسافة بينهما على الخريطة. لكن الخريطة قد تضع كلمتين متضادتين جنبًا إلى جنب لأنهما تظهران في الجمل نفسها.
التضمين متجه كثيف يُتعلَّم بحيث تتقارب الكلمات أو الجمل المتشابهة. التشابه الجيبي cos = a·b / (|a||b|) يقيس الزاوية بينها. التضمينات تعكس السياقات المشتركة لا المعنى المنطقي، فقد يتقارب المتضادان، وتحمل تحيّزات بيانات التدريب.
قبل أن تبدأ
📐الجبر الخطي عمليًا
تعرف الضرب النقطي وطول المتّجه، فالجيب تمام ليس إلا الضرب النقطي مقسومًا على الطولين.
✂️الترميز
تعرف أن النص صار أرقامًا، وأن الرقم 512 لا يقول شيئًا عن معنى الرمز، وهذا ما يحلّه التضمين.
الفكرة الجوهرية
في هذا الدرس تحوّل الرمز إلى متّجه، ثم تقيس التشابه بين متّجهين، ثم ترى من أين تأتي الأرقام، وكيف نبحث عن أقرب الجيران، وأين يخدعنا التشابه.
1من رقم إلى متّجه
بعد التقطيع صار كل رمز رقمًا، لكن 512 ليس أقرب في المعنى إلى 513. لذلك نعطي كل رمز صفًّا من مئات الأرقام في جدول حجمه المفردات في العرض.
انظر إلى الرسم: الرقم لا يدخل النموذج كما هو، بل يُستعمل لاختيار صف. هذا الجدول هو أول طبقة في كل محوّل.
2التشابه: جيب تمام الزاوية
نقيس التشابه بجيب تمام الزاوية بين متّجهين: حاصل الضرب النقطي مقسومًا على طوليهما. القيمة 1 تعني الاتجاه نفسه، و0 تعني لا علاقة.
لنجرّب ذلك على أربع كلمات لكل منها ثلاثة أبعاد فقط نعرف معناها.
import numpy as np
# three made-up dimensions: [royalty, person, food]
vec = {
"king": np.array([0.9, 0.8, 0.0]),
"queen": np.array([0.9, 0.7, 0.1]),
"man": np.array([0.1, 0.9, 0.0]),
"bread": np.array([0.0, 0.0, 0.9]),
}
def cosine(a, b):
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
for w in ["queen", "man", "bread"]:
print(f"king vs {w:5}: {cosine(vec['king'], vec[w]):.3f}")
«ملك» و«ملكة» شبه متطابقتين، و«رجل» قريبة لأنها تشترك في بعد «شخص»، و«خبز» صفر لأنها لا تشترك في أي بعد. في التضمينات الحقيقية الأبعاد بالمئات ولا يسمّيها أحد، لكن الحساب نفسه بالضبط.
3الكلمة تُعرف بصحبتها
في المثال السابق اخترنا الأبعاد ومعانيها بأنفسنا. انظر إلى الرسم: في التدريب الحقيقي تبدأ النقاط مبعثرة، ثم تتجمّع الكلمات التي تُستعمل معًا.
لم يكتب أحد أن «ملك» = [0.9, 0.8, 0.0]. النموذج يبدأ بأرقام عشوائية، ويعدّلها أثناء التدريب. والفكرة التي تجعل هذا ممكنًا قديمة: «تُعرف الكلمة من صحبتها». الكلمات التي تظهر في سياقات متشابهة تنتهي بمتّجهات متشابهة.
4أقرب الجيران
أقرب الجيران هي المتّجهات الأكثر تشابهًا مع متّجه معيّن. البحث الدلالي كله هو هذا: أعطني أقرب الجيران لمتّجه سؤالي.
نعدّ كم مرة تظهر كل كلمة قرب الأخرى (نافذة كلمتين)، ثم نضغط الجدول إلى أربعة أبعاد بـSVD، ونبحث عن أقرب جار لكل كلمة. لا شبكة عصبية، ومع ذلك يظهر المعنى.
import numpy as np
corpus = """
the cat drinks milk . the dog drinks water . the cat eats fish .
the dog eats meat . a kitten drinks milk . a puppy drinks water .
the car needs fuel . the truck needs fuel . a car has wheels .
a truck has wheels . the kitten eats fish . the puppy eats meat .
""".split()
vocab = sorted(set(corpus) - {".", "the", "a"})
idx = {w: i for i, w in enumerate(vocab)}
counts = np.zeros((len(vocab), len(vocab)))
for i, w in enumerate(corpus):
for j in range(max(0, i - 2), min(len(corpus), i + 3)):
if j != i and w in idx and corpus[j] in idx:
counts[idx[w], idx[corpus[j]]] += 1
u, s, _ = np.linalg.svd(np.log1p(counts))
emb = u[:, :4] * s[:4]
emb /= np.linalg.norm(emb, axis=1, keepdims=True)
nouns = ["cat", "dog", "kitten", "puppy", "car", "truck", "milk", "water", "fish", "meat"]
def nearest(word, k=2):
sims = {w: float(emb[idx[w]] @ emb[idx[word]]) for w in nouns if w != word}
return sorted(sims, key=sims.get, reverse=True)[:k]
for w in ["cat", "puppy", "car", "milk"]:
print(w, "->", nearest(w))
«قطة» أقرب إلى «هريرة» و«جرو» أقرب إلى «قطة» و«كلب»، و«سيارة» أقرب إلى «شاحنة»، مع أن أحدًا لم يخبر الكود بأي معنى. الجار الثاني أحيانًا ضجيج (water بجانب car) لأن النص 48 كلمة فقط. نماذج التضمين الحقيقية تفعل شيئًا أذكى على مليارات الكلمات، لكن المبدأ هو هذا.
5أين يخدعنا التشابه
التضمين يقيس التشابه في الاستعمال لا في المعنى. لذلك تكون الأضداد قريبة جدًّا، لأنها تظهر في الجمل نفسها.
في نص صغير تظهر «حار» و«بارد» في نفس المواضع تمامًا.
import numpy as np
corpus = ("the tea is hot today . the tea is cold today . "
"the soup is hot now . the soup is cold now . "
"the room is warm today . the room is big now . ") * 3
words = corpus.split()
vocab = sorted(set(words))
idx = {w: i for i, w in enumerate(vocab)}
counts = np.zeros((len(vocab), len(vocab)))
for i, w in enumerate(words):
for j in range(max(0, i - 2), min(len(words), i + 3)):
if j != i:
counts[idx[w], idx[words[j]]] += 1
emb = counts / np.linalg.norm(counts, axis=1, keepdims=True)
def sim(a, b):
return float(emb[idx[a]] @ emb[idx[b]])
print(f"hot vs cold: {sim('hot', 'cold'):.2f} (opposites)")
print(f"hot vs warm: {sim('hot', 'warm'):.2f} (close in meaning)")
print(f"hot vs tea: {sim('hot', 'tea'):.2f}")
«حار» و«بارد» متطابقتان تمامًا (1.00) لأن صحبتهما واحدة، وأقرب من «حار» و«دافئ». النماذج الكبيرة تخفّف هذا لكنها لا تلغيه. في نظام بحث، سؤال «ما الأدوية الممنوعة؟» قد يسترجع فقرة «الأدوية المسموحة» بثقة. لهذا نضيف البحث بالكلمات وإعادة الترتيب في درس البحث الهجين.
انظر إلى الرسم: الضدّان متطابقان تمامًا، وأقرب من كلمتين معناهما متقارب.
كيف يعمل، خطوة بخطوة: كيف تنشأ متّجهات التضمين
يبدأ كل رمز بصفّ من أرقام عشوائية في جدول حجمه المفردات في العرض.
أثناء التدريب تتعدّل هذه الأرقام حتى يساعد كل صفّ على توقّع ما حوله من كلمات.
الكلمات التي تظهر في صحبة متشابهة، مثل «cat» و«kitten»، تنتهي بمتّجهات متقاربة الاتجاه.
عند البحث تطبّع المتّجهات إلى طول 1، فيصير التشابه ضربًا نقطيًّا، وأقرب الجيران هم الأعلى درجة.
أعمق قليلًا
نموذج التضمين الجيد للعربية ليس بالضرورة الجيد للإنجليزية. قبل أن تبني نظام بحث، خذ عشرين سؤالًا من مجالك مع الفقرة الصحيحة لكل منها، وقِس كم مرة تكون الفقرة الصحيحة ضمن أقرب ثلاثة. هذا الاختبار الصغير يختار لك النموذج أفضل من أي جدول ترتيب عام.
مثال محلول
تخيّل أن لكل كلمة ثلاثة أرقام فقط: كم هي ملكية، وكم هي شخص، وكم هي طعام. الكلمات المتشابهة ستكون أرقامها متقاربة. التشابه بين متّجهين نقيسه بجيب تمام الزاوية بينهما (Cosine similarity): 1 يعني نفس الاتجاه، و0 لا علاقة.
مثال: تضمين جمل بنموذج حقيقي
هذا ما ستكتبه فعليًّا. يحتاج sentence-transformers وتنزيل نموذج متعدّد اللغات، فهو للقراءة هنا.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/multilingual-e5-small")
docs = ["passage: مواعيد الدوام من الثامنة إلى الرابعة",
"passage: يمكن استرجاع المبلغ خلال أربعة عشر يومًا"]
query = "query: متى أستطيع إرجاع المنتج؟"
d = model.encode(docs, normalize_embeddings=True)
q = model.encode(query, normalize_embeddings=True)
print(d @ q) # the second passage should score higher
normalize_embeddings=True يجعل كل متّجه بطول 1، فيصير الضرب النقطي هو الجيب تمام نفسه. وبعض النماذج مثل E5 تتوقّع بادئة query: وpassage:، ونسيانها يخفض الجودة بصمت. اقرأ بطاقة النموذج دائمًا.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| لكل بُعد في التضمين معنى مسمّى، مثل «ملكي» أو «طعام». | الأبعاد المسمّاة في الدرس للتوضيح فقط. في النماذج الحقيقية لا أحد يسمّي الأبعاد، والمهم هو المسافات بين المتّجهات. |
| التشابه العالي يعني أن المعنى واحد. | التشابه يقيس الاستخدام المشترك. «hot» و«cold» أخذا 1.00 في المثال لأن صحبتهما واحدة، مع أنهما ضدّان. |
| نموذج التضمين الأعلى في لوحة الترتيب هو الأفضل لمشروعك. | اللوحات تقيس بيانات عامة غالبًا إنجليزية. عشرون سؤالًا من مجالك مع الفقرة الصحيحة لكل منها تختار نموذجك بشكل أدق. |
جرّب بنفسك
متّجهان مطبّعان بطول 1. ما أسرع طريقة لحساب الجيب تمام بينهما؟
الجيب تمام هو الضرب النقطي مقسومًا على حاصل ضرب الطولين، والطولان 1. ولهذا تطبّع قواعد البيانات المتّجهية المتّجهات مرة عند الإدخال، ثم تبحث بالضرب النقطي.
لماذا تقترب «يقبل» و«يرفض» في فضاء التضمين؟
التضمين يتعلّم من الصحبة، و«قبِل الطلب» و«رفض الطلب» نفس الصحبة. هذا ليس خطأ بل حدّ للطريقة نفسها.
بطاقات مراجعة سريعة
تحقق من فهمك
لماذا لا تكفي عتبة ثابتة مثل «التشابه فوق 0.8 يعني إجابة صحيحة»؟
لأن مدى القيم يختلف من نموذج لآخر: بعضها يعطي 0.7 لجمل لا علاقة بينها وبعضها 0.3. والتشابه العالي لا يعني صحة، كما رأيت مع الأضداد. العتبة تُختار من بياناتك بقياس: انظر إلى توزيع التشابه للأزواج الصحيحة والخاطئة واختر حيث يفترقان.
في مثال العدّ، لماذا ظهرت «water» جارًا ثانيًا لـ«car»؟
لأن النص 48 كلمة فقط، فالإحصاء فيه ضجيج. مع نص أكبر بكثير تستقرّ الصحبة وتختفي هذه الجيرة العارضة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي1
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.