Broadcasting — অ্যারের জাদু
এই পাঠে যা শিখবেন
- 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 এর উপর দাঁড়িয়ে।
marks - mean। Broadcasting ৩০ বার গড়-row যোগ করার বদলে — চুপচাপ stretch করে দেয়।
২ · Scalar + array — সবচেয়ে সাধারণ কেস
Broadcasting-এর সরলতম রূপ — scalar (একটি সংখ্যা) ও array। Scalar-কে ভাবুন shape (); এটি যেকোনো array-এর shape-এ stretch হয়।
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
10 কে NumPy স্বয়ংক্রিয়ভাবে (৩, ৪) shape-এ ভাবে — element-wise যোগ। কোনো explicit loop নেই, কিন্তু underneath C-level optimized।
৩ · Broadcasting rules — কীভাবে align হয়
NumPy দু'টি array compare করার সময় শেষ axis (rightmost) থেকে শুরু করে। প্রতিটি axis-এ একটি 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 ক্রমাগত আসে।
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)
(৪,) ও matrix (৩, ৪) — broadcasting-এ row-টি ৩ বার stack হয়। Column vector (৩, ১) — ৪ বার stack হয় column-wise।
৫ · np.newaxis ও reshape — dim যোগ
কখনো ১-D array-কে row বা column vector হিসাবে treat করতে হয়। এর জন্য np.newaxis বা reshape ব্যবহার করুন।
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 বারবার পড়া হয়।
একটি (৪,) 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-এ লেখা হয়।
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))
৮ · Common pitfalls — silent broadcast bug
Broadcasting শক্তিশালী, কিন্তু একই কারণে বিপজ্জনক। সবচেয়ে কুখ্যাত bug — accidental broadcast।
(৩,) ও একটি (৩, ১) array যোগ করুন — উত্তর (৩, ৩) আসবে! আপনি হয়তো (৩,) চেয়েছিলেন। NumPy error দেয় না — silently ভুল shape-এ result দেয়। AI training-এ এই ধরনের bug — model "শেখে" কিন্তু metric অদ্ভুত।
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)
.shape print করুন ও assert result.shape == expected দিয়ে guard লিখুন।
৯ · Visualization helper — broadcast_to ও broadcast_shapes
NumPy দু'টি utility দেয় broadcasting বুঝতে।
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-এ "ছোট 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_toresult 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=Trueshape (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-এর উপর দাঁড়িয়ে।
অনুশীলন
-
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) -
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)) -
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-এ আপনার পরবর্তী পদক্ষেপ
- পাঠ ১২ · NumPy linalg — ম্যাট্রিক্স গণিত পরবর্তী পাঠ matmul, inverse, eigenvalue — broadcasting-এর পরের ধাপ। AI-এর core math।
- পাঠ ১০ · NumPy indexing ও slicing আগের পাঠ Broadcasting বুঝতে indexing fluency দরকার — এক ধাপ পিছিয়ে refresh।
- পাঠ ০৯ · NumPy basics — ndarray এই পাঠের সাথে সম্পর্কিত shape, dtype, stride — broadcasting-এর foundation।
- সব AI Courses দেখুন ABCL TECH Python, ML, DL, NLP, CV, GenAI, RL, MLOps — সব AI কোর্স একসাথে।