EduFlow

الوحدة 2 · الانحدار

كيف تتعلّم النماذج: دالة الخسارة والـ Gradient Descent

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

الدرس في لمحة

اكتب الـ gradient descent بيدك في NumPy لتدريب خط مستقيم، وشاهد كيف يجعله معدّل التعلّم يزحف أو يتقارب أو ينفجر، واشرح لماذا يجب توحيد مقاييس الـ features.

  1. 1التعلّم نزولًا على المنحدر
  2. 2الـ gradient descent في ثمانية أسطر
  3. 3معدّل التعلّم يحسم كل شيء
  4. 4لماذا نوحّد مقاييس الـ features

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

أعاد LinearRegression().fit أفضل خط فورًا، دون أي بحث ظاهر. فللخط المستقيم معادلة دقيقة، وهذا منصف. لكن معظم النماذج (الانحدار اللوجستي، والشبكات العصبية، والـ gradient boosting الذي ستتعرّف عليه في القسم 3) ليس لها معادلة. إنها تجد معاملاتها بالبحث، وتكاد كلها تستخدم شكلًا ما من البحث نفسه: الـ gradient descent (النزول بالتدرّج).

التعلّم نزولًا على المنحدر

خذ النموذج المعتمد على مؤشر كتلة الجسم وحده من الدرس السابق: prediction = w * x + b. لأي زوج من w وb يمكنك حساب متوسط الخطأ المربّع على بيانات التدريب. تخيّل كل قيمة ممكنة لـ w مرتّبةً من اليسار إلى اليمين، والخسارة المقابلة لكل منها ارتفاعًا. في الخط المستقيم مع الخطأ المربّع، تكون هذه الصورة وعاءً أملس، وأفضل w تقع في قاعه.

لا يمكنك رؤية الوعاء، لكن في أي نقطة يمكنك حساب ميله: التدرّج (gradient). إذا كان الميل موجبًا، فزيادة w تجعل الأمور أسوأ، فأنقصها. وإذا كان سالبًا، فزِدها. خذ خطوة صغيرة عكس الميل، وأعد الحساب، وكرّر. هذا هو الـ gradient descent.

منحنى خسارة على شكل حرف U فوق المعامل w. نقاط تحدّد الخطوات المتتالية من أعلى اليسار نزولًا نحو القاع، وكل خطوة أقصر من سابقتها مع تسطّح الميل. w الخسارة ميل كبير، خطوة كبيرة مسطّح عند القاع
يأخذ الـ gradient descent خطوات نزولًا على منحنى الخسارة

تصغر الخطوات من تلقاء نفسها كلما اقتربت من القاع، لأن الميل يصبح أكثر تسطّحًا. وحجم كل خطوة هو التدرّج مضروبًا في معدّل التعلّم (learning rate)، وهو hyperparameter تختاره أنت.

الـ gradient descent في ثمانية أسطر

مع الخطأ المربّع، للتدرّجات معادلات قصيرة: الميل بالنسبة إلى w هو 2 * mean(error * x) وبالنسبة إلى b هو 2 * mean(error)، حيث error = prediction - actual. إليك ذلك في NumPy، بعد توحيد مقياس مؤشر كتلة الجسم إلى متوسط 0 وانحراف معياري 1 أولًا (سترى السبب بعد قليل).

Python
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 يضعها جميعًا على قدم المساواة.

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

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

أثناء الـ gradient descent، كان تدرّج دالة الخسارة بالنسبة إلى w موجبًا. ماذا يفعل التحديث بـ w؟

السؤال 2 من 4

ضاعفت معدّل التعلّم فصارت الخسارة المطبوعة 3,900 ← 12,000 ← 95,000 ← 2,000,000. ماذا حدث؟

السؤال 3 من 4

أين يجب أن تدرّب الـ StandardScaler؟

السؤال 4 من 4

أيّ نموذج تتغيّر تنبؤاته حين تعيد التعبير عن total_spent بالسنتات بدل الدولارات؟

تدريب

تمرين Python

درّب خطًّا بالـ gradient descent

أكمل حلقة الـ gradient descent كي تطابق prediction = w * x + b مع هدف بيانات السكري، حيث x هو مؤشر كتلة الجسم بعد توحيد مقياسه.

داخل الحلقة، حدّث w وb بطرح حاصل ضرب learning_rate في تدرّج كلٍّ منهما. مع متوسط الخطأ المربّع، التدرّج هو 2 * mean(error * x) لـ w و2 * mean(error) لـ b. أبقِ القائمة losses: فالفحوص تعتمد عليها.

import numpy as np
from sklearn.datasets import load_diabetes
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)
bmi = X_train["bmi"]
x = ((bmi - bmi.mean()) / bmi.std(ddof=0)).to_numpy()   # standardised feature
target = y_train.to_numpy()

w, b = 0.0, 0.0
learning_rate = 0.05
losses = []

for step in range(200):
    error = (w * x + b) - target
    losses.append(np.mean(error ** 2))
    # Update w and b here: step against the gradient of the mean squared error
    pass
جارٍ تحميل المحرّر…

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

(لا مخرجات)

الفحوص

  • لا تزيد الخسارة أبدًا من خطوة إلى التي تليها
  • الخسارة النهائية أقل من 20% من الخسارة الابتدائية
  • تتقارب w إلى نحو 45.97
  • تتقارب b إلى نحو 154.34