الوحدة 2 · الانحدار
كيف تتعلّم النماذج: دالة الخسارة والـ Gradient Descent
22 دقراءة 4 دتمرين Python
الدرس في لمحة
اكتب الـ gradient descent بيدك في NumPy لتدريب خط مستقيم، وشاهد كيف يجعله معدّل التعلّم يزحف أو يتقارب أو ينفجر، واشرح لماذا يجب توحيد مقاييس الـ features.
- 1التعلّم نزولًا على المنحدر
- 2الـ gradient descent في ثمانية أسطر
- 3معدّل التعلّم يحسم كل شيء
- 4لماذا نوحّد مقاييس الـ features
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
أعاد LinearRegression().fit أفضل خط فورًا، دون أي بحث ظاهر. فللخط المستقيم معادلة دقيقة، وهذا منصف. لكن معظم النماذج (الانحدار اللوجستي، والشبكات العصبية، والـ gradient boosting الذي ستتعرّف عليه في القسم 3) ليس لها معادلة. إنها تجد معاملاتها بالبحث، وتكاد كلها تستخدم شكلًا ما من البحث نفسه: الـ gradient descent (النزول بالتدرّج).
التعلّم نزولًا على المنحدر
خذ النموذج المعتمد على مؤشر كتلة الجسم وحده من الدرس السابق: prediction = w * x + b. لأي زوج من w وb يمكنك حساب متوسط الخطأ المربّع على بيانات التدريب. تخيّل كل قيمة ممكنة لـ w مرتّبةً من اليسار إلى اليمين، والخسارة المقابلة لكل منها ارتفاعًا. في الخط المستقيم مع الخطأ المربّع، تكون هذه الصورة وعاءً أملس، وأفضل w تقع في قاعه.
لا يمكنك رؤية الوعاء، لكن في أي نقطة يمكنك حساب ميله: التدرّج (gradient). إذا كان الميل موجبًا، فزيادة w تجعل الأمور أسوأ، فأنقصها. وإذا كان سالبًا، فزِدها. خذ خطوة صغيرة عكس الميل، وأعد الحساب، وكرّر. هذا هو الـ gradient descent.
تصغر الخطوات من تلقاء نفسها كلما اقتربت من القاع، لأن الميل يصبح أكثر تسطّحًا. وحجم كل خطوة هو التدرّج مضروبًا في معدّل التعلّم (learning rate)، وهو hyperparameter تختاره أنت.
الـ gradient descent في ثمانية أسطر
مع الخطأ المربّع، للتدرّجات معادلات قصيرة: الميل بالنسبة إلى w هو 2 * mean(error * x) وبالنسبة إلى b هو 2 * mean(error)، حيث error = prediction - actual. إليك ذلك في NumPy، بعد توحيد مقياس مؤشر كتلة الجسم إلى متوسط 0 وانحراف معياري 1 أولًا (سترى السبب بعد قليل).
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
X, y = load_diabetes(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
x = ((X_train["bmi"] - X_train["bmi"].mean()) / X_train["bmi"].std(ddof=0)).to_numpy()
target = y_train.to_numpy()
w, b, learning_rate = 0.0, 0.0, 0.1
for step in range(101):
error = (w * x + b) - target
if step % 25 == 0:
print(f"step {step:3} loss {np.mean(error ** 2):9.1f} w {w:6.2f} b {b:7.2f}")
w -= learning_rate * 2 * np.mean(error * x)
b -= learning_rate * 2 * np.mean(error)
exact = LinearRegression().fit(x.reshape(-1, 1), target)
print("LinearRegression:", exact.coef_.round(2), round(exact.intercept_, 2))تنخفض الخسارة من نحو 29,900 إلى 3,931 خلال 25 خطوة ثم تتوقف عن الحركة. وقيمتا w وb اللتان تستقر عندهما تطابقان LinearRegression حتى منزلتين عشريتين. لقد درّبت نموذجًا بيدك.
معدّل التعلّم يحسم كل شيء
غيّر learning_rate وأعد التشغيل. مع 0.01، لا تبلغ مئة خطوة إلا w ≈ 40 وb ≈ 134: البحث يزحف. ومع 0.5، يصل في خطوة واحدة. ومع 1.05، تتجاوز كل خطوة القاع بأكثر من سابقتها، وبعد مئة خطوة تصبح الخسارة بالتريليونات. هذا هو التباعد (divergence)، وحين ترى خسارة تكبر بدل أن تصغر، فمعدّل التعلّم الكبير جدًا هو المشتبه الأول.
لماذا نوحّد مقاييس الـ features
الجداول الحقيقية تخلط المقاييس. في بيانات الانقطاع، يصل total_spent إلى الآلاف بينما used_coupon إما 0 أو 1. ومع اثنين من الـ features كهذين، لم يعد سطح الخسارة وعاءً مستديرًا بل واديًا طويلًا ضيقًا. ومعدّل التعلّم الصغير بما يكفي للبقاء مستقرًا في الاتجاه الشديد الانحدار يكون بطيئًا على نحو مؤلم في الاتجاه المسطّح، فيتعرّج المسار عبر الوادي.
والمقياس يؤثر أيضًا في نماذج لا تستخدم التدرّجات إطلاقًا. فأقرب k جيران يقيس المسافة بين العملاء، وفرق 300 دولار في الإنفاق يطغى على فرق أربع تذاكر دعم. وتوحيد كل عمود إلى متوسط 0 وانحراف معياري 1 يضعها جميعًا على قدم المساواة.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
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"])
scaler = StandardScaler().fit(X_train) # learn means and stds from TRAIN only
X_train_s, X_test_s = scaler.transform(X_train), scaler.transform(X_test)
print("learned means:", scaler.mean_[:3].round(1))
raw = KNeighborsClassifier().fit(X_train, y_train).score(X_test, y_test)
scaled = KNeighborsClassifier().fit(X_train_s, y_train).score(X_test_s, y_test)
print(f"k-NN raw {raw:.2f} scaled {scaled:.2f}")توحيد المقاييس وحده يرفع k-NN من 0.72 إلى 0.75، متجاوزًا قاعدة الـ 80 يومًا من القسم 1. النموذج نفسه والبيانات نفسها؛ والتغيير الوحيد أن كل feature صار له صوت عادل.
الـ scaler أيضًا estimator. له fit (تعلّم متوسط كل عمود وانحرافه المعياري) وtransform (تطبيقهما)، وتسري عليه القاعدة نفسها التي تسري على النماذج: درّبه على بيانات التدريب وحدها. والتنقّل بين X_train_s وX_test_s يدويًا يصبح عرضة للأخطاء، لذا يجمع القسم 4 الـ scaler والنموذج في كائن pipeline واحد.
سيجد الـ gradient descent أفضل خط، لكن “الأفضل على بيانات التدريب” قد يكون فخًّا حين يكون النموذج مرنًا. في الدرس القادم يصبح الخط منحنيًا، وسترى كيف ينحني أكثر مما يجب.
أهم النقاط
- تدريب النموذج يعني البحث عن قيم المعاملات التي تجعل دالة الخسارة أصغر ما يمكن.
- يكرّر الـ gradient descent خطوة واحدة: احسب ميل دالة الخسارة لكل معامل، ثم حرّك كل معامل قليلًا في اتجاه النزول.
- يحدّد معدّل التعلّم (learning rate) حجم الخطوة: الصغير جدًا يزحف، والكبير جدًا يتجاوز الهدف ويتباعد.
- الـ features ذات المقاييس المتباعدة جدًا تمطّ سطح دالة الخسارة، فتبطّئ الـ gradient descent وتشوّه النماذج المعتمدة على المسافة مثل k-NN.
- يتعلّم
StandardScalerمتوسط كل عمود وانحرافه المعياري من بيانات التدريب، ثم يعيد المعايرة إلى متوسط 0 وانحراف معياري 1.