EduFlow

الوحدة 3 · التصنيف

الغابات العشوائية والـ Gradient Boosting

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

الدرس في لمحة

اجمع أشجارًا كثيرة باستخدام الغابات العشوائية والـ gradient boosting، واستفد من تعامل HistGradientBoostingClassifier المدمج مع القيم المفقودة، وقِس أهمية الـ features بالـ permutation importance.

  1. 1الغابات العشوائية: أشجار كثيرة، ومتوسط
  2. 2الـ Gradient Boosting: أشجار يصلح بعضها بعضًا
  3. 3كلاهما على بيانات الانقطاع، مع عودة العمود الناقص
  4. 4على ماذا تعتمد الأشجار؟

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

شجرة القرار الواحدة سهلة القراءة وسهلة الكسر: غيّر بضعة صفوف تدريب وستتبدّل الفروع. والعلاج حيلة إحصائية قديمة: اسأل مقدِّرات كثيرة مستقلة ومختلفة قليلًا، واجمع إجاباتها. أخطاء كلٍّ منها تتلاشى جزئيًا، وما يتبقى أكثر موثوقية من أي عضو منفرد. النماذج المبنية بهذه الطريقة هي النماذج المجمّعة (ensembles)، وفي البيانات الجدولية مثل بيانات الانقطاع، هي ما يلجأ إليه معظم الممارسين بعد خطوط الأساس والنموذج الخطي.

الغابات العشوائية: أشجار كثيرة، ومتوسط

تنمّي الغابة العشوائية بضع مئات من الأشجار العميقة، كلٌّ منها على عيّنة bootstrap مختلفة (صفوف التدريب مسحوبة مع الإرجاع، فترى كل شجرة مجموعة بيانات مختلفة قليلًا)، ولا ترى عند كل تقسيم إلا مجموعة فرعية عشوائية من الـ features. هذه الحيلة الثانية تمنع كل الأشجار من أن تبدأ بالـ feature المهيمن نفسه، فتصبح الأشجار أكثر اختلافًا عن بعضها، وتصبح أخطاؤها أقل ترابطًا. وللتنبؤ، تأخذ الغابة متوسط احتمالات الأشجار.

الـ Gradient Boosting: أشجار يصلح بعضها بعضًا

يسلك الـ boosting النهج المعاكس. فهو يبني أشجارًا ضحلة واحدة في كل مرة. الأولى تعطي تنبؤات تقريبية؛ والثانية تُدرَّب على الأخطاء التي تركتها الأولى؛ والثالثة على ما لا تزال الأوليان تخطئان فيه، وهكذا. ومساهمة كل شجرة تُصغَّر بمقدار learning_rate، فتزحف المجموعة نحو إجابة جيدة بدل أن تترنّح. والمزيد من الأشجار (max_iter) ومعدّل تعلّم أعلى يلائمان بيانات التدريب بدقة أكبر، ثم يقعان في النهاية في الـ overfitting.

الصف العلوي، الغابة العشوائية: ثلاث أشجار مدرَّبة باستقلال على عيّنات مختلفة تصبّ في متوسط. الصف السفلي، الـ gradient boosting: الشجرة 1 تمرّر أخطاءها إلى الشجرة 2، التي تمرّر أخطاءها المتبقية إلى الشجرة 3، وتُجمع مخرجاتها بعد تصغيرها. الغابة العشوائية (bagging) شجرة على العيّنة 1 شجرة على العيّنة 2 شجرة على العيّنة 3 متوسط الأصوات Gradient boosting الشجرة 1 الشجرة 2 على الأخطاء الشجرة 3 على الأخطاء مجموع مصغَّر بمعدّل التعلّم كل شجرة تعتمد على ما سبقها
الـ bagging يأخذ المتوسط بالتوازي؛ والـ boosting يصحّح بالتتابع

كلاهما على بيانات الانقطاع، مع عودة العمود الناقص

حتى الآن استبعدت avg_satisfaction لأن نصف قيمه فارغة. أما HistGradientBoostingClassifier، وهو تطبيق الـ boosting السريع في scikit-learn، فيتعامل مع القيم المفقودة بشكل أصيل: عند كل تقسيم يتعلّم إلى أيّ جهة يجب أن تذهب الفراغات. والإصدارات الحديثة من DecisionTreeClassifier وRandomForestClassifier تقبل NaN أيضًا. أما LogisticRegression وk-NN فما زالا لا يقبلانها؛ ويرفعان ValueError: Input X contains NaN.

Python
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_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",
            "avg_satisfaction"]
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"])

no_gaps = features[:-1]  # logistic regression can't take the NaNs in avg_satisfaction
models = {
    "logistic (no satisfaction)": (make_pipeline(StandardScaler(), LogisticRegression()), no_gaps),
    "random forest": (RandomForestClassifier(n_estimators=300, min_samples_leaf=5,
                                             random_state=42), features),
    "boosting, defaults": (HistGradientBoostingClassifier(random_state=42), features),
    "boosting, gentle": (HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05,
                                                        max_iter=200, random_state=42), features),
}
for name, (model, cols) in models.items():
    model.fit(X_train[cols], y_train)
    print(f"{name:27} train {model.score(X_train[cols], y_train):.2f}  "
          f"test {model.score(X_test[cols], y_test):.2f}")

تبلغ الغابة 0.76 والـ boosting المضبوط بلطف 0.78، مقابل 0.72 للانحدار اللوجستي وللقاعدة. أما الـ boosting بإعداداته الافتراضية فيلائم بيانات التدريب بشكل شبه مثالي (0.99) ويؤدي أسوأ من النسخة اللطيفة: فعلى 298 صفًا، الإعدادات الافتراضية متحمّسة أكثر من اللازم. والأشجار الأضحل مع معدّل تعلّم أصغر تُبقيه تحت السيطرة.

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

على ماذا تعتمد الأشجار؟

توفّر الغابات feature_importances_، لكن هذه الأرقام تُحسب على بيانات التدريب من مقدار ما خفّضه كل feature من عدم النقاوة، وهي منحازة لصالح الـ features المتصلة ذات العتبات المحتملة الكثيرة. أما الـ permutation importance (أهمية التبديل) فتطرح سؤالًا أدقّ على بيانات محجوزة: إذا خلطت هذا العمود وحده، فقطعت صلته بالهدف، فكم تنخفض النتيجة؟

Python
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.model_selection import 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",
            "avg_satisfaction"]
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"])
forest = RandomForestClassifier(n_estimators=300, min_samples_leaf=5, random_state=42).fit(X_train, y_train)

result = permutation_importance(forest, X_test, y_test, n_repeats=10, random_state=42)
table = pd.DataFrame({"impurity (train)": forest.feature_importances_,
                      "permutation (test)": result.importances_mean}, index=features)
print(table.sort_values("permutation (test)", ascending=False).round(3))

تضع أهمية عدم النقاوة tenure_days في المرتبة الثانية. أما الـ permutation importance على بيانات الاختبار فتقول إنه لا يكاد يهمّ: لقد استخدمته الغابة لملاءمة الضجيج في بيانات التدريب. خلط حداثة آخر طلب يكلّف نحو 13 نقطة من الـ accuracy؛ وخلط الرضا لا يكلّف شيئًا (القيم السالبة الصغيرة ضجيج). والخلاصة الأمينة لـ Cartwheel: حداثة آخر طلب وعدد الطلبات هما ما يحرّك التنبؤات، واستبيان الرضا بحالته الراهنة نصف الفارغة لا يضيف شيئًا.

حملتك الـ accuracy حتى هنا، لكنها تعامل العميل المنقطع الذي فاتك والإنذار الكاذب على أنهما سيئان بالقدر نفسه، وتتجاهل الترتيب الذي بنيته لفريق التسويق. الدرس القادم يستبدلها بمقاييس تنظر في كل نوع من الأخطاء على حدة.

أهم النقاط

  • تدرّب الغابة العشوائية (random forest) أشجارًا عميقة كثيرة على عيّنات bootstrap مع مجموعات فرعية عشوائية من الـ features، ثم تأخذ متوسط أصواتها، فيُلغى جزء كبير من تباين الشجرة الواحدة.
  • يبني الـ gradient boosting أشجارًا ضحلة واحدة تلو الأخرى، كلٌّ منها مدرَّبة على أخطاء المجموعة حتى تلك اللحظة؛ ويوازن learning_rate وmax_iter بين السرعة والـ overfitting.
  • HistGradientBoostingClassifier هو نموذج الـ boosting السريع في scikit-learn، ويقبل القيم المفقودة (NaN) مباشرة.
  • قيم feature_importances_ المبنية على عدم النقاوة تحابي الـ features ذات القيم المختلفة الكثيرة؛ أما الـ permutation importance على بيانات محجوزة فتقيس ما يعتمد عليه النموذج فعلًا.
  • في مجموعات البيانات الجدولية الصغيرة، كثيرًا ما لا تضيف النماذج المجمّعة إلا بضع نقاط فوق نموذج خطي جيد، فقارنها به بدل أن تفترض أنها ستفوز.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

لماذا تتفوّق الغابة العشوائية عادةً على شجرة واحدة غير مقيّدة مع البيانات الجديدة؟

السؤال 2 من 4

في الـ gradient boosting، ماذا تتعلّم كل شجرة جديدة؟

السؤال 3 من 4

العمود avg_satisfaction فارغ لنصف العملاء. أيّ نموذج يمكنك تدريبه عليه دون معالجة مسبقة إضافية؟

السؤال 4 من 4

يضع feature_importances_ الخاص بالغابة tenure_days في المرتبة الثانية، لكن الـ permutation importance على بيانات الاختبار تعطيه ما يقارب الصفر. ما التفسير الأرجح؟

تدريب

تمرين Python

طبّق الـ boosting مع إبقاء الفجوات

درّب نموذج gradient boosting على الـ features التسعة كلها، بما فيها avg_satisfaction نصف الفارغ، واكتشف على ماذا يعتمد.

  1. booster: HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, max_iter=200, random_state=42) مدرَّبًا على X_train كاملة (دون حذف أي عمود).
  2. test_acc: دقته على بيانات الاختبار.
  3. ranking: قائمة بأسماء الـ features التسعة مرتّبة بحسب الـ permutation importance على بيانات الاختبار (n_repeats=5, random_state=0)، الأهم أولًا.

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

import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.inspection import permutation_importance
from sklearn.model_selection import 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",
            "avg_satisfaction"]
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"])

# 1. Gradient boosting on all nine features (missing values included):
#    max_depth=3, learning_rate=0.05, max_iter=200, random_state=42
booster = HistGradientBoostingClassifier(random_state=42)
booster.fit(X_train.drop(columns="avg_satisfaction"), y_train)

# 2. Test accuracy
test_acc = None

# 3. Permutation importance on the TEST set (n_repeats=5, random_state=0),
#    then the feature names sorted from most to least important
ranking = []
جارٍ تحميل المحرّر…

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

(لا مخرجات)

الفحوص

  • النموذج مدرَّب على الـ features التسعة كلها
  • يستخدم الـ booster الإعدادات اللطيفة
  • test_acc هي الدقة على بيانات الاختبار
  • يسرد ranking الـ features التسعة بحسب الـ permutation importance، وحداثة آخر طلب أولًا