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

الوحدة 10 — مشروع: مصنّف مستندات بلغة الدورة

تسع وحدات من الآليات. تجمعها هذه الوحدة في مشروع كامل قابل للتنفيذ. المهمّة: مصنّف مراجعات عربية إلى ثلاث مشاعر (سلبي/محايد/إيجابي)، مع مقارنة رسمية بين ثلاث مقاربات: TF-IDF مع انحدار لوجستي، تضمينات جُمَل ثابتة مع مصنّف صغير، وAraBERT مضبوط دقيقًا. الهدف ليس اختيار «الأفضل» بشكل مطلق، بل إظهار مقايضة صادقة بين الجودة والكلفة والزمن.

البيانات وخصوصيّات العربية

الجسم: 10 000 مراجعة بالعربية على منتجات إلكترونية، من مصادر متنوّعة (متاجر، تطبيقات، منصّات تقييم). كلّ مراجعة نصّ من 15 إلى 200 كلمة، ونجمة من 1 إلى 5 نحوّلها إلى ثلاث فئات.

قبل الشروع، خصوصيات العربية التي يجب أن يعالجها المشروع بشكل صريح:

  • التشكيل: نادر ومتقطّع في المراجعات؛ يُحذَف بالكامل.
  • أشكال الهمزة والألف المقصورة والتاء المربوطة: توحيدها في مسار TF-IDF فقط، ومنعه في مسار AraBERT (يفضّل النصّ كما هو).
  • الالتصاق: وبالمنتج، فسنشتريه. TF-IDF يعاني، والوحدات الجزئية في المقطّعات تحلّها.
  • العامّية: بعض المراجعات بالدارجة (مصرية، خليجية، مغربية). AraBERT الأصلي مُدرَّب على الفصحى؛ CAMeLBERT مقسّم لتغطية العامّيات.
  • الكتابة العربيّزية: مراجعات مكتوبة بأحرف لاتينية (el mountaj momtaz). هذه معضلة قائمة بذاتها؛ نستبعدها من الجسم في هذا المشروع أو نُصنّفها منفصلة.

الخطوة 1: التنظيف حسب المسار

import re
import unicodedata
import pandas as pd

TASHKEEL = re.compile(r"[\u064B-\u0652\u0670\u0640]")

def nazzif_asasi(nass: str) -> str:
"""التنظيف الأساسي: NFC + إزالة التشكيل. مناسب لكل المسارات."""
nass = unicodedata.normalize("NFC", nass)
nass = TASHKEEL.sub("", nass)
return nass.strip()

def nazzif_muwahhad(nass: str) -> str:
"""توحيد كامل: لـ TF-IDF فقط. لا يُستعمل قبل AraBERT."""
nass = nazzif_asasi(nass)
nass = re.sub(r"[إأآا]", "ا", nass)
nass = nass.replace("ى", "ي").replace("ة", "ه")
return nass

df = pd.read_csv("muraja3at.csv")
df["nass_asasi"] = df["nass"].apply(nazzif_asasi)
df["nass_muwahhad"] = df["nass"].apply(nazzif_muwahhad)
df["mash3ar"] = df["nujum"].map({1: 0, 2: 0, 3: 1, 4: 2, 5: 2})

قرار صريح: نحتفظ بعمودَي نصّ. كلّ مسار يستعمل النسخة المناسبة له. هذا يجنّبنا اختراع تنظيف «متوسّط» لا يخدم أحدًا.

الخطوة 2: التقسيم المشترك

قاعدة ذهبية: تقسيم واحد لكلّ المقاربات، حتّى تكون المقارنة صادقة. إن اختلف تقسيم كلّ مقاربة، لا تعود F1 قابلة للمقارنة.

from sklearn.model_selection import train_test_split

X_tr, X_te, y_tr, y_te = train_test_split(
df[["nass_asasi", "nass_muwahhad"]],
df["mash3ar"],
test_size=0.2,
stratify=df["mash3ar"],
random_state=42,
)

stratify=df["mash3ar"] يضمن حفظ نِسَب الأصناف في التدريب والاختبار. بدونها قد نُنشئ اختبارًا شبه إيجابي بالكامل، فتبدو النتائج مبهرة زيفًا.

الخطوة 3: المقاربة الأولى — TF-IDF + انحدار لوجستي

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import f1_score, classification_report

khatt_asasi = Pipeline([
("tfidf", TfidfVectorizer(
ngram_range=(1, 2),
min_df=3,
max_df=0.95,
max_features=30000,
)),
("logreg", LogisticRegression(
max_iter=2000,
class_weight="balanced",
C=1.0,
)),
])

khatt_asasi.fit(X_tr["nass_muwahhad"], y_tr)
tanabbu_tfidf = khatt_asasi.predict(X_te["nass_muwahhad"])
f1_tfidf = f1_score(y_te, tanabbu_tfidf, average="weighted")
print(f"TF-IDF F1: {f1_tfidf:.3f}")

نتيجة نمطية: F1 = 0,857. زمن التدريب: أقلّ من دقيقة على معالج. زمن الاستدلال: أقلّ من ملّي‌ثانية لكلّ نصّ.

الخطوة 4: المقاربة الثانية — تضمينات جُمَل + مصنّف صغير

نستعمل نموذج تضمين جُمَل جاهزًا (Sentence-BERT متعدّد اللغات). لا نُدرّبه ولا نُعدّله؛ نستعمله مولّدًا للمزايا.

from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression

saber = SentenceTransformer("paraphrase-multilingual-mpnet-base-v2")

E_tr = saber.encode(X_tr["nass_asasi"].tolist(), batch_size=64, show_progress_bar=True)
E_te = saber.encode(X_te["nass_asasi"].tolist(), batch_size=64, show_progress_bar=True)

musannif = LogisticRegression(max_iter=2000, class_weight="balanced", C=1.0)
musannif.fit(E_tr, y_tr)
tanabbu_emb = musannif.predict(E_te)
f1_emb = f1_score(y_te, tanabbu_emb, average="weighted")
print(f"Sentence embeddings F1: {f1_emb:.3f}")

نتيجة نمطية: F1 = 0,881. زمن التدريب على LogReg: بضع ثوانٍ (المتّجهات الثابتة صغيرة). زمن التشفير الأوّلي: 3-5 دقائق للجسم كلّه على GPU. زمن الاستدلال: 10 ملّي‌ثانية لكلّ نصّ على GPU، 100 ملّي‌ثانية على معالج.

الخطوة 5: المقاربة الثالثة — AraBERT مضبوط دقيقًا

from datasets import Dataset
from transformers import (
AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer, DataCollatorWithPadding,
)
import numpy as np

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

def qatti3(m):
return tok(m["text"], truncation=True, max_length=128)

ds_tr = Dataset.from_dict({"text": X_tr["nass_asasi"].tolist(), "label": y_tr.tolist()}).map(qatti3, batched=True)
ds_te = Dataset.from_dict({"text": X_te["nass_asasi"].tolist(), "label": y_te.tolist()}).map(qatti3, batched=True)

model = AutoModelForSequenceClassification.from_pretrained(ism, num_labels=3)

def maqayis(pred):
y_hat = np.argmax(pred.predictions, axis=1)
return {"f1_weighted": f1_score(pred.label_ids, y_hat, average="weighted")}

args = TrainingArguments(
output_dir="./arabert_musannif",
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
learning_rate=2e-5,
warmup_ratio=0.1,
weight_decay=0.01,
fp16=True,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1_weighted",
)

mudarrib = Trainer(
model=model, args=args,
train_dataset=ds_tr, eval_dataset=ds_te,
tokenizer=tok,
data_collator=DataCollatorWithPadding(tok),
compute_metrics=maqayis,
)
mudarrib.train()

f1_bert = mudarrib.evaluate()["eval_f1_weighted"]
print(f"AraBERT F1: {f1_bert:.3f}")

نتيجة نمطية: F1 = 0,912. زمن التدريب: 20-25 دقيقة على GPU T4. زمن الاستدلال: 30 ملّي‌ثانية لكلّ نصّ على GPU، 500 ملّي‌ثانية على معالج.

الخطوة 6: المقارنة الرسمية

المقاربةF1 موزونزمن تدريبذاكرة نموذجزمن استدلال (GPU)كلفة تقديرية (10ك مراجعة/يوم)
TF-IDF + LogReg0,85745 ثانية (CPU)5 ميغابايت0,5 ملّي‌ثانيةمعالج بسيط، لا كلفة GPU
Sentence-BERT + LogReg0,8815 دقائق (GPU للتشفير)500 ميغابايت10 ملّي‌ثانيةGPU مشترك، بضعة دولارات شهريًّا
AraBERT مضبوط دقيقًا0,91225 دقيقة (GPU)500 ميغابايت30 ملّي‌ثانيةGPU مخصّص، عشرات الدولارات شهريًّا

الفارق بين TF-IDF وAraBERT: 5,5 نقاط F1 مقابل 60 ضعفًا في زمن الاستدلال وكلفة أعلى بمرّات. هل يستحقّ؟ يتوقّف على السياق:

  • تقرير أسبوعي داخلي على 1000 مراجعة: TF-IDF كافٍ ومناسب.
  • رصد تلقائيّ للمشاعر على مئات آلاف المراجعات يوميًّا للتنبيه في الوقت الحقيقي: AraBERT يستحقّ.
  • مسار تصنيف أوّلي على ملايين المراجعات مع رجوع بشري على العيّنات المشكوك فيها: تضمينات الجُمَل الخيار الوسط الأمثل، تعطي 88 بالمئة بكلفة مقبولة.

الخطوة 7: تحليل الأخطاء المشترك

بغضّ النظر عن المقاربة، بعض الأنماط تتكرّر في الأخطاء على العربية:

  • المراجعات القصيرة جدًّا: جيد جدا، لا أنصح. النموذج لا يملك سياقًا كافيًا.
  • المراجعات المتناقضة داخليًّا: المنتج ممتاز لكن التوصيل كارثي. أيّ فئة تلك؟ الاختيار البشري نفسه غير متّفَق عليه.
  • السخرية: شكرًا للتوصيل السريع بعد شهر واحد فقط. النموذج يقرأها إيجابية؛ البشر يعرفون أنّها سلبية بامتياز.
  • العامّية الشديدة: المنتج على قد الحال. الفصحى تقول «متوسّط»؛ النموذج المدرَّب على الفصحى لا يعرف هذه الصيغة.

هذه الأنماط تُوجّه الجولة القادمة: جمع بيانات مستهدفة على العامّيات، أو نموذج ثنائي فقط (إيجابي/سلبي) لتجنّب مراوغة الفئة الوسطى.

لا تُدرِّب على العامّية وتختبر على الفصحى (أو العكس)

تقسيم عشوائيّ على جسم يخلط الفصحى والعامّية قد يبدو صحيحًا، لكنّه يخفي فجوة أداء ضخمة بين اللهجات. قسّم صراحة حسب اللهجة، وقِس F1 على كلّ منها. نموذج بـ0,90 على الفصحى و0,65 على المغربية ليس نموذجًا بـ0,85 موزونًا؛ هو نموذجان.

المصنّف المزدوج بدل النموذج الواحد الأكبر

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

في الخلاصة

  • العربية تفرض تنظيفًا مسارًا بمسار: نصّ موحَّد لـTF-IDF، ونصّ أساسي لـAraBERT، وأبدًا خلط بين الاثنين.
  • تقسيم واحد يخدم كلّ المقاربات، مع stratify على الصنف، لضمان مقارنة صادقة بين نتائج المقاربات.
  • الفارق بين TF-IDF والنموذج الأثقل يقلّص إلى بضع نقاط F1، بينما يزيد زمن الاستدلال والكلفة عشرات المرّات؛ القرار سياقيّ لا مبدئي.
  • تحليل الأخطاء المشترك يكشف أنماطًا لا يحلّها مزيد من التدريب: القصر، التناقض، السخرية، اللهجات؛ الحلول تنظيمية (بيانات، تصنيف مزدوج) لا نموذجية بحتة.

الوحدة التالية: مراجعة شاملة للدورة، شجرة قرار لاختيار المقاربة المناسبة لكلّ مهمّة، والاختبار النهائي.