7.14. YOLOv8:n läpikäynti

YOLOv8-jälkikäsittelijä on tarpeeksi pieni käytäväksi läpi syötetensorista palautettuun listaan. Sen lukeminen kerran näyttää, mitä jokainen muu luettelon jälkikäsittelijä tekee: kynnystä kvantisoidut pisteytykset edullisesti, ota se mikä säilyy, dekvantisoi, dekoodaa geometria, työnnä NMS:lle, palauta luokkakohtaiset listat.

7.14.1. Lähtötensori

YOLOv8-malli tuottaa yhden ulostulotensorin, jonka muoto on (1, C, A) – yksi kehys, C kanavaa, A ankkuriennustetta. Ensimmäiset neljä kanavaa ovat laatikon geometriaa – cx, cy, w, h – normalisoituna välille [0, 1] verkon syötemittasuhteista. Loput C - 4 kanavaa ovat luokkakohtaisia pisteytyksiä, jo välillä [0, 1] koulutetulle mallille. Jokainen ankkuri on sarake alas kanavia pitkin.

Sivuttain asetettu ruudukko: rivit merkittyinä cx, cy, w, h, score_0, score_1, ..., score_(N-1); sarakkeet merkittyinä ankkuri 0:sta ankkuri A-1:een. Yksi sarake on rajattu osoittamaan, että yksittäisen ankkurin ennuste on yksi sarake alas kanavia pitkin.

Yksi YOLOv8-ankkuri on yksi sarake alas kanavia pitkin: neljä laatikkolukua ja N luokkapisteytystä.

Mukana toimitettu yolov8n_192.tflite on yhden luokan henkilötunnistin, joten C = 5 ja A on tuhansissa; mukautetulla mallilla, joka on koulutettu täydellä 80-luokan COCO-joukolla, on C = 84 samaa A:ta vasten. Alla oleva dekoodaus pätee mille tahansa luokkamäärälle.

7.14.2. Kynnystäminen ennen dekvantisointia

Edullinen vaihe ensin. Ulostulotensori on mallin kvantisoidussa kokonaisluku-dtype-tyypissä, ja jokaisen arvon dekvantisointi koskettaisi jokaista elementtiä tensorissa, jossa on tuhansia merkintöjä – joista useimmat ovat pisteytyksen kynnysarvon alapuolella ja päätyvät hylätyiksi. Jälkikäsittelijä sen sijaan kvantisoi kynnysarvon kerran ja vertaa raa’assa kvantisoidussa avaruudessa:

from ml.utils import quantize, threshold, dequantize, NMS
from ulab import numpy as np

oh, ow, oc = model.output_shape[0]
scale = model.output_scale[0]
t = quantize(model, self.threshold)

column_outputs = outputs[0].reshape((oh * ow, oc))

score_block = column_outputs[4:, :]
score_indices = threshold(score_block, t, scale,
                          find_max=True,
                          find_max_axis=0)
if not len(score_indices):
    return ()

column_outputs on tensori muotoiltuna uudelleen muotoon (C, A) niin, että kanavat ovat rivejä ja ankkurit sarakkeita. score_block on luokkapisteytyksen alitensori – kaikki rivistä 4 alaspäin. ml.utils.threshold() pelkistää tämän lohkon akselia 0 pitkin (find_max=True, find_max_axis=0) ankkurikohtaiseksi maksimipisteytykseksi ja palauttaa sitten niiden ankkureiden indeksit, joiden maksimi läpäisee kvantisoidun kynnysarvon. Koko tensoria ei koskaan dekvantisoitu; vain sarakekohtainen maksimipelkistys kvantisoidussa kokonaislukuavaruudessa.

Jos yksikään ankkuri ei läpäise, jälkikäsittelijä palauttaa tyhjän monikon, jonka predict() tulkitsee ei-tunnistukseksi.

Kaksi tässä koodissa tehtyä päätöstä tekevät eron ajettavissa olevan jälkikäsittelijän ja käyttökelvottoman hitaan välillä. Ensimmäinen on laskennan ajaminen numpy:n läpi: ulostulotensorissa on tuhansia elementtejä, ja sen iterointi raa’assa Pythonissa vie kokonaisia sekunteja päättelyä kohti, kun taas sama laskenta vektoroituna numpy:n läpi suoriutuu millisekunneissa. Toinen on dekvantisointi jälkeen kynnyssuodatuksen sen sijaan, että se tehtäisiin ennen. Ensin dekvantisointi varaisi liukulukutensorin, joka on neljä kertaa kvantisoidun kokoinen, ja kävisi läpi jokaisen elementin ennen kuin lähes kaikki hylättäisiin; vain säilyneiden sarakkeiden dekvantisointi koskettaa korkeintaan kourallista arvoja ja säästää sekä ajan että RAM-muistin, jonka täysi muunnos olisi kuluttanut.

7.14.3. Säilyneiden dekvantisointi

Vain säilyneiden ankkureiden geometria tarvitsee dekoodata. numpy.take() vetää nämä sarakkeet ulos ja yksittäinen ml.utils.dequantize()-kutsu muuntaa ne liukuluvuiksi:

bb = dequantize(model,
                np.take(column_outputs, score_indices, axis=1))

bb on nyt (C, K), jossa K on säilyneiden ankkureiden määrä – tyypillisesti kourallinen, vaikka A olikin tuhansissa.

7.14.4. Geometrian lukeminen

Neljä laatikkokanavaa ja luokkakohtaiset pisteytykset vedetään ulos suoraan:

bb_scores  = np.max(bb[4:, :],    axis=0)
bb_classes = np.argmax(bb[4:, :], axis=0)

x_center = bb[0, :]
y_center = bb[1, :]
w_half   = bb[2, :] * 0.5
h_half   = bb[3, :] * 0.5

bb_scores on paras luokkapisteytys säilynyttä ankkuria kohti; bb_classes on luokkaindeksi, joka tuotti tuon pisteytyksen. Laatikon geometria on edelleen normalisoituna välille [0, 1] verkon syötemittasuhteista, joten seuraava vaihe skaalaa sen pikseleiksi:

ib, ih, iw, ic = model.input_shape[0]
xmin = (x_center - w_half) * iw
ymin = (y_center - h_half) * ih
xmax = (x_center + w_half) * iw
ymax = (y_center + h_half) * ih

Tämän jälkeen laatikot ovat verkon syötteen pikseliavaruudessa – koordinaattiavaruus, jota NMS odottaa syötteenä.

7.14.5. Non-max suppression

Säilyneet kulkevat NMS:n läpi ja palautetaan luokkakohtaisina listoina:

nms = NMS(iw, ih, inputs[0].roi)
for i in range(bb.shape[1]):
    nms.add_bounding_box(xmin[i], ymin[i],
                         xmax[i], ymax[i],
                         bb_scores[i], bb_classes[i])
return nms.get_bounding_boxes(threshold=self.nms_threshold,
                              sigma=self.nms_sigma)

NMS lukee inputs[0].roi-arvon, joten palautetut laatikot ovat alkuperäisen kuvan koordinaattiavaruudessa, ei verkon – sovellus piirtää ne suoraan kaapatulle kehykselle ilman lisäuudelleenkuvausta.

7.14.6. Mitä skripti saa takaisin

Paluuarvo on lista luokkakohtaisia listoja indeksoituna luokan mukaan. Kolmiluokkainen esimerkki voisi näyttää tältä:

[
    [((23, 41, 95, 142), 0.92), ((180, 60, 88, 130), 0.71)],
    [],
    [((310, 95, 55, 70), 0.85)],
]

Jokainen merkintä on ((x, y, w, h), score)-monikko: (x, y) on rajauslaatikon vasen yläkulma alkuperäisen kuvan pikselikoordinaateissa, w ja h ovat sen leveys ja korkeus pikseleinä, ja score on luottamus, jonka verkko antoi tunnistukselle. Niinpä ((180, 60, 88, 130), 0.71) luetaan laatikkona, jonka vasen yläkulma sijaitsee pikselissä (180, 60), ulottuu 88 pikseliä oikealle ja 130 pikseliä alas, ja jonka luottamukseksi raportoitiin 0.71.

Ulompi lista näyttää kaksi säilynyttä laatikkoa luokalle 0, ei mitään luokalle 1, yhden luokalle 2. Luokan 1 tyhjä lista pidetään paikallaan, jotta ulompi indeksi vastaa aina luokkaindeksiä. Mukana toimitetulle henkilötunnistimelle ulommalla listalla on yksittäinen elementti, jonka sisempi lista sisältää säilyneet henkilölaatikot. 80-luokan mallilla siinä on 80 sisempää listaa, joista useimmat tyhjiä millä tahansa kehyksellä, ja ei-tyhjät merkinnät sisältävät laukenneiden luokkien laatikot. Sovellus lukee tuloksen käyttäen enumerate(boxes) käydäkseen luokkaindeksit läpi yhdessä laatikkolistojen kanssa – sama muoto, jota tunnistuksen jälkikäsittelijät tavoittelevat koko luettelossa.