সরাসরি পর্বে যান
পর্ব ৩৮ · ৭৭-এর মধ্যে · মডিউল ৭

Tabular ডেটায় model বাছাই — tree, linear না neural, আর tuning-এর বাজেট

Choosing a Model for Tabular Data — Baselines, Per-Family Preprocessing, Fair Comparison, Tuning Budgets, and Trees vs Linear Models vs Neural Networks

Taught in Bangla, this lesson sets up dummy and linear baselines, per-family preprocessing with ColumnTransformer, and a fair comparison on the same repeated folds with a corrected t-test. Real runs show rankings change with data size and tuning budget, expected best-of-k search is computed exactly, and small data and extrapolation cases show where linear models beat trees.

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

  • যেকোনো tabular সমস্যায় প্রথমে dummy আর linear baseline দাঁড় করানো, আর কেন
  • ColumnTransformer দিয়ে প্রতিটি model পরিবারের জন্য আলাদা, কিন্তু leakage-মুক্ত প্রস্তুতি
  • একই repeated stratified fold-এ পাঁচ-ছয়টি পরিবারের সৎ তুলনা, mean ± sd পড়া আর corrected t-test
  • RandomizedSearchCV-এর বাজেট বাড়ালে ফল আর ক্রম কীভাবে বদলায়, হাতে হিসাব করে
  • কখন linear, কখন forest, কখন boosting, আর neural network নিয়ে প্রকাশিত তুলনা কী বলে; শেষে একটি সিদ্ধান্তের চেকলিস্ট

মডিউল ৭-এর আগের ছয়টি পর্বে গাছ আর ensemble-এর ভেতরটা দেখা হয়েছে: পর্ব ৩২ (Decision tree গভীরে — CART, impurity, pruning আর missing value), পর্ব ৩৩ (Bagging আর random forest — variance কমানো আর OOB error), পর্ব ৩৪ (Boosting-এর ধারণা — AdaBoost আর weak learner), পর্ব ৩৫ (Gradient boosting — residual থেকে functional gradient descent), পর্ব ৩৬ (XGBoost, LightGBM আর CatBoost — পার্থক্য আর হাতে-কলমে) আর পর্ব ৩৭ (Stacking, blending আর voting — ensemble কখন সত্যিই সাহায্য করে)। এই পর্ব মডিউলের শেষ, আর প্রশ্নটি ব্যবহারিক: একটি নতুন tabular সমস্যা হাতে এলে কোন model দিয়ে শুরু করবেন, কীভাবে সৎভাবে তুলনা করবেন, আর tuning-এ কতটা খরচ করবেন? এখানে tabular ডেটা মানে সারি আর কলামের টেবিল, যেখানে প্রতিটি কলামের নিজস্ব অর্থ আর একক: বয়স, টাকা, জেলা, পেমেন্টের ধরন। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, scikit-learn 1.8.0); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে, আর তুলনার ব্লক দুটি কয়েক সেকেন্ড নেয়। বড় ডেটা আর বড় বাজেটের মাপ এসেছে ডেস্কটপের সত্যিকারের run থেকে।

১ · আগে baseline — dummy আর একটি সরল linear model

ডাক্তার জ্বরের রোগী দেখে প্রথমেই MRI লেখেন না। আগে থার্মোমিটার, নাড়ি আর দুটি সাধারণ প্রশ্ন। সাধারণ পরীক্ষায় রোগ ধরা পড়লে দামি পরীক্ষা লাগে না; না পড়লে দামি পরীক্ষার ফলকে মাপার একটি মানদণ্ড অন্তত হাতে থাকে। Model বাছাইয়েও একই নিয়ম: আগে সস্তা, বোঝা যায় এমন baseline, তারপর জটিল model, আর জটিলটিকে প্রমাণ করতে হবে যে সে baseline-এর চেয়ে সত্যিই ভালো।

দুটি baseline প্রায় সব সময় লাগে। এক, dummy: যে model ইনপুট দেখেই না। DummyClassifier(strategy="prior") সবাইকে একই সম্ভাবনা দেয় (churn-এর হার), তাই তার ROC AUC ঠিক ০.৫. কোনো model এর কাছে থাকলে বুঝবেন pipeline-এ গোলমাল, বা ডেটায় সংকেতই নেই। দুই, ভালো feature-সহ একটি regularized linear model: classification-এ logistic regression, regression-এ ridge. AI-এর ভিত্তি কোর্সের পর্ব ৫৯-এ logistic regression আর পর্ব ৬০-এ ROC AUC দেখেছেন; এই কোর্সের পর্ব ০২-এ baseline-কে সমস্যা গঠনের অংশ হিসেবে দেখেছেন। Linear baseline কয়েক মিলিসেকেন্ডে শেখে, model ফাইল কয়েক KB, আর প্রতিটি ওজন পড়া যায়।

Baseline-এর আরেকটি কাজ আছে: এটি ডেটার সমস্যা ধরে। Logistic regression যদি হঠাৎ AUC ০.৯৯ দেয়, প্রায়ই কোনো কলামে উত্তর লুকিয়ে আছে (leakage, যেমন “বাতিলের তারিখ” কলাম)। আর জটিল model যদি baseline-এর চেয়ে সামান্যই ভালো হয়, তবে প্রশ্ন ওঠে: সেই সামান্য উন্নতি তার বাড়তি খরচ, ধীরগতি আর ব্যাখ্যার কষ্টের দাম দেয় কি না।

২ · বাজারঘরের churn ডেটা আর প্রতিটি পরিবারের নিজস্ব প্রস্তুতি

এই পর্বের ডেটা: বাজারঘরের কাল্পনিক গ্রাহক, seed দিয়ে বানানো। ছয়টি সংখ্যার কলাম (কত মাস ধরে গ্রাহক, শেষ ৯০ দিনে অর্ডার, গড় ঝুড়ির টাকা, শেষ অর্ডারের পর কত দিন, দেরিতে পৌঁছানোর হার, অ্যাপের রেটিং) আর তিনটি category (জেলা, পেমেন্ট, courier). লক্ষ্য: গ্রাহক চলে যাবে (churn) কি না। নিয়মটি ইচ্ছা করে বাস্তবের মতো এলোমেলো: কিছু ধাপ (৪৫ দিনের বেশি অনুপস্থিত), কিছু interaction (cash on delivery আর বেশি দেরি একসঙ্গে, courier B আর দেরি একসঙ্গে), আর ফাঁকা মান যা নিজেই তথ্য বহন করে: churn করা গ্রাহক রেটিং কম দেন।

# bazar_churn.py — বাজারঘরের কাল্পনিক গ্রাহক churn (seed দিয়ে বানানো)
import numpy as np
import pandas as pd

DISTRICTS = ["Dhaka", "Ctg", "Sylhet", "Khulna", "Rajshahi", "Barishal", "Rangpur", "Mymensingh"]

def make_churn(n, seed=38):
    r = np.random.default_rng(seed)
    tenure = r.integers(1, 61, n)                                  # মাস
    orders = r.poisson(np.clip(6 - tenure / 15, 1, None) * r.gamma(2, 0.6, n))
    basket = np.round(r.lognormal(6.8, 0.5, n), -1)                # টাকা
    days = np.minimum(r.exponential(35, n), 180).round()           # শেষ অর্ডারের পর দিন
    late = r.beta(2, 8, n).round(2)                                # দেরিতে পৌঁছানোর হার
    rating = np.clip(r.normal(3.9, 0.8, n) - 2 * late, 1, 5).round(1)
    district = r.choice(DISTRICTS, n, p=[.34, .16, .1, .1, .1, .07, .07, .06])
    payment = r.choice(["cod", "bkash", "card"], n, p=[.55, .35, .1])
    courier = r.choice(["A", "B", "C"], n)
    z = (-2.0 + 1.8 * (days > 45) - 0.4 * np.log1p(orders) + 1.0 * (tenure < 6)
         + 1.6 * (late > 0.25) * (payment == "cod") + 1.5 * (courier == "B") * (late > 0.3)
         + 0.7 * np.isin(district, ["Rangpur", "Barishal"]) + 0.8 * (rating < 3)
         + 0.8 * (np.abs(np.log(basket) - 6.8) > 0.6))
    y = (r.random(n) < 1 / (1 + np.exp(-z))).astype(int)
    rating[r.random(n) < 0.08 + 0.15 * y] = np.nan                 # রেটিং প্রায়ই ফাঁকা
    days[r.random(n) < 0.05] = np.nan
    X = pd.DataFrame({"tenure_m": tenure, "orders_90d": orders, "avg_basket": basket,
                      "days_since_last": days, "late_rate": late, "rating": rating,
                      "district": district, "payment": payment, "courier": courier})
    return X, y

এখন মূল কথা। একই টেবিল সব model একইভাবে খেতে পারে না। কারণ প্রতিটি পরিবার ডেটাকে আলাদা চোখে দেখে:

  • Linear model, kNN, SVM, MLP: গুণ, যোগ আর দূরত্ব দিয়ে কাজ করে। ফাঁকা মান (NaN) নিতে পারে না, তাই imputation লাগে; সঙ্গে একটি missing indicator কলাম, যাতে “রেটিং দেননি” তথ্যটি হারায় না। এককের পার্থক্য (টাকা বনাম হার) দূরত্ব আর gradient-কে বাঁকায়, তাই scaling. আর জেলাকে ০ থেকে ৭ সংখ্যা বানালে linear model ভাববে রংপুর (৬) বরিশালের (০) চেয়ে “ছয় ধাপ বেশি”, তাই one-hot.
  • Random forest: গাছ শুধু “x ≤ সীমা?” প্রশ্ন করে। একক বা scaling-এ split বদলায় না, কারণ ক্রম একই থাকে। জেলার জন্য ordinal code যথেষ্ট: মিথ্যা ক্রম থাকলেও গাছ কয়েকটি কাটায় দলগুলো আলাদা করে নেয়। scikit-learn 1.8-এর গাছ আর forest NaN নিজেই নেয়, প্রতিটি split-এ শেখে ফাঁকা মানের সারি কোন দিকে যাবে।
  • HistGradientBoosting: কোনো প্রস্তুতি লাগে না। কলামের dtype pandas-এর category হলে categorical_features="from_dtype" (1.6 থেকে এটিই ডিফল্ট) তাকে native category হিসেবে ধরে, আর একটি split-এ জেলাগুলোকে যেকোনো দুই দলে ভাগ করতে পারে। ফাঁকা মানও নিজেই সামলায়।

এই সব প্রস্তুতি pipeline-এর ভেতরে থাকে (AI-এর ভিত্তি কোর্সের পর্ব ৬৪), যাতে median, গড় আর category-র তালিকা প্রতিটি fold-এর শুধু train অংশ থেকে শেখে। ColumnTransformer কলামের দল আলাদা করে আলাদা transformer-এ পাঠায় (AI-এর ভিত্তি কোর্সের পর্ব ৬৩-এ one-hot আর ordinal-এর প্রথম পরিচয়)। প্রথম ব্লক ডেটা বানায়, দুই ধরনের প্রস্তুতি সাজায়, আর ছয়টি model-এর একটি তালিকা তৈরি করে। পরের দুটি ব্লক এই ব্লকের ফল ব্যবহার করে, তাই তিনটি একই namespace-এ চলে।

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler
from bazar_churn import make_churn
X, y = make_churn(1000); cats = ["district", "payment", "courier"]
X[cats] = X[cats].astype("category")       # HGB এই dtype দেখেই category চেনে
print(X.head(3).to_string())
print("missing %:", X.isna().mean().mul(100).round(1)[lambda s: s > 0].to_dict(),
      f"| churn rate {y.mean():.3f}")
num = make_column_selector(dtype_include="number")
scaled = ColumnTransformer([                # linear, kNN, MLP: ভরাট + scale + one-hot
    ("num", make_pipeline(SimpleImputer(strategy="median", add_indicator=True),
                          StandardScaler()), num),
    ("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), cats)])
ordinal = ColumnTransformer([               # forest: NaN যেমন আছে, category → সংখ্যা
    ("num", "passthrough", num),
    ("cat", OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1), cats)])
models = {"dummy": DummyClassifier(strategy="prior"),
          "logistic": make_pipeline(scaled, LogisticRegression(max_iter=1000)),
          "kNN": make_pipeline(scaled, KNeighborsClassifier(25, algorithm="kd_tree")),
          "forest": make_pipeline(ordinal, RandomForestClassifier(
              40, min_samples_leaf=3, random_state=0)),
          "hist GBM": HistGradientBoostingClassifier(max_iter=50, random_state=0),
          "MLP": make_pipeline(scaled, MLPClassifier(
              (16,), alpha=1.0, learning_rate_init=0.03, max_iter=500, random_state=0))}
for name, t in [("scaled + one-hot", scaled), ("ordinal codes", ordinal)]:
    print(f"{name:16s} -> {t.fit_transform(X).shape[1]} columns")

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

   tenure_m  orders_90d  avg_basket  days_since_last  late_rate  rating district payment courier
0        15           6      1920.0             10.0       0.13     3.6   Khulna     cod       A
1        30           1      2270.0              8.0       0.43     5.0    Dhaka     cod       C
2        20           3       920.0             93.0       0.12     3.2      Ctg     cod       B
missing %: {'days_since_last': 4.8, 'rating': 12.7} | churn rate 0.268
scaled + one-hot -> 22 columns
ordinal codes    -> 9 columns

১,০০০ গ্রাহকের ২৬.৮% churn করেছে। রেটিং ১২.৭% সারিতে ফাঁকা, শেষ অর্ডারের দিন ৪.৮%-এ। Linear পরিবারের প্রস্তুতি ২২টি কলাম বানায়: ছয়টি scale করা সংখ্যা, দুটি missing indicator, আর জেলার ৮ + পেমেন্টের ৩ + courier-এর ৩ = ১৪টি one-hot কলাম। Forest-এর প্রস্তুতি ৯টি কলামেই থাকে: সংখ্যাগুলো হুবহু (NaN-সহ) আর তিনটি category-র সংখ্যা-কোড। Boosting-এর জন্য কোনো প্রস্তুতি নেই: সে সরাসরি X খায়। kNN-এ algorithm="kd_tree" ইচ্ছা করে লেখা: ব্রাউজারের Python-এ ডিফল্ট পথ একটি অপ্রাসঙ্গিক সতর্কবার্তা ছাপে, ফল একই।

একটি সারি ধরে দেখা যাক, প্রতিটি পরিবার তাকে কীভাবে দেখে। সারি ২০: ৪২ মাসের গ্রাহক, শেষ ৯০ দিনে কোনো অর্ডার নেই, শেষ অর্ডারের দিন ফাঁকা, বরিশাল, card, courier C; এই গ্রাহক churn করেছেন। সংখ্যাগুলো ডেস্কটপে একই কোড চালিয়ে বের করা (পুরো ১,০০০ সারিতে fit):

সারি ২০, churn = 1সংখ্যা: ১,০০০ সারিতে fit

            
  1. আসল সারি। নয়টি কলাম, একটি ফাঁকা, তিনটি লেখা। কোনো model এটিকে হুবহু খেতে পারে কি না, তা model-এর ওপর নির্ভর করে।
  2. ফাঁকা ভরাট। Linear পরিবারের জন্য NaN-এর জায়গায় train-এর median ২৪ দিন। সঙ্গে একটি ০/১ কলাম, যাতে “তথ্য নেই” নিজেই একটি সংকেত হিসেবে থাকে।
  3. Scaling. ৪২ মাস হলো গড়ের ০.৭৩৯ sd ওপরে; ভরাট করা ২৪ দিন গড়ের ০.৩১৭ sd নিচে। Indicator-ও scale হয়: ফাঁকা কম বলে ১ মানটি ৪.৪৫৩-এ যায়, যা সমস্যা নয়, শুধু ওজনের মাপ বদলায়।
  4. One-hot. প্রতিটি জেলা আলাদা কলাম, তাই প্রতিটি জেলা নিজের ওজন পায়, কোনো মিথ্যা ক্রম নেই। মোট ২২টি কলাম, প্রথম ব্লকের আউটপুটের সঙ্গে মেলে।
  5. Forest-এর রূপ। জেলা বর্ণানুক্রমে কোড পায়, বরিশাল ০। NaN থেকে যায়, কারণ scikit-learn 1.8-এর forest ফাঁকা মান নিয়ে split করতে পারে। Scaling-এর দরকার নেই।
  6. Boosting-এর রূপ। কোনো রূপান্তর নেই। Category dtype দেখে HistGradientBoosting এক split-এই রংপুর আর বরিশালকে বাকিদের থেকে আলাদা করতে পারে, যেখানে ordinal code-এ ০ আর ৬ পাশাপাশি নয়।
  7. নিয়ম। Model যা দিয়ে কাজ করে, প্রস্তুতি তার জন্য। আর যত প্রস্তুতিই হোক, তা pipeline-এর ভেতরে, যাতে প্রতিটি fold নিজের train থেকে শেখে।

অনেক category থাকলে। জেলা ৬৪টি বা পণ্য হাজারটি হলে one-hot অনেক চওড়া হয়। তখন target encoding (scikit-learn-এর TargetEncoder, যা ভেতরে cross-fitting করে নিজের সারির label গড়ে ঢুকতে দেয় না) বা boosting-এর native category বিকল্প। নিজে হাতে “জেলার churn-এর গড়” বসালে সেটি leakage, আর CV-তে চমৎকার দেখিয়ে আসল ডেটায় ভেঙে পড়ে।

৩ · সৎ তুলনার নিয়ম — একই fold, একই metric, একই বাজেট

দুটি model তুলনা করতে গিয়ে সবচেয়ে সাধারণ অসততা ইচ্ছাকৃত নয়: একটিকে ভালো fold, বেশি tuning বা ভুল metric দেওয়া। নিচের ছবিটি এই পর্বের তুলনার নিয়ম। প্রতিটি ধাপ পর্ব ০৪-এর পুনরুৎপাদনযোগ্য experiment আর পর্ব ২২-এর পরিসংখ্যান থেকে আসে।

সৎ তুলনার সাত ধাপ

সব model পরিবার একই পথে যায়; বদলায় শুধু প্রস্তুতি আর model

Fair comparison protocol for tabular models ১. তালাবন্ধ test আলাদা শেষ সিদ্ধান্তের আগে কেউ ছোঁবে না ২. সবার জন্য একই fold RepeatedStratifiedKFold, একটি seed ৩. পরিবারের নিজস্ব প্রস্তুতি scale, one-hot বা কিছুই না; pipeline-এর ভেতরে ৪. একই tuning বাজেট যেমন প্রত্যেকে ২০টি candidate, একই CV ৫. mean ± sd, সময়, memory শুধু একটি সংখ্যা নয়, ছড়ানো আর খরচও ৬. শীর্ষ দুটি: corrected t-test একই fold-এর জোড়া পার্থক্য (পর্ব ০৪, ২২) ৭. সিদ্ধান্ত, তারপর test একবার প্রমাণ না থাকলে: সস্তা, ছোট, বোঝা যায় এমন model
ছবি ৩৮.১ — Model বদলায়, নিয়ম বদলায় না। সবচেয়ে বেশি ভুল হয় ধাপ ২ আর ৪-এ: আলাদা fold আর অসম tuning.

কেন একই fold? কিছু fold “কঠিন” পড়ে, কিছু “সহজ”. দুটি model আলাদা fold পেলে পার্থক্যের অনেকটা আসে fold-এর ভাগ্য থেকে। একই fold হলে প্রতিটি fold-এ জোড়া পার্থক্য নেওয়া যায়, আর fold-এর কাঠিন্য কেটে যায়। কেন repeated? একটি k-fold একটি এলোমেলো ভাগ; কয়েকবার আলাদা seed-এ ভাগ করলে গড় স্থির হয় (repeated CV, stratified যাতে প্রতিটি fold-এ churn-এর হার এক)। দ্বিতীয় ব্লক ছয়টি model-কে ৩-fold × ২ বার, অর্থাৎ ছয়টি একই fold-এ চালায়, ROC AUC আর fit time মাপে, তারপর শীর্ষ দুটির মধ্যে naive আর corrected t-test করে।

import matplotlib.pyplot as plt
from scipy import stats
from sklearn.model_selection import RepeatedStratifiedKFold, cross_validate
cv = RepeatedStratifiedKFold(n_splits=3, n_repeats=2, random_state=38)  # সবার জন্য একই fold
auc, fit = {}, {}
print("model      ROC AUC mean ± sd   fit s/fold")
for name, m in models.items():
    out = cross_validate(m, X, y, cv=cv, scoring="roc_auc")
    auc[name], fit[name] = out["test_score"], out["fit_time"].mean()
    print(f"{name:9s}  {auc[name].mean():.3f} ± {auc[name].std(ddof=1):.3f}     {fit[name]:.3f}")
a, b = sorted(auc, key=lambda k: -auc[k].mean())[:2]
d = auc[a] - auc[b]; J = len(d)            # একই fold-এ জোড়া পার্থক্য
t_naive = d.mean() / np.sqrt(d.var(ddof=1) / J)
t_corr = d.mean() / np.sqrt(d.var(ddof=1) * (1 / J + 1 / 2))   # n_test/n_train = 1/2
print(f"{a} - {b}: mean diff {d.mean():+.4f} over {J} folds")
for lab, t in [("naive    ", t_naive), ("corrected", t_corr)]:
    print(f"  {lab} t = {t:5.2f}, p = {2 * stats.t.sf(abs(t), J - 1):.3f}")
names = list(auc)
fig, (a1, a2) = plt.subplots(1, 2, figsize=(9, 3.6))
a1.errorbar([auc[n].mean() for n in names], names, fmt="o", capsize=4,
            xerr=[auc[n].std(ddof=1) for n in names])
a1.set(xlabel="ROC AUC (mean +/- 1 sd, 6 folds)", title="Same folds for every model")
a2.barh(names, [fit[n] for n in names], color="tab:orange")
a2.set(xlabel="mean fit time per fold (s)", title="Cost")
for ax in (a1, a2):
    ax.invert_yaxis()
fig.subplots_adjust(wspace=0.6, bottom=0.15)

আসল আউটপুট (ব্রাউজারের Python, একটি run; সময় যন্ত্রভেদে আলাদা), নিচে দুটি ছবি:

model      ROC AUC mean ± sd   fit s/fold
dummy      0.500 ± 0.000     0.002
logistic   0.765 ± 0.024     0.041
kNN        0.732 ± 0.020     0.025
forest     0.767 ± 0.030     0.154
hist GBM   0.758 ± 0.024     0.184
MLP        0.752 ± 0.033     0.270
forest - logistic: mean diff +0.0024 over 6 folds
  naive     t =  0.20, p = 0.847
  corrected t =  0.10, p = 0.923

Dummy ঠিক ০.৫০০, যেমন হওয়ার কথা। শীর্ষে তিনজন প্রায় সমান: forest ০.৭৬৭, logistic ০.৭৬৫, boosting ০.৭৫৮; MLP ০.৭৫২ আর kNN ০.৭৩২ একটু পেছনে। কিন্তু প্রতিটির sd ০.০২–০.০৩, অর্থাৎ fold থেকে fold-এ AUC যতটা নড়ে, model-দের পার্থক্য তার দশ ভাগের এক ভাগ। Forest − logistic-এর গড় পার্থক্য +০.০০২৪; সংশোধিত t মাত্র ০.১০, p = ০.৯২. কোনো প্রমাণ নেই যে forest ভালো। খরচে কিন্তু বড় পার্থক্য: forest আর boosting-এর fit logistic-এর প্রায় চার গুণ সময় নেয়, MLP প্রায় সাত গুণ। এই ডেটায়, এই আকারে, যুক্তিসঙ্গত পছন্দ logistic regression. বাঁ দিকের ছবিতে sd-এর দাগগুলো প্রায় পুরোপুরি একে অপরের ওপর পড়েছে।

এমন টেবিল কীভাবে পড়বেন, ধাপে ধাপে। Fold-ভিত্তিক সংখ্যাগুলো একই কোড ডেস্কটপে চালিয়ে নেওয়া; গড়গুলো ব্রাউজারের আউটপুটের সঙ্গে হুবহু মেলে।

৩-fold × ২, J = 6n_test / n_train = 1/2

            
  1. সাজানো টেবিল। গড়ে forest প্রথম, কিন্তু প্রথম তিনটির ব্যবধান ০.০০৯-এর মধ্যে। Dummy-র ০.৫০০ মনে করিয়ে দেয় যে সবাই সত্যিকারের সংকেত শিখেছে।
  2. ± কী মাপে। এটি ছয়টি fold-score-এর sd, অর্থাৎ একটি fold থেকে আরেকটিতে ওঠানামা। Forest আর logistic-এর mean ± sd পরিসর প্রায় পুরোপুরি এক। তবে শুধু দাগ মেলা দেখে সিদ্ধান্ত নেওয়া যায় না, কারণ দুটি model একই fold-এ ওঠে-নামে।
  3. জোড়া পার্থক্য। একই fold-এ forest থেকে logistic বাদ। চারটি fold-এ forest এগিয়ে, দুটিতে পিছিয়ে, আর fold ২-এ বড় ব্যবধানে পিছিয়ে। গড় +০.০০২৪, sd ০.০২৮৭.
  4. Naive t. Fold-গুলো স্বাধীন ধরলে SE = s/√J = ০.০১১৭, t = ০.২০. এখানেই p = ০.৮৫, কোনো প্রমাণ নেই। তবে এই পদ্ধতি সাধারণভাবে অতি-আশাবাদী।
  5. Corrected t. Fold-গুলোর train অংশ অনেকটা এক, তাই score পরস্পর-সম্পর্কিত। Nadeau–Bengio-র গুণক ১/৬ + ১/২ = ০.৬৬৭; SE প্রায় দ্বিগুণ, t = ০.১০, p = ০.৯২ (পর্ব ২২)।
  6. সিদ্ধান্ত। AUC দিয়ে আলাদা করা গেল না, তাই খরচ, আকার আর ব্যাখ্যা দিয়ে বাছাই: এখন logistic. কিন্তু এটি ১,০০০ সারির রায়; ডেটা বাড়লে ক্রম বদলাতে পারে, পরের অংশে ঠিক সেটিই দেখবেন।

৪ · ডেটা বাড়লে ক্রম বদলায় — সময় আর memory-সহ

১,০০০ সারির রায় কি ১০ গুণ ডেটাতেও টিকবে? ব্রাউজারে বড় n চালানো যায় না, তাই একই make_churn নিয়ম আর হুবহু একই ছয়টি model-সেটিং (প্রথম ব্লকের models) ডেস্কটপে চালানো হয়েছে। সত্যিকারের run: একটি সাধারণ ল্যাপটপ, Python 3.14.4, scikit-learn 1.8.0, একটি run; প্রতিটি n-এ নতুন seed-এর ডেটা, একই ৩-fold × ২ CV. শেষ কলাম হলো শেখানো model-এর pickle আকার।

~/bazarghar-ml$ python scale_curve.py
Python 3.14.4, scikit-learn 1.8.0
      n  model      ROC AUC mean ± sd  fit s/fold  model size
  1,000  logistic   0.726 ± 0.034       0.01          3 KB
  1,000  kNN        0.727 ± 0.030       0.01        140 KB
  1,000  forest     0.757 ± 0.035       0.04        458 KB
  1,000  hist GBM   0.727 ± 0.028       0.10        162 KB
  1,000  MLP        0.704 ± 0.030       0.06         24 KB
  4,000  logistic   0.765 ± 0.009       0.01          3 KB
  4,000  kNN        0.754 ± 0.007       0.01        551 KB
  4,000  forest     0.782 ± 0.006       0.09      1,720 KB
  4,000  hist GBM   0.786 ± 0.010       0.09        200 KB
  4,000  MLP        0.777 ± 0.009       0.09         22 KB
 16,000  logistic   0.751 ± 0.008       0.02          3 KB
 16,000  kNN        0.748 ± 0.008       0.05      2,195 KB
 16,000  forest     0.784 ± 0.007       0.33      6,922 KB
 16,000  hist GBM   0.798 ± 0.006       0.10        161 KB
 16,000  MLP        0.762 ± 0.007       0.22         22 KB
 64,000  logistic   0.754 ± 0.005       0.06          3 KB
 64,000  kNN        0.758 ± 0.004       0.23      8,771 KB
 64,000  forest     0.790 ± 0.003       1.47     27,419 KB
 64,000  hist GBM   0.809 ± 0.004       0.17        203 KB
 64,000  MLP        0.760 ± 0.004       0.72         22 KB

তিনটি জিনিস লক্ষ করুন। এক, ক্রম বদলায়। ১,০০০ সারিতে (অন্য seed) forest এগিয়ে, boosting logistic-এর সমান। ৪,০০০-এ boosting আর forest logistic-কে প্রায় ০.০২ ছাড়িয়ে যায়, আর sd এখন ০.০১-এর নিচে, তাই পার্থক্য স্পষ্ট। ৬৪,০০০-এ boosting ০.৮০৯, forest ০.৭৯০, logistic ০.৭৫৪. কারণ: ডেটার নিয়মে ধাপ আর interaction আছে, যা গাছ নিজে খুঁজে নেয়, কিন্তু linear model শুধু হাতে বানানো feature দিয়ে পায়। বেশি ডেটায় গাছ সেই কাঠামো নির্ভরযোগ্যভাবে শেখে; কম ডেটায় variance তাকে খেয়ে ফেলে। এটি আপনার পরিচিত learning curve-এর গল্প (পর্ব ০৩)। দুই, logistic-এর ছাদ আছে। ৪,০০০-এর পর তার AUC আর বাড়ে না: সমস্যা variance নয়, bias. ঠিক interaction feature হাতে যোগ করলে সে উঠত (পর্ব ২৩), কিন্তু সেই feature জানতে হয়। তিন, খরচ সমান নয়। ৬৪,০০০-এ forest-এর ফাইল ২৭ MB আর fit ১.৫ সেকেন্ড; boosting ০.২ MB আর ০.১৭ সেকেন্ড; logistic ৩ KB. kNN পুরো train ডেটাই মনে রাখে বলে তার ফাইল ডেটার সঙ্গে বাড়ে (৮.৮ MB). MLP এখানে চলনসই, কিন্তু কোনো আকারেই সেরা নয়।

সাবধান: এটি একটি কাল্পনিক ডেটার একটি run. সংখ্যাগুলো সাধারণ সত্য নয়; শুধু প্রবণতাটি (n বাড়লে গাছের লাভ বাড়ে, linear-এর ছাদ আসে) অনেক বাস্তব tabular ডেটায় দেখা যায়, সব ডেটায় নয়।

৫ · Tuning-এর বাজেট — কত চেষ্টা, কত লাভ

প্রতিটি model-এর hyperparameter আছে, আর ডিফল্ট মান আপনার ডেটার জন্য বানানো নয়। কিন্তু tuning-এর খরচ আছে, আর তুলনায় একটি লুকানো অসততা ঢোকে: যে model বেশি tuning পায়, সে বেশি ভাগ্যবান candidate পায়। James Bergstra আর Yoshua Bengio ২০১২ সালে (JMLR) দেখান, একই বাজেটে random search প্রায়ই grid search-এর চেয়ে ভালো, কারণ সাধারণত অল্প কয়েকটি hyperparameter-ই আসলে গুরুত্বপূর্ণ, আর grid সেগুলোর অল্প কয়েকটি মানই চেষ্টা করে। scikit-learn-এ RandomizedSearchCV, যেখানে n_iter হলো বাজেট (AI-এর ভিত্তি কোর্সের পর্ব ৬৬-এ প্রথম দেখা)।

বাজেট বাড়ালে কী পাওয়া যায়, তা সরল গণিতে বলা যায়। ধরুন N টি candidate-এর CV score জানা, ছোট থেকে বড় সাজানো v(1) ≤ … ≤ v(N). এদের থেকে এলোমেলো k টি নিলে (বাজেট k), সেরাটি v(i) হওয়ার সম্ভাবনা C(i − 1, k − 1) / C(N, k), কারণ বাকি k − 1 টি তার নিচের i − 1 টি থেকে আসতে হবে। তাই

E[k টির সেরা] = ∑i v(i) · C(i − 1, k − 1) / C(N, k)

আর baseline-এর সমান বা নিচে m টি candidate থাকলে, k টির অন্তত একটি baseline ছাড়ানোর সম্ভাবনা ১ − C(m, k)/C(N, k). এখানে C(a, b) হলো a থেকে b টি বাছার উপায়ের সংখ্যা। তৃতীয় ব্লক boosting-এর ১২টি এলোমেলো candidate একটি আলাদা ৩-fold-এ মাপে, একই fold-এ ডিফল্ট logistic-এর সঙ্গে তুলনা করে, আর এই দুটি সূত্র দিয়ে বাজেট ১, ৩, ৬, ১২-এর ফল বের করে। RandomizedSearchCV একই random_state-এ candidate একই ক্রমে টানে, তাই n_iter=4-এর search হলো n_iter=12-এর প্রথম চারটি (ডেস্কটপে মিলিয়ে দেখা)।

import time
from math import comb
from scipy.stats import loguniform, randint
from sklearn.model_selection import RandomizedSearchCV, StratifiedKFold, cross_val_score
cv3 = StratifiedKFold(3, shuffle=True, random_state=1)      # search-এর জন্য আলাদা fold
base = cross_val_score(models["logistic"], X, y, cv=cv3, scoring="roc_auc").mean()
space = {"learning_rate": loguniform(0.03, 0.5), "max_leaf_nodes": randint(3, 32),
         "min_samples_leaf": randint(5, 100), "l2_regularization": loguniform(1e-3, 10)}
s = RandomizedSearchCV(HistGradientBoostingClassifier(max_iter=40, random_state=0),
                       space, n_iter=12, cv=cv3, scoring="roc_auc", random_state=7).fit(X, y)
sc = s.cv_results_["mean_test_score"]
v, ks = np.sort(sc), np.arange(1, 13)
m = (v <= base).sum()                      # কতগুলো candidate logistic-এর চেয়ে খারাপ
print(f"logistic, default, same folds: {base:.3f}")
print("12 hist GBM candidates:", np.round(sc, 3))
exp_best = [sum(v[i] * comb(i, k - 1) for i in range(12)) / comb(12, k) for k in ks]
for k in (1, 3, 6, 12):                    # 12টি থেকে এলোমেলো k টি নিলে
    print(f"budget {k:2d}: expected best {exp_best[k - 1]:.3f},"
          f" P(beats logistic) = {1 - comb(m, k) / comb(12, k):.2f}")
print("best:", {p: round(float(q), 3) for p, q in s.best_params_.items()})
fig, ax = plt.subplots(figsize=(6.5, 3.6))
ax.plot(ks, sc, "o", color="silver", label="candidate i (search order)")
ax.plot(ks, exp_best, "-", label="expected best of k (hist GBM)")
ax.axhline(base, color="tab:green", ls="--", label="logistic, default")
ax.set(xlabel="search budget (number of candidates)", ylabel="3-fold CV ROC AUC",
       title="What a bigger tuning budget buys")
ax.legend(fontsize=8)

আসল আউটপুট (ব্রাউজারের Python), নিচে ছবি:

logistic, default, same folds: 0.769
12 hist GBM candidates: [0.77  0.74  0.746 0.776 0.772 0.767 0.782 0.776 0.781 0.744 0.761 0.744]
budget  1: expected best 0.763, P(beats logistic) = 0.50
budget  3: expected best 0.776, P(beats logistic) = 0.91
budget  6: expected best 0.780, P(beats logistic) = 1.00
budget 12: expected best 0.782, P(beats logistic) = 1.00
best: {'l2_regularization': 0.011, 'learning_rate': 0.248, 'max_leaf_nodes': 3.0, 'min_samples_leaf': 39.0}

একটি এলোমেলো boosting configuration ০.৭৪০ থেকে ০.৭৮২-এর যেকোনো জায়গায় পড়তে পারে; ডিফল্ট logistic ০.৭৬৯. বারোটির ছয়টি logistic-এর সমান বা নিচে, তাই বাজেট ১-এ boosting জেতার সম্ভাবনা ঠিক অর্ধেক (১ − ৬/১২)। বাজেট ৩-এ ১ − C(৬,৩)/C(১২,৩) = ১ − ২০/২২০ = ০.৯১, বাজেট ৬-এ প্রায় নিশ্চিত। প্রত্যাশিত সেরা ০.৭৬৩ থেকে ০.৭৮২-এ ওঠে, কিন্তু লাভ দ্রুত কমে: ১ থেকে ৩-এ +০.০১৩, ৬ থেকে ১২-এ মাত্র +০.০০২. ছবিতে রেখাটি প্রথমে খাড়া, তারপর সমতল। সেরা configuration খুব সরল গাছ (max_leaf_nodes = ৩) আর বেশ বড় learning rate; ছোট ডেটায় এটাই স্বাভাবিক। একটি সতর্কতা: ০.৭৮২ একটি আশাবাদী সংখ্যা, কারণ এই fold-গুলোতেই ১২টির মধ্যে সেরাটি বাছা হয়েছে (selection bias). সৎ অনুমানের জন্য nested CV বা তালাবন্ধ test.

বড় বাজেট আর পাঁচ-fold CV ব্রাউজারে চলে না। তাই ডেস্কটপে সত্যিকারের run (একই ল্যাপটপ আর version): ৪,০০০ সারিতে search, ভেতরে ৫-fold CV, প্রতিটি পরিবারকে প্রথমে ৫টি, তারপর ২০টি candidate (একই seed, তাই ২০-এর প্রথম ৫টিই ৫-এর search); আর ২০,০০০ সারির একটি আলাদা, তালাবন্ধ test. Forest এখানে ২০০টি গাছের, boosting ৩০০ iteration-এর; “default CV” কলাম হলো search-এর শুরুর সেটিং, কোনো tuning ছাড়া।

~/bazarghar-ml$ python budget.py
Python 3.14.4, scikit-learn 1.8.0
n_train = 4,000, 5-fold CV inside the search, locked test = 20,000 rows
family     default CV  budget 5: CV / test   budget 20: CV / test   time 20 (s)
logistic   0.767        0.767 / 0.760        0.767 / 0.760        1.6
forest     0.783        0.798 / 0.800        0.798 / 0.800       55.4
hist GBM   0.757        0.790 / 0.801        0.798 / 0.804       27.7
MLP        0.775        0.775 / 0.776        0.779 / 0.772       19.0

শুরুর সেটিং-এ ক্রম ছিল forest > MLP > logistic > boosting: ৩০০ iteration-এর boosting ছোট ডেটায় overfit করেছে (০.৭৫৭)। মাত্র ৫টি candidate-এ boosting লাফিয়ে ০.৭৯০-এ, ২০টিতে ০.৭৯৮; forest-ও ০.৭৯৮-এ থামে। তালাবন্ধ test-এ বাজেট ২০-এর boosting ০.৮০৪, forest ০.৮০০, logistic ০.৭৬০. অর্থাৎ শুধু শুরুর সেটিং দেখে তুলনা করলে boosting-কে শেষ ঘোষণা করতেন; সমান বাজেটে সে CV-তে forest-এর সমান আর তালাবন্ধ test-এ প্রথম। আর খরচ: ২০টি candidate × ৫ fold-এ logistic ২ সেকেন্ডের কম, boosting প্রায় ৩০, forest প্রায় এক মিনিট। বাজেট গোনা উচিত candidate-এ নয়, সময়ে।

Optuna, ডেস্কটপে। এলোমেলো খোঁজের চেয়ে বুদ্ধিমান পথ হলো Bayesian optimization-জাতীয় পদ্ধতি, যা আগের চেষ্টা দেখে পরেরটি বাছে (বিস্তারিত পর্ব ৪৯-এ, Bayesian optimization — অল্প চেষ্টায় hyperparameter খোঁজা)। Optuna (Takuya Akiba ও সহকর্মীরা, KDD ২০১৯) এর একটি জনপ্রিয় library; তার ডিফল্ট sampler TPE (James Bergstra ও সহকর্মীরা, NIPS ২০১১)। Optuna ব্রাউজারে লোড হয় না, তাই একই boosting search ২০টি trial-এ, ডেস্কটপে সত্যিকারের run (optuna 5.0.0):

~/bazarghar-ml$ python optuna_hgb.py
Python 3.14.4, scikit-learn 1.8.0, optuna 5.0.0
best CV after trials 1, 5, 10, 20: 0.795 0.799 0.799 0.802
best CV 0.802, locked test 0.808, 27.6 s
{'learning_rate': 0.027, 'max_leaf_nodes': 6, 'min_samples_leaf': 22, 'l2_regularization': 0.009}

২০টি trial-এ CV ০.৮০২, test ০.৮০৮; random search-এর ০.৭৯৮ আর ০.৮০৪-এর চেয়ে সামান্য বেশি, প্রায় একই সময়ে। একটি run-এ এই পার্থক্য seed-এর ভাগ্যও হতে পারে; বড় search space আর দামি model-এ এ ধরনের পদ্ধতির লাভ সাধারণত বেশি দেখা যায়।

৬ · যেখানে linear model জেতে — ছোট n আর পরিসরের বাইরে

Boosting-এর জয় দেখে মনে হতে পারে linear model শুধু baseline. কিন্তু অন্তত চারটি পরিস্থিতিতে সে প্রায়ই সঠিক উত্তর। শেষ ব্লক প্রথম দুটি মেপে দেখায়। (ক) খুব ছোট n: বাজারঘরের নতুন সদস্যপদ “বাজারঘর প্লাস”-এ মাত্র ৮০ জন, চারটি সংখ্যার feature, আর প্রভাব মোটামুটি মসৃণ (একটির কোনো প্রভাবই নেই)। (খ) Extrapolation: ডেলিভারির সময় দূরত্বের সঙ্গে প্রায় রৈখিক; model শেখে শুধু ২০ কিলোমিটারের ভেতরের অর্ডারে, কিন্তু নতুন দূরের এলাকায় অনুমান লাগবে।

import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import (HistGradientBoostingClassifier, HistGradientBoostingRegressor,
                              RandomForestClassifier, RandomForestRegressor)
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score

r = np.random.default_rng(38)
Xs = r.normal(size=(80, 4))                       # ক. মাত্র 80 জন সদস্য, প্রভাব প্রায় রৈখিক
ys = (r.random(80) < 1 / (1 + np.exp(-Xs @ [1.0, -0.8, 0.6, 0.0]))).astype(int)
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=4, random_state=0)
small = {"logistic": LogisticRegression(), "forest": RandomForestClassifier(50, random_state=0),
         "hist GBM": HistGradientBoostingClassifier(random_state=0)}
for name, m in small.items():
    s = cross_val_score(m, Xs, ys, cv=cv, scoring="roc_auc")
    print(f"n = 80   {name:9s} AUC {s.mean():.3f} ± {s.std(ddof=1):.3f}")
km = r.uniform(2, 40, 300)                        # খ. ডেলিভারির ঘণ্টা বনাম দূরত্ব
hours = 4 + 1.5 * km + r.normal(0, 3, 300)
tr = km <= 20                                     # শেখা শুধু শহরের ভেতরের অর্ডারে
grid = np.linspace(0, 42, 200)[:, None]
fig, ax = plt.subplots(figsize=(7, 4))
ax.scatter(km, hours, s=8, c=np.where(tr, "gray", "orange"), label="data (orange = unseen > 20 km)")
regs = {"linear": LinearRegression(), "forest": RandomForestRegressor(50, random_state=0),
        "hist GBM": HistGradientBoostingRegressor(random_state=0)}
for name, m in regs.items():
    m.fit(km[tr, None], hours[tr])
    mae_in = np.abs(m.predict(km[tr, None]) - hours[tr]).mean()
    mae_out = np.abs(m.predict(km[~tr, None]) - hours[~tr]).mean()
    print(f"{name:9s} train MAE (<= 20 km) {mae_in:5.2f} h   unseen > 20 km {mae_out:5.2f} h")
    ax.plot(grid, m.predict(grid), lw=2, label=name)
ax.axvline(20, color="k", ls=":")
ax.set(xlabel="distance (km)", ylabel="delivery time (hours)",
       title="Trees cannot extrapolate beyond the training range")
ax.legend(fontsize=8)

আসল আউটপুট (ব্রাউজারের Python), নিচে ছবি:

n = 80   logistic  AUC 0.838 ± 0.080
n = 80   forest    AUC 0.767 ± 0.124
n = 80   hist GBM  AUC 0.792 ± 0.091
linear    train MAE (<= 20 km)  2.34 h   unseen > 20 km  2.43 h
forest    train MAE (<= 20 km)  1.01 h   unseen > 20 km 14.95 h
hist GBM  train MAE (<= 20 km)  1.91 h   unseen > 20 km 15.36 h

ছোট n: ৮০ সারিতে logistic ০.৮৩৮, boosting ০.৭৯২, forest ০.৭৬৭. Logistic-এর sd-ও সবচেয়ে কম (০.০৮০ বনাম forest-এর ০.১২৪)। ৬৪টি train সারিতে গাছ মসৃণ ঢাল শেখে সিঁড়ির ধাপ দিয়ে, আর প্রতিটি ধাপ গোলমালে নড়ে; linear model-এর আকৃতি ঠিক হলে তার variance অনেক কম। তবে sd বড়, তাই এক ডেটায় এই ব্যবধান “প্রমাণ” নয়; ছোট n-এ সব তুলনাই দুর্বল। Extrapolation: train পরিসরে forest-এর ভুল সবচেয়ে কম (১.০১ ঘণ্টা, কারণ এটি train ডেটাই, প্রায় মুখস্থ)। কিন্তু ২০ কিলোমিটারের পরে forest আর boosting-এর ভুল ১৫ ঘণ্টা, linear-এর ২.৪৩. ছবিতে দেখবেন গাছের রেখা ২০-এর পরে সমতল: গাছের প্রতিটি পাতা train-এর কিছু সারির গড়, তাই সে কখনো দেখা মানের বাইরে যেতে পারে না।

(গ) একমুখী সম্পর্কের বাধ্যবাধকতা: “দূরত্ব বাড়লে প্রতিশ্রুত সময় কখনো কমবে না” বা “দেরি বাড়লে churn-এর ঝুঁকি কমবে না”, এমন শর্ত ব্যবসা বা নিয়ম চাইতে পারে। Linear model-এ ওজনের চিহ্ন দেখলেই হয়। গাছেও এখন উপায় আছে: HistGradientBoosting আর scikit-learn 1.4 থেকে decision tree ও random forest-এ monotonic_cst দিয়ে monotonic constraint (forest-এ classification-এর ক্ষেত্রে ফাঁকা মান থাকলে চলে না, docs দেখে নেবেন)। কিন্তু monotone গাছও extrapolate করে না, শুধু সমতল থাকে। (ঘ) ব্যাখ্যা আর নিরীক্ষা: ঋণ বা বিমার মতো ক্ষেত্রে প্রতিটি ওজন পড়া, একটি scorecard ছাপানো, আর নিয়ন্ত্রকের কাছে যুক্তি দেখানো সহজ হয় linear বা GLM-এ (পর্ব ২০)। জটিল model-এর ব্যাখ্যা পরে SHAP-এর মতো সরঞ্জামে আসবে (পর্ব ৫৭, একটি অনুমানের ব্যাখ্যা — SHAP আর LIME), কিন্তু সেই ব্যাখ্যা নিজেই একটি আনুমানিক মডেল।

রিকশাচালক শুধু ধানমন্ডির ভেতরে চালিয়ে ভাড়ার নিয়ম শিখেছেন: প্রতি কিলোমিটারে মোটামুটি এত টাকা। কেউ সাভার যেতে চাইলে তিনি “কিলোমিটার × হার” দিয়ে একটি যুক্তিসঙ্গত ভাড়া বলতে পারেন। কিন্তু যে চালক শুধু মুখস্থ করেছেন “ধানমন্ডি ২৭ থেকে সবচেয়ে দূরের জায়গা ৮০ টাকা”, তিনি সাভারের জন্যও ৮০ টাকাই বলবেন। গাছ দ্বিতীয় চালকের মতো।

৭ · প্রতিটি পরিবারের শক্তি আর দুর্বলতা — neural network-সহ

নিচের টেবিল অভিজ্ঞতার নিয়ম, প্রমাণিত সূত্র নয়; শেষ কথা সবসময় আপনার ডেটায় সৎ তুলনা। David Wolpert-এর no free lunch ফলাফল (১৯৯৬) মনে করিয়ে দেয়, সব সম্ভাব্য সমস্যার গড়ে কোনো learner অন্যটির চেয়ে ভালো নয়; বাস্তবের ডেটার বিশেষ কাঠামোই কাউকে এগিয়ে রাখে।

পরিবারপ্রস্তুতিশক্তিদুর্বলতাকখন প্রথমে
Logistic / ridge / GLMভরাট, scale, one-hotসস্তা, স্থির, ব্যাখ্যা সহজ, extrapolate করে, সম্ভাবনা প্রায়ই ভালো calibratedinteraction আর ধাপ নিজে শেখে নাসবসময় baseline; ছোট n; নিয়ন্ত্রিত ক্ষেত্র
Random forestordinal; NaN চলেডিফল্টেই শক্ত, tuning কম লাগে, overfit-এ ধীরবড় ফাইল, ধীর predict, extrapolate করে নাদ্রুত একটি শক্ত দ্বিতীয় মত
Gradient boosting (Hist, XGBoost, LightGBM, CatBoost)প্রায় কিছুই নামিশ্র feature, interaction, missing; মাঝারি-বড় ডেটায় প্রায়ই সেরা; ছোট ফাইলtuning লাগে; ডিফল্টে ছোট ডেটায় overfit করতে পারেহাজার থেকে কোটি সারির মিশ্র টেবিল
kNNভরাট, scale (জরুরি)সরল, প্রশিক্ষণ নেইপুরো ডেটা মনে রাখে, অপ্রাসঙ্গিক feature-এ দুর্বলছোট, অল্প মাত্রার সংখ্যার ডেটা; মিলের খোঁজ
SVMভরাট, scale, one-hotছোট-মাঝারি সংখ্যার ডেটায় অ-রৈখিক সীমাবড় n-এ ধীর, সম্ভাবনা নয়কয়েক হাজার সারির সংখ্যার ডেটা (পর্ব ২৯, ৩১)
MLP / tabular deep learningভরাট, scale, one-hot বা embeddingঅন্য ধরনের ডেটার (লেখা, ছবি) সঙ্গে জোড়া যায়অনেক knob, ধীর, ছোট টেবিলে প্রায়ই পিছিয়েখুব বড় ডেটা, বা টেবিলের সঙ্গে লেখা/ছবি মিশলে

SVM-এর বিস্তারিত দাম আগের মডিউলে দেখেছেন (পর্ব ২৯, পর্ব ৩১)। এখানে বাকি প্রশ্ন neural network নিয়ে, কারণ ছবি আর ভাষায় তার সাফল্য দেখে মনে হতে পারে টেবিলেও সে জিতবে। প্রকাশিত তুলনাগুলো সতর্ক উত্তর দেয়। Léo Grinsztajn, Edouard Oyallon আর Gaël Varoquaux (NeurIPS ২০২২, datasets ও benchmarks track) ৪৫টি ডেটাসেটে মাঝারি আকারের (প্রায় দশ হাজার সারি) টেবিলে দেখান, গাছভিত্তিক model তখনো এগিয়ে, এমনকি neural network-এর বেশি tuning-এর পরেও। তাঁদের ব্যাখ্যা: tabular লক্ষ্য প্রায়ই অনিয়মিত (ধাপ, হঠাৎ বদল), যেখানে network মসৃণ সমাধানের দিকে ঝোঁকে; অপ্রাসঙ্গিক কলাম network-কে বেশি ক্ষতি করে; আর কলামের অর্থ ঘোরালে (rotation) নষ্ট হয়, যা network-এর গঠন সম্মান করে না। Ravid Shwartz-Ziv আর Amitai Armon (Information Fusion, ২০২২) দেখান, নতুন deep model-গুলো নিজেদের প্রবন্ধের বাইরের ডেটাসেটে সাধারণত XGBoost-এর চেয়ে পিছিয়ে ছিল, যদিও দুটির ensemble সবচেয়ে ভালো করেছিল।

ছবিটি স্থির নয়। পরে Duncan McElfresh ও সহকর্মীদের বড় তুলনায় (NeurIPS ২০২৩) দেখা যায়, অনেক ডেটাসেটে GBM আর neural network-এর পার্থক্য নগণ্য, আর অনেক সময় model-পরিবারের চেয়ে boosting-এর হালকা tuning বেশি গুরুত্বপূর্ণ। আর TabPFN-এর মতো আগে-থেকে-শেখানো tabular foundation model (Noah Hollmann ও সহকর্মীরা, ICLR ২০২৩; পরের সংস্করণ ২০২৫ সালে Nature-এ) ছোট টেবিলে শক্ত ফল দাবি করে। এগুলো নতুন, ডেটার আকার আর hardware-এর সীমা আছে; ব্যবহার করলে নিজের ডেটায় একই সৎ নিয়মে যাচাই করবেন। Neural network-এর ভেতরের কাজ পরে deep learning কোর্সে আসবে।

৮ · সিদ্ধান্তের পথ — বাজারঘরের তিন প্রকল্প আর চেকলিস্ট

সব মিলিয়ে, সিদ্ধান্ত নেওয়া হয় চারটি প্রশ্নে: ডেটা কত বড়? feature কেমন (সংখ্যা, category, ফাঁকা, interaction)? অনুমান কোথায় লাগবে (train-এর পরিসরে না বাইরে)? আর আউটপুটের কী শর্ত (সম্ভাবনা, ব্যাখ্যা, একমুখী, গতি)? নিচে বাজারঘরের তিনটি কাল্পনিক প্রকল্পে প্রশ্নগুলো ধাপে ধাপে।

আকার · feature · পরিসর · শর্তসব সংখ্যা কাল্পনিক

            
  1. তিন প্রকল্প। একই দোকান, কিন্তু আকার, feature, অনুমানের জায়গা আর শর্ত তিনটিতেই আলাদা। তাই শুরুর model-ও আলাদা হবে।
  2. ক: churn ঝুঁকি। দুই লাখ সারি, মিশ্র কলাম, ফাঁকা মান আর সম্ভবত interaction: boosting-এর চেনা মাঠ। Logistic baseline পাশে, সমান বাজেট, আর যেহেতু সম্ভাবনা দিয়ে টাকা খরচ হবে, শেষে calibration যাচাই।
  3. খ: বাজারঘর প্লাস। আশি জন। এখানে variance-ই বড় শত্রু, তাই regularized logistic. গাছ চালাতে পারেন তুলনার জন্য, কিন্তু অংশ ৬-এর মতো তার ছড়ানো বেশি হবে। কয়েক মাস পরে ডেটা জমলে প্রশ্নটি আবার করা।
  4. গ: দূরের ডেলিভারি। অনুমান লাগবে train-এর পরিসরের বাইরে, যেখানে গাছ শেষ দেখা মানে আটকে থাকে। Linear বা GLM দিয়ে শুরু, দূরত্বের ওজন ধনাত্মক কি না যাচাই। কাছের এলাকার সূক্ষ্মতার জন্য linear অনুমানের residual-এ গাছ বসানো যায়।
  5. সারাংশ। কোনো পরিবার সবখানে সেরা নয়। চারটি প্রশ্নের উত্তর থেকে শুরুর model আসে, আর সৎ তুলনা থেকে শেষ সিদ্ধান্ত।

শেষ সিদ্ধান্তের চেকলিস্ট

আরও গভীরে: Rich Caruana আর Alexandru Niculescu-Mizil ICML ২০০৬-এ এগারোটি ডেটাসেট আর আটটি metric-এ দশটি learner তুলনা করে দেখান, calibration-এর পরে boosted tree, তারপর random forest সবচেয়ে ভালো ছিল বলে জানা যায়। Manuel Fernández-Delgado ও সহকর্মীরা (JMLR ২০১৪) ১৭৯টি classifier-এর তুলনায় random forest-কে শীর্ষে পান; Michael Wainberg, Babak Alipanahi আর Brendan Frey ২০১৬ সালে দেখান যে সেই তুলনায় tuning আর মূল্যায়নের একটি ত্রুটি ফলকে প্রভাবিত করেছিল। Hyperband (Lisha Li ও সহকর্মীরা, JMLR ২০১৮) খারাপ candidate-কে অল্প সম্পদে আগেই বাদ দিয়ে বাজেট বাঁচায়; scikit-learn-এ তার কাছাকাছি HalvingRandomSearchCV এখনো experimental.

পরীক্ষা ও ইন্টারভিউতে:

  • কোনো tabular সমস্যায় প্রথম model কোনটি? Dummy আর regularized linear/logistic baseline, তারপর gradient boosting, একই CV fold-এ।
  • Random forest-এ scaling লাগে না কেন? Split শুধু “x ≤ সীমা” প্রশ্ন; একমুখী রূপান্তরে ক্রম বদলায় না, তাই split-ও বদলায় না।
  • গাছ কেন extrapolate করে না? প্রতিটি পাতার অনুমান train সারির গড়, তাই অনুমান কখনো দেখা লক্ষ্যমানের পরিসরের বাইরে যায় না।
  • ১২টি candidate-এর ৬টি baseline-এর নিচে; বাজেট ৩-এ boosting জেতার সম্ভাবনা? ১ − C(৬,৩)/C(১২,৩) = ১ − ২০/২২০ ≈ ০.৯১.
  • ৩-fold × ২-এ corrected t-এর variance গুণক? ১/J + ntest/ntrain = ১/৬ + ১/২ = ০.৬৬৭.

৯ · AI-এর যুগে — model বাছাইয়ে AI-কে যাচাই

AI assistant-কে “আমার churn ডেটার জন্য সেরা model বলো” জিজ্ঞেস করলে প্রায়ই একটি আত্মবিশ্বাসী উত্তর আসে, সাধারণত “XGBoost”, আপনার ডেটার আকার বা শর্ত না জেনেই। কোড দিলে তাতে চেনা ভুলগুলো থাকে:

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
Boosting-কে ১০০ candidate, logistic-কে ডিফল্ট দিয়ে তুলনাঅসম বাজেট; বিজয়ী আগে থেকেই ঠিকসবাইকে একই n_iter বা একই সময়
প্রতিটি model আলাদা train_test_split-এ, একটি করে সংখ্যাfold-এর ভাগ্য পার্থক্যে মিশে যায়একটি cv object সবার জন্য; mean ± sd আর জোড়া পার্থক্য
Pipeline-এর বাইরে পুরো ডেটায় scaler বা imputertest-এর তথ্য train-এ ঢোকেসব প্রস্তুতি Pipeline/ColumnTransformer-এর ভেতরে
পুরোনো argument: LogisticRegression(multi_class=…), OneHotEncoder(sparse=…)scikit-learn 1.8-এ সরানো; error দেয়নিজের version-এর docs; কোড চালিয়ে দেখা
Search-এর best_score_-কে চূড়ান্ত ফল বলাবাছাইয়ের কারণে আশাবাদীnested CV বা তালাবন্ধ test একবার
Repeated CV-র fold-score-এ সাধারণ ttest_relfold স্বাধীন নয়; মিথ্যা “significant”corrected t (পর্ব ২২)

AI-কে উত্তর নয়, পরিকল্পনা আর যাচাইযোগ্য কোড চাইতে বলুন:

আমার কাছে বাজারঘরের প্রায় ৫০,০০০ গ্রাহকের একটি টেবিল আছে: ৮টি সংখ্যার কলাম (কিছুতে ফাঁকা), জেলা (৬৪টি মান), পেমেন্ট আর courier; লক্ষ্য churn (০/১), হার প্রায় ২৫%. scikit-learn 1.8.0. ১. কোনো model বেছে দেওয়ার আগে জিজ্ঞেস করো: অনুমান কি train-এর পরিসরের বাইরে লাগবে, সম্ভাবনা বা ব্যাখ্যার শর্ত আছে কি না। ২. DummyClassifier, logistic regression, random forest আর HistGradientBoostingClassifier-এর তুলনার কোড দাও: প্রতিটির আলাদা ColumnTransformer প্রস্তুতি pipeline-এর ভেতরে, একটি RepeatedStratifiedKFold object সবার জন্য, metric ROC AUC. ৩. প্রতিটি পরিবারকে RandomizedSearchCV-এ একই n_iter দাও, আর fit time ও model-এর আকারও মাপো। ৪. শীর্ষ দুটির মধ্যে Nadeau–Bengio corrected t-test নিজে হিসাব করো, সূত্রসহ। ৫. তালাবন্ধ test শুধু শেষে একবার; কোনো warning চাপা দিও না, আর যে argument ব্যবহার করছ তা 1.8.0-এর docs-এ আছে কি না বলো।

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

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

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

প্র ০১ছবি আর ভাষায় neural network সবাইকে ছাড়িয়ে গেল, কিন্তু টেবিলে গাছভিত্তিক model এখনো প্রায়ই এগিয়ে। কেন?

ছবির একটি pixel একা কিছু বলে না; তার অর্থ আসে পাশের pixel থেকে। Convolution আর attention-এর মতো গঠন ঠিক এই স্থানিক বা ক্রমের সম্পর্ক ধরার জন্য বানানো। টেবিলের কলাম আলাদা: “বয়স”, “জেলা”, “শেষ অর্ডারের দিন” প্রতিটির নিজস্ব অর্থ আর একক, আর তাদের ক্রম অর্থহীন। এখানে network-এর সেই বিশেষ সুবিধা নেই।

Grinsztajn, Oyallon আর Varoquaux (NeurIPS ২০২২) তিনটি নির্দিষ্ট কারণ পরীক্ষা করে দেখান। এক, tabular লক্ষ্য প্রায়ই অনিয়মিত: ধাপ, হঠাৎ লাফ, সীমার কাছে বদল; MLP-এর মতো network মসৃণ ফাংশনের দিকে ঝোঁকে, আর লক্ষ্যকে মসৃণ করে দিলে গাছের সুবিধা কমে যায়। দুই, অপ্রাসঙ্গিক কলাম যোগ করলে MLP-এর ক্ষতি গাছের চেয়ে বেশি, কারণ গাছ split বাছার সময় সেগুলো উপেক্ষা করতে পারে। তিন, ডেটার কলামগুলো এলোমেলোভাবে ঘোরালে (rotation) গাছের ফল খারাপ হয়, MLP-এর হয় না; অর্থাৎ MLP কলামের নিজস্ব অর্থের সুবিধা নেয় না, অথচ টেবিলে সেই অর্থই মূল্যবান।

Shwartz-Ziv আর Armon (২০২২) আরও একটি বাস্তব দিক দেখান: নতুন deep tabular model-গুলো নিজেদের প্রবন্ধে ভালো দেখালেও অন্য ডেটাসেটে প্রায়ই XGBoost-এর পেছনে ছিল, আর সেগুলোর tuning-এ অনেক বেশি সময় লেগেছিল। এটি benchmark-এ “নিজের মাঠে খেলা”র পরিচিত সমস্যা।

তবে উত্তর চিরস্থায়ী নয়। খুব বড় টেবিল, টেবিলের সঙ্গে লেখা বা ছবি মেশানো ডেটা, বা আগে-থেকে-শেখানো tabular model-এর মতো নতুন দিক (TabPFN) পরিস্থিতি বদলাতে পারে; McElfresh ও সহকর্মীদের ২০২৩ সালের তুলনা দেখায় অনেক ডেটাসেটে পার্থক্য নগণ্য। এই পর্বের ডেস্কটপ run-এ MLP ৪,০০০ সারিতে তৃতীয় হয়েছিল, কিন্তু কোনো আকারে সেরা নয়।

মূল উপলব্ধি: model-এর গঠন যখন ডেটার গঠনের সঙ্গে মেলে তখনই সে জেতে; টেবিলের অনিয়মিত, অর্থবহ কলামে গাছের “x ≤ সীমা” প্রশ্ন স্বাভাবিকভাবে মানায়।

প্র ০২“১৭৯টি classifier-এর মধ্যে random forest সেরা” — এমন বড় benchmark-এর রায় কতটা বিশ্বাস করবেন?

২০১৪ সালে Manuel Fernández-Delgado ও সহকর্মীরা JMLR-এ “Do we need hundreds of classifiers to solve real world classification problems?” প্রকাশ করেন: শতাধিক ডেটাসেটে ১৭৯টি classifier, আর শীর্ষে random forest, কাছেই Gaussian kernel SVM. প্রবন্ধটি খুব জনপ্রিয় হয়, আর “সন্দেহ হলে random forest” প্রায় প্রবাদ হয়ে যায়।

২০১৬ সালে Michael Wainberg, Babak Alipanahi আর Brendan Frey একই জার্নালে একটি মন্তব্য প্রকাশ করেন। তাঁরা দেখান, মূল তুলনায় hyperparameter বাছাইয়ের একটি অংশে মূল্যায়নের ডেটা ব্যবহৃত হয়েছিল, যা কিছু model-কে অন্যায্য সুবিধা দেয়; ত্রুটি শোধরালে শীর্ষের কয়েকটি classifier-এর মধ্যে পার্থক্য পরিসংখ্যানগতভাবে আলাদা করা কঠিন হয়ে যায়। অর্থাৎ “সেরা” দাবিটি প্রায়ই “শীর্ষ কয়েকজনের একজন”-এ নেমে আসে।

এর পেছনে এই পর্বের প্রতিটি নিয়ম কাজ করে। অসম tuning বাজেট (অংশ ৫-এর ডেস্কটপ টেবিলে দেখেছেন, শুধু বাজেট বদলে boosting শেষ থেকে শীর্ষে ওঠে)। মূল্যায়নের ডেটা বাছাইয়ে ঢুকে পড়া। আর অনেক ডেটাসেটের গড় র‍্যাঙ্ক, যা আপনার নির্দিষ্ট ডেটা সম্পর্কে সরাসরি কিছু বলে না। অনেক ডেটাসেটে অনেক model-এর তুলনার সঠিক পরিসংখ্যান (Friedman test, critical difference) পর্ব ২২-এ দেখেছেন।

তাহলে benchmark কী কাজে লাগে? একটি ভালো prior হিসেবে: কোন দুই-তিনটি পরিবার দিয়ে শুরু করবেন, তা ঠিক করতে। কিন্তু চূড়ান্ত রায় আপনার ডেটায়, সমান বাজেটে, একই fold-এ। Benchmark-এর পদ্ধতিও পড়ে নিন: tuning কীভাবে, test কীভাবে আলাদা, কোন ধরনের ডেটা বেশি।

মূল উপলব্ধি: বড় benchmark-এর রায় শুরুর বিন্দু, শেষ কথা নয়; তার পদ্ধতির একটি ছোট অসমতাও র‍্যাঙ্ক উল্টে দিতে পারে।

প্র ০৩Grid search, random search, Bayesian optimization না Hyperband — tuning-এর বাজেট কীভাবে খরচ করবেন?

Grid search সহজ আর পুনরুৎপাদনযোগ্য, কিন্তু অপচয়ী। চারটি hyperparameter-এ প্রতিটির পাঁচটি মান মানে ৬২৫টি configuration. Bergstra আর Bengio (JMLR ২০১২) দেখান, বেশিরভাগ সমস্যায় অল্প কয়েকটি hyperparameter-ই আসলে ফল ঠিক করে (তাঁদের ভাষায় কম “কার্যকর মাত্রা”)। Grid সেই গুরুত্বপূর্ণ মাত্রায় মাত্র পাঁচটি মান দেখে, অথচ random search একই বাজেটে প্রতিটি চেষ্টায় নতুন মান দেখে। তাই একই বাজেটে random search প্রায়ই ভালো, আর বাজেট যেকোনো সময় থামানো যায়।

Bayesian optimization (বিস্তারিত পর্ব ৪৯-এ) আগের ফল দেখে পরের চেষ্টা বাছে। Optuna-র TPE ভালো আর খারাপ চেষ্টার ঘনত্ব আলাদা আন্দাজ করে। লাভ সবচেয়ে বেশি যখন প্রতিটি চেষ্টা দামি আর search space বড়; সস্তা model-এ random search-এর সঙ্গে পার্থক্য প্রায়ই ছোট, অংশ ৫-এর ডেস্কটপ run-এর মতো।

Hyperband (Lisha Li ও সহকর্মীরা, JMLR ২০১৮) অন্য দিক থেকে বাঁচায়: অনেক candidate অল্প সম্পদে (কম গাছ, কম ডেটা) চালিয়ে খারাপগুলোকে আগেই বাদ দেয়, ভালোগুলোকে বেশি সম্পদ দেয়। scikit-learn-এর HalvingRandomSearchCV এই successive halving ধারণার কাছাকাছি, তবে এখনো experimental, আলাদা import লাগে।

ব্যবহারিক নিয়ম তিনটি। এক, বাজেট সময়ে মাপুন: অংশ ৫-এ ২০টি candidate-এ logistic ২ সেকেন্ড, forest এক মিনিট। দুই, তুলনায় প্রতিটি পরিবারকে সমান সময় দিন, নইলে বাজেটই বিজয়ী ঠিক করে। তিন, search-এর সেরা score আশাবাদী; চূড়ান্ত সংখ্যার জন্য nested CV বা তালাবন্ধ test. আর মনে রাখুন, প্রত্যাশিত সেরা দ্রুত সমতল হয়: প্রথম কয়েকটি চেষ্টাতেই বেশিরভাগ লাভ।

মূল উপলব্ধি: tuning-এর বাজেট model-এর অংশ; কোন model “ভালো” তা বলার সময় কত বাজেটে, সেটাও বলতে হবে।

প্র ০৪ঋণ বা বিমার মতো উচ্চ-ঝুঁকির সিদ্ধান্তে কি জটিল model বানিয়ে পরে ব্যাখ্যা করা উচিত, নাকি শুরু থেকেই ব্যাখ্যাযোগ্য model?

Cynthia Rudin ২০১৯ সালে Nature Machine Intelligence-এ একটি প্রভাবশালী প্রবন্ধে যুক্তি দেন: উচ্চ-ঝুঁকির সিদ্ধান্তে black box বানিয়ে পরে তাকে ব্যাখ্যার চেষ্টা না করে, শুরু থেকেই ব্যাখ্যাযোগ্য model ব্যবহার করা উচিত। তাঁর দাবি, ভালোভাবে তৈরি feature-এর টেবিলে ব্যাখ্যাযোগ্য model আর black box-এর accuracy-র পার্থক্য প্রায়ই ছোট, আর পরে দেওয়া ব্যাখ্যা মূল model-এর হুবহু আচরণ নয়, একটি আনুমানিক গল্প।

অন্য পক্ষের যুক্তিও শক্ত। কিছু ডেটায় boosting-এর লাভ বাস্তব, যেমন এই পর্বের ডেস্কটপ run-এ ৬৪,০০০ সারিতে ০.৮০৯ বনাম ০.৭৫৪. প্রতারণা ধরার মতো কাজে সেই ব্যবধান অনেক টাকার। আর monotone constraint, ছোট গভীরতার গাছ, বা GLM-এর মতো কিন্তু অ-রৈখিক additive model (যেমন explainable boosting machine) মাঝামাঝি পথ দেয়।

ঋণের scorecard ঐতিহ্যগতভাবে logistic regression-ভিত্তিক, কারণ প্রতিটি পয়েন্ট একটি কারণের সঙ্গে বাঁধা, আর আবেদনকারীকে প্রত্যাখ্যানের কারণ বলা যায়। বিভিন্ন দেশের নিয়ন্ত্রক কী চান তা দেশ আর আইনের ব্যাখ্যার ওপর নির্ভর করে; এখানে কোনো আইনি দাবি নয়।

বাজারঘরের দৃষ্টিতে প্রশ্নটি হলো: ভুলের দাম কে দেয়? churn-এর অফারে ভুল হলে কিছু টাকা নষ্ট। কিন্তু কোনো গ্রাহকের cash on delivery সুবিধা বন্ধ করার মতো সিদ্ধান্তে কারণ বলতে পারা জরুরি। সেখানে আগে linear বা monotone model, আর জটিল model শুধু তখন, যখন সমান বাজেটের সৎ তুলনায় তার লাভ স্পষ্ট আর তার ব্যাখ্যা যাচাই করা যায়। ন্যায্যতার প্রশ্ন পর্ব ৬০-এ আসবে।

মূল উপলব্ধি: ব্যাখ্যাযোগ্যতা model বাছাইয়ের একটি শর্ত, পরে জোড়া দেওয়ার জিনিস নয়; accuracy-র লাভ মেপে তবেই তা ছাড়ার সিদ্ধান্ত।

প্র ০৫প্রতিযোগিতার leaderboard-এ জেতা বিশাল ensemble বাস্তবের product-এ কেন প্রায়ই চলে না?

সবচেয়ে পরিচিত উদাহরণ Netflix Prize (২০০৬–২০০৯)। বিজয়ী দলের সমাধান ছিল শত শত model-এর মিশ্রণ। কোম্পানির প্রকৌশল ব্লগে ২০১২ সালে জানানো হয়, পুরো বিজয়ী ensemble তারা চালু করেনি: বাড়তি accuracy-র তুলনায় প্রকৌশলের খরচ বেশি মনে হয়েছিল, আর ততদিনে ব্যবসার প্রয়োজনও বদলে গিয়েছিল। এই ঘটনা প্রায়ই উদ্ধৃত হয়; বিস্তারিত তাদের নিজস্ব বিবরণের ওপর নির্ভর করে।

কারণগুলো এই পর্বের মাপের সঙ্গে মেলে। ৬৪,০০০ সারিতে forest-এর ফাইল ২৭ MB, boosting-এর ০.২ MB, logistic-এর ৩ KB. প্রতিটি model-এর fit time, predict-এর latency, memory আর আবার শেখানোর সময় যোগ হয়। প্রতিটি model-এর নিজের প্রস্তুতি, নিজের version, নিজের নজরদারি লাগে। D. Sculley ও সহকর্মীরা (NIPS ২০১৫, “Hidden Technical Debt in Machine Learning Systems”) দেখান, বাস্তবের ML system-এ model-এর কোড ছোট অংশ; চারপাশের ডেটা, configuration আর নজরদারির জটিলতাই দীর্ঘমেয়াদি দাম।

পর্ব ৩৭ (Stacking, blending আর voting — ensemble কখন সত্যিই সাহায্য করে)-এ দেখেছেন, একটি ভালোভাবে tune করা boosting-এর ওপর ensemble-এর লাভ প্রায়ই ছোট। Leaderboard-এ ০.০০১-এর পার্থক্য র‍্যাঙ্ক বদলায়, কিন্তু বাস্তবে তা fold-এর ওঠানামার ভেতরে পড়ে, যেমন অংশ ৩-এ ০.০০২৪ পার্থক্যের p ছিল ০.৯২.

তাই production-এর সিদ্ধান্তের টেবিলে AUC-এর পাশে আরও কলাম থাকা উচিত: fit time, predict latency, ফাইলের আকার, ব্যাখ্যা, আর দলের কেউ এটি এক বছর পরে রক্ষণাবেক্ষণ করতে পারবে কি না। Production-এর নজরদারি পর্ব ৬৯-এ আসবে।

মূল উপলব্ধি: leaderboard একটি সংখ্যা মাপে, product অনেকগুলো; সমান accuracy-তে সবচেয়ে সরল, সস্তা আর রক্ষণাবেক্ষণযোগ্য model-ই বাস্তবে জেতে।

অনুশীলন

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

  1. পূর্ণ numerical: একই ৫-fold-এ (প্রতিটি fold-এ test ২০%) boosting আর logistic-এর AUC-এর জোড়া পার্থক্য: +০.০১২, +০.০২০, −০.০০৪, +০.০১৬, +০.০০৬. (ক) গড় আর sd বের করুন। (খ) naive t আর Nadeau–Bengio corrected t. (গ) t0.975, 4 = ২.৭৭৬ ধরে দুটির সিদ্ধান্ত কী? (ঘ) আলাদা একটি প্রশ্ন: ১০টি candidate-এর ৪টি baseline-এর নিচে; বাজেট ২-এ অন্তত একটি baseline ছাড়ানোর সম্ভাবনা কত?

    (ক) যোগফল ০.০৫০, গড় d̄ = ০.০১০. গড় থেকে বিচ্যুতি: ০.০০২, ০.০১০, −০.০১৪, ০.০০৬, −০.০০৪; বর্গের যোগ ০.০০০০০৪ + ০.০০০১ + ০.০০০১৯৬ + ০.০০০০৩৬ + ০.০০০০১৬ = ০.০০০৩৫২. s² = ০.০০০৩৫২/৪ = ০.০০০০৮৮, s = ০.০০৯৪.

    (খ) Naive: SE = ০.০০৯৪/√৫ = ০.০০৪২, t = ০.০১০/০.০০৪২ ≈ ২.৩৮. Corrected: গুণক ১/৫ + ০.২/০.৮ = ০.২ + ০.২৫ = ০.৪৫; SE = √(০.০০০০৮৮ × ০.৪৫) = √০.০০০০৩৯৬ ≈ ০.০০৬৩, t ≈ ১.৫৯.

    (গ) দুটিই ২.৭৭৬-এর কম, তাই ৫% স্তরে কোনোটিতেই পার্থক্যের প্রমাণ নেই। লক্ষ করুন, sd ছোট হলেও (fold-এর ৫টির ৪টিতে boosting এগিয়ে) মাত্র পাঁচটি fold-এ প্রমাণ দুর্বল; repeated CV বা বড় test দরকার।

    (ঘ) ১ − C(৪,২)/C(১০,২) = ১ − ৬/৪৫ = ০.৮৭.

  2. সমালোচনা ও নকশা: একজন সহকর্মী লিখেছেন: “পুরো ডেটায় StandardScaler আর OneHotEncoder চালিয়ে train/test ভাগ করেছি। XGBoost-কে GridSearchCV-তে ২০০টি combination দিয়েছি, logistic ডিফল্টে। একই test set-এ XGBoost ০.৮১২, logistic ০.৭৯৮, তাই XGBoost জিতেছে; সেরা combination-ও test দেখে বেছেছি। জেলার ৬৪টি মান LabelEncoder দিয়ে logistic-এ দিয়েছি।” কী কী সমস্যা, আর সঠিক নকশা কী?

    সমস্যা: (১) ভাগের আগে পুরো ডেটায় scaler আর encoder: test-এর গড়, sd আর category-তালিকা train-এ ঢুকেছে (leakage). (২) বাজেট অসম: ২০০ বনাম ০. (৩) Test দেখে combination বাছা হয়েছে, তাই ০.৮১২ আশাবাদী, আর test আর “তালাবন্ধ” নেই। (৪) একটি split-এ একটি করে সংখ্যা; ০.০১৪ পার্থক্য fold-এর ওঠানামার ভেতরে হতে পারে। (৫) LabelEncoder জেলাকে ০–৬৩ সংখ্যা বানায়; logistic তখন মিথ্যা ক্রম আর রৈখিক প্রভাব শেখে। (৬) Boosting-এ scaling বা one-hot দরকারই ছিল না।

    সঠিক নকশা: প্রথমে তালাবন্ধ test আলাদা। তারপর একটি RepeatedStratifiedKFold object সবার জন্য; logistic-এর জন্য ColumnTransformer (imputer + scaler + one-hot বা TargetEncoder) pipeline-এর ভেতরে; boosting-এর জন্য native category. প্রত্যেককে একই বাজেটের RandomizedSearchCV, বা আরও ভালো, nested CV. শীর্ষ দুটির মধ্যে corrected t. শেষে নির্বাচিত model তালাবন্ধ test-এ একবার।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    from sklearn.linear_model import LinearRegression
    from sklearn.tree import DecisionTreeRegressor
    x = np.arange(1, 11).reshape(-1, 1)
    y = 2.0 * x.ravel()
    new = np.array([[5.5], [20]])
    print(LinearRegression().fit(x, y).predict(new).round(2))
    print(DecisionTreeRegressor(random_state=0).fit(x, y).predict(new))
    [11. 40.]
    [10. 20.]

    Linear regression y = ২x হুবহু শেখে, তাই ৫.৫-এ ১১, আর ২০-এ (পরিসরের অনেক বাইরে) ৪০. গাছ প্রতিটি x-কে আলাদা পাতায় রাখে, সীমাগুলো পাশাপাশি দুই x-এর মাঝখানে (১.৫, ২.৫, …)। ৫.৫ ঠিক সীমার ওপর পড়ে, আর scikit-learn-এ “x ≤ সীমা” বাঁ দিকে যায়, তাই x = ৫-এর পাতা: ১০. আর ২০ সবচেয়ে ডানের পাতায় যায়, x = ১০-এর: ২০, অর্থাৎ শেষ দেখা মান। (ডেস্কটপে scikit-learn 1.8.0-এ চালিয়ে মেলানো।)

  4. কোড লিখুন: একটি search-এর ১২টি candidate-এর CV AUC: ০.৭৭০, ০.৭৪০, ০.৭৪৬, ০.৭৭৬, ০.৭৭২, ০.৭৬৭, ০.৭৮২, ০.৭৭৬, ০.৭৮১, ০.৭৪৪, ০.৭৬১, ০.৭৪৪; baseline ০.৭৬৯. বাজেট k = ১, ৩, ৬-এর প্রতিটিতে ২০,০০০ বার এলোমেলো k টি (পুনরাবৃত্তি ছাড়া) টেনে সেরার গড় আর baseline ছাড়ানোর হার simulation-এ বের করুন, আর অংশ ৫-এর দুটি সূত্রের সঙ্গে মেলান।
    import numpy as np
    from math import comb
    sc = np.array([0.770, 0.740, 0.746, 0.776, 0.772, 0.767,
                   0.782, 0.776, 0.781, 0.744, 0.761, 0.744])
    base, n = 0.769, len(sc)
    rng = np.random.default_rng(0)
    v = np.sort(sc)
    m = (v <= base).sum()
    for k in (1, 3, 6):
        sims = np.array([rng.choice(sc, k, replace=False).max() for _ in range(20000)])
        exact = sum(v[i] * comb(i, k - 1) for i in range(n)) / comb(n, k)
        print(f"k = {k}: simulated E[best] {sims.mean():.4f}, formula {exact:.4f} | "
              f"P(beat) sim {np.mean(sims > base):.3f}, formula {1 - comb(m, k) / comb(n, k):.3f}")

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

    k = 1: simulated E[best] 0.7633, formula 0.7633 | P(beat) sim 0.500, formula 0.500
    k = 3: simulated E[best] 0.7764, formula 0.7764 | P(beat) sim 0.909, formula 0.909
    k = 6: simulated E[best] 0.7802, formula 0.7802 | P(beat) sim 0.999, formula 0.999

    Simulation আর সূত্র চার দশমিক পর্যন্ত মেলে। লক্ষ করুন v[i]-এর নিচে ঠিক i টি মান (০ থেকে গোনা), তাই সূত্রে C(i, k − 1); অংশ ৫-এর লেখায় ১ থেকে গোনা বলে C(i − 1, k − 1).

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

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

ক্রমTabular ডেটায় কয়েকটি model পরিবারের সৎ তুলনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একটি তালাবন্ধ test set আলাদা করা
  2. সব model-এর জন্য একটি repeated stratified CV object ঠিক করা
  3. প্রতিটি পরিবারের প্রস্তুতি pipeline-এর ভেতরে সাজানো
  4. প্রতিটি পরিবারকে একই tuning বাজেট দেওয়া
  5. mean ± sd, সময় আর আকার লেখা, শীর্ষ দুটিতে corrected t-test
  6. নির্বাচিত model তালাবন্ধ test-এ একবার মাপা

একটি টেবিলে সংখ্যার কলাম, ফাঁকা মান আর category আছে। কোন model-এর জন্য scikit-learn 1.8-এ কোনো imputation, scaling বা encoding ছাড়াই (category কলাম pandas-এর category dtype হলে) ডেটা সরাসরি দেওয়া যায়?

  • LogisticRegression
  • KNeighborsClassifier
  • HistGradientBoostingClassifier
  • MLPClassifier

Random forest-এর আগে StandardScaler দেওয়া প্রায় অর্থহীন কেন?

  • Forest সব feature একসঙ্গে ব্যবহার করে না বলে
  • গাছের split শুধু ক্রমের ওপর নির্ভর করে, আর scaling ক্রম বদলায় না
  • StandardScaler forest-এ error দেয়
  • Forest কখনো overfit করে না বলে

একই ৩-fold × ২ repeated CV-তে দুটি model-এর ছয়টি জোড়া পার্থক্যের sd s. Nadeau–Bengio corrected t-এর হরে কোন standard error?

  • s / √6
  • s
  • s × √(1/6 + 1/3)
  • s × √(1/6 + 1/2)

ডেলিভারির সময়ের model শেখানো হয়েছে শুধু ২০ কিলোমিটারের ভেতরের অর্ডারে। ৩৫ কিলোমিটারের নতুন এলাকায় random forest কী অনুমান করবে বলে আশা করা যায়?

  • প্রায় ২০ কিলোমিটারের অর্ডারের সময়, কারণ গাছ train-এর পরিসরের বাইরে যেতে পারে না
  • দূরত্বের সঙ্গে রৈখিকভাবে বাড়ানো সময়
  • NaN, কারণ মানটি অজানা
  • শূন্য

১২টি এলোমেলো hyperparameter candidate-এর ৬টির CV score baseline-এর সমান বা নিচে। এদের থেকে এলোমেলো ৩টি চেষ্টা করলে (বাজেট ৩) অন্তত একটি baseline ছাড়ানোর সম্ভাবনা কত?

  • ০.৫০
  • প্রায় ০.৯১
  • ০.৮৭৫
  • ১.০০

বাজারঘরের একটি নতুন সেবায় মাত্র ৮০ জন গ্রাহক, চারটি সংখ্যার feature, প্রভাব মোটামুটি মসৃণ। কোনটি সবচেয়ে যুক্তিসঙ্গত প্রথম model?

  • HistGradientBoosting, ১০০টি candidate-এর search-সহ
  • একটি MLP, কারণ এটি সবচেয়ে নমনীয়
  • Regularized logistic regression, repeated CV-সহ
  • k = ১-এর kNN, কোনো CV ছাড়া

RandomizedSearchCV-এর best_score_ ০.৭৮২ এল। এটিকে model-এর ভবিষ্যৎ AUC হিসেবে রিপোর্ট করা কেন ঠিক নয়?

  • অনেক candidate থেকে একই fold-এ সেরাটি বাছা হয়েছে, তাই সংখ্যাটি আশাবাদী
  • এটি train-এর score
  • RandomizedSearchCV ROC AUC মাপতে পারে না
  • random_state স্থির থাকলে সংখ্যাটি অর্থহীন

শব্দকোষ

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

মনে রাখুন

  1. Baseline আগে: dummy (AUC ০.৫) আর regularized linear; জটিল model-কে প্রমাণ করতে হবে যে সে সত্যিই ভালো।
  2. পরিবার অনুযায়ী প্রস্তুতি: linear/kNN/SVM/MLP-এ ভরাট + scale + one-hot; forest-এ ordinal; HistGradientBoosting-এ কিছুই না; সব pipeline-এর ভেতরে।
  3. সৎ তুলনা: একই repeated stratified fold, একই metric, একই বাজেট; mean ± sd, সময় আর আকার; শীর্ষ দুটিতে corrected t (গুণক 1/J + ntest/ntrain).
  4. বাজেট: প্রত্যাশিত সেরা দ্রুত সমতল হয়; অসম বাজেট র‍্যাঙ্ক উল্টে দেয়; search-এর সেরা score আশাবাদী।
  5. কে কোথায়: বড়, মিশ্র টেবিলে boosting প্রায়ই সেরা; ছোট n, extrapolation, ব্যাখ্যা বা একমুখী শর্তে linear; forest শক্ত ডিফল্ট; neural network টেবিলে প্রায়ই পিছিয়ে, তবে ক্ষেত্রটি বদলাচ্ছে।

পরের পর্বে: মডিউল ৭ এখানে শেষ। পর্ব ৩৯ (Gaussian mixture আর EM algorithm — নরম clustering) দিয়ে মডিউল ৮ শুরু: label ছাড়া ডেটায় দল খোঁজা, যেখানে প্রতিটি গ্রাহক একাধিক দলের অংশ হতে পারেন, সম্ভাবনাসহ।

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

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