در این فصل چه یاد میگیری#
فصلِ قبل 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.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.