এই পর্বে যা শিখবেন
- Leaf আর non-leaf tensor, grad_fn-এর শিকল, আর প্রতিটি node-এ কী জমা থাকে
- Non-scalar output-এ
gradient=কেন লাগে, আর reverse mode কেন পুরো Jacobian ছাড়াই চলে - no_grad, inference_mode, detach আর freezing-এর পার্থক্য, আর কোনটি কখন
torch.autograd.grad, create_graph দিয়ে Hessian-vector product, আর finite difference দিয়ে যাচাই- নিজের
autograd.Functionলেখা, gradcheck দিয়ে ধরা, hook দিয়ে gradient দেখা, আর পাঁচটি সাধারণ ফাঁদ
পর্ব ০৫ (নিজে বানানো autograd — computational graph আর reverse mode)-এ নিজের হাতে একটি autograd বানিয়েছেন, আর শেষে PyTorch-এর requires_grad, backward(), .grad, retain_graph-এর error আর zero_grad একবার দেখেছেন। Jacobian আর VJP-এর তত্ত্ব মেশিন লার্নিং কোর্সের পর্ব ১১-এ আছে। এই পর্ব সেখান থেকে শুরু করে PyTorch-এর autograd-এর ভেতরে ঢোকে: কোন tensor-এর gradient রাখা হয়, কোন মান মনে রাখা হয়, কখন graph বন্ধ করবেন, আর নিজের backward লিখলে কীভাবে যাচাই করবেন। PyTorch পাতার ভেতরে চলে না; তাই PyTorch-এর সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে নেওয়া (Python 3.14.4, PyTorch 2.15.0 CPU build, NumPy 2.5.4; লেখার সময়ের সংস্করণ, পরে বদলাতে পারে)। তিনটি NumPy ব্লক পাতাতেই চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে।
১ · Leaf, grad_fn আর node-এ কী জমা থাকে
ঢাকার একটি পোশাক কারখানায় প্রতিটি লাইনের সুপারভাইজার একটি খাতা রাখেন। কোনো চালানে ত্রুটি পাওয়া গেলে খাতা ধরে পেছনে যাওয়া হয়: কোন লাইন, কোন দর্জি, কোন কাপড়ের রোল। কিন্তু সুপারভাইজার সব তথ্য লেখেন না, শুধু সেটুকু, যা পরে দায় খুঁজতে লাগবে। কাপড়ের রোল যদি ক্রেতার দেওয়া হয় আর তার দায় খোঁজার দরকার না থাকে, রোলের বিবরণ বাদ। PyTorch-এর autograd ঠিক এমন হিসাবি খাতা।
পর্ব ০৫-এর এক লাইনের সারকথা: requires_grad=True দিয়ে তৈরি tensor একটি leaf tensor; তার ওপর প্রতিটি কাজ একটি node বানায়, আর backward() সেই computational graph ধরে পেছনে হাঁটে। এবার node-এর ভেতরে তাকাই। ছোট একটি উদাহরণ নিই, বাজারঘরের একটি অর্ডার “দ্রুত পৌঁছাবে কি না”-এর একটি ছোট logistic neuron: weight w = (০.৫, −১), feature x = (২, ৩), z = Σ w·x, y = sigmoid(z).
যে tensor কোনো কাজ থেকে তৈরি, যার grad_fn আছে, সেটি non-leaf tensor. Backward-এর পর gradient জমা হয় শুধু সেই leaf-এ, যার requires_grad=True; non-leaf-এর .grad default-ভাবে None থাকে, কারণ সেগুলো রাখলে memory অকারণে খরচ হতো। কোনো মাঝের মানের gradient দেখতে চাইলে আগে retain_grad() ডাকতে হয়। নিচের ছবিতে বাঁ দিকে forward-এর tensor, ডান দিকে প্রতিটির grad_fn node, আর প্রতিটি node-এ কী রাখা আছে।
y = σ(Σ w·x)-এর tensor আর grad_fn node
Forward tensors (blue, top to bottom); backward nodes with saved values (red, bottom to top)
w.grad-এ যোগ করে। x-এর requires_grad False, তাই তার জন্য কোনো node নেই।ছবির সব কথা সত্যিকারের run-এ যাচাই করা যায়। নিচের script প্রতিটি tensor-এর flag, grad_fn-এর শিকল (next_functions ধরে) আর প্রতিটি node-এর রাখা মান (_saved_… নামের attribute) ছাপে। ডেস্কটপে uv দিয়ে একটি আলাদা project বানিয়ে CPU-র PyTorch install করা হয়েছে, পর্ব ০৫-এর মতোই (uv add torch numpy, PyTorch-এর CPU wheel index দিয়ে)।
import sys, warnings, numpy as np, torch
print("Python", sys.version.split()[0], "| torch", torch.__version__, "| numpy", np.__version__)
w = torch.tensor([0.5, -1.0], requires_grad=True) # parameter: leaf, grad চাই
x = torch.tensor([2.0, 3.0]) # ডেটা: leaf, grad লাগে না
z = (w * x).sum() # মাঝের ফল: non-leaf
y = torch.sigmoid(z)
for name, t in [("w", w), ("x", x), ("z", z), ("y", y)]:
fn = type(t.grad_fn).__name__ if t.grad_fn else None
print(f"{name}: is_leaf={t.is_leaf}, requires_grad={t.requires_grad}, grad_fn={fn}")
fn = y.grad_fn # শিকল ধরে পেছনে হাঁটা
while fn is not None:
nxt = [type(n).__name__ if n else None for n, _ in fn.next_functions]
print(f" {fn.name()} -> {nxt}")
fn = fn.next_functions[0][0] if fn.next_functions else None
s, m = z.grad_fn, z.grad_fn.next_functions[0][0]
print("Sigmoid saved result:", y.grad_fn._saved_result)
print("Sum saved sizes :", s._saved_self_sym_sizes)
print("Mul saved self/other:", m._saved_self, m._saved_other)
z.retain_grad() # non-leaf-এর grad রাখতে বলা
y.backward()
print("w.grad", w.grad, "| x.grad", x.grad, "| z.grad", z.grad)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
print("y.grad:", y.grad) # non-leaf, retain_grad নেই
print("UserWarning:", str(caught[0].message).split(". ")[0] + ".")
try:
y.grad_fn._saved_result # backward-এর পর
except RuntimeError as e:
print("RuntimeError:", str(e).split(" (")[0] + " …")সত্যিকারের run (ডেস্কটপ, CPU):
~/bazarghar-dl$ uv run python graph_inside.py
Python 3.14.4 | torch 2.15.0+cpu | numpy 2.5.4
w: is_leaf=True, requires_grad=True, grad_fn=None
x: is_leaf=True, requires_grad=False, grad_fn=None
z: is_leaf=False, requires_grad=True, grad_fn=SumBackward0
y: is_leaf=False, requires_grad=True, grad_fn=SigmoidBackward0
SigmoidBackward0 -> ['SumBackward0']
SumBackward0 -> ['MulBackward0']
MulBackward0 -> ['AccumulateGrad', None]
struct torch::autograd::AccumulateGrad -> []
Sigmoid saved result: tensor(0.1192, grad_fn=<SigmoidBackward0>)
Sum saved sizes : (2,)
Mul saved self/other: None tensor([2., 3.])
w.grad tensor([0.2100, 0.3150]) | x.grad None | z.grad tensor(0.1050)
y.grad: None
UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed.
RuntimeError: Trying to backward through the graph a second time …
চারটি জিনিস লক্ষ করুন। এক, x নিজেও leaf (কোনো কাজ থেকে আসেনি), কিন্তু requires_grad False, তাই তার .grad None; MulBackward0-এর দ্বিতীয় parent-এর জায়গায় তাই None. দুই, MulBackward0 শুধু x রেখেছে, w নয়: ∂(w·x)/∂w-এর জন্য x লাগে, কিন্তু w লাগত কেবল x-এর gradient-এর জন্য, যা কেউ চায়নি। তিন, SumBackward0 কোনো মান রাখে না, শুধু আকার (২,), কারণ যোগের backward কেবল উপরের gradient-কে সেই আকারে ছড়িয়ে দেয়। চার, backward-এর পর রাখা মানগুলো মুছে যায়; তখন সেগুলো পড়তে চাইলেও পর্ব ০৫-এর সেই retain_graph-এর error আসে।
| node | |
| forward-এর মান | |
| যা রাখা হলো | |
| backward-এর নিয়ম |
- Leaf. w তৈরি হলো requires_grad=True দিয়ে। তার জন্য একটি AccumulateGrad node থাকে, যার কাজ শুধু আগত gradient w.grad-এ যোগ করা। x-এর জন্য কোনো node নেই।
- গুণ. m = (০.৫ × ২, −১ × ৩) = (১, −৩). w-এর gradient-এর জন্য অন্য গুণনীয়ক x লাগবে, তাই x রাখা হলো; w রাখা হয়নি, কারণ x-এর gradient কেউ চায়নি।
- যোগফল. z = ১ − ৩ = −২. যোগের local gradient সব ঘরে ১, তাই কোনো মান রাখার দরকার নেই; শুধু আকার, যাতে backward-এ একটি সংখ্যাকে দুই ঘরে ছড়ানো যায়।
- Sigmoid. y = σ(−২) = ০.১১৯২. σ-এর derivative y(১ − y), তাই output y রাখলেই যথেষ্ট, input z নয়।
- Backward. ১ × ০.১১৯২ × ০.৮৮০৮ = ০.১০৫০; যোগ তা দুই ঘরে দেয়; গুণ x দিয়ে গুণ করে (০.১০৫ × ২, ০.১০৫ × ৩) = (০.২১, ০.৩১৫), যা AccumulateGrad w.grad-এ যোগ করে। তারপর রাখা মানগুলো মুছে যায়।
২ · Non-scalar output — gradient= আসলে একটি vector-Jacobian product
পর্ব ০৫-এ backward() সবসময় একটি সংখ্যা (loss) থেকে ডাকা হয়েছিল। Output যদি একটি vector হয়, যেমন বাজারঘরের তিনটি জেলার পূর্বাভাস একসঙ্গে? তখন “gradient” একটি নয়, একটি পুরো Jacobian matrix: Jij = ∂yi/∂xj. PyTorch পুরো J বানায় না। তার বদলে আপনার কাছে একটি vector v চায়, আকারে y-এর মতো, আর ফেরত দেয় vector-Jacobian product vᵀJ. অর্থ: “L = v · y নামের একটি scalar ধরে নিন, তার gradient দিন।” y.sum().backward() আসলে এর বিশেষ রূপ, যেখানে v-এর সব ঘর ১।
import torch
from torch.autograd.functional import jacobian
W = torch.tensor([[1.0, 2.0, 0.0],
[0.0, -1.0, 3.0]]) # (2, 3)
x = torch.tensor([1.0, 2.0, -1.0], requires_grad=True)
y = W @ x # (2,), scalar নয়
try:
y.backward()
except RuntimeError as e:
print("RuntimeError:", e)
v = torch.tensor([1.0, 0.5]) # output-এর আকারে একটি vector
y.backward(gradient=v)
print("x.grad (gradient=v) :", x.grad)
J = jacobian(lambda t: W @ t, x.detach())
print("Jacobian J:\n", J)
print("v @ J :", v @ J)
# non-linear: y = tanh(W x) * x[:2]
f = lambda t: torch.tanh(W @ t) * t[:2]
x.grad = None
f(x).backward(gradient=v)
J2 = jacobian(f, x.detach())
print("max |x.grad - v @ J| =", (x.grad - v @ J2).abs().max().item())
x.grad = None
f(x).sum().backward() # sum() মানে v = সব ১
print("sum().backward() == ones @ J:", torch.allclose(x.grad, torch.ones(2) @ J2))সত্যিকারের run:
~/bazarghar-dl$ uv run python vjp_check.py
RuntimeError: grad can be implicitly created only for scalar outputs
x.grad (gradient=v) : tensor([1.0000, 1.5000, 1.5000])
Jacobian J:
tensor([[ 1., 2., 0.],
[ 0., -1., 3.]])
v @ J : tensor([1.0000, 1.5000, 1.5000])
max |x.grad - v @ J| = 2.9802322387695312e-08
sum().backward() == ones @ J: True
প্রথম লাইনের error-টি পরিষ্কার: scalar না হলে “শুরুর gradient” (পর্ব ০৫-এর বীজ ১) PyTorch নিজে বানাতে পারে না, আপনাকে দিতে হয়। y = Wx-এর Jacobian হুবহু W, আর vᵀW = (১, ১.৫, ১.৫), backward-এর সঙ্গে এক। Non-linear function-এ পার্থক্য প্রায় ৩ × 10−8, float32-এর rounding-এর মাপ। নিচের stepper হাতে একই হিসাব করে।
- Forward. y = Wx = (৫, −৫), দুটি সংখ্যা। Output-এর আকার (২,), input-এর (৩,).
- সমস্যা. y scalar নয়, তাই “y কত বাড়বে” প্রশ্নটি অস্পষ্ট: y1, নাকি y2, নাকি দুটির কোনো মিশ্রণ? PyTorch তাই RuntimeError দেয়।
- Jacobian. প্রতিটি output-এর একটি সারি। Linear function-এ J = W নিজেই, প্রতিটি ঘর একটি ধ্রুবক।
- VJP. v-এর প্রতিটি ঘর J-এর একটি সারির ওজন: ১ × প্রথম সারি + ০.৫ × দ্বিতীয় সারি = (১, ১.৫, ১.৫), সত্যিকারের run-এর x.grad-এর হুবহু।
- sum(). v = (১, ১) মানে দুই সারির সাধারণ যোগ, (১, ১, ৩). Backward সবসময় এমন একটি “ওজনসহ যোগফলের” gradient দেয়; আলাদা আলাদা সারি চাইলে আলাদা backward লাগে।
y = σ(Wx + b), W আকার (m × n), x (n,), b ও y (m,). উপরের gradient v (m,) এলে প্রথমে u = v ⊙ σ′(z), আকার (m,), যেখানে ⊙ মানে ঘর ধরে গুণ আর z = Wx + b. তারপর তিনটি VJP: x.grad = Wᵀu (n,), W.grad = u xᵀ (m × n, একটি outer product), b.grad = u (m,). Batch-এ x-এর জায়গায় X (B × n) হলে W.grad = Uᵀ X আর b.grad = U-এর সারি ধরে যোগ, পর্ব ০৫-এর broadcasting-এর নিয়মে। প্রতিটি উত্তরের আকার তার tensor-এর আকারের সমান হতেই হবে; উত্তর মেলানোর আগে আকার মেলান।
কেন PyTorch পুরো J দেয় না? কারণ বড় network-এ J বিশাল, আর training-এ তার দরকারও নেই: loss একটি scalar, তাই একটি VJP-ই পুরো gradient. নিচের NumPy ব্লক দুই স্তরের একটি tanh network (১০০০ input, ৬৪ hidden, ৩ output) নিয়ে দেখায় যে VJP পুরো J না বানিয়ে শুধু vector দিয়ে বাঁ দিক থেকে গুণ করে, আর m-টি one-hot v দিয়ে m-বার VJP চালালে পুরো J-ও পাওয়া যায়।
import numpy as np
rng = np.random.default_rng(42)
n, h, m = 1000, 64, 3 # input, hidden, output
W1 = rng.normal(0, n ** -0.5, (h, n))
W2 = rng.normal(0, h ** -0.5, (m, h))
x = rng.normal(size=n)
a = np.tanh(W1 @ x) # forward; মাঝের মান a, y রাখা হলো
y = np.tanh(W2 @ a)
def vjp(v): # vᵀJ, বাঁ দিক থেকে: শুধু vector
u = v * (1 - y ** 2) # (m,)
u = (u @ W2) * (1 - a ** 2) # (h,)
return u @ W1 # (n,)
J = np.diag(1 - y ** 2) @ W2 @ np.diag(1 - a ** 2) @ W1 # পুরো Jacobian
v = np.array([1.0, 0.5, -2.0])
print("vjp(v):", vjp(v).shape, "| J:", J.shape)
print(f"max |vjp(v) − v @ J| = {np.abs(vjp(v) - v @ J).max():.1e}")
rows = np.stack([vjp(e) for e in np.eye(m)]) # m-টি VJP
print("m-টি one-hot VJP একসঙ্গে = J ?", np.allclose(rows, J))
print("VJP-তে গুণ :", m + m * h + h + h * n)
print("পুরো J-তে গুণ:", h * n + m * h * n + m * n)
print("output 1টি (loss) হলে gradient = vjp([1]) — একটিমাত্র backward")আউটপুট (পাতার Python-এ চালানো):
vjp(v): (1000,) | J: (3, 1000)
max |vjp(v) − v @ J| = 6.9e-17
m-টি one-hot VJP একসঙ্গে = J ? True
VJP-তে গুণ : 64259
পুরো J-তে গুণ: 259000
output 1টি (loss) হলে gradient = vjp([1]) — একটিমাত্র backward
VJP আর v @ J-এর পার্থক্য 10−17-এর ঘরে, অর্থাৎ একই সংখ্যা। VJP-র ভেতরে মাঝের মান সবই vector: (৩,), (৬৪,), (১০০০,); বড় matrix-এর সঙ্গে শুধু vector-গুণ। পুরো J বানাতে প্রায় চার গুণ বেশি গুণ লেগেছে (ব্লকের এই ক্রমে; আগে ছোট W2-এর দিকের গুণগুলো সারলে প্রায় ১,৯২,৩৮৪, তবু প্রায় তিন গুণ), আর output যত বাড়ে, অনুপাত তত বাড়ে, কারণ J-এর খরচ প্রায় m-টি VJP-র সমান। Loss-এ m = ১, তাই একটি backward-ই যথেষ্ট; এটিই reverse mode-এর মূল সুবিধা, যা পর্ব ০৫-এ কাজ গুনে দেখেছেন।
পুরো Jacobian সত্যিই লাগলে? যেমন একটি ছোট model-এর sensitivity বিশ্লেষণে। torch.autograd.functional.jacobian ভেতরে ঠিক ব্লকের rows-এর মতো কাজ করে: প্রতিটি output-এর জন্য একটি one-hot v নিয়ে একটি VJP; vectorize=True দিলে সেগুলো একসঙ্গে চালায়। Input কম আর output বেশি হলে উল্টো পথ সস্তা: forward mode, যা প্রতিটি input দিকের জন্য একটি Jacobian-vector product দেয়। নতুন torch.func module-এ দুটিই আছে: jacrev (reverse) আর jacfwd (forward), সঙ্গে vjp, jvp আর vmap. নিয়মটি পর্ব ০৫-এর মতোই: n-টি input আর m-টি output হলে forward-এ n-টি pass, reverse-এ m-টি।
৩ · no_grad, inference_mode, detach আর freezing — graph বন্ধ করার চার উপায়
Graph তৈরি বিনা খরচে নয়: প্রতিটি কাজে একটি node, আর রাখা মানগুলো memory দখল করে রাখে যতক্ষণ graph বেঁচে আছে। তাই যেখানে gradient লাগবে না, সেখানে graph বন্ধ রাখা উচিত। PyTorch চারটি হাতিয়ার দেয়, আর এদের পার্থক্য সূক্ষ্ম।
with torch.no_grad():ভেতরের কোনো কাজ graph-এ লেখা হয় না; ফল সাধারণ tensor, পরে চাইলে অন্য graph-এ ব্যবহার করা যায়।with torch.inference_mode():no_grad-এর চেয়ে কড়া: tensor-এর version counter আর view-এর হিসাবও বাদ দেয়, তাই সামান্য দ্রুত হতে পারে। দাম: এর ভেতরে তৈরি “inference tensor” পরে কোনো autograd হিসাবে ঢোকানো যায় না।t.detach(): একটি নির্দিষ্ট tensor-কে graph থেকে কেটে আনে। নতুন tensor একই memory ভাগ করে (কপি নয়), শুধু graph-এর ইতিহাস নেই। হিসাবের একটি অংশে gradient থামাতে এটি লাগে।p.requires_grad_(False): parameter-কে freeze করা: এরপর থেকে তার জন্য কোনো gradient হিসাব হয় না।
import torch
w = torch.tensor([1.0, -2.0, 3.0], requires_grad=True)
with torch.no_grad():
a = w * 2
print("no_grad :", a.requires_grad, a.grad_fn, "| is_inference", a.is_inference())
with torch.inference_mode():
b = w * 2
print("inference :", b.requires_grad, b.grad_fn, "| is_inference", b.is_inference())
try:
(b * w).sum().backward() # inference tensor graph-এ
except RuntimeError as e:
print("RuntimeError:", str(e).split(". ")[0] + ".")
(a * w).sum().backward() # no_grad-এর ফল চলে
print("no_grad-এর ফল দিয়ে backward, w.grad =", w.grad)
d = w.detach() # একই memory, graph-এর বাইরে
print("detach: same memory?", d.data_ptr() == w.data_ptr(), "| requires_grad", d.requires_grad)
w.grad = None
(w * w).sum().backward(); print("d(w*w)/dw :", w.grad)
w.grad = None
(w * w.detach()).sum().backward(); print("d(w*w.detach())/dw :", w.grad)
w.grad = None # accumulation বনাম autograd.grad
for _ in range(2):
(w ** 2).sum().backward()
print("দুবার backward, w.grad:", w.grad)
g, = torch.autograd.grad((w ** 2).sum(), w)
print("autograd.grad ফেরত দেয়:", g, "| w.grad বদলায়নি:", w.grad)
w.requires_grad_(False) # freeze
print("frozen: (w*2).requires_grad =", (w * 2).requires_grad)সত্যিকারের run:
~/bazarghar-dl$ uv run python modes.py
no_grad : False None | is_inference False
inference : False None | is_inference True
RuntimeError: Inference tensors cannot be saved for backward.
no_grad-এর ফল দিয়ে backward, w.grad = tensor([ 2., -4., 6.])
detach: same memory? True | requires_grad False
d(w*w)/dw : tensor([ 2., -4., 6.])
d(w*w.detach())/dw : tensor([ 1., -2., 3.])
দুবার backward, w.grad: tensor([ 4., -8., 12.])
autograd.grad ফেরত দেয়: tensor([ 2., -4., 6.]) | w.grad বদলায়নি: tensor([ 4., -8., 12.])
frozen: (w*2).requires_grad = False
no_grad আর inference_mode দুটিই grad_fn ছাড়া ফল দিয়েছে, কিন্তু inference tensor-কে আবার graph-এ ঢোকাতেই error. পুরো বার্তায় PyTorch নিজেই দুটি সমাধান বলে: clone() করে সাধারণ tensor বানানো, বা inference_mode-এর জায়গায় no_grad. detach-এর লাইন দুটি সবচেয়ে শিক্ষণীয়: w·w-এর derivative 2w = (২, −৪, ৬), কিন্তু w·w.detach()-এ একটি গুণনীয়ক ধ্রুবক ধরা হয়েছে, তাই gradient শুধু w.detach() = (১, −২, ৩). একই সংখ্যা, অর্ধেক gradient. শেষ দুটি লাইন পর্ব ০৫-এর accumulation মনে করায়: দুটি backward-এ .grad দ্বিগুণ (৪, −৮, ১২); কিন্তু torch.autograd.grad gradient ফেরত দেয়, কোনো .grad-এ যোগ করে না। Training loop-এ প্রতি ধাপের আগে zero_grad লাগে; PyTorch 2.0 থেকে optimizer-এর zero_grad()-এর default set_to_none=True, অর্থাৎ .grad-কে শূন্যে ভরে না, None করে দেয়। পুরো loop আসবে পর্ব ১০ (Training loop — loss, optimizer, train/eval mode আর checkpoint)-এ।
| বৈশিষ্ট্য | no_grad | inference_mode | detach() | requires_grad_(False) |
|---|---|---|---|---|
| পরিধি | একটি code block | একটি code block | একটি tensor | একটি tensor, স্থায়ী |
| graph তৈরি | না | না | কাটা জায়গার পরে না | ওই tensor থেকে না |
| version counter | চলে | বন্ধ | মূলের সঙ্গে ভাগ | চলে |
| ফল পরে graph-এ? | যায় (ধ্রুবক হিসেবে) | যায় না (clone ছাড়া) | যায় (ধ্রুবক হিসেবে) | যায় (ধ্রুবক হিসেবে) |
| সাধারণ ব্যবহার | validation, হাতে আপডেট | শুধু পূর্বাভাস | target, logging, NumPy-তে পাঠানো | freezing |
Graph বন্ধ রাখলে আসলে কতটা memory বাঁচে? PyTorch-এর saved_tensors_hooks দিয়ে প্রতিটি saved tensor গুনে দেখা যায়, কারণ graph কোনো মান রাখতে চাইলে সেটি আগে pack function-এর হাত দিয়ে যায়। নিচে বাজারঘরের ৫১২টি অর্ডারের একটি batch, ২৫৬ → ১০২৪ → ১০২৪ → ১ MLP. Parameter-গুলো গোনা থেকে বাদ, শুধু activation.
import torch
from torch.autograd.graph import saved_tensors_hooks
gen = torch.Generator().manual_seed(0)
X = torch.randn(512, 256, generator=gen) # ৫১২টি অর্ডার, ২৫৬টি feature
Ws = [torch.randn(a, b, generator=gen).mul_(0.05).requires_grad_()
for a, b in [(256, 1024), (1024, 1024), (1024, 1)]]
params = {W.data_ptr() for W in Ws}
def forward():
h = X
for W in Ws[:-1]:
h = torch.relu(h @ W)
return (h @ Ws[-1]).pow(2).mean()
def measure(label):
seen = {}
def pack(t): # প্রতিটি saved tensor এখানে আসে
if t.data_ptr() not in params:
seen[t.data_ptr()] = t.numel() * t.element_size()
return t
with saved_tensors_hooks(pack, lambda t: t):
loss = forward()
print(f"{label:8s}: requires_grad={loss.requires_grad}, saved activation "
f"{len(seen)}টি, {sum(seen.values()) / 2**20:.2f} MB")
measure("grad")
with torch.no_grad():
measure("no_grad")
print(f"parameter-এর মাপ: {sum(W.numel() for W in Ws) * 4 / 2**20:.2f} MB")সত্যিকারের run:
~/bazarghar-dl$ uv run python saved_bytes.py
grad : requires_grad=True, saved activation 4টি, 4.50 MB
no_grad : requires_grad=False, saved activation 0টি, 0.00 MB
parameter-এর মাপ: 5.00 MB
হিসাবটি হাতে মেলানো যায়। প্রথম গুণে W-এর gradient-এর জন্য X রাখা হয়েছে: ৫১২ × ২৫৬ × ৪ byte = ০.৫ MB. দুটি ReLU-র output, প্রতিটি ৫১২ × ১০২৪ × ৪ byte = ২ MB; একই tensor ReLU আর পরের গুণ দুজনেই রাখে, কিন্তু memory একবারই (তাই data_ptr দিয়ে একবার গোনা)। শেষ output (৫১২ × ১) প্রায় ২ KB. মোট ৪.৫০ MB, প্রায় parameter-এর সমান। Batch দ্বিগুণ হলে activation দ্বিগুণ হয়, parameter একই থাকে; বড় batch-এ তাই activation-ই memory-র বড় অংশ। no_grad-এ শূন্য। (এক run-এর মাপ; GPU-তে memory মাপার কৌশল পর্ব ১২-এ।)
| পরিস্থিতি | |
| হাতিয়ার | |
| কেন | |
| ভুল করলে |
- Evaluation. শুধু মাপা হচ্ছে, শেখা নয়; তাই no_grad. ভুলে গেলে ফল একই থাকে, কিন্তু প্রতিটি batch-এর graph অকারণে memory নেয়। (
model.eval()আলাদা জিনিস, পর্ব ০৯-এ।) - হাতে আপডেট. optimizer ভেতরে ঠিক এটাই করে। no_grad ছাড়া
w -= …লিখলে leaf-এ in-place error আসে (সত্যিকারের run অংশ ৭-এ)। - শুধু inference. কোনো gradient কখনো লাগবে না, এমন নিশ্চিত হলে inference_mode. ফলটি পরে training-এ লাগতে পারলে no_grad নিরাপদ।
- detach. হিসাবের বাকিটা graph-এ থাকবে, শুধু একটি শাখা কাটা হবে। Run-এ w·w.detach()-এর gradient অর্ধেক হয়েছিল, ঠিক এই কারণে।
- Logging.
losses.append(loss)লিখলে প্রতিটি loss তার পুরো graph ধরে রাখে, আর memory ধাপে ধাপে বাড়ে।loss.item()একটি Python সংখ্যা দেয়, graph ছাড়া। - Freezing. requires_grad_(False) দিলে ওই parameter-এর জন্য gradient-ই হিসাব হয় না, তাই সময় আর memory দুটিই বাঁচে। Transfer learning-এ এর বড় ব্যবহার আসবে পর্ব ২৩-এ।
৪ · দ্বিতীয় ক্রমের gradient — create_graph আর Hessian-vector product
সাধারণত gradient একটি শেষ ফল: তা দিয়ে আপডেট হয়, তারপর ফেলে দেওয়া হয়। কিন্তু কখনো gradient-এর gradient লাগে: Hessian-এর তথ্য (মেশিন লার্নিং কোর্সের পর্ব ১৫-এ Newton পদ্ধতি), gradient-এর আকারের ওপর শাস্তি, বা meta-learning. সমস্যা হলো, backward-এর হিসাব নিজে default-ভাবে graph-এ লেখা হয় না। create_graph=True দিলে লেখা হয়, তখন gradient নিজেই একটি non-leaf tensor, যাকে আবার differentiate করা যায়।
পুরো Hessian H (n × n) বানানো বড় model-এ অসম্ভব: কোটি parameter হলে কোটি × কোটি ঘর। কিন্তু বেশিরভাগ কাজে দরকার শুধু Hessian-vector product Hv. কৌশলটি সহজ: ∇f · v একটি scalar, আর তার gradient ∇(∇f · v) = Hv (H প্রতিসম বলে)। অর্থাৎ দুটি backward, কোনো n × n matrix নেই। পরীক্ষার জন্য একটি function নিই যার উত্তর হাতে জানা: f(x) = ¼‖x‖⁴. তাহলে ∇f = ‖x‖² x, আর Hv = ‖x‖² v + 2(x·v) x. x = (১, ২, −১), v = (১, ১, ০) হলে ‖x‖² = ৬, x·v = ৩, তাই Hv = ৬(১, ১, ০) + ৬(১, ২, −১) = (১২, ১৮, −৬).
import torch
from torch.autograd.functional import hessian
f = lambda t: (t @ t) ** 2 / 4 # f(x) = ¼‖x‖⁴
x = torch.tensor([1.0, 2.0, -1.0], requires_grad=True)
v = torch.tensor([1.0, 1.0, 0.0])
g, = torch.autograd.grad(f(x), x, create_graph=True) # ∇f = ‖x‖² x, নিজেও graph-এ
print("grad :", g)
hv, = torch.autograd.grad(g @ v, x) # ∇(∇f · v) = H v
print("H v :", hv)
xd = x.detach()
print("analytic :", (xd @ xd) * v + 2 * (xd @ v) * xd) # ‖x‖² v + 2 (x·v) x
print("full H @ v:", hessian(f, xd) @ v)
g2, = torch.autograd.grad(f(x), x) # create_graph ছাড়া
print("create_graph ছাড়া g2.requires_grad =", g2.requires_grad)
try:
torch.autograd.grad(g2 @ v, x)
except RuntimeError as e:
print("RuntimeError:", str(e).split(". ")[0] + ".")সত্যিকারের run:
~/bazarghar-dl$ uv run python hvp.py
grad : tensor([ 6., 12., -6.], grad_fn=<AddBackward0>)
H v : tensor([12., 18., -6.])
analytic : tensor([12., 18., -6.])
full H @ v: tensor([12., 18., -6.])
create_graph ছাড়া g2.requires_grad = False
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn.
Gradient (৬, ১২, −৬) = ৬x, আর তার গায়ে একটি grad_fn: create_graph-এর চিহ্ন। দ্বিতীয় backward-এর Hv, হাতের সূত্র আর পুরো Hessian গুণ v, তিনটিই (১২, ১৮, −৬). create_graph ছাড়া gradient একটি সাধারণ সংখ্যার tensor, graph নেই, তাই দ্বিতীয়বার differentiate করতে গেলে error. এখানে torch.autograd.grad-ই সঠিক হাতিয়ার: কোনো .grad-এ জমা না করে সরাসরি ফল দেয়, তাই প্রথম আর দ্বিতীয় ক্রমের gradient মিশে যায় না। দাম memory: create_graph-এ backward-এর graph-ও রাখতে হয়।
torch.autograd.grad-এর আরও দুটি option কাজে লাগে। retain_graph-এর default মান create_graph-এর মানই; তাই create_graph=True দিলে graph নিজে থেকেই থাকে। allow_unused=True না দিলে, কোনো input যদি output-এ মোটেই ব্যবহৃত না হয়, PyTorch error দেয়; দিলে সেই input-এর জন্য None ফেরত আসে। একটি তৈরি function হিসেবে torch.autograd.functional.hvp আর torch.func-এর রূপও আছে, কিন্তু ভেতরে ধারণা একই: gradient-এর ওপর আরেকটি VJP.
Autograd ছাড়াও Hv আনুমানিকভাবে পাওয়া যায়: দুটি gradient-এর পার্থক্য, [∇f(x + εv) − ∇f(x − εv)] / 2ε. নিচের ব্লক ε বদলে এর ভুল মাপে, আর একটি বড় least-squares সমস্যায় দেখায় যে Hv পুরো H না বানিয়েই হিসাব করা যায়।
import numpy as np
grad = lambda x: (x @ x) * x # f(x) = ¼‖x‖⁴ হলে ∇f = ‖x‖² x
x = np.array([1.0, 2.0, -1.0]); v = np.array([1.0, 1.0, 0.0])
exact = (x @ x) * v + 2 * (x @ v) * x # analytic: H v = ‖x‖² v + 2 (x·v) x
print("analytic H v:", exact)
for eps in (1e-1, 1e-3, 1e-5, 1e-8, 1e-11):
fd = (grad(x + eps * v) - grad(x - eps * v)) / (2 * eps) # দুটি gradient-এর পার্থক্য
print(f"eps={eps:.0e}: H v ≈ {np.round(fd, 6)}, error {np.abs(fd - exact).max():.1e}")
# বড় উদাহরণ: least squares, f(w) = ‖Xw − y‖² / (2N), H = XᵀX / N
rng = np.random.default_rng(42)
N, d = 500, 200
X = rng.normal(size=(N, d)); y = rng.normal(size=N)
g = lambda w: X.T @ (X @ w - y) / N
w, u = rng.normal(size=d), rng.normal(size=d)
hvp_exact = X.T @ (X @ u) / N # H না বানিয়েই H u
hvp_fd = (g(w + 1e-5 * u) - g(w - 1e-5 * u)) / 2e-5
H = X.T @ X / N # তুলনার জন্য পুরো H
print(f"least squares: max |FD − exact| = {np.abs(hvp_fd - hvp_exact).max():.1e}, "
f"|H u − exact| = {np.abs(H @ u - hvp_exact).max():.1e}")
print(f"পুরো H-এ সংখ্যা {H.size:,}; H u-তে লাগে শুধু {u.size} সংখ্যার vector")আউটপুট (পাতার Python-এ চালানো):
analytic H v: [12. 18. -6.]
eps=1e-01: H v ≈ [12.02 18.02 -6. ], error 2.0e-02
eps=1e-03: H v ≈ [12.000002 18.000002 -6. ], error 2.0e-06
eps=1e-05: H v ≈ [12. 18. -6.], error 3.8e-10
eps=1e-08: H v ≈ [12. 18. -6.], error 1.1e-07
eps=1e-11: H v ≈ [12.000001 18.000001 -6. ], error 1.5e-06
least squares: max |FD − exact| = 3.2e-10, |H u − exact| = 3.3e-15
পুরো H-এ সংখ্যা 40,000; H u-তে লাগে শুধু 200 সংখ্যার vector
ভুলের ধাঁচটি U-আকৃতির। ε বড় হলে (০.১) সূত্রের নিজের আনুমানিকতা (truncation) ভুল দেয়, প্রায় ε²-এর অনুপাতে: ε একশো ভাগ ছোট হলে ভুল দশ হাজার ভাগ কমে (2 × 10−2 থেকে 2 × 10−6-এ নামে)। ε খুব ছোট হলে দুটি প্রায় সমান সংখ্যার বিয়োগে float64-এর rounding বড় হয়ে ওঠে, তাই 10−8-এ ভুল আবার বাড়ে। সেরা ε এখানে 10−5-এর কাছে, ভুল প্রায় 4 × 10−10. Autograd-এর Hv-তে এই সমস্যাই নেই, তাই সে সঠিক উত্তর (১২, ১৮, −৬) দিয়েছিল। Least-squares অংশে H-এ ৪০,০০০ ঘর, কিন্তু X.T @ (X @ u) দুটি matrix-vector গুণেই Hu দিয়েছে।
৫ · নিজের autograd.Function — stable softplus, ভুল backward আর gradcheck
কখনো PyTorch-এর তৈরি backward যথেষ্ট নয়: সংখ্যাগতভাবে স্থির একটি সূত্র চাই, memory বাঁচাতে চাই, বা ইচ্ছা করে gradient বদলাতে চাই। তখন custom autograd.Function লেখা হয়, পর্ব ০৫-এর T class-এর vjp-এর মতোই: forward(ctx, …) ফল দেয় আর ctx.save_for_backward দিয়ে দরকারি মান রাখে; backward(ctx, g) উপরের gradient g পেয়ে প্রতিটি input-এর gradient ফেরত দেয়। উদাহরণ softplus, log(1 + eˣ), ReLU-এর একটি মসৃণ রূপ যার derivative σ(x). সরাসরি লিখলে x = ১০০০-এ eˣ overflow করে। স্থির রূপ: max(x, ০) + log1p(e−|x|), পর্ব ০৩-এর log-sum-exp কৌশলের আত্মীয়।
import torch
class SoftPlus(torch.autograd.Function): # log(1 + eˣ), overflow ছাড়া
@staticmethod
def forward(ctx, x):
ctx.save_for_backward(x) # backward-এর জন্য রেখে দেওয়া
return x.clamp(min=0) + torch.log1p(torch.exp(-x.abs()))
@staticmethod
def backward(ctx, g): # g = উপরের gradient
x, = ctx.saved_tensors
return g * torch.sigmoid(x) # d softplus/dx = σ(x)
class BadSoftPlus(SoftPlus): # ইচ্ছাকৃত ভুল backward
@staticmethod
def backward(ctx, g):
x, = ctx.saved_tensors
s = torch.sigmoid(x)
return g * s * (1 - s) # ভুল: এটি σ-এর derivativeএখন যাচাই। torch.autograd.gradcheck প্রতিটি input-কে ছোট ε (default 10−6) দিয়ে দুই দিকে সরিয়ে finite difference-এ একটি “numerical” Jacobian বানায়, আর backward থেকে একটি “analytical” Jacobian; দুটি সহনসীমার মধ্যে না মিললে GradcheckError. Script-টি তিনবার চালায়: সঠিক backward float64-এ, ভুল backward float64-এ, আর সঠিক backward float32-এ। শেষে overflow-এর পরীক্ষা।
import warnings, torch
from torch.autograd.gradcheck import gradcheck, GradcheckError
from softplus_fn import SoftPlus, BadSoftPlus
def check(fn, x):
try:
return gradcheck(fn, (x,))
except GradcheckError as e:
lines = str(e).splitlines()
print(" GradcheckError:", lines[0])
print("\n".join(" " + l[:42] + " …" for l in lines if l.startswith(("numer", "analy"))))
gen = torch.Generator().manual_seed(42)
x = torch.randn(5, dtype=torch.float64, generator=gen, requires_grad=True)
print("float64, SoftPlus :", check(SoftPlus.apply, x))
print("float64, BadSoftPlus:"); check(BadSoftPlus.apply, x)
x32 = x.detach().float().requires_grad_() # একই সংখ্যা, float32
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
print("float32, SoftPlus (সঠিক backward):"); check(SoftPlus.apply, x32)
print(" UserWarning:", str(w[0].message)[:60], "…")
big = torch.tensor([1000.0, -1000.0], requires_grad=True)
naive = torch.log(1 + torch.exp(big)); naive.sum().backward()
print("naive :", naive.detach(), big.grad)
big.grad = None
mine = SoftPlus.apply(big); mine.sum().backward()
print("stable:", mine.detach(), big.grad)সত্যিকারের run:
~/bazarghar-dl$ uv run python custom_fn.py
float64, SoftPlus : True
float64, BadSoftPlus:
GradcheckError: Jacobian mismatch for output 0 with respect to input 0,
numerical:tensor([[0.5834, 0.0000, 0.0000, …
analytical:tensor([[0.2430, 0.0000, 0.0000 …
float32, SoftPlus (সঠিক backward):
GradcheckError: Jacobian mismatch for output 0 with respect to input 0,
numerical:tensor([[0.5960, 0.0000, 0.0000, …
analytical:tensor([[0.5834, 0.0000, 0.0000 …
UserWarning: Input #0 requires gradient and is not a double precision flo …
naive : tensor([inf, 0.]) tensor([nan, 0.])
stable: tensor([1000., 0.]) tensor([1., 0.])
সঠিক backward float64-এ পাস। ভুল backward ধরা পড়েছে: প্রথম input x = ০.৩৩৬৭-এ numerical ০.৫৮৩৪ = σ(০.৩৩৬৭), কিন্তু analytical ০.২৪৩০ = σ(1 − σ). এই ভুলটি বাস্তবে খুব সম্ভব, কারণ দুটি সূত্র দেখতে কাছাকাছি, আর training-এ loss তবুও কমতে পারে, শুধু ভুল দিকে ও ভুল গতিতে। float32-এর লাইন সবচেয়ে গুরুত্বপূর্ণ: backward সঠিক, তবু gradcheck fail করেছে, কারণ numerical দিকটাই ভুল (০.৫৯৬০, আসল ০.৫৮৩৪); PyTorch নিজেও warning দিয়েছে যে input double precision নয়। শেষ দুই লাইন: সরল সূত্রে x = ১০০০-এ মান inf আর gradient NaN (backward-এ ০ × inf); স্থির সূত্রে মান ১০০০, gradient σ(১০০০) = ১।
float32-এ কেন numerical দিক এত ভুল? নিচের ব্লক একই পাঁচটি input-এ central difference-এর ভুল মাপে, float64 আর float32-এ, কয়েকটি ε-তে।
import numpy as np
def softplus(x): # stable: max(x, 0) + log1p(e^(−|x|))
return np.maximum(x, 0) + np.log1p(np.exp(-np.abs(x)))
def sigmoid(x): return 1 / (1 + np.exp(-x))
x64 = np.array([0.3367, 0.1288, 0.2345, 0.2303, -1.1229]) # gradcheck-এর উদাহরণের input
exact = sigmoid(x64) # সঠিক derivative σ(x)
print("eps float64 error float32 error")
for eps in (1e-2, 1e-3, 1e-4, 1e-6):
row = []
for dt in (np.float64, np.float32):
x, e = x64.astype(dt), dt(eps)
fd = (softplus(x + e) - softplus(x - e)) / (2 * e) # central difference
row.append(np.abs(fd.astype(np.float64) - exact).max())
print(f"{eps:.0e} {row[0]:.1e} {row[1]:.1e}")
x32 = np.float32(0.3367); e = np.float32(1e-6)
print("float32, eps=1e-6, x=0.3367:",
(softplus(x32 + e) - softplus(x32 - e)) / (2 * e), "| সঠিক", round(sigmoid(0.3367), 4))
big = np.array([1000.0])
with np.errstate(over="ignore"): # ডেস্কটপে overflow warning চাপা
print("naive log(1+e^x), x=1000:", np.log(1 + np.exp(big)), "| stable:", softplus(big))আউটপুট (পাতার Python-এ চালানো):
eps float64 error float32 error
1e-02 1.6e-06 2.3e-06
1e-03 1.6e-08 3.8e-05
1e-04 1.6e-10 2.8e-04
1e-06 5.2e-11 2.2e-02
float32, eps=1e-6, x=0.3367: 0.59604645 | সঠিক 0.5834
naive log(1+e^x), x=1000: [inf] | stable: [1000.]
float64-এ ε ছোট হলে ভুল কমে, 10−6-এও 5 × 10−11. float32-এ উল্টো: ε যত ছোট, ভুল তত বড়, কারণ float32 প্রায় সাত অঙ্ক রাখে, আর f(x + ε) ও f(x − ε)-এর পার্থক্য ওই সাত অঙ্কের শেষ দিকে পড়ে যায়। ε = 10−6-এ মান ০.৫৯৬০৪৬৪৫, ডেস্কটপের gradcheck-এর float32 লাইনের ০.৫৯৬০-এর সঙ্গে মিলে যায়। তাই নিয়ম: gradcheck সবসময় float64 input-এ। (NumPy-র এই অংশ PyTorch নয়, কিন্তু একই গাণিতিক কারণ।)
backward প্রতিটি forward input-এর জন্য একটি করে মান ফেরত দেয়, একই ক্রমে; যে input-এর gradient অর্থহীন (যেমন একটি int সূচক বা একটি flag), তার জায়গায় None. ctx.needs_input_grad একটি tuple, যা বলে কোন input-এর gradient সত্যিই চাওয়া হয়েছে; অপ্রয়োজনীয় হিসাব এড়াতে এটি দেখা যায়। Input বা output tensor রাখতে ctx.save_for_backward; সাধারণ Python মান (যেমন একটি সীমা) সরাসরি ctx-এর attribute-এ। ডাকতে হয় SoftPlus.apply(x) দিয়ে, কখনো SoftPlus()(x) বা forward সরাসরি নয়। gradcheck-এর default সহনসীমা eps = 10−6, atol = 10−5, rtol = 10−3; দ্বিতীয় ক্রম যাচাইয়ে gradgradcheck.
কখনো ইচ্ছা করেই “ভুল” gradient লাগে। Round-এর derivative প্রায় সব জায়গায় শূন্য, তাই round করা মানের মধ্য দিয়ে কিছুই শেখা যায় না। Straight-through estimator forward-এ round করে, কিন্তু backward-এ gradient-কে বদল ছাড়াই পার করে দেয়; quantization-এ এটি প্রচলিত। এমন function-এ gradcheck fail করবেই, কারণ সেটাই নকশা; অনুশীলন ৪-এ নিজে লিখবেন।
৬ · Hook দিয়ে gradient দেখা আর বদলানো
Backward চলার সময় মাঝপথে কী gradient যাচ্ছে, তা দেখার উপায় gradient hook: t.register_hook(fn). Backward যখন t-এর gradient হিসাব করে, তখন fn সেটি হাতে পায়। fn কিছু ফেরত না দিলে শুধু দেখা হয়; একটি tensor ফেরত দিলে সেটিই নতুন gradient. Non-leaf tensor-এও hook বসে, তাই retain_grad ছাড়াই মাঝের স্তরের gradient দেখা যায়। নিচে বাজারঘরের ৮টি অর্ডার আর ৩টি feature নিয়ে একটি ছোট ৩ → ৪ → ১ network.
import torch
gen = torch.Generator().manual_seed(42) # বাজারঘর: ৮টি অর্ডার, ৩টি feature
X = torch.randn(8, 3, generator=gen)
y = torch.randn(8, 1, generator=gen)
W1 = (torch.randn(3, 4, generator=gen) * 0.5).requires_grad_()
W2 = (torch.randn(4, 1, generator=gen) * 0.5).requires_grad_()
def show(name): # hook: gradient দেখে, বদলায় না
def hook(g):
print(f" {name:6s} grad shape {tuple(g.shape)}, norm {g.norm():.4f}")
return hook
h1 = W1.register_hook(show("W1"))
W2.register_hook(show("W2"))
H = torch.relu(X @ W1)
H.register_hook(show("H")) # non-leaf-এও hook বসে
loss = ((H @ W2 - y) ** 2).mean()
print("backward-এর ক্রমে hook:"); loss.backward()
print(f"W1.grad: norm {W1.grad.norm():.4f}, max |ঘর| {W1.grad.abs().max():.4f}")
h1.remove() # দেখা শেষ, hook সরানো
W1.register_hook(lambda g: g.clamp(-0.1, 0.1)) # ফেরত দিলে gradient বদলায়
W1.grad = None; W2.grad = None
((torch.relu(X @ W1) @ W2 - y) ** 2).mean().backward()
print("clamp hook-এর পর W1.grad-এর max |ঘর| =", f"{W1.grad.abs().max():.4f}")সত্যিকারের run:
~/bazarghar-dl$ uv run python hooks.py
backward-এর ক্রমে hook:
W2 grad shape (4, 1), norm 0.4047
H grad shape (8, 4), norm 0.2331
W1 grad shape (3, 4), norm 0.2553
W1.grad: norm 0.2553, max |ঘর| 0.1264
W2 grad shape (4, 1), norm 0.4047
clamp hook-এর পর W1.grad-এর max |ঘর| = 0.1000
Hook-গুলো backward-এর ক্রমে ডাকা হয়েছে: শেষ স্তর W2 আগে, তারপর মাঝের H, তারপর প্রথম স্তর W1, ঠিক পর্ব ০৪-এর reverse ক্রম। প্রতিটি gradient-এর আকার তার tensor-এর আকারের সমান। দ্বিতীয় অংশে W1-এর দেখার hook সরানো হয়েছে (remove()), তাই পরের backward-এ শুধু W2-এর লাইন ছাপা; আর নতুন hook প্রতিটি ঘর ±০.১-এ কেটে দিয়েছে, তাই সর্বোচ্চ ঘর ০.১২৬৪ থেকে ০.১০০০. বাস্তবে gradient-এর norm স্তর ধরে লিখে রাখা vanishing আর exploding gradient খোঁজার প্রথম হাতিয়ার (পর্ব ১৭ আর পর্ব ১৮-এ বিস্তারিত)। Hook বসিয়ে ভুলে গেলে সেটি প্রতিটি backward-এ চলতে থাকে, তাই কাজ শেষে সরিয়ে দিন।
আরও দুই ধরনের hook জানা ভালো। register_hook gradient পায় .grad-এ জমা হওয়ার আগে; জমা হওয়ার পরের মুহূর্তে কিছু করতে নতুন সংস্করণে register_post_accumulate_grad_hook আছে (এই run-এর 2.15.0-এ আছে)। আর পুরো একটি স্তরের (nn.Module) input ও output-এর gradient দেখতে register_full_backward_hook, পর্ব ০৯-এর পর কাজে লাগবে। Hook-এর ভেতরে gradient in-place বদলানো এড়িয়ে চলুন; নতুন tensor ফেরত দেওয়াই নিরাপদ রীতি।
৭ · ফাঁদের তালিকা — in-place, .data, numpy() আর integer tensor
অংশ ১-এ দেখেছেন, grad_fn node forward-এর কিছু মান মনে রাখে। যদি সেই মান backward-এর আগেই বদলে যায়? এর পাহারাদার version counter: প্রতিটি tensor-এ একটি সংখ্যা, যা প্রতিটি in-place operation-এ (নামের শেষে আন্ডারস্কোর, যেমন mul_, বা +=) এক বাড়ে। মান রাখার সময় version লিখে রাখা হয়; backward-এ মিল না পেলে RuntimeError. নিচের script পাঁচটি সাধারণ ফাঁদ একসঙ্গে দেখায়।
import torch
def first(e): return str(e).split(". ")[0].rstrip(".") + "."
x = torch.tensor([0.0, 1.0, 2.0], requires_grad=True)
y = torch.sigmoid(x) # sigmoid নিজের output মনে রাখে
y.mul_(2) # ফাঁদ ১: সেই output-এ in-place
try:
y.sum().backward()
except RuntimeError as e:
print("১)", first(e))
y = torch.sigmoid(x) * 2 # সমাধান: নতুন tensor
y.sum().backward(); print(" fix: x.grad =", x.grad)
try:
x += 1 # ফাঁদ ২: leaf-এ in-place
except RuntimeError as e:
print("২)", first(e))
with torch.no_grad():
x -= 0.1 * x.grad # optimizer step-এর রূপ
print(" no_grad-এ আপডেট:", x, "is_leaf", x.is_leaf)
x.grad = None
y = torch.sigmoid(x); y.data.mul_(2) # ফাঁদ ৩: .data, কোনো error নেই
y.sum().backward(); print("৩) .data-র পর x.grad =", x.grad)
s = torch.sigmoid(x.detach()); print(" সঠিক σ(1−σ) =", s * (1 - s))
try:
x.numpy() # ফাঁদ ৪
except RuntimeError as e:
print("৪)", first(e)); print(" fix:", x.detach().numpy())
try:
torch.tensor([1, 2, 3], requires_grad=True) # ফাঁদ ৫: integer
except RuntimeError as e:
print("৫)", first(e))সত্যিকারের run:
~/bazarghar-dl$ uv run python traps.py
১) one of the variables needed for gradient computation has been modified by an inplace operation: [torch.FloatTensor [3]], which is output 0 of Sigmoid, is at version 1; expected version 0 instead.
fix: x.grad = tensor([0.5000, 0.3932, 0.2100])
২) a leaf Variable that requires grad is being used in an in-place operation.
no_grad-এ আপডেট: tensor([-0.0500, 0.9607, 1.9790], requires_grad=True) is_leaf True
৩) .data-র পর x.grad = tensor([ 0.0244, -0.6459, -1.3304])
সঠিক σ(1−σ) = tensor([0.2498, 0.2002, 0.1067])
৪) Can't call numpy() on Tensor that requires grad.
fix: [-0.05 0.9606776 1.9790013]
৫) Only Tensors of floating point and complex dtype can require gradients.
| ফাঁদ | কী ঘটে | সমাধান |
|---|---|---|
| ১. graph-এর রাখা মানে in-place | sigmoid-এর output version ০ থেকে ১ হয়েছে; backward থামে | নতুন tensor বানান (y = y * 2); কোন কাজটি দোষী জানতে torch.autograd.set_detect_anomaly(True) |
| ২. requires_grad leaf-এ in-place | আপডেট নিজে graph-এ ঢুকত, তাই নিষেধ | with torch.no_grad():-এর ভেতরে আপডেট; x তখনো leaf থাকে |
৩. .data দিয়ে বদলানো | version বাড়ে না, error নেই; gradient (০.০২৪৪, −০.৬৪৬, −১.৩৩) সম্পূর্ণ ভুল, এমনকি চিহ্নও | .data নয়; detach() বা no_grad, যাতে version counter পাহারা দেয় |
৪. requires_grad tensor-এ .numpy() | NumPy graph চেনে না, তাই PyTorch আটকায় | x.detach().numpy() (GPU-তে হলে .cpu()-ও) |
| ৫. integer tensor-এ requires_grad | পূর্ণসংখ্যায় ছোট বদলের অর্থ নেই | float dtype দিন (torch.tensor([1., 2., 3.])) |
তৃতীয় ফাঁদটিই সবচেয়ে বিপজ্জনক, কারণ বাকি চারটি চিৎকার করে, এটি চুপ থাকে। .data একটি পুরোনো রাস্তা (PyTorch ০.৪-এর আগের Variable যুগের); detach() একই memory দেয় কিন্তু version counter ভাগ করে, তাই একই ভুলে সেখানে error আসত। আরেকটি সাধারণ সূক্ষ্মতা: ReLU-র মতো কিছু কাজ নিজের output রাখে, কিছু input; কোন মান রাখা হয়েছে তা না জেনে in-place করা মানে ভাগ্যের ওপর ভরসা। Tensor-এর view, stride আর in-place-এর নিয়ম আরও আসে পর্ব ০৭ (PyTorch tensor — shape, dtype, device আর broadcasting)-এ।
আরও গভীরে: PyTorch-এর autograd-এর নকশা Paszke ও সহকর্মীদের ২০১৭ সালের NIPS workshop প্রবন্ধে (“Automatic differentiation in PyTorch”) বর্ণিত; সেখানে version counter দিয়ে in-place কাজ নিরাপদ করার কথাও আছে, আর প্রভাব হিসেবে Chainer আর HIPS-এর autograd library-র নাম উল্লেখ আছে। Hessian-vector product-কে দুটি derivative-এর ক্রম হিসেবে নির্ভুলভাবে হিসাব করার কৌশল Barak Pearlmutter-এর ১৯৯৪ সালের Neural Computation-এর প্রবন্ধে সুপরিচিত হয়। PyTorch docs-এর “Autograd mechanics” পাতায় saved tensor, inference mode আর in-place-এর নিয়ম বিস্তারিত; torch.func (আগে functorch) jvp, vjp, hessian আর vmap-এর মতো function-ভিত্তিক রূপ দেয়।
পরীক্ষা ও ইন্টারভিউতে:
- W আকার (2, 3), y = Wx,
y.backward(gradient=v)-এর পর x.grad? Wᵀv, আকার (৩,). - no_grad আর detach-এর পার্থক্য? no_grad একটি পুরো block-এ graph বন্ধ করে; detach একটি tensor-কে graph থেকে কেটে আনে, বাকি হিসাব graph-এ থাকে।
- Hessian-vector product কীভাবে, পুরো H ছাড়া? create_graph=True দিয়ে g = ∇f, তারপর (g · v)-এর gradient.
- gradcheck কেন float64-এ? float32-এ finite difference-এর rounding ভুল সঠিক backward-কেও fail করায়।
- Backward-এর পর non-leaf-এর .grad কেন None? Memory বাঁচাতে শুধু leaf-এ জমা হয়; দরকার হলে retain_grad() বা hook.
৮ · AI-এর যুগে — AI-এর লেখা PyTorch কোডে autograd-এর ভুল ধরা
AI assistant PyTorch কোড খুব দ্রুত লেখে, আর তা প্রায়ই চলে। কিন্তু এই পর্বের বেশিরভাগ ভুল চলমান কোডেই থাকে: .data-র নিঃশব্দ ভুল, evaluation-এ graph, log-এ পুরো tensor জমিয়ে memory বাড়ানো, ভুল backward-ওয়ালা custom Function. AI-এর প্রশিক্ষণের ডেটায় পুরোনো PyTorch-এর কোড প্রচুর, তাই পুরোনো রীতিও ফিরে আসে।
| AI প্রায়ই যা করে | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
Variable(x, requires_grad=True) বা w.data -= lr * w.grad | Variable পুরোনো API; .data version counter এড়িয়ে যায় | আপনার version-এর docs; আপডেট no_grad-এর ভেতরে |
| Validation-এ no_grad বা inference_mode নেই | প্রতিটি batch-এ graph, memory অকারণে বাড়ে | Eval-এর ফল দেখুন: out.requires_grad False কি না |
losses.append(loss) | প্রতিটি loss পুরো graph ধরে রাখে | loss.item(); কয়েকশো ধাপে memory মাপুন |
| Custom Function-এর backward-এ ভুল সূত্র | Training চলে, কিন্তু ভুল দিকে | float64 input-এ gradcheck |
In-place error চাপা দিতে .clone() বা retain_graph সব জায়গায় | আসল কারণ লুকায়, memory বাড়ে | Anomaly detection দিয়ে কোন কাজ দায়ী আগে খুঁজুন |
Non-scalar output-এ backward(torch.ones_like(y)) না বুঝে | সেটি সব output-এর যোগফলের gradient, আলাদা সারি নয় | ছোট উদাহরণে jacobian()-এর সঙ্গে মেলান |
AI-কে কোডের সঙ্গে যাচাইও চাইতে বলুন:
AI-এর লেখা autograd কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১PyTorch কেন শুধু দরকারি মানই রাখে, আর in-place কাজ পাহারা দিতে version counter বেছে নিল?
Reverse mode-এর আসল দাম memory: forward-এর যে মান backward-এ লাগবে, সেটি forward শেষ হওয়া পর্যন্ত রাখতে হয়। বড় network-এ parameter-এর চেয়ে এই activation-ই প্রায়ই বেশি জায়গা নেয় (পর্ব ০৪-এ activation memory দেখেছেন)। তাই PyTorch প্রতিটি কাজের জন্য আলাদা করে ঠিক করে কী রাখবে: গুণে শুধু সেই গুণনীয়ক যার অপর দিকের gradient চাওয়া হয়েছে, যোগফলে শুধু আকার, sigmoid আর tanh-এ output, ReLU-তে output. অংশ ১-এর run-এ MulBackward0 w-কে রাখেনি, কারণ x-এর gradient কেউ চায়নি।
এই নকশার একটি পার্শ্বফল আছে: যে মান রাখা হলো, কেউ পরে in-place বদলে দিলে backward নিঃশব্দে ভুল সংখ্যা দেবে। দুটি সরল সমাধান ছিল: সব in-place নিষিদ্ধ করা, বা রাখার সময় সবসময় কপি করা। প্রথমটি memory-র জন্য কষ্টকর, কারণ in-place কাজ বড় tensor-এ memory বাঁচায়; দ্বিতীয়টি রাখার উদ্দেশ্যই নষ্ট করে। Paszke ও সহকর্মীদের ২০১৭ সালের বর্ণনা অনুযায়ী PyTorch মাঝের পথ নেয়: প্রতিটি tensor-এ একটি সস্তা গোনার সংখ্যা, রাখার সময় সেটি লিখে রাখা, আর backward-এ মিলিয়ে দেখা। খরচ প্রায় শূন্য, আর ভুল হলে স্পষ্ট error.
এর সীমাও আছে। .data এই পাহারা এড়িয়ে যায় (অংশ ৭-এর তৃতীয় ফাঁদ), আর error শুধু তখনই আসে যখন বদলানো মানটি সত্যিই রাখা হয়েছিল; তাই কোনো in-place কোড একটি মডেলে চলে, অন্য মডেলে ভাঙে। একই চিন্তা ডাটাবেসের optimistic concurrency control-এ দেখা যায়: লেখার সময় version মিলিয়ে দেখা, তালা না দিয়ে।
মূল উপলব্ধি: PyTorch memory বাঁচাতে কম রাখে আর version counter দিয়ে সেই সঞ্চয়কে নিরাপদ করে; error-টি শত্রু নয়, ভুল gradient-এর বিরুদ্ধে পাহারা।
প্র ০২no_grad থাকতেও PyTorch কেন inference_mode যোগ করল, আর কখন সেটি ক্ষতিকর হতে পারে?
no_grad graph বন্ধ করে, কিন্তু tensor-এর অন্য কিছু হিসাব চালু রাখে: প্রতিটি in-place কাজে version counter বাড়ানো, আর view-এর সঙ্গে মূল tensor-এর সম্পর্ক মনে রাখা। কারণ no_grad-এর ভেতরে তৈরি tensor পরে কোনো graph-এ ঢুকতে পারে, তখন সেই তথ্য লাগবে। কিন্তু production server-এ, যেখানে একটি model দিনে লাখবার শুধু পূর্বাভাস দেয়, এই সামান্য হিসাবও বাড়তি খরচ। PyTorch তাই ২০২১ সালের দিকে (১.৯ সংস্করণে, docs অনুযায়ী) inference_mode আনে: এর ভেতরে তৈরি tensor-এ version counter বা view-এর হিসাব থাকে না।
লাভ কতটা, তা model আর হার্ডওয়্যারের ওপর নির্ভর করে; ছোট কাজে প্রায় চোখে পড়ে না, অনেক ছোট op-ওয়ালা model-এ বেশি হতে পারে। তাই “inference_mode সবসময় দ্রুত” এমন সাধারণ দাবি না করে নিজের model-এ মাপা ভালো। দাম স্পষ্ট: অংশ ৩-এর run-এ inference tensor-কে graph-এ ঢোকাতেই RuntimeError. ধরুন একটি pipeline-এ একটি encoder inference_mode-এ embedding বানায়, আর পরে কেউ সিদ্ধান্ত নেয় সেই embedding-এর ওপর একটি ছোট স্তর শেখাবে; তখনই error. সমাধান clone() করে সাধারণ tensor বানানো, বা শুরু থেকেই no_grad.
একটি বাস্তব নিয়ম দাঁড়ায়: যে কোড শুধু পূর্বাভাস দেয় আর ফল কখনো training-এ যাবে না, সেখানে inference_mode; training loop-এর ভেতরের validation বা এমন জায়গা যেখানে ফল পরে graph-এ যেতে পারে, সেখানে no_grad. কোনোটিই model.eval()-এর বিকল্প নয়; সেটি dropout আর normalization-এর আচরণ বদলায়, পর্ব ০৯ আর ১০-এ আসবে।
মূল উপলব্ধি: inference_mode no_grad-এর কড়া রূপ: একটু কম খরচ, কিন্তু ফল আর কখনো autograd-এ ফেরে না; নিশ্চিত না হলে no_grad.
প্র ০৩দ্বিতীয় ক্রমের gradient deep learning-এ কোথায় কাজে লাগে, আর এর খরচ কেন সাবধানে মাপতে হয়?
Hessian-vector product-এর কৌশল নতুন নয়। Pearlmutter ১৯৯৪ সালে দেখান যে পুরো Hessian না বানিয়ে Hv প্রায় একটি gradient-এর কয়েক গুণ খরচে নির্ভুলভাবে পাওয়া যায়। এর ওপর দাঁড়িয়ে James Martens ২০১০ সালে “Hessian-free” optimization প্রস্তাব করেন, যেখানে conjugate gradient পদ্ধতি শুধু Hv দিয়ে Newton-এর ধাপ আনুমানিকভাবে বের করে। Newton আর L-BFGS-এর তত্ত্ব মেশিন লার্নিং কোর্সের পর্ব ১৫-এ আছে।
আজকের deep learning-এ দ্বিতীয় ক্রম আরও কয়েক জায়গায় আসে। Gulrajani ও সহকর্মীদের ২০১৭ সালের WGAN-GP-তে loss-এর ভেতরে gradient-এর norm-এর ওপর শাস্তি থাকে; তাই backward-এ gradient-এর gradient লাগে, অর্থাৎ create_graph. Finn ও সহকর্মীদের ২০১৭ সালের MAML (meta-learning) কয়েকটি ভেতরের gradient ধাপের মধ্য দিয়ে differentiate করে। Loss surface কতটা তীক্ষ্ণ, তা মাপতে Hessian-এর বড় eigenvalue খোঁজা হয় power iteration দিয়ে, যার প্রতিটি ধাপে একটি Hv লাগে।
খরচ দুই দিকে। সময়: একটি Hv মোটামুটি কয়েকটি backward-এর সমান। Memory: create_graph=True দিলে প্রথম backward-এর সব মাঝের ফলও graph-এ থাকে, তাই memory প্রায় দ্বিগুণ বা বেশি হতে পারে। তাই MAML-এর অনেক ব্যবহারে “first-order” আনুমানিকতা নেওয়া হয়, যেখানে দ্বিতীয় ক্রমের পদ বাদ। আর finite difference দিয়ে Hv সস্তা, কিন্তু অংশ ৪-এর ব্লকে দেখেছেন, ε বাছাই করা কঠিন।
মূল উপলব্ধি: দ্বিতীয় ক্রমের তথ্য দরকার হলে পুরো Hessian নয়, Hessian-vector product চান; create_graph-এর memory-দাম মেপে নিয়ে তবে ব্যবহার করুন।
প্র ০৪Straight-through estimator গাণিতিকভাবে “ভুল” gradient দেয় — তবু quantization আর discrete model-এ কেন ব্যাপকভাবে চলে?
Round, sign বা threshold-এর মতো কাজের derivative প্রায় সব জায়গায় শূন্য, আর লাফের বিন্দুতে সংজ্ঞায়িত নয়। এমন কাজ network-এর ভেতরে থাকলে backward সেখানে শূন্য দেয়, আর তার আগের সব স্তর কিছুই শেখে না। অনুশীলন ৪-এর run-এ torch.round-এর gradient (০, ০, ০). সমস্যাটি বাস্তব: কম bit-এ weight রাখা (quantization), binary network, বা বিচ্ছিন্ন code বাছাই করা model, সবখানেই এমন কাজ লাগে।
Straight-through estimator-এর ধারণা Hinton-এর বক্তৃতায় আগে উল্লেখ হয়েছিল বলে বলা হয়, আর Bengio, Léonard আর Courville ২০১৩ সালের একটি প্রবন্ধে একে নিয়মিতভাবে বিশ্লেষণ করেন: forward-এ আসল discrete কাজ, backward-এ যেন সেটি identity. গাণিতিকভাবে এটি পক্ষপাতদুষ্ট (biased) অনুমান, সত্যিকারের derivative নয়। তবু কাজ করে, কারণ round-এর আগের মানটি বাড়ালে round-এর পরের মানও গড়ে বাড়ে; দিকটা মোটামুটি ঠিক থাকে। van den Oord ও সহকর্মীদের ২০১৭ সালের VQ-VAE-তে code বাছাইয়ের ওপর দিয়ে gradient পার করতে এই কৌশল ব্যবহৃত হয়, আর quantization-aware training-এও এটি প্রচলিত।
সীমাও আছে। Gradient-এর পক্ষপাত কখনো training-কে অস্থির করে, আর ক্লিপ ছাড়া পার করলে বড় মানে ভুল সংকেত যায়; তাই অনেক রূপে শুধু একটি সীমার ভেতরে gradient পার হয়। আর এমন Function-এ gradcheck fail করবেই, তাই যাচাই হয় অন্য পথে: ছোট সমস্যায় শেখে কি না, আর সরল baseline-এর চেয়ে ভালো কি না।
মূল উপলব্ধি: custom backward দিয়ে gradient-কে ইচ্ছা করে বদলানো যায়; তখন সেটি একটি নকশাগত অনুমান, তাই গণিতে নয়, পরীক্ষায় তার মূল্য প্রমাণ করতে হয়।
প্র ০৫Autograd থাকতে gradient checking-এর দরকার কী, আর কোথায় এটি এখনো অপরিহার্য?
Autograd প্রোগ্রামের derivative নির্ভুলভাবে দেয়, যদি প্রতিটি মৌলিক কাজের backward ঠিক থাকে। সাধারণ কাজে (matmul, exp, sigmoid) PyTorch-এর backward বহু বছর ধরে পরীক্ষিত। কিন্তু যেই আপনি নিজের autograd.Function, নিজের GPU kernel, বা একটি নতুন সংখ্যাগত কৌশল লেখেন, সেই কাজের backward-এর একমাত্র পাহারাদার আপনি। অংশ ৫-এর BadSoftPlus দেখিয়েছে, এমন ভুল চোখে পড়া কঠিন: σ আর σ(1 − σ) দেখতে কাছাকাছি, আর training-এ loss তবুও কিছুটা কমতে পারে।
Gradient checking-এর ধারণা পুরোনো: বিভিন্ন কোর্স আর টিউটোরিয়ালে finite difference দিয়ে backpropagation যাচাই বহু বছর ধরে শেখানো হয়, আর পর্ব ০৪-এ আপনি নিজেও করেছেন। PyTorch এটিকে library-র অংশ করেছে: gradcheck প্রথম ক্রম, gradgradcheck দ্বিতীয় ক্রম যাচাই করে; PyTorch নিজের অনেক কাজের পরীক্ষায় এগুলো ব্যবহার করে। নিয়মগুলো সংখ্যাগত সত্য থেকে আসে: float64-এ চালানো (অংশ ৫-এর ব্লকে float32-এ ভুল প্রায় 2 × 10−2), ছোট এলোমেলো input নেওয়া, আর অমসৃণ বিন্দু (ReLU-র ০, |x|-এর ০) এড়ানো, কারণ সেখানে দুই দিকের finite difference নিজেই অর্থহীন।
gradcheck ধীর, কারণ প্রতিটি input-এর প্রতিটি ঘর দুবার সরাতে হয়; তাই ছোট আকারে চালানো হয়, পুরো model-এ নয়। বড় model-এ একই দর্শন অন্য রূপে আসে: এক batch overfit করা, loss-এর শুরুর মান তত্ত্বের সঙ্গে মেলানো, স্তর ধরে gradient-এর norm দেখা (পর্ব ১৮-এ)। আর পরীক্ষা হিসেবে gradcheck CI-তে রাখা যায়, যাতে পরে কেউ backward বদলালে ধরা পড়ে।
মূল উপলব্ধি: যেখানে backward আপনি নিজে লিখেছেন, সেখানে float64-এ gradcheck বাধ্যতামূলক; autograd-এর নির্ভুলতা শুধু ততটা, যতটা তার সবচেয়ে দুর্বল হাতে লেখা backward.
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান। PyTorch থাকলে নিজের যন্ত্রে চালিয়ে দেখুন।
-
পূর্ণ numerical: W = [[২, −১], [০, ৩], [১, ১]] (আকার ৩ × ২), x = (০.৫, ১), requires_grad=True, y = Wx. (ক)
y.backward(gradient=v), v = (১, −১, ২) হলে x.grad কত? (খ) zero করেy.sum().backward()চালালে? (গ) f(x) = x1² x2, x = (১, ২), v = (১, ০) হলে create_graph দিয়ে পাওয়া Hv কত?(ক) x.grad = Wᵀv = ১ × (২, −১) + (−১) × (০, ৩) + ২ × (১, ১) = (২ + ০ + ২, −১ − ৩ + ২) = (৪, −২). (খ) v = (১, ১, ১): তিন সারির যোগফল (২ + ০ + ১, −১ + ৩ + ১) = (৩, ৩). x-এর মান উত্তরে আসেনি, কারণ linear function-এর Jacobian ধ্রুবক। (গ) ∇f = (2x1x2, x1²); H = [[2x2, 2x1], [2x1, ০]] = [[৪, ২], [২, ০]]; Hv = (৪, ২). ডেস্কটপে চালালে তিনটি উত্তর
tensor([ 4., -2.]),tensor([3., 3.])আরtensor([4., 2.])আসে। -
সমালোচনা ও নকশা: একটি AI assistant বাজারঘরের model-এর জন্য এই কোড দিয়েছে। কোথায় কোথায় সমস্যা, আর কীভাবে লিখবেন?
losses = [] for X, y in loader: loss = ((X @ w - y) ** 2).mean() loss.backward() w.data -= 0.01 * w.grad losses.append(loss) val_pred = X_val @ w print(val_pred.numpy())১. zero_grad নেই:
w.gradধাপে ধাপে জমে (পর্ব ০৫)। ২.w.data -= …: চলে, কিন্তু version counter এড়িয়ে যায়; সঠিক রূপwith torch.no_grad(): w -= 0.01 * w.grad, তারপরw.grad = None. ৩.losses.append(loss)প্রতিটি graph ধরে রাখে;loss.item(). ৪. Validation-এ graph তৈরি হচ্ছে;with torch.no_grad():. ৫.val_pred.numpy()RuntimeError দেবে, কারণ val_pred-এর requires_grad True; no_grad-এর ভেতরে হিসাব করলে, বা.detach().numpy(). নকশা: loop-এর ছাঁচ zero → forward → loss → backward → no_grad-এ আপডেট; log-এ শুধু সংখ্যা; evaluation আলাদা function-এ no_grad দিয়ে। আসল প্রকল্পে হাতের আপডেটের বদলে optimizer, পর্ব ১০-এ। -
আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন কী ছাপবে।
w = torch.tensor([1.0, 2.0], requires_grad=True) y = (w * w.detach() + w).sum() y.backward() print(w.grad) with torch.no_grad(): z = w * 3 print(z.requires_grad, w.grad is None)tensor([2., 3.]) False Falsew.detach() ধ্রুবক c = (১, ২), তাই y = Σ(w·c + w), আর ∂y/∂w = c + ১ = (২, ৩). detach না থাকলে উত্তর হতো 2w + 1 = (৩, ৫). no_grad-এর ভেতরে z-এর requires_grad False; আর w.grad আগের backward থেকে (২, ৩), None নয়, কারণ no_grad gradient মোছে না। (ডেস্কটপের PyTorch 2.15.0-এ চালিয়ে মেলানো।)
-
কোড লিখুন: একটি
RoundSTEautograd.Function লিখুন: forward-এtorch.round, backward-এ gradient বদল ছাড়াই পার। x = (০.৪, ১.৬, −২.৩)-এtorch.round(x).sum()আরRoundSTE.apply(x).sum()-এর gradient তুলনা করুন। এতে gradcheck কেন fail করবে?class RoundSTE(torch.autograd.Function): # straight-through estimator @staticmethod def forward(ctx, x): return torch.round(x) @staticmethod def backward(ctx, g): return g # gradient সোজা পার করে দেওয়া x = torch.tensor([0.4, 1.6, -2.3], requires_grad=True) torch.round(x).sum().backward(); print("torch.round grad:", x.grad) x.grad = None RoundSTE.apply(x).sum().backward(); print("RoundSTE grad :", x.grad)সত্যিকারের run (ডেস্কটপ, PyTorch 2.15.0):
torch.round grad: tensor([0., 0., 0.]) RoundSTE grad : tensor([1., 1., 1.])torch.round-এর আসল derivative প্রায় সব জায়গায় ০, তাই আগের স্তর কিছুই শিখত না; STE ১ পার করে দেয়। backward-এ কোনো মান লাগে না, তাই save_for_backward নেই। gradcheck fail করবে, কারণ finite difference round-এর আসল derivative (০) মাপবে, আর analytical দিক ১ বলবে; এখানে অমিলটাই নকশা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি custom autograd.Function লিখে নিরাপদে ব্যবহারের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Derivative হাতে derive করে লেখা
- forward লেখা, backward-এ লাগবে এমন মান ctx.save_for_backward দিয়ে রাখা
- backward লেখা, প্রতিটি input-এর জন্য একটি gradient ফেরত
- ছোট float64 input (requires_grad=True) বানিয়ে gradcheck চালানো
- অমিল হলে numerical আর analytical Jacobian মিলিয়ে ভুল খোঁজা
- পাস করলে model-এ
.applyদিয়ে ব্যবহার
a = torch.rand(3, requires_grad=True); b = a * 2; c = b.sum(); c.backward(). retain_grad ব্যবহার করা হয়নি। backward-এর পর কোন tensor-এর .grad-এ মান থাকে?
- a, b আর c তিনটিরই
- শুধু c-এর
- শুধু a-এর, মান (২, ২, ২)
- কোনোটির নয়, কারণ retain_graph দেওয়া হয়নি
W = [[1, 0, 2], [3, 1, 0]] (আকার 2 × 3), x আকার (3,), requires_grad=True, y = W @ x. y.backward(gradient=torch.tensor([2., 1.]))-এর পর x.grad কত?
- (3, 4)
- (5, 1, 4)
- (4, 1, 2)
- RuntimeError, কারণ y scalar নয়
একটি tensor with torch.inference_mode():-এর ভেতরে তৈরি হলো। পরে training কোডে তাকে requires_grad থাকা weight-এর সঙ্গে গুণ করে backward ডাকা হলো। কী হবে?
- সাধারণভাবে চলবে, tensor-টি ধ্রুবক ধরা হবে
- mode শেষ হওয়ায় tensor-টি নিজে থেকে সাধারণ tensor হয়ে যাবে
- চলবে, কিন্তু weight-এর gradient শূন্য হবে
- RuntimeError: inference tensor backward-এর জন্য রাখা যায় না
f(x) = ¼‖x‖⁴, x = (1, 2, −1), v = (1, 1, 0). PyTorch-এ Hessian-vector product পেতে কোনটি সঠিক?
- g = grad(f(x), x, create_graph=True), তারপর grad(g @ v, x)
- g = grad(f(x), x), তারপর grad(g @ v, x)
- f(x).backward(retain_graph=True), তারপর x.grad @ v
- f(x).backward(), তারপর x.grad @ v
একটি custom autograd.Function-এর backward সঠিক, কিন্তু float32 input-এ torch.autograd.gradcheck fail করছে। সবচেয়ে সম্ভাব্য কারণ কী?
- Input খুব ছোট, gradcheck-এর বড় tensor লাগে
- Finite difference-এর numerical Jacobian float32-এর rounding-এ ভুল হয়; float64 লাগবে
- gradcheck-এর আগে backward একবার চালানো হয়নি
- float32-এ save_for_backward কাজ করে না
y = torch.sigmoid(x); y.mul_(2); y.sum().backward() (x requires_grad=True) চালালে RuntimeError আসে। কেন, আর সঠিক সমাধান কী?
- sigmoid-এর output-এর gradient নেওয়া যায় না; sigmoid বাদ দিতে হবে
- version counter বেড়েছে; y.data.mul_(2) লিখলে ঠিক হবে
- sigmoid backward-এর জন্য নিজের output রেখেছিল, in-place বদলে তার version বদলেছে; y = y * 2 লিখতে হবে
- retain_graph=True দিলে ঠিক হবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Node: প্রতিটি non-leaf-এর grad_fn শুধু দরকারি মান রাখে; gradient জমে শুধু requires_grad leaf-এ, non-leaf-এ চাইলে retain_grad বা hook.
- VJP: non-scalar output-এ
backward(gradient=v)= vᵀJ;sum()মানে v = সব ১; পুরো J কখনো বানানো হয় না। - Graph বন্ধ: no_grad (evaluation, আপডেট), inference_mode (শুধু inference, ফল আর graph-এ যায় না), detach (একটি শাখা কাটা), requires_grad_(False) (freeze).
- দ্বিতীয় ক্রম আর যাচাই: create_graph=True দিয়ে Hv = ∇(∇f · v); নিজের backward-এ float64-এ gradcheck.
- ফাঁদ: রাখা মানে in-place error দেয়,
.dataনিঃশব্দে ভুল করে; requires_grad tensor-এ.numpy()-এর আগে detach; integer tensor gradient নিতে পারে না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৯ · nn.Module দিয়ে model বানানো — layer, parameter আর state_dictপরবর্তী পর্বLayer নিবন্ধন, parameter গোনা, buffer, state_dict আর নিরাপদ load শেখা।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ০৯ (nn.Module) আর পর্ব ১০ (training loop) এখান থেকে খুলবেন।
- পর্ব ০৫ · নিজে বানানো autograd — computational graph আর reverse modeআগের পর্বValue class, forward বনাম reverse mode আর NumPy-তে tensor autograd.
- পর্ব ০৪ · Backpropagation হাতে-কলমেআগের পর্বMatrix backprop, gradient check আর activation-এর memory.
- মেশিন লার্নিং · পর্ব ১১ · Matrix calculusআগের ভিত্তিGradient, Jacobian আর Hessian-এর তত্ত্ব।
- মেশিন লার্নিং · পর্ব ১৫ · Second-order পদ্ধতিআগের ভিত্তিNewton, L-BFGS আর Hessian কোথায় কাজে লাগে।
- AI-এর ভিত্তি · পর্ব ১৯ · NumPy-র গতিপাশের ট্র্যাকMemory, dtype, view আর in-place কাজের খরচ।