در این فصل چه یاد میگیری#
یادگیری یعنی «پارامتر را کمی عوض کن تا خطا کمتر شود» — و برای دانستنِ اینکه در چه جهتی عوضش کنی، به مشتق نیاز داری. برای یک تابعِ ساده با دست حسابش میکنی؛ برای شبکهای با میلیونها پارامتر محال است.
autograd این کار را برایت میکند. در این فصل میبینی دقیقاً چطور — و مهمتر، سه رفتارش را که اگر ندانی، مدلت بیصدا آموزش نمیبیند: انباشتِ گرادیان، گرافی که بعد از یک بار مصرف میشود، و جایی که باید ردیابی را خاموش کنی.

آخر این فصل میتوانی:
- با
requires_gradو.backward()مشتق بگیری - بگویی گرافِ محاسباتی چیست و چه وقتی ساخته میشود
- بگویی چرا
zero_gradاجباری است no_gradوdetachرا در جای درست بهکار ببری
قبل از شروع#
از فصل ۱: tensor، shape، dtype.
از ریشه ترمِ ۸: مشتق و قاعدهٔ زنجیرهای — پایهٔ کلِ این فصل.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. اولین مشتقِ خودکار#
import torch
x = torch.tensor(3.0, requires_grad=True)
y = x ** 2
y.backward()
print("x =", x.item())
print("y = x^2 =", y.item())
print("dy/dx =", x.grad.item())
x = 3.0
y = x^2 = 9.0
dy/dx = 6.0
مشتقِ برابرِ است و در میشود ۶. PyTorch دقیقاً همان را داد، بدونِ اینکه فرمولِ مشتق را جایی نوشته باشیم.
سه خط، سه کارِ متفاوت:
requires_grad=Trueیعنی «حواست به این tensor باشد». بدونِ آن هیچ مشتقی گرفته نمیشود.y = x ** 2نهفقط حساب کرد، بلکه یادداشت کرد که چه کاری رویxانجام شد..backward()آن یادداشت را از آخر به اول پیمود و مشتق را درx.gradگذاشت.
🌱 ریشهاش کجاست: «مشتق در یک نقطه» دقیقاً همان چیزی است که ریشه، ترم ۸ فصل ۳ از پایه میسازد — شیبِ منحنی در یک نقطه، نه شیبِ متوسط. اگر عددِ ۶ برایت فقط «فرمول» است و نه «شیب»، همان فصل تصویرش را میدهد.
۲. چند متغیر، یک backward#
a = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(5.0, requires_grad=True)
out = a * b + a ** 2
out.backward()
print("out =", out.item())
print("d/da =", a.grad.item(), "(انتظار: b + 2a = 9)")
print("d/db =", b.grad.item(), "(انتظار: a = 2)")
out = 14.0
d/da = 9.0 (انتظار: b + 2a = 9)
d/db = 2.0 (انتظار: a = 2)
یک backward()، مشتقِ نسبت به هر دو متغیر. و دقیقاً همین است که در یک شبکه با میلیونها پارامتر اتفاق میافتد: یک فراخوانی، و گرادیانِ همهٔ پارامترها پر میشود.
اسمِ رسمیِ این کار backpropagation است، و هیچ چیزِ تازهای نیست: فقط قاعدهٔ زنجیرهای، اعمالشده از خروجی به سمتِ ورودی.
🌱 ریشهاش کجاست: ریشه، ترم ۸ فصل ۵ — قاعدهٔ زنجیرهای این را از پایه میسازد، و همانجا هم میگوید که این قاعده «ریشهٔ
backpropagation» است. اگر یک روز خواستی بفهمی چرا شبکهٔ خیلی عمیق سخت آموزش میبیند، جوابش در ضربِ پیاپیِ همان مشتقهای میانی است — و آنجا با چشمِ خودت میبینیاش.
۳. تلهٔ اول: گرادیان انباشته میشود#
w = torch.tensor(1.0, requires_grad=True)
for step in range(3):
loss = (w - 4.0) ** 2
loss.backward()
print(f"قدم {step + 1}: grad = {w.grad.item()}")
قدم 1: grad = -6.0
قدم 2: grad = -12.0
قدم 3: grad = -18.0
w اصلاً عوض نشد، ولی گرادیان سه برابر شد.
دلیلش این است که .backward() گرادیانِ تازه را به .grad اضافه میکند، نه اینکه جایگزینش کند. مقدارِ درست در هر سه قدم ۶- بود؛ چیزی که دیدیم جمعِ انباشته است.
راهِ حل یک خط است:
w = torch.tensor(1.0, requires_grad=True)
for step in range(3):
loss = (w - 4.0) ** 2
loss.backward()
print(f"قدم {step + 1}: grad = {w.grad.item()}")
w.grad.zero_()
قدم 1: grad = -6.0
قدم 2: grad = -6.0
قدم 3: grad = -6.0
✅ چک کن: حالا هر سه عدد باید ۶- باشند. اگر هنوز ۶- و ۱۲- و ۱۸- میبینی،
w.grad.zero_()یا بیرونِ حلقه افتاده یا اصلاً اجرا نشده. جای درستش دقیقاً همان است که در کدِ بالا هست: داخلِ حلقه، بعد از اینکه گرادیان را خواندی و قبل از دورِ بعد.
⚠️ مواظب باش: فراموش کردنِ
zero_gradرایجترین باگِ خاموشِ کلِ این دوره است. هیچ خطایی نمیگیری؛ فقط گرادیانها بزرگ و بزرگتر میشوند، قدمها بیقواره میشوند، وlossیا بالا میرود یاnanمیشود. در فصلِ ۸ همین را در فهرستِ «چراlossپایین نمیآید» دوباره میبینی — چون واقعاً اینقدر رایج است.
و چرا PyTorch اینطور طراحی شده؟ چون گاهی واقعاً میخواهی گرادیانِ چند batch را جمع کنی و بعد یک قدم برداری — ترفندی که وقتی حافظه کم است بهکار میآید. این انتخاب است، نه اشتباه؛ ولی پیشفرضش برخلافِ شهودِ توست.
۴. گراف: چیزی که موقعِ محاسبه ساخته میشود#
p = torch.tensor([1.0, 2.0], requires_grad=True)
q = (p ** 2).sum()
q.backward()
print("p.grad =", p.grad)
print("requires_grad دارد؟", p.requires_grad, "| برگ است؟", p.is_leaf)
print("q.grad_fn:", type(q.grad_fn).__name__)
p.grad = tensor([2., 4.])
requires_grad دارد؟ True | برگ است؟ True
q.grad_fn: SumBackward0
p یک برگ است: خودت ساختیاش و چیزی قبلش نیست. q نتیجهٔ محاسبه است و یک grad_fn دارد — یعنی میداند از کجا آمده.
اینکه آخرین عملیات sum بود در نامِ SumBackward0 پیداست. PyTorch با همین اشارهگرها یک زنجیرهٔ کامل از خروجی تا ورودی نگه میدارد، و .backward() همان زنجیره را برعکس میپیماید.
نکتهٔ کلیدی: این گراف در همان لحظهٔ اجرای کد ساخته میشود، نه از قبل. یعنی میتوانی داخلِ forward مدلت if و حلقه بگذاری و همهچیز کار کند. این همان چیزی است که PyTorch را برای آزمایشکردن راحت میکند — و دلیلِ اصلیِ اینکه پژوهش با آن انجام میشود.
۵. تلهٔ دوم: گراف بعد از یک بار مصرف میشود#
z = torch.tensor(2.0, requires_grad=True)
out = z ** 3
try:
out.backward()
out.backward()
except RuntimeError as error:
print("RuntimeError:", str(error)[:70])
RuntimeError: Trying to backward through the graph a second time (or directly access
backward() بعد از کارش گراف را آزاد میکند تا حافظه اشغال نماند.
در عمل این تقریباً هیچوقت مشکل نمیسازد، چون در حلقهٔ آموزش هر بار loss را از نو حساب میکنی و گرافِ تازهای ساخته میشود. ولی اگر این خطا را دیدی، معنایش دقیقاً یکی است: داری روی یک loss که قبلاً backward شده دوباره backward میزنی. معمولاً چون محاسبهٔ loss را اشتباهی بیرونِ حلقه گذاشتهای.
۶. no_grad و detach: خاموش کردنِ ردیابی#
m = torch.tensor(3.0, requires_grad=True)
with torch.no_grad():
n = m * 2
print("n.requires_grad:", n.requires_grad)
k = m * 2
print("k.requires_grad:", k.requires_grad)
print("detach:", (m * 2).detach().requires_grad)
n.requires_grad: False
k.requires_grad: True
detach: False
| ابزار | چه میکند | کِی بهکارش میبری |
|---|---|---|
torch.no_grad() |
داخلِ بلوکش هیچ گرافی ساخته نمیشود | ارزیابی، پیشبینی، بهروزرسانیِ دستیِ وزنها |
.detach() |
یک tensor را از گراف جدا میکند | نگهداشتنِ یک مقدار برای گزارش یا نمودار |
requires_grad_(False) |
ردیابیِ یک پارامتر را خاموش میکند | freeze کردنِ لایهها (ترمِ ۳ فصلِ ۵) |
اهمیتش فقط سرعت نیست، درستی هم هست. وقتی در فصلِ بعد وزنها را با دست بهروز میکنی، آن بهروزرسانی نباید بخشی از گراف باشد — وگرنه PyTorch سعی میکند از خودِ بهروزرسانی هم مشتق بگیرد و همهچیز بههم میریزد.
🔧 اگر کار نکرد: سه خطای رایجِ همین فصل.
element 0 of tensors does not require grad:requires_grad=Trueرا جا انداختهای، یا داخلِno_gradمحاسبه کردهای.grad can be implicitly created only for scalar outputs: روی چیزیbackwardزدهای که یک عدد نیست — رویlossبزن، نه روی خروجیِ برداریِ مدل..gradبرابرِNoneاست: یاbackwardهنوز صدا زده نشده، یا آن tensor برگ نیست.
🤖 از دستیارت بپرس: «چرا PyTorch گرادیانها را انباشته میکند بهجای اینکه جایگزین کند؟» جوابش را بگیر و بعد از خودت بپرس در چه سناریویی این رفتار واقعاً به کار میآید. (راهنمایی: وقتی
batchبزرگ در حافظه جا نمیشود.) این عادت — «پیشفرضِ عجیب را نه بهعنوان باگ، بلکه بهعنوان یک تصمیم بخوان» — در کلِ کار با کتابخانههای سطحِ پایین به دردت میخورد.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| autograd | اتوگرد | مشتقگیریِ خودکار از روی محاسباتِ انجامشده |
| requires_grad | ریکوایرز گرد | «حواست به این tensor باشد» |
| backward | بکورد | پیمایشِ گراف از خروجی به ورودی و پر کردنِ .grad |
| computational graph | کامپیوتیشنال گراف | یادداشتِ اینکه هر مقدار از کجا آمده |
| leaf tensor | لیف تنسور | tensorی که خودت ساختی، نه نتیجهٔ محاسبه |
| no_grad | نو-گرد | خاموش کردنِ موقتِ ساختِ گراف |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
مشتق را داری. حالا کاری که با مشتق باید بکنی: قدم برداشتن در جهتِ کاهشِ خطا. حلقهٔ gradient descent را با دست مینویسی — پنج خط، بدونِ هیچ optimizerی — و میبینی نرخِ یادگیری چطور تفاوتِ «همگرا شد» و «منفجر شد» را میسازد.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.