পাঠ ২৬ · ৪০-এর মধ্যে · মডিউল ৪

BPTT — Backprop through time

Backpropagation through time
৭ মিনিট পড়া মাঝারি · Intermediate Math-heavy

এই পাঠে যা শিখবেন

  • RNN unrolled — কেন এটা একটি deep network
  • BPTT-এর গাণিতিক রূপ — chain rule সময়ের পেছনে
  • Shared weight-এর gradient — কেন timestep-এ যোগ
  • Truncated BPTT — practical training trick
  • Gradient clipping — exploding-এর সমাধান
  • PyTorch-এ BPTT কীভাবে handle হয় (autograd magic)

১ · Recap — RNN forward

পূর্ববর্তী পাঠ থেকে — RNN forward equations:

$$h_t = \tanh(W_h h_{t-1} + W_x x_t + b)$$ $$y_t = W_y h_t$$ $$L = \sum_{t=1}^{T} \mathcal{L}(y_t, y_t^*)$$

Loss $L$ পুরো sequence-এর সব output-এর যোগফল (অথবা শেষেরটা — depends on task)। আমরা $\partial L / \partial W_h, \partial L / \partial W_x$ চাই — gradient descent-এর জন্য।

২ · Unrolled view — একটি deep network

RNN-কে timestep-এ unroll করুন। $T = 4$ হলে network কার্যত ৪ স্তরের deep network — কিন্তু সব স্তরে weight share। এটা মাথায় রেখে standard backprop apply করুন — এটাই BPTT।

Insight

BPTT কোনো নতুন algorithm নয় — এটা শুধু "RNN-কে unroll করুন, তারপর regular backprop"। Magic হলো — shared weight-এর কারণে gradient সব timestep থেকে যোগ হয়।

৩ · Chain rule — সময়ের পেছনে

একটি timestep $t$-এ loss $L_t$-এর কথা ভাবুন। $W_h$-এর প্রতি gradient:

$$\frac{\partial L_t}{\partial W_h} = \frac{\partial L_t}{\partial h_t} \cdot \sum_{k=1}^{t} \frac{\partial h_t}{\partial h_k} \cdot \frac{\partial h_k}{\partial W_h}$$

যেখানে:

$$\frac{\partial h_t}{\partial h_k} = \prod_{j=k+1}^{t} \frac{\partial h_j}{\partial h_{j-1}} = \prod_{j=k+1}^{t} \text{diag}(\tanh'(\cdot)) \cdot W_h$$

লক্ষ্যণীয়: এই product-এ $t-k$টি $W_h$ গুণ। Long sequence-এ $W_h^{T}$ কার্যত — eigenvalue $< 1$ হলে vanish, $> 1$ হলে explode।

৪ · Total gradient — সব timestep-এর যোগফল

পুরো loss $L = \sum_t L_t$-এর জন্য:

$$\frac{\partial L}{\partial W_h} = \sum_{t=1}^{T} \frac{\partial L_t}{\partial W_h}$$

Shared weight-এর কারণে — প্রতিটি timestep $W_h$-কে "ব্যবহার" করছে, তাই প্রতিটির gradient যোগ। CNN-এ একই pattern — একই kernel image-এর সব position-এ ব্যবহৃত, gradient সব position থেকে যোগ।

ভাবুন একজন শিক্ষক ১০০ ছাত্রকে একই বই দিয়ে পড়ান। কোনো ছাত্র ভুল করলে শিক্ষকের শেখানো কৌশল-এ একটু পরিবর্তন আসা উচিত। ১০০ ছাত্রের সম্মিলিত feedback — এটাই shared weight-এর gradient। RNN-এর প্রতিটি timestep এক একটি "ছাত্র" — সবার থেকে gradient signal accumulate হয়।

৫ · Computational graph — visualization

BPTT — forward (নীল) ও backward (লাল) flow ∂L/∂W_h = Σ_t ∂L_t/∂W_h (shared weight) h₁ h₂ h₃ h₄ L W_h (shared across all timesteps) x₁ x₂ x₃ x₄ ∂L/∂h₁ ∂L/∂h₂ ∂L/∂h₃ প্রতিটি timestep gradient W_h-এ যোগ — shared weight rule।
BPTT — forward computation hidden state propagate, backward gradient সময়ের পেছনে flow। Shared weight-এর gradient সব timestep থেকে accumulate।

৬ · PyTorch-এ BPTT — autograd handle করে

Modern framework-এ BPTT manual implement করতে হয় না — autograd computational graph automatically build ও backward করে। নিচের কোডে দেখুন।

Python · PyTorch
import torch
import torch.nn as nn

torch.manual_seed(0)
rnn = nn.RNN(10, 20, batch_first=True)
linear = nn.Linear(20, 5)
optimizer = torch.optim.SGD(
    list(rnn.parameters()) + list(linear.parameters()),
    lr=0.01)

x = torch.randn(4, 8, 10)        # batch=4, seq=8, feat=10
y = torch.randint(0, 5, (4,))    # 4 labels (many-to-one)

# Forward
out, h = rnn(x)                  # out: (4, 8, 20)
last_h = out[:, -1, :]           # (4, 20)
logits = linear(last_h)
loss = nn.functional.cross_entropy(logits, y)

# Backward — BPTT happens here (autograd unrolls)
optimizer.zero_grad()
loss.backward()
optimizer.step()

print(f"Loss: {loss.item():.4f}")
print(f"W_h grad norm: {rnn.weight_hh_l0.grad.norm():.4f}")

    
loss.backward() call-এ PyTorch autograd full unrolled graph traverse করে — সব timestep থেকে $W_h$-এ gradient যোগ। Manual chain rule writing লাগে না।

৭ · Truncated BPTT (TBPTT)

Long sequence-এ (যেমন ১০,০০০ token-এর book) full BPTT — memory ও compute prohibitive। সমাধান — truncated BPTT: শুধু শেষ $k$ timestep-এ backward।

Python · TBPTT
import torch
import torch.nn as nn

rnn = nn.RNN(10, 20, batch_first=True)
optimizer = torch.optim.SGD(rnn.parameters(), lr=0.01)

# একটি দীর্ঘ sequence — ১০০০ timestep
long_seq = torch.randn(1, 1000, 10)
chunk_size = 50  # truncation length

h = None
for i in range(0, 1000, chunk_size):
    chunk = long_seq[:, i:i+chunk_size, :]
    if h is not None:
        h = h.detach()  # ⚡ gradient flow এখানে কাটো
    out, h = rnn(chunk, h)
    loss = out.pow(2).mean()  # dummy loss

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

print("TBPTT training complete — memory bounded")

    

Key trick: h.detach() — hidden state-এর value রাখে কিন্তু gradient connection break করে। প্রতিটি chunk-এ শুধু সেই chunk-এর gradient compute হয়।

৮ · Gradient clipping — exploding-এর সমাধান

Pascanu et al. (২০১৩) — gradient norm threshold-এ cap। সহজ কিন্তু কার্যকর।

Python · Gradient clipping
import torch.nn.utils as utils

loss.backward()

# Method 1 — norm-based (recommended)
utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# Method 2 — value-based (each element clipped)
# utils.clip_grad_value_(model.parameters(), clip_value=0.5)

optimizer.step()

    

Norm clipping — gradient direction preserve, magnitude cap। Most production RNN training-এ ব্যবহৃত।

৯ · BPTT-এর resource cost

  • Memory: $O(T \cdot H)$ — সব timestep-এ activation store (backward-এর জন্য)। ১০K sequence + ১K hidden = ১০M float = ৪০MB per sample।
  • Compute: $O(T \cdot H^2)$ — forward + backward। Sequential — parallelize hard।
  • Gradient checkpointing: activation save না করে recompute। Memory-compute trade-off।
  • Mixed precision: FP16 — memory অর্ধেক, speed double।
Vanilla BPTT-এ long sequence training prohibitive। সর্বদা TBPTT + clipping + gating (LSTM/GRU) — production recipe। শুধু gradient compute যথেষ্ট না — stable training-ই challenge।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ BPTT-এ shared weight gradient — কেন "সমষ্টি" (sum), "গড়" (average) না? Mathematical justification কী?

এটি BPTT বুঝার সবচেয়ে subtle পয়েন্ট। উত্তর — calculus-এর chain rule-এর সরাসরি ফলাফল।

Multivariable chain rule:

  • $L$ একটি function — যা $W_h$-কে multiple time use করে।
  • Total derivative — প্রতিটি ব্যবহার-এর contribution-এর যোগফল।
  • $\frac{\partial L}{\partial W_h} = \sum_{t} \frac{\partial L}{\partial W_h^{(t)}}$ — যেখানে $W_h^{(t)}$ হলো timestep $t$-এ "instance"।
  • Constraint $W_h^{(1)} = W_h^{(2)} = \ldots = W_h$ — তাই সব contribution একই variable-এ যোগ।

কেন average না:

  • Calculus rule — derivative additive, not averaging।
  • $f(x) = x^2 + x^3$ — $f'(x) = 2x + 3x^2$, average না (সমষ্টি)।
  • একই principle BPTT-এ।
  • Average নিতে চাইলে — gradient $T$ দিয়ে ভাগ করতে পারেন (effectively learning rate scale), কিন্তু এটা design choice, calculus rule না।

Practical implication:

  • Long sequence — gradient magnitude বড়, $T$-এর সমানুপাতিক।
  • Learning rate adjust করতে হতে পারে।
  • Gradient clipping এজন্যই critical।
  • Effective batch size — sequence length অন্তর্ভুক্ত।

CNN-এর সাথে analogy:

  • CNN-এ একই kernel image-এর সব position-এ। Gradient সব position-এর contribution-এর যোগ।
  • Image-এ ১০০x১০০ position = ১০,০০০ contribution।
  • RNN-এ $T$ timestep = $T$ contribution।
  • Same mathematical principle।

Numerical analysis:

  • Sum-এর কারণে — long sequence-এ effective gradient large।
  • Adam-এর adaptive learning rate এই variation handle করে কিছুটা।
  • SGD-তে — manual learning rate tune।

Misconception:

  • "Average নিলে stable হবে" — না, calculus rule break।
  • Gradient meaning বদলে যাবে।
  • Convergence guarantee হারাবে।

মূল উপলব্ধি: Sum vs average — গভীর mathematical question। Calculus chain rule additive। Shared weight-এর জন্য — সব ব্যবহার-এর gradient সরাসরি যোগ। Learning rate স্পেশাল tune করুন। সরল সূত্রের পেছনে গভীর mathematical structure — DL-এ ভালভাবে বুঝে কাজ করুন।

প্র ০২ Truncated BPTT — short window-এ train। Long-range dependency কীভাবে শেখে তাহলে? Window choice trade-off কী?

TBPTT — practical compromise। Theoretically lossy, কিন্তু empirically powerful। Trade-off গভীর।

TBPTT কী miss করে:

  • $k$-step বাইরে gradient signal নেই।
  • $k+1$-step আগের information শেখা সম্ভব না।
  • "৫০০ শব্দ আগে subject — verb-এ agree" — gradient miss।

তাহলে কীভাবে long-range শেখে:

  • Hidden state propagation: hidden state-এ পুরো history-এর information। Forward pass-এ flow।
  • Indirect learning: short-window pattern শিখে — generalize হয়।
  • Cumulative effect: overlapping window — boundary থেকে boundary information leak।

Window choice trade-offs:

  • ছোট ($k=10$): fast, less memory। কিন্তু long-range miss।
  • মাঝারি ($k=50-100$): typical sweet spot। Balance।
  • বড় ($k=500+$): long-range capture। কিন্তু memory expensive, training slow।

Domain-specific guidelines:

  • Sentence-level NLP: $k$ = sentence length (~20-40)।
  • Document classification: $k$ = ১০০-৫০০।
  • Audio frame-level: $k$ = ৫০-২০০ frame।
  • Stock price daily: $k$ = ৩০-৯০ day।
  • Char-level language model: $k$ = ১০০-২৫০ char।

Hyperparameter sensitivity:

  • $k$ কমালে — under-fitting risk।
  • $k$ বাড়ালে — overfitting + slow।
  • Loss curve plateau দেখলে — $k$ বাড়ান।
  • Memory limit-এ — gradient accumulate use।

Alternative — overlapping windows:

  • Window-এ overlap রাখুন (e.g., ৫০% overlap)।
  • Boundary information loss কম।
  • Compute slightly more।
  • Practical-এ helpful।

Modern alternative — attention:

  • Self-attention — direct connection across long range।
  • Gradient flow $O(1)$ regardless of distance।
  • Transformer eliminates TBPTT issue।
  • Memory $O(n^2)$ — সমস্যা ভিন্ন।

Hybrid approach:

  • RNN + attention — RNN local, attention long-range।
  • Memory-augmented network।
  • Hierarchical RNN — multiple time-scale।

মূল উপলব্ধি: TBPTT — practical compromise। Long-range learning indirect — hidden state forward propagate। Window choice domain-specific। Modern attention এই limitation overcome করে। RNN-এ গভীর understanding শিখলে — modern architecture-এর design choice clearer।

প্র ০৩ Gradient clipping value কীভাবে choose করব? Value-based vs norm-based — কোনটা better এবং কেন?

Gradient clipping — RNN training-এ standard practice। Choice subtle ও empirical।

Norm-based clipping:

  • $\|g\|_2 > c$ হলে $g \leftarrow g \cdot c / \|g\|_2$।
  • Direction preserve, magnitude cap।
  • Pascanu et al. (২০১৩) recommended।

Value-based clipping:

  • প্রতিটি element clip — $g_i \leftarrow \text{clamp}(g_i, -v, v)$।
  • Direction বদলে যেতে পারে।
  • Less common, less principled।

কেন norm better:

  • Gradient direction = best descent direction।
  • Magnitude শুধু "step size" determine করে।
  • Direction বদলালে — wrong direction-এ যাবে।
  • Norm preserve করলে — same direction, smaller step।

Threshold value কীভাবে choose:

  • Start: ১.০ (most common)।
  • Logging: training-এ gradient norm log করুন।
  • Quantile-based: ৯০-percentile gradient norm-এর কিছুটা উপর।
  • Too low — under-fitting।
  • Too high — clipping ineffective।

Gradient norm patterns:

  • Training শুরু — norm large (random init)।
  • Mid-training — norm settle।
  • Spike — exploding event। Clipping save করল।
  • Logarithmic plot — pattern সহজে দেখা।

Domain-specific values:

  • NLP RNN: ০.২৫-৫.০।
  • Char-level: ৫.০-১০.০ (longer sequence)।
  • Speech: ১.০-৫.০।
  • Reinforcement learning: ০.৫।

Per-parameter clipping:

  • Each layer-এ আলাদা norm।
  • More fine-grained control।
  • Modern practice — global norm common।

Adam-এ clipping:

  • Adam adaptive — কিন্তু extreme spike-এ confused।
  • Clipping + Adam — stable।
  • Both compatible — different purposes।

Modern alternatives:

  • Gradient noise: add noise — implicit regularization।
  • Adaptive clipping: running statistics-based threshold।
  • Layer norm: implicitly stabilize।

মূল উপলব্ধি: Norm-based clipping — direction preserve, magnitude cap। Value-based — direction distort। Threshold ১.০ default safe। Training metrics-এ gradient norm monitor। Combination clipping + careful init + appropriate architecture (LSTM) = stable training। Modern Transformer-ও এই lesson follow।

প্র ০৪ Bangladesh-এ একটি startup — Bangla SMS spam ১ সপ্তাহের data ১ লক্ষ message। RNN train কীভাবে? Memory-compute budget কী?

Practical scenario — Bangladesh-এর telecom companies (GP, Robi, Banglalink) এই problem face করে।

Data analysis:

  • ১ লক্ষ message — modest size।
  • Average length — ১০-৫০ token।
  • Class imbalance — spam ~২-৫%, ham ~৯৫%।
  • Bangla + English mix — code-switch।
  • SMS-এর special vocabulary — abbreviation, slang।

Architecture choice:

  • Bi-LSTM — short SMS, full BPTT possible।
  • Hidden ১২৮, ২ layer, dropout ০.৩।
  • Embedding ১০০-১৫০-D।
  • Subword tokenization — OOV handle।

Training pipeline:

import torch
import torch.nn as nn

class SMSClassifier(nn.Module):
    def __init__(self, vocab=10000, embed=128,
                 hidden=128, num_classes=2):
        super().__init__()
        self.embed = nn.Embedding(vocab, embed,
                                   padding_idx=0)
        self.lstm = nn.LSTM(embed, hidden,
                            num_layers=2,
                            bidirectional=True,
                            dropout=0.3,
                            batch_first=True)
        self.fc = nn.Linear(hidden * 2, num_classes)

    def forward(self, x):
        emb = self.embed(x)
        _, (h, _) = self.lstm(emb)
        h = torch.cat([h[-2], h[-1]], dim=1)
        return self.fc(h)

Memory budget:

  • Average length ৩০, max ১০০।
  • Batch ৬৪ — ৬৪x১০০x১২৮ float = ৩.২MB activation।
  • Model parameters — ~৫MB।
  • GPU 4GB — comfortable।

Compute budget:

  • ১ epoch — ১০০K message / ৬৪ batch = ১৫০০ batch।
  • Per batch — ~১০ms GPU।
  • Per epoch — ১৫ second।
  • ২০ epoch — ৫ মিনিট।
  • Even CPU-তে — ১ ঘণ্টা।

Class imbalance handling:

# Class-weighted loss
weights = torch.tensor([0.1, 0.9])  # ham, spam
criterion = nn.CrossEntropyLoss(weight=weights)

# Or — focal loss
def focal_loss(pred, target, gamma=2):
    ce = F.cross_entropy(pred, target, reduction='none')
    pt = torch.exp(-ce)
    return ((1 - pt) ** gamma * ce).mean()

BPTT consideration:

  • SMS short (~৩০ token avg) — full BPTT fine।
  • TBPTT লাগে না।
  • Pad-to-max + masking।
  • Pack padded sequence efficient।

Gradient clipping:

import torch.nn.utils as utils

loss.backward()
utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

Evaluation strategy:

  • Train/val/test split — ৮০/১০/১০।
  • Time-based split — newer SMS validate।
  • Metrics — F1 (imbalance), recall, precision।
  • FP costly (legitimate SMS block) — high precision target।

Production deployment:

  • Model size — ~১০MB।
  • Inference latency — <১০ms per SMS।
  • Telecom-এ ১M+ SMS/sec — batch processing।
  • ONNX export, C++ inference।

Continuous learning:

  • Spam pattern evolves।
  • Daily/weekly retrain।
  • Online learning option।
  • Drift detection — monitor production accuracy।

Bangla-specific care:

  • Banglish (English alphabet দিয়ে Bangla) — common।
  • Number-token (e.g., "৫০০ tk" — money)।
  • URL/phone — feature enrich।
  • Cultural spam pattern — "lottery", "ব্যাংক transaction"।

Ethical consideration:

  • Privacy — SMS content sensitive।
  • On-device inference preferred।
  • Data retention policy।
  • User consent।

Realistic numbers:

  • Bi-LSTM — F1 ~০.৮৫-০.৯০।
  • BanglaBERT fine-tune — ~০.৯২-০.৯৫।
  • Production threshold — F1 ০.৯০।
  • Continuous improvement।

মূল উপলব্ধি: Bangladesh SMS spam — modest data, short sequence, full BPTT। Class imbalance critical। Bi-LSTM compact, mobile-friendly। Production — F1 prioritize, FP minimize। Continuous learning essential — adversary evolve। Bangla NLP — practical impact, telecom industry-এ direct revenue।

অনুশীলন

  1. Manual gradient: $T=2$, $h_0=0$, $W_h=0.5$, $W_x=1$, $b=0$, $x_1=x_2=1$, loss $L = (h_2 - 1)^2$। Linear approx ব্যবহার ($\tanh' \approx 1$)। $\partial L / \partial W_h$ কত?

    $h_1 = 0.5 \cdot 0 + 1 \cdot 1 = 1$। $h_2 = 0.5 \cdot 1 + 1 \cdot 1 = 1.5$।

    $\partial L / \partial h_2 = 2(h_2 - 1) = 1$।

    $\partial h_2 / \partial W_h$ = $h_1 + W_h \cdot \partial h_1 / \partial W_h = 1 + 0.5 \cdot 0 = 1$ (since $\partial h_1 / \partial W_h = h_0 = 0$)।

    $\partial L / \partial W_h = 1 \cdot 1 = 1$।

  2. TBPTT code: ১০০০-step input, $k=20$ truncation। PyTorch-এ implement।
    x = torch.randn(1, 1000, 10)
    h = None
    for i in range(0, 1000, 20):
        if h is not None:
            h = h.detach()
        out, h = rnn(x[:, i:i+20], h)
        loss = out.pow(2).mean()
        loss.backward()
        optim.step()
        optim.zero_grad()
  3. Clip vs no-clip: একটি toy RNN-এ random init, ৫০-step sequence। Clipping ছাড়া vs সহিত — gradient norm trajectory plot।
    norms_no_clip, norms_clip = [], []
    for step in range(100):
        loss = train_step(no_clip=True)
        norms_no_clip.append(grad_norm())
        # ... and with clipping
    plt.plot(norms_no_clip, label='No clip')
    plt.plot(norms_clip, label='Clip 1.0')
    plt.yscale('log'); plt.legend()

    সাধারণত — no-clip ক্ষেত্রে spike, clip-এ smooth।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

কোড রানার কাজ না করলে? ব্রাউজারে কাজ না করলে Google Colab ব্যবহার করুন — Google-এর ফ্রি অনলাইন Python পরিবেশ, শুধু Gmail অ্যাকাউন্ট লাগে।
পূর্ববর্তী পাঠ
পাঠ ২৫ · RNN