Pytorch搭建SSD目标检测平台
- 學習前言
- 什么是SSD目標檢測算法
- 源碼下載
- SSD實現(xiàn)思路
-
- 一、預(yù)測部分
-
- 1、主干網(wǎng)絡(luò)介紹
- 2、從特征獲取預(yù)測結(jié)果
- 3、預(yù)測結(jié)果的解碼
- 4、在原圖上進行繪制
- 二、訓(xùn)練部分
-
- 1、真實框的處理
- 2、利用處理完的真實框與對應(yīng)圖片的預(yù)測結(jié)果計算loss
- 訓(xùn)練自己的ssd模型
?
學習前言
一起來看看SSD的Pytorch實現(xiàn)吧,順便訓(xùn)練一下自己的數(shù)據(jù)。
什么是SSD目標檢測算法
SSD是一種非常優(yōu)秀的one-stage目標檢測方法,one-stage算法就是目標檢測和分類是同時完成的,其主要思路是利用CNN提取特征后,均勻地在圖片的不同位置進行密集抽樣,抽樣時可以采用不同尺度和長寬比,物體分類與預(yù)測框的回歸同時進行,整個過程只需要一步,所以其優(yōu)勢是速度快。
但是均勻的密集采樣的一個重要缺點是訓(xùn)練比較困難,這主要是因為正樣本與負樣本(背景)極其不均衡(參見Focal Loss),導(dǎo)致模型準確度稍低。
SSD的英文全名是Single Shot MultiBox Detector,Single shot說明SSD算法屬于one-stage方法,MultiBox說明SSD算法基于多框預(yù)測。
源碼下載
https://github.com/bubbliiiing/ssd-pytorch
喜歡的可以點個star噢。
SSD實現(xiàn)思路
一、預(yù)測部分
1、主干網(wǎng)絡(luò)介紹
SSD采用的主干網(wǎng)絡(luò)是VGG網(wǎng)絡(luò),關(guān)于VGG的介紹大家可以看我的另外一篇博客https://blog.csdn.net/weixin_44791964/article/details/102779878,這里的VGG網(wǎng)絡(luò)相比普通的VGG網(wǎng)絡(luò)有一定的修改,主要修改的地方就是:
1、將VGG16的FC6和FC7層轉(zhuǎn)化為卷積層。
2、去掉所有的Dropout層和FC8層;
3、新增了Conv6、Conv7、Conv8、Conv9。
如圖所示,輸入的圖片經(jīng)過了改進的VGG網(wǎng)絡(luò)(Conv1->fc7)和幾個另加的卷積層(Conv6->Conv9),進行特征提取:
a、輸入一張圖片后,被resize到300x300的shape
b、conv1,經(jīng)過兩次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為64,輸出為(300,300,64),再2X2最大池化,輸出net為(150,150,64)。
c、conv2,經(jīng)過兩次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為128,輸出net為(150,150,128),再2X2最大池化,輸出net為(75,75,128)。
d、conv3,經(jīng)過三次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為256,輸出net為(75,75,256),再2X2最大池化,輸出net為(38,38,256)。
e、conv4,經(jīng)過三次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為512,輸出net為(38,38,512),再2X2最大池化,輸出net為(19,19,512)。
f、conv5,經(jīng)過三次[3,3]卷積網(wǎng)絡(luò),輸出的特征層為512,輸出net為(19,19,512),再步長為1,卷積核大小為3X3最大池化,輸出net為(19,19,512)。
g、利用卷積代替全連接層,進行了一次[3,3]卷積網(wǎng)絡(luò)和一次[1,1]卷積網(wǎng)絡(luò),輸出的特征層為1024,因此輸出的net為(19,19,1024)。(從這里往前都是VGG的結(jié)構(gòu))
h、conv6,經(jīng)過一次[1,1]卷積網(wǎng)絡(luò),調(diào)整通道數(shù),一次步長為2的[3,3]卷積網(wǎng)絡(luò),輸出的特征層為512,因此輸出的net為(10,10,512)。
i、conv7,經(jīng)過一次[1,1]卷積網(wǎng)絡(luò),調(diào)整通道數(shù),一次步長為2的[3,3]卷積網(wǎng)絡(luò),輸出的特征層為256,因此輸出的net為(5,5,256)。
j、conv8,經(jīng)過一次[1,1]卷積網(wǎng)絡(luò),調(diào)整通道數(shù),一次padding為valid的[3,3]卷積網(wǎng)絡(luò),輸出的特征層為256,因此輸出的net為(3,3,256)。
k、conv9,經(jīng)過一次[1,1]卷積網(wǎng)絡(luò),調(diào)整通道數(shù),一次padding為valid的[3,3]卷積網(wǎng)絡(luò),輸出的特征層為256,因此輸出的net為(1,1,256)。
實現(xiàn)代碼:
base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',512, 512, 512]def vgg(i):layers = []in_channels = ifor v in base:if v == 'M':layers += [nn.MaxPool2d(kernel_size=2, stride=2)]elif v == 'C':layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]else:conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)layers += [conv2d, nn.ReLU(inplace=True)]in_channels = vpool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)conv7 = nn.Conv2d(1024, 1024, kernel_size=1)layers += [pool5, conv6,nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]return layersdef add_extras(i, batch_norm=False):# Extra layers added to VGG for feature scalinglayers = []in_channels = i# Block 6# 19,19,1024 -> 10,10,512layers += [nn.Conv2d(in_channels, 256, kernel_size=1, stride=1)]layers += [nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)]# Block 7# 10,10,512 -> 5,5,256layers += [nn.Conv2d(512, 128, kernel_size=1, stride=1)]layers += [nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)]# Block 8# 5,5,256 -> 3,3,256layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]# Block 9# 3,3,256 -> 1,1,256layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]return layers2、從特征獲取預(yù)測結(jié)果
由上圖我們可以知道,我們分別取conv4的第三次卷積的特征、fc7的特征、conv6的第二次卷積的特征、conv7的第二次卷積的特征、conv8的第二次卷積的特征、conv9的第二次卷積的特征,為了和普通特征層區(qū)分,我們稱之為有效特征層,來獲取預(yù)測結(jié)果。
對獲取到的每一個有效特征層,我們分別對其進行一次num_priors x 4的卷積、一次num_priors x num_classes的卷積、并需要計算每一個有效特征層對應(yīng)的先驗框。而num_priors指的是該特征層所擁有的先驗框數(shù)量。
其中:
num_priors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。(為什么說是變化情況呢,這是因為ssd的預(yù)測結(jié)果需要結(jié)合先驗框獲得預(yù)測框,預(yù)測結(jié)果就是先驗框的變化情況。)
num_priors x num_classes的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個預(yù)測框對應(yīng)的種類。
每一個有效特征層對應(yīng)的先驗框?qū)?yīng)著該特征層上 每一個網(wǎng)格點上 預(yù)先設(shè)定好的多個框。
所有的特征層對應(yīng)的預(yù)測結(jié)果的shape如下:
實現(xiàn)代碼為:
3、預(yù)測結(jié)果的解碼
我們通過對每一個特征層的處理,可以獲得三個內(nèi)容,分別是:
num_priors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。**
num_priors x num_classes的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個預(yù)測框對應(yīng)的種類。
每一個有效特征層對應(yīng)的先驗框?qū)?yīng)著該特征層上 每一個網(wǎng)格點上 預(yù)先設(shè)定好的多個框。
我們利用 num_priors x 4的卷積 與 每一個有效特征層對應(yīng)的先驗框 獲得框的真實位置。
每一個有效特征層對應(yīng)的先驗框就是,如圖所示的作用:
每一個有效特征層將整個圖片分成與其長寬對應(yīng)的網(wǎng)格,如conv4-3的特征層就是將整個圖像分成38x38個網(wǎng)格;然后從每個網(wǎng)格中心建立多個先驗框,如conv4-3的特征層就是建立了4個先驗框;對于conv4-3的特征層來講,整個圖片被分成38x38個網(wǎng)格,每個網(wǎng)格中心對應(yīng)4個先驗框,一共包含了,38x38x4個,5776個先驗框。
先驗框雖然可以代表一定的框的位置信息與框的大小信息,但是其是有限的,無法表示任意情況,因此還需要調(diào)整,ssd利用num_priors x 4的卷積的結(jié)果對先驗框進行調(diào)整。
num_priors x 4中的num_priors表示了這個網(wǎng)格點所包含的先驗框數(shù)量,其中的4表示了x_offset、y_offset、h和w的調(diào)整情況。
x_offset與y_offset代表了真實框距離先驗框中心的xy軸偏移情況。
h和w代表了真實框的寬與高相對于先驗框的變化情況。
SSD解碼過程就是將每個網(wǎng)格的中心點加上它對應(yīng)的x_offset和y_offset,加完后的結(jié)果就是預(yù)測框的中心,然后再利用 先驗框和h、w結(jié)合 計算出預(yù)測框的長和寬。這樣就能得到整個預(yù)測框的位置了。
當然得到最終的預(yù)測結(jié)構(gòu)后還要進行得分排序與非極大抑制篩選這一部分基本上是所有目標檢測通用的部分。
1、取出每一類得分大于self.obj_threshold的框和得分。
2、利用框的位置和得分進行非極大抑制。
實現(xiàn)代碼如下:
4、在原圖上進行繪制
通過第三步,我們可以獲得預(yù)測框在原圖上的位置,而且這些預(yù)測框都是經(jīng)過篩選的。這些篩選后的框可以直接繪制在圖片上,就可以獲得結(jié)果了。
二、訓(xùn)練部分
1、真實框的處理
從預(yù)測部分我們知道,每個特征層的預(yù)測結(jié)果,num_priors x 4的卷積 用于預(yù)測 該特征層上 每一個網(wǎng)格點上 每一個先驗框的變化情況。
也就是說,我們直接利用ssd網(wǎng)絡(luò)預(yù)測到的結(jié)果,并不是預(yù)測框在圖片上的真實位置,需要解碼才能得到真實位置。
而在訓(xùn)練的時候,我們需要計算loss函數(shù),這個loss函數(shù)是相對于ssd網(wǎng)絡(luò)的預(yù)測結(jié)果的。我們需要把圖片輸入到當前的ssd網(wǎng)絡(luò)中,得到預(yù)測結(jié)果;同時還需要把真實框的信息,進行編碼,這個編碼是把真實框的位置信息格式轉(zhuǎn)化為ssd預(yù)測結(jié)果的格式信息。
也就是,我們需要找到 每一張用于訓(xùn)練的圖片的每一個真實框?qū)?yīng)的先驗框,并求出如果想要得到這樣一個真實框,我們的預(yù)測結(jié)果應(yīng)該是怎么樣的。
從預(yù)測結(jié)果獲得真實框的過程被稱作解碼,而從真實框獲得預(yù)測結(jié)果的過程就是編碼的過程。
因此我們只需要將解碼過程逆過來就是編碼過程了。
實現(xiàn)代碼如下:
def encode(matched, priors, variances):g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]g_cxcy /= (variances[0] * priors[:, 2:])g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]g_wh = torch.log(g_wh) / variances[1]return torch.cat([g_cxcy, g_wh], 1)在訓(xùn)練的時候我們只需要選擇iou最大的先驗框就行了,這個iou最大的先驗框就是我們用來預(yù)測這個真實框所用的先驗框。
因此我們還要經(jīng)過一次篩選,將上述代碼獲得的真實框?qū)?yīng)的所有的iou較大先驗框的預(yù)測結(jié)果中,iou最大的那個篩選出來。
實現(xiàn)代碼如下:
def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):# 計算所有的先驗框和真實框的重合程度overlaps = jaccard(truths,point_form(priors))# 所有真實框和先驗框的最好重合程度# [truth_box,1]best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)best_prior_idx.squeeze_(1)best_prior_overlap.squeeze_(1)# 所有先驗框和真實框的最好重合程度# [1,prior]best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)best_truth_idx.squeeze_(0)best_truth_overlap.squeeze_(0)# 找到與真實框重合程度最好的先驗框,用于保證每個真實框都要有對應(yīng)的一個先驗框best_truth_overlap.index_fill_(0, best_prior_idx, 2)# 對best_truth_idx內(nèi)容進行設(shè)置for j in range(best_prior_idx.size(0)):best_truth_idx[best_prior_idx[j]] = j# 找到每個先驗框重合程度最好的真實框matches = truths[best_truth_idx] # Shape: [num_priors,4]conf = labels[best_truth_idx] + 1 # Shape: [num_priors]# 如果重合程度小于threhold則認為是背景conf[best_truth_overlap < threshold] = 0 # label as backgroundloc = encode(matches, priors, variances)loc_t[idx] = loc # [num_priors,4] encoded offsets to learnconf_t[idx] = conf # [num_priors] top class label for each prior2、利用處理完的真實框與對應(yīng)圖片的預(yù)測結(jié)果計算loss
loss的計算分為三個部分:
1、獲取所有正標簽的框的預(yù)測結(jié)果的回歸loss。
2、獲取所有正標簽的種類的預(yù)測結(jié)果的交叉熵loss。
3、獲取一定負標簽的種類的預(yù)測結(jié)果的交叉熵loss。
由于在ssd的訓(xùn)練過程中,正負樣本極其不平衡,即 存在對應(yīng)真實框的先驗框可能只有十來個,但是不存在對應(yīng)真實框的負樣本卻有幾千個,這就會導(dǎo)致負樣本的loss值極大,因此我們可以考慮減少負樣本的選取,對于ssd的訓(xùn)練來講,常見的情況是取三倍正樣本數(shù)量的負樣本用于訓(xùn)練。這個三倍呢,也可以修改,調(diào)整成自己喜歡的數(shù)字。
實現(xiàn)代碼如下:
class MultiBoxLoss(nn.Module):def __init__(self, num_classes, overlap_thresh, prior_for_matching,bkg_label, neg_mining, neg_pos, neg_overlap, encode_target,use_gpu=True):super(MultiBoxLoss, self).__init__()self.use_gpu = use_gpuself.num_classes = num_classesself.threshold = overlap_threshself.background_label = bkg_labelself.encode_target = encode_targetself.use_prior_for_matching = prior_for_matchingself.do_neg_mining = neg_miningself.negpos_ratio = neg_posself.neg_overlap = neg_overlapself.variance = Config['variance']def forward(self, predictions, targets):# 回歸信息,置信度,先驗框loc_data, conf_data, priors = predictions# 計算出batch_sizenum = loc_data.size(0)# 取出所有的先驗框priors = priors[:loc_data.size(1), :]# 先驗框的數(shù)量num_priors = (priors.size(0))num_classes = self.num_classes# 創(chuàng)建一個tensor進行處理loc_t = torch.Tensor(num, num_priors, 4)conf_t = torch.LongTensor(num, num_priors)for idx in range(num):# 獲得框truths = targets[idx][:, :-1].data# 獲得標簽labels = targets[idx][:, -1].data# 獲得先驗框defaults = priors.data# 找到標簽對應(yīng)的先驗框match(self.threshold, truths, defaults, self.variance, labels,loc_t, conf_t, idx)if self.use_gpu:loc_t = loc_t.cuda()conf_t = conf_t.cuda()# 轉(zhuǎn)化成Variableloc_t = Variable(loc_t, requires_grad=False)conf_t = Variable(conf_t, requires_grad=False)# 所有conf_t>0的地方,代表內(nèi)部包含物體pos = conf_t > 0# 求和得到每一個圖片內(nèi)部有多少正樣本num_pos = pos.sum(dim=1, keepdim=True)# 計算回歸losspos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)loc_p = loc_data[pos_idx].view(-1, 4)loc_t = loc_t[pos_idx].view(-1, 4)loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)# 轉(zhuǎn)化形式batch_conf = conf_data.view(-1, self.num_classes)# 你可以把softmax函數(shù)看成一種接受任何數(shù)字并轉(zhuǎn)換為概率分布的非線性方法# 獲得每個框預(yù)測到真實框的類的概率loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))loss_c = loss_c.view(num, -1)loss_c[pos] = 0 # 獲得每一張圖新的softmax的結(jié)果_, loss_idx = loss_c.sort(1, descending=True)_, idx_rank = loss_idx.sort(1)# 計算每一張圖的正樣本數(shù)量num_pos = pos.long().sum(1, keepdim=True)# 限制負樣本數(shù)量num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)neg = idx_rank < num_neg.expand_as(idx_rank)# 計算正樣本的loss和負樣本的losspos_idx = pos.unsqueeze(2).expand_as(conf_data)neg_idx = neg.unsqueeze(2).expand_as(conf_data)conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)targets_weighted = conf_t[(pos+neg).gt(0)]loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)# Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / NN = num_pos.data.sum()loss_l /= Nloss_c /= Nreturn loss_l, loss_c訓(xùn)練自己的ssd模型
ssd整體的文件夾構(gòu)架如下:
本文使用VOC格式進行訓(xùn)練。
訓(xùn)練前將標簽文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
訓(xùn)練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
在訓(xùn)練前利用voc2ssd.py文件生成對應(yīng)的txt。
再運行根目錄下的voc_annotation.py,運行前需要將classes改成你自己的classes。
- 1
就會生成對應(yīng)的2007_train.txt,每一行對應(yīng)其圖片位置及其真實框的位置。
在訓(xùn)練前需要修改model_data里面的voc_classes.txt文件,需要將classes改成你自己的classes。
還有config文件下面的Num_Classes,修改成分類的數(shù)量+1。
運行train.py即可開始訓(xùn)練。
總結(jié)
以上是生活随笔為你收集整理的Pytorch搭建SSD目标检测平台的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Pytorch搭建Faster R-CN
- 下一篇: Pytorch搭建yolo3目标检测平台