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

ML coding ইন্টারভিউ — NumPy দিয়ে algorithm, pandas আর scikit-learn

Machine Learning Coding Interview — Algorithms from Scratch in NumPy, Leakage-Free pandas Tasks, scikit-learn Pipelines and Debugging Rounds

Taught in Bangla, this practice page works through 12 ML coding interview problems: linear and logistic regression by gradient descent, stable softmax with a gradient check, k-means, PCA by SVD, a decision-stump split, ROC AUC with ties, k-fold and bootstrap, point-in-time pandas features, cohorts and a leakage-free scikit-learn pipeline, each verified against a library in real in-page runs, plus debugging rounds.

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

  • NumPy-তে linear আর logistic regression, স্থির softmax ও cross-entropy, k-means আর SVD দিয়ে PCA লিখে library-র সাথে মেলানো
  • Decision stump-এর সেরা split, ties-সহ ROC AUC, k-fold splitter আর bootstrap confidence interval নিজে লেখা
  • pandas-এ ভবিষ্যৎ না দেখে সময়ের জানালার feature, sessionisation আর cohort টেবিল বানানো
  • scikit-learn-এ ColumnTransformer আর নিজের transformer দিয়ে leakage-মুক্ত pipeline, সঠিক cross-validation-সহ
  • Shape-এর বাগ, ভুল metric আর leak খুঁজে বের করা; সময় ভাগ করা আর কোড লিখতে লিখতে কথা বলা

এই পাতার প্রশ্ন সম্পর্কে: সমস্যাগুলো এই কোর্সের পর্ব ২ থেকে ৭২-এর বিষয় থেকে সাজানো। কোনোটি কোনো নির্দিষ্ট প্রতিষ্ঠান, বিশ্ববিদ্যালয় বা নিয়োগ পরীক্ষা থেকে নেওয়া নয়। এ ধরনের কাজ data scientist, ML engineer আর data analyst-এর coding দফায় প্রায়ই আসে, তবে আপনার ইন্টারভিউ ভিন্ন হতে পারে; বিশ্ববিদ্যালয়ের ল্যাব পরীক্ষায়ও একই দক্ষতা লাগে। ডেটা বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান), seed দিয়ে বানানো; একটি ব্লকে scikit-learn-এর সাথে আসা Wine ডেটাসেট। পাতার ব্লক চলে Python 3.14.2, NumPy 2.4.6, pandas 3.0.2 আর scikit-learn 1.8.0-এ। প্রথমবার চালালে package নামতে কয়েক সেকেন্ড লাগে, scikit-learn-এ প্রায় ১০ সেকেন্ড; পরের run দ্রুত।

নতুন হলে আগে: NumPy আর pandas-এর মৌলিক ইন্টারভিউ প্রশ্ন (view বনাম copy, dtype, merge, groupby) আছে AI-এর ভিত্তি কোর্সের পর্ব ৭৮-এ, আর মৌলিক ধারণার প্রশ্ন সেই কোর্সের পর্ব ৭৬ (MCQ) ও পর্ব ৭৭ (লিখিত ও ভাইভা)-এ। এই পাতা সেখান থেকে এক ধাপ ওপরে শুরু করে।

রান্নার প্রতিযোগিতায় বিচারক শুধু শেষ প্লেট চেখে দেখেন না; তিনি দেখেন মসলা কখন দিলেন, চুলার আঁচ কখন কমালেন, আর মাঝপথে একবার চেখে নিলেন কি না। ML coding ইন্টারভিউও তেমন। “Logistic regression লিখুন” শুনে আসল পরীক্ষা হয় তিন জায়গায়: আপনি shape আর গণিত মুখে বলতে পারেন কি না, নিজের কোড কোনো বিশ্বাসযোগ্য উৎসের সাথে মিলিয়ে দেখেন কি না, আর সময় ফুরানোর আগে একটি চলমান, যাচাই করা সমাধানে পৌঁছান কি না।

১ · ML coding ইন্টারভিউ কীভাবে চলে — সময় ভাগ আর কথা বলার ছক

রিভিশন: পর্ব ৪ · পুনরুৎপাদনযোগ্য experiment, পর্ব ২ · সমস্যা থেকে ML task, AI-এর ভিত্তি কোর্সের পর্ব ১১ · পরিচ্ছন্ন কোড আর ছোট test

ML coding দফা সাধারণত তিন রকমের প্রশ্ন দেয়: একটি algorithm library ছাড়া লেখা (k-means, logistic regression, AUC), একটি ডেটার কাজ pandas-এ (সময়ের feature, cohort), আর একটি ভাঙা কোড ঠিক করা। প্রতিষ্ঠান ভেদে সময় আর নিয়ম আলাদা; নিচের ছবিটি ৪৫ মিনিটের একটি সাধারণ ভাগ, কোনো নির্দিষ্ট প্রতিষ্ঠানের নিয়ম নয়।

৪৫ মিনিটের একটি from-scratch সমস্যার সময় ভাগ

Time-boxing: clarify, tiny example, simplest correct code, verify, complexity

৪৫ মিনিটের সময় ভাগ 0510303845 কোড ০–৫ মিনিট: প্রশ্ন, ইনপুট-আউটপুটের shape ৫–১০: হাতে ছোট উদাহরণ, পরিকল্পনা জোরে ১০–৩০: সরল সঠিক রূপ, ধাপে ধাপে চালানো ৩০–৩৮: যাচাই (library বা হাতের হিসাব), edge case ৩৮–৪৫: জটিলতা, বড় ডেটায় কী বদলাবেন নিয়ম: ৩০ মিনিটে না চললে সরল রূপে ফিরুন, vectorization পরে
ছবি ৭৬.১ — সবুজ অংশটিই সবচেয়ে বড়, কিন্তু যাচাইয়ের কমলা অংশ বাদ দিলে চলমান কোডও প্রমাণহীন থাকে।

কোড লিখতে লিখতে কথা বলার ছক

Live coding-এ ইন্টারভিউয়ার আপনার চিন্তার পথ শোনেন, তাই think-aloud এখানে মূল দক্ষতা। ML-এর কোডে একটি বাড়তি অভ্যাস যোগ করুন: প্রতিটি array-এর shape মুখে বলা। “X হলো (n, d), w হলো (d,), তাই X @ w হবে (n,)” — এই এক বাক্য অর্ধেক বাগ আগেই আটকে দেয়। আর time-boxing, মানে প্রতিটি ধাপের জন্য আগে থেকে সময় বেঁধে রাখা, আপনাকে নিখুঁত vectorization-এর পেছনে পুরো সময় খরচ করা থেকে বাঁচায়।

ধাপযা বলবেন বা করবেনউদাহরণ বাক্য
১ · চাহিদা আর shapeইনপুটের ধরন, label-এর মান, ফাঁকা ঘর, ফল কোন আকারে“y কি ০/১, নাকি −১/+১? X-এ NaN থাকতে পারে?”
২ · গণিত এক লাইনেযে সূত্র কোড করবেন তা বোর্ডে লেখা“Gradient হবে Xᵀ(p − y)/n.”
৩ · সরল সঠিক রূপআগে loop চললেও চলবে, পরে vectorize“প্রথমে সব জোড়া গুনে AUC, পরে rank দিয়ে দ্রুত।”
৪ · যাচাইlibrary, হাতের ছোট উদাহরণ, invariant, gradient check“loss প্রতিটি epoch-এ কমছে কি না দেখি।”
৫ · জটিলতাসময় আর memory, n আর d বাড়লে“প্রতি iteration O(nkd), memory O(nk).”
৬ · সীমা আর উন্নতিnumerical stability, বড় ডেটা, কী বাদ রইল“১ কোটি সারি হলে mini-batch আর partial_fit.”
প্র ১“NumPy দিয়ে k-means লিখুন।” কোড শুরুর আগে কী কী জিজ্ঞেস করবেন? (সহজ)

ভালো উত্তর: চারটি প্রশ্ন। k কি দেওয়া, নাকি বাছতে হবে? শুরুর কেন্দ্র কীভাবে বাছব: এলোমেলো, নাকি k-means++? কোনো দল খালি হলে কী করব? থামার শর্ত কী: কেন্দ্র আর না সরলে, নাকি সর্বোচ্চ কয়টি iteration? তারপর shape বলুন: “X (n, d), কেন্দ্র (k, d), দূরত্বের টেবিল (n, k).” শেষে যাচাইয়ের পরিকল্পনা: “একই শুরু দিলে SciPy-র ফলের সাথে মেলাব, আর inertia প্রতি ধাপে কমছে কি না দেখব।”

দুর্বল উত্তর: সঙ্গে সঙ্গে from sklearn.cluster import KMeans লেখা। প্রশ্নটাই ছিল library ছাড়া লেখা।

প্র ২২৫ মিনিট চলে গেছে, vectorised রূপ এখনো ভুল ফল দিচ্ছে। কী করবেন? (মাঝারি)

ভালো উত্তর: জোরে বলুন, “সময় বাঁচাতে আগে loop-এর সরল রূপে ফিরছি, যা আমি নিশ্চিতভাবে ঠিক লিখতে পারি।” তারপর ছোট ইনপুটে (৩–৫ সারি) দুই রূপের মধ্যবর্তী array-এর shape আর মান মিলিয়ে দেখুন কোথায় আলাদা হয়। প্রায়ই কারণ একটি ভুল axis বা broadcasting-এ (n,) আর (n, 1)-এর মিশ্রণ (অংশ ৯-এর বাগ ১)। সময় না থাকলে সরল রূপ জমা দিয়ে বলুন কোন লাইন vectorize করতেন আর কেন দ্রুত হতো।

দুর্বল উত্তর: চুপচাপ এলোমেলো axis বদলে বারবার চালানো। ইন্টারভিউয়ার বোঝেন না আপনি কী অনুমান যাচাই করছেন।

প্র ৩Library নেই। নিজের লেখা algorithm ঠিক কি না কীভাবে প্রমাণ করবেন? (মাঝারি)

ভালো উত্তর: চারটি স্তর। (১) হাতে হিসাব করা ছোট উদাহরণ, যেমন তিনটি বিন্দুতে একটি gradient ধাপ (অংশ ২-এর stepper). (২) Invariant: k-means-এ inertia কখনো বাড়ে না, softmax-এর প্রতিটি সারির যোগফল ১, AUC সবসময় ০ থেকে ১-এর মধ্যে। (৩) Gradient check: finite difference দিয়ে সংখ্যায় gradient বের করে বিশ্লেষণী gradient-এর সাথে মেলানো (অংশ ৩)। (৪) সরল কিন্তু ধীর দ্বিতীয় রূপ, যেমন AUC-এর জন্য সব জোড়া গোনা (অংশ ৫)। Library থাকলে তার সাথে মেলানো পঞ্চম স্তর, প্রথম নয়।

দুর্বল উত্তর: “Output দেখে ঠিকঠাক মনে হচ্ছে।” বিশ্বাসযোগ্য দেখতে ভুল সংখ্যাই ML কোডের সবচেয়ে সাধারণ বাগ।

২ · NumPy-তে linear আর logistic regression — gradient descent হাতে

রিভিশন: পর্ব ১৩ · gradient descent-এর পরিবার, পর্ব ৭ · least squares, পর্ব ১৮ · MLE, পর্ব ১১ · matrix calculus, AI-এর ভিত্তি কোর্সের পর্ব ৫৯

সমস্যা ১ · Gradient descent দিয়ে linear regression

প্রশ্ন: “বাজারঘরের ২০০টি ডেলিভারি: দূরত্ব (km), ওজন (kg) আর সময় (ঘণ্টা)। NumPy দিয়ে gradient descent-এ linear regression fit করুন, intercept-সহ।” শর্ত: scikit-learn নয়; ফল মূল এককে।

যা জিজ্ঞেস করবেন: কয়টি epoch, learning rate কি দেওয়া? Feature scale করা আছে? Closed-form সমাধানের সাথে মেলানো চলবে কি?

ইঙ্গিত: দূরত্ব ১–৩০, ওজন ০.২–৮; মাপ আলাদা হলে একটি learning rate দুই দিকেই ভালো চলে না। আগে standardize, শেষে weight মূল এককে ফেরত।

পরিকল্পনা জোরে বলুন: “Loss ধরি (1/2n)·Σ(Xw + b − y)²; gradient Xᵀ(err)/n আর err-এর গড়। Z = (X − μ)/σ-তে শিখে শেষে w/σ আর b − (w/σ)·μ। যাচাই np.linalg.lstsq-এর least squares সমাধানের সাথে।”

import numpy as np
rng = np.random.default_rng(42)
n = 200
X = np.column_stack([rng.uniform(1, 30, n),      # দূরত্ব, km
                     rng.uniform(0.2, 8, n)])    # ওজন, kg
y = 2.0 + 0.8 * X[:, 0] + 1.5 * X[:, 1] + rng.normal(0, 2, n)   # ডেলিভারির ঘণ্টা

def fit_linear_gd(X, y, lr=0.1, epochs=2000):
    mu, sd = X.mean(axis=0), X.std(axis=0)
    Z = (X - mu) / sd                   # scale না করলে একটি lr সব দিকে চলে না
    w, b = np.zeros(Z.shape[1]), 0.0
    for _ in range(epochs):
        err = Z @ w + b - y             # shape (n,), (n, 1) নয়
        w -= lr * (Z.T @ err) / len(y)  # (1/2n)·Σerr²-এর gradient
        b -= lr * err.mean()
    return w / sd, b - (w / sd) @ mu    # মূল এককে ফেরত

w, b = fit_linear_gd(X, y)
A = np.column_stack([np.ones(n), X])
ref, *_ = np.linalg.lstsq(A, y, rcond=None)
print("GD    b, w:", np.round([b, *w], 4))
print("lstsq b, w:", np.round(ref, 4))
assert np.allclose([b, *w], ref, atol=1e-6)
print("max |diff| < 1e-6:", np.abs(np.r_[b, w] - ref).max() < 1e-6)

সত্যিকারের আউটপুট:

GD    b, w: [1.5828 0.8278 1.4723]
lstsq b, w: [1.5828 0.8278 1.4723]
max |diff| < 1e-6: True

২০০০ epoch-এ gradient descent আর least squares-এর উত্তর চার দশমিক পর্যন্ত এক, আর পার্থক্য 10⁻⁶-এর চেয়ে ছোট। লক্ষ করুন, ডেটা বানানো হয়েছিল ২.০, ০.৮ আর ১.৫ দিয়ে, কিন্তু উত্তর ১.৫৮, ০.৮৩, ১.৪৭: noise-সহ ২০০টি সারি থেকে এটুকু সরে যাওয়া স্বাভাবিক। ইন্টারভিউতে “সত্যিকারের মান” নয়, একই loss-এর সঠিক সমাধানের সাথে মেলানোই যাচাই।

প্র ৪ফলো-আপ: জটিলতা কত? কখন normal equation, কখন gradient descent? (মাঝারি)

উত্তর: প্রতিটি epoch-এ Z @ w আর Z.T @ err: O(nd), মোট O(T·n·d), memory O(nd). Normal equation বা QR দিয়ে সরাসরি সমাধান O(nd² + d³): d কয়েক হাজারের কম হলে এটিই দ্রুত আর নির্ভুল, যদিও XᵀX বানালে condition number বর্গ হয়ে যায়, তাই lstsq (SVD-ভিত্তিক) নিরাপদ (পর্ব ৭)। d খুব বড় হলে, ডেটা memory-তে না ধরলে, বা নতুন ডেটা আসতেই থাকলে mini-batch বা SGD (পর্ব ১৩, পর্ব ৭০)। Ridge যোগ করতে gradient-এ শুধু λw যোগ হয়, b-তে নয়।

প্র ৫ওয়ার্ম-আপ: নিজের gradient descent-এ loss প্রতি epoch-এ বাড়ছে। প্রথম তিনটি সন্দেহ কী? (সহজ)

ভালো উত্তর: (১) Learning rate বেশি বড়: প্রতিটি ধাপ খাদের অন্য পাড়ে লাফিয়ে আরও ওপরে ওঠে। প্রমাণ: lr দশ ভাগের এক ভাগ করলে loss কমতে শুরু করে কি না। (২) চিহ্নের ভুল: w += lr * grad লিখলে আপনি পাহাড় বেয়ে উঠছেন, নামছেন না। (৩) Feature-এর মাপ খুব আলাদা: এক দিকে খাড়া, আরেক দিকে সমতল, তাই এক lr দুই দিকে চলে না; standardize করুন (পর্ব ১৩)। চতুর্থ সন্দেহ shape: gradient-এর shape w-এর সমান কি না একটি assert-এ দেখুন।

দুর্বল উত্তর: “Epoch বাড়িয়ে দেখি।” Loss বাড়তে থাকলে বেশি epoch শুধু দ্রুত nan-এ পৌঁছায়।

সমস্যা ২ · Gradient descent দিয়ে logistic regression, স্থির loss-সহ

প্রশ্ন: “৩০০ জন ক্রেতার দুটি scaled feature আর churn label (০/১)। NumPy-তে logistic regression লিখুন, log loss ছাপুন, আর scikit-learn-এর penalty-ছাড়া fit-এর সাথে মেলান।”

যা জিজ্ঞেস করবেন: Regularization লাগবে কি? Label কি ০/১? ডেটা কি পুরো আলাদা করা যায় (separable)? তাহলে penalty ছাড়া weight অসীমের দিকে যায়।

ইঙ্গিত: 1/(1 + exp(−z)) বড় ঋণাত্মক z-এ overflow করে। একই sigmoid লেখা যায় 0.5·(1 + tanh(z/2)) হিসেবে, আর loss logaddexp(0, z) − y·z.

পরিকল্পনা জোরে বলুন: “Negative log-likelihood-এর gradient linear regression-এর মতোই দেখতে: Xᵀ(p − y)/n, শুধু ভবিষ্যদ্বাণী এখন p = σ(Xw + b). scikit-learn-এ C খুব বড় দিলে penalty কার্যত শূন্য।”

import numpy as np
from sklearn.linear_model import LogisticRegression
rng = np.random.default_rng(1)
n = 300
X = np.column_stack([rng.normal(0, 1, n), rng.normal(0, 1, n)])  # দিন থেকে শেষ অর্ডার, অভিযোগ (scaled)
z_true = -0.5 + 1.2 * X[:, 0] + 0.7 * X[:, 1]
y = (rng.random(n) < 1 / (1 + np.exp(-z_true))).astype(int)     # churn = 1

def sigmoid(z):
    return 0.5 * (1 + np.tanh(z / 2))       # exp-এর overflow ছাড়া একই ফল

def fit_logreg_gd(X, y, lr=0.5, epochs=3000):
    w, b = np.zeros(X.shape[1]), 0.0
    for _ in range(epochs):
        p = sigmoid(X @ w + b)
        w -= lr * X.T @ (p - y) / len(y)   # log loss-এর gradient: Xᵀ(p − y)/n
        b -= lr * (p - y).mean()
    z = X @ w + b
    loss = np.mean(np.logaddexp(0, z) - y * z)   # স্থির log loss
    return w, b, loss

w, b, loss = fit_logreg_gd(X, y)
sk = LogisticRegression(C=1e10, tol=1e-10, max_iter=1000).fit(X, y)  # C বিশাল = penalty নেই
print("GD      b, w:", np.round([b, *w], 4), "| log loss:", round(loss, 4))
print("sklearn b, w:", np.round([sk.intercept_[0], *sk.coef_[0]], 4))
assert np.allclose([b, *w], [sk.intercept_[0], *sk.coef_[0]], atol=1e-4)

সত্যিকারের আউটপুট:

GD      b, w: [-0.5192  1.1314  0.2522] | log loss: 0.5617
sklearn b, w: [-0.5192  1.1314  0.2522]

নিজের লেখা আর scikit-learn-এর maximum likelihood উত্তর চার দশমিক পর্যন্ত এক। এই পাতার scikit-learn 1.8.0-এ penalty বাদ দিতে C=1e10 লেখা হয়েছে; penalty=None এই version-এ একটি FutureWarning দেয় (কোর্সের আগের পর্বগুলোতে দেখা)। দ্বিতীয় feature-এর আসল সহগ ০.৭ হলেও উত্তর ০.২৫: ৩০০টি noisy label থেকে অনুমানের অনিশ্চয়তা বড়, যা পর্ব ২১-এর bootstrap দিয়ে মাপা যায়। নিচের stepper তিনটি বিন্দুতে একটি ধাপ হাতে দেখায়।

x = [−1, 0, 2], y = [0, 1, 1], শুরু w = 0, b = 0
ধাপ
মান
log loss
  1. শুরু. w = 0, b = 0, তাই তিনটি বিন্দুরই z = 0.
  2. সম্ভাবনা. σ(0) = 0.5, সব বিন্দুতে। গড় log loss ln 2 ≈ 0.6931, মুদ্রা ছোড়ার সমান।
  3. ভুলের মাপ. p − y = [0.5, −0.5, −0.5]: প্রথম বিন্দুতে বেশি বলেছি, বাকি দুটিতে কম।
  4. Gradient. ∂w = Σx(p − y)/3 = (−0.5 + 0 − 1)/3 = −0.5; ∂b = (0.5 − 0.5 − 0.5)/3 ≈ −0.1667.
  5. Update. w = 0 − 0.5 × (−0.5) = 0.25, b = 0 − 0.5 × (−0.1667) ≈ 0.0833. নতুন loss 0.5697, আগের চেয়ে কম: ধাপটি ঠিক দিকে।
প্র ৬ফলো-আপ: ডেটা পুরো আলাদা করা গেলে কী হয়? Multiclass হলে কী বদলাবে? (কঠিন)

উত্তর: Perfect separation-এ penalty ছাড়া MLE নেই: w-এর মাপ যত বাড়ে loss তত শূন্যের কাছে যায়, তাই gradient descent চলতেই থাকে আর weight অসীমের দিকে বাড়ে (পর্ব ১৮)। প্রতিকার L2 penalty (λw যোগ), অর্থাৎ MAP (পর্ব ১৭), অথবা early stopping. Multiclass-এ w হয় (d, K), sigmoid-এর বদলে softmax, আর gradient Xᵀ(P − Y)/n, যেখানে Y one-hot (n, K); পরের সমস্যাটি সেটিই (পর্ব ২৫)। জটিলতা প্রতি epoch O(ndK).

৩ · Softmax আর cross-entropy — overflow ছাড়া, gradient check-সহ

রিভিশন: পর্ব ২৫ · softmax, পর্ব ৩৯ · log-sum-exp, পর্ব ১১ · gradient check, AI-এর ভিত্তি কোর্সের পর্ব ৩৮ · entropy

সমস্যা ৩ · স্থির softmax, cross-entropy আর তার gradient

প্রশ্ন: “Logit-এর একটি (n, K) matrix আর সঠিক class-এর index দেওয়া। Softmax, গড় cross-entropy আর logit-এর সাপেক্ষে তার gradient লিখুন। Logit ১০০০-এর মতো বড় হলেও যেন ভাঙে না।”

যা জিজ্ঞেস করবেন: Label কি index, নাকি one-hot? গড় loss, নাকি যোগ? Float32 নাকি float64?

ইঙ্গিত: Softmax-এ প্রতিটি সারি থেকে একই সংখ্যা বিয়োগ করলে ফল বদলায় না। সারির সর্বোচ্চ মান বিয়োগ করলে সবচেয়ে বড় exp হয় ১। এটিই log-sum-exp কৌশল।

import numpy as np
from scipy.special import logsumexp, softmax as sp_softmax
z = np.array([[1000., 1001., 1002.],     # বড় logit: naive exp() ভেঙে পড়ে
              [   1.,    2.,    3.]])
y = np.array([2, 0])                      # সঠিক class

def softmax_naive(z):
    e = np.exp(z)
    return e / e.sum(axis=1, keepdims=True)

def log_softmax(z):
    m = z.max(axis=1, keepdims=True)      # সারিপ্রতি সর্বোচ্চ বাদ: exp ≤ 1
    s = z - m
    return s - np.log(np.exp(s).sum(axis=1, keepdims=True))
def cross_entropy(z, y):
    return -log_softmax(z)[np.arange(len(y)), y].mean()
def ce_grad(z, y):                        # ∂CE/∂z = (softmax − one-hot)/n
    g = np.exp(log_softmax(z))
    g[np.arange(len(y)), y] -= 1
    return g / len(y)

print("naive :", softmax_naive(z)[0])
print("stable:", np.exp(log_softmax(z)).round(4).tolist())
print("CE    :", round(cross_entropy(z, y), 4))
ref = -(z - logsumexp(z, axis=1, keepdims=True))[np.arange(2), y].mean()
assert np.isclose(cross_entropy(z, y), ref)
assert np.allclose(np.exp(log_softmax(z)), sp_softmax(z, axis=1))
e = np.zeros_like(z); e[1, 2] = 1e-6      # finite difference দিয়ে gradient যাচাই
fd = (cross_entropy(z + e, y) - cross_entropy(z - e, y)) / 2e-6
print("grad[1,2]:", round(ce_grad(z, y)[1, 2], 6), "| finite diff:", round(fd, 6))

সত্যিকারের আউটপুট:

naive : [nan nan nan]
stable: [[0.09, 0.2447, 0.6652], [0.09, 0.2447, 0.6652]]
CE    : 1.4076
grad[1,2]: 0.33262 | finite diff: 0.33262

সরল রূপে exp(1000) হয় inf, আর inf/inf হয় nan: পুরো সারি নষ্ট। স্থির রূপে দুটি সারিই [0.09, 0.2447, 0.6652], কারণ softmax শুধু পার্থক্য দেখে, আর দুই সারির পার্থক্য একই (০, ১, ২)। Finite difference আর বিশ্লেষণী gradient ছয় দশমিক পর্যন্ত এক। এই পাতার ব্রাউজার-Python-এ nan আসে কোনো সতর্কবার্তা ছাড়াই, কারণ WebAssembly-তে floating-point exception flag নেই। ডেস্কটপে একই কোড সতর্ক করে; NumPy 2.4.6 আর Python 3.14-এ সত্যিকারের run (path ছোট করা):

~/ml-interview/softmax_ce.py:8: RuntimeWarning: overflow encountered in exp
  e = np.exp(z)
~/ml-interview/softmax_ce.py:9: RuntimeWarning: invalid value encountered in divide
  return e / e.sum(axis=1, keepdims=True)
naive : [nan nan nan]
log_softmax(z) = (z − m) − log Σ exp(z − m)
ধাপ
মান
overflow?
  1. সর্বোচ্চ মান. m = 1002. সরাসরি exp(1002) float64-এর সীমা (প্রায় 1.8 × 10³⁰⁸) পেরিয়ে inf হতো।
  2. সরানো. s = z − m = [−2, −1, 0]. Softmax বদলায় না, কারণ লব আর হর দুটিই e−m দিয়ে গুণ হয়।
  3. exp. [0.1353, 0.3679, 1]: সবচেয়ে বড়টি ঠিক ১, তাই overflow অসম্ভব।
  4. যোগফলের log. 0.1353 + 0.3679 + 1 = 1.5032; log 1.5032 ≈ 0.4076.
  5. Log-softmax. s − 0.4076 = [−2.4076, −1.4076, −0.4076]; exp করলে [0.09, 0.2447, 0.6652], যোগফল ১.
  6. Cross-entropy. সঠিক class ২, তাই CE = 0.4076. দ্বিতীয় সারিতে (z = [1, 2, 3], সঠিক class ০) CE = 2.4076; গড় (0.4076 + 2.4076)/2 = 1.4076, ব্লকের আউটপুটের সমান।
প্র ৭ফলো-আপ: softmax আর log আলাদা না করে একসাথে কেন? জটিলতা কত? (মাঝারি)

উত্তর: আলাদা করলে দুটি বিপদ। খুব ছোট সম্ভাবনা float-এ ০ হয়ে যায় (underflow), তখন log 0 = −inf আর loss inf. আর gradient-এর হিসাবে অকারণে ভাগ আসে। একসাথে লিখলে log-softmax সরাসরি স্থির, আর gradient-এর রূপও সরল: softmax − one-hot, ভাগ n দিয়ে। এজন্যই বেশিরভাগ library logit নেয়, সম্ভাবনা নয়; PyTorch-এর CrossEntropyLoss এর পরিচিত উদাহরণ। জটিলতা O(nK) সময় আর memory. আরেকটি ফাঁদ: numerical stability-র জন্য float32-এ এই কৌশল আরও জরুরি, কারণ সেখানে exp(89)-এর কাছেই overflow.

৪ · k-means আর PCA — library-র class ছাড়া

রিভিশন: পর্ব ৩৯ · GMM আর EM, পর্ব ৮ · eigen আর PCA, পর্ব ৯ · SVD, AI-এর ভিত্তি কোর্সের পর্ব ৬৫ · clustering আর PCA

সমস্যা ৪ · Lloyd-এর k-means, কয়েকটি শুরুসহ

প্রশ্ন: “বাজারঘরের ১৫০টি ঠিকানার (x, y) স্থানাঙ্ক। তিনটি ডেলিভারি হাব বসাতে k-means লিখুন। পাঁচবার আলাদা শুরু দিয়ে চালিয়ে সবচেয়ে কম inertia-র ফল রাখুন।”

যা জিজ্ঞেস করবেন: দূরত্ব কি সাধারণ Euclidean? খালি দল হলে কী? কয়টি শুরু?

ইঙ্গিত: দুই ধাপ পালাক্রমে: assign (প্রতিটি বিন্দু নিকটতম কেন্দ্রে) আর update (প্রতিটি দলের গড়)। দূরত্বের টেবিল broadcasting দিয়ে: X[:, None, :] − C[None, :, :], shape (n, k, d).

যাচাই: SciPy-র kmeans2-কে একই শুরুর কেন্দ্র দিলে সেটিও Lloyd-এর ধাপ চালায়; দুই ফল মিলতে হবে। scikit-learn-এর KMeans এই পাতার ব্রাউজার-Python-এ একটি threadpool সংক্রান্ত সতর্কবার্তা ছাপে, তাই এখানে SciPy.

import numpy as np
from scipy.cluster.vq import kmeans2
rng = np.random.default_rng(0)
true_c = np.array([[2, 2], [8, 3], [5, 8]])            # তিনটি এলাকার কেন্দ্র (km)
X = np.vstack([c + rng.normal(0, 1, (50, 2)) for c in true_c])

def kmeans(X, C0, max_iter=100):
    C = C0.copy()
    for it in range(1, max_iter + 1):
        d2 = ((X[:, None, :] - C[None, :, :]) ** 2).sum(axis=2)   # (n, k)
        lab = d2.argmin(axis=1)                                     # assignment
        newC = np.array([X[lab == j].mean(axis=0) if (lab == j).any() else C[j]
                         for j in range(len(C))])                   # update
        if np.allclose(newC, C):
            break
        C = newC
    return C, lab, d2[np.arange(len(X)), lab].sum(), it

best = None
for seed in range(5):                                   # n_init = 5: সেরা inertia রাখি
    C0 = X[np.random.default_rng(seed).choice(len(X), 3, replace=False)]
    C, lab, inertia, it = kmeans(X, C0)
    print(f"seed {seed}: inertia {inertia:7.2f}, iterations {it}")
    if best is None or inertia < best[2]:
        best = (C, lab, inertia, C0)
C, lab, inertia, C0 = best
C_sp, lab_sp = kmeans2(X, C0, minit="matrix", iter=100)  # একই শুরু, SciPy-র Lloyd
assert np.allclose(C, C_sp) and (lab == lab_sp).all()
print("centres:", np.round(C[np.argsort(C[:, 0])], 2).tolist())

সত্যিকারের আউটপুট:

seed 0: inertia  302.32, iterations 4
seed 1: inertia  302.32, iterations 4
seed 2: inertia  302.32, iterations 4
seed 3: inertia  302.32, iterations 4
seed 4: inertia 1143.55, iterations 5
centres: [[1.99, 2.18], [4.65, 8.08], [7.87, 3.03]]

চারটি শুরু একই উত্তরে পৌঁছেছে (inertia ৩০২.৩২), কিন্তু seed ৪ আটকে গেছে ১১৪৩.৫৫-এ: একটি local minimum, যেখানে সম্ভবত দুটি কেন্দ্র এক এলাকায় আর একটি কেন্দ্র দুই এলাকার মাঝে। এজন্যই একাধিক শুরু (scikit-learn-এর n_init) বা k-means++ লাগে। পাওয়া কেন্দ্র (১.৯৯, ২.১৮), (৪.৬৫, ৮.০৮), (৭.৮৭, ৩.০৩), ডেটা বানানোর আসল কেন্দ্রের কাছে। নিচের stepper এক মাত্রায় পুরো চক্র দেখায়।

শুরুর কেন্দ্র c₁ = 1, c₂ = 2 (প্রথম দুটি বিন্দু)
ধাপ
দল ১
দল ২
কেন্দ্র
inertia
  1. শুরু. দুটি কেন্দ্র পাশাপাশি, 1 আর 2: খারাপ শুরু, ইচ্ছে করেই।
  2. Assign ১. 1 যায় c₁-এ, বাকি সবাই c₂-এর কাছে। Inertia = 0 + 0 + 1 + 36 + 49 + 169 = 255.
  3. Update ১. c₁ = 1, c₂ = (2 + 3 + 8 + 9 + 15)/5 = 7.4.
  4. Assign ২. 2 আর 3 এখন c₁-এর কাছে (দূরত্ব 1 ও 2, বনাম 5.4 ও 4.4). Inertia কমে 65.68.
  5. Update ২. c₁ = (1 + 2 + 3)/3 = 2, c₂ = (8 + 9 + 15)/3 ≈ 10.67. Inertia 2 + 28.67 = 30.67.
  6. Assign ৩. কোনো বিন্দু দল বদলায় না, তাই কেন্দ্রও বদলাবে না: থামুন। প্রতিটি ধাপে inertia কমেছে বা সমান থেকেছে, এটিই invariant.
প্র ৮ফলো-আপ: জটিলতা কত? n = ১০ লাখ, d = ৫০, k = ১০০ হলে broadcasting-এর কী হবে? (কঠিন)

উত্তর: প্রতিটি iteration O(nkd) সময়। কিন্তু X[:, None, :] − C[None, :, :] মধ্যবর্তী (n, k, d) array বানায়: ১০⁶ × ১০০ × ৫০ = ৫ × ১০⁹ float64, প্রায় ৪০ GB. সমাধান: ‖x − c‖² = ‖x‖² − 2x·c + ‖c‖², যেখানে মাঝের অংশ X @ C.T, তাই memory O(nk). বিনিময়ে কাছাকাছি বিন্দুতে catastrophic cancellation-এর ঝুঁকি, তাই ছোট ঋণাত্মক মান ০-তে কাটা হয়। আরও বড় হলে ব্লক ধরে হিসাব বা mini-batch k-means (পর্ব ৭০)। আর বলুন: k-means গোল, প্রায় সমান আকারের দল ধরে নেয়; ভিন্ন আকারের দলে পর্ব ৩৯-এর GMM, যার hard রূপই k-means.

সমস্যা ৫ · SVD দিয়ে PCA

প্রশ্ন: “একটি (n, d) ডেটা থেকে প্রথম ৩টি principal component, প্রতিটির explained variance ratio আর প্রতিটি সারির নতুন স্থানাঙ্ক (score) দিন, SVD দিয়ে।” ডেটা: scikit-learn-এর সাথে আসা Wine ডেটাসেট, ১৭৮টি ওয়াইনের ১৩টি রাসায়নিক মাপ।

যা জিজ্ঞেস করবেন: Standardize করব কি? (এখানে হ্যাঁ: মাপগুলোর একক আলাদা।) চিহ্ন (sign) কি মিলতে হবে?

ইঙ্গিত: Center করা X = UΣVᵀ হলে Vᵀ-এর সারিগুলোই component, σᵢ²/Σσ² হলো explained variance ratio, আর score = XV = UΣ। প্রতিটি component-এর চিহ্ন উল্টালেও উত্তর একই।

import numpy as np
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
X = load_wine().data                      # scikit-learn-এর সাথে আসা Wine ডেটা: 178 × 13
X = (X - X.mean(axis=0)) / X.std(axis=0)  # মাপ আলাদা, তাই standardize

def pca_svd(X, k):
    Xc = X - X.mean(axis=0)               # center না করলে প্রথম অক্ষ গড়ের দিকে যায়
    U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
    evr = S**2 / (S**2).sum()             # explained variance ratio
    scores = Xc @ Vt[:k].T                # = U[:, :k] * S[:k]
    return scores, Vt[:k], evr[:k]

scores, comps, evr = pca_svd(X, 3)
sk = PCA(n_components=3).fit(X)
sign = np.sign((comps * sk.components_).sum(axis=1))   # প্রতিটি অক্ষের চিহ্ন মেলানো
print("shapes:", X.shape, "->", scores.shape)
print("ours   evr:", evr.round(4))
print("sklearn evr:", sk.explained_variance_ratio_.round(4))
assert np.allclose(comps * sign[:, None], sk.components_)
assert np.allclose(scores * sign, sk.transform(X))
print("same up to sign:", sign.astype(int).tolist())

সত্যিকারের আউটপুট:

shapes: (178, 13) -> (178, 3)
ours   evr: [0.362  0.1921 0.1112]
sklearn evr: [0.362  0.1921 0.1112]
same up to sign: [-1, 1, 1]

প্রথম তিনটি অক্ষ মোট variance-এর প্রায় ৩৬%, ১৯% আর ১১% ধরে, scikit-learn-এর সাথে হুবহু এক। এখানে প্রথম component-এর চিহ্ন উল্টো এসেছে: SVD-র চিহ্ন গাণিতিকভাবে অনির্ধারিত, আর scikit-learn একটি নিজস্ব নিয়মে চিহ্ন স্থির করে। একই কোড ডেস্কটপে (NumPy 2.4.6, Windows) চালিয়ে পেয়েছি [-1, -1, 1]: ভেতরের LAPACK আলাদা হলে চিহ্নও আলাদা আসতে পারে। তাই সরাসরি allclose কোনো মেশিনে পাস, কোনোটিতে ফেল করত; চিহ্ন মিলিয়ে তারপর তুলনা করাই সঠিক test.

প্র ৯ফলো-আপ: covariance-এর eigen-decomposition না করে SVD কেন? খরচ কত? (মাঝারি)

উত্তর: Covariance XᵀX/(n − 1) বানালে condition number বর্গ হয়, তাই ছোট variance-এর দিকগুলোতে নির্ভুলতা হারায়; সরাসরি X-এর SVD সংখ্যাগতভাবে নিরাপদ (পর্ব ৯)। খরচ: thin SVD O(n·d·min(n, d)); eigen পথে XᵀX বানাতে O(nd²) আর ভাঙতে O(d³), d ছোট আর n বিশাল হলে এটি সস্তা হতে পারে। শুধু প্রথম কয়েকটি component লাগলে randomized SVD বা truncated SVD. আর center করা না হলে প্রথম অক্ষ প্রায়ই শুধু গড়ের দিক দেখায়; sparse ডেটায় center করা যায় না বলে সেখানে TruncatedSVD (পর্ব ৮)।

৫ · Tree-র split আর ROC AUC — sort করে একবারে

রিভিশন: পর্ব ৩২ · CART আর impurity, পর্ব ৩৪ · decision stump, পর্ব ৩৬ · histogram split, পর্ব ৫০ · metric, AI-এর ভিত্তি কোর্সের পর্ব ৬০

সমস্যা ৬ · Decision stump-এর সেরা split

প্রশ্ন: “Feature matrix X (n, d) আর binary label y. Gini impurity-র ওজনসহ গড় সবচেয়ে কমায় এমন একটি (feature, threshold) খুঁজুন, যেভাবে CART করে।” শর্ত: প্রতিটি সম্ভাব্য threshold-এ আবার পুরো ডেটা গোনা চলবে না।

যা জিজ্ঞেস করবেন: Threshold কোথায় বসবে: দুই মানের মাঝখানে? সমান মান কীভাবে সামলাব? Feature-এ NaN আছে কি?

ইঙ্গিত: Feature ধরে sort করলে, i নম্বর সারির পরে কাটলে বাঁয়ের positive সংখ্যা হলো cumulative sum. তাই সব threshold-এর gini একবারে, sort-এর খরচে।

import numpy as np
from sklearn.tree import DecisionTreeClassifier
rng = np.random.default_rng(3)
n = 120
X = np.column_stack([rng.integers(1, 60, n),          # দিন থেকে শেষ অর্ডার
                     rng.integers(0, 15, n)])         # মোট অর্ডার
y = ((X[:, 0] > 30) ^ (rng.random(n) < 0.15)).astype(int)   # churn, ১৫% noise

def best_split(X, y):
    """প্রতিটি feature-এ sort, তারপর cumsum দিয়ে সব threshold একবারে: O(d·n log n)."""
    n, best = len(y), (np.inf, None, None)
    for j in range(X.shape[1]):
        o = np.argsort(X[:, j], kind="stable")
        xs, ys = X[o, j], y[o]
        nl = np.arange(1, n)                       # বাঁয়ে 1..n-1 টি সারি
        pl = np.cumsum(ys)[:-1] / nl               # বাঁয়ের positive অনুপাত
        pr = (ys.sum() - np.cumsum(ys)[:-1]) / (n - nl)
        gini = (nl * 2 * pl * (1 - pl) + (n - nl) * 2 * pr * (1 - pr)) / n
        gini[xs[1:] == xs[:-1]] = np.inf           # সমান মানের মাঝে কাটা যায় না
        i = gini.argmin()
        if gini[i] < best[0]:
            best = (gini[i], j, (xs[i] + xs[i + 1]) / 2)
    return best

g, j, t = best_split(X, y)
tree = DecisionTreeClassifier(max_depth=1, random_state=0).fit(X, y)
print(f"ours   : feature {j}, threshold {t}, weighted gini {g:.4f}")
print(f"sklearn: feature {tree.tree_.feature[0]}, threshold {tree.tree_.threshold[0]}")
assert j == tree.tree_.feature[0] and np.isclose(t, tree.tree_.threshold[0])

সত্যিকারের আউটপুট:

ours   : feature 0, threshold 30.0, weighted gini 0.2149
sklearn: feature 0, threshold 30.0

দুটিই feature ০ (দিন থেকে শেষ অর্ডার) আর threshold ৩০.০ বেছেছে; ডেটা বানানোই হয়েছিল “৩০ দিনের বেশি হলে churn”, ১৫% noise-সহ। Threshold দুই পাশাপাশি আলাদা মানের মাঝখানে, scikit-learn-এর মতো। সমান মানের মাঝে কাটা নিষেধ করার লাইনটি না থাকলে একই মানের দুটি সারি দুই দিকে চলে যেত, যা বাস্তবে কোনো threshold দিয়েই সম্ভব নয়।

প্র ১০ফলো-আপ: জটিলতা কত? পুরো tree বানাতে আর বড় ডেটায় কী করবেন? (মাঝারি)

উত্তর: প্রতিটি feature-এ sort O(n log n), cumsum O(n), তাই একটি node-এ O(d·n log n). সরল পথে (প্রতিটি threshold-এ আবার গোনা) O(d·n²)। পুরো tree: প্রতিটি স্তরে মোট সারি n, তাই ভারসাম্যপূর্ণ tree-তে প্রায় O(d·n log² n), বা একবার sort রেখে দিয়ে O(d·n log n) (পর্ব ৩২)। বড় ডেটায় LightGBM আর scikit-learn-এর HistGradientBoosting আগে মানগুলোকে ২৫৫-এর মতো bin-এ ভাগ করে, তখন split খোঁজা O(bins) আর sort লাগে না (পর্ব ৩৬)। Regression tree-তে gini-র বদলে variance, আর একই cumsum কৌশল চলে Σy আর Σy²-এ।

সমস্যা ৭ · ROC AUC, সমান score-সহ

প্রশ্ন: “Label y আর model-এর score s দেওয়া। ROC AUC লিখুন, scikit-learn ছাড়া। সমান score থাকতে পারে।”

যা জিজ্ঞেস করবেন: একটিই class থাকলে কী ফেরত দেব? Score কি সম্ভাবনা, নাকি যেকোনো সংখ্যা? (AUC শুধু ক্রম দেখে, তাই দুটিই চলে।)

ইঙ্গিত: সংজ্ঞা: এলোমেলো একটি positive-এর score এলোমেলো একটি negative-এর চেয়ে বেশি হওয়ার সম্ভাবনা, সমান হলে অর্ধেক। এটি Mann–Whitney U পরিসংখ্যান থেকে আসে, তাই rank দিয়ে O(n log n).

import numpy as np
from scipy.stats import rankdata
from sklearn.metrics import roc_auc_score

def auc_rank(y, s):
    """Mann–Whitney: positive-গুলোর rank-যোগ থেকে। sort-এর জন্য O(n log n)."""
    y = np.asarray(y); r = rankdata(s)           # সমান score পায় গড় rank
    n1 = y.sum(); n0 = len(y) - n1
    if n1 == 0 or n0 == 0:
        raise ValueError("AUC needs both classes")
    return (r[y == 1].sum() - n1 * (n1 + 1) / 2) / (n1 * n0)

def auc_pairs(y, s):
    """সংজ্ঞা থেকে সরাসরি: সব (pos, neg) জোড়া, O(n1·n0)."""
    sp, sn = s[y == 1][:, None], s[y == 0][None, :]
    return ((sp > sn) + 0.5 * (sp == sn)).mean()

y = np.array([0, 0, 1, 1, 0, 1])
s = np.array([0.1, 0.4, 0.35, 0.8, 0.4, 0.4])    # তিনটি score সমান: 0.4
print(f"tiny  : rank {auc_rank(y, s):.4f} | pairs {auc_pairs(y, s):.4f} | sklearn {roc_auc_score(y, s):.4f}")

rng = np.random.default_rng(5)
y = rng.integers(0, 2, 2000)
s = np.round(y * 0.6 + rng.normal(0, 1, 2000), 1)  # এক দশমিকে গোল: অনেক tie
a, b, c = auc_rank(y, s), auc_pairs(y, s), roc_auc_score(y, s)
print(f"random: rank {a:.6f} | pairs {b:.6f} | sklearn {c:.6f}")
assert np.isclose(a, b) and np.isclose(a, c)

সত্যিকারের আউটপুট:

tiny  : rank 0.6667 | pairs 0.6667 | sklearn 0.6667
random: rank 0.659134 | pairs 0.659134 | sklearn 0.659134

তিনটি পথ একই উত্তর দিল: ছোট উদাহরণে ০.৬৬৬৭, আর ২০০০টি সারির, প্রচুর tie-ওয়ালা ডেটায় ছয় দশমিক পর্যন্ত এক। Rank-এর পথে সমান score গড় rank পায় (rankdata-র default), যা সংজ্ঞার “সমান হলে অর্ধেক”-এর সাথে হুবহু মেলে। শুধু একটি class থাকলে হর শূন্য, তাই ফাংশনটি স্পষ্ট error দেয়। scikit-learn 1.8.0 এই ক্ষেত্রে UndefinedMetricWarning দিয়ে nan ফেরত দেয় (ডেস্কটপে সত্যিকারের run); কোনটি চান, ইন্টারভিউতে তা আগে জিজ্ঞেস করে নিন।

positive score: 0.35, 0.8, 0.4 │ negative score: 0.1, 0.4, 0.4
positive
negative-এর সাথে তুলনা
নম্বর
  1. Positive 0.35. 0.1-এর চেয়ে বেশি (১ নম্বর), দুটি 0.4-এর চেয়ে কম (০)। মোট ১.
  2. Positive 0.8. তিনটি negative-এর চেয়েই বেশি: ৩.
  3. Positive 0.4. 0.1-এর চেয়ে বেশি (১), আর দুটি 0.4-এর সমান: প্রতিটিতে ০.৫. মোট ২.
  4. যোগফল. ৬টি “জয়” ৯টি জোড়ায়: AUC = 0.6667. এই পথ O(n₁·n₀), ছোট ডেটায় যাচাইয়ের জন্য ভালো।
  5. Rank-এর পথ. সব ছয়টি score সাজালে 0.35-এর rank ২, তিনটি 0.4-এর গড় rank ৪, 0.8-এর rank ৬. Positive-দের rank-যোগ 2 + 6 + 4 = 12; তা থেকে n₁(n₁ + 1)/2 = 6 বাদ, ভাগ n₁·n₀ = 9: আবার 0.6667, এবার O(n log n)-এ।
প্র ১১ফলো-আপ: ৯৯% negative-এর ডেটায় AUC ০.৯৫। ভালো খবর? (মাঝারি)

উত্তর: সতর্ক হওয়ার কারণ আছে। ROC AUC-এর x অক্ষ false positive rate, যার হর বিশাল negative সংখ্যা; তাই হাজারো ভুল সতর্কতাও সেখানে ছোট দেখায়। ব্যবসার প্রশ্ন সাধারণত “সতর্ক করা ক্রেতার কতজন আসলেই চলে যাবেন”, অর্থাৎ precision; তাই average precision বা নির্দিষ্ট recall-এ precision দেখুন (পর্ব ৫০)। আর AUC সম্ভাবনার মাপ ঠিক কি না বলে না: score-কে ২ দিয়ে ভাগ করলেও AUC একই; তার জন্য calibration (পর্ব ২৬)। জটিলতা: rank পথে O(n log n), memory O(n).

প্র ১২ওয়ার্ম-আপ: y = [1, 0, 1, 1, 0, 0], pred = [1, 1, 0, 1, 1, 0]. NumPy-তে precision আর recall লিখুন, মানসহ। (সহজ)

ভালো উত্তর: আগে চারটি গণনা boolean mask দিয়ে: tp = ((pred == 1) & (y == 1)).sum(), একইভাবে fp আর fn. এখানে TP = ২ (প্রথম আর চতুর্থ), FP = ২ (দ্বিতীয় আর পঞ্চম), FN = ১ (তৃতীয়)। তাই precision = TP/(TP + FP) = ২/৪ = ০.৫, আর recall = TP/(TP + FN) = ২/৩ ≈ ০.৬৬৭। সাথে বলুন: কোনো positive ভবিষ্যদ্বাণী না থাকলে precision-এর হর ০, তখন কী ফেরত দেবেন তা আগে ঠিক করুন (scikit-learn-এ এর জন্য zero_division parameter আছে)। জটিলতা O(n).

দুর্বল উত্তর: Python loop-এ প্রতিটি সারিতে চারটি if, আর হর শূন্য হওয়ার কথা না ভাবা।

৬ · Resampling — নিজের k-fold splitter আর bootstrap CI

রিভিশন: পর্ব ৪ · experiment আর CV, পর্ব ২১ · bootstrap, পর্ব ২২ · model তুলনা, AI-এর ভিত্তি কোর্সের পর্ব ৬৬

সমস্যা ৮ · k-fold index আর bootstrap percentile interval

প্রশ্ন: “(ক) n আর k দিলে প্রতিটি fold-এর (train, test) index দেয় এমন একটি generator লিখুন, scikit-learn-এর KFold-এর মতো আকারে। (খ) ৬০টি ডেলিভারির সময় থেকে গড় আর median-এর ৯৫% percentile bootstrap interval দিন।”

যা জিজ্ঞেস করবেন: n যদি k দিয়ে ভাগ না যায়, বাড়তি সারি কোন fold-এ? Shuffle লাগবে কি? Bootstrap-এ কয়টি resample?

ইঙ্গিত: Fold-এর মাপ n // k, প্রথম n % k টি fold-এ একটি বেশি। Bootstrap loop ছাড়া: rng.integers(0, n, (B, n)) দিয়ে একবারে B × n index.

import numpy as np
from sklearn.model_selection import KFold

def kfold_indices(n, k, shuffle=False, seed=None):
    idx = np.arange(n)
    if shuffle:
        np.random.default_rng(seed).shuffle(idx)
    sizes = np.full(k, n // k)
    sizes[: n % k] += 1                       # প্রথম n % k টি fold-এ একটি বেশি
    start = 0
    for s in sizes:
        test = idx[start:start + s]
        yield np.setdiff1d(idx, test), test   # train sorted, test-এর বাইরে সব
        start += s

ours = list(kfold_indices(11, 3))
ref = list(KFold(n_splits=3).split(np.zeros(11)))
print("test folds:", [t.tolist() for _, t in ours])
assert all((a == c).all() and (b == d).all() for (a, b), (c, d) in zip(ours, ref))

rng = np.random.default_rng(8)
hours = rng.gamma(2.0, 12.0, 60)              # ৬০টি ডেলিভারির সময় (ঘণ্টা), ডানে লম্বা লেজ
B = 5000
boot = hours[rng.integers(0, len(hours), (B, len(hours)))]   # (B, n): একবারে সব resample
for name, f in [("mean", np.mean), ("median", np.median)]:
    lo, hi = np.percentile(f(boot, axis=1), [2.5, 97.5])
    print(f"{name:6s}: {f(hours):5.2f}  95% percentile CI [{lo:5.2f}, {hi:5.2f}]")
se = hours.std(ddof=1) / np.sqrt(len(hours))
print(f"normal CI for mean: [{hours.mean() - 1.96*se:5.2f}, {hours.mean() + 1.96*se:5.2f}]")

সত্যিকারের আউটপুট:

test folds: [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10]]
mean  : 25.30  95% percentile CI [20.57, 30.32]
median: 19.67  95% percentile CI [15.58, 26.83]
normal CI for mean: [20.43, 30.16]

১১টি সারি, ৩টি fold: মাপ ৪, ৪, ৩, আর index KFold-এর সাথে হুবহু এক (assert পেরিয়েছে)। Bootstrap-এ গড়ের interval আর সাধারণ normal সূত্রের interval প্রায় এক, কারণ ৬০টি মানে গড়ের বণ্টন প্রায় normal. Median-এর interval গড়ের তুলনায় নিচের দিকে সরানো আর অসম (১৯.৬৭ থেকে নিচে ৪, ওপরে ৭): ডানে লম্বা লেজের ডেটায় যেটি প্রত্যাশিত, আর যার কোনো সহজ সূত্র নেই। Bootstrap-এর আসল কাজ এখানেই।

প্র ১৩ফলো-আপ: কখন সাধারণ k-fold ভুল? Bootstrap কখন ভুল interval দেয়? (কঠিন)

উত্তর: সাধারণ k-fold ধরে নেয় সারিগুলো পরস্পর স্বাধীন। একই ক্রেতার একাধিক সারি থাকলে GroupKFold; label ভারসাম্যহীন হলে stratified; সময়ের ডেটায় সামনে-এগোনো ভাগ (TimeSeriesSplit, rolling origin, পর্ব ৫২)। Hyperparameter বাছাই আর মূল্যায়ন একই fold-এ করলে ফল আশাবাদী, তাই nested CV (পর্ব ৪)। Bootstrap ভুল করে যখন সারিগুলো নির্ভরশীল (সময় বা দল: block বা cluster bootstrap), খুব ছোট n-এ, আর max বা খুব প্রান্তের quantile-এর মতো পরিসংখ্যানে (পর্ব ২১)। জটিলতা: vectorised bootstrap-এর memory O(B·n); B বা n বড় হলে ব্লক করে চালান।

৭ · pandas — ভবিষ্যৎ না দেখে সময়ের feature, session আর cohort

রিভিশন: পর্ব ২ · point-in-time feature, পর্ব ৫২ · lag feature, পর্ব ৬১ · session আর cohort, SQL-এ, পর্ব ৬৫ · retention, পর্ব ৭২ · ক্যাপস্টোন

সমস্যা ৯ · প্রতিটি অর্ডারে “আগের ৩০ দিনের খরচ” — আর leak খোঁজা

প্রশ্ন: “অর্ডারের টেবিলে cust, ts, taka. প্রতিটি অর্ডারের জন্য দুটি feature: ওই ক্রেতার আগের ৩০ দিনের মোট খরচ, আর এর আগে কয়টি অর্ডার। একজন সহকর্মী তৃতীয় একটি feature দিয়েছেন, ক্রেতার ‘মোট খরচ’। কোনটিতে leak আছে, প্রমাণ করুন।”

যা জিজ্ঞেস করবেন: অনুমানটি কখন করা হয়: অর্ডারের আগে, নাকি পরে? একই মুহূর্তের দুটি অর্ডার কীভাবে গুনব? জানালা কি ৩০টি সারি, নাকি ৩০ ক্যালেন্ডার দিন?

ইঙ্গিত: সময় ধরে rolling-এ closed="left" জানালা [t − 30D, t) দেয়, বর্তমান সারি বাদ। আর leak-এর সবচেয়ে সরল test: ভবিষ্যতের সারি মুছে আবার হিসাব করলে অতীতের feature বদলায় কি না।

import numpy as np
import pandas as pd
rng = np.random.default_rng(9)
n = 40
orders = pd.DataFrame({
    "cust": rng.choice(["c1", "c2", "c3"], n),
    "ts": pd.Timestamp("2026-01-01") + pd.to_timedelta(rng.integers(0, 120, n), unit="D"),
    "taka": rng.integers(200, 2000, n)}).sort_values(["cust", "ts"], ignore_index=True)

def add_features(o):
    o = o.sort_values(["cust", "ts"], ignore_index=True)
    g = o.set_index("ts").groupby("cust")["taka"]
    # শুধু আগের ৩০ দিন: [ts − 30D, ts) — বর্তমান অর্ডার আর ভবিষ্যৎ বাদ
    o["spend_30d"] = g.rolling("30D", closed="left").sum().fillna(0).to_numpy()
    o["n_prev"] = o.groupby("cust").cumcount()            # এর আগে কয়টি অর্ডার
    o["leaky_total"] = o.groupby("cust")["taka"].transform("sum")   # বাগ: ভবিষ্যৎও যোগ
    return o

full = add_features(orders)
cut = pd.Timestamp("2026-03-01")
past = add_features(orders[orders["ts"] < cut])          # ভবিষ্যৎ মুছে আবার হিসাব
f_full = full[full["ts"] < cut].reset_index(drop=True)
for col in ["spend_30d", "n_prev", "leaky_total"]:
    same = np.array_equal(f_full[col].to_numpy(), past[col].to_numpy())
    print(f"{col:12s} unchanged without future rows: {same}")
print(full[full["cust"] == "c1"].head(4).to_string(index=False))

সত্যিকারের আউটপুট:

spend_30d    unchanged without future rows: True
n_prev       unchanged without future rows: True
leaky_total  unchanged without future rows: False
cust         ts  taka  spend_30d  n_prev  leaky_total
  c1 2026-01-10   203        0.0       0         9456
  c1 2026-01-14  1392      203.0       1         9456
  c1 2026-01-16  1501     1595.0       2         9456
  c1 2026-01-16   213     1595.0       3         9456

প্রথম দুটি feature ১ মার্চের পরের সারি মুছে ফেললেও অতীতের সারিতে হুবহু এক: তারা শুধু অতীত দেখে, এটিই point-in-time feature. “মোট খরচ” বদলে গেছে, কারণ transform("sum") ক্রেতার ভবিষ্যতের অর্ডারও যোগ করে; c1-এর প্রথম অর্ডারেই ৯৪৫৬ টাকা দেখাচ্ছে, যা ১০ জানুয়ারিতে কেউ জানত না। এটি temporal leakage: offline score ফুলে ওঠে, production-এ ভেঙে পড়ে। শেষ দুটি সারিতে একটি সূক্ষ্মতা: ১৬ জানুয়ারির দুটি অর্ডারের দ্বিতীয়টিতে n_prev ৩, অথচ spend_30d একই দিনের প্রথম অর্ডারটি গোনেনি। একই মুহূর্তের অর্ডার গোনা হবে কি না, তা ব্যবসার প্রশ্ন; দুই feature-এ একই নিয়ম রাখুন।

প্র ১৪ফলো-আপ: জটিলতা কত? Label কীভাবে বানাবেন যাতে সেখানেও leak না হয়? (মাঝারি)

উত্তর: Sort O(n log n), তারপর দলভিত্তিক সময়ের rolling প্রায় O(n), memory O(n). Label-এর নিয়ম feature-এর উল্টো: একটি cutoff তারিখ ঠিক করে feature শুধু cutoff-এর আগের ডেটা থেকে, label শুধু cutoff-এর পরের একটি নির্দিষ্ট জানালা থেকে (যেমন পরের ৯০ দিনে অর্ডার নেই = churn). জানালা শেষ না হওয়া ক্রেতার label এখনো অজানা (immature label), তাদের বাদ দিন (পর্ব ২, পর্ব ৭২)। Validation-ও সময় ধরে: পুরোনো cutoff-এ train, নতুন cutoff-এ test. দুই টেবিল সময় ধরে জুড়তে pd.merge_asof, যা প্রতিটি সারির জন্য সেই মুহূর্ত পর্যন্ত সর্বশেষ মান নেয়।

সমস্যা ১০ · Sessionisation আর cohort retention টেবিল

প্রশ্ন: “(ক) ক্লিকের log থেকে প্রতিটি ঘটনার session নম্বর দিন: একই user-এর আগের ঘটনা থেকে ৩০ মিনিটের বেশি বিরতি হলে নতুন session. (খ) মাসিক অর্ডার থেকে cohort retention টেবিল: সারিতে প্রথম অর্ডারের মাস, কলামে তার পরের কত মাস।”

যা জিজ্ঞেস করবেন: ঠিক ৩০ মিনিট হলে নতুন session কি? Cohort-এ যে মাস এখনো আসেনি, তা ০ না ফাঁকা?

ইঙ্গিত: Sessionisation হলো “gaps and islands”: নতুন session শুরু হলে ১, তারপর cumulative sum. Cohort-এ মাসকে পূর্ণসংখ্যায় (বছর × ১২ + মাস) নিলে বয়স শুধু বিয়োগ।

import pandas as pd
ev = pd.DataFrame({"user": ["u1"] * 5 + ["u2"] * 3,
    "ts": pd.to_datetime(["09:00", "09:10", "09:55", "10:05", "13:00",
                          "09:20", "09:45", "09:50"], format="%H:%M")})
ev = ev.sort_values(["user", "ts"], ignore_index=True)
gap = ev.groupby("user")["ts"].diff()                    # প্রতিটি user-এর ভেতরে বিরতি
new = gap.isna() | (gap > pd.Timedelta("30min"))         # প্রথম ঘটনা বা ৩০ মিনিটের বেশি
ev["session"] = new.cumsum()                             # gaps and islands
print(ev.assign(t=ev["ts"].dt.strftime("%H:%M"))[["user", "t", "session"]].T.to_string(header=False))

orders = pd.DataFrame({"cust": [1, 1, 2, 2, 3, 3, 3, 4, 5, 5],
    "month": pd.to_datetime(["2026-01", "2026-02", "2026-01", "2026-03", "2026-02",
        "2026-03", "2026-04", "2026-02", "2026-03", "2026-04"]).to_period("M")})
m = orders["month"].dt.year * 12 + orders["month"].dt.month   # মাসের পূর্ণসংখ্যা
orders["cohort"] = orders.groupby("cust")["month"].transform("min")
orders["age"] = m - m.groupby(orders["cust"]).transform("min")
active = orders.groupby(["cohort", "age"])["cust"].nunique().unstack(fill_value=0)
active = active.reindex(columns=range(m.max() - m.min() + 1), fill_value=0)
ret = active.div(active[0], axis=0)
seen = m.max() - (ret.index.year * 12 + ret.index.month).to_numpy()  # কত মাস দেখা গেছে
ret = ret.where(ret.columns.to_numpy()[None, :] <= seen[:, None]).round(2)
print(ret)                                               # NaN = এখনো দেখা হয়নি

সত্যিকারের আউটপুট:

user        u1     u1     u1     u1     u1     u2     u2     u2
t        09:00  09:10  09:55  10:05  13:00  09:20  09:45  09:50
session      1      1      2      2      3      4      4      4
age        0    1    2    3
cohort                     
2026-01  1.0  0.5  0.5  0.0
2026-02  1.0  0.5  0.5  NaN
2026-03  1.0  1.0  NaN  NaN

u1-এর ০৯:১০ থেকে ০৯:৫৫ বিরতি ৪৫ মিনিট, তাই session ২; ১৩:০০-তে session ৩। u2-এর প্রথম ঘটনায় diff ফাঁকা, তাই নতুন session ৪: user বদলালে session-ও বদলায়, আলাদা শর্ত ছাড়াই। Cohort টেবিলে জানুয়ারির দলের ৩ মাস পরে ০.০, কিন্তু মার্চের দলের ২ মাস পরে NaN: এপ্রিলের পরে ডেটা নেই, তাই সেটি “কেউ ফেরেনি” নয়, “এখনো দেখা হয়নি”। এই পার্থক্য না রাখলে নতুন দলের retention মিথ্যা করে কম দেখায় (পর্ব ৬৫)।

প্র ১৫ফলো-আপ: জটিলতা? ১০ কোটি ঘটনা হলে? SQL-এ কীভাবে? (মাঝারি)

উত্তর: দুটিই sort O(n log n), তারপর diff আর cumsum O(n). Cohort-এ groupby-nunique প্রায় O(n). ১০ কোটি ঘটনায় pandas-এর memory সীমা ছাড়াতে পারে: দরকারি কলাম আর ছোট dtype, user ধরে ভাগ করে আলাদা আলাদা চালানো (session কখনো দুই user-এ ছড়ায় না, তাই ভাগ নিরাপদ), অথবা Polars বা DuckDB (পর্ব ৭০)। SQL-এ একই কাজ LAG দিয়ে বিরতি আর running SUM দিয়ে session নম্বর (পর্ব ৬১)। সময় UTC-তে থাকলে বিরতিতে সমস্যা নেই, কিন্তু “মাস” বা “দিন” গুনতে আগে স্থানীয় সময়ে রূপান্তর।

৮ · scikit-learn — leakage-মুক্ত pipeline, নিজের transformer আর সঠিক CV

রিভিশন: পর্ব ৩৮ · tabular model আর pipeline, পর্ব ৬৮ · serving-এ একই pipeline, পর্ব ৪, AI-এর ভিত্তি কোর্সের পর্ব ৬৪

সমস্যা ১১ · ColumnTransformer, নিজের transformer আর stratified CV

প্রশ্ন: “৬০০ জন ক্রেতা: জেলা (লেখা), মোট খরচ (ডানে লম্বা লেজ, ১০% ফাঁকা), শেষ অর্ডারের পরে কত দিন। Churn-এর জন্য একটি pipeline বানান: সংখ্যার কলামে median দিয়ে ভরা, train-এর ৯৯তম percentile-এ ছেঁটে log, তারপর scale; জেলায় one-hot. ৫-fold CV-তে AUC দিন। অচেনা জেলা এলেও যেন না ভাঙে।”

যা জিজ্ঞেস করবেন: ছাঁটার সীমা কি train থেকে শিখব? (হ্যাঁ, নইলে test-এর তথ্য ঢোকে।) Class কি ভারসাম্যহীন? Hyperparameter বাছতে হবে কি?

ইঙ্গিত: নিজের transformer-এ __init__ শুধু parameter রাখে, fit যা শেখে তা শেষে underscore দিয়ে (cap_), আর transform শুধু শেখা মান ব্যবহার করে। তাহলে clone আর CV ঠিক চলে।

প্রতিটি fold-এ পুরো pipeline নতুন করে fit হয়

ColumnTransformer routes columns; every learned value comes from the training fold only

ColumnTransformer-সহ pipeline trainfold spend, days_sinceImputer → CapLog → Scaler districtOneHotEncoder(ignore) LogisticRegression median, cap_, গড় আর category: শুধু train fold থেকে test fold শুধু transform হয়, fit কখনো নয়
ছবি ৭৬.২ — cross_val_score প্রতিটি fold-এ পুরো pipeline-এর একটি নতুন copy (clone) fit করে; তাই কোনো শেখা মান test fold দেখে না।
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
class CapLog(BaseEstimator, TransformerMixin):        # train-এর q-quantile-এ ছেঁটে log1p
    def __init__(self, q=0.99):
        self.q = q
    def fit(self, X, y=None):                         # cap_ শেখে শুধু fit-এ, মানে train fold-এ
        self.cap_ = np.quantile(np.asarray(X, dtype=float), self.q, axis=0)
        return self
    def transform(self, X):
        return np.log1p(np.minimum(np.asarray(X, dtype=float), self.cap_))
rng, n = np.random.default_rng(11), 600
df = pd.DataFrame({"district": rng.choice(["ঢাকা", "খুলনা", "সিলেট", "রংপুর"], n),
                   "spend": rng.lognormal(7, 1, n), "days_since": rng.integers(1, 90, n)})
df.loc[rng.random(n) < 0.1, "spend"] = np.nan                     # ১০% ফাঁকা
y = (rng.random(n) < 1 / (1 + np.exp(-(df["days_since"] - 45) / 15))).astype(int)
num = make_pipeline(SimpleImputer(strategy="median"), CapLog(), StandardScaler())
pre = ColumnTransformer([("num", num, ["spend", "days_since"]),
                         ("cat", OneHotEncoder(handle_unknown="ignore"), ["district"])])
pipe = make_pipeline(pre, LogisticRegression(max_iter=1000))
auc = cross_val_score(pipe, df, y, cv=StratifiedKFold(5, shuffle=True, random_state=0), scoring="roc_auc")
print("fold AUC:", auc.round(3), "| mean", auc.mean().round(3))
new = pd.DataFrame({"district": ["বান্দরবান"], "spend": [np.nan], "days_since": [80]})
print("unseen district, missing spend ->", pipe.fit(df, y).predict_proba(new)[:, 1].round(3))

সত্যিকারের আউটপুট:

fold AUC: [0.783 0.85  0.851 0.868 0.836] | mean 0.837
unseen district, missing spend -> [0.829]

পাঁচটি fold-এ AUC ০.৭৮ থেকে ০.৮৭, গড় ০.৮৪; একটি সংখ্যার বদলে fold-গুলোর ছড়ানোও বলুন, কারণ ৬০০ সারিতে ভাগের ভাগ্য বড়। শেষ লাইনে বান্দরবান জেলা train-এ ছিল না আর খরচ ফাঁকা, তবু pipeline ভাঙেনি: handle_unknown="ignore" অচেনা জেলাকে সব-শূন্য one-hot দেয়, আর imputer train-এর median বসায়। ৮০ দিন অর্ডার নেই, তাই churn-এর সম্ভাবনা বেশি (০.৮৩)। পুরো pipeline একটি object, তাই serving-এও একই ধাপ চলে (পর্ব ৬৮)।

প্র ১৬ওয়ার্ম-আপ: fit, transform আর fit_transform — test set-এ কোনটি ডাকবেন? (সহজ)

ভালো উত্তর: fit ডেটা থেকে মান শেখে (গড়, median, category-র তালিকা), transform শেখা মান দিয়ে ডেটা বদলায়, আর fit_transform দুটি একসাথে, শুধু train-এ। Test বা নতুন ডেটায় সবসময় শুধু transform, নইলে test নিজের গড় দিয়ে scale হয় আর train-এর সাথে তুলনা অর্থহীন হয়ে যায়। Pipeline ব্যবহার করলে এই ভুলের সুযোগই থাকে না: pipe.fit(X_train) আর pipe.predict(X_test) ভেতরে ঠিক ক্রমে ডাকে (AI-এর ভিত্তি কোর্সের পর্ব ৬৪)।

দুর্বল উত্তর: “Train আর test দুটিতেই fit_transform; একই তো কাজ।” কাজ একই নয়: দুই সেটের মাপকাঠি আলাদা হয়ে যায়।

প্র ১৭ফলো-আপ: নিজের transformer-এ কোন ভুলে CV নীরবে ভেঙে যায়? Tuning কীভাবে যোগ করবেন? (কঠিন)

উত্তর: তিনটি পরিচিত ভুল। (১) __init__-এ হিসাব বা parameter-এর নাম বদলে রাখা: get_params আর clone তখন ভুল copy বানায়। (২) transform-এর ভেতরে নতুন করে quantile বা গড় হিসাব: তখন test fold নিজের মাপ দিয়ে ছাঁটা হয়, train-এর নয়। (৩) Module-স্তরের কোনো variable-এ অবস্থা রাখা, যা fold থেকে fold-এ গড়িয়ে যায়। Tuning-এ পুরো pipeline-ই GridSearchCV-কে দিন, parameter-এর নাম ধাপ ধরে, যেমন logisticregression__C; আর সৎ মূল্যায়নের জন্য বাইরে আরেকটি CV, অর্থাৎ nested CV, অথবা একটি তালাবন্ধ test set (পর্ব ৩৮)। একই ক্রেতার একাধিক সারি থাকলে StratifiedGroupKFold.

৯ · ডিবাগিং রাউন্ড — shape-এর বাগ, ভুল metric আর leak

রিভিশন: পর্ব ৬৯ · ML-এর test, পর্ব ৫০, পর্ব ২ · baseline, AI-এর ভিত্তি কোর্সের পর্ব ৫৬ · leakage

ডিবাগিং দফায় সাধারণত একটি ছোট কোড আর একটি অভিযোগ আসে। ভালো উত্তরের ক্রম: লক্ষণ মিলিয়ে নেওয়া, একটি অনুমান বলা, print বা assert দিয়ে প্রমাণ, ঠিক করা, আর একটি test দিয়ে আটকে রাখা। নিচের ব্লকে দুটি বাগ, বাগওয়ালা আর ঠিক রূপ পাশাপাশি।

সমস্যা ১২ · “MSE ৯৯, অথচ model তো ঠিক” আর “accuracy ৯৫%”

লক্ষণ ১: “সঠিক সহগ দিয়েও ডেলিভারি সময়ের MSE ৯৯ আসছে, noise-এর variance মাত্র ৪।” লক্ষণ ২: “৫% churn-এর ডেটায় model-এর accuracy ৯৫%, AUC ০.৫। কী হচ্ছে?”

কারণ খোঁজা: প্রথমটিতে প্রথম সন্দেহ shape: একটি (n,) আর একটি (n, 1) বিয়োগ করলে broadcasting কোনো error ছাড়াই (n, n) বানায়। দ্বিতীয়টিতে metric-কে baseline-এর সাথে মেলান, আর দেখুন AUC-কে কী দেওয়া হয়েছে: label, নাকি score.

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
rng = np.random.default_rng(12)
df = pd.DataFrame({"km": rng.uniform(1, 30, 500)})
df["hours"] = 2 + 0.8 * df["km"] + rng.normal(0, 2, 500)

# বাগ ১: y-কে (n, 1) কলাম হিসেবে নেওয়া, pred (n,) — broadcasting নীরবে (n, n) বানায়
X, y_col = df[["km"]].to_numpy(), df[["hours"]].to_numpy()
pred = 2 + 0.8 * X[:, 0]
err = pred - y_col
print("bug  shape:", err.shape, "| MSE:", round((err**2).mean(), 2))
err = pred - y_col.ravel()                     # সমাধান: ravel, আর shape-এ assert
assert err.shape == (len(df),)
print("fix  shape:", err.shape, "| MSE:", round((err**2).mean(), 2))

# বাগ ২: ৫% positive-এ accuracy আর predict() দিয়ে AUC
x = rng.normal(0, 1, (2000, 1))
y = (rng.random(2000) < 1 / (1 + np.exp(-(x[:, 0] * 1.5 - 4)))).astype(int)
m = LogisticRegression().fit(x[:1000], y[:1000])
xt, yt = x[1000:], y[1000:]
print("positive rate:", yt.mean().round(3))
print("accuracy, model     :", round(accuracy_score(yt, m.predict(xt)), 3))
print("accuracy, all zeros :", round(accuracy_score(yt, np.zeros_like(yt)), 3))
print("AUC from predict()  :", round(roc_auc_score(yt, m.predict(xt)), 3))
print("AUC from proba      :", round(roc_auc_score(yt, m.predict_proba(xt)[:, 1]), 3))

সত্যিকারের আউটপুট:

bug  shape: (500, 500) | MSE: 99.43
fix  shape: (500,) | MSE: 3.98
positive rate: 0.047
accuracy, model     : 0.953
accuracy, all zeros : 0.953
AUC from predict()  : 0.5
AUC from proba      : 0.846

বাগ ১: err-এর shape (500, 500): ৫০০টি ভবিষ্যদ্বাণীর প্রতিটি ৫০০টি আসল মানের সবকটি থেকে বিয়োগ হয়েছে, তাই MSE ৯৯.৪৩। ravel()-এর পরে (500,), MSE ৩.৯৮, noise-এর variance (২² = ৪)-এর কাছে। আটকানোর test: assert err.shape == (n,)। বাগ ২: ৪.৭% positive-এ model আর “সবাইকে ০ বলা” baseline-এর accuracy হুবহু এক, ০.৯৫৩: model আসলে একজনকেও positive বলেনি। তাই predict()-এর ০/১ দিয়ে AUC ঠিক ০.৫। সম্ভাবনা দিলে AUC ০.৮৫: model ক্রম ঠিকই শিখেছে, শুধু ০.৫ threshold এখানে কাজের নয় (পর্ব ৫০)। শিক্ষা: AUC-কে score দিন, আর প্রতিটি metric-এর পাশে baseline.

বাগ ক · Series দিয়ে fit: shape-এর সবচেয়ে চেনা error

একটি feature-এর জন্য অনেকে df["km"] দেন, যা এক-মাত্রার Series. ডেস্কটপে সত্যিকারের run (Python 3.14, scikit-learn 1.8.0; traceback ছোট করা, path বদলানো):

$ python shape_err.py
Traceback (most recent call last):
  File "~/ml-interview/shape_err.py", line 4, in <module>
    model = LinearRegression().fit(df["km"], df["hours"])
…
ValueError: Expected a 2-dimensional container but got <class 'pandas.Series'> instead. Pass a DataFrame containing a single row (i.e. single sample) or a single column (i.e. single feature) instead.

কারণ আর সমাধান: scikit-learn-এ X সবসময় (n_samples, n_features). সমাধান df[["km"]] (দুই বন্ধনী: এক কলামের DataFrame), বা NumPy-তে x.reshape(-1, 1). y এক-মাত্রার থাকাই ঠিক; উল্টোটা, মানে y-কে (n, 1) দিলে, অনেক estimator একটি সতর্কবার্তা দেয়, আর নিজের NumPy কোডে বাগ ১-এর মতো নীরব ভুল বানায়।

বাগ খ · Leak: “CV-তে AUC ০.৯৯”

অংশ ৭-এর সমস্যা ৯ ছিল এই দফার সবচেয়ে সাধারণ রূপ: ভবিষ্যৎ দেখা feature. আরও তিনটি রূপ চিনে রাখুন, প্রতিটির একটি প্রমাণের পথসহ। (১) ভাগের আগে পুরো ডেটায় scaler, imputer বা target encoding fit (preprocessing leakage): প্রমাণ, একই কাজ pipeline-এর ভেতরে নিলে score নামে কি না। (২) Label-এর প্রায় নকল একটি কলাম, যেমন “refund_amount” দিয়ে “ফেরত দিয়েছেন কি না” অনুমান (target leakage): প্রমাণ, permutation importance-এ একটি feature একাই প্রায় সব। (৩) একই ক্রেতা train আর test দুই দিকে: প্রমাণ, GroupKFold-এ score নামে কি না। অবিশ্বাস্য ভালো score-এর প্রথম ব্যাখ্যা প্রায় সবসময় leak, প্রতিভা নয়।

প্র ১৮সহকর্মীর notebook-এ test AUC ০.৯৯, production-এ ০.৬১। প্রথম আধা ঘণ্টায় কী কী দেখবেন? (কঠিন)

উত্তর: ক্রম ধরে বলুন। (১) Split: এলোমেলো সারি-ভাগ, নাকি সময় বা ক্রেতা ধরে? সময়ের সমস্যায় এলোমেলো ভাগ নিজেই leak. (২) Feature-এর সময়: প্রতিটি feature অনুমানের মুহূর্তে জানা যেত কি? সমস্যা ৯-এর “ভবিষ্যৎ মুছে আবার হিসাব” test চালান। (৩) Preprocessing কোথায় fit হয়েছে: pipeline-এর ভেতরে, নাকি আগে? (৪) Permutation importance-এ কোনো একটি feature কি প্রায় সব ব্যাখ্যা করছে? (৫) Training-serving skew: production-এ feature একই কোডে বানানো হচ্ছে কি না, আর একই কয়েকটি সারিতে দুই জায়গার feature মেলে কি না (পর্ব ৬৯)। শেষে বলুন: leak বাদ দিলেও যদি ফারাক থাকে, তখন distribution shift দেখব (পর্ব ৫৯)।

দুর্বল উত্তর: “Production-এর ডেটা খারাপ, model আবার train করি।” কারণ না জেনে retrain একই leak আবার বানায়।

১০ · জমা দেওয়ার আগে — জটিলতার ছক আর চেকলিস্ট

রিভিশন: পর্ব ৪, পর্ব ৬৯, পর্ব ৭০, DSA · coding ইন্টারভিউ

প্রতিটি সমাধানের শেষে জটিলতা বলা ইন্টারভিউয়ারের প্রত্যাশা। নিচের ছকে এই পাতার বারোটি সমস্যার সারাংশ; n সারি, d feature, k দল বা class, T iteration, B resample.

সমস্যাসময়memoryযাচাই
১ · linear regression, GDO(T·n·d)O(nd)lstsq
২ · logistic regression, GDO(T·n·d)O(nd)scikit-learn, C=1e10
৩ · softmax + CEO(nK)O(nK)SciPy, finite difference
৪ · k-meansO(T·n·k·d)O(nkd) বা O(nk)SciPy kmeans2, inertia
৫ · PCA (SVD)O(n·d·min(n, d))O(nd)scikit-learn, চিহ্ন মিলিয়ে
৬ · split finderO(d·n log n)O(n)max_depth=1 tree
৭ · ROC AUCO(n log n)O(n)সব জোড়া, scikit-learn
৮ · k-fold, bootstrapO(n), O(B·n)O(n), O(B·n)KFold, normal CI
৯ · সময়ের featureO(n log n)O(n)ভবিষ্যৎ মুছে আবার হিসাব
১০ · session, cohortO(n log n)O(n)হাতের ছোট উদাহরণ
১১ · pipeline + CVfold × fit-এর খরচmodel-এর ওপরঅচেনা category, ফাঁকা ঘর
১২ · ডিবাগিং——shape-এ assert, baseline

সমাধান জমা দেওয়ার আগে: আট লাইনের চেকলিস্ট

আরও গভীরে: Lloyd-এর k-means ১৯৫৭ সালে Bell Labs-এর একটি অভ্যন্তরীণ প্রতিবেদনে লেখা, প্রকাশিত হয় ১৯৮২ সালে; k-means++ Arthur ও Vassilvitskii-র ২০০৭ সালের প্রবন্ধ। AUC আর Mann–Whitney U-এর সম্পর্ক রোগনির্ণয় পরীক্ষার সাহিত্যে পরিচিত করেন Hanley ও McNeil (১৯৮২)। scikit-learn-এর নিজের estimator লেখার নিয়ম (__init__-এ শুধু parameter, শেখা মান underscore-সহ) তার “Developing scikit-learn estimators” নথিতে আছে, আর check_estimator দিয়ে নিজের class পরীক্ষা করা যায়। Floating-point-এ log-sum-exp-এর মতো কৌশলের পূর্ণ ব্যাখ্যা Nicholas Higham-এর “Accuracy and Stability of Numerical Algorithms” বইয়ে।

AI-এর যুগে ML coding ইন্টারভিউর প্রস্তুতি

AI সহকারী ধৈর্যশীল অনুশীলনের সঙ্গী হতে পারে, তবে ML কোডে তার ভুলগুলো চেনা দরকার। সে প্রায়ই পুরোনো API লেখে: scikit-learn-এর সরানো parameter (LogisticRegression-এর multi_class, যা 1.8.0-এ TypeError দেয়), pandas-এর সরানো append, বা পুরোনো XGBoost-এর argument. নেই এমন hyperparameter বানায়। ভাগের আগে পুরো ডেটায় scaler বা target encoding fit করে, test set দেখে tuning করে, আর training বা একটিমাত্র split-এর metric-কে চূড়ান্ত বলে। Class imbalance আর calibration ভুলে যায়, SHAP-এর মানকে কারণ হিসেবে পড়ে। আর বিশ্বাসযোগ্য দেখতে কিন্তু ভুল derivation দেয়, যেমন logistic loss-এর gradient-এ একটি বাড়তি σ′। Live ইন্টারভিউতে অনুমতি ছাড়া AI ব্যবহার করবেন না।

AI-কে সমাধান লিখতে না বলে আপনাকে চেপে ধরতে বলুন। তারপর তার প্রতিটি দাবি নিজে চালিয়ে মিলিয়ে নিন।

তুমি একজন কড়া ML coding ইন্টারভিউয়ার। আমার স্তর: [মাঝারি/উন্নত]। Version: Python 3.14, NumPy 2.4, pandas 3.0, scikit-learn 1.8. ১. আমাকে একটি from-scratch সমস্যা দাও (যেমন k-means, AUC, logistic regression, k-fold), ৩০ লাইনের মধ্যে করা যায় এমন। শুধু চাহিদা বলো, সমাধান নয়। ২. আমি লিখতে লিখতে প্রতিটি array-এর shape জিজ্ঞেস করো। আমি না বললে থামাও। ৩. আমি কোড দিলে জিজ্ঞেস করো: কীভাবে যাচাই করলে? খালি ইনপুট, একটি class, সমান মান আর বড় সংখ্যায় কী হবে? জটিলতা কত? ৪. তারপর একটি বাগওয়ালা ML কোড দাও (leak, ভুল metric বা shape) আর শুধু লক্ষণ বলো। ৫. কোনো API নিয়ে নিশ্চিত না হলে "নিশ্চিত নই" লেখো আর কোন অফিসিয়াল নথির পাতা দেখব বলো। ৬. শেষে ছয়টি ক্ষেত্রে ০ থেকে ২ নম্বর দাও: চাহিদা, shape, সঠিকতা, যাচাই, জটিলতা, ব্যাখ্যা।

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

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

ক্রম“NumPy দিয়ে একটি algorithm লিখুন” পেলে ভালো কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ইনপুট, label-এর মান আর shape নিয়ে প্রশ্ন করে চাহিদা মেলানো
  2. যে সূত্র কোড করবেন তা এক লাইনে লেখা আর ছোট উদাহরণে ফল বলা
  3. সরল কিন্তু সঠিক রূপ লিখে ধাপে ধাপে চালানো
  4. Library, হাতের হিসাব বা gradient check দিয়ে যাচাই
  5. জটিলতা আর বড় ডেটায় কী বদলাবেন তা বলা

(সহজ) X-এর shape (100, 3), w-এর (3,), আর y = df[["target"]].to_numpy()-এর shape (100, 1). (X @ w - y)-এর shape কী?

  • (100,)
  • (100, 1)
  • (100, 100)
  • ValueError

(মাঝারি) Logit z = [1000, 1001, 1002]. Softmax স্থিরভাবে হিসাবের জন্য প্রথমে z থেকে কোন সংখ্যা বিয়োগ করবেন, আর বিয়োগের পরে সবচেয়ে বড় exp কত?

  • 1001, আর সবচেয়ে বড় exp ≈ 2.718
  • 1002, আর সবচেয়ে বড় exp = 1
  • 1000, আর সবচেয়ে বড় exp ≈ 7.389
  • কিছুই না; float64 যথেষ্ট বড়

(মাঝারি) Positive-দের score [0.35, 0.8, 0.4], negative-দের [0.1, 0.4, 0.4]. সমান score-কে অর্ধেক ধরে ROC AUC কত?

  • 0.5
  • 0.5556
  • 0.7778
  • 0.6667

(মাঝারি) Lloyd-এর k-means-এ একটি assign আর একটি update ধাপের পরে inertia-র কী হতে পারে?

  • কমে বা সমান থাকে, কখনো বাড়ে না
  • এলোমেলোভাবে বাড়ে-কমে
  • প্রতিটি ধাপে কঠোরভাবে কমে
  • সবসময় global minimum-এ পৌঁছায়

(মাঝারি) প্রতিটি অর্ডারের feature হিসেবে df.groupby("cust")["taka"].transform("sum") ব্যবহার করা হলো, আর ভাগ এলোমেলো সারি ধরে। সমস্যা কী?

  • Shape মেলে না, তাই error আসবে
  • টাকার মান scale করা হয়নি
  • ক্রেতার ভবিষ্যতের অর্ডারও যোগ হয়েছে: temporal leakage
  • groupby খুব ধীর

(কঠিন) নিজের scikit-learn transformer-এ ছাঁটার সীমা কোথায় হিসাব করলে cross-validation সৎ থাকে?

  • __init__-এ, পুরো ডেটা থেকে
  • fit-এ, আর ফল cap_-এ রেখে
  • transform-এ, প্রতিবার নতুন করে
  • CV শুরুর আগে, পুরো ডেটায় একবার

(সহজ) ৫% positive-এর test set-এ একটি classifier-এর accuracy 0.95, আর roc_auc_score(y, model.predict(X)) ঠিক 0.5. সবচেয়ে সম্ভাব্য ব্যাখ্যা কী?

  • Model চমৎকার, AUC-এর হিসাবে বাগ
  • Model কিছুই শেখেনি, এটি নিশ্চিত
  • Test set খুব ছোট
  • Model সবাইকে ০ বলছে, আর AUC-কে score না দিয়ে label দেওয়া হয়েছে

(মাঝারি) n = 11 সারিকে 3-fold-এ ভাগ করলে, scikit-learn-এর KFold (shuffle ছাড়া) প্রতিটি test fold-এ কয়টি সারি রাখে?

  • ৪, ৪, ৩
  • ৩, ৩, ৫
  • ৩, ৪, ৪
  • ৩, ৩, ৩, একটি সারি বাদ

শব্দকোষ

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

মনে রাখুন

  1. Shape মুখে বলুন: (n,) আর (n, 1)-এর মিশ্রণ কোনো error ছাড়াই (n, n) বানায়; assert x.shape == ... সবচেয়ে সস্তা test.
  2. নিজের কোড প্রমাণ করুন: library, হাতের ছোট উদাহরণ, invariant (inertia কমে, softmax-এর যোগ ১), finite difference আর ধীর দ্বিতীয় রূপ।
  3. Numerical stability: সর্বোচ্চ বিয়োগ করে log-sum-exp, logaddexp, আর sigmoid-এর tanh রূপ; ব্রাউজারের Python-এ overflow নীরব।
  4. Sort করে একবারে: split finder আর AUC দুটিই O(n log n), সরল O(n²)-এর বদলে।
  5. সময় আর fold-এ সৎ থাকুন: feature শুধু অতীত দেখে, সব শেখা মান pipeline-এর fit-এ, AUC-কে score, আর প্রতিটি metric-এর পাশে baseline.

পরের পর্বে: পর্ব ৭৭ · ML ধারণা আর system design ইন্টারভিউ। Model বাছাই, imbalance, calibration, drift আর A/B test নিয়ে গভীর আলোচনা, সাথে পুরো ML system design-এর নকল ইন্টারভিউ।

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

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