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

Object detection আর segmentation-এর পরিচয় — bounding box, IoU আর U-Net

Introduction to Object Detection and Segmentation — Bounding Boxes, IoU, NMS, mAP and U-Net

Taught in Bangla, this lesson separates classification, object detection, semantic and instance segmentation. It computes IoU, writes non-maximum suppression in in-page NumPy and matches torchvision.ops.nms on 500 boxes, explains anchors and two-stage versus one-stage detectors, and works out average precision and mAP. Real desktop runs apply a pretrained torchvision detector to a CC0 photo and train a small U-Net with and without skip connections, measured by Dice.

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

  • Classification, localisation, detection, semantic আর instance segmentation-এর পার্থক্য আর প্রতিটির output-এর আকার
  • Bounding box-এর ফরম্যাট, IoU হাতে আর NumPy-তে, আর greedy non-maximum suppression লেখা ও যাচাই করা
  • Anchor, two-stage (Faster R-CNN) আর one-stage (YOLO, SSD) detector-এর মূল ধারণা
  • একটি ছোট উদাহরণে precision-recall টেবিল থেকে average precision আর mAP হিসাব
  • U-Net-এর encoder–decoder, transposed convolution আর skip connection; Dice আর IoU দিয়ে segmentation মাপা ও শেখানো

এতদিন CNN-কে একটি প্রশ্ন করেছেন: “ছবিটি কীসের?” পর্ব ২০ (Convolution) থেকে পর্ব ২৪ (Data augmentation) পর্যন্ত এই মডিউলের সব পর্ব এই এক প্রশ্নের উত্তর আরও ভালো করার কথা। বাস্তবে প্রশ্ন প্রায়ই অন্য: বাজারঘরের গুদামের তাকে কয়টি বোতল আছে, কোনটি কোথায়, কোন তাক ফাঁকা? এই পর্বে তিনটি পাতার ব্লক ব্রাউজারের Python আর NumPy-তে চলে (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে)। PyTorch আর torchvision-এর সব সংখ্যা ডেস্কটপের সত্যিকারের run থেকে, CPU-তে।

১ · ছবির চারটি প্রশ্ন — classification থেকে instance segmentation

ধরুন বাজারঘরের গুদামে একজন নতুন কর্মী তাকের ছবি দেখছেন। ম্যানেজার চার রকম প্রশ্ন করতে পারেন। “ছবিতে কী আছে?” — “বোতল।” “বোতলটা কোথায়?” — আঙুল দিয়ে একটি জায়গা দেখানো। “সব জিনিস কোথায় কোথায়?” — প্রতিটির চারপাশে চক দিয়ে চারকোনা দাগ। “তাকের ঠিক কোন অংশে মাল আছে?” — রং দিয়ে মালের প্রতিটি অংশ ভরাট করা। প্রশ্ন যত সূক্ষ্ম, উত্তর তত বড় আর লেবেল বানানোর খরচ তত বেশি।

একই তাক, পাঁচটি কাজ

One shelf, five vision tasks and the shape of each answer

Classification, localisation, detection, semantic and instance segmentation on one shelf ১. Classification bottle ২. Localisation bottle ৩. Object detection .95 .88 box .92 ৪. Semantic seg. bottle নীল box কমলা ৫. Instance seg. #1, #2 আলাদা mask output-এর আকার ১. C টি score ২. ১ box = 4 সংখ্যা ৩. N × (box, class) ৪. H × W map ৫. N × (mask, class)
ছবি ২৫.১ — Classification-এ পুরো ছবির একটি উত্তর; detection-এ যত বস্তু তত box; segmentation-এ প্রতিটি pixel-এর উত্তর। Semantic-এ দুটি বোতল একই রঙ, instance-এ আলাদা।

Classification-এ network-এর শেষে C টি শ্রেণির logits. Localisation-এ (ছবিতে একটিই প্রধান বস্তু ধরে নিয়ে) তার সঙ্গে একটি box-এর চারটি সংখ্যা regression করা হয়। Detection-এ বস্তুর সংখ্যা আগে থেকে জানা নেই: শূন্য, এক বা পঞ্চাশ; output একটি পরিবর্তনশীল দৈর্ঘ্যের তালিকা, যেখানে প্রতিটি সারি (box, শ্রেণি, score). এই “কয়টি জানি না” সমস্যাটিই detection-কে classification-এর চেয়ে কঠিন করে, আর NMS, anchor আর matching-এর মতো সব যন্ত্রপাতি এখান থেকেই আসে। Semantic segmentation প্রতিটি pixel-এ একটি শ্রেণি দেয়, output H × W; একই শ্রেণির দুটি বস্তু ছুঁয়ে থাকলে তাদের আলাদা করে না। Instance segmentation প্রতিটি বস্তুর আলাদা mask দেয়, যেন detection আর segmentation একসঙ্গে। দুটি মিলিয়ে আকাশ বা রাস্তার মতো “অগণনীয়” অংশসহ পুরো ছবি ভাগ করাকে বলে panoptic segmentation (Kirillov ও সহকর্মীরা, ২০১৯)।

ছবি কীভাবে array হয়, channel কী, তা AI-এর ভিত্তি কোর্সের পর্ব ১৮-এ দেখেছেন। এখানে একটি নিয়ম মনে রাখুন: ছবির স্থানাঙ্কে x ডানে বাড়ে, y নিচে বাড়ে, আর (0, 0) বাম-ওপরের কোণ।

২ · Bounding box আর IoU — দুটি box কতটা মেলে

একটি অক্ষ-সমান্তরাল box চারটি সংখ্যায় লেখা যায়, কিন্তু কোন চারটি, তা লাইব্রেরি ভেদে আলাদা। ভুল ফরম্যাট হলো detection কোডের সবচেয়ে সাধারণ নীরব বাগ:

নামচারটি সংখ্যাকোথায় দেখবেন
xyxyx1, y1 (বাম-ওপর), x2, y2 (ডান-নিচ)torchvision-এর detection model আর ops
xywhx1, y1, প্রস্থ, উচ্চতাCOCO-র annotation ফাইল
cxcywhকেন্দ্র x, কেন্দ্র y, প্রস্থ, উচ্চতা; প্রায়ই ০–১-এ normalisedYOLO-ধাঁচের লেবেল ফাইল

torchvision-এ torchvision.ops.box_convert(boxes, in_fmt="xywh", out_fmt="xyxy") এই রূপান্তর করে। এবার মূল মাপ। দুটি box A আর B কতটা মেলে, তা বলে Intersection over Union:

IoU(A, B) = |A ∩ B| ÷ |A ∪ B| = ছেদের ক্ষেত্রফল ÷ (|A| + |B| − ছেদের ক্ষেত্রফল)

|A| মানে A-এর ক্ষেত্রফল। IoU ০ মানে একেবারে না মেলা, ১ মানে হুবহু এক। ধারণাটি পুরোনো: উদ্ভিদবিজ্ঞানী Paul Jaccard বিশ শতকের শুরুতে দুটি এলাকার উদ্ভিদ-প্রজাতির মিল মাপতে এই অনুপাত ব্যবহার করেন, তাই একে Jaccard index-ও বলে। ছেদ বের করার কৌশল: ছেদের বাম-ওপর কোণ হলো দুটি বাম-ওপর কোণের বড়টি, ডান-নিচ কোণ দুটি ডান-নিচ কোণের ছোটটি; প্রস্থ বা উচ্চতা ঋণাত্মক হলে ছেদ নেই।

Box = [x1, y1, x2, y2]; ছেদ = [max(x1), max(y1), min(x2), min(y2)]
ধাপ
হিসাব
মানে
  1. ছেদের কোণ. বাম-ওপর কোণ (৩০, ৪০), ডান-নিচ কোণ (৫০, ৮০)। y নিচে বাড়ে, তাই “ওপর” মানে ছোট y.
  2. প্রস্থ আর উচ্চতা. ২০ আর ৪০, দুটিই ধনাত্মক। কোনোটি ঋণাত্মক হলে কোডে max(0, …) দিয়ে ০ করা হয়, নইলে দুটি ঋণাত্মক গুণ করে ভুয়া ধনাত্মক ক্ষেত্রফল আসত।
  3. ছেদের ক্ষেত্রফল. ২০ × ৪০ = ৮০০ বর্গ pixel.
  4. দুটি box. A-এর ৪০ × ৬০ = ২৪০০, B-এর ৪০ × ৫০ = ২০০০. এদের যোগে ছেদের ৮০০ দুবার গোনা।
  5. সংযোগ. ২৪০০ + ২০০০ − ৮০০ = ৩৬০০.
  6. IoU. ৮০০ ÷ ৩৬০০ ≈ ০.২২২২. প্রচলিত নিয়মে IoU ≥ ০.৫ হলে দুটি box-কে “একই বস্তু” ধরা হয়; এখানে তা নয়।

IoU-এর একটি গুণ হলো এটি মাপ-নিরপেক্ষ: দুটি box-কে একসঙ্গে দ্বিগুণ বড় করলে IoU বদলায় না। তাই ছোট আর বড় বস্তুর জন্য একই threshold চলে। কিন্তু ছোট বস্তুতে কয়েক pixel সরে যাওয়াও IoU অনেক কমায়: ১০ × ১০ box ২ pixel ডানে সরলে IoU ৮০ ÷ ১২০ ≈ ০.৬৭, অথচ ১০০ × ১০০ box ২ pixel সরলে প্রায় ০.৯৬. ছোট বস্তু detection-এ কঠিন হওয়ার একটি কারণ এটাই।

৩ · NumPy-তে IoU আর NMS — তাকের ওপর একাধিক box ছাঁটা

একটি detector প্রায় কখনোই একটি বস্তুর জন্য একটি box দেয় না। কাছাকাছি অনেক অবস্থান থেকে একই বোতল “দেখা” যায়, তাই একই বস্তুর চারপাশে কয়েকটি একটু আলাদা box আসে, প্রতিটির একটি confidence score. Non-maximum suppression (NMS) এই ডুপ্লিকেট ছাঁটে, greedy নিয়মে:

  1. সব box score অনুযায়ী বড় থেকে ছোট সাজান।
  2. তালিকার প্রথমটি (সর্বোচ্চ score) রাখুন।
  3. তালিকার বাকি যেসব box-এর তার সঙ্গে IoU threshold-এর বেশি, সেগুলো বাদ দিন।
  4. তালিকা খালি না হওয়া পর্যন্ত ধাপ ২-এ ফিরুন।

নিচের ব্লকে বাজারঘরের গুদামের একটি কৃত্রিম তাক: চারটি পণ্য (দুটি বোতল, একটি বাক্স, একটি বয়াম), আর একটি কাল্পনিক detector-এর আটটি box, d0 থেকে d7. box_iou vectorised: N × 4 আর M × 4 থেকে broadcasting দিয়ে একবারে N × M IoU-এর matrix.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

def box_iou(A, B):                       # A: (N,4), B: (M,4), [x1,y1,x2,y2] -> (N,M)
    lt = np.maximum(A[:, None, :2], B[None, :, :2])   # ছেদের বাম-ওপর কোণ
    rb = np.minimum(A[:, None, 2:], B[None, :, 2:])   # ছেদের ডান-নিচ কোণ
    wh = np.clip(rb - lt, 0, None)                     # না মিললে প্রস্থ 0
    inter = wh[..., 0] * wh[..., 1]
    area = lambda X: (X[:, 2] - X[:, 0]) * (X[:, 3] - X[:, 1])
    return inter / (area(A)[:, None] + area(B)[None, :] - inter)
def nms(boxes, scores, thr=0.5):         # greedy: সেরাটি রাখা, বেশি মেলা বাকিগুলো বাদ
    order, keep = np.argsort(-scores), []
    while order.size:
        i, rest = order[0], order[1:]
        keep.append(int(i))
        order = rest[box_iou(boxes[[i]], boxes[rest])[0] <= thr]
    return keep
A, B = np.array([[10, 20, 50, 80.]]), np.array([[30, 40, 70, 90.]])
print("IoU(A, B) =", round(box_iou(A, B)[0, 0], 4))
D = np.array([[21, 11, 46, 53], [18, 8, 44, 50], [59, 12, 86, 51], [62, 6, 90, 55],
              [108, 18, 168, 53], [130, 22, 175, 50], [188, 26, 226, 53], [40, 15, 65, 50]], float)
S = np.array([0.92, 0.81, 0.88, 0.55, 0.95, 0.60, 0.70, 0.40])
for thr in (0.5, 0.3): print(f"NMS thr {thr}: keep", nms(D, S, thr))
img = np.full((70, 240, 3), 0.93); img[54:60] = (0.55, 0.38, 0.22)    # দেয়াল আর তাক
for (x1, y1, x2, y2), c in zip([[20, 10, 45, 54], [60, 10, 86, 54], [110, 20, 170, 54], [190, 26, 225, 54]],
                               [(.1, .5, .2), (.2, .4, .8), (.9, .6, .2), (.8, .2, .3)]):
    img[y1:y2, x1:x2] = c                                         # চারটি পণ্য
keep = nms(D, S, 0.5); fig, ax = plt.subplots(figsize=(7, 2.6)); ax.imshow(img)
for k, ((x1, y1, x2, y2), s) in enumerate(zip(D, S)):
    on = k in keep                                                # রাখা হলে মোটা কালো, নইলে ড্যাশ
    ax.add_patch(Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, lw=2 if on else 1,
                           ec="black" if on else "gray", ls="-" if on else "--"))
    ax.text(x1, y1 - 1 if on and k != 7 else y2 + 9, f"d{k} {s:.2f}", fontsize=7)
ax.set_title("Shelf: kept boxes solid, suppressed dashed (thr 0.5)"); ax.axis("off"); plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে তাকের ছবি):

IoU(A, B) = 0.2222
NMS thr 0.5: keep [4, 0, 2, 6, 5, 7]
NMS thr 0.3: keep [4, 0, 2, 6, 7]

প্রথম লাইন stepper-এর হিসাবই মিলিয়ে দেয়: ০.২২২২. threshold ০.৫-এ আটটির মধ্যে ছয়টি টিকে থাকে: d1 (প্রথম বোতলের ডুপ্লিকেট) আর d3 (দ্বিতীয় বোতলের ডুপ্লিকেট) বাদ। ছবিতে দুটি জিনিস দেখার মতো। d5 বাক্সের ডান অর্ধেক ঢাকে, d4-এর সঙ্গে IoU ০.৪৬৩, তাই ০.৫-এ টিকে যায় আর একটি বাক্স দুবার গোনা হয়; threshold ০.৩-এ সে বাদ পড়ে। d7 দুটি বোতলের মাঝের ফাঁকা জায়গার একটি ভুল box; কোনো box-এর সঙ্গে বেশি মেলে না, তাই NMS তাকে ছোঁয় না। NMS শুধু ডুপ্লিকেট সরায়, ভুল box সরায় না; সেটি সরে score threshold দিয়ে (যেমন score ≥ ০.৫ রাখলে d7 বাদ)।

score অনুযায়ী সাজানো: d4 .95, d0 .92, d2 .88, d1 .81, d6 .70, d5 .60, d3 .55, d7 .40
ধাপ
বাছাইয়ের সঙ্গে IoU
বাদ
রাখা (keep)
বাকি তালিকা
  1. শুরু. আটটি box score অনুযায়ী সাজানো। কিছুই রাখা হয়নি।
  2. d4 (০.৯৫). সর্বোচ্চ, রাখা হলো। d5-এর সঙ্গে IoU ০.৪৬৩, threshold-এর নিচে, তাই d5 তালিকায় থেকে যায়।
  3. d0 (০.৯২). রাখা হলো। d1-এর সঙ্গে IoU ০.৭২০ > ০.৫, তাই d1 বাদ: একই বোতলের কম নিশ্চিত box.
  4. d2 (০.৮৮). রাখা হলো। d3-এর সঙ্গে ০.৬২৯, তাই d3 বাদ। d7 দুই বোতলের সঙ্গেই মাত্র ০.১২২ মেলে, টিকে থাকে।
  5. বাকি তিনটি. d6, d5, d7 পরস্পর ছোঁয় না, তিনটিই রাখা হলো। NMS শেষ: আটটির মধ্যে ছয়টি।
  6. Score threshold. NMS-এর পরে (বা আগে) কম score-এর box ফেলা আলাদা ধাপ। ০.৫ নিলে d7 যায়; d5 থেকে যায়, কারণ তার score ০.৬০.

এখন যাচাই। একই NumPy function দুটি ডেস্কটপে ৫০০টি এলোমেলো box-এ চালিয়ে torchvision-এর C++ বাস্তবায়নের সঙ্গে মেলানো হলো (torchvision.ops.nms নিয়ম লেখে: IoU > threshold হলে বাদ, তাই NumPy-তে <= thr রাখা হয়েছে)। সত্যিকারের run, লেখার সময়ের সংস্করণ:

~/bazarghar-dl$ uv run python nms_check.py
Python 3.14.4 | torch 2.15.0+cpu | torchvision 0.29.2+cpu
box_iou: 500x500 matrix, max |numpy - torchvision| = 9.23e-07
nms thr 0.3: numpy keeps 239, torchvision keeps 239, identical order: True
nms thr 0.5: numpy keeps 409, torchvision keeps 409, identical order: True
nms thr 0.7: numpy keeps 488, torchvision keeps 488, identical order: True
batched_nms (3 classes, thr 0.5) keeps 462: boxes of different classes never suppress each other

IoU-এর ৫০০ × ৫০০ matrix-এ সবচেয়ে বড় পার্থক্য float32-এর গোলকরণের মাপে, আর তিনটি threshold-এই রাখা box-এর তালিকা ক্রমসহ এক। শেষ লাইনটি বাস্তবের একটি খুঁটিনাটি: আসল detector-এ NMS চলে প্রতিটি শ্রেণির ভেতরে আলাদাভাবে (batched_nms), যাতে একটি বোতল তার গায়ে লাগা কাপের box মুছে না দেয়।

৪ · Detector কীভাবে box খোঁজে — anchor, two-stage আর one-stage

প্রথম ভাবনা হতে পারে: ছবির ওপর অনেক আকারের জানালা সরিয়ে প্রতিটিতে classifier চালানো (sliding window). কাজ করে, কিন্তু হাজার হাজার জানালায় আলাদা CNN চালানো অত্যন্ত ধীর। আধুনিক detector দুটি চালাকি করে। প্রথমত, পুরো ছবিতে একবার CNN চালিয়ে একটি feature map নেয়, তারপর জানালার কাজ সেই map-এর ওপর করে, কারণ convolution-এর পরে map-এর প্রতিটি অবস্থান ছবির একটি অংশের সারাংশ (পর্ব ২০, Convolution-এর receptive field). দ্বিতীয়ত, প্রতিটি অবস্থানে কয়েকটি পূর্বনির্ধারিত box রাখে, যাদের বলে anchor: যেমন অংশ ৬-এর torchvision model-এর RPN-এ ৫টি মাপ (৩২ থেকে ৫১২ pixel) × ৩টি অনুপাত (০.৫, ১, ২) = ১৫টি। Network প্রতিটি anchor-এর জন্য দুটি জিনিস শেখে: এতে বস্তু আছে কি না (আর কোন শ্রেণির), আর anchor-কে কতটা সরিয়ে-বড় করে আসল box পাওয়া যায় (চারটি offset).

Training-এর সময় প্রতিটি anchor-কে লেবেল দেওয়া হয় IoU দিয়েই: কোনো ground-truth box-এর সঙ্গে IoU বেশি হলে (Faster R-CNN-এর RPN-এ ০.৭-এর ওপরে) “বস্তু”, খুব কম হলে (০.৩-এর নিচে) “পটভূমি”, মাঝের গুলো উপেক্ষা। তাই IoU শুধু মূল্যায়নের মাপ নয়, training-এর লেবেল বানানোর যন্ত্রও। পটভূমির anchor বস্তুর anchor-এর চেয়ে বহু গুণ বেশি; এই ভারসাম্যহীনতা সামলাতে focal loss প্রস্তাব হয়েছিল (পর্ব ১৩)।

Two-stage: Faster R-CNN

Ren, He, Girshick আর Sun (২০১৫)। প্রথম ধাপে একটি ছোট network (Region Proposal Network) anchor থেকে কয়েকশো “সম্ভাব্য বস্তু”-র box প্রস্তাব করে। দ্বিতীয় ধাপে প্রতিটি প্রস্তাবের feature কেটে (RoI pooling বা RoIAlign) শ্রেণি আর সূক্ষ্ম box বের করে। সাধারণত বেশি নিখুঁত, তুলনায় ধীর। আগের R-CNN (২০১৪) আর Fast R-CNN (২০১৫) থেকে ধাপে ধাপে এসেছে।

One-stage: YOLO, SSD, RetinaNet

একবারেই প্রতিটি অবস্থান থেকে শ্রেণি আর box বের করে, আলাদা প্রস্তাবের ধাপ নেই। YOLO (Redmon ও সহকর্মীরা, ২০১৬) ছবিকে grid-এ ভাগ করে; SSD (Liu ও সহকর্মীরা, ২০১৬) কয়েকটি মাপের feature map থেকে predict করে। দ্রুত, তাই ভিডিও আর ছোট যন্ত্রে জনপ্রিয়। “YOLO” নামে পরে বহু দল বহু সংস্করণ এনেছে; সংস্করণের নম্বর একটি ধারাবাহিক গবেষণা নয়।

Anchor-free আর DETR

পরে anchor ছাড়াই প্রতিটি অবস্থান থেকে box-এর চার দিকের দূরত্ব predict করার পদ্ধতি এসেছে (যেমন FCOS, ২০১৯)। DETR (Carion ও সহকর্মীরা, ২০২০) Transformer দিয়ে সরাসরি box-এর একটি set predict করে আর matching দিয়ে শেখে, ফলে NMS লাগে না। Transformer পর্ব ৩৫-এ আসবে।

একটি সাধারণ অংশ প্রায় সব আধুনিক detector-এ: backbone (ResNet বা MobileNet-এর মতো CNN, প্রায়ই ImageNet-এ pretrained, পর্ব ২২ আর ২৩-এর বিষয়) আর তার ওপর Feature Pyramid Network (FPN; Lin ও সহকর্মীরা, ২০১৭), যা গভীর স্তরের “কী” আর অগভীর স্তরের “কোথায়” মিলিয়ে কয়েকটি মাপের feature map বানায়। বড় বস্তু খোঁজা হয় ছোট (মোটা দাগের) map-এ, ছোট বস্তু বড় map-এ। গতি আর নির্ভুলতার তুলনা নির্ভর করে backbone, ছবির মাপ আর যন্ত্রের ওপর; “one-stage সবসময় দ্রুত” বা “two-stage সবসময় নিখুঁত” কোনোটিই নিয়ম নয়, নিজের যন্ত্রে মেপে দেখতে হয়।

৫ · mAP — detector-এর স্কোরকার্ড

Classifier-এর accuracy এক সংখ্যায় বলা যায়। Detector-এর ক্ষেত্রে প্রতিটি predicted box-কে আগে ঠিক বা ভুল ঘোষণা করতে হয়, তারপর precision আর recall (AI-এর ভিত্তি কোর্সের পর্ব ৬০)। নিয়মটি ক্রিকেটের স্কোরকার্ডের মতো কড়া:

  • একটি শ্রেণির সব detection, সব ছবি মিলিয়ে, score অনুযায়ী সাজান।
  • প্রতিটি detection-কে তার ছবির সবচেয়ে বেশি মেলা ground-truth box-এর সঙ্গে মেলান। IoU ≥ ০.৫ আর সেই ground truth আগে কারও সঙ্গে মেলেনি হলে TP; নইলে FP. একই বোতলের দ্বিতীয় box তাই FP: ডুপ্লিকেটের শাস্তি এখানেই।
  • প্রতিটি অবস্থানে জমা TP থেকে precision = TP ÷ (এ পর্যন্ত detection) আর recall = TP ÷ (মোট ground truth).
  • AP = precision-recall curve-এর নিচের ক্ষেত্রফল, তবে আগে curve-কে “interpolate” করা হয়: প্রতিটি recall-এ precision হিসেবে নেওয়া হয় ওই recall বা তার ডানের সর্বোচ্চ precision. ফলে curve সিঁড়ির মতো, কখনো ওপরে ওঠে না।

নিচের ব্লকে দুটি ছবিতে মোট চারটি বোতল আর সাতটি detection; ডেটা হাতে বানানো যাতে প্রতিটি ধরনের ঘটনা (ঠিক, ডুপ্লিকেট, কম IoU, পটভূমি) একবার করে আসে।

import numpy as np
import matplotlib.pyplot as plt

def iou1(a, g):                          # একটি box বনাম অনেক ground-truth box
    w = np.clip(np.minimum(a[2], g[:, 2]) - np.maximum(a[0], g[:, 0]), 0, None)
    h = np.clip(np.minimum(a[3], g[:, 3]) - np.maximum(a[1], g[:, 1]), 0, None)
    area = lambda X: (X[..., 2] - X[..., 0]) * (X[..., 3] - X[..., 1])
    return w * h / (area(a) + area(g) - w * h)

gt = {"A": np.array([[10, 10, 40, 60], [60, 10, 90, 60.]]),      # দুটি ছবি, মোট ৪টি bottle
      "B": np.array([[20, 15, 50, 65], [70, 20, 100, 70.]])}
dets = [("A", .95, [12, 12, 41, 61]), ("B", .90, [21, 14, 52, 66]), ("A", .80, [9, 8, 39, 58]),
        ("A", .70, [58, 12, 88, 62]), ("B", .60, [80, 35, 110, 85]), ("A", .50, [100, 30, 130, 80]),
        ("B", .40, [68, 18, 99, 69])]                              # score অনুযায়ী সাজানো
used = {k: np.zeros(len(v), bool) for k, v in gt.items()}
n_gt, tp, rows = 4, [], []
for img, s, box in dets:
    ious = iou1(np.array(box, float), gt[img]); j = ious.argmax()
    ok = ious[j] >= 0.5 and not used[img][j]                     # একটি GT একবারই মেলে
    used[img][j] |= ok; tp.append(ok)
    rows.append((img, s, round(ious[j], 3), "TP" if ok else "FP"))
ctp = np.cumsum(tp); prec = ctp / np.arange(1, len(tp) + 1); rec = ctp / n_gt
print("img score maxIoU  TP/FP precision recall")
for (img, s, m, t), p, r in zip(rows, prec, rec):
    print(f" {img}   {s:.2f}  {m:5.3f}   {t}    {p:6.3f}   {r:5.2f}")
r = np.concatenate([[0], rec, [1]]); p = np.concatenate([[1], prec, [0]])
p_env = np.maximum.accumulate(p[::-1])[::-1]                     # ডানদিকের সর্বোচ্চ precision
idx = np.where(r[1:] != r[:-1])[0]
print("AP (all-point) =", round(np.sum((r[idx + 1] - r[idx]) * p_env[idx + 1]), 4))
grid = np.linspace(0, 1, 101)
print("AP (COCO 101-point) =", round(np.mean([p_env[r >= t].max() for t in grid]), 4))
plt.figure(figsize=(4.6, 3.2)); plt.plot(rec, prec, "o--", label="raw")
plt.step(r[:-1], p_env[:-1], where="post", label="interpolated"); plt.xlabel("recall"); plt.ylabel("precision")
plt.title("PR curve, class 'bottle', IoU 0.5"); plt.legend(); plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে PR curve):

img score maxIoU  TP/FP precision recall
 A   0.95  0.852   TP     1.000    0.25
 B   0.90  0.872   TP     1.000    0.50
 A   0.80  0.866   FP     0.667    0.50
 A   0.70  0.812   TP     0.750    0.75
 B   0.60  0.304   FP     0.600    0.75
 A   0.50  0.000   FP     0.500    0.75
 B   0.40  0.856   TP     0.571    1.00
AP (all-point) = 0.8304
AP (COCO 101-point) = 0.832

তৃতীয় সারির box-টি প্রথম বোতলের সঙ্গে ০.৮৬৬ মেলে, তবু FP: বোতলটি আগেই ০.৯৫-এর box পেয়ে গেছে। পঞ্চম সারি একটি বোতল ছোঁয়, কিন্তু IoU ০.৩০৪, তাই FP; ষষ্ঠটি পটভূমিতে। সপ্তমটি শেষ বোতল খুঁজে recall ১.০ করে। এখন interpolation: recall ০.৫ থেকে ০.৭৫-এর অংশে precision নেওয়া হয় ০.৭৫ (ডানের সর্বোচ্চ), আর ০.৭৫ থেকে ১.০-তে ০.৫৭১. তাই AP = ০.২৫ × ১ + ০.২৫ × ১ + ০.২৫ × ০.৭৫ + ০.২৫ × ০.৫৭১৪ = ০.৮৩০৪. COCO-র নিয়মে ০, ০.০১, …, ১.০ এই ১০১টি recall-এ interpolated precision-এর গড়, ০.৮৩২: প্রায় একই, কিন্তু হুবহু নয়। কোন নিয়মে AP মাপা হয়েছে না জানলে দুটি প্রবন্ধের সংখ্যা তুলনা করা যায় না।

AP থেকে mAP. প্রতিটি শ্রেণির AP-এর গড় হলো mAP. PASCAL VOC-তে IoU threshold একটিই, ০.৫ (তাই লেখা হয় AP50 বা mAP@0.5). COCO আরও কড়া: IoU ০.৫০, ০.৫৫, …, ০.৯৫ এই দশটি threshold-এ AP মেপে গড় করে, যাকে সাধারণত শুধু “AP” বা mAP@[.5:.95] লেখা হয়; ফলে box-এর অবস্থান যত নিখুঁত, নম্বর তত বেশি। COCO বস্তুর মাপ ধরে APS, APM, APL-ও আলাদা দেয়। ভারসাম্যহীন ডেটায় precision-recall curve কেন ROC-এর চেয়ে বেশি বলে, তা মেশিন লার্নিং কোর্সের পর্ব ৫০-এ আছে।

৬ · সত্যিকারের detector — torchvision-এর pretrained model

এবার একটি আসল, COCO-তে প্রশিক্ষিত detector. torchvision-এ দুটি ছোট model বেছে নেওয়া হলো, যাতে ল্যাপটপের CPU-তে চলে: fasterrcnn_mobilenet_v3_large_fpn (two-stage, MobileNetV3 backbone আর FPN) আর ssdlite320_mobilenet_v3_large (one-stage, ৩২০ pixel ছবি)। Weights এসেছে PyTorch-এর অফিশিয়াল সার্ভার থেকে, weights enum FasterRCNN_MobileNet_V3_Large_FPN_Weights.COCO_V1 আর SSDLite320_MobileNet_V3_Large_Weights.COCO_V1. COCO-র ৮০টি শ্রেণির মধ্যে আছে bottle, cup, chair, spoon, dining table ইত্যাদি। ছবি হিসেবে নেওয়া হলো scikit-image লাইব্রেরির সঙ্গে আসা data.coffee(): একটি কফির কাপের ছবি, যার লাইসেন্স scikit-image-এর নথিতে CC0 লেখা; ছবিতে কোনো মানুষ নেই।

W = FasterRCNN_MobileNet_V3_Large_FPN_Weights.COCO_V1
model = fasterrcnn_mobilenet_v3_large_fpn(weights=W).eval()
pre, cats = W.transforms(), W.meta["categories"]
x = [pre(img)]                          # একটি CHW uint8 ছবির list
with torch.inference_mode():
    out = model(x)[0]                   # dict: boxes (N,4 xyxy), labels, scores
for b, l, s in zip(out["boxes"], out["labels"], out["scores"]):
    if s >= 0.3:
        x1, y1, x2, y2 = [round(v) for v in b.tolist()]
        print(f"  {cats[l]:<14s} score {s:.3f}  box [{x1}, {y1}, {x2}, {y2}]")

পুরো ফাইল একই loop দুটি model-এ চালায়, সময় মাপে আর ০.৩-এর নিচের box গোনে। সত্যিকারের run (ডেস্কটপ, CPU-তে; সময় এক run-এর, warm-up-এর পরে):

~/bazarghar-dl$ TORCH_HOME=./torch_home uv run python detect.py
image: skimage.data.coffee() (CC0) (3, 400, 600) torch.uint8

FasterRCNN_MobileNet_V3_Large_FPN_Weights.COCO_V1  params 19.39M  CPU time 418 ms  raw detections 5
  COCO box mAP (from weights meta): 32.8
  cup            score 0.995  box [164, 19, 421, 311]
  dining table   score 0.969  box [9, 0, 596, 395]
  below 0.3: 3 boxes, highest: [('spoon', 0.296), ('bowl', 0.147), ('spoon', 0.09)]

SSDLite320_MobileNet_V3_Large_Weights.COCO_V1  params 3.44M  CPU time 70 ms  raw detections 300
  COCO box mAP (from weights meta): 21.3
  cup            score 0.991  box [170, 19, 418, 265]
  dining table   score 0.866  box [5, 2, 596, 399]
  below 0.3: 298 boxes, highest: [('cup', 0.231), ('spoon', 0.064), ('spoon', 0.031)]

দুটি model-ই কাপটি প্রায় নিশ্চিতভাবে খুঁজেছে (০.৯৯৫ আর ০.৯৯১), আর পুরো ছবি জুড়ে কাঠের টেবিলকে বলেছে “dining table”. কিন্তু box আলাদা: Faster R-CNN-এর box নিচে ৩১১ পর্যন্ত নেমে হাতল ধরেছে, SSDLite-এর box ২৬৫-তে থেমে হাতলের বেশির ভাগ বাদ দিয়েছে। দুটি box-এর IoU প্রায় ০.৮১: একই আসল box-এর বিপরীতে IoU ০.৫-এ দুটিই সহজে TP হতে পারত, কিন্তু COCO-র ০.৯-এর মতো কড়া threshold-এ দুটিরই TP হওয়া কঠিন। ছবিতে স্পষ্ট দেখা যায় এমন চামচটি Faster R-CNN দেখেছে ০.২৯৬ score-এ, ০.৩-এর ঠিক নিচে: threshold বাছাই একটি সিদ্ধান্ত, প্রকৃতির নিয়ম নয়। SSDLite ৩০০টি box ফেরত দিয়েছে, কারণ তার builder-এর default score threshold মাত্র ০.০০১ আর সর্বোচ্চ ৩০০টি box; নিজের threshold না বসালে output অর্থহীন লম্বা হয়, যেখানে Faster R-CNN-এর default ০.০৫। SSDLite-এর parameter প্রায় ছয় ভাগের এক ভাগ আর এই CPU-তে প্রায় ছয় গুণ দ্রুত, কিন্তু weights-এর meta-তে লেখা COCO box mAP ২১.৩ বনাম ৩২.৮. সময়গুলো এক যন্ত্রের এক run, সাধারণ ফল নয়।

কয়েকটি ব্যবহারিক কথা এই run থেকে। Model-কে দিতে হয় ছবির একটি list, কারণ ছবির মাপ আলাদা হতে পারে; ভেতরে model নিজেই মাপ বদলায় আর box মূল ছবির স্থানাঙ্কে ফেরত দেয়। Model-এর ভেতরে NMS আর score threshold-এর ধাপ আগে থেকেই আছে (Faster R-CNN-এ builder-এর box_score_thresh, box_nms_thresh), তাই output-এ সাধারণত ডুপ্লিকেট কম। .eval() না দিলে model training mode-এ থাকে আর target চায়। আর একটি সতর্কতা: torchvision-এর নথি বলে, pretrained model-এর নিজস্ব লাইসেন্স বা শর্ত থাকতে পারে যা training ডেটা থেকে আসে, আর ব্যবহারের অনুমতি আছে কি না তা ব্যবহারকারীকেই যাচাই করতে হয়; model card আর লাইসেন্সের অভ্যাস AI-এর ভিত্তি কোর্সের পর্ব ৭১-এ। নিজের পণ্যের জন্য সাধারণত এমন pretrained detector নিয়ে নিজের ছবিতে fine-tune করা হয় (পর্ব ২৩, Transfer learning).

৭ · Semantic segmentation আর U-Net — encoder, decoder আর skip connection

Segmentation-এ প্রতিটি pixel-এর জন্য একটি উত্তর চাই, তাই output-এর মাপ input-এর সমান। কিন্তু classification CNN স্তরে স্তরে ছবি ছোট করে (pooling, stride), যাতে প্রতিটি অবস্থান বড় এলাকা দেখে আর “কী” বোঝে। এই দুই চাহিদা মেলানোর কাঠামো encoder–decoder: encoder ছোট করে অর্থ বোঝে, decoder আবার বড় করে অবস্থান ফেরায়। Long, Shelhamer আর Darrell-এর Fully Convolutional Network (CVPR ২০১৫) দেখায় যে শেষের fully connected স্তর বাদ দিয়ে পুরোটা convolution রাখলে যেকোনো মাপের ছবিতে pixel ধরে predict করা যায়।

২০১৫ সালে Olaf Ronneberger, Philipp Fischer আর Thomas Brox চিকিৎসার microscopy ছবিতে কোষ আলাদা করতে U-Net প্রস্তাব করেন। নাম এসেছে আকার থেকে: বাম বাহু encoder নামে, ডান বাহু decoder ওঠে, আর প্রতিটি স্তরে encoder-এর feature map সরাসরি decoder-এর একই মাপের স্তরে পাঠানো হয় এবং channel ধরে জোড়া (concatenate) হয়। এই skip connection পর্ব ১৭-এর residual connection-এর মতো যোগ নয়, পাশাপাশি জোড়া; উদ্দেশ্যও আলাদা: gradient নয়, স্থানিক খুঁটিনাটি পৌঁছে দেওয়া।

ছোট U-Net-এর কাঠামো (ডেস্কটপ run-এর model)

Channels × height × width; dashed arrows = skip connections (concatenate)

Tiny U-Net: encoder, bottleneck, decoder and skip connections enc116×64×64 enc232×32×32 bottleneck64×16×16 dec2in 32+32 → 32 dec1in 16+16 → 16 max-pool ÷2 max-pool ÷2 ConvT ×2 ConvT ×2 skip 1 (concat) skip 2 (concat) 1×1 conv → mask 1×64×64
ছবি ২৫.২ — বাম বাহু ছবি ছোট করে channel বাড়ায়, ডান বাহু উল্টো। সবুজ ড্যাশ তীর encoder-এর বড় মাপের map decoder-এ পৌঁছে দেয়।

ছোট থেকে বড় করা: transposed convolution. Decoder-এ মাপ বাড়ানোর একটি উপায় সাধারণ upsampling (প্রতিটি pixel পাশাপাশি কপি করা, বা bilinear), তারপর convolution. আরেকটি শেখা যায় এমন উপায় nn.ConvTranspose2d: input-এর প্রতিটি মান kernel দিয়ে গুণ হয়ে output-এ একটি ছোট টুকরো “ছাপ” দেয়, আর stride সেই ছাপগুলোর দূরত্ব ঠিক করে। Output-এর মাপ (dilation আর output_padding ছাড়া):

out = (in − 1) × stride − 2 × padding + kernel

U-Net-এর মতো kernel 2, stride 2, padding 0 নিলে (16 − 1) × 2 + 2 = ৩২: ঠিক দ্বিগুণ, আর ছাপগুলো একটি আরেকটির ওপর পড়ে না। Kernel ৩ আর stride ২ নিলে ছাপ অসমানভাবে ওভারল্যাপ করে, আর output-এ দাবার ছকের মতো দাগ (checkerboard artifact) আসতে পারে; Odena, Dumoulin আর Olah ২০১৬ সালে এর ব্যাখ্যা দেন, আর upsampling-এর পরে convolution বিকল্প হিসেবে প্রস্তাব করেন। নাম “transposed” কারণ সাধারণ convolution-কে matrix হিসেবে লিখলে এটি তার transpose-এর গুণ, যা convolution-এর backward pass-এর হিসাবও। একে “deconvolution” বলাটা প্রচলিত হলেও গাণিতিকভাবে ভুল নাম: এটি convolution-কে উল্টায় না।

Skip connection কেন লাগে. নিচের stepper একটি ৮ pixel-এর সারি দিয়ে দেখায়, max-pooling-এ কী হারায় আর skip তা কীভাবে ফেরায়। ধরে নেওয়া হলো decoder শিখেছে “upsample করা মান আর skip-এর মান দুটিই ১ হলে ১”, অর্থাৎ দুটির ছোটটি; আসল decoder-এ convolution এমন কিছু নিজে শেখে। পাশে আসল ছোট U-Net-এর shape.

সত্যিকারের mask: [0 0 1 1 1 0 0 0] · decoder-এর নিয়ম: min(upsampled, skip)
ধাপ
৮ pixel-এর সারি
ভুল pixel
আসল U-Net-এর shape
  1. Input. বস্তু pixel ২, ৩ আর ৪-এ (গোনা ০ থেকে)। বাম কিনারা জোড়ার শুরুতে পড়েছে, কিন্তু ডান কিনারা (৪, ৫) জোড়ার মাঝখানে; এই কিনারাই pooling-এ হারাবে।
  2. Encoder ধাপ ১. জোড়ায় জোড়ায় max: (0,0) → 0, (1,1) → 1, (1,0) → 1, (0,0) → 0. দৈর্ঘ্য অর্ধেক; বস্তু ৪ নম্বর pixel-এ শেষ নাকি ৫-এ, এই তথ্য হারিয়েছে। এই map-টি skip 2-এর জন্য রেখে দেওয়া হয়।
  3. Bottleneck. আবার max-pool: [1 1]. এখন শুধু জানা যায় “দুই অর্ধেকেই কিছু আছে”: “কী” আছে বোঝা যায়, “ঠিক কোথায়” প্রায় নেই।
  4. Skip ছাড়া. শুধু bottleneck থেকে দুবার দ্বিগুণ করলে পুরো সারি ১: আটটির পাঁচটি pixel ভুল। হারানো তথ্য decoder নিজে বানাতে পারে না।
  5. Skip 2. Upsample করা [1 1 1 1]-এর পাশে encoder-এর [0 1 1 0] জোড়া হলো; নিয়ম অনুযায়ী ফল [0 1 1 0]. আসল U-Net-এ ৩২ + ৩২ = ৬৪ channel জোড়া লেগে dec2-তে ঢোকে।
  6. Skip 1. আবার দ্বিগুণ করে [0 0 1 1 1 1 0 0], এতে pixel ৫ ভুল; পূর্ণ মাপের skip-এর সঙ্গে মিলিয়ে [0 0 1 1 1 0 0 0], হুবহু ঠিক। প্রতিটি skip এক মাপের খুঁটিনাটি ফেরায়।

৮ · Dice আর IoU — segmentation মাপা আর শেখানো

Segmentation-এ একটি mask-কে pixel-এর একটি set ধরলে box-এর IoU একই সংজ্ঞায় চলে: |P ∩ G| ÷ |P ∪ G|, যেখানে P predicted আর G ground-truth pixel. চিকিৎসা-ছবিতে বেশি প্রচলিত Dice coefficient, যা উদ্ভিদবিজ্ঞানে Lee R. Dice (১৯৪৫) আর Thorvald Sørensen (১৯৪৮)-এর মিলের মাপ থেকে এসেছে:

Dice = 2|P ∩ G| ÷ (|P| + |G|) = 2·IoU ÷ (1 + IoU)

দুটি একই ক্রমে চলে (একটি বাড়লে অন্যটিও), তবে Dice সবসময় IoU-এর সমান বা বেশি। Pixel accuracy (ঠিক pixel ÷ মোট pixel) সহজ কিন্তু বিভ্রান্তিকর: পটভূমি যদি ছবির বেশির ভাগ হয়, “সব পটভূমি” বলেও accuracy প্রায় ১। Training-এ hard mask-এর বদলে probability p বসিয়ে soft Dice বানানো যায়, যা differentiable; Dice loss = 1 − soft Dice. Milletari, Navab আর Ahmadi-র V-Net (২০১৬) প্রবন্ধ এটিকে loss হিসেবে জনপ্রিয় করে। প্রায়ই BCE-এর সঙ্গে যোগ করে ব্যবহার হয়: BCE প্রতিটি pixel-এ স্থির gradient দেয়, Dice ছোট বস্তুকে উপেক্ষা হতে দেয় না।

import numpy as np
import matplotlib.pyplot as plt

def dice(p, g, eps=1e-7):                # p, g: 0/1 mask বা 0..1 probability
    return (2 * np.sum(p * g) + eps) / (np.sum(p) + np.sum(g) + eps)

def iou(p, g, eps=1e-7):
    inter = np.sum(p * g)
    return (inter + eps) / (np.sum(p) + np.sum(g) - inter + eps)

g = np.zeros((10, 10)); g[2:8, 2:7] = 1      # ground truth: ৬ × ৫ = ৩০ pixel
p = np.zeros((10, 10)); p[3:9, 3:8] = 1      # prediction: এক ঘর ডানে-নিচে সরানো
print("pixels: gt", int(g.sum()), "| pred", int(p.sum()), "| overlap", int((p * g).sum()))
d, j = dice(p, g), iou(p, g)
print(f"Dice = {d:.4f} | IoU = {j:.4f} | 2*IoU/(1+IoU) = {2 * j / (1 + j):.4f}")
print(f"pixel accuracy = {np.mean(p == g):.2f}")

prob = np.where(p == 1, 0.9, 0.1)            # নিশ্চিত নয় এমন model-এর probability
print(f"soft Dice = {dice(prob, g):.4f} -> Dice loss = {1 - dice(prob, g):.4f}")

g2 = np.zeros((64, 64)); g2[30:34, 30:34] = 1    # বড় ছবিতে ছোট বস্তু: ১৬ pixel
empty = np.zeros((64, 64))                        # model কিছুই খুঁজে পায়নি
print(f"tiny object: pixel accuracy = {np.mean(empty == g2):.4f}, Dice = {dice(empty, g2):.4f}")

rgb = np.ones((10, 10, 3))
rgb[(p == 1) & (g == 1)] = (0.2, 0.7, 0.3)   # TP সবুজ
rgb[(p == 1) & (g == 0)] = (0.9, 0.3, 0.3)   # FP লাল
rgb[(p == 0) & (g == 1)] = (0.3, 0.4, 0.9)   # FN নীল
fig, axs = plt.subplots(1, 3, figsize=(7, 2.6))
for ax, im, t in zip(axs, [g, p, rgb], ["ground truth", "prediction", "TP green, FP red, FN blue"]):
    ax.imshow(im, cmap="gray_r" if im.ndim == 2 else None); ax.set_title(t, fontsize=9)
    ax.set_xticks([]); ax.set_yticks([])
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে তিনটি mask-এর ছবি):

pixels: gt 30 | pred 30 | overlap 20
Dice = 0.6667 | IoU = 0.5000 | 2*IoU/(1+IoU) = 0.6667
pixel accuracy = 0.80
soft Dice = 0.5938 -> Dice loss = 0.4062
tiny object: pixel accuracy = 0.9961, Dice = 0.0000

৩০ pixel-এর বস্তু, prediction এক ঘর ডানে-নিচে সরানো: মেলে ২০টি। Dice = ৪০ ÷ ৬০ = ০.৬৬৬৭, IoU = ২০ ÷ ৪০ = ০.৫, আর সূত্র 2·IoU ÷ (1 + IoU) একই ০.৬৬৬৭ দেয়। Pixel accuracy ০.৮০, শুনতে ভালো, কারণ ১০০টি pixel-এর ৭০টিই পটভূমি। Soft Dice ০.৫৯৩৮: model ০.৯-এর বেশি নিশ্চিত হলে loss কমবে, তাই এটি শেখানোর সংকেত দেয়। শেষ লাইনটি সবচেয়ে জরুরি: ৬৪ × ৬৪ ছবিতে ১৬ pixel-এর বস্তু, model কিছুই খুঁজে পায়নি, তবু pixel accuracy ০.৯৯৬১; Dice ০, যা সত্যি কথা বলে। (দুটি mask-ই খালি হলে Dice-এর হর ০; এখানে ছোট eps দিয়ে তা ১ ধরা হয়েছে, যা প্রচলিত কিন্তু একটি সিদ্ধান্ত।)

এবার সব মিলিয়ে একটি সত্যিকারের ছোট U-Net. ডেটা seed দিয়ে বানানো বাজারঘরের কৃত্রিম “পণ্যের ছবি”: ৬৪ × ৬৪ ধূসর ছবিতে ১–৩টি উপবৃত্ত, বাক্স আর সরু দণ্ড (বোতলের গলার মতো), ছায়া আর noise-সহ; mask হলো বস্তুর pixel. ২০০০টি train, ৪০০টি validation ছবি। একই model দুবার শেখানো হলো: skip connection-সহ, আর skip বাদ দিয়ে (decoder শুধু upsampled map পায়)। Loss = BCE with logits + (1 − soft Dice), AdamW lr 3e-3, batch ৩২, ৮ epoch. প্রতিটি block-এ দুটি 3 × 3 convolution, BatchNorm আর ReLU.

~/bazarghar-dl$ uv run python unet_train.py
Python 3.14.4 | torch 2.15.0+cpu | CPU
train (2000, 1, 64, 64), val (400, 1, 64, 64), foreground pixels in val: 0.094
shapes through TinyUNet (batch 2):
  input          (2, 1, 64, 64)
  enc1 (skip 1)  (2, 16, 64, 64)
  enc2 (skip 2)  (2, 32, 32, 32)
  bottleneck     (2, 64, 16, 16)
  up2            (2, 32, 32, 32)
  dec2           (2, 32, 32, 32)
  up1            (2, 16, 64, 64)
  dec1           (2, 16, 64, 64)
  logits         (2, 1, 64, 64)

U-Net (skips): 117,393 parameters
epoch  train loss  val Dice  val IoU  edge acc
    1      0.9162    0.9289   0.8672     0.7921
    2      0.3485    0.9638   0.9301     0.8907
    3      0.1346    0.9654   0.9332     0.8994
    4      0.0865    0.9688   0.9394     0.9067
    5      0.0672    0.9612   0.9254     0.8868
    6      0.0610    0.9701   0.9419     0.9122
    7      0.0548    0.9715   0.9445     0.9141
    8      0.0509    0.9714   0.9443     0.9146
time: 108 s on CPU

no skips: 105,873 parameters
epoch  train loss  val Dice  val IoU  edge acc
    1      0.7871    0.9096   0.8343     0.7305
    2      0.2929    0.9442   0.8943     0.8259
    3      0.1354    0.9523   0.9089     0.8514
    4      0.0912    0.9612   0.9252     0.8842
    5      0.0785    0.9571   0.9177     0.8767
    6      0.0711    0.9625   0.9277     0.8864
    7      0.0637    0.9656   0.9335     0.8978
    8      0.0593    0.9668   0.9357     0.9004
time: 86 s on CPU

প্রথমে shape-এর তালিকা: stepper-এর শেষ সারির সঙ্গে হুবহু মেলে (সামনে batch-এর মাত্রা ২ বাড়তি)। U-Net প্রথম epoch-এই validation Dice ০.৯২৮৯ পেয়েছে, আট epoch-এ ০.৯৭১৪ (IoU ০.৯৪৪৩)। শেষ কলাম “edge acc” হলো বস্তুর কিনারার দুই পাশের এক pixel চওড়া পট্টিতে ঠিক pixel-এর অনুপাত; খুঁটিনাটি হারালে এখানেই ধরা পড়ে। Skip বাদ দেওয়া model প্রতিটি epoch-এ পিছিয়ে, শেষে Dice ০.৯৬৬৮ আর edge acc ০.৯০০৪ (U-Net-এ ০.৯১৪৬)। Dice-এর পার্থক্য ছোট, কিনারার পার্থক্য তার প্রায় তিন গুণ, যা stepper-এর যুক্তির সঙ্গে মেলে। পার্থক্য আরও বড় নয় কারণ এই ছোট network মাত্র দুবার অর্ধেক করে (৬৪ থেকে ১৬), তাই bottleneck-এ এখনো অনেক অবস্থান-তথ্য থাকে; মূল U-Net-এর মতো চারবার অর্ধেক করলে skip ছাড়া হারায় অনেক বেশি। পঞ্চম epoch-এ দুটিরই সামান্য পতন ছোট ডেটায় স্বাভাবিক ওঠানামা। Parameter-এর পার্থক্য (১,১৭,৩৯৩ বনাম ১,০৫,৮৭৩) শুধু decoder-এর বাড়তি input channel থেকে। এটি একটি seed-এর এক run, আর Dice মাপা হয়েছে dataset-স্তরে, সব pixel একসঙ্গে যোগ করে।

৯ · বাস্তবে চালানো — গতি, ভুলের ধরন আর ডেটা

গতি আর আকার

Detector-এর সময় শুধু network নয়: ছবির মাপ বদল, NMS আর box আঁকাও যোগ হয়। ছবির মাপ ছোট করলে গতি বাড়ে কিন্তু ছোট বস্তু হারায়। ONNX-এ export, quantization আর ছোট backbone হলো প্রচলিত উপায় (মেশিন লার্নিং কোর্সের পর্ব ৬৮)। সময় মাপুন নিজের যন্ত্রে, warm-up-এর পরে, কয়েকবার।

লেবেলের খরচ

একটি ছবির classification লেবেল কয়েক সেকেন্ডের কাজ; box আঁকা তার চেয়ে বেশি, আর pixel ধরে mask সবচেয়ে বেশি। লেবেলকারীদের মধ্যেও box-এর কিনারা নিয়ে মতভেদ হয়, তাই IoU ০.৯৫-এর মতো কড়া threshold-এ ground truth নিজেই অনিশ্চিত।

ভুলের ধরন

ছোট বস্তু, একটি আরেকটির আড়ালে থাকা বস্তু, ভিড় (ঘন তাকে পাশাপাশি বোতল, যেখানে NMS আসল প্রতিবেশীকেও মুছে ফেলতে পারে), আর training-এ না দেখা পরিবেশ: অন্য আলো, অন্য ক্যামেরা, অন্য দেশের প্যাকেট (মেশিন লার্নিং কোর্সের পর্ব ৫৯)।

Augmentation-এ একটি ফাঁদ: ছবি উল্টালে, কাটলে বা ঘোরালে box আর mask-কেও একইভাবে বদলাতে হয়। torchvision-এর transforms.v2 tv_tensors.BoundingBoxes আর tv_tensors.Mask চিনে ছবির সঙ্গে একসঙ্গে বদলায় (পর্ব ২৪, Data augmentation). গোপনীয়তার দিকও আছে: মানুষ বা গাড়ির নম্বরপ্লেটের ছবি নিয়ে detection মানে নজরদারির ক্ষমতা; কাদের ছবি, কার অনুমতিতে, কী কাজে, তা নকশার শুরুতেই ঠিক করা দরকার।

আরও গভীরে: Mask R-CNN (He, Gkioxari, Dollár আর Girshick, ২০১৭) Faster R-CNN-এর প্রতিটি box-এ একটি ছোট mask-শাখা যোগ করে instance segmentation করে, আর RoIAlign দিয়ে feature কাটার গোলকরণের ভুল দূর করে। Soft-NMS (Bodla ও সহকর্মীরা, ২০১৭) মেলা box মুছে না দিয়ে তার score কমায়, যা ভিড়ের ছবিতে সাহায্য করতে পারে। Box regression-এ সরাসরি IoU-ভিত্তিক loss (যেমন GIoU, Rezatofighi ও সহকর্মীরা, ২০১৯) ব্যবহারও প্রচলিত। ২০২৩ সালের Segment Anything (Kirillov ও সহকর্মীরা) একটি বিন্দু বা box দেখিয়ে দিলে mask দেওয়ার মতো “promptable” segmentation-এর ধারণা জনপ্রিয় করে; এর ক্ষমতা আর সীমা নিয়ে মূল্যায়ন এখনও চলছে।

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

  • A = [0, 0, 4, 4], B = [2, 2, 6, 6]-এর IoU? ছেদ ২ × ২ = ৪, সংযোগ ১৬ + ১৬ − ৪ = ২৮, IoU = ১/৭ ≈ ০.১৪৩.
  • NMS কী সরায়, কী সরায় না? একই বস্তুর কম score-এর ডুপ্লিকেট সরায়; একা দাঁড়ানো ভুল box সরায় না, সেটি score threshold-এর কাজ।
  • mAP@0.5 আর COCO AP-এর পার্থক্য? প্রথমটি একটি IoU threshold ০.৫-এ; দ্বিতীয়টি ০.৫০ থেকে ০.৯৫ পর্যন্ত দশটি threshold-এর গড়, তাই অবস্থানের নিখুঁততাও মাপে।
  • Dice ০.৮ হলে IoU কত? IoU = Dice ÷ (2 − Dice) = ০.৮ ÷ ১.২ ≈ ০.৬৬৭.
  • ConvTranspose2d(k = 2, s = 2), input 16 × 16-এর output? (১৬ − ১) × ২ + ২ = ৩২, অর্থাৎ ৩২ × ৩২.

১০ · AI-এর যুগে — detection আর segmentation কোডে AI-এর ভুল ধরা

AI assistant দিয়ে detection-এর কোড লেখানো সহজ, কারণ torchvision আর জনপ্রিয় লাইব্রেরির উদাহরণ অনলাইনে প্রচুর। কিন্তু এখানে ভুল প্রায়ই নীরব: কোড চলে, ছবিতে box-ও আঁকে, শুধু ভুল জায়গায় বা ভুল সংখ্যায়।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
xywh আর xyxy গুলিয়ে ফেলেbox সরে যায়, IoU আর mAP ভুলএকটি ছবিতে box এঁকে চোখে দেখুন; box_convert ব্যবহার
পুরোনো pretrained=True লেখেনতুন torchvision-এ weights enum লাগে; পুরোনো প্যারামিটার সরানো বা সতর্কবার্তাসহনিজের version-এর docs-এ builder-এর signature দেখুন
.eval() বা inference_mode() ভুলে যায়BatchNorm-এর আচরণ বদলায়; detection model training mode-এ target চায়Inference কোডে দুটি লাইন খুঁজুন
NMS-এর তুলনা < আর <= নিয়ে অসাবধান, বা শ্রেণি না মেনে NMSসীমার box-এ ফল আলাদা; বোতল কাপের box মুছে দেয়torchvision.ops.nms/batched_nms-এর সঙ্গে মিলিয়ে দেখুন
নিজের লেখা mAP দিয়ে “৯০%” দাবিডুপ্লিকেট TP গোনা, ভুল interpolation বা অন্য IoU thresholdএকটি প্রতিষ্ঠিত evaluator (যেমন pycocotools বা torchmetrics) দিয়ে একই ফল আসে কি না
Augmentation-এ ছবি উল্টায়, box উল্টায় নালেবেল আর ছবি আলাদা হয়ে যায়Augment-এর পরে কয়েকটি নমুনা এঁকে দেখুন

AI-কে কোডের সঙ্গে যাচাইয়ের উপায়ও চাইতে বলুন:

আমি torchvision দিয়ে একটি pretrained object detector চালাতে চাই, আমার torch আর torchvision-এর version নিচে দিলাম। ১. আমার version-এ যে weights enum আর builder আছে, শুধু সেগুলো দিয়ে কোড লেখো; নিশ্চিত না হলে বলো। ২. Box কোন ফরম্যাটে (xyxy নাকি xywh) আর কোন স্থানাঙ্কে আসে, স্পষ্ট লেখো। ৩. Model.eval() আর torch.inference_mode() কোথায় বসছে দেখাও। ৪. একটি ছবিতে box এঁকে চোখে যাচাই করার ছোট কোড দাও। ৫. নিজে NMS বা IoU লিখলে torchvision.ops.nms আর box_iou-এর সঙ্গে মেলানোর পরীক্ষা দাও। ৬. mAP হিসাবের জন্য নিজে না লিখে কোন প্রতিষ্ঠিত টুল ব্যবহার করা যায় আর তার IoU threshold কী, তা বলো।

AI-এর দেওয়া detection বা segmentation কোড — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Sliding window থেকে R-CNN, তারপর Faster R-CNN — detection কীভাবে “অনেক জায়গায় classifier চালানো” থেকে একটি network-এ এল?

Deep learning-এর আগে detection-এর প্রধান পদ্ধতি ছিল হাতে বানানো feature আর sliding window. Viola আর Jones-এর ২০০১ সালের মুখ খোঁজার পদ্ধতি সরল feature আর ধাপে ধাপে ছাঁকনি দিয়ে লক্ষ লক্ষ জানালা দ্রুত বাদ দিত। Dalal আর Triggs (২০০৫) HOG feature দিয়ে পথচারী খুঁজলেন, আর Felzenszwalb ও সহকর্মীদের deformable part model বস্তুকে কয়েকটি অংশের সমষ্টি ধরে PASCAL VOC-এ বহু বছর এগিয়ে ছিল। এসব পদ্ধতিতেও NMS ছিল, তাই NMS deep learning-এর আবিষ্কার নয়।

২০১২ সালে AlexNet ImageNet classification-এ বড় লাফ দেওয়ার পর প্রশ্ন উঠল, CNN-এর feature detection-এ কাজে লাগানো যায় কি না। Ross Girshick ও সহকর্মীদের R-CNN (CVPR ২০১৪) এর সরল উত্তর দিল: একটি পুরোনো পদ্ধতি (selective search) দিয়ে প্রতি ছবিতে প্রায় দুই হাজার প্রস্তাবিত এলাকা, প্রতিটি কেটে মাপ বদলে CNN-এ, তারপর SVM দিয়ে শ্রেণি। ফল অনেক ভালো, কিন্তু প্রতিটি এলাকায় আলাদা CNN চালানোয় এক ছবিতে অনেক সময় লাগত।

Fast R-CNN (Girshick, ২০১৫) পুরো ছবিতে একবার CNN চালিয়ে feature map থেকে প্রতিটি এলাকার feature কাটল (RoI pooling), আর শ্রেণি ও box একসঙ্গে শেখাল। বাকি থাকল প্রস্তাবের ধাপ, যা তখনও CNN-এর বাইরে আর ধীর। Faster R-CNN (Ren, He, Girshick আর Sun, ২০১৫) সেটিও network-এর ভেতরে আনল: একই feature map-এর ওপর Region Proposal Network, anchor দিয়ে। এভাবে পুরো pipeline প্রথমবার মোটামুটি end-to-end শেখা গেল।

এই পর্বের desktop run-এ যে fasterrcnn_mobilenet_v3_large_fpn চলেছে, তা এই বংশেরই, শুধু backbone হালকা আর FPN যোগ করা। প্রতিটি ধাপে মূল চালাকি একটিই: একই হিসাব বারবার না করে একবার করা আর ভাগ করে নেওয়া।

মূল উপলব্ধি: Detection-এর ইতিহাস হলো আলাদা আলাদা হাতে বানানো ধাপকে একটি ভাগ করা feature map আর শেখা অংশে বদলে ফেলার ইতিহাস; গতি এসেছে পুনরাবৃত্ত হিসাব বাদ দিয়ে।

প্র ০২YOLO-র “একবার দেখো” ধারণা কেন এত প্রভাবশালী, আর “YOLO সংস্করণ”-এর নম্বর নিয়ে কেন সাবধান থাকা দরকার?

Joseph Redmon, Santosh Divvala, Ross Girshick আর Ali Farhadi-র “You Only Look Once” প্রবন্ধ (arXiv-এ ২০১৫, CVPR ২০১৬) detection-কে একটি একক regression সমস্যা হিসেবে দেখল: ছবিকে একটি grid-এ ভাগ করে প্রতিটি ঘর থেকে একবারেই কয়েকটি box, তাদের confidence আর শ্রেণির সম্ভাবনা। আলাদা প্রস্তাবের ধাপ নেই, তাই গতি অনেক বেশি; প্রবন্ধটি real-time গতির দাবি করে। দাম ছিল নির্ভুলতায়, বিশেষ করে ছোট আর গাদাগাদি বস্তুতে, কারণ প্রতিটি ঘর অল্প কয়েকটি box দিতে পারত। প্রায় একই সময়ে SSD (Liu ও সহকর্মীরা, ECCV ২০১৬) কয়েকটি মাপের feature map থেকে anchor-ভিত্তিক predict করে one-stage-এর নির্ভুলতা বাড়াল।

One-stage detector-এর একটি বড় সমস্যা ছিল পটভূমির বিপুল anchor: হাজার হাজার সহজ “কিছু নেই” উদাহরণ loss-এ ডুবিয়ে দিত কঠিন উদাহরণকে। Lin ও সহকর্মীদের RetinaNet (২০১৭) focal loss দিয়ে সহজ উদাহরণের ওজন কমাল আর দেখাল, one-stage detector-ও two-stage-এর কাছাকাছি নির্ভুল হতে পারে। এই যুক্তি পর্ব ১৩-এর focal loss-এর আলোচনার সঙ্গে মেলে।

নামের প্রশ্নে সাবধানতা দরকার। Redmon নিজে YOLOv2 আর YOLOv3 পর্যন্ত কাজ করেন, পরে জানান যে সামরিক ব্যবহার আর গোপনীয়তার উদ্বেগ নিয়ে তিনি computer vision গবেষণা ছেড়েছেন বলে প্রতিবেদনে আসে। এর পরের “YOLO”-নামের সংস্করণগুলো বিভিন্ন দল আর কোম্পানির; কিছু প্রবন্ধে প্রকাশিত, কিছু শুধু কোড আর blog-এ। তাই একটি বড় নম্বর মানেই আগেরটির চেয়ে ভালো, বা একই দলের কাজ, এমন নয়। লাইসেন্সও আলাদা হতে পারে, যা বাণিজ্যিক ব্যবহারে গুরুত্বপূর্ণ।

ব্যবহারিক সিদ্ধান্তে তাই নাম নয়, মাপ দেখতে হয়: নিজের ডেটায় mAP, নিজের যন্ত্রে latency, আর লাইসেন্স। এই পর্বের run-এ দুটি model একই ছবিতে আলাদা ফল দিয়েছে; এক ছবির ফল দিয়ে কোনটি ভালো বলা যায় না।

মূল উপলব্ধি: One-stage নকশা গতির জন্য একটি ভিন্ন দৃষ্টিভঙ্গি এনেছে; কিন্তু model বাছাইয়ে সংস্করণের নম্বর বা জনপ্রিয়তা নয়, নিজের ডেটা, যন্ত্র আর লাইসেন্সে মাপা ফলই বিচারক।

প্র ০৩PASCAL VOC থেকে COCO — benchmark-এর নিয়ম বদলালে “ভালো detector”-এর সংজ্ঞা কীভাবে বদলায়?

PASCAL Visual Object Classes challenge (২০০৫ থেকে ২০১২) ২০টি শ্রেণির ছবি আর একটি নিয়ম দিয়েছিল: IoU ≥ ০.৫ হলে TP, আর প্রতিটি শ্রেণির AP. ২০০৭ সালের নিয়মে AP হতো ১১টি recall বিন্দুতে (০, ০.১, …, ১) interpolated precision-এর গড়; ২০১০ থেকে সব বিন্দু ধরে ক্ষেত্রফল, যা এই পর্বের ব্লকের all-point হিসাব। একই detector-এর দুটি নিয়মে সংখ্যা আলাদা হয়, তাই পুরোনো প্রবন্ধ পড়তে নিয়মটা জানা দরকার।

Microsoft COCO (Lin ও সহকর্মীরা, ২০১৪) ৮০টি শ্রেণি, প্রতি ছবিতে অনেক বেশি বস্তু আর প্রচুর ছোট বস্তু আনল। এর মূল নম্বর IoU ০.৫০ থেকে ০.৯৫ পর্যন্ত দশটি threshold-এর গড়। এতে box-এর ঢিলেঢালা অবস্থান আর পুরস্কার পায় না: ০.৫-এ যা TP, ০.৯-এ তা FP. COCO ১০১টি recall বিন্দু ব্যবহার করে, আর ছোট, মাঝারি, বড় বস্তুর আলাদা AP দেয়, যাতে বোঝা যায় দুর্বলতা কোথায়।

নিয়ম বদলালে গবেষণার দিকও বদলায়। COCO-র কড়া IoU-র পরে box-কে আরও নিখুঁত করার কৌশল (যেমন কয়েক ধাপে box শোধরানো, IoU-ভিত্তিক loss) গুরুত্ব পেল। আবার benchmark-এর সমালোচনাও আছে: COCO-র লেবেলেও ভুল আর অসংগতি পাওয়া গেছে, আর একটি benchmark-এ বছরের পর বছর ছোট উন্নতি বাস্তবের অন্য ডেটায় একই উন্নতি আনে, এমন নিশ্চয়তা নেই। এই পর্বের detector-এর weights-এর meta-তে লেখা box mAP-ও COCO-র এই নিয়মে, COCO-র validation ছবিতে মাপা; বাজারঘরের গুদামের ছবিতে সংখ্যা আলাদা হবে।

বাস্তব প্রকল্পে তাই প্রশ্ন হওয়া উচিত: আমার কাজের জন্য কোন ভুল বেশি ক্ষতিকর? তাকে বোতল গোনায় box-এর নিখুঁত কিনারা কম জরুরি, IoU ০.৫ যথেষ্ট হতে পারে; রোবটের হাতে বোতল তোলায় অবস্থানের নিখুঁততা জরুরি। নিজের মাপ বাছাই করা মূল্যায়নের নকশার অংশ।

মূল উপলব্ধি: mAP একটি সংখ্যা নয়, একটি নিয়মের পরিবার; কোন IoU, কোন interpolation, কোন ডেটা, তা না বললে সংখ্যার কোনো অর্থ নেই।

প্র ০৪চিকিৎসার ছবির জন্য বানানো U-Net কীভাবে উপগ্রহের ছবি থেকে শুরু করে ছবি-বানানো model পর্যন্ত ছড়িয়ে গেল?

Ronneberger, Fischer আর Brox-এর U-Net প্রবন্ধ (MICCAI ২০১৫) একটি নির্দিষ্ট সমস্যা থেকে এসেছিল: microscopy ছবিতে কোষ আলাদা করা, যেখানে লেবেল করা ছবি খুবই কম, কারণ প্রতিটি mask বিশেষজ্ঞের হাতে আঁকা। তাঁরা তিনটি জিনিস মেলালেন: encoder–decoder-এর সঙ্গে প্রতিটি মাপে skip connection, প্রচুর augmentation (বিশেষ করে elastic deformation, যাতে কোষের আকার একটু বাঁকানো যায়), আর গায়ে গায়ে লাগা কোষের মাঝের সরু ফাঁকে বেশি ওজনের loss. প্রবন্ধে কয়েক ডজন লেবেল-করা ছবি দিয়েই সেই সময়ের প্রতিযোগিতায় ভালো ফলের কথা বলা হয়েছে।

নকশাটি সরল আর সাধারণ, তাই দ্রুত অন্য ক্ষেত্রে গেল: CT আর MRI-তে অঙ্গ বা টিউমার আলাদা করা (3D রূপে, যেমন V-Net), উপগ্রহের ছবিতে রাস্তা, বাড়ি বা জলাভূমি চিহ্নিত করা, কৃষিতে ফসলের এলাকা মাপা। বাংলাদেশে বন্যার এলাকা বা নদীভাঙন মানচিত্রে উপগ্রহ-ছবির segmentation নিয়ে গবেষণার কথা শোনা যায়; তবে কোনো নির্দিষ্ট প্রকল্পের নির্ভুলতা যাচাই না করে উদ্ধৃত করা ঠিক নয়।

সবচেয়ে অপ্রত্যাশিত বিস্তার ছবি-বানানো model-এ। Diffusion model প্রতিটি ধাপে একটি noisy ছবি নিয়ে সমান মাপের একটি “noise-এর অনুমান” দেয়: input আর output-এর মাপ সমান, ঠিক segmentation-এর মতো। তাই অনেক diffusion model-এর মূল network একটি U-Net, attention স্তরসহ (Ho, Jain আর Abbeel, ২০২০, আর পরের অনেক কাজ)। পরে কিছু model U-Net-এর বদলে Transformer-ভিত্তিক কাঠামো ব্যবহার করেছে। Diffusion এই কোর্সের Generative AI মডিউলে আসবে।

এই পর্বের ছোট run-এ skip বাদ দিলে কিনারা আর সরু দণ্ডে ফল খারাপ হয়েছে; diffusion-এ একই কারণে skip সূক্ষ্ম texture ফেরাতে সাহায্য করে বলে ধরা হয়।

মূল উপলব্ধি: “input-এর সমান মাপের output, যেখানে বড় প্রসঙ্গ আর সূক্ষ্ম খুঁটিনাটি দুটিই লাগে” — এই চাহিদা যেখানেই আছে, U-Net-এর নকশা সেখানেই কাজে লাগে।

প্র ০৫Pixel accuracy, IoU আর Dice — কোন মাপ কখন বিভ্রান্ত করে, আর চিকিৎসা বা নিরাপত্তার কাজে কী দেখা উচিত?

Pixel accuracy-র সমস্যা এই পর্বের ব্লকেই দেখেছেন: ছোট বস্তু কিছুই না খুঁজেও ০.৯৯৬১. এটি class imbalance-এর সেই পরিচিত ফাঁদ, যা classification-এ “সবাইকে না বলা” model-এর উঁচু accuracy-তে দেখা যায়। Segmentation-এ পটভূমি প্রায় সবসময় বিশাল, তাই pixel accuracy প্রায় কিছুই বলে না। IoU আর Dice পটভূমির ঠিক pixel গোনে না (TN নেই), তাই বস্তুর ওপর মনোযোগ দেয়।

Dice আর IoU একই ক্রম দেয়, কিন্তু সংখ্যা আলাদা: IoU ০.৫ মানে Dice ০.৬৬৭, তাই একটি প্রবন্ধের Dice আরেকটির IoU-এর সঙ্গে তুলনা করা ভুল। আরেকটি সূক্ষ্মতা গড়ের পদ্ধতি: সব ছবির pixel একসঙ্গে যোগ করে একটি Dice (dataset-স্তরে), নাকি প্রতিটি ছবির Dice আলাদা করে গড়। প্রথমটিতে বড় বস্তুর ছবি প্রাধান্য পায়; দ্বিতীয়টিতে খালি mask-এর ছবিতে কী করবেন, সেই সিদ্ধান্ত লাগে। এই পর্বের desktop run dataset-স্তরের Dice দেখিয়েছে।

কিন্তু Dice-ও যথেষ্ট নয়, যেখানে কিনারা জরুরি। দুটি mask-এর Dice ০.৯ হতে পারে অথচ এক জায়গায় কিনারা কয়েক মিলিমিটার সরে আছে, যা অস্ত্রোপচার বা রেডিওথেরাপির পরিকল্পনায় গুরুত্বপূর্ণ। এজন্য চিকিৎসা-ছবিতে কিনারার দূরত্বের মাপ (যেমন Hausdorff distance) পাশাপাশি দেখা হয়। Maier-Hein ও সহকর্মীরা ২০১৮ সালে চিকিৎসা-ছবির প্রতিযোগিতাগুলোর বিশ্লেষণে দেখান, মাপ বা র‍্যাঙ্কিংয়ের নিয়ম বদলালেই বিজয়ী বদলে যেতে পারে, আর পরে কাজের ধরন বুঝে মাপ বাছাইয়ের নির্দেশিকা তৈরির উদ্যোগ নেওয়া হয়।

নিরাপত্তার কাজে প্রশ্ন আরও সরাসরি: কোনটি বেশি বিপজ্জনক, একটি বস্তু না দেখা (FN) নাকি ভুল বস্তু দেখা (FP)? ডাক্তারের রোগনির্ণয়ে ছোট টিউমার না দেখা বড় ভুল; সেখানে প্রতিটি বস্তু ধরা পড়ল কি না (object-level recall) আলাদা করে মাপা উচিত, শুধু গড় Dice নয়।

মূল উপলব্ধি: কোনো একক মাপ segmentation-এর মান পুরোপুরি বলে না; কাজের ঝুঁকি থেকে মাপ বাছাই করুন, আর গড়ের পাশাপাশি সবচেয়ে খারাপ ছবিগুলো চোখে দেখুন।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। পাতার ব্লক বদলে নিজের সংখ্যাও যাচাই করতে পারেন।

  1. পূর্ণ numerical: (ক) A = [0, 0, 10, 10] আর B = [5, 5, 15, 15] (xyxy)-এর IoU কত? (খ) চারটি box আর score: P = [0, 0, 10, 10] ০.৯, Q = [1, 1, 11, 11] ০.৮, R = [20, 20, 30, 30] ০.৭, S = [5, 5, 15, 15] ০.৬; IoU threshold ০.৫-এ NMS-এর পরে কোনগুলো থাকবে? (গ) একটি শ্রেণির ৩টি ground truth আর score অনুযায়ী সাজানো ৫টি detection: TP, FP, TP, TP, FP. Precision আর recall-এর তালিকা আর all-point AP বের করুন।

    (ক) ছেদ [5, 5, 10, 10], ক্ষেত্রফল ২৫; সংযোগ ১০০ + ১০০ − ২৫ = ১৭৫; IoU = ২৫ ÷ ১৭৫ = ১/৭ ≈ ০.১৪৩. (খ) P রাখা; P–Q: ছেদ ৯ × ৯ = ৮১, সংযোগ ২০০ − ৮১ = ১১৯, IoU ≈ ০.৬৮১ > ০.৫, Q বাদ; P–S: ০.১৪৩, S থাকে; P–R: ০. তারপর R রাখা (S-এর সঙ্গে ছেদ নেই), তারপর S রাখা। ফল P, R, S. (গ) জমা TP ১, ১, ২, ৩, ৩; precision ১, ০.৫, ০.৬৬৭, ০.৭৫, ০.৬; recall ০.৩৩৩, ০.৩৩৩, ০.৬৬৭, ১.০, ১.০. Interpolated precision: recall ০.৩৩৩ পর্যন্ত ১, ০.৩৩৩ থেকে ০.৬৬৭-এ ০.৭৫ (ডানের সর্বোচ্চ), ০.৬৬৭ থেকে ১.০-এ ০.৭৫. AP = (১/৩)(১) + (১/৩)(০.৭৫) + (১/৩)(০.৭৫) = ০.৮৩৩.

  2. সমালোচনা ও নকশা: একজন সহকর্মী বাজারঘরের গুদামের তাকে বোতল গোনার জন্য একটি detector বানিয়েছেন। রিপোর্টে লেখা “pixel accuracy ৯৮%, mAP ৯১%”. NMS threshold ০.৩, আর ঘন তাকে পাশাপাশি বোতলের গোনা প্রায়ই কম আসছে। কী কী প্রশ্ন করবেন আর কী বদলের প্রস্তাব দেবেন?

    প্রথমত, detection-এ “pixel accuracy” অর্থহীন; মাপটি কোথা থেকে এল জানতে চাইতে হবে। mAP ৯১%: কোন IoU threshold (০.৫ নাকি ০.৫০–০.৯৫), কোন ডেটায় (training-এর ছবি কি test-এ ঢুকেছে?), কোন evaluator দিয়ে? দ্বিতীয়ত, গোনা কম আসার সম্ভাব্য কারণ NMS threshold ০.৩: পাশাপাশি দুটি বোতলের box-এর IoU ০.৩-এর বেশি হলেই একটি মুছে যায়। প্রস্তাব: threshold বাড়িয়ে (যেমন ০.৫–০.৬) validation ডেটায় গোনার ভুল মাপা, Soft-NMS চেষ্টা করা, ঘন তাকের ছবি training-এ বেশি রাখা, আর কাজের আসল মাপ হিসেবে প্রতি তাকে গোনার ভুল (গড় পরম ভুল) রিপোর্ট করা। কয়েকটি ঘন তাকের ছবিতে box এঁকে চোখে দেখা সবার আগে।

  3. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
    import torch
    from torch import nn
    from torchvision.ops import box_iou
    up = nn.ConvTranspose2d(8, 4, kernel_size=2, stride=2)
    print(tuple(up(torch.zeros(1, 8, 16, 16)).shape))
    a = torch.tensor([[0., 0., 4., 4.]])
    b = torch.tensor([[2., 0., 6., 4.], [4., 4., 8., 8.]])
    print(box_iou(a, b))
    (1, 4, 32, 32)
    tensor([[0.3333, 0.0000]])

    Transposed convolution: (১৬ − ১) × ২ + ২ = ৩২, আর out channel ৪, তাই (1, 4, 32, 32). প্রথম জোড়ায় ছেদ ২ × ৪ = ৮, সংযোগ ১৬ + ১৬ − ৮ = ২৪, IoU = ১/৩. দ্বিতীয় box শুধু একটি কোণে ছোঁয় (৪, ৪): ছেদের প্রস্থ আর উচ্চতা ০, তাই IoU ০. Output একটি 1 × 2 matrix, কারণ box_iou সব জোড়ার IoU দেয়।

  4. কোড লিখুন: NumPy-তে একটি function dice_per_class(pred, gt, num_classes) লিখুন, যা দুটি H × W integer label map (প্রতিটি pixel-এ শ্রেণির নম্বর) নিয়ে প্রতিটি শ্রেণির Dice-এর একটি list ফেরত দেয়; যে শ্রেণি দুটি map-এর কোনোটিতেই নেই, তার জন্য nan.
    import numpy as np
    
    def dice_per_class(pred, gt, num_classes):
        out = []
        for c in range(num_classes):
            p, g = (pred == c), (gt == c)          # এই শ্রেণির binary mask
            denom = p.sum() + g.sum()
            if denom == 0:
                out.append(float("nan"))           # কোথাও নেই: গড়ে বাদ দেওয়ার জন্য
            else:
                out.append(float(2 * np.logical_and(p, g).sum() / denom))
        return out
    
    pred = np.array([[0, 1, 1], [0, 1, 2]])
    gt   = np.array([[0, 1, 1], [0, 0, 2]])
    print(dice_per_class(pred, gt, 4))              # [0.8, 0.8, 1.0, nan]
    print(np.nanmean(dice_per_class(pred, gt, 4)))  # 0.8666666666666667

    শ্রেণি ০: pred-এ ২টি, gt-তে ৩টি pixel, মেলে ২টি; Dice = ৪ ÷ ৫ = ০.৮. শ্রেণি ১: pred-এ ৩টি, gt-তে ২টি, মেলে ২টি; আবার ৪ ÷ ৫ = ০.৮. শ্রেণি ২: দুটিতেই একটি করে, মেলে, Dice ১. শ্রেণি ৩ কোথাও নেই, তাই nan, আর গড়ে np.nanmean তাকে বাদ দেয়: (০.৮ + ০.৮ + ১) ÷ ৩ ≈ ০.৮৬৬৭. float(…) দেওয়া হয়েছে কারণ NumPy 2 list-এর ভেতরের সংখ্যা np.float64(0.8) রূপে ছাপে। খালি শ্রেণিকে ০ বা ১ ধরলে গড় কৃত্রিমভাবে নামে বা ওঠে; কোনটি করবেন তা রিপোর্টে লিখুন।

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

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

ক্রমএকটি শ্রেণির detection-এ greedy non-maximum suppression-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সব box score অনুযায়ী বড় থেকে ছোট সাজানো
  2. তালিকার সর্বোচ্চ score-এর box রাখা
  3. রাখা box-এর সঙ্গে বাকি প্রতিটির IoU হিসাব
  4. IoU threshold-এর বেশি এমন box তালিকা থেকে বাদ
  5. তালিকা খালি না হওয়া পর্যন্ত আবার সর্বোচ্চটি বাছাই

দুটি box A = [10, 20, 50, 80] আর B = [30, 40, 70, 90] (xyxy, pixel)-এর IoU কত?

  • ৮০০
  • প্রায় ০.৩৩৩
  • প্রায় ০.২২২
  • প্রায় ০.১৮২

Greedy NMS (IoU threshold ০.৫) একটি ছবিতে কোন box সরাতে পারে না?

  • একই বোতলের চারপাশে কম score-এর একটি দ্বিতীয় box, IoU ০.৮
  • দুটি বোতলের মাঝের ফাঁকা জায়গায় একটি একা ভুল box, অন্য কোনো box-এর সঙ্গে IoU ০.১২
  • একটি বাক্সের ওপর ০.৬৩ IoU-এর কম score-এর box
  • পাশের কাপের box, যা বোতলের box-এর সঙ্গে ০.৭ IoU, যখন NMS শ্রেণি না মেনে চলে

একটি শ্রেণির ২টি ground truth, আর score অনুযায়ী সাজানো ৩টি detection: TP, FP, TP. All-point interpolated AP কত?

  • ০.৬৬৭
  • প্রায় ০.৭২২
  • ১.০
  • প্রায় ০.৮৩৩

একটি segmentation model-এর IoU ০.৬. একই prediction-এর Dice কত?

  • ০.৭৫
  • ০.৬
  • ১.২
  • প্রায় ০.৪২৯

U-Net-এর skip connection-এর প্রধান কাজ কোনটি?

  • Encoder আর decoder-এর map যোগ করে gradient-এর গুণফল ১ রাখা
  • Parameter-এর সংখ্যা কমানো
  • Pooling-এ হারানো স্থানিক খুঁটিনাটি decoder-এর একই মাপের স্তরে পৌঁছে দেওয়া
  • ছোট map-কে দ্বিগুণ মাপে বড় করা

nn.ConvTranspose2d(64, 32, kernel_size=2, stride=2, padding=0)-এ 16 × 16 map দিলে output-এর উচ্চতা আর প্রস্থ কত?

  • ৮ × ৮
  • ৩২ × ৩২
  • ৩০ × ৩০
  • ১৬ × ১৬

৬৪ × ৬৪ ছবিতে ১৬ pixel-এর একটি বস্তু; model প্রতিটি pixel-কে পটভূমি বলল। কোন বক্তব্য ঠিক?

  • Pixel accuracy প্রায় ০.৯৯৬, তাই model ভালো
  • IoU প্রায় ০.৫
  • Dice অসংজ্ঞায়িত, কারণ হর ০
  • Dice আর IoU দুটিই ০, যদিও pixel accuracy প্রায় ০.৯৯৬

শব্দকোষ

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

মনে রাখুন

  1. কাজ: Classification “কী”, detection “কী আর কোথায়, প্রতিটি বস্তু”, semantic segmentation প্রতিটি pixel-এর শ্রেণি, instance segmentation প্রতিটি বস্তুর আলাদা mask.
  2. IoU: ছেদ ÷ (|A| + |B| − ছেদ); box-এর ফরম্যাট (xyxy, xywh, cxcywh) আগে নিশ্চিত করুন।
  3. NMS: score অনুযায়ী সাজিয়ে সেরাটি রাখা, IoU > threshold বাকিগুলো বাদ; শ্রেণি ধরে (batched_nms); ভুল একা box সরায় score threshold.
  4. mAP: TP মানে IoU ≥ threshold আর ground truth আগে মেলেনি; interpolated PR curve-এর ক্ষেত্রফল = AP; শ্রেণি (আর COCO-তে IoU ০.৫০–০.৯৫) জুড়ে গড় = mAP. নিয়মটা সবসময় লিখুন।
  5. U-Net আর Dice: Encoder “কী”, decoder “কোথায়”, skip হারানো খুঁটিনাটি ফেরায়; ConvTranspose-এর মাপ (in − 1)·s − 2p + k; Dice = 2·IoU ÷ (1 + IoU), pixel accuracy বিভ্রান্ত করে।

পরের পর্বে: পর্ব ২৬ (CNN কী দেখে — saliency, Grad-CAM আর adversarial example): একটি CNN কোন pixel দেখে সিদ্ধান্ত নিচ্ছে তা heatmap দিয়ে দেখা, আর খুব ছোট বদলে কীভাবে তাকে ভুল বলানো যায়, শুধু বোঝা আর প্রতিরক্ষার জন্য।

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

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