فاز 2 · درس 2

رگرسیون خطی از صفر تا پیاده‌سازی با scikit-learn

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک خط مستقیم درخشان که از میان مجموعه‌ای از نقاط پراکنده روی یک سطح شبکه‌ای ریاضی عبور می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. مدل
  5. تابع هزینه (میانگین مربعات خطا)
  6. گرادیان کاهشی
  7. معادله‌ی نرمال (راه‌حل تحلیلی)
  8. رگرسیون خطی چندگانه
  9. رگرسیون چندجمله‌ای
  10. ضریب تعیین (امتیاز R-Squared)
  11. پیش‌درآمدی بر تنظیم مدل (رگرسیون ریج)
  12. پیاده‌سازی از صفر
  13. گام 1: تولید داده‌های نمونه
  14. گام 2: رگرسیون خطی از صفر با گرادیان کاهشی
  15. گام 3: معادله‌ی نرمال (راه‌حل تحلیلی)
  16. گام 4: رگرسیون خطی چندگانه
  17. گام 5: رگرسیون چندجمله‌ای
  18. گام 6: رگرسیون ریج (تنظیم L2)
  19. استفاده از ابزارهای واقعی
  20. خروجی این درس
  21. تمرین‌ها
  22. اصطلاحات کلیدی
  23. برای مطالعه‌ی بیشتر

رگرسیون خطی دروازه‌ی ورود به دنیای یادگیری ماشین است؛ الگوریتمی بنیادین که پایه‌های اصلی آموزش مدل‌ها یعنی تعریف تابع هزینه، گرادیان کاهشی و بهینه‌سازی پارامترها را به شفاف‌ترین شکل ممکن نشان می‌دهد. تسلط بر این مفهوم به شما امکان می‌دهد الگوهای اساسی یادگیری نظارت‌شده را درک کرده و در مسائل واقعی مانند پیش‌بینی قیمت، تحلیل تقاضا و ارزیابی داده‌ها به کار بگیرید.

در این درس علاوه بر درک شهودی و ریاضیاتی مفاهیمی مانند میانگین مربعات خطا (MSE) و معادله‌ی نرمال، رگرسیون خطی ساده و چندگانه را ابتدا از صفر با پایتون پیاده‌سازی می‌کنید و سپس پیاده‌سازی صنعتی آن را با کتابخانه‌ی scikit-learn فرا می‌گیرید. تمام مراحل این درس نیازی به تجهیزات پردازشی پیچیده ندارد و کدهای آن به‌آسانی روی پردازنده‌های معمولی (CPU) بدون هیچ وابستگی ابری قابل اجرا هستند.

رگرسیون خطی بهترین خط مستقیم ممکن را از میان داده‌های شما عبور می‌دهد. این مدل در واقع «Hello World» دنیای یادگیری ماشین است.

اهداف یادگیری

  • به‌دست‌آوردن رابطه‌های به‌روزرسانی گرادیان کاهشی برای میانگین مربعات خطا و پیاده‌سازی رگرسیون خطی از صفر
  • مقایسه‌ی گرادیان کاهشی و معادله‌ی نرمال از نظر پیچیدگی محاسباتی و تعیین زمان مناسب برای استفاده از هرکدام
  • ساخت مدل رگرسیون خطی چندگانه با استانداردسازی ویژگی‌ها و تفسیر وزن‌های یادگرفته‌شده
  • توضیح نحوه‌ی جلوگیری رگرسیون ریج (تنظیم L2) از بیش‌برازش از طریق جریمه‌کردن وزن‌های بزرگ

صورت مسئله

داده‌هایی شامل متراژ چند خانه و قیمت فروش آن‌ها دارید و می‌خواهید قیمت یک خانه‌ی جدید را بر اساس متراژ آن پیش‌بینی کنید. شاید بتوان روی نمودار پراکندگی چشمی حدس‌هایی زد، اما شما به یک فرمول نیاز دارید؛ خطی که به بهترین شکل روی داده‌ها بنشیند تا بتوانید هر متراژی را به آن بدهید و قیمت پیش‌بینی‌شده را تحویل بگیرید.

رگرسیون خطی (linear regression) همین خط را در اختیارتان می‌گذارد. مهم‌تر از آن، کل چرخه‌ی آموزش در یادگیری ماشین را به شما معرفی می‌کند: تعریف مدل، تعریف تابع هزینه و بهینه‌سازی پارامترها. همه‌ی الگوریتم‌های یادگیری ماشین دقیقاً از همین الگو پیروی می‌کنند. اگر این فرایند را در این حالتِ پایه‌ای به‌خوبی یاد بگیرید، ساختارش را در تمام مباحث بعدی تشخیص خواهید داد.

کاربرد این روش محدود به مسائل ساده نیست؛ رگرسیون خطی در سیستم‌های عملیاتی واقعی برای پیش‌بینی تقاضا، تحلیل آزمون‌های A/B، مدل‌سازی مالی و همچنین به‌عنوان مدل پایه (baseline) در تمام مسائل رگرسیون استفاده می‌شود.

مفاهیم پایه

مدل

رگرسیون خطی فرض می‌کند رابطه‌ای خطی میان ورودی (xx) و خروجی (yy) برقرار است:

y=wx+by = wx + b
  • w (وزن/شیب): میزان تغییر yy به‌ازای 1 واحد افزایش در xx
  • b (بایاس/عرض از مبدا): مقدار yy وقتی که x=0x = 0

این فرمول برای چند ورودی (ویژگی) به‌شکل زیر تعمیم می‌یابد:

y=w1x1+w2x2+⋯+wnxn+by = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b

یا به‌صورت برداری: y=wTx+by = w^T x + b

هدف: پیدا کردن مقادیری برای ww و bb که در تمام نمونه‌های آموزشی، مقدار پیش‌بینی‌شده‌ی yy را تا جای ممکن به مقدار واقعی yy نزدیک کند.

تابع هزینه (میانگین مربعات خطا)

میزان «نزدیک بودن» را چطور اندازه می‌گیرید؟ به عددی واحد نیاز دارید که میزان خطای پیش‌بینی‌ها را نشان دهد. رایج‌ترین معیار، میانگین مربعات خطا یا MSE (مخفف Mean Squared Error) است:

MSE=1n∑(ypredicted−yactual)2\text{MSE} = \frac{1}{n} \sum (y_{\text{predicted}} - y_{\text{actual}})^2

چرا خطا را به توان 2 می‌رسانیم؟ به دو دلیل: اول اینکه خطاهای بزرگ را بسیار شدیدتر از خطاهای کوچک جریمه می‌کند (خطای 10 به‌اندازه‌ی 100 برابر بدتر از خطای 1 است، نه 10 برابر). دوم اینکه تابع درجه‌دو، هموار و در همه‌جا مشتق‌پذیر است و کار بهینه‌سازی را بسیار ساده می‌کند.

تابع هزینه سطحی هندسی می‌سازد. برای یک وزن (ww) و یک بایاس (bb)، سطح MSE شبیه به یک کاسه (یک سهمی‌گون کوژ) است. کف این کاسه نقطه‌ای است که MSE در آن کمینه می‌شود؛ بنابراین، آموزش مدل یعنی پیدا کردن همین کف کاسه.

گرادیان کاهشی

گرادیان کاهشی (gradient descent) با برداشتن گام‌هایی رو به پایین، کف کاسه را پیدا می‌کند.

خیربلهمقداردهی تصادفی w و bمحاسبه پیش‌بینی‌ها: y_hat = wx + bمحاسبه هزینه: MSEمحاسبه گرادیان‌ها: dMSE/dw، dMSE/dbبه‌روزرسانی پارامترهاهزینه به اندازه کافی کم است؟پایان: مقادیر بهینه w و b یافت شد

گرادیان‌ها دو نکته را مشخص می‌کنند: پارامترها را در چه جهتی حرکت دهیم و چقدر جابه‌جا کنیم.

برای تابع MSE به‌همراه y^=wx+b\hat{y} = wx + b:

∂MSE∂w=2n∑(y^−y) x∂MSE∂b=2n∑(y^−y)\begin{aligned} \frac{\partial \text{MSE}}{\partial w} &= \frac{2}{n} \sum (\hat{y} - y) \, x \\ \frac{\partial \text{MSE}}{\partial b} &= \frac{2}{n} \sum (\hat{y} - y) \end{aligned}

قاعده‌ی به‌روزرسانی پارامترها:

w=w−learning_rate⋅∂MSE∂wb=b−learning_rate⋅∂MSE∂b\begin{aligned} w &= w - \text{learning\_rate} \cdot \frac{\partial \text{MSE}}{\partial w} \\ b &= b - \text{learning\_rate} \cdot \frac{\partial \text{MSE}}{\partial b} \end{aligned}

نرخ یادگیری (learning rate) طول هر گام را کنترل می‌کند. اگر بیش از حد بزرگ باشد، از نقطه‌ی کمینه رد می‌شوید و مدل واگرا می‌شود؛ اگر خیلی کوچک باشد، آموزش بی‌اندازه طول می‌کشد. مقادیر اولیه‌ی معمول عبارت‌اند از: 0.01، 0.001 یا 0.0001.

معادله‌ی نرمال (راه‌حل تحلیلی)

به‌طور خاص برای رگرسیون خطی، فرمولی مستقیم وجود دارد که وزن‌های بهینه را بدون نیاز به تکرار به دست می‌آورد:

w=(XTX)−1XTyw = (X^T X)^{-1} X^T y

این فرمول با معکوس کردن یک ماتریس، مقدار ww را در یک گام محاسبه می‌کند. این شیوه برای مجموعه‌داده‌های کوچک فوق‌العاده است؛ اما برای داده‌های حجیم (میلیون‌ها سطر یا هزاران ویژگی)، استفاده از گرادیان کاهشی اولویت دارد، زیرا هزینه‌ی محاسباتی معکوس کردن ماتریس متناسب با O(n3)O(n^3) برحسب تعداد ویژگی‌ها رشد می‌کند.

رگرسیون خطی چندگانه

با وجود چند ویژگی، مدل به‌شکل زیر درمی‌آید:

y=w1x1+w2x2+⋯+wnxn+by = w_1 x_1 + w_2 x_2 + \cdots + w_n x_n + b

همه‌چیز مشابه قبل پیش می‌رود: MSE تابع هزینه باقی می‌ماند و گرادیان کاهشی تمام وزن‌ها را هم‌زمان به‌روزرسانی می‌کند. تنها تفاوت این است که این‌بار به‌جای یک خط، یک ابرصفحه (hyperplane) را برازش می‌دهید.

در این حالت مقیاس‌بندی ویژگی‌ها اهمیت زیادی پیدا می‌کند. اگر بازه‌ی تغییرات یک ویژگی بین 0 تا 1 و ویژگی دیگر بین 0 تا 1,000,000 باشد، سطح تابع هزینه کشیده می‌شود و گرادیان کاهشی به‌سختی همگرا خواهد شد. بنابراین پیش از شروع آموزش، ویژگی‌ها را استاندارد کنید (میانگین را کم و بر انحراف معیار تقسیم کنید).

رگرسیون چندجمله‌ای

اگر رابطه‌ی بین داده‌ها خطی نباشد چه؟ باز هم می‌توانید با ساختن ویژگی‌های چندجمله‌ای از رگرسیون خطی استفاده کنید:

y=w1x+w2x2+w3x3+by = w_1 x + w_2 x^2 + w_3 x^3 + b

این مدل همچنان رگرسیون «خطی» نامیده می‌شود، چون رابطه نسبت به وزن‌ها (w1,w2,w3w_1, w_2, w_3) خطی است؛ شما فقط از ویژگی‌های غیرخطیِ xx استفاده کرده‌اید.

چندجمله‌ای‌های درجه‌بالاتر می‌توانند منحنی‌های پیچیده‌تری را برازش دهند، اما خطر بیش‌برازش (overfitting) را بالا می‌برند. یک چندجمله‌ای درجه‌ی 10 شاید دقیقاً از تمام 10 نقطه‌ی داده‌ی شما عبور کند، ولی روی داده‌های جدید پیش‌بینی‌های بسیار ضعیفی ارائه خواهد داد.

ضریب تعیین (امتیاز R-Squared)

معیار MSE نشان می‌دهد چقدر خطا دارید، اما این عدد به مقیاس yy بستگی دارد. ضریب تعیین یا R2R^2 معیاری مستقل از مقیاس به دست می‌دهد:

R2=1−sum of squared residualssum of squared deviations from mean=1−SSresSStot\begin{aligned} R^2 &= 1 - \frac{\text{sum of squared residuals}}{\text{sum of squared deviations from mean}} \\ &= 1 - \frac{SS_{\text{res}}}{SS_{\text{tot}}} \end{aligned}
  • R2=1.0R^2 = 1.0: پیش‌بینی بی‌نقص
  • R2=0.0R^2 = 0.0: عملکرد مدل تفاوتی با پیش‌بینی مداومِ میانگین ندارد
  • R2<0.0R^2 < 0.0: عملکرد مدل حتی از پیش‌بینی میانگین هم بدتر است

پیش‌درآمدی بر تنظیم مدل (رگرسیون ریج)

وقتی تعداد ویژگی‌ها زیاد باشد، مدل ممکن است با نسبت دادن وزن‌های بزرگ دچار بیش‌برازش شود. رگرسیون ریج (Ridge regression یا تنظیم L2) یک جریمه به تابع هزینه اضافه می‌کند:

Cost=MSE+λ∑wi2\text{Cost} = \text{MSE} + \lambda \sum w_i^2

این جمله‌ی جریمه مانع از بزرگ شدن وزن‌ها می‌شود. هایپرپارامتر λ\lambda این تعادل را مدیریت می‌کند: هرچه λ\lambda بزرگ‌تر باشد، وزن‌ها کوچک‌تر می‌شوند و قدرت تنظیم مدل افزایش می‌یابد. این موضوع را در درس‌های آینده مفصل بررسی خواهیم کرد؛ فعلاً در همین حد بدانید که چنین روشی وجود دارد و به چه دلیلی به کار می‌آید.

برازش رگرسیون خطیشیب و عرض از مبدا را بکشید
LINEAR REGRESSION FITEach orange bar is a residual, the gap between a point and the line. Squaring and averaging them gives the mean squared error. Least squares is just the choice of slope and intercept that makes that average as small as possible.
هر میله‌ی نارنجی یک باقی‌مانده (residual) است؛ یعنی فاصله‌ی بین یک نقطه و خط. با مربع کردن و میانگین‌گیری از آن‌ها، میانگین مربعات خطا (MSE) به دست می‌آید. روش کمترین مربعات صرفاً انتخاب شیب و عرض از مبدایی است که این میانگین را تا حد ممکن کوچک کند.

پیاده‌سازی از صفر

گام 1: تولید داده‌های نمونه

import random
import math

random.seed(42)

TRUE_W = 3.0
TRUE_B = 7.0
N_SAMPLES = 100

X = [random.uniform(0, 10) for _ in range(N_SAMPLES)]
y = [TRUE_W * x + TRUE_B + random.gauss(0, 2.0) for x in X]

print(f"Generated {N_SAMPLES} samples")
print(f"True relationship: y = {TRUE_W}x + {TRUE_B} (+ noise)")
print(f"First 5 points: {[(round(X[i], 2), round(y[i], 2)) for i in range(5)]}")

گام 2: رگرسیون خطی از صفر با گرادیان کاهشی

class LinearRegression:
    def __init__(self, learning_rate=0.01):
        self.w = 0.0
        self.b = 0.0
        self.lr = learning_rate
        self.cost_history = []

    def predict(self, X):
        return [self.w * x + self.b for x in X]

    def compute_cost(self, X, y):
        predictions = self.predict(X)
        n = len(y)
        cost = sum((pred - actual) ** 2 for pred, actual in zip(predictions, y)) / n
        return cost

    def compute_gradients(self, X, y):
        predictions = self.predict(X)
        n = len(y)
        dw = (2 / n) * sum((pred - actual) * x for pred, actual, x in zip(predictions, y, X))
        db = (2 / n) * sum(pred - actual for pred, actual in zip(predictions, y))
        return dw, db

    def fit(self, X, y, epochs=1000, print_every=200):
        for epoch in range(epochs):
            dw, db = self.compute_gradients(X, y)
            self.w -= self.lr * dw
            self.b -= self.lr * db
            cost = self.compute_cost(X, y)
            self.cost_history.append(cost)
            if epoch % print_every == 0:
                print(f"  Epoch {epoch:4d} | Cost: {cost:.4f} | w: {self.w:.4f} | b: {self.b:.4f}")
        return self

    def r_squared(self, X, y):
        predictions = self.predict(X)
        y_mean = sum(y) / len(y)
        ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
        ss_tot = sum((actual - y_mean) ** 2 for actual in y)
        return 1 - (ss_res / ss_tot)


print("=== Training Linear Regression (Gradient Descent) ===")
model = LinearRegression(learning_rate=0.005)
model.fit(X, y, epochs=1000, print_every=200)
print(f"\nLearned: y = {model.w:.4f}x + {model.b:.4f}")
print(f"True:    y = {TRUE_W}x + {TRUE_B}")
print(f"R-squared: {model.r_squared(X, y):.4f}")

گام 3: معادله‌ی نرمال (راه‌حل تحلیلی)

class LinearRegressionNormal:
    def __init__(self):
        self.w = 0.0
        self.b = 0.0

    def fit(self, X, y):
        n = len(X)
        x_mean = sum(X) / n
        y_mean = sum(y) / n
        numerator = sum((X[i] - x_mean) * (y[i] - y_mean) for i in range(n))
        denominator = sum((X[i] - x_mean) ** 2 for i in range(n))
        self.w = numerator / denominator
        self.b = y_mean - self.w * x_mean
        return self

    def predict(self, X):
        return [self.w * x + self.b for x in X]

    def r_squared(self, X, y):
        predictions = self.predict(X)
        y_mean = sum(y) / len(y)
        ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
        ss_tot = sum((actual - y_mean) ** 2 for actual in y)
        return 1 - (ss_res / ss_tot)


print("\n=== Normal Equation (Closed-Form) ===")
model_normal = LinearRegressionNormal()
model_normal.fit(X, y)
print(f"Learned: y = {model_normal.w:.4f}x + {model_normal.b:.4f}")
print(f"R-squared: {model_normal.r_squared(X, y):.4f}")

گام 4: رگرسیون خطی چندگانه

class MultipleLinearRegression:
    def __init__(self, n_features, learning_rate=0.01):
        self.weights = [0.0] * n_features
        self.bias = 0.0
        self.lr = learning_rate
        self.cost_history = []

    def predict_single(self, x):
        return sum(w * xi for w, xi in zip(self.weights, x)) + self.bias

    def predict(self, X):
        return [self.predict_single(x) for x in X]

    def compute_cost(self, X, y):
        predictions = self.predict(X)
        n = len(y)
        return sum((pred - actual) ** 2 for pred, actual in zip(predictions, y)) / n

    def fit(self, X, y, epochs=1000, print_every=200):
        n = len(y)
        n_features = len(X[0])
        for epoch in range(epochs):
            predictions = self.predict(X)
            errors = [pred - actual for pred, actual in zip(predictions, y)]
            for j in range(n_features):
                grad = (2 / n) * sum(errors[i] * X[i][j] for i in range(n))
                self.weights[j] -= self.lr * grad
            grad_b = (2 / n) * sum(errors)
            self.bias -= self.lr * grad_b
            cost = self.compute_cost(X, y)
            self.cost_history.append(cost)
            if epoch % print_every == 0:
                print(f"  Epoch {epoch:4d} | Cost: {cost:.4f}")
        return self

    def r_squared(self, X, y):
        predictions = self.predict(X)
        y_mean = sum(y) / len(y)
        ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
        ss_tot = sum((actual - y_mean) ** 2 for actual in y)
        return 1 - (ss_res / ss_tot)


random.seed(42)
N = 100
X_multi = []
y_multi = []
for _ in range(N):
    size = random.uniform(500, 3000)
    bedrooms = random.randint(1, 5)
    age = random.uniform(0, 50)
    price = 50 * size + 10000 * bedrooms - 1000 * age + 50000 + random.gauss(0, 20000)
    X_multi.append([size, bedrooms, age])
    y_multi.append(price)


def standardize(X):
    n_features = len(X[0])
    means = [sum(X[i][j] for i in range(len(X))) / len(X) for j in range(n_features)]
    stds = []
    for j in range(n_features):
        variance = sum((X[i][j] - means[j]) ** 2 for i in range(len(X))) / len(X)
        stds.append(variance ** 0.5)
    X_scaled = []
    for i in range(len(X)):
        row = [(X[i][j] - means[j]) / stds[j] if stds[j] > 0 else 0 for j in range(n_features)]
        X_scaled.append(row)
    return X_scaled, means, stds


y_mean_val = sum(y_multi) / len(y_multi)
y_std_val = (sum((yi - y_mean_val) ** 2 for yi in y_multi) / len(y_multi)) ** 0.5
y_scaled = [(yi - y_mean_val) / y_std_val for yi in y_multi]

X_scaled, x_means, x_stds = standardize(X_multi)

print("\n=== Multiple Linear Regression (3 features) ===")
print("Features: house size, bedrooms, age")
multi_model = MultipleLinearRegression(n_features=3, learning_rate=0.01)
multi_model.fit(X_scaled, y_scaled, epochs=1000, print_every=200)

print(f"\nWeights (standardized): {[round(w, 4) for w in multi_model.weights]}")
print(f"Bias (standardized): {multi_model.bias:.4f}")
print(f"R-squared: {multi_model.r_squared(X_scaled, y_scaled):.4f}")

گام 5: رگرسیون چندجمله‌ای

class PolynomialRegression:
    def __init__(self, degree, learning_rate=0.01):
        self.degree = degree
        self.weights = [0.0] * degree
        self.bias = 0.0
        self.lr = learning_rate

    def make_features(self, X):
        return [[x ** (d + 1) for d in range(self.degree)] for x in X]

    def predict(self, X):
        features = self.make_features(X)
        return [sum(w * f for w, f in zip(self.weights, row)) + self.bias for row in features]

    def fit(self, X, y, epochs=1000, print_every=200):
        features = self.make_features(X)
        n = len(y)
        for epoch in range(epochs):
            predictions = [sum(w * f for w, f in zip(self.weights, row)) + self.bias for row in features]
            errors = [pred - actual for pred, actual in zip(predictions, y)]
            for j in range(self.degree):
                grad = (2 / n) * sum(errors[i] * features[i][j] for i in range(n))
                self.weights[j] -= self.lr * grad
            grad_b = (2 / n) * sum(errors)
            self.bias -= self.lr * grad_b
            if epoch % print_every == 0:
                cost = sum(e ** 2 for e in errors) / n
                print(f"  Epoch {epoch:4d} | Cost: {cost:.6f}")
        return self

    def r_squared(self, X, y):
        predictions = self.predict(X)
        y_mean = sum(y) / len(y)
        ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
        ss_tot = sum((actual - y_mean) ** 2 for actual in y)
        return 1 - (ss_res / ss_tot)


random.seed(42)
X_poly = [x / 10.0 for x in range(0, 50)]
y_poly = [0.5 * x ** 2 - 2 * x + 3 + random.gauss(0, 1.0) for x in X_poly]

x_max = max(abs(x) for x in X_poly)
X_poly_norm = [x / x_max for x in X_poly]
y_poly_mean = sum(y_poly) / len(y_poly)
y_poly_std = (sum((yi - y_poly_mean) ** 2 for yi in y_poly) / len(y_poly)) ** 0.5
y_poly_norm = [(yi - y_poly_mean) / y_poly_std for yi in y_poly]

print("\n=== Polynomial Regression (degree 2 vs degree 5) ===")
print("True relationship: y = 0.5x^2 - 2x + 3")

print("\nDegree 2:")
poly2 = PolynomialRegression(degree=2, learning_rate=0.1)
poly2.fit(X_poly_norm, y_poly_norm, epochs=2000, print_every=500)
print(f"  R-squared: {poly2.r_squared(X_poly_norm, y_poly_norm):.4f}")

print("\nDegree 5:")
poly5 = PolynomialRegression(degree=5, learning_rate=0.1)
poly5.fit(X_poly_norm, y_poly_norm, epochs=2000, print_every=500)
print(f"  R-squared: {poly5.r_squared(X_poly_norm, y_poly_norm):.4f}")

print("\nDegree 2 fits the true curve well. Degree 5 fits training data slightly better")
print("but risks overfitting on new data.")

گام 6: رگرسیون ریج (تنظیم L2)

class RidgeRegression:
    def __init__(self, n_features, learning_rate=0.01, alpha=1.0):
        self.weights = [0.0] * n_features
        self.bias = 0.0
        self.lr = learning_rate
        self.alpha = alpha

    def predict_single(self, x):
        return sum(w * xi for w, xi in zip(self.weights, x)) + self.bias

    def predict(self, X):
        return [self.predict_single(x) for x in X]

    def fit(self, X, y, epochs=1000, print_every=200):
        n = len(y)
        n_features = len(X[0])
        for epoch in range(epochs):
            predictions = self.predict(X)
            errors = [pred - actual for pred, actual in zip(predictions, y)]
            mse = sum(e ** 2 for e in errors) / n
            reg_term = self.alpha * sum(w ** 2 for w in self.weights)
            cost = mse + reg_term
            for j in range(n_features):
                grad = (2 / n) * sum(errors[i] * X[i][j] for i in range(n))
                grad += 2 * self.alpha * self.weights[j]
                self.weights[j] -= self.lr * grad
            grad_b = (2 / n) * sum(errors)
            self.bias -= self.lr * grad_b
            if epoch % print_every == 0:
                print(f"  Epoch {epoch:4d} | Cost: {cost:.4f} | L2 penalty: {reg_term:.4f}")
        return self


print("\n=== Ridge Regression (L2 Regularization) ===")
print("Same data as multiple regression, with alpha=0.1")
ridge = RidgeRegression(n_features=3, learning_rate=0.01, alpha=0.1)
ridge.fit(X_scaled, y_scaled, epochs=1000, print_every=200)
print(f"\nRidge weights: {[round(w, 4) for w in ridge.weights]}")
print(f"Plain weights: {[round(w, 4) for w in multi_model.weights]}")
print("Ridge weights are smaller (shrunk toward zero) due to the L2 penalty.")

استفاده از ابزارهای واقعی

حالا همین کارها را با scikit-learn پیاده می‌کنیم؛ یعنی همان ابزاری که در محیط کار واقعی استفاده خواهید کرد.

from sklearn.linear_model import LinearRegression as SklearnLR
from sklearn.linear_model import Ridge
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np

np.random.seed(42)
X_sk = np.random.uniform(0, 10, (100, 1))
y_sk = 3.0 * X_sk.squeeze() + 7.0 + np.random.normal(0, 2.0, 100)

X_train, X_test, y_train, y_test = train_test_split(X_sk, y_sk, test_size=0.2, random_state=42)

lr = SklearnLR()
lr.fit(X_train, y_train)
y_pred = lr.predict(X_test)

print("=== Scikit-learn Linear Regression ===")
print(f"Coefficient (w): {lr.coef_[0]:.4f}")
print(f"Intercept (b): {lr.intercept_:.4f}")
print(f"R-squared (test): {r2_score(y_test, y_pred):.4f}")
print(f"MSE (test): {mean_squared_error(y_test, y_pred):.4f}")

poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly_sk = poly.fit_transform(X_train)
X_poly_test = poly.transform(X_test)

lr_poly = SklearnLR()
lr_poly.fit(X_poly_sk, y_train)
print(f"\nPolynomial degree 2 R-squared: {r2_score(y_test, lr_poly.predict(X_poly_test)):.4f}")

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_train)
print(f"Ridge R-squared: {r2_score(y_test, ridge.predict(X_test_scaled)):.4f}")
print(f"Ridge coefficient: {ridge.coef_[0]:.4f}")

کدی که خودتان از صفر نوشتید و scikit-learn خروجی یکسانی تولید می‌کنند. تفاوت اصلی اینجاست: scikit-learn حالت‌های خاص (edge cases)، پایداری عددی (numerical stability) و بهینه‌سازی‌های عملکردی را مدیریت می‌کند. در محیط عملیاتی از این کتابخانه استفاده کنید و از نسخه‌ی دست‌نویس برای درک عمیق پشت‌صحنه بهره ببرید.

خروجی این درس

این درس خروجی زیر را تولید می‌کند:

  • outputs/skill-regression.md - مهارتی برای انتخاب رویکرد رگرسیون مناسب بر اساس نوع مسئله

تمرین‌ها

  1. الگوریتم‌های گرادیان کاهشی دسته‌ای (batch)، گرادیان کاهشی تصادفی (SGD) و گرادیان کاهشی با دسته‌ی کوچک (mini-batch) را پیاده‌سازی کنید. سرعت همگرایی آن‌ها را روی یک مجموعه‌داده‌ی یکسان مقایسه کنید. کدام‌یک زودتر همگرا می‌شود؟ نمودار تابع هزینه‌ی کدام‌یک هموارتر است؟
  2. داده‌هایی بر اساس یک تابع درجه‌سه تولید کنید (y=ax3+bx2+cx+d+noisey = ax^3 + bx^2 + cx + d + \text{noise}). چندجمله‌ای‌هایی با درجات 1، 3 و 10 را روی آن‌ها برازش دهید. مقدار R2R^2 داده‌های آموزش و R2R^2 داده‌های آزمون را مقایسه کنید. بیش‌برازش از چه درجه‌ای خودش را نشان می‌دهد؟
  3. رگرسیون لاسو (Lasso regression یا تنظیم L1: penalty=α∑∣wi∣\text{penalty} = \alpha \sum \lvert w_i \rvert) را پیاده‌سازی کنید. آن را روی داده‌های چندویژگیِ مسکن آموزش دهید. ببینید کدام وزن‌ها برخلاف ریج صفر می‌شوند. چرا L1 جواب‌های تنک (sparse) تولید می‌کند اما L2 این‌طور نیست؟

اصطلاحات کلیدی

اصطلاح در گفتگوهای عامیانه معنای دقیق فنی
رگرسیون خطی «کشیدن یک خط از میان داده‌ها» یافتن وزن ww و بایاس bb به‌نحوی که مجموع توان دوم اختلاف بین مقادیر wx+bwx+b و مقادیر واقعی yy کمینه شود
تابع هزینه «مدل چقدر بد کار می‌کند» تابعی که پارامترهای مدل را به یک عدد واحد نگاشت می‌کند تا خطای پیش‌بینی را بسنجد؛ الگوریتم بهینه‌سازی این عدد را کمینه می‌کند
میانگین مربعات خطا «میانگین خطاهای به توان دو رسیده» 1n∑(predicted−actual)2\frac{1}{n} \sum (\text{predicted} - \text{actual})^2، که خطاهای بزرگ را بسیار سنگین‌تر جریمه می‌کند
گرادیان کاهشی «حرکت به سمت پایین تپه» تنظیم گام‌به‌گام پارامترها در جهتی که تابع هزینه را کم کند، با استفاده از مشتق جزئی (partial derivative)
نرخ یادگیری «اندازه‌ی گام» عددی اسکالر که میزان تغییر پارامترها را در هر گام از گرادیان کاهشی کنترل می‌کند
معادله‌ی نرمال «حل مسئله به‌صورت مستقیم» راه‌حل تحلیلی w=(XTX)−1XTyw = (X^T X)^{-1} X^T y که وزن‌های بهینه را بدون نیاز به تکرار به دست می‌آورد
ضریب تعیین (R-squared) «کیفیت برازش چقدر خوب است» کسری از واریانس yy که مدل آن را توضیح می‌دهد؛ بازه‌ی تغییرات آن از منفی بی‌نهایت تا 1.0 است
مقیاس‌بندی ویژگی‌ها «یک‌دست کردن ابعاد ویژگی‌ها» تغییر بازه‌ی ویژگی‌ها به مقادیری مشابه (مثلاً میانگین صفر و واریانس یک) تا گرادیان کاهشی سریع‌تر همگرا شود
تنظیم مدل (Regularization) «جریمه کردن پیچیدگی مدل» افزودن یک جمله به تابع هزینه برای کوچک نگه‌داشتن وزن‌ها تا از بیش‌برازش جلوگیری شود
رگرسیون ریج «تنظیم L2» رگرسیون خطی با اضافه‌شدن جریمه‌ی λ∑wi2\lambda \sum w_i^2 به تابع MSE
رگرسیون چندجمله‌ای «برازش منحنی با ریاضیات خطی» رگرسیون خطی روی ویژگی‌های چندجمله‌ای (x,x2,x3,…x, x^2, x^3, \ldots)، که همچنان نسبت به وزن‌ها خطی باقی می‌ماند
بیش‌برازش (Overfitting) «حفظ کردن داده‌های آموزش» استفاده از مدلی چنان پیچیده که نوفه (نویز) داده‌های آموزش را هم یاد می‌گیرد و روی داده‌های جدید شکست می‌خورد

برای مطالعه‌ی بیشتر

سؤالات متداول

تفاوت گرادیان کاهشی با معادله نرمال در رگرسیون خطی چیست؟

معادله‌ی نرمال یک راه‌حل تحلیلی یک‌گامه است که وزن‌ها را از طریق معکوس کردن ماتریس محاسبه می‌کند، اما با افزایش ویژگی‌ها به دلیل پیچیدگی محاسباتی کند می‌شود. در مقابل، گرادیان کاهشی روشی تکرارشونده و مقیاس‌پذیر است که برای مجموعه‌داده‌های بزرگ کارایی بهتری دارد.

چرا در رگرسیون خطی چندگانه نیاز به مقیاس‌بندی ویژگی‌ها داریم؟

تفاوت چشمگیر بازه‌ی ویژگی‌ها باعث کشیدگی سطح تابع هزینه می‌شود و سرعت همگرایی گرادیان کاهشی را به‌شدت کاهش می‌دهد. استانداردسازی ویژگی‌ها همگرایی را بسیار سریع‌تر و پایدارتر می‌کند.

رگرسیون ریج چه نقشی در جلوگیری از بیش‌برازش دارد؟

رگرسیون ریج با افزودن جریمه‌ی L2 (مجموع توان دوم وزن‌ها ضرب در ضریب تنظیم) به تابع هزینه، مانع از رشد بیش از اندازه‌ی وزن‌ها می‌شود و از یادگیری نوفه‌ها جلوگیری می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.