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

LSTM আর GRU — gate দিয়ে দীর্ঘ স্মৃতি

LSTM and GRU — Gates, Cell State and Long Memory, Measured

Taught in Bangla, this lesson measures the long-memory problem with a delayed recall task where a vanilla RNN fails. It explains the LSTM cell state and gates, steps one cell by hand and in in-page NumPy, and shows why the additive update keeps gradients alive. It tests forget-gate bias, introduces the GRU and parameter counts, matches nn.LSTMCell's weight layout, compares RNN, GRU and LSTM by delay, and runs a Bangla review task with LSTM plus PackedSequence.

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

  • LSTM-এর ছয়টি সমীকরণ, প্রতিটি gate-এর কাজ, আর এক ধাপ হাতে হিসাব
  • যোগের cell update কেন gradient বাঁচায়, lag ধরে মেপে দেখা
  • GRU-তে কোন gate কোথায় মিশেছে, আর RNN, GRU, LSTM-এর parameter গোনা
  • NumPy-র LSTM-কে nn.LSTMCell-এর সাথে মেলানো, weight-এর সারি-ক্রমসহ
  • PackedSequence দিয়ে বাংলা রিভিউয়ের LSTM, আর baseline-এর সাথে সৎ তুলনা

১ · দীর্ঘ স্মৃতির সমস্যা — delay বাড়লে RNN ভেঙে পড়ে

চায়ের দোকানের মালিক দুইভাবে বাকির হিসাব রাখতে পারেন। এক, মাথায় রেখে: প্রতিটি নতুন খদ্দেরের কথা শুনলে আগের হিসাব একটু ঘেঁটে যায়, বিশজন পরে প্রথমজনের বাকি আর মনে নেই। দুই, খাতায় রেখে: খাতার লেখা নিজে থেকে বদলায় না; তিনি শুধু ঠিক করেন কোনটি কাটবেন, কোনটি নতুন লিখবেন আর এখন কোনটি পড়ে শোনাবেন। Vanilla RNN প্রথম পদ্ধতি, LSTM দ্বিতীয়।

পর্ব ২৮ (Recurrent neural network — hidden state আর backpropagation through time) দেখিয়েছে, RNN-এর প্রতিটি ধাপে hidden state একই weight দিয়ে গুণ হয়, তারপর tanh পেরোয়: ht = tanh(Whht−1 + Wxxt + b). তাই BPTT-তে k ধাপ পেছনে যেতে k-টি Jacobian গুণ করতে হয়। পর্ব ১৭-এর গভীর MLP-এর মতোই গুণফলটি মিলিয়ে যায় বা বিস্ফোরিত হয়, কেবল এখানে স্তরের বদলে সময়ের ধাপ, আর সব ধাপে একই Wh.

কথাটি মাপার জন্য একটি ছোট কাজ বানানো হলো, delayed-recall task: শুরুতে ৪টি চিহ্নের একটি, তারপর delay-সংখ্যক এলোমেলো অপ্রাসঙ্গিক token, শেষে একটি প্রশ্ন-চিহ্ন। Model-কে বলতে হবে শুরুর চিহ্নটি কী ছিল। আন্দাজে accuracy ০.২৫।

input:  [2] 6 5 7 4 7 ... 5 6 [?]      (delay = মাঝের token-এর সংখ্যা)
উত্তর:  2

সত্যিকারের run-এ (লেখার সময়ের সংস্করণ PyTorch 2.15.0, Python 3.14.4, CPU-তে) nn.RNN, hidden 32, ১৫০০ ধাপ, batch 64, Adam 3e-3, clip 1.0, প্রতি delay-তে পাঁচটি seed. প্রতিটি ঘরে গড় accuracy আর বন্ধনীতে যতগুলো seed ০.৯৫ পেরিয়েছে:

delay৫১০২০৪০৮০
RNN1.00 [5/5]0.90 [4/5]0.25 [0/5]0.45 [1/5]0.25 [0/5]

১০ ধাপে এক seed হোঁচট খেয়েছে, ২০ ধাপে সব seed আন্দাজে নেমেছে। ৪০ ধাপের একটি সফল seed লক্ষণীয়: এটি ভাগ্য, আর এজন্যই একটি seed দেখে সিদ্ধান্ত নেওয়া যায় না। ফলটি এই ধাপ-সংখ্যার মধ্যে; আরও বেশি training-এ ছবি কিছুটা বদলাতে পারে।

২ · LSTM-এর নকশা — cell state আর তিন gate

LSTM hidden state-এর পাশে একটি দ্বিতীয় vector রাখে, cell state ct. এটিই খাতা। ছবির ওপরের সোজা রেখাটি দেখুন: এই পথে কোনো W গুণ হয় না, কোনো tanh নেই; আছে একটি গুণ আর একটি যোগ।

একটি LSTM cell — ওপরে cell state-এর সোজা পথ

Three gates read [h, x]. The cell path has only one multiply and one add.

LSTM cell: the cell-state path and three gates c(t−1) c(t) × + h(t−1) x(t) fforget iinput c̃tanh ooutput × tanh(c) × h(t)
ছবি ২৯.১ — নিচের h(t−1) আর x(t) একসাথে চার জায়গায় যায়। সবুজ পথে স্মৃতি বয়ে যায়, এখানে কোনো W নেই; লাল forget gate কতটা থাকবে ঠিক করে, নীল input gate কতটা নতুন লেখা হবে, কমলা output gate কতটা বাইরে যাবে।

সমীকরণগুলো (σ হলো sigmoid, ফল ০ থেকে ১; ⊙ হলো ঘরে ঘরে গুণ, যাকে Hadamard product বলে; [h, x] মানে দুটি vector পাশাপাশি জোড়া):

ft = σ(Wf[ht−1, xt] + bf)
it = σ(Wi[ht−1, xt] + bi)
c̃t = tanh(Wc[ht−1, xt] + bc)
ct = ft ⊙ ct−1 + it ⊙ c̃t
ot = σ(Wo[ht−1, xt] + bo)
ht = ot ⊙ tanh(ct)

Forget gate f

খাতার পুরোনো লেখার কতটা থাকবে। f = 1 মানে সব থাকে, f = 0 মানে মুছে যায়।

Input gate i

নতুন প্রস্তাব c̃-এর কতটা লেখা হবে। i = 0 হলে এই ধাপের token খাতায় ওঠে না।

Output gate o

খাতার কতটা এখন পড়ে শোনানো হবে। খাতা বদলায় না, শুধু বাইরের h বদলায়।

একটি আদর্শ ছবি কল্পনা করুন, “কাপড় ভালো না” পড়ার সময়। “কাপড়”-এ বিশেষ কিছু লেখার নেই। “ভালো”-তে input gate খুলে ইতিবাচক কিছু খাতায় ওঠে। “না”-তে একটি unit-এর forget gate আগের লেখা মুছে নতুন, উল্টো চিহ্ন লিখতে পারে। শেষে output gate সেই অবস্থা classifier-কে দেয়। এটি আমাদের গল্প, প্রমাণ নয়: বাস্তব network এই কাজ অনেক unit-এ ছড়িয়ে করে, আর সব unit-এর এমন সহজ অর্থ থাকে না।

দুটি প্রশ্ন প্রায়ই আসে। h আর c-র পার্থক্য কী? c ভেতরের স্মৃতি, মান ±১-এ আটকে থাকে না; h বাইরের জন্য tanh আর o পেরোনো অংশ, তাই |h| ≤ 1, আর এটিই পরের ধাপের gate পড়ে। c̃-তে tanh কেন, gate-এ σ কেন? Gate একটি অনুপাত, তাই ০ থেকে ১। c̃ একটি প্রস্তাব, যার যোগ-বিয়োগ দুটোই লাগে, তাই −১ থেকে ১, যাতে খাতার মান বাড়া-কমা দুটোই হয়।

LSTM-এর প্রবন্ধ Sepp Hochreiter আর Jürgen Schmidhuber-এর, ১৯৯৭ সালে Neural Computation-এ। দীর্ঘ নির্ভরতা শেখার অসুবিধা নিয়ে আগেই কাজ ছিল: Hochreiter-এর ১৯৯১ সালের থিসিস আর Bengio, Simard ও Frasconi-র ১৯৯৪ সালের প্রবন্ধ। একটি প্রচলিত ভুল: আজকের forget gate মূল ১৯৯৭ সংস্করণে ছিল না; এটি এসেছে Gers, Schmidhuber ও Cummins-এর “Learning to Forget” কাজে (প্রায় ১৯৯৯–২০০০)।

৩ · এক ধাপ হাতে, তারপর NumPy-তে

সবকিছু scalar রেখে একটি ধাপ। x = 1, h = 0.5, c = 0.2. প্রতিটি gate-এর weight [h-এর, x-এর] আর bias: f: [1, 1], b = 1; i: [0.5, 1], b = −1; c̃: [1, 0.5], b = 0; o: [1, 1], b = −1.

প্রতি ধাপে একটি নতুন মান
f
i
c̃
c_t
o
h_t
  1. শুরু. আগের ধাপ থেকে h = 0.5, c = 0.2 এসেছে; এই ধাপের input x = 1.
  2. Forget gate. 1×0.5 + 1×1 + 1 = 2.5, σ(2.5) = ০.৯২৪১. পুরোনো খাতার প্রায় ৯২ শতাংশ থাকবে।
  3. Input gate. 0.5×0.5 + 1×1 − 1 = 0.25, σ(0.25) = ০.৫৬২২. নতুন প্রস্তাবের অর্ধেকের একটু বেশি লেখা হবে।
  4. Candidate. 1×0.5 + 0.5×1 + 0 = 1, tanh(1) = ০.৭৬১৬. এটি প্রস্তাব, এখনো খাতায় ওঠেনি।
  5. Cell update. ০.৯২৪১ × 0.2 = ০.১৮৪৮ থাকল, ০.৫৬২২ × ০.৭৬১৬ = ০.৪২৮২ যোগ হলো; ct = ০.৬১৩০।
  6. Output gate. 1×0.5 + 1×1 − 1 = 0.5, σ(0.5) = ০.৬২২৫।
  7. Hidden state. tanh(০.৬১৩০) = ০.৫৪৬২; ht = ০.৬২২৫ × ০.৫৪৬২ = ০.৩৪০০. খেয়াল করুন, gate-গুলো পড়েছে পুরোনো h, ct পড়েনি।

একই ফাংশন scalar আর vector, দুই ক্ষেত্রেই চলে। প্রথমে উপরের হিসাব মিলিয়ে নেওয়া হবে, তারপর nx = 2, nh = 3-এর দুই ধাপ:

import numpy as np

sig = lambda z: 1 / (1 + np.exp(-z))
np.set_printoptions(precision=4, suppress=True)

def lstm_step(x, h, c, W, b):
    """W[k]: (n_h, n_h + n_x) weight, b[k]: bias; k = f, i, g, o"""
    z = np.concatenate([h, x])
    f = sig(W["f"] @ z + b["f"])       # forget gate
    i = sig(W["i"] @ z + b["i"])       # input gate
    g = np.tanh(W["g"] @ z + b["g"])   # candidate
    o = sig(W["o"] @ z + b["o"])       # output gate
    c_new = f * c + i * g
    h_new = o * np.tanh(c_new)
    print("f", f, "| i", i, "\ncand", g, "| o", o)
    print("c_t", c_new, "| h_t", h_new)
    return h_new, c_new

# হাতে করা উদাহরণ: সব scalar, z = [h, x]
W = {"f": np.array([[1, 1.]]), "i": np.array([[.5, 1]]),
     "g": np.array([[1, .5]]), "o": np.array([[1, 1.]])}
b = {"f": 1., "i": -1., "g": 0., "o": -1.}
print("scalar LSTM: x = 1, h = 0.5, c = 0.2")
lstm_step(np.array([1.]), np.array([.5]), np.array([.2]), W, b)

# একই ফাংশন, vector-এ: n_x = 2, n_h = 3
rng = np.random.default_rng(42)
W = {k: rng.normal(0, .5, (3, 5)) for k in "figo"}
b = {k: rng.normal(0, .5, 3) for k in "figo"}
b["f"] = b["f"] + 1.0                  # forget-gate bias
print("\nvector LSTM: n_x = 2, n_h = 3")
h, c = np.zeros(3), np.zeros(3)
for t, x in enumerate(rng.normal(size=(2, 2)), 1):
    print(f"-- t = {t}")
    h, c = lstm_step(x, h, c, W, b)

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

scalar LSTM: x = 1, h = 0.5, c = 0.2
f [0.9241] | i [0.5622] 
cand [0.7616] | o [0.6225]
c_t [0.613] | h_t [0.34]

vector LSTM: n_x = 2, n_h = 3
-- t = 1
f [0.3189 0.6521 0.6636] | i [0.4693 0.5794 0.5779] 
cand [0.3389 0.3001 0.3375] | o [0.4035 0.3526 0.518 ]
c_t [0.159  0.1738 0.195 ] | h_t [0.0636 0.0607 0.0998]
-- t = 2
f [0.3935 0.6186 0.7987] | i [0.5682 0.5575 0.6452] 
cand [ 0.0106 -0.0746  0.4564] | o [0.5051 0.2958 0.4383]
c_t [0.0686 0.066  0.4502] | h_t [0.0346 0.0195 0.185 ]

Scalar অংশ stepper-এর সাথে হুবহু মিলেছে। Vector অংশে প্রতিটি gate-এ ৩টি করে মান: gate একটি সংখ্যা নয়, প্রতিটি unit-এর আলাদা সিদ্ধান্ত। t = 2-এ ১ নম্বর unit-এর f ০.৩৯, মানে সে বেশিরভাগ ভুলছে; ৩ নম্বরের f ০.৮০, সে ধরে রাখছে, আর তার c ০.১৯৫ থেকে ০.৪৫০-এ উঠেছে। সবই এলোমেলো weight-এ; অর্থপূর্ণ আচরণ আসে training-এর পরে।

৪ · f·c + i·c̃ — যোগের update কেন gradient বাঁচিয়ে রাখে

RNN-এ পুরোনো অবস্থা থেকে নতুন অবস্থায় যেতে সব সময় Wh আর tanh-এর ঢাল গুণ হয়। LSTM-এ c ধরে সরাসরি পথের derivative হলো শুধু f. নিচের stepper একটি সরল scalar হিসাব: ধরা যাক RNN-এর প্রতি ধাপের গুণনীয়ক ০.৬ (w = 0.9, tanh-এর ঢাল প্রায় ০.৬৭), আর LSTM-এর f = ০.৯৫।

∂state(t) / ∂state(t − k)-এর আকার
lag k
RNN: 0.6^k
LSTM: f^k
  1. RNN. ht = tanh(w·ht−1 + …), তাই ∂ht/∂ht−1 = w·tanh′(…)। tanh′ ১-এর বেশি হয় না। গুণনীয়ক ১-এ রাখতে w বড় করলে input বড় হয়, tanh saturate করে, ঢাল কমে।
  2. LSTM. ct = f·ct−1 + i·c̃, ct−1-এর সাপেক্ষে সরাসরি derivative = f. i·c̃ পদে ct−1 নেই, তাই যোগের কারণে এটি গুণফলে ঢোকে না।
  3. ৫ ধাপ. RNN ০.০৭৮, LSTM ০.৭৭৪।
  4. ২০ ধাপ. RNN ৩.৬৬ × ১০−৫, LSTM ০.৩৫৮. প্রায় দশ হাজার গুণ পার্থক্য।
  5. ৫০ ধাপ. RNN কার্যত শূন্য; LSTM ০.০৭৭, ছোট কিন্তু কাজে লাগানোর মতো।
  6. ১০০ ধাপ, f = ০.৯৯৯. f শেখা যায়, এটি weight-এর স্থায়ী গুণ নয়। Token-ভেদে f বদলাতে পারে: দরকারি জায়গায় ১-এর কাছে, মোছার সময় ০-এর কাছে। RNN-এর এমন কোনো সুইচ নেই।

১৯৯৭ সালের প্রবন্ধ এই ধারণাকে বলেছিল constant error carousel, আর চেহারায় এটি পর্ব ১৭-এর residual connection-এর আত্মীয়: সেখানে h + F(h), এখানে f·c + i·c̃। একটি সতর্কতা: এটি শুধু সরাসরি পথ। পুরো state-এর Jacobian-এ h হয়ে gate-এ ঢোকা পথও আছে, তাই gradient বাড়তেও পারে, আর gradient clipping LSTM-এও দরকার।

পরীক্ষার ধরনে দুই ধাপ হাতে। ধরা যাক gate-গুলোকে এই হিসাবে ধ্রুবক ধরা হলো: f1 = 0.9, f2 = 0.8. তাহলে c2 = f2(f1c0 + i1c̃1) + i2c̃2, আর ∂c2/∂c0 = f2f1 = 0.72. লক্ষ করুন, i আর c̃-এর কোনো মান এই ফলে নেই: নতুন তথ্য যত আসুক, পুরোনো তথ্যের সংকেত তাতে ঘষা খায় না। RNN-এ এমন আলাদা হওয়া যায় না, কারণ নতুন x আর পুরোনো h একসাথে একই tanh-এ ঢোকে, আর x বড় হলে ঢাল কমে পুরোনো সংকেতও কমে।

এবার সত্যিকারের cell-এ মাপা। প্রথম input একটু নড়ালে k ধাপ পরের অবস্থা কতটা নড়ে, অর্থাৎ ‖∂statek/∂x1‖, ৩২ unit-এর অপ্রশিক্ষিত cell-এ, পাঁচ seed-এর গড়। হিসাবে complex step ব্যবহার করা হয়েছে: input-এ 10−30-এর কাল্পনিক অংশ যোগ করলে output-এর কাল্পনিক অংশ ÷ 10−30 হলো derivative, আর ১০−১৫ মাপেও rounding প্রায় নেই (পর্ব ০৪-এর central difference এত ছোট মানে ভেঙে পড়ে)। অনুচ্ছেদ ৭-এ এটি autograd-এর সাথে মেলানো হয়েছে।

import numpy as np
import matplotlib.pyplot as plt
sig = lambda z: 1 / (1 + np.exp(-z))
n_x, n_h, T, EPS = 8, 32, 100, 1e-30
LAGS = [1, 5, 10, 20, 50, 100]

def curve(seed, bf=None):              # bf = None মানে vanilla RNN
    rng = np.random.default_rng(seed)
    W = rng.normal(0, 1 / np.sqrt(n_h + n_x), (4, n_h, n_h + n_x))
    W[1] *= 0.3                        # f-এর weight ছোট, যাতে bias-ই f ঠিক করে
    v = rng.normal(size=n_x)
    xs = rng.normal(size=(T, n_x)).astype(complex) / np.sqrt(n_x)
    xs[0] += 1j * EPS * v / np.linalg.norm(v)    # complex step: শুধু প্রথম input-এ
    h, c = np.zeros((2, n_h), complex)
    out = []
    for x in xs:
        z = np.concatenate([h, x])
        if bf is None:
            state = h = np.tanh(W[0] @ z)
        else:
            state = c = sig(W[1] @ z + bf) * c + sig(W[0] @ z) * np.tanh(W[2] @ z)
            h = sig(W[3] @ z) * np.tanh(c)
        out.append(np.linalg.norm(state.imag) / EPS)
    return out

fig, ax = plt.subplots(figsize=(6.2, 3.7))
print("cell        " + "".join(f"lag{l:<6d}" for l in LAGS))
for name, bf in [("RNN tanh", None), ("LSTM b_f=0", 0), ("LSTM b_f=1", 1), ("LSTM b_f=3", 3)]:
    y = np.mean([curve(s, bf) for s in range(5)], axis=0)
    print(f"{name:12s}" + "".join(f"{y[l - 1]:<9.1e}" for l in LAGS))
    ax.semilogy(range(1, T + 1), y, label=name)
ax.set(xlabel="lag k (steps after the first input)", ylabel="norm of d state / d x_1 (log)",
       title="Sensitivity to the first input, untrained cells")
ax.legend(fontsize=8); fig.subplots_adjust(bottom=0.16)
plt.show()

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

cell        lag1     lag5     lag10    lag20    lag50    lag100   
RNN tanh    8.9e-01  4.4e-01  1.9e-01  4.0e-02  1.1e-03  2.1e-06  
LSTM b_f=0  4.6e-01  6.9e-02  1.3e-02  5.3e-04  5.2e-08  5.7e-15  
LSTM b_f=1  4.6e-01  2.2e-01  1.5e-01  9.3e-02  3.5e-02  2.6e-03  
LSTM b_f=3  4.6e-01  5.2e-01  9.7e-01  2.9e+00  1.0e+01  2.0e+01  

পড়ার সময় চারটি জিনিস দেখুন। এক, bf = 0-এ LSTM RNN-এর চেয়েও খারাপ: f প্রায় ০.৫, তাই প্রতি ধাপে অর্ধেক। “LSTM মানেই লম্বা স্মৃতি” কথাটি শুরুতে সত্য নয়। দুই, bf = 1-এ ১০০ ধাপে RNN-এর চেয়ে প্রায় হাজার গুণ বেশি। তিন, bf = 3-এ রেখা নামেই না, বরং ওঠে: gate-এর পথেও সংকেত জমে। অর্থাৎ বড় bias vanishing সারায়, কিন্তু বৃদ্ধির ঝুঁকি আনে। চার, এগুলো training-এর আগের মাপ।

Training-এর পরে কী হয়? সত্যিকারের run, delay 40, seed 0: বাস্তব loss-এর gradient ধাপ ধরে (lag 41 মানে শুরুর চিহ্ন), আর distractor ধাপে forget gate:

~/bazarghar-dl$ uv run python trained_gates.py
RNN  None শুরুতে acc 0.26 | ‖∂L/∂x_t‖ lag 1: 1.4e-04, lag 10: 2.8e-07, lag 41: 3.9e-15
RNN  None শেষে   acc 0.29 | ‖∂L/∂x_t‖ lag 1: 4.1e-05, lag 10: 8.8e-07, lag 41: 4.8e-13
LSTM 1.0  শুরুতে acc 0.26 | ‖∂L/∂x_t‖ lag 1: 3.6e-05, lag 10: 3.9e-06, lag 41: 2.5e-08
      forget gate, distractor ধাপে: গড় 0.734 | সর্বোচ্চ unit 0.781 | f > 0.95: 0/32 unit
LSTM 1.0  শেষে   acc 0.23 | ‖∂L/∂x_t‖ lag 1: 1.0e-05, lag 10: 2.0e-07, lag 41: 7.7e-12
      forget gate, distractor ধাপে: গড় 0.636 | সর্বোচ্চ unit 0.702 | f > 0.95: 0/32 unit
LSTM 2.0  শুরুতে acc 0.26 | ‖∂L/∂x_t‖ lag 1: 3.7e-05, lag 10: 1.7e-05, lag 41: 6.1e-06
      forget gate, distractor ধাপে: গড় 0.882 | সর্বোচ্চ unit 0.912 | f > 0.95: 0/32 unit
LSTM 2.0  শেষে   acc 1.00 | ‖∂L/∂x_t‖ lag 1: 1.1e-08, lag 10: 2.4e-10, lag 41: 5.0e-09
      forget gate, distractor ধাপে: গড় 0.934 | সর্বোচ্চ unit 0.971 | f > 0.95: 5/32 unit

শুরুতেই lag 41-এ RNN 3.9e-15, bf = 2-এর LSTM 6.1e-6: প্রায় ১০৯ গুণ বেশি সংকেত, আর ঠিক সেটিই শেখার সুযোগ দিল। শেষে accuracy 1.00; f-এর গড় ০.৮৮২ থেকে ০.৯৩৪, আর ৫টি unit ০.৯৫ পেরিয়েছে। bf = 1 শুরুই করেছে 2.5e-8 সংকেত নিয়ে, আর এই seed-এ ১৫০০ ধাপে পারেনি, f উল্টো ০.৬৩৬-এ নেমেছে। শেষের ছোট gradient ব্যর্থতা নয়: loss প্রায় শূন্য হলে gradient ছোট হয়ই।

৫ · Gate ধাপে ধাপে দেখা, আর forget-gate bias

Gate-এর কাজ চোখে দেখতে একটি হাতে-বসানো cell. এর weight শেখা নয়, আমরা নিজেরা বসিয়েছি, শুধু দেখানোর জন্য: “রাখুন” চিহ্ন s এলে পুরোনোটা মুছে নতুনটা লেখে, “বলুন” চিহ্ন r এলে পড়ে শোনায়।

import numpy as np
import matplotlib.pyplot as plt

sig = lambda z: 1 / (1 + np.exp(-z))
#        t:  1    2    3    4    5    6    7    8    9   10
v = np.array([.8, -.5, .3, -.9, .2, 0, -.7, .4, -.2, 0])    # মান
s = np.array([1,   0,  0,  0,  0, 0,  1,  0,  0, 0])        # "রাখুন" চিহ্ন
r = np.array([0,   0,  0,  0,  0, 1,  0,  0,  0, 1])        # "বলুন" চিহ্ন

c, rows = 0.0, []
print(" t     v  s r |    f     i  cand |     c     o     h")
for t in range(len(v)):
    f = sig(4 - 8 * s[t])          # চিহ্ন এলে পুরোনোটা ভোলা, নইলে ধরে রাখা
    i = sig(8 * s[t] - 4)          # চিহ্ন এলেই লেখা
    cand = np.tanh(2 * v[t])       # লেখার প্রার্থী
    c = f * c + i * cand           # c_t = f·c + i·c̃
    o = sig(8 * r[t] - 4)          # শুধু "বলুন"-এ দরজা খোলা
    h = o * np.tanh(c)
    rows.append((f, i, c, o, h))
    print(f"{t + 1:2d} {v[t]:5.1f}  {s[t]} {r[t]} | {f:.3f} {i:.3f} {cand:5.2f} | "
          f"{c:5.2f} {o:.3f} {h:5.2f}")

f, i, c, o, h = np.array(rows).T
ts = np.arange(1, len(v) + 1)
fig, (a1, a2) = plt.subplots(2, 1, figsize=(6.2, 4.2), sharex=True)
a1.plot(ts, f, "o-", label="forget f")
a1.plot(ts, i, "s-", label="input i")
a1.plot(ts, o, "^-", label="output o")
a1.set(ylabel="gate value", title="Hand-set LSTM cell: gates over 10 steps")
a1.legend(fontsize=8, ncols=3, loc="center")
a2.plot(ts, c, "o-", label="cell c_t")
a2.plot(ts, h, "s-", label="output h_t")
a2.set(xlabel="step t", ylabel="value")
a2.legend(fontsize=8)
plt.show()

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

 t     v  s r |    f     i  cand |     c     o     h
 1   0.8  1 0 | 0.018 0.982  0.92 |  0.91 0.018  0.01
 2  -0.5  0 0 | 0.982 0.018 -0.76 |  0.88 0.018  0.01
 3   0.3  0 0 | 0.982 0.018  0.54 |  0.87 0.018  0.01
 4  -0.9  0 0 | 0.982 0.018 -0.95 |  0.84 0.018  0.01
 5   0.2  0 0 | 0.982 0.018  0.38 |  0.83 0.018  0.01
 6   0.0  0 1 | 0.982 0.018  0.00 |  0.81 0.982  0.66
 7  -0.7  1 0 | 0.018 0.982 -0.89 | -0.85 0.018 -0.01
 8   0.4  0 0 | 0.982 0.018  0.66 | -0.83 0.018 -0.01
 9  -0.2  0 0 | 0.982 0.018 -0.38 | -0.82 0.018 -0.01
10   0.0  0 1 | 0.982 0.018  0.00 | -0.80 0.982 -0.65

t = 1-এ f ০.০১৮, i ০.৯৮২: খাতায় ০.৯১ লেখা হলো। t = 2 থেকে ৫-এ এলোমেলো মান আসছে, কিন্তু i ০.০১৮ বলে c প্রায় নড়ে না, h-ও প্রায় ০। t = 6-এ o ০.৯৮২: h হলো ০.৬৬। t = 7-এ নতুন মান পুরোনোটাকে সরিয়ে −০.৮৫ লিখল। c যে ০.৯১ থেকে ০.৮১-এ একটু একটু নামছে, তা f ১-এর চেয়ে কম হওয়ার দাম। আরও বড় weight-এ ক্ষয় কমে; অনুশীলন ৪ দেখুন।

Forget-gate bias. bf ধনাত্মক রাখলে শুরুতেই f বেশি: bf = 1-এ প্রায় ০.৭৩, 2-এ ০.৮৮। ধারণাটি Gers ও সহলেখকদের কাজে ছিল, আর Jozefowicz, Zaremba ও Sutskever-এর ২০১৫ সালের পরীক্ষা এটিকে জনপ্রিয় করে। PyTorch নিজে এটি করে না: nn.LSTM-এর সব parameter uniform(−1/√h, 1/√h) থেকে আসে (পর্ব ১৪-এর init-এর সূত্র এখানে খাটে না), তাই bf গড়ে ০-এর কাছে। নিজে বসাতে হয়:

# bias-এর সারি-ক্রম i, f, g, o: দ্বিতীয় চতুর্থাংশই forget
hidden = lstm.hidden_size
lstm.bias_ih_l0.data[hidden:2 * hidden] = 2.0
lstm.bias_hh_l0.data[hidden:2 * hidden] = 0.0   # দুটি bias যোগ হয়!

একটি ফাঁদ, যাতে এই পর্বের প্রথম run-ই পড়েছিল। PyTorch-এ bias দুটি, bias_ih আর bias_hh, আর দুটিই গেট-এ যোগ হয়। প্রচলিত উদাহরণ কোডে প্রায়ই থাকে “নামে bias থাকলে 1 বসান”। তাতে দুটোই 1 হয়, আসল bf হয় 2. আমাদের প্রথম run-এ bf = 1 নামের সারিটি আসলে 2 ছিল, আর অনুচ্ছেদ ৮-এর টেবিল দেখাবে দুটির ফল কত আলাদা। শুরুর f হাতে মেপে যাচাই করুন: bf = 1 হলে গড় প্রায় ০.৭৩।

৬ · GRU — দুই gate-এ একই ধরনের কাজ

GRU এসেছে Cho ও সহলেখকদের ২০১৪ সালের encoder–decoder প্রবন্ধে (পর্ব ৩০-এ সেই কাঠামো)। আলাদা খাতা নেই, h-ই খাতা। PyTorch-এর ডকুমেন্টের রূপ:

rt = σ(Wr[ht−1, xt] + br)
zt = σ(Wz[ht−1, xt] + bz)
h̃t = tanh(Wxxt + bx + rt ⊙ (Whht−1 + bh))
ht = zt ⊙ ht−1 + (1 − zt) ⊙ h̃t

r হলো reset gate, z update gate. নামটি বিভ্রান্তিকর: এই রূপে z বড় মানে পুরোনোটা থাকে। কোনো কোনো বইয়ে z আর 1 − z-এর জায়গা উল্টো লেখা হয়; গণিতে এটি কেবল নামের অদলবদল, কিন্তু পরীক্ষায় উত্তর দেওয়ার সময় সূত্রটি লিখে নিন।

একই input: x = 1, h = 0.5 (LSTM-এ c = 0.2)
LSTM
GRU
  1. দুই অবস্থা এক হলো. LSTM-এর c আর h মিশে গেল একটি h-এ।
  2. Forget + input → update. LSTM-এ f আর i স্বাধীন, দুটোই বড় হলে পুরোনোর সাথে নতুন জমতে পারে। GRU-তে z আর 1 − z-এর যোগফল সব সময় ১, পুরোনো আর নতুনের একটি মিশ্রণ।
  3. Output gate বাদ. h-ই বাইরে যায়, পুরোটা। “মনে আছে কিন্তু এখন বলব না” আলাদা করার উপায় নেই।
  4. Reset gate নতুন. Candidate বানাতে পুরোনো h-এর কতটা কাজে লাগবে। r ≈ 0 হলে h̃ শুধু x থেকে আসে। হিসাব: tanh(০.৭৩১১ × 0.5 + 0.5 × 1) = ০.৬৯৯১।
  5. দাম আর ফল. ৪ block-এর বদলে ৩; ht = ০.৬২২৫ × 0.5 + ০.৩৭৭৫ × ০.৬৯৯১ = ০.৫৭৫২। LSTM আর GRU-র সংখ্যা তুলনীয় নয়, weight আলাদা, দেখানোর জন্য এক input.

এবার GRU-র ধাপ আর parameter গোনা। প্রতিটি block-এ Wx (nh × nx), Wh (nh × nh) আর bias. RNN-এ ১টি block, GRU-তে ৩টি, LSTM-এ ৪টি:

import numpy as np

sig = lambda z: 1 / (1 + np.exp(-z))

def gru_step(x, h, Wh, Wx, b):
    """PyTorch-এর রূপ: h_t = z·h + (1 − z)·h̃"""
    r = sig(Wh["r"] * h + Wx["r"] * x + b["r"])      # reset gate
    z = sig(Wh["z"] * h + Wx["z"] * x + b["z"])      # update gate
    cand = np.tanh(Wh["n"] * (r * h) + Wx["n"] * x + b["n"])
    h_new = z * h + (1 - z) * cand
    print(f"r = {r:.4f} | z = {z:.4f} | cand = {cand:.4f}")
    print(f"h_t = {z:.4f}×{h} + {1 - z:.4f}×{cand:.4f} = {h_new:.4f}")
    return h_new

Wh = {"r": 1.0, "z": 1.0, "n": 1.0}
Wx = {"r": 0.5, "z": 1.0, "n": 0.5}
b = {"r": 0.0, "z": -1.0, "n": 0.0}
print("scalar GRU: x = 1, h = 0.5")
gru_step(1.0, 0.5, Wh, Wx, b)

# Parameter গোনা: প্রতিটি "block" = একটি W_x, একটি W_h আর bias
def count(n_x, n_h, blocks, bias_vectors):
    return blocks * (n_x * n_h + n_h * n_h + bias_vectors * n_h)

n_x, n_h = 8, 16
print(f"\nn_x = {n_x}, n_h = {n_h}")
print(f"{'cell':5s} {'blocks':>6s} {'book (1 bias)':>14s} {'PyTorch (2 bias)':>17s}")
for name, k in [("RNN", 1), ("GRU", 3), ("LSTM", 4)]:
    print(f"{name:5s} {k:6d} {count(n_x, n_h, k, 1):14d} {count(n_x, n_h, k, 2):17d}")
print("\nএক সেকেন্ডের হিসাব: LSTM(32, 64) + Embedding(5000, 32) + Linear(64, 2)")
print("LSTM:", count(32, 64, 4, 2), "| Embedding:", 5000 * 32, "| Linear:", 64 * 2 + 2)

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

scalar GRU: x = 1, h = 0.5
r = 0.7311 | z = 0.6225 | cand = 0.6991
h_t = 0.6225×0.5 + 0.3775×0.6991 = 0.5752

n_x = 8, n_h = 16
cell  blocks  book (1 bias)  PyTorch (2 bias)
RNN        1            400               416
GRU        3           1200              1248
LSTM       4           1600              1664

এক সেকেন্ডের হিসাব: LSTM(32, 64) + Embedding(5000, 32) + Linear(64, 2)
LSTM: 25088 | Embedding: 160000 | Linear: 130

বইয়ের সূত্রে k × nh(nx + nh + 1): 400, 1200, 1600. PyTorch-এ bias দুটি, তাই 2: 416, 1248, 1664; অনুচ্ছেদ ৭-এ numel() ঠিক এগুলোই দেবে। পরীক্ষায় প্রশ্নে “PyTorch” লেখা না থাকলে বইয়ের সূত্র লিখে bias-এর ধারণাটি জানিয়ে দিন। শেষ লাইন একটি বাস্তব শিক্ষা: ছোট NLP model-এ parameter-এর বড় অংশ অনেক সময় LSTM-এ নয়, embedding টেবিলে।

৭ · PyTorch-এ মেলানো — nn.LSTMCell-এর weight layout

নিজের সূত্র ঠিক আছে কি না যাচাইয়ের সেরা পথ: একই weight-এ আসল library-র সাথে মেলানো। PyTorch-এ চারটি gate আলাদা নেই; সবগুলো একটি matrix-এর সারিতে। weight_ih-এর আকার (4·nh, nx), আর সারিগুলো চার ভাগে এই ক্রমে: i, f, g, o (g মানে candidate). সমীকরণের ক্রম f, i ধরে ধরলে ফল চুপচাপ ভুল আসবে, কোনো error ছাড়াই।

Wx_i, Wx_f, Wx_g, Wx_o = np.split(cell.weight_ih.detach().numpy(), 4)
Wh_i, Wh_f, Wh_g, Wh_o = np.split(cell.weight_hh.detach().numpy(), 4)
b_i, b_f, b_g, b_o = np.split((cell.bias_ih + cell.bias_hh).detach().numpy(), 4)

def np_lstm(x, h, c):
    i = sig(Wx_i @ x + Wh_i @ h + b_i)
    f = sig(Wx_f @ x + Wh_f @ h + b_f)
    g = np.tanh(Wx_g @ x + Wh_g @ h + b_g)
    o = sig(Wx_o @ x + Wh_o @ h + b_o)
    c_new = f * c + i * g
    return o * np.tanh(c_new), c_new

সত্যিকারের run (double precision-এ, .double()):

~/bazarghar-dl$ uv run python lstm_check.py
torch 2.15.0+cpu
weight_ih  (8, 3)
weight_hh  (8, 2)
bias_ih    (8,)
bias_hh    (8,)
NumPy h: [0.143723 0.493184] c: [0.289136 0.864941]
Torch h: [0.143723 0.493184] c: [0.289136 0.864941]
সর্বোচ্চ পার্থক্য: 5.6e-17

প্যারামিটার সংখ্যা (input 8, hidden 16)
RNN   416
GRU   1248
LSTM  1664

GRU-র সারি-ক্রম: r, z, n
NumPy h: [-0.409742 -0.172929] | Torch h: [-0.409742 -0.172929] | পার্থক্য: 0.0e+00

nn.LSTM-এর shape (batch_first=True, num_layers=2)
output (5, 12, 16) | h_n (2, 5, 16) | c_n (2, 5, 16)
out[:, -1] == h_n[-1]: True

পাতার complex-step gradient বনাম autograd (LSTM cell, lag 10)
autograd : [ 0.003989 -0.003737 -0.00096 ]
complex  : [ 0.003989 -0.003737 -0.00096 ]

LSTM মিলেছে 5.6e-17 পার্থক্যে, মানে rounding-এর সীমায়। GRU-তে প্রথমে মেলেনি: 2.9e-2 পার্থক্য এসেছিল, কারণ আমরা লিখেছিলাম W(r·h), মূল প্রবন্ধের রূপ। PyTorch-এর ডকুমেন্টে স্পষ্ট লেখা আছে তারা r·(W·h + b) করে। বদলানোর পরে পার্থক্য 0. Scalar-এ দুটি সমান, vector-এ নয়; ONNX বা অন্য framework-এ model সরালে এই পার্থক্য সত্যিকারের bug হয়ে দাঁড়াতে পারে।

Shape-এ তিনটি জিনিস মনে রাখুন। output সব ধাপের, শুধু শেষ স্তরের: (5, 12, 16). h_n আর c_n শুধু শেষ ধাপের, সব স্তরের: (2, 5, 16), এদের প্রথম মাত্রা num_layers, batch নয়, batch_first=True থাকলেও। তাই h_n[-1] হলো শেষ স্তর। আর nn.LSTM ফেরত দেয় (output, (h_n, c_n)), জোড়ার ভেতরে জোড়া; nn.GRU ফেরত দেয় (output, h_n). RNN থেকে LSTM-এ গেলে এখানেই কোড ভাঙে।

বাস্তবে আরও চারটি সিদ্ধান্ত, যেগুলো সমীকরণে দেখা যায় না:

  • শুরুর অবস্থা. h আর c না দিলে PyTorch দুটোকেই শূন্য ধরে। Batch আকার বদলালে আগের ব্যাচের অবস্থা ব্যবহার করা যায় না।
  • Dropout. nn.LSTM(..., dropout=0.2) বসে শুধু স্তরের মাঝে, তাই num_layers=1 হলে কাজই করে না আর PyTorch warning দেয়। Input আর output-এর dropout (পর্ব ১৬) আলাদা করে বসাতে হয়।
  • Truncated BPTT. লম্বা sequence টুকরো করে চালালে অবস্থা পরের টুকরোয় দেওয়ার আগে দুটোকেই detach() করতে হয়: h, c = h.detach(), c.detach()। নইলে graph বড় হতে হতে memory ভরে, অথবা দ্বিতীয় backward()-এ error আসে (পর্ব ০৮)।
  • স্তর কয়টি. এক বা দুই স্তর দিয়ে শুরু করা প্রচলিত; স্তর বাড়লে training ধীর হয়, আর ছোট ডেটায় লাভ না-ও হতে পারে। মেপে সিদ্ধান্ত নিন।

৮ · RNN, GRU আর LSTM পাশাপাশি — delay বনাম accuracy

অনুচ্ছেদ ১-এর পরীক্ষাই, সব model-এ। প্রতিটি model আলাদা process-এ, প্রতিটি পাঁচ seed-এ; সংখ্যাগুলো আউটপুট থেকে হুবহু। Run-গুলো আসলে দুই দফায় চলেছিল, নিচে একটি লাইনে সংক্ষেপে দেখানো:

~/bazarghar-dl$ for m in "RNN -" "GRU -" "LSTM -" "LSTM 1" "LSTM 2" "LSTM 3"; do
    uv run python delayed_recall.py $m & done; wait
model৫১০২০৪০৮০
RNN1.00 [5/5]0.90 [4/5]0.25 [0/5]0.45 [1/5]0.25 [0/5]
GRU1.00 [5/5]1.00 [5/5]0.70 [3/5]0.26 [0/5]0.25 [0/5]
LSTM (default)1.00 [5/5]1.00 [5/5]0.24 [0/5]0.26 [0/5]0.24 [0/5]
LSTM b_f=11.00 [5/5]1.00 [5/5]0.90 [4/5]0.26 [0/5]0.24 [0/5]
LSTM b_f=21.00 [5/5]1.00 [5/5]1.00 [5/5]1.00 [5/5]0.25 [0/5]
LSTM b_f=31.00 [5/5]1.00 [5/5]1.00 [5/5]1.00 [5/5]0.75 [3/5]

প্রতি model-এ সময় CPU-তে এক core-এ প্রায় ৩ মিনিট; GRU-র প্রায় ৫ মিনিট লেগেছে, তবে একই সময়ে যন্ত্রে অন্য কাজও চলছিল, তাই এই সময়ের তুলনা করবেন না।

কয়েকটি সৎ পর্যবেক্ষণ:

  • LSTM নাম নিজে কিছু বাঁচায়নি. Default LSTM ২০ ধাপে ব্যর্থ, RNN-এর মতোই। শেখা শুরু হয়েছে bias বদলানোর পরে।
  • Bias-এর ধারা স্পষ্ট. ০.৭৩ → ০.৮৮ → ০.৯৫ শুরুর f-এ পৌঁছানোর সীমা বেড়েছে। অনুচ্ছেদ ৪-এর lag-এর হিসাবের সাথে মেলে।
  • GRU default-এ LSTM default-এর চেয়ে ভালো করেছে ২০ ধাপে। কিন্তু GRU-কে কোনো bias দেওয়া হয়নি, LSTM-কে হয়েছে, তাই “LSTM GRU-র চেয়ে ভালো” এই টেবিল থেকে বলা যায় না।
  • এই ফলের সীমা. একটি কৃত্রিম কাজ, ১৫০০ ধাপের সীমা, পাঁচটি seed. বেশি ধাপে ৮০ delay-ও হয়তো আসবে। এটি একটি local run; অন্য hyperparameter-এ সংখ্যা বদলাবে।

Forecasting-এ কী? একই cell দিয়ে বাজারঘরের দৈনিক অর্ডারের পূর্বাভাসও করা যায়, তবে সেখানে delay-এর চেয়ে trend আর মৌসুম বড় বিষয়। মূল্যায়ন আর lag feature-এর কথা মেশিন লার্নিং কোর্সের পর্ব ৫২-এ আছে; TCN আর LSTM-এর তুলনা পর্ব ৩১-এ। আর HMM-এর সাথে পার্থক্য: AI-এর ভিত্তি কোর্সের পর্ব ৫৩-এর HMM-এ অবস্থা একটি বিচ্ছিন্ন মান আর সম্ভাবনা-টেবিল, এখানে অবস্থা একটি বাস্তব সংখ্যার vector, যা gradient দিয়ে শেখা।

৯ · বাংলা রিভিউ — LSTM + PackedSequence বনাম bag-of-embeddings

পর্ব ২৭-এর bag-of-embeddings baseline শব্দের গড় নেয়, তাই ক্রম হারায়। এবার বাজারঘরের seed দেওয়া ২৪০০টি বানানো রিভিউতে sentiment. Label-এর নিয়ম আমরাই ঠিক করেছি, আর ইচ্ছা করেই সেটি ক্রমের ওপর নির্ভরশীল: “কাপড় ভালো না” নেতিবাচক; “না কাপড় ভালো”-তে না-টি উত্তরের “না”, তাই ইতিবাচক; “A তবে B”-তে শেষ অংশই রায়। শব্দ-ভাগ আর ডেটার ধারণার জন্য দেখুন AI-এর ভিত্তি কোর্সের পর্ব ৬৭, আর Dataset ও vocab-এর জন্য পর্ব ১১।

ডেটার কয়েকটি নমুনা, seed 7-এর জেনারেটর থেকে হুবহু (বাঁয়ে label, 1 মানে ইতিবাচক):

0 না মান খারাপ সবাই নিজে যাচাই করবেন
1 কাপড় চমৎকার
0 ভাইয়ের জন্য কিনলাম মান ভালো তবে ডেলিভারি বাজে সবাই নিজে যাচাই করবেন
0 ঈদের আগে নিয়েছিলাম ডেলিভারি ভালো না
1 ঈদের আগে নিয়েছিলাম প্যাকিং খারাপ না আবার দেখব

ভিন্ন দৈর্ঘ্যের রিভিউ এক batch-এ রাখতে padding লাগে। PackedSequence pad বাদ দিয়ে শুধু আসল token চালায়। তিনটি ছোট রিভিউয়ে দেখা যাক:

এই ধাপে RNN কোন token চালাচ্ছে
রিভিউ ১
রিভিউ ২
রিভিউ ৩
batch_sizes
  1. Padded batch. আকার (3, 3), মোট ৯ ঘর, কিন্তু আসল token ৬টি। দৈর্ঘ্যের তালিকা lens = [3, 2, 1] আলাদা রাখা হলো।
  2. ধাপ ১. তিনটি রিভিউই চলছে, তাই এই ধাপের batch ৩।
  3. ধাপ ২. রিভিউ ৩ শেষ। Batch ছোট হয়ে ২; pad-এর ওপর RNN চলে না।
  4. ধাপ ৩. শুধু রিভিউ ১ বাকি। Batch ১।
  5. ফল. data-তে ৬টি token এই ক্রমে: কাপড়, দাম, চমৎকার, ভালো, খারাপ, না। প্রতিটি রিভিউয়ের h_n তার নিজের শেষ ধাপের। ডিফল্টে দৈর্ঘ্য অনুযায়ী সাজানো চায়; enforce_sorted=False দিলে PyTorch নিজে সাজিয়ে পরে আগের ক্রমে ফেরত দেয়।

Model-এর ভেতরে কোড মাত্র কয়েক লাইন:

e = self.emb(x)                                # (B, T, d), pad সহ
p = pack_padded_sequence(e, lens.cpu(), batch_first=True,
                         enforce_sorted=False)
_, (h_n, _) = self.rnn(p)                      # pad-এর ওপর চলে না
return self.head(h_n[-1]).squeeze(-1)          # প্রতিটি রিভিউয়ের আসল শেষ ধাপ

সত্যিকারের run (CPU-তে; AdamW 3e-3, ১২ epoch, তিন seed):

~/bazarghar-dl$ uv run python review_lstm.py
torch 2.15.0+cpu | train 1800, test 600, vocab 42
দৈর্ঘ্য: 2 থেকে 15 শব্দ | test-এ positive: 0.497
model          params  seed0  seed1  seed2   mean     na  tobe pad60  sec/seed
bag              2433  0.652  0.660  0.658  0.657  0.548 0.671 0.657  1.7
LSTM+pack        9825  1.000  1.000  1.000  1.000  1.000 1.000 1.000  4.8
LSTM pad-only    9825  1.000  1.000  1.000  1.000  1.000 1.000 1.000  2.4
(na = test-এর 392টি না-সহ রিভিউ, tobe = 311টি তবে-সহ রিভিউ, pad60 = pad দিয়ে দৈর্ঘ্য 60 করে test)

একই শব্দের থলি, ভিন্ন ক্রম: P(positive), seed 2-এর model
   bag   LSTM  review
 0.452  0.003  কাপড় ভালো না
 0.452  1.000  না কাপড় ভালো
 0.549  0.999  ডেলিভারি খারাপ না
 0.549  0.001  না ডেলিভারি খারাপ
 0.412  0.000  দাম ভালো তবে সেলাই খারাপ
 0.412  1.000  সেলাই খারাপ তবে দাম ভালো

batch-এর দৈর্ঘ্য: [8, 6, 12, 9] | padded shape: (4, 12)
PackedSequence.data: (35, 32) | batch_sizes: [4, 4, 4, 4, 4, 4, 3, 3, 2, 1, 1, 1]
h_n[-1] == আসল শেষ ধাপ: True
pad-এর ওপর দিয়ে চালালে শেষ ধাপের দূরত্ব: [2.471, 2.497, 0.0, 3.645]

ফল পড়া। Bag ০.৬৫৭-এ আটকে; না-সহ রিভিউতে ০.৫৪৮, প্রায় আন্দাজ। আর জোড়াগুলো দেখুন: bag-এর দুই সারির সংখ্যা হুবহু এক (০.৪৫২, ০.৪৫২), কারণ শব্দের থলি এক, গণিতেই আলাদা হওয়ার উপায় নেই। LSTM জোড়া ভেঙেছে, ০.০০৩ বনাম ১.০০০।

১.০০০ দেখে মুগ্ধ হবেন না। ডেটা বানানো, নিয়ম পরিষ্কার, label-এ কোনো ভুল নেই, vocab মাত্র ৪২। আসল রিভিউতে বানান, ব্যঙ্গ আর ভাষার মিশ্রণ থাকে; সেখানে এমন সংখ্যা আসবে না। পরীক্ষাটি শুধু দেখায়, ক্রম লাগলে bag পারে না। বাস্তবে ক্রমের ভূমিকা অনেক কম হলে bag-ও প্রায়ই খুব কাছে আসে, তাই আগে baseline চালান। পর্ব ৪২-এ text classification আরও গভীরে।

PackedSequence কি accuracy বাড়াল? এখানে না। pad-only সারিও ১.০০০, এমনকি pad দিয়ে দৈর্ঘ্য ৬০ করলেও (pad60). কারণ pad-এর embedding শূন্য আর কাজটি সহজ, LSTM pad-এর ওপর দিয়ে স্মৃতি ধরে রাখতে পেরেছে। সময়ও এখানে pack-এ বেশি লেগেছে (seed-প্রতি ৪.৮ বনাম ২.৪ সেকেন্ড, একটি local run). তবু সে যা নিশ্চিত করে তা হলো সঠিকতা: শেষের লাইনে h_n মিলেছে আসল শেষ ধাপের সাথে, আর pad-এর ওপর চালালে অবস্থা ২.৫ থেকে ৩.৬ দূরে সরে যায় (সবচেয়ে লম্বা রিভিউটির জন্য ০.০, কারণ তার pad নেই)। batch_sizes [4, 4, 4, 4, 4, 4, 3, 3, 2, 1, 1, 1] মানে প্রতি ধাপে কতগুলো রিভিউ এখনো চলছে; যোগফল ৩৫, যেখানে pad সহ ৪ × ১২ = ৪৮। Bidirectional model-এ pack-এর দরকার আরও বেশি, কারণ পেছনের দিক pad দিয়েই শুরু হতো।

আরও গভীরে: Greff ও সহলেখকদের “LSTM: A Search Space Odyssey” আটটি LSTM রূপ তুলনা করে কোনোটিকে সাধারণ LSTM-এর চেয়ে স্পষ্টভাবে ভালো পায়নি, আর forget gate ও output activation-কে সবচেয়ে জরুরি বলে। Peephole connection (gate cell-কে সরাসরি দেখে) PyTorch-এর nn.LSTM-এ নেই। proj_size দিয়ে h-কে ছোট করা যায়, c-কে নয়। হাতে লেখা Python লুপের cell সাধারণত nn.LSTM-এর চেয়ে ধীর।

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

  • LSTM(100, 256)-এর parameter? বইয়ে 4 × 256 × (100 + 256 + 1) = 365,568; PyTorch-এ 366,592.
  • f = 0.8, i = 0.5, c̃ = −0.6, c = 1 হলে ct? 0.8 − 0.3 = 0.5.
  • LSTM vanishing পুরোপুরি সারায়? না; f-এর গুণফল থাকেই, শুধু শেখার যোগ্য একটি পথ পায়। Exploding-এর জন্য clip লাগে।
  • GRU-র gate কয়টি? ২টি, update আর reset; block ৩টি, কারণ candidate-ও একটি block.
  • Lengths [5, 3, 2]-এর batch_sizes? [3, 3, 2, 1, 1].

১০ · AI-এর যুগে — LSTM কোডে AI-এর ভুল ধরা

AI assistant LSTM কোড দ্রুত লেখে, আর ফাঁদগুলো এমন যে কোড চলে, error আসে না, শুধু ফল খারাপ হয়।

AI প্রায়ই যা করেসমস্যাকীভাবে ধরবেন
h_n-কে batch-আগে ধরে h_n[:, -1]h_n-এর প্রথম মাত্রা স্তর, batch_first সত্ত্বেওShape print; h_n[-1] আর আসল শেষ ধাপ মেলান
Padded batch-এ out[:, -1]ছোট রিভিউয়ের শেষ ধাপটি pad-এর ওপরpack ব্যবহার, বা lens − 1 দিয়ে index
“bias” নামের সব parameter-এ 1 বসায়পুরো bias বদলায়, আর দুটি bias যোগ হয়ে f-এ 2 হয়শুরুর f হাতে মাপুন
Forget gate-এর সারি ভুল ধরেআসল ক্রম i, f, g, oNumPy step দিয়ে nn.LSTMCell মেলান
“LSTM vanishing gradient দূর করে” বলে দেয়default-এ ২০ ধাপেই ব্যর্থ হতে দেখলেনLag ধরে gradient মাপুন, কয়েক seed-এ
Citation বা GRU-র সূত্র আত্মবিশ্বাসে লেখেz আর 1 − z-এর জায়গা, r-এর অবস্থান ভিন্ন হতে পারেনিজের version-এর docs পড়ুন, ছোট উদাহরণে মেলান

AI-কে কোড লিখতে দিলে যাচাইয়ের দায়িত্বও প্রম্পটে লিখে দিন। ব্যক্তিগত ডেটা বা key কখনো পেস্ট করবেন না; নিচে ডেটা শুধু বর্ণনা করা হয়েছে।

আমি PyTorch 2.15-এ বাংলা রিভিউয়ের জন্য একটি LSTM classifier লিখছি। batch_first=True, ডানে padding, দৈর্ঘ্য lens-এ আছে। ১. PackedSequence দিয়ে forward লেখো, enforce_sorted=False দিয়ে। ২. প্রতিটি লাইনের পাশে tensor-এর shape লেখো। ৩. forget gate bias বসানোর কোড দাও, আর বলো PyTorch-এ bias-এর সারি-ক্রম কী এবং bias কয়টি। ৪. একটি ছোট assert লেখো, যা h_n[-1] আর pad_packed_sequence-এর আসল শেষ ধাপ মিলিয়ে দেখে। ৫. কোন অংশ তুমি নিশ্চিত নও, স্পষ্ট বলো; অফিশিয়াল docs-এর কোন পাতায় যাচাই করব, তাও বলো।

AI-এর LSTM কোড হাতে পেলে — চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর উত্তর খুলুন।

প্র ০১১৯৯৭ সালে প্রকাশিত LSTM প্রায় দুই দশক পরে কেন sequence-এর প্রধান হাতিয়ার হলো, আর তারপর Transformer কেন তার জায়গা অনেকটা নিল?

LSTM প্রকাশের সময় ধারণাটি ঠিক ছিল, কিন্তু ডেটা আর গণনা ছিল সামান্য। প্রথম দিকের প্রদর্শনগুলো ছিল মূলত কৃত্রিম কাজে, ঠিক এই পর্বের delayed recall-এর মতো। ধীরে ধীরে যোগ হলো forget gate (প্রায় ২০০০), আর হাতের লেখা ও বক্তৃতা চেনায় প্রতিযোগিতামূলক ফল (২০০০-এর দশকের শেষে আর ২০১০-এর শুরুতে, Graves ও সহলেখকদের কাজ উল্লেখযোগ্য)।

২০১৪ সালে Sutskever, Vinyals ও Le-এর sequence-to-sequence আর একই সময়ে Cho ও সহলেখকদের কাজ দেখাল, encoder–decoder দিয়ে অনুবাদের মতো কাজও শেখা যায়। ২০১৪–২০১৫ সালের attention সেই fixed-size bottleneck কমাল। ২০১৬ সালের দিকে বড় অনুবাদ সিস্টেমেও LSTM ব্যবহার হয়েছে বলে প্রকাশিত প্রতিবেদনে পাওয়া যায়।

দুর্বলতাগুলো কাঠামোগত। প্রথমত, ধাপ t-র হিসাবের আগে ধাপ t − 1 শেষ করতে হয়, তাই সময় ধরে সমান্তরাল করা কঠিন, আর GPU-র ক্ষমতা পুরো কাজে লাগে না। দ্বিতীয়ত, এই পর্বের টেবিল যেমন দেখাল, gate থাকলেও অনেক ধাপ দূরের সম্পর্ক শেখা সহজ নয়। ২০১৭ সালের Transformer যেকোনো দুটি অবস্থানের মধ্যে সরাসরি পথ দিল (পর্ব ৩২ ও ৩৩-এ এই ধারণা), আর পুরো sequence একসাথে হিসাব করা গেল।

তবে “LSTM মৃত” বলাও অতিরঞ্জন। ছোট ডেটা, ছোট যন্ত্র, স্ট্রিমিং input, যেখানে প্রতিটি নতুন মানে অবস্থা এক ধাপ এগোলেই চলে, সেখানে এটি এখনো বিবেচনার যোগ্য। Time series-এ আবার gradient boosting প্রায়ই প্রতিযোগিতা করে (মেশিন লার্নিং কোর্সের পর্ব ৫২), আর পর্ব ৩১-এ TCN-এর সাথে একটি তুলনা আছে।

মূল উপলব্ধি: একটি ধারণার সাফল্য তার সঠিকতার পাশাপাশি ডেটা, হার্ডওয়্যার আর সমান্তরাল করার সুবিধার ওপরও নির্ভর করে।

প্র ০২Forget-gate bias এত কাজে এলে PyTorch কেন এটি default করে না? Default না বদলানোর দায় কার?

ঘটনাটি আগে পরিষ্কার করা যাক। Keras-এর LSTM-এ unit_forget_bias=True default, আর তার docs এই সিদ্ধান্তের কারণ হিসেবে Jozefowicz ও সহলেখকদের ২০১৫ সালের কাজ উল্লেখ করে। PyTorch সব parameter একই uniform নিয়মে শুরু করে, যা আমরা source-এ দেখেছি। দুটি জনপ্রিয় library, দুটি ভিন্ন সিদ্ধান্ত।

PyTorch কেন এমন, তার কোনো আনুষ্ঠানিক যুক্তি আমরা যাচাই করতে পারিনি, তাই শুধু সম্ভাব্য বিবেচনাগুলো বলা যায়। পুরোনো ফলের সাথে সামঞ্জস্য একটি: default বদলালে আগের লেখা কোডের আচরণ চুপচাপ বদলে যায়। সরলতা আরেকটি: সব parameter এক নিয়মে শুরু হলে কোড সহজ। আর একটি সংখ্যা সব কাজে ভালো নয়: এই পর্বের টেবিলে bf = 1 ৪০ ধাপে পারেনি, 2 পেরেছে; ছোট delay-তে সব সমান। অনুচ্ছেদ ৪-এর bf = 3-এ দেখা গেছে সংবেদনশীলতা বাড়ছে, যা অন্য কাজে অস্থিরতা আনতে পারে।

দ্বিতীয় প্রশ্নটি প্রকৌশলের। আপনি যখন একটি library-র default ব্যবহার করেন, আপনি তার লেখকের অনুমানগুলো কিনে নিচ্ছেন, প্রায়ই না জেনেই। একই model অন্য framework থেকে পোর্ট করলে ফল বদলাতে পারে, শুধু এই একটি সংখ্যার জন্য। কোনো প্রবন্ধের ফল মিলছে না মনে হলে init আগে মিলিয়ে দেখা উচিত। প্রথম চেষ্টায় আমরা নিজেরাই দুই bias-এর ফাঁদে পড়েছিলাম, যা ধরা পড়ে কোড আবার পড়তে গিয়ে, কোনো error থেকে নয়।

তাই নিজের কাজে দুই-তিনটি মান চেষ্টা করে দেখা, আর শুরুর f মেপে রাখা, খুব অল্প খরচে অনেক ঝামেলা বাঁচায়।

মূল উপলব্ধি: Default কোনো সত্য নয়, একটি সিদ্ধান্ত। যে সংখ্যা ফল বদলাতে পারে, সেটি মেপে দেখুন আর experiment-এর config-এ লিখে রাখুন (পর্ব ১৯)।

প্র ০৩LSTM না GRU: বেছে নেওয়ার সৎ উপায় কী? গবেষণা কী বলে?

গবেষণায় স্পষ্ট বিজয়ী নেই। Chung ও সহলেখকদের ২০১৪ সালের তুলনায় gate-সহ দুটি unit-ই tanh RNN-এর চেয়ে ভালো করেছিল, কিন্তু কে সেরা তা ডেটার ওপর নির্ভর করেছে। Jozefowicz ও সহলেখকদের ২০১৫ সালের বিশাল অনুসন্ধানে দশ হাজারের বেশি কাঠামো পরীক্ষা হয়; সেখানে GRU অনেক কাজে LSTM-কে ছাড়িয়েছে, কিন্তু forget-gate bias দিলে LSTM প্রায় ধরে ফেলেছে। Greff ও সহলেখকদের কাজে LSTM-এর রূপগুলোর মধ্যে বড় উন্নতি দেখা যায়নি, আর hyperparameter-ই বেশি প্রভাব ফেলেছে, বিশেষত learning rate.

এই পর্বের টেবিল সেই কথাই ছোট করে বলে: GRU default-এ LSTM default-কে হারিয়েছে, LSTM bf = 2 GRU-কে। অর্থাৎ তুলনাটি নকশার যতটা, init-এর ততটাই। সঠিক তুলনার আগে দুটিকেই নিজের সেরা অবস্থায় আনতে হবে, আর কয়েকটি seed লাগবে, নইলে ৪০ ধাপের সেই ভাগ্যবান RNN seed-এর মতো ভুল সিদ্ধান্ত হবে।

ব্যবহারিক সিদ্ধান্তে আরও কিছু ওজন আছে। GRU-তে parameter চার ভাগের তিন ভাগ, তাই খুব ছোট ডেটায় বা ছোট যন্ত্রে এটি দিয়ে শুরু করা যুক্তিসঙ্গত, যদিও একই মোট parameter রেখে তুলনা করলে hidden size বাড়াতে হবে। আলাদা output gate-এর কারণে LSTM তথ্য লুকিয়ে রাখতে পারে, তাত্ত্বিকভাবে কিছু কাজে সুবিধা। আর প্রশ্নটির আগে আরেকটি প্রশ্ন আছে: কাজটিতে আদৌ RNN লাগে কি? বাজারঘরের অর্ডার পূর্বাভাসে lag feature-সহ boosting, বা রিভিউয়ে bag, হয়তো যথেষ্ট; আর লম্বা টেক্সটে হয়তো Transformer.

তুলনার সময় training-এর সময় আর memory-ও লিখে রাখুন, কারণ বাস্তব সিদ্ধান্তে শুধু accuracy নয়, খরচও একটি ওজন।

মূল উপলব্ধি: ডেটা ঠিক করে দেয় কোনটি ভালো। নিজের validation set-এ, কয়েক seed-এ, baseline-সহ তুলনা করুন; ব্লগের রায় নয়।

প্র ০৪LSTM-এর cell update, Highway network আর ResNet-এর মধ্যে কি একই চিন্তা কাজ করছে?

হ্যাঁ, আর ইতিহাসেও যোগ আছে। LSTM সময় ধরে: ct = f·ct−1 + i·c̃। ২০১৫ সালে Srivastava, Greff ও Schmidhuber-এর Highway network একই গড়ন গভীরতা ধরে বসায়: y = T(x)·H(x) + (1 − T(x))·x, এখানে T একটি শেখা gate; এটি দেখলে GRU-র z আর 1 − z মনে পড়বে। একই বছর He ও সহলেখকদের ResNet (পর্ব ২২) gate-ই ফেলে দিল: y = x + F(x), যেন f সব সময় ১।

অভিন্ন চিন্তাটি হলো: গুণের শিকলে একটি এমন পথ রাখা, যার derivative প্রায় ১। পর্ব ১৭-এ যেমন দেখলেন, সেখানে I + F′ gradient বাঁচায়। পার্থক্যও জরুরি। ResNet-এর স্তরগুলোর weight আলাদা, আর সংখ্যা নির্দিষ্ট। RNN-এ একই weight প্রতিটি ধাপে, আর দৈর্ঘ্য ভিন্ন হতে পারে; তাই gate-এর সুবিধা আছে, কারণ কোন token-এ মুছবে আর কোনটিতে রাখবে তা input দেখে ঠিক হয়। f = 1 সব সময় হলে c শুধু জমতেই থাকবে, মোছার উপায় থাকবে না, আর এই সমস্যাই forget gate আনার মূল প্রেরণা ছিল। Transformer block-এও একই residual আছে (পর্ব ৩৫)।

একটি সীমাও মনে রাখুন। এই পথ gradient-কে পৌঁছাতে সাহায্য করে, কিন্তু কী শিখতে হবে তা খুঁজে দেয় না। আমাদের run-এ ৮০ delay-তে bf = 2 ব্যর্থ হয়েছে, যদিও f তুলনায় উঁচু। আর সংকেত পৌঁছানোর বিপরীত সমস্যা বৃদ্ধি: bf = 3-এর লেখচিত্র সেটি দেখিয়েছে, তাই gradient clipping-এর জায়গা থাকে।

এই কারণে gate-কে জাদু না ভেবে একটি নকশাগত দর-কষাকষি হিসেবে দেখা ভালো: বেশি parameter আর বেশি হিসাবের বিনিময়ে সংকেতের একটি নিয়ন্ত্রিত পথ পাওয়া যায়, আর সেই পথ কতটা খোলা থাকবে তা ডেটা থেকে শিখতে হয়।

মূল উপলব্ধি: নতুন কাঠামো দেখলে জিজ্ঞেস করুন: gradient কোন পথে ফেরে, আর সেই পথের গুণনীয়ক কত? এই একটি প্রশ্ন অনেক নকশার পেছনের যুক্তি খুলে দেয়।

প্র ০৫একটি LSTM unit কি সত্যিই “না” শব্দের অর্থ বোঝে? Unit-এর আচরণ দেখে আমরা কতটা বলতে পারি?

আমাদের LSTM “কাপড় ভালো না”-কে ০.০০৩ দিয়েছে, তাই লোভ হয় বলতে: সে নেতিবাচকতা বোঝে। কিন্তু মাপা ঘটনা এটুকুই: বানানো ডেটার একটি পরিষ্কার নিয়মে input আর output-এর সম্পর্ক সে শিখেছে। “বোঝা” দাবি করার মতো প্রমাণ এখানে নেই। Vocab-এর বাইরের শব্দ, ভিন্ন বাক্যগঠন (“ভালো তো নয়ই”), বা ব্যঙ্গ (“কী চমৎকার সেলাই, দুই দিনেই ছিঁড়ল”) দিলে ফল সম্ভবত ভেঙে পড়বে, কারণ এই নিয়মগুলো train-এ ছিল না। এটি একটি যাচাইযোগ্য প্রশ্ন, অনুমান নয়।

এই ধরনের অনুসন্ধানের একটি পরিচিত উদাহরণ Karpathy, Johnson ও Fei-Fei-র ২০১৫ সালের “Visualizing and Understanding Recurrent Networks”. সেখানে character-level LSTM-এর কিছু unit উদ্ধৃতি-চিহ্নের ভেতরে-বাইরে বা লাইনের দৈর্ঘ্যের মতো জিনিস অনুসরণ করছে বলে দেখা যায়। কিন্তু একই কাজ এটাও জানায়, বহু unit-এর আচরণের সহজ ব্যাখ্যা মেলে না। একটি unit একটি ধারণার সাথে মিলছে দেখলে সেটি কৌতূহলজনক প্রমাণ, তবে সেই unit সরালে আচরণ বদলায় কি না, তা না দেখলে কার্যকারণ দাবি করা যায় না (পর্ব ২৬-এর sanity check-এর কথা মনে করুন)।

ব্যবহারিকভাবে তাই পরীক্ষাই পথ। বাস্তব model-এ ক্রম বদলে contrastive জোড়া বানান, যেমন এই পর্বের জোড়াগুলো; না-সহ আর না-ছাড়া রিভিউয়ের accuracy আলাদা করে দেখুন, মোট সংখ্যায় থেমে থাকবেন না। মোট accuracy-র ভেতরে bag-এর না-সহ ০.৫৪৮ লুকিয়ে ছিল। AI-এর ভিত্তি কোর্সের পর্ব ৭০-এ বড় language model নিয়েও একই সাবধানতার কথা আছে।

মূল উপলব্ধি: আচরণ মাপুন, উপাধি দেবেন না। “এই ধরনের input-এ এই ফল” এমন বাক্যই যাচাই করা যায়, “সে বোঝে” যায় না।

অনুশীলন

প্রথমে নিজে করুন, তারপর উত্তর মেলান।

  1. হাতে হিসাব: f = 0.8, i = 0.5, c̃ = −0.6, o = 0.5, ct−1 = 1.0 হলে ct আর ht কত? আর LSTM(100, 256)-এর parameter বইয়ের সূত্রে, আর PyTorch-এ কত? PyTorch-এর GRU(100, 256)?

    ct = 0.8 × 1.0 + 0.5 × (−0.6) = 0.5; ht = 0.5 × tanh(0.5) = 0.5 × 0.4621 = 0.2311. প্রতি block-এ 100 × 256 + 256 × 256 = 91,136. বইয়ে এক bias: 4 × (91,136 + 256) = 365,568. PyTorch-এ দুই bias: 4 × (91,136 + 512) = 366,592. GRU: 3 × 91,648 = 274,944. শেষ দুটি numel() দিয়ে চালিয়ে মেলানো হয়েছে।

  2. আউটপুট অনুমান করুন: কোড চালানোর আগে তিনটি shape আর batch_sizes বলুন।
    m = nn.LSTM(10, 20, num_layers=3, batch_first=True, bidirectional=True)
    out, (h, c) = m(torch.zeros(4, 7, 10))
    print(tuple(out.shape), tuple(h.shape), tuple(c.shape))
    p = pack_padded_sequence(torch.zeros(3, 5, 2), torch.tensor([5, 3, 2]),
                             batch_first=True)
    print(p.batch_sizes.tolist(), tuple(p.data.shape))

    সত্যিকারের run-এর আউটপুট:

    (4, 7, 40) (6, 4, 20) (6, 4, 20)
    [3, 3, 2, 1, 1] (10, 2)

    out-এ দুই দিক জোড়া, তাই 40. h-এর প্রথম মাত্রা স্তর × দিক = 3 × 2 = 6, তারপর batch 4, আর প্রতি দিকের আকার 20. প্রথম দুই ধাপে ৩টি sequence, তৃতীয়টিতে ২টি, শেষ দুটিতে ১টি; যোগফল ১০, ঠিক আসল token-এর সংখ্যা।

  3. সমালোচনা করুন: একজন সহকর্মী লিখেছেন: “আমাদের LSTM-এ out[:, -1] নিয়েছি, ডানে padding. সব bias-এ 1 বসিয়েছি, যেন forget-bias হয়। এক seed-এ validation accuracy 0.91, তাই RNN-এর চেয়ে ভালো প্রমাণিত, baseline দরকার নেই।” কী কী বদলাবেন?

    এক, out[:, -1] ছোট রিভিউয়ে pad-এর ওপরের অবস্থা; PackedSequence বা lens − 1 দিয়ে index করুন। দুই, সব bias-এ 1 মানে input, candidate আর output gate-ও বদলাল, আর দুই bias যোগে আসল মান 2. শুধু forget-এর সারি ঠিক করুন, শুরুর f মাপুন। তিন, এক seed-এ কিছু প্রমাণ হয় না; টেবিলে একটি RNN seed-ই ৪০ ধাপে সফল হয়েছিল। অন্তত ৩–৫ seed দিন। চার, bag বা TF-IDF baseline ছাড়া ০.৯১-এর অর্থ নেই: ডেটা ভারসাম্যহীন হলে ০.৯১ আন্দাজের কাছেও হতে পারে (AI-এর ভিত্তি কোর্সের পর্ব ৬৭ দেখুন)।

  4. কোড বদলান: পাতার gate_trace.py-তে 4 আর 8 দ্বিগুণ করুন (f = σ(8 − 16s), i = σ(16s − 8)). t = 6-এ c কত হবে অনুমান করুন, চালান, আর কারণ বলুন।

    আগে t = 6-এ c ছিল ০.৮১; এখন প্রায় ০.৯২০, আর t = 1 থেকে প্রায় নড়েই না। Gate-গুলো ০ আর ১-এর আরও কাছে: f প্রায় ০.৯৯৯৭, i প্রায় ০.০০০৩। মানে স্পষ্ট দরজা চাইলে gate-এর input বড় হতে হয়, আর তাতে sigmoid saturate করে, তখন gate-এর নিজের gradient ছোট হয়ে আসে। ধরে রাখা আর দ্রুত বদলানোর মধ্যে এই টানাপোড়েন gate-এ রয়েই যায়।

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

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

ক্রমএকটি LSTM ধাপের হিসাব সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. পুরোনো h আর নতুন x জোড়া দেওয়া
  2. f, i আর c̃ বের করা
  3. c_t = f·c + i·c̃ দিয়ে cell state বদলানো
  4. output gate o বের করা
  5. h_t = o·tanh(c_t) দিয়ে hidden state বের করা

একটি LSTM cell-এ f = 0.9, i = 0.5, c̃ = 0.4, আর পুরোনো c = 1.0. নতুন c কত?

  • 0.9
  • 1.1
  • 0.85
  • 1.0

বইয়ের সূত্রে (প্রতি block-এ একটি bias vector), input আকার 8 আর hidden আকার 16-এর একটি LSTM স্তরে parameter কতটি?

  • 400
  • 1200
  • 1664
  • 1600

LSTM-এ ct = f·ct−1 + i·c̃ সমীকরণে ct−1 থেকে ct-এর সরাসরি পথের derivative কী, আর তার গুরুত্ব কোথায়?

  • f; এটি ১-এর কাছে শেখা গেলে অনেক ধাপ পরেও gradient থাকে
  • i; নতুন তথ্য যত বেশি, gradient তত বড়
  • tanh′(c); RNN-এর মতোই
  • সব সময় ঠিক 1, তাই LSTM-এ gradient কখনো ছোট হয় না

GRU সম্পর্কে কোন কথাটি সঠিক?

  • GRU-তে তিনটি gate: forget, input, output
  • GRU-তে LSTM-এর মতোই আলাদা cell state আছে
  • GRU একটি gate-এর z আর 1 − z দিয়ে পুরোনো আর নতুন মেশায়
  • একই আকারে GRU-তে LSTM-এর চেয়ে parameter বেশি

তিনটি রিভিউয়ের দৈর্ঘ্য 6, 4 আর 2. Padding সহ batch-এর আকার (3, 6). pack_padded_sequence-এর পরে batch_sizes কী হবে?

  • [6, 4, 2]
  • [3, 3, 2, 2, 1, 1]
  • [3, 3, 3, 3, 3, 3]
  • [1, 1, 2, 2, 3, 3]

PyTorch-এর nn.LSTM-এ একজন bias_ih আর bias_hh, দুটিরই forget-সারিতে 1 বসালেন। শুরুতে forget gate-এর আসল bias প্রায় কত, আর weight ছোট হলে f প্রায় কত?

  • 1, আর f প্রায় 0.73
  • 0, আর f প্রায় 0.5
  • 2, আর f প্রায় 0.88
  • 1 × 1 = 1, আর f ঠিক 1

শব্দকোষ

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

মনে রাখুন

  1. ct = f·c + i·c̃, ht = o·tanh(ct); তিন gate পড়ে পুরোনো h আর নতুন x.
  2. c ধরে সরাসরি পথের derivative f, তাই k ধাপে fk; f শেখা যায়, W-এর শিকল নেই।
  3. PyTorch-এর default-এ bf প্রায় ০; forget-gate bias নিজে বসান, আর দুই bias-এর যোগ মনে রাখুন। সারি-ক্রম i, f, g, o.
  4. GRU: দুই gate, এক অবস্থা, ৩ block; parameter k·nh(nx + nh + 1), PyTorch-এ +2, k = 1, 3, 4.
  5. PackedSequence pad-কে হিসাবের বাইরে রাখে; baseline আর কয়েক seed ছাড়া কোনো ফলই সিদ্ধান্ত নয়।

পরের পর্বে: এতক্ষণ একটি পুরো sequence থেকে একটি উত্তর বেরোচ্ছিল। এবার output-ও একটি sequence: encoder–decoder, teacher forcing, আর একটি fixed-size vector-এ সব ভরলে লম্বা input-এ কী হারায়, তা মেপে দেখা।

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

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