تخطَّ إلى المحتوى

تحسين النموذج: ضبط الإعدادات والغابة العشوائية

الدرس 6 من 8 · تعلّم الآلة

بنيت نموذجاً وقيّمته بصدق. السؤال التالي طبيعي: كيف أجعله أفضل؟ في درس النموذج الأول سمعت عن الإفراط في التعلّم (Overfitting): نموذج يحفظ بيانات التدريب بدل أن يفهمها. في هذا الدرس سترى هذه المشكلة بالأرقام، وتتعلم ثلاث طرق عملية للتحسين، ثم تحفظ نموذجك لتستخدمه في تطبيق.

نكمل على بيانات الأورام من الدرس السابق، بالتقسيمة نفسها:

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)

الإعدادات: ما يختاره المبرمج

Section titled “الإعدادات: ما يختاره المبرمج”

النموذج يتعلم أشياء من البيانات: أسئلة الشجرة وأرقامها. لكن هناك أشياء تحددها أنت قبل التدريب، مثل أقصى عمق للشجرة max_depth. هذه اسمها الإعدادات الفائقة (Hyperparameters).

مثل فرن الخبز: العجين يتحول إلى خبز بنفسه، لكنك أنت من يضبط الحرارة والوقت. ضبط خاطئ يعطي خبزاً نيئاً أو محروقاً.

رؤية الإفراط في التعلّم

Section titled “رؤية الإفراط في التعلّم”

ندرّب أشجاراً بأعماق مختلفة، ونقيس الدقة على التدريب وعلى الاختبار:

for depth in [1, 2, 3, 5, 10, None]:
m = DecisionTreeClassifier(max_depth=depth, random_state=42)
m.fit(X_train, y_train)
train_acc = accuracy_score(y_train, m.predict(X_train))
test_acc = accuracy_score(y_test, m.predict(X_test))
print(depth, f'{train_acc:.3f}', f'{test_acc:.3f}')
الناتج
1 0.923 0.923
2 0.958 0.909
3 0.977 0.944
5 0.995 0.937
10 1.000 0.923
None 1.000 0.923

العمود الأول العمق (None تعني بلا حد)، والثاني دقة التدريب، والثالث دقة الاختبار. اقرأ الجدول من الأعلى:

  • دقة التدريب ترتفع باستمرار حتى تصل 100%: الشجرة العميقة حفظت كل عينة.
  • دقة الاختبار ترتفع حتى العمق 3 ثم تتراجع.

الفجوة بين الرقمين هي علامة الإفراط في التعلّم. عند العمق 10: 100% على ما رآه النموذج، و 92.3% على ما لم يره. مثل طالب حفظ إجابات الامتحانات السابقة حرفياً: علامة كاملة فيها، وتعثر أمام أي سؤال جديد.

الحالة التدريب الاختبار العلاج
نقص التعلّم منخفضة منخفضة نموذج أقوى أو ميزات أكثر
توازن جيد عالية قريبة منها هذا ما نريده
إفراط في التعلّم عالية جداً أقل بوضوح تبسيط النموذج أو بيانات أكثر

لا تختر الإعدادات ببيانات الاختبار

Section titled “لا تختر الإعدادات ببيانات الاختبار”

قد تقول: العمق 3 أعطى أفضل اختبار، فلنختره. هنا فخ دقيق: إذا جرّبت عشرات الإعدادات واخترت ما يناسب بيانات الاختبار، فقد «تسرّبت» هذه البيانات إلى قراراتك، ولم تعد اختباراً نزيهاً.

الحل ما تعلمته في الدرس السابق: التحقق المتقاطع على بيانات التدريب وحدها، وبيانات الاختبار تبقى مغلقة حتى النهاية:

for depth in [1, 2, 3, 5, 10]:
scores = cross_val_score(
DecisionTreeClassifier(max_depth=depth, random_state=42),
X_train, y_train, cv=5
)
print(depth, f'{scores.mean():.3f}')
الناتج
1 0.894
2 0.923
3 0.930
5 0.932
10 0.930

العمق 1 ضعيف بوضوح. ومن العمق 3 فصاعداً الأرقام متقاربة جداً، والفروق بينها أصغر من التذبذب الطبيعي بين تقسيمة وأخرى. الدرس الصادق هنا: ضبط العمق وحده لن يأخذنا أبعد من نحو 93%. نحتاج فكرة أقوى.

شجرة واحدة تتأثر بسهولة بتفاصيل بياناتها. ماذا لو درّبنا مئة شجرة، كل واحدة على عينة عشوائية مختلفة من البيانات، ثم أخذنا رأي الأغلبية؟ هذه هي الغابة العشوائية (Random Forest):

from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=100, random_state=42)
forest.fit(X_train, y_train)
cv_scores = cross_val_score(
RandomForestClassifier(n_estimators=100, random_state=42),
X_train, y_train, cv=5
)
print(f'{cv_scores.mean():.3f}')
print(f'{accuracy_score(y_test, forest.predict(X_test)):.3f}')
الناتج
0.960
0.958

قفزة من نحو 93% إلى 96% في التحقق المتقاطع، وعند فتح بيانات الاختبار أخيراً: 95.8%، رقم قريب جداً مما توقعه التحقق المتقاطع. تغيير سطر واحد.

الفكرة مثل لجنة أطباء بدل طبيب واحد: كل طبيب قد يخطئ في حالة، لكن أخطاءهم مختلفة، فرأي الأغلبية أصح من رأي أي فرد. n_estimators عدد الأشجار في الغابة.

والغابة تخبرك أيضاً أي الميزات كانت الأهم في قراراتها:

import pandas as pd
importance = pd.Series(forest.feature_importances_, index=data.feature_names)
print(importance.sort_values(ascending=False).head(3).round(3))
الناتج
worst area 0.150
worst concave points 0.127
mean concave points 0.105
dtype: float64

مساحة الورم وتقعّر حوافه أكثر ما يعتمد عليه النموذج. معلومة تفيدك في فهم المشكلة، وفي شرح النموذج للأطباء.

بعض النماذج حساسة لـ وحدات الميزات. خوارزمية الجيران الأقرب التي تعرفها تحسب المسافة بين العينات. وفي بياناتنا ميزة «المساحة» تتراوح بين 143 و 2501، وميزة «النعومة» بين 0.05 و 0.16. المساحة ستطغى على المسافة كلها، كأن بقية الميزات غير موجودة!

العلاج توحيد المقاييس (Scaling): تحويل كل ميزة ليصبح متوسطها صفراً وتشتتها واحداً:

from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train, y_train)
print(f'{accuracy_score(y_test, knn.predict(X_test)):.3f}')
scaled_knn = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
scaled_knn.fit(X_train, y_train)
print(f'{accuracy_score(y_test, scaled_knn.predict(X_test)):.3f}')
الناتج
0.930
0.979

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

التدريب يحدث مرة واحدة. بعدها تحفظ النموذج في ملف، ويحمّله تطبيقك ليتوقع مباشرة:

import joblib
joblib.dump(forest, 'tumor_model.joblib')
loaded = joblib.load('tumor_model.joblib')
print(loaded.predict(X_test[:5]))
print(y_test[:5])
الناتج
[1 0 1 1 1]
[1 0 1 1 0]

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

  1. كرر تجربة الأعماق مع إضافة الإعداد min_samples_leaf=5 للشجرة، ولاحظ هل تقل الفجوة بين التدريب والاختبار.
  2. درّب غابات بعدد أشجار 10 و 50 و 200، وقارن متوسط التحقق المتقاطع.
  3. جرّب KNeighborsClassifier مع توحيد المقاييس بقيم مختلفة لـ n_neighbors (مثل 1 و 5 و 15).
  4. تحدٍّ: احسب Recall للفئة الخبيثة لأفضل نموذج وصلت إليه، وقارنه بـ Recall شجرة الدرس السابق (0.906).
  • اختيار الإعدادات ببيانات الاختبار: يجعل تقييمك النهائي متفائلاً أكثر من اللازم. اضبط بالتحقق المتقاطع على التدريب، وافتح الاختبار مرة واحدة في النهاية.
  • الفرح بدقة تدريب 100%: هذه علامة تحذير لا إنجاز. قارنها دائماً بدقة الاختبار.
  • توحيد المقاييس قبل التقسيم: حساب المتوسط على البيانات كلها يسرّب معلومات الاختبار. استخدم make_pipeline فيتعلم التوحيد من التدريب وحده.
  • مطاردة آخر نصف بالمئة: بعد حد معيّن، بيانات أكثر وأنظف تفيد أكثر من أي ضبط للإعدادات.

سؤال

نموذج دقته 100% على بيانات التدريب و 80% على بيانات الاختبار. ما المشكلة غالباً؟

سؤال

كيف تتخذ الغابة العشوائية قرارها؟

  • الإعدادات الفائقة مثل max_depth تحددها أنت، وتؤثر في قدرة النموذج على التعميم.
  • فجوة كبيرة بين دقة التدريب والاختبار علامة الإفراط في التعلّم.
  • اضبط الإعدادات بالتحقق المتقاطع، واترك بيانات الاختبار للتقييم النهائي.
  • الغابة العشوائية تجمع أشجاراً كثيرة، وتوحيد المقاييس ضروري للنماذج المبنية على المسافة.
  • احفظ النموذج المدرَّب بـ joblib.dump وحمّله بـ joblib.load، فالتدريب يحدث مرة واحدة.