সরাসরি পর্বে যান
পর্ব ০৫ · ৭৭-এর মধ্যে · মডিউল ১

Label আর ডেটার মান — annotation, noise, agreement আর data versioning

Labels and Data Quality — Annotation, Inter-Annotator Agreement, Label Noise and Data Versioning

Taught in Bangla, this lesson covers where labels come from, writing annotation guidelines, and computing Cohen's and Fleiss' kappa by hand and in code. It measures how label noise hurts different models, finds suspect labels with out-of-fold probabilities and confident learning, and shows real desktop runs of cleanlab and DVC for label versioning, plus documentation and privacy checks.

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

  • Label-এর তিন উৎস, আর একটি কাজের annotation guideline কীভাবে লিখতে হয়
  • Cohen আর Fleiss kappa হাতে হিসাব করা, কোডে মিলিয়ে দেখা, আর kappa-র ফাঁদ চেনা
  • Label noise-এর ধরন, model-এর ওপর তার প্রভাব, আর noisy test set কীভাবে মাপ বিকৃত করে
  • Confident learning-এর নিয়ম দিয়ে ভুল label খোঁজা, আর detector-এর precision ও recall মাপা
  • Label-এর গোপনীয়তা, datasheet, আর সারি-ধরে hash ও DVC দিয়ে ডেটার version রাখা

১ · Label কোথা থেকে আসে — মানুষ, log আর নিয়ম

AI-এর ভিত্তি কোর্সের পর্ব ৭৪-এ দেখেছেন, একটি ML প্রকল্পের প্রথম কঠিন সিদ্ধান্ত হলো label-এর সংজ্ঞা। আর পর্ব ৭৩-এর ক্যাপস্টোনে বাজারঘরের রিভিউয়ে সত্যিই কয়েকটি ভুল label পাওয়া গিয়েছিল। এই পর্বে প্রশ্নটি আরও গভীরে: label কে বানায়, কতটা বিশ্বাস করা যায়, আর ভুল থাকলে কী হয়।

পরীক্ষার খাতা দেখার কথা ভাবুন। একই খাতা দুজন পরীক্ষক দেখলে নম্বর পুরোপুরি মেলে না; বিশেষ করে রচনামূলক প্রশ্নে। তাই বোর্ড একটি উত্তর-নির্দেশিকা দেয়, আর বড় পার্থক্য হলে তৃতীয় পরীক্ষক দেখেন। ML-এর label ঠিক এই খাতার নম্বর: একটি মানুষের বিচার, যা নির্দেশিকা, ক্লান্তি আর রুচির ওপর নির্ভর করে।

বাজারঘরের তিনটি কাজ ধরা যাক: রিভিউয়ের মনোভাব (positive, negative, neutral), কোন ক্রেতা চলে যাবেন (churn), আর কোন অর্ডার ভুয়া। তিনটির label তিন রকম উৎস থেকে আসে।

মানুষের annotation

নিজের দল বা বাইরের crowdsourcing কর্মী প্রতিটি রিভিউ পড়ে label দেন। অর্থ বোঝা যায়, কিন্তু ধীর, দামি, আর মানুষে মানুষে মতভেদ থাকে।

উদাহরণ: রিভিউয়ের মনোভাব

Log থেকে আসা label

আচরণ থেকে label: ৯০ দিনে অর্ডার নেই মানে churn, chargeback হলে ভুয়া। প্রচুর আর সস্তা, কিন্তু দেরিতে আসে (label delay), আর শুধু যা ঘটেছে তাই দেখায় (selective labels).

উদাহরণ: churn, ফেরত, ভুয়া অর্ডার

Weak label

নিয়ম, শব্দতালিকা বা পুরোনো model দিয়ে দ্রুত অনেক label (weak supervision). প্রতিটি নিয়মকে বলে labeling function. বেশি ভুল, তবে ভুলগুলো আন্দাজ করা যায়।

উদাহরণ: “ফেরত চাই” থাকলে negative

যে উৎসই হোক, পথটা মোটামুটি একই। নিচের ছবিতে পুরো পর্বের মানচিত্র: কোন অংশে কোন ধাপ।

কাঁচা ডেটা থেকে version-করা label-সহ dataset

Where labels come from and the checks they pass before training

Label pipeline: sources, agreement, noise check, documentation, versioning কাঁচা ডেটা: রিভিউ, অর্ডার, log মানুষannotation Logআচরণ থেকে নিয়মweak label মিল: guideline, kappa, adjudicationঅংশ ২–৪ Noise: confident learning + মানুষঅংশ ৫–৭ নথি: datasheet, PII ঢাকাঅংশ ৮ Version: সারি-hash, DVC → trainingঅংশ ৯
ছবি ৫.১ — label-এর তিন উৎস একই চার ধাপের পথ পার হয়; প্রতিটি ধাপ এই পর্বের একটি অংশ।

Log-এর label-এ লুকোনো ফাঁদ: “ফেরত দিয়েছেন” label শুধু তাঁদের জন্য আছে, যাঁরা ফেরতের ঝামেলা নিতে রাজি ছিলেন; অসন্তুষ্ট কিন্তু চুপ থাকা ক্রেতা “সন্তুষ্ট” হিসেবে গোনা হন। একে বলা যায় নীরব label noise, যা নিয়মিত একদিকে ঝোঁকে। অংশ ৫-এ দেখবেন, এমন একমুখী noise এলোমেলো noise-এর চেয়ে বেশি ক্ষতি করে।

২ · Annotation guideline — label-দাতার উত্তর-নির্দেশিকা

“রিভিউটি positive না negative?” প্রশ্নটি সহজ শোনায়, যতক্ষণ না এমন রিভিউ আসে: “দাম হিসেবে খারাপ না”, “বাহ! মাত্র তিন সপ্তাহে ডেলিভারি 🙄”, বা “অর্ডার কবে আসবে?” তখন দুজন মানুষ দুই রকম label দেন, আর দুজনেই নিজের যুক্তিতে ঠিক। Annotation guideline এই ধূসর জায়গার জন্য লিখিত নিয়ম।

নিয়মউদাহরণLabel
পণ্য বা সেবা নিয়ে স্পষ্ট সন্তুষ্টি“মধুর স্বাদ চমৎকার”pos
নেতিবাচক শব্দ, কিন্তু অর্থ সন্তুষ্টি“দাম হিসেবে খারাপ না”pos
প্রশংসার শব্দে ব্যঙ্গ (🙄, 👏, “বাহ!”)“বাহ! মাত্র তিন সপ্তাহে ডেলিভারি 🙄”neg
শুধু প্রশ্ন বা তথ্য, মত নেই“অর্ডার কবে আসবে?”neu
ভালো আর খারাপ দুটোই, কোনোটি প্রধান নয়“ঠিকঠাক, তবে আরও সস্তা হলে ভালো হতো”neu
নিশ্চিত হতে না পারলেযেকোনো“অনিশ্চিত” চিহ্ন দিন, অনুমান নয়

ভালো নির্দেশিকা একবারে লেখা যায় না; এটি কয়েক দফায় বড় হয়। বাস্তবে কাজের ধারা মোটামুটি এরকম:

  1. খসড়া: শ্রেণির সংজ্ঞা, প্রতিটির ২–৩টি পরিষ্কার উদাহরণ আর কয়েকটি সীমান্তের উদাহরণ।
  2. Pilot: ৫০–১০০টি সারি অন্তত দুজন আলাদাভাবে label দেন, একে অপরের উত্তর না দেখে।
  3. মিল মাপা: kappa হিসাব (অংশ ৩–৪), আর মতভেদের সারিগুলো একসঙ্গে পড়া। যে ধরনের সারিতে বারবার মতভেদ, সেটি নির্দেশিকার ফাঁক।
  4. সংশোধন আর আবার pilot: নিয়ম যোগ বা পরিষ্কার করে নতুন সারিতে আবার মাপা।
  5. পূর্ণ কাজ: একটি অংশ (ধরুন ১০–২০%) সবসময় দুজনে করেন; আগে থেকে নিশ্চিত উত্তরের একটি gold set মাঝে মাঝে গোপনে মিশিয়ে প্রত্যেকের মান দেখা হয়।
  6. নিষ্পত্তি: মতভেদের সারি একজন অভিজ্ঞ মানুষ চূড়ান্ত করেন (adjudication), কারণসহ লিখে রাখেন।
মূল ধারণা

নির্দেশিকার version-ও ডেটার অংশ। v1 নির্দেশিকায় দেওয়া label আর v2-এ দেওয়া label মেশালে একই রিভিউ দুই নিয়মে দুই রকম হতে পারে। তাই প্রতিটি label-এর পাশে কে দিয়েছেন, কবে, কোন নির্দেশিকা-version, তা রাখা ভালো অভ্যাস।

৩ · দুজনের মিল — Cohen-এর kappa হাতে

রিনা আর সুমন বাজারঘরের ২০টি কাল্পনিক রিভিউ আলাদাভাবে label দিলেন। ১৫টিতে দুজনের উত্তর এক। তাহলে মিল ৭৫%? সংখ্যাটি সত্য, কিন্তু অর্ধেক গল্প। দুজন চোখ বন্ধ করে এলোমেলো label দিলেও কিছু সারিতে কাকতালীয়ভাবে মিলে যেত। Inter-annotator agreement-এর ভালো মাপ এই কাকতালীয় অংশ বাদ দেয়।

Jacob Cohen ১৯৬০ সালে এর জন্য প্রস্তাব করেন Cohen's kappa (κ):

κ = (po − pe) / (1 − pe)

এখানে po হলো observed agreement, অর্থাৎ যত ভাগ সারিতে দুজন মিলেছেন। pe হলো chance agreement: দুজন যদি নিজের নিজের অভ্যাসমতো (রিনা ৩৫% pos, সুমন ৪০% pos, ইত্যাদি) কিন্তু একে অপরের থেকে স্বাধীনভাবে label দিতেন, তাহলে প্রত্যাশিত মিল। লব “কাকতালীয়ের ওপরে বাড়তি মিল”, হর “কাকতালীয়ের ওপরে সর্বোচ্চ সম্ভব বাড়তি মিল”। κ = ১ মানে পুরো মিল, ০ মানে কাকতালীয়ের সমান, ঋণাত্মক মানে কাকতালীয়ের চেয়েও খারাপ।

নিচের stepper-এ দুজনের ৩×৩ contingency table থেকে ঘর ধরে ধরে κ বের করা হলো।

সারি = রিনা, কলাম = সুমনN = 20

            
হিসাবের অবস্থা
po
pe
κ
  1. ছক। প্রতিটি রিভিউ ঠিক একটি ঘরে যায়: সারি বলে রিনার label, কলাম সুমনের। যেমন রিনা pos আর সুমন neg, এমন রিভিউ ১টি (“দাম হিসেবে খারাপ না”)। সব ঘরের যোগ ২০.
  2. কর্ণ মানে মিল। কর্ণের ঘর ৬, ৫, ৪; মোট ১৫টি রিভিউয়ে দুজন একমত। তাই po = ১৫/২০ = ০.৭৫.
  3. সারি আর কলামের যোগ। রিনা দিয়েছেন pos ৭, neg ৭, neu ৬; সুমন pos ৮, neg ৭, neu ৫. এগুলোই দুজনের “অভ্যাস”, কাকতালীয় মিল হিসাবের কাঁচামাল।
  4. pos-এ কাকতালীয় মিল। স্বাধীন হলে দুজন একসঙ্গে pos বলার সম্ভাবনা গুণফল: ০.৩৫ × ০.৪০ = ০.১৪.
  5. বাকি দুই শ্রেণি। neg: ০.৩৫ × ০.৩৫ = ০.১২২৫; neu: ০.৩০ × ০.২৫ = ০.০৭৫. যোগ করে pe = ০.৩৩৭৫. অর্থাৎ অন্ধভাবেও প্রায় এক-তৃতীয়াংশ মিল আশা করা যেত।
  6. κ. (০.৭৫ − ০.৩৩৭৫) / (১ − ০.৩৩৭৫) = ০.৪১২৫ / ০.৬৬২৫ ≈ ০.৬২. কাঁচা মিল ৭৫% শুনতে যত ভালো, কাকতালীয় অংশ বাদ দিলে ছবিটি “মাঝারি থেকে ভালো”-র কাছাকাছি।

Kappa-র ফাঁদ — বেশি মিল, কম kappa

ধরুন ১০০টি রিভিউয়ের ৮৫টিতে দুজনেই pos, ৫টিতে দুজনেই neg, আর ১০টিতে মতভেদ (৫টি এদিকে, ৫টি ওদিকে)। po = ০.৯০. দুজনেই ৯০% pos দেন, তাই pe = ০.৯ × ০.৯ + ০.১ × ০.১ = ০.৮২, আর κ = (০.৯০ − ০.৮২) / (১ − ০.৮২) = ০.০৮ / ০.১৮ ≈ ০.৪৪. মিল ৯০%, তবু kappa মাঝারি। কারণ এক শ্রেণি খুব বেশি প্রচলিত (prevalence বেশি) হলে কাকতালীয় মিলই অনেক বড়। Feinstein আর Cicchetti ১৯৯০ সালে একে “high agreement but low kappa” প্যারাডক্স নামে আলোচনা করেন (kappa paradox). তাই kappa-র পাশে সবসময় কাঁচা মিল আর শ্রেণির ভাগও লিখুন।

কত kappa “ভালো”? Landis আর Koch ১৯৭৭ সালে একটি মোটা দাগের সারণি দেন (যেমন ০.৪১–০.৬০ moderate, ০.৬১–০.৮০ substantial). তাঁরা নিজেরাই একে যথেচ্ছ বলেছিলেন; কাজভেদে মানদণ্ড আলাদা। Sarcasm বা মনোভাবের মতো কঠিন কাজে ০.৬ ভালো হতে পারে, আর ওষুধের ডোজ লেখার মতো কাজে ০.৯-ও যথেষ্ট নয়। ক্রমযুক্ত label (যেমন ১–৫ স্টার) হলে weighted kappa ব্যবহার করুন, যাতে ৪ বনাম ৫-এর মতভেদ ১ বনাম ৫-এর চেয়ে কম শাস্তি পায়; scikit-learn-এ cohen_kappa_score(..., weights="quadratic").

৪ · তিন বা বেশি জন — Fleiss-এর kappa, আর কোডে মিলিয়ে দেখা

তৃতীয় জন তানিয়া যোগ দিলেন। Cohen-এর kappa জোড়ায় জোড়ায় হিসাব করা যায়, কিন্তু সবার মিলের একটি সংখ্যা চাইলে Joseph Fleiss-এর ১৯৭১ সালের পদ্ধতি, Fleiss' kappa. ভাবনা একই, “মিল বিয়োগ কাকতালীয় মিল”, তবে প্রতিটি রিভিউ ধরে:

  • প্রতিটি রিভিউয়ে মিল Pi: n জন label-দাতার সম্ভাব্য সব জোড়ার মধ্যে কত ভাগ জোড়া একমত। n = ৩ হলে জোড়া ৩টি; তিনজন একমত হলে Pi = ১, ২–১ ভাগ হলে Pi = ১/৩, তিনজন তিন রকম হলে ০. সূত্রে Pi = (∑j nij2 − n) / (n(n − 1)), যেখানে nij হলো রিভিউ i-কে শ্রেণি j দিয়েছেন এমন মানুষের সংখ্যা।
  • গড় মিল P̄ = সব Pi-এর গড়।
  • কাকতালীয় মিল P̄e = ∑j pj2, যেখানে pj হলো সব label-এর মধ্যে শ্রেণি j-এর ভাগ। এখানে সবার label একসঙ্গে ধরা হয়, প্রত্যেকের আলাদা অভ্যাস নয়; এই দিক থেকে এটি Cohen-এর চেয়ে Scott-এর ১৯৫৫ সালের π-এর কাছাকাছি।
  • κ = (P̄ − P̄e) / (1 − P̄e)

আমাদের ২০টি রিভিউয়ের ১৩টিতে তিনজন একমত, ৭টিতে ২–১ ভাগ। তাই P̄ = (১৩ × ১ + ৭ × ১/৩) / ২০ ≈ ০.৭৬৬৭. মোট ৬০টি label-এর মধ্যে pos ২২, neg ১৯, neu ১৯; P̄e = (২২² + ১৯² + ১৯²) / ৬০² = ১২০৬ / ৩৬০০ ≈ ০.৩৩৫. κ ≈ (০.৭৬৬৭ − ০.৩৩৫) / ০.৬৬৫ ≈ ০.৬৫. এবার কোডে, হাতের হিসাব আর library দুটোই।

ডেটা পাতার ভেতরে labels.csv নামে রাখা: কাল্পনিক ২০টি রিভিউ, তিনজনের label. এই পাতায় প্রথমবার scikit-learn আর statsmodels লোড হতে কয়েক সেকেন্ড (scikit-learn প্রায় ১০ সেকেন্ড) লাগতে পারে।

import numpy as np
import pandas as pd
from sklearn.metrics import cohen_kappa_score
from statsmodels.stats.inter_rater import aggregate_raters, fleiss_kappa

df = pd.read_csv("labels.csv")                  # ২০টি কাল্পনিক রিভিউ, তিনজন label-দাতা
cats = ["pos", "neg", "neu"]

# Cohen: রিনা বনাম সুমন, হাতে
tab = pd.crosstab(df["rina"], df["sumon"]).reindex(index=cats, columns=cats, fill_value=0)
print(tab, "\n")
N = tab.to_numpy().sum()
p_o = np.trace(tab.to_numpy()) / N                                  # কর্ণের যোগ / N
p_e = (tab.sum(axis=1) / N * tab.sum(axis=0) / N).sum()             # কাকতালীয় মিল
print(f"p_o={p_o:.4f}  p_e={p_e:.4f}  kappa(হাতে)={(p_o - p_e) / (1 - p_e):.4f}")
print(f"kappa(sklearn)={cohen_kappa_score(df['rina'], df['sumon']):.4f}")

# Fleiss: তিনজন একসঙ্গে
counts = pd.DataFrame({c: (df[["rina", "sumon", "tania"]] == c).sum(axis=1) for c in cats})
n = 3
P_i = ((counts**2).sum(axis=1) - n) / (n * (n - 1))                 # প্রতিটি রিভিউয়ে মিল
p_j = counts.sum() / (len(df) * n)                                  # প্রতিটি শ্রেণির ভাগ
P_bar, Pe_bar = P_i.mean(), (p_j**2).sum()
print(f"\nP_bar={P_bar:.4f}  Pe_bar={Pe_bar:.4f}  Fleiss(হাতে)={(P_bar - Pe_bar) / (1 - Pe_bar):.4f}")
table, _ = aggregate_raters(df[["rina", "sumon", "tania"]].to_numpy())
print(f"Fleiss(statsmodels)={fleiss_kappa(table, method='fleiss'):.4f}")
for a, b in [("rina", "tania"), ("sumon", "tania")]:
    print(f"Cohen {a}-{b}: {cohen_kappa_score(df[a], df[b]):.4f}")

আসল আউটপুট:

sumon  pos  neg  neu
rina
pos      6    1    0
neg      1    5    1
neu      1    1    4

p_o=0.7500  p_e=0.3375  kappa(হাতে)=0.6226
kappa(sklearn)=0.6226

P_bar=0.7667  Pe_bar=0.3350  Fleiss(হাতে)=0.6491
Fleiss(statsmodels)=0.6491
Cohen rina-tania: 0.7015
Cohen sumon-tania: 0.6283

হাতের হিসাব আর library হুবহু মিলেছে: Cohen ০.৬২২৬, Fleiss ০.৬৪৯১. aggregate_raters প্রতিটি সারির জন্য “কোন শ্রেণি কতজন দিয়েছেন” ছক বানায়, যা fleiss_kappa-র ইনপুট; আমাদের counts-ও ঠিক সেই ছক। জোড়ায় জোড়ায় দেখলে রিনা–তানিয়া ০.৭০, সুমন–তানিয়া ০.৬৩: সুমনের মতভেদ বেশি। তবে ২০টি সারিতে এই পার্থক্য খুব অনিশ্চিত; একজন মানুষকে দোষ দেওয়ার আগে মতভেদের সারিগুলো পড়ুন। এখানে সেগুলো ব্যঙ্গ (r08), “খারাপ না” (r07) আর মিশ্র মত (r15): তিনটিই নির্দেশিকার সীমান্তের নিয়ম।

আরও গভীরে: Fleiss-এর kappa ধরে নেয় প্রতিটি সারিতে সমান সংখ্যক label আছে। বাস্তবে কেউ কোনো সারি বাদ দেন, বা label ক্রমযুক্ত; তখন Klaus Krippendorff-এর Krippendorff's alpha বেশি নমনীয় (missing label, nominal থেকে interval মাপ)। আর মানুষের ভুলের হার আলাদা ধরে “আসল” label অনুমান করার ক্লাসিক পদ্ধতি Dawid আর Skene-এর ১৯৭৯ সালের EM model, যা crowdsourcing-এর label মেলাতে আজও ব্যবহৃত হয়; EM নিজে পর্ব ৩৯-এ আসবে।

৫ · Label noise — ধরন আর model-এর ওপর প্রভাব

মিল কখনো পুরো হয় না, তাই training ডেটায় কিছু label ভুল থাকবেই। একে বলে label noise. গবেষণায় তিনটি ধরন আলাদা করা হয়:

  • এলোমেলো (symmetric noise): প্রতিটি label একই সম্ভাবনায় অন্য শ্রেণিতে যায়, শ্রেণি বা সারির ওপর নির্ভর না করে। ক্লান্ত annotator-এর ভুল বোতাম।
  • শ্রেণিনির্ভর (class-conditional noise): উল্টে যাওয়ার সম্ভাবনা আসল শ্রেণির ওপর নির্ভর করে। যেমন churn হওয়া ক্রেতার ৩০% “থেকে গেছেন” হিসেবে লেখা হলো, কিন্তু উল্টোটা প্রায় হয় না। এই সম্ভাবনাগুলোর ছককে বলে noise transition matrix T, যেখানে Tij = P(দেওয়া label j ∣ আসল label i).
  • সারিনির্ভর (instance-dependent noise): কঠিন বা দ্ব্যর্থক সারিতে ভুল বেশি, যেমন ব্যঙ্গ। বাস্তবে সবচেয়ে সাধারণ, আর সবচেয়ে কঠিন।

একটি উল্টানো label decision boundary কতটা সরায়? নিচের stepper-এ এক-মাত্রার ছোট উদাহরণ: x = ডেলিভারিতে কত দিন লেগেছে, label ১ = ক্রেতা অভিযোগ করেছেন। আটটি বিন্দু, model হলো scikit-learn-এর default logistic regression (C = 1). সংখ্যাগুলো একটি ছোট script চালিয়ে বের করা।

x = ডেলিভারির দিন, * = উল্টানো labellogistic, C = 1

            
Fit-এর ফল
ঢাল w
p(অভিযোগ ∣ x = 5)
p(অভিযোগ ∣ x = 9)
আসল label-এ ঠিক
  1. পরিষ্কার ডেটা। ১–৪ দিনে অভিযোগ নেই, ৬–৯ দিনে আছে। Boundary ঠিক মাঝখানে, x = ৫. ঢাল ১.১৪, তাই ৯ দিনে অভিযোগের সম্ভাবনা ০.৯৮৯. আটটি বিন্দুই ঠিক।
  2. সীমান্তের কাছের একটি ভুল। x = ৬-এর label ভুল করে ০. Boundary সরে ৬.৩৩৫-এ, ঢাল প্রায় একই (১.০৭)। এখন ৬ দিনের ডেলিভারিকে model “অভিযোগ হবে না” বলে, যা আসল নিয়মে ভুল: ৮-এর ৭টি ঠিক।
  3. দূরের একটি ভুল। শুধু x = ৯-এর label ০ (বাকি সব পরিষ্কার)। Boundary ৬.৫০১-এ, আর ঢাল নেমে ০.৪৮: model সবখানে কম নিশ্চিত, ৯ দিনেও মাত্র ০.৭৬৮. দূরের ভুল বিন্দু log loss-এ বড় শাস্তি আনে, তাই পুরো বক্ররেখা চ্যাপ্টা হয়।
  4. দুই দিকে একটি করে। x = ৯ → ০ আর x = ১ → ১. টানাটানি সমান, তাই boundary আবার ৫.০০০, আর সিদ্ধান্ত আটটিতেই ঠিক। কিন্তু ঢাল ০.১৩; ৯ দিনে সম্ভাবনা মাত্র ০.৬২৪. এলোমেলো noise সিদ্ধান্ত কম বদলায়, সম্ভাবনা নষ্ট করে বেশি (calibration-এর কথা পর্ব ২৬-এ)।
  5. একমুখী noise. x = ৬ আর ৭, দুটিই ০ (অভিযোগ হয়েছিল, কিন্তু লেখা হয়নি)। Boundary ৭.৫৪৭-এ সরে গেল; ৬ আর ৭ দিনের ডেলিভারি এখন “নিরাপদ”। আটটির ৬টি ঠিক। একদিকে ঝোঁকা noise boundary-কে সেদিকেই টেনে নেয়।

এবার বড় পরিসরে। নিচের ব্লকে ১৫০০ সারির একটি কাল্পনিক churn ডেটা (scikit-learn-এর make_classification), ১০০০ train আর ৫০০ test. শুধু train-এর label উল্টানো হয়, ০ থেকে ৪০% পর্যন্ত; test-এর label পরিষ্কার থাকে, যাতে আসল ক্ষতি মাপা যায়। দুটি model: logistic regression আর সীমাহীন গভীরতার decision tree; প্রতিটি মান তিনটি seed-এর গড়।

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier

X, y = make_classification(n_samples=1500, n_features=10, n_informative=5,
                           random_state=42)            # কাল্পনিক churn ডেটা, দুই শ্রেণি
Xtr, ytr, Xte, yte = X[:1000], y[:1000], X[1000:], y[1000:]   # test label পরিষ্কার

def flip(y, rate, kind, rng):
    y = y.copy()
    hit = rng.random(len(y)) < rate
    if kind == "class-dependent":
        hit &= (y == 1)                    # শুধু churn হওয়া ক্রেতা "থেকে গেছেন" লেখা হয়
    y[hit] = 1 - y[hit]
    return y

models = {"logistic": lambda: LogisticRegression(max_iter=1000),
          "tree": lambda: DecisionTreeClassifier(random_state=0)}
rates = [0.0, 0.1, 0.2, 0.3, 0.4]
fig, axes = plt.subplots(1, 2, figsize=(9, 3.4), sharey=True)
for ax, kind in zip(axes, ["random", "class-dependent"]):
    for name, make in models.items():
        acc = [np.mean([make().fit(Xtr, flip(ytr, r, kind, np.random.default_rng(s)))
                        .score(Xte, yte) for s in range(3)]) for r in rates]
        print(f"{kind:15s} {name:8s}", " ".join(f"{a:.3f}" for a in acc))
        ax.plot(rates, acc, marker="o", label=name)
    ax.set(title=f"{kind} label noise", xlabel="flip rate")
axes[0].set_ylabel("accuracy on clean test")
axes[0].legend()
share = (flip(ytr, 0.4, "class-dependent", np.random.default_rng(0)) != ytr).mean()
print("class-dependent, rate 0.4 → মোট label-এর উল্টানো ভাগ:", round(share, 3))
plt.show()

আসল আউটপুট (সঙ্গে দুই প্যানেলের chart; প্রতিটি সারিতে flip rate ০, ০.১, ০.২, ০.৩, ০.৪-এ test accuracy):

random          logistic 0.848 0.850 0.844 0.823 0.765
random          tree     0.854 0.787 0.693 0.641 0.551
class-dependent logistic 0.848 0.856 0.832 0.792 0.732
class-dependent tree     0.854 0.827 0.791 0.743 0.711
class-dependent, rate 0.4 → মোট label-এর উল্টানো ভাগ: 0.205

পরিষ্কার label-এ দুই model কাছাকাছি (logistic ০.৮৪৮, tree ০.৮৫৪)। এলোমেলো noise: logistic ২০% পর্যন্ত প্রায় অটল (০.৮৪৪), ৪০%-এ নামে ০.৭৬৫-এ; tree প্রতিটি ধাপে পড়ে, ৪০%-এ ০.৫৫১, মুদ্রা ছোড়ার কাছাকাছি। দুই শ্রেণিতে উল্টানোর হার ৫০%-এর নিচে থাকলে প্রতিটি এলাকায় সংখ্যাগরিষ্ঠ label একই থাকে, তাই কম ক্ষমতার model-এর boundary প্রায় একই জায়গায় থাকে; সীমাহীন tree প্রতিটি ভুল label-এর জন্য আলাদা পাতা বানিয়ে তা মুখস্থ করে। শ্রেণিনির্ভর noise: rate ০.৪ মানে churn-শ্রেণির ৪০% উল্টানো, কিন্তু মোট label-এর মাত্র ২০.৫%. তবু logistic নামে ০.৭৩২-এ, যেখানে প্রায় একই মোট পরিমাণের এলোমেলো noise-এ (rate ০.২) ছিল ০.৮৪৪. একমুখী noise boundary-কে একদিকে টেনে নেয়, stepper-এর শেষ ধাপের মতো। Tree-র ক্ষতি দুই ধরনে প্রায় সমান (০.৭১১ বনাম ০.৬৯৩), কারণ সে যেকোনো ভুলই মুখস্থ করে। এগুলো একটি কাল্পনিক ডেটায়, তিনটি seed-এ, একটি run-এর ফল; ধারাটি শিক্ষণীয়, সংখ্যাগুলো সাধারণ নিয়ম নয়।

৬ · ভুল label খোঁজা — confident learning

ক্যাপস্টোনে (AI-এর ভিত্তি, পর্ব ৭৩) একটি সরল নিয়ম চালানো হয়েছিল: যে model সারিটি দেখেনি, সে নিজের label-কে যত কম সম্ভাবনা দেয়, সারিটি তত সন্দেহজনক। এখানে সেই ভাবনার আরও পদ্ধতিগত রূপ, Curtis Northcutt, Lu Jiang আর Isaac Chuang-এর confident learning (২০১৯ সালে preprint, ২০২১ সালে journal-এ)। তিনটি উপাদান:

  1. Out-of-fold সম্ভাবনা: cross-validation-এ প্রতিটি সারির সম্ভাবনা আসে এমন model থেকে, যে ওই সারি দিয়ে train হয়নি। নইলে model ভুল label মুখস্থ করে তাকেই সমর্থন করত।
  2. শ্রেণিভিত্তিক threshold tj: যেসব সারির দেওয়া label j, তাদের pj-এর গড়। কিছু শ্রেণি স্বভাবতই কঠিন (neu); সেখানে model-এর আস্থা গড়ে কম, তাই এক সীমা সবার জন্য অন্যায্য।
  3. নিয়ম: সারির যেসব শ্রেণিতে pk ≥ tk, তাদের মধ্যে সবচেয়ে বড়টি “আস্থার শ্রেণি”। সেটি দেওয়া label থেকে আলাদা হলে সারিটি সন্দেহভাজন। কোনো শ্রেণি সীমা না ছুঁলে সারিটি গোনা হয় না।
Out-of-fold সম্ভাবনা (pos, neg, neu)কাল্পনিক সংখ্যা

            
তালিকা
দেখা সারি
সন্দেহভাজন
  1. Threshold আগে। পুরো ডেটায় যেসব সারির label pos, model তাদের গড়ে ০.৭০ pos-সম্ভাবনা দেয়; neg-এ ০.৬৫, neu-তে ০.৫৫. এগুলোই তিন শ্রেণির “আস্থার সীমা”। (সংখ্যাগুলো বোঝানোর জন্য বানানো।)
  2. সারি A. pos-এর সম্ভাবনা ০.৮৫, সীমা ০.৭০ পার। আস্থার শ্রেণি pos, দেওয়া label-ও pos: কোনো সন্দেহ নেই।
  3. সারি B. দেওয়া label neg, কিন্তু model pos-এ ০.৭৮ দেয়, যা pos-এর সীমার ওপরে; নিজের label neg-এ মাত্র ০.১২. আস্থার শ্রেণি আর দেওয়া label আলাদা: সন্দেহভাজন।
  4. সারি C. নিজের label-এ আস্থা কম (০.২৫), তবু তালিকায় নেই, কারণ অন্য কোনো শ্রেণিও নিজের সীমা ছোঁয়নি। Model দ্বিধায় থাকলে সে label-এর বিরুদ্ধে সাক্ষী নয়। শুধু “কম আস্থা” নিয়ম এটিকেও ধরত।
  5. সারি D. neu-তে ০.৬০, যা pos-এর সীমা ০.৭০-এর নিচে হলেও neu-এর সীমা ০.৫৫-এর ওপরে। শ্রেণিভিত্তিক সীমার জন্যই এটি ঠিক বলে গণ্য। সব শ্রেণিতে একই সীমা ০.৭০ হলে neu-এর ০.৬০ তা ছুঁত না, আর সারিটি “আস্থার শ্রেণি নেই” হয়ে বিচারের বাইরে চলে যেত।
  6. সারি E. দেওয়া label pos, কিন্তু neg-এ ০.৬৮ ≥ ০.৬৫. আস্থার শ্রেণি neg: সন্দেহভাজন।
  7. ফল। সন্দেহভাজন B আর E. নিজের label-এ আস্থা যত কম, তত আগে মানুষ দেখবেন: B (০.১২), তারপর E (০.৩০)। Model শুধু তালিকা দেয়; label বদলাবেন মানুষ, কারণ লিখে।

এবার একটি ডেটায় ইচ্ছে করে ৪৫টি ভুল label বসানো হলো, যাতে আমরা “আসল উত্তর” জানি আর detector-এর precision (তালিকার কত ভাগ সত্যিই ভুল) আর recall (সব ভুলের কত ভাগ ধরা পড়ল) মাপতে পারি। তুলনার জন্য সরল নিয়ম “model-এর argmax ≠ দেওয়া label”.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_predict

X, y_true = make_classification(n_samples=600, n_features=8, n_informative=5, n_classes=3,
                                class_sep=2.5, random_state=7)   # কাল্পনিক: pos / neg / neu
rng = np.random.default_rng(42)
bad = rng.choice(len(y_true), size=45, replace=False)            # ৪৫টি label ইচ্ছে করে ভুল
y = y_true.copy()
y[bad] = (y[bad] + rng.integers(1, 3, size=45)) % 3              # অন্য একটি শ্রেণিতে

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
P = cross_val_predict(LogisticRegression(max_iter=1000), X, y, cv=cv, method="predict_proba")
t = np.array([P[y == j, j].mean() for j in range(3)])           # শ্রেণিভিত্তিক threshold
print("threshold t_j:", t.round(3))

ok = P >= t                                                      # কোন শ্রেণিতে আস্থা যথেষ্ট
assigned = np.where(ok.any(axis=1), np.where(ok, P, -1).argmax(axis=1), -1)
cl_flag = (assigned != -1) & (assigned != y)                     # confident-learning নিয়ম
argmax_flag = P.argmax(axis=1) != y                              # সরল নিয়ম: model-এর মত আলাদা

truth = np.zeros(len(y), bool); truth[bad] = True
for name, f in [("confident learning", cl_flag), ("argmax != label", argmax_flag)]:
    tp = (f & truth).sum()
    print(f"{name:18s} flagged={f.sum():3d}  ধরা={tp:2d}/45  "
          f"precision={tp / f.sum():.2f}  recall={tp / 45:.2f}")

order = np.argsort(P[np.arange(len(y)), y])[:45]                 # সবচেয়ে কম self-confidence
print("lowest 45 self-confidence: precision =", round(truth[order].mean(), 2))

আসল আউটপুট:

threshold t_j: [0.656 0.796 0.716]
confident learning flagged= 46  ধরা=32/45  precision=0.70  recall=0.71
argmax != label    flagged= 99  ধরা=43/45  precision=0.43  recall=0.96
lowest 45 self-confidence: precision = 0.78

Threshold তিন শ্রেণিতে আলাদা (০.৬৫৬, ০.৭৯৬, ০.৭১৬): এক সীমা সবার জন্য ঠিক হতো না। Confident learning ৪৬টি সারি চিহ্নিত করেছে, যা আসল ভুলের সংখ্যা ৪৫-এর খুব কাছে; এই গোনাটিই ডেটায় কতটা noise আছে তার একটি অনুমান। ৪৬-এর ৩২টি সত্যিই ভুল: precision ০.৭০, recall ০.৭১. argmax নিয়ম ৯৯টি চিহ্নিত করে ৪৩টি ধরেছে (recall ০.৯৬), কিন্তু পর্যালোচকদের ৫৬টি ঠিক সারিও পড়তে হবে (precision ০.৪৩)। আর শুধু নিজের label-এ সবচেয়ে কম আস্থার ৪৫টি নিলে ৩৫টি ভুল (০.৭৮)। কোনটি ভালো, তা নির্ভর করে পর্যালোচনার বাজেটে: অল্প সারি পড়ার সময় থাকলে ক্রম ধরে পড়ুন; প্রায় সব ভুল ধরতে চাইলে বড় তালিকা আর বেশি মানুষের সময়। Confident learning যে ১৩টি ভুল ধরেনি, সেগুলোর বেশিরভাগ এমন সারি যেখানে model নিজেই দ্বিধায়; detector কখনো model-এর চেয়ে ভালো দেখে না।

ডেস্কটপে সত্যিকারের run: একই ডেটা আর একই out-of-fold সম্ভাবনা নিয়ে open-source cleanlab library (লেখার সময়ে version 2.9.0; Python 3.13.5, scikit-learn 1.9.1). এর find_label_issues-এ filter_by="confident_learning" আমাদের হাতে লেখা নিয়মটিই; বাকি দুটি বিকল্প আরও কয়েকটি সারি যোগ করে, অনুমিত noise-এর হার অনুযায়ী।

from cleanlab.filter import find_label_issues
for method in ["confident_learning", "prune_by_noise_rate", "both"]:
    f = find_label_issues(labels=y, pred_probs=P, filter_by=method, n_jobs=1)
    tp = (f & truth).sum()
    print(f"{method:20s} flagged={f.sum():3d} caught={tp:2d}/45 precision={tp / f.sum():.2f} recall={tp / 45:.2f}")
~/bazarghar-ml$ python cl_real.py
confident_learning   flagged= 46 caught=32/45 precision=0.70 recall=0.71
prune_by_noise_rate  flagged= 63 caught=40/45 precision=0.63 recall=0.89
both                 flagged= 59 caught=39/45 precision=0.66 recall=0.87

প্রথম সারিটি পাতার ব্লকের সঙ্গে হুবহু মিলেছে (৪৬টি চিহ্নিত, ৩২টি ধরা), তাই হাতের নিয়মটি library-র ভেতরের নিয়মই। Windows-এ n_jobs=1 না দিলে library একাধিক process চালু করতে চায়, আর if __name__ == "__main__": ছাড়া script-এ তা আটকে যায়; আমাদের প্রথম চেষ্টায় ঠিক এটাই ঘটেছিল।

৭ · Noise নিয়ে কাজ — সারানো, সওয়া, আর noisy test set-এর হিসাব

সন্দেহের তালিকা পাওয়ার পর কয়েকটি পথ, প্রায়ই একসঙ্গে:

উপায়কখন কাজে লাগেদাম বা ঝুঁকি
মানুষ দিয়ে আবার label (human-in-the-loop)তালিকা ছোট, বাজেট আছেসময়; model-এর মত দেখে মানুষ প্রভাবিত হতে পারেন
সন্দেহভাজন সারি বাদডেটা অনেক, ভুল স্পষ্টকঠিন কিন্তু সঠিক সারিও যায়; ছোট শ্রেণি আরও ছোট হয়
একাধিক annotator, majority voteএলোমেলো ভুল বেশিখরচ দ্বিগুণ-তিনগুণ; সবাই একই ভুল করলে লাভ নেই
Soft label (যেমন ২ জন pos, ১ জন neu → ০.৬৭, ০.৩৩)মতভেদ আসল দ্ব্যর্থতাসব model বা metric এটি নেয় না
Noise-সহনশীল পদ্ধতিLabel সারানো অসম্ভবধারণা (যেমন T জানা) ভুল হলে উল্টো ফল

Test set সবচেয়ে বেশি যত্ন চায়। Train-এর ভুল model-কে একটু দুর্বল করে; test-এর ভুল আপনার মাপকাঠিকেই বাঁকা করে। আর test-এর label সারাতে হবে model-এর অনুমান না দেখিয়ে, স্বাধীন মানুষ দিয়ে; নইলে আপনি test-কে model-এর মতো করে বদলাচ্ছেন, যা একরকম leakage (AI-এর ভিত্তি কোর্সের পর্ব ৫৬)।

একটি numerical: noisy test set-এ মাপা accuracy

দুই শ্রেণি। Model-এর আসল accuracy a, আর test-এর প্রতিটি label স্বাধীনভাবে ε সম্ভাবনায় উল্টানো। Model-এর উত্তর তখনই “ঠিক” গোনা হয়, যখন (model ঠিক আর label ঠিক) অথবা (model ভুল আর label-ও উল্টানো)। Model-এর ভুল আর label-এর ভুল পরস্পর স্বাধীন ধরলে:

মাপা accuracy = a(1 − ε) + (1 − a)ε

a = ০.৯৫, ε = ০.০৫ হলে মাপা = ০.৯৫ × ০.৯৫ + ০.০৫ × ০.০৫ = ০.৯০২৫ + ০.০০২৫ = ০.৯০৫. অর্থাৎ ৫% ভুল label-এ একটি ৯৫% model ৯০.৫% দেখায়। আর একটি নিখুঁত model (a = ১)-ও দেখাবে ১ − ε = ০.৯৫, এর বেশি নয়: noisy test-এ accuracy-র একটি ছাদ আছে। স্বাধীনতার ধারণা ভাঙলে ছবিটি আরও খারাপ: যে model annotator-দের একই ভুল শিখেছে, সে উল্টানো label-এর সঙ্গে “মিলে” পুরস্কার পায়, আর ভালো model দণ্ড পায়। দুটি model-এর পার্থক্য তাই অল্প হলে, আগে test-এর label-এর মান দেখুন, তারপর পর্ব ২২-এর পরিসংখ্যান।

৮ · Label-এর নথি আর গোপনীয়তা

AI-এর ভিত্তি কোর্সের পর্ব ৭২-এ datasheet আর model card লিখেছেন, আর গোপনীয়তার মূল ধারণা (data minimization, k-anonymity, differential privacy) দেখেছেন। Timnit Gebru ও সহকর্মীদের Datasheets for Datasets (২০১৮ সালে preprint) প্রশ্নের তালিকায় label-এর জন্য আলাদা অংশ আছে। বাজারঘরের রিভিউ-ডেটার datasheet-এ label অংশটি এমন হতে পারে:

## Labels (v3, নির্দেশিকা v2.1)
- শ্রেণি: pos / neg / neu; "অনিশ্চিত" চিহ্নিত সারি বাদ (মোট 3.1%)
- কে দিয়েছেন: 3 জন প্রশিক্ষিত কর্মী, বাংলা ও Banglish পড়তে পারেন
- মিল: 20% সারি তিনজনে; Fleiss kappa 0.65, কাঁচা মিল 77%
- নিষ্পত্তি: মতভেদ এক জ্যেষ্ঠ সদস্য চূড়ান্ত করেছেন, কারণ লেখা আছে
- জানা দুর্বলতা: ব্যঙ্গ আর মিশ্র মতে মতভেদ বেশি
- Noise যাচাই: confident learning, 46টি সারি পর্যালোচিত, 31টি বদল
- গোপনীয়তা: ফোন নম্বর ও ঠিকানা label দেওয়ার আগে ঢাকা

(উদাহরণ; সংখ্যাগুলো বোঝানোর জন্য বানানো।)

যে ডেটায় label দেওয়া হয়, তার গোপনীয়তা

Label দিতে মানুষকে কাঁচা ডেটা পড়তে হয়, তাই annotation নিজেই একটি ডেটা-ভাগাভাগি। কয়েকটি নিয়ম:

  • আগে ঢাকুন: রিভিউয়ে ক্রেতা প্রায়ই ফোন নম্বর, ঠিকানা, অর্ডার নম্বর লেখেন। এগুলো PII; label-দাতাকে পাঠানোর আগে regex আর হাতে-দেখা দিয়ে [PHONE], [ADDRESS] বসান (pseudonymization). মনোভাব বুঝতে নম্বর লাগে না।
  • যতটুকু লাগে ততটুকু (data minimization): label-দাতা শুধু লেখাটি দেখবেন, ক্রেতার নাম বা কেনার ইতিহাস নয়।
  • উদ্দেশ্যের সীমা: যে কাজের কথা বলে ডেটা নেওয়া, label-ও সেই কাজেই। রিভিউয়ের মনোভাবের label দিয়ে পরে ক্রেতার ব্যক্তিগত প্রোফাইল বানানো আলাদা উদ্দেশ্য।
  • বাইরের সেবা আর AI tool: ক্রেতার আসল লেখা কোনো সাধারণ AI chat-এ বা অচেনা crowdsourcing সেবায় পাঠানোর আগে চুক্তি, ডেটা কোথায় থাকে আর কতদিন, তা দেখুন; প্রয়োজনে পাঠাবেন না।
  • Label-দাতার কল্যাণ: ঘৃণা-ভাষণ বা সহিংস ছবির label দেওয়া মানুষের জন্য ক্ষতিকর হতে পারে, সংবাদমাধ্যমে এমন অভিযোগ বারবার এসেছে। কাজের সময় সীমিত রাখা, বেছে না নেওয়ার সুযোগ আর মানসিক সহায়তা নৈতিক দায়।

৯ · Data versioning — কোন সারি, কবে, কেন বদলাল

AI-এর ভিত্তি কোর্সের পর্ব ১২-এ পুরো ফাইলের SHA-256 ছাপ আর DVC-এর ধারণা দেখেছেন। ফাইলের ছাপ বলে “কিছু বদলেছে”, কিন্তু কী বদলেছে বলে না। আর শুধু সারির ক্রম বদলালে বা CSV-র উদ্ধৃতিচিহ্নের ধরন বদলালেও ফাইলের ছাপ বদলায়, যদিও ডেটা একই। Label-এর কাজে দরকার আরও সূক্ষ্ম data versioning:

  • সারি-ধরে hash: প্রতিটি সারির একটি ছাপ, একটি স্থির id-র সঙ্গে। দুই version মেলালে ঠিক কোন id বদলেছে, তা বেরিয়ে আসে।
  • Dataset-এর ছাপ: id ধরে সাজানো সারি-ছাপগুলোর ছাপ; সারির ক্রমে নির্ভর করে না।
  • কাঁচা ডেটা আর label আলাদা: রিভিউ একবার আসে, label বারবার বদলায়। আলাদা টেবিলে রাখলে প্রতিটি label-বদলের ইতিহাস (কে, কবে, কোন নির্দেশিকা) রাখা সহজ; এটিই data lineage-এর ভিত্তি।
import hashlib
import pandas as pd

def row_hashes(df):
    # প্রতিটি সারির ছাপ: id বাদে সব কলাম, নির্দিষ্ট ক্রমে, একটি স্থির বিভাজক দিয়ে
    cols = sorted(c for c in df.columns if c != "id")
    text = df[cols].astype(str).agg("\x1f".join, axis=1)
    return pd.Series([hashlib.sha256(t.encode("utf-8")).hexdigest()[:12] for t in text],
                     index=df["id"])

def dataset_hash(h):
    # পুরো version-এর ছাপ: id ধরে সাজানো সারি-ছাপের ছাপ (সারির ক্রম বদলালেও একই)
    joined = "\n".join(f"{i}:{v}" for i, v in h.sort_index().items())
    return hashlib.sha256(joined.encode()).hexdigest()[:16]

def file_hash(df):
    return hashlib.sha256(df.to_csv(index=False).encode("utf-8")).hexdigest()[:16]

v1 = pd.read_csv("labels.csv")[["id", "text", "rina"]].rename(columns={"rina": "label"})
v2 = v1.sample(frac=1, random_state=1).reset_index(drop=True)   # শুধু ক্রম বদল
v3 = v1.copy()
v3.loc[v3["id"] == "r08", "label"] = "pos"                     # একটি label বদল

h1, h2, h3 = row_hashes(v1), row_hashes(v2), row_hashes(v3)
print("file hash   v1:", file_hash(v1), " v2:", file_hash(v2))
print("dataset     v1:", dataset_hash(h1), " v2:", dataset_hash(h2))
print("dataset     v3:", dataset_hash(h3))
changed = h1.index[h1 != h3.reindex(h1.index)]
print("বদলানো সারি:", list(changed))
print(v1.set_index("id").loc[changed, "label"].tolist(), "→",
      v3.set_index("id").loc[changed, "label"].tolist())

আসল আউটপুট:

file hash   v1: ae5bd571de7993d2  v2: b80dd57bf67254b9
dataset     v1: 1e2c0ce5004dab58  v2: 1e2c0ce5004dab58
dataset     v3: 1b137a1aa456c107
বদলানো সারি: ['r08']
['neg'] → ['pos']

v1 আর v2-তে ডেটা হুবহু এক, শুধু সারির ক্রম আলাদা; তবু ফাইলের ছাপ আলাদা, কারণ byte-এর ক্রম বদলেছে। Dataset-এর ছাপ এক, কারণ সারি-ছাপগুলো id ধরে সাজিয়ে তারপর ছাপ নেওয়া হয়েছে। v3-তে একটি label বদলাতেই dataset-এর ছাপ বদলেছে, আর সারি-ধরে তুলনা ঠিক r08 দেখিয়েছে: neg থেকে pos. দুটি খুঁটিনাটি: কলামের মাঝে \x1f (একটি অদৃশ্য “unit separator” অক্ষর) বসানো, যাতে "ab" + "c" আর "a" + "bc" একই লেখা না হয়ে যায়; আর মানগুলো astype(str)-এ লেখায় বদলায়, তাই সংখ্যা আর ফাঁকা ঘরের লেখার ধরন (যেমন 1 বনাম 1.0) আগে থেকে নির্দিষ্ট রাখতে হয়, নইলে একই ডেটা দুই রকম ছাপ দেবে। এখানে পড়ার সুবিধায় ছাপ ছেঁটে ১২ বা ১৬ অক্ষর দেখানো হয়েছে; রেকর্ডে পুরো ৬৪ অক্ষর রাখুন।

ডেস্কটপে সত্যিকারের run: DVC দিয়ে label ফাইলের version

DVC (লেখার সময়ে version 3.67.1, pip দিয়ে একটি venv-এ, Python 3.13.5) কোনো account ছাড়াই চলে। সাধারণত এটি Git-এর পাশে বসে; এখানে --no-scm দিয়ে Git ছাড়া চালানো হলো, যাতে শুধু DVC-র নিজের কাজ দেখা যায়। ফাইল একই labels.csv; মাঝে r08-এর রিনার label হাতে neg থেকে pos করা হয়েছে। (Windows-এ চালানো, তাই পথে \; লম্বা অংশ “…” দিয়ে ছাঁটা।)

~/bazarghar-ml$ dvc init --no-scm
Initialized DVC repository.
…
~/bazarghar-ml$ dvc add data/labels.csv
~/bazarghar-ml$ cat data/labels.csv.dvc
outs:
- md5: a05b323327f3af427897555392e0c9b8
  size: 1518
  hash: md5
  path: labels.csv
~/bazarghar-ml$ dvc status
Data and pipelines are up to date.

# r08-এর label হাতে বদলানোর পর
~/bazarghar-ml$ dvc status
data\labels.csv.dvc:
	changed outs:
		modified:           data\labels.csv
~/bazarghar-ml$ dvc add data/labels.csv
~/bazarghar-ml$ cat data/labels.csv.dvc
outs:
- md5: eb3aae35198e4db8d2964e4555b2a1ec
  size: 1518
  hash: md5
  path: labels.csv

# .dvc ফাইলে পুরোনো md5 ফিরিয়ে দিয়ে
~/bazarghar-ml$ dvc checkout
M       data\labels.csv
~/bazarghar-ml$ grep r08 data/labels.csv
r08,"বাহ! মাত্র তিন সপ্তাহে ডেলিভারি 🙄",neg,pos,neg

তিনটি জিনিস লক্ষ করুন। এক, .dvc ফাইলটি ছোট একটি “রসিদ”: ফাইলের ছাপ আর আকার; ডেটা নিজে থাকে .dvc/cache-এ, ছাপের নামে (content-addressable storage, Git-এর object store-এর মতো; দেখুন Software Engineering & Git কোর্সের Git internals). দুই, আকার একই (১৫১৮ byte) কারণ “neg” আর “pos” দুটিই তিন অক্ষর, তবু ছাপ বদলেছে; আকার দেখে বদল ধরা যায় না। তিন, পুরোনো রসিদ ফিরিয়ে dvc checkout করলেই আগের label ফিরে এলো। বাস্তবে রসিদটি Git-এ commit হয়, তাই “কোন commit-এ কোন label” জানা যায়। DVC এখানে MD5 ব্যবহার করে; বদল ধরতে তা যথেষ্ট, তবে ইচ্ছাকৃত জালিয়াতি ঠেকানোর নিরাপত্তায় নয়। DVC ফাইল-স্তরে কাজ করে; কোন সারি বদলেছে, তা জানতে আগের ব্লকের মতো সারি-hash বা টেবিল-তুলনা লাগে।

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

  • Kappa আর শতকরা মিলের পার্থক্য? Kappa কাকতালীয় মিল (pe) বাদ দেয়: κ = (po − pe)/(1 − pe).
  • ৯০% মিল, তবু kappa ০.৪৪ কেন হতে পারে? এক শ্রেণি খুব প্রচলিত হলে pe বড় হয় (kappa paradox).
  • Cohen বনাম Fleiss? Cohen দুজন নির্দিষ্ট annotator; Fleiss তিন বা বেশি, সব label একসঙ্গে ধরে কাকতালীয় মিল।
  • Confident learning-এ out-of-fold কেন? Train-এ দেখা সারিতে model ভুল label মুখস্থ করে সমর্থন করে।
  • ৫% এলোমেলো test-noise-এ নিখুঁত model-এর মাপা accuracy? ০.৯৫, অর্থাৎ ১ − ε।

১০ · AI-এর যুগে — AI দিয়ে label, আর AI-এর লেখা label-কোড যাচাই

বড় ভাষা-model (LLM) দিয়ে label দেওয়া এখন সাধারণ চর্চা। কয়েকটি গবেষণায় কিছু লেখা-শ্রেণিকরণ কাজে LLM-এর label crowdsourcing কর্মীদের সমান বা ভালো এসেছে (যেমন ২০২৩ সালে Gilardi ও সহকর্মীদের একটি PNAS প্রবন্ধ), আবার অন্য কাজে, বিশেষ করে কম-সম্পদের ভাষা, ব্যঙ্গ আর সাংস্কৃতিক প্রসঙ্গে, ফল দুর্বল। বাংলা আর Banglish রিভিউয়ে কেমন হবে, তা আগে থেকে বলা যায় না; মেপে দেখতে হয়। LLM-কে তাই আরেকজন annotator ভাবুন: gold set-এ মানুষের সঙ্গে তার kappa মাপুন, মতভেদের সারি পড়ুন, আর যে model-এর label দিয়ে train করছেন, সেই একই model দিয়ে মূল্যায়ন করবেন না।

Label আর ডেটার মান নিয়ে কোড লিখতে AI assistant সাধারণত যে ভুলগুলো করে: confident learning-এ out-of-fold-এর বদলে train-এ fit করা model-এর সম্ভাবনা নেয়; kappa-র বদলে শুধু শতকরা মিল দেখায়; Fleiss-এর জন্য raw label সরাসরি fleiss_kappa-তে দেয় (ইনপুট হতে হবে সারি × শ্রেণির গোনা-ছক); test set-এর label model-এর মত দেখে “সারায়”; কোনো library-র এমন parameter বানিয়ে লেখে যা নেই; আর সারির ক্রমে নির্ভরশীল hash-কে dataset version বলে।

একটি ভালো prompt-এ ডেটার আকার, label-এর ধরন, আর কী যাচাই করবেন তা বলে দিন:

আমার কাছে 2,000টি বাংলা ও Banglish পণ্য-রিভিউ আছে, তিনজন মানুষ প্রতিটিতে pos/neg/neu label দিয়েছেন (pandas DataFrame, কলাম: id, text, a1, a2, a3). scikit-learn 1.8 আর statsmodels দিয়ে: ১. জোড়ায় জোড়ায় Cohen kappa আর তিনজনের Fleiss kappa হিসাব করো; fleiss_kappa-র ইনপুটের আকার কী হবে ব্যাখ্যা করো। ২. majority vote দিয়ে একটি label বানাও, তিনজন তিন রকম হলে সারিটি আলাদা তালিকায় রাখো। ৩. StratifiedKFold দিয়ে out-of-fold predict_proba নিয়ে confident learning-এর নিয়মে সন্দেহভাজন সারি খোঁজো। শর্ত: test set-এর সারি এই ধাপে ব্যবহার করবে না; যে function বা parameter ব্যবহার করবে, তার official docs-এর নাম দাও; কোনো আউটপুট নিজে বানিয়ে লিখবে না।

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

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

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

প্র ০১শতকরা মিল সহজে বোঝা যায়; তবু kappa কেন? আর kappa নিজে কোথায় বিভ্রান্ত করে?

শতকরা মিলের সমস্যা হলো এটি কাকতালীয় মিলকে দক্ষতা হিসেবে গোনে। দুই শ্রেণির কাজে দুজন মুদ্রা ছুড়ে label দিলেও গড়ে ৫০% মিলবে; আর যদি ৯৫% সারি একই শ্রেণির হয়, “সবকিছু pos” বলা দুজন মানুষ কোনো চিন্তা ছাড়াই প্রায় ৯০% মিল দেখাবেন। William Scott ১৯৫৫ সালে জরিপ-গবেষণায় এই সমস্যার জন্য π প্রস্তাব করেন, আর Jacob Cohen ১৯৬০ সালে মনোবিজ্ঞানের মাপে κ, যেখানে প্রত্যেক মানুষের নিজের শ্রেণি-অভ্যাস থেকে কাকতালীয় মিল হিসাব হয়। ১৯৬৮ সালে Cohen-ই weighted kappa দেন, যাতে ক্রমযুক্ত label-এ কাছাকাছি মতভেদ কম শাস্তি পায়।

কিন্তু kappa-র নিজের সমস্যা আছে। প্রথমত, এটি prevalence-এর ওপর নির্ভর করে: একই মানের দুজন মানুষ, একটি ডেটায় শ্রেণি ভারসাম্যপূর্ণ, অন্যটিতে খুব একপেশে, সেখানে kappa অনেক আলাদা আসবে। Feinstein আর Cicchetti ১৯৯০ সালে চিকিৎসা-গবেষণায় এই “high agreement but low kappa” প্যারাডক্স বিস্তারিত দেখান। দ্বিতীয়ত, দুজনের শ্রেণি-অভ্যাস খুব আলাদা হলে (একজন বেশি neg দেন) kappa অদ্ভুতভাবে বাড়তে পারে, যাকে bias paradox বলা হয়। তৃতীয়ত, Landis আর Koch-এর ১৯৭৭ সালের “moderate”, “substantial” শব্দগুলো তাঁরা নিজেরাই যথেচ্ছ বলেছিলেন, অথচ বহু প্রবন্ধে এগুলো আইনের মতো ব্যবহার হয়।

এ কারণে কিছু গবেষক বিকল্প প্রস্তাব করেছেন, যেমন Gwet-এর AC1, বা Krippendorff-এর alpha, যা missing label আর নানা মাপের ধরন সামলায়। কিন্তু কোনো একটি সংখ্যা পুরো গল্প বলে না। ভালো রিপোর্টে থাকে কাঁচা মিল, kappa, শ্রেণির ভাগ, প্রতিটি শ্রেণিতে আলাদা মিল, আর মতভেদের কয়েকটি উদাহরণ। আর ছোট নমুনায় (যেমন ২০টি সারি) kappa-র অনিশ্চয়তা বড়; bootstrap দিয়ে একটি পরিসর দেওয়া যায় (পর্ব ২১)।

মূল উপলব্ধি: kappa কাকতালীয় মিলের একটি সৎ সংশোধন, কিন্তু শ্রেণির ভাগ না দেখে এটি পড়লে আপনিই বিভ্রান্ত হবেন; সংখ্যার পাশে ছক আর উদাহরণ রাখুন।

প্র ০২বিখ্যাত benchmark-এর test set-এও ভুল label পাওয়া গেছে। এতে model-এর তুলনা নিয়ে কী শিক্ষা?

২০২১ সালে Curtis Northcutt, Anish Athalye আর Jonas Mueller ছবি, লেখা আর শব্দের দশটি জনপ্রিয় dataset-এর test set-এ confident learning চালিয়ে সন্দেহভাজন সারি বের করেন, তারপর crowdsourcing-এ মানুষ দিয়ে যাচাই করান। তাঁদের হিসাবে এসব test set-এ গড়ে কয়েক শতাংশ label ভুল, কোনো কোনো dataset-এ আরও বেশি। সংখ্যাটি নিজেও একটি অনুমান, কারণ যাচাইকারী মানুষও ভুল করেন; কিন্তু দিকটি পরিষ্কার: “স্বর্ণমান” test set-ও স্বর্ণ নয়।

এর প্রভাব শুধু accuracy কিছুটা কমে যাওয়া নয়। এই পর্বের সূত্রে দেখেছেন, noise এলোমেলো আর model-এর ভুল থেকে স্বাধীন হলে মাপা accuracy একটি অনুমেয় পরিমাণে কমে, আর দুই model-এর ক্রম মোটামুটি টিকে থাকে। কিন্তু বাস্তবে ভুল label প্রায়ই দ্ব্যর্থক বা কঠিন সারিতে জমে, আর বড় model সেই একই annotator-অভ্যাস শিখে ফেলে। তখন যে model “ভুল label-এর সঙ্গে মেলে”, সে পুরস্কার পায়। উল্লিখিত গবেষণায় লেখকেরা দেখান যে ভুল label সারানো test set-এ কিছু ক্ষেত্রে ছোট, সরল model বড় model-কে ছাড়িয়ে যায়, যদিও মূল test-এ উল্টো ছিল। অর্থাৎ leaderboard-এর এক-দুই শতাংশের পার্থক্য অনেক সময় label noise-এর ভেতরেই হারিয়ে যায়।

বাজারঘরের জন্য শিক্ষা তিনটি। এক, test set-এর label সবচেয়ে বেশি যত্ন পাবে: দুজন স্বাধীন মানুষ, নিষ্পত্তি, আর model না দেখিয়ে সংশোধন। দুই, দুটি model-এর পার্থক্য ছোট হলে আগে দেখুন test-এর label কতটা নির্ভরযোগ্য, তারপর পর্ব ২২-এর paired test. তিন, ভুল label সারানোর সিদ্ধান্ত লিখে রাখুন, যাতে পুরোনো আর নতুন test-এর ফল কেউ একসঙ্গে মিশিয়ে না ফেলেন; test-এর নিজস্ব version থাকবে।

মূল উপলব্ধি: মাপকাঠি যত ভালো, তুলনা তত অর্থবহ; test-এর label-এর মান না জেনে ছোট পার্থক্যকে অগ্রগতি বলবেন না।

প্র ০৩বড় neural network এলোমেলো label-ও মুখস্থ করতে পারে। তাহলে noise-এর বিরুদ্ধে model-এর ক্ষমতা নিয়ে আমরা কী বুঝব?

২০১৭ সালে Chiyuan Zhang ও সহকর্মীরা “Understanding deep learning requires rethinking generalization” প্রবন্ধে একটি সরল কিন্তু চমকে দেওয়া পরীক্ষা করেন: ছবির dataset-এর সব label পুরো এলোমেলো করে দেওয়ার পরও বড় convolutional network train-এর সব সারি প্রায় নিখুঁতভাবে মিলিয়ে ফেলে, যদিও test-এ ফল কাকতালীয়ের সমান। অর্থাৎ যথেষ্ট বড় model-এর কাছে ভুল label “অসম্ভব” কিছু নয়; সে যেকোনো label মুখস্থ করতে পারে।

এই পর্বের noise-পরীক্ষায় ছোট আকারে একই জিনিস দেখেছেন: সীমাহীন গভীরতার decision tree প্রতিটি ভুল label-এর জন্য আলাদা পাতা বানায়, তাই এলোমেলো noise বাড়ার সঙ্গে test accuracy দ্রুত পড়ে। Logistic regression-এর ক্ষমতা কম, সে একটি সমতল ছাড়া কিছু আঁকতে পারে না, তাই এলোমেলো উল্টানো label অনেকটাই গড়ে কেটে যায়। Angluin আর Laird ১৯৮৮ সালে এলোমেলো classification noise-এর তাত্ত্বিক মডেল দেন, আর পরে Natarajan ও সহকর্মীরা ২০১৩ সালে দেখান, শ্রেণিনির্ভর noise-এর হার জানা থাকলে loss সংশোধন করে নিরপেক্ষ শেখা সম্ভব। মূল কথা: সহনশীলতা আসে model-এর সীমিত ক্ষমতা, regularization বা loss-এর গঠন থেকে, “বুদ্ধি” থেকে নয়।

Deep learning-এ পরে আরও পর্যবেক্ষণ এসেছে: network প্রথমে সহজ, সাধারণ ধাঁচ শেখে, ভুল label মুখস্থ করে পরে; তাই early stopping কিছুটা রক্ষা দেয়। Label smoothing, noise-সহনশীল loss, দুটি network-এর একে অপরের জন্য পরিষ্কার সারি বাছাই, এসব কৌশল পরের Deep Learning কোর্সে আসবে। কিন্তু কোনোটিই একমুখী, পদ্ধতিগত noise থেকে পুরো রক্ষা দেয় না, কারণ সেখানে ভুলটাই “নিয়ম” হয়ে দাঁড়ায়।

মূল উপলব্ধি: যে model সব মুখস্থ করতে পারে, সে ভুলও মুখস্থ করবে; train-এ নিখুঁত মিল label-এর মানের প্রমাণ নয়, আর ক্ষমতা নিয়ন্ত্রণ ও ভালো label দুটোই লাগে।

প্র ০৪হাতে label দেওয়ার বদলে নিয়ম আর log দিয়ে হাজার হাজার weak label বানানো কি ভালো বুদ্ধি?

অনেক ক্ষেত্রে হ্যাঁ, শর্তসহ। Alexander Ratner ও সহকর্মীরা ২০১৬ সালে “data programming” ধারণা দেন, আর পরের বছর Snorkel নামের একটি open-source ব্যবস্থা: বিশেষজ্ঞ সরাসরি label না দিয়ে অনেকগুলো labeling function লেখেন (“লেখায় ‘ফেরত’ আর ‘টাকা’ থাকলে neg”, “৫ স্টার দিলে pos”, “পুরোনো model ০.৯-এর বেশি বললে তাই”)। প্রতিটি নিয়ম কিছু সারিতে চুপ থাকে, কিছুতে ভুল বলে, আর নিয়মগুলো একে অপরের সঙ্গে দ্বিমত করে। একটি label model তাদের মিল-অমিল থেকে প্রতিটি নিয়মের নির্ভরযোগ্যতা অনুমান করে একটি সম্ভাব্য label দেয়; এর পূর্বসূরি ভাবনা Dawid আর Skene-এর ১৯৭৯ সালের annotator-ভুলের model.

সুবিধা স্পষ্ট: নির্দেশিকা বদলালে কয়েক হাজার label নতুন করে দিতে হয় না, শুধু নিয়ম বদলান। বাজারঘরের মতো দোকানে, যেখানে বাংলা, Banglish আর ইংরেজি মেশানো লেখা অনেক, একজন মানুষের হাতে হাজারো সারির চেয়ে কয়েকটি নিয়ম দ্রুত।

কিন্তু ঝুঁকিগুলোও বাস্তব। এক, model নিয়মটিকেই শেখে: label যদি “ফেরত শব্দ আছে কি না” থেকে আসে, model সেই শব্দই খুঁজবে, আর ব্যঙ্গ বা নিয়মের বাইরের লেখায় ব্যর্থ হবে; এটি একরকম target leakage. দুই, log-এর label-এ selective labels: ফেরত দেননি মানে সন্তুষ্ট নন, হয়তো ঝামেলা এড়িয়েছেন। তিন, noise একমুখী হয়, যা এই পর্বে দেখেছেন সবচেয়ে ক্ষতিকর। তাই নিয়মে বানানো label দিয়ে train করলেও মূল্যায়নের জন্য মানুষের হাতে দেওয়া, স্বাধীন একটি ছোট test set রাখতেই হবে। কম label-এ শেখার অন্য পথ, যেমন semi-supervised আর active learning, পর্ব ৫৪-এ।

মূল উপলব্ধি: weak label গতি দেয়, কিন্তু নিয়মের অন্ধ জায়গাও সঙ্গে আনে; মানুষের হাতে দেওয়া একটি পরিষ্কার মূল্যায়ন-সেট ছাড়া সেই অন্ধ জায়গা দেখা যায় না।

প্র ০৫Annotator-দের মতভেদ কি সবসময় “noise”, যা মুছে একটি সঠিক label বানাতে হবে?

না, সবসময় নয়। “দাম হিসেবে খারাপ না” রিভিউটি একজন pos পড়েন, আরেকজন neutral; দুজনেই নির্দেশিকা মেনে চলেছেন, আর বাস্তব ক্রেতারাও একে নানাভাবে পড়েন। এখানে মতভেদ ভুল নয়, লেখাটির আসল দ্ব্যর্থতা। Majority vote এই তথ্য মুছে দেয়: ২–১ ভোটের সারি আর ৩–০ ভোটের সারি একই “নিশ্চিত” label পায়।

২০১৯ সালে Joshua Peterson ও সহকর্মীরা একটি পরিচিত ছোট ছবির dataset-এর test অংশের প্রতিটি ছবিতে অনেক মানুষের label সংগ্রহ করে (CIFAR-10H নামে প্রকাশিত) দেখান, মানুষের এই বণ্টন, অর্থাৎ soft label দিয়ে train করলে model অচেনা ধরনের ছবিতে কিছুটা বেশি টেকসই হয়। ভাষা-প্রক্রিয়াকরণে একদল গবেষক আরও এগিয়ে বলেন, ঘৃণা-ভাষণ বা অপমানের মতো বিষয়ে label-দাতার পরিচয় আর অভিজ্ঞতা তাঁর বিচারকে প্রভাবিত করে; সেখানে “একটি সত্য label” খোঁজা মানে সংখ্যাগরিষ্ঠের দৃষ্টিকোণ চাপিয়ে দেওয়া। এই অবস্থানকে কখনো perspectivism বলা হয়; এটি নিয়ে বিতর্ক চলছে, সবাই একমত নন।

ব্যবহারিক মধ্যপথ: দুই রকম মতভেদ আলাদা করুন। নির্দেশিকার ফাঁক বা অমনোযোগ থেকে আসা মতভেদ সারাতে হবে (নিয়ম লিখে, আবার pilot করে)। আর আসল দ্ব্যর্থতা থেকে আসা মতভেদ রেখে দিন: soft label, “অনিশ্চিত” শ্রেণি, বা model-এর মূল্যায়নে ওই সারিগুলো আলাদা রিপোর্ট। Confident learning-এর তালিকাও তাই সরাসরি “ভুল” তালিকা নয়; কিছু সারি আসলে দ্ব্যর্থক, আর সেখানে সঠিক কাজ হলো সারিটিকে দ্ব্যর্থক বলে চিহ্নিত করা।

মূল উপলব্ধি: মতভেদ হয় ভুল, নয়তো তথ্য; আগে বুঝুন কোনটি, তারপর ঠিক করুন মুছবেন না রাখবেন।

অনুশীলন

হাতে হিসাব, আউটপুট অনুমান, আর একটি পরিকল্পনার সমালোচনা।

  1. হাতে kappa (numerical): ৫০টি অর্ডারে দুজন “অভিযোগ আছে / নেই” label দিলেন। দুজনেই “আছে”: ২০; দুজনেই “নেই”: ১৫; প্রথমজন “আছে”, দ্বিতীয়জন “নেই”: ৫; প্রথমজন “নেই”, দ্বিতীয়জন “আছে”: ১০. po, pe আর Cohen-এর κ বের করুন।

    po = (২০ + ১৫) / ৫০ = ০.৭০. প্রথমজনের “আছে” = ২০ + ৫ = ২৫, দ্বিতীয়জনের “আছে” = ২০ + ১০ = ৩০. pe = (২৫/৫০)(৩০/৫০) + (২৫/৫০)(২০/৫০) = ০.৩০ + ০.২০ = ০.৫০. κ = (০.৭০ − ০.৫০) / (১ − ০.৫০) = ০.২০ / ০.৫০ = ০.৪০. (cohen_kappa_score দিয়ে যাচাই করলে 0.4 আসে।) ৭০% মিল শুনতে ভালো, কিন্তু কাকতালীয় মিলই ৫০%; নির্দেশিকায় “অভিযোগ”-এর সংজ্ঞা পরিষ্কার করা দরকার।

  2. আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে।
    import numpy as np
    from sklearn.metrics import cohen_kappa_score as k
    A = np.array([1, 1, 0, 0, 1, 0])
    B = np.array([1, 0, 0, 0, 1, 1])
    print(round(float(np.mean(A == B)), 3), round(k(A, B), 3), round(k(A, 1 - A), 3))

    0.667 0.333 -1.0. ছয়টির চারটিতে মিল, তাই ০.৬৬৭. দুজনেরই তিনটি ১ আর তিনটি ০, তাই pe = ০.৫ × ০.৫ + ০.৫ × ০.৫ = ০.৫; κ = (০.৬৬৭ − ০.৫) / ০.৫ ≈ ০.৩৩৩. 1 - A প্রতিটি label উল্টে দেয়: মিল ০, pe আবার ০.৫, κ = (০ − ০.৫) / ০.৫ = −১: কাকতালীয়ের চেয়েও খারাপ, পদ্ধতিগত উল্টো মত। (scikit-learn 1.9.1-এ যাচাই করা।)

  3. পরিকল্পনার সমালোচনা (design or critique): একজন সহকর্মী প্রস্তাব দিলেন: “ডেলিভারি-অভিযোগের ১০,০০০ রিভিউ একজন intern এক সপ্তাহে label দেবেন। নির্দেশিকা লাগবে না, বিষয়টি সহজ। শেষে model দিয়ে সন্দেহভাজন বের করে ওগুলো model-এর মত অনুযায়ী বদলে দেব, test set-সহ। রিভিউয়ে ফোন নম্বর থাকলে থাকুক।” অন্তত পাঁচটি সমস্যা আর প্রতিটির প্রতিকার লিখুন।
    • নির্দেশিকা নেই: “দেরি হয়েছে, তবে ঠিক আছে” কোন শ্রেণি? লিখিত সংজ্ঞা আর সীমান্তের উদাহরণসহ নির্দেশিকা, আগে pilot.
    • একজন মানুষ: মিল মাপার উপায় নেই, ক্লান্তির ভুল ধরা পড়বে না। অন্তত ১০–২০% সারি দুজনে, kappa মাপা, আর একটি gold set.
    • model-এর মতে বদল: model নিজের ভুলকে “সত্য” বানাবে। সন্দেহভাজন সারি মানুষ পড়বেন, কারণ লিখে বদলাবেন।
    • test set-ও বদল: model-এর মত দেখে test সারানো leakage; test-এর সংশোধন স্বাধীন মানুষ দিয়ে, model না দেখিয়ে।
    • PII: ফোন নম্বর label-দাতার সামনে অপ্রয়োজনীয়; আগে ঢাকুন, শুধু লেখা দেখান।
    • Version নেই: label-বদলের ইতিহাস আর dataset-এর ছাপ না রাখলে পরে ফল পুনরুৎপাদন করা যাবে না।
  4. Noisy test (numerical): দুই শ্রেণির একটি test set-এর ১০% label এলোমেলোভাবে উল্টানো (ε = ০.১০), model-এর ভুলের সঙ্গে স্বাধীন। Model X-এর আসল accuracy ০.৯০, Model Y-এর ০.৯৪. দুটির মাপা accuracy কত হবে? মাপা পার্থক্য আসল পার্থক্যের চেয়ে বড় না ছোট?

    মাপা = a(১ − ε) + (১ − a)ε. X: ০.৯০ × ০.৯০ + ০.১০ × ০.১০ = ০.৮১ + ০.০১ = ০.৮২. Y: ০.৯৪ × ০.৯০ + ০.০৬ × ০.১০ = ০.৮৪৬ + ০.০০৬ = ০.৮৫২. আসল পার্থক্য ০.০৪, মাপা ০.০৩২. সাধারণভাবে মাপা পার্থক্য = আসল পার্থক্য × (১ − ২ε), তাই ছোট হয়ে আসে, ক্রম বদলায় না। তবে ছোট test set-এ এই সংকুচিত পার্থক্য এলোমেলো ওঠানামার নিচে চাপা পড়তে পারে; আর ভুলগুলো স্বাধীন না হলে ক্রমও উল্টাতে পারে।

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

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

ক্রমএকটি নতুন label-কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সংজ্ঞা আর উদাহরণসহ নির্দেশিকার খসড়া লেখা
  2. ছোট pilot: দুজন আলাদাভাবে একই সারিতে label দেন
  3. Kappa মেপে মতভেদের সারি পড়া, নির্দেশিকা সংশোধন
  4. পূর্ণ label-কাজ, একটি অংশ দুজনে আর মাঝে মাঝে gold set
  5. মতভেদের নিষ্পত্তি আর out-of-fold দিয়ে সন্দেহভাজন সারির পর্যালোচনা
  6. Datasheet লেখা আর dataset-এর নতুন version-এর ছাপ রেকর্ড

দুজন annotator ১০০টি সারিতে label দিলেন; po = ০.৮০ আর pe = ০.৬০. Cohen-এর kappa কত?

  • ০.৮০
  • ০.৫০
  • ০.২০
  • ০.৬০

দুজন annotator ৯২% সারিতে একমত, অথচ Cohen-এর kappa মাত্র ০.৩৫. সবচেয়ে সম্ভাব্য কারণ কী?

  • Kappa ভুল হিসাব হয়েছে, এটি অসম্ভব
  • Annotator দুজনের বেশি হওয়া উচিত ছিল
  • এক শ্রেণি খুব বেশি প্রচলিত, তাই কাকতালীয় মিল বড়
  • Label ক্রমহীন, তাই kappa কাজ করে না

ভুল label খুঁজতে model-এর সম্ভাবনা কেন out-of-fold (cross-validation-এ, সারিটি না দেখা model থেকে) নিতে হয়?

  • Train-এ দেখা সারির ভুল label model মুখস্থ করে সমর্থন করে
  • এতে হিসাব দ্রুত হয়
  • এতে সম্ভাবনা আপনা থেকে calibrated হয়
  • এতে test set-এর label-ও একসঙ্গে সারানো যায়

দুই শ্রেণির ডেটায় train-এর label-এর ৩০% এলোমেলোভাবে (শ্রেণি-নিরপেক্ষ) উল্টানো, test পরিষ্কার। কোন model-এর test accuracy সাধারণত সবচেয়ে বেশি পড়বে?

  • L2 regularization-সহ logistic regression
  • সবসময় majority শ্রেণি বলা baseline
  • max_depth = 3-এর decision tree
  • সীমাহীন গভীরতার decision tree

Confident learning-এ একটি সারির দেওয়া label neu; out-of-fold সম্ভাবনা (pos, neg, neu) = (০.৪০, ০.৩৫, ০.২৫); শ্রেণিভিত্তিক threshold (০.৭০, ০.৬৫, ০.৫৫)। সারিটির কী হবে?

  • সন্দেহভাজন, কারণ pos-এর সম্ভাবনা সবচেয়ে বেশি
  • কোনো শ্রেণি নিজের সীমা ছোঁয়নি, তাই সারিটি সন্দেহের তালিকায় যায় না
  • neu নিশ্চিতভাবে সঠিক বলে গণ্য
  • সারিটি ডেটা থেকে মুছে ফেলা হবে

একটি label-ফাইলের শুধু সারির ক্রম বদলানো হলো, কোনো মান বদলায়নি। কোন ছাপ বদলাবে না?

  • পুরো ফাইলের SHA-256
  • DVC-র .dvc ফাইলের md5
  • id ধরে সাজানো সারি-ছাপগুলোর ছাপ
  • উপরের কোনোটিই নয়, সব ছাপ বদলাবে

একটি LLM দিয়ে ৫,০০০ বাংলা রিভিউয়ের label দেওয়া হলো। এই label ব্যবহারের আগে সবচেয়ে জরুরি যাচাই কোনটি?

  • একই LLM-কে জিজ্ঞেস করা, label ঠিক আছে কি না
  • প্রতিটি label-এর সঙ্গে LLM-এর লেখা ব্যাখ্যা পড়ে সন্তুষ্ট হওয়া
  • এই label দিয়ে train করে training accuracy দেখা
  • মানুষের দেওয়া একটি gold set-এ LLM-এর সঙ্গে kappa আর মতভেদের সারি দেখা

শব্দকোষ

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

মনে রাখুন

  1. Label আসে মানুষ, log আর নিয়ম থেকে; প্রতিটির নিজস্ব ভুলের ধরন, আর নির্দেশিকার version-ও ডেটার অংশ।
  2. κ = (po − pe)/(1 − pe); দুজনে Cohen, তিন বা বেশি জনে Fleiss; kappa-র পাশে কাঁচা মিল আর শ্রেণির ভাগ লিখুন।
  3. এলোমেলো noise সরল model অনেকটা সয়, মুখস্থকারী model সয় না; একমুখী noise boundary সরায়; noisy test accuracy-র ছাদ ১ − ε.
  4. Confident learning: out-of-fold সম্ভাবনা, শ্রেণিভিত্তিক threshold, আস্থার শ্রেণি ≠ দেওয়া label হলে সন্দেহ; সিদ্ধান্ত মানুষের।
  5. Label-এর আগে PII ঢাকুন, datasheet লিখুন, আর সারি-hash ও DVC দিয়ে প্রতিটি version চিহ্নিত রাখুন।

পরের পর্বে: মডিউল ২ শুরু, পর্ব ০৬ “Vector space — span, basis, linear independence আর rank”: ডেটার প্রতিটি সারি যে vector, তার জগতের ভাষা।

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

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