এই পর্বে যা শিখবেন
- Chain rule-এর গুণফল থেকে vanishing আর exploding gradient কেন আসে, আর activation-এর ঢাল ও weight-এর মাপ কী ভূমিকা রাখে
- NumPy-তে আর PyTorch-এর backward hook দিয়ে স্তর ধরে gradient-এর norm মাপা
- Residual connection-এর derivative I + F′ কেন gradient বাঁচিয়ে রাখে, আর normalization ছাড়া এর কী ঝুঁকি
- ৪০ স্তরের plain আর residual MLP পাশাপাশি শেখানো আর loss curve পড়া
clip_grad_norm_আরclip_grad_value_-এর হিসাব, training loop-এ ঠিক কোথায় বসে, আর কোনটি কখন
পর্ব ০৩-এ দেখেছিলেন, দশটি sigmoid স্তরে gradient প্রায় শূন্য হয়ে যায়, আর তখন প্রতিকারের কথা এই পর্বে রেখেছিলাম। এখানে সমস্যাটি প্রথমে মাপা হবে, তারপর ব্যাখ্যা, তারপর দুটি প্রধান প্রতিকার: residual connection (gradient মিলিয়ে যাওয়ার বিরুদ্ধে) আর gradient clipping (বিস্ফোরণের বিরুদ্ধে)। পাতার তিনটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে। PyTorch-এর সব সংখ্যা ডেস্কটপের সত্যিকারের run থেকে, CPU-তে।
১ · সমস্যাটা কী — অনেক ছোট সংখ্যার গুণফল
একটি গ্রামে ত্রাণের খবর মুখে মুখে ত্রিশ জন পার হয়ে পৌঁছাবে। প্রত্যেকে যা শুনলেন তার চার ভাগের এক ভাগ মনে রেখে পরের জনকে বলেন। তৃতীয় জনের কাছেই খবর প্রায় নেই, ত্রিশতম জনের কাছে শূন্য। উল্টোটাও হয়: প্রত্যেকে একটু বাড়িয়ে বলেন, দেড় গুণ করে; ত্রিশ জন পরে “দশ বস্তা চাল” হয়ে যায় কয়েক লাখ বস্তা। Backpropagation-এ gradient-ও এমনই একটি খবর, যা output থেকে স্তরে স্তরে পেছনে প্রথম স্তরে যায়।
পর্ব ০৪-এ chain rule দিয়ে দেখেছিলেন, একটি স্তরের gradient হলো তার ওপরের সব স্তরের স্থানীয় derivative-এর গুণফল। একটি scalar শিকলে, যেখানে প্রতিটি স্তর hk = σ(w·hk−1):
∂h10/∂h0 = ∏k=110 w · σ′(zk)
এখানে σ হলো sigmoid, zk = w·hk−1 স্তরের input, আর σ′(z) = σ(z)(1 − σ(z)), যার সর্বোচ্চ মান z = 0-তে ০.২৫. w = 1 হলে প্রতিটি গুণনীয়ক ০.২৫-এর বেশি হতে পারে না। নিচের stepper h0 = 1 থেকে শুরু করে আসল সংখ্যাগুলো দেখায়।
| স্তর | |
| z আর h | |
| এই স্তরের σ′ | |
| মোট গুণফল |
- প্রথম স্তর. z = 1, σ(1) = ০.৭৩১১, ঢাল ০.৭৩১১ × ০.২৬৮৯ = ০.১৯৬৬. প্রথম ধাপেই gradient পাঁচ ভাগের এক ভাগ।
- দ্বিতীয় স্তর. ঢাল ০.২১৯৪; গুণফল ০.১৯৬৬ × ০.২১৯৪ = ০.০৪৩১৩.
- তৃতীয় স্তর. গুণফল ০.০০৯৬৪, অর্থাৎ শুরুর এক শতাংশেরও কম।
- পঞ্চম স্তর. h প্রায় ০.৬৫৯-এ থিতু, প্রতিটি ঢাল প্রায় ০.২২৫; গুণফল ৪.৮৬ × ১০−৪.
- দশম স্তর. ২.৭৯ × ১০−৭. প্রথম স্তরের weight এখন শেষ স্তরের তুলনায় প্রায় ৩৬ লাখ গুণ ধীরে বদলাবে।
- সেরা অবস্থা. প্রতিটি z ঠিক ০ হলেও ০.২৫১০ = ৯.৫৪ × ১০−৭. w বড় করে গুণনীয়ক ১ বানাতে গেলে (w = 4) z বড় হয়ে sigmoid saturate করে, ঢাল আরও কমে।
দুটি পরিণতি। Vanishing gradient: শুরুর স্তরগুলো প্রায় শেখে না, অথচ সেগুলোই কাঁচা input থেকে মূল feature বের করার কথা। Loss কমে খুব ধীরে, বা একটি মানে আটকে থাকে। Exploding gradient: গুণনীয়ক ১-এর বেশি হলে gradient এত বড় হয় যে একটি আপডেটে weight লাফিয়ে যায়, loss হঠাৎ বেড়ে inf, তারপর NaN. দুটিই একই মুদ্রার দুই পিঠ: গভীরতা মানে গুণফল, আর গুণফল ১ থেকে সামান্য দূরে থাকলেও গভীরতায় সূচকীয়ভাবে বড় হয়।
২ · কেন ঘটে — Jacobian-এর শিকল আর তিনটি উৎস
বাস্তব network-এ প্রতিটি স্তর vector থেকে vector, তাই scalar derivative-এর জায়গায় আসে Jacobian matrix Jl = ∂hl/∂hl−1 (মেশিন লার্নিং কোর্সের পর্ব ১১)। hl = f(Wlhl−1) স্তরের জন্য Jl = diag(f′(zl)) · Wl, অর্থাৎ activation-এর ঢাল দিয়ে প্রতিটি সারি গুণ করা weight matrix. Backward pass আসলে vector-Jacobian product-এর শিকল (পর্ব ০৫):
∂L/∂h0 = J1T J2T ⋯ JLT · ∂L/∂hL
একটি matrix কোনো vector-কে সর্বোচ্চ কত গুণ টানতে পারে, তা তার সবচেয়ে বড় singular value σmax বলে, আর সবচেয়ে কম কত গুণ, তা σmin. তাই gradient-এর L2 norm শিকলের শেষে মোটামুটি ∏ σmax(Jl)-এর নিচে থাকে। সব স্তরে σmax < 1 হলে gradient সূচকীয়ভাবে মিলিয়ে যায় নিশ্চিত; সাধারণ অবস্থায় গড় বৃদ্ধির হার ১-এর কম না বেশি, সেটাই ঠিক করে কোনটি ঘটবে। এই হার তিনটি জিনিস থেকে আসে:
Activation-এর ঢাল
Sigmoid-এর ঢাল ≤ ০.২৫, tanh-এর ≤ ১ কিন্তু দুই প্রান্তে প্রায় ০ (saturation). ReLU-এর ঢাল ঠিক ০ বা ১, তাই নিজে ক্ষয় করে না, কিন্তু প্রায় অর্ধেক unit বন্ধ থাকে।
Weight-এর মাপ
n প্রস্থের স্তরে weight-এর variance v হলে সংকেত প্রতি স্তরে প্রায় √(n·v·c) গুণ হয়, c activation-এর ওপর নির্ভর করে (ReLU-তে ½)। n·v·c = 1 হলে মাপ টিকে থাকে; এই হিসাবই Xavier আর He init-এর ভিত্তি, পর্ব ১৪ (Weight initialization আর normalization)-এর বিষয়।
গভীরতা
প্রতি স্তরে মাত্র ০.৯ গুণ হলেও ৩০ স্তরে ০.৯৩০ ≈ ০.০৪২, আর ১.১ গুণ হলে ১.১৩০ ≈ ১৭.৪. গভীরতা ছোট ভুলকে সূচকীয় করে।
একটি সূক্ষ্ম কথা: gradient-এর দুটি মাপ আছে। ∂L/∂hl (activation-এর gradient) শিকল ধরে পেছনে যায়; ∂L/∂Wl = δl hl−1T (weight-এর gradient) তার সঙ্গে সেই স্তরের input-ও গুণ করে। তাই forward-এ সংকেত বড় হলে weight-এর gradient বড় দেখাতে পারে, যদিও পেছনে যাওয়া খবর ছোট। রোগনির্ণয়ে দুটিই দেখা ভালো; নিচের পরীক্ষাগুলো মূলত প্রথমটি মাপে।
৩ · NumPy-তে মাপা — ৩০ স্তরে sigmoid, tanh আর ReLU
নিচের ব্লক ৬৪ প্রস্থের ৩০টি স্তরের একটি MLP-এ forward চালায়, তারপর ওপরে একটি এলোমেলো gradient বসিয়ে হাতে backward করে, প্রতিটি স্তরে ‖∂L/∂h‖ লিখে রাখে। পাঁচটি অবস্থা: sigmoid, tanh আর ReLU-তে weight-এর variance 1/n; ReLU-তে He init (variance 2/n); আর He-এর চেয়ে ১.৫ গুণ বড় standard deviation, ইচ্ছা করে।
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
L, n, B = 30, 64, 64 # স্তর, প্রস্থ, batch
X = rng.normal(size=(B, n))
G_top = rng.normal(size=(B, n)) # ওপর থেকে আসা gradient, ধরা যাক loss থেকে
sig = lambda z: 1 / (1 + np.exp(-z))
relu = lambda z: np.maximum(z, 0)
cases = { # নাম: (activation, output h থেকে derivative, weight-এর variance × n)
"sigmoid": (sig, lambda h: h * (1 - h), 1.0),
"tanh": (np.tanh, lambda h: 1 - h**2, 1.0),
"relu": (relu, lambda h: (h > 0) * 1.0, 1.0),
"relu He": (relu, lambda h: (h > 0) * 1.0, 2.0),
"relu He x1.5": (relu, lambda h: (h > 0) * 1.0, 2.0 * 1.5**2)}
fig, ax = plt.subplots(figsize=(6.4, 3.8))
print("activation ‖∂L/∂h‖: layer 1 layer 15 layer 30")
for name, (f, df, var) in cases.items():
Ws = [rng.normal(0, np.sqrt(var / n), size=(n, n)) for _ in range(L)]
hs = [X]
for W in Ws: # forward: h_l = f(h_(l-1) W_l)
hs.append(f(hs[-1] @ W))
g, norms = G_top, []
for l in range(L, 0, -1): # backward: ওপর থেকে নিচে
g = (g * df(hs[l])) @ Ws[l - 1].T # ∂L/∂h_(l-1) = (∂L/∂h_l ⊙ f′) Wᵀ
norms.append(np.linalg.norm(g))
norms = norms[::-1] # এখন index 0 = স্তর 1
print(f"{name:13s} {norms[0]:16.2e} {norms[14]:10.2e} {norms[-1]:10.2e}")
ax.semilogy(range(1, L + 1), norms, marker="o", ms=3, label=name)
ax.set_xlabel("layer (1 = next to the input)")
ax.set_ylabel("gradient norm (log scale)")
ax.set_title("30-layer plain MLP: gradient norm per layer")
ax.legend(fontsize=8)
plt.show()আউটপুট (পাতার Python-এ চালানো; সঙ্গে log-scale-এর ছবি):
activation ‖∂L/∂h‖: layer 1 layer 15 layer 30
sigmoid 1.00e-17 6.58e-09 1.51e+01
tanh 9.67e+00 3.77e+01 6.39e+01
relu 1.53e-03 3.00e-01 4.80e+01
relu He 4.76e+01 4.89e+01 6.60e+01
relu He x1.5 3.51e+06 2.12e+04 9.39e+01
ছবিতে log scale-এ প্রতিটি রেখা প্রায় সোজা, কারণ ক্ষয় বা বৃদ্ধি সূচকীয়। Sigmoid: ৩০ স্তরে ১৫ থেকে ১০−১৭, প্রায় ১৮ ঘর নিচে; এমন gradient-এ প্রথম স্তরের weight কার্যত নড়বেই না। tanh: এই init-এ মাত্র প্রায় ৬.৬ গুণ কমেছে; ছোট weight-এ tanh প্রায় সরলরেখার মতো চলে, তাই ক্ষয় ধীর। ReLU, variance 1/n: প্রতি স্তরে সংকেতের variance অর্ধেক, তাই প্রায় ৩১,০০০ গুণ ক্ষয়। ReLU He: variance 2/n সেই অর্ধেক পূরণ করে, gradient প্রায় সমান। He × ১.৫: প্রতি স্তরে প্রায় ১.৪ গুণ বৃদ্ধি, ৩০ স্তরে প্রায় ৩৭,০০০ গুণ: exploding. একই ReLU, শুধু weight-এর মাপ বদলে তিনটি ভিন্ন ভাগ্য।
৪ · PyTorch-এ hook দিয়ে স্তর ধরে মাপা
বাস্তব model-এ হাতে backward লেখেন না; PyTorch-কে জিজ্ঞেস করতে হয়। পর্ব ০৮-এ tensor-এর gradient hook দেখেছেন। পুরো একটি স্তরের (পর্ব ০৯-এর nn.Module) output-এর gradient পেতে register_full_backward_hook: backward যখন ওই module পার হয়, hook grad_output হাতে পায়, যা ∂L/∂hl. ডেটা বাজারঘরের seed দিয়ে বানানো কাল্পনিক গ্রাহক-ভাগ: ১৬টি feature, ৩টি segment, ৪০৯৬ সারি। Model-এ input স্তর, ৩০টি (Linear 64 → 64, activation) block আর output স্তর; init PyTorch-এর default.
def layer_grad_norms(model):
norms = {}
def make_hook(i):
def hook(module, grad_input, grad_output):
norms[i] = grad_output[0].norm().item() # ‖∂L/∂h_i‖
return hook
handles = [b.register_full_backward_hook(make_hook(i + 1))
for i, b in enumerate(model.blocks)]
loss_fn(model(xb), yb).backward()
for h in handles:
h.remove() # কাজ শেষে hook সরানো
w = [b[0].weight.grad.norm().item() for b in model.blocks]
return norms, wসত্যিকারের run (ডেস্কটপ, CPU-তে; লেখার সময়ের সংস্করণ):
~/bazarghar-dl$ uv run python grad_norms.py
Python 3.14.4 | torch 2.15.0+cpu
30 hidden layers, width 64, PyTorch default init, batch 256
model layer 1 layer 5 layer10 layer20 layer30 first/last
plain sigmoid 0.0e+00 0.0e+00 2.6e-19 9.2e-11 3.0e-02 0.0e+00
weight.grad 0.0e+00 0.0e+00 5.5e-19 1.8e-10 5.7e-02
plain tanh 1.4e-09 1.6e-08 3.9e-07 1.1e-04 3.0e-02 4.6e-08
weight.grad 1.3e-08 1.8e-08 2.8e-07 7.9e-05 1.8e-02
plain relu 3.0e-13 8.4e-12 7.0e-10 6.0e-06 3.0e-02 1.0e-11
weight.grad 1.6e-12 2.7e-12 1.8e-10 1.6e-06 1.1e-02
residual relu 4.8e+00 2.2e+00 6.4e-01 1.2e-01 4.6e-02 1.1e+02
weight.grad 5.2e+01 9.0e+01 1.3e+02 2.5e+02 9.6e+02
residual tanh 2.6e-01 1.7e-01 1.1e-01 5.9e-02 3.5e-02 7.3e+00
weight.grad 1.8e+00 1.9e+00 1.9e+00 1.6e+00 1.2e+00
prenorm res relu 1.7e-01 1.0e-01 7.2e-02 4.9e-02 3.7e-02 4.5e+00
weight.grad 1.3e+00 1.0e+00 7.8e-01 6.1e-01 5.1e-01
প্রথম তিন সারি plain network. Sigmoid-এ প্রথম পাঁচ স্তরের gradient ঠিক ০.০: সংখ্যাটি এত ছোট যে float32-এ underflow করেছে; ওই স্তরগুলো আক্ষরিক অর্থেই কিছু শিখবে না। এবার tanh আর ReLU-ও মিলিয়ে যাচ্ছে, যা পাতার ব্লকে হয়নি। কারণ PyTorch-এর nn.Linear-এর default init-এ weight-এর variance প্রায় 1/(3n) (পর্ব ০৯), পাতার 1/n-এর তিন ভাগের এক ভাগ। ReLU-তে তাই প্রতি স্তরে variance-এর গুণক প্রায় n · 1/(3n) · ½ = 1/6. এই ফলাফলই বলে, গভীর network-এ default init-এর ওপর চোখ বুজে ভরসা করা যায় না।
পরের তিন সারি residual (অংশ ৫-এর বিষয়)। Residual ReLU-তে gradient আর মিলিয়ে যায়নি, বরং উল্টো: প্রথম স্তরে শেষ স্তরের প্রায় ১১০ গুণ। প্রতিটি block আগের h-এর সঙ্গে আরও কিছু যোগ করে, তাই একটি স্থূল হিসাবে মাপ প্রতি block-এ মোটামুটি (1 + 1/6) গুণ ধরা যায়, আর (7/6)৩০ ≈ ১০২, মাপা অনুপাতের কাছাকাছি; এটি নিখুঁত তত্ত্ব নয়, শুধু প্রবণতার আভাস। শেষ সারিতে প্রতিটি block-এর আগে LayerNorm (pre-norm) বসালে বৃদ্ধি থামে: ৪.৫ গুণ মাত্র। weight.grad-এর সারিও দেখুন: residual ReLU-তে শেষ স্তরের weight-gradient ৯৬০, কারণ সেখানে input h নিজেই বিশাল (অংশ ২-এর সূক্ষ্ম কথা)।
৫ · Residual connection — x + F(x) কেন gradient বাঁচিয়ে রাখে
২০১৫ সালে Kaiming He, Xiangyu Zhang, Shaoqing Ren আর Jian Sun-এর “Deep Residual Learning for Image Recognition” প্রবন্ধ (CVPR ২০১৬-তে প্রকাশিত) একটি সরল বদল আনে: প্রতিটি block-এ hl = f(hl−1)-এর বদলে
hl = hl−1 + F(hl−1)
এখানে F হলো block-এর শেখা অংশ (এক বা দুটি স্তর), আর যোগের পথটিকে বলে residual বা skip connection. F = 0 হলে block হুবহু input ফেরত দেয়, অর্থাৎ identity mapping. Block-কে পুরো রূপান্তর শিখতে হয় না, শুধু input থেকে “কতটা বদল” (residual) শিখতে হয়। Derivative নিলে:
∂hl/∂hl−1 = I + ∂F/∂hl−1 = I + F′
I হলো identity matrix. Backward-এ তাই প্রতিটি block-এ gradient দুই পথে ভাগ হয়: shortcut দিয়ে অবিকৃত (× I), আর F দিয়ে (× F′)। F′ ছোট হলেও গুণনীয়ক প্রায় ১, ০ নয়।
Plain block বনাম residual block — backward-এ gradient-এর পথ
Plain: gradient must pass through F′. Residual: an identity path adds g directly.
| ধাপ | |
| এই block-এর F′ | |
| শুধু F-এর পথ: ∏ F′ | |
| residual: ∏ (1 + F′) |
- Block ১. x = 0.5, F-এর input 0.25; F′ = 0.5 × σ′(0.25) = ০.১২৩১. শুধু F-এর পথে gradient আট ভাগের এক ভাগ, residual-এ ১.১২৩১ গুণ।
- Block ২. F′ = ০.১১৬৬. F-এর পথে গুণফল ০.০১৪৩৫; residual-এ ১.১২৩১ × ১.১১৬৬ = ১.২৫৪০.
- Block ৩. F-এর পথে ০.০০১৫১, অর্থাৎ হাজার ভাগের দেড় ভাগ; residual-এ ১.৩৮৫৭.
- Block ৫. x বড় হয়ে sigmoid saturate করতে শুরু করেছে, F′ = ০.০৭০৮. F-এর পথে ৯.৫ × ১০−৬; residual-এ ১.৬১৬২.
- Block ১০. F-এর পথে ৭.৬ × ১০−১৪, কার্যত শূন্য। Residual-এ ১.৮৫৬১: প্রতিটি গুণনীয়ক 1 + F′ ≥ 1, তাই ক্ষয় অসম্ভব। উল্টো দিকের ঝুঁকিও এখান থেকে বোঝা যায়: F′ সব block-এ ধনাত্মক হলে গুণফল ধীরে বাড়ে।
- Matrix রূপ. পাতার ব্লক ২-এ একটি ৬৪ প্রস্থের sigmoid স্তরের Jacobian J-এর singular value ০.০০৪০৩ থেকে ০.৪৪২; I + J-এর ০.৭২৮ থেকে ১.৩২৫. Identity যোগ করায় সব দিকের টান ১-এর আশপাশে চলে এসেছে।
এবার একই ৩০ স্তরের পরীক্ষা residual block দিয়ে। চতুর্থ অবস্থায় F-এর weight-এর variance 1/(nL), অর্থাৎ শুরুতে প্রতিটি block প্রায় identity; এটি একটি পরিচিত কৌশলের সরল রূপ।
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
L, n, B = 30, 64, 64
X, G_top = rng.normal(size=(B, n)), rng.normal(size=(B, n))
sig, relu = lambda z: 1 / (1 + np.exp(-z)), lambda z: np.maximum(z, 0)
cases = { # নাম: (F-এর activation, derivative, variance × n, shortcut আছে?)
"sigmoid plain": (sig, lambda a: a * (1 - a), 1.0, False),
"sigmoid residual": (sig, lambda a: a * (1 - a), 1.0, True),
"relu residual": (relu, lambda a: (a > 0) * 1.0, 1.0, True),
"relu residual small": (relu, lambda a: (a > 0) * 1.0, 1.0 / L, True)}
fig, ax = plt.subplots(figsize=(6.4, 3.8))
print("model layer 1 layer 30 ‖h_30‖")
for name, (f, df, var, res) in cases.items():
Ws = [rng.normal(0, np.sqrt(var / n), size=(n, n)) for _ in range(L)]
hs, Fs = [X], []
for W in Ws: # h_l = h_(l-1) + F(h_(l-1))
Fs.append(f(hs[-1] @ W))
hs.append(hs[-1] + Fs[-1] if res else Fs[-1])
g, norms = G_top, []
for l in range(L, 0, -1):
branch = (g * df(Fs[l - 1])) @ Ws[l - 1].T # F-এর পথ: g·F′
g = g + branch if res else branch # shortcut-এর পথ: g·I
norms.insert(0, np.linalg.norm(g)) # সামনে বসানো: index 0 = স্তর 1
print(f"{name:20s} {norms[0]:8.2e} {norms[-1]:10.2e} {np.linalg.norm(hs[-1]):9.1e}")
ax.semilogy(range(1, L + 1), norms, marker="o", ms=3, label=name)
W = rng.normal(0, np.sqrt(1 / n), size=(n, n)); a = sig(rng.normal(size=n) @ W)
J = (W * (a * (1 - a))).T # একটি sigmoid স্তরের Jacobian
for name, M in [("J", J), ("I + J", np.eye(n) + J)]:
s = np.linalg.svd(M, compute_uv=False)
print(f"singular values of {name:5s}: max {s.max():.3f}, min {s.min():.5f}")
ax.set_xlabel("layer"); ax.set_ylabel("gradient norm (log scale)")
ax.set_title("Plain vs residual, 30 layers"); ax.legend(fontsize=8)
plt.show()আউটপুট (পাতার Python-এ চালানো):
model layer 1 layer 30 ‖h_30‖
sigmoid plain 1.00e-17 1.51e+01 3.2e+01
sigmoid residual 7.97e+01 6.45e+01 1.0e+03
relu residual 8.25e+05 7.76e+01 3.7e+06
relu residual small 9.91e+01 6.53e+01 2.9e+02
singular values of J : max 0.442, min 0.00403
singular values of I + J: max 1.325, min 0.72753
Sigmoid residual: একই sigmoid, একই weight-এর মাপ, কিন্তু প্রথম স্তরের gradient ১০−১৭ থেকে ৮০-এ; ৩০ স্তর জুড়ে প্রায় সমতল রেখা। ReLU residual: উল্টো সমস্যা, প্রথম স্তরে প্রায় ৮ লাখ, আর forward-এর ‖h30‖ ৩৭ লাখ। ReLU-র output সবসময় অ-ঋণাত্মক, প্রতিটি block শুধু যোগ করে, তাই সংকেত আর gradient দুটিই সূচকীয়ভাবে বাড়ে; অংশ ৪-এর PyTorch run-এ একই প্রবণতা ছোট মাপে দেখেছেন। ছোট init: block-গুলো শুরুতে প্রায় identity, তাই দুটিই নিয়ন্ত্রণে। শিক্ষা: residual vanishing সারায়, কিন্তু মাপ নিয়ন্ত্রণে রাখতে লাগে normalization বা যত্নশীল init.
৬ · ৪০ স্তরের plain বনাম residual — পাশাপাশি training
মাপা gradient থেকে আসল প্রশ্নে: network কি শেখে? একই বাজারঘর ডেটা (৩০৭২ সারি train, ১০২৪ validation), ৪০টি ReLU block, প্রস্থ ৬৪, AdamW lr 1e-3, batch ১২৮, ১২ epoch, একই seed. তিনটি model: plain, residual (h + F(h)) আর pre-norm residual (h + F(LayerNorm(h))). প্রতি epoch-এ train loss লেখা হয়েছে (পর্ব ১০-এর loop).
~/bazarghar-dl$ uv run python train_deep.py
40 hidden layers, width 64, ReLU, AdamW lr 1e-3, batch 128, 12 epochs
ln(3) = 1.0986 (loss of a model that always says 1/3)
plain val acc 0.363 | last-batch grad norm, layer 1: 2.6e-18 | 1.7 s on CPU
residual val acc 0.683 | last-batch grad norm, layer 1: 6.0e-01 | 1.6 s on CPU
prenorm residual val acc 0.699 | last-batch grad norm, layer 1: 9.0e-01 | 2.7 s on CPU
epoch plain residual prenorm residual
1 1.0961 194.4191 1.4333
2 1.0955 4.4705 0.6982
3 1.0950 1.1243 0.5920
4 1.0950 0.9216 0.5113
…
10 1.0950 0.7093 0.2030
11 1.0950 0.6886 0.1868
12 1.0950 0.6745 0.1162
৪০ স্তরের তিনটি MLP-এর train loss
Train loss per epoch, one local run (values above 1.6 drawn at the top edge)
Plain: loss ১.০৯৫, প্রায় ln 3 = ১.০৯৮৬, অর্থাৎ model তিনটি segment-কেই প্রায় ১/৩ সম্ভাবনা দিচ্ছে, ইনপুট যা-ই হোক। প্রথম স্তরের gradient ২.৬ × ১০−১৮; শুধু শেষ কয়েকটি স্তর আর bias নড়ছে, তাই validation accuracy ০.৩৬৩, প্রায় সবচেয়ে বড় segment-এর অনুপাত। এটি overfitting নয়, train loss-ই কমেনি: degradation-এর চেহারা। Residual: প্রথম epoch-এ গড় loss ১৯৪, কারণ ৪০টি block যোগ করে logit বিশাল হয়েছিল (অংশ ৫-এর ReLU residual); AdamW-এর আপডেটের মাপ gradient-এর মাপের ওপর প্রায় নির্ভর করে না বলে run টিকে গেছে আর epoch ৩ থেকে স্বাভাবিকভাবে শিখেছে। Pre-norm residual: প্রতিটি F-এর আগে LayerNorm সংকেতের মাপ ঠিক রাখে, loss ০.১১৬. Train loss ০.১২ কিন্তু validation accuracy ০.৬৯৯: ছোট ডেটায় বড় model overfit করছে, যার প্রতিকার পর্ব ১৬ (Deep learning-এ regularization)-এর বিষয়। এক seed-এর এক run, CPU-তে কয়েক সেকেন্ড; ধরনটি স্পষ্ট, কিন্তু সংখ্যাগুলো সাধারণ ফল নয়।
Normalization আর residual আলাদা কাজ করে: residual gradient-এর পথ খোলা রাখে, normalization প্রতিটি স্তরে সংকেতের মাপ ঠিক রাখে। BatchNorm আর LayerNorm-এর ভেতরের হিসাব, আর pre-norm বনাম post-norm-এর আলোচনা পর্ব ১৪-এ; Transformer-এর প্রতিটি block ঠিক এই “residual + LayerNorm” কাঠামো (পর্ব ৩৫, Transformer block), আর ছবির জন্য ResNet-এর পুরো স্থাপত্য পর্ব ২২ (ResNet আর আধুনিক CNN)-এ।
৭ · Exploding gradient আর gradient clipping — norm ধরে না value ধরে
Exploding gradient-এর সরাসরি প্রতিরক্ষা gradient clipping: আপডেটের আগে gradient-কে সীমায় আনা। PyTorch-এ দুটি function:
torch.nn.utils.clip_grad_norm_(params, max_norm): সব parameter-এর gradient মিলিয়ে একটি মোট norm ‖g‖ বের করে; ‖g‖ > max_norm হলে সবকিছুকে max_norm / (‖g‖ + 10−6) দিয়ে গুণ করে। দিক অপরিবর্তিত, শুধু দৈর্ঘ্য ছোট। ফেরত দেয় ছাঁটার আগের মোট norm, যা লগ করার জন্য চমৎকার।torch.nn.utils.clip_grad_value_(params, clip_value): প্রতিটি ঘর আলাদাভাবে [−c, c]-এ কাটে। সহজ, কিন্তু দিক বদলাতে পারে; কিছু ফেরত দেয় না।
| ধাপ | |
| হিসাব | |
| ফল |
- বর্গের যোগ. সব parameter-এর সব ঘরের বর্গ একসঙ্গে যোগ হয়; এখানে একটি parameter, যোগফল ১৬৯.
- মোট norm. √১৬৯ = ১৩, সীমা ৫-এর বেশি।
- গুণক. ৫ / ১৩ ≈ ০.৩৮৪৬; হর-এ ১০−৬ শূন্য দিয়ে ভাগ ঠেকায়। গুণক ১-এর বেশি হলে ১-এ আটকে যায়, ছোট gradient বাড়ানো হয় না।
- গুণ. [১.১৫৩৮, ১.৫৩৮৫, ৪.৬১৫৪]; norm ঠিক ৫, অনুপাত ৩ : ৪ : ১২ অপরিবর্তিত।
- Value ধরে. সীমা ২ হলে তিনটি ঘরই ২: আসলে যেটি প্রধান ছিল (১২), তার গুরুত্ব হারিয়ে গেল। আগের দিকের সঙ্গে cosine ০.৮৪৩৮.
- ফেরত মান.
clip_grad_norm_১৩.০ ফেরত দেয়, ছাঁটার পরের ৫ নয়। প্রতি ধাপে এটি লগ করলে বোঝা যায় বিস্ফোরণ কখন আর কত বড়।
import numpy as np
np.set_printoptions(precision=4, suppress=True)
def clip_by_norm(grads, max_norm): # clip_grad_norm_-এর হুবহু হিসাব
total = np.sqrt(sum(np.sum(g**2) for g in grads))
coef = min(1.0, max_norm / (total + 1e-6))
return [g * coef for g in grads], total
def clip_by_value(grads, c): # clip_grad_value_
return [np.clip(g, -c, c) for g in grads]
def cos(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
for g in [np.array([3.0, 4.0, 12.0]), # বড়, একটি ঘর প্রধান
np.array([0.3, -0.4, 0.0]), # ছোট: কিছুই বদলাবে না
np.array([900.0, 1.0, -2.0])]: # একটি ঘর বিস্ফোরিত
(gn,), total = clip_by_norm([g], max_norm=5.0)
(gv,) = clip_by_value([g], c=2.0)
print(f"g = {g} ‖g‖ = {total:.2f}")
print(f" by norm (5): {gn} ‖·‖ = {np.linalg.norm(gn):.3f} cos = {cos(g, gn):.4f}")
print(f" by value (2): {gv} ‖·‖ = {np.linalg.norm(gv):.3f} cos = {cos(g, gv):.4f}")
# দুটি parameter tensor মিলিয়ে একটি মোট norm: আলাদা আলাদা নয়
W_grad, b_grad = np.array([[6.0, 0.0], [0.0, 8.0]]), np.array([0.0, 0.0])
(cw, cb), total = clip_by_norm([W_grad, b_grad], max_norm=1.0)
print("W.grad and b.grad together: total norm", total, "-> after:", cw.ravel(), cb)আউটপুট (পাতার Python-এ চালানো):
g = [ 3. 4. 12.] ‖g‖ = 13.00
by norm (5): [1.1538 1.5385 4.6154] ‖·‖ = 5.000 cos = 1.0000
by value (2): [2. 2. 2.] ‖·‖ = 3.464 cos = 0.8438
g = [ 0.3 -0.4 0. ] ‖g‖ = 0.50
by norm (5): [ 0.3 -0.4 0. ] ‖·‖ = 0.500 cos = 1.0000
by value (2): [ 0.3 -0.4 0. ] ‖·‖ = 0.500 cos = 1.0000
g = [900. 1. -2.] ‖g‖ = 900.00
by norm (5): [ 5. 0.0056 -0.0111] ‖·‖ = 5.000 cos = 1.0000
by value (2): [ 2. 1. -2.] ‖·‖ = 3.000 cos = 0.6685
W.grad and b.grad together: total norm 10.0 -> after: [0.6 0. 0. 0.8] [0. 0.]
ছোট gradient দুই পদ্ধতিতেই অপরিবর্তিত: clipping শুধু বড় ধাপে কাজ করে। তৃতীয় vector-এ একটি ঘর বিস্ফোরিত; norm ধরে কাটলে দিক অক্ষত থাকে কিন্তু বাকি দুটি ঘর প্রায় শূন্য হয়ে যায়, আর value ধরে কাটলে সব ঘর সমান গুরুত্ব পায়, cosine ০.৬৬৮৫. কোনটি “ঠিক”, তা প্রসঙ্গ বলে; norm clipping প্রচলিত কারণ এটি gradient-এর দিক, অর্থাৎ loss কমার দিক, রাখে। শেষ লাইন: দুটি tensor-এর মোট norm √(36 + 64) = ১০, তাই দুটিই একই গুণক ০.১ পায়।
এবার সত্যিকারের বিস্ফোরণ। ১৬টি ReLU block-এর plain MLP, He init-এর ১.৬ গুণ weight (ইচ্ছা করে বড়), SGD lr 0.05, ৩০০ ধাপ। তিনটি run: clipping নেই; clip_grad_value_(0.01); clip_grad_norm_(max_norm=1.0). প্রথম দুটিতেও মোট norm মাপতে clip_grad_norm_(…, max_norm=math.inf) ডাকা হয়েছে, যা ছাঁটে না, শুধু norm ফেরত দেয়।
loss.backward()
if mode == "clip_norm":
total = nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
else: # শুধু মাপা, ছাঁটা নয়
total = nn.utils.clip_grad_norm_(model.parameters(), max_norm=math.inf)
if mode == "clip_value":
nn.utils.clip_grad_value_(model.parameters(), clip_value=0.01)
opt.step()~/bazarghar-dl$ uv run python explode.py
plain ReLU MLP, 16 layers, He init x 1.6, SGD lr 0.05, batch 128
step | no clip: loss norm | value 0.01: loss | norm 1.0: loss norm
1 | 350.8 5.99e+03 | 350.8 | 350.8 5.99e+03
2 | 2.979e+17 2.2e+17 | 242 | 186.4 6.24e+03
3 | nan nan | 127.4 | 218.8 6.35e+03
4 | nan nan | 68.85 | 145.3 4e+03
10 | nan nan | 58.86 | 146.2 3.8e+03
50 | nan nan | 20.09 | 69.66 2.04e+03
100 | nan nan | 9.116 | 33.74 1.04e+03
200 | nan nan | 2.802 | 8.003 257
300 | nan nan | 2.201 | 5.436 198
Clipping ছাড়া: প্রথম ধাপে norm প্রায় ৬০০০, তাই lr 0.05-এ পুরো parameter vector-এর লাফের দৈর্ঘ্য প্রায় ৩০০; দ্বিতীয় ধাপে loss ৩ × ১০১৭, তৃতীয় ধাপে NaN. একবার NaN ঢুকলে প্রতিটি পরের হিসাবও NaN, run শেষ। Norm clipping: প্রতি ধাপে আপডেটের দৈর্ঘ্য ≤ lr × 1 = 0.05, তাই run টিকে থাকে আর loss ৩৫০ থেকে ৫.৪-এ নামে; ছাঁটার আগের norm ৬০০০ থেকে ২০০-এর ঘরে নামা দেখায় model নিজেই স্থির হচ্ছে। Value clipping: এই run-এ দ্রুত নেমেছে (২.২), তবে প্রায় ৬৫,০০০ parameter-এ ০.০১-এর সীমা মানে সর্বোচ্চ norm প্রায় ২.৬, অর্থাৎ ধাপের মাপ আলাদা; এক run থেকে কোনটি ভালো বলা যায় না। মনে রাখার কথা: clipping বিস্ফোরণ ঠেকায়, কারণ সারায় না; ৩০০ ধাপ পরেও loss ln 3-এর অনেক ওপরে, কারণ init-ই খারাপ। সঙ্গে একটি নিরাপত্তা-সুইচ: error_if_nonfinite=True দিলে NaN বা inf gradient-এ সঙ্গে সঙ্গে error.
~/bazarghar-dl$ uv run python clip_one.py
returned total norm: 13.0
grad after clip_grad_norm_(max_norm=5): [1.153846025466919, 1.538461446762085, 4.615384101867676] | new norm 5.0
grad after clip_grad_value_(2): [2.0, 2.0, 2.0]
RuntimeError: The total norm of order 2.0 for gradients from `parameters` is non-finite, so it cannot be clipped. To disable ...
loss.backward()-এর পরে, optimizer.step()-এর আগে। float16 mixed precision-এ আগে scaler.unscale_(optimizer), নইলে বড় করা gradient কাটা হয় (পর্ব ১২)। max_norm-এর সাধারণ শুরু ০.৫ থেকে ১.০; ফেরত দেওয়া norm লগ করে দেখুন কত ভাগ ধাপে ছাঁটা হচ্ছে। প্রায় সব ধাপে ছাঁটা হলে সীমা খুব ছোট, বা lr বড়।
৮ · প্রতিকারের পুরো বাক্স — init, normalization, activation, gate আর পর্যবেক্ষণ
| প্রতিকার | কোন সমস্যায় | কীভাবে কাজ করে | কোথায় বিস্তারিত |
|---|---|---|---|
| Xavier/He init | দুটিই, শুরুতে | প্রতি স্তরে variance-এর গুণক ১ রাখে | পর্ব ১৪ |
| BatchNorm/LayerNorm | দুটিই, পুরো training-এ | প্রতিটি স্তরে সংকেতের মাপ নতুন করে ঠিক করে | পর্ব ১৪ |
| ReLU-পরিবার | vanishing | ধনাত্মক দিকে ঢাল ১; তবে dead ReLU-এর ঝুঁকি | পর্ব ০৩ |
| Residual connection | vanishing | derivative I + F′, gradient-এর সরাসরি পথ | এই পর্ব, পর্ব ২২, ৩৫ |
| Gradient clipping | exploding | আপডেটের দৈর্ঘ্য সীমায় বাঁধে | এই পর্ব |
| Gate (LSTM/GRU) | sequence-এ vanishing | cell state যোগের পথে চলে, forget gate ঠিক করে কতটা থাকবে | পর্ব ২৯ |
| ছোট lr, warmup | exploding, শুরুর অস্থিরতা | শুরুতে ছোট ধাপ | পর্ব ১৫ |
Sequence-এ সমস্যা আরও তীব্র। Recurrent network একই weight matrix W প্রতিটি সময়-ধাপে আবার ব্যবহার করে, তাই ১০০ শব্দের বাক্যে gradient-এ প্রায় W১০০-এর মতো গুণফল আসে: W-এর বড় singular value ১-এর একটু বেশি হলে বিস্ফোরণ, কম হলে ক্ষয়। এই কারণে RNN-এ gradient clipping প্রায় নিয়মিত অভ্যাস (পর্ব ২৮, Recurrent neural network). LSTM-এর মূল চাল residual-এর মতোই: cell state ct = ft ⊙ ct−1 + (নতুন অংশ), যেখানে ft forget gate; ft প্রায় ১ হলে ∂ct/∂ct−1 প্রায় ১, তাই gradient অনেক ধাপ পেরোয়। তফাত হলো, LSTM এই “১” শেখে, residual-এ এটি স্থির।
পর্যবেক্ষণই প্রথম ওষুধ। প্রতিকার বসানোর আগে জানতে হবে রোগ কোনটি। প্রতিটি training run-এ অন্তত মোট gradient norm (clip_grad_norm_-এর ফেরত মান) লগ করুন; সন্দেহ হলে স্তর ধরে (named_parameters() ঘুরে p.grad.norm(), বা অংশ ৪-এর hook). Loss হঠাৎ লাফ দেওয়ার ঠিক আগে norm-এর লাফ থাকে প্রায়ই। NaN কোথা থেকে এল খোঁজা, dead ReLU গোনা আর learning curve পড়ার পুরো চেকলিস্ট পর্ব ১৮ (Training ডিবাগ করা)-এ।
আরও গভীরে: গভীর network-এ সংকেত কীভাবে চলে তার তত্ত্বকে বলে signal propagation; Saxe, McClelland আর Ganguli (২০১৪) orthogonal init আর Pennington, Schoenholz আর Ganguli (২০১৭) “dynamical isometry”-র ধারণায় দেখান, Jacobian-এর সব singular value ১-এর কাছে রাখলে খুব গভীর network-ও শেখে। He ও সহকর্মীদের ২০১৬ সালের “Identity Mappings in Deep Residual Networks” প্রবন্ধ shortcut-কে একেবারে পরিষ্কার identity রাখার (pre-activation) যুক্তি দেয়। Normalization ছাড়া residual network শেখানোর একটি পরিচিত কৌশল Fixup (Zhang, Dauphin আর Ma, ২০১৯), যা অংশ ৫-এর “ছোট init”-এর মতো branch-কে শুরুতে প্রায় শূন্য রাখে।
পরীক্ষা ও ইন্টারভিউতে:
- দশটি sigmoid স্তরে সর্বোচ্চ কত gradient পৌঁছাতে পারে (w = 1)? ০.২৫১০ ≈ ৯.৫৪ × ১০−৭.
- Residual block y = x + F(x)-এর ∂y/∂x? I + F′; F′ ছোট হলেও gradient-এর একটি অবিকৃত পথ থাকে।
- g = [6, 8], max_norm = 5-এ clip_grad_norm_-এর ফল? ‖g‖ = ১০, গুণক ০.৫, নতুন g = [3, 4]; ফেরত মান ১০.
- Norm বনাম value clipping? Norm: পুরো vector একসঙ্গে ছোট, দিক অক্ষত। Value: প্রতিটি ঘর আলাদা, দিক বদলাতে পারে।
- Loss ln C-তে আটকে থাকলে প্রথম সন্দেহ? Model কিছুই শিখছে না; স্তর ধরে gradient norm দেখে vanishing কি না যাচাই।
৯ · AI-এর যুগে — gradient সমস্যায় AI-এর পরামর্শ যাচাই
“আমার loss NaN হচ্ছে” লিখলে AI assistant প্রায় সবসময় বলবে “gradient clipping দিন”। প্রায়ই তা সাময়িক উপশম, কারণ নির্ণয় নয়। এই পর্বের পদ্ধতিই যাচাইয়ের হাতিয়ার: আগে মাপুন, তারপর বদলান।
| AI প্রায়ই যা করে | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
clip_grad_norm_ বসায় optimizer.step()-এর পরে, বা backward()-এর আগে | step-এর পরে কাটলে আপডেট ইতিমধ্যে হয়ে গেছে; আগে কাটলে gradient-ই নেই | ক্রম: backward → (unscale_) → clip → step |
পুরোনো নাম লেখে: clip_grad_norm (শেষে _ ছাড়া) | নতুন version-এ in-place রূপটিই clip_grad_norm_; পুরোনো নাম deprecated | নিজের version-এর docs দেখুন; warning পড়ুন |
AMP-এ scaler.unscale_ ছাড়া clip | scale গুণ করা gradient কাটা হয়, আসল সীমা অনেক ছোট হয়ে যায় | কোডে unscale_-এর লাইন খুঁজুন |
| Vanishing-এর জন্যও clipping বলে | clipping ছোট gradient বড় করে না | স্তর ধরে norm মেপে দেখুন কোন দিকের সমস্যা |
| “ResNet-এ vanishing gradient থাকে না” বলে সরল দাবি | normalization বা যত্নশীল init ছাড়া residual-ও বিস্ফোরিত হতে পারে (অংশ ৪, ৫) | forward-এর ‖h‖ আর gradient দুটিই মাপুন |
Hook-এ register_backward_hook (পুরোনো) দেয় | এটি deprecated আর কিছু module-এ ভুল gradient দিতে পারে | register_full_backward_hook ব্যবহার করুন |
AI-কে সমাধানের আগে রোগনির্ণয়ের কোড চাইতে বলুন:
AI-এর দেওয়া সমাধান হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Vanishing gradient-এর সমস্যা কবে আর কীভাবে চেনা গেল, আর কেন প্রায় দুই দশক ধরে গভীর network শেখানো কঠিন মনে হতো?
১৯৮৬ সালে Rumelhart, Hinton আর Williams backpropagation জনপ্রিয় করার পর ধারণা ছিল, স্তর বাড়ালেই আরও জটিল function শেখা যাবে। বাস্তবে কয়েকটির বেশি স্তরে training আটকে যেত। Sepp Hochreiter ১৯৯১ সালে মিউনিখে জার্মান ভাষায় লেখা diploma থিসিসে recurrent network-এ এই সমস্যার গাণিতিক বিশ্লেষণ করেন বলে সাধারণত উল্লেখ করা হয়: সময়ের পেছনে যেতে যেতে error-এর সংকেত সূচকীয়ভাবে ছোট বা বড় হয়। ১৯৯৪ সালে Bengio, Simard আর Frasconi একটি প্রবন্ধে দেখান, দীর্ঘমেয়াদি নির্ভরতা gradient descent দিয়ে শেখা মৌলিকভাবে কঠিন, কারণ স্থিরভাবে তথ্য ধরে রাখার শর্তই gradient-কে ক্ষয় করে।
সমাধান এসেছে কয়েক ধাপে। ১৯৯৭ সালে Hochreiter আর Schmidhuber LSTM প্রস্তাব করেন, যার cell state যোগের পথে চলে। Feedforward network-এ ২০০৬ সালের দিকে Hinton ও সহকর্মীদের স্তর ধরে unsupervised pretraining একটি বিকল্প পথ দেখায়। ২০১০ সালে Glorot আর Bengio-র প্রবন্ধ দেখায়, sigmoid আর তখনকার প্রচলিত init মিলে সংকেত কীভাবে স্তরে স্তরে নষ্ট হয়, আর Xavier init প্রস্তাব করে। ২০১০–২০১১ সালে ReLU-র ব্যবহার (Nair আর Hinton; Glorot, Bordes আর Bengio) ঢালের সমস্যা অনেকটা কমায়। ২০১৫ সালে BatchNorm আর ResNet শেষ বড় বাধা সরায়।
এই ইতিহাসের শিক্ষা: সমস্যাটি কোনো একটি “ভুল” ছিল না, বরং গভীরতার গাণিতিক স্বভাব। এই পর্বের মাপে দেখেছেন, ৩০ স্তরের sigmoid network-এ প্রথম স্তরের gradient float32-এ আক্ষরিক শূন্য। তখনকার গবেষকেরা এমন network-কে “শেখে না” দেখে সিদ্ধান্তে আসতেন যে গভীরতা কাজে লাগে না, অথচ সমস্যা ছিল শেখানোর কৌশলে।
মূল উপলব্ধি: গভীর network-এর জয় একটি আবিষ্কারে আসেনি; activation, init, normalization, residual আর gate মিলে গুণফলকে ১-এর কাছে রাখার কৌশলের সমষ্টি থেকে এসেছে।
প্র ০২ResNet প্রবন্ধ কোন পর্যবেক্ষণ থেকে শুরু হয়েছিল, আর “degradation problem” কেন overfitting থেকে আলাদা?
২০১৫ সালের শেষে Microsoft Research-এর He, Zhang, Ren আর Sun একটি অদ্ভুত পর্যবেক্ষণ দিয়ে প্রবন্ধ শুরু করেন: CIFAR-10-এ ২০ স্তরের plain CNN-এর চেয়ে ৫৬ স্তরের plain CNN-এর training error বেশি (ImageNet-এ ১৮ বনাম ৩৪ স্তরে একই চেহারা)। Overfitting হলে training error কম আর test error বেশি হতো; এখানে training error-ই বেশি, তাই সমস্যা শেখানোর, মনে রাখার নয়। তাঁরা যুক্তি দেন, গভীর network অন্তত অগভীরটির সমান ভালো হওয়ার কথা: বাড়তি স্তরগুলো identity হলেই হয়। কিন্তু কয়েকটি nonlinear স্তরের পক্ষে হুবহু identity শেখা কঠিন। তাই তাঁরা কাঠামোতেই identity বসান, block-কে শুধু বাকিটুকু শিখতে দেন।
প্রবন্ধে BatchNorm ব্যবহার করা হয়েছিল, তাই লেখকেরা নিজেরাই বলেন plain network-এর সমস্যা শুধু vanishing gradient দিয়ে পুরোপুরি ব্যাখ্যা হয় না; এটি optimisation-এর একটি বৃহত্তর কঠিনতা। ImageNet-এ ১৫২ স্তরের ResNet ২০১৫ সালের ILSVRC প্রতিযোগিতার classification-এ প্রথম হয়, আর CIFAR-10-এ তাঁরা ১০০০-এর বেশি স্তরও শেখান। ধারণাটির একটি পূর্বসূরি Srivastava, Greff আর Schmidhuber-এর একই বছরের Highway Network, যেখানে shortcut-এ gate ছিল; ResNet-এর shortcut gate-হীন, সরল identity.
এই পর্বের ৪০ স্তরের run-এ একই চেহারা দেখেছেন: plain MLP-এর train loss ln 3-এ আটকে, অর্থাৎ training-ই ব্যর্থ, overfitting-এর প্রশ্নই ওঠে না; residual একই ডেটায় শিখেছে। পরে ২০১৬ সালে একই দল দেখায়, shortcut-এ কোনো activation না রেখে একেবারে পরিষ্কার identity রাখলে আরও গভীর network সহজে শেখে।
মূল উপলব্ধি: Degradation হলো optimisation-এর ব্যর্থতা; residual connection সমস্যাটিকে “নতুন function শেখা” থেকে “identity থেকে কতটা সরা” শেখায় বদলে দেয়, যা optimizer-এর জন্য অনেক সহজ।
প্র ০৩Residual network আসলে কী — একটি খুব গভীর network, অনেক অগভীর network-এর সমষ্টি, নাকি একটি differential equation-এর ধাপ?
Residual network-কে দেখার অন্তত তিনটি প্রভাবশালী দৃষ্টিভঙ্গি আছে। প্রথমটি Veit, Wilber আর Belongie-র ২০১৬ সালের প্রবন্ধ: n-টি residual block-এর output খুলে লিখলে 2n-টি পথের যোগফল পাওয়া যায়, কারণ প্রতিটি block-এ সংকেত F দিয়ে যেতে পারে বা shortcut দিয়ে। তাঁরা দেখান, প্রশিক্ষিত ResNet থেকে একটি block মুছে দিলেও কার্যকারিতা সামান্য কমে, যেখানে plain network-এ একটি স্তর মুছলে ভেঙে পড়ে; আর বেশির ভাগ gradient আসে তুলনামূলক ছোট পথ দিয়ে। তাঁদের ব্যাখ্যায় ResNet অনেকটা অগভীর network-এর ensemble-এর মতো আচরণ করে। এই ব্যাখ্যা নিয়ে আলোচনা আছে, তবে stepper-এর গুণফল ∏(1 + F′) খুললে সব পথের যোগফলই আসে, যা এর গাণিতিক ভিত্তি।
দ্বিতীয় দৃষ্টিভঙ্গি গতিবিদ্যার: hl = hl−1 + F(hl−1) দেখতে differential equation dh/dt = F(h)-এর Euler পদ্ধতির এক ধাপের মতো। এই মিল থেকে Chen ও সহকর্মীদের ২০১৮ সালের “Neural Ordinary Differential Equations” প্রবন্ধ স্তরের সংখ্যাকে অবিচ্ছিন্ন সময় হিসেবে দেখে, আর ODE solver দিয়ে forward চালায়।
তৃতীয়টি loss-এর ভূপ্রকৃতির: Li ও সহকর্মীদের ২০১৮ সালের “Visualizing the Loss Landscape of Neural Nets” প্রবন্ধে খুব গভীর plain network-এর loss surface বিশৃঙ্খল আর খাঁজকাটা, আর skip connection যোগ করলে অনেক মসৃণ দেখায়। এটি দ্বিমাত্রিক অংশের ছবি, পুরো উচ্চমাত্রিক জগতের প্রমাণ নয়, তবু optimizer কেন সহজে পথ পায় তার একটি স্বজ্ঞা দেয়।
তিনটি দৃষ্টিভঙ্গি পরস্পরবিরোধী নয়; প্রতিটি একটি দিক আলো করে। বাস্তব কাজে সবচেয়ে কাজের কথাটি এই পর্বের মাপ থেকেই আসে: shortcut থাকায় gradient-এর অন্তত একটি পথে গুণনীয়ক ঠিক ১।
মূল উপলব্ধি: একটি সরল যোগ চিহ্ন একসঙ্গে পথের ensemble, ODE-এর ধাপ আর মসৃণ loss surface-এর ব্যাখ্যা দেয়; ভালো নকশার ধারণা প্রায়ই অনেক দিক থেকে বোঝা যায়।
প্র ০৪Gradient clipping RNN-এর কৌশল হিসেবে শুরু হয়ে আজ বড় language model-এর training-এ প্রায় নিয়মিত কেন, আর এর দাম কী?
Gradient clipping-এর পরিচিত সূত্র Tomas Mikolov-এর RNN language model-এর কাজ (২০১২ সালের থিসিস) আর Pascanu, Mikolov আর Bengio-র ২০১৩ সালের প্রবন্ধ “On the difficulty of training recurrent neural networks”. শেষোক্ত প্রবন্ধ জ্যামিতিক ছবি দেয়: RNN-এর loss surface-এ কোথাও কোথাও খাড়া “দেয়াল” থাকে; সেখানে gradient হঠাৎ বিশাল, আর পুরো ধাপ নিলে parameter অনেক দূরে ছিটকে যায়। Norm ধরে কাটলে দিক একই থাকে কিন্তু ধাপ ছোট, তাই দেয়ালে ধাক্কা খেয়েও যুক্তিসংগত জায়গায় থাকা যায়। তাঁরা value clipping-এর তুলনায় norm clipping পছন্দ করেন দিক রক্ষার কারণে, যা এই পর্বের stepper-এ দেখেছেন।
Transformer আর বড় language model-এ সমস্যার ধরন একটু আলাদা: লম্বা training-এ মাঝে মাঝে “loss spike”, হঠাৎ loss লাফিয়ে ওঠা, যা কখনো কখনো run নষ্ট করে। অনেক প্রকাশিত training configuration-এ মোট gradient norm-এ ১.০-এর আশপাশের সীমা দেখা যায়, সঙ্গে warmup আর AdamW (পর্ব ১৫)। Zhang ও সহকর্মীদের ২০২০ সালের একটি ICLR প্রবন্ধ তাত্ত্বিকভাবে দেখানোর চেষ্টা করে, কিছু অবস্থায় clipping শুধু নিরাপত্তা নয়, training দ্রুতও করতে পারে। এই ফলের সাধারণতা নিয়ে আলোচনা চলছে।
দাম তিনটি। প্রথমত, clipping gradient-এর মাপ বদলায়, তাই কার্যকর learning rate বদলায়; খুব ছোট সীমায় প্রায় প্রতিটি ধাপ কাটা পড়ে আর training ধীর হয়। দ্বিতীয়ত, এটি উপসর্গ লুকাতে পারে: এই পর্বের run-এ clipping NaN ঠেকিয়েছে, কিন্তু খারাপ init-এর কারণে loss ৩০০ ধাপ পরেও উঁচু। তৃতীয়ত, distributed training-এ সব GPU-র gradient মিলিয়ে মোট norm বের করতে বাড়তি যোগাযোগ লাগে। এজন্য অভিজ্ঞ দল clipping-এর হার (কত ভাগ ধাপে কাটা পড়ল) লগ করে; হার হঠাৎ বাড়লে সেটি নিজেই একটি সতর্কসংকেত।
মূল উপলব্ধি: Clipping হলো সিটবেল্ট: দুর্ঘটনায় বাঁচায়, কিন্তু খারাপ চালানো সারায় না; আর কতবার টান পড়ল, সেই হিসাবই চালকের মান বলে দেয়।
প্র ০৫খুব গভীর Transformer-এ residual আর LayerNorm কোথায় বসবে — pre-norm বনাম post-norm বিতর্ক কী শেখায়?
২০১৭ সালের মূল Transformer-এ LayerNorm বসেছিল residual যোগের পরে: h = LN(h + F(h)), যাকে বলে post-norm. এতে shortcut আর পরিষ্কার identity থাকে না, কারণ প্রতিটি block-এর পরে normalization সংকেতকে বদলায়। ব্যবহারিকভাবে post-norm Transformer-এ learning-rate warmup ছাড়া training প্রায়ই অস্থির হতো। Xiong ও সহকর্মীদের ২০২০ সালের প্রবন্ধ “On Layer Normalization in the Transformer Architecture” বিশ্লেষণ করে দেখায়, post-norm-এ শুরুতে output-এর কাছের স্তরে gradient বড় থাকে, তাই warmup লাগে; আর LayerNorm-কে F-এর ভেতরে, যোগের আগে বসালে (pre-norm, h + F(LN(h))) shortcut পরিষ্কার থাকে আর warmup ছাড়াও training স্থির হতে পারে।
এই পর্বের ৪০ স্তরের run-এ pre-norm residual সবচেয়ে ভালো শিখেছে, আর hook-এর মাপে তার gradient স্তর জুড়ে প্রায় সমান। পরে প্রকাশিত অনেক বড় language model pre-norm বা তার কোনো রূপ ব্যবহার করে বলে জানা যায়। কিন্তু বিতর্ক শেষ হয়নি: কিছু কাজে দেখা গেছে, ভালোভাবে শেখাতে পারলে post-norm শেষ মানে সামান্য ভালো হতে পারে, আর pre-norm-এ গভীর স্তরগুলোর অবদান কমে যাওয়ার প্রবণতা থাকতে পারে। Wang ও সহকর্মীদের ২০২২ সালের DeepNet প্রবন্ধ residual-এর আগে একটি ধ্রুবক দিয়ে shortcut-কে বড় করে আর init ছোট করে, আর হাজার স্তরের Transformer শেখানোর কথা জানায়।
সাধারণ শিক্ষা হলো একটি ভারসাম্য: shortcut যত পরিষ্কার, gradient তত নিরাপদ; কিন্তু প্রতিটি block-এর অবদানের মাপও নিয়ন্ত্রণে রাখতে হয়, নইলে অংশ ৫-এর ReLU residual-এর মতো সংকেত স্তরে স্তরে বাড়ে। এই কারণেই branch-এর শেষ স্তর শূন্য বা ছোট মানে init করা, বা branch-কে একটি ছোট ধ্রুবক দিয়ে গুণ করার মতো কৌশল প্রচলিত। পর্ব ৩৫-এ Transformer block-এ এই নকশা আবার দেখবেন।
মূল উপলব্ধি: Residual আর normalization-এর অবস্থান একটি ছোট নকশা-সিদ্ধান্ত, কিন্তু গভীরতায় তা training-এর স্থিরতা ঠিক করে; কোনো একটি বিন্যাস সব ক্ষেত্রে সেরা, এমন ঐকমত্য নেই।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান। অংশ ৭-এর ব্লক দিয়েও যাচাই করতে পারেন।
-
পূর্ণ numerical: (ক) ছয়টি sigmoid স্তরের scalar শিকলে প্রতিটি স্তরের ঢাল ০.২ আর weight w = 1.5. প্রথম স্তরে কত গুণ gradient পৌঁছায়? (খ) একই শিকলে প্রতিটি স্তর residual (y = x + F(x), F′ = 1.5 × 0.2) হলে? (গ) দুটি parameter-এর gradient W.grad = [[2, 0], [0, 1]] আর b.grad = [2, 4]; max_norm = 2.5-এ clip_grad_norm_-এর পরে কী হবে, আর কী ফেরত আসবে?
(ক) প্রতিটি গুণনীয়ক w · σ′ = ১.৫ × ০.২ = ০.৩; ০.৩৬ = ০.০০০৭২৯. (খ) প্রতিটি গুণনীয়ক 1 + 0.3 = 1.3; ১.৩৬ ≈ ৪.৮২৭: ক্ষয় নেই, বরং কিছুটা বৃদ্ধি, তাই residual-এ মাপের দিকেও নজর লাগে। (গ) বর্গের যোগ ৪ + ০ + ০ + ১ + ৪ + ১৬ = ২৫, মোট norm ৫; গুণক ২.৫ / ৫ = ০.৫; নতুন W.grad = [[1, 0], [0, 0.5]], b.grad = [1, 2] (PyTorch-এ হর-এর 10−6-এর জন্য 0.9999998-এর মতো দেখায়); ফেরত মান ৫.০ (ছাঁটার আগের norm). দুটি tensor একটি মোট norm ভাগ করে নেয়, আলাদা আলাদা নয়।
-
সমালোচনা ও নকশা: একজন সহকর্মীর ৫০ স্তরের MLP-এর loss প্রথম epoch থেকেই ln 10 ≈ ২.৩০২৬-এ আটকে (১০টি class). তিনি
clip_grad_norm_(…, 1.0)যোগ করেছেন, কিছু বদলায়নি; এখন lr ১০ গুণ বাড়ানোর কথা ভাবছেন। কী ভুল হচ্ছে, আর আপনি কী ক্রমে এগোতেন?Loss ln 10-এ আটকে মানে model সব class-কে প্রায় সমান সম্ভাবনা দিচ্ছে, কিছুই শিখছে না; এটি exploding নয়, তাই clipping-এর কিছু করার নেই (clipping ছোট gradient বড় করে না)। lr বাড়ালে শেষ স্তরগুলো অস্থির হতে পারে, অথচ প্রথম স্তরগুলো তবুও প্রায় স্থির। ক্রম: (১) এক batch overfit করার চেষ্টা (পর্ব ১৮); (২)
register_full_backward_hookবাnamed_parameters()দিয়ে স্তর ধরে gradient norm লগ, প্রথম আর শেষ স্তরের অনুপাত দেখা; (৩) activation কী, init কী (default init আর sigmoid/tanh হলে সন্দেহ জোরালো); (৪) প্রতিকার: ReLU আর He init, normalization, residual block, বা pre-norm residual; (৫) বদলের পরে একই মাপ আবার, আর কয়েকটি seed-এ তুলনা। -
আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
import torch from torch import nn p = nn.Parameter(torch.zeros(3)) p.grad = torch.tensor([-3.0, 0.5, 2.0]) nn.utils.clip_grad_value_([p], clip_value=1.0) print(p.grad.tolist()) p.grad = torch.tensor([6.0, 8.0, 0.0]) t = nn.utils.clip_grad_norm_([p], max_norm=5.0) print(round(t.item(), 2), [round(v, 2) for v in p.grad.tolist()])[-1.0, 0.5, 1.0] 10.0 [3.0, 4.0, 0.0]Value clipping প্রতিটি ঘর [−1, 1]-এ আনে: −৩ → −১, ০.৫ অপরিবর্তিত, ২ → ১. Norm clipping-এ ‖[6, 8, 0]‖ = ১০, গুণক ৫ / ১০ = ০.৫ (হর-এর 10−6 দুই দশমিক ঘরে চোখে পড়ে না), ফল [3, 4, 0]; ফেরত মান ছাঁটার আগের norm, ১০.০.
-
কোড লিখুন: একটি function
grad_report(model, low=1e-7, high=1e3)লিখুন, যাloss.backward()-এর পরে ডাকলে প্রতিটি weight-এর gradient norm ছাপে, যেগুলোlow-এর কম তাদের “vanishing?” আরhigh-এর বেশি তাদের “exploding?” চিহ্ন দেয়, আর মোট norm ফেরত দেয়।import torch def grad_report(model, low=1e-7, high=1e3): sq = 0.0 for name, p in model.named_parameters(): if p.grad is None: continue # এই ধাপে ব্যবহার হয়নি g = p.grad.norm().item() sq += g ** 2 if not torch.isfinite(p.grad).all(): flag = "NaN/inf!" elif g < low: flag = "vanishing?" elif g > high: flag = "exploding?" else: flag = "" if name.endswith("weight"): print(f"{name:30s} {g:10.2e} {flag}") return sq ** 0.5 # clip_grad_norm_-এর মোট norm-এর সমানমোট norm সব parameter-এর norm-এর বর্গের যোগফলের বর্গমূল, যা
clip_grad_norm_-এর ফেরত মানের সঙ্গে মেলে। সীমাগুলো নিয়ম নয়, শুধু সতর্কসংকেত: আসল সিদ্ধান্ত আসে স্তর জুড়ে অনুপাত আর সময়ের সঙ্গে প্রবণতা থেকে। প্রতি ধাপে ছাপা ধীর, তাই প্রতি কয়েকশো ধাপে একবার ডাকুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমGradient clipping-সহ PyTorch-এর একটি training ধাপ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Forward pass চালিয়ে loss হিসাব
- পুরোনো gradient মোছা (
zero_grad) loss.backward()দিয়ে gradient হিসাবclip_grad_norm_দিয়ে মোট norm সীমায় আনা আর ফেরত মান লগoptimizer.step()দিয়ে weight আপডেট
দশটি sigmoid স্তরের একটি scalar শিকলে সব weight ১ আর সব স্তরের input ০ হলে প্রথম স্তরে পৌঁছানো gradient-এর গুণক কত?
- ০.২৫
- প্রায় ৯.৫৪ × ১০−৭
- ২.৫
- ১, কারণ input ০-তে ঢাল ১
Residual block y = x + F(x)-এ ∂y/∂x কী, আর এটি কেন গুরুত্বপূর্ণ?
- F′, কারণ x-এর নিজের derivative ০
- x · F′
- I + F′; F′ ছোট হলেও gradient shortcut দিয়ে অবিকৃত পৌঁছায়
- সবসময় ঠিক I, তাই F কিছু শেখে না
একটি gradient g = [3, 4, 12]-এ clip_grad_norm_(max_norm=5) প্রয়োগ করলে কী হয়?
- সব ঘর ৫/১৩ দিয়ে গুণ হয়ে প্রায় [1.154, 1.538, 4.615]; ফেরত মান ১৩
- প্রতিটি ঘর ৫-এ কাটা হয়ে [3, 4, 5]
- কিছুই বদলায় না, কারণ কোনো ঘর ১৩-এর বেশি নয়
- সব ঘর সমান হয়ে norm ৫, ফেরত মান ৫
৪০ স্তরের একটি plain ReLU MLP-এর train loss তিন class-এর ডেটায় পুরো training জুড়ে প্রায় ১.০৯৯-এ স্থির থাকল। সবচেয়ে সম্ভাব্য ব্যাখ্যা কী?
- Model overfit করছে
- Exploding gradient-এ weight বিশাল হয়েছে
- ডেটা খুব কম, তাই কিছু শেখার নেই
- Vanishing gradient-এ শুরুর স্তরগুলো শিখছে না, model প্রতিটি class-কে প্রায় ১/৩ দিচ্ছে
float16 mixed precision আর GradScaler দিয়ে training-এ gradient clipping-এর সঠিক ক্রম কোনটি?
- scaler.scale(loss).backward() → clip → scaler.step(opt)
- scaler.scale(loss).backward() → scaler.unscale_(opt) → clip → scaler.step(opt) → scaler.update()
- clip → scaler.scale(loss).backward() → scaler.step(opt)
- scaler.step(opt) → clip → scaler.update()
Normalization ছাড়া, PyTorch-এর default init-এ ৩০টি residual ReLU block (h + ReLU(Wh)) বানালে প্রথম স্তরের gradient শেষ স্তরের প্রায় ১০০ গুণ এলো। কারণ কী?
- Residual connection vanishing gradient তৈরি করে
- Backward hook gradient বাড়িয়ে দেয়
- প্রতিটি block শুধু যোগ করে, তাই মাপ প্রতি block-এ (1 + কিছু) গুণ বাড়ে; normalization বা ছোট init লাগে
- Default init-এর weight খুব বড়
g = [900, 1, −2]-এ value clipping (সীমা ২) আর norm clipping (সীমা ৫)-এর পার্থক্য কোনটি ঠিক বলে?
- Value clipping দিক বদলায় (প্রায় [2, 1, −2]); norm clipping দিক রাখে (প্রায় [5, 0.0056, −0.011])
- দুটি একই ফল দেয়
- Norm clipping দিক বদলায়, value clipping রাখে
- Value clipping gradient শূন্য করে দেয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- কারণ: Backward-এ Jacobian গুণ হয়; গড় গুণক ১-এর কম হলে vanishing, বেশি হলে exploding. Activation-এর ঢাল, weight-এর মাপ আর গভীরতা তিনটিই দায়ী।
- মাপুন: স্তর ধরে ‖∂L/∂h‖ আর ‖∂L/∂W‖,
register_full_backward_hookবাnamed_parameters()দিয়ে; loss ln C-তে আটকে থাকলে প্রথমেই দেখুন। - Residual: y = x + F(x)-এর derivative I + F′, তাই gradient-এর অবিকৃত পথ থাকে; কিন্তু মাপ নিয়ন্ত্রণে normalization (pre-norm) বা ছোট init লাগে।
- Clipping: backward-এর পরে, step-এর আগে; AMP-এ আগে unscale_. Norm ধরে কাটলে দিক অক্ষত; ফেরত মান ছাঁটার আগের norm, লগ করুন।
- বড় ছবি: Clipping বিস্ফোরণ ঠেকায়, কারণ সারায় না; init, normalization, activation, residual আর gate মিলে গুণফলকে ১-এর কাছে রাখে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৮ · Training ডিবাগ করা — এক batch overfit, learning curve আর সাধারণ ভুলপরবর্তী পর্বএক batch overfit, শুরুর loss যাচাই, NaN শিকার আর learning curve পড়া শেখা।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ১৮ আর মডিউল ৩-এর বাকি পর্বগুলো এখান থেকে খুলবেন।
- পর্ব ০৪ · Backpropagation হাতে-কলমেআগের পর্বChain rule থেকে স্তর ধরে gradient, যার গুণফলই এই পর্বের সমস্যা।
- পর্ব ০৮ · PyTorch autogradআগের পর্বTensor hook, retain_grad আর gradient-এর ফাঁদ।
- মেশিন লার্নিং · পর্ব ১১ · Matrix calculusআগের ভিত্তিJacobian, gradient আর chain rule-এর matrix রূপ।
- মেশিন লার্নিং · পর্ব ০৯ · SVDআগের ভিত্তিSingular value কী মাপে, যা Jacobian-এর টান বোঝায়।
- AI-এর ভিত্তি · পর্ব ৩৪ · Derivative আর gradient descentআগের ভিত্তিঢাল ধরে নিচে নামার মূল ধারণা।