一、網路結構概述
1、Yolov5s是一種基於Deep CNN的目標檢測演算法,網路結構採用輕量化實現,旨在提高檢測速度和準確率。
2、其網路結構可以分為主幹網路和檢測頭兩部分,主幹網路利用CSP Darknet53,檢測頭部分則包括多個卷積層和檢測層,其中檢測層主要實現目標檢測的過程。
3、相較於其他目標檢測演算法,Yolov5s在推理階段不需要藉助Anchor,從而降低了複雜度,提高了速度。
二、主幹網路
1、CSP Darknet53作為Yolov5s的主幹網路,由極深的CNN網路和空間扭曲層(CSP:Cross Stage Partial connection)組成,設計的主要目的是有效地減少CNN層的計算複雜度和過擬合的問題。
2、空間扭曲層採用預測殘差的方式,把輸入特徵圖拆分成兩個部分,其中一部分再通過一系列卷積、BN、激活等層的處理後作為輸出,另一部分通過卷積層處理後再與輸出特徵圖融合。這種處理方式既可以提升特徵的抽象能力,也可以減少訓練參數和計算複雜度。
3、整個主幹網路採用預訓練的方式進行優化,使用了ImageNet數據集,Master Training Set(MTS)和Target Training Set(TTS)。
class CSPDarknet(nn.Module):
# CSPDarknet結構定義
def __init__(self, depths, wid_mult=1.0):
super(CSPDarknet, self).__init__()
# 定義三個不同深度的卷積層
depths = [int(x * wid_mult) for x in depths]
self.base = nn.ModuleList([
# 初始化一個ConvBNLeakyReLU類,輸入通道數為3,輸出通道數為32,卷積核大小為3,步長為1
ConvBNLeakyReLU(3, depths[0], 3, 1),
# 注意CSPBlock裡面也有兩個卷積層,不過這裡第一個卷積層不計在depths里
CSPBlock(depths[0], depths[1], n=1),
CSPBlock(depths[1], depths[2], n=2),
CSPBlock(depths[2], depths[2], n=8),
CSPBlock(depths[2], depths[1], n=2, shortcut=False),
])
# 最後再接一個卷積層,通道數為depths[1],輸出通道數為depths[2],卷積核大小為1,步長為1
# 這裡由於不需要經過BN和激活函數,所以可以用nn.Conv2d
self.tip = nn.Conv2d(depths[1], depths[2], 1, 1)
三、檢測頭
1、檢測頭部分包括若干個卷積層和檢測層,其中檢測層主要實現目標檢測的過程。檢測頭中的卷積層主要是進行特徵圖的尺度調整和特徵圖的融合,而檢測層則定義用於預測類別和邊界框的模型。
2、Yolov5的檢測層可以進行三個不同尺度的預測,其輸出由5個信息組成,分別是中心坐標和長寬,以及類別置信度,其中長和寬採取的是先驗框的形式,而檢測結果的置信度則是經過softmax處理後的結果。
3、為了減少FPN結構帶來的計算延遲,Yolov5採用了SPPnet結構,可以通過不同尺度的池化操作得到不同大小的感受野從而提高檢測準確率。
class Detect(nn.Module):
"""
檢測頭,負責將特徵圖傳輸到預測層,實現目標的檢測
"""
def __init__(self, nc, anchors):
super(Detect, self).__init__()
self.anchors = torch.Tensor(anchors)
# 類別的個數,包含背景類別
self.nc = nc
# 利用nn.ModuleList定義多個卷積層,依次是1個卷積層和3個卷積層,其中第2個卷積層採用SPP結構
self.m = nn.ModuleList(nn.Conv2d(x, (self.nc + 5) * len(self.anchors), 1) for x in [512, 1024, 512])
self.export = True
self.half = False
def forward(self, x):
z = []
for i in range(3):
# 通過nn.Conv2d之後,矩陣的形狀為[batch_size, anchor_num*(5+nc), grid_xy, grid_xy]
# 後續還需要在grid_xy這個維度拆分出anchor_num個通道,在不同尺寸的預測結果中調用
# hidden.shape => [batch_size, anchor_num*(5+nc), grid_xy, grid_xy]
# grid => [grid_xy, grid_xy]
# stride => [img_size/grid_xy, img_size/grid_xy]
hidden = self.m[i](x)
grid = hidden.shape[-2:]
stride = self.img_size // grid[-1]
# view(-1, anchor_num, 5+nc, grid_xy, grid_xy)
# permute(0, 1, 3, 4, 2)
# 這裡是我們在上文提到的拆開hidden中的anchor_num個通道,並調整形狀和次序
hidden = hidden.view(hidden.shape[0], len(self.anchors), 5 + self.nc, grid[0], grid[1]).permute(0, 1, 3, 4, 2).contiguous()
# 對於前兩個數,也就是預測框的中心坐標,我們希望從相對網格坐標中心偏移量預測絕對坐標
# 在 along_width 和 along_height 兩個維度,生成?×?個尺度為 [1, 1, ?, ?]的網格,
# 偏移量先乘26倍作為目標框中心坐標的初始化,與anchor配對後減去自己的偏移量
aa = self.anchors.clone().view(len(self.anchors), 1, 1, 2).repeat(1, grid[0], grid[1], 1).cuda()
# 已經sigmoid求過,所以只需要用exp()還原即可
hidden[..., 0:2] = (hidden[..., 0:2].sigmoid() * 2.0 - 0.5 + aa) * stride
# 對於寬和高,先用exp()還原,再與ancor相乘得到相對於當前網格左上角的絕對距離
hidden[..., 2:4] = (hidden[..., 2:4].sigmoid() * 2) ** 2 * aa * stride
z.append(hidden.view(hidden.shape[0], -1, 5 + self.nc))
return torch.cat(z, 1).detach()
四、模型訓練
1、Yolov5s模型在訓練時,採用GIOU損失函數來衡量預測框和真實框之間的差異,同時採用了焦點損失函數來平衡正負樣本的數量,從而提高模型的泛化能力。
2、模型的訓練過程採用了分步訓練的方法,首先只訓練主幹網路,再訓練檢測頭,最後將整個模型聯合訓練,從而提高模型的收斂速度和準確率。
model = Model(cfg).to(device)
# Step 1: 只訓練主幹網路
optimizer = optim.SGD(model.backbone.parameters(), lr=lr0, momentum=momentum, nesterov=True)
# Step 2: 只訓練檢測頭
optimizer = optim.SGD(model.detect.parameters(), lr=lr0, momentum=momentum, nesterov=True)
# Step 3: 模型聯合訓練
optimizer = optim.SGD(
[{'params': model.detect.parameters()}, {'params': model.backbone.parameters(), 'lr': lr0 * 0.1}],
lr=lr0, momentum=momentum, nesterov=True)
五、模型優化
1、為了提高模型的性能和效果,可以考慮採用數據增強的方式來增加訓練集的大小,從而提高模型的泛化能力。
2、可以考慮採用小批量隨機梯度下降演算法(Min-Batch SGD)來訓練模型,從而加快收斂速度。
3、可以考慮使用反向梯度裁剪(Gradient Clipping)來避免梯度爆炸問題。
def train(data_loader, model, loss_func, optimizer, epoch):
# 模型轉換為train狀態
model.train()
for i, (img, target) in enumerate(data_loader):
# 將數據推送到GPU
img, target = img.to(device), target.to(device)
# 進行預測
loss, _, _ = model(img, target)
# 反向傳播
loss.backward()
# 反向梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0, norm_type=2)
# 更新模型
optimizer.step()
optimizer.zero_grad()
# 列印日誌
if i % 10 == 0:
print(f'Epoch[{epoch}], Step[{i}/{len(data_loader)}], Loss: {loss.item()}')
原創文章,作者:ZNVIS,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/371914.html