YOLOv1 —《You Only Look Once: Unified, Real-Time Object Detection》
論文地址:1506.02640] You Only Look Once: Unified, Real-Time Object Detection (arxiv.org)
代碼地址:pjreddie/darknet: Convolutional Neural Networks (github.com)
1、YOLOv1概述
YOLOv1是一種end to end目標檢測算法,由Joseph Redmon等人于2015年提出。它是一種基于單個神經(jīng)網(wǎng)絡的實時目標檢測算法。
YOLOv1的中文名稱是"你只看一次",這個名字源于算法的工作原理。相比于傳統(tǒng)的目標檢測算法,YOLOv1采用了全新的思路。**它將目標檢測問題轉(zhuǎn)化為一個回歸問題,并將整個圖像作為輸入,一次性地在圖像上進行目標檢測和定位(單階段檢測模型)。這與傳統(tǒng)的滑動窗口或區(qū)域提議方法不同,傳統(tǒng)方法如RCNN系列(兩階段檢測模型)**需要在圖像上進行多次檢測。
YOLOv1的網(wǎng)絡結(jié)構(gòu)由卷積層和全連接層組成,可以將輸入圖像分割成較小的網(wǎng)格(grid cell)。對于每個網(wǎng)格,YOLOv1預測多個邊界框以及每個邊界框中是否包含目標物體及其類別。通過對預測結(jié)果進行置信度評估和非極大值抑制,可以得到最終的目標檢測結(jié)果。
相比于傳統(tǒng)的目標檢測算法,YOLOv1具有較快的檢測速度,可以實現(xiàn)實時目標檢測。然而,由于網(wǎng)絡結(jié)構(gòu)的限制,YOLOv1在小目標檢測和物體定位精度上可能存在一定的問題(主要因為YOLO中的一個gird cell只能預測判別一個物體,后文細說)。在后續(xù)的版本中,如YOLOv2和YOLOv3等,一些改進措施被提出來解決這些問題(比如從Faster Rcnn中引入anchor等),我們在后續(xù)文章中會對YOLO其他版本進行繼續(xù)講解。
我們可以將YOLOv1網(wǎng)絡看作一個黑盒,在推理階段,我們將圖像resize到448 * 448后輸入到網(wǎng)絡中,輸出的是一個SxSx(B*5+C)的張量,該張量是關于檢測框的位置信息以及檢測目標的類別信息等,再經(jīng)過非極大值抑制處理(關于非極大值抑制NMS,如果不理解,可以參考我們以前的內(nèi)容:【目標檢測】 非極大值抑制—NMS_賣報的大地主的博客-CSDN博客),得到最后的檢測結(jié)果(最終的預測框位置信息、類別及其置信度信息)。
2、YOLOv1進行目標檢測的詳細步驟
-
網(wǎng)絡輸入和分割:將一幅待檢測圖像分割分割成S*S個網(wǎng)格,,比如7 * 7, 13 * 13,在原文中S=7。
-
邊界框預測: 每個網(wǎng)格預測B個邊界框(bounding box),在原文中B=2,**每個邊界框包含5個基本參數(shù):位置(x、y坐標)、寬度w、高度h和置信度C。**其中:(x、y)表示邊界框的中心相對于其所屬網(wǎng)格左上角的位置偏移量;寬度w、高度h表示相對于整個圖像的比例;C表示邊界框的置信度,confidence = Pr(object) * IOU (pred, truth), Pr(object)代表邊界框中存在物體的概率,非0即1,IOU (pred, truth)代表預測框與真實標簽框的交并比。
在訓練時,某物體的真實邊界框的中心落在哪個網(wǎng)格上就由哪個網(wǎng)格負責預測該物體,并且在網(wǎng)格所生成的B個邊界框中,與真實邊界框的IOU最大的邊界框負責預測該物體,這一部分在loss函數(shù)中有所體現(xiàn)。每個網(wǎng)格只能預測一個物體(這也是造成YOLOv1對小物體和密集型物體檢測效果差的主要原因)。
-
類別預測: 每個網(wǎng)格還預測一組類別的條件概率Pr(class | object) ,即在當前邊界框已包含物體的先驗條件下該物體為各類別的概率,用于確定邊界框中物體的類別。
在進行推理預測時,每個邊界框的類別預測概率與該邊界框的置信度相乘,得到最終的類別置信度,即Pr(class | object) * Pr(object) * IOU (pred, truth) = Pr(class) * IOU (pred, truth)。根據(jù)同樣的方法可以計算得到7 x 7 x 2 = 98個邊界框的confidence score(當S =7, B =2時),然后根據(jù)confidence score對預測得到的98個邊界框進行非極大值抑制,得到最終的檢測結(jié)果。
-
網(wǎng)絡輸出: 網(wǎng)絡的輸出是一個SxSx(B*5+C)的張量,相當于每個網(wǎng)格都輸出一個形狀為B * 5+C的張量,包含B個邊界框中每個邊界框的位置信息和置信度信息(邊界框的中心點xy坐標和寬w高h以及置信度信息),以及該網(wǎng)格所負責預測物體的類別概率(C個值,所屬C個類別的概率,原文中C等于20,即檢測20個類別)。這些位置預測結(jié)果是相對于輸入圖像的尺度的,因此可以通過將相對尺度乘以圖像的寬度和高度來得到實際的邊界框。
-
置信度評估和非極大值抑制: 對于每個邊界框,根據(jù)其置信度和類別置信度進行綜合評估。通常,可以使用閾值來篩選置信度較高的邊界框,并使用非極大值抑制來消除重疊的邊界框,從而得到最終的目標檢測結(jié)果。
3、YOLOv1的網(wǎng)絡結(jié)構(gòu)及pytorch實現(xiàn)
YOLOv1的網(wǎng)絡結(jié)構(gòu)比較簡單,有24個卷積層以及兩個全連接層組成。
如下給出網(wǎng)絡的PyTorch實現(xiàn):
import torch
import torch.nn as nn
import torch.nn.functional as F
class YOLOv1(nn.Module):
def __init__(self, num_classes, num_boxes):
super(YOLOv1, self).__init__()
self.num_classes = num_classes
self.num_boxes = num_boxes
# 網(wǎng)絡的各層定義
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(64, 192, kernel_size=3, stride=1, padding=1)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv3 = nn.Conv2d(192, 128, kernel_size=1, stride=1, padding=0)
self.conv4 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv5 = nn.Conv2d(256, 256, kernel_size=1, stride=1, padding=0)
self.conv6 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv7 = nn.Conv2d(512, 256, kernel_size=1, stride=1, padding=0)
self.conv8 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.conv9 = nn.Conv2d(512, 256, kernel_size=1, stride=1, padding=0)
self.conv10 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.conv11 = nn.Conv2d(512, 256, kernel_size=1, stride=1, padding=0)
self.conv12 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.conv13 = nn.Conv2d(512, 256, kernel_size=1, stride=1, padding=0)
self.conv14 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.conv15 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0)
self.conv16 = nn.Conv2d(512, 1024, kernel_size=3, stride=1, padding=1)
self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv17 = nn.Conv2d(1024, 512, kernel_size=1, stride=1, padding=0)
self.conv18 = nn.Conv2d(512, 1024, kernel_size=3, stride=1, padding=1)
self.conv19 = nn.Conv2d(1024, 512, kernel_size=1, stride=1, padding=0)
self.conv20 = nn.Conv2d(512, 1024, kernel_size=3, stride=1, padding=1)
self.conv21 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)
self.conv22 = nn.Conv2d(1024, 1024, kernel_size=3, stride=2, padding=1)
self.conv23 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)
self.conv24 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)
self.fc1 = nn.Linear(7 * 7 * 1024, 4096)
self.fc2 = nn.Linear(4096, 7 * 7 * (self.num_classes + 5 * self.num_boxes))
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
x = F.leaky_relu(self.conv1(x), negative_slope=0.1)
x = self.pool1(x)
x = F.leaky_relu(self.conv2(x), negative_slope=0.1))
x = self.pool2(x)
x = F.leaky_relu(self.conv3(x), negative_slope=0.1))
x = F.leaky_relu(self.conv4(x), negative_slope=0.1))
x = F.leaky_relu(self.conv5(x), negative_slope=0.1))
x = self.pool3(x)
x = F.leaky_relu(self.conv6(x), negative_slope=0.1))
x = F.leaky_relu(self.conv7(x), negative_slope=0.1))
x = F.leaky_relu(self.conv8(x), negative_slope=0.1))
x = F.leaky_relu(self.conv9(x), negative_slope=0.1))
x = F.leaky_relu(self.conv10(x), negative_slope=0.1))
x = F.leaky_relu(self.conv11(x), negative_slope=0.1))
x = F.leaky_relu(self.conv12(x), negative_slope=0.1))
x = F.leaky_relu(self.conv13(x), negative_slope=0.1))
x = F.leaky_relu(self.conv14(x), negative_slope=0.1))
x = F.leaky_relu(self.conv15(x), negative_slope=0.1))
x = F.leaky_relu(self.conv16(x), negative_slope=0.1))
x = self.pool4(x)
x = F.leaky_relu(self.conv17(x), negative_slope=0.1))
x = F.leaky_relu(self.conv18(x), negative_slope=0.1))
x = F.leaky_relu(self.conv19(x), negative_slope=0.1))
x = F.leaky_relu(self.conv20(x), negative_slope=0.1))
x = F.leaky_relu(self.conv21(x), negative_slope=0.1))
x = F.leaky_relu(self.conv22(x), negative_slope=0.1))
x = F.leaky_relu(self.conv23(x), negative_slope=0.1))
x = F.leaky_relu(self.conv24(x), negative_slope=0.1))
x = x.view(x.size(0), -1)
x = F.leaky_relu(self.fc1(x), negative_slope=0.1))
x = self.fc2(x)
x = self.softmax(x)
return x
4、YOLOv1的loss函數(shù)及pytorch實現(xiàn)
網(wǎng)絡的Loss部分:
YOLOv1將檢測視為回歸任務,所以選用回歸常用的平方和損失作為loss函數(shù),主要由位置坐標回歸誤差、置信度回歸誤差、類別預測誤差三部分組成。
-
位置坐標誤差只統(tǒng)計負責檢測物體的邊界框的定位誤差,由邊界框的中心坐標定位誤差和寬高定位誤差組成。
-
置信度回歸誤差對負責檢測物體的邊界框和不負責的邊界框都進行懲罰,公式中的置信度預測值為模型正向計算的輸出結(jié)果中的置信度,置信度真實值為confidence = Pr(object) * IOU (pred, truth)。
-
類別預測誤差僅對存在物體的網(wǎng)格(真實邊界框的中心點落在該網(wǎng)格中)進行錯誤懲罰.
Note:
-
平方和loss對各類誤差一視同仁,圖像中不包含物體的網(wǎng)格的邊界框置信度等于0,容易超過包含物體網(wǎng)格的gradient,會導致模型訓練不穩(wěn)定,所以論文中采用了不同的權(quán)重因子,對定位誤差和分類誤差進行權(quán)衡。
λcoord
=5 — 增強邊界框定位誤差;λnoobj
=5 — 削弱不包含物體的邊界框置信度誤差; -
平方和loss對大小邊界框的懲罰力度一致,但我們應該使得大框的小偏差比小框中的偏差對loss訓練優(yōu)化的影響要小,原文中將Loss中的寬高進行了開平方根,而不是直接使用寬高,使得小框的偏差對總體誤差的影響更加敏感。文章來源:http://www.zghlxwxcb.cn/news/detail-479956.html
YOLOv1 Loss的Pytorch實現(xiàn):文章來源地址http://www.zghlxwxcb.cn/news/detail-479956.html
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
class MSEWithLogitsLoss(nn.Module):
def __init__(self, ):
super(MSEWithLogitsLoss, self).__init__()
def forward(self, logits, targets):
inputs = torch.clamp(torch.sigmoid(logits), min=1e-4, max=1.0 - 1e-4)
pos_id = (targets==1.0).float()
neg_id = (targets==0.0).float()
pos_loss = pos_id * (inputs - targets)**2
neg_loss = neg_id * (inputs)**2
loss = 5.0*pos_loss + 1.0*neg_loss
return loss
def generate_dxdywh(gt_label, w, h, s):
xmin, ymin, xmax, ymax = gt_label[:-1]
# 計算邊界框的中心點
c_x = (xmax + xmin) / 2 * w
c_y = (ymax + ymin) / 2 * h
box_w = (xmax - xmin) * w
box_h = (ymax - ymin) * h
if box_w < 1e-4 or box_h < 1e-4:
# print('Not a valid data !!!')
return False
# 計算中心點所在的網(wǎng)格坐標
c_x_s = c_x / s
c_y_s = c_y / s
grid_x = int(c_x_s)
grid_y = int(c_y_s)
# 計算中心點偏移量和寬高的標簽
tx = c_x_s - grid_x
ty = c_y_s - grid_y
tw = np.log(box_w)
th = np.log(box_h)
# 計算邊界框位置參數(shù)的損失權(quán)重
weight = 2.0 - (box_w / w) * (box_h / h)
return grid_x, grid_y, tx, ty, tw, th, weight
def gt_creator(input_size, stride, label_lists=[]):
# 必要的參數(shù)
batch_size = len(label_lists)
w = input_size
h = input_size
ws = w // stride
hs = h // stride
s = stride
gt_tensor = np.zeros([batch_size, hs, ws, 1+1+4+1])
# 制作訓練標簽
for batch_index in range(batch_size):
for gt_label in label_lists[batch_index]:
gt_class = int(gt_label[-1])
result = generate_dxdywh(gt_label, w, h, s)
if result:
grid_x, grid_y, tx, ty, tw, th, weight = result
if grid_x < gt_tensor.shape[2] and grid_y < gt_tensor.shape[1]:
gt_tensor[batch_index, grid_y, grid_x, 0] = 1.0
gt_tensor[batch_index, grid_y, grid_x, 1] = gt_class
gt_tensor[batch_index, grid_y, grid_x, 2:6] = np.array([tx, ty, tw, th])
gt_tensor[batch_index, grid_y, grid_x, 6] = weight
gt_tensor = gt_tensor.reshape(batch_size, -1, 1+1+4+1)
return torch.from_numpy(gt_tensor).float()
def compute_loss(pred_conf, pred_cls, pred_txtytwth, targets):
batch_size = pred_conf.size(0)
# 損失函數(shù)
conf_loss_function = MSEWithLogitsLoss()
cls_loss_function = nn.CrossEntropyLoss(reduction='none')
txty_loss_function = nn.BCEWithLogitsLoss(reduction='none')
twth_loss_function = nn.MSELoss(reduction='none')
# 預測
pred_conf = pred_conf[:, :, 0] # [B, HW,]
pred_cls = pred_cls.permute(0, 2, 1) # [B, C, HW]
pred_txty = pred_txtytwth[:, :, :2] # [B, HW, 2]
pred_twth = pred_txtytwth[:, :, 2:] # [B, HW, 2]
# 標簽
gt_obj = targets[:, :, 0] # [B, HW,]
gt_cls = targets[:, :, 1].long() # [B, HW,]
gt_txty = targets[:, :, 2:4] # [B, HW, 2]
gt_twth = targets[:, :, 4:6] # [B, HW, 2]
gt_box_scale_weight = targets[:, :, 6] # [B, HW,]
batch_size = pred_conf.size(0)
# 置信度損失
conf_loss = conf_loss_function(pred_conf, gt_obj)
conf_loss = conf_loss.sum() / batch_size
# 類別損失
cls_loss = cls_loss_function(pred_cls, gt_cls) * gt_obj
cls_loss = cls_loss.sum() / batch_size
# 邊界框txty的損失
txty_loss = txty_loss_function(pred_txty, gt_txty).sum(-1) * gt_obj * gt_box_scale_weight
txty_loss = txty_loss.sum() / batch_size
# 邊界框twth的損失
twth_loss = twth_loss_function(pred_twth, gt_twth).sum(-1) * gt_obj * gt_box_scale_weight
twth_loss = twth_loss.sum() / batch_size
bbox_loss = txty_loss + twth_loss
# 總的損失
total_loss = conf_loss + cls_loss + bbox_loss
return conf_loss, cls_loss, bbox_loss, total_loss
5、YOLOv1的不足
- 因為YOLO中每個cell只預測兩個邊界框和一個物體,使得對小物體以及密集型物體檢測效果差。
- 此外,不像Faster R-CNN一樣預測offset,YOLO是直接預測邊界框的位置的,這就增加了訓練的難度,并且識別精度弱于Faster R-CNN,但是快。
- YOLO是根據(jù)訓練數(shù)據(jù)來預測邊界框的,但是當測試數(shù)據(jù)中的物體出現(xiàn)了訓練數(shù)據(jù)中的物體沒有的長寬比時,YOLO的泛化能力低。
- 同時經(jīng)過多次下采樣,使得最終得到的feature的分辨率比較低,就是得到深語義的粗特征信息,忽略了細語義特征,造成定位精度下降。
- 損失函數(shù)的設計存在缺陷,使得物體的定位誤差有點兒大,尤其在不同尺寸大小的物體的處理上還有待加強。
6、其他
另外,YOLOv1中的訓練過程中還是用到了Warm up、Dropout、數(shù)據(jù)增強(縮放、平移、HSV變換)等技巧,這些我們會在以后關于模型訓練技巧的篇章中進行講解。
參考鏈接:
- 【精讀AI論文】YOLO V1目標檢測,看我就夠了_嗶哩嗶哩_bilibili
- 【目標檢測論文閱讀】YOLOv1 - 知乎 (zhihu.com)
- yjh0410/PyTorch_YOLOv1: A new version of YOLOv1 (github.com)
到了這里,關于【目標檢測——YOLO系列】YOLOv1 —《You Only Look Once: Unified, Real-Time Object Detection》的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!