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

XGBoost, LightGBM আর CatBoost — পার্থক্য আর হাতে-কলমে

XGBoost, LightGBM and CatBoost — Second-Order Boosting, Histogram Splits, Categorical Features, Constraints and a Hands-on Comparison

Taught in Bangla, this lesson derives XGBoost's leaf weight and split gain from a second-order expansion and checks them by brute force. In-browser HistGradientBoosting runs cover histogram splits, native categoricals, monotonic and interaction constraints and early stopping; LightGBM's leaf-wise growth, GOSS and CatBoost's ordered target statistics are traced, and a real desktop run compares XGBoost, LightGBM and CatBoost.

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

  • Loss-কে দ্বিতীয় ক্রম পর্যন্ত বিস্তার করে XGBoost-এর leaf weight আর split gain derive করা, আর ছোট টেবিলে হাতে হিসাব করা
  • Histogram দিয়ে split খোঁজা কেন দ্রুত, তার দাম কী, আর missing value ও categorical feature কীভাবে সামলানো হয়
  • ব্রাউজারে HistGradientBoostingClassifier দিয়ে native categorical বনাম one-hot, max_bins, monotonic আর interaction constraint, আর validation set দিয়ে early stopping
  • LightGBM-এর leaf-wise growth, GOSS, EFB আর CatBoost-এর ordered target statistics ধাপে ধাপে বোঝা
  • XGBoost, LightGBM আর CatBoost-এর সত্যিকারের run পড়া, আর তিন লাইব্রেরির hyperparameter একটি টেবিলে মেলানো

পর্ব ৩৫ (Gradient boosting — residual থেকে functional gradient descent)-এ দেখেছেন, gradient boosting প্রতিটি ধাপে loss-এর ঢালের উল্টো দিকে একটি ছোট গাছ যোগ করে। এই পর্ব সেই ধারণার তিনটি জনপ্রিয় শিল্প-মানের রূপ নিয়ে: XGBoost, LightGBM আর CatBoost. প্রথমে গণিত (দুটি সূত্র, হাতে ও কোডে), তারপর গতি কোথা থেকে আসে, তারপর প্রতিটি লাইব্রেরির নিজস্ব কৌশল, শেষে একই ডেটায় সত্যিকারের তুলনা। AI-এর ভিত্তি কোর্সের পর্ব ৬৬-এ boosting প্রথমবার দেখেছেন; এখানে তার ভেতরটা খুলে দেখা। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, scikit-learn 1.8.0); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে।

১ · একই পরিবারের তিন সদস্য — gradient থেকে Newton boosting

বাজারে একজন দোকানি দাম ঠিক করছেন। শুধু “দাম কমালে বিক্রি বাড়ে” জানা থাকলে তিনি আন্দাজে ৫ টাকা করে কমান: এটি gradient, শুধু দিক। অভিজ্ঞ দোকানি আরও জানেন ক্রেতারা দামের প্রতি কতটা সংবেদনশীল; সংবেদনশীলতা বেশি হলে অল্প কমালেই যথেষ্ট, কম হলে বেশি কমাতে হয়। এই দ্বিতীয় তথ্যটি হলো বক্রতা, Hessian. XGBoost-জাতীয় boosting দুটিই ব্যবহার করে প্রতিটি পাতার মান একবারে ঠিক করে।

ধরুন এখন পর্যন্ত শেখা model প্রতিটি গ্রাহক i-এর জন্য একটি raw score F(xi) দেয় (log-odds), আর সম্ভাবনা pi = σ(F) (sigmoid ফাংশন দিয়ে)। নতুন গাছ f যোগ করলে প্রতিটি সারির loss হয় ℓ(yi, F + f). একে f-এর চারপাশে দ্বিতীয় ক্রম পর্যন্ত Taylor বিস্তার করুন:

ℓ(y, F + f) ≈ ℓ(y, F) + g·f + ½·h·f²,   g = ∂ℓ/∂F,   h = ∂²ℓ/∂F²

এখানে g হলো প্রথম আংশিক অবকলজ (ঢাল), h দ্বিতীয় (বক্রতা)। Log loss ℓ = −[y log p + (১ − y) log(১ − p)]-এর জন্য, F-এর সাপেক্ষে অবকলন করলে খুব সরল ফল আসে: g = p − y আর h = p(১ − p). অর্থাৎ g হলো “ভুলের পরিমাণ” আর h হলো “এই সারিটি নিয়ে model কতটা অনিশ্চিত” (p = ০.৫-এ h সবচেয়ে বড়, ০.২৫)। Squared loss ½(y − F)²-এ g = F − y আর h = ১, তাই সেখানে নতুন কিছু নেই; পার্থক্য দেখা যায় log loss-এর মতো loss-এ।

শুধু g ব্যবহার করা হলো সাধারণ gradient boosting (Jerome Friedman, ২০০১): গাছ −g-কে লক্ষ্য ধরে শেখে, তারপর পাতার মান আলাদা করে ঠিক হয়। g আর h দুটি ব্যবহার করলে প্রতিটি পাতায় একটি Newton ধাপ হয়, যা পর্ব ১৫-এ দেখেছেন: মান = −(ঢালের যোগফল)/(বক্রতার যোগফল)। একে বলা হয় Newton boosting বা second-order boosting. ধারণাটি নতুন নয়; Friedman, Hastie আর Tibshirani-র ২০০০ সালের LogitBoost-এ Newton ধাপ ছিল বলে সাধারণত উল্লেখ করা হয়। Tianqi Chen আর Carlos Guestrin-এর XGBoost (KDD ২০১৬ প্রবন্ধ, লাইব্রেরি তার আগে থেকেই জনপ্রিয়) এর সঙ্গে যোগ করে regularization আর দ্রুত বাস্তবায়নের প্রকৌশল।

XGBoost

Regularized objective (λ, γ), missing value-এর জন্য শেখা “default direction”, আর দ্রুত histogram পদ্ধতি (tree_method="hist", ২.০ থেকে ডিফল্ট)। গাছ বাড়ে স্তর ধরে, গভীরতা max_depth (ডিফল্ট ৬)।

LightGBM

Guolin Ke ও সহকর্মীরা, Microsoft (NIPS ২০১৭)। Histogram, পাতা ধরে গাছ বাড়ানো (num_leaves, ডিফল্ট ৩১), আর ডেটা কমানোর দুটি কৌশল: GOSS আর EFB.

CatBoost

Liudmila Prokhorenkova ও সহকর্মীরা, Yandex (NeurIPS ২০১৮)। Categorical feature-এর জন্য ordered target statistics, ordered boosting, আর প্রতি স্তরে একই প্রশ্নের symmetric গাছ।

HistGradientBoosting

scikit-learn-এর নিজস্ব, LightGBM-এর ধারণায় অনুপ্রাণিত histogram boosting. এই পাতার সব চালানো যায় এমন কোড এটি দিয়ে, কারণ এটি ব্রাউজারে নীরবে চলে (অংশ ৮-এ কারণ)।

২ · Regularized objective — leaf weight আর split gain

একটি গাছ f-এর T টি পাতা, পাতা j-এর মান (weight) wj; সারি i যে পাতায় পড়ে, f(xi) সেই পাতার w. XGBoost যে objective কমায়:

Obj = ∑i [gi f(xi) + ½ hi f(xi)²] + γT + ½λ ∑j wj²

প্রথম অংশ Taylor বিস্তারের loss (ধ্রুবক বাদ), γT প্রতিটি নতুন পাতার জন্য জরিমানা, আর ½λ∑w² পাতার মানের L2 regularization. একটি পাতার সব সারি একই w পায়, তাই পাতা j-এর জন্য Gj = ঐ পাতার সব g-এর যোগফল আর Hj = সব h-এর যোগফল লিখলে পাতাটির অংশ দাঁড়ায় একটি সাধারণ দ্বিঘাত:

Gj w + ½ (Hj + λ) w²  ⟹  wj* = −Gj / (Hj + λ),   সর্বনিম্ন মান = −½ Gj² / (Hj + λ)

w-এর সাপেক্ষে অবকলন শূন্য ধরলেই প্রথম সূত্র (leaf weight-এর সূত্র)। পুরো গাছের সেরা objective = −½∑j Gj²/(Hj + λ) + γT. এখন একটি পাতাকে বাঁ (L) আর ডান (R)-এ ভাগ করলে objective কতটা কমে, সেটিই split gain:

Gain = ½ [ GL²/(HL+λ) + GR²/(HR+λ) − (GL+GR)²/(HL+HR+λ) ] − γ

Gain ধনাত্মক হলেই ভাগ লাভজনক। তিনটি নিয়ন্ত্রক এখান থেকেই অর্থ পায়: λ (XGBoost-এ reg_lambda, ডিফল্ট ১) হর বাড়িয়ে পাতার মান শূন্যের দিকে টানে, বিশেষ করে যে পাতায় H ছোট; γ (gamma, ডিফল্ট ০) প্রতিটি ভাগের ন্যূনতম লাভ, যা গাছ ছাঁটে; আর min_child_weight (ডিফল্ট ১) একটি পাতায় H-এর ন্যূনতম যোগফল। Log loss-এ h ≤ ০.২৫, তাই H হলো “কার্যকর সারির সংখ্যা”-র মতো: নিশ্চিত সারিগুলো (p ≈ ০ বা ১) প্রায় কিছুই যোগ করে না।

নিচে পাঁচজন গ্রাহকের ছোট টেবিলে পুরো হিসাব: feature x = ডেলিভারি চার্জ (টাকা), লক্ষ্য y = churn করেছেন (১) কি না, λ = ১, γ = ০, শুরুতে সবার raw score ০ অর্থাৎ p = ০.৫.

w* = −G/(H+λ), Gain = ½[ΣL + ΣR − Σroot]λ = 1, γ = 0

            
  1. শুরু। কোনো গাছ নেই, সবার raw score ০, তাই সবার জন্য p = ০.৫. পাঁচজনের তিনজন churn করেছেন।
  2. g আর h. churn করা সারিতে g = ০.৫ − ১ = −০.৫, বাকিদের +০.৫. সবার h = ০.৫ × ০.৫ = ০.২৫. যোগফল G = −০.৫, H = ১.২৫.
  3. একটি পাতা হলে। w* = ০.৫/২.২৫ = +০.২২২২: সবার score একটু বাড়ানো, কারণ churn-এর হার (৬০%) অনুমানের (৫০%) চেয়ে বেশি। Objective −০.০৫৫৬.
  4. একটি ভাগ: x ≤ ৬০. বাঁ দিকে তিনজন (একজন churn), ডান দিকে দুজন (দুজনই churn করেছেন)। প্রতিটি দিকের G আর H আলাদা যোগ করে G²/(H + λ) হিসাব।
  5. Gain. ০.৩৪৯২. চারটি সম্ভাব্য ভাগের মধ্যে এটিই সবচেয়ে বড়; x ≤ ৪০-এর লাভ মাত্র ০.০১৫৯, কারণ দুই দিকেই churn মেশানো।
  6. পাতার মান। বাঁ পাতা −০.২৮৫৭ (কম churn), ডান +০.৬৬৬৭. পুরো মান না দিয়ে learning rate ০.৩ (XGBoost-এর ডিফল্ট eta) দিয়ে গুণ করে যোগ, তাই নতুন p ০.৪৭৮৬ আর ০.৫৪৯৮.
  7. γ-এর কাজ। γ = ০.৪ হলে লাভ জরিমানার চেয়ে কম, ভাগটি বাতিল। γ এভাবে সরাসরি গাছ ছাঁটে।

৩ · হাতে-কলমে — g, h, w* আর gain কোডে, brute force দিয়ে যাচাই

সূত্র মুখস্থ করার চেয়ে যাচাই করা ভালো। প্রথম ব্লক একই পাঁচ সারিতে g, h, root-এর w*, চারটি ভাগের gain হিসাব করে। তারপর সেরা ভাগের দুই পাতার জন্য w-কে সূত্র ছাড়াই scipy.optimize.minimize_scalar দিয়ে সরাসরি খোঁজে (brute force), দুটি objective-এ: দ্বিতীয় ক্রমের আনুমানিক objective আর আসল log loss + ½λw².

import numpy as np
from scipy.optimize import minimize_scalar

x = np.array([20, 40, 60, 80, 100])      # ডেলিভারি চার্জ (টাকা)
y = np.array([0, 1, 0, 1, 1])            # churn করেছেন কি না
lam = 1.0                                # পাতার weight-এর L2 শাস্তি λ
p = np.full(5, 0.5)                      # শুরু: raw score 0, তাই p = 0.5
g, h = p - y, p * (1 - p)                # log loss-এর gradient আর Hessian
print("g =", g, " h =", h)

def w_star(m): return -g[m].sum() / (h[m].sum() + lam)
def score(m):  return g[m].sum() ** 2 / (h[m].sum() + lam)

root = np.ones(5, bool)
print(f"root: G = {g.sum():+.2f}, H = {h.sum():.2f}, w* = {w_star(root):+.4f}")
for t in (30, 50, 70, 90):               # পাশাপাশি মানের মাঝখানে threshold
    L = x < t
    gain = 0.5 * (score(L) + score(~L) - score(root))
    print(f"split x < {t}: gain = {gain:.4f}")

def obj2(w, m):                          # দ্বিতীয় ক্রমের আনুমানিক objective
    return np.sum(g[m] * w + 0.5 * h[m] * w * w) + 0.5 * lam * w * w
def obj_exact(w, m):                     # আসল log loss + ½λw²
    q = 1 / (1 + np.exp(-w))
    return -np.sum(y[m] * np.log(q) + (1 - y[m]) * np.log(1 - q)) + 0.5 * lam * w * w

L = x < 70
for name, m in (("left ", L), ("right", ~L)):
    wb = minimize_scalar(obj2, args=(m,)).x
    we = minimize_scalar(obj_exact, args=(m,)).x
    print(f"{name}: formula {w_star(m):+.4f}  brute force {wb:+.4f}  exact log loss {we:+.4f}")
drop = minimize_scalar(obj2, args=(root,)).fun - sum(minimize_scalar(obj2, args=(m,)).fun for m in (L, ~L))
print(f"objective drop by brute force = {drop:.4f}")

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

g = [ 0.5 -0.5  0.5 -0.5 -0.5]  h = [0.25 0.25 0.25 0.25 0.25]
root: G = -0.50, H = 1.25, w* = +0.2222
split x < 30: gain = 0.2944
split x < 50: gain = 0.0159
split x < 70: gain = 0.3492
split x < 90: gain = 0.0444
left : formula -0.2857  brute force -0.2857  exact log loss -0.2865
right: formula +0.6667  brute force +0.6667  exact log loss +0.6748
objective drop by brute force = 0.3492

প্রথম চার লাইন stepper-এর সংখ্যার সঙ্গে হুবহু মেলে। পরের দুই লাইন গুরুত্বপূর্ণ: দ্বিতীয় ক্রমের objective-এর brute-force সমাধান সূত্রের সঙ্গে চার দশমিক পর্যন্ত এক (−০.২৮৫৭, +০.৬৬৬৭), কারণ ওটি একটি নিখুঁত দ্বিঘাত। আসল log loss-এর সর্বনিম্ন বিন্দু একটু আলাদা (−০.২৮৬৫, +০.৬৭৪৮): Newton ধাপ একটি আনুমানিক ধাপ, ঠিক উত্তর নয়। পার্থক্য ছোট, আর পরের গাছগুলো বাকিটা সারায়। শেষ লাইন: দুই পাতার brute-force objective root-এর চেয়ে ০.৩৪৯২ কম, যা gain সূত্রের সঙ্গে হুবহু এক। অর্থাৎ gain সত্যিই “এই ভাগে objective কতটা কমে”।

৪ · Histogram দিয়ে split খোঁজা — খোপে ভাগ করলে কাজ কমে

সাধারণ (exact) পদ্ধতিতে প্রতিটি নোডে প্রতিটি feature সাজাতে হয় আর প্রতিটি পাশাপাশি জোড়ার মাঝখানে কাটা যাচাই করতে হয়: খরচ মোটামুটি n log n প্রতি feature (সাজানো একবার করে রাখলেও প্রতিটি নোডে n). পর্ব ৩২ (Decision tree গভীরে — CART, impurity, pruning আর missing value)-এ এই হিসাব দেখেছেন। Histogram পদ্ধতি আগে একবারই প্রতিটি feature-কে অল্প কয়েকটি খোপে (bin) ভাগ করে, সাধারণত quantile দিয়ে, আর প্রতিটি মানকে খোপের নম্বর (০–২৫৪, এক বাইট) দিয়ে বদলে রাখে। তারপর প্রতিটি নোডে:

  1. একবার সারিগুলো পেরিয়ে প্রতিটি খোপের G আর H যোগ করা (খরচ n);
  2. খোপগুলো বাঁ থেকে ডানে পেরিয়ে gain হিসাব (খরচ খোপের সংখ্যা B, সাধারণত ২৫৫, n-এর ওপর নির্ভর করে না)।

আরেকটি কৌশল: একটি সন্তান-নোডের histogram বানালে অন্যটির histogram = মা-নোডের histogram − প্রথমটি। তাই ছোট সন্তানেরটি বানিয়ে বড়টি বিয়োগ করে পাওয়া যায় (histogram subtraction, LightGBM আর scikit-learn দুটিতেই আছে)। দাম: কাটা শুধু খোপের সীমায় সম্ভব। নিচে আট সারিতে তার মানে দেখুন।

Gain = ½[G_L²/(H_L+λ) + G_R²/(H_R+λ) − G²/(H+λ)]λ = 1, p = 0.5 সবার

            
  1. ডেটা। আট গ্রাহক, চার্জ অনুযায়ী সাজানো; শেষ তিনজন churn করেছেন। সবার p = ০.৫, তাই g = ±০.৫ আর h = ০.২৫.
  2. খোপে ভাগ। max_bins = 4 হলে প্রতিটি খোপে প্রায় সমান সংখ্যক মান (quantile দিয়ে)। সীমা পাশাপাশি মানের মাঝখানে। এই কাজ শেখার শুরুতে একবারই হয়।
  3. Histogram. প্রতিটি খোপের g আর h যোগ। খোপ ২-এ একজন churn আর একজন নয়, তাই তার G = ০.
  4. কাটা খোঁজা। শুধু তিনটি খোপ-সীমা। বাঁ দিকের যোগফল জমাতে জমাতে gain: সেরা ≤ ৫২.৫, gain ১.০৮৩৩.
  5. Exact-এর সঙ্গে তুলনা। সব সাতটি কাটা দেখলে সেরা ৭৭.৫, gain ১.৮৬৫১: ঠিক যেখানে churn শুরু। কিন্তু ৬০ আর ৯৫ একই খোপে, তাই ৪ খোপে এই কাটা অসম্ভব।
  6. শিক্ষা। খোপ কম মানে দ্রুত আর কম memory, কিন্তু সীমা মোটা দাগের। বাস্তবে ২৫৫টি খোপে হাজার হাজার সারিতে এই ক্ষতি প্রায়ই নগণ্য, আর মোটা খোপ একটু regularization-এর কাজও করে।

Missing value. Histogram পদ্ধতিতে missing মানের জন্য একটি আলাদা খোপ থাকে। প্রতিটি কাটায় সেই খোপের G আর H একবার বাঁয়ে, একবার ডানে রেখে gain দেখা হয়, যেদিকে লাভ বেশি সেদিকেই missing যায়। XGBoost-এর প্রবন্ধ একে sparsity-aware split finding আর শেখা “default direction” বলে। scikit-learn-এর docs অনুযায়ী, train-এ কোনো feature-এ missing না থাকলে predict-এর সময় missing সারি যায় বেশি সারির সন্তানে। ফলে “রেটিং দেননি” নিজেই একটি সংকেত হতে পারে, আলাদা imputation ছাড়া।

Categorical feature. k টি category-কে দুই দলে ভাগ করার উপায় 2k−1 − ১, যা ৬৪টি জেলায় অসম্ভব বড়। কৌশল: নোডে প্রতিটি category-কে G/(H + λ)-এর মতো একটি মানে সাজানো, তারপর সেই ক্রমে সংখ্যার মতো কাটা খোঁজা। Squared loss-এ এই ক্রমে সেরা ভাগ পাওয়া যায় বলে Walter Fisher-এর ১৯৫৮ সালের ফল প্রায়ই উদ্ধৃত হয়; LightGBM আর scikit-learn এই ধরনের পদ্ধতি নেয়। কোনো one-hot কলাম লাগে না, কিন্তু অল্প সারির অনেক category-তে এটি overfitting-এর পথ খুলে দেয়; পরের অংশে মেপে দেখবেন।

৫ · HistGradientBoosting হাতে-কলমে — missing, categorical, max_bins আর সময়

এই পর্বের ডেটা: বাজারঘরের কাল্পনিক গ্রাহক, seed দিয়ে বানানো, কোনো আসল দোকানের নয়। লক্ষ্য: পরের মাসে গ্রাহক চলে যাবেন (churn) কি না। প্রায় ২০% গ্রাহক রেটিং দেননি (NaN), জেলা ৬৪টি কোডে (categorical), পেমেন্টের ধরন চারটি। সত্যিকারের নিয়মে churn ডেলিভারি চার্জের সঙ্গে বাড়ে, আর দেরি ও বেশি চার্জ একসঙ্গে হলে আরও বাড়ে।

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

ZILA = [f"Z{i:02d}" for i in range(1, 65)]        # ৬৪টি জেলার কোড
PAY = ["bKash", "Nagad", "card", "COD"]

def make_churn(n, seed=36):
    rng = np.random.default_rng(seed)
    tenure = rng.integers(1, 61, n)                 # কত মাসের গ্রাহক
    orders = rng.poisson(4, n)                      # গত ৯০ দিনের অর্ডার
    fee = rng.integers(0, 151, n).astype(float)     # ডেলিভারি চার্জ, 0–150 টাকা
    basket = np.round(rng.lognormal(6.6, 0.5, n))   # গড় অর্ডারের দাম (টাকা)
    late = rng.poisson(0.8, n)                      # দেরিতে পৌঁছানো অর্ডার
    rating = rng.integers(1, 6, n).astype(float)    # শেষ রেটিং 1–5
    zila = rng.integers(0, 64, n)
    pay = rng.choice(4, n, p=[0.35, 0.2, 0.15, 0.3])
    no_rating = rng.random(n) < 0.2                 # ২০% রেটিং দেননি
    z_eff = np.random.default_rng(7).normal(0, 0.7, 64)   # জেলার প্রভাব
    z = (-0.6 - 0.03 * tenure - 0.3 * orders + 0.012 * fee + 0.4 * late
         - 0.35 * np.where(no_rating, 0, rating - 3) + 0.6 * no_rating
         - 0.2 * np.log(basket / 735) + z_eff[zila] + 0.5 * (pay == 3)
         + 0.8 * (late >= 2) * (fee >= 100))         # দেরি আর বেশি চার্জ একসঙ্গে
    churn = (rng.random(n) < 1 / (1 + np.exp(-1.5 * z))).astype(int)
    rating[no_rating] = np.nan
    return pd.DataFrame({
        "tenure": tenure, "orders": orders, "fee": fee, "late": late,
        "rating": rating, "basket": basket,
        "zila": pd.Categorical.from_codes(zila, ZILA),
        "pay": pd.Categorical.from_codes(pay, PAY),
        "churn": churn})

দ্বিতীয় ব্লক ৪,০০০ সারিতে শেখায়, ২,০০০ সারিতে পরীক্ষা করে। একই HistGradientBoostingClassifier চারভাবে: জেলা আর পেমেন্ট pandas-এর category dtype হিসেবে সরাসরি (categorical_features="from_dtype", ১.৮-এ ডিফল্ট), one-hot করে, আর one-hot-এর ওপর মাত্র ৪ ও ১৬টি খোপে। শেষে exact পদ্ধতির GradientBoostingClassifier, একই আকারের গাছ (৩১টি পাতা) আর ১০০টি গাছ; সে NaN নেয় না, তাই তার জন্য NaN-কে ০ বসানো হয়েছে। Metric: test-এর log loss (কম ভালো) আর AUC (বেশি ভালো)।

import time
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier as HGB
from sklearn.ensemble import GradientBoostingClassifier as GBC
from sklearn.metrics import log_loss, roc_auc_score
from bazar_churn import make_churn

df = make_churn(6000)
y = df.pop("churn")
Xtr, Xte, ytr, yte = df[:4000], df[4000:], y[:4000], y[4000:]
print(f"churn rate {ytr.mean():.3f}, rating missing {Xtr['rating'].isna().mean():.1%}")

def report(name, model, A, B):
    t0 = time.perf_counter()
    model.fit(A, ytr)
    t = time.perf_counter() - t0
    p = model.predict_proba(B)[:, 1]
    print(f"{name:<24}{t:6.2f} s  {log_loss(yte, p):.4f}  {roc_auc_score(yte, p):.4f}")

kw = dict(max_iter=100, learning_rate=0.1, early_stopping=False, random_state=0)
print("model                     fit      logloss  AUC")
report("HGB native categorical", HGB(**kw), Xtr, Xte)
oh_tr, oh_te = pd.get_dummies(Xtr, dtype=float), pd.get_dummies(Xte, dtype=float)
report(f"HGB one-hot ({oh_tr.shape[1]} cols)", HGB(**kw), oh_tr, oh_te)
for b in (4, 16):
    report(f"HGB one-hot max_bins={b}", HGB(max_bins=b, **kw), oh_tr, oh_te)
gb_tr, gb_te = oh_tr.fillna(0), oh_te.fillna(0)   # GBC NaN নেয় না
report("GradientBoosting exact", GBC(n_estimators=100, max_leaf_nodes=31, random_state=0), gb_tr, gb_te)

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

churn rate 0.221, rating missing 19.9%
model                     fit      logloss  AUC
HGB native categorical    0.59 s  0.4332  0.8210
HGB one-hot (74 cols)     0.88 s  0.4084  0.8282
HGB one-hot max_bins=4    0.69 s  0.4100  0.8245
HGB one-hot max_bins=16   0.68 s  0.4065  0.8293
GradientBoosting exact    0.90 s  0.4006  0.8330

তিনটি পর্যবেক্ষণ। এক, native categorical এখানে one-hot-এর চেয়ে খারাপ (log loss ০.৪৩৩২ বনাম ০.৪০৮৪)। কারণ: ৬৪টি জেলা, প্রতিটিতে গড়ে প্রায় ৬০ জন; প্রতিটি নোডে জেলাগুলোকে যেকোনো দুই দলে ভাগ করার স্বাধীনতা ছোট দলের গোলমালও শিখে ফেলে। One-hot-এ একটি কাটা একটিমাত্র জেলাকে আলাদা করে, তাই সে ধীরে শেখে। Native সবসময় ভালো, এমন ধারণা ভুল; কম সারি আর অনেক category-তে min_samples_leaf বা l2_regularization বাড়ানো, early stopping, বা নিচের CatBoost-এর পথ ভাবুন, আর validation দিয়ে মিলিয়ে নিন। দুই, মাত্র ৪টি খোপেও log loss প্রায় একই (০.৪১০০), ১৬টিতে ০.৪০৬৫: এই ডেটায় মোটা খোপের দাম সামান্য। তিন, exact GradientBoosting এখানে সবচেয়ে ভালো (০.৪০০৬) আর ৪,০০০ সারিতে সময়ের পার্থক্যও ছোট। পার্থক্যগুলো ০.০১-এর ঘরে, একটি seed-এ; সিদ্ধান্তের আগে কয়েকটি fold আর seed দেখা দরকার (পর্ব ০৪)।

সময়ের আসল পার্থক্য দেখা যায় বড় n-এ, যা ব্রাউজারে মাপা অবাস্তব। একই ডেটা-নিয়মে ডেস্কটপে সত্যিকারের run (Python 3.14.4, scikit-learn 1.9.1, একটি run; একই one-hot ইনপুট, ১০০টি গাছ, ৩১টি পাতা):

~/bazarghar-ml$ uv run python exact_vs_hist.py
one-hot input: 74 columns
n =  10,000  GBC exact    1.07 s   HGB hist  0.39 s   ratio   2.8x
n =  40,000  GBC exact    4.29 s   HGB hist  0.61 s   ratio   7.1x
n = 120,000  GBC exact   14.17 s   HGB hist  1.02 s   ratio  13.9x

Exact পদ্ধতির সময় n-এর সঙ্গে প্রায় সমানুপাতিক হারে বাড়ে (১০ হাজার থেকে ১ লাখ ২০ হাজারে প্রায় ১৩ গুণ), histogram পদ্ধতির মাত্র আড়াই গুণের মতো। সাবধান: HistGradientBoostingClassifier OpenMP দিয়ে একাধিক CPU core ব্যবহার করে, GradientBoostingClassifier একটি; তাই অনুপাতের একটি অংশ সমান্তরাল কাজের ফল, শুধু histogram-এর নয়। scikit-learn-এর docs নিজেও দশ হাজারের বেশি সারিতে histogram সংস্করণটি সুপারিশ করে।

৬ · LightGBM আর CatBoost-এর নিজস্ব কৌশল

গাছ কীভাবে বাড়ে — স্তর ধরে, পাতা ধরে, না একই প্রশ্নে

XGBoost-এর ডিফল্ট (grow_policy="depthwise") স্তর ধরে বাড়ায়: একটি স্তরের সব পাতা ভাঙে, তারপর পরের স্তর। LightGBM বাড়ায় পাতা ধরে: এই মুহূর্তে যে পাতা ভাঙলে gain সবচেয়ে বেশি, শুধু সেটি ভাঙে, যতক্ষণ না num_leaves পূর্ণ হয়। একই সংখ্যক পাতায় এতে loss বেশি কমে, কিন্তু গাছ এক দিকে খুব গভীর হতে পারে, আর ছোট ডেটায় overfitting সহজ; LightGBM-এর docs তাই num_leaves-কে 2max_depth-এর চেয়ে কম রাখতে আর দরকারে max_depth বেঁধে দিতে বলে। scikit-learn-এর HistGradientBoosting-ও পাতা ধরে বাড়ে (max_leaf_nodes দিয়ে)। CatBoost-এর ডিফল্ট symmetric (oblivious) গাছ: একই স্তরের সব নোড একই প্রশ্ন করে। এমন গাছ দুর্বল একা, কিন্তু খুব নিয়মিত (regularization-এর মতো), আর prediction দ্রুত, কারণ d টি প্রশ্নের উত্তর মিলিয়ে সরাসরি 2d-টি পাতার একটির নম্বর পাওয়া যায়।

তিনভাবে গাছ বাড়ানো

level-wise (XGBoost depthwise) · leaf-wise (LightGBM, HistGradientBoosting) · symmetric (CatBoost)

Level-wise, leaf-wise and symmetric tree growth স্তর ধরে দুই স্তর, সব পাতা ভাঙে পাতা ধরে 1 2 3 সবচেয়ে বেশি gain আগে Symmetric: প্রতি স্তরে একই প্রশ্ন চার্জ › 100? দেরি ≥ 2? দেরি ≥ 2?
ছবি ৩৬.১ — একই চারটি পাতা, তিন রকম আকার। পাতা ধরে বাড়লে গাছ এক দিকে গভীর হয়; symmetric গাছে পাতার নম্বর = দুটি হ্যাঁ/না উত্তরের বাইনারি সংখ্যা।

LightGBM-এর আরও দুটি কৌশল — GOSS আর EFB

GOSS (Gradient-based One-Side Sampling): যে সারিগুলোর |g| ছোট, model সেগুলো প্রায় ঠিক বলছে; তাদের সবাইকে প্রতিটি গাছে গোনার দরকার কম। তাই বড় |g|-এর উপরের a অংশ সবটা রাখা হয়, বাকিদের থেকে এলোমেলো b অংশ নিয়ে তাদের (১ − a)/b দিয়ে গুণ করা হয়, যাতে যোগফলের প্রত্যাশা না বদলায়। ক্লাসে শিক্ষক যেমন যারা বেশি ভুল করছে তাদের সবার খাতা দেখেন, বাকিদের কয়েকটি নমুনা দেখে পুরো ক্লাসের অবস্থা আন্দাজ করেন। LightGBM ৪.x-এ এটি data_sample_strategy="goss"; পুরোনো boosting="goss" লিখলে সে সতর্কবার্তা দিয়ে নিজেই বদলে নেয় (ডেস্কটপে lightgbm 4.7.0-এ দেখা)। EFB (Exclusive Feature Bundling): one-hot-এর মতো sparse কলামগুলো প্রায় কখনো একসঙ্গে অশূন্য হয় না; এমন কলামগুলোকে একটি কলামে মিলিয়ে (মানের পরিসর সরিয়ে) histogram-এর কাজ কমানো। প্রবন্ধে বড় গতির উন্নতি দেখানো হয়েছে, তবে সেই সংখ্যা তাঁদের ডেটা আর যন্ত্রের, সাধারণ নিয়ম নয়।

CatBoost — ordered target statistics আর target leakage

অনেক category-র feature-এ জনপ্রিয় কৌশল target encoding: প্রতিটি জেলাকে সেই জেলার churn-এর হার দিয়ে বদলানো (AI-এর ভিত্তি কোর্সের পর্ব ৬৩)। ফাঁদ: train-এ হার হিসাবের সময় নিজের সারির label-ও যোগ হয়। যে জেলায় একজনই গ্রাহক, তার encoding পুরোপুরি তার নিজের label: train-এ নিখুঁত সংকেত, test-এ কিছুই না। এটি target leakage (AI-এর ভিত্তি কোর্সের পর্ব ৫৬)। CatBoost-এর সমাধান ordered target statistics: সারিগুলোকে একটি এলোমেলো ক্রমে সাজিয়ে প্রতিটি সারির encoding হিসাব করা শুধু তার আগের সারি থেকে, একটি prior P আর ওজন a দিয়ে: TS = (আগের churn সংখ্যা + a·P)/(আগের মোট + a).

TS = (churn সংখ্যা + a·P) / (মোট + a)P = 0.5, a = 1

            
  1. ডেটা। ছয়জন গ্রাহক, একটি এলোমেলো ক্রমে। তিনটি জেলা, যার একটিতে (Z33) একজনই।
  2. Greedy encoding. প্রতিটি জেলার হার সব সারি থেকে, prior-এর দিকে একটু টেনে (smoothing করে)। দেখতে নিরীহ।
  3. সমস্যা। Z33-এর encoding ০.৭৫০ বা ০.২৫০ হবে শুধু তার নিজের label-এর ওপর নির্ভর করে। হাজারো ছোট category থাকলে model এই “ছায়া” থেকে label মুখস্থ করে, validation-এ ধরা পড়ে না যদি encoding-ও পুরো ডেটায় করা হয়।
  4. Ordered, প্রথম তিন সারি। প্রতিটি সারি শুধু তার আগের সারিগুলো দেখে। প্রথমবার দেখা জেলা prior ০.৫ পায়।
  5. Ordered, বাকি তিন সারি। সারি ৪ দেখে আগের দুজন Z05-এর দুজনই churn, তাই ০.৮৩৩. Z33 নিজের label দেখে না, তাই ০.৫. কোনো সারিতে নিজের label নেই।
  6. Test-এ। পুরো train-এর পরিসংখ্যান। দাম: শুরুর দিকের সারির encoding কম তথ্যের, তাই CatBoost একাধিক ক্রম ব্যবহার করে। একই যুক্তিতে সে ordered boosting দিয়ে gradient-এর পক্ষপাতও (prediction shift) কমাতে চায়।

একই সমস্যার সাধারণ সমাধান হলো out-of-fold encoding: K ভাগে ভাগ করে প্রতিটি ভাগের encoding বাকি ভাগ থেকে। scikit-learn-এর TargetEncoder (১.৩ থেকে) fit_transform-এ ঠিক এই cross fitting করে। একই ধারণা পর্ব ৩৭ (Stacking, blending আর voting — ensemble কখন সত্যিই সাহায্য করে)-এর out-of-fold prediction-এও আসবে।

৭ · Constraint আর early stopping — model-কে নিয়মে বাঁধা

ব্যবসার জ্ঞান বলে: অন্য সব এক থাকলে ডেলিভারি চার্জ বাড়লে churn-এর ঝুঁকি কমতে পারে না। কিন্তু গোলমেলে ডেটায় model চার্জের কোনো পরিসরে উল্টো ঢেউ শিখে ফেলে, যা ব্যাখ্যা করা যায় না আর দাম ঠিক করার সিদ্ধান্তে বিপদ। Monotonic constraint (monotonic_cst, +১ মানে বাড়বে বা সমান থাকবে) গাছ বানানোর সময়ই এই নিয়ম চাপায়: চার্জে প্রতিটি ভাগে ডান পাতার মান বাঁয়ের চেয়ে কম হতে পারে না, আর নিচের নোডের মান সেই সীমার মধ্যে আটকে থাকে। Interaction constraint (interaction_cst) বলে কোন feature-গুলো একই গাছের পথে একসঙ্গে আসতে পারবে; এখানে শুধু চার্জ আর দেরি একসঙ্গে, বাকিরা আলাদা দলে। scikit-learn ১.৮-এ monotonic_cst কলামের নামের dict নেয়, কিন্তু interaction_cst শুধু কলামের অবস্থান (০ থেকে) নেয়।

তৃতীয় ব্লক তিনটি model শেখায় আর প্রতিটির partial dependence আঁকে: ১,০০০টি সারির প্রতিটিতে চার্জ একটি মানে বসিয়ে predict করা, তারপর গড়। এই গড় চার্জ বাড়তে বাড়তে কতবার নামে, তাও গোনে।

import numpy as np
import pandas as pd                      # bazar_churn-এর জন্য দরকার
import matplotlib.pyplot as plt
from sklearn.ensemble import HistGradientBoostingClassifier as HGB
from sklearn.inspection import partial_dependence
from sklearn.metrics import log_loss
from bazar_churn import make_churn

df = make_churn(6000)
y = df.pop("churn")
Xtr, Xte, ytr, yte = df[:4000], df[4000:], y[:4000], y[4000:]
cols = list(Xtr.columns)                 # fee = 2, late = 3
kw = dict(max_iter=100, learning_rate=0.1, early_stopping=False, random_state=0)
models = {
    "no constraint": HGB(**kw),
    "monotonic_cst fee +1": HGB(monotonic_cst={"fee": 1}, **kw),
    "interaction_cst": HGB(interaction_cst=[[2, 3], [0, 1, 4, 5, 6, 7]], **kw),
}
fig, ax = plt.subplots(figsize=(7, 3.6))
for name, m in models.items():
    m.fit(Xtr, ytr)
    pd_res = partial_dependence(m, Xtr[:1000], ["fee"], grid_resolution=40,
                                method="brute")      # fee বসিয়ে predict, তারপর গড়
    grid, avg = pd_res["grid_values"][0], pd_res["average"][0]
    drops = int(np.sum(np.diff(avg) < -1e-9))
    ll = log_loss(yte, m.predict_proba(Xte)[:, 1])
    print(f"{name:<22} test logloss {ll:.4f}  PD falls {drops:2d} times")
    if name != "interaction_cst":
        ax.plot(grid, avg, label=name)
ax.set(xlabel="delivery fee (Tk)", ylabel="mean predicted P(churn)",
       title="Effect of delivery fee on churn")
ax.legend()
fig.subplots_adjust(left=0.13, bottom=0.15)

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

no constraint          test logloss 0.4332  PD falls 11 times
monotonic_cst fee +1   test logloss 0.4321  PD falls  0 times
interaction_cst        test logloss 0.4154  PD falls  8 times

Constraint ছাড়া model-এর গড় churn-সম্ভাবনা চার্জ বাড়ার পথে ১১ বার নামে: যেমন ২৫–৩৫ টাকা আর ১৩৫–১৪৫ টাকার কাছে স্পষ্ট ঢেউ, যা শুধু গোলমাল। monotonic_cst-সহ রেখাটি একবারও নামে না, আর test log loss বরং সামান্য ভালো (০.৪৩২১ বনাম ০.৪৩৩২)। অর্থাৎ সঠিক নিয়ম চাপালে accuracy-র দাম প্রায় নেই, ব্যাখ্যা আর ভরসা অনেক বেশি। Interaction constraint-এর ফল আরও চমকপ্রদ: log loss ০.৪১৫৪, তিনটির মধ্যে সেরা। এই ডেটার সত্যিকারের নিয়ম প্রায় যোগফল-ধরনের (additive), শুধু চার্জ × দেরি একসঙ্গে কাজ করে; অপ্রয়োজনীয় interaction নিষেধ করায় model গোলমাল কম শিখেছে। তবে এটি একটি ডেটা, একটি seed; constraint সঠিক না হলে উল্টো ক্ষতি হবে। একটি সতর্কতা: এই ডেটায় ডিফল্ট method="recursion" (native categorical-সহ model-এ) monotone model-এর জন্যও নামা-ওঠা দেখিয়েছিল, তাই এখানে method="brute": সংজ্ঞা অনুযায়ী সরাসরি predict করে গড়।

Early stopping অন্য ধরনের লাগাম: কতগুলো গাছ। প্রতিটি নতুন গাছের পর একটি আলাদা validation set-এ loss দেখা হয়; পরপর কয়েকবার (n_iter_no_change) উন্নতি না হলে থামা। পর্ব ১৭-এ দেখেছেন, early stopping নিজেই একধরনের regularization. চতুর্থ ব্লক তিনটি learning rate-এ নিজের validation set (X_val, y_val) দিয়ে থামায় আর learning curve আঁকে।

import numpy as np
import pandas as pd                      # bazar_churn-এর জন্য দরকার
import matplotlib.pyplot as plt
from sklearn.ensemble import HistGradientBoostingClassifier as HGB
from sklearn.metrics import log_loss
from bazar_churn import make_churn

df = make_churn(6000)
y = df.pop("churn")
Xtr, Xva, Xte = df[:3000], df[3000:4000], df[4000:]      # train, validation, test
ytr, yva, yte = y[:3000], y[3000:4000], y[4000:]
fig, ax = plt.subplots(figsize=(7, 3.6))
print("lr     n_iter_  best iter  best val loss  test logloss")
for lr in (0.3, 0.1, 0.03):
    m = HGB(max_iter=2000, learning_rate=lr, early_stopping=True,
            n_iter_no_change=20, scoring="loss", random_state=0)
    m.fit(Xtr, ytr, X_val=Xva, y_val=yva)    # নিজের validation set
    val = -m.validation_score_           # score = −loss; সূচক 0 = শুরুর অবস্থা
    best = int(np.argmin(val))
    ll = log_loss(yte, m.predict_proba(Xte)[:, 1])
    print(f"{lr:<6} {m.n_iter_:7d}  {best:9d}  {val[best]:13.4f}  {ll:12.4f}")
    ax.plot(val, label=f"validation, lr={lr}")
    if lr == 0.1:
        ax.plot(-m.train_score_, "--", color="gray", label="train, lr=0.1")
ax.set(xlabel="boosting iteration", ylabel="log loss",
       title="Early stopping on a validation set", ylim=(0.2, 0.6))
ax.legend(fontsize=8)
fig.subplots_adjust(left=0.12, bottom=0.15)

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

lr     n_iter_  best iter  best val loss  test logloss
0.3         33         13         0.4324        0.4723
0.1         41         21         0.4297        0.4090
0.03       136        116         0.4246        0.4075

Learning rate ০.৩-এ validation loss ১৩টি গাছেই সর্বনিম্ন, তারপর দ্রুত ওঠে; ০.০৩-এ ১১৬টি গাছ লাগে, কিন্তু সর্বনিম্ন loss একটু কম (০.৪২৪৬ বনাম ০.৪৩২৪)। ছোট পা, বেশি গাছ, সামান্য ভালো ফল: shrinkage-এর পরিচিত বিনিময়। Train loss (ধূসর) নামতেই থাকে, তাই train দেখে থামার সময় বোঝা যায় না। সবচেয়ে গুরুত্বপূর্ণ সূক্ষ্মতা: n_iter_ = সেরা + ২০. scikit-learn-এর HistGradientBoosting সেরা বিন্দুর পরের ২০টি গাছও রেখে দেয়, পেছনে ফেরে না। Learning rate ০.৩-এ এই বাড়তি গাছগুলোর জন্য test log loss ০.৪৭২৩, অন্য দুটির চেয়ে অনেক খারাপ। XGBoost-এর predict নিজেই সেরা গাছ পর্যন্ত থামে, LightGBM আর CatBoost model-কে সেরা গাছে ছেঁটে দেয় (অংশ ৮-এর run-এ দেখবেন)। HistGradientBoosting-এ তাই ছোট learning rate বা ছোট n_iter_no_change, অথবা পাওয়া সেরা সংখ্যায় max_iter বসিয়ে আবার শেখানো।

৮ · ডেস্কটপে তিন লাইব্রেরি — একই ডেটা, একই বাজেট

ব্রাউজারে কেন নয়? লেখার আগে যাচাই করা হয়েছে। এই পাতার Pyodide 314.0.7-এ xgboost 2.1.4 আর lightgbm 4.6.0 bundled আছে, কিন্তু কোনোটিই সহজভাবে নীরবে চলেনি: XGBClassifier ব্লকে আলাদা করে import sklearn না থাকলে “sklearn needs to be installed” ImportError দেয় (পাতা শুধু ব্লকের import দেখে প্যাকেজ নামায়), আর LGBMClassifier-এর predict NumPy array-তেও “X does not have valid feature names” UserWarning ছাপে। LightGBM-এর নিজস্ব lgb.train নীরবে চলেছে। দুটির version-ও ডেস্কটপের সর্বশেষের চেয়ে পুরোনো, আর CatBoost নেই। তাই পাতার কোড scikit-learn-এ, আর তিন লাইব্রেরি ডেস্কটপে।

সত্যিকারের run: একটি uv project-এ সর্বশেষ স্থিতিশীল version (লেখার সময়ে), Python 3.14.4. একই ডেটা-নিয়মে ২ লাখ সারি: ১.২ লাখ train, ৪০ হাজার validation, ৪০ হাজার test. একই বাজেট: learning rate ০.০৫, সর্বোচ্চ ৩,০০০ গাছ, validation-এ ৫০ ধাপ উন্নতি না হলে থামা, সব লাইব্রেরিতে native categorical. গাছের আকার যতটা সম্ভব কাছাকাছি: ৩১টি পাতা (HistGradientBoosting, LightGBM) বা গভীরতা ৬ (XGBoost, CatBoost; ৬৪টি পর্যন্ত পাতা)। একটি run, একটি যন্ত্র; সাধারণ benchmark নয়।

~/bazarghar-ml$ uv add xgboost lightgbm catboost scikit-learn pandas
…
~/bazarghar-ml$ uv run python compare.py
Python 3.14.4, scikit-learn 1.9.1, xgboost 3.4.1, lightgbm 4.7.0, catboost 1.2.10
train 120,000  valid 40,000  test 40,000  lr 0.05  max rounds 3000
model                  fit (s)  trees  best  test logloss     AUC
HistGradientBoosting      0.94    220   170        0.3565  0.8716
XGBoost (hist)            0.92    222   172        0.3569  0.8710
LightGBM                  0.52    215   215        0.3566  0.8716
CatBoost                 17.11    331   331        0.3538  0.8734
xgboost predict() uses only the best trees: True

Test log loss চারটিতে ০.৩৫৩৮ থেকে ০.৩৫৬৯: পার্থক্য তৃতীয় দশমিকে। CatBoost সামান্য এগিয়ে, সম্ভবত ৬৪টি জেলার ordered encoding-এর জন্য, কিন্তু সময় নিয়েছে প্রায় ১৭ সেকেন্ড, বাকিরা এক সেকেন্ডের কাছে (CatBoost-এর গভীরতা ৬-এর symmetric গাছ, বেশি গাছ, আর ভিন্ন ভেতরের কাজ; সব লাইব্রেরি ডিফল্টে সব CPU core ব্যবহার করেছে)। “trees” আর “best” কলাম early stopping-এর আচরণ দেখায়: HistGradientBoosting আর XGBoost সেরা বিন্দুর পরের ৫০টি গাছ রাখে, LightGBM আর CatBoost model-কে সেরা গাছে ছেঁটে দেয়, আর শেষ লাইন নিশ্চিত করে XGBoost-এর predict নিজে থেকেই শুধু সেরা ১৭২টি গাছ ব্যবহার করে। HistGradientBoosting-এর ২২০টি গাছের সবই predict-এ যায়, তবু এখানে ক্ষতি হয়নি, কারণ learning rate ছোট। ব্রাউজারের ৪,০০০ সারিতে native categorical one-hot-এর চেয়ে খারাপ ছিল; ১.২ লাখ সারিতে প্রতিটি জেলায় প্রায় ১,৯০০ জন, তাই সেই overfitting-এর ভয় কম।

Hyperparameter-এর মানচিত্র

একই ধারণা, চার নাম। বন্ধনীতে ডিফল্ট (লেখার সময়ের docs আর ডেস্কটপের version অনুযায়ী; আপনার version-এ মিলিয়ে নিন)। “—” মানে সরাসরি একই parameter নেই।

ধারণাHistGradientBoostingXGBoost (sklearn API)LightGBM (sklearn API)CatBoost
গাছের সংখ্যাmax_iter (100)n_estimators (100)n_estimators (100)iterations (1000)
Learning rate ηlearning_rate (0.1)learning_rate (0.3)learning_rate (0.1)learning_rate (ডেটা দেখে নিজে ঠিক করে)
গাছের আকারmax_leaf_nodes (31), max_depthmax_depth (6), max_leavesnum_leaves (31), max_depth (−1)depth (6)
পাতার ন্যূনতম আকারmin_samples_leaf (20)min_child_weight (1, H-এর যোগফল)min_child_samples (20)min_data_in_leaf (symmetric-এ নয়)
পাতার L2 শাস্তি λl2_regularization (0)reg_lambda (1)reg_lambda (0)l2_leaf_reg (3)
ন্যূনতম gain γ—gamma (0)min_split_gain (0)—
সারির নমুনা—subsamplesubsample + subsample_freqsubsample, bootstrap_type
কলামের নমুনাmax_features (প্রতি নোডে)colsample_bytree, colsample_bynodecolsample_bytreersm
খোপের সংখ্যাmax_bins (255)max_bin (256)max_bin (255)border_count (254, CPU)
Categoricalcategorical_features ("from_dtype")enable_categorical=Truepandas category বা categorical_featurecat_features
Monotonicmonotonic_cstmonotone_constraintsmonotone_constraintsmonotone_constraints
Interactioninteraction_cstinteraction_constraintsinteraction_constraints—
Early stoppingearly_stopping, n_iter_no_change, X_valearly_stopping_rounds (constructor-এ), eval_setlgb.early_stopping() callback, eval_X/eval_yearly_stopping_rounds, eval_set

লক্ষ্য করুন XGBoost-এর ডিফল্ট learning rate ০.৩, বাকিদের ০.১: “একই ডিফল্টে তুলনা” আসলে অসম তুলনা। আর λ-এর ডিফল্ট HistGradientBoosting-এ ০, XGBoost-এ ১, CatBoost-এ ৩; সূত্র −G/(H + λ) মনে রাখলে বুঝবেন একই গাছেও পাতার মান আলাদা হবে।

আরও গভীরে: XGBoost-এর প্রবন্ধ (Chen ও Guestrin, KDD ২০১৬) অ্যালগরিদমের সঙ্গে সিস্টেমের নকশাও দেখায়: weighted quantile sketch (h-কে ওজন ধরে খোপের সীমা), column block আর cache-সচেতন ডেটা বিন্যাস, disk থেকে out-of-core শেখা। LightGBM-এর প্রবন্ধ (Ke ও সহকর্মীরা, NIPS ২০১৭) GOSS-এর জন্য gain-এর আনুমানিক ভুলের সীমা দেয়, আর EFB-কে graph coloring সমস্যায় রূপান্তর করে greedy সমাধান নেয়। CatBoost-এর প্রবন্ধ (Prokhorenkova ও সহকর্মীরা, NeurIPS ২০১৮) যুক্তি দেয় যে একই সারি দিয়ে gradient হিসাব আর গাছ শেখানো একটি পক্ষপাত (prediction shift) তৈরি করে, আর ordered boosting তা কমায়।

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

  • Log loss-এর g আর h? g = p − y, h = p(১ − p); p = ০.৮, y = ১ হলে g = −০.২, h = ০.১৬.
  • XGBoost-এর leaf weight আর gain? w* = −G/(H + λ); Gain = ½[GL²/(HL+λ) + GR²/(HR+λ) − G²/(H+λ)] − γ.
  • Histogram পদ্ধতি দ্রুত কেন? একবার খোপে ভাগ; প্রতিটি নোডে খরচ n (যোগ) + B (খোপ পেরোনো), সাজানোর দরকার নেই; সন্তানের histogram বিয়োগ করে পাওয়া যায়।
  • Leaf-wise growth-এর ঝুঁকি? একই পাতাসংখ্যায় গভীর, একপেশে গাছ; ছোট ডেটায় overfitting, তাই num_leaves, max_depth, min_child_samples দিয়ে লাগাম।
  • CatBoost-এর ordered target statistics কেন? নিজের সারির label encoding-এ ঢুকলে target leakage; শুধু আগের সারি থেকে হিসাব করলে তা হয় না।

৯ · AI-এর যুগে — boosting কোড যাচাই

XGBoost আর LightGBM-এর API কয়েক বছরে অনেক বদলেছে, আর AI assistant-এর শেখা ডেটায় পুরোনো টিউটোরিয়াল বেশি। ফল: চলে না এমন কোড, বা চলে কিন্তু সতর্কবার্তা চাপা পড়ে। ডেস্কটপে সর্বশেষ version-এ দুটি পুরোনো ধরনের call-এর সত্যিকারের আউটপুট (xgboost 3.4.1, lightgbm 4.7.0):

~/bazarghar-ml$ uv run python old_api.py
xgboost: XGBClassifier.fit() got an unexpected keyword argument 'early_stopping_rounds'
lightgbm: LGBMDeprecationWarning - The argument 'eval_set' is deprecated, use 'eval_X' and 'eval_y' instead.
AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
fit(..., early_stopping_rounds=…), boosting="goss", eval_set (LightGBM)পুরোনো API; কোথাও TypeError, কোথাও সতর্কবার্তানিজের version-এর docs আর changelog; warning চাপা দেবেন না
Early stopping-এর জন্য test set ব্যবহারগাছের সংখ্যা test দেখে বাছা মানে test-এ leakagetrain, validation, test তিন ভাগ; test শেষে একবার
জেলার মতো category-তে LabelEncoder বা পুরো ডেটায় target encodingকৃত্রিম ক্রম, বা নিজের label encoding-এnative categorical, out-of-fold TargetEncoder, বা CatBoost
তিন লাইব্রেরির “ডিফল্টে” তুলনা করে বিজয়ী ঘোষণাডিফল্ট learning rate, λ, গাছের আকার আলাদাএকই বাজেট আর early stopping, কয়েকটি seed বা fold
Leaf weight বা gain-এর সূত্রে চিহ্ন বা ½ ভুলদেখতে বিশ্বাসযোগ্য derivation-এ ছোট ভুলপাঁচ সারির উদাহরণে brute force দিয়ে মেলানো (অংশ ৩)

AI-কে কোডের সঙ্গে version, বাজেট আর যাচাই চাইতে বলুন:

আমার কাছে বাজারঘরের কাল্পনিক churn ডেটা আছে (pandas DataFrame, ১ লাখ সারি; tenure, orders, fee, late, rating-এ ২০% NaN, basket; zila ৬৪টি category, pay-এ ৪টি category রয়েছে)। ১. XGBoost 3.4, LightGBM 4.7, CatBoost 1.2 আর scikit-learn 1.9-এর HistGradientBoostingClassifier দিয়ে একই তুলনা লেখো। প্রতিটি argument এই version-এর docs-এ আছে কি না বলো; পুরোনো API (fit-এ early_stopping_rounds, boosting="goss", LightGBM-এ eval_set) ব্যবহার কোরো না। ২. ডেটা train, validation, test-এ ভাগ করো; early stopping শুধু validation-এ, test শেষে একবার। ৩. সবার জন্য একই learning rate, সর্বোচ্চ গাছ আর থামার নিয়ম লিখে দাও; গাছের আকার কীভাবে কাছাকাছি করলে, তা ব্যাখ্যা করো। ৪. Categorical সরাসরি দাও; LabelEncoder বা পুরো ডেটায় target encoding নয়। ৫. fee-তে monotonic constraint (+1) দিয়ে আর ছাড়া partial dependence তুলনা করো। ৬. কোন model early stopping-এর পর সেরা গাছে থামে আর কোনটি বাড়তি গাছ রাখে, তা কোডে যাচাই করে দেখাও। কোনো warning চাপা দিও না।

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

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

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

প্র ০১XGBoost কি সত্যিই নতুন অ্যালগরিদম ছিল, নাকি পুরোনো ধারণার ভালো প্রকৌশল? এর সাফল্য থেকে ML চর্চা নিয়ে কী শেখার আছে?

গণিতের দিক থেকে XGBoost-এর মূল উপাদানগুলো আগে থেকেই ছিল। Jerome Friedman ২০০১ সালে gradient boosting-কে function space-এ gradient descent হিসেবে দাঁড় করান। তার আগের বছর Friedman, Hastie আর Tibshirani দেখান, AdaBoost-কে additive logistic regression হিসেবে দেখা যায়, আর LogitBoost প্রতিটি ধাপে Newton-ধরনের হালনাগাদ নেয়। গাছের পাতায় L2 শাস্তি, বা ভাগের জন্য ন্যূনতম লাভ, এগুলোও নতুন ভাবনা নয়।

XGBoost-এর অবদান ছিল এগুলোকে একটি পরিষ্কার objective-এ বাঁধা (Taylor বিস্তার + γT + ½λ∑w², যা থেকে এই পর্বের দুটি সূত্র আসে) আর তার চারপাশে দ্রুত, নির্ভরযোগ্য সফটওয়্যার বানানো: missing value-এর শেখা দিক, sparse ডেটা, weighted quantile sketch, একাধিক থ্রেড, disk থেকে শেখা। Chen আর Guestrin-এর ২০১৬ সালের প্রবন্ধের অর্ধেকের বেশি অংশই সিস্টেম নিয়ে। ২০১৪ সালের দিকে Kaggle-এর Higgs Boson প্রতিযোগিতার সময় থেকে লাইব্রেরিটি দ্রুত ছড়ায়, আর পরের কয়েক বছর tabular প্রতিযোগিতায় বহু বিজয়ী সমাধানে এটি ছিল বলে প্রবন্ধেই দাবি করা হয়; সংখ্যাগুলো তাঁদের নিজেদের হিসাব, স্বাধীনভাবে যাচাই করা নয়।

শিক্ষা দুটি। এক, ML-এ “নতুন অ্যালগরিদম” আর “ভালো প্রকৌশল”-এর সীমা ঝাপসা; একটি পদ্ধতি দশ গুণ দ্রুত হলে মানুষ বেশি পরীক্ষা চালাতে পারে, বেশি feature চেষ্টা করতে পারে, আর তাতেই ফল ভালো হয়। দুই, ডিফল্ট আর API-ও সিদ্ধান্তকে প্রভাবিত করে: XGBoost-এর ডিফল্ট learning rate ০.৩ আর λ = ১, যা অনেকে না জেনেই ব্যবহার করেন। তাই কোনো লাইব্রেরির জয় দেখলে প্রশ্ন করুন, অ্যালগরিদম জিতেছে না কি তার ডিফল্ট, বাজেট আর ব্যবহারকারীর দক্ষতা।

মূল উপলব্ধি: ভালো ধারণা আর ভালো প্রকৌশল আলাদা করা কঠিন; তুলনার সময় অ্যালগরিদম, ডিফল্ট আর বাজেট আলাদা করে দেখতে হয়।

প্র ০২LightGBM-এর পাতা ধরে গাছ বাড়ানো একই পাতাসংখ্যায় loss বেশি কমায়। তাহলে সবাই কেন এটি নেয় না, আর কখন এটি বিপদ?

Best-first বা পাতা ধরে গাছ বাড়ানোর ধারণা পুরোনো; Haijian Shi-র ২০০৭ সালের থিসিস আর Friedman-এর পরের কাজে boosting-এ এটি আলোচিত হয়েছে বলে উল্লেখ পাওয়া যায়। LightGBM এটিকে ডিফল্ট বানায়। যুক্তি সরল: প্রতিটি ধাপে যে পাতা ভাঙলে gain সবচেয়ে বেশি, সেটিই ভাঙলে একই সংখ্যক পাতায় training loss স্তর-ধরা পদ্ধতির চেয়ে কম বা সমান হয়, কারণ স্তর-ধরা পদ্ধতি এমন পাতাও ভাঙে যার লাভ নগণ্য।

বিপদ হলো আকার নিয়ন্ত্রণে। ৩১টি পাতা স্তর ধরে মানে প্রায় গভীরতা ৫, কিন্তু পাতা ধরে একটি শাখা ২০–৩০ স্তর গভীর হতে পারে, যেখানে প্রতিটি পাতায় হাতে গোনা কয়েকটি সারি। ছোট ডেটায় এটি সরাসরি গোলমাল শেখা। LightGBM-এর নিজস্ব parameter-tuning নির্দেশিকা তাই num_leaves-কে 2max_depth-এর চেয়ে বেশ কম রাখতে, আর min_child_samples (ডিফল্ট ২০) বা min_child_weight বাড়াতে বলে। আরেকটি সূক্ষ্মতা: পাতা ধরে বাড়ানো ক্রমিক সিদ্ধান্ত, তাই একই স্তরের নোডগুলো একসঙ্গে সমান্তরালে ভাঙা কঠিন; GPU-তে স্তর-ধরা বা symmetric গাছ প্রায়ই সহজ।

CatBoost উল্টো দিকে যায়: symmetric গাছ সবচেয়ে বাধ্য আকৃতি, প্রতিটি স্তরে একটিই প্রশ্ন। একা দুর্বল, কিন্তু বৈচিত্র্য কম বলে overfitting কম, আর prediction খুব দ্রুত। XGBoost দুটিই দেয় (grow_policy “depthwise” বা “lossguide” বেছে নিয়ে)।

অর্থাৎ পাতা ধরে বাড়ানো এক ধরনের capacity বাড়ানো। পর্ব ০৩-এর bias–variance-এর ভাষায়: bias কমে, variance বাড়ে। ডেটা বড় হলে লাভ, ছোট হলে ক্ষতি, আর কোনটি, তা বলবে শুধু validation.

মূল উপলব্ধি: গাছ বাড়ানোর নিয়ম model-এর capacity-র একটি নিয়ন্ত্রক; সবচেয়ে “লোভী” নিয়ম সবসময় সবচেয়ে ভালো generalise করে না।

প্র ০৩Target encoding-এর leakage এত সূক্ষ্ম কেন যে অভিজ্ঞরাও পড়ে যান? CatBoost-এর ordered পদ্ধতি আর out-of-fold পদ্ধতির পার্থক্য কী?

Target encoding দেখতে খুব যুক্তিসঙ্গত: একটি জেলার churn-এর হারই তো সেই জেলা সম্পর্কে সবচেয়ে কাজের তথ্য। সমস্যা হলো train-এ হার হিসাবের সময় প্রতিটি সারির নিজের label-ও সেই হারে ঢোকে। বড় category-তে প্রভাব নগণ্য, কিন্তু ছোট category-তে encoding প্রায় label-এর প্রতিচ্ছবি। এই পর্বের stepper-এ একজন গ্রাহকের জেলা Z33-এর encoding ০.৭৫০ বা ০.২৫০, শুধু তার নিজের label অনুযায়ী। Model এটি দেখে train-এ চমৎকার করে, আর যদি encoding পুরো ডেটায় একবার করে তারপর cross-validation চালানো হয়, তবে validation-ও ভুলভাবে ভালো দেখায়। তাই ভুলটি সহজে ধরা পড়ে না।

দুটি পরিচিত প্রতিকার। Out-of-fold: ডেটাকে K ভাগে ভাগ করে প্রতিটি ভাগের encoding বাকি ভাগ থেকে; scikit-learn ১.৩ থেকে TargetEncoder fit_transform-এ এই cross fitting করে, আর smoothing নিজে ঠিক করতে পারে। CatBoost-এর ordered target statistics: একটি এলোমেলো ক্রমে প্রতিটি সারি শুধু আগের সারি দেখে। Prokhorenkova ও সহকর্মীরা ২০১৮ সালের প্রবন্ধে যুক্তি দেন যে এতে train আর test-এ encoding-এর বণ্টন একই রকম থাকে; একই চিন্তা থেকে তাঁরা gradient হিসাবেও “ordered boosting” আনেন।

বিনিময়ও আছে। Ordered পদ্ধতিতে ক্রমের শুরুর সারিগুলোর encoding প্রায় শুধু prior, তাই গোলমেলে; CatBoost একাধিক ক্রম নিয়ে তা কমায়, যার দাম সময়, যেমন অংশ ৮-এর run-এ CatBoost-এর বেশি সময় দেখেছেন। Out-of-fold পদ্ধতিতে প্রতিটি ভাগের encoding একটু কম ডেটা থেকে, তাই সামান্য পক্ষপাত। কোনটি ভালো, তা ডেটা-নির্ভর।

একই নীতি অন্যত্রও: stacking-এ base model-এর prediction (পর্ব ৩৭), বা time series-এ ভবিষ্যতের তথ্য। নিয়মটি এক: কোনো সারির feature-এ সেই সারির label-এর ছাপ থাকা চলবে না।

মূল উপলব্ধি: label থেকে বানানো feature সবসময় সন্দেহের তালিকায়; ঠিকভাবে বানাতে হলে প্রতিটি সারিকে তার নিজের label না দেখিয়ে encode করতে হয়।

প্র ০৪ঋণ দেওয়া বা দাম ঠিক করার মতো ক্ষেত্রে monotonic constraint কেন প্রায় বাধ্যতামূলক মনে করা হয়? এতে কী হারান আর কী পান?

ঋণের ঝুঁকির model-এ যদি দেখা যায়, অন্য সব এক থাকলে আয় বাড়লে ঝুঁকি কোনো পরিসরে বাড়ছে, তবে সেই model কাউকে ব্যাখ্যা করা কঠিন: আবেদনকারী, নিয়ন্ত্রক বা ব্যবসার মানুষ, কেউই এটি মানবেন না। গোলমেলে ডেটায় gradient boosting এমন ঢেউ সহজেই শেখে, যেমন এই পর্বের ব্লকে constraint ছাড়া ডেলিভারি চার্জের রেখা কয়েকবার নেমেছে। অনেক দেশে ঋণের সিদ্ধান্তের কারণ ব্যাখ্যা করার আইনি বা নিয়ন্ত্রক প্রত্যাশা আছে বলে আলোচনা হয়; আর traditional credit scorecard-গুলো সাধারণত monotone ভাবেই বানানো হয়। তাই boosting সেখানে ঢুকতে চাইলে monotone আচরণ প্রমাণ করার একটি সহজ পথ দরকার ছিল। XGBoost, LightGBM আর scikit-learn সবাই সেই পথ দিয়েছে।

লাভ তিনটি। ব্যাখ্যা: partial dependence-এর রেখা কথা দেওয়া দিকেই যায়, প্রতিটি গ্রাহকের জন্যও। স্থিতিশীলতা: ডেটা একটু বদলালে উল্টো ঢেউ লাফিয়ে আসে না। আর অনেক সময় generalisation: সঠিক জ্ঞান চাপালে model-কে কম কিছু শিখতে হয়, ঠিক যেমন ব্লকে log loss সামান্য কমেছে।

দাম দুটি। প্রথমত, যদি সত্যিকারের সম্পর্ক monotone না হয় (যেমন বয়সের সঙ্গে কিছু ঝুঁকি U-আকৃতির), তবে constraint ভুল ধারণা চাপিয়ে accuracy নষ্ট করে। দ্বিতীয়ত, monotonicity শুধু ঐ একটি feature-এর সাপেক্ষে, অন্য সব স্থির রেখে; সেই feature-এর সঙ্গে সম্পর্কিত অন্য feature-এর মধ্য দিয়ে প্রভাব ঢুকতে পারে। ন্যায্যতা নিয়েও সাবধান: monotone model-ও অন্যায্য হতে পারে, তা আলাদা প্রশ্ন (পর্ব ৬০)।

ব্যবহারিক নিয়ম: constraint বসান শুধু সেখানে, যেখানে ব্যবসা বা বিজ্ঞানের জ্ঞান পরিষ্কার, আর validation-এ constraint-সহ ও ছাড়া দুটির তুলনা নথিতে রাখুন।

মূল উপলব্ধি: monotonic constraint মানুষের জানা নিয়মকে model-এর ভেতরে লিখে দেয়; সঠিক হলে প্রায় বিনা দামে ভরসা কেনা যায়, ভুল হলে তা accuracy-র ক্ষতি।

প্র ০৫“XGBoost, LightGBM না CatBoost — কোনটি সেরা?” এই প্রশ্নের সৎ উত্তর কী, আর তুলনা কীভাবে ন্যায্য করবেন?

ইন্টারনেটে প্রচুর benchmark আছে, প্রায়ই পরস্পরবিরোধী। প্রতিটি লাইব্রেরির নিজের প্রবন্ধে নিজের পদ্ধতি এগিয়ে, যা আশ্চর্যের নয়: লেখকেরা নিজের টুল সবচেয়ে ভালো জানেন আর ডেটাসেট বাছাইও তাঁদের। স্বাধীন তুলনাগুলোতে সাধারণত দেখা যায়, ভালোভাবে tune করলে তিনটির পার্থক্য ছোট, আর ডেটাসেটভেদে বিজয়ী বদলায়। এই পর্বের run-ও তাই বলে: test log loss-এ পার্থক্য তৃতীয় দশমিকে, অথচ সময়ে পার্থক্য ৩০ গুণের বেশি।

তাই প্রশ্নটি ঘুরিয়ে করুন: আমার ডেটা আর প্রয়োজনে কোনটি? অনেক উচ্চ-cardinality categorical feature আর কম tuning-এর সময় থাকলে CatBoost-এর ডিফল্ট প্রায়ই ভালো শুরু। খুব বড় ডেটা আর দ্রুত শেখা দরকার হলে LightGBM বা XGBoost-এর hist. scikit-learn-এর pipeline, cross-validation আর একটি dependency কম রাখতে চাইলে HistGradientBoosting যথেষ্ট শক্ত। GPU, model-এর আকার, prediction-এর গতি, deploy করার পরিবেশ (ONNX বা অন্য runtime-এ রূপান্তর) আর দলের অভিজ্ঞতাও সিদ্ধান্তের অংশ।

ন্যায্য তুলনার নিয়ম কয়েকটি। একই fold বা একই train/validation/test ভাগ। একই বাজেট: হয় একই সংখ্যক tuning চেষ্টা, নয়তো একই সময়। একই early stopping নিয়ম, আর মাথায় রাখা যে কোন লাইব্রেরি সেরা গাছে থামে। কয়েকটি seed বা fold-এ পুনরাবৃত্তি আর পর্ব ২২-এর পরিসংখ্যানিক তুলনা। একটি সরল baseline, যেমন logistic regression, পাশে রাখা। আর সময় ও memory-ও মাপা, শুধু accuracy নয়। পর্ব ৩৮ (Tabular ডেটায় model বাছাই — tree, linear না neural, আর tuning-এর বাজেট)-এ এই প্রোটোকল পুরোটা দেখবেন।

মূল উপলব্ধি: সাধারণ “সেরা” লাইব্রেরি নেই; আপনার ডেটায় একই বাজেটে, কয়েকটি seed-এ মাপা ফলই একমাত্র সৎ উত্তর।

অনুশীলন

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

  1. পূর্ণ numerical: চারটি সারি, feature অনুযায়ী সাজানো। এখনকার সম্ভাবনা p = (০.৮, ০.৩, ০.৬, ০.২), লক্ষ্য y = (১, ০, ০, ০). λ = ১, γ = ০.১. (ক) প্রতিটি সারির g আর h. (খ) Root-এর w*. (গ) তিনটি সম্ভাব্য ভাগের (প্রথম ১ | বাকি ৩, প্রথম ২ | বাকি ২, প্রথম ৩ | বাকি ১) gain − γ. (ঘ) কোন ভাগ হবে, আর তার দুই পাতার মান কত?

    (ক) g = p − y = (−০.২, ০.৩, ০.৬, ০.২); h = p(১ − p) = (০.১৬, ০.২১, ০.২৪, ০.১৬). G = ০.৯, H = ০.৭৭.

    (খ) w* = −০.৯/১.৭৭ = −০.৫০৮৫. Root-এর G²/(H + λ) = ০.৮১/১.৭৭ = ০.৪৫৭৬.

    (গ) ১ | ৩: বাঁ (−০.২, ০.১৬) → ০.০৪/১.১৬ = ০.০৩৪৫; ডান (১.১, ০.৬১) → ১.২১/১.৬১ = ০.৭৫১৬; gain = ½(০.০৩৪৫ + ০.৭৫১৬ − ০.৪৫৭৬) = ০.১৬৪২, বিয়োগ γ = ০.০৬৪২. ২ | ২: বাঁ (০.১, ০.৩৭) → ০.০০৭৩; ডান (০.৮, ০.৪০) → ০.৪৫৭১; gain ০.০০৩৪, বিয়োগ γ = −০.০৯৬৬. ৩ | ১: বাঁ (০.৭, ০.৬১) → ০.৩০৪৩; ডান (০.২, ০.১৬) → ০.০৩৪৫; gain −০.০৫৯৪, বিয়োগ γ = −০.১৫৯৪. লক্ষ করুন, λ > ০ হলে γ ছাড়াও gain ঋণাত্মক হতে পারে।

    (ঘ) শুধু প্রথম ভাগ ধনাত্মক, তাই ১ | ৩. বাঁ পাতা w = ০.২/১.১৬ = +০.১৭২৪ (এই সারি churn করেছে, p বাড়াতে হবে), ডান পাতা w = −১.১/১.৬১ = −০.৬৮৩২.

  2. সমালোচনা ও নকশা: একটি AI assistant বাজারঘরের churn-এর জন্য এই পরিকল্পনা দিয়েছে: “জেলা আর পেমেন্ট LabelEncoder দিয়ে সংখ্যা বানাব। পুরো ডেটায় প্রতিটি জেলার churn হার বসিয়ে একটি নতুন কলাম। তারপর train/test ভাগ করে XGBoost, LightGBM আর CatBoost ডিফল্ট parameter-এ চালাব, early stopping-এর জন্য test set দেব, আর যার test AUC বেশি, সেটি production-এ।” কী কী সমস্যা, আর সঠিক নকশা কী?

    সমস্যা: (১) LabelEncoder জেলাগুলোকে ০, ১, ২… বানায়, যেন ক্রম আছে; গাছ এতে কাজ চালাতে পারে, কিন্তু অর্থহীন ক্রমে ভাগ খুঁজতে হয়। (২) পুরো ডেটায় churn হার মানে target leakage: নিজের সারির label আর test-এর label দুটিই encoding-এ। (৩) ডিফল্টে তুলনা অসম: XGBoost-এর learning rate ০.৩, বাকিদের ০.১, CatBoost ১,০০০টি গাছ, বাকিরা ১০০; λ-ও আলাদা। (৪) Test-এ early stopping মানে test দেখে গাছের সংখ্যা বাছা, তারপর সেই test-এ বিজয়ী বাছা: দুবার leakage, আর ফলটি আশাবাদী। (৫) একটি split, একটি seed-এ ছোট পার্থক্য দিয়ে সিদ্ধান্ত।

    সঠিক নকশা: train, validation আর তালাবন্ধ test. Categorical সরাসরি (enable_categorical, pandas category, cat_features), বা out-of-fold TargetEncoder. একই learning rate, সর্বোচ্চ গাছ আর validation-এ early stopping; tuning-এর বাজেটও এক। কয়েকটি fold বা seed-এ তুলনা, পাশে logistic regression baseline. বিজয়ী বাছার পর তালাবন্ধ test-এ একবার, সঙ্গে সময়, memory আর calibration (পর্ব ২৬)।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    zila = ["Z07", "Z07", "Z19", "Z07", "Z19", "Z42"]
    y    = [1,     0,     1,     1,     0,     1]
    prior, a = 0.5, 1.0
    seen = {}                                   # জেলা -> (churn সংখ্যা, মোট)
    for z, t in zip(zila, y):
        c, n = seen.get(z, (0, 0))
        print(z, round((c + a * prior) / (n + a), 3))
        seen[z] = (c + t, n + 1)
    print(seen)
    Z07 0.5
    Z07 0.75
    Z19 0.5
    Z07 0.5
    Z19 0.75
    Z42 0.5
    {'Z07': (2, 3), 'Z19': (1, 2), 'Z42': (1, 1)}

    প্রতিটি সারির encoding ছাপা হয় তার নিজের label যোগ করার আগে, তাই এটি ordered target statistics. প্রথম Z07: আগে কেউ নেই, (০ + ০.৫)/(০ + ১) = ০.৫. দ্বিতীয় Z07: আগের একজন churn, (১ + ০.৫)/২ = ০.৭৫. তৃতীয় Z07: আগের দুজনের একজন churn, (১ + ০.৫)/৩ = ০.৫. দ্বিতীয় Z19: আগের একজন churn, ০.৭৫. Z42 প্রথমবার, ০.৫. শেষ লাইনে প্রতিটি জেলার (churn, মোট) জমা, যা test-এর encoding-এ ব্যবহার হবে। (ডেস্কটপে চালিয়ে মেলানো।)

  4. কোড লিখুন: একটি function best_split(x, g, h, lam, gamma) লিখুন, যা একটি সংখ্যার feature সাজিয়ে প্রতিটি পাশাপাশি জোড়ার মাঝখানে কাটার gain হিসাব করে (সমান মানের মাঝে কাটা নয়) আর সেরা threshold ও gain ফেরত দেয়। অংশ ২-এর পাঁচ সারিতে (ক্রম এলোমেলো করে) যাচাই করুন, আর γ = ০.৪ দিয়ে আবার চালান।
    import numpy as np
    
    def best_split(x, g, h, lam=1.0, gamma=0.0):
        order = np.argsort(x)
        x, g, h = x[order], g[order], h[order]
        G, H = g.sum(), h.sum()
        GL = np.cumsum(g)[:-1]                   # বাঁ দিকের যোগফল, প্রতিটি কাটার জন্য
        HL = np.cumsum(h)[:-1]
        gain = 0.5 * (GL**2 / (HL + lam) + (G - GL)**2 / (H - HL + lam)
                      - G**2 / (H + lam)) - gamma
        ok = x[1:] > x[:-1]                      # সমান মানের মাঝে কাটা যায় না
        gain = np.where(ok, gain, -np.inf)
        k = int(np.argmax(gain))
        return (x[k] + x[k + 1]) / 2, gain[k]
    
    x = np.array([100, 20, 60, 80, 40.0])
    y = np.array([1, 0, 0, 1, 1])
    p = np.full(5, 0.5)
    thr, gain = best_split(x, p - y, p * (1 - p))
    print(f"best threshold {thr}, gain {gain:.4f}")
    thr2, gain2 = best_split(x, p - y, p * (1 - p), gamma=0.4)
    print(f"gamma = 0.4: gain {gain2:.4f}, split {'yes' if gain2 > 0 else 'no'}")

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

    best threshold 70.0, gain 0.3492
    gamma = 0.4: gain -0.0508, split no

    Cumulative sum দিয়ে সব কাটার বাঁ দিকের G আর H একবারে পাওয়া যায়, তাই সাজানোর পর খরচ মাত্র n: exact পদ্ধতির হৃদয় এটিই। ফল অংশ ২-এর সঙ্গে মেলে: x = ৬০ আর ৮০-এর মাঝে, ৭০-এ কাটা, gain ০.৩৪৯২. γ = ০.৪ হলে −০.০৫০৮, তাই ভাগ নয়।

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

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

ক্রমHistogram-ভিত্তিক second-order boosting-এর একটি গাছ যোগ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. এখনকার raw score থেকে প্রতিটি সারির p হিসাব
  2. প্রতিটি সারির g = p − y আর h = p(১ − p)
  3. নোডে প্রতিটি feature-এর প্রতিটি খোপের G আর H যোগ করা
  4. খোপের সীমা ধরে gain − γ হিসাব করে সেরা ভাগ বাছা
  5. পাতার মান w = −G/(H + λ) বসানো
  6. সবার score-এ η × w যোগ করা

Log loss-এ একটি সারির y = ১ আর এখনকার p = ০.৮. Raw score-এর সাপেক্ষে তার gradient g আর Hessian h কত?

  • g = +০.২, h = ০.১৬
  • g = −০.২, h = ০.৮
  • g = −০.২, h = ০.১৬
  • g = −০.২, h = ১

একটি পাতায় G = −৩, H = ২, আর λ = ১. XGBoost-এর সূত্রে পাতার সেরা মান w* কত?

  • ১.৫
  • ১.০
  • −১.০
  • ৩.০

XGBoost-এর split gain সূত্রে γ (gamma) কী ভূমিকা রাখে?

  • পাতার মানকে শূন্যের দিকে টানে
  • প্রতিটি গাছের অবদান ছোট করে
  • পাতায় H-এর ন্যূনতম যোগফল ঠিক করে
  • প্রতিটি ভাগের ন্যূনতম লাভ; এর চেয়ে কম লাভে ভাগ হয় না

Histogram-ভিত্তিক boosting-এ একটি নোডে একটি feature-এর সেরা কাটা খুঁজতে মোটামুটি খরচ কত (n = নোডের সারি, B = খোপের সংখ্যা)?

  • n + B
  • n log n
  • n²
  • n × B

CatBoost categorical feature-এর target statistics হিসাব করে একটি এলোমেলো ক্রমে শুধু আগের সারি থেকে। এর মূল উদ্দেশ্য কী?

  • শেখার গতি বাড়ানো
  • One-hot-এর memory বাঁচানো
  • নিজের সারির label encoding-এ ঢুকে target leakage হওয়া আটকানো
  • Missing value পূরণ করা

৩,০০০ সারির একটি ডেটায় LightGBM-এ num_leaves=255 আর max_depth=-1 দিয়ে train loss খুব কম কিন্তু validation loss খারাপ। সবচেয়ে সম্ভাব্য কারণ আর প্রথম প্রতিকার কী?

  • Underfitting; learning rate বাড়ানো
  • পাতা ধরে বাড়ানো গভীর গাছ গোলমাল শিখছে; num_leaves কমানো, max_depth ও min_child_samples বাঁধা
  • খোপ খুব কম; max_bin বাড়ানো
  • গাছ কম; n_estimators অনেক বাড়ানো

scikit-learn-এর HistGradientBoostingClassifier(early_stopping=True, n_iter_no_change=20) validation loss-এ ৩০তম গাছে সেরা বিন্দু পেয়ে থামল। শেখানো model-এ কতটি গাছ থাকে?

  • ৩০টি, সেরা বিন্দুতে ছেঁটে
  • max_iter-এর সমান
  • ১০টি, কারণ ৩০ − ২০
  • প্রায় ৫০টি, সেরার পরের ২০টিসহ

শব্দকোষ

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

মনে রাখুন

  1. Second-order: নতুন গাছ শেখে g আর h দিয়ে; log loss-এ g = p − y, h = p(১ − p). পাতার মান একটি Newton ধাপ।
  2. দুটি সূত্র: w* = −G/(H + λ); Gain = ½[GL²/(HL+λ) + GR²/(HR+λ) − G²/(H+λ)] − γ. λ পাতার মান ছোট করে, γ ভাগ ছাঁটে।
  3. Histogram: একবার খোপে ভাগ, প্রতিটি নোডে খরচ n + B; missing-এর আলাদা খোপ আর শেখা দিক; categorical সাজিয়ে ভাগ, কিন্তু ছোট ডেটায় overfitting-এর ঝুঁকি।
  4. লাইব্রেরির নিজস্বতা: LightGBM পাতা ধরে বাড়ায়, GOSS আর EFB; CatBoost ordered target statistics, ordered boosting, symmetric গাছ। Early stopping-এর পর কে সেরা গাছে থামে, তা লাইব্রেরিভেদে আলাদা।
  5. সৎ তুলনা: একই ভাগ, একই বাজেট, একই থামার নিয়ম, কয়েকটি seed; আমাদের run-এ পার্থক্য তৃতীয় দশমিকে, সময়ে বহু গুণ। Constraint আর validation-এর শৃঙ্খলা লাইব্রেরি বাছাইয়ের চেয়ে বেশি কাজের।

পরের পর্বে: পর্ব ৩৭ (Stacking, blending আর voting — ensemble কখন সত্যিই সাহায্য করে)। একাধিক model-এর prediction কীভাবে মেলাবেন, out-of-fold prediction কেন জরুরি, আর একটি ভালোভাবে tune করা boosting model-এর ওপর ensemble আসলে কতটা যোগ করে।

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

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