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

Dataset আর DataLoader — batching, shuffling আর বাংলা ডেটা

PyTorch Dataset and DataLoader — Batching, Seeded Shuffling, Padding Masks, Samplers and Bangla Text

Taught in Bangla, this lesson splits the data job between Dataset and DataLoader. It cleans Bangla review text with Unicode NFC normalization, builds the vocabulary from the train split only, seeds shuffling with a dedicated generator, writes a collate_fn that pads and returns an attention mask, rebalances classes with WeightedRandomSampler, and splits data safely. Real desktop PyTorch runs time num_workers on Windows and show the __main__ guard, with a glance at transforms.v2 and IterableDataset.

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

  • টেবিলের ডেটা আর বাংলা রিভিউয়ের জন্য নিজের Dataset লেখা: NFC, ছোট vocab, <pad> আর <unk>
  • DataLoader-এর batch_size, seed দেওয়া shuffle, drop_last আর batch সংখ্যার হিসাব
  • collate_fn দিয়ে padding, lengths আর attention mask; mask ছাড়া গড় কেন ভুল
  • WeightedRandomSampler দিয়ে বিরল শ্রেণি, আর random_split-এ leakage ছাড়া ভাগ
  • transforms.v2, IterableDataset, num_workers, Windows-এর __main__ পাহারা আর মাপা throughput

পর্ব ১০ (Training loop — loss, optimizer, train/eval mode আর checkpoint) প্রতিটি ধাপে “একটি batch” নেয়। কিন্তু batch আসে কোথা থেকে? ডেটা ফাইলে থাকে কাঁচা লেখা আর সংখ্যা হিসেবে; model চায় সমান আকারের tensor, এলোমেলো ক্রমে, প্রতি epoch-এ নতুন করে। এই পথটাই data pipeline. AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এ বাংলা রিভিউ পরিষ্কার করা আর tokenization দেখেছেন; এখানে সেই কাজ PyTorch-এর কাঠামোয় বসবে। পাতার তিনটি ব্লক ব্রাউজারেই চলে (Python আর NumPy; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে)। PyTorch পাতায় চলে না, তাই বাকি সব ডেস্কটপের সত্যিকারের run: Python 3.14.4, PyTorch 2.15.0 (CPU build), torchvision 0.29.2, NumPy 2.5.4, লেখার সময়ের সংস্করণ।

১ · Dataset — “কয়টি আছে” আর “i-তম কোনটি”

বাজারঘরের গুদাম ভাবুন। তাকে প্রতিটি পার্সেলের একটি নম্বর আছে; গুদাম-রক্ষক শুধু দুটি প্রশ্নের উত্তর দেন: “মোট কয়টি?” আর “১৪২ নম্বরটি দিন”। কোন পার্সেল আগে যাবে, একেক ভ্যানে কয়টি উঠবে, কয়জন কুলি তুলবে, সেটা ঠিক করেন ডেলিভারি ম্যানেজার। PyTorch-এ গুদাম-রক্ষক Dataset, ম্যানেজার DataLoader.

PyTorch-এর সাধারণ Dataset-কে বলে map-style dataset: একটি class, যার দুটি method. __len__ মোট নমুনার সংখ্যা দেয়, __getitem__(i) i-তম নমুনা। কোনো জাদু নেই; Python কোর্সের পর্ব ৩২-এর dunder method দিয়েই len(ds) আর ds[i] কাজ করে। পুরো pipeline নিচের ছবিতে।

একটি batch-এর যাত্রা: Dataset থেকে training loop

Sampler gives indices, workers call __getitem__, collate_fn builds tensors

PyTorch data pipeline: sampler, workers, dataset, collate_fn, batch ১ Sampler (Generator, seed) ২ Worker / মূল process ৩ Dataset.__getitem__(i) ৪ collate_fn ৫ Batch → training loop index: [76, 42, 92] প্রতিটি index-এর নমুনা আনে NFC → শব্দ → id: দৈর্ঘ্য 8, 5, 6 pad, lengths, mask ids (3, 8) · mask (3, 8) · y (3,) Dataset: কী আর কতগুলো DataLoader: কোন ক্রমে, কতগুলো, কে আনবে
ছবি ১১.১ — Sampler শুধু index দেয়, Dataset শুধু একটি নমুনা দেয়, collate_fn তালিকাকে tensor বানায়। প্রতিটি অংশ আলাদা বদলানো যায়: নতুন sampler, নতুন collate, একই Dataset.

বাজারঘরের দুটি Dataset বানানো হলো, ফাইল bz_data.py-তে। OrdersDataset টেবিলের ডেটা: ১০০০টি অর্ডারের দূরত্ব আর ওজন থেকে ডেলিভারির ঘণ্টা (seed দেওয়া, বানানো)। পুরো ডেটা একবারেই float32 tensor-এ রাখা, __getitem__ শুধু একটি সারি কেটে দেয়। ReviewDataset বাংলা টেক্সট: make_reviews() ৬টি প্রশংসা আর ৫টি অভিযোগের বাক্যাংশ থেকে ১–৩টি জুড়ে ২০০টি রিভিউ বানায়, অভিযোগের সম্ভাবনা ১২%. ইচ্ছা করে অর্ধেক রিভিউতে “য়” এক code point-এ (U+09DF) লেখা, বাকিগুলোতে দুটিতে (য + নুক্তা), যেমন বাস্তবের কপি-পেস্ট করা ডেটায় হয়।

import unicodedata
from collections import Counter
import torch
from torch.utils.data import Dataset

def tokenize(text):
    return unicodedata.normalize("NFC", text).split()

def build_vocab(texts, min_freq=2):
    freq = Counter(w for t in texts for w in tokenize(t))
    itos = ["<pad>", "<unk>"] + sorted(w for w, c in freq.items() if c >= min_freq)
    return {w: i for i, w in enumerate(itos)}

class ReviewDataset(Dataset):
    def __init__(self, texts, labels, vocab):
        self.texts, self.labels, self.vocab = texts, labels, vocab
    def __len__(self):
        return len(self.texts)
    def __getitem__(self, i):
        ids = [self.vocab.get(w, 1) for w in tokenize(self.texts[i])]   # 1 = <unk>
        return torch.tensor(ids, dtype=torch.long), self.labels[i]

তিনটি নকশাগত সিদ্ধান্ত। এক, Unicode normalization tokenize-এর ভেতরে, তাই train, test আর পরে আসা নতুন রিভিউ সবাই একই পথে যায়। দুই, id ০ রাখা <pad>-এর জন্য, id ১ <unk>-এর জন্য; দুবারের কম দেখা শব্দ vocab-এ ঢোকে না। তিন, Dataset ভিন্ন দৈর্ঘ্যের tensor ফেরত দেয়; সমান করার দায়িত্ব পরে collate_fn-এর। ডেস্কটপে uv দিয়ে আলাদা project বানানো হলো (uv-এর পরিচয় AI-এর ভিত্তি কোর্সের পর্ব ৭-এ):

~/bazarghar-dl$ uv init --vcs none --author-from none --name bazarghar-dl --python 3.14
# pyproject.toml-এ PyTorch-এর CPU wheel index যোগ করা হলো (explicit = true)
~/bazarghar-dl$ uv add "torch==2.15.0" torchvision numpy
 …
 + torch==2.15.0+cpu
 + torchvision==0.29.2+cpu

datasets_demo.py দুটি Dataset বানায়, NFC-র প্রভাব গোনে, random_split দিয়ে ৭০:১৫:১৫ ভাগ করে আর শুধু train ভাগ থেকে vocab বানায়। সত্যিকারের run:

~/bazarghar-dl$ uv run python datasets_demo.py
Python 3.14.4 | torch 2.15.0+cpu | numpy 2.5.4
orders: 1000 | item 0: tensor([19.1278,  6.9786]) tensor([53.2907]) | torch.float32 (2,)
reviews: 200 | অভিযোগ: 23
আলাদা শব্দ: কাঁচা 28 | NFC-র পরে 26 | এক-code-point য়-ওয়ালা রিভিউ: 59
split: 140 30 30 | train-এর প্রথম ৫: [142, 56, 198, 77, 50]
vocab: 28 | [('<pad>', 0), ('<unk>', 1), ('অনুযায়ী', 2), ('অনেক', 3)]
train[0]: দাম অনুযায়ী মান ভালো পণ্য খুব ভালো প্যাকেট সুন্দর ছিল -> [14, 2, 24, 21, 17, 8, 21, 18, 27, 11] label 0
নতুন রিভিউ: [17, 1, 13, 1] (1 = <unk>)

টেবিলের একটি নমুনা (দূরত্ব ১৯.১ কিমি, ওজন ৭.০ কেজি) থেকে ঘণ্টা ৫৩.৩; dtype float32, আকার (2,). রিভিউতে ২৩টি অভিযোগ, মানে প্রায় ১১.৫%. “সময়মতো” আর “অনুযায়ী” দুই রূপে থাকায় কাঁচা ডেটায় ২৮টি আলাদা শব্দ, NFC-র পরে ২৬টি: NFC ছাড়া model এই দুটি শব্দকে চারটি আলাদা শব্দ ভাবত, আর প্রতিটির উদাহরণ অর্ধেক হয়ে যেত। Vocab-এ ২৬টি শব্দ আর দুটি বিশেষ token. শেষ লাইনে “চমৎকার” আর “দ্রুত” train-এ ছিল না, তাই দুটিই ১, অর্থাৎ <unk>.

Vocab কেন শুধু train থেকে: পুরো ডেটা থেকে vocab বানালে test-এর শব্দও আগে থেকে id পায়, অথচ বাস্তবে নতুন রিভিউয়ের অচেনা শব্দ <unk> হবে। ফলে test-এর ফল একটু বেশি আশাবাদী আসে। এটি data leakage-এর ছোট রূপ, AI-এর ভিত্তি কোর্সের পর্ব ৫৬-এর preprocessing leakage-এর মতো। নিয়ম একই: ডেটা থেকে যা কিছু “শেখা” হয় (vocab, গড়, scaler), তা শুধু train ভাগ থেকে।

২ · DataLoader — batch, shuffle আর shuffle-এর নিজের seed

DataLoader-এর মূল কাজ তিনটি: index সাজানো (shuffle=True হলে প্রতি epoch-এ নতুন এলোমেলো ক্রম), batch_size করে কাটা, আর প্রতিটি টুকরোকে collate করা। N-টি নমুনা আর batch size B হলে প্রতি epoch-এ batch সংখ্যা ⌈N/B⌉, শেষ batch-এ N mod B-টি নমুনা (০ হলে পূর্ণ)। drop_last=True দিলে শেষের ছোট batch বাদ, batch সংখ্যা ⌊N/B⌋. ছোট শেষ batch সমস্যা করে যখন কোনো স্তর batch-এর পরিসংখ্যান নেয় (পর্ব ১৪-এ BatchNorm), বা যখন সব batch-এর আকার একই হওয়া দরকার।

নিচের ব্লকে NumPy দিয়ে DataLoader-এর মূল যুক্তির একটি ছোট নকল। শেষ অংশে একটি পরীক্ষা: shuffle আর weight init যদি একই random উৎস ভাগ করে, তাহলে কী হয়।

import numpy as np

X = np.arange(10) * 10                   # ১০টি অর্ডার (মান: 0, 10, … 90)

def loader(n, batch_size, rng, shuffle=True, drop_last=False):
    """DataLoader-এর মূল কাজ: index সাজানো, তারপর batch_size করে কাটা।"""
    idx = rng.permutation(n) if shuffle else np.arange(n)
    for s in range(0, n, batch_size):
        b = idx[s:s + batch_size]
        if drop_last and len(b) < batch_size:
            break
        yield b

data_rng = np.random.default_rng(42)     # শুধু shuffle-এর জন্য আলাদা RNG
for epoch in range(2):
    print("epoch", epoch, [X[b].tolist() for b in loader(10, 4, data_rng)])

n, B = 10, 4
print("batch সংখ্যা: ceil =", -(-n // B), "| drop_last =", n // B)

# আরেকটি random কাজ (যেমন weight init) মাঝে ঢুকলে কী হয়?
def first_batch(share_rng, init_first):
    rng = np.random.default_rng(0)
    if init_first:
        rng.normal(size=5)               # model-এর ৫টি weight
    shuffle_rng = rng if share_rng else np.random.default_rng(1)
    return next(loader(10, 4, shuffle_rng)).tolist()

for share in (True, False):
    print("একই RNG" if share else "আলাদা RNG", "| init আগে নয়:", first_batch(share, False),
          "| init আগে:", first_batch(share, True))

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

epoch 0 [[50, 60, 0, 70], [30, 20, 40, 90], [10, 80]]
epoch 1 [[40, 80, 20, 60], [50, 90, 70, 30], [0, 10]]
batch সংখ্যা: ceil = 3 | drop_last = 2
একই RNG | init আগে নয়: [4, 6, 2, 7] | init আগে: [2, 8, 3, 6]
আলাদা RNG | init আগে নয়: [8, 4, 7, 0] | init আগে: [8, 4, 7, 0]

দুই epoch-এর ক্রম আলাদা, কিন্তু প্রতিটিতে ১০টি অর্ডারই ঠিক একবার: shuffle মানে নমুনা বদলানো নয়, ক্রম বদলানো। ১০টি নমুনা, batch ৪: তিনটি batch (৪, ৪, ২), drop_last-এ দুটি। শেষ দুই লাইন এই অংশের মূল কথা। Shuffle আর weight init একই RNG ভাগ করলে, শুধু init-এর লাইনটি আগে আসায় প্রথম batch [4, 6, 2, 7] থেকে [2, 8, 3, 6] হয়ে গেল। Shuffle-এর নিজের RNG থাকলে দুই ক্ষেত্রেই [8, 4, 7, 0].

১০টি নমুনা, batch ৪ · প্রথম batch-এর index
অবস্থা
প্রথম batch
মানে
  1. এক উৎস. পুরো প্রোগ্রামে একটি random উৎস, seed ০. Shuffle সেখান থেকে একটি permutation টানে; প্রথম batch [4, 6, 2, 7]. বারবার চালালে একই।
  2. একটি লাইন যোগ. কেউ shuffle-এর আগে model তৈরি করলেন; init ৫টি random সংখ্যা খরচ করল। Seed এখনো ০, তবু shuffle এখন উৎসের পরের অংশ পায়: প্রথম batch [2, 8, 3, 6].
  3. ক্ষতিটা কোথায়. আপনি দুটি model তুলনা করছেন, একটিতে একটি স্তর বেশি। ফলের পার্থক্যের কিছুটা আসতে পারে শুধু ডেটার ভিন্ন ক্রম থেকে; কোনটি স্থাপত্যের প্রভাব, আলাদা করা যায় না।
  4. আলাদা উৎস. Shuffle-এর জন্য নিজের RNG (seed ১): model-এর আগে-পরে যা-ই হোক, প্রথম batch [8, 4, 7, 0]. ডেটার ক্রম এখন model-এর কোড থেকে স্বাধীন।
  5. PyTorch-এ. ডেস্কটপে একই পরীক্ষা: global torch.manual_seed(0)-এ shuffle-এর ক্রম nn.Linear তৈরির পরে বদলে গেল, আর DataLoader-কে নিজের Generator দিলে দুই ক্ষেত্রেই এক থাকল।

PyTorch-এ নিজের উৎস মানে একটি torch.Generator. loader_demo.py-র প্রথম অংশ:

import torch
from torch.utils.data import DataLoader, TensorDataset
from bz_data import ReviewDataset, make_reviews, build_vocab, pad_collate

ten = TensorDataset(torch.arange(10))                       # ১০টি সারি: 0..9
def orders(seed, epochs=2):
    dl = DataLoader(ten, batch_size=4, shuffle=True,
                    generator=torch.Generator().manual_seed(seed))
    return [[b[0].tolist() for b in dl] for _ in range(epochs)]
print("seed 42:", orders(42))
print("seed 42:", orders(42), "<- আবার চালালে")
print("seed  7:", orders(7))
dl = DataLoader(ten, batch_size=4, drop_last=True)
print("drop_last=True:", [b[0].tolist() for b in dl], "| len(dl) =", len(dl))

texts, labels = make_reviews()
ds = ReviewDataset(texts, labels, build_vocab(texts))
try:
    next(iter(DataLoader(ds, batch_size=3)))                 # default collate
except RuntimeError as e:
    print("RuntimeError:", e)
dl = DataLoader(ds, batch_size=3, shuffle=True, collate_fn=pad_collate,
                generator=torch.Generator().manual_seed(42))
ids, mask, lengths, y = next(iter(dl))
print("ids", tuple(ids.shape), ids.dtype, "| lengths", lengths.tolist(), "| y", y.tolist())
print(ids); print(mask.int())

সত্যিকারের run:

~/bazarghar-dl$ uv run python loader_demo.py
seed 42: [[[6, 5, 4, 0], [8, 9, 2, 1], [3, 7]], [[3, 6, 5, 8], [9, 4, 2, 0], [7, 1]]]
seed 42: [[[6, 5, 4, 0], [8, 9, 2, 1], [3, 7]], [[3, 6, 5, 8], [9, 4, 2, 0], [7, 1]]] <- আবার চালালে
seed  7: [[[1, 3, 5, 7], [9, 4, 6, 2], [8, 0]], [[5, 1, 6, 0], [9, 7, 8, 4], [3, 2]]]
drop_last=True: [[0, 1, 2, 3], [4, 5, 6, 7]] | len(dl) = 2
RuntimeError: stack expects each tensor to be equal size, but got [10] at entry 0 and [6] at entry 1
ids (3, 8) torch.int64 | lengths [8, 5, 6] | y [0, 0, 0]
tensor([[ 4,  7, 13, 26,  6, 25, 10, 23],
        [ 4,  7, 25, 10, 23,  0,  0,  0],
        [13, 26,  6, 17,  8, 21,  0,  0]])
tensor([[1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 0, 0]], dtype=torch.int32)

একই Generator seed-এ দুই epoch-এর পুরো ক্রম হুবহু ফিরে এল, আর একই DataLoader-এর দ্বিতীয় epoch প্রথমটির থেকে আলাদা: Generator এগিয়ে চলে। seed ৭ সম্পূর্ণ অন্য ক্রম। drop_last-এ ৮ আর ৯ বাদ, len(dl) = ২. পঞ্চম লাইনের RuntimeError পরের অংশের বিষয়: default collate সব নমুনাকে torch.stack করতে চায়, আর দৈর্ঘ্য ১০ আর ৬ এক matrix-এ বসে না।

ছোট খুঁটিনাটি: একই seed ৪২ দিয়ে random_split-এর train ভাগের প্রথম index ছিল ১৪২, অথচ DataLoader-এর প্রথম batch [76, 42, 92]. কারণ DataLoader প্রতিটি epoch-এর শুরুতে Generator থেকে worker-দের জন্য একটি base seed টেনে নেয়, তারপর shuffle করে। শিক্ষা: “একই seed” মানে একই ক্রম শুধু একই কোডের পথে; seed-এর সংখ্যা মিলিয়ে দুটি ভিন্ন function-এর ফল মেলানোর আশা করবেন না। Seed আর পুনরুৎপাদনের বড় ছবি মেশিন লার্নিং কোর্সের পর্ব ৪-এ, আর deep learning-এর বিশেষ দিক পর্ব ১৯-এ।

৩ · collate_fn — padding, lengths আর attention mask

Batch মানে একটি আয়তাকার tensor. ভিন্ন দৈর্ঘ্যের রিভিউকে আয়তাকার করার সহজ উপায় padding: সবচেয়ে লম্বা রিভিউয়ের দৈর্ঘ্য Lmax ধরে বাকিগুলোর শেষে <pad> (id ০) বসানো। কিন্তু pad কোনো শব্দ নয়; model-কে জানাতে হবে কোন ঘর আসল। সেই খবর বহন করে attention mask: আকার ids-এর সমান, আসল token-এ ১ (True), pad-এ ০. নামটি এসেছে Transformer থেকে, যেখানে attention pad-এর দিকে না তাকাতে এটি লাগে (পর্ব ৩৩); কিন্তু গড় নেওয়া, loss হিসাব, RNN সবখানেই একই mask কাজে লাগে। collate_fn এই সব একসঙ্গে বানায়:

def pad_collate(batch, pad_id=0):
    seqs, labels = zip(*batch)                    # [(ids, y), …] থেকে দুই তালিকা
    lengths = torch.tensor([len(s) for s in seqs])
    ids = torch.full((len(seqs), int(lengths.max())), pad_id, dtype=torch.long)
    for r, s in enumerate(seqs):
        ids[r, :len(s)] = s
    mask = torch.arange(ids.shape[1])[None, :] < lengths[:, None]
    return ids, mask, lengths, torch.tensor(labels)

Mask-এর লাইনটি broadcasting: (1, Lmax) আকারের 0, 1, …, Lmax−1-কে (B, 1) আকারের lengths-এর সঙ্গে তুলনা করলে (B, Lmax) আকারের True/False matrix. ids != pad_id লিখলেও প্রায়ই চলে, কিন্তু lengths থেকে বানানো mask নিরাপদ: কোনো tokenizer-এ আসল token-এর id ০ হলেও ভুল হয় না। PyTorch-এ torch.nn.utils.rnn.pad_sequence একই padding করে দেয়। নিচের stepper আগের run-এর সেই প্রথম batch ধাপে ধাপে দেখায়।

DataLoader(ds, batch_size=3, shuffle=True, generator seed 42, collate_fn=pad_collate)
ধাপ
হাতে কী
আকার / dtype
  1. Index. Sampler নিজের Generator থেকে ২০০টি index-এর একটি permutation বানায়; প্রথম তিনটি ৭৬, ৪২, ৯২. এখনো কোনো ডেটা ছোঁয়া হয়নি।
  2. প্রথম নমুনা. ds[76]: NFC, ফাঁকায় ভাঙা, vocab থেকে id. আটটি শব্দ, তাই (8,) আকারের int64 tensor আর label ০.
  3. বাকি দুটি. দৈর্ঘ্য ৫ আর ৬. তিনটি tensor-এর আকার আলাদা, তাই torch.stack এখানেই আটকে যেত।
  4. আকার ঠিক. সবচেয়ে লম্বা ৮, তাই ৩ × ৮ matrix, শুরুতে সব ঘরে pad id ০.
  5. ভরা আর ফেরত. প্রতিটি সারির শুরুতে নিজের id বসে, বাকি ঘরে ০ থাকে। সঙ্গে mask, lengths আর y; এই চারটি tensor-ই training loop-এর “একটি batch”.

এবার একই কাজ NumPy-তে, তিনটি ছোট রিভিউ দিয়ে। শেষে দেখা যাবে mask ছাড়া গড় নিলে কী হয়: এখানে প্রতিটি token-এর একটি নকল “score” (id থেকে একটি সংখ্যা) ধরা হয়েছে, যেমন পরে embedding-এর গড় নেওয়া হবে।

import numpy as np

vocab = {"<pad>": 0, "<unk>": 1, "অনেক": 3, "আবার": 4, "এসেছে": 6, "কিনব": 7,
         "চাই": 9, "টাকা": 12, "ডেলিভারি": 13, "দেরিতে": 15, "পণ্য": 17,
         "ফেরত": 19, "ভাঙা": 20}
reviews = ["আবার কিনব", "পণ্য ভাঙা এসেছে", "ডেলিভারি অনেক দেরিতে টাকা ফেরত চাই"]
seqs = [[vocab.get(w, 1) for w in r.split()] for r in reviews]
print("token id:", seqs)

def pad_collate(seqs, pad_id=0):
    lengths = np.array([len(s) for s in seqs])
    ids = np.full((len(seqs), lengths.max()), pad_id, dtype=np.int64)
    for r, s in enumerate(seqs):
        ids[r, :len(s)] = s
    mask = np.arange(ids.shape[1])[None, :] < lengths[:, None]   # broadcasting
    return ids, mask, lengths

ids, mask, lengths = pad_collate(seqs)
print("ids", ids.shape); print(ids)
print("mask"); print(mask.astype(int)); print("lengths", lengths)
print(f"pad ঘর: {(~mask).sum()} / {mask.size} = {(~mask).mean():.0%}")

# প্রতিটি token-এর একটি নকল score; গড় নেওয়ার দুই উপায়
emb = np.linspace(-1, 1, 21)             # id -> একটি সংখ্যা
score = emb[ids]                         # (3, 6)
naive = score.mean(axis=1)               # pad-ও গড়ে ঢুকছে
masked = (score * mask).sum(axis=1) / mask.sum(axis=1)
print("naive mean :", naive.round(3))
print("masked mean:", masked.round(3))

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

token id: [[4, 7], [17, 20, 6], [13, 3, 15, 12, 19, 9]]
ids (3, 6)
[[ 4  7  0  0  0  0]
 [17 20  6  0  0  0]
 [13  3 15 12 19  9]]
mask
[[1 1 0 0 0 0]
 [1 1 1 0 0 0]
 [1 1 1 1 1 1]]
lengths [2 3 6]
pad ঘর: 7 / 18 = 39%
naive mean : [-0.817 -0.283  0.183]
masked mean: [-0.45   0.433  0.183]

১৮টি ঘরের ৭টি pad, অর্থাৎ ৩৯% হিসাব অকারণ। সাধারণ গড়ে pad-এর score (−১) ঢুকে ছোট রিভিউগুলোকে টেনে নামিয়েছে: “পণ্য ভাঙা এসেছে”-র গড় ০.৪৩৩ থেকে −০.২৮৩, চিহ্নই উল্টে গেল। শুধু পূর্ণ দৈর্ঘ্যের তৃতীয় রিভিউতে দুটি এক। অর্থাৎ ফল নির্ভর করছে batch-এ পাশে কোন রিভিউ বসেছে তার ওপর; mask দিয়ে যোগ করে আসল দৈর্ঘ্যে ভাগ করলে সেই নির্ভরতা চলে যায়।

“আবার কিনব” · “পণ্য ভাঙা এসেছে” · “ডেলিভারি অনেক দেরিতে টাকা ফেরত চাই”
ধাপ
matrix
নিয়ম
  1. Id. তিনটি রিভিউ, দৈর্ঘ্য ২, ৩ আর ৬. এখনো তিনটি আলাদা তালিকা।
  2. আকার. সবচেয়ে লম্বা ৬, তাই batch-এর আকার (3, 6), মোট ১৮টি ঘর।
  3. ভরা. প্রতিটি সারির শুরুতে আসল id; বাকি ঘরে ০, অর্থাৎ <pad>. প্রথম সারিতে চারটি, দ্বিতীয়তে তিনটি pad.
  4. Mask. কলাম নম্বর j যদি সারির দৈর্ঘ্যের চেয়ে ছোট হয়, ১; না হলে ০. মোট ১১টি ১ (২ + ৩ + ৬) আর ৭টি ০.
  5. ব্যবহার. গড়, loss বা attention-এ mask দিয়ে pad বাদ। ৩৯% pad মানে প্রায় এক-তৃতীয়াংশের বেশি হিসাব নষ্ট; কাছাকাছি দৈর্ঘ্যের রিভিউ একসঙ্গে রাখলে (bucketing, অনুশীলন ৪) তা কমে।

Sequence ডেটার padding, masking আর RNN-এর জন্য packing বিস্তারিত আসবে পর্ব ২৭-এ (Sequence ডেটা — সময়, টেক্সট আর padding ও masking). Bucketing-এর ধারণা এখানেই মনে রাখুন: দৈর্ঘ্যে কাছাকাছি নমুনা একই batch-এ রাখলে pad কমে, কিন্তু shuffle কম এলোমেলো হয়, তাই সাধারণত bucket-এর ভেতরে আর bucket-গুলোর ক্রমে আলাদা করে shuffle করা হয়।

৪ · Sampler — বিরল শ্রেণিকে বেশিবার দেখানো

২০০টি রিভিউয়ের ২৩টি অভিযোগ: এটি class imbalance. সাধারণ shuffle-এ ৩২-এর একটি batch-এ গড়ে মাত্র ৩–৪টি অভিযোগ থাকে, আর model “সব রিভিউ ভালো” বলেও প্রায় ৮৮% ঠিক পায়। মেশিন লার্নিং কোর্সের পর্ব ৫০-এ oversampling, class weight আর threshold-এর তুলনা হয়েছে। Deep learning-এ oversampling-এর সবচেয়ে সহজ জায়গা sampler: DataLoader-কে কোন index দেওয়া হবে, তা sampler ঠিক করে। WeightedRandomSampler প্রতিটি সারিকে একটি ওজন দেয় আর সেই অনুপাতে replacement-সহ index টানে।

হিসাব: শ্রেণি c-তে nc-টি সারি থাকলে প্রতিটি সারির ওজন w = 1/nc. তাহলে শ্রেণি c-এর মোট ওজন nc × 1/nc = ১, সব শ্রেণির সমান; সম্ভাবনায় প্রতিটি শ্রেণি 1/K (K = শ্রেণির সংখ্যা)। একটি সারি টানার সম্ভাবনা p = w / Σw. N বার টানলে একটি নির্দিষ্ট সারি অন্তত একবার আসার সম্ভাবনা 1 − (1 − p)N.

import numpy as np

y = np.array([0] * 177 + [1] * 23)       # ২০০টি রিভিউ, ২৩টি অভিযোগ
counts = np.bincount(y)
w_class = 1 / counts                     # শ্রেণির ওজন = 1 / সংখ্যা
w_row = w_class[y]                       # প্রতিটি সারির ওজন
p = w_row / w_row.sum()                  # টানার সম্ভাবনা
print("counts:", counts, "| p (সংখ্যাগুরু সারি):", round(float(p[0]), 5),
      "| p (অভিযোগ সারি):", round(float(p[-1]), 5))
print("প্রত্যাশিত শ্রেণি-ভাগ:", [round(float(p[y == c].sum()), 3) for c in (0, 1)])

rng = np.random.default_rng(0)
n = len(y)
for epoch in range(3):
    idx = rng.choice(n, size=n, replace=True, p=p)
    print(f"epoch {epoch}: শ্রেণি {np.bincount(y[idx], minlength=2)}",
          f"| আলাদা সারি {len(np.unique(idx))}",
          f"| অভিযোগ সারির গড় পুনরাবৃত্তি {np.mean(y[idx]) * n / 23:.2f}")

# একটি নির্দিষ্ট সারি এক epoch-এ অন্তত একবার আসার সম্ভাবনা: 1 - (1 - p)^n
for c in (0, 1):
    pr = 1 - (1 - p[y == c][0]) ** n
    print(f"শ্রেণি {c}: অন্তত একবার আসে {pr:.3f}")
print("প্রত্যাশিত আলাদা সারি:", round(float(177 * (1 - (1 - p[0]) ** n) + 23 * (1 - (1 - p[-1]) ** n)), 1))

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

counts: [177  23] | p (সংখ্যাগুরু সারি): 0.00282 | p (অভিযোগ সারি): 0.02174
প্রত্যাশিত শ্রেণি-ভাগ: [0.5, 0.5]
epoch 0: শ্রেণি [ 88 112] | আলাদা সারি 95 | অভিযোগ সারির গড় পুনরাবৃত্তি 4.87
epoch 1: শ্রেণি [ 95 105] | আলাদা সারি 93 | অভিযোগ সারির গড় পুনরাবৃত্তি 4.57
epoch 2: শ্রেণি [ 91 109] | আলাদা সারি 94 | অভিযোগ সারির গড় পুনরাবৃত্তি 4.74
শ্রেণি 0: অন্তত একবার আসে 0.432
শ্রেণি 1: অন্তত একবার আসে 0.988
প্রত্যাশিত আলাদা সারি: 99.2

একটি অভিযোগ-সারির সম্ভাবনা ০.০২১৭৪, একটি সাধারণ সারির ০.০০২৮২: অনুপাত ১৭৭/২৩ ≈ ৭.৭ গুণ। দুই শ্রেণির মোট সম্ভাবনা ঠিক ০.৫ আর ০.৫. বাস্তব টানায় ভাগ ৮৮:১১২ থেকে ৯৫:১০৫, প্রত্যাশার চারপাশে ওঠানামা। প্রতিটি অভিযোগ-সারি এক epoch-এ গড়ে প্রায় ৪.৬–৪.৯ বার আসে। উল্টো দিকে একটি সাধারণ সারি এক epoch-এ অন্তত একবার আসার সম্ভাবনা মাত্র ০.৪৩২, অর্থাৎ ১৭৭টির প্রায় ৫৭% সারি সেই epoch-এ দেখাই হয় না। সূত্রে প্রত্যাশিত আলাদা সারি ৯৯.২, আর টানায় পাওয়া ৯৩–৯৫ তার কাছাকাছি।

এবার PyTorch-এ, ReviewDataset আর pad_collate-সহ (sampler_demo.py-র মূল অংশ):

y = torch.tensor(labels)
counts = torch.bincount(y)
w_class = 1.0 / counts.float()                    # বিরল শ্রেণির ওজন বেশি
weights = w_class[y]                              # প্রতিটি সারির ওজন
plain = DataLoader(ds, batch_size=32, shuffle=True, collate_fn=pad_collate,
                   generator=torch.Generator().manual_seed(0))
sampler = WeightedRandomSampler(weights, num_samples=len(ds), replacement=True,
                                generator=torch.Generator().manual_seed(0))
bal = DataLoader(ds, batch_size=32, sampler=sampler, collate_fn=pad_collate)
for ep in range(3):
    print(f"epoch {ep}: shuffle {epoch_counts(plain)} | weighted {epoch_counts(bal)}")
idx = list(sampler)                               # আরেকবার (চতুর্থ) টানা
rare = [i for i in idx if labels[i] == 1]
print("চতুর্থ টানায় বিরল শ্রেণি:", len(rare), "| আলাদা সারি:", len(set(rare)),
      "| মোট আলাদা সারি:", len(set(idx)), "/", len(ds))
~/bazarghar-dl$ uv run python sampler_demo.py
শ্রেণির সংখ্যা: [177, 23]
প্রতি শ্রেণির ওজন: [0.0056, 0.0435]
epoch 0: shuffle [177, 23] | weighted [100, 100]
epoch 1: shuffle [177, 23] | weighted [101, 99]
epoch 2: shuffle [177, 23] | weighted [99, 101]
চতুর্থ টানায় বিরল শ্রেণি: 98 | আলাদা সারি: 23 | মোট আলাদা সারি: 98 / 200

Shuffle প্রতি epoch-এ ঠিক ১৭৭:২৩ দেয়, কারণ সে শুধু ক্রম বদলায়। Weighted sampler প্রায় ১০০:১০০, তবে টানা random বলে প্রতিবার একটু ওঠানামা। শেষ লাইনটিই দাম: ২৩টি অভিযোগ ৯৮ বার এসেছে, প্রতিটি গড়ে চারবারের বেশি, আর ২০০ টানায় মাত্র ৯৮টি আলাদা সারি; সংখ্যাগুরু শ্রেণির অর্ধেকের বেশি সারি এই epoch-এ দেখাই হয়নি। দুটি সতর্কতা: sampler আর shuffle=True একসঙ্গে দেওয়া যায় না (ValueError: “sampler option is mutually exclusive with shuffle”), আর validation বা test-এ কখনো weighted sampler নয়; সেখানে আসল অনুপাতেই মাপতে হয়।

৫ · ভাগ করা — random_split, আগে ভাগ পরে Dataset

অংশ ১-এর run-এ random_split(range(200), [0.7, 0.15, 0.15], generator=g) ১৪০, ৩০, ৩০-এ ভাগ করেছে। ভগ্নাংশ দিলে PyTorch সংখ্যায় রূপান্তর করে নেয়; Generator ছাড়া প্রতিবার আলাদা ভাগ আসত, আর আজকের test সেট কালকের train-এ ঢুকে যেত। দুটি প্রচলিত পথ আছে:

পথকখন ঠিকঝুঁকি
পুরো Dataset বানিয়ে random_split(ds, …)Dataset কিছু “শেখে” না, যেমন OrdersDatasetDataset যদি পুরো ডেটা থেকে vocab, গড় বা scaler বানায়, test-এর তথ্য train-এ ঢোকে
আগে index ভাগ, তারপর train থেকে vocab, তারপর তিনটি Datasetটেক্সট, normalisation বা যেকোনো fit করা ধাপ থাকলেকোড একটু বেশি; কিন্তু leakage নেই
শ্রেণি বা গ্রাহক ধরে ভাগ (stratified, group)বিরল শ্রেণি, বা একই গ্রাহকের অনেক রিভিউrandom_split এটা করে না; scikit-learn-এর splitter দিয়ে index বানিয়ে Subset

আমরা দ্বিতীয় পথ নিয়েছি। ৩০টির test সেটে অভিযোগ মাত্র কয়েকটি থাকতে পারে, তাই বাস্তব কাজে stratified ভাগ ভালো; আর একই গ্রাহক বা প্রায় একই লেখা দুই ভাগে পড়লে নকল-ভালো ফল আসে। এখানে অনেক রিভিউ একই বাক্যাংশ থেকে তৈরি (যেমন অংশ ১-এর ১৪২ আর ০ নম্বর হুবহু এক লেখা), তাই এই বানানো ডেটায় test-এর ফল আশাবাদী হবে; বাস্তব ডেটায় ভাগ করার আগে নকল সারি খোঁজা জরুরি।

৬ · ছবির transforms.v2 আর IterableDataset — এক ঝলক

ছবির Dataset-এর __getitem__ সাধারণত ফাইল পড়ে, তারপর torchvision.transforms.v2-এর একটি pipeline চালায়: HWC uint8 array থেকে CHW tensor, random crop আর flip (training-এ data augmentation), float-এ রূপান্তর, normalisation. বাজারঘরের বদলে এখানে ১২টি ছোট কৃত্রিম “পণ্যের ছবি” (random pixel, আকার ৪০ × ৪৮). ছবি যে array, তা AI-এর ভিত্তি কোর্সের পর্ব ১৮-এ দেখেছেন; augmentation-এর পুরো আলোচনা পর্ব ২৪-এ।

import numpy as np, torch, torchvision
from torchvision.transforms import v2
from torch.utils.data import Dataset, DataLoader
print("torchvision", torchvision.__version__)

rng = np.random.default_rng(3)                       # ছোট কৃত্রিম "পণ্যের ছবি"
imgs = rng.integers(0, 256, size=(12, 40, 48, 3), dtype=np.uint8)   # N, H, W, C
labels = rng.integers(0, 3, size=12)

train_tf = v2.Compose([
    v2.ToImage(),                                    # HWC uint8 -> CHW tensor
    v2.RandomResizedCrop((32, 32), scale=(0.6, 1.0)),
    v2.RandomHorizontalFlip(p=0.5),
    v2.ToDtype(torch.float32, scale=True),           # 0..255 -> 0..1
    v2.Normalize(mean=[0.5] * 3, std=[0.25] * 3),
])

class ProductImages(Dataset):
    def __init__(self, imgs, labels, tf): self.imgs, self.labels, self.tf = imgs, labels, tf
    def __len__(self): return len(self.imgs)
    def __getitem__(self, i): return self.tf(self.imgs[i]), int(self.labels[i])

torch.manual_seed(0)                                 # v2-র random কাজ global RNG নেয়
ds = ProductImages(imgs, labels, train_tf)
x, y = ds[0]
print("আগে :", imgs[0].shape, imgs[0].dtype, imgs[0].min(), imgs[0].max())
print("পরে :", tuple(x.shape), x.dtype, round(x.min().item(), 3), round(x.max().item(), 3))
a, b = ds[0][0], ds[0][0]
print("একই ছবি দুবার পড়লে একই?", torch.equal(a, b))
xb, yb = next(iter(DataLoader(ds, batch_size=4)))
print("batch:", tuple(xb.shape), xb.dtype, "| labels", yb.tolist())
~/bazarghar-dl$ uv run python images_demo.py
torchvision 0.29.2+cpu
আগে : (40, 48, 3) uint8 0 255
পরে : (3, 32, 32) torch.float32 -1.639 1.702
একই ছবি দুবার পড়লে একই? False
batch: (4, 3, 32, 32) torch.float32 | labels [0, 0, 0, 1]

আকার (40, 48, 3) uint8 থেকে (3, 32, 32) float32: channel সামনে এল, crop ৩২ × ৩২-এ নামাল। (x/255 − 0.5)/0.25-এর তাত্ত্বিক সীমা −২ থেকে ২, কিন্তু resize পাশের pixel মিশিয়ে চরম মান কমায়, তাই −১.৬৪ থেকে ১.৭০. একই ছবি দুবার পড়লে আলাদা ফল: augmentation প্রতিটি __getitem__-এ নতুন করে random, তাই প্রতি epoch-এ model একটু ভিন্ন ছবি দেখে। এজন্য validation-এর transform আলাদা রাখতে হয়, random অংশ ছাড়া। Default collate এখানে চলেছে, কারণ সব ছবি এখন একই আকারের।

IterableDataset — যখন index নেই, শুধু stream

ডেটা যদি একটি বিশাল ফাইল বা চলমান উৎস হয়, যেখানে “১৪২ নম্বরটি দিন” বলা যায় না, তখন IterableDataset: __getitem__-এর বদলে __iter__, যা একটির পর একটি নমুনা yield করে (Python কোর্সের পর্ব ৩৬-এর generator). দুটি ফাঁদ নিচের run-এ:

import torch
from torch.utils.data import IterableDataset, DataLoader, get_worker_info

class ReviewStream(IterableDataset):          # যেমন: বড় ফাইল বা নেটওয়ার্ক থেকে লাইন ধরে
    def __init__(self, n, shard=True): self.n, self.shard = n, shard
    def __iter__(self):
        info = get_worker_info()
        start, step = 0, 1
        if self.shard and info is not None:   # প্রতিটি worker আলাদা ভাগ পড়ে
            start, step = info.id, info.num_workers
        for i in range(start, self.n, step):
            yield torch.tensor(i)

if __name__ == "__main__":
    for shard in (False, True):
        dl = DataLoader(ReviewStream(8, shard), batch_size=4, num_workers=2)
        print(f"shard={shard}:", [b.tolist() for b in dl])
    try:
        DataLoader(ReviewStream(8), batch_size=4, shuffle=True)
    except ValueError as e:
        print("ValueError:", e)
~/bazarghar-dl$ uv run python stream_demo.py
shard=False: [[0, 1, 2, 3], [0, 1, 2, 3], [4, 5, 6, 7], [4, 5, 6, 7]]
shard=True: [[0, 2, 4, 6], [1, 3, 5, 7]]
ValueError: DataLoader with IterableDataset: expected unspecified shuffle option, but got shuffle=True

দুটি worker-এর প্রতিটি Dataset-এর নিজের কপি পায়; ভাগ না করলে দুজনেই পুরো stream পড়ে, আর প্রতিটি নমুনা দুবার আসে, কোনো error ছাড়াই। get_worker_info() দিয়ে worker নিজের ভাগ বেছে নিলে প্রতিটি একবার। আর IterableDataset-এ shuffle=True চলে না; stream-এ এলোমেলো করতে সাধারণত একটি ছোট buffer-এ জমিয়ে তার ভেতরে মেশানো হয়, যা পুরো shuffle নয়।

৭ · num_workers, Windows-এর __main__ পাহারা আর মাপা গতি

num_workers=0 হলে মূল process নিজেই প্রতিটি নমুনা আনে; model-এর হিসাব আর ডেটা আনা পালা করে চলে। num_workers=k হলে k-টি আলাদা worker process আগেভাগে batch বানিয়ে রাখে। Linux-এ বহুদিন default ছিল fork (মূল process-এর কপি, দ্রুত; Python 3.14 থেকে Linux-এর default forkserver); Windows-এ (আর macOS-এও default) spawn: প্রতিটি worker একটি নতুন Python interpreter, যা আপনার মূল script আবার import করে। Script-এর একদম ওপরে DataLoader চালানোর কোড থাকলে প্রতিটি worker নিজেও worker বানাতে চায়। পাহারা ছাড়া একটি script চালানোর সত্যিকারের ফল (path আর pid ছাঁটা):

~/bazarghar-dl$ uv run python noguard.py      # num_workers=2, if __name__ নেই
Traceback (most recent call last):
  …
RuntimeError:
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.
        …
            if __name__ == '__main__':
                freeze_support()
  …
RuntimeError: DataLoader worker (pid(s) …) exited unexpectedly

সমাধান: DataLoader তৈরি আর লুপ if __name__ == "__main__":-এর ভেতরে, আর Dataset class আর collate_fn module-এর ওপরের স্তরে (worker যেন import করে পায়; lambda বা ভেতরের function spawn-এ pickle হয় না)। Process আর thread-এর পার্থক্য Python কোর্সের পর্ব ৪৪-এ। Jupyter notebook-এ Windows-এ worker প্রায়ই এ কারণেই আটকায়; তখন Dataset একটি .py ফাইলে রেখে import করুন, বা num_workers=0.

এবার মাপা যাক worker আসলে কতটা সাহায্য করে। throughput.py-তে ৬৪০০টি নমুনা, batch ৬৪, দুই epoch; “হালকা” Dataset প্রায় কিছুই করে না, “ভারী” প্রতি নমুনায় একটি Python লুপ চালায় (টেক্সট পরিষ্কার আর tokenize-এর খরচের নকল)। প্রতিটি epoch-এ প্রথম batch আসার সময় আর মোট সময় মাপা হলো; persistent_workers=True হলে worker epoch-এর শেষে বন্ধ হয় না।

import time, torch
from torch.utils.data import Dataset, DataLoader

class Reviews(Dataset):
    def __init__(self, n, work): self.n, self.work = n, work
    def __len__(self): return self.n
    def __getitem__(self, i):
        x = torch.full((64,), float(i % 97))
        s = 0
        for k in range(self.work):            # প্রতি সারিতে নকল "পরিষ্কার আর tokenize" খরচ
            s += (k * i) % 7
        return x + s, i % 2

def run(ds, nw, epochs=2, persistent=False):
    dl = DataLoader(ds, batch_size=64, shuffle=True, num_workers=nw,
                    persistent_workers=persistent and nw > 0,
                    generator=torch.Generator().manual_seed(0))
    out = []
    for _ in range(epochs):
        t0 = time.perf_counter(); first = None
        for b, (x, y) in enumerate(dl):
            if b == 0: first = time.perf_counter() - t0
        out.append((first, time.perf_counter() - t0))
    return out

if __name__ == "__main__":                    # Windows-এ worker spawn-এর জন্য বাধ্যতামূলক
    print("torch", torch.__version__)
    for name, work in [("হালকা", 0), ("ভারী", 20000)]:
        ds = Reviews(6400, work)
        for nw in (0, 2, 4):
            for pers in ((False, True) if nw else (False,)):
                (f1, t1), (f2, t2) = run(ds, nw, persistent=pers)
                tag = f"{name} nw={nw}" + (" persistent" if pers else "")
                print(f"{tag:<22} epoch1 প্রথম batch {f1:6.3f}s মোট {t1:6.2f}s"
                      f" | epoch2 প্রথম batch {f2:6.3f}s মোট {t2:6.2f}s")

সত্যিকারের run (ডেস্কটপ, Windows, CPU-তে; একটি run-এর মাপ, অন্য যন্ত্রে সংখ্যা আলাদা হবে):

~/bazarghar-dl$ uv run python throughput.py
torch 2.15.0+cpu
হালকা nw=0             epoch1 প্রথম batch  0.001s মোট   0.03s | epoch2 প্রথম batch  0.000s মোট   0.04s
হালকা nw=2             epoch1 প্রথম batch  1.342s মোট   1.66s | epoch2 প্রথম batch  1.336s মোট   1.66s
হালকা nw=2 persistent  epoch1 প্রথম batch  1.374s মোট   1.41s | epoch2 প্রথম batch  0.001s মোট   0.03s
হালকা nw=4             epoch1 প্রথম batch  1.375s মোট   1.74s | epoch2 প্রথম batch  1.469s মোট   1.81s
হালকা nw=4 persistent  epoch1 প্রথম batch  1.415s মোট   1.44s | epoch2 প্রথম batch  0.002s মোট   0.02s
ভারী nw=0              epoch1 প্রথম batch  0.041s মোট   4.10s | epoch2 প্রথম batch  0.040s মোট   4.08s
ভারী nw=2              epoch1 প্রথম batch  1.350s মোট   3.63s | epoch2 প্রথম batch  1.338s মোট   3.61s
ভারী nw=2 persistent   epoch1 প্রথম batch  1.345s মোট   3.34s | epoch2 প্রথম batch  0.042s মোট   2.13s
ভারী nw=4              epoch1 প্রথম batch  1.460s মোট   2.77s | epoch2 প্রথম batch  1.383s মোট   2.71s
ভারী nw=4 persistent   epoch1 প্রথম batch  1.403s মোট   2.41s | epoch2 প্রথম batch  0.056s মোট   1.09s

এই সংখ্যা থেকে তিনটি শিক্ষা। এক, worker চালু হওয়ার দাম: প্রতিটি epoch-এ worker নতুন করে spawn হলে প্রথম batch আসতে প্রায় ১.৩–১.৫ সেকেন্ড, কারণ প্রতিটি worker-এ Python আর torch আবার import হয়। দুই, হালকা ডেটায় worker ক্ষতি করে: ০.০৩ সেকেন্ডের কাজ ১.৭ সেকেন্ডে উঠল; টেবিলের মতো আগে থেকে tensor-এ রাখা ডেটায় num_workers=0-ই সবচেয়ে দ্রুত। তিন, ভারী ডেটায় worker সাহায্য করে, persistent হলে আরও: দ্বিতীয় epoch-এ ৪.০৮ থেকে ১.০৯ সেকেন্ড (চারটি persistent worker), প্রায় ৩.৭ গুণ throughput; non-persistent চারটি worker-এ ২.৭১, কারণ প্রতি epoch-এ আবার spawn-এর দাম। Linux-এ (fork বা forkserver) worker চালুর দাম সাধারণত কম বলে জানা যায়, তাই এই সংখ্যা শুধু এই Windows যন্ত্রের একটি run.

pin_memory=True batch-কে page-locked memory-তে রাখে, যাতে GPU-তে কপি দ্রুত আর non_blocking=True হয়। এই CPU build-এ চালালে PyTorch সতর্ক করে: UserWarning: 'pin_memory' argument is set as true but no accelerator is found, then device pinned memory won't be used. GPU-তে ডেটা পাঠানো আর ডেটা-লোডিং-এর bottleneck ধরার profiler পর্ব ১২-এ।

আরও গভীরে: PyTorch-এর ডকুমেন্টেশন map-style আর iterable-style dataset, sampler, batch_sampler আর worker-এর random seed-এর নিয়ম বিস্তারিত বর্ণনা করে (torch.utils.data). প্রতিটি worker-এ PyTorch-এর seed base_seed + worker id; লেখার সময়ের সংস্করণের worker কোড NumPy-র global seed-ও আলাদা করে বসায় (ডেস্কটপের একটি ছোট পরীক্ষায় দুই worker-এর np.random মান আলাদা এসেছে)। বড় মাপে data pipeline-এর নকশা নিয়ে একটি পরিচিত প্রবন্ধ Murray ও সহকর্মীদের “tf.data: A Machine Learning Data Processing Framework” (VLDB, ২০২১)। Hugging Face-এর datasets library memory-mapped Arrow ফাইলে বড় ডেটা রাখে; পর্ব ৫৩-এ।

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

  • N = ১০৫০, B = ৬৪: batch কয়টি? ⌈১০৫০/৬৪⌉ = ১৭, শেষটিতে ২৬; drop_last-এ ১৬.
  • দৈর্ঘ্য [৫, ২, ৭]-এর padding-এর পর ids আর mask-এর আকার? দুটিই (3, 7); mask-এ ১৪টি ১, ৭টি ০.
  • Map-style আর iterable-style-এর পার্থক্য? প্রথমটিতে __len__ আর __getitem__, index দিয়ে যেকোনো নমুনা; দ্বিতীয়টিতে __iter__, stream, shuffle আর worker-ভাগ নিজে সামলাতে হয়।
  • ৯০:১০ ডেটায় 1/nc ওজনে একটি বিরল সারি টানার সম্ভাবনা সংখ্যাগুরু সারির কত গুণ? ৯ গুণ (৯০/১০)।
  • Windows-এ num_workers > 0 দিলে script আটকায় বা RuntimeError দেয় কেন? spawn মূল script আবার import করে; DataLoader-এর কোড if __name__ == "__main__":-এর ভেতরে রাখতে হয়।

৮ · AI-এর যুগে — AI-এর লেখা data pipeline যাচাই

“আমার CSV থেকে একটি PyTorch DataLoader বানিয়ে দাও” বললে AI assistant সেকেন্ডে কোড দেয়, আর তা প্রায়ই চলে। কিন্তু data pipeline-এর বেশির ভাগ বাগ error দেয় না: ভুল vocab, pad-সহ গড়, দুবার পড়া stream, test-এ weighted sampler. Model শুধু একটু খারাপ শেখে, বা মাপা ফল একটু বেশি ভালো আসে।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
পুরো ডেটা থেকে vocab বা scaler বানিয়ে তারপর ভাগ করেtest-এর তথ্য train-এ ঢোকেকোডে ভাগের লাইনটি vocab-এর আগে কি না দেখুন
বাংলা টেক্সটে normalization ছাড়াই split()একই শব্দ দুই id পায়NFC-র আগে-পরে আলাদা শব্দ গুনুন
Padding করে, কিন্তু mask ফেরত দেয় না বা ব্যবহার করে নাগড় আর loss-এ pad ঢোকেএকটি batch-এ ছোট রিভিউয়ের ফল একা আর batch-এ মেলান
shuffle=True দেয়, Generator দেয় না; বা validation-এও shuffle আর augmentationক্রম পুনরুৎপাদন হয় না; মাপ এলোমেলোএকই seed-এ দুবার চালিয়ে প্রথম batch-এর index মেলান
Windows-এ num_workers=8, __main__ পাহারা নেই, collate_fn একটি lambdaRuntimeError, বা pickle errornum_workers=0-তে আগে ঠিক চলে কি না দেখুন, তারপর মেপে বাড়ান
পুরোনো API (যেমন torchvision.transforms-এর v1, বা সরানো argument)সংস্করণে না মেলানিজের torchvision সংস্করণের docs মিলিয়ে দেখুন

AI-কে শুধু কোড নয়, যাচাইসহ কোড চাইতে বলুন:

আমার কাছে বাংলা পণ্য-রিভিউ আর label আছে (প্রায় ১২% অভিযোগ)। PyTorch 2.x-এ একটি Dataset আর DataLoader লিখে দাও। ১. প্রথমে index ধরে train/val/test ভাগ করো (seed দেওয়া), তারপর শুধু train থেকে vocab বানাও; <pad> = 0, <unk> = 1. ২. টেক্সট NFC-তে normalize করো, আর কেন, এক লাইনে বলো। ৩. collate_fn-এ padding, lengths আর lengths থেকে বানানো attention mask দাও; প্রতিটি tensor-এর আকার আর dtype মন্তব্যে লেখো। ৪. Train-এ WeightedRandomSampler, val/test-এ sampler বা shuffle ছাড়া; shuffle-এর জন্য আলাদা torch.Generator. ৫. Windows-এ num_workers ব্যবহারের জন্য __main__ পাহারা দাও, আর একটি ছোট assert-ভিত্তিক পরীক্ষা লেখো যা একটি batch-এর আকার, mask-এর যোগফল আর লেবেলের ভাগ যাচাই করে। ৬. কোন অংশ তোমার মনে সবচেয়ে ভুল-প্রবণ, সেটাও বলো।

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

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

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

প্র ০১দামি GPU কেন প্রায়ই ডেটার জন্য অপেক্ষা করে বসে থাকে, আর framework-গুলো এর সমাধানে কোন পথে গেছে?

একটি training ধাপে দুটি কাজ: ডেটা আনা-বানানো (ফাইল পড়া, decode, augmentation, tokenize, padding) আর model-এর হিসাব। GPU যত দ্রুত হয়েছে, দ্বিতীয় কাজ তত ছোট হয়েছে, কিন্তু প্রথম কাজটি মূলত CPU আর ডিস্কে, আর Python-এ লেখা হলে তার গতি সহজে বাড়ে না। ফলে অনেক বাস্তব প্রকল্পে GPU-র ব্যবহার কম দেখায়, কারণ সে পরের batch-এর অপেক্ষায় থাকে। এই পর্বের মাপে হালকা ডেটায় পুরো epoch ০.০৩ সেকেন্ড, ভারী Python প্রক্রিয়ায় ৪ সেকেন্ড: model-এর আগেই ১০০ গুণের বেশি পার্থক্য।

Framework-গুলো তিন রকম সমাধান নিয়েছে। প্রথম, সমান্তরাল process: PyTorch-এর worker, যা Python-এর GIL এড়াতে আলাদা process ব্যবহার করে, আর prefetch করে কয়েকটি batch আগেভাগে বানিয়ে রাখে। দ্বিতীয়, graph আকারে input pipeline: TensorFlow-এর tf.data কাজগুলোকে (map, batch, prefetch, interleave) একটি graph হিসেবে নেয়, যাতে C++-এ সমান্তরাল চালানো আর স্বয়ংক্রিয়ভাবে মাত্রা ঠিক করা যায়; Murray ও সহকর্মীদের ২০২১ সালের VLDB প্রবন্ধে Google-এর ভেতরের বহু training job বিশ্লেষণ করে দেখানো হয়েছে যে input pipeline-এর খরচ অনেক job-এ উল্লেখযোগ্য। তৃতীয়, কাজ GPU-তে সরানো: ছবি decode আর augmentation GPU-তে করার library (যেমন NVIDIA-র DALI) আছে।

আরেকটি দিক হলো ফাইলের গঠন। লাখো ছোট ছবি আলাদা ফাইলে থাকলে ডিস্ক আর নেটওয়ার্কে প্রতিটি খোলার খরচ বেশি; তাই বড় প্রকল্পে ডেটা কয়েকটি বড় shard ফাইলে (tar, Arrow, record ফাইল) রাখা হয়, আর IterableDataset সেগুলো ক্রমানুসারে পড়ে। দাম: পুরো random shuffle সম্ভব হয় না, buffer-এর ভেতরে মেশানোই ভরসা।

শিক্ষার্থীর জন্য বাস্তব শিক্ষা: আগে মাপুন। একটি epoch শুধু DataLoader দিয়ে, model ছাড়া, চালিয়ে সময় নিন; তারপর model-সহ। দুটির পার্থক্য কম হলে সমস্যা model-এ নয়, ডেটায়।

মূল উপলব্ধি: দ্রুত model ডেটা দ্রুত না পেলে ধীর; data pipeline-কে training-এর সমান গুরুত্বের প্রকৌশল হিসেবে মাপতে আর নকশা করতে হয়।

প্র ০২Padding, bucketing আর packing — ভিন্ন দৈর্ঘ্যের টেক্সট সামলানোর পথগুলো কীভাবে বদলেছে, আর প্রতিটির দাম কী?

সবচেয়ে সরল পথ এই পর্বের: batch-এর সবচেয়ে লম্বা নমুনা পর্যন্ত pad, সঙ্গে mask. দাম দুটি। হিসাবের অপচয়: তিনটি রিভিউয়ে ৩৯% ঘর pad, আর একটি লম্বা রিভিউ পুরো batch-কে লম্বা করে। আরেকটি ঝুঁকি সঠিকতার: mask কোথাও ভুলে গেলে ফল batch-এর গঠনের ওপর নির্ভর করে, যেমন pad-সহ গড় একটি রিভিউয়ের চিহ্নই উল্টে দিয়েছিল।

RNN-এর যুগে, ২০১০-এর দশকের মাঝামাঝি sequence-to-sequence অনুবাদ model-এ, bucketing প্রচলিত ছিল: বাক্যগুলোকে দৈর্ঘ্য অনুযায়ী কয়েকটি ঝুড়িতে ভাগ করে প্রতিটি batch এক ঝুড়ি থেকে নেওয়া। TensorFlow-এর তখনকার seq2seq tutorial-এ নির্দিষ্ট দৈর্ঘ্যের bucket ব্যবহারের কথা ছিল বলে মনে করা হয়। PyTorch-এ RNN-এর জন্য pack_padded_sequence আছে, যা lengths দেখে pad অংশে হিসাবই চালায় না; এর জন্য collate_fn-এর lengths কাজে লাগে। Bucketing-এর দাম এলোমেলোতা কমা: একই দৈর্ঘ্যের নমুনা সবসময় একসঙ্গে থাকে, তাই bucket-এর ভেতরে আর batch-এর ক্রমে আলাদা shuffle লাগে।

Transformer-ভিত্তিক বড় language model-এর pretraining-এ আরেক পথ জনপ্রিয়: packing. অনেক ছোট লেখা পরপর জুড়ে, মাঝে একটি শেষ-চিহ্ন বসিয়ে, নির্দিষ্ট দৈর্ঘ্যের টুকরোয় কাটা; তাতে প্রায় কোনো pad থাকে না। দাম: এক লেখার token যেন পাশের লেখার দিকে attention না দেয়, তা নিশ্চিত করতে বিশেষ mask লাগে, নইলে model অসম্পর্কিত লেখা মিশিয়ে শেখে। এ নিয়ে বিভিন্ন গবেষণা দল ভিন্ন সিদ্ধান্ত নিয়েছে; কোনটি সবসময় ভালো, তা নিয়ে ঐকমত্য নেই। বিস্তারিত পর্ব ২৭ আর পর্ব ৪৭-এ।

বাংলার জন্য একটি বাড়তি কথা: শব্দ-স্তরের tokenization-এ বাংলা বাক্য কম token-এ ভাঙে, কিন্তু subword tokenizer-এ (পর্ব ৪০) একই বাক্য ইংরেজির চেয়ে অনেক বেশি token হতে পারে, ফলে padding আর দৈর্ঘ্যের সমস্যা আরও বড় হয়।

মূল উপলব্ধি: দৈর্ঘ্য সামলানো মানে সঠিকতা (mask) আর দক্ষতা (কম pad)-এর ভারসাম্য; যে পথই নিন, mask-এর আকার আর ব্যবহার আগে যাচাই করুন।

প্র ০৩Imbalanced ডেটায় deep learning-এ oversampling, loss-এ ওজন আর threshold বদলানো — কোনটি কখন, আর গবেষণা কী বলে?

তিনটি পথ একই লক্ষ্যে যায়, বিরল শ্রেণির ভুলকে বেশি গুরুত্ব দেওয়া, কিন্তু ভিন্ন জায়গায়। WeightedRandomSampler ডেটার স্তরে কাজ করে: বিরল সারি বেশিবার দেখায়। Loss-এ ওজন (যেমন CrossEntropyLoss-এর weight) প্রতিটি সারি একবারই দেখায়, কিন্তু বিরল শ্রেণির ভুলে বেশি শাস্তি দেয়। Threshold বদলানো training-এ কিছুই বদলায় না; শেষে সিদ্ধান্তের সীমা সরায়। মেশিন লার্নিং কোর্সের পর্ব ৫০-এ এগুলোর তুলনা আছে।

Deep learning-এর প্রেক্ষাপটে একটি প্রায়ই উদ্ধৃত গবেষণা Buda, Maki আর Mazurowski-র ২০১৮ সালের প্রবন্ধ (Neural Networks জার্নালে), যেখানে convolutional network-এ কৃত্রিমভাবে imbalance তৈরি করে কয়েকটি পদ্ধতি তুলনা করা হয়। তাদের পরীক্ষায় oversampling প্রায় সব ক্ষেত্রে ভালো কাজ করেছিল আর classical model-এর মতো স্পষ্ট overfitting দেখায়নি বলে তারা রিপোর্ট করেন। তবে এটি নির্দিষ্ট ডেটাসেট আর স্থাপত্যের ফল; সব সমস্যায় খাটবে, এমন দাবি করা যায় না।

দাম আর সতর্কতা। Oversampling-এ বিরল সারিগুলো বারবার আসে: এই পর্বের run-এ ২৩টি সারি এক epoch-এ ৯৮ বার। Augmentation না থাকলে model ঠিক সেই ২৩টি লেখা মুখস্থ করতে পারে। একই সঙ্গে সংখ্যাগুরু শ্রেণির অর্ধেকের বেশি সারি এক epoch-এ দেখা হয় না, তাই সমান সংখ্যক “ধাপ”-এ model কম বৈচিত্র্য দেখে। আরেকটি সূক্ষ্ম ব্যাপার: balanced sampler-এ শেখা model-এর সম্ভাবনা আসল ডেটার অনুপাতে calibrated নয়; output-এর “৫০% অভিযোগ” বাস্তবের ১২% ভিত্তি-হারে অনেক বেশি আশাবাদী। Calibration দরকার হলে পরে ঠিক করতে হয়।

বাস্তব পরামর্শ: আগে একটি baseline নিন (সাধারণ shuffle, ঠিক metric যেমন বিরল শ্রেণির recall বা PR-AUC), তারপর sampler আর loss weight আলাদা আলাদা চেষ্টা করুন, কয়েকটি seed-এ, val ভাগে আসল অনুপাত রেখে।

মূল উপলব্ধি: sampler ডেটার অনুপাত বদলায়, loss weight ভুলের দাম বদলায়, threshold সিদ্ধান্ত বদলায়; কোনটি কাজ করবে তা মেপে দেখতে হয়, আর মূল্যায়ন সবসময় আসল অনুপাতে।

প্র ০৪Worker-এর random seed নিয়ে একটি নীরব বাগ বহু প্রকল্পে ছিল বলে আলোচনা হয়েছিল — ঘটনাটি কী, আর এ থেকে কী শিখবেন?

২০২১ সালের দিকে একটি blog লেখা আর তার পরের অনলাইন আলোচনা ব্যাপক মনোযোগ পায়। দাবি ছিল: অনেক open-source PyTorch প্রকল্প augmentation-এ NumPy-র global random (np.random) ব্যবহার করত, আর তখনকার PyTorch worker-গুলোকে fork করার সময় NumPy-র অবস্থা হুবহু কপি হতো। ফলে প্রতিটি worker একই “random” সংখ্যার ক্রম পেত, আর ভিন্ন worker-এর ছবিতে একই crop বা flip পড়ত। কোনো error নেই, শুধু augmentation যতটা বৈচিত্র্য দেওয়ার কথা, তার চেয়ে কম দিত। PyTorch-এর ফোরামে এর কয়েক বছর আগেও “workers generate the same random augmentations” ধরনের প্রশ্ন দেখা যায়, আর ডকুমেন্টেশনে worker_init_fn দিয়ে seed বসানোর পরামর্শ ছিল।

পরে PyTorch নিজের worker কোডে NumPy-র seed-ও প্রতিটি worker-এ আলাদা বসানো শুরু করে। এই পর্বের ডেস্কটপ সংস্করণের worker কোডে এর লাইন আছে, আর একটি ছোট পরীক্ষায় দুই worker-এর np.random মান আলাদা এসেছে। কোন সংস্করণ থেকে এই আচরণ, তা উৎসভেদে আলাদা লেখা পাওয়া যায়, তাই নিজের সংস্করণে যাচাই করাই নিরাপদ। অন্য library-র (যেমন নিজের বানানো random object) seed এখনো আপনার দায়িত্ব।

উল্টো দিকের বাগও আছে: প্রতি epoch-এ নতুন DataLoader বানিয়ে একই seed-এর Generator দিলে প্রতিটি epoch-এ হুবহু একই augmentation আর একই ক্রম আসে; ডেস্কটপের সেই পরীক্ষায় দুই epoch-এর মান অক্ষরে অক্ষরে এক এসেছিল। পুনরুৎপাদন চাই, কিন্তু epoch থেকে epoch-এ বৈচিত্র্যও চাই; তাই DataLoader একবার বানিয়ে Generator-কে এগিয়ে চলতে দেওয়াই ঠিক।

বড় শিক্ষা হলো random উৎসের হিসাব রাখা: কোন উৎস কোন কাজে (weight init, shuffle, augmentation, dropout), কোথায় seed বসে, process ভাগ হলে কী হয়। অংশ ২-এর stepper ঠিক এই কথাই বলে।

মূল উপলব্ধি: randomness-এর বাগ নীরব; seed বসানো যথেষ্ট নয়, প্রতিটি worker আর epoch আসলে ভিন্ন কিন্তু পুনরুৎপাদনযোগ্য সংখ্যা পাচ্ছে কি না ছোট পরীক্ষায় দেখে নিতে হয়।

প্র ০৫বাংলা টেক্সটের data pipeline-এ কোথায় কোথায় চুপচাপ ভুল ঢোকে, আর একটি ডেটাসেটের “পরিচয়পত্র” কেন দরকার?

বাংলা লিপি Unicode-এ একাধিক উপায়ে একই দেখতে লেখা যায়। এই পর্বে “য়”-এর দুই রূপ ২৮টি শব্দকে ২৬টিতে নামাল। বাস্তব ডেটায় আরও আছে: zero-width joiner আর non-joiner (যুক্তাক্ষর আর হসন্তের চেহারা বদলায়), পুরোনো non-Unicode font-এ লেখা টেক্সট যা কপি করলে উল্টাপাল্টা অক্ষর আসে, বাংলা আর ইংরেজি অঙ্ক, “।” আর “.”-এর মিশ্রণ, Banglish (রোমান হরফে বাংলা)। AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এ এগুলোর পরিষ্কার করা দেখেছেন। একটি সূক্ষ্ম কথা: Unicode-এর নিয়মে য়, ড়, ঢ় “composition exclusion” তালিকায়, তাই NFC এগুলোকে দুই code point-এ রাখে; অর্থাৎ NFC সবসময় “ছোট” রূপ বেছে নেয়, এই ধারণা বাংলায় খাটে না।

এর প্রতিটি ভুল DataLoader পর্যন্ত নীরবে পৌঁছায়: vocab ফুলে যায়, একই শব্দের উদাহরণ ভাগ হয়ে যায়, <unk> বাড়ে। কোনো error নেই, শুধু model খারাপ শেখে। তাই normalization Dataset-এর ভেতরে, train আর inference একই function দিয়ে, যাতে deploy-এর পরে আসা নতুন রিভিউও একই পথে যায়।

দ্বিতীয় দিক ডকুমেন্টেশন। Gebru ও সহকর্মীদের “Datasheets for Datasets” (২০১৮ সালে preprint, পরে Communications of the ACM-এ ২০২১ সালে) প্রস্তাব করে, প্রতিটি ডেটাসেটের সঙ্গে প্রশ্নের উত্তর লেখা থাকুক: কে কেন বানিয়েছে, কোন উৎস থেকে, কোন preprocessing হয়েছে, কোন গোষ্ঠী কম বা বেশি আছে। বাংলা ডেটায় এতে লেখা উচিত কোন normalization হয়েছে, কোন উপভাষা বা অঞ্চলের লেখা, Banglish রাখা হয়েছে কি না। এই পর্বের ২০০টি রিভিউয়ের datasheet-এ সবচেয়ে জরুরি লাইন: “সব লেখা বানানো, ১১টি বাক্যাংশ থেকে; নকল সারি অনেক, তাই test-এর ফল বাস্তবের মাপ নয়।”

মূল উপলব্ধি: বাংলা টেক্সটে pipeline-এর প্রথম ধাপ normalization, আর শেষ ধাপ লিখে রাখা আপনি কী করেছেন; দুটিই না থাকলে model-এর ফল বিশ্বাস করার ভিত্তি থাকে না।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। চাইলে অংশ ২ আর ৩-এর ব্লক বদলে যাচাই করুন।

  1. পূর্ণ numerical: বাজারঘরের ১০৫০টি রিভিউ, batch size ৬৪. (ক) প্রতি epoch-এ কয়টি batch, শেষটিতে কয়টি রিভিউ, আর drop_last=True হলে কয়টি batch ও কয়টি রিভিউ বাদ? (খ) একটি batch-এর চারটি রিভিউয়ের দৈর্ঘ্য [১২, ৫, ৯, ৩]: pad-এর পর ids আর mask-এর আকার, pad ঘরের সংখ্যা আর অনুপাত কত? (গ) এই চারটিকে দৈর্ঘ্য অনুযায়ী দুটি করে দুটি batch-এ ভাগ করলে মোট pad কত হয়?

    (ক) ৬৪ × ১৬ = ১০২৪, বাকি ২৬; তাই ⌈১০৫০/৬৪⌉ = ১৭টি batch, শেষটিতে ২৬টি। drop_last-এ ১৬টি batch, প্রতি epoch-এ ২৬টি রিভিউ বাদ (shuffle থাকলে প্রতি epoch-এ ভিন্ন ২৬টি)। (খ) Lmax = ১২, তাই ids আর mask দুটিই (4, 12), মোট ৪৮ ঘর; আসল token ১২ + ৫ + ৯ + ৩ = ২৯, pad ১৯টি, প্রায় ৩৯.৬%; mask-এর যোগফল ২৯. (গ) সাজালে [৩, ৫] আর [৯, ১২]: প্রথমটি (2, 5), pad ২; দ্বিতীয়টি (2, 12), pad ৩; মোট ৫, ১৯-এর জায়গায়। দাম: ছোট রিভিউ সবসময় ছোটদের সঙ্গে, তাই bucket-এর ক্রম আলাদা করে shuffle করতে হয়।

  2. সমালোচনা ও নকশা: একজন সহকর্মীর (বা AI-এর) কোড: পুরো ২০০টি রিভিউ থেকে vocab, তারপর random_split(ds, [140, 30, 30]) Generator ছাড়া; train-এ DataLoader(train, batch_size=32, shuffle=True, sampler=WeightedRandomSampler(...)); val-এও একই weighted sampler; Windows-এ num_workers=8, collate_fn=lambda b: ..., কোনো __main__ পাহারা নেই। কী কী সমস্যা, আর আপনি কীভাবে লিখবেন?

    ১, leakage: vocab পুরো ডেটা থেকে, তাই test-এর শব্দ আগে থেকে id পায়। ২, পুনরুৎপাদন নেই: Generator ছাড়া প্রতি run-এ নতুন ভাগ। ৩, ValueError: sampler আর shuffle=True একসঙ্গে দেওয়া যায় না (“sampler option is mutually exclusive with shuffle”). ৪, ভুল মাপ: val-এ weighted sampler আসল ১২%-কে প্রায় ৫০% বানায় আর একই সারি বারবার গোনে; val-এ shuffle বা sampler কিছুই নয়। ৫, Windows: পাহারা ছাড়া spawn-এ RuntimeError; lambda spawn-এ pickle হয় না; আর ২০০ সারির হালকা ডেটায় ৮টি worker শুধু চালুর দাম বাড়ায় (এই পর্বের মাপে প্রতি epoch-এ প্রায় ১.৩ সেকেন্ড)। নকশা: seed দেওয়া Generator-এ index ভাগ (সম্ভব হলে stratified) → শুধু train থেকে vocab → তিনটি Dataset → train-এ শুধু sampler (নিজের Generator-সহ), val/test-এ shuffle=False → collate_fn module-এর ওপরের স্তরে একটি সাধারণ function → if __name__ == "__main__":-এর ভেতরে লুপ → num_workers মেপে বাছাই।

  3. আউটপুট অনুমান করুন: অংশ ৩-এর pad_collate ধরে নিন। চালানোর আগে লিখুন কী ছাপবে।
    ds = TensorDataset(torch.arange(7))
    for d in (False, True):
        dl = DataLoader(ds, batch_size=3, drop_last=d)
        print(d, [len(b[0]) for b in dl], len(dl))
    batch = [(torch.tensor([5, 2, 9]), 1), (torch.tensor([7]), 0), (torch.tensor([4, 4]), 1)]
    ids, mask, lengths, y = pad_collate(batch)
    print(ids); print(mask); print(lengths, y)
    False [3, 3, 1] 3
    True [3, 3] 2
    tensor([[5, 2, 9],
            [7, 0, 0],
            [4, 4, 0]])
    tensor([[ True,  True,  True],
            [ True, False, False],
            [ True,  True, False]])
    tensor([3, 1, 2]) tensor([1, 0, 1])

    ৭টি নমুনা, batch ৩: ⌈৭/৩⌉ = ৩ (শেষটিতে ১), drop_last-এ ⌊৭/৩⌋ = ২. Padding-এ Lmax = ৩, তাই (3, 3); mask-এ ছয়টি True. খেয়াল করুন, mask এখানে bool (অংশ ২-এর run-এ .int() দিয়ে ছাপা হয়েছিল)। এই আউটপুট ডেস্কটপে চালিয়ে মেলানো।

  4. কোড লিখুন: NumPy-তে bucket_batches(lengths, batch_size, rng) লিখুন: দৈর্ঘ্য অনুযায়ী index সাজিয়ে batch_size করে কাটবে, তারপর batch-গুলোর ক্রম rng দিয়ে এলোমেলো করবে। দৈর্ঘ্য [১২, ৫, ৯, ৩, ৭, ৪], batch ২-এ মোট pad কত, আর সাধারণ ক্রমে (০-১, ২-৩, ৪-৫) কত?
    import numpy as np
    def bucket_batches(lengths, batch_size, rng):
        order = np.argsort(lengths, kind="stable")          # ছোট থেকে বড়
        batches = [order[i:i + batch_size] for i in range(0, len(order), batch_size)]
        rng.shuffle(batches)                                # batch-এর ক্রম এলোমেলো
        return batches
    
    L = np.array([12, 5, 9, 3, 7, 4])
    bs = bucket_batches(L, 2, np.random.default_rng(0))
    print([b.tolist() for b in bs], [L[b].tolist() for b in bs])
    print("pad:", sum(int(L[b].max() * len(b) - L[b].sum()) for b in bs))
    # [[2, 0], [3, 5], [1, 4]] [[9, 12], [3, 4], [5, 7]]
    # pad: 6

    সাজানো জোড়া [৩, ৪], [৫, ৭], [৯, ১২]: pad ১ + ২ + ৩ = ৬. সাধারণ ক্রমে [১২, ৫], [৯, ৩], [৭, ৪]: pad ৭ + ৬ + ৩ = ১৬. Batch-এর ক্রম rng দিয়ে মেশানো হলো, কিন্তু কোন রিভিউ কার সঙ্গে বসবে তা প্রতি epoch-এ একই; বাস্তবে প্রতিটি bucket-এর ভেতরেও আগে shuffle করা হয়, বা দৈর্ঘ্যে সামান্য random শব্দ যোগ করে সাজানো হয়।

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

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

ক্রমPyTorch-এ বাংলা রিভিউয়ের একটি batch তৈরির ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Seed দেওয়া Generator দিয়ে index ভাগ করা (train/val/test)
  2. শুধু train ভাগের NFC করা টেক্সট থেকে vocab বানানো
  3. Sampler থেকে একটি batch-এর index পাওয়া
  4. প্রতিটি index-এ Dataset-এর __getitem__ থেকে token id-এর তালিকা
  5. collate_fn-এ pad করে ids, lengths আর mask বানানো
  6. Batch tensor training loop-এ model-কে দেওয়া

একটি map-style PyTorch Dataset-এ ন্যূনতম কোন দুটি method থাকা দরকার?

  • __iter__ আর __next__
  • __len__ আর __getitem__
  • __getitem__ আর forward
  • __len__ আর collate_fn

তিনটি রিভিউয়ের দৈর্ঘ্য ৪, ২ আর ৭. Padding-এর পর ids-এর আকার আর attention mask-এ মোট কয়টি ১ (True)?

  • (3, 4), ১২টি ১
  • (3, 7), ২১টি ১
  • (3, 7), ১৩টি ১
  • (7, 3), ১৩টি ১

১০০০টি নমুনা, batch_size ৬৪, drop_last=False. প্রতি epoch-এ কয়টি batch, আর শেষ batch-এ কয়টি নমুনা?

  • ১৬টি, শেষটিতে ৪০
  • ১৫টি, শেষটিতে ৬৪
  • ১৬টি, শেষটিতে ৬৪
  • ১৭টি, শেষটিতে ৪

একটি ডেটায় ১৮০টি সাধারণ আর ২০টি বিরল সারি। WeightedRandomSampler-এ প্রতিটি সারির ওজন 1/(নিজের শ্রেণির সংখ্যা)। একটি বিরল সারি টানার সম্ভাবনা একটি সাধারণ সারির কত গুণ, আর প্রত্যাশিত শ্রেণি-ভাগ কত?

  • সমান, ভাগ ৯০:১০
  • ২০ গুণ, ভাগ ৫০:৫০
  • ৯ গুণ, ভাগ ৯০:১০
  • ৯ গুণ, ভাগ প্রায় ৫০:৫০

Windows-এ একটি PyTorch script-এর একদম ওপরে DataLoader(ds, num_workers=2) দিয়ে লুপ চালালে “An attempt has been made to start a new process before the current process has finished its bootstrapping phase” RuntimeError আসে। মূল কারণ কী?

  • Windows-এ DataLoader worker একেবারেই চলে না
  • spawn-এ worker মূল script আবার import করে, তাই if __name__ == "__main__" পাহারা লাগে
  • batch_size worker সংখ্যার গুণিতক নয়
  • pin_memory=True দেওয়া হয়নি

একজন ডেভেলপার torch.manual_seed(0) দিয়ে DataLoader-এ shuffle=True দিলেন (নিজের Generator ছাড়া)। পরে DataLoader তৈরির আগে একটি নতুন nn.Linear স্তর যোগ করলেন। সবচেয়ে সম্ভাব্য ফল কী?

  • ডেটার ক্রম হুবহু আগের মতো থাকে, কারণ seed একই
  • Shuffle বন্ধ হয়ে যায়
  • ডেটার ক্রম বদলে যায়; এড়াতে DataLoader-কে নিজের seed দেওয়া Generator দিতে হয়
  • RuntimeError আসে

বাংলা রিভিউয়ের একটি টেক্সট pipeline-এ vocabulary কোন ডেটা থেকে বানানো উচিত?

  • শুধু train ভাগ থেকে, ভাগ করার পরে
  • পুরো ডেটা থেকে, যাতে কোনো <unk> না থাকে
  • শুধু test ভাগ থেকে
  • train আর validation মিলিয়ে, test বাদে, ভাগের আগে

শব্দকোষ

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

মনে রাখুন

  1. ভাগাভাগি: Dataset = __len__ + __getitem__ (কী আর কতগুলো); DataLoader = sampler + batch + collate + worker (কোন ক্রমে, কতগুলো, কে আনবে)।
  2. বাংলা টেক্সট: NFC Dataset-এর ভেতরে; ভাগ আগে, vocab শুধু train থেকে; <pad> = ০, <unk> = ১.
  3. Batch-এর হিসাব: ⌈N/B⌉ batch, drop_last-এ ⌊N/B⌋; padding-এর পর ids আর mask (B, Lmax), mask-এর যোগফল = lengths-এর যোগফল; গড় আর loss mask দিয়ে।
  4. Seed: shuffle, sampler আর random_split-কে নিজের torch.Generator; val/test-এ shuffle, sampler বা random augmentation নয়।
  5. Worker: Windows-এ if __name__ == "__main__":; হালকা ডেটায় num_workers=0 প্রায়ই দ্রুত, ভারী ডেটায় worker আর persistent_workers সাহায্য করে; অনুমান নয়, মেপে বাছুন।

পরের পর্বে: পর্ব ১২ (GPU, mixed precision আর গতি — device, AMP আর profiling) ডেটা আর model-কে GPU-তে নেবে: একটি device চলক, ঠিকভাবে সময় মাপা, memory-র হিসাব, mixed precision আর profiler দিয়ে দেখা কোথায় সময় যায়, ডেটা-লোডিংসহ।

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

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