البناء والتقييم
درس 2 من 3
2البناء والتقييم
الصورة الكبيرةالبناء يبدأ بأبسط نسخة تعمل من البداية للنهاية، حتى لو كانت ضعيفة: مدخل حقيقي يدخل، ونتيجة تخرج، ورقم يُحسب على مجموعة التقييم. بعدها كل تحسين تجربة لها سطر في جدول: ماذا غيّرت، ولماذا، وكم صار الرقم. والتوقّف قرار بالأرقام أيضًا: حين تتجاوز معيار النجاح، أو حين تتوقّف التجارب عن تحريك الرقم.
ماذا تشاهد في الرسم: كل دورة تجربة واحدة لها سطر في الجدول، وتحليل الأخطاء يختار التغيير التالي. الاختبار في الوسط لا في الدورة: يُفتح مرة في النهاية، كما قيس forest_bal مرة فأعطى 0.738.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
ابدأ بأبسط نسخة تعمل من البداية إلى النهاية، حتى لو كانت ضعيفة، مثل بيت من غرفة واحدة لكن له سقف وباب. ثم حسّنها خطوة خطوة، وبعد كل خطوة اسأل: هل تحسّن الرقم؟ لا تعتمد على شعورك، واكتب كل تجربة في دفتر.
خط كامل بسيط أولًا، ثم تحسينات يقاس كل منها على مجموعة التقييم نفسها، مع تتبع التجارب (الإعدادات والبيانات والإصدار والنتيجة) بأداة مثل MLflow أو جدول. تغيّر شيئًا واحدًا في كل تجربة لتعرف سبب الفرق.
قبل أن تبدأ
🎯اختيار المشكلة
عندك مستخدم ومعيار نجاح مكتوب ومجموعة تقييم مجمّدة وخط أساس.
✂️تقسيم البيانات
تعرف لماذا يُختار على التحقّق ويُقاس على الاختبار.
الفكرة الجوهرية
البناء يمرّ بأربع خطوات: نسخة أولى تعمل من البداية للنهاية، ثم جدول تجارب بتغيير واحد في كل سطر، ثم تحليل الأخطاء بالشرائح، ثم قرار التوقّف بالأرقام.
1النسخة الأولى: من البداية للنهاية
ابدأ بأبسط شيء يعمل كاملًا خلال أيام، ولو كان ضعيفًا. انظر إلى الرسم: مدخل حقيقي يدخل، ونتيجة تخرج، ورقم يُحسب على مجموعة التقييم.
مجموعة التقييم مجمّدة، وفيها شرائح موسومة مثل اللهجة والطول ونوع الطلب. بهذا تعرف أين يفشل النظام، لا كم فقط.
2جدول التجارب: تغيير واحد في كل سطر
كل تجربة سطر: ما تغيّر، ولماذا، والرقم، والوقت. تغيير واحد في كل مرة حتى تعرف ما الذي أثّر.
انظر إلى الرسم: أربع تجارب على مشكلة غير متوازنة من المثال المحلول، وF1 على مجموعة التحقّق.
موازنة الأوزان ضاعفت الرقم، والغابة تفوّقت لأن العلاقة غير خطية، والجمع بين الفكرتين أعطى الأفضل. تختار على التحقّق، وتفتح الاختبار مرة واحدة في النهاية.
3تحليل الأخطاء بالشرائح
الرقم الإجمالي يخبرك كم يخطئ النظام، وتحليل الأخطاء يخبرك ماذا تفعل بعد ذلك. اقرأ الأخطاء نفسها وصنّفها في شرائح قبل أي تجربة جديدة.
انظر إلى الرسم: من أين تأتي 23 خطأ في 120 رسالة.
نتائج تقييم لـ120 رسالة، كل منها موسومة بشريحتها عند بناء المجموعة. الدقة الإجمالية تخفي القصة، فنقسّمها.
from collections import defaultdict
# eval results: (text, slice, correct?) - slices were tagged when the eval set was built
results = (
[("رسالة فصحى قصيرة", "msa_short", True)] * 46 + [("رسالة فصحى قصيرة", "msa_short", False)] * 4
+ [("رسالة عامية قصيرة", "dialect_short", True)] * 33 + [("رسالة عامية قصيرة", "dialect_short", False)] * 7
+ [("رسالة طويلة فيها أكثر من طلب", "multi_intent", True)] * 9 + [("رسالة طويلة فيها أكثر من طلب", "multi_intent", False)] * 11
+ [("English message", "english", True)] * 9 + [("English message", "english", False)] * 1
)
overall = sum(ok for *_, ok in results) / len(results)
print(f"overall accuracy: {overall:.0%} on {len(results)} examples\n")
by_slice = defaultdict(list)
for _, s, ok in results:
by_slice[s].append(ok)
total_errors = sum(not ok for *_, ok in results)
print(f"{'slice':<14} {'n':>3} {'acc':>5} {'share of errors':>16}")
for s, oks in sorted(by_slice.items(), key=lambda kv: sum(kv[1]) / len(kv[1])):
errors = len(oks) - sum(oks)
print(f"{s:<14} {len(oks):>3} {sum(oks) / len(oks):>5.0%} {errors / total_errors:>16.0%}")
81% إجمالًا يبدو مقبولًا، لكن الرسائل التي تحتوي أكثر من طلب تفشل في أكثر من نصف الحالات، وهي وحدها نصف الأخطاء تقريبًا رغم أنها سدس المجموعة. التجربة التالية واضحة إذن: لا ضبط معاملات، بل تقسيم الرسالة إلى طلبات قبل التصنيف، أو إحالتها لإنسان. والعامية في المرتبة الثانية، فهي هدف التجربة بعدها.
4متى تتوقّف
التوقّف قرار بالأرقام لا بالتعب. انظر إلى الرسم: سؤالان يقودانك إلى القرار.
إن تجاوزت المعيار بهامش واضح فتوقّف. وإن لم تحرّك ثلاث تجارب متتالية الرقم أكثر من مدى الخطأ، فالتجارب الإضافية لن تغيّر شيئًا، فتوقّف أيضًا وراجع المشكلة.
كيف يعمل، خطوة بخطوة: كيف تدير التجارب حتى تتوقّف
تبني أبسط نسخة تعمل من البداية للنهاية في الأسبوع الأول، وتقيسها على مجموعة التقييم.
تقرأ الأخطاء وتقسّمها شرائح، وتختار تجربة موجّهة للشريحة الأكبر في الأخطاء.
تغيّر شيئًا واحدًا في كل تجربة، وتسجّل في الجدول ما غيّرت ولماذا والرقم والزمن.
تتوقّف حين تتجاوز المعيار بفارق واضح أو حين تتحرّك ثلاث تجارب داخل مدى الخطأ، ثم تقيس الاختبار مرة.
أعمق قليلًا
في مشاريع النماذج اللغوية، التجربة قد تكون تعديلًا في التعليمات أو في عدد المقاطع المسترجعة أو في النموذج نفسه. نفس القاعدة: سطر في الجدول لكل تغيير، ونسخة التعليمات محفوظة في git، والتقييم يُشغَّل آليًّا على كل المجموعة لا على خمسة أمثلة تتذكّرها. والتقييم بنموذج لغوي حَكَم يحتاج أن تتحقّق منه يدويًّا على عيّنة أولًا، كما رأيت في درس التقييم.
مثال محلول
جدول التجارب هو ذاكرة مشروعك. بعد أسبوعين لن تتذكّر لماذا اخترت هذا الإعداد، ولا أي نسخة أعطت أفضل رقم. هنا أربع تجارب على مشكلة تصنيف غير متوازنة، كل منها تُسجَّل في ملف بسطر واحد.
١. أربع تجارب وجدول
import json
import time
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import f1_score
X, y = make_classification(n_samples=2000, n_features=20, n_informative=6,
weights=[0.8, 0.2], random_state=7)
X_tr, X_rest, y_tr, y_rest = train_test_split(X, y, test_size=0.4, stratify=y, random_state=0)
X_val, X_te, y_val, y_te = train_test_split(X_rest, y_rest, test_size=0.5, stratify=y_rest, random_state=0)
log = []
def run(name, model, note):
t0 = time.perf_counter()
model.fit(X_tr, y_tr)
entry = dict(run=len(log) + 1, name=name, note=note,
val_f1=round(f1_score(y_val, model.predict(X_val)), 3),
seconds=round(time.perf_counter() - t0, 2))
log.append(entry)
with open("runs.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
return model
models = {
"logreg": run("logreg", LogisticRegression(max_iter=1000), "first version"),
"logreg_bal": run("logreg_bal", LogisticRegression(max_iter=1000, class_weight="balanced"), "minority class is rare"),
"forest": run("forest", RandomForestClassifier(n_estimators=200, random_state=0), "non-linear"),
"forest_bal": run("forest_bal", RandomForestClassifier(n_estimators=200, class_weight="balanced", random_state=0), "both ideas"),
}
print(f"{'run':>3} {'name':<11} {'val F1':>6} note")
for e in log:
print(f"{e['run']:>3} {e['name']:<11} {e['val_f1']:>6} {e['note']}")
best = max(log, key=lambda e: e["val_f1"])
test_f1 = f1_score(y_te, models[best["name"]].predict(X_te))
print(f"\nchosen on validation: {best['name']}; test F1, measured once: {test_f1:.3f}")
كل تجربة غيّرت شيئًا واحدًا وكتبت سبب التغيير. الانحدار اللوجستي بدأ بـ0.21 لأن الفئة النادرة تُتجاهل، وموازنة الأوزان ضاعفت الرقم، والغابة العشوائية تفوّقت لأن العلاقة غير خطية، والجمع بين الفكرتين أعطى الأفضل. ثم اخترنا على مجموعة التحقّق، وقسنا على الاختبار مرة واحدة فقط: 0.738، أقل قليلًا من 0.769 كما هو متوقّع، لأن الاختيار نفسه يجعل رقم التحقّق متفائلًا.
مثال: تسجيل التجارب بـMLflow
حين يكبر الجدول، أداة مثل MLflow تحفظ المعاملات والمقاييس والنموذج نفسه وتعرضها في واجهة. تحتاج pip install mlflow، فهي للقراءة هنا.
import mlflow
from sklearn.metrics import f1_score
mlflow.set_experiment("shop-support-classifier")
with mlflow.start_run(run_name="forest_bal"):
mlflow.log_params({"model": "random_forest", "n_estimators": 200,
"class_weight": "balanced", "data_version": "eval-v1-frozen"})
mlflow.set_tag("why", "minority class ignored + non-linear signal")
model.fit(X_tr, y_tr)
mlflow.log_metric("val_f1", f1_score(y_val, model.predict(X_val)))
mlflow.sklearn.log_model(model, "model")
mlflow ui يفتح جدولًا تقارن فيه التشغيلات وترتّبها، ونسخة البيانات مسجّلة مع كل تشغيل فتعرف إن كانت المقارنة عادلة. لكن ملف JSONL بسيط كما في المثال الأول يكفي تمامًا لمشروع فردي، والمهم العادة لا الأداة.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| تجارب أكثر على المعاملات الفائقة هي طريق التحسّن. | تجربة موجّهة لشريحة فاشلة تتفوّق غالبًا على عشر تجارب عشوائية. في المثال كانت الرسائل متعدّدة الطلبات نحو نصف الأخطاء. |
| رقم التحقّق للنسخة المختارة هو رقمها الحقيقي. | الاختيار نفسه يجعله متفائلًا: 0.769 على التحقّق و0.738 على الاختبار المقاس مرة. |
| التجارب الفاشلة لا تستحق التسجيل. | سطر «جرّبت نموذجًا أكبر فلم يتحرّك الرقم وتضاعف الزمن» يوفّر تكرارها، ويُقنع المراجع أنك لم تنتقِ النتيجة الجيدة. |
جرّب بنفسك
جرّبت 30 إعدادًا وقست كلًّا منها على مجموعة الاختبار، واخترت الأفضل. ما مشكلة رقمه؟
حين تختار الأعلى بين محاولات كثيرة على نفس البيانات، تختار جزئيًّا الأكثر حظًّا معها. لهذا يُختار على التحقّق ويُقاس الاختبار مرة واحدة.
الدقة الإجمالية 81%، وشريحة الرسائل متعدّدة الطلبات 45% ونصف الأخطاء منها. أي تجربة تبدأ بها؟
الشريحة الأكبر في الأخطاء هي أكبر فرصة. إصلاحها يرفع الإجمالي بأكثر مما يرفعه تحسين عام، وغالبًا بتكلفة أقل.
بطاقات مراجعة سريعة
تحقق من فهمك
بعد تسع تجارب، الرقم تحرّك من 0.81 إلى 0.83 ثم 0.82 ثم 0.83، ومدى الخطأ على مجموعتك ±0.05، والمعيار 0.85. ماذا تفعل؟
توقّف عن نفس النوع من التجارب، فالتغيّرات داخل الضجيج. ارجع لتحليل الأخطاء وابحث عن شريحة تفشل بشكل منهجي، أو عن مشكلة في البيانات نفسها مثل تسميات متناقضة. وإن لم تجد، ناقش المستخدم: ربما 0.83 مع إحالة الحالات غير المؤكّدة لإنسان يحقّق ما يحتاجه فعلًا.
لماذا يُكتب إصدار البيانات مثل eval-v1-frozen مع كل تجربة؟
حتى تعرف أن تجربتين قيستا على نفس المجموعة. مقارنة رقمين من مجموعتين مختلفتين ليست مقارنة عادلة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.