এই পর্বে যা শিখবেন
- ML system-এর কোন স্তরে কী test লাগে, আর ML Test Score-এর চার ভাগ চেনা
- pandas-এ data contract লেখা, আর একই নিয়ম pytest আর pandera দিয়ে চালানো
- Slice, invariance, directional আর regression test দিয়ে নতুন model যাচাই
- Deploy-এর পরে চার স্তরের নজরদারি সাজানো, আর alert-এর সীমার ভুয়া সতর্কতা হিসাব করা
- দেরিতে আসা label দিয়ে মান মাপা, retraining trigger লেখা, আর incident থেকে rollback পর্যন্ত ধাপ জানা
AI-এর ভিত্তি কোর্সের পর্ব ৭৪-এ নজরদারি, PSI, label delay আর rollback-এর প্রথম পরিচয় পেয়েছেন, একই কোর্সের পর্ব ৬৪-এ Pipeline আর পর্ব ১২-এ uv আর পিন করা version. pytest-এর মূল কথা আছে Python প্রোগ্রামিং কোর্সের টেস্টিং পর্বে। Drift মাপার পরিসংখ্যান (KS, PSI, domain classifier) এই কোর্সের পর্ব ৫৯-এ; এখানে আর নতুন করে নয়। এই পর্বের প্রশ্ন ব্যবহারিক: কী test লিখবেন, কোন সংখ্যা রোজ দেখবেন, কখন কাউকে ঘুম থেকে তুলবেন, আর কখন model আবার শেখাবেন। চারটি চালানো যায় এমন ব্লক পাতাতেই চলে (ব্রাউজারের Python); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে। pytest আর pandera পাতায় চলে না, তাই সেগুলো ডেস্কটপে সত্যিকারের run.
১ · ML system-এ কী কী ভাঙে — কোড, ডেটা আর model
একটি পোশাক কারখানার মান-নিয়ন্ত্রণ ভাবুন। কাপড় গেটে ঢোকার সময় মাপ আর রং দেখা হয়। কাটিংয়ের পর প্যাটার্ন মেলানো হয়। তৈরি শার্টের কিছু নমুনা টেনে, ধুয়ে, সেলাই পরীক্ষা হয়। আর দোকানে পাঠানোর পরও ফেরত আর অভিযোগের হিসাব রাখা হয়। শুধু সেলাই মেশিন ঠিক আছে কি না দেখলে চলে না: খারাপ কাপড় এলে নিখুঁত মেশিনও খারাপ শার্ট বানায়।
সাধারণ সফটওয়্যারে আচরণ লেখা থাকে কোডে, তাই কোডের unit test অনেকটা নিশ্চয়তা দেয়। ML system-এর আচরণ আসে ডেটা থেকে শেখা থেকে, তাই কোড একটুও না বদলালেও আচরণ বদলাতে পারে: উৎসের ডেটা বদলালে, feature-এর হিসাব training আর serving-এ আলাদা হলে, বা পৃথিবী বদলালে। D. Sculley ও সহকর্মীরা ২০১৫ সালে NIPS (এখন NeurIPS) সম্মেলনে “Hidden Technical Debt in Machine Learning Systems” প্রবন্ধে দেখান, বাস্তব ML system-এ model-এর কোড ছোট একটি অংশ; চারপাশের ডেটা সংগ্রহ, যাচাই, feature আর নজরদারির কাঠামোই বড়। Eric Breck ও সহকর্মীরা (Google) ২০১৭ সালে ML Test Score নামে একটি নম্বর-তালিকা প্রস্তাব করেন: ডেটা, model, অবকাঠামো আর নজরদারি, চার ভাগে মোট ২৮টি test; হাতে করলে আধা নম্বর, স্বয়ংক্রিয় হলে পুরো, আর চূড়ান্ত নম্বর চার ভাগের সবচেয়ে কমটি। সবচেয়ে দুর্বল স্তরই system-এর নির্ভরযোগ্যতা ঠিক করে, এই ভাবনাটি এই পর্বের মেরুদণ্ড।
বাজারঘরের ML system আর প্রতিটি ধাপের পাহারা
বাঁয়ে ডেটার পথ, ডানে সেই ধাপের test বা নজরদারি; নিচ থেকে ওপরের ভাঙা তীর হলো retraining-এর পথ
সাধারণ সফটওয়্যারের test pyramid (সফটওয়্যার ইঞ্জিনিয়ারিং কোর্সের test pyramid পর্ব) ML-এও কাজে লাগে, তবে স্তরগুলো একটু আলাদা। নিচের stepper নিচ থেকে ওপরে যায়: নিচে অনেক, দ্রুত, সস্তা test; ওপরে কম, ধীর, দামি।
- Unit test
features.pyশত শত, মিলিসেকেন্ডে - Data contract
check(batch)প্রতিটি ব্যাচে - Model test
test_model.pyপ্রতিটি নতুন model-এ - Pipeline test
raw → scoreছোট ডেটায় পুরো পথ - নজরদারি
daily metricsচালু অবস্থায়, অবিরাম
- Unit test, feature-এর কোড। “৯০ দিনের অর্ডার গোনা” function জানা ছোট টেবিলে ঠিক উত্তর দেয় কি না। ডেটা বা model লাগে না, তাই হাজারটি test-ও সেকেন্ডে চলে। ভুল এখানে ধরা সবচেয়ে সস্তা।
- Data contract. কোড ঠিক, কিন্তু আজকের ফাইল? প্রতিটি নতুন ব্যাচ model-এ ঢোকার আগে schema, null, সীমা আর সতেজতা যাচাই। ব্যর্থ হলে ব্যাচ আলাদা করে রাখা হয় (quarantine), চুপচাপ বাদ নয়।
- Model test. নতুন শেখা model-কে চালু করার আগে: নির্দিষ্ট slice-এ ন্যূনতম মান, আচরণের নিয়ম আর আগের version-এর সঙ্গে তুলনা। এক একটিতে কয়েক সেকেন্ড থেকে মিনিট।
- Pipeline test. কাঁচা ডেটার কয়েকশো সারি থেকে feature, model আর score পর্যন্ত পুরো পথ একবার চালানো, যাতে ধাপগুলোর জোড়া ঠিক থাকে (কলামের নাম, ক্রম, dtype). সংখ্যায় কম, কারণ ধীর।
- নজরদারি। চালু হওয়ার পর পৃথিবী বদলায়; কোনো test আগে থেকে তা জানে না। তাই pyramid-এর চূড়া আসলে test নয়, অবিরাম মাপ: service, ডেটার মান, অনুমানের বণ্টন আর label এলে আসল মান।
২ · Data contract — প্রতিটি ব্যাচের গেট-পরীক্ষা
Data contract হলো ডেটা যে পাঠায় আর যে ব্যবহার করে, তাদের মধ্যে লিখিত চুক্তি: কোন কলাম থাকবে, কোন schema আর dtype, কোন সীমার মধ্যে মান, কোথায় null চলবে না, কোন ID অনন্য, কোন শ্রেণি অনুমোদিত, ব্যাচে কমপক্ষে কত সারি, আর ডেটা কত পুরোনো হলে চলবে না (freshness). নামটি ২০২০-এর দশকের শুরুতে ডেটা-প্রকৌশলের আলোচনায় জনপ্রিয় হয়েছে, তবে ভাবনাটি পুরোনো data validation; Breck ও সহকর্মীরা ২০১৯ সালে SysML-এ Google-এর ডেটা যাচাইয়ের অভিজ্ঞতা লেখেন। নিচের module বাজারঘরের (কাল্পনিক দোকান) একদিনের অর্ডারের ব্যাচ বানায়, আর bad=True দিলে বাস্তবে যা যা ঘটে তার একটি মিশ্রণ: অর্ধেক ফাইল, নকল ID, টাকার বদলে পয়সা, ঋণাত্মক দাম, ফাঁকা জেলা, “dhaka ” বানানের জেলা, অচেনা পেমেন্ট, লেখা হয়ে যাওয়া সংখ্যা আর তিন দিনের পুরোনো ফাইল। একই module churn আর জালিয়াতির ডেটাও বানায়, যা পরের অংশগুলোতে লাগবে।
# বাজারঘর (কাল্পনিক দোকান): test আর নজরদারির seeded ডেটা
import numpy as np
import pandas as pd
DISTRICTS = ["Dhaka", "Chattogram", "Khulna", "Rajshahi",
"Sylhet", "Barishal", "Rangpur", "Mymensingh"]
PAY = ["COD", "bKash", "Nagad", "card"]
FEATS = ["orders_90d", "late_deliveries", "days_since_last", "complaints"]
def orders(n=500, seed=69, bad=False): # এক দিনের অর্ডারের ব্যাচ
rng = np.random.default_rng(seed)
df = pd.DataFrame({
"order_id": np.arange(100000, 100000 + n),
"district": rng.choice(DISTRICTS, n),
"payment": rng.choice(PAY, n, p=[0.45, 0.3, 0.15, 0.1]),
"amount_tk": rng.lognormal(7.0, 0.7, n).round(),
"items": rng.integers(1, 8, n),
"created_at": pd.Timestamp("2026-11-05 08:00")
+ pd.to_timedelta(rng.uniform(0, 14, n), unit="h")})
if bad: # ভাঙা ব্যাচ: বাস্তবে যা যা ঘটে
df = df.iloc[: n // 2].copy() # অর্ধেক ফাইল এসেছে
df.loc[df.index[:4], "order_id"] = df.order_id.iloc[4]
df.loc[df.index[10:40], "amount_tk"] *= 100 # টাকার বদলে পয়সা
df.loc[df.index[40:43], "amount_tk"] = -1.0
df.loc[df.index[50:62], "district"] = None
df.loc[df.index[62:66], "district"] = "dhaka "
df.loc[df.index[70:73], "payment"] = "EMI"
df["items"] = df["items"].astype(str) # CSV থেকে লেখা হয়ে এল
df["created_at"] -= pd.Timedelta(days=3) # পুরোনো ফাইল
return df
def customers(n, seed): # churn model-এর ডেটা
rng = np.random.default_rng(seed)
o = rng.poisson(4, n) + 1
late = rng.binomial(o, 0.2)
days = rng.exponential(25, n).round()
comp = rng.poisson(0.3, n)
z = -1.2 + 0.45 * late - 0.3 * o + 0.035 * days + 0.6 * comp
return pd.DataFrame({
"customer_id": rng.permutation(n) + 5000,
"name": rng.choice(["Rahim", "Karim", "Ayesha", "Nusrat", "Tanvir"], n),
"district": rng.choice(DISTRICTS, n),
"orders_90d": o, "late_deliveries": late, "days_since_last": days,
"complaints": comp, "churn": rng.binomial(1, 1 / (1 + np.exp(-z)))})
def prod_log(days=60, seed=69): # জালিয়াতি model-এর ৬০ দিনের log
rng, parts = np.random.default_rng(seed), []
for d in range(days):
fri = d % 7 == 4 # শুক্রবার: flash sale
n = rng.poisson(900 if fri else 600)
new = rng.binomial(1, 0.32 if fri else 0.2, n)
amt = rng.lognormal(7.25 if fri else 7.0, 0.7, n)
cod = rng.binomial(1, 0.45, n)
mism = rng.binomial(1, 0.08, n).astype(float)
mism[rng.random(n) < 0.005] = np.nan # স্বাভাবিক অল্প null
if 48 <= d <= 51: # app-এর নতুন version-এর bug
mism[rng.random(n) < 0.3] = np.nan
# চালু model-এর score (null-কে 0 ধরে); z = আসল ঝুঁকি
x = -4.2 + 1.4 * new + 0.4 * (np.log(amt) - 7) + 0.6 * cod + 1.6 * np.nan_to_num(mism)
z = x + (1.8 * cod * (1 - new) if d >= 30 else 0) # দিন ৩০ থেকে নতুন কৌশল
parts.append(pd.DataFrame({"day": d, "new_acct": new, "amount": amt, "cod": cod,
"addr_mismatch": mism, "score": 1 / (1 + np.exp(-x)),
"fraud": rng.binomial(1, 1 / (1 + np.exp(-z)))}))
log = pd.concat(parts, ignore_index=True)
log["label_day"] = log.day + 14 # chargeback নিশ্চিত হয় ১৪ দিন পরে
return logপ্রথম ব্লক contract-টিকে একটি function হিসেবে লেখে। দুটি নকশার সিদ্ধান্ত লক্ষ করুন। প্রথমত, প্রথম ভুলেই থেমে না গিয়ে সব নিয়ম চালিয়ে সব ব্যর্থতা জমানো হয়, যাতে একবারেই পুরো ছবি পাওয়া যায়। দ্বিতীয়ত, শুধু সারি-ধরে নিয়ম (সীমা, null) নয়, পুরো ব্যাচের একটি নিয়মও আছে: দামের গড়।
import pandas as pd
from pandas.api import types as T
from bazar_mon import orders, DISTRICTS, PAY
NOW = pd.Timestamp("2026-11-05 23:00") # ব্যাচ পৌঁছানোর সময়
DTYPE = {"order_id": T.is_integer_dtype, "amount_tk": T.is_float_dtype,
"items": T.is_integer_dtype, "district": T.is_string_dtype,
"created_at": T.is_datetime64_any_dtype}
def check(df): # data contract: সব নিয়ম, সব ব্যর্থতা
fails = []
def rule(name, ok, detail):
if not ok: fails.append(f"{name}: {detail}")
for col, is_ok in DTYPE.items():
rule(f"dtype {col}", is_ok(df[col]), str(df[col].dtype))
rule("rows >= 400", len(df) >= 400, f"{len(df)} rows")
rule("order_id unique", df.order_id.is_unique,
f"{df.order_id.duplicated().sum()} duplicates")
for col in ["district", "payment", "amount_tk"]:
rule(f"no null {col}", df[col].notna().all(), f"{df[col].isna().sum()} nulls")
for col, allowed in [("district", DISTRICTS), ("payment", PAY)]:
odd = df[col].dropna()[~df[col].dropna().isin(allowed)]
rule(f"{col} allowed", odd.empty, f"{len(odd)} rows {sorted(odd.unique())}")
out = ~df.amount_tk.between(20, 100_000)
rule("amount_tk 20..100000", not out.any(), f"{out.sum()} rows, max {df.amount_tk.max():.0f}")
m = df.amount_tk.mean() # রেফারেন্স গড় প্রায় 1400 টাকা
rule("mean amount 700..2800", 700 <= m <= 2800, f"mean {m:.0f}")
age = (NOW - df.created_at.max()).total_seconds() / 3600
rule("fresh <= 24h", age <= 24, f"newest row {age:.0f} h old")
return fails
for name, df in [("clean", orders()), ("corrupted", orders(bad=True))]:
fails = check(df)
print(f"{name}: {len(df)} rows, {len(fails)} failed rule(s)")
for f in fails: print(" FAIL", f)আসল আউটপুট (ব্রাউজারের Python):
clean: 500 rows, 0 failed rule(s)
corrupted: 250 rows, 9 failed rule(s)
FAIL dtype items: str
FAIL rows >= 400: 250 rows
FAIL order_id unique: 4 duplicates
FAIL no null district: 12 nulls
FAIL district allowed: 4 rows ['dhaka ']
FAIL payment allowed: 3 rows ['EMI']
FAIL amount_tk 20..100000: 19 rows, max 655100
FAIL mean amount 700..2800: mean 19359
FAIL fresh <= 24h: newest row 73 h old
পরিষ্কার ব্যাচে কোনো নিয়ম ভাঙেনি; এটিও একটি জরুরি পরীক্ষা, কারণ যে contract ভালো ডেটাতেও বাজে, তাকে কেউ বেশিদিন বিশ্বাস করে না। ভাঙা ব্যাচে ৯টি নিয়ম ভেঙেছে। সবচেয়ে শিক্ষণীয় সারি দুটি দামের। ৩০টি দাম পয়সায় লেখা, কিন্তু সীমার নিয়ম ধরেছে ১৯টি সারি: ৩টি ঋণাত্মক আর পয়সার ৩০টির মাত্র ১৬টি, কারণ বাকি ১৪টি ১,০০,০০০-এর নিচে, “সম্ভব” দেখায়। গড়ের নিয়ম (১৯,৩৫৯ টাকা, স্বাভাবিক প্রায় ১,৪০০) বাকিটা ধরেছে। শিক্ষা: সারি-ধরে নিয়ম আর বণ্টনের নিয়ম দুটোই লাগে। items-এর dtype ধরা পড়েছে str হিসেবে: সংখ্যাগুলো ঠিক আছে, কিন্তু লেখা হিসেবে এলে model-এ গিয়ে হয় error দেবে, নয়তো নীরবে ভুল encoding হবে। আর ফাইলটি ৭৩ ঘণ্টা পুরোনো: আজকের নামে গতকালের আগের দিনের ডেটা।
ভুল ব্যাচ নিয়ে কী করবেন? খারাপ সারিগুলো চুপচাপ মুছে বাকিটা চালানো সবচেয়ে বিপজ্জনক অভ্যাস: অর্ধেক ফাইলের মতো ভুল তাতে লুকিয়ে যায়। সাধারণ নীতি: গুরুতর নিয়ম (schema, সতেজতা, সারির সংখ্যা) ভাঙলে পুরো ব্যাচ আটকে রাখা আর মালিককে জানানো; হালকা নিয়মে সারিগুলো চিহ্নিত করে আলাদা টেবিলে রাখা। কোন নিয়ম গুরুতর, তা contract-এই লেখা থাকা উচিত।
৩ · Feature test, pytest আর pandera — ডেস্কটপে সত্যিকারের run
ডেটা ঠিক থাকলেও feature-এর কোড ভুল হতে পারে। দুটি test প্রায় প্রতিটি ML প্রকল্পে লাগে। (১) ভবিষ্যৎ ঢোকে না: “১ নভেম্বর পর্যন্ত” feature বানালে পরের অর্ডার যোগ করলেও ফল বদলাবে না; বদলালে training-এ temporal leakage (AI-এর ভিত্তি কোর্সের পর্ব ৫৬)। (২) একজন আর সবাই একই: training-এ feature বানানো হয় পুরো টেবিলে একসঙ্গে, serving-এ একজন ক্রেতার জন্য; দুটির ফল আলাদা হলে তা training-serving skew, যার প্রতিরোধ পর্ব ৬৮ (Model serving — FastAPI, batch scoring আর ONNX)-এর বিষয়। নিচের test ফাইল এই দুটি আর আগের contract-কে pytest-এর test বানায়; contracts.py-তে প্রথম ব্লকের check function হুবহু রাখা।
import pandas as pd
def customer_features(orders, as_of):
past = orders[orders.created_at < as_of] # শুধু as_of-এর আগের অর্ডার
win = past[past.created_at >= as_of - pd.Timedelta(days=90)]
out = pd.DataFrame({"orders_90d": win.groupby("customer_id").size(),
"days_since_last": (as_of - past.groupby("customer_id").created_at.max()).dt.days})
return out.fillna({"orders_90d": 0}).astype(int).sort_index()import pandas as pd
import pytest
from bazar_mon import orders
from contracts import check
from features import customer_features
@pytest.mark.parametrize("bad", [False, True], ids=["clean", "corrupted"])
def test_batch_meets_contract(bad):
fails = check(orders(bad=bad))
assert not fails, "\n".join(fails)
T = pd.Timestamp
HIST = pd.DataFrame({"customer_id": [1, 1, 2],
"created_at": [T("2026-08-01"), T("2026-10-20"), T("2026-10-30")]})
def test_no_future_rows_leak_into_features():
as_of = T("2026-11-01")
future = pd.DataFrame({"customer_id": [1, 2], "created_at": [T("2026-11-03")] * 2})
before = customer_features(HIST, as_of)
after = customer_features(pd.concat([HIST, future]), as_of)
pd.testing.assert_frame_equal(before, after)
def test_one_customer_equals_batch(): # serving-এ এক জন, training-এ সবাই
as_of = T("2026-11-01")
batch = customer_features(HIST, as_of)
single = customer_features(HIST[HIST.customer_id == 1], as_of)
pd.testing.assert_frame_equal(single, batch.loc[[1]])এবার একই contract একটি প্রচলিত library-তে। pandera DataFrame-এর schema ঘোষণা করে যাচাই করে; lazy=True দিলে প্রথম ভুলে না থেমে সব ব্যর্থতার একটি টেবিল (failure_cases) দেয়। সাম্প্রতিক version-এ pandas-এর জন্য import pandera.pandas as pa লেখার পরামর্শ; পুরোনো টিউটোরিয়ালের import pandera as pa এখন সতর্কবার্তা দিতে পারে। Great Expectations আরেকটি জনপ্রিয় বিকল্প, যার API ১.০ version-এ অনেক বদলেছে; AI-এর দেওয়া পুরোনো উদাহরণ সেখানে প্রায়ই চলে না।
from importlib.metadata import version
import pandas as pd
import pandera.pandas as pa
from bazar_mon import orders, DISTRICTS, PAY
NOW = pd.Timestamp("2026-11-05 23:00")
schema = pa.DataFrameSchema(
{"order_id": pa.Column(int, unique=True),
"district": pa.Column(str, pa.Check.isin(DISTRICTS)),
"payment": pa.Column(str, pa.Check.isin(PAY)),
"amount_tk": pa.Column(float, pa.Check.in_range(20, 100_000)),
"items": pa.Column(int, pa.Check.in_range(1, 50)),
"created_at": pa.Column("datetime64[ns]", pa.Check(
lambda s: s.max() >= NOW - pd.Timedelta(hours=24), name="fresh_24h"))},
checks=pa.Check(lambda df: len(df) >= 400, name="rows_400"),
strict=True) # অজানা কলাম এলেও ব্যর্থ
print("pandera", version("pandera"), "| pandas", version("pandas"))
schema.validate(orders()) # পরিষ্কার ব্যাচ: কোনো exception নেই
print("clean batch: OK")
try:
schema.validate(orders(bad=True), lazy=True) # lazy: সব ভুল একসঙ্গে
except pa.errors.SchemaErrors as e:
fc = e.failure_cases.assign(check=lambda d: d.check.str.slice(0, 22))
print(fc.groupby(["column", "check"], dropna=False).size().rename("rows").to_string())সত্যিকারের run (ডেস্কটপ, Python 3.14.4; লেখার সময়ে pandera 0.35.0, pandas 3.1.1; project বানানো uv init --vcs none --author-from none দিয়ে):
$ uv run python pandera_check.py
pandera 0.35.0 | pandas 3.1.1
clean batch: OK
column check
amount_tk in_range(20, 100000) 19
created_at fresh_24h 1
district isin(['Dhaka', 'Chatto 4
not_nullable 12
items dtype('int64') 1
in_range(1, 50) 1
order_id field_uniqueness 5
payment isin(['COD', 'bKash', 3
NaN rows_400 1
নিজে লেখা contract-এর সঙ্গে মিলিয়ে দেখুন: একই ১৯টি দামের সারি, ১২টি null আর ৪টি অচেনা জেলা, ৩টি “EMI”, সারির সংখ্যা আর সতেজতা। দুটি পার্থক্য শিক্ষণীয়। নকল ID-তে pandera ৫টি সারি গোনে (মূল সারিসহ সব নকল), আমাদের duplicated() ৪টি (প্রথমটি বাদে); সংখ্যা পড়ার আগে সংজ্ঞা জানুন। আর গড়ের নিয়ম pandera নিজে থেকে জানে না; আপনি না লিখলে পয়সার বাকি ১৪টি দাম পার পেয়ে যেত। Library ছকটি দেয়, কোন নিয়ম লিখবেন তা আপনার ব্যবসার জ্ঞান।
৪ · Model test — slice, invariance, directional আর আগের version
একটি AUC সংখ্যা model-এর আচরণের খুব সামান্যই বলে। Marco Tulio Ribeiro ও সহকর্মীরা ২০২০ সালে ACL-এ CheckList নামে NLP model-এর behavioural testing-এর একটি পদ্ধতি দেন, যার তিন ধরনের test সারণির ডেটাতেও সহজে খাটে। এর সঙ্গে আরও দুটি যোগ করে বাজারঘরের churn model-এর জন্য পাঁচটি নিয়ম:
- ন্যূনতম মান, নির্দিষ্ট slice-এ (slice evaluation): পুরো test set, ঢাকা, আর নতুন ক্রেতা (৯০ দিনে ≤ ২টি অর্ডার), প্রতিটিতে AUC ≥ ০.৭৫. Slice-গুলো আগে থেকে ঠিক করা, ফল দেখে বাছা নয়।
- Invariance test: যা ফল বদলানোর কথা নয় (নাম, customer ID), তা বদলালে score হুবহু একই থাকবে।
- Directional expectation test: দেরিতে ডেলিভারি একটি বাড়ালে churn ঝুঁকি কোনো ক্রেতার জন্যই কমবে না।
- মোটা দাগের calibration: গড় সম্ভাবনা আর আসল churn হারের পার্থক্য ≤ ০.০২ (পুরো ছবি পর্ব ২৬-এ)।
- Regression test: কোনো slice-এ চালু model (v1)-এর চেয়ে ০.০১-এর বেশি খারাপ নয়।
তিনটি model পরীক্ষা হবে। v1 চালু logistic regression. v2 নতুন HistGradientBoostingClassifier, যার কলাম বাছা হয়েছে “সব সংখ্যার কলাম” দিয়ে, AI-এর লেখা কোডে যা খুব সাধারণ; তাতে customer_id-ও ঢুকে গেছে। v3 একই model, কিন্তু কলামের তালিকা হাতে লেখা আর monotonic constraint দিয়ে দিক বাঁধা (অর্ডার বাড়লে ঝুঁকি কমে, বাকি তিনটিতে বাড়ে; পর্ব ৩৬)।
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import HistGradientBoostingClassifier as HGB
from sklearn.metrics import roc_auc_score
from bazar_mon import customers, FEATS
tr, te = customers(4000, seed=1), customers(3000, seed=2)
num = tr.select_dtypes("number").columns.drop("churn").tolist() # ভুল: customer_id-ও ঢোকে
MODELS = {"v1 live": (LogisticRegression(max_iter=1000), FEATS),
"v2 new": (HGB(max_iter=100, max_depth=3, learning_rate=0.05, random_state=0), num),
"v3 fixed": (HGB(max_iter=100, max_depth=3, learning_rate=0.05,
monotonic_cst=[-1, 1, 1, 1], random_state=0), FEATS)} # দিক বেঁধে দেওয়া
SLICES = {"all": te.churn >= 0, "Dhaka": te.district == "Dhaka",
"new (<=2 orders)": te.orders_90d <= 2}
res, base = {}, None
for name, (m, cols) in MODELS.items():
m.fit(tr[cols], tr.churn)
score = lambda d: m.predict_proba(d[cols])[:, 1]
p, r = score(te), {}
auc = {s: roc_auc_score(te.churn[k], p[k]) for s, k in SLICES.items()}
r["AUC all/Dhaka/new"] = "/".join(f"{v:.3f}" for v in auc.values())
r["slice AUC >= 0.75"] = min(auc.values()) >= 0.75
for col, val in [("name", "Rahim"), ("customer_id", 99999)]:
r[f"{col} invariant"] = np.abs(score(te.assign(**{col: val})) - p).max() < 1e-12
lower = score(te.assign(late_deliveries=te.late_deliveries + 1)) < p - 1e-9
r["late+1 not lower"] = "PASS" if lower.sum() == 0 else f"FAIL {lower.sum()} rows"
r["|mean p-rate|<=.02"] = abs(p.mean() - te.churn.mean()) <= 0.02
base = base or auc
r["vs v1: drop<=0.01"] = all(auc[s] >= base[s] - 0.01 for s in SLICES)
res[name] = r
t = pd.DataFrame(res).replace({True: "PASS", False: "FAIL"})
print("churn rate:", round(te.churn.mean(), 3))
print(t.to_string())আসল আউটপুট (ব্রাউজারের Python):
churn rate: 0.268
v1 live v2 new v3 fixed
AUC all/Dhaka/new 0.765/0.771/0.760 0.754/0.776/0.772 0.756/0.778/0.769
slice AUC >= 0.75 PASS PASS PASS
name invariant PASS PASS PASS
customer_id invariant PASS FAIL PASS
late+1 not lower PASS FAIL 17 rows PASS
|mean p-rate|<=.02 PASS PASS PASS
vs v1: drop<=0.01 PASS FAIL PASS
v2 ঢাকা আর নতুন ক্রেতার slice-এ v1-এর চেয়ে ভালো (০.৭৭৬ আর ০.৭৭২), সামগ্রিক AUC মাত্র ০.০১১ কম; শুধু AUC দেখলে কেউ হয়তো চালু করে দিতেন। কিন্তু customer ID বদলালে তার score বদলায়: model ক্রেতার নম্বর থেকে “শিখেছে”, যা নতুন ক্রেতার ID-তে অর্থহীন। আর ৩,০০০ জনের ১৭ জনের ক্ষেত্রে একটি দেরি বাড়ালে ঝুঁকি কমে; ক্রেতা-সেবা দল এমন model-এর ব্যাখ্যা দিতে পারবে না। নামের invariance তিনটিতেই পাস, কারণ নামের কলাম কোনো model-এ নেই; test-টি তবু রাখার কারণ, ভবিষ্যতে কেউ ভুল করে কলামটি যোগ করলে এটিই ধরবে। v3 সব test পাস করে, কিন্তু v1-এর চেয়ে স্পষ্ট ভালোও নয় (০.৭৫৬ বনাম ০.৭৬৫)। এই কাল্পনিক ডেটার আসল নিয়ম রৈখিক, তাই logistic model-ই প্রায় সেরা। Test পাস করা মানে “চালু করা নিরাপদ”, “চালু করা উচিত” নয়; সেই সিদ্ধান্তের জন্য অনলাইন তুলনা (পর্ব ৬২) লাগে।
একই test-গুলো pytest-এ থাকলে প্রতিটি নতুন model CI-তে আপনাআপনি যাচাই হয়। models.py উপরের তিনটি model শেখায়; fixture-এর params দিয়ে প্রতিটি test v2 আর v3, দুটির ওপরেই চলে।
import numpy as np
import pytest
from sklearn.metrics import roc_auc_score
from bazar_mon import customers
from models import train_all
TE = customers(3000, seed=2)
SLICES = {"all": TE.churn >= 0, "Dhaka": TE.district == "Dhaka", "new": TE.orders_90d <= 2}
M = train_all()
def score(v, df):
m, cols = M[v]
return m.predict_proba(df[cols])[:, 1]
def aucs(v):
p = score(v, TE)
return {s: roc_auc_score(TE.churn[k], p[k]) for s, k in SLICES.items()}
@pytest.fixture(params=["v2", "v3"])
def cand(request):
return request.param
def test_every_slice_auc_at_least_075(cand):
assert min(aucs(cand).values()) >= 0.75
@pytest.mark.parametrize("col, val", [("name", "Rahim"), ("customer_id", 99999)])
def test_invariant_to_irrelevant_field(cand, col, val):
diff = np.abs(score(cand, TE.assign(**{col: val})) - score(cand, TE))
assert diff.max() < 1e-12, f"{(diff > 1e-12).sum()} scores changed"
def test_more_late_deliveries_never_lower_risk(cand):
up = score(cand, TE.assign(late_deliveries=TE.late_deliveries + 1))
assert (up >= score(cand, TE) - 1e-9).all(), f"{(up < score(cand, TE) - 1e-9).sum()} rows went down"
def test_no_slice_worse_than_live_model(cand):
new, old = aucs(cand), aucs("v1")
worse = {s: round(old[s] - new[s], 4) for s in SLICES if new[s] < old[s] - 0.01}
assert not worse, f"AUC drop vs v1: {worse}"সত্যিকারের run (ডেস্কটপ, Python 3.14.4, pytest 9.2.0, scikit-learn 1.9.1; ফোল্ডারের পথ, শতাংশের কলাম আর “=”-এর সারি ছোট করা):
$ uv run pytest -v --tb=no -rf tests/test_data.py tests/test_model.py
== test session starts ==
platform -- Python 3.14.4, pytest-9.2.0, pluggy-1.7.0
rootdir: ~/bazarghar-ml
configfile: pyproject.toml
collecting ... collected 14 items
tests/test_data.py::test_batch_meets_contract[clean] PASSED
tests/test_data.py::test_batch_meets_contract[corrupted] FAILED
tests/test_data.py::test_no_future_rows_leak_into_features PASSED
tests/test_data.py::test_one_customer_equals_batch PASSED
tests/test_model.py::test_every_slice_auc_at_least_075[v2] PASSED
tests/test_model.py::test_every_slice_auc_at_least_075[v3] PASSED
tests/test_model.py::test_invariant_to_irrelevant_field[v2-name-Rahim] PASSED
tests/test_model.py::test_invariant_to_irrelevant_field[v2-customer_id-99999] FAILED
tests/test_model.py::test_invariant_to_irrelevant_field[v3-name-Rahim] PASSED
tests/test_model.py::test_invariant_to_irrelevant_field[v3-customer_id-99999] PASSED
tests/test_model.py::test_more_late_deliveries_never_lower_risk[v2] FAILED
tests/test_model.py::test_more_late_deliveries_never_lower_risk[v3] PASSED
tests/test_model.py::test_no_slice_worse_than_live_model[v2] FAILED
tests/test_model.py::test_no_slice_worse_than_live_model[v3] PASSED
== short test summary info ==
FAILED tests/test_data.py::test_batch_meets_contract[corrupted] - AssertionError: dtype items: str
FAILED tests/test_model.py::test_invariant_to_irrelevant_field[v2-customer_id-99999] - AssertionError: 3000 scores changed
FAILED tests/test_model.py::test_more_late_deliveries_never_lower_risk[v2] - AssertionError: 68 rows went down
FAILED tests/test_model.py::test_no_slice_worse_than_live_model[v2] - AssertionError: AUC drop vs v1: {'all': 0.0103}
== 4 failed, 10 passed in 1.26s ==
১৪টি test-এর ১০টি পাস, ৪টি ফেল, আর প্রতিটি ফেলের বার্তা সরাসরি কারণ বলে: “3000 scores changed”, “68 rows went down”, “AUC drop vs v1: {'all': 0.0103}”। ভালো test-এর বার্তা পড়েই কী করতে হবে বোঝা যায়। একটি পার্থক্য লক্ষ করুন: পাতার scikit-learn 1.8.0-এ v2-এর ১৭টি সারিতে ঝুঁকি কমেছিল, ডেস্কটপের 1.9.1-এ ৬৮টিতে; বাঁধনহীন gradient boosting version-ভেদে একটু আলাদা গাছ বানাতে পারে। সংখ্যা বদলেছে, রায় বদলায়নি, আর v3-এর বাঁধন দুই জায়গাতেই ধরে রেখেছে। তাই CI-তে library-র version পিন করুন। ভাঙা ব্যাচের contract test ফেল করেছে প্রথম ভাঙা নিয়মের নাম দিয়ে; পুরো তালিকা assertion-এর বার্তায় আছে। Feature-এর দুটি test পাস: ভবিষ্যতের অর্ডার ঢোকে না, আর একজন ক্রেতার feature পুরো ব্যাচের সঙ্গে মেলে।
৫ · Deploy-এর পরে — নজরদারির চার স্তর
Test বলে “আজ ঠিক আছে”; model monitoring বলে “এখনো ঠিক আছে কি না”। বাজারঘরের জালিয়াতি-ধরার model প্রতিটি অর্ডারে একটি score দেয়, কিন্তু কোনো অর্ডার সত্যিই জালিয়াতি কি না, তা নিশ্চিত হয় ব্যাংকের টাকা ফেরতের দাবি (chargeback) এলে, গড়ে ১৪ দিন পরে (label delay). তাই নজরদারি চার স্তরে সাজানো হয়, দ্রুততম থেকে ধীরতম:
| স্তর | কী মাপবেন | কখন পাওয়া যায় | কী ধরে না |
|---|---|---|---|
| Service | latency (p95), error-এর হার, অনুরোধের সংখ্যা | সেকেন্ডে | ভুল কিন্তু দ্রুত উত্তর |
| ডেটার মান | null-এর হার, schema, সতেজতা, অচেনা শ্রেণি | মিনিট থেকে দিনে | ঠিক দেখতে কিন্তু বদলে যাওয়া ডেটা |
| অনুমানের বণ্টন | score-এর গড়, আটকানো অর্ডারের ভাগ, feature-এর PSI (পর্ব ৫৯) | দিনে | input একই রেখে নিয়ম বদল (concept drift) |
| আসল মান | AUC, precision, calibration, slice ধরে; টাকায় ক্ষতি | label এলে (এখানে ১৪ দিন পরে) | কিছু নয়, কিন্তু দেরিতে |
Service স্তরের নকশা (SLO, burn rate, on-call) সাধারণ সফটওয়্যারের মতোই; Cloud আর DevOps কোর্সের Alerting আর SLO পর্ব-এ বিস্তারিত। ML-এর বিশেষ সমস্যা নিচের দুই স্তরে: data drift আর prediction drift সঙ্গে সঙ্গে দেখা যায় কিন্তু তা মান কমার প্রমাণ নয়, আর আসল মান প্রমাণ কিন্তু দেরিতে। দুটির মাঝের ফাঁক কমাতে প্রতিদিন কিছু অর্ডার এলোমেলোভাবে বেছে হাতে যাচাই করা যায়। এই সংখ্যাগুলো কোন dashboard-এ কীভাবে দেখাবেন, তা পর্ব ৬৬-এ দেখেছেন: প্রতিটি সংখ্যার পাশে তার স্বাভাবিক ওঠানামা, আর মালিকের নাম।
৬ · Alert-এর সীমা — ভুয়া সতর্কতার দাম
প্রচলিত প্রথম চেষ্টা: প্রতিটি metric-এর গত ১৪ দিনের গড় আর standard deviation (σ) নিয়ে, আজকের মান গড় ± 2σ-এর বাইরে গেলে alert. শুনতে যুক্তিসংগত, কিন্তু অনেক metric একসঙ্গে দেখলে হিসাব বদলে যায়; এটি পর্ব ২২-এর multiple comparisons-এর সমস্যাই, শুধু প্রতিদিন।
| এখন দেখা হচ্ছে | |
| হিসাব | |
| ফল |
- একটি metric. স্বাভাবিক বণ্টনে মান গড় থেকে 2σ-এর বেশি দূরে যাওয়ার সম্ভাবনা ৪.৫৫%, অর্থাৎ কোনো সমস্যা ছাড়াই প্রায় প্রতি ২২ দিনে একবার। একটি metric-এর জন্য এটি সহনীয়।
- ৩০টি metric. একটিও না বাজার সম্ভাবনা ০.৯৫৪৫-কে ৩০ বার গুণ: ০.২৪৭. তাই প্রায় চার দিনের তিন দিনই অন্তত একটি ভুয়া false alarm.
- এক মাস। প্রতিদিন গড়ে ১.৩৭টি, মাসে প্রায় ৪১টি সতর্কতা, যার একটিও আসল নয়। কয়েক সপ্তাহের মধ্যে দল এগুলো না দেখে মুছতে শুরু করে (alert fatigue), আর আসল সতর্কতাটিও সেভাবেই মুছে যায়।
- 3σ. প্রতি metric-এ সম্ভাবনা ০.২৭%, ৩০টিতে দিনে ৭.৮%, মাসে প্রায় ২.৪টি। ভুয়া কমল, কিন্তু আসল ছোট বদল ধরার ক্ষমতাও কমল: সীমা সবসময় দুই ভুলের বিনিময়।
- Bonferroni. সব মিলিয়ে দিনে ৫% চাইলে প্রতি metric-এ α = ০.০৫/৩০, অর্থাৎ ±৩.১৪σ. তখনো প্রায় ২০ দিনে একবার ভুয়া alert; শূন্য কখনো নয়।
- বাস্তব। Metric-গুলো একে অপরের সঙ্গে জড়িত (অর্ডার বাড়লে নতুন account-ও বাড়ে), শুক্রবারে সাপ্তাহিক ঢেউ, আর বণ্টনের লেজ মোটা। তাই সূত্রের সংখ্যা শুধু দিক দেখায়। পরের ব্লকে ৬টি metric-এর ২১টি স্বাভাবিক দিনে প্রত্যাশিত ৫.৭টি ভুয়া alert, পাওয়া গেছে ১৩টি।
এখন বাজারঘরের জালিয়াতি model-এর ৬০ দিনের একটি simulated log. দিন ০ সোমবার, প্রতি শুক্রবারে flash sale (অর্ডার বেশি, নতুন account বেশি, দাম বেশি: স্বাভাবিক seasonality). দিন ৪৮ থেকে ৫১-তে app-এর একটি নতুন version-এর bug-এ ঠিকানা-না-মেলার কলাম (addr_mismatch) প্রায় ৩০% অর্ডারে ফাঁকা আসে: এটিই আসল ঘটনা। ব্লকটি ছয়টি দৈনিক metric বানিয়ে দুটি নিয়ম তুলনা করে: (ক) গত ১৪ দিনের গড় ± 2σ (একবার 3σ দিয়েও), আর (খ) একটি স্থির reference সময় (দিন ৭–৩৪) থেকে সপ্তাহের প্রতিটি বারের আলাদা গড় নিয়ে, সেখান থেকে দূরত্ব 3σ-এর বেশি হলে alert. মূল্যায়ন দিন ৩৫ থেকে, যাতে দুই নিয়মেরই যথেষ্ট ইতিহাস থাকে।
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from bazar_mon import prod_log
log = prod_log()
g = log.groupby("day")
D = pd.DataFrame({"orders": g.size(), "pred_mean": g.score.mean(),
"new_share": g.new_acct.mean(), "cod_share": g.cod.mean(),
"log_amount": np.log(log.amount).groupby(log.day).mean(),
"addr_null": log.addr_mismatch.isna().groupby(log.day).mean()})
mu, sd = D.rolling(14).mean().shift(1), D.rolling(14).std().shift(1) # গত ১৪ দিন
ref = D.loc[7:34] # স্থির reference: চার সপ্তাহ
prof = ref.groupby(ref.index % 7).mean() # সপ্তাহের প্রতিটি বারের গড়
res = D - prof.loc[D.index % 7].set_axis(D.index)
RULES = {"naive 2sd": (D - mu).abs() > 2 * sd, "naive 3sd": (D - mu).abs() > 3 * sd,
"weekday 3sd": res.abs() > 3 * res.loc[7:34].std()}
ev, inc = D.index >= 35, D.index.isin(range(48, 52)) # আসল ঘটনা: দিন 48-51
for name, A in RULES.items():
fa = A[ev & ~inc]
print(f"{name:11s}: false alarms {int(fa.values.sum()):2d}, normal days alerted "
f"{int(fa.any(axis=1).sum())}/{len(fa)}, incident days caught {int(A[inc].any(axis=1).sum())}/4")
print("addr_null day 46-49:", D.addr_null.loc[46:49].round(3).tolist())
fig, ax = plt.subplots(2, 1, figsize=(6.4, 4.6), sharex=True)
for a, col in zip(ax, ["pred_mean", "addr_null"]):
a.plot(D.index, D[col], "k.-", lw=0.8)
a.fill_between(D.index, mu[col] - 2 * sd[col], mu[col] + 2 * sd[col], color="0.85")
for name, mk in [("naive 2sd", "rx"), ("weekday 3sd", "go")]:
hit = RULES[name][col] & ev
a.plot(D.index[hit], D[col][hit], mk, mfc="none", ms=9, label=name)
a.axvspan(47.5, 51.5, color="orange", alpha=0.25); a.set_ylabel(col)
ax[0].legend(fontsize=8); ax[1].set_xlabel("day (grey band = mean ± 2 sd of last 14 days)")
fig.subplots_adjust(hspace=0.12); plt.show()আসল আউটপুট (ব্রাউজারের Python; ছবিটি ব্লকের নিচে আঁকা হয়):
naive 2sd : false alarms 13, normal days alerted 6/21, incident days caught 3/4
naive 3sd : false alarms 2, normal days alerted 2/21, incident days caught 1/4
weekday 3sd: false alarms 3, normal days alerted 3/21, incident days caught 4/4
addr_null day 46-49: [0.007, 0.007, 0.317, 0.276]
সাধারণ 2σ ২১টি স্বাভাবিক দিনের ৬টিতে বেজেছে, মোট ১৩টি ভুয়া alert; ছবির ওপরের অংশে লাল ক্রসগুলো পড়েছে শুক্রবারের চূড়ায় (দিন ৪৬, ৫৩), যা প্রতি সপ্তাহের স্বাভাবিক ঘটনা। আর আসল ঘটনার চার দিনের একটি সে ধরেনি: ঘটনার প্রথম দিনের বিশাল মান ১৪ দিনের σ-কে ফুলিয়ে দেয় (ছবির নিচের অংশে ধূসর পট্টি হঠাৎ চওড়া), তাই পরের দিনগুলো “স্বাভাবিক” দেখায়। সাধারণ 3σ ভুয়া কমায় (২টি), কিন্তু ঘটনার মাত্র এক দিন ধরে। বার মিলিয়ে 3σ ভুয়া ৩টি, আর ঘটনার চার দিনই ধরে, কারণ তার reference ঘটনায় দূষিত হয় না আর শুক্রবারকে শুক্রবারের সঙ্গে তুলনা করে। তিনটি ভুয়াই এসেছে COD-এর ভাগে, যার কোনো সাপ্তাহিক ঢেউ নেই: চার সপ্তাহের ছোট reference থেকে σ অনুমান একটু কম আসে। ভালো নিয়মও শূন্য ভুয়া দেয় না; এটি একটি seed-এর একটি simulation, সাধারণ ফল নয়।
ব্যবহারিক নিয়ম তিনটি। প্রথম, ডেটার মানের metric-এ পরিসংখ্যান নয়, contract-এর স্থির সীমা: null-এর হার ২%-এর বেশি মানেই সমস্যা, σ যা-ই হোক। দ্বিতীয়, alert-কে দুই ভাগে: যেটিতে এখনই মানুষ লাগবে (page), আর যেটি কাজের তালিকায় যাবে (ticket). তৃতীয়, প্রতিটি alert-এর একজন মালিক আর একটি লিখিত runbook; যে alert-এ কেউ কিছু করে না, তা মুছে ফেলুন।
৭ · Label এলে মান, আর কখন আবার শেখাবেন
একই log-এ একটি দ্বিতীয়, নীরব বদলও লুকানো: দিন ৩০ থেকে জালিয়াতেরা পুরোনো account আর COD ব্যবহার শুরু করেছে। Input-এর বণ্টন একই, তাই আগের ব্লকের কোনো metric এটি দেখেনি; এটি concept drift, যা শুধু label এলে ধরা পড়ে। চতুর্থ ব্লক দুটি কাজ করে: সাপ্তাহিক AUC গোনে শুধু সেই সপ্তাহগুলোর, যাদের সব label এসে গেছে, আর একটি retraining trigger function বিভিন্ন দিনে কী সিদ্ধান্ত দিত তা ছাপায়। নিয়মের ক্রম ইচ্ছাকৃত: আগে ডেটার মান, তারপর label-এর পর্যাপ্ততা, তারপর মান কমা, শেষে সময়সূচি।
import pandas as pd
from sklearn.metrics import roc_auc_score
from bazar_mon import prod_log
log = prod_log()
BASE_AUC, TRAINED_DAY = 0.75, 0 # deploy-এর সময়ের validation AUC
def weekly_auc(today): # শুধু সেই সপ্তাহ, যার সব label এসে গেছে
seen = log[log.day // 7 * 7 + 6 + 14 <= today]
return seen.groupby((seen.day // 7).rename("week")).apply(
lambda w: pd.Series({"orders": len(w), "frauds": int(w.fraud.sum()),
"auc": roc_auc_score(w.fraud, w.score)}), include_groups=False)
def decide(today): # retraining trigger: একটি সিদ্ধান্ত, একটি কারণ
recent = log[log.day.between(today - 2, today)]
null = recent.addr_mismatch.isna().mean()
if null > 0.02:
return f"STOP addr null {null:.1%}: fix the pipeline, do not retrain"
w = weekly_auc(today)
if len(w) < 2:
return "WAIT not enough labelled weeks"
if (w.auc.tail(2) < BASE_AUC - 0.05).all():
return f"RETRAIN AUC {w.auc.iloc[-2]:.3f}, {w.auc.iloc[-1]:.3f}; drop days 48-51"
if today - TRAINED_DAY >= 90:
return "RETRAIN scheduled: model is 90 days old"
return f"OK last labelled week {w.index[-1]}: AUC {w.auc.iloc[-1]:.3f}"
print(weekly_auc(60).astype({"orders": int, "frauds": int}).round(3).to_string())
for today in [20, 30, 44, 50, 53, 54, 55]:
print(f"day {today}: {decide(today)}")আসল আউটপুট (ব্রাউজারের Python):
orders frauds auc
week
0 4504 210 0.732
1 4601 207 0.781
2 4516 209 0.743
3 4389 218 0.745
4 4528 321 0.690
5 4408 395 0.679
day 20: WAIT not enough labelled weeks
day 30: OK last labelled week 1: AUC 0.781
day 44: OK last labelled week 3: AUC 0.745
day 50: STOP addr null 30.2%: fix the pipeline, do not retrain
day 53: STOP addr null 8.6%: fix the pipeline, do not retrain
day 54: OK last labelled week 4: AUC 0.690
day 55: RETRAIN AUC 0.690, 0.679; drop days 48-51
প্রথম চার সপ্তাহে AUC ০.৭৩–০.৭৮-এর মধ্যে ওঠানামা করে: এক সপ্তাহের ২০০-এর মতো জালিয়াতিতে AUC-এর এতটা নড়াচড়া স্বাভাবিক, তাই এক সপ্তাহের পতনে retrain নয়, পরপর দুই সপ্তাহ সীমার (০.৭০) নিচে চাওয়া হয়েছে। সপ্তাহ ৪ (দিন ২৮–৩৪) থেকে AUC ০.৬৯০, তারপর ০.৬৭৯. কিন্তু সিদ্ধান্তের দিনগুলো দেখুন: বদল শুরু দিন ৩০-এ, সপ্তাহ ৪-এর সব label আসে দিন ৪৮-এ, সপ্তাহ ৫-এর দিন ৫৫-তে; তাই RETRAIN-এর রায় আসে ২৫ দিন পরে। এর মাঝে দিন ৫০ আর ৫৩-তে function থামিয়েছে: ঠিকানার কলাম ফাঁকা, এখন শেখালে model শিখবে “ঠিকানার তথ্য নেই” অবস্থা থেকে। দিন ৫৩-তেও সতর্কতা, কারণ তিন দিনের জানালায় ঘটনার শেষ দিনটি আছে। আর RETRAIN-এর বার্তাতেই লেখা: দিন ৪৮–৫১-এর সারি training থেকে বাদ।
নতুন model শেখানো মানেই চালু করা নয়। সেটিকেও অংশ ৪-এর একই test পাস করতে হবে, তারপর চালু model-এর পাশে নীরবে চালিয়ে তুলনা (shadow deployment), তারপর অল্প ট্রাফিকে (canary release); এগুলোর বিস্তার পর্ব ৬৮ (Model serving)-এ। আর আগের model-টি তার metadata-সহ একটি model registry-তে রাখুন, যাতে এক লাইনের বদলে ফিরে যাওয়া যায়। Drift-এর সময়ে কতটা পুরোনো ডেটা রাখবেন (sliding window বনাম recency weighting), তা পর্ব ৫৯-এর শেষ অংশে মেপে দেখেছেন।
৮ · Incident — alert থেকে rollback পর্যন্ত
দিন ৪৮-এর ঘটনাটি শুরু থেকে শেষ পর্যন্ত দেখা যাক। সংখ্যাগুলো উপরের simulated log থেকে; সময় আর দলের কাজ কাল্পনিক। সাধারণ incident response-এর কাঠামো (severity, ভূমিকা, যোগাযোগ) Cloud আর DevOps কোর্সের Incident management পর্ব-এ; এখানে ML-এর বিশেষ প্রশ্নগুলো।
| ধাপ | |
| কী দেখা গেল | |
| কী করা হলো |
- ধরা পড়া। সকালের দৈনিক হিসাবে null-এর হার ৩১.৭%, যেখানে স্বাভাবিক ০.৭%. Contract-এর স্থির সীমা (২%) ভেঙেছে, তাই এটি page, ticket নয়। বার মিলিয়ে নিয়মও একই দিনে বেজেছিল।
- কারণ খোঁজা। প্রথম প্রশ্ন: ডেটা না model? model ফাইল আর তার version বদলায়নি, কিন্তু ফাঁকা মান আসছে শুধু নতুন app থেকে। Model rollback করলে কিছুই বদলাত না, কারণ পুরোনো model-ও একই ফাঁকা কলাম পেত।
- ক্ষতি থামানো আগে। কারণ পুরোপুরি বোঝার আগেই ক্ষতি থামানো: ঝুঁকিপূর্ণ ধরনের (ফাঁকা কলাম আর COD) অর্ডার হাতে যাচাইয়ের সারিতে, আর app দলকে আগের version-এ ফেরার অনুরোধ (rollback). Model নিজে ভুল হলে এখানে model registry থেকে আগের model-এ ফেরা হতো।
- যাচাই। দিন ৫২-তে null ০.৩৫%, স্বাভাবিকে ফিরেছে। Alert বন্ধ করার আগে সংখ্যা দেখে নিশ্চিত হওয়া, আর ঘটনার দিনগুলো training ডেটায় চিহ্নিত করা, যাতে চতুর্থ ব্লকের মতো retrain-এ বাদ পড়ে।
- ক্ষতির হিসাব। চার দিনে ৭১৯টি অর্ডারে কলাম ফাঁকা ছিল। তাদের ক’টি জালিয়াতি, তা জানতে আবার label-এর ১৪ দিন অপেক্ষা: ৫২টি। এটি ML incident-এর বিশেষত্ব: ক্ষতির পুরো মাপও দেরিতে আসে।
- Postmortem. দোষারোপহীন postmortem-এ মূল কারণ মানুষ নয়, ব্যবস্থা: app দল জানত না কলামটি model ব্যবহার করে। প্রতিকার: app-এর CI-তে একই data contract test, null-এর alert স্থায়ী, আর runbook-এ এই ধরনের ঘটনার ধাপ।
Rollback-এর কয়েকটি রূপ আগে থেকে প্রস্তুত রাখুন: আগের model-এ ফেরা (registry-র একটি pointer বদল), feature pipeline-এর আগের version, model ছাড়া একটি সরল নিয়মের fallback, আর একেবারে বন্ধ করার সুইচ। কোনোটিই বিপদের দিনে প্রথমবার চালানো উচিত নয়; শান্ত দিনে অনুশীলন করুন।
নজরদারির চেকলিস্ট
- Deploy-এর আগে: data contract, feature test, model test (slice, invariance, directional, আগের version) CI-তে সবুজ; reference সময় আর সপ্তাহের বার-ধরে প্রোফাইল সংরক্ষিত।
- প্রতি ব্যাচে: contract; ব্যর্থ হলে quarantine আর মালিককে খবর।
- প্রতিদিন: service metric, null আর সতেজতা, score-এর গড় আর আটকানোর হার, বার মিলিয়ে সীমা।
- Label এলে: সাপ্তাহিক AUC আর calibration, slice ধরে, একটি baseline-এর পাশে; টাকায় ক্ষতি।
- প্রতিটি alert-এর: মালিক, page না ticket, runbook, আর মাসে একবার “কোন alert-এ কেউ কিছু করেনি” পর্যালোচনা।
- Retrain-এর আগে: ডেটার সতর্কতা নেই, ঘটনার দিন বাদ, একই test, shadow, আর rollback-এর পথ পরীক্ষিত।
আরও গভীরে: নিয়ন্ত্রণের সীমার ধারণা Walter Shewhart-এর ১৯২০-এর দশকের control chart থেকে; ছোট কিন্তু টেকসই বদল দ্রুত ধরতে E. S. Page-এর ১৯৫৪ সালের CUSUM আর S. W. Roberts-এর ১৯৫৯ সালের EWMA chart, যারা একদিনের বিচ্যুতি নয়, জমা বিচ্যুতি দেখে। Google-এর TFX-এ ডেটা যাচাইয়ের নকশা Breck ও সহকর্মীদের ২০১৯ সালের SysML প্রবন্ধে আছে, যেখানে schema ডেটা থেকে অনুমান করে মানুষ পর্যালোচনা করে। আর Shreya Shankar ও সহকর্মীদের ২০২২ সালের সাক্ষাৎকার-ভিত্তিক গবেষণা (“Operationalizing Machine Learning”) বাস্তবের দলগুলোর সাধারণ ভোগান্তির তালিকায় alert fatigue আর দেরিতে আসা label-কেও তুলে ধরে।
পরীক্ষা ও ইন্টারভিউতে: (১) ML system-এ সাধারণ সফটওয়্যারের বাইরে কী test লাগে? ডেটা (contract), feature (leakage, skew), model-এর আচরণ (slice, invariance, directional, আগের version) আর চালু অবস্থার নজরদারি। (২) ২০টি স্বাধীন metric-এ 2σ হলে দিনে অন্তত একটি ভুয়া alert-এর সম্ভাবনা? ১ − ০.৯৫৪৫২০ ≈ ০.৬১. (৩) Invariance আর directional test-এর পার্থক্য? প্রথমটিতে বদলে ফল একই থাকার কথা, দ্বিতীয়টিতে নির্দিষ্ট দিকে যাওয়ার কথা। (৪) Label ৩০ দিন দেরিতে আসে; মান কমার প্রথম সংকেত কোথায় খুঁজবেন? ডেটার মান, অনুমানের বণ্টন আর হাতে যাচাই করা ছোট নমুনায়। (৫) ডেটার alert আর AUC পতন একসঙ্গে: আগে কী? Pipeline ঠিক করা; ভাঙা ডেটায় retrain করলে ভুলটাই শেখে।
৯ · AI-এর যুগে — AI-এর লেখা test আর monitoring যাচাই
“আমার model-এর test লিখে দাও” বললে AI assistant প্রায়ই শুধু assert accuracy > 0.8 জাতীয় একটি test দেয়, যা training ডেটাতেই মাপা, আর monitoring বলতে প্রতিটি কলামে 2σ। এই পর্বের ব্লকগুলো দেখিয়েছে, তাতে v2-এর মতো model পার পেয়ে যায় আর alert প্রায় রোজ বাজে। প্রায়ই চোখে পড়া ভুল:
| AI প্রায়ই যা করে | কেন সমস্যা | কীভাবে ধরবেন |
|---|---|---|
| শুধু সামগ্রিক accuracy-র test | slice-এর পতন আর আচরণের ভুল লুকায় | নির্দিষ্ট slice, invariance, directional test যোগ করুন |
select_dtypes("number") দিয়ে feature বাছাই | ID, তারিখের সংখ্যা বা label-এর সঙ্গী কলাম ঢুকে যায় | হাতে লেখা কলামের তালিকা; ID বদলের invariance test |
খারাপ সারি চুপচাপ dropna() | অর্ধেক ফাইলের মতো বড় ভুল লুকিয়ে যায় | সব নিয়মের ব্যর্থতা জমিয়ে ছাপানো, ব্যাচ quarantine |
| প্রতিটি metric-এ 2σ, ঋতু উপেক্ষা | প্রায় রোজ ভুয়া alert, ঘটনায় σ ফুলে যায় | স্থির reference, বার মিলিয়ে সীমা, ডেটার মানে স্থির সীমা |
| Drift দেখলেই স্বয়ংক্রিয় retrain | ভাঙা ডেটায় শেখা, যাচাই ছাড়া চালু | আগে ডেটার মান, ঘটনার দিন বাদ, একই test, shadow |
| pandera বা Great Expectations-এর পুরোনো API | import আর class-এর নাম version-ভেদে বদলেছে | নিজের version-এর docs; একটি ভাঙা ব্যাচে চালিয়ে দেখা |
AI-কে কোডের সঙ্গে যাচাইও চাইতে বলুন:
AI-এর দেওয়া test বা monitoring কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১ML Test Score কেন চার ভাগের গড় না নিয়ে সবচেয়ে কম নম্বরটি নেয়, আর “hidden technical debt” ধারণার সঙ্গে এর সম্পর্ক কী?
D. Sculley ও সহকর্মীরা ২০১৫ সালে “Hidden Technical Debt in Machine Learning Systems” প্রবন্ধে দেখান, ML system দ্রুত বানানো যায় কিন্তু রক্ষণাবেক্ষণ ব্যয়বহুল, আর সেই ব্যয় লুকিয়ে থাকে model-এর চারপাশে: নানা উৎসের ডেটার ওপর নির্ভরতা, একটি model-এর output আরেকটির input হওয়া, কেউ না জানা feedback loop, আর “glue code”. তাঁদের একটি পর্যবেক্ষণ: model-এর input বদলালে সব বদলায় (তাঁরা একে “Changing Anything Changes Everything” বলেন)।
এর উত্তরে Eric Breck, Shanqing Cai, Eric Nielsen, Michael Salib আর D. Sculley ২০১৭ সালে IEEE Big Data সম্মেলনে ML Test Score প্রস্তাব করেন: ডেটা, model, অবকাঠামো আর নজরদারি, প্রতি ভাগে সাতটি করে ২৮টি পরীক্ষা। হাতে করে নথিভুক্ত করলে আধা নম্বর, স্বয়ংক্রিয়ভাবে নিয়মিত চললে পুরো নম্বর। চূড়ান্ত নম্বর চার ভাগের সর্বনিম্ন। যুক্তিটি সরল: একটি দল যদি model test-এ নিখুঁত কিন্তু নজরদারিতে শূন্য হয়, তার গড় ভালো দেখায়, অথচ চালু হওয়ার পর প্রথম drift-এই system অন্ধ। শিকল তার দুর্বলতম আংটার মতোই শক্ত।
প্রবন্ধে তাঁরা Google-এর ভেতরের কয়েক ডজন দলের স্ব-মূল্যায়নও উল্লেখ করেন, যেখানে অনেক দলের নম্বর কম এসেছিল; সংখ্যাগুলো একটি প্রতিষ্ঠানের, সাধারণ সত্য নয়। তবু দিকটি শিক্ষণীয়: model-এর মান নিয়ে দলগুলো বেশি ভাবে, ডেটা আর নজরদারির test সবচেয়ে বেশি অবহেলিত হয়। এই পর্বেও v2-এর সমস্যা কোনো AUC বলেনি, বলেছে আচরণের test; আর দিন ৩০-এর বদল কোনো test বলেনি, বলেছে নজরদারি।
তালিকাটি নিজের প্রকল্পে খাটাতে সব ২৮টি দরকার নেই। চার ভাগের প্রতিটিতে অন্তত একটি স্বয়ংক্রিয় পরীক্ষা থাকলেই সর্বনিম্ন নম্বর শূন্য থেকে ওঠে, আর সেটিই সবচেয়ে বড় লাভ।
মূল উপলব্ধি: ML system-এর ঝুঁকি model-এর চারপাশে ছড়ানো, তাই নির্ভরযোগ্যতা মাপা হয় সবচেয়ে অবহেলিত স্তর দিয়ে, গড় দিয়ে নয়।
প্র ০২কারখানার control chart থেকে আজকের alert: ১৯২০-এর দশকের একটি ধারণা কেন এখনো ML নজরদারির ভিত্তি, আর কোথায় তা খাটে না?
Bell Telephone Laboratories-এ Walter Shewhart ১৯২০-এর দশকে কারখানার উৎপাদনের ওঠানামাকে দুই ভাগে দেখেন: সাধারণ কারণের স্বাভাবিক ওঠানামা, আর বিশেষ কারণের অস্বাভাবিক বিচ্যুতি। তাঁর control chart-এ গড়ের দুই পাশে সাধারণত তিন standard deviation দূরে সীমা টানা হয়; সীমার বাইরে গেলে খোঁজ। 3σ বেছে নেওয়ার যুক্তি ছিল অর্থনৈতিক: বেশি সংবেদনশীল সীমায় অকারণ খোঁজের খরচ, কম সংবেদনশীল সীমায় দেরিতে ধরার খরচ। পরে Western Electric-এর হ্যান্ডবুক (১৯৫৬) একাধিক নিয়ম যোগ করে, যেমন পরপর কয়েকটি বিন্দু গড়ের একই দিকে, যাতে ছোট কিন্তু টেকসই সরণ ধরা পড়ে। E. S. Page ১৯৫৪ সালে CUSUM আর S. W. Roberts ১৯৫৯ সালে EWMA প্রস্তাব করেন, যারা জমা বিচ্যুতি দেখে।
মান-নিয়ন্ত্রণের সাহিত্যে এই বিনিময় মাপা হয় average run length দিয়ে: কোনো সমস্যা না থাকলে গড়ে কত দিন পরে একটি ভুয়া সংকেত, আর বদল হলে গড়ে কত দিনে ধরা। সীমা বাড়ালে প্রথমটি বাড়ে (ভালো), দ্বিতীয়টিও বাড়ে (খারাপ)। এই পর্বের stepper-এর হিসাব এর সরল রূপ।
ML নজরদারিতে কয়েকটি অনুমান ভাঙে। Shewhart-এর chart একটি স্থির প্রক্রিয়া ধরে নেয়; অনলাইন দোকানের metric-এ সাপ্তাহিক আর ঋতুর ঢেউ, উৎসবের মৌসুম আর বিপণনের প্রচার থাকে, তাই reference-কেও সেই ঢেউ বুঝতে হয়। কারখানায় একটি মেশিনের কয়েকটি মাপ, আর আজকের system-এ শত শত metric, ফলে multiple comparisons-এর সমস্যা প্রবল। আর ঘটনার দিনের মান যদি চলমান reference-এ ঢোকে, পরের দিনগুলো স্বাভাবিক দেখায়, যা ষষ্ঠ অংশের ব্লকে দেখা গেছে। Google-এর SRE বইয়ের (২০১৬) পরামর্শও একই দিকে: কারণ নয়, ব্যবহারকারীর লক্ষণ দেখে alert, আর যে alert-এ মানুষের কিছু করার নেই, তা page নয়।
মূল উপলব্ধি: সীমা মানে দুই ধরনের ভুলের দামের মধ্যে বাছাই; Shewhart-এর এই অর্থনৈতিক যুক্তি আজও খাটে, কিন্তু ঋতু, অনেক metric আর দূষিত reference সামলাতে নিয়মকে আরও বুদ্ধিমান হতে হয়।
প্র ০৩CheckList NLP-এর জন্য বানানো হয়েছিল। সারণির ডেটায় invariance আর directional test খাটানোর সময় কোন বিপদ আছে?
Marco Tulio Ribeiro, Tongshuang Wu, Carlos Guestrin আর Sameer Singh ২০২০ সালে ACL-এ “Beyond Accuracy: Behavioral Testing of NLP Models with CheckList” প্রবন্ধে যুক্তি দেন, held-out accuracy একটি গড়, যা model কী পারে আর কী পারে না তা লুকায়। তাঁরা সফটওয়্যার testing-এর ধারণা থেকে তিন ধরনের test প্রস্তাব করেন: ন্যূনতম কার্যকারিতা (সহজ, নির্দিষ্ট উদাহরণে ঠিক উত্তর), invariance (নাম বা স্থান বদলালে sentiment একই) আর directional expectation (বাক্যে নেতিবাচক অংশ যোগ করলে sentiment উঠবে না)। তাঁদের পরীক্ষায় বাণিজ্যিক আর গবেষণার বেশ কিছু model এমন সহজ test-এ ফেল করেছিল; প্রবন্ধটি সেই বছর সম্মেলনের সেরা প্রবন্ধের পুরস্কার পায় বলে জানা যায়।
সারণির ডেটায় ধারণাটি সহজে আসে, কিন্তু তিনটি বিপদ আছে। প্রথম, প্রত্যাশা কে ঠিক করে? “দেরি বাড়লে churn ঝুঁকি বাড়বে” ব্যবসার জ্ঞান, কিন্তু সব নিয়ম এত পরিষ্কার নয়। দাম বাড়লে জালিয়াতির ঝুঁকি বাড়বে? হয়তো কোনো সীমা পর্যন্ত। ভুল প্রত্যাশা চাপালে model-কে সত্য শিখতে বাধা দেওয়া হয়। দ্বিতীয়, অবাস্তব সারি: একটি feature এক বাড়ালে এমন সারি তৈরি হতে পারে যা বাস্তবে অসম্ভব (দেরিতে ডেলিভারি মোট অর্ডারের চেয়ে বেশি)। এমন সারিতে model-এর আচরণ নিয়ে test ফেল করা অর্থহীন হতে পারে; তাই পরিবর্তনগুলো সম্ভাব্য সীমার মধ্যে রাখুন। তৃতীয়, invariance আর ন্যায্যতা: নাম বদলে score একই রাখা সহজ, কারণ নাম model-এ নেই; কিন্তু জেলা বা লিঙ্গের সঙ্গে জড়িত অন্য feature থাকলে প্রভাব ঘুরপথে ঢোকে। সেটি পর্ব ৬০-এর proxy সমস্যা, invariance test একা তা ধরে না।
দিকের প্রত্যাশা model-এ সরাসরি বেঁধে দেওয়া যায় (monotonic constraint), যেমন v3-এ। তখন test-টি নিরাপত্তার জাল হয়ে থাকে, যাতে কেউ পরে constraint সরালে ধরা পড়ে।
মূল উপলব্ধি: আচরণের test ব্যবসার জ্ঞানকে যাচাইযোগ্য করে, কিন্তু প্রত্যাশাগুলো নিজেও অনুমান; সেগুলো লিখে রাখুন, সম্ভাব্য সারিতে চালান, আর ন্যায্যতার প্রশ্ন আলাদাভাবে মাপুন।
প্র ০৪Google Flu Trends-এর গল্প দেরিতে আসা “সত্য” আর নীরবে বদলানো model সম্পর্কে কী শেখায়?
২০০৮ সালে Google Flu Trends প্রকাশ পায়: মানুষ কী খোঁজে, তা থেকে ইনফ্লুয়েঞ্জার মতো অসুস্থতার হার অনুমান। ধারণাটি আকর্ষণীয় ছিল, কারণ যুক্তরাষ্ট্রের সরকারি নজরদারির সংখ্যা আসত এক-দুই সপ্তাহ দেরিতে, আর অনুসন্ধানের ডেটা প্রায় সঙ্গে সঙ্গে। ২০০৯ সালে Nature-এ পদ্ধতিটি প্রকাশিত হয়। কিন্তু ২০১২–১৩ মৌসুমে অনুমান সরকারি সংখ্যার প্রায় দ্বিগুণে পৌঁছায় বলে প্রতিবেদন আসে। David Lazer ও সহকর্মীরা ২০১৪ সালে Science-এ “The Parable of Google Flu” প্রবন্ধে কয়েকটি সম্ভাব্য কারণ আলোচনা করেন: অনুসন্ধান সেবার নিজের বদল (যেমন পরামর্শ দেওয়া শব্দ), মৌসুমি শব্দের সঙ্গে কাকতালীয় সম্পর্ক, আর পুরোনো model-কে নিয়মিত নতুন সত্যের সঙ্গে না মেলানো। প্রকল্পটি পরে প্রকাশ্যে বন্ধ হয়।
ML নজরদারির ভাষায় এখানে তিনটি শিক্ষা। প্রথম, input-এর উৎস নিজেই বদলায়। অনুসন্ধান-ইঞ্জিনের নকশা বদলালে একই অসুস্থতায় আলাদা অনুসন্ধান হয়; বাজারঘরে app-এর নতুন version ঠিক এই ভূমিকায়। দ্বিতীয়, সত্য দেরিতে আসলেও আসে, তাই তা নিয়মিত মেলাতে হয়। সরকারি সংখ্যা পাওয়া যেত; সমস্যা ছিল সেগুলোর সঙ্গে অনুমান নিয়মিত তুলনা করে model ঠিক না করা। চতুর্থ ব্লকের সাপ্তাহিক AUC-এর কাজ এটাই। তৃতীয়, সরল baseline-এর পাশে মাপুন। Lazer ও সহকর্মীরা দেখান, শুধু সাম্প্রতিক সরকারি সংখ্যা থেকে সরল অনুমানও অনেক সময় ভালো করত। নতুন model যদি সরল নিয়মকে না হারায়, তার জটিলতার দাম নেই।
গল্পটি নিয়ে মতভেদও আছে: পরে কিছু গবেষক দেখান, অনুসন্ধানের ডেটা সরকারি সংখ্যার সঙ্গে মিলিয়ে ব্যবহার করলে কাজে লাগে। অর্থাৎ ব্যর্থতা ডেটার নয়, নজরদারি আর নিয়মিত সংশোধনের অভাবের।
মূল উপলব্ধি: দেরিতে আসা সত্যকে নিয়মিত অনুমানের সঙ্গে মেলানো, input-এর উৎসের বদলে চোখ রাখা আর সরল baseline-এর পাশে মাপা, এই তিনটি না থাকলে ভালো শুরু করা model-ও নীরবে ভুল পথে যায়।
প্র ০৫২০১২ সালের Knight Capital ঘটনা deploy আর rollback সম্পর্কে কী শেখায়, আর ML model-এর rollback কেন আরও জটিল?
২০১২ সালের আগস্টে মার্কিন শেয়ার-ব্যবসার প্রতিষ্ঠান Knight Capital একটি নতুন trading software চালু করে। মার্কিন নিয়ন্ত্রক SEC-এর ২০১৩ সালের আদেশে বর্ণিত বিবরণ অনুযায়ী, নতুন কোড আটটি server-এর একটিতে পৌঁছায়নি, আর সেখানে বহু বছর অব্যবহৃত একটি পুরোনো ফাংশন একটি পুনর্ব্যবহৃত flag-এর কারণে জেগে ওঠে। প্রায় ৪৫ মিনিটে কয়েক মিলিয়ন অনাকাঙ্ক্ষিত লেনদেন হয়ে যায়, আর প্রতিষ্ঠানটির ক্ষতি প্রায় ৪৬ কোটি মার্কিন ডলার বলে জানানো হয়। প্রতিবেদন অনুযায়ী সমস্যা চলাকালে নতুন কোড সরিয়ে নেওয়ার চেষ্টা পরিস্থিতি আরও খারাপ করে, কারণ flag তখনো একই অর্থে চালু ছিল। এটি ML-এর ঘটনা নয়, কিন্তু ML দলের জন্য তিনটি শিক্ষা।
প্রথম, deploy প্রতিটি যন্ত্রে একই কি না, তা যাচাই করুন। ML-এ একই কথা model ফাইল, feature-এর কোড আর library-র version-এর জন্য; প্রতিটি অনুমানের সঙ্গে model-এর version লিখে রাখলে মিশ্র অবস্থা ধরা যায়। দ্বিতীয়, rollback নিজেও একটি পরিবর্তন, যা না পরীক্ষা করলে বিপদে ব্যর্থ হতে পারে। তৃতীয়, বন্ধ করার সুইচ আর পরিষ্কার নির্দেশ: কে, কখন, কোন সংকেতে system থামাবেন।
ML-এর rollback আরও জটিল, কারণ model একা নয়। আগের model চালাতে আগের feature-এর সংজ্ঞা লাগে; feature pipeline এর মধ্যে বদলে থাকলে পুরোনো model নতুন feature পেয়ে training-serving skew-এ পড়ে। নতুন model চালু থাকার সময়ে তার সিদ্ধান্ত ডেটাও বদলায় (যাদের আটকানো হয়েছে, তাদের label আসে না), তাই ফিরে যাওয়ার পর মাপের তুলনা সহজ নয়। আর বাজারঘরের দিন ৪৮-এর মতো অনেক ঘটনায় model-এর rollback কোনো কাজেই আসে না, কারণ সমস্যা ডেটার উৎসে। এজন্য model, feature-এর কোড আর তাদের মিলিত version একসঙ্গে registry-তে রাখা হয়, আর rollback শান্ত দিনে অনুশীলন করা হয়।
মূল উপলব্ধি: দ্রুত, পরীক্ষিত rollback যেকোনো deploy-এর অংশ; ML-এ ফিরে যেতে হয় model, feature আর ডেটার সংজ্ঞার একটি মিলিত অবস্থায়, শুধু একটি ফাইলে নয়।
অনুশীলন
হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার ব্লকগুলোতে সংখ্যা বদলে যাচাই করুন।
-
পূর্ণ numerical: (ক) ২০টি স্বাধীন metric-এ 2σ সীমা। কোনো সমস্যা না থাকলে দিনে অন্তত একটি ভুয়া alert-এর সম্ভাবনা কত, আর ৩০ দিনে প্রত্যাশিত ভুয়া alert কতটি? Bonferroni দিয়ে দিনে মোট ৫% চাইলে প্রতি metric-এর সীমা কত σ? (খ) একটি জালিয়াতি model দিনে ৬০০টি অর্ডারের ৪.৬%-কে ঝুঁকিপূর্ণ বলে। Binomial ধরে এই হারের standard deviation কত, আর 2σ পট্টি কোথা থেকে কোথা? (গ) Label আসে ১৪ দিন পরে, সপ্তাহ শুরু দিন ০-তে, আর retrain-এর নিয়মে পরপর দুটি পুরো সপ্তাহ সীমার নিচে লাগে। দিন ৩৫-এ (সপ্তাহ ৫-এর শুরু) concept drift শুরু হলে সবচেয়ে আগে কোন দিনে retrain-এর রায় সম্ভব?
(ক) ১ − ০.৯৫৪৫২০ = ১ − ০.৩৯৪ = ০.৬০৬. প্রত্যাশিত: ২০ × ০.০৪৫৫ × ৩০ = প্রায় ২৭টি. Bonferroni: α = ০.০৫/২০ = ০.০০২৫, দুই দিক মিলিয়ে, অর্থাৎ প্রতি দিকে ০.০০১২৫; সীমা ±৩.০২σ. (খ) √(০.০৪৬ × ০.৯৫৪ / ৬০০) = √০.০০০০৭৩১ = ০.০০৮৫৫, অর্থাৎ ০.৮৬ শতাংশ-বিন্দু। 2σ পট্টি ৪.৬% ± ১.৭%, মানে ২.৯% থেকে ৬.৩%. এর মধ্যে ৫.৫% দেখে ভয় পাওয়ার কারণ নেই, যদিও শুনতে “২০% বেড়েছে”। (গ) সপ্তাহ ৫ = দিন ৩৫–৪১, এর শেষ label আসে ৪১ + ১৪ = দিন ৫৫-এ; সপ্তাহ ৬ = দিন ৪২–৪৮, শেষ label দিন ৬২-তে। দুটি পুরো সপ্তাহ লাগে বলে দিন ৬২, বদলের ২৭ দিন পরে। দেরি কমানোর উপায়: এক সপ্তাহের বড় পতনে আলাদা দ্রুত নিয়ম, হাতে যাচাই করা নমুনা, বা দ্রুত আসা আংশিক label. সংখ্যাগুলো ডেস্কটপে Python দিয়ে যাচাই করা।
-
সমালোচনা ও নকশা: একটি দল লিখেছে: “আমাদের model-এর test: training ডেটায় accuracy ≥ ০.৯০. Monitoring: ৫০টি feature-এর প্রতিটিতে গত ৭ দিনের গড় ± 2σ, প্রতিটি alert সরাসরি on-call-এর ফোনে। কোনো alert তিন দিন চললে স্বয়ংক্রিয়ভাবে শেষ ৭ দিনের ডেটায় retrain হয়ে নতুন model চালু হয়। Label-এর জন্য অপেক্ষা করি না, কারণ drift না থাকলে model ঠিক আছে।” কী কী সমস্যা, আর আপনার নকশা কী?
সমস্যা: (১) Training ডেটার accuracy কিছুই প্রমাণ করে না; আলাদা সময়ের test ডেটা, slice আর আচরণের test নেই। Class-গুলো অসম হলে accuracy নিজেই বিভ্রান্তিকর। (২) ৫০টি metric-এ 2σ: দিনে অন্তত একটি ভুয়া alert-এর সম্ভাবনা ১ − ০.৯৫৪৫৫০ ≈ ০.৯০, প্রতিটি ফোনে গেলে অল্প দিনেই alert fatigue. ৭ দিনের σ খুব অস্থির, আর সাপ্তাহিক ঢেউ উপেক্ষিত। (৩) ডেটার মান আর drift আলাদা করা হয়নি: pipeline-এর bug-এ তিন দিন alert চললে ভাঙা ডেটাতেই retrain হবে। (৪) Retrain-এর পর কোনো test, shadow বা canary নেই; আগের model-এ ফেরার পথও নেই। (৫) “Drift না থাকলে model ঠিক” ভুল: concept drift input-এ দেখা যায় না (দিন ৩০-এর জালিয়াত-কৌশলের মতো)। নকশা: সময় ধরে আলাদা test set; CI-তে data contract, feature, slice, invariance, directional আর আগের version-এর test; ডেটার মানে contract-এর স্থির সীমা, অন্য metric-এ স্থির reference আর বার মিলিয়ে সীমা, কম কিন্তু গুরুত্বপূর্ণ metric page-এ আর বাকিটা ticket-এ; label এলে সাপ্তাহিক মান আর calibration, slice ধরে; retrain trigger-এ আগে ডেটার মান, ঘটনার দিন বাদ, তারপর একই test, shadow, canary আর registry থেকে rollback-এর প্রস্তুতি।
-
আউটপুট অনুমান করুন: চালানোর আগে তিনটি লাইন লিখুন।
import pandas as pd df = pd.DataFrame({"id": [1, 2, 2, 3], "amt": [100.0, None, 250.0, -5.0], "pay": ["COD", "bKash", "cod", "card"]}) print(df.id.is_unique, df.id.duplicated().sum()) print(df.amt.isna().sum(), (~df.amt.between(20, 1000)).sum()) print(sorted(set(df.pay) - {"COD", "bKash", "Nagad", "card"}))ডেস্কটপে সত্যিকারের run (Python 3.14.4, pandas 3.1.1):
False 1 1 2 ['cod']প্রথম লাইন: ID ২ দুবার, তাই অনন্য নয়, আর
duplicated()প্রথমটিকে বাদ দিয়ে ১টি গোনে। দ্বিতীয় লাইনের ফাঁদ: null একটি, কিন্তু সীমার বাইরে ২টি, কারণbetweenফাঁকা মানেFalseদেয়, আর~তাকে “বাইরে” বানায়। তাই −৫ আর ফাঁকা মান দুটিই গোনা হয়; contract-এ null আর সীমার নিয়ম আলাদা রাখলে একই ভুল দুবার রিপোর্ট হয়, যা জেনে রাখা ভালো। তৃতীয়: “cod” ছোট হাতের, তাই অনুমোদিত তালিকায় নেই; বানান-বৈচিত্র্য contract-এ ধরা পড়ে, পরিষ্কার করা আলাদা সিদ্ধান্ত। -
কোডিং: অংশ ৪-এর
tests/test_model.py-এর মতো একটি নতুন directional test লিখুন: অভিযোগ (complaints) একটি বাড়ালে churn ঝুঁকি কোনো সারিতে কমবে না। তিনটি model (v1, v2, v3)-এর ওপর চালান। কোনটি ফেল করবে বলে আশা করেন, আর কেন?import pytest from bazar_mon import customers from models import train_all TE, M = customers(3000, seed=2), train_all() @pytest.mark.parametrize("v", ["v1", "v2", "v3"]) def test_more_complaints_never_lower_risk(v): m, cols = M[v] p = m.predict_proba(TE[cols])[:, 1] up = m.predict_proba(TE.assign(complaints=TE.complaints + 1)[cols])[:, 1] down = (up < p - 1e-9).sum() assert down == 0, f"{down} of {len(TE)} rows went down"ডেস্কটপে সত্যিকারের run (Python 3.14.4, pytest 9.2.0, scikit-learn 1.9.1):
$ uv run pytest -q --tb=no -rf tests/test_complaints.py .F. == short test summary info == FAILED tests/test_complaints.py::test_more_complaints_never_lower_risk[v2] - AssertionError: 50 of 3000 rows went down 1 failed, 2 passed in 1.15sপড়া: v1 (logistic) পাস, কারণ রৈখিক model-এ অভিযোগের সহগ ধনাত্মক, তাই প্রতিটি সারিতে ঝুঁকি বাড়ে। v3 পাস, কারণ
monotonic_cst-এ অভিযোগের দিক +১ বাঁধা। v2 ফেল: ৩,০০০ জনের ৫০ জনের ঝুঁকি কমে, কারণ বাঁধনহীন gradient boosting-এর গাছগুলো অল্প ডেটার অঞ্চলে (বেশি অভিযোগ খুব কম ক্রেতার) noise শেখে। আসল ডেটায় অভিযোগ আর churn-এর নিয়ম এত পরিষ্কার না-ও হতে পারে; তখন constraint দেওয়ার আগে ব্যবসার দলের সঙ্গে প্রত্যাশাটি লিখিত করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি চালু ML system-এ ডেটার মানের alert বাজলে কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Alert পেয়ে severity ঠিক করা আর মালিককে জানানো
- সমস্যা ডেটায়, feature-এ না model-এ, তা আলাদা করা
- ক্ষতি থামানো: fallback, উৎসের rollback বা আগের model
- Metric স্বাভাবিকে ফিরেছে কি না যাচাই আর ঘটনার দিনগুলো চিহ্নিত করা
- দোষারোপহীন postmortem আর নতুন test বা alert যোগ
৩০টি স্বাধীন metric-এর প্রতিটিতে গড় ± 2σ সীমার alert. কোনো আসল সমস্যা না থাকলে একটি দিনে অন্তত একটি alert বাজার সম্ভাবনা প্রায় কত?
- ০.০৪৫
- ০.৭৫
- ১.৩৭
- ০.০৪৯
একটি churn model-এর test: প্রতিটি ক্রেতার customer ID বদলে নতুন score নেওয়া হলো, আর দেখা গেল ৩,০০০ জনের সবার score বদলেছে। কোন ধরনের test ফেল করেছে, আর সবচেয়ে সম্ভাব্য কারণ কী?
- Directional test; ID বাড়লে ঝুঁকি বাড়া উচিত
- Slice test; একটি দলের AUC কম
- Invariance test; ID-ও feature হিসেবে model-এ ঢুকেছে
- Calibration test; সম্ভাবনাগুলো বাঁকা
একটি অর্ডার-ব্যাচের contract-এ দামের সীমা ২০ থেকে ১,০০,০০০ টাকা। ৩০টি দাম ভুল করে পয়সায় (১০০ গুণ) লেখা, কিন্তু সীমার নিয়ম তার মাত্র ১৬টি ধরেছে। বাকিগুলো ধরার সবচেয়ে কার্যকর অতিরিক্ত নিয়ম কোনটি?
- ব্যাচের গড় দামের একটি স্বাভাবিক পরিসর
- দামের কলামে null নেই, এমন নিয়ম
- Order ID অনন্য, এমন নিয়ম
- দামের dtype float, এমন নিয়ম
একটি জালিয়াতি model-এর label আসে ১৪ দিন পরে। একই দিনে দুটি সংকেত: একটি input কলামে null-এর হার ০.৭% থেকে ৩০%, আর আগের সপ্তাহের (label-সহ) AUC সামান্য কম। সবচেয়ে ভালো প্রথম পদক্ষেপ কোনটি?
- সঙ্গে সঙ্গে সাম্প্রতিক ডেটায় retrain করা
- আগের model-এ rollback করা
- Label আসা পর্যন্ত অপেক্ষা করা
- ডেটার উৎস খুঁজে ঠিক করা, fallback চালু আর ঘটনার দিনগুলো চিহ্নিত করা
একটি অনলাইন দোকানে প্রতি শুক্রবার অর্ডার আর নতুন account স্বাভাবিকভাবে বেশি। “গত ১৪ দিনের গড় ± 2σ” নিয়ম প্রায় প্রতি শুক্রবারে alert দেয়। কোন পরিবর্তন সবচেয়ে উপযুক্ত?
- শুক্রবারে নজরদারি বন্ধ রাখা
- সপ্তাহের একই বারের স্থির reference থেকে প্রত্যাশা আর σ নেওয়া
- সব metric-এর সীমা 5σ করা
- গড়ের জানালা ১৪ দিন থেকে ৩ দিন করা
ML Test Score নম্বর-তালিকায় ডেটা, model, অবকাঠামো আর নজরদারি, চার ভাগের নম্বর আলাদা গোনা হয়। একটি দলের চার ভাগের নম্বর ৫, ৪.৫, ৩ আর ০.৫ হলে চূড়ান্ত নম্বর কত?
- ৩.২৫
- ৫
- ০.৫
- ১৩
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- স্তরে স্তরে পাহারা: feature-এর unit test, প্রতি ব্যাচে data contract, প্রতি model-এ model test, পুরো পথের pipeline test, আর চালু অবস্থায় নজরদারি। সবচেয়ে দুর্বল স্তরই নির্ভরযোগ্যতা ঠিক করে।
- Data contract: সব নিয়ম চালিয়ে সব ব্যর্থতা জমান; সারি-ধরে নিয়মের পাশে বণ্টনের নিয়ম (গড়) রাখুন; ভাঙা ব্যাচ quarantine, চুপচাপ মোছা নয়।
- Model test: নির্দিষ্ট slice-এ ন্যূনতম মান, invariance, directional, calibration আর আগের version-এর সঙ্গে regression; পাস করা মানে নিরাপদ, ভালো নয়।
- Alert: k-টি স্বাধীন metric-এ দিনে অন্তত একটি ভুয়া alert-এর সম্ভাবনা ১ − (১ − p)k; ডেটার মানে স্থির সীমা, অন্যগুলোতে স্থির reference আর বার মিলিয়ে সীমা।
- Label আর retrain: আসল মান আসে label এলে, তাই বদল ধরা পড়ে দেরিতে; trigger-এ আগে ডেটার মান, তারপর মান কমা; নতুন model-ও একই test, shadow আর rollback-এর পথ পেরিয়ে চালু।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭০ · বড় ডেটায় ML — out-of-core, incremental learning আর Polars/DuckDBপরবর্তী পর্বdtype, chunk, Welford, partial_fit আর Parquet দিয়ে বড় ডেটা সামলানো।
- মেশিন লার্নিং · কোর্স হোম৭৭টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ৭০ (বড় ডেটায় ML) এখান থেকেও খুলবেন।
- পর্ব ৫৯ · Distribution shift — covariate, label আর concept shift ধরা ও সামলানোআগের ভিত্তিPSI, KS, domain classifier আর retraining-এর কৌশল, যা এই পর্বের নজরদারির পরিসংখ্যান।
- পর্ব ৬৬ · Dashboard — Streamlit দিয়ে model আর metric দেখানোআগের ভিত্তিনজরদারির সংখ্যা কোন পাতায়, কোন ছন্দে, কার জন্য।
- AI-এর ভিত্তি · পর্ব ৭৪ · AI প্রকল্পের জীবনচক্র — সমস্যা বাছাই থেকে maintenance আর পরের পথআগের ভিত্তিDeploy, নজরদারি, retraining আর rollback-এর প্রথম পরিচয়।
- Python প্রোগ্রামিং · টেস্টিং — pytest দিয়ে কোডের প্রতিশ্রুতি যাচাইপাশের ট্র্যাকfixture, parametrize আর assert-এর ভিত্তি।
- Cloud আর DevOps · Incident management — severity, ভূমিকা, runbook আর postmortemপাশের ট্র্যাকযেকোনো system-এর incident সামলানোর পুরো কাঠামো।
- System Design · Observabilityপাশের ট্র্যাকLog, metric আর trace দিয়ে চালু system-কে দেখা।