7.14. Hướng dẫn chi tiết YOLOv8

Bộ xử lý hậu kỳ YOLOv8 đủ nhỏ để đi qua từng bước từ tensor đầu vào đến danh sách được trả về. Đọc nó một lần cho thấy những gì mọi bộ xử lý hậu kỳ khác trong danh mục đang làm: áp dụng ngưỡng điểm số lượng tử hóa một cách hiệu quả, lấy những gì sống sót, lượng tử hóa ngược, giải mã hình học, đẩy vào NMS, trả về danh sách theo lớp.

7.14.1. Tensor khởi đầu

Một mô hình YOLOv8 phát ra một tensor đầu ra duy nhất có hình dạng (1, C, A) -- một khung hình, C kênh, A dự đoán anchor. Bốn kênh đầu tiên là hình học hộp -- cx, cy, w, h -- được chuẩn hóa về [0, 1] của kích thước đầu vào mạng. C - 4 kênh còn lại là điểm số theo lớp, đã trong [0, 1] cho một mô hình đã huấn luyện. Mỗi anchor là một cột theo các kênh.

A grid laid on its side: rows labelled cx, cy, w, h, score_0, score_1, ..., score_(N-1); columns labelled anchor 0 through anchor A-1. One column is outlined to show that a single anchor's prediction is one column down the channels.

Một anchor YOLOv8 là một cột theo các kênh: bốn số hộp và N điểm số lớp.

File yolov8n_192.tflite đi kèm là bộ phát hiện người đơn lớp, vì vậy C = 5A ở mức hàng nghìn; một mô hình tùy chỉnh được huấn luyện trên toàn bộ tập 80 lớp COCO có C = 84 với cùng A. Quá trình giải mã dưới đây áp dụng cho bất kỳ số lớp nào.

7.14.2. Áp dụng ngưỡng trước khi lượng tử hóa ngược

Bước rẻ nhất trước. Tensor đầu ra ở dtype số nguyên lượng tử hóa của mô hình, và việc lượng tử hóa ngược mỗi giá trị sẽ phải xử lý mọi phần tử của một tensor với hàng nghìn mục -- hầu hết đều dưới ngưỡng điểm số và cuối cùng bị loại bỏ. Thay vào đó bộ xử lý hậu kỳ lượng tử hóa ngưỡng một lần và so sánh trong không gian lượng tử hóa thô:

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 là tensor được định hình lại thành (C, A) để các kênh là các hàng và các anchor là các cột. score_block là tensor con điểm số lớp -- mọi thứ từ hàng 4 trở xuống. ml.utils.threshold() giảm khối đó theo trục 0 (find_max=True, find_max_axis=0) thành điểm số tối đa theo anchor, sau đó trả về các chỉ mục của các anchor có điểm số tối đa vượt qua ngưỡng lượng tử hóa. Toàn bộ tensor chưa bao giờ được lượng tử hóa ngược; chỉ có quá trình giảm max theo cột trong không gian số nguyên lượng tử hóa.

Nếu không có anchor nào vượt qua, bộ xử lý hậu kỳ trả về tuple rỗng mà predict() diễn giải là không có phát hiện.

Hai quyết định trong đoạn code này tạo ra sự khác biệt giữa một bộ xử lý hậu kỳ có thể chạy được và một bộ chậm đến mức không thể dùng được. Điều đầu tiên là điều khiển số học thông qua numpy: tensor đầu ra có hàng nghìn phần tử, và việc lặp lại nó trong Python thuần sẽ mất cả giây mỗi lần suy luận, trong khi cùng số học được vector hóa thông qua numpy chạy trong mili giây. Điều thứ hai là lượng tử hóa ngược sau bộ lọc ngưỡng thay vì trước. Lượng tử hóa ngược trước sẽ cấp phát một tensor float gấp bốn lần kích thước tensor lượng tử hóa và duyệt qua mọi phần tử trước khi loại bỏ gần như tất cả chúng; lượng tử hóa ngược chỉ các cột sống sót chỉ xử lý một số ít giá trị và tiết kiệm cả thời gian lẫn RAM mà việc chuyển đổi đầy đủ đáng lẽ đã tiêu tốn.

7.14.3. Lượng tử hóa ngược các ứng viên sống sót

Chỉ các anchor sống sót mới cần giải mã hình học của chúng. numpy.take() lấy ra các cột đó và một lần gọi ml.utils.dequantize() chuyển đổi chúng sang float:

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

bb hiện có dạng (C, K) trong đó K là số anchor sống sót -- thường chỉ một số ít ngay cả khi A ở mức hàng nghìn.

7.14.4. Đọc hình học

Bốn kênh hộp và điểm số theo lớp được lấy ra trực tiếp:

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 là điểm số lớp tốt nhất cho mỗi anchor sống sót; bb_classes là chỉ mục lớp đã tạo ra điểm số đó. Hình học hộp vẫn ở dạng [0, 1] được chuẩn hóa của kích thước đầu vào mạng, vì vậy bước tiếp theo co giãn nó thành điểm ảnh:

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

Sau bước này, các hộp ở trong không gian điểm ảnh đầu vào mạng -- không gian tọa độ mà NMS mong đợi ở đầu vào.

7.14.5. Triệt tiêu non-max

Các ứng viên sống sót đi qua NMS và được trả về dưới dạng danh sách theo lớp:

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 đọc inputs[0].roi vì vậy các hộp được trả về ở trong không gian tọa độ của ảnh gốc, chứ không phải của mạng -- ứng dụng vẽ chúng trực tiếp lên khung hình chụp mà không cần ánh xạ lại thêm.

7.14.6. Những gì tập lệnh nhận được

Giá trị trả về là danh sách các danh sách theo lớp được lập chỉ mục theo lớp. Một ví dụ ba lớp có thể trông như thế này:

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

Mỗi mục là một tuple ((x, y, w, h), score): (x, y) là góc trên bên trái của hộp giới hạn trong tọa độ điểm ảnh của ảnh gốc, wh là chiều rộng và chiều cao tính bằng điểm ảnh, và score là độ tin cậy mà mạng gán cho phát hiện. Vì vậy ((180, 60, 88, 130), 0.71) có nghĩa là một hộp có góc trên bên trái tại điểm ảnh (180, 60), kéo dài 88 điểm ảnh sang phải và 130 điểm ảnh xuống dưới, và được báo cáo với độ tin cậy 0.71.

Danh sách ngoài cùng cho thấy hai hộp sống sót cho lớp 0, không có gì cho lớp 1, một hộp cho lớp 2. Danh sách rỗng cho lớp 1 được giữ nguyên vị trí để chỉ mục ngoài cùng luôn khớp với chỉ mục lớp. Đối với bộ phát hiện người đi kèm, danh sách ngoài cùng có một phần tử duy nhất mà danh sách bên trong chứa các hộp người sống sót. Đối với mô hình 80 lớp, nó có 80 danh sách bên trong, hầu hết rỗng ở bất kỳ khung hình nào, với các mục không rỗng chứa các hộp cho các lớp đã kích hoạt. Ứng dụng đọc kết quả với enumerate(boxes) để duyệt qua các chỉ mục lớp cùng với các danh sách hộp -- cùng hình dạng mà các bộ xử lý hậu kỳ phát hiện hướng tới trong toàn bộ danh mục.