手把手教你如何自制目標檢測框架(從理論到實現)
theme: channing-cyan highlight: agate
攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第1天,點選檢視活動詳情 >>
前言
好久沒有冒泡了,是時候來波大的了,也是由於特殊需求,不得不重啟關於目標檢測的一些內容。既然如此,那麼剛好把以前要做的yolo目標檢測相關的程式碼進行復現,並且好好把這個目標檢測說清楚一點兒。
此外本文基於Pytorch進行編寫,有空後期tensorflow也可以試試。
在閱讀這篇博文之前,如果讀者真的是還沒有接觸過這個目標檢測的話,我建議可以先看看這幾篇文章再來:
在程式碼部分還參考了原先這篇博文的設計: 嘿~全流程帶你基於Pytorch手擼圖片分類“框架“--HuClassify
那麼本文兩個目標:
一. 理論
- 搞清楚什麼是目標檢測
- 目標檢測的重難點
- 相關目標檢測演算法思想
- 如何設計一個目標檢測演算法
二. 編碼
- voc資料集的細節
- 目標檢測網路
- 目標分類網路
- 相關演算法
其中的理論部分像我說不會太深入只是快速入門,編碼部分的話倒是有很多相關演算法的實現。那麼編碼的話在目標檢測部分的網路,我們也是直接使用yolo的網路,當然這裡還是會做改動的。這篇博文的更多的一個目的其實還是說搭建一個簡單的目標檢測平臺,這樣感興趣的朋友可以自己DIY,對我本人的話也是有DIY的需求。
那麼廢話不多說,馬上發車了!
目標檢測
要說到目標檢測的話,那麼我們就不得不先說到圖片分類了。 因為圖片分類在我們的目標檢測當中是非常重要的但是二者的區別也是存在的,不過他們之間卻有很多相似的地方。
圖片分類
圖片分類是一個非常經典的問題,給定一張圖片然後對這個圖片進行分類,它的任務非常簡單,並且設計一個這樣的網路也非常簡單。 你只需要使用一定量的卷積,最後和一定量的全連線網路輸出一組大小和類別的最後一個維度一樣的tensor就行了,然後使用交叉熵作為你的損失函式。
比如最簡單的分類網路:LeNet
```python
class LeNet(nn.Module): def init(self,classes): super().init()
self.feature = Sequential(
nn.Conv2d(3,6,kernel_size=5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(6,16,5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2)
)
self.classifiar = nn.Sequential(
nn.Linear(16*5*5,120), # B
nn.ReLU(),
nn.Linear(120,84),
nn.ReLU(),
nn.Linear(84,classes)
)
def forward(self,x):
x = self.feature(x)
x = x.view(x.size()[0],-1)
x = self.classifiar(x)
return x
def initialize_weights(self):
#引數初始化,隨便給點權重,這樣的話會加快一點速度(訓練)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.xavier_normal_(m.weight.data)
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data, 0, 0.1)
m.bias.data.zero_()
```
我們只需要輸入一張圖片就闊以得到這張圖片的類別。
但是這還是遠遠不夠的。
目標檢測
目標檢測則是在圖片分類的基礎上,我們還需要知道我們對應的一個物體的位置,比如下面這張圖:
我們知道這是一隻貓,但是在圖片場景當中並不是只有一隻貓,貓只是圖片當中的一個很明顯的特徵,如果做圖片分類的話,你說這個是貓可以,但是我說這是個草貌似也可以。所以現在的任務是我不僅僅要知道這個圖片有貓,我還要知道這個貓在圖片的位置。
單目標檢測
現在我們假設,我們的圖片只有一個物體,就如上面的圖片一樣。那麼如果我們需要想辦法讓神經網路得到這樣一個框的,當然在此基礎上,我們還需要得到對應的概率,也就是,如果圖片只有一個目標的話,我們只需要在原來的基礎上想辦法多生成一組對應的框的座標就可以了。也就是說,我們以上面的LeNet網路為例子。我們可以這樣幹。
我們只要把原來的那個直接輸出概率的那一個全連線層拆掉,然後再來幾個全連線層之後分別預測就完了。
至於損失函式,這也好辦,一個是交叉熵得到Loss1 還有一個是求方差,求對應的框的點和標註的框的誤差就完了得到Loss2 之後Loss=Loss1+Loss2
多目標檢測
然而理想是很豐滿的,但是現實很殘酷。現在的圖片當中往往都是有多個目標的,而且哪怕是同一個目標,在一張圖片當中也可能有多個,那問題不就尷尬了,比如下面的圖片:
所以我們需要解決這個問題。
問題分析
首先我們來想想,我們現在面臨的問題,首先對於一張圖片,對送進神經網路的圖片來說(假設資料集不是我們 自己搞的)我們是不知道當前這個圖片它是有幾個目標的,所以如果是按照咱們先前那個對LeNet的改動的話,我們是壓根就不知道要生成幾個框,做幾個概率的預測的。假設我們知道了,或者說我們一股腦直接生成一堆框,那麼我們需要如何篩選這些有用的框出來?並且我們怎麼區別這些框對應的類別是啥?最後我們的損失函式又要怎麼設計?
那麼如果我們能夠找到一種方式能夠搞定上面的問題,那麼多目標檢測應該就能夠實現了,換句話說能夠通用的目標檢測演算法就ok了。
滑動視窗
前面分析了我們如果想要實現那個多目標檢測,我們需要解決的問題。那麼第一個問題,如何生成框。回到一開始的方式,我們是直接輸入了一張圖片,然後,對這張圖片生成一個框,然後做預測等操作,那麼既然如此,那麼我就直接這樣,我把一張圖片直接分成一個個區域,相當於截圖一樣,一個一個區域截圖,然後分別送進神經網路。然後你懂的,我們套用剛剛提出的方法。
也就是下面這樣
我可以生成不同的滑動視窗,然後瘋狂搞。 理論上只要電腦不冒煙,我就可以一直搞。只要效率顯然....
所以還需要優化一下。
RCNN
那麼這個時候,你可能會想了,剛剛的問題難度在於我們很難去得到這些框,因為做分類對我們來說還是非常簡單的事情,但是做檢測,偏偏有個預測框很難弄。如果我們可以直接得到一堆候選框,然後對每一個框所屬的類別進行預測之後再採用某一種方法去篩選出合適的框不久變得簡單了嘛。
那麼這個時候RCNN出現了,在2014年的時候,那個時候我應該還是個小學生。它的流程是這樣的:
- 對於一張圖片,找出預設2000個候選區域
- 2000個侯選區域做大小變換,輸入AlexNet當中,得到特徵向量 [2000,4096]
- 經過20個類別的SVM分類器,對於2000個候選區域做判斷,得 到[2000,20]得分矩陣
- 2000個候選區域做NMS,取出不好的,重度高的一些候選 區域,得到剩下分數高,結果好的相
- 修正候選框,bbox的迴歸微調
那麼現在既然提到了RCNN,那麼我們現在就不得不先提到兩個概念了,第一是IOU,第二是NMS演算法也就是那個篩選演算法。
不過在這裡我先說一些IOU,因為NMS在程式碼階段會詳細介紹,我們需要手動實現這個演算法,當然IOU也需要,但是它非常簡單。
就是這個東西
我們可以用這個玩意來衡量這兩個生成的框是不是重合了,重合了多少,如果重合太多的話,是不是說他們兩個框都在預測同一個物體,那麼我們就可不可以把概率低的給幹掉。而這個的話其實也是NMS的思想,具體還是看下文。
那麼這裡解決了可以自動生成框的問題,但是這裡的分類器用的還是SVM,並且這個SVM肯定也是需要先訓練好的,不然很難完成分類呀。而且在訓練SVM的時候,我們是把經過了一個神經網路的資料給SVM的,那麼意味還需要對AlexNet做處理,需要快取很多中間資料然後訓練。
而且每一個框都要進入神經網路,2000個要進去似乎也沒有比暴力好到那兒去。
SPPNet
前面說了RCNN,其實最大的一個改進相對於滑動視窗來說,似乎就是多了一個方式去生成候選框。實際上後面那些SVM我們也未嘗不可以和AlexNet直接合併成一個大網路然後對2000個候選框做分類,而不是分開來。
但是最大的問題並不是這個,問題在於我們還是需要進入2000次卷積。
那麼有沒有辦法可以減少卷積咧,有SPPNet!
首先候選框還是咱們RCNN那種方式提取出來的,但是它直接把一張圖片輸入進一個卷積裡面。
然後得到一個特徵向量,之後這個特徵向量裡面包含了原來的候選框的資訊,他們之間存在這樣的對映關係:
這個對映關係的不是咱們的重點,這裡就忽略了,感興趣的可以自己去了解,不夠這個拿到feature map 絕對是目標檢測史上最重要的一點之一!不過在這裡還沒有太大體現。
那麼後面的操作其實就和RCNN類似了,只是中間又加了一些池化等等操作
至於缺點:
1. 訓練依然過慢、效率低,特徵需要寫入磁碟(因為SVM的存在)
2. 分階段訓練網路:選取候選區域、訓練CNN、訓練SVM、訓練bbox迴歸器,SPPNet)反向傳播效率 低
Fast-RCNN
當我把標題單獨放在外面的時候,我想你應該知道了這玩意的重要性。
來我們直接看到整個圖:
前面的部分其實和SPPNet很像,也就是一個卷積,但是後面全部變成 net,這個好像有點像咱們一開始瞎扯提到的方式了,也就是在後半部分。不過有點可惜的是總體上FastRCNN 的改進其實是把SPPNet後面的東西改了,前面的候選框其實還是使用RCNN的那一套機制,也就是SS演算法。
不夠儘管如此,fast rcnn 總算是和咱們現在的目標檢測演算法的樣子有點像了,因為我們終於廢棄了SVM,終於讓我們的神經網路去做更多的事情了。
並且提到了咱們的多工損失,而且不用把網路拆來了訓練了,而是可以做到端到端了。
並且速度有了很大的提升
之後它的網路圖是這樣的:
那麼雖然已經很快了,那麼還有辦法嘛?原來RCNN 可是2000個候選區域啊。能不能縮減!有沒有辦法?
(這裡面還有很多細節沒有提到,需要讀者自行搜尋,不過不影響本文觀看)
答案是有!
Faster RCNN
前面我們的FastRCNN 已經讓神經網路做了很多事情了,那麼為什麼不能把候選框的提取也做了,讓神經網路做到更多的事情?並且還有哪些東西是可以加強改進的?feature map 能不能利用起來?
嘿!還真能。
我們直接在feature map上面做提取,在上面生成候選區域,然後再執行後續操作,後續操作和咱們fast rcnn是一樣的,我們只需要對這些候選框和分類器處理。於是我們的網路結構就變成了這樣
在feature後面提取的網路叫做RPN
RPN 工作流程
說到這個玩意咱們就必須提一下,因為這個東西的工作流程絕對是非常重要的,這意味著我們可以做出更大的改進在後面!
我們知道它的工作地方實在feature map上面
那麼他如何工作呢。
這裡引入一個名稱叫做anchor 其實也就是bbox,那個預測框。
他是這樣的,在那個feature map 的基礎上,每一個網格,都會生成9個框,假設那個特徵是20x20 的那麼他有9個就是20x20x9 如果要具體表示的話,xmin,ymin,xmax,ymax(左上角,右下角)那就是20x20x36的張量
那麼這裡為什麼是9個呢,因為是這樣的,原作者設計了三種比例三種大小的樣式,因為圖片當中物體的大小是不一樣的。
那麼剛好對應的就是9個組合。之後的部分我就不細說了。
Yolo
那麼到這裡,你可能又有疑問了,那個RPN一樣的網路能不能放在featur map 前面呢?如果我一開始就指定好圖片的網格,然後不同的網格去生成候選框會怎麼樣? 沒錯大名鼎鼎的yolo出來了:(這裡是v1)
我先直接這樣認為分成7x7的格子然後每個格子產生候選框,這裡是2個候選框。
之後得到7x7x30的張量.
這裡解釋一下30裡面包含了啥。
這裡面儲存了 兩大類資訊。 第一個 是 邊框資訊,起點,寬高,可信度。 第二個是 類別的條件概率,這裡主要是20個類。
之後我們通過NMS對這些候選框進行篩選。
然後進入損失函式,這部分我們後面說,它的損失函式是這樣的
我們接下來要自制的目標檢測框架其實也是基於yolov1的。
小結
那麼對於理論部分我們就先到這裡,這裡面的話還是有很多細節是沒有說到的,例如Fast rcnn 裡面,我們NMS處理以後,我們的那些剩下的框雖然是知道了所屬的分類,但是我們迴歸的時候我們是和那些手動標註的框進行迴歸?這部分我沒有說,由於篇幅問題,這部分也是需要讀者自行探索,其實讀者也可以大膽猜測一下和IOU有沒有關係咧?此外還有其他的優秀演算法沒有介紹到,比如SSD等等。
當然前面的大部分內容只是做了解即可,因為更加完整的將在程式碼部分進行。
編碼
接下來我們將針對yolov1 演算法進行實現然後將其封裝進去咱們自己搭建的平臺。
那麼對我們的編碼實現裡面最主要的其實有三個大點:
- 圖片資料怎麼處理,怎麼對圖片進行預處理
- IOU, NMS 演算法的具體實現
- 損失函式的設計
首先是咱們的第一點,對圖片是否需要,如何進行預處理。
神經網路實現
我們這邊的話是打算直接整合yolov1的神經網路結構。
所以的話我們需要先編寫神經網路。但是呢,為了更好地提高網路識別的精度和訓練效率,我們這邊還要考慮預訓練一個神經網路模型。
所以為了實現這個效果我們需要對這個網路做一點點的改動,提取出一個骨幹網路出來。
其中BackBone就是我們的核心網路,也就是其中的10幾個卷積,後面兩個一個是特徵提取網路一個是我們用於目標識別的網路。我們預訓練是訓練特徵提取網路,這個網路是依託與骨幹網路的。他們之間的關係是這樣的:
特徵提取網路其實就是在骨幹網路的基礎上用於分類,這樣一來就得到了權重,當我們訓練目標檢測網路的時候,我們可以把先前預訓練的特徵網路當中的骨幹網路的權重提取出來作為初始化權重,這也就是遷移學習。
骨幹網路
```python import torch.nn as nn import torch from collections import OrderedDict
class Convention(nn.Module): def init(self,in_channels,out_channels,conv_size,conv_stride,padding,need_bn = True):
"""
這邊對Conv2d進行一個封裝,引數一致
但是多加了LeakReLU,和歸一化,原因不多說了
:param in_channels:
:param out_channels:
:param conv_size:
:param conv_stride:
:param padding:
:param need_bn:
"""
super(Convention,self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, conv_size, conv_stride, padding, bias=False if need_bn else True)
self.leaky_relu = nn.LeakyReLU()
self.need_bn = need_bn
if need_bn:
self.bn = nn.BatchNorm2d(out_channels)
def forward(self, x):
return self.bn(self.leaky_relu(self.conv(x))) if self.need_bn else self.leaky_relu(self.conv(x))
def weight_init(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight.data)
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
class BackboneNet(nn.Module): """ 骨幹網路,因為那個論文中也提到了預訓練的概念 那麼這個預訓練其實是說訓練這個骨幹網路,而這個 網路的話其實是7x7x30的前半部分 那個yolo是24卷積+2個全連線得到7x7x1024之後flatten4096 最後變成7x7x30,然後就是NMS,預訓練需要先訓練一個 分類的網路,所以這部分是不一樣的 """ def init(self): super(BackboneNet,self).init()
"""
用於特徵提取的16個卷積
"""
self.Conv_Feature = nn.Sequential(
Convention(3, 64, 7, 2, 3),
nn.MaxPool2d(2, 2),
Convention(64, 192, 3, 1, 1),
nn.MaxPool2d(2, 2),
Convention(192, 128, 1, 1, 0),
Convention(128, 256, 3, 1, 1),
Convention(256, 256, 1, 1, 0),
Convention(256, 512, 3, 1, 1),
nn.MaxPool2d(2, 2),
Convention(512, 256, 1, 1, 0),
Convention(256, 512, 3, 1, 1),
Convention(512, 256, 1, 1, 0),
Convention(256, 512, 3, 1, 1),
Convention(512, 256, 1, 1, 0),
Convention(256, 512, 3, 1, 1),
Convention(512, 256, 1, 1, 0),
Convention(256, 512, 3, 1, 1),
Convention(512, 512, 1, 1, 0),
Convention(512, 1024, 3, 1, 1),
nn.MaxPool2d(2, 2),
)
self.Conv_Semanteme = nn.Sequential(
Convention(1024, 512, 1, 1, 0),
Convention(512, 1024, 3, 1, 1),
Convention(1024, 512, 1, 1, 0),
Convention(512, 1024, 3, 1, 1),
)
```
這裡可以看到這個網路啥也沒有,就是一個最基本的骨架。
特徵提取網路(預訓練)
```python import torch import torch.nn as nn from Models.Backbone import BackboneNet, Convention
class YOLOFeature(BackboneNet): def init(self,classes_num = 20): """ 原文說的就是20個所以咱們也就來個20 :param classes_num: """ super(YOLOFeature,self).init() self.classes_num = classes_num
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.linear = nn.Linear(1024, self.classes_num)
def forward(self, x):
x = self.Conv_Feature(x)
x = self.Conv_Semanteme(x)
x = self.avg_pool(x)
x = x.permute(0, 2, 3, 1)
x = torch.flatten(x, start_dim=1, end_dim=3)
x = self.linear(x)
return x
"""
初始化權重
"""
def initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight.data)
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
torch.nn.init.kaiming_normal_(m.weight.data)
m.bias.data.zero_()
elif isinstance(m, Convention):
m.weight_init()
```
目標檢測網路
最後是咱們的目標檢測網路。
```python import torch.nn as nn import torch
from Models.Backbone import BackboneNet, Convention
class YOLO(BackboneNet):
def __init__(self, B=2, classes_num=20):
super(YOLO, self).__init__()
self.B = B
self.classes_num = classes_num
self.Conv_Back = nn.Sequential(
Convention(1024, 1024, 3, 1, 1, need_bn=False),
Convention(1024, 1024, 3, 2, 1, need_bn=False),
Convention(1024, 1024, 3, 1, 1, need_bn=False),
Convention(1024, 1024, 3, 1, 1, need_bn=False),
)
self.Fc = nn.Sequential(
nn.Linear(7 * 7 * 1024, 4096),
nn.LeakyReLU(inplace=True, negative_slope=1e-1),
nn.Linear(4096, 7 * 7 * (B * 5 + classes_num)),
nn.Sigmoid()
)
self.sigmoid = nn.Sigmoid()
"""
batchx7x7x30讓最後一個維度對應的類別為概率和為1
"""
# self.softmax = nn.Softmax(dim=3)
def forward(self, x):
x = self.Conv_Feature(x)
x = self.Conv_Semanteme(x)
x = self.Conv_Back(x)
x = x.permute(0, 2, 3, 1)
x = torch.flatten(x, start_dim=1, end_dim=3)
x = self.Fc(x)
x = x.view(-1,7,7,(self.B*5 + self.classes_num))
# x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5])
# x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])
"""
在pytorch當中註釋部分的操作屬於inplace操作,而且在官方文件當中,明確表明
在多交叉熵當中,pytorch不需要使用softmax,因為在計算的時候是包括了這部分的操作的
並且在yolov1的損失函式當中,計算的類別損失也不是交叉熵
"""
x = self.sigmoid(x)
return x
def initialize_weights(self, net_param_dict):
for name, m in self.named_modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight.data)
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
torch.nn.init.kaiming_normal_(m.weight.data)
m.bias.data.zero_()
elif isinstance(m, Convention):
m.weight_init()
self_param_dict = self.state_dict()
for name, layer in self.named_parameters():
if name in net_param_dict:
self_param_dict[name] = net_param_dict[name]
self.load_state_dict(self_param_dict)
``` 這裡要特別注意我註釋的這段程式碼:
python
# x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5])
# x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])
接下來我會更加詳細地說明
資料集編碼
現在我們已經知道了咱們這邊的目的有兩個,一個是要預訓練,一個是要目標檢測
預訓練資料集
其中咱們的預訓練是訓練一個基本的過程。
那麼在這裡的話,其實很簡單,我們訓練的話我們只需要把那個特徵網路拿過來,重點是咱們的這個預訓練資料集怎麼來。
那麼這邊的話,如果是老盆友,或者是看來剛剛開頭推薦觀看的文章的朋友應該知道,這邊的話我們可以直接把咱們的HuDataSet拿過來。
首先這個資料集的定義非常簡單:
相信你一眼就知道了是怎麼一回事。分訓練很驗證集,然後每個分類的標籤放在對應的資料夾下面就可以了。
核心程式碼如下:
```python
from Config.Config import * import os from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision.transforms import transforms
from Utils.ReaderProcess.ReadDict import ReadDict
class MyDataSet(Dataset): def init(self, data_dir,ClassesName, transform=None): self.ClassesName = ClassesName self.label_name = ReadDict.ReadModelClasses(self.ClassesName)
self.data_info = self.get_img_info(data_dir)
self.transform = transform
def __getitem__(self, index):
path_img, label = self.data_info[index]
img = Image.open(path_img).convert('RGB')
if self.transform is not None:
img = self.transform(img)
return img, label
def __len__(self):
return len(self.data_info)
def get_img_info(self,data_dir):
data_info = list()
label_dict=ReadDict.ReadModelClasses(self.ClassesName)
for root, dirs, _ in os.walk(data_dir): #
# 遍歷類別
for sub_dir in dirs:
img_names = os.listdir(os.path.join(root, sub_dir))
img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))
# 遍歷圖片
for i in range(len(img_names)):
img_name = img_names[i]
path_img = os.path.join(root, sub_dir, img_name)
label = label_dict[sub_dir]
data_info.append((path_img, int(label)))
return data_info
```
目標檢測資料集
這裡的話我們採用VOC資料集,資料集的基本樣式其實很簡單。
一個是Annotations註解,還有一個是圖片
註解裡面是xml檔案
裡面包括了類別和手動標註的框的位置。
```python
```
由於我們需要進行目標檢測,但是呢,我們除了要提取裡面的標籤資訊的話,還要把裡面的標籤(類別,方框)資訊進行轉化,轉化的目的也是為了複合神經網路的輸出方便損失函式計算。
VOC標籤解析
解析的話很簡單,就這個
```python
for object_xml in objects_xml:
bnd_xml = object_xml.find("bndbox")
class_name = object_xml.find("name").text
if class_name not in self.class_dict: # 不屬於我們規定的類
continue
xmin = round((float)(bnd_xml.find("xmin").text))
ymin = round((float)(bnd_xml.find("ymin").text))
xmax = round((float)(bnd_xml.find("xmax").text))
ymax = round((float)(bnd_xml.find("ymax").text))
class_id = self.class_dict[class_name]
"""
這裡解析儲存的是5個值,縮放,歸一化後的座標和對應的類別的標籤
"""
coords.append([xmin, ymin, xmax, ymax, class_id])
```
完整與之配合的程式碼是這樣的: 這裡還使用了部分資料增強
```python import torch from torch.utils.data import Dataset import os import cv2 import xml.etree.ElementTree as ET import torchvision.transforms as transforms import numpy as np import random from Utils import image from Config.ConfigTrain import * class VOCDataSet(Dataset):
def __init__(self, imgs_path="../DataSet/VOC2007+2012/Train/JPEGImages",
annotations_path="../DataSet/VOC2007+2012/Train/Annotations",
is_train=True, class_num=Classes,
label_smooth_value=0.05, input_size=448, grid_size=64): # input_size:輸入影象的尺度
self.label_smooth_value = label_smooth_value
self.class_num = class_num
self.imgs_name = os.listdir(imgs_path)
self.input_size = input_size
self.grid_size = grid_size
self.is_train = is_train
self.transform_common = transforms.Compose([
transforms.ToTensor(), # height * width * channel -> channel * height * width
transforms.Normalize(mean=(0.408, 0.448, 0.471), std=(0.242, 0.239, 0.234)) # 歸一化後.不容易產生梯度爆炸的問題
])
self.imgs_path = imgs_path
self.annotations_path = annotations_path
self.class_dict = {}
class_index = 0
"""
讀取配置標籤
"""
for class_name in ClassesName:
self.class_dict[class_name] = class_index
class_index+=1
def __getitem__(self, item):
img_path = os.path.join(self.imgs_path, self.imgs_name[item])
annotation_path = os.path.join(self.annotations_path, self.imgs_name[item].replace(".jpg", ".xml"))
img = cv2.imread(img_path)
tree = ET.parse(annotation_path)
annotation_xml = tree.getroot()
objects_xml = annotation_xml.findall("object")
coords = []
for object_xml in objects_xml:
bnd_xml = object_xml.find("bndbox")
class_name = object_xml.find("name").text
if class_name not in self.class_dict: # 不屬於我們規定的類
continue
xmin = round((float)(bnd_xml.find("xmin").text))
ymin = round((float)(bnd_xml.find("ymin").text))
xmax = round((float)(bnd_xml.find("xmax").text))
ymax = round((float)(bnd_xml.find("ymax").text))
class_id = self.class_dict[class_name]
"""
這裡解析儲存的是5個值,縮放,歸一化後的座標和對應的類別的標籤
"""
coords.append([xmin, ymin, xmax, ymax, class_id])
coords.sort(key=lambda coord: (coord[2] - coord[0]) * (coord[3] - coord[1]))
if self.is_train:
transform_seed = random.randint(0, 4)
if transform_seed == 0: # 原圖
img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
img = self.transform_common(img)
elif transform_seed == 1: # 縮放+中心裁剪
img, coords = image.center_crop_with_coords(img, coords)
img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
img = self.transform_common(img)
elif transform_seed == 2: # 平移
img, coords = image.transplant_with_coords(img, coords)
img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
img = self.transform_common(img)
elif transform_seed == 3: # 明度調整 YOLO在論文中稱曝光度為明度
img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
H, S, V = cv2.split(img)
cv2.merge([np.uint8(H), np.uint8(S), np.uint8(V * 1.5)], dst=img)
cv2.cvtColor(src=img, dst=img, code=cv2.COLOR_HSV2BGR)
img = self.transform_common(img)
else: # 飽和度調整
img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
H, S, V = cv2.split(img)
cv2.merge([np.uint8(H), np.uint8(S * 1.5), np.uint8(V)], dst=img)
cv2.cvtColor(src=img, dst=img, code=cv2.COLOR_HSV2BGR)
img = self.transform_common(img)
else:
img, coords = image.resize_image_with_coords(img, self.input_size, self.input_size, coords)
img = self.transform_common(img)
ground_truth = self.encode(coords)
"""
這裡傳入的coords是經過圖片增強,然後歸一化之後的
之後的話,我們需要經過encode目的是的為了製作方便後期和pred對比的label
"""
return img,ground_truth
def __len__(self):
return len(self.imgs_name)
def encode(self, coords):
feature_size = self.input_size // self.grid_size
ground_truth = np.zeros([feature_size, feature_size, 10 + self.class_num],dtype=float)
for coord in coords:
# positive_num = positive_num + 1
# bounding box歸一化
xmin, ymin, xmax, ymax, class_id = coord
ground_width = (xmax - xmin)
ground_height = (ymax - ymin)
center_x = (xmin + xmax) / 2
center_y = (ymin + ymax) / 2
index_row = (int)(center_y * feature_size)
index_col = (int)(center_x * feature_size)
ground_box = [center_x * feature_size - index_col, center_y * feature_size - index_row,
ground_width, ground_height, 1,
round(xmin * self.input_size), round(ymin * self.input_size),
round(xmax * self.input_size), round(ymax * self.input_size),
round(ground_width * self.input_size * ground_height * self.input_size)
]
# ground_box.extend(class_list)
class_ = [0 for _ in range(self.class_num)]
class_[class_id]=1
ground_box.extend(class_)
ground_truth[index_row][index_col] = np.array(ground_box,dtype=float)
return ground_truth
```
格式轉化
現在請把目光轉移到這裡來:
```python
def encode(self, coords):
feature_size = self.input_size // self.grid_size
ground_truth = np.zeros([feature_size, feature_size, 10 + self.class_num],dtype=float)
for coord in coords:
# positive_num = positive_num + 1
# bounding box歸一化
xmin, ymin, xmax, ymax, class_id = coord
ground_width = (xmax - xmin)
ground_height = (ymax - ymin)
center_x = (xmin + xmax) / 2
center_y = (ymin + ymax) / 2
index_row = (int)(center_y * feature_size)
index_col = (int)(center_x * feature_size)
ground_box = [center_x * feature_size - index_col, center_y * feature_size - index_row,
ground_width, ground_height, 1,
round(xmin * self.input_size), round(ymin * self.input_size),
round(xmax * self.input_size), round(ymax * self.input_size),
1
]
# ground_box.extend(class_list)
class_ = [0 for _ in range(self.class_num)]
class_[class_id]=1
ground_box.extend(class_)
ground_truth[index_row][index_col] = np.array(ground_box,dtype=float)
return ground_truth
```
我們把VOC的格式解析出來了,也做了資料增強之後做了歸一化得到了幾個標註的框。但是由於在論文當中是這樣的:
作者將一張圖片劃分為了7x7的網格,讓每一格子預測兩個框,所以我們真實標註的框也需要轉化為這種格式,我們需要手動把我們的結果轉化為7x7x(10+類別個數)的樣子,因為網路最後的輸出就是7x7x(10+類別個數)
當然 實際上,我們標註的框轉化之後一個格子應該是隻有一個物體的,所以這裡我們轉化的話其實不用那麼嚴格只需要7x7x(5+類別個數)就可以了,但是這裡為了對得到,同時方便後面轉化,這裡還儲存了實際上圖片的框的座標(以這個格子為中心)
那麼一來在實際計算損失的時候,我們只需要這樣:
所以因為這個特性,我們需要把標籤這樣進行轉化,方便損失函式計算,而且損失函式的計算是一個一個格子來對比計算的,也就是一個一個的grad cell。
損失函式(目標檢測)
之後咱們的損失函式,前面說了為啥要轉化標籤,那麼現在咱們可以來看看損失函數了。
這裡提一下正負樣本的概念,這裡的話其實也簡單,就是一個一個格子去對比,然後呢有些格子是沒有目標的,但是我們預測的時候每個格子都是預測了兩個框的,那麼這兩個框顯然是沒有用的,那麼這個玩意就是負樣本,同理如果對應的格子有目標,但是兩個框的IOU不一樣(與實際的框)那麼IOU低的也算是負樣本。
```python import sys import torch.nn as nn import math import torch import torch.nn.functional as F from Config.ConfigTrain import ClassesName
class YOLOLoss(nn.Module):
def __init__(self, S=7, B=2, Classes=20, l_coord=5, l_noobj=0.5, epcoh_threshold=400):
"""
:param S:
:param B:
:param Classes:
:param l_coord:
:param l_noobj:
:param epcoh_threshold:
有物體的box損失權重設為l_coord,沒有物體的box損失權重設定為l_noobj
在論文當中應該是正樣本和負樣本之間的一個權重,因為我們不僅僅要預測有物體的,原來沒有物體的也不能有物體
"""
super(YOLOLoss, self).__init__()
self.S = S
self.B = B
self.Classes = Classes
self.l_coord = l_coord
self.l_noobj = l_noobj
self.epcoh_threshold = epcoh_threshold
def iou(self, bounding_box, ground_box, gridX, gridY, img_size=448, grid_size=64):
"""
計算交併比
:param bounding_box:
:param ground_box:
:param gridX:
:param gridY:
:param img_size:
:param grid_size:
:return:
由於predict_box 返回的是x y w h 這種格式,所以我們還是需要進行轉換回原來的xmin ymin xmax ymax
也就是左上右下
"""
predict_box = [0, 0, 0, 0]
predict_box[0] = (int)(gridX + bounding_box[0].item() * grid_size)
predict_box[1] = (int)(gridY + bounding_box[1].item() * grid_size)
predict_box[2] = (int)(bounding_box[2].item() * img_size)
predict_box[3] = (int)(bounding_box[3].item() * img_size)
predict_coord = list([max(0, predict_box[0] - predict_box[2] / 2),
max(0, predict_box[1] - predict_box[3] / 2),
min(img_size - 1, predict_box[0] + predict_box[2] / 2),
min(img_size - 1, predict_box[1] + predict_box[3] / 2)])
predict_Area = (predict_coord[2] - predict_coord[0]) * (predict_coord[3] - predict_coord[1])
ground_coord = list([ground_box[5].item() , ground_box[6].item() , ground_box[7].item() , ground_box[8].item() ])
ground_Area = (ground_coord[2] - ground_coord[0]) * (ground_coord[3] - ground_coord[1])
"""
轉化為原來左上右下之後進行計算
"""
CrossLX = max(predict_coord[0], ground_coord[0])
CrossRX = min(predict_coord[2], ground_coord[2])
CrossUY = max(predict_coord[1], ground_coord[1])
CrossDY = min(predict_coord[3], ground_coord[3])
if CrossRX < CrossLX or CrossDY < CrossUY: # 沒有交集
return 0
interSection = (CrossRX - CrossLX) * (CrossDY - CrossUY)
return interSection / (predict_Area + ground_Area - interSection)
def forward(self, bounding_boxes, ground_truth, batch_size=32, grid_size=64,
img_size=448): # 輸入是 S * S * ( 2 * B + Classes)
# 定義三個計算損失的變數 正樣本定位損失 樣本置信度損失 樣本類別損失
loss = 0
loss_coord = 0
loss_confidence = 0
loss_classes = 0
iou_sum = 0
object_num = 0
mseLoss = nn.MSELoss()
for batch in range(len(bounding_boxes)):
for indexRow in range(self.S): # 先行 - Y
for indexCol in range(self.S): # 後列 - X
"""
這裡額外統計了三個損失
"""
bounding_box = bounding_boxes[batch][indexRow][indexCol]
predict_box_one = bounding_box[0:5]
predict_box_two = bounding_box[5:10]
ground_box = ground_truth[batch][indexRow][indexCol]
# 1.如果此處ground_truth不存在 即只有背景 那麼兩個框均為負樣本
if (ground_box[4]) == 0: # 面積為0的grount_truth 表明此處只有背景
loss = loss + self.l_noobj * torch.pow(predict_box_one[4], 2) + torch.pow(
predict_box_two[4], 2)
loss_confidence += self.l_noobj * math.pow(predict_box_one[4].item(), 2) + math.pow(
predict_box_two[4].item(), 2)
else:
# print(ground_box[4].item(), ClassesName[int(ground_box[10].item())])
object_num = object_num + 1
predict_iou_one = self.iou(predict_box_one, ground_box, indexCol * 64, indexRow * 64)
predict_iou_two = self.iou(predict_box_two, ground_box, indexCol * 64, indexRow * 64)
# 改進:讓兩個預測的box與ground box擁有更大iou的框進行擬合 讓iou低的作為負樣本
if predict_iou_one > predict_iou_two: # 框1為正樣本 框2為負樣本
predict_box = predict_box_one
iou = predict_iou_one
no_predict_box = predict_box_two
else:
predict_box = predict_box_two
iou = predict_iou_two
no_predict_box = predict_box_one
# 正樣本:
# 定位
loss = loss + self.l_coord * (torch.pow((ground_box[0] - predict_box[0]), 2) + torch.pow(
(ground_box[1] - predict_box[1]), 2) + torch.pow(
torch.sqrt(ground_box[2] + 1e-8) - torch.sqrt(predict_box[2] + 1e-8), 2) + torch.pow(
torch.sqrt(ground_box[3] + 1e-8) - torch.sqrt(predict_box[3] + 1e-8), 2))
loss_coord += self.l_coord * (
math.pow((ground_box[0] - predict_box[0].item()), 2) + math.pow(
(ground_box[1] - predict_box[1].item()), 2) + math.pow(
math.sqrt(ground_box[2] + 1e-8) - math.sqrt(predict_box[2].item() + 1e-8),
2) + math.pow(
math.sqrt(ground_box[3] + 1e-8) - math.sqrt(predict_box[3].item() + 1e-8), 2))
# 置信度
loss = loss + torch.pow(predict_box[4] - iou, 2)
loss_confidence += math.pow(predict_box[4].item() - iou, 2)
iou_sum = iou_sum + iou
# 分類
ground_class = ground_box[10:]
predict_class = bounding_box[self.B * 5:]
loss = loss + mseLoss(ground_class, predict_class)
loss_classes += mseLoss(ground_class, predict_class).item()
# 負樣本 置信度:
loss = loss + self.l_noobj * torch.pow(no_predict_box[4] - 0, 2)
loss_confidence += math.pow(no_predict_box[4].item() - 0, 2)
return loss/batch_size, loss_coord/batch_size, loss_confidence/batch_size, loss_classes/batch_size, iou_sum, object_num
```
那麼在這裡的話我也要說說,剛剛註釋的這個程式碼:
python
# x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5])
# x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])
它為什麼不行了,第一個這個程式碼本身存在inplace操作。
第二如果真的需要使用交叉熵作為分類的損失函式的話,pytorch內部的交叉熵損失函式自己是計算了softmax的
第三,就是咱們的sunshine函式裡面壓根不是交叉熵來算類別損失的,人家就是MSE。
之後是關於置信度confidence的計算,這個玩意是表示這裡面有沒有(這個格子裡面)物體的,首先預測的時候,那個值是預測出來的,計算損失的時候,那個c(在有物品的情況下)是等於1的,這個在咱們voc資料集裡面可以看到,有物品直接為1
但是呢,實際計算的時候,這個c呢是咱們那個預測框和實際框的IOU。
這個論文當中也有描述。
訓練部分
接下來是咱們的訓練部分,這個呢,有兩個一個是預訓練一個是實際訓練。
預訓練得到的一個模型還可以用於圖片分類。
預訓練
這部分其實很簡單就不多說了。
```python import argparse import torch import torch.nn as nn from torch.utils.data import DataLoader import torch.optim as optim from Models.FeatureNet import YOLOFeature from Utils import ModelUtils from Config.ConfigPre import * from Utils.DataSet.MyDataSet import MyDataSet from Utils.DataSet.TransformAtions import TransFormAtions import os from Utils import SaveModel from Utils import Log from torch.utils.tensorboard import SummaryWriter
def train():
ModelUtils.set_seed()
# 初始化驅動
device = None
if (torch.cuda.is_available()):
if (not opt.device == 'cpu'):
div = "cuda:" + opt.device
# 這邊後面還得做一個檢測,看看有沒有坑貨,亂輸入
device = torch.device(div)
print("\033[0;31;0m使用GPU訓練中:{}\033[0m".format(torch.cuda.get_device_name()))
else:
device = torch.device("cpu")
print("\033[0;31;40m使用CPU訓練\033[0m")
else:
device = torch.device("cpu")
print("\033[0;31;40m使用CPU訓練\033[0m")
# 建立 runs exp 檔案
EPX_Path = SaveModel.CreatRun(0,"pre")
# 日誌相關的準備工作
wirter = None
openTensorboard = opt.tensorboardopen
path_board = None
if (openTensorboard):
path_board = EPX_Path + "\\logs"
wirter = SummaryWriter(path_board)
fo = Log.PrintLog(EPX_Path)
# 準備資料集
transformations = TransFormAtions()
train_data_dir = opt.train_dir
if (not train_data_dir):
train_data_dir = Data_Root + "\\" + Train
if (not os.path.exists(train_data_dir)):
raise Exception("訓練集路徑錯誤")
train_data = MyDataSet(data_dir=train_data_dir, transform=transformations.train_transform,ClassesName=ClassesName)
valid_data_dir = opt.valid_dir
if (not valid_data_dir):
valid_data_dir = Data_Root + "\\" + Valid
if (not os.path.exists(valid_data_dir)):
raise Exception("測試集路徑錯誤")
valid_data = MyDataSet(data_dir=valid_data_dir, transform=transformations.valid_transform,ClassesName=ClassesName)
# 構建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=opt.batch_size, num_workers=opt.works, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=opt.batch_size)
# 開始進入網路訓練
# 1 開始初始化網路,設定引數啥的
# 1.1 初始化網路
net = YOLOFeature(Classes)
net.initialize_weights()
net = net.to(device)
# 1.2選擇交叉熵損失函式,做分類問題一般是選擇這個損失函式的
criterion = nn.CrossEntropyLoss()
# 1.3設定優化器
optimizer = optim.SGD(net.parameters(), lr=opt.lr, momentum=0.09) # 選擇優化器
# 設定學習率下降策略,預設的也可以,那就不設定嘛,主要是不斷去自動調整學習的那個速度
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.01)
# 2 開始進入訓練步驟
# 2.1 進入網路訓練
Best_weight = None
Best_Acc = 0.0
for epoch in range(opt.epochs):
loss_mean = 0.0
correct = 0.0
total = 0.0
current_Acc_ecpho = 0.0
bacth_index = 0.
val_time = 0
net.train()
print("正在進行第{}輪訓練".format(epoch + 1))
for i, data in enumerate(train_loader):
bacth_index+=1
# forward
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
# print(inputs.shape,labels.shape)
outputs = net(inputs)
# print(outputs.shape, labels.shape)
# backward
optimizer.zero_grad()
loss = criterion(outputs, labels)
loss.backward()
# update weights
optimizer.step()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).squeeze().sum()
# 列印訓練資訊,進入對比
loss_mean += loss.item()
current_Acc = correct / total
current_Acc_ecpho+=current_Acc
if (i + 1) % opt.log_interval == 0:
loss_mean = loss_mean / opt.log_interval
info = "訓練:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}" \
.format \
(
epoch, opt.epochs, i + 1, len(train_loader), loss_mean, current_Acc
)
print(info, file=fo)
if (opt.show_log_console):
info_print = "\033[0;33;0m" + info + "\033[0m"
print(info_print)
loss_mean = 0.0
# tensorboard 繪圖
if (wirter):
wirter.add_scalar("訓練準確率", current_Acc_ecpho, (epoch))
wirter.add_scalar("訓練損失均值", loss_mean, (epoch))
current_Acc_ecpho/=bacth_index
# 儲存效果最好的玩意
if (current_Acc_ecpho > Best_Acc):
Best_weight = net.state_dict()
Best_Acc = current_Acc_ecpho
scheduler.step() # 更新學習率
# 2.2 進入訓練對比階段
if (epoch + 1) % opt.val_interval == 0:
correct_val = 0.0
total_val = 0.0
loss_val = 0.0
current_Acc_val = 0.0
current_Acc_ecpho_val = 0.
batch_index_val = 0.0
net.eval()
with torch.no_grad():
for j, data in enumerate(valid_loader):
batch_index_val+=1
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
outputs = net(inputs)
loss = criterion(outputs, labels)
loss_val += loss.item()
_, predicted = torch.max(outputs.data, 1)
total_val += labels.size(0)
correct_val += (predicted == labels).squeeze().sum()
current_Acc_val = correct_val / total_val
current_Acc_ecpho_val+=current_Acc_val
info_val = "測試:\tEpoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format \
(
epoch, opt.epochs, j + 1, len(valid_loader), loss_val, current_Acc_val
)
print(info_val, file=fo)
if (opt.show_log_console):
info_print_val = "\033[0;31;0m" + info_val + "\033[0m"
print(info_print_val)
current_Acc_ecpho_val/=batch_index_val
if (wirter):
wirter.add_scalar("測試準確率", current_Acc_ecpho_val, (val_time))
wirter.add_scalar("測試損失總值", loss_val, (val_time))
val_time+=1
# 最後一次的權重
Last_weight = net.state_dict()
# 儲存模型
SaveModel.Save_Model(EPX_Path, Best_weight, Last_weight)
fo.close()
if (wirter):
print("tensorboard dir is:", path_board)
wirter.close()
if name == 'main': parser = argparse.ArgumentParser() parser.add_argument('--epochs', type=int, default=10) parser.add_argument('--batch-size', type=int, default=8) parser.add_argument('--lr', type=float, default=0.01) parser.add_argument('--log_interval', type=int, default=10) # 訓練幾輪測試一次 parser.add_argument('--val_interval', type=int, default=1) parser.add_argument('--train_dir', type=str, default='') parser.add_argument('--valid_dir', type=str, default='') # 如果是Mac系注意這個引數可能需要設定為1,本地訓練,不推薦MAC parser.add_argument('--works', type=int, default=2) parser.add_argument('--show_log_console', type=bool, default=True) parser.add_argument('--device', type=str, default="0", help="預設使用顯示卡加速訓練引數選擇:0,1,2...or cpu") parser.add_argument('--tensorboardopen', type=bool, default=True) opt = parser.parse_args() train()
# tensorboard --logdir = runs/train/epx2/logs
``` 這部分還是簡單的。
目標檢測訓練
之後就是咱們目標檢測演算法的實現,這個其實核心流程都是一樣的,就是多了一些東西用來做記錄
```python import argparse import gc
import torch from torch.utils.data import DataLoader import torch.optim as optim from Models.Yolo import YOLO from Models.YoloLoss import YOLOLoss from Utils import ModelUtils from Config.ConfigTrain import * from Utils.DataSet.VOC import VOCDataSet import os from Utils import SaveModel from Utils import Log from torch.utils.tensorboard import SummaryWriter
def train():
ModelUtils.set_seed()
# 初始化驅動
device = None
if (torch.cuda.is_available()):
if (not opt.device == 'cpu'):
div = "cuda:" + opt.device
device = torch.device(div)
torch.backends.cudnn.benchmark = True
print("\033[0;31;0m使用GPU訓練中:{}\033[0m".format(torch.cuda.get_device_name()))
else:
device = torch.device("cpu")
print("\033[0;31;40m使用CPU訓練\033[0m")
else:
device = torch.device("cpu")
print("\033[0;31;40m使用CPU訓練\033[0m")
# 建立 runs exp 檔案
EPX_Path = SaveModel.CreatRun(0,"detect")
# 日誌相關的準備工作
wirter = None
openTensorboard = opt.tensorboardopen
path_board = None
if (openTensorboard):
path_board = EPX_Path + "\\logs"
wirter = SummaryWriter(path_board)
fo = Log.PrintLog(EPX_Path)
train_data_dir_image = opt.train_dir_image
train_data_dir_Ann = opt.train_dir_Ann
if (not train_data_dir_image):
train_data_dir_image = TrainImage
if (not os.path.exists(train_data_dir_image)):
raise Exception("訓練集路徑錯誤")
if (not train_data_dir_Ann):
train_data_dir_Ann = TrainAnn
if (not os.path.exists(train_data_dir_Ann)):
raise Exception("訓練集路徑錯誤")
train_data =VOCDataSet(imgs_path=train_data_dir_image,
annotations_path=train_data_dir_Ann,
is_train=True)
valid_data_dir_image = opt.valid_dir_image
valid_data_dir_Ann = opt.valid_dir_Ann
if (not valid_data_dir_image):
valid_data_dir_image = ValImage
if (not os.path.exists(valid_data_dir_image)):
raise Exception("訓練集路徑錯誤")
if (not valid_data_dir_Ann):
valid_data_dir_Ann = ValAnn
if (not os.path.exists(valid_data_dir_Ann)):
raise Exception("訓練集路徑錯誤")
valid_data = VOCDataSet(imgs_path=valid_data_dir_image,
annotations_path=valid_data_dir_Ann,
is_train=False)
# 構建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=opt.batch_size, num_workers=opt.works, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=opt.batch_size)
# 1 開始初始化網路,設定引數啥的
net = YOLO(B=2,classes_num=Classes)
#載入預訓練權重
if(PreWeight):
# 1.1 初始化網路
preweight = torch.load(PreWeight)
net.initialize_weights(preweight)
net = net.to(device)
loss_func = YOLOLoss(S=7,B=2,Classes=Classes).to(device)
# 1.3設定優化器
optimizer = optim.SGD(net.parameters(), lr=opt.lr, momentum=0.09) # 選擇優化器
# 設定學習率下降策略,預設的也可以,那就不設定嘛,主要是不斷去自動調整學習的那個速度
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.01)
# 2 開始進入訓練步驟
# 2.1 進入網路訓練
Best_weight = None
TotalLoss = 0.
ValLoss = 0.
ValTime = 0.
Best_loss = float("inf")
for epoch in range(opt.epochs):
"""
下面是一些用來記錄當前網路執行狀態的引數
"""
train_loss = 0
val_loss = 0
# train_iou = 0
# val_iou = 0
# train_object_num = 0
# val_object_num = 0
train_loss_coord = 0
val_loss_coord = 0
train_loss_confidence = 0
val_loss_confidence = 0
train_loss_classes = 0
val_loss_classes = 0
log_loss_mean_train = 0.
# log_loss_mean_val = 0.
net.train()
print("正在進行第{}輪訓練".format(epoch + 1))
for i, data in enumerate(train_loader):
# forward
inputs, labels = data
inputs, labels = inputs.float().to(device), labels.float().to(device)
outputs = net(inputs)
optimizer.zero_grad()
loss = loss_func(bounding_boxes=outputs, ground_truth=labels,batch_size = opt.batch_size )
batch_loss = loss[0]
batch_loss.backward()
optimizer.step()
log_loss_mean_train+=batch_loss
train_loss+=batch_loss
train_loss_coord+=loss[1]
train_loss_confidence+=loss[2]
train_loss_classes+=loss[3]
# train_iou+=train_iou+loss[4]
# train_object_num+=loss[5]
# update weights
if (i + 1) % opt.log_interval == 0:
log_loss_mean_train = log_loss_mean_train / opt.log_interval
info = "訓練:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f}" \
.format \
(
epoch, opt.epochs, i + 1, len(train_loader), log_loss_mean_train
)
print(info, file=fo)
if (opt.show_log_console):
info_print = "\033[0;33;0m" + info + "\033[0m"
print(info_print)
log_loss_mean_train = 0.0
#總體損失
TotalLoss+=train_loss
# tensorboard 繪圖
if (wirter):
wirter.add_scalar("總體損失值",TotalLoss,epoch)
wirter.add_scalar("每輪損失值",train_loss,epoch)
wirter.add_scalar("每輪預測預測框損失值",train_loss_coord,epoch)
wirter.add_scalar("每輪預測框置信度損失",train_loss_confidence,epoch)
wirter.add_scalar("每輪預測類別損失值",train_loss_classes,epoch)
# 儲存效果最好的玩意
if (train_loss < Best_loss):
Best_weight = net.state_dict()
Best_loss = train_loss
scheduler.step() # 更新學習率
# 2.2 進入訓練對比階段
if (epoch + 1) % opt.val_interval == 0:
"""
這部分和訓練的那部分是類似的,可以忽略這部分的程式碼
"""
net.eval()
with torch.no_grad():
for j, data in enumerate(valid_loader):
inputs, labels = data
inputs, labels = inputs.float().to(device), labels.float().to(device)
outputs = net(inputs)
loss = loss_func(outputs, labels)
batch_loss = loss[0]
# log_loss_mean_val += batchLoss
val_loss += batch_loss
val_loss_coord += loss[1]
val_loss_confidence += loss[2]
val_loss_classes += loss[3]
# val_iou += train_iou + loss[4]
# val_object_num += loss[5]
info_val = "測試:\tEpoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} ".format \
(
epoch, opt.epochs, (j+1), len(valid_loader), val_loss
)
print(info_val, file=fo)
if (opt.show_log_console):
info_print_val = "\033[0;31;0m" + info_val + "\033[0m"
print(info_print_val)
ValLoss+=val_loss
if (wirter):
wirter.add_scalar("測試總體損失",ValLoss, (ValTime))
wirter.add_scalar("每次測試總損失總值", val_loss, (ValTime))
wirter.add_scalar("每輪測試預測框損失值", val_loss_coord, ValTime)
wirter.add_scalar("每輪測試預測框置信度損失", val_loss_confidence, ValTime)
wirter.add_scalar("每輪測試預測類別損失值", val_loss_classes, ValTime)
ValTime+=1
# 最後一次的權重
Last_weight = net.state_dict()
# 儲存模型
SaveModel.Save_Model(EPX_Path, Best_weight, Last_weight)
fo.close()
if (wirter):
print("tensorboard dir is:", path_board)
wirter.close()
if name == 'main': parser = argparse.ArgumentParser() parser.add_argument('--epochs', type=int, default=300) parser.add_argument('--batch-size', type=int, default=4) parser.add_argument('--lr', type=float, default=0.01) #每5個batch輸出一次結果 parser.add_argument('--log_interval', type=int, default=2) # 訓練幾輪測試一次 parser.add_argument('--val_interval', type=int, default=10) parser.add_argument('--train_dir_image', type=str, default='') parser.add_argument('--train_dir_Ann', type=str, default='') parser.add_argument('--valid_dir_image', type=str, default='') parser.add_argument('--valid_dir_Ann', type=str, default='') # 如果是Mac系注意這個引數可能需要設定為1,本地訓練,不推薦MAC parser.add_argument('--works', type=int, default=0) parser.add_argument('--show_log_console', type=bool, default=True) parser.add_argument('--device', type=str, default="cpu", help="預設使用顯示卡加速訓練引數選擇:0,1,2...or cpu") parser.add_argument('--tensorboardopen', type=bool, default=True) opt = parser.parse_args() train()
# tensorboard --logdir = runs/train/epx2/logs
```
分類與目標檢測
之後就是咱們的後處理階段,其實也就是咱們的使用部分。
這裡也是兩個部分,一個是圖片分類的實現,還有一個就是咱們目標檢測的實現。
圖片分類
這裡面也是兩個部分,一個是預訓練模型,進行前向傳播,還有一個是進行識別後的處理。
```python import argparse from PIL import Image from Utils.DataSet.MyDataSet import MyDataSet from Utils.DataSet.TransformAtions import TransFormAtions import argparse import torch from torch.utils.data import DataLoader from Models.FeatureNet import YOLOFeature from Config.ConfigPre import * import outProcessClassfiy def detect():
ways = opt.valid_imgs
transformations = TransFormAtions()
net = YOLOFeature(Classes)
state_dict_load = torch.load(opt.path_state_dict)
net.load_state_dict(state_dict_load)
if(ways):
test_data = MyDataSet(data_dir=opt.valid_dir, transform=transformations.valid_transform,ClassesName=ClassesName)
valid_loader = DataLoader(dataset=test_data, batch_size=1)
net.eval()
with torch.no_grad():
for i, data in enumerate(valid_loader):
# forward
inputs, labels = data
outputs = net(inputs)
_, predicted = torch.max(outputs.data, 1)
# 輸出處理器
outProcessClassfiy.Function(predicted.numpy()[0])
else:
#指定的是單張圖片,少給我來奇奇怪怪的輸入,這個版本容錯很差滴!!!
path_img = opt.valid_dir
if(".jpg" not in path_img):
raise Exception("小爺打不開這圖片")
image = Image.open(path_img)
image = transformations.valid_transform(image)
image = torch.reshape(image, (1, 3, 32, 32))
net.eval()
with torch.no_grad():
out = net(image)
outProcessClassfiy.Function(out.argmax(1).item())
if name == 'main': parser = argparse.ArgumentParser() # False表示識別單張圖片,True表示多張圖片,此時指定路徑即可。 parser.add_argument('--valid_imgs',type=bool,default=False) parser.add_argument('--valid_dir', type=str, default=r'F:\projects\PythonProject\HuLook\Data\PreData\train\貓羽雫\1.jpg') parser.add_argument('--path_state_dict', type=str, default=r'runs\trainpre\epx0\weights\best.pth') opt = parser.parse_args() detect()
```
之後是咱們的後處理
```python from Config.ConfigPre import *
def Function(out): print("類別為:", ClassesName[out])
```
目標檢測
這個也是類似的,但是的話,這裡就不去拆什麼後置處理器了哈 那麼這裡要注意的就是編碼的時候opencv是不支援中文的,解決方案的話也不難,需要自己準備一個字型檔案就完了,當然咱們的專案工程裡面是帶了一個的。
```python import cv2 import torchvision.transforms as transforms from Models.Yolo import YOLO import argparse import torch from Config.ConfigTrain import * import numpy as np from PIL import Image,ImageDraw,ImageFont
def iou(box_one, box_two): LX = max(box_one[0], box_two[0]) LY = max(box_one[1], box_two[1]) RX = min(box_one[2], box_two[2]) RY = min(box_one[3], box_two[3]) if LX >= RX or LY >= RY: return 0 return (RX - LX) * (RY - LY) / ((box_one[2]-box_one[0]) * (box_one[3] - box_one[1]) + (box_two[2]-box_two[0]) * (box_two[3] - box_two[1]))
def NMS(bounding_boxes,S=7,B=2,img_size=448,confidence_threshold=0.5,iou_threshold=0.0,possible_pred=0.4): bounding_boxes = bounding_boxes.cpu().detach().numpy().tolist() predict_boxes = [] nms_boxes = [] grid_size = img_size / S for batch in range(len(bounding_boxes)): for i in range(S): for j in range(S): gridX = grid_size * j gridY = grid_size * i if bounding_boxes[batch][i][j][4] < bounding_boxes[batch][i][j][9]: bounding_box = bounding_boxes[batch][i][j][5:10] else: bounding_box = bounding_boxes[batch][i][j][0:5] class_possible = (bounding_boxes[batch][i][j][10:]) bounding_box.extend(class_possible) possible = max(class_possible) if (bounding_box[4] < confidence_threshold
):
continue
if(bounding_box[4]*possible < possible_pred):
continue
# print(bounding_box[4]*possible)
centerX = (int)(gridX + bounding_box[0] * grid_size)
centerY = (int)(gridY + bounding_box[1] * grid_size)
width = (int)(bounding_box[2] * img_size)
height = (int)(bounding_box[3] * img_size)
bounding_box[0] = max(0, (int)(centerX - width / 2))
bounding_box[1] = max(0, (int)(centerY - height / 2))
bounding_box[2] = min(img_size - 1, (int)(centerX + width / 2))
bounding_box[3] = min(img_size - 1, (int)(centerY + height / 2))
predict_boxes.append(bounding_box)
while len(predict_boxes) != 0:
predict_boxes.sort(key=lambda box:box[4])
assured_box = predict_boxes[0]
temp = []
classIndex = np.argmax(assured_box[5:])
#print("類別:{}".format(ClassesName[classIndex))
assured_box[4] = assured_box[4] * assured_box[5 + classIndex]
#修正置信度為 物體分類準確度 × 含有物體的置信度
assured_box[5] = classIndex
nms_boxes.append(assured_box)
i = 1
while i < len(predict_boxes):
if iou(assured_box,predict_boxes[i]) <= iou_threshold:
temp.append(predict_boxes[i])
i = i + 1
predict_boxes = temp
return nms_boxes
def detect(): transform = transforms.Compose([ transforms.ToTensor(), # height * width * channel -> channel * height * width transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) ])
image_dir = opt.valid_dir
img_data = cv2.imread(image_dir)
img_data = cv2.resize(img_data, (448, 448), interpolation=cv2.INTER_AREA)
train_data = transform(img_data)
train_data = train_data.unsqueeze(0)
net = YOLO(B=2,classes_num=Classes)
state_dict_load = torch.load(opt.path_state_dict)
net.load_state_dict(state_dict_load)
net.eval()
with torch.no_grad():
bounding_boxes = net(train_data)
NMS_boxes = NMS(bounding_boxes,confidence_threshold=opt.confidence,iou_threshold=opt.iou,possible_pred=opt.possible_pre)
font = ImageFont.truetype(r'font/simsun.ttc', 20, encoding='utf-8')
for box in NMS_boxes:
img_data = cv2.rectangle(img_data, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 1)
"""
處理中文
"""
pil_img = Image.fromarray(cv2.cvtColor(img_data, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_img)
draw.text((box[0], box[1]),"{}:{}".format(ClassesName[box[5]], round(box[4], 2)),(148,175,100),font)
print("class_name:{} confidence:{}".format(ClassesName[int(box[5])],round(box[4],2)))
img_data = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
if(opt.show_img):
cv2.imshow("img_detection", img_data)
cv2.waitKey()
cv2.destroyAllWindows()
if(opt.save_dir):
cv2.imwrite(opt.save_dir, img_data)
if name == 'main': parser = argparse.ArgumentParser() parser.add_argument('--valid_dir', type=str, default=r'F:\projects\PythonProject\HuLook\Data\DetData\train\images\002.jpg') parser.add_argument('--path_state_dict', type=str, default=r'F:\projects\PythonProject\HuLook\runs\traindetect\epx0\weights\best.pth') parser.add_argument("--iou",type=float,default=0.2) parser.add_argument("--confidence",type=float,default=0.5) parser.add_argument("--possible_pre",type=float,default=0.35) parser.add_argument("--show_img",type=bool,default=True) parser.add_argument("--save_dir",type=str,default="") opt = parser.parse_args() detect()
```
專案獲取
那麼整個玩意咱們就搞定了,考慮到特殊原因,專案上傳至碼雲:https://gitee.com/Huterox/hu-look
此外由於咱們訓練出來的權重檔案太大了,所以這理的話就不上傳入權重檔案了。
當然其實還有一個原因是,咱們的這個權重檔案只是用來做測試的,所以實際的意義不大。
不過你以為這就完了嘛,不,接下來是咱們的這個玩意如何使用!
專案使用
預訓練資料集
這個的話其實可以考慮省去,我們可以選擇直接訓練,問題不大。
這個預訓練資料集就和前面說的一樣,按照類別放在不同的資料夾下面。
例如我這裡準備這幾種圖片:
(我這個是用於測試的資料集所以很小,就幾十張圖片)
預訓練
這部分的話需要開啟配置
配置一下就好了
當然在訓練檔案當中也是可以配置的
訓練完畢後,你可以開啟tensorboad
我們的訓練過程當中的資料都在這兒
之後的話,預訓練完之後,這個網路是具備圖片分類功能的,可以使用
進行圖片分類。
不過這裡注意的是,預訓練的只是一個用於分類的網路,目的為了讓骨幹網路具備權重。所以準備的資料集最好是一張圖片裡面只有一個目標,因為那玩意只是用來分類的。
目標檢測資料集
這部分的話就是咱們的voc資料集,和正常的一樣就可以了,咱們可以直接使用labelimg進行標註。 那個怎麼使用前面的部落格有,那麼在咱們這裡的話還是需要手動劃分一下訓練集和驗證集的。
然後裡面的內容就和voc一樣了
訓練目標檢測
之後就是咱們的訓練
還是先到配置處
之後開啟tensorboard
python
tensorboard --logdir=runs/traindetect/epx0/logs
識別
這個就不用我說了,開啟detect
我們可以看到是識別的情況
這裡的話由於咱們的資料集太那啥了,而且資料集本身設定的就不好,所以導致這裡的效果也不好,同時這其實我不上傳權重的原因之一,只是用來做測試的。
總結
以上就是全部內容了,全網應該找不到比我這個還全的了吧?如果闊以的話給個start~
- 還在調API寫所謂的AI“女友”,嘮了嘮了,教你基於python咱們“new”一個(深度學習)
- Java前後端分離實戰Auto2.0使用者登入註冊--基本的使用者登入 郵箱驗證
- 卡爾曼濾波器(目標跟蹤一)(上)
- 手把手教你如何自制目標檢測框架(從理論到實現)
- 基於Python深度圖生成3D點雲
- Pandas基礎使用(機器學習基礎)
- CEC2017基礎函式說明Python版本
- 全國空氣質量爬取實戰
- 智慧演算法整合測試平臺V0.1實戰開發
- DDPG神經網路實戰(基於強化學習優化粒子群演算法)
- 關於強化學習優化粒子群演算法的論文解讀(全)
- 關於強化學習優化粒子群演算法的論文解讀(上)
- 基於多種群機制的PSO演算法(優化與探索三 *混合種群思想優化多種群與廣義PSO求解JSP)
- 基於多種群機制的PSO演算法Python實現(優化與探索二)
- 基於多種群機制的PSO演算法Python實現
- 520桌面手勢告白
- 嘿~瞎話一下,為啥要用Sigmoid?!