測試 AI 產品實戰系列:目標檢測模型的測試

孙高飞發表於2025-03-12

什麼是目標檢測

用一張圖來表達:

這是我週末帶老婆孩子去體育場玩時拍下來的照片。 我使用這張照片輸入到模型中,希望模型可以識別出圖片中的人類並畫出人類所在位置的長方形的框。 而這就是目標檢測.

目標檢測的流程。

本質上目標檢測是一個分類 + 迴歸的複合問題。 這裡再解釋一下什麼是分類和迴歸。 在人工智慧的監督學習類目中,基本上所有模型都逃不開二分類,多分類和迴歸這 3 種型別。

  • 二分類:比如一張圖片,告訴使用者這個圖片中有沒有人類。回答要麼是有,要麼是沒有。 只有兩個答案,所以叫二分類。 在後臺,模型其實並不會直接告訴分類結果,而是輸出這張圖片有人類的機率是多少。 使用者需要自己設定一個機率閾值來進行判定。 比如使用者設定的閾值是 0.8. 那麼只要模型輸出的機率大於 0.8 就會被分類成有人類的結果。
  • 多分類:多分類對比二分類其實就是有多個分類,用的演算法也都一樣,只不過演算法最後用的啟用函式從 sigmod 換成了 softmax。 在多分類裡,模型會輸出每個分類的機率,比如圖片裡有人的機率是 0.5,有貓的機率是 0.3,有狗的機率是 0.2 。 同樣由使用者自己設定閾值來進行判定。
  • 迴歸:迴歸與分類不同, 模型輸出的是一個具體數值,比如模型預測房價,股價,兩地之間的通勤時間等等。 而在我們的目標檢測場景中, 迴歸主要用來識別目標物體(比如人類)的座標。

所以在目標檢測中,模型實際上會透過一些演算法把圖片分割成很多個小塊, 然後分別對這些塊進行分類判斷(區域中含有目標的機率)以及識別這個目標的座標地址(中心點座標,長寬。或者直接 4 個點的座標)。

如何評估目標檢測的效果

針對一個圖片,我們需要事先進行標註,比如識別人類,貓,狗這 3 個目標的場景中,圖片中每個目標的類別和座標都要事先標註好。 這樣才能跟模型輸出的結果進行對比,以此來評估模型識別的準確程度。

AI 模型的效果測試,都是基於統計學的,輸入大量事先已經標註好答案的資料,與模型輸出的資料進行對比,從而在大量的資料下評估模型的準確程度

IOU

IOU 評估的是圖片中目標預期的座標和模型識別出的座標的重疊程度,以此來判斷模型識別的座標是否準確。 它的評估程式碼如下:

# IoU是目標檢測中常用的一種重疊度量,用於衡量檢測框和真實標註框之間的重疊程度。IoU值越大,表示檢測結果越準確。它的達標標準取決於具體的應用場景和需求。
# 在目標檢測任務中,常用的IoU閾值為0.5或0.7,即當檢測框與真實標註框的IoU值大於等於0.5或0.7時,認為檢測結果正確。在一些特定的應用場景中,IoU閾值可能會更高或更低
# 需要注意的是,IoU值並不是唯一的評估指標,還需要考慮精度、召回率、F1值、AP等指標。在實際應用中,需要根據具體的需求選擇合適的評估指標和閾值。

def compute_iou(box1, box2):
    # 計算兩個矩形框的面積
    area1 = (box1[2] - box1[0] + 1) * (box1[3] - box1[1] + 1)
    area2 = (box2[2] - box2[0] + 1) * (box2[3] - box2[1] + 1)

    # 計算交集的座標範圍
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])

    # 計算交集的面積
    inter_area = max(0, x2 - x1 + 1) * max(0, y2 - y1 + 1)

    # 計算並集的面積
    union_area = area1 + area2 - inter_area

    # 計算IoU值
    iou = inter_area / union_area

    return iou


box1 = [50, 50, 150, 150]  # 左上角座標為(50, 50),右下角座標為(150, 150)
box2 = [100, 100, 200, 200]  # 左上角座標為(100, 100),右下角座標為(200, 200)

# 計算兩個矩形框之間的IoU值
iou = compute_iou(box1, box2)

# 輸出結果
print('IoU:', iou)

一些細節我們都寫在了上面程式碼中的註釋裡。

召回/精準

召回和精準是分類模型中最主要的評估指標,在之前的文章中已經詳細介紹過,這裡我再簡單介紹一下。

舉一個例子,假設我們有一個預測癌症的場景,健康的人有 99 個 (y=0),得癌症的病人有 1 個 (y=1)。我們用一個特別糟糕的模型,永遠都輸出 y=0,就是讓所有的病人都是健康的。這個時候我們的 “準確率” accuracy=99%,判斷對了 99 個,判斷錯了 1 個,但是很明顯地這個模型相當糟糕,在很多模型評估場景中,準確率是不足以表達模型真是的效果的,這是因為在真實世界中,正負樣本的比例本就是十分懸殊的。因此需要一種很好的評測方法,來把這些 “作弊的” 模型給揪出來。

  • 精準率:precision:所有被查出來得了癌症的人中,有多少個是真的癌症病人。公式是 TP/TP+FP
  • 召回率:recall:所有得了癌症的病人中,有多少個被查出來得癌症。公式是:TP/TP+FN。 意思是真正類在所有正樣本中的比率,也就是真正類率 (TPR)。

召回和精準理解起來可能比較繞,我多解釋一下,我們說要統計召回率,因為我們要知道所有得了癌症中的人中,我們預測出來多少。因為預測癌症是我們這個模型的主要目的, 我們希望的是所有得了癌症的人都被查出來。不能說得了癌症的我預測說是健康的,這樣耽誤人家的病情是不行的。 但同時我們也要統計精準率, 為什麼呢, 假如我們為了追求召回率,我又輸入一個特別糟糕的模型,永遠判斷你是得了癌症的,這樣真正得了癌症的患者肯定不會漏掉了。但明顯這也是不行的對吧, 人家明明是健康的你硬說人家得了癌症,結果人家回去悲憤欲絕,生無可戀,自殺了。或者回去以後散盡家財,出家為僧。結果你後來跟人說我們誤診了, 那人家砍死你的心都有。 所以在統計召回的同時我們也要加入精準率, 計算所有被查出來得了癌症的人中,有多少是真的癌症病人。 說到這大家可能已經看出來召回和精準在某稱程度下是互斥的, 因為他們追求的是幾乎相反的目標。 有些時候召回高了,精準就會低。精準高了召回會變低。 所以這時候就要根據我們的業務重心來選擇到底選擇召回高的模型還是精準高的模型。 有些業務比較看重召回,有些業務比較看重精準。 當然也有兩樣都很看重的業務,就例如我們說的這個預測癌症的例子。或者說銀行的反欺詐場景。 反欺詐追求高召回率,不能讓真正的欺詐場景漏過去,在一定程度上也注重精準率,不能隨便三天兩頭的判斷錯誤把使用者的卡給凍結了對吧,來這麼幾次使用者就該換銀行了。

而上面計算指標的公式中 TP/FP 這些則是混淆矩陣的概念。 那什麼是混淆矩陣呢。

混淆矩陣是一個用於描述分類模型效能的矩陣,它顯示了模型對於每個類別的預測結果與實際結果的對比情況。

以分類模型中最簡單的二分類為例,對於這種問題,我們的模型最終需要判斷樣本的結果是 0 還是 1,或者說是 positive 還是 negative。

我們透過樣本的採集,能夠直接知道真實情況下,哪些資料結果是 positive,哪些結果是 negative。同時,我們透過用樣本資料跑出分型別模型的結果,也可以知道模型認為這些資料哪些是 positive,哪些是 negative。

因此,我們就能得到這樣四個基礎指標,我稱他們是一級指標(最底層的):


真實值是positive,模型認為是positive的數量(True Positive=TP)
真實值是positive,模型認為是negative的數量(False Negative=FN)
真實值是negative,模型認為是positive的數量(False Positive=FP)
真實值是negative,模型認為是negative的數量(True Negative=TN)

將這四個指標一起呈現在表格中,就能得到一個矩陣,我們稱它為混淆矩陣(Confusion Matrix)

一個統計召回和精準的測試指令碼

下面我使用 yolo 模型來做一個實戰演示,yolo 是目標檢測領域最出名的一個開源模型之一。使用者可以在網路上隨意下載對應的模型使用。 實際上我也用這個模型做計算機是視覺的資料探勘工作(這個後面再講。)

yolo 是一個 80 分類的模型,其中 class_id 為 0 的是人類。 我就用這個模型來寫一個測試 yolo 識別人類的實戰測試指令碼。 這裡我主要統計召回和精準。 具體的實現細節都寫在了程式碼註釋中。

PS:下面程式碼中最後註釋的部分是用開源的庫來計算混淆矩陣,召回和精準。

import cv2
import numpy as np

# yolov3下載地址:
# 網路結構檔案:https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg
# 模型(權重)檔案:https://pjreddie.com/media/files/yolov3.weights
# 80個類別標籤的文字檔案:https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names

net = cv2.dnn.readNet('yolov3.weights', 'yolov3.cfg')

classes = []
datas = []
# 讀取標註檔案
with open('static/pic_cls/labels.txt', 'r') as f:
    for line in f:
        datas.append(list(line.strip('\n').split(' ')))
print(datas)

# 定義混淆矩陣
TP = 0
FN = 0
FP = 0
TN = 0

# 遍歷資料,把資料輸給模型進行識別,並統計混淆矩陣
for image in datas:
    path = image[0]
    label_y = int(image[1])
    frame = cv2.imread(path)
    height, width, _ = frame.shape

    # 構建輸入影像
    blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False)

    # 設定輸入層和輸出層
    net.setInput(blob)
    output_layers = net.getUnconnectedOutLayersNames()

    # 前向傳播,模型推理
    outputs = net.forward(output_layers)

    # 解析輸出
    boxes = []
    confidences = []
    class_ids = []
    for output in outputs:
        for detection in output:
            scores = detection[5:]
            class_id = np.argmax(scores)
            confidence = scores[class_id]
            if confidence > 0.5 and class_id == 0:  # 只檢測人類,在yolo的定義中class_id為0的是人類目標(yolo是80分類的模型). 閾值設定為0.5,只有大於0.5的才認為是有人類的。
                # 計算每個目標的座標, 後面可以用這些座標做IOU指標的計算
                center_x = int(detection[0] * width)
                center_y = int(detection[1] * height)
                w = int(detection[2] * width)
                h = int(detection[3] * height)
                x = int(center_x - w / 2)
                y = int(center_y - h / 2)
                boxes.append([x, y, w, h])
                confidences.append(float(confidence))
                class_ids.append(class_id)

    # 非極大值抑制
    indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)

    y = 0
    # # 判斷是否存在人類
    if len(boxes) > 0:
        y = 1
        print('There is a human in the image.')
    else:
        y = 0
        print('There is no human in the image.')
    if label_y == 1 and y == 1:
        TP += 1
    elif label_y == 1 and y == 0:
        FN += 1
    elif label_y == 0 and y == 1:
        FP += 1
    elif label_y == 0 and y == 0:
        TN += 1

recall = TP / (TP + FN)
precision = TP / (TP + FP)

print("recall: " + str(recall))
print("precision: " + str(precision))



#
# from sklearn.metrics import confusion_matrix
# import pandas as pd
#
# # 建立一個字典,其中包含'label'和'answer'的鍵,以及相應的資料
# data = {
#     'label': ['fig_other', 'fig_data', 'fig_mind', 'fig_proc','fig_other', 'fig_data', 'fig_mind', 'fig_proc', 'fig_other'],  # 示例標籤
#     'answer': ['fig_data', 'fig_data', 'fig_other', 'fig_proc','fig_proc', 'fig_proc', 'fig_mind', 'fig_proc', 'fig_other']         # 示例答案
# }
#
# # 使用字典建立DataFrame
# df = pd.DataFrame(data)
#
#
# # 計算混淆矩陣
# cm = confusion_matrix(df['label'], df['answer'],
#                               labels=['fig_other', 'fig_data', 'fig_mind', 'fig_proc'])
#
# # 定義分類標籤
# labels = ['fig_other', 'fig_data', 'fig_mind', 'fig_proc']
#
# # 計算每個分類的召回率和精確率
# recall = {}
# precision = {}
# for i, label in enumerate(labels):
#             recall[label] = round(cm[i, i] / cm[:, i].sum(), 4)
#             precision[label] = round(cm[i, i] / cm[i, :].sum(), 4)
#
# # 找出每個分類的badcase
# badcases = {}
# for i, label in enumerate(labels):
#     badcases[label] = df[(df['label'] == label) & (df['answer'] != label)]
#
# print(recall)
# print(precision)
# print('下面是badcase')
# for key, value in badcases.items():
#     print(key)
#     print(value)



歡迎加入我的星球

最後再推薦一下自己的星球,裡面有大量的 AI 場景的測試資料和教程,我也會在星球中定期直播:

如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章