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

الوحدة 6 — تصنيف النصوص وتحليل المشاعر

بلغنا نقطة التحوّل. الوحدات الأولى أعدّت النصّ (1، 2)، بنَتْ حدًّا أدنى (3)، وأنتجت متّجهات لكلمات وجُمَل (4، 5). حان الوقت لضبط نموذج مدرَّب مسبقًا على مهمّة تصنيف كاملة، ومقارنة النتيجة بالمرجع الذي رفعناه في الوحدة 3.

المهمّة: توقّع نجمة المراجعة (سلبية: 1-2، محايدة: 3، إيجابية: 4-5) من نصّها. نستعمل AraBERT كنموذج مدرَّب مسبقًا، ونضبطه دقيقًا على 8000 مراجعة، مع مجموعة تحقّق من 2000 مراجعة.

لماذا الضبط الدقيق أفضل من التدريب من الصفر

النموذج المدرَّب مسبقًا يعرف العربية عمومًا: نحوها، ومفرداتها، وسياقاتها الشائعة. جسم مراجعاتنا صغير (10 000 نصّ) بعيدًا كلّ البعد عمّا يلزم لتعلّم اللغة من الصفر (مليار كلمة). الضبط الدقيق يستعمل هذه المعرفة الجاهزة، ويُغيّر منها فقط ما يلزم لتخصيص التنبّؤ.

عمليًّا: نضيف طبقة تصنيف بسيطة (خطّية) فوق متّجه الجُذاذة الخاصّة [CLS] أو فوق متوسّط الجُذاذات، ونُدرّب الجميع بمعدّل تعلّم صغير (نموذجيًّا 21052 \cdot 10^{-5}) لثلاث حقب فقط. النموذج يصل غالبًا إلى ذروته في هذه الحقب الثلاث؛ إطالة التدريب تُنتج إفراطًا في التخصيص.

على مراجعاتنا، هذا يعطي F1 موزون في حدود 0,90، مقابل 0,86 للـTF-IDF. الفارق أربع نقاط. هل يستحقّ؟ الإجابة تتوقّف على البقيّة.

الشيفرة الكاملة بمكتبة Transformers

from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import TrainingArguments, Trainer
import numpy as np
from sklearn.metrics import classification_report

# 1. تحميل الجسم وتحويله إلى Dataset
bianat = {
"text": muraja3at, # قائمة النصوص المنظفة
"label": tasnifat, # 0=سلبي، 1=محايد، 2=إيجابي
}
ds = Dataset.from_dict(bianat).train_test_split(test_size=0.2, seed=42)

# 2. المقطع والنموذج
namudhaj_ism = "aubmindlab/bert-base-arabertv02"
tok = AutoTokenizer.from_pretrained(namudhaj_ism)

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

ds_muqatta3 = ds.map(qatti3, batched=True)

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

# 3. التدريب
def maqayis(pred):
y_hat = np.argmax(pred.predictions, axis=1)
y = pred.label_ids
from sklearn.metrics import f1_score
return {"f1_weighted": f1_score(y, y_hat, average="weighted")}

args = TrainingArguments(
output_dir="./musannif",
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
learning_rate=2e-5,
weight_decay=0.01,
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_muqatta3["train"],
eval_dataset=ds_muqatta3["test"],
compute_metrics=maqayis,
)
mudarrib.train()

يُنجَز التدريب في نحو 20 دقيقة على GPU من فئة T4. النموذج النهائي محفوظ في ./musannif، جاهز للتحميل والاستعمال.

اختلال الأصناف: المشكلة الحقيقية

مراجعات النجوم موزّعة توزيعًا غير متساوٍ. في جسمنا:

الصنفالعددالنسبة
إيجابي (4-5 نجوم)620062%
محايد (3 نجوم)150015%
سلبي (1-2 نجوم)230023%

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

ثلاث تقنيات معتادة، غالبًا تُجمَع:

  • إعادة الوزن في الخسارة: يُعطى كلّ صنف وزنًا معكوسًا لتواتره في التدريب. الخطأ على مراجعة سلبية «يكلّف» أكثر من الخطأ على إيجابية.
  • إعادة العيّنة: نأخذ عيّنة عشوائية أكبر من الصنف النادر، أو نقلّص الصنف السائد. المشكلة: إفراط في تخصيص الصنف النادر إذا أفرطنا في التكرار.
  • الخسارة البؤرية (Focal Loss): تركّز الخسارة على الأمثلة الصعبة، وتخفّف من وزن الأمثلة السهلة. مفيدة حين يكون النموذج شبه متأكّد.
import torch
from torch import nn

# أوزان معكوسة للتواتر
awzan_asnaaf = torch.tensor([0.62 / 0.23, 0.62 / 0.15, 1.0])

class MudarribMawzun(Trainer):
def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None):
labels = inputs.pop("labels")
outputs = model(**inputs)
logits = outputs.logits
loss_fct = nn.CrossEntropyLoss(weight=awzan_asnaaf.to(logits.device))
loss = loss_fct(logits, labels)
return (loss, outputs) if return_outputs else loss

مصفوفة الالتباس: أين يخطئ النموذج فعلًا؟

الدقّة الإجمالية لا تحكي القصّة كاملة. مصفوفة الالتباس تحكيها.

from sklearn.metrics import confusion_matrix
import pandas as pd

y_haqiqi = ds_muqatta3["test"]["label"]
y_tanabbu = np.argmax(mudarrib.predict(ds_muqatta3["test"]).predictions, axis=1)

mat = confusion_matrix(y_haqiqi, y_tanabbu)
print(pd.DataFrame(
mat,
index=["سلبي", "محايد", "إيجابي"],
columns=["توقع سلبي", "توقع محايد", "توقع إيجابي"],
))

نتيجة نمطية على مراجعاتنا العربية بعد إعادة الوزن:

توقع سلبيتوقع محايدتوقع إيجابي
سلبي3805525
محايد4019060
إيجابي15851150

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

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

المقارنة مع الحدّ الأدنى TF-IDF

على مجموعة الاختبار نفسها:

نموذجF1 موزونزمن التدريبذاكرةكلفة استدلال
TF-IDF + LogReg0,8630 ثانية200 ميغابايتملّي‌ثانية على معالج
AraBERT مضبوط دقيقًا0,9020 دقيقة على GPU500 ميغابايت30 ملّي‌ثانية على GPU

فارق 4 نقاط F1 مقابل زمن استدلال 30 مرّة أكبر وذاكرة ضعف. القرار يعتمد على السياق. لمنصّة تعالج مليون مراجعة يوميًّا، الفارق يترجم إلى فاتورة GPU ملموسة. لتقرير أسبوعي داخلي على 1000 مراجعة، الاختيار الأمثل هو TF-IDF لبساطته وقابلية تفسيره.

عائد الضبط الدقيق يتناقص مع الجسم الكبير

مع 500 مراجعة، الفارق بين TF-IDF وAraBERT قد يبلغ 15 نقطة F1. مع 100 000 مراجعة، ينكمش إلى نقطتين. النموذج المدرَّب مسبقًا يقدّم قيمته حين تكون البيانات قليلة، لأنّه يأتي بمعرفة سابقة كبيرة. حين تكون البيانات وافرة، يمكن للنماذج البسيطة أن تلحق. قِس الفارق على حجمك الفعلي لا على أدبيّات مبنيّة على أجسام صغيرة.

حفظ المصنّف والمقطّع معًا

احفظ دائمًا المقطّع مع أوزان النموذج بـtok.save_pretrained("./musannif"). النموذج بلا مقطّعه المطابق لا يعمل. أشيع خطأ في النشر هو تحميل المقطّع الافتراضي بدل المقطّع الذي عولج به التدريب، فتظهر جُذاذات لا تفهمها الشبكة وتنهار الدقّة صمتًا.

في الخلاصة

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

الوحدة التالية: التعرّف على الكيانات المسمّاة، حيث ننتقل من تصنيف الجملة كاملة إلى وسم كلّ جُذاذة على حدة، وسنستخرج أسماء المنتجات من المراجعات.