Part 03 · Deep Learning
Training Neural Networks
বড় আকারে gradient descent: mini-batch, momentum আর Adam, এমন শুরুর weight যেটা signal মেরে ফেলে না, আর exploding বা vanishing gradient loss curve এ কেমন দেখায়।
⏱ 1-2 ঘণ্টা · Station 18 / 92
জিনিসটা কী, কেন দরকার
শেখার দুইটা অর্ধেক এখন জানো: backprop gradient বের করে, gradient descent একটা step নেয়। 12 টা data point এর notebook এ এটুকুই যথেষ্ট। দশ লাখ ছবি আর 50 layer এর network এ সাধারণ version টা একসাথে কয়েক জায়গায় ভাঙে: সব data র উপর একটা step নিতেই অনেক সময় লাগে, প্রথম layer গুলোতে পৌঁছানোর আগেই signal মিলিয়ে যায়, আর loss এর আকার একটা সরু খাদের মতো যেখানে সাধারণ step এদিক ওদিক বাড়ি খায়।
বাস্তবে neural network train করা মানে ঠিক এই সমস্যাগুলোর কয়েকটা সমাধান: mini-batch, ভালো optimiser (momentum, Adam), ঠিকঠাক weight initialisation, আর এমন activation যেটা gradient কে বাঁচিয়ে রাখে। যত training script পড়বে সবগুলোতে এগুলোই default, আর training ফেল করলে সাধারণত এদের কোনো একটাই কারণ।
ভেতরে কীভাবে চলে
Mini-batch SGD। সব data র gradient না নিয়ে, ছোট একটা random batch (32 থেকে 4,096 টা example) এ gradient বের করে step নাও। প্রতিটা step সস্তা, আর gradient টা noisy হলেও কাজ চালানোর মতো ভালো আন্দাজ। পুরো dataset এ একবার ঘোরা মানে এক epoch। Noise টা বরং উপকারই করে, weight কে খারাপ সমতল জায়গা থেকে ঝাঁকিয়ে বের করে।
Momentum। আগের gradient গুলোর একটা চলমান গড় রাখো আর সেই দিকে step নাও, ঢাল বেয়ে গড়ানো একটা বলের মতো যেটা গতি ধরে রাখে। সরু উপত্যকায় এপাশ ওপাশের gradient গুলো কাটাকাটি হয়ে যায় আর উপত্যকা বরাবর gradient গুলো যোগ হয়, তাই যেদিকে যাওয়া উচিত সেদিকে দ্রুত এগোয়।
Adam। Momentum, সাথে প্রতিটা weight এর আলাদা step size: যেসব weight এর gradient সবসময় ছোট সেগুলো বড় step পায়, যাদের gradient বড় আর noisy সেগুলো ছোট। অল্প tune করেই ভালো কাজ করে, transformer এর default (সাধারণত AdamW হিসেবে, যেখানে weight decay ঠিকভাবে করা হয়)।
Weight initialisation। দুইটা ফাঁদ:
- সবার একই শুরুর weight: একটা layer এর প্রতিটা neuron একই হিসাব করে, একই gradient পায়, আর চিরকাল একে অপরের কপি থাকে। Weight কে random দিয়ে শুরু করতেই হবে (symmetry breaking)।
- ভুল scale: বেশি ছোট হলে signal layer এর পর layer এ ছোট হতে থাকে; বেশি বড় হলে ফেটে যায়। He init (ReLU র জন্য) আর Xavier/Glorot init (tanh/sigmoid এর জন্য) layer এর সাইজ দেখে random এর scale ঠিক করে, যাতে network এর ভেতর দিয়ে signal মোটামুটি একই সাইজে থাকে।
Vanishing আর exploding gradient। Backprop প্রতি layer এ একটা local slope গুণ করে। Sigmoid এর slope সর্বোচ্চ 0.25, তাই 20 টা sigmoid layer gradient কে সর্বোচ্চ 0.25²⁰ ≈ 10⁻¹² দিয়ে গুণ করে। প্রথম layer গুলো শেখা বন্ধ করে দেয়। সমাধান: ReLU পরিবারের activation (চালু থাকলে slope 1), ঠিক init, normalisation layer (BatchNorm, LayerNorm), আর residual connection (Part 4), যেটা gradient কে একটা shortcut দেয়। Exploding gradient এর জন্য gradient norm কে একটা সর্বোচ্চ মানে clip করো।
Sigmoid activation এর 30 layer এর একটা network train হচ্ছে, কিন্তু loss প্রায় নড়ছে না, আর log বলছে প্রথম layer গুলোর gradient 1e-15 এর আশেপাশে। প্রথমে কী ঠিক করবে?
ছবিতে দেখো
PyTorch এ তিনটা পরীক্ষা
দুই প্যাঁচের একটা dataset এ (যেটা কোনো সোজা লাইন ভাগ করতে পারে না) PyTorch এ তিনটা পরীক্ষা: 20 layer এর প্রথমটায় কতটা gradient পৌঁছায়, sigmoid বনাম ReLU + He init; সবার একই শুরুর weight কী করে; আর একই 150 টা mini-batch step এর বাজেটে SGD বনাম momentum বনাম Adam।
import torch
import torch.nn as nn
torch.manual_seed(0)
# Data: two interleaved spirals, a classic "not a straight line" problem. 400 points.
n = 200
t = torch.linspace(0.3, 3.5, n)
spiral = lambda sign: torch.stack([sign * t * torch.cos(2 * t), sign * t * torch.sin(2 * t)], dim=1)
X = torch.cat([spiral(1), spiral(-1)]) + 0.05 * torch.randn(2 * n, 2)
y = torch.cat([torch.zeros(n), torch.ones(n)]).long()
def make_net(depth=4, width=32, act=nn.ReLU, init=None):
layers, d_in = [], 2
for _ in range(depth):
lin = nn.Linear(d_in, width)
if init == "same":
nn.init.constant_(lin.weight, 0.1)
nn.init.zeros_(lin.bias)
elif init == "he":
nn.init.kaiming_normal_(lin.weight, nonlinearity="relu")
layers += [lin, act()]
d_in = width
out = nn.Linear(d_in, 2)
if init == "same":
nn.init.constant_(out.weight, 0.1)
nn.init.zeros_(out.bias)
layers.append(out)
return nn.Sequential(*layers)
# 1) Vanishing gradients: how much gradient reaches the FIRST layer of a 20-layer network?
print("first-layer gradient size, 20 layers deep")
for name, act, init in (("sigmoid, default init", nn.Sigmoid, None), ("ReLU, He init", nn.ReLU, "he")):
torch.manual_seed(1)
net = make_net(depth=20, act=act, init=init)
nn.functional.cross_entropy(net(X), y).backward()
print(f" {name:<22} {net[0].weight.grad.norm():.2e}")
# 2) Same starting weights everywhere: every hidden neuron gets the same gradient, so they stay clones.
torch.manual_seed(1)
net = make_net(depth=2, width=4, init="same")
opt = torch.optim.SGD(net.parameters(), lr=0.1)
for _ in range(50):
opt.zero_grad()
nn.functional.cross_entropy(net(X), y).backward()
opt.step()
w = net[2].weight
print(f"\nidentical init after 50 steps: distinct rows in layer-2 weights = {len(torch.unique(w.detach(), dim=0))} of {w.shape[0]}")
# 3) Optimisers: same network, same data, 600 mini-batch steps of 32 points each.
def train(opt_name):
torch.manual_seed(2)
net = make_net(depth=4, init="he")
opt = {"SGD": lambda p: torch.optim.SGD(p, lr=0.05),
"SGD+momentum": lambda p: torch.optim.SGD(p, lr=0.05, momentum=0.9),
"Adam": lambda p: torch.optim.Adam(p, lr=0.005)}[opt_name](net.parameters())
g = torch.Generator().manual_seed(3)
for step in range(150):
idx = torch.randint(0, len(X), (32,), generator=g) # a random mini-batch
opt.zero_grad()
nn.functional.cross_entropy(net(X[idx]), y[idx]).backward()
opt.step()
with torch.no_grad():
logits = net(X)
return nn.functional.cross_entropy(logits, y).item(), (logits.argmax(1) == y).float().mean().item()
print("\noptimiser loss accuracy (150 steps, batch 32)")
for name in ("SGD", "SGD+momentum", "Adam"):
loss, acc = train(name)
print(f" {name:<13} {loss:.3f} {acc:.0%}")
Output:
first-layer gradient size, 20 layers deep
sigmoid, default init 1.60e-18
ReLU, He init 8.59e-02
identical init after 50 steps: distinct rows in layer-2 weights = 1 of 4
optimiser loss accuracy (150 steps, batch 32)
SGD 0.295 82%
SGD+momentum 0.079 95%
Adam 0.005 100%
কী দেখলাম:
- Vanishing gradient, মাপা: 20 টা sigmoid layer এ প্রথম layer এ পৌঁছানো gradient 1.6 × 10⁻¹⁸। ReLU আর He init এ 0.086, প্রায় 10¹⁶ গুণ বড়। প্রথম network তার শুরুর layer গুলো train ই করতে পারে না।
- একই init: 50 step পরেও layer 2 এর চারটা neuron এর মধ্যে আলাদা weight row মাত্র একটা। চারটা neuron, একটাই ধারণা। Random init বাদ দেওয়ার উপায় নেই।
- Optimiser, একই বাজেট: সাধারণ SGD পৌঁছায় 82% accuracy তে, momentum 95%, Adam 100% আর loss 0.005। একই network, একই data, একই সংখ্যক step; বদলেছে শুধু update এর নিয়ম।
Production এ যা মাথায় রাখবে
- পরীক্ষিত default দিয়ে শুরু করো: AdamW, ছোট net এ learning rate 1e-3 এর আশেপাশে (বড় model fine-tune এ 1e-4 বা কম), শুরুতে অল্প warm-up তারপর cosine decay, memory যতটা দেয় তত বড় batch, gradient clipping norm 1.0 এ।
- Batch size আর learning rate একসাথে নড়ে। Batch দ্বিগুণ করলে প্রায়ই বড় learning rate লাগে; খুব বড় batch একটু খারাপ generalise করতে পারে।
- Seed fix করো, তবু একটু noise আশা করো। GPU kernel সবসময় deterministic না। ছোট একটা উন্নতি বিশ্বাস করার আগে কয়েকটা seed এ run মিলিয়ে দেখো।
- Mixed precision (float16/bfloat16) এখনকার GPU তে memory প্রায় অর্ধেক করে আর training দ্রুত করে; framework এর loss scaling ব্যবহার করো যাতে ছোট gradient round হয়ে শূন্য না হয়ে যায়।
- শুধু loss দেখো না: প্রতি layer এর gradient norm, learning rate, আর মরা ReLU কতগুলো। একটা training dashboard loss curve এর ঘণ্টা খানেক আগেই সমস্যা ধরে।
এক কথায়
আসল training এ লাগে mini-batch, momentum আর প্রতি weight এর আলাদা step size ওয়ালা optimiser (Adam), ঠিক scale এর random weight (He/Xavier), আর normalisation সহ ReLU ধরনের activation, যাতে gradient মিলিয়েও না যায়, ফেটেও না যায়।
নিজে করে দেখো
Example এ ReLU network এর init "he" থেকে PyTorch এর default এ বদলাও, 20 layer রেখেই, আর প্রথম layer এর gradient আবার print করো। তারপর train এ opt.step() এর আগে torch.nn.utils.clip_grad_norm_(net.parameters(), 1.0) যোগ করো আর দেখো Adam এর result বদলায় কিনা। কী আশা করো, আর কেন?
Answer
PyTorch এর default init এ প্রথম layer এর gradient নেমে আসে প্রায় 7 × 10⁻¹¹ এ, He init এ যেটা 0.086। ReLU প্রতিটা signal এর অর্ধেক শূন্য করে দেয়, আর default scale তার জন্য বেশি ছোট, তাই 20 টা layer এর প্রতিটায় signal ছোট হয়: শুধু ReLU গভীর network কে বাঁচায় না, init এর scale ও সমান জরুরি। Norm 1.0 এ clipping এখানে আসলে সাহায্য করে: SGD ওঠে 91% এ, momentum 100% এ loss 0.005 সহ, আর Adam এর loss নামে 0.001 এ। প্যাঁচ গুলোতে শুরুর step গুলো কিছু বড় gradient দেয়, আর সেগুলো আটকে রাখলে update স্থির থাকে। শান্ত problem এ clipping প্রায়ই কিছুই বদলায় না; এটা সস্তা একটা safety net, তাই চালু রাখো।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →