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

ML-এ ন্যায্যতা — pre-, in- আর post-processing প্রতিকার

Fairness in ML — Pre-, In- and Post-processing Mitigation

Taught in Bangla, this lesson measures group fairness gaps with bootstrap intervals on fictional buy-now-pay-later data with a built-in label bias. It implements reweighing, a fairness-penalised logistic regression and randomised equalized-odds thresholds in the browser, compares them with a real desktop fairlearn run, and discusses the calibration conflict, levelling down and the limits of metrics.

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

  • দলভিত্তিক ফাঁকের bootstrap CI বের করা, আর ছোট ও intersectional দলের মাপ কতটা নড়বড়ে তা বোঝা
  • ক্ষতির ধরন (allocative বনাম quality-of-service) থেকে fairness metric বাছা আর লিখে রাখা
  • Kamiran–Calders reweighing হাতে ও কোডে, আর তার প্রভাব মাপা
  • Fairness penalty-সহ logistic regression নিজে লেখা, λ বাড়িয়ে accuracy–fairness curve আঁকা, আর fairlearn-এর ExponentiatedGradient চালানো
  • Equalized odds-এর দলভিত্তিক ও randomised সীমা, তিন পথের তুলনা, calibration-এর সঙ্গে দ্বন্দ্ব আর metric-এর সীমা

AI-এর ভিত্তি কোর্সের পর্ব ৭২-এ দলভিত্তিক মাপ (selection rate, demographic parity, equal opportunity, TPR আর FPR), proxy, base rate-এর অসম্ভাব্যতা আর re-weighting ও দলভিত্তিক সীমার প্রথম পরিচয় এসেছে; confusion matrix-এর মাপগুলো একই কোর্সের পর্ব ৬০-এ। এই পর্ব সেখান থেকে এগোয়: ফাঁক কতটা নিশ্চিত, কোন ফাঁক জরুরি, আর তিনটি প্রতিকার নিজে লিখে একই ডেটায় মাপলে কী দাম দিতে হয়। চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python: NumPy, SciPy, pandas, scikit-learn, matplotlib) আর একই namespace ভাগ করে; প্রথমবার scikit-learn নামতে প্রায় ১০ সেকেন্ড লাগে। পরের ব্লক আগে চালালে আগের ব্লকগুলো নিজে থেকে আগে চলে।

১ · ডেটা — “পরে টাকা দিন” আর শুধু অডিটের জন্য রাখা দল

বাজারঘর কিছু গ্রাহককে “পরে টাকা দিন” সুবিধা দেয়। একটি logistic regression model চারটি feature দেখে: আগের অর্ডার (orders), ফেরতের হার (ret), অ্যাকাউন্টের বয়স মাসে (tenure) আর দূরের এলাকায় ঠিকানা কি না (far). সম্ভাবনা ০.৫ বা বেশি হলে অনুমোদন।

দুটি কাল্পনিক দলের কলামও আছে: জেলা-দল (A, আর নদীচর নামের একটি কাল্পনিক নদী-তীরের জেলা) আর বয়স-দল (৫০+ কি না)। এগুলো protected attribute-এর ভূমিকায়, আর রাখা হয়েছে শুধু অডিটের জন্য: model input হিসেবে দেখে না, সীমিত প্রবেশাধিকারে শুধু মাপার কাজে লাগে। সব সংখ্যা seed দেওয়া কোডে বানানো; কোনো বাস্তব জেলা বা বয়সের মানুষের পরিসংখ্যান নয়।

ডেটায় ইচ্ছা করে AI-এর ভিত্তি কোর্সের পর্ব ৭২-এর মতো একটি label bias রাখা: নদীচরে courier টাকা জমা দেন দেরিতে, তাই সময়মতো দেওয়া গ্রাহকদের ৩০% খাতায় “দেরি”। আসল ফল (repaid) শুধু simulation জানে; বাস্তবে আপনার হাতে থাকে খাতার label. আর far কলামটি নদীচরের proxy: নদীচরের ৮৫% আর A-র ৫% দূরের এলাকায়। Proxy মুছলে কেন সমস্যা যায় না, তা AI-এর ভিত্তি কোর্সের পর্ব ৭২-এ দেখানো আছে; এখানে আবার দেখাব না।

def make_data(n=6000, seed=60):
    r = np.random.default_rng(seed)
    B = r.random(n) < 0.30                         # নদীচর (কাল্পনিক জেলা দল), শুধু অডিটে
    old = r.random(n) < 0.10                       # বয়স 50+ (কাল্পনিক), শুধু অডিটে
    far = np.where(B, r.random(n) < 0.85, r.random(n) < 0.05)   # দূরের area: proxy
    orders = r.poisson(np.where(B, 4.0, 5.0))      # আগের অর্ডার
    ret = r.beta(2, 12, n)                         # ফেরতের হার
    tenure = r.integers(1, 37, n)                  # অ্যাকাউন্টের বয়স, মাস
    p_true = 1 / (1 + np.exp(-(-1.0 + 0.4 * orders - 6 * ret + 0.03 * tenure)))
    repaid = (r.random(n) < p_true).astype(int)    # আসল সত্য, শুধু simulation জানে
    late = B & (repaid == 1) & (r.random(n) < 0.30)  # courier-এর দেরি = "গ্রাহক দেরি"
    return pd.DataFrame({"B": B.astype(int), "old": old.astype(int), "far": far.astype(int),
                         "orders": orders, "ret": ret.round(3), "tenure": tenure,
                         "repaid": repaid, "label": np.where(late, 0, repaid)})

২ · অনিশ্চয়তাসহ মাপা — bootstrap CI, ছোট দল আর intersectional দল

ক্রিকেটের স্কোরকার্ড ভাবুন। এক ব্যাটার ২০০ ইনিংসে গড় ৪০, আরেকজন ৩ ইনিংসে গড় ৮০. দ্বিতীয়জন কি দ্বিগুণ ভালো? তিন ইনিংসে বলা যায় না। দলভিত্তিক fairness-এর ফাঁকও তেমন: ছোট দলের হার অল্প কয়েকজনের ওপর দাঁড়িয়ে, তাই একটি সংখ্যার পাশে সবসময় তার নড়াচড়ার পরিসর লাগে।

একটি ফাঁক (যেমন TPRA − TPRনদীচর) test set থেকে মাপা একটি অনুমান. তার অনিশ্চয়তা মাপতে এই কোর্সের পর্ব ২১-এর bootstrap: test-এর সারি ফেরত রেখে আবার টেনে ১,০০০ বার ফাঁক মাপা, তারপর ২.৫ আর ৯৭.৫ percentile নিলে percentile interval. এটি ৯৫% confidence interval-এর একটি আনুমানিক রূপ; এখানে শুধু test সারির এলোমেলোতা ধরা হয়, model-এর training-এর এলোমেলোতা নয়।

দ্বিতীয় প্রশ্ন ছোট দলের। দুটি বৈশিষ্ট্য একসঙ্গে ধরলে (নদীচর এবং ৫০+) যে দল পাই, তাকে বলে intersectional দল। এর ধারণা এসেছে Kimberlé Crenshaw-এর ১৯৮৯ সালের আইনি প্রবন্ধ থেকে: আলাদা আলাদা দলে বৈষম্য না দেখা গেলেও দুটির মিলনে দেখা যেতে পারে। সমস্যা হলো, মিলনের দল ছোট, তাই তার মাপ কাঁপে। নিচের ব্লক baseline model শেখায়, তারপর দুটি তুলনার জন্য selection rate, TPR আর FPR-এর ফাঁক ও তার CI ছাপে। ফাঁক মাপা হয় “বাকি সবাই − দলটি” হিসেবে, খাতার label ধরে, কারণ বাস্তবে সেটিই হাতে থাকে।

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from bnpl import make_data

df = make_data()
tr, te = df.iloc[:3000], df.iloc[3000:]           # data এলোমেলো, তাই প্রথম অর্ধেক train
F = ["orders", "ret", "tenure", "far"]             # model দল দেখে না; B আর old শুধু অডিটে
base = LogisticRegression(C=1e10, max_iter=1000).fit(tr[F], tr.label)
p_te = base.predict_proba(te[F])[:, 1]
yhat = (p_te >= 0.5).astype(int)

def rates(y, h, g):                                # দল g (True/False)-এর তিনটি হার
    return np.array([h[g].mean(), h[g & (y == 1)].mean(), h[g & (y == 0)].mean()])
def gaps(y, h, g):                                 # অন্যরা − দল g: selection, TPR, FPR ফাঁক
    return rates(y, h, ~g) - rates(y, h, g)

y, B = te.label.to_numpy(), te.B.to_numpy() == 1
BO = B & (te.old.to_numpy() == 1)                  # intersectional: নদীচর আর 50+
rng = np.random.default_rng(0)
for name, g in [("নদীচর", B), ("নদীচর ও 50+", BO)]:
    boot = []
    for _ in range(1000):                          # bootstrap: test সারি ফেরত রেখে নেওয়া
        i = rng.integers(0, len(y), len(y))
        boot.append(gaps(y[i], yhat[i], g[i]))
    lo, hi = np.percentile(boot, [2.5, 97.5], axis=0)
    est = gaps(y, yhat, g)
    print(f"{name}: n={g.sum()}, y=1 আছে {(g & (y == 1)).sum()}")
    for k, m in enumerate(["selection", "TPR", "FPR"]):
        print(f"   {m:9s} ফাঁক {est[k]:+.3f}  95% CI [{lo[k]:+.3f}, {hi[k]:+.3f}]")
print("label হার: অন্যরা", y[~B].mean().round(3), "| নদীচর", y[B].mean().round(3),
      "| আসল হার নদীচর", te.repaid[B].mean().round(3))

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

নদীচর: n=905, y=1 আছে 352
   selection ফাঁক +0.364  95% CI [+0.328, +0.401]
   TPR       ফাঁক +0.314  95% CI [+0.255, +0.368]
   FPR       ফাঁক +0.273  95% CI [+0.223, +0.325]
নদীচর ও 50+: n=94, y=1 আছে 42
   selection ফাঁক +0.263  95% CI [+0.162, +0.354]
   TPR       ফাঁক +0.176  95% CI [+0.030, +0.325]
   FPR       ফাঁক +0.257  95% CI [+0.152, +0.348]
label হার: অন্যরা 0.647 | নদীচর 0.389 | আসল হার নদীচর 0.537

নদীচর (৯০৫ জন): অনুমোদনের হার বাকিদের চেয়ে ০.৩৬৪ কম, TPR (সময়মতো দেওয়া বলে খাতায় লেখা গ্রাহকদের অনুমোদন) ০.৩১৪ কম; CI-এর চওড়া প্রায় ০.১১, আর শূন্য থেকে অনেক দূরে। এই ফাঁক এলোমেলোতা নয়। নদীচর ও ৫০+ (৯৪ জন, y = ১ মাত্র ৪২ জন): TPR ফাঁকের অনুমান ০.১৭৬, কিন্তু CI ০.০৩০ থেকে ০.৩২৫, চওড়া প্রায় ০.৩০. এই দলের ফাঁক পুরো নদীচরের চেয়ে “ছোট” বলা যায় না; ডেটা শুধু বলে ফাঁকটি সম্ভবত শূন্যের ওপরে। শেষ লাইনটি সমস্যার মূল দেখায়: নদীচরে খাতার হার ০.৩৮৯, অথচ আসল হার ০.৫৩৭.

ছোট দলের তিন ফাঁদ। (১) চওড়া CI-কে “ফাঁক নেই” পড়া: CI শূন্য ছুঁলেও বড় ফাঁক সম্ভব। (২) অনেক দল আর অনেক metric একসঙ্গে দেখে সবচেয়ে খারাপটি তুলে ধরা: এটি পর্ব ২২-এর একাধিক তুলনার সমস্যা। (৩) খুব ছোট দল (ধরুন ৩০-এর কম positive) চুপচাপ বাদ দেওয়া: তখন সবচেয়ে ঝুঁকির মানুষেরাই রিপোর্ট থেকে হারিয়ে যান। সমাধান: n আর CI সবসময় পাশে লিখুন, আগে থেকে ঠিক করা দলগুলো মাপুন, আর ছোট দলের জন্য বেশি ডেটা সংগ্রহের পরিকল্পনা করুন।

৩ · ক্ষতি থেকে metric বাছাই — allocative বনাম quality-of-service

AI-এর ভিত্তি কোর্সের পর্ব ৭২ দেখিয়েছে, সব fairness সংজ্ঞা একসঙ্গে মেলে না। তাই বাছতে হয়, আর বাছার সবচেয়ে কাজের প্রশ্ন: কোন ভুলে কার কী ক্ষতি? Kate Crawford ২০১৭ সালের NeurIPS-এর একটি বক্তৃতায় allocative আর representational (উপস্থাপনার) ক্ষতি আলাদা করেন, আর Fairlearn-এর নথিতে quality-of-service ক্ষতিও আলাদা নাম পেয়েছে:

Allocative ক্ষতি

সুযোগ বা সম্পদ ভাগের সিদ্ধান্ত: বাকি, ঋণ, চাকরির ডাক। Allocative harm-এ মূল প্রশ্ন, যোগ্য মানুষ কোন দলে বেশি বাদ পড়ছেন (TPR), আর অযোগ্যকে সুবিধা দিয়ে কোন দলকে দেনায় ফেলা হচ্ছে (FPR).

সেবার মানের ক্ষতি

সবাই সেবা পান, কিন্তু কারও জন্য খারাপ মানের: delivery-র সময়ের অনুমান নদীচরে বেশি ভুল, বা বাংলা কণ্ঠস্বর চেনায় এক উপভাষায় বেশি ভুল। Quality-of-service harm মাপা হয় দলভেদে ভুলের মাপ (MAE, error rate) দিয়ে।

উপস্থাপনার ক্ষতি

কোনো দলকে হেয় বা অদৃশ্য দেখানো, যেমন search বা ছবির ফলে গৎবাঁধা ধারণা। এটি মূলত ভাষা ও ছবির model-এর প্রশ্ন; পরের generative AI কোর্সে আসবে।

প্রতিটি ধাপে একটি প্রশ্ন, উত্তর আর তার metric
প্রশ্ন
উত্তর
metric
  1. সিদ্ধান্তের ধরন। “পরে টাকা দিন” একটি সুবিধা ভাগের সিদ্ধান্ত, তাই allocative. Metric বাছার আগে এই শ্রেণিবিভাগ।
  2. FN-এর ক্ষতি। সময়মতো দেবেন এমন গ্রাহক প্রত্যাখ্যাত হলে তিনি সুবিধা হারান। এই ক্ষতি দলভেদে সমান রাখতে চাইলে equal opportunity: TPR সমান।
  3. FP-এর ক্ষতিও। যিনি দিতে পারবেন না, তাঁকে বাকি দেওয়াও তাঁর ক্ষতি (দেনা, জরিমানা, পরের বার নিষেধ)। দুটি ক্ষতিই জরুরি হলে equalized odds: TPR আর FPR দুটিই সমান।
  4. Label-এর প্রশ্ন। TPR আর FPR আসল ফল জানে বলে ধরে নেয়; এখানে খাতার label নদীচরের বিরুদ্ধে বিকৃত। তাই label ছাড়া মাপা যায় এমন selection rate পাশে রাখুন, আর label-এর উৎস ঠিক করাকে প্রথম কাজ ধরুন।
  5. Quality-of-service. Delivery-র সময়ের model-এ কেউ “বাদ” পড়েন না; ক্ষতি হলো খারাপ অনুমান। দলভেদে MAE, আর পরিসর দিলে দলভেদে coverage (পর্ব ৫৮, Conformal prediction-এর বিষয়)।
  6. লিখে রাখা। বাছাই একটি মূল্যবোধের সিদ্ধান্ত, তাই metric, গ্রহণযোগ্য ফাঁক, CI-সহ মাপার নিয়ম আর কোন অংশীজনের মত নেওয়া হলো, সব model card-এ লিখুন।

লক্ষ করুন, demographic parity এই তালিকায় প্রধান metric হয়নি। এটি আসল যোগ্যতা দেখে না, তাই base rate সত্যিই আলাদা হলে সমান অনুমোদন জোর করে চাপালে এক দলে অযোগ্যকে বাকি দিতে হয়। তবু label অবিশ্বাস্য হলে এটি একটি দরকারি সতর্ক-সংকেত। Hardt, Price আর Srebro ২০১৬ সালের প্রবন্ধে ঠিক এই যুক্তিতে equal opportunity প্রস্তাব করেন।

৪ · Pre-processing — Kamiran–Calders reweighing

প্রতিকার তিন জায়গায় বসানো যায়: শেখার আগে ডেটায়, শেখার সময় loss-এ, আর শেখার পরে সিদ্ধান্তের সীমায়। নিচের ছবি তিনটিকে পাশাপাশি দেখায়, আর একটি ব্যবহারিক পার্থক্যও: দলের কলাম কখন লাগে।

প্রতিকার কোথায় বসে

Pre-processing (data), in-processing (loss), post-processing (threshold)

Where the three kinds of fairness mitigation act ডেটা আর label শেখা (loss) score p(x) সীমা → সিদ্ধান্ত Pre-processing ডেটার ওজন বদল In-processing loss + λ · ফাঁক² Post-processing দলভিত্তিক সীমা দলের কলাম কখন লাগে? pre আর in: শুধু training-এ post: প্রতিটি সিদ্ধান্তের সময়
ছবি ৪.১ — তিন ধরনের প্রতিকার। Pre আর in-processing-এ দলের কলাম শুধু শেখার সময় লাগে, তাই চালু model দল দেখে না; post-processing-এ প্রতিটি নতুন আবেদনেই দল জানতে হয়, যা আইনি ও গোপনীয়তার প্রশ্ন তোলে।

Pre-processing-এর সবচেয়ে পরিচিত রূপ Faisal Kamiran আর Toon Calders-এর reweighing (Knowledge and Information Systems জার্নালে ২০১২ সালে প্রকাশিত; ধারণাটি তাঁদের আগের কাজেও ছিল)। খাতায় দল আর label পরস্পর-নির্ভর: নদীচরে label = ১ কম। Reweighing প্রতিটি (দল, label) ঘরকে এমন ওজন দেয়, যাতে ওজনসহ গুনলে দল আর label পরস্পর-নিরপেক্ষ দেখায়:

w(g, c) = P(দল = g) · P(label = c) ÷ P(দল = g, label = c)

লবটি “দল আর label নিরপেক্ষ হলে ঘরে যত সারি থাকার কথা” তার অনুপাত, হরটি আসলে যত আছে। তাই w = প্রত্যাশিত সংখ্যা ÷ দেখা সংখ্যা। ওজন ঢোকে fit(..., sample_weight=w) দিয়ে (sample weight); ডেটার কোনো সারি বা feature বদলায় না।

n = 150: A-তে label 1 = 60, label 0 = 40; নদীচরে label 1 = 15, label 0 = 35
ধাপ
A
নদীচর
সব মিলিয়ে
  1. গোনা। A-র ১০০ জনের ৬০ জন খাতায় “সময়মতো”, নদীচরের ৫০ জনের মাত্র ১৫ জন। মোট label ১ = ৭৫.
  2. প্রান্তিক হার। P(A) = ০.৬৬৭, P(নদীচর) = ০.৩৩৩, P(label ১) = ০.৫.
  3. যৌথ হার। প্রতিটি ঘরকে ১৫০ দিয়ে ভাগ: ০.৪০০, ০.২৬৭, ০.১০০, ০.২৩৩.
  4. ওজন। A-র “সময়মতো” ঘর বেশি ভরা, তাই ওজন ০.৮৩৩ (হালকা); নদীচরের “সময়মতো” ঘর ফাঁকা, ওজন ১.৬৬৭ (ভারী)। দুই “দেরি” ঘরে ১.২৫ আর ০.৭১৪.
  5. যাচাই। ওজনসহ A-তে ৫০ বনাম ৫০, নদীচরে ২৫ বনাম ২৫: দুই দলেই label ১-এর হার ০.৫, পুরো ডেটার হারের সমান। মোট ওজন ১৫০, তাই ডেটার “আকার” বদলায়নি।
  6. মানে। প্রতিটি ঘরের ওজন = নিরপেক্ষ হলে যত থাকত ÷ যত আছে; যেমন A-র “সময়মতো”: ১০০ × ০.৫ = ৫০ থাকার কথা, আছে ৬০, তাই ৫০/৬০ = ০.৮৩৩.

নিচের ব্লক train-এর ওপর একই হিসাব করে, প্রতিটি দলের ওজনসহ label-হার যাচাই করে, ওজন দিয়ে model আবার শেখায়, আর baseline-এর পাশে ফাঁকগুলো ছাপে। শেষ কলাম আসল ফল ধরে TPR ফাঁক, যা শুধু simulation-এ জানা যায়।

g_tr, y_tr = tr.B.to_numpy(), tr.label.to_numpy()    # দল শুধু training-এর ওজনে লাগে
W = np.zeros((2, 2))
for g in (0, 1):
    for c in (0, 1):                               # w = P(দল) · P(label) / P(দল, label)
        both = ((g_tr == g) & (y_tr == c)).mean()
        W[g, c] = (g_tr == g).mean() * (y_tr == c).mean() / both
print(pd.DataFrame(W.round(3), index=["A", "নদীচর"], columns=["label 0", "label 1"]))
w = W[g_tr, y_tr]                                  # প্রতিটি সারির ওজন
for g, name in ((0, "A"), (1, "নদীচর")):
    s = g_tr == g
    print(f"{name}: label=1 হার {y_tr[s].mean():.3f} → ওজনসহ {np.average(y_tr[s], weights=w[s]):.3f}")

rw = LogisticRegression(C=1e10, max_iter=1000).fit(tr[F], tr.label, sample_weight=w)
p_rw = rw.predict_proba(te[F])[:, 1]
yt = te.repaid.to_numpy()                          # আসল ফল: বাস্তবে অজানা
def show(name, h):
    gl, gt = gaps(y, h, B), gaps(yt, h, B)
    print(f"{name:10s} acc {np.mean(h == y):.3f} | ফাঁক: selection {gl[0]:+.3f}, "
          f"TPR {gl[1]:+.3f}, FPR {gl[2]:+.3f} | আসল-ফলে TPR ফাঁক {gt[1]:+.3f}")
yhat_rw = (p_rw >= 0.5).astype(int)
show("baseline", yhat); show("reweighing", yhat_rw)
print("ওজন (orders, ret, tenure, far):", base.coef_[0].round(2), "→", rw.coef_[0].round(2))

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

       label 0  label 1
A        1.208    0.879
নদীচর    0.723    1.444
A: label=1 হার 0.632 → ওজনসহ 0.555
নদীচর: label=1 হার 0.385 → ওজনসহ 0.555
baseline   acc 0.682 | ফাঁক: selection +0.364, TPR +0.314, FPR +0.273 | আসল-ফলে TPR ফাঁক +0.316
reweighing acc 0.673 | ফাঁক: selection +0.108, TPR +0.019, FPR +0.032 | আসল-ফলে TPR ফাঁক +0.048
ওজন (orders, ret, tenure, far): [ 0.38 -6.31  0.03 -0.71] → [ 0.37 -6.39  0.03  0.12]

ওজন: নদীচরের “সময়মতো” সারি ১.৪৪৪ গুণ ভারী, “দেরি” সারি ০.৭২৩; A-তে উল্টো, মৃদু। ওজনসহ দুই দলের label-হার ০.৫৫৫, পুরো train-এর হার। ফল: TPR ফাঁক ০.৩১৪ থেকে ০.০১৯, FPR ফাঁক ০.২৭৩ থেকে ০.০৩২, selection ফাঁক ০.১০৮; accuracy (খাতা ধরে) ০.৬৮২ থেকে ০.৬৭৩. কীভাবে: শেষ লাইন দেখায় model-এর ওজনে কী বদলেছে: proxy far-এর ওজন −০.৭১ থেকে +০.১২. খাতার পক্ষপাত শিখতে model এতদিন proxy ব্যবহার করছিল; ওজনসহ ডেটায় দল আর label নিরপেক্ষ, তাই সেই পথ বন্ধ। আসল ফল ধরে TPR ফাঁকও ০.৩১৬ থেকে ০.০৪৮: এই ডেটায় label-এর বিকৃতি ঠিক দল-অনুযায়ী, তাই reweighing আসল ক্ষতিও কমিয়েছে। বিকৃতি অন্য রূপের হলে (যেমন দলের ভেতরে শুধু কিছু গ্রাহকের) এমন হবে, তার নিশ্চয়তা নেই।

৫ · In-processing — fairness penalty আর accuracy–fairness curve

In-processing মানে শেখার লক্ষ্যই বদলানো। সবচেয়ে সরল রূপ fairness penalty: log loss-এর সঙ্গে ফাঁকের বর্গ যোগ, একটি গুণক λ দিয়ে:

L(β) = log loss(β) + λ · [(p̄A,1 − p̄নদী,1)² + (p̄A,0 − p̄নদী,0)²]

এখানে β model-এর ওজন, p̄A,1 হলো দল A-র label ১ সারিগুলোর গড় অনুমিত সম্ভাবনা, অন্যগুলো একইভাবে। আসল TPR (০.৫ পেরোনো সারির ভাগ) ধাপে ধাপে বদলায়, তাই তার gradient নেই; গড় সম্ভাবনা তার মসৃণ বিকল্প (“নরম TPR”). প্রথম বর্গ তাই নরম TPR ফাঁক, দ্বিতীয়টি নরম FPR ফাঁক: equalized odds-এর মসৃণ রূপ। λ = ০ হলে সাধারণ logistic regression; λ বাড়লে ফাঁকের দাম বাড়ে। Optimizer হলো SciPy-র L-BFGS-B (পর্ব ১৫), gradient সংখ্যায় আনুমানিক; penalty-র এই ধারণার আত্মীয় Kamishima ও সহকর্মীদের ২০১২ সালের “prejudice remover” আর Zafar ও সহকর্মীদের ২০১৭ সালের শর্তযুক্ত পদ্ধতি।

import matplotlib.pyplot as plt
from scipy.optimize import minimize

mu, sd = tr[F].mean().to_numpy(), tr[F].std().to_numpy()
Xtr = np.c_[np.ones(len(tr)), (tr[F].to_numpy() - mu) / sd]   # প্রথম কলাম intercept
Xte = np.c_[np.ones(len(te)), (te[F].to_numpy() - mu) / sd]
cells = [((g_tr == 0) & (y_tr == c), (g_tr == 1) & (y_tr == c)) for c in (1, 0)]

def loss(b, lam):                                  # log loss + λ · (নরম TPR ফাঁক² + নরম FPR ফাঁক²)
    p = 1 / (1 + np.exp(-Xtr @ b))
    ll = -np.mean(y_tr * np.log(p + 1e-12) + (1 - y_tr) * np.log(1 - p + 1e-12))
    return ll + lam * sum((p[a].mean() - p[bb].mean()) ** 2 for a, bb in cells)

b, curve = np.zeros(5), []
for lam in [0, 1, 3, 10, 30, 100, 300]:
    b = minimize(loss, b, args=(lam,), method="L-BFGS-B").x   # আগের সমাধান থেকে শুরু
    h = (Xte @ b >= 0).astype(int)                 # p ≥ 0.5 মানে logit ≥ 0
    gl = gaps(y, h, B)
    eo = max(abs(gl[1]), abs(gl[2]))               # equalized odds difference
    curve.append((lam, np.mean(h == y), eo))
    print(f"λ={lam:3d}: acc {np.mean(h == y):.3f}, TPR ফাঁক {gl[1]:+.3f}, FPR ফাঁক {gl[2]:+.3f}, "
          f"selection ফাঁক {gl[0]:+.3f}, far-এর ওজন {b[4]:+.2f}")
    if lam == 10:
        yhat_pen = h
lam_, acc_, eo_ = np.array(curve).T
fig, ax = plt.subplots(figsize=(6, 3.8))
ax.plot(eo_, acc_, "o-")
for l, a, x in curve:
    ax.annotate(f"λ={l:g}", (x, a), textcoords="offset points", xytext=(6, 4), fontsize=8)
ax.set(xlabel="equalized odds difference (test)", ylabel="accuracy vs recorded label",
       title="Fairness penalty: accuracy-fairness trade-off")
plt.show()

আসল আউটপুট (ব্রাউজারের Python; ছবিটি এর নিচে আঁকা হয়):

λ=  0: acc 0.682, TPR ফাঁক +0.314, FPR ফাঁক +0.273, selection ফাঁক +0.364, far-এর ওজন -0.33
λ=  1: acc 0.674, TPR ফাঁক +0.088, FPR ফাঁক +0.102, selection ফাঁক +0.174, far-এর ওজন -0.03
λ=  3: acc 0.672, TPR ফাঁক +0.032, FPR ফাঁক +0.046, selection ফাঁক +0.120, far-এর ওজন +0.03
λ= 10: acc 0.670, TPR ফাঁক +0.016, FPR ফাঁক +0.019, selection ফাঁক +0.095, far-এর ওজন +0.05
λ= 30: acc 0.660, TPR ফাঁক +0.023, FPR ফাঁক -0.004, selection ফাঁক +0.076, far-এর ওজন +0.02
λ=100: acc 0.622, TPR ফাঁক +0.026, FPR ফাঁক +0.016, selection ফাঁক +0.056, far-এর ওজন -0.00
λ=300: acc 0.581, TPR ফাঁক +0.005, FPR ফাঁক +0.008, selection ফাঁক +0.014, far-এর ওজন -0.01

প্রথম ধাপেই বড় লাভ: λ = ১-এ TPR ফাঁক ০.৩১৪ থেকে ০.০৮৮, accuracy মাত্র ০.৬৮২ থেকে ০.৬৭৪. λ = ১০-এ দুই ফাঁকই ০.০২-এর নিচে, accuracy ০.৬৭০. তারপর দাম বাড়ে: λ = ১০০-এ accuracy ০.৬২২, ৩০০-এ ০.৫৮১, অথচ ফাঁক আর তেমন কমে না। ছবিতে curve-টি একটি “কনুই”: বাম দিকে খাড়া নামা মানে বাড়তি fairness-এর দাম হঠাৎ বেশি। Proxy-র ওজন (standardised মাপে) −০.৩৩ থেকে λ = ১-এই প্রায় শূন্য (−০.০৩): penalty প্রথমে proxy-র পথটিই বন্ধ করে। λ = ৩০০-এ accuracy ০.৫৮১, যা সবাইকে অনুমোদন দেওয়ার accuracy-র (test-এ খাতার label ১-এর হার ০.৫৬৯) কাছাকাছি: model প্রায় সবাইকে এক রকম দেখে। কোন বিন্দু বাছবেন, তা metric-এর কাজ নয়, সিদ্ধান্তের: আগে থেকে লেখা গ্রহণযোগ্য ফাঁক (অংশ ৩) আর validation ডেটায় বাছাই, test-এ নয়। এখানে সরলতার জন্য test-এ পুরো curve দেখানো হয়েছে।

fairlearn দিয়ে সত্যিকারের run — MetricFrame আর ExponentiatedGradient

Penalty-র একটি অসুবিধা: λ আর আসল ফাঁকের সম্পর্ক সরাসরি নয়। Agarwal, Beygelzimer, Dudík, Langford আর Wallach ২০১৮ সালে (ICML) একটি “reductions” পদ্ধতি দেন: শর্তটি সরাসরি লেখা হয় (ফাঁক ≤ ε), তারপর পর্ব ১৬-এর Lagrange multiplier-এর খেলায় সাধারণ classifier বারবার নতুন ওজনে শেখানো হয়। ফল কয়েকটি classifier-এর একটি এলোমেলো মিশ্রণ (randomized classifier). Fairlearn লাইব্রেরির ExponentiatedGradient এটি করে। লাইব্রেরিটি ব্রাউজারের Python-এ নেই, তাই ডেস্কটপে একটি uv project-এ সত্যিকারের run (লেখার সময়ের সর্বশেষ স্থিতিশীল version, প্রথম লাইনে ছাপা; একটি run, একটি যন্ত্র), একই bnpl.py আর একই ভাগ:

import sys
import numpy as np
import sklearn, fairlearn
from sklearn.linear_model import LogisticRegression
from fairlearn.metrics import (MetricFrame, selection_rate, true_positive_rate,
                               false_positive_rate, equalized_odds_difference)
from fairlearn.reductions import ExponentiatedGradient, EqualizedOdds, DemographicParity
from bnpl import make_data

print(f"Python {sys.version.split()[0]} | scikit-learn {sklearn.__version__} | fairlearn {fairlearn.__version__}")
df = make_data()
tr, te = df.iloc[:3000], df.iloc[3000:]
F = ["orders", "ret", "tenure", "far"]
group = te.B.map({0: "A", 1: "Nodichor"})
metrics = {"selection": selection_rate, "TPR": true_positive_rate, "FPR": false_positive_rate}

base = LogisticRegression(C=1e10, max_iter=1000).fit(tr[F], tr.label)
mf = MetricFrame(metrics=metrics, y_true=te.label, y_pred=base.predict(te[F]),
                 sensitive_features=group)
print("baseline by group:\n", mf.by_group.round(3))
print("difference:", mf.difference().round(3).to_dict())

for name, cons in [("EqualizedOdds", EqualizedOdds()), ("DemographicParity", DemographicParity())]:
    eg = ExponentiatedGradient(LogisticRegression(C=1e10, max_iter=1000), constraints=cons, eps=0.01)
    eg.fit(tr[F], tr.label, sensitive_features=tr.B)
    yp = eg.predict(te[F], random_state=0)
    mf2 = MetricFrame(metrics=metrics, y_true=te.label, y_pred=yp, sensitive_features=group)
    print(f"\nExponentiatedGradient({name}, eps=0.01): {len(eg.predictors_)} predictors, "
          f"accuracy {np.mean(yp == te.label):.3f}")
    print(mf2.by_group.round(3))
    print("EO difference:", round(equalized_odds_difference(te.label, yp, sensitive_features=group), 3))
~/bazarghar-ml$ uv add scikit-learn fairlearn
…
~/bazarghar-ml$ uv run python fair_check.py
Python 3.14.4 | scikit-learn 1.9.1 | fairlearn 0.15.0
baseline by group:
           selection    TPR    FPR
B                                
A             0.705  0.808  0.517
Nodichor      0.341  0.494  0.244
difference: {'selection': 0.364, 'TPR': 0.314, 'FPR': 0.273}

ExponentiatedGradient(EqualizedOdds, eps=0.01): 12 predictors, accuracy 0.625
          selection    TPR    FPR
B                                
A             0.780  0.841  0.668
Nodichor      0.713  0.827  0.640
EO difference: 0.028

ExponentiatedGradient(DemographicParity, eps=0.01): 10 predictors, accuracy 0.655
          selection    TPR    FPR
B                                
A             0.622  0.730  0.424
Nodichor      0.613  0.781  0.506
EO difference: 0.083

প্রথম টেবিল পাতার baseline-এর সঙ্গে হুবহু মেলে (ফাঁক ০.৩৬৪, ০.৩১৪, ০.২৭৩)। EqualizedOdds শর্তে ১২টি classifier-এর মিশ্রণ EO ফাঁক ০.০২৮-এ নামায়, কিন্তু accuracy ০.৬২৫; দেখুন কীভাবে: নদীচরের অনুমোদন ০.৩৪১ থেকে ০.৭১৩, আর দুই দলের FPR প্রায় ০.৬৫. খাতা ধরে “দেরি” এমন অনেককে বাকি দেওয়া হচ্ছে। DemographicParity-তে selection প্রায় সমান (০.৬২২ বনাম ০.৬১৩), অথচ TPR আর FPR-এর ফাঁক উল্টো দিকে খোলে (EO ফাঁক ০.০৮৩): এক সংজ্ঞা মেলালে অন্যটি মেলে না। এই run-এ penalty-র λ = ১০ বিন্দু (accuracy ০.৬৭০) ExponentiatedGradient-এর চেয়ে ভালো দেখায়; একটি ডেটা আর একটি ভাগের ফল, সাধারণ নিয়ম নয়।

৬ · Post-processing — equalized odds-এর দলভিত্তিক আর randomised সীমা

Post-processing model ছোঁয় না, শুধু score থেকে সিদ্ধান্তের নিয়ম বদলায়। Hardt, Price আর Srebro (২০১৬) দেখান: প্রতিটি দলের ROC curve আঁকুন (প্রতিটি সীমায় (FPR, TPR) বিন্দু); দুই দলের TPR আর FPR সমান মানে দুই দল একই বিন্দুতে থাকবে। সমস্যা: খারাপ ROC-র দলের (এখানে নদীচর, কারণ তার label শব্দে ভরা) বিন্দুটি ভালো দলের curve-এর নিচে থাকে, আর ভালো দলের কোনো একক সীমা সেখানে পৌঁছায় না।

দুটি বাসের ভাড়া ২০ আর ৪০ টাকা, আপনার বাজেট গড়ে ৩০. কোনো একটি বাসে ৩০ হয় না, কিন্তু অর্ধেক দিন এটি আর অর্ধেক দিন ওটি নিলে গড় ঠিক ৩০. Randomised সীমা একই কৌশল: দুটি সীমার মাঝে মুদ্রা ছুড়লে প্রত্যাশিত TPR আর FPR দুটি বিন্দুর মাঝের রেখায় যেকোনো জায়গায় বসানো যায়।

প্রতিটি বিন্দু (FPR, TPR), baseline model-এর train score থেকে
ধাপ
বিন্দু
মানে
  1. লক্ষ্য। নদীচরের সীমা ০.২৬-এ FPR ০.৬৬৭, TPR ০.৮৮০. খোঁজার কোড নদীচরের সব সীমা চেষ্টা করে এটি বেছেছে, কারণ এখানে দুই দলের মোট accuracy সবচেয়ে বেশি।
  2. A-র একটি সীমা। A-তে সীমা ০.৫৬ দেয় (০.৩৮৭, ০.৭৬৫): দুই হারই কম। A-র ROC নদীচরের চেয়ে ওপরে, তাই FPR ০.৬৬৭-এ A-র TPR ০.৮৮০-এর বেশি; কোনো একক সীমায় দুই হার একসঙ্গে মেলে না।
  3. আরেকটি সীমা। সীমা ০.১৬ দেয় (০.৯৫২, ০.৯৯৫): প্রায় সবাই অনুমোদিত, দুই হারই বেশি।
  4. মিশ্রণ। প্রতিটি A-গ্রাহকের জন্য মুদ্রা ছুড়ে অর্ধেক সম্ভাবনায় সীমা ০.১৬, নইলে ০.৫৬. প্রত্যাশিত বিন্দু (০.৬৬৯, ০.৮৮০), নদীচরের সঙ্গে প্রায় হুবহু।
  5. কেন রৈখিক। প্রত্যাশিত হার দুই সীমার হারের q-ওজনের গড়, তাই q ০ থেকে ১ বদলালে বিন্দুটি দুই ROC বিন্দুর সংযোগ রেখা ধরে চলে। এভাবে ROC-র নিচের যেকোনো বিন্দু পাওয়া যায় (Hardt ও সহকর্মী, ২০১৬)।
  6. দাম। Train-এ নির্ধারিত সীমা-জোড়া (ফাঁক ০.০২৬) accuracy ০.৬৩৩, randomised ০.৬৩৬: এখানে মুদ্রা accuracy কেনেনি, কিনেছে হুবহু মিল। দুই পথেই A-র FPR বাড়িয়ে নদীচরের সমান করা হয়েছে, তাই test-এ accuracy ০.৬৮২ থেকে ০.৬২৮.

নিচের ব্লক দুটি পথই চেষ্টা করে। প্রথমে নির্ধারিত দলভিত্তিক সীমা: সব (A, নদীচর) সীমা-জোড়ার মধ্যে যেগুলোর TPR আর FPR ফাঁক ০.০৩-এর মধ্যে, তার সবচেয়ে বেশি accuracy-রটি। তারপর randomised: নদীচরের প্রতিটি সীমার বিন্দু ছুঁতে A-র দুটি সীমা আর একটি সম্ভাবনা q খোঁজা। সীমা বাছা হয় train-এর score-এ, মাপা হয় test-এ; শেষে চারটি পদ্ধতির তুলনা, প্রতিটি দলের PPV-সহ।

p_tr = base.predict_proba(tr[F])[:, 1]             # সীমা বাছা হয় train-এর score-এ
T = np.round(np.arange(0, 1.01, 0.02), 2)
def roc(s):                                        # দল s-এর প্রতিটি সীমায় (FPR, TPR)
    p, yy = p_tr[s], y_tr[s]
    return np.stack([(p[yy == 0, None] >= T).mean(0), (p[yy == 1, None] >= T).mean(0)], 1)
RA, RB, piA, piB = roc(g_tr == 0), roc(g_tr == 1), y_tr[g_tr == 0].mean(), y_tr[g_tr == 1].mean()
acc = lambda R, pi: pi * R[..., 1] + (1 - pi) * (1 - R[..., 0])
nA, nB = (g_tr == 0).sum(), (g_tr == 1).sum()
def total(RA_, RB_): return (nA * acc(RA_, piA) + nB * acc(RB_, piB)) / (nA + nB)
d = np.abs(RA[:, None] - RB[None]).max(-1)         # সব নির্ধারিত সীমা-জোড়া (A, B)
ok = d <= 0.03
a, k = np.unravel_index(np.where(ok, total(RA[:, None], RB[None]), 0).argmax(), d.shape)
print(f"নির্ধারিত: A {T[a]}, B {T[k]} → A {RA[a].round(3)}, B {RB[k].round(3)}, ফাঁক {d[a, k]:.3f}, "
      f"train acc {total(RA[a], RB[k]):.3f}")
q = np.linspace(0, 1, 21)[:, None, None, None]     # A: সম্ভাবনা q-তে সীমা i, নইলে j
mix = q * RA[None, :, None] + (1 - q) * RA[None, None, :]
best = (-1,)
for k in range(len(T)):                            # B-র প্রতিটি সীমার বিন্দু ছোঁয়ার চেষ্টা
    dd = np.abs(mix - RB[k]).max(-1)
    idx = np.unravel_index(dd.argmin(), dd.shape)
    if dd[idx] <= 0.005 and total(mix[idx], RB[k]) > best[0]:
        best = (total(mix[idx], RB[k]), k, idx)
acc_r, k, (qi, i, j) = best
print(f"randomised: B {T[k]} {RB[k].round(3)}; A: {q.ravel()[qi]:.2f} সম্ভাবনায় {T[i]} "
      f"{RA[i].round(3)}, নইলে {T[j]} {RA[j].round(3)} → {mix[qi, i, j].round(3)}, acc {acc_r:.3f}")
coin = np.random.default_rng(1).random(len(te)) < q.ravel()[qi]
yhat_eo = (p_te >= np.where(B, T[k], np.where(coin, T[i], T[j]))).astype(int)
rows = []
for name, h in [("baseline", yhat), ("reweighing", yhat_rw), ("penalty λ=10", yhat_pen),
                ("post EO", yhat_eo)]:
    gl, gt = gaps(y, h, B), gaps(yt, h, B)
    rows.append([name, np.mean(h == y), *gl, gt[1], y[~B & (h == 1)].mean(), y[B & (h == 1)].mean()])
print(pd.DataFrame(rows, columns=["পদ্ধতি", "acc", "sel", "TPR", "FPR", "TPR_আসল",
                                  "PPV_A", "PPV_নদী"]).round(3).to_string(index=False))

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

নির্ধারিত: A 0.32, B 0.2 → A [0.763 0.962], B [0.783 0.936], ফাঁক 0.026, train acc 0.633
randomised: B 0.26 [0.667 0.88 ]; A: 0.50 সম্ভাবনায় 0.16 [0.952 0.995], নইলে 0.56 [0.387 0.765] → [0.669 0.88 ], acc 0.636
      পদ্ধতি   acc   sel    TPR   FPR  TPR_আসল  PPV_A  PPV_নদী
    baseline 0.682 0.364  0.314 0.273    0.316  0.742    0.563
  reweighing 0.673 0.108  0.019 0.032    0.048  0.759    0.534
penalty λ=10 0.670 0.095  0.016 0.019    0.034  0.750    0.514
     post EO 0.628 0.070 -0.002 0.048   -0.005  0.693    0.457

নির্ধারিত বনাম randomised: সবচেয়ে ভালো নির্ধারিত জোড়া (A ০.৩২, নদীচর ০.২০) ফাঁক ০.০২৬-এ থামে; randomised পথ train-এ ফাঁক ০.০০৫-এর মধ্যে আনে, accuracy প্রায় একই (০.৬৩৬ বনাম ০.৬৩৩)। তুলনার টেবিল (test, খাতা ধরে; sel, TPR, FPR হলো “বাকিরা − নদীচর” ফাঁক): post-processing-এ TPR ফাঁক −০.০০২, কিন্তু FPR ফাঁক ০.০৪৮, কারণ train-এ বাছা সীমা test-এ পুরো মেলে না, আর মুদ্রাও শব্দ যোগ করে। Accuracy ০.৬২৮, চারটির মধ্যে সবচেয়ে কম: ফাঁক মেলাতে A-র অনেক “দেরি”-label গ্রাহককে অনুমোদন দিতে হয়েছে, অর্থাৎ ভালো দলের ফল নামিয়ে আনা (levelling down). Reweighing আর penalty প্রায় একই জায়গায় (accuracy ০.৬৭৩ আর ০.৬৭০, দুই ফাঁক ০.০৩৫-এর নিচে)। TPR_আসল আসল ফল ধরে: তিনটি প্রতিকারেই ০.০৫-এর নিচে, কারণ এই simulation-এ খাতার বিকৃতি দলের সঙ্গে বাঁধা। শেষ দুই কলাম অংশ ৭-এর বিষয়। সবই একটি seed আর একটি ভাগের ফল; সিদ্ধান্তের আগে কয়েকটি seed আর CI দেখুন।

৭ · তিন পথ পাশাপাশি, আর calibration-এর সঙ্গে দ্বন্দ্ব

পথদল কখন লাগেভালো দিকখারাপ দিক
Pre: reweighingশুধু training-এসরল, যেকোনো model-এ sample_weight দিয়ে চলেশুধু (দল, label)-এর অনুপাত সারায়; কোন metric মিলবে, সরাসরি নিয়ন্ত্রণ নেই
In: penalty বা শর্তশুধু training-এযে ফাঁক চান সেটিই লক্ষ্য; λ দিয়ে curveনিজের loss লাগে বা ExponentiatedGradient-এর মতো মোড়ক; ফল এলোমেলো মিশ্রণ হতে পারে
Post: দলভিত্তিক সীমাপ্রতিটি সিদ্ধান্তেmodel বদলাতে হয় না; ফাঁক প্রায় ঠিক মাপে মেলেProtected attribute সরাসরি সিদ্ধান্তে; randomisation ব্যাখ্যা করা কঠিন; ভালো দলের ফল নামে

এখন অংশ ৬-এর শেষ টেবিলের PPV কলাম দুটি পড়ুন। AI-এর ভিত্তি কোর্সের পর্ব ৭২-এ Chouldechova-র অভেদ এসেছে; এখানে সেটি নিজেদের সংখ্যায় যাচাই করা যায়। প্রতিটি দলে base rate π হলে:

PPV = π · TPR ÷ [π · TPR + (১ − π) · FPR]

Post-processing-এর পর দুই দলে TPR প্রায় ০.৮৮ আর FPR প্রায় ০.৬৭ (train-এ বাছা বিন্দু)। Test-এ খাতার base rate A-তে ০.৬৪৭, নদীচরে ০.৩৮৯. সূত্রে বসালে A: ০.৫৬৯ ÷ (০.৫৬৯ + ০.৩৫৩ × ০.৬৭) ≈ ০.৭১; নদীচর: ০.৩৪২ ÷ (০.৩৪২ + ০.৬১১ × ০.৬৭) ≈ ০.৪৬. টেবিলের আসল সংখ্যা ০.৬৯৩ আর ০.৪৫৭. অর্থাৎ ভুলের হার সমান করার পর “অনুমোদিতদের কত ভাগ খাতায় সময়মতো” দলভেদে প্রায় ২৪ শতাংশ-বিন্দু আলাদা; দোকানের চোখে নদীচরের অনুমোদন কম নিরাপদ দেখায়। Baseline-এও PPV আলাদা ছিল (০.৭৪২ বনাম ০.৫৬৩), আর প্রতিকারের পর ফাঁকটি বেড়েছে, কমেনি। সমান PPV দলের ভেতরে calibration-এর ঘনিষ্ঠ আত্মীয়; base rate আলাদা আর model নিখুঁত না হলে সেটি আর equalized odds একসঙ্গে মেলে না।

Pleiss ও সহকর্মীরা (NeurIPS ২০১৭) আরও এক ধাপ দেখান: দলের ভেতরে calibration রাখলে বড়জোর একটি ভুলের হার (ধরুন শুধু FNR) মেলানো যায়, আর সেটিও প্রায়ই এক দলের কিছু সিদ্ধান্ত এলোমেলো করে। তাই প্রশ্ন “সব fairness পাব কীভাবে” নয়, বরং “কোনটি ছাড়ছি, কেন, আর কে মেনে নিলেন”। পর্ব ২৬-এর calibration-এর কাজ এখানে দলভিত্তিক হয়ে যায়: reliability diagram প্রতিটি দলের জন্য আলাদা আঁকুন।

৮ · সীমা — খারাপ label, ক্ষতিকর উদ্দেশ্য, আইন, নথি আর অংশীজন

ভুল label সারানো metric-এর কাজ নয়। এই পর্বের প্রতিটি প্রতিকার খাতার label-এর সঙ্গে ফাঁক মেলায়। এখানে বিকৃতি ঠিক দল-অনুযায়ী ছিল বলে আসল ফলেও ফাঁক কমেছে, কিন্তু সেটি ভাগ্য। Obermeyer ও সহকর্মীরা ২০১৯ সালে (Science) যুক্তরাষ্ট্রের একটি স্বাস্থ্য-ঝুঁকির algorithm বিশ্লেষণ করে দেখান, “ভবিষ্যৎ চিকিৎসা-খরচ” label হিসেবে “অসুস্থতা”-র proxy হওয়ায় একই ঝুঁকি-score-এ কৃষ্ণাঙ্গ রোগীরা গড়ে বেশি অসুস্থ ছিলেন; label বদলালে পক্ষপাতের বড় অংশ কমে বলে তাঁরা জানান। বাজারঘরে প্রকৃত সমাধান তেমনই: courier-এর জমার তারিখ নয়, গ্রাহকের পাঠানোর সময় দিয়ে “দেরি” মাপা (পর্ব ০৫-এর label-এর মান)।

উদ্দেশ্য ক্ষতিকর হলে fairness অর্থহীন। যে model গ্রাহককে বেশি ঋণের ফাঁদে ফেলতে বানানো, তার TPR সব দলে সমান হলেও ক্ষতি সমানভাবে ভাগ হয় মাত্র। Selbst ও সহকর্মীরা (২০১৯) একে বলেন সমস্যাকে তার সামাজিক প্রেক্ষাপট থেকে বিচ্ছিন্ন করার ফাঁদ। কখনো সৎ উত্তর হলো model না চালানো।

Feedback loop. যাঁরা বাকি পান না, তাঁদের নতুন label আসে না (selective labels), তাই আজকের ফাঁক কালকের খাতায় জমে (feedback loop). একটি ছোট, পরিকল্পিত অংশকে নিয়মের বাইরে অনুমোদন দিয়ে ফল দেখা একটি পরিচিত কৌশল, তবে সেটির নিজের ঝুঁকি ও খরচ আছে।

আইন আর নীতি। অনেক দেশে বৈষম্যবিরোধী, ঋণ আর তথ্য-সুরক্ষার আইন আছে, আর কিছু জায়গায় protected attribute সরাসরি সিদ্ধান্তে ব্যবহারে কড়া নিষেধ আছে বলে আইন-গবেষকেরা আলোচনা করেন। তাই post-processing-এর দলভিত্তিক সীমা কোথাও নিষিদ্ধ, কোথাও প্রতিকার হিসেবে গ্রহণযোগ্য হতে পারে। এই পর্ব কোনো দেশের আইনের ব্যাখ্যা নয়; বাস্তব কাজে স্থানীয় আইনি পরামর্শ নিন, আর সিদ্ধান্তের কারণ লিখে রাখুন। ঝুঁকিভিত্তিক AI নিয়ন্ত্রণ নিয়ে বিস্তারিত পরের AI ethics কোর্সে আসবে।

নথি আর অংশীজন। Model card-এ (আর ডেটার জন্য datasheet) লিখুন: কোন দল কেন মাপা হলো, দলের তথ্য কীভাবে সংগ্রহ ও সুরক্ষিত, কোন metric আর কোন সীমা, প্রতিটি দলের n আর CI, কোন প্রতিকার আর তার দাম (accuracy, ভালো দলের ফল), আর আপিলের পথ। কোন ক্ষতি জরুরি, তা ঠিক করায় ক্ষতিগ্রস্ত গ্রাহকদের প্রতিনিধি, ঝুঁকি-দল আর আইনি দলকে ডাকুন; এটি একা data scientist-এর সিদ্ধান্ত নয়।

আরও গভীরে: Kearns, Neel, Roth আর Wu (ICML ২০১৮) দেখান, কয়েকটি বড় দলে ফাঁক মেলালেও তাদের মিলনের অসংখ্য উপদলে ফাঁক লুকিয়ে থাকতে পারে (“fairness gerrymandering”), আর উপদলের বড় পরিবারের ওপর শর্ত দেওয়ার পদ্ধতি দেন। Hébert-Johnson ও সহকর্মীদের (২০১৮) multicalibration একই ভাবনায় calibration-কে অনেক উপদলে চায়। Post-processing-এ ভালো দলের ফল নামিয়ে ফাঁক মেলানোকে Mittelstadt, Wachter আর Russell ২০২৩ সালের একটি প্রবন্ধে “levelling down” বলে সমালোচনা করেন। আর Corbett-Davies ও সহকর্মীদের পর্যালোচনা (২০১৭ ও পরে) যুক্তি দেন, অনেক ক্ষেত্রে সবার জন্য এক সীমাই বেশি ন্যায্য; বিতর্কটি এখনও খোলা।

পরীক্ষা ও ইন্টারভিউতে: এই ধরনের প্রশ্ন বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই আসে।

  • n = ১০০, দল B ৪০ জন যাদের ১০ জনের label ১, মোট label ১ = ৫০. w(B, ১)? ০.৪ × ০.৫ ÷ ০.১ = ২.০.
  • Pre, in আর post-processing-এর মধ্যে কোনটিতে চালুর সময় protected attribute লাগে? Post-processing.
  • একক সীমায় TPR আর FPR একসঙ্গে মেলে না কেন, আর সমাধান? খারাপ দলের বিন্দু ভালো দলের ROC-র নিচে; দুটি সীমার মাঝে randomise করুন।
  • Base rate ০.৬ আর ০.৩, দুই দলে TPR ০.৮ আর FPR ০.২. PPV? ০.৪৮/০.৫৬ = ০.৮৫৭ আর ০.২৪/০.৩৮ = ০.৬৩২: সমান নয়।
  • ৪০ জনের দলে ফাঁকের CI [০.০৩, ০.৩৩] মানে? ফাঁক সম্ভবত ধনাত্মক, কিন্তু আকার প্রায় অজানা; আরও ডেটা লাগে।

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

“Model-টা fair করে দাও” বললে AI assistant দ্রুত কোড দেয়, আর ভুলগুলো প্রায়ই চোখে পড়ে না:

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
একটি ফাঁকের সংখ্যা দিয়ে “bias নেই” বা “bias আছে” রায়ছোট দলে CI বিশালপ্রতিটি দলের n আর bootstrap CI চান
Protected attribute-কে feature বানায়, বা মুছে দিয়ে “fair” বলেপ্রথমটি সরাসরি ব্যবহার; দ্বিতীয়টিতে proxy থেকে যায়অডিটের কলাম আলাদা রাখুন; proxy আর ফাঁক মাপুন
পুরোনো বা বানানো API: fairlearn.postprocessing-এ অচেনা arguments, ExponentiatedGradient-এ ভুল নামVersion বদলালে API বদলায়; বানানো parameter চুপচাপ ভুল চলে বা error দেয়নিজের version-এর docs; ছোট উদাহরণে চালিয়ে দেখুন
Test set-এ λ বা সীমা টিউন করে সেখানেই ফল রিপোর্টআশাবাদী ফল; test আর না-ছোঁয়া নয়validation-এ বাছাই, test একবার
শুধু accuracy আর একটি fairness metric দেখায়অন্য metric উল্টো দিকে খুলতে পারে, PPV বদলায়Selection, TPR, FPR, PPV সব দলে পাশাপাশি
খাতার label-কে সত্য ধরে নেয়Label bias থাকলে সব metric বিকৃতlabel কীভাবে তৈরি, তা আগে জানুন

প্রম্পটে ডেটার কাঠামো দিন, আসল গ্রাহকের ডেটা নয়; উত্তরের সঙ্গে যাচাইয়ের কোড চেয়ে নিন।

আমার একটি "পরে টাকা দিন" অনুমোদন model আছে (scikit-learn 1.8 LogisticRegression). একটি জেলা-দলের কলাম শুধু অডিটের জন্য আছে, model-এর input নয়। Label-এ একটি দলের বিরুদ্ধে courier-এর দেরির পক্ষপাত থাকতে পারে। ১. প্রতিটি দলের selection rate, TPR, FPR আর PPV, আর দলের মধ্যে ফাঁকের 95% bootstrap CI বের করার কোড দাও; প্রতিটি দলের n ছাপাও। ২. Reweighing (Kamiran–Calders) নিজে লিখে দাও, আর ওজনসহ দলভেদে label-হার সমান হলো কি না যাচাই করো। ৩. Fairlearn-এর বর্তমান version-এ ExponentiatedGradient আর EqualizedOdds-এর সঠিক ব্যবহার দেখাও; কোন parameter কোন version-এ আছে, docs-এর নাম বলো। ৪. λ বা সীমা কোথায় বাছব (validation, test নয়), আর test-এ একবার মাপার পুরো ক্রম লেখো। ৫. যেখানে তুমি নিশ্চিত নও, বা যেখানে আইনি পরামর্শ লাগবে, স্পষ্ট বলো।

AI-এর দেওয়া fairness সমাধান হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Intersectional দল কেন আলাদা করে মাপতে হয়, অথচ তার মাপ এত নড়বড়ে কেন?

Kimberlé Crenshaw ১৯৮৯ সালে যুক্তরাষ্ট্রের কয়েকটি কর্মসংস্থান-মামলা বিশ্লেষণ করে দেখান, আদালত “নারী” আর “কৃষ্ণাঙ্গ” দুটি দলকে আলাদা আলাদা দেখেছিল; কোম্পানি নারী নিয়োগ করেছে (সাদা নারী) আর কৃষ্ণাঙ্গ নিয়োগ করেছে (কৃষ্ণাঙ্গ পুরুষ), তাই দুই দলের হিসাবে বৈষম্য ধরা পড়েনি, অথচ কৃষ্ণাঙ্গ নারীরা বাদ পড়ছিলেন। এই ধারণার নাম তিনি দেন intersectionality.

Machine learning-এ এর সবচেয়ে পরিচিত উদাহরণ Joy Buolamwini আর Timnit Gebru-র ২০১৮ সালের “Gender Shades”: কয়েকটি বাণিজ্যিক মুখ-বিশ্লেষণ সেবার লিঙ্গ-শ্রেণিবিভাগের ভুল গাঢ় ত্বকের নারীদের মধ্যে সবচেয়ে বেশি ছিল বলে তাঁরা জানান, যা শুধু লিঙ্গ বা শুধু ত্বকের রং ধরে দেখলে এতটা স্পষ্ট হতো না। এটি quality-of-service ক্ষতির একটি পরিষ্কার উদাহরণ। পরে Kearns ও সহকর্মীরা (২০১৮) দেখান, কয়েকটি বড় দলে ফাঁক মেলানো model-ও উপদলের মিলনে বড় ফাঁক লুকাতে পারে।

কিন্তু পরিসংখ্যানের দাম আছে। দুটি বৈশিষ্ট্যের মিলনে দল ছোট হয়, তিনটিতে আরও ছোট; এই পর্বের ৯৪ জনের দলে TPR ফাঁকের CI চওড়ায় প্রায় ০.৩০, যেখানে পুরো নদীচরের ০.১১. অনেক উপদল একসঙ্গে দেখলে কিছু উপদলে শুধু এলোমেলোতায় বড় ফাঁক দেখা যাবে (একাধিক তুলনা)। তাই পথ তিনটি: কোন উপদল দেখবেন, তা ক্ষতি আর অংশীজনের পরামর্শে আগে ঠিক করা; প্রতিটির n আর CI সবসময় রিপোর্ট করা; আর ছোট উপদলের জন্য লক্ষ্যভেদী ডেটা সংগ্রহ বা দীর্ঘ সময় ধরে জমানো। Bayesian পদ্ধতিতে ছোট দলের অনুমান বড় দলের দিকে খানিক টেনে আনা (shrinkage) আরেকটি বিকল্প, তবে সেটিও একটি অনুমান।

মূল উপলব্ধি: সবচেয়ে ঝুঁকির মানুষেরা প্রায়ই সবচেয়ে ছোট উপদলে থাকেন; তাঁদের মাপতেই হবে, কিন্তু চওড়া CI-সহ, আর “ফাঁক দেখা যায়নি”-কে “ফাঁক নেই” না পড়ে।

প্র ০২Equal opportunity কেন demographic parity-র চেয়ে বেশি জনপ্রিয় হলো, আর কখন parity-ই ঠিক?

২০১২ সালের দিকে Dwork ও সহকর্মীদের “Fairness Through Awareness” প্রবন্ধ demographic parity-র (তাঁদের ভাষায় statistical parity) একটি দুর্বলতা দেখায়: সমান selection rate মেলানো যায় এক দলে যোগ্য মানুষ আর অন্য দলে এলোমেলো মানুষ বেছে, যা স্পষ্টতই অন্যায্য। Hardt, Price আর Srebro ২০১৬ সালে যুক্তি দেন, ঋণের মতো সিদ্ধান্তে আসল প্রশ্ন হলো যোগ্য মানুষের সুযোগ, তাই TPR সমান রাখা (equal opportunity) বা TPR আর FPR দুটিই (equalized odds) বেশি অর্থবহ। তাঁদের আরেকটি যুক্তি: নিখুঁত classifier (ŷ = y) সবসময় equalized odds মানে, কিন্তু base rate আলাদা হলে demographic parity মানে না; parity তাই কখনো কখনো নিখুঁত model-কেও “অন্যায্য” বলে।

তবে এই যুক্তির ভিত্তি একটি বড় অনুমান: y বিশ্বাসযোগ্য। এই পর্বের খাতায় নদীচরের সৎ গ্রাহকদের একটি অংশ “দেরি”, তাই খাতা ধরে TPR সমান করা মানে বিকৃত সংজ্ঞায় সমান সুযোগ। যখন label নিজেই অতীতের বৈষম্যের ফল (যেমন অতীতের নিয়োগ-সিদ্ধান্ত থেকে বানানো “ভালো কর্মী” label), তখন parity বা তার নরম রূপ একটি যুক্তিসংগত সতর্ক-সংকেত। আর কিছু সিদ্ধান্তে সুযোগের বণ্টনই মূল প্রশ্ন, যেমন বিজ্ঞাপন কাকে দেখানো হবে বা একটি ছোট প্রশিক্ষণ-বৃত্তি কোন এলাকায় যাবে।

এই পর্বের fairlearn run-এ দেখা গেছে, DemographicParity শর্তে selection প্রায় সমান হলেও TPR আর FPR-এর ফাঁক উল্টো দিকে খোলে। দুটি সংজ্ঞা দুটি ভিন্ন মূল্যবোধ মাপে, আর base rate আলাদা হলে দুটি একসঙ্গে মেলে না। তাই বাছাই প্রসঙ্গনির্ভর, আর সেটি লিখিতভাবে, ক্ষতিগ্রস্ত মানুষের মত নিয়ে করা উচিত।

মূল উপলব্ধি: Equal opportunity ধরে নেয় label সৎ; label-এর ওপর আস্থা যত কম, label ছাড়া মাপা যায় এমন selection rate তত গুরুত্বপূর্ণ সংকেত।

প্র ০৩COMPAS বিতর্কে দুই পক্ষই কেন “ঠিক” ছিল, আর এর সঙ্গে calibration-এর সম্পর্ক কী?

২০১৬ সালে ProPublica যুক্তরাষ্ট্রের একটি কাউন্টির ডেটায় COMPAS নামের পুনরায় অপরাধের ঝুঁকি-score বিশ্লেষণ করে জানায়: যাঁরা পরে অপরাধ করেননি, তাঁদের মধ্যে কৃষ্ণাঙ্গদের “উচ্চ ঝুঁকি” বলার হার (FPR) শ্বেতাঙ্গদের প্রায় দ্বিগুণ। নির্মাতা প্রতিষ্ঠান (তখন Northpointe) উত্তরে দেখায়, একই score-এ দুই দলের আসল পুনরায় অপরাধের হার প্রায় সমান, অর্থাৎ score দলের ভেতরে calibrated. দুই দাবিই মোটামুটি সংখ্যায় টেকে বলে পরে বিশ্লেষকেরা দেখান।

কেন? Kleinberg, Mullainathan আর Raghavan (২০১৬ সালে প্রকাশিত, ITCS ২০১৭) আর Chouldechova (২০১৭) প্রমাণ করেন, base rate আলাদা হলে আর model নিখুঁত না হলে calibration আর সমান FPR ও FNR একসঙ্গে সম্ভব নয়। এই পর্বের অংশ ৭-এ একই কথা নিজেদের সংখ্যায়: equalized odds মেলানোর পর base rate ০.৬৪৭ বনাম ০.৩৮৯ হওয়ায় PPV আলাদা হতে বাধ্য। Pleiss ও সহকর্মীরা (২০১৭) দেখান, calibration রেখে বড়জোর একটি ভুলের হার মেলানো যায়, আর তার জন্য এক দলের কিছু সিদ্ধান্ত এলোমেলো করতে হয়, যা প্রায়ই অগ্রহণযোগ্য।

বাজারঘরের দিক থেকে: দোকান চায় “০.৭ score মানে সব দলেই ৭০% সময়মতো দেন” (calibration), যাতে ঝুঁকির হিসাব সৎ থাকে। গ্রাহক চান “আমি সৎ হলে আমার বাদ পড়ার সম্ভাবনা দলের ওপর নির্ভর করবে না” (সমান FNR). দুটিই যুক্তিসংগত, আর base rate আলাদা হলে একসঙ্গে পাওয়া যায় না। কোনটিকে প্রাধান্য দেবেন, সেটি প্রযুক্তির নয়, মূল্যবোধ আর ক্ষমতার প্রশ্ন: কার ক্ষতিকে বেশি ওজন দেওয়া হচ্ছে।

মূল উপলব্ধি: অসম্ভাব্যতার ফল fairness-কে অসম্ভব বলে না; বলে প্রতিটি পছন্দের একটি দাম আছে, আর সেই দাম কে বহন করবেন, তা খোলাখুলি ঠিক করতে হয়।

প্র ০৪মুদ্রা ছুড়ে সিদ্ধান্ত (randomised সীমা) কি গ্রাহকের কাছে ন্যায্য মনে হবে? Levelling down নিয়ে বিতর্ক কী?

গাণিতিকভাবে randomised সীমা পরিষ্কার: দুটি সীমার মাঝে q সম্ভাবনায় বাছলে দলের প্রত্যাশিত TPR আর FPR দুটি ROC বিন্দুর মাঝের রেখায় বসে, তাই অন্য দলের বিন্দু ছোঁয়া যায়। Hardt ও সহকর্মীদের ২০১৬ সালের প্রবন্ধ আর fairlearn-এর ThresholdOptimizer দুটিই এমন এলোমেলো সিদ্ধান্ত ব্যবহার করে; ExponentiatedGradient-এর ফলও একটি এলোমেলো মিশ্রণ।

কিন্তু গ্রাহকের চোখে দেখুন। একই score-এর দুজন A-জেলার গ্রাহক, একজন অনুমোদন পেলেন, অন্যজন পেলেন না, কারণ মুদ্রা। “একই রকম মানুষের সঙ্গে একই আচরণ” (Dwork ও সহকর্মীদের individual fairness-এর মূল ভাবনা) এখানে ভাঙে। আপিলে কর্মকর্তা কী ব্যাখ্যা দেবেন? আবার আবেদন করলে কি ফল বদলাবে? অনেক প্রতিষ্ঠান তাই randomised নিয়ম এড়ায়, বা মুদ্রার বদলে একটি স্থির কিন্তু ব্যাখ্যাযোগ্য নিয়ম খোঁজে।

দ্বিতীয় প্রশ্ন levelling down. এই পর্বের post-processing-এ ফাঁক মিলেছে মূলত A-র সিদ্ধান্ত বদলে: A-র FPR বেড়ে নদীচরের সমান, আর accuracy ০.৬৮২ থেকে ০.৬২৮. কারও ফল ভালো না করে একটি দলের ফল খারাপ করে ফাঁক কমানোকে Mittelstadt, Wachter আর Russell ২০২৩ সালের একটি প্রবন্ধে সমালোচনা করেন: সমতা এসেছে, কিন্তু কারও উপকার হয়নি। এর বিপরীতে যুক্তি হলো, কিছু ক্ষেত্রে বেশি মানুষকে সুযোগ দেওয়াই লক্ষ্য। এখানে আসল সমাধান ছিল নদীচরের label ঠিক করা, যাতে তার ROC উন্নত হয় আর দুই দল ওপরের দিকে মেলে।

মূল উপলব্ধি: গড়ে ন্যায্য একটি নিয়ম ব্যক্তির কাছে খামখেয়ালি মনে হতে পারে; ফাঁক মেলানোর আগে জিজ্ঞেস করুন কোন দলের ফল উন্নত হলো, আর কারও হলো কি না।

প্র ০৫Fairness toolkit কি “fair AI” বানায়? Obermeyer-এর গবেষণা আর “abstraction trap” কী শেখায়?

২০১৮ সালের দিকে কয়েকটি খোলা লাইব্রেরি আসে: IBM-এর AIF360 (reweighing, prejudice remover, equalized odds post-processing একসঙ্গে), Microsoft-এর গবেষকদের শুরু করা Fairlearn (এখন সম্প্রদায় পরিচালিত), আর Google-এর What-If Tool. এগুলো মাপা সহজ করেছে, যা বড় অগ্রগতি। কিন্তু একটি ঝুঁকিও এনেছে: একটি ফাংশন চালিয়ে ফাঁক শূন্যে নামালে মনে হয় কাজ শেষ।

Obermeyer, Powers, Vogeli আর Mullainathan-এর ২০১৯ সালের Science প্রবন্ধ দেখায় কেন তা নয়। যুক্তরাষ্ট্রে ব্যাপকভাবে ব্যবহৃত একটি algorithm রোগীদের অতিরিক্ত যত্নের তালিকায় তুলত ভবিষ্যৎ চিকিৎসা-খরচের অনুমান দিয়ে। খরচ অসুস্থতার proxy হিসেবে ব্যবহৃত, কিন্তু চিকিৎসায় প্রবেশাধিকারের অসমতায় একই অসুস্থতায় কৃষ্ণাঙ্গ রোগীদের খরচ গড়ে কম ছিল বলে লেখকেরা জানান। ফলে একই score-এ তাঁরা বেশি অসুস্থ। খরচের label ধরে score হয়তো calibrated-ই ছিল; সমস্যা ছিল label-এর পছন্দে, যা কোনো fairness metric খাতা ধরে দেখতে পেত না। লেখকেরা অসুস্থতার সরাসরি মাপ দিয়ে label বদলে পক্ষপাতের বড় অংশ কমানোর কথা জানান।

Selbst, boyd, Friedler, Venkatasubramanian আর Vertesi (২০১৯) এমন ভুলগুলোকে কয়েকটি “abstraction trap” নামে সাজান: সমস্যাকে শুধু একটি classifier আর একটি metric বানানো, প্রেক্ষাপট বাদ দিয়ে এক জায়গার সমাধান অন্য জায়গায় বসানো, আর ধরে নেওয়া যে প্রযুক্তিই সমাধান। বাজারঘরের দিক থেকে প্রশ্নগুলো: “দেরি” কে কীভাবে লিখছে? “পরে টাকা দিন” কি গ্রাহককে সাহায্য করছে, না অপ্রয়োজনীয় ঋণে ফেলছে? যাঁরা বাদ পড়ছেন, তাঁদের কী বিকল্প আছে?

মূল উপলব্ধি: Toolkit মাপে আর মেলায়; কী মাপা উচিত, label সৎ কি না, আর কাজটি আদৌ করা উচিত কি না, তা মানুষের বিচার আর অংশীজনের অংশগ্রহণ ছাড়া ঠিক হয় না।

অনুশীলন

হিসাবগুলো খাতায় করুন, তারপর উত্তর মেলান। একটির জন্য কোড লেখা লাগবে।

  1. হাতের হিসাব: (ক) ১০০টি সারি: দল A-তে label ১ = ৩০, label ০ = ২০; দল B-তে label ১ = ১০, label ০ = ৪০. চারটি reweighing ওজন বের করুন আর যাচাই করুন। (খ) B-র লক্ষ্য বিন্দু (FPR ০.৩০, TPR ০.৭০)। A-র দুটি সীমা দেয় (০.১০, ০.৫০) আর (০.৫০, ০.৯০)। কোন সম্ভাবনা q-তে প্রথম সীমা বাছলে A ঠিক B-র বিন্দুতে পৌঁছায়? (গ) Base rate ০.৬ আর ০.৩, দুই দলেই TPR ০.৮ আর FPR ০.২ হলে PPV কত?

    (ক) P(A) = P(B) = ০.৫, P(label ১) = ৪০/১০০ = ০.৪. w(A,১) = ০.৫ × ০.৪ ÷ ০.৩০ = ০.৬৬৭; w(A,০) = ০.৫ × ০.৬ ÷ ০.২০ = ১.৫; w(B,১) = ০.৫ × ০.৪ ÷ ০.১০ = ২.০; w(B,০) = ০.৫ × ০.৬ ÷ ০.৪০ = ০.৭৫. যাচাই: A-তে ৩০ × ০.৬৬৭ = ২০ আর ২০ × ১.৫ = ৩০, হার ০.৪; B-তে ১০ × ২ = ২০ আর ৪০ × ০.৭৫ = ৩০, হার ০.৪. মোট ওজন ১০০.

    (খ) মিশ্রণ q·(০.১০, ০.৫০) + (১ − q)·(০.৫০, ০.৯০) = (০.৫০ − ০.৪০q, ০.৯০ − ০.৪০q). FPR ০.৩০ চাইলে q = ০.৫; তখন TPR = ০.৯০ − ০.২০ = ০.৭০, ঠিক মিলে যায়, কারণ লক্ষ্য বিন্দুটি দুই বিন্দুর সংযোগ রেখায়।

    (গ) PPV = π·TPR ÷ [π·TPR + (১ − π)·FPR]. প্রথম দল: ০.৪৮ ÷ (০.৪৮ + ০.০৮) = ০.৮৫৭. দ্বিতীয়: ০.২৪ ÷ (০.২৪ + ০.১৪) = ০.৬৩২. সমান ভুলের হার, অথচ “অনুমোদিত মানে কতটা নিরাপদ” দলভেদে আলাদা।

  2. সমালোচনা ও নকশা: একটি fintech দলের রিপোর্ট: “আমরা fairlearn দিয়ে জেলা অনুযায়ী DP difference মেপেছি: ০.০৪, তাই model fair. দশটি জেলার মধ্যে তিনটিতে ৫০-এর কম আবেদন ছিল, সেগুলো বাদ দিয়েছি। ThresholdOptimizer-এর সীমা test set-এ বাছা হয়েছে। Model card লাগবে না, কারণ model সরল।” কী কী সমস্যা, আর কীভাবে নতুন করে সাজাবেন?

    সমস্যা: (১) Metric বাছা হয়নি ক্ষতি থেকে; ঋণের মতো allocative সিদ্ধান্তে TPR আর FPR-ও দেখা দরকার, শুধু DP নয়। (২) কোনো CI নেই; ০.০৪-এর পরিসর অজানা। (৩) ছোট জেলা বাদ দেওয়ায় সবচেয়ে কম প্রতিনিধিত্বের মানুষ রিপোর্ট থেকে হারিয়েছেন। (৪) Test set-এ সীমা বাছা মানে আশাবাদী ফল। (৫) ThresholdOptimizer চালুর সময় জেলা চায়: আইনি আর গোপনীয়তার দিক আলোচনা হয়নি। (৬) Label-এর উৎস যাচাই নেই। (৭) Model card “সরল” model-এরও লাগে।

    নতুন নকশা: ক্ষতি আর অংশীজন চিহ্নিত করে metric আর গ্রহণযোগ্য ফাঁক লেখা; train/validation/test ভাগ; প্রতিটি জেলার selection, TPR, FPR, PPV, n আর bootstrap CI; ছোট জেলা চিহ্নিত রেখে বেশি ডেটার পরিকল্পনা; প্রতিকার (reweighing, penalty, সীমা) validation-এ তুলনা; test-এ একবার; model card-এ সব, আপিলের পথ আর deploy-এর পর মাসিক নজরদারি।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    g = np.array([0, 0, 0, 1, 1, 1, 1, 0])
    y = np.array([1, 1, 0, 0, 0, 1, 0, 1])
    w = np.array([(g == a).mean() * (y == c).mean() / ((g == a) & (y == c)).mean()
                  for a, c in zip(g, y)])
    print(w.round(3))
    print(round(np.average(y[g == 1], weights=w[g == 1]), 3), y[g == 1].mean())
    [0.667 0.667 2.    0.667 0.667 2.    0.667 0.667]
    0.5 0.25

    দুই দলেই ৪টি সারি, মোট label ১ = ৪ (P = ০.৫)। দল ০-এ label ১ তিনটি, ০ একটি; দল ১-এ উল্টো। তাই বেশি ভরা ঘরের ওজন ০.২৫/০.৩৭৫ = ০.৬৬৭, ফাঁকা ঘরের ০.২৫/০.১২৫ = ২. দল ১-এর ওজনসহ label-হার (২ ÷ ৪) = ০.৫, ওজন ছাড়া ০.২৫. আউটপুট একটি ডেস্কটপ run-এ যাচাই করা।

  4. কোড লিখুন: অংশ ৫-এর penalty-কে demographic parity-র জন্য বদলান: শুধু দুই দলের গড় অনুমিত সম্ভাবনার ফাঁকের বর্গ। λ = ০, ১০ আর ১০০-এ selection, TPR আর FPR-এর ফাঁক ছাপুন। কী আশা করেন?
    def dp_loss(b, lam):                     # demographic parity: শুধু দলের গড় p তুলনা
        p = 1 / (1 + np.exp(-Xtr @ b))
        ll = -np.mean(y_tr * np.log(p + 1e-12) + (1 - y_tr) * np.log(1 - p + 1e-12))
        return ll + lam * (p[g_tr == 0].mean() - p[g_tr == 1].mean()) ** 2
    
    b = np.zeros(5)
    for lam in [0, 10, 100]:
        b = minimize(dp_loss, b, args=(lam,), method="L-BFGS-B").x
        h = (Xte @ b >= 0).astype(int)
        print(lam, round(np.mean(h == y), 3), gaps(y, h, B).round(3))

    পাতার ব্লক ১–৩-এর পরে একটি ডেস্কটপ run-এ (scikit-learn 1.9.1, SciPy 1.18.1) আউটপুট:

    0 0.682 [0.364 0.314 0.273]
    10 0.659 [ 0.025 -0.048 -0.058]
    100 0.655 [ 0.001 -0.062 -0.092]

    λ = ১০০-এ selection-এর ফাঁক প্রায় শূন্য (০.০০১), কিন্তু TPR আর FPR-এর ফাঁক উল্টো দিকে খোলে (নদীচরের পক্ষে −০.০৬২ আর −০.০৯২): খাতায় নদীচরের label-হার কম, তাই সমান অনুমোদন মানে খাতা ধরে সেখানে “দেরি” বেশি জনকে বাকি দেওয়া। Equalized-odds penalty-র λ = ১০ (accuracy ০.৬৭০) এর চেয়ে বেশি accuracy রাখে। ঠিক দশমিক ভিন্ন যন্ত্রে সামান্য বদলাতে পারে।

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

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

ক্রমএকটি ঋণ-অনুমোদন model-এ fairness নিয়ে কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ক্ষতিগ্রস্ত হতে পারেন এমন দল আর অংশীজন চিহ্নিত করা
  2. ক্ষতি থেকে metric আর গ্রহণযোগ্য ফাঁক লিখে রাখা
  3. Baseline model-এর দলভিত্তিক মাপ, n আর CI-সহ
  4. প্রতিকারগুলো validation ডেটায় তুলনা করে একটি বাছা
  5. Test-এ একবার মেপে model card-এ লেখা আর deploy-এর পর নজরদারি

১০০০টি সারির মধ্যে দল B-তে ২০০টি, যার ৫০টির label ১; মোট label ১ = ৫০০. Kamiran–Calders reweighing-এ (দল B, label ১) ঘরের ওজন কত?

  • ২.০
  • ০.৫
  • ০.২৫
  • ১.০

একটি ছোট দলে (মাত্র ৪০ জনের label ১) TPR ফাঁকের অনুমান ০.১৮, আর ৯৫% bootstrap CI [০.০৩, ০.৩৩]. কোন সিদ্ধান্তটি সবচেয়ে যুক্তিসংগত?

  • ফাঁক ঠিক ০.১৮, তাই প্রতিকার সেই মাপেই করতে হবে
  • দল ছোট, তাই এটি রিপোর্ট থেকে বাদ দেওয়া উচিত
  • ফাঁক সম্ভবত ধনাত্মক, কিন্তু আকার অনিশ্চিত; n আর CI-সহ রিপোর্ট করে আরও ডেটা জোগাড় করা
  • CI চওড়া, তাই ফাঁক নেই বলে ধরা যায়

একটি ঋণ-অনুমোদন model-এ মূল উদ্বেগ: যাঁরা সময়মতো শোধ করবেন, এমন যোগ্য আবেদনকারী কোনো দলে বেশি প্রত্যাখ্যাত হচ্ছেন কি না। কোন fairness মাপ সবচেয়ে সরাসরি এই ক্ষতি ধরে?

  • দলভেদে selection rate-এর ফাঁক (demographic parity)
  • দলভেদে TPR-এর ফাঁক (equal opportunity)
  • দলভেদে PPV-এর ফাঁক
  • মোট accuracy

Equalized odds post-processing-এ একটি দলের জন্য দুটি সীমার মাঝে এলোমেলোভাবে বাছা (randomised threshold) হয় কেন?

  • Model-এর AUC বাড়ানোর জন্য
  • যাতে সিদ্ধান্তে দলের তথ্য না লাগে
  • যাতে দলের ভেতরে calibration রক্ষা পায়
  • একক সীমায় TPR আর FPR দুটি একসঙ্গে মেলে না; মিশ্রণে দুই ROC বিন্দুর মাঝের যেকোনো বিন্দু পাওয়া যায়

দুটি দলের base rate ০.৬৫ আর ০.৩৯. একটি classifier দুই দলেই TPR ০.৮৮ আর FPR ০.৬৭ দেয়। দুই দলের PPV সম্পর্কে কী বলা যায়?

  • সমান, কারণ TPR আর FPR সমান
  • যে দলের base rate বেশি, তার PPV কম
  • আলাদা: প্রায় ০.৭১ বনাম ০.৪৬
  • তথ্য যথেষ্ট নয়, বলা যায় না

Reweighing (pre-processing), fairness penalty (in-processing) আর দলভিত্তিক সীমা (post-processing): কোনটিতে চালু model-কে প্রতিটি নতুন সিদ্ধান্তের সময় আবেদনকারীর দল জানতে হয়?

  • শুধু দলভিত্তিক সীমা
  • শুধু reweighing
  • শুধু fairness penalty
  • তিনটিতেই

শব্দকোষ

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

মনে রাখুন

  1. ফাঁক একটি অনুমান: প্রতিটি দলের n আর bootstrap CI দিন; intersectional দল ছোট, তাই তার CI চওড়া, কিন্তু তাকে বাদ দেবেন না।
  2. Metric আসে ক্ষতি থেকে: allocative সিদ্ধান্তে TPR (আর দরকারে FPR) ফাঁক, সেবার মানে দলভেদে ভুলের মাপ; বাছাই আর গ্রহণযোগ্য ফাঁক লিখে রাখুন।
  3. Reweighing: w = P(দল)·P(label) ÷ P(দল, label) = প্রত্যাশিত ÷ দেখা; ওজনসহ দল আর label নিরপেক্ষ, দল লাগে শুধু training-এ।
  4. Penalty আর সীমা: log loss + λ·ফাঁক² থেকে accuracy–fairness curve; post-processing-এ দলভিত্তিক, দরকারে randomised সীমা, যা চালুর সময় দল চায় আর ভালো দলের ফল নামাতে পারে।
  5. সীমা মনে রাখুন: base rate আলাদা হলে equalized odds আর calibration (সমান PPV) একসঙ্গে মেলে না; আর ভুল label বা ক্ষতিকর উদ্দেশ্য কোনো metric সারায় না, তার জন্য ডেটা, নথি আর অংশীজন।

পরের পর্বে: মডিউল ১১ এখানে শেষ। পর্ব ৬১ (Data science-এর জন্য SQL — window function, cohort আর funnel) দিয়ে শুরু হবে মডিউল ১২, data science-এর কাজের ধারা।

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

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