ژرفا — خودآموز یادگیری عمیق و مهندسی هوش مصنوعی (متوسط)

فصل ۲ از ۸

پیشرفت ترم
۰٪

ترم ۲ · آموزشِ درست: تعمیم و آزمایشِ منظم

جلوگیری از حفظ‌کردن

فصل ۲پیش‌نمایش رایگان

در این فصل چه یاد می‌گیری#

فصلِ قبل overfitting را دیدی، ولی روی داده‌ای تمیز — و آنجا خیلی آزاردهنده نبود. دادهٔ واقعی تمیز نیست. بخشی از برچسب‌هایش غلط است، و مدلی که آزادانه حفظ کند، غلط‌ها را هم حفظ می‌کند.

در این فصل عمداً برچسب‌ها را خراب می‌کنیم تا مسئله واقعی شود، و بعد سه ابزارِ مهار را می‌سنجیم: weight decay، dropout و توقفِ زودهنگام. و می‌بینی که هر سه بده‌بستان دارند — هیچ‌کدام رایگان نیست.

یک افسارِ کنترل که جلوی حرکتِ بیش از حد را می‌گیرد

آخر این فصل می‌توانی:

  • weight decay و dropout را به‌کار ببری و اثرشان را بسنجی
  • توقفِ زودهنگام را پیاده کنی
  • بگویی چرا هر کدام از این‌ها می‌تواند ضرر بزند
  • شدتِ مهار را با آزمایش انتخاب کنی، نه با حدس

قبل از شروع#

از فصل ۱: make_model، train، accuracy و تفسیرِ دو منحنی.

از ترمِ ۱ فصل ۷: حالتِ train/eval — اینجا برای اولین بار واقعاً اهمیت پیدا می‌کند.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

۱. دادهٔ واقعی‌تر: برچسبِ نویزی#

import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader

SUBSET, EPOCHS = 300, 80

torch.manual_seed(0)
Xn, yn = Xtr[:SUBSET].clone(), ytr[:SUBSET].clone()
mask = torch.rand(SUBSET) < 0.25
yn[mask] = torch.randint(0, 10, (int(mask.sum()),))

print("برچسب‌های خراب‌شده:", int(mask.sum()), "از", SUBSET)
برچسب‌های خراب‌شده: 82 از 300

۸۲ نمونه از ۳۰۰ برچسبِ تصادفی گرفتند. این نه یک بازیِ ساختگی، بلکه شبیه‌سازیِ چیزی است که در هر مجموعهٔ دادهٔ برچسب‌خوردهٔ واقعی وجود دارد: خطای انسانِ برچسب‌زن، موردِ مبهم، یا برچسبِ خودکارِ اشتباه.

و نکتهٔ کلیدی: دادهٔ اعتبارسنجی دست‌نخورده است. پس عددِ اعتبارسنجی همچنان حقیقت را می‌گوید، در حالی که دادهٔ آموزش دروغ‌های زیادی دارد.

۲. حالا overfitting واقعاً درد دارد#

def make_model(hidden=256, dropout=0.0, seed=0):
    torch.manual_seed(seed)
    layers = [nn.Linear(64, hidden), nn.ReLU()]
    if dropout:
        layers.append(nn.Dropout(dropout))
    layers += [nn.Linear(hidden, hidden), nn.ReLU()]
    if dropout:
        layers.append(nn.Dropout(dropout))
    return nn.Sequential(*layers, nn.Linear(hidden, 10))


@torch.no_grad()
def accuracy(model, X, y):
    model.eval()
    return float((model(X).argmax(1) == y).float().mean())


def train_noisy(model, weight_decay=0.0, epochs=EPOCHS):
    loader = DataLoader(TensorDataset(Xn, yn), batch_size=32, shuffle=True)
    opt = torch.optim.Adam(model.parameters(), lr=0.005, weight_decay=weight_decay)
    loss_fn = nn.CrossEntropyLoss()
    history = []
    for epoch in range(1, epochs + 1):
        model.train()
        for xb, yb in loader:
            opt.zero_grad()
            loss_fn(model(xb), yb).backward()
            opt.step()
        history.append((epoch, accuracy(model, Xn, yn), accuracy(model, Xva, yva)))
    return history


history = train_noisy(make_model())
for epoch in (1, 5, 10, 20, 40, 80):
    _, tr, va = history[epoch - 1]
    print(f"epoch {epoch:>2}: دقت آموزش={tr:.4f} | دقت اعتبارسنجی={va:.4f}")
epoch  1: دقت آموزش=0.5433 | دقت اعتبارسنجی=0.5738
epoch  5: دقت آموزش=0.7300 | دقت اعتبارسنجی=0.8691
epoch 10: دقت آموزش=0.8067 | دقت اعتبارسنجی=0.8384
epoch 20: دقت آموزش=0.8867 | دقت اعتبارسنجی=0.7549
epoch 40: دقت آموزش=0.9767 | دقت اعتبارسنجی=0.7159
epoch 80: دقت آموزش=1.0000 | دقت اعتبارسنجی=0.7409

این دقیقاً همان منحنیِ کلاسیکِ overfitting است، و این‌بار دردناک:

  • تا epoch ۵، اعتبارسنجی به ۸۷ درصد می‌رسد. مدل دارد الگوی واقعیِ ارقام را یاد می‌گیرد.
  • بعد از آن، دقتِ آموزش بالا می‌رود و اعتبارسنجی سقوط می‌کند — تا ۷۲ درصد.
  • در epoch ۸۰، دقتِ آموزش صد درصد است: مدل حتی آن ۸۲ برچسبِ تصادفی را هم حفظ کرده.

و حفظ کردنِ یک برچسبِ غلط یعنی یاد گرفتنِ یک چیزِ غلط. برای همین اعتبارسنجی بدتر می‌شود، نه فقط ثابت می‌ماند.

۳. توقفِ زودهنگام: ارزان‌ترین درمان#

best = max(history, key=lambda row: row[2])
print("بهترین اعتبارسنجی:", round(best[2], 4), "در epoch", best[0])
print("اعتبارسنجی در epoch آخر:", round(history[-1][2], 4))
print("چقدر از دست دادیم:", round(best[2] - history[-1][2], 4))
بهترین اعتبارسنجی: 0.8802 در epoch 6
اعتبارسنجی در epoch آخر: 0.7409
چقدر از دست دادیم: 0.1393

چهارده درصد دقت، فقط به‌خاطرِ اینکه هفتاد و چهار epoch بیشتر آموزش دادیم.

early stopping یعنی همین: دقتِ اعتبارسنجی را در هر epoch بسنج، بهترین وضعیتِ مدل را نگه دار، و اگر چند epoch بهتر نشد، بایست.

best_acc, best_state, patience = 0.0, None, 10
for epoch in range(1, epochs + 1):
    train_one_epoch(model)
    acc = accuracy(model, Xva, yva)
    if acc > best_acc:
        best_acc, best_state, waited = acc, copy.deepcopy(model.state_dict()), 0
    else:
        waited += 1
        if waited >= patience:
            break
model.load_state_dict(best_state)      # به بهترین حالت برگرد، نه آخرین

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

📏 اندازه بگیر: early stopping ارزان‌ترین regularization است: صفر پارامترِ تنظیمی جز patience، صفر هزینهٔ محاسباتی، و اغلب بزرگ‌ترین اثر. قبل از هر چیزِ دیگری امتحانش کن. و patience را کوچک نگذار — دقتِ اعتبارسنجی نوسان دارد و ایستادنِ زودهنگام روی یک نوسان، خودش یک خطاست.

۴. weight decay: جریمهٔ وزن‌های بزرگ#

for wd in [0.0, 0.001, 0.01, 0.05]:
    h = train_noisy(make_model(), weight_decay=wd)
    print(f"weight_decay={wd:<6}: آموزش={h[-1][1]:.4f} اعتبارسنجی={h[-1][2]:.4f}")
weight_decay=0.0   : آموزش=1.0000 اعتبارسنجی=0.7409
weight_decay=0.001 : آموزش=1.0000 اعتبارسنجی=0.6908
weight_decay=0.01  : آموزش=0.8367 اعتبارسنجی=0.7883
weight_decay=0.05  : آموزش=0.4133 اعتبارسنجی=0.4234

یک منحنیِ U کامل:

  • 0.001 خیلی ضعیف است — مدل باز هم همه‌چیز را حفظ کرد و نتیجه حتی کمی بدتر شد.
  • 0.01 درست است — دقتِ آموزش به ۸۴ درصد افتاد (یعنی دیگر برچسب‌های غلط را حفظ نمی‌کند) و اعتبارسنجی به ۷۹ درصد رسید.
  • 0.05 خیلی زیاد است — مدل حتی الگوی واقعی را هم نتوانست یاد بگیرد. این underfitting است، همان چیزی که فصلِ ۱ تشخیصش را داد.

weight decay چه می‌کند؟ به تابعِ خطا یک جریمه اضافه می‌کند که با بزرگیِ وزن‌ها زیاد می‌شود. وزنِ بزرگ یعنی مدل به یک ویژگیِ خاص خیلی حساس است، و جریمه کردنش مدل را به سمتِ راه‌حل‌های ساده‌تر و هموارتر هُل می‌دهد.

🌱 ریشه‌اش کجاست: «بزرگیِ وزن‌ها» یک استعاره نیست، طولِ یک بردار است — همان چیزی که ریشه، ترم ۷ فصل ۲ — طول و مقیاس از فیثاغورس می‌سازدش و در یادگیری ماشین norm نامیده می‌شود. وزن‌های یک لایه یک بردارند، و weight decay مربعِ طولِ همان بردار را به loss اضافه می‌کند. پس «مدلِ ساده‌تر» در اینجا تعریفِ دقیقی دارد: مدلی که بردارِ وزنش کوتاه‌تر است.

⚠️ مواظب باش: آن سطرِ 0.001 را جدی بگیر: مقدارِ کمِ regularization نه‌فقط بی‌اثر است، بلکه می‌تواند بدتر کند. پس «کمی weight decay بگذارم که ضرر ندارد» یک استدلالِ غلط است. یا اندازه‌اش را با آزمایش پیدا کن، یا اصلاً نگذارش.

۵. dropout: خاموش کردنِ تصادفیِ نورون‌ها#

for p in [0.0, 0.3, 0.6]:
    h = train_noisy(make_model(dropout=p))
    print(f"dropout={p:<4}: آموزش={h[-1][1]:.4f} اعتبارسنجی={h[-1][2]:.4f}")
dropout=0.0 : آموزش=1.0000 اعتبارسنجی=0.7409
dropout=0.3 : آموزش=1.0000 اعتبارسنجی=0.7521
dropout=0.6 : آموزش=0.8767 اعتبارسنجی=0.8496

dropout=0.6 دقت را از ۷۴ به ۸۵ درصد برد — بهترین نتیجهٔ این فصل بعد از توقفِ زودهنگام.

منطقش زیباست: در هر قدمِ آموزش، ۶۰ درصدِ نورون‌ها تصادفاً خاموش می‌شوند. پس مدل نمی‌تواند به هیچ نورونِ خاصی تکیه کند و مجبور می‌شود اطلاعات را پخش کند. و مسیرِ حفظ کردنِ یک نمونهٔ خاص، که معمولاً از چند نورونِ مشخص می‌گذرد، شکسته می‌شود.

و به 0.3 نگاه کن: تقریباً هیچ اثری نداشت. دوباره همان درس: شدتِ مهار باید با شدتِ مسئله بخواند.

چک کن: Dropout فقط در حالتِ train فعال است. اگر model.eval() را در تابعِ ارزیابی جا بیندازی، عددِ اعتبارسنجی‌ات مصنوعاً بد می‌شود و تو نتیجه می‌گیری که dropout ضرر دارد — در حالی که فقط اشتباه سنجیده‌ای. آن model.eval() داخلِ accuracy دقیقاً برای همین است، و این رایج‌ترین خطای سنجش در کلِ این ترم است.

۶. جمع‌بندیِ صادقانه#

ابزار بهترین نتیجه اینجا هزینه‌اش
هیچ‌کدام ۰٫۷۴۰۹
weight decay = 0.01 ۰٫۷۸۸۳ یک پارامترِ حساس برای تنظیم
dropout = 0.6 ۰٫۸۴۹۶ آموزش کندتر همگرا می‌شود
توقف در epoch ۶ ۰٫۸۸۰۲ باید هر epoch ارزیابی کنی

ساده‌ترین ابزار بهترین نتیجه را داد. این تصادفی نیست: وقتی مشکل «آموزشِ بیش از حد» است، درمانِ مستقیمش «کمتر آموزش بده» است، نه پیچیده‌ترش کردن.

و این سه ابزار با هم هم به‌کار می‌روند — ولی هر ترکیبی را باید بسنجی، چون اثرشان جمع‌شونده نیست. فصلِ ۷ روشِ منظمِ این سنجش را می‌دهد و فصلِ ۸ رویش پروژه می‌سازد.

🔧 اگر کار نکرد: dropout هیچ اثری ندارد — احتمالاً بعد از لایهٔ آخر گذاشته‌ای، که بی‌معناست؛ dropout بینِ لایه‌های پنهان می‌رود. weight_decay باعثِ nan شد — مقدارش خیلی بزرگ است. دقتِ آموزش هیچ‌وقت به ۱۰۰ نمی‌رسد — با dropout طبیعی است و نشانهٔ خرابی نیست.

🤖 از دستیارت بپرس: «چه تفاوتی بینِ weight_decay در Adam و AdamW هست؟» این یکی از معدود جاهایی است که یک جزئیاتِ پیاده‌سازی واقعاً روی نتیجه اثر می‌گذارد، و بیشترِ کدهای امروزی به همین دلیل AdamW را به‌کار می‌برند. بعد آزمایشِ بخشِ ۴ را با AdamW تکرار کن و ببین عددها فرق می‌کنند یا نه.

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
regularization رگیولاریزیشن هر تکنیکی که جلوی حفظ‌کردن را می‌گیرد
weight decay وِیت دیکِی جریمهٔ بزرگیِ وزن‌ها در تابعِ خطا
dropout دراپ‌اوت خاموش کردنِ تصادفیِ نورون‌ها در آموزش
early stopping ارلی استاپینگ ایستادن وقتی اعتبارسنجی بهتر نمی‌شود
patience پیشنس چند epoch صبر قبل از ایستادن
label noise لیبل نویز برچسبِ غلط در دادهٔ آموزش

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

مهار را یاد گرفتی. حالا سراغِ چیزی می‌رویم که بدونش شبکهٔ عمیق اصلاً آموزش نمی‌بیند: نرمال‌سازی. هم روی ورودی — که اثرش را در ترمِ ۱ فصلِ ۸ دیدی — و هم داخلِ خودِ شبکه، با BatchNorm و LayerNorm.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.