7.14. คำอธิบาย YOLOv8 ทีละขั้นตอน

ตัวประมวลผลหลัง YOLOv8 มีขนาดเล็กพอที่จะอธิบายตั้งแต่เทนเซอร์อินพุตจนถึงรายการที่ส่งคืน การอ่านครั้งเดียวแสดงให้เห็นว่าตัวประมวลผลหลังอื่น ๆ ทุกตัวในแคตตาล็อกกำลังทำอะไร ได้แก่ กำหนดค่าขีดแบ่งคะแนน quantized อย่างถูกราคา เลือกสิ่งที่รอดพ้น dequantize ถอดรหัสรูปทรง ผลักไปยัง NMS ส่งคืนรายการต่อคลาส

7.14.1. เทนเซอร์เริ่มต้น

โมเดล YOLOv8 ส่งออกเทนเซอร์ขาออกเดี่ยวที่มีรูปร่าง (1, C, A) -- หนึ่งเฟรม ช่อง C การทำนาย anchor A รายการ สี่ช่องแรกเป็นรูปทรงกรอบ -- cx, cy, w, h -- ทำให้เป็นมาตรฐานใน [0, 1] ของมิติอินพุตของโครงข่าย ช่อง C - 4 ที่เหลือเป็นคะแนนต่อคลาส อยู่ใน [0, 1] แล้วสำหรับโมเดลที่ฝึกแล้ว แต่ละ anchor คือคอลัมน์ตามช่องต่าง ๆ

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.

anchor YOLOv8 หนึ่งตัวคือหนึ่งคอลัมน์ตามช่องต่าง ๆ ประกอบด้วยตัวเลขกรอบสี่ตัวและคะแนนคลาส N รายการ

ไฟล์ yolov8n_192.tflite ที่ส่งมาเป็นตัวตรวจจับบุคคลคลาสเดียว ดังนั้น C = 5 และ A อยู่ในระดับพัน โมเดลแบบกำหนดเองที่ฝึกกับชุด COCO 80 คลาสเต็มมี C = 84 กับ A เดียวกัน การถอดรหัสด้านล่างใช้ได้กับจำนวนคลาสใด ๆ

7.14.2. การกำหนดค่าขีดแบ่งก่อน dequantize

ขั้นตอนที่ถูกราคาก่อน เทนเซอร์ขาออกอยู่ใน dtype integer แบบ quantized ของโมเดล และการ dequantize ทุกค่าจะสัมผัสทุกองค์ประกอบของเทนเซอร์ที่มีหลายพันรายการ -- ซึ่งส่วนใหญ่อยู่ต่ำกว่าค่าขีดแบ่งคะแนนและถูกทิ้งท้ายที่สุด ตัวประมวลผลหลังจะ quantize ค่าขีดแบ่งครั้งเดียวแทนและเปรียบเทียบในพื้นที่ quantized ดิบ:

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 คือเทนเซอร์ที่ปรับรูปร่างเป็น (C, A) เพื่อให้ช่องเป็นแถวและ anchor เป็นคอลัมน์ score_block คือเทนเซอร์ย่อยคะแนนคลาส -- ทุกอย่างตั้งแต่แถว 4 ลงมา ml.utils.threshold() ลดบล็อกนั้นตามแกน 0 (find_max=True, find_max_axis=0) เป็นคะแนนสูงสุดต่อ anchor จากนั้นส่งคืนดัชนีของ anchor ที่ค่าสูงสุดผ่านค่าขีดแบ่ง quantized ทั้งเทนเซอร์ไม่เคยถูก dequantize มีเพียงการลด max ต่อคอลัมน์ในพื้นที่ integer quantized เท่านั้น

หากไม่มี anchor ผ่าน ตัวประมวลผลหลังจะส่งคืน tuple ว่างที่ predict() ตีความว่าไม่มีการตรวจจับ

การตัดสินใจสองอย่างในโค้ดนี้ทำให้ความแตกต่างระหว่างตัวประมวลผลหลังที่รันได้กับที่ช้าเกินใช้งานได้ อย่างแรกคือการขับเคลื่อนการคำนวณผ่าน numpy เทนเซอร์ขาออกมีหลายพันองค์ประกอบ และการวนซ้ำใน Python ดิบใช้เวลาหลายวินาทีต่อการอนุมาน ในขณะที่การคำนวณแบบเวกเตอร์เดียวกันผ่าน numpy ทำงานในมิลลิวินาที อย่างที่สองคือการ dequantize หลัง ตัวกรองค่าขีดแบ่งแทนที่จะก่อน การ dequantize ก่อนจะจัดสรรเทนเซอร์ float ขนาดสี่เท่าของเทนเซอร์ quantized และวนซ้ำทุกองค์ประกอบก่อนทิ้งเกือบทั้งหมด การ dequantize เฉพาะคอลัมน์ที่รอดพ้นจะสัมผัสค่าเพียงไม่กี่ค่าเท่านั้น และประหยัดทั้งเวลาและ RAM ที่การแปลงทั้งหมดจะใช้ไป

7.14.3. การ dequantize ผู้รอดพ้น

เฉพาะ anchor ที่รอดพ้นเท่านั้นที่ต้องถอดรหัสรูปทรง numpy.take() ดึงคอลัมน์เหล่านั้นออกมาและการเรียก ml.utils.dequantize() ครั้งเดียวแปลงพวกมันเป็น float:

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

bb ตอนนี้มีรูปร่างเป็น (C, K) โดยที่ K คือจำนวน anchor ที่รอดพ้น -- โดยทั่วไปเพียงไม่กี่รายการแม้ว่า A จะอยู่ในระดับพัน

7.14.4. การอ่านรูปทรง

สี่ช่องกรอบและคะแนนต่อคลาสถูกดึงออกมาโดยตรง:

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 คือคะแนนคลาสที่ดีที่สุดต่อ anchor ที่รอดพ้น bb_classes คือดัชนีคลาสที่ได้คะแนนนั้น รูปทรงกรอบยังอยู่ใน [0, 1] ที่ทำให้เป็นมาตรฐานของมิติอินพุตของโครงข่าย ดังนั้นขั้นตอนถัดไปจะปรับขนาดเป็นพิกเซล:

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

หลังจากนี้กรอบจะอยู่ในพื้นที่พิกเซลอินพุตของโครงข่าย -- พื้นที่พิกัดที่ NMS คาดหวังเป็นอินพุต

7.14.5. Non-max suppression

ผู้รอดพ้นผ่าน NMS และส่งคืนเป็นรายการต่อคลาส:

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 อ่าน inputs[0].roi เพื่อให้กรอบที่ส่งคืนอยู่ในพื้นที่พิกัดของภาพต้นฉบับ ไม่ใช่ของโครงข่าย -- แอปพลิเคชันวาดพวกมันลงบนเฟรมที่ถ่ายมาโดยตรงโดยไม่ต้องแมปเพิ่มเติม

7.14.6. สิ่งที่สคริปต์ได้รับกลับคืน

ค่าที่ส่งคืนเป็นรายการของรายการต่อคลาสที่มีดัชนีตามคลาส ตัวอย่างสามคลาสอาจมีลักษณะดังนี้:

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

แต่ละรายการเป็น tuple ((x, y, w, h), score) โดยที่ (x, y) คือมุมซ้ายบนของกรอบล้อมรอบในพิกัดพิกเซลของภาพต้นฉบับ w และ h คือความกว้างและความสูงในหน่วยพิกเซล และ score คือความมั่นใจที่โครงข่ายกำหนดให้กับการตรวจจับ ดังนั้น ((180, 60, 88, 130), 0.71) หมายความว่ากรอบที่มีมุมซ้ายบนอยู่ที่พิกเซล (180, 60) ขยาย 88 พิกเซลไปทางขวาและ 130 พิกเซลลงล่าง และรายงานด้วยความมั่นใจ 0.71

รายการชั้นนอกแสดงกรอบที่รอดพ้นสองกรอบสำหรับคลาส 0 ไม่มีสำหรับคลาส 1 หนึ่งกรอบสำหรับคลาส 2 รายการว่างสำหรับคลาส 1 ถูกเก็บไว้ในตำแหน่งเพื่อให้ดัชนีชั้นนอกตรงกับดัชนีคลาสเสมอ สำหรับตัวตรวจจับบุคคลที่ส่งมา รายการชั้นนอกมีองค์ประกอบเดียวที่รายการชั้นในมีกรอบบุคคลที่รอดพ้น สำหรับโมเดล 80 คลาส มี 80 รายการชั้นใน ส่วนใหญ่ว่างในแต่ละเฟรม โดยรายการที่ไม่ว่างมีกรอบสำหรับคลาสที่ตรวจพบ แอปพลิเคชันอ่านผลลัพธ์ด้วย enumerate(boxes) เพื่อวนซ้ำดัชนีคลาสพร้อมกับรายการกรอบ -- รูปร่างเดียวกับที่ตัวประมวลผลหลังการตรวจจับมุ่งหมายทั่วทั้งแคตตาล็อก