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

Anomaly detection — isolation forest, LOF আর one-class SVM

Anomaly Detection — Isolation Forest, LOF and One-Class SVM

Taught in Bangla, this lesson finds point, contextual and collective anomalies in fictional shop transactions. It works isolation forest scores and LOF by hand, compares them with z-score, robust z, Mahalanobis and one-class SVM using precision at k and average precision, and picks a threshold from review and fraud costs, with real in-browser runs.

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

  • Anomaly-র তিন ধরন, আর outlier detection বনাম novelty detection-এর পার্থক্য
  • অল্প label দিয়ে detector মূল্যায়ন: precision at k, average precision, আর কেন ROC AUC এখানে বিভ্রান্ত করে
  • z-score, robust z আর Mahalanobis-χ² baseline, আর প্রতিটির দুর্বলতা
  • Isolation forest-এর path length, c(n) আর score হাতে হিসাব; LOF-এর k-distance, reachability আর local density পাঁচটি বিন্দুতে
  • One-class SVM-এর ν আর γ, তিন ধরনের anomaly-তে সব পদ্ধতির তুলনা, আর টাকার খরচ দিয়ে threshold বাছাই

AI-এর ভিত্তি কোর্সের পর্ব ৬০-এ precision, recall আর precision-recall curve, পর্ব ৬২-এ IQR আর robust z দিয়ে outlier খোঁজা, আর পর্ব ৬৫-এ label ছাড়া গঠন খোঁজা (k-means, DBSCAN) এসেছে। এই কোর্সের পর্ব ১৯ দেখিয়েছে Mahalanobis দূরত্ব আর χ² সীমা দিয়ে outlier ধরা, আর তার ফাঁদ (masking, বাঁকা ডেটা)। এই পর্ব সেখান থেকে এগোয়: Gaussian না ধরে, label প্রায় ছাড়াই, কীভাবে অস্বাভাবিক লেনদেন খোঁজা যায়, আর কোন পদ্ধতি কোন ধরনের অস্বাভাবিকতা ধরে। চালানো যায় এমন চারটি ব্লক পাতাতেই চলে (ব্রাউজারের Python: NumPy, SciPy, matplotlib আর scikit-learn), আর ব্লকগুলো একই ডেটা ভাগ করে নেয়; প্রথমবার scikit-learn নামতে প্রায় ১০ সেকেন্ড লাগে।

১ · Anomaly কী — তিন ধরন আর label ছাড়া সমস্যা

পাড়ার চায়ের দোকানের হিসাব খাতা ভাবুন। রোজ ৩০০ থেকে ৮০০ টাকার বিক্রি। একদিন লেখা ৮,০০০ টাকা: চোখে পড়ার মতো একক চরম মান। আরেকদিন ৭০০ টাকা, যা সাধারণ, কিন্তু সেদিন হরতাল, দোকান অর্ধেক বেলা বন্ধ ছিল: প্রসঙ্গ জানলে তবেই অস্বাভাবিক। আর এক সপ্তাহে প্রতিদিন রাত ৩টায় ঠিক ৪৯৯ টাকার বাকি: প্রতিটি আলাদাভাবে নিরীহ, একসঙ্গে দেখলে সন্দেহজনক একটি ছক। দোকানির কাছে কোনো “জালিয়াতির তালিকা” নেই; তিনি শুধু জানেন স্বাভাবিক দিন দেখতে কেমন।

Anomaly detection মানে স্বাভাবিক আচরণ থেকে অনেক আলাদা উদাহরণ খোঁজা। Douglas Hawkins-এর ১৯৮০ সালের বই Identification of Outliers-এর বহুল উদ্ধৃত সংজ্ঞা: এমন পর্যবেক্ষণ, যা বাকিদের থেকে এত আলাদা যে সন্দেহ জাগে এটি ভিন্ন কোনো প্রক্রিয়া থেকে এসেছে। Chandola, Banerjee আর Kumar-এর ২০০৯ সালের survey তিনটি ভাগ জনপ্রিয় করে:

Point anomaly

একটি সারি একাই অস্বাভাবিক, যেমন ৪৫,০০০ টাকার অর্ডার। Point anomaly সবচেয়ে সহজ, আর বেশিরভাগ পদ্ধতি এটিই খোঁজে।

Contextual anomaly

মানটি সাধারণ, কিন্তু প্রসঙ্গে নয়: দুপুরের লাঞ্চ-অর্ডারের দলে একটি অর্ডার যা অন্য লাঞ্চগুলোর চেয়ে দামি আর দূরের। Contextual anomaly ধরতে “কার সঙ্গে তুলনা” ঠিক করতে হয়।

Collective anomaly

অনেকগুলো সারি একসঙ্গে অস্বাভাবিক: রাত ৩টায় একই রকম ১২টি অর্ডার। Collective anomaly নিজেরাই একটি ঘন দল বানায়, তাই “প্রতিবেশী কাছে কি না” ধরনের পদ্ধতিকে ধোঁকা দেয়।

Label কতটা আছে, তা দিয়ে কাজটির ধরন বদলায়। সবচেয়ে গুরুত্বপূর্ণ পার্থক্যটি scikit-learn-এর docs-ও আলাদা করে লেখে:

পরিস্থিতিTraining ডেটাপ্রশ্নউদাহরণ
Outlier detectionএকটি ব্যাচ, ভেতরে anomaly মেশানো, label নেইএই ব্যাচের কোন সারিগুলো বাকিদের থেকে আলাদা?গত সপ্তাহের সব লেনদেন একসঙ্গে যাচাই
Novelty detectionশুধু যাচাই করা পরিষ্কার ডেটানতুন আসা সারিটি কি পরিচিত বণ্টনের বাইরে?পরিষ্কার পুরোনো মাস দিয়ে শিখে আজকের লেনদেন যাচাই
Supervisedঅনেক label-সহ জালিয়াতি আর সাধারণএটি কোন শ্রেণির?Class imbalance-সহ classification (পর্ব ৫০, Imbalanced ডেটা)

যথেষ্ট label থাকলে সাধারণত supervised classifier জেতে, কারণ সে জানে জালিয়াতি দেখতে কেমন। কিন্তু জালিয়াতি বিরল, নতুন ধরনের জালিয়াতির label আগে থাকে না, আর প্রতিটি label মানে কারও হাতে যাচাই। তাই এই পর্বের পদ্ধতিগুলো unsupervised: এরা শুধু “স্বাভাবিক কেমন” শেখে, আর প্রতিটি সারিকে একটি anomaly score দেয়। সিদ্ধান্ত (কোনটি যাচাই হবে) আসে সেই score-এর ওপর একটি সীমা থেকে, যা অংশ ৭-এর বিষয়।

২ · অল্প label দিয়ে মূল্যায়ন — precision at k আর PR AUC

Detector শেখে label ছাড়া, কিন্তু মূল্যায়নের জন্য কিছু যাচাই করা উদাহরণ লাগে: অডিট দলের একটি নমুনা, বা আগে ধরা পড়া জালিয়াতি। দুটি মাপ এখানে স্বাভাবিক।

Precision at k (P@k): score অনুযায়ী সাজিয়ে শীর্ষ k-টি সারির কত ভাগ সত্যিই anomaly. অডিট দল দিনে ৩০টি লেনদেন যাচাই করতে পারলে P@৩০ ঠিক সেই প্রশ্নের উত্তর: “আমাদের সময়ের কত ভাগ কাজে লাগল?” Average precision (AP, precision-recall curve-এর নিচের এলাকার একটি ধাপ-রূপ): প্রতিটি সত্যিকারের anomaly-র অবস্থানে precision মেপে তাদের গড়। একটি ছোট উদাহরণ: ১০টি লেনদেন সাজানো, সত্যিকারের জালিয়াতি ১, ৩ আর ৬ নম্বরে।

AP = (১/১ + ২/৩ + ৩/৬) / ৩ = (১ + ০.৬৬৭ + ০.৫) / ৩ = ০.৭২২

এখানে P@৩ = ২/৩, P@৫ = ২/৫ = ০.৪. এলোমেলো ক্রমের AP মোটামুটি anomaly-র অনুপাতের সমান (এখানে ০.৩), তাই AP-কে সবসময় সেই ভিত্তির সঙ্গে তুলনা করুন। scikit-learn-এর average_precision_score ঠিক এই ধাপ-সূত্র ব্যবহার করে (কোনো interpolation নেই), একটি ডেস্কটপ run-এ এই উদাহরণে ০.৭২২২ দেয়। ROC AUC কেন এখানে কম কাজের, তা ভাবনার প্রশ্ন ২-এ: anomaly ০.১%-এর মতো বিরল হলে হাজার হাজার সাধারণ সারির মধ্যে সামান্য ভুল-ক্রমও ROC-তে প্রায় দেখা যায় না, কিন্তু অডিট দলের কাজের তালিকা ভরিয়ে দেয়।

এই পর্বের ডেটা। বাজারঘরের কাল্পনিক ৭০৩টি লেনদেন, চারটি feature: টাকার অঙ্ক (ডানে বাঁকা, তাই log), ঘণ্টা (০–২৩), পণ্যের সংখ্যা, আর দূরত্ব (km). ৬০০টি সাধারণ গ্রাহক, আর ৮০টি অফিসের লাঞ্চ-অর্ডারের একটি আঁটসাঁট দল (প্রায় ৩৫০ টাকা, দুপুর ১টা, ২টি পণ্য, ১.৫ km). তার মধ্যে seed দিয়ে বসানো ২৩টি anomaly, তিন ধরনের:

ধরনসংখ্যাদেখতে কেমনকোন পদ্ধতিকে কঠিন পরীক্ষা দেয়
Global চরম৫৪৫,০০০ টাকা; ৪০টি পণ্য; ৬০ km; ভোর ৪টায় ৩০,০০০ টাকাপ্রায় কাউকেই নয়
ঘন জালিয়াতির দল১২রাত ৩টায় প্রায় ৪,৮০০ টাকা, ১টি পণ্য, ১৬ km, সবগুলো প্রায় হুবহুপ্রতিবেশী-ভিত্তিক পদ্ধতি (LOF)
Local৬লাঞ্চ-দলের ঠিক পাশে: ৪৭০ টাকা, দুপুর ১টা, ৩.২ kmগড়-ভিত্তিক পদ্ধতি (z, Mahalanobis)

Label (কোনটি বসানো anomaly) শুধু মূল্যায়নে ব্যবহার হবে, কোনো detector শেখার সময় তা দেখবে না। মোট anomaly ২৩টি, তাই প্রতিটি পদ্ধতির P@২৩ আর AP মাপা হবে, আর শীর্ষ ২৩-এ প্রতিটি ধরনের কতগুলো এল, তাও।

৩ · পরিসংখ্যানের baseline — z, robust z আর Mahalanobis

প্রথম চেষ্টা সবসময় সরল কিছু, কারণ জটিল পদ্ধতি তার চেয়ে ভালো কি না, তা মাপার ওই একটিই উপায়। প্রতিটি feature-এর z-score (x − গড়)/σ, আর সারির score হিসেবে সবচেয়ে বড় |z|. সমস্যা: চরম মান নিজেই গড় আর σ টেনে নেয়। নিচে আটটি অর্ডারের টাকা, যার একটি ৫০,০০০ আর একটি ২,৬০০.

x = 800, 900, 1000, 1100, 1200, 1300, 2600, 50000 (টাকা), n = 8
ধাপ
হিসাব
সংখ্যায়
মানে
  1. গড় আর σ. সাতটি সাধারণ অর্ডারের গড় প্রায় ১,২৭১, কিন্তু ৫০,০০০ যোগ হতেই গড় ৭,৩৬২.৫, আর σ (n দিয়ে ভাগ) ১৬,১২৪: একটি মান পুরো মাপকাঠি দখল করেছে।
  2. z-score. ৫০,০০০-এর z মাত্র ২.৬৪, আর ২,৬০০-এর z −০.৩০: সন্দেহজনক অর্ডারটি এখন “গড়ের নিচে”। একে বলে masking: বড় outlier ছোটটিকে লুকায়।
  3. ছাদ। Samuelson-এর অসমতা (১৯৬৮): n দিয়ে ভাগ করা σ-তে যেকোনো মানের |z| ≤ √(n − ১). n = ৮-এ ছাদ √৭ ≈ ২.৬৫, তাই “z > ৩ হলে চিহ্নিত” নিয়ম এখানে কখনোই কাউকে ধরবে না।
  4. Median আর MAD. Median ১,১৫০. প্রতিটি মান থেকে median-এর দূরত্ব সাজালে মাঝের দুটি ১৫০ আর ২৫০, তাই MAD = ২০০. চরম মানটি শুধু তালিকার শেষ ঘর দখল করেছে, মাঝের মান বদলায়নি।
  5. Robust z. ১.৪৮২৬ দিয়ে গুণ করলে normal ডেটায় MAD σ-এর অনুমান হয় (১.৪৮২৬ ≈ ১/০.৬৭৪৫). ২,৬০০ → ১,৪৫০/২৯৬.৫২ = ৪.৮৯; ৫০,০০০ → ১৬৪.৭.
  6. সিদ্ধান্ত। Iglewicz আর Hoaglin (১৯৯৩)-এর প্রচলিত সীমা ৩.৫ ধরলে robust z দুটি অর্ডারই ধরে, সাধারণ z একটিও না।

এক feature-এ robust z স্পষ্টভাবে ভালো। কিন্তু অনেক feature-এ দুটি নতুন সমস্যা আসে। প্রথমত, প্রতিটি feature আলাদা দেখলে “সম্পর্ক ভাঙা” সারি ধরা পড়ে না; সমাধান Mahalanobis দূরত্ব, যার বর্গ Gaussian ডেটায় χ²d মানে (পর্ব ১৯)। চার feature-এ ৯৯% সীমা ১৩.২৮. দ্বিতীয়ত, MAD ধরে নেয় ডেটার মাঝের অংশ মোটামুটি প্রতিসম; বাঁকা বা পূর্ণসংখ্যার feature-এ সে লেজকে অস্বাভাবিক ভাবে। নিচের ব্লক ডেটা বানায় (পরের তিনটি ব্লক এটিই ব্যবহার করবে) আর তিনটি baseline মাপে।

import numpy as np
from scipy.stats import chi2
from sklearn.metrics import average_precision_score

def make_data(seed):                      # কাল্পনিক বাজারঘর: টাকা, ঘণ্টা, পণ্য, km
    r = np.random.default_rng(seed)
    a = np.c_[r.lognormal(7, .45, 600), r.normal(18, 3.5, 600),
              r.poisson(3, 600) + 1, r.gamma(4, 1.5, 600)]   # সাধারণ গ্রাহক
    b = np.c_[r.normal(350, 15, 80), r.normal(13, .5, 80),
              np.full(80, 2), r.normal(1.5, .2, 80)]         # অফিসের লাঞ্চ: আঁটসাঁট দল
    g = [[45000, 20, 4, 6], [1200, 18, 40, 5], [900, 19, 3, 60],
         [30000, 4, 25, 2], [2000, 22, 2, 45]]               # ১) global চরম
    f = np.c_[r.normal(4800, 40, 12), r.normal(3, .3, 12),
              np.ones(12), r.normal(16, .3, 12)]             # ২) ঘন জালিয়াতির দল
    l = np.c_[r.normal(470, 10, 6), r.normal(13, .4, 6),
              np.full(6, 2), r.normal(3.2, .1, 6)]           # ৩) local: লাঞ্চ-দলের পাশে
    X = np.vstack([a, b, g, f, l])
    X[:, 0] = np.log(X[:, 0])             # টাকা ডানে বাঁকা, তাই log
    return X, np.repeat([0, 0, 1, 2, 3], [600, 80, 5, 12, 6])

X, kind = make_data(42); y = (kind > 0).astype(int)     # label শুধু মূল্যায়নে
def report(name, score, k=23):            # score বড় = বেশি সন্দেহজনক
    top = np.argsort(-score)[:k]
    g, c, l = (np.sum(kind[top] == t) for t in (1, 2, 3))
    print(f"{name:13s} P@{k}={y[top].mean():.2f} AP={average_precision_score(y, score):.2f}"
          f"  top-{k} has global {g}/5 cluster {c}/12 local {l}/6")
z = np.abs((X - X.mean(0)) / X.std(0)).max(1)            # প্রতিটি feature-এর z, সবচেয়ে বড়টি
med = np.median(X, 0); mad = np.median(np.abs(X - med), 0)
Xs = (X - med) / (1.4826 * mad)                          # robust z (পরের ব্লকেও লাগবে)
D = X - X.mean(0)
d2 = np.einsum("ij,ij->i", D @ np.linalg.inv(np.cov(X, rowvar=False)), D)
cut = chi2.ppf(0.99, df=4)
print(f"{len(X)} rows, {y.sum()} planted | chi2(4) 99% cut {cut:.2f}, d2 flags {np.sum(d2 > cut)}")
for name, s in (("max |z|", z), ("max robust z", np.abs(Xs).max(1)), ("Mahalanobis", d2)):
    report(name, s)

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

703 rows, 23 planted | chi2(4) 99% cut 13.28, d2 flags 18
max |z|       P@23=0.74 AP=0.71  top-23 has global 5/5 cluster 12/12 local 0/6
max robust z  P@23=0.43 AP=0.48  top-23 has global 5/5 cluster 5/12 local 0/6
Mahalanobis   P@23=0.74 AP=0.76  top-23 has global 5/5 cluster 12/12 local 0/6

Mahalanobis আর z: শীর্ষ ২৩-এ ১৭টি সত্যিকারের anomaly (০.৭৪): সব global আর পুরো জালিয়াতির দল, কারণ দলটি রাত ৩টায়, যা ঘণ্টার বণ্টনের অনেক বাইরে। χ² সীমা ১৮টি সারি চিহ্নিত করে, যার ১৭টি সত্যিকারের (একটি ডেস্কটপ run-এ গোনা)। Local ৬টি: একটিও নয়। এরা গড়ের কাছাকাছি, শুধু লাঞ্চ-দলের তুলনায় অস্বাভাবিক; গড় আর একটি covariance দিয়ে পুরো ডেটা বর্ণনা করলে এই “স্থানীয়” তথ্য হারায়। Robust z-এর পতন (০.৪৩): পণ্যের সংখ্যা পূর্ণসংখ্যা, তার MAD মাত্র ১, তাই ৯–১০টি পণ্যের সাধারণ অর্ডারই robust z ৩.৪–৪.০৫ পায়; দূরত্বের লম্বা ডান লেজও একই কাজ করে। ভুল চিহ্নিত ১৩টি সারির প্রতিটির সর্বোচ্চ robust z আসে পণ্য বা দূরত্ব থেকে (ডেস্কটপ run-এ গোনা)। শিক্ষা: robust মানে সব জায়গায় ভালো নয়; robust z ধরে নেয় মাঝের অংশ প্রতিসম আর ঘন।

৪ · Isolation forest — অস্বাভাবিককে একা করা সহজ

“বিশ প্রশ্ন” খেলার মতো ভাবুন। ক্লাসের কোনো একজনকে খুঁজতে হ্যাঁ/না প্রশ্ন করতে হবে। “সে কি ৬ ফুটের বেশি লম্বা?” এক প্রশ্নেই একমাত্র লম্বা ছেলেটি আলাদা হয়ে যায়। কিন্তু গড়পড়তা উচ্চতা, গড়পড়তা বয়সের কাউকে আলাদা করতে অনেক প্রশ্ন লাগে। অস্বাভাবিক মানে “অল্প প্রশ্নে আলাদা করা যায়”।

Fei Tony Liu, Kai Ming Ting আর Zhi-Hua Zhou ২০০৮ সালে (ICDM সম্মেলনে) এই ধারণাকে একটি পদ্ধতি বানান। একটি isolation tree বানাতে: একটি এলোমেলো feature নিন, তার বর্তমান সর্বনিম্ন আর সর্বোচ্চের মাঝে একটি এলোমেলো কাটার জায়গা নিন, ডেটা দুই ভাগ করুন, আর প্রতিটি ভাগে আবার একই কাজ, যতক্ষণ প্রতিটি বিন্দু একা না হয় বা গভীরতার সীমায় না পৌঁছায়। কোনো impurity, কোনো label নেই (পর্ব ৩২-এর CART-এর মতো “সবচেয়ে ভালো কাট” খোঁজাও নেই)। একটি বিন্দুর path length h(x) হলো root থেকে তার পাতায় পৌঁছাতে কয়টি কাট লাগল।

এলোমেলো কাটে অস্বাভাবিক বিন্দু আগে একা হয়

বাঁয়ে ডেটা আর কাট, ডানে একই কাটের গাছ (ছবি মাপমতো নয়)

An isolation tree: the anomaly is isolated after 2 cuts, a normal point needs about 8 ডেটা আর কাট isolation tree ১ ২ সবুজ: কাট ৩…৮, অনেক কাট কাট ১ কাট ২ … বাকি h = 2 h ≈ 8 বা বেশি
ছবি ৪.১ — লাল বিন্দুটি বাকিদের থেকে দূরে, তাই এলোমেলো কাটের সম্ভাবনা বেশি যে সেটি তাকে আলাদা করবে: এখানে দুটি কাটেই সে একা। মেঘের ভেতরের সবুজ বিন্দু একা করতে অনেক কাট লাগে। অনেক গাছের গড় path length ছোট হলে বিন্দুটি অস্বাভাবিক।

একটি গাছ এলোমেলো, তাই isolation forest অনেক গাছের (scikit-learn-এ ডিফল্ট ১০০টি) গড় E[h(x)] নেয়। প্রতিটি গাছ পুরো ডেটা নয়, ২৫৬টি সারির একটি ছোট নমুনায় বানানো (max_samples="auto" মানে min(২৫৬, n)). ছোট নমুনা অদ্ভুত শোনালেও এটি সাহায্য করে: ঘন জালিয়াতির দলের অনেক সদস্য নমুনায় না আসায় দলটি পাতলা হয়, masking কমে। গভীরতার সীমা ⌈log2 ২৫৬⌉ = ৮: অস্বাভাবিকেরা এর আগেই একা হয়, তাই সাধারণ বিন্দুর জন্য বাড়তি কাট খরচ করার দরকার নেই।

Path length-কে score বানাতে একটি মাপকাঠি লাগে: n-টি বিন্দুর একটি এলোমেলো গাছে গড়ে path কত? Binary search tree-তে ব্যর্থ খোঁজের গড় দৈর্ঘ্য থেকে আসে c(n):

c(n) = 2H(n − 1) − 2(n − 1)/n, H(i) ≈ ln i + 0.5772, s(x, n) = 2−E[h(x)] / c(n)

এখানে H(i) হলো harmonic number ১ + ১/২ + … + ১/i, আর ০.৫৭৭২ Euler–Mascheroni ধ্রুবক। c(n)-কে প্রায় ২ ln n ভাবতে পারেন। Score s সবসময় ০ থেকে ১-এর মধ্যে। পাতায় পৌঁছে থেমে গেলে (গভীরতার সীমায়) সেখানে বাকি m-টি বিন্দুর জন্য c(m) যোগ হয়, যেন গাছটি আরও বাড়ানো হলে গড়ে কত লাগত।

c(n) = 2H(n − 1) − 2(n − 1)/n, s = 2^(−E[h]/c(n))
ধাপ
হিসাব
সংখ্যায়
মানে
  1. Harmonic number. ln ২৫৫ = ৫.৫৪১৩, সঙ্গে Euler–Mascheroni ধ্রুবক ০.৫৭৭২ যোগ করে H(২৫৫) ≈ ৬.১১৮৫.
  2. দুই পদ। ২ × ৬.১১৮৫ = ১২.২৩৭০, আর ২ × ২৫৫/২৫৬ = ১.৯৯২২.
  3. c(২৫৬) = ১০.২৪৫. ২৫৬টি সারির নমুনায় একটি এলোমেলো বিন্দু একা করতে গড়ে প্রায় ১০টি কাট লাগে। ছোট n-এ c(২) = ১ আর c(১) = ০ সংজ্ঞা অনুযায়ী।
  4. সাধারণ সারি। পরের ব্লকে বাজারঘরের প্রথম সারির গড় path ১৪.২২, অনুপাত ১.৩৮৮, score ০.৩৮২: ০.৫-এর নিচে, তাই সাধারণ।
  5. Global চরম সারি। ৪৫,০০০ টাকার অর্ডারের গড় path ৮.০৮, অনুপাত ০.৭৮৯, score ০.৫৭৯: ০.৫-এর ওপরে। এটি “নিশ্চিত” anomaly-র মতো উঁচু নয়, কারণটি পরের ব্লকের explain-এ।
  6. সীমা। E[h] = ২ হলে s = ০.৮৭৩; E[h] = c(n) হলে ঠিক ০.৫; E[h] যত বড়, s তত শূন্যের দিকে। মূল প্রবন্ধের নির্দেশনা: সব score ০.৫-এর কাছে হলে ডেটায় স্পষ্ট anomaly নেই।

নিচের ব্লক একটি ক্ষুদ্র isolation tree নিজে লেখে: পুরো গাছ জমা না রেখে শুধু একটি বিন্দুর পথ অনুসরণ করে। তারপর ১০০টি গাছের গড় path থেকে score, আর scikit-learn-এর IsolationForest-এর সঙ্গে তুলনা। score_samples উল্টো চিহ্নে −s দেয় (বড় মানে বেশি স্বাভাবিক), তাই আমরা মাইনাস দিয়ে উল্টাই।

from sklearn.ensemble import IsolationForest

def c(n):                                 # n বিন্দুর গাছে গড় path length
    return 0.0 if n <= 1 else 1.0 if n == 2 else 2 * (np.log(n - 1) + np.euler_gamma) - 2 * (n - 1) / n

def path(x, S, rng, depth=0, limit=8):    # একটি এলোমেলো গাছে x কত ধাপে একা হয়
    if len(S) <= 1 or depth >= limit:
        return depth + c(len(S))          # থেমে গেলে বাকি গাছের গড় অনুমান যোগ
    j = rng.integers(S.shape[1])          # এলোমেলো feature
    lo, hi = S[:, j].min(), S[:, j].max()
    if lo == hi:
        return depth + c(len(S))
    t = rng.uniform(lo, hi)               # এলোমেলো কাটার জায়গা
    side = S[S[:, j] < t] if x[j] < t else S[S[:, j] >= t]
    return path(x, side, rng, depth + 1, limit)

rng = np.random.default_rng(0)
for i in (0, 680):                        # একটি সাধারণ সারি আর ৪৫,০০০ টাকার সারি
    h = []
    for _ in range(100):                  # ১০০টি গাছ, প্রতিটি ২৫৬ সারির নমুনায়
        S = Xs[rng.choice(len(Xs), 256, replace=False)]
        h.append(path(Xs[i], S, rng))
    Eh = np.mean(h)
    print(f"row {i}: first paths {np.round(h[:5], 1)}  E[h]={Eh:.2f}  "
          f"score=2^(-E[h]/c(256))={2 ** (-Eh / c(256)):.3f}")
iso = IsolationForest(n_estimators=100, max_samples=256, random_state=42).fit(Xs)
print(f"c(256)={c(256):.3f} | sklearn score: row 0 {-iso.score_samples(Xs[[0]])[0]:.3f},"
      f" row 680 {-iso.score_samples(Xs[[680]])[0]:.3f}")
report("IsolationForest", -iso.score_samples(Xs))
for cont in ("auto", 0.02, 0.05):         # contamination শুধু কাটার সীমা বদলায়
    m = IsolationForest(contamination=cont, random_state=42).fit(Xs)
    print(f"contamination={cont}: offset {m.offset_:.3f}, flagged {np.sum(m.predict(Xs) == -1)}")

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

row 0: first paths [13.6 15.1 13.5 16.6 15.3]  E[h]=14.22  score=2^(-E[h]/c(256))=0.382
row 680: first paths [ 1.  14.2  5.  14.  12.2]  E[h]=8.08  score=2^(-E[h]/c(256))=0.579
c(256)=10.245 | sklearn score: row 0 0.390, row 680 0.591
IsolationForest P@23=0.74 AP=0.75  top-23 has global 5/5 cluster 12/12 local 0/6
contamination=auto: offset -0.500, flagged 88
contamination=0.02: offset -0.649, flagged 15
contamination=0.05: offset -0.547, flagged 36

নিজের গাছ বনাম scikit-learn: ০.৩৮২ বনাম ০.৩৯০, আর ০.৫৭৯ বনাম ০.৫৯১: আলাদা এলোমেলো সংখ্যা, একই ছবি। ৪৫,০০০ টাকার সারির path: কখনো ১ (প্রথম কাটই টাকার feature-এ পড়েছে), কখনো ১৪.২ (চারটি feature-এর বাকি তিনটি একদম সাধারণ, সেখানে কাট পড়লে সে মেঘের ভেতরেই থাকে)। একটি মাত্র feature-এ চরম হলে এলোমেলো feature বাছাই সেই সংকেতকে পাতলা করে; অপ্রাসঙ্গিক feature যত বেশি, তত বেশি। ফল: P@২৩ = ০.৭৪, baseline-এর সমান, আর local ৬টি আবার শূন্য। Contamination: "auto" মানে s > ০.৫ হলেই চিহ্নিত, এখানে ৮৮টি সারি (১২.৫%), যদিও আসল anomaly ৩.৩%. ০.০২ দিলে ১৫টি, ০.০৫ দিলে ৩৬টি। Score একই থাকে; contamination শুধু offset_, অর্থাৎ predict-এর সীমা, ঠিক করে।

৫ · Local Outlier Factor — প্রতিবেশীর তুলনায় কতটা একা

Global পদ্ধতি পুরো ডেটার একটি “কেন্দ্র” থেকে দূরত্ব মাপে। কিন্তু বাজারঘরের লাঞ্চ-দল খুব ঘন (৩৫০ ± ১৫ টাকা), আর সাধারণ গ্রাহকেরা ছড়ানো। লাঞ্চ-দলের ঠিক পাশের একটি বিন্দু global মাপে নিরীহ, কিন্তু তার প্রতিবেশীদের তুলনায় সে অনেক একা। Markus Breunig, Hans-Peter Kriegel, Raymond Ng আর Jörg Sander ২০০০ সালে (SIGMOD) এই ধারণা থেকে Local Outlier Factor দেন। চারটি সংজ্ঞা, ক্রমানুসারে:

  • k-distance(p): p থেকে তার k-তম নিকটতম প্রতিবেশীর দূরত্ব। Nk(p) হলো সেই k-টি প্রতিবেশী।
  • Reachability distance: reachk(p, o) = max(k-distance(o), d(p, o)). o-এর খুব কাছের বিন্দুগুলোর দূরত্ব o-এর k-distance পর্যন্ত “সমান করে” ধরা হয়, যাতে ঘন এলাকার ছোট ওঠানামা ফল না নাড়ায়।
  • Local reachability density: lrd(p) = ১ / (Nk(p)-এর ওপর reachk(p, o)-এর গড়)। প্রতিবেশী যত কাছে, ঘনত্ব তত বেশি।
  • LOF(p) = প্রতিবেশীদের lrd-এর গড় / lrd(p). প্রায় ১ মানে প্রতিবেশীর মতোই ঘন; ১-এর অনেক বেশি মানে প্রতিবেশীরা তার চেয়ে অনেক ঘন জায়গায় থাকে।
A = 0, B = 1, C = 2, D = 3, E = 8; k = 2
ধাপ
হিসাব
সংখ্যায়
মানে
  1. প্রতিবেশী। k = ২. D-এর দুই প্রতিবেশী C (১) আর B (২), কারণ E আছে ৫ দূরে। E-এর প্রতিবেশী D (৫) আর C (৬)।
  2. k-distance. দ্বিতীয় প্রতিবেশীর দূরত্ব: A ২, B ১, C ১, D ২, আর E ৬.
  3. Reachability. E থেকে: reach(E, D) = max(kd(D) = ২, ৫) = ৫, reach(E, C) = max(১, ৬) = ৬. A থেকে: reach(A, B) = max(১, ১) = ১, reach(A, C) = max(১, ২) = ২.
  4. Local density. A-র reach-এর গড় ১.৫, lrd = ০.৬৬৭; B, C, D-ও একই হিসাবে ০.৬৬৭. E-এর গড় ৫.৫, lrd = ০.১৮২.
  5. LOF(E). E-এর প্রতিবেশী D আর C-র lrd-এর গড় ০.৬৬৭, ভাগ E-এর ০.১৮২: LOF = ৩.৬৬৭.
  6. বাকিরা। A–D-এর LOF ঠিক ১. scikit-learn-এর LocalOutlierFactor(n_neighbors=2) একটি ডেস্কটপ run-এ [১, ১, ১, ১, ৩.৬৬৭] দেয় (negative_outlier_factor_-এ চিহ্ন উল্টো)।

LOF-এর শক্তি তার দুর্বলতাও। “Local” মানে k-টি প্রতিবেশীর জগৎ। ঘন জালিয়াতির দলে ১২টি সদস্য; k ≤ ১১ হলে প্রতিটি সদস্যের সব প্রতিবেশী দলের ভেতরেই, সবার ঘনত্ব সমান, LOF ≈ ১: দলটি নিজেকে “স্বাভাবিক” প্রমাণ করে। এটি collective anomaly-র masking. আর ঘনত্বের অনুপাত বলে LOF-এর কোনো সর্বজনীন সীমা নেই: এক ডেটায় ১.৫ অস্বাভাবিক, আরেকটিতে ২-ও সাধারণ। scikit-learn-এর ডিফল্ট n_neighbors=20, আর মূল প্রবন্ধ কয়েকটি k-এর সর্বোচ্চ LOF নেওয়ার পরামর্শ দেয়।

novelty=False বনাম novelty=True. ডিফল্ট novelty=False হলো outlier detection: fit_predict(X) একই ব্যাচকে নিজের সঙ্গে তুলনা করে, আর score থাকে negative_outlier_factor_-এ; নতুন ডেটায় predict করা যায় না। novelty=True হলো novelty detection: পরিষ্কার ডেটায় fit, তারপর নতুন ডেটায় predict, decision_function বা score_samples. Training ডেটাতেই predict চালালে ভুল ফল আসে, কারণ প্রতিটি বিন্দু নিজেকেই নিজের প্রতিবেশী হিসেবে পায়; docs এ নিয়ে স্পষ্ট সতর্ক করে।

৬ · One-class SVM, আর তিন ধরনের anomaly-তে সবার তুলনা

One-class SVM (Schölkopf, Platt, Shawe-Taylor, Smola আর Williamson; Neural Computation, ২০০১) ভিন্ন প্রশ্ন করে: স্বাভাবিক ডেটার চারপাশে সবচেয়ে ছোট কোন সীমা আঁকা যায়? পর্ব ২৯-এর RBF kernel দিয়ে ডেটাকে একটি feature space-এ নিয়ে, সেখানে মূলবিন্দু থেকে ডেটাকে যতটা সম্ভব দূরে সরানো একটি hyperplane খোঁজা হয়। ফলাফলের function f(x) = ∑ αi k(xi, x) − ρ: ধনাত্মক হলে ভেতরে (সাধারণ), ঋণাত্মক হলে বাইরে। দুটি knob:

ν (nu), ০ থেকে ১

Training ডেটার সর্বোচ্চ কত ভাগ সীমার বাইরে থাকতে পারে, আর সর্বনিম্ন কত ভাগ support vector হবে: ν দুটিরই সীমা (বড় n-এ)। ডিফল্ট ০.৫, যা anomaly খোঁজার জন্য প্রায় সবসময় অনেক বেশি।

γ (gamma)

RBF-এর প্রস্থ। ছোট γ মানে মসৃণ, প্রায় উপবৃত্তাকার সীমা; বড় γ মানে প্রতিটি বিন্দুর চারপাশে ছোট বুদবুদ। ডিফল্ট "scale" = ১/(feature সংখ্যা × X-এর variance).

Tax আর Duin-এর SVDD (support vector data description, ২০০৪) একই কাজ করে ডেটার চারপাশে সবচেয়ে ছোট গোলক খুঁজে; RBF kernel-এ দুটি পদ্ধতি সমতুল্য। Kernel matrix-এর জন্য খরচ n-এর সঙ্গে প্রায় বর্গ থেকে ঘন হারে বাড়ে, তাই বড় ডেটায় পর্ব ৩১-এর Nyström বা random features দিয়ে SGDOneClassSVM একটি বিকল্প।

এবার সব একসঙ্গে। নিচের ব্লক LOF (তিনটি k, আর novelty মোডে আগের মাসের পরিষ্কার লেনদেনে শিখিয়ে) আর one-class SVM (একই ব্যাচে দুটি γ, আর novelty মোডে) মাপে, তারপর দুটি feature-এ (log টাকা আর দূরত্ব) তিন পদ্ধতির score-এর contour আঁকে।

import matplotlib.pyplot as plt
from sklearn.neighbors import LocalOutlierFactor
from sklearn.svm import OneClassSVM

for k in (5, 10, 20):                     # novelty=False: এই ব্যাচকেই নিজের সঙ্গে তুলনা
    lof = LocalOutlierFactor(n_neighbors=k, algorithm="kd_tree")
    lof.fit_predict(Xs)
    report(f"LOF k={k}", -lof.negative_outlier_factor_)
H, hk = make_data(7)                      # আগের মাসের যাচাই করা পরিষ্কার লেনদেন
Hs = ((H - med) / (1.4826 * mad))[hk == 0]
lofn = LocalOutlierFactor(n_neighbors=20, novelty=True, algorithm="kd_tree").fit(Hs)
report("LOF novelty", -lofn.score_samples(Xs))
for g in (0.02, 1.0):                     # এই ব্যাচেই fit, γ বদলে
    oc = OneClassSVM(nu=0.05, gamma=g).fit(Xs)
    report(f"OCSVM g={g}", -oc.score_samples(Xs))
    print(f"   support vectors {len(oc.support_)}, flagged {np.mean(oc.predict(Xs) == -1):.3f}")
report("OCSVM novelty", -OneClassSVM(nu=0.05, gamma="scale").fit(Hs).score_samples(Xs))

P, HP = Xs[:, [0, 3]], Hs[:, [0, 3]]      # ছবির জন্য দুটি feature: log টাকা, km
gx, gy = np.meshgrid(np.linspace(-4, 9, 120), np.linspace(-3, 20, 120))
G = np.c_[gx.ravel(), gy.ravel()]
models = {"IsolationForest": IsolationForest(random_state=42),
          "LOF k=20 (novelty)": LocalOutlierFactor(20, novelty=True, algorithm="kd_tree"),
          "One-class SVM nu=0.05": OneClassSVM(nu=0.05, gamma=0.5)}
fig, axes = plt.subplots(1, 3, figsize=(11, 3.8), sharey=True)
for ax, (name, m) in zip(axes, models.items()):
    Z = -m.fit(HP).score_samples(G).reshape(gx.shape)   # বড় = বেশি অস্বাভাবিক
    ax.contourf(gx, gy, Z, levels=12, cmap="Greys")
    for t, mk, lab in ((0, ".", "normal"), (1, "s", "global"), (2, "^", "cluster"), (3, "*", "local")):
        ax.scatter(*P[kind == t].T, s=6 if t == 0 else 30, marker=mk, label=lab)
    ax.set_title(name, fontsize=10); ax.set_xlabel("log amount (robust z)")
axes[0].set_ylabel("distance (robust z)"); axes[0].legend(fontsize=7, loc="upper left")
fig.subplots_adjust(left=0.06, right=0.99, bottom=0.14, wspace=0.08)
plt.show()

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

LOF k=5       P@23=0.22 AP=0.24  top-23 has global 5/5 cluster 0/12 local 0/6
LOF k=10      P@23=0.48 AP=0.42  top-23 has global 5/5 cluster 0/12 local 6/6
LOF k=20      P@23=0.74 AP=0.83  top-23 has global 5/5 cluster 6/12 local 6/6
LOF novelty   P@23=0.87 AP=0.88  top-23 has global 5/5 cluster 9/12 local 6/6
OCSVM g=0.02  P@23=0.52 AP=0.52  top-23 has global 5/5 cluster 7/12 local 0/6
   support vectors 38, flagged 0.051
OCSVM g=1.0   P@23=0.00 AP=0.04  top-23 has global 0/5 cluster 0/12 local 0/6
   support vectors 230, flagged 0.171
OCSVM novelty P@23=0.74 AP=0.76  top-23 has global 5/5 cluster 12/12 local 0/6

LOF-এর k আর দলের আকার: k = ৫-এ জালিয়াতির দল (১২) আর local দল (৬) দুটিই নিজেদের ভেতরে প্রতিবেশী পায়, দুটিই লুকায়। k = ১০ local দলের (৬) চেয়ে বড়, তাই ৬টিই ধরা পড়ে, কিন্তু ১২-র দল তখনো লুকানো। k = ২০ দুটিরই চেয়ে বড়: AP ০.৮৩, এখন পর্যন্ত সর্বোচ্চ। Novelty মোড: আগের মাসের পরিষ্কার ডেটায় শেখালে জালিয়াতির দল আর নিজের প্রতিবেশী হতে পারে না; P@২৩ = ০.৮৭ (২০টি সঠিক), AP ০.৮৮. One-class SVM, দূষিত ব্যাচে: γ = ০.০২-এ ৩৮টি support vector, আর চিহ্নিত ৫.১%, ν = ০.০৫-এর প্রায় সমান, যেমন তত্ত্ব বলে। γ = ১-এ ২৩০টি support vector, আর P@২৩ শূন্য: প্রতিটি global outlier training-এ ছিল, তাই প্রত্যেকে নিজের বুদবুদ পেয়ে “স্বাভাবিক” হয়ে গেছে। ১৭.১% চিহ্নিত দেখে ν-এর নিয়ম ভাঙা মনে হলেও একটি ডেস্কটপ run-এ দেখা গেছে, এদের সবার decision মান −০.০০১-এর চেয়েও শূন্যের কাছে: সীমার ওপরের support vector, solver-এর সহনসীমার (tol = ০.০০১) ভেতরে। Novelty মোডে SVM ০.৭৪-এ ওঠে, কিন্তু local ৬টি ধরে না: মসৃণ সীমা লাঞ্চ-দলের পাশের ছোট ফাঁক দেখতে পায় না।

ছবিতে (গাঢ় = বেশি অস্বাভাবিক) তিনটি স্বভাব স্পষ্ট। Isolation forest-এর contour অক্ষের সমান্তরাল ডোরা আর আয়তক্ষেত্রের মতো, কারণ প্রতিটি কাট একটি feature-এ। LOF-এর contour মেঘের আকার মেনে ধীরে ধীরে গাঢ় হয়, আর লাঞ্চ-দলের চারপাশে আলাদা একটি আঁটসাঁট সাদা এলাকা রাখে। One-class SVM-এর সীমা একটি মসৃণ বলয়, যার বাইরে প্রায় সব সমান গাঢ়।

পদ্ধতি (এই ডেটায়)P@২৩APGlobal / দল / localমূল দুর্বলতা
max |z|০.৭৪০.৭১৫ / ১২ / ০চরম মান σ ফুলায়; সম্পর্ক দেখে না
max robust z০.৪৩০.৪৮৫ / ৫ / ০বাঁকা বা পূর্ণসংখ্যার feature-এ ভুল সতর্কতা
Mahalanobis০.৭৪০.৭৬৫ / ১২ / ০একটি কেন্দ্র, একটি উপবৃত্ত
Isolation forest০.৭৪০.৭৫৫ / ১২ / ০অক্ষ-সমান্তরাল কাট; local ধরে না
LOF k = ২০, ব্যাচে০.৭৪০.৮৩৫ / ৬ / ৬k-নির্ভর; ঘন দল লুকায়
LOF, novelty০.৮৭০.৮৮৫ / ৯ / ৬পরিষ্কার পুরোনো ডেটা লাগে
One-class SVM, novelty০.৭৪০.৭৬৫ / ১২ / ০γ আর ν-এ খুব সংবেদনশীল; বড় n-এ ধীর

এই টেবিল একটি কাল্পনিক ডেটার একটি seed-এর ফল, সাধারণ রায় নয়। তবু একটি শিক্ষা টেকসই: কোন পদ্ধতি জিতবে, তা নির্ভর করে আপনি কোন ধরনের anomaly খুঁজছেন তার ওপর। Global চরম মান সবাই ধরে; local অস্বাভাবিকতা শুধু LOF; আর ঘন দলের জন্য পরিষ্কার training ডেটা বা বড় k লাগে। বাস্তবে কয়েকটি পদ্ধতির score-এর rank গড় করা (ensemble) প্রায়ই একক পদ্ধতির চেয়ে স্থিতিশীল।

৭ · Contamination, threshold আর টাকার হিসাব

Score একটি ক্রম দেয়; সিদ্ধান্তের জন্য একটি সীমা লাগে। scikit-learn-এর IsolationForest আর LocalOutlierFactor-এ (OneClassSVM-এ এর জায়গায় ν) contamination parameter আছে, যা training score-এর একটি quantile থেকে offset_ ঠিক করে। সমস্যা: আসল anomaly-র হার কেউ জানে না, আর "auto"-র সীমা (isolation forest-এ s > ০.৫, LOF-এ LOF > ১.৫) কোনো ব্যবসায়িক যুক্তি থেকে আসেনি। সীমার আসল প্রশ্ন হলো খরচ: একটি জালিয়াতি না ধরলে কত ক্ষতি, আর একটি সাধারণ লেনদেন হাতে যাচাই করতে কত খরচ (পর্ব ০২-এর cost matrix-এর মতো)।

মোট খরচ(t) = ১৫০ × (চিহ্নিত সংখ্যা) + ৫,০০০ × (ধরা না পড়া anomaly)

সংখ্যাগুলো কাল্পনিক: একটি হাতে-যাচাইয়ে কর্মীর সময় ১৫০ টাকা, আর একটি জালিয়াতি গড়ে ৫,০০০ টাকার ক্ষতি। নিচের ব্লক LOF novelty-র score নিয়ে অডিট করা এই ব্যাচে (label জানা) সবচেয়ে সস্তা সীমা খোঁজে, তারপর সেই score-এর মান পরের সপ্তাহের নতুন ব্যাচে প্রয়োগ করে, আর contamination দিয়ে বাছা সীমার সঙ্গে তুলনা করে।

MISS, REVIEW = 5000, 150                  # কাল্পনিক টাকা: ধরা না পড়া জালিয়াতি, একটি হাতে-যাচাই
def cost(score, y, t):                    # সীমা t বা তার ওপরে সব হাতে যাচাই
    flag = score >= t
    missed = np.sum(y[~flag])
    return REVIEW * flag.sum() + MISS * missed, flag.sum(), missed

s = -lofn.score_samples(Xs)               # LOF novelty-র score, অডিট করা এই ব্যাচ
cands = np.sort(s)[::-1][:80]             # সম্ভাব্য সীমা: শীর্ষ ৮০টি score
table = [cost(s, y, t) + (t,) for t in cands]
best = min(table)
print(f"no review at all: {MISS * y.sum():,} taka")
for k in (5, 10, 20, 23, 30, 50):
    tot, f, m, t = table[k - 1]
    print(f"review top {k:2d}: flagged {f:2d} missed {m:2d} cost {tot:6,} taka")
print(f"best on audit batch: threshold {best[3]:.2f}, flagged {best[1]}, "
      f"missed {best[2]}, cost {best[0]:,} taka")

Xn, kn = make_data(8)                     # পরের সপ্তাহ: নতুন seed, একই ধরনের লেনদেন
yn = (kn > 0).astype(int)
sn = -lofn.score_samples((Xn - med) / (1.4826 * mad))
tot, f, m = cost(sn, yn, best[3])
print(f"next week, same threshold: flagged {f}, missed {m}, cost {tot:,} taka")
for cont in ("auto", 0.03):               # contamination দিয়ে সীমা ঠিক করলে
    off = LocalOutlierFactor(20, novelty=True, contamination=cont,
                             algorithm="kd_tree").fit(Hs).offset_
    tot, f, m = cost(sn, yn, -off)
    print(f"contamination={cont}: threshold {-off:.2f}, flagged {f}, missed {m}, cost {tot:,}")

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

no review at all: 115,000 taka
review top  5: flagged  5 missed 18 cost 90,750 taka
review top 10: flagged 10 missed 15 cost 76,500 taka
review top 20: flagged 20 missed  6 cost 33,000 taka
review top 23: flagged 23 missed  3 cost 18,450 taka
review top 30: flagged 30 missed  0 cost  4,500 taka
review top 50: flagged 50 missed  0 cost  7,500 taka
best on audit batch: threshold 3.64, flagged 26, missed 0, cost 3,900 taka
next week, same threshold: flagged 27, missed 0, cost 4,050 taka
contamination=auto: threshold 1.50, flagged 51, missed 0, cost 7,650
contamination=0.03: threshold 1.49, flagged 56, missed 0, cost 8,400

কিছু যাচাই না করলে ২৩টি জালিয়াতির ক্ষতি ১,১৫,০০০ টাকা। শীর্ষ ২৩টি যাচাই করলে ৩টি ফসকায়, খরচ ১৮,৪৫০; শীর্ষ ২৬টিতে (score ≥ ৩.৬৪) সব ধরা পড়ে, খরচ সবচেয়ে কম, ৩,৯০০ টাকা। একটি ফসকানো জালিয়াতি ৩৩টি বাড়তি যাচাইয়ের সমান দামি (৫,০০০/১৫০), তাই সীমা “সবাইকে ধরা”-র দিকে ঝোঁকে। পরের সপ্তাহে একই score-সীমা ২৭টি চিহ্নিত করে, কিছু ফসকায় না, খরচ ৪,০৫০. contamination-এর দুটি সীমাই (LOF ≥ ১.৫০ আর ১.৪৯) প্রায় দ্বিগুণ খরচ করে, কারণ এরা পরিষ্কার ডেটার একটি quantile থেকে আসে, টাকার হিসাব থেকে নয়। সতর্কতা: “পরের সপ্তাহ” একই জেনারেটর থেকে এসেছে, তাই এখানে সীমা সহজে টিকেছে; বাস্তবে নতুন ধরনের জালিয়াতি আসে, আর অডিটের মাত্র ২৩টি positive থেকে বাছা সীমা নিজেই অনিশ্চিত (পর্ব ২১-এর bootstrap দিয়ে তার পরিসর মাপা যায়)।

Drift বনাম anomaly. ঈদের আগের সপ্তাহে হঠাৎ অনেক বড় অর্ডার, অনেক দূরের ঠিকানা। Detector-এর চোখে শত শত “anomaly”, অথচ এরা জালিয়াতি নয়: পুরো বণ্টন সরে গেছে (data drift). একটি সারি বাকিদের থেকে আলাদা হলে anomaly; অনেক সারি একসঙ্গে পুরোনো বণ্টন থেকে সরলে drift. চিহ্নিতের হার হঠাৎ লাফালে আগে drift সন্দেহ করুন, training জানালা হালনাগাদ করুন, আর মৌসুমি প্রসঙ্গ (ঈদ, পহেলা বৈশাখ) feature হিসেবে যোগ করুন। Input আর উত্তরের সম্পর্ক বদলালে তা concept drift; দুটির মাপা আর সামলানো পর্ব ৫৯ (Distribution shift) আর পর্ব ৬৯ (ML system-এর test আর নজরদারি)-এ আসবে।

আরও গভীরে: Isolation forest-এর অক্ষ-সমান্তরাল কাটের ডোরা দূর করতে Hariri ও সহকর্মীরা (২০১৯ সালের দিকে) Extended Isolation Forest প্রস্তাব করেন, যেখানে কাট হয় এলোমেলো দিকের hyperplane বরাবর। Campos ও সহকর্মীদের ২০১৬ সালের benchmark আর পরের ADBench (২০২২) প্রায় একই কথা বলে: কোনো একক unsupervised detector সব ডেটায় জেতে না, আর k-NN-দূরত্ব বা isolation forest-এর মতো সরল পদ্ধতি প্রায়ই শক্ত প্রতিদ্বন্দ্বী। বহু পদ্ধতি একসঙ্গে পেতে PyOD (Zhao ও সহকর্মী, JMLR ২০১৯) একটি পরিচিত Python লাইব্রেরি; autoencoder-ভিত্তিক detector-এর আলোচনা পরের deep learning কোর্সে।

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

  • c(২৫৬) কত, আর E[h] = c(n) হলে score? ১০.২৪৫; score ঠিক ০.৫.
  • Outlier detection আর novelty detection-এর পার্থক্য? প্রথমটিতে training ব্যাচেই anomaly মেশানো; দ্বিতীয়টিতে training পরিষ্কার, প্রশ্ন নতুন ডেটা নিয়ে।
  • LOF ≈ ১ মানে? বিন্দুটির ঘনত্ব তার প্রতিবেশীদের মতো, অর্থাৎ স্বাভাবিক।
  • One-class SVM-এ ν কী বোঝায়? Training-এর বাইরে-থাকা অংশের ঊর্ধ্বসীমা আর support vector-এর অংশের নিম্নসীমা।
  • বিরল anomaly-তে কোন metric? ROC AUC নয়; precision at k আর average precision.

৮ · AI-এর যুগে — anomaly detector-এর কোড আর দাবি যাচাই

AI assistant এই বিষয়ে চটপটে কোড দেয়, কিন্তু ভুলগুলো চুপচাপ থাকে, কারণ label ছাড়া কোড “চলে” আর কিছু একটা চিহ্নিত করে। নিচের ভুলগুলো বারবার দেখা যায়:

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
score_samples-এর চিহ্ন উল্টো পড়ে, সবচেয়ে স্বাভাবিকগুলোকে “শীর্ষ সন্দেহজনক” বলেscikit-learn-এ বড় মানে বেশি স্বাভাবিকএকটি স্পষ্ট চরম সারি বানিয়ে তার র‍্যাঙ্ক দেখুন
novelty=True LOF-এ fit_predict, বা training ডেটায় predictপ্রথমটি নেই; দ্বিতীয়টি নিজেকে প্রতিবেশী গোনেnovelty মোডে শুধু নতুন ডেটায় score করুন
contamination=0.1 ধরে নিয়ে সেটিকেই ফল বলেএটি শুধু সীমা ঠিক করে, আসল হার মাপে নাখরচ বা যাচাই করার ক্ষমতা থেকে সীমা বাছুন
Scaling ছাড়া LOF বা one-class SVM চালায়টাকার মতো বড় এককের feature দূরত্ব দখল করেlog আর robust scaling, তারপর আবার মাপুন
ROC AUC ০.৯৭ দেখিয়ে “চমৎকার” বলেবিরল positive-এ ROC আশাবাদীP@k আর AP দেখুন, এলোমেলো ভিত্তির সঙ্গে
সমস্ত ঐতিহাসিক label দিয়ে k, γ, ν টিউন করে তারপর একই label-এ মূল্যায়নLabel-এ টিউন মানে আশাবাদী ফলঅডিট নমুনা দুই ভাগ: একটিতে বাছাই, অন্যটিতে মাপ

উত্তরের সঙ্গে যাচাইয়ের কোড চেয়ে নিন; নিজে চালানো অংশটিই আসল পরীক্ষা।

আমার কাছে ৫০,০০০টি লেনদেন আছে (টাকা, সময়, পণ্য সংখ্যা, দূরত্ব), আর অডিট করা মাত্র ৪০০টি, যার ১৫টি জালিয়াতি। ১. IsolationForest, LocalOutlierFactor আর OneClassSVM দিয়ে anomaly score বের করার কোড দাও, scikit-learn 1.8-এর API অনুযায়ী; প্রতিটির score-এর চিহ্ন কোন দিকে, স্পষ্ট লেখো। ২. কোন feature-এ log বা robust scaling লাগবে, কেন, বলো। ৩. অডিটের ৪০০টি দিয়ে precision at k আর average precision মাপার কোড দাও; টিউনিং আর মাপার জন্য অডিট নমুনা কীভাবে ভাগ করব, বলো। ৪. novelty=False আর novelty=True কখন কোনটা, আর contamination আসলে কী বদলায় — ব্যাখ্যা করো। ৫. তোমার দেওয়া প্রতিটি parameter docs-এর কোন পাতায় আছে, আর তোমার উত্তরে কোথায় ভুলের ঝুঁকি বেশি, লেখো।

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

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

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

প্র ০১Outlier মানেই কি ভুল ডেটা? ফেলে দেওয়া আর আবিষ্কারের মধ্যে রেখা কোথায়?

ডেটা পরিষ্কারের প্রচলিত অভ্যাস হলো outlier মুছে ফেলা। কিন্তু Hawkins-এর সংজ্ঞাতেই উত্তর লুকানো: outlier সম্ভবত “ভিন্ন প্রক্রিয়া” থেকে এসেছে। সেই প্রক্রিয়া হতে পারে ভুল এন্ট্রি (৪৫০-এর জায়গায় ৪৫,০০০), সেন্সরের ত্রুটি, অথবা ঠিক সেই জিনিস যা আপনি খুঁজছেন: জালিয়াতি, যন্ত্রের আসন্ন বিকল, নতুন কোনো রোগের প্রাদুর্ভাব।

ইতিহাসে বহুল উদ্ধৃত একটি উদাহরণ অ্যান্টার্কটিকার ওজোন গহ্বর। ১৯৮৫ সালে British Antarctic Survey-র Joseph Farman, Brian Gardiner আর Jonathan Shanklin মাটিতে বসানো যন্ত্রের মাপ থেকে বসন্তকালে ওজোনের নাটকীয় পতনের খবর প্রকাশ করেন। প্রচলিত গল্প বলে, NASA-র উপগ্রহের সফটওয়্যার অস্বাভাবিক কম মানগুলো “ভুল” ধরে বাদ দিয়েছিল, তাই উপগ্রহ আগে দেখেও দেখেনি। NASA-র বিজ্ঞানীদের পরের বিবরণ অনুযায়ী গল্পটি অতিসরল: মানগুলো মুছে ফেলা হয়নি, সন্দেহজনক হিসেবে চিহ্নিত হয়েছিল, আর মাটির একটি স্টেশনের সঙ্গে মিলিয়ে দেখার সময় সেই স্টেশনের মানই ভুল মনে হয়েছিল। কোন বিবরণ কতটা ঠিক, তা নিয়ে আলোচনা আছে; কিন্তু দুটি বিবরণেই শিক্ষা এক: স্বয়ংক্রিয় “সন্দেহজনক” পতাকা মানে কাজ শুরু, শেষ নয়।

বাজারঘরে এর মানে: detector-এর চিহ্নিত সারি সরাসরি মুছবেন না, আটকেও দেবেন না। প্রথমে শ্রেণিতে ভাগ করুন: ডেটার ভুল (ঠিক করুন), বৈধ কিন্তু বিরল (রাখুন, model-কে শেখান), আর সন্দেহজনক আচরণ (তদন্ত)। আর model training-এর আগে outlier মুছলে model পরে ঠিক সেই ধরনের ঘটনায় অন্ধ হতে পারে, যা সবচেয়ে দামি।

মূল উপলব্ধি: Anomaly score একটি প্রশ্ন তোলে, উত্তর দেয় না; মোছা, ঠিক করা না তদন্ত, সেই সিদ্ধান্তে মানুষের বিচার আর প্রসঙ্গ লাগে।

প্র ০২বিরল anomaly-তে ROC AUC কেন আশাবাদী ছবি দেখায়, আর precision at k ও PR AUC কী আলাদা বলে?

ROC curve-এর x অক্ষ false positive rate: সব সাধারণ সারির কত ভাগ ভুলে চিহ্নিত। Anomaly বিরল হলে সাধারণ সারি বিপুল, তাই হাজারটি ভুল সতর্কতাও এই হারে ছোট দেখায়। একটি পরিচিত সর্বজনীন উদাহরণ: ইউরোপের কার্ড লেনদেনের একটি dataset (ULB-র গবেষকদের প্রকাশিত, Kaggle-এ জনপ্রিয়), যেখানে প্রায় ২,৮৫,০০০ লেনদেনের মধ্যে জালিয়াতি প্রায় ০.১৭%. এমন ডেটায় ০.৯৫-এর বেশি ROC AUC সহজেই আসে, অথচ শীর্ষ ১০০টি সতর্কতার অর্ধেকও ভুল হতে পারে।

Precision এই ক্ষতিটি সরাসরি দেখায়: আপনার চিহ্নিতদের কত ভাগ সত্যি। Jesse Davis আর Mark Goadrich ২০০৬ সালে (ICML) দেখান, ROC space-এ একটি curve অন্যটির চেয়ে সর্বত্র ভালো হলে PR space-এও তাই, কিন্তু দুটির ক্ষেত্রফল ভিন্ন ক্রম দিতে পারে। Takaya Saito আর Marc Rehmsmeier ২০১৫ সালে (PLoS ONE) ভারসাম্যহীন ডেটায় PR plot-কে বেশি তথ্যবহুল বলে যুক্তি দেন। মূল কারণ ভিত্তি-হার: ROC AUC-র এলোমেলো ভিত্তি সবসময় ০.৫, AP-র ভিত্তি anomaly-র অনুপাত। তাই AP ০.৩ খারাপ শোনালেও ভিত্তি ০.০০২ হলে সেটি ১৫০ গুণ উন্নতি।

Precision at k আরও ব্যবহারিক: অডিট দল দিনে যতগুলো দেখতে পারে, k তত। এটি পুরো ক্রমের গড় নয়, শুধু শীর্ষের মান মাপে, আর সেটিই কাজের অংশ। তবে P@k-এর দুর্বলতা হলো k-এর পরের সব তথ্য উপেক্ষা, আর অল্প positive-এ বড় অনিশ্চয়তা: ২৩টি anomaly-তে একটি বেশি ধরা মানে P@২৩-এ ০.০৪-এর লাফ। তাই কয়েকটি seed বা bootstrap দিয়ে পরিসর দেখা ভালো।

একটি সূক্ষ্ম ফাঁদও আছে: যাচাই করা নমুনা প্রায়ই আগের detector-এর চিহ্নিত সারি থেকেই আসে। তখন সেই detector যা ধরেনি, তার label কখনো তৈরি হয় না, আর নতুন detector-এর recall আসলের চেয়ে ভালো দেখায়। এলোমেলো একটি ছোট অডিট নমুনা এই পক্ষপাত ভাঙে।

মূল উপলব্ধি: বিরল ঘটনায় প্রশ্ন হলো “আমার সতর্কতাগুলোর কত ভাগ সত্যি”, যা precision-ভিত্তিক মাপ সরাসরি দেয়; ROC AUC বিপুল সাধারণ সারির আড়ালে ভুলগুলো ঢেকে রাখে।

প্র ০৩Isolation forest দূরত্ব বা ঘনত্ব না মেপেও কেন কাজ করে, আর কোথায় ব্যর্থ হয়?

২০০৮ সালের আগে বেশিরভাগ পদ্ধতি “স্বাভাবিক কেমন” তার একটি profile বানাত (density, দূরত্ব, cluster), তারপর যা মেলে না তাকে anomaly বলত। Liu, Ting আর Zhou উল্টো দিক থেকে ভাবেন: anomaly-কে সরাসরি আলাদা করা যায়, কারণ তারা “অল্প আর আলাদা”। এলোমেলো কাটের প্রত্যাশিত সংখ্যা ঘনত্বের সঙ্গে সম্পর্কিত হলেও পদ্ধতিটি কোনো দূরত্ব হিসাব করে না, তাই খরচ প্রায় রৈখিক, আর ছোট নমুনায় (২৫৬) গাছ বানানোয় খুব বড় ডেটাতেও দ্রুত। এজন্যই এটি শিল্পে অন্যতম জনপ্রিয় ডিফল্ট।

ব্যর্থতার তিনটি উৎস। প্রথমত, অক্ষ-সমান্তরাল কাট: অংশ ৬-এর ছবিতে contour-এর ডোরা দেখা গেছে। দুটি feature-এর তির্যক সম্পর্ক ভাঙা বিন্দু (যেমন “কাছে অথচ অনেক দেরি”) এমন জায়গায় থাকতে পারে যা অক্ষ-বরাবর কাটে সহজে আলাদা হয় না; Extended Isolation Forest-এর এলোমেলো দিকের কাট এর একটি প্রতিকার। দ্বিতীয়ত, অপ্রাসঙ্গিক feature: অংশ ৪-এর ব্লকে ৪৫,০০০ টাকার সারির path কখনো ১, কখনো ১৪.২, কারণ চারটি feature-এর তিনটিতে সে সাধারণ। একশোটি feature-এর মধ্যে একটিতে চরম হলে সংকেত প্রায় ডুবে যায়, তাই feature বাছাই এখানে গুরুত্বপূর্ণ। তৃতীয়ত, local anomaly: পুরো ডেটার এক রকম কাট ঘন আর পাতলা এলাকার পার্থক্য বোঝে না, তাই লাঞ্চ-দলের পাশের ৬টি সারি সে একটিও ধরেনি।

মূল প্রবন্ধ masking আর swamping-কেও আলোচনা করে। Masking: অনেক anomaly একসঙ্গে থাকলে তারা পরস্পরকে আড়াল করে। Swamping: anomaly-র কাছাকাছি থাকা সাধারণ বিন্দুকেও anomaly বলে ফেলা। ছোট sub-sample দুটিই কমায়, কারণ প্রতিটি গাছে দলটির অল্প কয়েকজনই থাকে। এই ডেটায় জালিয়াতির ১২টির সবগুলো ধরা পড়েছে, যদিও রাত ৩টার ঘণ্টাও এতে সাহায্য করেছে।

মূল উপলব্ধি: Isolation forest দ্রুত, scaling-এ কম সংবেদনশীল আর global anomaly-তে শক্তিশালী; কিন্তু অক্ষ-বরাবর আর পুরো-ডেটার দৃষ্টি তাকে তির্যক, local আর বহু-অপ্রাসঙ্গিক-feature-এর পরিস্থিতিতে দুর্বল করে।

প্র ০৪“কোন anomaly detector সবচেয়ে ভালো?” প্রশ্নটির উত্তর কেন নেই, আর বাস্তবে কীভাবে বাছবেন?

অংশ ৬-এর টেবিলই উত্তরের অর্ধেক: z-score, Mahalanobis আর isolation forest একই ১৭টি ধরেছে, LOF ধরেছে local-গুলো কিন্তু k-এর ওপর নির্ভর করে জালিয়াতির দল হারিয়েছে, আর one-class SVM-এর ফল γ বদলালে ০.৫২ থেকে শূন্যে নেমেছে। প্রতিটি পদ্ধতি “স্বাভাবিক”-এর একটি ভিন্ন সংজ্ঞা ধরে নেয়: একটি কেন্দ্র আর উপবৃত্ত, সহজে আলাদা হওয়া, স্থানীয় ঘনত্ব, বা একটি মসৃণ সীমা। কোন সংজ্ঞা ঠিক, তা ডেটা আর ব্যবসার প্রশ্ন থেকে আসে, অ্যালগরিদম থেকে নয়।

গবেষণাও একই কথা বলে। Campos ও সহকর্মীদের ২০১৬ সালের তুলনামূলক গবেষণা (Data Mining and Knowledge Discovery) অনেক dataset-এ বহু পদ্ধতি চালিয়ে দেখায়, ফল dataset-ভেদে অনেক বদলায়, আর মূল্যায়নের নকশা (কোন শ্রেণিকে “anomaly” ধরা হলো, কত অনুপাতে) সিদ্ধান্তকে প্রভাবিত করে। ২০২২ সালের ADBench (NeurIPS-এর datasets track) কয়েক ডজন পদ্ধতি তুলনা করে জানায়, কোনো unsupervised পদ্ধতি সামগ্রিকভাবে পরিসংখ্যানগতভাবে স্পষ্ট এগিয়ে নেই, আর অল্প label পেলেই semi-supervised পদ্ধতি অনেক এগিয়ে যায়। এগুলো benchmark-এর পর্যবেক্ষণ; আপনার ডেটায় একই হবে, এমন নিশ্চয়তা নেই।

বাস্তব পথ তাই পরীক্ষামূলক। প্রথমে ডোমেইন থেকে ঠিক করুন কোন ধরনের anomaly খুঁজছেন (একক চরম, প্রসঙ্গে অস্বাভাবিক, না সংঘবদ্ধ দল)। সরল baseline রাখুন। একটি ছোট এলোমেলো অডিট নমুনা তৈরি করুন, তার অর্ধেকে k, γ, ν বাছুন, বাকি অর্ধেকে P@k আর AP মাপুন, কয়েকটি seed-এ। কয়েকটি ভিন্ন স্বভাবের detector-এর score rank-এ রূপান্তর করে গড় নিলে প্রায়ই একক পদ্ধতির চেয়ে কম ঝুঁকি থাকে। আর যত তাড়াতাড়ি সম্ভব label জমিয়ে supervised বা semi-supervised পদ্ধতিতে যান (পর্ব ৫৪, কম label-এ শেখা)।

মূল উপলব্ধি: প্রতিটি detector “স্বাভাবিক”-এর একটি অনুমান; সেরা detector আপনার ডেটার anomaly-র ধরনের সঙ্গে মেলে এমনটি, আর তা জানার একমাত্র উপায় অল্প হলেও যাচাই করা label দিয়ে মাপা।

প্র ০৫জালিয়াতি ধরার detector চালু হওয়ার পর পৃথিবী বদলায়। Feedback loop আর প্রতিপক্ষের অভিযোজন কীভাবে মূল্যায়ন আর training দুটিকেই বিকৃত করে?

সাধারণ ML সমস্যায় ডেটার পেছনের প্রক্রিয়া মোটামুটি স্থির ধরা হয়। জালিয়াতি তা নয়: অপর পক্ষে মানুষ আছে, যারা ধরা পড়লে কৌশল বদলায়। বাজারঘর যদি রাত ৩টার ৪,৮০০ টাকার অর্ডার ধরতে শুরু করে, পরের মাসে একই চক্র হয়তো সন্ধ্যা ৭টায় ২,০০০ টাকার অনেক ছোট অর্ডার দেবে, যা ঠিক সাধারণ গ্রাহকের মেঘের ভেতরে। Detector-এর ভাষায়, anomaly তখন আর anomaly নয়; এটি তখন contextual বা collective সমস্যা, যার জন্য গ্রাহক-স্তরের feature (একই ফোনে কতগুলো অ্যাকাউন্ট, ঠিকানার পুনরাবৃত্তি) লাগে।

দ্বিতীয় সমস্যা label-এর উৎস। যাচাই হয় শুধু চিহ্নিত লেনদেন, তাই জালিয়াতির label জমে শুধু সেই ধরনের, যা detector আগেই ধরতে পারে। এই label দিয়ে পরের model শেখালে সে পুরোনো ধরনে আরও দক্ষ হয়, নতুন ধরনে অন্ধই থাকে; মূল্যায়নেও recall আসলের চেয়ে ভালো দেখায়। Credit scoring-এ একই ঘটনার নাম reject inference সমস্যা: প্রত্যাখ্যাত আবেদনকারীরা ঋণ ফেরত দিতেন কি না, তা কখনো জানা যায় না। প্রতিকার হলো ইচ্ছাকৃত এলোমেলোতা: প্রতিদিন চিহ্নিত-না-হওয়া লেনদেন থেকেও একটি ছোট এলোমেলো নমুনা যাচাই করা।

তৃতীয় সমস্যা training-এর দূষণ। Novelty detection-এ “পরিষ্কার” ইতিহাস লাগে, কিন্তু যে জালিয়াতি ধরা পড়েনি, তা ইতিহাসে “স্বাভাবিক” হিসেবেই থেকে যায়। প্রতি মাসে পুরো ইতিহাসে নতুন করে শেখালে ধীরে ধীরে চক্রের আচরণ “স্বাভাবিক”-এর অংশ হয়ে যেতে পারে। তাই training জানালা, কোন সারি বাদ পড়ল তার নিয়ম, আর চিহ্নিতের হারের প্রবণতা নথিভুক্ত রাখা জরুরি। এই প্রশ্নগুলো পর্ব ৫৯ (Distribution shift) আর পর্ব ৬৯ (ML system-এর test আর নজরদারি)-এর বিষয়, আর বিস্তারিত নিরাপত্তা-দৃষ্টিকোণ সাইবার নিরাপত্তা কোর্সে।

মূল উপলব্ধি: Anomaly detection একটি স্থির পরীক্ষা নয়, প্রতিপক্ষের সঙ্গে চলমান খেলা; detector নিজেই ভবিষ্যতের ডেটা আর label বদলায়, তাই এলোমেলো অডিট আর নিয়মিত নজরদারি ছাড়া ভালো ফলও ধীরে ধীরে ভ্রম হয়ে যায়।

অনুশীলন

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

  1. হাতের হিসাব: (ক) ৬৪টি সারির sub-sample-এ c(৬৪) কত? গড় path ৩, ৭.৪৭২ আর ১২ হলে score কত? (খ) এক মাত্রায় পাঁচটি বিন্দু ১, ২, ৪, ৫, ১১; k = ২ নিয়ে ১১-এর LOF বের করুন।

    (ক) ln ৬৩ = ৪.১৪৩১, H(৬৩) ≈ ৪.১৪৩১ + ০.৫৭৭২ = ৪.৭২০৩; c(৬৪) = ২ × ৪.৭২০৩ − ২ × ৬৩/৬৪ = ৯.৪৪০৬ − ১.৯৬৮৮ = ৭.৪৭২. Score: E[h] = ৩ → ২−৩/৭.৪৭২ = ২−০.৪০১৫ = ০.৭৫৭; E[h] = ৭.৪৭২ → ০.৫; E[h] = ১২ → ২−১.৬০৬ = ০.৩২৯.

    (খ) প্রতিবেশী আর k-distance: ১ → {২, ৪}, kd ৩; ২ → {১, ৪}, kd ২; ৪ → {৫, ২}, kd ২; ৫ → {৪, ২}, kd ৩; ১১ → {৫, ৪}, kd ৭. Reach: ১-এর জন্য max(২, ১) = ২ আর max(২, ৩) = ৩, গড় ২.৫, lrd ০.৪; ২, ৪ আর ৫-এরও একই হিসাবে lrd ০.৪. ১১-এর জন্য reach(১১, ৫) = max(৩, ৬) = ৬, reach(১১, ৪) = max(২, ৭) = ৭, গড় ৬.৫, lrd = ০.১৫৩৮. LOF(১১) = ০.৪/০.১৫৩৮ = ২.৬. একটি ডেস্কটপ run-এ LocalOutlierFactor(n_neighbors=2) [১, ১, ১, ১, ২.৬] দেয়।

  2. সমালোচনা ও নকশা: বাজারঘরের একজন বিশ্লেষক লিখলেন: “গত দুই বছরের সব লেনদেনে (আগে ধরা পড়া জালিয়াতিসহ) IsolationForest(contamination=0.1) শেখালাম। জানা জালিয়াতিগুলোয় ROC AUC ০.৯৭. প্রতিদিন predict = −১ হওয়া সব লেনদেন আটকে দেওয়া হবে।” কী কী সমস্যা, আর কীভাবে নতুন করে সাজাবেন?

    সমস্যা: (১) contamination=0.1 মানে প্রতিদিন প্রায় ১০% লেনদেন আটকানো; এটি কোনো মাপা হার নয়, আর বেশিরভাগ সৎ গ্রাহক ক্ষতিগ্রস্ত হবেন। (২) জানা জালিয়াতি training-এ রেখে দেওয়া (novelty নয়, দূষিত ব্যাচ), আর মূল্যায়নও একই ডেটায়। (৩) ROC AUC বিরল ঘটনায় আশাবাদী; P@k আর AP দেখা হয়নি। (৪) “জানা জালিয়াতি” আগের নিয়মে ধরা পড়া ধরনগুলোই, তাই মূল্যায়ন পক্ষপাতদুষ্ট। (৫) দুই বছরে ঈদ, দাম আর গ্রাহকের ধরন বদলেছে (drift). (৬) সরাসরি আটকানো, মানুষের যাচাই নেই।

    নতুন নকশা: জানা জালিয়াতি বাদ দিয়ে সাম্প্রতিক জানালায় শেখানো; log আর robust scaling; সময়ভিত্তিক ভাগে (আগের মাসে শিখে পরের মাসে মাপা) P@k আর AP, একটি সরল baseline-এর পাশে; সীমা আসবে যাচাই খরচ আর ক্ষতির হিসাব থেকে; শীর্ষ score যাবে হাতে-যাচাইয়ে, আটকানোয় নয়; প্রতিদিন একটি এলোমেলো অডিট নমুনা; আর চিহ্নিতের হার নজরে রাখা।

  3. আউটপুট অনুমান করুন: চালানোর আগে লিখে ফেলুন নিচের কোড কী ছাপবে।
    import numpy as np
    x = np.array([10, 11, 12, 13, 14, 100.])
    z = (x - x.mean()) / x.std()
    med = np.median(x); mad = np.median(np.abs(x - med))
    rz = (x - med) / (1.4826 * mad)
    print(np.round(z, 2))
    print(med, mad)
    print(np.round(rz, 1))
    [-0.51 -0.48 -0.45 -0.42 -0.39  2.23]
    12.5 1.5
    [-1.1 -0.7 -0.2  0.2  0.7 39.3]

    গড় ১৬০/৬ = ২৬.৬৬৭, σ (n দিয়ে ভাগ) ৩২.৮২১. ১০০-এর z = ৭৩.৩৩/৩২.৮২ = ২.২৩, যা √৫ = ২.২৩৬-এর ছাদের প্রায় সমান: ৩-এর সীমা কখনো পেরোবে না, আর সাধারণ মানগুলো সবাই ঋণাত্মক। Median (১২ + ১৩)/২ = ১২.৫; বিচ্যুতি ২.৫, ১.৫, ০.৫, ০.৫, ১.৫, ৮৭.৫, সাজালে মাঝের দুটি ১.৫ আর ১.৫, তাই MAD = ১.৫. ১০০-এর robust z = ৮৭.৫/(১.৪৮২৬ × ১.৫) = ৮৭.৫/২.২২৪ = ৩৯.৩. ছাপা ফরম্যাট একটি ডেস্কটপ run-এ যাচাই করা।

  4. কোড লিখুন: একটি function rank_ensemble(scores) লিখুন, যা কয়েকটি detector-এর anomaly score (প্রতিটিতে বড় = বেশি সন্দেহজনক) নিয়ে প্রতিটিকে ০ থেকে ১-এর rank-এ রূপান্তর করে গড় দেয়। কেন সরাসরি score-এর গড় না নিয়ে rank-এর গড়?
    import numpy as np
    from scipy.stats import rankdata
    
    def rank_ensemble(scores):
        # scores: একই সারিগুলোর জন্য কয়েকটি detector-এর score-এর list
        ranks = [(rankdata(s) - 1) / (len(s) - 1) for s in scores]
        return np.mean(ranks, axis=0)
    
    # ব্যবহার (আগের ব্লকগুলোর নাম ধরে):
    # ens = rank_ensemble([-iso.score_samples(Xs), -lofn.score_samples(Xs), d2])
    # report("rank ensemble", ens)

    এটি একটি উদাহরণ কোড, এখানে চালানো হয়নি। কারণ: বিভিন্ন detector-এর score-এর মাপ সম্পূর্ণ আলাদা (isolation score ০.৩–০.৭, LOF ১ থেকে কয়েকশো, Mahalanobis d² হাজার পর্যন্ত), তাই সরাসরি গড়ে সবচেয়ে বড় মাপের detector একাই সিদ্ধান্ত নেয়। Rank সবাইকে একই ০–১ মাপে আনে। দাম: score-এর দূরত্বের তথ্য (কতটা বেশি অস্বাভাবিক) হারায়; বিকল্প হলো প্রতিটি score-কে robust z-তে মানানো। আর rank-গড়ের ফলও অডিট নমুনার আলাদা অংশে মেপে দেখতে হবে।

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

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

ক্রমএকটি বিন্দু p-এর LOF হিসাবের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Feature scaling করে প্রতিটি বিন্দুর k-টি নিকটতম প্রতিবেশী খোঁজা
  2. প্রতিটি বিন্দুর k-distance (k-তম প্রতিবেশীর দূরত্ব)
  3. reach(p, o) = max(k-distance(o), d(p, o))
  4. lrd(p) = ১ / প্রতিবেশীদের reach-এর গড়
  5. LOF(p) = প্রতিবেশীদের lrd-এর গড় / lrd(p)

Isolation forest-এ sub-sample-এর আকার n = 256, তাই c(n) ≈ 10.245. একটি বিন্দুর গড় path length ঠিক 10.245 হলে তার anomaly score কত?

  • 1.0
  • 0.0
  • 0.5
  • 0.382

আটটি মানের একটি তালিকায় একটি মান বাকিদের চেয়ে বহু গুণ বড়। n দিয়ে ভাগ করা σ ব্যবহার করে “|z| > 3 হলে outlier” নিয়ম চালালে কী হবে?

  • বড় মানটি নিশ্চিতভাবে ধরা পড়বে
  • কোনো মানই ধরা পড়বে না, কারণ |z| কখনো √7 ≈ 2.65 পেরোতে পারে না
  • সব সাধারণ মান ধরা পড়বে
  • ফল এলোমেলো, কিছু বলা যায় না

লেনদেনের একটি ব্যাচে একই রকম 12টি জালিয়াতির অর্ডার একটি ঘন দল বানিয়েছে। LocalOutlierFactor(n_neighbors=8) এদের প্রায় স্বাভাবিক বলছে। সবচেয়ে সম্ভাব্য কারণ কী?

  • Feature scaling করা হয়নি
  • LOF শুধু ঘন এলাকাকেই অস্বাভাবিক বলে
  • contamination খুব কম রাখা হয়েছে
  • k দলের আকারের চেয়ে ছোট, তাই প্রতিটি সদস্যের প্রতিবেশী দলের ভেতরেই (masking)

OneClassSVM(nu=0.05) একটি বড় training set-এ শেখানো হলো। ν = 0.05 সম্পর্কে কোনটি সত্য?

  • Training-এর সর্বোচ্চ প্রায় 5% সীমার বাইরে থাকবে, আর অন্তত প্রায় 5% support vector হবে
  • ডেটার আসল anomaly-র হার 5%
  • RBF kernel-এর প্রস্থ 0.05
  • নতুন যেকোনো ব্যাচে ঠিক 5% চিহ্নিত হবে

10টি লেনদেন score অনুযায়ী সাজানো, সত্যিকারের জালিয়াতি আছে 1, 3 আর 6 নম্বর অবস্থানে। Average precision কত?

  • 0.30
  • 0.40
  • 0.722
  • 1.0

scikit-learn-এর IsolationForest-এ contamination=0.02 থেকে 0.05 করলে কী বদলায়?

  • প্রতিটি সারির score_samples
  • শুধু offset_, অর্থাৎ predict-এর সীমা, তাই কতগুলো সারি −1 পায়
  • প্রতিটি গাছের sub-sample-এর আকার
  • গাছের সংখ্যা

শব্দকোষ

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

মনে রাখুন

  1. Anomaly point, contextual আর collective; training ব্যাচে anomaly মেশানো থাকলে outlier detection, পরিষ্কার ইতিহাস থাকলে novelty detection.
  2. অল্প label-এ মাপুন precision at k আর average precision দিয়ে, এলোমেলো ভিত্তির (anomaly-র অনুপাত) সঙ্গে তুলনা করে; ROC AUC বিরল ঘটনায় আশাবাদী।
  3. চরম মান σ ফুলায় (|z| ≤ √(n − ১)); robust z = (x − median)/(১.৪৮২৬ · MAD) তা এড়ায়, কিন্তু বাঁকা বা পূর্ণসংখ্যার feature-এ ভুল সতর্কতা দেয়।
  4. Isolation forest: s = 2−E[h]/c(n), c(২৫৬) = ১০.২৪৫, ০.৫ মানে গড়পড়তা। LOF: প্রতিবেশীর ঘনত্ব / নিজের ঘনত্ব; k ঘন দলের আকারের চেয়ে বড় রাখুন। One-class SVM: ν বাইরের অংশের ঊর্ধ্বসীমা, γ সীমার মসৃণতা।
  5. contamination শুধু সীমা ঠিক করে; সীমা বাছুন খরচ থেকে, আলাদা অডিট নমুনায় মেপে, আর drift-কে anomaly ভাববেন না।

পরের পর্বে: পর্ব ৪৪ (Topic modeling — LDA আর NMF দিয়ে বাংলা রিভিউয়ের বিষয় খোঁজা) label ছাড়া শেখাকে লেখায় নিয়ে যায়: বাজারঘরের কাল্পনিক বাংলা রিভিউ থেকে TF-IDF আর NMF দিয়ে বিষয় খোঁজা, আর LDA-র generative গল্প, যেখানে প্রতিটি রিভিউ কয়েকটি বিষয়ের মিশ্রণ।

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

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