এই পর্বে যা শিখবেন
- Confusion matrix হাতে আর
confusion_matrixদিয়ে, আর accuracy paradox ধরতে baseline-এর অভ্যাস - Precision, recall, specificity, false positive rate, F1 আর Fβ, দোকানের ভাষায় আর পরীক্ষার হিসাবে
- Threshold সরিয়ে precision আর recall-এর বিনিময়, আর টাকার হিসাবে expected cost দিয়ে threshold বাছা
- ROC curve ও AUC ধাপে ধাপে, precision-recall curve আর average precision, আর কখন কোনটি সৎ ছবি দেয়
- Multiclass-এ macro, micro, weighted; regression-এ MAE, RMSE, R², MAPE; আর bootstrap দিয়ে metric-এর অনিশ্চয়তা
আগের পর্বগুলোতে এই প্রশ্নের টুকরো দেখেছেন। পর্ব ৩৫-এ দুর্লভ জালিয়াতির উদাহরণে base rate কীভাবে একটি “ভালো” test-কেও বিভ্রান্তিকর করে, আর পর্ব ৫০-এ sensitivity, specificity আর Bayes-এর হালনাগাদ। পর্ব ৫১-এ বাংলা SMS-এর spam filter-এ confusion matrix প্রথম কাজে লেগেছিল, আর দেখেছিলেন আসল বার্তাকে spam বলা (false positive) আর spam ছেড়ে দেওয়ার (false negative) দাম এক নয়। Model হিসেবে নেব logistic regression, যার ভেতরের কাজ পর্ব ৫৯ (Logistic regression — সম্ভাবনা, sigmoid আর decision boundary)-এ; এখানে তাকে শুধু “প্রতিটি অর্ডারের জন্য ০ থেকে ১-এর একটি score দেয়” এমন একটি যন্ত্র ধরলেই চলবে। চালানো যায় এমন ব্লকগুলো পাতাতেই চলে (ব্রাউজারের Python, NumPy 2.4.6, scikit-learn 1.8.0, matplotlib 3.10.8). scikit-learn প্রথমবার নামতে প্রায় ১০ সেকেন্ড লাগে; পরের run দ্রুত।
১ · একটি সংখ্যা কেন মিথ্যা বলতে পারে — বাজারঘরের ভুয়া অর্ডার
বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান) cash on delivery-তে অনেক অর্ডার পায়। এর একটি ছোট অংশ ভুয়া: ভুল ঠিকানা, ঠাট্টা করে দেওয়া অর্ডার, বা একই ফোন থেকে রাতে একের পর এক দামি অর্ডার যা কেউ গ্রহণ করে না। দোকান চায় একটি model, যে প্রতিটি অর্ডারকে score দেবে আর সন্দেহজনকগুলোকে পাঠানোর আগে ফোনে যাচাইয়ের জন্য আটকাবে। এটি একটি binary classification: label ১ মানে ভুয়া, ০ মানে সৎ।
হাটের গেটের একজন পাহারাদারের কথা ভাবুন, যাঁর কাজ পকেটমার ঢুকতে না দেওয়া। দিনে ১০০০ জন ঢোকেন, তার মধ্যে পকেটমার বড়জোর ২০ জন। পাহারাদার যদি চোখ বন্ধ করে সবাইকে ঢুকতে দেন, তবু তিনি “৯৮% মানুষের ব্যাপারে ঠিক”। আবার সবাইকে থামালে সব পকেটমার ধরা পড়বে, কিন্তু হাটে আর কেউ আসবেন না। তাই দুটি প্রশ্ন লাগে: কত পকেটমার ধরলেন, আর যাঁদের থামালেন তাঁদের কতজন সত্যিই পকেটমার।
আসল গ্রাহকের ডেটা আমরা ব্যবহার করব না। তাই seed দেওয়া NumPy দিয়ে ৬০০০টি অর্ডার বানানো হয়েছে, প্রতিটিতে চারটি feature:
| Feature | কীভাবে বানানো | ভুয়ার দিকে টান (বানানো নিয়মে) |
|---|---|---|
| অর্ডারের দাম | lognormal, মাঝামাঝি প্রায় ৳১১০০ | দাম যত বেশি, তত বেশি |
| অ্যাকাউন্টের বয়স | exponential, গড় ৪০০ দিন | নতুন অ্যাকাউন্টে বেশি |
| রাতের অর্ডার | ১৫% অর্ডার রাত ১২টা থেকে ৫টায় | রাতে বেশি |
| একই ফোনে আরও অর্ডার | Poisson, গড় ০.৩, শেষ ২৪ ঘণ্টায় | যত বেশি, তত বেশি |
এই চারটি থেকে একটি গোপন score আর কিছু এলোমেলো গোলমাল যোগ করে label ঠিক করা হয়েছে, এমনভাবে যে প্রায় ২% অর্ডার ভুয়া হয় (আসলে ৬০০০-এ ১১৩টি, ১.৮৮%). এমন ডেটাকে বলে অসম শ্রেণির ডেটা বা class imbalance, আর positive-এর ভাগকে বলে prevalence, যা পর্ব ৩৫-এর base rate-এরই আরেক নাম। Stratified ভাগে ৭০% train (৪২০০টি, ৭৯টি ভুয়া) আর ৩০% test (১৮০০টি, ৩৪টি ভুয়া); test-এর সংখ্যাগুলোই এই পর্বে বারবার আসবে। কেন test আলাদা রাখতে হয়, তার পুরো আলোচনা পর্ব ৫৬ (Train, validation আর test — data leakage-এর ফাঁদ)-এ।
সৎ সতর্কতা: ডেটা আর ভুয়া হওয়ার নিয়ম দুটোই বানানো, আর test-এ ভুয়া মাত্র ৩৪টি। এখানের প্রতিটি সংখ্যা একটি seed-এর একটি ভাগের ফল; অন্য seed-এ সংখ্যা নড়বে (অংশ ৯-এ দেখবেন কতটা)। বাস্তবের কোনো দোকানের জালিয়াতি এমন সরল নিয়মে চলে না। লক্ষ্য মাপার পদ্ধতি শেখা।
২ · Confusion matrix — চার ঘরে সব ঠিক আর সব ভুল
প্রথমে ঠিক করতে হয় কোন শ্রেণিকে আমরা খুঁজছি; তাকে বলে positive class. এখানে positive = ভুয়া। “Positive”-এ ভালো-মন্দের অর্থ নেই, যেমন ডাক্তারের test-এ রোগ ধরা পড়া। Model-এর প্রতিটি অনুমান আর আসল label মিলিয়ে চারটি ঘটনা হতে পারে, আর চারটি ঘর মিলে confusion matrix:
- True positive (TP): আসলে ভুয়া, model আটকেছে। এটিই চাওয়া কাজ।
- False positive (FP): আসলে সৎ, model আটকেছে। সৎ গ্রাহক বিরক্ত হলেন, অর্ডার দেরি হলো।
- False negative (FN): আসলে ভুয়া, model ছেড়ে দিয়েছে। পণ্য আর রাইডারের খরচ নষ্ট।
- True negative (TN): আসলে সৎ, model ছেড়ে দিয়েছে। সাধারণ, নিঃশব্দ সাফল্য।
নামগুলো মনে রাখার কৌশল: দ্বিতীয় শব্দ (positive/negative) হলো model কী বলেছে, প্রথম শব্দ (true/false) হলো সেই কথা ঠিক কি না। নিচের ছবিতে test set-এর আসল সংখ্যাগুলো বসানো হয়েছে (threshold ০.৫, অংশ ৩-এর কোড থেকে)। scikit-learn-এর বিন্যাসই এখানে নেওয়া হয়েছে: সারি = আসল label, কলাম = অনুমান, আর ছোট label (০) আগে।
Confusion matrix: কোন সারি আর কোন কলাম থেকে কোন metric
Rows = actual, columns = predicted (scikit-learn order): [[TN, FP], [FN, TP]]
ছোট উদাহরণে হাতে ঘর ভরলে বিষয়টি পরিষ্কার হয়। ধরুন ১০টি অর্ডারের score আর আসল label এমন (৪টি ভুয়া): #১ ০.৯২ ভুয়া, #২ ০.৮১ সৎ, #৩ ০.৭৪ ভুয়া, #৪ ০.৫৮ সৎ, #৫ ০.৪৭ ভুয়া, #৬ ০.৩৬ সৎ, #৭ ০.২৮ সৎ, #৮ ০.১৯ ভুয়া, #৯ ০.১২ সৎ, #১০ ০.০৫ সৎ। নিয়ম: score ≥ ০.৫ হলে আটকানো (positive), নইলে ছেড়ে দেওয়া।
| TP (ভুয়া, ধরা) | |
| FP (সৎ, আটকানো) | |
| FN (ভুয়া, ছাড়া) | |
| TN (সৎ, ছাড়া) | |
| মোট বসানো |
- নিয়ম আর শূন্য ছক। ১০টি অর্ডার, ৪টি আসলে ভুয়া। প্রতিটি অর্ডার চারটি ঘরের ঠিক একটিতে যাবে, তাই শেষে চার ঘরের যোগফল হবে ১০.
- #১, score ০.৯২. ০.৫-এর বেশি, তাই আটকানো; আসলেও ভুয়া। ঠিক positive: TP = ১.
- #২, score ০.৮১. আটকানো, কিন্তু আসলে সৎ গ্রাহক। ভুল positive: FP = ১. উঁচু score মানেই নিশ্চিত ভুয়া নয়।
- #৩ আর #৪. দুটিই ০.৫-এর ওপরে, তাই আটকানো। #৩ ভুয়া (TP = ২), #৪ সৎ (FP = ২)।
- #৫ আর #৮. দুটিই ০.৫-এর নিচে, তাই ছেড়ে দেওয়া, অথচ আসলে ভুয়া: FN = ২. #৫-এর score ০.৪৭, সীমার খুব কাছে।
- বাকি চারটি। #৬, #৭, #৯, #১০ সবই ছাড়া আর সবই সৎ: TN = ৪. চার ঘরের যোগফল ২ + ২ + ২ + ৪ = ১০.
- মাপগুলো। Accuracy ০.৬০; precision ০.৫০ (আটকানো ৪টির ২টি ভুয়া); recall ০.৫০ (৪টি ভুয়ার ২টি ধরা); specificity ৪/৬ ≈ ০.৬৭ (৬টি সৎ অর্ডারের ৪টি নির্বিঘ্নে গেছে)।
বিন্যাসের ফাঁদ: অনেক বই আর ওয়েবসাইট positive-কে প্রথম সারি-কলামে রাখে, [[TP, FN], [FP, TN]]; scikit-learn label-এর ছোট থেকে বড় ক্রমে রাখে, তাই ০/১-এ [[TN, FP], [FN, TP]], আর .ravel() দেয় tn, fp, fn, tp ক্রমে। কোনো ছক পড়ার আগে দেখে নিন সারি আসল না অনুমান, আর কোন শ্রেণি আগে। labels=[1, 0] দিলে scikit-learn-ও ক্রম বদলায়।
৩ · Accuracy-র ফাঁদ, আর আরও চারটি মাপ — precision, recall, specificity, FPR
সবচেয়ে পরিচিত মাপ accuracy: (TP + TN) / মোট, অর্থাৎ কত ভাগ অনুমান ঠিক। শ্রেণি দুটি মোটামুটি সমান হলে এটি কাজের। কিন্তু ভুয়া মাত্র ১.৯% হলে, যে model কখনো কাউকে আটকায় না, তার accuracy-ও ৯৮%-এর বেশি। এই অদ্ভুত অবস্থার নাম accuracy paradox: কিছুই না শেখা model “নির্ভুল” দেখায়, কখনো কখনো কাজের model-এর চেয়েও বেশি। তাই প্রতিটি মূল্যায়নের প্রথম অভ্যাস একটি baseline, বিশেষ করে সবচেয়ে বেশি শ্রেণি বলার baseline; scikit-learn-এ এর জন্য আছে DummyClassifier, যা feature দেখেই না।
Accuracy-র বদলে প্রশ্নগুলো আলাদা করে করতে হয়। চারটি মাপ, প্রতিটি confusion matrix-এর একটি সারি বা কলাম থেকে:
| মাপ | সূত্র | যে প্রশ্নের উত্তর | বাজারঘরের ভাষায় |
|---|---|---|---|
| Precision | TP / (TP + FP) | যাদের positive বললাম, তাদের কত ভাগ সত্যিই positive? | আটকানো অর্ডারের কত ভাগ সত্যিই ভুয়া; বাকিরা বিনা দোষে বিরক্ত সৎ গ্রাহক |
| Recall বা sensitivity (TPR) | TP / (TP + FN) | সব আসল positive-এর কত ভাগ ধরলাম? | সব ভুয়া অর্ডারের কত ভাগ আটকানো গেল; বাকিরা পার পেয়ে টাকা নষ্ট করল |
| Specificity (TNR) | TN / (TN + FP) | সব আসল negative-এর কত ভাগ ঠিকভাবে ছেড়ে দিলাম? | সৎ গ্রাহকের কত ভাগ কোনো ঝামেলা ছাড়া অর্ডার পেলেন |
| False positive rate (FPR) | FP / (FP + TN) = 1 − specificity | সব আসল negative-এর কত ভাগ ভুল করে positive বললাম? | সৎ গ্রাহকের কত ভাগ ভুল করে আটকানো হলো |
লক্ষ করুন, precision আর recall-এর লব একই (TP), শুধু হর আলাদা: precision-এর হর model-এর “হ্যাঁ” বলা সব অর্ডার, recall-এর হর সব আসল ভুয়া। আরেকটি মাপ false negative rate = FN/(FN + TP) = 1 − recall. চিকিৎসায় sensitivity, তথ্য খোঁজায় recall, ROC-এ TPR: নাম তিনটি, সংখ্যা একটি। Precision-এর একটি গুরুত্বপূর্ণ বৈশিষ্ট্য: এটি prevalence-এর ওপর নির্ভর করে। একই sensitivity আর specificity-র একটি test, রোগ যত দুর্লভ, তার positive ফলের তত কম ভাগ সত্যি; এটিই পর্ব ৫০-এর Bayes-হিসাব, আর precision আসলে P(ভুয়া | আটকানো).
নিচের ব্লক পুরো ডেটা বানায়, একটি logistic regression train করে, test-এর প্রতিটি অর্ডারের score p বের করে, আর threshold ০.৫-এ চারটি ঘর হাতে গোনে। তারপর সব মাপ হাতে আর scikit-learn দিয়ে মিলিয়ে দেখে, আর শেষে DummyClassifier. এই ব্লক আর পরের তিনটি একই namespace ভাগ করে (data-shared); পরের কোনো ব্লক আগে চালালে এটি আপনা থেকে আগে চলে।
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.dummy import DummyClassifier
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
rng = np.random.default_rng(42)
n = 6000 # বাজারঘরের কাল্পনিক অর্ডার
value = rng.lognormal(7.0, 0.6, n).round() # অর্ডারের দাম, টাকা
age = rng.exponential(400, n).round() # অ্যাকাউন্টের বয়স, দিন
night = (rng.random(n) < 0.15).astype(int) # রাত ১২টা থেকে ৫টার অর্ডার
phone = rng.poisson(0.3, n) # একই ফোনে ২৪ ঘণ্টায় আর কত অর্ডার
z = -3 + 1.2*np.log(value/1100) - 0.003*age + 1.5*night + phone + rng.normal(0, 1, n)
y = (z > 0).astype(int) # 1 = ভুয়া অর্ডার (বানানো নিয়ম)
X = np.column_stack([np.log(value), np.log1p(age), night, phone])
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42)
model = LogisticRegression(max_iter=1000).fit(Xtr, ytr)
p = model.predict_proba(Xte)[:, 1] # প্রতিটি test অর্ডারের score
pred = (p >= 0.5).astype(int)
print("ভুয়ার ভাগ:", round(float(y.mean()), 4), "| test:", len(yte), "অর্ডার,", int(yte.sum()), "ভুয়া")
TP = int(((pred == 1) & (yte == 1)).sum()); FP = int(((pred == 1) & (yte == 0)).sum())
FN = int(((pred == 0) & (yte == 1)).sum()); TN = int(((pred == 0) & (yte == 0)).sum())
print("হাতে TN FP FN TP:", TN, FP, FN, TP, "| sklearn:", confusion_matrix(yte, pred).ravel())
P, R = TP / (TP + FP), TP / (TP + FN)
print(f"accuracy {(TP + TN) / len(yte):.4f} | sklearn {accuracy_score(yte, pred):.4f}")
print(f"precision {P:.4f} | sklearn {precision_score(yte, pred):.4f}")
print(f"recall {R:.4f} | sklearn {recall_score(yte, pred):.4f}")
print(f"F1 {2 * P * R / (P + R):.4f} | sklearn {f1_score(yte, pred):.4f}")
print(f"specificity {TN / (TN + FP):.4f} | FPR {FP / (FP + TN):.4f}")
dummy = DummyClassifier(strategy="most_frequent").fit(Xtr, ytr)
dp = dummy.predict(Xte) # সবাইকে "সৎ" বলে
print(f"Dummy: accuracy {accuracy_score(yte, dp):.4f} | recall {recall_score(yte, dp):.1f}",
f"| precision {precision_score(yte, dp, zero_division=0):.1f}")আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):
ভুয়ার ভাগ: 0.0188 | test: 1800 অর্ডার, 34 ভুয়া
হাতে TN FP FN TP: 1763 3 27 7 | sklearn: [1763 3 27 7]
accuracy 0.9833 | sklearn 0.9833
precision 0.7000 | sklearn 0.7000
recall 0.2059 | sklearn 0.2059
F1 0.3182 | sklearn 0.3182
specificity 0.9983 | FPR 0.0017
Dummy: accuracy 0.9811 | recall 0.0 | precision 0.0
হাতের হিসাব আর scikit-learn হুবহু মেলে। এবার পড়ুন কী বলছে। Model-এর accuracy ৯৮.৩৩%, “সবাই সৎ” বলা Dummy-র ৯৮.১১%; পার্থক্য মাত্র ০.২২ শতাংশ বিন্দু, অর্থাৎ ১৮০০-তে ৪টি অর্ডার। Accuracy দেখে মনে হবে দুটি প্রায় সমান। অথচ model ৩৪টি ভুয়ার ৭টি ধরেছে আর Dummy একটিও না। Precision ০.৭০ মানে আটকানো ১০টির ৭টি সত্যিই ভুয়া, ভালো; কিন্তু recall ০.২১ মানে ভুয়ার প্রায় চার-পঞ্চমাংশ (২৭টি) পার পেয়ে যাচ্ছে। Specificity ০.৯৯৮ দেখতে নিখুঁত, কারণ সৎ অর্ডার এত বেশি (১৭৬৬টি) যে ৩টি ভুল প্রায় চোখেই পড়ে না।
Dummy কাউকে positive বলেনি, তাই TP + FP = ০ আর precision অসংজ্ঞায়িত; zero_division=0 না দিলে scikit-learn UndefinedMetricWarning দেখায়, যা একটি দরকারি সংকেত। শিক্ষা: অসম ডেটায় accuracy প্রায় পুরোটা TN. আর threshold ০.৫ প্রকৃতির নিয়ম নয়; অংশ ৫-এ সেটি সরাব।
৪ · F1 — দুই সংখ্যাকে এক করা, harmonic mean কেন, আর Fβ
দুটি সংখ্যা নিয়ে model তুলনা করা ঝামেলার: একটির precision বেশি, আরেকটির recall. তাই প্রায়ই দুটিকে মিলিয়ে একটি সংখ্যা বানানো হয়, F1 score:
F1 = 2 · P · R / (P + R) = 2TP / (2TP + FP + FN)
এখানে P = precision, R = recall. এটি P আর R-এর সাধারণ গড় নয়, harmonic mean: উল্টো মানগুলোর গড়ের উল্টো, 1/F1 = (1/P + 1/R)/2. ডান পাশের রূপটি দেখায় F1-এ TN কোথাও নেই; অসম ডেটায় এটি ইচ্ছাকৃত, কারণ বিপুল TN-ই accuracy-কে অন্ধ করেছিল।
Harmonic mean কোথায় স্বাভাবিক, তার একটি পরিচিত উদাহরণ। ঢাকা থেকে সাভার ৩০ কিলোমিটার। যাওয়ার সময় ফাঁকা রাস্তায় ঘণ্টায় ৬০ কিলোমিটার, ফেরার সময় জ্যামে ঘণ্টায় ১০. গড় গতি কি ৩৫? না। যেতে আধা ঘণ্টা, ফিরতে তিন ঘণ্টা, মোট ৬০ কিলোমিটার সাড়ে তিন ঘণ্টায়: ঘণ্টায় প্রায় ১৭.১, যা ঠিক ২ × ৬০ × ১০ / (৬০ + ১০). ধীর অংশটি বেশি সময় খায়, তাই গড়কে নিচে টানে। F1-ও তাই: যেটি দুর্বল, সেটি বেশি গুরুত্ব পায়।
একটি কষে দেখা উদাহরণ। ধরুন একটি model শুধু সবচেয়ে নিশ্চিত দুই-একটি অর্ডার আটকায়: P = ০.৯, R = ০.১. সাধারণ গড় (০.৯ + ০.১)/২ = ০.৫, যা শুনতে মাঝারি। Harmonic mean: ২ × ০.৯ × ০.১ / (০.৯ + ০.১) = ০.১৮/১ = ০.১৮. F1 বলছে “এই model প্রায় অকেজো”, কারণ ৯০% ভুয়া পার পাচ্ছে। আবার P = R = ০.৫ হলে দুই গড়ই ০.৫; harmonic mean শুধু অসমতার শাস্তি দেয়। অংশ ৩-এর model-এ P = ০.৭০, R = ০.২০৬: সাধারণ গড় ০.৪৫৩, F1 ০.৩১৮.
যখন একটি ভুল বেশি দামি — Fβ
F1 precision আর recall-কে সমান গুরুত্ব দেয়। কিন্তু বাজারঘরে একটি ভুয়া অর্ডার পার পাওয়া সম্ভবত একজন সৎ গ্রাহককে একটি যাচাইয়ের ফোন করার চেয়ে দামি। তখন Fβ score:
Fβ = (1 + β²) · P · R / (β² · P + R)
β একটি ধনাত্মক সংখ্যা যা বলে recall-কে precision-এর তুলনায় কতগুণ গুরুত্ব দেওয়া হচ্ছে। β = ১ হলে F1; β = ২ (F2) হলে recall বেশি গুরুত্ব পায়; β = ০.৫ হলে precision. অংশ ৩-এর model-এ (P = ০.৭০, R = ০.২০৬): F2 = ৫ × ০.৭ × ০.২০৬ / (৪ × ০.৭ + ০.২০৬) ≈ ০.২৪০, F1-এর চেয়ে কম, কারণ F2 দুর্বল recall-কে আরও কড়া শাস্তি দেয়; আর F0.5 ≈ ০.৪৭৩. scikit-learn-এ fbeta_score(y, pred, beta=2).
Precision আর recall-এর জন্ম ১৯৫০ ও ১৯৬০-এর দশকে নথি খোঁজার মূল্যায়নে (Cyril Cleverdon-এর Cranfield পরীক্ষাগুলো প্রায়ই উল্লেখ করা হয়)। F-measure সাধারণত C. J. van Rijsbergen-এর ১৯৭৯ সালের বই Information Retrieval-এর মাপ E থেকে আসা বলে ধরা হয় (F = 1 − E), β-এর ধারণাসহ।
৫ · Threshold সরালে কী বদলায় — precision বনাম recall, আর টাকার হিসাব
Logistic regression নিজে কোনো সিদ্ধান্ত দেয় না, দেয় একটি score. সিদ্ধান্ত আসে একটি threshold থেকে: score ≥ t হলে আটকানো। model.predict() চুপচাপ t = ০.৫ ব্যবহার করে, কিন্তু সেটি শুধু একটি প্রথা। t কমালে বেশি অর্ডার আটকানো হয়: বেশি ভুয়া ধরা পড়ে (recall বাড়ে), কিন্তু বেশি সৎ গ্রাহকও আটকায় (precision সাধারণত কমে)। t বাড়ালে উল্টো। আগের ১০টি অর্ডারেই এটি দেখা যায়।
| threshold t | |
| আটকানো অর্ডার | |
| TP · FP · FN · TN | |
| precision | |
| recall | |
| F1 |
- t = ০.৯, খুব কড়া। শুধু #১ (০.৯২) আটকানো, আর সেটি সত্যিই ভুয়া: precision ১.০০. কিন্তু ৪টি ভুয়ার ৩টি পার পেল, recall ০.২৫. যত কম আটকাবেন, “যাদের আটকালেন তারা ঠিক” বলা তত সহজ।
- t = ০.৭. #২ (০.৮১, সৎ) আর #৩ (০.৭৪, ভুয়া) যোগ হলো। একটি ভুয়া বাড়ল, একটি সৎ গ্রাহকও আটকাল: precision ০.৬৭-এ নামল, recall ০.৫০-এ উঠল।
- t = ০.৫, প্রথাগত সীমা। #৪ (০.৫৮, সৎ) যোগ হলো, শুধু একটি FP বাড়ল: precision ০.৫০, recall অপরিবর্তিত ০.৫০. Recall শুধু তখনই বাড়ে যখন সীমা একটি ভুয়া অর্ডার পার হয়।
- t = ০.৩. #৫ (০.৪৭, ভুয়া) আর #৬ (০.৩৬, সৎ) যোগ হলো। Recall ০.৭৫, precision ০.৫০-এ স্থির। দুটি মাপ সবসময় বিপরীতে নড়ে না; precision মাঝে মাঝে বাড়েও।
- t = ০.১, খুব ঢিলা। #৭, #৮ (০.১৯, ভুয়া), #৯ যোগ হলো: সব ভুয়া ধরা, recall ১.০০, কিন্তু ৯টি আটকানোর ৫টি সৎ গ্রাহক, precision ০.৪৪. সবাইকে আটকালে recall ১ হয়ই; একা recall দেখে model ভালো বলা যায় না।
কোন threshold? খরচ দিয়ে ঠিক করুন
F1-সেরা threshold একটি উত্তর, কিন্তু F1 টাকা চেনে না। ব্যবসার প্রশ্ন হলো: প্রতিটি ভুলের দাম কত? এই পর্বের জন্য বানানো দুটি দাম ধরি (বাস্তবে এগুলো দোকানের হিসাব থেকে আসবে): একটি ভুয়া অর্ডার পার পেলে পণ্য আনা-নেওয়া, প্যাকিং আর রাইডারের খরচে CFN = ৳১৫০০ ক্ষতি; একজন সৎ গ্রাহককে আটকে ফোনে যাচাই করলে কল-সেন্টারের সময়, দেরি আর কিছু হারানো বিক্রিতে CFP = ৳১০০. এমন দামের টেবিলকে বলে cost matrix (ঠিক সিদ্ধান্তের দাম এখানে ০ ধরা)। তখন প্রতিটি threshold-এর expected cost:
খরচ(t) = CFN · FN(t) + CFP · FP(t)
এটি পর্ব ৫৪-এর expected utility-রই উল্টো পিঠ: লাভ সর্বোচ্চ করার বদলে ক্ষতি সর্বনিম্ন। একটি সুন্দর তাত্ত্বিক ফলও আছে। Model-এর score যদি সত্যিকারের সম্ভাবনা হয় (অর্থাৎ calibrated), তাহলে একটি অর্ডার আটকানো লাভজনক যখন p · CFN > (1 − p) · CFP, অর্থাৎ
p > t* = CFP / (CFP + CFN) = ১০০ / ১৬০০ = ০.০৬২৫
Charles Elkan ২০০১ সালের “The Foundations of Cost-Sensitive Learning” প্রবন্ধে এমন নিয়ম আলোচনা করেন। দাম ১৫ গুণ অসম, তাই সীমা ০.৫ থেকে অনেক দূরে। নিচের ব্লক t = ০.০২ থেকে ০.৯০ ঘুরে প্রতিটিতে precision, recall, F1 আর প্রতি ১০০০ অর্ডারে খরচ হিসাব করে ছবি আঁকে।
import numpy as np
import matplotlib.pyplot as plt
C_FN, C_FP = 1500, 100 # কাল্পনিক টাকা: ছাড় পাওয়া ভুয়া অর্ডার, আটকানো সৎ অর্ডার
ths = np.round(np.arange(0.02, 0.91, 0.02), 2)
rows = []
for t in ths:
pr = p >= t
tp = int((pr & (yte == 1)).sum()); fp = int((pr & (yte == 0)).sum())
fn = int((~pr & (yte == 1)).sum())
P = tp / (tp + fp) if tp + fp else 1.0
R = tp / (tp + fn)
F = 2 * P * R / (P + R) if P + R else 0.0
cost = (C_FN * fn + C_FP * fp) / len(yte) * 1000 # প্রতি ১০০০ অর্ডারে
rows.append((t, P, R, F, cost, tp, fp, fn))
rows = np.array(rows)
print(" t prec recall F1 TP FP FN cost/1000")
for r in rows[[0, 2, 4, 7, 14, 24, 39]]:
print(f"{r[0]:.2f} {r[1]:.3f} {r[2]:.3f} {r[3]:.3f} {r[5]:3.0f} {r[6]:3.0f} {r[7]:3.0f} {r[4]:7.0f}")
b, f = rows[rows[:, 4].argmin()], rows[rows[:, 3].argmax()]
print(f"সবচেয়ে কম খরচ: t = {b[0]:.2f}, প্রতি ১০০০ অর্ডারে {b[4]:.0f} টাকা")
print(f"সবচেয়ে বেশি F1: t = {f[0]:.2f}, F1 {f[3]:.3f}, খরচ {f[4]:.0f} টাকা")
print("তত্ত্বের সীমা C_FP / (C_FP + C_FN) =", round(C_FP / (C_FP + C_FN), 4))
print("কাউকে না আটকালে:", round(C_FN * yte.sum() / len(yte) * 1000), "টাকা")
fig, (a1, a2) = plt.subplots(1, 2, figsize=(9, 3.4))
a1.plot(ths, rows[:, 1], label="precision"); a1.plot(ths, rows[:, 2], label="recall")
a1.plot(ths, rows[:, 3], label="F1", ls="--")
a1.set(xlabel="threshold", ylabel="score", title="Moving the threshold"); a1.legend()
a2.plot(ths, rows[:, 4] / 1000, color="tab:red"); a2.axvline(b[0], color="gray", ls=":")
a2.set(xlabel="threshold", ylabel="cost (thousand taka)", title="Expected cost per 1000 orders")
fig.subplots_adjust(wspace=0.35, bottom=0.17)
plt.show()আসল আউটপুট (ব্রাউজারের Python, matplotlib 3.10.8; ছবিটি কোডের নিচে আঁকা হয়):
t prec recall F1 TP FP FN cost/1000
0.02 0.099 0.853 0.177 29 265 5 18889
0.06 0.188 0.735 0.299 25 108 9 13500
0.10 0.250 0.618 0.356 21 63 13 14333
0.16 0.293 0.500 0.370 17 41 17 16444
0.30 0.500 0.265 0.346 9 9 25 21333
0.50 0.700 0.206 0.318 7 3 27 22667
0.80 0.000 0.000 0.000 0 1 34 28389
সবচেয়ে কম খরচ: t = 0.06, প্রতি ১০০০ অর্ডারে 13500 টাকা
সবচেয়ে বেশি F1: t = 0.16, F1 0.370, খরচ 16444 টাকা
তত্ত্বের সীমা C_FP / (C_FP + C_FN) = 0.0625
কাউকে না আটকালে: 28333 টাকা
বাঁ ছবিতে threshold বাড়লে recall নামে আর precision মোটামুটি ওঠে, তবে আঁকাবাঁকা: ৩৪টি ভুয়ায় একটি অর্ডার এদিক-ওদিক হলেই precision লাফায়। কেউ আটকানো না হলে (t ≥ ০.৮৮) প্রথা মেনে precision ১.০ লেখা হয়েছে।
ডান ছবি আর শেষ চার লাইন আসল গল্প। t = ০.৫-এ প্রতি ১০০০ অর্ডারে খরচ ৳২২,৬৬৭; t = ০.০৬-এ ৳১৩,৫০০, প্রায় ৪০% কম, যদিও precision তখন মাত্র ০.১৮৮ (আটকানো ১৩৩টির ১০৮টি সৎ)। কারণ একটি ছাড় পাওয়া ভুয়ার দাম ১৫টি অপ্রয়োজনীয় ফোনের সমান। খরচ-সেরা ০.০৬ তত্ত্বের ০.০৬২৫-এর খুব কাছে, যা ইঙ্গিত দেয় এই ডেটায় logistic regression-এর score মোটামুটি calibrated (বানানো ডেটায় এটি প্রত্যাশিত; বাস্তবে যাচাই করতে হয়)। F1-সেরা ০.১৬ ভিন্ন জায়গায়, খরচ ৳১৬,৪৪৪: F1 আর ব্যবসার লক্ষ্য এক জিনিস নয়। তুলনার জন্য, কাউকে না আটকালে ৳২৮,৩৩৩, আর সবাইকে আটকালে ১৭৬৬ × ১০০ / ১.৮ ≈ ৳৯৮,১১১.
এখানে একটি অসততা লুকানো আছে: threshold বাছা হয়েছে test set দেখেই, তাই ৳১৩,৫০০ নতুন অর্ডারে একটু আশাবাদী। সৎ পথ হলো train-এর ভেতর থেকে একটি আলাদা validation set বা cross-validation-এ threshold বাছা, আর test-এ শুধু একবার চূড়ান্ত মাপা (পর্ব ৫৬ আর পর্ব ৬৬, model বাছাই — cross-validation আর hyperparameter search). scikit-learn 1.5 থেকে TunedThresholdClassifierCV ঠিক এই কাজটি cross-validation দিয়ে করে, আর FixedThresholdClassifier একটি বাছাই করা threshold model-এর সঙ্গে বেঁধে রাখে।
৬ · ROC curve আর AUC — score থেকে ধাপে ধাপে
Threshold-এর প্রতিটি মান একটি operating point: একজোড়া (FPR, TPR). সব threshold-এর বিন্দু একসঙ্গে আঁকলে পাওয়া যায় ROC curve (receiver operating characteristic): x অক্ষে false positive rate, y অক্ষে true positive rate বা recall. Curve যত বাঁ-ওপরের কোণ (০, ১) ঘেঁষে, model তত ভালো; কোণাকুণি রেখা হলো এলোমেলো অনুমান। পুরো curve-এর নিচের ক্ষেত্রফলকে বলে AUC (area under the curve), যা threshold বাছার আগেই model-এর score কতটা ভালোভাবে সাজায় তার একটি সারাংশ।
হাতে আঁকার নিয়ম সহজ। Score বড় থেকে ছোট সাজান, আর threshold একে একে নামান। প্রতিবার একটি ভুয়া (positive) অর্ডার সীমা পার হলে ১/P ঘর ওপরে উঠুন, একটি সৎ (negative) অর্ডার পার হলে ১/N ঘর ডানে যান (P আর N হলো positive আর negative-এর সংখ্যা)। ছয়টি অর্ডার নিই: A ০.৯ ভুয়া, B ০.৮ ভুয়া, C ০.৭ সৎ, D ০.৬ ভুয়া, E ০.৪ সৎ, F ০.২ সৎ। P = N = ৩, তাই প্রতিটি পা ১/৩.
| threshold | |
| নতুন পার হলো | |
| (FPR, TPR) |
- শুরু। Threshold সব score-এর ওপরে, কেউ আটকানো নয়: TPR = FPR = ০. সব ROC curve (০, ০) থেকে শুরু হয়।
- t = ০.৯. A (ভুয়া) আটকানো হলো: TPR ১/৩-এ উঠল, কোনো সৎ গ্রাহক আটকায়নি, FPR ০. খাড়া ওপরে।
- t = ০.৮. B-ও ভুয়া: আবার ওপরে, (০, ২/৩). এখন পর্যন্ত একটিও ভুল নেই।
- t = ০.৭. C সৎ, তবু তার score D-র চেয়ে বেশি: প্রথম ভুল। ডানে এক পা, (১/৩, ২/৩).
- t = ০.৬. D ভুয়া: ওপরে, (১/৩, ১). সব ভুয়া ধরা হলো, একজন সৎ গ্রাহকের দামে।
- t = ০.৪. E সৎ: ডানে, (২/৩, ১). এখন আর recall বাড়ার কিছু নেই, শুধু ভুল বাড়ছে।
- t = ০.২. F সৎ: ডানে, (১, ১). সবাইকে আটকালে সব curve এখানে শেষ হয়।
- ক্ষেত্রফল। ছকটি ৩ × ৩ = ৯ ঘর; রেখার নিচে প্রথম কলামে ২টি, বাকি দুই কলামে ৩টি করে, মোট ৮. AUC = ৮/৯ ≈ ০.৮৮৯. একমাত্র “ভুল” হলো C-র score D-র ওপরে থাকা।
AUC-এর অর্থ: জোড়ায় জোড়ায় জেতা
ক্ষেত্রফলের একটি সুন্দর সমতুল্য অর্থ আছে: AUC = একটি এলোমেলো positive আর একটি এলোমেলো negative নিলে, positive-টি বেশি score পাওয়ার সম্ভাবনা (সমান score হলে অর্ধেক জেতা)। ছয়টি অর্ডারে ৩ × ৩ = ৯টি (ভুয়া, সৎ) জোড়া। A আর B তিনটি সৎ অর্ডারকেই হারায় (৩ + ৩), D হারায় E আর F-কে কিন্তু C-র কাছে হারে (২): ৮/৯. Stepper-এর প্রতিটি ঘর আসলে একটি জোড়া। এই সমতা Mann–Whitney U পরিসংখ্যানের সঙ্গে AUC-এর যোগসূত্র, যা James Hanley আর Barbara McNeil ১৯৮২ সালে রেডিওলজির প্রেক্ষাপটে ব্যাখ্যা করে জনপ্রিয় করেন। তিনটি ফল: (১) AUC শুধু ক্রম দেখে, তাই calibration সম্পর্কে কিছু বলে না; (২) ০.৫ মানে এলোমেলো, ১ মানে নিখুঁত ক্রম, ০.৫-এর নিচে মানে ক্রম উল্টো; (৩) এটি কোনো একটি threshold-এর গুণ নয়, সব threshold-এর গড় ছবি।
ROC নামটি রাডারের জগতের (“receiver operating characteristic”); প্রায়ই বলা হয় দ্বিতীয় বিশ্বযুদ্ধের রাডার-বিশ্লেষণে এর শুরু, পরে signal detection theory আর চিকিৎসার রোগনির্ণয়ে প্রতিষ্ঠা। Tom Fawcett-এর ২০০৬ সালের “An introduction to ROC analysis” একটি বহুল পঠিত পরিচিতি।
৭ · ভারী imbalance-এ ROC কেন বেশি সুন্দর দেখায় — precision–recall curve আর average precision
ROC-এর x অক্ষ FPR = FP / (সব সৎ অর্ডার). বাজারঘরের test-এ সৎ অর্ডার ১৭৬৬টি, তাই ৬৩টি সৎ গ্রাহককে আটকানোও FPR-এ মাত্র ০.০৩৬, curve-এ প্রায় অদৃশ্য একটি ডানে সরা। অথচ সেই একই threshold-এ (t = ০.১০) আটকানো ৮৪টির ৬৩টি সৎ: precision ০.২৫. ROC বিপুল TN-এর আড়ালে FP-এর দাম লুকিয়ে ফেলতে পারে; ঠিক accuracy-র মতো, শুধু একটু সূক্ষ্মভাবে।
বিকল্প হলো precision–recall curve: x অক্ষে recall, y অক্ষে precision, প্রতিটি threshold-এ একটি বিন্দু। এতে TN কোথাও নেই। এর সারাংশ average precision (AP): threshold নামাতে নামাতে প্রতিবার recall যতটুকু বাড়ে, তাকে সেখানকার precision দিয়ে গুণ করে যোগ, AP = ∑n (Rn − Rn−1) · Pn. scikit-learn-এর average_precision_score ঠিক এই ধাপে ধাপে যোগফল নেয়, রেখা টেনে মসৃণ করে (interpolation) নয়। দুটি মাপের “এলোমেলো model”-এর মানও আলাদা: ROC AUC-এর ০.৫, কিন্তু AP-র প্রায় prevalence, এখানে ০.০১৯. তাই AP ০.৩৪ মানে এলোমেলোর ১৮ গুণ, আর AUC ০.৯৫ মানে … সেটিই প্রশ্ন।
নিচের ব্লক একই score দিয়ে দুটি curve পাশাপাশি আঁকে, AUC-কে ৬০০৪৪টি জোড়া গুনে যাচাই করে, আর একটি পরীক্ষা করে: test-এর সৎ অর্ডার এলোমেলোভাবে কমিয়ে ৩৪:৩৪ বানালে দুটি মাপ কী করে (৫০ বার, গড়)। Model আর score একই থাকে; বদলায় শুধু prevalence.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import (roc_curve, roc_auc_score, precision_recall_curve,
average_precision_score)
fpr, tpr, _ = roc_curve(yte, p)
prec, rec, _ = precision_recall_curve(yte, p)
auc, ap = roc_auc_score(yte, p), average_precision_score(yte, p)
print(f"ROC AUC {auc:.4f} | average precision {ap:.4f} | ভুয়ার ভাগ {yte.mean():.4f}")
pos, neg = p[yte == 1], p[yte == 0] # প্রতিটি (ভুয়া, সৎ) জোড়া তুলনা
wins = (pos[:, None] > neg[None, :]).sum() + 0.5 * (pos[:, None] == neg[None, :]).sum()
print(f"জোড়া {pos.size} × {neg.size} = {pos.size * neg.size}, ভুয়া এগিয়ে {wins:.0f}",
f"→ {wins / (pos.size * neg.size):.4f}")
r = np.random.default_rng(0) # সৎ অর্ডার কমিয়ে ৫০:৫০ বানাই, ৫০ বার
A, P = [], []
for _ in range(50):
k = np.r_[np.flatnonzero(yte == 1), r.choice(np.flatnonzero(yte == 0), pos.size, replace=False)]
A.append(roc_auc_score(yte[k], p[k])); P.append(average_precision_score(yte[k], p[k]))
print(f"৫০:৫০ ডেটায় গড়: ROC AUC {np.mean(A):.4f} | average precision {np.mean(P):.4f}")
fig, (a1, a2) = plt.subplots(1, 2, figsize=(9, 3.6))
a1.plot(fpr, tpr, label=f"model (AUC {auc:.3f})")
a1.plot([0, 1], [0, 1], ls=":", color="gray", label="random (0.5)")
a1.set(xlabel="false positive rate", ylabel="recall (TPR)", title="ROC curve"); a1.legend()
a2.step(rec, prec, where="post", label=f"model (AP {ap:.3f})")
a2.axhline(yte.mean(), ls=":", color="gray", label=f"random ({yte.mean():.3f})")
a2.set(xlabel="recall", ylabel="precision", title="Precision-recall curve", ylim=(0, 1.02))
a2.legend()
fig.subplots_adjust(wspace=0.35, bottom=0.17)
plt.show()আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0; দুটি curve কোডের নিচে আঁকা হয়):
ROC AUC 0.9488 | average precision 0.3408 | ভুয়ার ভাগ 0.0189
জোড়া 34 × 1766 = 60044, ভুয়া এগিয়ে 56969 → 0.9488
৫০:৫০ ডেটায় গড়: ROC AUC 0.9482 | average precision 0.9388
প্রথম লাইন একই model-এর দুই রকম ছবি: ROC AUC ০.৯৪৯ শুনলে মনে হয় প্রায় নিখুঁত, average precision ০.৩৪১ বলে “মাঝারি, অনেক কাজ বাকি”. দ্বিতীয় লাইন AUC-এর জোড়া-অর্থের প্রমাণ: ৬০০৪৪টি (ভুয়া, সৎ) জোড়ার ৫৬৯৬৯টিতে ভুয়া অর্ডারের score বেশি, আর অনুপাতটি roc_auc_score-এর সঙ্গে চার দশমিক পর্যন্ত হুবহু এক।
তৃতীয় লাইন সবচেয়ে শিক্ষণীয়। সৎ অর্ডার কমিয়ে ৫০:৫০ করলে ROC AUC প্রায় নড়ে না (০.৯৪৮), কারণ TPR আর FPR প্রতিটি নিজের শ্রেণির ভেতরে ভাগ, তাই শ্রেণির অনুপাত বদলালেও গড়ে একই থাকে। Average precision কিন্তু ০.৩৪ থেকে ০.৯৪-এ লাফায়: একই score, একই ক্রম, শুধু হাজারো সৎ অর্ডার আর ভিড় করছে না। অর্থাৎ ROC বলে “model ক্রম কতটা ভালো সাজায়”, আর PR বলে “এই দোকানের বাস্তব অনুপাতে আটকানো তালিকা কতটা পরিষ্কার হবে”. কল-সেন্টারের জন্য দ্বিতীয় প্রশ্নটিই আসল।
Takaya Saito আর Marc Rehmsmeier ২০১৫ সালে PLOS ONE-এ প্রকাশিত একটি বহুল উদ্ধৃত প্রবন্ধে যুক্তি দেন যে অসম ডেটায় binary classifier-এর মূল্যায়নে precision–recall plot, ROC plot-এর চেয়ে বেশি তথ্যবহ; তার আগে Jesse Davis আর Mark Goadrich (২০০৬) দুই curve-এর গাণিতিক সম্পর্ক দেখান (এক model-এর ROC আরেকটির ওপরে থাকলে PR-ও ওপরে থাকে)। তবু ROC বাতিল নয়: এটি prevalence-নিরপেক্ষ, তাই ভিন্ন অনুপাতের ডেটায় তুলনায় সুবিধাজনক। দুটিই দেখুন, আর রিপোর্টে prevalence লিখুন।
৮ · একাধিক শ্রেণি আর সংখ্যার উত্তর — macro, micro, weighted; MAE, RMSE, R², MAPE
Multiclass: প্রতিটি শ্রেণির জন্য একটি করে confusion matrix
শ্রেণি তিন বা তার বেশি হলে (multi-class classification) প্রতিটি শ্রেণিকে পালা করে “positive” ধরে বাকিগুলোকে একসঙ্গে “negative” ধরা হয় (one-vs-rest). তাতে প্রতিটি শ্রেণির নিজের precision, recall, F1. সেগুলো এক সংখ্যায় আনার তিনটি উপায়:
- Macro average: প্রতিটি শ্রেণির F1-এর সাধারণ গড়। ১২টি উদাহরণের শ্রেণিও ৭১টির শ্রেণির সমান ওজন পায়; ছোট শ্রেণিতে ব্যর্থতা সরাসরি দেখা যায়।
- Micro average: সব শ্রেণির TP, FP, FN আগে যোগ, তারপর একবার হিসাব। প্রতিটি উদাহরণ সমান ওজন পায়। প্রতি উদাহরণে ঠিক একটি label হলে মোট FP = মোট FN, তাই micro precision = micro recall = micro F1 = accuracy.
- Weighted average: প্রতিটি শ্রেণির F1-কে সেই শ্রেণির উদাহরণ-সংখ্যা (support) দিয়ে ওজন দিয়ে গড়; বড় শ্রেণির প্রভাব বেশি।
পার্থক্যটি দেখাতে scikit-learn-এর সঙ্গে আসা load_wine ডেটাসেট নেওয়া হয়েছে (UCI-এর Wine ডেটা: তিন জাতের ১৭৮টি ওয়াইনের রাসায়নিক মাপ)। ইচ্ছা করে দুটি কাজ: তৃতীয় শ্রেণির মাত্র ১২টি উদাহরণ রাখা (অসম ডেটা বানাতে), আর শুধু দুটি feature (alcohol আর malic_acid) দেওয়া, যাতে model দুর্বল হয়। ফলাফল দেখায় classification_report:
import numpy as np, sklearn
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, f1_score, accuracy_score
X, y = load_wine(return_X_y=True) # UCI Wine, scikit-learn-এর সঙ্গে আসে
keep = np.r_[np.flatnonzero(y != 2), np.flatnonzero(y == 2)[:12]] # শ্রেণি 2 ইচ্ছা করে কমানো
X, y = X[keep][:, [0, 1]], y[keep] # শুধু alcohol আর malic_acid
print("scikit-learn", sklearn.__version__, "| শ্রেণির গোনা:", np.bincount(y))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42)
pred = LogisticRegression(max_iter=1000).fit(Xtr, ytr).predict(Xte)
print(classification_report(yte, pred, digits=3, zero_division=0))
for avg in ("macro", "micro", "weighted"):
print(f"F1 {avg:8} {f1_score(yte, pred, average=avg, zero_division=0):.3f}")
print(f"accuracy {accuracy_score(yte, pred):.3f}")সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0, NumPy 2.5.4):
~/bazarghar-ai$ python multiclass.py
scikit-learn 1.8.0 | শ্রেণির গোনা: [59 71 12]
precision recall f1-score support
0 0.762 0.889 0.821 18
1 0.773 0.810 0.791 21
2 0.000 0.000 0.000 4
accuracy 0.767 43
macro avg 0.512 0.566 0.537 43
weighted avg 0.696 0.767 0.730 43
F1 macro 0.537
F1 micro 0.767
F1 weighted 0.730
accuracy 0.767
শ্রেণি ২-এর ৪টি test উদাহরণের একটিও ঠিক হয়নি: precision, recall, F1 সব ০. Micro F1 (০.৭৬৭) হুবহু accuracy-র সমান, আর তা এই ব্যর্থতা প্রায় লুকিয়ে ফেলে; weighted (০.৭৩০) একটু কম, কারণ শ্রেণি ২-এর ওজন মাত্র ৪/৪৩. Macro (০.৫৩৭) চিৎকার করে বলে একটি শ্রেণি পুরো বাদ পড়ছে। classification_report-এ আলাদা “micro avg” সারি নেই, কারণ এখানে সেটি accuracy-র সারির সমান। ছোট শ্রেণিটি গুরুত্বপূর্ণ হলে macro দেখুন। Binary সমস্যায় অবশ্য average="macro" সৎ শ্রেণির প্রায় নিখুঁত F1 গড়ে ঢুকিয়ে ভুয়ার দুর্বলতা ঢেকে দেয়। ৪৩টির test set বলে সংখ্যাগুলো নড়বড়ে।
Regression-এর মাপ, সংক্ষেপে
উত্তর একটি সংখ্যা হলে ভুল ei = yi − ŷi (আসল বিয়োগ অনুমান) থেকে মাপ; model আর residual পর্ব ৫৮-এ। মাপগুলো:
- MAE = (1/n) ∑ |ei|: গড়ে কত ঘণ্টা ভুল। একই এককে, সহজে বোঝানো যায়।
- RMSE = √((1/n) ∑ ei²), অর্থাৎ MSE-এর বর্গমূল: বড় ভুলকে বেশি শাস্তি দেয়। ভুল [১, ১, ১, ১, ১৬]-এ MAE ৪.০, কিন্তু RMSE ৭.২১.
- R² = 1 − ∑ ei² / ∑ (yi − ȳ)²: “সবসময় গড় বলা” baseline-এর তুলনায় বর্গ ভুল কত ভাগ কমল। ১ নিখুঁত, ০ মানে গড়ের সমান, আর নতুন ডেটায় ঋণাত্মকও হতে পারে।
- MAPE = (1/n) ∑ |ei / yi|: শতাংশের ভুল। ব্যবসায় জনপ্রিয়, কিন্তু আসল মান শূন্যের কাছে হলে ভাগফল বিস্ফোরিত হয়।
rng = np.random.default_rng(42)
n = 400
km = rng.uniform(1, 25, n); kg = rng.uniform(0.2, 10, n)
hours = 4 + 0.9 * km + 0.6 * kg + rng.normal(0, 1.5, n) # কাল্পনিক ডেলিভারির সময়
X = np.column_stack([km, kg])
Xtr, Xte, ytr, yte = train_test_split(X, hours, test_size=0.25, random_state=42)
yp = LinearRegression().fit(Xtr, ytr).predict(Xte)
print("scikit-learn", sklearn.__version__)
print(f"delivery: MAE {mean_absolute_error(yte, yp):.2f} h | RMSE {root_mean_squared_error(yte, yp):.2f} h"
f" | R2 {r2_score(yte, yp):.3f} | MAPE {mean_absolute_percentage_error(yte, yp):.3f}")
print(f"baseline (train mean): MAE {mean_absolute_error(yte, np.full_like(yte, ytr.mean())):.2f} h"
f" | R2 {r2_score(yte, np.full_like(yte, ytr.mean())):.3f}")
returns_true = np.array([0, 1, 2, 10, 20]) # দিনে ফেরত আসা পণ্য
returns_pred = np.array([1, 1, 3, 11, 19])
print("returns: MAE", mean_absolute_error(returns_true, returns_pred),
"| MAPE", mean_absolute_percentage_error(returns_true, returns_pred))
print("without the zero day: MAPE",
round(mean_absolute_percentage_error(returns_true[1:], returns_pred[1:]), 3))
err = np.array([1, 1, 1, 1, 16]) # একটি বড় ভুল
print("errors", err.tolist(), "| MAE", err.mean(), "| RMSE", round(float(np.sqrt((err**2).mean())), 2))সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0; import-এর লাইনগুলো বাদ দেখানো):
~/bazarghar-ai$ python regression.py
scikit-learn 1.8.0
delivery: MAE 1.09 h | RMSE 1.43 h | R2 0.956 | MAPE 0.070
baseline (train mean): MAE 5.76 h | R2 -0.020
returns: MAE 0.8 | MAPE 900719925474099.2
without the zero day: MAPE 0.163
errors [1, 1, 1, 1, 16] | MAE 4.0 | RMSE 7.21
ডেলিভারির model গড়ে ১.০৯ ঘণ্টা ভুল করে, আর train-এর গড় বলা baseline ৫.৭৬; R² ০.৯৫৬. Baseline-এর R² সামান্য ঋণাত্মক (−০.০২০), কারণ train-এর গড় test-এর গড়ের সমান নয়। ফেরতের উদাহরণে MAE মাত্র ০.৮, অথচ MAPE প্রায় ৯ × ১০১৪: প্রথম দিনে আসল মান ০, আর scikit-learn শূন্য দিয়ে ভাগ এড়াতে হরে খুব ছোট একটি সংখ্যা (float64-এর machine epsilon, প্রায় ২.২ × ১০−১৬) বসায়, তাই একটি মাত্র ১-এর ভুল আকাশছোঁয়া শতাংশ হয়। সেই দিনটি বাদ দিলে MAPE ০.১৬৩. যে লক্ষ্য শূন্য ছুঁতে পারে (ফেরত, লাভ-ক্ষতি, বৃষ্টির পরিমাণ), সেখানে MAPE ব্যবহার করবেন না; MAE বা মোট ভুলকে মোট আসল দিয়ে ভাগ করা মাপ নিন।
৯ · Metric নিজেও অনিশ্চিত — bootstrap CI, দুই model-এর ন্যায্য তুলনা, আর metric লক্ষ্য নয়
F1 = ০.৩৭ একটি নমুনার সংখ্যা। Test set বদলালে এটি বদলাবে, আর আমাদের test-এ মাত্র ৩৪টি ভুয়া, তাই একটি ভুয়া এদিক-ওদিক হলেই recall প্রায় ০.০৩ নড়ে। কতটা নড়তে পারে, তা মাপার একটি সহজ পথ bootstrap: test set থেকেই ফেরত রেখে (sampling with replacement) একই আকারের অনেক নতুন নমুনা তোলা, প্রতিটিতে metric হিসাব করা, আর সেই হাজারো মানের মাঝের ৯৫% নেওয়া। এটি একটি percentile confidence interval; পর্ব ৩০-এ seaborn-এর error bar-এর পেছনে এই কৌশলই দেখেছেন, আর “৯৫%”-এর সঠিক অর্থ পর্ব ৩৭-এ। Bradley Efron ১৯৭৯ সালে bootstrap প্রস্তাব করেন।
দুই model-এর তুলনাও এভাবে ন্যায্য করা যায়, তিনটি শর্তে: (১) একই test set, (২) একই operating point নেওয়ার একই নিয়ম, আর (৩) প্রতিটি bootstrap নমুনায় দুই model-কে একসঙ্গে মাপা, যাতে পার্থক্যের অনিশ্চয়তা পাওয়া যায়; একে বলে paired comparison. নিচে A = আগের logistic regression, B = একটি Gaussian Naive Bayes (পর্ব ৫১-এর পরিবারের, সংখ্যার feature-এর জন্য)। ধরি কল-সেন্টার দিনে বড়জোর ৩% অর্ডার যাচাই করতে পারে; তাই দুই model-ই নিজের সবচেয়ে সন্দেহজনক ৫৪টি অর্ডার আটকায়। এতে threshold বাছার সুবিধা কোনো model পায় না।
import numpy as np
from sklearn.naive_bayes import GaussianNB
from sklearn.metrics import f1_score
k = 54 # দুই model-ই নিজের সবচেয়ে সন্দেহজনক ৩% (৫৪টি) অর্ডার আটকায়
def top_k(s):
flag = np.zeros(len(s), dtype=bool)
flag[np.argsort(-s, kind="stable")[:k]] = True
return flag
pB = GaussianNB().fit(Xtr, ytr).predict_proba(Xte)[:, 1]
yA, yB = top_k(p), top_k(pB) # A = logistic regression, B = Gaussian NB
print(f"F1: A {f1_score(yte, yA):.3f} | B {f1_score(yte, yB):.3f}")
def f1_rows(Y, Q): # প্রতিটি সারি একটি bootstrap নমুনা
tp = (Y & Q).sum(1); fp = (~Y & Q).sum(1); fn = (Y & ~Q).sum(1)
return 2 * tp / np.maximum(2 * tp + fp + fn, 1)
rng = np.random.default_rng(7)
idx = rng.integers(0, len(yte), size=(2000, len(yte))) # ফেরত রেখে ১৮০০টি তোলা
Y = yte.astype(bool)[idx]
fA, fB = f1_rows(Y, yA[idx]), f1_rows(Y, yB[idx]) # একই নমুনায় দুই model
for name, f in (("A", fA), ("B", fB), ("A − B", fA - fB)):
lo, hi = np.percentile(f, [2.5, 97.5])
print(f"{name:6} গড় {f.mean():+.3f} 95% CI [{lo:+.3f}, {hi:+.3f}]")
print(f"B সমান বা এগিয়ে: {np.mean(fB >= fA):.1%} নমুনায়")
print("নমুনাপ্রতি ভুয়া অর্ডার:", Y.sum(1).min(), "থেকে", Y.sum(1).max())আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):
F1: A 0.364 | B 0.318
A গড় +0.361 95% CI [+0.225, +0.485]
B গড় +0.314 95% CI [+0.182, +0.436]
A − B গড় +0.047 95% CI [-0.039, +0.134]
B সমান বা এগিয়ে: 14.4% নমুনায়
নমুনাপ্রতি ভুয়া অর্ডার: 17 থেকে 55
A-এর F1 ০.৩৬৪, B-এর ০.৩১৮; একটি সংখ্যা দেখলে A-কে বিজয়ী ঘোষণা করতে ইচ্ছা হবে। কিন্তু A-এর একার CI প্রায় ০.২৩ থেকে ০.৪৯, প্রায় দ্বিগুণ পরিসর; মাত্র ৩৪টি ভুয়ায় এর চেয়ে নিশ্চিত হওয়া যায় না (শেষ লাইন দেখায় নমুনাপ্রতি ভুয়ার সংখ্যাই ১৭ থেকে ৫৫ নড়েছে)। জোড়া পার্থক্যের CI [−০.০৩৯, +০.১৩৪] শূন্যকে ধরে, আর প্রায় ১৪% নমুনায় B সমান বা ভালো। সৎ বাক্য তাই: “A সম্ভবত একটু ভালো, তবে এই test set-এ পার্থক্য নিশ্চিত নয়; আরও ডেটা লাগবে”. সীমা: bootstrap ধরে নেয় test set বাস্তবের প্রতিনিধি, আর training-এর এলোমেলোতা (অন্য seed-এ অন্য model) এতে ধরা পড়ে না।
Metric লক্ষ্য নয়, মাপ
একটি metric ভালো হলেই কাজ ভালো হলো, এমন নয়। প্রথমত, সামগ্রিক সংখ্যা দলভেদে আলাদা ভুল লুকাতে পারে। ধরুন নতুন অ্যাকাউন্ট বা একটি নির্দিষ্ট জেলার গ্রাহকেরা বেশি আটকাচ্ছেন; সামগ্রিক FPR ০.০৩ হলেও তাঁদের জন্য হয়তো ০.১৫. প্রতিটি গুরুত্বপূর্ণ দলের জন্য আলাদা FPR আর recall মাপা fairness যাচাইয়ের প্রথম ধাপ; পূর্ণ আলোচনা পর্ব ৭২ (দায়িত্বশীল AI-এর অভ্যাস — bias যাচাই, গোপনীয়তা আর নথিপত্র)-এ। দ্বিতীয়ত, metric-কে লক্ষ্য বানালে সেটি “খেলা” শুরু হয় (Goodhart's law): সব নতুন গ্রাহক আটকালে recall বাড়বে, দোকানের বৃদ্ধি থামবে। তৃতীয়ত, ব্যবসার প্রভাব offline metric থেকে অনুমান করা যায়, প্রমাণ হয় না; সাবধানে নকশা করা A/B পরীক্ষা আর প্রকাশের পরের পর্যবেক্ষণ লাগে, কারণ প্রতারকেরাও আচরণ বদলান।
সৎ মূল্যায়নের রিপোর্টে থাকে: prevalence আর n, একটি baseline, confusion matrix, threshold কীভাবে বাছা হয়েছে, সমস্যার উপযোগী একটি বা দুটি মাপ (এখানে PR curve, AP আর expected cost), তার CI, আর গুরুত্বপূর্ণ দলভিত্তিক ভাঙন। একটি সংখ্যা একা কখনো নয়।
১০ · AI-এর যুগে — AI assistant-এর দেওয়া metric যাচাই
AI assistant দ্রুত মূল্যায়নের কোড দেয়, আর তা সুন্দর সংখ্যাও ছাপে; ভুলগুলো চুপচাপ, শুধু সংখ্যার অর্থ ভুল।
| AI যা প্রায়ই করে | কীভাবে ধরবেন |
|---|---|
| অসম ডেটায় শুধু accuracy বলে, “৯৮% নির্ভুল” | DummyClassifier-এর accuracy, prevalence আর confusion matrix পাশে রাখুন |
roc_auc_score(y, model.predict(X)): score-এর বদলে ০/১ label দিয়ে AUC | AUC আর AP-তে predict_proba(X)[:, 1] বা decision_function লাগে |
| Training ডেটায় বা threshold বাছার ডেটাতেই চূড়ান্ত metric | Threshold validation-এ, চূড়ান্ত মাপ test-এ একবার; বা TunedThresholdClassifierCV |
পুরোনো API, যেমন mean_squared_error(..., squared=False) (1.6-এ সরানো হয়েছে) | নিজের version-এর docs; RMSE-র জন্য root_mean_squared_error |
| Confusion matrix-এর সারি-কলাম বা positive শ্রেণি উল্টে পড়ে | labels= আর pos_label= স্পষ্ট দিন, একটি ছোট উদাহরণে হাতে মেলান |
Multiclass-এ average="micro" দিয়ে “F1” বলে, যা আসলে accuracy | Macro আর প্রতি শ্রেণির সারি দেখুন |
UndefinedMetricWarning লুকাতে warnings.filterwarnings("ignore") | Warning পড়ুন: সাধারণত model কোনো positive-ই দেয়নি |
| শূন্য ছুঁতে পারে এমন লক্ষ্যে MAPE | আসল মানের সর্বনিম্ন দেখুন; MAE নিন |
নির্দিষ্ট করে চান, আর এমন আউটপুট চান যা আপনি যাচাই করতে পারবেন। আসল গ্রাহকের অর্ডার বা ফোন নম্বর কখনো পেস্ট করবেন না; কলামের নাম আর কয়েকটি বানানো সারিই যথেষ্ট।
AI-এর দেওয়া মূল্যায়ন হাতে পেলে — যাচাইয়ের চেকলিস্ট
আরও গভীরে: David Hand ২০০৯ সালে Machine Learning জার্নালে যুক্তি দেন যে AUC আসলে ভুলের দামের একটি এমন বণ্টন ধরে নেয় যা model ভেদে বদলায়, তাই ভিন্ন model তুলনায় এটি অসংগত হতে পারে; তিনি H-measure প্রস্তাব করেন। Score-এর সম্ভাবনা হিসেবে মান যাচাইয়ে Brier score আর reliability diagram লাগে (পর্ব ৫৯-এর calibration-এর কথা)। Davide Chicco আর Giuseppe Jurman ২০২০ সালে binary মূল্যায়নে F1 আর accuracy-র বদলে Matthews correlation coefficient (MCC) ব্যবহারের পক্ষে যুক্তি দেন, কারণ এটি চার ঘরই দেখে; scikit-learn-এ matthews_corrcoef.
পরীক্ষা ও ইন্টারভিউতে:
- TN = ৯৫০, FP = ৩০, FN = ৫, TP = ১৫: precision আর recall? ১৫/৪৫ ≈ ০.৩৩৩ আর ১৫/২০ = ০.৭৫; F1 = ৩০/৬৫ ≈ ০.৪৬২.
- অসম ডেটায় accuracy কেন বিভ্রান্তিকর? TN প্রায় পুরোটা ভরে রাখে; সবাইকে negative বললেও accuracy ≈ 1 − prevalence.
- F1-এ harmonic mean কেন? দুর্বল মানটির দিকে টানে; P = ০.৯, R = ০.১-এ F1 ০.১৮, সাধারণ গড় ০.৫.
- AUC = ০.৮ মানে কী? একটি এলোমেলো positive একটি এলোমেলো negative-এর চেয়ে বেশি score পাওয়ার সম্ভাবনা ০.৮.
- দাম CFN = ৯০০, CFP = ১০০ হলে calibrated সম্ভাবনায় সর্বোত্তম threshold? ১০০/(১০০ + ৯০০) = ০.১.
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১ROC AUC এত জনপ্রিয় কেন, আর কোন পরিস্থিতিতে এটি ভুল পথে নিয়ে যেতে পারে?
AUC-এর জনপ্রিয়তার পেছনে কয়েকটি সত্যিকারের গুণ আছে। এটি threshold বাছার আগেই model-এর score কতটা ভালো ক্রম সাজায় তা এক সংখ্যায় বলে; এর একটি পরিষ্কার সম্ভাবনার অর্থ আছে (এলোমেলো positive এলোমেলো negative-কে হারানোর সম্ভাবনা, যা Hanley আর McNeil ১৯৮২ সালে রেডিওলজিতে জনপ্রিয় করেন); আর TPR ও FPR প্রতিটি নিজের শ্রেণির ভেতরে ভাগ বলে এটি prevalence বদলালে মোটামুটি স্থির থাকে। রাডার আর signal detection থেকে চিকিৎসা হয়ে machine learning-এ আসার দীর্ঘ ইতিহাসও এর পরিচিতি বাড়িয়েছে।
কিন্তু এই গুণগুলোই কখনো দুর্বলতা। প্রথমত, ভারী imbalance-এ ROC-এর বেশিরভাগ ক্ষেত্রফল এমন অঞ্চল থেকে আসে যেখানে কেউ কাজ করবে না। বাজারঘরের model-এর AUC ০.৯৪৯, কিন্তু average precision ০.৩৪১; FPR ০.০৩৬ শুনতে নগণ্য, অথচ তখন আটকানো তালিকার চার ভাগের তিন ভাগ সৎ গ্রাহক। Saito আর Rehmsmeier ২০১৫ সালে এই কারণেই অসম ডেটায় precision–recall plot-এর পক্ষে যুক্তি দেন। দ্বিতীয়ত, AUC সব threshold-এর গড়, অথচ বাস্তবে model চলে একটি threshold-এ; দুটি model-এর curve পরস্পরকে ছেদ করলে বেশি AUC-ওয়ালাটি আপনার আসল operating point-এ খারাপও হতে পারে। এজন্য কেউ কেউ partial AUC (শুধু কম FPR-এর অংশ) ব্যবহার করেন। তৃতীয়ত, David Hand ২০০৯ সালে দেখান যে AUC-কে খরচের ভাষায় পড়লে এটি এমন একটি খরচ-বণ্টন ধরে নেয় যা model-ভেদে বদলায়, তাই দুই model তুলনায় এটি নীতিগতভাবে অসংগত হতে পারে। চতুর্থত, AUC calibration দেখে না: সব score অর্ধেক করলেও AUC একই, অথচ খরচ-ভিত্তিক threshold তখন পুরো ভুল জায়গায় পড়বে।
মূল উপলব্ধি: AUC ক্রম সাজানোর ক্ষমতার একটি ভালো প্রথম মাপ, কিন্তু সিদ্ধান্তের মাপ নয়; অসম ডেটায় PR curve আর AP, আসল operating point-এর precision ও recall, আর খরচ পাশে না রেখে শুধু AUC দিয়ে model বাছবেন না।
প্র ০২দুর্লভ ঘটনা খোঁজার প্রায় সব কাজে (জালিয়াতি, রোগ, spam) “বেশি recall” আর “কম false alarm” একসঙ্গে পাওয়া কঠিন কেন? Base rate এখানে কী ভূমিকা রাখে?
পর্ব ৩৫ আর পর্ব ৫০-এর হিসাবটি মনে করুন। একটি test-এর sensitivity ৯৯% আর specificity ৯৯% হলেও, যে অবস্থা ১০০০ জনে ১ জনের, তার positive ফলের মাত্র প্রায় ৯% সত্যি। কারণ ৯৯৯ জন সুস্থের ১% (প্রায় ১০ জন) ভুল positive হন, আর আসল রোগী মাত্র ১ জন। এটিই precision-এর prevalence-নির্ভরতা: precision = P(positive | আটকানো), আর Bayes-এর নিয়মে এটি prior, অর্থাৎ base rate, দিয়ে গুণ হয়। Specificity একটু কমলেই, বিপুল negative-এর কারণে FP-এর সংখ্যা লাফিয়ে বাড়ে।
চিকিৎসায় এই সমস্যা পুরোনো। Screening test-এর আলোচনায় দীর্ঘদিন ধরে বলা হয় যে দুর্লভ রোগে সবাইকে পরীক্ষা করলে অনেক মানুষ অপ্রয়োজনীয় দুশ্চিন্তা আর পরের পরীক্ষার ভার বহন করেন; তাই অনেক ক্ষেত্রে ঝুঁকিপূর্ণ দলেই screening সীমিত করা হয়, যাতে প্রভাবিত জনগোষ্ঠীর base rate বাড়ে। জালিয়াতি ধরায় একই কৌশল: সব অর্ডারে একটি model চালানোর বদলে আগে সহজ নিয়মে (যেমন দাম ৳৫০০০-এর বেশি, নতুন অ্যাকাউন্ট) একটি ছোট দল আলাদা করলে সেই দলে ভুয়ার ভাগ বেশি, আর একই model-এর precision ভালো দেখায়। বাজারঘরের পরীক্ষায় একই score নিয়ে শুধু সৎ অর্ডার কমিয়ে ৫০:৫০ করতেই AP ০.৩৪ থেকে ০.৯৪ হয়েছিল; model একই, base rate আলাদা।
ব্যবহারিক শিক্ষা তিনটি। এক, precision রিপোর্ট করলে prevalence-ও লিখুন, নইলে অন্য ডেটায় তুলনা অর্থহীন। দুই, model প্রকাশের পর prevalence বদলালে (উৎসবের মৌসুমে অর্ডার বাড়ল, প্রতারকেরা কৌশল বদলালেন) precision আপনা থেকে বদলাবে, তাই নিয়মিত পর্যবেক্ষণ লাগে। তিন, দুই ধাপের নকশা (সস্তা ছাঁকনি, তারপর মানুষের যাচাই) প্রায়ই একটি “নিখুঁত” model খোঁজার চেয়ে বাস্তবসম্মত।
মূল উপলব্ধি: দুর্লভ positive-এ সমস্যাটি model-এর নয়, গণিতের; specificity-র সামান্য ঘাটতিও বিপুল negative-এ গুণ হয়ে precision খেয়ে ফেলে, তাই base rate জেনে, প্রয়োজনে দল ছোট করে, আর মানুষের যাচাই রেখে নকশা করুন।
প্র ০৩Threshold বাছতে F1 সর্বোচ্চ করবেন, নাকি টাকার খরচ সর্বনিম্ন? খরচগুলো কে ঠিক করবেন, আর তাতে কী ঝুঁকি আছে?
F1 একটি সুবিধাজনক কিন্তু নির্বিচার বাছাই: এটি ধরে নেয় precision আর recall সমান গুরুত্বের, আর TN-এর কোনো মূল্য নেই। বাজারঘরের হিসাবে F1-সেরা threshold ০.১৬ দিয়েছিল প্রতি ১০০০ অর্ডারে ৳১৬,৪৪৪ খরচ, আর খরচ-সেরা ০.০৬ দিয়েছিল ৳১৩,৫০০; প্রায় ১৮% পার্থক্য, শুধু মাপ বদলে। Charles Elkan ২০০১ সালের “The Foundations of Cost-Sensitive Learning” প্রবন্ধে দেখান, calibrated সম্ভাবনা থাকলে খরচের অনুপাত থেকেই সর্বোত্তম threshold সরাসরি বের হয়, CFP/(CFP + CFN), আর অসম ডেটায় নতুন করে training না করেও শুধু সীমা সরিয়ে অনেকটা কাজ হয়। এটি পর্ব ৫৪-এর expected utility-র সরাসরি প্রয়োগ।
কিন্তু খরচের সংখ্যা নিজেই একটি মূল্যবোধের সিদ্ধান্ত। “একজন সৎ গ্রাহককে আটকানো = ৳১০০” লেখা সহজ, কিন্তু সেই গ্রাহক যদি অপমানিত বোধ করে আর কখনো না ফেরেন, বা সামাজিক মাধ্যমে অভিযোগ লেখেন? আর যদি একটি নির্দিষ্ট এলাকার বা নতুন গ্রাহকেরা বারবার আটকান, তাহলে ক্ষতিটি সমানভাবে ভাগ হচ্ছে না; গড় খরচ কম হলেও কিছু মানুষ নিয়মিত বেশি দাম দিচ্ছেন। তাই খরচ ঠিক করা শুধু ডেটা দলের কাজ নয়; অর্থ বিভাগ, গ্রাহকসেবা আর সম্ভব হলে প্রভাবিত গ্রাহকদের দৃষ্টিভঙ্গিও লাগে। দাম অনিশ্চিত হলে sensitivity analysis করুন: CFP ৳৫০ থেকে ৳৩০০ বদলে দেখুন সেরা threshold কতটা নড়ে।
আরেকটি ঝুঁকি calibration. খরচ-সূত্র তখনই কাজ করে যখন score সত্যিকারের সম্ভাবনা; Naive Bayes-এর মতো অতিরিক্ত আত্মবিশ্বাসী model-এ সূত্রের threshold ভুল জায়গায় পড়বে। তখন validation ডেটায় সরাসরি খরচ সর্বনিম্ন করা নিরাপদ।
মূল উপলব্ধি: threshold একটি ব্যবসায়িক ও নৈতিক সিদ্ধান্ত যা একটি সংখ্যার আকারে আসে; খরচগুলো স্পষ্ট লিখুন, কে কোন দাম দিচ্ছেন তা দলভেদে দেখুন, আর validation ডেটায় বাছুন।
প্র ০৪দুটি দলের জন্য একই সঙ্গে “ন্যায্য” metric পাওয়া কি সবসময় সম্ভব? COMPAS বিতর্ক এ ব্যাপারে কী শেখায়?
২০১৬ সালে সাংবাদিকতা প্রতিষ্ঠান ProPublica যুক্তরাষ্ট্রের আদালতে ব্যবহৃত COMPAS নামের একটি পুনরপরাধ-ঝুঁকির score বিশ্লেষণ করে লেখে যে কৃষ্ণাঙ্গ আসামিদের মধ্যে যাঁরা পরে আর অপরাধ করেননি, তাঁদের “উচ্চ ঝুঁকি” বলার হার (অর্থাৎ false positive rate) শ্বেতাঙ্গদের তুলনায় অনেক বেশি ছিল। নির্মাতা প্রতিষ্ঠান পাল্টা যুক্তি দেয় যে একই score-এ দুই দলের আসল পুনরপরাধের হার প্রায় সমান, অর্থাৎ score দুই দলেই calibrated, আর precision-ও তুলনীয়। দুই পক্ষ দুটি ভিন্ন metric-এ “ন্যায্যতা” মাপছিল।
পরের বছরগুলোতে Alexandra Chouldechova (২০১৭) আর Jon Kleinberg, Sendhil Mullainathan ও Manish Raghavan (২০১৬ সালে প্রকাশিত কাজ) গাণিতিকভাবে দেখান যে দুই দলের base rate আলাদা হলে, নিখুঁত model-এর মতো ব্যতিক্রম ছাড়া, calibration আর দুই দলে সমান FPR ও FNR একসঙ্গে পাওয়া সম্ভব নয়। এটি এই পর্বের হিসাব থেকেই আসে: precision prevalence-এর ওপর নির্ভর করে, তাই একই TPR আর FPR দুই ভিন্ন base rate-এ ভিন্ন precision দেবে, আর উল্টোটাও। “কোন metric-এ সমতা চাই” প্রশ্নটি তাই প্রযুক্তিগত নয়, মূল্যবোধের: ভুল অভিযুক্ত হওয়ার বোঝা সমান ভাগ করা বেশি জরুরি, নাকি score-এর অর্থ সবার জন্য এক থাকা?
বাজারঘরে ঝুঁকি অনেক কম, কিন্তু কাঠামো একই। যদি নতুন অ্যাকাউন্টে ভুয়ার ভাগ বেশি হয়, তাহলে একটি calibrated model স্বাভাবিকভাবেই নতুন গ্রাহকদের বেশি আটকাবে, আর তাঁদের মধ্যে সৎ গ্রাহকের FPR বেশি হবে। দোকান হয়তো সিদ্ধান্ত নেবে যে নতুন গ্রাহকদের জন্য আটকানোর বদলে আগাম পরিশোধের বিকল্প দেওয়া কম ক্ষতিকর। এমন সিদ্ধান্তের জন্য প্রতিটি দলের confusion matrix আলাদা করে দেখা প্রথম ধাপ; বিস্তারিত পর্ব ৭২-এ, আর আরও গভীরে একটি ভবিষ্যৎ AI Ethics কোর্সে।
মূল উপলব্ধি: base rate আলাদা হলে সব ন্যায্যতার metric একসঙ্গে সমান হয় না; কোনটি বাছবেন তা স্পষ্ট মূল্যবোধের সিদ্ধান্ত, আর তা নিতে হলে আগে প্রতিটি দলের ভুল আলাদা করে মাপতে হবে।
প্র ০৫একই test set বারবার ব্যবহার করলে কী হয়? দুই model-এর “০.০৫ বেশি” কখন বিশ্বাস করবেন?
প্রতিবার test set দেখে কিছু বদলালে (একটি feature, একটি threshold, একটি hyperparameter) test set একটু একটু করে training-এর অংশ হয়ে যায়। একশোটি ধারণা চেষ্টা করে test-এ সেরাটি রাখলে, সেটি সত্যিই ভালো বলে নয়, ভাগ্যের জোরেও জিততে পারে; ফলে রিপোর্ট করা সংখ্যা আশাবাদী। Machine learning প্রতিযোগিতার public leaderboard-এ এটি পরিচিত সমস্যা, যেখানে প্রকাশ্য তালিকার সেরা দল গোপন চূড়ান্ত test-এ প্রায়ই নিচে নামে। ২০১৯ সালে Benjamin Recht ও সহকর্মীরা CIFAR-10 আর ImageNet-এর জন্য মূল প্রক্রিয়া অনুসরণ করে নতুন test set বানিয়ে দেখান যে বহু প্রকাশিত model-এর accuracy উল্লেখযোগ্যভাবে কমে, যদিও model-গুলোর পারস্পরিক ক্রম মোটামুটি টিকে থাকে; তাঁদের ব্যাখ্যায় এর বড় অংশ ডেটার সূক্ষ্ম বণ্টন-পার্থক্য, শুধু বারবার ব্যবহারের ফল নয়।
দুই model-এর পার্থক্য বিশ্বাস করার আগে তিনটি প্রশ্ন। এক, test set কত বড়, বিশেষ করে positive কতগুলো? বাজারঘরে ৩৪টি ভুয়ায় F1-এর CI প্রায় ০.২৬ চওড়া; ০.০৫-এর পার্থক্য সেখানে গোলমালের মধ্যেই। দুই, তুলনাটি কি জোড়া ধরে? একই উদাহরণে দুই model মাপলে উদাহরণের কঠিনতার প্রভাব বাদ যায়, তাই পার্থক্যের CI আলাদা দুটি CI-এর চেয়ে সরু হয়। Thomas Dietterich ১৯৯৮ সালে classifier তুলনার পরিসংখ্যানিক পরীক্ষা নিয়ে একটি পরিচিত প্রবন্ধে একই test set-এ জোড়া ভুলের জন্য McNemar test-এর মতো পদ্ধতির সুপারিশ করেন। তিন, পার্থক্যটি কি অনেক seed আর ভাগে টেকে? Bootstrap শুধু test-নমুনার অনিশ্চয়তা ধরে, training-এর এলোমেলোতা নয়; cross-validation বা কয়েকটি seed-এ পুরো pipeline চালালে সেটিও দেখা যায় (পর্ব ৬৬)।
অনেক তুলনায় multiple comparisons-এর সমস্যাও আসে (পর্ব ৩৭); তাই প্রধান metric আগেই ঠিক করুন।
মূল উপলব্ধি: test set একটি এককালীন সম্পদ; ছোট পার্থক্য বিশ্বাস করার আগে জোড়া ধরা CI, যথেষ্ট positive আর একাধিক seed দেখুন, আর সিদ্ধান্তগুলো validation ডেটায় নিন।
অনুশীলন
হাতে হিসাব, কোড, সমালোচনা আর আউটপুট অনুমান; প্রতিটির উত্তর নিচে লুকানো আছে।
-
Confusion matrix থেকে সব মাপ (হিসাব): একটি দোকানের ভুয়া-অর্ডার model ১০০০টি test অর্ডারে দিয়েছে TN = ৯৫০, FP = ৩০, FN = ৫, TP = ১৫. বের করুন: accuracy, precision, recall, specificity, FPR, F1, F2; “কেউ ভুয়া নয়” বলা baseline-এর accuracy; আর CFN = ৳১৫০০, CFP = ৳১০০ ধরে model আর baseline-এর মোট খরচ।
- Accuracy = (১৫ + ৯৫০)/১০০০ = ০.৯৬৫. Baseline সবাইকে সৎ বলে: ৯৮০/১০০০ = ০.৯৮০, model-এর চেয়ে বেশি! Accuracy paradox.
- Precision = ১৫/(১৫ + ৩০) = ১৫/৪৫ ≈ ০.৩৩৩; recall = ১৫/(১৫ + ৫) = ০.৭৫০.
- Specificity = ৯৫০/৯৮০ ≈ ০.৯৬৯; FPR = ৩০/৯৮০ ≈ ০.০৩১.
- F1 = ২ × ১৫/(২ × ১৫ + ৩০ + ৫) = ৩০/৬৫ ≈ ০.৪৬২. F2 = ৫ × ০.৩৩৩ × ০.৭৫/(৪ × ০.৩৩৩ + ০.৭৫) = ১.২৫/২.০৮৩ = ০.৬০০; recall ভালো বলে F2 বেশি।
- খরচ: model ৫ × ১৫০০ + ৩০ × ১০০ = ৳১০,৫০০; baseline ২০ × ১৫০০ = ৳৩০,০০০. কম accuracy-র model প্রায় ৳১৯,৫০০ বাঁচায়।
-
জোড়া গুনে AUC আর খরচ-সেরা threshold (কোড): আটটি অর্ডারের label
y = [1, 0, 1, 0, 0, 1, 0, 0]আর scores = [0.9, 0.7, 0.6, 0.6, 0.3, 0.2, 0.15, 0.1]. NumPy দিয়ে সব (positive, negative) জোড়া গুনে AUC বের করুন (সমান score অর্ধেক জয়),roc_auc_score-এর সঙ্গে মেলান, তারপর প্রতিটি আলাদা score-কে threshold ধরে CFN = ১৫০০, CFP = ১০০-এ খরচ ছাপান।import numpy as np from sklearn.metrics import roc_auc_score y = np.array([1, 0, 1, 0, 0, 1, 0, 0]) s = np.array([0.9, 0.7, 0.6, 0.6, 0.3, 0.2, 0.15, 0.1]) pos, neg = s[y == 1], s[y == 0] wins = (pos[:, None] > neg).sum() + 0.5 * (pos[:, None] == neg).sum() print("pairs", pos.size * neg.size, "wins", wins, "AUC", wins / (pos.size * neg.size)) print("sklearn", roc_auc_score(y, s)) for t in sorted(set(s), reverse=True): pr = s >= t fn = int((~pr & (y == 1)).sum()); fp = int((pr & (y == 0)).sum()) print(f"t {t:.2f}: FN {fn} FP {fp} cost {1500 * fn + 100 * fp}")সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0):
pairs 15 wins 10.5 AUC 0.7 sklearn 0.7 t 0.90: FN 2 FP 0 cost 3000 t 0.70: FN 2 FP 1 cost 3100 t 0.60: FN 1 FP 2 cost 1700 t 0.30: FN 1 FP 3 cost 1800 t 0.20: FN 0 FP 3 cost 300 t 0.15: FN 0 FP 4 cost 400 t 0.10: FN 0 FP 5 cost 500৩টি positive × ৫টি negative = ১৫টি জোড়া। ০.৯ পাঁচটিকেই হারায় (৫); ০.৬ হারায় ০.৩, ০.১৫, ০.১-কে (৩), ০.৬-এর সঙ্গে সমান (০.৫), ০.৭-এর কাছে হারে; ০.২ হারায় দুটিকে (২)। মোট ১০.৫/১৫ = ০.৭. খরচ-সেরা t = ০.২০: তিনটি সৎ অর্ডার আটকানোর দাম ৳৩০০, একটি ভুয়া ছাড়ার ৳১৫০০-এর চেয়ে অনেক কম। ৮টি অর্ডারে বাছা threshold অবশ্য নিজেই overfit; সত্যিকারের কাজে validation ডেটা লাগে।
-
সমালোচনা ও নতুন নকশা: একজন সহকর্মী লিখেছেন: “আমাদের নতুন fraud model-এর accuracy ৯৯.১% আর ROC AUC ০.৯৭. আমি পুরো ডেটায় ১০টি model চেষ্টা করে test set-এ সেরাটি বেছেছি, threshold-ও test-এ F1 দেখে ঠিক করেছি। পুরোনো model-এর F1 ছিল ০.৪১, নতুনটির ০.৪৪, তাই এটি স্পষ্টতই ভালো; সোমবার থেকে চালু।” কী কী সমস্যা, আর আপনি কীভাবে রিপোর্ট সাজাবেন?
- Accuracy-র অর্থ নেই baseline ছাড়া: ভুয়া ১% হলে “কেউ ভুয়া নয়” বললেই ৯৯%. Prevalence আর DummyClassifier লাগবে।
- AUC একা: অসম ডেটায় ০.৯৭ AUC-এর সঙ্গে খুব কম precision থাকতে পারে; PR curve, average precision আর আসল threshold-এর precision ও recall দেখান।
- Test set দিয়ে বাছাই: ১০টি model আর threshold দুটোই test-এ বাছা, তাই test আর নিরপেক্ষ নয়; F1 ০.৪৪ আশাবাদী। Validation বা cross-validation-এ বাছাই, test-এ একবার মাপা।
- ০.০৩-এর পার্থক্য: CI ছাড়া অর্থহীন; একই test-এ জোড়া bootstrap বা McNemar-ধরনের পরীক্ষা লাগবে।
- F1 বনাম ব্যবসা: threshold খরচ দিয়ে বাছা উচিত, আর দলভিত্তিক FPR দেখা দরকার।
নতুন রিপোর্ট: ভাগ আর prevalence; baseline; validation-এ খরচ-সেরা threshold; test-এ confusion matrix, precision, recall, AP, খরচ ও তাদের CI; জোড়া পার্থক্যের CI; দলভেদে FPR; তারপর ছোট অংশে সাবধানে চালু।
-
আউটপুট অনুমান: নিচের কোড কী ছাপবে? তৃতীয় লাইনের ক্রম কেন আলাদা?
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score y_true = [0, 0, 0, 0, 1, 1, 0, 1] y_pred = [0, 1, 0, 0, 1, 0, 0, 1] print(confusion_matrix(y_true, y_pred)) print(precision_score(y_true, y_pred), recall_score(y_true, y_pred), round(f1_score(y_true, y_pred), 3)) print(confusion_matrix(y_true, y_pred, labels=[1, 0]).ravel()) print(precision_score(y_true, [0] * 8, zero_division=0))সত্যিকারের run (ডেস্কটপ, Python 3.14.4, scikit-learn 1.8.0):
[[4 1] [1 2]] 0.6666666666666666 0.6666666666666666 0.667 [2 1 1 4] 0.0৫টি আসল ০-এর ৪টি ঠিক (TN = ৪), একটি ভুল ১ (FP = ১); ৩টি আসল ১-এর ২টি ধরা (TP = ২), একটি ছাড়া (FN = ১)। তাই [[4 1] [1 2]]. Precision = ২/৩, recall = ২/৩, F1-ও ০.৬৬৭. তৃতীয় লাইনে
labels=[1, 0]দেওয়ায় সারি-কলামের ক্রম উল্টে [[TP, FN], [FP, TN]] হয়েছে, তাইravel()দেয় ২, ১, ১, ৪, অর্থাৎ tp, fn, fp, tn; “tn, fp, fn, tp” ধরে খুললে ভুল হতো। শেষ লাইন: কাউকে positive বলা হয়নি, precision অসংজ্ঞায়িত, আরzero_division=0তাকে 0.0 বানিয়ে warning চুপ করিয়েছে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ভুয়া-অর্ডার classifier সৎভাবে মূল্যায়নের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Positive শ্রেণি ঠিক করা আর stratified ভাবে train, validation ও test-এ ভাগ করা
- DummyClassifier দিয়ে baseline আর prevalence মাপা
- Train-এ model fit করে validation-এ score বের করা
- Validation-এ খরচ বা লক্ষ্য-metric দিয়ে threshold বাছা
- Test-এ একবার confusion matrix, precision, recall, AP আর খরচ মাপা
- Bootstrap CI আর দলভিত্তিক ভুলের হার যোগ করে রিপোর্ট লেখা
১০০০টি অর্ডারের ২০টি ভুয়া। একটি model সবাইকে “সৎ” বলে। এর accuracy আর ভুয়া শ্রেণির recall কত?
- accuracy ০, recall ০
- accuracy ০.৯৮, recall ১
- accuracy ০.৯৮, recall ০
- accuracy ০.০২, recall ০
একটি model-এর precision ০.৮ আর recall ০.২. এর F1 score কত?
- ০.৫০
- ০.৩২
- ০.১৬
- ০.৪০
একটি classifier-এর ROC AUC = ০.৯. এর সবচেয়ে সঠিক ব্যাখ্যা কোনটি?
- Model ৯০% অনুমান ঠিক করে
- আটকানো উদাহরণের ৯০% সত্যিই positive
- Model-এর সম্ভাবনাগুলো ৯০% calibrated
- একটি এলোমেলো positive একটি এলোমেলো negative-এর চেয়ে বেশি score পাওয়ার সম্ভাবনা ০.৯
মাত্র ১% positive-এর একটি ডেটায় একই score-এর ROC AUC ০.৯৫ কিন্তু average precision ০.৩০. সবচেয়ে যুক্তিসঙ্গত ব্যাখ্যা কী?
- অনেক negative থাকায় অল্প FPR-ও বহু false positive, যা precision কমায় কিন্তু ROC-এ প্রায় চোখে পড়ে না
- দুটির একটি অবশ্যই ভুল হিসাব হয়েছে
- ROC AUC prevalence-এর ওপর বেশি নির্ভর করে
- AP ০.৫-এর নিচে মানে model এলোমেলোর চেয়ে খারাপ
একটি ভুয়া অর্ডার ছাড় পেলে ক্ষতি ৳৯০০, একজন সৎ গ্রাহককে আটকালে ক্ষতি ৳১০০, ঠিক সিদ্ধান্তে ক্ষতি ০. Model-এর score যদি calibrated সম্ভাবনা হয়, expected cost সর্বনিম্ন করতে কোন সীমার ওপরে আটকানো উচিত?
- ০.৫০
- ০.৯০
- ০.১০
- ০.১১১
তিন শ্রেণির একটি classifier-এ (প্রতি উদাহরণে ঠিক একটি label) সবচেয়ে ছোট শ্রেণিটির একটিও ঠিক হয় না, বাকি দুটি ভালো। কোন F1 গড় এই ব্যর্থতা সবচেয়ে স্পষ্ট দেখাবে, আর micro F1 কীসের সমান?
- Weighted; micro F1 = macro F1
- Macro; micro F1 = accuracy
- Micro; micro F1 = weighted F1
- তিনটিই সমান দেখাবে
দুটি model-কে একই test set-এ মাপা হলো: F1 ০.৩৬ আর ০.৩২. একই bootstrap নমুনায় জোড়া ধরে পার্থক্যের 95% CI এল [−০.০৪, +০.১৩]. কোন সিদ্ধান্ত সবচেয়ে সঠিক?
- প্রথম model নিশ্চিতভাবে ভালো
- দুটি model প্রমাণিতভাবে সমান
- Test set-এ threshold আবার tune করে পার্থক্য বাড়ানো উচিত
- পার্থক্য এই test set-এ অনিশ্চিত; আরও ডেটা বা অন্য বিবেচনা দরকার
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Baseline আগে: অসম ডেটায় accuracy প্রায় পুরোটা TN; “কেউ ভুয়া নয়” বলা DummyClassifier ৯৮.১%, আসল model ৯৮.৩%. Prevalence আর baseline ছাড়া কোনো সংখ্যা রিপোর্ট নয়।
- চার ঘর, চার প্রশ্ন: precision = TP/(TP + FP), recall = TP/(TP + FN), specificity = TN/(TN + FP), FPR = 1 − specificity. scikit-learn-এর ছক [[TN, FP], [FN, TP]].
- F1 আর Fβ: F1 = 2PR/(P + R) = 2TP/(2TP + FP + FN), দুর্বল মানের দিকে টানে; β > 1 হলে recall-এর গুরুত্ব বেশি।
- Threshold একটি সিদ্ধান্ত: খরচ(t) = CFN·FN + CFP·FP; calibrated score-এ t* = CFP/(CFP + CFN). বাছুন validation-এ, মাপুন test-এ একবার।
- ROC, PR আর অনিশ্চয়তা: AUC = এলোমেলো positive এলোমেলো negative-কে হারানোর সম্ভাবনা, prevalence-নিরপেক্ষ; অসম ডেটায় PR curve আর AP বেশি সৎ। প্রতিটি সংখ্যার পাশে bootstrap CI, আর তুলনা জোড়া ধরে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬১ · Overfitting, bias আর variance — regularization আর learning curveপরবর্তী পর্বBias–variance-এর simulation, learning ও validation curve, ridge আর lasso.
- AI-এর ভিত্তি আর Python for AI · কোর্স হোম৭৯টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ৫৯ · Logistic regression — সম্ভাবনা, sigmoid আর decision boundaryআগের পর্বএই পর্বের score যে model থেকে আসে, আর calibration-এর কথা।
- পর্ব ৫৬ · Train, validation আর test — data leakage-এর ফাঁদআগের ভিত্তিকেন threshold আর model test দেখে বাছা যায় না।
- পর্ব ৫১ · Naive Bayes — বাংলা SMS-কে spam না ham বলাআগের ভিত্তিConfusion matrix-এর প্রথম ব্যবহার আর log-odds-এর threshold.
- পর্ব ৩৫ · AI-এর জন্য সম্ভাবনাআগের ভিত্তিদুর্লভ জালিয়াতি, base rate আর Bayes-এর নিয়মের পরিচয়।
- পর্ব ৩৭ · ডেটার পরিসংখ্যানআগের ভিত্তিConfidence interval, hypothesis test আর multiple comparisons.
- Python · pytest দিয়ে testingপাশের ট্র্যাকমূল্যায়নের কোডকেও ছোট test দিয়ে যাচাই করার অভ্যাস।