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

nn.Module দিয়ে model বানানো — layer, parameter আর state_dict

Building Models with nn.Module — Registration, nn.Linear, Parameter Counts, Buffers, state_dict and train/eval

Taught in Bangla, this lesson explains nn.Module as a registry: how __setattr__ registers layers, parameters and buffers, why nn.Linear stores weight as (out, in), and how to count parameters layer by layer for a delivery-time MLP. Real desktop PyTorch runs show the plain-list trap and nn.ModuleList, buffers, reading a state_dict and reproducing the forward pass in in-page NumPy, strict loading and weights_only in torch.load, and what train() and eval() really switch.

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

  • nn.Module-এর subclass লিখে একটি model বানানো, আর __setattr__ কীভাবে layer, parameter আর buffer নিবন্ধন করে
  • nn.Linear-এর ভেতর: weight-এর আকার (out, in), y = xWᵀ + b, default initialization
  • প্রতিটি স্তরের parameter গোনা, আর Sequential, ModuleList, ModuleDict কখন কোনটি
  • register_buffer, state_dict-এর key পড়া, strict loading, আর torch.load-এর weights_only নিরাপত্তা
  • model.train() বনাম model.eval(), আর NumPy-তে একই forward চালিয়ে PyTorch-এর সঙ্গে মেলানো

এই কোর্সের পর্ব ০৩-এ NumPy-তে MLP-এর forward pass নিজে লিখেছেন, আর পর্ব ০৫-এ নিজের autograd. পর্ব ০৭ (PyTorch tensor — shape, dtype, device আর broadcasting) আর পর্ব ০৮ (PyTorch autograd — requires_grad, backward আর gradient-এর ফাঁদ) tensor আর gradient-এর দিকটি দেখায়। কিন্তু হাতে লেখা model-এ weight-গুলো আলাদা আলাদা চলক: ছয়টি স্তর হলে বারোটি চলক, সেগুলো optimizer-কে দেওয়া, ফাইলে রাখা, GPU-তে পাঠানো, সবই হাতে। এই পর্বের প্রশ্ন: PyTorch কীভাবে একটি model-এর সব weight নিজে খুঁজে পায়, গোনে, সেভ করে আর ফিরিয়ে আনে? উত্তর nn.Module. PyTorch পাতায় চলে না, তাই মূল কাজ ডেস্কটপের সত্যিকারের run; পাতার তিনটি ব্লক (ব্রাউজারের Python আর NumPy) সেই model-এর হিসাব আলাদাভাবে যাচাই করে।

১ · nn.Module কী — layer আর weight-এর হিসাব খাতা

বাজারঘরের গুদামে প্রতিটি তাকের একটি রেজিস্টার আছে। নতুন বাক্স তাকে রাখলে রেজিস্টারে নাম ওঠে: “তাক ২, বাক্স ০”. মাস শেষে মালিক জিজ্ঞেস করেন “মোট কত পণ্য?” বা বলেন “সব মাল নতুন গুদামে সরাতে হবে”; কর্মীরা রেজিস্টার ধরে কাজ করেন, একটি একটি করে বাক্স খোঁজেন না। কিন্তু কেউ যদি একটি বাক্স রেজিস্টারে না তুলে কোণে রেখে দেন, হিসাবেও আসে না, নতুন গুদামেও যায় না। nn.Module এই রেজিস্টার, আর “রেজিস্টারে না তোলা বাক্স” এই পর্বের সবচেয়ে পরিচিত বাগ।

nn.Module PyTorch-এর সব model আর layer-এর মূল class. nn.Linear, nn.ReLU, nn.Dropout সবাই এর subclass, আর আপনার নিজের model-ও তাই হবে। নিয়ম দুটি: __init__-এ অংশগুলো তৈরি করে self-এর attribute হিসেবে রাখুন, আর forward-এ input থেকে output-এর হিসাব লিখুন। Class, subclass আর super() Python কোর্সের পর্ব ৩১ আর পর্ব ৩৩-এ আছে; এখানে শুধু ব্যবহার। model(x) লিখলে আসলে __call__ চলে, যা কিছু hook সামলে forward(x) ডাকে; তাই model.forward(x) সরাসরি না ডেকে model(x) লেখাই নিয়ম।

সারা পর্বে একটি model: বাজারঘরের ডেলিভারি সময়ের অনুমান। Input তিনটি: দূরত্ব (km), পণ্যের সংখ্যা আর বৃষ্টি (০ বা ১); output মিনিটে সময়। ডেটা seed দিয়ে বানানো ৪০০টি কাল্পনিক অর্ডার, সময় = ২০ + ২.৫ × দূরত্ব + ৬√(পণ্য) + ৮ × বৃষ্টি + দূরত্বের একটি ছোট বর্গ পদ + noise. Model একটি ৩ → ১৬ → ৮ → ১ MLP. ভেতরে input-কে গড় বিয়োগ করে standard deviation দিয়ে ভাগ করা হয় (standardization), আর সেই গড় আর standard deviation model-এর সঙ্গেই থাকে।

import torch
from torch import nn
# make_data(): seed দিয়ে ৪০০টি কাল্পনিক অর্ডার (X, y) বানায়; এখানে বাদ

class DeliveryMLP(nn.Module):
    def __init__(self, x_mean, x_std, p_drop=0.1):
        super().__init__()                            # প্রথম লাইন, বাদ দেওয়া যাবে না
        self.register_buffer("x_mean", x_mean)        # parameter নয়, কিন্তু state_dict-এ যায়
        self.register_buffer("x_std", x_std)
        self.hidden = nn.Sequential(
            nn.Linear(3, 16), nn.ReLU(), nn.Dropout(p_drop),
            nn.Linear(16, 8), nn.ReLU())
        self.head = nn.Linear(8, 1)

    def forward(self, x):
        z = (x - self.x_mean) / self.x_std
        return self.head(self.hidden(z)).squeeze(-1)

নিচের ছবিটি এই model-এর গাছ। প্রতিটি অংশের নাম তার পথ: hidden-এর ভেতরের শূন্য নম্বর Linear-এর weight-এর পুরো নাম hidden.0.weight. এই নামগুলোই পরে state_dict-এর key হবে।

DeliveryMLP-এর module গাছ আর state_dict-এর key

Submodules (blue), buffers (amber), layers without parameters (grey)

Module tree of DeliveryMLP with the state_dict key of every registered tensor DeliveryMLP buffer: x_mean, x_std hidden: Sequential 0 Linear(3, 16) 1 ReLU 2 Dropout(0.1) 3 Linear(16, 8) 4 ReLU head: Linear(8, 1) x_mean (3,) x_std (3,) hidden.0.weight (16,3) + .bias parameter নেই parameter নেই hidden.3.weight (8,16) + .bias head.weight (1,8) + .bias লাল লেখা: state_dict-এর key আর আকার
ছবি ৯.১ — নাম তৈরি হয় attribute-এর নাম আর Sequential-এর ভেতরের ক্রমিক নম্বর জুড়ে। ReLU আর Dropout-এর কোনো parameter নেই, তাই key-তে hidden.1 বা hidden.2 আসে না, কিন্তু নম্বরগুলো জায়গা ধরে রাখে।

ডেস্কটপে uv দিয়ে একটি আলাদা project বানিয়ে CPU-র PyTorch install করা হলো (uv-এর পরিচয় AI-এর ভিত্তি কোর্সের পর্ব ৭-এ; pyproject.toml-এ PyTorch-এর CPU wheel index যোগ করা)। তারপর model বানিয়ে ছাপানো, প্রতিটি parameter-এর নাম, আকার আর সংখ্যা, আর buffer দেখা হলো।

~/bazarghar-dl$ uv add "torch==2.15.0" numpy
 …
 + numpy==2.5.4
 …
 + torch==2.15.0+cpu
 …
import torch
from bazarghar import make_data, DeliveryMLP
print("torch", torch.__version__)

X, y = make_data()                          # ৪০০টি কাল্পনিক অর্ডার, seed 42
torch.manual_seed(42)
model = DeliveryMLP(X.mean(0), X.std(0))
print(model)

total = 0
for name, p in model.named_parameters():
    total += p.numel()
    print(f"{name:16s} {str(tuple(p.shape)):9s} {p.numel():4d}")
print("মোট parameter:", total)
for name, b in model.named_buffers():
    print("buffer:", name, b.numpy().round(3))

lin = model.hidden[0]                       # nn.Linear(3, 16)
z = (X[:2] - model.x_mean) / model.x_std
manual = z @ lin.weight.T + lin.bias        # y = x Wᵀ + b
print("weight", tuple(lin.weight.shape), "| bias", tuple(lin.bias.shape))
print("max |lin(z) − (z Wᵀ + b)| =", (lin(z) - manual).abs().max().item())
print("|W| সর্বোচ্চ:", round(lin.weight.abs().max().item(), 4),
      "| 1/√in =", round(3 ** -0.5, 4))

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

~/bazarghar-dl$ uv run python module_tour.py
torch 2.15.0+cpu
DeliveryMLP(
  (hidden): Sequential(
    (0): Linear(in_features=3, out_features=16, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.1, inplace=False)
    (3): Linear(in_features=16, out_features=8, bias=True)
    (4): ReLU()
  )
  (head): Linear(in_features=8, out_features=1, bias=True)
)
hidden.0.weight  (16, 3)     48
hidden.0.bias    (16,)       16
hidden.3.weight  (8, 16)    128
hidden.3.bias    (8,)         8
head.weight      (1, 8)       8
head.bias        (1,)         1
মোট parameter: 209
buffer: x_mean [6.185 3.102 0.29 ]
buffer: x_std [3.209 1.392 0.454]
weight (16, 3) | bias (16,)
max |lin(z) − (z Wᵀ + b)| = 0.0
|W| সর্বোচ্চ: 0.5702 | 1/√in = 0.5774

চারটি জিনিস লক্ষ করুন। এক: print(model) গাছটি দেখায়, কিন্তু buffer দেখায় না; buffer দেখতে named_buffers() লাগে। দুই: আমরা কোথাও weight-এর তালিকা লিখিনি, তবু named_parameters() ছয়টি tensor খুঁজে পেয়েছে, ঠিক ছবির পথ ধরে। তিন: প্রথম Linear-এর weight (১৬, ৩), অর্থাৎ (out, in), আর z @ W.T + b হাতে হিসাব করলে layer-এর output-এর সঙ্গে পার্থক্য হুবহু ০. চার: weight-এর সবচেয়ে বড় মান ০.৫৭০২, যা 1/√৩ ≈ ০.৫৭৭৪-এর নিচে; কারণ অংশ ৩-এ।

২ · নিবন্ধন কীভাবে হয় — __setattr__-এর ভেতরে

সাধারণ Python object-এ self.fc = … লিখলে মানটি শুধু object-এর __dict__-এ বসে। nn.Module নিজের __setattr__ লিখে রেখেছে (dunder method-এর ধারণা Python কোর্সের পর্ব ৩২-এ)। প্রতিটি assignment-এ সে মানের type দেখে: nn.Parameter হলে _parameters অভিধানে, অন্য একটি nn.Module হলে _modules-এ, আর register_buffer দিয়ে আসা tensor _buffers-এ। এর বাইরে সব কিছু (list, সাধারণ tensor, সংখ্যা) সাধারণ attribute; PyTorch সেগুলো দেখেও না। parameters(), state_dict(), .to(device), .train() সবাই শুধু এই তিন অভিধান ধরে পুরো গাছে হাঁটে।

class Tiny(nn.Module) · __init__ লাইন ধরে লাইন
লাইন
কোথায় গেল
ফলাফল
  1. খাতা খোলা. super().__init__() তিনটি খালি অভিধান বানায়। এটি না ডাকলে প্রথম Module assignment-এই PyTorch বলে “cannot assign module before Module.__init__() call”.
  2. Submodule. মানটি একটি nn.Module, তাই _modules-এ নাম “fc” দিয়ে বসল। fc-এর নিজের parameter (weight, bias) তার নিজের খাতায়; বাবা model গাছ ধরে সেগুলো খুঁজে নেবে।
  3. Parameter. nn.Parameter হলো tensor-এর একটি subclass, যার requires_grad default-এ True. এটি সরাসরি এই module-এর _parameters-এ।
  4. Buffer. Tensor-টি model-এর অবস্থার অংশ, কিন্তু শেখার জিনিস নয়, যেমন normalization-এর গড়। Buffer state_dict-এ যায় আর .to(), .double()-এর সঙ্গে বদলায়।
  5. দেখা যায় না এমন জিনিস. List-এর ভেতরের Linear আর সাধারণ tensor কোনো অভিধানে ওঠেনি। Forward-এ এদের ব্যবহার করলে হিসাব চলবে, কিন্তু model-এর হিসাবের খাতায় এরা নেই।
  6. ফল যাচাই. state_dict-এ চারটি key: আগে এই module-এর নিজের parameter আর buffer, তারপর সন্তানদের। double()-এর পর নিবন্ধিত mu float64 হলো, কিন্তু mu2 আর list-এর Linear float32-ই রয়ে গেল।

Stepper-এর প্রতিটি দাবি ডেস্কটপে চালিয়ে দেখা হয়েছে (script-এ ঠিক ওপরের Tiny class, আর super().__init__() ছাড়া একটি Forgot class):

~/bazarghar-dl$ uv run python registry.py
_modules   : ['fc']
_parameters: ['scale']
_buffers   : ['mu']
state_dict : ['scale', 'mu', 'fc.weight', 'fc.bias']
'extra' in __dict__: True | 'mu2': True
double() পরে: torch.float64 torch.float32 torch.float32
AttributeError: cannot assign module before Module.__init__() call

শেষ লাইনটির শিক্ষা: super().__init__() সবসময় __init__-এর প্রথম লাইন। আর দ্বিতীয় শেষ লাইনটি একটি চুপচাপ বাগের রূপ: GPU-তে model পাঠালে (model.to("cuda")) নিবন্ধিত না হওয়া tensor CPU-তেই থেকে যায়, আর forward-এ “Expected all tensors to be on the same device” ধরনের error আসে। Device-এর বিস্তারিত পর্ব ১২ (GPU, mixed precision আর গতি — device, AMP আর profiling)-এ।

৩ · nn.Linear-এর ভেতর — weight (out, in), y = xWᵀ + b

nn.Linear(in_features, out_features) হলো সবচেয়ে মৌলিক layer. এর দুটি parameter: weight, আকার (out, in), আর bias, আকার (out,). হিসাব:

y = x Wᵀ + b

এখানে x হলো input-এর batch, আকার (N, in), যেখানে N একসঙ্গে কতগুলো উদাহরণ; W হলো weight; Wᵀ তার transpose, আকার (in, out); b bias, যা broadcasting-এ প্রতিটি সারিতে যোগ হয়। ফল y-এর আকার (N, out). পর্ব ০৩-এ আমরা উল্টো রীতি নিয়েছিলাম: W-এর আকার (in, out) আর y = XW + b. দুটি গাণিতিকভাবে একই; PyTorch (out, in) রাখে, কারণ তখন W-এর প্রতিটি সারি একটি output neuron-এর সব weight. NumPy-তে PyTorch-এর weight ব্যবহার করতে গেলে এই transpose ভুলে যাওয়া খুব সাধারণ বাগ, আর in = out হলে আকারও মিলে যায়, তাই error আসে না।

in = 3, out = 2W: (2, 3), b: (2,)

            
  1. আকার. W-এর দুটি সারি দুটি output neuron; প্রতিটি সারিতে তিনটি input-এর weight. তাই আকার (২, ৩) = (out, in), আর bias-এ প্রতি neuron-এ একটি।
  2. Transpose. x-এর আকার (১, ৩), W-এর (২, ৩); সরাসরি গুণ চলে না। Wᵀ (৩, ২) নিলে ভেতরের মাপ ৩ = ৩ মেলে, আর ফল (১, ২)।
  3. প্রথম neuron. W-এর প্রথম সারি আর x-এর dot product: ০.৫ − ২.০ + ৬.০ = ৪.৫.
  4. দ্বিতীয় neuron. দ্বিতীয় সারি: ১.০ + ০ − ১.৫ = −০.৫. প্রতিটি output আসলে “একটি সারি · x”.
  5. Bias. দুটি bias যোগ করে y = [৪.৬, −০.৭]. এটিই nn.Linear(3, 2)-এর output, যদি তার weight আর bias এই মানগুলো হয়।
  6. Batch আর গোনা. N-টি input একসঙ্গে দিলে X Wᵀ-এর আকার (N, ২), আর bias প্রতিটি সারিতে broadcast হয়। Parameter ২ × ৩ + ২ = ৮; সাধারণভাবে out × in + out.

Default initialization. নতুন nn.Linear তৈরি হলে weight এলোমেলো মানে ভরা হয়। লেখার সময়ের PyTorch docs অনুযায়ী weight আর bias দুটিই সমবণ্টন U(−k, k) থেকে আসে, যেখানে k = 1/√in; ভেতরের কোডে এটি kaiming_uniform_(a=√5) নামে লেখা, যা গণিতে একই সীমা দেয়। আমাদের run-এ in = ৩, তাই সব weight ±০.৫৭৭৪-এর ভেতরে, আর সবচেয়ে বড়টি ০.৫৭০২. Input বেশি হলে weight ছোট হয়, যাতে যোগফল বিস্ফোরিত না হয়। এটি একটি যুক্তিসঙ্গত শুরু, সব ক্ষেত্রে সেরা নয়; Xavier, He আর কেন initialization এত গুরুত্বপূর্ণ, তা পর্ব ১৪ (Weight initialization আর normalization — Xavier, He, BatchNorm আর LayerNorm)-এ।

৪ · Parameter গোনা — প্রতিটি স্তর আলাদা করে

Parameter-এর সংখ্যা জানা দরকার তিন কারণে: memory-র হিসাব (float32-এ প্রতিটি ৪ byte), overfitting-এর ঝুঁকির একটি মোটা আন্দাজ, আর ভুল ধরা (গোনা শূন্য বা অপ্রত্যাশিত হলে কোথাও নিবন্ধন ভুল)। PyTorch-এ এক লাইন: sum(p.numel() for p in model.parameters()); numel() মানে tensor-এ মোট ঘরের সংখ্যা। শুধু শেখার যোগ্যগুলো চাইলে if p.requires_grad জুড়ে দিন, কারণ freeze করা parameter-ও parameters()-এ আসে। পরীক্ষায় প্রায়ই হাতে গুনতে হয়, তাই নিচের ব্লক Linear-এর স্তূপের জন্য একটি ছোট হিসাবযন্ত্র। একটি তালিকা [in, h1, h2, …, out] দিন; প্রতিটি পাশাপাশি জোড়া একটি nn.Linear.

def count_linear_stack(sizes, bias=True, bytes_per=4):
    """sizes = [in, h1, h2, ..., out]; প্রতিটি জোড়া একটি nn.Linear"""
    rows, total = [], 0
    for a, b in zip(sizes, sizes[1:]):
        w, bb = b * a, (b if bias else 0)     # weight (out, in), bias (out,)
        total += w + bb
        rows.append(f"  Linear({a}, {b}): {w:,} + {bb} = {w + bb:,}")
    print(f"{sizes}  bias={bias}")
    print("\n".join(rows))
    print(f"  মোট {total:,} ≈ {total * bytes_per / 1024:,.1f} KiB (float32)")
    return total

count_linear_stack([3, 16, 8, 1])            # বাজারঘরের ডেলিভারি MLP
count_linear_stack([784, 256, 128, 10])      # 28×28 ছবি → ১০টি শ্রেণি
count_linear_stack([784, 256, 128, 10], bias=False)

wide = count_linear_stack([1024, 1024])      # একটি চওড়া স্তর
thin = count_linear_stack([1024, 64, 1024])  # সরু মাঝখান (bottleneck)
print(f"bottleneck-এ parameter {wide / thin:.1f} গুণ কম")

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

[3, 16, 8, 1]  bias=True
  Linear(3, 16): 48 + 16 = 64
  Linear(16, 8): 128 + 8 = 136
  Linear(8, 1): 8 + 1 = 9
  মোট 209 ≈ 0.8 KiB (float32)
[784, 256, 128, 10]  bias=True
  Linear(784, 256): 200,704 + 256 = 200,960
  Linear(256, 128): 32,768 + 128 = 32,896
  Linear(128, 10): 1,280 + 10 = 1,290
  মোট 235,146 ≈ 918.5 KiB (float32)
[784, 256, 128, 10]  bias=False
  Linear(784, 256): 200,704 + 0 = 200,704
  Linear(256, 128): 32,768 + 0 = 32,768
  Linear(128, 10): 1,280 + 0 = 1,280
  মোট 234,752 ≈ 917.0 KiB (float32)
[1024, 1024]  bias=True
  Linear(1024, 1024): 1,048,576 + 1024 = 1,049,600
  মোট 1,049,600 ≈ 4,100.0 KiB (float32)
[1024, 64, 1024]  bias=True
  Linear(1024, 64): 65,536 + 64 = 65,600
  Linear(64, 1024): 65,536 + 1024 = 66,560
  মোট 132,160 ≈ 516.2 KiB (float32)
bottleneck-এ parameter 7.9 গুণ কম

প্রথম তালিকা ডেস্কটপের named_parameters()-এর সঙ্গে হুবহু মেলে: ৬৪ + ১৩৬ + ৯ = ২০৯. দ্বিতীয়টির (২৮ × ২৮ = ৭৮৪ pixel-এর ছবি থেকে ১০টি শ্রেণি) ৮৫ শতাংশের বেশি parameter প্রথম স্তরেই, কারণ গুণফল in × out-এ সবচেয়ে বড় input সেখানে। একই model ডেস্কটপে nn.Sequential দিয়ে বানিয়ে গুনলে ২,৩৫,১৪৬ আসে। Bias বাদ দিলে কমে মাত্র ৩৯৪টি: weight-ই প্রধান। শেষ দুটি সারি একটি নকশার শিক্ষা: ১০২৪ থেকে সরাসরি ১০২৪-এ যাওয়ার চেয়ে মাঝে ৬৪-এর একটি সরু স্তর রাখলে parameter প্রায় ৮ ভাগের ১ ভাগ, বিনিময়ে মাঝে ৬৪টি সংখ্যায় তথ্য চাপতে হয়। ছবির model-এ convolution কেন এর চেয়েও কম parameter-এ কাজ করে, তা পর্ব ২০ (Convolution — filter, stride, padding আর receptive field)-এ।

গোনার নিয়ম

nn.Linear(in, out): out × in + out (bias=False হলে শুধু out × in). ReLU, Sigmoid-এর মতো activation আর Dropout-এ parameter শূন্য। পুরো স্তূপে প্রতিটি পাশাপাশি জোড়ার যোগফল। Memory: float32-এ parameter × ৪ byte; training-এ gradient আর optimizer-এর অবস্থাও জায়গা নেয়, যার হিসাব পর্ব ১২-এ।

৫ · Sequential, ModuleList, ModuleDict — আর সাধারণ list-এর ফাঁদ

অনেক layer একসঙ্গে রাখার তিনটি container আছে, প্রতিটি নিজেই একটি nn.Module, তাই ভেতরের সব কিছু নিবন্ধিত হয়:

Containerনিজের forward আছে?key-এর রূপকখন
nn.Sequentialহ্যাঁ, ক্রমে একের পর এক চালায়hidden.0.weightসোজা শিকল, কোনো শাখা নেই
nn.ModuleListনা, আপনি লুপে চালাবেনlayers.2.biasস্তরের সংখ্যা চলক, বা লুপে মাঝে কিছু করতে হয়
nn.ModuleDictনাheads.time.weightনাম দিয়ে বাছাই, যেমন কয়েকটি output head

এখন ফাঁদটি। স্তরের সংখ্যা একটি তালিকা থেকে ঠিক করতে চাইলে স্বাভাবিক Python অভ্যাসে মানুষ list comprehension লেখে। অংশ ২-এর নিয়ম মনে রাখলে বুঝবেন কী হবে: self.layers-এর মান একটি list, Module নয়, তাই PyTorch ভেতরে তাকায় না। ডেস্কটপে দুটি class পাশাপাশি চালানো হলো; দ্বিতীয়টিতে শুধু list-কে nn.ModuleList-এ মোড়ানো।

import torch
from torch import nn

class ListMLP(nn.Module):                   # ভুল: সাধারণ Python list
    def __init__(self, sizes):
        super().__init__()
        self.layers = [nn.Linear(a, b) for a, b in zip(sizes, sizes[1:])]
    def forward(self, x):
        for i, layer in enumerate(self.layers):
            x = layer(x)
            if i < len(self.layers) - 1: x = torch.relu(x)
        return x

class FixedMLP(ListMLP):                    # ঠিক: nn.ModuleList
    def __init__(self, sizes):
        super().__init__(sizes)
        self.layers = nn.ModuleList(self.layers)

torch.manual_seed(0)
for cls in (ListMLP, FixedMLP):
    m = cls([3, 16, 8, 1])
    n = sum(p.numel() for p in m.parameters())
    print(f"{cls.__name__}: forward {tuple(m(torch.zeros(4, 3)).shape)}, "
          f"parameter {n}, state_dict keys {len(m.state_dict())}")
    try:
        torch.optim.SGD(m.parameters(), lr=0.1)
        print("  optimizer তৈরি হলো")
    except ValueError as e:
        print("  ValueError:", e)

সত্যিকারের run (একই project আর সংস্করণ):

~/bazarghar-dl$ uv run python list_trap.py
ListMLP: forward (4, 1), parameter 0, state_dict keys 0
  ValueError: optimizer got an empty parameter list
FixedMLP: forward (4, 1), parameter 209, state_dict keys 6
  optimizer তৈরি হলো

সবচেয়ে বিপজ্জনক অংশ প্রথম লাইনের প্রথম অংশ: forward ঠিকঠাক চলে, আকারও সঠিক (৪, ১)। এখানে optimizer অন্তত error দিয়েছে, কারণ model-এ আর কিছুই ছিল না। কিন্তু কল্পনা করুন model-এ একটি নিবন্ধিত self.head আছে আর শুধু মাঝের স্তরগুলো list-এ: তখন optimizer head-এর parameter পায়, কোনো error নেই, training চলে, loss একটু কমেও, অথচ মাঝের স্তরগুলো চিরকাল শুরুর এলোমেলো weight-এ আটকে থাকে। সেভ করা ফাইলেও তারা নেই, তাই পরের দিন load করা model আলাদা আচরণ করে। ধরার সহজ উপায়: model বানিয়েই parameter গুনুন আর হাতের হিসাবের সঙ্গে মেলান (অংশ ৪)। একই নিয়ম dict-এ খাটে (nn.ModuleDict), আর সরাসরি parameter-এর তালিকার জন্য nn.ParameterList.

৬ · Buffer — শেখা হয় না, কিন্তু model-এর অংশ

আমাদের model input-কে গড় আর standard deviation দিয়ে মানানসই করে (normalization; কেন দরকার, AI-এর ভিত্তি কোর্সের feature engineering-এর পর্বে)। এই দুটি সংখ্যা training ডেটা থেকে একবার হিসাব হয়, gradient দিয়ে শেখানো হয় না। কোথায় রাখবেন? তিনটি পথ:

  • nn.Parameter হিসেবে: ভুল। Optimizer এদের বদলাতে থাকবে, যদিও এরা ডেটার স্থির পরিসংখ্যান।
  • সাধারণ attribute self.x_mean = …: forward চলে, কিন্তু state_dict-এ যায় না, আর .to("cuda")-তে সরে না। অন্য কম্পিউটারে model load করলে normalization-এর মান হারিয়ে যায়; অংশ ২-এর mu2-এর মতো।
  • self.register_buffer("x_mean", …): ঠিক। state_dict-এ যায়, device আর dtype-এর সঙ্গে সরে, কিন্তু parameters()-এ আসে না, তাই optimizer ছোঁয় না।

PyTorch-এর নিজের layer-ও buffer ব্যবহার করে। সবচেয়ে পরিচিত উদাহরণ BatchNorm-এর running_mean আর running_var, যা training-এর সময় চলমান গড় হিসেবে বদলায়, কিন্তু gradient দিয়ে নয় (পর্ব ১৪-এ)। যে buffer সেভ করার দরকার নেই (যেমন প্রতিবার আবার হিসাব করা যায় এমন একটি mask), তার জন্য register_buffer(name, t, persistent=False): ডেস্কটপের একটি পরীক্ষায় এমন buffer named_buffers()-এ এসেছে, কিন্তু state_dict-এ আসেনি।

ডেটা ফাঁস থেকে সাবধান। Buffer-এর গড় আর standard deviation শুধু training অংশ থেকে হিসাব করুন, পুরো ডেটা থেকে নয়; নইলে test ডেটার তথ্য model-এ ঢুকে যায় (data leakage) (AI-এর ভিত্তি কোর্সের পর্ব ৫৬-এ leakage). এই পর্বের ডেমোতে সরলতার জন্য পুরো ৪০০টি অর্ডার ব্যবহার করা হয়েছে, কারণ এখানে কোনো test মূল্যায়ন নেই।

৭ · state_dict পড়া — আর NumPy-তে হুবহু একই forward

model.state_dict() একটি ক্রমবদ্ধ অভিধান: key হলো ছবি ৯.১-এর পথ, মান হলো tensor. এতে থাকে সব parameter আর persistent buffer; থাকে না model-এর কোড, class-এর নাম, বা optimizer-এর অবস্থা (optimizer-এর নিজের আলাদা state_dict আছে, পর্ব ১০-এ checkpoint-এর সঙ্গে)। অর্থাৎ state_dict হলো “কোন তাকে কোন সংখ্যা”, আর তাকের নকশা আপনার Python class. নিচের stepper আমাদের শেখানো model-এর state_dict-এর আটটি key পড়ে।

key · আকার · কী বোঝা যায়
key
আকার
যা বোঝা যায়
  1. Root-এর buffer. state_dict প্রথমে module-এর নিজের parameter আর buffer লেখে, তারপর সন্তানদের। Root-এর নিজের parameter নেই, তাই দুটি buffer আগে। আকার (৩,) থেকে বোঝা যায় input feature তিনটি।
  2. প্রথম weight. (১৬, ৩) মানে (out, in): ৩টি input থেকে ১৬টি hidden unit. নামের “০” Sequential-এর ভেতরের ক্রম।
  3. প্রথম bias. ১৬টি, প্রতি neuron-এ একটি। এই স্তরে ৪৮ + ১৬ = ৬৪টি parameter.
  4. লাফ দেওয়া নম্বর. ০-এর পরে সরাসরি ৩. মাঝের ১ (ReLU) আর ২ (Dropout)-এর কোনো tensor নেই। Weight (৮, ১৬) মানে ১৬ → ৮; ১২৮ + ৮ = ১৩৬.
  5. Head. (১, ৮): ৮টি input থেকে একটি output; ৮ + ১ = ৯. শুধু key আর আকার পড়েই পুরো স্থাপত্য ৩ → ১৬ → ৮ → ১ লেখা গেল।
  6. মোট. ৬৪ + ১৩৬ + ৯ = ২০৯টি parameter আর ৩ + ৩ = ৬টি buffer-এর মান। কিন্তু activation কোনটি, Dropout-এর p কত, forward-এ কী ক্রমে কী হয়, তা state_dict বলে না; সেটি কোডে।

এখন একটি জোরালো পরীক্ষা: state_dict-এ যদি সত্যিই পুরো model-এর সংখ্যা থাকে, তাহলে PyTorch ছাড়াই, শুধু NumPy দিয়ে একই prediction পাওয়া উচিত। ডেস্কটপে model-টি ৪০০টি অর্ডারে কয়েকশো ধাপ শেখানো হলো (training loop-এর বিস্তার পর্ব ১০-এ; এখানে training MSE এলো ৪.২১৫, ইচ্ছা করে রাখা noise-এর variance ৪-এর কাছাকাছি), তারপর state_dict সেভ করে প্রতিটি tensor JSON-এ লেখা হলো, আর তিনটি নমুনা অর্ডারের prediction ছাপা হলো:

model.eval()                                # training শেষে
with torch.no_grad():
    print("train MSE:", round(((model(X) - y) ** 2).mean().item(), 3))
torch.save(model.state_dict(), "delivery_mlp.pt")
sd = model.state_dict()
with open("delivery_mlp.json", "w") as f:
    json.dump({k: v.tolist() for k, v in sd.items()}, f, separators=(",", ":"))
print("json bytes:", len(open("delivery_mlp.json").read()))

orders = torch.tensor([[2.0, 1, 0], [8.5, 3, 1], [11.0, 5, 0]])
with torch.no_grad():
    p32 = model(orders)
    p64 = model.double()(orders.double())
print("float32:", [f"{v:.12f}" for v in p32.tolist()])
print("float64:", [f"{v:.12f}" for v in p64.tolist()])
~/bazarghar-dl$ uv run python train_export.py
train MSE: 4.215
json bytes: 4474
float32: ['34.320652008057', '61.145069122314', '65.167137145996']
float64: ['34.320649724125', '61.145068057894', '65.167140847360']

সেই ৪,৪৭৪ byte-এর JSON ফাইলটি এই পাতায় delivery_mlp.json নামে রাখা আছে। নিচের ব্লক ফাইলটি পড়ে প্রতিটি key-এর আকার দেখায়, তারপর অংশ ৩-এর সূত্রে (y = xWᵀ + b) NumPy-তে পুরো forward চালায়, float64 আর float32 দুইভাবে। Eval mode-এ Dropout কিছুই করে না, তাই NumPy-তে তার কোনো লাইন নেই।

import json
import numpy as np

with open("delivery_mlp.json") as f:          # PyTorch-এর state_dict, JSON-এ
    raw = json.load(f)
for k, v in raw.items():
    print(f"{k:15s} {np.shape(v)}")

def linear(x, W, b):                           # nn.Linear: y = x Wᵀ + b
    return x @ W.T + b

def forward(x, dt):                            # eval mode: Dropout কিছুই করে না
    sd = {k: np.array(v, dtype=dt) for k, v in raw.items()}
    z = (x.astype(dt) - sd["x_mean"]) / sd["x_std"]
    h = np.maximum(linear(z, sd["hidden.0.weight"], sd["hidden.0.bias"]), 0)
    h = np.maximum(linear(h, sd["hidden.3.weight"], sd["hidden.3.bias"]), 0)
    return linear(h, sd["head.weight"], sd["head.bias"])[:, 0]

orders = np.array([[2.0, 1, 0], [8.5, 3, 1], [11.0, 5, 0]])
p64, p32 = forward(orders, np.float64), forward(orders, np.float32)
t64 = np.array([34.320649724125, 61.145068057894, 65.167140847360])  # ডেস্কটপ
t32 = np.array([34.320652008057, 61.145069122314, 65.167137145996])  # PyTorch
for o, p in zip(orders, p64):
    print(f"{o[0]:4.1f} km, {int(o[1])} পণ্য, বৃষ্টি {int(o[2])}: {p:.12f} মিনিট")
print(f"float64: max |NumPy − PyTorch| = {np.abs(p64 - t64).max():.1e}")
print(f"float32: max |NumPy − PyTorch| = {np.abs(p32 - t32).max():.1e}")
print(f"float32 বনাম float64: {np.abs(p32 - p64).max():.1e} মিনিট")

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

x_mean          (3,)
x_std           (3,)
hidden.0.weight (16, 3)
hidden.0.bias   (16,)
hidden.3.weight (8, 16)
hidden.3.bias   (8,)
head.weight     (1, 8)
head.bias       (1,)
 2.0 km, 1 পণ্য, বৃষ্টি 0: 34.320649724125 মিনিট
 8.5 km, 3 পণ্য, বৃষ্টি 1: 61.145068057894 মিনিট
11.0 km, 5 পণ্য, বৃষ্টি 0: 65.167140847360 মিনিট
float64: max |NumPy − PyTorch| = 3.8e-13
float32: max |NumPy − PyTorch| = 3.8e-06
float32 বনাম float64: 3.7e-06 মিনিট

প্রথম আটটি লাইন অংশ ৭-এর stepper-এর আটটি key আর আকার, এবার একটি সাধারণ JSON ফাইল থেকে পড়া। তারপর তিনটি অর্ডারের prediction: ২ km-এর একটি পণ্য প্রায় ৩৪.৩ মিনিট, বৃষ্টিতে ৮.৫ km-এর তিনটি পণ্য প্রায় ৬১.১ মিনিট। float64-এ NumPy আর PyTorch-এর পার্থক্য 3.8 × 10−13, অর্থাৎ বারো দশমিক পর্যন্ত এক; বাকিটুকু শুধু ডেস্কটপের মানগুলো বারো দশমিকে ছাপার rounding. মানে state_dict-এ সত্যিই পুরো model-এর সংখ্যা আছে, আর nn.Linear ঠিক y = xWᵀ + b-ই করে। float32-এ সর্বোচ্চ পার্থক্য 3.8 × 10−6, আর ডেস্কটপে আলাদা করে দেখা গেছে এটি শুধু প্রথম অর্ডারে (৩৪.৩২); বাকি দুটি হুবহু এক। ৩২ থেকে ৬৪-এর মধ্যে float32-এর পাশাপাশি দুটি সংখ্যার ব্যবধান 2−18 ≈ 3.8 × 10−6, তাই এটি শেষ bit-এর একটি ধাপ, যোগের ক্রম আলাদা হওয়ার ফল। মিনিটের হিসাবে নগণ্য, কিন্তু শিক্ষাটি বড়: দুটি implementation float32-এ “হুবহু” মেলানোর আশা করবেন না; মেলানোর পরীক্ষা float64-এ করুন, বা float32-এ একটি যুক্তিসঙ্গত সহনসীমা রাখুন।

৮ · Save, load আর strict — নাম না মিললে কী হয়, আর weights_only

PyTorch-এর docs যে পথটি সুপারিশ করে: শুধু state_dict সেভ করুন, torch.save(model.state_dict(), path); load-এর সময় আগে একই class দিয়ে model বানান, তারপর model.load_state_dict(torch.load(path)). load_state_dict-এর default strict=True: ফাইলের key আর model-এর key হুবহু এক সেট না হলে, বা কোনো tensor-এর আকার না মিললে, সঙ্গে সঙ্গে RuntimeError. নিচের script তিনটি পরিস্থিতি দেখায়: সঠিক load, কেউ self.head-এর নাম বদলে self.out রেখেছেন, আর head-এ output দুটি করা হয়েছে। শেষে পুরো model object সেভ করে default-এ খোলার চেষ্টা।

import inspect, pickle, torch
from torch import nn
from bazarghar import DeliveryMLP

sd = torch.load("delivery_mlp.pt")           # default আর্গুমেন্টে
print(type(sd).__name__, "| keys:", list(sd.keys()))
print("torch.load default weights_only =",
      inspect.signature(torch.load).parameters["weights_only"].default)
fresh = DeliveryMLP(torch.zeros(3), torch.ones(3))
print(fresh.load_state_dict(sd))             # strict=True default
fresh.eval()
with torch.no_grad():
    print("prediction:", round(fresh(torch.tensor([[8.5, 3.0, 1.0]])).item(), 4))

r = Renamed(torch.zeros(3), torch.ones(3))   # head-এর নাম বদলে out রাখা subclass
try: r.load_state_dict(sd)
except RuntimeError as e: print("RuntimeError:", e)
print("strict=False:", r.load_state_dict(sd, strict=False))

big = DeliveryMLP(torch.zeros(3), torch.ones(3)); big.head = nn.Linear(8, 2)
try: big.load_state_dict(sd)
except RuntimeError as e: print("RuntimeError:", str(e).splitlines()[-1].strip())

torch.save(fresh, "whole_model.pt")           # পুরো object, pickle দিয়ে
try: torch.load("whole_model.pt")
except pickle.UnpicklingError as e: print_key_lines(e)   # তিনটি মূল লাইন ছাপে
m = torch.load("whole_model.pt", weights_only=False)     # শুধু নিজের বিশ্বস্ত ফাইলে
print("weights_only=False:", type(m).__name__)

সত্যিকারের run (Renamed class আর ANSI রং-চিহ্ন মুছে লাইন বাছাইয়ের ছোট function-টি ফাইলে আছে, এখানে জায়গা বাঁচাতে বাদ; দীর্ঘ লাইন “…” দিয়ে ছাঁটা):

~/bazarghar-dl$ uv run python state_io.py
OrderedDict | keys: ['x_mean', 'x_std', 'hidden.0.weight', 'hidden.0.bias', 'hidden.3.weight', 'hidden.3.bias', 'head.weight', 'head.bias']
torch.load default weights_only = None
<All keys matched successfully>
prediction: 61.1451
RuntimeError: Error(s) in loading state_dict for Renamed:
	Missing key(s) in state_dict: "out.weight", "out.bias".
	Unexpected key(s) in state_dict: "head.weight", "head.bias".
strict=False: _IncompatibleKeys(missing_keys=['out.weight', 'out.bias'], unexpected_keys=['head.weight', 'head.bias'])
RuntimeError: size mismatch for head.bias: copying a param with shape torch.Size([1]) from checkpoint, the shape in current model is torch.Size([2]).
  Weights only load failed. This file can still be loaded, to do so you have two options, do those steps only if you trust the source of the checkpoint.
  (1) In PyTorch 2.6, we changed the default value of the `weights_only` argument in `torch.load` from `False` to `True`. …
  WeightsUnpickler error: Unsupported global: GLOBAL bazarghar.DeliveryMLP was not an allowed global by default. …
weights_only=False: DeliveryMLP

সঠিক load: নতুন model-টি শূন্য গড় আর এক standard deviation দিয়ে বানানো হয়েছিল, তবু load-এর পর prediction ৬১.১৪৫১, অংশ ৭-এর float32 মানের সঙ্গে এক: buffer-ও ফাইল থেকে এসেছে। নাম বদল: error বার্তাটি দুই তালিকায় পরিষ্কার বলে কী নেই (Missing) আর কী বাড়তি (Unexpected). strict=False error দেয় না, শুধু তালিকা ফেরত দেয়, আর মিলে যাওয়া key-গুলোই বসায়; ফলে out শুরুর এলোমেলো weight-এ থাকে। তাই strict=False ব্যবহার করলে ফেরত আসা তালিকা অবশ্যই পরীক্ষা করুন; এটি ইচ্ছাকৃত আংশিক load-এর জন্য (যেমন pretrained body নিয়ে নতুন head, পর্ব ২৩-এ transfer learning). আকার: strict=False দিলেও আকারের অমিল error দেয়।

weights_only. torch.save ভেতরে Python-এর pickle ব্যবহার করে, আর pickle ফাইল খোলার সময় ফাইলের নির্দেশমতো Python object বানাতে পারে, এমনকি function ডাকতে পারে। অচেনা উৎসের ফাইল খোলা তাই কোড চালানোর ঝুঁকি (AI-এর ভিত্তি কোর্সের পর্ব ৭১-এ এই ঝুঁকি একটি সত্যিকারের ফাইলে দেখানো হয়েছে)। Run-এর বার্তাই বলছে, PyTorch 2.6 থেকে weights_only-এর default False থেকে True হয়েছে। Signature-এ default None দেখায়, কারণ ভেতরে None-কে সিদ্ধান্তে বদলানো হয় (এই সংস্করণের source-এ TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD নামের একটি environment variable দিয়ে পুরোনো আচরণ জোর করে ফেরানোর ব্যবস্থাও আছে)। ফল: tensor-এর অভিধান (state_dict) default-এ খোলে, কিন্তু আপনার নিজের class-সহ পুরো model খোলে না, কারণ bazarghar.DeliveryMLP নিরাপদ তালিকায় নেই। দুটি পথ: নিজের বিশ্বস্ত ফাইলে weights_only=False, বা class-টিকে torch.serialization.add_safe_globals-এ যোগ করা। অচেনা ফাইলে কোনোটিই নয়।

torch.save(model.state_dict())torch.save(model)
ফাইলে কীশুধু নাম → tensorপুরো object, class-এর module পথসহ (যেমন bazarghar.DeliveryMLP)
Load-এ কী লাগেএকই class-এর কোড, নিজে instance বানানোঠিক একই নামের module আর class, একই জায়গায়
Default torch.load (2.6+)খোলেUnpicklingError; weights_only=False লাগে
কোড বদলালেkey আর আকার মিললে চলেফাইল বা class সরালে ভাঙে

শেয়ার করার জন্য আরও নিরাপদ বিকল্প safetensors ফরম্যাট, যেখানে শুধু tensor-এর কাঁচা সংখ্যা আর একটি ছোট header থাকে, কোনো কোড নয়; Hugging Face-এর model-এ এটি এখন বহুল ব্যবহৃত। Deployment-এ model পাঠানোর অন্য পথ (ONNX ইত্যাদি) মেশিন লার্নিং কোর্সের পর্ব ৬৮-এ।

৯ · train() আর eval() — শুধু একটি flag, কিন্তু ফল বদলায়

প্রতিটি module-এর একটি boolean attribute আছে, self.training. model.train() গাছের সব module-এ এটি True করে, model.eval() False (eval mode); আর কিছু না। কয়েকটি layer এই flag দেখে আচরণ বদলায়: Dropout training-এ এলোমেলোভাবে কিছু মান শূন্য করে, eval-এ কিছুই করে না; BatchNorm training-এ batch-এর পরিসংখ্যান নেয়, eval-এ জমানো buffer. নতুন model default-এ train mode-এ থাকে। ডেস্কটপে শেখানো model-এ একই অর্ডার (৮.৫ km, ৩টি পণ্য, বৃষ্টি) তিনবার করে চালানো হলো:

import torch
from bazarghar import DeliveryMLP

model = DeliveryMLP(torch.zeros(3), torch.ones(3))
model.load_state_dict(torch.load("delivery_mlp.pt"))
order = torch.tensor([[8.5, 3.0, 1.0]])
torch.manual_seed(7)

model.train()                                 # default অবস্থা
print("training flags:", model.training, model.hidden[2].training)
print("train mode, 3 বার:", [round(model(order).item(), 2) for _ in range(3)])
model.eval()
print("training flags:", model.training, model.hidden[2].training)
print("eval mode, 3 বার:", [round(model(order).item(), 2) for _ in range(3)])
print("eval-এও graph তৈরি হয়? requires_grad =", model(order).requires_grad)
with torch.no_grad():
    print("no_grad-এর ভেতরে requires_grad =", model(order).requires_grad)
drop = torch.nn.Dropout(p=0.5).train()
print("Dropout(0.5) on ones(8):", drop(torch.ones(8)).tolist())
~/bazarghar-dl$ uv run python modes.py
training flags: True True
train mode, 3 বার: [66.34, 52.36, 66.34]
training flags: False False
eval mode, 3 বার: [61.15, 61.15, 61.15]
eval-এও graph তৈরি হয়? requires_grad = True
no_grad-এর ভেতরে requires_grad = False
Dropout(0.5) on ones(8): [0.0, 0.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0]

Train mode-এ একই অর্ডারের উত্তর ৬৬.৩৪, ৫২.৩৬, ৬৬.৩৪ মিনিট: প্রতিবার Dropout আলাদা hidden unit বন্ধ করছে। একজন গ্রাহককে একই অর্ডারে একবার ৫২ আর একবার ৬৬ মিনিট বলা অগ্রহণযোগ্য; eval mode-এ তিনবারই ৬১.১৫, যা সেভ করা model আর NumPy-র মান। একই কারণে শেখানো model-এর ৪০০টি অর্ডারে MSE eval mode-এ ৪.২১৫, কিন্তু train mode-এ মাপলে ডেস্কটপের একটি run-এ এসেছিল ১৮.০৯৮: mode ভুল হলে মাপও ভুল। শেষ লাইনে দেখুন: বেঁচে থাকা মান ১ নয়, ২. একে inverted dropout বলে: training-এ বেঁচে থাকা মানকে 1/(1 − p) দিয়ে গুণ করা হয়, যাতে গড় অপরিবর্তিত থাকে আর eval-এ কিছুই করতে না হয়। আর মাঝের দুটি লাইন একটি সাধারণ ভুল ধারণা ভাঙে: eval() gradient বন্ধ করে না। Prediction-এর সময় model.eval() আর torch.no_grad() (বা torch.inference_mode()) দুটিই লাগে; প্রথমটি layer-এর আচরণের জন্য, দ্বিতীয়টি graph না বানিয়ে memory আর সময় বাঁচানোর জন্য (পর্ব ০৮-এ বিস্তারিত)।

Inverted dropout-এর হিসাবটি NumPy-তে হুবহু লেখা যায়। নিচের ব্লক nn.Dropout-এর মতো একটি ছোট class লেখে, যার নিজের training flag আর train()/eval() আছে, তারপর বহু উদাহরণে মেপে দেখে গড় সত্যিই বদলায় কি না।

import numpy as np

class Dropout:                                  # nn.Dropout-এর NumPy প্রতিচ্ছবি
    def __init__(self, p, rng):
        self.p, self.rng, self.training = p, rng, True
    def train(self): self.training = True; return self
    def eval(self): self.training = False; return self
    def __call__(self, h):
        if not self.training or self.p == 0:
            return h                            # eval: হুবহু ফেরত
        keep = self.rng.random(h.shape) >= self.p
        return h * keep / (1 - self.p)          # inverted dropout: টিকে থাকাদের বড় করা

rng = np.random.default_rng(42)
drop = Dropout(0.5, rng)
h = np.ones(8)
print("train:", drop.train()(h))
print("train:", drop(h))
print("eval :", drop.eval()(h))

big = np.full((10000, 16), 2.0)                 # বহু উদাহরণে গড় কি বদলায়?
drop.train()
for p in (0.1, 0.5):
    drop.p = p
    out = drop(big)
    print(f"p = {p}: শূন্য {np.mean(out == 0):.3f}, গড় {out.mean():.4f} (আসল 2.0)")
print("scale ছাড়া হলে গড়:", (big * (rng.random(big.shape) >= 0.5)).mean().round(4))

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

train: [2. 0. 2. 2. 0. 2. 2. 2.]
train: [0. 0. 0. 2. 2. 2. 0. 0.]
eval : [1. 1. 1. 1. 1. 1. 1. 1.]
p = 0.1: শূন্য 0.099, গড় 2.0018 (আসল 2.0)
p = 0.5: শূন্য 0.501, গড় 1.9956 (আসল 2.0)
scale ছাড়া হলে গড়: 0.9952

Train mode-এ দুবার চালালে দুটি আলাদা mask: প্রথমবার আটটির ছয়টি টিকেছে, দ্বিতীয়বার তিনটি; যারা টিকেছে তারা ১ থেকে ২ হয়েছে, ঠিক ডেস্কটপের nn.Dropout(0.5)-এর মতো। Eval-এ input হুবহু ফেরত। বহু উদাহরণে (১০,০০০ × ১৬টি মান) p = ০.১-এ প্রায় ৯.৯ শতাংশ শূন্য, p = ০.৫-এ প্রায় ৫০.১ শতাংশ, আর দুই ক্ষেত্রেই গড় আসল ২.০-এর খুব কাছে (২.০০১৮, ১.৯৯৫৬)। Scale বাদ দিলে গড় নেমে আসে ০.৯৯৫২-এ, প্রায় অর্ধেক: তখন training আর eval-এ পরের স্তর দুই রকম মাপের input পেত। (এলোমেলো mask-এর জন্য এই শতাংশগুলো এই seed-এর একটি run.) Dropout কেন overfitting কমায় আর p কীভাবে বাছবেন, তা পর্ব ১৬ (Deep learning-এ regularization — dropout, weight decay আর early stopping)-এ।

আরও গভীরে: nn.Module-এর নকশা Lua-ভিত্তিক Torch7 (২০১১ সালের দিকে Collobert ও সহকর্মীদের বর্ণনা)-এর nn package-এর উত্তরসূরি বলে সাধারণত উল্লেখ করা হয়; PyTorch-এর নিজের নকশা Paszke ও সহকর্মীদের NeurIPS ২০১৯-এর প্রবন্ধে বর্ণিত। ভেতরে আরও কিছু যন্ত্র আছে: forward_pre_hook আর forward_hook (প্রতিটি layer-এর input-output দেখা), _load_from_state_dict override করে পুরোনো key-কে নতুন নামে বদলানো, আর nn.LazyLinear, যা প্রথম input দেখে in_features ঠিক করে। PyTorch-এর serialization docs weights_only-এর নিরাপদ type-এর তালিকা আর torch.serialization.safe_globals ব্যাখ্যা করে; Dropout-এর মূল প্রবন্ধ Srivastava ও সহকর্মীদের ২০১৪ সালের JMLR প্রবন্ধ, যার বিস্তার পর্ব ১৬-এ।

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

  • nn.Linear(128, 10)-এর weight-এর আকার আর parameter? (10, 128); ১২৮০ + ১০ = ১২৯০.
  • Layer-গুলো Python list-এ রাখলে কী হয়? নিবন্ধিত হয় না: parameters()-এ নেই, optimizer পায় না, state_dict-এ নেই, .to()-তে সরে না। সমাধান nn.ModuleList.
  • Buffer আর parameter-এর পার্থক্য? দুটিই state_dict-এ যায় আর device-এর সঙ্গে সরে; parameter শেখানো হয় (optimizer পায়), buffer হয় না।
  • model.eval() কি gradient বন্ধ করে? না; শুধু training flag বদলায় (Dropout, BatchNorm). Gradient বন্ধ করতে torch.no_grad() বা inference_mode().
  • torch.save(model) না করে state_dict কেন? ফাইল class-এর কোড আর module পথের ওপর নির্ভর করে না, আর PyTorch 2.6+-এ default weights_only=True-তে নিরাপদে খোলে।

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

AI assistant এক মিনিটে একটি nn.Module subclass লিখে দেয়, আর প্রায় সবসময় তা চলে। এই পর্বের বাগগুলো ঠিক সেই ধরনের, যা চলে কিন্তু ভুল: list-এ রাখা layer, সাধারণ attribute-এ রাখা normalization-এর মান, transpose ছাড়া weight, eval ছাড়া prediction. পুরোনো টিউটোরিয়াল থেকে শেখা AI প্রায়ই torch.load(path) লিখে ধরে নেয় পুরো model খুলবে, বা error এলে কোনো সতর্কতা ছাড়াই weights_only=False বসিয়ে দেয়।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
Layer list বা dict-এ রাখেনিবন্ধিত হয় না; শেখা, সেভ, device কিছুই হয় নাParameter গুনে হাতের হিসাবের সঙ্গে মেলান
গড়/std সাধারণ attribute-এ রাখেstate_dict-এ যায় না, অন্য মেশিনে হারায়state_dict().keys() পড়ুন; register_buffer ব্যবহার করুন
Untrusted ফাইলে weights_only=Falsepickle কোড চালাতে পারেশুধু state_dict বা safetensors নিন; উৎস যাচাই করুন
error চাপা দিতে strict=Falseঅমিল key চুপচাপ এলোমেলো থাকেফেরত আসা missing/unexpected তালিকা দেখুন
Prediction-এ model.eval() বা no_grad ভুলে যায়Dropout চালু, উত্তর এলোমেলো; অকারণে graphএকই input দুবার চালিয়ে উত্তর এক কি না দেখুন

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

আমি PyTorch-এ (আমার version: 2.x, নিজে লিখে দেব) একটি tabular regression model লিখছি: input ৩টি feature, hidden 16 আর 8, output ১টি, ReLU আর Dropout 0.1. ১. nn.Module subclass লেখো; input normalization-এর গড় আর std register_buffer দিয়ে রাখো। ২. প্রতিটি স্তরের parameter হাতে গুনে লেখো, তারপর named_parameters() দিয়ে মেলানোর কোড দাও। ৩. state_dict-এর key কী কী হবে, চালানোর আগে লিখে দাও। ৪. শুধু state_dict সেভ আর strict=True-তে load-এর কোড দাও; torch.load-এর weights_only নিয়ে আমার version-এ কী আচরণ, তা docs-এর লিংকসহ বলো। ৫. Prediction-এর জন্য eval() আর no_grad() কোথায় বসবে দেখাও, আর একই input দুবার চালিয়ে মিল যাচাইয়ের একটি assert দাও। ৬. কোন অংশে তোমার ভুলের ঝুঁকি বেশি, সেটিও বলো।

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

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

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

প্র ০১PyTorch “জাদু”র মতো attribute assignment থেকে নিবন্ধন করে, যেখানে অন্য library স্পষ্ট তালিকা চায়। এই নকশার সুবিধা আর দাম কী?

nn.Module-এর পথ হলো object-oriented: model একটি object, তার অংশগুলো attribute, আর __setattr__ চুপচাপ খাতা রাখে। এই ধারা Lua-ভিত্তিক Torch7-এর nn package থেকে এসেছে বলে সাধারণত বলা হয়, আর Chainer-এর Link/Chain-এও প্রায় একই ধারণা ছিল। সুবিধা স্পষ্ট: model লেখা সাধারণ Python class লেখার মতো, কোনো আলাদা নিবন্ধনের লাইন নেই, আর গাছের মতো গঠন (block-এর ভেতরে block) নিজে নিজে নাম পায়। ফলে বিশাল model-এও model.parameters() এক লাইনে সব weight দেয়।

দামটি এই পর্বেই দেখেছেন: নিয়ম অদৃশ্য। যিনি __setattr__-এর কথা জানেন না, তাঁর কাছে list আর ModuleList-এর পার্থক্য অর্থহীন, আর ভুলটি error দেয় না। একই কারণে super().__init__() বাদ দিলে অদ্ভুত বার্তা আসে। এটি পুরোনো একটি trade-off: “implicit” সুবিধা বনাম “explicit” স্বচ্ছতা। Python-এর নিজের নীতিবাক্যেও আছে, explicit is better than implicit.

অন্য ধারা হলো functional: JAX-ভিত্তিক কিছু library-তে (যেমন Flax বা Haiku) parameter একটি আলাদা গাছের মতো অভিধান, যা function-কে স্পষ্টভাবে দিতে হয়; model নিজে অবস্থা রাখে না। সেখানে list-এর ফাঁদ নেই, কারণ parameter কোথায় তা আপনি নিজেই ধরে আছেন; কিন্তু অভিধান সামলানো আর নাম মেলানো কোড বেশি লাগে। Keras আরেক মাঝামাঝি পথ নেয়: layer attribute হিসেবে রাখলে track হয়, অনেক ক্ষেত্রে list-এর ভেতরেরও।

বাস্তব শিক্ষা হলো, যে framework-ই ব্যবহার করুন, তার নিবন্ধনের নিয়ম জানুন আর প্রতিটি নতুন model-এ parameter গুনে একটি যাচাই রাখুন। নিয়ম জানলে জাদু আর জাদু থাকে না, শুধু সুবিধা থাকে।

মূল উপলব্ধি: অদৃশ্য নিবন্ধন লেখা সহজ করে কিন্তু ভুলকে নীরব করে; তাই parameter গোনা আর state_dict-এর key পড়া প্রতিটি model-এর প্রথম পরীক্ষা হওয়া উচিত।

প্র ০২pickle-ভিত্তিক model ফাইল কেন নিরাপত্তার ঝুঁকি, আর weights_only-এর default বদলাতে PyTorch-এর এত সময় লাগল কেন?

Python-এর pickle আসলে একটি ছোট stack-ভিত্তিক নির্দেশ-ভাষা। ফাইল খোলার সময় এই নির্দেশ চলে, আর তার মধ্যে “এই module-এর এই function-টি এই আর্গুমেন্ট দিয়ে ডাকো” ধরনের নির্দেশও বৈধ। Python-এর নিজের docs বহু বছর ধরে সতর্ক করে আসছে: অবিশ্বস্ত উৎসের pickle কখনো খুলবেন না। Model ফাইল যখন শুধু নিজের কম্পিউটারে থাকত, ঝুঁকিটি তাত্ত্বিক ছিল। কিন্তু model hub-এর যুগে অচেনা মানুষের ফাইল নামানো রোজকার কাজ, আর ২০২৩–২০২৪ সালের দিকে কয়েকটি নিরাপত্তা গবেষণা দল জনসমক্ষে থাকা model ফাইলে ক্ষতিকর pickle পাওয়ার কথা জানায় বলে রিপোর্ট হয়েছে।

PyTorch অনেক আগেই weights_only=True বিকল্প যোগ করেছিল, যা একটি সীমিত unpickler দিয়ে শুধু tensor, অভিধান, তালিকা আর কিছু নিরাপদ type খোলে। কিন্তু default বদলানো কঠিন ছিল, কারণ লাখো পুরোনো script torch.save(model) দিয়ে পুরো object সেভ করত, আর optimizer-এর checkpoint-এও নানা Python object থাকত। Default বদলালে সেগুলো এক রাতে ভেঙে যেত। তাই কয়েকটি সংস্করণ ধরে সতর্কবার্তা দেখিয়ে শেষে 2.6-এ বদলানো হয়, যা এই পর্বের run-এর error বার্তাতেই লেখা। এই ধীর পথটি software-এ পরিচিত: deprecation warning, তারপর default বদল, সঙ্গে পুরোনো আচরণ ফেরানোর একটি স্পষ্ট পথ।

আরও মৌলিক সমাধান হলো এমন ফরম্যাট, যেখানে কোড রাখার জায়গাই নেই। Hugging Face-এর safetensors ফাইলে একটি JSON header (নাম, dtype, আকার, অবস্থান) আর তারপর কাঁচা byte; খোলা মানে শুধু পড়া। এতে memory-map করে দ্রুত load-ও সহজ হয়। AI-এর ভিত্তি কোর্সের পর্ব ৭১-এ এই তুলনা একটি আসল ফাইলে দেখানো হয়েছে।

মনে রাখার মতো একটি সূক্ষ্মতা: weights_only=True ঝুঁকি অনেক কমায়, কিন্তু কোনো নিরাপত্তা ব্যবস্থাই চিরকালের নিশ্চয়তা নয়; PyTorch আর অন্য library-তেও সময়ে সময়ে নিরাপত্তা ত্রুটি রিপোর্ট আর ঠিক হয়েছে। তাই নিয়ম: library হালনাগাদ রাখুন, উৎস যাচাই করুন, আর সম্ভব হলে safetensors নিন।

মূল উপলব্ধি: model ফাইল ডেটা মনে হলেও pickle-এ তা কোড হতে পারে; তাই শুধু সংখ্যা রাখে এমন ফরম্যাট আর নিরাপদ default-ই ভালো অভ্যাস।

প্র ০৩nn.Linear-এর weight (out, in) আকারে কেন, যেখানে অনেক পাঠ্যবই আর NumPy কোড (in, out) লেখে?

গণিতে একটি linear map সাধারণত y = Wx লেখা হয়, যেখানে x একটি কলাম vector আর W-এর আকার (out, in): প্রতিটি সারি একটি output. PyTorch-এর weight এই প্রচলিত গাণিতিক রূপ রাখে। কিন্তু deep learning-এ ডেটা আসে সারি ধরে: batch-এর প্রতিটি উদাহরণ একটি সারি, X-এর আকার (N, in). তাই সারির রূপে সূত্র দাঁড়ায় Y = XWᵀ + b. পর্ব ০৩-এর মতো অনেক NumPy কোড সরাসরি W-কে (in, out) রাখে আর XW লেখে, transpose ছাড়া। দুটিই ঠিক; শুধু রীতি আলাদা।

PyTorch-এর রীতির কয়েকটি ব্যবহারিক সুবিধা আছে। প্রথম, প্রতিটি output neuron-এর weight একটি সারি, তাই weight[j] মানে j-তম neuron-এর পুরো weight; একটি neuron বাদ দেওয়া, তার norm মাপা বা দুটি স্তর জোড়া সহজ। দ্বিতীয়, এই রীতি পুরোনো Torch7 আর অনেক BLAS-ভিত্তিক কোডের সঙ্গে সামঞ্জস্যপূর্ণ বলে মনে করা হয়। তৃতীয়, embedding (ভাষা model-এ, যেমন পর্ব ৩৬-এর ছোট বাংলা language model-এ) প্রায়ই output স্তরের সঙ্গে weight ভাগ করে (weight tying): embedding-এর আকার (vocab, dim) আর output Linear(dim, vocab)-এর weight-ও (vocab, dim), তাই একই tensor সরাসরি ব্যবহার করা যায়। Transpose আসলে কোনো কপি বানায় না; এটি শুধু stride বদলানো একটি view (পর্ব ০৭-এ), আর matrix গুণের library দুই রূপই দক্ষভাবে চালায়।

বিপদটি আসে রীতি মেশানোর সময়: PyTorch-এর weight NumPy-তে নিয়ে XW লিখলে আকার (N, in) @ (out, in), যা in ≠ out হলে error দেয়, ভালো খবর। কিন্তু in = out হলে (যেমন একটি বর্গাকার স্তর) কোনো error নেই, শুধু ভুল ফল। এই পর্বের NumPy ব্লক তাই linear() function-এ transpose একবারই লিখেছে আর float64-এ PyTorch-এর সঙ্গে মিলিয়েছে।

মূল উপলব্ধি: (out, in) বনাম (in, out) একটি রীতি, সত্য নয়; কিন্তু দুই রীতির মাঝে weight সরালে আকার আর একটি সংখ্যাগত মিল দিয়ে যাচাই করা বাধ্যতামূলক।

প্র ০৪Parameter-এর সংখ্যা কি model-এর ক্ষমতা বা ভালোত্বের মাপ? কোন অর্থে হ্যাঁ, কোন অর্থে না?

Parameter গোনা সহজ, তাই এটি model বর্ণনার সবচেয়ে প্রচলিত সংখ্যা হয়ে উঠেছে: ছোট model কয়েক হাজার, ছবির ক্লাসিক CNN কয়েক কোটি, বড় ভাষা model শত শত কোটি। কিছু অর্থে এটি দরকারি মাপ। Memory আর খরচ সরাসরি এর ওপর নির্ভর করে: float32-এ প্রতি parameter ৪ byte, training-এ gradient আর Adam-এর দুটি অবস্থা মিলে আরও কয়েক গুণ। ভাষা model-এ “scaling law” নিয়ে কাজগুলো (যেমন Kaplan ও সহকর্মীদের ২০২০ আর Hoffmann ও সহকর্মীদের ২০২২ সালের প্রবন্ধ) দেখায়, একই ধরনের স্থাপত্যে parameter আর ডেটা বাড়ালে loss একটি অনুমানযোগ্য হারে কমে, যদিও সেই সম্পর্ক নির্দিষ্ট পরিসরে আর নির্দিষ্ট পরিমাপে।

কিন্তু parameter-এর সংখ্যা “কত ভালো” তা বলে না। প্রথম, স্থাপত্য: convolution একই weight পুরো ছবিতে বারবার ব্যবহার করে, তাই অংশ ৪-এর ৭৮৪-২৫৬-১২৮-১০ MLP-এর চেয়ে অনেক কম parameter-এর CNN সাধারণত ছবিতে ভালো করে (পর্ব ২০-২১)। দ্বিতীয়, একই parameter ব্যবহারের খরচ আলাদা: weight tying বা RNN-এর ধাপে ধাপে একই weight হিসাব বাড়ায়, parameter নয়; আবার mixture-of-experts model-এ মোট parameter বিশাল, কিন্তু প্রতিটি token-এ তার একটি অংশই চলে। তৃতীয়, শাস্ত্রীয় ধারণা “parameter বেশি মানে overfitting” আধুনিক network-এ সরল নয়: পর্ব ০৬-এ double descent দেখেছেন, যেখানে interpolation-এর সীমা পেরিয়ে বড় model আবার ভালো generalize করে।

তাই ব্যবহারিক পরামর্শ: parameter গুনুন memory, খরচ আর বাগ ধরার জন্য; model তুলনা করুন একই ডেটায় validation-এর মাপ দিয়ে, কয়েকটি seed-এ। Marketing-এ শুধু parameter-এর সংখ্যা দিয়ে “বড় মানে ভালো” দাবি দেখলে সন্দেহ করুন।

মূল উপলব্ধি: parameter-এর সংখ্যা খরচের মাপ, গুণের নয়; গুণ মাপতে হয় ডেটায়, একই শর্তে।

প্র ০৫train/eval একটি বৈশ্বিক flag, যা হাতে বদলাতে হয়। এই নকশা থেকে কী কী সত্যিকারের বাগ আসে, আর কীভাবে এড়াবেন?

PyTorch-এ mode হলো প্রতিটি module-এর একটি অবস্থা, function-এর আর্গুমেন্ট নয়। সুবিধা হলো forward-এর signature সরল থাকে; অসুবিধা হলো অবস্থা ভুলে যাওয়া যায়, আর ভুলের লক্ষণ প্রায়ই সূক্ষ্ম। প্রথম পরিচিত বাগ: validation-এ model.eval() ভুলে যাওয়া। Dropout-ওয়ালা model-এ validation loss বেশি আর এলোমেলো দেখায়; BatchNorm-ওয়ালা model-এ আরও খারাপ, কারণ ছোট validation batch-এর পরিসংখ্যান দিয়ে normalize হয়, আর একটি উদাহরণের prediction নির্ভর করে একই batch-এ আর কারা আছে তার ওপর।

দ্বিতীয় বাগ উল্টো: validation-এর পর আবার model.train() না ডাকা। তখন পরের epoch-এর training Dropout ছাড়াই চলে, BatchNorm-এর চলমান গড় আর হালনাগাদ হয় না; কোনো error নেই, শুধু model একটু অন্যরকম শেখে। এজন্য পর্ব ১০-এর ছাঁচে প্রতিটি epoch-এর শুরুতে train() আর validation-এর শুরুতে eval() ডাকা হয়, ধরে না নিয়ে। তৃতীয়, freeze করা অংশ: transfer learning-এ body-র parameter-এ requires_grad False করলেও BatchNorm-এর buffer train mode-এ বদলাতে থাকে; অনেকে ওই অংশকে আলাদাভাবে eval-এ রাখেন (পর্ব ২৩)।

অন্য library ভিন্ন পথ নেয়: Keras-এ layer ডাকার সময় training= আর্গুমেন্ট দেওয়া যায়, আর JAX-ভিত্তিক library-তে সাধারণত একটি deterministic বা train flag সরাসরি function-এ যায়। এতে অবস্থা লুকানো থাকে না, কিন্তু প্রতিটি স্তর পর্যন্ত flag পৌঁছে দিতে হয়।

এড়ানোর অভ্যাস: inference-এর একটি ছোট function লিখুন যা নিজেই eval() আর no_grad() বসায়; একই input দুবার চালিয়ে মিল যাচাইয়ের একটি test রাখুন; আর training loop-এ mode বদলের লাইন স্পষ্ট রাখুন। এই পর্বের run-এ ৫২ বনাম ৬৬ মিনিটের পার্থক্যই দেখায়, ভুলের দাম ছোট নয়।

মূল উপলব্ধি: mode একটি লুকানো অবস্থা; প্রতিটি পর্যায়ের শুরুতে স্পষ্টভাবে বসানো আর একটি পুনরাবৃত্তি-পরীক্ষা রাখাই নিরাপদ পথ।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। অংশ ৪-এর হিসাবযন্ত্র দিয়ে গোনা যাচাই করা যায়।

  1. পূর্ণ numerical: বাজারঘরের গ্রাহক-শ্রেণি বাছাইয়ের একটি model: nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 3)). (ক) প্রতিটি স্তরের parameter আর মোট। (খ) float32-এ weight-এর memory কত byte? (গ) state_dict-এর key-গুলো লিখুন।

    (ক) Linear(20, 64): ৬৪ × ২০ + ৬৪ = ১৩৪৪; Linear(64, 32): ৩২ × ৬৪ + ৩২ = ২০৮০; Linear(32, 3): ৩ × ৩২ + ৩ = ৯৯. ReLU আর Dropout-এ ০. মোট ৩৫২৩. (খ) ৩৫২৩ × ৪ = ১৪,০৯২ byte ≈ ১৩.৮ KiB. (গ) Sequential-এর ক্রম ০ থেকে ৫; parameter আছে শুধু ০, ৩, ৫-এ: 0.weight, 0.bias, 3.weight, 3.bias, 5.weight, 5.bias. ডেস্কটপে চালিয়ে ঠিক এই ছয়টি key আর ৩৫২৩ পাওয়া গেছে।

  2. সমালোচনা ও নকশা: একজন সহকর্মীর model-এ: (১) তিনটি hidden স্তর self.blocks = [...] list-এ; (২) self.mean = X.mean(0) সাধারণ attribute, X পুরো ডেটা; (৩) সেভ torch.save(model, "m.pt"); (৪) একটি ফোরাম থেকে নামানো ফাইল torch.load(path, weights_only=False) দিয়ে খোলা; (৫) prediction-এ শুধু with torch.no_grad(). প্রতিটির সমস্যা আর আপনার নকশা লিখুন।

    (১) list-এর স্তর নিবন্ধিত নয়: শেখে না, সেভ হয় না, GPU-তে যায় না। nn.ModuleList বা nn.Sequential. (২) দুটি সমস্যা: state_dict-এ যায় না (অন্য মেশিনে হারায়), আর পুরো ডেটা থেকে গড় নেওয়া leakage. শুধু training অংশ থেকে হিসাব করে register_buffer. (৩) পুরো object pickle হয়, class-এর module পথে বাঁধা, আর 2.6+-এ default load-এ খোলে না। torch.save(model.state_dict(), …). (৪) অচেনা উৎসে weights_only=False মানে ফাইলের কোড চালানোর অনুমতি। Default (True) রাখুন, বা safetensors রূপ খুঁজুন; উৎস যাচাই করুন। (৫) no_grad graph বন্ধ করে, কিন্তু Dropout চালু থাকে: উত্তর এলোমেলো। আগে model.eval(). শেষে একটি test: model বানিয়ে parameter গোনা, সেভ-load করে একই input-এ একই উত্তর।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন দুটি print কী দেখাবে।
    class M(nn.Module):
        def __init__(self):
            super().__init__()
            self.a = nn.Linear(4, 2)
            self.b = [nn.Linear(2, 2)]
            self.register_buffer("s", torch.ones(2))
            self.c = nn.Sequential(nn.ReLU(), nn.Linear(2, 1))
    m = M()
    print(list(m.state_dict().keys()))
    print(sum(p.numel() for p in m.parameters()))
    ['s', 'a.weight', 'a.bias', 'c.1.weight', 'c.1.bias']
    13

    state_dict আগে root-এর নিজের parameter (নেই) আর buffer (s) লেখে, তারপর সন্তানদের নিবন্ধনের ক্রমে: a, তারপর c. b list-এ, তাই অদৃশ্য। c-এর ভেতরে ReLU ০ নম্বরে, তাই Linear-এর key c.1. Parameter: a-তে ২ × ৪ + ২ = ১০, c.1-এ ১ × ২ + ১ = ৩, মোট ১৩; buffer গোনায় আসে না। ডেস্কটপে চালিয়ে হুবহু এই আউটপুট পাওয়া গেছে।

  4. কোড লিখুন: একটি model লিখুন যার একটি ভাগ করা body (Linear(3, 16) + ReLU) আর nn.ModuleDict-এ দুটি head: “time” (Linear(16, 1), ডেলিভারির সময়) আর “late” (Linear(16, 2), দেরি হবে কি না)। forward একটি dict ফেরত দেবে। সঙ্গে একটি function লিখুন যা named_modules() ধরে প্রতিটি স্তরের নিজের parameter ছাপে।
    class TwoHead(nn.Module):
        def __init__(self):
            super().__init__()
            self.body = nn.Sequential(nn.Linear(3, 16), nn.ReLU())
            self.heads = nn.ModuleDict({"time": nn.Linear(16, 1),
                                        "late": nn.Linear(16, 2)})
        def forward(self, x):
            h = self.body(x)
            return {k: head(h) for k, head in self.heads.items()}
    
    def per_layer(model):
        for name, mod in model.named_modules():
            own = sum(p.numel() for p in mod.parameters(recurse=False))
            if own: print(f"{name:12s} {type(mod).__name__:7s} {own:5d}")
        print("মোট", sum(p.numel() for p in model.parameters()))

    ডেস্কটপে per_layer(TwoHead()) ছাপে: body.0 Linear 64, heads.time Linear 17, heads.late Linear 34, মোট ১১৫; আর ৫টি input-এ output-এর আকার time (৫, ১), late (৫, ২)। recurse=False জরুরি: না দিলে Sequential আর root-ও তাদের সন্তানদের parameter আবার গুনত। Key: heads.time.weight ইত্যাদি, অর্থাৎ ModuleDict-এর নামই পথের অংশ।

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

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

ক্রমশেখানো PyTorch model সেভ করে পরে অন্য process-এ prediction-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. torch.save(model.state_dict(), path) দিয়ে শুধু weight আর buffer সেভ করা
  2. নতুন process-এ একই class দিয়ে model-এর একটি instance বানানো
  3. default torch.load(path) দিয়ে state_dict পড়া (weights_only)
  4. load_state_dict(..., strict=True) দিয়ে বসানো আর ফলাফল দেখা
  5. model.eval() ডেকে Dropout আর BatchNorm-কে inference আচরণে নেওয়া
  6. torch.no_grad()-এর ভেতরে prediction চালানো

PyTorch-এ nn.Linear(128, 10)-এর weight-এর আকার আর মোট parameter কত?

  • (128, 10), মোট ১২৮০
  • (10, 128), মোট ১২৮০
  • (10, 128), মোট ১২৯০
  • (10, 128), মোট ১৩৮

একটি nn.Module-এর __init__-এ self.layers = [nn.Linear(3, 16), nn.Linear(16, 1)] লেখা হলো (সাধারণ Python list), আর model-এ অন্য কোনো layer নেই। কী ঘটবে?

  • forward-এ TypeError আসবে
  • forward চলবে, কিন্তু parameters() খালি; optimizer বানাতে গেলে ValueError
  • PyTorch list-টিকে নিজে ModuleList-এ বদলে নেবে
  • সব চলবে, শুধু state_dict-এ key-এর নাম বদলে যাবে

Input normalization-এর গড় আর standard deviation একটি model-এর ভেতরে রাখতে register_buffer ব্যবহারের মূল কারণ কোনটি?

  • যাতে optimizer এগুলো শেখাতে পারে
  • যাতে forward দ্রুত চলে
  • যাতে print(model)-এ মানগুলো দেখা যায়
  • যাতে state_dict-এ সেভ হয় আর device/dtype-এর সঙ্গে সরে, কিন্তু শেখানো না হয়

একটি MLP-এর স্তর ১০ → ৫ → ২, প্রতিটি nn.Linear-এ bias আছে। মোট parameter কত?

  • ৬৭
  • ৬০
  • ১০০
  • ১৭

PyTorch 2.6 বা পরের সংস্করণে torch.save(model, "m.pt") দিয়ে নিজের class-এর পুরো model সেভ করে পরে torch.load("m.pt") (কোনো আর্গুমেন্ট ছাড়া) ডাকলে সাধারণত কী হয়?

  • পুরো model object নিঃশব্দে ফেরত আসে
  • শুধু state_dict ফেরত আসে
  • UnpicklingError, কারণ default weights_only=True আর নিজের class নিরাপদ তালিকায় নেই
  • ফাইলটি নষ্ট বলে RuntimeError

Dropout-সহ একটি PyTorch model-এ model.eval() ডাকার পর কোন কথাটি সত্য?

  • gradient-এর graph তৈরি হওয়াও বন্ধ হয়
  • Dropout কিছুই করে না, তাই একই input-এ প্রতিবার একই output
  • weight-গুলো শেষ সেভ করা মানে ফিরে যায়
  • Dropout-এর p নিজে থেকে ০.৫ হয়ে যায়

শব্দকোষ

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

মনে রাখুন

  1. নিবন্ধন: super().__init__() প্রথমে; তারপর self.x = Module/Parameter বা register_buffer নিজে নিজে খাতায় ওঠে। List, dict বা সাধারণ tensor ওঠে না।
  2. nn.Linear(in, out): weight (out, in), y = xWᵀ + b, parameter out × in + out; default init U(−1/√in, 1/√in).
  3. Container: সোজা শিকলে Sequential, লুপে ModuleList, নামে ModuleDict; model বানিয়েই parameter গুনে যাচাই।
  4. state_dict: পথ-নাম → tensor (parameter + persistent buffer); শুধু এটি সেভ করুন, strict=True-তে load করুন, অমিল হলে Missing/Unexpected পড়ুন। 2.6+-এ torch.load-এর default weights_only=True.
  5. Mode: train()/eval() শুধু flag; prediction-এ eval() আর no_grad() দুটিই।

পরের পর্বে: Model তৈরি, এখন তাকে শেখানো। পর্ব ১০ (Training loop — loss, optimizer, train/eval mode আর checkpoint) পুরো লুপ গড়বে: forward, loss, zero_grad, backward, step; validation-এ eval আর no_grad; আর model, optimizer আর epoch একসঙ্গে checkpoint-এ রেখে ঠিক একই জায়গা থেকে আবার শুরু।

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

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