এই পর্বে যা শিখবেন
- Seed-ভেদে বিস্তার মাপা, আর একটি run দিয়ে সিদ্ধান্ত না নেওয়া
- Randomness-এর প্রতিটি উৎস,
seed_everything, worker-এর seed আর deterministic mode - Dataclass আর argparse দিয়ে config, তার hash থেকে run-id, আর প্রতিটি run-এ কী সেভ করবেন
- TensorBoard-এ metric লেখা আর ফাইল থেকে আবার পড়া
- Random search, Optuna-র pruning, আর দুই model-কে কয়েকটি seed-এ ন্যায্যভাবে তুলনা
মেশিন লার্নিং কোর্সের পর্ব ৪-এ একটি run-এর পাঁচ উপাদান (code, environment, ডেটা, config, seed), JSONL খাতা আর MLflow দেখেছেন; একই কোর্সের পর্ব ৪৯-এ Bayesian optimization, TPE আর successive halving. এই পর্ব সেগুলো আবার শেখাবে না। এখানে প্রশ্ন হলো, deep learning-এ কী আলাদা: weight-এর এলোমেলো শুরু, dropout, shuffle আর GPU-র যোগের ক্রম মিলে ফল অনেক বেশি নড়ে; একটি run মিনিট থেকে দিন নেয়, তাই খোঁজে খারাপ চেষ্টা আগেই থামাতে হয়; আর metric আসে প্রতি epoch-এ, তাই লগ লাগে সময় ধরে। এই কোর্সের পর্ব ১০-এর training loop আর checkpoint, আর পর্ব ১১-এর seed দেওয়া DataLoader এখানে ভিত্তি। পাতার তিনটি ব্লক NumPy আর pandas দিয়ে চলে (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড); PyTorch, TensorBoard আর Optuna-র সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে।
১ · একটি run কিছুই প্রমাণ করে না — seed-ভেদে বিস্তার
একজন ব্যাটসম্যান এক ইনিংসে ৯০ করলেন। তিনি কি দলের সেরা? ক্রিকেটের স্কোরকার্ড এক ইনিংস দেখে বিচার করে না, দেখে গড় আর ধারাবাহিকতা। Neural network-এর একটি training run-ও একটি ইনিংস: শুরুর weight, batch-এর ক্রম আর dropout-এর mask ভাগ্যের ওপর নির্ভর করে। Seed বদলালেই একই model, একই ডেটায় আলাদা স্কোর।
নিচের ব্লকে বাজারঘরের customer segment ডেটা (পর্ব ১০-এর একই নিয়মে বানানো: ৫টি feature, ৩টি segment, ১০% label noise), ৫ → ৮ → ৩-এর একটি ছোট MLP, mini-batch SGD আর ৩০ epoch. Seed শুধু শুরুর weight আর shuffle ঠিক করে; ডেটা আর validation ভাগ সব run-এ এক। ১০টি seed-এ চালিয়ে দেখি accuracy কতটা নড়ে, আর গড়টি কতটা নিশ্চিত, তা bootstrap দিয়ে মাপি (১০টি সংখ্যা থেকে ফেরত রেখে বারবার ১০টি নিয়ে গড়)।
import numpy as np, matplotlib.pyplot as plt
rng = np.random.default_rng(7) # বাজারঘরের segment ডেটা, স্থির
y = rng.integers(0, 3, 600)
mu = np.array([[1.5, 900, 40, .20, .3], [6, 1400, 6, .15, .7], [4, 650, 12, .65, .6]])
X = mu[y] + rng.normal(0, 1, (600, 5)) * np.array([2.2, 520, 25, .25, .32])
y = np.where(rng.random(600) < .1, rng.integers(0, 3, 600), y) # ১০% label noise
X = (X - X[200:].mean(0)) / X[200:].std(0)
Xva, yva, Xtr, ytr = X[:200], y[:200], X[200:], y[200:]
def train(seed, h=8, lr=0.1, epochs=30):
r = np.random.default_rng(seed) # শুরুর weight আর shuffle: শুধু এই seed
W1 = r.normal(0, np.sqrt(2 / 5), (5, h)); b1 = np.zeros(h)
W2 = r.normal(0, np.sqrt(1 / h), (h, 3)); b2 = np.zeros(3)
for _ in range(epochs):
for idx in np.array_split(r.permutation(400), 13):
x, t = Xtr[idx], ytr[idx]
a = np.maximum(x @ W1 + b1, 0); z = a @ W2 + b2
p = np.exp(z - z.max(1, keepdims=True)); p /= p.sum(1, keepdims=True)
p[np.arange(len(t)), t] -= 1; g = p / len(t) # dL/dz = P − Y
ga = (g @ W2.T) * (a > 0)
W2 -= lr * a.T @ g; b2 -= lr * g.sum(0); W1 -= lr * x.T @ ga; b1 -= lr * ga.sum(0)
z = np.maximum(Xva @ W1 + b1, 0) @ W2 + b2
return (z.argmax(1) == yva).mean()
acc = np.array([train(s) for s in range(10)])
print("seed 0–9-এর val accuracy:", acc)
m, sd = acc.mean(), acc.std(ddof=1)
boot = np.random.default_rng(0).choice(acc, (5000, 10)).mean(1); lo, hi = np.percentile(boot, [2.5, 97.5])
print(f"mean ± sd = {m:.4f} ± {sd:.4f} | সর্বনিম্ন {acc.min():.3f}, সর্বোচ্চ {acc.max():.3f}")
print(f"গড়ের bootstrap 95% interval: [{lo:.4f}, {hi:.4f}]")
fig, ax = plt.subplots(figsize=(6, 2.4))
ax.scatter(acc, np.zeros(10), s=60, alpha=.6, label="one run per seed")
ax.errorbar(m, .4, xerr=[[m - lo], [hi - m]], fmt="o", color="C3", capsize=5, label="mean, bootstrap 95% CI")
ax.set_yticks([]); ax.set_ylim(-.5, 1); ax.set_xlabel("validation accuracy"); ax.legend(loc="upper left", fontsize=8)
ax.set_title("Same model, same data, 10 seeds")আউটপুট (পাতার Python-এ চালানো):
seed 0–9-এর val accuracy: [0.8 0.81 0.81 0.775 0.82 0.81 0.805 0.805 0.815 0.8 ]
mean ± sd = 0.8050 ± 0.0122 | সর্বনিম্ন 0.775, সর্বোচ্চ 0.820
গড়ের bootstrap 95% interval: [0.7970, 0.8115]
একই model, একই ডেটা, তবু seed ৩-এ ০.৭৭৫ আর seed ৪-এ ০.৮২০: সাড়ে চার শতাংশের ফারাক, যা অনেক "নতুন পদ্ধতির উন্নতি"-র চেয়ে বড়। কেউ seed ৪ চালিয়ে "৮২%" লিখলে, আর আরেকজন seed ৩ চালিয়ে "৭৭.৫%", দুজনেই সত্যি বলছেন, কিন্তু কেউই model-এর আসল মান বলছেন না। গড় ০.৮০৫০, sd ০.০১২২; গড়ের bootstrap interval [০.৭৯৭০, ০.৮১১৫] অনেক সরু, কারণ গড় একটি run-এর চেয়ে অনেক স্থির। ছবিতে নীল বিন্দুগুলো একেকটি run, লাল দাগ গড় আর তার interval. (Validation-এ ২০০ জন, তাই accuracy ০.০০৫-এর ধাপে বদলায়।)
দুটি সংখ্যা দুটি আলাদা প্রশ্নের উত্তর। Standard deviation বলে একটি নতুন run কতটা নড়তে পারে; n বাড়লেও তা কমে না। Confidence interval বলে গড়টি কতটা নিশ্চিত; n বাড়লে সরু হয়। এর তত্ত্ব আর t-interval মেশিন লার্নিং কোর্সের পর্ব ৪-এ, আর bootstrap-এর খুঁটিনাটি পর্ব ২১-এ। Deep learning-এ বাড়তি কথা একটাই: এখানে seed-এর ভূমিকা অনেক বড়, কারণ শুরুর weight, প্রতিটি batch আর প্রতিটি dropout mask সবই এলোমেলো। বড় model-এর fine-tuning-এ শুধু seed বদলে ফলের বড় ফারাক প্রকাশিত গবেষণাতেও দেখা গেছে (ভাবনার প্রশ্ন ১)।
২ · Randomness কোথা থেকে আসে — প্রতিটি seed
একটি PyTorch run-এ এলোমেলো সংখ্যা অনেক জায়গা থেকে আসে, আর প্রতিটির নিজের RNG state. একটি seed ভুলে গেলে বাকিগুলো দিয়েও run আবার একই হয় না। নিচের stepper প্রতিটি উৎস, তার seed দেওয়ার লাইন, আর বাদ দিলে আমাদের সত্যিকারের run-এ কী হলো (অংশ ৩-এ পুরো আউটপুট) দেখায়।
| উৎস | |
| লাইন | |
| কে ব্যবহার করে | |
| বাদ দিলে |
- Python-এর random. নিজের লেখা augmentation বা
random.shuffleএটি ব্যবহার করে।PYTHONHASHSEED(set-এর ক্রম ঠিক করে) চালু process-এ বদলানো যায় না, শুধু নতুন process-এ কাজ করে। - NumPy-র global RNG.
np.random.rand-এর মতো পুরোনো ধাঁচের ডাক এখান থেকে নেয়। নিজেরdefault_rng(seed)Generator থাকলে তার seed আলাদাভাবে দিতে হয়। - torch.
torch.manual_seedCPU আর সব GPU-র generator একসঙ্গে seed করে। Weight-এর শুরু আর dropout-এর mask এখান থেকে। আমাদের run-এ seed ছাড়া দুবার চালিয়ে val loss ০.৬৬৮০৭৫ আর ০.৬৯০১৮৮. - DataLoader.
shuffle=Trueনিজেরgeneratorপেলে batch-এর ক্রম global RNG থেকে আলাদা হয়; তখন model-এ একটি layer যোগ করলেও shuffle বদলায় না। পর্ব ১১-এ এর বিস্তারিত। - Worker. PyTorch প্রতিটি worker-এ
random, torch আর NumPy-র global RNG নিজেই আলাদা seed দেয়: base seed + worker id. কিন্তু Dataset-এর ভেতরে রাখা নিজের NumPy Generator প্রতিটি worker-এ হুবহু কপি হয়, তাই দুই worker একই "এলোমেলো" সংখ্যা দেয়। প্রতিকারworker_init_fn. - GPU. কিছু GPU kernel ফল যোগ করে atomic অপারেশনে, যার ক্রম প্রতিবার আলাদা;
cudnn.benchmark=Trueপ্রতিবার সময় মেপে convolution algorithm বাছে। Deterministic mode এগুলো বন্ধ করে, বা যার deterministic রূপ নেই সেখানে error দেয়। - এরপরও. Floating point যোগে (a + b) + c আর a + (b + c) শেষ বিটে আলাদা হতে পারে। Thread-এর সংখ্যা যোগের ক্রম বদলায়: একই seed-এ ১ thread বনাম default-এ ৪২,৭২৯টি weight আলাদা, সর্বোচ্চ ৫.৯৬ × ১০−৮. ভিন্ন hardware বা library version-এ বিট ধরে মিলের নিশ্চয়তা নেই।
বিট ধরে একই (একই যন্ত্র, একই version): debug আর resume-এর জন্য দরকার, seed আর deterministic mode দিয়ে পাওয়া যায়। পরিসংখ্যানে একই (অন্য যন্ত্র, অন্য seed): বৈজ্ঞানিক দাবির জন্য দরকার, শুধু কয়েকটি seed-এর গড় আর interval দিয়ে পাওয়া যায়। প্রথমটি দ্বিতীয়টির বিকল্প নয়।
৩ · seed_everything আর deterministic algorithms — সত্যিকারের run
সব seed এক জায়গায় দেওয়ার প্রচলিত নাম seed_everything (কিছু library-তে এই নামে function আছে; এখানে নিজেই লিখছি)। ডেস্কটপে একটি uv project (uv init --vcs none, তারপর uv add; লেখার সময়ে torch 2.15.0+cpu, numpy 2.5.4, Python 3.14.4):
import os, random, numpy as np, torch
def seed_everything(seed: int, deterministic: bool = False) -> None:
random.seed(seed) # Python-এর random
np.random.seed(seed) # NumPy-র global RNG
torch.manual_seed(seed) # CPU আর সব GPU-র torch RNG
os.environ["PYTHONHASHSEED"] = str(seed) # শুধু নতুন subprocess-এ কাজ করে
if deterministic:
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8") # GPU cuBLAS-এর জন্য
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False
def seed_worker(worker_id): # DataLoader(worker_init_fn=seed_worker)
s = torch.initial_seed() % 2**32 # PyTorch এই worker-কে যে seed দিয়েছে
np.random.seed(s); random.seed(s)determinism.py বাজারঘরের ডেটায় ৫ → ২৫৬ → ২৫৬ → ৩-এর MLP (dropout ০.২, AdamW) ১০ epoch train করে, তারপর সব weight-এর SHA-256 hash-এর প্রথম ১২ অক্ষর আর val loss ছাপে। প্রতিটি লাইন একটি নতুন process:
~/bazarghar-dl$ for m in noseed noseed seed seed seed1thread det; do uv run python determinism.py $m; done
noseed threads=default weights 168d72b80538 | val loss 0.668075 acc 0.780
noseed threads=default weights 2294257a1b71 | val loss 0.690188 acc 0.765
seed threads=default weights d80ccd83dff3 | val loss 0.679952 acc 0.790
seed threads=default weights d80ccd83dff3 | val loss 0.679952 acc 0.790
seed1thread threads=1 weights 5af51bfd95e5 | val loss 0.679952 acc 0.790
det threads=default weights d80ccd83dff3 | val loss 0.679952 acc 0.790
~/bazarghar-dl$ uv run python det_extras.py
threads default vs 1: equal=False | আলাদা weight 42729/68099 | সর্বোচ্চ পার্থক্য 5.96e-08
cudnn defaults: benchmark False | deterministic False
deterministic algorithms: False
deterministic algorithms: True
torch.empty(3) এখন: tensor([nan, nan, nan])
CPU index_add_ deterministic mode-এ: tensor([2., 1., 0., 0., 0.])
শিক্ষা চারটি। (১) Seed ছাড়া দুটি run-এর accuracy ০.৭৮০ আর ০.৭৬৫: শুধু ভাগ্যে দেড় শতাংশ। (২) Seed দিলে দুটি আলাদা process-এর weight hash হুবহু এক। (৩) একই seed, শুধু thread ১টি: hash আলাদা, অথচ val loss ছয় দশমিক পর্যন্ত এক; পার্থক্য শেষ বিটে, কিন্তু লম্বা training-এ এমন ছোট পার্থক্য বড় হতে পারে। (৪) CPU-তে deterministic mode ফল বদলায়নি, কারণ এই কাজগুলো CPU-তে আগেই deterministic. তবে mode-টি আসলে চালু: torch.empty এখন খালি memory-র এলোমেলো মানের বদলে NaN দিয়ে ভরা (PyTorch-এর fill_uninitialized_memory নিয়ম), যাতে শুরু না করা memory-র ওপর নির্ভরশীল bug চোখে পড়ে।
Worker-এর ফাঁদটিও সত্যিকারের run-এ (Windows-এ worker নতুন process হিসেবে শুরু হয়; batch ২, worker ২; প্রতিটি সারি (index, worker, noise)):
~/bazarghar-dl$ uv run python workers.py global # np.random-এর global RNG
global | (index, worker, noise): (0,0,-0.370) (1,0,+1.108) (2,1,-0.557) (3,1,+0.223) …
~/bazarghar-dl$ uv run python workers.py own # Dataset-এর ভেতরে default_rng(0)
own | (index, worker, noise): (0,0,+0.126) (1,0,-0.132) (2,1,+0.126) (3,1,-0.132) (4,0,+0.640) (5,0,+0.105) (6,1,+0.640) (7,1,+0.105)
~/bazarghar-dl$ uv run python workers.py fixed # worker_init_fn-এ নতুন Generator
fixed | (index, worker, noise): (0,0,-1.220) (1,0,+0.670) (2,1,+0.237) (3,1,+0.098) …
প্রতিটি mode দুবার চালানো হয়েছে, আর দুবারই হুবহু একই সারি এসেছে। "own" সারিতে দেখুন: worker ০-এর +০.১২৬, −০.১৩২ ঠিক worker ১-এও। অর্থাৎ augmentation-এর অর্ধেক বৈচিত্র্য চুপচাপ হারিয়ে গেছে, কোনো error ছাড়া। Worker-এর ভেতরে torch.initial_seed() থেকে নতুন Generator বানালে ("fixed") প্রতিটি worker আলাদা, আর run আবার চালালে একই।
GPU-তে (এখানে চালানো হয়নি, PyTorch-এর "Reproducibility" নোট অনুযায়ী): use_deterministic_algorithms(True) দিলে যে কাজের deterministic রূপ নেই তা RuntimeError দেয়, আর কিছু কাজ ধীর বিকল্প নেয়; CUDA 10.2 বা পরের cuBLAS-এ CUBLAS_WORKSPACE_CONFIG (:4096:8 বা :16:8) না থাকলেও error আসে। cudnn.benchmark=True গতি বাড়াতে পারে, কিন্তু প্রতিবার আলাদা algorithm বাছতে পারে। তাই নিয়ম: debug আর বিট ধরে মেলানোর সময় deterministic; বড় experiment-এ গতি, আর ভরসা কয়েকটি seed-এর গড়ে।
৪ · Config — dataclass, argparse আর YAML
Deep learning-এর একটি run-এ hyperparameter অনেক: learning rate, hidden-এর মাপ, dropout, weight decay, batch size, epoch. সেগুলো কোডের মাঝে ছড়িয়ে থাকলে কোন run কোন মানে চলেছিল তা হারিয়ে যায়। সমাধান একটি config বস্তু: সব মান এক জায়গায়, type-সহ। Python-এর dataclass এর জন্য সহজ; frozen=True দিলে run চলার মাঝে কেউ মান বদলাতে পারে না। তার field থেকেই argparse-এর flag বানালে command line থেকে বদলানো যায়, আর YAML ফাইল থেকে মান পড়া যায়:
import argparse, hashlib, json, yaml
from dataclasses import dataclass, asdict, fields
@dataclass(frozen=True)
class Config:
lr: float = 3e-3
hidden: int = 64
dropout: float = 0.2
weight_decay: float = 1e-2
batch_size: int = 64
epochs: int = 20
seed: int = 0
def parse(argv=None) -> Config:
p = argparse.ArgumentParser(description="বাজারঘর segment MLP")
p.add_argument("--config", help="YAML ফাইল; CLI-র মান তার ওপরে বসে")
for f in fields(Config): # প্রতিটি field থেকে একটি --flag
p.add_argument(f"--{f.name}", type=f.type, default=None)
a = vars(p.parse_args(argv))
base = yaml.safe_load(open(a.pop("config"), encoding="utf-8")) if a.get("config") else {}
merged = {**asdict(Config()), **base, **{k: v for k, v in a.items() if v is not None}}
return Config(**merged) # অজানা key থাকলে এখানে TypeError
def config_id(cfg: Config) -> str: # seed বাদে সব: একই config-এর সব seed এক দলে
d = {k: v for k, v in asdict(cfg).items() if k != "seed"}
return hashlib.sha256(json.dumps(d, sort_keys=True).encode()).hexdigest()[:8]
def run_id(cfg: Config) -> str:
return f"{config_id(cfg)}-s{cfg.seed}"অগ্রাধিকারের ক্রম: default, তার ওপর YAML, তার ওপর command line. সত্যিকারের run (PyYAML 6.0.3):
~/bazarghar-dl$ uv run python config.py
Config(lr=0.003, hidden=64, dropout=0.2, weight_decay=0.01, batch_size=64, epochs=20, seed=0)
config_id: 2c6a83d9 | run_id: 2c6a83d9-s0
~/bazarghar-dl$ uv run python config.py --seed 3
Config(lr=0.003, hidden=64, dropout=0.2, weight_decay=0.01, batch_size=64, epochs=20, seed=3)
config_id: 2c6a83d9 | run_id: 2c6a83d9-s3
~/bazarghar-dl$ uv run python config.py --config small_lr.yaml --dropout 0.1 # YAML-এ lr 0.001, hidden 128
Config(lr=0.001, hidden=128, dropout=0.1, weight_decay=0.01, batch_size=64, epochs=20, seed=0)
config_id: c72d8ef1 | run_id: c72d8ef1-s0
~/bazarghar-dl$ uv run python config.py --lr abc
config.py: error: argument --lr: invalid float value: 'abc'
~/bazarghar-dl$ uv run python config.py --hiden 32
config.py: error: unrecognized arguments: --hiden 32
~/bazarghar-dl$ uv run python config.py --config typo.yaml # YAML-এ "hiden: 128"
TypeError: Config.__init__() got an unexpected keyword argument 'hiden'. Did you mean 'hidden'?
শেষ তিনটি লাইনই আসল লাভ: ভুল type আর বানান ভুল run শুরুর আগেই ধরা পড়ল। Dict-ভিত্তিক config-এ cfg.get("hidden", 64) লিখলে "hiden" চুপচাপ উপেক্ষিত হয়, আর আপনি ভাবেন ১২৮-এ চালিয়েছেন। (argparse-এর একটি সূক্ষ্মতা: flag-এর শুরুর অংশ লিখলেও চলে, যেমন --hid; allow_abbrev=False দিলে বন্ধ হয়।) বড় প্রকল্পে Hydra-র মতো library একাধিক YAML জোড়া লাগানো, command line থেকে override আর একসঙ্গে অনেক config চালানোর কাজ করে; ধারণা একই, তাই প্রথমে এই ছোট রূপটি বুঝে নিন।
৫ · Run-id, ফোল্ডার আর প্রতিটি run-এ কী সেভ করবেন
config_id seed বাদ দিয়ে config-এর hash নেয়, তাই একই config-এর পাঁচটি seed পায় 2c6a83d9-s0 … -s4: পরে দল বাঁধা সহজ। মেশিন লার্নিং কোর্সের পর্ব ৪-এর cfg_id-এর সঙ্গে পার্থক্য এটুকুই, কিন্তু deep learning-এ এটাই কাজের, কারণ এখানে প্রতিটি config কয়েকটি seed-এ চলে। নিচের ছবিতে একটি run কী নেয় আর কী রেখে যায়:
একটি run-এর হিসাব — চারটি input, একটি ফোল্ডার
Code, config, data and seed go in; one folder per run comes out
নিচের ব্লক একই config_id পাতায় হিসাব করে। দেখুন desktop-এর id-গুলো (2c6a83d9, 742483ae) হুবহু ফেরে, কারণ JSON সাজানো আর SHA-256 সব যন্ত্রে এক। তারপর অংশ ৯-এর ১২টি সত্যিকারের run-এর ফল একটি টেবিলে, config ধরে দল বেঁধে।
import hashlib, json
import pandas as pd
def config_id(cfg): # seed বাদে সব key, সাজানো JSON-এর hash
d = {k: v for k, v in cfg.items() if k != "seed"}
return hashlib.sha256(json.dumps(d, sort_keys=True).encode()).hexdigest()[:8]
A = dict(lr=0.003, hidden=64, dropout=0.2, weight_decay=0.01, batch_size=64, epochs=20, seed=0)
B = {**A, "lr": 0.00088, "hidden": 128, "dropout": 0.1, "weight_decay": 4.4e-05}
for name, c in [("A", A), ("A, seed 3", {**A, "seed": 3}), ("A, lr=3e-3", {**A, "lr": 3e-3}),
("A, lr=0.0030001", {**A, "lr": 0.0030001}), ("B", B)]:
print(f"{name:<16} run_id {config_id(c)}-s{c['seed']}")
# ডেস্কটপের ১২টি সত্যিকারের run-এর best val loss (অংশ ৯-এর compare.py)
loss = {"A": [0.5854, 0.6065, 0.5881, 0.6021, 0.5865, 0.6226],
"B": [0.5783, 0.5970, 0.5899, 0.6058, 0.6011, 0.5993]}
rows = [{"run_id": f"{config_id(A if k == 'A' else B)}-s{s}", "config": k, "seed": s, "loss": v}
for k in loss for s, v in enumerate(loss[k])]
runs = pd.DataFrame(rows)
print(runs.iloc[[0, 1, 6, 7]].to_string(index=False))
table = runs[runs.seed > 0].groupby("config")["loss"].agg(["count", "mean", "std"])
print(table.round(4))আউটপুট (পাতার Python-এ চালানো):
A run_id 2c6a83d9-s0
A, seed 3 run_id 2c6a83d9-s3
A, lr=3e-3 run_id 2c6a83d9-s0
A, lr=0.0030001 run_id 5deae390-s0
B run_id 742483ae-s0
run_id config seed loss
2c6a83d9-s0 A 0 0.5854
2c6a83d9-s1 A 1 0.6065
742483ae-s0 B 0 0.5783
742483ae-s1 B 1 0.5970
count mean std
config
A 5 0.6012 0.0148
B 5 0.5986 0.0058
3e-3 আর 0.003 একই float, তাই একই id; কিন্তু 0.0030001 নতুন id পেল। এটা ইচ্ছাকৃত: মান সামান্য বদলালেও সেটি আলাদা config. সাবধান হওয়ার জায়গা উল্টোটা: YAML-এ hidden: 64.0 লিখলে JSON-এ 64.0 হয়, 64 নয়, তাই একই config নতুন id পায় (অনুশীলন ৩)। Dataclass-এর type-এ রূপান্তর করে নিলে এই সমস্যা কমে। টেবিলের B-এর std ০.০০৫৮ আর ডেস্কটপের ০.০০৫৯-এর ফারাক শুধু চার দশমিকে গোল করা মান থেকে। টেবিলে seed ০ বাদ, কারণ অংশ ৮-এর খোঁজ seed ০-তেই হয়েছিল; কেন, তা অংশ ৯-এ।
একটি ছোট experiment template. উপরের ধারণাগুলো একটি ফোল্ডারে (একটি পরামর্শ, নিয়ম নয়):
bazarghar-dl/
├── pyproject.toml, uv.lock # environment: হুবহু version
├── common.py # ডেটা, model, seed_everything
├── config.py # Config dataclass, parse(), run_id()
├── train.py # এক run: config → runs/<run_id>/
├── search.py, compare.py # Optuna খোঁজ, কয়েক seed-এ তুলনা
├── configs/b.yaml # নাম দেওয়া config, Git-এ
└── runs/ # Git-এ নয় (.gitignore)
└── 742483ae-s1/
├── config.yaml, meta.json, metrics.json
├── events.out.tfevents.…
└── best.pt
সত্যিকারের meta.json (প্রথম লাইনের commit এসেছে run-এর সময় git rev-parse --short HEAD থেকে; ফোল্ডারে না-commit-করা বদল থাকলে কোড +dirty যোগ করে):
~/bazarghar-dl$ uv run python train.py --config configs/b.yaml --seed 1
742483ae-s1 | best val loss 0.5970 | final val acc 0.800
~/bazarghar-dl$ cat runs/742483ae-s1/meta.json
{
"git": "01a56de",
"data": "1ed8cb9a7ea6",
"torch": "2.15.0+cpu",
"python": "3.14.4",
"start": "2026-11-06 20:41"
}
প্রতিটি run-এ যা রাখা দরকার: code-এর version (commit আর dirty চিহ্ন; Git-এর commit বোঝার জন্য সফটওয়্যার ইঞ্জিনিয়ারিং কোর্সের পর্ব ৮), ডেটার version (hash বা data versioning-এর tag), config, environment (lockfile আর version; AI-এর ভিত্তি কোর্সের পর্ব ১২), metric সময় ধরে, আর checkpoint (পর্ব ১০-এর পূর্ণ checkpoint, resume লাগলে)। ব্যর্থ আর থামানো run-ও রাখুন; না রাখলে পরে বলতে পারবেন না কতগুলো চেষ্টা থেকে বিজয়ী এসেছে।
৬ · TensorBoard — metric লেখা আর ফাইল থেকে ফেরত পড়া
Deep learning-এ একটি শেষ সংখ্যা যথেষ্ট নয়; loss কীভাবে নামল, কোথায় val loss উঠতে শুরু করল, তা দেখতে হয়। TensorBoard এর জন্য পরিচিত open-source টুল: training চলার সময় ছোট event ফাইল-এ (step, মান, সময়) লেখা হয়, আর একটি ছোট web server সেগুলো থেকে curve আঁকে। PyTorch-এ লেখার কাজ করে torch.utils.tensorboard.SummaryWriter (tensorboard package লাগে)। train.py-র মূল অংশ:
with SummaryWriter(log_dir=str(run_dir)) as tb: # pruned হলেও ফাইল বন্ধ হয়
for ep in range(1, cfg.epochs + 1):
tot = 0.0
for xb, yb in dl:
loss = torch.nn.functional.cross_entropy(model(xb), yb)
opt.zero_grad(); loss.backward(); opt.step(); tot += loss.item() * len(yb)
va_loss, va_acc = evaluate(model, Xva, yva)
tb.add_scalar("loss/train", tot / len(ytr), ep)
tb.add_scalar("loss/val", va_loss, ep); tb.add_scalar("acc/val", va_acc, ep)
if va_loss < best:
best = va_loss; torch.save({"model": model.state_dict(), "epoch": ep}, run_dir / "best.pt")
if report: report(va_loss, ep) # Optuna-র জন্য (অংশ ৮)Tag-এ "/" দিলে TensorBoard একই দলের curve এক জায়গায় রাখে। Server ছাড়াই ফাইলটি TensorBoard-এর নিজের EventAccumulator দিয়ে পড়া যায়; এভাবে script থেকে সেরা epoch বের করা বা পরীক্ষা লেখা সহজ। সত্যিকারের run (tensorboard 2.21.0):
~/bazarghar-dl$ uv run python tb_read.py runs/2c6a83d9-s0
21681 best.pt
94 config.yaml
2868 events.out.tfevents.1793975953.<host>.25144.0
127 meta.json
74 metrics.json
scalar tags: ['loss/train', 'loss/val', 'acc/val']
loss/val: 20 টি বিন্দু
step 1 value 0.8947
step 2 value 0.7185
step 3 value 0.6210
step 19 value 0.6452
step 20 value 0.6680
সবচেয়ে কম val loss: step 4, 0.5854
Event ফাইলের নামে থাকে সময়, যন্ত্রের নাম আর process id; এখানে যন্ত্রের নাম <host> দিয়ে ঢাকা। Run ফোল্ডার কাউকে পাঠানোর আগে এটি মনে রাখবেন। ২০ epoch-এর ৬০টি scalar মাত্র ২,৮৬৮ byte. Val loss epoch ৪-এ সবচেয়ে কম (০.৫৮৫৪), তারপর বাড়ে (epoch ২০-এ ০.৬৬৮০): overfitting-এর সেই পরিচিত ছবি, যা শুধু শেষ সংখ্যায় ধরা পড়ত না।
Curve দেখতে: tensorboard --logdir runs --host 127.0.0.1 --port 6127. সত্যিকারের run-এ এটি লিখল TensorBoard 2.21.0 at http://127.0.0.1:6127/, আর browser-এ সব run-এর curve পাশাপাশি এল; কাজ শেষে নিজের process id ধরে server বন্ধ করা হয়েছে। 127.0.0.1 মানে শুধু নিজের কম্পিউটার থেকে দেখা যাবে। অন্য টুল: MLflow (মেশিন লার্নিং কোর্সের পর্ব ৪-এ ব্যবহার করেছেন) param, metric, artifact আর model registry একসঙ্গে রাখে; Weights & Biases-এর মতো hosted সেবায় account লাগে আর আপনার metric (কখনো ডেটার নমুনাও) অন্যের server-এ যায়। কোনটি বাছবেন তার চেয়ে জরুরি: প্রতিটি run নিয়মিত লেখা হচ্ছে কি না।
৭ · Deep net-এ hyperparameter খোঁজ — grid নয়, random আর log scale
Grid search-এ প্রতিটি hyperparameter-এর কয়েকটি মান নিয়ে সব সমাহার চালানো হয়; random search-এ প্রতিবার প্রতিটি মান এলোমেলো টানা হয়। James Bergstra আর Yoshua Bengio ২০১২ সালে (JMLR) neural network-এর tuning-এ দেখান যে সাধারণত অল্প কয়েকটি hyperparameter-ই আসলে গুরুত্বপূর্ণ, আর কোনগুলো তা আগে জানা থাকে না। তখন একই বাজেটে random search গুরুত্বপূর্ণ মাত্রায় অনেক বেশি আলাদা মান পরীক্ষা করে। নিচের ব্লকে একটি কৃত্রিম "val loss" (learning rate গুরুত্বপূর্ণ, dropout প্রায় অপ্রাসঙ্গিক), ৯টি চেষ্টা:
import numpy as np, matplotlib.pyplot as plt
def val_loss(u, v): # u: log10(lr) গুরুত্বপূর্ণ; v: dropout প্রায় অপ্রাসঙ্গিক
return 0.95 - 0.37 * np.exp(-((u + 2.9) / 0.35) ** 2) + 0.01 * v
lo, hi = -4.0, -1.0 # lr 1e-4 থেকে 1e-1, log scale-এ
g = np.linspace(lo, hi, 3)
grid = np.array([(u, v) for u in g for v in (0.0, 0.25, 0.5)]) # 3 × 3 = 9 চেষ্টা
rng = np.random.default_rng(0)
rand = np.column_stack([rng.uniform(lo, hi, 9), rng.uniform(0, 0.5, 9)]) # 9 চেষ্টা
for name, pts in [("grid", grid), ("random", rand)]:
L = val_loss(*pts.T)
print(f"{name:<6}: lr-এর আলাদা মান {len(np.unique(pts[:, 0]))}টি | সেরা loss {L.min():.4f}"
f" (lr = {10 ** pts[L.argmin(), 0]:.1e})")
best = [val_loss(*np.column_stack([r.uniform(lo, hi, 9), r.uniform(0, .5, 9)]).T).min()
for r in (np.random.default_rng(s) for s in range(1, 201))]
win = np.mean(np.array(best) < val_loss(*grid.T).min())
print(f"random, ২০০ বার: সেরা loss-এর গড় {np.mean(best):.4f}; grid-এর চেয়ে ভালো {win:.0%} বার")
print(f"সত্যিকারের সর্বনিম্ন: {val_loss(-2.9, 0):.4f} (lr = {10 ** -2.9:.1e})")
u = np.linspace(lo, hi, 300)
fig, axes = plt.subplots(1, 2, figsize=(8, 3.2), sharey=True)
for ax, (name, pts) in zip(axes, [("Grid search, 9 trials", grid), ("Random search, 9 trials", rand)]):
ax.plot(u, 0.5 * (val_loss(u, 0) - 0.55) / 0.4, color="0.6", label="effect of lr (scaled)")
ax.scatter(pts[:, 0], pts[:, 1], s=50, color="C0", label="trial")
for x in np.unique(pts[:, 0]): ax.axvline(x, color="C1", lw=0.6, alpha=0.6)
ax.set_title(name); ax.set_xlabel("log10(learning rate): important")
axes[0].set_ylabel("dropout: unimportant"); axes[0].legend(fontsize=7, loc="upper left")আউটপুট (পাতার Python-এ চালানো):
grid : lr-এর আলাদা মান 3টি | সেরা loss 0.8498 (lr = 3.2e-03)
random: lr-এর আলাদা মান 9টি | সেরা loss 0.7684 (lr = 6.4e-04)
random, ২০০ বার: সেরা loss-এর গড় 0.6631; grid-এর চেয়ে ভালো 94% বার
সত্যিকারের সর্বনিম্ন: 0.5800 (lr = 1.3e-03)
Grid-এর ৯টি চেষ্টার মধ্যে learning rate মাত্র ৩টি (১০−৪, ১০−২.৫, ১০−১); বাকি ৬টি চেষ্টা অপ্রাসঙ্গিক dropout বদলাতে খরচ হলো, আর ভালো অঞ্চল (১০−৩-এর কাছে) দুটি grid রেখার মাঝে পড়ে গেল। Random search ৯টি আলাদা learning rate দেখে। একটি random খোঁজ ভাগ্যে খারাপও হতে পারে (seed ০-এর খোঁজটি গড়ের চেয়ে খারাপ), কিন্তু ২০০টি খোঁজের অধিকাংশ grid-কে হারায়। ছবিতে কমলা রেখাগুলো দেখায় learning rate-এর কোন কোন মান আসলে পরীক্ষা হয়েছে।
তিনটি ব্যবহারিক নিয়ম। (১) Learning rate আর weight decay-এর মতো মান log scale-এ টানুন: ১০−৪ থেকে ১০−১-এ সমান ভাগে টানলে প্রায় ৯০% চেষ্টা ১০−২-এর ওপরে পড়ে। (২) প্রথমে বড় পরিসরে কম epoch-এ মোটা খোঁজ, তারপর ভালো অঞ্চলে সরু খোঁজ। (৩) আগের ফল দেখে পরের বিন্দু বাছার পদ্ধতি (Bayesian optimization, TPE) আর অল্প সম্পদে অনেক চেষ্টা চালিয়ে খারাপগুলো ছাঁটা (successive halving, Hyperband) মেশিন লার্নিং কোর্সের পর্ব ৪৯-এ; scikit-learn-এর grid আর random search AI-এর ভিত্তি কোর্সের পর্ব ৬৬-এ। Deep learning-এ "সম্পদ" স্বাভাবিকভাবেই epoch, আর প্রতি epoch-এ val loss পাওয়া যায়; তাই চলমান trial-কে মাঝপথে থামানো যায়। সেটিই পরের অংশ। Learning rate কেন সবচেয়ে গুরুত্বপূর্ণ, আর LR range test, পর্ব ১৫ (Optimizer আর learning-rate schedule)-এর বিষয়।
৮ · Optuna আর MedianPruner — খারাপ trial আগেই থামানো
Optuna একটি open-source hyperparameter খোঁজের library (Takuya Akiba ও সহকর্মীরা, KDD ২০১৯)। প্রতিটি চেষ্টা একটি trial; trial নিজেই suggest_float বা suggest_categorical ডেকে মান নেয়, তাই search space কোডের ভেতরেই লেখা। Deep learning-এর জন্য মূল সুবিধা pruning: প্রতি epoch-এ trial.report(val_loss, epoch), তারপর trial.should_prune() সত্য হলে optuna.TrialPruned. MedianPruner-এর নিয়ম (Optuna 5.0.0-এর source থেকে যাচাই করা): এই trial-এর এখন পর্যন্ত সেরা val loss যদি একই epoch-এ শেষ হওয়া (COMPLETE) trial-গুলোর মানের median-এর চেয়ে খারাপ হয়, তবে trial থামানো হয়। n_startup_trials=5: প্রথম ৫টি trial শেষ না হলে কেউ থামে না; n_warmup_steps=3: epoch ৩-এর আগে কখনো নয়।
import optuna
from config import Config
from train import train
def objective(trial):
cfg = Config(lr=trial.suggest_float("lr", 1e-4, 1e-1, log=True),
hidden=trial.suggest_categorical("hidden", [16, 32, 64, 128, 256]),
dropout=trial.suggest_float("dropout", 0.0, 0.5),
weight_decay=trial.suggest_float("weight_decay", 1e-5, 1e-1, log=True),
epochs=20, seed=0) # খোঁজের সময় seed স্থির
def report(val_loss, epoch):
trial.report(val_loss, epoch) # প্রতিটি epoch-এর মাঝের ফল
if trial.should_prune(): # MedianPruner-এর সিদ্ধান্ত
raise optuna.TrialPruned()
best, acc = train(cfg, report=report)
return best
study = optuna.create_study(
study_name="segment-mlp", storage="sqlite:///optuna.db", # ফাইলে: থামলেও ফল থাকে
direction="minimize", sampler=optuna.samplers.TPESampler(seed=0),
pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=3))
study.optimize(objective, n_trials=30)সত্যিকারের run (optuna 5.0.0; ৩০টি trial, CPU-তে প্রায় ১৫ সেকেন্ড, একটি local run; প্রতিটি trial অংশ ৫-এর মতো নিজের run ফোল্ডারও লিখেছে):
~/bazarghar-dl$ uv run python search.py
trials 30 | complete 18 | pruned 12 | epochs চলল 399/600
# 0 COMPLETE ep 20 | best-so-far 0.6193 | lr 4.4e-03 h 16 drop 0.22 wd 4e-02
# 1 COMPLETE ep 20 | best-so-far 0.7959 | lr 7.8e-02 h 256 drop 0.04 wd 2e-05
# 2 COMPLETE ep 20 | best-so-far 0.6842 | lr 1.1e-04 h 128 drop 0.23 wd 1e-02
# 3 COMPLETE ep 20 | best-so-far 0.7494 | lr 2.3e-04 h 64 drop 0.13 wd 1e-02
# 4 COMPLETE ep 20 | best-so-far 0.5873 | lr 2.3e-03 h 64 drop 0.47 wd 5e-03
# 5 PRUNED ep 3 | best-so-far 0.9969 | lr 1.2e-03 h 32 drop 0.11 wd 3e-05
# 6 COMPLETE ep 20 | best-so-far 0.5782 | lr 8.8e-04 h 128 drop 0.10 wd 4e-05
# 7 COMPLETE ep 20 | best-so-far 0.6038 | lr 9.1e-03 h 32 drop 0.33 wd 4e-05
# 8 PRUNED ep 3 | best-so-far 0.8950 | lr 3.9e-04 h 128 drop 0.49 wd 7e-04
# 9 PRUNED ep 6 | best-so-far 0.6855 | lr 8.5e-02 h 32 drop 0.15 wd 3e-05
…
#23 PRUNED ep 3 | best-so-far 1.0162 | lr 1.0e-04 h 128 drop 0.42 wd 3e-03
…
#28 COMPLETE ep 20 | best-so-far 0.5801 | lr 1.2e-03 h 128 drop 0.33 wd 2e-02
#29 PRUNED ep 3 | best-so-far 0.7836 | lr 5.5e-04 h 128 drop 0.27 wd 3e-02
best: 6 0.5782 {'lr': 0.0008836597074694361, 'hidden': 128, 'dropout': 0.10443837804741735, 'weight_decay': 4.418125737902546e-05}
১২টি pruned trial-এর ১১টি epoch ৩-এ থেমেছে, একটি epoch ৬-এ: ৬০০ epoch-এর বাজেটের ২০১টি বাঁচল, প্রায় এক-তৃতীয়াংশ। থামানো trial-গুলোর বেশিরভাগের learning rate ছোট (১০−৪ থেকে ৬ × ১০−৪), যারা ২০ epoch-এ ধীরে নামত। Trial ৯ সবচেয়ে শিক্ষণীয়, কারণ সে তিনবার বেঁচে গিয়ে চতুর্থবার থেমেছে:
| epoch | |
| এই epoch-এর val loss | |
| এখন পর্যন্ত সেরা | |
| ৭টির median | |
| সিদ্ধান্ত |
- Epoch ১. Val loss ১.০১৯৭, median ১.০৪৫৩-এর চেয়ে ভালোই। কিন্তু
n_warmup_steps=3, তাই epoch ৩-এর আগে pruner কিছু তুলনাই করে না। - Epoch ২. এখনো warmup. এই সুরক্ষা ধীরে শুরু করা কিন্তু পরে ভালো trial-কে বাঁচায়।
- Epoch ৩. প্রথম আসল তুলনা: সেরা ০.৮১২৩, আগে শেষ হওয়া ৭টির epoch ৩-এর median ০.৮৭৫১. Median-এর চেয়ে ভালো, তাই চলুক।
- Epoch ৪. বড় learning rate-এ loss লাফিয়ে ০.৯৩৬৩. কিন্তু pruner দেখে এখন পর্যন্ত সেরা মান, ০.৮১২৩, যা median ০.৮১৬৪-এর সামান্য নিচে। অল্পের জন্য বাঁচল।
- Epoch ৫. আবার ভালো: ০.৬৮৫৫, median ০.৭৩৪২. চলুক।
- Epoch ৬. Loss ০.৮২৭২-এ লাফাল; সেরা ০.৬৮৫৫ কিন্তু এবার median ০.৬৭৮১-এর চেয়ে খারাপ।
should_prune()True, trial PRUNED. বাকি ১৪ epoch আর চলল না। - সারকথা. অস্থির (খুব বড় learning rate-এর) trial-কে median কয়েক epoch ধৈর্য দেয়, তারপর ছাঁটে। লক্ষ করুন, তুলনা শুধু শেষ হওয়া trial-এর সঙ্গে, আর একই epoch-এ; তাই শুরুর দিকে যত ভালো trial শেষ হয়, পরে ছাঁটাই তত কড়া।
দুটি সতর্কতা। প্রথম, warmup বা পরে নামা schedule-এ অনেক ভালো trial শুরুতে খারাপ দেখায়; তখন n_warmup_steps বাড়ান বা ধৈর্যশীল pruner (যেমন PatientPruner, SuccessiveHalvingPruner) বিবেচনা করুন। দ্বিতীয়, TPESampler(seed=0) এক process-এ ক্রমানুসারে চালালে খোঁজ আবার একই হয়; একাধিক process বা machine একসঙ্গে চালালে trial-এর ক্রম, তাই খোঁজও, বদলায়। sqlite:///optuna.db storage-এর কারণে বিদ্যুৎ গেলেও শেষ হওয়া trial হারায় না, load_if_exists=True দিয়ে খোঁজ আবার চালানো যায়।
৯ · দুটি model ন্যায্যভাবে তুলনা — কয়েকটি seed আর বিজয়ীর অভিশাপ
Optuna-র বিজয়ী (trial ৬, val loss ০.৫৭৮২) default config-এর (০.৫৮৫৪) চেয়ে ভালো দেখাচ্ছে। কিন্তু ৩০টি trial-এর সেরাটি বাছা মানে seed ০-এর ভাগ্যও বাছা: অনেক চেষ্টার সর্বোচ্চটি গড়ে নিজের আসল মানের চেয়ে ভালো দেখায়। একে বলে winner's curse. ন্যায্য পরীক্ষা: খোঁজে ব্যবহার না হওয়া নতুন seed (১–৫), দুই config প্রতিটি seed-এ, আর প্রতিটি seed-এর পার্থক্য দেখা। এটি একটি paired comparison, কারণ একই seed-এ ডেটার ক্রম আর ভাগ এক। compare.py (B-এর মান ছোট করে লেখা, যেমন YAML-এ রাখবেন) সত্যিকারের run:
~/bazarghar-dl$ uv run python compare.py
A 2c6a83d9 | B 742483ae
seed 0 | A 0.5854 | B 0.5783 | B − A -0.0071
seed 1 | A 0.6065 | B 0.5970 | B − A -0.0095
seed 2 | A 0.5881 | B 0.5899 | B − A +0.0017
seed 3 | A 0.6021 | B 0.6058 | B − A +0.0037
seed 4 | A 0.5865 | B 0.6011 | B − A +0.0146
seed 5 | A 0.6226 | B 0.5993 | B − A -0.0233
seed 1–5: A 0.6012 ± 0.0148 | B 0.5986 ± 0.0059
পার্থক্য B − A: গড় -0.0026, sd 0.0144, 95% CI [-0.0204, +0.0153]
| seed | |
| A · B | |
| B − A | |
| B জিতল |
- Seed ০. এখানে B ০.০০৭১ ভালো, কিন্তু এই seed-এর ওপরেই B বাছা হয়েছে। এই সংখ্যা প্রমাণ নয়, নির্বাচনের ফল; তাই গোনা থেকে বাদ।
- Seed ১. নতুন seed-এ B আবার ভালো, ০.০০৯৫. একটি seed দেখেই অনেকে এখানে থামেন।
- Seed ২. এবার A সামান্য ভালো (+০.০০১৭)। দুই seed-এ ফল দুই দিকে।
- Seed ৩. আবার A ভালো, +০.০০৩৭. এখন পর্যন্ত B জিতেছে ৩-এর মধ্যে ১টিতে।
- Seed ৪. A স্পষ্ট ভালো, +০.০১৪৬.
- Seed ৫. B অনেক ভালো, −০.০২৩৩. পাঁচটির মধ্যে B জিতল ২টিতে।
- সারকথা. পার্থক্যের গড় −০.০০২৬, sd ০.০১৪৪; ৯৫% t-interval = গড় ± ২.৭৭৬ × ০.০১৪৪/√৫ = [−০.০২০৪, +০.০১৫৩], যা ০-কে ধরে আছে। এই ডেটায় B ভালো, তা প্রমাণিত নয়। B-এর sd ছোট (০.০০৫৯ বনাম ০.০১৪৮): সে হয়তো বেশি স্থির, সেটিও একটি তথ্য, তবে ৫টি seed-এ sd-ও অনিশ্চিত।
এখানে t-এর মান ২.৭৭৬ আসে ৪ মুক্তমাত্রার t-distribution থেকে। পাঁচটি seed কম, তাই interval চওড়া; সত্যিকারের কাজে ছোট পার্থক্য দাবি করতে আরও seed লাগে। আরেকটি সৎ স্বীকারোক্তি: এই validation set-ই খোঁজে আর তুলনায় ব্যবহার হয়েছে; চূড়ান্ত সংখ্যা আসা উচিত এমন test set থেকে, যা কোনো সিদ্ধান্তে লাগেনি। Paired test, McNemar আর একাধিক তুলনার সংশোধন মেশিন লার্নিং কোর্সের পর্ব ২২-এ। Bug খোঁজার সময়ও এই শিক্ষা কাজে লাগে: "fix-এর পরে loss কমেছে" দাবি একটি seed-এ না দেখে কয়েকটিতে দেখুন (পর্ব ১৮, Training ডিবাগ করা)।
আরও গভীরে: Seed-এর প্রভাব নিয়ে পরিচিত কাজ: Nils Reimers আর Iryna Gurevych (EMNLP ২০১৭) NLP-তে শুধু seed বদলে score-এর বড় ফারাক দেখিয়ে score-এর বণ্টন রিপোর্ট করার পরামর্শ দেন; Peter Henderson ও সহকর্মীরা (AAAI ২০১৮) reinforcement learning-এ একই সমস্যা দেখান; Xavier Bouthillier ও সহকর্মীরা (MLSys ২০২১) benchmark-এর variance-এর উৎস ভাগ করে মাপেন। GPU-র nondeterminism-এর তালিকা আর প্রতিকার PyTorch docs-এর "Reproducibility" নোটে, যা version-এর সঙ্গে বদলায়। Hyperband-এর মূল প্রবন্ধ Lisha Li ও সহকর্মীদের (JMLR, ২০১৮ সালে প্রকাশিত)।
পরীক্ষা ও ইন্টারভিউতে:
- PyTorch run হুবহু আবার পেতে কী কী seed? random, np.random, torch.manual_seed, DataLoader-এর generator, worker-এর নিজের RNG; GPU-তে deterministic algorithms, cudnn.benchmark False, CUBLAS_WORKSPACE_CONFIG.
- ৫টি seed-এ accuracy ০.৮০, ০.৮১, ০.৮১, ০.৭৭৫, ০.৮২; গড় আর sd? গড় ০.৮০৩, sd ≈ ০.০১৭২, SE ≈ ০.০০৭৭.
- Grid-এর চেয়ে random search কেন? অল্প hyperparameter গুরুত্বপূর্ণ; একই বাজেটে random প্রতিটি মাত্রায় বেশি আলাদা মান পরীক্ষা করে।
- MedianPruner কখন থামায়? Warmup-এর পরে, trial-এর এখন পর্যন্ত সেরা মান একই step-এ শেষ হওয়া trial-গুলোর median-এর চেয়ে খারাপ হলে।
- ৩০ trial × ২০ epoch, ১৮টি পূর্ণ, ১১টি epoch ৩-এ, ১টি epoch ৬-এ থামলে কত epoch? ৩৬০ + ৩৩ + ৬ = ৩৯৯.
১০ · AI-এর যুগে — AI-এর experiment code আর "উন্নতির" দাবি যাচাই
AI assistant-কে "আমার model tune করে দাও" বললে সে কয়েক মিনিটে Optuna বা training script দেয়, আর প্রায়ই একটি সংখ্যা দিয়ে উন্নতি ঘোষণা করে। এই পর্বের প্রায় সব ভুলই চুপচাপ: কোড চলে, শুধু সিদ্ধান্ত ভুল।
| AI প্রায়ই যা করে | ফল | কীভাবে ধরবেন |
|---|---|---|
| একটি seed-এর একটি run দেখে "B ভালো" বলে | ভাগ্যকে উন্নতি ভাবা | নতুন কয়েকটি seed-এ জোড়ায় তুলনা, পার্থক্যের interval |
শুধু torch.manual_seed; worker বা নিজের Generator ভুলে যায় | Run মেলে না, বা worker-এ একই augmentation | দুটি আলাদা process-এ weight-এর hash মেলান |
পুরোনো Optuna API (suggest_uniform, suggest_loguniform) বা ভুল pruner argument | Error বা বাদ পড়া ফাংশন | নিজের installed version-এর docs; inspect.signature |
| Learning rate সমান ভাগে (log নয়) টানে | বেশিরভাগ চেষ্টা বড় lr-এ | Search space-এ log=True আছে কি না দেখুন |
| Validation-এ tune করে একই validation সংখ্যা রিপোর্ট করে | আশাবাদী ফল | আলাদা test set, আর চেষ্টার সংখ্যাও রিপোর্টে |
| Hosted tracking সেবায় key বা ডেটা পাঠানোর কোড দেয় | গোপন তথ্য বাইরে যায় | Local TensorBoard বা MLflow; key কখনো কোডে নয় |
AI-কে কোডের সঙ্গে যাচাইয়ের ব্যবস্থাও চাইতে বলুন:
AI-এর দেওয়া experiment code বা ফল হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Deep learning-এর গবেষণায় দীর্ঘদিন একটি run-এর সংখ্যা রিপোর্ট করা স্বাভাবিক ছিল। এটি কেন সমস্যা, আর বদলাচ্ছে কীভাবে?
একটি বড় network train করতে একসময় দিন বা সপ্তাহ লাগত, তাই একাধিক seed চালানো বিলাসিতা মনে হতো। Leaderboard-এর সংস্কৃতিও একটি সংখ্যা চায়: নতুন পদ্ধতি আগের সেরার চেয়ে ০.৩ বেশি। কিন্তু যখন একই পদ্ধতির seed-ভেদে বিস্তারই কয়েক দশমিক, তখন এমন পার্থক্য ভাগ্যও হতে পারে। এই পর্বের ছোট MLP-তেই seed বদলে accuracy ০.৭৭৫ থেকে ০.৮২০ নড়েছে।
প্রকাশিত কাজ এটি স্পষ্ট করেছে। Nils Reimers আর Iryna Gurevych (EMNLP ২০১৭) sequence labeling-এ দেখান, শুধু seed বদলালে দুটি পদ্ধতির ক্রম উল্টে যেতে পারে, আর তাঁরা একটি score-এর বদলে score-এর বণ্টন রিপোর্ট করার প্রস্তাব দেন। Peter Henderson ও সহকর্মীদের "Deep Reinforcement Learning that Matters" (AAAI ২০১৮) দেখায়, একই algorithm-এর দুই দল seed-এর গড় এতটা আলাদা হতে পারে যে মনে হয় দুটি আলাদা পদ্ধতি। Jesse Dodge ও সহকর্মীরা (২০২০, arXiv) BERT fine-tuning-এ দেখান, weight-এর শুরু আর ডেটার ক্রম বদলালেই ছোট ডেটাসেটে ফল অনেক নড়ে। ২০২১ সালের একটি arXiv লেখা প্রায় মজা করেই দেখিয়েছিল যে হাজারো seed-এর মধ্যে খুঁজলে "সেরা seed" পাওয়া যায়; বার্তাটি ছিল সতর্কতা, পরামর্শ নয়।
বদল আসছে কয়েকভাবে। বড় সম্মেলনের reproducibility checklist-এ এখন error bar আর run-এর সংখ্যা জানতে চাওয়া হয়। Benchmark-এর গবেষণা (যেমন Bouthillier ও সহকর্মীরা, MLSys ২০২১) seed, ডেটার ভাগ আর hyperparameter খোঁজ, এই উৎসগুলো আলাদা করে মাপার পদ্ধতি দেয়। তবু বড় language model-এর ক্ষেত্রে একটি run-এর খরচ এত বেশি যে কয়েকটি seed প্রায়ই সম্ভব নয়; তখন ছোট model-এ কয়েকটি seed চালিয়ে বিস্তারের আন্দাজ নেওয়া, আর দাবি সংযত রাখা একটি বাস্তব আপস।
মূল উপলব্ধি: একটি run একটি নমুনা; পার্থক্য দাবি করার আগে জানতে হয় seed-ভেদে বিস্তার কত, আর যেখানে কয়েকটি seed সম্ভব নয় সেখানে দাবি সেই অনুযায়ী ছোট রাখতে হয়।
প্র ০২GPU-তে বিট ধরে reproducibility কেন কঠিন, আর সবসময় deterministic mode চালু রাখা কি বুদ্ধিমানের কাজ?
GPU হাজার হাজার thread একসঙ্গে চালায়। অনেক thread যখন একই জায়গায় যোগ করে (যেমন embedding-এর gradient, যেখানে একই শব্দ batch-এ কয়েকবার আছে), তখন atomic যোগ ব্যবহার হয়, আর কোন thread আগে পৌঁছাবে তা প্রতিবার আলাদা। Floating point যোগ associative নয়, তাই ক্রম বদলালে শেষ বিট বদলায়। এই পর্বের CPU run-এ শুধু thread-এর সংখ্যা বদলেই ৪২,৭২৯টি weight শেষ বিটে আলাদা হয়েছে; GPU-তে এমন উৎস আরও বেশি। আরেকটি উৎস cuDNN: benchmark=True প্রথমবার কয়েকটি convolution algorithm-এর সময় মেপে দ্রুততমটি বাছে, আর মাপের সামান্য পার্থক্যে অন্য দিন অন্য algorithm জিততে পারে, যাদের গণনার ক্রমও আলাদা।
PyTorch-এর use_deterministic_algorithms(True) এর উত্তর: deterministic বিকল্প থাকলে তা নেয়, না থাকলে RuntimeError দেয়, যাতে চুপচাপ ভুল না হয়। দাম আছে: কিছু kernel ধীর, cuBLAS-এর জন্য workspace-এর environment variable লাগে, আর কিছু কাজ একেবারে বন্ধ। তার ওপর deterministic মানে শুধু একই hardware, driver আর library version-এ একই ফল; অন্য GPU-তে বিট ধরে মিলের নিশ্চয়তা কেউ দেয় না।
তাই বাস্তব নীতি দুই স্তরের। Debug, regression test আর resume-এর সময় deterministic mode খুব কাজের: একটি bug প্রতিবার একই জায়গায় এলে তাকে ধরা সহজ, আর "আমার পরিবর্তনে ফল বদলেছে কি না" প্রশ্নের উত্তর বিট ধরে পাওয়া যায়। বড় training-এ অনেকে গতি বেছে নেন, আর বৈজ্ঞানিক দাবির ভরসা রাখেন কয়েকটি seed-এর গড়ে। বিট ধরে মিল একটি run-কে আবার চালানোর নিশ্চয়তা দেয়, ফলটি সাধারণভাবে সত্য কি না সেই নিশ্চয়তা দেয় না। একই seed-এ ঠিক একই ভাগ্য আবার পাওয়া আর ফলটি ভাগ্যের ওপর নির্ভর না করা, দুটি আলাদা জিনিস।
মূল উপলব্ধি: Deterministic mode debug আর পুনরাবৃত্তির হাতিয়ার, গতির বিনিময়ে; বৈজ্ঞানিক নির্ভরযোগ্যতা আসে কয়েকটি seed আর interval থেকে।
প্র ০৩Grid search, random search আর Bayesian পদ্ধতি — deep learning-এ কোনটি কখন, আর বড় model-এ মানুষ বাস্তবে কী করে?
Grid search সহজ আর পুনরাবৃত্তিযোগ্য, কিন্তু মাত্রা বাড়লে খরচ গুণে বাড়ে: ৫টি hyperparameter-এ ৪টি করে মান মানে ১,০২৪টি run. Bergstra আর Bengio-র ২০১২ সালের JMLR প্রবন্ধের মূল যুক্তি "low effective dimensionality": বেশিরভাগ সমস্যায় অল্প কয়েকটি hyperparameter ফল ঠিক করে, কিন্তু কোনগুলো তা ডেটাভেদে বদলায়। তখন grid অপ্রাসঙ্গিক মাত্রায় চেষ্টা নষ্ট করে, আর random search প্রতিটি মাত্রায় বেশি আলাদা মান দেখে। এই পর্বের ব্লকে ৯টি চেষ্টায় grid learning rate দেখেছে ৩টি, random ৯টি।
Bayesian optimization আর TPE আগের ফল দেখে পরের বিন্দু বাছে, তাই চেষ্টা খুব দামি আর সংখ্যায় কম হলে লাভ বেশি। তবে মেশিন লার্নিং কোর্সের পর্ব ৪৯-এ দেখেছেন, বাস্তব noisy সমস্যায় তার সুবিধা random search-এর তুলনায় কখনো খুব ছোট। Deep learning-এ বড় লাভ আসে অন্য দিক থেকে: epoch ধরে মাঝের ফল পাওয়া যায়, তাই successive halving, Hyperband বা median pruning খারাপ চেষ্টায় সম্পদ কম দেয়। এই পর্বের খোঁজে pruning বাজেটের প্রায় এক-তৃতীয়াংশ বাঁচিয়েছে।
বড় model-এ পুরো খোঁজ প্রায়ই অসম্ভব। তখন প্রচলিত কৌশল: ছোট model বা ছোট ডেটায় খুঁজে বড়তে নেওয়া, আগের প্রকাশিত recipe থেকে শুরু করে শুধু learning rate আর batch size-এর চারপাশে খোঁজা, আর এমন parameterization ব্যবহার যেখানে model বড় করলেও ভালো learning rate মোটামুটি একই থাকে (যেমন μP নামে প্রস্তাবিত পদ্ধতি; এর কার্যকারিতা নিয়ে আলোচনা চলছে)। ছোট থেকে বড়তে নেওয়ার ঝুঁকি একটাই: ছোট মাপের র্যাঙ্ক সবসময় বড় মাপে টেকে না।
মূল উপলব্ধি: ছোট বাজেটে random search আর log scale, মাঝের ফল থাকলে pruning, খুব দামি চেষ্টায় Bayesian পদ্ধতি; আর সব ক্ষেত্রে আগে ভাবুন কোন hyperparameter আসলে গুরুত্বপূর্ণ।
প্র ০৪Pruning কি কখনো ভালো trial-কেও মেরে ফেলে? কোন ধরনের training-এ এই ঝুঁকি বেশি?
হ্যাঁ। Median pruning ধরে নেয়, শুরুতে যে trial পিছিয়ে, শেষেও সে পিছিয়ে থাকবে। অনেক ক্ষেত্রে এটি মোটামুটি সত্য, তাই পদ্ধতিটি কাজ করে। কিন্তু কয়েকটি পরিচিত পরিস্থিতিতে এটি ভাঙে। ছোট learning rate-এর trial শুরুতে ধীরে নামে, পরে প্রায়ই বেশি স্থির ও ভালো জায়গায় পৌঁছায়; এই পর্বের খোঁজে থামানো trial-গুলোর বেশিরভাগই ছোট learning rate-এর, আর তাদের কেউ ২০ epoch-এ জিতত কি না, আমরা জানি না, কারণ চালাইনি। Warmup আর পরে নামা schedule (পর্ব ১৫-এর বিষয়) শুরুতে ইচ্ছা করেই ধীর; বেশি regularization বা বেশি dropout-ও শুরুতে loss উঁচু রাখে, শেষে ভালো সাধারণীকরণ দেয়।
আরেকটি সূক্ষ্ম পক্ষপাত: median হয় শুধু শেষ হওয়া trial-গুলো থেকে। খোঁজ যত এগোয়, তত ভালো trial শেষ হয়, ফলে পরের trial-এর জন্য বাধা কড়া হয়। একই config খোঁজের শুরুতে বাঁচত, শেষে মরতে পারে। এ ছাড়া এক epoch-এর val loss noisy; এই পর্বের trial ৯-এ loss ০.৬৮৫৫ থেকে ০.৮২৭২-এ লাফিয়েছে।
প্রতিকার কয়েকটি। Warmup step বাড়ানো, যাতে আসল তুলনা schedule-এর অস্থির অংশের পরে হয়। Patience দেওয়া pruner, যা কয়েকটি টানা খারাপ মাপের পরে থামে। Successive halving বা Hyperband, যেখানে কয়েকটি আলাদা শুরুর বাজেটে ঝুঁকি ভাগ করা হয় (Li ও সহকর্মীরা, JMLR ২০১৮)। আর খোঁজের পরে সেরা কয়েকটির সঙ্গে কয়েকটি থামানো trial পুরো বাজেটে চালিয়ে দেখা, pruner আসলে কতটা ভুল করছে। এর কোনোটিই বিনামূল্যে নয়; pruning সবসময় সময় বাঁচানো আর ভুল বাদ দেওয়ার মধ্যে আপস।
মূল উপলব্ধি: Pruning একটি বাজি যে শুরুর র্যাঙ্ক শেষের র্যাঙ্ক বলে দেয়; schedule, learning rate আর noise সেই বাজি দুর্বল করে, তাই warmup আর ধৈর্য ঠিক করুন আর মাঝে মাঝে বাজিটি যাচাই করুন।
প্র ০৫TensorBoard, MLflow, hosted tracking সেবা না শুধু ফোল্ডার আর JSON — tracking টুল বাছবেন কীভাবে?
TensorBoard এসেছিল TensorFlow-এর সঙ্গে (TensorFlow open-source হয় ২০১৫ সালে); পরে PyTorch নিজের SummaryWriter যোগ করে, তাই আজ এটি framework-নিরপেক্ষ একটি ফাইল ফরম্যাট আর viewer. এর শক্তি সময় ধরে curve, histogram আর ছবি; দুর্বলতা হলো এটি মূলত দেখার টুল: কোন run কোন commit থেকে, কোন ডেটায়, তা আপনাকে নিজে রাখতে হয় (এই পর্বের meta.json যেমন)। MLflow (মেশিন লার্নিং কোর্সের পর্ব ৪) param, metric, artifact আর model registry একসঙ্গে রাখে, local-এ চলে, আর দলের জন্য server হিসেবেও চালানো যায়।
Hosted সেবাগুলো (যেমন Weights & Biases) সুন্দর তুলনা, দলের ভাগাভাগি আর sweep-এর ব্যবস্থা দেয়, কিন্তু account লাগে, আর metric, config, কখনো ডেটার নমুনা বা ছবি অন্য প্রতিষ্ঠানের server-এ যায়। গ্রাহকের তথ্য থাকলে আগে প্রতিষ্ঠানের নীতি আর আইন দেখা দরকার। খরচ আর সেবা বন্ধ হওয়ার ঝুঁকিও ভাবতে হয়: কয়েক বছর পরে পুরোনো run খুলতে পারবেন কি না।
শুধু ফোল্ডার আর JSON সবচেয়ে সরল আর টেকসই: grep, pandas আর Git দিয়েই কাজ চলে, কোনো server নেই। ছোট দল বা একক গবেষকের জন্য প্রায়ই এটুকুই যথেষ্ট, সঙ্গে দেখার জন্য TensorBoard. দল বড় হলে, অনেক মানুষ একই experiment-এ কাজ করলে, বা model deploy করার আগে অনুমোদনের ধাপ লাগলে MLflow-এর মতো ব্যবস্থার মূল্য বাড়ে।
টুলের চেয়ে বড় প্রশ্ন অভ্যাস: প্রতিটি run, ব্যর্থ আর থামানোসহ, শুরুতেই config, commit আর ডেটার hash লেখে কি না; run-id থেকে ফোল্ডার আর ফোল্ডার থেকে run আবার চালানো যায় কি না। যে টুলেই হোক, এই শৃঙ্খলা না থাকলে সুন্দর dashboard শুধু সুন্দর বিভ্রান্তি।
মূল উপলব্ধি: Tracking টুল বাছুন দলের আকার, গোপনীয়তা আর দীর্ঘস্থায়িত্ব দেখে; কিন্তু মূল কাজ প্রতিটি run-এর পূর্ণ হিসাব রাখার অভ্যাস, যা একটি ফোল্ডার দিয়েও শুরু করা যায়।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান।
-
পূর্ণ numerical: একটি model পাঁচটি seed-এ validation accuracy দিল ০.৮০, ০.৮১, ০.৮১, ০.৭৭৫, ০.৮২. (ক) গড়, sd (n − ১ দিয়ে ভাগ) আর SE বের করুন। (খ) ৪ মুক্তমাত্রায় t = ২.৭৭৬ ধরে গড়ের ৯৫% interval. (গ) একটি Optuna খোঁজে ৩০টি trial, প্রতিটির বাজেট ২০ epoch; ১৮টি পূর্ণ হলো, ১১টি epoch ৩-এ আর ১টি epoch ৬-এ থামল। মোট কত epoch চলল, আর বাজেটের কত শতাংশ বাঁচল?
(ক) গড় = ৪.০১৫/৫ = ০.৮০৩. বিচ্যুতি −০.০০৩, ০.০০৭, ০.০০৭, −০.০২৮, ০.০১৭; বর্গের যোগ ০.০০০০০৯ + ০.০০০০৪৯ + ০.০০০০৪৯ + ০.০০০৭৮৪ + ০.০০০২৮৯ = ০.০০১১৮; ÷ ৪ = ০.০০০২৯৫; sd = √০.০০০২৯৫ ≈ ০.০১৭২. SE = ০.০১৭২/√৫ ≈ ০.০০৭৭. (খ) অর্ধ-প্রস্থ = ২.৭৭৬ × ০.০০৭৬৮ ≈ ০.০২১৩, তাই interval ≈ [০.৭৮১৭, ০.৮২৪৩]. একটি seed-এ ০.৮২ পেয়ে "৮২%" লেখা কতটা ভাগ্যনির্ভর, তা এখান থেকেই বোঝা যায়। (গ) ১৮ × ২০ + ১১ × ৩ + ১ × ৬ = ৩৬০ + ৩৩ + ৬ = ৩৯৯; বাজেট ৬০০, বাঁচল ২০১, প্রায় ৩৩.৫%.
-
সমালোচনা ও নকশা: একজন সহকর্মীর রিপোর্ট: "নতুন model-এর val accuracy ০.৮৪২, পুরোনোটির ০.৮৩৬; seed ৪২; Optuna-য় ৬০টি trial validation set-এ চালিয়ে সেরাটি নেওয়া; GPU-তে
cudnn.benchmark=True; config notebook-এর ঘরে ঘরে লেখা।" কী কী সমস্যা, আর কীভাবে নতুন করে নকশা করবেন?১, একটি seed: ০.০০৬-এর পার্থক্য seed-ভেদে বিস্তারের ভেতরেই পড়তে পারে। দুই model-কে খোঁজে ব্যবহার না হওয়া অন্তত ৫টি seed-এ জোড়ায় চালিয়ে পার্থক্যের গড় আর interval দিন। ২, Winner's curse: ৬০টির সেরাটি validation-এর ভাগ্যও বেছেছে; পুরোনো model-কেও সমান খোঁজ দেওয়া হয়েছিল কি না জানুন, আর চূড়ান্ত সংখ্যা আলাদা test set-এ, "৬০টি trial চেষ্টা করা হয়েছে" লিখে। ৩, benchmark=True: run আবার চালালে বিট ধরে মিলবে না; debug আর পুনরুৎপাদনের সময় deterministic mode, বড় run-এ গতি রাখলে seed বেশি। ৪, Config ছড়ানো: একটি frozen dataclass, YAML ফাইল Git-এ, seed বাদে hash থেকে run-id, আর প্রতিটি run-এর ফোল্ডারে commit, ডেটার hash, version, TensorBoard event আর checkpoint. যাচাই: দুটি process-এ একই seed-এ weight-এর hash মেলান।
-
আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন তিনটি True/False:
import hashlib, json def config_id(cfg): d = {k: v for k, v in cfg.items() if k != "seed"} return hashlib.sha256(json.dumps(d, sort_keys=True).encode()).hexdigest()[:8] a = {"lr": 0.001, "hidden": 64, "seed": 0} b = {"hidden": 64, "lr": 1e-3, "seed": 7} c = {"lr": 0.0010, "hidden": 64.0, "seed": 0} d = {"lr": "0.001", "hidden": 64, "seed": 0} print(config_id(a) == config_id(b), config_id(a) == config_id(c), config_id(a) == config_id(d))True False False(সত্যিকারের run, Python 3.14.) a আর b: key-এর ক্রম
sort_keysঠিক করে,1e-3আর0.001একই float, seed বাদ; তাই এক। a আর c:0.0010একই float, কিন্তু64.0JSON-এ64.0লেখা হয়,64নয়; তাই আলাদা। a আর d: string"0.001"JSON-এ উদ্ধৃতিসহ; আলাদা। শিক্ষা: hash নেওয়ার আগে মানগুলোকে Config-এর type-এ রূপান্তর করুন। -
কোড লিখুন: একটি function লিখুন যা PyTorch-এর DataLoader বানায় এমনভাবে যে (১) shuffle-এর নিজস্ব seed আছে, (২) worker থাকলেও প্রতিটি worker-এর NumPy আর Python RNG আলাদা কিন্তু run থেকে run-এ একই।
import random, numpy as np, torch from torch.utils.data import DataLoader def seed_worker(worker_id): s = torch.initial_seed() % 2**32 # PyTorch: base_seed + worker_id np.random.seed(s); random.seed(s) def make_loader(ds, batch_size, seed, workers=2): g = torch.Generator().manual_seed(seed) return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=workers, generator=g, worker_init_fn=seed_worker)প্রতিটি epoch-এ DataLoader generator থেকে একটি base seed টানে, আর worker পায় base seed + worker id; তাই
torch.initial_seed()worker-ভেদে আলাদা, আর generator-এর seed একই থাকলে run-ভেদে একই। Dataset-এ নিজেরnp.random.default_rngরাখলে সেটিওseed_worker-এ এই seed থেকে নতুন করে বানান, অংশ ৩-এর "fixed"-এর মতো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি hyperparameter খোঁজ থেকে রিপোর্ট পর্যন্ত কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Config dataclass, seed_everything আর run ফোল্ডারের নিয়ম ঠিক করা, code commit করা
- এক seed-এ (seed ০) log scale-এর search space দিয়ে Optuna খোঁজ, pruning-সহ
- বিজয়ী আর baseline config বাছাই, YAML-এ লেখা
- খোঁজে ব্যবহার না হওয়া কয়েকটি seed-এ দুই config জোড়ায় চালানো
- পার্থক্যের গড় আর interval হিসাব, তারপর আলাদা test set-এ চূড়ান্ত মাপ
- রিপোর্টে চেষ্টার সংখ্যা, seed-এর সংখ্যা আর run-id-সহ ফল লেখা
একই PyTorch model আর ডেটায় শুধু seed বদলে ১০টি run-এ validation accuracy-র sd ০.০১২. আরও ৯০টি run চালালে (মোট ১০০) কী আশা করবেন?
- sd প্রায় ১০ গুণ ছোট হবে
- sd প্রায় একই থাকবে, কিন্তু গড়ের interval প্রায় ৩.২ গুণ সরু হবে
- সব run-এর accuracy এক হয়ে যাবে
- গড়ের interval চওড়া হবে
PyTorch-এর DataLoader-এ num_workers=2; Dataset-এর __init__-এ self.rng = np.random.default_rng(0) বানিয়ে __getitem__-এ তা থেকে augmentation-এর noise নেওয়া হয়। কোনো worker_init_fn নেই। কী ঘটে?
- PyTorch প্রতিটি worker-এর Generator আলাদা seed করে, তাই সব ঠিক
- Worker শুরুর সময় RuntimeError আসে
- প্রতিটি run-এ noise আলাদা, তাই reproducibility নষ্ট
- দুই worker একই ক্রমে হুবহু একই noise দেয়, কোনো error ছাড়াই
CPU-তে একই seed দিয়ে একই PyTorch training দুবার চালানো হলো: একবার torch.set_num_threads(1), একবার default thread সংখ্যায়। Final weight-এ কী আশা করবেন?
- অনেক weight শেষ বিটে আলাদা হতে পারে, যদিও loss প্রায় একই
- Seed একই, তাই সবসময় বিট ধরে এক
- এক thread-এ training ব্যর্থ হয়
- দুটি সম্পূর্ণ আলাদা model, accuracy-তে বড় পার্থক্য
Optuna-র MedianPruner(n_startup_trials=5, n_warmup_steps=3)-এ (loss কমানো লক্ষ্য) একটি trial epoch ৪-এ। তার epoch ১–৪-এর val loss ০.৯০, ০.৮০, ০.৭৫, ০.৮৫; শেষ হওয়া trial-গুলোর epoch ৪-এর median ০.৭৮. কী হবে?
- PRUNED, কারণ ০.৮৫ > ০.৭৮
- Warmup চলছে বলে তুলনাই হয় না
- চলতে থাকে, কারণ এখন পর্যন্ত সেরা ০.৭৫ < ০.৭৮
- শেষ হওয়া trial না থাকায় error আসে
৯টি চেষ্টার বাজেটে দুটি hyperparameter: learning rate (খুব গুরুত্বপূর্ণ) আর dropout (প্রায় অপ্রাসঙ্গিক)। 3 × 3 grid search বনাম ৯টি random search-এর মূল পার্থক্য কী?
- Random search প্রতিবার নিশ্চিতভাবে grid-এর চেয়ে ভালো ফল দেয়
- Grid learning rate-এর ৩টি মান দেখে, random ৯টি আলাদা মান দেখে
- Random search-এ বেশি চেষ্টা লাগে
- শুধু grid-এ log scale ব্যবহার করা যায়
৩০টি trial-এর খোঁজে seed ০-তে বিজয়ী config B-এর val loss ০.৫৭৮, default A-এর ০.৫৮৫. নতুন seed ১–৫-এ B − A পার্থক্যের গড় −০.০০২৬ আর ৯৫% interval [−০.০২০৪, +০.০১৫৩]. সবচেয়ে সৎ সিদ্ধান্ত কোনটি?
- B ভালো, কারণ seed ০-তে ০.০০৭ কম loss
- A ভালো, কারণ ৫টির ৩টিতে A জিতেছে
- Seed দিয়ে তুলনা অর্থহীন, তাই কিছুই বলা যায় না
- পার্থক্য প্রমাণিত নয়; দাবি করতে হলে আরও seed আর আলাদা test set লাগবে
PyTorch-এ torch.use_deterministic_algorithms(True) চালু করার পরে কোনটি ঘটে না?
- যে কাজের deterministic রূপ নেই, তা RuntimeError দেয়
torch.emptyNaN দিয়ে ভরা tensor দেয়- Seed না দিলেও প্রতিটি run একই ফল দেয়
- কিছু GPU কাজ ধীর হতে পারে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- একটি run একটি নমুনা: কয়েকটি seed, mean ± sd, আর গড়ের interval (t বা bootstrap); sd নতুন run-এর বিস্তার, interval গড়ের অনিশ্চয়তা।
- সব seed: random, np.random, torch.manual_seed, DataLoader-এর generator, worker_init_fn; GPU-তে use_deterministic_algorithms, cudnn.benchmark False, CUBLAS_WORKSPACE_CONFIG. Thread আর hardware বদলালে শেষ বিট তবু বদলাতে পারে।
- Config আর run: frozen dataclass + argparse + YAML; seed বাদে hash থেকে run-id; প্রতিটি run-এর ফোল্ডারে config, commit, ডেটার hash, version, TensorBoard event, metric আর checkpoint.
- খোঁজ: grid নয়, random আর log scale; epoch ধরে মাঝের ফল থাকলে pruning (MedianPruner: সেরা-এখন-পর্যন্ত বনাম একই epoch-এর median, warmup-এর পরে)।
- ন্যায্য তুলনা: খোঁজে ব্যবহার না হওয়া seed, জোড়ায় পার্থক্য, interval; বিজয়ীর অভিশাপ মনে রেখে চূড়ান্ত সংখ্যা আলাদা test set-এ আর চেষ্টার সংখ্যাসহ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২০ · Convolution — filter, stride, padding আর receptive fieldপরবর্তী পর্বমডিউল ৪-এর শুরু: convolution হাতে আর NumPy-তে, padding, stride আর receptive field শেখা।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ২০ (Convolution) এখান থেকে খুলবেন।
- পর্ব ১০ · Training loopআগের পর্বCheckpoint, RNG state আর হুবহু resume.
- পর্ব ১১ · Dataset আর DataLoaderআগের পর্বSeed দেওয়া shuffle, worker আর generator.
- মেশিন লার্নিং · পর্ব ৪ · পুনরুৎপাদনযোগ্য ML experimentআগের ভিত্তিপাঁচ উপাদান, JSONL খাতা, MLflow, ablation আর forking paths.
- মেশিন লার্নিং · পর্ব ৪৯ · Bayesian optimizationআগের ভিত্তিGP, EI, TPE আর successive halving.
- মেশিন লার্নিং · পর্ব ২২ · Model তুলনার পরিসংখ্যানআগের ভিত্তিPaired test, McNemar আর একাধিক তুলনা।
- AI-এর ভিত্তি · পর্ব ১২ · পুনরুৎপাদনযোগ্য প্রকল্পআগের ভিত্তিFolder, lockfile, data version আর Git.