今天,給大家分享一篇來自知乎的一篇關于目標檢測相關的一些內容,
本文基于Pytorch進行編寫。
在閱讀這篇博文之前,如果讀者真的是還沒有接觸過這個目標檢測的話,作者建議可以先看看這幾篇文章再來:
GitHub 水項目之 快速上手 YOLOV5
(https://blog.csdn.net/FUTEROX/article/details/124079281)
YOLOV5 參數設定與模型訓練的坑點一二三
(https://blog.csdn.net/FUTEROX/article/details/124079281)
YOLOV1論文小整理
(https://blog.csdn.net/FUTEROX/article/details/124111506)
在代碼部分還參考了原先這篇博文的設計:
嘿~全流程帶你基于Pytorch手擼圖片分類“框架“--HuClassify
那么本文兩個目標:
一. 理論
搞清楚什么是目標檢測
目標檢測的重難點
相關目標檢測算法思想
如何設計一個目標檢測算法
二. 編碼
voc數據集的細節
目標檢測網絡
目標分類網絡
相關算法
其中的理論部分像我說不會太深入只是快速入門,編碼部分的話倒是有很多相關算法的實現。那么編碼的話在目標檢測部分的網絡,我們也是直接使用yolo的網絡,當然這里還是會做改動的。這篇博文的更多的一個目的其實還是說搭建一個簡單的目標檢測平臺,這樣感興趣的朋友可以自己DIY,對我本人的話也是有DIY的需求。
那么廢話不多說,馬上發車了!
目標檢測
要說到目標檢測的話,那么我們就不得不先說到圖片分類了。
因為圖片分類在我們的目標檢測當中是非常重要的但是二者的區別也是存在的,不過他們之間卻有很多相似的地方。
圖片分類
圖片分類是一個非常經典的問題,給定一張圖片然后對這個圖片進行分類,它的任務非常簡單,并且設計一個這樣的網絡也非常簡單。
你只需要使用一定量的卷積,最后和一定量的全連接網絡輸出一組大小和類別的最后一個維度一樣的tensor就行了,然后使用交叉熵作為你的損失函數。
比如最簡單的分類網絡:LeNet
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幾個卷積,后面兩個一個是特征提取網絡一個是我們用于目標識別的網絡。我們預訓練是訓練特征提取網絡,這個網絡是依托與骨干網絡的。他們之間的關系是這樣的:
特征提取網絡其實就是在骨干網絡的基礎上用于分類,這樣一來就得到了權重,當我們訓練目標檢測網絡的時候,我們可以把先前預訓練的特征網絡當中的骨干網絡的權重提取出來作為初始化權重,這也就是遷移學習。
骨干網絡
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), )
這里可以看到這個網絡啥也沒有,就是一個最基本的骨架。
特征提取網絡(預訓練)
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()
目標檢測網絡
最后是咱們的目標檢測網絡。
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)
這里要特別注意我注釋的這段代碼:
# x[:,:,:, 0 : self.B * 5] = self.sigmoid(x[:,:,:, 0 : self.B * 5]) # x[:,:,:, self.B * 5 : ] = self.softmax(x[:,:,:, self.B * 5 : ])
接下來我會更加詳細地說明
數據集編碼
現在我們已經知道了咱們這邊的目的有兩個,一個是要預訓練,一個是要目標檢測
預訓練數據集
其中咱們的預訓練是訓練一個基本的過程。
那么在這里的話,其實很簡單,我們訓練的話我們只需要把那個特征網絡拿過來,重點是咱們的這個預訓練數據集怎么來。
那么這邊的話,如果是老盆友,或者是看來剛剛開頭推薦觀看的文章的朋友應該知道,這邊的話我們可以直接把咱們的HuDataSet拿過來。
首先這個數據集的定義非常簡單:
相信你一眼就知道了是怎么一回事。分訓練很驗證集,然后每個分類的標簽放在對應的文件夾下面就可以了。
核心代碼如下:
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文件
里面包括了類別和手動標注的框的位置。
images 001.jpg F:projectsPythonProjectyolov5-5.0mydataimages01.jpg 1200 701 3 0
由于我們需要進行目標檢測,但是呢,我們除了要提取里面的標簽信息的話,還要把里面的標簽(類別,方框)信息進行轉化,轉化的目的也是為了復合神經網絡的輸出方便損失函數計算。
VOC標簽解析
解析的話很簡單,就這個
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])
完整與之配合的代碼是這樣的:
這里還使用了部分數據增強
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
格式轉化
現在請把目光轉移到這里來:
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低的也算是負樣本。
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: 由于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
那么在這里的話我也要說說,剛剛注釋的這個代碼:
# 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。
這個論文當中也有描述。
訓練部分
接下來是咱們的訓練部分,這個呢,有兩個一個是預訓練一個是實際訓練。
預訓練得到的一個模型還可以用于圖片分類。
預訓練
這部分其實很簡單就不多說了。
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("33[0;31;0m使用GPU訓練中:{}33[0m".format(torch.cuda.get_device_name())) else: device = torch.device("cpu") print("33[0;31;40m使用CPU訓練33[0m") else: device = torch.device("cpu") print("33[0;31;40m使用CPU訓練33[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 = "33[0;33;0m" + info + "33[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 = "測試: Epoch[{: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 = "33[0;31;0m" + info_val + "33[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
這部分還是簡單的。
目標檢測訓練
之后就是咱們目標檢測算法的實現,這個其實核心流程都是一樣的,就是多了一些東西用來做記錄
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("33[0;31;0m使用GPU訓練中:{}33[0m".format(torch.cuda.get_device_name())) else: device = torch.device("cpu") print("33[0;31;40m使用CPU訓練33[0m") else: device = torch.device("cpu") print("33[0;31;40m使用CPU訓練33[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 = "33[0;33;0m" + info + "33[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 = "測試: Epoch[{: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 = "33[0;31;0m" + info_val + "33[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
分類與目標檢測
之后就是咱們的后處理階段,其實也就是咱們的使用部分。
這里也是兩個部分,一個是圖片分類的實現,還有一個就是咱們目標檢測的實現。
圖片分類
這里面也是兩個部分,一個是預訓練模型,進行前向傳播,還有一個是進行識別后的處理。
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:projectsPythonProjectHuLookDataPreData rain貓羽雫1.jpg') parser.add_argument('--path_state_dict', type=str, default=r'runs rainpreepx0weightsest.pth') opt = parser.parse_args() detect()
之后是咱們的后處理
from Config.ConfigPre import * def Function(out): print("類別為:", ClassesName[out])
目標檢測
這個也是類似的,但是的話,這里就不去拆什么后置處理器了哈
那么這里要注意的就是編碼的時候opencv是不支持中文的,解決方案的話也不難,需要自己準備一個字體文件就完了,當然咱們的項目工程里面是帶了一個的。
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:projectsPythonProjectHuLookDataDetData rainimages02.jpg') parser.add_argument('--path_state_dict', type=str, default=r'F:projectsPythonProjectHuLook uns raindetectepx0weightsest.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
tensorboard --logdir=runs/traindetect/epx0/logs
識別
這個就不用我說了,打開detect
我們可以看到這識別的情況
這里的話由于咱們的數據集太那啥了,而且數據集本身設置的就不好,所以導致這里的效果也不好,同時這其實我不上傳權重的原因之一,只是用來做測試的。
總結
以上就是全部內容了,全網應該找不到比這個還全的了吧?
-
目標檢測
+關注
關注
0文章
209瀏覽量
15611 -
pytorch
+關注
關注
2文章
808瀏覽量
13225
原文標題:近兩萬字長文,從理論到實現!手把手教你如何自制目標檢測框架
文章出處:【微信號:vision263com,微信公眾號:新機器視覺】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論