এই পর্বে যা শিখবেন
- Multiclass, multilabel, multi-output আর hierarchical সমস্যা আলাদা করে চেনা, আর কোনটির জন্য কোন model ও metric
- One-vs-rest, one-vs-one আর ECOC কয়টি model বানায়, কোন ডেটায় train হয় আর কীভাবে সিদ্ধান্ত নেয়, হাতে গুনে বলা
- Softmax-এর সূত্র, loss আর gradient লেখা, আর scikit-learn 1.8-এ
LogisticRegressionবহু শ্রেণিতে আসলে কী চালায় তা জানা - Binary relevance, classifier chain আর label powerset-এর পার্থক্য, আর ট্যাগের পারস্পরিক নির্ভরতা কখন কাজে লাগে
- Hamming loss, subset accuracy, micro আর macro F1 হাতে হিসাব করা, আর validation ডেটায় ট্যাগভিত্তিক threshold বাছা
AI-এর ভিত্তি কোর্সের পর্ব ৫৯-এ দুইয়ের বেশি শ্রেণির জন্য softmax এক ঝলক দেখেছেন, iris ফুলে; পর্ব ৬০-এ বহু শ্রেণির precision, recall আর macro, micro, weighted গড়। এই কোর্সের পর্ব ১৮-এ দেখেছেন, cross-entropy আসলে categorical বণ্টনের negative log-likelihood. এই পর্ব সেখান থেকে এগোয়: দুই শ্রেণির classifier দিয়ে বহু শ্রেণির কাজ করার কৌশলগুলো কীভাবে কাজ করে আর কোথায় ভাঙে, আর একটি পণ্য একসঙ্গে কয়েকটি ট্যাগ পেলে model আর metric কীভাবে বদলায়। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python, NumPy, pandas, scikit-learn আর matplotlib); প্রথমবার scikit-learn নামতে প্রায় দশ সেকেন্ড লাগে, পরের run দ্রুত।
১ · এক শ্রেণি, নাকি কয়েকটি ট্যাগ — সমস্যার ধরন চেনা
বাজারঘরের গুদামে নতুন একটি পণ্য এল: একটি কাচের বোতলের সুগন্ধি, দাম ৳২,৫০০. গুদামের কর্মীকে দুটি আলাদা কাজ করতে হয়। প্রথমত, পণ্যটিকে ঠিক একটি তাকে রাখতে হবে: মুদি, পোশাক, ইলেকট্রনিক্স, বই, না প্রসাধনী। দুটি তাকে একসঙ্গে রাখা যায় না। দ্বিতীয়ত, প্যাকেটের গায়ে স্টিকার লাগাতে হবে: “প্রিমিয়াম”, “উপহারের উপযোগী”, “ভঙ্গুর, সাবধানে”, “দ্রুত ডেলিভারি সম্ভব”। এখানে শূন্য, এক বা চারটি স্টিকারই লাগতে পারে। প্রথম কাজটি multiclass, দ্বিতীয়টি multilabel. দুটিই “শ্রেণি বলা”, কিন্তু উত্তরের আকৃতি আলাদা, তাই ভুলের মাপও আলাদা।
Multiclass classification-এ K > ২টি শ্রেণি, প্রতিটি সারির উত্তর ঠিক একটি: y ∈ {১, …, K}. Model সাধারণত প্রতিটি শ্রেণির একটি score বা সম্ভাবনা দেয়, আর উত্তর হয় সবচেয়ে বড়টির index, অর্থাৎ argmax. Multilabel classification-এ L-টি ট্যাগ, প্রতিটি সারির উত্তর L দৈর্ঘ্যের একটি ০/১ vector, যেমন (১, ১, ১, ০): প্রিমিয়াম, উপহার আর ভঙ্গুর হ্যাঁ, দ্রুত ডেলিভারি না। scikit-learn এই রূপকে বলে label indicator matrix: n সারি × L কলাম।
| ধরন | উত্তরের আকৃতি | বাজারঘরে উদাহরণ | scikit-learn-এ সাধারণ পথ |
|---|---|---|---|
| Binary | ০ বা ১ | অর্ডার ফেরত আসবে কি না | LogisticRegression |
| Multiclass | K-টির একটি | পণ্যের শ্রেণি (৫টির একটি) | LogisticRegression (softmax), OneVsRestClassifier, OneVsOneClassifier |
| Multilabel | L দৈর্ঘ্যের ০/১ vector | পণ্যের ট্যাগ (৪টির যেকোনো কয়টি) | MultiOutputClassifier, ClassifierChain, OneVsRestClassifier |
| Multi-output multiclass | কয়েকটি আলাদা multiclass উত্তর | পোশাকের রং (৬টির একটি) আর মাপ (S/M/L) | MultiOutputClassifier |
| Hierarchical | একটি গাছের পথ | ইলেকট্রনিক্স → মোবাইল → চার্জার | সরাসরি নেই; অংশ ৮-এর নোট দেখুন |
কিছু algorithm নিজে থেকেই বহু শ্রেণি সামলায়: decision tree আর random forest (প্রতিটি পাতায় শ্রেণির ভাগ), k-NN (প্রতিবেশীদের ভোট), Naive Bayes, আর softmax-এর logistic regression. আবার কিছু algorithm জন্মগতভাবে দুই শ্রেণির, যেমন একটি রেখার দুই পাশ বলে দেওয়া linear classifier আর SVM (পর্ব ২৭, Support Vector Machine — margin, support vector আর hard margin). এদের বহু শ্রেণিতে নিতে হলে সমস্যাটিকে কয়েকটি দুই-শ্রেণির সমস্যায় ভাঙতে হয়। এই ভাঙার কৌশলকে বলে reduction; পরের অংশে দুটি প্রধান কৌশল।
২ · One-vs-rest আর one-vs-one — দুই শ্রেণির classifier দিয়ে অনেক শ্রেণি
One-vs-rest (OvR, অন্য নাম one-vs-all): প্রতিটি শ্রেণির জন্য একটি model, “এই শ্রেণি বনাম বাকি সব”। K শ্রেণিতে K-টি model, প্রতিটি পুরো n সারিতে train হয়। নতুন সারিতে K-টি score fk(x) আসে; উত্তর argmaxk fk(x). One-vs-one (OvO): প্রতিটি শ্রেণি-জোড়ার জন্য একটি model, “শুধু এই দুই শ্রেণির মধ্যে কে”। জোড়ার সংখ্যা K(K − ১)/২, আর প্রতিটি model শুধু ওই দুই শ্রেণির সারিতে train হয়। নতুন সারিতে প্রতিটি duel একটি ভোট দেয়; সবচেয়ে বেশি ভোট পাওয়া শ্রেণি জেতে, অর্থাৎ majority vote.
চার শ্রেণিতে তিন কৌশল — কয়টি model, আর কে সিদ্ধান্ত নেয়
শ্রেণি: মুদি, পোশাক, ইলেকট্রনিক্স (ইলেক.), বই; K = 4
দুই কৌশলের খরচ হিসাব করুন। শ্রেণিগুলো সমান মাপের হলে প্রতিটি শ্রেণিতে n/K সারি। OvR-এ K-টি model × n সারি = Kn সারি-ব্যবহার। OvO-তে K(K − ১)/২-টি model × ২n/K সারি = (K − ১)n. মোট সারির হিসাবে প্রায় সমান! পার্থক্য আসে যখন training-এর খরচ সারির সংখ্যার চেয়ে দ্রুত বাড়ে। Kernel SVM-এর খরচ মোটামুটি n²-এর মতো বাড়ে; তখন অনেকগুলো ছোট সমস্যা একটি বড় সমস্যার চেয়ে সস্তা, আর সেজন্যই LIBSVM (আর তার ওপর বানানো scikit-learn-এর SVC) ভেতরে OvO চালায়। বিপরীতে অনুমানের সময় OvO-কে K(K − ১)/২-টি model চালাতে হয়: ১০০ শ্রেণিতে ৪,৯৫০টি।
নিচের stepper একটি পণ্যে দুই নিয়ম পাশাপাশি চালায়। Score-গুলো বোঝানোর জন্য বানানো, কোনো fit থেকে নয়; লক্ষ করুন, দুই নিয়ম আলাদা উত্তর দিতে পারে।
| ধাপ | |
| মুদি | |
| পোশাক | |
| ইলেক. | |
| বই | |
| খাতা |
- চারটি আলাদা প্রশ্ন। OvR-এর প্রতিটি model নিজের শ্রেণি বনাম বাকি সবার প্রশ্নের উত্তর দেয়। মুদি (+০.৪) আর পোশাক (+০.৬) দুজনেই “আমার” বলছে, বই আর ইলেকট্রনিক্স “না”। Score-গুলো আলাদা model থেকে, তাই তাদের মাপকাঠিও এক নয়।
- Argmax. OvR-এর নিয়ম সহজ: যার score সবচেয়ে বড়, সে জেতে। পোশাক +০.৬, তাই উত্তর পোশাক। কেউ “হ্যাঁ” না বললেও argmax একটি উত্তর দেয়, শুধু তখন সবচেয়ে কম “না”-টি জেতে।
- প্রথম তিন duel. OvO-তে প্রতিটি জোড়া আলাদা model. মুদি পোশাক আর ইলেকট্রনিক্সকে হারায়, কিন্তু বইয়ের কাছে হারে। এখন মুদি ২, বই ১ ভোট।
- বাকি তিন duel. পোশাক ইলেকট্রনিক্সকে হারায়, কিন্তু বইয়ের কাছে হারে; বই ইলেকট্রনিক্সকেও হারায়। চূড়ান্ত ভোট: বই ৩, মুদি ২, পোশাক ১, ইলেকট্রনিক্স ০.
- দুই উত্তর। OvR বলে পোশাক, OvO বলে বই। দুই কৌশল ভিন্ন প্রশ্নের উত্তর জোড়া দেয়, তাই সীমানার কাছের পণ্যে এমন মতভেদ স্বাভাবিক। ভোট সমান হলে scikit-learn-এর
OneVsOneClassifierপ্রতিটি duel-এর আত্মবিশ্বাসের যোগফলকে ছোট একটি সংখ্যায় এনে ভোটের সঙ্গে যোগ করে টাই ভাঙে।
দুই কৌশলেরই দুর্বলতা আছে। OvR-এর প্রতিটি model একটি অসম সমস্যা দেখে: ৫ শ্রেণিতে সবচেয়ে ছোট শ্রেণি (৮%) বনাম বাকি ৯২%, ফলে সেই model প্রায় সব জায়গায় “না” বলতে শেখে (class imbalance). আর K-টি score আলাদা আলাদা শেখা, তাই তুলনা করা কঠিন: একটি model সাধারণভাবে বেশি আত্মবিশ্বাসী হলে সে বেশি জেতে। OvO-র প্রতিটি model সীমিত ডেটা দেখে, আর একটি duel-এর model এমন সারিতেও ভোট দেয় যা তার দুই শ্রেণির কোনোটিই নয়: বই-বনাম-প্রসাধনী model-কে একটি টিভির ব্যাপারে মত দিতে হয়, যদিও সে কখনো টিভি দেখেনি। OneVsRestClassifier.predict_proba K-টি sigmoid সম্ভাবনাকে শুধু যোগফল দিয়ে ভাগ করে ১-এ আনে; এটি কাজ চালানোর উপায়, তত্ত্বের ফল নয়।
৩ · Softmax — একটি model, সব শ্রেণি একসঙ্গে
তৃতীয় পথ হলো সমস্যাটিকে না ভেঙে একটি model-এ সব শ্রেণি শেখা। Multinomial logistic regression-এ প্রতিটি শ্রেণির একটি ওজন-সারি wk আর bias bk, তাই K-টি score বা logits zk = wkᵀx + bk. তারপর softmax:
pk = ezk / Σj=1..K ezj
e-এর ঘাত সব score-কে ধনাত্মক করে, আর যোগফল দিয়ে ভাগ করলে p1 + … + pK = ১. Loss হলো cross-entropy: আসল শ্রেণি c হলে −log pc, সব সারির গড়। এটি categorical বণ্টনের negative log-likelihood, অর্থাৎ পর্ব ১৮-এর MLE. Score z-এর সাপেক্ষে gradient অসাধারণ সরল: ∂loss/∂z = p − y, যেখানে y আসল শ্রেণির one-hot vector. তাই ∂loss/∂wk = (pk − yk)·x: যে শ্রেণির সম্ভাবনা বেশি দেওয়া হয়েছে তার ওজন x থেকে দূরে সরে, আসল শ্রেণির ওজন x-এর দিকে আসে। Binary logistic-এর σ − y gradient-এর এটিই বহু শ্রেণির রূপ (পর্ব ১১)। নিচের stepper একটি পণ্যে পুরো হিসাব চালায়।
- চারটি score. একটি পণ্যের জন্য model চারটি logits দিয়েছে। পোশাকের score সবচেয়ে বড় (২.০), কিন্তু এখনো জানি না সে কতটা নিশ্চিত।
- Stability-র কৌশল। লব আর হর দুটিকেই e−max দিয়ে গুণ করলে ভাগফল বদলায় না। তাই আগে সবচেয়ে বড় score বিয়োগ করা হয়; বড় score-এ ez আর overflow করে না। এটিই log-sum-exp-এর কৌশল।
- e-এর ঘাত। সব মান ধনাত্মক হলো; সবচেয়ে বড়টি ঠিক ১. যোগফল ১.৭৭২৭.
- সম্ভাবনা। ভাগ করার পর পোশাক ০.৫৬৪১, মুদি ০.২৫৩৫, বই ০.১২৫৯, ইলেকট্রনিক্স ০.০৫৬৬; যোগ ১. Argmax পোশাক, কিন্তু model প্রায় ৪৪% সম্ভাবনা অন্য শ্রেণিগুলোকে দিচ্ছে।
- Loss আর gradient. আসল শ্রেণি বই, তাই loss −ln ০.১২৫৯ ≈ ২.০৭. Gradient p − y: বইয়ের score বাড়ানোর দিকে −০.৮৭, বাকিগুলো কমানোর দিকে। সব score-এ একই সংখ্যা যোগ করলে p বদলায় না, তাই শুধু score-এর পার্থক্যই অর্থবহ।
শেষ ধাপের পর্যবেক্ষণটি গুরুত্বপূর্ণ। সব wk-এ একই vector যোগ করলে softmax বদলায় না, তাই parameter অনন্য নয়; K-টির একটি শ্রেণিকে ভিত্তি (score ০) ধরলে K − ১টি সারিই যথেষ্ট। পরিসংখ্যানের বইয়ে multinomial logit এভাবেই লেখা হয়। scikit-learn বা neural network-এ সাধারণত সব K-টি সারি রাখা হয় আর L2 শাস্তি (পর্ব ১৭) সমাধানটিকে অনন্য করে। দুই শ্রেণিতে softmax হুবহু sigmoid: p2 = σ(z2 − z1).
LogisticRegression তিন বা বেশি শ্রেণি পেলে নিজে থেকেই একটি multinomial (softmax) model fit করে; default solver lbfgs (পর্ব ১৫-এর L-BFGS). আগের multi_class parameter 1.5-এ deprecated হয়ে এখন বাদ, তাই দিলে TypeError আসে। OvR চাইলে model-টিকে OneVsRestClassifier-এ মুড়তে হয়। ডেস্কটপে scikit-learn 1.9.1-এ solver="liblinear" তিন শ্রেণিতে সরাসরি error দেয়, আর সেই বার্তাতেই OneVsRestClassifier ব্যবহারের পরামর্শ থাকে।
OvR-এর সঙ্গে softmax-এর একটি গভীর পার্থক্য আছে। OvR-এর K-টি model একে অপরকে চেনে না; softmax-এ একটি শ্রেণির score বাড়লে বাকিদের সম্ভাবনা নিজে থেকেই কমে, কারণ সবাই একই হর ভাগ করে। তাই softmax-এর সম্ভাবনাগুলো একসঙ্গে অর্থবহ। সম্ভাবনা কতটা বিশ্বাসযোগ্য, তা মাপার আর ঠিক করার পদ্ধতি পর্ব ২৬ (Probability calibration — Platt, isotonic আর reliability diagram)-এর বিষয়।
৪ · বাজারঘরের ৫ শ্রেণি — OvR, OvO, multinomial আর ECOC পাশাপাশি
এই পর্বের ডেটা: বাজারঘরের ক্যাটালগের পণ্য, সব seed দিয়ে বানানো, কোনো আসল দোকানের নয়। প্রতিটি পণ্যের চারটি feature: দামের log10 (২ মানে ৳১০০, ৩ মানে ৳১,০০০), ওজন (kg), বিজ্ঞাপনে ছবির সংখ্যা, আর বিবরণে শব্দের সংখ্যা। শ্রেণি পাঁচটি, ভাগ অসম: মুদি ৩৮%, পোশাক ২৬%, ইলেকট্রনিক্স ১৬%, বই ১২%, প্রসাধনী ৮% (কোডে ইংরেজি নামে: grocery, clothing, electronics, books, beauty). একই module পরে চারটি ট্যাগও বানাবে। সব ব্লক নিচের module থেকে ডেটা নেয়।
# বাজারঘর (কাল্পনিক দোকান): পণ্যের শ্রেণি আর ট্যাগ, seed দিয়ে বানানো
import numpy as np
import pandas as pd
CATS = ["grocery", "clothing", "electronics", "books", "beauty"]
TAGS = ["premium", "gift", "fragile", "express"]
def make_products(n=600, seed=25):
rng = np.random.default_rng(seed)
y = rng.choice(5, size=n, p=[0.38, 0.26, 0.16, 0.12, 0.08]) # imbalance
# শ্রেণির গড়: log10(দাম ৳), ওজন kg, ছবির সংখ্যা, বিবরণের শব্দ
mid = np.array([[2.3, 1.4, 2, 45], [3.0, 0.6, 5, 60], [3.6, 1.1, 4, 95],
[2.6, 0.7, 2, 80], [2.9, 0.4, 3, 55]])
X = mid[y] + rng.normal(0, 1, (n, 4)) * [0.3, 0.35, 1.2, 22]
X[:, 1] = np.clip(X[:, 1], 0.05, None)
X[:, 2:] = np.clip(np.round(X[:, 2:]), 0, None)
df = pd.DataFrame(X.round(2), columns=["log_price", "weight", "photos", "words"])
df["category"] = np.array(CATS)[y]
return df
def make_tags(df, seed=26):
rng = np.random.default_rng(seed)
n, c = len(df), df.category.to_numpy()
e = rng.normal(0, 1, (n, 4))
u = rng.normal(0, 1, n) # অদেখা কারণ (যেমন উৎসবের সংস্করণ), X-এ নেই
prem = (df.log_price + 0.25 * e[:, 0] + 0.3 * u > 3.1).to_numpy()
gift = 1.6 * prem + 1.2 * np.isin(c, ["beauty", "books"]) + u + e[:, 1] > 1.2
frag = 2.0 * np.isin(c, ["electronics", "beauty"]) + 0.6 * gift + e[:, 2] > 1.4
expr = 1.0 - 1.5 * frag - 0.9 * df.weight.to_numpy() - u + e[:, 3] > -0.3
return pd.DataFrame(np.column_stack([prem, gift, frag, expr]).astype(int),
columns=TAGS)প্রথম ব্লক একই LogisticRegression-কে চারভাবে চালায়: OvR-এ মুড়ে, OvO-তে মুড়ে, নিজের মতো (multinomial), আর error-correcting output codes (ECOC)-এ। ECOC-এর ভাবনা: প্রতিটি শ্রেণিকে একটি ০/১ কোড-শব্দ দিন, যেমন মুদি = ১০১১০; প্রতিটি কলাম একটি দুই-শ্রেণির সমস্যা (কোন শ্রেণিগুলো ১-এর দলে, কোনগুলো ০-এর দলে)। নতুন সারিতে সব কলামের model একটি করে বিট দেয়, আর যে শ্রেণির কোড-শব্দ সবচেয়ে কাছে, সে জেতে। কয়েকটি বিট ভুল হলেও কাছের কোড-শব্দ ঠিক থাকতে পারে, ঠিক যেমন ডিজিটাল যোগাযোগের error-correcting code কয়েকটি উল্টে যাওয়া বিট সামলায়। scikit-learn-এর OutputCodeClassifier-এ code_size=2 মানে কলাম = ২ × K = ১০টি, কোড এলোমেলোভাবে বানানো। OvR আসলে একটি বিশেষ কোড: identity matrix.
import warnings
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import (OneVsRestClassifier, OneVsOneClassifier,
OutputCodeClassifier)
from sklearn.metrics import accuracy_score, f1_score, confusion_matrix
from bazar_catalog import make_products, CATS
warnings.filterwarnings("ignore", message="JsProxy") # ব্রাউজারের threadpoolctl বার্তা
df = make_products() # ৬০০টি পণ্য, ৫ শ্রেণি
print(df.category.value_counts().to_dict())
Xtr, Xte, ytr, yte = train_test_split(df.drop(columns="category"), df.category,
test_size=0.3, stratify=df.category, random_state=42)
sc = StandardScaler().fit(Xtr) # শুধু train দিয়ে fit
Xtr, Xte = sc.transform(Xtr), sc.transform(Xte)
lr = LogisticRegression(max_iter=1000) # 1.8: নিজে থেকেই multinomial
models = {"OvR": OneVsRestClassifier(lr), "OvO": OneVsOneClassifier(lr),
"multinomial": lr,
"ECOC x2": OutputCodeClassifier(lr, code_size=2, random_state=42)}
for name, m in models.items():
p = m.fit(Xtr, ytr).predict(Xte)
k = len(getattr(m, "estimators_", [m])) # কয়টি model train হলো
print(f"{name:12s} models={k:2d} acc={accuracy_score(yte, p):.3f}"
f" macroF1={f1_score(yte, p, average='macro'):.3f}")
print("multinomial coef_ shape:", lr.coef_.shape)
cm = confusion_matrix(yte, lr.predict(Xte), labels=CATS)
print(pd.DataFrame(cm, index=CATS, columns=[c[:5] for c in CATS]))
bal = LogisticRegression(class_weight="balanced", max_iter=1000).fit(Xtr, ytr)
cmb = confusion_matrix(yte, bal.predict(Xte), labels=CATS)
print(pd.DataFrame({"recall": cm.diagonal() / cm.sum(axis=1),
"recall_bal": cmb.diagonal() / cmb.sum(axis=1)},
index=CATS).round(2).T)
print("balanced macroF1:", round(f1_score(yte, bal.predict(Xte), average="macro"), 3))আসল আউটপুট (ব্রাউজারের Python):
{'grocery': 213, 'clothing': 164, 'electronics': 102, 'books': 65, 'beauty': 56}
OvR models= 5 acc=0.800 macroF1=0.673
OvO models=10 acc=0.828 macroF1=0.736
multinomial models= 1 acc=0.822 macroF1=0.735
ECOC x2 models=10 acc=0.739 macroF1=0.609
multinomial coef_ shape: (5, 4)
groce cloth elect books beaut
grocery 62 0 0 2 0
clothing 1 41 2 1 4
electronics 0 4 27 0 0
books 1 1 1 13 3
beauty 0 9 1 2 5
grocery clothing electronics books beauty
recall 0.97 0.84 0.87 0.68 0.29
recall_bal 0.88 0.71 0.87 0.68 0.59
balanced macroF1: 0.729
(ব্লকের warnings.filterwarnings লাইনটি শুধু একটি বার্তা লুকায়: ব্রাউজারের Python-এ ECOC-এর predict ভেতরে threadpoolctl ডাকে, আর সেটি Pyodide-এর একটি deprecation বার্তা ছাপে; ফলের ওপর এর কোনো প্রভাব নেই, ডেস্কটপে লাইনটি লাগে না।)
Model-এর সংখ্যা সূত্রের সঙ্গে হুবহু মেলে: OvR ৫, OvO ৫ × ৪/২ = ১০, multinomial ১ (তার coef_-এর আকার (৫, ৪): প্রতি শ্রেণিতে চারটি feature-এর এক সারি ওজন), ECOC ২ × ৫ = ১০. ১৮০টি test পণ্যে OvO (০.৮২৮) আর multinomial (০.৮২২) প্রায় সমান, OvR পিছিয়ে (০.৮০০), আর macro F1-এ OvR-এর ঘাটতি আরও বড় (০.৬৭৩ বনাম ০.৭৩৫): ছোট শ্রেণিগুলোতেই OvR বেশি হারায়। ECOC সবচেয়ে খারাপ (০.৭৩৯)। কারণ এলোমেলো কোড এমন দল বানাতে পারে, যেমন “মুদি আর ইলেকট্রনিক্স বনাম পোশাক আর বই”, যা একটি সরলরেখায় আলাদা করা কঠিন; linear base model-এ কোডের দলগুলো ডেটার জ্যামিতির সঙ্গে মিলতে হয়। ১৮০টি সারির একটি split-এ ০.০০৬-এর পার্থক্য কোনো সিদ্ধান্তের ভিত্তি নয়; দুই model-এর ন্যায্য তুলনার পদ্ধতি পর্ব ২২-এ।
Confusion matrix (সারি = আসল, কলাম = অনুমান) বলে ভুল কোথায়: প্রসাধনীর ১৭টির মাত্র ৫টি ঠিক, ৯টি পোশাক বলে ধরা। Recall-এর সারিতে মুদি ০.৯৭ আর প্রসাধনী ০.২৯: accuracy ০.৮২ শুনতে ভালো, কিন্তু সবচেয়ে ছোট শ্রেণিতে model প্রায় অন্ধ। class_weight="balanced" দিলে প্রতিটি শ্রেণির ভুলের ওজন তার মাপের উল্টো অনুপাতে বাড়ে: প্রসাধনীর recall ০.২৯ থেকে ০.৫৯, কিন্তু মুদি ০.৯৭ থেকে ০.৮৮ আর পোশাক ০.৮৪ থেকে ০.৭১; macro F1 প্রায় একই (০.৭২৯)। কোন বণ্টন ভালো, তা নির্ভর করে কোন ভুলের খরচ কত।
অনেক শ্রেণিতে imbalance দুই শ্রেণির চেয়ে জটিল, কারণ “ছোট শ্রেণি” একটি নয়, অনেক, আর তারা প্রায়ই একে অপরের সঙ্গে গোলমাল করে (এখানে প্রসাধনী আর পোশাক)। তিনটি অভ্যাস কাজে দেয়: accuracy-র পাশে macro F1 আর প্রতিটি শ্রেণির recall, split-এ stratify (stratified k-fold-এর মতো), আর confusion matrix দেখে গুলিয়ে যাওয়া জোড়ার জন্য নতুন feature ভাবা। Resampling, SMOTE আর threshold-এর পুরো আলোচনা পর্ব ৫০ (Imbalanced ডেটা — resampling, SMOTE, class weight আর threshold)-এ।
OvO-র খরচ বড় ডেটায় কেমন দাঁড়ায়, দেখতে ডেস্কটপে scikit-learn-এর সঙ্গে আসা digits ডেটাসেট (৮×৮ পিক্সেলের হাতে লেখা অঙ্ক, ১০ শ্রেণি, ১,৭৯৭টি ছবি) চালানো হয়েছে।
import time
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.multiclass import OneVsRestClassifier, OneVsOneClassifier
X, y = load_digits(return_X_y=True) # ১০ শ্রেণি, 1797 ছবি
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42)
sc = StandardScaler().fit(Xtr); Xtr, Xte = sc.transform(Xtr), sc.transform(Xte)
lr = LogisticRegression(max_iter=2000)
for name, m in [("OvR", OneVsRestClassifier(lr)), ("OvO", OneVsOneClassifier(lr)),
("multinomial", lr), ("SVC (OvO inside)", SVC())]:
t0 = time.perf_counter(); m.fit(Xtr, ytr); dt = time.perf_counter() - t0
k = len(getattr(m, "estimators_", [m]))
print(f"{name:17s} models={k:2d} fit={dt*1000:6.0f} ms acc={m.score(Xte, yte):.3f}")
svc = SVC(decision_function_shape="ovo").fit(Xtr, ytr)
print("SVC ovo decision_function shape:", svc.decision_function(Xte[:1]).shape)ডেস্কটপে সত্যিকারের run (Python 3.14.4, scikit-learn 1.9.1, লেখার সময়ে):
$ python digits_cost.py
OvR models=10 fit= 50 ms acc=0.969
OvO models=45 fit= 125 ms acc=0.981
multinomial models= 1 fit= 20 ms acc=0.981
SVC (OvO inside) models= 1 fit= 19 ms acc=0.983
SVC ovo decision_function shape: (1, 45)
১০ শ্রেণিতে OvO ৪৫টি model বানিয়েছে আর fit-এ সময় নিয়েছে multinomial-এর প্রায় ছয় গুণ; সময়গুলো একটি ল্যাপটপের একটি run, অন্য যন্ত্রে আলাদা হবে। SVC বাইরে থেকে একটি model, কিন্তু decision_function_shape="ovo" দিলে দেখা যায় ভেতরে ৪৫টি duel-এর score. Default "ovr"-এ scikit-learn সেই ভোটগুলোকে (১, ১০) আকারের একটি OvR-ধাঁচের score-এ রূপান্তর করে দেখায়, কিন্তু train হয় OvO-তেই।
৫ · Decision region — OvR-এর ফাঁকা আর দ্বন্দ্বের এলাকা
OvR-এর argmax সবসময় একটি উত্তর দেয়, কিন্তু তার নিচে কী ঘটছে তা দেখা দরকার। দুটি feature (দামের log আর ওজন) নিয়ে model fit করলে প্রতিটি OvR model সমতলে একটি রেখা টানে: রেখার এক পাশে “আমার”, অন্য পাশে “না”। পাঁচটি রেখা মিলে সমতলকে টুকরো টুকরো করে। কোনো টুকরোয় ঠিক একজন দাবি করে (পরিষ্কার), কোথাও কেউ না (ফাঁকা), কোথাও দুজন (দ্বন্দ্ব)। Argmax ফাঁকা আর দ্বন্দ্ব দুটিই চুপচাপ মিটিয়ে দেয়, আর decision boundary সেখানে score-এর মাপকাঠির ওপর নির্ভর করে। ব্লকটি ৩০০ × ৩০০ বিন্দুর একটি জালে তিনটি ছবি আঁকে।
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import pandas as pd # ডেটা module-এর জন্য
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier
from bazar_catalog import make_products, CATS
df = make_products()
X = df[["log_price", "weight"]].to_numpy() # শুধু ২টি feature, ছবির জন্য
y = df.category.map({c: i for i, c in enumerate(CATS)}).to_numpy()
ovr = OneVsRestClassifier(LogisticRegression()).fit(X, y)
mn = LogisticRegression().fit(X, y)
g1, g2 = np.meshgrid(np.linspace(0.9, 4.7, 300), np.linspace(0, 2.6, 300))
G = np.c_[g1.ravel(), g2.ravel()]
S = np.column_stack([e.decision_function(G) for e in ovr.estimators_])
claims = (S > 0).sum(axis=1) # কয়টি model "আমার!" বলছে
print("claims:", {k: round(float(np.mean(claims == k)), 3) for k in range(4)})
for name, m in [("OvR", ovr), ("multinomial", mn)]:
area = np.bincount(m.predict(G), minlength=5) / len(G)
print(f"{name:12s} area:", dict(zip(CATS, area.round(3).tolist())))
fig, ax = plt.subplots(1, 3, figsize=(12, 4.2), sharey=True)
claim_cm = ListedColormap(["#d62728", "#ffffff", "#ff7f0e", "#7f0000"])
panels = [(ovr.predict(G), "OvR: argmax of 5 scores", "tab10", 9),
(mn.predict(G), "Multinomial (softmax)", "tab10", 9),
(claims, "OvR claims: red 0, white 1, orange 2", claim_cm, 3)]
for a, (Z, title, cmap, vmax) in zip(ax, panels):
a.contourf(g1, g2, Z.reshape(g1.shape), levels=np.arange(-0.5, 5),
cmap=cmap, vmin=0, vmax=vmax, alpha=0.45)
a.scatter(X[:, 0], X[:, 1], c=y, cmap="tab10", vmin=0, vmax=9, s=6)
a.set_title(title, fontsize=10); a.set_xlabel("log10(price)")
ax[0].set_ylabel("weight (kg)")
fig.suptitle("blue grocery, orange clothing, green electronics, red books, purple beauty")
plt.show()আসল আউটপুট (ব্রাউজারের Python), আর তার নিচে তিনটি ছবি:
claims: {0: 0.115, 1: 0.664, 2: 0.22, 3: 0.001}
OvR area: {'grocery': 0.477, 'clothing': 0.155, 'electronics': 0.347, 'books': 0.021, 'beauty': 0.0}
multinomial area: {'grocery': 0.436, 'clothing': 0.133, 'electronics': 0.348, 'books': 0.081, 'beauty': 0.002}
প্রথম লাইন: জালের ৬৬.৪% বিন্দুতে ঠিক একটি OvR model “আমার” বলে, ১১.৫%-এ কেউ বলে না, ২২%-এ দুজন বলে, আর ০.১%-এ তিনজন। অর্থাৎ প্রায় এক-তৃতীয়াংশ জায়গায় উত্তর আসে argmax-এর টাই-ভাঙা থেকে, কোনো model-এর স্পষ্ট দাবি থেকে নয়। তৃতীয় ছবিতে লাল এলাকা (কেউ দাবি করে না) ঠিক মাঝখানে, যেখানে বই (লাল বিন্দু) আর প্রসাধনী (বেগুনি) মিশে আছে। ছোট শ্রেণির model “আমার” বলার মতো আত্মবিশ্বাস কোথাও পায় না, আর বড় শ্রেণিগুলো সেখানে “না” বলে।
এলাকার হিসাবে প্রসাধনী OvR-এ জালের ০.০% পায়: দুই feature-এ OvR কখনো প্রসাধনী বলে না। Multinomial-এ সামান্য ০.২%, আর বই ২.১% থেকে ৮.১%. Softmax-এর সীমানা আসে score-এর পার্থক্য থেকে, zk − zj = ০, আর সব শ্রেণি একসঙ্গে শেখা হয়; তাই কোনো ফাঁকা বা দ্বন্দ্বের এলাকা থাকে না, প্রতিটি বিন্দুতে ঠিক একটি সর্বোচ্চ সম্ভাবনা। দুই feature এখানে ইচ্ছা করে কম রাখা; চার feature-এ প্রসাধনী আংশিকভাবে আলাদা হয় (আগের ব্লকে recall ০.২৯)।
৬ · Multilabel — binary relevance, classifier chain আর label powerset
এবার স্টিকারের কাজ। বাজারঘরের চারটি ট্যাগ: premium (দামি পণ্য), gift (উপহারের উপযোগী), fragile (ভঙ্গুর), express (দ্রুত ডেলিভারি সম্ভব)। ট্যাগগুলো একে অপরের সঙ্গে জড়িত: দামি পণ্য প্রায়ই উপহারের, ভঙ্গুর পণ্য সাধারণত দ্রুত ডেলিভারিতে যায় না। ডেটার সূত্রে একটি অদেখা কারণও আছে (যেমন উৎসবের সংস্করণ), যা feature-এ নেই কিন্তু premium আর gift বাড়ায়, express কমায়। তিনটি প্রধান কৌশল:
Binary relevance
প্রতিটি ট্যাগের জন্য আলাদা হ্যাঁ/না model; L-টি model, একে অপরকে দেখে না। scikit-learn-এ MultiOutputClassifier বা OneVsRestClassifier. সরল, সমান্তরালে চলে।
Classifier chain
ট্যাগগুলোকে একটি ক্রমে সাজান; j-তম model x-এর সঙ্গে আগের ট্যাগগুলোর মানও feature হিসেবে পায়। ClassifierChain. ট্যাগের নির্ভরতা ধরে, কিন্তু ক্রম বেছে নিতে হয়।
Label powerset
প্রতিটি আলাদা ট্যাগ-সমষ্টিকে একটি শ্রেণি বানিয়ে একটি multiclass model. ৪ ট্যাগে সর্বোচ্চ ২৪ = ১৬ শ্রেণি, ২০ ট্যাগে দশ লাখের বেশি; অনেক সমষ্টি ডেটায় দেখাই যায় না।
Binary relevance-এর অনুমান হলো, x জানা থাকলে ট্যাগগুলো পরস্পর স্বাধীন। Classifier chain সম্ভাবনার chain rule অনুসরণ করে: P(y1, …, yL | x) = P(y1 | x) · P(y2 | x, y1) · …. Training-এর সময় প্রতিটি model আগের ট্যাগগুলোর আসল মান feature হিসেবে পায়; অনুমানের সময় আসল মান জানা নেই, তাই আগের model-গুলোর অনুমিত মান যায়। ফলে প্রথম দিকের একটি ভুল পরের model-গুলোতে ছড়াতে পারে। scikit-learn-এর নতুন version-এ chain_method দিয়ে অনুমিত ০/১-এর বদলে সম্ভাবনাও পাঠানো যায়। Label powerset নির্ভরতা পুরোপুরি ধরে, কিন্তু যে সমষ্টি training-এ নেই, তা কখনো অনুমান করতে পারে না। নিচের stepper একটি পণ্যে chain চালায়; ওজনগুলো বোঝানোর জন্য বানানো।
| model | |
| input | |
| z | |
| P(হ্যাঁ) | |
| ŷ (t = 0.5) |
- প্রথম model শুধু x দেখে। দাম ৳২,৫০০, তাই premium-এর z = ০.৮, sigmoid-এ ০.৬৯০; threshold ০.৫ পেরিয়েছে, ŷ = ১.
- দ্বিতীয় model আগের উত্তরও দেখে। Gift model-এর নিজের x-অংশ −০.৯, কিন্তু premium = ১ হলে ১.৭ যোগ হয়: z = ০.৮, সম্ভাবনা ০.৬৯০, ŷ = ১. Training-এ এই ১.৭ শেখা হয়েছে আসল premium মান থেকে।
- তৃতীয় model দুটি আগের উত্তর দেখে। Fragile-এর z = −১.০ + ০.৫ + ০.৭ = ০.২, সম্ভাবনা ০.৫৫০: অল্প ব্যবধানে হ্যাঁ।
- শেষ model. Express-এর z-এ fragile = ১-এর ওজন −১.৬, তাই z = −১.১, সম্ভাবনা ০.২৫০, ŷ = ০: ভঙ্গুর পণ্য দ্রুত ডেলিভারিতে নয়। ট্যাগগুলোর মধ্যে সংগতি এসেছে।
- Binary relevance-এর সঙ্গে তুলনা। আলাদা gift model premium-এর খবর পায় না; তার z = −০.২, সম্ভাবনা ০.৪৫০, তাই “না”। Chain-এর উত্তর (১, ১, ১, ০), BR-এর (১, ০, ১, ০). তবে premium ভুল হলে chain-এ ভুলটি gift আর fragile পর্যন্ত গড়াত।
Chain-এর ক্রম: ক্রম বদলালে ফল বদলায়, আর L-টি ট্যাগের ক্রম L! রকম। একটি পরিচিত সমাধান হলো কয়েকটি এলোমেলো ক্রমের chain বানিয়ে তাদের সম্ভাবনার গড় নেওয়া (ensemble of classifier chains); scikit-learn-এ order="random" আর আলাদা random_state দিয়ে কয়েকটি chain বানিয়ে হাতে গড় করা যায়। Label powerset-এর বিস্ফোরণ সামলাতে RAkEL পদ্ধতি ট্যাগের ছোট ছোট এলোমেলো দলে powerset চালায়।
৭ · Multilabel-এর metric — Hamming loss, subset accuracy, micro আর macro F1
Multilabel-এ “ঠিক” কথাটির কয়েকটি মানে। পুরো স্টিকার-সমষ্টি হুবহু মিললে ঠিক? নাকি প্রতিটি স্টিকার আলাদা করে? n সারি আর L ট্যাগে চারটি প্রধান মাপ:
- Hamming loss = ভুল ঘরের সংখ্যা / (n × L). প্রতিটি ট্যাগ-ঘর সমান ওজন পায়; কম ভালো।
- Subset accuracy (exact match) = যত ভাগ সারিতে পুরো vector হুবহু মেলে। খুব কঠোর: ২০ ট্যাগের একটি ভুল হলেই সারিটি শূন্য। scikit-learn-এ multilabel ডেটায়
accuracy_scoreএটিই দেয়। - Micro F1: সব ট্যাগের TP, FP আর FN একসঙ্গে যোগ করে একটি F1; ঘন ঘন আসা ট্যাগ বেশি প্রভাব ফেলে।
- Macro F1: প্রতিটি ট্যাগের F1 আলাদা হিসাব করে সাধারণ গড়; বিরল ট্যাগও সমান ওজন পায়। (আরও আছে:
average="samples", প্রতিটি সারির F1-এর গড়।)
- দুটি ছক। তিনটি পণ্য, চারটি ট্যাগ: মোট ১২টি হ্যাঁ/না ঘর। A পুরোপুরি ঠিক, B-তে দুটি ভুল, C-তে একটি।
- Hamming loss. শুধু ভুল ঘর গুনুন: ০ + ২ + ১ = ৩, ভাগ ১২ = ০.২৫. অর্থাৎ ৭৫% স্টিকার-সিদ্ধান্ত ঠিক।
- Subset accuracy. পুরো সারি হুবহু মিলেছে শুধু A-তে: ১/৩ ≈ ০.৩৩৩. Hamming-এর ৭৫% ঠিকের পাশে এই সংখ্যা অনেক কঠোর।
- Micro F1. সব ঘর এক ঝুড়িতে: TP ৪, FP ২, FN ১. Precision ০.৬৬৭, recall ০.৮, F1 = ৮/১১ ≈ ০.৭২৭. লক্ষ করুন, TN (দুই দিকেই ০) F1-এ ঢোকে না।
- Macro F1. প্রতিটি ট্যাগের F1: premium ১.০, বাকি তিনটি ০.৬৬৭; গড় ০.৭৫. প্রতিটি সারির F1-এর গড় (samples) ০.৭২২. একই অনুমানে চারটি metric চার রকম সংখ্যা দেয়।
কোন metric বাছবেন, তা নির্ভর করে সিদ্ধান্তের ধরনের ওপর। প্রতিটি স্টিকার আলাদা কাজে লাগলে (fragile মানে আলাদা প্যাকিং, express মানে আলাদা রুট) Hamming loss বা ট্যাগভিত্তিক F1. পুরো সমষ্টি একসঙ্গে একটি সিদ্ধান্ত হলে (যেমন একটি নির্দিষ্ট ট্যাগ-সমষ্টি মানে একটি বিশেষ গুদাম) subset accuracy. Krzysztof Dembczyński ও সহকর্মীদের তাত্ত্বিক বিশ্লেষণ একটি পরিষ্কার নিয়ম দেয়: Hamming loss কমাতে প্রতিটি ট্যাগের আলাদা (marginal) সম্ভাবনা ০.৫-এর বেশি হলে হ্যাঁ বলাই সেরা, অর্থাৎ ভালো binary relevance-ই যথেষ্ট; subset ০/১ loss কমাতে যৌথ বণ্টনের সবচেয়ে সম্ভাব্য সমষ্টি লাগে, যেখানে ট্যাগের নির্ভরতা কাজে আসে। পরের ব্লক এটি বাজারঘরের ১,৫০০টি পণ্যে পরীক্ষা করে।
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.multioutput import MultiOutputClassifier, ClassifierChain
from sklearn.metrics import hamming_loss, accuracy_score, f1_score
from bazar_catalog import make_products, make_tags, TAGS
df = make_products(n=1500, seed=7)
Y = make_tags(df) # ৪টি ট্যাগ, প্রতিটি 0/1
X = pd.get_dummies(df, columns=["category"], dtype=float)
print("tag rate:", Y.mean().round(2).to_dict())
print("corr(gift, premium) =", round(Y.gift.corr(Y.premium), 2),
" corr(express, fragile) =", round(Y.express.corr(Y.fragile), 2))
Xtr, Xte, Ytr, Yte = train_test_split(X, Y, test_size=0.4, random_state=42)
sc = StandardScaler().fit(Xtr)
Xtr, Xte = sc.transform(Xtr), sc.transform(Xte)
lr = LogisticRegression(max_iter=1000)
models = {"binary relevance": MultiOutputClassifier(lr),
"chain p>g>f>e": ClassifierChain(lr, order=[0, 1, 2, 3]),
"chain e>f>g>p": ClassifierChain(lr, order=[3, 2, 1, 0])}
rows = {}
for name, m in models.items():
P = m.fit(Xtr, Ytr).predict(Xte).astype(int)
rows[name] = {"hamming": hamming_loss(Yte, P),
"subset_acc": accuracy_score(Yte, P),
"micro_F1": f1_score(Yte, P, average="micro"),
"macro_F1": f1_score(Yte, P, average="macro")}
print(pd.DataFrame(rows).T.round(3))
combos = Y.astype(str).agg("".join, axis=1) # label powerset-এর "শ্রেণি"
print("distinct tag sets:", combos.nunique(), "of", 2 ** len(TAGS), "possible")আসল আউটপুট (ব্রাউজারের Python):
tag rate: {'premium': 0.31, 'gift': 0.42, 'fragile': 0.29, 'express': 0.5}
corr(gift, premium) = 0.5 corr(express, fragile) = -0.35
hamming subset_acc micro_F1 macro_F1
binary relevance 0.265 0.330 0.629 0.644
chain p>g>f>e 0.267 0.372 0.644 0.658
chain e>f>g>p 0.278 0.365 0.599 0.608
distinct tag sets: 16 of 16 possible
ট্যাগগুলো সত্যিই জড়িত: gift আর premium-এর correlation ০.৫, express আর fragile-এর −০.৩৫. ৬০০টি test পণ্যে Hamming loss তিন পদ্ধতিতে প্রায় একই (০.২৬৫, ০.২৬৭, ০.২৭৮): তত্ত্ব যেমন বলে, ঘর-ধরে ভুলের জন্য binary relevance যথেষ্ট। Subset accuracy-তে chain এগিয়ে: ০.৩৩০ থেকে ০.৩৭২ (premium → gift → fragile → express) আর ০.৩৬৫ (উল্টো ক্রম)। পুরো সমষ্টি মেলাতে আগের ট্যাগের খবর কাজে লাগছে। কিন্তু ক্রমের প্রভাবও স্পষ্ট: উল্টো ক্রমে micro F1 ০.৬২৯ থেকে কমে ০.৫৯৯, কারণ সবচেয়ে কঠিন ট্যাগ (express) আগে বসলে তার ভুল বাকিদের ঘাড়ে চাপে। শেষ লাইন: ১৬টি সম্ভাব্য সমষ্টির সবগুলোই ডেটায় আছে, তাই এখানে label powerset ১৬ শ্রেণির একটি multiclass সমস্যা হতো; ট্যাগ বেশি হলে সেটি আর সম্ভব থাকে না।
সতর্কতা: এটি একটি split আর একটি seed-এর ফল; কয়েকটি seed বা fold-এ যাচাই না করে ০.০৪-এর পার্থক্যে ভরসা করবেন না। আর scikit-learn-এ MultiOutputClassifier.predict_proba একটি array নয়, প্রতিটি ট্যাগের জন্য একটি করে (n, ২) array-র list দেয়; পরের ব্লকে তাই প্রতিটি থেকে দ্বিতীয় কলাম তোলা হয়েছে।
৮ · ট্যাগভিত্তিক threshold, আর শ্রেণির শ্রেণিবিন্যাস
Binary relevance প্রতিটি ট্যাগে একটি সম্ভাবনা দেয়, আর default threshold ০.৫. কিন্তু ০.৫ শুধু তখনই স্বাভাবিক, যখন metric প্রতিটি ঘরের ভুলকে সমান গোনে (Hamming). F1 positive-গুলোর ওপর জোর দেয়, তাই প্রায়ই নিচু threshold-এ সেরা হয়। নিয়ম একটাই: threshold বাছা হলো একটি hyperparameter বাছাই, তাই সেটি হবে আলাদা validation set-এ; test set ছোঁয়া হবে শুধু শেষে, একবার। Test দেখে threshold বাছলে test-এর ফল আর নিরপেক্ষ থাকে না, এটি data leakage.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.multioutput import MultiOutputClassifier
from sklearn.metrics import f1_score, hamming_loss
from bazar_catalog import make_products, make_tags, TAGS
df = make_products(n=1500, seed=7)
Y = make_tags(df).to_numpy()
X = pd.get_dummies(df, columns=["category"], dtype=float).to_numpy()
Xa, Xte, Ya, Yte = train_test_split(X, Y, test_size=0.4, random_state=42)
Xtr, Xva, Ytr, Yva = train_test_split(Xa, Ya, test_size=0.33, random_state=0)
sc = StandardScaler().fit(Xtr)
br = MultiOutputClassifier(LogisticRegression(max_iter=1000))
br.fit(sc.transform(Xtr), Ytr)
proba = lambda X: np.column_stack([p[:, 1] for p in br.predict_proba(sc.transform(X))])
Pva, Pte = proba(Xva), proba(Xte) # প্রতিটি ট্যাগের P(হ্যাঁ)
grid = np.round(np.arange(0.10, 0.91, 0.05), 2)
best = []
for j, tag in enumerate(TAGS): # শুধু validation দেখে বাছাই
f1s = [f1_score(Yva[:, j], Pva[:, j] >= t) for t in grid]
best.append(grid[int(np.argmax(f1s))])
print(f"{tag:8s} best t={best[-1]:.2f} val F1: 0.5 -> {f1s[8]:.3f},"
f" best -> {max(f1s):.3f}")
for name, t in [("t = 0.5", 0.5), ("per-label t", np.array(best))]:
P = (Pte >= t).astype(int) # test-এ একবারই
print(f"TEST {name:12s} micro F1={f1_score(Yte, P, average='micro'):.3f}"
f" macro F1={f1_score(Yte, P, average='macro'):.3f}"
f" hamming={hamming_loss(Yte, P):.3f}")আসল আউটপুট (ব্রাউজারের Python):
premium best t=0.50 val F1: 0.5 -> 0.800, best -> 0.800
gift best t=0.30 val F1: 0.5 -> 0.630, best -> 0.710
fragile best t=0.30 val F1: 0.5 -> 0.734, best -> 0.738
express best t=0.30 val F1: 0.5 -> 0.589, best -> 0.675
TEST t = 0.5 micro F1=0.627 macro F1=0.643 hamming=0.264
TEST per-label t micro F1=0.677 macro F1=0.689 hamming=0.278
ডেটা তিন ভাগে: train (৬০৩), validation (২৯৭), test (৬০০)। Validation-এ premium-এর সেরা threshold ০.৫ নিজেই, কিন্তু বাকি তিনটির ০.৩০; gift আর express-এ F1 অনেক বাড়ে (০.৬৩০ থেকে ০.৭১০, ০.৫৮৯ থেকে ০.৬৭৫), fragile-এ সামান্য। Test-এ একবার প্রয়োগ করে micro F1 ০.৬২৭ থেকে ০.৬৭৭ আর macro F1 ০.৬৪৩ থেকে ০.৬৮৯. কিন্তু Hamming loss ০.২৬৪ থেকে বেড়ে ০.২৭৮: নিচু threshold বেশি স্টিকার লাগায়, কয়েকটি ভুল স্টিকারসহ। একই পরিবর্তন এক metric-এ উন্নতি, অন্যটিতে অবনতি; তাই metric আগে ঠিক করে তারপর threshold বাছতে হয়।
সেরা F1-এর threshold কেন ০.৫-এর নিচে? ধরুন একটি ট্যাগের সম্ভাবনা সত্যিই ঠিক (calibrated). তখন দেখানো যায়, F1 সর্বোচ্চ করার threshold সেই সর্বোচ্চ F1-এর ঠিক অর্ধেক। এখানে সেরা F1 ০.৬৮–০.৭৪, তাই প্রত্যাশিত threshold ০.৩৪–০.৩৭-এর কাছে; grid-এ পাওয়া ০.৩০ এর কাছাকাছি। সম্ভাবনা calibrated না হলে এই সম্পর্ক খাটে না, আর তখন validation-এর grid-ই ভরসা। scikit-learn 1.5 থেকে TunedThresholdClassifierCV cross-validation দিয়ে threshold বাছে, তবে শুধু binary classifier-এর জন্য; multilabel-এ ব্লকের মতো প্রতিটি ট্যাগে হাতে করতে হয়।
শ্রেণির শ্রেণিবিন্যাস (hierarchical labels): বাস্তব ক্যাটালগে শ্রেণি একটি গাছ: ইলেকট্রনিক্স → মোবাইল আনুষঙ্গিক → চার্জার. তিনটি পথ প্রচলিত। (১) Flat: শুধু পাতার শ্রেণিগুলো নিয়ে একটি বড় multiclass model; সহজ, কিন্তু গাছের তথ্য হারায়। (২) Local: প্রতিটি শাখায় একটি ছোট classifier, ওপর থেকে নিচে নামা; প্রথম স্তরের ভুল আর শোধরানো যায় না। (৩) Global: একটি model পুরো পথ একসঙ্গে অনুমান করে। Hierarchical classification-এ ভুলেরও মাত্রা আছে: চার্জারকে ডেটা ক্যাবল বলা আর শ্যাম্পু বলা সমান ভুল নয়, তাই গাছের দূরত্ব ধরে মাপা metric ব্যবহার হয়। একটি সাবধানতা: গাছের নিচের দিকে শ্রেণিগুলো খুব ছোট হয়, তাই সেখানে imbalance আর label-এর মানের (পর্ব ০৫) সমস্যা সবচেয়ে বেশি।
আরও গভীরে: ECOC-এর মূল প্রবন্ধ Dietterich ও Bakiri, “Solving Multiclass Learning Problems via Error-Correcting Output Codes”, Journal of Artificial Intelligence Research (১৯৯৫); Allwein, Schapire ও Singer (২০০০) OvR, OvO আর ECOC-কে margin-ভিত্তিক একটি কাঠামোয় এক করেন। Rifkin ও Klautau-র “In Defense of One-Vs-All Classification” (JMLR, ২০০৪) দেখায়, ভালোভাবে tune করা base classifier-এ সাধারণ OvR প্রায়ই জটিল বিকল্পের সমান। Classifier chain এসেছে Read, Pfahringer, Holmes ও Frank-এর ২০০৯ সালের কাজ থেকে; Dembczyński, Waegeman, Cheng ও Hüllermeier (Machine Learning, ২০১২) দেখান কোন loss-এর জন্য ট্যাগের নির্ভরতা জরুরি। F1-এর সেরা threshold = সেরা F1-এর অর্ধেক, এই ফলটি Lipton, Elkan ও Naryanaswamy-র ২০১৪ সালের প্রবন্ধে।
পরীক্ষা ও ইন্টারভিউতে: (১) K = ১০ শ্রেণিতে OvR আর OvO কয়টি model? ১০ আর ৪৫. (২) z = (২, ১, ০)-এর softmax? (০.৬৬৫, ০.২৪৫, ০.০৯০); সব z-এ একই সংখ্যা যোগ করলে বদলায় না। (৩) ৫ পণ্য × ৪ ট্যাগে ৩টি ঘর ভুল, Hamming loss? ৩/২০ = ০.১৫. (৪) Multilabel-এ micro আর macro F1-এর পার্থক্য? Micro সব ঘর একসঙ্গে গোনে, তাই ঘন ঘন আসা ট্যাগ প্রাধান্য পায়; macro প্রতিটি ট্যাগকে সমান ওজন দেয়। (৫) Classifier chain কখন binary relevance-এর চেয়ে ভালো? যখন ট্যাগগুলো x জানার পরও পরস্পর-নির্ভর আর metric পুরো সমষ্টি মাপে (subset accuracy); Hamming loss-এ সাধারণত লাভ কম।
৯ · AI-এর যুগে — AI-এর লেখা multiclass আর multilabel কোড যাচাই
“পণ্যের শ্রেণি আর ট্যাগ predict করার কোড দাও” বললে AI assistant দ্রুত চলনসই কোড দেয়, কিন্তু এই বিষয়ে কয়েকটি ভুল বারবার দেখা যায়, আর সেগুলো প্রায়ই কোনো error ছাড়াই ভুল সংখ্যা দেয়।
| AI প্রায়ই যা করে | কেন সমস্যা | কীভাবে ধরবেন |
|---|---|---|
LogisticRegression(multi_class="ovr") বা penalty=None | পুরোনো API; 1.8-এ multi_class দিলে TypeError, penalty=None-তে FutureWarning | নিজের version-এর docs; OvR চাইলে OneVsRestClassifier |
Multilabel-এ accuracy_score ছেপে “accuracy ৩৩%, model খারাপ” বলে | এটি subset accuracy, সবচেয়ে কঠোর মাপ | Hamming loss আর micro/macro F1 পাশে ছাপুন |
Multilabel-এ f1_score(Y, P) default-এ ডাকে | average="binary"-এ ValueError, বা ভুল average বাছে | average স্পষ্ট লিখুন, আর কেন সেটি |
| Test set দেখে threshold বা chain-এর ক্রম বাছে | Test-এর ফল আশাবাদী, leakage | Train/validation/test আলাদা; test একবারই |
Multilabel-এর জন্য LabelEncoder-এ ট্যাগ-স্ট্রিং জোড়ে | অজান্তে label powerset; অদেখা সমষ্টি অনুমান অসম্ভব | ট্যাগ তালিকা থেকে MultiLabelBinarizer দিয়ে n × L matrix |
| শুধু accuracy দেখিয়ে বহু শ্রেণির model “ভালো” বলে | ছোট শ্রেণির recall লুকিয়ে থাকে | Confusion matrix, প্রতি শ্রেণির recall আর macro F1 |
predict_proba-কে সবসময় (n, L) array ধরে | MultiOutputClassifier list দেয়; ভুল সূচিতে ভুল ট্যাগ | আকার ছাপুন: type, len, প্রথম উপাদানের shape |
AI-কে শুধু কোড নয়, সমস্যার ধরন, metric আর যাচাইও চাইতে বলুন:
AI-এর দেওয়া কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১SVM-এর জগতে দীর্ঘ বিতর্ক ছিল: OvR, OvO, নাকি সরাসরি বহু শ্রেণির SVM? Rifkin আর Klautau-র “In Defense of One-Vs-All” কী বলেছিল, আর এখান থেকে কী শিক্ষা?
১৯৯০-এর দশকে SVM জনপ্রিয় হওয়ার পর একটি প্রশ্ন বড় হয়ে ওঠে: SVM স্বভাবে দুই শ্রেণির, তাহলে হাতে লেখা অঙ্ক (১০ শ্রেণি) বা অক্ষর চেনায় কোন পথ? কয়েকটি গবেষক দল সরাসরি বহু শ্রেণির SVM প্রস্তাব করেন, যেখানে একটি optimization-এ সব শ্রেণির margin একসঙ্গে। Chih-Wei Hsu আর Chih-Jen Lin ২০০২ সালে IEEE Transactions on Neural Networks-এ কয়েকটি পদ্ধতি তুলনা করে দেখান, OvO নির্ভুলতায় অন্যদের প্রায় সমান আর training-এ ব্যবহারিকভাবে সুবিধাজনক; তাঁদের LIBSVM library ভেতরে OvO-ই চালায়, আর scikit-learn-এর SVC আজও সেই পথে।
২০০৪ সালে Ryan Rifkin আর Aldebaro Klautau Journal of Machine Learning Research-এ একটি প্রবন্ধে বিপরীত দিক থেকে একটি দাবি করেন: base classifier ভালোভাবে tune করা হলে (যেমন regularization আর kernel-এর parameter যত্ন করে বাছা) সাধারণ OvR জটিল বহু শ্রেণির পদ্ধতির সমান নির্ভুল, আর আগের অনেক তুলনায় যে পার্থক্য দেখা গিয়েছিল তার বড় অংশ এসেছিল অসম tuning থেকে। এই দাবি সব ডেটায় সমানভাবে খাটে কি না, তা নিয়ে আলোচনা চলেছে, তবে প্রবন্ধটির মূল সতর্কবার্তা বহুল উদ্ধৃত।
শিক্ষাটি পদ্ধতির চেয়ে পরীক্ষার নকশা নিয়ে। দুটি কৌশল তুলনা করার সময় একটির hyperparameter যত্ন করে বাছা আর অন্যটির default রাখা একটি সাধারণ ভুল; তখন “নতুন পদ্ধতি ভালো” আসলে “বেশি tune করা পদ্ধতি ভালো”। এই পর্বের প্রথম ব্লকেও OvR আর multinomial-এর পার্থক্য একটি split আর default C-তে মাপা; C বদলালে বা অন্য split নিলে ক্রম বদলাতে পারে। ন্যায্য তুলনার জন্য প্রতিটি পদ্ধতিকে একই বাজেটে tune করা, একাধিক fold বা seed দেখা, আর পার্থক্যের অনিশ্চয়তা মাপা লাগে (পর্ব ২২-এর paired test).
মূল উপলব্ধি: বহু শ্রেণির কৌশলের পার্থক্য প্রায়ই tuning-এর পার্থক্যের চেয়ে ছোট; তুলনা ন্যায্য না হলে সিদ্ধান্তও বিশ্বাসযোগ্য নয়।
প্র ০২Richard Hamming-এর error-correcting code থেকে Dietterich আর Bakiri-র ECOC: যোগাযোগের প্রকৌশলের একটি ধারণা classification-এ কীভাবে এল, আর কোথায় তার সীমা?
Richard Hamming ১৯৪০-এর দশকের শেষে Bell Labs-এ কাজ করার সময় যন্ত্রের পড়া ডেটার ভুলে বিরক্ত হয়ে এমন কোড খোঁজেন, যা কয়েকটি উল্টে যাওয়া বিট নিজেই ধরতে আর শোধরাতে পারে; তাঁর বিখ্যাত প্রবন্ধ ১৯৫০ সালে প্রকাশিত হয়। মূল ভাবনা: বৈধ কোড-শব্দগুলোকে একে অপরের থেকে দূরে রাখুন। দুটি কোড-শব্দের মধ্যে কমপক্ষে d-টি বিটের পার্থক্য থাকলে (Hamming distance d), প্রায় (d − ১)/২টি বিট ভুল হলেও সবচেয়ে কাছের বৈধ শব্দটি আসলটিই থাকে। এই পর্বের Hamming loss-ও একই দূরত্বের ধারণা থেকে নাম পেয়েছে।
Thomas Dietterich আর Ghulum Bakiri ১৯৯৫ সালে Journal of Artificial Intelligence Research-এ এই ভাবনা বহু শ্রেণির শেখায় আনেন। প্রতিটি শ্রেণি একটি কোড-শব্দ, প্রতিটি বিট একটি দুই-শ্রেণির classifier. কয়েকটি classifier ভুল করলেও, কোড-শব্দগুলো যথেষ্ট দূরে থাকলে চূড়ান্ত উত্তর ঠিক থাকতে পারে। তাঁদের পরীক্ষায় decision tree আর neural network-এ ECOC প্রায়ই OvR-এর চেয়ে ভালো করেছিল। পরে Erin Allwein, Robert Schapire আর Yoram Singer ২০০০ সালে দেখান, OvR, OvO আর ECOC একই কাঠামোর ভিন্ন code matrix; OvO-তে প্রতিটি কলামে দুটি শ্রেণি ছাড়া বাকিরা “উপেক্ষিত”।
কিন্তু যোগাযোগ আর classification-এর মধ্যে একটি বড় পার্থক্য আছে। যোগাযোগের চ্যানেলে বিটের ভুল সাধারণত পরস্পর স্বাধীন; classification-এ একই feature থেকে সব classifier শেখে, তাই তাদের ভুল প্রায়ই একসঙ্গে ঘটে। দ্বিতীয়ত, প্রতিটি কলামের দুই-শ্রেণির সমস্যা কতটা কঠিন, তা নির্ভর করে কোন শ্রেণিগুলো এক দলে পড়ল তার ওপর। এই পর্বের run-এ এলোমেলো কোড আর linear base model-এ ECOC সবচেয়ে কম accuracy দিয়েছে (০.৭৩৯), সম্ভবত কিছু কলাম এমন দল বানিয়েছে যা সরলরেখায় আলাদা হয় না।
মূল উপলব্ধি: ECOC দেখায় যে বহু শ্রেণির সমস্যা ভাঙার পথ অসংখ্য; কিন্তু redundancy তখনই কাজ করে, যখন ভুলগুলো যথেষ্ট স্বাধীন আর প্রতিটি উপ-সমস্যা base model-এর সাধ্যের মধ্যে।
প্র ০৩Classifier chain আসার পর অনেকে ধরে নিয়েছিলেন ট্যাগের নির্ভরতা ধরলেই সব metric-এ লাভ। Dembczyński ও সহকর্মীদের বিশ্লেষণ কেন এই ধারণা শুধরে দিল?
Jesse Read, Bernhard Pfahringer, Geoff Holmes আর Eibe Frank ২০০৯ সালের ECML PKDD সম্মেলনে classifier chain প্রস্তাব করেন (জার্নাল রূপ Machine Learning, ২০১১)। ধারণাটি সরল আর আকর্ষণীয়: binary relevance-এর প্রায় সমান খরচে ট্যাগের নির্ভরতা ধরা। সংগীতের আবেগ, খবরের বিষয়, ছবির দৃশ্য, এমন অনেক multilabel ডেটায় chain আর তার ensemble ভালো ফল দেখায়, আর পদ্ধতিটি দ্রুত জনপ্রিয় হয়।
Krzysztof Dembczyński, Eyke Hüllermeier আর সহকর্মীরা ২০১০ থেকে কয়েকটি প্রবন্ধে প্রশ্নটি অন্যভাবে করেন: কোন loss-এর জন্য কোন নিয়ম তত্ত্বে সেরা? উত্তরে একটি পরিষ্কার বিভাজন আসে। Hamming loss প্রতিটি ঘর আলাদা গোনে, তাই সেরা নিয়ম প্রতিটি ট্যাগের marginal সম্ভাবনা P(yj = ১ | x) দেখে আলাদা সিদ্ধান্ত; নির্ভরতা জানার দরকার নেই। Subset ০/১ loss পুরো সমষ্টি গোনে, তাই সেরা নিয়ম যৌথ বণ্টনের mode, অর্থাৎ সবচেয়ে সম্ভাব্য সমষ্টি; সেখানে নির্ভরতা জরুরি। দুটি সমাধান আলাদা হতে পারে, আর একটি model দুই loss-এ একসঙ্গে সেরা হতে পারে না সাধারণভাবে। তাঁরা ২০১০ সালে probabilistic classifier chain প্রস্তাব করেন, যা chain rule দিয়ে যৌথ সম্ভাবনা হিসাব করে আসল mode খোঁজে; সাধারণ chain-এর লোভী (greedy) অনুমান সেই mode-এর একটি আসন্ন হিসাব মাত্র।
এই পর্বের run এই তত্ত্বের সঙ্গে মেলে: chain-এ subset accuracy ০.৩৩০ থেকে ০.৩৭২, কিন্তু Hamming loss প্রায় একই (০.২৬৫ বনাম ০.২৬৭)। আরেকটি সূক্ষ্ম কথা: ডেটায় ট্যাগগুলোর correlation থাকলেই chain লাভ দেবে না। যদি সব নির্ভরতা x দিয়েই ব্যাখ্যা হয় (দুটি ট্যাগই শুধু দামের ওপর নির্ভর করে), তাহলে x জানার পর ট্যাগগুলো স্বাধীন, আর chain-এর বাড়তি feature কোনো নতুন তথ্য আনে না। এই পর্বের ডেটায় একটি অদেখা কারণ রাখা হয়েছে ঠিক এজন্যই।
মূল উপলব্ধি: “নির্ভরতা ধরা” নিজে লক্ষ্য নয়; loss ঠিক করে কোন তথ্য দরকার, আর সেই অনুযায়ী পদ্ধতি বাছাই।
প্র ০৪Softmax নামটি এল ১৯৯০ সালের দিকে neural network থেকে, কিন্তু একই সূত্র অর্থনীতিতে আগেই “multinomial logit” নামে ছিল। McFadden-এর পছন্দের মডেল আর “লাল বাস, নীল বাস” সমস্যা আমাদের কী শেখায়?
একজন মানুষ কয়েকটি বিকল্প থেকে একটি বাছেন: বাস, ট্রেন, না গাড়ি। মনোবিজ্ঞানী R. Duncan Luce ১৯৫৯ সালে একটি পছন্দের নিয়ম (choice axiom) প্রস্তাব করেন, যার গাণিতিক রূপ softmax-এর মতো: প্রতিটি বিকল্পের একটি “ওজন”, আর বাছার সম্ভাবনা সেই ওজনের অনুপাত। অর্থনীতিবিদ Daniel McFadden ১৯৭০-এর দশকে এই ধরনের model-কে প্রতিটি বিকল্পের উপযোগিতা (utility) আর এলোমেলো অংশ দিয়ে ভিত্তি দেন: এলোমেলো অংশ একটি বিশেষ (Gumbel) বণ্টন মানলে বাছার সম্ভাবনা হুবহু softmax হয়। যাতায়াতের পরিকল্পনায় এই কাজের জন্য তিনি ২০০০ সালে অর্থনীতিতে নোবেল পুরস্কার ভাগ করে পান। Neural network-এ John Bridle ১৯৯০ সালের দিকে “softmax” নামটি জনপ্রিয় করেন: এটি max-এর একটি নরম, derivative নেওয়া যায় এমন রূপ।
অর্থনীতির এই ইতিহাস softmax-এর একটি লুকানো অনুমান সামনে আনে, যাকে বলে irrelevant alternatives থেকে স্বাধীনতা (IIA): দুটি বিকল্পের সম্ভাবনার অনুপাত অন্য কোনো বিকল্প থাকল কি না তার ওপর নির্ভর করে না, কারণ pa/pb = eza − zb. বিখ্যাত উদাহরণ: একজন গাড়ি আর লাল বাসের মধ্যে সমান ভাগে বাছেন। এখন একটি নীল বাস যোগ হলো, যা লাল বাসের হুবহু মতো। বাস্তবে বাসযাত্রীরা দুই বাসে ভাগ হবেন, গাড়ি ১/২-ই থাকবে। কিন্তু IIA মানলে গাড়ি : লাল বাস : নীল বাস = ১ : ১ : ১, গাড়ির ভাগ নেমে ১/৩.
Classification-এ এর অর্থ: খুব কাছাকাছি দুটি শ্রেণি (যেমন “পুরুষের শার্ট” আর “পুরুষের টি-শার্ট”) একটি softmax-এ থাকলে model তাদের মধ্যে সম্ভাবনা ভাগ করে দেয়, আর প্রতিটির সম্ভাবনা কম দেখায়, যদিও “উপরের পোশাক” হওয়ার সম্ভাবনা উঁচু। অর্থনীতির সমাধান nested logit (আগে দল, তারপর দলের ভেতরে বাছাই); classification-এ তার ভাই অংশ ৮-এর শ্রেণি-গাছ।
মূল উপলব্ধি: Softmax শুধু একটি normalization নয়; এটি একটি পছন্দের model, যার অনুমান (IIA) কাছাকাছি শ্রেণির ক্ষেত্রে ভাঙতে পারে, আর তখন শ্রেণি-গাছ বা দলভিত্তিক model ভালো বিকল্প।
প্র ০৫বাজারঘরের ক্যাটালগ ৫ শ্রেণি থেকে ৫,০০০ শ্রেণি আর কয়েকশো ট্যাগে গেলে কী কী ভাঙে? “Extreme” multilabel আর long-tail শ্রেণির সমস্যা কীভাবে ভাবা হয়?
বড় অনলাইন দোকানের ক্যাটালগে শ্রেণির সংখ্যা হাজার ছাড়ায়, আর শ্রেণিগুলোর মাপ সাধারণত দীর্ঘ লেজের (long tail) বণ্টন মানে: কয়েকটি শ্রেণিতে লাখো পণ্য, বেশিরভাগ শ্রেণিতে কয়েকটি মাত্র। গবেষণায় লাখ বা কোটি label-এর সমস্যাকে extreme multi-label classification বলা হয়, যেমন একটি লেখার জন্য বিশাল বিষয়-তালিকা থেকে প্রাসঙ্গিক কয়েকটি বাছা; এর জন্য আলাদা benchmark আর পদ্ধতির একটি পুরো ধারা আছে।
প্রথমে খরচ ভাঙে। OvO-তে ৫,০০০ শ্রেণিতে প্রায় ১.২৫ কোটি model: অসম্ভব। OvR-এ ৫,০০০টি model, প্রতিটি পুরো ডেটায়; অনুমানের সময় প্রতিটি পণ্যে ৫,০০০টি score. Softmax-এর হর-ও ৫,০০০টি পদের যোগফল। তাই বড় পরিসরে শ্রেণি-গাছ, label-এর গুচ্ছ বানানো (cluster), বা পণ্য আর শ্রেণি দুটিকে একই vector-জগতে এনে কাছের শ্রেণি খোঁজা, এমন পদ্ধতি লাগে।
দ্বিতীয়ত metric ভাঙে। হাজার ট্যাগে subset accuracy প্রায় সবসময় শূন্যের কাছে, আর Hamming loss প্রায় শূন্য, কারণ বেশিরভাগ ঘরই ০ আর model “সব না” বললেই ভালো দেখায়। তাই এমন সমস্যায় ranking-এর metric চলে: সেরা k-টি অনুমানের মধ্যে কয়টি ঠিক (precision@k). বিরল label-এ নজর দিতে propensity-ভিত্তিক সংশোধিত metric-ও প্রস্তাবিত হয়েছে, কারণ বিরল label-এর ঠিক অনুমানই সাধারণত বেশি মূল্যবান।
তৃতীয়ত ডেটা ভাঙে। Long tail-এর শ্রেণিগুলোতে কয়েকটি উদাহরণ, আর তাদের label প্রায়ই অসম্পূর্ণ: একটি পণ্যে প্রাসঙ্গিক ট্যাগটি না লাগানো থাকলে সেটি “না” নয়, “অজানা”। এই missing-label সমস্যা multilabel-এ সাধারণ, আর পর্ব ০৫-এর label noise-এর একটি বিশেষ রূপ। Stratified split-ও কঠিন হয়, কারণ সব ট্যাগ-সমষ্টি test-এ রাখা যায় না; multilabel-এর জন্য iterative stratification নামে বিশেষ কৌশল প্রস্তাবিত।
মূল উপলব্ধি: শ্রেণি বাড়লে শুধু model নয়, খরচ, metric আর label-এর মান তিনটিই নতুন করে ভাবতে হয়; ছোট সমস্যার নিয়ম বড় সমস্যায় চুপচাপ ভুল সংখ্যা দেয়।
অনুশীলন
হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। চাইলে পাতার ব্লকগুলোতে সংখ্যা বদলে যাচাই করুন।
-
পূর্ণ numerical: ১০টি সমান মাপের শ্রেণি, মোট ১০,০০০ সারি। (ক) OvR আর OvO কয়টি model বানায়, প্রতিটি model কত সারিতে train হয়, আর মোট সারি-ব্যবহার কত? (খ) Training-এর খরচ সারির সংখ্যার বর্গের সমানুপাতিক হলে (kernel SVM-এর মতো), কোনটি কত গুণ সস্তা? (গ) একটি পণ্যের তিন শ্রেণির score z = (২, ১, ০) হলে softmax সম্ভাবনা কত, আর আসল শ্রেণি দ্বিতীয়টি হলে cross-entropy loss কত?
(ক) OvR: ১০টি model, প্রতিটি ১০,০০০ সারিতে; মোট ১,০০,০০০ সারি-ব্যবহার। OvO: ১০ × ৯/২ = ৪৫টি model, প্রতিটি দুই শ্রেণির ২,০০০ সারিতে; মোট ৯০,০০০. সারির হিসাবে প্রায় সমান। (খ) খরচ ∝ m² হলে OvR: ১০ × (১০৪)² = ১০৯; OvO: ৪৫ × (২,০০০)² = ৪৫ × ৪ × ১০৬ = ১.৮ × ১০৮. OvO প্রায় ৫.৬ গুণ সস্তা। (গ) সবচেয়ে বড় ২ বিয়োগ করে (০, −১, −২); e-এর ঘাত (১, ০.৩৬৭৯, ০.১৩৫৩), যোগ ১.৫০৩২. সম্ভাবনা (০.৬৬৫২, ০.২৪৪৭, ০.০৯০০). দ্বিতীয় শ্রেণি আসল হলে loss = −ln ০.২৪৪৭ = ১.৪০৭৬.
-
সমালোচনা ও নকশা: বাজারঘরের একজন বিশ্লেষক ২০টি ট্যাগের model নিয়ে লিখেছেন: “প্রতিটি পণ্যের ট্যাগগুলো কমা দিয়ে জুড়ে একটি স্ট্রিং বানিয়ে
LabelEncoderদিয়ে শ্রেণি করেছি, তারপরLogisticRegression(multi_class='multinomial'). Test set-এ accuracy মাত্র ০.১২, তাই test set দেখে threshold কমিয়ে ০.২ করেছি, এখন accuracy ০.১৮. Model ব্যবহারযোগ্য নয়, বাদ দেওয়া উচিত।” কী কী সমস্যা, আর আপনি কীভাবে সাজাবেন?সমস্যা: (১) ট্যাগ জুড়ে শ্রেণি বানানো আসলে label powerset: ২০ ট্যাগে সম্ভাব্য সমষ্টি দশ লাখের বেশি, বেশিরভাগ training-এ নেই, আর অদেখা সমষ্টি কখনো অনুমান হবে না। (২)
multi_classscikit-learn 1.8-এ বাদ, এই কোড TypeError দেবে। (৩) Accuracy এখানে subset accuracy, ২০ ট্যাগে স্বাভাবিকভাবেই খুব কম; একমাত্র মাপ হিসেবে বিভ্রান্তিকর। (৪) Test set দেখে threshold বাছা leakage; ০.১৮ আর নিরপেক্ষ নয়। (৫) কোনো baseline নেই। নকশা: ট্যাগ-তালিকা থেকেMultiLabelBinarizer-এ n × ২০ matrix; binary relevance দিয়ে শুরু, পরে chain বা ensemble of chains; metric আগে ঠিক করা (micro/macro F1 আর Hamming loss, subset accuracy শুধু তথ্য হিসেবে); train/validation/test ভাগ, threshold শুধু validation-এ, প্রতি ট্যাগে আলাদা; baseline হিসেবে প্রতিটি ট্যাগের হার দিয়ে বানানো অনুমান; কয়েকটি seed বা fold-এ ফল। -
আউটপুট অনুমান করুন: চারটি সারি, তিনটি ট্যাগ। চালানোর আগে তিনটি লাইন লিখুন।
import numpy as np from sklearn.metrics import hamming_loss, accuracy_score, f1_score Y = np.array([[1, 0, 1], [0, 1, 0], [1, 1, 0], [0, 0, 1]]) P = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0], [1, 1, 1]]) print(round(hamming_loss(Y, P), 3), accuracy_score(Y, P)) print(round(f1_score(Y, P, average="micro"), 3), round(f1_score(Y, P, average="macro"), 3)) print(f1_score(Y, P, average=None).round(3))ডেস্কটপে সত্যিকারের run (Python 3.14.4, scikit-learn 1.9.1):
0.333 0.25 0.667 0.656 [0.8 0.5 0.667]Hamming: ভুল ঘর: সারি ১-এর তৃতীয় ট্যাগ, সারি ৩-এর দ্বিতীয়, সারি ৪-এর প্রথম আর দ্বিতীয়: ৪/১২ = ০.৩৩৩. Subset accuracy: শুধু সারি ২ হুবহু মেলে: ১/৪ = ০.২৫. Micro: TP = ৪ (১-১, ২-২, ৩-১, ৪-৩), FP = ২ (৪-১, ৪-২), FN = ২ (১-৩, ৩-২); F1 = ৮/(৮ + ২ + ২) = ০.৬৬৭. প্রতি ট্যাগ: প্রথম TP ২, FP ১ → ৪/৫ = ০.৮; দ্বিতীয় TP ১, FP ১, FN ১ → ২/৪ = ০.৫; তৃতীয় TP ১, FN ১ → ২/৩ = ০.৬৬৭; macro = ১.৯৬৭/৩ = ০.৬৫৬.
-
কোডিং:
OneVsOneClassifierব্যবহার না করে নিজে OvO লিখুন: প্রতিটি শ্রেণি-জোড়ায় শুধু সেই দুই শ্রেণির সারিতে একটিLogisticRegression, test-এ ভোট গোনা আর argmax. বাজারঘরের ৫ শ্রেণির ডেটায় scikit-learn-এর ফলের সঙ্গে মিলিয়ে দেখান, আর কয়টি সারিতে সর্বোচ্চ ভোট টাই হয়েছে তা গুনুন।import numpy as np from itertools import combinations from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.multiclass import OneVsOneClassifier from bazar_catalog import make_products df = make_products() X = df.drop(columns="category").to_numpy() y = df.category.astype("category").cat.codes.to_numpy() Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) sc = StandardScaler().fit(Xtr) # আগে ভাগ, তারপর scale Xtr, Xte = sc.transform(Xtr), sc.transform(Xte) def ovo_fit_predict(Xtr, ytr, Xte, K): votes = np.zeros((len(Xte), K)) for a, b in combinations(range(K), 2): # K(K-1)/2 duel m = (ytr == a) | (ytr == b) # শুধু এই দুই শ্রেণির সারি clf = LogisticRegression().fit(Xtr[m], (ytr[m] == b).astype(int)) win_b = clf.predict(Xte) == 1 votes[:, b] += win_b votes[:, a] += ~win_b return votes.argmax(axis=1), votes mine, votes = ovo_fit_predict(Xtr, ytr, Xte, 5) skl = OneVsOneClassifier(LogisticRegression()).fit(Xtr, ytr).predict(Xte) print("duels:", 5 * 4 // 2, " agree with sklearn:", round(float(np.mean(mine == skl)), 3)) print("accuracy mine:", round(float(np.mean(mine == yte)), 3), " sklearn:", round(float(np.mean(skl == yte)), 3)) top2 = np.sort(votes, axis=1)[:, -2:] print("rows with a tied top vote:", int(np.sum(top2[:, 0] == top2[:, 1])))ডেস্কটপে সত্যিকারের run (Python 3.14.4, scikit-learn 1.9.1, পাতার
bazar_catalog.pyএকই ফোল্ডারে রেখে):duels: 10 agree with sklearn: 1.0 accuracy mine: 0.828 sklearn: 0.828 rows with a tied top vote: 0১০টি duel, আর নিজের লেখা OvO scikit-learn-এর সঙ্গে ১৮০টি test সারির সবগুলোতে মেলে; accuracy ০.৮২৮, পাতার প্রথম ব্লকের OvO-র সমান। কোনো সারিতে সর্বোচ্চ ভোট টাই হয়নি, তাই scikit-learn-এর আত্মবিশ্বাস-ভিত্তিক টাই-ভাঙা এখানে কাজে আসেনি; টাই থাকলে
argmaxপ্রথম শ্রেণিটি নিত আর ফল আলাদা হতে পারত। খেয়াল করুন: scaler-কে শুধু train দিয়ে fit করা হয়েছে, আর প্রতিটি duel-এ label (y == b) ০/১ বানানো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমMultilabel model-এ ট্যাগভিত্তিক threshold বাছার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ডেটাকে train, validation আর test-এ ভাগ করা
- শুধু train দিয়ে scaler আর প্রতিটি ট্যাগের model fit করা
- Validation সারিতে প্রতিটি ট্যাগের সম্ভাবনা বের করা
- প্রতিটি ট্যাগের জন্য validation-এ সেরা F1-এর threshold বাছা
- বাছা threshold test set-এ একবার প্রয়োগ করে metric রিপোর্ট করা
১২টি শ্রেণির একটি সমস্যায় one-vs-one কৌশল কয়টি দুই-শ্রেণির model train করে?
- ১২
- ২৪
- ৬৬
- ১৩২
৫টি পণ্য আর ৪টি ট্যাগের একটি multilabel অনুমানে মোট ৩টি ঘর ভুল, আর ৫টি পণ্যের মধ্যে ২টির সব ট্যাগ হুবহু ঠিক। Hamming loss আর subset accuracy কত?
- Hamming loss ০.৬, subset accuracy ০.৪
- Hamming loss ০.১৫, subset accuracy ০.৪
- Hamming loss ০.৮৫, subset accuracy ০.৬
- Hamming loss ০.১৫, subset accuracy ০.৮৫
একটি multinomial logistic regression-এর চারটি শ্রেণির score (১.২, ২.০, −০.৩, ০.৫). প্রতিটি score-এ ১০ যোগ করলে softmax সম্ভাবনার কী হয়?
- সবচেয়ে বড় score-এর সম্ভাবনা ১-এর আরও কাছে যায়
- সব সম্ভাবনা ০.২৫-এর দিকে যায়
- সম্ভাবনার যোগফল ১-এর বেশি হয়ে যায়
- কিছুই বদলায় না
ট্যাগের ক্রম premium → gift → fragile দিয়ে একটি classifier chain বানানো হয়েছে। নতুন পণ্যে অনুমানের সময় gift-এর model কী feature হিসেবে পায়?
- পণ্যের feature x আর premium model-এর অনুমিত premium
- পণ্যের feature x আর আসল premium মান
- শুধু পণ্যের feature x
- x, premium আর fragile-এর অনুমিত মান
একটি multilabel model প্রতিটি ট্যাগের আলাদা সম্ভাবনা ০.৫-এর বেশি হলে “হ্যাঁ” বলে, ট্যাগের পারস্পরিক নির্ভরতা না দেখে। সম্ভাবনাগুলো সঠিক ধরলে, তত্ত্বে কোন metric-এর জন্য এই নিয়ম সেরা?
- Subset accuracy
- Hamming loss
- Micro F1
- Macro F1
scikit-learn 1.8-এ ৫ শ্রেণির ডেটায় LogisticRegression() default সেটিংয়ে fit করলে কী ঘটে, আর one-vs-rest চাইলে কী করতে হয়?
- Default-এ OvR চলে; multinomial চাইলে multi_class="multinomial" দিতে হয়
- Error আসে, কারণ logistic regression শুধু দুই শ্রেণির
- একটি multinomial (softmax) model fit হয়; OvR চাইলে OneVsRestClassifier-এ মুড়তে হয়
- Default-এ OvO চলে, ১০টি model
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ধরন আগে: multiclass = K-টির ঠিক একটি; multilabel = L দৈর্ঘ্যের ০/১ vector. Y-এর আকার (n,) না (n, L), সেটিই প্রথম যাচাই।
- গোনা: OvR K-টি model (প্রতিটি সব সারিতে, argmax), OvO K(K − ১)/২-টি (প্রতিটি দুই শ্রেণিতে, ভোট), softmax ১টি, ECOC code_size × K. OvR-এ ফাঁকা আর দ্বন্দ্বের এলাকা থাকে, softmax-এ থাকে না।
- Softmax: pk = ezk/Σezj, loss −log pআসল, gradient p − y; সব score-এ একই সংখ্যা যোগ করলে কিছু বদলায় না। scikit-learn 1.8-এ
LogisticRegressionবহু শ্রেণিতে নিজেই softmax;multi_classনেই। - Multilabel: binary relevance (Hamming loss-এর জন্য যথেষ্ট), classifier chain (যৌথ সমষ্টি, ক্রম-নির্ভর, ভুল ছড়ায়), label powerset (২L বিস্ফোরণ)। Metric: Hamming loss, subset accuracy, micro আর macro F1.
- Threshold আর imbalance: প্রতিটি ট্যাগের threshold validation-এ বাছুন, test-এ একবার; F1-এর সেরা threshold প্রায়ই ০.৫-এর নিচে। বহু শ্রেণিতে accuracy-র পাশে প্রতি শ্রেণির recall আর macro F1.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৬ · Probability calibration — Platt, isotonic আর reliability diagramপরবর্তী পর্বReliability diagram, ECE, Brier, Platt আর isotonic দিয়ে সম্ভাবনাকে সৎ করা।
- মেশিন লার্নিং · কোর্স হোম৭৭টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ২৬ (Probability calibration) এখান থেকে খুলবেন।
- পর্ব ১৮ · Maximum likelihood আর MAP — model fit করার মূলনীতিআগের ভিত্তিCross-entropy কেন negative log-likelihood, আর MAP-এ regularization.
- পর্ব ২২ · Model তুলনার পরিসংখ্যান — paired test, McNemar আর একাধিক তুলনাআগের ভিত্তিOvR আর multinomial-এর ছোট পার্থক্য বিশ্বাসযোগ্য কি না, যাচাইয়ের পথ।
- পর্ব ০৫ · Label আর ডেটার মানআগের ভিত্তিট্যাগ দেওয়ার নির্দেশিকা, label noise আর annotator-দের মিল।
- AI-এর ভিত্তি · পর্ব ৫৯ · Logistic regressionআগের ভিত্তিSigmoid, log loss আর softmax-এর প্রথম পরিচয়।
- AI-এর ভিত্তি · পর্ব ৬০ · মূল্যায়নের metricআগের ভিত্তিConfusion matrix, F1 আর macro, micro, weighted গড়।
- AI-এর ভিত্তি · পর্ব ৬৪ · scikit-learn pipelineআগের ভিত্তিScaler আর model একসঙ্গে, leakage-মুক্তভাবে।
- Data Structures আর Algorithmsপাশের ট্র্যাকCombination গোনা, গাছের গঠন আর জটিলতার হিসাব, যা OvO আর শ্রেণি-গাছের খরচ বোঝায়।