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

Vectorization আর broadcasting — loop ছাড়াই হিসাব

Vectorization and Broadcasting — Element-wise Maths, Shape Rules, Centering, Distance Matrices and Memory Cost

Taught in Bangla, this lesson turns order-total loops into NumPy array expressions and times them in the browser, states the three broadcasting rules and predicts result shapes or errors, adds size-1 axes with np.newaxis, reshape and keepdims, and applies common patterns: centering, row shares, outer products and distance matrices with their real memory cost. It also shows that np.vectorize is a loop.

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

  • Element-wise হিসাব বোঝা, আর বাজারঘরের অর্ডারের loop-কে (দাম × পরিমাণ, ছাড়, ভ্যাট) একটি array expression-এ লিখে সময় মাপা
  • NumPy docs-এর ভাষায় broadcasting-এর নিয়ম, আর যেকোনো দুটি shape দেখে ফলের shape বা error আগেই বলে দেওয়া
  • np.newaxis, None, reshape(-1, 1) আর keepdims দিয়ে ঠিক জায়গায় মাপ 1-এর মাত্রা বসানো
  • Centering, সারির শতাংশ, outer product আর বিন্দুগুলোর distance matrix-এর মতো রোজকার broadcasting ছাঁচ
  • বড় অস্থায়ী array-র মেমরির খরচ মাপা, np.vectorize-এর সত্য জানা, আর কখন সাধারণ loop-ই যথেষ্ট তা চেনা

আগের দুই পর্বে NumPy array-র shape আর dtype (পর্ব ১৩ · NumPy array — shape, dtype আর list-এর চেয়ে কেন দ্রুত) আর indexing ও slicing (পর্ব ১৪ · Indexing আর slicing — boolean mask, fancy index, view আর copy) এসেছে। এই পর্বে সেই array দিয়ে আসল হিসাব। দুটি ধারণা: vectorization মানে loop না লিখে পুরো array-র ওপর একটি expression লেখা, আর broadcasting মানে ভিন্ন shape-এর দুটি array-কে কপি না করে মিলিয়ে নেওয়ার নিয়ম। কেন খাঁটি Python-এর loop সংখ্যায় ধীর, তার প্রথম মাপ দেখেছেন পর্ব ৯-এর শেষ অংশে। এই পাতার সব কোড ব্রাউজারেই চলে (Pyodide, মানে WebAssembly-তে বানানো আসল CPython); প্রথমবার NumPy নামাতে কয়েক সেকেন্ড লাগতে পারে।

১ · Element-wise হিসাব — একসঙ্গে সব ঘরে একই কাজ

পরীক্ষার খাতা দেখার পর একজন শিক্ষক ঠিক করলেন, প্রশ্নে ভুল থাকায় সবার নম্বরে ৫ যোগ হবে। তিনি একটি একটি খাতা খুলে যোগ করতে পারেন, অথবা অফিসের কম্পিউটারে নম্বরের পুরো কলামে একবারে “+৫” বসিয়ে দিতে পারেন। দুই পথে ফল এক, কিন্তু দ্বিতীয়টিতে প্রতিটি খাতার জন্য আলাদা করে সিদ্ধান্ত নিতে হয় না। NumPy-র array expression দ্বিতীয় পথটি।

NumPy-তে দুটি একই shape-এর array-র মধ্যে +, -, *, /, ** বা তুলনার চিহ্ন (>, ==) লিখলে কাজটি হয় element-wise: প্রথম ঘরের সঙ্গে প্রথম ঘর, দ্বিতীয়টির সঙ্গে দ্বিতীয়টি। ফলও একই shape-এর একটি নতুন array.

price = np.array([120, 450, 80])      # টাকা
qty   = np.array([  2,   1,  5])
price * qty                           # array([240, 450, 400])
price * qty > 300                     # array([False,  True,  True])
np.sqrt(np.array([9.0, 16.0]))        # array([3., 4.])

এখানে দুটি জিনিস লক্ষ করুন। এক, Python-এর list-এ [120, 450] * 2 লিখলে list দুবার পাশাপাশি জুড়ে যায়, সংখ্যা দ্বিগুণ হয় না; array-তে একই চিহ্নের মানে সম্পূর্ণ আলাদা। দুই, np.sqrt, np.exp, np.log, np.maximum-এর মতো function-গুলোও element-wise কাজ করে। NumPy এদের বলে ufunc (universal function): C-তে লেখা ছোট একটি loop, যা একটি dtype-এর সংখ্যার ওপর টানা চলে। a + b লেখা আসলে np.add(a, b) ডাকা। ufunc-এর পুরো পরিবার, axis ধরে যোগফল আর random সংখ্যা আসবে পর্ব ১৬ · গাণিতিক কাজ, aggregation আর random-এ।

Element-wise হিসাবের ভেতরে আসলে কী হয়, ছোট করে বলি। Python-এর loop-এ প্রতিটি উপাদানের জন্য interpreter-কে type দেখতে হয়, গুণের নিয়ম খুঁজতে হয়, ফলের জন্য একটি নতুন Python object বানাতে হয়। ufunc-এ এই যাচাই পুরো array-র জন্য একবার হয় (দুটিই int64, তাই int64-এর গুণ), তারপর compiled কোড কাঁচা সংখ্যাগুলোর ওপর দিয়ে চলে যায়। আধুনিক CPU একটি নির্দেশে কয়েকটি সংখ্যা একসঙ্গে গুণ করতে পারে (SIMD), আর NumPy-র অনেক ufunc তা কাজে লাগায়। “Vectorization” শব্দটি এই দুই অর্থেই ব্যবহার হয়: Python-এর ভাষায় loop-হীন array expression, আর CPU-র ভাষায় একসঙ্গে কয়েকটি সংখ্যার নির্দেশ। এই কোর্সে প্রথম অর্থটিই প্রধান।

মূল ধারণা

Vectorization মানে loop মুছে ফেলা নয়, loop-কে Python থেকে compiled কোডে সরিয়ে দেওয়া। আপনি লেখেন “প্রতিটি অর্ডারে দাম × পরিমাণ”, আর loop-টি চলে NumPy-র ভেতরে। তাই কাজের পরিমাণ একই থাকে, শুধু প্রতি ধাপের হিসাবনিকাশের খরচ চলে যায়।

২ · Loop থেকে array expression — বাজারঘরের অর্ডারের মোট দাম

বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) প্রতিটি অর্ডারে একটি দাম (পয়সায়), একটি পরিমাণ আর একটি ছাড়ের হার আছে, আর মোটের ওপর ১৫% ভ্যাট বসে (এখানে শুধু উদাহরণের হার; আসল ভ্যাটের নিয়ম হিসাবের নীতি থেকে নিন)। একটি অর্ডারের মোট:

মোট = দাম × পরিমাণ × (১ − ছাড়) × (১ + ভ্যাট)

Loop দিয়ে লিখলে প্রতিটি অর্ডারের জন্য এই সূত্র একবার। Array দিয়ে লিখলে সূত্রটি একবারই, পুরো কলামের ওপর। রূপান্তরের নিয়মটি যান্ত্রিক: loop-এর ভেতরের price[i]-কে price দিয়ে বদলান, i মুছে দিন, আর if থাকলে তাকে np.where বা boolean mask দিয়ে বদলান (mask এসেছে পর্ব ১৪-এ)।

Loop-এ যা লেখেনArray-তে একই কাজ
t[i] = p[i] * q[i]t = p * q
t[i] = p[i] * 1.15t = p * 1.15 (scalar সবার সঙ্গে)
if q[i] >= 5: d[i] = 0.1d = np.where(q >= 5, 0.1, 0.0)
s += t[i]s = t.sum()
if t[i] > m: m = t[i]m = t.max()

এবার এক লাখ অর্ডারে তিনটি পথ মেপে দেখি: খাঁটি Python (list আর comprehension), array expression, আর np.vectorize দিয়ে মোড়ানো Python function (এর কথা অংশ ৮-এ)। হিসাব পয়সায়; ছাড় আর ভ্যাটের পরে ভগ্নাংশ আসে, তাই শেষে পূর্ণ পয়সায় round. দুই পথেই round একই নিয়মে হয় (Python-এর round আর np.round দুটিই ঠিক মাঝখানের মানকে জোড় সংখ্যার দিকে নেয়), তাই ফল হুবহু মেলার কথা।

import time
import numpy as np

rng = np.random.default_rng(42)
n = 100_000                                     # এক লাখ অর্ডার
price = rng.integers(5_000, 300_000, size=n, dtype=np.int64)  # পয়সায়
qty = rng.integers(1, 6, size=n, dtype=np.int64)
disc = rng.choice([0.0, 0.05, 0.10], size=n)    # ছাড়ের হার
VAT = 0.15

t0 = time.perf_counter()                        # ১) Python loop
P, Q, D = price.tolist(), qty.tolist(), disc.tolist()
loop = [round(p * q * (1 - d) * (1 + VAT)) for p, q, d in zip(P, Q, D)]
t1 = time.perf_counter()                        # ২) array expression
vec = np.round(price * qty * (1 - disc) * (1 + VAT)).astype(np.int64)
t2 = time.perf_counter()                        # ৩) np.vectorize
f = np.vectorize(lambda p, q, d: round(p * q * (1 - d) * (1 + VAT)))
npv = f(price, qty, disc)
t3 = time.perf_counter()

print("প্রথম ৩টি অর্ডার (পয়সা):", vec[:3])
print("তিন পথের ফল এক?", np.array_equal(vec, loop), np.array_equal(vec, npv))
print("মোট বিক্রি (টাকা):", vec.sum() // 100)
for name, t in [("loop", t1 - t0), ("array", t2 - t1), ("np.vectorize", t3 - t2)]:
    print(f"{name:13}{t * 1000:8.1f} ms")
print(f"array প্রায় {(t1 - t0) / (t2 - t1):.0f} গুণ দ্রুত loop-এর চেয়ে")

ব্রাউজারে (Pyodide) একটি run-এর আসল আউটপুট:

প্রথম ৩টি অর্ডার (পয়সা): [162128 724449 615094]
তিন পথের ফল এক? True True
মোট বিক্রি (টাকা): 499916713
loop             39.8 ms
array             2.4 ms
np.vectorize     47.9 ms
array প্রায় 17 গুণ দ্রুত loop-এর চেয়ে

তিনটি পথে এক লাখ অর্ডারের ফল হুবহু এক, পয়সা পর্যন্ত। প্রথম অর্ডারটি হাতে মিলিয়ে দেখার মতো: যেকোনো একটি সারির price, qty, disc ছেপে সূত্রে বসালেই একই সংখ্যা আসবে, আর এই অভ্যাসটিই vectorized কোডের প্রথম পরীক্ষা। এই run-এ loop প্রায় ৪০ মিলিসেকেন্ড, array expression প্রায় ২.৪, মানে প্রায় ১৭ গুণ; আর np.vectorize প্রায় ৪৮, loop-এর চেয়েও একটু বেশি। সময়ের সংখ্যাগুলো একটি যন্ত্রে, একটি ব্রাউজারে, একটি run-এর; আপনার পর্দায় অন্য সংখ্যা আসবে, আর আবার চালালেও বদলাবে। লেখকের যন্ত্রে সাধারণ Python 3.14.4 আর NumPy 2.5.1-এ একটি run-এ এসেছিল loop ১৯.১, array ২.৩ আর np.vectorize ১৯.৫ মিলিসেকেন্ড। দুই জায়গাতেই ছবিটা এক: array expression কয়েক গুণ দ্রুত, আর np.vectorize loop-এর চেয়ে দ্রুত নয়। Loop-এর সময়ে tolist()-ও ধরা আছে, কারণ loop চালাতে হলে Python-এর সংখ্যাই লাগে।

একটি সূক্ষ্ম কথা: array expression-এ price * qty হয় int64-এ (নিখুঁত), তারপর (1 - disc)-এর সঙ্গে গুণে float64. সবচেয়ে বড় মান এখানে প্রায় ৩ লাখ পয়সা × ৫ = ১৫ লাখ পয়সা, int64-এর সীমার অনেক নিচে। কিন্তু ব্রাউজারের Python-এ dtype না বললে NumPy-র পূর্ণসংখ্যা int32 (সীমা প্রায় ২১৪ কোটি), তাই কোডে dtype=np.int64 স্পষ্ট লেখা।

Vectorized কোডেও ফাঁদ আছে। Array expression-এ একটি ভুল এক লাখ সারিতে একসঙ্গে ছড়ায়, আর কোনো error আসে না। তাই নতুন সূত্র লেখার পর দুটি কাজ অভ্যাস করুন: ২–৩টি সারি হাতে হিসাব করে মেলানো, আর ছোট ডেটায় loop-এর পুরোনো সংস্করণের সঙ্গে np.array_equal বা np.allclose দিয়ে তুলনা। ছোট test লেখার নিয়ম এসেছে পর্ব ১১-এ।

৩ · Broadcasting-এর নিয়ম — ডান দিক থেকে মেলানো

অংশ ২-এ price * 1.15 লিখেছেন: একটি এক লাখ ঘরের array আর একটি মাত্র সংখ্যা। Shape আলাদা, তবু কাজ হলো। কারণ NumPy সংখ্যাটিকে এমনভাবে ব্যবহার করেছে যেন সেটি এক লাখ ঘরে বসানো, কিন্তু আসলে এক লাখ কপি বানায়নি। এটিই broadcasting-এর সবচেয়ে সরল রূপ। একই নিয়ম আরও সাধারণ: একটি টেবিলের প্রতিটি সারিতে একটি সারি যোগ করা, বা প্রতিটি কলামে একটি কলাম।

ক্রিকেটের স্কোরকার্ডের কথা ভাবুন। এগারোজন ব্যাটারের রানের একটি কলাম আছে, আর বোর্ড ঠিক করল সবার রান থেকে “ফ্রি হিট”-এর একটি ভুল হিসাব বাদ যাবে, প্রত্যেকের জন্য আলাদা মান। সেই মানগুলোও একটি কলাম। দুটি কলাম পাশাপাশি রাখলে সারি ধরে বিয়োগ সহজ। কিন্তু যদি একটি মাত্র মান থাকে (সবার জন্য একই), সেটি মনে মনে পুরো কলামে “টেনে” নেওয়া হয়। Broadcasting হলো এই “টেনে নেওয়ার” স্পষ্ট নিয়ম: কোন মাত্রা টানা যাবে আর কোনটি যাবে না।

NumPy-র অফিসিয়াল docs (Broadcasting পাতা) নিয়মটি এভাবে বলে; বাংলায় তিনটি বাক্যে:

Broadcasting-এর নিয়ম

১. ডান দিক থেকে মেলানো: দুটি array-র shape শেষ (সবচেয়ে ডানের) মাত্রা থেকে শুরু করে বাঁ দিকে একটি একটি করে তুলনা হয়।
২. মিলের শর্ত: দুটি মাত্রা মেলে যদি সমান হয়, অথবা দুটির একটি 1 হয়। 1 হলে সেটি অন্যটির মাপ পর্যন্ত টানা ধরা হয়। কোনো জোড়া না মিললে ValueError: operands could not be broadcast together.
৩. না থাকা মাত্রা = 1: দুটির মাত্রার সংখ্যা সমান হতে হবে না। কম মাত্রার array-র বাঁ দিকে যে মাত্রা নেই, সেগুলো মাপ 1 ধরা হয়। ফলের প্রতিটি মাত্রা হয় দুটির মধ্যে বড় মাপটি।

একটি উদাহরণে নিয়ম তিনটি একসঙ্গে দেখি: a-এর shape (3, 1) (তিনটি সারির একটি কলাম), b-এর shape (4,) (চারটি সংখ্যার একটি সারি)। নিচের ছবি আর তার পরের ধাপগুলো একই হিসাব।

(3, 1) + (4,) → (3, 4): কোন মাত্রা কোথায় টানা হলো

Broadcasting a column against a row: size-1 axes are stretched, not copied

(3, 1) + (4,) broadcasting হয়ে (3, 4) a — shape (3, 1) 10 10 10 10 20 20 20 20 30 30 30 30 নীল = আসল, ধূসর = ধার করা (stride 0) + b — shape (4,) → (1, 4) 1 2 3 4 1 2 3 4 1 2 3 4 = a + b — shape (3, 4), নতুন মেমরি 11 12 13 14 21 22 23 24 31 32 33 34 প্রতিটি ঘর = সারির a + কলামের b
ছবি ১৫.১ — ওপরে a: আসল ঘর শুধু বাঁয়ের নীল কলামটি; ধূসর দাগের তিনটি কলাম “ধার করা”, মেমরিতে নেই। মাঝে b: আসল শুধু ওপরের হলুদ সারি; নিচের দুটি সারি ধার করা। নিচে a + b: এই ১২টি ঘর সত্যিই নতুন মেমরিতে তৈরি হয়। প্রতিটি ঘর = তার সারির a + তার কলামের b.
ধাপ ১দুটি array আর তাদের shape
# shape (3, 1)
a = np.array([[10], [20], [30]])
# shape (4,)
b = np.array([1, 2, 3, 4])
a + b   # ফলের shape কী?
ধাপ ২ডান দিকে সমান করে লেখা (নিয়ম ৩)
a.shape :  (3, 1)
b.shape :  (   4)
বাঁয়ে 1 বসিয়ে:
b.shape :  (1, 4)
ধাপ ৩শেষ মাত্রা: 1 বনাম 4
a.shape :  (3, 1)
b.shape :  (1, 4)
           ─────
শেষ ঘর: 1 আর 4
একটি 1, তাই মেলে → 4
ধাপ ৪প্রথম মাত্রা: 3 বনাম 1
a.shape :  (3, 1)
b.shape :  (1, 4)
           ─────
প্রথম ঘর: 3 আর 1
একটি 1, তাই মেলে → 3
ফলের shape = (3, 4)
ধাপ ৫মনে মনে টানা (কপি নয়)
a টানা হলো ডানে:
[[10 10 10 10]
 [20 20 20 20]
 [30 30 30 30]]
b টানা হলো নিচে:
[[1 2 3 4]
 [1 2 3 4]
 [1 2 3 4]]
strides → (8, 0)
ধাপ ৬Element-wise যোগ, নতুন (3, 4) array
a + b
[[11 12 13 14]
 [21 22 23 24]
 [31 32 33 34]]
  1. দুটি ভিন্ন shape. a তিন সারি এক কলাম, b শুধু চারটি সংখ্যা। দুটির মাত্রার সংখ্যাই আলাদা: a ২-মাত্রার, b ১-মাত্রার।
  2. ডান দিকে সমান করে লেখা। নিয়ম ৩ অনুযায়ী b-এর বাঁ দিকে না থাকা মাত্রাটি 1 ধরা হয়, তাই (4,) হয়ে যায় (1, 4). খেয়াল করুন, 1 বসে বাঁ দিকে, ডানে নয়।
  3. শেষ মাত্রা আগে। a-এর 1 আর b-এর 4: সমান নয়, কিন্তু একটি 1, তাই মেলে। ফলের এই মাত্রা 4.
  4. তারপর বাঁয়ের মাত্রা। a-এর 3 আর b-এর 1: আবার একটি 1, তাই মেলে, ফল 3. সব জোড়া মিলেছে, তাই ফলের shape (3, 4).
  5. টানা, কিন্তু কপি নয়। মাপ 1-এর মাত্রা ধরে NumPy একই মান বারবার পড়ে। np.broadcast_to(a, (3, 4))-এর strides (8, 0): কলামের দিকে এগোতে ০ বাইট সরতে হয়, মানে একই ঘর আবার পড়া (লেখকের যন্ত্রে int64 দিয়ে চালানো)।
  6. ফল। প্রতিটি ঘর = তার সারির a + তার কলামের b: যেমন সারি ২, কলাম ৩-এ 20 + 3 = 23. ইনপুটে ছিল ৩ + ৪ = ৭টি সংখ্যা, ফলে ১২টি; শুধু এই ফলটিই নতুন মেমরি নেয়।

Strides হলো প্রতিটি মাত্রায় এক ঘর এগোতে মেমরিতে কত বাইট সরতে হয় তার হিসাব; view আর copy এসেছে পর্ব ১৪-এ, আর strides-সহ মেমরির পুরো ছবি পর্ব ১৯ · NumPy-র গতি — memory, dtype, view আর কখন loop-ই ঠিক-এ। Broadcasting-এর মূল কৌশল এই stride 0: টানা মাত্রায় NumPy পা না সরিয়ে একই সংখ্যা পড়তে থাকে।

৪ · Shape-এর হিসাব — কোন জোড়া মেলে, কোনটি error দেয়

Broadcasting-এ দক্ষ হওয়ার একমাত্র উপায় অনেকগুলো shape-এর জোড়া নিজে মিলিয়ে দেখা। নিচের টেবিলের প্রতিটি সারি ডান দিক থেকে মেলানো হয়েছে; NumPy docs-এর উদাহরণের মতো কয়েকটি জোড়াও রাখা আছে। প্রথমে ফলের কলাম ঢেকে নিজে চেষ্টা করুন।

ABডান থেকে মেলানোফল
(5, 4)() scalarB = (1, 1)(5, 4)
(5, 4)(4,)4=4, 5 বনাম 1(5, 4)
(5, 4)(5, 1)4 বনাম 1, 5=5(5, 4)
(3, 1)(4,)1 বনাম 4, 3 বনাম 1(3, 4)
(256, 256, 3)(3,)3=3, বাকি দুটি 1(256, 256, 3)
(15, 3, 5)(15, 1, 5)5=5, 3 বনাম 1, 15=15(15, 3, 5)
(15, 3, 5)(3, 1)5 বনাম 1, 3=3, 15 বনাম 1(15, 3, 5)
(8, 1, 6, 1)(7, 1, 5)1↔5, 6↔1, 1↔7, 8↔1(8, 7, 6, 5)
(6, 1, 2)(1, 6, 2)2=2, 1↔6, 6↔1(6, 6, 2)
(0, 3)(1, 3)3=3, 0 বনাম 1(0, 3) খালি
(3,)(4,)3 বনাম 4❌ ValueError
(3, 4)(3,)4 বনাম 3❌ ValueError
(2, 3)(3, 2)3 বনাম 2❌ ValueError
(2, 1)(8, 4, 3)1↔3 ঠিক, তারপর 2 বনাম 4❌ ValueError

কয়েকটি সারি আলাদা করে দেখার মতো। (256, 256, 3) আর (3,) হলো একটি রঙিন ছবি (উচ্চতা × প্রস্থ × লাল-সবুজ-নীল) আর তিনটি channel-এর জন্য তিনটি সংখ্যা; প্রতিটি pixel-এর লাল ভাগ প্রথম সংখ্যা দিয়ে, সবুজ দ্বিতীয়টি দিয়ে গুণ হয়। ছবি নিয়ে পুরো কাজ আসবে পর্ব ১৮ · ছবি মানেই array-এ। (0, 3)-এর সারিটি মনে করিয়ে দেয়, 0 মাপের মাত্রাও নিয়ম মানে: 0 আর 1 মেলে, ফল 0, মানে একটি খালি array, error নয়। আর শেষ সারিতে ডানের জোড়া (1 আর 3) মিলেছে, কিন্তু পরের জোড়ায় আটকে গেছে; একটি জোড়া না মিললেই পুরো হিসাব বাতিল।

সবচেয়ে বেশি ভুল হয় (3, 4) আর (3,)-এ। মানুষের চোখে “৩টি সারি, ৩টি মান, প্রতিটি সারিতে একটি” স্পষ্ট লাগে। কিন্তু NumPy বাঁ দিক থেকে নয়, ডান দিক থেকে মেলায়। নিচের ধাপে দেখুন কেন এটি ভেঙে পড়ে, আর ঠিক করার পথ।

উদ্দেশ্যপ্রতিটি জেলার সারি থেকে সেই জেলার গড় বাদ
sales = np.array(
  [[12, 15,  9, 14],  # ঢাকা
   [ 8,  6, 10,  8],  # চট্টগ্রাম
   [ 3,  5,  4,  4]]) # সিলেট
m = sales.mean(axis=1)
# m = [12.5  8.   4. ]
sales - m   # ?
ডানে সমান করে লেখা(3,) হয়ে যায় (1, 3)
sales.shape : (3, 4)
m.shape     : (   3)
বাঁয়ে 1     : (1, 3)
শেষ মাত্রা4 বনাম 3
4 আর 3:
সমান নয়, কোনোটি 1 নয়
→ মেলে না
বাকি মাত্রা দেখার দরকারই নেই
NumPy যা বলেআসল error বার্তা
ValueError: operands
could not be broadcast
together with shapes
(3,4) (3,)
# আসলে এক লাইনে ছাপা হয়
সমাধানগড়কে কলাম বানান: (3, 1)
m_col = m[:, None]
# বা m.reshape(-1, 1)
sales - m_col
# (3, 4) − (3, 1) → (3, 4)
[[-0.5  2.5 -3.5  1.5]
 [ 0.  -2.   2.   0. ]
 [-1.   1.   0.   0. ]]
  1. উদ্দেশ্য পরিষ্কার। তিন জেলার চার দিনের অর্ডার। প্রতিটি জেলার গড় (১২.৫, ৮, ৪) সেই জেলার সারি থেকে বাদ দিতে চাই। mean(axis=1) সারি ধরে গড় দেয়, shape (3,).
  2. ডানে সমান করে লেখা। m-এর একটি মাত্রা, তাই বাঁ দিকে 1 বসে: (1, 3). মানে NumPy-র চোখে m একটি সারি, কলাম নয়।
  3. শেষ মাত্রায় আটকে গেল। sales-এর 4 (দিন) আর m-এর 3 (জেলা): সমানও নয়, 1-ও নয়। নিয়ম ২ ভাঙল।
  4. আসল error. NumPy-র বার্তায় দুটি shape-ই লেখা থাকে। Error দেখলে প্রথম কাজ দুটি shape ডান দিকে সমান করে লেখা; প্রায় সবসময় এক নজরেই কারণ ধরা পড়ে।
  5. কলাম বানিয়ে সমাধান। m[:, None]-এর shape (3, 1). এখন 4 বনাম 1 মেলে, 3 বনাম 3 মেলে, ফল (3, 4). প্রথম সারি: 12 − 12.5 = −0.5, 15 − 12.5 = 2.5, 9 − 12.5 = −3.5, 14 − 12.5 = 1.5.

Error না আসা আরও বিপজ্জনক। যদি টেবিলটি ৩ জেলা × ৩ দিন হতো, (3, 3) − (3,) কোনো error দিত না: m সারি হিসেবে টানা হতো, আর প্রথম কলাম থেকে ঢাকার গড়, দ্বিতীয় কলাম থেকে চট্টগ্রামের গড় বাদ যেত। ভুল ফল, পরিষ্কার মুখে। বর্গাকার টেবিলে broadcasting-এর উদ্দেশ্য সবসময় keepdims=True বা [:, None] দিয়ে স্পষ্ট করুন।

এবার নিয়মগুলো একটি ছোট function-এ লিখে NumPy-র সঙ্গে মিলিয়ে দেখি। predict শুধু নিয়ম তিনটি মানে; actual সত্যিই দুটি শূন্যের array যোগ করে দেখে NumPy কী করে। দুটির উত্তর প্রতিটি জোড়ায় মিললে বুঝবেন নিয়মটি ঠিক বুঝেছেন।

import numpy as np

def predict(a, b):
    """NumPy docs-এর নিয়মে ফলের shape, নয়তো কারণসহ error."""
    n = max(len(a), len(b))
    a = (1,) * (n - len(a)) + tuple(a)      # না থাকা মাত্রা = 1
    b = (1,) * (n - len(b)) + tuple(b)
    out = []
    for x, y in zip(reversed(a), reversed(b)):   # ডান দিক থেকে
        if x == y or y == 1:
            out.append(x)
        elif x == 1:
            out.append(y)
        else:
            return f"error ({x} বনাম {y})"
    return tuple(reversed(out))

def actual(a, b):                           # NumPy নিজে কী বলে
    try:
        return (np.zeros(a) + np.zeros(b)).shape
    except ValueError:
        return "ValueError"

pairs = [((3, 4), (4,)), ((3, 1), (4,)), ((5, 7), (5, 1)),
         ((5, 7), (5,)), ((2, 1, 3), (4, 1)), ((6, 1, 2), (1, 6, 2)),
         ((8, 1, 6, 1), (7, 1, 5)), ((3,), ()), ((2, 3), (3, 2)),
         ((0, 3), (1, 3))]
for a, b in pairs:
    p, r = predict(a, b), actual(a, b)
    same = p == r or (p[0] == "e" and r == "ValueError")
    print(f"{str(a):13}{str(b):10}→ {str(p):17}{str(r):13}{'মিলেছে' if same else 'মেলেনি'}")
print(np.broadcast_shapes((8, 1, 6, 1), (7, 1, 5)))

ব্রাউজারে আসল আউটপুট:

(3, 4)       (4,)      → (3, 4)           (3, 4)       মিলেছে
(3, 1)       (4,)      → (3, 4)           (3, 4)       মিলেছে
(5, 7)       (5, 1)    → (5, 7)           (5, 7)       মিলেছে
(5, 7)       (5,)      → error (7 বনাম 5) ValueError   মিলেছে
(2, 1, 3)    (4, 1)    → (2, 4, 3)        (2, 4, 3)    মিলেছে
(6, 1, 2)    (1, 6, 2) → (6, 6, 2)        (6, 6, 2)    মিলেছে
(8, 1, 6, 1) (7, 1, 5) → (8, 7, 6, 5)     (8, 7, 6, 5) মিলেছে
(3,)         ()        → (3,)             (3,)         মিলেছে
(2, 3)       (3, 2)    → error (3 বনাম 2) ValueError   মিলেছে
(0, 3)       (1, 3)    → (0, 3)           (0, 3)       মিলেছে
(8, 7, 6, 5)

দশটি জোড়ার প্রতিটিতে নিয়মের অনুমান আর NumPy-র আচরণ এক। কয়েকটি সারি ব্যাখ্যা করার মতো। (2, 1, 3) আর (4, 1): দ্বিতীয়টির বাঁয়ে 1 বসে (1, 4, 1), তারপর ডান থেকে 3↔1, 1↔4, 2↔1, ফল (2, 4, 3); দুটি array-ই কোনো না কোনো মাত্রায় টানা হয়েছে। (3,) আর (): খালি tuple মানে scalar, আর scalar সব shape-এর সঙ্গে মেলে। (2, 3) আর (3, 2): উপাদানের সংখ্যা দুটিতেই ৬, কিন্তু broadcasting উপাদান গোনে না, মাত্রা মেলায়; তাই error. শেষ লাইনে np.broadcast_shapes কোনো array না বানিয়েই ফলের shape বলে দেয়; বড় হিসাব শুরুর আগে shape যাচাইয়ের জন্য এটি সবচেয়ে সস্তা উপায়।

৫ · কলাম বানানো — np.newaxis, None, reshape(-1, 1) আর keepdims

অংশ ৪-এর সমাধানে সব কাজ হলো একটি মাপ 1-এর মাত্রা ঠিক জায়গায় বসানো। এর চারটি প্রচলিত উপায় আছে, আর চারটিই একই ফল দেয়:

লেখাx.shape = (3,) হলে ফলকখন সুবিধা
x[:, np.newaxis](3, 1) কলামপড়তে সবচেয়ে স্পষ্ট
x[:, None](3, 1) কলামছোট; np.newaxis is None সত্যি
x[None, :](1, 3) সারিকোন দিকে টানা হবে তা স্পষ্ট করতে
x.reshape(-1, 1)(3, 1) কলামscikit-learn-এ একটি feature দিতে
np.expand_dims(x, 1)(3, 1) কলামaxis একটি variable হলে
x.mean(axis=1, keepdims=True)ছেঁটে ফেলা axis 1 হয়ে থাকেaggregation-এর ফল মূল টেবিলে ফেরাতে

np.newaxis আসলে None-এরই আরেক নাম (লেখকের যন্ত্রে np.newaxis is None চালালে True); index-এর যে জায়গায় এটি বসে, সেখানে মাপ 1-এর একটি নতুন মাত্রা ঢোকে। reshape-এ -1 মানে “এই মাত্রার মাপ আপনি হিসাব করে নিন”: ১২টি উপাদানের array-তে reshape(-1, 1) দিলে (12, 1), reshape(-1, 4) দিলে (3, 4). দুটি পথেই ডেটা কপি হয় না, একটি নতুন view তৈরি হয়। আর scikit-learn-এর প্রায় সব model ইনপুট হিসেবে ২-মাত্রার টেবিল (সারি = নমুনা, কলাম = feature) চায়; একটি মাত্র feature থাকলে x.reshape(-1, 1) সেই কারণেই সবখানে দেখবেন (scikit-learn আসবে পরের মডিউলগুলোতে)।

keepdims সবচেয়ে কাজের অভ্যাস। sales.sum(axis=1)-এর shape (3,), কিন্তু sales.sum(axis=1, keepdims=True)-এর shape (3, 1): যে axis ধরে যোগ হলো, সেটি হারিয়ে না গিয়ে মাপ 1 হয়ে থাকে। ফলে ফলটি কোনো বাড়তি কাজ ছাড়াই মূল (3, 4) টেবিলের সঙ্গে ঠিক দিকে broadcast হয়, আর টেবিলটি বর্গাকার হলেও ভুল দিকে যাওয়ার সুযোগ থাকে না।

একটি পরিচিত সূত্র: “যে axis ধরে কমালেন, সেই axis-এ None বা keepdims.” সারি ধরে গড় (axis=1) নিলে ফলকে কলাম বানাতে হয় ([:, None]); কলাম ধরে গড় (axis=0) নিলে ফল এমনিতেই (4,), যা ডান দিক থেকে মিলে প্রতিটি সারিতে বসে, তাই কিছু করতে হয় না। কলামের গড় বাদ দেওয়ার কাজে এজন্যই X - X.mean(axis=0) কোনো কৌশল ছাড়াই চলে।

৬ · রোজকার তিন ছাঁচ — centering, সারির শতাংশ আর outer product

Centering — প্রতিটি জেলার নিজের গড় থেকে দূরত্ব

বাজারঘরের ম্যানেজার জানতে চান, কোন জেলায় কোন দিন “স্বাভাবিকের চেয়ে বেশি” অর্ডার এসেছে। ঢাকার ৬০টি অর্ডার আর সিলেটের ৬০টির মানে এক নয়, কারণ দুই জেলার স্বাভাবিক মাত্রা আলাদা। তাই প্রতিটি জেলার সংখ্যা থেকে সেই জেলারই গড় বাদ দেওয়া হয়। একে বলে centering: নতুন মানগুলোর গড় শূন্য, ধনাত্মক মানে গড়ের বেশি, ঋণাত্মক মানে কম। মেশিন লার্নিং-এর অনেক পদ্ধতির (যেমন PCA, আর scaling-এর পর gradient-ভিত্তিক মডেল) আগে ঠিক এই ধাপটি করা হয়, সাধারণত কলাম ধরে। নিচের ধাপে কোড চলতে দেখুন।

1sales = np.array([[12, 15, 9, 14],
  [8, 6, 10, 8], [3, 5, 4, 4]])2m = sales.mean(axis=1,
         keepdims=True)3centred = sales - m4centred.sum(axis=1)
মান (Watch)
sales.shape
সারির গড়
m.shape
মেলানো
centred
  1. টেবিল। তিন জেলা (ঢাকা, চট্টগ্রাম, সিলেট) × চার দিন, shape (3, 4). সারি = জেলা, কলাম = দিন।
  2. সারি ধরে গড়। axis=1 মানে কলামগুলো পেরিয়ে, প্রতিটি সারির ভেতরে: ঢাকা (12 + 15 + 9 + 14) / 4 = 12.5, চট্টগ্রাম 32 / 4 = 8, সিলেট 16 / 4 = 4. keepdims ছাড়া এর shape হতো (3,), যা অংশ ৪-এর error দিত।
  3. keepdims=True. একই তিনটি গড়, কিন্তু shape (3, 1), মানে একটি কলাম। এখন ডান থেকে মেলানো: 4 বনাম 1 মেলে, 3 বনাম 3 মেলে।
  4. বিয়োগ। প্রতিটি সারির চারটি ঘর থেকে সেই সারির গড় বাদ গেল। চট্টগ্রামের দ্বিতীয় দিন: 6 − 8 = −2, মানে স্বাভাবিকের চেয়ে ২টি কম। ফলের shape মূল টেবিলের মতোই (3, 4).
  5. যাচাই। Centering ঠিক হলে প্রতিটি সারির যোগফল শূন্য: −0.5 + 2.5 − 3.5 + 1.5 = 0. বড় ডেটায় float-এর কারণে শূন্যের খুব কাছের মান (যেমন 1e-15) আসতে পারে, তাই যাচাই করুন np.allclose দিয়ে।

সারির শতাংশ — প্রতিটি জেলার সপ্তাহ কোন দিনে ভারী

একই ছাঁচে ভাগ: প্রতিটি সারিকে তার নিজের যোগফল দিয়ে ভাগ করলে প্রতিটি দিনের ভাগ (শতাংশ) পাওয়া যায়, আর প্রতিটি সারির যোগফল ১০০. এবার আসল আকারে চালাই: পাঁচ জেলা × সাত দিন, seed দেওয়া অর্ডার-সংখ্যা। কোডের শুরুতে ইচ্ছে করে ভুল পথটিও আছে, যাতে আসল error বার্তাটি নিজের চোখে দেখেন।

import numpy as np
np.set_printoptions(precision=1, suppress=True)

rng = np.random.default_rng(42)
districts = ["ঢাকা", "চট্টগ্রাম", "রাজশাহী", "সিলেট", "খুলনা"]
sales = rng.integers(20, 120, size=(5, 7))      # ৫ জেলা × ৭ দিন, অর্ডার-সংখ্যা
print("sales.shape:", sales.shape)

try:                                            # ভুল চেষ্টা: (5, 7) − (5,)
    sales - sales.mean(axis=1)
except ValueError as e:
    print("ValueError:", e)

mean_col = sales.mean(axis=1, keepdims=True)    # প্রতিটি জেলার গড়, (5, 1)
print("mean_col.shape:", mean_col.shape)
centred = sales - mean_col                      # (5, 7) − (5, 1) → (5, 7)
for name, row, m in zip(districts, centred, mean_col[:, 0]):
    print(f"{name:<9} গড় {m:5.1f} →", row)
print("প্রতিটি সারির যোগফল:", centred.sum(axis=1).round(10))

pct = sales / sales.sum(axis=1, keepdims=True) * 100   # সারির শতাংশ
print("ঢাকার ৭ দিনের ভাগ (%):", pct[0])
print("প্রতিটি সারিতে মোট %:", pct.sum(axis=1))

ব্রাউজারে আসল আউটপুট:

sales.shape: (5, 7)
ValueError: operands could not be broadcast together with shapes (5,7) (5,) 
mean_col.shape: (5, 1)
ঢাকা      গড়  67.0 → [-39.  30.  18.  -4.  -4.  38. -39.]
চট্টগ্রাম গড়  76.6 → [ 12.4 -36.6 -47.6  -4.6  40.4  16.4  19.4]
রাজশাহী   গড়  75.7 → [ 15.3  22.3  -4.7 -43.7  27.3 -10.7  -5.7]
সিলেট     গড়  78.7 → [-21.7 -40.7  33.3  19.3   5.3 -18.7  23.3]
খুলনা     গড়  65.3 → [  8.7  -1.3  -0.3 -23.3 -36.3   9.7  42.7]
প্রতিটি সারির যোগফল: [ 0.  0.  0.  0. -0.]
ঢাকার ৭ দিনের ভাগ (%): [ 6.  20.7 18.1 13.4 13.4 22.4  6. ]
প্রতিটি সারিতে মোট %: [100. 100. 100. 100. 100.]

প্রথম চেষ্টায় (5, 7) আর (5,): ডান থেকে 7 বনাম 5, তাই error, আর বার্তায় দুটি shape-ই লেখা (শেষে একটি ফাঁকা জায়গাসহ, NumPy এভাবেই ছাপে)। keepdims=True দেওয়া গড়ের shape (5, 1), আর তাতে বিয়োগ ঠিক দিকে হয়েছে। ঢাকার সারি দেখুন: গড় ৬৭, আর প্রথম আর শেষ দিনে ৩৯টি কম, ষষ্ঠ দিনে ৩৮টি বেশি। প্রতিটি সারির যোগফল শূন্য; খুলনার -0. হলো float-এর খুব ছোট একটি ঋণাত্মক অবশেষ, যা round(10)-এর পরে শূন্য হলেও চিহ্নটুকু রেখে গেছে। শতাংশের লাইনে ঢাকার সাত দিনের ভাগ, যোগফল ১০০; একই এক লাইনের কোড পাঁচ জেলার জন্যই কাজ করেছে।

Outer product — প্রতিটি জোড়ার গুণফলের টেবিল

বাজারঘরের দাম-তালিকায় তিনটি পণ্য (চাল কেজি ৬০, ডাল কেজি ১২০, আলু কেজি ৪৫ টাকা) আর চারটি পরিমাণ (১, ২, ৫, ১০ কেজি)। প্রতিটি পণ্য-পরিমাণ জোড়ার দাম চাই, মানে একটি ৩ × ৪ টেবিল। দামকে কলাম আর পরিমাণকে সারি হিসেবে রাখলে broadcasting নিজেই সেই টেবিল বানায়:

price = np.array([60, 120, 45])
kg    = np.array([1, 2, 5, 10])
price[:, None] * kg          # (3, 1) × (4,) → (3, 4)
# [[  60  120  300  600]
#  [ 120  240  600 1200]
#  [  45   90  225  450]]
np.outer(price, kg)          # একই ফল

দুটি vector-এর প্রতিটি জোড়ার গুণফলের এই টেবিলের নাম outer product. দৈর্ঘ্য m আর n হলে ফল m × n. ছবি ১৫.১-এর যোগটিও আসলে একই ছাঁচ, শুধু গুণের বদলে যোগ (np.add.outer) ব্যবহার হয়। Linear algebra-য় outer product-এর ভূমিকা (rank-1 matrix, covariance) আসবে পর্ব ১৭ · NumPy-তে linear algebra-এ।

৭ · Distance matrix — ছয়টি ডেলিভারি পয়েন্ট আর মেমরির হিসাব

বাজারঘরের একজন ডেলিভারিম্যান গুদাম থেকে বের হয়ে পাঁচটি বাড়িতে যাবেন। রুট ঠিক করার আগে প্রতিটি জায়গা থেকে প্রতিটি জায়গার দূরত্ব লাগে, রিকশাওয়ালা যেমন মনে মনে জানেন কোন মোড় থেকে কোন গলি কত দূর। এই টেবিলের নাম distance matrix: সারি i, কলাম j-এর ঘরে বিন্দু i থেকে j-এর দূরত্ব। kNN, clustering, আর রুট খোঁজার মতো অনেক AI কাজের ভেতরে এই টেবিলটিই থাকে।

দুটি বিন্দু (x1, y1) আর (x2, y2)-এর সরলরেখার (Euclidean) দূরত্ব:

d = √((x1 − x2)2 + (y1 − y2)2)

এখানে √ মানে বর্গমূল। n টি বিন্দুর shape (n, 2) , প্রতিটি সারিতে x আর y. কৌশলটি হলো একই array-কে দুইভাবে দেখা: P[:, None, :]-এর shape (n, 1, 2) (“প্রতিটি i”), আর P[None, :, :]-এর shape (1, n, 2) , মানে “প্রতিটি j”. বিয়োগ করলে ডান থেকে 2↔2, 1↔n, n↔1, ফল (n, n, 2): প্রতিটি জোড়ার x আর y-এর পার্থক্য। তারপর বর্গ, শেষ axis ধরে যোগ, বর্গমূল, আর ফল (n, n). ছয়টি বিন্দু নিয়ে চালিয়ে দেখি, double loop-এর সঙ্গে মিলিয়ে, আর তারপর ১০০০টি বিন্দুতে মেমরি মাপি।

import math
import numpy as np
np.set_printoptions(precision=2, suppress=True)

# ছয়টি ডেলিভারি পয়েন্ট, গুদাম থেকে (x, y) কিলোমিটারে
P = np.array([[0.0, 0.0], [3.0, 4.0], [6.0, 0.0],
              [1.0, 7.0], [8.0, 6.0], [2.0, 2.0]])

diff = P[:, None, :] - P[None, :, :]           # (6,1,2) − (1,6,2) → (6,6,2)
D = np.sqrt((diff ** 2).sum(axis=-1))          # (6, 6)
print("diff.shape:", diff.shape, " D.shape:", D.shape)
print(D)

L = [[math.dist(a, b) for b in P.tolist()] for a in P.tolist()]
print("double loop-এর সঙ্গে মিলেছে?", np.allclose(D, L))
print("সমমিত?", np.allclose(D, D.T), " কর্ণ শূন্য?", np.all(np.diag(D) == 0))

m = 1000                                       # ১০০০টি পয়েন্ট হলে
Q = np.random.default_rng(42).random((m, 2))
big = Q[:, None, :] - Q[None, :, :]           # মাঝের অস্থায়ী array
Dbig = np.sqrt((big ** 2).sum(axis=-1))
for name, arr in [("পয়েন্ট Q", Q), ("মাঝের big", big), ("ফল Dbig", Dbig)]:
    print(f"{name:10}{str(arr.shape):15}{arr.nbytes:>12,} বাইট")

ব্রাউজারে আসল আউটপুট:

diff.shape: (6, 6, 2)  D.shape: (6, 6)
[[ 0.    5.    6.    7.07 10.    2.83]
 [ 5.    0.    5.    3.61  5.39  2.24]
 [ 6.    5.    0.    8.6   6.32  4.47]
 [ 7.07  3.61  8.6   0.    7.07  5.1 ]
 [10.    5.39  6.32  7.07  0.    7.21]
 [ 2.83  2.24  4.47  5.1   7.21  0.  ]]
double loop-এর সঙ্গে মিলেছে? True
সমমিত? True  কর্ণ শূন্য? True
পয়েন্ট Q (1000, 2)            16,000 বাইট
মাঝের big (1000, 1000, 2)  16,000,000 বাইট
ফল Dbig   (1000, 1000)      8,000,000 বাইট

প্রথম সারিটি হাতে মেলানো যায়: গুদাম (0, 0) থেকে (3, 4) ঠিক ৫ কিলোমিটার (৩-৪-৫ ত্রিভুজ), (6, 0) ৬, (8, 6) ১০, আর (1, 7) √50 ≈ 7.07. ৩৬টি ঘরের প্রতিটি math.dist-এর double loop-এর সঙ্গে মিলেছে। টেবিলটি সমমিত (i থেকে j আর j থেকে i একই দূরত্ব) আর কর্ণে শূন্য (নিজের থেকে নিজের দূরত্ব), যা যেকোনো distance matrix-এর সহজ যাচাই।

শেষ তিন লাইনই এই অংশের আসল শিক্ষা। ১০০০টি বিন্দু রাখতে লাগে মাত্র ১৬,০০০ বাইট (1000 × 2 × 8, কারণ float64-এর প্রতিটি সংখ্যা ৮ বাইট)। কিন্তু মাঝের big array-টি 1000 × 1000 × 2 × 8 = ১,৬০,০০,০০০ বাইট, মানে ইনপুটের এক হাজার গুণ। Broadcasting ইনপুট কপি করেনি, কিন্তু বিয়োগের ফল তো প্রতিটি জোড়ার জন্য সত্যিই আলাদা সংখ্যা, তাই সেটি পুরো মেমরি নেয়। big ** 2 আরেকটি সমান মাপের অস্থায়ী array বানায়, যা দেখা যায় না, কিন্তু মেমরি নেয়।

এখান থেকে একটি সরল হিসাব। n টি বিন্দুতে মাঝের array n × n × 2 × 8 বাইট, মানে n2-এর সঙ্গে বাড়ে। বিন্দু ১০ গুণ (১০,০০০) হলে মেমরি ১০০ গুণ: ১৬০ কোটি বাইট, প্রায় ১.৬ GB, শুধু একটি অস্থায়ী array-র জন্য। আরও ১০ গুণ (১ লাখ বিন্দু) হলে ১৬০ GB, যা সাধারণ ল্যাপটপে বরাদ্দই হয় না। লেখকের যন্ত্রে (Python 3.14.4, NumPy 2.5.1) এমন একটি array চাইলে এই সত্যিকারের MemoryError এসেছিল:

>>> np.zeros((100_000, 100_000, 2))
MemoryError: Unable to allocate 149. GiB for an array with shape (100000, 100000, 2) and data type float64

১৬০ GB আর ১৪৯ GiB একই পরিমাণ: GiB-তে ১ GiB = 230 বাইট। বড় ডেটায় তিনটি প্রচলিত পথ আছে। এক, ভাগে ভাগে (chunk) হিসাব: একবারে ১০০০টি সারি নিয়ে (1000, n) টুকরো বানানো। দুই, অস্থায়ী array এড়ানোর গাণিতিক কৌশল: ‖a − b‖2 = ‖a‖2 + ‖b‖2 − 2 a·b, যেখানে ‖a‖ মানে vector-এর দৈর্ঘ্য আর a·b হলো dot product; এতে (n, n, 2) নয়, সরাসরি (n, n) তৈরি হয় (dot product আর norm আসবে পর্ব ১৭-এ)। তিন, তৈরি টুল: SciPy-র scipy.spatial.distance.cdist, বা scikit-learn-এর pairwise_distances_chunked, যা ভাগে ভাগে কাজ করে। আর অনেক সময় পুরো টেবিলের দরকারই নেই: “প্রতিটি বাড়ির সবচেয়ে কাছের ৩টি বাড়ি” চাইলে KD-tree-র মতো কাঠামো পুরো n × n টেবিল না বানিয়েই উত্তর দেয়।

৮ · np.vectorize-এর সত্য, আর কখন loop-ই ঠিক

নাম শুনে মনে হয় np.vectorize যেকোনো Python function-কে vectorized করে দ্রুত বানিয়ে দেয়। আসলে তা করে না। NumPy-র docs-এ numpy.vectorize-এর পাতায় স্পষ্ট লেখা আছে যে এটি মূলত সুবিধার জন্য, গতির জন্য নয়, আর এর ভেতরের কাজ মূলত একটি for loop. অর্থাৎ এটি আপনার function-কে প্রতিটি উপাদানের জন্য একবার করে ডাকে, প্রতিবার Python-এর পুরো খরচসহ। অংশ ২-এর মাপে ঠিক এটিই দেখেছেন: np.vectorize-এর সময় comprehension-এর কাছাকাছি (ব্রাউজারের run-এ একটু বেশিও), আর array expression-এর বহু গুণ।

তাহলে এর কাজ কী? এটি broadcasting-এর সুবিধা দেয়: scalar-এর জন্য লেখা একটি function-কে আপনি সরাসরি array, এমনকি ভিন্ন shape-এর array দিয়েও ডাকতে পারেন, আর ফল আসে NumPy array হিসেবে। ছোট ডেটায়, পরীক্ষার কোডে, বা যেখানে function-টির ভেতরে জটিল if-else আর বাইরের library ডাকা আছে, সেখানে এটি সুবিধার। গতির জন্য আসল পথ হলো function-টিকে NumPy-র ufunc আর np.where দিয়ে নতুন করে লেখা।

✅ Array expression যেখানে ভালো

একই সূত্র অনেক সারিতে, যেমন দাম, ভ্যাট, শতাংশ, দূরত্ব; সারিগুলো একে অপরের ওপর নির্ভর করে না; ডেটা বড়।

উদাহরণ: এক লাখ অর্ডারের মোট

🔁 সাধারণ loop যেখানে ঠিক

ডেটা ছোট (কয়েকশো সারি); প্রতিটি ধাপ আগের ধাপের ফলের ওপর নির্ভর করে; জটিল শর্ত আর আগেভাগে থেমে যাওয়া আছে; পড়ার সহজতা বেশি জরুরি।

উদাহরণ: স্টক শেষ হলে অর্ডার থামানো

⚠️ Broadcasting যেখানে ক্ষতিকর

মাঝের array n2 বা আরও দ্রুত বাড়ে আর মেমরি ছাড়িয়ে যায়; তখন chunk, গাণিতিক কৌশল বা বিশেষ টুল।

উদাহরণ: ১ লাখ বিন্দুর distance matrix

কখন loop-ই ঠিক, তার একটি পরিষ্কার উদাহরণ: বাজারঘরের গুদামে চালের স্টক ৫০০ কেজি, অর্ডারগুলো একে একে আসে, আর স্টক শূন্যের নিচে নামার আগের অর্ডারগুলোই শুধু পূরণ হবে। প্রতিটি অর্ডারের সিদ্ধান্ত আগের সব অর্ডারের ওপর নির্ভর করে। এমন কিছু কাজ np.cumsum-এর মতো কৌশলে vectorize করা যায়, কিন্তু নিয়ম জটিল হলে (ফেরত, আংশিক পূরণ, অগ্রাধিকার) সাধারণ loop-ই পরিষ্কার আর যথেষ্ট দ্রুত। আর loop-কেও দ্রুত করার পথ আছে (Numba-র মতো compiler), যার আলোচনা, মাপজোক আর memory layout আসবে পর্ব ১৯ · NumPy-র গতি — memory, dtype, view আর কখন loop-ই ঠিক-এ। সাধারণ মাপজোকের নিয়ম আছে Python কোর্সের পারফরম্যান্স পর্বে।

আরও গভীরে। np.einsum (Einstein summation, NumPy 1.6 থেকে আছে) একটি ছোট লেখায় গুণ আর যোগের হিসাব প্রকাশ করে: np.einsum('ij,ij->i', s, s) মানে “i আর j ধরে গুণ, তারপর j ধরে যোগ”, অর্থাৎ প্রতিটি সারির বর্গের যোগফল, (s * s).sum(axis=1)-এর সমান; 'i,j->ij' হলো outer product. সুবিধা হলো এটি অনেক সময় মাঝের বড় array না বানিয়েই কাজ সারে, আর optimize=True দিলে কয়েকটি array-র গুণের ক্রম নিজে বেছে নেয়। Broadcasting-এর ধারণার শিকড় পুরোনো: ১৯৬০-এর দশকে Kenneth Iverson-এর APL ভাষা পুরো array-র ওপর একটি চিহ্নে কাজ করত, আর NumPy-র আগের Numeric package-এই broadcasting-এর নিয়ম ছিল বলে প্রচলিত বিবরণ বলে। নিয়মের সরকারি বর্ণনা NumPy docs-এর “Broadcasting” আর “Universal functions” পাতায়।

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):

  • Broadcasting-এর নিয়ম কী? Shape ডান দিক থেকে মেলানো; দুটি মাত্রা মেলে যদি সমান বা একটি 1 হয়; কম মাত্রার array-র বাঁয়ে না থাকা মাত্রা 1 ধরা হয়।
  • (4, 3) + (4,) কেন error? ডান থেকে 3 বনাম 4 মেলে না; সারি ধরে যোগ চাইলে b[:, None] দিয়ে (4, 1).
  • (5, 1) আর (1, 6)-এর ফলের shape? (5, 6); দুটিই টানা হয়, আর এটিই outer product-এর ছাঁচ।
  • np.vectorize কি কোড দ্রুত করে? না; ভেতরে Python loop চলে। এটি broadcasting-এর সুবিধা দেয়, গতি নয়।
  • Broadcasting কি মেমরিতে কপি বানায়? টানা ইনপুটের কপি নয় (stride 0), কিন্তু ফল আর মাঝের অস্থায়ী array পুরো মাপে তৈরি হয়।

৯ · AI-এর যুগে — AI সহকারীর লেখা array-কোড যাচাই

টেবিলের প্রতিটি কলাম standardize করার বা বিন্দুগুলোর দূরত্ব বের করার কোড চাইলে একটি AI সহকারী কয়েক সেকেন্ডে NumPy কোড দেবে। বেশির ভাগ সময় তা চলবে। কিন্তু broadcasting-এর ভুল প্রায়ই error দেয় না, ভুল shape-এর একটি বিশ্বাসযোগ্য ফল দেয়। AI সহকারীরা প্রায়ই যা করে:

  • (n,) আর (n, 1) মিশিয়ে ফেলে: y_true একটি কলাম আর y_pred একটি সারি হলে ((y_true - y_pred) ** 2).mean() একটি n × n টেবিলের গড় দেয়, কোনো error ছাড়াই। লেখকের যন্ত্রে ৪টি মানের এমন উদাহরণে আসল ফল 0.0, কিন্তু কোড ফেরত দিয়েছিল 2.5.
  • বর্গাকার টেবিলে axis উল্টে দেয়, বা keepdims ছাড়া সারির গড় বাদ দেয়; ফল ভুল, কিন্তু shape ঠিক দেখায়।
  • “দ্রুত করতে” np.vectorize বসায়, বা বড় ডেটায় (n, n, d)-এর অস্থায়ী array বানায়, যা আপনার ডেটায় মেমরি ছাড়িয়ে যাবে।
  • পুরোনো API লেখে (NumPy 1.24-এ সরানো np.float, np.int, বা নতুন কোডে অনুৎসাহিত np.matrix-নির্ভর কোড), বা নেই এমন parameter বানায়; আর error চাপা দিতে np.seterr(all="ignore") বা warnings.filterwarnings("ignore") বসায়।
  • গতির দাবি করে মাপ ছাড়াই (“এটি ১০০ গুণ দ্রুত”), আর train আর test ডেটা একসঙ্গে রেখে পুরো ডেটার গড় দিয়ে centering করে, যা পরে মডেলের মাপকে ভুলভাবে ভালো দেখায় (data leakage; split আগে, তারপর শুধু train-এর গড়)।

কোড চাওয়ার সময় shape আর যাচাইয়ের পথ নিজেই বলে দিন। নিচের প্রম্পটে খেয়াল করুন: আসল ডেটা নেই, শুধু shape আর একটি বানানো ছোট উদাহরণ; প্রতিটি লাইনের shape চাওয়া হয়েছে; আর loop-এর সঙ্গে মিলিয়ে দেখার test চাওয়া হয়েছে।

আমি Python 3.14 আর NumPy 2.x ব্যবহার করছি। আমার কাছে X নামে একটি float64 array আছে, shape (n, d), যেখানে n প্রায় ৫০,০০০ আর d = 8. (আসল ডেটা দেব না; বানানো উদাহরণ: X = np.arange(12.0).reshape(4, 3).) ১. প্রতিটি কলামের গড় বাদ দিয়ে আর standard deviation দিয়ে ভাগ করে standardize করার কোড লেখো, loop ছাড়া। ২. প্রতিটি লাইনের পাশে comment-এ ফলের shape লেখো, আর কোথায় broadcasting হচ্ছে, কোন নিয়মে, এক লাইনে বলো। ৩. কোনো কলামের standard deviation শূন্য হলে কী করবে, স্পষ্ট বলো; warning চাপা দেবে না। ৪. বানানো উদাহরণের জন্য একটি সাধারণ double loop লিখে np.allclose দিয়ে মিলিয়ে দেখার ছোট test দাও। ৫. মাঝে n × n বা তার চেয়ে বড় কোনো অস্থায়ী array তৈরি হলে আমাকে সতর্ক করো, আর মোট মেমরি আনুমানিক কত লাগবে, বাইটে হিসাব করে দেখাও। কোনো function বা parameter নিয়ে নিশ্চিত না হলে বলো, বানিয়ে লিখবে না।

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

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

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

প্র ০১“Array-র ভাষায় ভাবা” কোথা থেকে এলো? APL থেকে NumPy পর্যন্ত এই ধারণার পথ কী শেখায়?

পুরো array-র ওপর একটি চিহ্নে হিসাব করার ধারণাটি NumPy-র চেয়ে অনেক পুরোনো। Kenneth Iverson ১৯৬২ সালে “A Programming Language” নামে একটি বই প্রকাশ করেন, আর সেখান থেকেই APL ভাষার নাম। APL-এ দুটি সংখ্যার তালিকা যোগ করতে loop লাগত না; একটি চিহ্নেই পুরো তালিকা। Iverson ১৯৭৯ সালে Turing Award পান, আর তাঁর ভাষণের শিরোনাম ছিল “Notation as a Tool of Thought”: ভালো notation শুধু লেখা ছোট করে না, চিন্তাকেও বদলায়। এই ধারার উত্তরসূরি হিসেবে প্রায়ই MATLAB, R আর NumPy-র নাম আসে, যদিও প্রতিটির নিজস্ব ইতিহাস আছে আর প্রভাবের মাত্রা নিয়ে বিবরণ সব জায়গায় এক নয়।

একই সময়ে হার্ডওয়্যারেও একই দিকে যাত্রা। ১৯৭০-এর দশকের Cray-1-এর মতো “vector” supercomputer একটি নির্দেশে সংখ্যার একটি সারির ওপর কাজ করত; আজকের সাধারণ CPU-তে SIMD নির্দেশ আর GPU-তে হাজারো ছোট core একই ধারণার নতুন রূপ। “Vectorization” শব্দের দুই অর্থ (array expression আর CPU-র নির্দেশ) এই দুই ইতিহাসের মিলন।

এই পথ থেকে তিনটি শিক্ষা। এক, ভাষা যে চিন্তা সহজ করে, মানুষ সেই চিন্তাই বেশি করে। Python-এর loop-এ “প্রতিটি অর্ডারের জন্য” ভাবতে হয়; NumPy-তে “দাম-কলাম গুণ পরিমাণ-কলাম” ভাবতে হয়, আর দ্বিতীয়টি ডেটার পুরো ছবি একসঙ্গে দেখতে শেখায়। দুই, array-চিন্তার একটি দাম আছে: APL-এর ছোট কোড বাইরের মানুষের কাছে প্রায় দুর্বোধ্য বলে সমালোচিত হতো, আর অতি চালাক NumPy-র এক লাইনও একই সমস্যা তৈরি করে। তিন, deep learning-এর framework-গুলো (PyTorch, JAX) মূলত একই array-ভাষা GPU-তে চালায়, broadcasting-এর প্রায় একই নিয়মসহ; তাই এই পর্বের নিয়ম পরের কোর্সগুলোতেও কাজে লাগবে।

মূল উপলব্ধি: Vectorization শুধু গতির কৌশল নয়, ডেটা নিয়ে ভাবার একটি notation; সেটি শক্তিশালী, কিন্তু পড়ার সহজতা রক্ষা করাও প্রোগ্রামারেরই দায়িত্ব।

প্র ০২Broadcasting ডান দিক থেকে মেলায় কেন, বাঁ দিক থেকে নয়? অন্য টুলে একই প্রশ্নের উত্তর কী?

ডান থেকে মেলানোর পেছনে একটি ব্যবহারিক যুক্তি আছে। ডেটার টেবিলে প্রচলিত রীতি হলো সারি = নমুনা (অর্ডার, ক্রেতা, ছবি) আর শেষ মাত্রা = বৈশিষ্ট্য (feature, যেমন ছবির রঙের ভাগ)। নমুনার সংখ্যা বদলায়, কিন্তু প্রতিটি নমুনার বৈশিষ্ট্যের সংখ্যা স্থির। তাই “প্রতিটি feature-এর একটি মান” (যেমন কলামের গড়) shape (d,) হিসেবে স্বাভাবিকভাবেই (n, d)-এর সঙ্গে মেলে, আবার (batch, height, width, 3)-এর ছবির স্তূপেও (3,) সরাসরি মেলে। বাঁ থেকে মেলালে প্রতিবার নমুনার সংখ্যা জানতে হতো। তা ছাড়া NumPy-র default মেমরি-ক্রম C-ধাঁচের, যেখানে শেষ মাত্রার ঘরগুলো মেমরিতে পাশাপাশি থাকে; শেষ মাত্রাকে “ভেতরের” মাত্রা ধরার এই রীতির সঙ্গে ডান থেকে মেলানো সহজে খাপ খায়।

অন্য টুলে ছবি ভিন্ন। MATLAB দীর্ঘদিন ভিন্ন মাপের matrix-এ এমন যোগ নিজে থেকে করত না; এর জন্য bsxfun নামে আলাদা function লাগত। পরে R2016b সংস্করণে “implicit expansion” আসে, যা NumPy-র broadcasting-এর মতো কাজ করে। সেই পরিবর্তন নিয়ে ব্যবহারকারীদের মধ্যে আলোচনা হয়েছিল, কারণ যে কোড আগে error দিত, তা এখন নীরবে একটি বড় matrix বানাতে পারত। PyTorch, JAX আর TensorFlow মোটামুটি NumPy-র নিয়মই নিয়েছে, যাতে কোড এক টুল থেকে আরেকটিতে সহজে যায়।

সমালোচনাও আছে। Shape-এর অবস্থান দিয়ে মাত্রা চেনা ভঙ্গুর: কেউ ভুলে একটি transpose করলে, বা (n,)-এর জায়গায় (n, 1) এলে, broadcasting ভুল দিকে চলে যায়। তাই কিছু টুল নাম-দেওয়া মাত্রা ব্যবহার করে, যেমন xarray-তে মাত্রার নাম “district” বা “day”, আর মেলানো হয় নাম ধরে, অবস্থান ধরে নয়। PyTorch-এও “named tensors” নামে একটি পরীক্ষামূলক সুবিধা এসেছিল। কোন পথ ভালো, তা নিয়ে মত ভিন্ন: অবস্থানভিত্তিক নিয়ম ছোট আর দ্রুত, নামভিত্তিক নিয়ম নিরাপদ কিন্তু বেশি লেখা লাগে।

মূল উপলব্ধি: ডান থেকে মেলানো “শেষ মাত্রা = বৈশিষ্ট্য” রীতির ওপর দাঁড়ানো একটি নকশা-সিদ্ধান্ত; এটি সুবিধার, কিন্তু রীতি ভাঙলে নীরব ভুলের দরজাও খোলে।

প্র ০৩Broadcasting-এর সবচেয়ে বিপজ্জনক ভুল কেন error দেয় না? একটি মডেলের মাপকাঠি এতে কীভাবে ভুল হতে পারে?

ধরুন একটি মডেল ডেলিভারির সময় অনুমান করছে। আসল সময় y_true এসেছে একটি টেবিলের কলাম থেকে, shape (n, 1), আর মডেলের অনুমান y_pred shape (n,). Mean squared error লিখলেন ((y_true - y_pred) ** 2).mean(). ডান থেকে মেলানো: 1 বনাম n মেলে, n বনাম 1 মেলে, ফল (n, n). অর্থাৎ প্রতিটি আসল মানের সঙ্গে প্রতিটি অনুমানের পার্থক্য, আর গড় হলো সেই n2টি সংখ্যার। ফলটি একটি সংখ্যা, দেখতে স্বাভাবিক, কোনো error বা warning নেই। লেখকের যন্ত্রে ৪টি মান দিয়ে এমন একটি উদাহরণে নিখুঁত অনুমানেও (আসল আর অনুমান হুবহু এক) ফল এসেছিল 2.5, যেখানে সঠিক উত্তর 0.

এই ভুল ধরা কঠিন কেন? কারণ broadcasting-এর নিয়ম ইচ্ছে করেই উদার: যে জোড়া গাণিতিকভাবে অর্থপূর্ণ হতে পারে, NumPy তা চালিয়ে দেয়। কোন shape আপনার উদ্দেশ্য, NumPy তা জানে না। n বড় হলে আরেকটি লক্ষণ দেখা দেয়: হঠাৎ মেমরি বা সময় অনেক বেড়ে যাওয়া, কারণ একটি n × n টেবিল তৈরি হচ্ছে; ৫০,০০০ সারিতে float64-এ সেটি ২০ GB, যা প্রায়ই MemoryError দিয়ে ধরা পড়ে। কিন্তু ছোট test ডেটায় সব চুপচাপ চলে যায়।

প্রতিরোধের অভ্যাসগুলো সরল। এক, লক্ষ্য আর অনুমানকে সবসময় একই shape-এ আনা, সাধারণত ravel() বা reshape(-1) দিয়ে ১-মাত্রায়। দুই, মাপের আগে assert y_true.shape == y_pred.shape. তিন, নিজে না লিখে পরীক্ষিত function ব্যবহার করা; scikit-learn-এর metric function-গুলো ইনপুটের shape যাচাই করে, আর অমিল হলে error দেয়। চার, অন্তত একটি ক্ষেত্রে উত্তর জানা ডেটায় (যেমন নিখুঁত অনুমান, যার error শূন্য হওয়ার কথা) function-টি চালিয়ে দেখা। এই শেষ অভ্যাসটি পর্ব ১১-এর ছোট test-এর ধারণা।

মূল উপলব্ধি: Broadcasting আপনার উদ্দেশ্য জানে না, শুধু shape জানে; তাই shape-এর assert আর উত্তর জানা ছোট test-ই নীরব ভুলের প্রধান পাহারাদার।

প্র ০৪Distance matrix-এর ‖a‖² + ‖b‖² − 2a·b কৌশল মেমরি বাঁচায়; তাহলে সবসময় এটিই ব্যবহার করা হয় না কেন?

অংশ ৭-এ দেখেছেন, সরাসরি broadcasting-এ মাঝের array (n, n, d), যেখানে d হলো প্রতিটি বিন্দুর মাত্রা। ২-মাত্রার মানচিত্রে d = 2, কিন্তু AI-তে প্রায়ই d কয়েকশো: একটি লেখার embedding-এ ৩৮৪ বা ৭৬৮টি সংখ্যা থাকা সাধারণ। তখন n = 10,000 হলেই মাঝের array কয়েকশো GB. বিস্তার-সূত্র ‖a − b‖2 = ‖a‖2 + ‖b‖2 − 2 a·b দিয়ে কাজটি হয় একটি matrix গুণ (X @ X.T, shape (n, n)) আর দুটি ছোট vector দিয়ে; d যত বড়ই হোক, মাঝের array (n, n)-এর বেশি নয়। আর matrix গুণ BLAS নামের অত্যন্ত দ্রুত library-তে চলে। scikit-learn-এর euclidean_distances-এর docs-এও এই কৌশলের কথা বলা আছে।

তবু এটি সবসময় নিরাপদ নয়, কারণ float-এর হিসাব। দুটি বিন্দু খুব কাছাকাছি হলে ‖a‖2 + ‖b‖2 আর 2 a·b দুটিই বড় আর প্রায় সমান সংখ্যা, আর তাদের বিয়োগে অঙ্কের বেশির ভাগ নির্ভুলতা হারিয়ে যায়। একে বলে catastrophic cancellation. ফল হিসেবে দূরত্বের বর্গ সামান্য ঋণাত্মক আসতে পারে, যার বর্গমূল nan; আর নিজের থেকে নিজের দূরত্ব ঠিক শূন্য না এসে ক্ষুদ্র একটি সংখ্যা আসতে পারে। ব্যবহারিক কোডে তাই ঋণাত্মক মানকে শূন্যে কেটে দেওয়া হয় (np.maximum(d2, 0)), আর scikit-learn-এর docs নিজেই সতর্ক করে যে এই পদ্ধতি সবচেয়ে নির্ভুল নয়। SciPy-র cdist সরাসরি পার্থক্য থেকে হিসাব করে, তাই সাধারণত বেশি নির্ভুল, কিন্তু বড় d-তে ধীর হতে পারে।

অর্থাৎ এখানে তিনটি জিনিসের টানাটানি: মেমরি, গতি আর নির্ভুলতা। ছোট n-এ সরাসরি broadcasting সবচেয়ে সরল আর নির্ভুল; বড় n আর বড় d-তে matrix গুণের কৌশল; খুব বড় n-এ chunk, বা পুরো টেবিল না বানিয়ে KD-tree বা approximate nearest neighbour-এর মতো পদ্ধতি। এর গাণিতিক আলোচনা আসবে পর্ব ১৭ আর পর্ব ৩৩ · Vector আর similarity-তে।

মূল উপলব্ধি: একই গাণিতিক ফলের কয়েকটি কম্পিউটার-রূপ থাকে, আর প্রতিটির মেমরি, গতি আর নির্ভুলতার হিসাব আলাদা; ডেটার মাপ দেখেই রূপটি বাছতে হয়।

প্র ০৫লম্বা array expression-এ অস্থায়ী array-র খরচ কতটা? numexpr, Numba বা einsum কি এর সমাধান?

price * qty * (1 - disc) * (1 + VAT) লিখলে NumPy বাঁ থেকে ডানে একটি একটি করে কাজ করে: আগে price * qty-এর জন্য একটি পুরো array, তারপর 1 - disc-এর জন্য আরেকটি, তারপর দুটির গুণফলের জন্য আরেকটি। এক লাখ অর্ডারে প্রতিটি অস্থায়ী array ৮ লাখ বাইট, তেমন কিছু নয়। কিন্তু দশ কোটি সারিতে প্রতিটি ৮০ কোটি বাইট, আর তার ওপর প্রতিটি অস্থায়ী array একবার মেমরিতে পুরোটা লেখা আর আবার পুরোটা পড়া হয়। আধুনিক CPU-তে হিসাবের চেয়ে মেমরি থেকে ডেটা আনাই প্রায়ই বেশি সময় নেয়, তাই অনেক ধাপের একটি expression-এ আসল খরচ গুণ-যোগ নয়, মেমরির আসা-যাওয়া।

এর কয়েকটি প্রচলিত সমাধান আছে, প্রতিটির নিজস্ব দাম। NumPy-র ভেতরেই out= parameter দিয়ে ফল একটি আগে থেকে রাখা array-তে লেখা যায় (np.multiply(a, b, out=buf)), আর *=-এর মতো in-place কাজ নতুন array বানায় না; কোড একটু কম পড়ার মতো হয়। numexpr নামের library একটি expression-কে string হিসেবে নিয়ে ছোট ছোট খণ্ডে হিসাব করে, যাতে অস্থায়ী ফলগুলো CPU-র cache-এ থেকে যায়; pandas-এর eval আর query ভেতরে এটি ব্যবহার করতে পারে। Numba একটি সাধারণ Python loop-কে compile করে machine code বানায়, ফলে loop-এর ভেতরেই সব হিসাব হয়, কোনো অস্থায়ী array ছাড়া। আর einsum নির্দিষ্ট গুণ-যোগের ছাঁচে (যেমন সারির বর্গের যোগফল) মাঝের array এড়াতে পারে।

কিন্তু এগুলো প্রথম পদক্ষেপ নয়। বেশির ভাগ কাজে সাধারণ array expression যথেষ্ট দ্রুত আর সবচেয়ে পরিষ্কার। ঠিক পথ হলো আগে পরিষ্কার কোড, তারপর মাপ (time.perf_counter, মেমরির জন্য nbytes), আর কেবল যেখানে মাপে সত্যিই সমস্যা দেখা যায়, সেখানে বিশেষ টুল। “Premature optimization” নিয়ে Donald Knuth-এর বহুল উদ্ধৃত সতর্কবাণী এখানেও খাটে। মাপের পুরো পদ্ধতি পর্ব ১৯-এর বিষয়।

মূল উপলব্ধি: বড় ডেটায় array expression-এর খরচ প্রায়ই মেমরির আসা-যাওয়ায়; বিশেষ টুল আছে, কিন্তু মাপ ছাড়া সেগুলো ধরা অকারণ জটিলতা।

অনুশীলন

প্রথমটি shape আর মেমরির হিসাব, দ্বিতীয়টি আউটপুট অনুমান, তৃতীয়টি নকশা ও সমালোচনা, শেষটি কোড লেখার।

  1. হিসাব: (ক) নিচের প্রতিটি জোড়ার ফলের shape বলুন, অথবা কোন মাত্রায় error: (7, 1) আর (1, 5); (4, 3, 2) আর (3, 1); (4, 3, 2) আর (4, 1); (10,) আর (10, 1). (খ) ২০০০টি বিন্দু, প্রতিটিতে ৩টি float64 সংখ্যা। P[:, None, :] - P[None, :, :]-এর shape আর মেমরি কত বাইট?

    (ক) (7, 1) আর (1, 5) → ডান থেকে 1↔5, 7↔1, ফল (7, 5). (4, 3, 2) আর (3, 1) → দ্বিতীয়টি (1, 3, 1); 2↔1, 3=3, 4↔1, ফল (4, 3, 2). (4, 3, 2) আর (4, 1) → দ্বিতীয়টি (1, 4, 1); 2↔1 ঠিক, তারপর 3 বনাম 4: error. (10,) আর (10, 1) → প্রথমটি (1, 10); 10↔1, 1↔10, ফল (10, 10), কোনো error নেই, আর প্রায়ই এটিই অংশ ৯-এর নীরব ভুল।

    (খ) Shape (2000, 2000, 3). উপাদান 2000 × 2000 × 3 = ১,২০,০০,০০০টি, প্রতিটি ৮ বাইট, মোট ৯,৬০,০০,০০০ বাইট, প্রায় ৯৬ MB , মানে প্রায় ৯১.৬ MiB. বিন্দুগুলো রাখতে লাগে মাত্র 2000 × 3 × 8 = ৪৮,০০০ বাইট। নিজের যন্ত্রে np.zeros((2000, 2000, 3)).nbytes লিখে মিলিয়ে নিতে পারেন।

  2. আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি লাইনের আউটপুট লিখুন, তারপর এই পাতার যেকোনো কোড-বাক্সে বসিয়ে মিলিয়ে দেখুন।
    import numpy as np
    a = np.array([1, 2, 3])
    print(a[:, None] * a)
    print((a[:, None] - a).sum())
    print(np.ones((2, 3)) + np.array([[10], [20]]))
    print((a.reshape(-1, 1) + np.zeros(4)).shape)
    [[1 2 3]
     [2 4 6]
     [3 6 9]]
    0
    [[11. 11. 11.]
     [21. 21. 21.]]
    (3, 4)

    প্রথম লাইন (3, 1) × (3,) → (3, 3), গুণের নামতা, মানে একটি outer product. দ্বিতীয় লাইনে ঘর (i, j)-তে ai − aj; প্রতিটি ধনাত্মক পার্থক্যের সঙ্গে তার ঋণাত্মক জোড়া আছে (কর্ণের দুই পাশে), তাই যোগফল 0. তৃতীয় লাইন (2, 3) + (2, 1) → প্রতিটি সারিতে তার নিজের সংখ্যা যোগ; np.ones float দেয়, তাই 11.. শেষ লাইন (3, 1) + (4,) → (3, 4), ছবি ১৫.১-এর ছাঁচ।

  3. নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) বাজারঘরের ক্রেতাদের feature টেবিল standardize করার জন্য নিচের কোড দিয়েছেন। X-এর shape (n, 4), y-এর (n, 1). অন্তত চারটি সমস্যা চিহ্নিত করে ভালো সংস্করণ লিখুন।
    import warnings; warnings.filterwarnings("ignore")
    mu = X.mean(axis=1)
    sd = X.std(axis=1)
    Z = (X - mu) / sd
    pred = Z[:, 0] * 0.5
    err = ((y - pred) ** 2).mean()
    • ভুল axis: feature standardize করতে কলাম ধরে (axis=0) গড় লাগে; axis=1 প্রতিটি ক্রেতার চারটি ভিন্ন এককের সংখ্যার গড় নেয়, যার কোনো অর্থ নেই।
    • Shape-এর ভাগ্য: mu-এর shape (n,); (n, 4) − (n,) সাধারণত error দেবে, কিন্তু n = 4 হলে নীরবে ভুল দিকে চলবে।
    • শূন্য দিয়ে ভাগ: কোনো কলামের সব মান সমান হলে sd শূন্য, ফল nan বা inf; আর warning চাপা দেওয়ায় তা কেউ দেখবে না।
    • নীরব n × n: y (n, 1) আর pred (n,), তাই y - pred একটি (n, n) টেবিল; error-এর মান সম্পূর্ণ ভুল, আর বড় n-এ মেমরিও ফুরাবে।
    • Leakage-এর ঝুঁকি: পুরো ডেটার গড় নেওয়া হয়েছে; train আর test আলাদা হলে গড় আর sd শুধু train থেকে নিতে হয়।
    mu = X_train.mean(axis=0)                 # (4,)
    sd = X_train.std(axis=0)                  # (4,)
    sd = np.where(sd == 0, 1.0, sd)           # স্থির কলাম: ভাগ না করে রাখা
    Z = (X_test - mu) / sd                    # (m, 4) − (4,) → (m, 4)
    pred = Z[:, 0] * 0.5                      # (m,)
    y1 = y_test.ravel()                       # (m, 1) → (m,)
    assert y1.shape == pred.shape
    err = ((y1 - pred) ** 2).mean()

    এখন প্রতিটি broadcasting-এর উদ্দেশ্য স্পষ্ট, shape-এর একটি assert আছে, স্থির কলামের নিয়ম লেখা, আর কোনো warning চাপা নেই। স্থির কলামে কী করবেন (বাদ দেবেন, নাকি ১ দিয়ে ভাগ), তা ডেটার সিদ্ধান্ত; scikit-learn-এর StandardScaler-ও শূন্য variance-এর কলামে ভাগের মান ১ ধরে।

  4. কোড লিখুন: বাজারঘরের ৪টি পণ্যের ৩ মাসের বিক্রি (টাকায়) একটি (4, 3) array: [[100, 200, 100], [50, 50, 100], [300, 0, 300], [10, 20, 30]]. loop ছাড়া (ক) প্রতিটি মাসে কোন পণ্যের ভাগ কত শতাংশ (প্রতিটি কলামের যোগফল ১০০), আর (খ) প্রতিটি পণ্যের বিক্রি থেকে সেই পণ্যের ৩ মাসের গড় বাদ দেওয়া টেবিল বের করুন। প্রতিটি লাইনের shape লিখুন।
    S = np.array([[100, 200, 100], [50, 50, 100],
                  [300, 0, 300], [10, 20, 30]])          # (4, 3)
    col_pct = S / S.sum(axis=0) * 100                    # (4, 3) / (3,) → (4, 3)
    centred = S - S.mean(axis=1, keepdims=True)          # (4, 3) − (4, 1) → (4, 3)

    কলামের যোগফল [460, 270, 530], shape (3,), যা ডান থেকে সরাসরি মেলে, তাই keepdims লাগে না। প্রথম মাসে প্রথম পণ্যের ভাগ 100 / 460 × 100 ≈ 21.7%, দ্বিতীয় মাসে 200 / 270 × 100 ≈ 74.1%. প্রতিটি পণ্যের গড় [133.3, 66.7, 200, 20], আর keepdims=True তাকে (4, 1) রাখে। প্রথম সারি: 100 − 133.3 ≈ −33.3, 200 − 133.3 ≈ 66.7, 100 − 133.3 ≈ −33.3; চতুর্থ সারি: −10, 0, 10. যাচাই: col_pct.sum(axis=0) সব ১০০, আর centred.sum(axis=1) সব শূন্যের কাছে।

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

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

ক্রমদুটি array-র broadcasting-এ ফলের shape বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📐 দুটি array-র shape লিখে ফেলা
  2. ➡️ ডান দিকে সমান করে বসানো, কম মাত্রার array-র বাঁয়ে 1 যোগ করা
  3. 🔍 সবচেয়ে ডানের মাত্রা থেকে বাঁ দিকে একটি একটি জোড়া তুলনা করা
  4. ⚖️ প্রতিটি জোড়ায় দেখা: সমান, নাকি একটি 1; না হলে ValueError
  5. 🧮 প্রতিটি মাত্রায় বড় মাপটি নিয়ে ফলের shape লেখা

NumPy-তে A-এর shape (6, 1, 4) আর B-এর shape (5, 1). A + B-এর ফলের shape কী?

  • (6, 1, 4)
  • (6, 5, 4)
  • ValueError
  • (6, 5, 1, 4)

sales-এর shape (5, 7) (৫ জেলা × ৭ দিন)। প্রতিটি জেলার সারি থেকে সেই জেলার গড় বাদ দেওয়ার সঠিক লেখা কোনটি?

  • sales - sales.mean(axis=1)
  • sales - sales.mean(axis=0)
  • sales - sales.mean(axis=1, keepdims=True)
  • sales - sales.mean(axis=1)[None, :]

একটি ডেলিভারি-সময়ের মডেলে y_true-এর shape (1000, 1) আর y_pred-এর shape (1000,). ((y_true - y_pred) ** 2).mean() কী করবে?

  • কোনো error ছাড়াই ১০ লাখ পার্থক্যের গড় দেবে, যা ভুল মাপ
  • ValueError দেবে
  • ঠিক mean squared error দেবে, কারণ NumPy shape নিজে ঠিক করে নেয়
  • ১০০০টি সংখ্যার একটি array দেবে

একটি Python function f(p, q)-কে g = np.vectorize(f) দিয়ে মুড়ে এক লাখ উপাদানের array-তে চালানো হলো। কোন কথাটি সঠিক?

  • np.vectorize f-কে compiled C কোডে রূপান্তর করে, তাই array expression-এর সমান দ্রুত
  • np.vectorize কাজটি GPU-তে পাঠায়
  • np.vectorize শুধু একই shape-এর array নিতে পারে
  • এটি broadcasting-এর সুবিধা দেয়, কিন্তু ভেতরে প্রতিটি উপাদানে f ডাকে, তাই গতি প্রায় Python loop-এর মতো

১০,০০০টি বিন্দু, প্রতিটিতে ২টি float64 সংখ্যা। P[:, None, :] - P[None, :, :] কত বাইটের array তৈরি করবে?

  • ১,৬০,০০০ বাইট
  • প্রায় ১৬০ কোটি বাইট (১.৬ GB)
  • ৮০ কোটি বাইট
  • শূন্য, কারণ broadcasting কপি করে না

x = np.arange(5). নিচের কোনটি x-কে shape (5, 1)-এর কলামে রূপ দেয় না?

  • x[:, None]
  • x.reshape(-1, 1)
  • x[None, :]
  • x[:, np.newaxis]

শব্দকোষ

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

মনে রাখুন

  1. Vectorization মানে loop-কে compiled কোডে সরানো: price * qty * (1 - disc) এক লাইনে সব সারি; ছোট ডেটায় loop-এর সঙ্গে মিলিয়ে যাচাই করুন।
  2. Broadcasting: ডান থেকে মেলানো; সমান বা একটি 1 হলে মেলে; না থাকা মাত্রা 1. না মিললে operands could not be broadcast together.
  3. কলাম বানান [:, None], np.newaxis বা reshape(-1, 1) দিয়ে; সারি ধরে কমালে keepdims=True, বিশেষ করে বর্গাকার টেবিলে।
  4. রোজকার ছাঁচ: centering (X - X.mean(axis=0)), সারির শতাংশ, outer product (a[:, None] * b), distance matrix (P[:, None] - P[None]) তৈরি করা।
  5. মেমরির হিসাব আগে: ফল আর মাঝের array পুরো মাপে তৈরি হয় (n2 বাড়ে); np.vectorize গতি দেয় না; ছোট, ধাপে-নির্ভর কাজে সাধারণ loop-ই ঠিক।

পরের পর্বে: ufunc-এর পুরো পরিবার, axis ধরে যোগফল-গড়-সর্বোচ্চ, আর seed দেওয়া random সংখ্যা দিয়ে বাজারঘরের নতুন ডেটা তৈরি: পর্ব ১৬ · গাণিতিক কাজ, aggregation আর random.

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

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