সরাসরি পর্বে যান
পর্ব ২৮ · ৯১-এর মধ্যে · মডিউল ৫

Recurrent neural network — hidden state আর backpropagation through time

Recurrent Neural Networks — Hidden State, Unrolling and Backpropagation Through Time

Taught in Bangla, this lesson builds the recurrent network from h_t = tanh(W_xh x_t + W_hh h_(t-1) + b). It unrolls the network, counts parameters, and writes BPTT by hand in in-page NumPy, checked against central differences and PyTorch autograd. It shows how the spectral radius of W_hh makes gradients vanish or explode, trains a running-parity task in NumPy and with nn.RNN, tests truncated BPTT with detach(), and shows how a bidirectional RNN leaks the future in forecasting.

এই পর্বে যা শিখবেন

  • Vanilla RNN cell-এর সূত্র, unrolling, weight sharing, আর হাতে parameter গোনা
  • Many-to-one, many-to-many আর one-to-many task, আর hidden state আসলে কতটা “স্মৃতি”
  • BPTT দুই ধাপে হাতে, তারপর NumPy-তে সাধারণ রূপে, gradient check-সহ
  • Spectral radius ধরে gradient-এর ক্ষয় আর বিস্ফোরণ মাপা
  • nn.RNN-এর output আর h_n-এর shape, truncated BPTT, আর bidirectional RNN-এর leakage-এর ঝুঁকি

পর্ব ২৭ (Sequence ডেটা)-এ দেখেছেন, bag-of-embeddings শব্দের ক্রম হারায়। এবার এমন একটি network, যা ক্রম ধরে পড়ে। পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে। PyTorch-এর সব সংখ্যা ডেস্কটপের সত্যিকারের run থেকে, CPU-তে; সংস্করণ Python 3.14.4, torch 2.15.0 (লেখার সময়ে)।

১ · একটি খাতা, যা প্রতি ধাপে বদলায় — hidden state

ক্রিকেটের স্কোরার প্রতিটি বল দেখেন, আর খাতার একটি ছোট ঘরে স্কোর, উইকেট, ওভার হালনাগাদ করেন। পরের বলের জন্য আগের সব বল মনে রাখতে হয় না, শুধু ওই ঘরটি। ঘর ছোট, তাই সব তথ্য থাকে না: তৃতীয় ওভারের দ্বিতীয় বল কেমন ছিল, তা হারিয়ে যায়। Hidden state ঠিক এই ঘর। পার্থক্য হলো, ঘরে কী লেখা হবে তা কেউ বলে দেয় না; training ঠিক করে।

পর্ব ০৩-এর MLP-এর input-এর মাপ নির্দিষ্ট, আর প্রতিটি অবস্থানের জন্য আলাদা weight. ১০০ শব্দের review-এর জন্য তাহলে ১০০ অবস্থানের weight লাগত, আর “ভালো” শব্দটি প্রথমে না শেষে, তা আলাদা করে শিখতে হতো। RNN cell এর বদলে একটিই ছোট function শেখে, আর sequence-এর প্রতিটি time step-এ সেটিই চালায়:

ht = tanh(Wxh xt + Whh ht−1 + b)
ŷt = Why ht + c

xt হলো t ধাপের input (মাপ D), ht hidden state (মাপ H), আর h0 সাধারণত শূন্য। Wxh input-কে, Whh আগের অবস্থাকে নতুন অবস্থায় আনে; b আর c হলো bias. tanh h-কে −১ থেকে ১-এর মধ্যে রাখে। ŷt একটি logit বা সংখ্যা, task-এর ওপর নির্ভর করে। পর্ব ২০-এ convolution জায়গা জুড়ে weight share করেছিল; RNN সময় জুড়ে করে। এই সূত্রের চেহারাটি প্রায়ই Elman RNN বা vanilla RNN নামে চলে।

W_hh = [[0.5, −0.2], [0.3, 0.6]] · W_xh = [1, −0.5]ᵀ · b = [0.1, 0] · W_hy = [1, 2] · h_prev = [0.2, −0.4]
ধাপ
হিসাব
ফল
  1. শুরু. খাতায় আগের অবস্থা [০.২, −০.৪], নতুন input ০.৫. সব weight নির্দিষ্ট।
  2. আগের অবস্থা. Whh hprev = [০.১৮, −০.১৮]: পুরোনো খাতা থেকে কতটা পরের ধাপে যাবে।
  3. নতুন input. Wxh x = [০.৫, −০.২৫]: এই ধাপের নতুন খবর।
  4. যোগ. দুটি আর b মিলে z = [০.৭৮, −০.৪৩]. পুরোনো আর নতুন এখানে মেশে।
  5. tanh. h = [০.৬৫২৭, −০.৪০৫৩]; সব মান −১ থেকে ১-এর মধ্যে। এটিই নতুন খাতা।
  6. Output. ŷ = ১ × ০.৬৫২৭ + ২ × (−০.৪০৫৩) = −০.১৫৭৯.
  7. পরের ধাপ. একই weight, x = −১, hprev এবার আগের ফল। z = [−০.৪৯২৬, ০.৪৫২৬], h = [−০.৪৫৬৩, ০.৪২৪০], ŷ = ০.৩৯১৮. Weight বদলায়নি, শুধু খাতা বদলেছে।

২ · Unrolling আর parameter গোনা — forward pass আর shape

Cell-এর ভেতরে একটি লুপ আছে: h আবার নিজের দিকেই ফেরে। T দৈর্ঘ্যের একটি input-এর জন্য লুপটিকে T বার খুলে লিখলে একটি সাধারণ feedforward network পাওয়া যায়, যার প্রতিটি স্তরে একই weight. একে বলে unrolling. Backward-এর সব হিসাব এই খোলা ছবিতে হয়।

RNN: ভাঁজ করা আর খোলা

Same-coloured arrows are the same weights. Dashed red arrows show BPTT.

A recurrent network folded and unrolled over three time steps hxy ভাঁজ করা h₁h₂h₃h₀ x₁x₂x₃ ŷ₁ŷ₂ŷ₃ W_hh W_hh W_xh W_hy BPTT: gradient পেছনে
ছবি ২৮.১ — খোলা ছবিতে T = ৩. কমলা, নীল আর সবুজ তীরের প্রতিটি রঙ একটিই weight matrix, তাই T বাড়লেও parameter বাড়ে না।

Parameter গোনা. Wxh হলো H × D, Whh হলো H × H, b-এর মাপ H: মোট H(D + H + 1). Output স্তর Why আর c মিলে O(H + 1). T এই হিসাবে নেই: একই model ৫ ধাপ বা ৫০০ ধাপ পড়তে পারে। নিচের ব্লক batch ধরে forward চালায়, আর দেখায় একটি মাঝের input বদলালে কোন ধাপের h বদলায়।

import numpy as np
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(42)
B, T, D, H, O = 2, 5, 3, 4, 2                # batch, সময়, input, hidden, output
s = 1 / np.sqrt(H)
Wx = rng.normal(0, s, (H, D))                # input থেকে hidden
Wh = rng.normal(0, s, (H, H))                # hidden থেকে hidden
Wy = rng.normal(0, s, (O, H))                # hidden থেকে output
b, c = np.zeros(H), np.zeros(O)              # দুটি bias
X = rng.normal(size=(B, T, D))               # (B, T, D)

def rnn_forward(X):
    h = np.zeros((X.shape[0], H))            # h_0 = 0
    hs = []
    for t in range(X.shape[1]):              # একই Wx, Wh, b প্রতিটি ধাপে
        h = np.tanh(X[:, t] @ Wx.T + h @ Wh.T + b)
        hs.append(h)
    return np.stack(hs, axis=1)              # (B, T, H)

Hs = rnn_forward(X)
Y = Hs @ Wy.T + c                            # (B, T, O): প্রতি ধাপে output
print("X", X.shape, "| Hs", Hs.shape, "| Y", Y.shape)
print("last hidden state, one row per sequence:\n", Hs[:, -1])
print("many-to-one output:", Y[:, -1].shape, "| many-to-many output:", Y.shape)
print("parameters:", Wx.size + Wh.size + Wy.size + b.size + c.size,
      "= H(D+H+1) + O(H+1) =", H * (D + H + 1) + O * (H + 1))
X2 = np.concatenate([X, X], axis=1)          # দ্বিগুণ লম্বা, weight একই
print("T = 10 uses the same weights:", rnn_forward(X2).shape)
X3 = X.copy(); X3[:, 2] += 1.0               # শুধু t = 3-এর input বদলানো
print("change in h per step after editing t=3:",
      np.abs(rnn_forward(X3) - Hs).max(axis=(0, 2)))

আউটপুট (পাতার Python-এ চালানো):

X (2, 5, 3) | Hs (2, 5, 4) | Y (2, 5, 2)
last hidden state, one row per sequence:
 [[-0.452  0.271  0.26   0.189]
 [-0.427 -0.579 -0.127  0.29 ]]
many-to-one output: (2, 2) | many-to-many output: (2, 5, 2)
parameters: 42 = H(D+H+1) + O(H+1) = 42
T = 10 uses the same weights: (2, 10, 4)
change in h per step after editing t=3: [0.    0.    0.981 0.567 0.139]

Parameter ৪২: ৪(৩ + ৪ + ১) = ৩২ আর ২(৪ + ১) = ১০. T = ১০ চালাতে কোনো নতুন weight লাগেনি। শেষ লাইনটি আসল শিক্ষা: t = ৩-এর input বদলালে t = ১, ২ অপরিবর্তিত, কারণ তথ্য শুধু সামনে যায়; t = ৩-এ বদল ০.৯৮১, তারপর ০.৫৬৭, ০.১৩৯. খবর ক্রমে মিলিয়ে যাচ্ছে, যা অংশ ৫-এ মাপা হবে।

৩ · কোন task-এ কোন আকার, আর hidden state কতটা স্মৃতি

তিন ধরনের task

Blue: input, orange: hidden state, green: output with a loss.

Many-to-one, many-to-many and one-to-many RNN tasks Many-to-onereview → sentimenthxhxhxhxy Many-to-manyদিন → পরের দিনhxyhxyhxyhxy One-to-manyবিষয় → লেখাhxyhyhyhy
ছবি ২৮.২ — একই cell; পার্থক্য শুধু কোথায় input দেওয়া হয় আর কোথায় loss হিসাব হয়।
  • Many-to-one: পুরো review পড়ে একটি sentiment; শেষ h (বা পর্ব ১১-এর mask-এর সঙ্গে শেষ আসল ধাপের h) থেকে output.
  • Many-to-many: বাজারঘরের প্রতিদিনের অর্ডার থেকে প্রতিদিন পরের দিনের পূর্বাভাস, বা প্রতিটি শব্দে একটি tag. প্রতি ধাপে loss.
  • One-to-many: একটি input থেকে একটি sequence তৈরি। সাধারণত আগের ধাপের output পরের ধাপের input হয়; পর্ব ৩০ (Seq2seq আর encoder–decoder)-এ এটি পুরোটা আসবে।

Hidden state কী, আর কী নয়. এটি শেখা একটি সারাংশ, রেকর্ড নয়। মাপ H নির্দিষ্ট, tanh-এ সীমিত, তাই ১০০০ ধাপের সব তথ্য সেখানে হুবহু থাকার কথা নয়। AI-এর ভিত্তি কোর্সের পর্ব ৫৩-এর HMM-এর সঙ্গে তুলনা করুন: সেখানে অবস্থা একটি সংখ্যা (বিচ্ছিন্ন), আর একসঙ্গে একটি অবস্থা। RNN-এর h একটি বাস্তব সংখ্যার vector, আর এর হালনাগাদ নিশ্চিত হিসাব, সম্ভাবনা নয়। অনেকটা বেশি প্রকাশক্ষমতা, তবে “স্মৃতি” শব্দটি একটি রূপক; model কী ধরে রাখছে, তা মেপে দেখতে হয়।

h0 আর batch-এর মাঝের অবস্থা. nn.RNN-এ h0 না দিলে শূন্য ধরে। আলাদা আলাদা review-এ প্রতিটি batch শূন্য থেকে শুরু হওয়াই ঠিক, কারণ এক review-এর খাতা অন্য review-এ যাওয়ার কারণ নেই। একটি লম্বা series-কে টুকরো করে পড়লে আগের টুকরোর h পরের টুকরোয় নিয়ে যাওয়া হয়; এটিই অংশ ৮-এর নকশা, যেখানে batch-এর প্রতিটি সারিকে আগের batch-এর একই সারির ধারাবাহিকতা হতে হয়। এলোমেলো shuffle করলে এই ধারাবাহিকতা ভাঙে, তখন h নিয়ে যাওয়া অর্থহীন।

পথকতটা অতীত দেখেদৈর্ঘ্য বদলালেদুর্বলতা
Lag feature + MLPনির্দিষ্ট windowWindow বদলাতে নতুন modelWindow-এর বাইরে অন্ধ; অবস্থানভেদে আলাদা weight
Vanilla RNNতত্ত্বে সব, h দিয়েএকই weightদূরের gradient দুর্বল; ধাপে ধাপে চলে
1D convolution (পর্ব ৩১)Receptive fieldএকই weightReceptive field-এর বাইরে অন্ধ
Attention (পর্ব ৩২)সব ধাপ সরাসরিএকই weightলম্বা sequence-এ খরচ দ্রুত বাড়ে

৪ · BPTT — খোলা network-এ সাধারণ backpropagation

BPTT নতুন কোনো অ্যালগরিদম নয়। পর্ব ০৪-এর backward pass খোলা ছবিতে চালানো হয়; পার্থক্য একটাই। একই Whh প্রতিটি ধাপে আছে, তাই তার gradient হলো সব ধাপের অবদানের যোগফল। δt = ∂L/∂ht ধরলে, আর at = δt ⊙ tanh′(zt) (⊙ মানে ঘর ধরে গুণ):

δt−1 = WhhT at + (t−1 ধাপের loss-এর সরাসরি gradient)
∂L/∂Whh = ∑t at ht−1T  ·  ∂L/∂Wxh = ∑t at xtT

h_t = tanh(w·h_(t−1) + u·x_t) · ŷ = v·h₂ · L = ½(ŷ − 1)²
ধাপ
হিসাব
ফল
  1. Forward, t = ১. h1 = tanh(১.১) = ০.৮০০৫. এই মান মনে রাখতে হবে, backward-এ লাগবে।
  2. Forward, t = ২. একই w আর u; h2 = ০.৭১৬৪.
  3. Loss. ŷ = ১.৪৩২৮, L = ½ × ০.৪৩২৮² = ০.০৯৩৭. এখানে loss শুধু শেষ ধাপে।
  4. δ2. Loss থেকে h2-এ: (ŷ − ১) × v = ০.৮৬৫৭.
  5. t = ২-এর অবদান. tanh′ = ১ − h2² = ০.৪৮৬৭, তাই a2 = ০.৪২১৪. w-এ অবদান a2h1 = ০.৩৩৭৩; u-তে a2x2 = ০.২১০৭.
  6. এক ধাপ পেছনে. δ1 = w × a2 = ০.২১০৭: এখানেই একবার w দিয়ে গুণ, আর একবার tanh′-এর ক্ষয় হয়ে গেছে।
  7. t = ১-এর অবদান. tanh′ = ০.৩৫৯২, a1 = ০.০৭৫৭. w-এ a1h0 = ০.০১৫১; u-তে a1x1 = ০.০৭৫৭. a2-এর তুলনায় প্রায় ছয় ভাগের এক ভাগ।
  8. যোগফল. একই weight, দুটি ধাপের অবদান যোগ: ∂L/∂w = ০.৩৫২৪, ∂L/∂u = ০.২৮৬৪. v-এর পথ সরাসরি: ০.৩১০১.
  9. যাচাই. Central difference ০.৩৫২৪৩৬, ০.২৮৬৩৫৮, ০.৩১০০৯৪: চার দশমিক ঘর পর্যন্ত মিলেছে। lr ০.১ দিয়ে তিনটি weight একবার আপডেট করলে L ০.০৯৩৭ থেকে ০.০৬৫৬ হয়।

PyTorch-এ এই সবকিছু loss.backward() নিজেই করে, কারণ লুপটি computational graph-এ এমনিতেই খোলা থাকে। তবু হাতে লেখার তিনটি লাভ: গুণফলটি চোখে পড়ে, যা অংশ ৫-এর বিষয়; memory আর সময়ের খরচ বোঝা যায়; আর পরীক্ষায় দুই ধাপের BPTT এই ছকেই আসে।

এবার matrix রূপে, প্রতি ধাপে loss-সহ। ব্লকটি পর্ব ০৪-এর gradient check দিয়ে নিজের উত্তর যাচাই করে।

import numpy as np
rng = np.random.default_rng(42)
T, D, H = 6, 3, 4
P = {"Wx": rng.normal(0, .5, (H, D)), "Wh": rng.normal(0, .5, (H, H)),
     "b": rng.normal(0, .1, H), "Wy": rng.normal(0, .5, (1, H))}
X = rng.normal(size=(T, D)); t_y = rng.normal(size=T)     # লক্ষ্য প্রতি ধাপে
def loss_fn(P):
    h, L = np.zeros(H), 0.0
    for t in range(T):
        h = np.tanh(P["Wx"] @ X[t] + P["Wh"] @ h + P["b"])
        L += 0.5 * ((P["Wy"] @ h)[0] - t_y[t]) ** 2
    return L
def bptt(P):
    hs = [np.zeros(H)]                                    # forward: সব h মনে রাখা
    for t in range(T):
        hs.append(np.tanh(P["Wx"] @ X[t] + P["Wh"] @ hs[-1] + P["b"]))
    G = {k: np.zeros_like(v) for k, v in P.items()}
    d = np.zeros(H)                                       # d = ∂L/∂h_t
    for t in range(T, 0, -1):                             # backward: শেষ থেকে শুরু
        e = (P["Wy"] @ hs[t])[0] - t_y[t - 1]
        G["Wy"] += e * hs[t][None, :]
        d = d + e * P["Wy"][0]                            # এই ধাপের loss যোগ
        s = d * (1 - hs[t] ** 2)                          # tanh′ দিয়ে z-এ নামা
        G["Wh"] += np.outer(s, hs[t - 1]); G["Wx"] += np.outer(s, X[t - 1]); G["b"] += s
        d = P["Wh"].T @ s                                 # এক ধাপ পেছনে
    return G
G, eps = bptt(P), 1e-5
for k in P:                                               # central difference
    num = np.zeros_like(P[k])
    for i in np.ndindex(P[k].shape):
        old = P[k][i]; P[k][i] = old + eps; Lp = loss_fn(P)
        P[k][i] = old - eps; Lm = loss_fn(P); P[k][i] = old
        num[i] = (Lp - Lm) / (2 * eps)
    rel = np.linalg.norm(num - G[k]) / np.linalg.norm(num + G[k])
    print(f"{k:3s} shape {str(P[k].shape):7s} ‖grad‖ {np.linalg.norm(G[k]):8.4f}  relative error {rel:.2e}")

আউটপুট (পাতার Python-এ চালানো):

Wx  shape (4, 3)  ‖grad‖   1.3088  relative error 2.39e-11
Wh  shape (4, 4)  ‖grad‖   0.7295  relative error 4.24e-11
b   shape (4,)    ‖grad‖   0.8315  relative error 1.05e-10
Wy  shape (1, 4)  ‖grad‖   2.5401  relative error 6.84e-12

চারটি parameter-এ আপেক্ষিক ভুল প্রায় ১০−১০ বা তার নিচে, তাই হাতের সূত্র ঠিক। কোডে তিনটি জিনিস লক্ষ করুন: forward-এ সব h জমা রাখতে হয় (T ধাপে memory T-এর সমানুপাতিক); G["Wh"]-এ যোগ হয় প্রতিটি ধাপে; আর d-তে প্রতি ধাপে সেই ধাপের loss-ও ঢোকে। এক পাসের খরচ T-এর সমানুপাতিক।

কীমাপকেন
এক ধাপের গুণপ্রায় H² + HDWhhh আর Wxhx
একটি পুরো পাসপ্রায় T(H² + HD), backward-এ একই ক্রমT ধাপ, একটির পর একটি; সমান্তরাল করা যায় না
Memoryপ্রায় BTH, সঙ্গে computational graphBackward-এ প্রতিটি h লাগে
ParameterH(D + H + 1)T নেই, weight share করা

হিসাবের একটি নমুনা: H = ৩২, D = ১, T = ১০০০, B = ৩২ হলে শুধু h জমাতেই ৩২ × ১০০০ × ৩২ = ১০,২৪,০০০টি সংখ্যা, float32-এ প্রায় ৪ MB; সঙ্গে graph-এর হিসাব। Memory-র চেয়েও বড় বাধা প্রায়ই আপডেটের সংখ্যা, যা অংশ ৮-এ দেখবেন। Cell-কে নিজে লুপে চালাতে চাইলে PyTorch-এ এক ধাপের রূপ nn.RNNCell, আর tanh-এর বদলে ReLU চাইলে nonlinearity="relu".

৫ · একই weight T বার — সময়ের পথে vanishing আর exploding

পর্ব ১৭-এ স্তরে স্তরে Jacobian-এর গুণফল দেখেছেন। RNN-এ সেই শিকলের প্রতিটি কড়ায় একই Whh:

∂hT/∂ht = ∏i=t+1T Di Whh,   Di = diag(tanh′(zi))

Di-এর ঘরগুলো tanh-এ ১-এর বেশি নয়। Whh-এর সবচেয়ে বড় eigenvalue-এর absolute মানকে বলে spectral radius ρ (মেশিন লার্নিং কোর্সের পর্ব ০৮). Wk-এর দীর্ঘমেয়াদি আচরণ ρk ঠিক করে।

Scalar RNN: গুণনীয়ক w প্রতি ধাপে একই, ধরা যাক tanh′ = 1 · শেষে matrix রূপ
T
0.9ᵀ
1.0ᵀ
1.1ᵀ
  1. এক ধাপ. তিনটি প্রায় সমান; ০.৯, ১, ১.১.
  2. পাঁচ ধাপ. ০.৫৯ আর ১.৬১: এখনো বড় পার্থক্য নয়।
  3. দশ ধাপ. ০.৩৫ আর ২.৫৯: পার্থক্য এখন সাত গুণের বেশি।
  4. বিশ ধাপ. ০.১২ আর ৬.৭. একটি ২০ শব্দের বাক্যই এই দৈর্ঘ্যের।
  5. পঞ্চাশ ধাপ. ০.০০৫ আর ১১৭: প্রায় ২৩,০০০ গুণের পার্থক্য, শুধু w-এর ০.২ পার্থক্য থেকে।
  6. একশ ধাপ. ২.৭ × ১০−৫ আর প্রায় ১৪,০০০. ঠিক ১ ছাড়া কোনো গুণনীয়ক নিরাপদ নয়, আর ঠিক ১-এ ধরে রাখা গণিতে সহজ, শেখার সময়ে কঠিন।
  7. Matrix. W = diag(১.২, ০.৫) হলে v = [১, ১]-এর ০.৫-এর দিকটি মরে যায়, ১.২-এর দিকটি ফুলে ওঠে: ‖Wkv‖ দ্রুত ১.২k-এর মতো চলে। তাই সবচেয়ে বড় eigenvalue-ই গল্পটি বলে।

এবার মাপা। একটি এলোমেলো W-কে স্কেল করে ρ ঠিক করা হয়েছে, আর ‖∂hT/∂xT−k‖ মাপা হয়েছে: শেষ অবস্থা k ধাপ আগের input-এর প্রতি কতটা সংবেদনশীল। Linear cell (tanh ছাড়া) আর tanh দুটিই।

import numpy as np
import matplotlib.pyplot as plt
H, T, D = 32, 60, 8
rng = np.random.default_rng(42)
Q = rng.normal(0, 1, (H, H))
Q /= np.abs(np.linalg.eigvals(Q)).max()                   # spectral radius ঠিক 1
Wx = rng.normal(0, 1 / np.sqrt(H), (H, D))
X = rng.normal(size=(T, D))
print(f"largest singular value of Q: {np.linalg.norm(Q, 2):.2f} (spectral radius 1)")
fig, ax = plt.subplots(figsize=(6.4, 3.8))
print("cell   rho  slope | k=1      k=10     k=30     k=59")
for act, rho in [("linear", .8), ("linear", 1.0), ("linear", 1.2),
                 ("tanh", .8), ("tanh", 1.0), ("tanh", 1.2), ("tanh", 3.0)]:
    W = rho * Q; h = np.zeros(H); slopes = []
    for t in range(T):
        z = W @ h + Wx @ X[t]
        h = z if act == "linear" else np.tanh(z)
        slopes.append(np.ones(H) if act == "linear" else 1 - h ** 2)
    J, norms = np.diag(slopes[-1]), []                    # J = ∂h_T/∂h_t
    for k in range(1, T):
        J = J @ (np.diag(slopes[T - 1 - k]) @ W)          # আরেকটি ধাপ পেছনে
        norms.append(np.linalg.norm(J @ Wx))              # ∂h_T/∂x_(T-k)
    print(f"{act:6s} {rho:.1f}  {np.mean(slopes):.2f}  | "
          + " ".join(f"{norms[k - 1]:.1e} " for k in (1, 10, 30, 59)))
    ax.semilogy(range(1, T), norms, ls="--" if act == "linear" else "-",
                label=f"{act}, rho = {rho}")
ax.set_xlabel("lag k (how far back the input is)")
ax.set_ylabel("gradient norm (log scale)")
ax.set_title("Last hidden state vs earlier inputs"); ax.legend(fontsize=8, ncol=2)
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে log-scale-এর ছবি):

largest singular value of Q: 1.78 (spectral radius 1)
cell   rho  slope | k=1      k=10     k=30     k=59
linear 0.8  1.00  | 2.2e+00  2.3e-01  5.0e-03  1.5e-05
linear 1.0  1.00  | 2.7e+00  2.1e+00  4.0e+00  8.0e+00
linear 1.2  1.00  | 3.3e+00  1.3e+01  9.5e+02  3.8e+05
tanh   0.8  0.77  | 1.5e+00  2.4e-02  4.4e-06  1.6e-11
tanh   1.0  0.72  | 1.7e+00  1.2e-01  5.4e-04  9.7e-08
tanh   1.2  0.67  | 1.9e+00  4.3e-01  1.0e-02  1.7e-05
tanh   3.0  0.31  | 4.0e-01  3.5e+00  1.5e+02  4.2e+04

Linear cell: বইয়ের হিসাব, ρ = ০.৮-এ রেখা সোজা নিচে, ১.২-এ সোজা ওপরে, ৫৯ ধাপে প্রায় ১০১০ গুণ পার্থক্য। tanh: slope কলামে tanh′-এর গড়। ρ = ১ আর ১.২-এও এই run-এ ক্ষয়, কারণ h বড় হতেই ঢাল ১-এর নিচে নামে; ρ = ১.২ মানেই বিস্ফোরণ নয়। কিন্তু ρ = ৩-এ ঢাল ০.৩১ হলেও ৫৯ ধাপে প্রায় ৪ × ১০৪: tanh h-কে সীমায় রাখে, gradient-কে নয়। Input ছোট করলে ঢাল ১-এর কাছে থাকে আর ছবি linear-এর দিকে সরে, তাই সংখ্যাগুলো input-এর মাপের ওপরও নির্ভর করে।

ρ একা পুরো গল্প নয়. প্রথম লাইনে দেখুন, ρ = ১ হলেও সবচেয়ে বড় singular value ১.৭৮ (মেশিন লার্নিং কোর্সের পর্ব ০৯)। অপ্রতিসম matrix কিছু ধাপ বাড়িয়ে তারপর কমাতে পারে: W = [[০.৯, ০.৫], [০, ০.৯]]-এর ρ = ০.৯, কিন্তু ‖Wk‖ হলো ১.১৮ (k = ১), ২.০০ (k = ১০), ০.১৪ (k = ৫০). Pascanu, Mikolov আর Bengio-র ২০১৩ সালের বিশ্লেষণে tanh-এ সবচেয়ে বড় singular value ১-এর কম হওয়া ক্ষয়ের পক্ষে যথেষ্ট, আর ১-এর বেশি হওয়া বিস্ফোরণের জন্য জরুরি; কোনোটিই উল্টো দিকে নিশ্চয়তা দেয় না।

একটি কষে দেখা হিসাব. প্রতি ধাপের গুণনীয়ক গড়ে ০.৯ হলে gradient হাজার ভাগের এক ভাগ হতে কত ধাপ লাগে? ০.৯n = ১০−৩ থেকে n = ln(১০−৩) / ln(০.৯) ≈ ৬৫.৬, অর্থাৎ প্রায় ৬৬ ধাপ। ০.৫ হলে n ≈ ১০. গড় গুণনীয়ক আর দূরত্বের এই সূত্র ধরে আগেই আন্দাজ করা যায়, কতদূরের নির্ভরতা শেখা কঠিন হবে। তবে এটি শুধু শিকলের মাপ; শেখা হবে কি না, তা অংশ ৭-এর মতো task-এর ওপরও নির্ভর করে।

কী করবেন

Exploding-এর বিরুদ্ধে gradient clipping (পর্ব ১৭): RNN-এ এটি প্রায় নিয়মিত অভ্যাস, এই পর্বের PyTorch-এর সব run-এ আছে। Vanishing-এর বিরুদ্ধে clipping অকেজো; সেখানে পর্ব ২৯ (LSTM আর GRU)-এর gate, আর Whh-এর orthogonal বা identity init সাহায্য করতে পারে (পর্ব ১৪)।

৬ · একটি ছোট task — NumPy-তে running parity শেখানো

Task: ০ আর ১-এর একটি ধারা; প্রতি ধাপে বলতে হবে এ পর্যন্ত ১ জোড় না বিজোড়। বাজারঘরের ভাষায়, প্রতিদিন একটি পণ্য ফেরত এল কি না (১ বা ০), আর এ পর্যন্ত ফেরতের সংখ্যা বিজোড় কি না। এক শব্দে running parity. শেষ input একা কিছুই বলে না: উত্তর পুরো ইতিহাসের ওপর নির্ভর করে, তবে দরকারি স্মৃতি মাত্র এক bit. BPTT, loss আর একটি ছোট Adam (মেশিন লার্নিং কোর্সের পর্ব ১৪) সব হাতে, H = ৮, T = ১২, ৬০০ ধাপ। Loss logistic (binary cross-entropy).

import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
B, T, H, lr = 32, 12, 8, 0.01
s = 1 / np.sqrt(H)
P = {"Wh": rng.normal(0, s, (H, H)), "Wx": rng.normal(0, s, (H, 1)), "b": np.zeros(H),
     "Wy": rng.normal(0, s, (1, H)), "c": np.zeros(1)}
M = {k: 0 * v for k, v in P.items()}; V = {k: 0 * v for k, v in P.items()}
sig = lambda z: 1 / (1 + np.exp(-z))
data = lambda n: (lambda X: (X, np.cumsum(X, 1) % 2))(rng.integers(0, 2, (n, T, 1)).astype(float))
curve = []
for step in range(1, 601):
    X, Y = data(B); hs = [np.zeros((B, H))]
    for t in range(T):
        hs.append(np.tanh(X[:, t] @ P["Wx"].T + hs[-1] @ P["Wh"].T + P["b"]))
    G = {k: 0 * v for k, v in P.items()}; d, loss = np.zeros((B, H)), 0.0
    for t in range(T, 0, -1):                     # হাতে লেখা BPTT
        p = sig(hs[t] @ P["Wy"].T + P["c"]); y = Y[:, t - 1]
        loss -= np.sum(y * np.log(p) + (1 - y) * np.log(1 - p)) / (B * T)
        e = (p - y) / (B * T)
        G["Wy"] += e.T @ hs[t]; G["c"] += e.sum(0); d = d + e @ P["Wy"]
        a = d * (1 - hs[t] ** 2)
        G["Wh"] += a.T @ hs[t - 1]; G["Wx"] += a.T @ X[:, t - 1]; G["b"] += a.sum(0)
        d = a @ P["Wh"]
    for k in P:                                   # Adam, ছোট করে
        M[k] = .9 * M[k] + .1 * G[k]; V[k] = .999 * V[k] + .001 * G[k] ** 2
        P[k] -= lr * M[k] / (1 - .9 ** step) / (np.sqrt(V[k] / (1 - .999 ** step)) + 1e-8)
    curve.append(loss)
    if step in (1, 100, 200, 300, 600): print(f"step {step:3d}  loss {loss:.4f}")
X, Y = data(1000); h = np.zeros((1000, H))
for t in range(T): h = np.tanh(X[:, t] @ P["Wx"].T + h @ P["Wh"].T + P["b"])
print("accuracy at t = 12, 1000 new sequences:", np.mean(((h @ P["Wy"].T + P["c"]) > 0) == Y[:, -1]))
print("mean h, odd minus even:", (h[Y[:, -1, 0] == 1].mean(0) - h[Y[:, -1, 0] == 0].mean(0)).round(2))
plt.figure(figsize=(6.4, 3.2)); plt.plot(curve); plt.axhline(np.log(2), ls="--", color="gray", label="ln 2")
plt.xlabel("training step"); plt.ylabel("loss"); plt.title("Running parity, hand-written BPTT"); plt.legend(); plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে loss curve):

step   1  loss 0.6981
step 100  loss 0.6780
step 200  loss 0.6037
step 300  loss 0.5550
step 600  loss 0.0127
accuracy at t = 12, 1000 new sequences: 1.0
mean h, odd minus even: [-0.98  0.74  0.24  0.71  0.61 -0.01 -0.35 -0.89]

শুরুতে loss ln 2 ≈ ০.৬৯৩১-এর কাছে: model অনুমান করছে। প্রথম ১০০ ধাপে প্রায় কিছুই হয় না, তারপর নামে, আর ৬০০ ধাপে প্রায় ০.০১. নতুন ১০০০টি sequence-এর ১২তম ধাপে accuracy ১.০. আটটি hidden unit আর ১০০-এর কম parameter একটি হিসাব শিখেছে, যা কোনো “শেষ কয়েক input দেখা” model পারত না। শেষ লাইনে খাতার ভেতরটা দেখা যায়: বিজোড় আর জোড় ধারার h-এর গড়ের পার্থক্য। প্রথম আর শেষ unit প্রায় ±০.৯ দূরে, অর্থাৎ কয়েকটি unit প্রায় সরাসরি উত্তরটি ধরে রেখেছে, আর ষষ্ঠটি প্রায় কিছুই নয়। কোন unit কী নেবে, তা কেউ বলে দেয়নি; seed বদলালে সাজানোও বদলাবে। শুরুর সমতল অংশটিও স্বাভাবিক: XOR-এর ধরনের task-এ শুরুতে কোনো একটি input একা উত্তরের সঙ্গে সরলভাবে সম্পর্কিত নয়। তবে এটি সহজ রূপ: প্রতি ধাপে উত্তর মেলানোর সুযোগ আছে। অংশ ৭ দেখাবে, সেই সুযোগ কেড়ে নিলে কী হয়।

৭ · PyTorch-এর nn.RNN — shape, মিলিয়ে দেখা, আর একই task

nn.RNN(input_size, hidden_size, num_layers=1, bidirectional=False, batch_first=False) দুটি জিনিস ফেরত দেয়: output (শেষ স্তরের, প্রতিটি ধাপের h) আর h_n (প্রতিটি স্তরের, শেষ ধাপের h). মনে রাখার সূত্র: output হলো সময় ধরে, h_n হলো স্তর ধরে।

~/bazarghar-dl$ uv run python rnn_shapes.py
Python 3.14.4 | torch 2.15.0+cpu
input x (batch_first=True): (3, 7, 4) = (B, T, D)
layers bidir | output         h_n         params
     1 False | (3, 7, 8)      (1, 3, 8)     112
     2 False | (3, 7, 8)      (2, 3, 8)     256
     1 True  | (3, 7, 16)     (2, 3, 8)     224
     2 True  | (3, 7, 16)     (4, 3, 8)     640
names: [('weight_ih_l0', (8, 4)), ('weight_hh_l0', (8, 8)), ('bias_ih_l0', (8,)), ('bias_hh_l0', (8,))]
1 layer: out[:,-1] == h_n[0]? True
2 layers: out[:,-1] == h_n[-1]? True
bidir: forward  out[:,-1,:H] == h_n[0]? True
bidir: backward out[:,0,H:]  == h_n[1]? True
bidir: out[:,-1] == cat(h_n)? False

তিনটি ফাঁদ এখানেই। এক: batch_first=True output-এ কাজ করে, কিন্তু h_n সবসময় (স্তর × দিক, B, H). দুই: Parameter ১১২ = ৮(৪ + ৮ + ২), বইয়ের H(D + H + 1) = ১০৪ নয়: nn.RNN-এ দুটি bias আলাদা (bias_ih, bias_hh), যদিও যোগ হয়ে একটিই কাজ করে। পরীক্ষায় কোন সূত্র চাওয়া হচ্ছে দেখে নিন। তিন: Bidirectional-এ backward দিকের “শেষ” ধাপ হলো t = ১, তাই out[:, -1] আর h_n-এর জোড়া এক নয়। দ্বিতীয় স্তরের input H (bidirectional হলে ২H), তাই ২৫৬ = ১১২ + ৮(৮ + ৮ + ২). একাধিক স্তরের RNN-কে বলে stacked RNN.

গঠনপ্রথম স্তরপরের প্রতিটি স্তরD = ৪, H = ৮, দুই স্তর
এক দিকH(D + H + 2)H(H + H + 2)১১২ + ১৪৪ = ২৫৬
দুই দিক২H(D + H + 2)২H(২H + H + 2)২২৪ + ৪১৬ = ৬৪০

NumPy-এর cell কি সত্যিই একই? Weight কপি করে দেখা (float64-এ; b = bias_ih + bias_hh):

Wx = rnn.weight_ih_l0.detach().numpy(); Wh = rnn.weight_hh_l0.detach().numpy()
b = (rnn.bias_ih_l0 + rnn.bias_hh_l0).detach().numpy()
h = np.zeros((B, H)); hs = []
for t in range(T):
    h = np.tanh(X[:, t] @ Wx.T + h @ Wh.T + b)
    hs.append(h)
~/bazarghar-dl$ uv run python match_numpy.py
weights: (4, 3) (4, 4) bias (ih + hh): (4,)
output shape: torch (2, 6, 4) | numpy (2, 6, 4)
max |torch - numpy| over all h_t: 1.6653345369377348e-16
max |autograd - hand BPTT|: W_hh 6.245004513516506e-17 | W_xh 1.1102230246251565e-16 | bias_hh 7.632783294297951e-17
float32 model, same check on outputs:
max diff at h_T: 1.1920929e-07

Forward আর হাতের BPTT দুটিই nn.RNN আর autograd-এর সঙ্গে float64-এর গোলকরণ ভুলের মধ্যে মেলে; float32-এ পার্থক্য প্রায় ১০−৭, যা ওই dtype-এর স্বাভাবিক মাপ। ভেতরে আর কোনো রহস্য নেই।

একই task PyTorch-এ, আর একটি বড় শিক্ষা. এবার দুই রকম: প্রতি ধাপে label (many-to-many), আর শুধু শেষে label (logit[:, -1], many-to-one). H = ১৬, Adam lr 0.01, ১৫০০ ধাপ, clipping ১.০, তিনটি seed:

~/bazarghar-dl$ uv run python parity_torch.py
running parity | hidden 16 | Adam lr 0.01 | 1500 steps | torch 2.15.0+cpu
accuracy of the LAST step on 2000 fresh sequences, seeds 42, 1, 2
   T  many-to-many (label every step)   many-to-one (label only at the end)
   2  1.000 1.000 1.000                  1.000 1.000 1.000
   3  1.000 1.000 1.000                  0.501 0.374 1.000
   5  1.000 1.000 1.000                  0.491 0.516 0.459
  10  1.000 1.000 1.000                  0.500 0.493 0.504
  20  1.000 1.000 1.000                  0.472 0.506 0.520
total 91 s on CPU

একই model, একই ক্ষমতা। প্রতি ধাপে label থাকলে T = ২০-তেও ১.০; শুধু শেষে label থাকলে T = ৫-এই আন্দাজের স্তরে। আলাদা একটি run-এ lr 0.003, H = ৩২ আর ৩০০০ ধাপ দিয়েও T = ৩-এ তিনটির একটি seed আর T ≥ ৪-এ কোনো seed শেখেনি। T = ৫-এ gradient-এর পথ মাত্র ৫ ধাপ, তাই শুধু vanishing এখানে কারণ বলে মনে হয় না। একটি ব্যাখ্যা: parity-তে কোনো একটি bit একা উত্তরের সঙ্গে সম্পর্কহীন, সবগুলো একসঙ্গে না দেখলে loss কমার কোনো ইঙ্গিত নেই; আর প্রতি ধাপের label সমস্যাটিকে ছোট ছোট দুই-bit XOR-এ ভেঙে দেয়। এটি কয়েকটি seed আর এক সেট hyperparameter-এর ফল, সাধারণ সীমা নয়। শিক্ষাটি বাস্তবের: কোথায় loss বসছে, তা model-এর আকারের মতোই গুরুত্বপূর্ণ।

অক্ষর ধরে পরের অক্ষর, আর one-to-many. একটি ছোট লেখা (“bazarghar sells chal dal tel. bazarghar delivers in dhaka.”, ৪০ বার) থেকে পরের অক্ষর অনুমান করা শেখানো হলো: input একটি one-hot অক্ষর, output ১৮টি অক্ষরের logit, H = ৩২, window ৩২. শেষে প্রতিটি নতুন অক্ষর আবার input হয়ে পরের অক্ষর তৈরি করে: এটিই one-to-many. তুলনার জন্য bigram baseline, যা শুধু শেষ অক্ষরটি দেখে।

~/bazarghar-dl$ uv run python char_rnn.py
step   1  loss 2.9029
step 100  loss 0.0887
step 300  loss 0.0494
step 600  loss 0.0561
18 characters, 2258 parameters, 2.6 s on CPU
next-char accuracy: bigram baseline 0.593 | RNN 1.000
'bazarghar sells chal dal tel. bazarghar delivers in dhaka. baza'
'delivers in dhaka. bazarghar sel.sbazarghar delivers in dhaka. b'

“a”-এর পরে z, r, l, k বা “.” আসতে পারে, তাই শেষ অক্ষর একা যথেষ্ট নয় (baseline ০.৫৯৩)। RNN-এর h আগের কয়েকটি অক্ষর থেকে অবস্থান বুঝে নেওয়ায় training লেখায় accuracy ১.০০০. Parameter ২২৫৮ = ৩২(১৮ + ৩২ + ২) + ১৮(৩২ + ১). তবে সতর্ক থাকুন: এটি পুনরাবৃত্ত লেখা মুখস্থ করা, ভাষা বোঝা নয়, আর এখানে আলাদা test লেখাও নেই। দ্বিতীয় নমুনায় “sel.sbazarghar” দেখুন: sampling একবার ভুল অক্ষর নিলে h অচেনা অবস্থায় পড়ে, আর পরে চেনা পথে ফেরে।

RNN না শিখলে প্রথম পাঁচ পরীক্ষা (পর্ব ১৮-এর চেকলিস্টের RNN রূপ):

  1. Shape: output আর h_n ছাপুন; batch_first ভুল হলে error ছাড়াই সময় আর batch অদলবদল হয়ে যেতে পারে।
  2. শুরুর loss: V-টি class-এ প্রায় ln V হওয়ার কথা; অক্ষরের run-এ ln 18 ≈ ২.৮৯, আর প্রথম ধাপে এলো ২.৯০.
  3. এক batch overfit: একটি batch-ও মুখস্থ না হলে সমস্যা কোডে।
  4. Gradient norm: clip_grad_norm_-এর ফেরত মান লগ করুন; প্রায় সব ধাপে সীমা ছাড়ালে lr কমান।
  5. Loss কোথায়: Padding-এর ধাপ loss-এ ঢুকছে কি না (পর্ব ১৩-এর ignore_index), আর label যথেষ্ট আছে কি না।

৮ · Truncated BPTT — detach() দিয়ে gradient থামানো

১০০০ দিনের একটি series-এ পুরো BPTT মানে একটি আপডেটের জন্য ১০০০ ধাপ forward আর backward, আর সবগুলো h memory-তে। Truncated BPTT sequence-কে k দৈর্ঘ্যের window-এ কাটে। প্রতিটি window শেষে আপডেট হয়, h পরের window-তে চলে যায়, কিন্তু detach() (পর্ব ০৮) gradient-এর পথ কেটে দেয়।

Truncated BPTT: h যায়, gradient থামে

The state crosses windows forward. Each backward pass stays inside its own window.

Truncated BPTT: hidden state crosses windows, gradient does not forwardbackward window 1 ✕ window 2 ✕ window 3 h moves on, the gradient stops at h.detach()
ছবি ২৮.৩ — প্রতিটি window-এর backward পরের সীমায় থামে, তাই k-এর বেশি পেছনের কারণ গুণ দিয়ে শেখা যায় না।
h = None
for s in range(0, T, k):
    xb, yb = X[:, s:s + k], Y[:, s:s + k]
    logit, h = net(xb, h)
    loss = bce(logit, yb)
    opt.zero_grad(); loss.backward()
    nn.utils.clip_grad_norm_(net.parameters(), 1.0)
    opt.step()
    h = h.detach()                 # এখানেই ছাঁটা

পরীক্ষা: ৩২টি bit-ধারা, প্রতিটি ১০০০ ধাপ; লক্ষ্য হলো ঠিক ৮ ধাপ আগের bit. H = ৩২, Adam lr 3e-3, ১৫টি পাস। k-ই একমাত্র বদল। আসল run-এ প্রতিটি k-এ তিনটি seed ছিল; জায়গার জন্য seed 2-এর সারি বাদ, তার ফল একই ধরনের।

~/bazarghar-dl$ uv run python tbptt.py
stream task: target = input 8 steps ago | 2.15.0+cpu CPU | ln2 = 0.6931
    k  seed  val_loss  val_acc  updates   secs
    2    42    0.6816    0.558     7500    6.0
    2     1    0.6927    0.521     7500    6.5
    4    42    0.6909    0.529     3750    6.2
    4     1    0.6901    0.530     3750    6.7
    8    42    0.6913    0.519     1875    3.9
    8     1    0.6905    0.525     1875    3.8
   16    42    0.0005    1.000      945    2.5
   16     1    0.0003    1.000      945    2.4
   32    42    0.0014    1.000      480    2.1
   32     1    0.0012    1.000      480    2.2
  100    42    0.0082    1.000      150    1.4
  100     1    0.0552    0.991      150    1.5
 1000    42    0.6427    0.650       15    1.4
 1000     1    0.6635    0.613       15    1.4
…
~/bazarghar-dl$ uv run python tbptt_full.py
k=1000 (full BPTT), 300 passes: val_loss 0.0022 val_acc 1.000 updates 300 secs 20.8
--- same loop without h = h.detach() ---
window at step 0 ok
…
RuntimeError: Trying to backward through the graph a second time (or directly access saved tensors after they have already been freed). …
  • k ≤ ৮: প্রায় ln 2. ৮ ধাপ পেছনে পৌঁছাতে ৯ ধাপের window লাগে, তাই কোনো gradient ওই কারণটি দেখে না। Accuracy ০.৫-এর সামান্য ওপরে যেতে পারে: h এলোমেলোভাবে কিছু পুরোনো তথ্য বয়ে আনে আর head তা পড়ে। ট্রাঙ্কেশন শেখা সীমিত করে, forward-এর প্রবাহ নয়।
  • k = ১৬, ৩২: সমাধান. নির্ভরতা window-এর মধ্যে, আর আপডেট অনেক।
  • k = ১০০০, পুরো BPTT: সঠিক gradient, কিন্তু প্রতি পাসে একটি আপডেট। ১৫ পাসে accuracy প্রায় ০.৬৫; ৩০০ পাসে শিখল, এই run-এ ২০.৮ সেকেন্ডে, যেখানে k = ১৬ শিখল প্রায় ২.৫ সেকেন্ডে।
  • সময়: ছোট k-তে আপডেট বেশি, তাই আপডেটের খরচে k = ২ বেশি ধীর (৬ সেকেন্ড বনাম ১.৪)। একটি মেশিনের এক run; অনুপাত ধরুন, সংখ্যা নয়।

শেষ লাইনটিও শিক্ষা: detach() ভুলে গেলে দ্বিতীয় window-এর backward আগের, ইতিমধ্যে মুক্ত graph-এ ঢুকতে যায়, আর error আসে। “retain_graph=True দিন” পরামর্শটি এই ক্ষেত্রে প্রায় সবসময় ভুল: graph প্রতি window-এ লম্বা হয়, memory আর সময় বাড়তে থাকে। সঠিক k নির্ভরতার দৈর্ঘ্যের চেয়ে বড় হওয়া দরকার, যা বাস্তবে আগে জানা থাকে না। k-ও তাই validation দিয়ে বাছাই করার একটি hyperparameter.

বইয়ে প্রায়ই দুটি সংখ্যা দেখবেন: BPTT(k1, k2), যেখানে k1 ধাপ পরপর আপডেট হয় আর প্রতিবার k2 ধাপ পেছনে যায়। ওপরের loop হলো k1 = k2 = k, যা PyTorch-এ সবচেয়ে সহজ। k1 < k2 করলে window একে অন্যের ওপর পড়ে; ফল আরও ভালো হতে পারে, খরচ বেশি, আর কোড জটিল। ছোট ছোট review-এর batch-এ সাধারণত truncation লাগে না, পুরো review-এর BPTT-ই চলে।

৯ · Bidirectional RNN — দুই দিক থেকে পড়া, আর forecasting-এ leakage

Bidirectional RNN দুটি আলাদা RNN চালায়: একটি শুরু থেকে শেষে, অন্যটি শেষ থেকে শুরুতে; প্রতিটি ধাপে দুই h জোড়া লাগে (Schuster আর Paliwal, ১৯৯৭)। Tagging-এ এটি খুব কাজের: “ব্যাংক”-এর tag পরের শব্দ দেখে ভালো ঠিক হয়। পুরো বাক্য যেখানে আগেই হাতে থাকে, সেখানে এটি নিরাপদ। Many-to-one classification-এ সাধারণত দুই দিকের শেষ অবস্থা torch.cat([h_n[-2], h_n[-1]], dim=1) জুড়ে নেওয়া হয়, যার মাপ ২H; অংশ ৭ দেখিয়েছে out[:, -1] এর সমান নয়। খরচ প্রায় দ্বিগুণ।

Forecasting-এ নয়। t ধাপে backward RNN t + ১-এর input দেখে, আর পূর্বাভাসের লক্ষ্যও ঠিক t + ১-এর মান: data leakage (AI-এর ভিত্তি কোর্সের পর্ব ৫৬). বাজারঘরের seed দেওয়া কাল্পনিক ডেটা: ৮টি জেলা, ৪০০ দিন, প্রথম ৩০০ দিনে train, শেষ ১০০ দিনে test. একই model, শুধু bidirectional বদল:

~/bazarghar-dl$ uv run python orders_leak.py
MSE on standardised orders; test = last 100 days
last-value baseline, test MSE: 0.828
model   seed  train   test(whole series fed)  test(only past fed)
uni      42  0.168         0.176                  0.176
uni       1  0.167         0.172                  0.172
uni       2  0.166         0.173                  0.173
bi       42  0.001         0.030                  2.874
bi        1  0.000         0.030                  2.769
bi        2  0.000         0.030                  2.879

Bidirectional train-এ প্রায় ০, আর পুরো series দিয়ে “test” করলেও ০.০৩: দেখে মনে হয় ছয় গুণ ভালো model. কিন্তু বাস্তবে আগামীকাল হাতে থাকে না। শুধু অতীত দিলে MSE ২.৮, last-value baseline-এর তিন গুণের বেশি খারাপ। Unidirectional-এর দুই test কলাম হুবহু এক, এটাই causal model-এর পরীক্ষা: ভবিষ্যৎ মুছে দিলে অতীতের পূর্বাভাস বদলায় না। সঠিক backtest-এর নিয়ম মেশিন লার্নিং কোর্সের পর্ব ৫২-এ, আর এই ধরনের causal নকশা convolution-এ পর্ব ৩১-এ।

আরও গভীরে: BPTT-র বিকল্প real-time recurrent learning (Williams আর Zipser, ১৯৮৯) সামনে চলতে চলতে gradient হিসাব করে, তবে খরচ অনেক বেশি (H৪-এর ক্রমে), তাই UORO-র মতো (Tallec আর Ollivier, ২০১৮) আনুমানিক রূপ নিয়ে গবেষণা হয়েছে। Truncated BPTT-র gradient একটি পক্ষপাতদুষ্ট আনুমান; নির্ভরতা k-এর বেশি হলে এটি সেই দিকটি দেখতেই পায় না, যা অংশ ৮-এ মাপলেন।

পরীক্ষা ও ইন্টারভিউতে:

  • D = ১০, H = ২০, O = ৫-এ vanilla RNN-এর parameter? ২০(১০ + ২০ + ১) + ৫(২০ + ১) = ৬২০ + ১০৫ = ৭২৫; nn.RNN-এর দুই bias ধরলে cell ৬৪০.
  • Whh-এর gradient-এ কয়টি পদ? T ধাপে T-টি, প্রতিটি atht−1T.
  • w = ০.৯-এ ২০ ধাপে কত? ০.৯২০ ≈ ০.১২২; w = ১.১-এ ≈ ৬.৭৩.
  • output বনাম h_n? Output সব সময়-ধাপের, শেষ স্তরের; h_n সব স্তরের, শেষ ধাপের।
  • Forecasting-এ bidirectional কেন নয়? Backward দিক ভবিষ্যৎ দেখে, যা পূর্বাভাসের সময় থাকে না।

১০ · AI-এর যুগে — RNN-এর কোডে AI-এর পরামর্শ যাচাই

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
h_n-কে batch_first ধরে h_n[:, -1] লেখেh_n সবসময় (স্তর × দিক, B, H)একটি ছোট batch-এ shape ছাপুন
Detach error-এ retain_graph=True বসায়Error থামে, graph বাড়তে থাকেপ্রতি window-এর সময় আর memory মাপুন
Forecasting-এ bidirectional=Trueভবিষ্যৎ input দেখে; test অবাস্তব ভালোশুধু অতীত দিয়ে আবার মাপুন; causal model-এ ফল একই থাকে
Parameter গোনে H(D + H + 1), nn.RNN-এর জন্যওএখানে দুটি bias, H(D + H + 2)sum(p.numel() …) চালান
Padding-সহ batch-এ out[:, -1] নেয়ছোট sequence-এ এটি pad-এর ধাপপর্ব ২৭-এর mask বা PackedSequence
ভুল দাবি: “RNN মনে রাখে” বলে long dependency-র আশ্বাসনির্ভরতা না মেপে দাবিlag ধরে gradient আর accuracy মাপুন

AI-কে কোড লিখতে বলার সময় যাচাই-ও চাইতে পারেন:

আমার ডেটা: বাজারঘরের দৈনিক অর্ডার, shape (জেলা, দিন, 1). আমি PyTorch 2.x-এ nn.RNN দিয়ে পরের দিনের পূর্বাভাস চাই। ১. প্রতিটি লাইনের পরে tensor-এর shape কমেন্টে লেখো, আর batch_first-এর সঙ্গে h_n-এর shape কী থাকে বলো। ২. Parameter-এর হিসাব সূত্রে দেখাও, আর sum(p.numel()) দিয়ে মিলিয়ে নেওয়ার লাইন দাও। ৩. লম্বা series-এর জন্য truncated BPTT লেখো: detach কোথায়, clip কোথায়, আর k কীভাবে বাছব বলো। ৪. একটি পরীক্ষা দাও যা প্রমাণ করে model ভবিষ্যৎ দেখছে না (ভবিষ্যতের input বদলে অতীতের output একই আছে কি না)। ৫. Last-value baseline-এর সঙ্গে কয়েকটি seed-এ তুলনার কোড দাও। ৬. যা নিশ্চিত নও, তা অনুমান বলে চিহ্নিত করো; কোনো প্রবন্ধ বা সংখ্যা বানিও না।

AI-এর দেওয়া RNN কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Recurrent network-এর ধারণা কোথা থেকে এল, আর Elman-এর ১৯৯০ সালের প্রবন্ধ কেন এত প্রভাবশালী হলো?

সময়কে সামলানোর একটি পুরোনো উপায় ছিল সময়কে জায়গায় বদলে ফেলা: কয়েকটি আগের input একসঙ্গে একটি window-এ দেওয়া। এতে window-এর দৈর্ঘ্য আগে থেকে ঠিক করতে হয়, যা lag feature-এর সীমাও (মেশিন লার্নিং কোর্সের পর্ব ৫২)। Michael Jordan ১৯৮৬ সালের দিকে output-কে আবার input-এ ফেরানোর একটি নকশা দেন। Jeffrey Elman ১৯৯০ সালে “Finding Structure in Time” প্রবন্ধে hidden স্তরের অবস্থাকেই কপি করে পরের ধাপে ফেরান: এটিই আজ যাকে simple RNN বা Elman network বলা হয়।

প্রবন্ধের জোর ছিল সূত্রে নয়, পর্যবেক্ষণে। Elman একটি temporal XOR task-এ দেখান, network সময়ের কাঠামো ধরে; আর ছোট বাক্যের ওপর শেখানোর পর hidden state-এর cluster বিশ্লেষণে বিশেষ্য আর ক্রিয়ার মতো দল দেখা যায়, যদিও কেউ এই ব্যাকরণ বলে দেয়নি। তখনকার ভাষাতত্ত্বের বিতর্কে এটি আলোচিত হয়েছিল। Rumelhart, Hinton আর Williams-এর ১৯৮৬ সালের কাজেও সময় ধরে খোলা network-এর কথা ছিল, আর Paul Werbos ১৯৯০ সালে “Backpropagation Through Time” নামে পদ্ধতিটির একটি পরিচ্ছন্ন বিবরণ দেন।

এই পর্বের running parity ঠিক সেই গোত্রের: কোনো নিয়ম না বলে দিলেও আটটি unit এক bit স্মৃতি রাখতে শেখে। তবে সাবধান: “ব্যাকরণ আবিষ্কার” একটি ব্যাখ্যা, আর ক্লাস্টার দেখে মডেলের ভেতরে ভাষাবোধ আছে বলা যায় না; এটি ছিল পরিসংখ্যানগত কাঠামোর একটি ছাপ।

মূল উপলব্ধি: একটি সরল লুপ সময়কে আগে থেকে বাঁধা window-এর হাত থেকে মুক্তি দিল; বাকি ইতিহাস হলো সেই লুপকে শেখানোর কঠিনতার ইতিহাস।

প্র ০২RNN তত্ত্বে Turing-সমান শক্তিশালী বলে দেখানো হয়েছে; তাহলে কেন একটি সাধারণ দীর্ঘ নির্ভরতাও শেখে না?

Siegelmann আর Sontag ১৯৯০-এর দশকে দেখান, নির্দিষ্ট আকারের recurrent network, বাস্তব বা যথেষ্ট সূক্ষ্ম মানের অবস্থা নিয়ে, Turing machine-এর সমান হিসাব করতে পারে। এটি অস্তিত্বের ফল: এমন weight আছে। কিন্তু gradient descent সেই weight খুঁজে পাবে, এমন কোনো কথা নেই। এই পর্বেই আপনি দুটি আলাদা প্রশ্নের পার্থক্য দেখেছেন। প্রকাশক্ষমতা: H = ১৬-এর RNN many-to-many-তে T = ২০-এর parity সমাধান করল, তাই ওই weight আছে। শেখার ক্ষমতা: শুধু শেষে label দিলে একই model T = ৫-এই আন্দাজে থাকল।

১৯৯১ সালে Hochreiter-এর থিসিস আর ১৯৯৪ সালে Bengio, Simard আর Frasconi-র প্রবন্ধ এই ফাঁক নিয়ে কাজ করে। পরেরটির একটি মূল যুক্তি: তথ্য স্থিরভাবে ধরে রাখতে হলে dynamics-কে এমন জায়গায় থাকতে হয়, যেখানে ছোট বিচ্যুতি মুছে যায়; অথচ ঠিক এই শর্ত gradient-কেও ক্ষয় করে। অর্থাৎ শব্দ-সহনশীল স্মৃতি আর দীর্ঘ পথের gradient একে অন্যের বিরুদ্ধে কাজ করে। উল্টো দিকে ρ ১-এর ওপরে গেলে gradient বড় হয় বটে, তবে loss surface-এ খাড়া দেয়াল দেখা দেয়।

আরও একটি সীমা আছে: h-এর মাপ নির্দিষ্ট। বাস্তব float-এ অসীম তথ্য রাখা যায় না, তাই তাত্ত্বিক ফলটির সূক্ষ্মতার ধারণা বাস্তবে খাটে না। পর্ব ৩০-এ এই সীমাই encoder–decoder-এর bottleneck হিসেবে মাপা হবে।

মূল উপলব্ধি: “এই model পারে” আর “এই model শিখতে পারবে” দুটি আলাদা দাবি; নকশার বড় অংশ দ্বিতীয়টিকে প্রথমটির কাছে আনার চেষ্টা।

প্র ০৩Truncated BPTT-র gradient “ভুল”, তবু কেন এটি দশকের পর দশক মানক পদ্ধতি থাকল?

Williams আর Peng ১৯৯০ সালের একটি প্রবন্ধে আর পরে Williams আর Zipser-এর কাজে সময়-কাটা BPTT-র পদ্ধতিগত বিবরণ পাওয়া যায়। এর gradient একটি পক্ষপাতদুষ্ট আনুমান: k-এর বেশি পেছনের অবদান শূন্য ধরে নেওয়া হয়। এর পরও কাজ চলে কয়েকটি কারণে।

প্রথমত, vanishing-এর ফলে সেই দূরের পদগুলো অনেক সময় এমনিতেই ছোট, তাই বাদ দিয়ে খুব কম হারায়। দ্বিতীয়ত, আপডেট অনেক বেশি পাওয়া যায়। অংশ ৮-এর run-এ k = ১৬ পনেরো পাসে ৯৪৫টি আপডেট পেল আর সমাধান করল, যেখানে সঠিক gradient-এর k = ১০০০ পেল ১৫টি আপডেট। Noisy হলেও ঘন ঘন আপডেট প্রায়ই একটি নিখুঁত দামি আপডেটকে হারায়, যা SGD-এর মূল যুক্তির সঙ্গে মেলে। তৃতীয়ত, memory: backward-এর জন্য k-টি ধাপ রাখলেই চলে, যা লম্বা ধারায় একমাত্র বাস্তব পথ হতে পারে। ২০১০-এর দশকের চরিত্র-স্তরের language model-এর অনেক কোড ঠিক এই নকশা নিয়েছিল।

দাম হলো অদৃশ্যতা। k-এর বাইরের নির্ভরতা ভুল loss-এর মতো শব্দ করে জানান দেয় না; model শুধু চুপচাপ ওটা শেখে না। অংশ ৮-এ k = ৮-এর সঙ্গে k = ১৬-এর ফারাক ছিল ঠিক দুই দলে: কাজ করে, বা আন্দাজ। পক্ষপাত সরানোর গবেষণাও আছে, যেমন Tallec আর Ollivier-এর ২০১৭ সালের একটি প্রবন্ধ, যেখানে ছাঁটার দৈর্ঘ্য এলোমেলো রেখে নতুন ওজন দেওয়া হয়; তবে প্রচলিত কোডে সরল রূপটিই বেশি দেখা যায় বলে মনে হয়।

মূল উপলব্ধি: সঠিক কিন্তু বিরল gradient-এর চেয়ে আনুমানিক কিন্তু ঘন আপডেট প্রায়ই জেতে; শুধু k-এর সীমাটি জেনে রাখতে হয়।

প্র ০৪Spectral radius ১-এর কাছে রাখলেই সমস্যা শেষ? Echo state network আর orthogonal init থেকে কী শেখা যায়?

২০০০-এর দশকের শুরুতে Herbert Jaeger-এর echo state network একটি সাহসী পথ নেয়: Whh শেখানোই হবে না। একটি বড় এলোমেলো W-কে স্কেল করে ρ ১-এর একটু নিচে রাখা হয়, যাতে পুরোনো input ধীরে মিলিয়ে যায় (প্রায়ই echo state property বলা হয়), আর শুধু output স্তর linear regression দিয়ে শেখে। Wolfgang Maass-এর liquid state machine একই সময়ের কাছাকাছি ধারণা। অংশ ৮-এ k = ২-এ accuracy-র সামান্য ওপরে ওঠা এই ঘরানার একটি ছোট ছায়া: শেখানো না হলেও h কিছু পুরোনো তথ্য বয়ে আনে।

Gradient দিয়ে শেখানো network-এ একই চিন্তা init-এ আসে: Le, Jaitly আর Hinton (২০১৫) ReLU RNN-এ Whh = I দিয়ে শুরুর প্রস্তাব দেন; Arjovsky, Shah আর Bengio (২০১৬) training জুড়েই W-কে unitary রাখার চেষ্টা করেন। Unitary matrix-এর সব singular value ঠিক ১, তাই linear পথে norm অক্ষত।

কিন্তু অংশ ৫ দেখিয়েছে, ρ-এর গল্প অসম্পূর্ণ। ρ = ১ হলেও এই পর্বের W-এর সবচেয়ে বড় singular value ১.৭৮; tanh-এর ঢাল input-এর মাপে বদলায়; আর training চলার সঙ্গে W নিজেও বদলায়। উপরন্তু gradient রক্ষা করলেই দরকারি তথ্য রক্ষা হয় না: একটি নিখুঁত orthogonal cell কিছু ভুলতে পারে না, অথচ অনেক task-এ ভোলাই দরকার। এই ভারসাম্যের একটি শেখা সমাধান LSTM-এর gate, পর্ব ২৯-এর বিষয়।

মূল উপলব্ধি: ρ একটি ভালো প্রথম নির্ণয়, শেষ কথা নয়; আসল লক্ষ্য হলো কোন তথ্য কতদিন থাকবে, তা নিয়ন্ত্রণ করা, আর সেটি স্থির কোনো সংখ্যায় ধরা পড়ে না।

প্র ০৫Transformer আসার পরে vanilla RNN কেন শেখার দরকার, আর recurrent ধারণা কোথায় এখনো টিকে আছে?

Mikolov ও সহকর্মীরা ২০১০ সালে RNN দিয়ে language model দেখান; তার কয়েক বছর পর LSTM-ভিত্তিক model অনুবাদ আর speech-এ প্রধান হয়ে ওঠে। ২০১৭ সালের Transformer প্রবন্ধের পরে বড় ভাষা-model-এ recurrent স্থাপত্য অনেকটা পিছিয়ে যায়। কারণ অংশত এই পর্বেই আছে: RNN-এর ধাপ একটির পর একটি চলে, তাই T ধাপের সমান্তরাল হিসাব কঠিন; দূরের নির্ভরতার পথ T ধাপ লম্বা, আর attention-এ তা প্রায় এক ধাপ।

তবু তিনটি কারণে এই পর্ব কাজে আসে। প্রথমত, ধারণাগত: hidden state, weight sharing, gradient-এর গুণফল, teacher forcing, আর causal চিন্তা পরের সব স্থাপত্যে ফিরে আসে। পর্ব ৩৭ (দক্ষ attention)-এ যে KV cache দেখবেন, তাও এক ধরনের জমা অবস্থা। দ্বিতীয়ত, ব্যবহারিক: h-এর মাপ নির্দিষ্ট, তাই লম্বা ধারাতেও প্রতি ধাপের খরচ আর memory স্থির। সীমিত যন্ত্রে, চলমান সংকেতে বা ছোট series-এ এটি এখনো কাজের হতে পারে। তৃতীয়ত, কিছু নতুন গবেষণা (যেমন RWKV বা state-space ধারার Mamba, রিপোর্ট অনুযায়ী) লম্বা context-এর খরচ কমাতে আবার recurrent ধরনের হালনাগাদ নিচ্ছে; এগুলো সব ক্ষেত্রে Transformer-এর চেয়ে ভালো, এমন দাবি করা যায় না।

আর বাস্তব কথা: অনেক ছোট tabular series-এ gradient boosting আর lag feature-ই কঠিন প্রতিদ্বন্দ্বী (পর্ব ৩১-এ মাপা হবে)। স্থাপত্য বেছে নেওয়ার আগে baseline মাপা দরকার; অংশ ৯-এ last-value baseline-ই একটি “ভালো দেখানো” model-এর আসল চেহারা ধরিয়ে দিয়েছে।

মূল উপলব্ধি: Vanilla RNN আজ প্রায়ই চূড়ান্ত সমাধান নয়, কিন্তু sequence-এ gradient কীভাবে চলে, তা বোঝার সবচেয়ে সরল পরীক্ষাগার।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান।

  1. RNN চালান হাতে: Scalar RNN, w = ০.৮, u = ০.৫, bias ০, h0 = ০, x = (১, −১, ২). (ক) h1, h2, h3 কত? (খ) D = ১০, H = ২০, O = ৫ হলে parameter কত? nn.RNN(10, 20)-এ কত? (গ) গুণনীয়ক ০.৯৫ হলে ২০ ধাপে কত থাকে, ১.০৫ হলে?

    (ক) z1 = ০.৫, h1 = ০.৪৬২১. z2 = ০.৮ × ০.৪৬২১ − ০.৫ = −০.১৩০৩, h2 = −০.১২৯৬. z3 = ০.৮ × (−০.১২৯৬) + ১ = ০.৮৯৬৩, h3 = ০.৭১৪৫. (খ) ২০(১০ + ২০ + ১) + ৫(২০ + ১) = ৭২৫; nn.RNN-এ ২০(১০ + ২০ + ২) = ৬৪০, output স্তর ছাড়া। (গ) ০.৯৫২০ ≈ ০.৩৫৮; ১.০৫২০ ≈ ২.৬৫.

  2. BPTT দুই ধাপে: ht = tanh(w·ht−1 + u·xt), ŷ = v·h2, L = ½(ŷ − ০)². w = ১, u = ০.৫, v = ১, h0 = ০, x1 = x2 = ১. ∂L/∂w আর ∂L/∂u বের করুন।

    Forward: h1 = tanh(০.৫) = ০.৪৬২১; z2 = ০.৯৬২১, h2 = ০.৭৪৫২; ŷ = ০.৭৪৫২; L = ০.২৭৭৭. Backward: δ2 = ০.৭৪৫২; tanh′ = ১ − h2² = ০.৪৪৪৬; a2 = ০.৩৩১৪. δ1 = w·a2 = ০.৩৩১৪; tanh′ = ০.৭৮৬৪; a1 = ০.২৬০৬. ∂L/∂w = a2h1 + a1h0 = ০.১৫৩১ + ০ = ০.১৫৩১. ∂L/∂u = a2x2 + a1x1 = ০.৩৩১৪ + ০.২৬০৬ = ০.৫৯২০. Central difference একই ফল দেয় (০.১৫৩১২৭, ০.৫৯১৯৫৮).

  3. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে? ফাঁদটি কোথায়?
    import torch
    from torch import nn
    rnn = nn.RNN(10, 20, num_layers=3, bidirectional=True)
    out, h_n = rnn(torch.zeros(5, 7, 10))
    print(out.shape, h_n.shape)
    print(sum(p.numel() for p in rnn.parameters()))
    torch.Size([5, 7, 40]) torch.Size([6, 7, 20])
    6240

    ফাঁদ: batch_first default-এ False, তাই (৫, ৭, ১০) মানে T = ৫, B = ৭. Output-এর শেষ মাপ ২ × ২০ = ৪০; h_n-এর প্রথম মাপ ৩ × ২ = ৬. Parameter: প্রথম স্তর ২ × ২০(১০ + ২০ + ২) = ১২৮০; পরের দুটিতে input ৪০, তাই ২ × ২০(৪০ + ২০ + ২) = ২৪৮০ করে। মোট ৬২৪০. সত্যিকারের run-এ হুবহু এই ফল এসেছে; batch_first=True দিলে h_n হতো (৬, ৫, ২০).

  4. সমালোচনা ও নকশা: একজন সহকর্মী বাজারঘরের দৈনিক অর্ডারের পূর্বাভাসে bidirectional RNN বানিয়েছেন। Test MSE ০.০৩, baseline ০.৮৩. শেষ ১০০ দিন test-এ, আর তিনি পুরো series model-এ দিয়ে মেপেছেন। তিনি এখন এটি production-এ দিতে চান। কী বলবেন, আর কী মাপবেন?

    এই সংখ্যা leakage. Backward RNN t ধাপে t + ১-এর input পড়ে, আর লক্ষ্যও সেটিই। Production-এ আগামীকাল থাকবে না। মাপা উচিত: (১) প্রতিটি test দিনে শুধু সেই দিন পর্যন্ত input দিয়ে পূর্বাভাস (অংশ ৯-এ এতে MSE ২.৮); (২) unidirectional model আর last-value বা seasonal-naive baseline, কয়েকটি seed-এ; (৩) rolling-origin backtest, মেশিন লার্নিং কোর্সের পর্ব ৫২ অনুযায়ী; (৪) causal পরীক্ষা: ভবিষ্যতের input বদলালে অতীতের পূর্বাভাস একচুলও বদলানো চলবে না। Bidirectional অতীতের ডেটা পূরণ করা বা tagging-এর মতো কাজে ঠিক আছে, যেখানে পুরো sequence আগেই হাতে থাকে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমTruncated BPTT-র একটি window-এর কাজ সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Sequence থেকে k দৈর্ঘ্যের window কাটা
  2. আগের window-এর h দিয়ে forward চালানো
  3. Window-এর loss হিসাব, zero_grad আর backward
  4. clip_grad_norm_ আর optimizer.step()
  5. h = h.detach() করে পরের window-এ যাওয়া

D = ৪, H = ৮-এর একটি vanilla RNN cell-এর জন্য textbook সূত্র H(D + H + 1) কত parameter দেয়, আর nn.RNN(4, 8)-এ আসলে কত থাকে?

  • দুটিতেই ১০৪
  • ৯৬ আর ১০৪
  • ১০৪ আর ১১২
  • T-এর ওপর নির্ভর করে, তাই বলা যায় না

x-এর shape (3, 7, 4) আর rnn = nn.RNN(4, 8, num_layers=2, batch_first=True, bidirectional=True) হলে out, h_n = rnn(x)-এর shape কী?

  • out (3, 7, 16), h_n (3, 4, 8)
  • out (3, 7, 16), h_n (4, 3, 8)
  • out (3, 7, 8), h_n (2, 3, 8)
  • out (3, 7, 32), h_n (4, 3, 16)

BPTT-তে একটি RNN-এর Whh-এর gradient T ধাপের sequence-এ কীভাবে আসে?

  • প্রতিটি ধাপের অবদান যোগ হয়ে, ∑ at ht−1T
  • প্রতিটি ধাপের জন্য আলাদা একটি gradient, আলাদা Whh-এ
  • শুধু শেষ ধাপ থেকে
  • সব ধাপের h-এর গড় নিয়ে একবার

একটি লম্বা ধারায় লক্ষ্য হলো ঠিক ৮ ধাপ আগের input. Truncated BPTT-র window k = ৪ হলে সবচেয়ে সম্ভাব্য ফল কী?

  • আপডেট বেশি, তাই দ্রুত সমাধান
  • h-এর মান প্রতিটি window-এ শূন্য হয়ে যায়, তাই শেখে না
  • RuntimeError আসে
  • Loss প্রায় আন্দাজের স্তরে থাকে, কারণ gradient ৮ ধাপ পেছনে পৌঁছায় না

পরের দিনের অর্ডারের পূর্বাভাসে একটি bidirectional RNN train MSE প্রায় ০ দেখাল, কিন্তু production-এ শুধু অতীত দিলে baseline-এর চেয়েও খারাপ। কারণ কী?

  • Hidden size বড়, তাই overfit
  • Exploding gradient
  • Backward দিক ভবিষ্যৎ input দেখে, যা পূর্বাভাসের সময় থাকে না: data leakage
  • ডেটা standardise করা হয়েছে

tanh RNN-এ Whh-এর spectral radius ১.২ হলে নিচের কোন কথাটি সবচেয়ে ঠিক?

  • Gradient সবসময় বিস্ফোরিত হবে
  • Gradient বাড়তেও পারে, কমতেও পারে, কারণ tanh-এর ঢাল input আর h-এর মাপের ওপর নির্ভর করে
  • Gradient সবসময় মিলিয়ে যাবে, এটি গাণিতিকভাবে নিশ্চিত
  • tanh থাকায় gradient নিয়ে কোনো চিন্তা নেই

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Cell: ht = tanh(Wxhxt + Whhht−1 + b); weight সব ধাপে এক, parameter T-এর ওপর নির্ভর করে না।
  2. BPTT: খোলা network-এ সাধারণ backprop; একই weight-এর gradient সব ধাপের অবদানের যোগফল। Central difference দিয়ে যাচাই করুন।
  3. গুণফল: Whh বারবার গুণ হয়; ρ আর singular value প্রথম নির্ণয়, tanh-এর ঢালও ভূমিকা রাখে। Exploding-এ clipping, vanishing-এ পর্ব ২৯.
  4. PyTorch: output সময় ধরে, h_n স্তর ধরে; truncated BPTT-তে window শেষে h.detach(), আর k নির্ভরতার চেয়ে বড়।
  5. Bidirectional: tagging-এ কাজের, forecasting-এ leakage. শুধু অতীত দিয়ে মাপুন, আর baseline-এর সঙ্গে তুলনা করুন।

পরের পর্বে: পর্ব ২৯ (LSTM আর GRU — gate দিয়ে দীর্ঘ স্মৃতি): delay বাড়লে vanilla RNN কেমন ব্যর্থ হয়, আর যোগের পথের cell state gradient-কে কীভাবে বাঁচায়, তা সেখানে মাপা হবে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান