এই পর্বে যা শিখবেন
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)
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() সবাই শুধু এই তিন অভিধান ধরে পুরো গাছে হাঁটে।
| লাইন | |
| কোথায় গেল | |
| ফলাফল |
- খাতা খোলা.
super().__init__()তিনটি খালি অভিধান বানায়। এটি না ডাকলে প্রথম Module assignment-এই PyTorch বলে “cannot assign module before Module.__init__() call”. - Submodule. মানটি একটি
nn.Module, তাই_modules-এ নাম “fc” দিয়ে বসল। fc-এর নিজের parameter (weight, bias) তার নিজের খাতায়; বাবা model গাছ ধরে সেগুলো খুঁজে নেবে। - Parameter.
nn.Parameterহলো tensor-এর একটি subclass, যারrequires_graddefault-এ True. এটি সরাসরি এই module-এর_parameters-এ। - Buffer. Tensor-টি model-এর অবস্থার অংশ, কিন্তু শেখার জিনিস নয়, যেমন normalization-এর গড়। Buffer state_dict-এ যায় আর
.to(),.double()-এর সঙ্গে বদলায়। - দেখা যায় না এমন জিনিস. List-এর ভেতরের Linear আর সাধারণ tensor কোনো অভিধানে ওঠেনি। Forward-এ এদের ব্যবহার করলে হিসাব চলবে, কিন্তু model-এর হিসাবের খাতায় এরা নেই।
- ফল যাচাই. 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 আসে না।
- আকার. W-এর দুটি সারি দুটি output neuron; প্রতিটি সারিতে তিনটি input-এর weight. তাই আকার (২, ৩) = (out, in), আর bias-এ প্রতি neuron-এ একটি।
- Transpose. x-এর আকার (১, ৩), W-এর (২, ৩); সরাসরি গুণ চলে না। Wᵀ (৩, ২) নিলে ভেতরের মাপ ৩ = ৩ মেলে, আর ফল (১, ২)।
- প্রথম neuron. W-এর প্রথম সারি আর x-এর dot product: ০.৫ − ২.০ + ৬.০ = ৪.৫.
- দ্বিতীয় neuron. দ্বিতীয় সারি: ১.০ + ০ − ১.৫ = −০.৫. প্রতিটি output আসলে “একটি সারি · x”.
- Bias. দুটি bias যোগ করে y = [৪.৬, −০.৭]. এটিই
nn.Linear(3, 2)-এর output, যদি তার weight আর bias এই মানগুলো হয়। - 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 | |
| আকার | |
| যা বোঝা যায় |
- Root-এর buffer. state_dict প্রথমে module-এর নিজের parameter আর buffer লেখে, তারপর সন্তানদের। Root-এর নিজের parameter নেই, তাই দুটি buffer আগে। আকার (৩,) থেকে বোঝা যায় input feature তিনটি।
- প্রথম weight. (১৬, ৩) মানে (out, in): ৩টি input থেকে ১৬টি hidden unit. নামের “০” Sequential-এর ভেতরের ক্রম।
- প্রথম bias. ১৬টি, প্রতি neuron-এ একটি। এই স্তরে ৪৮ + ১৬ = ৬৪টি parameter.
- লাফ দেওয়া নম্বর. ০-এর পরে সরাসরি ৩. মাঝের ১ (ReLU) আর ২ (Dropout)-এর কোনো tensor নেই। Weight (৮, ১৬) মানে ১৬ → ৮; ১২৮ + ৮ = ১৩৬.
- Head. (১, ৮): ৮টি input থেকে একটি output; ৮ + ১ = ৯. শুধু key আর আকার পড়েই পুরো স্থাপত্য ৩ → ১৬ → ৮ → ১ লেখা গেল।
- মোট. ৬৪ + ১৩৬ + ৯ = ২০৯টি 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=False | pickle কোড চালাতে পারে | শুধু state_dict বা safetensors নিন; উৎস যাচাই করুন |
error চাপা দিতে strict=False | অমিল key চুপচাপ এলোমেলো থাকে | ফেরত আসা missing/unexpected তালিকা দেখুন |
Prediction-এ model.eval() বা no_grad ভুলে যায় | Dropout চালু, উত্তর এলোমেলো; অকারণে graph | একই input দুবার চালিয়ে উত্তর এক কি না দেখুন |
AI-কে শুধু class নয়, যাচাইসহ class চাইতে বলুন:
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 একটি লুকানো অবস্থা; প্রতিটি পর্যায়ের শুরুতে স্পষ্টভাবে বসানো আর একটি পুনরাবৃত্তি-পরীক্ষা রাখাই নিরাপদ পথ।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান। অংশ ৪-এর হিসাবযন্ত্র দিয়ে গোনা যাচাই করা যায়।
-
পূর্ণ 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 আর ৩৫২৩ পাওয়া গেছে। -
সমালোচনা ও নকশা: একজন সহকর্মীর 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-এ একই উত্তর। -
আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন দুটি 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'] 13state_dict আগে root-এর নিজের parameter (নেই) আর buffer (
s) লেখে, তারপর সন্তানদের নিবন্ধনের ক্রমে: a, তারপর c.blist-এ, তাই অদৃশ্য। c-এর ভেতরে ReLU ০ নম্বরে, তাই Linear-এর keyc.1. Parameter: a-তে ২ × ৪ + ২ = ১০, c.1-এ ১ × ২ + ১ = ৩, মোট ১৩; buffer গোনায় আসে না। ডেস্কটপে চালিয়ে হুবহু এই আউটপুট পাওয়া গেছে। -
কোড লিখুন: একটি 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-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
torch.save(model.state_dict(), path)দিয়ে শুধু weight আর buffer সেভ করা- নতুন process-এ একই class দিয়ে model-এর একটি instance বানানো
- default
torch.load(path)দিয়ে state_dict পড়া (weights_only) load_state_dict(..., strict=True)দিয়ে বসানো আর ফলাফল দেখাmodel.eval()ডেকে Dropout আর BatchNorm-কে inference আচরণে নেওয়া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 নিজে থেকে ০.৫ হয়ে যায়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- নিবন্ধন:
super().__init__()প্রথমে; তারপরself.x = Module/Parameterবাregister_bufferনিজে নিজে খাতায় ওঠে। List, dict বা সাধারণ tensor ওঠে না। - nn.Linear(in, out): weight (out, in), y = xWᵀ + b, parameter out × in + out; default init U(−1/√in, 1/√in).
- Container: সোজা শিকলে Sequential, লুপে ModuleList, নামে ModuleDict; model বানিয়েই parameter গুনে যাচাই।
- state_dict: পথ-নাম → tensor (parameter + persistent buffer); শুধু এটি সেভ করুন, strict=True-তে load করুন, অমিল হলে Missing/Unexpected পড়ুন। 2.6+-এ
torch.load-এর default weights_only=True. - Mode: train()/eval() শুধু flag; prediction-এ eval() আর no_grad() দুটিই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১০ · Training loop — loss, optimizer, train/eval mode আর checkpointপরবর্তী পর্বএক ধাপের পাঁচটি কাজ, validation, checkpoint থেকে হুবহু আবার শুরু।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ১০ (Training loop) আর পর্ব ১১ (Dataset আর DataLoader) এখান থেকে খুলবেন।
- পর্ব ০৩ · NumPy-তে multilayer perceptronআগের পর্বএকই forward pass NumPy-তে, (in, out) রীতিতে।
- পর্ব ০৫ · নিজে বানানো autogradআগের পর্বparameter-এর .grad কোথা থেকে আসে।
- AI-এর ভিত্তি · পর্ব ৭১ · Pretrained model ব্যবহারআগের ভিত্তিpickle-এর ঝুঁকি, weights_only আর safetensors একটি আসল ফাইলে।
- মেশিন লার্নিং · পর্ব ৬৮ · Model servingপাশের ট্র্যাকসেভ করা model দিয়ে FastAPI, batch scoring আর ONNX.
- Python · পর্ব ৩৩ · inheritance বনাম compositionপাশের ট্র্যাকsubclass আর super(): nn.Module যে ভাষার কৌশলে দাঁড়িয়ে।