الوحدة 4 · العمل بالطريقة الصحيحة
البحث عن الـ Hyperparameters
20 دقراءة 4 دتمرين Python
الدرس في لمحة
اضبط الـ hyperparameters باستخدام GridSearchCV وRandomizedSearchCV مع أسماء خطوات الـ pipeline، واقرأ cv_results_، وحافظ على أمانة نتيجة الاختبار النهائية بعد البحث.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
كل نموذج حتى الآن كانت له إعدادات اخترتها يدويًا أو تركتها على قيمها الافتراضية: C=1.0 وmax_depth=3 وmin_samples_leaf=5. بعض هذه الخيارات مهم جدًا، وبعضها لا يكاد يهم، والطريقة الوحيدة لمعرفة ذلك هي التجربة. والتجربة يدويًا تعني حلقات تكرار ومسك دفاتر وإغراءً باختلاس النظر إلى بيانات الاختبار. أدوات البحث في scikit-learn تفعل ذلك كما ينبغي، مع تحقق متقاطع مدمج.
البحث الشبكي
يأخذ GridSearchCV estimator وقاموسًا من قيم المعاملات ومخطط تحقق متقاطع. ويطبّق التحقق المتقاطع على كل تركيبة، ويختار صاحبة أفضل متوسط، ويعيد تدريبها على بيانات التدريب كلها. وبعد ذلك يتصرّف كالنموذج الفائز.
يُشار إلى المعاملات داخل الـ pipeline باسم الخطوة، ثم شرطتين سفليتين، ثم اسم المعامل. ولهذا تؤتي تسميةُ خطواتك ثمارها.
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
"days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
X_train, X_test, y_train, y_test = train_test_split(
churn[features], churn["churned"], test_size=0.25, random_state=42,
stratify=churn["churned"])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
pipe = Pipeline([("scale", StandardScaler()), ("model", LogisticRegression())])
search = GridSearchCV(pipe, {"model__C": [0.01, 0.03, 0.1, 0.3, 1, 3, 10]},
cv=cv, scoring="roc_auc")
search.fit(X_train, y_train)
results = pd.DataFrame(search.cv_results_)
print(results[["param_model__C", "mean_test_score", "std_test_score"]].round(3))
print("best:", search.best_params_, "CV AUC", round(search.best_score_, 3))يحمل cv_results_ نتائج الطيّات لكل تركيبة؛ وتصفّحه سهل حين يكون DataFrame. وهنا التصفّح نفسه هو الاستنتاج: من C=0.03 إلى C=10 يتحرك متوسط الـ AUC بأقل من نصف نقطة، وهذا أصغر بكثير من التشتّت بين الطيّات. ثمانية features حسنة السلوك لا تترك للانحدار اللوجستي مجالًا للـ overfitting، فلا تكاد عقوبته تهم. الضبط لا يستطيع إنقاذ نموذج بلغ سقفه أصلًا؛ أما features أفضل أو عائلة نماذج مختلفة فقد تستطيع.
لاحظ أن القيم متباعدة بعوامل تقارب الثلاثة. في المعاملات التي تعمل بشكل ضربي (C وalpha ومعدّلات التعلّم)، ابحث على مقياس لوغاريتمي؛ فالقيم 0.01 و0.1 و1 و10 تغطي مساحة أكبر بكثير من 1 و2 و3 و4.
شبكة حيث يهمّ الأمر
الأشجار قصة أخرى. العمق والحدّ الأدنى لحجم الورقة يتحكمان في مقدار ما تستطيع الشجرة حفظه، والقيم الصحيحة تعتمد على البيانات.
import pandas as pd
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from sklearn.tree import DecisionTreeClassifier
churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
"days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
X_train, X_test, y_train, y_test = train_test_split(
churn[features], churn["churned"], test_size=0.25, random_state=42,
stratify=churn["churned"])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
grid = {"max_depth": [2, 3, 4, 5, None], "min_samples_leaf": [1, 5, 10, 20, 40]}
search = GridSearchCV(DecisionTreeClassifier(random_state=42), grid, cv=cv, scoring="roc_auc")
search.fit(X_train, y_train)
top = pd.DataFrame(search.cv_results_).sort_values("rank_test_score")
print(top[["param_max_depth", "param_min_samples_leaf", "mean_test_score"]].head(5).round(3))
print("worst:", round(top["mean_test_score"].min(), 3))
print("best:", search.best_params_, "| test AUC", round(search.score(X_test, y_test), 3))25 تركيبة × 5 طيّات = 125 عملية تدريب، إضافة إلى إعادة تدريب نهائية واحدة. والتشتّت واسع: أسوأ الأشجار (بعمق غير محدود وأوراق من عميل واحد) تحقق نتائج أدنى بكثير من الأفضل. تستخدم search.score مقياس scoring نفسه المستخدم في البحث، فهي تُبلغ هنا عن الـ ROC AUC على بيانات الاختبار.
البحث العشوائي
تكبر الشبكات بشكل ضربي. أربعة معاملات بستّ قيم لكلٍّ منها تعني 1,296 تركيبة، مضروبة في خمس طيّات. أما RandomizedSearchCV فيسحب بدلًا من ذلك عددًا ثابتًا من التركيبات (n_iter)، ويقبل التوزيعات إلى جانب القوائم.
حين لا يهمّ حقًا إلا معامل أو اثنان، وهي الحالة المعتادة، يستكشف البحث العشوائي قيمًا مختلفة لهما أكثر بكثير بالميزانية نفسها.
import pandas as pd
from scipy.stats import randint
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV, StratifiedKFold, train_test_split
churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
"days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
X_train, X_test, y_train, y_test = train_test_split(
churn[features], churn["churned"], test_size=0.25, random_state=42,
stratify=churn["churned"])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
space = {"max_features": [1, 2, 3, 4, "sqrt"], "min_samples_leaf": randint(1, 30)}
search = RandomizedSearchCV(RandomForestClassifier(n_estimators=60, random_state=42), space,
n_iter=8, cv=cv, scoring="roc_auc", random_state=42)
search.fit(X_train, y_train)
print("best:", search.best_params_)
print("CV AUC", round(search.best_score_, 3), "| test AUC", round(search.score(X_test, y_test), 3))تسحب randint(1, 30) من SciPy أعدادًا صحيحة من 1 إلى 29؛ وloguniform(0.01, 10) هو الخيار المعتاد لـ C أو معدّلات التعلّم. اضبط random_state على البحث كي يسحب المرشّحين أنفسهم في كل تشغيل.
البحث في ما هو أبعد من إعدادات النموذج
لأن البحث يضبط الـ pipeline كله، فخيارات المعالجة المسبقة مشمولة أيضًا. إدخال في الشبكة مثل "prep__sat__simpleimputer__strategy": ["mean", "median"] يقارن استراتيجيات الملء بالطيّات الأمينة نفسها. بل يمكنك تمرير قائمة من الشبكات، واحدة لكل عائلة نماذج، وترك البحث يبدّل الخطوة الأخيرة: [{"model": [LogisticRegression()], "model__C": [0.1, 1]}, {"model": [RandomForestClassifier()], "model__min_samples_leaf": [5, 20]}]. هذا أنيق، لكنه يضاعف عمليات التدريب بسرعة، ويُخفي جدول المقارنة الذي تريد أن تعرضه على زميل. ولحفنة من النماذج المرشّحة، كثيرًا ما تكون حلقة بسيطة من استدعاءات cross_validate بالـ splitter نفسه أوضح.
عمليات البحث تضاعف عدد النماذج التي تدرّبها، ومعها عدد الفرص لتسرّب المعلومات من بيانات التحقق إلى التدريب. الدرس القادم دليل ميداني لتسرّب البيانات: كيف يحدث، وكيف تكتشفه، ولماذا ينتج أفضل النتائج التي ستراها في حياتك.
أهم النقاط
- يجرّب
GridSearchCVكل تركيبة في الشبكة مع تحقق متقاطع لكلٍّ منها؛ ويسحبRandomizedSearchCVعددًا ثابتًا من التركيبات من قوائم أو توزيعات. - داخل الـ pipeline، يُشار إلى المعاملات بالصيغة
step__parameter، مثلmodel__C. - بعد البحث، يحمل
best_params_الفائز، وbest_estimator_هو ذلك النموذج بعد إعادة تدريبه على بيانات التدريب كلها. - قيمة
best_score_متفائلة قليلًا لأنها الأفضل من بين محاولات كثيرة؛ وبيانات الاختبار تعطي الرقم غير المنحاز. - ابحث على مقياس لوغاريتمي في معاملات مثل
Cوalphaوlearning_rate، وتوقّع أن يضيف الضبط نقاطًا، لا معجزات.