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

Distribution shift — covariate, label আর concept shift ধরা ও সামলানো

Distribution Shift — Covariate, Label and Concept Shift: Detection with PSI, KS and Domain Classifiers, Importance Weighting, BBSE and Retraining

Taught in Bangla, this lesson separates covariate, label and concept shift on fictional returns data. In-browser runs compute PSI, KS and chi-square tests and an out-of-fold domain classifier, apply importance weights with effective sample size, estimate a new class prior with BBSE, and compare frozen, sliding-window and recency-weighted retraining under concept drift.

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

  • P(x, y)-এর দুই ভাঙন দিয়ে covariate, label আর concept shift আলাদা করা, আর কোনটি label ছাড়া ধরা যায় তা জানা
  • KS, chi-square আর PSI নিজে লিখে প্রতিটি feature যাচাই, আর একাধিক test-এর ফাঁদ চেনা
  • Domain classifier দিয়ে সব feature একসঙ্গে shift মাপা, আর label দেরিতে এলে কী নজরে রাখবেন তা জানা
  • Importance weight বানানো, effective sample size দিয়ে তার দাম মাপা, আর BBSE দিয়ে নতুন শ্রেণি-হার অনুমান
  • Concept drift-এ স্থির model, sliding window আর recency weighting তুলনা করে retraining-এর নীতি বাছা

এতদিন প্রায় প্রতিটি পর্বে একটি নীরব অনুমান ছিল: test ডেটা training ডেটার মতো একই বণ্টন থেকে আসে। বাস্তবে model চালু হয় ভবিষ্যতে, নতুন জায়গায়, নতুন নিয়মে। AI-এর ভিত্তি কোর্সের পর্ব ৭৪-এ data drift, PSI, KS test, label delay আর ট্রিগার দিয়ে retraining দেখেছেন, আর পর্ব ৫৬-এ সময় ধরে ভাগ করার কারণ। এই পর্ব আরও গভীরে যায়: shift-এর ধরনগুলো গণিতে আলাদা করা, সব feature একসঙ্গে যাচাই, আর label ছাড়াই কিছু shift সংশোধন। চারটি চালানো যায় এমন ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy, pandas, SciPy, scikit-learn আর matplotlib); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে।

১ · তিন ধরনের shift — P(x, y)-এর কোন অংশ বদলাল

একজন অভিজ্ঞ ডাক্তারের কথা ভাবুন, যিনি বছরের পর বছর ঢাকার একটি হাসপাতালে রোগ নির্ণয় করেছেন। (১) তাঁকে উপকূলের একটি জেলায় পাঠানো হলো: রোগীরা আলাদা ধরনের, বয়স আর পেশা ভিন্ন, কিন্তু একই উপসর্গ একই রোগ বোঝায়। (২) বর্ষায় ডেঙ্গুর মৌসুম এল: ডেঙ্গুর রোগী অনেক বেশি, কিন্তু ডেঙ্গুর উপসর্গ আগের মতোই। (৩) একটি নতুন ভাইরাস এল, যার উপসর্গ পুরোনো অসুখের মতো: একই জ্বর-কাশি এখন অন্য রোগ বোঝায়। প্রথম দুটিতে তাঁর জ্ঞান কাজে লাগে, শুধু হিসাব একটু বদলাতে হয়। তৃতীয়টিতে নতুন রোগী দেখে আবার শিখতে হয়।

ডেটা আর label-এর যৌথ বণ্টন P(x, y)-কে দুইভাবে ভাঙা যায় (AI-এর ভিত্তি কোর্সের পর্ব ৩৫-এর গুণের নিয়ম):

P(x, y) = P(x) · P(y | x) = P(y) · P(x | y)

এখানে x হলো feature (অর্ডারের দাম, দূরত্ব, পোশাক কি না, COD কি না), আর y হলো label (অর্ডার ফেরত এসেছে কি না)। Training-এর বণ্টন P আর ব্যবহারের সময়ের বণ্টন Q আলাদা হলে তাকে বলে distribution shift বা dataset shift. কোন অংশ বদলাল, তা দিয়ে তিনটি প্রধান ধরন:

ধরনকী বদলায়কী একই থাকেবাজারঘরেlabel ছাড়া ধরা যায়?
Covariate shiftP(x)P(y | x)নতুন জেলা: দূরের ঠিকানা, বেশি CODহ্যাঁ, x দেখে
Label shift (prior shift)P(y)P(x | y)ঈদ: ফেরতের হার ২৫% থেকে ৪৫%আংশিক, অনুমানের বণ্টন দেখে
Concept shift (concept drift)P(y | x)P(x) একই থাকতে পারেনীতি বদল: COD-এ অগ্রিম টাকানা, নতুন label লাগে

তিন ধরনের shift: কোন অংশ নড়ে, কোনটি স্থির

নীল = training-এর সময়; কমলা = ব্যবহারের সময় (ছবিটি গুণগত, ক আর গ-এর হার বাজারঘরের simulation থেকে)

Covariate shift, label shift and concept shift compared ক) Covariate: P(x) সরে, P(y | x) একই খ) Label: P(y) বদলায়, P(x | y) একই গ) Concept: P(y | x) বদলায় train নতুন জেলা ঝুঁকি (একই) দূরত্ব (km) → সাধারণ মাস ঈদ 25%45% লাল = ফেরত, সবুজ = না-ফেরত। প্রতিটি দলের অর্ডারের চেহারা একই, বদলায় শুধু মিশ্রণ। 30%7% 20%20% CODCOD নয় আগে পরে
ছবি ১.১ — (ক) নতুন জেলায় দূরত্ব ডানে সরেছে, কিন্তু একই দূরত্বে ফেরতের ঝুঁকি একই। (খ) ঈদে ফেরত-অর্ডারের ভাগ বেড়েছে, কিন্তু একটি ফেরত-অর্ডার দেখতে আগের ফেরত-অর্ডারের মতোই। (গ) অর্ডারগুলো দেখতে একই, কিন্তু COD অর্ডারে ১০০ টাকা অগ্রিম নেওয়ার নীতির পর তাদের ফেরতের হার ৩০% থেকে ৭%: নিয়মটাই বদলেছে।

বাস্তবে এরা প্রায়ই মিশে আসে। নতুন জেলায় হয়তো courier-ও আলাদা, তাই একই দূরত্বে ঝুঁকিও বদলায়। একটি নির্দিষ্ট দলের (ধরুন নতুন ক্রেতা) ভাগ বদলালে তাকে subpopulation shift বলে, যা covariate shift-এরই একটি রূপ। নামকরণও সব বইয়ে এক নয়: কেউ ধীরে বদলকে “drift” আর এক ধাক্কার বদলকে “shift” বলেন। এই পর্বে নাম নয়, প্রশ্নটি জরুরি: কোন অংশটি স্থির আছে বলে ধরে নিচ্ছি? সেই অনুমানই ঠিক করে কোন পদ্ধতি কাজ করবে।

২ · বাজারঘরের ফেরত-অর্ডার আর তিনটি দৃশ্য

নিচের module কাল্পনিক বাজারঘরের অর্ডার বানায়: দামের log (logp), ঠিকানার দূরত্ব km-এ (dist), পোশাক কি না (cloth), আর ক্যাশ অন ডেলিভারি কি না (cod). ফেরতের আসল নিয়মে দূরত্ব ১০ km-এর পর ঝুঁকি দ্রুত বাড়ে (max(0, dist − 10) পদ), যা একটি সরল logistic regression পুরোপুরি ধরতে পারে না; এটি পরে গুরুত্বপূর্ণ হবে। Training-এ ৩,০০০টি অর্ডার, ফেরতের হার প্রায় ২৫%. তারপর ২,০০০টি অর্ডারের চারটি “ব্যবহারের” দৃশ্য: same (কোনো বদল নেই, তুলনার জন্য), district (নতুন জেলা: গড় দূরত্ব ৬ থেকে ১৪ km, COD ৫০% থেকে ৭৫%), eid (একটি বড় ভান্ডার থেকে ফেরত আর না-ফেরত আলাদা করে তুলে হার ৪৫% করা, তাই P(x | y) হুবহু একই), আর policy (x একই, কিন্তু COD-এর প্রভাব +০.৬ থেকে −১.৪). month() অংশ ৮-এর জন্য: মাস ৯ থেকে ১৬-র মধ্যে নীতিটি ধীরে চালু হয়।

# বাজারঘর (কাল্পনিক দোকান): ফেরত-অর্ডার আর তিন ধরনের shift, seeded ডেটা
import numpy as np
import pandas as pd

FEATS = ["logp", "dist", "cloth", "cod"]

def make(n, rng, kind="base", t=0.0):
    far = kind == "district"                     # নতুন জেলা: দূরের ঠিকানা, বেশি COD
    price = rng.lognormal(7.0, 0.6, n)
    dist = rng.gamma(4.0, 3.5, n) if far else rng.gamma(2.0, 3.0, n)
    cloth = rng.binomial(1, 0.4, n)
    cod = rng.binomial(1, 0.75 if far else 0.5, n)
    b_cod = 0.6 - 2.0 * t                        # নীতি বদল: t = 0 পুরোনো, t = 1 নতুন
    z = (-2.3 + 0.5 * (np.log(price) - 7) + 1.0 * cloth + b_cod * cod
         + 0.04 * dist + 0.25 * np.maximum(0, dist - 10))
    y = rng.binomial(1, 1 / (1 + np.exp(-z)))
    return pd.DataFrame({"logp": np.log(price), "dist": dist, "cloth": cloth,
                         "cod": cod, "returned": y})

def train_data(n=3000, seed=59):
    return make(n, np.random.default_rng(seed))

def scenario(name, n=2000, seed=590):
    rng = np.random.default_rng(seed)
    if name == "district":
        return make(n, rng, "district")
    if name == "policy":
        return make(n, np.random.default_rng(seed + 4), t=1.0)
    if name == "eid":                            # label shift: P(x | y) একই, হার 0.45
        pool = make(40 * n, rng)
        k = int(0.45 * n)
        pos = pool[pool.returned == 1].sample(k, random_state=seed)
        neg = pool[pool.returned == 0].sample(n - k, random_state=seed)
        return pd.concat([pos, neg]).sample(frac=1, random_state=seed)
    return make(n, np.random.default_rng(seed + 1))    # "same": কোনো shift নেই

def month(m, n=600):                             # ধীরে বদলানো নীতি: মাস 9 থেকে 16
    t = min(1.0, max(0.0, (m - 8) / 8))
    return make(n, np.random.default_rng(5900 + m), t=t)

একটি সতর্কতা: simulation-এ আমরা প্রতিটি দৃশ্যের আসল label জানি, তাই পদ্ধতিগুলোর ভুল মাপা যায়। বাস্তবে নতুন জেলা বা ঈদের প্রথম সপ্তাহে label থাকে না; ফেরত চূড়ান্ত হতে কয়েক সপ্তাহ লাগে। তাই প্রতিটি পদ্ধতির প্রশ্ন: label ছাড়া কী জানা যায়, আর কোন অনুমানের ওপর?

৩ · এক এক feature করে — KS, chi-square আর PSI

AI-এর ভিত্তি কোর্সের পর্ব ৭৪-এ দুটি হাতিয়ার দেখেছেন। Kolmogorov–Smirnov test দুই নমুনার cumulative বণ্টনের সবচেয়ে বড় ফাঁক D মাপে আর একটি p-value দেয়। PSI reference ডেটার quantile দিয়ে bin বানিয়ে প্রতিটি bin-এ পুরোনো ভাগ e আর নতুন ভাগ a তুলনা করে:

PSI = ∑i (ai − ei) · ln(ai / ei)

এখানে তিনটি জিনিস যোগ হয়। প্রথম, PSI আসলে কী। ∑ a ln(a/e) হলো KL divergence KL(a‖e), আর ∑ e ln(e/a) হলো KL(e‖a). দুটি যোগ করলে ঠিক PSI আসে। অর্থাৎ PSI হলো দুই দিকের KL-এর যোগফল (Jeffreys divergence), তাই প্রতিসম আর সবসময় ≥ ০. দ্বিতীয়, ০/১ feature-এ KS নয়। cloth বা cod-এর মতো শ্রেণিভিত্তিক feature-এ ২×২ গণনার টেবিলে chi-square test চলে, আর PSI-এর bin হয় শ্রেণিগুলোই। তৃতীয়, সীমাগুলো হাতের নিয়ম। PSI < ০.১ “স্থির”, ০.১–০.২৫ “নজরে রাখুন”, > ০.২৫ “বড় বদল”: ঋণ-scoring-এর চর্চা থেকে আসা প্রচলিত সীমা, কোনো তত্ত্ব থেকে প্রমাণিত নয়। Bin-এর সংখ্যা, নমুনার আকার আর খালি bin-এ কী বসানো হয়, তার ওপর মান বদলায়। নিচের stepper তিনটি bin-এ পুরো হিসাব করে।

পুরোনো ভাগ e = (0.50, 0.30, 0.20) · নতুন ভাগ a = (0.30, 0.30, 0.40)
এখন দেখা হচ্ছে
হিসাব
ফল
  1. Bin. পুরোনো ডেটায় অর্ডারগুলোর অর্ধেক bin ১-এ, ৩০% bin ২-এ, ২০% bin ৩-এ। নতুন মাসে ভাগ (০.৩০, ০.৩০, ০.৪০): বণ্টন ডানে সরেছে।
  2. bin ১. ভাগ কমেছে, তাই (a − e) ঋণাত্মক আর ln(a/e)-ও ঋণাত্মক; গুণফল ধনাত্মক ০.১০২২. চিহ্ন সবসময় মেলে বলে কোনো পদ ঋণাত্মক হয় না।
  3. bin ২ আর ৩. bin ২-এ বদল নেই, পদ শূন্য। bin ৩-এ ভাগ দ্বিগুণ: ০.২০ × ln ২ = ০.১৩৮৬.
  4. যোগ। PSI = ০.২৪০৮: হাতের নিয়মে “নজরে রাখুন”-এর ওপরের প্রান্তে, ০.২৫-এর ঠিক নিচে। ১০টি bin নিলে একই বদলে সংখ্যাটি অন্যরকম হতে পারত; তাই PSI তুলনা করুন একই bin-নীতির মধ্যে।
  5. KL দিয়ে যাচাই। KL(a‖e) = ০.১২৪০ আর KL(e‖a) = ০.১১৬৮; যোগফল হুবহু PSI. একদিকের KL প্রতিসম নয়, যোগফল প্রতিসম।
  6. খালি bin. নতুন মাসে bin ১ একেবারে খালি হলে ln(০) সংজ্ঞাহীন। কোড সাধারণত একটি ছোট ε বসায়, আর সেই পছন্দেই PSI ৪.৬০ বা ৩.৪৫. সংখ্যাটির সঙ্গে গণনাও রিপোর্ট করুন: “bin ১-এ ০টি অর্ডার” অনেক বেশি বলে।

অনেক test, একটি ভুয়া সতর্কতা। ৪০টি feature প্রতিদিন ৫% স্তরে test করলে কোনো বদল না থাকলেও গড়ে দুটি “তাৎপর্যপূর্ণ” ফল আসে। এটি পর্ব ২২-এর multiple comparisons সমস্যা; Bonferroni (α ÷ feature-সংখ্যা) সরল সমাধান। উল্টো দিকে, নমুনা বড় হলে KS খুব ছোট বদলেও p ≈ ০ দেয়। তাই p-value দিয়ে “বদল আছে কি না” আর PSI বা D দিয়ে “কতটা” দেখুন।

৪ · সব feature একসঙ্গে — domain classifier

এক এক feature করে দেখলে একটি বড় ফাঁক থাকে। ধরুন নতুন জেলায় দূরত্ব আর দামের বণ্টন আলাদাভাবে একই, কিন্তু দূরের অর্ডারগুলো এখন বেশি দামি। প্রতিটি univariate test শান্ত, অথচ যৌথ বণ্টন বদলেছে। সমাধানটি সুন্দরভাবে সরল: একটি classifier-কে শেখান, কোন সারি training-এর (d = ০) আর কোনটি নতুন (d = ১)। এর নাম domain classifier, আর পরিসংখ্যানে এটি classifier two-sample test নামে পরিচিত (David Lopez-Paz আর Maxime Oquab ২০১৭ সালে ICLR-এ এর তত্ত্ব আর পরীক্ষা বিশদ করেন)।

  • দুই বণ্টন এক হলে কোনো classifier-ই সারিগুলো আলাদা করতে পারবে না: out-of-fold ROC AUC ≈ ০.৫.
  • AUC যত ১-এর দিকে, বদল তত সহজে ধরা যায়। কোন feature-এ classifier সবচেয়ে বেশি নির্ভর করছে, তা দেখলে বদলের উৎস বোঝা যায়; এর পদ্ধতি পর্ব ৫৬ (Model বোঝা — permutation importance, PDP আর ICE)-এ দেখেছেন।
  • AUC অবশ্যই cross-validation-এর out-of-fold অনুমান থেকে, নইলে একটি নমনীয় model নিজের training সারি মুখস্থ করে AUC বাড়িয়ে দেখাবে।

প্রথম ব্লক চারটি দৃশ্যে প্রতিটি feature-এর PSI আর p-value (সংখ্যার feature-এ KS, ০/১ feature-এ chi-square) ছাপায়, তারপর একটি ছোট HistGradientBoostingClassifier-কে domain classifier বানিয়ে ৩-fold cross_val_predict-এর AUC দেয়।

import numpy as np
import pandas as pd
from scipy.stats import ks_2samp, chi2_contingency
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import roc_auc_score
from bazar_shift import train_data, scenario, FEATS

def psi(ref, new, bins=10):                       # PSI, নিজে লেখা
    edges = np.quantile(ref, np.linspace(0, 1, bins + 1))   # ref-এর quantile-এ bin
    if ref.nunique() <= 2:
        edges = np.array([0, 0.5, 1.0])           # 0/1 feature: দুটি bin
    edges[0], edges[-1] = -np.inf, np.inf
    e = np.clip(np.histogram(ref, edges)[0] / len(ref), 1e-4, None)
    a = np.clip(np.histogram(new, edges)[0] / len(new), 1e-4, None)
    return np.sum((a - e) * np.log(a / e))

def test_p(ref, new):                             # সংখ্যা: KS; 0/1: chi-square
    if ref.nunique() > 2:
        return ks_2samp(ref, new).pvalue
    return chi2_contingency(pd.crosstab(np.r_[ref, new], np.r_[0 * ref, 0 * new + 1])).pvalue

ref, names = train_data(), ["same", "district", "eid", "policy"]
P, Pv, auc = {}, {}, {}
for name in names:
    new = scenario(name)
    P[name] = [psi(ref[f], new[f]) for f in FEATS]
    Pv[name] = [f"{test_p(ref[f], new[f]):.0e}" for f in FEATS]
    X = pd.concat([ref[FEATS], new[FEATS]])       # domain classifier: 0 = train, 1 = নতুন
    d = np.r_[np.zeros(len(ref)), np.ones(len(new))]
    clf = HistGradientBoostingClassifier(max_iter=60, max_depth=3, random_state=0)
    auc[name] = roc_auc_score(d, cross_val_predict(clf, X, d, cv=3, method="predict_proba")[:, 1])
print("PSI:\n", pd.DataFrame(P, index=FEATS).round(3))
print("p-value (KS / chi-square):\n", pd.DataFrame(Pv, index=FEATS))
print("domain AUC:", {k: round(v, 3) for k, v in auc.items()})

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

PSI:
         same  district    eid  policy
logp   0.008     0.015  0.010   0.012
dist   0.003     2.111  0.011   0.006
cloth  0.000     0.000  0.006   0.000
cod    0.000     0.235  0.001   0.000
p-value (KS / chi-square):
         same district    eid policy
logp   6e-01    3e-01  7e-02  3e-01
dist   8e-01   1e-317  2e-01  6e-01
cloth  8e-01    6e-01  9e-03  6e-01
cod    6e-01    4e-60  3e-01  6e-01
domain AUC: {'same': 0.5, 'district': 0.86, 'eid': 0.51, 'policy': 0.493}

নতুন জেলা স্পষ্ট: দূরত্বের PSI ২.১১, COD-এর ০.২৩৫, p-value প্রায় শূন্য, domain AUC ০.৮৬. কোনো বদল নেই এমন দৃশ্যে সব PSI ০.০১-এর নিচে, AUC ০.৫০০: এটিই “শান্ত” অবস্থার ছবি। ঈদ প্রায় অদৃশ্য: ফেরতের হার ২৫% থেকে ৪৫% হলেও PSI সবই ০.০১১ বা তার কম, AUC ০.৫১. শুধু পোশাকের chi-square p = ০.০০৯, কারণ পোশাকে ফেরত বেশি, তাই ফেরত-অর্ডারের ভাগ বাড়লে পোশাকও একটু বাড়ে। চারটি feature-এ Bonferroni সীমা ০.০৫/৪ = ০.০১২৫, তাই এটি সীমার ঠিক নিচে; এক দিনের এই একটি ফল দেখে কেউ সিদ্ধান্ত নেবে না। নীতি বদল সম্পূর্ণ অদৃশ্য: x-এর বণ্টন সত্যিই একই, তাই কোনো input-ভিত্তিক test-ই কিছু বলতে পারে না, অথচ (পরের ব্লকগুলোতে দেখবেন) model-এর ভুল বেড়েছে। শিক্ষা: “input শান্ত” মানে “model ঠিক আছে” নয়।

৫ · Label দেরিতে এলে — অনুমানের বণ্টন আর মানের নজরদারি

ফেরত চূড়ান্ত হতে প্রায় এক মাস লাগে (label delay). এই সময়ে আসল accuracy অজানা, তবু কিছু সংকেত সঙ্গে সঙ্গে পাওয়া যায়। সবচেয়ে কাজের হলো prediction drift: model-এর গড় সম্ভাবনা আর “ঝুঁকিপূর্ণ” বলা অর্ডারের ভাগ। ঈদে input প্রায় শান্ত হলেও ঝুঁকিপূর্ণ বলা ভাগ ৩২% থেকে ৩৭%-এ ওঠে (তৃতীয় ব্লকে দেখবেন), কারণ ফেরত-ধাঁচের অর্ডার বেশি। নীচের সারণি সংকেতগুলো সাজায়।

সংকেতকখন পাওয়া যায়কী ধরেকী ধরে না
Feature-এর PSI, KS, missing-এর হারসঙ্গে সঙ্গেcovariate shift, pipeline-এর ভুলconcept shift
Domain classifier-এর AUCসঙ্গে সঙ্গে (একটি ব্যাচ জমলে)যৌথ বণ্টনের বদলconcept shift
অনুমানের বণ্টন, ঝুঁকিপূর্ণ বলার হারসঙ্গে সঙ্গেlabel shift, বড় covariate shiftP(x) একই রেখে নিয়ম বদল
Accuracy, log loss, calibrationlabel আসার পরসব ধরনের ক্ষতিকিছু নয়, তবে দেরিতে
হাতে যাচাই করা ছোট এলোমেলো নমুনাকয়েক দিনেconcept shift-এর প্রথম আভাসনমুনা ছোট বলে অনিশ্চয়তা বড়

শেষ সারিটি প্রায়ই উপেক্ষিত। প্রতিদিন কয়েকশো অর্ডার এলোমেলোভাবে বেছে দ্রুত যাচাই করলে (ধরুন ফোন করে নিশ্চিত করা) label delay কমে। আর একটি সূক্ষ্ম বিপদ: model যে অর্ডার আটকায়, তার ফল কখনো দেখা যায় না, তাই শুধু অনুমতি পাওয়া অর্ডারে মান মাপলে ছবি বাঁকা হয় (feedback loop). এজন্য একটি ছোট নিয়ন্ত্রণ দল রাখা হয়, যেখানে model-এর সিদ্ধান্ত প্রয়োগ হয় না। Label ছাড়া calibrated সম্ভাবনা থেকে প্রত্যাশিত accuracy গোনার পদ্ধতিও আছে, কিন্তু concept shift-এ তা অন্ধ, কারণ সম্ভাবনাগুলোই তখন ভুল।

৬ · Covariate shift সামলানো — importance weighting

Covariate shift-এ P(y | x) একই, তাই training-এর প্রতিটি সারি এখনো সঠিক শিক্ষা দেয়; সমস্যা শুধু অনুপাতে। নতুন জেলায় দূরের অর্ডার বেশি, training-এ কম। সমাধান: নতুন বণ্টনে যে ধরনের সারি বেশি, তাকে বেশি ওজন দেওয়া। গণিতে, নতুন বণ্টনে প্রত্যাশিত loss

EQ[ℓ(x, y)] = EP[ w(x) · ℓ(x, y) ], যেখানে w(x) = q(x) / p(x)

q(x) আর p(x) হলো নতুন আর পুরোনো ঘনত্ব। এটি importance weighting, যাকে Hidetoshi Shimodaira ২০০০ সালে log-likelihood-এ ওজন দেওয়ার পদ্ধতি হিসেবে বিশ্লেষণ করেন। কিন্তু চার মাত্রায় ঘনত্ব অনুমান করা কঠিন (পর্ব ৪০), আর দুটি অনুমানের ভাগফল আরও অস্থির। কৌশলটি domain classifier দিয়ে: Bayes-এর সূত্রে

w(x) = q(x) / p(x) = [ P(d=1 | x) / P(d=0 | x) ] × ( ntrain / nnew )

অর্থাৎ classifier-এর odds, দুই দলের আকারের অনুপাত দিয়ে সংশোধিত। তারপর scikit-learn-এর প্রায় সব estimator-এ fit(X, y, sample_weight=w) (sample weight). দাম কোথায়? কয়েকটি সারি বিশাল ওজন পেলে model আসলে অল্প কয়েকটি সারি থেকে শেখে। তা মাপার জন্য Leslie Kish-এর জরিপ-পরিসংখ্যান থেকে আসা effective sample size: ESS = (∑w)² / ∑w². সব ওজন সমান হলে ESS = n; একটি ওজন বাকিদের ছাপিয়ে গেলে ESS → ১.

ntrain = 3000, nnew = 2000 · w = p/(1 − p) × 3000/2000 · পাঁচটি training অর্ডার A–E
এখন দেখা হচ্ছে
P(new | x) → odds
ওজন w
ESS = (∑w)² / ∑w²
  1. সূত্র। ৫,০০০ সারির ২,০০০টি নতুন, তাই কোনো তথ্য ছাড়া P(new) = ০.৪. যে অর্ডারে classifier-ও ঠিক ০.৪ বলে, সেটি দুই জগতে সমান সাধারণ: w = ০.৬৬৭ × ১.৫ = ১.
  2. অর্ডার A. classifier বলে এটি নতুন হওয়ার সম্ভাবনা ০.২: নতুন জেলায় এমন অর্ডার কম। w = ০.৩৭৫, শেখায় এর গুরুত্ব কমে।
  3. অর্ডার B. p = ০.৪, w = ১: এমন অর্ডার দুই জগতে সমান অনুপাতে আছে।
  4. অর্ডার E. ২৫ km দূরের COD অর্ডার; training-এ এমন অর্ডার বিরল, নতুন জেলায় সাধারণ। p = ০.৯, odds ৯, w = ১৩.৫: একাই প্রায় সাড়ে তেরোটি সাধারণ অর্ডারের সমান।
  5. ESS. ওজনের যোগ ১৮.৬২৫, বর্গের যোগ ১৯০.৭০. ESS = ১.৮২: পাঁচটি সারি থাকলেও কার্যত দুটিরও কম সারির তথ্য। অনুমানের variance সেই অনুযায়ী বাড়ে।
  6. কাটা (clipping). সবচেয়ে বড় ওজন ৫-এ আটকালে ESS ৩.০৬. দাম: সংশোধন অসম্পূর্ণ, তাই একটু bias. এটি bias আর variance-এর বিনিময়; বাস্তবে সাধারণত ৯৫ বা ৯৯ percentile-এ কাটা হয়।

দ্বিতীয় ব্লক তিনটি আলাদা training নমুনায় পুরো পদ্ধতি চালায়: domain classifier-এর out-of-fold সম্ভাবনা থেকে ওজন, ৯৯তম percentile-এ কাটা ওজন, আর তিন রকম logistic regression. মূল্যায়ন নতুন জেলার ২,০০০টি label-সহ অর্ডারে, যা training-এ কখনো ঢোকেনি। তুলনার জন্য একটি “oracle”: নতুন জেলার label-সহ ডেটায় সরাসরি শেখানো model, বাস্তবে যা পাওয়া যায় না।

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import accuracy_score, log_loss
from bazar_shift import train_data, scenario, FEATS

test = scenario("district", seed=777)            # label-সহ, শুধু মূল্যায়নে
rows = []
for s in range(3):                               # তিনটি আলাদা training নমুনা
    ref = train_data(seed=59 + s)
    new = scenario("district", seed=590 + s)[FEATS]   # নতুন জেলা, label ছাড়া
    X = pd.concat([ref[FEATS], new])
    d = np.r_[np.zeros(len(ref)), np.ones(len(new))]
    dom = HistGradientBoostingClassifier(max_iter=60, max_depth=3, random_state=0)
    p = cross_val_predict(dom, X, d, cv=3, method="predict_proba")[:len(ref), 1]
    w = p / (1 - p) * len(ref) / len(new)        # odds × (n_train / n_new)
    w_clip = np.minimum(w, np.quantile(w, 0.99))
    for name, sw in [("none", None), ("weighted", w), ("clipped", w_clip)]:
        m = LogisticRegression().fit(ref[FEATS], ref.returned, sample_weight=sw)
        q = m.predict_proba(test[FEATS])[:, 1]
        ess = len(ref) if sw is None else sw.sum() ** 2 / (sw ** 2).sum()
        rows.append({"seed": s, "weights": name, "ESS": ess,
                     "max_w": w.max() if sw is not None else 1,
                     "acc": accuracy_score(test.returned, q > 0.5),
                     "logloss": log_loss(test.returned, q), "mean_p": q.mean()})
lab = scenario("district", seed=999)             # তুলনা: নতুন জেলার label-সহ ডেটায় শেখা
q = LogisticRegression().fit(lab[FEATS], lab.returned).predict_proba(test[FEATS])[:, 1]
print("true rate:", round(test.returned.mean(), 3), " oracle: acc",
      accuracy_score(test.returned, q > 0.5), "logloss", round(log_loss(test.returned, q), 3))
t = pd.DataFrame(rows)
print(t.groupby("weights", sort=False)[["ESS", "acc", "logloss", "mean_p"]].mean().round(3))
print(t.pivot(index="seed", columns="weights", values="logloss").round(3))
print("largest weight per seed:", t[t.weights == "weighted"].max_w.round(1).tolist())

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

true rate: 0.522  oracle: acc 0.7475 logloss 0.506
               ESS    acc  logloss  mean_p
weights
none      3000.000  0.727    0.531   0.504
weighted   306.260  0.746    0.507   0.535
clipped    673.489  0.746    0.508   0.530
weights  clipped   none  weighted
seed
0          0.506  0.532     0.506
1          0.511  0.532     0.508
2          0.506  0.530     0.506
largest weight per seed: [87.9, 138.3, 111.6]

লাভ: ওজন ছাড়া log loss ০.৫৩১, ওজনসহ ০.৫০৭, যা oracle-এর ০.৫০৬-এর প্রায় সমান; accuracy ০.৭২৭ থেকে ০.৭৪৬, আর গড় সম্ভাবনা আসল হার ০.৫২২-এর দিকে সরেছে (০.৫০৪ থেকে ০.৫৩৫)। তিনটি seed-এই উন্নতি একই দিকে। কেন কাজ করল: সরল logistic model দূরত্বের বাঁক (১০ km-এর পর ঝুঁকির লাফ) ধরতে পারে না, তাই সে একটি সরলরেখা আপস করে বসায়। ওজন ছাড়া সেই আপস কাছের অর্ডারের পক্ষে, ওজনসহ দূরের অর্ডারের পক্ষে। দাম: ৩,০০০ সারির ESS প্রায় ৩০৬, অর্থাৎ ৯০% তথ্য কার্যত হারিয়েছে; সবচেয়ে বড় ওজন ৮৮ থেকে ১৩৮. ৯৯তম percentile-এ কাটলে ESS দ্বিগুণের বেশি (৬৭৩), আর log loss প্রায় একই। সততা: লাভ ছোট, ০.০২৪, আর এটি একটি অনুকূল নকশা। Model যদি P(y | x) ঠিকভাবে ধরতে পারত, ওজনের প্রায় কোনো লাভ থাকত না, শুধু variance বাড়ত। আর নতুন জেলার কিছু অর্ডার যদি training-এ একেবারেই না থাকত (দূরত্ব ৪০ km), কোনো ওজনই সেই অঞ্চল শেখাতে পারত না।

মূল ধারণা

Importance weighting তিনটি শর্তে কাজ করে: (১) সত্যিই covariate shift, অর্থাৎ P(y | x) একই; (২) নতুন বণ্টনের প্রতিটি অঞ্চলে training-এ অন্তত কিছু সারি আছে (support মেলে); (৩) model সীমিত, তাই কোন অঞ্চলে ভালো করবে তা বাছতে হয়। শর্ত ভাঙলে ওজন হয় অকেজো, নয়তো শুধু অস্থিরতা বাড়ায়; তখন নতুন জেলার কিছু অর্ডার label করাই সবচেয়ে সৎ পথ।

৭ · Label shift সামলানো — নতুন prior অনুমান (BBSE)

পর্ব ২৬-এ দেখেছেন, নতুন prior π′ জানা থাকলে odds-কে [π′/(১ − π′)] / [π/(১ − π)] দিয়ে গুণ করলেই সম্ভাবনা সংশোধন হয়, model আবার না শিখিয়েই। কিন্তু ঈদের প্রথম সপ্তাহে π′ জানা নেই; সেটিই তো label. সরল উত্তর “model যত ভাগকে ঝুঁকিপূর্ণ বলছে, ততই” ভুল, কারণ classifier নিখুঁত নয়: কিছু ফেরত সে ধরে না, কিছু না-ফেরতকে ঝুঁকিপূর্ণ বলে।

Label shift-এ P(x | y) একই, তাই classifier-এর অনুমান ŷ-এর শর্তসাপেক্ষ আচরণও একই: P(ŷ | y) পুরোনো আর নতুন জগতে সমান। তাহলে নতুন ডেটায় অনুমানের বণ্টন

μi = Q(ŷ = i) = ∑j P(ŷ = i | y = j) · Q(y = j)

এটি অজানা Q(y)-এর একটি রৈখিক সমীকরণ। বাম পাশ নতুন ডেটা থেকে গোনা যায় (label ছাড়াই), আর P(ŷ | y) আসে পুরোনো ডেটার validation অংশের confusion matrix থেকে। Zachary Lipton, Yu-Xiang Wang আর Alexander Smola ২০১৮ সালে ICML-এ এই পদ্ধতি black box shift estimation (BBSE) নামে বিশ্লেষণ করেন: Cij = P(ŷ = i, y = j) পুরোনো ডেটায়, তারপর C·w = μ সমাধান করলে wj = Q(y = j)/P(y = j). দুই শ্রেণিতে এটি পুরোনো একটি সূত্রে নামে, যাকে quantification-এর সাহিত্যে adjusted classify-and-count বলা হয় (George Forman-এর ২০০৮ সালের কাজে সুপরিচিত): π′ = (r − FPR) / (TPR − FPR), যেখানে r হলো নতুন ডেটায় ঝুঁকিপূর্ণ বলার হার।

পুরোনো validation: π = 0.25TPR = 0.60, FPR = 0.20

            
  1. পুরোনো জগৎ। Validation-এ ফেরতের হার ০.২৫; classifier ফেরতের ৬০% ধরে (TPR), আর না-ফেরতের ২০%-কে ভুল করে ঝুঁকিপূর্ণ বলে (FPR). যৌথ সম্ভাবনার চারটি ঘর মিলে C.
  2. নতুন জগৎ। ঈদের সপ্তাহে ৩৮% অর্ডারকে ঝুঁকিপূর্ণ বলা হলো। এটিকেই ফেরতের হার ধরা (classify and count) ভুল, কারণ দুই ধরনের ভুল এতে মিশে আছে।
  3. সমীকরণ। P(ŷ | y) বদলায়নি বলে r = TPR·π′ + FPR·(১ − π′). সমাধান π′ = ০.৪৫: আসল ফেরতের হার ঝুঁকিপূর্ণ বলার হারের চেয়েও বেশি, কারণ classifier ফেরতের ৪০% ধরতে পারে না।
  4. ম্যাট্রিক্সে। একই কাজ C·w = μ সমাধান করে: w = (০.৭৩৩, ১.৮). ফেরতের হার পুরোনোর ১.৮ গুণ, ০.২৫ × ১.৮ = ০.৪৫. শ্রেণি অনেক হলেও এই রূপ চলে।
  5. সংশোধন। পুরোনো model যে অর্ডারকে ০.৩০ দিয়েছিল, ঈদের জগতে তার সম্ভাবনা ০.৫১৩. এটি পর্ব ২৬-এর সূত্র, শুধু π′ এখন অনুমিত।
  6. অসম্ভব উত্তর। r = ০.৭০ হলে π′ = ১.২৫ আর একটি ওজন ঋণাত্মক। এটি গণনার ভুল নয়, অনুমানের ভুল: P(ŷ | y) আর আগের মতো নেই। বাজারঘরের নতুন জেলায় ঠিক এটিই ঘটবে।

তৃতীয় ব্লক training ডেটার প্রথম ২,০০০ সারিতে model শেখায়, বাকি ১,০০০ সারিতে C গোনে (সীমা ০.৩), তারপর চারটি দৃশ্যে অনুমানের গড়, ঝুঁকিপূর্ণ বলার হার, BBSE-এর π′ আর তিন রকম log loss ছাপায়: যেমন আছে, BBSE-এর π′ দিয়ে সংশোধিত, আর আসল π′ দিয়ে সংশোধিত (যা prior সংশোধনের সর্বোচ্চ সম্ভাব্য লাভ)।

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
from bazar_shift import train_data, scenario, FEATS

ref = train_data()
fit, val = ref.iloc[:2000], ref.iloc[2000:]       # val: confusion matrix-এর জন্য
model = LogisticRegression().fit(fit[FEATS], fit.returned)
T = 0.3                                           # ঝুঁকিপূর্ণ বলার সীমা
pv = model.predict_proba(val[FEATS])[:, 1]
yv = val.returned.to_numpy()
pi = yv.mean()                                    # train-এর prior
C = np.array([[np.mean((pv < T) & (yv == j)) for j in (0, 1)],
              [np.mean((pv >= T) & (yv == j)) for j in (0, 1)]])
print("C = P(pred, true) on val:\n", C.round(3), " prior:", round(pi, 3))

def adjust(p, pi, pi_new):                        # পর্ব ২৬-এর odds সংশোধন
    a = p * pi_new / pi
    return a / (a + (1 - p) * (1 - pi_new) / (1 - pi))

rows = []
for name in ["same", "district", "eid", "policy"]:
    new = scenario(name)
    p = model.predict_proba(new[FEATS])[:, 1]
    mu = np.array([np.mean(p < T), np.mean(p >= T)])     # নতুন ডেটায় অনুমানের ভাগ
    w = np.linalg.solve(C, mu)                    # C·w = μ, w = q(y) / p(y)
    pi_hat = w[1] * pi                            # অনুমিত নতুন prior
    y = new.returned
    rows.append({"scenario": name, "true": y.mean(), "mean_p": p.mean(),
                 "flag_rate": mu[1], "BBSE": pi_hat, "LL_asis": log_loss(y, p),
                 "LL_bbse": log_loss(y, adjust(p, pi, np.clip(pi_hat, 0.01, 0.99))),
                 "LL_oracle": log_loss(y, adjust(p, pi, y.mean()))})
print(pd.DataFrame(rows).set_index("scenario").round(3).T)

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

C = P(pred, true) on val:
 [[0.579 0.111]
 [0.169 0.141]]  prior: 0.252
scenario    same  district    eid  policy
true       0.254     0.530  0.450   0.135
mean_p     0.250     0.494  0.273   0.253
flag_rate  0.316     0.748  0.366   0.314
BBSE       0.270     1.565  0.418   0.262
LL_asis    0.510     0.527  0.715   0.429
LL_bbse    0.510     2.363  0.634   0.436
LL_oracle  0.510     0.623  0.631   0.382

ঈদ: model-এর গড় সম্ভাবনা ০.২৭৩, ঝুঁকিপূর্ণ বলার হার ০.৩৬৬, কিন্তু BBSE বলে ০.৪১৮, সত্য ০.৪৫০-এর অনেক কাছে। সংশোধনে log loss ০.৭১৫ থেকে ০.৬৩৪, যা আসল prior দিয়ে পাওয়া ০.৬৩১-এর প্রায় সমান। ছোট ঘাটতি (০.৪১৮ বনাম ০.৪৫০) আসে মাত্র ১,০০০ সারির validation থেকে: C-এর ঘরগুলোর নিজস্ব অনিশ্চয়তা আছে, আর TPR − FPR = ০.৫৬৪ − ০.২২৬ ছোট হলে সেই অনিশ্চয়তা বড় হয়ে যায়। কোনো বদল নেই: BBSE ০.২৭০, সত্য ০.২৫৪; সংশোধনে log loss বদলায় না। নতুন জেলা: π′ = ১.৫৬৫, একটি অসম্ভব সংখ্যা; জোর করে ০.৯৯-এ আটকে সংশোধন করলে log loss ০.৫২৭ থেকে ২.৩৬৩: বিপর্যয়। এখানে P(x | y) একই নয় (দূরের অর্ডার নিজেরাই ঝুঁকিপূর্ণ), তাই অনুমানটি ভেঙেছে, আর সংখ্যাটি নিজেই তা জানিয়ে দিয়েছে। নীতি বদল: সত্য ০.১৩৫, BBSE ০.২৬২, সংশোধনে সামান্য খারাপ। Concept shift-এ অনুমানের বণ্টন বদলায় না, কারণ x বদলায়নি, তাই কোনো label-ছাড়া পদ্ধতি এটি ধরতে পারে না; এমনকি আসল prior দিলেও (০.৩৮২) সমস্যা পুরো মেটে না, কারণ বদলটি নিয়মে, শুধু হারে নয়।

π′ অনুমানের আরেকটি পুরোনো পথ Marco Saerens ও সহকর্মীদের ২০০২ সালের EM (expectation–maximization): বর্তমান π′ দিয়ে প্রতিটি অর্ডারের সম্ভাবনা সংশোধন, তাদের গড়কে নতুন π′ ধরা, আবার, যতক্ষণ না স্থির হয়। এটি calibrated সম্ভাবনা চায়; BBSE চায় শুধু একটি স্থির classifier আর তার confusion matrix. অনুশীলন ৪-এ EM নিজে লিখে দুটি তুলনা করবেন।

৮ · Concept shift — নতুন label ছাড়া উপায় নেই

নিয়ম বদলালে পুরোনো ডেটার কোনো ওজন বা prior সংশোধন নতুন নিয়ম শেখাতে পারে না; নতুন label লাগে। প্রশ্ন শুধু: নতুন label কীভাবে ব্যবহার করবেন? তিনটি সাধারণ কৌশল:

  • স্থির (frozen): একবার শেখা model, আর কখনো নয়। Baseline হিসেবে জরুরি।
  • Sliding window: প্রতি মাসে শুধু শেষ k মাসের label-সহ ডেটায় নতুন করে শেখানো (পর্ব ৫২-এর backtesting-এর sliding window-এর মতো)। পুরোনো নিয়ম দ্রুত ভোলে, কিন্তু ডেটা কম।
  • Recency weighting: সব মাস রাখা, কিন্তু প্রতিটি মাস যত পুরোনো, ওজন তত কম (এখানে প্রতি মাসে ০.৭৫ গুণ)। ডেটা বেশি, কিন্তু পুরোনো নিয়মের ছায়া থেকে যায়।

চতুর্থ ব্লক ২৪ মাসের একটি ধারা বানায়, প্রতি মাসে ৬০০টি অর্ডার। মাস ৯ থেকে ১৬-র মধ্যে COD-এর অগ্রিম নীতি ধীরে চালু হয়। Label আসে এক মাস দেরিতে, তাই মাস m-এর model শুধু m − ১ পর্যন্ত ডেটা দেখে। প্রতিটি মাসে তিনটি কৌশলের log loss মাপা হয়, আর ধূসর অংশে নীতি চালু হচ্ছে।

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
from bazar_shift import month, FEATS

M = {m: month(m) for m in range(1, 25)}           # ২৪ মাস, প্রতি মাসে ৬০০ অর্ডার
fit = lambda ms, w=None: LogisticRegression().fit(
    pd.concat([M[k][FEATS] for k in ms]), pd.concat([M[k].returned for k in ms]),
    sample_weight=w)
frozen = fit([1, 2, 3])                           # প্রথম তিন মাসে একবার শেখা
rows = []
for m in range(4, 25):                            # label এক মাস দেরিতে: m−1 পর্যন্ত জানা
    past = list(range(1, m))
    window = fit(past[-3:])                       # sliding window: শেষ ৩ মাস
    w = np.concatenate([np.full(600, 0.75 ** (m - 1 - k)) for k in past])
    recency = fit(past, w)                        # সব মাস, পুরোনো মাসের ওজন কম
    y = M[m].returned
    rows.append({"month": m} | {n: log_loss(y, mod.predict_proba(M[m][FEATS])[:, 1])
                 for n, mod in [("frozen", frozen), ("window", window), ("recency", recency)]})
t = pd.DataFrame(rows).set_index("month")
print(t.iloc[[0, 5, 9, 13, 17, 20]].round(3))
print("mean 4-8  :", t.loc[4:8].mean().round(3).to_dict())
print("mean 9-16 :", t.loc[9:16].mean().round(3).to_dict())
print("mean 17-24:", t.loc[17:24].mean().round(3).to_dict())
ax = t.plot(marker="o", figsize=(6.4, 3.2), title="Concept drift: monthly log loss")
ax.axvspan(8.5, 16.5, color="0.9"); ax.set_ylabel("log loss"); plt.show()

আসল আউটপুট (ব্রাউজারের Python; ছবিটি ব্লকের নিচে আঁকা হয়):

       frozen  window  recency
month
4       0.549   0.549    0.549
9       0.496   0.491    0.492
13      0.465   0.428    0.439
17      0.443   0.355    0.365
21      0.454   0.347    0.348
24      0.460   0.359    0.357
mean 4-8  : {'frozen': 0.52, 'window': 0.52, 'recency': 0.52}
mean 9-16 : {'frozen': 0.46, 'window': 0.422, 'recency': 0.429}
mean 17-24: {'frozen': 0.432, 'window': 0.336, 'recency': 0.339}

নীতির আগে (মাস ৪–৮) তিনটি কৌশল সমান, ০.৫২০: স্থির জগতে বারবার শেখানোয় লাভ নেই। বদলের সময় (৯–১৬) sliding window এগিয়ে (০.৪২২), recency weighting একটু পিছনে (০.৪২৯), কারণ সে পুরোনো নিয়মের মাসগুলোও কিছুটা মনে রাখে। বদলের পরে (১৭–২৪) দুটিই প্রায় সমান (০.৩৩৬ আর ০.৩৩৯), আর স্থির model অনেক পিছনে (০.৪৩২). স্থির model-এর log loss-ও কিছুটা কমেছে, কারণ নতুন নীতিতে ফেরত কম, আর কম ফেরতের মাসে log loss স্বভাবতই ছোট; তাই মাসে মাসে মাপের তুলনা সবসময় একই মাসের কৌশলগুলোর মধ্যে করুন। মাসে ডেটা কম হলে ছোট জানালার variance বাড়ে, তখন recency-র সুবিধা বেশি হওয়ার কথা। এটি একটি seed-এর একটি run, সাধারণ ফল নয়।

কতটা পুরোনো ডেটা রাখবেন, তা বদলের গতি আর ডেটার পরিমাণের বিনিময়: দ্রুত বদলে ছোট জানালা, ধীর বদলে আর কম ডেটায় বড় জানালা বা ধীর ক্ষয়। জানালার দৈর্ঘ্য বা ক্ষয়ের হার নিজেও একটি hyperparameter, যা অতীতের ধারায় backtesting দিয়ে বাছাই করা উচিত, ভবিষ্যৎ দেখে নয়। ভুল কখন বাড়ছে তা স্বয়ংক্রিয়ভাবে ধরার জন্য স্ট্রিম-ভিত্তিক detector আছে (যেমন Albert Bifet আর Ricard Gavaldà-র ২০০৭ সালের ADWIN, যা ভুলের হারের জানালা নিজে ছোট-বড় করে); João Gama ও সহকর্মীদের ২০১৪ সালের একটি জরিপে এদের তুলনা আছে। নিয়মিত না ট্রিগারে retrain, তা AI-এর ভিত্তি কোর্সের পর্ব ৭৪-এ দেখেছেন।

৯ · নজরদারির চেকলিস্ট আর আসল টুল

সব মিলিয়ে একটি চালু model-এর জন্য কাজের ক্রম:

  1. Deploy-এর আগে: reference ডেটা (সাধারণত training বা validation-এর একটি অংশ) সংরক্ষণ, bin-এর সীমা একবার ঠিক করে রাখা, আর ঋতুর জন্য আলাদা reference (ঈদের মাসকে সাধারণ মাসের সঙ্গে তুলনা করলে প্রতিবছর ভুয়া সতর্কতা)।
  2. প্রতিদিন: pipeline-এর মৌলিক যাচাই (missing, অসম্ভব মান), প্রতিটি feature-এর PSI, অনুমানের গড় আর ঝুঁকিপূর্ণ বলার হার।
  3. প্রতি সপ্তাহে: domain classifier-এর AUC আর তার শীর্ষ feature; BBSE দিয়ে শ্রেণি-হারের অনুমান, আর অসম্ভব মান এলে তা আলাদা সতর্কসংকেত হিসেবে।
  4. Label এলে: log loss, accuracy আর calibration, একটি সরল baseline-এর পাশে, আর মূল দলভেদে (জেলা, পণ্যের ধরন)।
  5. সতর্কতা এলে: আগে কারণ (pipeline-এর ভুল, ঋতু, ব্যবসার নীতি), তারপর ধরন (covariate, label, concept), তারপর প্রতিকার: ওজন, prior সংশোধন, বা নতুন label-এ আবার শেখানো।

এসব কাজের জন্য খোলা উৎসের Python library আছে। Evidently ডেটা আর অনুমানের drift-এর রিপোর্ট বানায়, feature-এর ধরন দেখে KS, chi-square, PSI বা Wasserstein-এর মতো test বাছে। NannyML label আসার আগেই model-এর মান অনুমানের পদ্ধতিতে (CBPE, regression-এর জন্য DLE) জোর দেয়, আর multivariate drift-এর জন্য PCA দিয়ে ডেটা ছোট করে আবার গড়ার ভুল (reconstruction error) মাপে। এরা পাতার Python-এ চলে না, আর API সংস্করণভেদে বদলেছে; নিজের version-এর docs দেখে ছোট ডেটায় এই পর্বের নিজে লেখা হিসাবের সঙ্গে মিলিয়ে নিন। মাসের পর মাসের নজরদারি, alert আর dashboard-এর পুরো নকশা পর্ব ৬৯ (ML system-এর test আর নজরদারি — data test, model test আর drift)-এ আসবে।

আরও গভীরে: Stephan Rabanser, Stephan Günnemann আর Zachary Lipton ২০১৯ সালে NeurIPS-এ (“Failing Loudly”) বহু shift-detection পদ্ধতি তুলনা করেন; তাঁদের পরীক্ষায় model-এর নিজের অনুমান বা ছোট করা representation-এর ওপর সরল univariate test (Bonferroni-সহ) অনেক সময় বেশ ভালো কাজ করেছে। ঘনত্বের অনুপাত সরাসরি শেখার পদ্ধতিও আছে: kernel mean matching (Jiayuan Huang ও সহকর্মীরা, NIPS ২০০৬) আর KLIEP (Masashi Sugiyama ও সহকর্মীরা, ২০০৮)। আর Bernhard Schölkopf ও সহকর্মীরা ২০১২ সালে যুক্তি দেন, কারণের দিক (x থেকে y, না y থেকে x) জানলে কোন shift বেশি স্বাভাবিক তা বোঝা যায় (ভাবনার প্রশ্ন ৪)।

পরীক্ষা ও ইন্টারভিউতে: (১) Covariate, label আর concept shift-এর পার্থক্য? যথাক্রমে P(x), P(y) আর P(y | x) বদলায়; প্রথম দুটিতে যথাক্রমে P(y | x) আর P(x | y) স্থির। (২) e = (০.৫, ০.৩, ০.২), a = (০.৩, ০.৩, ০.৪) হলে PSI? ০.১০২২ + ০ + ০.১৩৮৬ = ০.২৪১. (৩) Domain classifier-এর AUC ০.৫ মানে? x দেখে পুরোনো আর নতুন আলাদা করা যায় না; concept shift তবু থাকতে পারে। (৪) Domain classifier P(new | x) = ০.৮, ntrain = nnew হলে ওজন? ০.৮/০.২ = ৪. (৫) TPR ০.৮, FPR ০.১, নতুন ডেটায় ঝুঁকিপূর্ণ বলার হার ০.৪৫ হলে π′? (০.৪৫ − ০.১)/(০.৭) = ০.৫.

১০ · AI-এর যুগে — drift-এর কোড আর দাবি যাচাই

“আমার model-এ drift monitoring যোগ করো” বললে AI assistant প্রায়ই প্রতিটি feature-এ KS test আর “p < ০.০৫ হলে retrain” দেয়। এই পর্বের ব্লকগুলো দেখিয়েছে, তাতে নীতি বদল একেবারেই ধরা পড়ে না, আর বড় নমুনায় প্রায় প্রতিদিন ভুয়া সতর্কতা আসে। নিচের ভুলগুলো প্রায়ই চোখে পড়ে।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
শুধু input-এর drift test দিয়ে “model ঠিক আছে” বলেconcept shift input-এ দেখা যায় নাlabel-এর পথ আর দেরিতে আসা মানের মাপ যোগ করুন
প্রতিটি feature-এ p < ০.০৫, কোনো সংশোধন নেইঅনেক test-এ ভুয়া সতর্কতা; বড় n-এ নগণ্য বদলও তাৎপর্যপূর্ণBonferroni বা FDR, আর PSI বা D-এর মতো আকারের মাপ
Domain classifier-এর AUC training সারিতেই মাপেমুখস্থ করা model AUC ফুলিয়ে দেখায়out-of-fold অনুমান; “same” দৃশ্যে AUC ≈ ০.৫ কি না দেখুন
Importance weight দেয়, ESS দেখে নাকয়েকটি সারি সব ঠিক করে, variance বিশালESS, সবচেয়ে বড় ওজন, clipping, কয়েকটি seed
Label shift ধরে prior সংশোধন করে, অনুমান যাচাই না করেঅনুমান ভাঙলে সংশোধন ক্ষতি করেπ′ ০ আর ১-এর মধ্যে কি না, অল্প label-এ মিলিয়ে দেখা
Evidently বা NannyML-এর পুরোনো API লেখেclass আর parameter-এর নাম সংস্করণভেদে বদলেছেনিজের version-এর docs; ছোট ডেটায় নিজে লেখা PSI-এর সঙ্গে মেলান

AI-কে কোডের সঙ্গে অনুমান আর যাচাইও চাইতে বলুন:

বাজারঘরের ফেরত-অর্ডারের একটি logistic regression model চালু আছে (feature: দাম, দূরত্ব, পোশাক কি না, COD কি না)। ফেরত চূড়ান্ত হতে এক মাস লাগে। ১. প্রতিদিনের নজরদারির কোড দাও: প্রতিটি feature-এর PSI (training-এর quantile দিয়ে ঠিক করা bin), ০/১ feature-এ chi-square, আর multiple testing-এর সংশোধন। প্রতিটি সীমা কোথা থেকে এসেছে, লেখো। ২. সপ্তাহে একবার out-of-fold domain classifier-এর AUC আর তার শীর্ষ feature দাও। ৩. Label shift ধরে BBSE দিয়ে ফেরতের হার অনুমান করো; অনুমিত হার ০ থেকে ১-এর বাইরে গেলে কী করবে, লেখো। ৪. Covariate shift-এ importance weight দিলে ESS আর সবচেয়ে বড় ওজন ছাপাও, আর তিনটি seed-এ ওজনসহ ও ওজন ছাড়া log loss তুলনা করো। ৫. Concept shift ধরার জন্য কোন label কখন লাগবে, আর retrain-এর নীতি কী, লেখো। কোনো warning চাপা দিয়ো না, আর কোন library-র কোন version ধরে কোড লিখছ, তা বলো।

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

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

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

প্র ০১Sample selection bias থেকে “dataset shift”: এই সমস্যাটি কোন কোন শাখায় আলাদা নামে জন্মেছিল?

Training আর ব্যবহারের ডেটা আলাদা হওয়ার সমস্যা ML-এর চেয়ে পুরোনো। অর্থনীতিতে James Heckman ১৯৭৯ সালে Econometrica-তে sample selection bias বিশ্লেষণ করেন: ধরুন শুধু চাকরিজীবী নারীদের বেতন দেখে সব নারীর সম্ভাব্য বেতন অনুমান করা হচ্ছে, অথচ কারা চাকরি করেন তা নিজেই বেতনের সম্ভাবনার সঙ্গে জড়িত। এই কাজের জন্য তিনি ২০০০ সালে অর্থনীতিতে নোবেল স্মারক পুরস্কারের অংশীদার হন। জরিপ-পরিসংখ্যানে নমুনা জনসংখ্যার প্রতিনিধি না হলে ওজন দিয়ে সংশোধন বহু পুরোনো চর্চা; Leslie Kish-এর effective sample size সেখান থেকেই এসেছে।

ML-এ Hidetoshi Shimodaira ২০০০ সালে “covariate shift” নামটি আর ওজনসহ likelihood-এর বিশ্লেষণ জনপ্রিয় করেন। পরের দশকে ক্ষেত্রটি দ্রুত বাড়ে: Joaquin Quiñonero-Candela, Masashi Sugiyama, Anton Schwaighofer আর Neil Lawrence-এর সম্পাদনায় ২০০৯ সালে MIT Press থেকে Dataset Shift in Machine Learning নামে একটি বই বেরোয়। ২০১২ সালে Jose Moreno-Torres ও সহকর্মীরা Pattern Recognition-এ নামকরণের বিশৃঙ্খলা নিয়ে একটি একীভূত দৃষ্টিভঙ্গি প্রস্তাব করেন, কারণ একই ধারণা বিভিন্ন লেখায় concept drift, population drift, domain shift ইত্যাদি নামে চলছিল।

অন্য শাখাতেও একই প্রশ্ন আলাদা ভাষায়। চিকিৎসায় একটি রোগনির্ণয় পরীক্ষার sensitivity আর specificity হাসপাতাল থেকে হাসপাতালে মোটামুটি স্থির ধরা হয়, কিন্তু রোগের প্রাদুর্ভাব বদলায়; সেখান থেকে positive predictive value নতুন করে গোনা আসলে label shift-এর সংশোধন। আবহাওয়ার পূর্বাভাসে, ঋণ-scoring-এ (যেখান থেকে PSI এসেছে) আর spam filtering-এ (যেখানে প্রেরকেরা ইচ্ছা করে নিয়ম বদলায়) প্রতিটির নিজস্ব হাতিয়ার আছে।

এই ইতিহাস থেকে একটি ব্যবহারিক শিক্ষা: নতুন সমস্যায় পড়লে অন্য শাখার নাম ধরেও খুঁজুন। Domain adaptation, transfer learning, quantification, survey reweighting আর selection bias সবই একই পরিবারের।

মূল উপলব্ধি: Distribution shift অর্থনীতি, জরিপ, চিকিৎসা আর ML-এ আলাদা নামে বারবার আবিষ্কৃত হয়েছে; কোন অংশ স্থির ধরা হচ্ছে, সেই অনুমানই প্রতিটি শাখার পদ্ধতিকে একসূত্রে বাঁধে।

প্র ০২Google Flu Trends আর বাস্তব ডেটার benchmark: চালু model shift-এ কীভাবে ব্যর্থ হয়েছে, প্রমাণ কী বলে?

সবচেয়ে আলোচিত উদাহরণগুলোর একটি Google Flu Trends. ২০০৮ সালের দিকে চালু এই প্রকল্প search query থেকে যুক্তরাষ্ট্রে ফ্লু-জাতীয় অসুস্থতার হার অনুমান করত। David Lazer ও সহকর্মীরা ২০১৪ সালে Science-এ (“The Parable of Google Flu”) দেখান, ২০১২–১৩ মৌসুমে এর অনুমান সরকারি নজরদারির হিসাবের প্রায় দ্বিগুণে পৌঁছায়। তাঁদের ব্যাখ্যায় কয়েকটি কারণ আসে: search engine নিজেই বদলাচ্ছিল (প্রস্তাবিত query, ফলাফলের নকশা), মানুষের search-এর অভ্যাস বদলাচ্ছিল, আর model এমন query-ও ব্যবহার করেছিল যা শুধু মৌসুমের সঙ্গে মেলে, ফ্লুর সঙ্গে নয়। ভাষাটি এই পর্বের: x (query) আর y (অসুস্থতা)-র সম্পর্ক বদলেছিল, অর্থাৎ concept shift, আর পুরোনো correlation-এর ওপর ভর দেওয়া model তা টের পায়নি।

গবেষণার দিক থেকে Pang Wei Koh ও সহকর্মীরা ২০২১ সালে ICML-এ WILDS নামে একটি benchmark প্রকাশ করেন, যেখানে বাস্তব shift-সহ ডেটাসেট একত্র করা হয়েছে: ভিন্ন হাসপাতালের টিস্যুর ছবি, ভিন্ন অঞ্চলের উপগ্রহ-ছবি, ভিন্ন সময়ের লেখা ইত্যাদি। তাঁদের ফলাফলে বহু ক্ষেত্রে একই বণ্টনের test-এর তুলনায় নতুন domain-এ মান চোখে পড়ার মতো কমে, আর তখনকার বিশেষ robust পদ্ধতিগুলো সাধারণ training-কে ধারাবাহিকভাবে ছাড়াতে পারেনি। সংখ্যাগুলো ডেটাসেটভেদে আলাদা, তাই একটি সাধারণ “এত শতাংশ কমে” বলা ঠিক নয়।

২০২০ সালে মহামারিতে কেনাকাটা আর লেনদেনের ধরন হঠাৎ বদলালে বহু প্রতিষ্ঠান চাহিদা-অনুমান আর প্রতারণা-শনাক্তের model নিয়ে সমস্যার কথা বলেছে; তবে এসব বেশিরভাগই অভিজ্ঞতার বর্ণনা, নিয়ন্ত্রিত তুলনা নয়।

মূল উপলব্ধি: বাস্তবের বড় ব্যর্থতাগুলো প্রায়ই concept shift, যা input-এর পরিসংখ্যানে ধরা পড়ে না; নিয়মিত আসল ফলের সঙ্গে মেলানো আর সত্যিকারের নতুন domain-এ মূল্যায়ন ছাড়া model-এর স্থায়িত্বে আস্থা রাখা যায় না।

প্র ০৩Importance weighting তত্ত্বে নিখুঁত, তবু বাস্তবে প্রায়ই হতাশ করে কেন? Deep learning-এ এর প্রভাব নিয়ে কী জানা গেছে?

তত্ত্বে ওজনসহ গড় নতুন বণ্টনের প্রত্যাশিত loss-এর নিরপেক্ষ অনুমান। কিন্তু তিনটি জায়গায় বাস্তব হোঁচট খায়। প্রথম, ওজনগুলো অনুমিত, সত্য নয়। Domain classifier ভুল calibrated হলে, বিশেষত প্রান্তে, odds অনেক বেশি ফুলে যায়; ০.৯৯ আর ০.৯৯৯-এর মধ্যে ছোট পার্থক্য ওজনে দশগুণ পার্থক্য আনে। তাই domain classifier calibrate করা (পর্ব ২৬) আর ওজন কাটা প্রচলিত। দ্বিতীয়, variance. এই পর্বের ব্লকে ৩,০০০ সারির ESS ছিল প্রায় ৩০০; ছোট ডেটায় বা বড় shift-এ ESS কয়েক ডজনে নামতে পারে, তখন ওজনসহ model এলোমেলো ওঠানামা করে। তৃতীয়, support. নতুন বণ্টনের যে অঞ্চলে training-এ কোনো সারি নেই, সেখানে কোনো ওজন তথ্য তৈরি করতে পারে না।

আরেকটি সূক্ষ্ম বিষয় হলো model কতটা নমনীয়। Shimodaira-র বিশ্লেষণে ওজনের লাভ মূলত misspecified model-এ: যখন model সত্য P(y | x) ধরতে পারে না, তখন কোন অঞ্চলে ভালো হবে তা ঠিক করতে ওজন লাগে। Model যদি যথেষ্ট নমনীয় আর ডেটা যথেষ্ট হয়, ওজন ছাড়াও সব অঞ্চলে সঠিক নিয়ম শেখে। Jonathon Byrd আর Zachary Lipton ২০১৯ সালে ICML-এ দেখান, অনেক বড় neural network-এ, যেখানে training ডেটা প্রায় নিখুঁতভাবে আলাদা হয়ে যায়, দীর্ঘ training-এর পর importance weighting-এর প্রভাব অনেকটাই মিলিয়ে যায়; regularization বা early stopping-এর সঙ্গে প্রভাব কিছুটা ফেরে। এটি একটি নির্দিষ্ট পরিস্থিতির পর্যবেক্ষণ, সব ক্ষেত্রে সাধারণীকরণ নয়।

এর উল্টো বিপদও আছে। বাজারঘরের নতুন জেলার ডেটায় ডেস্কটপে আলাদা একটি পরীক্ষায় ওজন ছাড়া HistGradientBoosting-এর log loss ছিল প্রায় ০.৫৭ (তিনটি training seed, একটি run), সরল logistic-এর ০.৫৩-এর চেয়েও খারাপ, কারণ tree training-এর পরিসরের বাইরে সমতল অনুমান দেয়। অর্থাৎ নমনীয়তা extrapolation-এর সমস্যা সমাধান করে না।

সবচেয়ে সোজা বিকল্প প্রায়ই নতুন domain-এর কয়েকশো সারি label করা।

মূল উপলব্ধি: Importance weighting সীমিত model আর ভালো overlap-এ সাহায্য করে, কিন্তু অনুমিত ওজন, ছোট ESS আর অনুপস্থিত অঞ্চল তাকে দুর্বল করে; ESS আর কয়েকটি seed না দেখে এর লাভ দাবি করবেন না।

প্র ০৪কোন shift-এর অনুমান স্বাভাবিক, তা কি কারণের দিক থেকে বোঝা যায়? “causal আর anticausal learning”-এর যুক্তি কী?

Covariate shift ধরে P(y | x) স্থির, label shift ধরে P(x | y) স্থির। কোনটি বিশ্বাসযোগ্য? Bernhard Schölkopf, Dominik Janzing, Jonas Peters ও সহকর্মীরা ২০১২ সালে ICML-এ “On causal and anticausal learning” প্রবন্ধে একটি যুক্তি দেন: যে প্রক্রিয়া কারণ থেকে ফল তৈরি করে, তা সাধারণত স্থির থাকে, আর কারণের বণ্টন বদলায়। তাঁরা এই স্বাধীন প্রক্রিয়ার ধারণাকে বিস্তারিতভাবে বিশ্লেষণ করেন।

এখান থেকে একটি সুন্দর নিয়ম আসে। যদি x কারণ আর y ফল (causal দিক), যেমন অর্ডারের দূরত্ব আর COD থেকে ফেরত, তাহলে P(y | x) হলো সেই প্রক্রিয়া, আর নতুন জেলায় শুধু P(x) বদলায়: covariate shift স্বাভাবিক। যদি y কারণ আর x ফল (anticausal দিক), যেমন রোগ থেকে উপসর্গ বা ফেরতের ইচ্ছা থেকে ক্রেতার আচরণ, তাহলে P(x | y) হলো প্রক্রিয়া, আর ডেঙ্গুর মৌসুমে শুধু P(y) বদলায়: label shift স্বাভাবিক। ডাক্তারের উপমার প্রথম দুটি দৃশ্য ঠিক এই দুই দিক।

বাস্তবে দিক প্রায়ই মিশ্র। বাজারঘরের feature-এর কিছু কারণ (দূরত্ব, দাম), কিছু ফলের লক্ষণ (ডেলিভারির আগে ক্রেতার ফোন ধরা না-ধরা)। আর concept shift আসে যখন প্রক্রিয়াটিই বদলায়: নীতি, প্রতিযোগী, আইন। তবু প্রশ্নটি কাজের: “নতুন পরিস্থিতিতে কোন প্রক্রিয়া একই থাকার কথা?” এর উত্তর ব্যবসা আর ক্ষেত্রের জ্ঞান থেকে আসে, ডেটা থেকে নয়।

একটি সতর্কতা: এই যুক্তি একটি কাঠামো, প্রমাণ নয়। ডেটা থেকে কারণের দিক নির্ণয় নিজেই কঠিন সমস্যা, যার পরিচয় পর্ব ৬৪ (Causal inference-এর পরিচয় — DAG, matching আর difference-in-differences)-এ আসবে। আর একই সমস্যায় দুই দিকের shift একসঙ্গে থাকতে পারে, তখন কোনো একটি সরল সংশোধন যথেষ্ট নয়।

মূল উপলব্ধি: কারণ থেকে ফলের প্রক্রিয়া সাধারণত স্থির থাকে, তাই x → y হলে covariate shift আর y → x হলে label shift স্বাভাবিক অনুমান; কোনটি বেছে নেবেন, তা ক্ষেত্রের জ্ঞান দিয়ে যুক্তি করুন।

প্র ০৫Model নিজেই যখন ডেটা বদলায়: feedback loop, আটকানো অর্ডারের অদেখা ফল আর “performative prediction”

এই পর্বে shift এসেছে বাইরে থেকে: নতুন জেলা, ঈদ, নীতি। কিন্তু model চালু হলে সে নিজেও জগৎ বদলায়। ধরুন বাজারঘর ঝুঁকিপূর্ণ COD অর্ডারে অগ্রিম টাকা চায়। ফলে ওই ধরনের ক্রেতার কেউ কেউ অর্ডার করা ছেড়ে দেন, কেউ অগ্রিম দিয়ে আর ফেরত দেন না। পরের মাসের ডেটায় model দেখবে, “ঝুঁকিপূর্ণ” অর্ডারে ফেরত কম; সে ভাববে সংকেতটি দুর্বল হয়েছে, অথচ সংকেতটি নিজের কাজের ফলেই লুকিয়েছে। Juan Perdomo, Tijana Zrnic, Celestine Mendler-Dünner আর Moritz Hardt ২০২০ সালে ICML-এ একে “performative prediction” নামে বিশ্লেষণ করেন: যে অনুমান নিজের লক্ষ্য-বণ্টনকে প্রভাবিত করে।

আরেকটি রূপ selective labels. প্রতারণার সন্দেহে আটকানো অর্ডার কখনো পাঠানো হয় না, তাই সেগুলো সত্যিই প্রতারণা ছিল কি না, তা জানা যায় না। শুধু অনুমোদিত অর্ডারে নতুন model শেখালে সে ধীরে ধীরে এমন জগৎ শেখে যেখানে ঝুঁকিপূর্ণ অর্ডার প্রায় নেই।

প্রতিকার নকশার। প্রথম, একটি ছোট এলোমেলো নিয়ন্ত্রণ দল, যেখানে model-এর সিদ্ধান্ত প্রয়োগ হয় না; এর খরচ আছে, কিন্তু এটিই অপক্ষপাত label-এর উৎস। দ্বিতীয়, প্রতিটি সিদ্ধান্তের সময় model-এর সম্ভাবনা লগ করে রাখা, যাতে পরে inverse propensity weighting দিয়ে সংশোধন করা যায়। তৃতীয়, প্রতিটি নতুন ব্যবসায়িক নীতিকে একটি সম্ভাব্য concept shift ধরে model-এর পুনর্মূল্যায়ন।

এই প্রশ্নগুলো ন্যায্যতার সঙ্গেও জড়িত: কোনো দল বারবার আটকানো হলে তাদের সম্পর্কে নতুন তথ্য আসে না, আর ভুল ধারণা স্থায়ী হয়। পরের পর্ব ৬০ (ML-এ ন্যায্যতা — pre-, in- আর post-processing প্রতিকার) সেই দিকটি দেখবে।

মূল উপলব্ধি: চালু model নিজের সিদ্ধান্ত দিয়ে ভবিষ্যতের ডেটা বদলায় আর কিছু ফল চিরতরে অদেখা রাখে; এলোমেলো নিয়ন্ত্রণ দল আর সিদ্ধান্তের সম্ভাবনা লগ না রাখলে এই shift মাপাই যায় না।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার ব্লকগুলোতে সংখ্যা বদলে যাচাই করুন।

  1. পূর্ণ numerical: (ক) চারটি quartile bin-এ পুরোনো ভাগ সবই ০.২৫, নতুন ভাগ (০.১০, ০.২০, ০.৩০, ০.৪০)। PSI কত, আর হাতের নিয়মে রায় কী? (খ) ntrain = ৪,০০০, nnew = ১,০০০. তিনটি training সারিতে domain classifier-এর P(new | x) = ০.১, ০.৫, ০.৮. ওজন আর তিনটির ESS কত? (গ) পুরোনো validation-এ ফেরতের হার π = ০.২০, TPR = ০.৭০, FPR = ০.১০. নতুন সপ্তাহে ৩১% অর্ডার ঝুঁকিপূর্ণ বলা হয়েছে। Label shift ধরে π′ কত, আর model-এর ০.৫০ সম্ভাবনা সংশোধিত হয়ে কত হবে?

    (ক) পদগুলো: (০.১০ − ০.২৫)·ln(০.৪) = ০.১৩৭৪; (০.২০ − ০.২৫)·ln(০.৮) = ০.০১১২; (০.৩০ − ০.২৫)·ln(১.২) = ০.০০৯১; (০.৪০ − ০.২৫)·ln(১.৬) = ০.০৭০৫. যোগ PSI = ০.২২৮২: “নজরে রাখুন” স্তর, ০.২৫-এর নিচে। দুই প্রান্তের bin-ই বেশিরভাগ অবদান রাখে। (খ) অনুপাত ৪,০০০/১,০০০ = ৪. Odds: ০.১১১, ১, ৪; ওজন ০.৪৪৪, ৪, ১৬. ∑w = ২০.৪৪৪, ∑w² = ২৭২.২০, ESS = ৪১৭.৯৫/২৭২.২০ = ১.৫৪: তিনটি সারির তথ্য কার্যত দেড়টির সমান। (গ) π′ = (০.৩১ − ০.১০)/(০.৭০ − ০.১০) = ০.২১/০.৬০ = ০.৩৫. সংশোধন: odds ১ × (০.৩৫/০.৬৫)/(০.২০/০.৮০) = ০.৫৩৮৫/০.২৫ = ২.১৫৩৮; p′ = ২.১৫৩৮/৩.১৫৩৮ = ০.৬৮৩. সংখ্যাগুলো ডেস্কটপে NumPy দিয়ে যাচাই করা।

  2. সমালোচনা ও নকশা: একটি দল লিখেছে: “আমাদের নজরদারি: প্রতি ঘণ্টায় ৮০টি feature-এ KS test, কোনোটিতে p < ০.০৫ হলে স্বয়ংক্রিয় retrain, training-এর শেষ ৭ দিনের ডেটায়। Reference হলো গত বছরের পুরো ডেটা। Label-এর দরকার নেই, কারণ input ঠিক থাকলে model ঠিক থাকে। ঈদের মাসে retrain অনেক বেড়ে যায়, তবে সেটা স্বাভাবিক।” কী কী সমস্যা, আর আপনার নকশা কী?

    সমস্যা: (১) ৮০টি test প্রতি ঘণ্টায়: কোনো বদল না থাকলেও গড়ে প্রতি ঘণ্টায় চারটি p < ০.০৫, তাই retrain প্রায় অবিরাম; সংশোধন নেই। (২) বড় reference (পুরো বছর) নিয়ে KS নগণ্য বদলেও p ≈ ০ দেয়; আকারের মাপ (PSI, D) নেই। (৩) “Input ঠিক থাকলে model ঠিক” ভুল: নীতি বদলের মতো concept shift input-এ দেখা যায় না, বাজারঘরের ব্লকে domain AUC ০.৪৯৩ থাকলেও ভুল বেড়েছিল। (৪) ৭ দিনের ডেটায় retrain: ছোট, গোলমেলে, আর তার label হয়তো এখনো চূড়ান্ত নয় (label delay), তাই অসম্পূর্ণ label-এ শেখা। (৫) ঈদের মাসকে সাধারণ reference-এর সঙ্গে তুলনা: প্রত্যাশিত ঋতুর বদলকে সমস্যা ধরা, আর ঈদের ৭ দিনে retrain করা model ঈদের পরে ভুল করবে। (৬) Retrain-এর পর যাচাই বা rollback-এর কথা নেই। নকশা: দৈনিক PSI (training-এর quantile দিয়ে ঠিক করা bin) আর Bonferroni-সংশোধিত test, ঋতুভেদে আলাদা reference; সাপ্তাহিক out-of-fold domain classifier আর অনুমানের বণ্টন; label চূড়ান্ত হলে log loss আর calibration একটি baseline-এর পাশে; প্রতিদিন একটি ছোট এলোমেলো নমুনা দ্রুত যাচাই; retrain-এর নীতি (জানালা বা recency) অতীতের ধারায় backtest করে বাছা, নতুন model shadow-তে চালিয়ে তবে চালু, আর পুরোনোটি rollback-এর জন্য রাখা।

  3. আউটপুট অনুমান করুন: চালানোর আগে তিনটি লাইন লিখুন।
    import numpy as np
    e = np.array([0.4, 0.4, 0.2]); a = np.array([0.2, 0.4, 0.4])
    print(round(float(np.sum((a - e) * np.log(a / e))), 4))
    p = np.array([0.5, 0.75, 0.9])
    w = p / (1 - p)
    print(w, round(w.sum() ** 2 / (w ** 2).sum(), 3))
    tpr, fpr, r = 0.8, 0.2, 0.5
    print(round((r - fpr) / (tpr - fpr), 3))

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

    0.2773
    [1. 3. 9.] 1.857
    0.5

    প্রথম লাইন: (−০.২)·ln ০.৫ = ০.১৩৮৬, মাঝের bin শূন্য, (+০.২)·ln ২ = ০.১৩৮৬; যোগ ০.২৭৭৩, “বড় বদল” স্তরে। দ্বিতীয়: odds ১, ৩, ৯ (দুই দলের আকার সমান ধরে, তাই অনুপাতের গুণক নেই); ESS = ১৩²/৯১ = ১৬৯/৯১ = ১.৮৫৭. NumPy float array বলে [1. 3. 9.] দশমিক বিন্দুসহ ছাপে। তৃতীয়: (০.৫ − ০.২)/(০.৮ − ০.২) = ০.৫: ঝুঁকিপূর্ণ বলার হার ০.৫ আর আসল হারও ০.৫, এখানে দুই ধরনের ভুল কাকতালীয়ভাবে কাটাকাটি।

  4. কোডিং: একটি function লিখুন em_prior(p, pi), যা Saerens ও সহকর্মীদের EM দিয়ে নতুন prior অনুমান করে: বর্তমান π′ দিয়ে প্রতিটি সম্ভাবনা সংশোধন (পর্ব ২৬-এর সূত্র), তাদের গড়কে নতুন π′ ধরা, পরিবর্তন ১০−৬-এর কম হওয়া পর্যন্ত। পুরো ৩,০০০ সারির training ডেটায় একটি LogisticRegression শিখিয়ে চারটি দৃশ্যে EM-এর π′, আসল হার আর পুনরাবৃত্তির সংখ্যা ছাপান। BBSE-এর সঙ্গে তুলনা করে কী দেখছেন?
    import numpy as np
    from sklearn.linear_model import LogisticRegression
    from bazar_shift import train_data, scenario, FEATS
    
    def em_prior(p, pi, iters=100, tol=1e-6):       # Saerens ও সহকর্মীদের EM
        pi_new = pi
        for k in range(iters):
            a = p * pi_new / pi
            post = a / (a + (1 - p) * (1 - pi_new) / (1 - pi))   # E-ধাপ
            nxt = post.mean()                                     # M-ধাপ
            if abs(nxt - pi_new) < tol:
                break
            pi_new = nxt
        return nxt, k + 1
    
    ref = train_data()
    model = LogisticRegression().fit(ref[FEATS], ref.returned)
    pi = ref.returned.mean()
    for name in ["same", "eid", "district", "policy"]:
        new = scenario(name)
        p = model.predict_proba(new[FEATS])[:, 1]
        est, k = em_prior(p, pi)
        print(f"{name:8s} true {new.returned.mean():.3f}  EM {est:.3f}  ({k} iterations)")

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

    same     true 0.255  EM 0.221  (64 iterations)
    eid      true 0.450  EM 0.394  (60 iterations)
    district true 0.529  EM 1.000  (28 iterations)
    policy   true 0.135  EM 0.245  (51 iterations)

    পড়া: ঈদে EM ০.৩৯৪, BBSE-এর ০.৪১৮-এর চেয়ে সত্য (০.৪৫০) থেকে একটু দূরে। কোনো বদল না থাকলেও EM ০.২২১ বলে, যেখানে সত্য ০.২৫৫: EM ধরে নেয় সম্ভাবনাগুলো নিখুঁত calibrated, অথচ এই সরল logistic model দূরত্বের বাঁক ধরতে পারে না বলে সম্পূর্ণ calibrated নয়, আর EM সেই ভুল বাড়িয়ে দেয়। BBSE শুধু confusion matrix-এ ভর দেয়, তাই এখানে কম পক্ষপাতদুষ্ট, কিন্তু তার variance বেশি। নতুন জেলায় EM ১.০০০-এ গিয়ে ঠেকে (সব অর্ডারকে ফেরত ধরা), BBSE-এর অসম্ভব ১.৫৬৫-এর মতো একই সংকেত: label shift-এর অনুমান ভেঙেছে। নীতি বদলে দুটিই ব্যর্থ। (আসল হার ০.২৫৫ বনাম তৃতীয় ব্লকের ০.২৫৪: একই ০.২৫৪৫, শুধু গোলকরণ আলাদা।)

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

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

ক্রমএকটি চালু model-এ সতর্কতা এলে কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Pipeline-এর ভুল (missing, ভুল একক, ফাঁকা কলাম) আছে কি না যাচাই করা
  2. Feature-এর PSI আর domain classifier দিয়ে কোথায় আর কতটা বদল, তা মাপা
  3. অনুমানের বণ্টন আর আসা label দিয়ে ধরন ঠিক করা: covariate, label না concept
  4. ধরন অনুযায়ী প্রতিকার বাছা: ওজন, prior সংশোধন, না নতুন label-এ retrain
  5. নতুন model আলাদা সময়ের ডেটায় baseline-এর সঙ্গে যাচাই করে তবে চালু করা

একটি অনলাইন দোকানে নতুন একটি পেমেন্ট নীতির পর অর্ডারের দাম, দূরত্ব আর পণ্যের ধরনের বণ্টন হুবহু আগের মতো, কিন্তু একই ধরনের অর্ডারে ফেরতের হার বদলে গেছে। এটি কোন ধরনের shift?

  • Covariate shift
  • Label shift
  • Concept shift
  • কোনো shift নয়, কারণ input একই

তিনটি bin-এ পুরোনো ভাগ (০.৫০, ০.৩০, ০.২০) আর নতুন ভাগ (০.৩০, ০.৩০, ০.৪০)। PSI প্রায় কত?

  • ০.১৩৯
  • ০.২৪১
  • ০.১২৪
  • ০, কারণ ভাগের যোগ দুই দিকেই ১

Training-এর ৬,০০০ সারি আর নতুন ২,০০০ সারিতে একটি domain classifier শেখানো হলো। একটি training সারির জন্য P(new | x) = ০.৬. এই সারির importance weight কত?

  • ০.৬
  • ১.৫
  • ০.৫
  • ৪.৫

চারটি training সারির importance weight ১, ১, ১ আর ৯. Kish-এর effective sample size কত?

  • প্রায় ১.৭১
  • ৪
  • ১২
  • ০.৫

পুরোনো ডেটায় একটি classifier-এর TPR = ০.৮০, FPR = ০.২০. নতুন ডেটায় সে ৩৮% সারিকে positive বলছে। Label shift ধরে নতুন positive হার কত?

  • ০.৩৮
  • ০.৩০
  • ০.৩০৪
  • ০.৪৭৫

একটি model-এর input-এর সব feature-এ PSI ০.০২-এর নিচে আর out-of-fold domain classifier-এর AUC ০.৫০, কিন্তু এক মাস পরে আসা label-এ log loss অনেক বেড়েছে। সবচেয়ে যুক্তিসংগত ব্যাখ্যা কোনটি?

  • বড় covariate shift
  • শুধু কাকতালীয় ওঠানামা, কিছুই করার নেই
  • Concept shift: একই input-এ উত্তরের নিয়ম বদলেছে
  • Importance weight দিলেই ঠিক হবে

শব্দকোষ

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

মনে রাখুন

  1. তিন ধরন: covariate (P(x) বদলায়, P(y | x) স্থির), label (P(y) বদলায়, P(x | y) স্থির), concept (P(y | x) বদলায়)। কোন অংশ স্থির, সেই অনুমানই পদ্ধতি ঠিক করে।
  2. ধরা: PSI = ∑(a − e)ln(a/e) = দুই দিকের KL; KS আর chi-square-এ multiple testing মাথায় রাখুন। Out-of-fold domain classifier-এর AUC ≈ ০.৫ মানে input-এ ধরার মতো বদল নেই, model ঠিক আছে এমন নয়।
  3. Covariate: w = odds × ntrain/nnew; ESS = (∑w)²/∑w² দিয়ে দাম মাপুন, প্রয়োজনে কাটুন। লাভ মূলত সীমিত model আর ভালো overlap-এ।
  4. Label: π′ = (r − FPR)/(TPR − FPR), সাধারণ রূপে C·w = μ (BBSE); তারপর odds সংশোধন। π′ ০–১-এর বাইরে গেলে অনুমান ভাঙা।
  5. Concept: নতুন label ছাড়া উপায় নেই; sliding window বা recency weighting, একটি স্থির baseline-এর সঙ্গে backtest করে বাছুন।

পরের পর্বে: পর্ব ৬০ (ML-এ ন্যায্যতা — pre-, in- আর post-processing প্রতিকার)। Model-এর ভুল সব দলে সমান নয়; group-এর মাপ অনিশ্চয়তাসহ গোনা, আর reweighing, শাস্তিযুক্ত শেখা আর দলভেদে threshold দিয়ে প্রতিকারের দাম মাপা।

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

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