فاز 2 · درس 9

ارزیابی مدل در یادگیری ماشین: از صفر تا پیاده‌سازی و تحلیل

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

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

در این درس می‌آموزید که چطور استراتژی‌های تفکیک داده (آموزش، اعتبارسنجی و آزمون) و روش‌های اعتبارسنجی متقاطع مانند K-fold و Stratified K-fold را از پایه پیاده کنید. همچنین با درک عمیق ماتریس درهم‌ریختگی، معیارهای دسته‌بندی و رگرسیون، و تحلیل منحنی‌های یادگیری، نقاط ضعف مدل شامل کم‌برازش، بیش‌برازش و خطای مهلک نشت داده را شناسایی و برطرف خواهید کرد.

ارزش یک مدل فقط به اندازه‌ی شیوه‌ی ارزیابی آن است.

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

  • پیاده‌سازی اعتبارسنجی متقاطع K-fold و K-fold لایه‌بندی‌شده (stratified) از صفر و توضیح دلیل اهمیت لایه‌بندی برای داده‌های نامتوازن
  • محاسبه‌ی معیارهای دقت (precision)، بازخوانی (recall)، امتیاز F1، AUC-ROC و معیارهای رگرسیون (MSE، RMSE، MAE و ضریب تعیین یا R-squared) از صفر
  • تحلیل منحنی‌های یادگیری برای تشخیص بایاس بالا (high bias) یا واریانس بالا (high variance) در مدل
  • شناسایی خطاهای رایج ارزیابی، از جمله نشت داده (data leakage)، انتخاب معیار نامناسب و آلودگی مجموعه‌ی آزمون

صورت مسئله

مدلی آموزش داده‌اید که روی داده‌هایتان به صحت (accuracy) 95% می‌رسد. آیا مدل خوبی است؟

شاید بله، شاید هم نه. اگر 95% داده‌هایتان متعلق به یک کلاس باشند، مدلی که همیشه همان کلاس را پیش‌بینی کند به صحت 95% می‌رسد، در حالی که کاملاً بی‌استفاده است. اگر مدل را روی همان داده‌هایی ارزیابی کرده باشید که با آن‌ها آموزش دیده، عدد 95% بی‌معنی است؛ چون مدل فقط پاسخ‌ها را حفظ کرده است. اگر مجموعه‌داده‌تان مؤلفه‌ی زمانی داشته باشد و داده‌ها را پیش از تفکیک به‌طور تصادفی بُر زده باشید، شاید مدل دارد از داده‌های آینده برای پیش‌بینی گذشته استفاده می‌کند.

بیشتر پروژه‌های یادگیری ماشین دقیقاً در مرحله‌ی ارزیابی به بیراهه می‌روند. معیار اشتباه مدلی ضعیف را عالی نشان می‌دهد. تفکیک نادرست به مدل امکان تقلب می‌دهد. مقایسه‌ی نادرست هم باعث می‌شود مدل بدتر را انتخاب کنید. درست ارزیابی کردن اختیاری نیست؛ دقیقاً تفاوت بین مدلی است که در محیط عملیاتی واقعاً کار می‌کند و مدلی که به محض دیدن داده‌های واقعی زمین می‌خورد.

مفاهیم پایه

مجموعه‌های آموزش، اعتبارسنجی و آزمون

مجموعه داده کاملمجموعه آموزش 60-70%مجموعه اعتبارسنجی 15-20%مجموعه آزمون 15-20%برازش مدلتنظیم هایپرپارامترهامدل نهاییگزارش عملکرد

سه تفکیک، سه هدف:

  • مجموعه‌ی آموزش (Training set): مدل از این داده‌ها یاد می‌گیرد و این نمونه‌ها را در طول فرایند آموزش می‌بیند.
  • مجموعه‌ی اعتبارسنجی (Validation set): برای تنظیم ابرپارامترها و انتخاب بین مدل‌های مختلف به کار می‌رود. مدل هرگز روی این داده‌ها آموزش نمی‌بیند، اما تصمیم‌های شما تحت تأثیر آن است.
  • مجموعه‌ی آزمون (Test set): دقیقاً یک بار و در پایان کار ارزیابی می‌شود تا عملکرد نهایی را گزارش کند. اگر به عملکرد مدل روی مجموعه‌ی آزمون نگاه کنید و سپس برگردید و مدل را تغییر دهید، آن مجموعه دیگر مجموعه‌ی آزمون نیست؛ بلکه به یک مجموعه‌ی اعتبارسنجی دوم تبدیل شده است.

مجموعه‌ی آزمون تضمین نگه‌داشته‌ی شماست تا عملکرد گزارش‌شده، بازتاب‌دهنده‌ی رفتار واقعی مدل روی داده‌های واقعاً دیده‌نشده باشد.

اعتبارسنجی متقاطع K-Fold

در مجموعه‌داده‌های کوچک، یک تفکیک ساده به آموزش و اعتبارسنجی، داده‌ها را هدر می‌دهد و برآوردهای پر از خطایی تولید می‌کند. اعتبارسنجی متقاطع K-fold از تمام داده‌ها هم برای آموزش و هم برای اعتبارسنجی بهره می‌برد:

فولد 5فولد 4فولد 3فولد 2فولد 1اعتبارسنجیآموزشآموزشآموزشآموزشآموزشاعتبارسنجیآموزشآموزشآموزشآموزشآموزشاعتبارسنجیآموزشآموزشآموزشآموزشآموزشاعتبارسنجیآموزشآموزشآموزشآموزشآموزشاعتبارسنجیمیانگین امتیازها
  1. داده‌ها را به KK بخش (fold) هم‌اندازه تقسیم کنید.
  2. برای هر بخش، مدل را روی K−1K-1 بخش آموزش دهید و روی بخش باقی‌مانده ارزیابی کنید.
  3. میانگین امتیازهای اعتبارسنجی KK بخش را بگیرید.

انتخاب‌های استاندارد معمولاً K=5K=5 یا K=10K=10 هستند. هر نقطه‌ی داده دقیقاً یک بار برای اعتبارسنجی استفاده می‌شود. امتیاز میانگین، برآورد پایدارتری نسبت به هر تک‌تفکیک به دست می‌دهد.

K-fold لایه‌بندی‌شده (Stratified K-fold): توزیع کلاس‌ها را در هر بخش حفظ می‌کند. اگر مجموعه‌داده‌تان شامل 70% کلاس A و 30% کلاس B باشد، هر بخش تقریباً همین نسبت را حفظ خواهد کرد. این روش برای مجموعه‌داده‌های نامتوازن حیاتی است؛ زیرا تفکیک تصادفی ممکن است تمام نمونه‌های کلاس اقلیت را در یک بخش بیندازد.

معیارهای دسته‌بندی

ماتریس درهم‌ریختگی (Confusion matrix): پایه‌ی همه‌چیز است. برای دسته‌بندی دودویی:

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

تمام معیارهای دیگر از همین ماتریس به دست می‌آیند:

  • صحت (Accuracy) = (TP+TN)/(TP+TN+FP+FN)(TP + TN) / (TP + TN + FP + FN): نسبت پیش‌بینی‌های درست. در شرایط نامتوازن بودن کلاس‌ها گمراه‌کننده است.
  • دقت (Precision) = TP/(TP+FP)TP / (TP + FP): از بین تمام نمونه‌هایی که مثبت پیش‌بینی شده‌اند، چند نمونه واقعاً مثبت بوده‌اند؟ وقتی مثبت‌های کاذب پرهزینه‌اند از این معیار استفاده کنید (مثلاً فیلتر هرزنامه‌ای که ایمیل واقعی را اشتباهاً هرزنامه علامت می‌زند).
  • بازخوانی (Recall یا sensitivity) = TP/(TP+FN)TP / (TP + FN): از بین تمام مثبت‌های واقعی، چند مورد را شناسایی کردیم؟ وقتی منفی‌های کاذب پرهزینه‌اند به کار می‌آید (مثل غربالگری سرطان که ندیدن تومور در آن خطرساز است).
  • امتیاز F1 (F1 score) = 2⋅precision⋅recall/(precision+recall)2 \cdot \text{precision} \cdot \text{recall} / (\text{precision} + \text{recall}): میانگین همساز (harmonic mean) دقت و بازخوانی. وقتی هیچ‌کدام از این دو برتری مطلقی ندارند، بین آن‌ها تعادل برقرار می‌کند.
  • AUC-ROC: سطح زیر منحنی مشخصه‌ی عملکرد سیستم (Area Under the Receiver Operating Characteristic curve). این منحنی نرخ مثبت واقعی را در برابر نرخ مثبت کاذب در آستانه‌های مختلف دسته‌بندی رسم می‌کند. مقدار AUC = 0.5 به معنی حدس تصادفی و AUC = 1.0 نشان‌دهنده‌ی تفکیک بی‌نقص است. این معیار مستقل از آستانه است و نشان می‌دهد مدل چقدر در رتبه دادن به نمونه‌های مثبت بالاتر از نمونه‌های منفی موفق است؛ فارغ از این‌که چه حد آستانه‌ای انتخاب کنید.

معیارهای رگرسیون

  • MSE (میانگین مربعات خطا) = mean((ytrue−ypred)2)\text{mean}((y_{\text{true}} - y_{\text{pred}})^2): خطاهای بزرگ را به شکل درجه‌دو جریمه می‌کند و به داده‌های پرت (outliers) حساس است.
  • RMSE (ریشه‌ی میانگین مربعات خطا) = MSE\sqrt{\text{MSE}}: واحدی همسان با متغیر هدف دارد و تفسیرش ساده‌تر از MSE است.
  • MAE (میانگین قدر مطلق خطا) = mean(∣ytrue−ypred∣)\text{mean}(\lvert y_{\text{true}} - y_{\text{pred}} \rvert): با تمام خطاها رفتار خطی دارد و در برابر داده‌های پرت مقاوم‌تر از MSE است.
  • ضریب تعیین (R-squared) = 1−SSres/SStot1 - SS_{\text{res}} / SS_{\text{tot}}، که در آن SSres=∑(ytrue−ypred)2SS_{\text{res}} = \sum (y_{\text{true}} - y_{\text{pred}})^2 و SStot=∑(ytrue−ymean)2SS_{\text{tot}} = \sum (y_{\text{true}} - y_{\text{mean}})^2 است: کسری از واریانس که توسط مدل توجیه می‌شود. مقدار R2=1.0R^2 = 1.0 نشانه‌ی عملکرد بی‌نقص است. R2=0.0R^2 = 0.0 یعنی مدل هیچ مزیتی نسبت به پیش‌بینی همیشگی میانگین ندارد. اگر عملکرد مدل از میانگین هم ضعیف‌تر باشد، مقدار R2R^2 می‌تواند منفی شود.

منحنی‌های یادگیری

رسم امتیازهای آموزش و اعتبارسنجی بر حسب اندازه‌ی مجموعه‌ی آموزش:

  • بایاس بالا یا کم‌برازش (High bias / underfitting): هر دو منحنی روی امتیازی پایین همگرا می‌شوند. اضافه کردن داده‌ی بیشتر کمکی نمی‌کند؛ به مدل پیچیده‌تری نیاز دارید.
  • واریانس بالا یا بیش‌برازش (High variance / overfitting): امتیاز آموزش بالاست اما امتیاز اعتبارسنجی فاصله‌ی زیادی با آن دارد و بسیار پایین‌تر است. افزودن داده‌ی بیشتر معمولاً کمک می‌کند.

منحنی‌های اعتبارسنجی

رسم امتیازهای آموزش و اعتبارسنجی بر حسب تغییرات یک ابرپارامتر:

  • در پیچیدگی کم: هر دو امتیاز پایین هستند (کم‌برازش)
  • در پیچیدگی مناسب: هر دو امتیاز بالا و نزدیک به هم هستند
  • در پیچیدگی زیاد: امتیاز آموزش بالا می‌ماند ولی امتیاز اعتبارسنجی افت می‌کند (بیش‌برازش)

مقدار بهینه‌ی ابرپارامتر جایی است که امتیاز اعتبارسنجی در آن اوج می‌گیرد.

اشتباهات رایج در ارزیابی

نشت داده (Data leakage): وقتی اطلاعاتی از مجموعه‌ی آزمون به فرایند آموزش رخنه می‌کند. نمونه‌ها: فیت کردن مقیاس‌بند (scaler) روی کل داده‌ها قبل از تفکیک، استفاده از داده‌های آینده در پیش‌بینی سری‌های زمانی، یا استفاده از ویژگی‌هایی که از خودِ متغیر هدف مشتق شده‌اند. همیشه اول داده‌ها را تفکیک کنید، سپس پیش‌پردازش را انجام دهید.

نامتوازنی کلاس‌ها (Class imbalance): اگر 99% تراکنش‌ها عادی و 1% کلاهبرداری باشند، مدلی که همیشه گزینه‌ی «عادی» را پیش‌بینی کند به صحت 99% می‌رسد. در چنین حالتی از دقت، بازخوانی، F1 یا AUC-ROC استفاده کنید.

معیار اشتباه (Wrong metric): بهینه‌سازی صحت به جای بازخوانی (در تشخیص‌های پزشکی)، یا بهینه‌سازی RMSE وقتی داده‌ها نویز شدید و مقادیر پرت سنگین دارند (که MAE انتخاب بهتری است).

استفاده نکردن از تفکیک لایه‌بندی‌شده (Not using stratified splits): در داده‌های نامتوازن، تفکیک تصادفی ممکن است تعداد بسیار کمی از نمونه‌های کلاس اقلیت را در بخش اعتبارسنجی بگذارد و برآوردهایی ناپایدار تولید کند.

ارزیابی مکرر روی مجموعه‌ی آزمون (Testing too often): هر بار که عملکرد آزمون را چک می‌کنید و مدل را دستکاری می‌کنید، مدل را به مجموعه‌ی آزمون بیش‌برازش می‌کنید. مجموعه‌ی آزمون مصرفی یک‌باره دارد.

دقت / بازخوانیآستانه را بکشید
PRECISION / RECALLGrey is the negative class, blue the positive; the orange line is the threshold. Move it right and you predict positive less often: precision rises, recall falls. F1 is their harmonic mean, highest where the two curves cross.
خاکستری کلاس منفی و آبی کلاس مثبت است؛ خط نارنجی نشان‌دهندهٔ آستانه است. حرکت دادن آن به سمت راست باعث می‌شود مثبت‌ها را کمتر پیش‌بینی کنید: دقت (precision) افزایش و بازخوانی (recall) کاهش می‌یابد. معیار F1 میانگین هارمونیک آن‌هاست و در محل تقاطع دو منحنی به بیشترین مقدار خود می‌رسد.

پیاده‌سازی

گام 1: تفکیک به آموزش، اعتبارسنجی و آزمون

import random
import math


def train_val_test_split(X, y, train_ratio=0.6, val_ratio=0.2, seed=42):
    random.seed(seed)
    n = len(X)
    indices = list(range(n))
    random.shuffle(indices)

    train_end = int(n * train_ratio)
    val_end = int(n * (train_ratio + val_ratio))

    train_idx = indices[:train_end]
    val_idx = indices[train_end:val_end]
    test_idx = indices[val_end:]

    X_train = [X[i] for i in train_idx]
    y_train = [y[i] for i in train_idx]
    X_val = [X[i] for i in val_idx]
    y_val = [y[i] for i in val_idx]
    X_test = [X[i] for i in test_idx]
    y_test = [y[i] for i in test_idx]

    return X_train, y_train, X_val, y_val, X_test, y_test

گام 2: اعتبارسنجی متقاطع K-fold و Stratified K-fold

def kfold_split(n, k=5, seed=42):
    random.seed(seed)
    indices = list(range(n))
    random.shuffle(indices)

    fold_size = n // k
    folds = []

    for i in range(k):
        start = i * fold_size
        end = start + fold_size if i < k - 1 else n
        val_idx = indices[start:end]
        train_idx = indices[:start] + indices[end:]
        folds.append((train_idx, val_idx))

    return folds


def stratified_kfold_split(y, k=5, seed=42):
    random.seed(seed)

    class_indices = {}
    for i, label in enumerate(y):
        class_indices.setdefault(label, []).append(i)

    for label in class_indices:
        random.shuffle(class_indices[label])

    folds = [{"train": [], "val": []} for _ in range(k)]

    for label, indices in class_indices.items():
        fold_size = len(indices) // k
        for i in range(k):
            start = i * fold_size
            end = start + fold_size if i < k - 1 else len(indices)
            val_part = indices[start:end]
            train_part = indices[:start] + indices[end:]
            folds[i]["val"].extend(val_part)
            folds[i]["train"].extend(train_part)

    return [(f["train"], f["val"]) for f in folds]


def cross_validate(X, y, model_fn, k=5, metric_fn=None, stratified=False):
    n = len(X)

    if stratified:
        folds = stratified_kfold_split(y, k)
    else:
        folds = kfold_split(n, k)

    scores = []
    for train_idx, val_idx in folds:
        X_train = [X[i] for i in train_idx]
        y_train = [y[i] for i in train_idx]
        X_val = [X[i] for i in val_idx]
        y_val = [y[i] for i in val_idx]

        model = model_fn()
        model.fit(X_train, y_train)
        predictions = [model.predict(x) for x in X_val]

        if metric_fn:
            score = metric_fn(y_val, predictions)
        else:
            score = sum(1 for yt, yp in zip(y_val, predictions) if yt == yp) / len(y_val)
        scores.append(score)

    return scores

گام 3: ماتریس درهم‌ریختگی و معیارهای دسته‌بندی

def confusion_matrix(y_true, y_pred):
    tp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 1)
    tn = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 0)
    fp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 1)
    fn = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 0)
    return tp, tn, fp, fn


def accuracy(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    total = tp + tn + fp + fn
    return (tp + tn) / total if total > 0 else 0.0


def precision(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    return tp / (tp + fp) if (tp + fp) > 0 else 0.0


def recall(y_true, y_pred):
    tp, tn, fp, fn = confusion_matrix(y_true, y_pred)
    return tp / (tp + fn) if (tp + fn) > 0 else 0.0


def f1_score(y_true, y_pred):
    p = precision(y_true, y_pred)
    r = recall(y_true, y_pred)
    return 2 * p * r / (p + r) if (p + r) > 0 else 0.0


def roc_curve(y_true, y_scores):
    thresholds = sorted(set(y_scores), reverse=True)
    tpr_list = []
    fpr_list = []

    total_positives = sum(y_true)
    total_negatives = len(y_true) - total_positives

    for threshold in thresholds:
        y_pred = [1 if s >= threshold else 0 for s in y_scores]
        tp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 1 and yp == 1)
        fp = sum(1 for yt, yp in zip(y_true, y_pred) if yt == 0 and yp == 1)

        tpr = tp / total_positives if total_positives > 0 else 0.0
        fpr = fp / total_negatives if total_negatives > 0 else 0.0

        tpr_list.append(tpr)
        fpr_list.append(fpr)

    return fpr_list, tpr_list, thresholds


def auc_roc(y_true, y_scores):
    fpr_list, tpr_list, _ = roc_curve(y_true, y_scores)

    pairs = sorted(zip(fpr_list, tpr_list))
    fpr_sorted = [p[0] for p in pairs]
    tpr_sorted = [p[1] for p in pairs]

    area = 0.0
    for i in range(1, len(fpr_sorted)):
        width = fpr_sorted[i] - fpr_sorted[i - 1]
        height = (tpr_sorted[i] + tpr_sorted[i - 1]) / 2
        area += width * height

    return area

گام 4: معیارهای رگرسیون

def mse(y_true, y_pred):
    n = len(y_true)
    return sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred)) / n


def rmse(y_true, y_pred):
    return math.sqrt(mse(y_true, y_pred))


def mae(y_true, y_pred):
    n = len(y_true)
    return sum(abs(yt - yp) for yt, yp in zip(y_true, y_pred)) / n


def r_squared(y_true, y_pred):
    mean_y = sum(y_true) / len(y_true)
    ss_res = sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred))
    ss_tot = sum((yt - mean_y) ** 2 for yt in y_true)
    if ss_tot == 0:
        return 0.0
    return 1.0 - ss_res / ss_tot

گام 5: منحنی‌های یادگیری

def learning_curve(X, y, model_fn, metric_fn, train_sizes=None, val_ratio=0.2, seed=42):
    random.seed(seed)
    n = len(X)
    indices = list(range(n))
    random.shuffle(indices)

    val_size = int(n * val_ratio)
    val_idx = indices[:val_size]
    pool_idx = indices[val_size:]

    X_val = [X[i] for i in val_idx]
    y_val = [y[i] for i in val_idx]

    if train_sizes is None:
        train_sizes = [int(len(pool_idx) * r) for r in [0.1, 0.2, 0.4, 0.6, 0.8, 1.0]]

    train_scores = []
    val_scores = []

    for size in train_sizes:
        subset = pool_idx[:size]
        X_train = [X[i] for i in subset]
        y_train = [y[i] for i in subset]

        model = model_fn()
        model.fit(X_train, y_train)

        train_pred = [model.predict(x) for x in X_train]
        val_pred = [model.predict(x) for x in X_val]

        train_scores.append(metric_fn(y_train, train_pred))
        val_scores.append(metric_fn(y_val, val_pred))

    return train_sizes, train_scores, val_scores

گام 6: یک دسته‌بند ساده برای آزمایش به همراه دموی کامل

class SimpleLogistic:
    def __init__(self, lr=0.1, epochs=100):
        self.lr = lr
        self.epochs = epochs
        self.weights = None
        self.bias = 0.0

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

    def fit(self, X, y):
        n_features = len(X[0])
        self.weights = [0.0] * n_features
        self.bias = 0.0

        for _ in range(self.epochs):
            for xi, yi in zip(X, y):
                z = sum(w * x for w, x in zip(self.weights, xi)) + self.bias
                pred = self.sigmoid(z)
                error = yi - pred
                for j in range(n_features):
                    self.weights[j] += self.lr * error * xi[j]
                self.bias += self.lr * error

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

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


class SimpleLinearRegression:
    def __init__(self, lr=0.001, epochs=200):
        self.lr = lr
        self.epochs = epochs
        self.weights = None
        self.bias = 0.0

    def fit(self, X, y):
        n_features = len(X[0])
        self.weights = [0.0] * n_features
        self.bias = 0.0
        n = len(X)

        for _ in range(self.epochs):
            for xi, yi in zip(X, y):
                pred = sum(w * x for w, x in zip(self.weights, xi)) + self.bias
                error = yi - pred
                for j in range(n_features):
                    self.weights[j] += self.lr * error * xi[j] / n
                self.bias += self.lr * error / n

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


def standardize(values):
    n = len(values)
    mean = sum(values) / n
    var = sum((v - mean) ** 2 for v in values) / n
    std = math.sqrt(var) if var > 0 else 1.0
    return [(v - mean) / std for v in values], mean, std


def make_classification_data(n=300, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        x1 = random.gauss(0, 1)
        x2 = random.gauss(0, 1)
        label = 1 if (x1 + x2 + random.gauss(0, 0.5)) > 0 else 0
        X.append([x1, x2])
        y.append(label)
    return X, y


def make_regression_data(n=200, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        x1 = random.uniform(0, 10)
        x2 = random.uniform(0, 5)
        target = 3 * x1 + 2 * x2 + random.gauss(0, 2)
        X.append([x1, x2])
        y.append(target)
    return X, y


def make_imbalanced_data(n=300, minority_ratio=0.05, seed=42):
    random.seed(seed)
    X = []
    y = []
    for _ in range(n):
        if random.random() < minority_ratio:
            x1 = random.gauss(3, 0.5)
            x2 = random.gauss(3, 0.5)
            label = 1
        else:
            x1 = random.gauss(0, 1)
            x2 = random.gauss(0, 1)
            label = 0
        X.append([x1, x2])
        y.append(label)
    return X, y


if __name__ == "__main__":
    X_clf, y_clf = make_classification_data(300)

    print("=== Train/Validation/Test Split ===")
    X_train, y_train, X_val, y_val, X_test, y_test = train_val_test_split(X_clf, y_clf)
    print(f"  Train: {len(X_train)}, Val: {len(X_val)}, Test: {len(X_test)}")
    print(f"  Train class distribution: {sum(y_train)}/{len(y_train)} positive")
    print(f"  Val class distribution: {sum(y_val)}/{len(y_val)} positive")

    model = SimpleLogistic(lr=0.1, epochs=200)
    model.fit(X_train, y_train)

    print("\n=== Classification Metrics ===")
    y_pred = [model.predict(x) for x in X_test]
    tp, tn, fp, fn = confusion_matrix(y_test, y_pred)
    print(f"  Confusion matrix: TP={tp}, TN={tn}, FP={fp}, FN={fn}")
    print(f"  Accuracy:  {accuracy(y_test, y_pred):.4f}")
    print(f"  Precision: {precision(y_test, y_pred):.4f}")
    print(f"  Recall:    {recall(y_test, y_pred):.4f}")
    print(f"  F1 Score:  {f1_score(y_test, y_pred):.4f}")

    y_scores = [model.predict_proba(x) for x in X_test]
    auc = auc_roc(y_test, y_scores)
    print(f"  AUC-ROC:   {auc:.4f}")

    print("\n=== K-Fold Cross-Validation (K=5) ===")
    cv_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        k=5,
        metric_fn=accuracy,
    )
    mean_cv = sum(cv_scores) / len(cv_scores)
    std_cv = math.sqrt(sum((s - mean_cv) ** 2 for s in cv_scores) / len(cv_scores))
    print(f"  Fold scores: {[round(s, 4) for s in cv_scores]}")
    print(f"  Mean: {mean_cv:.4f} (+/- {std_cv:.4f})")

    print("\n=== Stratified K-Fold Cross-Validation (K=5) ===")
    strat_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        k=5,
        metric_fn=accuracy,
        stratified=True,
    )
    strat_mean = sum(strat_scores) / len(strat_scores)
    strat_std = math.sqrt(sum((s - strat_mean) ** 2 for s in strat_scores) / len(strat_scores))
    print(f"  Fold scores: {[round(s, 4) for s in strat_scores]}")
    print(f"  Mean: {strat_mean:.4f} (+/- {strat_std:.4f})")

    print("\n=== Imbalanced Data: Why Accuracy Lies ===")
    X_imb, y_imb = make_imbalanced_data(300, minority_ratio=0.05)
    positives = sum(y_imb)
    print(f"  Class distribution: {positives} positive, {len(y_imb) - positives} negative ({positives/len(y_imb)*100:.1f}% positive)")

    always_negative = [0] * len(y_imb)
    print(f"  Always-negative baseline:")
    print(f"    Accuracy:  {accuracy(y_imb, always_negative):.4f}")
    print(f"    Precision: {precision(y_imb, always_negative):.4f}")
    print(f"    Recall:    {recall(y_imb, always_negative):.4f}")
    print(f"    F1 Score:  {f1_score(y_imb, always_negative):.4f}")

    X_tr_i, y_tr_i, X_v_i, y_v_i, X_te_i, y_te_i = train_val_test_split(X_imb, y_imb)
    model_imb = SimpleLogistic(lr=0.5, epochs=500)
    model_imb.fit(X_tr_i, y_tr_i)
    y_pred_imb = [model_imb.predict(x) for x in X_te_i]
    print(f"\n  Trained model on imbalanced data:")
    print(f"    Accuracy:  {accuracy(y_te_i, y_pred_imb):.4f}")
    print(f"    Precision: {precision(y_te_i, y_pred_imb):.4f}")
    print(f"    Recall:    {recall(y_te_i, y_pred_imb):.4f}")
    print(f"    F1 Score:  {f1_score(y_te_i, y_pred_imb):.4f}")

    print("\n=== Regression Metrics ===")
    X_reg, y_reg = make_regression_data(200)

    col0 = [x[0] for x in X_reg]
    col1 = [x[1] for x in X_reg]
    col0_s, m0, s0 = standardize(col0)
    col1_s, m1, s1 = standardize(col1)
    X_reg_scaled = [[col0_s[i], col1_s[i]] for i in range(len(X_reg))]

    X_tr_r, y_tr_r, X_v_r, y_v_r, X_te_r, y_te_r = train_val_test_split(X_reg_scaled, y_reg)
    reg_model = SimpleLinearRegression(lr=0.01, epochs=500)
    reg_model.fit(X_tr_r, y_tr_r)
    y_pred_r = [reg_model.predict(x) for x in X_te_r]

    print(f"  MSE:       {mse(y_te_r, y_pred_r):.4f}")
    print(f"  RMSE:      {rmse(y_te_r, y_pred_r):.4f}")
    print(f"  MAE:       {mae(y_te_r, y_pred_r):.4f}")
    print(f"  R-squared: {r_squared(y_te_r, y_pred_r):.4f}")

    mean_baseline = [sum(y_tr_r) / len(y_tr_r)] * len(y_te_r)
    print(f"\n  Mean baseline:")
    print(f"    MSE:       {mse(y_te_r, mean_baseline):.4f}")
    print(f"    R-squared: {r_squared(y_te_r, mean_baseline):.4f}")

    print("\n=== Learning Curve ===")
    sizes, train_sc, val_sc = learning_curve(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=200),
        metric_fn=accuracy,
    )
    print(f"  {'Size':>6} {'Train':>8} {'Val':>8}")
    for s, tr, va in zip(sizes, train_sc, val_sc):
        print(f"  {s:>6} {tr:>8.4f} {va:>8.4f}")

    print("\n=== Statistical Model Comparison ===")
    model_a_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=100),
        k=5, metric_fn=accuracy,
    )
    model_b_scores = cross_validate(
        X_clf, y_clf,
        model_fn=lambda: SimpleLogistic(lr=0.1, epochs=500),
        k=5, metric_fn=accuracy,
    )
    diffs = [a - b for a, b in zip(model_a_scores, model_b_scores)]
    mean_diff = sum(diffs) / len(diffs)
    std_diff = math.sqrt(sum((d - mean_diff) ** 2 for d in diffs) / len(diffs))
    t_stat = mean_diff / (std_diff / math.sqrt(len(diffs))) if std_diff > 0 else 0.0
    print(f"  Model A (100 epochs) mean: {sum(model_a_scores)/len(model_a_scores):.4f}")
    print(f"  Model B (500 epochs) mean: {sum(model_b_scores)/len(model_b_scores):.4f}")
    print(f"  Mean difference: {mean_diff:.4f}")
    print(f"  Paired t-statistic: {t_stat:.4f}")
    print(f"  (|t| > 2.78 for significance at p<0.05 with df=4)")

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

در scikit-learn، ارزیابی بخشی از روند کاری است:

from sklearn.model_selection import cross_val_score, StratifiedKFold, learning_curve
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    roc_auc_score, confusion_matrix, mean_squared_error, r2_score,
)
from sklearn.linear_model import LogisticRegression

model = LogisticRegression()
scores = cross_val_score(model, X, y, cv=StratifiedKFold(5), scoring="f1")

نسخه‌های پیاده‌سازی‌شده از صفر دقیقاً نشان می‌دهند که اعتبارسنجی متقاطع چه می‌کند (هیچ جادویی در کار نیست؛ صرفاً حلقه‌های for و دنبال کردن اندیس‌هاست)، هر معیار چگونه محاسبه می‌شود (فقط شمارش TP/FP/TN/FN) و چرا لایه‌بندی اهمیت دارد (حفظ نسبت کلاس‌ها در هر بخش). نسخه‌های کتابخانه‌ای قابلیت اجرای موازی، گزینه‌های امتیازدهی متنوع‌تر و یکپارچگی با پایپ‌لاین‌ها را به این ساختار اضافه می‌کنند.

خروجی این درس

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

  • outputs/skill-evaluation.md - یک مهارت در زمینه‌ی استراتژی ارزیابی برای مدل‌های دسته‌بندی و رگرسیون

تمرین‌ها

  1. منحنی‌های دقت-بازخوانی (precision-recall) را پیاده‌سازی کنید: دقت را در برابر بازخوانی در آستانه‌های مختلف رسم کنید. میانگین دقت (سطح زیر منحنی PR) را محاسبه کنید. منحنی PR را با منحنی ROC روی یک مجموعه‌داده‌ی نامتوازن مقایسه کنید و توضیح دهید در چه شرایطی هر کدام اطلاعات مفیدتری ارائه می‌دهند.
  2. یک حلقه‌ی اعتبارسنجی متقاطع تو‌در‌تو (nested cross-validation) بسازید: حلقه‌ی بیرونی عملکرد مدل را ارزیابی کند و حلقه‌ی درونی ابرپارامترها را تنظیم کند. از آن برای مقایسه‌ی منصفانه‌ی دو مدل استفاده کنید، طوری که داده‌های اعتبارسنجی به فرایند ارزیابی نشت نکنند.
  3. یک آزمون جایگشت (permutation test) برای مقایسه‌ی مدل پیاده‌سازی کنید: برچسب‌ها را بُر بزنید، مدل را بازآموزی کنید و عملکرد را بسنجید. این کار را 100 بار تکرار کنید تا توزیع خنثی (null distribution) شکل بگیرد. در نهایت، مقدار p-value را برای عملکرد واقعی مدل در مقایسه با این توزیع به دست آورید.

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

اصطلاح در گفتگوهای عامیانه معنای دقیق فنی
بیش‌برازش (Overfitting) «حفظ کردن داده‌های آموزش» مدل نویز موجود در داده‌های آموزش را یاد می‌گیرد؛ در نتیجه روی داده‌های آموزش عالی عمل می‌کند اما روی داده‌های ندیده ضعیف است
اعتبارسنجی متقاطع (Cross-validation) «تست کردن روی بخش‌های مختلف» جابه‌جایی منظم بخشی از داده که برای اعتبارسنجی به کار می‌رود و میانگین‌گیری از نتایج در تمام این چرخش‌ها
دقت (Precision) «چند تا از پیش‌بینی‌های مثبت درست بوده‌اند» TP/(TP+FP)TP / (TP + FP): کسری از پیش‌بینی‌های مثبت که واقعاً مثبت هستند
بازخوانی (Recall) «چند تا از مثبت‌های واقعی را توانستیم پیدا کنیم» TP/(TP+FN)TP / (TP + FN): کسری از مثبت‌های واقعی که به درستی شناسایی شده‌اند
AUC-ROC «مدل چقدر خوب کلاس‌ها را از هم جدا می‌کند» سطح زیر منحنیِ نرخ مثبت واقعی در برابر نرخ مثبت کاذب در تمام آستانه‌ها، از 0.5 (تصادفی) تا 1.0 (بی‌نقص)
ضریب تعیین (R-squared) «چه مقدار از واریانس توجیه شده است» 1−(sum of squared residuals/total sum of squares)1 - (\text{sum of squared residuals} / \text{total sum of squares}): کسری از واریانس متغیر هدف که مدل توانسته آن را توضیح دهد
نشت داده (Data leakage) «مدل تقلب کرده است» استفاده از اطلاعاتی در زمان آموزش که هنگام پیش‌بینی در دسترس نخواهند بود، که منجر به ارزیابی بیش‌ازحد خوش‌بینانه می‌شود
منحنی یادگیری (Learning curve) «عملکرد مدل با افزایش داده چگونه تغییر می‌کند» نمودار امتیازهای آموزش و اعتبارسنجی در برابر اندازه‌ی مجموعه‌ی آموزش، که کم‌برازش یا بیش‌برازش را مشخص می‌کند
تفکیک لایه‌بندی‌شده (Stratified split) «متوازن نگه‌داشتن نسبت کلاس‌ها» تفکیک داده‌ها به شکلی که در هر زیرمجموعه، نسبت هر کلاس دقیقاً مانند کل مجموعه‌داده باشد

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

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

چرا معیار صحت (Accuracy) برای داده‌های نامتوازن مناسب نیست؟

اگر 95% داده‌ها متعلق به یک کلاس باشند، مدلی که کورکورانه همیشه آن کلاس را پیش‌بینی کند به صحت 95% می‌رسد در حالی که عملاً هیچ یادگیری مفیدی نداشته است. در چنین شرایطی باید از معیارهایی مثل Precision، Recall، F1 یا AUC-ROC استفاده کرد.

تفاوت K-fold معمولی با Stratified K-fold چیست؟

در Stratified K-fold نسبت توزیع کلاس‌ها در تمام بخش‌ها (folds) مشابه کل مجموعه‌داده حفظ می‌شود، در حالی که در K-fold معمولی تفکیک تصادفی ممکن است باعث شود نمونه‌های یک کلاس اقلیت در یک بخش تجمیع شده یا حذف شوند.

نشت داده (Data Leakage) چگونه اتفاق می‌افتد و راه جلوگیری از آن چیست؟

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

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

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