الوحدة 3 · التصنيف
أشجار القرار
18 دقراءة 4 دتمرين Python
الدرس في لمحة
ازرع أشجار قرار على بيانات الانقطاع، واقرأها كقواعد if/else بسيطة، وتحكّم في حجمها بـ max_depth وmin_samples_leaf، واعرف متى لا يمكن الوثوق باحتمالاتها.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
اسأل قائد فريق الدعم كيف يكتشف العملاء الذين أوشكوا على الرحيل، وستسمع شيئًا مثل: “إذا لم يطلبوا شيئًا منذ أشهر، فقد رحلوا. وإذا كانوا حديثين لكنهم لم يشتروا إلا مرة أو مرتين، فالأمر متأرجح. أما الزبائن الدائمون الذين طلبوا مؤخرًا فلا خوف عليهم”. هذه شجرة قرار (decision tree). وفي هذا الدرس تنمّي واحدة من البيانات وتتحقق إن كانت توافقه.
عشرون سؤالًا تختارها البيانات
تتنبأ شجرة القرار بطرح أسئلة نعم/لا عن feature واحد في كل مرة. ولبنائها، تنظر الخوارزمية في كل feature وكل عتبة ممكنة، وتختار السؤال الذي يقسم عملاء التدريب إلى أنقى مجموعتين: في الحالة المثالية، كل المنقطعين في جهة وكل الباقين في الأخرى. وتُقاس النقاوة افتراضيًا بـ Gini impurity (0 لمجموعة من فئة واحدة فقط، و0.5 لخليط 50/50). ثم تكرّر البحث داخل كل مجموعة، وهكذا نزولًا.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text
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"])
tree = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)
print(export_text(tree, feature_names=features, show_weights=True))تطبع export_text القواعد، ويضيف show_weights=True عدد عملاء التدريب من كل فئة (بقي، انقطع) الذين وصلوا إلى كل ورقة.
ثلاث أوراق تطابق كلام قائد فريق الدعم حرفًا بحرف أو تكاد. العملاء الصامتون منذ زمن طويل ينقطعون (84 من 95). والزبائن الدائمون الحديثون بأربعة طلبات أو أكثر يبقون (60 من 73). والعملاء الحديثون قليلو الطلبات متأرجحون (67 من 127 ينقطعون). واكتشفت الشجرة أيضًا تفاعلًا (interaction) لم يضطر أحد إلى كتابته: عدد الطلبات مهم، لكن بين العملاء الحديثين فقط.
أما الورقة الرابعة فتحذير. لقد أنفقت الشجرة سؤالًا كاملًا لعزل ثلاثة عملاء ذوي عضوية طويلة جدًا. هذا هو البحث الجشع (greedy) أثناء عمله: يختار التقسيم الذي يساعد أكثر في اللحظة الحالية، حتى لو جاءت المساعدة من مجموعة أصغر من أن تعني شيئًا.
كيف تتنبأ الشجرة بالاحتمالات
العميل الذي يقع في ورقة يحصل على حصص الفئات في تلك الورقة كاحتمالات. قع في ورقة “الزبائن الدائمين” وستُعيد predict_proba نحو [0.82, 0.18]. لذا فإن شجرة بعمق 2 لا تستطيع أبدًا أن تُخرج إلا أربعة احتمالات مختلفة، واحدًا لكل ورقة. هذا خشن، لكنه صادق. وتبدأ المشكلة حين لا تكون للشجرة حدود.
النمو أكثر من اللازم، وكيف توقفه
تواصل الشجرة غير المقيّدة التقسيم حتى تصبح كل ورقة نقية. على بيانات الانقطاع، هذا يعني 64 ورقة، بعمق 11 مستوى، ودقة تدريب 100%. كل ورقة نقية، فكل احتمال يساوي 0 أو 1 بالضبط: الشجرة تدّعي يقينًا تامًا بشأن كل عميل اختبار بينما لا تصيب إلا في 72% من الحالات. ومع ترتيب العملاء بمثل هذه الاحتمالات، يتعادل معظمهم، وتتحوّل قائمة الاتصالات من الدرس السابق إلى يانصيب.
اثنان من الـ hyperparameters يتوليان معظم مهمة إبقاء الشجرة صادقة:
max_depthيحدّد أقصى عدد من الأسئلة المتتالية التي تستطيع الشجرة طرحها.min_samples_leafيرفض أي تقسيم يترك في ورقة عددًا من عملاء التدريب أقل من هذه القيمة. وهو يستهدف بالضبط مشكلة الورقة الرابعة أعلاه، ويتيح للشجرة أن تتعمّق حيث البيانات وفيرة وأن تبقى ضحلة حيث ليست كذلك.
import pandas as pd
from sklearn.model_selection import 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"])
for settings in [{}, {"max_depth": 3}, {"min_samples_leaf": 20}]:
t = DecisionTreeClassifier(random_state=42, **settings).fit(X_train, y_train)
distinct = len(set(t.predict_proba(X_test)[:, 1]))
print(f"{str(settings):24} leaves {t.get_n_leaves():3} train {t.score(X_train, y_train):.2f} "
f"test {t.score(X_test, y_test):.2f} distinct probabilities {distinct}")
t = DecisionTreeClassifier(min_samples_leaf=20, random_state=42).fit(X_train, y_train)
print(pd.Series(t.feature_importances_, index=features).sort_values(ascending=False).round(2))يقول feature_importances_ كم خفّض كل feature من عدم النقاوة عبر كل تقسيماته، بعد تطبيع القيم ليكون مجموعها 1. تستحوذ حداثة آخر طلب على أكثر من النصف، ويأتي عدد الطلبات ثانيًا؛ وتحصل القسائم وتذاكر الدعم على صفر لأن الشجرة لم تستخدمها قط. عامِل هذه الأرقام كوصف لهذه الشجرة، لا للعالم: فالـ features التي لم تحتجها الشجرة قد تظل مفيدة، والدرس القادم يبيّن طريقة أجدر بالثقة لقياس الأهمية.
وعدم الاستقرار هذا فرصة أيضًا. فإذا كانت الشجرة الواحدة مليئة بالضجيج، فخذ متوسط أشجار كثيرة نمت على بيانات مختلفة قليلًا، وسيلغي الضجيج بعضه بعضًا. هذه هي الفكرة وراء الغابات العشوائية والـ gradient boosting، موضوع الدرس القادم.
أهم النقاط
- تقسم شجرة القرار البيانات بسؤال نعم/لا واحد في كل مرة، وتختار في كل خطوة السؤال الذي يجعل المجموعات أنقى ما يمكن.
- الشجرة المدرَّبة مجموعة من القواعد المقروءة؛ و
export_textتطبعها. - الأشجار غير المقيّدة تنمو حتى تصبح كل ورقة نقية، فتحفظ بيانات التدريب وتنتج احتمالات مفرطة الثقة بقيمة 0 أو 1.
- يحدّ
max_depthوmin_samples_leafمن النمو؛ ويمنعmin_samples_leafالشجرة من إنفاق تقسيم على حفنة من العملاء. - لا تحتاج الأشجار إلى توحيد مقاييس الـ features وتلتقط التفاعلات تلقائيًا، لكن الشجرة الواحدة غير مستقرة: تغييرات صغيرة في البيانات قد تعيد تشكيلها.