فاز 4 · درس 6

تشخیص اشیاء: پیاده‌سازی YOLO از صفر در پایتورچ

نوع
ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python
یک سطح شطرنجی دوبعدی با چند قاب فلزی رنگی با ابعاد متفاوت و یک قاب مستطیلی پرنور متحرک در حال اندازه‌گیری ابعاد
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. تشخیص اشیاء به‌مثابه‌ی پیش‌بینی متراکم
  5. چرا شبکه و انکرها؟
  6. رمزگشایی پیش‌بینی‌ها
  7. اشتراک روی اجتماع (IoU)
  8. حذف غیرحداکثر (NMS)
  9. تابع زیان
  10. معیارهای ارزیابی تشخیص اشیاء
  11. پیاده‌سازی از صفر
  12. مرحله‌ی 1: محاسبه‌ی IoU
  13. مرحله‌ی 2: حذف غیرحداکثر (NMS)
  14. مرحله‌ی 3: کدگذاری و رمزگشایی جعبه‌ها
  15. مرحله‌ی 4: سر مینیمال YOLO
  16. مرحله‌ی 5: تخصیص اهداف به انکرها
  17. مرحله‌ی 6: سه تابع زیان
  18. مرحله‌ی 7: پایپ‌لاین استنتاج
  19. استفاده از ابزارهای واقعی
  20. خروجی این درس
  21. تمرین‌ها
  22. اصطلاحات کلیدی
  23. برای مطالعه‌ی بیشتر

در بیشتر پروژه‌های بینایی ماشین، صرفاً تشخیص این‌که در تصویر چه چیزی وجود دارد کافی نیست؛ بلکه باید محل دقیق تک‌تک اشیاء و تعداد آن‌ها را با جعبه‌های محصورکننده مشخص کنیم. در نگاه اول، استفاده از کتابخانه‌های آماده مانند ultralytics بسیار ساده به نظر می‌رسد، اما وقتی مدل در داده‌های واقعی دچار خطاهایی مثل هم‌پوشانی جعبه‌ها، ناتوانی در تشخیص اشیاء ریز یا گزارش اشیاء کاذب می‌شود، کار با یک جعبه‌ی سیاه بدون فهم ریاضیات درونی آن متوقف خواهد شد.

در این درس، الگوریتم جریان‌ساز YOLO را از صفر و روی مفاهیم شبکه (grid)، انکرها (anchors)، محاسبه‌ی IoU و حذف غیرحداکثر (NMS) پیاده‌سازی می‌کنیم تا دقیقاً ببینید هر تنسور خروجی چه مفهومی دارد و پایپ‌لاین استنتاج چگونه کار می‌کند. برای پیاده‌سازی و تست کدهای این درس نیازی به سرورهای ابری گران‌قیمت یا پردازنده‌های گرافیکی قدرتمند خارجی ندارید؛ معماری روی ابعاد کوچک طراحی شده و به‌راحتی روی CPU سیستم شخصی شما قابل اجراست.

اگر برای تست مدل‌های آماده یا بسته‌های جانبی با دانلود از مخازنی مانند PyPI با افت سرعت یا ناپایداری ارتباط مواجه شدید، می‌توانید از میرورهای داخلی بسته‌های پایتون استفاده کنید.

تشخیص اشیاء (Detection) یعنی دسته‌بندی به‌علاوه‌ی رگرسیون؛ این فرایند روی تک‌تک نقاط نقشه‌ی ویژگی اجرا و در نهایت با حذف غیرحداکثر (NMS) پالایش می‌شود.

اهداف یادگیری

  • طراحی مبتنی بر شبکه و انکر (grid-and-anchor) را که تشخیص را به یک مسئله‌ی پیش‌بینی متراکم (dense prediction) تبدیل می‌کند توضیح دهید و مفهوم تک‌تک اعداد موجود در تنسور خروجی را بیان کنید.
  • اشتراک روی اجتماع (IoU) را بین دو جعبه محاسبه کنید و الگوریتم حذف غیرحداکثر (NMS: Non-Maximum Suppression) را از صفر پیاده‌سازی کنید.
  • یک سر (head) مینیمال به سبک YOLO روی یک ستون‌فقرات (backbone) از پیش آموزش‌دیده بسازید که شامل توابع زیان دسته‌بندی، شیءبودگی (objectness) و رگرسیون جعبه باشد.
  • یک ردیف از معیارهای ارزیابی تشخیص اشیاء (precision@0.5، recall، mAP@0.5، mAP@0.5:0.95) را تحلیل کنید و تصمیم بگیرید در مرحله‌ی بعد کدام پیچ تنظیم را بچرخانید.

صورت مسئله

دسته‌بندی می‌گوید: «این تصویر یک سگ است.» تشخیص اشیاء می‌گوید: «یک سگ در پیکسل‌های (112, 40, 280, 210) و یک گربه در (400, 180, 560, 310) قرار دارد و چیز دیگری در کادر نیست.» همین یک تغییر ساختاری — یعنی پیش‌بینی تعداد متغیری از جعبه‌های برچسب‌دار به‌جای یک برچسب برای کل تصویر — پایه‌ی تمام سیستم‌های خودران، سامانه‌های نظارتی، ابزارهای تحلیل چیدمان اسناد و خطوط بینایی ماشین در کارخانه‌هاست.

تشخیص اشیاء نقطه‌ای است که تمام بده‌بستان‌های مهندسی بینایی ماشین هم‌زمان ظاهر می‌شوند. جعبه‌هایی با موقعیت دقیق می‌خواهید (سر رگرسیون)، کلاس درست را برای هر جعبه لازم دارید (سر دسته‌بندی)، می‌خواهید مدل بفهمد چه زمانی چیزی برای تشخیص وجود ندارد (امتیاز شیءبودگی)، و برای هر شیء واقعی دقیقاً یک پیش‌بینی می‌خواهید (حذف غیرحداکثر). اگر هر کدام از این بخش‌ها بلنگد، پایپ‌لاین یا اشیاء را جا می‌اندازد، جعبه‌های توهمی گزارش می‌دهد، یا یک شیء واحد را 15 بار در موقعیت‌های کمی متفاوت پیش‌بینی می‌کند.

مدل YOLO (You Only Look Once، ردمون و همکاران، 2016) همان طرحی بود که با انجام کل این کارها در یک گذر رو به جلو (forward pass) درون یک شبکه‌ی کانولوشنی، اجرای بی‌درنگ (real-time) را ممکن کرد. این تصمیم‌های ساختاری هنوز ستون فقرات دیتکتورهای مدرن (مانند YOLOv8، YOLOv9، YOLO-NAS و RT-DETR) هستند. هسته‌ی اصلی را که یاد بگیرید، تمام نسخه‌ها فقط چیدمان متفاوتی از همین قطعات خواهند بود.

مفاهیم پایه

تشخیص اشیاء به‌مثابه‌ی پیش‌بینی متراکم

یک دسته‌بند به ازای هر تصویر C عدد خروجی می‌دهد. یک دیتکتور سبکِ YOLO به ازای هر تصویر (S x S x (5 + C)) عدد تولید می‌کند که در آن S اندازه‌ی شبکه‌ی مکانی (spatial grid) است.

ورودی 416x416 RGBستون فقرات (Backbone)(ResNet, DarkNet, ...)نقشه ویژگی(C_feat, 13, 13)سر تشخیص(کانولوشن‌های 1x1)تنسور خروجی(13, 13, B * (5 + C))رمزگشایی(شبکه + sigmoid + exp)حذف غیرحداکثر (NMS)باکس‌های نهایی

هر سلول از این شبکه‌ی S * S، تعداد B جعبه را پیش‌بینی می‌کند. برای هر جعبه:

  • 4 عدد هندسه‌ی جعبه را توصیف می‌کنند: tx, ty, tw, th.
  • 1 عدد امتیاز شیءبودگی است: «آیا مرکز یک شیء در این سلول قرار دارد؟»
  • C عدد احتمال‌های مربوط به هر کلاس هستند.

مجموع اعداد به ازای هر سلول: B * (5 + C). برای مجموعه‌داده‌ی VOC با مقادیر S=13, B=2, C=20، این مقدار برابر با 50 عدد در هر سلول است.

چرا شبکه و انکرها؟

در رگرسیون ساده، مختصات (x, y, w, h) هر شیء به‌صورت مقادیر مطلق پیش‌بینی می‌شود. یادگیری چنین چیزی برای یک شبکه‌ی کانولوشنی سخت است؛ زیرا جابه‌جایی تصویر نباید تمام پیش‌بینی‌ها را به یک اندازه جابه‌جا کند — هر شیء به مکان خاصی متصل است. شبکه (grid) این مشکل را حل می‌کند: هر جعبه‌ی واقعیت زمینی (ground-truth) را به سلولی واگذار می‌کند که مرکزش در آن افتاده است؛ فقط همان سلول مسئول آن شیء خواهد بود.

انکرها (Anchors) مشکل دوم را برطرف می‌کنند. یک لایه‌ی کانولوشن 3x3 نمی‌تواند از یک سلول ویژگی با میدان دید 16 پیکسلی، یک جعبه‌ی 500 پیکسلی را به‌راحتی رگرسیون کند. به‌جای آن، از پیش B شکل جعبه‌ی پیشین (prior box) یا انکر به ازای هر سلول تعریف می‌کنیم و مدل فقط تغییرات کوچک (delta) را نسبت به هر انکر پیش‌بینی می‌کند. مدل یاد می‌گیرد انکر مناسب را بردارد و کمی تغییرش دهد، نه این‌که جعبه را از صفر حدس بزند.

Anchor box priors (example for 416x416 input):

  small:   (30,  60)
  medium:  (75,  170)
  large:   (200, 380)

At each grid cell, every anchor emits (tx, ty, tw, th, obj, c_1, ..., c_C).

دیتکتورهای امروزی معمولاً از FPN استفاده می‌کنند که در هر سطحِ تفکیک‌پذیری، انکرهای متفاوتی دارد — انکرهای کوچک روی نقشه‌های کم‌عمق با وضوح بالا، و انکرهای بزرگ روی نقشه‌های عمیق با وضوح پایین. ایده همان است، فقط مقیاس‌ها بیشتر شده‌اند.

رمزگشایی پیش‌بینی‌ها

مقادیر خام tx, ty, tw, th مختصات نهایی جعبه نیستند؛ این‌ها اهداف رگرسیون هستند که قبل از رسم باید تبدیل شوند:

centre x=(σ(tx)+cellx)⋅stridecentre y=(σ(ty)+celly)⋅stridewidth=anchorw⋅exp⁡(tw)height=anchorh⋅exp⁡(th)\begin{aligned} \text{centre } x &= (\sigma(t_x) + \text{cell}_x) \cdot \text{stride} \\ \text{centre } y &= (\sigma(t_y) + \text{cell}_y) \cdot \text{stride} \\ \text{width} &= \text{anchor}_w \cdot \exp(t_w) \\ \text{height} &= \text{anchor}_h \cdot \exp(t_h) \end{aligned}

تابع sigmoid آفست‌های مرکز را داخل سلول نگه می‌دارد. تابع exp به عرض و ارتفاع اجازه می‌دهد آزادانه نسبت به انکر بزرگ و کوچک شوند بدون این‌که علامت‌شان منفی شود. مقدار stride هم مختصات شبکه را دوباره به مقیاس پیکسل برمی‌گرداند. این مرحله‌ی رمزگشایی (decode) از زمان YOLOv2 در همه‌ی نسخه‌ها ثابت مانده است.

اشتراک روی اجتماع (IoU)

معیار استاندارد و جهانی شباهت بین دو جعبه در تشخیص اشیاء:

IoU(A,B)=area(A∩B)area(A∪B)\mathrm{IoU}(A, B) = \frac{\mathop{\mathrm{area}}(A \cap B)}{\mathop{\mathrm{area}}(A \cup B)}

مقدار IoU=1\mathrm{IoU} = 1 یعنی دو جعبه دقیقاً بر هم منطبق‌اند؛ و IoU=0\mathrm{IoU} = 0 یعنی هیچ هم‌پوشانی ندارند. مقدار IoU بین پیش‌بینی و جعبه‌ی واقعی مشخص می‌کند که آیا پیش‌بینی مثبت واقعی (true positive) به حساب می‌آید یا نه (معمولاً با شرط IoU≥0.5\mathrm{IoU} \ge 0.5). همچنین IoU بین دو جعبه‌ی پیش‌بینی‌شده همان ابزاری است که NMS برای حذف موارد تکراری به کار می‌برد.

حذف غیرحداکثر (NMS)

شبکه‌ی کانولوشنی که روی انکرهای مجاور آموزش دیده، معمولاً برای یک شیء واحد چندین جعبه‌ی هم‌پوشان پیش‌بینی می‌کند. الگوریتم NMS پیش‌بینی با بالاترین امتیاز اطمینان را نگه می‌دارد و جعبه‌های دیگری را که IoU آن‌ها با این جعبه بیش از یک آستانه (threshold) مشخص باشد حذف می‌کند.

NMS(boxes, scores, iou_threshold):
    sort boxes by score descending
    keep = []
    while boxes not empty:
        pick the top-scoring box, add to keep
        remove every box with IoU > iou_threshold to the picked box
    return keep

آستانه‌ی معمول: 0.45 برای تشخیص اشیاء. دیتکتورهای جدیدتر گاهی به‌جای NMS استاندارد از soft-NMS یا DIoU-NMS استفاده می‌کنند، یا فرآیند حذف را به‌صورت مستقیم آموزش می‌دهند (مثل RT-DETR)؛ با این حال هدف ساختاری تغییری نکرده است.

تابع زیان

تابع زیان YOLO حاصل جمع وزنیِ سه تابع زیان (loss) مختلف است:

L=λcoord⋅Lbox(pred,target,where obj=1)+λobj⋅Lobj(pred,1,where obj=1)+λnoobj⋅Lobj(pred,0,where obj=0)+λcls⋅Lcls(pred,target,where obj=1)\begin{aligned} L = {} & \lambda_{\text{coord}} \cdot L_{\text{box}}(\text{pred}, \text{target}, \text{where obj}=1) \\ + {} & \lambda_{\text{obj}} \cdot L_{\text{obj}}(\text{pred}, 1, \text{where obj}=1) \\ + {} & \lambda_{\text{noobj}} \cdot L_{\text{obj}}(\text{pred}, 0, \text{where obj}=0) \\ + {} & \lambda_{\text{cls}} \cdot L_{\text{cls}}(\text{pred}, \text{target}, \text{where obj}=1) \end{aligned}

فقط سلول‌هایی که حاوی شیء هستند در توابع زیان رگرسیون جعبه و دسته‌بندی سهم دارند. سلول‌های خالی فقط روی زیان شیءبودگی اثر می‌گذارند (تا مدل یاد بگیرد وقتی چیزی نیست، ساکت بماند). ضریب lambda_noobj معمولاً کوچک انتخاب می‌شود (حدود 0.5)؛ چون بیشتر سلول‌ها خالی هستند و اگر این ضریب کوچک نباشد، زیان آن‌ها بر کل تابع زیان غالب می‌شود.

نسخه‌های جدیدتر، زیان خطای میانگین مربعات (MSE) جعبه را با CIoU یا DIoU (که مستقیماً IoU را بهینه می‌کنند) جایگزین کرده‌اند، برای مشکل عدم‌توازن کلاس‌ها (class imbalance) از focal loss بهره می‌برند، و شیءبودگی را با quality focal loss تنظیم می‌کنند. اما ساختار سه‌بخشی کماکان دست‌نخورده باقی مانده است.

معیارهای ارزیابی تشخیص اشیاء

معیار دقت (accuracy) در تشخیص اشیاء کاربردی ندارد. چهار معیاری که باید بررسی کنید:

  • Precision@IoU=0.5 — از میان جعبه‌هایی که مثبت تشخیص داده شده‌اند، چه کسری واقعاً درست هستند.
  • Recall@IoU=0.5 — از میان اشیاء واقعی تصویر، چه کسری را توانسته‌ایم پیدا کنیم.
  • AP@0.5 — مساحت زیر منحنی دقت-بازخوانی در آستانه‌ی IoU برابر 0.5؛ یک عدد برای هر کلاس.
  • mAP@0.5:0.95 — میانگین AP در آستانه‌های IoU از 0.5 تا 0.95 با گام‌های 0.05. این معیار COCO، سخت‌گیرانه‌ترین و آموزنده‌ترین سنجه است.

هر چهار معیار را گزارش کنید. مدلی که mAP@0.5 بالایی دارد اما mAP@0.5:0.95 آن ضعیف است، اشیاء را به‌طور تقریبی پیدا می‌کند ولی مرز جعبه‌هایش دقیق نیست؛ این مشکل را با بهبود تابع زیان رگرسیون جعبه برطرف کنید. مدلی با دقت (precision) بالا و بازخوانی (recall) پایین بیش از حد محافظه‌کار است؛ آستانه‌ی اطمینان را کم کنید یا وزن شیءبودگی را بالا ببرید.

تشخیص شیء · NMSجعبه‌ها پدیدار می‌شوند، هم‌پوشانی‌ها هرس می‌شوند
OBJECT DETECTION · NMSA YOLO head predicts a box at every grid cell, so one object spawns many overlapping candidates (dashed). Non-maximum suppression sorts them by confidence, keeps the strongest, and deletes every box that overlaps it too much. What survives is one tight box per object.صحنه · تمام جعبه‌های نامزدdog 0.941 · شبکه‌ی متراکم جعبه‌ها2 · امتیازدهی به هر جعبه3 · مرتب‌سازی بر اساس شیئیت4 · حذف هم‌پوشانی‌های IoU بالا5 · یک جعبه به ازای هر شیء
هد YOLO در هر سلول شبکه یک جعبه پیش‌بینی می‌کند، بنابراین یک شیء چندین جعبه‌ی نامزد هم‌پوشان (خط‌چین) ایجاد می‌کند. سرکوب غیربیشینه (NMS) آن‌ها را بر اساس اطمینان مرتب می‌کند، قوی‌ترین را نگه می‌دارد و هر جعبه‌ای که هم‌پوشانی زیادی با آن دارد را حذف می‌کند. در نهایت برای هر شیء یک جعبه‌ی دقیق باقی می‌ماند.

پیاده‌سازی از صفر

مرحله‌ی 1: محاسبه‌ی IoU

ابزار اصلی کل این درس. این تابع روی دو آرایه از جعبه‌ها در قالب (x1, y1, x2, y2) کار می‌کند.

import numpy as np

def box_iou(boxes_a, boxes_b):
    ax1, ay1, ax2, ay2 = boxes_a[:, 0], boxes_a[:, 1], boxes_a[:, 2], boxes_a[:, 3]
    bx1, by1, bx2, by2 = boxes_b[:, 0], boxes_b[:, 1], boxes_b[:, 2], boxes_b[:, 3]

    inter_x1 = np.maximum(ax1[:, None], bx1[None, :])
    inter_y1 = np.maximum(ay1[:, None], by1[None, :])
    inter_x2 = np.minimum(ax2[:, None], bx2[None, :])
    inter_y2 = np.minimum(ay2[:, None], by2[None, :])

    inter_w = np.clip(inter_x2 - inter_x1, 0, None)
    inter_h = np.clip(inter_y2 - inter_y1, 0, None)
    inter = inter_w * inter_h

    area_a = (ax2 - ax1) * (ay2 - ay1)
    area_b = (bx2 - bx1) * (by2 - by1)
    union = area_a[:, None] + area_b[None, :] - inter
    return inter / np.clip(union, 1e-8, None)

خروجی یک ماتریس با ابعاد (N_a, N_b) شامل IoUهای دوبه‌دو است. برای سنجش نسبت به یک جعبه‌ی واقعیِ تنها، کافی است بعد یکی از آرایه‌ها را (1, 4) بگذارید.

مرحله‌ی 2: حذف غیرحداکثر (NMS)

def nms(boxes, scores, iou_threshold=0.45):
    order = np.argsort(-scores)
    keep = []
    while len(order) > 0:
        i = order[0]
        keep.append(i)
        if len(order) == 1:
            break
        rest = order[1:]
        ious = box_iou(boxes[[i]], boxes[rest])[0]
        order = rest[ious <= iou_threshold]
    return np.array(keep, dtype=np.int64)

این تابع قطعی (deterministic) است، به دلیل مرتب‌سازی پیچیدگی زمانی O(N log N) دارد، و خروجی آن با ورودی‌های یکسان، دقیقاً مطابق torchvision.ops.nms است.

مرحله‌ی 3: کدگذاری و رمزگشایی جعبه‌ها

تبدیل مختصات پیکسلی به اهداف رگرسیون (tx, ty, tw, th) که شبکه واقعاً پیش‌بینی می‌کند و برعکس.

def encode(box_xyxy, cell_x, cell_y, stride, anchor_wh):
    x1, y1, x2, y2 = box_xyxy
    cx = 0.5 * (x1 + x2)
    cy = 0.5 * (y1 + y2)
    w = x2 - x1
    h = y2 - y1
    off_x = np.clip(cx / stride - cell_x, 1e-6, 1 - 1e-6)
    off_y = np.clip(cy / stride - cell_y, 1e-6, 1 - 1e-6)
    tx = np.log(off_x / (1 - off_x))
    ty = np.log(off_y / (1 - off_y))
    tw = np.log(w / anchor_wh[0] + 1e-8)
    th = np.log(h / anchor_wh[1] + 1e-8)
    return np.array([tx, ty, tw, th])


def decode(tx_ty_tw_th, cell_x, cell_y, stride, anchor_wh):
    tx, ty, tw, th = tx_ty_tw_th
    cx = (sigmoid(tx) + cell_x) * stride
    cy = (sigmoid(ty) + cell_y) * stride
    w = anchor_wh[0] * np.exp(tw)
    h = anchor_wh[1] * np.exp(th)
    return np.array([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2])


def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

آزمون درستی: جعبه‌ای را که مرکزش دقیقاً داخل سلول است encode کنید، سپس خروجی را decode کنید؛ در این حالت باید تا حد خطای اعشاری به همان جعبه‌ی اول برسید. تابع encode آفست مرکز را به شکل لاجیت (معکوس sigmoid در decode) ذخیره می‌کند. استفاده از تابع clip لاجیت‌ها را در بازه‌ی متناهی نگه می‌دارد؛ مرکزی که دقیقاً روی لبه‌ی سلول بیفتد حداکثر به اندازه‌ی 1e-6 سلول جابه‌جا می‌شود. تابع assign_targets در ادامه نیز همین لاجیت‌ها را ذخیره می‌کند تا سر مدل در همان فضایی آموزش ببیند که decode انتظارش را دارد.

مرحله‌ی 4: سر مینیمال YOLO

یک لایه‌ی کانولوشن 1x1 روی نقشه‌ی ویژگی که ابعاد خروجی را به (B, S, S, num_anchors, 5 + C) تغییر می‌دهد.

import torch
import torch.nn as nn

class YOLOHead(nn.Module):
    def __init__(self, in_c, num_anchors, num_classes):
        super().__init__()
        self.num_anchors = num_anchors
        self.num_classes = num_classes
        self.conv = nn.Conv2d(in_c, num_anchors * (5 + num_classes), kernel_size=1)

    def forward(self, x):
        n, _, h, w = x.shape
        y = self.conv(x)
        y = y.view(n, self.num_anchors, 5 + self.num_classes, h, w)
        y = y.permute(0, 3, 4, 1, 2).contiguous()
        return y

شکل خروجی: (N, H, W, num_anchors, 5 + C). آخرین بعد شامل [tx, ty, tw, th, obj, cls_0, ..., cls_{C-1}] است.

مرحله‌ی 5: تخصیص اهداف به انکرها

برای هر جعبه‌ی واقعی، تعیین کنید کدام (cell, anchor) مسئول پیش‌بینی آن است.

def assign_targets(boxes_xyxy, classes, anchors, stride, grid_size, num_classes):
    num_anchors = len(anchors)
    target = np.zeros((grid_size, grid_size, num_anchors, 5 + num_classes), dtype=np.float32)
    has_obj = np.zeros((grid_size, grid_size, num_anchors), dtype=bool)

    for box, cls in zip(boxes_xyxy, classes):
        x1, y1, x2, y2 = box
        cx, cy = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
        gx, gy = int(cx / stride), int(cy / stride)
        bw, bh = x2 - x1, y2 - y1

        ious = np.array([
            (min(bw, aw) * min(bh, ah)) / (bw * bh + aw * ah - min(bw, aw) * min(bh, ah))
            for aw, ah in anchors
        ])
        best = int(np.argmax(ious))
        aw, ah = anchors[best]

        off_x = np.clip(cx / stride - gx, 1e-6, 1 - 1e-6)
        off_y = np.clip(cy / stride - gy, 1e-6, 1 - 1e-6)
        target[gy, gx, best, 0] = np.log(off_x / (1 - off_x))
        target[gy, gx, best, 1] = np.log(off_y / (1 - off_y))
        target[gy, gx, best, 2] = np.log(bw / aw + 1e-8)
        target[gy, gx, best, 3] = np.log(bh / ah + 1e-8)
        target[gy, gx, best, 4] = 1.0
        target[gy, gx, best, 5 + cls] = 1.0
        has_obj[gy, gx, best] = True
    return target, has_obj

انتخاب انکر بر اساس «بهترین IoU شکلی با جعبه‌ی واقعی» انجام می‌شود — رویکردی سریع و سبک که دقیقاً مشابه سازوکار YOLOv2/v3 است. در YOLOv5 و نسخه‌های بعد از روش‌های پیچیده‌تری استفاده می‌شود (مانند task-aligned matching و dynamic k) که نسخه‌های بهینه‌شده‌ی همین ایده هستند.

مرحله‌ی 6: سه تابع زیان

def yolo_loss(pred, target, has_obj, lambda_coord=5.0, lambda_obj=1.0, lambda_noobj=0.5, lambda_cls=1.0):
    has_obj_t = torch.from_numpy(has_obj).bool()
    target_t = torch.from_numpy(target).float()

    # تابع زیان (loss) رگرسیون جعبه: فقط روی سلول‌های دارای شیء
    box_pred = pred[..., :4][has_obj_t]
    box_true = target_t[..., :4][has_obj_t]
    loss_box = torch.nn.functional.mse_loss(box_pred, box_true, reduction="sum")

    # تابع زیان (loss) شیئیت (objectness)
    obj_pred = pred[..., 4]
    obj_true = target_t[..., 4]
    loss_obj_pos = torch.nn.functional.binary_cross_entropy_with_logits(
        obj_pred[has_obj_t], obj_true[has_obj_t], reduction="sum")
    loss_obj_neg = torch.nn.functional.binary_cross_entropy_with_logits(
        obj_pred[~has_obj_t], obj_true[~has_obj_t], reduction="sum")

    # تابع زیان (loss) طبقه‌بندی روی سلول‌های دارای شیء
    cls_pred = pred[..., 5:][has_obj_t]
    cls_true = target_t[..., 5:][has_obj_t]
    loss_cls = torch.nn.functional.binary_cross_entropy_with_logits(
        cls_pred, cls_true, reduction="sum")

    total = (lambda_coord * loss_box
             + lambda_obj * loss_obj_pos
             + lambda_noobj * loss_obj_neg
             + lambda_cls * loss_cls)
    return total, {"box": loss_box.item(), "obj_pos": loss_obj_pos.item(),
                   "obj_neg": loss_obj_neg.item(), "cls": loss_cls.item()}

چهار وزن مختلف برای تابع زیان وجود دارد که در آموزش‌های YOLO یا مقدار ثابت می‌گیرند یا با جستجو تنظیم می‌شوند. نسبت این اعداد مهم است: مقادیر lambda_coord=5, lambda_noobj=0.5 دقیقاً از مقاله‌ی اصلی YOLOv1 برداشته شده‌اند و هنوز هم به عنوان مقادیر اولیه‌ی پیش‌فرض عملکرد مناسبی دارند.

مرحله‌ی 7: پایپ‌لاین استنتاج

رمزگشایی خروجی خام سر مدل، اعمال تابع‌های sigmoid و exp، فیلتر کردن بر اساس آستانه‌ی شیءبودگی، و اجرای NMS.

def postprocess(pred_tensor, anchors, stride, img_size, conf_threshold=0.25, iou_threshold=0.45):
    pred = pred_tensor.detach().cpu().numpy()
    grid_h, grid_w = pred.shape[1], pred.shape[2]
    num_anchors = len(anchors)

    boxes, scores, classes = [], [], []
    for gy in range(grid_h):
        for gx in range(grid_w):
            for a in range(num_anchors):
                tx, ty, tw, th, obj, *cls = pred[0, gy, gx, a]
                score = sigmoid(obj) * sigmoid(np.array(cls)).max()
                if score < conf_threshold:
                    continue
                cls_idx = int(np.argmax(cls))
                cx = (sigmoid(tx) + gx) * stride
                cy = (sigmoid(ty) + gy) * stride
                w = anchors[a][0] * np.exp(tw)
                h = anchors[a][1] * np.exp(th)
                boxes.append([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2])
                scores.append(float(score))
                classes.append(cls_idx)

    if not boxes:
        return np.zeros((0, 4)), np.zeros((0,)), np.zeros((0,), dtype=int)
    boxes = np.array(boxes)
    scores = np.array(scores)
    classes = np.array(classes)
    keep = nms(boxes, scores, iou_threshold)
    return boxes[keep], scores[keep], classes[keep]

این پایپ‌لاینِ کامل ارزیابی است: سر مدل -> رمزگشایی -> اعمال آستانه -> حذف غیرحداکثر (NMS).

استفاده از ابزارهای واقعی

بسته‌ی torchvision.models.detection دیتکتورهای آماده‌ی محیط عملیاتی را با همین ساختار مفهومی ارائه می‌دهد. بارگذاری یک مدل از پیش آموزش‌دیده تنها با سه خط کد انجام می‌شود.

import torch
from torchvision.models.detection import fasterrcnn_resnet50_fpn_v2

model = fasterrcnn_resnet50_fpn_v2(weights="DEFAULT")
model.eval()
with torch.no_grad():
    predictions = model([torch.randn(3, 400, 600)])
print(predictions[0].keys())
print(f"boxes:  {predictions[0]['boxes'].shape}")
print(f"scores: {predictions[0]['scores'].shape}")
print(f"labels: {predictions[0]['labels'].shape}")

برای پایپ‌لاین‌های استنتاج بی‌درنگ، کتابخانه‌ی ultralytics (شامل YOLOv8/v9) انتخاب استاندارد به حساب می‌آید: from ultralytics import YOLO; model = YOLO('yolov8n.pt'); model(img). این مدل‌ها مراحل رمزگشایی و NMS را درونی انجام می‌دهند و دقیقاً همان سه‌گانه‌ی boxes / scores / labels را که بالا ساختید بازمی‌گردانند.

خروجی این درس

از این درس خروجی‌های زیر را به دست می‌آورید:

  • outputs/prompt-detection-metric-reader.md — یک پرامپت که یک سطر از مقادیر precision, recall, AP, mAP@0.5:0.95 را دریافت می‌کند و تحلیلی در یک خط به همراه مفیدترین آزمایش پیشنهادی بعدی تحویل می‌دهد.
  • outputs/skill-anchor-designer.md — مهارتی که با دریافت مجموعه‌داده‌ای از جعبه‌های واقعی، الگوریتم k-means را روی (w, h) اجرا می‌کند، مجموعه‌ی انکرها را برای هر سطح FPN به همراه آمارهای پوشش ارائه می‌دهد تا تعداد بهینه‌ی انکرها را تعیین کنید.

تمرین‌ها

  1. (ساده) تابع box_iou را بنویسید و روی 1000 جفت‌جعبه‌ی تصادفی با torchvision.ops.box_iou مقایسه کنید. مطمئن شوید حداکثر اختلاف مطلق کمتر از 1e-6 است.
  2. (متوسط) کد yolo_loss را به‌گونه‌ای تغییر دهید که به‌جای MSE برای جعبه‌ها از تابع زیان CIoU استفاده کند. روی یک مجموعه‌داده‌ی ساختگی 100 تصویری نشان دهید که در تعداد دورهای آموزشی (epoch) یکسان، تابع زیان CIoU در نهایت به mAP@0.5:0.95 بهتری نسبت به MSE می‌رسد.
  3. (سخت) استنتاج چندمقیاسی (multi-scale inference) را پیاده کنید: یک تصویر یکسان را با سه مقیاس مختلف به مدل بدهید، جعبه‌های پیش‌بینی‌شده را با هم اجتماع بگیرید، و در پایان یک NMS واحد روی همه‌ی آن‌ها اجرا کنید. میزان افزایش mAP را در مقایسه با استنتاج تک‌مقیاسی روی مجموعه‌ی اعتبارسنجی بسنجید.

اصطلاحات کلیدی

اصطلاح در گفت‌وگوها معنای فنی و دقیق
انکر (Anchor) «جعبه‌ی پیشین» شکل از پیش تعریف‌شده‌ی جعبه در هر سلول شبکه که شبکه به‌جای مختصات مطلق، تغییرات (delta) نسبت به آن را پیش‌بینی می‌کند
IoU «هم‌پوشانی» اشتراک روی اجتماع دو جعبه؛ سنجه‌ی جهانی سنجش شباهت در تشخیص اشیاء
NMS «حذف موارد تکراری» الگوریتمی حریصانه که پیش‌بینی‌ها با بالاترین امتیاز را نگه می‌دارد و موارد هم‌پوشان با تداخل بیش از آستانه را پاک می‌کند
شیءبودگی (Objectness) «آیا این‌جا چیزی هست؟» یک عدد اسکالر به ازای هر انکر و هر سلول که احتمال قرار گرفتن مرکز یک شیء در آن سلول را بیان می‌کند
گام شبکه (Grid stride) «ضریب کاهش ابعاد» تعداد پیکسل‌ها به ازای هر سلول شبکه؛ مثلاً ورودی 416 پیکسلی با سر شبکه‌ی 13 تایی، گام 32 دارد
mAP «میانگینِ میانگین دقت» میانگین مساحت زیر منحنی دقت-بازخوانی، با میانگین‌گیری روی کلاس‌ها و (در سنجه‌ی COCO) روی آستانه‌های مختلف IoU
AP@0.5 «معیار PASCAL VOC» میانگین دقت با آستانه‌ی هم‌پوشانی IoU برابر 0.5؛ نسخه‌ی آسان‌گیرانه‌تر این معیار
mAP@0.5:0.95 «معیار COCO» میانگین روی آستانه‌های IoU از 0.5 تا 0.95 با گام 0.05؛ نسخه‌ی سخت‌گیرانه‌تر و استاندارد فعلی جامعه‌ی هوش مصنوعی

برای مطالعه‌ی بیشتر

  • YOLOv1: You Only Look Once (Redmon et al., 2016) — مقاله‌ی بنیادینی که مسیر را باز کرد؛ تمام نسخه‌های بعدی YOLO بهینه‌سازی همین ساختار هستند
  • YOLOv3 (Redmon & Farhadi, 2018) — مقاله‌ای که سرهای چندمقیاسی به سبک FPN را معرفی کرد؛ شفاف‌ترین نمودار ساختار را در این مقاله پیدا می‌کنید
  • Ultralytics YOLOv8 docs — مرجع اصلی محیط‌های عملیاتی فعلی؛ شامل توضیح فرمت داده‌ها، داده‌افزایی‌ها (augmentations) و دستورالعمل‌های آموزش
  • The Illustrated Guide to Object Detection (Jonathan Hui) — بهترین راهنمای ساده و همه‌فهم برای مقایسه‌ی خانواده‌های مختلف دیتکتورها؛ برای درک ارتباط میان DETR، RetinaNet، FCOS و YOLO بسیار خواندنی است

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسیک آشکارساز YOLO با شبکه‌ی 13x13، تعداد 3 انکر در هر سلول و 20 کلاس، خروجی هدی با ابعاد (N, 13, 13, 3, 25) تولید می‌کند. چرا بعد آخر 25 است؟
  2. پیش از درسچرا YOLO مقادیر tx و ty را قبل از اضافه کردن به مختصات سلول شبکه از یک تابع sigmoid عبور می‌دهد؟
  3. پس از درسآشکارساز شما بعد از هد، 15 جعبه در اطراف یک شیء منفرد پیش‌بینی می‌کند. شما NMS را با iou_threshold=0.45 اعمال می‌کنید و 1 جعبه تحویل می‌گیرید. NMS چه کاری انجام داده است؟
  4. پس از درسمدل شما دارای mAP@0.5 = 0.75 اما mAP@0.5:0.95 = 0.32 است. دقیق‌ترین تفسیر چیست؟
  5. پس از درسوزن‌های تابع زیان در YOLO، یعنی lambda_coord=5.0 و lambda_noobj=0.5 تقریباً مشابه مقاله اصلی هستند. این نسبت‌ها نشان‌دهنده‌ی چه واقعیتی هستند؟

سؤالات متداول

چرا در YOLO از انکرها (Anchors) به جای رگرسیون مستقیم مختصات استفاده می‌شود؟

پیش‌بینی ابعاد بزرگ یا تغییرات شدید جعبه‌ها از طریق میدان دید کوچک لایه‌های کانولوشن بسیار سخت است؛ انکرها ابعاد اولیه‌ی استانداردی فراهم می‌کنند تا مدل فقط تغییرات کوچک (delta) را نسبت به آن‌ها یاد بگیرد.

الگوریتم NMS چیست و چه نقشی در استنتاج YOLO دارد؟

الگوریتم حذف غیرحداکثر (Non-Maximum Suppression) جعبه با بالاترین امتیاز اطمینان را نگه می‌دارد و پیش‌بینی‌های هم‌پوشانی را که IoU آن‌ها از یک آستانه معین بیشتر است، حذف می‌کند تا از تکرار چندباره‌ی یک شیء جلوگیری شود.

تفاوت معیار AP@0.5 با معیار mAP@0.5:0.95 در ارزیابی مدل چیست؟

معیار AP@0.5 فقط هم‌پوشانی حداقلی (IoU=0.5) را می‌سنجد و آسان‌گیر است، در حالی که mAP@0.5:0.95 میانگین دقت را در بازه‌ای از آستانه‌های سخت‌گیرانه‌تر (تا 0.95) اندازه می‌گیرد و دقت مکان‌یابی مرز جعبه‌ها را بررسی می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.