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

Sequence ডেটা — সময়, টেক্সট আর padding ও masking

Sequence Data — Windows, Time Splits, Padding, Masks, Embeddings and PackedSequence

Taught in Bangla, this lesson prepares sequence data for neural networks. It shapes everything as (B, T, F), builds sliding windows from a daily series and shows how a random split leaks neighbouring days compared with a time split. In-page NumPy shows padding and masked averages, nn.Embedding as a lookup table whose gradient touches only used rows, and a bag-of-embeddings baseline blind to word order. Real desktop runs cover PackedSequence and the silent batch_first bug.

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

  • Time series আর টেক্সটকে (B, T, F) tensor-এ রাখা, আর batch_first-এর আসল অর্থ
  • Series থেকে window বানানো, আকার হিসাব করা, আর সময় ধরে ভাগ করা
  • Padding, mask, masked pooling আর “শেষ আসল ধাপ” বের করা
  • nn.Embedding, padding_idx, আর gradient কোন সারিতে পড়ে
  • Last value আর bag-of-embeddings baseline, এবং pack_padded_sequence-এর ভেতরের হিসাব

পর্ব ১১ (Dataset আর DataLoader) এ আপনি collate_fn-এ padding আর attention mask বানিয়েছেন। পর্ব ২০ থেকে মডিউল ৪ পর্যন্ত ডেটা ছিল ছবি, যার আকার সাধারণত ঠিক করা। এবার ডেটা এমন, যার সবচেয়ে বড় তথ্য ক্রমে লুকানো। এই পর্বে এখনো কোনো recurrent model শেখাব না; কোষের ভেতরটা আসবে পর্ব ২৮-এ (Recurrent neural network — hidden state আর backpropagation through time). আগে ডেটাকে ঠিক আকারে আনা আর ফাঁদগুলো চেনা দরকার, কারণ এখানকার ভুল প্রায়ই কোনো error দেয় না।

১ · Sequence কেন আলাদা — ক্রম, দৈর্ঘ্য আর নির্ভরতা

ক্রিকেটের স্কোরকার্ড ভাবুন। “৬, ৪, ১, ০, ০, W” আর “W, ০, ০, ১, ৪, ৬”: রান একই, উইকেটও একই, কিন্তু খেলার গল্প আলাদা। আবার একটি টি-টোয়েন্টি ম্যাচ ২০ ওভারের আগেও শেষ হতে পারে, তাই দৈর্ঘ্য সব সময় এক নয়। আর আগের বলের চাপ পরের বলে পড়ে, তাই বলগুলো একে অপর থেকে স্বাধীন নয়।

এই তিনটি বৈশিষ্ট্যই sequence-কে একটি সাধারণ টেবিলের সারি থেকে আলাদা করে:

বৈশিষ্ট্যউদাহরণযে ভুল হয়
ক্রমের অর্থ আছে“ভালো না” বনাম “না ভালো”গড় বা গণনায় ক্রম হারানো
দৈর্ঘ্য ভিন্ন২ শব্দের রিভিউ, ৪০ শব্দের রিভিউpad-কে আসল ডেটা ধরা
মানগুলো নির্ভরশীলআজকের অর্ডার গতকালের কাছাকাছিএলোমেলো ভাগে leak

এই কোর্সের প্রথম দিকের network ধরে নিত প্রতিটি নমুনা স্বাধীন আর প্রতিটি নমুনার আকার সমান। Sequence-এ দুটিই ভাঙে। Time series নিয়ে pandas-এর কাজ AI-এর ভিত্তি কোর্সের পর্ব ২৬-এ, Markov ধারণা পর্ব ৫৩-এ, আর ARIMA-র মতো ক্লাসিক পদ্ধতি মেশিন লার্নিং কোর্সের পর্ব ৫১-এ আছে; সেগুলো আবার শেখাব না।

২ · Tensor-এ রূপ — (B, T, F)

Sequence-এর সাধারণ আকার তিন মাত্রার: B = batch-এ কয়টি sequence, T = কয়টি সময়-ধাপ, F = প্রতি ধাপে কয়টি সংখ্যা। Time series-এ সংখ্যাগুলো আগেই আছে। টেক্সটে আগে শব্দ বা token-কে id বানাতে হয় (B, T), তারপর embedding দিয়ে (B, T, D).

একই ছাঁচ, দুই ধরনের ডেটা

Time series and text both become (Batch, Time, Features)

Time series and text as batch, time, feature tensors Time series: দৈনিক অর্ডার 98109118115 0010 অর্ডার ছুটির দিন? কলাম = সময়, সারি = feature ৩২টি window: (32, 7, 2) টেক্সট: বাংলা রিভিউ 123 id: (32, 40) E[ids], টেবিল থেকে E₁E₂E₃ প্রতিটিতে D সংখ্যা D = 128: (32, 40, 128)
ছবি ২৭.১ — ডেটার উৎস আলাদা হলেও RNN-কে দেওয়ার আগে দুটিই একই ছাঁচে আসে।

এখানে একটি নিয়ম মনে রাখুন: nn.RNN, nn.LSTM আর nn.GRU default-এ (T, B, F) চায়; batch_first=True দিলে (B, T, F). আবার nn.Conv1d চায় (B, F, T), যা পর্ব ৩১-এ (Sequence-এর জন্য 1D convolution আর time series-এ deep learning) আসবে। পর্ব ০৭-এর shape-অভ্যাস এখানে সবচেয়ে কাজে লাগে।

ডেটাআকারপ্রতি ধাপে কী থাকে
এক series, ৭ দিনের window(32, 7, 1)শুধু অর্ডারের সংখ্যা
সঙ্গে ছুটি আর দাম(32, 7, 3)অর্ডার, ছুটির দিন কি না, দাম
শব্দের id(32, 40)একটি পূর্ণসংখ্যা, এখনো অর্থহীন নম্বর
শব্দের embedding(32, 40, 128)১২৮টি শেখা সংখ্যা
অক্ষরের id(32, 200)একটি অক্ষর; T অনেক বড়, vocabulary ছোট

একটি দরকারি পার্থক্য: time series-এর সংখ্যার মান আর দূরত্বের অর্থ আছে (১২০ অর্ডার ১১০-এর কাছাকাছি)। শব্দের id-এর তা নেই: id ৭ আর id ৮ পাশাপাশি হলেও শব্দ দুটির কোনো মিল নাও থাকতে পারে। তাই id-কে সরাসরি সংখ্যা হিসেবে network-এ দেওয়া ভুল। এজন্যই টেক্সটে একটি বাড়তি ধাপ লাগে, যা ৫ নম্বর অংশে দেখবেন। আর time series-এ F-এর সংখ্যাগুলোর মাপ এক রকম না হলে আগে scale করে নিন: পর্ব ১৪-এর কারণগুলো এখানেও খাটে।

৩ · Series থেকে window — নমুনা বানানো আর leak

Model চায় “ইনপুট → উত্তর” জোড়া, কিন্তু time series একটিই লম্বা সারি। তাই সারির ওপর একটি জানালা সরানো হয়: আগের L দিন ইনপুট, পরের দিনটি উত্তর। ধারণাটি মেশিন লার্নিং কোর্সের পর্ব ৫২-এর lag feature-এর মতোই; সেখানে lag-গুলো টেবিলের কলাম, আর এখানে সেগুলো সময়-অক্ষে সাজানো। Backtest-এর পুরো পদ্ধতিও সেখানেই আছে।

বাজারঘরের দৈনিক অর্ডার, seed 27
দেখা অংশ
X
y
হিসাব
  1. Series. ২০০ দিনের একটি সারি, আকার (200,). এখনো কোনো “প্রশ্ন-উত্তর” নেই।
  2. ইনপুট. প্রথম ৭ দিন (দিন ০ থেকে ৬) হলো X[0]. বন্ধনীতে যা আছে, model সেটুকুই দেখে।
  3. উত্তর. পরের দিন, অর্থাৎ দিন ৭-এর অর্ডার ১১১ হলো y[0]. গোল বন্ধনী মানে উত্তর।
  4. এক ঘর সরা. X[1] দিন ১ থেকে ৭. খেয়াল করুন: আগের উত্তর ১১১ এখন ইনপুটের ভেতরে। পাশের দুটি window-এর ৬টি মান এক। Leak-এর বীজ এখানেই।
  5. মোট. নমুনা T − L = ১৯৩টি, X (193, 7), y (193,). RNN-এর জন্য F = ১ যোগ করলে (193, 7, 1). পরের H দিনের পূর্বাভাস চাইলে নমুনা T − L − H + 1.

এবার সত্যিকারের কোড। sliding_window_view কোনো loop ছাড়াই window দেয়। তারপর একই ডেটা দুইভাবে ভাগ করে একটি মুখস্থ-করা model চালানো হয়: ১-nearest neighbour (AI-এর ভিত্তি কোর্সের পর্ব ৫৭), যা সবচেয়ে কাছের train window-এর উত্তর ফেরত দেয়। সঙ্গে থাকে দুটি শেখা-ছাড়া baseline: last value (গতকাল যা ছিল) আর seasonal naive (গত সপ্তাহের একই বারে যা ছিল)।

import numpy as np, matplotlib.pyplot as plt

rng = np.random.default_rng(27)
T, L = 200, 7                                   # ২০০ দিন, ৭ দিনের window
level = 100 + 0.5 * np.arange(T) + np.cumsum(rng.normal(0, 2, T))  # বাড়তে থাকা স্তর
week = 15 * np.sin(2 * np.pi * np.arange(T) / 7)
orders = np.round(level + week + rng.normal(0, 4, T))

W = np.lib.stride_tricks.sliding_window_view(orders, L + 1)
X, y = W[:, :L], W[:, L]
print("orders", orders.shape, "| W", W.shape, "| X", X.shape, "| y", y.shape)
print("X[0] =", X[0].astype(int), "→ y[0] =", int(y[0]))

def nn1(tr, te):                                # মুখস্থ-করা model: 1-nearest neighbour
    d = ((X[te][:, None] - X[tr][None]) ** 2).sum(-1)
    return y[tr][d.argmin(1)]
def mae(p, q): return round(float(np.abs(p - q).mean()), 2)

n, cut = len(X), int(0.8 * len(X))
perm = rng.permutation(n)
splits = {"random": (perm[:cut], perm[cut:]),
          "time": (np.arange(cut), np.arange(cut, n))}
for name, (tr, te) in splits.items():
    seen = [np.any((tr > i) & (tr <= i + L)) for i in te]   # test-এর y কি train-এর X-এ?
    print(f"{name:6s} 1-NN MAE {mae(nn1(tr, te), y[te]):6.2f} | "
          f"test y inside train X: {np.mean(seen):.0%}")
te = splits["time"][1]
print("time   last value MAE", mae(X[te, -1], y[te]),
      "| same weekday MAE", mae(X[te, 0], y[te]))

fig, ax = plt.subplots(figsize=(7, 2.8))
ax.plot(orders, lw=1, label="daily orders")
ax.axvspan(0, L - 1, alpha=.25, label="X[0]: 7 days"); ax.scatter(L, y[0], c="r", label="y[0]")
ax.axvline(cut + L, ls="--", c="k", label="time split")
ax.set_xlabel("day"); ax.set_ylabel("orders"); ax.legend(fontsize=7, ncol=2)

আউটপুট বলে X (193, 7), y (193,). Random split-এ MAE 8.31, আর test-এর 100% উত্তর কোনো train window-এর ইনপুটে আগেই ছিল। সময় ধরে ভাগে একই model-এর MAE 20.87, ওভারল্যাপ 0%. কেন এত ফারাক? Random split-এ সবচেয়ে কাছের window প্রায়ই সময়েও পাশের দিন, যা ভবিষ্যৎ থেকেও আসতে পারে। সময় ধরে ভাগে অর্ডারের স্তর train-এর সীমা ছাড়িয়ে যায়, আর ১-NN পুরোনো মানের বাইরে যেতে পারে না।

সবচেয়ে শিক্ষণীয় সংখ্যা 7.82: seasonal naive কিছু না শিখেই ১-NN-কে হারিয়েছে, আর last value (9.79)-ও। আরও তিনটি seed (৪২, ৭, ১১)-এ চালিয়ে দেখেছি: random split-এর MAE প্রতিবার কম (৫.৯৭ বনাম ১৫.৫৪, ৫.২৩ বনাম ৭.০৩, ৬.৭২ বনাম ২২.৮২), তবে ফারাকের পরিমাণ অনেক বদলায়। এটি বানানো ডেটায় একটি local পরীক্ষা, সাধারণ সূত্র নয়।

নিয়ম: আগে সময়ের দাগ, তারপর দুই পাশে আলাদা করে window. পরের H দিনের পূর্বাভাসে train-এর শেষ উত্তর যেন test-এর প্রথম উত্তরের আগে শেষ হয়। Scaling-এর গড়ও শুধু train অংশ থেকে নিন। Temporal leakage-এর সাধারণ আলোচনা AI-এর ভিত্তি কোর্সের পর্ব ৫৬-এ আছে।

বাস্তব ডেটায় আরও তিনটি প্রশ্ন

  • দিন বাদ পড়লে? Window ধরে নেয় পাশের দুই ঘরের ফারাক এক দিন। হরতাল বা সার্ভার বন্ধের দিনে সারি না থাকলে ৭ ঘরে আসলে ৯ দিন ঢুকে যায়। তাই আগে তারিখের পূর্ণ সূচি বানিয়ে ফাঁক চিহ্নিত করুন। “বিক্রি ০” আর “তথ্য নেই” এক কথা নয়; দ্বিতীয়টির জন্য একটি আলাদা feature রাখা যায়। AI-এর ভিত্তি কোর্সের পর্ব ২৬-এর resample এখানে কাজে লাগে।
  • অনেক জেলা? প্রতিটি জেলার series আলাদা করে window করুন, তারপর একসঙ্গে জুড়ুন। পুরো টেবিল একটানা জুড়ে window করলে একটি window ঢাকার শেষ দিন আর চট্টগ্রামের প্রথম দিন মিশিয়ে ফেলবে, আর কোনো error আসবে না। ৮টি জেলা আর প্রতিটিতে ১৯৩টি window মানে (1544, 7, F).
  • নতুন পণ্যের ইতিহাস ছোট? মাত্র ৩ দিনের পুরোনো পণ্যের ৭ দিনের window নেই। এখানেও padding আর mask লাগে, সাধারণত বাম পাশে, যাতে শেষ ধাপ আজকের দিনই থাকে। অর্থাৎ পরের অংশের কৌশল শুধু টেক্সটের নয়।

Memory-র হিসাব: sliding_window_view ডেটা কপি করে না, একই সারির ওপর একটি দৃশ্য দেয়। ১০ লাখ দিনের series-এ L = ১০০-এর window কপি করলে প্রায় ১০ কোটি সংখ্যা লাগত। PyTorch-এর Dataset-এ তাই সাধারণত পুরো window জমিয়ে না রেখে __getitem__-এর ভেতরে কেটে দেওয়া হয়। Dataset-এর গঠন পর্ব ১১-এ আছে।

৪ · Padding আর mask — এবার model-এর চোখে

পর্ব ১১-এ আয়তাকার batch বানানো পর্যন্ত দেখেছিলেন। এবার প্রশ্ন: সেই batch ঢোকার পরে model কোথায় ভুল করতে পারে? তিনটি জায়গায়: গড় বা pooling-এ (masked pooling), “শেষ ধাপ” তোলায়, আর loss-এ। Loss-এর ignore_index আছে পর্ব ১৩-এ; বাকি দুটি এখানে। আগে তিনটি রিভিউ ধাপে ধাপে দেখুন।

“ডেলিভারি ভালো না” · “দাম বেশি” · “রং সুন্দর আবার কিনব”
ids
mask
আকার
ফল
  1. Id. Vocabulary থেকে প্রতিটি শব্দের id. তিনটি তালিকার দৈর্ঘ্য ৩, ২ আর ৪.
  2. লম্বা কত? সবচেয়ে লম্বা রিভিউ ৪ শব্দের, তাই T = ৪. Vocabulary-তে id ০ রাখা আছে <pad>-এর জন্য।
  3. Padding. ডান পাশে ০ বসল। এখন batch একটি (3, 4) tensor, যার ১২টি ঘরের ৩টি আসল নয়।
  4. Mask. j < length হলে ১, না হলে ০. আকার ids-এর সমান। ০.২৫ অংশ pad, অর্থাৎ এক-চতুর্থাংশ ঘর হিসাবের বাইরে রাখতে হবে।
  5. Masked mean. “দাম বেশি”-র সাধারণ গড় আসল গড়ের ঠিক অর্ধেক, কারণ দুটি শূন্য vector ভাগফলে ঢুকেছে। Mask দিলে ভাগ হয় ২ দিয়ে, ৪ দিয়ে নয়। ফল (3, 2): প্রতিটি রিভিউয়ের একটি vector.
import numpy as np
np.set_printoptions(precision=2, suppress=True)

vocab = {w: i for i, w in enumerate(
    "<pad> ডেলিভারি ভালো না দাম বেশি রং সুন্দর আবার কিনব".split())}
reviews = ["ডেলিভারি ভালো না", "দাম বেশি", "রং সুন্দর আবার কিনব"]
seqs = [[vocab[w] for w in r.split()] for r in reviews]

def pad(seqs, T=None):
    lens = np.array([len(s) for s in seqs])
    T = T or int(lens.max())
    ids = np.zeros((len(seqs), T), dtype=np.int64)
    for i, s in enumerate(seqs):
        ids[i, :len(s)] = s
    mask = np.arange(T)[None, :] < lens[:, None]
    return ids, mask, lens

E = np.random.default_rng(27).normal(0, 1, (len(vocab), 2))
E[0] = 0                                        # <pad>-এর vector শূন্য
def masked_mean(x, m):
    return (x * m[..., None]).sum(1) / m.sum(1, keepdims=True)

ids, mask, lens = pad(seqs)
print("ids", ids.shape, "\n", ids)
print("mask\n", mask.astype(int), "| pad share", round(1 - mask.mean(), 2))
x = E[ids]
print("x", x.shape, "→ pooled", masked_mean(x, mask).shape)

ids6, mask6, _ = pad(seqs, T=6)                 # একই রিভিউ, লম্বা batch-এ
print("দাম বেশি, plain mean  T=4:", x[1].mean(0), " T=6:", E[ids6][1].mean(0))
print("দাম বেশি, masked mean T=4:", masked_mean(x, mask)[1],
      " T=6:", masked_mean(E[ids6], mask6)[1])
print("x[:, -1] row 1:", x[1, -1], "| last real:", x[1, lens[1] - 1])

একই “দাম বেশি”-র সাধারণ গড় T = ৪-এ [-0.23 -0.62], T = ৬-এ [-0.15 -0.41]. অর্থাৎ একটি রিভিউয়ের vector নির্ভর করছে batch-এর সবচেয়ে লম্বা রিভিউয়ের ওপর! Shuffle হলে একই রিভিউ প্রতি epoch-এ আলাদা হবে। Masked mean দুই জায়গাতেই [-0.45 -1.24]. শেষ লাইনে দ্বিতীয় ফাঁদ: x[:, -1] এই রিভিউয়ের জন্য শুধু pad-এর শূন্য vector তোলে; আসল শেষ শব্দ x[1, lens[1] - 1]-এ.

কোন পাশে pad? ডান পাশে pad করলে শুরু মেলে, আর এটিই প্রচলিত ধারা। বাম পাশে pad করলে শেষ মেলে, তখন x[:, -1] ঠিক কাজ করে। পরে LLM দিয়ে একসঙ্গে অনেক prompt চালানোর সময় বাম-padding-এর দরকার দেখবেন। যে পাশেই হোক, mask আর lengths যেন একই নিয়ম মানে।

প্রতি ধাপে উত্তর থাকলে loss-এও mask. ধরুন একটি model প্রতিটি শব্দে একটি label দেয়, আর batch-এ দুটি sequence: দৈর্ঘ্য ৩ ও ১, T = ৩. আসল ধাপের loss ০.৬, ০.৪, ০.২ আর ০.৮; pad-এর দুই ঘরে model-এর যা আসে, ধরুন ২.০ করে। সব ঘরের গড় = (০.৬ + ০.৪ + ০.২ + ০.৮ + ২.০ + ২.০) / ৬ = ১.০. আসল গড় = ২.০ / ৪ = ০.৫. দ্বিগুণ ভুল সংখ্যা, আর তার গণ্ডগোল gradient-এও যায়, যদি pad-এর ঘরের output কোনো parameter-এর ওপর নির্ভর করে। PyTorch-এ সহজ পথ হলো pad-এর label-এ −১০০ বসিয়ে ignore_index=-100 (পর্ব ১৩), অথবা নিজে (loss * mask).sum() / mask.sum(). আবার সাবধান: mask.sum() দিয়ে ভাগ, batch-এর আকার দিয়ে নয়।

৫ · One-hot থেকে nn.Embedding — একটি শেখার যোগ্য টেবিল

চায়ের দোকানের বাকির খাতায় প্রতিটি ক্রেতার একটি পাতা। নাম ধরে পাতাটি খোলা হয়, আর সেই পাতাতেই হিসাব বদলায়; যিনি আজ আসেননি তাঁর পাতায় কলম পড়ে না। Embedding টেবিলও তেমন: id ধরে সারিটি তোলা হয়, আর শুধু সেই সারির সংখ্যা বদলায়।

One-hot vector-এ V মাপের একটি সারিতে একটি ঘর ১, বাকি সব ০. এর সঙ্গে V × D মাপের matrix E গুণ করলে E-এর সেই সারিটিই বের হয়। তাই গুণটি আসলে index দিয়ে তোলা: E[ids]. এই সাশ্রয় বিশাল: ৫০,০০০ শব্দের vocabulary আর (32, 100) batch-এ one-hot লাগে ১৬ কোটি সংখ্যা, id লাগে ৩,২০০টি। অর্থের দিক দিয়ে embedding কী ধরে, তার পরিচয় AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ আছে, আর word2vec-এর গভীরতা আসবে পর্ব ৪১-এ (Word embedding — word2vec, GloVe আর fastText). এখানে আমরা শুধু যন্ত্রটি দেখছি।

import numpy as np
np.set_printoptions(precision=3, suppress=True)

V, D = 8, 3                                     # ৮টি শব্দ, ৩ মাত্রার vector
rng = np.random.default_rng(27)
E, w = rng.normal(0, .5, (V, D)), rng.normal(0, 1, D)
ids = np.array([2, 5, 5, 7])                    # শব্দ ৫ দুইবার এসেছে

one_hot = np.eye(V)[ids]
print("one_hot", one_hot.shape, "@ E", E.shape, "→", (one_hot @ E).shape,
      "| same as E[ids]:", np.allclose(one_hot @ E, E[ids]))

def loss(E):                                    # mean → score → squared error
    return 0.5 * (E[ids].mean(0) @ w - 1.0) ** 2

err = E[ids].mean(0) @ w - 1.0
G = np.zeros_like(E)
np.add.at(G, ids, err * w / len(ids))           # হাতের gradient
print("|grad| per row:", np.linalg.norm(G, axis=1))

eps = 1e-6                                      # সংখ্যায় যাচাই
Ep, Em = E.copy(), E.copy()
Ep[5, 0] += eps; Em[5, 0] -= eps
print("row 5: hand", round(G[5, 0], 6),
      "numeric", round((loss(Ep) - loss(Em)) / (2 * eps), 6))

bad = np.zeros_like(E)
bad[ids] += err * w / len(ids)                  # ফাঁদ: দুইবারের শব্দ একবারই গোনা
print("row 5 with G[ids] +=:", bad[5], "| np.add.at:", G[5])

E2 = E - 0.1 * G                                # এক ধাপ update
print("rows changed:", np.flatnonzero((E2 != E).any(1)))

গণিতটি ছোট। h = (১/n) ∑ E[idsi] গড়, score = h · w, loss = ½(score − ১)২. তাই সারি k-এর gradient = (score − ১) · w · ck/n, যেখানে ck মানে শব্দ k কতবার এসেছে। ফলাফলও তাই বলে: |grad| শুধু সারি ২, ৫, ৭-এ অশূন্য, আর ৫-এ ঠিক দ্বিগুণ (0.86 বনাম 0.43). হাতের আর সংখ্যার মান দুটিই -0.130739. একটি NumPy-ফাঁদও দেখুন: bad[ids] += … একই index দুইবার থাকলে একবারই যোগ করে (-0.065); np.add.at দুইবার করে (-0.131). পিছনের গণিত পর্ব ০৪-এর chain rule.

PyTorch-এ এই টেবিল nn.Embedding. সত্যিকারের run (ডেস্কটপে, CPU-তে; লেখার সময়ে, নভেম্বর ২০২৬: Python 3.14.4, torch 2.15.0):

import torch, torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

print("torch", torch.__version__)
torch.manual_seed(27)

# তিনটি রিভিউ: id 0 = <pad>
ids = torch.tensor([[4, 7, 0, 0],      # দুই শব্দ
                    [4, 7, 9, 2],      # চার শব্দ
                    [5, 3, 8, 0]])     # তিন শব্দ
lengths = torch.tensor([2, 4, 3])

emb = nn.Embedding(num_embeddings=10, embedding_dim=3, padding_idx=0)
print("weight", tuple(emb.weight.shape), "| params", emb.weight.numel())
print("row 0 (pad):", emb.weight[0].tolist())
x = emb(ids)
print("ids", tuple(ids.shape), "dtype", ids.dtype, "-> x", tuple(x.shape), x.dtype)

x.sum().backward()
g = emb.weight.grad
print("grad rows that are non-zero:", (g.abs().sum(1) > 0).nonzero().flatten().tolist())
print("grad row 0:", g[0].tolist(), "| grad row 4:", g[4].tolist())
emb.zero_grad()
~/bazarghar-dl$ uv run python seq_shapes.py
torch 2.15.0+cpu
weight (10, 3) | params 30
row 0 (pad): [0.0, 0.0, 0.0]
ids (3, 4) dtype torch.int64 -> x (3, 4, 3) torch.float32
grad rows that are non-zero: [2, 3, 4, 5, 7, 8, 9]
grad row 0: [0.0, 0.0, 0.0] | grad row 4: [2.0, 2.0, 2.0]
…

তিনটি জিনিস লক্ষ্য করুন। padding_idx-এর সারি শূন্য দিয়ে শুরু হয়, আর batch-এ pad ৩ বার এলেও তার gradient শূন্য, তাই সারিটি train-এ বদলায় না। সারি ৪-এর gradient ২, কারণ id ৪ দুইবার এসেছে। আর ইনপুট int64 হতে হবে: float id দিলে nn.Embedding error দেয়।

আরও গভীরে: Default-এ gradient একটি পূর্ণ V × D tensor, যার বেশিরভাগ ঘর শূন্য। sparse=True দিলে শুধু ব্যবহৃত সারি থাকে, কিন্তু কেবল কয়েকটি optimizer তা সামলায় (ডকুমেন্টেশনে SGD, SparseAdam, Adagrad-এর উল্লেখ আছে; আপনার version-এ দেখে নিন)। একটি সূক্ষ্ম কথা: gradient শূন্য হলেও momentum বা AdamW-এর weight decay এই batch-এ না আসা সারিও সরাতে পারে, যেমন বিরল শব্দের সারি, যা আগের ধাপে gradient পেয়েছিল। padding_idx-এর সারি শুরু থেকে শূন্য আর gradient-ও সব সময় শূন্য, তাই সাধারণত শূন্যই থাকে; তবে নিজে শূন্য-নয় এমন মান বসালে weight decay তাকেও ছোট করবে। Optimizer-এর হিসাব আছে পর্ব ১৫-এ।

৬ · Bag-of-embeddings — ভালো baseline, কিন্তু ক্রমে অন্ধ

Bag-of-embeddings সবচেয়ে সরল text model: রিভিউয়ের সব শব্দের vector-এর masked mean, তার ওপর একটি linear স্তর। এটি AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এর bag-of-words-এর কাছের আত্মীয়, শুধু শব্দের জন্য গণনার বদলে শেখা vector. শেখা-ছাড়া last value-র মতো এটিও একটি মাপকাঠি: নতুন জটিল model এর চেয়ে ভালো না করলে সেই জটিলতার দাম নেই। কিন্তু একটি জন্মগত অন্ধত্ব আছে।

ধরি E[ভালো] = [1.0, 0.5], E[না] = [−0.8, 0.3], w = [2, −1], b = 0
“ভালো না”
“না ভালো”
ফল
  1. দুটি বাক্য. একই দুটি শব্দ, উল্টো ক্রম। প্রথমটি নেতিবাচক; দ্বিতীয়টি “না, ভালোই” অর্থে ইতিবাচক ধরা হলো।
  2. Lookup. এই ধাপে ক্রম আছে: (2, 2) আকারের দুটি ভিন্ন matrix, সারি দুটি উল্টো।
  3. গড়. (1.0 − 0.8)/২ = ০.১ আর (0.5 + 0.3)/২ = ০.৪. যোগের ক্রম বদলালে যোগফল বদলায় না, তাই দুটি vector হুবহু এক। তথ্যটি এখানেই চিরতরে গেল।
  4. Score. ২ × ০.১ + (−১) × ০.৪ = −০.২, p = ০.৪৫০. ইনপুট এক হলে কোনো weight দিয়েই দুটি আলাদা উত্তর সম্ভব নয়। আরও বেশি train বা বড় D-তে লাভ নেই।
  5. সমাধানের দিক. ক্রম রাখলে ইনপুট আলাদা হয়, যেমন পাশাপাশি জোড়া দিলে। Bigram, 1D convolution, RNN: সবাই কোনো না কোনোভাবে এই তথ্যটুকু রাখে।

প্রমাণটি এবার ডেটায়। ৮টি পণ্য-শব্দের সঙ্গে সহজ রিভিউ (“সেবা সুন্দর”, “সেবা নষ্ট”) আর ক্রম-নির্ভর রিভিউ (“সেবা ভালো না”, “সেবা না ভালো”) বানানো হলো। শেষ দুটি পণ্য-শব্দ শুধু test-এ। Model NumPy-তে হাতে লেখা। দ্বিতীয় চালানোতে একই কোডে শুধু token-এর তালিকায় পাশাপাশি শব্দজোড়া যোগ হয়। প্রথমবার চালাতে কয়েক সেকেন্ড লাগতে পারে।

import numpy as np

S = "ডেলিভারি প্যাকেট জুতা ব্যাগ ফোন রং সেবা সাবান".split()
EASY = [("সুন্দর", 1), ("চমৎকার", 1), ("ভালো", 1), ("নষ্ট", 0), ("বাজে", 0), ("খারাপ", 0)]
ORDER = [("ভালো না", 0), ("না ভালো", 1), ("খারাপ না", 1), ("না খারাপ", 0),
         ("খারাপ না ভালো", 1), ("ভালো না খারাপ", 0)]
def make(subj, T): return [(f"{s} {t}", y) for s in subj for t, y in T]
train = make(S[:6], EASY + ORDER)               # শেষ দুটি পণ্য-শব্দ test-এর জন্য

def with_pairs(r):                              # শব্দ + পাশাপাশি শব্দজোড়া
    w = r.split(); return w + [a + "_" + b for a, b in zip(w, w[1:])]

def fit(tok, steps=4000, lr=1.0, D=8):
    vocab = {}
    for r, _ in train:
        for t in tok(r): vocab.setdefault(t, len(vocab))
    def bag(data):                              # C: প্রতিটি শব্দের ভাগ; ক্রম এখানেই হারায়
        C = np.zeros((len(data), len(vocab)))
        for i, (r, _) in enumerate(data):
            ts = [t for t in tok(r) if t in vocab]
            for t in ts: C[i, vocab[t]] += 1 / len(ts)
        return C, np.array([y for _, y in data])
    rng = np.random.default_rng(27)
    E, w, b = rng.normal(0, .3, (len(vocab), D)), rng.normal(0, .3, D), 0.0
    C, y = bag(train); n = len(y)
    prob = lambda C: 1 / (1 + np.exp(-(C @ E @ w + b)))
    for _ in range(steps):
        e = prob(C) - y; h = C @ E
        E -= lr * C.T @ np.outer(e, w) / n; w -= lr * h.T @ e / n; b -= lr * e.mean()
    acc = lambda T: ((prob(bag(make(S[6:], T))[0]) > .5) == bag(make(S[6:], T))[1]).mean()
    p = lambda r: round(float(prob(bag([(r, 0)])[0])[0]), 3)
    return acc(EASY), acc(ORDER), p("সেবা ভালো না"), p("সেবা না ভালো"), len(vocab)

for name, tok in [("words only", str.split), ("words + pairs", with_pairs)]:
    print(name, "easy %.2f | order %.2f | ভালো না %.3f | না ভালো %.3f | vocab %d" % fit(tok))

শুধু শব্দে সহজ test ঠিক থাকে (easy 1.00), কিন্তু ক্রম-নির্ভর test ঠিক 0.50. কারণটি পরিসংখ্যানের নয়, গঠনের: প্রতিটি জোড়ার ইনপুট এক, তাই একটি ঠিক হলে অন্যটি ভুল। দুই রিভিউয়ের সম্ভাবনাও এক (0.758). শব্দজোড়া যোগ করলে order 1.00, কিন্তু vocabulary ১৩ থেকে ৫৯ হয়। মনে রাখুন, এই ডেটায় নেতিবাচক শব্দের দূরত্ব সব সময় এক ঘর আর ধরন মাত্র ছয়টি। “ভালো তো মোটেও বলা যাবে না”-র মতো দূরের নির্ভরতায় জোড়া কাজে আসে না। সেই তুলনাটি পর্ব ২৯-এ (LSTM আর GRU — gate দিয়ে দীর্ঘ স্মৃতি) আসবে।

PyTorch-এ এই গড়ের তৈরি রূপ nn.EmbeddingBag. সত্যিকারের run-এ দেখা গেছে mode="mean" আর padding_idx=0-তে এটি হাতের masked mean-এর সমান, আর [7, 9] ও [9, 7] ইনপুটে একই vector দেয়:

EmbeddingBag (3, 3) | equals masked mean: True
order swapped, same bag vector: True
মূল ধারণা

Baseline-এর কাজ জেতা নয়, মাপকাঠি ঠিক করা। Time series-এ last value আর seasonal naive; টেক্সটে সবচেয়ে ঘন শ্রেণি, TF-IDF-এর ওপর linear model আর bag-of-embeddings. এগুলো ছাড়া “৮৫% accuracy” বা “MAE ৩” ভালো না খারাপ, বলার উপায় নেই। আর সামগ্রিক সংখ্যার পাশে সেই ধরনের ছোট পরীক্ষার সেট রাখুন, যেখানে আপনার নতুন model-এর আসল এগিয়ে থাকার কথা। এই পর্বের ক্রম-নির্ভর জোড়া তেমনই একটি সেট।

৭ · PackedSequence — pad-এর ওপর হিসাব না করা

পর্ব ২৮-এর RNN ধাপে ধাপে চলে। Padding দেওয়া batch সরাসরি দিলে ছোট রিভিউ শেষ হওয়ার পরও সে pad-এর ওপর চলতে থাকে, আর শেষ hidden state নষ্ট হয়। PackedSequence সমাধান করে এভাবে: সব sequence-এর আসল ধাপগুলো সময় ধরে একটি লম্বা সারিতে রাখে, সঙ্গে রাখে প্রতি ধাপে কয়টি sequence বেঁচে আছে।

ids: সারি ০ [4 7 0 0] · সারি ১ [4 7 9 2] · সারি ২ [5 3 8 0]
ধাপ
সাজানো সারি
এই ধাপের id
data
batch_sizes
  1. লম্বা আগে. sorted_indices = [1, 2, 0]: ৪, ৩, ২ দৈর্ঘ্যের ক্রম। ফেরার পথের জন্য unsorted_indices = [2, 0, 1] মনে রাখা হয়।
  2. প্রথম ধাপ. তিনটি sequence-ই বেঁচে আছে, তাই batch_sizes-এর প্রথম সংখ্যা ৩.
  3. দ্বিতীয় ধাপ. এখনো তিনটি। এই ধাপের পরে সারি ০ শেষ।
  4. তৃতীয় ধাপ. দুটি বাকি, তাই RNN এই ধাপে মাত্র ২টি সারি নিয়ে কাজ করে; সারি ০-এর pad কোথাও ঢোকে না।
  5. শেষ ধাপ. একটি বাকি। মোট ৯টি আসল ধাপ, যেখানে padding-এ ঘর ছিল ১২টি। batch_sizes-এর যোগফল সব সময় lengths-এর যোগফলের সমান।
x = emb(ids).detach()
rnn = nn.RNN(input_size=3, hidden_size=5, batch_first=True)
pk = pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False)
print("packed.data", tuple(pk.data.shape), "| batch_sizes", pk.batch_sizes.tolist())
print("sorted_indices", pk.sorted_indices.tolist(), "| unsorted_indices", pk.unsorted_indices.tolist())

out_pk, h_pk = rnn(pk)
out, lens = pad_packed_sequence(out_pk, batch_first=True)
print("out", tuple(out.shape), "| lens", lens.tolist(), "| h_n", tuple(h_pk.shape))
print("zero after end? out[0, 2:] all zero:", bool((out[0, 2:] == 0).all()))
idx = torch.arange(3)
print("h_n == out at last real step:", torch.allclose(h_pk[0], out[idx, lens - 1]))

out_raw, h_raw = rnn(x)                 # packing ছাড়া
print("no packing: out", tuple(out_raw.shape), "h_n", tuple(h_raw.shape))
diff = (h_raw[0] - h_pk[0]).norm(dim=1)
print("|h_n(no pack) - h_n(pack)| per review:", [round(v, 4) for v in diff.tolist()])
print("out[:, -1] == h_n(no pack):", torch.allclose(out_raw[:, -1], h_raw[0]))

out2, l2 = pad_packed_sequence(out_pk, batch_first=True, total_length=6)
print("total_length=6 ->", tuple(out2.shape))
# এরপর দুটি ভুল ইচ্ছা করে: সাজানো নয় এমন lengths, আর দৈর্ঘ্য ০ (message ৯০ অক্ষরে কাটা)

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

packed.data (9, 3) | batch_sizes [3, 3, 2, 1]
sorted_indices [1, 2, 0] | unsorted_indices [2, 0, 1]
out (3, 4, 5) | lens [2, 4, 3] | h_n (1, 3, 5)
zero after end? out[0, 2:] all zero: True
h_n == out at last real step: True
no packing: out (3, 4, 5) h_n (1, 3, 5)
|h_n(no pack) - h_n(pack)| per review: [0.2641, 0.0, 0.9034]
out[:, -1] == h_n(no pack): True
total_length=6 -> (3, 6, 5)
enforce_sorted=True, unsorted -> RuntimeError: `lengths` array must be sorted in decreasing order when `enforce_sorted` is True. You can
length 0 -> RuntimeError: Length of all samples has to be greater than 0, but found an element in 'lengths' that is 

packed.data (9, 3): ৯টি আসল ধাপ, প্রতিটিতে ৩টি সংখ্যা; B আর T আর আলাদা নেই। pad_packed_sequence আবার (3, 4, 5) ফিরিয়ে দেয়, মূল সারির ক্রমে, আর দৈর্ঘ্যের বাইরের ঘরে শূন্য বসায়। আসল প্রমাণ দুই লাইনে: packing-এ h_n প্রতিটি রিভিউয়ের শেষ আসল ধাপের সমান। Packing ছাড়া দুই ছোট রিভিউয়ের h_n আলাদা (0.2641, 0.9034), আর পুরো দৈর্ঘ্যের রিভিউতে ফারাক 0.0. আকার দুই ক্ষেত্রেই (1, 3, 5), তাই কোনো error আসে না!

enforce_sorted=False দিলে নিজে সাজাতে হয় না; ডকুমেন্টেশন বলে True শুধু ONNX export-এর জন্য লাগে। DataParallel বা batch ভিন্ন হলে T মেলাতে total_length কাজে লাগে। দৈর্ঘ্য ০-এর রিভিউ এখানে error দেয়, তাই পরিষ্কার করার পরে ফাঁকা হয়ে যাওয়া রিভিউ collate-এর আগেই বাদ দিন।

Mask, packing, না bucketing? গড় বা attention-এর মতো একসঙ্গে-হিসাবে mask যথেষ্ট, Transformer-এও তাই। RNN-এ শেষ state নিতে packing, অথবা lens - 1 দিয়ে শেষ আসল ধাপ তোলা (bidirectional, অর্থাৎ দুই দিকে চলা RNN-এ packing-ই সহজ পথ, কারণ উল্টো দিকের চলা pad থেকে শুরু হলে নষ্ট হয়)। পর্ব ১১-এর bucketing এদের বিকল্প নয়; সে শুধু pad কমায়।

পুরো পথের আকার, এক টেবিলে

পরীক্ষা বা কোড রিভিউতে প্রায়ই এই পথটি হাতে লিখতে হয়। ওপরের run-এর সংখ্যায় (B = ৩, T = ৪, D = ৩, H = ৫, lengths = [2, 4, 3]):

ধাপআকারমনে রাখার কথা
Padding দেওয়া ids(3, 4)dtype long
emb(ids)(3, 4, 3)একটি মাত্রা বাড়ল
Packed data(9, 3)৯ = lengths-এর যোগফল
RNN-এর packed output-এর data(9, 5)F বদলে H
pad_packed_sequence-এর পরে(3, 4, 5)মূল সারির ক্রমে, দৈর্ঘ্যের বাইরে ০
h_n(1, 3, 5)batch_first এখানে নেই; মূল সারির ক্রমে
h_n[-1] → linear → logits (২ শ্রেণি)(3, 2)প্রতি রিভিউতে একটি উত্তর

“মূল সারির ক্রমে” কথাটি গুরুত্বপূর্ণ। ভেতরে packing সারি সাজিয়ে নিলেও enforce_sorted=False-এ h_n আর out দুটিই unsorted_indices দিয়ে আগের ক্রমে ফেরে, তাই label-এর সঙ্গে মিল থাকে। নিজে হাতে সারি সাজালে (যেমন পুরোনো কোডে lengths.sort()) ফেরানোর দায়িত্ব আপনার; ভুলে গেলে label আর উত্তর আলাদা রিভিউয়ের হয়, কিন্তু error আসে না। ওপরের run-এর h_n == out at last real step: True লাইনটি এই ক্রম মেলারও প্রমাণ।

৮ · batch_first — error ছাড়া ভুল

Shape-এর সবচেয়ে বিপজ্জনক ভুল সেটি, যা চলে। (8, 7, 2) ইনপুট নিন: ৮টি window, ৭ দিন, ২টি feature. batch_first ভুলে গেলে RNN ভাববে ৮ ধাপের সময়, batch ৭. নিচে একই weight-এ দুটি model.

import torch, torch.nn as nn
torch.manual_seed(27)
# ৮টি window, প্রতিটিতে ৭ দিন, প্রতিদিন ২টি feature (অর্ডার, ছুটির দিন কি না)
x = torch.randn(8, 7, 2)                      # (B, T, F)

good = nn.RNN(2, 5, batch_first=True)
bad = nn.RNN(2, 5, batch_first=False)         # default
bad.load_state_dict(good.state_dict())        # হুবহু একই weight

o1, h1 = good(x)
o2, h2 = bad(x)                               # কোনো error নেই
print("batch_first=True :", "out", tuple(o1.shape), "h_n", tuple(h1.shape))
print("batch_first=False:", "out", tuple(o2.shape), "h_n", tuple(h2.shape))
print("same out shape:", o1.shape == o2.shape, "| same values:", torch.allclose(o1, o2))

# সমান আকার, আলাদা অর্থ: শুধু window 1 বদলালে window 2-এর ফল কি বদলায়?
x2 = x.clone(); x2[1] += 10.0
print("True : window 2 changed?", not torch.allclose(good(x2)[0][2], o1[2]))
print("False: window 2 changed?", not torch.allclose(bad(x2)[0][2], o2[2]))

# সঠিক উপায়: batch_first=False হলে আগে transpose
o3, h3 = bad(x.transpose(0, 1))               # (T, B, F)
print("transpose:", "out", tuple(o3.shape), "| equals good:",
      torch.allclose(o3.transpose(0, 1), o1), "| h_n equal:", torch.allclose(h3, h1))

সত্যিকারের run (একই পরিবেশ, CPU-তে):

~/bazarghar-dl$ uv run python batch_first.py
batch_first=True : out (8, 7, 5) h_n (1, 8, 5)
batch_first=False: out (8, 7, 5) h_n (1, 7, 5)
same out shape: True | same values: False
True : window 2 changed? False
False: window 2 changed? True
transpose: out (7, 8, 5) | equals good: True | h_n equal: True

out-এর আকার হুবহু এক, মান আলাদা। ভুল model-এ এক window-এর ফল অন্য window-এর ওপর নির্ভর করে (changed? True): batch-এর সারিগুলো সময় বলে ধরা হচ্ছে। Shuffle করলে model চলবে, loss-ও কমতে পারে, তবু শেখা হবে বেঠিক। এই পরীক্ষাটি নিজের model-এ কাজে লাগান: একটি নমুনা বদলে দেখুন batch-এর অন্য নমুনার output বদলায় কি না। আর একটি ফাঁদ: batch_first=True h_n-এর ওপর খাটে না। সেটি সব সময় (layers, B, H); প্রথম সারিতে (1, 8, 5) দেখুন। Packing ছাড়া out[:, -1] আর h_n মিলে যায়, কিন্তু দুটিই pad-এর ওপর চলার ফল।

আকারের ভুল ধরার পাঁচ মিনিটের রুটিন

  1. B, T, F-এর জন্য তিনটি ভিন্ন সংখ্যা নিন, যেমন B = ৩, T = ৪, F = ২. সমান সংখ্যা (৪, ৪, ৪) নিলে উল্টো মাত্রাও ধরা পড়ে না।
  2. প্রতিটি ধাপের পরে x.shape ছাপুন, comment-এ মাত্রার নাম লিখুন।
  3. স্বাধীনতার পরীক্ষা: একটি নমুনা বদলে দেখুন অন্য নমুনার output স্থির থাকে কি না (BatchNorm থাকলে eval mode-এ)।
  4. Padding-এর পরীক্ষা: একই নমুনা লম্বা batch-এ দিয়ে দেখুন output সমান কি না।
  5. এক batch overfit করুন, পর্ব ১৮-এর মতো। না পারলে আগে ডেটার পথ সন্দেহ করুন।

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

  • T = ২০০, L = ৭, H = ১ হলে window কয়টি? T − L − H + 1 = ১৯৩; X (193, 7), y (193,).
  • Time series-এ random split কেন সন্দেহজনক? পাশের window-গুলো প্রায় একই ডেটা, আর test-এর উত্তর train-এর ইনপুটে থাকতে পারে।
  • lengths [2, 4, 3]-এর batch_sizes? [3, 3, 2, 1]; যোগফল ৯.
  • padding_idx কী করে? সেই সারি শূন্য দিয়ে শুরু হয়, gradient পায় না; তবে গড়ে ভাগের সংখ্যা ঠিক করতে mask তবু লাগে।
  • Bag কেন “ভালো না” আর “না ভালো” আলাদা করতে পারে না? গড় ক্রম-নিরপেক্ষ, তাই দুই ইনপুটের vector এক।

৯ · AI-এর যুগে — AI-এর লেখা sequence কোড যাচাই

AI assistant-এর লেখা sequence কোড প্রায়ই চলে, আর সেটিই সমস্যা। এই ভুলগুলো খুঁজুন:

AI-এর ভুলযেভাবে ধরবেন
Window বানিয়ে তারপর train_test_split বা shuffleভাগের পরে train আর test-এর সময়সীমা ছাপুন; ওভারল্যাপ গুনুন
x.mean(1) বা out[:, -1] padding দেওয়া batch-এএকই নমুনা ভিন্ন T-তে চালিয়ে তুলনা করুন
batch_first বাদ, অথচ ডেটা (B, T, F)এক নমুনা বদলে অন্যটির output দেখুন
padding_idx ঠিক আছে ভেবে mask বাদগড়ের ভাগফল হাতে মেলান
পুরোনো বা বানানো API, যেমন torchtext-এর ফাংশনআপনার version-এর ডকুমেন্টেশন খুলে দেখুন
একটি seed-এর ভালো ফলকে “অগ্রগতি” বলাকয়েকটি seed-এ baseline-এর পাশে রাখুন

AI-কে কোড লিখতে বলার সময় শর্তগুলো আগেই লিখে দিন, আর আকারের ছাপ চেয়ে নিন। প্রয়োজন হলে শব্দগুলো নিজের প্রকল্প অনুযায়ী বদলে নিন; নিজের গ্রাহকের আসল রিভিউ পেস্ট করবেন না।

তুমি PyTorch 2.x-এ একজন সতর্ক প্রকৌশলীর মতো কোড লেখো। কাজ: বাংলা রিভিউ classification-এর জন্য collate_fn আর model. শর্ত: ১. ইনপুট batch_first=True, আকার (B, T); প্রতিটি লাইনের পাশে আকার comment-এ লেখো। ২. pad id 0; nn.Embedding-এ padding_idx=0, আর গড়ে mask ব্যবহার করো। ৩. দৈর্ঘ্য ০-এর রিভিউ এলে কী হবে, সেটি বলো। ৪. একটি ছোট test দাও: একই রিভিউ দুই ভিন্ন দৈর্ঘ্যের batch-এ দিলে output সমান কি না যাচাই করে। ৫. যে API নিয়ে নিশ্চিত নও, সেটি বলে দাও; বানিয়ে লিখো না।

AI-এর কোড চালানোর আগে চেকলিস্ট

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

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

প্র ০১Time series-এ random split কি সব সময় ভুল? গবেষণা কী বলে?

প্রচলিত উত্তর “হ্যাঁ, সব সময়”, কিন্তু আসল উত্তর একটু সূক্ষ্ম। Bergmeir, Hyndman আর Koo ২০১৮ সালের একটি প্রবন্ধে (“A note on the validity of cross-validation for evaluating autoregressive time series prediction”) দেখান: বিশুদ্ধ autoregressive model-এ, যদি residual-গুলো পরস্পর সম্পর্কহীন হয়, সাধারণ k-fold cross-validation-ও বৈধ হতে পারে। কারণ, ইনপুট আগের মান হলে প্রতিটি সারিকে শর্তসাপেক্ষে প্রায় স্বাধীন নমুনা ধরা যায়। শর্তটি খেয়াল করুন: model-কে পর্যাপ্ত lag দিয়ে সব নির্ভরতা ধরতে হবে।

আমাদের পরীক্ষার series-এ শর্তটি ভাঙে। সেখানে স্তর ধীরে বাড়ে, তাই ভবিষ্যৎ শুধু “নতুন নমুনা” নয়, train-এর সীমার বাইরের জগৎ। সময় ধরে ভাগ ঠিক এই ধাক্কাটি মাপে; random split মাপে শুধু “পাশের দিনের মধ্যে ফাঁক ভরা”। বাস্তব ব্যবসায় ঈদ, দাম বদল, নতুন প্রতিযোগী: distribution shift প্রায় অনিবার্য, তাই সময় ধরে ভাগই নিরাপদ ডিফল্ট।

এটি শুধু তাত্ত্বিক নয়। Kapoor আর Narayanan ২০২৩ সালে (Patterns জার্নালে) নানা বিজ্ঞান ক্ষেত্রের ML গবেষণা ঘেঁটে দেখান, leakage অনেক দাবিকে ফুলিয়ে তুলেছে, আর তাঁদের শ্রেণিবিভাগে সময়ের leakage-ও একটি ধরন। মডেলের জটিলতা নয়, ডেটা ভাগই সেখানে ফল বদলে দিয়েছিল।

মূল উপলব্ধি: প্রশ্নটি “কোন split নিয়মসম্মত” নয়, বরং “বাস্তবে চালানোর দিনে model কোন প্রশ্নের মুখে পড়বে?” ভবিষ্যৎ অনুমান করতে হলে মূল্যায়নও ভবিষ্যতের ওপরই হোক; তত্ত্বের ব্যতিক্রম জেনে রাখুন, কিন্তু প্রমাণ ছাড়া তার ওপর ভরসা করবেন না।

প্র ০২গড়ে ক্রম হারায়, তবু bag-of-embeddings এত ভালো ফল দেয় কেন? তাহলে জটিল model কেন?

Text classification-এর অনেক কাজে কয়েকটি শব্দই উত্তর বলে দেয়: “নষ্ট”, “টাকা ফেরত”, “চমৎকার”। এই কারণেই bag-ধরনের model গবেষণায় বারবার শক্ত প্রতিপক্ষ হয়েছে। Wang আর Manning ২০১২ সালের “Baselines and Bigrams” প্রবন্ধে দেখান, শব্দজোড়া-সহ সরল linear model অনেক sentiment ডেটায় তখনকার জটিল পদ্ধতির কাছাকাছি বা ভালো। Iyyer ও সহলেখকেরা ২০১৫ সালে (“Deep Unordered Composition Rivals Syntactic Methods”) গড়ের ওপর কয়েকটি স্তর বসিয়ে syntax-নির্ভর model-এর কাছাকাছি ফল পান। আর Joulin ও সহলেখকদের fastText (“Bag of Tricks for Efficient Text Classification”, ২০১৬-১৭) গড়ের সঙ্গে n-gram জুড়ে খুব দ্রুত ফল দেখায়।

তাহলে ক্রম কখন লাগে? যখন উত্তরের ভাগ্য নির্ধারণ করে কাঠামো: নিষেধ, তুলনা (“আগেরটার চেয়ে খারাপ না”), “কিন্তু”-র পরের অংশ, অনুবাদ, প্রশ্নোত্তর। Socher ও সহলেখকেরা ২০১৩ সালে Stanford Sentiment Treebank নিয়ে কাজে দেখান, নিষেধের মতো কাঠামোতে bag-ধরনের পদ্ধতি বিশেষ করে দুর্বল। গড় accuracy এই দুর্বলতা লুকিয়ে রাখে, কারণ এমন বাক্য ডেটায় হয়তো কমই থাকে।

ব্যবহারিক দিক: bag কম খরচে চলে, ব্যাখ্যা করা সহজ, CPU-তে দ্রুত। চালানোর খরচ আর ত্রুটির দাম মিলিয়ে সিদ্ধান্ত নিতে হয়। উদাহরণ হিসেবে গ্রাহকের রিভিউ ভাগ করার প্রথম সংস্করণে bag দিয়ে শুরু করলে একটি পরিমাপযোগ্য মাপকাঠি পাওয়া যায়, যা পরের যেকোনো model-কে পেরোতে হবে।

মূল উপলব্ধি: প্রথমে সরল baseline, তারপর আলাদা করে “ক্রম-নির্ভর” পরীক্ষার সেট। জটিল model-এর দাম তখনই ন্যায্য, যখন সে সেই সেটে স্পষ্টভাবে এগিয়ে থাকে, শুধু সামগ্রিক সংখ্যায় নয়।

প্র ০৩বাংলায় “না”-এর জায়গা কেন আলাদা করে ভাবার বিষয়? ইংরেজি থেকে ধার করা অভ্যাস কোথায় ভুল করতে পারে?

ইংরেজিতে “not” সাধারণত বিশেষণ বা ক্রিয়ার আগে বসে: “not good”. তাই ইংরেজি sentiment-এর অনেক টিউটোরিয়াল “not”-এর পরের কয়েকটি শব্দ চিহ্নিত করে। বাংলায় মূল বাক্যে “না” সাধারণত পরে আসে: “ভালো না”, “আমি যাব না”। আবার শর্তের বাক্যে আগে আসে: “না গেলে”। তার ওপর নিষেধের আরও রূপ আছে: “নয়”, “নেই”, আর ক্রিয়ার সঙ্গে জোড়া “-নি” (“যাইনি”, “পাইনি”)। শেষের ক্ষেত্রে আলাদা শব্দই নেই; শব্দভিত্তিক tokenizer “পাইনি”-কে একটি ভিন্ন token ভাববে।

বাংলার সাধারণ গঠন কর্তা-কর্ম-ক্রিয়া, আর শব্দের ক্রমেও বেশ নমনীয়তা আছে: “ডেলিভারি ভালো ছিল না” আর “ভালো ছিল না ডেলিভারি” দুটিই বোঝা যায়। ফলে “ভালো” আর “না”-র মাঝে শব্দ বসতে পারে। পাশাপাশি শব্দজোড়া তখন ধরতে পারে না; আমাদের ছোট পরীক্ষার ১০০% শুধু কারণ ডেটায় মাঝখানে কিছু ছিল না। আর সত্যিকারের রিভিউতে আছে Banglish (“valo na”), আর ভিন্ন বানান: AI-এর ভিত্তি কোর্সের পর্ব ৬৭ তার কিছু দেখিয়েছে।

মনে রাখুন, “না ভালো” কথাটি সাধারণ বাক্যে কম শোনা যায়; এই পর্বে এটি মূলত “না, ভালোই” অর্থে একটি শিক্ষামূলক উদাহরণ। “খারাপ না, ভালো” বেশি স্বাভাবিক। বাংলায় বড় মান-সম্মত ডেটা তুলনামূলক কম বলে মনে করা হয়, তাই নিজের পরীক্ষার সেট নিজে যত্ন করে বানানোর দাম বেশি।

মূল উপলব্ধি: Tokenizer, n-gram আর mask-এর সিদ্ধান্ত ভাষা-নিরপেক্ষ নয়। ইংরেজির অভ্যাস নকল করার আগে বাংলায় আলাদা করে নিষেধের একটি ছোট পরীক্ষার সেট বানান। Tokenization-এর বিস্তার আসবে পর্ব ৪০-এ।

প্র ০৪PackedSequence এত গুছানো হলে Transformer-এ কেন নেই? Padding-এর খরচ আসলে কত?

Packing RNN-এর চলার ধরনের সঙ্গে মেলে: RNN ধাপে ধাপে চলে, তাই প্রতি ধাপে সারি ছোট হয়ে আসা স্বাভাবিক। batch_sizes ধরে loop ঘুরলেই হয়। Transformer পুরো sequence একসঙ্গে হিসাব করে (পর্ব ৩৩ আর পর্ব ৩৫-এ আসবে)। সেখানে আয়তাকার matrix আর attention-এ মানগুলো mask করে বাদ দেওয়াই স্বাভাবিক; packed আকারে দৈর্ঘ্য ছাড়া সেই গুণটি করা যায় না।

একটি সাবধানতা: নাম একই হলেও রীতি এক নয়। Hugging Face tokenizer-এর attention_mask-এ ১ মানে আসল token. PyTorch-এর src_key_padding_mask-এ True মানে “এই ঘর বাদ”। উল্টো mask দিলে কোড চলে, আর model pad ছাড়া কিছুই দেখে না। AI-এর লেখা কোডে এই গোলমাল খুঁজে দেখার মতো।

Padding-এর খরচ নির্ভর করে দৈর্ঘ্যের ছড়ানোর ওপর। ১০টি শব্দের ৩১টি রিভিউ আর ২০০ শব্দের একটি একই batch-এ পড়লে ৬,৪০০ ঘরের ৫,৮৯০টি pad, প্রায় ৯২%. এখানেই bucketing কাজ করে: দৈর্ঘ্যে কাছাকাছি রিভিউ এক batch-এ। আবার সর্বোচ্চ দৈর্ঘ্য কেটে ছোট করাও একটি সিদ্ধান্ত: দীর্ঘ অভিযোগের শেষ অংশে হয়তো আসল কথা ছিল। Mask আর lengths-এর খরচ সামান্য, কিন্তু pad-এর ওপর attention-এর গুণের খরচ T২ হারে বাড়ে।

ঐতিহাসিকভাবে packed ধারণা RNN-যুগে জনপ্রিয় হয়। আজও ছোট, CPU-তে চলা RNN-এ এটি উপযোগী, তবে export-এ enforce_sorted=True-এর মতো সীমা মনে রাখতে হয়।

মূল উপলব্ধি: Mask ও packing দুটি ভিন্ন model-গঠনের সঙ্গে মানানো উত্তর। Model কীভাবে হিসাব করে, তা জানলে সিদ্ধান্ত নিজে থেকেই আসে।

প্র ০৫একটি সাধারণ lookup টেবিল কীভাবে NLP-র কেন্দ্রে এল? আর টেবিলের সীমা কোথায়?

পুরোনো পদ্ধতিতে “ভালো” আর “চমৎকার” দুটি সম্পূর্ণ আলাদা চিহ্ন, one-hot-এ তাদের মিল শূন্য। শব্দকে সংখ্যার সারি দিয়ে প্রকাশের ধারণা (distributed representation) ১৯৮০-এর দশকে Hinton ও অন্যদের লেখায় পাওয়া যায়। Bengio ও সহলেখকেরা ২০০৩ সালের “A Neural Probabilistic Language Model”-এ শব্দের এমন টেবিল network-এর সঙ্গে একসঙ্গে শেখান। Collobert আর Weston-এর ২০০৮ ও ২০১১ সালের কাজ আর ২০১৩ সালের word2vec ধারণাটিকে ব্যাপক ব্যবহারে আনে। আজকের LLM-ও শুরুতে এমন একটি টেবিল দিয়েই token তোলে।

ছাঁচটি শব্দের বাইরেও চলে: জেলা, পণ্যের ধরন, বা দোকানের id-কে embedding দেওয়া যায়; Guo আর Berkhahn ২০১৬ সালে টেবিল ডেটার ক্যাটেগরির জন্য এমন “entity embedding” নিয়ে লেখেন। Time series-এ বাজারঘরের ৬৪ জেলার জন্য একটি ছোট টেবিল একটিমাত্র global model-কে জেলাভেদে ভিন্ন আচরণ করতে দেয় (মেশিন লার্নিং কোর্সের পর্ব ৫২-এর global model-এর ধারণা)।

সীমাগুলোও স্পষ্ট। প্রথমত, টেবিলে যে শব্দ নেই, তার সারি নেই: নতুন শব্দ, ভুল বানান <unk>-এ পড়ে। fastText-এর উপশব্দ আর আধুনিক tokenizer এটি কমায়। দ্বিতীয়ত, একটি শব্দের একটিই সারি: “মাথা” (শরীরের) আর “মাথা” (দলের) একই vector পায়; প্রসঙ্গ-নির্ভর vector আসবে পর্ব ৪৪-এ। তৃতীয়ত, প্রচুর parameter: ৫০,০০০ × ১২৮ = ৬৪ লাখ, প্রায়ই network-এর বাকি অংশের চেয়ে বেশি, আর বিরল শব্দের সারি প্রায় শেখেই না।

মূল উপলব্ধি: Embedding-এর শক্তি জাদুতে নয়, গঠনে: id থেকে শেখার যোগ্য vector, gradient-এ মিল তৈরি। তবে vector-এর কাছাকাছি হওয়া মানেই “অর্থ বোঝা” নয়; সেটি কাজের জন্য শেখা একটি পরিসংখ্যানগত মিল।

অনুশীলন

আগে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. হিসাব: ৩৬৫ দিনের series, L = ২৮, আর একসঙ্গে পরের H = ৭ দিনের পূর্বাভাস। ২টি feature. Window কয়টি? X আর y-এর আকার? শেষ ৬০টি window-কে test করলে কয়টি train window বাদ দিলে train-এর উত্তর test-এর প্রথম ইনপুটের আগেই শেষ হবে?

    Window = T − L − H + 1 = ৩৬৫ − ২৮ − ৭ + ১ = ৩৩১. X (331, 28, 2), y (331, 7). Window i-এর ইনপুট দিন i থেকে i + 27, উত্তর i + 28 থেকে i + 34. Test শুরু i = ২৭১ (৩৩১ − ৬০). তার ইনপুট শুরু দিন ২৭১. Train window i-এর শেষ উত্তর i + 34 < ২৭১ দরকার, অর্থাৎ i ≤ ২৩৬. তাই i = ০ থেকে ২৩৬, ২৩৭টি train window; ২৩৭ থেকে ২৭০, মোট ৩৪টি বাদ (L + H − 1 = ৩৪). শুধু test-এর উত্তর train-এর উত্তর থেকে আলাদা রাখতে চাইলে কম বাদ দিলেও চলে: H − 1 = ৬টি। কোন শর্ত লাগবে তা নির্ভর করে আপনি কোন স্বাধীনতা চান; আসল forecast-এ test-এর ইনপুট তো অতীতই।

  2. আউটপুট অনুমান: B = ৪, lengths = [5, 2, 2, 1], F = ৩, hidden = ৮. pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False) আর nn.RNN(3, 8, batch_first=True)-এ কী ছাপা হবে: pk.data.shape, pk.batch_sizes, out_pk.data.shape, h_n.shape, আর pad_packed_sequence-এর পরে out.shape?

    pk.data (10, 3) (৫ + ২ + ২ + ১ = ১০). batch_sizes [4, 3, 1, 1, 1]: t = ০-তে ৪টি, t = ১-এ ৩টি (দৈর্ঘ্য ১-এরটি শেষ), t = ২, ৩, ৪-এ ১টি করে। out_pk.data (10, 8). h_n (1, 4, 8). out (4, 5, 8). যাচাই: batch_sizes-এর যোগফল ১০, lengths-এর যোগফলও ১০.

  3. কোড: Masked max pooling লিখুন: x (B, T, F) আর mask (B, T) থেকে (B, F), যেখানে pad কখনো জেতে না। পরীক্ষা: x = [[[1, 5], [4, 2], [0, 0]], [[3, 1], [0, 0], [0, 0]]], mask = [[1, 1, 0], [1, 0, 0]]. সব মান ঋণাত্মক হলে (x * mask).max(1) কেন ভুল?

    np.where(mask[..., None], x, -np.inf).max(1), আর PyTorch-এ x.masked_fill(~mask[..., None], float("-inf")).max(1).values (mask এখানে bool). চালিয়ে ফল: [[4, 5], [3, 1]]. গুণের ভুলটি হলো pad-এর ঘর ০ হয়ে যায়, আর সব আসল মান ঋণাত্মক হলে সর্বোচ্চ ০, যা কোনো আসল শব্দের নয়। Mean-এ শূন্য চলে, কারণ ভাগফল ঠিক করা যায়; max-এ চলে না।

  4. সমালোচনা: একজন সহকর্মী রিপোর্ট দিলেন: “LSTM-এ দৈনিক অর্ডারের পূর্বাভাসে MAE ৩.১, ARIMA-র ৮.৫-কে বড় ব্যবধানে হারিয়েছে।” কোডে আছে: X, y = make_windows(orders); X_tr, X_te, y_tr, y_te = train_test_split(X, y); scaler পুরো orders-এ fit; ARIMA মাপা হয়েছে শেষ ২০%-এ। কী কী প্রশ্ন করবেন?

    (ক) Random split: LSTM-এর test window-এর চারপাশে train window, ওভারল্যাপ বিপুল। (খ) ভিন্ন test: LSTM আর ARIMA একই দিনে মাপা হয়নি, তাই সংখ্যা দুটি তুলনীয়ই নয়। (গ) Scaler-এর leak: train-এর গড়ে ভবিষ্যৎ ঢুকেছে। (ঘ) Baseline নেই: last value আর seasonal naive কই? (ঙ) এক seed আর এক দাগ: মেশিন লার্নিং কোর্সের পর্ব ৫২-এর rolling-origin backtest লাগবে, MASE-এ। ঠিক করার পরে বিজয়ী বদলে যেতে পারে; আর সেই ফলও একটি series-এর, সাধারণ সিদ্ধান্ত নয়।

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

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

ক্রমরিভিউ থেকে RNN-এর ফল পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. টেক্সট পরিষ্কার করে token আর id বানানো
  2. collate_fn-এ pad দিয়ে (B, T) আর lengths
  3. nn.Embedding দিয়ে (B, T, D)
  4. pack_padded_sequence দিয়ে packed করা
  5. RNN চালিয়ে packed output আর h_n
  6. pad_packed_sequence দিয়ে (B, T, H)-এ ফেরা

T = ১০০ দিনের series, L = ১০ দিনের ইনপুট, পরের H = ১ দিন উত্তর। X-এর আকার কী?

  • (100, 10)
  • (90, 10)
  • (91, 10)
  • (10, 90)

দৈনিক বিক্রির window-গুলোকে এলোমেলো ভাগে train আর test করলে সবচেয়ে সম্ভাব্য ফল কী?

  • Model কম overfit করে, তাই সত্যিকারের ফল ভালো হয়
  • কোড সঙ্গে সঙ্গে shape error দেয়
  • Test স্কোর বাস্তবের চেয়ে ভালো দেখায়, কারণ পাশের দিনের তথ্য দুই ভাগেই থাকে
  • কোনো প্রভাব নেই, কারণ দুই ভাগের আকার সমান

x-এর আকার (3, 4, 2), lengths = [3, 2, 4], ডান পাশে padding করা। প্রতিটি sequence-এর শেষ আসল ধাপ কোন expression দেয়?

  • x[torch.arange(3), lengths - 1]
  • x[:, -1]
  • x[-1]
  • x[:, lengths - 1]

Lengths = [4, 1, 3] হলে pack_padded_sequence(..., enforce_sorted=False)-এর batch_sizes কী?

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

গড় embedding-এর ওপর linear classifier “খাবার ভালো না” আর “খাবার না ভালো”-তে একই সম্ভাবনা দিল। সবচেয়ে সঠিক ব্যাখ্যা কী?

  • Model যথেষ্ট train হয়নি; আরও epoch দিলে ঠিক হবে
  • গড় ক্রম-নিরপেক্ষ, তাই দুই বাক্যের ইনপুট হুবহু এক
  • Embedding-এর মাত্রা ছোট; D বাড়ালেই ঠিক হবে
  • Padding token ভুল দেওয়া হয়েছে

nn.RNN(2, 5)-কে (default batch_first=False) (8, 7, 2) আকারের (B, T, F) ডেটা দেওয়া হলো। কী ঘটবে?

  • RuntimeError দিয়ে থেমে যাবে
  • ঠিকঠাক B = ৮ ধরে চলবে, কারণ PyTorch আকার দেখে বোঝে
  • h_n-এর আকার হবে (1, 8, 5)
  • কোনো error ছাড়া চলবে, কিন্তু ৮-কে সময় আর ৭-কে batch ধরে

শব্দকোষ

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

মনে রাখুন

  1. Sequence-এর সাধারণ আকার (B, T, F); টেক্সট আগে (B, T) id, তারপর embedding. RNN-এর default (T, B, F), আর h_n-এ batch_first খাটে না।
  2. Window-এর সংখ্যা T − L − H + 1. আগে সময়ের দাগ, পরে window; last value আর seasonal naive-কে সব সময় পাশে রাখুন।
  3. গড় আর শেষ ধাপে mask ও lengths লাগে: নইলে একই রিভিউ batch ভেদে আলাদা ফল দেয়, error ছাড়াই।
  4. Embedding = শেখার যোগ্য টেবিল; gradient শুধু ব্যবহৃত সারিতে, padding_idx-এর সারিতে শূন্য।
  5. Bag ক্রম-নিরপেক্ষ, তাই “ভালো না” / “না ভালো”-তে একই উত্তর। PackedSequence pad-এর ওপর RNN চালায় না; batch_sizes বলে প্রতি ধাপে কয়টি সারি বাকি।

পরের পর্বে: ডেটা এখন তৈরি। এবার (B, T, F)-এর ওপর আসল ধাপে ধাপে চলা model: hidden state, সময়ে ভাগ করা weight, আর NumPy-তে হাতে লেখা backpropagation through time.

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

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