در این فصل چه یاد میگیری#
هیچ مفهومِ تازهای نیست. کاری که میکنی این است: هفت فصلِ این ترم را روی یک مسئلهٔ واقعاً سخت بهکار میبری — دادهٔ برچسبنویزیِ فصلِ ۲ — پنج آزمایشِ منظم انجام میدهی، هر کدام با سه 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 را از پایه میسازد، و بعد کاری میکند که ۹۹ درصدِ کارِ واقعی است — گرفتنِ یک مدلِ آماده و تطبیقش با دادهٔ خودت.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.