Part 03 · Deep Learning
Forward Pass and Backpropagation
Network কীভাবে বোঝে কোন weight এর দোষ: forward pass, computational graph, আর autograd করে দেওয়ার আগে নিজের হাতে, সংখ্যা ধরে ধরে backpropagation trace করা।
⏱ 2-3 ঘণ্টা · Station 17 / 92
জিনিসটা কী, কেন দরকার
একটা restaurant এর তরকারি বেশি লবণাক্ত হয়ে গেছে। Head chef সবাইকে বের করে দেন না। তিনি জিজ্ঞেস করেন: শেষ স্বাদ নির্ভর করে sauce এর উপর, sauce নির্ভর করে stock আর soy এর উপর, আর soy ই সবচেয়ে লবণাক্ত জিনিস যেটা দেওয়া হয়েছে। দোষ recipe ধরে পেছন দিকে যায়, যে ধাপ result এ যতটা প্রভাব ফেলেছে ততটা।
Backpropagation neural network এর জন্য ঠিক এটাই করে। Loss বলে output কতটা ভুল ছিল। Backprop network ধরে পেছনে হাঁটে আর প্রতিটা weight কে বলে সে কতটা অবদান রেখেছে, মানে তার gradient। তারপর gradient descent (Part 1) প্রতিটা weight কে একটু সরায়। তুমি যত network train করবে, কোটি কোটি weight এর LLM সহ, সব এভাবেই শেখে। Framework তোমার হয়ে এটা করে দেয়, কিন্তু training ভাঙলে জানতে হবে ওরা আসলে কী করছে।
ভেতরে কীভাবে চলে
Forward pass। Input কে layer গুলোর ভেতর দিয়ে পাঠাও, মাঝের প্রতিটা মান হিসাব করো (প্রতিটা weight দেওয়া যোগফল z আর প্রতিটা activation h), আর মনে রাখো। শেষ মান হলো loss।
Computational graph। প্রতিটা operation (গুণ, যোগ, sigmoid, বর্গ) একটা node। প্রতিটা node একটাই জিনিস জানে: নিজের local derivative। Sigmoid এর জন্য s'(z) = s·(1 − s)। w·h গুণের জন্য w এর সাপেক্ষে derivative হলো h, আর h এর সাপেক্ষে w।
Backward pass। Loss থেকে gradient 1 নিয়ে শুরু করো আর graph উল্টো দিকে হাঁটো। প্রতিটা node এ আসা gradient কে node এর local derivative দিয়ে গুণ করে সামনে পাঠাও। এটা Part 1 এর chain rule, node ধরে ধরে:
d loss / d c1 = d loss/d out × d out/d zo × d zo/d c1
= 2(out − y) × out(1 − out) × h1
একটা মান কয়েক জায়গায় গেলে, প্রতিটা জায়গা থেকে ফিরে আসা gradient গুলো যোগ হয়।
কেন এত কার্যকর। একটা backward pass প্রতিটা weight এর gradient দেয় মোটামুটি আরেকটা forward pass এর খরচে, কারণ প্রতিটা node এর কাজ তার আগের সবাই আবার ব্যবহার করে। প্রতিটা weight এর gradient আলাদা করে বের করতে গেলে (নাড়িয়ে) প্রতি weight এ একটা করে forward pass লাগত: লাখ লাখ weight এ অসম্ভব।
Autograd। Forward pass চলার সময় PyTorch requires_grad=True ওয়ালা tensor এর প্রতিটা operation লিখে রাখে। .backward() ডাকলে সেই লেখা উল্টো দিকে হাঁটে। নিচে তুমি হাতে যা করবে, এটা হুবহু সেই algorithm।
Backprop এর সময় একটা sigmoid node এ −0.96 gradient এলো, আর sigmoid এর output ছিল 0.52। Node টা পেছনে কত gradient পাঠাবে?
ছবিতে দেখো
হাতে backprop, তারপর autograd
Sigmoid সহ একটা 2-2-1 network আর একটা training example। Program টা forward pass চালায়, ছয়টা weight এর জন্যই হাতে backward pass করে, একই কাজ PyTorch autograd দিয়ে করে, আর একটা gradient descent step নেয়।
"""Backpropagation by hand on a tiny network, then the same numbers from PyTorch autograd."""
import math
import torch
# Network: 2 inputs -> 2 hidden (sigmoid) -> 1 output (sigmoid). Loss = (out - y)^2. One example.
x1, x2, y = 1.0, 0.5, 1.0
w = {"a1": 0.4, "a2": -0.2, "b1": 0.3, "b2": 0.6, "c1": 0.7, "c2": -0.5} # a: into h1, b: into h2, c: into out
sig = lambda z: 1 / (1 + math.exp(-z))
# ---- forward pass: compute and remember every intermediate value ----
z1 = w["a1"] * x1 + w["a2"] * x2
z2 = w["b1"] * x1 + w["b2"] * x2
h1, h2 = sig(z1), sig(z2)
zo = w["c1"] * h1 + w["c2"] * h2
out = sig(zo)
loss = (out - y) ** 2
print(f"forward: z1={z1:.4f} h1={h1:.4f} z2={z2:.4f} h2={h2:.4f} out={out:.4f} loss={loss:.4f}")
# ---- backward pass: chain rule from the loss back to every weight ----
d_out = 2 * (out - y) # d loss / d out
d_zo = d_out * out * (1 - out) # sigmoid'(z) = s * (1 - s)
grads = {"c1": d_zo * h1, "c2": d_zo * h2}
d_h1, d_h2 = d_zo * w["c1"], d_zo * w["c2"] # blame flows back along each weight
d_z1, d_z2 = d_h1 * h1 * (1 - h1), d_h2 * h2 * (1 - h2)
grads.update({"a1": d_z1 * x1, "a2": d_z1 * x2, "b1": d_z2 * x1, "b2": d_z2 * x2})
print("by hand: ", " ".join(f"{k}={v:+.5f}" for k, v in grads.items()))
# ---- PyTorch builds the same graph during the forward pass and runs it backward ----
t = {k: torch.tensor(v, dtype=torch.float64, requires_grad=True) for k, v in w.items()}
h1t = torch.sigmoid(t["a1"] * x1 + t["a2"] * x2)
h2t = torch.sigmoid(t["b1"] * x1 + t["b2"] * x2)
outt = torch.sigmoid(t["c1"] * h1t + t["c2"] * h2t)
((outt - y) ** 2).backward()
print("autograd:", " ".join(f"{k}={t[k].grad.item():+.5f}" for k in grads))
# ---- one gradient descent step lowers the loss ----
new = {k: w[k] - 1.0 * grads[k] for k in w}
h1n = sig(new["a1"] * x1 + new["a2"] * x2)
h2n = sig(new["b1"] * x1 + new["b2"] * x2)
outn = sig(new["c1"] * h1n + new["c2"] * h2n)
print(f"after one step (lr=1): out {out:.4f} -> {outn:.4f}, loss {loss:.4f} -> {(outn - y) ** 2:.4f}")
Output:
forward: z1=0.3000 h1=0.5744 z2=0.6000 h2=0.6457 out=0.5198 loss=0.2306
by hand: c1=-0.13770 c2=-0.15478 a1=-0.04102 a2=-0.02051 b1=+0.02742 b2=+0.01371
autograd: c1=-0.13770 c2=-0.15478 a1=-0.04102 a2=-0.02051 b1=+0.02742 b2=+0.01371
after one step (lr=1): out 0.5198 -> 0.5675, loss 0.2306 -> 0.1871
কী দেখলাম:
- Forward: answer যেখানে 1, network দিচ্ছে 0.520, তাই loss 0.231।
- হাতে করা আর autograd মিলে যায় প্রতিটা weight এ পাঁচ দশমিক পর্যন্ত। Autograd জাদু না: হুবহু এই হিসাব, নিজে নিজে করা।
- Sign গুলো একটা গল্প বলে। c1 আর c2 এর gradient negative: এদের বাড়ালে output 1 এর দিকে ওঠে। a1 ও negative (h1 output কে সাহায্য করে, কারণ c1 = +0.7), কিন্তু b1 আর b2 positive: h2 output এর ক্ষতি করে (c2 = −0.5), তাই network h2 কে ছোট চায়, মানে b1 আর b2 কমাতে চায়। দোষ প্রতিটা weight এর sign ধরে পেছনে গেছে।
- Output এর কাছে বড় gradient, পেছনে ছোট: |c| এর gradient 0.14 এর আশেপাশে, |a| আর |b| 0.02 থেকে 0.04। ফেরার পথে প্রতিটা sigmoid সর্বোচ্চ 0.25 দিয়ে গুণ করেছে। এমন 20 টা চাপালে প্রথম দিকের layer প্রায় শিখতেই পারে না: vanishing gradient, পরের lesson এ।
- একটা step কাজ করে: learning rate 1 দিয়ে output 0.520 থেকে 0.568 এ যায়, loss 0.231 থেকে 0.187 এ নামে।
Production এ যা মাথায় রাখবে
- প্রায়ই সীমা হলো memory, compute না। Backward pass এর জন্য forward pass কে মাঝের প্রতিটা মান রেখে দিতে হয়। এজন্যই training এ inference এর চেয়ে কয়েক গুণ বেশি GPU memory লাগে, আর gradient checkpointing (কিছু মান রেখে না দিয়ে আবার হিসাব করা) এর মতো কৌশল আছে।
- নিজের লেখা gradient check করো। নিজে কোনো layer বা loss লিখলে, train করার আগে তার gradient কে
torch.autograd.gradcheckবা Part 1 এর নাড়িয়ে মাপার পদ্ধতির সাথে মিলিয়ে নাও। - Gradient এর সাইজ চোখে রাখো। প্রতি layer এ gradient কত বড় log করো। প্রথম দিকের layer এ প্রায় শূন্য মানে vanishing gradient; বিশাল বা NaN মানে exploding। Loss এ দেখা দেওয়ার আগেই দুইটা ওখানে দেখা যায়।
zero_grad()ভুলবে না। PyTorch default ভাবে নতুন gradient পুরনোর সাথে যোগ করে ("gradient যোগ হয়" নিয়মের কারণে)। প্রতি step এ সেগুলো মুছতে ভুলে যাওয়া একটা ক্লাসিক bug: কোনো error নেই, training অদ্ভুতভাবে উল্টাপাল্টা হয়।- Inference এ graph লাগে না। Prediction এর code
torch.no_grad()(বাinference_mode()) এর ভেতরে রাখো, যাতে PyTorch operation লিখে না রাখে: কম memory, দ্রুত serving।
এক কথায়
Backprop network কে সামনে চালিয়ে প্রতিটা মান মনে রাখে, তারপর পেছনে হাঁটতে হাঁটতে প্রতিটা node এর local derivative গুণ করে (chain rule), এক sweep এ প্রতিটা weight কে তার gradient দেয়; autograd হলো তোমার হয়ে এই হিসাব রাখা।
নিজে করে দেখো
Example এ target y কে 1.0 থেকে 0.0 করে আবার চালাও। কোন gradient গুলোর sign বদলায়, আর কেন? তারপর ব্যাখ্যা করো a1 এর gradient কেন ঠিক a2 এর দ্বিগুণ।
Answer
y = 0 হলে d loss / d out = 2(0.520 − 0) negative এর বদলে positive, তাই প্রতিটা gradient এর sign উল্টে যায়: network এখন output কম চায়। Forward এর মান বদলায় না, বদলায় শুধু শুরুর factor, আর তার পরের সবকিছু উল্টে যায়। a1 এর gradient হলো d_z1 × x1 আর a2 এর d_z1 × x2। দুইটার d_z1 একই, আর x1 = 1.0 হলো x2 = 0.5 এর ঠিক দ্বিগুণ, তাই gradient ও দ্বিগুণ: একটা weight এর gradient তার ভেতর দিয়ে যাওয়া input এর সমানুপাতিক।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →