Məzmuna keç
Educora
Universitet22 dəq38 / 42

PyTorch: autograd və avtomatik törəmə

PyTorch törəmələri necə özü hesablayır: hesablama qrafı, requires_grad, backward() və .grad, zəncir qaydası, qradiyentlərin toplanması, torch.no_grad() və autograd ilə qradiyent enişi.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Hesablama qrafının və geri keçidin (backpropagation) necə işlədiyini izah etmək
  • requires_grad, .backward() və .grad ilə qradiyentləri hesablamaq və zəncir qaydası ilə əl ilə yoxlamaq
  • Qradiyentləri sıfırlamaq və torch.no_grad()-dan düzgün istifadə etmək
  • Autograd ilə qradiyent enişini sıfırdan yazmaq

Birinci dərsdə xətti reqressiyanın qradiyentlərini əl ilə çıxardıq — cəmi iki parametr üçün. Müasir neyron şəbəkələrdə isə milyonlarla, böyük dil modellərində milyardlarla parametr var; hər biri üçün törəməni kağızda çıxarmaq mümkün deyil. PyTorch-un autograd mexanizmi bu işi avtomatik görür: tenzorlarla etdiyin hər əməliyyatı yadda saxlayır, sonra isə bir əmrlə bütün parametrlərə görə dəqiq törəmələri hesablayır. Bu, nə təxmini ədədi fərqlər, nə də simvolik riyaziyyatdır — bu, avtomatik törəmədir (automatic differentiation).

Hesablama qrafı

requires_grad=True olan tenzorla hər əməliyyat istiqamətlənmiş dövrsüz qrafda (DAG) yeni düyün yaradır. Qrafın yarpaqları girişlər və parametrlərdir, kökü isə itkidir. İrəli keçid (forward pass) zamanı qraf qurulur və qiymətlər hesablanır; geri keçid (backward pass) zamanı autograd kökdən yarpaqlara doğru hərəkət edir və hər düyündə zəncir qaydasını tətbiq edir. Neyron şəbəkələrdə bu prosesə geriyə yayılma (backpropagation) deyilir.

Düyünİrəli keçidLokal törəməgrad_fn
uu = w · x∂u/∂w = xMulBackward0
ŷŷ = u + b∂ŷ/∂u = 1, ∂ŷ/∂b = 1AddBackward0
ee = ŷ − y∂e/∂ŷ = 1SubBackward0
LL = e²∂L/∂e = 2ePowBackward0
L = (w · x + b − y)² itkisinin qrafı. Geri keçiddə lokal törəmələr aşağıdan yuxarıya vurulur: ∂L/∂w = 2e · 1 · 1 · x.

PyTorch-da qraf dinamikdir (define-by-run): hər irəli keçiddə yenidən qurulur. Ona görə modelin içində adi Python if və for operatorlarından sərbəst istifadə etmək olar — qraf həmin anda icra olunan yola uyğun gələcək.

ÜsulNecə işləyirÇatışmazlığı
Ədədi törəmə(f(θ + h) − f(θ − h)) / 2htəxminidir və hər parametr üçün iki hesablama tələb edir
Simvolik törəmədüsturu cəbri qaydalarla çevirir (SymPy kimi)böyük modellərdə ifadələr həddən artıq şişir, dövrləri və şərtləri çətin emal edir
Avtomatik törəmə (autograd)icra olunan əməliyyatların lokal törəmələrini zəncir qaydası ilə birləşdiriraralıq qiymətləri yadda saxlamaq üçün əlavə yaddaş lazımdır

requires_grad, backward() və .grad

Python
import torch

x = torch.tensor(3.0, requires_grad=True)
y = x ** 2 + 2 * x + 1
print(y)
y.backward()
print(x.grad)
Gözlənilən nəticə
tensor(16., grad_fn=<AddBackward0>)
tensor(8.)
  • requires_grad=True PyTorch-a deyir: «bu tenzora görə törəmə lazım olacaq, əməliyyatları yadda saxla». Parametrlər (nn.Linear-in çəkiləri və s.) bunu avtomatik edir.
  • grad_fn tenzoru hansı əməliyyatın yaratdığını göstərir; bu, qrafdakı düyündür.
  • y.backward() dy/dx-i hesablayır və nəticəni yarpağın .grad atributuna yazır: y = x² + 2x + 1 üçün dy/dx = 2x + 2 = 8 (x = 3).

Parametr vektor olanda .grad da eyni formada vektordur: hər element üçün xüsusi törəmə. Məsələn, f = ∑ vᵢ² funksiyası üçün ∂f/∂vᵢ = 2vᵢ:

Python
import torch

v = torch.tensor([1.0, -2.0, 3.0], requires_grad=True)
f = (v ** 2).sum()
f.backward()
print(v.grad)
Gözlənilən nəticə
tensor([ 2., -4.,  6.])

requires_grad bayrağını sonradan da dəyişmək olar: p.requires_grad_(False) parametri «dondurur» — onun qradiyenti hesablanmır və o, öyrətmə zamanı dəyişmir. Bu üsuldan köçürmə öyrənməsində (transfer learning) istifadə olunur: əvvəlcədən öyrədilmiş şəbəkənin əksər qatları dondurulur, yalnız son qat yeni tapşırığa uyğunlaşdırılır. Bunu konvolyusiya şəbəkələri dərsində görəcəyik.

Zəncir qaydası

dL/dx = dL/dy · dy/dx ∂L/∂w = ∂L/∂ŷ · ∂ŷ/∂wdL/dx = dL/dy · dy/dx ∂L/∂w = ∂L/∂ŷ · ∂ŷ/∂w
burada:
  • Litki (qrafın kökü)
  • y, ŷaralıq qiymətlər (qrafın daxili düyünləri)
  • x, wyarpaqlar: girişlər və parametrlər

Mürəkkəb funksiyanın törəməsi lokal törəmələrin hasilinə bərabərdir. Bir dəyişən bir neçə yolla itkiyə təsir edirsə, bütün yollar üzrə hasillər toplanır: ∂L/∂x = ∑ₖ ∂L/∂yₖ · ∂yₖ/∂x.

Nümunə 1: xətti modelin qradiyenti

ŷ = w · x + b, L = (ŷ − y)². w = 2, b = 1, x = 3, y = 10 üçün ∂L/∂w və ∂L/∂b-ni zəncir qaydası ilə tap.

Həllini göstər
İrəli keçid: ŷ = 2 · 3 + 1 = 7, L = (7 − 10)² = 9.
∂L/∂ŷ = 2(ŷ − y) = −6.
∂ŷ/∂w = x = 3 ⇒ ∂L/∂w = −6 · 3 = −18.
∂ŷ/∂b = 1 ⇒ ∂L/∂b = −6.
Hər iki qradiyent mənfidir: w və b-ni artırmaq itkini azaldar — doğrudan da, ŷ = 7 hədəfdən (10) kiçikdir.
Python
import torch

w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
x, y = torch.tensor(3.0), torch.tensor(10.0)
y_hat = w * x + b
loss = (y_hat - y) ** 2
print(type(loss.grad_fn).__name__, type(y_hat.grad_fn).__name__)
loss.backward()
print(loss.item(), w.grad.item(), b.grad.item())
Gözlənilən nəticə
PowBackward0 AddBackward0
9.0 -18.0 -6.0
Autograd əl ilə tapdığımız −18 və −6 qiymətlərini təsdiqləyir. x və y üçün requires_grad verilməyib — onlar verilənlərdir, parametr deyil.
σ(z) = 1 / (1 + e⁻ᶻ) σ′(z) = σ(z) · (1 − σ(z))σ(z) = 1 / (1 + e⁻ᶻ) σ′(z) = σ(z) · (1 − σ(z))
burada:
  • σ(z)siqmoid funksiyası, qiymətləri (0; 1) aralığında
  • σ′(z)onun törəməsi; ən böyük qiyməti z = 0-da 0,25-dir

İsbat: σ′(z) = e⁻ᶻ / (1 + e⁻ᶻ)² = σ(z) · e⁻ᶻ / (1 + e⁻ᶻ), sonuncu kəsr isə 1 − σ(z)-dir.

Nümunə 2: siqmoid neyronun qradiyenti

y = σ(w · x), w = 0, x = 2. dy/dw-ni tap.

Həllini göstər
z = w · x = 0, y = σ(0) = 0,5.
dy/dw = σ′(z) · dz/dw = σ(z)(1 − σ(z)) · x = 0,5 · 0,5 · 2 = 0,5.
Nəticə: σ′ heç vaxt 0,25-dən böyük olmadığı üçün qradiyent hər siqmoid qatından keçəndə ən azı 4 dəfə kiçilir. 10 qatdan sonra bu, 4¹⁰ ≈ 10⁶ dəfə deməkdir — itən qradiyent problemi. Dərin şəbəkələrdə ReLU-nun üstünlük təşkil etməsinin səbəbi budur.
Python
import torch

w = torch.tensor(0.0, requires_grad=True)
x = torch.tensor(2.0)
y = torch.sigmoid(w * x)
y.backward()
print(y.item(), w.grad.item())
Gözlənilən nəticə
0.5 0.5
Nümunə 3: dəyişən iki yolla təsir edəndə

f = x · y + x², x = 2, y = 3. ∂f/∂x və ∂f/∂y-ni tap. Autograd burada nə edir?

Həllini göstər
x qrafda iki düyünə daxil olur: u = x · y və v = x².
Birinci yol: ∂u/∂x = y = 3. İkinci yol: ∂v/∂x = 2x = 4.
Yollar toplanır: ∂f/∂x = 3 + 4 = 7; ∂f/∂y = x = 2.
Autograd da eyni şeyi edir: x-ə gələn hər qradiyent axınını .grad-da toplayır. Qradiyentlərin toplanması (akkumulyasiya) elə buradan gəlir.

Geri keçidin gücü onun səmərəliliyindədir: bir backward() çağırışı itkinin bütün parametrlərə görə qradiyentini hesablayır və bunun dəyəri irəli keçidin dəyəri ilə eyni tərtibdədir (kiçik sabit əmsal qədər). Hər parametr üçün ayrıca hesablama aparsaydıq, milyon parametrli şəbəkə üçün milyon irəli keçid lazım olardı.

Qradiyentlər toplanır: sıfırlama və torch.no_grad()

.backward() köhnə qradiyenti əvəz etmir, ona əlavə edir. Aşağıda y = x³ funksiyasının x = 2-də törəməsi 3x² = 12-dir, amma üç çağırışdan sonra .grad 36 olur:

Python
import torch

x = torch.tensor(2.0, requires_grad=True)
for i in range(3):
    y = x ** 3
    y.backward()
    print(x.grad)
x.grad.zero_()
print(x.grad)
with torch.no_grad():
    z = x * 2
print(z.requires_grad, x.detach().requires_grad)
Gözlənilən nəticə
tensor(12.)
tensor(24.)
tensor(36.)
tensor(0.)
False False
  • x.grad.zero_() qradiyenti yerində sıfırlayır (alt xətt _ yerində dəyişiklik deməkdir). Növbəti dərslərdə bunu optimizer.zero_grad() edəcək.
  • with torch.no_grad(): blokunda qraf qurulmur: parametrləri yeniləyərkən və modeli qiymətləndirərkən (inference) işlədilir, yaddaşa və vaxta qənaət edir.
  • .detach() eyni verilənə baxan, lakin qrafdan ayrılmış tenzor qaytarır — məsələn, qiyməti çap etmək və ya NumPy-a çevirmək üçün.

Autograd ilə qradiyent enişi

İndi hər şeyi birləşdirək. y = 2x + 1 düzxəttindən küylə yaradılmış 20 nöqtəyə xətti model uyğunlaşdıraq. Bu dəfə törəmə düsturlarını yazmırıq — hər addımda autograd onları hesablayır. Dövrün beş addımı bütün dərin öyrənmənin əsasıdır: irəli keçid → itki → backward → no_grad daxilində yeniləmə → qradiyentləri sıfırlama.

Python
import torch

torch.manual_seed(42)
X = torch.linspace(0, 1, 20)
Y = 2 * X + 1 + 0.1 * torch.randn(20)
w = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
lr = 0.5
for step in range(201):
    loss = ((w * X + b - Y) ** 2).mean()
    loss.backward()
    with torch.no_grad():
        w -= lr * w.grad
        b -= lr * b.grad
    w.grad.zero_()
    b.grad.zero_()
    if step % 50 == 0:
        print(f'step {step:3d}  loss {loss.item():.4f}')
print(f'w = {w.item():.3f}, b = {b.item():.3f}')
Gözlənilən nəticə
step   0  loss 4.4302
step  50  loss 0.0085
step 100  loss 0.0084
step 150  loss 0.0084
step 200  loss 0.0084
w = 1.907, b = 1.068
Cəmi 50 addımda itki 4,43-dən 0,0085-ə endi. Sonuncu itki küyün dispersiyasına (0,1² = 0,01) yaxındır — bundan aşağı düşmək həddən artıq uyğunlaşma olardı. w və b dəqiq 2 və 1 deyil, çünki 20 küylü nöqtə üçün ən yaxşı düzxətt budur.

Bu dövrü diqqətlə yadda saxla: növbəti dərslərdə onu qısaldacağıq, amma mahiyyəti dəyişməyəcək. torch.optim.SGD optimallaşdırıcısının step() metodu hər parametr üçün məhz p -= lr * p.grad əməliyyatını no_grad daxilində icra edir, zero_grad() isə qradiyentləri təmizləyir. nn.Linear kimi qatlar w və b-ni özləri yaradır və requires_grad-ı özləri qoşur. Yəni «sehrli» görünən yüksək səviyyəli kod bu beş sətrin rahat qablaşdırılmasıdır.

Əsas fikirlər

  • Autograd tenzorlarla əməliyyatlardan hesablama qrafı qurur və backward() ilə zəncir qaydasını kökdən yarpaqlara tətbiq edir.
  • requires_grad=True olan yarpaqların qradiyentləri .grad-da saxlanılır; backward() yalnız skalyar üçün çağırılır.
  • Zəncir qaydası: dL/dx = dL/dy · dy/dx; siqmoid üçün σ′ = σ(1 − σ).
  • Qradiyentlər toplanır — hər addımdan sonra sıfırla; yeniləmələr və qiymətləndirmə torch.no_grad() daxilində aparılır.
  • Öyrətmə dövrü: irəli keçid → itki → backward → yeniləmə → sıfırlama.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
x = 1, y = x³ + 4x. y.backward()-dan sonra x.grad nəyə bərabərdir?