7.14. Detaljni vodič kroz YOLOv8¶
Naknadni procesor za YOLOv8 dovoljno je malen da se prođe korak po korak od ulaznog tenzora do vraćene liste. Jedno čitanje pokazuje što radi svaki drugi naknadni procesor u katalogu: jeftino primijeni prag na kvantizirane rezultate, uzmi što preživi, dekvantiziraj, dekodiraj geometriju, proslijedi u NMS, vrati liste po klasi.
7.14.1. Polazni tenzor¶
YOLOv8 model emitira jedan izlazni tenzor čiji je oblik (1, C, A) – jedna sličica, C kanala, A predviđanja sidara. Prva četiri kanala su geometrija okvira – cx, cy, w, h – normalizirana na [0, 1] ulaznih dimenzija mreže. Preostalih C - 4 kanala su rezultati po klasama, već u [0, 1] za trenirani model. Svako sidro je stupac niz kanale.
Jedno YOLOv8 sidro je jedan stupac niz kanale: četiri broja okvira i N rezultata klasa.¶
Isporučeni yolov8n_192.tflite jednoklasni je detektor osoba, pa je C = 5, a A u tisućama; prilagođeni model treniran na punom 80-klasnom COCO skupu ima C = 84 uz isti A. Dekodiranje u nastavku vrijedi za bilo koji broj klasa.
7.14.2. Primjena praga prije dekvantizacije¶
Najprije jeftin korak. Izlazni tenzor je u kvantiziranom cjelobrojnom dtype modela, a dekvantiziranje svake vrijednosti dotaknulo bi svaki element tenzora s tisućama unosa – od kojih je većina ispod praga rezultata i na kraju se odbacuje. Naknadni procesor umjesto toga jednom kvantizira prag i uspoređuje u sirovom kvantiziranom prostoru:
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 je tenzor preoblikovan u (C, A) tako da su kanali redovi, a sidra stupci. score_block je podtenzor rezultata klasa – sve od retka 4 naniže. ml.utils.threshold() reducira taj blok duž osi 0 (find_max=True, find_max_axis=0) na maksimalni rezultat po sidru, a zatim vraća indekse sidara čiji maksimum prolazi kvantizirani prag. Cijeli tenzor nikada nije dekvantiziran; samo redukcija maksimuma po stupcu u kvantiziranom cjelobrojnom prostoru.
Ako nijedno sidro ne prođe, naknadni procesor vraća praznu torku koju predict() tumači kao odsutnost detekcije.
Dvije odluke u ovom kodu čine razliku između upotrebljivog naknadnog procesora i neupotrebljivo sporog. Prva je vođenje aritmetike kroz numpy: izlazni tenzor ima tisuće elemenata, a njihovo prolaženje u čistom Pythonu traje cijele sekunde po zaključivanju, dok ista aritmetika vektorizirana kroz numpy radi u milisekundama. Druga je dekvantiziranje nakon filtra praga, a ne prije. Dekvantiziranje najprije alociralo bi float tenzor četiri puta veći od kvantiziranog i prošlo kroz svaki element prije nego što gotovo sve odbaci; dekvantiziranje samo preživjelih stupaca dotiče najviše šačicu vrijednosti i štedi i vrijeme i RAM koje bi puna pretvorba potrošila.
7.14.3. Dekvantiziranje preživjelih¶
Samo preživjela sidra trebaju dekodiranu geometriju. numpy.take() izvlači te stupce, a jedan poziv ml.utils.dequantize() pretvara ih u float vrijednosti:
bb = dequantize(model,
np.take(column_outputs, score_indices, axis=1))
bb je sada (C, K) gdje je K broj preživjelih sidara – obično šačica čak i kada je A bio u tisućama.
7.14.4. Čitanje geometrije¶
Četiri kanala okvira i rezultati po klasama izvlače se izravno:
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 je najbolji rezultat klase po preživjelom sidru; bb_classes je indeks klase koja je dala taj rezultat. Geometrija okvira još je u normaliziranom [0, 1] ulaznih dimenzija mreže, pa je sljedeći korak skalira u piksele:
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
Nakon ovoga okviri su u pikselskom prostoru mrežnog ulaza – koordinatnom prostoru koji NMS očekuje na ulazu.
7.14.5. Potiskivanje koje nije najveće (non-max suppression)¶
Preživjeli prolaze kroz NMS i vraćaju se kao liste po klasi:
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 čita inputs[0].roi tako da su vraćeni okviri u koordinatnom prostoru izvorne slike, a ne mreže – aplikacija ih crta izravno na uhvaćenu sličicu bez daljnjeg ponovnog mapiranja.
7.14.6. Što skripta dobiva natrag¶
Povratna vrijednost je lista listi po klasi indeksirana prema klasi. Primjer s tri klase mogao bi izgledati ovako:
[
[((23, 41, 95, 142), 0.92), ((180, 60, 88, 130), 0.71)],
[],
[((310, 95, 55, 70), 0.85)],
]
Svaki unos je torka ((x, y, w, h), score): (x, y) je gornji lijevi kut graničnog okvira u pikselskim koordinatama izvorne slike, w i h su njegova širina i visina u pikselima, a score je pouzdanost koju je mreža dodijelila detekciji. Tako se ((180, 60, 88, 130), 0.71) čita kao okvir čiji gornji lijevi kut leži na pikselu (180, 60), proteže se 88 piksela udesno i 130 piksela prema dolje, te je prijavljen s pouzdanošću 0.71.
Vanjska lista pokazuje dva preživjela okvira za klasu 0, ništa za klasu 1, jedan za klasu 2. Prazna lista za klasu 1 ostaje na mjestu kako bi se vanjski indeks uvijek podudarao s indeksom klase. Za isporučeni detektor osoba vanjska lista ima jedan element čija unutarnja lista sadrži preživjele okvire osoba. Za 80-klasni model ima 80 unutarnjih listi, većinom praznih u bilo kojoj sličici, s nepraznim unosima koji drže okvire za klase koje su okinule. Aplikacija čita rezultat s enumerate(boxes) kako bi prošla kroz indekse klasa zajedno s listama okvira – isti oblik koji naknadni procesori za detekciju ciljaju kroz cijeli katalog.