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

Logistic regression — সম্ভাবনা, sigmoid আর decision boundary

Logistic Regression — Odds, Sigmoid, Log Loss and the Decision Boundary, from NumPy Gradient Descent to scikit-learn

Taught in Bangla, this lesson shows why linear regression fails for yes/no targets, then builds logistic regression from odds, log-odds and the sigmoid. It trains the model in NumPy with a stable log loss and gradient checking, matches scikit-learn, reads weights as odds ratios, picks a cost-based threshold, and covers perfect separation, regularization, softmax and calibration with real runs.

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

  • হ্যাঁ/না প্রশ্নে linear regression কোথায় ভাঙে, আর odds, log-odds ও sigmoid কীভাবে তা সারায়
  • Model P(y = 1 | x) = σ(w·x + b), তার সরল রেখার decision boundary, আর ওজনকে odds ratio হিসেবে পড়া
  • Log loss কেন, MSE কেন নয়, gradient-এর সূত্র, আর হাতে এক ধাপ gradient descent
  • NumPy দিয়ে নিজের হাতে training, overflow-মুক্ত হিসাব, gradient checking আর scikit-learn-এর সঙ্গে মেলানো
  • খরচ দেখে threshold, পুরো আলাদা ডেটায় ওজনের বিস্ফোরণ আর regularization, softmax দিয়ে অনেক শ্রেণি, আর calibration

এই পর্বের তিনটি ভিত আগেই তৈরি। পর্ব ৩৪-এ gradient descent, learning rate আর gradient checking দেখেছেন; এখানে শুধু ব্যবহার করব। পর্ব ৩৮-এ cross-entropy আর log loss-এর জন্ম ও অর্থ; এখানে তাকে loss হিসেবে কমাব। পর্ব ৫০-এ odds, log-odds আর MLE বনাম MAP; এখানে odds-ই হবে model-এর ভাষা। আর পর্ব ৫১-এর শেষে দেখেছিলেন, Naive Bayes-এর log-odds শব্দ-গোনার একটি রৈখিক যোগফল; logistic regression ঠিক সেই রূপের ওজন সরাসরি ডেটা থেকে শেখে। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy 2.4, scikit-learn 1.8.0, matplotlib 3.10). scikit-learn প্রথমবার নামতে প্রায় ১০ সেকেন্ড লাগে; পরের run দ্রুত। চারটি ব্লক একই namespace ভাগ করে, তাই পরের কোনো ব্লক আগে চালালে আগেরগুলো আপনা থেকে আগে চলে।

১ · কাজটি — বাজারঘরের কোন অর্ডার ফেরত আসবে?

বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) লক্ষ করেছে, কিছু অর্ডার গ্রাহক ফিরিয়ে দেন: জামার মাপ মেলেনি, অনেক দেরিতে এসেছে, ছাড় দেখে ঝোঁকের মাথায় কিনেছিলেন। দোকান চায় পাঠানোর আগেই প্রতিটি অর্ডারের জন্য একটি সংখ্যা: এই অর্ডারটি ফেরত আসার সম্ভাবনা কত? সম্ভাবনা বেশি হলে ডেলিভারির আগে গ্রাহককে একটি ফোন করে মাপ আর রং নিশ্চিত করা হবে।

এটি একটি binary classification: প্রতিটি অর্ডারের label y = ১ (ফেরত এসেছে) বা y = ০ (আসেনি)। প্রতিটি অর্ডারের পাঁচটি feature:

Feature (কোডের নাম)মানলুকানো আসল ওজন
ছাড় (discount)০ থেকে ৫০ শতাংশ+0.05 প্রতি শতাংশ বিন্দু
ডেলিভারির দিন (days)১ থেকে ৭+0.6 প্রতি দিন
প্রথমবারের গ্রাহক (first_time)০ বা ১+1.0
পণ্য: পোশাক (clothing)০ বা ১+1.5
পণ্য: ইলেকট্রনিক্স (electronics)০ বা ১+0.6
ধ্রুবক (b)—−6.5

পণ্যের ধরন তিনটি: মুদি, পোশাক আর ইলেকট্রনিক্স। এমন শ্রেণিবাচক কলামকে সংখ্যায় আনার সাধারণ উপায় one-hot encoding: প্রতিটি ধরনের জন্য একটি ০/১ কলাম। আমরা মুদির কলামটি বাদ রেখেছি; তখন মুদি হলো তুলনার ভিত্তি, আর clothing-এর ওজন মানে “মুদির তুলনায় পোশাক হলে কত বেশি”. তিনটি কলামই রাখলে তাদের যোগফল সবসময় ১, যা ধ্রুবকের কলামের হুবহু নকল; linear model-এ তখন ওজনগুলো একভাবে ঠিক হয় না। Encoding-এর বিস্তারিত পর্ব ৬৩ (Feature engineering — scaling, encoding আর নতুন feature)-এ।

ডেটা সত্যিকারের নয়; নিচের কোড একটি seed দিয়ে ৮০০টি অর্ডার বানায়, যাতে প্রত্যেক পাঠক একই সংখ্যা পান। “লুকানো আসল ওজন” দিয়ে প্রতিটি অর্ডারের আসল ফেরতের সম্ভাবনা হিসাব করে, তারপর মুদ্রা ছোড়ার মতো করে ঠিক করা হয় অর্ডারটি সত্যিই ফেরত এসেছে কি না। ফলে আমরা জানি সঠিক উত্তর কী, আর দেখতে পারব model তা কতটা খুঁজে পায়। বাস্তবে আসল ওজন কেউ জানে না, আর আসল ফেরতের কারণ অনেক জটিল; এই নিয়মগুলো শেখানোর জন্য বানানো।

২ · Linear regression কেন চলে না — ০ থেকে ১-এর বাইরে অনুমান

প্রথম ভাবনাটি স্বাভাবিক: y তো একটি সংখ্যা (০ বা ১), তাহলে পর্ব ৫৮ (Linear regression — loss, gradient descent আর closed form)-এর মতো একটি linear regression fit করে তার অনুমানকে “সম্ভাবনা” বলি না কেন? একে linear probability model বলে, আর কখনো মোটামুটি কাজও দেয়। কিন্তু দুটি মৌলিক সমস্যা আছে।

  1. সীমার বাইরে অনুমান: একটি রেখা দুই দিকে অসীম পর্যন্ত যায়, তাই কিছু অর্ডারে অনুমান ০-এর কম বা ১-এর বেশি হবেই। −০.২ সম্ভাবনার কোনো মানে নেই।
  2. প্রভাব সবখানে সমান: রেখায় এক দিন বেশি দেরি মানে সবসময় একই পরিমাণ সম্ভাবনা যোগ। অথচ বাস্তবে ০.০২ থেকে ০.০৬-এ যাওয়া আর ০.৯৬ থেকে ১.০০-এর ওপরে যাওয়া এক জিনিস নয়।

আবহাওয়া অফিস বলে “কাল বৃষ্টির সম্ভাবনা ৭০%”. মেঘ যত ঘন হোক, কখনো “১৩০%” বলে না, রোদ যত কড়া হোক “−২০%”-ও না; প্রমাণ বাড়লে সম্ভাবনা ধীরে ধীরে ১০০%-এর দিকে যায়, কখনো পেরোয় না। আমাদের দরকার এমন একটি “চাপ দেওয়ার যন্ত্র”, যা যেকোনো স্কোরকে ০ থেকে ১-এর মধ্যে চেপে আনে।

নিচের ব্লক ডেটা বানায়, প্রথম ৬০০টি অর্ডারকে train আর শেষ ২০০টিকে test রাখে (ডেটা এলোমেলোভাবে তৈরি, তাই এভাবে কাটলেও কোনো ক্রমের ঝোঁক নেই; বাস্তবে ভাগের নিয়ম পর্ব ৫৬-এ)। তারপর পর্ব ১৭-এর lstsq দিয়ে linear regression চালায়, আর পাশে sigmoid ও তার derivative আঁকে।

import numpy as np
import matplotlib.pyplot as plt

def make_orders(n=800, seed=42):                       # বাজারঘরের কাল্পনিক অর্ডার
    rng = np.random.default_rng(seed)
    disc = rng.integers(0, 51, n)                      # ছাড় (%)
    days = rng.integers(1, 8, n)                       # ডেলিভারিতে কত দিন
    first = (rng.random(n) < 0.3).astype(int)          # প্রথমবারের গ্রাহক?
    cat = rng.choice(["grocery", "clothing", "electronics"], n, p=[0.4, 0.35, 0.25])
    X = np.column_stack([disc, days, first, cat == "clothing", cat == "electronics"]).astype(float)
    z = X @ np.array([0.05, 0.6, 1.0, 1.5, 0.6]) - 6.5  # লুকানো আসল নিয়ম
    y = (rng.random(n) < 1 / (1 + np.exp(-z))).astype(int)   # 1 = ফেরত এসেছে
    return X, y

X, y = make_orders()
Xtr, ytr, Xte, yte = X[:600], y[:600], X[600:], y[600:]
print("ফেরতের হার: train", ytr.mean().round(3), "| test", yte.mean().round(3))
print("প্রথম ৩টি অর্ডার:\n", X[:3], y[:3])

A = np.column_stack([np.ones(600), Xtr])               # linear regression (lstsq)
beta = np.linalg.lstsq(A, ytr, rcond=None)[0]
pred = A @ beta
print("linear: min", pred.min().round(3), "max", pred.max().round(3), "| 0-এর নিচে", (pred < 0).sum(), "টি")
eid = np.array([1, 60, 12, 1, 1, 0])                   # ৬০% ছাড়, ১২ দিন
print("ঈদের ভিড়ের অর্ডারে linear:", (eid @ beta).round(3))

z = np.linspace(-8, 8, 200); s = 1 / (1 + np.exp(-z))
fig, ax = plt.subplots(1, 2, figsize=(9, 3.4), layout="constrained")
ax[0].hist(pred, bins=30, color="#1f77b4")
ax[0].axvspan(pred.min(), 0, color="#dc2626", alpha=0.25, label="impossible (< 0)")
ax[0].set(title="Linear regression on a 0/1 target", xlabel="predicted value", ylabel="orders"); ax[0].legend()
ax[1].plot(z, s, label="σ(z)"); ax[1].plot(z, s * (1 - s), "--", label="σ(z)(1 − σ(z))")
ax[1].axhline(0.5, color="gray", lw=0.8)
ax[1].set(title="Sigmoid and its derivative", xlabel="z = w·x + b"); ax[1].legend()
plt.show()

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4):

ফেরতের হার: train 0.208 | test 0.185
প্রথম ৩টি অর্ডার:
 [[ 4.  1.  0.  0.  1.]
 [39.  3.  0.  0.  0.]
 [33.  4.  0.  1.  0.]] [0 0 0]
linear: min -0.244 max 0.735 | 0-এর নিচে 82 টি
ঈদের ভিড়ের অর্ডারে linear: 1.172

প্রায় ২০% অর্ডার ফেরত এসেছে। Linear regression-এর অনুমান −০.২৪৪ থেকে ০.৭৩৫: ৬০০টির মধ্যে ৮২টি অর্ডারের “সম্ভাবনা” ঋণাত্মক, অর্থাৎ অসম্ভব। আর training-এর সীমার বাইরের একটি অর্ডার (৬০% ছাড়, ঈদের ভিড়ে ১২ দিন, নতুন গ্রাহক, পোশাক) পায় ১.১৭২, যা ১-এর বেশি। বাঁয়ের ছবির লাল অংশটিই সমস্যা। ডানের ছবিতে sigmoid: z যত বড়ই হোক মান ১ ছোঁয় না, যত ছোটই হোক ০ ছোঁয় না, আর z = ০-তে ঠিক ০.৫. ড্যাশ দেওয়া curve-টি তার ঢাল, যা z = ০-তে সর্বোচ্চ ০.২৫ আর দুই প্রান্তে প্রায় শূন্য; এই তথ্যটি অংশ ৪-এ খুব কাজে লাগবে। (এই অর্ডারে পরে-শেখা logistic model দেয় প্রায় ০.৯৯৭: খুব বেশি, কিন্তু ১-এর কম।)

৩ · Odds, log-odds আর sigmoid — স্কোর থেকে সম্ভাবনা

সমাধানের চাবি পর্ব ৫০-এর odds: সম্ভাবনা p হলে odds = p/(1 − p). p = ০.২ মানে odds ০.২৫ (১ : ৪, অর্থাৎ পাঁচটির মধ্যে একটি ফেরত), p = ০.৫ মানে ১, p = ০.৮ মানে ৪. সম্ভাবনা ০ থেকে ১-এ আটকা, কিন্তু odds ০ থেকে অসীম। এবার odds-এর স্বাভাবিক log নিই: এই log-odds (একে logit-ও বলে) −অসীম থেকে +অসীম পর্যন্ত যেকোনো মান নিতে পারে, আর p = ০.৫-এ ঠিক ০.

সম্ভাবনা p0.10.20.50.80.9
odds = p/(1 − p)0.1110.25149
log-odds = ln odds−2.197−1.3860+1.386+2.197

লক্ষ করুন log-odds-এর প্রতিসাম্য: ০.২ আর ০.৮ সমান দূরে, উল্টো চিহ্নে। Log-odds-এর পরিসর সব বাস্তব সংখ্যা, ঠিক একটি রৈখিক স্কোরের মতো। তাই logistic regression-এর মূল অনুমানটি এক লাইনের: log-odds হলো feature-এর একটি রৈখিক যোগফল।

ln [ p/(1 − p) ] = z = w·x + b = w1x1 + w2x2 + … + wdxd + b

এখানে x হলো feature-এর vector, w হলো প্রতিটি feature-এর ওজন (coefficient), w·x হলো পর্ব ৩৩-এর dot product, আর b হলো bias term বা intercept: সব feature শূন্য হলে স্কোর। সমীকরণটি p-এর জন্য উল্টে সমাধান করলে আসে sigmoid বা logistic function:

p = σ(z) = 1 / (1 + e−z) · σ′(z) = σ(z) · (1 − σ(z))

দ্বিতীয় সূত্রটি sigmoid-এর derivative, যা নিজের মান দিয়েই লেখা যায়: p = ০.৫-এ ০.২৫, p = ০.৮ বা ০.২-এ ০.১৬, p = ০.৯৯-এ মাত্র ০.০০৯৯. এটি পর্ব ৩৪-এর chain rule থেকে আসে: σ = (1 + e−z)−1, তাই σ′ = e−z/(1 + e−z)2 = σ · (1 − σ). আরও দুটি কাজের ধর্ম: σ(−z) = 1 − σ(z), আর z যত বড়, সম্ভাবনা তত ১-এর কাছে, কিন্তু কখনো ১ নয়।

Logistic regression-এর পথ: feature থেকে স্কোর, স্কোর থেকে সম্ভাবনা, সম্ভাবনা থেকে সিদ্ধান্ত

features → linear score z → sigmoid → probability → threshold → class

features → z = w·x + b → σ(z) → p → p ≥ t? → class feature x ছাড়, দিন, নতুন, পণ্য স্কোর z w·x + b −∞ থেকে +∞ sigmoid 1/(1+e⁻ᶻ) সম্ভাবনা p ০ থেকে ১ P(ফেরত | x) threshold t p ≥ t ? ব্যবসার সিদ্ধান্ত শ্রেণি ১: ফোন করুন ০: ঠিক আছে model শেখে w আর b; t বাছেন মানুষ, খরচ দেখে

নিচের stepper একটি অর্ডারকে পুরো পথটি পার করায়। ওজনগুলো অংশ ৫-এ ৬০০টি train অর্ডার থেকে শেখা, তিন দশমিকে গোল করা: w = (0.043, 0.613, 0.597, 1.493, 0.309), b = −6.211.

অর্ডার: ৩০% ছাড়, ৫ দিন, নতুন গ্রাহক, পোশাক
ধাপ
হিসাব
ফল
  1. Feature vector. ছাড় ৩০, দিন ৫, নতুন গ্রাহক ১, পোশাক ১, ইলেকট্রনিক্স ০. মুদির কোনো কলাম নেই; মুদি হলে শেষ দুটিই ০ হতো।
  2. প্রতিটি feature তার ওজনে গুণ। সবচেয়ে বড় অবদান দেরির (৫ দিন × ০.৬১৩ = ৩.০৬৫), তারপর পোশাকের (১.৪৯৩) আর ছাড়ের (১.২৯০). ইলেকট্রনিক্সের পদ ০, কারণ feature-টিই ০.
  3. যোগ করে b. পাঁচটি পদের যোগফল ৬.৪৪৫, তার সঙ্গে b = −৬.২১১: z = ০.২৩৪. এটি log-odds, এখনো সম্ভাবনা নয়; যেকোনো চিহ্নের যেকোনো সংখ্যা হতে পারত।
  4. e−z. e−০.২৩৪ ≈ ০.৭৯১. z ধনাত্মক বলে এটি ১-এর কম।
  5. Sigmoid. ১ ÷ ১.৭৯১ ≈ ০.৫৫৮. ফেরতের সম্ভাবনা প্রায় ৫৬%. (পূর্ণ দশমিকের ওজনে scikit-learn দেয় ০.৫৬২; গোল করার ছোট পার্থক্য।)
  6. Odds আর সিদ্ধান্ত। odds = ০.৫৫৮/০.৪৪২ ≈ ১.২৬, যা e০.২৩৪ ≈ ১.২৬৪-এর সমান (গোল করার তফাত বাদে); প্রতি ১০০টি না-ফেরতের বিপরীতে প্রায় ১২৬টি ফেরত। Threshold ০.৫ হোক বা অংশ ৭-এর খরচভিত্তিক ০.১৫, দুটিতেই ০.৫৫৮ সীমা পেরোয়, তাই “ফোন করুন”.

Logistic curve-এর জন্ম জনসংখ্যার হিসাবে। বেলজিয়ামের গণিতবিদ Pierre-François Verhulst ১৮৩০-এর দশকের শেষে এমন বৃদ্ধির সূত্র দেন, যা শুরুতে দ্রুত, পরে সম্পদের সীমায় ধীর হয়ে থামে, আর ১৮৪০-এর দশকের মাঝামাঝি একটি লেখায় curve-টিকে “logistique” নাম দেন; নামটি কেন বেছেছিলেন, তা স্পষ্ট নয়। সম্ভাবনার model হিসেবে এর প্রচারে বড় ভূমিকা Joseph Berkson-এর: ১৯৪৪ সালে তিনি “logit” শব্দটি চালু করেন আর তখনকার জনপ্রিয় probit model-এর বিকল্প হিসেবে logistic-এর পক্ষে লেখেন। হ্যাঁ/না ফলের ওপর regression-এর আধুনিক রূপের জন্য প্রায়ই David Cox-এর ১৯৫৮ সালের প্রবন্ধ “The regression analysis of binary sequences”-কে কৃতিত্ব দেওয়া হয়। J. S. Cramer-এর ২০০২ সালের একটি ইতিহাস-প্রবন্ধ এই ধারা বিস্তারিত দেখায়; কে “প্রথম”, তা নিয়ে বিবরণ ভিন্ন।

৪ · Loss — log loss কেন, MSE কেন নয়, আর এক ধাপ হাতে

Model-এর রূপ ঠিক হলো; এবার w আর b বাছার নিয়ম। পর্ব ৩৮-এর এক লাইনের সারাংশ: একটি অর্ডার আসলে যা ঘটেছে, model তাকে যে সম্ভাবনা দিয়েছিল, তার −ln-এর গড়ই log loss বা binary cross-entropy:

L(w, b) = −(1/n) ∑i [ yi ln pi + (1 − yi) ln(1 − pi) ], pi = σ(w·xi + b)

n হলো অর্ডারের সংখ্যা। ফেরত আসা অর্ডারে (y = ১) শুধু প্রথম পদ বাঁচে, −ln p: p ১-এর কাছে হলে loss প্রায় ০, p ০-এর কাছে হলে বিশাল। প্রতিটি অর্ডারকে একটি মুদ্রা ছোড়া ভাবলে (ফেরতের সম্ভাবনা pi), পুরো ডেটা দেখার likelihood হলো ∏ piyi(1 − pi)1 − yi. এর log নিয়ে ঋণাত্মক করে n দিয়ে ভাগ করলেই ওপরের সূত্র। তাই log loss কমানো মানে maximum likelihood-এ ওজন বাছা; একই জিনিসের তৃতীয় নাম negative log-likelihood (NLL). (পর্ব ৫০-এর ভাষায়, ওজনের ওপর একটি prior যোগ করলে তা MAP হয়, অংশ ৯-এর regularization ঠিক সেটাই।)

MSE দিলে কী হয়?

Linear regression-এর mean squared error, (σ(z) − y)2-এর গড়, sigmoid-এর ওপরেও বসানো যায়। দুটি সমস্যা। এক, loss-এর আকার আর বাটির মতো থাকে না, অর্থাৎ convex থাকে না; তাতে সমতল মালভূমি আর একাধিক বাঁক তৈরি হয়। দুই, chain rule-এ σ′ = σ(1 − σ) গুণ হয়ে আসে: model খুব আত্মবিশ্বাসী আর ভুল হলে (y = ১ অথচ σ ≈ ০) এই গুণনীয়ক প্রায় শূন্য, তাই gradient প্রায় শূন্য; যেখানে সবচেয়ে বেশি শেখার দরকার, সেখানেই শেখা থেমে যায়। Log loss-এ এই গুণনীয়কটি কাটাকাটি হয়ে যায় (নিচের “আরও গভীরে” বাক্সে)। ডেস্কটপে একটি ছোট পরীক্ষা: একটি ফেরত আসা অর্ডারে z-এর সাপেক্ষে দুই loss-এর gradient, পাঁচটি অর্ডারের একটি ছোট ডেটায় loss-এর আকার, আর w = −৬ (খুব ভুল) থেকে ২০০ ধাপ gradient descent.

y = 1.0                                    # অর্ডারটি আসলে ফেরত এসেছে
for z in (-8.0, -4.0, 0.0, 4.0):
    s = 1 / (1 + np.exp(-z))
    g_log = s - y                          # d(log loss)/dz
    g_mse = 2 * (s - y) * s * (1 - s)      # d((σ − y)²)/dz
    print(f"z = {z:+.0f}  σ = {s:.4f}  log-loss grad {g_log:+.4f}  MSE grad {g_mse:+.6f}")

x = np.array([-3.0, -1.0, 1.0, 2.0, 3.0]); t = np.array([0.0, 1.0, 0.0, 1.0, 1.0])
w = np.linspace(-6, 6, 1201)               # loss-এর আকার: দ্বিতীয় পার্থক্যের চিহ্ন
sig = 1 / (1 + np.exp(-np.outer(w, x)))
L_mse = ((sig - t) ** 2).mean(axis=1)
L_log = -(t * np.log(sig) + (1 - t) * np.log(1 - sig)).mean(axis=1)
# … দুই loss-এ np.diff(L, 2) < 0 কতবার, তারপর w = -6 থেকে ২০০ ধাপ GD (η = 0.5)

সত্যিকারের run (ডেস্কটপ, Python 3.14.4, NumPy 2.5.4):

~/bazarghar-ai$ python mse_vs_logloss.py
z = -8  σ = 0.0003  log-loss grad -0.9997  MSE grad -0.000670
z = -4  σ = 0.0180  log-loss grad -0.9820  MSE grad -0.034690
z = +0  σ = 0.5000  log-loss grad -0.5000  MSE grad -0.250000
z = +4  σ = 0.9820  log-loss grad -0.0180  MSE grad -0.000635
MSE      second difference < 0 at 1055 of 1199 points -> NOT convex
log loss second difference < 0 at 0 of 1199 points -> convex
MSE      after 200 steps from w = -6: w = -6.000
log loss after 200 steps from w = -6: w = +0.607

z = −৮-এ model ফেরতের সম্ভাবনা দিচ্ছে ০.০০০৩, অথচ অর্ডারটি ফেরত এসেছে। Log loss-এর gradient −০.৯৯৯৭, পূর্ণ শক্তিতে ঠিক দিকে; MSE-এর মাত্র −০.০০০৬৭, প্রায় দেড় হাজার গুণ দুর্বল। MSE-এর curve ১১৯৯টির মধ্যে ১০৫৫টি জায়গায় নিচের দিকে বাঁকা (convex নয়), log loss কোথাও নয়। ২০০ ধাপ পর MSE w = −৬-এ প্রায় নড়েনি, log loss পৌঁছেছে ০.৬০৭-এ, যা scikit-learn-এর penalty-হীন উত্তরের সঙ্গে মেলে। (এই পাঁচটি অর্ডার ইচ্ছা করে এমন রাখা যাতে কোনো সরল রেখা দুই শ্রেণিকে পুরো আলাদা করতে না পারে; কেন তা জরুরি, অংশ ৯-এ।)

আরও গভীরে — gradient-এর সূত্র: একটি অর্ডারের loss ℓ = −[y ln σ(z) + (1 − y) ln(1 − σ(z))], z = w·x + b. Chain rule-এ ∂ℓ/∂z = −y·σ′/σ + (1 − y)·σ′/(1 − σ); σ′ = σ(1 − σ) বসালে = −y(1 − σ) + (1 − y)σ = σ(z) − y, অর্থাৎ সোজা “অনুমান বিয়োগ সত্য”, σ′-এর গুণনীয়ক উধাও। তারপর ∂z/∂w = x আর ∂z/∂b = ১, তাই n টি অর্ডারের X matrix (প্রতি সারিতে একটি অর্ডার) ধরে ∇wL = Xᵀ(σ(Xw + b) − y)/n আর ∂L/∂b = (σ − y)-এর গড়; linear regression-এর MSE gradient-এর সঙ্গে রূপ হুবহু এক, শুধু অনুমানের জায়গায় σ. দ্বিতীয় derivative-এর matrix (Hessian) হলো XᵀSX/n, যেখানে S-এর কর্ণে σi(1 − σi) ≥ ০; এমন matrix positive semidefinite, তাই log loss convex: যেকোনো local minimum-ই global. এই কারণেই scikit-learn-এর default solver lbfgs, বা Newton-এর পদ্ধতির মতো দ্রুত পথ এখানে নিরাপদে কাজ করে।

এক ধাপ gradient descent, হাতে

বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এক ধাপ হাতে কষতে দেওয়া খুব সাধারণ। চারটি অর্ডার, একটি feature (standardize করা ডেলিভারির দিন): x = (−1, 0, 1, 2), y = (0, 0, 1, 1). শুরু w = ০, b = ০, learning rate η = ০.৫. নিয়ম: w ← w − η · mean((σ − y)·x), b ← b − η · mean(σ − y).

চার অর্ডার: x = (−1, 0, 1, 2), y = (0, 0, 1, 1)
w, b
z = wx + b
σ(z)
e = σ − y
gradient
log loss
  1. শুরু। সব ওজন ০. Model এখনো কিছু জানে না।
  2. সামনে হিসাব। w = b = ০, তাই চারটি z-ই ০ আর σ = ০.৫: প্রতিটি অর্ডারে “আধাআধি”. Log loss = ln ২ ≈ ০.৬৯৩১, মুদ্রা ছোড়ার baseline.
  3. ভুল। e = σ − y: যেগুলো ফেরত আসেনি (প্রথম দুটি), সেখানে +০.৫ (বেশি বলেছি); যেগুলো এসেছে, সেখানে −০.৫ (কম বলেছি)।
  4. Gradient. e·x = (−০.৫, ০, −০.৫, −১); গড় −০.৫. ∂b = e-এর গড় = ০, কারণ দুই দিকের ভুল সমান। ঋণাত্মক gradient মানে w বাড়ালে loss কমবে।
  5. হালনাগাদ। w = ০ − ০.৫ × (−০.৫) = ০.২৫, b = ০ − ০.৫ × ০ = ০. নতুন σ এখন ফেরত আসা অর্ডারে ০.৫-এর বেশি, অন্যগুলোয় কম বা সমান; loss নেমে ০.৫৭৯৮. পরের ধাপে gradient −০.৪০৮, একটু ছোট, কারণ ভুল কমেছে।

৫ · নিজের হাতে training — stable হিসাব, gradient checking আর scikit-learn

কম্পিউটারে দুটি ফাঁদ। প্রথমটি overflow: z = −১০০০ হলে np.exp(-z) = e১০০০, যা float64-এর সীমা (প্রায় 1.8 × 10308) পেরিয়ে inf হয়। দ্বিতীয়টি log(0): z = ৪০-এ σ(z) float64-এ হুবহু 1.0 হয়ে যায় (1 − e−৪০-এর পার্থক্য float ধরতে পারে না), তাই y = ০ হলে ln(1 − σ) = ln 0 = −∞. ডেস্কটপে সাধারণ সূত্রে (সত্যিকারের run, NumPy 2.5.4):

~/bazarghar-ai$ python overflow.py
overflow.py:3: RuntimeWarning: overflow encountered in exp
  s = 1 / (1 + np.exp(-z))                  # naive sigmoid
overflow.py:5: RuntimeWarning: divide by zero encountered in log
  print("naive log loss for y = 0:", -np.log(1 - s))
naive sigmoid: [0. 1.]
naive log loss for y = 0: [-0. inf]
stable log loss for y = 0: [ 0. 40.]

সমাধান numerical stability-র একটি পরিচিত কৌশল: log loss-কে সম্ভাবনা দিয়ে নয়, সরাসরি z দিয়ে লেখা। একটু বীজগণিতে ℓ = ln(1 + ez) − y·z, আর ln(1 + ez) = ln(e0 + ez), যা np.logaddexp(0, z) নিরাপদে হিসাব করে (log-sum-exp কৌশল, সবচেয়ে বড় পদটি আগে আলাদা করে)। Sigmoid-ও একইভাবে: σ(z) = exp(−ln(1 + e−z)). আরেকটি পথ σ-কে clip করা (পর্ব ৩৮); কাজ চলে, তবে চরম z-এ loss-এর আসল মান হারায়। নিচের ব্লকে বাকি সবকিছু: ডেটা standardize করা (প্রতিটি feature থেকে গড় বিয়োগ, standard deviation দিয়ে ভাগ, যাতে ছাড়ের ০–৫০ আর দিনের ১–৭ একই মাপে আসে আর gradient descent দ্রুত মেলে), ৩০০০ ধাপ gradient descent, পর্ব ৩৪-এর central difference দিয়ে gradient checking, আর scikit-learn-এর সঙ্গে তুলনা।

from sklearn.linear_model import LogisticRegression

def sigmoid(z):                       # 1/(1+e^−z), কিন্তু বড় |z|-এ overflow নেই
    return np.exp(-np.logaddexp(0, -z))
def loss(w, b, S, y):                 # log loss = mean[log(1+e^z) − y·z]
    z = S @ w + b
    return np.mean(np.logaddexp(0, z) - y * z)
def grad(w, b, S, y):                 # Sᵀ(σ − y)/n
    e = sigmoid(S @ w + b) - y
    return S.T @ e / len(y), e.mean()

zz = np.array([-1000.0, -40.0, 0.0, 40.0])
print("naive σ :", 1 / (1 + np.exp(-zz)), "| stable σ:", sigmoid(zz))
print("y=0, z=40 → naive loss", -np.log(1 - 1 / (1 + np.exp(-40.0))),
      "| stable", np.logaddexp(0, 40.0))

mu, sd = Xtr.mean(0), Xtr.std(0)
S = (Xtr - mu) / sd                   # standardize করলে GD অনেক দ্রুত
w, b, eta = np.zeros(5), 0.0, 0.5
for step in range(1, 3001):
    gw, gb = grad(w, b, S, ytr)
    w, b = w - eta * gw, b - eta * gb
    if step in (1, 10, 100, 3000):
        print(f"step {step:4d}  loss {loss(w, b, S, ytr):.5f}")

rng = np.random.default_rng(0); w0, h = rng.normal(size=5), 1e-5
num = [(loss(w0 + h*e, 0.3, S, ytr) - loss(w0 - h*e, 0.3, S, ytr)) / (2*h) for e in np.eye(5)]
print("gradient check, সর্বোচ্চ পার্থক্য:", np.abs(num - grad(w0, 0.3, S, ytr)[0]).max())

w_raw, b_raw = w / sd, b - np.sum(w * mu / sd)      # আসল এককে ফেরা
sk = LogisticRegression(C=1e10, max_iter=1000).fit(Xtr, ytr)   # C বিশাল ≈ penalty নেই
print("scratch:", w_raw.round(3), b_raw.round(3))
print("sklearn:", sk.coef_[0].round(3), sk.intercept_.round(3))

আসল আউটপুট (ব্রাউজারের Python, NumPy 2.4, scikit-learn 1.8.0):

naive σ : [0.00000000e+00 4.24835426e-18 5.00000000e-01 1.00000000e+00] | stable σ: [0.00000000e+00 4.24835426e-18 5.00000000e-01 1.00000000e+00]
y=0, z=40 → naive loss inf | stable 40.0
step    1  loss 0.63646
step   10  loss 0.45272
step  100  loss 0.38731
step 3000  loss 0.38696
gradient check, সর্বোচ্চ পার্থক্য: 6.230849169952535e-12
scratch: [0.043 0.613 0.597 1.494 0.309] -6.211
sklearn: [0.043 0.613 0.597 1.493 0.309] [-6.211]

প্রথম দুই লাইন: সাধারণ sigmoid-এর মানগুলো এখানে ঠিকই আছে, কিন্তু y = ০, z = ৪০-এ সাধারণ loss inf, stable loss ঠিক ৪০ (কারণ ln(1 + e৪০) ≈ ৪০)। খেয়াল করুন, ব্রাউজারে কোনো RuntimeWarning আসেনি, যদিও ডেস্কটপে এই লাইনেই ওপরের মতো দুটি warning আসে; WebAssembly-তে floating-point-এর সতর্কসংকেত নেই, তাই ব্রাউজারের চুপ থাকা মানে নিরাপদ নয়।

Loss ০.৬৩৬ থেকে ১০০ ধাপেই ০.৩৮৭-এ, তারপর প্রায় স্থির: convex loss-এ gradient descent নিশ্চিন্তে তলায় পৌঁছায়। Gradient check-এ সূত্র আর সংখ্যায় পাওয়া derivative-এর পার্থক্য ৬.২ × ১০−১২: সূত্রে বাগ নেই (১০−৪-এর মতো হলে সন্দেহ করুন)। Standardize করা জগতের ওজন আসল এককে ফেরালে (w/sd, আর b থেকে গড়ের প্রভাব বিয়োগ) scikit-learn-এর সঙ্গে তিন দশমিক পর্যন্ত মেলে; একটি ঘরে শেষ অঙ্কে ১-এর তফাত, থামার নিয়মের।

আসল ওজনের সঙ্গে তুলনা: দিন আর পোশাক খুব কাছে, কিন্তু নতুন গ্রাহক ০.৫৯৭ (আসল ১.০) আর ইলেকট্রনিক্স ০.৩০৯ (আসল ০.৬). ৬০০টি অর্ডারে মাত্র ১২৫টির মতো ফেরত, তাই অনুমান নড়বড়ে; অন্য seed-এ অন্য সংখ্যা আসবে।

scikit-learn-এর LogisticRegression — default-এ penalty আছে

একটি গুরুত্বপূর্ণ খুঁটিনাটি: LogisticRegression()-এর default শুধু log loss কমায় না, সঙ্গে ওজনের বর্গের একটি শাস্তি (L2 penalty) যোগ করে, যার শক্তি ১/C; default C = ১.০. তাই default model-এর ওজন আমাদের penalty-হীন উত্তরের চেয়ে ছোট হবে। তুলনার জন্য C খুব বড় দিয়েছি (C=1e10), যাতে শাস্তি প্রায় শূন্য। Penalty-হীন model বোঝাতে পুরোনো লেখায় penalty=None দেখবেন; scikit-learn 1.8-এ penalty parameter-টিই deprecated (1.10-এ সরানোর কথা), আর docs বলে C=np.inf ব্যবহার করতে। ডেস্কটপে চার রকম লিখে দেখা হয়েছে (সত্যিকারের run, scikit-learn 1.8.0):

~/bazarghar-ai$ python param_check.py
scikit-learn 1.8.0
{'penalty': None}  coef [[ 1.747 -0.114]]  FutureWarning: 'penalty' was deprecated in version 1.8 and will be removed  ...
{'C': inf}         coef [[ 1.747 -0.114]]  UserWarning: Setting penalty=None will ignore the C and l1_ratio paramete ...
{'C': 10000000000.0} coef [[ 1.747 -0.114]]  no warning
{}                 coef [[ 1.644 -0.107]]  no warning

প্রথম তিনটির ওজন হুবহু এক, default-এরটি ছোট (১.৬৪৪ বনাম ১.৭৪৭)। মজার ব্যাপার, 1.8.0-এ docs-এর পরামর্শ C=np.inf নিজেই একটি বিভ্রান্তিকর UserWarning দেয়; এটি এই version-এর একটি খুঁত বলে মনে হয়, পরের version-এ বদলাতে পারে। তাই এই পাতায় C=1e10. শিক্ষা: parameter আর default নিজের version-এর docs-এ দেখুন, warning পড়ুন। (1.8-এ multi_class parameter-ও আর নেই, যা অনেক পুরোনো কোডে দেখবেন।)

৬ · Decision boundary আর ওজনের মানে — odds ratio

Threshold ০.৫ হলে “ফেরত বলব” মানে σ(z) ≥ ০.৫, অর্থাৎ z ≥ ০. তাই দুই শ্রেণির সীমানা হলো w·x + b = ০. দুই feature-এ এটি একটি সরল রেখা, তিনটিতে একটি সমতল, বেশি মাত্রায় একটি hyperplane. Model “linear” এই অর্থেই: সম্ভাবনা S-আকারে বাঁকে, কিন্তু সমান-সম্ভাবনার সব রেখা সমান্তরাল সরল রেখা, কারণ σ(z) = স্থির মানে z = স্থির। সীমানা বাঁকানো চাইলে feature-ই বদলাতে হয় (যেমন x2 বা x1x2 যোগ), যা পর্ব ৬১ (Overfitting, bias আর variance)-এর polynomial feature-এর ধারণা।

নিচের ব্লক দুটি কাজ করে। প্রথমে অংশ ৫-এর পাঁচ-feature model-এর প্রতিটি ওজনকে ew করে odds ratio দেখায়। তারপর শুধু ছাড় আর দিন নিয়ে একটি দুই-feature model fit করে ছবিতে সম্ভাবনার contour (সমান-সম্ভাবনার রেখা) আর ২০০টি test অর্ডার আঁকে: লাল বিন্দু ফেরত এসেছে, নীল আসেনি (দিনের মান একটু ছড়ানো, যাতে বিন্দু একে অপরকে না ঢাকে)।

names = ["discount (1%)", "days (1 day)", "first_time", "clothing", "electronics"]
for n, c in zip(names, sk.coef_[0]):
    print(f"{n:<14} w = {c:+.3f}   odds ratio e^w = {np.exp(c):.3f}")
print("১০% বেশি ছাড়ে odds গুণ হয়:", np.exp(10 * sk.coef_[0][0]).round(3))

m2 = LogisticRegression(C=1e10).fit(Xtr[:, :2], ytr)     # শুধু ছাড় আর দিন
(w1, w2), b2 = m2.coef_[0], m2.intercept_[0]
print(f"2-feature model: z = {b2:.3f} + {w1:.4f}·discount + {w2:.3f}·days")
print("৫০% ছাড়ে boundary-র দিন:", round(-(b2 + w1 * 50) / w2, 2))

g1, g2 = np.meshgrid(np.linspace(0, 50, 101), np.linspace(0.5, 7.5, 101))
P = m2.predict_proba(np.column_stack([g1.ravel(), g2.ravel()]))[:, 1].reshape(g1.shape)
fig, ax = plt.subplots(figsize=(6.4, 4.2), layout="constrained")
cs = ax.contourf(g1, g2, P, levels=np.linspace(0, 1, 11), cmap="RdBu_r", alpha=0.75)
ax.contour(g1, g2, P, levels=[0.15, 0.5], colors="black", linewidths=[1, 2], linestyles=["--", "-"])
jit = np.random.default_rng(1).uniform(-0.25, 0.25, len(Xte))
ax.scatter(Xte[:, 0], Xte[:, 1] + jit, c=yte, cmap="bwr", edgecolors="k", s=18)
fig.colorbar(cs, label="P(return)")
ax.set(xlabel="discount (%)", ylabel="delivery days",
       title="Test orders: P(return), solid = 0.5, dashed = 0.15")
plt.show()

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

discount (1%)  w = +0.043   odds ratio e^w = 1.044
days (1 day)   w = +0.613   odds ratio e^w = 1.846
first_time     w = +0.597   odds ratio e^w = 1.817
clothing       w = +1.493   odds ratio e^w = 4.453
electronics    w = +0.309   odds ratio e^w = 1.362
১০% বেশি ছাড়ে odds গুণ হয়: 1.544
2-feature model: z = -5.039 + 0.0395·discount + 0.566·days
৫০% ছাড়ে boundary-র দিন: 5.41

ওজন কীভাবে পড়বেন: z যেহেতু log-odds, একটি feature এক একক বাড়লে (বাকি সব একই রেখে) log-odds বাড়ে w, অর্থাৎ odds গুণ হয় ew. তাই প্রতিটি বাড়তি ডেলিভারির দিন ফেরতের odds-কে প্রায় ১.৮৫ গুণ করে; দুই দিন বেশি মানে ১.৮৫২ ≈ ৩.৪১ গুণ। মুদির তুলনায় পোশাকে odds প্রায় ৪.৪৫ গুণ, নতুন গ্রাহকে ১.৮২ গুণ। ছাড়ের প্রতি শতাংশ বিন্দুতে মাত্র ১.০৪৪, কিন্তু ১০ বিন্দুতে ১.০৪৪১০ ≈ ১.৫৪৪ গুণ। ওজনের আকার তুলনা করতে হলে একক মনে রাখুন: ছাড়ের ওজন ছোট দেখায় কারণ তার একক ছোট।

ছবিতে মোটা কালো রেখা ০.৫-এর boundary (৫০% ছাড়ে ৫.৪১ দিনের ওপরে), ড্যাশ রেখা ০.১৫-এর (অংশ ৭)। সব contour সমান্তরাল সরল রেখা, আর লাল ও নীল বিন্দু অনেক জায়গায় মেশানো: এই দুটি feature দিয়ে ফেরত নিখুঁতভাবে আলাদা করা যায় না, তাই সম্ভাবনা দরকার। (দুই-feature model-এ নতুন গ্রাহক আর পণ্যের ধরন বাদ, তাই এর ওজন পাঁচ-feature model থেকে একটু আলাদা।)

Odds ratio সম্ভাবনার গুণ নয়: “১.৮৫ গুণ” odds-এর কথা। একটি অর্ডারের p = ০.২ হলে odds ০.২৫; এক দিন বেশিতে odds ০.২৫ × ১.৮৪৬ ≈ ০.৪৬২, অর্থাৎ p ≈ ০.৩১৬, ০.২-এর ১.৮৫ গুণ (০.৩৭) নয়। আর এই ওজন একটি সম্পর্ক বলে, কারণ নয়: আমাদের বানানো ডেটায় দেরি সত্যিই ফেরত বাড়ায়, কারণ আমরা তা লিখেছি; বাস্তবে দেরির পেছনে দূরের জেলা বা বড় পণ্যের মতো অন্য কারণ লুকিয়ে থাকতে পারে (ভাবনার প্রশ্ন ৩)।

৭ · Threshold — ০.৫ পবিত্র নয়, খরচ দেখে বাছুন

Model দেয় একটি সম্ভাবনা; “ফোন করব কি না” সিদ্ধান্তটি আমাদের। ০.৫ threshold তখনই স্বাভাবিক, যখন দুই রকম ভুলের দাম সমান। এখানে তা নয়। দুটি কাল্পনিক, সরল করা খরচ ধরি:

  • ফেরত ধরতে না পারা (false negative, FN): ফোন করা হয়নি, অর্ডারটি ফেরত এল; দুই দিকের কুরিয়ার আর প্যাকিংয়ে ৳৪০০.
  • অকারণ সতর্কতা (false positive, FP): ফোন করা হলো, অথচ অর্ডারটি এমনিতেই রাখা হতো; কর্মীর সময় আর একটু বিরক্ত গ্রাহক, ধরি ৳৫০.

সরল রাখতে ধরে নিচ্ছি ফোন করলে ফেরত আটকে যায়, আর ঠিক সিদ্ধান্তের কোনো খরচ নেই। একটি অর্ডারের ফেরতের সম্ভাবনা p হলে, ফোন না করার প্রত্যাশিত খরচ p × ৪০০, ফোন করার (1 − p) × ৫০. ফোন করা লাভজনক যখন p × ৪০০ > (1 − p) × ৫০, অর্থাৎ

p > t* = CFP / (CFP + CFN) = 50 / 450 ≈ 0.111

এটি পর্ব ৫৪-এর expected utility-র নিয়মই, শুধু লাভের বদলে খরচ। একটি শর্ত আছে: সূত্রটি ধরে নেয় model-এর সম্ভাবনা সৎ, অর্থাৎ calibrated (অংশ ৯)। Threshold বদলালে boundary কীভাবে সরে, তা দুই-feature model-এ দেখুন; সীমারেখার সমীকরণ w·x + b = ln(t/(1 − t)), তাই t বদলালে রেখা সমান্তরালে সরে।

Boundary for thresholds 0.7, 0.5, 0.3, 0.15 (2-feature model) 01020304050 1357 ছাড় (%) · discount দিন · days t = 0.7 · ফোন: 0টি t = 0.5 · ফোন: 14টি t = 0.3 · ফোন: 42টি t = 0.15 · ফোন: 93টি
  1. t = ০.৭. ln(০.৭/০.৩) = +০.৮৪৭, তাই রেখা z = ০.৮৪৭: শুধু খুব বেশি ছাড় আর প্রায় ৭ দিনের কোণটি লাল। ২০০টি test অর্ডারের একটিও এর মধ্যে পড়ে না; ৩৭টি ফেরতের সবগুলো ছাড়া পায়।
  2. t = ০.৫. রেখা z = ০: ৫০% ছাড়ে ৫.৪১ দিন, ২০% ছাড়ে ৭.৫ দিন। ফোন ১৪টি অর্ডারে: ৮টি সত্যিই ফেরত (TP), ৬টি অকারণ (FP); ২৯টি ফেরত ধরা পড়েনি (FN).
  3. t = ০.৩. ln(০.৩/০.৭) = −০.৮৪৭: রেখা সমান্তরালে নামল, ৫০% ছাড়ে ৩.৯১ দিন। ফোন ৪২টি: TP ১৮, FP ২৪, FN ১৯.
  4. t = ০.১৫. ln(০.১৫/০.৮৫) = −১.৭৩৫: ৫০% ছাড়ে ২.৩৫ দিন, ছাড় ছাড়া ৫.৮৩ দিন। ফোন ৯৩টি: TP ৩১, FP ৬২, FN মাত্র ৬. ঢাল কখনো বদলায়নি; বদলেছে শুধু অবস্থান। কম threshold মানে বেশি ফেরত ধরা, কিন্তু বেশি অকারণ ফোন।

এই বিনিময়ের নিয়মিত ভাষা precision (যতগুলো ফোন, তার কত ভাগ সত্যিই ফেরত) আর recall (যত ফেরত, তার কত ভাগ ধরা পড়ল)। Threshold কমালে সাধারণত recall বাড়ে আর precision কমে। পূর্ণ আলোচনা, ROC আর PR curve পর্ব ৬০ (মূল্যায়ন — accuracy, precision, recall, F1 আর ROC)-এর বিষয়; এখানে শুধু মনে রাখুন, সঠিক threshold ডেটা থেকে নয়, ভুলের দাম থেকে আসে।

৮ · দুইয়ের বেশি শ্রেণি — softmax, আর কোডে threshold ও iris

সব প্রশ্ন হ্যাঁ/না নয়। বাজারঘর হয়তো জানতে চায় একটি ফেরতের কারণ “মাপ”, “দেরি” না “নষ্ট পণ্য”: এটি multiclass classification. Logistic regression-এর স্বাভাবিক সম্প্রসারণ হলো প্রতিটি শ্রেণি k-এর জন্য আলাদা ওজন wk আর bk, আলাদা স্কোর zk = wk·x + bk, তারপর softmax:

P(y = k | x) = ezk / ∑j ezj

e-এর ঘাত সব স্কোরকে ধনাত্মক করে, আর যোগফল দিয়ে ভাগ করলে সম্ভাবনাগুলোর যোগ ১. Loss হলো multinomial cross-entropy: প্রতিটি উদাহরণে −ln P(আসল শ্রেণি)-এর গড়, পর্ব ৩৮-এর cross-entropy-ই। দুই শ্রেণিতে softmax হুবহু sigmoid: ez1/(ez0 + ez1) = σ(z1 − z0). একই overflow-এর ভয়, তাই হিসাবের আগে সব স্কোর থেকে সবচেয়ে বড়টি বিয়োগ করা হয়; উত্তর বদলায় না, কারণ লব আর হর একই e−m-এ গুণ হয়।

নিচের ব্লক দুটি কাজ করে। প্রথমে অংশ ৫-এর পাঁচ-feature model দিয়ে ২০০টি test অর্ডারে কয়েকটি threshold-এর FP, FN আর মোট খরচ হিসাব করে। তারপর scikit-learn-এর সঙ্গে আসা load_iris ডেটা (R. A. Fisher-এর ১৯৩৬ সালের প্রবন্ধে ব্যবহৃত, Edgar Anderson-এর মাপা ১৫০টি iris ফুল; চারটি মাপ, তিনটি প্রজাতি) দিয়ে একটি তিন-শ্রেণির model চালায় আর নিজের লেখা softmax-কে predict_proba-র সঙ্গে মেলায়।

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

C_FN, C_FP = 400, 50          # কাল্পনিক টাকা: ধরা-না-পড়া ফেরত, অকারণ ফোন
p = sk.predict_proba(Xte)[:, 1]
print(" t    FP  FN  খরচ(৳)")
best = None
for t in [0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6]:
    flag = p >= t
    fp, fn = np.sum(flag & (yte == 0)), np.sum(~flag & (yte == 1))
    cost = C_FN * fn + C_FP * fp
    print(f"{t:4.2f} {fp:4d} {fn:3d} {cost:7d}")
    if best is None or cost < best[1]: best = (t, int(cost))
print("সবচেয়ে সস্তা t:", best, "| তত্ত্ব:", round(C_FP / (C_FP + C_FN), 3))

Xi, yi = load_iris(return_X_y=True)
Xa, Xb, ya, yb = train_test_split(Xi, yi, test_size=0.3, stratify=yi, random_state=42)
m3 = LogisticRegression(max_iter=1000).fit(Xa, ya)      # ৩ শ্রেণি: softmax
Z = m3.decision_function(Xb[:3])                         # প্রতি শ্রেণির score
E = np.exp(Z - Z.max(axis=1, keepdims=True))             # stable softmax
print("score:\n", Z.round(2))
print("নিজের softmax:\n", (E / E.sum(axis=1, keepdims=True)).round(3))
print("predict_proba:\n", m3.predict_proba(Xb[:3]).round(3))
print("test accuracy:", round(m3.score(Xb, yb), 3), "| coef_ shape:", m3.coef_.shape)

আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):

 t    FP  FN  খরচ(৳)
0.05  105   1    5650
0.10   76   6    6200
0.15   54   7    5500
0.20   41  10    6050
0.30   23  13    6350
0.40   14  19    8300
0.50    8  23    9600
0.60    6  27   11100
সবচেয়ে সস্তা t: (0.15, 5500) | তত্ত্ব: 0.111
score:
 [[-7.63  2.12  5.51]
 [-3.02  2.11  0.91]
 [-4.2   2.11  2.09]]
নিজের softmax:
 [[0.    0.032 0.968]
 [0.005 0.764 0.232]
 [0.001 0.506 0.493]]
predict_proba:
 [[0.    0.032 0.968]
 [0.005 0.764 0.232]
 [0.001 0.506 0.493]]
test accuracy: 0.933 | coef_ shape: (3, 4)

০.৫ threshold-এ খরচ ৳৯৬০০: মাত্র ৮টি অকারণ ফোন, কিন্তু ৩৭টি ফেরতের ২৩টি ছাড়া পেয়েছে। ০.১৫-এ খরচ ৳৫৫০০, প্রায় অর্ধেক। তত্ত্বের ০.১১১-এর কাছের ০.১০ আর ০.১৫ দুটিই ভালো; ২০০টি অর্ডারের ছোট test set-এ সবচেয়ে সস্তা বিন্দু একটু নড়াচড়া করে, তাই ০.১৫-কে “নিখুঁত” ভাববেন না। আরেকটি সৎ সতর্কতা: এখানে threshold বাছা হয়েছে test set-এর ওপরেই। সঠিক পথে threshold বাছতে হয় আলাদা validation ডেটায়, তারপর test-এ একবার মাপতে হয় (পর্ব ৫৬ আর পর্ব ৬৬)। ০.৫-এ precision ১৪/২২ ≈ ০.৬৩৬, recall ১৪/৩৭ ≈ ০.৩৭৮; ০.১৫-এ precision ৩০/৮৪ ≈ ০.৩৫৭, recall ৩০/৩৭ ≈ ০.৮১১.

Iris-এ প্রতিটি ফুলের তিনটি স্কোর; প্রথম ফুলে তৃতীয় প্রজাতির স্কোর ৫.৫১ সবচেয়ে বড়, softmax-এ ০.৯৬৮. নিজের লেখা softmax আর predict_proba হুবহু এক, অর্থাৎ scikit-learn 1.8 তিন শ্রেণিতে সত্যিই একটি multinomial (softmax) model চালায়। তৃতীয় ফুলটি প্রায় সমান দুই ভাগে (০.৫০৬ বনাম ০.৪৯৩); শুধু শ্রেণি বললে এই অনিশ্চয়তা হারিয়ে যেত। coef_-এর আকার (৩, ৪): প্রতি শ্রেণিতে চারটি মাপের একটি করে ওজন-সারি। ৪৫টি test ফুলের ৪২টি ঠিক (০.৯৩৩); ছোট, সহজ ডেটা, তাই এ সংখ্যা অন্য সমস্যায় খাটে না। (এখানে default C = ১, অর্থাৎ penalty চালু; শুধু শ্রেণি ভাগ দেখতে তা সমস্যা নয়।)

৯ · পুরো আলাদা ডেটা, regularization আর calibration

যখন একটি রেখা সব আলাদা করে ফেলে

ধরুন একটি ছোট শাখায় ৮টি অর্ডার, আর কাকতালীয়ভাবে ৫ দিন বা তার বেশি লাগা প্রতিটি অর্ডার ফেরত এসেছে, কম লাগা একটিও না। ডেটা linearly separable; এই অবস্থার নাম perfect separation. তখন log loss-এর কোনো সর্বনিম্ন বিন্দুই নেই: w যত বড় করবেন, sigmoid তত খাড়া সিঁড়ি হবে, প্রতিটি অর্ডারের সম্ভাবনা তত ০ বা ১-এর কাছে যাবে, আর loss তত কমবে, কিন্তু কখনো শূন্য হবে না। Gradient descent তাই ওজন অনন্তকাল বাড়াতে থাকে। ডেস্কটপে (সত্যিকারের run, NumPy 2.5.4, scikit-learn 1.8.0):

~/bazarghar-ai$ python separable.py
step    100  w =   3.295  loss = 4.59e-02
step   1000  w =   6.939  loss = 7.67e-03
step  10000  w =  11.481  loss = 8.02e-04
step 100000  w =  16.092  loss = 8.01e-05
C = 1e+10  w =  17.094  P(return | 4 days) = 0.000  P(return | 5 days) = 1.000
C = 100    w =   5.645  P(return | 4 days) = 0.056  P(return | 5 days) = 0.944
C = 1      w =   1.195  P(return | 4 days) = 0.364  P(return | 5 days) = 0.654
C = 0.1    w =   0.414  P(return | 4 days) = 0.459  P(return | 5 days) = 0.562

ধাপ দশ গুণ করলে w প্রতিবার আরও ৩.৬ থেকে ৪.৬ বাড়ে, আর loss প্রায় ছয় থেকে দশ ভাগের এক ভাগে নামে: কোনো থামার জায়গা নেই। Penalty-হীন scikit-learn (C = ১০১০) থেমেছে কেবল তার নিজের থামার নিয়মে, w ≈ ১৭-এ, আর বলছে ৪ দিনে ফেরতের সম্ভাবনা ০.০০০, ৫ দিনে ১.০০০. মাত্র ৮টি অর্ডার দেখে এমন নিশ্চয়তা অযৌক্তিক; এটি overfitting-এর একটি চরম রূপ।

প্রতিকার regularization: loss-এর সঙ্গে ওজনের আকারের শাস্তি যোগ করা। scikit-learn-এর L2 রূপটি C · ∑ (প্রতিটি অর্ডারের log loss) + ½‖w‖² কমায়, তাই ছোট C মানে কড়া শাস্তি। C = ১০০-এ w = ৫.৬৪৫ আর সম্ভাবনা ০.০৫৬ ও ০.৯৪৪; C = ১-এ ০.৩৬৪ ও ০.৬৫৪, অনেক বিনয়ী। পর্ব ৫০-এর ভাষায়, L2 penalty মানে ওজনের ওপর শূন্যকে কেন্দ্র করা একটি normal prior, আর penalty-সহ fit হলো MAP. কোন C ঠিক, তা ডেটা থেকে cross-validation দিয়ে বাছতে হয়; L1 penalty, early stopping আর bias–variance-এর পুরো আলোচনা পর্ব ৬১-এ। অনেক feature আর অল্প উদাহরণে (যেমন বহু শব্দের text) এটি খুব সাধারণ।

Calibration — ০.৮ বললে কি ৮০% সত্যি?

অংশ ৭-এর খরচের সূত্র ধরে নিয়েছিল সম্ভাবনাগুলো সৎ। যাচাইয়ের পথ reliability diagram: test-এর অনুমানগুলো কয়েকটি ঝুড়িতে ভাগ করে প্রতিটি ঝুড়ির গড় অনুমান আর বাস্তবে ফেরতের হার পাশাপাশি রাখা। scikit-learn-এ sklearn.calibration.calibration_curve. ২০০টি test অর্ডার এর জন্য খুব কম, তাই ডেস্কটপে একই নিয়মে ২০,০০০টি নতুন অর্ডার বানিয়ে (seed 7) অর্ধেকে train, অর্ধেকে যাচাই করা হয়েছে, পাঁচটি সমান-সংখ্যার ঝুড়িতে (সত্যিকারের run, scikit-learn 1.8.0):

~/bazarghar-ai$ python calibration.py
C = 1e+10
  predicted 0.020   actually returned 0.018
  predicted 0.061   actually returned 0.063
  predicted 0.135   actually returned 0.138
  predicted 0.269   actually returned 0.275
  predicted 0.570   actually returned 0.569
C = 0.0001
  predicted 0.090   actually returned 0.042
  predicted 0.141   actually returned 0.115
  predicted 0.196   actually returned 0.168
  predicted 0.264   actually returned 0.256
  predicted 0.380   actually returned 0.482

Penalty-হীন model প্রায় নিখুঁত calibrated: ০.১৩৫ বলা অর্ডারগুলোর ১৩.৮% সত্যিই ফেরত। কিন্তু এটি প্রত্যাশিতই, কারণ ডেটা ঠিক একটি logistic নিয়মে বানানো, model-এর অনুমান পুরো সত্য। খুব কড়া penalty-তে (C = ০.০০০১) সম্ভাবনাগুলো গড়ের দিকে চেপে যায়: কম-ঝুঁকির ঝুড়িতে বলে ০.০৯০, আসলে ০.০৪২; বেশি-ঝুঁকিরটিতে বলে ০.৩৮০, আসলে ০.৪৮২. বাস্তব ডেটায় logistic regression প্রায়ই মোটামুটি ভালো calibrated বলে পরিচিত (log loss নিজেই সৎ সম্ভাবনাকে পুরস্কার দেয়), কিন্তু model-এর রূপ ভুল হলে, শ্রেণির অনুপাত বদলালে বা penalty কড়া হলে তা ভাঙে। নিশ্চয়তা নেই; প্রতিবার মেপে দেখুন। Calibration সারানোর পদ্ধতি (Platt scaling, isotonic regression) পরের Machine Learning কোর্সে আসবে।

১০ · AI-এর যুগে — AI assistant-এর লেখা classifier যাচাই

“একটা logistic regression লিখে দাও” বললে AI assistant কয়েক সেকেন্ডে চলনসই কোড দেয়। কিন্তু এই বিষয়ে তার কয়েকটি ভুল খুব নিয়মিত, আর সেগুলো চোখে পড়ে না, কারণ কোড চলে এবং একটি সংখ্যাও ছাপে।

AI যা প্রায়ই করেকীভাবে ধরবেন
পুরোনো parameter দেয়: multi_class="multinomial", penalty="none" বা penalty=Noneনিজের version-এর অফিসিয়াল docs; কোড চালিয়ে সব warning পড়া
Default LogisticRegression()-এর ওজনকে “penalty-হীন” বা “আসল প্রভাব” বলে ব্যাখ্যা করেC-এর মান দেখা; আলাদা C-তে ওজন কতটা বদলায় তা মেপে দেখা
Scaling-এর fit পুরো ডেটায় করে, তারপর ভাগ করে (data leakage)আগে ভাগ, তারপর শুধু train-এ fit; scikit-learn-এর Pipeline (পর্ব ৬৪)
Training set-এ accuracy রিপোর্ট করে, ০.৫ threshold বিনা প্রশ্নে রাখেআলাদা test-এ FP, FN আর খরচ; সবচেয়ে বেশি শ্রেণি বলার baseline-এর সঙ্গে তুলনা
নিজে sigmoid আর loss লিখলে np.log(1 - p) সরাসরি দেয়চরম z (±৪০, ±১০০০) দিয়ে পরীক্ষা; np.logaddexp বা clip
Odds ratio-কে “সম্ভাবনা ১.৮ গুণ” বা কারণের প্রমাণ বলেOdds আর সম্ভাবনা আলাদা করে হিসাব; ডেটা কীভাবে সংগ্রহ হয়েছে তা প্রশ্ন করা
“Model ৯০% নিশ্চিত” বলে predict_proba-কে সত্য ধরে নেয়calibration_curve দিয়ে অনেক উদাহরণে যাচাই

নির্দিষ্ট করে চান, আর পরীক্ষাযোগ্য আউটপুট চান। আসল গ্রাহকের অর্ডার, নাম বা ফোন নম্বর কখনো পেস্ট করবেন না; কয়েকটি কাল্পনিক সারিই যথেষ্ট।

আমার কাছে একটি অর্ডারের টেবিল আছে: কলাম discount (০–৫০), days (১–৭), first_time (০/১), category (grocery/clothing/electronics) আর returned (০/১). Python 3.14, NumPy 2 আর scikit-learn 1.8 দিয়ে কোড লেখো যা: ১. আগে stratify আর random_state=42 দিয়ে train আর test-এ ভাগ করবে, তারপর শুধু train-এ fit হওয়া একটি Pipeline-এ category-কে one-hot (grocery বাদ দিয়ে) আর সংখ্যার কলামকে StandardScaler করবে। ২. NumPy দিয়ে নিজে লেখা logistic regression (np.logaddexp দিয়ে stable log loss, gradient descent) আর LogisticRegression(C=1e10) দুটির ওজন পাশাপাশি ছাপাবে, সঙ্গে central difference দিয়ে gradient check. ৩. প্রতিটি ওজনের odds ratio আসল এককে দেখাবে, আর comment-এ লিখবে এটি কারণ নয়, সম্পর্ক। ৪. FN-এর খরচ ৪০০ আর FP-এর ৫০ ধরে validation ডেটায় threshold বাছবে, তারপর test-এ একবার খরচ, FP আর FN ছাপাবে, সঙ্গে সবসময় “ফেরত নয়” বলার baseline. ৫. calibration_curve দিয়ে একটি reliability check করবে। কোনো deprecated parameter (multi_class, penalty) ব্যবহার করবে না, আর কোথাও বলবে না যে model গ্রাহকের মন বোঝে।

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

পরীক্ষা ও ইন্টারভিউতে:

  • Logistic regression নামে regression, কাজে কী? Classification: log-odds-কে feature-এর রৈখিক যোগফল ধরে সম্ভাবনা অনুমান; “regression” নামটি log-odds-এর ওপর regression থেকে।
  • σ(z) = ০.৮ হলে z কত? ln(০.৮/০.২) = ln ৪ ≈ ১.৩৮৬; আর সেখানে σ′ = ০.৮ × ০.২ = ০.১৬.
  • Loss-এর gradient? ∇w = Xᵀ(σ(Xw + b) − y)/n, ∂b = mean(σ − y); log loss convex, MSE-sigmoid নয়।
  • ওজন ০.৬ মানে? Feature এক একক বাড়লে odds e০.৬ ≈ ১.৮২ গুণ (বাকি সব একই রেখে)।
  • FN-এর খরচ ৪০০, FP-এর ১০০ হলে threshold? ১০০/(১০০ + ৪০০) = ০.২, যদি সম্ভাবনা calibrated হয়।

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

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

প্র ০১যে model শ্রেণি বাছে, তার নামে “regression” কেন? জনসংখ্যার একটি curve কীভাবে আজকের সবচেয়ে পরিচিত classifier-গুলোর একটি হলো?

নামটি ইতিহাসের ছাপ, আর কিছুটা গণিতেরও। Verhulst যখন ১৮৩০ আর ১৮৪০-এর দশকে logistic curve লেখেন, তখন তা ছিল একটি দেশের জনসংখ্যা সময়ের সঙ্গে কীভাবে বেড়ে একটি সীমায় থামে, তার model: শ্রেণি বা সম্ভাবনার কোনো কথা ছিল না। ১৯২০ সালে Raymond Pearl আর Lowell Reed একই curve আবার খুঁজে পান।

সম্ভাবনার দিকে মোড় আসে জীববিজ্ঞানের “dose–response” প্রশ্নে: বিষের মাত্রা বাড়লে কত ভাগ পোকা মরে? ১৯৩০-এর দশকে Chester Bliss-এর probit model ছিল প্রচলিত; Joseph Berkson ১৯৪৪ সালে “logit” নাম দিয়ে যুক্তি দেন, logistic curve প্রায় একই আকারের আর হাতে হিসাবে অনেক সহজ। পরে David Cox-এর ১৯৫৮ সালের কাজ দেখায়, হ্যাঁ/না ফলকে কয়েকটি ব্যাখ্যাকারী চলকের ওপর “regress” করা যায়, ঠিক linear regression-এর মতো, শুধু y-এর বদলে তার log-odds-এর ওপর।

গাণিতিক অর্থে তাই নামটি অন্যায্য নয়: model-টি আসলে একটি সংখ্যা, log-odds বা সম্ভাবনা, অনুমান করে; শ্রেণি আসে পরে, threshold দিয়ে, যা model-এর বাইরের সিদ্ধান্ত। ১৯৭২ সালে John Nelder আর Robert Wedderburn “generalized linear model” কাঠামোয় linear, logistic আর Poisson regression-কে এক পরিবারে আনেন: একটি রৈখিক স্কোর, একটি link function (এখানে logit), আর একটি সম্ভাবনা বণ্টন (এখানে Bernoulli). এই দৃষ্টিতে logistic regression হলো “০/১ ডেটার linear regression”, ঠিকভাবে করা।

মূল উপলব্ধি: logistic regression প্রথমে সম্ভাবনার model, তারপর classifier; সম্ভাবনাটিই আসল পণ্য, আর শ্রেণি হলো সেই সম্ভাবনা থেকে একটি খরচভিত্তিক সিদ্ধান্ত।

প্র ০২অনেকে শুধু accuracy দেখে model বাছেন। বাজারঘরের মতো খরচের সিদ্ধান্তে একটি ভালো calibrated সম্ভাবনা কেন accuracy-র চেয়ে বেশি মূল্যবান হতে পারে?

Accuracy একটি নির্দিষ্ট threshold-এ (সাধারণত ০.৫) ঠিক-ভুল গোনে, আর ধরে নেয় সব ভুলের দাম সমান। বাজারঘরে তা নয়: একটি ফসকে যাওয়া ফেরতের দাম একটি অকারণ ফোনের আট গুণ (কাল্পনিক হিসাবে)। ২০০টি test অর্ডারে ০.৫ threshold-এ মাত্র ৩১টি ভুল (FP ৮ + FN ২৩, accuracy ০.৮৪৫), অথচ খরচ ৳৯৬০০; ০.১৫-এ ভুল ৬১টি (accuracy ০.৬৯৫), কিন্তু খরচ ৳৫৫০০. কম accuracy, কম খরচ। কারণ আমাদের লক্ষ্য কখনো “বেশি ঠিক” ছিল না, ছিল “কম লোকসান”.

এই হিসাব সম্ভব হয় কেবল তখন, যখন model একটি অর্থবহ সম্ভাবনা দেয়। t* = CFP/(CFP + CFN) সূত্রটি তখনই কাজ করে, যখন ০.২ বলা অর্ডারগুলোর সত্যিই প্রায় ২০% ফেরত আসে। খরচ বদলালে নতুন threshold শুধু একটি নতুন ভাগফল; model আবার train করতে হয় না। আবহাওয়া পূর্বাভাসে এই ভাবনা পুরোনো: Glenn Brier ১৯৫০ সালে সম্ভাবনার পূর্বাভাস মাপার একটি score প্রস্তাব করেন, আর আবহাওয়াবিদরা দীর্ঘদিন ধরে পূর্বাভাসের reliability diagram আঁকেন, কারণ কৃষক বা জেলের সিদ্ধান্ত সম্ভাবনার ওপর।

Model-ভেদে calibration আলাদা। ২০০৫ সালে Alexandru Niculescu-Mizil আর Rich Caruana কয়েকটি ডেটাসেটে বিভিন্ন model তুলনা করে দেখান, logistic regression-এর সম্ভাবনা তুলনামূলকভাবে ভালো calibrated থাকে, আর কিছু model (যেমন boosted tree বা Naive Bayes) প্রায়ই চরম বা চাপা সম্ভাবনা দেয়। তবে এটি গড় প্রবণতা: model-এর রূপ ভুল হলে, শ্রেণির অনুপাত বদলালে বা penalty কড়া হলে logistic regression-ও ভুল calibrated হয়, যা অংশ ৯-এ C = ০.০০০১-এ দেখেছেন।

মূল উপলব্ধি: একটি সৎ সম্ভাবনা যেকোনো খরচের কাঠামোর সঙ্গে খাপ খায়, accuracy শুধু একটি; তাই সম্ভাবনা মাপুন, calibration যাচাই করুন, আর threshold বাছুন লোকসান দেখে।

প্র ০৩Model বলছে “প্রতিটি বাড়তি ডেলিভারির দিন ফেরতের odds ১.৮৫ গুণ করে”. এ থেকে কি বলা যায় যে ডেলিভারি এক দিন দ্রুত করলে ফেরত কমবে?

আমাদের বানানো ডেটায় হ্যাঁ, কারণ আমরা নিয়মটি নিজেরাই লিখেছি: দেরি সরাসরি ফেরতের সম্ভাবনা বাড়ায়। বাস্তব ডেটায় সাবধান হওয়ার অন্তত তিনটি কারণ আছে। প্রথমটি confounding: দূরের জেলার অর্ডার বেশি দেরিতে পৌঁছায়, আর সেখানে হয়তো দোকানে গিয়ে মিলিয়ে দেখার সুযোগ কম, তাই ফেরতও বেশি। তখন দেরির ওজনে জেলার প্রভাব মিশে থাকে; ডেলিভারি দ্রুত করলেও ফেরত হয়তো ততটা কমবে না।

দ্বিতীয়টি সম্পর্কিত feature: ভারী পণ্য দেরিতে যায় আর হয়তো বেশি ফেরতও আসে। দুটি feature একসঙ্গে থাকলে ওজনগুলো প্রভাব ভাগাভাগি করে, আর একটি বাদ দিলে অন্যটির ওজন বদলে যায়; দুই-feature model-এ দিনের ওজন ০.৫৬৬, পাঁচ-feature-এ ০.৬১৩, একই ডেটায়। তৃতীয়টি odds ratio-র নিজের ফাঁদ: অনেকে “odds ১.৮৫ গুণ”-কে “ঝুঁকি ১.৮৫ গুণ” বলে পড়েন। ঘটনাটি বিরল হলে (p অনেক ছোট) দুটি কাছাকাছি, কিন্তু p = ০.২-এ এক দিনে সম্ভাবনা ০.২ থেকে ০.৩১৬ হয়, ১.৫৮ গুণ।

কারণ-ফল জানতে সবচেয়ে ভরসার পথ পরীক্ষা: কিছু এলাকায় এলোমেলোভাবে দ্রুত ডেলিভারি চালু করে ফেরতের হার তুলনা (A/B test). না পারলে causal inference-এর পদ্ধতি লাগে, যা এই কোর্সের বাইরে। Logistic regression-এর ওজন তবু দামি: কোন feature-এর সঙ্গে ঝুঁকি জোরালোভাবে জড়িত, তা দেখায়, আর কোথায় পরীক্ষা চালানো উচিত তার ইঙ্গিত দেয়।

মূল উপলব্ধি: ওজন হলো “বাকি সব একই রেখে” odds-এর পরিবর্তন, যেসব feature model-এ আছে শুধু সেগুলো একই রেখে; এটি অনুমানের জন্য চমৎকার, কিন্তু নীতি বদলের সিদ্ধান্তের আগে কারণ যাচাই করতে আলাদা প্রমাণ লাগে।

প্র ০৪Perfect separation-এ ওজন অসীমের দিকে যায়। Model তো তখন training ডেটায় নির্ভুল; তবু এটিকে সমস্যা বলা হয় কেন, আর regularization কি শুধু একটি কৌশল, নাকি এর পেছনে কোনো যুক্তি আছে?

Training-এ নির্ভুল হওয়া আর ভবিষ্যতে ভালো হওয়া আলাদা জিনিস। অংশ ৯-এর ৮টি অর্ডারে penalty-হীন model বলেছে ৪ দিনে ফেরতের সম্ভাবনা ০.০০০, ৫ দিনে ১.০০০. পরের মাসে ৪ দিনে পৌঁছানো একটি অর্ডার ফেরত এলে log loss হবে বিশাল (প্রায় অসীম), কারণ model সেটিকে অসম্ভব বলেছিল। আর আসল সীমানা ৪ আর ৫-এর মাঝে কোথায়, ৮টি অর্ডার তা বলতে পারে না; যেকোনো জায়গায় রাখা রেখা সমান নিখুঁত।

পরিসংখ্যানে এই সমস্যা পুরোনো। A. Albert আর J. A. Anderson ১৯৮৪ সালে দেখান, ডেটা পুরো আলাদা (বা প্রায় পুরো আলাদা, “quasi-separation”) হলে logistic regression-এর maximum likelihood অনুমানের অস্তিত্বই থাকে না। Software তখন বিশাল coefficient ছাপিয়ে প্রায়ই চুপ থাকে। ১৯৯৩ সালে David Firth একটি সংশোধিত likelihood প্রস্তাব করেন, যা আজও বিরল ঘটনার চিকিৎসা-গবেষণায় ব্যবহার হয়।

Regularization কৌশলের চেয়ে বেশি, এর পেছনে সম্ভাবনার যুক্তি আছে। পর্ব ৫০-এর MAP-এ ফিরে যান: ওজনের ওপর শূন্যকে কেন্দ্র করা একটি normal prior রাখলে posterior-এর সর্বোচ্চ বিন্দু খুঁজতে হুবহু log loss + ওজনের বর্গের শাস্তি কমাতে হয়। prior বলে “বিশাল ওজনের প্রমাণ না পেলে ছোট ওজন বিশ্বাস করুন”. ৮টি অর্ডার সেই প্রমাণ নয়, তাই উত্তর বিনয়ী থাকে। ডেটা বাড়লে likelihood prior-কে ছাপিয়ে যায়, আর penalty-র প্রভাব কমে।

মূল উপলব্ধি: নিখুঁতভাবে আলাদা training ডেটা সাফল্য নয়, সতর্কসংকেত; penalty হলো “অল্প প্রমাণে চরম দাবি নয়” নীতির গাণিতিক রূপ, আর তার শক্তি (C) ডেটা দিয়েই বাছতে হয়।

প্র ০৫Deep learning-এর যুগে প্রায় সত্তর বছরের পুরোনো logistic regression কেন এখনো শেখানো আর ব্যবহার করা হয়? বড় neural network-এর সঙ্গে এর সম্পর্ক কী?

প্রথম কারণ, এটি একটি neural network-এর সবচেয়ে ছোট রূপ। একটি “neuron” ওজনযুক্ত যোগফল নিয়ে একটি activation function দিয়ে পাঠায়; activation sigmoid হলে আর loss log loss হলে সেটিই হুবহু logistic regression. Frank Rosenblatt-এর ১৯৫৮ সালের perceptron-ও ওজনযুক্ত যোগফল নিত, তবে তার শেষে ছিল একটি ধাপ (০ বা ১), কোনো সম্ভাবনা নয়, আর শেখার নিয়মও আলাদা। বড় network-এর শেষ স্তর প্রায়ই একটি softmax আর cross-entropy loss: অর্থাৎ শেষ স্তরটি আগের স্তরগুলোর বানানো feature-এর ওপর একটি multinomial logistic regression. বড় ভাষা model-ও train হয় পরের token-এর ওপর softmax আর cross-entropy দিয়ে। তাই এখানে শেখা σ − y gradient, overflow-এর ভয় আর log-sum-exp প্রতিটি deep learning library-র ভেতরে আছে (পর্ব ৬৮)।

দ্বিতীয় কারণ, ব্যাখ্যা আর নিয়ন্ত্রণ। ঋণ-ঝুঁকির scorecard, হাসপাতালের ঝুঁকি-score বা বীমার মতো ক্ষেত্রে সিদ্ধান্তের কারণ ব্যাখ্যা করতে হয়, আর নিয়ন্ত্রক সংস্থা প্রায়ই সরল, পরীক্ষাযোগ্য model পছন্দ করে বলে শোনা যায়; প্রতিটি ওজন একটি odds ratio, যা একজন বিশেষজ্ঞ পড়তে পারেন। তৃতীয় কারণ, এটি একটি শক্ত baseline: সেকেন্ডে train হয়, convex বলে প্রতিবার একই উত্তর দেয়; নতুন জটিল model একে স্পষ্টভাবে না হারালে সেই জটিলতার দাম দেওয়ার মানে নেই।

সীমাও স্পষ্ট: boundary সরল রেখা, feature-এর পারস্পরিক ক্রিয়া নিজে খোঁজে না, আর ছবি বা শব্দের মতো কাঁচা ডেটায় neural network বা tree-ভিত্তিক model সাধারণত এগিয়ে; সৎ পথ একই মূল্যায়নে দুটি চালিয়ে দেখা।

মূল উপলব্ধি: logistic regression পুরোনো হলেও অচল নয়; এটি একটি শক্ত, ব্যাখ্যাযোগ্য baseline, আর আধুনিক network-এর প্রতিটি classifier-এর শেষ ধাপে একই sigmoid/softmax আর cross-entropy কাজ করে।

অনুশীলন

হাতে হিসাব, কোড, সমালোচনা আর আউটপুট অনুমান; প্রতিটির উত্তর নিচে লুকানো আছে।

  1. এক ধাপ gradient descent (হিসাব): তিনটি অর্ডার, একটি feature: x = (1, 2, −1), y = (1, 0, 0). শুরু w = ০.৫, b = ০, learning rate η = ০.১. প্রতিটি অর্ডারের z, σ(z), ভুল e = σ − y, তারপর ∂L/∂w, ∂L/∂b, নতুন w আর b, আর আগে-পরের log loss বের করুন।
    • z = wx + b = (০.৫, ১.০, −০.৫); σ = (০.৬২২৫, ০.৭৩১১, ০.৩৭৭৫).
    • e = σ − y = (−০.৩৭৭৫, ০.৭৩১১, ০.৩৭৭৫).
    • ∂L/∂w = mean(e·x) = (−০.৩৭৭৫ + ১.৪৬২২ − ০.৩৭৭৫)/৩ = ০.৭০৭২/৩ ≈ ০.২৩৫৭; ∂L/∂b = mean(e) = ০.৭৩১১/৩ ≈ ০.২৪৩৭.
    • w ← ০.৫ − ০.১ × ০.২৩৫৭ ≈ ০.৪৭৬৪; b ← ০ − ০.১ × ০.২৪৩৭ ≈ −০.০২৪৪.
    • Log loss: আগে ০.৭৫৩৮, পরে ০.৭৪২৬ (ডেস্কটপে NumPy দিয়ে যাচাই করা)।

    দ্বিতীয় অর্ডারটি (x = ২, y = ০) সবচেয়ে বড় ভুল (০.৭৩১১, ফেরত নয় অথচ ৭৩% বলেছে), আর x বড় বলে তার টানই সবচেয়ে বেশি: w কমেছে। b-ও কমেছে, কারণ গড়ে model বেশি বলছিল। প্রতিটি ভুলের গুণনীয়ক সোজা σ − y; এখানে কোনো σ(1 − σ) নেই, যা log loss-এর সুবিধা।

  2. Gradient-এ L2 penalty যোগ (কোড): অংশ ৫-এর logreg_scratch.py-এ gradient descent বদলে L2 penalty যোগ করুন, যাতে তা scikit-learn-এর LogisticRegression(C=...)-এর সঙ্গে মেলে। ইঙ্গিত: scikit-learn কমায় C · ∑ log loss + ½‖w‖², আর b-তে কোনো penalty নেই। Standardize করা S-এ C = ১ আর C = ০.০১-এ দুটির ওজন তুলনা করুন।

    দুই দিককে C · n দিয়ে ভাগ করলে লক্ষ্য দাঁড়ায় mean log loss + (λ/2)‖w‖², যেখানে λ = 1/(C·n). তাই w-এর gradient-এ শুধু λ·w যোগ:

    n = len(ytr)
    for C in (1.0, 0.01):
        lam = 1 / (C * n)                    # C·Σloss + ½‖w‖²  ⇔  mean loss + (λ/2)‖w‖²
        w, b = np.zeros(5), 0.0
        for _ in range(5000):
            gw, gb = grad(w, b, S, ytr)
            w, b = w - 0.5 * (gw + lam * w), b - 0.5 * gb      # b-তে penalty নেই
        sk2 = LogisticRegression(C=C).fit(S, ytr)
        print(f"C = {C}: scratch {w.round(3)}  sklearn {sk2.coef_[0].round(3)}")

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0):

    C = 1.0: scratch [0.625 1.189 0.267 0.685 0.126]  sklearn [0.625 1.189 0.267 0.686 0.127]
    C = 0.01: scratch [ 0.246  0.455  0.114  0.244 -0.029]  sklearn [ 0.246  0.455  0.114  0.244 -0.029]

    দুটি মেলে (C = ১-এ শেষ অঙ্কে থামার নিয়মের ছোট পার্থক্য)। C = ০.০১-এ সব ওজন অনেক ছোট, ইলেকট্রনিক্সেরটি এমনকি একটু ঋণাত্মক: কড়া penalty দুর্বল প্রমাণের ওজনকে শূন্যের দিকে, এমনকি পেরিয়েও ঠেলে দিতে পারে। খেয়াল করুন, penalty standardize করা feature-এ বসানো হয়েছে; আসল এককে বসালে ছাড়ের মতো ছোট-এককের feature অন্যায্যভাবে কম শাস্তি পেত, তাই penalty দিলে scaling প্রায় বাধ্যতামূলক।

  3. সমালোচনা ও নতুন নকশা: একজন সহকর্মী লিখেছেন: “৫০০টি অর্ডারে LogisticRegression(multi_class='multinomial') চালালাম, কোনো scaling ছাড়া। Training set-এ accuracy ৮৯%. ছাড়ের ওজন সবচেয়ে ছোট (০.০৪), তাই ছাড়ের কোনো প্রভাব নেই; বরং ঈদে ছাড় বাড়িয়ে দিন। আর ০.৫-এর বেশি সম্ভাবনার অর্ডারগুলো বাতিল করে দেওয়া হবে।” কী কী ভুল, আর আপনি কীভাবে নতুন করে সাজাবেন?
    • পুরোনো parameter: scikit-learn 1.8-এ multi_class আর নেই; কোড error দেবে। দুই শ্রেণিতে এর দরকারও নেই।
    • Training accuracy: model যা দেখেছে তাতেই মাপা; আলাদা test, FP/FN, baseline দরকার। ২০% ফেরতের ডেটায় “সবসময় ফেরত নয়” বললেই accuracy ৮০%, তাই ৮৯% খুব বেশি কিছু নয়।
    • ওজনের আকার আর একক: ছাড়ের একক এক শতাংশ বিন্দু; ০.০৪ মানে ১০ বিন্দুতে odds প্রায় e০.৪ ≈ ১.৪৯ গুণ, যা ছোট নয়। তুলনার আগে একক বা standardize করা ওজন দেখতে হয়। আর default C = ১-এ penalty চালু থাকে; scaling ছাড়া সেই penalty-ও feature-এর এককের ওপর নির্ভর করে।
    • সম্পর্ক বনাম কারণ: “ছাড় বাড়ান” একটি নীতির সিদ্ধান্ত; model থেকে তা আসে না, পরীক্ষা লাগে।
    • বাতিল করা আর ০.৫: অর্ডার বাতিল খুব কড়া ব্যবস্থা, তার খরচ (হারানো বিক্রি, অসন্তুষ্ট গ্রাহক) হিসাব করা হয়নি; ০.৫ বিনা কারণে বাছা।

    নতুন নকশা: stratified train/validation/test ভাগ; Pipeline-এ one-hot আর scaling শুধু train-এ fit; penalty-র C validation-এ বাছা; validation-এ খরচ দেখে threshold, যেমন FN ৪০০ আর FP ৫০ হলে প্রায় ০.১১, তারপর calibration যাচাই; ব্যবস্থা বাতিল নয়, ফোন করে নিশ্চিত করা; ওজন odds ratio হিসেবে এককসহ রিপোর্ট; আর ছাড়ের প্রভাব জানতে আলাদা A/B পরীক্ষা।

  4. আউটপুট অনুমান: নিচের কোড ডেস্কটপের Python-এ কী ছাপবে? শেষ লাইনের দুটি সংখ্যার কোনটি নিরাপদ, আর কেন?
    import numpy as np
    w, b = np.array([0.5, -1.0]), 0.25
    x = np.array([2.0, 1.0])
    z = w @ x + b
    p = 1 / (1 + np.exp(-z))
    print(z, round(p, 3), round(np.exp(w[0]), 3))
    print((np.array([0.2, 0.6, 0.45]) >= 0.5).astype(int))
    print(np.logaddexp(0, 800.0), 1 / (1 + np.exp(800.0)))

    সত্যিকারের run (ডেস্কটপ, Python 3.14.4, NumPy 2.5.4):

    predict.py:8: RuntimeWarning: overflow encountered in exp
      print(np.logaddexp(0, 800.0), 1 / (1 + np.exp(800.0)))
    0.25 0.562 1.649
    [0 1 0]
    800.0 0.0

    z = ০.৫ × ২ + (−১) × ১ + ০.২৫ = ০.২৫; σ(০.২৫) ≈ ০.৫৬২; প্রথম feature-এর odds ratio e০.৫ ≈ ১.৬৪৯. দ্বিতীয় লাইন ০.৫ threshold: শুধু ০.৬ সীমা পেরোয়। তৃতীয় লাইনে np.logaddexp(0, 800) নিরাপদে ৮০০.০ দেয় (ln(1 + e৮০০) ≈ ৮০০), কিন্তু np.exp(800.0) overflow করে inf, তাই 1/(1 + inf) = 0.0, সঙ্গে RuntimeWarning (stderr-এ, তাই সাধারণত আউটপুটের আগে দেখা যায়)। এখানে উত্তর 0.0 মোটামুটি ঠিক, কিন্তু একই inf একটি log বা বিয়োগে ঢুকলে nan হতে পারে। ব্রাউজারের Python-এ একই কোড warning ছাড়াই চলে।

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

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

ক্রমNumPy দিয়ে নিজের হাতে logistic regression train আর যাচাইয়ের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Seed দিয়ে ডেটা train আর test-এ ভাগ করা
  2. শুধু train-এর গড় আর standard deviation দিয়ে feature standardize করা
  3. ওজন শূন্যে শুরু করে z = Xw + b আর stable sigmoid হিসাব করা
  4. Gradient Xᵀ(σ − y)/n বের করে gradient checking দিয়ে যাচাই করা
  5. w ← w − η·gradient বারবার করে loss স্থির না হওয়া পর্যন্ত চালানো
  6. Test-এ খরচ দেখে threshold-এর ফল মাপা আর scikit-learn-এর সঙ্গে ওজন মেলানো

একটি logistic regression model একটি অর্ডারের জন্য σ(z) = ০.৮ দিয়েছে। z (log-odds) কত, আর সেখানে sigmoid-এর derivative কত?

  • z = ০.৮, derivative ০.২
  • z = ৪, derivative ০.১৬
  • z ≈ ১.৩৮৬, derivative ০.১৬
  • z ≈ ১.৩৮৬, derivative ০.২৫

একটি ফেরত-অনুমানের model-এ “ডেলিভারির দিন” feature-এর ওজন w = ০.৬ (বাকি feature একই রেখে)। এর সঠিক ব্যাখ্যা কোনটি?

  • প্রতি বাড়তি দিনে ফেরতের সম্ভাবনা ০.৬ বাড়ে
  • প্রতি বাড়তি দিনে ফেরতের odds প্রায় ১.৮২ গুণ হয়
  • প্রতি বাড়তি দিনে ফেরতের সম্ভাবনা ১.৮২ গুণ হয়
  • ডেলিভারি এক দিন কমালে ফেরত নিশ্চিতভাবে কমবে

Logistic regression-এর loss হিসেবে mean squared error, অর্থাৎ (σ(z) − y)²-এর গড়, সাধারণত ব্যবহার করা হয় না কেন?

  • কারণ sigmoid-এর ওপর MSE হিসাব করা যায় না
  • কারণ MSE ঋণাত্মক হতে পারে
  • কারণ MSE-এর gradient সবসময় খুব বড় হয়ে overflow করে
  • কারণ ওজনের সাপেক্ষে এটি convex নয়, আর আত্মবিশ্বাসী ভুলে gradient প্রায় শূন্য হয়ে যায়

একটি দোকানে ফেরত ধরতে না পারার (false negative) খরচ ৳৪০০, আর অকারণ সতর্কতার (false positive) খরচ ৳১০০. Model-এর সম্ভাবনা calibrated হলে প্রত্যাশিত খরচ সবচেয়ে কম রাখতে কোন threshold-এর ওপরে সতর্ক করা উচিত?

  • ০.২
  • ০.৫
  • ০.২৫
  • ০.৮

একটি ছোট ডেটায় দুই শ্রেণিকে একটি সরল রেখা পুরোপুরি আলাদা করতে পারে। penalty ছাড়া logistic regression-কে gradient descent দিয়ে train করলে কী ঘটে, আর সাধারণ প্রতিকার কী?

  • ওজন দ্রুত একটি সসীম মানে স্থির হয়; কোনো প্রতিকার লাগে না
  • ওজন সীমাহীনভাবে বাড়তে থাকে; regularization (scikit-learn-এ ছোট C) দিয়ে সংযত করা হয়
  • ওজন শূন্যে নেমে যায়; C বাড়িয়ে সারানো হয়
  • শুধু learning rate কমালেই ওজন একটি সসীম মানে থামে

একটি তিন-শ্রেণির model একটি ফুলের জন্য score দিয়েছে z = (2, 0, 0). Softmax-এ প্রথম শ্রেণির সম্ভাবনা প্রায় কত?

  • ১.০
  • ০.৩৩৩
  • ০.৭৮৭
  • ০.৮৮১

শব্দকোষ

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

মনে রাখুন

  1. Model: log-odds = w·x + b, তাই P(y = 1 | x) = σ(w·x + b), σ(z) = 1/(1 + e−z), σ′ = σ(1 − σ). Linear regression হ্যাঁ/না-তে ০–১-এর বাইরে যায় (৬০০টির ৮২টি ঋণাত্মক)।
  2. Loss আর gradient: log loss = NLL = maximum likelihood; convex; ∇w = Xᵀ(σ − y)/n. MSE-sigmoid convex নয়, আত্মবিশ্বাসী ভুলে gradient প্রায় শূন্য।
  3. Stable কোড: np.logaddexp(0, z) − y·z দিয়ে loss, gradient checking, standardize; scikit-learn-এর default-এ L2 penalty (C = ১), তুলনায় C বড় রাখুন, আর 1.8-এ penalty deprecated.
  4. ব্যাখ্যা: boundary w·x + b = ln(t/(1 − t)), একটি সরল রেখা; ew হলো odds ratio (এক দিনে ১.৮৫ গুণ), সম্ভাবনার গুণ বা কারণ নয়।
  5. সিদ্ধান্ত: threshold = CFP/(CFP + CFN), calibrated হলে; perfect separation-এ ওজন বিস্ফোরিত হয়, regularization সারায়; softmax দিয়ে অনেক শ্রেণি, দুই শ্রেণিতে যা sigmoid.

পরের পর্বে: এই পর্বে threshold বদলে FP আর FN-এর বিনিময় দেখেছেন। পর্ব ৬০ (মূল্যায়ন — accuracy, precision, recall, F1 আর ROC)-এ সেই বিনিময়ের পুরো ভাষা: confusion matrix, অসম শ্রেণিতে accuracy-র ব্যর্থতা, precision, recall, F1, ROC আর AUC, PR curve, আর খরচ দেখে metric বাছা।

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

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