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

الوحدة 7 — التعرّف على الكيانات المسمّاة

في الوحدة السابقة، وسمنا الجملة كاملة بصنف واحد. في هذه الوحدة، سنَسِمُ كلّ جُذاذة على حدة. المهمّة تسمّى التعرّف على الكيانات المسمّاة (NER)، وهدفها استخراج أشياء بعينها من النصّ: أسماء أشخاص، وأماكن، ومنظّمات، ومنتجات، وتواريخ، ومبالغ.

على مراجعاتنا، سنطلب من النموذج استخراج:

  • اسم المنتج: آيفون 15، سماعات سوني
  • العلامة التجارية: أبل، سامسونغ
  • مكان التوصيل: الرياض، الدار البيضاء

هذا يفتح استعمالات فوريّة: تجميع المشاعر لكلّ منتج، ورصد المدن التي يكثر فيها التذمّر من التوصيل، وحصر العلامات الأكثر ذكرًا.

وسم BIO: كيف يُشير النموذج إلى الكيان

الجُذاذة الواحدة قد تكون جزءًا من كيان يمتدّ على عدّة جُذاذات. آيفون 15 برو ماكس كيان واحد من أربع جُذاذات. كيف نُعبّر عن ذلك؟

المخطّط المعياري هو BIO (Begin, Inside, Outside):

  • B-PRODUIT: أوّل جُذاذة من كيان من نوع «منتج».
  • I-PRODUIT: جُذاذة داخلية من كيان من نوع «منتج».
  • O: جُذاذة خارج أيّ كيان.

مثال:

الجملةاشتريتآيفون15بروماكسمننون
الوسمOB-PRODI-PRODI-PRODI-PRODOB-BRAND

المخطّط بسيط لكنّه يفرض قيدًا مهمًّا: لا يمكن أن تسبق I-X جُذاذة ليست B-X أو I-X. هذا يمنع تداخل الكيانات ويسمح بفكّها بلا لبس.

بديل شائع هو BILOU (يُضاف L- لنهاية الكيان وU- للكيان الوحيد الجُذاذة). أدقّ نظريًّا، لكنّه يُنتج عددًا مضاعفًا من الوسوم ويتطلّب بيانات أكثر. BIO هو المعيار الفعليّ.

المشكلة الحرجة: محاذاة الجُذاذات والوسوم

هنا يقع أشيع خطأ في تدريب نماذج NER بالوحدات الجزئية. البشر يوسمون على مستوى الكلمات، والنموذج يعمل على مستوى الوحدات الجزئية. آيفون قد يقطّعها المقطّع إلى آي + ##فون. الوسم الأصلي B-PROD يجب توزيعه بشكل ما على الجزأين.

الاختيار الشائع: الجزء الأوّل يأخذ الوسم الأصلي (B-PROD)، والأجزاء التالية تأخذ إمّا الوسم I-PROD أو تُتجاهل (-100 في PyTorch).

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("aubmindlab/bert-base-arabertv02")

def hadhi_wusum(kalimat, wusum):
"""تحاذي كلمات مع وحدات جزئية، وتوزع الوسوم على الوحدات."""
m = tok(kalimat, is_split_into_words=True, truncation=True, padding=True)
wusum_muhadhat = []
for i, w in enumerate(wusum):
kalimah_id = m.word_ids(batch_index=i)
sabiq = None
wusum_shatra = []
for id_ in kalimah_id:
if id_ is None:
wusum_shatra.append(-100) # جذاذة خاصة
elif id_ != sabiq:
wusum_shatra.append(w[id_]) # أول جزء من الكلمة
else:
# الأجزاء التالية: I-X إذا كان الوسم B-X، وإلا كما هو
wusum_shatra.append(-100) # اختيار "تجاهل"
sabiq = id_
wusum_muhadhat.append(wusum_shatra)
m["labels"] = wusum_muhadhat
return m

خطأ محاذاة لا يظهر إلّا في الاختبار: النموذج يعلم أن الجُذاذة الأولى فقط تحمل معلومة الكيان. عند إعادة التركيب، إذا أخذنا وسم كلّ الجُذاذات بدل الأولى فقط، نُنتج كيانات مقطَّعة. القاعدة: وسم الكلمة = وسم أوّل جُذاذاتها.

بناء نموذج NER كامل

الشيفرة قريبة جدًّا من الوحدة السابقة، مع فارق واحد: AutoModelForTokenClassification بدل ForSequenceClassification.

from transformers import AutoModelForTokenClassification, TrainingArguments, Trainer
from datasets import Dataset
import numpy as np

WUSUM = ["O", "B-PROD", "I-PROD", "B-BRAND", "I-BRAND", "B-LOC", "I-LOC"]
wasm2id = {w: i for i, w in enumerate(WUSUM)}

# الجسم: قوائم كلمات وقوائم وسوم بموازاتها
bianat = {"kalimat": ..., "wusum": ...}
ds = Dataset.from_dict(bianat).map(
lambda x: hadhi_wusum(x["kalimat"], [[wasm2id[w] for w in seq] for seq in x["wusum"]]),
batched=True,
)

model = AutoModelForTokenClassification.from_pretrained(
"aubmindlab/bert-base-arabertv02",
num_labels=len(WUSUM),
id2label={i: w for w, i in wasm2id.items()},
label2id=wasm2id,
)

args = TrainingArguments(
output_dir="./ner",
num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=3e-5,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)

mudarrib = Trainer(model=model, args=args, train_dataset=ds["train"], eval_dataset=ds["test"])
mudarrib.train()

المقاييس: F1 لكلّ كيان، لا الدقّة الإجمالية

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

المقاييس الصحيحة تُحسَب على مستوى الكيانات الكاملة، بمكتبة seqeval:

from seqeval.metrics import classification_report, f1_score
from seqeval.scheme import IOB2

y_haqiqi = [[WUSUM[w] for w in seq if w != -100] for seq in ...]
y_tanabbu = [[WUSUM[np.argmax(p)] for p, w in zip(seq_p, seq_w) if w != -100]
for seq_p, seq_w in zip(tanabbu, wusum_haqiqi)]

print(classification_report(y_haqiqi, y_tanabbu, scheme=IOB2, mode="strict", digits=3))
print("F1 عام:", f1_score(y_haqiqi, y_tanabbu, mode="strict"))

الوضع «الصارم» (mode="strict") يعتبر الكيان صحيحًا فقط إذا كانت حدوده كاملة ونوعه صحيح. هذا هو المعيار الحقيقي. آيفون 15 برو صحيح فقط إذا التقطنا الجُذاذات الثلاث كلّها؛ التقاط آيفون 15 وحدها خطأ.

نتيجة نمطية على مراجعاتنا العربية:

              precision    recall  f1-score   support
PROD 0.876 0.842 0.859 412
BRAND 0.923 0.951 0.937 298
LOC 0.789 0.723 0.755 87

micro avg 0.891 0.865 0.878 797
macro avg 0.863 0.839 0.850 797

قراءة: BRAND أدقّ من PROD لأنّ العلامات كائنات مغلقة العدد (بضع عشرات). LOC أضعف لأنّ الأماكن غير المعروفة كثيرة (قرى، أحياء) ولا نموذج مسبق يحفظها كلّها.

معالجة الحالات الخاصّة في العربية

الالتصاق: والآيفون، فسامسونغ. المقطّع الجيّد يقسم و + ال + آيفون تلقائيًّا. النموذج المدرَّب على بيانات موسومة بشكل صحيح يتعلّم أنّ و وال جزء من O بينما آيفون جزء من B-PROD.

التنويعات الإملائية: آيفون، ايفون، أيفون. إذا لم نوحّد الهمزات في التنظيف (قد لا نفعل مع نموذج مدرَّب مسبقًا)، على النموذج أن يعمّم. عادة يفعل، بشرط رؤية عدد كافٍ من كلّ صيغة في التدريب.

التداخل مع الوصف: آيفون الجديد هل الجديد جزء من الكيان أو صفة خارجه؟ اتّفاق سلَف: الصفة خارج الكيان (O). لكن جوّال أحمر قد يكون كيانًا كاملًا لأنّ اللون يميّز المنتج فعليًّا. قرارات مثل هذه توثَّق في دليل الوسم الذي يوزَّع على المعلِّقين، ويحسم فيما بعد كلّ حالة مشابهة.

تجميع الاستنتاج على المراجعات

بعد التدريب، الاستنتاج مباشر:

from transformers import pipeline

qanat = pipeline("ner", model="./ner", aggregation_strategy="simple")
kiyanat = qanat("اشتريت آيفون 15 برو ماكس من نون توصيل بطيء في الرياض")
for k in kiyanat:
print(f"{k['entity_group']:6} {k['score']:.3f} {k['word']}")

# PROD 0.987 آيفون 15 برو ماكس
# BRAND 0.994 نون
# LOC 0.921 الرياض

aggregation_strategy="simple" تجمع الجُذاذات المتتالية من النوع نفسه تلقائيًّا. بدونها نحصل على قائمة جُذاذات مفكّكة يلزم إعادة تجميعها يدويًّا.

تجميع الاستنتاج ليس دائمًا الخيار الافتراضي

aggregation_strategy="none" (الافتراضي في بعض الإصدارات) يعطي جُذاذات مفصولة، فيبدو أنّ النموذج يخطئ (آيفون كيان و15 كيان آخر). قبل تشخيص خطأ في التدريب، تأكّد من استراتيجية التجميع. تسعة من عشرة «أخطاء» في NER الإنتاج هي في الحقيقة أخطاء تجميع.

قِس عملك بالكيانات لا بالنسبة المئوية

لتقدير مدى كفاية نموذجك، لا تكتفِ بعرض F1. اعرض عيّنة من 20 كيانًا التقطها النموذج و20 كيانًا فاته. القراءة اليدوية تكشف أنماطًا لا يكشفها الرقم: هل يفشل النموذج على أسماء بعينها؟ على أطوال معيّنة؟ على مواضع محدّدة في الجملة؟ ساعة قراءة تعطي دفعتين من الأفكار للتحسين.

في الخلاصة

  • وسم BIO يعبّر عن كيانات ممتدّة بجُذاذات B (بداية) وI (داخل) وO (خارج)؛ بديله BILOU أدقّ نظريًّا لكنّه يتطلّب بيانات أكثر.
  • محاذاة الوسوم مع الوحدات الجزئية حاسمة: الاصطلاح الأشيع أنّ أوّل جُذاذة من الكلمة تحمل الوسم، والتالية تُتجاهَل في الخسارة.
  • مقاييس seqeval بالوضع الصارم تُقيّم على مستوى الكيانات كاملة الحدود، وهي الوحيدة الصادقة؛ الدقّة على مستوى الجُذاذات مضلّلة بسبب هيمنة O.
  • العربية تطرح تحدّيات محدّدة (الالتصاق، تنويعات الإملاء، الوصف المتداخل)، تحلّها بيانات متنوّعة ومقطّع مناسب ودليل وسم واضح.

الوحدة التالية: التلخيص التلقائي والإجابة عن الأسئلة، حيث سيصبح المخرج نصًّا لا وسمًا، مع كلّ ما يجلبه ذلك من صعوبات في التقييم.