فاز 2 · درس 3

رگرسیون لجستیک و دسته‌بندی از صفر

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

در مسائل واقعی دنیای نرم‌افزار، بسیار پیش می‌آید که به‌جای پیش‌بینی یک عدد پیوسته، نیازمند تصمیم‌گیری‌های بله/خیر یا تفکیک کلاس‌ها باشیم؛ برای مثال تشخیص هرزنامه بودن یک ایمیل، دسته‌بندی تراکنش‌های مشکوک به کلاهبرداری، یا تشخیص بدخیم بودن یک تومور. استفاده از رگرسیون خطی برای چنین مسائلی به خروجی‌های بی‌کران و نامطمئن ختم می‌شود. رگرسیون لجستیک این چالش را با تبدیل خروجی خطی به احتمال‌های استاندارد بین 0 و 1 حل می‌کند.

در این درس یاد می‌گیرید که چگونه با ترکیب مدل خطی و تابع سیگموئید، یک الگوریتم دسته‌بندی پایدار بسازید. پیاده‌سازی از صفر این روش و درک عمیق توابعی چون آنتروپی متقاطع و معیارهایی مانند دقت (Precision) و بازخوانی (Recall)، شالوده‌ی ورود به مدل‌های پیچیده‌تر و یادگیری عمیق است.

کدهای این آموزش روی پردازنده‌ی استاندارد هر رایانه‌ای بدون نیاز به کارت گرافیک اجرا می‌شوند. فقط برای نصب کتابخانه‌های پایه‌ای مثل NumPy و scikit-learn ممکن است به دلیل نوسانات دانلود پکیج‌ها در ایران، به استفاده از میرورهای داخلی پایتون یا تنظیم مجدد pip نیاز پیدا کنید.

رگرسیون لجستیک (logistic regression) یک خط راست را خم می‌کند و به منحنی S تبدیل می‌سازد تا با احتمال‌ها به پرسش‌های بله/خیر پاسخ دهد.

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

  • پیاده‌سازی رگرسیون لجستیک از صفر با استفاده از تابع سیگموئید (sigmoid function) و تابع زیان (loss) آنتروپی متقاطع دودویی (binary cross-entropy loss)
  • محاسبه و تفسیر دقت (precision)، بازخوانی (recall)، امتیاز F1 و ماتریس درهم‌ریختگی (confusion matrix) برای دسته‌بندی دودویی
  • توضیح علت ناکارآمدی MSE در مسائل دسته‌بندی و چرایی ایجاد رویه‌ی هزینه‌ی محدب توسط آنتروپی متقاطع دودویی
  • ساخت مدل رگرسیون سافت‌مکس (softmax regression) برای دسته‌بندی چندکلاسه و ارزیابی مصالحه‌های ناشی از تنظیم آستانه (threshold tuning)

صورت مسئله

می‌خواهید بر اساس اندازه‌ی یک تومور، بدخیم یا خوش‌خیم بودن آن را پیش‌بینی کنید. رگرسیون خطی را امتحان می‌کنید. مدل اعدادی مثل 0.3، 1.7 یا 0.5- را خروجی می‌دهد. این اعداد چه معنایی دارند؟ آیا 1.7 یعنی «خیلی بدخیم»؟ آیا 0.5- یعنی «خیلی خوش‌خیم»؟ خروجی رگرسیون خطی اعدادی بی‌کران است. اما دسته‌بندی به احتمال‌هایی محدود بین 0 و 1 نیاز دارد و یک تصمیم مشخص: بله یا خیر.

رگرسیون لجستیک این مشکل را حل می‌کند. این روش همان ترکیب خطی (wx+bwx + b) را می‌گیرد و آن را از تابع سیگموئید عبور می‌دهد؛ تابعی که هر عددی را فشرده کرده و به بازه‌ی (0, 1) می‌برد. خروجی حالا یک احتمال است. یک آستانه (معمولاً 0.5) تعیین می‌کنید و تصمیم نهایی را می‌گیرید.

این الگوریتم یکی از پرکاربردترین‌ها در عمل است. بر خلاف نامش، رگرسیون لجستیک یک الگوریتم دسته‌بندی است، نه رگرسیون. این نام‌گذاری از تابع لجستیک (سیگموئید) استفاده‌شده در آن می‌آید.

مبانی نظری

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

تصور کنید می‌خواهید قبولی/مردودی (1/0) را بر اساس ساعت‌های مطالعه پیش‌بینی کنید. رگرسیون خطی یک خط را روی داده‌ها برازش می‌کند:

hours:  1   2   3   4   5   6   7   8   9   10
actual: 0   0   0   0   1   1   1   1   1   1

این برازش خطی ممکن است در ساعت 1 پیش‌بینی 0.2- و در ساعت 10 پیش‌بینی 1.3 بدهد. این مقادیر احتمال نیستند؛ از 0 کمتر و از 1 بیشتر می‌شوند. بدتر از آن، وجود حتی یک داده‌ی پرت (مثلاً کسی که 50 ساعت درس خوانده) کل خط را می‌کشد و پیش‌بینی را برای همه تغییر می‌دهد.

دسته‌بندی به تابعی نیاز دارد که:

  • مقادیری بین 0 و 1 (احتمال) تولید کند
  • گذاری تیز و ناگهانی بسازد (یک مرز تصمیم‌گیری یا decision boundary)
  • داده‌های پرت دور از مرز، آن را دچار اعوجاج نکنند

تابع سیگموئید

تابع سیگموئید دقیقاً همین کار را می‌کند:

σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}

ویژگی‌ها:

  • وقتی zz بزرگ و مثبت باشد، σ(z)\sigma(z) به 1 میل می‌کند
  • وقتی zz بزرگ و منفی باشد، σ(z)\sigma(z) به 0 میل می‌کند
  • وقتی z=0z = 0، آنگاه σ(z)=0.5\sigma(z) = 0.5
  • خروجی همیشه بین 0 و 1 است
  • تابع در همه‌جا هموار و مشتق‌پذیر است

مشتق این تابع شکل ساده و مناسبی دارد: σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z) (1 - \sigma(z)). این ویژگی محاسبه‌ی گرادیان را کارآمد می‌کند.

رگرسیون لجستیک = مدل خطی + سیگموئید

مدل ابتدا مقدار z=wx+bz = wx + b را حساب می‌کند (همانند رگرسیون خطی)، سپس تابع سیگموئید را روی آن اعمال می‌کند:

بلهخیرویژگی‌های ورودی xLinear: z = wx + bSigmoid: p = 1/(1+e^-z)p = 0.5?پیش‌بینی 1پیش‌بینی 0

خروجی pp به‌صورت P(y=1∣x)P(y=1 \mid x)، یعنی احتمال تعلق ورودی به کلاس 1، تفسیر می‌شود. مرز تصمیم‌گیری جایی است که wx+b=0wx + b = 0 باشد؛ نقطه‌ای که خروجی سیگموئید دقیقاً 0.5 می‌شود.

تابع زیان آنتروپی متقاطع دودویی

برای رگرسیون لجستیک نمی‌توانید از MSE استفاده کنید. ترکیب MSE با سیگموئید، یک رویه‌ی هزینه‌ی غیرمحدب با مینیمم‌های محلی پرشمار ایجاد می‌کند. به‌جای آن، از آنتروپی متقاطع دودویی (زیان لگاریتمی یا log loss) استفاده کنید:

Loss=−1n∑(ylog⁡(p)+(1−y)log⁡(1−p))\text{Loss} = -\frac{1}{n} \sum \left( y \log(p) + (1-y) \log(1-p) \right)

دلیل عملکرد این تابع:

  • وقتی y=1y=1 باشد و pp نزدیک 1: log⁡(1)=0\log(1) = 0، بنابراین زیان نزدیک 0 است (درست، هزینه‌ی پایین)
  • وقتی y=1y=1 باشد و pp نزدیک 0: log⁡(0)\log(0) به منفی بی‌نهایت میل می‌کند، بنابراین زیان بسیار بزرگ است (غلط، هزینه‌ی بالا)
  • وقتی y=0y=0 باشد و pp نزدیک 0: log⁡(1)=0\log(1) = 0، بنابراین زیان نزدیک 0 است (درست، هزینه‌ی پایین)
  • وقتی y=0y=0 باشد و pp نزدیک 1: log⁡(0)\log(0) به منفی بی‌نهایت میل می‌کند، بنابراین زیان بسیار بزرگ است (غلط، هزینه‌ی بالا)

این تابع زیان برای رگرسیون لجستیک محدب است؛ در نتیجه هر مینیمم محلی همان مینیمم سراسری است و گرادیان کاهشی نمی‌تواند در یک نقطه‌ی بد گیر بیفتد. روی داده‌هایی که کاملاً تفکیک‌پذیر هستند، با رشد وزن‌ها مقدار زیان پیوسته کاهش می‌یابد و بنابراین هیچ مینیمم متناهی وجود ندارد؛ منظم‌سازی L2 (یا L2 regularization) این مشکل را حل می‌کند و مینیمم را یکتا می‌سازد.

گرادیان کاهشی در رگرسیون لجستیک

گرادیان‌های آنتروپی متقاطع دودویی با سیگموئید، شکلی تمیز و سرراست دارند:

∂L∂w=1n∑(p−y) x∂L∂b=1n∑(p−y)\begin{aligned} \frac{\partial L}{\partial w} &= \frac{1}{n} \sum (p - y) \, x \\ \frac{\partial L}{\partial b} &= \frac{1}{n} \sum (p - y) \end{aligned}

این فرمول‌ها دقیقاً شبیه گرادیان‌های رگرسیون خطی هستند؛ با این تفاوت که اینجا p=σ(wx+b)p = \sigma(wx + b) جایگزین p=wx+bp = wx + b شده است. سیگموئید رفتار غیرخطی را وارد می‌کند، اما قاعده‌ی به‌روزرسانی در گرادیان کاهشی ثابت می‌ماند.

خیربلهمقداردهی اولیه w=0, b=0گذر رو به جلو: z = wx+b, p = sigmoid zمحاسبه تابع زیان (loss): آنتروپی متقاطعدودوییمحاسبه گرادیان‌ها: dw = (1/n) *sum((p-y)*x)به‌روزرسانی: w = w - lr*dw, b = b - lr*dbهمگرا شد؟مدل آموزش دید

مرز تصمیم‌گیری

برای یک ورودی دوبعدی (دو ویژگی)، مرز تصمیم‌گیری خطی است که در آن:

w1x1+w2x2+b=0w_1 x_1 + w_2 x_2 + b = 0

نقاط یک سمت خط در کلاس 1 و نقاط سمت دیگر در کلاس 0 دسته‌بندی می‌شوند. رگرسیون لجستیک همیشه یک مرز تصمیم‌گیری خطی تولید می‌کند. اگر به مرز منحنی نیاز دارید، باید یا ویژگی‌های چندجمله‌ای اضافه کنید یا سراغ مدلی غیرخطی بروید.

دسته‌بندی چندکلاسه با سافت‌مکس

رگرسیون لجستیک دودویی دو کلاس را مدیریت می‌کند. برای kk کلاس، از تابع سافت‌مکس (softmax) استفاده کنید:

softmax(zi)=ezi∑jezj\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}

هر کلاس بردار وزن خودش را دارد. مدل برای هر کلاس امتیازی مانند ziz_i حساب می‌کند، سپس سافت‌مکس این امتیازها را به احتمال‌هایی تبدیل می‌کند که مجموعشان 1 می‌شود. کلاس پیش‌بینی‌شده همان کلاسی است که بالاترین احتمال را دارد.

تابع زیان تبدیل به آنتروپی متقاطع دسته‌ای (categorical cross-entropy) می‌شود:

Loss=−1n∑∑kyklog⁡(pk)\text{Loss} = -\frac{1}{n} \sum \sum_k y_k \log(p_k)

که در آن yky_k برای کلاس واقعی برابر 1 و برای سایر کلاس‌ها 0 است (کدگذاری وان‌هات یا one-hot encoding).

معیارهای ارزیابی

صحت (accuracy) به‌تنهایی کافی نیست. در مجموعه‌داده‌ای با 95% نمونه‌ی منفی و 5% مثبت، مدلی که همیشه منفی پیش‌بینی کند به صحت 95% می‌رسد، اما کاملاً بی‌فایده است.

ماتریس درهم‌ریختگی:

پیش‌بینی مثبت پیش‌بینی منفی
مثبت واقعی مثبت واقعی (TP) منفی کاذب (FN)
منفی واقعی مثبت کاذب (FP) منفی واقعی (TN)

دقت: از کل مواردی که مثبت پیش‌بینی شده‌اند، چه تعداد واقعاً مثبت هستند؟ Precision=TPTP+FP\text{Precision} = \frac{TP}{TP + FP}

بازخوانی (حساسیت یا Sensitivity): از کل موارد واقعاً مثبت، چه تعداد را شناسایی کرده‌ایم؟ Recall=TPTP+FN\text{Recall} = \frac{TP}{TP + FN}

امتیاز F1: میانگین همساز (هارمونیک) دقت و بازخوانی که تعادل بین هر دو معیار را برقرار می‌کند. F1=2⋅Precision⋅RecallPrecision+RecallF_1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}

چه زمانی باید اولویت داد:

  • دقت: وقتی مثبت کاذب پرهزینه باشد (مثلاً فیلتر هرزنامه؛ نمی‌خواهید ایمیل کاری و معتبر حذف شود)
  • بازخوانی: وقتی منفی کاذب پرهزینه باشد (مثلاً غربالگری سرطان؛ نمی‌خواهید هیچ توموری جا بماند)
  • F1: وقتی به یک معیار متعادل نیاز دارید
سیگموئید لاجستیکw و b را بکشید
LOGISTIC SIGMOIDLogistic regression squashes a linear score through the sigmoid to get a probability. The weight controls how sharply the curve turns; the bias slides it left or right. The orange line is the decision boundary, where the probability crosses one half.
رگرسیون لاجستیک یک امتیاز خطی را از طریق تابع سیگموئید فشرده می‌کند تا به احتمال تبدیل شود. وزن شیب و تندی چرخش منحنی را تعیین می‌کند؛ بایاس آن را به چپ یا راست می‌لغزاند. خط نارنجی، مرز تصمیم‌گیری است؛ یعنی جایی که احتمال از 0.5 عبور می‌کند.

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

مرحله‌ی 1: تابع سیگموئید و تولید داده

import random
import math

def sigmoid(z):
    z = max(-500, min(500, z))
    return 1.0 / (1.0 + math.exp(-z))


random.seed(42)
N = 200
X = []
y = []

for _ in range(N // 2):
    X.append([random.gauss(2, 1), random.gauss(2, 1)])
    y.append(0)

for _ in range(N // 2):
    X.append([random.gauss(5, 1), random.gauss(5, 1)])
    y.append(1)

combined = list(zip(X, y))
random.shuffle(combined)
X, y = zip(*combined)
X = list(X)
y = list(y)

print(f"Generated {N} samples (2 classes, 2 features)")
print(f"Class 0 center: (2, 2), Class 1 center: (5, 5)")
print(f"First 5 samples:")
for i in range(5):
    print(f"  Features: [{X[i][0]:.2f}, {X[i][1]:.2f}], Label: {y[i]}")

مرحله‌ی 2: رگرسیون لجستیک از صفر

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

    def predict_proba(self, x):
        z = sum(w * xi for w, xi in zip(self.weights, x)) + self.bias
        return sigmoid(z)

    def predict(self, x, threshold=0.5):
        return 1 if self.predict_proba(x) >= threshold else 0

    def compute_loss(self, X, y):
        n = len(y)
        total = 0.0
        for i in range(n):
            p = self.predict_proba(X[i])
            p = max(1e-15, min(1 - 1e-15, p))
            total += y[i] * math.log(p) + (1 - y[i]) * math.log(1 - p)
        return -total / n

    def fit(self, X, y, epochs=1000, print_every=200):
        n = len(y)
        n_features = len(X[0])
        for epoch in range(epochs):
            dw = [0.0] * n_features
            db = 0.0
            for i in range(n):
                p = self.predict_proba(X[i])
                error = p - y[i]
                for j in range(n_features):
                    dw[j] += error * X[i][j]
                db += error
            for j in range(n_features):
                self.weights[j] -= self.lr * (dw[j] / n)
            self.bias -= self.lr * (db / n)
            loss = self.compute_loss(X, y)
            self.loss_history.append(loss)
            if epoch % print_every == 0:
                print(f"  Epoch {epoch:4d} | Loss: {loss:.4f} | w: [{self.weights[0]:.3f}, {self.weights[1]:.3f}] | b: {self.bias:.3f}")
        return self

    def accuracy(self, X, y):
        correct = sum(1 for i in range(len(y)) if self.predict(X[i]) == y[i])
        return correct / len(y)


split = int(0.8 * N)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

print("\n=== Training Logistic Regression ===")
model = LogisticRegression(n_features=2, learning_rate=0.1)
model.fit(X_train, y_train, epochs=1000, print_every=200)

print(f"\nTrain accuracy: {model.accuracy(X_train, y_train):.4f}")
print(f"Test accuracy:  {model.accuracy(X_test, y_test):.4f}")
print(f"Weights: [{model.weights[0]:.4f}, {model.weights[1]:.4f}]")
print(f"Bias: {model.bias:.4f}")

مرحله‌ی 3: ماتریس درهم‌ریختگی و معیارها از صفر

class ClassificationMetrics:
    def __init__(self, y_true, y_pred):
        self.tp = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 1)
        self.tn = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 0)
        self.fp = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 1)
        self.fn = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 0)

    def accuracy(self):
        total = self.tp + self.tn + self.fp + self.fn
        return (self.tp + self.tn) / total if total > 0 else 0

    def precision(self):
        denom = self.tp + self.fp
        return self.tp / denom if denom > 0 else 0

    def recall(self):
        denom = self.tp + self.fn
        return self.tp / denom if denom > 0 else 0

    def f1(self):
        p = self.precision()
        r = self.recall()
        return 2 * p * r / (p + r) if (p + r) > 0 else 0

    def print_confusion_matrix(self):
        print(f"\n  Confusion Matrix:")
        print(f"                  Predicted")
        print(f"                  Pos   Neg")
        print(f"  Actual Pos     {self.tp:4d}  {self.fn:4d}")
        print(f"  Actual Neg     {self.fp:4d}  {self.tn:4d}")

    def print_report(self):
        self.print_confusion_matrix()
        print(f"\n  Accuracy:  {self.accuracy():.4f}")
        print(f"  Precision: {self.precision():.4f}")
        print(f"  Recall:    {self.recall():.4f}")
        print(f"  F1 Score:  {self.f1():.4f}")


y_pred_test = [model.predict(x) for x in X_test]
print("\n=== Classification Report (Test Set) ===")
metrics = ClassificationMetrics(y_test, y_pred_test)
metrics.print_report()

مرحله‌ی 4: تحلیل مرز تصمیم‌گیری

print("\n=== Decision Boundary ===")
w1, w2 = model.weights
b = model.bias
print(f"Decision boundary: {w1:.4f}*x1 + {w2:.4f}*x2 + {b:.4f} = 0")
if abs(w2) > 1e-10:
    print(f"Solved for x2:     x2 = {-w1/w2:.4f}*x1 + {-b/w2:.4f}")

print("\nSample predictions near the boundary:")
test_points = [
    [3.0, 3.0],
    [3.5, 3.5],
    [4.0, 4.0],
    [2.5, 2.5],
    [5.0, 5.0],
]
for point in test_points:
    prob = model.predict_proba(point)
    pred = model.predict(point)
    print(f"  [{point[0]}, {point[1]}] -> prob={prob:.4f}, class={pred}")

مرحله‌ی 5: دسته‌بندی چندکلاسه با سافت‌مکس

class SoftmaxRegression:
    def __init__(self, n_features, n_classes, learning_rate=0.01):
        self.n_features = n_features
        self.n_classes = n_classes
        self.lr = learning_rate
        self.weights = [[0.0] * n_features for _ in range(n_classes)]
        self.biases = [0.0] * n_classes

    def softmax(self, scores):
        max_score = max(scores)
        exp_scores = [math.exp(s - max_score) for s in scores]
        total = sum(exp_scores)
        return [e / total for e in exp_scores]

    def predict_proba(self, x):
        scores = [
            sum(self.weights[k][j] * x[j] for j in range(self.n_features)) + self.biases[k]
            for k in range(self.n_classes)
        ]
        return self.softmax(scores)

    def predict(self, x):
        probs = self.predict_proba(x)
        return probs.index(max(probs))

    def fit(self, X, y, epochs=1000, print_every=200):
        n = len(y)
        for epoch in range(epochs):
            grad_w = [[0.0] * self.n_features for _ in range(self.n_classes)]
            grad_b = [0.0] * self.n_classes
            total_loss = 0.0
            for i in range(n):
                probs = self.predict_proba(X[i])
                for k in range(self.n_classes):
                    target = 1.0 if y[i] == k else 0.0
                    error = probs[k] - target
                    for j in range(self.n_features):
                        grad_w[k][j] += error * X[i][j]
                    grad_b[k] += error
                true_prob = max(probs[y[i]], 1e-15)
                total_loss -= math.log(true_prob)
            for k in range(self.n_classes):
                for j in range(self.n_features):
                    self.weights[k][j] -= self.lr * (grad_w[k][j] / n)
                self.biases[k] -= self.lr * (grad_b[k] / n)
            if epoch % print_every == 0:
                print(f"  Epoch {epoch:4d} | Loss: {total_loss / n:.4f}")
        return self

    def accuracy(self, X, y):
        correct = sum(1 for i in range(len(y)) if self.predict(X[i]) == y[i])
        return correct / len(y)


random.seed(42)
X_3class = []
y_3class = []

centers = [(1, 1), (5, 1), (3, 5)]
for label, (cx, cy) in enumerate(centers):
    for _ in range(50):
        X_3class.append([random.gauss(cx, 0.8), random.gauss(cy, 0.8)])
        y_3class.append(label)

combined = list(zip(X_3class, y_3class))
random.shuffle(combined)
X_3class, y_3class = zip(*combined)
X_3class = list(X_3class)
y_3class = list(y_3class)

split_3 = int(0.8 * len(X_3class))
X_train_3 = X_3class[:split_3]
y_train_3 = y_3class[:split_3]
X_test_3 = X_3class[split_3:]
y_test_3 = y_3class[split_3:]

print("\n=== Multi-class Softmax Regression (3 classes) ===")
softmax_model = SoftmaxRegression(n_features=2, n_classes=3, learning_rate=0.1)
softmax_model.fit(X_train_3, y_train_3, epochs=1000, print_every=200)
print(f"\nTrain accuracy: {softmax_model.accuracy(X_train_3, y_train_3):.4f}")
print(f"Test accuracy:  {softmax_model.accuracy(X_test_3, y_test_3):.4f}")

print("\nSample predictions:")
for i in range(5):
    probs = softmax_model.predict_proba(X_test_3[i])
    pred = softmax_model.predict(X_test_3[i])
    print(f"  True: {y_test_3[i]}, Predicted: {pred}, Probs: [{', '.join(f'{p:.3f}' for p in probs)}]")

مرحله‌ی 6: تنظیم آستانه

print("\n=== Threshold Tuning ===")
print("Default threshold: 0.5. Adjusting the threshold trades precision for recall.\n")

thresholds = [0.3, 0.4, 0.5, 0.6, 0.7]
print(f"{'Threshold':>10} {'Accuracy':>10} {'Precision':>10} {'Recall':>10} {'F1':>10}")
print("-" * 52)

for t in thresholds:
    y_pred_t = [1 if model.predict_proba(x) >= t else 0 for x in X_test]
    m = ClassificationMetrics(y_test, y_pred_t)
    print(f"{t:>10.1f} {m.accuracy():>10.4f} {m.precision():>10.4f} {m.recall():>10.4f} {m.f1():>10.4f}")

استفاده در عمل

حالا همان مراحل را با scikit-learn پیاده می‌کنیم.

from sklearn.linear_model import LogisticRegression as SklearnLR
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import numpy as np

np.random.seed(42)
X_0 = np.random.randn(100, 2) + [2, 2]
X_1 = np.random.randn(100, 2) + [5, 5]
X_sk = np.vstack([X_0, X_1])
y_sk = np.array([0] * 100 + [1] * 100)

X_tr, X_te, y_tr, y_te = train_test_split(X_sk, y_sk, test_size=0.2, random_state=42)

scaler = StandardScaler()
X_tr_sc = scaler.fit_transform(X_tr)
X_te_sc = scaler.transform(X_te)

lr = SklearnLR()
lr.fit(X_tr_sc, y_tr)
y_pred = lr.predict(X_te_sc)

print("=== Scikit-learn Logistic Regression ===")
print(f"Accuracy:  {accuracy_score(y_te, y_pred):.4f}")
print(f"Precision: {precision_score(y_te, y_pred):.4f}")
print(f"Recall:    {recall_score(y_te, y_pred):.4f}")
print(f"F1:        {f1_score(y_te, y_pred):.4f}")
print(f"\nConfusion Matrix:\n{confusion_matrix(y_te, y_pred)}")
print(f"\nClassification Report:\n{classification_report(y_te, y_pred)}")

پیاده‌سازی از صفر شما دقیقاً همان مرز تصمیم‌گیری و معیارها را تولید می‌کند. Scikit-learn امکاناتی مثل گزینه‌های حل‌کننده (liblinear، lbfgs، saga)، منظم‌سازی خودکار، استراتژی‌های چندکلاسه (one-vs-rest، multinomial) و بهینه‌سازی‌های پایداری عددی (numerical stability) را به آن می‌افزاید.

تحویل پروژه

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

  • code/logistic_regression.py - رگرسیون لجستیک از صفر به همراه معیارهای ارزیابی

تمرین‌ها

  1. مجموعه‌داده‌ای تولید کنید که تفکیک‌پذیر خطی نباشد (مثل دو دایره‌ی هم‌مرکز). رگرسیون لجستیک را آموزش دهید و شکست آن را مشاهده کنید. سپس ویژگی‌های چندجمله‌ای (x12,x22,x1x2x_1^2, x_2^2, x_1 x_2) اضافه کرده و دوباره مدل را آموزش دهید. نشان دهید که صحت بهبود می‌یابد.
  2. برای مدل سافت‌مکس 3 کلاسه، ماتریس درهم‌ریختگی چندکلاسه پیاده‌سازی کنید. دقت و بازخوانی را برای هر کلاس جداگانه به دست آورید. دسته‌بندی کدام کلاس سخت‌تر است؟
  3. یک منحنی ROC را از صفر بسازید. برای 100 مقدار آستانه از 0 تا 1، نرخ مثبت واقعی و نرخ مثبت کاذب را محاسبه کنید. سپس سطح زیر منحنی (AUC) را با استفاده از قاعده‌ی ذوزنقه‌ای حساب کنید.

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

اصطلاح اصطلاح رایج معنای دقیق فنی
رگرسیون لجستیک (Logistic regression) «رگرسیون برای دسته‌بندی» یک مدل خطی و در ادامه‌ی آن یک تابع سیگموئید که احتمال‌های کلاس‌ها را در خروجی می‌دهد
تابع سیگموئید (Sigmoid function) «منحنی S» تابع 1/(1+e−z)1/(1+e^{-z}) که هر عدد حقیقی را به بازه‌ی (0, 1) می‌برد
آنتروپی متقاطع دودویی (Binary cross-entropy) «زیان لگاریتمی» تابع زیان −[ylog⁡(p)+(1−y)log⁡(1−p)]-[y \log(p) + (1-y) \log(1-p)] که پیش‌بینی‌های غلط و با اطمینان بالا را به‌شدت جریمه می‌کند
مرز تصمیم‌گیری (Decision boundary) «خط جداکننده» رویه‌ای که در آن احتمال خروجی مدل برابر 0.5 است و کلاس‌های پیش‌بینی‌شده را از هم جدا می‌کند
سافت‌مکس (Softmax) «سیگموئید چندکلاسه» تابعی که برداری از امتیازها را به احتمال‌هایی تبدیل می‌کند که مجموعشان 1 می‌شود
دقت (Precision) «چه تعداد از برگزیده‌ها درستند» TP/(TP+FP)TP / (TP + FP)؛ کسری از پیش‌بینی‌های مثبت که واقعاً مثبت هستند
بازخوانی (Recall) «چه تعداد از درست‌ها انتخاب شدند» TP/(TP+FN)TP / (TP + FN)؛ کسری از مثبت‌های واقعی که مدل به‌درستی شناسایی کرده است
امتیاز F1 (F1 score) «صحت متوازن» میانگین همساز دقت و بازخوانی: 2PR/(P+R)2PR / (P+R)
ماتریس درهم‌ریختگی (Confusion matrix) «تفکیک خطاها» جدولی که تعداد TP، TN، FP و FN را برای هر جفت کلاس نشان می‌دهد
آستانه (Threshold) «نقطه‌ی برش» مقدار احتمالی که اگر خروجی بیشتر از آن باشد، مدل کلاس 1 را پیش‌بینی می‌کند (پیش‌فرض 0.5، قابل تنظیم)
کدگذاری وان‌هات (One-hot encoding) «ستون‌های دودویی برای دسته‌ها» نمایش کلاس kk به‌صورت برداری از صفرها با مقدار 1 در جایگاه kk
آنتروپی متقاطع دسته‌ای (Categorical cross-entropy) «زیان لگاریتمی چندکلاسه» تعمیم آنتروپی متقاطع دودویی به kk کلاس با استفاده از برچسب‌های کدگذاری‌شده به روش وان‌هات

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

چرا با وجود کلمه‌ی رگرسیون، این الگوریتم برای دسته‌بندی به کار می‌رود؟

نام رگرسیون لجستیک از تابع ریاضی لجستیک (سیگموئید) گرفته شده است، اما خروجی نهایی آن یک احتمال بین 0 و 1 است که با تعیین آستانه به برچسب‌های دسته‌ای تبدیل می‌شود.

چرا در رگرسیون لجستیک از تابع زیان MSE استفاده نمی‌شود؟

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

تفاوت دقت (Precision) و بازخوانی (Recall) در کجاست؟

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

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

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