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

Linear regression — loss, gradient descent আর closed form

Linear Regression — Matrix Form, MSE, Normal Equations, lstsq, Feature Scaling, Multicollinearity, Residuals and R²

Taught in Bangla, this lesson fits multi-feature linear regression on fictional delivery data: the design matrix, MSE versus MAE, normal equations solved by hand, why lstsq beats the matrix inverse, gradient descent with and without scaling, reading coefficients, multicollinearity, the dummy-variable trap, residual plots, R² and polynomial overfitting. Real in-browser and desktop runs with NumPy, scikit-learn and statsmodels.

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

  • এক feature-এর ŷ = w·x + b থেকে অনেক feature-এর ŷ = Xw, bias কলামসহ; MSE কেন, MAE কখন
  • Normal equations হাতে তিনটি বিন্দুতে, আর কেন বাস্তবের কোডে lstsq; condition number-এর আসল run
  • অনেক feature-এ vectorised gradient descent, scaling-সহ ও ছাড়া, আর mini-batch-এর জায়গা
  • Coefficient-এর অর্থ (“বাকি সব স্থির রেখে”), multicollinearity, one-hot encoding আর dummy-variable trap
  • Residual plot পড়া, বাঁক ধরতে নতুন feature, R², adjusted R², RMSE, polynomial feature, অনুমানগুলো আর statsmodels-এর standard error ও confidence interval

এই কোর্সে রেখা fit করা নতুন নয়। পর্ব ১৭-এ matrix, solve, condition number আর np.linalg.lstsq এসেছে; পর্ব ৩৪-এ বাজারঘরের ৮টি অর্ডারে এক আর দুই parameter-এর রেখা gradient descent দিয়ে fit করে lstsq-এর সঙ্গে মেলানো হয়েছে। এই পর্ব সেখান থেকে এগোয়: অনেক feature, আসল অর্থে একটি linear regression model, আর সেই model কতটা বিশ্বাস করা যায় তা যাচাইয়ের অভ্যাস। কোড পাতাতেই চলে (ব্রাউজারের ভেতরে Python, Pyodide); scikit-learn বা statsmodels প্রথমবার লোড হতে প্রায় ১০ সেকেন্ড বা তার বেশি লাগতে পারে, পরের run দ্রুত।

১ · Model-টি — ŷ = w·x + b থেকে ŷ = Xw

পর্ব ৩৪-এর model ছিল এক লাইনের: দিন ≈ w × দূরত্ব + b. এখানে w হলো রেখার ঢাল (দূরত্ব এক একক বাড়লে দিন কত বাড়ে), আর b হলো intercept, দূরত্ব শূন্য হলে রেখা যেখানে y-অক্ষ ছোঁয়। কিন্তু ডেলিভারির সময় শুধু দূরত্বে নির্ভর করে না। পার্সেল ভারী হলে বাইকে তুলতে সময় লাগে, পণ্য বেশি হলে মেলাতে সময় লাগে, বৃষ্টি হলে রাস্তা ধীর, অফিস ছুটির ভিড়ে আরও ধীর। প্রতিটি কারণ একটি feature, আর প্রতিটির জন্য একটি করে weight:

ŷ = b + w1x1 + w2x2 + … + wpxp

ŷ (পড়া হয় “y-hat”) হলো model-এর অনুমান, x1 … xp হলো p-টি feature-এর মান, আর w1 … wp হলো তাদের coefficient. চায়ের দোকানের হিসাবের খাতা ভাবুন: “চা ১০ টাকা করে, বিস্কুট ৫ টাকা করে, আর বসার জন্য কিছু না” — মোট বিল = 10 × চা + 5 × বিস্কুট + 0. Linear regression ঠিক এই রকম একটি দামের তালিকা খোঁজে, শুধু “দাম”-গুলো আগে জানা নেই, পুরোনো বিল থেকে বের করতে হয়। “Linear” মানে w-গুলোর ওপর রৈখিক: প্রতিটি w একবার করে, একটি সংখ্যা দিয়ে গুণ হয়ে যোগ হয়।

এখন কৌশলটি, যা পর্ব ৩৪-এর শেষ কোডেও ছিল (column_stack([x, ones])): প্রতিটি অর্ডারের সারিতে সবার আগে একটি বাড়তি কলাম বসাই, যার মান সবসময় 1. তাহলে b-ও একটি সাধারণ weight হয়ে যায় (1 × b = b), আর সব অর্ডারের অনুমান একসঙ্গে একটি matrix গুণ:

ŷ = X w · X-এর shape (n, p + 1), w-এর (p + 1,), ŷ-এর (n,)

Design matrix, weight আর অনুমান — shape মিলিয়ে

ŷ = X @ w for 200 Bazarghar orders, with a column of ones for the bias

X (200 × 6) গুণ w (6) সমান ŷ (200) 1kmkgitemrainpeak 119.611.7110 111.514.6310 121.67.6510 ⋮⋮⋮⋮⋮⋮ X · (200, 6) @ 13.362.430.781.419.116.87 w · (6,) = 80.666.188.0⋮ ŷ · (200,) হলুদ কলাম = সব 1, তাই w-এর প্রথম ঘর = b প্রথম সারি: 13.36 + 2.43×19.6 + 0.78×11.7 + 1.41×1 + 9.11×1 + 6.87×0 ≈ 80.6 মিনিট
ছবি ৫৮.১ — প্রতিটি সারি একটি অর্ডার, প্রতিটি কলাম একটি feature. ভেতরের মাত্রা ৬ মিলেছে বলে গুণ সম্ভব (পর্ব ১৭-এর shape-এর নিয়ম)। w-এর সংখ্যাগুলো অংশ ৪-এর run থেকে নেওয়া; শেষ ঘরের অনুমান হাতে মিলিয়ে দেখতে পারেন (ছোট পার্থক্য দশমিক ছাঁটার কারণে)।

এই এক লাইনের সুবিধা বিশাল। Feature পাঁচটি হোক বা পাঁচশো, সূত্র একই; NumPy-তে একটি @, কোনো loop নেই (পর্ব ১৫-এর vectorization). পরের সব অংশে X বললে এই bias কলামসহ matrix বুঝবেন।

২ · বাজারঘরের কাজ, আর loss কেন বর্গ

বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) শহরের ভেতরের অর্ডারে ক্রেতাকে বলতে চায়, “আপনার পার্সেল আনুমানিক কত মিনিটে পৌঁছাবে।” আমরা ২০০টি অর্ডারের ডেটা নিজেরাই বানাব, একটি জানা নিয়ম দিয়ে, যাতে পরে দেখা যায় পদ্ধতিগুলো সেই নিয়ম ফিরে পায় কি না। পরীক্ষার খাতা দেখার আগে শিক্ষক যেমন উত্তরপত্র নিজে জানেন, তেমন।

Featureকীভাবে বানানোআসল coefficientঅর্থ
biasসব 112প্যাকিং আর হাতবদলের স্থির সময়
km (দূরত্ব)1 থেকে 25, সমান সম্ভাবনায়2.5প্রতি কিমিতে আড়াই মিনিট
kg (ওজন)0.2 থেকে 150.8প্রতি কেজিতে ০.৮ মিনিট
items (পণ্যের সংখ্যা)1 থেকে 8, পূর্ণসংখ্যা1.5প্রতিটি পণ্য মেলাতে দেড় মিনিট
rain (বৃষ্টি)৩০% অর্ডারে 19বৃষ্টিতে ৯ মিনিট বেশি
peak (ভিড়ের সময়)৩৫% অর্ডারে 17ভিড়ে ৭ মিনিট বেশি
গোলমালnormal, গড় 0, SD 4—যা কোনো feature ব্যাখ্যা করে না

সংখ্যাগুলো এই পর্বের জন্য বানানো, কোনো আসল কুরিয়ারের নয়। বাস্তবে “আসল coefficient” কেউ জানে না; এখানে জানি বলেই পদ্ধতি যাচাই করা যায়। এখন প্রতিটি অর্ডারের ভুল বা residual ri = yi − ŷi, আর loss হলো MSE:

MSE(w) = (1/n) · ∑i (yi − ŷi)2 = (1/n) · ‖y − Xw‖2

‖ ‖ হলো vector-এর দৈর্ঘ্য বা L2 norm (পর্ব ১৭)। বর্গ কেন, পরম মান কেন নয়? তিনটি কারণ। এক, বর্গ মসৃণ: সব জায়গায় derivative আছে, তাই gradient descent সহজ, আর derivative শূন্য বসিয়ে সরাসরি সমাধানও পাওয়া যায় (অংশ ৩)। দুই, বর্গ বড় ভুলকে বেশি শাস্তি দেয়: ১০ মিনিটের একটি ভুল ১ মিনিটের দশটি ভুলের চেয়ে খারাপ (100 বনাম 10). ক্রেতার কাছে প্রায়ই সেটাই সত্য। তিন, গোলমাল যদি normal distribution মেনে চলে, তাহলে MSE কমানো আর maximum likelihood একই উত্তর দেয় (পর্ব ৩৮-এ MLE-এর যোগসূত্র)।

দ্বিতীয় কারণটির উল্টো পিঠও আছে। বড় ভুলকে বেশি শাস্তি দেওয়া মানে একটি অস্বাভাবিক বিন্দু, একটি outlier, রেখাকে নিজের দিকে অনেকখানি টেনে নিতে পারে। বিকল্প loss হলো MAE, ভুলের পরম মানের গড়; এটি কমালে যে রেখা আসে, তাকে least absolute deviations রেখাও বলে, আর এটি মধ্যমার (median) মতো আচরণ করে। দশটি কাল্পনিক অর্ডারে দুটিকে মেলানো হলো, তারপর একটি পার্সেলে ১২০ মিনিট যোগ করা হলো (ভুল ঠিকানায় দুই ঘণ্টা ঘুরেছে)। সত্যিকারের run, ডেস্কটপে, Python 3.14.4, scikit-learn 1.9.1 (MAE-রেখার জন্য QuantileRegressor(quantile=0.5, alpha=0)):

minutes: [21.1 16.9 27.8 30.9 36.1 41.6 43.  51.5 55.3 68.6]
clean data:
  MSE line: b =  10.71, w =  2.60
  MAE line: b =  10.36, w =  2.57
one outlier:
  MSE line: b = -13.29, w =  5.87
  MAE line: b =  10.36, w =  2.57

পরিষ্কার ডেটায় দুই রেখা প্রায় এক (আসল নিয়ম ছিল b = 12, w = 2.5). একটি বিন্দু বদলাতেই MSE-রেখার ঢাল দ্বিগুণের বেশি, 5.87, আর intercept ঋণাত্মক, −13.29: মানে ২ কিমির অর্ডারে model-এর অনুমান প্রায় −1.5 মিনিট! MAE-রেখা একটুও নড়েনি, কারণ শেষ বিন্দুটি এমনিতেই রেখার ওপরে ছিল, আর পরম মানের loss-এ “কত ওপরে” তার গুরুত্ব নেই, শুধু “ওপরে” না “নিচে”। তবে এর মূল্য আছে: MAE-এর কোনো সরল closed form নেই (linear programming লাগে), আর কোণাকার loss-এ gradient descent কম মসৃণ। মাঝামাঝি পথ হিসেবে Huber loss আছে (ছোট ভুলে বর্গ, বড় ভুলে পরম মান), scikit-learn-এ HuberRegressor.

সতর্কতা: outlier দেখলেই মুছে ফেলবেন না। হয়তো সেটি ডেটা লেখার ভুল, হয়তো সত্যিকারের ঘটনা (বন্যায় রাস্তা বন্ধ) যা model-এর জানা দরকার। আগে কারণ খুঁজুন (পর্ব ২৩-এর cleaning log-এর মতো লিখে রাখুন), তারপর সিদ্ধান্ত। বাকি পর্বে আমরা MSE-ই ব্যবহার করব, কারণ এটি প্রচলিত আর গণিতে সবচেয়ে সরল।

৩ · Closed form — normal equations, হাতে তিনটি বিন্দু

MSE একটি বাটির মতো (convex) loss, পর্ব ৩৪-এর ছবির মতো; তলা একটিই, আর তলায় gradient শূন্য। পর্ব ৩৪-এর chain rule অনেক feature-এ খাটালে MSE-এর gradient হয়:

∇MSE(w) = (2/n) · Xᵀ (Xw − y)

Xᵀ হলো X-এর transpose (সারি আর কলাম অদলবদল)। এটি শূন্য বসালে Xᵀ(Xw − y) = 0, মানে:

XᵀX w = Xᵀy ⇒ w = (XᵀX)−1 Xᵀy

এটিই normal equations, আর w-এর এই সরাসরি সূত্রই linear regression-এর closed form. সমীকরণটির একটি সুন্দর জ্যামিতিক অর্থ আছে: Xᵀr = 0 মানে residual vector r = y − Xw প্রতিটি কলামের সঙ্গে লম্ব (orthogonal, dot product শূন্য)। প্রথম কলাম সব 1, তাই একটি সঙ্গে সঙ্গে পাওয়া ফল: bias থাকলে residual-গুলোর যোগফল সবসময় শূন্য। ফুটবল মাঠে বলের ছায়া মাটিতে পড়ার মতো ভাবুন: Xw হলো y-এর “ছায়া” X-এর কলামগুলোর তৈরি সমতলে, আর r হলো বল থেকে ছায়া পর্যন্ত খাড়া দাগ। নাম “normal” এসেছে এই লম্ব (normal) হওয়া থেকে। এবার পরীক্ষার খাতার মতো তিনটি বিন্দুতে হাতে করি: দূরত্ব x = 2, 4, 6 কিমি, সময় y = 20, 26, 38 মিনিট।

1X = [[1,2],[1,4],[1,6]]2A = X.T @ X3c = X.T @ y4A_inv = inv(A)5b, w = A_inv @ c6r = y - X @ [b, w]
মান (y = 20, 26, 38)
A = XᵀX
c = Xᵀy
det A
A⁻¹
b, w
r
  1. X বানানো। প্রতিটি সারি [1, দূরত্ব]: [1, 2], [1, 4], [1, 6]. প্রথম কলামের 1 থেকেই b আসবে। তিনটি সমীকরণ, দুটি অজানা, তাই হুবহু মেলানো সম্ভব নয়; চাই “সবচেয়ে কাছের” উত্তর।
  2. XᵀX. ঘরগুলো চেনা যোগফল: ওপরে বাঁয়ে n = 3, পাশের দুই ঘরে ∑x = 2 + 4 + 6 = 12, নিচে ডানে ∑x² = 4 + 16 + 36 = 56.
  3. Xᵀy. ওপরে ∑y = 20 + 26 + 38 = 84, নিচে ∑xy = 40 + 104 + 228 = 372.
  4. ২×২-এর inverse. [[a, b], [c, d]]-এর inverse = (1/det)·[[d, −b], [−c, a]], det = ad − bc = 3 × 56 − 12 × 12 = 168 − 144 = 24. পরীক্ষার খাতায় এই সূত্রটিই লাগে।
  5. গুণ। b = (56 × 84 − 12 × 372) ÷ 24 = (4704 − 4464) ÷ 24 = 10. w = (−12 × 84 + 3 × 372) ÷ 24 = (−1008 + 1116) ÷ 24 = 4.5. রেখা: ŷ = 10 + 4.5x.
  6. যাচাই। অনুমান 19, 28, 37; residual 20 − 19 = 1, 26 − 28 = −2, 38 − 37 = 1. যোগ 0, আর ∑x·r = 2 − 8 + 6 = 0: residual দুই কলামের সঙ্গেই লম্ব, ঠিক normal equations যা বলে। SSE (বর্গের যোগ) = 1 + 4 + 1 = 6.

দুটি বিন্দু থাকলে কী হতো? তখন সমীকরণ দুটি, অজানা দুটি, রেখা দুই বিন্দু দিয়ে হুবহু যায়, residual শূন্য। যেমন x = 1, 3 আর y = 2, 6 হলে w = (6 − 2) ÷ (3 − 1) = 2, b = 0; normal equations-ও একই উত্তর দেবে। এই ছোট ঘটনাটি মনে রাখুন: parameter যত, বিন্দুও ততটি হলে model সব বিন্দু মুখস্থ করে ফেলে; অংশ ৯-এ polynomial-এর সঙ্গে এটি আবার আসবে।

৪ · কেন inverse নয়, lstsq — condition number আর তিনটি পথের তুলনা

কাগজে (XᵀX)−1Xᵀy নিখুঁত। কম্পিউটারে এটি একটি পরিচিত ফাঁদ। পর্ব ১৭-এর এক লাইনের সারাংশ: condition number 10k হলে float64-এর প্রায় ১৬টি দশমিক অঙ্কের মধ্যে k-টি হারানোর ঝুঁকি, আর সেই কারণে inv(A) @ b নয়, solve(A, b). এখানে একটি নতুন মোড় আছে: XᵀX বানানোর মুহূর্তেই condition number বর্গ হয়ে যায়, cond(XᵀX) ≈ cond(X)2. X-এ ৭টি অঙ্ক হারানোর ঝুঁকি থাকলে XᵀX-এ ১৪টি।

কখন X খারাপ হয়? যখন দুটি কলাম প্রায় একই তথ্য দেয়। উদাহরণ: “সোজা দূরত্ব” আর “রাস্তার দূরত্ব”, যেখানে রাস্তা প্রায় হুবহু সোজা দূরত্বের 1.3 গুণ। ৫০টি কাল্পনিক অর্ডারে গোলমাল ছাড়া y বানানো হলো, যাতে সঠিক উত্তর ঠিক w = [10, 1, 1.5] জানা থাকে; তারপর তিনটি পথে সমাধান। সত্যিকারের run, ডেস্কটপে, Python 3.14.4, NumPy 2.5.4:

cond(X)     = 4.1e+07
cond(X.T X) = 1.9e+15
inv(X.T X) X.T y     w = [9.9065 0.9563 1.5352]  rel. error 1.1e-02
solve(X.T X, X.T y)  w = [10.      0.6559  1.7647]  rel. error 4.3e-02
lstsq(X, y)          w = [10.   1.   1.5]  rel. error 2.0e-11

cond(X) প্রায় 4 × 107, আর XᵀX-এর প্রায় 2 × 1015, ঠিক বর্গের কাছাকাছি; float64-এর সীমার একদম কিনারায়। ফল: inverse-এর পথে আপেক্ষিক ভুল ১%, আর এখানে solve-ও বাঁচায়নি (৪%), কারণ সমস্যাটি inverse-এ নয়, XᵀX বানানোতেই; তথ্য সেখানেই হারিয়ে গেছে। lstsq XᵀX বানায়ই না; সে সরাসরি X-এর ওপর কাজ করে (NumPy-তে LAPACK-এর SVD-ভিত্তিক routine), তাই তার ভুল 2 × 10−11, inverse-এর পথের চেয়ে প্রায় ৫০ কোটি গুণ কম। পর্ব ১৭-এর নিয়ম তাই এখানে আরও এক ধাপ এগোয়: least squares-এ inv তো নয়ই, normal equations নিজে না বানিয়ে lstsq ব্যবহার করুন। এই ভুলের অঙ্কগুলো একটি মেশিনের একটি run; অন্য BLAS বা অন্য CPU-তে শেষ অঙ্ক বদলাতে পারে, ধরন বদলাবে না।

ভালো আচরণের ডেটায় তিন পথই একই উত্তর দেয়। নিচের কোড অংশ ২-এর নিয়মে ২০০টি অর্ডার বানায়, তারপর normal equations (solve দিয়ে), lstsq আর scikit-learn-এর LinearRegression দিয়ে coefficient বের করে আসল নিয়মের পাশে রাখে। scikit-learn নিজেই intercept যোগ করে, তাই তাকে bias কলাম ছাড়া X দেওয়া হয়েছে।

import numpy as np
from sklearn.linear_model import LinearRegression

# বাজারঘরের ২০০টি কাল্পনিক অর্ডার, জানা "সত্যিকারের" নিয়ম দিয়ে বানানো
rng = np.random.default_rng(42)
n = 200
km = rng.uniform(1, 25, n)            # দূরত্ব, কিমি
kg = rng.uniform(0.2, 15, n)          # ওজন, কেজি
items = rng.integers(1, 9, n)         # পণ্যের সংখ্যা
rain = rng.binomial(1, 0.3, n)        # বৃষ্টি: 1 = হ্যাঁ
peak = rng.binomial(1, 0.35, n)       # ভিড়ের সময়: 1 = হ্যাঁ
true = np.array([12, 2.5, 0.8, 1.5, 9, 7])   # b, তারপর ৫টি w
X = np.column_stack([np.ones(n), km, kg, items, rain, peak])
y = X @ true + rng.normal(0, 4, n)    # মিনিট, সঙ্গে গোলমাল

w_ne = np.linalg.solve(X.T @ X, X.T @ y)       # normal equations
w_ls = np.linalg.lstsq(X, y)[0]                # least squares
sk = LinearRegression().fit(X[:, 1:], y)       # bias নিজে যোগ করে
w_sk = np.r_[sk.intercept_, sk.coef_]

names = ["bias", "km", "kg", "items", "rain", "peak"]
print("        আসল   normal  lstsq  sklearn")
for nm, t, a, b, c in zip(names, true, w_ne, w_ls, w_sk):
    print(f"{nm:6s} {t:6.2f} {a:7.3f} {b:6.3f} {c:7.3f}")
print("সর্বোচ্চ পার্থক্য (normal − lstsq):", f"{np.abs(w_ne - w_ls).max():.1e}")
r = y - X @ w_ls
print(f"RMSE = {np.sqrt(np.mean(r**2)):.2f} মিনিট, R² = {sk.score(X[:, 1:], y):.3f}")
print("একটি নতুন অর্ডার (10 km, 3 kg, 2 items, বৃষ্টি, ভিড় নয়):",
      round(float(np.array([1, 10, 3, 2, 1, 0]) @ w_ls), 1), "মিনিট")

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

        আসল   normal  lstsq  sklearn
bias    12.00  13.361 13.361  13.361
km       2.50   2.434  2.434   2.434
kg       0.80   0.775  0.775   0.775
items    1.50   1.413  1.413   1.413
rain     9.00   9.107  9.107   9.107
peak     7.00   6.870  6.870   6.870
সর্বোচ্চ পার্থক্য (normal − lstsq): 1.2e-13
RMSE = 3.78 মিনিট, R² = 0.957
একটি নতুন অর্ডার (10 km, 3 kg, 2 items, বৃষ্টি, ভিড় নয়): 52.0 মিনিট

তিন পথ তিন দশমিক পর্যন্ত হুবহু এক; পার্থক্য 10−13-এর ঘরে, শুধু float-এর গোলমাল। এখানে X ভালো আচরণের (feature-গুলো প্রায় স্বাধীন), তাই normal equations-ও নিরাপদ ছিল। আসল নিয়মের সঙ্গে মিল কাছাকাছি, তবে হুবহু নয়: km 2.434 (আসল 2.5), বৃষ্টি 9.107 (আসল 9). পার্থক্যটি গোলমালের দান; ২০০টি অর্ডারের একটি নমুনা থেকে এর চেয়ে নিখুঁত আশা করা যায় না। Bias-এ পার্থক্য সবচেয়ে বেশি (13.36 বনাম 12), কারণ intercept হলো “সব feature শূন্য হলে” সময়, আর ডেটায় এমন অর্ডার নেই (দূরত্ব কমপক্ষে ১ কিমি, পণ্য কমপক্ষে ১টি), তাই এটি অনেক দূর থেকে টেনে আনা অনুমান। RMSE 3.78 মিনিট, গোলমালের আসল SD 4-এর কাছাকাছি: model গোলমালের নিচে নামতে পারে না, নামা উচিতও নয়। শেষ লাইনে নতুন অর্ডারের অনুমান 52.0 মিনিট; আসল নিয়মে হতো 12 + 25 + 2.4 + 3 + 9 = 51.4.

scikit-learn আর NumPy ভেতরে কী করে? LinearRegression ভেতরে scipy.linalg.lstsq ডাকে (dense ডেটায়), তাই উত্তর NumPy-র lstsq-এর সঙ্গে মেলে। NumPy 2-এ lstsq-এর rcond-এর ডিফল্ট বদলেছে, তাই পুরোনো টিউটোরিয়ালের rcond=None লেখার আর দরকার নেই। QR আর SVD-র পার্থক্য ভাবনার প্রশ্ন ৫-এ।

৫ · অনেক feature-এ gradient descent — vectorised, scaling আর mini-batch

Closed form থাকলে gradient descent কেন? পর্ব ৩৪-এর উত্তর মনে করুন: বড় model-এ (logistic regression থেকে neural network) সরাসরি সূত্র থাকে না, আর লাখো feature-এ XᵀX-এর মতো (p × p) matrix-ই বিশাল। Linear regression হলো সেই জায়গা, যেখানে gradient descent-কে সঠিক উত্তরের সঙ্গে মিলিয়ে দেখা যায়। নিয়মটি এক লাইনের, অংশ ৩-এর gradient দিয়ে:

w ← w − η · (2/n) · Xᵀ(Xw − y)

η হলো learning rate. এক feature-এর জন্য পর্ব ৩৪-এ w আর b আলাদা দুই লাইনে বদলাতে হয়েছিল; এখানে একটি matrix গুণেই সব weight একসঙ্গে। অংশ ৩-এর তিনটি অর্ডারে, w = [0, 0] থেকে, η = 0.01 দিয়ে এক ধাপ:

1w = np.zeros(2)2y_hat = X @ w3r = y_hat - y4g = 2 / n * X.T @ r5w = w - eta * g6mse = np.mean((X @ w - y)**2)
মান (y = 20, 26, 38)
w (b, w)
ŷ
r = ŷ − y
g
MSE
  1. শুরু। w = [b, w] = [0, 0]. শুরুর MSE = (20² + 26² + 38²) ÷ 3 = (400 + 676 + 1444) ÷ 3 = 840.
  2. অনুমান। ŷ = X @ w, এক গুণে তিনটি অর্ডারেরই অনুমান; সব 0.
  3. Residual vector. r = ŷ − y = [−20, −26, −38], shape (3,). সব ঋণাত্মক: model সবখানে কম বলছে।
  4. Gradient. Xᵀr-এর প্রথম ঘর ∑r = −84, দ্বিতীয় ঘর ∑x·r = −40 − 104 − 228 = −372. গুণ 2/3: g = [−56, −248]. ঢালের ঘর অনেক বড়, কারণ দূরত্বের মান (2, 4, 6) 1-এর চেয়ে বড়।
  5. পা। w = [0, 0] − 0.01 × [−56, −248] = [0.56, 2.48]. দুই weight-ই বেড়েছে, ঠিক দিকে।
  6. ফল। নতুন ŷ = 0.56 + 2.48 × [2, 4, 6] = [5.52, 10.48, 15.44], MSE 319.83. এক ধাপে loss অর্ধেকের বেশি কমেছে, তবে closed form-এর [10, 4.5] এখনো অনেক দূরে; বিশেষত b ধীরে এগোচ্ছে, কারণ তার gradient ছোট। এই অসমতাই scaling-এর গল্প।

শেষ ধাপের পর্যবেক্ষণটিই পর্ব ৩৪-এর “লম্বাটে বাটি”। Feature-গুলোর মাপ আলাদা হলে loss এক দিকে খুব খাড়া, অন্য দিকে প্রায় সমতল; খাড়া দিকের জন্য η ছোট রাখতে হয়, আর তাতে সমতল দিকে চলা কচ্ছপের গতিতে। প্রতিকার feature scaling, প্রচলিত রূপ standardization: প্রতিটি কলাম থেকে গড় বাদ দিয়ে standard deviation দিয়ে ভাগ। নিচের কোড অংশ ৪-এর একই ২০০টি অর্ডারে (একই seed, একই ক্রমে random সংখ্যা) দুইভাবে চালায়। দুই ক্ষেত্রেই η = 1 ÷ (loss-এর সবচেয়ে বড় বাঁক), যা নিরাপদ সীমার (2 ÷ বাঁক) ভেতরে, যাতে তুলনাটি ন্যায্য হয়। থামে gradient-এর দৈর্ঘ্য 10−6-এর নিচে নামলে।

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)                 # আগের মতোই একই ডেটা
n = 200
F = np.column_stack([rng.uniform(1, 25, n), rng.uniform(0.2, 15, n),
                     rng.integers(1, 9, n), rng.binomial(1, 0.3, n),
                     rng.binomial(1, 0.35, n)]).astype(float)
y = np.column_stack([np.ones(n), F]) @ [12, 2.5, 0.8, 1.5, 9, 7] + rng.normal(0, 4, n)

def gd(F, y, max_iter=100000, tol=1e-6):
    X = np.column_stack([np.ones(len(y)), F])
    H = 2 / len(y) * X.T @ X                     # loss-এর বাঁকের matrix
    eta = 1 / np.linalg.eigvalsh(H).max()       # নিরাপদ পায়ের মাপ
    w, hist = np.zeros(X.shape[1]), []
    for t in range(max_iter):
        r = X @ w - y                            # residual, shape (n,)
        g = 2 / len(y) * X.T @ r                 # vectorised gradient
        hist.append(np.mean(r ** 2))
        if np.linalg.norm(g) < tol:
            break
        w -= eta * g
    return w, t, hist, np.linalg.cond(H)

Z = (F - F.mean(axis=0)) / F.std(axis=0)         # standardize
for name, A in [("কাঁচা", F), ("standardized", Z)]:
    w, t, hist, c = gd(A, y)
    print(f"{name:12s}: cond = {c:7.1f}, ধাপ = {t:5d}, MSE = {hist[-1]:.3f}")
    plt.loglog(np.arange(1, len(hist) + 1), hist, label=f"{'raw' if name == 'কাঁচা' else 'standardized'} ({t} steps)")
print("standardized-এ w (প্রতি ১ SD-তে মিনিট):", w[1:].round(2))
print("মূল এককে ফেরত (প্রতি km, kg ...):", (w[1:] / F.std(axis=0)).round(3))
plt.xlabel("iteration"); plt.ylabel("training MSE")
plt.title("Same data, same rule: scaling changes the speed"); plt.legend()
plt.show()

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

কাঁচা       : cond =  3640.6, ধাপ = 51809, MSE = 14.314
standardized: cond =     1.2, ধাপ =    10, MSE = 14.314
standardized-এ w (প্রতি ১ SD-তে মিনিট): [16.45  3.3   3.28  4.33  3.23]
মূল এককে ফেরত (প্রতি km, kg ...): [2.434 0.775 1.413 9.107 6.87 ]

একই ডেটা, একই নিয়ম, একই শেষ MSE (14.314 = 3.78², অংশ ৪-এর RMSE-এর বর্গ)। পার্থক্য শুধু পথের দৈর্ঘ্যে: কাঁচা feature-এ ৫১,৮০৯ ধাপ, standardize করে ১০ ধাপ, প্রায় পাঁচ হাজার গুণ কম। কারণটি cond সংখ্যায় লেখা: কাঁচা ডেটায় loss-এর বাঁক সবচেয়ে খাড়া আর সবচেয়ে সমতল দিকে 3,641 গুণ আলাদা, standardize করলে মাত্র 1.2 গুণ, প্রায় গোল বাটি। মোটা দাগে gradient descent-এর ধাপ এই অনুপাতের সমানুপাতিক। ছবিতে কাঁচা রেখাটি প্রথম ধাপেই 4195 থেকে প্রায় 98-এ নামে, তারপর হামাগুড়ি দেয়: ডেস্কটপে একই কোডের হিসাবে ১,০০০ ধাপে MSE তখনো 26.3, ১০,০০০ ধাপে 14.345, আর শেষ অঙ্কগুলো মেলাতে বাকি হাজার হাজার ধাপ; সবচেয়ে ধীর দিকটি মূলত bias, কারণ কাঁচা feature-এর গড় শূন্য নয়। ডেস্কটপে মেপে দেখা গেছে, শুধু গড় বাদ দিলে cond নামে 221-এ, শুধু SD দিয়ে ভাগ করলে 177-এ, আর দুটি একসঙ্গে করলে 1.2-এ। Standardize করা feature-এ coefficient-এর অর্থ বদলায় (“এক standard deviation বাড়লে কত মিনিট”), তবে SD দিয়ে ভাগ করলে মূল এককের 2.434, 0.775 … ফিরে আসে, অংশ ৪-এর lstsq-এর হুবহু। (দুটি সতর্কতা: এখানে H-এর eigenvalue দিয়ে η ঠিক করা শেখানোর সুবিধার জন্য; বাস্তবে η খোঁজা হয় চেষ্টা করে। আর scaling-এর গড় ও SD নিতে হয় শুধু train ডেটা থেকে, পর্ব ৫৬-এর leakage-এর নিয়মে।)

Mini-batch-এর জায়গা। ওপরের প্রতিটি ধাপে ২০০টি সারিই পড়া হয়েছে (full batch). কোটি সারিতে প্রতি পায়ে কয়েকটি সারির এক মুঠো নিয়ে gradient আন্দাজ করাই mini-batch, আর একটি করে সারি নিলে SGD; পর্ব ৩৪-এর অংশ ৯-এ ৪০০টি অর্ডারে তিন পদ্ধতির আসল তুলনা আছে। scikit-learn-এ এর তৈরি রূপ SGDRegressor, যেটিও scaling ছাড়া ভালো কাজ করে না। ছোট বা মাঝারি ডেটায় linear regression-এর জন্য lstsq-ই দ্রুত আর নিখুঁত।

৬ · Coefficient পড়া — “বাকি সব স্থির রেখে”, আর multicollinearity

অংশ ৪-এর km coefficient 2.434. এর সঠিক অর্থ: “বাকি সব feature (ওজন, পণ্য, বৃষ্টি, ভিড়) একই থাকলে, এক কিমি বেশি দূরত্বে model গড়ে প্রায় ২.৪ মিনিট বেশি অনুমান করে।” বৃষ্টির 9.107: “একই দূরত্ব, ওজন, পণ্য আর ভিড়ের অবস্থায়, বৃষ্টির দিনে প্রায় ৯ মিনিট বেশি।” তিনটি সূক্ষ্ম কথা এতে লুকানো আছে। এক, “স্থির রেখে” অংশটি বাদ দিলে অর্থ বদলে যায়। দুই, coefficient-এর একক আছে (মিনিট প্রতি কিমি, মিনিট প্রতি কেজি), তাই km-এর 2.4 আর kg-এর 0.78 সরাসরি তুলনা করে “দূরত্ব বেশি গুরুত্বপূর্ণ” বলা যায় না; তুলনা করতে হলে standardize করা coefficient দেখুন (অংশ ৫-এর 16.45, 3.3 …)। তিন, এটি পর্যবেক্ষণের ডেটায় একটি সম্পর্ক, কারণ নয়; বৃষ্টির দিনে হয়তো রাইডারও কম থাকে, আর model সেই প্রভাবও বৃষ্টির ঘাড়ে চাপায় (পর্ব ৩৭-এর confounder).

“স্থির রেখে” কথাটি তখনই অর্থহীন হয়, যখন দুটি feature একসঙ্গে ওঠানামা করে। অংশ ৪-এর “সোজা দূরত্ব” আর “রাস্তার দূরত্ব” ফিরিয়ে আনি: রাস্তার দূরত্ব বাস্তবে না বাড়িয়ে সোজা দূরত্ব বাড়ানো যায় না। এই অবস্থার নাম multicollinearity. ডেটা যেভাবে বানানো: আসল নিয়মে সময় নির্ভর করে শুধু রাস্তার দূরত্বে (প্রতি কিমিতে ২ মিনিট) আর বৃষ্টিতে (৮ মিনিট)। কিন্তু model-কে দুটি দূরত্বই দেওয়া হলো। নিচের কোড ১০টি ভিন্ন seed-এ ৬০টি করে অর্ডার বানিয়ে fit করে, তারপর একটি seed-এ statsmodels-এর OLS দিয়ে প্রতিটি coefficient-এর standard error আর ৯৫% confidence interval ছাপে (CI-এর অর্থ পর্ব ৩৭-এ)। statsmodels প্রথমবার লোড হতে কয়েক সেকেন্ড লাগে।

import numpy as np
import statsmodels.api as sm

def orders(seed, n=60):
    rng = np.random.default_rng(seed)
    km = rng.uniform(1, 25, n)                     # সোজা দূরত্ব
    road = 1.3 * km + rng.normal(0, 0.3, n)        # রাস্তার দূরত্ব, প্রায় একই তথ্য
    rain = rng.binomial(1, 0.3, n)
    y = 10 + 2.0 * road + 8 * rain + rng.normal(0, 4, n)
    return np.column_stack([km, road, rain]), y

print("seed  w_km w_road    মোট  road একা")     # মোট = w_km + 1.3·w_road
for seed in range(10):
    F, y = orders(seed)
    w = np.linalg.lstsq(sm.add_constant(F), y)[0]
    w1 = np.linalg.lstsq(sm.add_constant(F[:, 1:]), y)[0]
    print(f"{seed:4d} {w[1]:5.2f} {w[2]:6.2f} {w[1] + 1.3 * w[2]:6.2f} {w1[1]:6.2f}")

F, y = orders(0)
print("corr(km, road) =", round(np.corrcoef(F[:, 0], F[:, 1])[0, 1], 4))
res = sm.OLS(y, sm.add_constant(F)).fit()
print(res.summary(xname=["const", "km", "road", "rain"]).tables[1])
res1 = sm.OLS(y, sm.add_constant(F[:, 1:])).fit()
print("শুধু road রেখে 95% CI:\n", res1.conf_int().round(2))

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

seed  w_km w_road    মোট  road একা
   0  1.13   1.11   2.57   1.97
   1  2.92  -0.15   2.72   2.09
   2  1.41   1.09   2.83   2.18
   3 -1.00   2.77   2.60   2.01
   4 -2.07   3.56   2.56   1.95
   5  0.68   1.45   2.56   1.97
   6 -1.72   3.36   2.64   2.04
   7  1.34   1.02   2.67   2.05
   8 -3.33   4.51   2.54   1.94
   9 -1.30   3.07   2.68   2.07
corr(km, road) = 0.9995
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const          9.9829      1.266      7.884      0.000       7.446      12.519
km             1.1286      2.525      0.447      0.657      -3.930       6.187
road           1.1068      1.943      0.570      0.571      -2.786       5.000
rain           7.4621      1.227      6.083      0.000       5.005       9.920
==============================================================================
শুধু road রেখে 95% CI:
 [[ 7.46 12.49]
 [ 1.85  2.1 ]
 [ 5.    9.87]]

দুই দূরত্বের correlation 0.9995. w_km কলামে সোজা দূরত্বের coefficient seed বদলালেই −3.33 থেকে +2.92 পর্যন্ত লাফায়; কখনো ঋণাত্মক, যেন “দূরত্ব বাড়লে সময় কমে”! কিন্তু “মোট” কলাম, মানে সোজা দূরত্ব ১ কিমি বাড়লে (সঙ্গে রাস্তাও প্রায় 1.3 কিমি) মোট প্রভাব, প্রতিবার 2.54 থেকে 2.83-এর মধ্যে, আসল 1.3 × 2 = 2.6-এর কাছে। Model-এর অনুমান স্থির, শুধু দুই কলামের মধ্যে ভাগাভাগি এলোমেলো। সোজা দূরত্ব বাদ দিয়ে শুধু road রাখলে (শেষ কলাম) coefficient 1.94 থেকে 2.18, আসল 2-এর কাছে, আর স্থির। statsmodels-এর টেবিলে একই গল্প সংখ্যায়: km আর road-এর standard error (2.5 আর 1.9) coefficient-এর চেয়েও বড়, CI শূন্যের দুই পাশে ছড়ানো (−3.9 থেকে 6.2), আর p-value 0.66 ও 0.57; আলাদাভাবে দেখলে যেন দুটির কোনোটিই কাজের নয়! অথচ একই ডেস্কটপ run-এ পুরো model-এর R² 0.95. Road একা রাখলে CI সরু হয়ে 1.85 থেকে 2.10, আসল 2-কে ধারণ করে।

সারকথা: multicollinearity অনুমানের (prediction) ক্ষতি সাধারণত করে না, কিন্তু coefficient-এর ব্যাখ্যা নষ্ট করে। ধরার উপায়: feature-এর correlation matrix, অংশ ৪-এর condition number, আর VIF (variance inflation factor), যা বলে অন্য feature-এর কারণে একটি coefficient-এর variance কত গুণ ফুলেছে। ওপরের seed 0-এ statsmodels-এর variance_inflation_factor ডেস্কটপে দিয়েছে km 937.1, road 937.6, rain 1.0 (সত্যিকারের run, statsmodels 0.15.0); মোটা নিয়মে ৫ বা ১০-এর বেশি হলে সাবধান হওয়ার সংকেত, যদিও এই সীমা সর্বজনীন নয়। প্রতিকার: একটি কলাম বাদ দেওয়া, দুটিকে মিলিয়ে একটি বানানো, বা regularization (পর্ব ৬১-এ ridge).

statsmodels বনাম scikit-learn: দুটি একই OLS coefficient দেয় (ওপরের seed 0-এ statsmodels-এর 1.1286 আর lstsq-এর 1.13 একই সংখ্যা)। পার্থক্য উদ্দেশ্যে। statsmodels পরিসংখ্যানের ঐতিহ্যের: standard error, CI, p-value আর অনুমানগুলোর পরীক্ষা, মানে “coefficient কতটা বিশ্বাসযোগ্য”। scikit-learn machine learning-এর ঐতিহ্যের: fit/predict, pipeline আর নতুন ডেটায় নির্ভুলতা। একটি বড় ফাঁদ: sm.OLS নিজে intercept যোগ করে না, তাই sm.add_constant ভুলে গেলে রেখা শূন্য দিয়ে যেতে বাধ্য হয়। (এই পাতার Pyodide-এ statsmodels আগে থেকেই আছে; ডেস্কটপে লেখার সময়ের version ছিল 0.15.0.)

৭ · Category feature — one-hot encoding আর dummy-variable trap

বৃষ্টি আর ভিড় দুটি হ্যাঁ/না feature, তাই 0 আর 1 বসালেই চলেছে। কিন্তু “এলাকা” যদি ঢাকা, চট্টগ্রাম আর সিলেট হয়? ঢাকা = 0, চট্টগ্রাম = 1, সিলেট = 2 বসালে model ধরে নেবে চট্টগ্রাম ঠিক ঢাকা আর সিলেটের মাঝামাঝি, আর সিলেটের প্রভাব চট্টগ্রামের ঠিক দ্বিগুণ; এমন কোনো ক্রম বাস্তবে নেই। সমাধান one-hot encoding: প্রতিটি এলাকার জন্য একটি আলাদা 0/1 কলাম। Pandas-এ pd.get_dummies.

এখানেই একটি পরিচিত ফাঁদ। তিনটি এলাকার তিনটি কলামের যোগফল প্রতিটি সারিতে ঠিক 1, কারণ প্রতিটি অর্ডার কোনো একটি এলাকারই। আর bias কলামও সব 1. অর্থাৎ একটি কলাম অন্যগুলো থেকে হুবহু বের করা যায়: নিখুঁত multicollinearity. এর নাম dummy-variable trap. ৯০টি কাল্পনিক অর্ডারে (আসল নিয়ম: ঢাকায় বাড়তি 0, চট্টগ্রামে 6, সিলেটে 10 মিনিট) সত্যিকারের run, ডেস্কটপে, pandas 3.1.1, NumPy 2.5.4, scikit-learn 1.9.1:

['km', 'zone_Ctg', 'zone_Dhaka', 'zone_Sylhet']
columns: 5  rank: 4  cond: 1.0e+17
trap, sklearn: [20.02  2.52  0.81 -5.56  4.75]
trap, lstsq  : [15.01  2.52  5.81 -0.55  9.75]
['km', 'zone_Dhaka', 'zone_Sylhet']  rank: 4
b, km, Dhaka, Sylhet = [20.83  2.52 -6.36  3.94]

প্রথম তিন লাইন ফাঁদে পড়া অবস্থা: পাঁচটি কলাম, কিন্তু rank মাত্র 4 (একটি কলাম বাড়তি), আর cond প্রায় 1017, মানে XᵀX-এর inverse কার্যত নেই। মজার ব্যাপার, কোনো error আসেনি! scikit-learn আর lstsq দুজনেই উত্তর দিয়েছে, কিন্তু আলাদা উত্তর: একজন বলছে intercept 20.02, অন্যজন 15.01; একজনের ঢাকা −5.56, অন্যজনের −0.55. দুটি উত্তরই একই অনুমান দেয় (যেমন চট্টগ্রাম − ঢাকা: 0.81 − (−5.56) = 6.37, আর 5.81 − (−0.55) = 6.36), কারণ অসীম সংখ্যক উত্তর সমান ভালো, আর প্রতিটি library নিজের নিয়মে একটি বেছে নেয়। অনুমান ঠিক, ব্যাখ্যা অর্থহীন। শেষ দুই লাইনে প্রতিকার: drop_first=True একটি এলাকা (বর্ণক্রমে প্রথম, এখানে Ctg) বাদ দেয়; সে হয় “ভিত্তি” (baseline). এখন উত্তর অনন্য: intercept 20.83 (আসলে 15 + 6 = 21, চট্টগ্রামের ভিত্তি), ঢাকা −6.36 (আসলে চট্টগ্রামের চেয়ে 6 কম), সিলেট +3.94 (আসলে 4 বেশি)। প্রতিটি এলাকার coefficient পড়তে হবে ভিত্তি-এলাকার সঙ্গে তুলনা হিসেবে।

কখন সব কলাম রাখা চলে? Intercept না থাকলে, অথবা regularization (ridge, lasso) থাকলে; তখন ফাঁদটি অঙ্কের দিক থেকে কেটে যায়, যদিও ব্যাখ্যা তখনো সাবধানে করতে হয়। scikit-learn-এর OneHotEncoder(drop="first") pipeline-এর ভেতরে একই কাজ করে, আর train-এ না-দেখা নতুন category এলে কী হবে তাও সামলাতে পারে। এলাকা অনেক বেশি হলে (যেমন ৬৪টি জেলা) কলামও অনেক; সেখানে কী করবেন, আর scaling-encoding-এর পুরো কারিগরি, পর্ব ৬৩-এ (Feature engineering).

৮ · Residual analysis — ভুলের ছবি পড়া, আর একটি নতুন feature

ডাক্তার শুধু জ্বরের মাপ দেখে রোগ ধরেন না; কোন সময়ে জ্বর বাড়ে, কোন উপসর্গ সঙ্গে আসে, সেই নকশা দেখেন। RMSE বা R²-এর একটি সংখ্যাও তেমনি সব বলে না। Model কী ভুল করছে, তা দেখার সবচেয়ে কাজের ছবি হলো residual plot: x-অক্ষে model-এর অনুমান (fitted value), y-অক্ষে residual (আসল − অনুমান)। ভালো model-এ এই ছবি হওয়া উচিত শূন্য রেখার চারপাশে একটি নকশাহীন মেঘ। নকশা দেখা গেলেই, model ডেটার এমন কিছু ধরেনি যা ধরা সম্ভব। চারটি প্রচলিত নকশা ধাপে ধাপে দেখুন (ছকে আঁকা নমুনা, seed-করা বানানো সংখ্যা দিয়ে, কোনো আসল ডেটা নয়)।

Residual plot-এর চারটি নকশা 0 +10 −10 অনুমান (fitted, মিনিট) → residual এলোমেলো মেঘ ✓ U-আকার: বাঁক ধরা পড়েনি ফানেল: ছড়ানো বাড়ছে একা দূরের বিন্দু
  1. এলোমেলো মেঘ। বিন্দুগুলো শূন্য রেখার দুই পাশে সমানভাবে, কোনো ঢেউ বা আকার নেই, আর বাঁ থেকে ডানে ছড়ানো প্রায় একই। এটাই চাই: যা বাকি আছে, তা গোলমাল। এখানে আর কোনো feature বা রূপান্তর সাহায্য করবে বলে ছবি বলছে না।
  2. U-আকার। ছোট আর বড় অনুমানে residual ধনাত্মক (আসল বেশি), মাঝে ঋণাত্মক। সরলরেখা একটি বাঁককে সোজা করে ফেলেছে: দুই প্রান্তে কম বলছে, মাঝে বেশি। প্রতিকার: বাঁক ধরার মতো feature যোগ করা, যেমন km², বা log(km). নিচের কোডে ঠিক এটিই হবে।
  3. ফানেল। গড় ঠিক আছে, কিন্তু বড় অনুমানে ছড়ানো অনেক বেশি; দূরের অর্ডারে অনিশ্চয়তাও বেশি। এর নাম heteroscedasticity (অসম variance). Coefficient তবু মোটামুটি ঠিক থাকতে পারে, কিন্তু সাধারণ standard error আর CI ভুল হয়। প্রতিকার: y-এর log নেওয়া, বা statsmodels-এ robust standard error (যেমন cov_type="HC3").
  4. একা দূরের বিন্দু। একটি অর্ডারের residual প্রায় +১১ মিনিট, বাকিরা ±৪-এর মধ্যে। অংশ ২-এর মতো: আগে খুঁজুন সেটি ডেটার ভুল না সত্যিকারের ঘটনা। খুব প্রভাবশালী বিন্দু কি না মাপতে statsmodels-এ Cook's distance আছে। মুছবেন শুধু কারণ জানলে, আর তা লিখে রাখবেন।

এবার আসল run. নতুন কাল্পনিক ডেটা: শহরের জ্যামে দূরত্ব বাড়লে সময় বাড়ে দ্রুততর হারে, আসল নিয়ম y = 15 + 0.6·km + 0.09·km² + 8·rain + গোলমাল। প্রথমে সরলরেখা (km আর rain), তারপর km²-কে তৃতীয় feature হিসেবে যোগ করে। km² নতুন feature হলেও model এখনো linear, কারণ w-গুলোর ওপর রৈখিক; শুধু x-এর জায়গায় একটি নতুন কলাম। কোডে R², adjusted R² আর RMSE-ও হিসাব হয় (অর্থ পরের অংশে)।

import numpy as np
import matplotlib.pyplot as plt

# নতুন কাল্পনিক ডেটা: শহরের জ্যামে দূরত্ব বাড়লে সময় বাড়ে দ্রুততর হারে
rng = np.random.default_rng(7)
n = 120
km = rng.uniform(1, 25, n)
rain = rng.binomial(1, 0.3, n)
y = 15 + 0.6 * km + 0.09 * km**2 + 8 * rain + rng.normal(0, 3, n)

def fit(cols):
    X = np.column_stack([np.ones(n)] + cols)
    w = np.linalg.lstsq(X, y)[0]
    fitted = X @ w
    r = y - fitted
    p = X.shape[1] - 1                             # feature-এর সংখ্যা
    r2 = 1 - np.sum(r**2) / np.sum((y - y.mean())**2)
    adj = 1 - (1 - r2) * (n - 1) / (n - p - 1)
    print(f"{p} feature: w = {w.round(3)}")
    print(f"   R² = {r2:.3f}, adjusted R² = {adj:.3f}, RMSE = {np.sqrt(np.mean(r**2)):.2f} মিনিট")
    return fitted, r

f1, r1 = fit([km, rain])                           # সরলরেখা
f2, r2 = fit([km, rain, km**2])                    # km² যোগ করে
fig, axes = plt.subplots(1, 2, figsize=(9, 3.4), sharey=True)
for ax, f, r, t in [(axes[0], f1, r1, "y ~ km + rain"),
                    (axes[1], f2, r2, "y ~ km + rain + km^2")]:
    ax.scatter(f, r, s=14, alpha=0.7)
    ax.axhline(0, color="k", lw=1)
    ax.set_title(t); ax.set_xlabel("fitted minutes")
axes[0].set_ylabel("residual (actual - fitted)")
plt.show()

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

2 feature: w = [4.073 2.948 7.603]
   R² = 0.950, adjusted R² = 0.949, RMSE = 4.80 মিনিট
3 feature: w = [14.521  0.671  7.667  0.088]
   R² = 0.983, adjusted R² = 0.982, RMSE = 2.83 মিনিট

সবচেয়ে বড় শিক্ষা প্রথম লাইনে: ভুল model-এরও R² 0.950! শুধু এই সংখ্যা দেখলে মনে হতো কাজ শেষ। কিন্তু বাঁয়ের ছবিতে স্পষ্ট U: ছোট আর বড় অনুমানে residual +৫ থেকে +১০, মাঝে −৫-এর কাছে। সরলরেখার intercept 4.07 (আসল 15) আর ঢাল 2.95 মিনিট প্রতি কিমি, দুটিই বাঁক সোজা করতে গিয়ে বিকৃত। km² যোগ করতেই ডানের ছবি নকশাহীন মেঘ, RMSE 4.80 থেকে 2.83 (গোলমালের আসল SD 3-এর কাছে), আর coefficient আসল নিয়মের কাছে ফিরেছে: 14.52, 0.671, বৃষ্টি 7.67, km² 0.088 (আসল 15, 0.6, 8, 0.09). বৃষ্টির coefficient দুই model-এ প্রায় একই, কারণ বৃষ্টি দূরত্বের সঙ্গে সম্পর্কিত নয়।

বাঁক যদি উল্টো দিকের হতো (দূরের অর্ডারে হাইওয়ে, তাই প্রতি কিমিতে সময় কমে), U-এর বদলে উল্টো U দেখা যেত, আর log(km) বা √km বেশি মানানসই হতো। কোন রূপান্তর, তা ঠিক করে ডোমেইনের জ্ঞান আর residual-এর ছবি মিলিয়ে; অন্ধভাবে km³, km⁴ যোগ করে নয়, সেই বিপদ পরের অংশে।

৯ · R², RMSE, polynomial feature আর অনুমানগুলো

R², adjusted R² আর RMSE — সাদা বাংলায়

সবচেয়ে সরল baseline হলো সব অর্ডারে একই অনুমান, y-এর গড় (পর্ব ৩৪-এর অনুশীলনে দেখেছেন, MSE-কে গড়ই সবচেয়ে কম করে)। R² বলে, model সেই baseline-এর ভুলের কত অংশ দূর করেছে:

R² = 1 − SSE ÷ SST, SSE = ∑(yi − ŷi)², SST = ∑(yi − ȳ)²

ȳ হলো y-এর গড়। অংশ ৩-এর তিনটি অর্ডারে: ȳ = 28, SST = 64 + 4 + 100 = 168, SSE = 6, তাই R² = 1 − 6 ÷ 168 ≈ 0.964: গড়ের তুলনায় ভুলের প্রায় ৯৬% দূর হয়েছে। R² 1 মানে নিখুঁত, 0 মানে গড়ের চেয়ে ভালো নয়, আর নতুন ডেটায় মাপলে ঋণাত্মকও হতে পারে (গড়ের চেয়েও খারাপ)। একটি ফাঁদ: train ডেটায় নতুন feature যোগ করলে R² কখনো কমে না, এমনকি feature-টি সম্পূর্ণ এলোমেলো হলেও। Adjusted R² এর জন্য শাস্তি দেয়: 1 − (1 − R²)(n − 1) ÷ (n − p − 1), যেখানে p feature-এর সংখ্যা; অকেজো feature যোগ করলে এটি কমতে পারে। আর RMSE = √MSE সবচেয়ে সহজে বোঝানো যায়, কারণ এর একক মিনিট: “model সাধারণত প্রায় ৩–৪ মিনিট এদিক-ওদিক করে।” কোন metric কখন, train নয় test ডেটায় মাপা, আর MAE বনাম RMSE বাছাই পর্ব ৬০-এ (মূল্যায়ন); এখানে শুধু পরিচয়।

Polynomial feature — overfitting-এর সেতু

km² কাজ করেছে; তাহলে km³, km⁴ … km¹² যোগ করলে আরও ভালো? এগুলোকে বলে polynomial features. মাত্র ১৫টি train অর্ডারে (অংশ ৮-এর নিয়মে, বৃষ্টি বাদ) বিভিন্ন degree-র polynomial fit করে ২০০টি নতুন অর্ডারে যাচাই। সত্যিকারের run, ডেস্কটপে, NumPy 2.5.4 (numpy.polynomial.Polynomial.fit):

degree  1: train RMSE =  3.68, test RMSE =     5.47
degree  2: train RMSE =  2.61, test RMSE =     3.70
degree  4: train RMSE =  1.66, test RMSE =     4.60
degree  8: train RMSE =  1.56, test RMSE =     8.38
degree 12: train RMSE =  0.99, test RMSE =  2210.82

Train RMSE প্রতিবার কমেছে, ৩.৬৮ থেকে ০.৯৯; test RMSE সবচেয়ে কম degree 2-এ (৩.৭০, যা আসল নিয়মের degree), তারপর বেড়েছে, আর degree 12-এ ২,২১০ মিনিট! ১৩টি parameter আর ১৫টি বিন্দু: model প্রায় প্রতিটি train বিন্দু মুখস্থ করেছে, আর বিন্দুগুলোর ফাঁকে ও প্রান্তের বাইরে বুনো দোল খেয়েছে (কিছু test অর্ডারের দূরত্ব train-এর সীমার বাইরে, সেখানে দোল সবচেয়ে বেশি)। এটিই overfitting, আর ছোট ডেটায় এক run-এর সংখ্যা বলে seed বদলালে এগুলো বেশ বদলাবে। কেন এমন হয় (bias আর variance), learning curve আর regularization দিয়ে কীভাবে সামলানো যায়, তা পর্ব ৬১-এ (Overfitting, bias আর variance).

অনুমানগুলো — নিয়ম নয়, নির্দেশিকা

Linear regression-এর coefficient পেতে কোনো অনুমান লাগে না; lstsq যেকোনো ডেটায় সবচেয়ে কম বর্গের রেখা দেয়। অনুমান লাগে যখন সেই রেখা দিয়ে কিছু দাবি করবেন: standard error, CI, p-value, বা “এই coefficient-ই আসল প্রভাব”। পাঠ্যবইয়ে প্রচলিত তালিকা, মোটামুটি গুরুত্বের ক্রমে:

  • Linearity: y-এর গড় feature-গুলোর রৈখিক যোগফল (দরকার হলে km²-এর মতো রূপান্তরিত feature-সহ)। না মানলে অংশ ৮-এর U.
  • Independent errors: এক অর্ডারের ভুল আরেকটির ভুলের ওপর নির্ভর করে না। একই দিনের বা একই রাইডারের অর্ডার, বা সময়ের ধারায় পরপর দিন (পর্ব ২৬), প্রায়ই এটি ভাঙে; তখন standard error অতিরিক্ত আশাবাদী হয়।
  • Constant variance: ভুলের ছড়ানো সব জায়গায় মোটামুটি এক; না মানলে ফানেল, আর CI ভুল।
  • Normal errors (সবচেয়ে কম জরুরি): ছোট নমুনায় CI আর p-value-এর জন্য লাগে; বড় নমুনায় central limit theorem-এর কারণে সাধারণত কম গুরুত্বপূর্ণ।

এগুলো কঠোর পাস-ফেল পরীক্ষা নয়; বাস্তবের ডেটা কোনোটিই নিখুঁতভাবে মানে না। প্রশ্নটি হলো, ভাঙন কতটা, আর আপনার সিদ্ধান্তে তার প্রভাব কতটা। শুধু অনুমান (prediction) চাইলে অনেক ভাঙন সহনীয়, কারণ শেষ বিচার হয় test ডেটায়; coefficient-এর অর্থ বা CI দাবি করলে এগুলো গুরুত্বপূর্ণ। কোন পরীক্ষা কতটা জরুরি, তা নিয়ে পরিসংখ্যানবিদদের মধ্যেও মতভেদ আছে।

একটু ইতিহাস। Least squares-এর পদ্ধতি প্রথম ছাপা হয় Adrien-Marie Legendre-এর নামে, ১৮০৫ সালে, ধূমকেতুর কক্ষপথের হিসাবে; Gauss ১৮০৯ সালে দাবি করেন তিনি বহু আগে থেকে এটি ব্যবহার করছিলেন। এই অগ্রাধিকার-বিতর্কের গল্প পর্ব ১৭-এর ভাবনার প্রশ্নে আছে। “Regression” নামটি এসেছে অন্য জায়গা থেকে: Francis Galton ১৮৮০-এর দশকে বাবা-মা আর সন্তানের উচ্চতা মেলাতে গিয়ে দেখেন, খুব লম্বা বাবা-মায়ের সন্তান গড়ে লম্বা হলেও বাবা-মায়ের চেয়ে কম লম্বা, গড়ের দিকে “ফিরে যায়” (regression); তাঁর ১৮৮৬ সালের প্রবন্ধের শিরোনামে “regression towards mediocrity” কথাটি ছিল বলে উল্লেখ করা হয়। নামটি থেকে গেছে, যদিও আজকের linear regression-এর সঙ্গে “ফিরে যাওয়া”-র সরাসরি যোগ নেই (ভাবনার প্রশ্ন ১)।

আরও গভীরে: Gauss–Markov theorem বলে, error-এর গড় শূন্য, variance সমান আর পরস্পর সম্পর্কহীন হলে (normal হওয়া লাগবে না), সব linear unbiased estimator-এর মধ্যে OLS-এর variance সবচেয়ে কম (“BLUE”). coefficient-এর standard error আসে σ²(XᵀX)−1-এর কর্ণ থেকে, যেখানে σ² residual-এর variance; multicollinearity-তে (XᵀX)−1-এর কর্ণ ফুলে ওঠে, এটিই অংশ ৬-এর বিশাল std err আর VIF-এর উৎস। সংখ্যাগত দিকে Gene Golub ১৯৬৫ সালে Householder QR দিয়ে least squares সমাধানের পদ্ধতি প্রকাশ করেন, যা normal equations-এর বর্গ-হওয়া condition number এড়ায়; আজকের LAPACK-এর routine-গুলো সেই ধারার। পরে পড়ার মতো একটি বই: Trefethen আর Bau-এর “Numerical Linear Algebra” বইয়ের least squares অধ্যায়।

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

  • Normal equations কী? XᵀX w = Xᵀy, MSE-এর gradient শূন্য বসিয়ে পাওয়া; সমাধান w = (XᵀX)−1Xᵀy, যদি XᵀX-এর inverse থাকে।
  • x = 1, 3; y = 2, 6 হলে রেখা? w = (6 − 2) ÷ (3 − 1) = 2, b = 2 − 2 × 1 = 0; ŷ = 2x.
  • কোডে inverse কেন নয়? cond(XᵀX) = cond(X)², তাই প্রায়-একই কলামে অঙ্ক হারায়; lstsq (QR বা SVD) X-এর ওপর সরাসরি কাজ করে।
  • Multicollinearity কী ক্ষতি করে? Coefficient অস্থির আর standard error বড়, ব্যাখ্যা অবিশ্বাস্য; অনুমান সাধারণত তেমন ক্ষতিগ্রস্ত হয় না। ধরতে VIF বা correlation.
  • R² বাড়ানোর সহজ উপায়, আর কেন তা ফাঁকি? যেকোনো feature যোগ করা; train R² কমে না। Adjusted R² বা test ডেটার মাপ দেখুন।

১০ · AI-এর যুগে — AI সহকারীর regression কোড আর ব্যাখ্যা যাচাই

“এই CSV-তে linear regression করে বলো কোন feature সবচেয়ে গুরুত্বপূর্ণ” বললে AI সহকারী কয়েক সেকেন্ডে কোড, টেবিল আর আত্মবিশ্বাসী ব্যাখ্যা দেয়। কোড প্রায়ই চলে; সমস্যা থাকে চোখের আড়ালে। যা প্রায়ই দেখা যায়:

  • সূত্র হুবহু কোডে: np.linalg.inv(X.T @ X) @ X.T @ y, অংশ ৪-এর ফাঁদ; বা statsmodels-এ add_constant ভুলে যাওয়া, ফলে intercept ছাড়া model.
  • পুরোনো বা অস্তিত্বহীন API: scikit-learn-এর LinearRegression(normalize=True) (১.২ version-এ সরানো হয়েছে), বা অস্তিত্বহীন কোনো parameter; ভুল error চাপা দিতে warnings.filterwarnings("ignore").
  • Coefficient-কে কারণ বলা, আর একক না দেখে “বড় coefficient মানে বেশি গুরুত্বপূর্ণ”; multicollinear feature-এর ঋণাত্মক coefficient-এর গল্প বানানো।
  • সব category-র dummy রেখে intercept-ও রাখা; scaling-এর গড় পুরো ডেটা থেকে নেওয়া (test-এর তথ্য train-এ ঢোকা); আর train ডেটাতেই R² মেপে “৯৮% নির্ভুল” দাবি।
  • Residual plot না দেখা, আর না-চালানো আউটপুট বা p-value বানিয়ে লেখা।

প্রম্পটে যাচাইয়ের কাজগুলো আগেই চেয়ে নিন, আর আসল ডেটার বদলে বানানো ডেটা দিন। নিচের প্রম্পটে খেয়াল করুন: জানা coefficient দিয়ে ডেটা বানানো, তাই ফল নিজেই মিলিয়ে দেখা যাবে।

আমি Python 3.14, NumPy 2.x, scikit-learn 1.8 আর statsmodels ব্যবহার করছি। আসল ডেটা দেব না। ১. seed 42 দিয়ে ২০০টি বানানো অর্ডার তৈরি করো: km (1–25), kg (0.2–15), items (1–8), rain আর peak (0/1). আসল নিয়ম: minutes = 12 + 2.5·km + 0.8·kg + 1.5·items + 9·rain + 7·peak + normal(0, 4) গোলমাল। ২. আগে train/test ভাগ করো (test 25%), তারপর np.linalg.lstsq আর sklearn LinearRegression দুটো দিয়েই fit করো, আর আসল coefficient-এর পাশে ছাপাও। inv() ব্যবহার করবে না। ৩. test ডেটায় RMSE আর R² ছাপাও, একটি baseline (train-এর গড়) সহ। ৪. residual বনাম fitted plot আঁকো, আর statsmodels OLS (add_constant দিয়ে) থেকে coefficient-এর 95% CI ছাপাও। ৫. coefficient-কে কারণ হিসেবে ব্যাখ্যা করবে না। কোনো function বা parameter আমার version-এ আছে কি না নিশ্চিত না হলে বলো; আউটপুট নিজে বানিয়ে লিখবে না, আমি নিজে চালাব।

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

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

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

প্র ০১Galton কেন “regression” শব্দটি বেছেছিলেন? আর “গড়ের দিকে ফিরে যাওয়া” আজও কোন ভুল সিদ্ধান্তের জন্ম দেয়?

Francis Galton, Charles Darwin-এর আত্মীয় এবং ভিক্টোরীয় যুগের এক বহুমুখী বিজ্ঞানী, বংশগতি মাপতে চেয়েছিলেন। প্রথমে মিষ্টি মটরের বীজের আকার, পরে মানুষের উচ্চতা নিয়ে তিনি একটি অদ্ভুত নিয়মিততা লক্ষ করেন: খুব লম্বা বাবা-মায়ের সন্তানেরা গড়ে লম্বাই হয়, কিন্তু বাবা-মায়ের মতো অতটা নয়; খুব খাটো বাবা-মায়ের সন্তানেরাও গড়ে খাটো, কিন্তু অতটা নয়। তিনি এর নাম দেন গড়ের দিকে “regression”, আর ১৮৮৬ সালের একটি প্রবন্ধের শিরোনামে “regression towards mediocrity in hereditary stature” কথাটি ছিল বলে প্রচলিত উদ্ধৃতি আছে। পরে Karl Pearson ও অন্যরা এর গণিত গুছিয়ে correlation-এর ধারণা দাঁড় করান।

এর পেছনে কোনো জৈবিক রহস্য নেই, আছে শুধু পরিসংখ্যান। উচ্চতা হলো বংশগত অংশ আর এলোমেলো অংশের যোগ। কেউ খুব লম্বা হলে সম্ভবত তাঁর এলোমেলো অংশও সেবার অনুকূলে ছিল; সন্তানের বেলায় সেই ভাগ্য নতুন করে আসে, তাই গড়ে কিছুটা কম চরম। দুটি মাপের correlation যত কম, এই ফিরে আসা তত বেশি। এর একটি সুন্দর দিক হলো, এটি উভয় দিকে কাজ করে: খুব লম্বা সন্তানের বাবা-মাও গড়ে সন্তানের চেয়ে কম লম্বা।

আজও এই ঘটনা ভুল কার্যকারণের জন্ম দেয়। ক্রিকেটে কোনো ব্যাটসম্যান এক মৌসুমে অবিশ্বাস্য রান করলে পরের মৌসুমে সাধারণত একটু কম করেন, আর ধারাভাষ্যে বলা হয় “চাপে পড়ে গেছেন”। পরীক্ষায় সবচেয়ে খারাপ ফল করা ছাত্রদের বিশেষ কোচিং দিলে পরের পরীক্ষায় তাদের গড় একটু বাড়ে, কোচিং কাজ না করলেও। বাজারঘরে যে মাসে ডেলিভারির দেরি সবচেয়ে বেশি ছিল, তার পরের মাসে নতুন কোনো নিয়ম চালু করলে দেরি কমবেই মনে হবে। মনোবিজ্ঞানী Daniel Kahneman প্রশিক্ষক আর প্রশংসা-তিরস্কারের এমন একটি উদাহরণ নিয়ে লিখেছেন বলে বহুল উদ্ধৃত। সঠিক যাচাইয়ের জন্য তাই তুলনামূলক দল (control group) লাগে।

মূল উপলব্ধি: চরম মাপ বেছে নিয়ে তার পরের মাপ দেখলে গড়ের দিকে ফিরে আসা প্রায় নিশ্চিত, কোনো কারণ ছাড়াই; “regression” নামটি সেই আবিষ্কারের স্মৃতি, আর এর শিক্ষা হলো তুলনামূলক দল ছাড়া উন্নতির দাবি সন্দেহের।

প্র ০২একই OLS, দুই ঐতিহ্য: statsmodels দেয় p-value, scikit-learn দেয় test score. Leo Breiman-এর “দুই সংস্কৃতি” বিতর্ক এখানে কী শেখায়?

২০০১ সালে পরিসংখ্যানবিদ Leo Breiman (classification tree আর random forest-এর অন্যতম স্রষ্টা) Statistical Science পত্রিকায় “Statistical Modeling: The Two Cultures” নামে একটি প্রবন্ধ লেখেন, যা আজও বহুল আলোচিত। তাঁর মতে এক সংস্কৃতি ধরে নেয় ডেটা একটি জানা ধরনের stochastic model থেকে আসছে (যেমন linear regression আর normal error), আর লক্ষ্য সেই model-এর parameter অনুমান করে প্রকৃতি বোঝা। অন্য সংস্কৃতি ডেটার উৎসকে একটি কালো বাক্স ধরে, আর লক্ষ্য নতুন ডেটায় নির্ভুল অনুমান; মাপকাঠি test ডেটার ভুল। Breiman যুক্তি দেন, প্রথম সংস্কৃতি পরিসংখ্যানে খুব বেশি প্রভাবশালী, আর এতে অনেক প্রশ্নে খারাপ সিদ্ধান্ত আসে। প্রবন্ধটির সঙ্গে প্রকাশিত মন্তব্যে David Cox, Bradley Efron প্রমুখ ভিন্নমত দেন; বিতর্কটি একতরফা ছিল না।

অংশ ৬-এর দুই library এই দুই সংস্কৃতির প্রতীকের মতো। statsmodels-এর টেবিলে std err, CI, p-value: “km-এর coefficient কতটা নিশ্চিত?” scikit-learn-এ score, cross-validation, pipeline: “নতুন অর্ডারে কত মিনিট ভুল?” একই coefficient, কিন্তু প্রশ্ন আলাদা। আর অংশ ৬ দেখিয়েছে, দুটি প্রশ্নের উত্তর আলাদা হতে পারে: multicollinear model-এর অনুমান চমৎকার (R² 0.95), অথচ coefficient-এর ব্যাখ্যা প্রায় অর্থহীন। উল্টোটাও হয়: খুব ভালো অনুমানের কালো বাক্স কেন ভালো, তা বলতে পারে না।

বাজারঘরের জন্য কোনটি? ক্রেতাকে “৪৫ মিনিট” দেখাতে চাইলে অনুমানের মান মুখ্য, test RMSE-ই বিচারক। কিন্তু ব্যবস্থাপক যদি জানতে চান “বৃষ্টির দিনে বাড়তি রাইডার রাখলে কত মিনিট বাঁচবে”, সেটি কারণের প্রশ্ন, আর তার জন্য coefficient-এর অনিশ্চয়তা, confounder আর হয়তো একটি পরিকল্পিত পরীক্ষা লাগে। আজকের অনেক কাজে দুই সংস্কৃতির মিশ্রণ দেখা যায়: causal inference, explainable ML আর prediction-এর জন্য তৈরি model-এর ওপর অনিশ্চয়তার মাপ।

মূল উপলব্ধি: একটি model তৈরির আগে জিজ্ঞেস করুন প্রশ্নটি অনুমানের না বোঝার; উত্তর ঠিক করে দেয় কোন সংখ্যা দেখবেন (test error, না CI), আর কোন ভুলটি বেশি বিপজ্জনক।

প্র ০৩বর্গই কেন? ১৮ শতকের Boscovich পরম মানের ভুল ব্যবহার করেছিলেন, Laplace-ও; তবু least squares জিতল কেন, আর আজ robust regression কোথায় ফিরে আসে?

Least squares-এর আগেই ভুল মেলানোর অন্য নিয়ম ছিল। ১৭৫০-এর দশকে Roger Boscovich পৃথিবীর আকার মাপার কাজে এমন রেখা খুঁজেছিলেন যাতে ভুলের পরম মানের যোগ সবচেয়ে কম হয় (সঙ্গে ভুলের যোগ শূন্য রাখার শর্ত), আর পরে Laplace এই ধারায় কাজ করেন; ইতিহাসবিদ Stephen Stigler-এর লেখায় এর বিস্তারিত পাওয়া যায়। এটি অংশ ২-এর MAE-রেখার পূর্বসূরি। সমস্যা ছিল হিসাব: পরম মানের যোগ কমানোর কোনো সরল সূত্র নেই, হাতে করা কঠিন।

Least squares জিতেছিল মূলত তিনটি কারণে। প্রথমত হিসাবের সুবিধা: derivative শূন্য বসালেই রৈখিক সমীকরণ (normal equations), যা কাগজ-কলমে সমাধান করা যায়, যেমন আমরা অংশ ৩-এ করেছি। দ্বিতীয়ত Gauss-এর যুক্তি: ভুল normal distribution মানলে least squares-ই সবচেয়ে সম্ভাব্য উত্তর (maximum likelihood). তৃতীয়ত পরে প্রমাণিত Gauss–Markov ফল: normal ছাড়াও, কিছু শর্তে, সব linear unbiased পদ্ধতির মধ্যে এর variance সবচেয়ে কম। এর সঙ্গে আছে জ্যামিতির সৌন্দর্য: projection, orthogonality, আর linear algebra-র পুরো যন্ত্রপাতি।

কিন্তু অংশ ২-এর run দেখিয়েছে মূল্যটা কী: একটি খারাপ বিন্দু পুরো রেখা বাঁকিয়ে দেয়। ১৯৬৪ সালে Peter Huber “Robust Estimation of a Location Parameter” প্রবন্ধে একটি আপস প্রস্তাব করেন: ছোট ভুলে বর্গ, বড় ভুলে পরম মান। এই Huber loss আজ scikit-learn-এর HuberRegressor-এ, আর deep learning-এও (smooth L1 নামে) ব্যবহৃত হয়। কম্পিউটার আসায় MAE-রেখাও এখন সহজ (linear programming), তাই আজকের প্রশ্ন আর “কোনটি হিসাব করা যায়” নয়, “কোন ভুলের দাম কত”। ডেলিভারির সময়ে মাঝে মাঝে বিশাল দেরি স্বাভাবিক হলে robust বা MAE-ভিত্তিক model বেশি বাস্তবসম্মত হতে পারে; গড় ঠিক রাখা জরুরি হলে (মোট খরচের হিসাব) MSE ভালো।

মূল উপলব্ধি: Least squares-এর আধিপত্য অংশত গণিতের সৌন্দর্য আর অংশত হিসাবের সুবিধার ফল; loss বাছাই আসলে একটি সিদ্ধান্ত যে কোন ভুলকে কতটা শাস্তি দেবেন, আর সেই সিদ্ধান্ত ডেটা ও কাজের ওপর নির্ভর করে।

প্র ০৪“বৃষ্টির coefficient ৯, তাই বৃষ্টি ৯ মিনিট দেরি ঘটায়” — এই বাক্যটি কখন ঠিক, কখন বিপজ্জনকভাবে ভুল?

অংশ ৪-এ বাক্যটি প্রায় ঠিক, কারণ আমরা নিজেরাই ডেটা বানিয়েছিলাম যাতে বৃষ্টি সরাসরি ৯ মিনিট যোগ করে আর অন্য কোনো feature-এর সঙ্গে জড়িত না থাকে। বাস্তবের পর্যবেক্ষণের ডেটায় এই নিশ্চয়তা নেই। Coefficient বলে, model-এ দেওয়া অন্য feature-গুলো স্থির রাখলে বৃষ্টির দিনগুলোতে গড়ে কত বেশি সময় দেখা গেছে। যদি এমন কোনো কারণ থাকে যা বৃষ্টি আর দেরি দুটিকেই প্রভাবিত করে, কিন্তু model-এ নেই (যেমন বৃষ্টির দিনে কম রাইডার কাজে আসেন), তাহলে তার প্রভাবও বৃষ্টির coefficient-এ ঢুকে যায়। পরিসংখ্যানে একে omitted variable bias বলে; পর্ব ৩৭-এ confounder আর Simpson's paradox-এর আলোচনা এরই আত্মীয়।

উল্টো ভুলও হয়: কোনো feature যোগ করলে যা আসলে কারণের পথের মাঝখানে। ধরুন model-এ “রাইডারের গড় গতি”-ও দেওয়া হলো। বৃষ্টি গতি কমায়, গতি দেরি বাড়ায়। গতি স্থির রেখে বৃষ্টির coefficient দেখলে তা প্রায় শূন্য হতে পারে, আর কেউ ভুল সিদ্ধান্তে পৌঁছাবে “বৃষ্টির কোনো প্রভাব নেই”। কোন feature রাখবেন, তা শুধু R² দেখে নয়, কারণের কাঠামো ভেবে ঠিক করতে হয়; Judea Pearl-এর মতো গবেষকেরা এ জন্য causal diagram ব্যবহারের পক্ষে জোরালো যুক্তি দিয়েছেন।

আর অংশ ৬-এর multicollinearity দেখিয়েছে, “স্থির রাখা” কখনো কখনো বাস্তবে অসম্ভব, তখন coefficient-এর একক অর্থই থাকে না। নিরাপদ ভাষা তাই: “এই ডেটায়, অন্য feature একই থাকলে, বৃষ্টির দিনের অর্ডার গড়ে প্রায় ৯ মিনিট বেশি সময় নিয়েছে, ৯৫% CI এত থেকে এত।” কারণ দাবি করতে চাইলে পরিকল্পিত পরীক্ষা (যেমন A/B test), বা causal inference-এর পদ্ধতি লাগে, আর সেগুলোরও নিজস্ব অনুমান থাকে।

মূল উপলব্ধি: Regression coefficient হলো model-এর ভেতরের একটি শর্তসাপেক্ষ সম্পর্ক, প্রকৃতির নিয়ম নয়; কোন feature model-এ আছে আর কোনটি নেই, তা বদলালেই coefficient-এর অর্থ বদলে যায়।

প্র ০৫একটি matrix বানানোতেই অঙ্ক হারায় কেন? Normal equations থেকে QR আর SVD-তে যাওয়ার পেছনের সংখ্যাগত গল্পটি কী?

১৯৫০-এর দশকে কম্পিউটারে least squares মানে প্রায় সবসময় normal equations: XᵀX বানানো, তারপর Cholesky বা Gaussian elimination. পদ্ধতিটি দ্রুত, আর ভালো ডেটায় চমৎকার। কিন্তু সংখ্যাগত বিশ্লেষণের গবেষকেরা লক্ষ করেন, X-এর কলামগুলো প্রায় নির্ভরশীল হলে ফল অবিশ্বাস্য হয়। কারণটি অংশ ৪-এর run-এ দেখেছেন: cond(XᵀX) = cond(X)². X-এর condition number 108 হলে float64-এ X নিয়ে কাজ করলে প্রায় ৮টি অঙ্ক টেকে, কিন্তু XᵀX বানালেই 1016, মানে প্রায় কোনো অঙ্কই টেকে না। সমস্যা পদ্ধতির পরের ধাপে নয়; XᵀX লেখার মুহূর্তেই তথ্য হারায়।

Gene Golub ১৯৬৫ সালে Numerische Mathematik-এ প্রকাশিত একটি প্রবন্ধে Householder reflection দিয়ে X-কে সরাসরি Q (orthogonal) আর R (ত্রিভুজাকার) matrix-এ ভাঙার পদ্ধতি দেখান। Orthogonal matrix দিয়ে গুণ দৈর্ঘ্য বদলায় না, তাই condition number বর্গ হয় না, আর সমাধান Rw = Qᵀy-এর সহজ পেছন-প্রতিস্থাপনে। একই সময়ে Golub আর William Kahan singular value decomposition (SVD) নির্ভরযোগ্যভাবে হিসাবের পদ্ধতি নিয়ে কাজ করেন; SVD আরও ধীর, কিন্তু প্রায়-নির্ভরশীল কলামে সবচেয়ে সহনশীল, আর rank কম হলে (অংশ ৭-এর dummy trap) সবচেয়ে ছোট দৈর্ঘ্যের উত্তর বেছে নিতে পারে। NumPy-র source-এ দেখা যায়, তার lstsq LAPACK-এর SVD-ভিত্তিক gelsd routine ডাকে; statsmodels-এর OLS ডিফল্টে (method="pinv") pseudo-inverse ব্যবহার করে, যেটিও SVD-ভিত্তিক।

তাহলে normal equations কি বাতিল? না। Feature খুব বেশি না হলে আর ডেটা ভালো আচরণের হলে এটি দ্রুততম, আর খুব বড় ডেটায় (কোটি সারি, কয়েকটি কলাম) XᵀX মাত্র p × p, অংশে অংশে জমানো যায়, যা QR-এ কঠিন। অনেক বড় ডেটার system এই কারণে normal equations-ই ব্যবহার করে, তবে scaling আর regularization দিয়ে ঝুঁকি কমিয়ে। আবারও trade-off: গতি আর মেমরি বনাম সংখ্যাগত নিরাপত্তা।

মূল উপলব্ধি: গণিতে সমান দুটি সূত্র কম্পিউটারে সমান নয়; কোন মধ্যবর্তী matrix বানানো হচ্ছে, তা-ই ঠিক করে কতটা নির্ভুলতা টিকবে, আর ভালো library-র ভেতরে এই সিদ্ধান্তগুলো দশকের গবেষণার ফল।

অনুশীলন

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

  1. হাতে হিসাব: তিনটি অর্ডার: x = 1, 2, 3 (কিমি), y = 2, 3, 5 (মিনিট, ছোট সংখ্যা হিসাবের সুবিধার জন্য)। (ক) Normal equations দিয়ে b আর w বের করুন। (খ) Residual, SSE আর R² কত? (গ) w = [0, 0] থেকে η = 0.05 দিয়ে vectorised gradient descent-এর এক ধাপ পরে [b, w] কত?

    (ক) XᵀX = [[3, 6], [6, 14]] (n = 3, ∑x = 6, ∑x² = 14), Xᵀy = [10, 23] (∑y = 10, ∑xy = 2 + 6 + 15 = 23). det = 42 − 36 = 6. b = (14 × 10 − 6 × 23) ÷ 6 = (140 − 138) ÷ 6 = 1/3 ≈ 0.333; w = (−6 × 10 + 3 × 23) ÷ 6 = 9 ÷ 6 = 1.5.

    (খ) অনুমান 1.833, 3.333, 4.833; residual 0.167, −0.333, 0.167 (যোগ 0). SSE = 0.0278 + 0.1111 + 0.0278 ≈ 0.167 (ঠিক 1/6). ȳ = 10/3, SST = 1.778 + 0.111 + 2.778 ≈ 4.667. R² = 1 − 0.167 ÷ 4.667 ≈ 0.964.

    (গ) r = ŷ − y = [−2, −3, −5]; Xᵀr = [−10, −23]; g = (2/3) × [−10, −23] ≈ [−6.667, −15.333]. নতুন w = [0, 0] − 0.05 × g ≈ [0.333, 0.767]. মজার কাকতালীয় ব্যাপার: b এক ধাপেই সঠিক 1/3-এ, কিন্তু w (0.767) সঠিক 1.5-এর অর্ধেকও নয়। (সংখ্যাগুলো ডেস্কটপে NumPy দিয়ে মেলানো।)

  2. আউটপুট অনুমান করুন: চালানোর আগে তিনটি লাইনের আউটপুট লিখুন, তারপর pandas 3-এ চালিয়ে মেলান। কেন এমন হলো?
    import pandas as pd
    df = pd.DataFrame({"zone": ["Dhaka", "Ctg", "Sylhet", "Ctg"],
                       "km": [3, 8, 5, 2]})
    d = pd.get_dummies(df, drop_first=True, dtype=int)
    print(d.columns.tolist())
    print(d.shape)
    print(d.iloc[1].tolist())
    ['km', 'zone_Dhaka', 'zone_Sylhet']
    (4, 3)
    [8, 0, 0]

    get_dummies শুধু লেখার কলাম (zone) ভাঙে, সংখ্যার কলাম km যেমন আছে তেমন রাখে, আর নতুন কলামগুলো শেষে যোগ করে। Category-গুলো বর্ণক্রমে সাজানো হয় (Ctg, Dhaka, Sylhet), আর drop_first=True প্রথমটি, Ctg, বাদ দেয়; সে হয় ভিত্তি। তাই ৪টি সারি, ৩টি কলাম। দ্বিতীয় সারি (index 1) Ctg-এর অর্ডার: km 8, আর দুই dummy-ই 0; “দুটিই শূন্য” মানেই ভিত্তি-এলাকা। dtype=int না দিলে pandas-এর নতুন version-এ dummy-গুলো True/False হিসেবে আসে। (ডেস্কটপে pandas 3.1.1-এ চালিয়ে মেলানো।)

  3. নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) ডেলিভারির সময়ের model নিয়ে এই প্রতিবেদন দিয়েছেন। অন্তত পাঁচটি সমস্যা চিহ্নিত করুন আর ভালো পথ বলুন।
    "৩০০টি অর্ডারে degree 10 polynomial + সব ৬৪ জেলার dummy + intercept
    দিয়ে w = inv(X.T @ X) @ X.T @ y বের করেছি। R² = 0.99 (একই ৩০০ অর্ডারে)।
    'রাস্তার দূরত্ব'-এর coefficient −3.1, তাই রাস্তা লম্বা হলে ডেলিভারি দ্রুত হয়।
    residual plot দেখার দরকার হয়নি, R² তো প্রায় 1."
    • R² train ডেটায়: একই ৩০০ অর্ডারে মাপা, আর degree 10 polynomial ও ৬৪টি dummy মিলিয়ে parameter অনেক; অংশ ৯-এর run-এর মতো overfitting প্রায় নিশ্চিত। আগে train/test ভাগ, তারপর test RMSE ও R², baseline-এর পাশে।
    • Dummy-variable trap: ৬৪টি dummy আর intercept একসঙ্গে, XᵀX singular. drop_first বা encoder-এর drop; ৩০০ অর্ডারে কয়েকটি জেলায় হয়তো একটি-দুটি অর্ডার, তাই জেলাকে বিভাগে মেলানো ভাবুন।
    • inv দিয়ে সমাধান: polynomial-এর কলামগুলো (x, x², … x¹⁰) প্রায় নির্ভরশীল, cond বিশাল, আর বর্গ হয়ে আরও খারাপ। lstsq বা scikit-learn, আর x-কে আগে scale করা।
    • Coefficient-এর ভুল ব্যাখ্যা: রাস্তার দূরত্ব নিশ্চয়ই সোজা দূরত্ব বা polynomial পদের সঙ্গে multicollinear; ঋণাত্মক চিহ্ন সম্ভবত অস্থিরতা, প্রকৃতি নয়। Seed বা bootstrap-এ coefficient কতটা লাফায় দেখুন, VIF মাপুন, আর কারণ দাবি করবেন না।
    • Residual না দেখা: R² বেশি হলেও নকশা থাকতে পারে (অংশ ৮-এ ভুল model-এর R² ছিল 0.95).
    • ভালো পথ: সরল model দিয়ে শুরু (দূরত্ব, ওজন, পণ্য, বৃষ্টি, ভিড়, বিভাগ), residual দেখে প্রয়োজন হলে km² বা log; সব যাচাই test ডেটায়।
  4. কোড লিখুন: একটি function fit_ols(F, y) লিখুন, যা feature matrix F (bias কলাম ছাড়া) নিয়ে নিজে bias কলাম যোগ করে lstsq দিয়ে w বের করে, আর w, R² ও RMSE ফেরত দেয়। অংশ ৩-এর তিনটি অর্ডারে (x = 2, 4, 6; y = 20, 26, 38) চালিয়ে দেখান।
    import numpy as np
    
    def fit_ols(F, y):
        F = np.asarray(F, dtype=float).reshape(len(y), -1)   # (n,) হলে (n, 1)
        X = np.column_stack([np.ones(len(y)), F])
        w = np.linalg.lstsq(X, y)[0]
        r = y - X @ w
        r2 = 1 - r @ r / np.sum((y - y.mean()) ** 2)
        return w, r2, np.sqrt(np.mean(r ** 2))
    
    y = np.array([20, 26, 38], dtype=float)
    w, r2, rmse = fit_ols([2, 4, 6], y)
    print(w.round(4), round(r2, 4), round(rmse, 4))

    ডেস্কটপে চালিয়ে পাওয়া ফল: [10. 4.5] 0.9643 1.4142. w হাতের হিসাবের [10, 4.5]-এর সঙ্গে মেলে, R² = 1 − 6/168 ≈ 0.9643, RMSE = √(6/3) = √2 ≈ 1.4142 মিনিট। reshape(len(y), -1) লাইনটি এক feature-এর (n,) আকারের ইনপুটকেও (n, 1) বানায়, যাতে একই function অনেক feature-এও চলে। r @ r হলো residual-এর বর্গের যোগ, SSE.

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

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

ক্রমএকটি linear regression model বানিয়ে যাচাই করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✂️ ডেটা train আর test-এ ভাগ করা
  2. 🧱 Train থেকে design matrix বানানো: bias কলাম, category-র one-hot (একটি বাদ দিয়ে), দরকারে scaling
  3. 🧮 lstsq বা LinearRegression দিয়ে coefficient বের করা
  4. 🔍 Residual plot দেখে নকশা খোঁজা, দরকারে নতুন feature বা রূপান্তর
  5. 📏 Test ডেটায় RMSE আর R², baseline-এর পাশে
  6. 🗣️ Coefficient-এর ব্যাখ্যা: একক, “বাকি সব স্থির রেখে”, CI, কারণ দাবি নয়

Design matrix X (প্রথম কলামে সব 1) আর লক্ষ্য y হলে MSE সবচেয়ে কম করার normal equations-এর সমাধান কোনটি?

  • w = X−1 y
  • w = (XᵀX)−1 Xᵀ y
  • w = (XXᵀ)−1 X y
  • w = Xᵀ y

দুটি বিন্দু (x, y) = (1, 2) আর (3, 6)-এ intercept-সহ least squares রেখা ŷ = b + w·x কী?

  • ŷ = 3x − 1
  • ŷ = 2 + 2x
  • ŷ = 2x
  • ŷ = 4

একটি design matrix X-এর condition number প্রায় 107. normal equations-এর জন্য XᵀX বানালে তার condition number মোটামুটি কত হবে, আর কী করা উচিত?

  • 107, তাই inverse নিরাপদ
  • 103.5, কারণ বর্গমূল হয়
  • 1014, তবে solve দিলেই সমস্যা মিটে যায়
  • 1014; XᵀX না বানিয়ে np.linalg.lstsq(X, y)

একটি “বিভাগ” feature-এ ৪টি category আছে, আর model-এ intercept আছে। One-hot encoding-এ কয়টি dummy কলাম রাখা উচিত?

  • ৩টি
  • ৪টি
  • ১টি, 0 থেকে 3 সংখ্যা দিয়ে
  • ২টি, binary কোডে

একটি linear regression-এর residual বনাম fitted ছবিতে স্পষ্ট U-আকার দেখা গেল (দুই প্রান্তে residual ধনাত্মক, মাঝে ঋণাত্মক)। এর সবচেয়ে সম্ভাব্য অর্থ কী?

  • ভুলের variance অসম (heteroscedasticity)
  • Model একটি অরৈখিক বাঁক ধরতে পারেনি
  • ডেটায় একটি outlier আছে
  • Model নিখুঁত, residual গোলমাল মাত্র

একটি regression-এ সম্পূর্ণ এলোমেলো (y-এর সঙ্গে সম্পর্কহীন) একটি নতুন feature যোগ করে একই train ডেটায় আবার fit করা হলো। কী আশা করবেন?

  • R² কমবে, adjusted R² বাড়বে
  • R² আর adjusted R² দুটিই কমবে
  • R² সমান বা সামান্য বেশি, adjusted R² সম্ভবত কমবে
  • কিছুই বদলাবে না

শব্দকোষ

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

মনে রাখুন

  1. ŷ = Xw, প্রথম কলাম সব 1 (bias); loss MSE, যা বড় ভুলে বেশি শাস্তি দেয়, তাই outlier-এ স্পর্শকাতর; MAE বা Huber বিকল্প।
  2. Normal equations XᵀXw = Xᵀy; residual প্রতিটি কলামের সঙ্গে লম্ব, bias থাকলে residual-এর যোগ শূন্য। কোডে lstsq, কারণ cond(XᵀX) = cond(X)².
  3. Gradient descent: w ← w − η(2/n)Xᵀ(Xw − y); scaling ছাড়া ধীর (এখানে ৫১,৮০৯ বনাম ১০ ধাপ), scaling-এর মাপ শুধু train থেকে।
  4. Coefficient = “বাকি সব স্থির রেখে” প্রভাব, এককসহ, কারণ নয়; প্রায়-একই feature-এ অস্থির (VIF, CI দেখুন); category-তে one-hot, একটি বাদ দিয়ে।
  5. Residual plot দেখুন (U = বাঁক, ফানেল = অসম variance, একা বিন্দু = outlier); R² ভুল model-এও উঁচু হতে পারে, polynomial বেশি হলে test-এ ভেঙে পড়ে।

পরের পর্বে: উত্তর যখন সংখ্যা নয়, “হ্যাঁ বা না” — অর্ডারটি কি ফেরত আসবে? Linear regression-এর যোগফলকে sigmoid দিয়ে সম্ভাবনায় বদলে logistic regression, cross-entropy loss আর decision boundary (পর্ব ৫৯)।

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

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