Asosiy qismga o'tish

Loss va tayinlash: model qanday o'rganadi

6-bob 11 kod bloki 4966 so'z

Bu bobning kodi: kod/06_loss_tayinlash.py — kitob papkasidan ishga tushiring: python kod/06_loss_tayinlash.py

O'qitish ikki savoldan iborat

5-bobda o'qitilgan model 8 400 katakdan 5 ta box ni qanday chiqarishini ko'rdik. O'qitishda esa teskari savol turadi: rasmda odam qo'lda belgilagan haqiqiy obyektlar bor (ground truth, qisqacha GT). Model qanchalik xato qildi va vaznlarni qaysi tomonga o'zgartirish kerak?

Buning uchun ikki savolga javob kerak:

  1. Tayinlash (assignment). 8 400 katakdan qaysilari qaysi obyekt uchun javobgar? Qolganlari "fon" — ular hech narsa topmasligi kerak.
  2. Loss. Bashorat haqiqatdan qanchalik uzoq? Uchta son: sinf xatosi, box xatosi, DFL xatosi.

Bu bobda ikkalasini ultralytics kodidan (ultralytics/utils/tal.py va ultralytics/utils/loss.py) qo'lda qayta yozamiz va natijani kutubxonaning o'zi hisoblagan loss bilan solishtiramiz.

Misol: COCO dagi bitta rasm

2-bobda yuklab olingan COCO val dan bitta rasm va uning label fayli. Label formati 1-bobdagi: sinf markaz_x markaz_y kenglik balandlik, 0..1 oralig'ida.

kod/06_loss_tayinlash.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python37 qator
import math
import cv2
import numpy as np
import pandas as pd
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
from ultralytics import YOLO
from ultralytics.cfg import get_cfg
from ultralytics.data.augment import LetterBox
from ultralytics.utils import DEFAULT_CFG, LOGGER
from ultralytics.utils.loss import E2ELoss, v8DetectionLoss
from ultralytics.utils.tal import TaskAlignedAssigner

LOGGER.setLevel("ERROR")

nom = "000000031093"
asl = cv2.imread(f"datasets/coco/images/val2017/{nom}.jpg")
h0, w0 = asl.shape[:2]
label = np.loadtxt(f"datasets/coco/labels/val2017/{nom}.txt", ndmin=2)
print(open(f"datasets/coco/labels/val2017/{nom}.txt").read().strip())

lb = LetterBox((640, 640), auto=False)(image=asl)                  # o'qitishdagi kabi to'liq kvadrat 640x640
g = min(640 / h0, 640 / w0)
px, py = (640 - w0 * g) / 2, (640 - h0 * g) / 2
x = torch.from_numpy(lb[:, :, ::-1].copy()).permute(2, 0, 1)[None].float() / 255

# GT: normallashgan xywh (asl rasm) -> letterbox pikselidagi xyxy
xc, yc = label[:, 1] * w0 * g + px, label[:, 2] * h0 * g + py
bw, bh = label[:, 3] * w0 * g, label[:, 4] * h0 * g
gt_box = torch.tensor(np.stack([xc - bw / 2, yc - bh / 2, xc + bw / 2, yc + bh / 2], 1), dtype=torch.float32)
gt_sinf = torch.tensor(label[:, 0]).long()

model = YOLO("weights/yolo11n.pt")
nomlar = model.names
show(pd.DataFrame({"GT": range(len(gt_box)), "sinf": [nomlar[int(c)] for c in gt_sinf],
                   "kenglik, px": bw.round(), "balandlik, px": bh.round()}))
Chiqish
0 0.521188 0.476725 0.363344 0.590587
36 0.461625 0.76034 0.14575 0.0287089
0 0.88225 0.0655751 0.0465313 0.0865493
24 0.982766 0.213415 0.0285938 0.0464554
36 0.773828 0.0523357 0.037 0.0211972
0 0.802531 0.0453286 0.050875 0.0906573
GTsinfkenglik, pxbalandlik, px
0person233252
1skateboard9312
2person3037
3backpack1820
4skateboard249
5person3339

Katta skeytbordchi, uning skeytbordi (yupqa va keng), fonda ikki kichik odam, yana bitta kichik skeytbord va o'ng chetda kichkina ryukzak. Katta va kichik obyektlar aralash — tayinlashni ko'rish uchun yaxshi misol.

Tarmoqning xom chiqishi va 8 400 katakning har biri chizgan box (5-bobdagi dekodlash):

kod/06_loss_tayinlash.pydavomi
python19 qator
def bashorat(tarmoq):
    """Xom chiqish -> (sinf logit lari (8400, 80), DFL logit lari (8400, 64), box lar (8400, 4) xyxy piksel)."""
    with torch.no_grad():
        _, ichki = tarmoq(x)
    z = ichki["boxes"][0].T
    d = (z.view(-1, 4, 16).softmax(-1) * torch.arange(16)).sum(-1) * qadam[:, None]   # DFL -> piksel masofa
    return ichki["scores"][0].T, z, torch.cat([anc - d[:, :2], anc + d[:, 2:]], 1)

tarmoq = model.model.eval()
anc, qadam = [], []
for H, s in [(80, 8), (40, 16), (20, 32)]:                        # 640x640 kirish: uch to'r
    yy, xx = torch.meshgrid(torch.arange(H) + 0.5, torch.arange(H) + 0.5, indexing="ij")
    anc.append(torch.stack([xx.flatten(), yy.flatten()], 1) * s)  # katak markazlari, pikselda
    qadam.append(torch.full((H * H,), float(s)))
anc, qadam = torch.cat(anc), torch.cat(qadam)                    # (8400, 2), (8400,)

logit, z, p_box = bashorat(tarmoq)
ball = logit.sigmoid()
print("kataklar:", len(anc), "| sinf logit lari:", tuple(logit.shape), "| box lar:", tuple(p_box.shape))
Chiqish
kataklar: 8400 | sinf logit lari: (8400, 80) | box lar: (8400, 4)

CIoU: box larni solishtirishning yaxshiroq usuli

Tayinlash ham, box loss ham oddiy IoU emas, CIoU (Complete IoU) ishlatadi:

ρ — ikki box markazlari orasidagi masofa, c — ikkalasini o'rab turuvchi eng kichik box diagonali, v — shakl (eni va bo'yi nisbati) farqi. Ultralytics kodini (utils/metrics.py, bbox_iou) aynan takrorlaymiz, kichik eps qo'shimchalari bilan birga:

kod/06_loss_tayinlash.pydavomi
python29 qator
def ciou(a, b, eps=1e-7):
    """Complete IoU. a, b: (..., 4) xyxy."""
    w1, h1 = a[..., 2] - a[..., 0], a[..., 3] - a[..., 1] + eps
    w2, h2 = b[..., 2] - b[..., 0], b[..., 3] - b[..., 1] + eps
    kesishma = (torch.minimum(a[..., 2], b[..., 2]) - torch.maximum(a[..., 0], b[..., 0])).clamp(0) * \
               (torch.minimum(a[..., 3], b[..., 3]) - torch.maximum(a[..., 1], b[..., 1])).clamp(0)
    iou = kesishma / (w1 * h1 + w2 * h2 - kesishma + eps)
    cw = torch.maximum(a[..., 2], b[..., 2]) - torch.minimum(a[..., 0], b[..., 0])    # o'rab turuvchi box
    ch = torch.maximum(a[..., 3], b[..., 3]) - torch.minimum(a[..., 1], b[..., 1])
    c2 = cw ** 2 + ch ** 2 + eps                                                      # uning diagonali kvadrati
    rho2 = ((b[..., 0] + b[..., 2] - a[..., 0] - a[..., 2]) ** 2 + (b[..., 1] + b[..., 3] - a[..., 1] - a[..., 3]) ** 2) / 4
    v = 4 / math.pi ** 2 * (torch.atan(w2 / h2) - torch.atan(w1 / h1)) ** 2
    alfa = v / (v - iou + 1 + eps)
    return iou - rho2 / c2 - alfa * v

def iou(a, b):
    """Oddiy IoU (1-bob)."""
    kesishma = (torch.minimum(a[2], b[2]) - torch.maximum(a[0], b[0])).clamp(0) * \
               (torch.minimum(a[3], b[3]) - torch.maximum(a[1], b[1])).clamp(0)
    yuza = lambda q: (q[2] - q[0]) * (q[3] - q[1])
    return kesishma / (yuza(a) + yuza(b) - kesishma)

gt = torch.tensor([100.0, 100, 200, 200])                          # 100x100 lik GT
holatlar = {"markazi bir xil, kattaroq": [79.29, 79.29, 220.71, 220.71],
            "o'ngga siljigan":            [133.33, 100, 233.33, 200],
            "yuzi bir xil, shakli yotiq": [75, 116.67, 225, 183.33],
            "umuman tegmaydi":           [300, 100, 400, 200]}
show(pd.DataFrame([{"holat": k, "IoU": round(float(iou(gt, torch.tensor(b))), 3),
                    "CIoU": round(float(ciou(gt, torch.tensor(b))), 3)} for k, b in holatlar.items()]))
holatIoUCIoU
markazi bir xil, kattaroq0.50.5
o'ngga siljigan0.50.46
yuzi bir xil, shakli yotiq0.50.495
umuman tegmaydi0-0.4

Birinchi uchta holatda IoU bir xil — 0.5. CIoU esa ularni ajratadi: markazi siljigan box ko'proq, shakli boshqa box ozroq jazolanadi. To'rtinchi holat eng muhimi: box lar tegmasa, IoU nol va u "qaysi tomonga siljitish kerak" haqida hech narsa demaydi — gradient ham nol. CIoU esa manfiy va box GT ga yaqinlashgan sari o'sadi. O'qitishning boshida, tasodifiy box lar hali GT ga tegmaganda, aynan shu kerak.

Tayinlash (TAL): qaysi kataklar javobgar

YOLOv8 dan beri ultralytics Task-Aligned Assigner (TAL) ishlatadi. U to'rt qadamda ishlaydi:

  1. Nomzodlar. Faqat markazi GT box ichiga tushgan kataklar. Qo'shimcha qoida: GT ning biror tomoni eng kichik qadamdan (8 px) qisqa bo'lsa, o'sha tomon 16 px ga kengaytiriladi — aks holda juda mayda obyekt ichiga birorta ham katak markazi tushmasligi mumkin.
  2. Moslik bali. Har nomzod uchun ikki narsa o'lchanadi: s — katak GT sinfiga bergan ehtimol, u — katak chizgan box ning GT bilan CIoU si. Ular bitta songa birlashtiriladi.
  3. Eng yaxshi 10 tasi. Har GT uchun moslik bali bo'yicha eng yaxshi 10 ta katak javobgar deb belgilanadi.
  4. Nizolar. Bitta katak ikki GT ga tanlansa, u box i ko'proq mos kelgan (u kattaroq) GT ga beriladi.

Moslik bali:

β = 6 ning ma'nosi raqamlarda:

kod/06_loss_tayinlash.pydavomi
python2 qator
show(pd.DataFrame([{"s (sinf ehtimoli)": s, "u (box mosligi)": u, "t = s^0.5 * u^6": round(s ** 0.5 * u ** 6, 4)}
                   for s, u in [(0.9, 0.5), (0.9, 0.7), (0.3, 0.9), (0.9, 0.9)]]))
s (sinf ehtimoli)u (box mosligi)t = s^0.5 * u^6
0.90.50.015
0.90.70.112
0.30.90.291
0.90.90.504

0.9 ishonch bilan yomon box chizgan katak (u = 0.5) ishonchi 0.3 bo'lsa ham yaxshi box chizgan katakdan (u = 0.9) ancha past baho oladi. Nomi shundan: tayinlash vazifaga moslangan — ikkala vazifada yaxshi bo'lgan kataklarni tanlaydi, box sifati esa ancha kuchliroq hisobga olinadi.

To'rt qadamni yozamiz:

kod/06_loss_tayinlash.pydavomi
python28 qator
def tal(ball, p_box, gt_sinf, gt_box, topk=10, alfa=0.5, beta=6.0):
    """Bitta rasm uchun TAL. Qaytaradi: musbat (M, N) 0/1, t (M, N), u (M, N), ichida (M, N)."""
    M, N = len(gt_box), len(anc)
    markaz, wh = (gt_box[:, :2] + gt_box[:, 2:]) / 2, gt_box[:, 2:] - gt_box[:, :2]
    wh = torch.where(wh < 8, torch.tensor(16.0), wh)                                  # juda qisqa tomon -> 16 px
    g = torch.cat([markaz - wh / 2, markaz + wh / 2], 1)
    ichida = torch.cat([anc[None] - g[:, None, :2], g[:, None, 2:] - anc[None]], -1).amin(-1) > 1e-9   # 1-qadam
    s = ball[:, gt_sinf].T * ichida                                                   # (M, N)
    u = torch.zeros(M, N)
    u[ichida] = ciou(gt_box[:, None].expand(M, N, 4)[ichida], p_box[None].expand(M, N, 4)[ichida]).clamp(0)
    t = s.pow(alfa) * u.pow(beta)                                                     # 2-qadam
    musbat = torch.zeros(M, N).scatter_(1, t.topk(topk, dim=1).indices, 1.0) * ichida   # 3-qadam
    nizo = musbat.sum(0) > 1                                                          # 4-qadam
    if nizo.any():
        eng_mos = torch.zeros(M, N).scatter_(0, u.argmax(0, keepdim=True), 1.0)
        musbat = torch.where(nizo[None], eng_mos, musbat)
    return musbat, t, u, ichida

musbat, t, u, ichida = tal(ball, p_box, gt_sinf, gt_box)
fg = musbat.sum(0) > 0                                               # javobgar kataklar
gt_idx = musbat.argmax(0)                                            # har katak qaysi GT ga javobgar

tayinlovchi = TaskAlignedAssigner(topk=10, num_classes=80, alpha=0.5, beta=6.0, stride=[8, 16, 32])
_, _, u_maqsad, u_fg, u_idx = tayinlovchi(ball[None], p_box[None], anc, gt_sinf.view(1, -1, 1),
                                          gt_box[None], torch.ones(1, len(gt_box), 1))
print("javobgar kataklar: bizda", int(fg.sum()), "| ultralytics da", int(u_fg.sum()))
print("farq qilgan kataklar:", int((fg != u_fg[0]).sum()),
      "| GT raqami farq qilganlar:", int((gt_idx[fg] != u_idx[0][fg]).sum()))
Chiqish
javobgar kataklar: bizda 50 | ultralytics da 50
farq qilgan kataklar: 0 | GT raqami farq qilganlar: 0

Har GT ga nechta katak tushdi va qaysi to'rlardan:

kod/06_loss_tayinlash.pydavomi
python7 qator
daraja = np.where(np.arange(8400) < 6400, "P3", np.where(np.arange(8400) < 8000, "P4", "P5"))
qatorlar = []
for k in range(len(gt_box)):
    tanlangan = daraja[torch.nonzero(musbat[k]).flatten().numpy()]
    qatorlar.append({"GT": k, "sinf": nomlar[int(gt_sinf[k])], "nomzodlar": int(ichida[k].sum()),
                     "javobgar": len(tanlangan), **{p: int((tanlangan == p).sum()) for p in ("P3", "P4", "P5")}})
show(pd.DataFrame(qatorlar))
GTsinfnomzodlarjavobgarP3P4P5
0person1 165100010
1skateboard2710802
2person1910730
3backpack77610
4skateboard33300
5person2610820
kod/06_loss_tayinlash.pydavomi
python13 qator
ranglar = plt.cm.tab10(np.arange(len(gt_box)))
_, axes = plt.subplots(1, 2, figsize=(13, 6.5), gridspec_kw={"width_ratios": [1, 1]})
for ax, (xlim, ylim, kattalik) in zip(axes, [((0, 640), (640, 0), 2.5), ((460, 640), (230, 95), 9)]):
    ax.imshow(lb[:, :, ::-1])
    for k, (b, r) in enumerate(zip(gt_box.tolist(), ranglar)):
        ax.add_patch(plt.Rectangle(b[:2], b[2] - b[0], b[3] - b[1], fill=False, color=r, lw=2))
        idx = torch.nonzero(musbat[k]).flatten()
        ax.scatter(anc[idx, 0], anc[idx, 1], s=qadam[idx] * kattalik, color=r, edgecolor="black", zorder=3)
    ax.set_xlim(*xlim)
    ax.set_ylim(*ylim)
    ax.axis("off")
axes[1].set_title("o'ng-yuqori burchak, kattalashtirilgan")
fig("tayinlash.png", "Rangli ramka — GT, shu rangdagi nuqtalar — unga javobgar kataklar markazi. Katta nuqta — P5, o'rtachasi — P4, kichik — P3.")

Rangli ramka — GT, shu rangdagi nuqtalar — unga javobgar kataklar markazi. Katta nuqta — P5, o'rtachasi — P4, kichik — P3.

Rangli ramka — GT, shu rangdagi nuqtalar — unga javobgar kataklar markazi. Katta nuqta — P5, o'rtachasi — P4, kichik — P3.

Katta odam 165 ta nomzoddan faqat 10 tasini oladi — hammasi P5 dan va hammasi tanasining markaziy qismida: chetdagi kataklarning box lari kamroq mos. Kichik odamlar va ryukzak asosan P3 dan. Kichik skeytbord ichiga atigi 3 ta katak markazi tushgan, shuning uchun topk = 10 bo'lsa ham u 3 ta javobgar oladi. Mayda obyekt o'qitishda kamroq signal oladi — bu 1-bobdagi kichik obyekt muammosining yana bir sababi. Kulrang yo'laklar — letterbox to'ldirishi.

Maqsad ballari: 1 emas

Javobgar katak uchun sinf maqsadi (target) 1 emas. U moslik baliga qarab kichraytiriladi: GT ning eng yaxshi katagi o'z CIoU sini oladi, qolganlari esa t / t_max nisbatida kamroq. Bu yerda u_max — shu GT ga javobgar kataklar ichidagi eng katta CIoU, t_max — eng katta moslik bali:

kod/06_loss_tayinlash.pydavomi
python9 qator
t_mus = t * musbat
norm = (t_mus * (u * musbat).amax(1, keepdim=True) / (t_mus.amax(1, keepdim=True) + 1e-9)).amax(0)   # (8400,)
maqsad = torch.zeros(8400, 80)
maqsad[fg, gt_sinf[gt_idx[fg]]] = norm[fg]
print("ultralytics maqsadlari bilan eng katta farq:", float((maqsad - u_maqsad[0]).abs().max()))

k0 = torch.nonzero(musbat[0]).flatten()                              # katta odamning 10 ta katagi
show(pd.DataFrame({"katak": k0.numpy(), "s": ball[k0, gt_sinf[0]].numpy(), "u (CIoU)": u[0, k0].numpy(),
                   "t": t[0, k0].numpy(), "maqsad y": norm[k0].numpy()}).sort_values("t", ascending=False).round(3), 10)
Chiqish
ultralytics maqsadlari bilan eng katta farq: 3.5762786865234375e-07
kataksu (CIoU)tmaqsad y
8 2110.910.940.6560.94
8 1910.9160.9390.6560.938
8 2090.8990.9390.6510.932
8 1920.9130.9370.6490.928
8 2100.9060.9380.6480.928
8 1710.9130.9370.6460.925
8 1700.9220.9360.6460.925
8 1890.9130.9360.6420.919
8 1900.9170.9350.6420.919
8 1690.9180.9340.6380.913

Bu nega muhim: model "odam bor" ni emas, "odam bor va mening box im shunchalik yaxshi" ni o'rganadi. Shuning uchun YOLO ning ishonch qiymati box sifati bilan bog'liq bo'ladi, va NMS eng ishonchli box ni qoldirganda, odatda eng aniq box ham shu bo'ladi.

Uchta loss

Sinf: BCE. Hamma 8 400 × 80 juftlik uchun binary cross-entropy. Fon kataklari ham qatnashadi — ularning maqsadi 0, ular "hech narsa yo'q" deyishni o'rganadi. T — hamma maqsadlar yig'indisi:

Box: CIoU. Faqat javobgar kataklar uchun, har biri o'z maqsad bali bilan tortilgan:

DFL. Haqiqiy masofa d (katak birligida) ikki bin orasida yotadi: i = ⌊d⌋ va i + 1. Loss shu ikki bin ga, d* ga yaqinligi bo'yicha tortilgan cross-entropy:

Masalan d* = 6.3 bo'lsa: 6-bin ga 0.7, 7-bin ga 0.3 og'irlik. 5-bobda ko'rgan "ikki qo'shni bin ga bo'lingan ehtimol" aynan shu loss natijasi.

Jami — uchalasi default.yaml dagi koeffitsiyentlar bilan (box: 7.5, cls: 0.5, dfl: 1.5):

Qo'lda yozamiz va ultralytics ning v8DetectionLoss klassi bilan solishtiramiz. O'qitilgan yolo11n va xuddi shu arxitekturaning o'qitilmagan (tasodifiy vaznli) nusxasi uchun:

kod/06_loss_tayinlash.pydavomi
python34 qator
def yoqotish(logit, z, p_box):
    """Qo'lda: TAL -> maqsadlar -> uchta loss, koeffitsiyentlar bilan."""
    musbat, t, u, _ = tal(logit.sigmoid(), p_box, gt_sinf, gt_box)
    fg, gt_idx = musbat.sum(0) > 0, musbat.argmax(0)
    t_mus = t * musbat
    norm = (t_mus * (u * musbat).amax(1, keepdim=True) / (t_mus.amax(1, keepdim=True) + 1e-9)).amax(0)
    maqsad = torch.zeros_like(logit)
    maqsad[fg, gt_sinf[gt_idx[fg]]] = norm[fg]
    T = max(float(maqsad.sum()), 1.0)
    L_cls = F.binary_cross_entropy_with_logits(logit, maqsad, reduction="sum") / T
    w, t_box = maqsad.sum(1)[fg], gt_box[gt_idx[fg]]
    L_box = ((1 - ciou(p_box[fg], t_box)) * w).sum() / T
    d_haq = (torch.cat([anc[fg] - t_box[:, :2], t_box[:, 2:] - anc[fg]], 1) / qadam[fg, None]).clamp(0, 14.99)
    i = d_haq.long()                                                 # chap bin
    log_p = F.log_softmax(z[fg].view(-1, 4, 16), -1)
    ce = -(log_p.gather(-1, i[..., None])[..., 0] * (i + 1 - d_haq) + log_p.gather(-1, (i + 1)[..., None])[..., 0] * (d_haq - i))
    L_dfl = (ce.mean(1) * w).sum() / T
    return [7.5 * float(L_box), 0.5 * float(L_cls), 1.5 * float(L_dfl)]

batch = {"batch_idx": torch.zeros(len(gt_box)), "cls": gt_sinf.float(),
         "bboxes": torch.tensor(np.stack([xc, yc, bw, bh], 1) / 640, dtype=torch.float32)}   # ultralytics formati

torch.manual_seed(0)                                                 # tasodifiy vaznlar har safar bir xil bo'lsin
qatorlar = []
for nom_, net in [("o'qitilgan", tarmoq), ("o'qitilmagan", YOLO("yolo11n.yaml").model.eval())]:
    net.args = get_cfg(DEFAULT_CFG)                                  # loss koeffitsiyentlari shu yerdan olinadi
    with torch.no_grad():
        _, ichki = net(x)
        ultra = v8DetectionLoss(net).get_assigned_targets_and_loss(ichki, batch)[2].tolist()
    biz = yoqotish(*bashorat(net))
    for tur, qiymat in [("biz", biz), ("ultralytics", ultra)]:
        qatorlar.append({"model": nom_, "hisob": tur, "7.5 x box": qiymat[0], "0.5 x cls": qiymat[1],
                         "1.5 x dfl": qiymat[2], "jami": sum(qiymat)})
show(pd.DataFrame(qatorlar).round(4))
modelhisob7.5 x box0.5 x cls1.5 x dfljami
o'qitilganbiz1.6851.471.2024.356
o'qitilganultralytics1.6851.471.2024.356
o'qitilmaganbiz1.9126.5134.15912.584
o'qitilmaganultralytics1.9126.5134.15912.584

Qo'lda hisob ultralytics bilan to'liq mos. O'qitilgan va o'qitilmagan model orasidagi katta farq sinf (6.5 → 1.5) va DFL (4.2 → 1.2) qismlarida. Box qismi esa deyarli bir xil: box loss faqat tanlangan kataklarda o'lchanadi, TAL esa tasodifiy model uchun ham o'sha paytda tasodifan eng yaxshi mos kelgan box larni tanlaydi. O'qitish — mana shu jami loss ni minglab rasmlar bo'yicha o'rtacha pasaytirish. 7-bobda buni o'zimiz qilamiz.

YOLO26: ikki bosh, ikki xil tayinlash

5-bobda YOLO26 ning ikki boshi borligini ko'rdik. Ularning farqi aynan tayinlashda. utils/loss.py dagi E2ELoss klassi:

  • one2many bosh — topk=10, YOLO11 dagi kabi. Har GT ga 10 tagacha katak: boy gradient, lekin ishlatishda takrorlar (NMS kerak).
  • one2one bosh — avval topk=7 nomzod, keyin ulardan topk2=1. Har GT ga aynan bitta katak. Bosh "bitta obyektga bitta javob" ni o'rganadi.

YOLO26 ning haqiqiy bashoratlari ustida tekshiramiz:

kod/06_loss_tayinlash.pydavomi
python13 qator
tarmoq26 = YOLO("weights/yolo26n.pt").model.eval()
tarmoq26.args = get_cfg(DEFAULT_CFG)
with torch.no_grad():
    _, ichki26 = tarmoq26(x)
qatorlar = []
for bosh, topk, topk2 in [("one2many", 10, None), ("one2one", 7, 1)]:
    kriteriy = v8DetectionLoss(tarmoq26, tal_topk=topk, tal_topk2=topk2)
    with torch.no_grad():
        (fg26, idx26, *_), _, _ = kriteriy.get_assigned_targets_and_loss(ichki26[bosh], batch)
    qatorlar.append({"bosh": bosh, "topk": topk, "topk2": topk2 or topk,
                     **{f"GT {k}": int((idx26[0][fg26[0]] == k).sum()) for k in range(len(gt_box))},
                     "DFL ishlatiladimi": kriteriy.use_dfl})
show(pd.DataFrame(qatorlar))
boshtopktopk2GT 0GT 1GT 2GT 3GT 4GT 5DFL ishlatiladimi
one2many10101010107310False
one2one71111111False

YOLO26 da reg_max = 1, DFL yo'q: loss ning uchinchi qismi DFL o'rniga masofalar farqining oddiy L1 qiymati bo'ladi (BboxLoss dagi else shoxi).

Ikkala bosh bir vaqtda o'qitiladi, lekin og'irliklari o'qitish davomida o'zgaradi. Har epoxa oxirida E2ELoss.update() chaqiriladi:

kod/06_loss_tayinlash.pydavomi
python5 qator
e2e = E2ELoss(tarmoq26)
print("epoxalar soni:", tarmoq26.args.epochs)
for epoxa in [0, 1, 25, 50, 75, 99]:
    o2m = e2e.decay(epoxa)
    print(f"{epoxa:3d} ta epoxadan keyin: one2many og'irligi {o2m:.3f}, one2one og'irligi {1 - o2m:.3f}")
Chiqish
epoxalar soni: 100
  0 ta epoxadan keyin: one2many og'irligi 0.800, one2one og'irligi 0.200
  1 ta epoxadan keyin: one2many og'irligi 0.793, one2one og'irligi 0.207
 25 ta epoxadan keyin: one2many og'irligi 0.623, one2one og'irligi 0.377
 50 ta epoxadan keyin: one2many og'irligi 0.446, one2one og'irligi 0.554
 75 ta epoxadan keyin: one2many og'irligi 0.270, one2one og'irligi 0.730
 99 ta epoxadan keyin: one2many og'irligi 0.100, one2one og'irligi 0.900

Boshida loss ning 80% i one2many dan keladi: model avval "obyekt qayerda" ni boy signal bilan tez o'rganadi. Oxiriga kelib 90% one2one ga o'tadi: endi "faqat bir marta javob ber" ni aniqlashtiradi. 2-bobda ko'rgan narxi — NMS siz rejimda 0.7–0.8 mAP yo'qotish — shu murosa natijasi.

Asosiy fikr

  • O'qitish = tayinlash + loss. TAL har GT uchun markazi GT ichida bo'lgan kataklardan t = s^0.5 · u^6 bo'yicha eng yaxshi 10 tasini tanlaydi — box sifati sinf ishonchidan ancha muhimroq.
  • Sinf maqsadi 1 emas, balki box sifatiga bog'liq kasr son. Shuning uchun YOLO ning ishonch qiymati "box qanchalik yaxshi" ni ham bildiradi.
  • Loss = 7.5 · CIoU loss + 0.5 · BCE + 1.5 · DFL. YOLO26 da ikki bosh ikki xil tayinlanadi (10 ta va 1 ta katak), og'irlik o'qitish davomida birinchisidan ikkinchisiga o'tadi.

Mashqlar

  1. tal() ni beta=1 bilan ishga tushiring. Katta odamga tanlangan kataklar o'zgaradimi? Nega ultralytics β ni 6 qilib qo'ygan?
  2. holatlar jadvaliga o'zingiz bitta holat qo'shing: GT ichida turgan, lekin undan ancha kichik box. IoU va CIoU ni oldindan taxmin qiling, keyin tekshiring.
  3. Ryukzak GT sini (18 × 20 px) 6 × 6 px ga kichraytiring va tal() ni qayta ishga tushiring. Nomzodlar soni nechta bo'ladi? 16 px ga kengaytirish qoidasini olib tashlasangiz-chi?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar