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

فصل ۸ از ۸

پیشرفت ترم
۰٪

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

پروژه: بهبودِ مستند با پنج آزمایش

فصل ۸پیش‌نمایش رایگان
۸ دقیقه مطالعه فصل ۸

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

هیچ مفهومِ تازه‌ای نیست. کاری که می‌کنی این است: هفت فصلِ این ترم را روی یک مسئلهٔ واقعاً سخت به‌کار می‌بری — دادهٔ برچسب‌نویزیِ فصلِ ۲ — پنج آزمایشِ منظم انجام می‌دهی، هر کدام با سه seed، و با عدد ثابت می‌کنی که کدام تغییر واقعی است.

و برخلافِ جدولِ فصلِ ۷، این‌بار تفاوت‌ها از نوسان بزرگ‌ترند. در پایان، برای اولین و آخرین بار در این ترم، مجموعهٔ آزمون را باز می‌کنی.

یک دفترچهٔ آزمایش با پنج ردیفِ ثبت‌شده و یک مهرِ نهایی

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

  • یک بهبودِ واقعی را از صفر تا گزارش مستند کنی
  • چند اهرم را با هم ترکیب کنی و سهمشان را جدا کنی
  • بگویی چرا مجموعهٔ آزمون فقط یک بار باز می‌شود
  • گزارشی بنویسی که کسِ دیگری بتواند تکرارش کند

قبل از شروع#

از کلِ ترمِ ۲. این فصل هیچ چیزِ تازه‌ای معرفی نمی‌کند.

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

۱. مسئله#

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

loss_fn = nn.CrossEntropyLoss()


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


torch.manual_seed(0)
SUBSET = 300
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()))
برچسب‌های خراب: 82

سیصد نمونهٔ آموزش که ۸۲تایشان برچسبِ غلط دارند. این مسئله سه ویژگیِ دادهٔ واقعی را با هم دارد: کم است، نویزی است، و اعتبارسنجی‌اش تمیز است — یعنی معیارِ سنجش حقیقت را می‌گوید حتی وقتی دادهٔ آموزش دروغ دارد.

۲. یک تابع، برای همهٔ آزمایش‌ها#

def build(dropout=0.0, width=256):
    layers = [nn.Linear(64, width), nn.ReLU()]
    if dropout:
        layers.append(nn.Dropout(dropout))
    layers += [nn.Linear(width, width), nn.ReLU()]
    if dropout:
        layers.append(nn.Dropout(dropout))
    return nn.Sequential(*layers, nn.Linear(width, 10))


def run(seed=0, dropout=0.0, wd=0.0, patience=None, epochs=80, lr=0.005):
    torch.manual_seed(seed)
    model = build(dropout)
    loader = DataLoader(TensorDataset(Xn, yn), batch_size=32, shuffle=True)
    opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
    best, best_state, waited = 0.0, None, 0
    for _ in range(epochs):
        model.train()
        for xb, yb in loader:
            opt.zero_grad()
            loss_fn(model(xb), yb).backward()
            opt.step()
        if patience:
            current = accuracy(model, Xva, yva)
            if current > best:
                best, waited = current, 0
                best_state = {k: v.clone() for k, v in model.state_dict().items()}
            else:
                waited += 1
                if waited >= patience:
                    break
    if patience and best_state:
        model.load_state_dict(best_state)
    return model

یک تابع، همهٔ اهرم‌ها به‌صورتِ parameter. این همان قاعدهٔ فصلِ ۷ است: کدِ کپی‌شده جایی است که تفاوتِ ناخواسته پنهان می‌شود.

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

۳. پنج آزمایش، هر کدام با سه seed#

CONFIGS = [
    ("۱ پایه", dict()),
    ("۲ + توقف زودهنگام", dict(patience=10)),
    ("۳ + dropout 0.6", dict(dropout=0.6)),
    ("۴ + weight decay", dict(wd=0.01)),
    ("۵ + هر سه", dict(dropout=0.6, wd=0.01, patience=10)),
]

for name, options in CONFIGS:
    values = [accuracy(run(seed=s, **options), Xva, yva) for s in (0, 1, 2)]
    print(f"{name:>18}: میانگین={sum(values) / 3:.4f} بازه={min(values):.4f}..{max(values):.4f}")
            ۱ پایه: میانگین=0.7372 بازه=0.7354..0.7409
 ۲ + توقف زودهنگام: میانگین=0.8737 بازه=0.8579..0.8830
   ۳ + dropout 0.6: میانگین=0.8273 بازه=0.7967..0.8496
  ۴ + weight decay: میانگین=0.8050 بازه=0.7883..0.8384
         ۵ + هر سه: میانگین=0.9044 بازه=0.8997..0.9109

و این‌بار، برخلافِ جدولِ فصلِ ۷، تفاوت‌ها واقعی‌اند.

معیارِ فصلِ ۷ را رویش اجرا کن — بازه‌ها هم‌پوشانی دارند یا نه؟

  • پایه (۰٫۷۳۵..۰٫۷۴۱) با همه بی‌هم‌پوشانی است. پس هر سه اهرم واقعاً کمک کردند.
  • توقفِ زودهنگام (۰٫۸۵۸..۰٫۸۸۳) از هر دو اهرمِ دیگر بالاتر است، و با هیچ‌کدامشان هم‌پوشانی ندارد — کفِ آن (۰٫۸۵۷۹) از سقفِ dropout (۰٫۸۴۹۶) و سقفِ weight decay (۰٫۸۳۸۴) بالاتر است. پس ادعای «توقفِ زودهنگام مؤثرترینِ تکِ اهرم‌هاست» قابلِ دفاع است.
  • ترکیبِ هر سه (۰٫۹۰۰..۰٫۹۱۱) از همهٔ تک‌اهرم‌ها بالاتر است، بدونِ هم‌پوشانی. پس اثرشان جمع می‌شود — چیزی که در فصلِ ۷ روی دادهٔ تمیز صادق نبود.

📏 اندازه بگیر: تفاوتِ این جدول با جدولِ فصلِ ۷ در کد نیست، در مسئله است. آنجا مدل مشکلِ جدی‌ای نداشت، پس هیچ درمانی اثرِ قابلِ اندازه‌گیری نداشت. اینجا مشکل واقعی است (۲۵ درصد برچسبِ غلط) و درمان‌ها واقعاً اثر می‌کنند. درسِ عملی: قبل از تنظیمِ اهرم‌ها، مطمئن شو مشکلی داری که تنظیم حلش کند — وگرنه ساعت‌ها روی نویز کار می‌کنی.

۴. آن یک بارِ نهایی#

هفت فصل، ده‌ها بار نگاه به مجموعهٔ اعتبارسنجی. حالا برای اولین و آخرین بار، مجموعهٔ آزمون.

winner = run(seed=0, dropout=0.6, wd=0.01, patience=10)
baseline = run(seed=0)

print("برنده روی آزمون:", round(accuracy(winner, Xte, yte), 4))
print("پایه روی آزمون :", round(accuracy(baseline, Xte, yte), 4))
برنده روی آزمون: 0.925
پایه روی آزمون : 0.7472

۷۴٫۷ درصد به ۹۲٫۵ درصد — تقریباً هجده واحد.

و این عدد قابلِ دفاع است، به چهار دلیل:

  • روی داده‌ای گرفته شد که در هیچ تصمیمی از آن استفاده نشده بود.
  • انتخابِ پیکربندی با اعتبارسنجی انجام شد، نه با آزمون.
  • برتری روی اعتبارسنجی با سه seed تأیید شده بود، نه با یک اجرا.
  • و بازه‌های اعتبارسنجی هم‌پوشانی نداشتند، پس این یک تفاوتِ تصادفی نیست.

دقتِ آزمون (۰٫۹۲۵) کمی بالاتر از میانگینِ اعتبارسنجی (۰٫۹۰۴) درآمد. این طبیعی است و نگران‌کننده نیست: دو مجموعهٔ متفاوت‌اند و هر کدام نوسانِ نمونه‌گیریِ خودشان را دارند. آنچه نگران‌کننده بود این می‌شد که آزمون خیلی بدتر از اعتبارسنجی دربیاید — نشانهٔ اینکه روی اعتبارسنجی overfit کرده‌ای.

۵. گزارشِ نهایی#

مسئله طبقه‌بندیِ رقمِ دست‌نویس ۸×۸، ده کلاس
داده ۳۰۰ نمونهٔ آموزش با ۸۲ برچسبِ خراب، ۳۵۹ اعتبارسنجی و ۳۶۰ آزمونِ تمیز
مدل شبکهٔ سه‌لایه، عرضِ ۲۵۶
پیکربندیِ نهایی dropout=0.6، weight_decay=0.01، توقفِ زودهنگام با patience=10
آموزش تا ۸۰ epoch، Adam، lr=0.005، batch=32، seed=0
اعتبارسنجی ۰٫۹۰۴۴ میانگین روی سه seed، بازهٔ ۰٫۹۰۰..۰٫۹۱۱
آزمون (یک بار) ۰٫۹۲۵۰ در برابرِ ۰٫۷۴۷۲ پایه
مؤثرترین تکِ اهرم توقفِ زودهنگام (۰٫۷۳۷ ← ۰٫۸۷۴)
محدودیت نویزِ برچسب ساختگی و یکنواخت است؛ نویزِ واقعی معمولاً روی کلاس‌های مشابه متمرکز است

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

چک کن: قبل از باور کردنِ عددِ نهایی، آزمونِ نشتیِ فصلِ ۷ را اجرا کن: برچسب‌های آموزش را کاملاً تصادفی کن و دوباره آموزش بده. دقتِ اعتبارسنجی باید به حدودِ ۰٫۱ سقوط کند. اگر نکرد، جایی نشتی داری و کلِ این جدول بی‌اعتبار است.

🔧 اگر کار نکرد: عددهایت با کتاب نمی‌خواندtorch.manual_seed(0) قبل از ساختنِ نویز جا مانده؛ آن‌وقت mask متفاوت می‌شود و کلِ مسئله عوض. توقفِ زودهنگام هیچ‌وقت فعال نمی‌شودpatience بزرگ‌تر از تعدادِ epoch است. اجرای جدول طول می‌کشد — پنج پیکربندی × سه seed = ۱۵ آموزش؛ روی CPU چند دقیقه، و همین هزینهٔ واقعیِ کارِ منظم است.

🤖 از دستیارت بپرس: جدولِ بخشِ ۳ را بده و بپرس: «چه آزمایشِ ششمی پیشنهاد می‌کنی؟» هر پیشنهادی را با یک سؤال بسنج: اگر این کار کند، چه چیزی یاد می‌گیرم که الان نمی‌دانم؟ آزمایشی که جوابش را از قبل می‌دانی، وقت است نه اطلاعات. و اگر پیشنهاد داد «داده بیشتر»، بدان که درست‌ترین جواب را داده — فصلِ ۱ همین را با عدد نشان داد.

تمرین‌ها

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

در فصل بعد#

ترمِ ۲ اینجا تمام می‌شود. سه چیز را با خودت ببر:

  • دو منحنی، همیشه. یک عددِ آموزش هیچ‌چیز نمی‌گوید.
  • تفاوتی که از نوسانِ seedها کوچک‌تر باشد، تفاوت نیست.
  • قبل از تنظیمِ اهرم، مطمئن شو مشکلی داری که آن اهرم حلش می‌کند.

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

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

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