الوحدة 6 — تصنيف النصوص وتحليل المشاعر
بلغنا نقطة التحوّل. الوحدات الأولى أعدّت النصّ (1، 2)، بنَتْ حدًّا أدنى (3)، وأنتجت متّجهات لكلمات وجُمَل (4، 5). حان الوقت لضبط نموذج مدرَّب مسبقًا على مهمّة تصنيف كاملة، ومقارنة النتيجة بالمرجع الذي رفعناه في الوحدة 3.
المهمّة: توقّع نجمة المراجعة (سلبية: 1-2، محايدة: 3، إيجابية: 4-5) من نصّها. نستعمل AraBERT كنموذج مدرَّب مسبقًا، ونضبطه دقيقًا على 8000 مراجعة، مع مجموعة تحقّق من 2000 مراجعة.
لماذا الضبط الدقيق أفضل من التدريب من الصفر
النموذج المدرَّب مسبقًا يعرف العربية عمومًا: نحوها، ومفرداتها، و سياقاتها الشائعة. جسم مراجعاتنا صغير (10 000 نصّ) بعيدًا كلّ البعد عمّا يلزم لتعلّم اللغة من الصفر (مليار كلمة). الضبط الدقيق يستعمل هذه المعرفة الجاهزة، ويُغيّر منها فقط ما يلزم لتخصيص التنبّؤ.
عمليًّا: نضيف طبقة تصنيف بسيطة (خطّية) فوق متّجه الجُذاذة الخاصّة [CLS] أو فوق متوسّط الجُذاذات، ونُدرّب الجميع بمعدّل تعلّم صغير (نموذجيًّا ) لثلاث حقب فقط. النموذج يصل غالبًا إلى ذروته في هذه الحقب الثلاث؛ إطالة التدريب تُنتج إفراطًا في التخصيص.
على مراجعاتنا، هذا يعطي 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 نجوم) | 6200 | 62% |
| محايد (3 نجوم) | 1500 | 15% |
| سلبي (1-2 نجوم) | 2300 | 23% |
إن دُرِّب النموذج بلا احتياط، تعلَّم غالبًا التنبّؤ الأكثر أمانًا: كلّ شيء إيجابي. ينال دقّة (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