ژرفا — خودآموز یادگیری عمیق و مهندسی هوش مصنوعی (متوسط)

فصل ۲ از ۹

پیشرفت ترم
۰٪

ترم ۱ · مکانیزمِ یادگیری: PyTorch از پایه

`autograd`: مشتق را ماشین حساب می‌کند

فصل ۲پیش‌نمایش رایگان
۹ دقیقه مطالعه فصل ۲

در این فصل چه یاد می‌گیری#

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

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ی — و می‌بینی نرخِ یادگیری چطور تفاوتِ «همگرا شد» و «منفجر شد» را می‌سازد.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.