EduFlow

الوحدة 4 · العمل بالطريقة الصحيحة

البحث عن الـ Hyperparameters

20 دقراءة 4 دتمرين Python

الدرس في لمحة

اضبط الـ hyperparameters باستخدام GridSearchCV وRandomizedSearchCV مع أسماء خطوات الـ pipeline، واقرأ cv_results_، وحافظ على أمانة نتيجة الاختبار النهائية بعد البحث.

  1. 1البحث الشبكي
  2. 2شبكة حيث يهمّ الأمر
  3. 3البحث العشوائي
  4. 4البحث في ما هو أبعد من إعدادات النموذج

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

كل نموذج حتى الآن كانت له إعدادات اخترتها يدويًا أو تركتها على قيمها الافتراضية: C=1.0 وmax_depth=3 وmin_samples_leaf=5. بعض هذه الخيارات مهم جدًا، وبعضها لا يكاد يهم، والطريقة الوحيدة لمعرفة ذلك هي التجربة. والتجربة يدويًا تعني حلقات تكرار ومسك دفاتر وإغراءً باختلاس النظر إلى بيانات الاختبار. أدوات البحث في scikit-learn تفعل ذلك كما ينبغي، مع تحقق متقاطع مدمج.

البحث الشبكي

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

يُشار إلى المعاملات داخل الـ pipeline باسم الخطوة، ثم شرطتين سفليتين، ثم اسم المعامل. ولهذا تؤتي تسميةُ خطواتك ثمارها.

Python
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.

شبكة حيث يهمّ الأمر

الأشجار قصة أخرى. العمق والحدّ الأدنى لحجم الورقة يتحكمان في مقدار ما تستطيع الشجرة حفظه، والقيم الصحيحة تعتمد على البيانات.

Python
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)، ويقبل التوزيعات إلى جانب القوائم.

مربّعان، المعامل المهم على المحور الأفقي وغير المهم على العمودي. تضع الشبكة تسع نقاط في شبكة 3 في 3، فلا تختبر إلا ثلاث قيم مختلفة للمعامل المهم. أما البحث العشوائي فينثر تسع نقاط، فيختبر تسع قيم مختلفة. الشبكة: 3 قيم مُجرَّبة العشوائي: 9 قيم مُجرَّبة المهم →
البحث الشبكي مقابل البحث العشوائي بالمحاولات التسع نفسها

حين لا يهمّ حقًا إلا معامل أو اثنان، وهي الحالة المعتادة، يستكشف البحث العشوائي قيمًا مختلفة لهما أكثر بكثير بالميزانية نفسها.

Python
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، وتوقّع أن يضيف الضبط نقاطًا، لا معجزات.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

الـ pipeline لديك هو Pipeline([("prep", preprocess), ("clf", LogisticRegression())]). أيّ شبكة تضبط قوة الـ regularization؟

السؤال 2 من 4

في شبكة 5 قيم لـ max_depth و5 قيم لـ min_samples_leaf، مع cv=5. كم نموذجًا يُدرَّب، بما في ذلك إعادة التدريب النهائية؟

السؤال 3 من 4

لماذا قد يتفوّق RandomizedSearchCV بعشرين تكرارًا على شبكة من 400 تركيبة بالميزانية الزمنية نفسها؟

السؤال 4 من 4

يُبلغ البحث عن best_score_ = 0.813، ويحقق النموذج المُعاد تدريبه 0.805 على بيانات الاختبار. ما الذي يحدث؟

تدريب

تمرين Python

اضبط أقرب k جيران

جد أفضل عدد من الجيران لـ pipeline من k-NN بعد توحيد المقاييس.

  1. أصلح grid بحيث تشير مفاتيحه إلى الخطوة knn في الـ pipeline (مفاتيح الكود المبدئي سترفع خطأ معامل غير صالح).
  2. search: نموذج GridSearchCV على pipe مع cv المعطى وscoring="roc_auc"، مدرَّبًا على بيانات التدريب.
  3. best_params وbest_cv_auc: الإعدادات الفائزة ومتوسط الـ AUC الخاص بها بالتحقق المتقاطع. وtest_auc: قيمة الـ ROC AUC للفائز بعد إعادة تدريبه، على بيانات الاختبار.

ملفات البيانات: churn.csv

import pandas as pd
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from sklearn.neighbors import KNeighborsClassifier
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()), ("knn", KNeighborsClassifier())])

# 1. Grid: n_neighbors in [5, 15, 25, 35, 51] and weights in ["uniform", "distance"]
grid = {"n_neighbors": [5, 15, 25, 35, 51], "weights": ["uniform", "distance"]}

# 2. Search with the provided cv, scoring ROC AUC, fitted on the training data
search = None

# 3. Results
best_params = None
best_cv_auc = None
test_auc = None   # ROC AUC of the refit best model on the test set
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

(لا مخرجات)

الفحوص

  • مفاتيح grid تستخدم البادئة knn__
  • جرّب search التركيبات العشر كلها مع ROC AUC
  • best_params وbest_cv_auc مأخوذان من البحث
  • test_auc هي قيمة ROC AUC للفائز المُعاد تدريبه على بيانات الاختبار