تحسين النموذج: ضبط الإعدادات والغابة العشوائية
بنيت نموذجاً وقيّمته بصدق. السؤال التالي طبيعي: كيف أجعله أفضل؟ في درس النموذج الأول سمعت عن الإفراط في التعلّم (Overfitting): نموذج يحفظ بيانات التدريب بدل أن يفهمها. في هذا الدرس سترى هذه المشكلة بالأرقام، وتتعلم ثلاث طرق عملية للتحسين، ثم تحفظ نموذجك لتستخدمه في تطبيق.
نكمل على بيانات الأورام من الدرس السابق، بالتقسيمة نفسها:
from sklearn.datasets import load_breast_cancerfrom sklearn.model_selection import train_test_split, cross_val_scorefrom sklearn.tree import DecisionTreeClassifierfrom sklearn.metrics import accuracy_score
data = load_breast_cancer()X, y = data.data, data.targetX_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.9232 0.958 0.9093 0.977 0.9445 0.995 0.93710 1.000 0.923None 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.8942 0.9233 0.9305 0.93210 0.930العمق 1 ضعيف بوضوح. ومن العمق 3 فصاعداً الأرقام متقاربة جداً، والفروق بينها أصغر من التذبذب الطبيعي بين تقسيمة وأخرى. الدرس الصادق هنا: ضبط العمق وحده لن يأخذنا أبعد من نحو 93%. نحتاج فكرة أقوى.
الغابة العشوائية
Section titled “الغابة العشوائية”شجرة واحدة تتأثر بسهولة بتفاصيل بياناتها. ماذا لو درّبنا مئة شجرة، كل واحدة على عينة عشوائية مختلفة من البيانات، ثم أخذنا رأي الأغلبية؟ هذه هي الغابة العشوائية (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.9600.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.150worst concave points 0.127mean concave points 0.105dtype: float64مساحة الورم وتقعّر حوافه أكثر ما يعتمد عليه النموذج. معلومة تفيدك في فهم المشكلة، وفي شرح النموذج للأطباء.
توحيد المقاييس
Section titled “توحيد المقاييس”بعض النماذج حساسة لـ وحدات الميزات. خوارزمية الجيران الأقرب التي تعرفها تحسب المسافة بين العينات. وفي بياناتنا ميزة «المساحة» تتراوح بين 143 و 2501، وميزة «النعومة» بين 0.05 و 0.16. المساحة ستطغى على المسافة كلها، كأن بقية الميزات غير موجودة!
العلاج توحيد المقاييس (Scaling): تحويل كل ميزة ليصبح متوسطها صفراً وتشتتها واحداً:
from sklearn.neighbors import KNeighborsClassifierfrom sklearn.preprocessing import StandardScalerfrom 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.9300.979النموذج نفسه والبيانات نفسها، ومن 93.0% إلى 97.9% بمجرد توحيد المقاييس. make_pipeline تربط الخطوتين في كائن واحد: كل بيانات تدخله تُوحَّد أولاً ثم تُصنَّف، فلا تنسى خطوة عند التوقع لاحقاً. (أشجار القرار والغابات لا تتأثر بالمقاييس، فلا تحتاج هذه الخطوة.)
حفظ النموذج
Section titled “حفظ النموذج”التدريب يحدث مرة واحدة. بعدها تحفظ النموذج في ملف، ويحمّله تطبيقك ليتوقع مباشرة:
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]النموذج المحمَّل من الملف يتوقع كالأصلي تماماً. (وفي العينة الخامسة ترى أحد أخطائه: ورم خبيث توقعه حميداً. لا نموذج بلا أخطاء.)
جرّب بنفسك
Section titled “جرّب بنفسك”- كرر تجربة الأعماق مع إضافة الإعداد
min_samples_leaf=5للشجرة، ولاحظ هل تقل الفجوة بين التدريب والاختبار. - درّب غابات بعدد أشجار 10 و 50 و 200، وقارن متوسط التحقق المتقاطع.
- جرّب
KNeighborsClassifierمع توحيد المقاييس بقيم مختلفة لـn_neighbors(مثل 1 و 5 و 15). - تحدٍّ: احسب Recall للفئة الخبيثة لأفضل نموذج وصلت إليه، وقارنه بـ Recall شجرة الدرس السابق (0.906).
أخطاء شائعة
Section titled “أخطاء شائعة”- اختيار الإعدادات ببيانات الاختبار: يجعل تقييمك النهائي متفائلاً أكثر من اللازم. اضبط بالتحقق المتقاطع على التدريب، وافتح الاختبار مرة واحدة في النهاية.
- الفرح بدقة تدريب 100%: هذه علامة تحذير لا إنجاز. قارنها دائماً بدقة الاختبار.
- توحيد المقاييس قبل التقسيم: حساب المتوسط على البيانات كلها يسرّب معلومات الاختبار. استخدم
make_pipelineفيتعلم التوحيد من التدريب وحده. - مطاردة آخر نصف بالمئة: بعد حد معيّن، بيانات أكثر وأنظف تفيد أكثر من أي ضبط للإعدادات.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
نموذج دقته 100% على بيانات التدريب و 80% على بيانات الاختبار. ما المشكلة غالباً؟
الفجوة الكبيرة بين التدريب والاختبار تعني أن النموذج حفظ بيانات التدريب ولم يتعلم القاعدة العامة.
سؤال
كيف تتخذ الغابة العشوائية قرارها؟
أخطاء الأشجار المنفردة مختلفة، فتصويت الأغلبية يلغي كثيراً منها.
الخلاصة
Section titled “الخلاصة”- الإعدادات الفائقة مثل
max_depthتحددها أنت، وتؤثر في قدرة النموذج على التعميم. - فجوة كبيرة بين دقة التدريب والاختبار علامة الإفراط في التعلّم.
- اضبط الإعدادات بالتحقق المتقاطع، واترك بيانات الاختبار للتقييم النهائي.
- الغابة العشوائية تجمع أشجاراً كثيرة، وتوحيد المقاييس ضروري للنماذج المبنية على المسافة.
- احفظ النموذج المدرَّب بـ
joblib.dumpوحمّله بـjoblib.load، فالتدريب يحدث مرة واحدة.