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

NumPy-তে linear algebra — vector, matrix, dot product আর norm

Linear Algebra in NumPy — Vectors, Matrices, the @ Operator, Solving Systems, Conditioning and Norms

Taught in Bangla, this lesson treats vectors and matrices as NumPy arrays, contrasts * with @, works the inner-dimension shape rule, solves a small linear system by hand and with np.linalg.solve, and measures inverse versus solve with condition numbers in real runs. It also covers L1, L2 and max norms as distances, unit scaling, and previews least squares, neural layers and eigenvalues.

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

  • বাজারঘরের দামের তালিকা আর অর্ডারকে vector আর matrix হিসেবে লেখা, আর shape পড়ে আগেই ফলের shape বলে দেওয়া
  • @, np.dot আর *-এর পার্থক্য, আর matrix-vector গুণকে weighted sum হিসেবে বোঝা
  • Transpose, identity আর inverse; তিনটি রসিদ থেকে তিনটি পণ্যের দাম np.linalg.solve দিয়ে বের করে np.allclose দিয়ে যাচাই
  • Condition number দিয়ে হিসাবের ঝুঁকি মাপা, আর inverse বনাম solve-এর ভুল নিজে মেপে দেখা
  • L1, L2, max norm দিয়ে ক্রেতাদের মধ্যে দূরত্ব; তারপর least squares, determinant, rank, eigenvalue আর neural network layer-এর সঙ্গে পরিচয়

এই পর্ব গণিতের ক্লাস নয়, NumPy-র হাতের কাজ। Vector কী, দুটি vector-এর মধ্যে কোণ বা মিল (similarity) কীভাবে ভাবতে হয়, সেই জ্যামিতিক ছবিটি আসবে পর্ব ৩৩-এ (Vector আর similarity — dot product, cosine আর embedding-এর ধারণা)। এখানে আমরা দেখব কম্পিউটারে সেই হিসাবগুলো কীভাবে লেখা হয়, কোন লাইনটি চুপচাপ ভুল ফল দেয়, আর ফল ঠিক কি না কীভাবে যাচাই করবেন। NumPy array-র shape আর dtype এসেছে পর্ব ১৩-এ, broadcasting পর্ব ১৫-এ; এখানে শুধু যতটুকু লাগে ততটুকু মনে করিয়ে দেওয়া হবে। কোড পাতাতেই চলবে (ব্রাউজারের ভেতরে Python, Pyodide); প্রথমবার চালাতে কয়েক সেকেন্ড লাগতে পারে।

১ · Vector আর matrix — NumPy-তে দুটিই শুধু array

বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) তিনটি জনপ্রিয় পণ্য ধরুন: চাল প্রতি কেজি ৭৫ টাকা, মসুর ডাল প্রতি কেজি ১৩০ টাকা, সয়াবিন তেল প্রতি লিটার ১৯০ টাকা। দোকানের দেয়ালে টাঙানো দামের এই তালিকাটি তিনটি সংখ্যার একটি সারি। গণিতের ভাষায় এটি একটি vector: ক্রম মেনে সাজানো কয়েকটি সংখ্যা, যেখানে প্রতিটি অবস্থানের একটি অর্থ আছে (প্রথম ঘর চাল, দ্বিতীয় ঘর ডাল, তৃতীয় ঘর তেল)।

import numpy as np
price = np.array([75, 130, 190])      # চাল/কেজি, ডাল/কেজি, তেল/লিটার
price.shape                           # (3,)  — ১-মাত্রার, ৩টি ঘর

এবার চারটি অর্ডার। প্রথম অর্ডারে ২ কেজি চাল, ১ কেজি ডাল আর ১ লিটার তেল; দ্বিতীয়টিতে ৫ কেজি চাল আর ২ কেজি ডাল, তেল নেই; এভাবে আরও দুটি। প্রতিটি অর্ডার নিজেও তিন ঘরের একটি vector, আর চারটি অর্ডার পাশাপাশি সাজালে পাওয়া যায় একটি টেবিল: সারি মানে অর্ডার, কলাম মানে পণ্য। এই আয়তাকার সংখ্যার টেবিলটিই matrix.

qty = np.array([[2, 1, 1],    # অর্ডার ১
                [5, 2, 0],    # অর্ডার ২
                [1, 0, 2],    # অর্ডার ৩
                [3, 1, 1]])   # অর্ডার ৪
qty.shape                     # (4, 3) — ৪টি সারি, ৩টি কলাম

খেয়াল করুন, NumPy-তে আলাদা কোনো “vector” বা “matrix” type নেই। দুটিই সেই একই ndarray, শুধু মাত্রার সংখ্যা (ndim) আলাদা। কোন array কোন ভূমিকায় আছে, তা বলে দেয় তার shape. পুরো এই পর্বে একটি অভ্যাস গড়ে তুলুন: যেকোনো গুণের আগে দুই পাশের shape জোরে পড়ুন। এই পর্বের প্রায় প্রতিটি বাগ shape পড়লেই ধরা পড়ে।

মূল ধারণা

Linear algebra হলো vector আর matrix-এর হিসাবের গণিত: যোগ, গুণ, সমীকরণ মেলানো, দৈর্ঘ্য মাপা। AI-তে এর গুরুত্ব সহজ কারণে: ডেটার একটি সারি একটি vector (প্রতিটি feature একটি ঘর), পুরো ডেটাসেট একটি matrix, আর একটি neural network-এর বেশির ভাগ হিসাবই matrix গুণ। NumPy এই গণিতকে অল্প কয়েকটি চিহ্ন আর np.linalg module-এ বেঁধে দিয়েছে।

একটি সূক্ষ্ম কথা এখনই জেনে রাখুন। খাতায় গণিত লেখার সময় vector-কে সাধারণত হয় খাড়া কলাম (column vector) হিসেবে, নয়তো শোয়ানো সারি (row vector) হিসেবে লেখা হয়। NumPy-র ১-মাত্রার array-র এমন কোনো দিক নেই: (3,) shape-এর array সারিও নয়, কলামও নয়, শুধু তিনটি সংখ্যা। গুণের সময় NumPy প্রসঙ্গ দেখে ঠিক করে নেয় এটিকে কীভাবে ধরবে। এটি সুবিধার, তবে অংশ ৪-এ দেখবেন কোথায় এটি বিভ্রান্তি তৈরি করে।

২ · * না @ — ঘরে ঘরে গুণ আর matrix গুণ

ম্যানেজার জানতে চান প্রতিটি অর্ডারের মোট বিল কত। নতুনদের প্রথম চেষ্টা প্রায়ই হয় qty * price. কোডটি চলে, কোনো error আসে না, একটি সংখ্যার টেবিলও আসে। কিন্তু সেটি মোট বিল নয়।

কারণটি পর্ব ১৫-এর broadcasting. * চিহ্নটি NumPy-তে সবসময় element-wise: দুই পাশের মিলে যাওয়া ঘরগুলো আলাদা আলাদা গুণ হয়। qty-র shape (4, 3), price-এর (3,); broadcasting-এর নিয়মে price-কে প্রতিটি সারির নিচে বসিয়ে দেওয়া হয়, তাই ফল হয় (4, 3): প্রতিটি অর্ডারের প্রতিটি পণ্যের আলাদা দাম, যাকে রসিদের “লাইন-আইটেম” বলা যায়। আর @ চিহ্নটি matrix গুণ: প্রতিটি সারির ঘরগুলো দামের সঙ্গে গুণ করে যোগ করে, তাই ফল হয় (4,), প্রতিটি অর্ডারের একটি করে মোট। নিচের কোড চালিয়ে দুটি ফল পাশাপাশি দেখুন।

import numpy as np

# বাজারঘরের দাম (টাকা): চাল/কেজি, ডাল/কেজি, তেল/লিটার
price = np.array([75, 130, 190])
# ৪টি অর্ডার × ৩টি পণ্য: কোন অর্ডারে কতটা
qty = np.array([[2, 1, 1],
                [5, 2, 0],
                [1, 0, 2],
                [3, 1, 1]])

line = qty * price      # element-wise: প্রতিটি ঘর আলাদা গুণ
total = qty @ price     # matrix গুণ: প্রতিটি সারির weighted sum

print("qty:", qty.shape, " price:", price.shape)
print("qty * price → shape", line.shape)
print(line)
print("qty @ price → shape", total.shape, total)
print("line.sum(axis=1):", line.sum(axis=1))
print("একই কি?", np.array_equal(total, line.sum(axis=1)))

# দুটি vector-এর ক্ষেত্রেও একই ফাঁদ
q = qty[0]
print("q * price:", q * price, "← এটি মোট নয়")
print("q @ price:", q @ price, " np.dot:", np.dot(q, price))

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

qty: (4, 3)  price: (3,)
qty * price → shape (4, 3)
[[150 130 190]
 [375 260   0]
 [ 75   0 380]
 [225 130 190]]
qty @ price → shape (4,) [470 635 455 545]
line.sum(axis=1): [470 635 455 545]
একই কি? True
q * price: [150 130 190] ← এটি মোট নয়
q @ price: 470  np.dot: 470

qty * price-এর ফল (4, 3): প্রথম সারিতে 2×75 = 150, 1×130 = 130, 1×190 = 190; এগুলো রসিদের লাইন-আইটেম, মোট নয়। qty @ price-এর ফল (4,): প্রথম অর্ডারের মোট 150 + 130 + 190 = 470 টাকা। তাহলে সম্পর্কটি পরিষ্কার: @ = ঘরে ঘরে গুণ, তারপর শেষ মাত্রা ধরে যোগ। line.sum(axis=1) হাতে সেই যোগটি করে একই ফল দিয়েছে। দুটি ১-মাত্রার vector-এর ক্ষেত্রেও একই কথা: q * price তিনটি সংখ্যার vector, আর q @ price একটিমাত্র সংখ্যা, যাকে বলে dot product. np.dot এখানে @-এর মতোই ফল দিয়েছে।

সবচেয়ে বিপজ্জনক ভুল হলো যেটি error দেয় না। যদি qty বর্গাকার হতো (ধরুন ৩টি অর্ডার × ৩টি পণ্য), তাহলে qty * price আর qty @ price দুটিই চলত, একটির shape (3, 3) আর অন্যটির (3,); আর পরের কোনো .sum() বা .mean() হয়তো shape-এর পার্থক্যটুকুও মুছে দিত। তাই গুণের পরে .shape ছাপা বা assert total.shape == (n_orders,) লিখে রাখা সস্তা বিমা। পুরোনো কোডে np.matrix নামের একটি class দেখতে পারেন, যেখানে * মানেই matrix গুণ ছিল; NumPy-র নিজের docs এখন এটি ব্যবহার না করার পরামর্শ দেয়। সাধারণ ndarray আর @-ই এখনকার নিয়ম।

@ চিহ্নটি Python-এ এসেছে তুলনামূলক দেরিতে। এর আগে matrix গুণ লিখতে হতো np.dot(np.dot(A, B), C)-এর মতো ঘোরানো ভাষায়। ২০১৪ সালে Nathaniel J. Smith-এর লেখা প্রস্তাব PEP 465 একটি আলাদা matrix-গুণের operator চায়, আর ২০১৫ সালে Python 3.5-এ @ যুক্ত হয়। মজার ব্যাপার, Python নিজে কোনো built-in type-এ @ ব্যবহার করে না; এটি রাখা হয়েছিল মূলত NumPy-র মতো library-র জন্য, যারা __matmul__ method লিখে এর অর্থ ঠিক করে।

৩ · Matrix × vector মানে weighted sum — সারি ধরে ধরে

চায়ের দোকানের হিসাব খাতার কথা ভাবুন। দিনের শেষে দোকানি প্রতিটি খদ্দেরের লাইন দেখেন: “২ কাপ চা, ১টা বিস্কুট, ১টা কলা”। তারপর প্রতিটি জিনিসের সংখ্যাকে তার দামের সঙ্গে গুণ করে যোগ করেন। এটিই matrix-vector গুণ: প্রতিটি সারির জন্য একটি weighted sum, মানে “ওজন দেওয়া যোগফল”, যেখানে ওজন হলো দাম। গণিতের ভাষায়, যদি ফলটির নাম t হয়, তাহলে i-তম অর্ডারের জন্য:

ti = ∑j qij · pj = qi1p1 + qi2p2 + qi3p3

এখানে qij মানে i-তম অর্ডারে j-তম পণ্যের পরিমাণ (qty[i, j]), pj মানে j-তম পণ্যের দাম, আর ∑ (সিগমা) মানে “সব j-এর জন্য যোগ করুন”। নিচের ধাপে চারটি সারি একটি একটি করে হিসাব হতে দেখুন। “পরে” চাপুন।

1total = np.zeros(4)2for i in range(4):3    total[i] = (qty[i] * price).sum()4# একই কাজ এক লাইনে: qty @ price
মান (Watch)
qty[i]
× price
যোগ
total
  1. অর্ডার ১: [2 1 1]. দামের সঙ্গে ঘরে ঘরে গুণ: 2×75 = 150, 1×130 = 130, 1×190 = 190. যোগ 150 + 130 + 190 = 470 টাকা। এটি অর্ডার ১-এর সারি আর দামের vector-এর dot product.
  2. অর্ডার ২: [5 2 0]. 5×75 = 375, 2×130 = 260, তেল নেই তাই 0. যোগ 635. শূন্য পরিমাণ শূন্য টাকা যোগ করে, কোনো বিশেষ নিয়ম লাগে না।
  3. অর্ডার ৩: [1 0 2]. 75 + 0 + 2×190 = 75 + 380 = 455. দুই লিটার তেলই এই বিলের বেশির ভাগ।
  4. অর্ডার ৪: [3 1 1]. 225 + 130 + 190 = 545. চারটি সারি, চারটি dot product, চারটি মোট।
  5. এক লাইনে। qty @ price ঠিক এই চারটি weighted sum একসঙ্গে দেয়: [470 635 455 545]. পার্থক্য শুধু গতিতে: loop-টি Python-এ চলে, আর @ চলে compiled কোডে (float array হলে ভেতরে সাধারণত BLAS নামের দ্রুত library-তে; এখানকার integer array-তে NumPy-র নিজের compiled loop-এ)। বড় ডেটায় এই পার্থক্য বহুগুণ।

একই গুণকে আরেকভাবেও পড়া যায়, আর দ্বিতীয় পড়াটিও কাজের। সারি ধরে না দেখে কলাম ধরে দেখুন: qty @ price = 75 × (চালের কলাম) + 130 × (ডালের কলাম) + 190 × (তেলের কলাম)। মানে ফলটি qty-র কলামগুলোর একটি weighted মিশ্রণ, ওজন হলো দাম। গণিতে একে বলে কলামগুলোর linear combination. অংশ ৬-এ rank বোঝার সময় এই দ্বিতীয় ছবিটি লাগবে।

@, np.dot আর np.matmul — কোনটি কখন

২-মাত্রা পর্যন্ত তিনটিই একই ফল দেয়, তাই অনেক কোডে এরা মিশে থাকে। পার্থক্য আসে কিনারায়। ডেস্কটপে NumPy 2.5.1 দিয়ে চালানো একটি ছোট পরীক্ষা (সত্যিকারের run):

>>> a = np.ones((2, 3, 4)); b = np.ones((2, 4, 5))
>>> np.dot(a, b).shape, (a @ b).shape
((2, 3, 2, 5), (2, 3, 5))
>>> 3 @ price
ValueError: matmul: Input operand 0 does not have enough dimensions (has 0, gufunc core with signature (n?,k),(k,m?)->(n?,m?) requires 1)
  • @ (= np.matmul): ৩ বা বেশি মাত্রায় শেষ দুটি মাত্রাকে matrix ধরে, আগের মাত্রাগুলোকে “batch” ধরে; ওপরে দুটি (3, 4) @ (4, 5) গুণ পাশাপাশি হয়ে (2, 3, 5). Scalar (একক সংখ্যা) দিয়ে গুণ করতে দেয় না; সেখানে * লিখতে হয়।
  • np.dot: ১ আর ২ মাত্রায় একই, কিন্তু বেশি মাত্রায় সম্পূর্ণ আলাদা নিয়ম মানে (প্রথমটির শেষ মাত্রা আর দ্বিতীয়টির শেষের-আগের মাত্রা ধরে যোগ), ফলে (2, 3, 2, 5)-এর মতো অপ্রত্যাশিত shape আসে। Scalar-ও নেয়।
  • পরামর্শ: matrix গুণের জন্য @, scalar গুণের জন্য *, আর ৩+ মাত্রায় np.dot এড়িয়ে চলুন। NumPy 2.0 থেকে দুটি vector-এর dot product ব্যাচ ধরে করার জন্য np.vecdot-ও আছে।

৪ · Shape-এর নিয়ম — ভেতরের মাত্রা মিলতে হবে, আর transpose

Matrix গুণের একটিমাত্র নিয়ম মুখস্থ রাখলেই চলে: বাঁয়ের শেষ মাত্রা আর ডানের প্রথম মাত্রা সমান হতে হবে, আর ফলের shape হয় বাকি দুটি বাইরের মাত্রা।

(m, k) @ (k, n) → (m, n)

এখানে m হলো বাঁয়ের সারির সংখ্যা, n ডানের কলামের সংখ্যা, আর k হলো সেই “ভেতরের” মাত্রা, যার ধরে গুণ করে যোগ হয়। বাজারঘরে k হলো পণ্যের সংখ্যা: অর্ডারের প্রতিটি কলাম আর দামের প্রতিটি ঘর একই পণ্যের কথা বলছে বলেই তাদের গুণ করে যোগ করার অর্থ আছে। ভেতরের মাত্রা না মিললে অর্থটাই থাকে না, তাই NumPy error দেয়। নিচের ছবিতে নিয়মটি দেখুন।

qty @ price — ভেতরের মাত্রা মেলে, বাইরেরটি থাকে

Matrix-vector product: (4, 3) @ (3,) → (4,)

Matrix-vector গুণে shape-এর নিয়ম qty price total চাল ডাল তেল 211 520 102 311 @ 75130190 = 470635455545 2×75 + 1×130 + 1×190 = 470 (4, 3) @ (3,) = (4,) ভেতরের 3 = 3: পণ্যের সংখ্যা মিলেছে
ছবি ১৭.১ — হলুদ সারিটি (অর্ডার ১) দামের সবুজ কলামের সঙ্গে ঘরে ঘরে গুণ হয়ে যোগ হয়েছে, ফল 470. নিচে shape: ভেতরের দুটি 3 (লাল) মিলেছে বলেই গুণ চলে, আর সেই মাত্রাটি যোগের সময় “খেয়ে ফেলা” হয়; বাকি থাকে বাইরের 4, তাই ফল (4,).

এবার নিয়মটি কয়েকটি ভিন্ন shape-এ ধাপে ধাপে খাটিয়ে দেখি। প্রতিটি ধাপে আগে নিজে ফলের shape বলার চেষ্টা করুন, তারপর মিলিয়ে নিন।

qty @ price(4, 3) @ (3,)
(4, 3) @ (3,)  →  (4,)
[470 635 455 545]
qty @ P(4, 3) @ (3, 2)
P = [[ 75,  72],      # ঢাকা শাখা, চট্টগ্রাম শাখা
     [130, 135],
     [190, 185]]
(4, 3) @ (3, 2)  →  (4, 2)
[[470 464]
 [635 630]
 [455 442]
 [545 536]]
qty.T @ ones(3, 4) @ (4,)
qty.T.shape  →  (3, 4)
(3, 4) @ (4,)  →  (3,)
[11  4  4]     # চাল ১১ কেজি, ডাল ৪, তেল ৪ লিটার
qty @ [1, 2, 3, 4](4, 3) @ (4,)
(4, 3) @ (4,)  →  ValueError
matmul: … mismatch in its
core dimension 0 …
(size 4 is different from 3)
1-D বনাম কলাম(3,) বনাম (3, 1)
price.T.shape          →  (3,)    # কিছুই বদলায়নি
price[:, None].shape   →  (3, 1)  # এবার সত্যিকারের কলাম
(qty @ price[:, None]).shape  →  (4, 1)
  1. মূল উদাহরণ। (4, 3) @ (3,): ভেতরের 3 আর 3 মেলে, ফল (4,). ১-মাত্রার price-কে এখানে NumPy কলাম হিসেবে ধরে, কাজ শেষে সেই বাড়তি মাত্রাটি সরিয়ে দেয়।
  2. দুটি দামের তালিকা একসঙ্গে। ধরুন ঢাকা আর চট্টগ্রাম শাখায় দাম একটু আলাদা; দুটি তালিকা পাশাপাশি রাখলে P-র shape (3, 2). (4, 3) @ (3, 2) → (4, 2): চারটি অর্ডারের বিল দুই শাখার দামে, এক গুণে আটটি weighted sum. যেমন অর্ডার ১ চট্টগ্রামে 2×72 + 135 + 185 = 464.
  3. Transpose দিয়ে উল্টো প্রশ্ন। qty.T সারি আর কলাম অদলবদল করে, shape (3, 4): এখন সারি মানে পণ্য। চারটি 1-এর vector দিয়ে গুণ করলে প্রতিটি পণ্যের মোট বিক্রি: চাল 2+5+1+3 = 11 কেজি, ডাল 4, তেল 4 লিটার। এটি qty.sum(axis=0)-এর সমান।
  4. ভেতরের মাত্রা মেলেনি। (4, 3) @ (4,): বাঁয়ে ৩টি পণ্য, ডানে ৪টি সংখ্যা। NumPy-র error বার্তাটি পড়তে শিখুন: “core dimension”-এ 4 আর 3 আলাদা। এটি আসলে একটি উপকার: অর্থহীন গুণ চুপচাপ চলেনি। অনেক সময় সমাধান হলো এক পাশে .T, কিন্তু আগে ভাবুন কোন মাত্রাটির অর্থ কী।
  5. ১-মাত্রার array-র transpose হয় না। price.T একই (3,) ফেরত দেয়, কারণ উল্টানোর মতো দ্বিতীয় মাত্রা নেই। সত্যিকারের কলাম চাইলে price[:, None] বা price.reshape(-1, 1), shape (3, 1); তখন ফলও কলাম, (4, 1). পরে scikit-learn বা neural network-এর কোডে এই (n,) বনাম (n, 1) পার্থক্য বহু বাগের উৎস।

Transpose (A.T) কোনো সংখ্যা বদলায় না, শুধু পড়ার দিক বদলায়: A.T[i, j] = A[j, i]. NumPy-তে এটি নতুন array বানায় না, একই মেমরির একটি নতুন “দৃষ্টি” (view) দেয়, তাই বিশাল matrix-এও এটি প্রায় বিনা খরচে হয় (view আর copy-র খুঁটিনাটি পর্ব ১৪-এ)। দুটি দরকারি সূত্র মনে রাখুন: (A @ B).T == B.T @ A.T (ক্রম উল্টে যায়), আর A.T @ A সবসময় বর্গাকার, shape (k, k); least squares আর PCA দুই জায়গাতেই এটি আসবে।

৫ · Identity, inverse আর linear system — রসিদ থেকে দাম বের করা

এবার প্রশ্নটি উল্টো। ধরুন বাজারঘরের দামের তালিকার ফাইলটি হারিয়ে গেছে, কিন্তু তিনটি পুরোনো রসিদ আছে। প্রতিটিতে লেখা কোন পণ্য কতটা, আর মোট কত টাকা; আলাদা দাম লেখা নেই।

রসিদচাল (কেজি)ডাল (কেজি)তেল (লিটার)মোট (টাকা)
১211470
২520635
৩102455

অজানা দাম তিনটির নাম দিই x1, x2, x3. প্রতিটি রসিদ একটি সমীকরণ দেয়, যেমন প্রথমটি 2x1 + x2 + x3 = 470. তিনটি সমীকরণ একসঙ্গে লিখলে পাওয়া যায় একটি linear system, matrix-এর ভাষায় এক লাইনে:

A x = b, যেখানে A = পরিমাণের matrix (3, 3), x = অজানা দাম (3,), b = রসিদের মোট (3,)

অংশ ৩-এ আমরা A আর x জেনে b বের করেছিলাম; এখন A আর b জেনে x চাই। সাধারণ সংখ্যায় 3x = 12 হলে দুই পাশকে 3 দিয়ে ভাগ করি। Matrix-এ “ভাগ” নেই, আছে inverse: A−1 এমন একটি matrix, যার সঙ্গে A গুণ করলে পাওয়া যায় identity matrix I. Identity হলো matrix জগতের “1”: কর্ণ বরাবর 1, বাকি সব 0, আর যেকোনো vector-কে I দিয়ে গুণ করলে vector বদলায় না (np.eye(3)). তাত্ত্বিকভাবে তাই x = A−1b. কিন্তু কম্পিউটারে এভাবে হিসাব করা প্রায় কখনোই সেরা পথ নয় (কেন, অংশ ৬-এ)। NumPy-র ঠিক কাজটির জন্য আলাদা function আছে: np.linalg.solve(A, b).

Solve ভেতরে কী করে, তা বুঝতে স্কুলের সেই পুরোনো পদ্ধতিটিই যথেষ্ট: এক সমীকরণ থেকে আরেকটি বিয়োগ করে একটি একটি অজানা সরিয়ে ফেলা, যার নাম Gaussian elimination. নিচের ধাপে হাতে করে দেখুন। সুবিধার জন্য রসিদ ৩-কে ওপরে আনা হয়েছে, কারণ তার প্রথম সংখ্যা 1.

1[A │ b], রসিদ ৩ ওপরে2R2 ← R2 − 2·R13R3 ← R3 − 5·R14R3 ← R3 − 2·R25নিচ থেকে ওপরে বসানো6np.allclose(A @ x, b)
সমীকরণের সারি [চাল ডাল তেল │ মোট]
R1
R2
R3
  1. সাজানো। তিনটি সমীকরণকে তিনটি সারি হিসেবে লিখলাম, ডানে “│”-এর পরে মোট টাকা। R1 এখন রসিদ ৩: x₁ + 2x₃ = 455. লক্ষ্য হলো নিচের সারিগুলো থেকে x₁ সরিয়ে দেওয়া।
  2. R2 থেকে x₁ সরানো। R2-র প্রথম সংখ্যা 2, তাই R1-এর দ্বিগুণ বিয়োগ: [2 1 1 │ 470] − [2 0 4 │ 910] = [0 1 −3 │ −440]. এখন R2-তে চালের দাম নেই।
  3. R3 থেকে x₁ সরানো। R3-র প্রথম সংখ্যা 5, তাই R1-এর পাঁচগুণ বিয়োগ: [5 2 0 │ 635] − [5 0 10 │ 2275] = [0 2 −10 │ −1640].
  4. R3 থেকে x₂ সরানো। R3-র দ্বিতীয় সংখ্যা 2, R2-র 1, তাই R2-র দ্বিগুণ বিয়োগ: [0 2 −10 │ −1640] − [0 2 −6 │ −880] = [0 0 −4 │ −760]. শেষ সারিতে একটিমাত্র অজানা বাকি: −4x₃ = −760.
  5. নিচ থেকে ওপরে বসানো। x₃ = 190 (তেল)। R2-তে বসালে x₂ − 3·190 = −440, তাই x₂ = 130 (ডাল)। R1-এ বসালে x₁ + 2·190 = 455, তাই x₁ = 75 (চাল)। একে বলে back substitution.
  6. যাচাই। পাওয়া দামগুলো মূল তিনটি রসিদে বসালে 455, 470, 635 হুবহু মেলে, তাই np.allclose(A @ x, b) হবে True. উত্তর বের করার পর মূল সমীকরণে ফিরিয়ে দেখা, এই অভ্যাসটিই সবচেয়ে সস্তা আর সবচেয়ে কাজের যাচাই।

np.linalg.solve ঠিক এই কাজটিই করে, তবে একটু বুদ্ধি করে: প্রতিটি ধাপে কলামের সবচেয়ে বড় (absolute) সংখ্যাটিকে ওপরে আনে (partial pivoting), যাতে ছোট সংখ্যা দিয়ে ভাগ করে ভুল বড় না হয়। আর elimination-এর হিসাবটি দুটি ত্রিভুজাকার matrix-এ জমা রাখে, যার নাম LU decomposition. এই কাজ NumPy নিজে লেখে না, ডাকে LAPACK নামের বহু দশকের পরীক্ষিত library. এবার নিজে চালিয়ে দেখুন, সঙ্গে একটি “বিপদের” উদাহরণও আছে।

import numpy as np

# তিনটি রসিদ: চাল (কেজি), ডাল (কেজি), তেল (লিটার)
A = np.array([[2, 1, 1],
              [5, 2, 0],
              [1, 0, 2]], dtype=float)
b = np.array([470, 635, 455], dtype=float)   # রসিদের মোট টাকা

x = np.linalg.solve(A, b)                    # A @ x = b থেকে x
print("দাম (চাল, ডাল, তেল):", x)
print("A @ x    :", A @ x)
print("allclose :", np.allclose(A @ x, b))
print("det, rank:", np.linalg.det(A), np.linalg.matrix_rank(A))
print("cond     :", round(np.linalg.cond(A), 2))
print("নতুন অর্ডার [3,1,1]-এর মোট:", np.array([3, 1, 1]) @ x)

# তৃতীয় রসিদ যদি প্রথম দুটির যোগফল হয়: নতুন তথ্য নেই
S = A.copy()
S[2] = S[0] + S[1]
bs = np.array([470, 635, 1105], dtype=float)
print("S-এর rank:", np.linalg.matrix_rank(S), " cond:", f"{np.linalg.cond(S):.1e}")
try:
    xs = np.linalg.solve(S, bs)
    print("solve উত্তর দিল:", xs.round(2))
    print("রসিদ মেলে?", np.allclose(S @ xs, bs))
except np.linalg.LinAlgError as e:
    print("LinAlgError:", e)

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

দাম (চাল, ডাল, তেল): [ 75. 130. 190.]
A @ x    : [470. 635. 455.]
allclose : True
det, rank: -3.999999999999999 3
cond     : 18.81
নতুন অর্ডার [3,1,1]-এর মোট: 545.0
S-এর rank: 2  cond: 2.5e+16
solve উত্তর দিল: [100.33  66.67 202.67]
রসিদ মেলে? True

প্রথম অংশে solve হাতের হিসাবের সঙ্গে মিলেছে: চাল ৭৫, ডাল ১৩০, তেল ১৯০ টাকা, আর উত্তর মূল রসিদে বসিয়ে allclose সত্য। খেয়াল করুন, det হুবহু −4 নয়, −3.999999999999999; হাতে হিসাবে det = 2(4 − 0) − 1(10 − 0) + 1(0 − 2) = −4, কিন্তু LU-র পথে float-এর শেষ অঙ্কে সামান্য গোলমাল এসেছে। Rank 3 আর cond প্রায় 18.81, মানে তিনটি রসিদ সত্যিই তিনটি আলাদা তথ্য দেয় আর সিস্টেমটি ভালো-আচরণের। একটি বাড়তি যাচাইও হয়েছে: পাওয়া দাম দিয়ে চতুর্থ একটি অর্ডারের ([3, 1, 1]) মোট হলো 545, যা অংশ ৩-এর হিসাবের সঙ্গে মেলে। যে ডেটা দিয়ে উত্তর বের করা হয়নি, সেখানে মিলিয়ে দেখাই আসল পরীক্ষা।

দ্বিতীয় অংশটি সতর্কবার্তা। তৃতীয় রসিদকে প্রথম দুটির যোগফল বানানো হয়েছে, তাই rank 2 আর cond প্রায় 2.5 × 1016, মানে matrix-টি কার্যত singular. তবু solve কোনো error দেয়নি! সে একটি উত্তর দিয়েছে (100.33, 66.67, 202.67), যা তিনটি রসিদই মেলায় (True), অথচ আসল দাম থেকে অনেক দূরে। কারণ এখানে অসংখ্য উত্তর আছে, আর float-এর গোলমালে pivot হুবহু শূন্য না হওয়ায় LAPACK তাদের একটি বেছে দিয়েছে। Pivot হুবহু শূন্য হলে (যেমন [[1, 2], [2, 4]]-এ) NumPy LinAlgError: Singular matrix দেয়; কোনটি ঘটবে তা সংখ্যার ওপর নির্ভর করে। শিক্ষা: allclose সত্য হওয়া যথেষ্ট নয়, rank আর cond-ও দেখুন।

দুটি ছোট কথা। প্রথমত, np.allclose কেন, == কেন নয়? Float-এর হিসাবে শেষ অঙ্কে সামান্য গোলমাল থাকতেই পারে (det হুবহু −4 না হয়ে −3.999… দেখাতে পারে), তাই সহনশীলতা (tolerance) রেখে তুলনা করাই নিয়ম; float-এর এই আচরণ এসেছে পর্ব ৯-এ, আর ভেতরের গঠন আছে Computer Architecture কোর্সের floating point পর্বে। দ্বিতীয়ত, A বর্গাকার না হলে (সমীকরণ আর অজানার সংখ্যা আলাদা) solve চলেই না; সেই ক্ষেত্রটি অংশ ৮-এর least squares.

৬ · কেন inverse নয়, solve — condition number আর হারানো অঙ্ক

একটি তুলনা দিয়ে শুরু করি। দাঁড়িপাল্লায় দুটি প্রায়-সমান বস্তার ওজনের পার্থক্য মাপতে গেলে পাল্লার সামান্য কাঁপুনিও ফলকে অনেকখানি বদলে দেয়। কিছু সমীকরণ-ব্যবস্থাও এমন স্পর্শকাতর: ইনপুটে খুব ছোট বদল (একটি রসিদে এক পয়সার ভুল, বা float-এর শেষ অঙ্কের গোলমাল) উত্তরে বিশাল বদল আনে। এমন matrix-কে বলে ill-conditioned, আর কতটা স্পর্শকাতর তার মাপ হলো condition number, np.linalg.cond(A).

মোটা দাগের নিয়মটি মনে রাখার মতো: condition number যদি প্রায় 10k হয়, তাহলে উত্তরে প্রায় k-টি দশমিক অঙ্ক হারানোর ঝুঁকি থাকে। float64-এ মোটামুটি ১৫–১৬টি অর্থপূর্ণ অঙ্ক থাকে (এর মাপ np.finfo(float).eps, প্রায় 2.2 × 10−16). তাই cond 103 হলে প্রায় ১২–১৩টি অঙ্ক টেকে, চিন্তার কিছু নেই; cond 1013 হলে টেকে মাত্র দু-তিনটি; আর cond 1016 ছুঁলে উত্তরের কোনো অঙ্কই ভরসার নয়। এটি একটি মোটা হিসাব, নিখুঁত সীমা নয়, কিন্তু সতর্ক হওয়ার জন্য যথেষ্ট।

এবার inverse বনাম solve-এর আসল তুলনা। পরীক্ষার জন্য বিখ্যাতভাবে কঠিন একটি matrix নেওয়া হলো, Hilbert matrix: H[i, j] = 1/(i + j + 1). এর সারিগুলো একে অপরের এত কাছাকাছি যে ১০×১০ আকারেই এটি খুব ill-conditioned. আমরা আসল উত্তর আগে থেকে ঠিক করে দিই (সব 1), সেখান থেকে b বানাই, তারপর দুই পথে উত্তর ফিরে পাওয়ার চেষ্টা করি। দুটি জিনিস মাপা হবে: উত্তর আসল উত্তর থেকে কতটা দূরে (ভুল), আর পাওয়া উত্তর সমীকরণে বসালে কতটা অমিল থাকে (residual).

import numpy as np

n = 10
i = np.arange(n)
H = 1.0 / (i[:, None] + i[None, :] + 1)   # Hilbert matrix: H[i, j] = 1/(i+j+1)
x_true = np.ones(n)                        # আসল উত্তর আমরা জানি
b = H @ x_true

x_solve = np.linalg.solve(H, b)
x_inv = np.linalg.inv(H) @ b

def err(x):    # উত্তরের আপেক্ষিক ভুল
    return np.linalg.norm(x - x_true) / np.linalg.norm(x_true)

def resid(x):  # সমীকরণ কতটা মেলে না
    return np.linalg.norm(H @ x - b) / np.linalg.norm(b)

print(f"cond(H) = {np.linalg.cond(H):.1e}")
print(f"solve  : ভুল {err(x_solve):.1e}  residual {resid(x_solve):.1e}")
print(f"inv @ b: ভুল {err(x_inv):.1e}  residual {resid(x_inv):.1e}")
print(f"float64-এর eps: {np.finfo(float).eps:.1e}")
print("আনুমানিক হারানো অঙ্ক:", round(np.log10(np.linalg.cond(H))))

A = np.array([[2, 1, 1], [5, 2, 0], [1, 0, 2]], dtype=float)
print(f"রসিদের সিস্টেমের cond = {np.linalg.cond(A):.1f}")

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

cond(H) = 1.6e+13
solve  : ভুল 1.7e-04  residual 1.2e-16
inv @ b: ভুল 4.1e-03  residual 2.4e-05
float64-এর eps: 2.2e-16
আনুমানিক হারানো অঙ্ক: 13
রসিদের সিস্টেমের cond = 18.8

Cond প্রায় 1.6 × 1013, তাই নিয়ম অনুযায়ী প্রায় ১৩টি অঙ্ক হারানোর ঝুঁকি; ১৬ থেকে ১৩ গেলে থাকে মোটামুটি তিনটি। বাস্তবেও তাই: solve-এর উত্তরে আপেক্ষিক ভুল 1.7 × 10−4, মানে প্রায় তিন-চারটি অঙ্ক ঠিক। Inverse-এর পথে ভুল 4.1 × 10−3, এই run-এ প্রায় ২৪ গুণ বেশি। পার্থক্য আরও স্পষ্ট residual-এ: solve-এর উত্তর সমীকরণ প্রায় machine precision পর্যন্ত মেলায় (1.2 × 10−16), inverse-এর উত্তর মেলায় অনেক খারাপভাবে (2.4 × 10−5). শেষ লাইনটি স্বস্তির: বাজারঘরের তিন রসিদের সিস্টেমের cond মাত্র 18.8, সেখানে দুই পথের পার্থক্য প্রায় চোখেই পড়বে না। সংখ্যাগুলো এই ব্রাউজারের NumPy-র একটি run; অন্য মেশিনে শেষ অঙ্ক আলাদা হবে, নিচের সারণিতে যেমন দেখছেন।

তুলনার জন্য একই পরীক্ষা ডেস্কটপে (Windows, Python 3.14.4, NumPy 2.5.1, সত্যিকারের run, এক বারের মাপ) ছোট-বড় তিনটি আকারে চালানো হয়েছিল:

ncond(H)solve: ভুল / residualinv @ b: ভুল / residual
81.53e+106.1e-08 / 6.6e-179.3e-07 / 7.8e-09
101.60e+138.7e-05 / 1.2e-162.8e-03 / 1.0e-05
121.81e+163.2e-01 / 1.2e-169.4e+00 / 2.3e-02

সারণিটি তিনটি শিক্ষা দেয়। এক, cond যত বড়, উত্তরের ভুল তত বড়, দুই পথেই; n = 12-তে solve-এর উত্তরও ৩২% ভুল, কারণ সমস্যাটিই float64-এর সাধ্যের বাইরে। দুই, solve-এর residual সব আকারেই প্রায় machine precision-এর ঘরে: সে যে উত্তর দেয়, তা অন্তত “প্রায় একই রকম একটি সমস্যার” নিখুঁত উত্তর (একে backward stable বলে)। inverse-এর residual অনেক বড়। তিন, এই সংখ্যাগুলো একটি মেশিনের একটি run; ভিন্ন LAPACK, BLAS বা CPU-তে শেষ অঙ্ক বদলাবে, কিন্তু প্রবণতাটি সাধারণভাবে এমনই থাকে বলে সংখ্যাগত গণিতের পাঠ্যবইগুলো জানায়।

মূল ধারণা

x = inv(A) @ b নয়, x = np.linalg.solve(A, b). কারণ তিনটি: (১) inverse বানাতে বেশি হিসাব লাগে, আর পরে আরেকটি গুণও; (২) ill-conditioned matrix-এ inverse-এর পথে ভুল সাধারণত বেশি জমে; (৩) inverse প্রায় কখনো নিজে দরকার হয় না, দরকার হয় তার সঙ্গে কোনো vector-এর গুণফল। একই A দিয়ে বহু b মেলাতে হলে b-গুলো কলাম হিসেবে একটি matrix-এ রেখে এক solve-এ দিন, অথবা SciPy-র scipy.linalg.lu_factor একবার করে বারবার ব্যবহার করুন। আর solve-এর আগে বা পরে np.linalg.cond দেখে নিন।

Determinant আর rank — সংক্ষেপে

Determinant (np.linalg.det) একটি বর্গাকার matrix থেকে পাওয়া একটি সংখ্যা। এটি শূন্য হলে matrix-টির inverse নেই (তাকে বলে singular). পরীক্ষার খাতায় এটি খুব জনপ্রিয়, কিন্তু কম্পিউটারে “matrix কতটা ভালো” বোঝার জন্য খারাপ মাপ: matrix-এর সব সংখ্যাকে 0.1 দিয়ে গুণ করলে ১০×১০ matrix-এর det 1010 গুণ ছোট হয়, অথচ সমীকরণ মেলানো একটুও কঠিন হয় না। ওপরের run-এ আরও দেখেছেন, singular matrix-এর det-ও float-এ হুবহু শূন্য না এসে খুব ছোট একটি সংখ্যা আসতে পারে। তাই ব্যবহারিক কাজে det-এর বদলে cond দেখুন।

Rank (np.linalg.matrix_rank) বলে matrix-এর কয়টি সারি (বা কলাম) সত্যিকারের নতুন তথ্য দেয়। অংশ ৫-এর উদাহরণে তৃতীয় রসিদ যদি প্রথম দুটির যোগফল হয়, তাহলে সারি তিনটি হলেও rank 2: তিনটি অজানার জন্য সমীকরণ আসলে দুটি, তাই দাম অনন্যভাবে বের করা সম্ভবই নয়। NumPy rank বের করে singular value দেখে, একটি tolerance মেনে; খুব ছোট singular value-কে শূন্য ধরে। বাস্তবের ডেটায় এটিই বেশি ঘটে: দুটি feature প্রায় একই তথ্য দেয় (যেমন কেজিতে ওজন আর গ্রামে ওজন), আর তখন matrix প্রায়-singular, cond আকাশছোঁয়া।

৭ · Norm — দৈর্ঘ্য, দূরত্ব আর L1, L2, max

বাজারঘর চায় একই ধরনের ক্রেতাদের এক দলে রাখতে, যাতে “আপনার মতো ক্রেতারা এটিও কিনেছেন” ধরনের প্রস্তাব দেওয়া যায়। প্রথম প্রশ্ন: দুজন ক্রেতা “কতটা কাছাকাছি”? প্রত্যেক ক্রেতার মাসিক কেনাকাটা একটি vector (চাল, ডাল, তেল, চিনি), তাই প্রশ্নটি হলো দুটি vector-এর মধ্যে দূরত্ব। আর দূরত্ব মানে পার্থক্য-vector-এর norm, মানে তার “দৈর্ঘ্য”। কিন্তু দৈর্ঘ্য মাপার উপায় একটি নয়।

ঢাকার রাস্তার কথা ভাবুন। বাসা থেকে বাজার পূর্বে ৪ ব্লক আর উত্তরে ৩ ব্লক। রিকশা দালানের ভেতর দিয়ে যেতে পারে না, রাস্তা ধরে যায়: 4 + 3 = 7 ব্লক। এটি L1 norm, যার আরেক নাম Manhattan দূরত্ব। কাক সোজা উড়ে যায়: √(4² + 3²) = 5 ব্লক। এটি L2 norm, স্কুলের পিথাগোরাসের দূরত্ব, যার আরেক নাম Euclidean distance. আর যদি প্রশ্ন হয় “কোন দিকে সবচেয়ে বেশি সরতে হবে”, উত্তর 4: এটি max norm (L∞, Chebyshev দূরত্ব)।

একই দুটি বিন্দু, তিন রকম দূরত্ব

L1 (Manhattan), L2 (Euclidean) and max (Chebyshev) for d = (4, 3)

L1, L2 আর max norm রাস্তার ছকে বাসা বাজার পূর্বে 4 ব্লক উত্তরে 3 L1 = |4| + |3| = 7 (রিকশার পথ) L2 = √(4² + 3²) = 5 (সোজা লাইন) max = max(|4|, |3|) = 4
ছবি ১৭.২ — পার্থক্য-vector d = (4, 3). হলুদ সিঁড়ির পথ যেভাবেই ঘুরুক, মোট সবসময় 7 ব্লক (L1). সবুজ সোজা লাইন 5 (L2). লাল দাগ শুধু সবচেয়ে বড় একক পার্থক্যটি গোনে (max). সবসময় max ≤ L2 ≤ L1.

সূত্রে, n ঘরের একটি vector d-এর জন্য (|·| মানে চিহ্ন বাদ দেওয়া মান, absolute value):

‖d‖1 = ∑ |dj| · ‖d‖2 = √(∑ dj2) · ‖d‖∞ = max |dj|

NumPy-তে তিনটিই np.linalg.norm: দ্বিতীয় argument না দিলে L2, 1 দিলে L1, np.inf দিলে max. দুটি সতর্কতা: ২-মাত্রার array দিলে default হলো পুরো matrix-এর Frobenius norm (সব ঘরের বর্গের যোগের বর্গমূল), সারি ধরে নয়; সারি ধরে চাইলে axis=1 দিতে হয়। আর matrix-এ ord=1 মানে কলামের যোগফলের সর্বোচ্চ, vector-এর L1 নয়। NumPy 2.0 থেকে এই বিভ্রান্তি এড়াতে আলাদা np.linalg.vector_norm আর np.linalg.matrix_norm-ও আছে। এবার তিনজন ক্রেতার দূরত্ব মাপি।

import numpy as np

# মাসে কেনা: চাল (কেজি), ডাল (কেজি), তেল (লিটার), চিনি (কেজি)
names = ["রিনা", "সুমন", "আলম"]
X = np.array([[20, 4, 5, 2],
              [ 5, 2, 1, 1],
              [18, 6, 4, 3]])

d = X[0] - X[2]                       # রিনা − আলম
print("পার্থক্য:", d)
print("L1 :", np.linalg.norm(d, 1), "=", np.abs(d).sum())
print("L2 :", round(np.linalg.norm(d), 3), "=", round(np.sqrt((d ** 2).sum()), 3))
print("max:", np.linalg.norm(d, np.inf), "=", np.abs(d).max())

diff = X[:, None, :] - X[None, :, :]  # (3,1,4) − (1,3,4) → (3,3,4)
print("সব জোড়ার L1:\n", np.abs(diff).sum(axis=2))
print("সব জোড়ার L2:\n", np.linalg.norm(diff, axis=2).round(2))

Xg = X.astype(float)
Xg[:, 3] *= 1000                      # চিনি এবার গ্রামে
dg = np.linalg.norm(Xg[:, None, :] - Xg[None, :, :], axis=2)
print(names, "থেকে রিনার L2 (চিনি গ্রামে):", dg[0].round(1))

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

পার্থক্য: [ 2 -2  1 -1]
L1 : 6.0 = 6
L2 : 3.162 = 3.162
max: 2.0 = 2
সব জোড়ার L1:
 [[ 0 22  6]
 [22  0 22]
 [ 6 22  0]]
সব জোড়ার L2:
 [[ 0.   15.68  3.16]
 [15.68  0.   14.07]
 [ 3.16 14.07  0.  ]]
['রিনা', 'সুমন', 'আলম'] থেকে রিনার L2 (চিনি গ্রামে): [   0.  1000.1 1000. ]

রিনা আর আলমের পার্থক্য [2, −2, 1, −1]: L1 = 2 + 2 + 1 + 1 = 6, L2 = √(4 + 4 + 1 + 1) = √10 ≈ 3.162, max = 2. প্রতিটি norm হাতের সূত্রের সঙ্গে মিলেছে (norm সবসময় float দেয়, তাই 6.0). সব জোড়ার টেবিল বানানো হয়েছে broadcasting দিয়ে: (3, 1, 4) − (1, 3, 4) → (3, 3, 4), তারপর শেষ মাত্রা ধরে norm. টেবিলটি প্রতিসম (রিনা থেকে সুমন = সুমন থেকে রিনা) আর কর্ণে 0. একটি মজার পার্থক্য: L1-এ সুমন রিনা আর আলম দুজন থেকেই সমান দূরে (22), কিন্তু L2-এ সে আলমের একটু কাছে (14.07 বনাম 15.68), কারণ রিনার সঙ্গে তার পার্থক্য একটি বড় ঘরে (চাল 15 কেজি) জমা, আর L2 বড় পার্থক্যকে বর্গ করে বেশি শাস্তি দেয়।

শেষ লাইনটি সবচেয়ে গুরুত্বপূর্ণ। শুধু চিনির একক কেজি থেকে গ্রামে বদলানো হয়েছে, ডেটার অর্থ একটুও বদলায়নি। কিন্তু এখন রিনা থেকে সুমন 1000.1 আর আলম 1000.0: যে আলম আগে স্পষ্টভাবে সবচেয়ে কাছের ছিল (3.16 বনাম 15.68), সেই পার্থক্য প্রায় মুছে গেছে, কারণ চিনির কলামের বিশাল সংখ্যাগুলো একাই দূরত্ব ঠিক করে দিচ্ছে। দূরত্বের হিসাব এককের দাস; তাই কলামগুলো তুলনীয় মাপে আনা (scaling) দূরত্বভিত্তিক যেকোনো পদ্ধতির আগের ধাপ।

কোন দূরত্ব কখন? L2 সবচেয়ে প্রচলিত, আর বড় পার্থক্যকে বর্গ করে বেশি গুরুত্ব দেয়; তাই একটি বড় ব্যতিক্রমী মান (outlier) L2-কে বেশি টানে। L1 প্রতিটি পার্থক্যকে সমান হারে গোনে, outlier-এ তুলনামূলক কম স্পর্শকাতর। max শুধু সবচেয়ে খারাপ ঘরটি দেখে, যেমন “কোনো পণ্যের মজুত যেন ৫ কেজির বেশি না সরে” ধরনের নিয়মে। পর্ব ৫৭-এ (k-Nearest Neighbors) এই দূরত্ব দিয়েই প্রতিবেশী খোঁজা হবে, আর পর্ব ৬৩-এ (feature engineering) দেখবেন কেন দূরত্বের আগে scaling লাগে। Vector-এর দৈর্ঘ্য আর কোণের জ্যামিতিক অর্থ, আর cosine similarity কেন অনেক সময় দূরত্বের চেয়ে ভালো, তা পর্ব ৩৩-এর বিষয়।

৮ · পরের পর্বগুলোর সেতু — least squares, neural layer আর eigenvalue

Least squares — সমীকরণ বেশি, উত্তর “সবচেয়ে কাছের”

বাস্তবে রসিদ তিনটি নয়, শত শত; আর সবগুলো নিখুঁতও নয়। কোথাও ডেলিভারির খরচ মিশে গেছে, কোথাও দোকানি ৫ টাকা ছাড় দিয়েছেন। তখন A লম্বা (সারি বেশি, কলাম কম), আর এমন কোনো দাম নেই যা সব রসিদ হুবহু মেলায়। এর সমাধান least squares: এমন x খোঁজা, যাতে অমিলের বর্গের যোগফল ‖Ax − b‖22 সবচেয়ে কম হয়। NumPy-তে np.linalg.lstsq. পাঁচটি রসিদ দিয়ে ডেস্কটপে চালানো একটি উদাহরণ (সত্যিকারের run, Python 3.14.4, NumPy 2.5.1):

>>> Q5 = np.array([[2, 1, 1], [5, 2, 0], [1, 0, 2], [3, 1, 1], [0, 2, 1]], dtype=float)
>>> t5 = np.array([472, 633, 455, 548, 447], dtype=float)   # সামান্য গোলমালসহ মোট
>>> x, res, rank, sv = np.linalg.lstsq(Q5, t5)
>>> x
array([ 75.6 , 128.35, 190.63])
>>> res, rank
(array([14.7]), np.int32(3))
>>> Q5 @ x
array([470.18, 634.69, 456.86, 545.78, 447.33])

(np.set_printoptions(precision=2) দিয়ে ছাপা; rank-এর int32 এসেছে ভেতরের LAPACK routine থেকে; অন্য build-এ dtype আলাদা দেখাতে পারে।) পাঁচটি সমীকরণ, তিনটি অজানা: উত্তর [75.6, 128.35, 190.63], আসল দামের (75, 130, 190) খুব কাছে, কিন্তু হুবহু নয়, কারণ রসিদগুলোতেই গোলমাল ছিল। res হলো অমিলের বর্গের যোগফল, 14.7 (যেমন প্রথম রসিদে 472 − 470.18 = 1.82). এটিই linear regression-এর হৃদয়: feature-এর matrix, লক্ষ্যের vector, আর সবচেয়ে ভালো ওজন। পর্ব ৫৮-এ (Linear regression — loss, gradient descent আর closed form) এই একই সমস্যা gradient descent আর closed form দুই পথে সমাধান হবে; closed form-এর বিখ্যাত সূত্রে A.T @ A-এর inverse আসে, আর অংশ ৬ পড়ার পর আপনি জানেন কেন বাস্তব library সরাসরি সেই inverse বানায় না।

Neural network-এর একটি layer = matrix গুণ + bias

Neural network শুনলে জটিল কিছু মনে হয়, কিন্তু তার একটি সাধারণ (dense বা fully connected) layer আসলে এই পর্বেরই হিসাব: ইনপুট vector x, একটি weight matrix W, একটি bias vector b, আর ফল W @ x + b, তারপর একটি সরল অরৈখিক (non-linear) function. ৩টি ইনপুট থেকে ২টি আউটপুট হলে W-এর shape (2, 3). একসঙ্গে অনেক সারি (batch) দিলে সাধারণত লেখা হয় X @ W.T + b: (4, 3) @ (3, 2) → (4, 2), আর b broadcasting-এ প্রতিটি সারিতে যোগ হয়। ডেস্কটপে চালানো (সত্যিকারের run):

>>> W = np.array([[0.2, -0.5, 0.1], [0.4, 0.3, -0.2]]); b = np.array([0.1, -0.3])
>>> W @ np.array([1.0, 2.0, 3.0]) + b
array([-0.4,  0.1])
>>> X = np.array([[1., 2., 3.], [0., 1., 0.], [2., 0., 1.], [1., 1., 1.]])
>>> (X @ W.T + b).shape
(4, 2)

হাতে মিলিয়ে নিন: প্রথম আউটপুট 0.2×1 − 0.5×2 + 0.1×3 + 0.1 = 0.2 − 1.0 + 0.3 + 0.1 = −0.4. Network-এর “শেখা” মানে আসলে এই W আর b-এর সংখ্যাগুলো বদলানো। পর্ব ৬৮-এ (Neural network-এর ধারণা — perceptron থেকে ছোট network, NumPy দিয়ে) ঠিক এই লাইনগুলো দিয়ে একটি ছোট network বানানো হবে। আর আধুনিক AI কেন GPU-তে চলে, তার একটি বড় উত্তর এখানেই: বিশাল মডেলের হিসাবের বড় অংশই এমন matrix গুণ, যা একসঙ্গে হাজারো ঘরে করা যায়।

আরও গভীরে: eigenvalue আর PCA. কিছু vector আছে যাদের একটি matrix দিয়ে গুণ করলে দিক বদলায় না, শুধু লম্বা বা খাটো হয়: A v = λ v. এমন v হলো eigenvector, আর λ (ল্যামডা) সংখ্যাটি eigenvalue. যেমন [[2, 1], [1, 2]]-এর eigenvalue 1 আর 3 (np.linalg.eigh, প্রতিসম matrix-এর জন্য; সাধারণ matrix-এ np.linalg.eig). ডেটার covariance matrix-এর সবচেয়ে বড় eigenvalue-র eigenvector বলে ডেটা কোন দিকে সবচেয়ে বেশি ছড়ানো; এটিই পর্ব ৬৫-এর (Clustering আর PCA) মূল ধারণা, যদিও বাস্তব library সাধারণত SVD (singular value decomposition) দিয়ে কাজটি করে, কারণ তা সংখ্যাগতভাবে বেশি স্থির। এই সব function-এর পেছনে আছে LAPACK, যার প্রথম প্রকাশ ১৯৯২ সালে বলে উল্লেখ করা হয়, আর তারও আগের LINPACK আর EISPACK; Gene Golub আর Charles Van Loan-এর বই “Matrix Computations” এই জগতের প্রামাণ্য পাঠ্য হিসেবে পরিচিত।

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

  • NumPy-তে A * B আর A @ B-এর পার্থক্য? প্রথমটি ঘরে ঘরে গুণ (broadcasting মেনে), দ্বিতীয়টি matrix গুণ, যেখানে ভেতরের মাত্রা মিলতে হয়।
  • (5, 3) @ (3, 7)-এর shape? (5, 7); ভেতরের 3 মিলেছে, বাইরের 5 আর 7 থাকে। (5, 3) @ (5, 3) হলে error; A.T @ A দিলে (3, 3).
  • inverse না করে কেন solve? কম হিসাব, ill-conditioned matrix-এ সাধারণত কম ভুল, আর inverse নিজে খুব কমই দরকার।
  • Condition number 108 মানে কী? float64-এর প্রায় ১৬ অঙ্কের মধ্যে প্রায় ৮টি হারানোর ঝুঁকি; উত্তরে মোটামুটি ৭–৮টি অঙ্ক ভরসার।
  • d = (3, −4)-এর L1, L2, max? 7, 5, 4.

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

“এই সমীকরণগুলো NumPy দিয়ে সমাধান করো” বা “ক্রেতাদের দূরত্ব বের করো” লিখলে AI সহকারী দ্রুত কোড দেয়, আর সেই কোড প্রায়ই চলে। কিন্তু এই পর্বের বিষয়গুলোতে চলা মানেই ঠিক নয়, কারণ ভুল shape বা ভুল পদ্ধতি সাধারণত error দেয় না। AI সহকারীরা প্রায়ই যা করে:

  • @-এর জায়গায় * লেখে, বা উল্টো; (n,) আর (n, 1) গুলিয়ে ফেলে, আর ভুল মাত্রা মেলাতে অন্ধভাবে .T বা reshape বসিয়ে দেয়।
  • np.linalg.inv(A) @ b লেখে, condition number না দেখেই; singular বা প্রায়-singular matrix-এ except: pass দিয়ে error চাপা দেয়, বা pinv দিয়ে “চুপচাপ একটি উত্তর” বের করে ফেলে।
  • পুরোনো বা বানানো API লেখে: np.matrix, NumPy 2-এ সরানো np.float, অথবা এমন parameter (np.linalg.norm(x, p=1)-এর মতো, যেখানে আসল নাম ord) যা নেই।
  • দূরত্ব মাপার আগে এককের পার্থক্য (কেজি বনাম গ্রাম, টাকা বনাম হাজার টাকা) উপেক্ষা করে, আর scaling-এর জন্য mean ও standard deviation পুরো ডেটা থেকে নিয়ে test ডেটার তথ্য train-এ মিশিয়ে ফেলে; শেষে train ডেটাতেই মাপা ফল দেখায়।
  • বিশ্বাসযোগ্য শোনায় কিন্তু না-মাপা দাবি করে: “এই matrix সবসময় invertible”, “এটি ১০০ গুণ দ্রুত”, বা একটি residual বা error-এর মান হাতে বানিয়ে লেখে।

কোড চাওয়ার সময় shape, পদ্ধতি আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে খেয়াল করুন: আসল ডেটা নেই, বানানো তিনটি সারি; shape স্পষ্ট; আর inverse আর error চাপা দেওয়া আগেই নিষেধ।

আমি Python 3.14 আর NumPy 2.x ব্যবহার করছি। বানানো উদাহরণ: A = [[2,1,1],[5,2,0],[1,0,2]] (shape (3,3), সারি = অর্ডার, কলাম = পণ্যের পরিমাণ), b = [470,635,455] (shape (3,), মোট টাকা)। ১. পণ্যের দাম x বের করার একটি function লেখো, np.linalg.solve দিয়ে; np.linalg.inv ব্যবহার করবে না। ২. solve-এর আগে A বর্গাকার কি না আর b-এর shape মেলে কি না assert দিয়ে দেখো, আর np.linalg.cond ছাপাও; cond 1e10-এর বেশি হলে সতর্কবার্তা দাও, কিন্তু কোনো error চাপা দেবে না। ৩. উত্তর পেলে np.allclose(A @ x, b) দিয়ে যাচাই করো। ৪. A বর্গাকার না হলে (সারি বেশি) np.linalg.lstsq ব্যবহার করো আর residual ফেরত দাও। ৫. প্রতিটি লাইনের পাশে ফলের shape কমেন্টে লেখো। কোনো function বা parameter আমার NumPy version-এ আছে কি না নিশ্চিত না হলে বলো, বানিয়ে লিখবে না।

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

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

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

প্র ০১“Matrix-এর inverse প্রায় কখনো হিসাব করবেন না” — সংখ্যাগত গণিতের এই পুরোনো উপদেশ কোথা থেকে এলো?

দ্বিতীয় বিশ্বযুদ্ধের পরপর যখন প্রথম ইলেকট্রনিক কম্পিউটার আসে, তখন বড় matrix নিয়ে হিসাব করা ছিল তাদের প্রধান কাজগুলোর একটি। একটি বড় দুশ্চিন্তা ছিল: হাজার হাজার গুণ-ভাগে প্রতিবার যে ছোট rounding ভুল হয়, তা জমে কি উত্তরকে পুরোপুরি নষ্ট করে দেবে? ১৯৪৭ সালে John von Neumann আর Herman Goldstine বড় matrix-এর inverse নিয়ে একটি দীর্ঘ প্রবন্ধ প্রকাশ করেন, যাকে অনেকে আধুনিক সংখ্যাগত বিশ্লেষণের শুরুর একটি দলিল বলেন। ১৯৪৮ সালে Alan Turing “Rounding-off errors in matrix processes” নামে একটি প্রবন্ধে matrix-এর হিসাবকে ত্রিভুজাকার অংশে ভাঙার (আজকের LU decomposition) কথা বলেন, আর সেখানেই “condition number” শব্দটি প্রথম ব্যবহৃত হয় বলে সাধারণভাবে ধরা হয়।

পরের দশকগুলোতে James Wilkinson “backward error analysis” নামে একটি দৃষ্টিভঙ্গি জনপ্রিয় করেন। প্রশ্নটি বদলে যায়: “উত্তর কতটা ভুল?” না জিজ্ঞেস করে জিজ্ঞেস করা হয় “পাওয়া উত্তরটি কোন সামান্য বদলানো সমস্যার নিখুঁত উত্তর?” যদি সেই বদল machine precision-এর ঘরে থাকে, পদ্ধতিটি backward stable. Partial pivoting-সহ Gaussian elimination বাস্তবে প্রায় সবসময় এমন, আর অংশ ৬-এর run-এ solve-এর residual যে 10−16-এর ঘরে ছিল, তা এরই চিহ্ন। Inverse বানিয়ে গুণ করার পথে এই নিশ্চয়তা দুর্বল; Nicholas Higham-এর মতো বিশেষজ্ঞদের বইয়ে এই তুলনা বিশদে আছে।

উপদেশটির আরেকটি দিক ব্যবহারিক। Inverse একটি ঘন (dense) matrix; বাস্তবের অনেক বড় matrix sparse (বেশির ভাগ ঘর শূন্য), আর তার inverse সাধারণত ঘন হয়ে বিশাল মেমরি খায়, অথচ solve বা factorization sparse গঠন অনেকটা রাখতে পারে। তবে উপদেশটি অন্ধ নিয়মও নয়। পরিসংখ্যানে কখনো inverse-এর ঘরগুলোরই অর্থ থাকে (যেমন regression-এ coefficient-এর variance), তখন সাবধানে হিসাব করা হয়। আর খুব ছোট, ভালো-আচরণের matrix-এ পার্থক্য প্রায় চোখেই পড়ে না।

মূল উপলব্ধি: কম্পিউটারে গণিতের “সমান” সূত্রগুলো সমান নির্ভুল নয়; x = A−1b আর “Ax = b-এর সরাসরি সমাধান” কাগজে এক, কিন্তু float-এর জগতে দ্বিতীয়টি সাধারণত বেশি নিরাপদ, আর সেটি কেন, তা বোঝাই সংখ্যাগত চিন্তার শুরু।

প্র ০২NumPy নিজে matrix গুণ লেখে না, ডাকে BLAS আর LAPACK — এই পুরোনো library-গুলো আজও কেন এত গুরুত্বপূর্ণ?

১৯৭০-এর দশকে বিজ্ঞানীরা লক্ষ করেন, প্রায় সব বৈজ্ঞানিক প্রোগ্রাম একই কয়েকটি মৌলিক কাজ বারবার করে: দুটি vector-এর dot product, vector-এ একটি সংখ্যা গুণ করে যোগ, matrix-vector গুণ, matrix-matrix গুণ। ১৯৭৯ সালে Lawson, Hanson, Kincaid আর Krogh এই মৌলিক কাজগুলোর একটি standard interface প্রকাশ করেন, যার নাম BLAS (Basic Linear Algebra Subprograms). পরে level 2 (matrix-vector) আর level 3 (matrix-matrix) যুক্ত হয়। এর ওপর গড়ে ওঠে LINPACK আর EISPACK, আর পরে দুটিকে মিলিয়ে আধুনিক কম্পিউটারের জন্য LAPACK, যার প্রথম সংস্করণ সাধারণত ১৯৯২ সালের বলে উল্লেখ করা হয়।

বুদ্ধিটি ছিল দায়িত্ব ভাগ করা। Interface একই থাকে, কিন্তু প্রতিটি CPU নির্মাতা বা দল নিজের মেশিনের জন্য সবচেয়ে দ্রুত সংস্করণ লেখে: OpenBLAS, Intel-এর MKL, Apple-এর Accelerate ইত্যাদি। এদের ভেতরে থাকে cache-এ ধরার মতো টুকরো করে গুণ (blocking), একসঙ্গে কয়েকটি সংখ্যা গুণের CPU নির্দেশ (SIMD) আর একাধিক core-এ ভাগ করা। ফলে একই NumPy কোড একটি ল্যাপটপে আর একটি সার্ভারে ভিন্ন গতিতে চলে, আর শেষ দশমিক অঙ্কও সামান্য ভিন্ন হতে পারে, কারণ যোগের ক্রম আলাদা। এই পাতার ব্রাউজার-run আর ডেস্কটপ-run-এর সংখ্যায় পার্থক্যের একটি কারণও এটি।

LINPACK থেকে আরেকটি বিখ্যাত জিনিস জন্ম নেয়: Jack Dongarra-র LINPACK benchmark, মানে একটি বড় linear system কত দ্রুত সমাধান হয় তার মাপ। ১৯৯৩ সাল থেকে বিশ্বের দ্রুততম supercomputer-এর TOP500 তালিকা এর একটি সংস্করণ (HPL) দিয়েই সাজানো হয়, আর Dongarra ২০২১ সালের Turing Award পান। সমালোচনাও আছে: একটি matrix সমাধানের গতি দিয়ে সব বৈজ্ঞানিক কাজের গতি বোঝা যায় না, তাই অন্য benchmark-ও প্রস্তাব করা হয়েছে।

মূল উপলব্ধি: A @ B লেখা সহজ, কারণ তার নিচে কয়েক দশকের পরিশ্রমী প্রকৌশল লুকানো; ভালো interface একবার ঠিক হলে ভেতরের বাস্তবায়ন বারবার উন্নত করা যায়, আর আপনার কোড না বদলেই দ্রুত হয়।

প্র ০৩Matrix গুণ কি এর চেয়ে দ্রুত করা যায়? Strassen থেকে আধুনিক AI-এর খোঁজ পর্যন্ত গল্পটি কী?

দুটি n×n matrix স্কুলের নিয়মে গুণ করলে ফলের n² ঘরের প্রতিটিতে n-টি গুণ লাগে, মোট প্রায় n³. বহুদিন ধারণা ছিল এর চেয়ে কম সম্ভব নয়। ১৯৬৯ সালে Volker Strassen দেখান, ২×২ matrix ৮টির বদলে ৭টি গুণে করা যায়, কিছু বাড়তি যোগের বিনিময়ে। বড় matrix-কে বারবার ২×২ টুকরোয় ভেঙে এই কৌশল খাটালে মোট খরচ নামে প্রায় n2.81-এ। এরপর তাত্ত্বিক গবেষকেরা ঘাতটি ধীরে ধীরে আরও কমিয়েছেন (Coppersmith–Winograd ধারার কাজ), বর্তমানে তা ২.৩৭-এর কাছাকাছি বলে জানা যায়, যদিও সঠিক সর্বশেষ মান নিয়মিত সামান্য বদলায়।

মজার ব্যাপার, বাস্তবের BLAS এই দ্রুততম তাত্ত্বিক পদ্ধতিগুলো সাধারণত ব্যবহার করে না। কারণ সেগুলোর লুকানো ধ্রুবক বিশাল, কেবল কল্পনাতীত বড় matrix-এ লাভ দেয়, আর কিছু ক্ষেত্রে সংখ্যাগত স্থিরতাও কম। Strassen নিজে কিছু বিশেষ লাইব্রেরিতে বড় matrix-এ ব্যবহৃত হয়, কিন্তু সাধারণ কাজে cache আর CPU-র সঙ্গে মানানসই n³ পদ্ধতিই রাজা। তাত্ত্বিক “কম গুণ” আর বাস্তব “কম সময়” এক জিনিস নয়, প্রশ্ন ২-এর blocking আর SIMD তার কারণ।

২০২২ সালে DeepMind-এর গবেষকেরা Nature-এ AlphaTensor নামে একটি কাজ প্রকাশ করেন, যেখানে reinforcement learning দিয়ে ছোট নির্দিষ্ট আকারের matrix গুণের নতুন পদ্ধতি খোঁজা হয়; কয়েকটি আকারে তারা আগের জানা পদ্ধতির চেয়ে কম গুণের পথ পান বলে জানান। পরে অন্য গবেষকেরা কিছু ফল হাতে-কলমে আরও উন্নত করেছেন বলেও প্রতিবেদন আছে। এটি AI দিয়ে গণিতের খোঁজের একটি আকর্ষণীয় উদাহরণ, তবে “AI গণিত বুঝে ফেলেছে” ধরনের দাবি এখান থেকে করা যায় না; একটি নির্দিষ্ট, ভালোভাবে সংজ্ঞায়িত খোঁজের জায়গায় এটি কাজে লেগেছে।

মূল উপলব্ধি: গুণের সংখ্যা কমানো একটি গভীর তাত্ত্বিক প্রশ্ন, কিন্তু বাস্তব গতি আসে মেমরি আর hardware-এর সঙ্গে মানিয়ে চলা থেকে; আর AI-এর এত বড় অংশ matrix গুণ বলেই এই পুরোনো প্রশ্নটি আজ আবার এত মনোযোগ পাচ্ছে।

প্র ০৪L1 না L2 — কোন দূরত্ব “ঠিক”? অনেক মাত্রার ডেটায় দূরত্বের কী অদ্ভুত আচরণ দেখা যায়?

কোনো দূরত্বই সব কাজে “ঠিক” নয়; প্রশ্ন হলো সমস্যার কোন অর্থটি আপনি ধরতে চান। শহরের রাস্তায় রিকশার খরচ L1-এর মতো, উড়োজাহাজের পথ L2-এর কাছাকাছি, আর একটি কারখানার মান-নিয়ন্ত্রণে “কোনো মাপ যেন সীমা না ছাড়ায়” হলো max norm. ডেটায় L2 বড় পার্থক্যকে বর্গ করে, তাই একটি অস্বাভাবিক মান তাকে অনেক টানে; L1 সেখানে তুলনামূলক শান্ত। একই কারণে পরিসংখ্যানে গড় (mean) L2-এর সঙ্গে আর মধ্যমা (median) L1-এর সঙ্গে স্বাভাবিকভাবে যুক্ত: গড় হলো সেই বিন্দু যেখান থেকে বর্গ-দূরত্বের যোগফল সবচেয়ে কম, median হলো যেখান থেকে absolute দূরত্বের যোগফল সবচেয়ে কম।

অনেক মাত্রায় (শত বা হাজার feature) একটি অদ্ভুত ঘটনা ঘটে, যাকে প্রায়ই “curse of dimensionality”-র অংশ বলা হয়। এলোমেলো বিন্দুর জন্য সবচেয়ে কাছের আর সবচেয়ে দূরের প্রতিবেশীর দূরত্ব আপেক্ষিকভাবে প্রায় সমান হয়ে আসে, ফলে “কাছের প্রতিবেশী” কথাটির অর্থ ক্ষীণ হয়ে যায়। ১৯৯৯ সালে Beyer ও সহকর্মীদের একটি প্রবন্ধ এই প্রশ্ন তোলে, আর ২০০১ সালে Aggarwal, Hinneburg ও Keim দেখান যে এমন পরিস্থিতিতে L1 প্রায়ই L2-এর চেয়ে ভালো পার্থক্য রাখে। তবে এগুলো নির্দিষ্ট ধরনের এলোমেলো ডেটার ফল; বাস্তব ডেটায় গঠন থাকে, আর ফল ডেটার ওপর নির্ভর করে।

Norm শুধু দূরত্ব মাপে না, মডেলকে নিয়ন্ত্রণও করে। ওজনের L2 norm-এর ওপর শাস্তি দেওয়ার ধারণা (ridge regression, Hoerl আর Kennard, ১৯৭০) ওজনকে ছোট রাখে; L1 norm-এর ওপর শাস্তি (lasso, Robert Tibshirani, ১৯৯৬) অনেক ওজনকে হুবহু শূন্যে নামায়, ফলে মডেল কম feature ব্যবহার করে। পর্ব ৬১-এ (Overfitting, bias আর variance — regularization আর learning curve) এই ধারণাই আসবে। আর অংশ ৭-এর run-এ যেমন দেখেছেন, একক বদলালেই (কেজি থেকে গ্রাম) দূরত্বের পুরো ছবি বদলে যায়; তাই কোন norm, তার আগে প্রশ্ন হলো কোন scaling.

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

প্র ০৫Least squares-এর আবিষ্কার নিয়ে Gauss আর Legendre-এর বিতর্কটি কী ছিল, আর একটি গ্রহাণু এর সঙ্গে কীভাবে জড়িত?

১৮০১ সালের ১ জানুয়ারি ইতালির জ্যোতির্বিদ Giuseppe Piazzi একটি নতুন জ্যোতিষ্ক দেখেন, পরে যার নাম হয় Ceres. কয়েক সপ্তাহ দেখার পর সেটি সূর্যের আলোর আড়ালে হারিয়ে যায়, আর মাত্র অল্প কয়েকটি পর্যবেক্ষণ থেকে আবার কোথায় দেখা যাবে, তা বলা খুব কঠিন হয়ে পড়ে। তরুণ গণিতবিদ Carl Friedrich Gauss সেই অল্প ডেটা থেকে কক্ষপথ হিসাব করে একটি অবস্থান বলেন, আর বছরের শেষ দিকে জ্যোতির্বিদেরা প্রায় সেখানেই Ceres-কে আবার খুঁজে পান। এই সাফল্য Gauss-কে বিখ্যাত করে দেয়, আর তাঁর পদ্ধতির কেন্দ্রে ছিল পর্যবেক্ষণের ভুলকে বুদ্ধিমানের মতো গড় করার একটি উপায়।

কিন্তু least squares পদ্ধতি প্রথম ছাপা হয় Adrien-Marie Legendre-এর নামে, ১৮০৫ সালে, ধূমকেতুর কক্ষপথ নিয়ে একটি লেখায়। ১৮০৯ সালে Gauss তাঁর বইয়ে পদ্ধতিটি প্রকাশ করে দাবি করেন যে তিনি ১৭৯৫ সাল থেকেই এটি ব্যবহার করছিলেন। Legendre এতে ক্ষুব্ধ হন, আর ইতিহাসবিদেরা আজও এই অগ্রাধিকারের প্রশ্নে পুরোপুরি একমত নন: Gauss যে আগে ব্যবহার করেছিলেন তার কিছু পরোক্ষ প্রমাণ আছে, কিন্তু প্রথম প্রকাশ নিঃসন্দেহে Legendre-এর। Gauss অবশ্য একটি গভীর যোগও দেন: ভুলগুলো যদি normal distribution মেনে চলে, তাহলে least squares-এর উত্তরই সবচেয়ে সম্ভাব্য উত্তর। পরে Gauss–Markov উপপাদ্য আরও দুর্বল শর্তে এর একটি বিশেষ গুণ প্রমাণ করে।

দুশো বছর পরেও np.linalg.lstsq একই প্রশ্নের উত্তর দেয়, তবে ভিন্ন হাতিয়ারে: পুরোনো “normal equations” (AᵀA x = Aᵀb) সরাসরি সমাধান না করে, সংখ্যাগতভাবে বেশি স্থির SVD-ভিত্তিক LAPACK routine ব্যবহার করে। কারণটি অংশ ৬-এ দেখা: AᵀA-র condition number মোটামুটি A-র condition number-এর বর্গ, তাই normal equations বানালেই আরও বেশি অঙ্ক হারানোর ঝুঁকি।

মূল উপলব্ধি: Least squares জন্ম নিয়েছিল অসম্পূর্ণ, গোলমেলে পর্যবেক্ষণ থেকে সবচেয়ে ভালো অনুমানের প্রয়োজনে; linear regression থেকে আজকের মেশিন লার্নিং পর্যন্ত সেই একই প্রশ্ন চলছে, আর সূত্রের চেয়ে তা কীভাবে নিরাপদে হিসাব করবেন, সেটিই প্রায়ই আসল কাজ।

অনুশীলন

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

  1. হাতে হিসাব: দাম p = [60, 120, 200] (টাকা)। দুটি অর্ডার Q = [[3, 0, 1], [1, 2, 2]]. (ক) Q @ p আর তার shape কী? (খ) Q * p-এর shape আর প্রথম সারি কী? (গ) দুটি অর্ডারের পার্থক্য-vector-এর L1, L2 আর max norm কত?

    (ক) প্রথম অর্ডার: 3×60 + 0×120 + 1×200 = 180 + 0 + 200 = 380. দ্বিতীয়: 1×60 + 2×120 + 2×200 = 60 + 240 + 400 = 700. ফল [380, 700], shape (2,); ভেতরের 3 মিলেছে, বাইরের 2 থাকে।

    (খ) broadcasting-এ shape (2, 3); প্রথম সারি [180, 0, 200], যার যোগফলই (ক)-এর 380.

    (গ) পার্থক্য [3, 0, 1] − [1, 2, 2] = [2, −2, −1]. L1 = 2 + 2 + 1 = 5. L2 = √(4 + 4 + 1) = √9 = 3. max = 2. লক্ষ করুন, max ≤ L2 ≤ L1 মেনেছে: 2 ≤ 3 ≤ 5.

  2. আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি লাইনের আউটপুট লিখুন, তারপর যেকোনো Python-এ চালিয়ে মিলিয়ে দেখুন।
    import numpy as np
    v = np.array([1, 2, 3])
    M = np.arange(6).reshape(2, 3)
    print(v.T.shape, v[:, None].shape)
    print((M @ v).shape, M @ v)
    print((M * v).shape)
    print((M.T @ M).shape)
    print(np.linalg.norm(np.array([3, 4])), np.linalg.norm(np.array([3, 4]), 1))
    (3,) (3, 1)
    (2,) [ 8 26]
    (2, 3)
    (3, 3)
    5.0 7.0

    M হলো [[0, 1, 2], [3, 4, 5]]. ১-মাত্রার v-র transpose কিছু বদলায় না, v[:, None] তাকে কলাম বানায়। M @ v: 0 + 2 + 6 = 8 আর 3 + 8 + 15 = 26. M * v broadcasting-এ (2, 3). M.T @ M: (3, 2) @ (2, 3) → (3, 3), সবসময় বর্গাকার। শেষে (3, 4)-এর L2 = 5 আর L1 = 7; norm সবসময় float দেয়, তাই 5.0 আর 7.0. (আউটপুটটি ডেস্কটপে NumPy 2.5.1-এ চালিয়ে মেলানো।)

  3. নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) শত শত রসিদ থেকে পণ্যের দাম বের করতে নিচের কোড লিখেছেন। অন্তত চারটি সমস্যা চিহ্নিত করুন আর ভালো একটি পথ বলুন।
    A = orders_qty          # shape (500, 3)
    b = orders_total        # shape (500,)
    x = np.linalg.inv(A.T @ A) @ A.T @ b
    pred = A * x
    print("accuracy:", (pred.sum(axis=1) == b).mean())
    • Inverse আর normal equations: inv(A.T @ A) বানানো হয়েছে। A.T @ A-র condition number মোটামুটি A-র cond-এর বর্গ, তার ওপর inverse; ভুল জমার দুই ধাপ। ভালো পথ np.linalg.lstsq(A, b), যা SVD-ভিত্তিক।
    • যাচাই নেই: np.linalg.cond(A) বা matrix_rank(A) দেখা হয়নি। দুটি পণ্য সবসময় একসঙ্গে কেনা হলে কলাম দুটি প্রায় সমানুপাতিক, rank কম, দাম অনন্য নয়।
    • * বনাম @: A * x-এর পরে .sum(axis=1) কাজ চালিয়ে দিলেও উদ্দেশ্যটি A @ x; সরাসরি লিখলে পড়তে সহজ আর দ্রুত।
    • ভুল মাপ: float-এর অনুমানকে == দিয়ে মেলানো প্রায় সবসময় 0 দেবে; আর regression-এ “accuracy” অর্থহীন। Residual, MAE বা RMSE দেখুন, আর সেটি নতুন রসিদে (test set), যে রসিদে দাম বের করা হয়েছে সেগুলোতে নয়।
    • ডেটার প্রশ্ন: ছাড়, ডেলিভারি চার্জ বা দাম বদলের তারিখ মিশে থাকলে একটি স্থির দামের মডেলই ভুল; আগে ডেটা দেখা দরকার।
  4. কোড লিখুন: দুটি পণ্য, চিনি আর লবণ। দুটি রসিদ: ৩ কেজি চিনি আর ২ প্যাকেট লবণ মিলে 450 টাকা; ১ কেজি চিনি আর ৪ প্যাকেট লবণ মিলে 250 টাকা। np.linalg.solve দিয়ে দাম বের করুন, np.allclose দিয়ে যাচাই করুন, আর cond ছাপান। তারপর হাতে মিলিয়ে নিন।
    import numpy as np
    A = np.array([[3, 2], [1, 4]], dtype=float)
    b = np.array([450, 250], dtype=float)
    x = np.linalg.solve(A, b)
    print(x, np.allclose(A @ x, b), round(np.linalg.cond(A), 2))

    প্রত্যাশিত ফল: চিনি 130 টাকা/কেজি, লবণ 30 টাকা/প্যাকেট, True, আর cond প্রায় 2.62 (ভালো-আচরণের matrix). হাতে: প্রথম সমীকরণকে 2 দিয়ে গুণ করে দ্বিতীয়টি বাদ দিলে… সহজ পথ, দ্বিতীয় সমীকরণ থেকে চিনি = 250 − 4·লবণ; প্রথমটিতে বসালে 3(250 − 4ল) + 2ল = 450, মানে 750 − 10ল = 450, ল = 30, চিনি = 250 − 120 = 130. যাচাই: 3×130 + 2×30 = 450 ✓, 130 + 4×30 = 250 ✓.

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

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

ক্রমরসিদের ডেটা থেকে NumPy দিয়ে পণ্যের দাম বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧾 পরিমাণ দিয়ে matrix A আর মোট টাকা দিয়ে vector b বানানো
  2. 📐 A আর b-এর shape মিলিয়ে দেখা
  3. 🌡️ np.linalg.cond আর matrix_rank দেখে ঝুঁকি মাপা
  4. 🧮 বর্গাকার হলে solve, লম্বা হলে lstsq দিয়ে x বের করা
  5. ✅ np.allclose(A @ x, b) বা residual দিয়ে যাচাই
  6. 🆕 নতুন একটি রসিদে অনুমিত মোট মিলিয়ে দেখা

NumPy-তে qty-র shape (4, 3) আর price-এর shape (3,). qty * price-এর shape কী?

  • (4,)
  • (4, 3)
  • (3,)
  • ValueError

NumPy-তে নিচের কোন matrix গুণটি ValueError দেবে?

  • (5, 3) @ (3, 7)
  • (5, 3) @ (3,)
  • A.T @ A, যেখানে A-র shape (5, 3)
  • (5, 3) @ (5,)

বর্গাকার matrix A আর vector b থেকে Ax = b-এর x বের করতে np.linalg.inv(A) @ b-এর বদলে np.linalg.solve(A, b) ব্যবহারের প্রধান কারণ কোনটি?

  • solve সবসময় হুবহু নিখুঁত উত্তর দেয়
  • solve singular matrix-এও অনন্য উত্তর দেয়
  • কম হিসাব লাগে আর সংখ্যাগতভাবে সাধারণত বেশি নির্ভরযোগ্য
  • NumPy 2-এ inv সরিয়ে দেওয়া হয়েছে

float64-এ (প্রায় ১৬টি দশমিক অঙ্ক) একটি matrix-এর condition number প্রায় 1010. এই matrix দিয়ে linear system সমাধান করলে উত্তরে মোটামুটি কয়টি অঙ্ক ভরসার?

  • প্রায় ৬টি
  • প্রায় ১৬টি
  • প্রায় ১০টি
  • একটিও নয়

পার্থক্য-vector d = (3, −4). এর L1, L2 আর max norm যথাক্রমে কত?

  • −1, 5, 3
  • 7, 25, 4
  • 7, 5, 4
  • 5, 7, 3

একটি neural network layer-এ ৮টি সারির ইনপুট X (shape (8, 3)), weight W (shape (3, 5)) আর bias b (shape (5,))। X @ W + b-এর shape কী?

  • (8, 3)
  • (8, 5)
  • (5,)
  • ValueError

শব্দকোষ

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

মনে রাখুন

  1. NumPy-তে vector = ১-মাত্রার array, matrix = ২-মাত্রার array; ১-মাত্রার array-র কোনো সারি-কলাম দিক নেই, .T তাকে বদলায় না, কলাম চাইলে [:, None].
  2. * ঘরে ঘরে গুণ, @ matrix গুণ; matrix × vector মানে প্রতিটি সারির weighted sum, যেমন পরিমাণ @ দাম = মোট বিল।
  3. Shape-এর নিয়ম: (m, k) @ (k, n) → (m, n); ভেতরের মাত্রা মিলতে হয়, প্রতিটি গুণের পরে .shape দেখুন।
  4. Ax = b-এর জন্য np.linalg.solve, inverse নয়; উত্তর np.allclose(A @ x, b) দিয়ে যাচাই, ঝুঁকি np.linalg.cond দিয়ে মাপা (cond ≈ 10k মানে প্রায় k অঙ্ক হারানো); সমীকরণ বেশি হলে lstsq.
  5. Norm দৈর্ঘ্য, পার্থক্যের norm দূরত্ব: L1 রিকশার পথ, L2 সোজা লাইন, max সবচেয়ে বড় পার্থক্য; দূরত্বের আগে একক আর scaling মেলান।

পরের পর্বে: একটি ছবি আসলে কী? দেখবেন, একটি ছবি মানেই সংখ্যার একটি array: pixel, রঙের channel, আর এই পর্বের slicing আর গুণ দিয়ে ছোট ছোট image processing.

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

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