در این فصل چه یاد میگیری#
اگر فقط یک عدد را بتوانی تنظیم کنی، آن نرخِ یادگیری است. در ترمِ ۱ دیدی که خیلی بزرگش همهچیز را منفجر میکند؛ اینجا یاد میگیری سیستماتیک پیدایش کنی بهجای اینکه حدس بزنی، و بعد یک قدم جلوتر میروی: نرخ لازم نیست ثابت بماند.
و یک نتیجهٔ صادقانه هم میگیری: یکی از تکنیکهای محبوبِ این حوزه روی این مسئله هیچ کمکی نمیکند — و همان هم درسی است.

آخر این فصل میتوانی:
- بازهٔ مناسبِ نرخ را با یک آزمایشِ کوتاه پیدا کنی
schedulerرا بهکار ببری و بگویی چرا کمک میکندwarmupرا بشناسی و بدانی کِی لازم است- ادعای «فلان تکنیک بهتر است» را روی مسئلهٔ خودت بسنجی
قبل از شروع#
از ترمِ ۱ فصل ۳ و ۸: اثرِ نرخ روی همگرایی و واگرایی.
از فصل ۱ همین ترم: سنجش با مجموعهٔ اعتبارسنجی.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. جاروبِ کامل: چه چیزی واقعاً اتفاق میافتد#
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
loss_fn = nn.CrossEntropyLoss()
def plain(seed=0):
torch.manual_seed(seed)
return nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 10))
@torch.no_grad()
def accuracy(model, X, y):
model.eval()
return float((model(X).argmax(1) == y).float().mean())
def fit_adam(model, epochs=15, lr=0.01):
loader = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True)
opt = torch.optim.Adam(model.parameters(), lr=lr)
for _ in range(epochs):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
return accuracy(model, Xva, yva)
for lr in [1e-4, 1e-3, 1e-2, 1e-1, 1.0]:
print(f"lr={lr:<7}: دقت اعتبارسنجی = {fit_adam(plain(), epochs=15, lr=lr):.4f}")
lr=0.0001 : دقت اعتبارسنجی = 0.7967
lr=0.001 : دقت اعتبارسنجی = 0.9331
lr=0.01 : دقت اعتبارسنجی = 0.9610
lr=0.1 : دقت اعتبارسنجی = 0.9387
lr=1.0 : دقت اعتبارسنجی = 0.1003
یک منحنیِ کوهانیشکل، با قلهٔ روشن در ۰٫۰۱.
- ۰٫۰۰۰۱ خیلی کوچک: مدل در پانزده
epochبه جایی نرسید. مشکل کیفیت نیست، وقت است. - ۰٫۰۱ بهترین.
- ۱٫۰ فاجعه: دقتِ ۰٫۱۰۰۳ یعنی دقیقاً حدسِ تصادفی بینِ ده کلاس. مدل هیچچیز یاد نگرفت.
و به فاصلهها دقت کن: هر پله ده برابر است، نه ۱۰ درصد. نرخِ یادگیری در مقیاسِ لگاریتمی جستوجو میشود؛ تفاوتِ ۰٫۰۱ و ۰٫۰۱۱ عملاً هیچ است، ولی تفاوتِ ۰٫۰۱ و ۰٫۱ واقعی.
🌱 ریشهاش کجاست: نرخِ یادگیری در چیزی ضرب میشود که اسمش گرادیان است — یک بردار، نه یک عدد — و ریشه، ترم ۸ فصل ۷ — مشتقِ جزئی و بردارِ گرادیان میسازدش. و همانجا دلیلِ حساسیتِ این فصل هم روشن میشود: گرادیان جهتِ تندترین شیب را میدهد و بزرگیاش هم در قدم ضرب میشود — ولی آن بزرگی به واحدِ
lossاست، نه به واحدِ قدمی که باید برداری.lrهمان ضریبِ تبدیل است، و چون هیچچیز در خودِ مسئله مقدارش را تعیین نمیکند، باید پیدایش کنی. برای همین این یک عدد اینقدر تعیینکننده است.
۲. آزمایشِ کوتاه: بدونِ آموزشِ کامل#
جاروبِ بالا پنج آموزشِ کامل خواست. راهِ خیلی ارزانترش این است: هر نرخ را فقط چند ده قدم اجرا کن و ببین loss چه میکند.
for lr in [1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1.0]:
model = plain()
opt = torch.optim.Adam(model.parameters(), lr=lr)
loader = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True)
model.train()
losses = []
for i, (xb, yb) in enumerate(loader):
opt.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
opt.step()
losses.append(loss.item())
if i == 16:
break
print(f"lr={lr:<7}: loss اول={losses[0]:.3f} | بعد از ۱۷ قدم={losses[-1]:.3f}")
lr=1e-05 : loss اول=2.324 | بعد از ۱۷ قدم=2.304
lr=0.0001 : loss اول=2.324 | بعد از ۱۷ قدم=2.291
lr=0.001 : loss اول=2.324 | بعد از ۱۷ قدم=2.134
lr=0.01 : loss اول=2.324 | بعد از ۱۷ قدم=0.752
lr=0.1 : loss اول=2.324 | بعد از ۱۷ قدم=0.728
lr=1.0 : loss اول=2.324 | بعد از ۱۷ قدم=3.098
هفده قدم — یعنی کمتر از یک epoch — و کلِ تصویر پیداست:
- تا
0.001،lossتقریباً تکان نمیخورد. خیلی کند. 0.01و0.1سریع پایین میآورند. ناحیهٔ مفید همینجاست.1.0بالاتر از نقطهٔ شروع رفت. ناپایدار.
قاعدهٔ عملی: بزرگترین نرخی را بردار که هنوز loss را سریع پایین میآورد، و بعد یک پله کوچکترش کن. اینجا یعنی جایی حولِ ۰٫۰۱ تا ۰٫۰۳ — و جاروبِ کاملِ بخشِ ۱ همین را تأیید کرد.
📏 اندازه بگیر: این آزمایش چند ثانیه طول میکشد و جاروبِ کامل چند دقیقه. روی یک مدلِ واقعی، همین نسبت میشود چند دقیقه در برابرِ چند روز. قبل از هر آموزشِ طولانی، این آزمایشِ کوتاه را بکن — و اگر
lossدر بیست قدمِ اول تکان نخورد، ادامه دادن هدر دادنِ وقت است.
۳. نرخ لازم نیست ثابت بماند#
def fit_scheduled(use_scheduler, epochs=30, lr=0.05):
torch.manual_seed(0)
model = plain()
loader = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True)
opt = torch.optim.Adam(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs) if use_scheduler else None
for _ in range(epochs):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
if scheduler:
scheduler.step()
return accuracy(model, Xva, yva), opt.param_groups[0]["lr"]
acc_fixed, lr_fixed = fit_scheduled(False)
acc_sched, lr_sched = fit_scheduled(True)
print(f"نرخ ثابت : دقت={acc_fixed:.4f} | نرخ پایانی={lr_fixed:.5f}")
print(f"با scheduler: دقت={acc_sched:.4f} | نرخ پایانی={lr_sched:.5f}")
نرخ ثابت : دقت=0.9638 | نرخ پایانی=0.05000
با scheduler: دقت=0.9861 | نرخ پایانی=0.00000
✅ چک کن: نرخِ پایانیِ ستونِ دوم باید عملاً صفر باشد. اگر همان ۰٫۰۵ اولیه ماند،
scheduler.step()صدا زده نشده — وschedulerبیصدا هیچ کاری نکرده است.
۰٫۹۶۳۸ به ۰٫۹۸۶۱ — بیش از دو درصد، از یک خط کد.
منطقش شهودی است: در ابتدا از جواب دوری و قدمِ بزرگ لازم داری؛ در انتها نزدیکی و قدمِ بزرگ فقط دورِ جواب میپراند. scheduler نرخ را در طولِ آموزش کم میکند.
opt = torch.optim.SGD([torch.zeros(1, requires_grad=True)], lr=0.1)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=10)
trace = []
for _ in range(10):
trace.append(round(opt.param_groups[0]["lr"], 4))
opt.step()
scheduler.step()
print("نرخ در ده epoch:", trace)
نرخ در ده epoch: [0.1, 0.0976, 0.0905, 0.0794, 0.0655, 0.05, 0.0345, 0.0206, 0.0095, 0.0024]
اول آرام کم میشود، وسط تندتر، آخر دوباره آرام — شکلِ نیمِ یک کسینوس. CosineAnnealing امروز رایجترین انتخاب است؛ گزینههای دیگر StepLR (پلهای) و ReduceLROnPlateau (وقتی اعتبارسنجی بهتر نشد، نصف کن) هستند.
⚠️ مواظب باش:
scheduler.step()جای مشخصی دارد و جابهجا کردنش خطای خاموش میدهد.CosineAnnealingLRیک بار در هرepochصدا زده میشود، نه در هرbatch— اگر داخلِ حلقهٔbatchبگذاریاش، نرخ دهها برابر سریعتر صفر میشود و آموزشت وسطِ راه متوقف میشود بدونِ اینکه بفهمی چرا. و همیشه بعد ازopt.step()، نه قبلش.
۴. warmup: و یک نتیجهٔ صادقانه#
warmup یعنی نرخ را از یک مقدارِ خیلی کوچک شروع کنی و در چند epoch اول بهتدریج به مقدارِ اصلی برسانی. در آموزشِ مدلهای بزرگ تقریباً استاندارد است. ببینیم اینجا چه میکند:
def fit_warmup(use_warmup, epochs=30, base=0.05):
torch.manual_seed(0)
model = plain()
loader = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True)
opt = torch.optim.Adam(model.parameters(), lr=base)
if use_warmup:
scheduler = torch.optim.lr_scheduler.SequentialLR(
opt,
[torch.optim.lr_scheduler.LinearLR(opt, start_factor=0.05, total_iters=5),
torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs - 5)],
milestones=[5],
)
else:
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
for _ in range(epochs):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
scheduler.step()
return accuracy(model, Xva, yva)
print("بدون warmup:", round(fit_warmup(False), 4))
print("با warmup :", round(fit_warmup(True), 4))
بدون warmup: 0.9861
با warmup : 0.9805
warmup نتیجه را بدتر کرد.
و این را همینطور صریح مینویسیم، چون درسِ اصلیِ این بخش همین است. warmup تکنیکِ بیارزشی نیست — مشکلی را حل میکند که این مسئله ندارد: در مدلهای خیلی بزرگ با batch بزرگ، قدمهای اولِ آموزش میتوانند وزنها را به ناحیهٔ بدی ببرند و دیگر برنگردند. مدلِ ما دو لایه دارد و چنین شکنندگیای ندارد.
📏 اندازه بگیر: هر تکنیکی مسئلهای را حل میکند؛ اگر آن مسئله را نداری، حل کردنش فقط هزینه است. و راهِ فهمیدنش هم آزمایش است، نه خواندنِ توصیه. قاعدهٔ کاریِ این ترم: هیچ تکنیکی را چون «استاندارد است» به کدت اضافه نکن، مگر اینکه روی مسئلهٔ خودت اندازه گرفته باشی که کمک میکند. فصلِ ۷ روشِ منظمِ این اندازهگیری را میدهد.
۵. ترتیبِ تنظیم#
| اولویت | چه چیزی | چطور |
|---|---|---|
| ۱ | نرخِ یادگیری | آزمایشِ کوتاهِ بخشِ ۲، بعد جاروبِ لگاریتمی |
| ۲ | تعدادِ epoch |
با توقفِ زودهنگام خودکارش کن |
| ۳ | scheduler |
CosineAnnealing پیشفرضِ خوبی است |
| ۴ | batch_size |
معمولاً بزرگترین چیزی که در حافظه جا شود |
| ۵ | معماری و regularization |
فصلهای ۱ و ۲ |
و یک رابطهٔ عملی که خیلی به کارت میآید: اگر batch_size را دو برابر کردی، نرخ را هم باید بزرگتر کنی — چون گرادیانِ میانگینگیریشده روی batch بزرگتر، کمنویزتر و کوچکتر است. قاعدهٔ سرانگشتی: نرخ را با همان نسبت (یا جذرش) بالا ببر، و بعد بسنج.
🔧 اگر کار نکرد:
schedulerهیچ اثری ندارد —scheduler.step()را صدا نزدهای، یاoptimizerرا دوباره ساختهای. نرخ زودتر از انتظار صفر شد —T_maxرا با تعدادِepochهماهنگ نکردهای، یاstep()را در حلقهٔbatchگذاشتهای. دقت بینِ اجراها خیلی فرق میکند —seedجا مانده، و فصلِ بعد دقیقاً دربارهٔ همین است.
🤖 از دستیارت بپرس: «
ReduceLROnPlateauچطور کار میکند و چه فرقی باCosineAnnealingLRدارد؟» بعد جایگزینش کن و روی همین مسئله بسنج. و این سؤالِ سختتر را هم بپرس: «چراReduceLROnPlateauبه مجموعهٔ اعتبارسنجی نیاز دارد ولیCosineAnnealingLRنه؟» — جوابش یک تفاوتِ مفهومیِ مهم است: یکی از داده تصمیم میگیرد و دیگری فقط از زمان.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| learning rate schedule | لرنینگ ریت شِدول | برنامهٔ تغییرِ نرخ در طولِ آموزش |
| cosine annealing | کساین انیلینگ | کم کردنِ نرخ با شکلِ نیمکسینوس |
| warmup | وارمآپ | شروع با نرخِ کوچک و رسیدنِ تدریجی به نرخِ اصلی |
| LR range test | الآر رنج تست | آزمایشِ کوتاه برای یافتنِ بازهٔ مفیدِ نرخ |
| plateau | پلاتو | جایی که معیار دیگر بهتر نمیشود |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
سه فصل دربارهٔ مهار و تنظیم بود. فصلِ بعد سراغِ ریشهٔ مشکل میرود: داده کم است. و راهی میدهد که بدونِ جمع کردنِ دادهٔ تازه، دادهٔ موجود را چند برابر کنی — بهشرطی که بدانی مرزِ «افزایشِ صادقانه» و «تقلب» کجاست.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.