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

PyTorch tensor — shape, dtype, device আর broadcasting

PyTorch Tensors — Shape, dtype, Device, Strides, Broadcasting, Batched Matmul and einsum

Taught in Bangla, this lesson covers PyTorch tensors through real desktop runs with version output: creation and seeds, shape, dtype and device rules, the storage and strides behind transpose, view versus reshape and contiguity, step-by-step broadcasting and the silent keepdim bug, batched matmul and einsum giving identical results, NumPy interop and in-place traps, plus a checklist for reading shape errors. Matching in-page Python with NumPy blocks let readers test the same ideas.

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

  • seed-ওয়ালা Generator দিয়ে tensor তৈরি, আর shape, ndim, numel পড়া
  • dtype বাছাই (float32, float64, int64, bool, float16 আর bfloat16-এর প্রথম পরিচয়), type promotion আর device
  • storage, stride আর contiguity; view বনাম reshape, permute, transpose, unsqueeze আর squeeze
  • broadcasting-এর নিয়ম, dim আর keepdim দিয়ে reduction, matmul, batched matmul আর einsum
  • NumPy-র সঙ্গে memory ভাগ, in-place কাজ আর autograd, আর shape error পড়ে বাগ ধরার একটি চেকলিস্ট

AI-এর ভিত্তি কোর্সের পর্ব ১৩ থেকে পর্ব ১৯ পর্যন্ত NumPy array, indexing, broadcasting, linear algebra আর memory-র গতি এসেছে; সেখানে PyTorch-এর প্রথম তুলনা দেখা গেছে পর্ব ৬৮-এ। এই কোর্সের পর্ব ০৩-এ NumPy-তে MLP-এর shape মিলিয়েছেন, আর পর্ব ০৫-এ নিজের tensor autograd বানিয়ে PyTorch-এর backward()-এর সঙ্গে মিলিয়েছেন। এবার মডিউল ২ শুরু: PyTorch-কে ভেতর থেকে চেনা। প্রথম প্রশ্ন সবচেয়ে সাধারণ, কিন্তু deep learning-এর বাগের বড় অংশ এখান থেকেই আসে: একটি tensor আসলে কী, memory-তে কীভাবে থাকে, আর দুটি tensor-এর shape মিলবে কি না তা আগে থেকে কীভাবে বলবেন? PyTorch ব্রাউজারে চলে না, তাই PyTorch-এর সব ফল ডেস্কটপের সত্যিকারের run থেকে নেওয়া। পাতার তিনটি ব্লক ব্রাউজারের Python-এ NumPy দিয়ে একই নিয়ম দেখায়; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে।

১ · Tensor কী — তৈরি, seed আর shape

কুরিয়ার অফিসে প্রতিটি পার্সেলের গায়ে তিনটি তথ্য লেখা থাকে: কয়টি খণ্ড আর কীভাবে সাজানো (shape), ভেতরে কী ধরনের জিনিস, যেমন কাচ না কাপড় (dtype), আর পার্সেলটি এখন কোন গুদামে (device). দুটি পার্সেল একসঙ্গে পাঠাতে হলে তিনটি তথ্যই মিলতে হয়: ঢাকার গুদামের পার্সেল আর চট্টগ্রামের গুদামের পার্সেল একই ট্রাকে ওঠে না। PyTorch-এর tensor-ও ঠিক তাই; প্রায় সব error এই তিন লেবেলের কোনো একটির অমিল।

Tensor হলো একই ধরনের সংখ্যার বহুমাত্রিক array, NumPy-র ndarray-এর খুব কাছের আত্মীয়। তফাত দুটি: tensor GPU-র মতো অন্য device-এ থাকতে পারে, আর পর্ব ০৫-এর মতো gradient রাখতে পারে। একটি scalar-এর ndim ০, একটি vector-এর ১, একটি টেবিলের ২, আর ছবির একটি batch-এর সাধারণত ৪: (N, C, H, W), অর্থাৎ ছবির সংখ্যা, channel, উচ্চতা, প্রস্থ। shape প্রতিটি অক্ষের মাপ, ndim অক্ষের সংখ্যা, numel() মোট ঘর।

ডেস্কটপে uv দিয়ে একটি আলাদা project বানিয়ে PyTorch-এর CPU build install করা হয়েছে, পর্ব ০৫-এর মতোই (uv-এর পরিচয় AI-এর ভিত্তি কোর্সের পর্ব ৭-এ)। প্রথম script বাজারঘরের দুটি অর্ডারের (দূরত্ব km, পণ্যের সংখ্যা) একটি ছোট tensor বানায়, তারপর সাধারণ তৈরির function আর seed দেখে।

import sys, numpy as np, torch
print("python", sys.version.split()[0], "| torch", torch.__version__, "| numpy", np.__version__)

t = torch.tensor([[12.5, 3.0], [4.0, 1.0]])     # দূরত্ব km, পণ্যের সংখ্যা
print(t, t.shape, t.ndim, t.numel(), t.dtype)
print(torch.zeros(2, 3).dtype, torch.arange(5), torch.arange(5).dtype)
print(torch.linspace(0, 1, 5))

g = torch.Generator().manual_seed(42)            # নিজের seed-ওয়ালা generator
a = torch.rand(2, 3, generator=g)
g.manual_seed(42)
b = torch.rand(2, 3, generator=g)
print(a)
print("same seed, same numbers:", torch.equal(a, b))
torch.manual_seed(42)                            # global generator
print(torch.randn(3))
print(torch.full((2, 2), 7), torch.eye(2, dtype=torch.int64))
x = torch.empty(3)                               # মান অনির্ধারিত, শুধু জায়গা
print("empty shape:", x.shape, "| zeros_like:", torch.zeros_like(a).shape)

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

~/bazarghar-dl$ uv run python t1_create.py
python 3.14.4 | torch 2.15.0+cpu | numpy 2.5.4
tensor([[12.5000,  3.0000],
        [ 4.0000,  1.0000]]) torch.Size([2, 2]) 2 4 torch.float32
torch.float32 tensor([0, 1, 2, 3, 4]) torch.int64
tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])
tensor([[0.8823, 0.9150, 0.3829],
        [0.9593, 0.3904, 0.6009]])
same seed, same numbers: True
tensor([0.3367, 0.1288, 0.2345])
tensor([[7, 7],
        [7, 7]]) tensor([[1, 0],
        [0, 1]])
empty shape: torch.Size([3]) | zeros_like: torch.Size([2, 3])

কয়েকটি জিনিস লক্ষ করুন। torch.Size আসলে Python-এর tuple-এর একটি রূপ, তাই t.shape[0] বা tuple(t.shape) স্বাভাবিকভাবে চলে। দশমিক সংখ্যা থেকে float32 এসেছে, NumPy-র মতো float64 নয়; arange(5) পূর্ণসংখ্যা দিয়েছে int64-এ। linspace(0, 1, 5) দুই প্রান্তসহ পাঁচটি সমান দূরত্বের বিন্দু। empty শুধু জায়গা নেয়, মান যা খুশি থাকতে পারে, তাই তার মান কখনো ছাপিয়ে বিশ্বাস করবেন না।

Seed-এর দুটি পথ আছে। torch.manual_seed(42) পুরো program-এর global random অবস্থা বদলায়; অন্য কোনো library বা কোডের অংশ মাঝখানে একটি random সংখ্যা নিলে আপনার ক্রম সরে যায়। নিজের Generator বানিয়ে generator=g দিলে ক্রমটি শুধু আপনার: একই seed দিলে একই সংখ্যা (torch.equal True হয়েছে)। পর্ব ১১ (Dataset আর DataLoader — batching, shuffling আর বাংলা ডেটা)-এ shuffle-এর জন্য এই কৌশলই লাগবে। পুনরুৎপাদনযোগ্যতার পুরো আলোচনা মেশিন লার্নিং কোর্সের পর্ব ৪-এ।

Functionকী দেয়default dtype
torch.tensor(data)list বা array থেকে নতুন কপিডেটা দেখে: int64, float32 বা bool
zeros, ones, full, emptyনির্দিষ্ট shape, নির্দিষ্ট বা অনির্ধারিত মানfloat32 (full-এ মান দেখে)
arange(start, end, step)end বাদ দিয়ে সমান ধাপসব পূর্ণসংখ্যা হলে int64
linspace(a, b, n)দুই প্রান্তসহ n-টি বিন্দুfloat32
rand, randn, randintসমবণ্টন [০, ১), standard normal, পূর্ণসংখ্যাfloat32, float32, int64
zeros_like(x) ইত্যাদিx-এর shape, dtype আর device নিয়েx-এর মতো

২ · dtype আর device — কী ধরনের সংখ্যা, কোথায় রাখা

dtype বলে প্রতিটি ঘর কত বাইটের আর বিটগুলো কীভাবে পড়তে হবে (NumPy-র dtype-এর পুরো আলোচনা AI-এর ভিত্তি কোর্সের পর্ব ১৩-এ)। Deep learning-এ float32 সাধারণ পছন্দ: float64-এর অর্ধেক memory, আর neural network-এর শেখার জন্য তার নির্ভুলতা সাধারণত যথেষ্ট। দুই বাইটের দুটি রূপ, float16 আর bfloat16, বড় model-এ গতি আর memory বাঁচাতে ব্যবহার হয়; এদের পুরো ব্যবহার পর্ব ১২ (GPU, mixed precision আর গতি — device, AMP আর profiling)-এ। এখানে শুধু পার্থক্যটি সংখ্যায় দেখি।

import numpy as np, torch

print(torch.tensor([1, 2]).dtype, torch.tensor([1.0]).dtype,
      torch.tensor([True]).dtype, torch.from_numpy(np.array([1.0])).dtype)
i, f = torch.tensor([3, 4]), torch.tensor([0.5, 0.5])
print("int64 + float32 ->", (i + f).dtype, "| int64 / int64 ->", (i / i).dtype)
print("int64 // 2 ->", i // 2, "| bool sum ->", torch.tensor([True, False, True]).sum())

for dt in (torch.float32, torch.float16, torch.bfloat16, torch.float64):
    fi = torch.finfo(dt)
    print(f"{str(dt):15s} bytes={dt.itemsize} max={fi.max:.3g} eps={fi.eps:.3g}")
print("70000 in float16 :", torch.tensor(70000.0).half())
print("70000 in bfloat16:", torch.tensor(70000.0).bfloat16())
print("1 + 1e-3 in bf16 :", torch.tensor(1.001).bfloat16())

W = torch.ones(2, 2)                          # float32
x64 = torch.ones(2, dtype=torch.float64)
try:
    W @ x64
except RuntimeError as e:
    print("RuntimeError:", e)
print("fix:", (W @ x64.float()).dtype)

print("cuda available:", torch.cuda.is_available())
device = "cuda" if torch.cuda.is_available() else "cpu"
y = W.to(device)
print("W.device:", W.device, "| y.device type:", y.device.type, "| same object:", y is W)
print("to(float16):", W.to(torch.float16).dtype, "| W still:", W.dtype)

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

~/bazarghar-dl$ uv run python t2_dtype.py
torch.int64 torch.float32 torch.bool torch.float64
int64 + float32 -> torch.float32 | int64 / int64 -> torch.float32
int64 // 2 -> tensor([1, 2]) | bool sum -> tensor(2)
torch.float32   bytes=4 max=3.4e+38 eps=1.19e-07
torch.float16   bytes=2 max=6.55e+04 eps=0.000977
torch.bfloat16  bytes=2 max=3.39e+38 eps=0.00781
torch.float64   bytes=8 max=1.8e+308 eps=2.22e-16
70000 in float16 : tensor(inf, dtype=torch.float16)
70000 in bfloat16: tensor(70144., dtype=torch.bfloat16)
1 + 1e-3 in bf16 : tensor(1., dtype=torch.bfloat16)
RuntimeError: addmv input tensors must have the same dtype, but got Double, Float, and Double
fix: torch.float32
cuda available: False
W.device: cpu | y.device type: cpu | same object: True
to(float16): torch.float16 | W still: torch.float32

dtype বেছে নেওয়া: from_numpy NumPy-র float64 ধরে রেখেছে; NumPy থেকে আনা ডেটা model-এ দেওয়ার আগে .float() না করলে ঠিক ওপরের error আসে। Error-এর ভাষায় Double মানে float64, Float মানে float32; addmv হলো matrix-vector গুণের ভেতরের কাজের নাম। Type promotion: int64 আর float32 মিশলে ফল float32; পূর্ণসংখ্যাকে / দিয়ে ভাগ করলেও float আসে, // দিলে পূর্ণসংখ্যা থাকে। bool যোগ করলে True-এর সংখ্যা গোনা হয়, accuracy গোনার পরিচিত কৌশল।

float16 বনাম bfloat16: দুটিই দুই বাইট, কিন্তু বিট ভাগ ভিন্ন। float16-এর সবচেয়ে বড় মান প্রায় ৬৫,৫০৪, তাই ৭০,০০০ ধরতে না পেরে inf হয়ে গেছে, যা overflow. bfloat16 float32-এর মতোই বিশাল পরিসর রাখে (সর্বোচ্চ প্রায় 3.39 × 1038), কিন্তু নির্ভুলতা কম: ৭০,০০০ হয়ে গেছে ৭০,১৪৪, আর ১.০০১ হয়ে গেছে ঠিক ১. eps হলো ১-এর পরের সবচেয়ে কাছের সংখ্যার দূরত্ব (machine epsilon): bfloat16-এ ০.০০৭৮১, অর্থাৎ ১-এর কাছে মোটামুটি দুই-তিনটি অর্থপূর্ণ অঙ্ক পর্যন্তই ভরসা, তাই ১.০০১ আর ১ আলাদা থাকে না। এক কথায়: float16 নির্ভুল কিন্তু পরিসর ছোট, bfloat16 পরিসর বড় কিন্তু মোটা দাগের।

Device: এই project-এ CPU build, তাই torch.cuda.is_available() False আর device "cpu". সাধারণ ছাঁচ হলো একটি device চলক রেখে সব জায়গায় সেটিই ব্যবহার। tensor ইতিমধ্যে সেই device আর dtype-এ থাকলে .to() একই object ফেরত দেয় (same object: True); না থাকলে নতুন কপি বানায়, আর মূল tensor যেমন ছিল তেমন থাকে (W still float32 দেখাচ্ছে)। তাই x.to(device) লিখে ফল কোনো নামে না রাখলে কিছুই বদলায় না। দুটি tensor ভিন্ন device-এ থাকলে একসঙ্গে হিসাব হয় না; RuntimeError আসে। CUDA build আর GPU থাকলে "cuda", Apple-এর যন্ত্রে "mps" device পাওয়া যায়। পুরো device ব্যবস্থাপনা আর সময় মাপা পর্ব ১২-এ।

৩ · Memory-তে tensor — storage, stride আর contiguous

Computer-এর memory একটিই লম্বা সারি। একটি (২, ৩) tensor-এর ছয়টি সংখ্যা সেই সারিতে পাশাপাশি থাকে, প্রথম সারির পরে দ্বিতীয় সারি; একে বলে row-major বা C-order. তাহলে [i, j] ঘরটি কোথায়? উত্তর দেয় stride: প্রতিটি অক্ষে এক ঘর এগোতে storage-এ কত ঘর লাফাতে হয়। (২, ৩)-এর stride (৩, ১): পরের সারিতে যেতে ৩ ঘর, পরের কলামে ১ ঘর। তাই ঠিকানা = i × ৩ + j × ১. PyTorch-এর stride() ঘর গোনে; NumPy-র .strides একই জিনিস বাইটে গোনে (memory layout-এর পুরো আলোচনা AI-এর ভিত্তি কোর্সের পর্ব ১৯-এ)।

একই storage, দুটি দৃষ্টি: a আর a.T

One storage, two views: only shape and stride differ

One storage of six numbers seen as a (2x3, stride 3,1) and a.T (3x2, stride 1,3) storage (memory): 0 1 2 3 4 5 012 345 012 345 03 14 25 a: shape (2, 3) stride (3, 1) contiguous ✓ a.T: shape (3, 2) stride (1, 3) contiguous ✗
ছবি ৭.১ — ছয়টি সংখ্যা memory-তে একবারই আছে। a সারি ধরে পড়লে storage-এর ক্রমেই পড়া হয়; a.T সারি ধরে পড়লে ০, ৩, ১, ৪, ২, ৫: storage-এ সামনে-পেছনে লাফ। কোনো সংখ্যা কপি হয়নি, শুধু shape আর stride বদলেছে।

এই কারণেই transpose, slicing আর permute প্রায় বিনা খরচের: এরা নতুন storage বানায় না, পুরোনো storage-এর ওপর নতুন shape আর stride নিয়ে একটি view দেয়। একটি tensor-কে contiguous বলা হয় যখন তার ঘরগুলো সারি ধরে পড়লে storage-এর ক্রমে, কোনো ফাঁক ছাড়া পড়া হয়; সংজ্ঞাটি stride দিয়ে যাচাই করা যায়: শেষ অক্ষের stride ১, আর প্রতিটি অক্ষের stride তার ডানের অক্ষের stride × মাপ। নিচের ব্লক NumPy দিয়ে এটি দেখায়, কারণ NumPy-র strides PyTorch-এর মতোই কাজ করে।

import numpy as np

a = np.arange(6, dtype=np.int64).reshape(2, 3)   # প্রতিটি ঘর 8 বাইট
t = a.T                                           # transpose: নতুন strides, একই memory
print("a  :", a.shape, "strides", a.strides, "C-contiguous", a.flags.c_contiguous)
print("a.T:", t.shape, "strides", t.strides, "C-contiguous", t.flags.c_contiguous)
print("same memory:", np.shares_memory(a, t))
for i, j in [(0, 1), (1, 0), (2, 1)]:             # ঠিকানা = i*stride0 + j*stride1
    off = i * t.strides[0] + j * t.strides[1]
    print(f"a.T[{i},{j}] = {t[i, j]} → offset {off} bytes = element {off // 8}")
print("memory order:", a.ravel(), "| a.T row by row:", t.ravel())

try:
    np.reshape(t, 6, copy=False)                  # torch-এর view-এর মতো: কপি নিষেধ
except ValueError as e:
    print("ValueError:", e)
c = np.ascontiguousarray(t)                       # torch-এর .contiguous()-এর মতো
print("copy:", c.strides, c.flags.c_contiguous, "| shares memory:", np.shares_memory(c, a))

x = np.zeros((8, 32, 32, 3), dtype=np.float32)    # N, H, W, C
p = x.transpose(0, 3, 1, 2)                       # N, C, H, W
print("NCHW strides (bytes):", p.strides, "→ elements:", tuple(s // 4 for s in p.strides))
print("contiguous:", p.flags.c_contiguous, "| reshape copies:",
      not np.shares_memory(p.reshape(8, -1), x))

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

a  : (2, 3) strides (24, 8) C-contiguous True
a.T: (3, 2) strides (8, 24) C-contiguous False
same memory: True
a.T[0,1] = 3 → offset 24 bytes = element 3
a.T[1,0] = 1 → offset 8 bytes = element 1
a.T[2,1] = 5 → offset 40 bytes = element 5
memory order: [0 1 2 3 4 5] | a.T row by row: [0 3 1 4 2 5]
ValueError: Unable to avoid creating a copy while reshaping.
copy: (16, 8) True | shares memory: False
NCHW strides (bytes): (12288, 4, 384, 12) → elements: (3072, 1, 96, 3)
contiguous: False | reshape copies: True

int64-এর প্রতিটি ঘর ৮ বাইট, তাই NumPy-র (২৪, ৮) বাইট মানে ঘরের হিসাবে (৩, ১), PyTorch-এর stride()-এর মতো। Transpose-এর strides উল্টে (৮, ২৪) হয়েছে, memory একই। ঠিকানার সূত্র হাতে মিলিয়ে দেখুন: a.T[২, ১] = ২ × ৮ + ১ × ২৪ = ৪০ বাইট, অর্থাৎ storage-এর ৫ নম্বর ঘর (০ থেকে গুনলে), মান ৫. copy=False দিয়ে reshape চাইলে NumPy মানা করেছে, ঠিক যেমন PyTorch-এর view মানা করে। ascontiguousarray নতুন memory-তে [০, ৩, ১, ৪, ২, ৫] ক্রমে কপি করেছে, তাই strides (১৬, ৮)। শেষ দুই লাইন ছবির ডেটার পরিচিত দৃশ্য: (N, H, W, C) থেকে (N, C, H, W)-তে permute করলে channel অক্ষের stride ১ হয়ে যায়, tensor আর contiguous থাকে না, আর reshape নতুন কপি বানায়।

storage: [0, 1, 2, 3, 4, 5] · ঠিকানা = Σ (index × stride)
দৃষ্টি
stride
ঠিকানা
contiguous?
  1. মূল tensor. ছয়টি সংখ্যা storage-এ পাশাপাশি। stride (৩, ১), তাই a[১, ২]-এর ঠিকানা ১ × ৩ + ২ × ১ = ৫.
  2. সারি ধরে পড়া. a-কে সারি ধরে পড়লে storage-এর ঘর ০ থেকে ৫ ক্রমে আসে, প্রতিবার ঠিক এক ঘর এগিয়ে। এটিই contiguous-এর অর্থ।
  3. Transpose. t() কিছুই কপি করে না; শুধু shape (৩, ২) আর stride উল্টে (১, ৩). a.T[২, ১] = ২ × ১ + ১ × ৩ = ৫, যা a[১, ২]-এর একই ঘর।
  4. লাফ. a.T-কে সারি ধরে পড়লে ঠিকানা ০, ৩, ১, ৪, ২, ৫: একবার তিন ঘর সামনে, একবার দুই ঘর পেছনে। শেষ অক্ষের stride ১ নয়, তাই tensor-টি contiguous নয়।
  5. view-এর সীমা. view(6) একটিমাত্র stride দিয়ে ছয়টি ঘর পড়তে চায়। কোনো একক stride দিয়ে ০, ৩, ১, ৪, ২, ৫ ক্রম পাওয়া যায় না, তাই কপি ছাড়া উপায় নেই, আর view কপি করে না: RuntimeError.
  6. সমাধান. contiguous() নতুন storage-এ সংখ্যাগুলো পড়ার ক্রমে সাজায়, stride (২, ১). এখন view চলে। reshape এই দুই ধাপ নিজেই করে, দরকার হলে।

৪ · view বনাম reshape, আর shape বদলানোর হাতিয়ার

এবার PyTorch-এ একই জিনিস, সঙ্গে ছবির একটি batch-এর পরিচিত permute. ছবি সাধারণত (H, W, C) ক্রমে পড়া হয়, আর PyTorch-এর convolution চায় (N, C, H, W) (channels-last বনাম channels-first রীতি)। Indexing আর slicing-এর নিয়ম NumPy-র মতোই (AI-এর ভিত্তি কোর্সের পর্ব ১৪); slicing-ও view দেয়, যা script-এর শেষ লাইনে দেখা যাবে।

import torch

x = torch.arange(6)                               # 0..5, int64
m = x.view(2, 3)
print(m, "| stride", m.stride(), "| contiguous", m.is_contiguous())
print("same storage:", m.data_ptr() == x.data_ptr())
m[0, 0] = 100                                     # view-তে লিখলে মূলও বদলায়
print("x after m[0,0]=100:", x)

t = m.t()                                         # transpose: কপি নয়
print("t shape", tuple(t.shape), "| stride", t.stride(), "| contiguous", t.is_contiguous())
try:
    t.view(6)
except RuntimeError as e:
    print("RuntimeError:", str(e)[:120], "…")
r = t.reshape(6)                                  # দরকার হলে কপি করে
print("reshape:", r, "| shares memory:", r.data_ptr() == x.data_ptr())
c = t.contiguous()
print("contiguous():", tuple(c.stride()), c.is_contiguous(), c.view(6))

img = torch.zeros(8, 32, 32, 3)                   # batch, H, W, C (NumPy/PIL ধাঁচ)
p = img.permute(0, 3, 1, 2)                       # PyTorch-এর N, C, H, W
print("permute:", tuple(p.shape), "| stride", p.stride(), "| contiguous", p.is_contiguous())
try:
    p.view(8, -1)
except RuntimeError:
    print("p.view(8, -1) -> RuntimeError")
print("p.reshape(8, -1):", tuple(p.reshape(8, -1).shape), "| flatten(1):", tuple(p.flatten(1).shape))

v = torch.tensor([1.0, 2.0, 3.0])
print(tuple(v.unsqueeze(0).shape), tuple(v.unsqueeze(1).shape), tuple(v[None, :, None].shape))
print(tuple(torch.zeros(1, 3, 1).squeeze().shape), tuple(torch.zeros(1, 3, 1).squeeze(0).shape))
s = v[::2]                                        # slicing-ও view
print("v[::2] stride", s.stride(), "| contiguous", s.is_contiguous(), "| clone shares:",
      s.clone().data_ptr() == v.data_ptr())

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

~/bazarghar-dl$ uv run python t3_views.py
tensor([[0, 1, 2],
        [3, 4, 5]]) | stride (3, 1) | contiguous True
same storage: True
x after m[0,0]=100: tensor([100,   1,   2,   3,   4,   5])
t shape (3, 2) | stride (1, 3) | contiguous False
RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subs …
reshape: tensor([100,   3,   1,   4,   2,   5]) | shares memory: False
contiguous(): (2, 1) True tensor([100,   3,   1,   4,   2,   5])
permute: (8, 3, 32, 32) | stride (3072, 1, 96, 3) | contiguous False
p.view(8, -1) -> RuntimeError
p.reshape(8, -1): (8, 3072) | flatten(1): (8, 3072)
(1, 3) (3, 1) (1, 3, 1)
(3,) (3, 1)
v[::2] stride (2,) | contiguous False | clone shares: False

পুরো error বার্তার শেষ অংশটি হলো “Use .reshape(...) instead.”, অর্থাৎ PyTorch নিজেই সমাধান বলে দেয়। তবু চোখ বুজে reshape বসানোর আগে বুঝুন কী ঘটছে: view-তে লিখলে মূল tensor বদলায় (m[০, ০] = ১০০ লেখার পর x-এর প্রথম ঘরও ১০০), কিন্তু reshape কপি করলে সেই সম্পর্ক ভেঙে যায় (shares memory: False এসেছে)। অর্থাৎ reshape-এর ফলে লিখলে মূল tensor কখনো বদলায়, কখনো বদলায় না, কোনটি হবে তা stride-এর ওপর নির্ভর করে। এই অনিশ্চয়তার কারণেই PyTorch-এর docs বলে, reshape-এর ফল view না কপি তার ওপর নির্ভর করবেন না; নিশ্চিত কপি চাইলে clone(), নিশ্চিত view চাইলে view().

কাজউদাহরণ (shape)কপি করে?
view(*shape)(6,) → (2, 3)কখনো না; না পারলে error
reshape(*shape)(3, 2) non-contiguous → (6,)দরকার হলে
t(), transpose(d0, d1)(2, 3) → (3, 2)না, stride অদলবদল
permute(0, 3, 1, 2)(8, 32, 32, 3) → (8, 3, 32, 32)না, stride সাজানো
unsqueeze(d), x[None](3,) → (1, 3) বা (3, 1)না, মাপ ১-এর অক্ষ যোগ
squeeze(d)(1, 3, 1) → (3,) বা d=0-তে (3, 1)না, মাপ ১-এর অক্ষ বাদ
flatten(start_dim)(8, 3, 32, 32) → (8, 3072)reshape-এর মতো
contiguous(), clone()shape একইcontiguous: দরকার হলে; clone: সবসময়

squeeze()-এর নিঃশব্দ বিপদ। আর্গুমেন্ট ছাড়া squeeze() মাপ ১-এর সব অক্ষ বাদ দেয়। Training-এ batch (৩২, ১) থেকে (৩২,) পাওয়ার জন্য লেখা squeeze() শেষ batch-এ একটিমাত্র উদাহরণ থাকলে (১, ১) থেকে একটি scalar () বানিয়ে ফেলে, আর পরের কোড অদ্ভুতভাবে ভাঙে। সবসময় অক্ষ বলে দিন: squeeze(-1).

৫ · Broadcasting — shape মেলানোর নিয়ম, ধাপে ধাপে

Broadcasting-এর ধারণা AI-এর ভিত্তি কোর্সের পর্ব ১৫-এ এসেছে, আর পর্ব ০৫-এ দেখেছেন এর backward-এ কেন sum লাগে। PyTorch হুবহু NumPy-র নিয়ম মানে। নিয়ম তিনটি:

Broadcasting-এর নিয়ম

১. দুটি shape ডান দিক থেকে মিলিয়ে লিখুন; ছোটটির বাঁ দিকে মাপ ১-এর অক্ষ বসান, যতক্ষণ দৈর্ঘ্য সমান না হয়। ২. প্রতিটি অক্ষে দুটি মাপ সমান হলে চলে, একটি ১ হলে সেটি অন্যটির মাপে “প্রসারিত” ধরা হয়, না হলে error. ৩. ফলের প্রতিটি অক্ষের মাপ দুটির বড়টি। প্রসারণে কোনো কপি হয় না: প্রসারিত অক্ষের stride ০, অর্থাৎ একই ঘর বারবার পড়া হয়।

A = arange(12).view(3, 1, 4) · B = arange(8).view(2, 4) × 100
A-এর shape
B-এর shape
এই ধাপে
ফল
  1. ডান দিক থেকে. দুটি shape-এর শেষ অক্ষ একই কলামে বসান। A-এর তিনটি অক্ষ, B-এর দুটি।
  2. দৈর্ঘ্য সমান. B-এর বাঁ দিকে মাপ ১-এর একটি অক্ষ বসে (২, ৪) থেকে (১, ২, ৪) হলো। এটি শুধু হিসাবের সুবিধা; B-এর ডেটা বদলায়নি।
  3. শেষ অক্ষ. ৪ আর ৪ সমান, তাই ফলের শেষ অক্ষ ৪. প্রতিটি পণ্য-কলাম নিজের সঙ্গেই মিলবে।
  4. মাঝের অক্ষ. A-তে ১, B-তে ২. A প্রসারিত হয়: A-এর একমাত্র সারিটি দুবার পড়া হয়, তাই ঐ অক্ষে A-এর stride ০.
  5. প্রথম অক্ষ. A-তে ৩, B-তে ১ (বসানো অক্ষ). এবার B প্রসারিত হয়ে তিনবার পড়া হয়। ফলের shape (৩, ২, ৪), মোট ২৪টি ঘর, অথচ A-তে ১২টি আর B-তে ৮টি সংখ্যা।
  6. একটি ঘর যাচাই. out[২, ১, ৩]-এ A-এর প্রসারিত অক্ষের index ০ হয়ে যায়: A[২, ০, ৩] = ১১, B[১, ৩] = ৭০০, যোগফল ৭১১. সত্যিকারের run-এ expand-এর stride ঠিক এই: A-এর (৪, ০, ১), B-এর (০, ৪, ১).

নিয়মটি হাতে লিখে NumPy-র সঙ্গে মেলানো যাক। নিচের function আটটি জোড়ার ফলের shape বা error আগে অনুমান করে, তারপর NumPy-তে সত্যিই যোগ করে মেলায়। জোড়াগুলো deep learning-এর পরিচিত দৃশ্য: token-এর feature-এ bias, ছবির channel-এ গড় বিয়োগ, সারির গড় বিয়োগ।

import numpy as np

def predict(s1, s2):                     # broadcasting-এর নিয়ম, হাতে
    n = max(len(s1), len(s2))
    a = (1,) * (n - len(s1)) + tuple(s1)  # ছোটটির বাঁ দিকে 1 বসানো
    b = (1,) * (n - len(s2)) + tuple(s2)
    out = []
    for i, (x, y) in enumerate(zip(a, b)):   # ডান দিক থেকে মিলুক বা না মিলুক
        if x == y or y == 1:
            out.append(x)
        elif x == 1:
            out.append(y)
        else:
            return f"error: dim {i - n} ({x} vs {y})"
    return tuple(out)

pairs = [((3, 1, 4), (2, 4)), ((32, 10, 16), (16,)), ((4, 5), (4,)),
         ((4, 1), (5,)), ((8, 3, 32, 32), (3, 1, 1)), ((8, 3, 32, 32), (3,)),
         ((2, 1, 5), (7, 1)), ((64, 16), (64, 1))]
for s1, s2 in pairs:
    guess = predict(s1, s2)
    try:
        real = (np.zeros(s1) + np.zeros(s2)).shape
    except ValueError:
        real = "error"
    same = guess == real or (str(guess).startswith("error") and real == "error")
    print(f"{s1} + {s2} → {guess}  {'✓' if same else '✗ NumPy: ' + str(real)}")

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

(3, 1, 4) + (2, 4) → (3, 2, 4)  ✓
(32, 10, 16) + (16,) → (32, 10, 16)  ✓
(4, 5) + (4,) → error: dim -1 (5 vs 4)  ✓
(4, 1) + (5,) → (4, 5)  ✓
(8, 3, 32, 32) + (3, 1, 1) → (8, 3, 32, 32)  ✓
(8, 3, 32, 32) + (3,) → error: dim -1 (32 vs 3)  ✓
(2, 1, 5) + (7, 1) → (2, 7, 5)  ✓
(64, 16) + (64, 1) → (64, 16)  ✓

আটটিতেই অনুমান আর NumPy এক। তিনটি জোড়া মনে রাখার মতো। (৪, ৫) + (৪,): চারটি দোকানের প্রতিটির গড় (৪,) সরাসরি বিয়োগ করতে গেলে শেষ অক্ষে ৫ আর ৪ মেলে না। (৮, ৩, ৩২, ৩২) + (৩,): তিনটি channel-এর গড় বিয়োগ করতে (৩,) দিলে তা শেষ অক্ষে, অর্থাৎ প্রস্থের সঙ্গে মেলানোর চেষ্টা হয়; সঠিক রূপ (৩, ১, ১), পঞ্চম লাইন। (৪, ১) + (৫,): একটি কলাম আর একটি সারি মিলে পুরো (৪, ৫) টেবিল, যা “সব জোড়ার” হিসাবের সহজ কৌশল, কিন্তু ভুল করে হলে memory হঠাৎ বিশাল।

dim, keepdim আর নিঃশব্দ ভুল

Reduction (sum, mean, max) কোন অক্ষ ধরে হবে তা বলে dim; সেই অক্ষ ফল থেকে বাদ যায়, যদি না keepdim=True দেন (NumPy-তে নাম axis আর keepdims; পর্ব ১৬, AI-এর ভিত্তি)। বাজারঘরের চারটি দোকানের পাঁচটি পণ্যের দাম থেকে প্রতিটি দোকানের গড় বিয়োগ করার সত্যিকারের run (script-এর প্রথম অংশ; শেষাংশ অংশ ৬-এ):

import torch
g = torch.Generator().manual_seed(7)

a = torch.ones(3, 1, 4); b = torch.arange(8.0).view(2, 4)
print("(3,1,4) + (2,4) ->", tuple((a + b).shape))
print(torch.broadcast_shapes((3, 1, 4), (2, 4)), torch.broadcast_shapes((5, 1), (1, 6)))
try:
    torch.ones(3, 4) + torch.ones(3)
except RuntimeError as e:
    print("RuntimeError:", e)

# বাজারঘর: ৪টি দোকান, প্রতিটিতে ৫টি পণ্যের দাম (টাকা)
price = torch.randint(50, 500, (4, 5), generator=g).float()
mu = price.mean(dim=1, keepdim=True)              # (4, 1)
print("mean keepdim:", tuple(mu.shape), "| without keepdim:", tuple(price.mean(dim=1).shape))
print("centred row sums ~0:", (price - mu).sum(dim=1).abs().max().item() < 1e-3)
try:
    price - price.mean(dim=1)
except RuntimeError as e:
    print("RuntimeError:", e)
sq = torch.rand(4, 4, generator=g)
silent = sq - sq.mean(dim=1)                      # (4,4)-(4,) চলে, কিন্তু ভুল
right = sq - sq.mean(dim=1, keepdim=True)
print("square case runs, row sums:", silent.sum(dim=1).abs().max().item() > 1e-3,
      "| keepdim row sums ~0:", right.sum(dim=1).abs().max().item() < 1e-5)
~/bazarghar-dl$ uv run python t4_broadcast.py
(3,1,4) + (2,4) -> (3, 2, 4)
torch.Size([3, 2, 4]) torch.Size([5, 6])
RuntimeError: The size of tensor a (4) must match the size of tensor b (3) at non-singleton dimension 1
mean keepdim: (4, 1) | without keepdim: (4,)
centred row sums ~0: True
RuntimeError: The size of tensor a (5) must match the size of tensor b (4) at non-singleton dimension 1
square case runs, row sums: True | keepdim row sums ~0: True
…

(৪, ৫) দামের টেবিলে price - price.mean(dim=1) ভাগ্যক্রমে error দিয়েছে। কিন্তু টেবিলটি বর্গাকার (৪, ৪) হলে একই কোড চলে যায়: (৪,) গড়টি শেষ অক্ষে বসে প্রতিটি কলাম থেকে ভুল দোকানের গড় বিয়োগ হয়, আর সারির যোগফল আর শূন্য থাকে না (“square case runs, row sums: True” মানে যোগফল শূন্য নয়)। keepdim=True দিলে গড় (৪, ১) থাকে, প্রতিটি সারি নিজের গড় পায়। নিয়মটি মনে রাখুন: reduction-এর ফল আবার মূল tensor-এর সঙ্গে মেলাবেন, তাহলে keepdim দিন। torch.broadcast_shapes কোনো ডেটা ছাড়াই ফলের shape বলে দেয়, যা assert লেখার জন্য সুবিধাজনক।

৬ · matmul, batched matmul আর einsum

Matrix গুণের নিয়ম মেশিন লার্নিং কোর্সের পর্ব ৭ আর AI-এর ভিত্তি কোর্সের পর্ব ১৭-এ আছে: (n, k) @ (k, m) = (n, m), ভেতরের দুটি মাপ সমান হতে হবে। Deep learning-এ প্রায় সবসময় আরও অক্ষ থাকে। @ বা torch.matmul-এর নিয়ম: শেষ দুটি অক্ষে matrix গুণ, আগের সব অক্ষ “batch” অক্ষ, আর সেগুলোতে broadcasting. একটি vector (এক অক্ষ) হলে তাকে সাময়িকভাবে matrix ধরা হয়, পরে সেই অক্ষ বাদ যায়।

বামডানফলকোথায় দেখা যায়
(n, k)(k, m)(n, m)একটি batch-এ linear স্তর
(B, T, F)(F, O)(B, T, O)প্রতিটি token-এ একই weight
(B, T, F)(B, F, O)(B, T, O)batched matmul, torch.bmm
(B, H, T, d)(B, H, d, T)(B, H, T, T)attention-এর score (পর্ব ৩৩)
(2, 1, 3, 4)(5, 4, 6)(2, 5, 3, 6)batch অক্ষে broadcasting
(n, k)(k,)(n,)matrix-vector

einsum একই হিসাব অক্ষের নাম দিয়ে লেখে। "btf,fo->bto" পড়ুন এভাবে: প্রথম tensor-এর অক্ষ b, t, f, দ্বিতীয়টির f, o; তীরের ডানে যে অক্ষ নেই (এখানে f), তার ওপর গুণ করে যোগ। নিয়ম দুটি: দুই input-এ একই অক্ষর মানে সেই অক্ষে মিলিয়ে গুণ; output-এ না থাকা অক্ষর মানে সেই অক্ষ ধরে যোগ। নিচের সত্যিকারের run ডেস্কটপে matmul আর einsum একে অপরের সঙ্গে মেলায় (একই t4_broadcast.py-এর শেষ অংশ):

X = torch.randn(32, 10, 16, generator=g)          # batch, token, feature
W = torch.randn(16, 8, generator=g)
B = torch.randn(32, 16, 8, generator=g)
print("X @ W:", tuple((X @ W).shape), "| bmm:", tuple(torch.bmm(X, B).shape))
e1 = torch.einsum("btf,fo->bto", X, W)
e2 = torch.einsum("btf,bfo->bto", X, B)
print("einsum == matmul:", torch.allclose(e1, X @ W, atol=1e-5), torch.allclose(e2, X @ B, atol=1e-5))
att = torch.einsum("bqf,bkf->bqk", X, X)          # প্রতিটি token জোড়ার dot product
print("scores:", tuple(att.shape), torch.allclose(att, X @ X.transpose(1, 2), atol=1e-4))
try:
    X @ torch.randn(10, 8)
except RuntimeError as e:
    print("RuntimeError:", e)
…
X @ W: (32, 10, 8) | bmm: (32, 10, 8)
einsum == matmul: True True
scores: (32, 10, 10) True
RuntimeError: mm: mat1 and mat2 shapes cannot be multiplied (320x16 and 10x8)

তিনটি allclose-ই True: einsum আর matmul একই সংখ্যা দিয়েছে, float32-এর rounding-এর সীমার ভেতরে; তাই তুলনায় == নয়, allclose ব্যবহার করা হয়েছে। "bqf,bkf->bqk" প্রতিটি batch-এ প্রতিটি token জোড়ার dot product, যা পর্ব ৩২ (Attention — query, key, value আর alignment)-এর score-এর হুবহু আকার; transpose-এর বদলে শুধু অক্ষরের ক্রম বদলেছে। শেষ লাইনের error বার্তায় 320 সংখ্যাটি কোথা থেকে এলো, তা অংশ ৮-এর stepper-এ পড়া হবে। পাতায় একই তুলনা NumPy দিয়ে, বাজারঘরের বিক্রির হিসাবে:

import numpy as np
rng = np.random.default_rng(42)

# বাজারঘর: 3টি দোকান × 5 দিন × 4টি পণ্যের বিক্রির সংখ্যা, আর পণ্যের দাম (টাকা)
qty = rng.integers(0, 10, size=(3, 5, 4)).astype(np.float64)
price = np.array([120.0, 45.0, 300.0, 80.0])
r1 = qty @ price                                  # (3,5,4) @ (4,) → (3,5)
r2 = np.einsum("sdp,p->sd", qty, price)           # একই কাজ, নাম দিয়ে লেখা
print("revenue shape:", r1.shape, "| equal:", np.allclose(r1, r2))
print("shop 0, day 0:", qty[0, 0], "·", price, "=", r1[0, 0])
print("per shop total:", np.einsum("sdp,p->s", qty, price))   # d-ও যোগ হলো

X = rng.normal(size=(2, 3, 4))                    # batch, token, feature
W = rng.normal(size=(4, 5))
Wb = rng.normal(size=(2, 4, 5))                   # প্রতিটি batch-এর নিজের matrix
print("X @ W     :", (X @ W).shape, np.allclose(X @ W, np.einsum("btf,fo->bto", X, W)))
print("batched   :", (X @ Wb).shape, np.allclose(X @ Wb, np.einsum("btf,bfo->bto", X, Wb)))
S = np.einsum("bqf,bkf->bqk", X, X)               # token জোড়ার dot product
print("scores    :", S.shape, np.allclose(S, X @ X.transpose(0, 2, 1)))
print("diag = squared norms:", np.allclose(np.einsum("bqq->bq", S), (X ** 2).sum(-1)))
try:
    X @ rng.normal(size=(3, 5))                   # ভেতরের মাপ 4 ≠ 3
except ValueError as e:
    print("ValueError:", e)

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

revenue shape: (3, 5) | equal: True
shop 0, day 0: [0. 7. 6. 4.] · [120.  45. 300.  80.] = 2435.0
per shop total: [13395. 11870. 13200.]
X @ W     : (2, 3, 5) True
batched   : (2, 3, 5) True
scores    : (2, 3, 3) True
diag = squared norms: True
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 3 is different from 4)

প্রথম দোকানের প্রথম দিনের আয় হাতে মিলিয়ে নিন: ০ × ১২০ + ৭ × ৪৫ + ৬ × ৩০০ + ৪ × ৮০ = ০ + ৩১৫ + ১৮০০ + ৩২০ = ২৪৩৫ টাকা। "sdp,p->s"-এ output-এ d আর p দুটিই নেই, তাই দিন আর পণ্য দুই অক্ষ ধরেই যোগ হয়ে প্রতিটি দোকানের পাঁচ দিনের মোট আয়। "bqq->bq" একই অক্ষর দুবার লিখে কর্ণ (diagonal) তুলে আনে: প্রতিটি token-এর নিজের সঙ্গে dot product, অর্থাৎ বর্গ-norm. NumPy-র error বার্তা ভিন্ন ভাষায় একই কথা বলে: “core dimension”-এ ৩ আর ৪ মেলেনি।

কখন কোনটি? সাধারণ matrix গুণে @ পড়তে সহজ আর library-র দ্রুততম পথে যায়। অক্ষ বেশি হলে, অথবা transpose আর sum একসঙ্গে লাগলে einsum ভুলের সুযোগ কমায়, কারণ প্রতিটি অক্ষের নাম চোখের সামনে থাকে। PyTorch-এর einsum ভেতরে সাধারণত matmul-এর মতো কাজেই রূপান্তরিত হয়, তবে খুব জটিল সূত্রে গতি সংস্করণভেদে বদলাতে পারে; গতি গুরুত্বপূর্ণ হলে নিজের যন্ত্রে মেপে দেখুন।

৭ · NumPy-র সঙ্গে আদান-প্রদান আর in-place কাজের ফাঁদ

বাস্তব প্রকল্পে ডেটা প্রায়ই pandas বা NumPy থেকে আসে, আর ফল আবার NumPy-তে ফিরে যায়। এখানে তিনটি নিয়ম মনে রাখার মতো, আর একটি ফাঁদ যা পর্ব ০৫-এর autograd-এর সঙ্গে সরাসরি জড়িত: নামের শেষে underscore থাকা কাজ (add_, mul_, zero_) in-place, অর্থাৎ নতুন tensor না বানিয়ে পুরোনোটাই বদলে দেয়।

import numpy as np, torch

arr = np.array([10.0, 20.0, 30.0])                # float64
t = torch.from_numpy(arr)                          # একই memory
arr[0] = -1.0
print("from_numpy sees change:", t, "| dtype", t.dtype)
c = torch.tensor(arr)                              # সবসময় কপি
arr[1] = -2.0
print("torch.tensor copy:", c)
back = t.numpy(); back[2] = 99.0
print("numpy() shares too:", arr)
print("item():", t.sum().item(), type(t.sum().item()).__name__)

w = torch.tensor([1.0, 2.0], requires_grad=True)
try:
    w.numpy()
except RuntimeError as e:
    print("RuntimeError:", e)
print("detach().numpy():", w.detach().numpy())

x = torch.tensor([1.0, 2.0, 3.0])
x.add_(1); x.mul_(2)                               # trailing underscore = in-place
print("in-place:", x)
w = torch.tensor([0.5, -1.0, 2.0], requires_grad=True)
try:
    w.add_(1)
except RuntimeError as e:
    print("RuntimeError:", e)
w = torch.tensor([0.5, -1.0, 2.0], requires_grad=True)
y = torch.sigmoid(w)                               # backward-এ y-এর মান লাগে
y.mul_(2)                                          # y বদলে গেল
try:
    y.sum().backward()
except RuntimeError as e:
    print("RuntimeError:", str(e)[:150], "…")
w = torch.tensor([0.5, -1.0, 2.0], requires_grad=True)
y = torch.sigmoid(w) * 2                           # out-of-place: নতুন tensor
y.sum().backward()
print("grad ok:", w.grad)
with torch.no_grad():
    w -= 0.1 * w.grad                              # parameter আপডেট: no_grad-এর ভেতরে
print("updated w:", w)

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

~/bazarghar-dl$ uv run python t5_interop.py
from_numpy sees change: tensor([-1., 20., 30.], dtype=torch.float64) | dtype torch.float64
torch.tensor copy: tensor([-1., 20., 30.], dtype=torch.float64)
numpy() shares too: [-1. -2. 99.]
item(): 96.0 float
RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
detach().numpy(): [1. 2.]
in-place: tensor([4., 6., 8.])
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation: [torch.FloatTensor [3]], which is output 0 of Sigmoid, …
grad ok: tensor([0.4700, 0.3932, 0.2100])
updated w: tensor([ 0.4530, -1.0393,  1.9790], requires_grad=True)

Memory ভাগ: from_numpy কপি করে না; arr বদলাতেই t বদলেছে। torch.tensor(arr) সবসময় কপি করে, তাই পরের বদল (−২) c-তে নেই। উল্টো দিকে CPU-র tensor-এর .numpy()-ও একই memory দেয়: back[২] = ৯৯ লিখতেই arr বদলেছে। ফলে item() দিয়েছে −১ − ২ + ৯৯ = ৯৬, একটি সাধারণ Python float; loss বা accuracy লগ করার সময় এটিই ব্যবহার করুন, কারণ পুরো tensor রেখে দিলে তার সঙ্গে graph-ও memory-তে থেকে যেতে পারে। GPU-র tensor-এ .numpy() চলে না; আগে .cpu() লাগে।

Autograd আর in-place: gradient রাখা tensor-কে NumPy-তে নিতে আগে detach() লাগে, কারণ NumPy graph চেনে না। দ্বিতীয় error-টি সহজ: যে leaf tensor-এর gradient চাওয়া হয়েছে, তাকে graph-এর বাইরে চুপচাপ বদলানো যায় না। তৃতীয়টি গভীর: sigmoid-এর backward-এ লাগে তার output y, কারণ σ′ = σ(১ − σ). y-কে in-place দ্বিগুণ করায় সেই মান নষ্ট, তাই backward থেমে গেছে। PyTorch এটি ধরে প্রতিটি tensor-এর একটি সংস্করণ-গণনা (version counter) দিয়ে: graph-এ মান রাখার সময়ের সংস্করণ আর backward-এর সময়ের সংস্করণ না মিললে error. সমাধান out-of-place লেখা: y = torch.sigmoid(w) * 2; gradient ২σ(১ − σ), যেমন w = ০.৫-এ ০.৪৭০০. Parameter আপডেট নিজেই in-place, তাই তা torch.no_grad()-এর ভেতরে; পর্ব ০৮ (PyTorch autograd — requires_grad, backward আর gradient-এর ফাঁদ) এই ফাঁদগুলো আরও গভীরে দেখাবে।

৮ · Shape error পড়া আর ডিবাগ করার চেকলিস্ট

Shape error ভয়ের কিছু নয়; বার্তাটিতে সাধারণত উত্তরের অর্ধেক লেখা থাকে। নিচের stepper অংশ ৬-এর সত্যিকারের error ধরে একে একে পড়ে: একটি (৩২, ১০, ১৬) tensor-কে (১০, ৮) weight দিয়ে গুণ করার চেষ্টা।

error বার্তাযা বোঝা গেল

            
  1. পুরো বার্তা পড়ুন. শুধু “RuntimeError” দেখে থামবেন না; বন্ধনীর সংখ্যাগুলোই আসল তথ্য।
  2. কাজের নাম. mm মানে দুটি ২-মাত্রার matrix-এর গুণ। Batched matmul-এ PyTorch অনেক সময় সামনের অক্ষগুলো একসঙ্গে চেপে একটি বড় ২-মাত্রার গুণ চালায়, তাই বার্তায় ৩ অক্ষের বদলে ২ অক্ষ দেখা যায়।
  3. অচেনা সংখ্যা ভাঙুন. ৩২০ কোথা থেকে? ৩২ × ১০ = ৩২০: batch আর token অক্ষ চেপে এক হয়েছে। শেষ মাপ ১৬ হলো feature. অর্থাৎ mat1 আমাদের X.
  4. শর্ত মেলান. mat2 = (১০, ৮), অর্থাৎ W. matrix গুণে mat1-এর শেষ মাপ আর mat2-এর প্রথম মাপ সমান হতে হয়: ১৬ আর ১০ মেলেনি।
  5. দোষী খুঁজুন. X-এর feature ১৬ ডেটা থেকে এসেছে, সেটি ঠিক; W-এর প্রথম মাপ ১৬ হওয়া উচিত। বাস্তবে এমন ভুল প্রায়ই nn.Linear-এর in_features ভুল লেখা থেকে আসে।
  6. যাচাই রাখুন. ভুলের লাইনের ঠিক আগে shape ছাপুন বা assert লিখুন। ঠিক করার পর (৩২, ১০, ১৬) @ (১৬, ৮) = (৩২, ১০, ৮).

PyTorch-এর অন্য পরিচিত বার্তাগুলোও একইভাবে পড়া যায়। “The size of tensor a (4) must match the size of tensor b (3) at non-singleton dimension 1” মানে broadcasting-এ অক্ষ ১-এ ৪ আর ৩ মেলেনি, আর কোনোটিই ১ নয় (singleton মানে মাপ ১)। “Expected all tensors to be on the same device” মানে device-এর অমিল, আর “expected … Float but found Double” বা “must have the same dtype” dtype-এর অমিল।

Shape ডিবাগ করার চেকলিস্ট
  1. Error-এর লাইনের ঠিক আগে প্রতিটি tensor-এর shape, dtype আর device ছাপুন।
  2. প্রতিটি অক্ষের একটি নাম লিখুন (B, T, F বা N, C, H, W) আর কোডের মন্তব্যে রাখুন।
  3. Broadcasting-এর ফল আগে খাতায় অনুমান করুন; সন্দেহ হলে torch.broadcast_shapes.
  4. Reduction-এর পর মূল tensor-এর সঙ্গে মেলালে keepdim=True; squeeze-এ অক্ষ বলে দিন।
  5. permute বা transpose-এর পর view error এলে কারণ বুঝে reshape বা contiguous().
  6. বর্গাকার ডেটায় পরীক্ষা করবেন না: batch, feature আর hidden-এর মাপ আলাদা রাখুন, যাতে ভুল অক্ষ error দেয়।
  7. গুরুত্বপূর্ণ জায়গায় assert x.shape == (B, T, F) রেখে দিন; এটি কোডের দলিলও।

আরও গভীরে: PyTorch-এর tensor ভেতরে একটি Storage (বাইটের টানা খণ্ড) আর তার ওপর একটি দৃষ্টি (shape, stride, storage offset, dtype, device) হিসেবে গঠিত; এই নকশা নিয়ে PyTorch-এর মূল লেখক দলের একজন, Edward Z. Yang-এর “PyTorch internals” (২০১৯) লেখাটি পরিচিত সূচনা। Broadcasting-এর নিয়ম এখন বহু library-তে একই, আর Python Array API Standard (Consortium for Python Data API Standards, ২০২০ সাল থেকে) এই নিয়মগুলো লিখিত রূপ দেওয়ার চেষ্টা করছে। Memory layout বদলানোর জন্য PyTorch-এ torch.channels_last নামে একটি memory format আছে, যা shape একই রেখে stride বদলায়; কিছু GPU-তে convolution এতে দ্রুত চলে বলে PyTorch-এর tutorial-এ বলা আছে। Shape ভুল কমাতে einops (Alex Rogozhnikov) আর jaxtyping-এর মতো library অক্ষের নাম দিয়ে লেখা আর যাচাইয়ের সুযোগ দেয়।

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

  • (৮, ১, ৬, ১) + (৭, ১, ৫)-এর ফল? ডান দিক থেকে মিলিয়ে (৮, ৭, ৬, ৫).
  • view আর reshape-এর পার্থক্য? view কখনো কপি করে না, non-contiguous tensor-এ error দেয়; reshape দরকার হলে কপি করে।
  • (২, ৩) contiguous tensor-এর transpose-এর stride? (১, ৩), contiguous নয়।
  • (B, H, T, d) @ (B, H, d, T)-এর shape? (B, H, T, T), attention score.
  • float16 আর bfloat16-এর মূল পার্থক্য? দুটিই ২ বাইট; float16-এর নির্ভুলতা বেশি কিন্তু সর্বোচ্চ মান প্রায় ৬৫,৫০৪, bfloat16-এর পরিসর float32-এর মতো কিন্তু নির্ভুলতা কম।

৯ · AI-এর যুগে — AI-এর লেখা tensor কোড যাচাই

AI assistant tensor-এর কোড খুব দ্রুত লেখে, কিন্তু এই পর্বের ভুলগুলো তার জন্যও সবচেয়ে সাধারণ: ভুল dim, ভুলে যাওয়া keepdim, permute-এর পর view, dtype-এর অমিল। এদের অনেকগুলো error দেয় না, শুধু ভুল সংখ্যা দেয়। AI-এর লেখা কোড পড়ার সময় নিচের ধাঁচগুলো খুঁজুন।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
x - x.mean(1), keepdim ছাড়াবর্গাকার ডেটায় চুপচাপ ভুল অক্ষে বিয়োগআলাদা মাপের ছোট tensor-এ চালান; সারির যোগফল ০ কি না দেখুন
permute-এর পর view, বা error চাপা দিতে সব জায়গায় reshapeerror, অথবা অক্ষের অর্থ গুলিয়ে যাওয়াpermute-এর আগে-পরে shape ও একটি ঘরের মান মিলিয়ে দেখুন
reshape(B, C, H, W) দিয়ে (B, H, W, C) থেকে ক্রম বদলানোreshape অক্ষ সাজায় না, শুধু নতুন মাপে কাটে; ছবি এলোমেলোক্রম বদলাতে permute; একটি pixel-এর মান দুই জায়গায় তুলনা করুন
NumPy-র float64 সরাসরি model-এ দেয়dtype-এর অমিলে error, বা অকারণে float64-এ ধীরmodel-এ ঢোকার আগে .float() আর dtype ছাপুন
পুরোনো বা বানানো API দেয়, যেমন Variable দিয়ে মোড়ানোপুরোনো সংস্করণের রীতি, বা আদৌ নেই এমন functionনিজের PyTorch সংস্করণের docs-এ নাম খুঁজুন
gradient-ওয়ালা tensor-এ .numpy(), বা ভুল জায়গায় in-placeRuntimeError, বা backward-এর মান নষ্টলগ করতে .item() বা detach(); আপডেট no_grad-এর ভেতরে

AI-কে শুধু কোড নয়, প্রতিটি লাইনের shape-সহ কোড চাইতে বলুন:

আমি PyTorch-এ (B, H, W, C) আকারের ছবির batch থেকে প্রতিটি channel-এর গড় বিয়োগ করে (B, C, H, W)-তে নিতে চাই। ১. প্রতিটি লাইনের পাশে মন্তব্যে ফলের shape লিখে কোড দাও। ২. কোথায় permute আর কোথায় reshape দরকার, আর কেন, এক লাইনে করে বলো। ৩. গড় বের করার সময় dim আর keepdim কী হবে, আর keepdim না দিলে কী হতো, তা broadcasting-এর নিয়ম দিয়ে দেখাও। ৪. B = 2, H = 3, W = 4, C = 5 এমন একটি ছোট seed-ওয়ালা tensor-এ assert দিয়ে shape আর একটি pixel-এর মান যাচাইয়ের পরীক্ষা লেখো। ৫. তুমি কোন PyTorch সংস্করণ ধরে লিখছ, আর কোন function সংস্করণভেদে বদলাতে পারে, তা বলো।

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

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

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

প্র ০১PyTorch-এর tensor-এর ভাষা এত NumPy-র মতো কেন, আর library-গুলোর এই মিল কোথা থেকে এলো?

Python-এ সংখ্যার array-র ইতিহাস NumPy-র আগে শুরু। ১৯৯০-এর দশকের মাঝামাঝি Numeric নামে একটি library বহুমাত্রিক array আর broadcasting-এর ধারণা আনে; পরে numarray নামে আরেকটি library বড় array-র জন্য আসে। দুই দলের ভাগাভাগি মেটাতে Travis Oliphant ২০০৬ সালের দিকে দুটির ধারণা মিলিয়ে NumPy প্রকাশ করেন। এরপর বৈজ্ঞানিক Python-এর প্রায় সব library, যেমন SciPy, pandas আর scikit-learn, NumPy-র ওপর দাঁড়ায়, আর shape, dtype, axis, broadcasting আর slicing-এর ভাষা লাখো মানুষের অভ্যাসে পরিণত হয়।

PyTorch-এর পূর্বসূরি Torch ছিল Lua ভাষার library; Torch7 ২০১০-এর দশকের শুরুতে গবেষণায় বেশ ব্যবহৃত হতো। ২০১৬–২০১৭ সালের দিকে PyTorch সেই C/CUDA-র ভিত্তি Python-এ নিয়ে আসে, আর API নকশায় স্পষ্টভাবে NumPy-র কাছাকাছি থাকার চেষ্টা করে: নতুন ব্যবহারকারী যেন আগের অভ্যাসেই লিখতে পারেন। তবে কিছু পার্থক্য রয়ে গেছে: PyTorch-এ dim আর keepdim, NumPy-তে axis আর keepdims; PyTorch-এর default float32, NumPy-র float64; PyTorch-এর stride ঘর গোনে, NumPy-র বাইট। এই পার্থক্যগুলোই অনেক বাগের উৎস, কারণ মানুষ এক library-র অভ্যাস অন্যটিতে চাপিয়ে দেন।

Library বাড়তে থাকায় (JAX, CuPy, TensorFlow, Dask) একই কাজের নাম আর আচরণ আলাদা হয়ে যাচ্ছিল। এ সমস্যা মেটাতে ২০২০ সালের দিকে Consortium for Python Data API Standards তৈরি হয়, যারা Array API Standard লিখছে: function-এর নাম, broadcasting আর type promotion-এর নিয়ম, যাতে একই কোড বিভিন্ন library-তে চলে। NumPy 2.0 আর PyTorch-সহ কয়েকটি library ধাপে ধাপে এর সঙ্গে মিল বাড়াচ্ছে; কতটা মিলেছে, তা সংস্করণভেদে আলাদা।

শেখার দিক থেকে এর অর্থ পরিষ্কার: NumPy-তে broadcasting আর stride বুঝলে PyTorch, JAX বা CuPy-তে সেই জ্ঞানের বেশিরভাগ কাজে লাগে, কিন্তু default dtype আর নামের ছোট পার্থক্যগুলো আলাদা করে মনে রাখতে হয়।

মূল উপলব্ধি: Tensor-এর ভাষা দুই দশকের NumPy-অভ্যাসের উত্তরাধিকার; মিলগুলো শেখা সহজ করে, আর অমিলগুলো (dtype, dim-এর নাম, stride-এর একক) সচেতনভাবে মনে রাখতে হয়।

প্র ০২দুই বাইটের দুটি float — float16 আর bfloat16 — কেন দুটিই টিকে আছে, আর কোনটি কখন?

একটি float সংখ্যা তিন ভাগে লেখা হয়: চিহ্নের একটি বিট, মাপের জন্য exponent-এর বিট, আর নির্ভুলতার জন্য mantissa-এর বিট। IEEE 754 মানদণ্ডের binary16, যাকে আমরা float16 বলি, ২০০৮ সালের সংশোধনে মানদণ্ডে আসে: ৫টি exponent বিট আর ১০টি mantissa বিট। ফলে নির্ভুলতা মোটামুটি ভালো (eps প্রায় ০.০০০৯৭৭), কিন্তু সর্বোচ্চ মান প্রায় ৬৫,৫০৪, আর খুব ছোট মান শূন্য হয়ে যায়। এই পর্বের run-এ ৭০,০০০ সরাসরি inf হয়েছে।

bfloat16 (“brain floating point”) Google-এর গবেষকেরা machine learning-এর hardware-এর জন্য জনপ্রিয় করেন, বিশেষ করে TPU-তে, ২০১৮–২০১৯ সালের দিকে। ধারণাটি সরল: float32-এর ৮টি exponent বিট রেখে mantissa ২৩ থেকে ৭ বিটে কেটে দেওয়া। তাই পরিসর float32-এর সমান, কিন্তু নির্ভুলতা মাত্র দুই-তিন দশমিক অঙ্ক; run-এ ১.০০১ হয়ে গেছে ১. যুক্তি ছিল, neural network-এর gradient আর activation-এ পরিসরের সমস্যা (overflow, underflow) নির্ভুলতার সমস্যার চেয়ে বেশি ক্ষতিকর।

float16-এ training-এর জন্য Micikevicius ও সহকর্মীদের ২০১৮ সালের mixed precision প্রবন্ধ একটি কৌশল জনপ্রিয় করে: weight-এর একটি float32 “মূল কপি” রাখা, হিসাব float16-এ, আর loss-কে একটি বড় সংখ্যা দিয়ে গুণ করে (loss scaling) ছোট gradient-কে শূন্য হওয়া থেকে বাঁচানো। bfloat16-এ পরিসর বড় বলে loss scaling সাধারণত লাগে না, তাই কোড সহজ। অন্যদিকে float16 পুরোনো অনেক GPU-তে চলে, আর inference-এ তার বাড়তি নির্ভুলতা কাজে লাগে। কোন hardware কোনটিতে দ্রুত, তা প্রজন্মভেদে আলাদা।

বাস্তবে আজ অনেক বড় model-এর training bfloat16-এ হয় বলে রিপোর্ট করা হয়, আর float16 বেশি দেখা যায় inference আর পুরোনো hardware-এ। দুটিতেই কিছু কাজ (যেমন softmax-এর যোগফল, normalization-এর গড়) float32-এ রাখা হয়, আর PyTorch-এর autocast এই বাছাই নিজে করে, যা পর্ব ১২-এ আসবে।

মূল উপলব্ধি: float16 নির্ভুলতা রাখে কিন্তু পরিসর হারায়, bfloat16 পরিসর রাখে কিন্তু নির্ভুলতা হারায়; কোনটি ভালো তা hardware আর কাজের ধরন ঠিক করে, আর দুটিতেই সংবেদনশীল হিসাব float32-এ রাখতে হয়।

প্র ০৩Broadcasting এত সুবিধাজনক, তবু অনেকে একে নিঃশব্দ বাগের কারখানা বলেন কেন? নিরাপদ রাখার কী উপায় আছে?

Broadcasting-এর সুবিধা স্পষ্ট: bias যোগ, গড় বিয়োগ, সব জোড়ার দূরত্ব, সবই লুপ ছাড়া এক লাইনে, আর memory-তে কোনো কপি ছাড়া (প্রসারিত অক্ষের stride ০)। এর শিকড় array ভাষার পুরোনো ঐতিহ্যে, যেমন APL-এ, আর Python-এ তা Numeric থেকে NumPy হয়ে সবখানে ছড়িয়েছে।

সমস্যা হলো নিয়মটি “ক্ষমাশীল”। দুটি shape মেলানোর যেকোনো বৈধ উপায় থাকলে library ধরে নেয় আপনি সেটিই চেয়েছেন। এই পর্বের বর্গাকার উদাহরণে (৪, ৪) থেকে (৪,) গড় বিয়োগ করা কোড error ছাড়াই চলে, কিন্তু প্রতিটি সারি থেকে নয়, প্রতিটি কলাম থেকে ভুল গড় বিয়োগ হয়। আরেকটি পরিচিত ফাঁদ regression-এ: prediction (N, ১) আর target (N,) থেকে (pred - y) ** 2 লিখলে ফল (N, N), আর তার গড় একটি অর্থহীন সংখ্যা যা দেখতে loss-এর মতোই। PyTorch-এর F.mse_loss এমন অবস্থায় একটি UserWarning দেয় (“This will likely lead to incorrect results due to broadcasting”, নিজের run-এ দেখা), কিন্তু হাতে লেখা হিসাবে কেউ সতর্ক করে না। এসব বাগ training চালায়, loss কমায়ও, শুধু model ভুল শেখে।

প্রতিরোধের কয়েকটি স্তর আছে। প্রথম, পরীক্ষায় সব অক্ষের মাপ আলাদা রাখা, যাতে ভুল অক্ষ error দেয়। দ্বিতীয়, reduction-এ keepdim আর squeeze-এ নির্দিষ্ট অক্ষ। তৃতীয়, গুরুত্বপূর্ণ জায়গায় shape-এর assert. চতুর্থ, অক্ষের নাম দিয়ে লেখা: einops-এর মতো library-তে rearrange(x, "b h w c -> b c h w") লিখলে অক্ষের অর্থ কোডেই থাকে; PyTorch ২০১৯ সালের দিকে “named tensors” নামে একটি পরীক্ষামূলক সুবিধা এনেছিল, কিন্তু লেখার সময়ের PyTorch 2.15.0-তে নিজের run-এ torch.zeros(2, 3, names=...) TypeError দিয়েছে, অর্থাৎ সুবিধাটি এই সংস্করণে আর নেই। jaxtyping-এর মতো টুল function-এর type hint-এ shape লিখে যাচাইয়ের সুযোগ দেয়।

দৈনন্দিন তুলনায়, broadcasting যেন পরীক্ষার হলে একজন খুব বিনয়ী পরীক্ষক, যিনি রোল নম্বর একটু অস্পষ্ট হলেও কাছের কোনো রোলে নম্বর বসিয়ে দেন। খাতা হারায় না, কিন্তু নম্বর ভুল জনের ঘরে যেতে পারে।

মূল উপলব্ধি: Broadcasting-এ error না আসা মানে ঠিক হওয়া নয়; অক্ষের মাপ আলাদা রেখে পরীক্ষা, keepdim আর shape-এর assert-ই নিঃশব্দ ভুলের প্রধান প্রতিরোধ।

প্র ০৪Stride আর contiguity শুধু তত্ত্ব নয় — memory layout গতিকে কীভাবে প্রভাবিত করে, আর channels-last নিয়ে বিতর্ক কী?

CPU বা GPU memory থেকে ডেটা এক-একটি সংখ্যা করে আনে না, টানা খণ্ডে আনে (CPU-তে cache line, GPU-তে coalesced access নামে পরিচিত)। পাশাপাশি থাকা সংখ্যা পরপর পড়লে একবারে অনেকগুলো আসে; লাফিয়ে লাফিয়ে পড়লে প্রতিটি লাফে নতুন খণ্ড আনতে হয়। তাই একই হিসাব contiguous tensor-এ সাধারণত দ্রুত, আর transpose-এর পর বারবার পড়া হলে একবার contiguous() করে নেওয়া লাভজনক হতে পারে। AI-এর ভিত্তি কোর্সের পর্ব ১৯-এ NumPy-তে এমন পার্থক্য মাপা হয়েছে; কত গুণ, তা যন্ত্র আর আকারভেদে আলাদা।

ছবির ক্ষেত্রে প্রশ্নটি আরও আকর্ষণীয়। PyTorch-এর ঐতিহ্যগত ক্রম NCHW: একটি channel-এর পুরো ছবি পাশাপাশি। TensorFlow-এর ঐতিহ্যগত default NHWC: একটি pixel-এর সব channel পাশাপাশি। Convolution প্রতিটি pixel-এ সব channel একসঙ্গে ব্যবহার করে, তাই অনেক আধুনিক GPU-র বিশেষ matrix-গুণের একক NHWC-তে ভালো কাজ করে বলে NVIDIA আর PyTorch-এর documentation-এ উল্লেখ আছে। PyTorch এ জন্য shape না বদলে শুধু stride বদলানোর ব্যবস্থা দিয়েছে: x.to(memory_format=torch.channels_last). shape তখনও (N, C, H, W) দেখায়, কিন্তু stride-এ channel-এর লাফ ১, ঠিক এই পর্বের permute-এর মতো।

বিতর্কটি trade-off-এর। একটি layout সব কাজে সেরা নয়: কিছু কাজ NCHW-তে দ্রুত, কিছু NHWC-তে, আর মাঝে layout বদলানোর খরচ লাভ খেয়ে ফেলতে পারে। Framework-গুলো তাই ভেতরে নিজে সিদ্ধান্ত নেওয়ার চেষ্টা করে, আর torch.compile-এর মতো compiler পুরো graph দেখে layout বেছে নিতে পারে। শেখার জন্য মূল কথা হলো shape একটি যুক্তিগত দৃষ্টি, আর stride আসল memory-র বিন্যাস; দুটি আলাদা জিনিস।

দৈনন্দিন তুলনায়, গুদামে একই পণ্য তাকে দুইভাবে সাজানো যায়: প্রতিটি দোকানের সব পণ্য এক তাকে, বা প্রতিটি পণ্য সব দোকানের জন্য এক তাকে। কোনটি দ্রুত, তা নির্ভর করে কর্মী সাধারণত কোন প্রশ্নের উত্তর খোঁজেন।

মূল উপলব্ধি: Shape বলে ডেটার অর্থ, stride বলে memory-তে বিন্যাস; গতির প্রশ্নে বিন্যাসটিই গুরুত্বপূর্ণ, আর তার সেরা রূপ hardware আর কাজের ওপর নির্ভর করে, তাই মেপে সিদ্ধান্ত নিন।

প্র ০৫In-place কাজ memory বাঁচায়, তবু PyTorch autograd-এ একে সন্দেহের চোখে দেখে, আর JAX একে পুরোপুরি বাদ দেয় — কেন?

In-place কাজের আকর্ষণ সহজ: নতুন tensor না বানিয়ে পুরোনোটাই বদলালে memory কম লাগে, আর বিশাল activation-এ সেটি গুরুত্বপূর্ণ হতে পারে। nn.ReLU(inplace=True) এমন একটি পুরোনো কৌশল। কিন্তু পর্ব ০৫-এ দেখেছেন, reverse mode-এর backward-এ forward-এর অনেক মান লাগে: গুণে অন্য গুণনীয়ক, sigmoid আর tanh-এ output, ReLU-তে চিহ্ন। সেই মান in-place বদলে গেলে backward ভুল সংখ্যা দিয়ে গুণ করবে, আর কেউ টেরও পাবে না।

PyTorch এর সমাধানে প্রতিটি tensor-এ একটি সংস্করণ-গণনা রাখে। প্রতিটি in-place কাজে সংখ্যাটি বাড়ে। Graph যখন backward-এর জন্য কোনো tensor মনে রাখে, তখনকার সংস্করণও লিখে রাখে; backward-এর সময় মিল না পেলে এই পর্বের run-এর মতো error দেয়: “modified by an inplace operation”. এটি একটি নিরাপত্তা-জাল, পূর্ণ সুরক্ষা নয়: .data দিয়ে বদলালে বা কিছু নিম্নস্তরের পথে এই গণনা এড়িয়ে যায়, আর তখন gradient চুপচাপ ভুল হতে পারে। PyTorch-এর docs সাধারণভাবে বলে, autograd-এর সঙ্গে in-place কাজ সমর্থিত হলেও প্রায় কখনো দরকার নয়, কারণ memory-র চাপ না থাকলে লাভ সামান্য।

JAX (Google, ২০১৮ সালের দিকে প্রকাশ্যে) অন্য দর্শন নেয়: array অপরিবর্তনীয় (immutable). x[0] = 1 লেখা যায় না; লিখতে হয় x = x.at[0].set(1), যা নতুন array ফেরত দেয়। এতে function “বিশুদ্ধ” থাকে, আর compiler (XLA) নিজেই নিরাপদ হলে ভেতরে in-place-এ রূপান্তর করে। অর্থাৎ memory বাঁচানোর সিদ্ধান্ত মানুষের হাত থেকে compiler-এর হাতে যায়। PyTorch-এর torch.func-ও এই কার্যকরী (functional) ধারার কিছু সুবিধা আনে।

বাস্তব নিয়ম তাই সরল: model-এর forward-এ in-place এড়িয়ে চলুন, যদি না memory মেপে নিশ্চিত হন যে লাভ আছে; parameter আপডেট আর zero_grad-এর মতো জায়গায় in-place স্বাভাবিক, কিন্তু সেগুলো no_grad-এর ভেতরে বা optimizer-এর মাধ্যমে।

মূল উপলব্ধি: In-place-এর memory-লাভ সত্যি, কিন্তু autograd-এর মনে রাখা মান নষ্ট করার ঝুঁকিও সত্যি; PyTorch সংস্করণ-গণনা দিয়ে পাহারা দেয়, JAX অপরিবর্তনীয় array দিয়ে সমস্যাটিই এড়ায়।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার NumPy ব্লক দিয়ে যাচাই করুন।

  1. পূর্ণ numerical: (ক) (৮, ১, ৬, ১) + (৭, ১, ৫)-এর ফলের shape কী? (খ) একটি contiguous float32 tensor-এর shape (১৬, ৩, ৩২, ৩২). তার stride কত, আর permute(0, 2, 3, 1)-এর পর shape আর stride কী হবে? Contiguous থাকবে? (গ) (১০২৪, ১০২৪) আকারের tensor float32-এ আর bfloat16-এ কত memory নেয়? (ঘ) (১৬, ১২, ৬৪, ৩২) @ (১৬, ১২, ৩২, ৬৪)-এর ফলের shape আর মোট গুণ-যোগ (multiply-add) সংখ্যা কত?

    (ক) ডান দিক থেকে মেলান: (৮, ১, ৬, ১) আর (১, ৭, ১, ৫) → অক্ষগুলো ৮, ৭, ৬, ৫. ফল (৮, ৭, ৬, ৫). (খ) Contiguous stride: শেষ অক্ষে ১, তারপর ৩২, ৩২ × ৩২ = ১০২৪, ৩ × ১০২৪ = ৩০৭২: (৩০৭২, ১০২৪, ৩২, ১). permute অক্ষগুলোকে ০, ২, ৩, ১ ক্রমে সাজায়, stride-ও একই ক্রমে: shape (১৬, ৩২, ৩২, ৩), stride (৩০৭২, ৩২, ১, ১০২৪). শেষ অক্ষের stride ১ নয়, তাই contiguous নয়। (সত্যিকারের run-এ ঠিক এই stride আর False এসেছে।) (গ) ১০২৪ × ১০২৪ = ১০,৪৮,৫৭৬টি ঘর; float32-এ ৪ বাইট করে ৪ MiB, bfloat16-এ ২ বাইট করে ২ MiB. (ঘ) সামনের (১৬, ১২) batch অক্ষ, শেষ দুটিতে (৬৪, ৩২) @ (৩২, ৬৪) = (৬৪, ৬৪): ফল (১৬, ১২, ৬৪, ৬৪). প্রতিটি ফলের ঘরে ৩২টি গুণ-যোগ: ১৬ × ১২ × ৬৪ × ৬৪ × ৩২ = ২,৫১,৬৫,৮২৪ (প্রায় আড়াই কোটি)। এটি attention-এর score হিসাবের আকার: ১৬টি batch, ১২টি head, ৬৪টি token, প্রতি head-এ ৩২ মাত্রা।

  2. সমালোচনা ও নকশা: একটি AI assistant বাজারঘরের পণ্যের ছবির জন্য এই কোড দিয়েছে: x = torch.from_numpy(imgs) (imgs NumPy-র float64, shape (B, H, W, C)), তারপর x = x.reshape(B, C, H, W), তারপর x = x - x.mean(dim=(2, 3)), শেষে x = x.view(B, -1). কোডটি চলে। কোথায় কোথায় সমস্যা, আর আপনি কীভাবে লিখবেন?

    সমস্যা ১, dtype: from_numpy float64 রাখে, model float32 চায়; তার ওপর from_numpy imgs-এর সঙ্গে memory ভাগ করে, তাই পরে imgs বদলালে x-ও বদলায়। সমস্যা ২, reshape দিয়ে ক্রম বদলানো: reshape অক্ষ সাজায় না, শুধু storage-এর ক্রমে নতুন মাপে কাটে, তাই pixel আর channel মিশে যায়; error আসে না কারণ মোট ঘর একই। লাগবে permute(0, 3, 1, 2). সমস্যা ৩, keepdim: mean(dim=(2, 3))-এর shape (B, C); (B, C, H, W) থেকে বিয়োগ করলে broadcasting তাকে শেষ দুই অক্ষ (H, W)-এর সঙ্গে মেলাতে চায়, যা সাধারণত error দেয়, আর H = B, W = C হলে চুপচাপ ভুল। সমস্যা ৪, view: permute ঠিক করলে tensor contiguous থাকে না, তখন view error দেবে; flatten(1) বা reshape. নকশা: x = torch.from_numpy(imgs).float().permute(0, 3, 1, 2); x = x - x.mean(dim=(2, 3), keepdim=True); x = x.flatten(1); প্রতিটি লাইনের পর shape-এর assert, আর B, H, W, C আলাদা মাপের একটি ছোট tensor-এ একটি pixel-এর মান হাতে মিলিয়ে দেখা।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের PyTorch কোড কী ছাপবে।
    x = torch.arange(6).view(2, 3)
    y = x.t()
    z = y.reshape(6)
    x[0, 1] = 50
    print(y[1, 0].item(), z[2].item(), y.is_contiguous())
    50 1 False

    y হলো x-এর transpose, একটি view; তাই y[১, ০] মানে x[০, ১], যা পরে ৫০ করা হয়েছে, আর view সেটি দেখতে পায়: ৫০. y contiguous নয়, তাই reshape(6) একটি নতুন কপি বানিয়েছে x বদলানোর আগেই: z = [০, ৩, ১, ৪, ২, ৫], তাই z[২] = ১, পরের বদল z-এ পৌঁছায়নি। y-এর stride (১, ৩), তাই False. ডেস্কটপের সত্যিকারের run-এ ঠিক 50 1 False এসেছে। শিক্ষা: reshape-এর ফল view না কপি, তার ওপর নির্ভর করে কোড লিখবেন না।

  4. কোড লিখুন: একটি function matmul_shape(a, b) লিখুন, যা দুটি shape (দুই বা তার বেশি অক্ষ) নিয়ে a @ b-এর ফলের shape ফেরত দেয়, বা মেলাতে না পারলে ValueError তোলে। শেষ দুই অক্ষে matrix গুণের নিয়ম, আগের অক্ষে broadcasting. (২, ১, ৩, ৪) আর (৫, ৪, ৬) দিয়ে পরীক্ষা করুন আর NumPy-র সঙ্গে মেলান।
    import numpy as np
    
    def matmul_shape(a, b):
        (n, k1), (k2, m) = a[-2:], b[-2:]
        if k1 != k2:
            raise ValueError(f"inner dims {k1} != {k2}")
        batch = np.broadcast_shapes(a[:-2], b[:-2])   # batch অক্ষে broadcasting
        return tuple(batch) + (n, m)
    
    print(matmul_shape((2, 1, 3, 4), (5, 4, 6)))                # (2, 5, 3, 6)
    print((np.zeros((2, 1, 3, 4)) @ np.zeros((5, 4, 6))).shape)  # (2, 5, 3, 6)

    শেষ দুই অক্ষ (৩, ৪) আর (৪, ৬): ভেতরের ৪ মেলে, ফল (৩, ৬). আগের অক্ষ (২, ১) আর (৫,): broadcasting-এ (২, ৫). মোট (২, ৫, ৩, ৬), NumPy-ও তাই দেয় (ডেস্কটপে যাচাই করা)। batch অক্ষ না মিললে broadcast_shapes নিজেই ValueError তোলে। এক অক্ষের vector-এর ক্ষেত্র (যেমন (n, k) @ (k,)) সামলাতে চাইলে আগে অক্ষ যোগ করে পরে বাদ দিতে হবে; সেটি বাড়তি অনুশীলন হিসেবে রইল।

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

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

ক্রমএকটি shape error পেলে ডিবাগ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Error বার্তার বন্ধনীর সংখ্যাগুলো পুরোটা পড়া
  2. অচেনা সংখ্যাকে অক্ষের গুণফলে ভাঙা (যেমন ৩২০ = ৩২ × ১০)
  3. ভুলের লাইনের আগে প্রতিটি tensor-এর shape, dtype আর device ছাপা
  4. Broadcasting বা matmul-এর নিয়মে কোন অক্ষ মেলেনি তা বের করা
  5. দোষী tensor বা স্তর ঠিক করা (যেমন in_features বা keepdim)
  6. Shape-এর assert যোগ করে আলাদা মাপের ছোট tensor-এ আবার চালানো

PyTorch-এ (3, 1, 4) আকারের একটি tensor-এর সঙ্গে (2, 4) আকারের tensor যোগ করলে ফলের shape কী?

  • (3, 1, 4)
  • Error, কারণ অক্ষের সংখ্যা আলাদা
  • (3, 2, 4)
  • (3, 1, 2, 4)

torch.tensor([1, 2, 3]) আর torch.tensor([1.0, 2.0])-এর dtype যথাক্রমে কী?

  • int32 আর float32
  • int64 আর float32
  • int64 আর float64
  • দুটিই float32

x = torch.arange(6).view(2, 3)-এর পর x.t().view(6) চালালে কী হয়?

  • নতুন কপি তৈরি হয়ে [0, 3, 1, 4, 2, 5] আসে
  • [0, 1, 2, 3, 4, 5] আসে, কারণ storage একই
  • dtype-এর কারণে TypeError
  • RuntimeError, কারণ transpose-টি contiguous নয়

NumPy array arr = np.array([1.0, 2.0]) থেকে t = torch.from_numpy(arr) বানিয়ে তারপর arr[0] = 9 করা হলো। t-এর প্রথম ঘর কত?

  • 9
  • 1
  • RuntimeError
  • NaN

X-এর shape (32, 10, 16) আর W-এর shape (16, 8). X @ W-এর shape কী?

  • (320, 8)
  • (32, 10, 8)
  • (32, 16, 8)
  • Error, কারণ অক্ষের সংখ্যা আলাদা

float16 আর bfloat16 দুটিই ২ বাইটের। কোন বক্তব্যটি সঠিক?

  • float16-এর পরিসর বড়, bfloat16-এর নির্ভুলতা বেশি
  • দুটির পরিসর আর নির্ভুলতা হুবহু এক
  • bfloat16-এর পরিসর float32-এর কাছাকাছি কিন্তু নির্ভুলতা কম; float16 বেশি নির্ভুল কিন্তু সর্বোচ্চ মান প্রায় 65504
  • 70000 সংখ্যাটি float16-এ ঠিকঠাক ধরা যায়

শব্দকোষ

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

মনে রাখুন

  1. তিন লেবেল: shape, dtype, device. পূর্ণসংখ্যা থেকে int64, দশমিক থেকে float32, from_numpy-তে float64 থাকে; seed-এর জন্য নিজের Generator.
  2. Stride: storage এক সারি; (২, ৩)-এর stride (৩, ১), transpose-এর (১, ৩). transpose, permute, slicing view দেয়, কপি নয়।
  3. view বনাম reshape: view কখনো কপি করে না, non-contiguous-এ error; reshape দরকার হলে কপি করে; নিশ্চিত কপির জন্য clone.
  4. Broadcasting: ডান দিক থেকে মেলান, সমান বা ১; reduction-এর ফল আবার মেলালে keepdim=True. matmul শেষ দুই অক্ষে, আগের অক্ষে broadcasting; einsum অক্ষের নাম দিয়ে একই হিসাব।
  5. ফাঁদ: from_numpy আর .numpy() memory ভাগ করে; gradient-ওয়ালা tensor-এ detach; backward-এর দরকারি মান in-place বদলালে error; parameter আপডেট no_grad-এর ভেতরে।

পরের পর্বে: tensor চিনলেন; এবার তার gradient. পর্ব ০৮ (PyTorch autograd — requires_grad, backward আর gradient-এর ফাঁদ) leaf tensor আর grad_fn, scalar নয় এমন output-এ backward, no_grad আর detach কখন, আর gradient-এর পরিচিত ফাঁদগুলো খুঁটিয়ে দেখাবে।

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

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