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

Imbalanced ডেটা — resampling, SMOTE, class weight আর threshold

Imbalanced Data — Resampling, SMOTE, Class Weights and Thresholds

Taught in Bangla, this lesson handles a fictional 2% fraud dataset: PR AUC, recall at precision and expected cost, a threshold derived from a cost matrix, and the prior correction that undoes class_weight. In-browser runs implement SMOTE by hand and show how resampling before the split inflates scores; a desktop imbalanced-learn run compares pipelines and balanced random forests.

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

  • Imbalance কেন ক্ষতি করে, আর কোন metric সত্যি বলে: PR AUC, নির্দিষ্ট precision-এ recall, প্রত্যাশিত খরচ
  • Cost matrix থেকে threshold বের করা, আর কেন এটিই প্রথম চেষ্টা
  • class_weight ও sample_weight কীভাবে odds সরায়, আর prior সংশোধনের সূত্র
  • Random over- ও under-sampling, SMOTE নিজে লেখা, আর তার দুর্বলতা
  • Resampling শুধু training fold-এ, imbalanced-learn-এর Pipeline, আর কখন anomaly detection ভালো বিকল্প

AI-এর ভিত্তি কোর্সের পর্ব ৬০-এ confusion matrix, precision, recall, threshold সরানো আর ভারসাম্যহীন ডেটায় ROC বনাম PR curve এসেছে; পর্ব ৫৪-এ প্রত্যাশিত খরচে সিদ্ধান্ত। এই কোর্সের পর্ব ০২-এ cost matrix, আর পর্ব ২৬-এ calibration ও prior বদল। এই পর্ব সেখান থেকে এগোয়: বিরল শ্রেণি নিয়ে কাজের পুরো কৌশল, আর কোন কৌশল আসলে কাজ করে, তা মেপে দেখা। চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python: NumPy, scikit-learn, matplotlib) আর একই ডেটা ভাগ করে নেয়; প্রথমবার scikit-learn নামতে প্রায় ১০ সেকেন্ড লাগে।

১ · Imbalance কেন ক্ষতি করে — accuracy-র ফাঁদ

বিমানবন্দরের নিরাপত্তা তল্লাশি ভাবুন। লাখে হয়তো একজন যাত্রী নিষিদ্ধ কিছু নিয়ে আসেন। যে প্রহরী সবাইকে “ঠিক আছে” বলে ছেড়ে দেন, তাঁর “নির্ভুলতা” ৯৯.৯৯৯%, কিন্তু তিনি ঠিক সেই কাজটিই করছেন না, যার জন্য তাঁকে রাখা হয়েছে। আবার সবার ব্যাগ খুললে লাইন শেষ হবে না। আসল প্রশ্ন: কতজনকে থামাবেন, আর কাকে?

Class imbalance মানে এক শ্রেণির উদাহরণ অন্যটির চেয়ে অনেক কম। বাজারঘরের কাল্পনিক লেনদেনে প্রায় ২% জালিয়াতি; বিরল শ্রেণিকে বলে minority, বড়টিকে majority. ক্ষতি আসে তিন দিক থেকে:

১. ভুল threshold

Logistic regression বা boosting সৎ সম্ভাবনা দিতে পারে, কিন্তু ২% ভিত্তি-হারে বেশিরভাগ জালিয়াতির সম্ভাবনাও ০.৫-এর নিচে থাকে। ০.৫ সীমা ধরে নেয় দুই ভুলের দাম সমান, যা এখানে সত্য নয়।

২. কম উদাহরণ

২,০০০ সারিতে মাত্র ৩৪টি জালিয়াতি। সমস্যা অনুপাত নয়, সংখ্যা: ৩৪টি বিন্দু দিয়ে জালিয়াতির চেহারা ভালোভাবে শেখা কঠিন, আর মূল্যায়নও কাঁপে।

৩. ভুল metric

Accuracy majority-তে ডুবে যায়। ROC AUC-ও আশাবাদী, কারণ তার false positive rate-এর হর বিশাল majority (পর্ব ৪৩-এর ভাবনার প্রশ্ন ২).

লক্ষ্য করুন, এই তালিকায় “model majority-র দিকে পক্ষপাতী” কথাটি নেই। Log loss কমানো একটি model ভিত্তি-হার ঠিকই শেখে; ২% ঝুঁকির কাউকে ০.০২ বলাই সৎ উত্তর। তাই প্রথম প্রশ্ন সবসময়: সম্ভাবনা থেকে সিদ্ধান্তে যাওয়ার নিয়মটি ঠিক আছে তো? Japkowicz আর Stephen-এর ২০০২ সালের একটি পদ্ধতিগত পরীক্ষায় দেখা যায়, অনুপাত একা ততটা ক্ষতি করে না; ক্ষতি বাড়ে যখন সঙ্গে থাকে ছোট নমুনা আর শ্রেণির মিশে থাকা (overlap).

২ · কী মাপবেন — PR AUC, নির্দিষ্ট precision-এ recall, আর খরচ

তিনটি মাপ তিনটি ভিন্ন প্রশ্নের উত্তর দেয়:

মাপপ্রশ্নএলোমেলো ভিত্তিকখন
PR AUC (average precision)Score অনুযায়ী সাজালে জালিয়াতি কতটা ওপরে আসে?positive-এর অনুপাত (এখানে ≈ ০.০১৭)Model-এর ক্রম (ranking) তুলনা, threshold ছাড়া
Recall at precision ≥ ০.৮সতর্কতার ৮০% সত্যি রাখার শর্তে কত ভাগ জালিয়াতি ধরি?প্রায় ০যখন ব্যবসার শর্ত “ভুল সতর্কতা ২০%-এর বেশি নয়”
প্রত্যাশিত খরচ (টাকা)একটি নির্দিষ্ট threshold-এ মোট ক্ষতি কত?কাউকে যাচাই না করার খরচচূড়ান্ত সিদ্ধান্ত; threshold বাছাই

PR AUC আর recall at precision দুটিই precision-recall curve থেকে আসে, কিন্তু খরচ আসে সিদ্ধান্ত থেকে। একটি model PR AUC-তে ভালো হয়েও ভুল threshold-এ বেশি খরচ করতে পারে। তাই নিয়ম: model বাছুন ranking দিয়ে, threshold বাছুন খরচ দিয়ে, আর দুটিই মাপুন না-ছোঁয়া ডেটায়। F1 এখানে কম কাজের, কারণ সে precision আর recall-কে সমান গুরুত্ব দেয়, যা আপনার টাকার হিসাবের সঙ্গে মেলার কোনো কারণ নেই।

৩ · প্রথম অস্ত্র — খরচ থেকে threshold

প্রতিটি লেনদেনে দুটি সিদ্ধান্ত: ছেড়ে দেওয়া, বা হাতে যাচাই। পর্ব ০২-এর cost matrix-এ চারটি ঘর লিখুন, তারপর প্রতিটি সিদ্ধান্তের প্রত্যাশিত খরচ তুলনা করুন। Charles Elkan-এর ২০০১ সালের প্রবন্ধ The Foundations of Cost-Sensitive Learning এই যুক্তিকে সাধারণ সূত্রে বাঁধে:

p* = (CFP − CTN) / [(CFP − CTN) + (CFN − CTP)]

এখানে CFP একটি সাধারণ লেনদেন ভুলে যাচাইয়ের খরচ, CFN একটি জালিয়াতি ফসকানোর ক্ষতি, CTP আর CTN সঠিক সিদ্ধান্তের খরচ। সম্ভাবনা p ≥ p* হলে যাচাই করুন। শর্ত একটিই: p সৎ (calibrated) হতে হবে, নইলে সূত্রের সীমা ভুল জায়গায় পড়ে। এভাবে threshold সরানোকে বলে threshold moving.

C_TP = 100, C_FP = 100, C_FN = 4000, C_TN = 0 (কাল্পনিক টাকা)
ধাপ
হিসাব
সংখ্যায়
মানে
  1. দাম লিখুন। হাতে-যাচাই ১০০ টাকা, লেনদেনটি জালিয়াতি হোক বা না হোক। জালিয়াতি ফসকালে ৪,০০০ টাকা ক্ষতি। সাধারণ লেনদেন ছেড়ে দিলে খরচ ০.
  2. ছেড়ে দেওয়ার খরচ। ঝুঁকি p হলে প্রত্যাশিত ক্ষতি p × ৪,০০০; p = ০.০১ হলে ৪০ টাকা।
  3. যাচাইয়ের খরচ। দুই অবস্থাতেই ১০০ টাকা, তাই প্রত্যাশিত খরচও ১০০.
  4. সমান বিন্দু। ৪,০০০ × p* = ১০০ থেকে p* = ০.০২৫. সাধারণ সূত্রে (১০০ − ০)/[(১০০ − ০) + (৪,০০০ − ১০০)] = ১০০/৪,০০০, একই উত্তর।
  5. তিনটি লেনদেন। p = ০.০১: ছাড়লে ৪০ < ১০০, ছেড়ে দিন। p = ০.১০: ৪০০ > ১০০, যাচাই। p = ০.৩০: ১,২০০ > ১০০, যাচাই।
  6. ০.৫ কেন ভুল। ডিফল্ট সীমায় p = ০.৩০-এর লেনদেন ছাড়া পায়, অথচ তার প্রত্যাশিত ক্ষতি যাচাইয়ের ১২ গুণ। ০.৫ সীমা ঠিক তখনই, যখন দুই ধরনের ভুলের দাম সমান।

নিচের ব্লক বাজারঘরের ডেটা বানায় (৪,০০০টি লেনদেন, চারটি feature: log টাকা, অ্যাকাউন্টের বয়সের log, রাতের দিকে কতটা, দূরত্বের log), অর্ধেকে logistic regression শেখায়, আর বাকি অর্ধেকে তিনটি মাপ নেয়। পরের তিনটি ব্লক এই নামগুলোই ব্যবহার করবে।

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import average_precision_score, precision_recall_curve

def make_data(n, seed=42):                 # কাল্পনিক বাজারঘর লেনদেন, প্রায় 2% জালিয়াতি
    r = np.random.default_rng(seed)
    y = (r.random(n) < 0.02).astype(int)
    X = r.normal(0, 1, (n, 4)) + np.outer(y, [1.6, -1.4, 1.2, 0.8])
    return X * [0.6, 1.2, 1.0, 0.5] + [7, 5, 0, 1.8], y   # log টাকা, log দিন, রাত, log km

X, y = make_data(4000)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.5, stratify=y, random_state=42)
MISS, REVIEW = 4000, 100                   # কাল্পনিক টাকা: ফসকানো জালিয়াতি, একটি হাতে-যাচাই
def cost(p, y, t):                         # p ≥ t হলে হাতে যাচাই
    flag = p >= t
    return REVIEW * flag.sum() + MISS * y[~flag].sum(), flag.sum(), y[flag].sum()
def recall_at_precision(y, p, pmin=0.8):
    prec, rec, _ = precision_recall_curve(y, p)
    return rec[prec >= pmin].max()

lr = LogisticRegression(C=1e10, max_iter=1000).fit(Xtr, ytr)
p = lr.predict_proba(Xte)[:, 1]
print(f"rows {len(y)}, fraud {y.sum()} ({y.mean():.1%}); test fraud {yte.sum()}")
print(f"'always normal' accuracy {1 - yte.mean():.3f}, cost {MISS * yte.sum():,} taka")
print(f"PR AUC {average_precision_score(yte, p):.3f} (random = {yte.mean():.3f}), "
      f"recall at precision>=0.8: {recall_at_precision(yte, p):.2f}")
t_star = REVIEW / MISS                     # খরচ-সীমা: 100 / (100 + 3900)
for t in (0.5, t_star):
    c, f, caught = cost(p, yte, t)
    print(f"threshold {t:.4f}: flagged {f:3d}, caught {caught}/{yte.sum()}, cost {c:,} taka")

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

rows 4000, fraud 68 (1.7%); test fraud 34
'always normal' accuracy 0.983, cost 136,000 taka
PR AUC 0.612 (random = 0.017), recall at precision>=0.8: 0.38
threshold 0.5000: flagged  24, caught 18/34, cost 66,400 taka
threshold 0.0250: flagged 219, caught 30/34, cost 37,900 taka

Accuracy-র ফাঁদ: “সবাই স্বাভাবিক” বললে accuracy ০.৯৮৩, অথচ test-এর ৩৪টি জালিয়াতির পুরো ক্ষতি ১,৩৬,০০০ টাকা। Ranking: PR AUC ০.৬১২, এলোমেলো ভিত্তির (০.০১৭) প্রায় ৩৬ গুণ; কিন্তু সতর্কতার অন্তত ৮০% সত্যি রাখতে চাইলে জালিয়াতির মাত্র ৩৮% ধরা যায়। Threshold: একই model, একই সম্ভাবনা। ০.৫ সীমায় ২৪টি যাচাই, ১৮টি জালিয়াতি ধরা, খরচ ৬৬,৪০০ টাকা; ০.০২৫ সীমায় ২১৯টি যাচাই, ৩০টি ধরা, খরচ ৩৭,৯০০, প্রায় ৪৩% কম। কোনো resampling বা নতুন model লাগেনি। সতর্কতা: test-এ মাত্র ৩৪টি জালিয়াতি, তাই প্রতিটি সংখ্যা বেশ নড়বড়ে (পর্ব ২১-এর bootstrap দিয়ে পরিসর মাপা যায়), আর এখানে সীমা বাছা হয়নি, সূত্র থেকে এসেছে, তাই test-এ টিউনের দোষ নেই।

৪ · class_weight আর sample_weight — সম্ভাবনা কোথায় সরে

দ্বিতীয় পরিচিত কৌশল loss-এ ওজন। scikit-learn-এ class_weight="balanced" প্রতিটি শ্রেণিকে ওজন দেয় wc = n / (২ · nc), যেখানে n মোট সারি আর nc শ্রেণি c-এর সারি। একই কাজ সারি ধরে করা যায় fit(X, y, sample_weight=w) দিয়ে (sample weight); দুটি একসঙ্গে দিলে গুণ হয়। ওজন মানে “এই ভুলটি বেশি দামি”: জালিয়াতির প্রতিটি সারি যেন ৫৮ বার গোনা হচ্ছে।

প্রশ্ন হলো, এতে model-এর সম্ভাবনার কী হয়? উত্তরটি Bayes-এর সূত্রে লুকানো, আর এটিই পরীক্ষায় প্রিয় প্রশ্ন।

train: n = 2000, n1 = 34 জালিয়াতি, n0 = 1966 স্বাভাবিক
ধাপ
সূত্র
সংখ্যায়
মানে
  1. Bayes-এর odds. P(১│x)/P(০│x) দুই অংশের গুণফল: likelihood ratio p(x│১)/p(x│০), যা বলে x দেখতে কোন শ্রেণির মতো, আর prior odds π1/π0, যা বলে শ্রেণিটি কত সাধারণ।
  2. ওজন। n = ২,০০০ হলে w1 = ২,০০০/৬৮ = ২৯.৪১, w0 = ২,০০০/৩,৯৩২ = ০.৫০৯.
  3. নতুন prior. ওজনসহ গুনলে জালিয়াতি ৩৪ × ২৯.৪১ = ১,০০০, স্বাভাবিক ১,৯৬৬ × ০.৫০৯ = ১,০০০: model যেন ৫০-৫০ ডেটা দেখছে।
  4. Likelihood একই থাকে। একই শ্রেণির সব সারির ওজন সমান, তাই শ্রেণির ভেতরে x-এর বণ্টন বদলায় না। বদলায় শুধু prior odds, ৫৭.৮ গুণ; log-odds-এ যোগ হয় ln ৫৭.৮ = ৪.০৫৭, প্রতিটি সারিতে একই।
  5. একটি সারি। আসল ঝুঁকি ০.০২ হলে odds ০.০২০৪, গুণ ৫৭.৮ = ১.১৮০, অর্থাৎ pw = ১.১৮০/২.১৮০ = ০.৫৪১. সম্ভাবনা হিসেবে পড়লে ভয়ানক ভুল।
  6. ফেরানো। oddsw-কে n1/n0 দিয়ে গুণ করলেই আসল odds. আর ওজনসহ model-এ ০.৫ সীমা মানে আসল ঝুঁকিতে ১/(১ + ৫৭.৮) = ০.০১৭: ওজন আসলে threshold সরানোর আরেকটি পথ।

এই যুক্তি পুরো সত্য হয় যখন model-টি সঠিক রূপের (well-specified) আর ডেটা অসীম; সসীম ডেটায় আর ভুল-রূপের model-এ ঢাল-গুলোও একটু বদলায়। Prior correction নামে এই সংশোধন case-control গবেষণায় পুরোনো; rare events-এর logistic regression নিয়ে King আর Zeng-এর ২০০১ সালের প্রবন্ধ এটি জনপ্রিয় করে, আর পর্ব ২৬-এ একই সূত্র prior বদলের (label shift) জন্য এসেছে। নিচের ব্লক তত্ত্বটি সংখ্যায় যাচাই করে।

from sklearn.metrics import log_loss

n1 = ytr.sum(); n0 = len(ytr) - n1
lw = LogisticRegression(C=1e10, max_iter=1000, class_weight="balanced").fit(Xtr, ytr)
pw = lw.predict_proba(Xte)[:, 1]
logit = lambda q: np.log(q / (1 - q))
shift = logit(pw) - logit(p)               # প্রতিটি test সারিতে log-odds কতটা সরল
print(f"train {n0} normal, {n1} fraud: w1/w0 = n0/n1 = {n0 / n1:.1f}, ln = {np.log(n0 / n1):.3f}")
print(f"log-odds shift, 2,000 train rows:  mean {shift.mean():.3f}, sd {shift.std():.3f}")
Xb, yb = make_data(60000, seed=7)          # একই নিয়মে অনেক বড় ডেটা
d = [LogisticRegression(C=1e10, max_iter=1000, class_weight=cw).fit(Xb, yb)
     .decision_function(Xte) for cw in (None, "balanced")]
r = (yb == 0).sum() / yb.sum()
print(f"log-odds shift, 60,000 train rows: mean {(d[1] - d[0]).mean():.3f}, "
      f"sd {(d[1] - d[0]).std():.3f}  vs ln(n0/n1) = {np.log(r):.3f}")

odds = pw / (1 - pw) * n1 / n0             # prior সংশোধন: odds-কে n1/n0 দিয়ে গুণ
pc = odds / (1 + odds)
for name, q in (("plain", p), ("balanced", pw), ("balanced+corrected", pc)):
    print(f"{name:18s} mean p {q.mean():.4f}  log loss {log_loss(yte, q):.3f}  "
          f"PR AUC {average_precision_score(yte, q):.3f}  cost@t* {cost(q, yte, t_star)[0]:,}")
c, f, caught = cost(pw, yte, 0.5)
print(f"balanced at 0.5 (= plain p >= {n1 / len(ytr):.3f}): flagged {f}, caught {caught}, cost {c:,}")

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

train 1966 normal, 34 fraud: w1/w0 = n0/n1 = 57.8, ln = 4.057
log-odds shift, 2,000 train rows:  mean 3.752, sd 0.389
log-odds shift, 60,000 train rows: mean 3.849, sd 0.112  vs ln(n0/n1) = 3.912
plain              mean p 0.0216  log loss 0.041  PR AUC 0.612  cost@t* 37,900
balanced           mean p 0.1784  log loss 0.295  PR AUC 0.620  cost@t* 116,600
balanced+corrected mean p 0.0237  log loss 0.042  PR AUC 0.620  cost@t* 34,300
balanced at 0.5 (= plain p >= 0.017): flagged 277, caught 32, cost 35,700

ধ্রুবক সরণ: তত্ত্ব বলে প্রতিটি সারির log-odds ln(n0/n1) সরবে। ২,০০০ সারিতে গড় সরণ ৩.৭৫২ (তত্ত্ব ৪.০৫৭), sd ০.৩৮৯: মাত্র ৩৪টি জালিয়াতিতে ঢালগুলোও খানিক নড়েছে। ৬০,০০০ সারিতে সরণ ৩.৮৪৯, sd মাত্র ০.১১২, তত্ত্ব ৩.৯১২: ডেটা বাড়লে সরণ প্রায় ধ্রুবক আর তত্ত্বের কাছে। ফোলা সম্ভাবনা: balanced model-এর গড় সম্ভাবনা ০.১৭৮, আসল হারের (প্রায় ০.০১৭) দশ গুণ, log loss প্রায় সাত গুণ খারাপ (০.২৯৫); এই সম্ভাবনায় খরচের সূত্রের সীমা বসালে খরচ লাফিয়ে ১,১৬,৬০০ টাকা। সংশোধনের পর গড় ০.০২৩৭, log loss ০.০৪২, খরচ ৩৪,৩০০: আবার ব্যবহারযোগ্য। PR AUC প্রায় একই (০.৬১২ বনাম ০.৬২০), কারণ ধ্রুবক সরণ ক্রম বদলায় না; ছোট পার্থক্যটি ঢালের সামান্য বদল থেকে। শেষ লাইন: balanced model-এর ০.৫ সীমা প্রায় আসল p ≥ ০.০১৭-এর সমান, খরচ ৩৫,৭০০, ০.০২৫ সীমার কাছেই; ৩৪টি জালিয়াতিতে এই পার্থক্য এলোমেলোতার মধ্যে।

৫ · Random oversampling আর undersampling

ওজনের বদলে ডেটাই বদলানো যায়। Random oversampling: জালিয়াতির সারি এলোমেলোভাবে কপি করে সংখ্যা বাড়ানো। Random undersampling: স্বাভাবিক সারির একটি এলোমেলো অংশ ফেলে দেওয়া। দুটিই প্রত্যাশায় class weight-এর মতো prior বদলায়, তাই অংশ ৪-এর সংশোধন এখানেও খাটে।

কৌশলভালো দিকখারাপ দিক
Oversampling (কপি)কোনো তথ্য হারায় নাহুবহু কপি; গাছ বা k-NN কপিগুলো মুখস্থ করে, training সময় বাড়ে
Undersamplingডেটা ছোট, শেখা দ্রুত; খুব বড় ডেটায় কার্যকরস্বাভাবিক সারির তথ্য ফেলে দেয়; ফল seed-এ নড়ে
class_weightকোনো সারি যোগ বা বাদ নেই, নির্ধারিত (deterministic)সব estimator সমর্থন করে না; সম্ভাবনা ফোলায়
Threshold movingmodel অপরিবর্তিত, সম্ভাবনা সৎ থাকেভালো calibrated সম্ভাবনা লাগে

Undersampling-এর একটি বুদ্ধিমান রূপ হলো অনেকগুলো আলাদা undersample-এ আলাদা model শিখিয়ে ensemble বানানো, যাতে ফেলে দেওয়া তথ্য অন্য কোনো model দেখে। Chen, Liaw আর Breiman-এর ২০০৪ সালের একটি প্রতিবেদনের balanced random forest ঠিক এই ধারণা: প্রতিটি গাছ পায় একটি bootstrap নমুনা, যেখানে দুই শ্রেণি সমান (পর্ব ৩৩-এর random forest-এর আত্মীয়)।

পরিষ্কার করার undersampling (নোট): Tomek link (Ivan Tomek, ১৯৭৬) হলো দুই ভিন্ন শ্রেণির এমন দুটি বিন্দু, যারা পরস্পরের নিকটতম প্রতিবেশী; সাধারণত এর majority সদস্যটি মোছা হয়, যাতে সীমানা পরিষ্কার হয়। Edited nearest neighbours (ENN, Dennis Wilson, ১৯৭২) যে বিন্দুর বেশিরভাগ প্রতিবেশী অন্য শ্রেণির, তাকে মোছে। দুটিই শব্দ (noise) কমায়, কিন্তু সীমানার কাছের আসল কঠিন উদাহরণও মুছতে পারে; SMOTE-এর পরে এগুলো চালানো (SMOTE + Tomek, SMOTE + ENN) একটি প্রচলিত জুটি।

৬ · SMOTE — দুটি প্রতিবেশীর মাঝে নতুন বিন্দু

দুটি পাশাপাশি বাজারে আলুর দাম ৩০ আর ৩৪ টাকা হলে মাঝের বাজারে ৩২ টাকা হওয়া বিশ্বাসযোগ্য। SMOTE ঠিক এই অনুমান করে: দুটি কাছের জালিয়াতির মাঝের রেখায় থাকা লেনদেনও জালিয়াতি। অনুমানটি টেকে যতক্ষণ “মাঝের জায়গা” সত্যিই জালিয়াতির এলাকা; দুই বাজারের মাঝে যদি নদী থাকে, অনুমান ভাঙে।

SMOTE (Synthetic Minority Over-sampling Technique) প্রস্তাব করেন Nitesh Chawla, Kevin Bowyer, Lawrence Hall আর Philip Kegelmeyer, ২০০২ সালে (Journal of Artificial Intelligence Research). কপির বদলে এটি নতুন কৃত্রিম বিন্দু বানায়: একটি minority বিন্দু xi নিন, minority-র মধ্যেই তার k-টি নিকটতম প্রতিবেশী খুঁজুন (k-NN, সাধারণত k = ৫), এলোমেলো একজন প্রতিবেশী xj বাছুন, আর ০ থেকে ১-এর এলোমেলো λ নিয়ে:

xnew = xi + λ · (xj − xi)

জালিয়াতি (standardised): A(1.2, −0.8), B(2.0, −1.6), C(0.9, −0.2), D(1.5, −1.9), E(3.0, 0.5)
ধাপ
হিসাব
সংখ্যায়
মানে
  1. শুরু। জালিয়াতি A = (১.২, −০.৮) নিন। SMOTE শুধু minority-র ভেতরে প্রতিবেশী খোঁজে; আশপাশে কত স্বাভাবিক লেনদেন আছে, সে দেখেই না।
  2. দূরত্ব। A থেকে B ১.১৩১, C ০.৬৭১, D ১.১৪০, E ২.২২০ (Euclidean). বাস্তবে এই খোঁজ চলে একটি k-d tree দিয়ে, যাতে প্রতিবার সব বিন্দুর দূরত্ব মাপতে না হয়।
  3. k = ৩. সবচেয়ে কাছের তিনটি C, B আর D; E বাদ।
  4. এলোমেলো বাছাই। ধরা যাক প্রতিবেশী এল B আর λ = ০.২৫. B − A = (০.৮, −০.৮).
  5. নতুন বিন্দু। (১.২ + ০.২৫ × ০.৮, −০.৮ + ০.২৫ × (−০.৮)) = (১.৪, −১.০): A থেকে B-এর দিকে এক-চতুর্থাংশ পথে। নতুন সারিটি label পায় “জালিয়াতি”।
  6. ঝুঁকি। B যদি ভুল label-এর লেনদেন হয়, বা A আর B-এর মাঝে স্বাভাবিক লেনদেনের ঘন এলাকা থাকে, তবে সেই এলাকায় কৃত্রিম “জালিয়াতি” ভরে যায়।

নিচের ব্লক SMOTE নিজে লেখে (প্রতিবেশী খোঁজা NearestNeighbors(algorithm="kd_tree") দিয়ে), শুধু দুটি feature-এ ৩০০টি কৃত্রিম বিন্দু বানায়, আর গোনে কতগুলো বিন্দু স্বাভাবিক ভিড়ের মাঝে পড়েছে: ১০টি নিকটতম আসল সারির মধ্যে একটির বেশি জালিয়াতি নেই।

import matplotlib.pyplot as plt
from sklearn.neighbors import NearestNeighbors

def smote(Xm, n_new, k=5, seed=0):         # Xm: শুধু minority সারি
    r = np.random.default_rng(seed)
    nn = NearestNeighbors(n_neighbors=k + 1, algorithm="kd_tree").fit(Xm)
    nbr = nn.kneighbors(Xm, return_distance=False)[:, 1:]   # প্রথমটি নিজেই, বাদ
    i = r.integers(0, len(Xm), n_new)                      # এলোমেলো একটি minority বিন্দু
    j = nbr[i, r.integers(0, k, n_new)]                    # তার k প্রতিবেশীর একটি
    lam = r.random((n_new, 1))                             # λ ~ Uniform(0, 1)
    return Xm[i] + lam * (Xm[j] - Xm[i])                   # দুটির মাঝের রেখায় নতুন বিন্দু

Z = (Xtr[:, :2] - Xtr[:, :2].mean(0)) / Xtr[:, :2].std(0)  # শুধু টাকা আর বয়স, 2-D
Zm = Z[ytr == 1]
S = smote(Zm, 300)
real = NearestNeighbors(n_neighbors=10, algorithm="kd_tree").fit(Z)
def fraud_share(P):                        # 10টি নিকটতম আসল সারির কত ভাগ জালিয়াতি
    return ytr[real.kneighbors(P, return_distance=False)].mean(1)
for name, P in (("real fraud", Zm), ("synthetic", S)):  # 10 আসল প্রতিবেশীর ≤1টি জালিয়াতি
    deep = np.sum(fraud_share(P) <= 0.1)
    print(f"{name:10s} points: {len(P):3d}, inside the normal crowd: {deep:3d} ({deep / len(P):.0%})")
fig, ax = plt.subplots(figsize=(6, 4.5))
ax.scatter(*Z[ytr == 0].T, s=4, c="0.75", label="normal (real)")
ax.scatter(*S.T, s=10, c="tab:orange", marker="x", label="SMOTE synthetic")
ax.scatter(*Zm.T, s=28, c="tab:red", edgecolors="k", label="fraud (real)")
ax.set(xlabel="log amount (standardised)", ylabel="log account age (standardised)",
       title="SMOTE fills lines between fraud neighbours")
ax.legend(loc="lower left", fontsize=8)
plt.show()

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

real fraud points:  34, inside the normal crowd:  14 (41%)
synthetic  points: 300, inside the normal crowd: 136 (45%)

আসল ৩৪টি জালিয়াতির ১৪টি (৪১%) এমনিতেই স্বাভাবিক ভিড়ের মাঝে: শুধু টাকা আর বয়স দিয়ে দুই শ্রেণি অনেকটা মিশে আছে (overlap). SMOTE সেই সমস্যাকে বহুগুণ করে: ৩০০টি কৃত্রিম বিন্দুর ১৩৬টি (৪৫%) ভিড়ের মাঝে বসেছে, আর প্রতিটির label “জালিয়াতি”। ছবিতে কমলা ✗ চিহ্নগুলো লাল বিন্দুগুলোর মাঝের সরলরেখা ধরে সাজানো; ওপরের দিকের প্রায় একা লাল বিন্দুগুলো থেকে টানা রেখা ধূসর মেঘের ভেতর দিয়ে গেছে। Model এখন ওই এলাকাকে জালিয়াতির এলাকা ভাবতে শেখে, যা ভুল সতর্কতা বাড়ায়। লক্ষ করুন, এই ছবি শুধু দুটি feature-এ; চারটি feature একসঙ্গে দেখলে শ্রেণিগুলো কম মিশে থাকে।

SMOTE-এর তিন দুর্বলতা। (১) Overlap: দুই শ্রেণি মিশে থাকলে কৃত্রিম বিন্দু majority এলাকায় পড়ে, ভুল সতর্কতা বাড়ায়। (২) Noise: একটি ভুল label-এর বিন্দু তার প্রতিবেশীদের সঙ্গে রেখা টেনে শব্দকে বহুগুণ করে। (৩) উচ্চ মাত্রা: অনেক feature-এ দূরত্ব প্রায় সমান হয়ে যায় (পর্ব ৪০-এর মাত্রার অভিশাপ), “নিকটতম প্রতিবেশী” অর্থ হারায়; Blagus আর Lusa (২০১৩) উচ্চ-মাত্রার ডেটায় SMOTE-এর সীমিত সুবিধা দেখিয়েছেন। Categorical feature-এ ১.৪টি “জেলা” অর্থহীন, তাই সেখানে SMOTENC-এর মতো রূপ লাগে। Borderline-SMOTE (Han ও সহকর্মী, ২০০৫) আর ADASYN (He ও সহকর্মী, ২০০৮) সীমানার কাছের বিন্দুতে বেশি জোর দেয়, কিন্তু মূল সমস্যাগুলো থেকে যায়।

৭ · বড় ফাঁদ — ভাগ করার আগে resampling

সবচেয়ে সাধারণ, আর সবচেয়ে ক্ষতিকর ভুল: পুরো ডেটায় SMOTE বা oversampling চালিয়ে তারপর train/test বা cross-validation-এ ভাগ। তখন একটি জালিয়াতির কপি train-এ আর আসলটি test-এ থাকে, বা test-এর জালিয়াতি থেকে বানানো কৃত্রিম পড়শি train-এ ঢোকে। Model পরীক্ষার প্রশ্ন আগেই দেখে ফেলে: এটি data leakage (AI-এর ভিত্তি কোর্সের পর্ব ৫৬)।

ভুল ক্রম বনাম ঠিক ক্রম

ওপরে resampling ভাগের আগে, নিচে শুধু training fold-এর ভেতরে

Resampling before splitting leaks; resampling only inside training folds is honest ✗ ভুল সব ডেটা SMOTE / কপি fold-এ ভাগ test fold-এ জালিয়াতির কপি বা কৃত্রিম পড়শি → score ফোলা (PR AUC ০.৬৮) ✓ ঠিক সব ডেটা fold-এ ভাগ training fold: SMOTE → fit test fold: ছোঁয়া নয়, শুধু score সৎ score (PR AUC ০.৩৯–০.৪৪)
ছবি ৭.১ — Resampling হলো শেখার অংশ, ডেটা প্রস্তুতির নয়। তাই এটি fit-এর মতোই প্রতিটি training fold-এ আলাদাভাবে চলবে, আর validation বা test fold কখনো resample হবে না। বন্ধনীর সংখ্যা নিচের ব্লকের, একটি run.

নিচের ব্লক cross-validation-এর fold loop নিজে লেখে, যাতে কোথায় কী ঘটে স্পষ্ট দেখা যায়। প্রথমে ফাঁদ: পুরো ডেটায় resampling, তারপর ৪-fold CV; মাপা হয় শুধু test fold-এর আসল সারিতে, যাতে ভিত্তি-হার একই থাকে। তারপর সৎ পথ: HistGradientBoosting দিয়ে চারটি কৌশল, ৪-fold × ২ বার (repeated stratified CV), প্রতিটির PR AUC আর খরচের গড় ± sd.

from sklearn.ensemble import HistGradientBoostingClassifier as HGB
from sklearn.model_selection import StratifiedKFold

def resample(Xa, ya, how, seed):           # minority-কে majority-র সমান করা
    n_new = (ya == 0).sum() - ya.sum()
    if how == "oversample":                # পুরোনো জালিয়াতি সারির হুবহু কপি
        extra = np.random.default_rng(seed).choice(np.flatnonzero(ya), n_new)
        return np.r_[Xa, Xa[extra]], np.r_[ya, ya[extra]]
    if how == "smote":
        return np.r_[Xa, smote(Xa[ya == 1], n_new, seed=seed)], np.r_[ya, np.ones(n_new, int)]
    return Xa, ya

def cv(X, y, how, reps=2, real=None):      # নিজের হাতে লেখা repeated stratified CV
    real = np.ones(len(y), bool) if real is None else real
    out = []
    for rep in range(reps):
        for tr, te in StratifiedKFold(4, shuffle=True, random_state=rep).split(X, y):
            Xa, ya = resample(X[tr], y[tr], how, rep)       # শুধু training fold-এ
            m = HGB(max_iter=30, max_leaf_nodes=15, random_state=0,
                    class_weight="balanced" if how == "class_weight" else None)
            q = m.fit(Xa, ya).predict_proba(X[te])[:, 1]
            k = real[te]                                    # মাপা হয় শুধু আসল সারিতে
            t = t_star if how in ("none", "leak") else 0.5
            out.append((average_precision_score(y[te][k], q[k]), cost(q[k], y[te][k], t)[0]))
    return np.array(out)

for how in ("oversample", "smote"):        # ফাঁদ: ভাগ করার আগেই পুরো ডেটায় resampling
    Xl, yl = resample(X, y, how, 0)
    r = cv(Xl, yl, "leak", reps=1, real=np.arange(len(yl)) < len(y))
    print(f"LEAK {how:10s} before split: PR AUC {r[:, 0].mean():.3f} (real rows only)")
for name, how in (("none + cost threshold", "none"), ("class_weight, t=0.5", "class_weight"),
                  ("oversample in folds, t=0.5", "oversample"), ("SMOTE in folds, t=0.5", "smote")):
    r = cv(X, y, how)
    print(f"{name:26s} PR AUC {r[:, 0].mean():.3f} ± {r[:, 0].std():.3f}  "
          f"cost {r[:, 1].mean():6,.0f} ± {r[:, 1].std():5,.0f}")

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

LEAK oversample before split: PR AUC 0.681 (real rows only)
LEAK smote      before split: PR AUC 0.529 (real rows only)
none + cost threshold      PR AUC 0.440 ± 0.100  cost 27,950 ± 4,805
class_weight, t=0.5        PR AUC 0.404 ± 0.058  cost 29,975 ± 5,119
oversample in folds, t=0.5 PR AUC 0.387 ± 0.060  cost 32,188 ± 3,126
SMOTE in folds, t=0.5      PR AUC 0.389 ± 0.072  cost 27,162 ± 4,160

ফাঁদ: ভাগের আগে oversampling-এ PR AUC ০.৬৮১, SMOTE-এ ০.৫২৯, অথচ সৎ পথে চারটি কৌশলই ০.৩৮৭ থেকে ০.৪৪০. মাপা হয়েছে শুধু আসল সারিতে, তাই এই ফোলা পুরোটা leakage: test-এর একটি জালিয়াতির হুবহু কপি train-এ থাকায় গাছ সেটি মুখস্থ চেনে; SMOTE-এ হুবহু কপি নেই, কিন্তু test-এর জালিয়াতি থেকে বানানো পড়শিরা train-এ থাকে। সৎ তুলনা: সবচেয়ে ভালো PR AUC শুধু-threshold পথের (০.৪৪০), কিন্তু চারটির পার্থক্য sd-এর (০.০৬–০.১০) চেয়ে ছোট: resampling ranking উন্নত করেনি। খরচে শুধু-threshold (২৭,৯৫০) আর SMOTE (২৭,১৬২) প্রায় সমান, class_weight ২৯,৯৭৫, oversampling ৩২,১৮৮; sd ৩,০০০–৫,০০০, আর প্রতিটি test fold-এ মাত্র ১৭টি জালিয়াতি, তাই এই ৮টি fold-এ কাউকে স্পষ্ট বিজয়ী বলা যায় না (জোড়া তুলনার সঠিক পদ্ধতি পর্ব ২২-এ)। লক্ষ করুন, resampling-এর পর ০.৫ সীমা আর শুধু-threshold-এর ০.০২৫ সীমা মোটামুটি একই খরচে পৌঁছায়, যেমন অংশ ৪ বলেছিল। একটি ডেস্কটপ run-এ একই কোড সামান্য আলাদা সংখ্যা দেয় (HistGradientBoosting-এর হিসাব যন্ত্রভেদে একটু বদলায়), কিন্তু ছবি একই। আর এই প্রায় রৈখিক ডেটায় ছোট boosting model (০.৩৯–০.৪৪) প্রথম ব্লকের logistic regression-এর (০.৬১, অন্য ভাগে) চেয়ে দুর্বল: শক্ত model বাছার আগেও baseline দেখুন।

৮ · imbalanced-learn দিয়ে সত্যিকারের run, আর সৎ সিদ্ধান্ত

বাস্তবে resampling নিজে লেখার দরকার নেই। imbalanced-learn (Lemaître, Nogueira আর Aridas, JMLR ২০১৭) scikit-learn-এর সঙ্গী লাইব্রেরি। এর imblearn.pipeline.Pipeline সাধারণ pipeline-এর মতো (AI-এর ভিত্তি কোর্সের পর্ব ৬৪), শুধু পার্থক্য: sampler ধাপ চলে কেবল fit-এর সময়, predict বা score-এর সময় নয়। তাই cross_val_score-এ দিলে resampling আপনাআপনি শুধু training fold-এ হয়। লাইব্রেরিটি ব্রাউজারের Python-এ নেই, তাই ডেস্কটপে একটি uv project-এ সত্যিকারের run (লেখার সময়ের সর্বশেষ স্থিতিশীল version; একই ডেটা-নিয়মে ৫০,০০০ সারি, ৫-fold CV; একটি run, একটি যন্ত্র):

~/bazarghar-ml$ uv add scikit-learn imbalanced-learn
…
~/bazarghar-ml$ uv run python imbalance_check.py
Python 3.14.4, scikit-learn 1.9.1, imbalanced-learn 0.14.2, numpy 2.5.4
rows 50,000, fraud 985 (1.97%)
PR AUC, 5-fold CV
  logistic, no resampling         0.603 ± 0.020
  imblearn Pipeline(SMOTE, LR)     0.602 ± 0.020
  SMOTE before CV (leak), 98,030 rows 0.970 ± 0.001
out-of-fold cost in taka (no review = 3,940,000)
  RandomForest          PR AUC 0.566  mean p 0.020  cost@0.5 2,528,600  cost@0.025 1,027,400
  BalancedRandomForest  PR AUC 0.514  mean p 0.137  cost@0.5 982,900  cost@0.025 2,662,100
  HistGradientBoosting  PR AUC 0.521  mean p 0.019  cost@0.5 2,438,800  cost@0.025 1,065,200

মূল অংশটুকু এমন: Pipeline([("smote", SMOTE(random_state=0)), ("lr", LogisticRegression(max_iter=1000))]), আর BalancedRandomForestClassifier(300, random_state=0). তিনটি জিনিস লক্ষ করুন। (১) imblearn Pipeline-এ SMOTE logistic regression-এর PR AUC বদলায়নি (০.৬০৩ বনাম ০.৬০২). (২) ভাগের আগে SMOTE করলে ০.৯৭০: test fold-এর প্রায় অর্ধেক সারি কৃত্রিম জালিয়াতি, যাদের উৎস train-এ, তাই ফাঁদের সবচেয়ে চরম রূপ। (৩) BalancedRandomForest-এর গড় সম্ভাবনা ০.১৩৭, আসল হারের (০.০২) প্রায় সাত গুণ; তাই সে ০.৫ সীমায় ভালো (৯,৮২,৯০০ টাকা) কিন্তু খরচের সীমা ০.০২৫-এ খারাপ। সাধারণ RandomForest ঠিক উল্টো: ০.৫-এ খারাপ (২৫,২৮,৬০০), খরচের সীমায় ১০,২৭,৪০০, প্রায় একই তলায়। PR AUC-তে balanced বনটি পিছিয়ে (০.৫১৪ বনাম ০.৫৬৬). আর এই রৈখিক ডেটায় সরল logistic regression সবার ওপরে, যা আবার মনে করায়: আগে baseline.

তাহলে কখন কী?

van den Goorbergh ও সহকর্মীরা ২০২২ সালে (JAMIA) logistic regression-এর ঝুঁকি-model-এ simulation আর একটি আসল উদাহরণে দেখান: random under/oversampling আর SMOTE সাধারণত AUC বাড়ায়নি, কিন্তু calibration স্পষ্টভাবে নষ্ট করেছে। Elor আর Averbuch-Elor-এর ২০২২ সালের একটি পরীক্ষা (arXiv প্রিপ্রিন্ট) বলে, দুর্বল classifier-এ balancing কখনো সাহায্য করে, কিন্তু শক্ত boosting model আর ঠিকমতো বাছা threshold থাকলে সুবিধা প্রায় থাকে না। এগুলো নির্দিষ্ট ডেটা আর পরীক্ষার ফল, সর্বজনীন আইন নয়; তবু এই পর্বের পরীক্ষাও একই দিকে ইশারা করে। একটি বাস্তব ক্রম:

  1. ঠিক metric আর টাকার খরচ লিখুন; একটি সরল baseline (logistic regression) বানান।
  2. শক্ত model (boosting) আর সৎ সম্ভাবনা; দরকার হলে পর্ব ২৬-এর calibration কাজে লাগান।
  3. খরচ থেকে threshold, out-of-fold সম্ভাবনায় বাছা।
  4. তারপরও minority-র recall কম হলে class_weight বা resampling পরীক্ষা করুন, শুধু training fold-এ, আর সম্ভাবনা সংশোধন করুন।
  5. সবচেয়ে বড় লাভ প্রায়ই আসে আরও minority উদাহরণ বা ভালো feature থেকে, resampling থেকে নয়।

চরম imbalance. জালিয়াতি যদি ০.০১% হয়, বা প্রতিটি নতুন জালিয়াতি আগেরগুলোর মতো না দেখায়, তবে supervised শেখার মতো যথেষ্ট label থাকে না। তখন পর্ব ৪৩-এর anomaly detection (isolation forest, LOF) একটি বিকল্প, বা দুই ধাপের নকশা: anomaly score দিয়ে সন্দেহজনকদের ছেঁকে, তাদের ওপর supervised model. Label কম হলে কোন সারিতে label কেনা সবচেয়ে লাভজনক, তা পর্ব ৫৪ (কম label-এ শেখা — semi-supervised আর active learning)-এর বিষয়। আর জালিয়াতির হার সময়ের সঙ্গে বদলালে prior বদলায়, যা পর্ব ৫৯ (Distribution shift)-এ আসবে।

আরও গভীরে: Elkan (২০০১) একটি উপপাদ্য দেন: negative উদাহরণের সংখ্যা একটি নির্দিষ্ট গুণকে বদলালে সেটি threshold সরানোর সমান, তাই resampling আর cost-sensitive threshold একই মুদ্রার দুই পিঠ। Undersampling-এর পর সম্ভাবনা ফেরানোর সূত্র Dal Pozzolo ও সহকর্মীরা (২০১৫) বিশ্লেষণ করেন: β অংশ স্বাভাবিক সারি রাখলে p = βps / (βps − ps + ১). Deployment-এ prior অজানা হলে Saerens, Latinne আর Decaestecker-এর (২০০২) EM পদ্ধতি নতুন ডেটা থেকে prior অনুমান করে। Deep learning-এ বিরল শ্রেণির জন্য focal loss (Lin ও সহকর্মী, ২০১৭) জনপ্রিয়, যা পরের deep learning কোর্সে আসবে।

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

  • CFN = ৪,০০০, CFP = CTP = ১০০, CTN = ০ হলে threshold? ১০০/৪,০০০ = ০.০২৫.
  • balanced class weight-এ odds কত গুণ বাড়ে? n0/n1 গুণ; ফেরাতে n1/n0 দিয়ে গুণ।
  • xi = (২, ৪), xj = (৫, ০), λ = ০.৪ হলে SMOTE বিন্দু? (২ + ১.২, ৪ − ১.৬) = (৩.২, ২.৪).
  • Oversampling কখন করবেন? শুধু training fold-এর ভেতরে, কখনো ভাগের আগে নয়।
  • ১% positive-এ কোন metric? Accuracy বা ROC AUC নয়; PR AUC, নির্দিষ্ট precision-এ recall, আর খরচ।

৯ · AI-এর যুগে — imbalance নিয়ে AI-এর কোড যাচাই

“ডেটা imbalanced, ঠিক করে দাও” বললে AI assistant প্রায় সবসময় SMOTE ধরিয়ে দেয়, প্রায়ই ভুল জায়গায়। নিচের ভুলগুলো বারবার দেখা যায়:

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
SMOTE().fit_resample(X, y), তারপর train_test_splitTest-এ কপি বা পড়শি ঢোকে; score ফোলাভাগ আগে; imblearn Pipeline দিয়ে CV
Accuracy বা ROC AUC দেখিয়ে “৯৮% সফল” বলেসবাইকে স্বাভাবিক বলেও ৯৮%PR AUC, recall at precision, খরচ; সঙ্গে majority baseline
Resampling-এর পর predict_proba-কে আসল ঝুঁকি বলেPrior বদলে গেছে, সম্ভাবনা ফোলাprior correction বা calibration; গড় p-কে ভিত্তি-হারের সঙ্গে মেলান
Threshold ০.৫ রেখে দেয়, বা test set-এ threshold টিউন করে০.৫ খরচ জানে না; test-এ টিউন মানে আশাবাদী ফলOut-of-fold সম্ভাবনায় খরচ থেকে বাছুন, test একবার
পুরোনো বা ভুল API: fit_sample, ratio=, sklearn Pipeline-এ SMOTEfit_sample আর ratio বহু আগে বাদ; sklearn Pipeline-এ sampler চলে নাআপনার imbalanced-learn version-এর docs; কোড চালিয়ে দেখুন
Categorical বা ID কলামে সাধারণ SMOTE“১.৪টি জেলা” অর্থহীনSMOTENC, বা class_weight

উত্তরের সঙ্গে যাচাইয়ের কোড চেয়ে নিন; নিজে চালানো অংশটিই আসল পরীক্ষা।

আমার কাছে ১ লাখ লেনদেন আছে, ১.৫% জালিয়াতি। একটি ফসকানো জালিয়াতির ক্ষতি ৪,০০০ টাকা, একটি হাতে-যাচাই ১০০ টাকা। ১. scikit-learn 1.8 আর imbalanced-learn-এর বর্তমান API অনুযায়ী এমন কোড দাও, যেখানে resampling শুধু training fold-এ হয় (imblearn Pipeline), আর ভাগ আগে হয়। ২. class_weight, SMOTE আর শুধু threshold সরানো — তিনটি repeated stratified CV-তে PR AUC আর টাকার খরচে তুলনা করো, প্রতিটির গড় ± sd দিয়ে। ৩. খরচ থেকে threshold-এর সূত্র লেখো, আর out-of-fold সম্ভাবনায় কীভাবে বাছব, দেখাও। ৪. Resampling বা class_weight-এর পর সম্ভাবনা কীভাবে আসল prior-এ ফেরাব, সূত্রসহ বলো। ৫. তোমার দেওয়া প্রতিটি function আর parameter কোন version-এর docs-এ আছে, আর কোথায় তুমি নিশ্চিত নও, স্পষ্ট লেখো।

AI-এর দেওয়া imbalance-সমাধান হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১সমস্যাটি কি সত্যিই “অনুপাত”, নাকি অল্প উদাহরণ আর শ্রেণির মিশে থাকা?

“Imbalanced” শব্দটি মনোযোগ টানে অনুপাতের দিকে: ২ বনাম ৯৮. কিন্তু দুটি পরিস্থিতি ভাবুন। এক, ১০ লাখ লেনদেনে ২% জালিয়াতি, মানে ২০,০০০টি জালিয়াতির উদাহরণ। দুই, ২,০০০ লেনদেনে ২%, মানে ৪০টি। অনুপাত একই, অথচ প্রথমটিতে model জালিয়াতির চেহারা ভালোভাবেই শিখতে পারে, দ্বিতীয়টিতে পারে না। Nathalie Japkowicz আর Shaju Stephen ২০০২ সালে (Intelligent Data Analysis জার্নালে) কৃত্রিম ডেটায় অনুপাত, নমুনার আকার আর ধারণার জটিলতা আলাদা আলাদা বদলে দেখান: শ্রেণিগুলো সহজে আলাদা হলে বা ডেটা যথেষ্ট বড় হলে অনুপাত একা খুব একটা ক্ষতি করে না; ক্ষতি বড় হয় যখন ছোট নমুনা আর জটিল, মিশে থাকা সীমানা একসঙ্গে আসে। পরের অনেক গবেষণা একই দিকে ইশারা করেছে, যদিও ফল ডেটা আর পদ্ধতির ওপর নির্ভর করে।

এই দৃষ্টিভঙ্গি সমাধানও বদলায়। সমস্যা যদি সিদ্ধান্তের নিয়ম হয়, তবে সমাধান threshold. সমস্যা যদি অল্প উদাহরণ হয়, তবে সমাধান আরও label (অডিট দল দিয়ে লক্ষ্যভেদী label কেনা), ভালো feature, বা কম জটিল model; resampling নতুন তথ্য আনে না, শুধু পুরোনো তথ্য অন্যভাবে সাজায়। আর সমস্যা যদি overlap হয়, অর্থাৎ একই রকম দেখতে লেনদেনের কিছু জালিয়াতি আর কিছু সৎ, তবে কোনো কৌশলই সেটি দূর করতে পারে না; তখন প্রশ্ন হলো নতুন কোন তথ্য (ডিভাইস, আচরণের ইতিহাস) দুটিকে আলাদা করবে।

বাজারঘরের ভাষায়: প্রথমে গুনুন কতটি জালিয়াতির উদাহরণ আছে, তারপর একটি learning curve আঁকুন (training সারি বাড়ালে PR AUC কতটা বাড়ে)। দ্রুত বাড়লে আরও ডেটা কিনুন; থমকে থাকলে feature-এ মন দিন।

মূল উপলব্ধি: অনুপাত একটি লক্ষণ; আসল রোগ প্রায়ই অল্প minority উদাহরণ, মিশে থাকা শ্রেণি, আর ভুল সিদ্ধান্ত-নিয়ম, আর প্রতিটির চিকিৎসা আলাদা।

প্র ০২SMOTE কেন এত জনপ্রিয় হলো, আর বিশ বছর পরে তার মূল্যায়ন কী?

Chawla, Bowyer, Hall আর Kegelmeyer-এর ২০০২ সালের প্রবন্ধ মেশিন লার্নিংয়ের সবচেয়ে বেশি উদ্ধৃত প্রবন্ধগুলোর একটি হয়ে ওঠে। কারণ বোঝা সহজ: সেই সময়ের প্রচলিত classifier (C4.5 decision tree, Ripper নিয়ম) কপি করা minority-তে খুব নির্দিষ্ট, ছোট এলাকা শিখত, অর্থাৎ overfit করত। SMOTE কৃত্রিম বিন্দু দিয়ে minority এলাকা “ছড়িয়ে” দেয়, ফলে গাছ বড়, সাধারণ এলাকা শেখে। ধারণাটি দুই লাইনের, কোড সহজ, আর প্রবন্ধে ROC-তে স্পষ্ট উন্নতি দেখানো হয়েছিল। তারপর এল অসংখ্য রূপ: Borderline-SMOTE (২০০৫), ADASYN (২০০৮), Safe-Level-SMOTE, SMOTE-ENN, আর আরও অনেক। ২০১৮ সালে Fernández, García, Herrera আর Chawla JAIR-এ ১৫ বছর পূর্তির একটি পর্যালোচনায় লেখেন, ৮৫টির বেশি রূপ প্রস্তাবিত হয়েছে।

কিন্তু পরিবেশ বদলেছে। আজকের gradient boosting আর regularised logistic regression কপি করা উদাহরণে C4.5-এর মতো ভাঙে না, আর এরা মোটামুটি সৎ সম্ভাবনা দেয়, যার ওপর খরচের threshold বসানো যায়। Blagus আর Lusa (২০১৩, BMC Bioinformatics) দেখান, উচ্চ-মাত্রার ডেটায় (যেমন হাজারো জিনের মাপ) SMOTE প্রায়ই সাহায্য করে না, কারণ দূরত্ব অর্থ হারায়। van den Goorbergh ও সহকর্মীরা (২০২২) চিকিৎসার ঝুঁকি-model-এ দেখান, resampling calibration নষ্ট করে, যা সেখানে মারাত্মক: “আপনার ঝুঁকি ৪০%” বলা আর আসলে ৫% হওয়া রোগীর সিদ্ধান্ত বদলে দেয়।

এর মানে SMOTE অকেজো নয়। দুর্বল বা threshold-সংবেদনশীল model-এ, বা যখন মূল্যায়নের মাপকাঠি একটি নির্দিষ্ট threshold-এ F1, তখন এটি সাহায্য করতে পারে। কিন্তু “imbalanced, তাই SMOTE” আর স্বয়ংক্রিয় অভ্যাস হওয়া উচিত নয়; এটি একটি পরীক্ষাযোগ্য বিকল্প, যা threshold-only baseline-কে হারাতে হবে।

মূল উপলব্ধি: SMOTE তার সময়ের model-এর একটি বাস্তব দুর্বলতা সারিয়েছিল; শক্ত, calibrated model-এর যুগে এটিকে নিজের জায়গা প্রমাণ করতে হয়, ধরে নেওয়া যায় না।

প্র ০৩ভাগের আগে oversampling-এর ভুল প্রকাশিত গবেষণায় কতটা ঢুকেছে, আর কীভাবে চেনা যায়?

ভুলটি এত স্বাভাবিক দেখায় যে প্রকাশিত গবেষণাতেও ঢুকেছে। একটি পরিচিত উদাহরণ Vandewiele ও সহকর্মীদের ২০২১ সালের প্রবন্ধ (Artificial Intelligence in Medicine). একটি সর্বজনীন জরায়ুর বৈদ্যুতিক সংকেতের (EHG) dataset দিয়ে অকাল প্রসব অনুমানের বেশ কিছু প্রকাশিত কাজ প্রায় নিখুঁত ফল দাবি করেছিল। লেখকেরা দেখান, এর বড় অংশ এসেছে ভাগের আগে oversampling থেকে; ঠিক ক্রমে, অর্থাৎ শুধু training অংশে oversample করলে ফল অনেক সাধারণ হয়ে যায়। কোন প্রবন্ধে ঠিক কতটা প্রভাব, তার বিস্তারিত মূল প্রবন্ধে দেখে নিন; তবে শিক্ষাটি স্পষ্ট।

কেন এত সাধারণ? কারণ ভুল ক্রমটি “পরিষ্কার” দেখায়: প্রথমে ডেটা প্রস্তুত (scaling, SMOTE), তারপর model. Scaling-এর leakage সাধারণত ছোট, তাই অভ্যাসটি টিকে যায়; কিন্তু oversampling-এর leakage বিশাল, কারণ test-এর উত্তর সরাসরি train-এ কপি হয়। এই পর্বের ব্লকেও ফোলা score সৎ score-এর চেয়ে স্পষ্টভাবে বড়, আর ডেস্কটপ run-এ, test fold-এ কৃত্রিম সারি রেখে মাপলে, PR AUC প্রায় ০.৯৭: নিখুঁতের কাছাকাছি।

চেনার লক্ষণ: (১) কঠিন সমস্যায় অবিশ্বাস্য রকম ভালো ফল; (২) পদ্ধতির বর্ণনায় “আমরা SMOTE দিয়ে dataset balance করলাম, তারপর ৮০/২০ ভাগ করলাম”; (৩) test set-এ দুই শ্রেণির সংখ্যা সমান, অথচ আসল ডেটায় তা নয়; (৪) কোড থাকলে fit_resample ডাকা হয়েছে ভাগের আগে। রিভিউয়ার হিসেবে প্রশ্ন করুন: test set কি আসল বণ্টনের, আর resampling কি CV loop-এর ভেতরে?

মূল উপলব্ধি: Resampling হলো শেখার অংশ; যে ধাপ label দেখে ডেটা বদলায়, সে test বা validation অংশে কখনো ঢুকবে না।

প্র ০৪Threshold সরানো আর resampling কি আসলে একই কাজ? Elkan-এর যুক্তি কী বলে?

Charles Elkan ২০০১ সালের IJCAI প্রবন্ধে cost-sensitive শেখার ভিত্তি গুছিয়ে লেখেন। তিনি দেখান, দুই শ্রেণির ক্ষেত্রে সবচেয়ে কম প্রত্যাশিত খরচের সিদ্ধান্ত হলো: সৎ সম্ভাবনা p যদি p* = CFP/(CFP + CFN)-এর (সঠিক সিদ্ধান্তের খরচ ০ ধরলে) বেশি হয়, তবে positive বলুন। আর একটি উপপাদ্য: training-এ negative উদাহরণের সংখ্যা একটি গুণক দিয়ে বদলালে, শেখা model-এর ০.৫ সীমা আসলে আসল সম্ভাবনার একটি ভিন্ন সীমার সমান। অর্থাৎ resampling আর threshold সরানো, দুটিই একই সিদ্ধান্ত-সীমা সরায়, এক ক্ষেত্রে ডেটা দিয়ে, অন্য ক্ষেত্রে সরাসরি।

এই পর্বের অংশ ৪ ঠিক এটাই দেখায়: balanced ওজনের model-এ ০.৫ সীমা আসল সম্ভাবনায় প্রায় ০.০১৭. আমাদের খরচের সীমা ০.০২৫, তাই balanced ওজন “প্রায় ঠিক” জায়গায় পৌঁছেছে, কিন্তু সেটি কাকতালীয়; খরচ বদলালে (ধরা যাক যাচাই ৫০০ টাকা) ওজনকে আবার বদলাতে হবে, অথচ threshold-এ শুধু একটি সংখ্যা।

তাহলে পার্থক্য কোথায়? প্রথমত, সম্ভাবনা: threshold সরালে model-এর সম্ভাবনা সৎ থাকে, যা খরচের বাজেট, ঝুঁকির শ্রেণিবিভাগ আর অন্য সিস্টেমে পাঠানোর জন্য দরকার। দ্বিতীয়ত, শেখার ধরন: সসীম ডেটায় আর সীমিত ক্ষমতার model-এ resampling বা ওজন model-কে সীমানার কাছের এলাকায় বেশি মনোযোগ দিতে বাধ্য করে, তাই ranking কিছুটা বদলাতে পারে, কখনো ভালো, কখনো খারাপ। গাছের মতো model-এ ওজন split-এর পছন্দও বদলায়। তৃতীয়ত, খরচ যদি প্রতিটি লেনদেনে আলাদা হয় (বড় অঙ্কের জালিয়াতি বেশি দামি), তবে sample_weight-এ টাকার অঙ্ক বসানো বা লেনদেন-নির্দিষ্ট threshold আরও স্বাভাবিক।

মূল উপলব্ধি: Resampling আর ওজন মূলত threshold সরানোর ঘুরপথ; সরাসরি পথটি সস্তা, সৎ আর খরচ বদলালে সহজে মানিয়ে নেয়, তাই সেটিই প্রথম চেষ্টা।

প্র ০৫Training-এর জালিয়াতির হার আর বাস্তবের হার আলাদা হলে কী হয়?

ধরুন বাজারঘর জালিয়াতি-model শেখাল এমন ডেটায়, যেখানে অডিট দল সন্দেহজনক লেনদেন বেশি বেছেছিল: training-এ জালিয়াতি ১০%, অথচ বাস্তবে ১.৫%. বা ঈদের মৌসুমে জালিয়াতির চেষ্টা বেড়ে গেল। দুই ক্ষেত্রেই prior বদলেছে। জালিয়াতি দেখতে একই রকম থাকলে (p(x│y) অপরিবর্তিত), model-এর ranking ঠিক থাকে, কিন্তু সম্ভাবনা ভুল মাপে, আর খরচের threshold ভুল জায়গায় পড়ে।

সমাধান সেই একই সূত্র: odds-কে [π′/(১ − π′)] / [π/(১ − π)] দিয়ে গুণ, যেখানে π training-এর হার আর π′ বাস্তবের। এটি case-control গবেষণার পুরোনো কৌশল: রোগীদের আলাদা করে বেশি সংখ্যায় নেওয়া হয়, তারপর আসল প্রাদুর্ভাব দিয়ে সংশোধন। Gary King আর Langche Zeng ২০০১ সালে (Political Analysis) বিরল ঘটনার logistic regression-এ এই prior correction আর ছোট নমুনার পক্ষপাত-সংশোধন নিয়ে লেখেন, যা সমাজবিজ্ঞানে বহুল ব্যবহৃত।

সমস্যা হলো, π′ প্রায়ই অজানা: নতুন মাসের লেনদেনের label আসে সপ্তাহখানেক পরে। Saerens, Latinne আর Decaestecker (২০০২, Neural Computation) একটি EM পদ্ধতি দেন: model-এর সম্ভাবনা দিয়ে নতুন ডেটার prior অনুমান, সেই prior দিয়ে সম্ভাবনা সংশোধন, আবার prior অনুমান, যতক্ষণ না স্থির হয়। এটি কাজ করে যদি model শুরুতে ভালো calibrated হয়, আর শুধু prior বদলায়। জালিয়াতির ধরন বদলালে (নতুন কৌশল) p(x│y)-ও বদলায়, তখন কোনো সংশোধন যথেষ্ট নয়, নতুন label আর নতুন training লাগে। এগুলো মাপার পদ্ধতি পর্ব ৫৯ (Distribution shift)-এর বিষয়।

মূল উপলব্ধি: Training-এর হার একটি নকশার সিদ্ধান্ত, বাস্তবের তথ্য নয়; deployment-এর হার জেনে বা অনুমান করে সম্ভাবনা সংশোধন না করলে খরচের threshold ভুল জায়গায় বসে।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। একটির জন্য কোড লেখা লাগবে।

  1. হাতের হিসাব: (ক) ফসকানো জালিয়াতি ২,০০০ টাকা, হাতে-যাচাই ৫০ টাকা (জালিয়াতি হোক বা না হোক), সঠিকভাবে ছেড়ে দেওয়া ০. Threshold কত? (খ) Training-এ ৯,৯০০টি স্বাভাবিক আর ১০০টি জালিয়াতি; class_weight="balanced"-এর একটি model একটি লেনদেনকে ০.৬ বলল। আসল ঝুঁকি কত? (গ) xi = (২, ৪), xj = (৫, ০), λ = ০.৪ হলে SMOTE বিন্দু কোথায়?

    (ক) p* = (৫০ − ০)/[(৫০ − ০) + (২,০০০ − ৫০)] = ৫০/২,০০০ = ০.০২৫.

    (খ) oddsw = ০.৬/০.৪ = ১.৫. n1/n0 = ১০০/৯,৯০০ = ১/৯৯, তাই odds = ১.৫/৯৯ = ০.০১৫১৫, p = ০.০১৫১৫/১.০১৫১৫ = ০.০১৪৯, অর্থাৎ প্রায় দেড় শতাংশ। “৬০% ঝুঁকি” আসলে ভিত্তি-হারের (১%) সামান্য ওপরে।

    (গ) xj − xi = (৩, −৪); ০.৪ গুণ = (১.২, −১.৬); নতুন বিন্দু (৩.২, ২.৪). এটি দুই বিন্দুর সংযোগ রেখার ৪০% পথে।

  2. সমালোচনা ও নকশা: একজন সহকর্মীর notebook: “লেনদেনের ডেটায় (১.৫% জালিয়াতি) প্রথমে StandardScaler আর SMOTE().fit_resample পুরো ডেটায়, তারপর train_test_split ৮০/২০, তারপর RandomForestClassifier. Test-এ accuracy ০.৯৯, ROC AUC ০.৯৯৮. ০.৫-এর ওপরের সব লেনদেন আটকে দেওয়া হবে।” কী কী সমস্যা, আর আপনি কীভাবে নতুন করে সাজাবেন?

    সমস্যা: (১) ভাগের আগে SMOTE: test-এ কৃত্রিম জালিয়াতি, যাদের “মা-বাবা” train-এ; score ফোলা। (২) Test set আর আসল বণ্টনের নয় (প্রায় ৫০% জালিয়াতি), তাই accuracy আর ROC AUC কিছুই বলে না। (৩) Scaler-ও পুরো ডেটায় fit (ছোট leakage, আর random forest-এ scaling লাগেই না)। (৪) ০.৫ সীমা খরচ জানে না, আর SMOTE-এর পর সম্ভাবনা ফোলা, তাই অনেক সৎ লেনদেন আটকাবে। (৫) সরাসরি আটকানো; মানুষের যাচাই নেই। (৬) একটি split, কোনো অনিশ্চয়তা নেই।

    নতুন নকশা: সময় ধরে ভাগ (পুরোনো মাসে শেখা, পরের মাসে test), test-এ হাত নয়; train-এ repeated stratified CV, যেখানে imblearn Pipeline-এ resampling থাকলে শুধু training fold-এ; তুলনা: শুধু threshold বনাম class_weight বনাম SMOTE, PR AUC আর খরচে, গড় ± sd; out-of-fold সম্ভাবনায় খরচের সীমা; দরকারে calibration বা prior correction; শেষে test-এ একবার মাপা; উচ্চ score যাবে হাতে-যাচাইয়ে।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    xi = np.array([1.0, 3.0]); xj = np.array([2.0, 1.0])
    for lam in (0.0, 0.5, 1.0):
        print(xi + lam * (xj - xi))
    p_w = np.array([0.5, 0.9])
    odds = p_w / (1 - p_w) / 49
    print(np.round(odds / (1 + odds), 3))
    [1. 3.]
    [1.5 2. ]
    [2. 1.]
    [0.02  0.155]

    λ = ০ দেয় xi নিজেই, λ = ১ দেয় xj, λ = ০.৫ মাঝের বিন্দু (১.৫, ২). দ্বিতীয় অংশ ধরে নেয় n0/n1 = ৪৯ (২% জালিয়াতি): ০.৫ → odds ১/৪৯ = ০.০২০৪ → p = ০.০২; ০.৯ → odds ৯/৪৯ = ০.১৮৩৭ → p = ০.১৫৫. ওজনসহ model-এর “৯০%” আসলে ১৫.৫%. ছাপা ফরম্যাট একটি ডেস্কটপ run-এ যাচাই করা।

  4. কোড লিখুন: একটি function undersample(X, y, beta, seed) লিখুন, যা সব জালিয়াতি রাখে আর স্বাভাবিক সারির এলোমেলো β অংশ রাখে। তারপর একটি function correct(p_s, beta) লিখুন, যা undersampled ডেটায় শেখা model-এর সম্ভাবনা আসল মাপে ফেরায়। এই সংশোধন class_weight-এর সংশোধনের সঙ্গে কীভাবে মেলে?
    import numpy as np
    
    def undersample(X, y, beta, seed=0):
        r = np.random.default_rng(seed)
        neg = np.flatnonzero(y == 0)
        keep_neg = r.choice(neg, int(round(beta * len(neg))), replace=False)
        idx = np.sort(np.r_[np.flatnonzero(y == 1), keep_neg])
        return X[idx], y[idx]
    
    def correct(p_s, beta):
        # undersampling negative-কে beta গুণ কমায়, তাই odds 1/beta গুণ বাড়ে
        return beta * p_s / (beta * p_s - p_s + 1)

    একটি ডেস্কটপ run-এ (২ লাখ কাল্পনিক সারি, β = ০.১) সংশোধিত সম্ভাবনা পুরো ডেটায় শেখা model-এর সম্ভাবনার সঙ্গে তৃতীয় দশমিক পর্যন্ত মিলেছে। যুক্তি: স্বাভাবিক সারির β অংশ রাখলে prior odds ১/β গুণ বাড়ে, তাই oddss = odds/β, অর্থাৎ odds = β · oddss. সম্ভাবনায় লিখলে p = βps/(βps − ps + ১), যা Dal Pozzolo ও সহকর্মীদের (২০১৫) সূত্র। class_weight-এ গুণক ছিল w1/w0; এখানে ১/β: দুটি একই নিয়ম, শুধু গুণক আলাদা। উদাহরণ: β = ০.১ আর ps = ০.৫ হলে p = ০.০৫/(০.০৫ − ০.৫ + ১) = ০.০৯১.

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

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

ক্রমImbalanced ডেটায় resampling-সহ একটি model সৎভাবে মূল্যায়নের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Stratified ভাগে একটি test set আলাদা করে তালাবন্ধ রাখা
  2. বাকি ডেটা cross-validation-এর fold-এ ভাগ করা
  3. প্রতিটি training fold-এ শুধু সেই fold-এর ওপর resampling
  4. Resample করা training fold-এ model fit করা
  5. না-ছোঁয়া validation fold-এ PR AUC আর out-of-fold সম্ভাবনা সংগ্রহ
  6. Out-of-fold সম্ভাবনায় খরচ থেকে threshold বেছে test set-এ একবার মাপা

একটি জালিয়াতি ফসকালে ক্ষতি 3000 টাকা, প্রতিটি হাতে-যাচাইয়ের খরচ 150 টাকা (জালিয়াতি হোক বা না হোক), আর সাধারণ লেনদেন ছেড়ে দিলে খরচ 0. Calibrated সম্ভাবনা p-এর কোন সীমার ওপরে যাচাই করা উচিত?

  • 0.5
  • 0.05
  • 0.0476
  • 0.95

Training ডেটায় 980টি স্বাভাবিক আর 20টি জালিয়াতি। LogisticRegression(class_weight="balanced") একটি লেনদেনকে ঠিক 0.5 সম্ভাবনা দিল। Prior সংশোধনের পর আসল সম্ভাবনা প্রায় কত?

  • 0.5
  • 0.25
  • 0.02
  • 0.0004

SMOTE-এ minority বিন্দু x_i = (0, 2), বাছা প্রতিবেশী x_j = (4, 6), আর এলোমেলো λ = 0.25. নতুন কৃত্রিম বিন্দু কোথায়?

  • (1, 3)
  • (2, 4)
  • (3, 5)
  • (1, 1.5)

একজন বিশ্লেষক পুরো ডেটায় SMOTE চালিয়ে তারপর 5-fold cross-validation করলেন, আর PR AUC পেলেন 0.97, যেখানে শুধু training fold-এ SMOTE করলে 0.60. সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • পুরো ডেটায় SMOTE বেশি তথ্য দেয়, তাই 0.97 আসল উন্নতি
  • 5-fold খুব কম, 10-fold করলে দুটি মিলে যাবে
  • দুটি run-এর seed আলাদা, তাই পার্থক্য এলোমেলো
  • Validation fold-এ কৃত্রিম জালিয়াতি আর তাদের উৎস train-এ, তাই score leakage-এ ফোলা

একটি ডেটায় 1% positive. একটি model সব সারিকে negative বলে। কোন মন্তব্যটি সঠিক?

  • Accuracy 0.99, তাই model ভালো
  • Recall 0, আর PR AUC হবে এলোমেলো ভিত্তির সমান, প্রায় 0.01
  • ROC AUC 0.99
  • Precision 1.0, কারণ কোনো ভুল positive নেই

Gradient boosting-এর মতো শক্ত model আর খরচ থেকে বাছা threshold থাকলে resampling নিয়ে সাম্প্রতিক গবেষণা (যেমন van den Goorbergh ও সহকর্মী, 2022) মোটামুটি কী বলে?

  • Resampling সবসময় ranking অনেক উন্নত করে
  • Resampling কখনো ব্যবহার করা যাবে না
  • ranking-এ লাভ প্রায়ই সামান্য বা নেই, কিন্তু সম্ভাবনার calibration নষ্ট হয়
  • Resampling করলে threshold বা calibration নিয়ে ভাবতে হয় না

শব্দকোষ

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

মনে রাখুন

  1. Imbalance-এর ক্ষতি আসে ভুল threshold, অল্প minority উদাহরণ আর ভুল metric থেকে; মাপুন PR AUC, নির্দিষ্ট precision-এ recall আর টাকার খরচ দিয়ে, সঙ্গে “সবাই স্বাভাবিক” baseline.
  2. প্রথমে threshold: p* = (CFP − CTN)/[(CFP − CTN) + (CFN − CTP)], calibrated সম্ভাবনায়, out-of-fold ডেটায় বাছা।
  3. class_weight আর resampling odds-কে ধ্রুবক গুণ করে (balanced-এ n0/n1); odds-কে উল্টো গুণক দিয়ে গুণ করলে আসল সম্ভাবনা ফেরে।
  4. SMOTE: xnew = xi + λ(xj − xi), প্রতিবেশী শুধু minority-র মধ্যে; overlap, noise আর উচ্চ মাত্রায় দুর্বল।
  5. Resampling শুধু training fold-এ (imblearn Pipeline); ভাগের আগে করলে score ফোলে। শক্ত model আর খরচের threshold থাকলে resampling প্রায়ই সামান্যই যোগ করে, আর চরম imbalance-এ anomaly detection ভাবুন।

পরের পর্বে: পর্ব ৫১ (Time series forecasting — decomposition, ARIMA আর exponential smoothing) সময়ের ক্রমে সাজানো ডেটায় যায়: বাজারঘরের দৈনিক অর্ডারে trend আর সাপ্তাহিক মৌসুম আলাদা করা, ARIMA আর exponential smoothing, আর এমন সরল baseline, যাকে হারাতেই হবে।

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

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