এই পর্বে যা শিখবেন
- একটি ML experiment আবার চালাতে ঠিক কী কী লিখে রাখতে হয়, আর config কেন code-এর বাইরে থাকে
- পাতার ভেতরে একটি config-চালিত experiment আর JSONL খাতা, আর ডেস্কটপে MLflow দিয়ে run, artifact আর তুলনা
- Seed, split আর fold বদলালে ফল কতটা নড়ে, আর mean ± sd, t-interval ও bootstrap interval পড়া
- একই fold-এ ablation study চালিয়ে কোন feature-দল রাখবেন তা ঠিক করা
- Garden of forking paths কীভাবে noise থেকে “আবিষ্কার” বানায়, আর একটি সৎ experiment report-এর ছাঁচ
১ · একই ফল আবার — repeat, reproduce আর replicate
বাজারঘরের ডেটা দলের রুমানা জানালেন, নতুন churn model-এর AUC ০.৭৪. দুই সপ্তাহ পরে ম্যানেজার একই সংখ্যা চাইলেন, কারণ বোর্ড মিটিংয়ে দেখাতে হবে। রুমানা notebook খুলে চালালেন: ০.৭২. আবার চালালেন: ০.৭৬. কোনটি আসল? কোন ডেটায়, কোন সেটিংয়ে ০.৭৪ এসেছিল, তা কেউ লিখে রাখেননি। এই পর্বের বিষয় ঠিক এই সমস্যা।
ভিত্তির কাজ আগেই হয়েছে। AI-এর ভিত্তি কোর্সের পর্ব ১২-এ ফোল্ডার, uv.lock, ডেটার SHA-256 ছাপ আর একটি JSONL খাতা দিয়ে নতুন clone-এ হুবহু একই metrics পাওয়া গেছে; পর্ব ৭৪-এ একটি ছোট run log আর একটি MLflow run দেখা গেছে। এখানে আমরা আরও গভীরে যাব: config-চালিত experiment, ফলের ওঠানামা মাপা, ন্যায্য তুলনা, ablation, আর অনেক চেষ্টার ফাঁদ।
শুরুতে তিনটি শব্দ আলাদা করা দরকার, কারণ গবেষণায় এগুলো প্রায়ই গুলিয়ে যায় (বিভিন্ন সংস্থা সংজ্ঞা একটু আলাদাভাবে দেয়, তাই এখানে প্রচলিত একটি রূপ):
| স্তর | কে, কী দিয়ে | কী প্রমাণ করে |
|---|---|---|
| Repeatability | একই মানুষ, একই কম্পিউটার, একই code ও ডেটা | হিসাবটি স্থির; seed বাঁধা আছে |
| Reproducibility | অন্য মানুষ, অন্য কম্পিউটার, একই code ও ডেটা | ফলটি কারও ব্যক্তিগত ল্যাপটপের ওপর নির্ভর করে না |
| Replicability | নতুন ডেটা বা নতুন implementation, একই প্রশ্ন | দাবিটি একটি ভাগ্যবান ডেটাসেটের ওপর দাঁড়িয়ে নেই |
পরীক্ষার খাতা দেখার কথা ভাবুন। একই শিক্ষক একই খাতা দুবার দেখলে একই নম্বর দেবেন কি না, সেটি repeatability. অন্য শিক্ষক একই খাতা আর একই উত্তরমালা পেলে একই নম্বর দেবেন কি না, সেটি reproducibility; উত্তরমালা (config) লেখা না থাকলে তিনি পারবেন না। আর নতুন প্রশ্নপত্রেও ছাত্রটি ভালো করবে কি না, সেটি replicability. ML-এ প্রথম দুটি আপনার শৃঙ্খলার ব্যাপার; তৃতীয়টি নতুন ডেটা চায়।
একটি run-কে একটি ফাংশন হিসেবে দেখলে পুরো ব্যাপারটি পরিষ্কার হয়: ফল = f(code, environment, ডেটা, config, seed). পাঁচটি ইনপুটের যেকোনো একটি অজানা থাকলে ফলটি আর ফাংশনের মান নয়, একটি গুজব। নিচের ছবিতে একটি run-এর পুরো রেকর্ড:
একটি experiment run-এর শারীরস্থান
Five inputs, one run, two kinds of output, one log
২ · Run আবার চালাতে কী লাগে — config ফাইল আর পাঁচ উপাদান
সবচেয়ে সাধারণ ভুল হলো সেটিং code-এর ভেতরে ছড়িয়ে রাখা: এক লাইনে C=0.5, আরেক জায়গায় max_depth=6, notebook-এর কোনো ঘরে হাতে বাদ দেওয়া একটি কলাম। তখন একটি experiment মানে code-এ হাত দেওয়া, আর পুরোনো run-এর সেটিং ফিরে পেতে Git-এর ইতিহাস খুঁড়তে হয়। প্রতিকার হলো config ফাইল: সব সেটিং এক জায়গায়, code শুধু সেটি পড়ে।
{"model": "logreg", "C": 1.0, "features": "all", "seed": 42, "folds": 5}এরপর নতুন experiment মানে নতুন একটি config ফাইল, code-এ হাত নয়। Config-এর ধরন JSON, YAML বা TOML যেকোনোটি হতে পারে; বড় প্রকল্পে Hydra-র মতো হাতিয়ার কয়েকটি config জুড়ে দেয় (একটি ডেটার, একটি model-এর) আর command line থেকে একটি মান বদলাতে দেয়। ধরন যা-ই হোক, তিনটি নিয়ম কাজে লাগে:
- প্রতিটি মান স্পষ্ট: library-র default-এর ওপর ভরসা নয়। scikit-learn-এর কোনো default পরের version-এ বদলালে config-এ লেখা মান আপনাকে বাঁচায়।
- Config-এর ছাপ: config-কে key সাজিয়ে JSON বানিয়ে তার hash নিলে একটি ছোট
cfg_idপাওয়া যায়। একই config দুবার চললে একই id, তাই খাতায় পুনরাবৃত্তি ধরা সহজ। - শুধু প্রাসঙ্গিক key: tree model-এর config-এ logistic regression-এর
Cরেখে দিলে খাতায় বিভ্রান্তি আসে (পরের ব্লকে তা চোখে পড়বে)।
Config একা যথেষ্ট নয়। একটি run আবার চালাতে কী কী লাগে, নিচের stepper-এ একটি রেকর্ড ধাপে ধাপে ভরে উঠছে; প্রতিটি ধাপে দেখুন, ওই মুহূর্তে কেউ run-টি আবার চালাতে পারতেন কি না।
| Code | |
| Environment | |
| ডেটা | |
| Config | |
| Seed | |
| ফল |
- শুধু code. Commit
7b6a64f, আর working tree পরিষ্কার (dirty=False, মানে commit-এর বাইরে কোনো বদল নেই)। কিন্তু একই code বহু config আর বহু ডেটায় চলে; শুধু এটুকু দিয়ে ফল ফেরানো যায় না। - Environment যোগ হলো। Python 3.14.4, scikit-learn 1.9.1, আর সব package-এর হুবহু version
uv.lock-এ। এখনো জানা নেই কোন ডেটা। - ডেটার ছাপ। SHA-256-এর প্রথম ১২টি অক্ষর
c4ef92b5edb4, ১,০০০টি সারি। ডেটায় একটি মান বদলালেও এই ছাপ পুরো বদলাবে। এবার শুধু সেটিং বাকি। - Config. logistic regression, C = 1.0, সব feature, ৫টি fold. প্রায় সব জানা, কিন্তু fold-এ কোন গ্রাহক কোথায় যাবেন তা seed ছাড়া এলোমেলো, তাই সংখ্যা তৃতীয় দশমিকে নড়তে পারে।
- Seed. ডেটা বানানোর seed 7, fold-ভাগের seed 42. পাঁচটি ইনপুটই জানা: যে কেউ একই ফল পাবেন।
- ফল আর artifact. AUC ০.৭৩৫৭ ± ০.০২৫৭ (৫টি fold-এর mean ± sd), সঙ্গে config-এর কপি আর model ফাইল। আবার চালিয়ে যিনি পাবেন, তিনি এই সংখ্যার সঙ্গে মিলিয়ে নিশ্চিত হবেন। এই সংখ্যাগুলো অংশ ৮-এর সত্যিকারের MLflow run থেকে।
Commit-এর hash সব বলে না। Commit হলো শেষ সংরক্ষিত অবস্থা; ফোল্ডারে না-commit-করা বদল থাকলে log-এ একই hash থাকবে অথচ code আলাদা। তাই git status --porcelain খালি কি না, সেটিও একটি dirty চিহ্ন হিসেবে লেখা হয়। অংশ ৮-এ এর সত্যিকারের উদাহরণ আছে।
৩ · Config থেকে run, run থেকে খাতা — পাতার ভেতরে
এবার পুরো চক্রটি পাতার ভেতরেই। ডেটা আসে নিচের module থেকে: বাজারঘরের ১,০০০ জন কাল্পনিক গ্রাহক, চারটি feature-দলে আটটি কলাম, আর লক্ষ্য churn (পরের ৯০ দিনে আর কেনেননি = 1). সব সংখ্যা seed দিয়ে বানানো, কোনো আসল গ্রাহকের নয়।
GROUPS = {"activity": ["orders_90d", "days_since"],
"money": ["basket", "discount"],
"service": ["late", "returns"],
"profile": ["dhaka", "tenure"]}
# z = -0.9 - 0.35*orders + 0.025*days + 0.45*late + 0.30*returns
# + 2.0*(discount - 0.3) - 0.2*log(basket/1100) + 0.05*dhaka
# churn = 1, সম্ভাবনা 1 / (1 + e^(-z)) দিয়ে; tenure-এর কোনো প্রভাব নেইলক্ষ করুন, ডেটা আমরা নিজেরাই বানিয়েছি, তাই সত্যটা জানা: activity আর service-এর প্রভাব বড়, money-র মাঝারি, profile-এর প্রায় শূন্য (dhaka-র ওজন মাত্র ০.০৫, tenure-এর শূন্য)। অংশ ৬-এর ablation এই সত্যটি খুঁজে পায় কি না, দেখব। নিচের ব্লকে একটি run_experiment(cfg) ফাংশন config নিয়ে ৫-fold cross-validation চালায়, আর খাতায় এক লাইন লেখে। scikit-learn প্রথমবার লোড হতে প্রায় ১০ সেকেন্ড লাগতে পারে।
import json, hashlib
import numpy as np
import pandas as pd
import sklearn
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from bazar_churn import make_customers
CODE_VERSION = "7b6a64f" # বাস্তবে: git rev-parse --short HEAD
df = make_customers(n=1000, seed=7)
data_hash = hashlib.sha256(df.to_csv(index=False).encode()).hexdigest()[:12]
def run_experiment(cfg):
m = (make_pipeline(StandardScaler(), LogisticRegression(C=cfg["C"]))
if cfg["model"] == "logreg" else
DecisionTreeClassifier(max_depth=cfg["depth"], random_state=cfg["seed"]))
cv = StratifiedKFold(cfg["folds"], shuffle=True, random_state=cfg["seed"])
s = cross_val_score(m, df.drop(columns="churn"), df.churn, cv=cv, scoring="roc_auc")
cfg_id = hashlib.sha256(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
rec = {"cfg_id": cfg_id, "code": CODE_VERSION, "data": data_hash,
"sklearn": sklearn.__version__, "params": cfg,
"auc_mean": round(s.mean(), 4), "auc_sd": round(s.std(ddof=1), 4)}
with open("runs.jsonl", "a", encoding="utf-8") as f: # শুধু শেষে যোগ
f.write(json.dumps(rec) + "\n")
base = {"model": "logreg", "C": 1.0, "seed": 42, "folds": 5}
for change in [{}, {"C": 0.01}, {"model": "tree", "depth": 4}, {}]: # শেষটি পুনরাবৃত্তি
run_experiment({**base, **change})
rows = [json.loads(line) for line in open("runs.jsonl", encoding="utf-8")]
log = pd.json_normalize(rows).rename(columns=lambda c: c.replace("params.", ""))
print("data", data_hash, "| sklearn", log.sklearn[0], "| code", CODE_VERSION)
print(log[["cfg_id", "model", "C", "depth", "auc_mean", "auc_sd"]].to_string())আসল আউটপুট (ব্রাউজারের Python, scikit-learn 1.8.0):
data 72706f45cd4b | sklearn 1.8.0 | code 7b6a64f
cfg_id model C depth auc_mean auc_sd
0 8061b6d9 logreg 1.00 NaN 0.7357 0.0257
1 848b0468 logreg 0.01 NaN 0.7361 0.0256
2 b3f95ae1 tree 1.00 4.0 0.6507 0.0347
3 8061b6d9 logreg 1.00 NaN 0.7357 0.0257
খাতাটি পড়ুন। প্রথম আর শেষ সারির cfg_id একই (8061b6d9), কারণ config হুবহু এক; ফলও হুবহু এক, ০.৭৩৫৭ ± ০.০২৫৭. এটিই repeatability-র প্রমাণ, আর খাতায় পুনরাবৃত্তি ধরার উপায়। C = ০.০১ (অনেক বেশি regularization) দিয়েও AUC কার্যত একই, ০.৭৩৬১: এই ডেটায় C প্রায় কিছুই বদলায় না। Depth ৪-এর tree ০.৬৫০৭, স্পষ্টভাবে পিছিয়ে।
একটি ছোট ফাঁদ: tree-এর সারিতেও C = ১.০০ দেখাচ্ছে, কারণ base config থেকে এসেছে, অথচ tree এটি ব্যবহারই করে না। ছয় মাস পরে কেউ ভাবতে পারেন C-ও tree-কে প্রভাবিত করেছে। Model অনুযায়ী আলাদা config অংশ রাখা এই বিভ্রান্তি দূর করে। "a" (append) মোডে খোলা হয়েছে বলে আগের লাইন কখনো মোছে না; ব্লকটি দুবার চালালে খাতায় আটটি সারি হবে, আর cfg_id দেখে আপনি পুনরাবৃত্তি চিনবেন।
ডেটার ছাপ: পাতায় 72706f45cd4b, অথচ অংশ ৮-এর ডেস্কটপ run-এ একই ডেটার ছাপ c4ef92b5edb4. AUC চার দশমিক পর্যন্ত হুবহু মেলে, তাই ডেটার মান একই বলেই মনে হয়; পার্থক্য সম্ভবত CSV লেখার সূক্ষ্ম পার্থক্যে (পাতায় pandas 3.0.2 আর 32-bit WebAssembly, ডেস্কটপে pandas 3.1.1), যদিও কারণটি এখানে খুঁটিয়ে দেখা হয়নি। শিক্ষা: hash লেখার প্রতিটি byte ধরে, তাই ডেটার ছাপ একই পরিবেশে তুলনা করুন, অথবা সংরক্ষিত ফাইলের byte থেকেই নিন (অংশ ৮-এর train.py যেমন করে)।
৪ · ফল কতটা নড়ে — split-এর seed, model-এর seed আর fold
খাতায় ০.৭৩৫৭ লেখা থাকলেই সেটি “model-এর AUC” নয়। সংখ্যাটি একটি নির্দিষ্ট ভাগের, একটি নির্দিষ্ট seed-এর। Seed বাঁধলে ফল স্থির হয়, কিন্তু সঠিক হয় না; শুধু একটি এলোমেলো বিন্দুতে আটকে যায়। কতটা এলোমেলো, সেটি জানতে ওঠানামার উৎসগুলো আলাদা করতে হয়:
- ভাগের ভাগ্য: কোন গ্রাহক train-এ, কোন গ্রাহক test-এ। ছোট test set-এ এটিই সাধারণত সবচেয়ে বড় উৎস।
- Model-এর ভেতরের এলোমেলোতা: tree-তে কোন feature আগে বিবেচনা হবে, forest-এ কোন নমুনা কোন গাছে, neural network-এ শুরুর ওজন।
- Fold-এর ভাগ: cross-validation-এ কোন গ্রাহক কোন fold-এ।
- হিসাবের ক্রম: একাধিক thread বা GPU-তে যোগের ক্রম বদলালে দশমিকের শেষ ঘর বদলায় (AI-এর ভিত্তি কোর্সের পর্ব ১২-এর “আরও গভীরে” অংশ)।
নিচের ব্লক প্রথম তিনটি উৎস আলাদা করে মাপে। (ক) logistic regression, ডেটা একই, শুধু ৭০/৩০ ভাগের seed ০ থেকে ১৯. (খ) একটি tree, ভাগ একটিই, শুধু model-এর seed ০ থেকে ১৯ (max_features=0.5 থাকায় tree-তেও এলোমেলোতা আছে)। (গ) একটি ৫-fold. শেষে একটি test set-এর AUC-র bootstrap interval: test-এর সারিগুলো ফেরত রেখে ৫০০ বার নতুন করে টেনে।
import numpy as np
import pandas as pd
from scipy import stats
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.metrics import roc_auc_score
from bazar_churn import make_customers
df = make_customers(n=1000, seed=7)
X, y = df.drop(columns="churn").to_numpy(), df.churn.to_numpy()
idx, lr = np.arange(len(y)), lambda: make_pipeline(StandardScaler(), LogisticRegression())
def auc(model, tr, te):
return roc_auc_score(y[te], model.fit(X[tr], y[tr]).predict_proba(X[te])[:, 1])
def summary(name, s):
m, sd, n = s.mean(), s.std(ddof=1), len(s)
h = stats.t.ppf(0.975, n - 1) * sd / np.sqrt(n) # t-interval-এর অর্ধেক
print(f"{name:<22} {m:.4f} ± {sd:.4f} CI [{m-h:.4f}, {m+h:.4f}] "
f"range {s.min():.3f}-{s.max():.3f}")
split = np.array([auc(lr(), *train_test_split(idx, test_size=0.3, random_state=s,
stratify=y)) for s in range(20)]) # শুধু ভাগ বদলায়
tr0, te0 = train_test_split(idx, test_size=0.3, random_state=0, stratify=y)
tree = np.array([auc(DecisionTreeClassifier(max_depth=5, max_features=0.5,
random_state=s), tr0, te0) for s in range(20)]) # শুধু model-এর seed
folds = np.array([auc(lr(), tr, te) for tr, te in
StratifiedKFold(5, shuffle=True, random_state=0).split(X, y)])
for name, s in [("logreg 20 split seeds", split), ("tree 20 model seeds", tree),
("logreg 5 folds", folds)]:
summary(name, s)
p0 = lr().fit(X[tr0], y[tr0]).predict_proba(X[te0])[:, 1]
B = np.random.default_rng(0).integers(0, len(te0), (500, len(te0))) # bootstrap
b = [roc_auc_score(y[te0][i], p0[i]) for i in B]
print("one split AUC", round(roc_auc_score(y[te0], p0), 4), "bootstrap 95%",
np.percentile(b, [2.5, 97.5]).round(4))আসল আউটপুট (ব্রাউজারের Python):
logreg 20 split seeds 0.7370 ± 0.0214 CI [0.7270, 0.7470] range 0.705-0.784
tree 20 model seeds 0.6587 ± 0.0210 CI [0.6489, 0.6685] range 0.607-0.693
logreg 5 folds 0.7398 ± 0.0455 CI [0.6834, 0.7963] range 0.664-0.776
one split AUC 0.7588 bootstrap 95% [0.7074 0.8119]
প্রথম সারি: একই model, একই ডেটা, শুধু কোন ৩০০ জন test-এ গেলেন তা বদলেছে, আর AUC ০.৭০৫ থেকে ০.৭৮৪ পর্যন্ত ছড়িয়েছে। রুমানার ০.৭২ আর ০.৭৬ দুটোই এই পরিসরের ভেতরে; কোনোটিই “ভুল” ছিল না, শুধু একা অর্থহীন। seed 0-এর ভাগটি (শেষ লাইন, ০.৭৫৮৮) গড়ের চেয়ে ভালো ভাগ্যের: কেউ ঘটনাক্রমে এটিই রিপোর্ট করলে model-কে প্রায় ০.০২ বেশি ভালো দেখাত।
দ্বিতীয় সারি: ভাগ একটিই, তবু tree-এর শুধু নিজের seed বদলালে AUC ০.৬০৭ থেকে ০.৬৯৩. অর্থাৎ “tree বনাম logistic” তুলনায় tree-এর একটি seed নেওয়া মানে একটি লটারির টিকিট নেওয়া। তৃতীয় সারি: ৫টি fold-এর sd (০.০৪৫৫) ২০টি split-এর sd-এর চেয়ে বড়, কারণ প্রতিটি fold-এ test মাত্র ২০০ জন; কিন্তু CI চওড়া, কারণ n মাত্র ৫. শেষ লাইন: একটি test set-এর bootstrap interval বলে, শুধু test-এর গ্রাহকদের ভাগ্যেই সংখ্যাটি মোটামুটি ০.৭১ থেকে ০.৮১-এর মধ্যে থাকতে পারত। তিনটি মাপ তিনটি আলাদা প্রশ্নের উত্তর দেয়, তাই রিপোর্টে লিখতে হয় কোনটি মাপা হলো।
৫ · mean ± sd পড়া, interval, আর fold কেন স্বাধীন নয়
“০.৭৩৮ ± ০.০২৬” দেখলে প্রথম প্রশ্ন: ± এর পরের সংখ্যাটি কী? দুটি সম্পূর্ণ আলাদা জিনিস এখানে বসতে পারে, আর রিপোর্টে সেটি না লিখলে পাঠক বিভ্রান্ত হন:
- Standard deviation (sd): একটি একক run-এর score কতটা ছড়ায়। নতুন একটি seed-এ চালালে মোটামুটি কোন পরিসরে পড়বে, তার আন্দাজ। n বাড়লেও এটি কমে না।
- Standard error (SE = sd/√n): গড়টি কতটা অনিশ্চিত। n বাড়লে কমে। Confidence interval এখান থেকেই আসে: গড় ± t × SE, যেখানে t আসে t-distribution থেকে (n − 1 মুক্তমাত্রা; n = ৫ হলে ৯৫%-এর জন্য t ≈ ২.৭৭৬)। t-interval-এর পুরো ব্যাখ্যা AI-এর ভিত্তি কোর্সের পর্ব ৩৭-এ।
এখন দুটি model তুলনা করা যাক, যা অংশ ৮-এর MLflow run থেকে এসেছে: A = profile-দল বাদ দেওয়া logistic regression, B = সব feature. দুজনই একই ৫টি fold-এ মাপা (fold-ভাগের seed 42). নিচের stepper হাতে হিসাব করে, আর শেষে জোড়া পার্থক্য দেখে।
| A: profile বাদ | |
| B: সব feature | |
| হিসাব |
- গড়। A-র পাঁচটি fold-এর যোগ ৩.৬৯২, গড় ০.৭৩৮৪. শুধু এই সংখ্যা দিয়ে A ভালো না খারাপ, বলা যায় না।
- sd. গড় থেকে বিচ্যুতি −০.০০৯৪, −০.০১৩৪, −০.০৩০৪, +০.০৩৪৬, +০.০১৮৬; এদের বর্গের যোগ ০.০০২৭৩৫. n − 1 = ৪ দিয়ে ভাগ করে বর্গমূল: sd ≈ ০.০২৬১. অর্থাৎ একটি fold-এর score গড় থেকে সাধারণত ০.০২–০.০৩ সরে থাকে।
- SE আর CI. SE = ০.০২৬১/√৫ ≈ ০.০১১৭. ৪ মুক্তমাত্রায় t = ২.৭৭৬, তাই অর্ধ-প্রস্থ ≈ ০.০৩২৫, আর ৯৫% t-interval প্রায় [০.৭০৬, ০.৭৭১]।
- B পাশে বসানো। B: ০.৭৩৫৮ ± ০.০২৫৭, interval [০.৭০৪, ০.৭৬৮]। দুটি interval প্রায় পুরোপুরি একে অপরের ওপর। আলাদা আলাদা interval দেখে মনে হয় “কোনো পার্থক্য বোঝা যাচ্ছে না”।
- জোড়া পার্থক্য। একই fold-এ দুজনই একই গ্রাহকদের দেখেছে, তাই fold-ভিত্তিক পার্থক্য নেওয়াই ঠিক: ০.০০৭, ০.০০৩, −০.০০১, ০.০০৪, ০.০০০. গড় ০.০০২৬, sd মাত্র ০.০০৩২. Fold-এর কঠিন-সহজ ভাগ্য দুজনের ওপর একইভাবে পড়েছিল, পার্থক্য নিলে তা কেটে যায়।
- সিদ্ধান্ত। সাধারণ জোড়া t-interval [−০.০০১৪, ০.০০৬৬], শূন্য ভেতরে। Fold-গুলো স্বাধীন নয় বলে Nadeau ও Bengio-র সংশোধন (নিচে) interval চওড়া করে [−০.০০৩৪, ০.০০৮৬]। দুটিতেই “A ভালো” বলার প্রমাণ নেই। তবু A-তে দুটি feature কম, আর ক্ষতির প্রমাণও নেই; তাই সরলতার যুক্তিতে A রাখা যায়, AUC-র যুক্তিতে নয়।
Fold-এর score কেন স্বাধীন নয়
t-interval ধরে নেয় সংখ্যাগুলো একে অপরের থেকে স্বাধীন। k-fold-এ তা সত্য নয়: ৫-fold-এ যেকোনো দুটি fold-এর training অংশের তিন-চতুর্থাংশ সারি একই, তাই তাদের model প্রায় একই, আর score একসঙ্গে ওঠানামা করে। ফল: sd/√k ছোট দেখায়, interval বেশি সরু। Bengio ও Grandvalet (২০০৪) দেখিয়েছেন, k-fold-এর score থেকে variance-এর কোনো নিরপেক্ষ (unbiased) অনুমান সাধারণভাবে সম্ভব নয়। ব্যবহারিক একটি প্রতিকার Nadeau ও Bengio (২০০৩)-র corrected resampled t-test: variance-কে 1/k দিয়ে গুণ না করে (1/k + ntest/ntrain) দিয়ে গুণ করা। ৫-fold-এ ntest/ntrain = ২০০/৮০০ = ০.২৫, তাই গুণক ০.২ থেকে বেড়ে ০.৪৫ হয়, আর interval প্রায় √(০.৪৫/০.২) ≈ ১.৫ গুণ চওড়া। এটি একটি আনুমানিক সংশোধন, নিখুঁত নয়।
তাহলে কী রিপোর্ট করবেন? একটি সৎ লাইন এমন: “৫-fold × ৪ পুনরাবৃত্তি, একই ভাগে, AUC ০.৭৩৯ (sd ০.০২৬); A − B জোড়া পার্থক্য +০.০০২৬, সংশোধিত ৯৫% interval [−০.০০৩, ০.০০৯]।” কোন উৎসের ওঠানামা (split, seed, fold), কতগুলো, আর ± মানে sd না SE, তিনটিই লেখা। দুই model-এর তুলনার পূর্ণ পরিসংখ্যান, যেমন paired test, McNemar test আর অনেক তুলনার সংশোধন, আসবে পর্ব ২২ (Model তুলনার পরিসংখ্যান)-এ; bootstrap-এর তত্ত্ব পর্ব ২১-এ।
৬ · Ablation study — একটি করে অংশ খুলে দেখা
চায়ের দোকানের মালিক ভাবছেন, কোন জিনিসটির জন্য বিক্রি বাড়ছে: নতুন কাপ, বিস্কুট, না পাশের টিভি? একসঙ্গে সব বদলালে বোঝা যায় না। এক সপ্তাহ একটি করে সরিয়ে দেখলে বোঝা যায় কোনটি না থাকলে বিক্রি কমে। এটিই ablation study: একটি সম্পূর্ণ ব্যবস্থা থেকে একটি অংশ (একটি feature-দল, একটি preprocessing ধাপ, model-এর একটি অংশ) সরিয়ে দেখা ফল কতটা বদলায়। শব্দটি এসেছে জীববিজ্ঞানের পরীক্ষা থেকে, যেখানে কোনো টিস্যু সরিয়ে তার কাজ বোঝা হতো।
Ablation-এর নিয়ম তিনটি। (১) একই ভাগ: প্রতিটি সংস্করণ হুবহু একই fold-এ, যাতে পার্থক্য শুধু সরানো অংশের জন্য হয়। (২) জোড়া পার্থক্য: প্রতিটি ভাগে পূর্ণ model থেকে কতটা সরল, আর কতগুলো ভাগে খারাপ হলো। (৩) দল ধরে: সম্পর্কিত feature একসঙ্গে, কারণ orders_90d সরালে days_since তার অনেকটা কাজ ঢেকে দিতে পারে। নিচের ব্লকে ৫-fold × ৪ পুনরাবৃত্তি = ২০টি একই ভাগে প্রতিটি দল একবার করে সরানো হয়েছে।
import numpy as np
import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
from bazar_churn import make_customers, GROUPS
df = make_customers(n=1000, seed=7)
y = df.churn
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=4, random_state=0) # ২০টি একই ভাগ
model = make_pipeline(StandardScaler(), LogisticRegression())
def scores(cols):
return cross_val_score(model, df[cols], y, cv=cv, scoring="roc_auc")
all_cols = [c for g in GROUPS.values() for c in g]
full = scores(all_cols)
rows = [{"dropped": "(none)", "auc": full.mean(), "delta": 0.0, "delta_sd": 0.0, "worse": "-"}]
for g, cols in GROUPS.items():
s = scores([c for c in all_cols if c not in cols])
d = s - full # একই ভাগে জোড়া পার্থক্য
rows.append({"dropped": g, "auc": s.mean(), "delta": d.mean(), "delta_sd": d.std(ddof=1),
"worse": f"{(d < 0).sum()}/{len(d)}"})
pd.set_option("display.width", 70)
print(pd.DataFrame(rows).round(4).to_string(index=False))আসল আউটপুট (ব্রাউজারের Python):
dropped auc delta delta_sd worse
(none) 0.7391 0.0000 0.0000 -
activity 0.6032 -0.1359 0.0389 20/20
money 0.7271 -0.0119 0.0130 17/20
service 0.7147 -0.0243 0.0186 17/20
profile 0.7408 0.0018 0.0019 5/20
ব্লক একটি RepeatedStratifiedKFold বস্তু সব সংস্করণে ব্যবহার করেছে; random_state বাঁধা থাকায় প্রতিবার split ডাকলে একই ২০টি ভাগ আসে। তাই d = s - full সত্যিকারের জোড়া পার্থক্য। worse কলাম গোনে ২০টির কতগুলো ভাগে AUC কমেছে। সংখ্যাগুলোর ব্যাখ্যা নিচের stepper-এ, একটি করে সিদ্ধান্ত।
| এই ধাপ | |
| দেখা যাচ্ছে | |
| অর্থ |
- মাপকাঠি। আটটি feature নিয়ে পূর্ণ model: ২০টি ভাগে গড় AUC ০.৭৩৯১. বাকি প্রতিটি সারি এর সঙ্গে একই ভাগে তুলনা।
- Activity সরানো। AUC নামে ০.৬০৩২-এ, Δ = −০.১৩৫৯, আর ২০টি ভাগের ২০টিতেই খারাপ। ডেটা বানানোর সূত্রেও অর্ডারের সংখ্যা আর শেষ অর্ডারের পরের দিন সবচেয়ে প্রভাবশালী ছিল; ablation সেটিই খুঁজে পেয়েছে।
- Money সরানো। Δ = −০.০১১৯, sd ০.০১৩০, ১৭/২০ ভাগে খারাপ। গড় ক্ষতি ছোট, প্রায় এক sd-এর সমান, কিন্তু দিক ধারাবাহিক: ২০টির ১৭টিতে কমেছে। রাখার পক্ষে যুক্তি আছে।
- Service সরানো। Δ = −০.০২৪৩, ১৭/২০ ভাগে খারাপ। দেরিতে ডেলিভারি আর ফেরত churn-এর স্পষ্ট সংকেত; এই দলও থাকবে।
- Profile সরানো। Δ = +০.০০১৮, মাত্র ৫/২০ ভাগে খারাপ। সত্য সূত্রে
dhaka-র ওজন ০.০৫ আরtenure-এর শূন্য; ১,০০০ সারিতে এত দুর্বল সংকেত শেখার চেয়ে noise শেখার ক্ষতিই বেশি। - সিদ্ধান্ত। Profile বাদ, বাকি তিনটি দল থাকে: কম feature, কম ডেটা সংগ্রহ, একই মান। সতর্কতা: এই সিদ্ধান্ত cross-validation দেখে নেওয়া, তাই এটিও একটি বাছাই; চূড়ান্ত model-এর নিরপেক্ষ সংখ্যা আসবে তালাবন্ধ test set থেকে, একবার।
Ablation-এর সীমা: একটি করে সরানো দলের মিথস্ক্রিয়া (interaction) ধরে না: দুটি দল একে অপরের বদলি হলে একটিকে সরালে কিছুই হয় না, দুটি একসঙ্গে সরালে বড় ক্ষতি। তখন জোড়ায় সরানো বা উল্টো পথ (শুধু একটি দল দিয়ে শুরু করে একটি করে যোগ) কাজে লাগে। Ablation model-কে নতুন করে train করে মাপে; train না করে একটি feature-এর মান এলোমেলো করে মাপার পদ্ধতি permutation importance, যা পর্ব ৫৬ (Model বোঝা)-এ আসবে। আর ablation “এই দল কারণ” প্রমাণ করে না; শুধু বলে model-এর অনুমানে দলটি কতটা কাজে লাগছে।
৭ · Garden of forking paths — অনেক চেষ্টা করলে noise-ও জেতে
AI-এর ভিত্তি কোর্সের পর্ব ৬২-এ EDA-র প্রসঙ্গে garden of forking paths দেখা গেছে: প্রতিটি “এটা দেখি, ওটা বাদ দিই” একটি শাখা। ML experiment-এ শাখাগুলো আরও বেশি: কোন feature, কোন outlier নিয়ম, কোন model, কোন C, কোন threshold, কোন metric. প্রতিটি ছোট সিদ্ধান্ত validation-এর ফল দেখে নিলে, শেষ পথটি অনেক পথের সেরা, আর সেরা হওয়ার পেছনে ভাগ্যের অংশ বড়।
সবচেয়ে কঠিন পরীক্ষা: এমন ডেটা যেখানে শেখার মতো কিছুই নেই। নিচের ব্লকে ৩০টি feature আর label সবই এলোমেলো, তাই সত্যিকারের accuracy ঠিক ০.৫. তারপর ২০০টি “বিশ্লেষণের পথ” চেষ্টা করা হয়: প্রতিটিতে এলোমেলো কয়েকটি feature, একটি C, আর outlier বাদ দেওয়ার একটি নিয়ম। প্রতিটি পথ ৬০ সারির validation-এ মাপা, আর সেরাটি তারপর ৫,০০০টি নতুন সারিতে।
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
rng = np.random.default_rng(42)
p = 30 # ৩০টি feature, সবই নিছক noise
Xtr, Xva, Xnew = (rng.normal(size=(n, p)) for n in (200, 60, 5000))
ytr, yva, ynew = (rng.integers(0, 2, n) for n in (200, 60, 5000)) # label-ও noise
results = []
for k in range(200): # ২০০টি "বিশ্লেষণের পথ"
cols = rng.choice(p, size=rng.choice([3, 5, 8, 12]), replace=False)
C = rng.choice([0.01, 0.1, 1.0, 10.0])
keep = np.abs(Xtr[:, cols]).max(axis=1) < rng.choice([2.0, 2.5, 99]) # outlier বাদ?
m = LogisticRegression(C=C).fit(Xtr[keep][:, cols], ytr[keep])
results.append((m.score(Xva[:, cols], yva), m, cols))
accs = np.array([r[0] for r in results])
best_acc, best_m, best_cols = max(results, key=lambda r: r[0])
print(f"validation accuracy: median {np.median(accs):.3f}, best {best_acc:.3f}")
print(f"best path on 5000 fresh rows: {best_m.score(Xnew[:, best_cols], ynew):.3f}")
print("paths with validation >= 0.60:", int((accs >= 0.60).sum()), "of", len(accs))
fig, ax = plt.subplots(figsize=(6, 3))
ax.hist(accs, bins=20, color="#5b8def")
ax.axvline(0.5, color="black", ls="--", label="true accuracy (0.5)")
ax.axvline(best_acc, color="#d9534f", label=f"best of 200 ({best_acc:.2f})")
ax.set_xlabel("validation accuracy (60 rows)")
ax.set_ylabel("number of paths")
ax.legend(fontsize=8)
plt.show()আসল আউটপুট (ব্রাউজারের Python; নিচে histogram):
validation accuracy: median 0.500, best 0.667
best path on 5000 fresh rows: 0.498
paths with validation >= 0.60: 10 of 200
মাঝের পথটি ঠিক যা হওয়ার কথা, ০.৫০০. কিন্তু সেরা পথ ০.৬৬৭: ৬০টির মধ্যে ৪০টি ঠিক, “noise-এর চেয়ে স্পষ্ট ভালো” মনে হওয়ার মতো। নতুন ৫,০০০ সারিতে সেই পথ ০.৪৯৮, মুদ্রা ছোড়ার সমান। ২০০টির ১০টি পথ ০.৬০ ছুঁয়েছে; যিনি শুধু সেই একটি পথ রিপোর্ট করবেন, তিনি মিথ্যা বলছেন না, কিন্তু সত্যও বলছেন না।
হিসাবটা সহজ: ৬০ সারিতে accuracy-র SE = √(০.৫ × ০.৫ / ৬০) ≈ ০.০৬৫. ২০০টি (পরস্পর-সম্পর্কিত) চেষ্টার সর্বোচ্চটি সাধারণত গড় থেকে ২–৩ SE ওপরে পড়ে, অর্থাৎ ০.৬৩–০.৭০. চেষ্টার সংখ্যা যত বেশি আর validation যত ছোট, “সেরা” তত বেশি ফোলানো। এটিই selection bias বা winner’s curse, আর multiple comparisons-এর ML রূপ।
প্রতিকার — পথ কমানো, পথ গোনা, আর অদেখা ডেটা
- আগে লেখা পরিকল্পনা: metric, প্রধান তুলনা আর থামার নিয়ম শুরুতেই লিখে commit করা। বিজ্ঞানে একে preregistration বলে; ML দলে একটি ছোট “experiment plan” ফাইলই যথেষ্ট।
- সব চেষ্টা খাতায়: JSONL বা MLflow-এ ব্যর্থ run-ও থাকে। রিপোর্টে লিখুন “৪৮টি config চেষ্টা করা হয়েছে”, শুধু বিজয়ীটি নয়।
- বাছাই আর মূল্যায়ন আলাদা: যত পথই চেষ্টা করুন, বিজয়ীর সংখ্যা আসবে এমন ডেটা থেকে যা বাছাইয়ে লাগেনি: তালাবন্ধ test set, বা পুরো বাছাই-পদ্ধতির জন্য nested cross-validation (AI-এর ভিত্তি কোর্সের পর্ব ৬৬)।
- বড় validation, কম পথ: ৬০ সারির বদলে ৬০০ সারিতে SE তিন গুণের বেশি কমে; আর চেষ্টার বাজেট আগে ঠিক করা থাকলে পথ সীমিত থাকে (tuning-এর বাজেট নিয়ে পর্ব ৩৮ আর পর্ব ৪৯)।
৮ · MLflow দিয়ে ডেস্কটপে — run, param, metric, artifact আর তুলনা
পাতার JSONL খাতা ছোট প্রকল্পে যথেষ্ট। Run শতাধিক হলে, প্রতিটি fold-এর metric, model ফাইল আর chart-ও রাখতে হলে, বা দলের কয়েকজন একসঙ্গে কাজ করলে একটি experiment tracking হাতিয়ার সুবিধাজনক। MLflow (Databricks ২০১৮ সালে open-source হিসেবে প্রকাশ করে) এর বহুল ব্যবহৃত একটি। এর চারটি মূল ধারণা:
- Run: একবার চালানো, যার একটি id আছে; কয়েকটি run মিলে একটি experiment (“churn-baseline”).
- Param: একবার লেখা সেটিং (config-এর মান), পরে বদলানো যায় না।
- Metric: সংখ্যা, যা
stepধরে বারবার লেখা যায়, যেমন প্রতিটি fold বা প্রতিটি epoch. - Artifact: যেকোনো ফাইল: config-এর কপি, chart, model. সঙ্গে tag: commit, dirty চিহ্ন, ডেটার hash.
MLflow ব্রাউজারের Python-এ চলে না, তাই একটি ডেস্কটপ venv-এ সত্যিকারের run করা হলো (লেখার সময়ের version: Python 3.14.4, MLflow 3.18.0, scikit-learn 1.9.1; শুধু CPU, কোনো server বা account নয়)। প্রকল্প ~/bazarghar-ml: পাতার মতোই bazar_churn.py, configs/-এ চারটি config, আর train.py. প্রকল্পটি Git-এ, commit-এর লেখক কাল্পনিক “Rahim”. train.py-এর MLflow অংশ:
mlflow.set_tracking_uri("sqlite:///mlflow.db") # একটি স্থানীয় ফাইল, server নয়
mlflow.set_experiment("churn-baseline")
with mlflow.start_run(run_name=sys.argv[1].split("/")[-1][:-5]) as run:
s = cross_val_score(model, df[cols], df.churn, cv=cv, scoring="roc_auc")
mlflow.log_params(cfg)
mlflow.set_tags({"code_version": commit, "git_dirty": dirty,
"data_sha256": data_hash, "sklearn": sklearn.__version__})
for i, v in enumerate(s):
mlflow.log_metric("fold_auc", v, step=i) # প্রতিটি fold আলাদা step
mlflow.log_metrics({"cv_auc_mean": s.mean(), "cv_auc_sd": s.std(ddof=1)})
mlflow.log_dict(cfg, "config.json") # artifact: config-এর কপি
model.fit(df[cols], df.churn)
trusted = ["sklearn.tree._tree.Tree"] if cfg["model"] == "tree" else None
mlflow.sklearn.log_model(model, name="model", skops_trusted_types=trusted,
input_example=df[cols].head(3).astype(float))এর আগে train.py config পড়ে, ডেটা বানিয়ে data.csv লেখে আর তার byte থেকে SHA-256 নেয়, আর git rev-parse --short HEAD ও git status --porcelain থেকে commit আর dirty চিহ্ন। চারটি config-এর সত্যিকারের run (MLflow-এর INFO আর কিছু সতর্কবার্তা “…” দিয়ে ছাঁটা):
~/bazarghar-ml$ git log --oneline
7b6a64f Save tree models with an explicit skops trust list
5e16dc0 Track runs in a local SQLite file
c5be8dd Churn baseline: data generator, configs, train script
~/bazarghar-ml$ for c in logreg_c1 logreg_c001 tree_d4 logreg_noprofile; do
> python train.py configs/$c.json; done
… INFO mlflow.store.db.utils: Creating initial MLflow database tables...
… INFO mlflow.tracking.fluent: Experiment with name 'churn-baseline' does not exist. Creating a new experiment.
logreg_c1 auc 0.7357 ± 0.0257 commit 7b6a64f data c4ef92b5edb4
logreg_c001 auc 0.7361 ± 0.0256 commit 7b6a64f data c4ef92b5edb4
tree_d4 auc 0.6507 ± 0.0347 commit 7b6a64f data c4ef92b5edb4
logreg_noprofile auc 0.7384 ± 0.0262 commit 7b6a64f data c4ef92b5edb4
প্রথম তিনটি সংখ্যা পাতার ব্লক ১-এর খাতার সঙ্গে হুবহু মেলে: একই ডেটা, একই fold-ভাগ, একই model. ডেস্কটপে scikit-learn 1.9.1, পাতায় 1.8.0; এই সরল model-এ দুই version একই ফল দিয়েছে। Run চলার পথে তিনটি সত্যিকারের বাধা এসেছিল, আর তিনটিই শিক্ষণীয়:
- ফাইল-ভিত্তিক store আর ডিফল্ট নয়। প্রথম চেষ্টায়
set_tracking_uri("file:./mlruns")দেওয়া হয়েছিল। MLflow 3.18 সেটি থামিয়ে দিল:MlflowException: The filesystem tracking backend (e.g., './mlruns') is in maintenance mode … Please migrate to a database backend (e.g., 'sqlite:///mlflow.db'). তাই একটি SQLite ফাইল (mlflow.db) ব্যবহার হয়েছে: এখনো স্থানীয়, কোনো server ছাড়া। Artifact তবুmlruns/ফোল্ডারে ফাইল হিসেবেই থাকে। পুরোনো টিউটোরিয়াল বা AI-এর দেওয়া code-এ./mlrunsদেখলে এই পরিবর্তনটি মনে রাখবেন। - Model সংরক্ষণে নিরাপত্তা। MLflow 3.18 scikit-learn model ডিফল্টভাবে skops ফরম্যাটে রাখে, pickle-এ নয়, কারণ pickle খোলার সময় ফাইলের ভেতরের নির্দেশে code চলতে পারে। Tree model রাখার সময় এটি থামল:
Untrusted types found in the file: ['sklearn.tree._tree.Tree']. আমরা নিজেরাই model-টি বানিয়েছি, তাই শুধু ওই একটি type-কেskops_trusted_types-এ বিশ্বস্ত বলা হয়েছে; MLflow-এর বার্তাও সতর্ক করে যে না দেখে সব type বিশ্বস্ত বলা উচিত নয়। - Dirty চিহ্ন প্রথমেই কাজে লাগল। প্রথম চালানোয় সব run
+dirtyহয়েছিল, কারণ ফোল্ডারে না-commit-করা কিছু লগ ফাইল ছিল। Run-এর আগে ফোল্ডার পরিষ্কার রাখা এই চিহ্নের উদ্দেশ্য।
এবার ইচ্ছা করে নিয়ম ভাঙা: configs/logreg_c1.json-এ C = ১.০ থেকে ০.৫ করা হলো, commit ছাড়াই, তারপর একই নামে run:
~/bazarghar-ml$ git status --short
M configs/logreg_c1.json
~/bazarghar-ml$ python train.py configs/logreg_c1.json
logreg_c1 auc 0.7357 ± 0.0258 commit 7b6a64f+dirty data c4ef92b5edb4
Commit একই 7b6a64f, run-এর নামও একই “logreg_c1”, অথচ config আলাদা। নাম বা commit দেখে এই run-কে আগেরটির সঙ্গে গুলিয়ে ফেলা খুব সহজ; শুধু dirty চিহ্ন আর param-এর C = ০.৫ সত্যটা বলে। এবার compare.py দিয়ে তুলনা: search_runs একটি SQL-এর মতো filter নেয়, metric ধরে সাজায়, আর একটি pandas DataFrame দেয়।
runs = mlflow.search_runs(experiment_names=["churn-baseline"],
filter_string="params.model = 'logreg'",
order_by=["metrics.cv_auc_mean DESC"])
best = runs.iloc[0]
client = MlflowClient()
hist = client.get_metric_history(best.run_id, "fold_auc")
print("artifacts:", [a.path for a in client.list_artifacts(best.run_id)])
model = mlflow.sklearn.load_model(f"runs:/{best.run_id}/model")~/bazarghar-ml$ python compare.py
run C features auc_mean auc_sd commit dirty
logreg_noprofile 1.0 no_profile 0.7384 0.0262 7b6a64f False
logreg_c001 0.01 all 0.7361 0.0256 7b6a64f False
logreg_c1 1.0 all 0.7357 0.0257 7b6a64f False
logreg_c1 0.5 all 0.7357 0.0258 7b6a64f True
fold_auc by step: [0.729, 0.725, 0.708, 0.773, 0.757]
artifacts: ['config.json']
reloaded model, churn prob of first 3 customers: [0.163 0.371 0.219]
কয়েকটি জিনিস খেয়াল করুন। (ক) Filter-এ শুধু logreg run, AUC অনুযায়ী সাজানো; dirty run-টি আলাদা চেনা যাচ্ছে। (খ) সেরা run-এর fold_auc-এর ইতিহাস ঠিক সেই পাঁচটি সংখ্যা, যা অংশ ৫-এর stepper-এ A হিসেবে ব্যবহার হয়েছে। (গ) Run-এর artifact তালিকায় শুধু config.json, model নেই! MLflow 3-এ model একটি আলাদা সত্তা (“logged model”, id m-…), যা run-এর সঙ্গে যুক্ত থাকে আর mlruns/1/models/-এ রাখা হয়। তবু runs:/<id>/model ঠিকানা দিয়ে লোড করা গেছে, আর ফেরত আনা model প্রথম তিনজন গ্রাহকের churn সম্ভাবনা দিয়েছে। সেই model ফোল্ডারে কী আছে:
~/bazarghar-ml$ ls mlruns/1/models/m-2816…/artifacts
MLmodel conda.yaml input_example.json model.skops python_env.yaml
requirements.txt serving_input_example.json
~/bazarghar-ml$ cat mlruns/1/models/m-2816…/artifacts/requirements.txt
mlflow==3.18.0
numpy==2.5.4
pandas==3.1.1
scikit-learn==1.9.1
scipy==1.18.1
skops==0.16.0
অর্থাৎ model-এর artifact নিজেই environment-এর একটি ছোট রেকর্ড বহন করে: কোন version-এ বানানো, কোন ইনপুটের নমুনা। MLmodel ফাইলে একটি artifact_path থাকে, যাতে এই কম্পিউটারের পুরো ফোল্ডারের ঠিকানা লেখা; দলের সঙ্গে ভাগ করার আগে এমন ফাইলে ব্যক্তিগত path আছে কি না দেখে নেওয়া ভালো। ওয়েব পাতায় তুলনা দেখার mlflow ui এখানে চালানো হয়নি; প্রয়োজনে সেটি 127.0.0.1-এ চালাতে হয়, যাতে বাইরের কেউ দেখতে না পান। Model registry, deployment আর পুরো MLOps পরের একটি কোর্সের বিষয়; এই কোর্সে production-এর দিক আসবে পর্ব ৬৮ আর পর্ব ৬৯-এ।
৯ · একটি সৎ experiment report — ছাঁচ
খাতা যন্ত্রের জন্য, report মানুষের জন্য। বাজারঘরের এই পর্বের কাজ দিয়ে ভরা একটি ছাঁচ নিচে; নিজের প্রকল্পে শিরোনামগুলো রেখে বিষয়বস্তু বদলাবেন। খেয়াল করুন, এখানে “কী চেষ্টা করা হয়েছিল” আর “কী জানি না” দুটোরই আলাদা জায়গা আছে।
# EXP-004 · churn model-এ profile-দল দরকার কি না (কাল্পনিক)
১. প্রশ্ন ও পরিকল্পনা (run-এর আগে commit 5e16dc0-এ লেখা)
- প্রশ্ন: profile (dhaka, tenure) বাদ দিলে AUC কমে কি না
- metric: ROC AUC; ভাগ: 5-fold × 4, random_state=0; test তালাবন্ধ
২. ইনপুট
- code 7b6a64f (dirty=False) · data sha256 c4ef92b5edb4 · uv.lock
- Python 3.14.4, scikit-learn 1.9.1, MLflow 3.18.0
৩. কী কী চেষ্টা হয়েছে
- ৪টি config + ৪টি ablation সংস্করণ; ১টি dirty run বাতিল
৪. ফল (mean, sd, একই ২০টি ভাগে জোড়া পার্থক্য)
- পূর্ণ 0.7391; profile বাদ Δ +0.0018 (sd 0.0019), খারাপ 5/20
৫. সিদ্ধান্ত
- profile বাদ: ক্ষতির প্রমাণ নেই, ২টি feature কম
৬. সীমা ও যা জানি না
- কাল্পনিক ডেটা, ১,০০০ সারি; fold স্বাধীন নয়, interval আনুমানিক
- বাছাই CV দেখে; test একবার খোলা হবে চূড়ান্ত model-এ
৭. পুনরুৎপাদন: uv sync --locked; python train.py configs/logreg_noprofile.jsonআরও গভীরে: ML-এ পুনরুৎপাদন নিয়ে গবেষণা-সমাজের কয়েকটি পরিচিত কাজ: Gundersen ও Kjensmo (AAAI ২০১৮) প্রকাশিত AI গবেষণাপত্রগুলোর একটি নমুনায় দেখান যে পুনরুৎপাদনের দরকারি তথ্য (code, ডেটা, পদ্ধতির বিবরণ) প্রায়ই অনুপস্থিত; NeurIPS ২০১৯ থেকে একটি reproducibility checklist চালু হয়, যার অভিজ্ঞতা Pineau ও সহকর্মীরা JMLR-এ (২০২১) লিখেছেন; Bouthillier ও সহকর্মীরা (MLSys ২০২১) benchmark-এর variance-এর উৎস আলাদা করে দেখান যে ডেটা-ভাগের এলোমেলোতা প্রায়ই বড় উৎস, আর তুলনায় একাধিক ভাগ নেওয়ার পরামর্শ দেন। আর Dietterich (১৯৯৮)-এর 5×2cv test দুই algorithm তুলনার একটি পুরোনো কিন্তু এখনো উদ্ধৃত পদ্ধতি।
১০ · AI-এর যুগে — AI assistant-এর experiment code আর তার “উন্নতি” যাচাই
AI assistant দিয়ে training script, MLflow-এর code বা tuning loop লেখানো এখন সাধারণ। সময় বাঁচে, কিন্তু experiment-এর ক্ষেত্রে AI-এর ভুলগুলো চুপচাপ থাকে: code চলে, সংখ্যা আসে, আর সংখ্যাটি ভুল জিনিস মাপে। নিচের টেবিলে প্রায়ই দেখা কয়েকটি ধরন, আর কীভাবে ধরবেন:
| AI প্রায়ই যা করে | কীভাবে ধরবেন |
|---|---|
পুরোনো API: file:./mlruns, artifact_path=, বা বাদ পড়া scikit-learn parameter | আপনার version-এর অফিসিয়াল docs পড়া, আর চালিয়ে warning-সহ দেখা |
| একটি seed, একটি split-এর সংখ্যাকে “উন্নতি” বলা | একাধিক seed বা fold-এ mean ± sd, আর একই ভাগে জোড়া পার্থক্য |
| Test set দেখে tuning, বা preprocessing পুরো ডেটায় fit | Split আগে, test তালাবন্ধ; preprocessing pipeline-এর ভেতরে |
| Seed বসায় এক জায়গায়, ভুলে যায় আরেক জায়গায় (model, fold, ডেটা) | একই config দুবার চালিয়ে হুবহু মেলে কি না দেখা |
| অস্তিত্বহীন ফাংশন বা hyperparameter বানিয়ে দেয় | নাম docs-এ খোঁজা; get_params() দিয়ে আসল নাম দেখা |
| Fold-এর score থেকে সরু t-interval, “significant” দাবি | Fold স্বাধীন নয় মনে রাখা; সংশোধন বা পর্ব ২২-এর পদ্ধতি |
AI-কে experiment-এর code লেখাতে চাইলে পাঁচটি ইনপুট আর রিপোর্টের নিয়ম শুরুতেই বলে দিন। নিচের prompt-টি নিজের প্রকল্প অনুযায়ী বদলে নিন; ব্যক্তিগত বা গ্রাহকের ডেটা কখনো পেস্ট করবেন না, শুধু কলামের নাম আর ধরন দিন।
AI-এর লেখা experiment code বা ফল হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে:
- Seed বেঁধে দিলেই কি ফল নির্ভরযোগ্য? না; seed ফলকে স্থির করে, সঠিক নয়। একাধিক seed-এর mean ± sd লাগে।
- sd আর SE-র পার্থক্য? sd একটি run-এর ছড়ানো; SE = sd/√n, গড়ের অনিশ্চয়তা, n বাড়লে কমে।
- k-fold score থেকে t-interval কেন বেশি সরু? Fold-গুলোর training ডেটা বড় অংশে এক, তাই score পরস্পর-সম্পর্কিত; Nadeau–Bengio সংশোধন (1/k + ntest/ntrain).
- Ablation study কী? একটি অংশ সরিয়ে একই ভাগে আবার মাপা, অংশটির অবদান বুঝতে।
- ৫০টি config চেষ্টা করে সেরাটির validation score রিপোর্ট করলে সমস্যা কী? Selection bias; সংখ্যা আশাবাদী। অদেখা test বা nested CV লাগে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১একই algorithm, শুধু seed আলাদা — তাতেই কি গবেষণার সিদ্ধান্ত উল্টে যেতে পারে?
পারে, আর এর সুপরিচিত প্রমাণ আছে। Henderson ও সহকর্মীরা (AAAI ২০১৮, “Deep Reinforcement Learning that Matters”) একই algorithm একই সেটিংয়ে কয়েকটি seed-এ চালিয়ে seed-গুলোকে দুটি দলে ভাগ করেন; দুই দলের গড় learning curve এতটাই আলাদা হয় যে কেউ চাইলে দেখাতে পারতেন “দুটি আলাদা algorithm, একটি স্পষ্ট ভালো”। Reinforcement learning-এ ওঠানামা বিশেষভাবে বড়, কিন্তু ব্যাপারটি শুধু সেখানে নয়। Picard (২০২১, arXiv)-এর “torch.manual_seed(3407) is all you need” শিরোনামের একটি কাজ computer vision-এ বহু seed চালিয়ে দেখায় যে শুধু seed বেছে নিয়েই নির্ভুলতায় এমন পার্থক্য আনা যায় যা অনেক প্রকাশিত “উন্নতির” কাছাকাছি; লেখক নিজেই কাজটির সীমা স্বীকার করেছেন, তাই সংখ্যাগুলো সাধারণ নিয়ম হিসেবে নেওয়া ঠিক নয়, কিন্তু বার্তাটি স্পষ্ট।
এই পর্বের ছোট উদাহরণেও একই জিনিস: শুধু split-এর seed বদলালে AUC ০.৭০৫ থেকে ০.৭৮৪, আর tree-এর নিজের seed বদলালে ০.৬০৭ থেকে ০.৬৯৩. দুটি model-এর আসল পার্থক্য যদি ০.০১ হয়, একটি seed-এর তুলনায় যেকোনো একটি জিততে পারে।
তাহলে করণীয় কী? প্রথমত, একাধিক seed-এ চালানো আর গড়ের সঙ্গে বিস্তার দেখানো। দ্বিতীয়ত, তুলনায় একই seed-গুলো দুই পক্ষে ব্যবহার করা (জোড়া পরিকল্পনা), যাতে ভাগের ভাগ্য দুজনের ওপর একইভাবে পড়ে। তৃতীয়ত, seed-এর সংখ্যা আগে ঠিক করা; ফল দেখে “আরও কয়েকটা seed চালাই” বলা নিজেই forking path. চতুর্থত, রিপোর্টে লেখা কোন উৎসের ওঠানামা মাপা হলো: model-এর seed, ডেটা-ভাগ, না দুটোই।
মূল উপলব্ধি: একটি seed-এর একটি সংখ্যা একটি নমুনা, model-এর গুণ নয়; সিদ্ধান্ত নেওয়া উচিত বিস্তার দেখে, আর তুলনা হওয়া উচিত একই ভাগে, জোড়ায়।
প্র ০২k-fold-এর পাঁচটি score থেকে t-interval বানানো কেন বিপজ্জনক, আর বিকল্প কী?
t-interval-এর ভিত্তি হলো স্বাধীন পর্যবেক্ষণ: পাঁচটি সংখ্যা পাঁচটি আলাদা, সম্পর্কহীন পরীক্ষা থেকে এসেছে। k-fold-এ test অংশগুলো আলাদা হলেও training অংশগুলো বড় পরিমাণে এক: ৫-fold-এ যেকোনো দুই fold-এর training ডেটার প্রায় ৭৫% সারি একই। ফলে model-গুলো প্রায় একই, আর তাদের score একসঙ্গে ওঠে-নামে। পারস্পরিক সম্পর্কযুক্ত সংখ্যাকে স্বাধীন ধরে sd/√k নিলে অনিশ্চয়তা কম দেখায়, interval বেশি সরু হয়, আর “significant” পার্থক্য বেশি ঘন ঘন পাওয়া যায় যা আসলে নেই।
এর তাত্ত্বিক দিকটি Bengio ও Grandvalet (JMLR ২০০৪) স্পষ্ট করেন: k-fold cross-validation-এর score থেকে তার variance-এর কোনো নিরপেক্ষ অনুমানক সাধারণভাবে নেই, কারণ পারস্পরিক সম্পর্কের অংশটি ডেটা থেকে আলাদা করে মাপা যায় না। আগের বছর Nadeau ও Bengio (Machine Learning, ২০০৩) একটি ব্যবহারিক সংশোধন প্রস্তাব করেন: variance-এর গুণক 1/k-এর বদলে (1/k + ntest/ntrain); পরে Bouckaert ও Frank (২০০৪) repeated cross-validation-এর সঙ্গে এর ব্যবহার নিয়ে লেখেন। আরও আগে Dietterich (Neural Computation, ১৯৯৮) দেখান যে সাধারণ resampled paired t-test বেশি ঘন ঘন মিথ্যা পার্থক্য খুঁজে পায়, আর 5×2cv test প্রস্তাব করেন।
ব্যবহারিক বিকল্প কয়েকটি: (১) Repeated k-fold-এ জোড়া পার্থক্য নিয়ে সংশোধিত interval; (২) একটি বড় তালাবন্ধ test set-এ দুই model-এর অনুমানের ওপর paired bootstrap বা McNemar test; (৩) সংখ্যা ছাড়াও “২০টি ভাগের কয়টিতে A জিতেছে” ধরনের বর্ণনা, যা পাঠক সহজে বোঝেন। এগুলোর পরিসংখ্যানিক ভিত্তি পর্ব ২২-এ আসবে। কোনো পদ্ধতিই নিখুঁত নয়; তাই সবচেয়ে ভালো অভ্যাস হলো interval-কে সূক্ষ্ম রেখা না ভেবে মোটামুটি পরিসর হিসেবে পড়া।
মূল উপলব্ধি: Fold-এর score একে অপরের আত্মীয়; তাদের স্বাধীন ধরে নিলে আত্মবিশ্বাস বাড়ে, সত্য নয়।
প্র ০৩Public leaderboard আর বারবার ব্যবহৃত benchmark-ও কি একধরনের “forking paths”?
হ্যাঁ, পুরো গবেষণা-সমাজের স্তরে। একটি প্রতিযোগিতায় হাজারো দল একই public leaderboard-এ বারবার জমা দেয়, আর প্রতিটি জমার ফল দেখে পরের চেষ্টা ঠিক করে। ফলে leaderboard-টি আর অদেখা test থাকে না, একটি validation set হয়ে যায়, আর অনেক দলের score সেখানে ফোলানো হয়। প্রতিযোগিতা শেষে গোপন অংশে (private leaderboard) র্যাংক উল্টে যাওয়ার ঘটনা এই কারণেই পরিচিত। Blum ও Hardt (ICML ২০১৫, “The Ladder”) এই সমস্যার একটি তাত্ত্বিক বিশ্লেষণ আর প্রতিকার দেন: leaderboard শুধু তখনই নতুন score দেখাবে যখন উন্নতি একটি সীমার চেয়ে বড়।
বড় benchmark-এও প্রশ্নটি উঠেছে। Recht, Roelofs, Schmidt ও Shankar (ICML ২০১৯, “Do ImageNet Classifiers Generalize to ImageNet?”) মূল পদ্ধতি অনুসরণ করে ImageNet আর CIFAR-10-এর জন্য নতুন test set বানান। প্রায় সব model-এর নির্ভুলতা নতুন test-এ বেশ কমে যায় (ImageNet-এ মোটামুটি ১১–১৪ শতাংশ বিন্দু), কিন্তু model-গুলোর আপেক্ষিক ক্রম মোটামুটি একই থাকে। লেখকদের ব্যাখ্যা ছিল, পতনের বড় অংশ সম্ভবত নতুন ডেটা সংগ্রহের সূক্ষ্ম পার্থক্য (distribution shift) থেকে, শুধু বারবার একই test ব্যবহার থেকে নয়। অর্থাৎ ফল দুই দিকেই শিক্ষা দেয়: benchmark-এ “অতিরিক্ত মানিয়ে নেওয়া” প্রত্যাশার চেয়ে কম ক্ষতিকর হতে পারে, আর ছোট ডেটা-পরিবর্তনও বড় পতন আনতে পারে।
একক প্রকল্পে শিক্ষাটি সরল। আপনার validation set যত বেশি বার দেখবেন, সেটি তত বেশি train set-এর মতো আচরণ করবে। তাই কয়েক মাসের প্রকল্পে মাঝে মাঝে নতুন ডেটা দিয়ে validation নবায়ন করা, test একবার খোলা, আর চেষ্টার সংখ্যা খাতায় রাখা জরুরি।
মূল উপলব্ধি: যে ডেটা দেখে সিদ্ধান্ত নেওয়া হয়, তা আর নিরপেক্ষ বিচারক থাকে না, সিদ্ধান্তটি একজন মানুষ নিন বা হাজারো দল মিলে।
প্র ০৪Ablation না থাকলে একটি নতুন পদ্ধতির দাবি দুর্বল কেন? উল্টো দিকে, ablation কখন বিভ্রান্ত করে?
একটি নতুন পদ্ধতি সাধারণত কয়েকটি বদল একসঙ্গে আনে: নতুন architecture, নতুন preprocessing, বেশি tuning, বড় training বাজেট। ফল ভালো হলে কোন বদলটি কাজ করল, তা ablation ছাড়া বলা যায় না। Lipton ও Steinhardt (২০১৮, “Troubling Trends in Machine Learning Scholarship”) ML গবেষণার কয়েকটি প্রবণতার সমালোচনা করেন, যার একটি হলো উন্নতির উৎস আলাদা না করা: হয়তো উন্নতির বড় অংশ এসেছে hyperparameter tuning থেকে, অথচ কৃতিত্ব দেওয়া হয়েছে নতুন architecture-কে। Melis, Dyer ও Blunsom (ICLR ২০১৮) language model-এ দেখান যে ভালোভাবে tune করা পুরোনো LSTM তখনকার কয়েকটি নতুন, জটিল architecture-এর সমান বা ভালো করেছে; অর্থাৎ baseline-কেও সমান যত্ন না দিলে তুলনা অন্যায্য।
কিন্তু ablation-এরও ফাঁদ আছে। প্রথমত, বদলি অংশ: দুটি feature-দল একই তথ্য বহন করলে একটিকে সরালে কিছুই হয় না, তাই মনে হয় দুটিই অপ্রয়োজনীয়, অথচ দুটি একসঙ্গে সরালে বড় ক্ষতি। দ্বিতীয়ত, মিথস্ক্রিয়া: একটি অংশ শুধু আরেকটির উপস্থিতিতে কাজ করে। তৃতীয়ত, retraining-এর এলোমেলোতা: প্রতিটি সংস্করণ নতুন করে train হয়, তাই ছোট Δ আসলে seed-এর noise হতে পারে; এজন্যই এই পর্বে একই ২০টি ভাগে জোড়া পার্থক্য আর “কয়টি ভাগে খারাপ” দেখা হয়েছে। চতুর্থত, ablation কারণ প্রমাণ করে না: profile বাদ দিলে ক্ষতি নেই মানে এই model-এ এই ডেটায় profile কাজে লাগছে না, এমন নয় যে ঢাকার গ্রাহকদের আচরণ আলাদা নয়।
ভালো অভ্যাস: অংশগুলো দল ধরে সরানো, সন্দেহজনক জোড়া একসঙ্গে সরিয়ে দেখা, প্রতিটি সংস্করণে একই ভাগ ও একই tuning বাজেট দেওয়া, আর ছোট Δ-কে সিদ্ধান্তের আগে বিস্তারের সঙ্গে মেলানো।
মূল উপলব্ধি: Ablation জিজ্ঞেস করে “এটি না থাকলে কী হতো”; প্রশ্নটি সৎ রাখতে প্রতিটি সংস্করণকে সমান সুযোগ আর একই ভাগ দিতে হয়।
প্র ০৫নিজের হাতে লেখা JSONL খাতা, নাকি MLflow-এর মতো হাতিয়ার — কোনটি কখন?
দুটোরই জায়গা আছে, আর বিতর্কটি অনেকটা “notebook বনাম script”-এর মতো: নির্ভর করে প্রকল্পের আকার আর দলের ওপর। হাতের JSONL খাতার সুবিধা: কোনো dependency নেই, একটি টেক্সট ফাইল, Git-এ পার্থক্য দেখা যায়, pandas এক লাইনে পড়ে, আর আপনি জানেন ঠিক কী লেখা হচ্ছে। এই পর্বের ব্লক ১ পুরোটাই ৩৫ লাইনে। অসুবিধা: model ফাইল, chart, প্রতিটি epoch-এর metric রাখা, অনেকজনের একসঙ্গে লেখা, আর তুলনার পাতা সবই নিজেকে বানাতে হয়।
MLflow-এর মতো হাতিয়ার এগুলো দেয়: run-এর id, step ধরে metric, artifact-এর ফোল্ডার, filter-সহ খোঁজ, model-এর সঙ্গে environment-এর তালিকা, আর ওয়েব পাতায় তুলনা। কিন্তু হাতিয়ারের নিজস্ব খরচ আছে, আর এই পর্বের সত্যিকারের run-এই তিনটি দেখা গেল: version বদলালে আচরণ বদলায় (ফাইল-ভিত্তিক store এখন থামিয়ে দেয়), নিরাপত্তার নতুন নিয়ম আসে (skops-এর trusted type), আর model-এর ঠিকানা run-এর artifact তালিকায় না থাকায় নতুনরা বিভ্রান্ত হন। আরও আছে সংরক্ষিত ফাইলে এই কম্পিউটারের path, আর একটি server চালালে তার নিরাপত্তা। Weights & Biases-এর মতো hosted বিকল্পে account আর ডেটা অন্যের server-এ যাওয়ার প্রশ্ন আসে; বাজারঘরের মতো গ্রাহক-ডেটায় এটি একটি গোপনীয়তার সিদ্ধান্ত।
একটি ব্যবহারিক নিয়ম: একা কাজ, কয়েক ডজন run, শুধু সংখ্যা: JSONL যথেষ্ট। দলের কাজ, শত শত run, model ফাইল আর তুলনার পাতা দরকার: MLflow বা সমতুল্য। কিন্তু হাতিয়ার যা-ই হোক, কী লিখতে হবে তা বদলায় না: পাঁচটি ইনপুট, dirty চিহ্ন, সব চেষ্টা, আর ফলের বিস্তার। হাতিয়ার শৃঙ্খলাকে সহজ করে, শৃঙ্খলার বদলি নয়।
মূল উপলব্ধি: খাতার ধরন বদলায়, খাতার বিষয়বস্তু বদলায় না; হাতিয়ার বাছাই করুন দলের আকার, ডেটার গোপনীয়তা আর রক্ষণাবেক্ষণের খরচ দেখে।
অনুশীলন
হিসাব হাতে করুন, তারপর উত্তর মেলান।
-
হাতে interval (numerical): একটি model-এর ৫-fold AUC: ০.৭১, ০.৭৪, ০.৬৯, ০.৭৬, ০.৭৫ (প্রতিটি fold-এ test ২০%). (ক) mean, sd, SE আর ৯৫% t-interval বের করুন (t0.975, 4 = ২.৭৭৬)। (খ) Nadeau–Bengio সংশোধনে interval কত চওড়া হয়?
(ক) যোগ ৩.৬৫, mean = ০.৭৩০. বিচ্যুতি −০.০২, ০.০১, −০.০৪, ০.০৩, ০.০২; বর্গ ০.০০০৪, ০.০০০১, ০.০০১৬, ০.০০০৯, ০.০০০৪; যোগ ০.০০৩৪. sd = √(০.০০৩৪/৪) = √০.০০০৮৫ ≈ ০.০২৯২. SE = ০.০২৯২/√৫ ≈ ০.০১৩০. অর্ধ-প্রস্থ = ২.৭৭৬ × ০.০১৩০ ≈ ০.০৩৬২, তাই interval ≈ [০.৬৯৪, ০.৭৬৬]।
(খ) ntest/ntrain = ২০/৮০ = ০.২৫, গুণক 1/5 + 0.25 = ০.৪৫. সংশোধিত SE = ০.০২৯২ × √০.৪৫ ≈ ০.০১৯৬, অর্ধ-প্রস্থ ≈ ২.৭৭৬ × ০.০১৯৬ ≈ ০.০৫৪৩, interval ≈ [০.৬৭৬, ০.৭৮৪]। প্রায় ১.৫ গুণ চওড়া। পার্থক্যটি মনে রাখার মতো: সাধারণ হিসাব নিজের অনিশ্চয়তাকে প্রায় এক-তৃতীয়াংশ কম দেখায়।
-
আউটপুট অনুমান করুন: নিচের code কী ছাপবে?
import json def log(rec): with open("runs.jsonl", "w") as f: f.write(json.dumps(rec) + "\n") for c in [0.01, 0.1, 1.0]: log({"C": c, "auc": 0.73}) lines = open("runs.jsonl").read().splitlines() print(len(lines), lines[0])1 {"C": 1.0, "auc": 0.73}"w"মোড প্রতিবার ফাইল খালি করে নতুন করে লেখে, তাই তিনটি run-এর মধ্যে শুধু শেষটি টিকে থাকে: একটি লাইন, C = ১.০. খাতার জন্য এটিই সবচেয়ে বিপজ্জনক ভুল, কারণ কোনো error আসে না, শুধু ইতিহাস নিঃশব্দে মুছে যায়। প্রতিকার"a"(append). -
নকশা ও সমালোচনা: একজন সহকর্মী লিখলেন: “নতুন feature যোগ করে churn model-এর AUC ০.৮০৫ থেকে ০.৮১২ হয়েছে (seed 42-এর একটি split). সব মিলিয়ে ৩০টি সেটিং চেষ্টা করেছি, এটিই সেরা। তাই নতুন feature চালু করা হোক।” দাবিটির দুর্বলতা কোথায়, আর সিদ্ধান্তের জন্য কোন experiment চালাবেন?
দুর্বলতা: (১) একটি split, একটি seed: এই পর্বের ডেটায় শুধু split বদলেই AUC প্রায় ০.০৮ পরিসরে নড়েছে, আর ০.০০৭-এর পার্থক্য সহজেই এর ভেতরে। (২) ৩০টি সেটিংয়ের সেরাটি: selection bias, সংখ্যাটি আশাবাদী। (৩) পুরোনো model আর নতুন model একই ভাগে মাপা হয়েছে কি না, বলা নেই। (৪) কোন ডেটা ভাগে বাছাই আর কোন ভাগে চূড়ান্ত মাপ, জানা নেই।
পরিকল্পনা: metric আর তুলনা আগে লিখে commit; একই
RepeatedStratifiedKFold(যেমন ৫ × ৪) ভাগে feature-সহ ও feature-ছাড়া দুই সংস্করণ, একই tuning বাজেটে; জোড়া পার্থক্যের গড়, sd, “কয়টি ভাগে ভালো” আর সংশোধিত interval; সব ৩০টি চেষ্টা খাতায়; সিদ্ধান্ত হলে তালাবন্ধ test-এ একবার। নতুন feature সংগ্রহের খরচ আর গোপনীয়তাও সিদ্ধান্তের অংশ। -
Code লিখুন: ব্লক ১-এর
run_experiment-এর শুরুতে এমন একটি পরীক্ষা যোগ করুন, যা খাতায় একইcfg_id, একইdataআর একইcode-এর run আগে থাকলে নতুন করে না চালিয়ে আগের ফল ফেরত দেয়।একটি সমাধান। ডেস্কটপে ব্লক ১-এর code-এ বসিয়ে সত্যিকারের run-এ যাচাই করা হয়েছে: চারটি config-এর মধ্যে শেষটি পুনরাবৃত্তি বলে training চলেছে তিনবার, আর খাতায় তিনটি লাইন।
import os def previous_run(cfg_id): if not os.path.exists("runs.jsonl"): return None for line in open("runs.jsonl", encoding="utf-8"): r = json.loads(line) if (r["cfg_id"], r["data"], r["code"]) == (cfg_id, data_hash, CODE_VERSION): return r return None # run_experiment-এ cfg_id হিসাবের পরেই: # old = previous_run(cfg_id) # if old: return oldতিনটি মিলতে হবে, শুধু config নয়: ডেটা বা code বদলালে একই config-এর ফলও বদলাতে পারে। এতে cfg_id হিসাবটি cross-validation-এর আগে আনতে হবে, যাতে পুরোনো ফল পেলে training-ই না চলে। সতর্কতা: library-র version বদলালেও ফল বদলাতে পারে, তাই বড় প্রকল্পে key-তে
sklearnversion-ও রাখা ভালো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি সৎ ML experiment-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রশ্ন, metric আর ভাগের নিয়ম লিখে commit; test set তালাবন্ধ
- Config ফাইল আর baseline; পরিষ্কার working tree
- একাধিক seed বা fold-এ run, প্রতিটি খাতায়
- একই ভাগে জোড়া তুলনা আর ablation
- বিজয়ী বাছাই, তারপর test set-এ একবার
- সব চেষ্টা আর সীমাসহ report
একটি ML run হুবহু আবার চালাতে নিচের কোনটি সবচেয়ে কম জরুরি?
- Git commit-এর hash আর dirty চিহ্ন
- ডেটার SHA-256 ছাপ
- সব জায়গার random seed
- Run চালানোর দিন আর সময়
দুটি model একই ৫টি fold-এ মাপা হয়েছে: A-র AUC 0.738 ± 0.026, B-র 0.736 ± 0.026 (± = sd). fold-ভিত্তিক পার্থক্য A − B: 0.007, 0.003, −0.001, 0.004, 0.000. কোন বক্তব্যটি সবচেয়ে যুক্তিসংগত?
- A স্পষ্টভাবে ভালো, কারণ তার গড় বেশি
- একই fold-এ জোড়া পার্থক্য ছোট আর শূন্যের দুই দিকে; পার্থক্যের প্রমাণ নেই
- দুটির sd সমান, তাই model দুটি হুবহু একই
- একই fold-এ মাপায় তুলনাটি অবৈধ
k-fold cross-validation-এর k টি score থেকে সাধারণ t-interval বানালে interval সাধারণত বেশি সরু হয় কেন?
- k ছোট বলে t-এর মান খুব ছোট হয়
- প্রতিটি fold-এ একই test ডেটা থাকে
- Fold-গুলোর training ডেটা বড় অংশে এক, তাই score স্বাধীন নয়
- Stratified ভাগে শ্রেণির অনুপাত সমান থাকে
একই ২০টি cross-validation ভাগে একটি feature-দল সরালে গড় AUC-র পরিবর্তন +0.0018 (sd 0.0019), আর ২০টির মাত্র ৫টি ভাগে AUC কমেছে। যুক্তিসংগত সিদ্ধান্ত কোনটি?
- দলটি বাদ দেওয়া যায়, আর চূড়ান্ত model অদেখা test set-এ একবার যাচাই
- দলটি বাদ দিলে model অনেক ভালো হয়, তাই এটি বড় আবিষ্কার
- দলটি অবশ্যই রাখতে হবে, কারণ ৫টি ভাগে AUC কমেছে
- দলটি churn-এর কারণ নয়, এটি প্রমাণিত হলো
শুধু noise-এর ডেটায় ২০০টি বিশ্লেষণ-পথ ৬০ সারির validation-এ চেষ্টা করে সেরা পথ accuracy 0.667 পেল। ৫,০০০টি নতুন সারিতে সেই পথের accuracy কত হবে বলে আশা করা উচিত?
- প্রায় 0.667, কারণ validation-এ সেটিই এসেছে
- প্রায় 0.5
- 0.7-এর বেশি, কারণ নতুন ডেটা বেশি
- প্রায় 0.3
MLflow-এ “metric” আর “artifact”-এর পার্থক্য কোনটি?
- Metric run-এর অংশ, artifact experiment-এর বাইরে রাখা হয়
- Metric একবারই লেখা যায়, artifact বারবার
- Metric হলো সংখ্যা (step ধরে লেখা যায়), artifact হলো ফাইল (config, chart, model)
- Artifact শুধু model ফাইল, অন্য কিছু নয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ফল = f(code, environment, ডেটা, config, seed); পাঁচটিই, সঙ্গে dirty চিহ্ন, খাতায় লিখুন।
- Config থাকে code-এর বাইরে; খাতা শুধু শেষে যোগ হয় (
"a"), আর ব্যর্থ run-ও থাকে। - Seed ফলকে স্থির করে, সঠিক নয়; রিপোর্ট করুন mean ± sd, আর লিখুন ± মানে sd না SE.
- তুলনা আর ablation একই ভাগে, জোড়ায়; fold-এর score স্বাধীন নয়, তাই interval আনুমানিক।
- অনেক পথ চেষ্টা করলে noise-ও জেতে: চেষ্টার সংখ্যা লিখুন, আর বিজয়ীর সংখ্যা নিন অদেখা ডেটা থেকে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৫ · Label আর ডেটার মান — annotation, noise, agreement আর data versioningপরবর্তী পর্বLabel কোথা থেকে আসে, কতটা বিশ্বাসযোগ্য, আর ডেটার version.
- মেশিন লার্নিং · কোর্স হোম৭৭টি পর্বপুরো কোর্সের মানচিত্র — ১৪টি মডিউল।
- AI-এর ভিত্তি · পর্ব ১২ · পুনরুৎপাদনযোগ্য প্রকল্পভিত্তিফোল্ডার, uv.lock, ডেটার hash আর clone থেকে একই metrics.
- AI-এর ভিত্তি · পর্ব ৬৬ · Model বাছাইভিত্তিCross-validation, grid আর random search, nested CV.
- Software Engineering & Git · Commit আর releaseপাশের ট্র্যাকভালো commit আর version-এর নিয়ম, যার ওপর code version দাঁড়ায়।