পাঠ ১১ · ২৫-এর মধ্যে · মডিউল ২

Broadcasting — অ্যারের জাদু

Broadcasting magic
৭ মিনিট পড়া মধ্যম · Intermediate NumPy কোডসহ

এই পাঠে যা শিখবেন

  • Broadcasting কী — এক লাইনে different shape array যোগ
  • Broadcasting rules — ৩টি step-এ shape compatibility check
  • Row vector + matrix, column vector + matrix — ২টি pattern
  • np.newaxis ও reshape — dim যোগ
  • Memory efficiency — virtual stretching, copy নয়
  • AI use case — column normalization, batch + bias, pairwise distance
  • Common pitfalls — silent broadcast bug

১ · Broadcasting কী?

কল্পনা করুন — একটি ৩×৪ matrix-এ প্রতিটি element-এ ১০ যোগ করতে চান। সাধারণভাবে — loop চালাবেন, ১২ বার যোগ। NumPy-তে শুধু লিখবেন arr + 10। NumPy broadcasting দিয়ে ১০-এর scalar-কে (৩, ৪) shape-এ "stretch" করে — কোনো explicit loop ছাড়াই। এটাই broadcasting — ভিন্ন shape-এর array-এ element-wise operation চালানো।

Broadcasting NumPy-এর সবচেয়ে শক্তিশালী feature। এটা ছাড়া vectorizationVectorizationPython loop-এর বদলে NumPy/BLAS-এর native C code ব্যবহার — ১০-১০০× দ্রুত। AI-তে training speed-এর মূল চাবিকাঠি। অসম্পূর্ণ — AI-এর প্রায় সব linear algebra operation এর উপর দাঁড়িয়ে।

ভাবুন একটা শ্রেণিকক্ষে — ৩০ জন ছাত্রের ৫টি subject-এর marks আছে (shape: ৩০×৫)। আপনি প্রতিটি subject-এর গড় বের করলেন (shape: ৫,)। এখন প্রত্যেকের প্রতিটি subject থেকে গড় বাদ দিতে চান — "তুমি গড়ের কত উপরে/নিচে?"। Loop ছাড়া শুধু লিখুন marks - mean। Broadcasting ৩০ বার গড়-row যোগ করার বদলে — চুপচাপ stretch করে দেয়।

২ · Scalar + array — সবচেয়ে সাধারণ কেস

Broadcasting-এর সরলতম রূপ — scalar (একটি সংখ্যা) ও array। Scalar-কে ভাবুন shape (); এটি যেকোনো array-এর shape-এ stretch হয়।

Python · NumPy
import numpy as np

# একটি ৩×৪ matrix
arr = np.array([[1, 2, 3, 4],
                [5, 6, 7, 8],
                [9, 10, 11, 12]])

print("আকার:", arr.shape)        # (3, 4)
print("arr + 10:")
print(arr + 10)                    # প্রতিটি element-এ 10 যোগ

print("\narr * 2:")
print(arr * 2)                     # প্রতিটি element 2 দিয়ে গুণ

# Boolean broadcasting — এটাও broadcast
print("\narr > 5:")
print(arr > 5)                     # প্রতিটি element-এ 5 compare

    
Scalar 10 কে NumPy স্বয়ংক্রিয়ভাবে (৩, ৪) shape-এ ভাবে — element-wise যোগ। কোনো explicit loop নেই, কিন্তু underneath C-level optimized।

৩ · Broadcasting rules — কীভাবে align হয়

NumPy দু'টি array compare করার সময় শেষ axis (rightmost) থেকে শুরু করে। প্রতিটি axis-এ একটি rule:

তিনটি Broadcasting Rule

(১) দু'টি shape-কে rightmost থেকে align করো। কম dim থাকলে — বাঁদিকে ১ pad করো।
(২) প্রতিটি axis — দু'টি size সমান হোক, অথবা এদের একটি ১ হোক।
(৩) উপরের শর্ত পূরণ না হলে — ValueError: operands could not be broadcast together।

উদাহরণ — shape (৩, ৪) ও (৪,) compatible:

$$(3, 4) \;+\; (4,) \;\Rightarrow\; (3, 4) \;+\; (1, 4) \;\to\; (3, 4)$$

আরও কয়েকটি কেস:

  • (৩, ৪) + (১, ৪) ✓ → (৩, ৪)
  • (৩, ৪) + (৩, ১) ✓ → (৩, ৪)
  • (৩, ১) + (১, ৪) ✓ → (৩, ৪) — outer-product-style
  • (৩, ৪) + (৩,) ✗ → ValueError (rightmost mismatch: ৪ ≠ ৩)

৪ · Row vector + matrix, column vector + matrix

AI-তে এই দু'টি pattern ক্রমাগত আসে।

Python · NumPy
import numpy as np

# 3 ছাত্রের 4 subject marks
marks = np.array([[80, 70, 90, 60],
                  [75, 85, 65, 95],
                  [60, 90, 80, 70]])
print("marks shape:", marks.shape)       # (3, 4)

# Row vector — 4 subject-এর গড়
mean = np.array([72, 82, 78, 75])
print("mean shape:", mean.shape)          # (4,)

# Broadcasting — প্রতিটি row থেকে mean বাদ
diff = marks - mean
print("\nগড় থেকে পার্থক্য:")
print(diff)

# Column vector — 3 ছাত্রের bonus
bonus = np.array([[5], [10], [3]])         # shape (3, 1)
print("\nbonus shape:", bonus.shape)

# প্রতিটি ছাত্রের সব subject-এ bonus যোগ
adjusted = marks + bonus
print("\nbonus সহ:")
print(adjusted)

    
Row vector (৪,) ও matrix (৩, ৪) — broadcasting-এ row-টি ৩ বার stack হয়। Column vector (৩, ১) — ৪ বার stack হয় column-wise।

৫ · np.newaxis ও reshape — dim যোগ

কখনো ১-D array-কে row বা column vector হিসাবে treat করতে হয়। এর জন্য np.newaxis বা reshape ব্যবহার করুন।

Python · NumPy
import numpy as np

a = np.array([1, 2, 3])
print("a shape:", a.shape)               # (3,)

# Row vector — (1, 3)
row = a[np.newaxis, :]
print("row shape:", row.shape)            # (1, 3)

# Column vector — (3, 1)
col = a[:, np.newaxis]
print("col shape:", col.shape)            # (3, 1)

# Equivalent — reshape
col2 = a.reshape(-1, 1)
print("col2 shape:", col2.shape)          # (3, 1)

# Outer-product-like via broadcasting
# (3, 1) + (1, 3) → (3, 3)
result = col + row
print("\n(3,1) + (1,3) → (3,3):")
print(result)

    
np.newaxis = None — slicing-এ একটি নতুন dim যোগ করে। এটা ছাড়া broadcasting অনেক pattern অসম্ভব হবে।

৬ · Memory efficiency — virtual stretching

Broadcasting-এর জাদু — ছোট array আসলে copy হয় না। NumPy একটি view তৈরি করে যেখানে stride 0 সেট করা — অর্থাৎ "পরবর্তী element-এ যেতে 0 byte লাফাও" — একই value বারবার পড়া হয়।

Stride trick

একটি (৪,) array-কে (১০০০, ৪)-এ broadcast করলে — memory-তে শুধু মূল ৪টি float থাকে। NumPy logical shape ১০০০×৪ দেখায়, কিন্তু physical storage একই। এটাই million-row dataset-এ feature normalization কে সম্ভব করে।

np.broadcast_to(arr, new_shape) দিয়ে আপনি explicitly view তৈরি করতে পারেন। এই view read-only — কারণ একই memory location অনেক index-এ map করে; লিখতে দিলে undefined behavior।

৭ · AI use case — normalization, bias, distance matrix

AI training pipeline-এর তিনটি সর্বব্যাপী pattern broadcasting-এ লেখা হয়।

Python · NumPy · AI patterns
import numpy as np

# ── (১) Feature normalization (z-score) ──
# 100 sample, 5 feature
np.random.seed(42)
X = np.random.randn(100, 5) * 10 + 50

mean = X.mean(axis=0)            # shape (5,) — প্রতি column-এর গড়
std  = X.std(axis=0)             # shape (5,) — প্রতি column-এর std

# Broadcasting: (100, 5) - (5,) → (100, 5)
X_norm = (X - mean) / std
print("normalized mean ≈ 0:", X_norm.mean(axis=0).round(3))
print("normalized std  ≈ 1:", X_norm.std(axis=0).round(3))

# ── (২) Batch + bias (neural net layer) ──
batch = np.random.randn(32, 10)   # 32 sample, 10 feature
bias  = np.random.randn(10)       # 10-D bias vector

# (32, 10) + (10,) → (32, 10) — প্রতি sample-এ একই bias
out = batch + bias
print("\nbatch+bias shape:", out.shape)

# ── (৩) Pairwise distance matrix ──
# 5 point in 2-D, এদের মধ্যে দূরত্ব
points = np.array([[0,0],[1,0],[0,1],[2,2],[3,1]])  # (5, 2)

# (5, 1, 2) - (1, 5, 2) → (5, 5, 2) → squared dist (5, 5)
diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]
dist = np.sqrt((diff ** 2).sum(axis=-1))
print("\npairwise distance:")
print(dist.round(2))

    
Feature normalization, batch+bias, pairwise distance — তিনটি pattern-ই broadcasting ছাড়া nested loop দরকার হত। এখন এক লাইনে, ১০-১০০× দ্রুত।

৮ · Common pitfalls — silent broadcast bug

Broadcasting শক্তিশালী, কিন্তু একই কারণে বিপজ্জনক। সবচেয়ে কুখ্যাত bug — accidental broadcast।

একটি (৩,) ও একটি (৩, ১) array যোগ করুন — উত্তর (৩, ৩) আসবে! আপনি হয়তো (৩,) চেয়েছিলেন। NumPy error দেয় না — silently ভুল shape-এ result দেয়। AI training-এ এই ধরনের bug — model "শেখে" কিন্তু metric অদ্ভুত।
Python · pitfall
import numpy as np

a = np.array([1, 2, 3])           # shape (3,)
b = np.array([[10], [20], [30]])  # shape (3, 1)

# আপনি ভাবলেন element-wise — (3,) + (3,)
result = a + b
print("shape:", result.shape)      # (3, 3) — সারপ্রাইজ!
print(result)

# সঠিক — column vector যোগ চাইলে
correct = a + b.flatten()          # (3,) + (3,) → (3,)
print("\nসঠিক:", correct)

    
Defense — সবসময় .shape print করুন ও assert result.shape == expected দিয়ে guard লিখুন।

৯ · Visualization helper — broadcast_to ও broadcast_shapes

NumPy দু'টি utility দেয় broadcasting বুঝতে।

Python · NumPy utilities
import numpy as np

# broadcast_shapes — যোগ ছাড়াই result shape দেখায়
print(np.broadcast_shapes((3, 4), (4,)))         # (3, 4)
print(np.broadcast_shapes((3, 1), (1, 4)))       # (3, 4)
print(np.broadcast_shapes((5, 1, 2), (1, 5, 2))) # (5, 5, 2)

# Incompatible — error
try:
    np.broadcast_shapes((3, 4), (3,))
except ValueError as e:
    print("error:", e)

# broadcast_to — explicitly stretch (read-only view)
a = np.array([1, 2, 3, 4])
stretched = np.broadcast_to(a, (3, 4))
print("\nstretched shape:", stretched.shape)
print(stretched)
print("base is original:", stretched.base is a)   # True — copy নয়

    
broadcast_shapes debug-এ অসাধারণ — কোড না চালিয়ে result shape predict। broadcast_to দেখায় memory-তে কী ঘটে — এটি true view, copy নয়।
Broadcasting — দু'টি pattern (১) Row vector + Matrix · (3, 4) + (4,) → (3, 4) A · shape (3, 4) a₁₁ a₁₂ a₁₃ a₁₄ a₂₁ a₂₂ a₂₃ a₂₄ a₃₁ a₃₂ a₃₃ a₃₄ + b · shape (4,) b₁ b₂ b₃ b₄ ↓ stretch ৩ বার (virtual) = result · (3, 4) a₁₁+b₁ a₁₂+b₂ a₁₃+b₃ a₁₄+b₄ a₂₁+b₁ a₂₂+b₂ a₂₃+b₃ a₂₄+b₄ a₃₁+b₁ a₃₂+b₂ a₃₃+b₃ a₃₄+b₄ (২) Column + Row · (3, 1) + (1, 4) → (3, 4) — outer-product-like c · (3, 1) c₁ c₂ c₃ + d · (1, 4) d₁ d₂ d₃ d₄ = result · (3, 4) c₁+d₁ c₁+d₂ c₁+d₃ c₁+d₄ c₂+d₁ c₂+d₂ c₂+d₃ c₂+d₄ c₃+d₁ c₃+d₂ c₃+d₃ c₃+d₄ size 1 axis সবসময় stretch হয় (virtually) — copy ছাড়াই, stride-trick দিয়ে।
Broadcasting visualization — উপরে row-vector matrix-এ stretch (৩ বার), নিচে column ও row একসাথে stretch হয়ে (৩, ৪) তৈরি। লাল arrow = virtual stretching, memory-তে copy নয়।

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

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

প্র ০১ Broadcasting-এ "ছোট array stretch হয়" — কিন্তু memory-তে copy হয় না কেন? Stride manipulation আসলে ভেতরে কী ঘটাচ্ছে?

NumPy-এর সবচেয়ে চতুর design — stride। প্রতিটি ndarray-এ shape ছাড়াও একটি strides tuple আছে — "এই axis-এ পরবর্তী element-এ যেতে কত byte লাফাও।"

সাধারণ stride: একটি (৩, ৪) float64 array-এ — প্রতি element ৮ byte। Strides হবে (৩২, ৮) — অর্থাৎ row-এ যেতে ৩২ byte (৪ × ৮), column-এ ৮ byte।

Broadcasting-এর জাদু:

  • একটি (৪,) array — strides (৮,), total memory ৩২ byte।
  • এটাকে (৩, ৪)-এ broadcast করতে — NumPy strides বানায় (0, ৮)।
  • "Row-এ যেতে 0 byte লাফাও" — অর্থাৎ একই memory location আবার পড়ো।
  • Logical view (৩, ৪), physical memory ৩২ byte — copy নেই।

কেন এটা genius:

  • Memory: ১ million row + (৫,) bias = ১ million × ৫ × ৮ byte সাশ্রয় (৪০ MB)। Big data-এ critical।
  • Speed: Cache-friendly — একই memory বারবার পড়লে CPU cache hit বেশি।
  • Composable: Multiple broadcast operation chain করলেও কোনো intermediate copy নেই।

Trade-off:

  • broadcast_to result read-only — কারণ একই memory একাধিক index-এ map। লিখতে দিলে data corruption।
  • কোনো কোনো operation (যেমন np.sort) view-তে কাজ করে না — internally copy বানায়।
  • Memory layout non-contiguous হলে — পরের কিছু operation slow হতে পারে।

যাচাই: চালান —

import numpy as np
a = np.array([1,2,3,4])
b = np.broadcast_to(a, (3,4))
print(b.strides)         # (0, 8) — row stride 0!
print(b.base is a)       # True
print(a.nbytes, b.nbytes) # 32, 32 — same memory

মূল উপলব্ধি: Broadcasting "magic" নয় — clever indexing। এই principle জানলে — আপনি memory-efficient ML pipeline ডিজাইন করতে পারবেন। PyTorch/TensorFlow-ও একই principle ব্যবহার করে (যেমন expand বনাম repeat)।

প্র ০২ Batch normalization, layer normalization — modern deep learning-এর backbone। এগুলো broadcasting দিয়ে কীভাবে express করা যায়? কী shape-এ mean/std নেওয়া হয়?

Normalization layer — DL-এ training stability ও convergence speed-এর সবচেয়ে বড় breakthrough (Ioffe & Szegedy ২০১৫, Ba et al. ২০১৬)। সবগুলোই broadcasting-এর pattern।

Setup: একটি batch — shape (N, D), যেখানে N = batch size (যেমন ৩২), D = feature dim (যেমন ৭৬৮)।

(১) Batch Normalization (BN):

  • Idea: "এই batch জুড়ে প্রতিটি feature-এর গড় ০, std ১ করো।"
  • mean = X.mean(axis=0) → shape (D,) — N dimension reduced।
  • std = X.std(axis=0) → shape (D,)।
  • X_norm = (X - mean) / (std + eps) — broadcasting (N, D) - (D,) → (N, D)।
  • প্রতিটি feature-এর statistic batch জুড়ে নেওয়া — sample-এর মধ্যে dependence।

(২) Layer Normalization (LN):

  • Idea: "প্রতিটি sample-এর ভেতরে সব feature-এর গড় ০, std ১ করো।"
  • mean = X.mean(axis=1, keepdims=True) → shape (N, 1)।
  • std = X.std(axis=1, keepdims=True) → shape (N, 1)।
  • X_norm = (X - mean) / (std + eps) — broadcasting (N, D) - (N, 1) → (N, D)।
  • Sample-এর মধ্যে independence — Transformer-এর preferred।

keepdims-এর ভূমিকা:

  • keepdims=True না দিলে — mean shape (N,), broadcasting (N, D) - (N,) → confusion বা error।
  • keepdims=True shape (N, 1) রাখে — broadcasting clean ও explicit।

(৩) Affine transform (γ, β):

  • Normalized output-এ learnable scale ও shift: Y = γ * X_norm + β।
  • γ, β shape (D,) — broadcasting (N, D) * (D,) → (N, D)।
  • মডেল শিখে — কোন feature বেশি/কম important।

RMSNorm (Llama-এ ব্যবহৃত):

  • Mean বাদ — শুধু RMS দিয়ে scale: X / sqrt(mean(X²))।
  • আরও দ্রুত, একই accuracy — broadcasting pattern same।

মূল উপলব্ধি: এই সব normalization variant — আসলে একই broadcasting pattern ভিন্ন axis-এ। যেদিকে statistic নেওয়া হল — সেই axis "1" হয়ে gone, বাকি broadcast। DL-এর hottest research এই axis-choice নিয়ে।

প্র ০৩ Broadcasting bug — সবচেয়ে কুখ্যাত। যেমন (3,) + (3,1) → (3,3) — আপনি (3,) চেয়েছিলেন, পেলেন (3,3)। NumPy error দেয় না। কীভাবে এই bug catch করবেন production code-এ?

এই bug শ্রেণিকক্ষে সহজ মনে হয়, production-এ disastrous। কারণ — code চলে, ফল আসে, কিন্তু metric অদ্ভুত। Debug কঠিন কারণ কোথাও exception নেই।

সবচেয়ে সাধারণ cases:

  • (N,) + (N,1) → (N, N) — element-wise চাইলে disaster।
  • (N,) - (1, N) → (1, N) — শুধু একটি sample।
  • predictions.shape (N, 1) ও labels.shape (N,) — loss N×N matrix হয়ে যায়, mean-ও ভুল।
  • Last layer-এ squeeze ভুলে গেলে — pyTorch-এ classic Kaggle bug।

Defense (১) — সবসময় shape print/assert:

def normalize(X, mean, std):
    assert X.ndim == 2, f"X must be 2D, got {X.shape}"
    assert mean.shape == (X.shape[1],), f"mean shape mismatch: {mean.shape}"
    assert std.shape == (X.shape[1],), f"std shape mismatch: {std.shape}"
    return (X - mean) / std

Defense (২) — explicit reshape:

  • x.flatten() বা x.squeeze() দিয়ে unwanted dim সরান।
  • x.reshape(-1) বা x.reshape(N, 1) — intent স্পষ্ট।
  • Broadcast-এর আগে print(a.shape, b.shape) — debug-এর সর্বোত্তম বন্ধু।

Defense (৩) — unit test for shape:

def test_loss_shape():
    pred = np.random.randn(32, 1)
    label = np.random.randn(32)
    loss = ((pred - label.reshape(-1, 1)) ** 2).mean()
    assert np.isscalar(loss) or loss.ndim == 0

Defense (৪) — type/shape annotation:

  • jaxtyping বা torchtyping — function signature-এ shape declare।
  • Example: def f(x: Float[Tensor, "batch d"]) -> ...
  • Runtime check + IDE hint — production-এ priceless।

Defense (৫) — einsum বা einops:

  • einops.rearrange(x, "n d -> n 1 d") — explicit, readable।
  • np.einsum("ij,j->i", X, w) — broadcasting confusion-এর জায়গায় নিজে অক্ষর দিয়ে contract।
  • আধুনিক DL codebase (PyTorch, JAX) এই দিকে যাচ্ছে।

Detection in training:

  • Loss সঠিক shape-এ আসছে কিনা — first iteration-এ assert।
  • Gradient-এর shape parameter shape-এর সমান কিনা — checkpoint-এ।
  • Loss curve অস্বাভাবিক plateau-এ গেলে — broadcast bug সন্দেহ করুন।

মূল উপলব্ধি: Broadcasting code-কে concise করে — কিন্তু "magic" থেকে cost আছে। Senior ML engineer-রা প্রতি broadcast-এর আগে shape verify করেন। এই discipline পারলে — আপনার ভুল model debugging-এ কয়েক দিন বাঁচবে।

প্র ০৪ Outer product, pairwise distance matrix, attention score — তিনটি AI core operation। এগুলো broadcasting pattern দিয়ে কীভাবে express করা যায়?

এই তিনটি operation আধুনিক AI-এর backbone। তিনটিই — একই broadcasting pattern, ভিন্ন interpretation।

(১) Outer product:

  • দু'টি vector $u \in \mathbb{R}^m, v \in \mathbb{R}^n$ — outer product $u \otimes v \in \mathbb{R}^{m \times n}$।
  • $(u \otimes v)_{ij} = u_i \cdot v_j$।
  • Broadcasting: u[:, None] * v[None, :] → (m, 1) * (1, n) → (m, n)।
  • Use: low-rank matrix approximation, attention weight construction।

(২) Pairwise distance matrix:

  • N points in D-dim — distance matrix $D \in \mathbb{R}^{N \times N}$, $D_{ij} = \|x_i - x_j\|$।
  • Naive: double loop, O(N²) iteration। Slow।
  • Broadcasting:
    diff = X[:, None, :] - X[None, :, :]   # (N, 1, D) - (1, N, D) → (N, N, D)
    dist = np.sqrt((diff ** 2).sum(axis=-1)) # (N, N)
  • একটি vectorized expression — C-level speed।
  • Use: k-NN, clustering, retrieval, embedding evaluation।

(৩) Attention score:

  • Query $Q \in \mathbb{R}^{N \times d}$, Key $K \in \mathbb{R}^{M \times d}$ — score matrix $S \in \mathbb{R}^{N \times M}$।
  • $S_{ij} = q_i \cdot k_j / \sqrt{d}$ — সব query-key pair-এর dot product।
  • Broadcasting + matmul: S = Q @ K.T / np.sqrt(d)।
  • Or explicit broadcasting: (Q[:, None, :] * K[None, :, :]).sum(axis=-1)।
  • Softmax row-wise: A = softmax(S, axis=-1) — broadcasting (N, M) - (N, 1) max-trick।
  • Output: A @ V — (N, M) @ (M, d) → (N, d)।

সাধারণ pattern:

  • Add a "1" dim → broadcast → reduce: এই তিনটিই একই গান।
  • $(N, D) \to (N, 1, D)$ এবং $(M, D) \to (1, M, D)$ — combined $(N, M, D)$ — reduce-এ pair-wise quantity।
  • matmul = optimized version of "broadcast + multiply + sum"।

Memory consideration:

  • $(N, M, D)$ intermediate — N=M=১০০০, D=৭৬৮ হলে ৩ GB! OOM common।
  • matmul দিয়ে — intermediate এড়িয়ে directly $(N, M)$ result।
  • Flash Attention — block-wise compute, memory-efficient।
  • einsum ও einops এই pattern explicit ও optimizable করে।

Self-attention এ দু'টি ব্যবহার:

  • Q-K দূরত্ব = "এই word অন্য কোন word-এর সাথে relevant?"
  • Outer-style construction — multi-head এ batch-wise।
  • $O(N^2)$ memory — Transformer scaling-এর মূল bottleneck।

মূল উপলব্ধি: Modern AI-এর সব "linear algebra heavy lifting" — broadcasting + reduction-এর variation। এই pattern চিনলে — Transformer code পড়া, custom attention লেখা, debugging — সবই সহজ। GPT, BERT, ViT — সব broadcasting-এর উপর দাঁড়িয়ে।

অনুশীলন

  1. Column normalize: একটি (১০০, ৪) random matrix নিন। প্রতিটি column-এর mean-০, std-১ করুন। শেষে assert করুন X.mean(axis=0) ≈ 0 এবং X.std(axis=0) ≈ 1।
    import numpy as np
    np.random.seed(0)
    X = np.random.randn(100, 4) * 10 + 50
    X_norm = (X - X.mean(axis=0)) / X.std(axis=0)
    assert np.allclose(X_norm.mean(axis=0), 0, atol=1e-10)
    assert np.allclose(X_norm.std(axis=0),  1, atol=1e-10)
    print("OK — shape:", X_norm.shape)
  2. Pairwise distance: ৫টি 2-D point নিন। broadcasting দিয়ে (৫, ৫) Euclidean distance matrix বানান। Verify — diagonal ০ এবং matrix symmetric।
    import numpy as np
    points = np.array([[0,0],[1,0],[0,1],[2,2],[3,1]], dtype=float)
    diff = points[:, None, :] - points[None, :, :]   # (5, 5, 2)
    dist = np.sqrt((diff ** 2).sum(axis=-1))         # (5, 5)
    assert np.allclose(np.diag(dist), 0)
    assert np.allclose(dist, dist.T)                  # symmetric
    print(dist.round(2))
  3. Bug hunt: নিচের কোডে কী ভুল হবে এবং কেন? a = np.arange(3); b = np.arange(3).reshape(-1,1); print((a + b).shape) — সঠিক করে (৩,) shape আনুন।

    সমস্যা: a.shape = (3,), b.shape = (3,1)। Broadcasting align করে (1, 3) ও (3, 1) → result (3, 3)। উদ্দেশ্য element-wise (3,) ছিল।

    import numpy as np
    a = np.arange(3)
    b = np.arange(3).reshape(-1, 1)
    print((a + b).shape)               # (3, 3) — bug
    
    # সঠিক — b কে flatten
    correct = a + b.flatten()
    print(correct.shape, correct)       # (3,) [0 2 4]
    
    # Defensive check
    assert correct.shape == a.shape, "shape mismatch!"

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

কোড রানার কাজ না করলে? ব্রাউজারে কাজ না করলে Google Colab ব্যবহার করুন — Google-এর ফ্রি অনলাইন Python পরিবেশ, শুধু Gmail অ্যাকাউন্ট লাগে।
পূর্ববর্তী পাঠ
পাঠ ১০ · NumPy indexing ও slicing