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

Training loop — loss, optimizer, train/eval mode আর checkpoint

The PyTorch Training Loop — Loss on Logits, Optimizers, Train/Eval Mode, Correct Metrics, Early Stopping and Exact Resume

Taught in Bangla, this lesson builds a correct training loop: the five steps of forward, loss, zero_grad, backward and step; why CrossEntropyLoss and BCEWithLogitsLoss take logits; optimizer and scheduler call order; why validation needs both model.eval() and torch.no_grad(); and weighting metrics by batch size. A NumPy loop runs in the page, then real desktop PyTorch runs train a customer-segment model and show a checkpoint resume that matches an uninterrupted run bit for bit.

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

  • এক training ধাপের পাঁচটি কাজের ক্রম, আর কোনটি কী বদলায়
  • Logits, CrossEntropyLoss, BCEWithLogitsLoss, আর optimizer (SGD-momentum, AdamW) ও scheduler ডাকার ক্রম
  • Validation loop-এ model.eval() আর torch.no_grad(), আর অসমান batch-এ সঠিক গড়
  • Early stopping, best checkpoint, আর থেমে যাওয়া training হুবহু আবার শুরু করা
  • তিনটি পরিচিত ভুলের সত্যিকারের প্রভাব মেপে দেখা

এই কোর্সের পর্ব ০৫-এ নিজের autograd দিয়ে MLP শিখিয়েছেন, আর দেখেছেন zero_grad ছাড়া gradient জমে যায়। এই মডিউলের আগের তিন পর্ব, পর্ব ০৭ (PyTorch tensor), পর্ব ০৮ (PyTorch autograd) আর পর্ব ০৯ (nn.Module দিয়ে model বানানো), tensor, gradient আর model-এর কাঠামো দেয়। এই পর্বে সব জোড়া লাগিয়ে একটি পূর্ণ training loop লিখব, যা আপনি বাকি কোর্স জুড়ে বারবার ব্যবহার করবেন। Optimizer-এর গণিত মেশিন লার্নিং কোর্সের পর্ব ১৪-এ derive করা আছে; এখানে শুধু তার ব্যবহার। PyTorch পাতায় চলে না, তাই PyTorch-এর সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে নেওয়া; পাতায় চালানো যায় এমন তিনটি ব্লক একই ধারণা NumPy দিয়ে দেখায় (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে)।

১ · এক training ধাপের পাঁচটি কাজ

ক্রিকেটের নেট সেশনের কথা ভাবুন। বোলার একটি বল করলেন (forward). কোচ দেখলেন লাইন কতটা বাইরে গেল (loss). আগের বলের নোট মুছে নতুন পাতায় লিখলেন (zero_grad). কোন আঙুল, কোন কবজির কারণে বলটি বাইরে গেল, তা খুঁজলেন (backward). তারপর বোলার কৌশল একটু বদলালেন (step). এক ঝুড়ির সব বল একবার শেষ হলে একটি epoch. আর ম্যাচের আগে একটি প্র্যাকটিস ম্যাচ, যেখানে কৌশল বদলানো হয় না, শুধু মাপা হয়: সেটিই validation.

PyTorch-এ এই পাঁচটি কাজ পাঁচটি লাইন। Mini-batch-এর input xb আর label yb হাতে থাকলে:

logits = model(xb)          # ১ forward: graph তৈরি হয়
loss = loss_fn(logits, yb)  # ২ loss: একটি scalar
opt.zero_grad()             # ৩ আগের ধাপের .grad মোছা
loss.backward()             # ৪ প্রতিটি parameter-এর .grad লেখা
opt.step()                  # ৫ .grad দেখে parameter বদলানো

এখানে opt একটি optimizer: যে object model-এর parameter-গুলোর তালিকা রাখে আর step()-এ নিয়ম মেনে সেগুলো বদলায়। নিচের ছবিতে পুরো loop: বাঁ দিকে প্রতি batch-এ ঘোরা train অংশ, ডান দিকে প্রতি epoch-এ একবার validation আর সিদ্ধান্ত।

একটি epoch-এর পুরো training loop

Inner loop per batch (left); validation and decisions once per epoch (right)

Training loop: five calls per batch, then validation, checkpoint and early stopping per epoch Train · model.train() Validation · eval + no_grad ১ forward ২ loss ৩ পুরোনো grad মোছা ৪ নতুন grad ৫ parameter বদল শুধু forward loss আর accuracy val loss কমেছে? patience শেষ? না হলে পরের epoch logits = model(xb) loss_fn(logits, yb) opt.zero_grad() loss.backward() opt.step() graph তৈরি হয় না batch-আকার দিয়ে ওজন হ্যাঁ → best.pt সেভ হ্যাঁ → থামুন scheduler.step() এখানে বাঁ লুপ: প্রতি batch-এ · ডান অংশ: প্রতি epoch-এ একবার
ছবি ১০.১ — বাঁ দিকের পাঁচটি কাজ প্রতি batch-এ ঘোরে; সব batch শেষ হলে ডান দিকে validation, best checkpoint আর early stopping-এর সিদ্ধান্ত। Scheduler থাকলে epoch-এর শেষে (বা নিজের নিয়মে প্রতি batch-এ) ডাকা হয়, সবসময় opt.step()-এর পরে।

এখন পাঁচটি কাজ খুব ছোট একটি উদাহরণে দেখি, যাতে প্রতিটি সংখ্যা চোখে পড়ে: nn.Linear(2, 3), চারটি উদাহরণ, তিনটি শ্রেণি, SGD (learning rate ০.১, momentum ০.৯)। নজর রাখুন weight-এর একটি ঘর w[0,0]-এর ওপর। সব সংখ্যা নিচের সত্যিকারের run থেকে।

প্রতিটি কাজের পরে w[0,0], তার .grad আর loss
কাজ
কোড
w[0,0]
w.grad[0,0]
loss
  1. শুরু. torch.manual_seed(0)-এর পরে Linear layer তৈরি; w[0,0] = −০.০০৫৩. এখনো কোনো backward হয়নি, তাই .grad হলো None.
  2. Forward. চারটি উদাহরণের জন্য ৪ × ৩ logits. ফলাফলের grad_fn AddmmBackward0: PyTorch হিসাবটি graph-এ লিখে রেখেছে। Parameter বা gradient কিছুই বদলায়নি।
  3. Loss. Cross-entropy একটি scalar, ১.৩০০৭. তিন শ্রেণিতে এলোমেলো অনুমানের loss ln ৩ ≈ ১.০৯৯; শুরুতে এর কাছাকাছি বা একটু বেশি হওয়াই স্বাভাবিক।
  4. zero_grad. আগের ধাপের gradient মোছা। PyTorch 2.0 থেকে default-ভাবে শূন্য না বসিয়ে None করে (set_to_none). Forward-এর আগে বা এখানে, দুই জায়গাই চলে; শর্ত একটাই: backward-এর আগে।
  5. Backward. এখন .grad-এ ০.১৬৪৮ এলো, কিন্তু w[0,0] আগের মতোই −০.০০৫৩. Backward শুধু হিসাব করে, কিছু বদলায় না।
  6. Step. Momentum-এর প্রথম ধাপে buffer = gradient, তাই w ← w − ০.১ × ০.১৬৪৮ = −০.০২১৮. একই ডেটায় পরের forward-এ loss ১.৩০০৭ থেকে নেমে ১.২৩৭১. পরের ধাপের zero_grad এই ০.১৬৪৮ মুছবে।

সংখ্যাগুলো এসেছে এই ছোট কোড থেকে (পুরো script extras.py-এর একটি অংশ; uv project তৈরির ধাপ অংশ ৭-এ):

torch.manual_seed(0)
lin = nn.Linear(2, 3)
x = torch.tensor([[0.5, -1.0], [1.5, 0.2], [-0.3, 0.8], [1.0, 1.0]]); y = torch.tensor([0, 2, 1, 2])
opt = torch.optim.SGD(lin.parameters(), lr=0.1, momentum=0.9)
print("start w[0,0] =", round(lin.weight[0, 0].item(), 4), "| grad:", lin.weight.grad)
logits = lin(x); print("1 forward: logits", tuple(logits.shape), logits.grad_fn.name())
loss = F.cross_entropy(logits, y); print("2 loss:", round(loss.item(), 4))
opt.zero_grad(); print("3 zero_grad: grad =", lin.weight.grad)
loss.backward(); print("4 backward: grad[0,0] =", round(lin.weight.grad[0, 0].item(), 4),
                       "| w[0,0] unchanged =", round(lin.weight[0, 0].item(), 4))
opt.step(); print("5 step: w[0,0] =", round(lin.weight[0, 0].item(), 4))
with torch.no_grad(): print("next loss:", round(F.cross_entropy(lin(x), y).item(), 4))

সত্যিকারের run (ডেস্কটপ, CPU):

~/bazarghar-dl$ uv run python extras.py step
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
start w[0,0] = -0.0053 | grad: None
1 forward: logits (4, 3) AddmmBackward0
2 loss: 1.3007
3 zero_grad: grad = None
4 backward: grad[0,0] = 0.1648 | w[0,0] unchanged = -0.0053
5 step: w[0,0] = -0.0218
next loss: 1.2371
ক্রমের নিয়ম

দুটি শর্তই আসল: zero_grad আগে, backward পরে (নইলে পুরোনো gradient যোগ হয়), আর backward আগে, step পরে (নইলে step পুরোনো বা খালি gradient দেখে)। zero_grad একদম শুরুতে বসালে ক্রম হয় zero_grad → forward → loss → backward → step; এটিও সমান সঠিক, অনেক কোডে এটাই দেখবেন।

২ · Loss: logits, CrossEntropyLoss আর BCEWithLogitsLoss

Model-এর শেষ Linear layer যে কাঁচা সংখ্যা দেয়, সেগুলো logits: যেকোনো বাস্তব সংখ্যা, যোগফল ১ হওয়ার শর্ত নেই। শ্রেণিবিভাগে প্রচলিত loss cross-entropy, আর PyTorch-এ তার রূপ nn.CrossEntropyLoss; এটি দুটি জিনিস চায়: আকার (N, C)-এর float logits, আর আকার (N,)-এর int64 শ্রেণি-সূচক (০ থেকে C − ১)। ভেতরে সে log-softmax আর negative log-likelihood একসঙ্গে করে, log-sum-exp কৌশলে, যাতে বড় logits-এও overflow না হয় (পর্ব ০৩-এর stable softmax). Gradient-ও সরল: ∂L/∂z = p − yone-hot, ঠিক পর্ব ০৪-এর P − Y.

দুটি লেখার ধরন আছে: module (nn.CrossEntropyLoss(), একবার তৈরি করে বারবার ডাকা; class weight, label_smoothing, reduction এর ভেতরে থাকে) আর function (F.cross_entropy(logits, y)). ফল একই। দুই শ্রেণির (হ্যাঁ/না) সমস্যায় একটি logit আর float label নিয়ে BCEWithLogitsLoss, যা sigmoid আর binary cross-entropy একসঙ্গে স্থিরভাবে করে। আলাদা sigmoid + BCELoss লিখলে কী হয়, সত্যিকারের run-এ দেখুন:

~/bazarghar-dl$ uv run python extras.py losses
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
nn.CrossEntropyLoss: 0.2413 | F.cross_entropy: 0.2413 | -log softmax: 0.2413
BCEWithLogitsLoss(200, 0): 200.0
BCELoss(sigmoid(200), 0): 100.0
scheduler আগে: UserWarning - Detected call of `lr_scheduler.step()` before `optimizer.step()`.

প্রথম লাইনে logits [২, ০.৫, −১] আর সঠিক শ্রেণি ০-এর জন্য তিনটি পথ একই ০.২৪১৩ দিল। পরের দুই লাইন সংখ্যাগত স্থিরতার গল্প: logit ২০০ আর আসল label ০ হলে সঠিক loss ২০০. Sigmoid(২০০) float32-এ হুবহু ১.০ হয়ে যায়, log(১ − ১) = −∞, আর BCELoss log-কে −১০০-এ কেটে দেয়; ফলে loss আটকে যায় ১০০-তে। শেষ লাইনটি অংশ ৩-এর বিষয়।

সবচেয়ে প্রচলিত ভুল: model-এর শেষে softmax বসিয়ে সেই probability CrossEntropyLoss-এ দেওয়া। তখন softmax দুবার চলে। নিচের ব্লক NumPy-তে তিনটি logits-এর জন্য দুই পথের loss আর gradient-এর আকার মাপে।

import numpy as np

def softmax(z):
    e = np.exp(z - z.max()); return e / e.sum()

def ce_and_grad(z, y, double=False):            # একটি উদাহরণ, ৩টি শ্রেণি
    p = softmax(z)
    if not double:                              # ঠিক: CE সরাসরি logits-এ
        g = p.copy(); g[y] -= 1                 # ∂L/∂z = p − one-hot
        return -np.log(p[y]), g
    q = softmax(p)                              # ভুল: probability-কে আবার softmax
    gq = q.copy(); gq[y] -= 1                   # ∂L/∂p
    J = np.diag(p) - np.outer(p, p)             # softmax-এর Jacobian
    return -np.log(q[y]), J @ gq                # chain rule: ∂L/∂z

y = 0                                           # সঠিক শ্রেণি 0
for z in ([4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 0.0]):
    z = np.array(z)
    l1, g1 = ce_and_grad(z, y); l2, g2 = ce_and_grad(z, y, double=True)
    print(f"logits {z} | ঠিক: loss {l1:.4f}, |grad| {np.linalg.norm(g1):.4f}"
          f" | softmax-এর পরে: loss {l2:.4f}, |grad| {np.linalg.norm(g2):.4f}")
best = -np.log(softmax(np.array([1.0, 0.0, 0.0]))[0])
print(f"softmax-এর পরে loss-এর নিচের সীমা (৩ শ্রেণি): {best:.4f}")

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

logits [4. 0. 0.] | ঠিক: loss 0.0360, |grad| 0.0433 | softmax-এর পরে: loss 0.5743, |grad| 0.0274
logits [0. 4. 0.] | ঠিক: loss 4.0360, |grad| 1.3769 | softmax-এর পরে: loss 1.5213, |grad| 0.0374
logits [0. 0. 0.] | ঠিক: loss 1.0986, |grad| 0.8165 | softmax-এর পরে: loss 1.0986, |grad| 0.2722
softmax-এর পরে loss-এর নিচের সীমা (৩ শ্রেণি): 0.5514

দ্বিতীয় লাইনটিই সবচেয়ে বিপজ্জনক: model দৃঢ়ভাবে ভুল শ্রেণি বলছে। সঠিক পথে gradient-এর আকার ১.৩৭৬৯, অর্থাৎ জোরালো সংশোধন; দ্বিগুণ softmax-এ ০.০৩৭৪, প্রায় ৩৭ গুণ ছোট। কারণ probability ০ থেকে ১-এর মধ্যে থাকে, তাই দ্বিতীয় softmax-এর input-গুলো কাছাকাছি, আর তার Jacobian (diag(p) − ppᵀ) gradient-কে আরও চেপে দেয়। শেষ লাইন: model নিখুঁত হলেও probability হবে [১, ০, ০], যার softmax e/(e + ২), তাই loss কখনো −ln(e/(e + ২)) ≈ ০.৫৫১৪-এর নিচে নামবে না। অংশ ৯-এ বাজারঘরের ডেটায় এর আসল প্রভাব দেখবেন।

৩ · Optimizer আর scheduler — SGD-momentum, AdamW আর ডাকার ক্রম

torch.optim-এর optimizer-কে parameter-এর তালিকা দিয়ে তৈরি করতে হয়: torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9) বা torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-2). Momentum আগের ধাপগুলোর একটি চলমান যোগফল রাখে; Adam প্রতিটি parameter-এর জন্য gradient-এর গড় আর বর্গের গড় রাখে; AdamW তার সঙ্গে weight decay gradient-এ না মিশিয়ে আলাদাভাবে ওজন ছোট করে। এই তিনটির derivation আর তুলনা মেশিন লার্নিং কোর্সের পর্ব ১৪-এ; SGD আর mini-batch-এর ভিত্তি পর্ব ১৩-এ।

এই পর্বের জন্য একটি ব্যবহারিক কথাই জরুরি: optimizer-এর নিজের অবস্থা (state) আছে। AdamW-এর প্রতিটি parameter-এর পাশে exp_avg আর exp_avg_sq নামে সমান আকারের দুটি tensor আর একটি step গণনা থাকে, যা opt.state_dict()-এ ধরা পড়ে। তাই training থামিয়ে আবার শুরু করলে শুধু model-এর weight ফেরালে চলে না (অংশ ৮)। আর param_groups-এ থাকে learning rate আর weight decay; আলাদা layer-এ আলাদা learning rate দিতে চাইলে এখানেই দেওয়া হয়।

Learning-rate schedule training চলার সঙ্গে learning rate বদলায়। PyTorch-এ scheduler একটি আলাদা object, যা optimizer-এর lr বদলে দেয়। নিয়ম: scheduler.step() সবসময় optimizer.step()-এর পরে। PyTorch 1.1 থেকে এই ক্রম চালু; উল্টো ক্রমে ডাকলে PyTorch সতর্ক করে, যা আগের run-এর শেষ লাইনে দেখেছেন: UserWarning - Detected call of lr_scheduler.step() before optimizer.step(). উল্টো ক্রমে schedule-এর প্রথম মানটি বাদ পড়ে যায়। কোন scheduler প্রতি epoch-এ আর কোনটি প্রতি batch-এ ডাকতে হয়, আর warmup ও cosine, তা পর্ব ১৫ (Optimizer আর learning-rate schedule)-এর বিষয়।

৪ · Validation loop — model.eval() আর torch.no_grad() কেন দুটোই

Validation-এর উদ্দেশ্য মাপা, শেখা নয়। তাই সেখানে দুটি জিনিস বদলাতে হয়, আর দুটির জন্য দুটি আলাদা সুইচ:

model.eval() — আচরণের সুইচ

Model আর তার সব submodule-এর training flag False করে, অর্থাৎ eval mode চালু। এতে কিছু layer আচরণ বদলায়: Dropout আর কিছু বন্ধ করে না, সব neuron রাখে; BatchNorm চলমান গড় ব্যবহার করে (পর্ব ১৪-এ)। model.train() আবার চালু করে। Gradient হিসাবের ওপর এর কোনো প্রভাব নেই।

torch.no_grad() — হিসাবের খাতার সুইচ

এই no_grad context-এর ভেতরে PyTorch graph লেখে না, তাই মাঝের activation মনে রাখতে হয় না: memory আর সময় বাঁচে, আর ভুল করে backward চালানোর পথ বন্ধ। Layer-এর আচরণ বদলায় না। গভীর আলোচনা, আর inference_mode-এর সঙ্গে পার্থক্য, পর্ব ০৮ (PyTorch autograd)-এ।

দুটির প্রভাব আলাদা করে দেখতে বাজারঘরের best model (অংশ ৭) দিয়ে ২০০টি validation উদাহরণে চারটি অবস্থায় দুবার করে loss মাপা হলো (model-এ ০.২ হারের দুটি dropout layer আছে):

best.pt (epoch 4), ২০০টি val উদাহরণ, সত্যিকারের run
model mode
gradient হিসাব
val loss, দুবার
out.requires_grad
  1. দুটিই ভুলে গেলে. Dropout প্রতিবার আলাদা neuron বন্ধ করে, তাই একই model, একই ডেটায় loss ০.৫৯২৫, তারপর ০.৬০৮৬. সঙ্গে পুরো graph তৈরি হচ্ছে, যা কেউ ব্যবহার করবে না।
  2. শুধু no_grad. Graph বন্ধ (requires_grad False), কিন্তু dropout তখনো চালু: ০.৫৭০৬ আর ০.৬২৩৪. Memory বাঁচল, সংখ্যা এখনো এলোমেলো।
  3. শুধু eval(). সংখ্যা স্থির, দুবারই ০.৫৮৩৭. কিন্তু output-এর requires_grad True: প্রতিটি batch-এর graph আর activation memory-তে জমছে। বড় model-এ এতেই memory ফুরিয়ে যায়।
  4. দুটিই. স্থির ০.৫৮৩৭ আর কোনো graph নেই। Validation, test আর inference-এ এটিই নিয়ম।
  5. সারকথা. eval() layer-এর আচরণ বদলায়, no_grad হিসাবের খাতা বন্ধ করে। একটি দিয়ে অন্যটির কাজ হয় না। আর validation শেষে model.train() ফিরিয়ে দিতে ভুলবেন না।

৫ · Metric ঠিকভাবে গড় করা — অসমান শেষ batch

CrossEntropyLoss-এর default reduction="mean", তাই প্রতিটি batch-এর loss সেই batch-এর উদাহরণগুলোর গড়। ২০০টি উদাহরণকে ৬৪-র batch-এ ভাগ করলে আকার হয় ৬৪, ৬৪, ৬৪ আর ৮. চারটি batch-এর গড়ের সরল গড় নিলে শেষের ৮টি উদাহরণ বাকি ১৯২টির সমান ওজন পায়। পুরো সেটের আসল গড় পেতে প্রতিটি batch-এর মানকে তার আকার nb দিয়ে ওজন দিতে হয়:

সঠিক গড় = Σb nb · mb / Σb nb

এখানে mb হলো batch b-এর গড় loss বা accuracy. এটিই weighted mean; কোডে সহজ রূপ হলো total += loss.item() * len(yb), শেষে total / n. বাজারঘরের best model-এ সত্যিকারের run:

~/bazarghar-dl$ uv run python extras.py val
torch 2.15.0+cpu | numpy 2.5.4 | python 3.14.4
best.pt epoch: 4 | keys: ['epoch', 'model', 'optimizer', 'best_val', 'bad_epochs', 'torch_rng', 'loader_rng']
batch sizes: [64, 64, 64, 8] | batch loss: [0.5634, 0.8096, 0.4105, 0.3248]
naive mean loss 0.5271 | weighted 0.5837 | full set 0.5837
naive mean acc 0.8164 | weighted 0.8000
train grad=True  | val loss x2 [0.5925, 0.6086] | requires_grad True
train grad=False | val loss x2 [0.5706, 0.6234] | requires_grad False
eval  grad=True  | val loss x2 [0.5837, 0.5837] | requires_grad True
eval  grad=False | val loss x2 [0.5837, 0.5837] | requires_grad False

শেষের ৮টি উদাহরণের loss ছোট (০.৩২৪৮), আর সরল গড় তাকে এক-চতুর্থাংশ ওজন দিয়ে val loss ০.৫৮৩৭ থেকে ০.৫২৭১-এ নামিয়ে দিল, প্রায় ১০% আশাবাদী ভুল। Weighted গড় পুরো সেটে একবারে হিসাব করা মানের সঙ্গে হুবহু এক। নিচের ব্লকে একই কাজ দুটি ছোট function দিয়ে, আর দেখুন কখন দুটি পথ এক উত্তর দেয়।

import numpy as np

rng = np.random.default_rng(42)                 # ২০০টি val উদাহরণ, batch 64 → 64, 64, 64, 8
loss = rng.exponential(0.55, 200)               # প্রতিটি উদাহরণের loss
correct = rng.random(200) < 0.80                # প্রতিটি উদাহরণ ঠিক হলো কি না
loss[192:] *= 0.3; correct[192:] = True         # শেষ ছোট batch-টি "সহজ"

def batch_stats(bs):
    out = []                                    # প্রতি batch: (গড় loss, accuracy, আকার)
    for s in range(0, len(loss), bs):
        out.append((loss[s:s + bs].mean(), correct[s:s + bs].mean(), len(loss[s:s + bs])))
    return out

def naive(stats):                               # ভুল: প্রতিটি batch সমান ওজন
    return np.mean([l for l, a, n in stats]), np.mean([a for l, a, n in stats])

def weighted(stats):                            # ঠিক: batch-এর আকার দিয়ে ওজন
    N = sum(n for l, a, n in stats)
    return sum(l * n for l, a, n in stats) / N, sum(a * n for l, a, n in stats) / N

for bs in (64, 50):
    st = batch_stats(bs)
    print(f"batch {bs}: আকার {[n for *_, n in st]}")
    print("  naive    loss %.4f acc %.4f" % naive(st))
    print("  weighted loss %.4f acc %.4f" % weighted(st))
print(f"পুরো সেটের আসল মান: loss {loss.mean():.4f} acc {correct.mean():.4f}")

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

batch 64: আকার [64, 64, 64, 8]
  naive    loss 0.4372 acc 0.8516
  weighted loss 0.5156 acc 0.8100
batch 50: আকার [50, 50, 50, 50]
  naive    loss 0.5156 acc 0.8100
  weighted loss 0.5156 acc 0.8100
পুরো সেটের আসল মান: loss 0.5156 acc 0.8100

Batch ৬৪-তে সরল গড় accuracy ০.৮৫১৬ দেখাল, আসল ০.৮১০০: শেষের ৮টি সহজ উদাহরণ একটি পুরো batch-এর ওজন পেয়েছে। Batch ৫০-এ সব batch সমান, তাই দুই পথ এক। এই কারণেই ভুলটি লুকিয়ে থাকে: batch-এর আকার ডেটার আকারকে ভাগ করলে কিছুই ধরা পড়ে না, batch size বদলালেই সংখ্যা চুপচাপ সরে যায়। Validation-এ drop_last=True দিয়ে সমস্যা এড়ানোও ভুল, কারণ তখন কিছু উদাহরণ মাপাই হয় না (DataLoader-এর খুঁটিনাটি পর্ব ১১-এ)।

৬ · NumPy-তে পুরো loop — পাতাতেই চালিয়ে দেখুন

PyTorch-এর loop-এর প্রতিটি অংশের একটি NumPy প্রতিরূপ লিখলে বোঝা যায় কোন লাইন আসলে কী করে। নিচের ব্লকে তিন শ্রেণির ২ feature-এর ৩০০টি বিন্দু (১৫% label উল্টানো), ২৪০টি train আর ৬০টি validation; ২ → ৬৪ → ৩-এর একটি MLP; momentum-সহ SGD; ৩২-এর এলোমেলো mini-batch; প্রতি epoch-এ validation; ২০ epoch উন্নতি না হলে early stopping; আর সেরা weight-এর একটি কপি, যা একটি checkpoint-এর কাজ করে। Backward-এর সূত্র পর্ব ০৪-এর।

import numpy as np, matplotlib.pyplot as plt

rng = np.random.default_rng(42)                  # ৩ শ্রেণি, ২ feature, ১৫% label noise
y = rng.integers(0, 3, 300); X = rng.normal(0, 1.1, (300, 2)) + np.array([[0, 2], [2, -1], [-2, -1]])[y]
y = np.where(rng.random(300) < 0.15, rng.integers(0, 3, 300), y)
Xtr, ytr, Xva, yva = X[:240], y[:240], X[240:], y[240:]
P = {"W1": rng.normal(0, 0.5, (2, 64)), "b1": np.zeros(64), "W2": rng.normal(0, 0.3, (64, 3)), "b2": np.zeros(3)}
V = {k: np.zeros_like(v) for k, v in P.items()}  # momentum buffer (optimizer state)

def forward(X):
    H = np.maximum(X @ P["W1"] + P["b1"], 0); Z = H @ P["W2"] + P["b2"]
    Z = Z - Z.max(1, keepdims=True); p = np.exp(Z) / np.exp(Z).sum(1, keepdims=True)
    return H, p
def ce(p, y): return -np.log(p[np.arange(len(y)), y]).mean()

hist, best, bad = [], (np.inf, 0, None), 0
for epoch in range(1, 301):
    idx = rng.permutation(240); tot = 0.0
    for s in range(0, 240, 32):                  # শেষ batch ১৬টির
        b = idx[s:s + 32]; H, p = forward(Xtr[b])            # ১ forward
        tot += ce(p, ytr[b]) * len(b)                        # ২ loss (ওজনসহ যোগ)
        G = p.copy(); G[np.arange(len(b)), ytr[b]] -= 1; G /= len(b)   # ৩-৪ নতুন gradient
        dH = (G @ P["W2"].T) * (H > 0)
        g = {"W2": H.T @ G, "b2": G.sum(0), "W1": Xtr[b].T @ dH, "b1": dH.sum(0)}
        for k in P: V[k] = 0.9 * V[k] + g[k]; P[k] -= 0.05 * V[k]   # ৫ step
    va = ce(forward(Xva)[1], yva); hist.append((tot / 240, va))  # validation: শুধু forward
    if va < best[0]: best, bad = (va, epoch, {k: v.copy() for k, v in P.items()}), 0
    else: bad += 1
    if bad >= 20: break
print(f"থামল epoch {epoch}-এ | best epoch {best[1]}, val loss {best[0]:.4f}")
P = best[2]; acc = (forward(Xva)[1].argmax(1) == yva).mean()
print(f"best checkpoint-এ val accuracy {acc:.3f} | শেষ epoch-এ train loss {hist[-1][0]:.4f}")
h = np.array(hist); plt.plot(h[:, 0], label="train loss"); plt.plot(h[:, 1], label="val loss")
plt.axvline(best[1] - 1, ls="--", c="gray", label="best epoch"); plt.xlabel("epoch")
plt.ylabel("cross-entropy"); plt.legend(); plt.title("NumPy training loop with early stopping"); plt.show()

আউটপুট (পাতার Python-এ চালানো; নিচে একটি চিত্রও আঁকা হয়):

থামল epoch 33-এ | best epoch 13, val loss 0.4291
best checkpoint-এ val accuracy 0.833 | শেষ epoch-এ train loss 0.4681

PyTorch-এর সঙ্গে মিল: forward = model(xb), ce = CrossEntropyLoss, G আর g তৈরি = backward() (প্রতিবার নতুন dict, তাই zero_grad-এর দরকার নেই), V আর তার আপডেট = opt.step(), যার V হলো optimizer state. Validation-এ শুধু forward চলে, কোনো gradient নয়: NumPy-তে এটিই no_grad. Val loss epoch ১৩-এ সবচেয়ে কম ছিল; তারপর ২০ epoch উন্নতি নেই, তাই epoch ৩৩-এ থামা, আর সেরা কপি ফিরিয়ে val accuracy ০.৮৩৩. চিত্রে train loss নামতে থাকে, val loss একটি জায়গার পরে আর নামে না: এখান থেকেই overfitting শুরু। Train loss গোনা হয়েছে epoch চলার সময়ের batch-গুলো থেকে, ওজন দিয়ে (tot / 240).

৭ · PyTorch-এ পূর্ণ training — বাজারঘরের customer segment

এবার আসল PyTorch. বাজারঘর তাদের ক্রেতাদের তিনটি দলে ভাগ করতে চায়: মাঝেমধ্যে কেনেন (occasional), নিয়মিত (regular), আর ছাড় খোঁজেন (deal_seeker). প্রতিটি ক্রেতার ৫টি feature: মাসে অর্ডার, গড় ঝুড়ির দাম (টাকা), শেষ অর্ডারের পর কত দিন, ছাড়ের পণ্যের ভাগ, আর অ্যাপ থেকে কেনার ভাগ। ডেটা বানানো আর seeded: ৬০০ জন ক্রেতা, ১০% label উল্টানো, অর্থাৎ label noise (বাস্তবেও label-এ ভুল থাকে)। Seed দেওয়া torch.Generator দিয়ে ২০০ জন validation-এ, ৪০০ জন train-এ; standardization-এর গড় আর মানবিচ্যুতি শুধু train থেকে, যাতে validation-এর তথ্য train-এ ঢুকে data leakage না ঘটে (AI-এর ভিত্তি কোর্সের পর্ব ৫৬)। Project আগের পর্বগুলোর মতো uv দিয়ে:

~/bazarghar-dl$ uv init --vcs none --author-from none --name bazarghar-dl --no-readme --python 3.14
Initialized project `bazarghar-dl`
# pyproject.toml-এ PyTorch-এর CPU wheel index যোগ করা হলো ([[tool.uv.index]], explicit = true)
~/bazarghar-dl$ uv add torch==2.15.0 numpy
 …
 + numpy==2.5.4
 …
 + torch==2.15.0+cpu
 …

Script-এর প্রথম অর্ধেক: ডেটা, DataLoader, model, optimizer আর এক epoch চালানোর function. run_epoch একই function-এ train আর validation দুটোই করে: model.train(False) আর model.eval() একই কাজ, আর torch.set_grad_enabled(False) = no_grad. Model-এ ১২৮ unit-এর দুটি hidden layer আর ০.২ হারের dropout, মোট ১৭,৬৬৭টি parameter (গোনার নিয়ম পর্ব ০৯-এ)।

import argparse, numpy as np, torch, torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from bazarghar_data import split

ap = argparse.ArgumentParser()
ap.add_argument("--resume"); ap.add_argument("--stop-after", type=int)
ap.add_argument("--skip", choices=["rng", "optimizer"])     # পরীক্ষা: কিছু বাদ দিয়ে resume
args = ap.parse_args()
torch.manual_seed(0)
Xtr, ytr, Xva, yva = split()
g = torch.Generator().manual_seed(0)                   # shuffle-এর নিজস্ব RNG
train_dl = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True, generator=g)
val_dl = DataLoader(TensorDataset(Xva, yva), batch_size=64)
model = nn.Sequential(nn.Linear(5, 128), nn.ReLU(), nn.Dropout(0.2),
                      nn.Linear(128, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 3))
opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-2)
loss_fn = nn.CrossEntropyLoss()                        # logits + class index

def run_epoch(dl, train):
    model.train(train)                                 # train(False) == eval()
    total, correct, n = 0.0, 0, 0
    with torch.set_grad_enabled(train):                # validation-এ no_grad
        for xb, yb in dl:
            logits = model(xb)                         # ১ forward
            loss = loss_fn(logits, yb)                 # ২ loss
            if train:
                opt.zero_grad()                        # ৩ পুরোনো gradient মোছা
                loss.backward()                        # ৪ backward
                opt.step()                             # ৫ parameter আপডেট
            total += loss.item() * len(yb)             # batch-এর আকার দিয়ে ওজন
            correct += (logits.argmax(1) == yb).sum().item(); n += len(yb)
    return total / n, correct / n

দ্বিতীয় অর্ধেক: checkpoint সেভ, resume আর epoch-এর লুপ। Early stopping-এর patience ৮: val loss টানা ৮ epoch আগের সেরার চেয়ে না কমলে থামা। প্রতি epoch-এ last.pt সেভ হয় (থামলে ফেরার জন্য), আর সেরা হলে best.pt (শেষে ব্যবহারের জন্য)।

def save(path, epoch):
    torch.save({"epoch": epoch, "model": model.state_dict(), "optimizer": opt.state_dict(),
                "best_val": best_val, "bad_epochs": bad, "torch_rng": torch.get_rng_state(),
                "loader_rng": g.get_state()}, path)

start, best_val, bad, patience = 1, float("inf"), 0, 8
if args.resume:
    ck = torch.load(args.resume)                       # weights_only=True (default)
    model.load_state_dict(ck["model"])
    if args.skip != "optimizer": opt.load_state_dict(ck["optimizer"])
    if args.skip != "rng": torch.set_rng_state(ck["torch_rng"]); g.set_state(ck["loader_rng"])
    start, best_val, bad = ck["epoch"] + 1, ck["best_val"], ck["bad_epochs"]
    print(f"resumed from epoch {ck['epoch']}")
for epoch in range(start, 101):
    tr_loss, tr_acc = run_epoch(train_dl, True)
    va_loss, va_acc = run_epoch(val_dl, False)
    mark = ""
    if va_loss < best_val:
        best_val, bad, mark = va_loss, 0, " *best"
        save("best.pt", epoch)
    else:
        bad += 1
    print(f"ep {epoch:3d} | train {tr_loss:.4f} {tr_acc:.3f} | val {va_loss:.4f} {va_acc:.3f}{mark}")
    save("last.pt", epoch)
    if bad >= patience:
        print(f"early stop: val loss {patience} epoch ধরে কমেনি; best epoch {epoch - patience}"); break
    if args.stop_after == epoch:
        print("(interrupted)"); break
torch.save(model.state_dict(), "final_weights.pt")

সত্যিকারের run (ডেস্কটপ, CPU, Python 3.14.4, PyTorch 2.15.0 CPU build, NumPy 2.5.4; লেখার সময়ের সংস্করণ, পরে বদলাতে পারে; পুরো run কয়েক সেকেন্ডের):

~/bazarghar-dl$ uv run python train.py
ep   1 | train 0.8820 0.698 | val 0.6440 0.765 *best
ep   2 | train 0.5751 0.760 | val 0.6244 0.785 *best
ep   3 | train 0.5379 0.787 | val 0.6282 0.800
ep   4 | train 0.5355 0.797 | val 0.5837 0.800 *best
ep   5 | train 0.5219 0.782 | val 0.6157 0.810
ep   6 | train 0.5131 0.795 | val 0.6073 0.805
ep   7 | train 0.4961 0.797 | val 0.6242 0.780
ep   8 | train 0.5064 0.792 | val 0.6245 0.800
ep   9 | train 0.4871 0.810 | val 0.6314 0.795
ep  10 | train 0.4949 0.815 | val 0.6552 0.785
ep  11 | train 0.4946 0.797 | val 0.6372 0.805
ep  12 | train 0.4955 0.805 | val 0.6489 0.785
early stop: val loss 8 epoch ধরে কমেনি; best epoch 4

বাজারঘর customer segment — train আর val loss (সত্যিকারের run)

Train loss keeps falling; val loss is lowest at epoch 4, then drifts up

Train and validation loss per epoch for the bazarghar customer-segment model 0.90 0.80 0.70 0.60 0.50 1 4 8 12 epoch best (epoch 4) early stop — train loss — val loss
ছবি ১০.২ — ওপরের run-এর ১২টি epoch. Epoch ৪-এর পরে train loss নামতে থাকে (০.৫৩৫৫ থেকে ০.৪৯৫৫), val loss উঠতে থাকে (০.৫৮৩৭ থেকে ০.৬৪৮৯): model train ডেটার noise মুখস্থ করছে।

তিনটি পর্যবেক্ষণ। এক: Val loss epoch ৪-এ সবচেয়ে কম (০.৫৮৩৭), তারপর ৮ epoch আর কমেনি, তাই epoch ১২-এ থামা, আর best.pt-এ epoch ৪-এর model. দুই: Accuracy আর loss একসঙ্গে চলে না: epoch ৫-এ val accuracy ০.৮১০, সবচেয়ে বেশি, অথচ val loss বেড়েছে। Loss probability-র আত্মবিশ্বাসও মাপে, accuracy শুধু সবচেয়ে বড় শ্রেণি। এখানে best বাছা হয়েছে val loss দিয়ে; ব্যবসার লক্ষ্য accuracy হলে সেটা দিয়েও বাছা যায়, কিন্তু তা আগে থেকে ঠিক করুন। তিন: Train loss আর val loss সরাসরি তুলনীয় নয়: train-এর সংখ্যা dropout চালু রেখে, epoch চলার সময় বদলাতে থাকা weight দিয়ে মাপা; epoch ১-এ তাই train loss (০.৮৮২০) val loss-এর (০.৬৪৪০) চেয়ে বেশি। আর এটি একটি seed-এর একটি run; ২০০ জনের val-এ accuracy-র ০.০১ পার্থক্য মানে মাত্র ২ জন ক্রেতা।

Early stopping আর best checkpoint দুটোই validation set দেখে সিদ্ধান্ত নেয়, তাই validation-এর সংখ্যা একটু আশাবাদী। Model-এর চূড়ান্ত মান জানাতে একটি আলাদা test set রাখুন, যা শুধু শেষে একবার দেখা হবে। Early stopping-এর তত্ত্ব (কেন এটি regularization-এর মতো কাজ করে) মেশিন লার্নিং কোর্সের পর্ব ১৭-এ, আর dropout, weight decay-এর সঙ্গে মিলিয়ে ব্যবহার পর্ব ১৬-তে।

৮ · Checkpoint আর resume — থামা জায়গা থেকে হুবহু একই পথে

Load-shedding-এ কম্পিউটার বন্ধ হয়ে গেল, তিন ঘণ্টার training-এর দুই ঘণ্টা শেষ। শুধু model-এর weight সেভ থাকলে আবার শুরু করা যায়, কিন্তু সেই run আর আগের run এক থাকে না। এটা যেন পরীক্ষার খাতা দেখার মাঝে উঠে যাওয়া: শুধু “কত নম্বর দেওয়া হয়েছে” লিখে রাখলে চলে না, কোন খাতা পর্যন্ত দেখা হলো, কোন নিয়মে কাটা হচ্ছিল, তাও লাগে।

Checkpoint হলো একটি dictionary, যা torch.save দিয়ে ফাইলে যায়: model আর optimizer-এর state_dict, loop-এর হিসাব আর RNG state. হুবহু resume-এর জন্য কী কী লাগে, আর কোনটি বাদ দিলে কী হয়, নিচের stepper-এ; “বাদ দিলে” সারির সংখ্যাগুলো সত্যিকারের পরীক্ষা: epoch ৬-এ থামিয়ে last.pt থেকে আবার চালানো, কখনো একটি অংশ বাদ দিয়ে।

Resume পরীক্ষা: অবিরাম run-এ epoch 7-এর train loss 0.4961
key
ভেতরে কী
বাদ দিলে
  1. Model. model.state_dict(): প্রতিটি layer-এর weight আর bias, নাম ধরে। এটি ছাড়া কিছুই হয় না; inference-এর জন্য এটুকুই যথেষ্ট। (state_dict-এর গঠন পর্ব ০৯-এ।)
  2. Optimizer. AdamW-এর প্রতিটি parameter-এর দুটি চলমান গড় আর ধাপের গণনা (৪ epoch × ৭ batch = ২৮)। বাদ দিলে Adam শূন্য থেকে গড় শুরু করে, আর bias correction-এর জন্য প্রথম কয়েক ধাপ অন্যরকম বড় হয়: epoch ৭-এর train loss ০.৪৯৬১-এর বদলে ০.৫১৩৩.
  3. Loop-এর অবস্থা. কোন epoch শেষ, এখন পর্যন্ত সেরা val loss, আর কত epoch উন্নতি নেই। বাদ দিলে epoch গোনা আবার শুরু হয় আর early stopping ভুল সময়ে থামে বা থামে না। Scheduler থাকলে তার state_dict()-ও এখানে লাগে।
  4. RNG state. Dropout বৈশ্বিক torch RNG থেকে mask নেয়, আর DataLoader-এর shuffle নিজের generator g থেকে। বাদ দিলে seed আবার শুরু থেকে চলে, epoch ৭ পায় epoch ১-এর shuffle: train loss ০.৪৮০২. NumPy বা Python-এর random ব্যবহার করলে সেগুলোর state-ও রাখুন।
  5. সব একসঙ্গে. ২২৩ KB-এর ফাইল: weight প্রায় ৬৯ KiB, Adam-এর দুটি গড় তার দ্বিগুণ, RNG প্রায় ১০ KiB. এর সবকিছু ফেরালে অবিরাম run-এর সঙ্গে প্রতিটি লাইন আর প্রতিটি weight বিট ধরে এক (নিচের run).

এখন আসল পরীক্ষা: একবার অবিরাম ১২ epoch (full.log), আর একবার epoch ৬-এ থামিয়ে নতুন process-এ --resume last.pt. তারপর compare.py নামের ছোট script দুই run-এর শেষ weight (final_weights.pt) torch.equal দিয়ে আর epoch ৭–১২-এর log লাইন মেলায়।

~/bazarghar-dl$ uv run python train.py > full.log
~/bazarghar-dl$ cp final_weights.pt full_weights.pt      # অবিরাম run-এর weight আলাদা নামে
~/bazarghar-dl$ uv run python train.py --stop-after 6
 …
ep   6 | train 0.5131 0.795 | val 0.6073 0.805
(interrupted)
~/bazarghar-dl$ uv run python train.py --resume last.pt | tee resumed.log
resumed from epoch 6
ep   7 | train 0.4961 0.797 | val 0.6242 0.780
ep   8 | train 0.5064 0.792 | val 0.6245 0.800
 …
ep  12 | train 0.4955 0.805 | val 0.6489 0.785
early stop: val loss 8 epoch ধরে কমেনি; best epoch 4
~/bazarghar-dl$ uv run python compare.py
tensors: 6 | সব হুবহু এক (torch.equal): True
epoch 7-12 log লাইন এক: True

# আবার epoch 6-এ থামিয়ে, এবার RNG state বাদ দিয়ে resume
~/bazarghar-dl$ uv run python train.py --resume last.pt --skip rng | tee resumed.log
resumed from epoch 6
ep   7 | train 0.4802 0.802 | val 0.6251 0.775
 …
~/bazarghar-dl$ uv run python compare.py
tensors: 6 | সব হুবহু এক (torch.equal): False
epoch 7-12 log লাইন এক: False

# আবার epoch 6-এ থামিয়ে, এবার optimizer state বাদ দিয়ে
~/bazarghar-dl$ uv run python train.py --resume last.pt --skip optimizer | tee resumed.log
resumed from epoch 6
ep   7 | train 0.5133 0.787 | val 0.6236 0.795
 …
~/bazarghar-dl$ uv run python compare.py
tensors: 6 | সব হুবহু এক (torch.equal): False
epoch 7-12 log লাইন এক: False

সব কিছু রাখলে মিল নিখুঁত, শুধু কাছাকাছি নয়। এই ধরনের reproducibility-র নিশ্চয়তা একই যন্ত্র, একই library version আর CPU-র জন্য। GPU-তে কিছু কাজ নিজে থেকেই অনির্ধারিত (non-deterministic), তাই সেখানে হুবহু মিলের জন্য torch.use_deterministic_algorithms(True)-এর মতো বাড়তি ব্যবস্থা লাগে, আর কিছু কাজ ধীর হয় (পর্ব ১২ আর পর্ব ১৯)। DataLoader-এ একাধিক worker থাকলে তাদের seed-ও ভাবতে হয় (পর্ব ১১)।

নিরাপদ load আর সেভ: PyTorch 2.6 থেকে torch.load-এর default weights_only=True: শুধু tensor, সংখ্যা, string আর সাধারণ container খোলে, ইচ্ছামতো Python object নয়। আমাদের checkpoint-এ শুধু এগুলোই আছে, তাই সমস্যা হয়নি। অজানা উৎসের checkpoint-এ weights_only=False দেবেন না, কারণ pickle ফাইল খোলার সময় কোড চালাতে পারে (AI-এর ভিত্তি কোর্সের পর্ব ৭১)। আরেকটি অভ্যাস: আগে একটি অস্থায়ী নামে লিখে তারপর os.replace দিয়ে নাম বদলান, যাতে লেখার মাঝে বিদ্যুৎ গেলে পুরোনো ভালো checkpoint নষ্ট না হয়।

৯ · তিনটি পরিচিত ভুল — সত্যিকারের ফল

এই তিনটি ভুল কোনো error দেয় না; শুধু model খারাপ শেখে বা ভুল সংখ্যা দেখায়। একই বাজারঘর ডেটা, একই model আর AdamW দিয়ে ১৫ epoch করে (early stopping ছাড়া) প্রতিটি ভুলসহ আর ভুল ছাড়া train করে মাপা হলো। Script-এর মূল অংশ:

def fit(bug=None, epochs=15):
    model = make(); g = torch.Generator().manual_seed(0)
    dl = DataLoader(TensorDataset(Xtr, ytr), batch_size=64, shuffle=True, generator=g)
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-2)
    for ep in range(epochs):
        model.train()
        for xb, yb in dl:
            out = model(xb)
            if bug == "softmax": out = torch.softmax(out, dim=1)   # ভুল: CE নিজেই log-softmax করে
            loss = loss_fn(out, yb)
            if bug != "no_zero_grad": opt.zero_grad()
            loss.backward(); opt.step()
    model.eval()
    with torch.no_grad():
        tr = loss_fn(model(Xtr), ytr).item()
        va_logits = model(Xva)
        va = loss_fn(va_logits, yva).item(); acc = (va_logits.argmax(1) == yva).float().mean().item()
    gnorm = torch.cat([p.grad.flatten() for p in model.parameters()]).norm().item()
    return model, tr, va, acc, gnorm
# … তারপর প্রতিটি ভুলের জন্য fit() ডেকে print; eval-এর পরীক্ষায় একই model-এ val loss তিনবার
~/bazarghar-dl$ uv run python mistakes.py
== ১) softmax-এর পরে CrossEntropyLoss (15 epoch, আসল logits দিয়ে loss মাপা)
None     train 0.4425 | val 0.6886 acc 0.790 | ভুল সূত্রে train loss 0.7604
softmax  train 1.0142 | val 1.3202 acc 0.785 | ভুল সূত্রে train loss 0.7309
softmax-এর পরে CE-র সর্বনিম্ন মান: 0.5514
== ২) zero_grad ভুলে যাওয়া
None         train 0.4425 | val 0.6886 acc 0.790 | শেষ grad norm 0.90
no_zero_grad train 0.8812 | val 0.9776 acc 0.710 | শেষ grad norm 52.58
== ৩) model.eval() ভুলে যাওয়া (একই model, val loss তিনবার)
train mode: [0.7181, 0.6821, 0.6816]
eval mode:  [0.6886, 0.6886, 0.6886]

১ · softmax-এর পরে CrossEntropyLoss

Accuracy প্রায় একই (০.৭৮৫ বনাম ০.৭৯০), তাই শুধু accuracy দেখলে ভুল চোখে পড়ে না। কিন্তু আসল logits-এ val loss ১.৩২০২, সঠিক model-এর প্রায় দ্বিগুণ: ভুল loss কখনো ০.৫৫১৪-এর নিচে নামে না, তাই model ছোট পার্থক্য পূরণ করতে logits অনেক বড় করে, আর তার probability অতি-আত্মবিশ্বাসী হয়ে যায়। Probability দিয়ে সিদ্ধান্ত (যেমন কোন ক্রেতাকে অফার পাঠানো) নিলে এটাই বিপদ।

২ · zero_grad ভুলে যাওয়া

Gradient শুরু থেকে জমতেই থাকে; শেষে gradient-এর norm ৫২.৫৮, সঠিক run-এর ০.৯০-এর প্রায় ৬০ গুণ। জমা যোগফলে পুরোনো ধাপের দিক প্রাধান্য পায়, তাই train loss ০.৮৮১২ আর val accuracy ০.৭১০-এ আটকে থাকে। Adam gradient-এর মাপ নিজে ভাগ করে নেয় বলে এখানে বিস্ফোরণ হয়নি; সাধারণ SGD-তে ধাপ নিজেই বড় হতে থাকে।

৩ · model.eval() ভুলে যাওয়া

একই model, একই ডেটায় তিনটি ভিন্ন val loss (০.৭১৮১, ০.৬৮২১, ০.৬৮১৬)। Early stopping বা best checkpoint এই এলোমেলো সংখ্যা দেখে সিদ্ধান্ত নিলে ভুল epoch বাছবে। eval()-এ তিনবারই ০.৬৮৮৬.

চতুর্থ ভুলটি অংশ ৫-এ দেখেছেন (সরল গড়), আর পঞ্চমটি অংশ ৩-এ (scheduler আগে)। একটি ভালো অভ্যাস: নতুন loop লিখে প্রথমে একটি ছোট batch-এ বারবার train করুন; loss প্রায় শূন্যে না নামলে loop-এ ভুল আছে। এই কৌশল আর learning curve পড়া পর্ব ১৮ (Training ডিবাগ করা)-এর বিষয়।

আরও গভীরে: Early stopping-এর কখন থামবেন সেই প্রশ্নের একটি পরিচিত পরীক্ষামূলক আলোচনা Lutz Prechelt-এর “Early Stopping — But When?” (Neural Networks: Tricks of the Trade, ১৯৯৮)। Dropout-এর বর্ণনা Hinton ও সহকর্মীদের ২০১২ সালের arXiv প্রবন্ধে আর Srivastava ও সহকর্মীদের ২০১৪ সালের JMLR প্রবন্ধে; সেখানে test-এর সময় সব unit রেখে ওজন মানিয়ে নেওয়ার ধারণা এসেছে, যা PyTorch উল্টো দিকে করে: training-এ টিকে থাকা unit-কে 1/(1 − p) গুণ করে (inverted dropout). AdamW আসে Loshchilov আর Hutter-এর “Decoupled Weight Decay Regularization” (arXiv ২০১৭, ICLR ২০১৯) থেকে। Optimizer, scheduler আর DataLoader-এর ঠিক আচরণ PyTorch-এর নিজের docs-এ (torch.optim, “Reproducibility” নোট) দেখে নিন, কারণ version-এর সঙ্গে default বদলায়।

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

  • এক training ধাপের পাঁচটি কাজের ক্রম? forward → loss → zero_grad → backward → step; zero_grad একদম শুরুতেও চলে।
  • CrossEntropyLoss-এর input? (N, C) float logits আর (N,) int64 শ্রেণি-সূচক; softmax নয়।
  • eval() আর no_grad()-এর পার্থক্য? eval() dropout/BatchNorm-এর আচরণ বদলায়; no_grad() graph বানানো বন্ধ করে; validation-এ দুটিই।
  • ৬৪, ৬৪, ৮ আকারের batch-এ accuracy ০.৭৫, ০.৭৫, ১.০; আসল accuracy? (৪৮ + ৪৮ + ৮)/১৩৬ = ০.৭৬৪৭, সরল গড় ০.৮৩৩৩ ভুল।
  • হুবহু resume-এর জন্য checkpoint-এ কী লাগে? model, optimizer (আর scheduler) state, epoch ও early stopping-এর হিসাব, সব RNG state.

১০ · AI-এর যুগে — AI-এর লেখা training loop যাচাই

“একটি PyTorch training loop লিখে দাও” বললে AI assistant কয়েক সেকেন্ডে চলনসই কোড দেয়। কিন্তু এই পর্বের প্রায় সব ভুল চুপচাপ: কোড চলে, loss নামে, শুধু সংখ্যা বা model একটু খারাপ। তাই AI-এর loop হাতে পেলে নিচের জায়গাগুলো আলাদা করে দেখুন।

AI প্রায়ই যা করেফলকীভাবে ধরবেন
Model-এর শেষে nn.Softmax বসিয়ে CrossEntropyLossLoss-এর নিচের সীমা, ছোট gradient, অতি-আত্মবিশ্বাসী probabilityModel-এর শেষ layer Linear কি না দেখুন; softmax শুধু inference-এ
Validation-এ model.eval() বা torch.no_grad() নেই, বা শেষে model.train() ফেরায় নাএলোমেলো val সংখ্যা, memory নষ্ট, বা পরের epoch-এ dropout বন্ধএকই model-এ val loss দুবার মাপুন; সংখ্যা এক কি না
Batch-এর loss-এর সরল গড় দেখায়অসমান শেষ batch-এ ভুল metricBatch size বদলে দেখুন metric বদলায় কি না
Checkpoint-এ শুধু model.state_dict()Resume-এর পরে run আলাদা পথে যায়থামিয়ে resume করে অবিরাম run-এর সঙ্গে torch.equal
পুরোনো API: Variable, loss.data[0], scheduler optimizer-এর আগেError, warning বা ভুল scheduleনিজের PyTorch version-এর docs; warning কখনো উপেক্ষা নয়
একটি seed-এর একটি run দেখে “model A ভালো” বলেভাগ্যের পার্থক্যকে উন্নতি ভাবাকয়েকটি seed আর একটি সরল baseline-এর সঙ্গে তুলনা

AI-কে কোডের সঙ্গে পরীক্ষাও চাইতে বলুন:

আমি PyTorch-এ ৩ শ্রেণির একটি tabular classifier train করছি (MLP, dropout আছে, AdamW). ১. একটি training loop লেখো: forward → loss → zero_grad → backward → step; CrossEntropyLoss-এ logits দাও, softmax নয়। ২. আলাদা evaluate() function দাও, যেখানে model.eval() আর torch.no_grad() আছে, loss আর accuracy batch-এর আকার দিয়ে ওজন করা, আর শেষে আগের mode ফেরানো। ৩. Early stopping (patience) আর best checkpoint যোগ করো; checkpoint-এ model, optimizer, epoch, best_val আর সব RNG state রাখো। ৪. Resume-এর পর অবিরাম run-এর সঙ্গে হুবহু মিল যাচাইয়ের একটি ছোট পরীক্ষা লেখো। ৫. আমার torch version-এ torch.load-এর weights_only default কী, docs-এর কোন অংশে আছে বলো; নিশ্চিত না হলে তা স্পষ্ট বলো। ৬. কোন লাইনে ভুলের ঝুঁকি সবচেয়ে বেশি, সেটিও বলো।

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

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

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

প্র ০১Early stopping এত সরল একটি নিয়ম — তবু কখন থামবেন, তা নিয়ে কেন এত আলোচনা, আর এর ঝুঁকি কী?

Early stopping-এর ধারণা neural network-এর প্রথম দিকের চর্চা থেকেই আছে: validation-এর ভুল বাড়তে শুরু করলে থামুন। সমস্যা হলো বাস্তবের val curve মসৃণ নয়। অংশ ৭-এর run-এই দেখেছেন, val loss epoch ৪-এ সবচেয়ে কম, epoch ৬-এ আবার একটু নামে, তারপর ওঠানামা। Lutz Prechelt ১৯৯৮ সালে Neural Networks: Tricks of the Trade-এ বিভিন্ন থামার নিয়ম (যেমন সেরা মান থেকে কত শতাংশ খারাপ হলে, বা কয়েক ধাপ টানা খারাপ হলে) পরীক্ষা করে দেখান যে ধীর নিয়ম সাধারণত সামান্য ভালো model দেয়, কিন্তু অনেক বেশি সময় নেয়। আজকের patience এই ধারার সরল রূপ।

তত্ত্বেও এর গুরুত্ব আছে। Gradient descent ছোট weight থেকে শুরু করলে আগে থামা মানে weight-কে শুরুর কাছাকাছি রাখা; linear model-এ দেখানো যায় এটি L2 regularization-এর মতো কাজ করে (মেশিন লার্নিং কোর্সের পর্ব ১৭)। তাই early stopping একটি “বিনা খরচের” regularizer.

ঝুঁকি তিনটি। প্রথম, validation set-এ overfitting: প্রতিটি epoch-এ val দেখে সিদ্ধান্ত নেওয়া মানে val-এর ওপর একটি ছোট search চালানো; সেরা মানটি কিছুটা ভাগ্যের। তাই আলাদা test set জরুরি। দ্বিতীয়, ছোট val set-এ noise: ২০০ জনের val-এ accuracy-র এক ঘর বদল মানে মাত্র ২ জন। তৃতীয়, আগে থামার ফাঁদ: কিছু বড় model-এ val loss একবার বেড়ে পরে আবার নামতে দেখা গেছে, যা epoch-ভিত্তিক double descent নামে আলোচিত (এই কোর্সের পর্ব ০৬-এ double descent-এর ধারণা)। অধৈর্য patience সেখানে সেরা জায়গার আগেই থামাতে পারে। এ কারণে বড় model-এ অনেকে নির্দিষ্ট সংখ্যক ধাপ আর একটি schedule দিয়ে train করেন, প্রতি কিছু ধাপে checkpoint রাখেন, আর পরে সেরাটি বাছেন।

মূল উপলব্ধি: Early stopping সস্তা আর কার্যকর regularizer, কিন্তু এটি validation-এর ওপর একটি সিদ্ধান্ত; তাই patience ভেবে বাছুন, checkpoint রাখুন, আর চূড়ান্ত মান test set-এ মাপুন।

প্র ০২PyTorch-এ train আর eval mode একটি global flag — এই নকশা কেন, আর কোথায় এটি অপ্রত্যাশিত ঝামেলা করে?

দুটি জনপ্রিয় layer training আর ব্যবহারের সময় আলাদা আচরণ করে বলেই mode-এর প্রয়োজন। Dropout (Hinton ও সহকর্মীরা ২০১২, Srivastava ও সহকর্মীরা ২০১৪) training-এ এলোমেলোভাবে unit বন্ধ করে, কিন্তু ব্যবহারের সময় সবগুলো রাখে। Batch Normalization (Ioffe আর Szegedy, ২০১৫) training-এ প্রতিটি batch-এর গড় আর variance দিয়ে normalize করে আর একটি চলমান গড় জমায়; ব্যবহারের সময় সেই জমানো মান ব্যবহার করে, যাতে একটি উদাহরণের ফল batch-এর বাকিদের ওপর নির্ভর না করে।

PyTorch এই পার্থক্যকে প্রতিটি nn.Module-এর একটি training flag দিয়ে প্রকাশ করে; model.eval() সব submodule-এ flag বদলায়। সুবিধা: forward কোড একটাই, mode বদলালে আচরণ বদলায়। অসুবিধা: এটি একটি লুকানো অবস্থা। Validation function শেষে model.train() না ফেরালে পরের epoch-এ dropout বন্ধ থাকে, আর কোনো error আসে না। কিছু framework অন্য পথ নেয়: প্রতিটি call-এ স্পষ্ট training=True/False argument (যেমন Keras-এর layer call-এ, বা JAX-ভিত্তিক library-তে ডাকার সময় flag দেওয়া)।

ঝামেলার জায়গাগুলো সূক্ষ্ম। Fine-tuning-এ ছোট batch হলে BatchNorm-এর চলমান গড় নষ্ট হতে পারে, তাই অনেকে BatchNorm layer-কে আলাদাভাবে eval-এ রাখেন, যদিও বাকি model train-এ। BatchNorm-এর চলমান গড় eval()-এ আপডেট হয় না, কিন্তু train mode-এ no_grad-এর ভেতরেও আপডেট হয়; অর্থাৎ ভুল mode-এ validation চালালে model-এর অবস্থা চুপচাপ বদলায়। আর inference server-এ eval() ভুলে গেলে একই অনুরোধে দুবার আলাদা উত্তর আসে, যা অংশ ৪-এর stepper-এ দেখেছেন।

মূল উপলব্ধি: Mode একটি লুকানো অবস্থা; evaluate function-এ eval() আর no_grad() দিন, শেষে আগের mode ফেরান, আর নতুন layer ব্যবহারের আগে জেনে নিন সে mode-এ আচরণ বদলায় কি না।

প্র ০৩হুবহু resume আর পুনরুৎপাদনযোগ্যতা — একই seed দিলেও কেন অনেক সময় ফল মেলে না?

অংশ ৮-এ CPU-তে বিট ধরে মিল পেয়েছি, কারণ ওই run-এ এলোমেলোতার সব উৎস জানা আর সংরক্ষিত: torch-এর বৈশ্বিক RNG (dropout) আর DataLoader-এর generator (shuffle). বাস্তব project-এ উৎস আরও বেশি: NumPy আর Python-এর random (augmentation-এ), প্রতিটি DataLoader worker-এর আলাদা seed, GPU-র নিজের RNG, আর শুধু এলোমেলোতা নয়, হিসাবের ক্রম।

শেষ বিষয়টি সবচেয়ে অবাক করে। Floating-point যোগ associative নয়: (a + b) + c আর a + (b + c) শেষ বিটে আলাদা হতে পারে। GPU-তে অনেক kernel হাজার thread-এর ফল atomic যোগে মেলায়, আর thread কোন ক্রমে শেষ হবে তা প্রতিবার আলাদা। ফলে একই seed, একই কোডেও শেষ বিটে পার্থক্য আসে, আর হাজার ধাপে তা বড় হয়। PyTorch-এর “Reproducibility” নোট স্পষ্ট বলে, ভিন্ন release, ভিন্ন platform বা CPU আর GPU-র মধ্যে হুবহু মিলের নিশ্চয়তা নেই; torch.use_deterministic_algorithms(True) দিলে অনির্ধারিত কাজে error দেয় বা ধীর বিকল্প নেয়।

এর একটি বড় বৈজ্ঞানিক দিক আছে। Henderson ও সহকর্মীদের ২০১৮ সালের প্রবন্ধ (“Deep Reinforcement Learning that Matters”) দেখায়, শুধু seed বদলালেই কিছু algorithm-এর ফল এতটা বদলায় যে প্রকাশিত তুলনার সিদ্ধান্ত উল্টে যেতে পারে। তাই হুবহু মিল আর পরিসংখ্যানগত পুনরুৎপাদন আলাদা লক্ষ্য: প্রথমটি debug আর resume-এর জন্য (একটি বাগ বারবার একইভাবে দেখা), দ্বিতীয়টি দাবির জন্য (কয়েকটি seed-এর গড় আর ছড়ানো)। মেশিন লার্নিং কোর্সের পর্ব ৪-এ seed আর config লেখার অভ্যাস, আর পর্ব ১৯-এ deep learning-এর experiment tracking.

বাস্তবে সমঝোতা হলো: debug আর resume-এর জন্য সব state রাখুন, সম্ভব হলে deterministic mode; আর ফল জানাতে কয়েকটি seed চালিয়ে গড় ও ছড়ানো দিন, library version লিখে রাখুন।

মূল উপলব্ধি: Seed একা যথেষ্ট নয়; এলোমেলোতার প্রতিটি উৎসের state আর হিসাবের ক্রম মিললে তবেই হুবহু মিল, আর বৈজ্ঞানিক দাবির জন্য হুবহু মিলের চেয়ে কয়েক seed-এর পরিসংখ্যান বেশি জরুরি।

প্র ০৪Framework কেন loss-কে logits দিতে বলে, probability নয় — সংখ্যাগত স্থিরতার এই নকশার পেছনে কী আছে?

Softmax আর log পাশাপাশি এলে একটি সুন্দর সরলীকরণ ঘটে: log softmax(z)k = zk − log Σj exp(zj). ডান দিকটি log-sum-exp, যা সবচেয়ে বড় z বিয়োগ করে স্থিরভাবে হিসাব করা যায় (পর্ব ০৩)। কিন্তু আগে softmax করে probability বানালে, তারপর log নিলে, দুটি সমস্যা হয়। একটি underflow: একটি শ্রেণির probability float32-এর ক্ষুদ্রতম মানের নিচে গেলে ঠিক ০ হয়ে যায়, আর log ০ = −∞. অন্যটি অংশ ২-এর BCELoss-এর উদাহরণ: sigmoid(২০০) হুবহু ১ হয়ে যায়, তাই PyTorch log-কে −১০০-এ কাটে, আর আসল loss ২০০ হারিয়ে যায়।

Gradient-এর দিকেও লাভ। Logits থেকে সরাসরি cross-entropy-র gradient p − y, সবসময় −১ থেকে ১-এর মধ্যে, কখনো বিস্ফোরিত হয় না। Probability থেকে শুরু করলে log-এর derivative 1/p, যা p ছোট হলে বিশাল, আর পরে softmax-এর Jacobian তাকে আবার ছোট করে; দুই চরমের গুণ সংখ্যাগতভাবে বিপজ্জনক। এই কারণেই PyTorch-এ CrossEntropyLoss = log_softmax + NLLLoss, আর BCEWithLogitsLoss আলাদাভাবে আছে; TensorFlow-এ একই কারণে from_logits=True option.

Mixed precision (পর্ব ১২)-এ বিষয়টি আরও জরুরি: float16-এর পরিসর ছোট, তাই PyTorch-এর autocast loss-এর মতো কিছু কাজ float32-এ চালায়। আর ভাষা model-এর বিশাল vocabulary-তে (লাখের ঘরে শ্রেণি) logits থেকে সরাসরি loss হিসাব না করলে memory আর নির্ভুলতা দুটোই যায়।

শিক্ষাটি সাধারণ: গণিতে সমান দুটি সূত্র কম্পিউটারে সমান নয়। Library যখন একটি “fused” function দেয়, সেটি প্রায়ই এই কারণেই।

মূল উপলব্ধি: Model শেষ করুন logits দিয়ে, loss-কে logits দিন, আর probability দরকার হলে শুধু inference-এ softmax করুন।

প্র ০৫নিজের training loop লিখবেন, নাকি Keras-এর fit, Lightning বা Hugging Face Trainer-এর মতো তৈরি কাঠামো ব্যবহার করবেন?

Deep learning-এর ইতিহাসে দুটি ধারা পাশাপাশি চলেছে। Keras (২০১৫ সালের দিকে প্রকাশিত) model.fit() দিয়ে পুরো loop লুকিয়ে দেয়: এক লাইনে training, validation, callback দিয়ে early stopping আর checkpoint. PyTorch ইচ্ছা করে উল্টো পথ নেয়: library loop দেয় না, আপনি নিজে লেখেন, যেমন এই পর্বে। পরে PyTorch-এর ওপর আবার কাঠামো এল: fastai, PyTorch Lightning (২০১৯-এর দিকে), Hugging Face-এর Trainer (২০২০-এর দিকে) আর accelerate, যা loop-এর পুনরাবৃত্ত অংশ (device, mixed precision, একাধিক GPU, logging, checkpoint) সামলায়। সময়রেখা উৎসভেদে একটু আলাদা লেখা হয়।

তৈরি কাঠামোর সুবিধা স্পষ্ট: পরীক্ষিত কোড, কম boilerplate, আর এই পর্বের ভুলগুলো (eval mode, metric গড়, scheduler ক্রম, checkpoint) সাধারণত ঠিকভাবে করা থাকে। একাধিক GPU বা mixed precision-এ এটি বিশাল সময় বাঁচায়। অসুবিধা: লুকানো আচরণ। Default-গুলো version-এর সঙ্গে বদলায়, কোন hook কখন চলে তা না জানলে অস্বাভাবিক কিছু (দুটি model একসঙ্গে, যেমন GAN; বা নিজের মতো gradient কৌশল) করতে গেলে কাঠামোর সঙ্গে লড়তে হয়। আর ভুল হলে ভেতরে কী ঘটছে না বুঝলে debug কঠিন।

সম্প্রদায়ে এ নিয়ে মতভেদ আছে। অনেকে বলেন শেখার সময় নিজে লিখুন আর production-এ কাঠামো নিন; অনেক গবেষক নিজের ছোট loop রাখেন কারণ তা পড়া সহজ। একটি যুক্তিসংগত নিয়ম: এই পর্বের পাঁচটি কাজ, validation-এর দুটি সুইচ আর checkpoint-এর বিষয়বস্তু আপনি এমনভাবে জানুন যাতে যেকোনো কাঠামোর log দেখে বুঝতে পারেন সে কী করছে। পরের মডিউলগুলোতে Hugging Face-এর Trainer ব্যবহারের সময় এই জ্ঞানই কাজে লাগবে।

মূল উপলব্ধি: কাঠামো loop লেখার শ্রম বাঁচায়, বোঝার দায়িত্ব নয়; নিজে একবার পুরো loop লিখে যাচাই করলে তবেই কাঠামোর default বিশ্বাস করা বা বদলানো যায়।

অনুশীলন

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

  1. পূর্ণ numerical: একটি validation set-এর চারটি batch-এর আকার ৬৪, ৬৪, ৬৪, ৮ আর গড় loss ০.৫৬৩৪, ০.৮০৯৬, ০.৪১০৫, ০.৩২৪৮. (ক) সরল গড় আর weighted গড় বের করুন। (খ) Model-এ ১৭,৬৬৭টি float32 parameter আর optimizer AdamW. Checkpoint-এ model আর optimizer-এর tensor মিলে প্রায় কত KiB? (গ) RNG-এর দুটি ৫০৫৬ byte-এর state যোগ করলে কত?

    (ক) সরল গড় = (০.৫৬৩৪ + ০.৮০৯৬ + ০.৪১০৫ + ০.৩২৪৮)/৪ = ২.১০৮৩/৪ ≈ ০.৫২৭১. Weighted = (৬৪ × ১.৭৮৩৫ + ৮ × ০.৩২৪৮)/২০০ = (১১৪.১৪৪ + ২.৫৯৮)/২০০ ≈ ০.৫৮৩৭, যা পুরো সেটে একবারে হিসাবের সমান। (খ) Model: ১৭,৬৬৭ × ৪ byte = ৭০,৬৬৮ byte ≈ ৬৯ KiB. AdamW প্রতিটি parameter-এর জন্য exp_avg আর exp_avg_sq রাখে, আরও দ্বিগুণ: মোট ৩ × ৭০,৬৬৮ = ২,১২,০০৪ byte ≈ ২০৭ KiB. (গ) আরও ১০,১১২ byte ≈ ৯.৯ KiB, মোট প্রায় ২১৭ KiB. সত্যিকারের ফাইল ২২৩ KB (১০২৪ দিয়ে ভাগ); বাকিটা নাম, step-এর সংখ্যা আর ফাইলের কাঠামো।

  2. সমালোচনা ও নকশা: একটি AI assistant এই loop দিয়েছে: model-এর শেষে nn.Softmax(dim=1); প্রতি batch-এ loss.backward(); opt.step(); scheduler.step(), কোথাও zero_grad নেই; validation একই model-এ কোনো mode বদল ছাড়া, আর val_loss = sum(batch_losses) / len(batch_losses); প্রতি epoch-এ torch.save(model.state_dict(), "ckpt.pt"). কোন কোন সমস্যা, আর আপনি কীভাবে ঠিক করবেন?

    ১, Softmax: CrossEntropyLoss-এর আগে softmax দুবার হয়; শেষ layer Linear রাখুন, softmax শুধু inference-এ। ২, zero_grad নেই: gradient জমে; backward-এর আগে opt.zero_grad(). ৩, Scheduler: step-এর পরে ক্রম ঠিক আছে, কিন্তু scheduler প্রতি batch না প্রতি epoch-এ ডাকতে হবে তা নির্দিষ্ট scheduler-এর docs দেখে ঠিক করুন। ৪, Validation: eval() আর no_grad() নেই; dropout চালু আর graph তৈরি। একটি evaluate() function-এ দুটি দিন, শেষে model.train(). ৫, গড়: সরল গড়; total += loss.item() * len(yb), শেষে মোট উদাহরণ দিয়ে ভাগ। ৬, Checkpoint: শুধু weight, প্রতিবার একই নামে ওভাররাইট; optimizer, scheduler, epoch, best_val, patience-এর গোনা আর RNG state যোগ করুন, best.pt আর last.pt আলাদা রাখুন, অস্থায়ী নামে লিখে os.replace. যাচাই: একটি ছোট batch overfit, val loss দুবার মেপে একই মান, আর থামিয়ে resume করে torch.equal.

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে। মনে রাখুন loss = (3w)²/2-এর derivative 9w, আর SGD-তে w ← w − 0.1 × grad.
    import torch
    w = torch.tensor(1.0, requires_grad=True)
    opt = torch.optim.SGD([w], lr=0.1)
    for step in range(3):
        loss = (3 * w) ** 2 / 2          # dL/dw = 9w
        loss.backward()                  # zero_grad নেই
        print(step, round(w.grad.item(), 3), end=" | ")
        opt.step()
        print(round(w.item(), 3))
    0 9.0 | 0.1
    1 9.9 | -0.89
    2 1.89 | -1.079

    (সত্যিকারের run, PyTorch 2.15.0.) ধাপ ০: grad = ৯ × ১ = ৯, w = ১ − ০.৯ = ০.১. ধাপ ১: নতুন gradient ৯ × ০.১ = ০.৯, কিন্তু আগের ৯-এর সঙ্গে যোগ হয়ে ৯.৯; w = ০.১ − ০.৯৯ = −০.৮৯. ধাপ ২: নতুন ৯ × (−০.৮৯) = −৮.০১, জমা ৯.৯ − ৮.০১ = ১.৮৯; w = −০.৮৯ − ০.১৮৯ = −১.০৭৯. সর্বনিম্ন বিন্দু w = ০, অথচ w তার থেকে দূরে সরে যাচ্ছে। প্রতি ধাপে opt.zero_grad() থাকলে grad হতো ৯, ০.৯, ০.০৯ আর w হতো ০.১, ০.০১, ০.০০১.

  4. কোড লিখুন: একটি evaluate(model, loader, loss_fn) function লিখুন, যা (১) eval mode আর no_grad-এ চলে, (২) loss আর accuracy batch-এর আকার দিয়ে ওজন করে, (৩) শেষে model-কে আগের mode-এ ফিরিয়ে দেয়।
    @torch.no_grad()
    def evaluate(model, loader, loss_fn):
        was_training = model.training
        model.eval()
        total, correct, n = 0.0, 0, 0
        for xb, yb in loader:
            logits = model(xb)
            total += loss_fn(logits, yb).item() * len(yb)
            correct += (logits.argmax(1) == yb).sum().item()
            n += len(yb)
        model.train(was_training)
        return total / n, correct / n

    @torch.no_grad() decorator পুরো function-কে no_grad-এ চালায়। was_training মনে রাখায় train mode-এ ডাকলে train-এ, eval-এ ডাকলে eval-এ ফেরে। বাজারঘরের best.pt-এ, model train mode-এ রেখে ডেকে সত্যিকারের run-এ এলো val loss 0.5837 | acc 0.800 | model.training after: True, যা অংশ ৫-এর weighted মানের সঙ্গে এক।

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

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

ক্রমPyTorch-এ একটি epoch-এর কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. model.train() দিয়ে training mode চালু করা
  2. প্রতিটি train batch-এ forward করে logits আর loss বের করা
  3. opt.zero_grad(), তারপর loss.backward()
  4. opt.step() দিয়ে parameter বদলানো (সব batch-এ পুনরাবৃত্তি)
  5. model.eval() আর torch.no_grad()-এ validation, batch-এর আকার দিয়ে ওজন করা metric
  6. Val loss আগের সেরার চেয়ে কম হলে best checkpoint সেভ, না হলে patience-এর গোনা বাড়ানো

PyTorch-এ ৩ শ্রেণির একটি classifier-এর ৩২টি উদাহরণের batch-এ nn.CrossEntropyLoss()-কে কী দিতে হয়?

  • Softmax-এর পরের probability (32, 3) আর one-hot label (32, 3)
  • Argmax করা অনুমিত শ্রেণি (32,) আর আসল শ্রেণি (32,)
  • কাঁচা logits (32, 3) float আর শ্রেণি-সূচক (32,) int64
  • Sigmoid-এর পরের মান (32, 3) আর float label (32,)

PyTorch-এ (default set_to_none=True) একটি training ধাপে কেউ লিখলেন loss.backward(); opt.zero_grad(); opt.step(). AdamW optimizer হলে parameter-এর কী হয়?

  • স্বাভাবিকভাবে আপডেট হয়, ক্রমে কিছু যায় আসে না
  • কিছুই বদলায় না, কারণ step-এর সময় gradient None; কোনো error-ও আসে না
  • RuntimeError আসে
  • Gradient দ্বিগুণ হয়ে জমে

একটি validation set-এ তিনটি batch-এর আকার ৬৪, ৬৪ আর ৮, accuracy যথাক্রমে ০.৭৫, ০.৭৫ আর ১.০. পুরো সেটের আসল accuracy কত?

  • ০.৮৩৩৩
  • ০.৭৫০০
  • ১.০০০০
  • ০.৭৬৪৭

Dropout আছে এমন একটি PyTorch model-এ validation-এর সময় শুধু torch.no_grad() দেওয়া হলো, model.eval() নয়। কী ঘটে?

  • Graph তৈরি হয় না, কিন্তু dropout চালু থাকে, তাই একই ডেটায় val loss প্রতিবার আলাদা
  • no_grad নিজেই dropout বন্ধ করে, তাই কোনো সমস্যা নেই
  • RuntimeError আসে, কারণ dropout-এর gradient লাগে
  • Dropout বন্ধ হয়, কিন্তু graph তৈরি হয়ে memory খরচ হয়

একটি checkpoint-এ model আর AdamW optimizer-এর state_dict আর epoch আছে, কিন্তু কোনো RNG state নেই। Model-এ dropout আর DataLoader-এ seed দেওয়া shuffle আছে। Resume করলে কী আশা করবেন?

  • অবিরাম run-এর সঙ্গে বিট ধরে হুবহু মিল
  • Training আবার শূন্য থেকে শুরু হয়
  • Training চলে, কিন্তু dropout mask আর batch-এর ক্রম আলাদা হওয়ায় সংখ্যা অবিরাম run থেকে আলাদা হয়
  • torch.load error দেয়

৩ শ্রেণির একটি model-এর logits-এর ওপর softmax করে সেই probability CrossEntropyLoss-এ দেওয়া হলো। Training loss-এর সর্বনিম্ন সম্ভাব্য মান প্রায় কত?

  • ০
  • প্রায় ০.৫৫১
  • প্রায় ১.০৯৯
  • কোনো নিচের সীমা নেই

শব্দকোষ

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

মনে রাখুন

  1. পাঁচটি কাজ: forward → loss → zero_grad → backward → step. Backward শুধু .grad লেখে, step parameter বদলায়; scheduler.step() আসে optimizer.step()-এর পরে।
  2. Logits-এ loss: CrossEntropyLoss নেয় (N, C) logits আর (N,) int64 সূচক; দুই শ্রেণিতে BCEWithLogitsLoss. আগে softmax দিলে loss-এর নিচের সীমা আর দুর্বল gradient.
  3. Validation: eval() আচরণ বদলায় (dropout, BatchNorm), no_grad() graph বন্ধ করে; দুটিই লাগে, আর শেষে train() ফেরান।
  4. সঠিক গড়: Σ nb·mb / Σ nb; অসমান শেষ batch-এ সরল গড় ভুল সংখ্যা দেয়।
  5. Checkpoint: model, optimizer (ও scheduler), epoch, best_val, patience-এর গোনা আর সব RNG state; তবেই resume অবিরাম run-এর সঙ্গে হুবহু মেলে। Early stopping val দেখে, তাই চূড়ান্ত মান test set-এ।

পরের পর্বে: এই পর্বে DataLoader-কে প্রায় কালো বাক্স ধরে নিয়েছি। পর্ব ১১ (Dataset আর DataLoader — batching, shuffling আর বাংলা ডেটা) তার ভেতরে ঢুকবে: নিজের Dataset লেখা, seed দেওয়া shuffle, বাংলা রিভিউয়ের মতো অসমান দৈর্ঘ্যের ডেটায় padding, আর worker দিয়ে গতি।

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

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