در این فصل چه یاد میگیری#
در ترمِ ۱ فصلِ ۸ دیدی که دادهٔ نرمالنشده همان مدل را از خطای ۰٫۶۴ به ۵۱۱ میبرد. ولی آنجا نگفتیم چرا و کِی. این فصل جوابش را میدهد — و نشان میدهد که قاعده آنطور که معمولاً گفته میشود ساده نیست.
بعد یک قدم جلوتر میرویم: نرمالسازی داخلِ خودِ شبکه، با BatchNorm و LayerNorm. و با عدد میبینی که در یک شبکهٔ ششلایه، بدونشان سیگنال در عمق آب میرود.

آخر این فصل میتوانی:
- بگویی نرمالسازیِ ورودی کِی حیاتی است و کِی تقریباً بیاثر
BatchNormوLayerNormرا در جای درست بهکار ببری- بزرگیِ فعالسازیها را در عمق اندازه بگیری
- بگویی مقداردهیِ اولیهٔ وزنها چرا اهمیت دارد
قبل از شروع#
از ترمِ ۱ فصل ۸: اثرِ دادهٔ نرمالنشده.
از فصل ۱ همین ترم: accuracy و سنجش با مجموعهٔ اعتبارسنجی.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. کِی نرمالسازیِ ورودی حیاتی است#
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
@torch.no_grad()
def accuracy(model, X, y):
model.eval()
return float((model(X).argmax(1) == y).float().mean())
def plain(seed=0):
torch.manual_seed(seed)
return nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 10))
def fit(model, X, y, Xv, epochs=15, lr=0.01):
"""عمداً SGD، نه Adam — دلیلش پایینِ همین بخش."""
loader = DataLoader(TensorDataset(X, y), batch_size=64, shuffle=True)
opt = torch.optim.SGD(model.parameters(), lr=lr)
loss_fn = nn.CrossEntropyLoss()
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, Xv, yva)
loud = int(Xtr.std(0).argmax()) # پرنوسانترین پیکسل
skew = torch.ones(64)
skew[loud] = 500.0 # فقط همان یکی را ۵۰۰ برابر کن
Xs, Xsv = Xtr * skew, Xva * skew
mu, sd = Xs.mean(0), Xs.std(0).clamp_min(1e-6)
mu2, sd2 = Xtr.mean(0), Xtr.std(0).clamp_min(1e-6)
print("پرنوسانترین ستون:", loud)
print("همگون، بدون نرمالسازی:", round(fit(plain(), Xtr, ytr, Xva), 4))
print("همگون، استانداردشده :", round(fit(plain(), (Xtr - mu2) / sd2, ytr, (Xva - mu2) / sd2), 4))
print("ناهمگون، بدون نرمالسازی:", round(fit(plain(), Xs, ytr, Xsv), 4))
print("ناهمگون، استانداردشده :", round(fit(plain(), (Xs - mu) / sd, ytr, (Xsv - mu) / sd), 4))
پرنوسانترین ستون: 42
همگون، بدون نرمالسازی: 0.7214
همگون، استانداردشده : 0.8217
ناهمگون، بدون نرمالسازی: 0.2423
ناهمگون، استانداردشده : 0.8217
چهار عدد، و سه چیزِ مهم در آنها:
- وقتی همهٔ ستونها هممقیاساند، نرمالسازی کمک میکند ولی حیاتی نیست — ۰٫۷۲ به ۰٫۸۲.
- وقتی فقط یک ستون ۵۰۰ برابر بزرگتر است، بدونِ نرمالسازی همهچیز فرو میریزد — ۰٫۲۴، یعنی کمی بهتر از حدسِ تصادفی بینِ ده کلاس.
- و مهمترینش: دو عددِ استانداردشده دقیقاً یکیاند. ۰٫۸۲۱۷ در هر دو سطر. استانداردسازی تفاوتِ مقیاس را کاملاً پاک کرد — انگار آن ستونِ ۵۰۰ برابری اصلاً وجود نداشت.
✅ چک کن: دو عددِ «استانداردشده» باید دقیقاً یکی باشند. اگر نبودند، میانگین و انحراف را از دادهٔ اشتباهی گرفتهای — باید از همان مجموعهای باشد که مدل رویش آموزش میبیند، و همان را روی اعتبارسنجی هم بهکار ببری. گرفتنِ آمار از کلِ داده، نشتی است.
پس قاعدهٔ درست این نیست که «همیشه نرمال کن چون خوب است». قاعدهٔ درست این است: نرمالسازی مدل را به مقیاسِ ورودیها بیاعتنا میکند — و هرچه ورودیهایت ناهمگونتر باشند، این بیاعتنایی حیاتیتر است.
🌱 ریشهاش کجاست: کلِ این فصل روی دو عدد میچرخد — میانگین و انحرافِ معیار — و ریشه، ترم ۶ فصل ۶ — میانگین، میانه، پراکندگی هر دو را از پایه میسازد. نکتهٔ کلیدی همانجاست: انحرافِ معیار «فاصلهٔ متوسطِ دادهها از میانگین» است. پس یعنی «این نقطه چند برابرِ پراکندگیِ معمول از مرکز فاصله دارد» — و همین است که واحد را از معادله بیرون میاندازد و سال و گیگابایت را قابلِ مقایسه میکند.
⚠️ مواظب باش: آن کامنتِ «عمداً SGD» را جدی بگیر.
Adamبرای هر پارامتر نرخِ خودش را تنظیم میکند و بخشِ بزرگی از این مشکل را پنهان میکند — باAdamهمان آزمایش تفاوتِ خیلی کمتری نشان میدهد. این یعنیAdamتو را از نرمالسازی بینیاز نمیکند، فقط علامتش را کمرنگ میکند تا وقتی که در جای دیگری به دردسر بخوری.
۲. سیگنال در عمق آب میرود#
def deep(bn=False, ln=False, depth=6, width=128, seed=0):
torch.manual_seed(seed)
layers = [nn.Linear(64, width)]
for _ in range(depth - 1):
if bn:
layers.append(nn.BatchNorm1d(width))
if ln:
layers.append(nn.LayerNorm(width))
layers += [nn.ReLU(), nn.Linear(width, width)]
return nn.Sequential(*layers, nn.Linear(width, 10))
for label, net in [("بدون BatchNorm", deep()), ("با BatchNorm ", deep(bn=True))]:
net.train()
h = Xtr[:64]
sizes = []
with torch.no_grad():
for layer in net:
h = layer(h)
if isinstance(layer, nn.Linear):
sizes.append(round(float(h.std()), 3))
print(f"{label}:", sizes)
بدون BatchNorm: [0.269, 0.117, 0.072, 0.06, 0.056, 0.056, 0.051]
با BatchNorm : [0.269, 0.401, 0.41, 0.404, 0.407, 0.409, 0.242]
این هفت عدد کلِ دلیلِ وجودِ BatchNorm را نشان میدهند.
بدونِ آن، انحرافِ معیارِ خروجیِ هر لایه از ۰٫۲۶۹ به ۰٫۰۵۱ میرسد — یعنی سیگنال در شش لایه به یکپنجم آب میرود. و چون گرادیانها از همین مسیر برمیگردند، گرادیانِ لایههای اول بهشدت کوچک میشود و آن لایهها عملاً آموزش نمیبینند.
با BatchNorm، همان عدد در کلِ عمق حولِ ۰٫۴ ثابت میماند.
کارِ BatchNorm دقیقاً همین است: خروجیِ هر لایه را میگیرد، میانگینش را صفر و انحرافش را یک میکند، و بعد با دو پارامترِ آموختنی دوباره مقیاسش را تنظیم میکند. نه فقط ورودیِ شبکه، بلکه ورودیِ هر لایه.
for name, model in [("بدون نرمالسازی", deep()), ("BatchNorm", deep(bn=True)), ("LayerNorm", deep(ln=True))]:
loader = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True)
opt = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
for _ in range(15):
model.train()
for xb, yb in loader:
opt.zero_grad()
loss_fn(model(xb), yb).backward()
opt.step()
print(f"{name:>16}: دقت اعتبارسنجی = {accuracy(model, Xva, yva):.4f}")
بدون نرمالسازی: دقت اعتبارسنجی = 0.9081
BatchNorm: دقت اعتبارسنجی = 0.9610
LayerNorm: دقت اعتبارسنجی = 0.9526
پنج درصد بهبود، فقط از افزودنِ لایههای نرمالسازی. همان معماری، همان seed، همان تعدادِ epoch.
۳. BatchNorm در برابرِ LayerNorm#
BatchNorm |
LayerNorm |
|
|---|---|---|
| روی چه چیزی میانگین میگیرد | هر ویژگی، در طولِ batch |
هر نمونه، در طولِ ویژگیها |
به batch_size وابسته است |
بله — با batch کوچک بیثبات |
نه |
رفتارش در train و eval |
متفاوت | یکسان |
| کجا رایج است | بینایی (CNN) |
زبان (transformer) |
آن ردیفِ سوم دامِ عملیِ BatchNorm است. در آموزش، میانگین و انحرافِ همان batch را بهکار میبرد؛ در ارزیابی، میانگینِ متحرکی که در طولِ آموزش جمع کرده. پس اگر model.eval() را جا بیندازی، عددِ ارزیابیات نهفقط بد، بلکه به batch_size هم وابسته میشود — و همین باعث میشود باگ ظاهراً «تصادفی» بهنظر برسد.
و ردیفِ چهارم دلیلِ تاریخی دارد: transformerها با دنبالههایی به طولهای متفاوت کار میکنند و batchشان اغلب کوچک است. LayerNorm که به batch کاری ندارد، آنجا انتخابِ طبیعی است — و در ترمِ ۴ دوباره میبینیاش.
۴. مقداردهیِ اولیهٔ وزنها#
torch.manual_seed(0)
default_layer = nn.Linear(64, 128)
print("پیشفرض PyTorch: انحراف معیار وزن =", round(float(default_layer.weight.std()), 4))
def with_init(bad=False, seed=0):
torch.manual_seed(seed)
model = nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 10))
if bad:
for layer in model:
if isinstance(layer, nn.Linear):
nn.init.normal_(layer.weight, std=1.0)
return model
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)
loss_fn = nn.CrossEntropyLoss()
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)
print("پیشفرض:", round(fit_adam(with_init(False)), 4))
print("بد :", round(fit_adam(with_init(True)), 4))
پیشفرض PyTorch: انحراف معیار وزن = 0.0719
پیشفرض: 0.9777
بد : 0.9276
پیشفرضِ PyTorch وزنها را حولِ ۰٫۰۷ میسازد، نه ۱. این عدد تصادفی نیست: از تعدادِ ورودیهای لایه حساب میشود، دقیقاً به این هدف که بزرگیِ سیگنال در عبور از لایه ثابت بماند — همان چیزی که در بخشِ ۲ اندازه گرفتیم.
با مقداردهیِ std=1، وزنها حدودِ چهارده برابر بزرگترند و نتیجه پنج درصد بدتر شد. در شبکهٔ عمیقتر این تفاوت به «اصلاً آموزش نمیبیند» میرسد.
📏 اندازه بگیر: خبرِ خوب: پیشفرضهای PyTorch امروز خوباند و در بیشترِ کارها لازم نیست دستشان بزنی. ولی اگر روزی شبکهای ساختی که
lossش از همان قدمِ اول ثابت مانده یاnanشده، مقداردهیِ اولیه یکی از سه چیزی است که باید چک کنی — کنارِ نرخِ یادگیری و نرمالسازیِ ورودی. و راهِ چکش همان کدِ بخشِ ۲ است: انحرافِ معیارِ خروجیِ هر لایه را چاپ کن.
🔧 اگر کار نکرد:
BatchNorm1dباbatchتکنمونهای خطا میدهد (Expected more than 1 value per channel) — چون نمیتواند از یک نمونه انحرافِ معیار بگیرد؛ یاbatchرا بزرگتر کن یاdrop_last=Trueبگذار. دقتِ ارزیابی باBatchNormعجیب پایین است —model.eval()جا مانده.LayerNormخطای شکل میدهد — بُعدِ آخر را میخواهد، نه تعدادِ کانالها.
🤖 از دستیارت بپرس: «
BatchNormرا قبل ازReLUبگذارم یا بعدش؟» جوابِ قطعی وجود ندارد و همین جالب است — مقالهٔ اصلی یکجور گفته و عملِ رایج جورِ دیگر. جوابش را بگیر و بعد هر دو را در همین چارچوب امتحان کن؛ تفاوتشان روی این داده احتمالاً کوچک است، و همان کوچک بودن هم یک نتیجهٔ معتبر است.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| standardization | استانداردایزیشن | میانگین صفر و انحرافِ معیار یک کردن |
| BatchNorm | بچنرم | نرمالسازیِ هر ویژگی در طولِ batch |
| LayerNorm | لیرنرم | نرمالسازیِ هر نمونه در طولِ ویژگیها |
| initialization | اینیشیالایزیشن | مقدارِ اولیهٔ وزنها قبل از آموزش |
| vanishing signal | ونیشینگ سیگنال | کوچک شدنِ فعالسازیها در عمق |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
نرمالسازی، مسیرِ سیگنال را هموار کرد. حالا سراغِ عددی میرویم که از همهٔ hyperparameterهای دیگر مهمتر است: نرخِ یادگیری. راهِ سیستماتیکِ پیدا کردنش، و scheduler — که با یک خط کد، دقت را از ۰٫۹۶۳۸ به ۰٫۹۸۶۱ میبرد.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.