در این فصل چه یاد میگیری#
در فصلِ قبل یک torch.relu وسطِ forward گذاشتیم و گفتیم بعداً توضیحش میدهیم. این فصل همان بعداً است — و ادعایش تند است: شبکهای با صد لایه و بدونِ تابعِ فعالسازی، دقیقاً بهاندازهٔ یک لایه توانایی دارد.
این را نه با استدلال، بلکه با کد ثابت میکنیم: دو لایهٔ پشتِسرِهم را میسازیم و یک لایهٔ معادلش را، و میبینیم عددهایشان یکی است.

آخر این فصل میتوانی:
- بگویی چرا ترکیبِ چند تبدیلِ خطی باز هم خطی است
ReLU،sigmoidوtanhرا بشناسی و بدانی کجا بهکار میروندsoftmaxرا بفهمی و بدانی چرا خروجیِ خام راlogitمیگویند- با عدد نشان بدهی که تابعِ فعالسازی چه چیزی اضافه میکند
قبل از شروع#
از فصل ۴: nn.Module، nn.Linear، و اینکه یک لایه فقط است.
از فصل ۳: حلقهٔ دستیِ gradient descent — در بخشِ ۵ همان را بهکار میبریم.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. اثبات: دو لایه = یک لایه#
import torch
import torch.nn as nn
torch.manual_seed(0)
a = nn.Linear(2, 4)
b = nn.Linear(4, 1)
x = torch.randn(3, 2)
stacked = b(a(x)) # دو لایه، پشتِ سرِ هم
W = b.weight @ a.weight # یک ماتریسِ معادل
bias = b(a.bias.unsqueeze(0)).squeeze(0) # و یک bias معادل
single = x @ W.T + bias
print("دو لایهٔ پشتسرهم:", stacked.detach().flatten())
print("یک لایهٔ معادل :", single.detach().flatten())
print("یکی هستند:", bool(torch.allclose(stacked, single, atol=1e-6)))
دو لایهٔ پشتسرهم: tensor([ 0.0330, -0.2449, 0.0089])
یک لایهٔ معادل : tensor([ 0.0330, -0.2449, 0.0089])
یکی هستند: True
✅ چک کن: خطِ آخر باید
Trueباشد. اگرFalseشد،atolرا کمی بزرگتر کن — این اختلافِ ریاضی نیست، خطای گردکردنِfloat32است و همان چیزی است که در بخشِ ۳ روی جمعِ softmax هم میبینی.
عددها یکیاند، تا شش رقمِ اعشار.
دلیلش ریاضیِ ساده است: اگر لایهٔ اول باشد و دومی ، حاصلشان میشود
و آن هم دقیقاً شکلِ یک لایهٔ خطیِ دیگر است، با ماتریسِ و بایاسِ جدید.
پس صد لایهٔ خطیِ پشتِ سرِ هم هم فقط یک ماتریسِ بزرگترند. عمق بدونِ تابعِ فعالسازی هیچ تواناییای اضافه نمیکند — فقط پارامترهای بیشتر برای یادگرفتنِ همان چیز.
🌱 ریشهاش کجاست: «ترکیبِ دو تبدیل باز هم یک تبدیل است» دقیقاً همان چیزی است که ریشه، ترم ۷ فصل ۵ اثباتش میکند — و آنجا میگوید ضربِ ماتریس برای همین اینطور تعریف شده. یعنی این محدودیتِ شبکههای عصبی نیست؛ یک واقعیتِ جبرِ خطی است که شبکهها باید از آن فرار کنند.
۲. راهِ فرار: یک تابعِ غیرخطی وسط#
v = torch.tensor([-2.0, -0.5, 0.0, 0.5, 2.0])
print("ورودی :", v)
print("ReLU :", torch.relu(v))
print("sigmoid:", torch.sigmoid(v).round(decimals=3))
print("tanh :", torch.tanh(v).round(decimals=3))
ورودی : tensor([-2.0000, -0.5000, 0.0000, 0.5000, 2.0000])
ReLU : tensor([0.0000, 0.0000, 0.0000, 0.5000, 2.0000])
sigmoid: tensor([0.1190, 0.3780, 0.5000, 0.6220, 0.8810])
tanh : tensor([-0.9640, -0.4620, 0.0000, 0.4620, 0.9640])
| تابع | چه میکند | بازه | کجا |
|---|---|---|---|
ReLU |
منفیها را صفر میکند | پیشفرضِ لایههای پنهان | |
sigmoid |
هر عددی را به بینِ ۰ و ۱ میبرد | خروجیِ طبقهبندیِ دودویی | |
tanh |
مثلِ sigmoid ولی حولِ صفر | گاهی در لایههای پنهان | |
softmax |
چند عدد را به یک توزیعِ احتمال | جمعشان ۱ | خروجیِ طبقهبندیِ چندکلاسه |
ReLU سادهترینشان است و بیشترین کاربرد را دارد — دقیقاً بهخاطرِ همان سادگی: مشتقش یا صفر است یا یک، پس محاسبهاش ارزان است و گرادیان در شبکهٔ عمیق کمتر آب میرود.
sigmoid و tanh برای عددهای بزرگ اشباع میشوند: مشتقشان تقریباً صفر میشود و یادگیری در آن ناحیه متوقف. همین یک دلیل است که شبکههای عمیقِ دههٔ ۲۰۰۰ سخت آموزش میدیدند و ReLU بخشِ بزرگی از حلش بود.
⚠️ مواظب باش:
ReLUدر مشتقِ ریاضیِ تعریفشده ندارد — چپ و راستش فرق دارند. PyTorch یک انتخابِ دلبخواهی کرده (صفر میگذارد) و در عمل هیچ مشکلی نمیسازد، چون احتمالِ افتادنِ دقیقاً روی صفر ناچیز است. ولی بدان که یک قرارداد است، نه یک حقیقتِ ریاضی — همان چیزی که در تمرینِ ستارهدارِ فصلِ ۲ دیدی.
۳. softmax و کلمهٔ logit#
logits = torch.tensor([2.0, 1.0, 0.1])
probs = torch.softmax(logits, dim=0)
print("logits :", logits)
print("softmax:", probs.round(decimals=4))
print("جمعشان :", probs.sum().item())
logits : tensor([2.0000, 1.0000, 0.1000])
softmax: tensor([0.6590, 0.2424, 0.0986])
جمعشان : 1.0000001192092896
softmax سه عددِ دلبخواه را به سه احتمال تبدیل کرد که جمعشان ۱ است. بزرگترین ورودی بزرگترین احتمال را گرفت، ولی نه بهطورِ خطی — اختلافِ ۲ و ۱ به نسبتِ ۰٫۶۵۹ به ۰٫۲۴۲ تبدیل شد، یعنی حدودِ ۲٫۷ برابر.
آن جمعِ 1.0000001 هم اشتباه نیست: float32 دقتِ محدود دارد و این خطای گردکردنِ طبیعی است. هیچوقت روی برابریِ دقیقِ اعداد اعشاری شرط نگذار؛ torch.allclose برای همین وجود دارد.
به خروجیِ خام قبل از softmax میگویند logit — و این کلمه در کلِ ادامهٔ دوره تکرار میشود.
big = torch.tensor([12.0, 11.0, 10.1])
print("softmax روی عددهای بزرگتر:", torch.softmax(big, dim=0).round(decimals=4))
print("همان اختلافها، همان جواب:", bool(torch.allclose(torch.softmax(big, dim=0), probs)))
softmax روی عددهای بزرگتر: tensor([0.6590, 0.2424, 0.0986])
همان اختلافها، همان جواب: True
ده واحد به همهٔ ورودیها اضافه کردیم و خروجی ذرهای عوض نشد. softmax فقط به اختلافها اهمیت میدهد، نه به مقدارِ مطلق.
این خاصیت در فصلِ بعد به دردت میخورد: چون خروجیِ مطلق مهم نیست، مدل لازم نیست عددهای معنادار بدهد و همان چیزی که میدهد کافی است.
🌱 ریشهاش کجاست:
softmaxاز تابعِ نمایی ساخته شده و رفتارِ عجیبش — اینکه اختلافِ کوچک در ورودی به اختلافِ بزرگ در خروجی تبدیل میشود — مستقیماً از شکلِ همان تابع میآید. ریشه، ترم ۴ فصل ۷ — رشدِ نمایی شهودش را میدهد.
۴. جای درستِ هر کدام#
# غلط: sigmoid روی خروجی، و بعد CrossEntropyLoss
out = torch.sigmoid(self.last_layer(x))
loss = nn.CrossEntropyLoss()(out, labels)
# درست: خروجیِ خام (logit) را بده؛ تابعِ خطا خودش softmax میزند
out = self.last_layer(x)
loss = nn.CrossEntropyLoss()(out, labels)
این یکی از رایجترین اشتباههای واقعی است و هیچ خطایی نمیدهد — فقط مدلت بدتر آموزش میبیند و تو نمیفهمی چرا. فصلِ بعد با عدد نشانش میدهد.
قاعدهٔ ساده: لایهٔ آخرِ مدلت هیچ تابعِ فعالسازیای ندارد. تابعِ خطا خودش هرچه لازم است اعمال میکند.
۵. حالا با عدد: چقدر فرق میکند#
میخواهیم یک شبکه را یاد بگیرد — منحنیای که قطعاً خطی نیست.
class NoAct(nn.Module):
def __init__(self):
super().__init__()
self.a = nn.Linear(1, 16)
self.b = nn.Linear(16, 1)
def forward(self, x):
return self.b(self.a(x))
class WithAct(nn.Module):
def __init__(self):
super().__init__()
self.a = nn.Linear(1, 16)
self.b = nn.Linear(16, 1)
def forward(self, x):
return self.b(torch.relu(self.a(x)))
X = torch.linspace(-3, 3, 100).unsqueeze(1)
Y = X ** 2
def fit(model_class, steps=3000, lr=0.02):
torch.manual_seed(0)
model = model_class()
for _ in range(steps):
loss = ((model(X) - Y) ** 2).mean()
loss.backward()
with torch.no_grad():
for p in model.parameters():
p -= lr * p.grad
p.grad.zero_()
return loss.item()
for name, cls in [("بدون فعالسازی", NoAct), ("با ReLU ", WithAct)]:
print(f"{name}: MSE نهایی = {fit(cls):.4f}")
بدون فعالسازی: MSE نهایی = 7.4916
با ReLU : MSE نهایی = 0.0125
۷٫۴۹ در برابرِ ۰٫۰۱ — تقریباً ششصد برابر. همان معماری، همان تعدادِ پارامتر، همان تعدادِ قدم، همان seed. تنها تفاوت یک torch.relu.
مدلِ بدونِ فعالسازی نمیتوانست بهتر شود، هرچقدر هم آموزش میدید: بهترین خطِ صافی که میشود روی سهمی کشید، همان است و بس.
📏 اندازه بگیر: این الگو را نگه دار: ادعا را با آزمایشِ کنترلشده بسنج، نه با استدلال. دو مدل که در همهچیز جز یک تفاوت یکساناند، تنها راهِ اثباتِ اثرِ آن تفاوت است. اسمِ رسمیاش
ablationاست و در ترمِ ۲ فصلِ ۷ بهعنوان یک روشِ کاری کاملاً بازش میکنیم. هر بار که کسی گفت «این بهترش میکند»، همین را از او بخواه.
🔧 اگر کار نکرد: اگر
lossهر دو مدلnanشد،lrرا نصف کن. اگر مدلِReLUهم بد ماند،reluرا درforwardجا انداختهای — چاپ کردنِ خودِ مدل کمکی نمیکند، چونtorch.reluلایه نیست و درprint(model)دیده نمیشود. این یکی از معدود جاهایی است که کد چیزی را نشان نمیدهد و بایدforwardرا با چشم بخوانی.
🤖 از دستیارت بپرس: «چه تابعهای فعالسازیِ دیگری هست و هرکدام چه مشکلی از
ReLUرا حل میکنند؟» (LeakyReLU،GELU،SiLUرا خواهی دید.) بعد یکیشان را درWithActجایگزین کن و همان آزمایشِ بخشِ ۵ را تکرار کن. اگر بهتر نشد، این هم یک نتیجهٔ معتبر است — و در ترمِ ۴ خواهی دید کهGELUدرtransformerها انتخابِ رایج است، به دلیلی که آنجا میبینی.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| activation function | اکتیویشن فانکشن | تابعِ غیرخطیِ بینِ لایهها |
| ReLU | رِلو | «منفی را صفر کن» |
| sigmoid / tanh | سیگموید / تانژانت هایپربولیک | فشرده کردن به یک بازهٔ محدود |
| softmax | سافتمکس | تبدیلِ چند عدد به توزیعِ احتمال |
| logit | لاجیت | خروجیِ خامِ مدل، قبل از softmax |
| saturation | سچوریشن | ناحیهای که مشتق تقریباً صفر میشود |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
سه قطعه از چهار قطعه را داری: داده، مدل، و حالا غیرخطیبودن. فصلِ بعد دو تای آخر را با نامِ رسمیشان میآورد: loss و optimizer — و همان چهار خطِ دستیِ فصلِ ۳ به یک خط تبدیل میشود، حالا که میدانی پشتش چه خبر است.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.