定位+分类
定位+分类问题是分类到目标检测的一个过渡问题,从单纯地图片分类到分类后给出目标所处的位置,再到多目标的类别和位置,定位问题需要模型返回目标所在的外界矩形框,即目标的(x,y,w,h)四元组。
将定位当作回归问题,具体步骤如下。
1)训练(或下载)一个分类模型,例如,AlexNet、VGGNet 或ResNet。
2)在分类网络最后一个卷积层的特征层(featuremap)上添加“regression head"
神经网络中不同的“head”通常用来训练不同的目标,每个“head”的损失函数和优化方向均不相同。如果你想让一个网络实现多个功能,那么通常是在神经网络后面接多个不同功能的“head”
3)同时训练“classificationhead”和“regression head”,为了同时训练分类和定位(定位是回归问题)两个问题,最终损失函数是分类和定位两个“head”产生的损失的加权和。
4)在预测时同时使用分类和回归“head”得到分类+定位的结果。这里需要强调一下的是,分类预测出的结果就是C个类别,回归预测的结果可能有两种:一种是类别无关,输出4个值;一种是类别相关,输出4*C个值
目标检测
目标检测需要获取图片中所有目标的位置及其类别,当图片中只有一个目标时,“regressionhead”预测4个值,当图片中有3个目标时,“regression head”预测12个值,
R-CNN
训练过程
(1)选出潜在目标候选框(ROI)
R-CNN使用selective search的方法选出了2000个潜在物体候选框。
(2)训练一个好的特征提取器
R-CNN的提出者使用卷积神经网络AlexNet提取4096维的特征向量,使用 VGGNet、GoogLeNet或ResNet等也可以。AlexNet等网络要求输入的图片尺寸是固定的,ROI尺寸大小不定,这就需要将每个ROI调整到指定尺寸,调整的方法有很多种,包含上下文的尺寸调整,不包含上下文的尺寸调整,尺度缩放。
接下来,为了获得一个好的特征提取器,一般会在ImageNet预训练好的模型基础上做调整(因为ImageNet预测的种类较多,特征学习相对比较完善),唯一的改动就是将ImageNet中的1000个类别的输出改为(C+1)个输出,其中,C是真实需要预测的类别个数,1是背景类
特征的训练方法是使用随机梯度下降(Stochastic Gradient Descent,SGD),与前几章介绍的普通神经网络的训练方法相同。
提到训练,就一定要有正样本和负样本,这里先抛出一个用于衡量两个矩形交叠情况的指标:IOU(Intersection Over Union)。IOU其实就是两个矩形面积的交集除以并集,一般情况下,当IOU>=0.5时,可以认为两个矩形基本相交,所以在这个任务中,假定在两个矩形框中,1个矩形代表ROI.另一个代表真实的矩形框,那么当ROI和真实矩形框的IOU>=0.5时则认为是正样本,其余为负样本。
至此,R-CNN的第二步特征提取器就可以开始训练了,不过在训练过程中应注意,需要对负样本进行采样,因为训练数据中正样本太少会导致正负样本极度不平衡。最终在该步得到的是一个卷积神经网络的特征提取器,其特征是一个4096维特征向量。
(3)训练最终的分类器
下面为每个类别单独训练一个SVM分类器。SVM的训练也需要选择正负样本,R-CNN的提出者做了一个实验来选择最优IOU阈值,最终仅仅选择真实值的矩形框作为正样本。
FastR-CNN和FasterR-CNN是根据IOU的大小选取正负样本
(4)训练回归模型
为每个类训练一个回归模型,用来微调ROI与真实矩形框位置和大小的偏差
IOU的计算
def bboxIOU (bboxA, bboxB): A_xmin = bboxA[0] A_ymin = bboxA[1] A_xmax = bboxA[2] A_ymax = bboxA[3] A_width = A_xmax - A_xmin A_height = A_ymax - A_ymin B_xmin = bboxB[0] B_ymin = bboxB[1] B_xmax = bboxB[2] B_ymax = bboxB[3] B_width = B_xmax - B_xmin B_height = B_ymax - B_ymin xmin = min(A_xmin, B_xmin) ymin = min(A_ymin, B_ymin) xmax = max(A_xmax, B_xmax) ymax = max(A_ymax, B_ymax) A_width_and = (A_width + B_width) - (xmax - xmin) #宽的交集 A_height_and = (A_height + B_height) - (ymax - ymin) #高的交集 if ( A_width_and <= 0.0001 or A_height_and <= 0.0001): return 0 area_and = (A_width_and * A_height_and) area_or = (A_width * A_height) + (B_width * B_height) IOU = area_and / (area_or - area_and) return IOU
预测阶段可分为如下几个步骤。
1)使用selective search方法先选出2000个ROI。
2)所有ROI调整为特征提取网络所需的输入大小并进行特征提取,得到与2000个 ROI对应的2000个4096维的特征向量。
3)将2000个特征向量分别输人到SVM中,得到每个ROI预测的类别。
4)通过回归网络微调ROI的位置。
5)最终使用非极大值抑制(Non-MaximumSuppression,NMS)方法对同一个类别的 ROI进行合并得到最终检测结果。NMS的原理是得到每个矩形框的分数(置信度 置信度通常指的是模型对其预测结果的信心程度。具体来说,在分类任务中,置信度表示模型对某个类别的预测概率。),如果两个矩形框的IOU超过指定阈值,则仅仅保留分数大的那个矩形框。
缺点
不论是训练还是预测,都需要对selective search出来的2000个 ROI全部通过CNN的 Forward过程来获取特征,这个过程花费的时间会非常长。
卷积神经网络的特征提取器和用来预测分类的SVM是分开的,也就是特征提取的过程不会因SVM和回归的调整而更新。
R-CNN具有非常复杂的操作流程,而且每一步都是分裂的,如特征提取器通过 Softmax分类获得,最终的分类结果由SVM获得,矩形框的位置则是通过回归方式获得。
Fast R-CNN
具体训练步骤如下。
1)将整张图片和ROI直接输人到全卷积的CNN中,得到特征层和对应在特征层上的ROI(特征层的ROI信息可用其几何位置加卷积坐标公式推导得出)。
2)与R-CNN类似,为了使不同尺寸的ROI可以统一进行训练,FastR-CNN将每块候选区域通过池化的方法调整到指定的M*N,此时特征层上将调整后的ROI作为分类器的训练数据。与R-CNN不同的是,这里将分类和回归任务合并到一起进行训练,这样就将整个流程串联起来。即先将整张图通过卷积神经网络进行处理,然后在特征层上找到ROI对应的位置并取出,对取出的ROI进行池化(此处的池化方法有很多)。池化后,全部2000个M*N个训练数据通过全连接层并分别经过2个head:softmax分类以及L2回归,最终的损失函数是分类和回归的损失函数的加权和。利用这种方式即可实现端到端的训练。
Fast R-CNN极大地提升了目标检测训练和预测的速度
Faster R-CNN
Faster R-CNN就是在Fast R-CNN的基础上构建一个小的网络,直接产生RegioProposal来代替其他方法(如selectivesearch)得到ROI。这个小型的网络被称为区域预测网络(Region Proposal Network,RPN)。其中的RPN 是关键,其余流程与 Fast R-CNN 基本一致。
RPN的核心思想是构建一个小的全卷积网络,对于任意大小的图片,输出ROI的具体位置以及该ROI是否为物体。RPN网络在卷积神经网的最后一个特征层上滑动。
为了适应多种形状的物体,RPN定义了k种不同尺度的滑窗(因为有的目标是长的有的是扁的,有的是大的,有的是小的,统一用一个3*3的滑窗难以很好地拟合多种情况),它有一个专业的名词--anchor,每个anchor都是以特征层(feature map)上的像点为中心并且根据其尺度大小进行后续计算的。
RPN包含2类输出:二分类网络输出是否为物体,回归网络返回矩形框位置对应的4个值。
首先,针对分类任务,对于滑窗生的每一个 anchor 都计算该,anchor 与真实标记矩形框的 IOU。当 IOU 大于 0.7时,便认为该 anchor 中含有物体;当IOU小于0.3,时,便认为该anchor 中不包含物体;当IOU介干0.3~0.7时,则不参与网络训练的选代过程。
对于回归任务,这里定义为 anchor 中心点的横、纵坐标以及 anchor 的宽、高,学习目标为 anchor与真实bbox在这四个值上的偏移。RPN为一个全卷积网络,可以用随机梯度下降的方式端到端地进行训练。
这里需要注意的是,训练过程中能与真实物体矩形框相交的IOU大于0.7的anchor不多,它们绝大多数都是负样本,因此会导致正负样本比例严重失衡,从而影响识别效果因此,在RPN训练的过程,对每个batch进行随机采样(每个batch中有256个样本)并保证正负样本的比例为1:1,而当正样本数量小于128时,取全部的正样本,其余的则随机使用负样本进行补全。
使用RPN产生ROI的好处是可以与检测网络共享卷积层,使用随机梯度下降的方式端到端地进行训练。接下来我们看下FasterR-CNN的训练过程,具体步骤如下。
1)使用ImageNet 预训练好的模型训练一个 RPN。
2)使用ImageNet预训练好的模型,以及第1步里产生的建议区域训练FastR-CNN得到物体的实际类别以及微调的矩形框位置。
3)使用第2步中的网络初始化RPN,固定前面的卷积层,只调整RPN层的参数
4)固定前面的卷积层,只训练并调整FastR-CNN 的 FC 层。
有了RPN的帮助,FasterR-CNN的速度得到了极大提升
YOLO
一改基于 proposal的预测思路,将输入图片划分成S*S个小格子,在每个小格子中做预测,最终将结果合并
1)YOLO对于网络输入图片的尺寸有要求,首先需要将图片缩放到指定尺寸(448*448),再将图片划分成S*S的小格。
2)在每个小格里进行这样几个预测:该小格是否包含物体? 包含物体对应的矩形框位置以及该小格对应C个类别的分数是多少?
因此,每个小格需要预测的维度为8*(1+4)+C其中,B代表每个小格最多可能交叠物体的个数,1为该小格是否包含物体的置信度,4用来预测矩形框,C表示任务中所有可能的类别个数(不包含背景)。因此,YOLO网络最终特征层的大小为S*S*(B*5+C)
由于YOLO直接将输入图片划分为S*S个小格,不需要产生proposal,所以速度比Faster R-CNN快很多、但是因为其粒度较粗,所以精度相比FasterR-CNN略逊一筹,YOLO的主要贡献是为目标检测提供了另外一种思路,并使实时目标检测成为可能
SSD
SSD!同时借鉴了 YOLO 网格的思想和Faster R-CNN的 anchor 机制,使 SSD可以在进行快速预测的同时又可以相对准确地获取目标的位置
SSD 的一些特点。
1)使用多尺度特征层进行检测。在FasterR-CNN的RPN中,anchor是在主干网路的最后一个特征层上生成的,而在SSD中,anchor不仅是在最后一个特征层上产生的,而且在几个高层特征层处同时也在产生anchor。这些特征层大小依次递减,使得 SST可以检测不同尺度的目标。这里简单解释下,比如同样一个3*3的anchor,它在conv6“看到的”目标(感受野)就要远小于conv10“看到的”目标,可以理解为靠前的特征层用于检测小目标,而靠后的特征层用来检测大目标。与RPN产生anchor的方法类似,SSD也是在特征层的每个点上产生多个比例、多个尺度的n个anchor。
2)SSD中所有特征层产生的anchor都将经过正负样本的选,然后进行分类分数以及bbox位置的学习。也就是说,特征层上生成的正负样本将直接进行最终的分类(ClassNum个类别)以及bbox的学习,不像Faster R-CNN那样先在第一步学习是否有物体(只有0/1两个类别)以及bbox 位置,然后在第二步学习最终的分类(ClassNum 个类别)以及对bbox位置的微调。
bbox(bounding box)指的是用来框定目标物体的矩形区域。它通常由四个参数定义:左上角的坐标(x_min, y_min)和右下角的坐标(x_max, y_max),或者是中心点坐标和宽高(center_x, center_y, width, height)
实际应用时,我们不仅要关注精度,很多情况下还要考虑速度,比如对视频内容进行实时地检测,这时候我们就希望有方法可以很好地进行速度和精度的平衡。
SSD 实现 VOC 目标检测
数据
<annotation>
<folder>VOC2007</folder>
<filename>000032.jpg</filename>
<source>
<database>The VOC2007 Database</database>
<annotation>PASCAL VOC2007</annotation>
<image>flickr</image>
<flickrid>311023000</flickrid>
</source>
<owner>
<flickrid>-hi-no-to-ri-mo-rt-al-</flickrid>
<name>?</name>
</owner>
<size>
<width>500</width>
<height>281</height>
<depth>3</depth>
</size>
<segmented>1</segmented>
<object>
<name>aeroplane</name>
<pose>Frontal</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>104</xmin>
<ymin>78</ymin>
<xmax>375</xmax>
<ymax>183</ymax>
</bndbox>
</object>
<object>
<name>aeroplane</name>
<pose>Left</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>133</xmin>
<ymin>88</ymin>
<xmax>197</xmax>
<ymax>123</ymax>
</bndbox>
</object>
<object>
<name>person</name>
<pose>Rear</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>195</xmin>
<ymin>180</ymin>
<xmax>213</xmax>
<ymax>229</ymax>
</bndbox>
</object>
<object>
<name>person</name>
<pose>Rear</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>26</xmin>
<ymin>189</ymin>
<xmax>44</xmax>
<ymax>238</ymax>
</bndbox>
</object>
</annotation>
在类中定义了_getitem_ 方法,那么它的实例对象(假定为s)都可以通过 s[key]的方法进行取值,而当实例对象调用s[key]时,就会自动调用类中的方法_getitem_,这样便会使我们更加方便地对原始数据进行更加复杂的操作
#数据准备 from os import listdir #解析VOC数据路径时使用 from os.path import join#用于将多个路径组件合并成一个完整的路径。这个函数会根据操作系统的不同,自动使用正确的路径分隔符。比如,在Windows系统上,路径分隔符是\,而在Unix/Linux/macOS系统上,路径分隔符是/。 from random import random from PIL import Image, ImageDraw import xml.etree.ElementTree #用于解析VOC的xmllabel,用于解析和创建 XML 数据 import torch import torch.utils.data as data import torchvision.transforms as transforms from sampling import sampleEzDetect __all__ = ["vocClassName", "vocClassID", "vocDataset"]#当其他Python文件使用from <your_module_name> import *语法导入你的模块时,只有vocClassName、vocClassID和vocDataset这三个名称会被导入。 vocClassName = [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'] def getVOCInfo(xmlFile): root = xml.etree.ElementTree.parse(xmlFile).getroot(); anns = root.findall('object') #findall('object')调用会搜索root元素下所有标签名为object的子元素,并将它们作为Element对象的列表存储在变量anns中。 bboxes = [] for ann in anns: name = ann.find('name').text newAnn = {} newAnn['category_id'] = name #将name变量的值(即<name>元素的文本内容)作为'category_id'键的值存储进去 bbox = ann.find('bndbox') newAnn['bbox'] = [-1,-1,-1,-1] newAnn['bbox'][0] = float( bbox.find('xmin').text ) newAnn['bbox'][1] = float( bbox.find('ymin').text ) newAnn['bbox'][2] = float( bbox.find('xmax').text ) newAnn['bbox'][3] = float( bbox.find('ymax').text ) bboxes.append(newAnn) return bboxes class vocDataset(data.Dataset): def __init__(self, config, isTraining=True): super(vocDataset, self).__init__() self.isTraining = isTraining self.config = config normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) #用均值和方差对图片的RGB值分别进行归一化(也有其他方法,这种相对比较简单) self.transformer = transforms.Compose([ transforms.ToTensor(), normalize]) def __getitem__(self, index): item = None if self.isTraining: item = allTrainingData[index % len(allTrainingData)] else: item = allTestingData[index % len(allTestingData)] img = Image.open(item[0]) #item[0]为图像数据 allBboxes = getVOCInfo(item[1]) #item[1]为通过getVOCInfo函数解析出真实label的数据 imgWidth, imgHeight = img.size targetWidth = int((random()*0.25 + 0.75) * imgWidth) targetHeight = int((random()*0.25 + 0.75) * imgHeight) # 对图片进行随机crop,并保证bbox大小 xmin = int(random() * (imgWidth - targetWidth) ) ymin = int(random() * (imgHeight - targetHeight) ) img = img.crop((xmin, ymin, xmin + targetWidth, ymin + targetHeight))#是从一个图像img中裁剪出一个新的图像区域 img = img.resize((self.config.targetWidth, self.config.targetHeight), Image.BILINEAR)#Image.BILINEAR插值方法进行缩放。 imgT = self.transformer(img) imgT = imgT * 256 # 调整bbox bboxes = [] for i in allBboxes: xl = i['bbox'][0] - xmin yt = i['bbox'][1] - ymin xr = i['bbox'][2] - xmin yb = i['bbox'][3] - ymin if xl < 0 : xl = 0; if xr >= targetWidth: xr = targetWidth - 1 if yt < 0: yt = 0 if yb >= targetHeight: yb = targetHeight - 1 xl = xl / targetWidth xr = xr / targetWidth 归一化 yt = yt / targetHeight yb = yb / targetHeight if (xr-xl >= 0.05 and yb-yt >= 0.05): bbox = [ vocClassID[ i['category_id'] ],#vocClassID是一个字典,用于将类别ID映射到类别名称。 xl, yt, xr, yb ] bboxes.append(bbox) if len(bboxes) == 0: return self[index+1] target = sampleEzDetect(self.config, bboxes); ''' ### 对预测图片进行测试 ########## draw = ImageDraw.Draw(img) num = int(target[0]) for j in range(0,num): offset = j * 6 if ( target[offset + 1] < 0): break k = int(target[offset + 6]) trueBox = [ target[offset + 2], target[offset + 3], target[offset + 4], target[offset + 5] ] predBox = self.config.predBoxes[k] draw.rectangle([trueBox[0]*self.config.targetWidth, #在图像上绘制一个矩形 trueBox[1]*self.config.targetHeight, trueBox[2]*self.config.targetWidth, trueBox[3]*self.config.targetHeight]) draw.rectangle([predBox[0]*self.config.targetWidth, predBox[1]*self.config.targetHeight, predBox[2]*self.config.targetWidth, predBox[3]*self.config.targetHeight], None, "red") del draw img.save("/tmp/{}.jpg".format(index) ) ''' return imgT, target def __len__(self): if self.isTraining: num = len(allTrainingData) - (len(allTrainingData) % self.config.batchSize) return num else: num = len(allTestingData) - (len(allTestingData) % self.config.batchSize) return num vocClassID = {} for i in range(len(vocClassName)): vocClassID[vocClassName[i]] = i + 1 print vocClassID allTrainingData = [] #第167行,该行后面的代码为从VOC2007中读取数据,会在调用voc_dataset.py文件时立即执行 allTestingData = [] allFloder = ["./VOCdevkit/VOC2007"] #我们把从VOC网站下载的数据放到本地,只使用VOC2007做实验 for floder in allFloder: imagePath = join(floder, "JPEGImages") infoPath = join(floder, "Annotations") index = 0 for f in listdir(imagePath): #遍历9964张原始图片 if f.endswith(".jpg"): imageFile = join(imagePath, f) infoFile = join(infoPath, f[:-4] + ".xml") if index % 10 == 0 : #每10张随机抽1个样本做测试 allTestingData.append( (imageFile, infoFile) ) else: allTrainingData.append( (imageFile, infoFile) ) index = index + 1
整个 voc_dataset.py文件中定义了一个函数和一个类,函数getVOCInfo用于解析前面介绍的PASCAL VOC的xml 标注文件,vocDataset用于定义数据类,以及对数据执行预处理等操作。第167行之后的代码是从VOC2007中读取数据,其会在调用voc_dataset.py文件时立即执行(实战时请注意,路径要改成自己的PASCAL VOC数据集所在的路径)。另外,还要注意的一点是,真正进行实战时,为了达到效果,要在预处理上做很多工作,如颜色变换、旋转、平移等,这里只写了随机 corp 一种数据预处理的方式作为示例。vocDataset类中_getitem 函数的最后一句话调用了一个非常重要的函数 sampleEzDetect。
构建模型
下面我们先构建模型类,创建model.py文件,
import os
import math
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.autograd import Function
import torch.nn.functional as F
import torchvision.models as models
from sampling import buildPredBoxes
__all__ = ["EzDetectConfig", "EzDetectNet", "ReorgModule"]
class EzDetectConfig(object):
def __init__(self, batchSize=4, gpu=False):
super(EzDetectConfig, self).__init__()
self.batchSize = batchSize
self.gpu = gpu
self.classNumber = 21
self.targetWidth = 330
self.targetHeight = 330
self.featureSize = [[42, 42], ## L2 1/8
[21, 21], ## L3 1/16
[11, 11], ## L4 1/32
[ 6, 6], ## L5 1/64
[ 3, 3]] ## L6 1/110
##[min, max, ratio, ]
priorConfig = [[0.10, 0.25, 2],
[0.25, 0.40, 2, 3],
[0.40, 0.55, 2, 3],
[0.55, 0.70, 2, 3],
[0.70, 0.85, 2]]
self.mboxes = []
for i in range(len(priorConfig)):
minSize = priorConfig[i][0]
maxSize = priorConfig[i][1]
meanSize = math.sqrt(minSize*maxSize)
ratios = priorConfig[i][2:]#它提取了索引为 i 的子列表中,从第三个元素开始到末尾的所有元素。
# aspect ratio 1 for min and max
self.mboxes.append([i, minSize, minSize])
self.mboxes.append([i, meanSize, meanSize])
# other aspect ratio
for r in ratios:
ar = math.sqrt(r)
self.mboxes.append([i, minSize*ar, minSize/ar])
self.mboxes.append([i, minSize/ar, minSize*ar])
self.predBoxes = buildPredBoxes(self)
class EzDetectNet(nn.Module):
def __init__(self, config, pretrained=False):
super(EzDetectNet, self).__init__()
self.config = config
resnet = models.resnet50(pretrained) #从Pytorch的预训练库中拿到ResNet50模型,直接载入
self.conv1 = resnet.conv1
self.bn1 = resnet.bn1
self.relu = resnet.relu
self.maxpool = resnet.maxpool
self.layer1 = resnet.layer1
self.layer2 = resnet.layer2
self.layer3 = resnet.layer3
self.layer4 = resnet.layer4
self.layer5 = nn.Sequential( #直到第5层才开始自定义,前面都直接复用ResNet50的结构
nn.Conv2d(2048, 1024, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(1024),
nn.ReLU(),
nn.Conv2d(1024, 1024, kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(1024),
nn.ReLU())
self.layer6 = nn.Sequential(
nn.Conv2d(1024, 512, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2),
nn.Conv2d(512, 512, kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2))
inChannles = [512, 1024, 2048, 1024, 512]
self.locConvs = []
self.confConvs = []
for i in range(len(config.mboxes)):
inSize = inChannles[ config.mboxes[i][0] ]
confConv = nn.Conv2d(inSize, config.classNumber, kernel_size=3, stride=1, padding=1, bias=True)
locConv = nn.Conv2d(inSize, 4, kernel_size=3, stride=1, padding=1, bias=True)
self.locConvs.append(locConv)
self.confConvs.append(confConv)
super(EzDetectNet, self).add_module("{}_conf".format(i), confConv)
super(EzDetectNet, self).add_module("{}_loc".format(i), locConv)
def forward(self, x):
batchSize = x.size()[0]
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
l2 = self.layer2(x)
l3 = self.layer3(l2)
l4 = self.layer4(l3)
l5 = self.layer5(l4)
l6 = self.layer6(l5)
featureSource = [l2, l3, l4, l5, l6]
confs = []
locs = []
for i in range(len(self.config.mboxes)):
x = featureSource[ self.config.mboxes[i][0] ]
loc = self.locConvs[i](x)
loc = loc.permute(0, 2, 3, 1)
loc = loc.contiguous()
loc = loc.view(batchSize, -1, 4)
locs.append(loc)
conf = self.confConvs[i](x)
conf = conf.permute(0, 2, 3, 1)
conf = conf.contiguous()
conf = conf.view(batchSize, -1, self.config.classNumber)
confs.append(conf)
locResult = torch.cat(locs, 1)
confResult = torch.cat(confs, 1)
return confResult, locResult
EzDetectConfg 类可用于定义一些配置,如网络输人图片的大小、每个特征层的大小
每个特征层 anchor 的尺寸比例等。这里补充说明一下anchor的设定细节相关的问题,SSD的发明者在原文中是这样描述的。
大小:后加的每个feature map上都有尺寸不同的 anchors,大小比例根据原图设定最大的 feature map上anchor是300*0.1,最后的特征层上anchor是300*0.95,中间层的 anchor 大小在 0.1~0.95 之间均匀分布。标准大小定义为 sk
角度:aspectratio ar=1,2,3,1/2,1/3,2表示高的,3 表示细长的。
最终:anchor的宽为sk*sqrt(ar),高为sk*sqrt(1/ar)。
其实,anchor的设定可以根据自己的训练数据进行调整,如只需要检测行人,那么ratio只保留>1的值即可(因为人都是直立的),另外大小也可以根据真实值的标定来确定。
EzDetectNet类实际是用来定义网络,从init中可以看出,在这里我们直接从PyTorch库里载入ResNet50模型,并且在layer4之前完全复用ResNet50的结构,仅在layer5 和layer6 中重新进行定义。注意后面这个循环结构,其用于定义 SSD每一个特征层的 bbox位置和分类得分的输出。
for i in range(len(config.mboxes)):
inSize = inChannles[ config.mboxes[i][0] ]
confConv = nn.Conv2d(inSize, config.classNumber, kernel_size=3, stride=1, padding=1, bias=True)
locConv = nn.Conv2d(inSize, 4, kernel_size=3, stride=1, padding=1, bias=True)
self.locConvs.append(locConv)
self.confConvs.append(confConv)
super(EzDetectNet, self).add_module("{}_conf".format(i), confConv)
super(EzDetectNet, self).add_module("{}_loc".format(i), locConv)
定义loss
import torch import torch.nn as nn from torch.autograd import Variable from torch.autograd import Function import torch.nn.functional as F from bbox import bboxIOU, encodeBox __all__ = ["EzDetectLoss"] def buildbboxTarget(config, bboxOut, target): bboxMasks = torch.ByteTensor(bboxOut.size()) bboxMasks.zero_() bboxTarget = torch.FloatTensor(bboxOut.size()) batchSize = target.size()[0] for i in range(0, batchSize): num = int(target[i][0]) for j in range(0,num): offset = j * 6 cls = int(target[i][offset + 1]) k = int(target[i][offset + 6]) trueBox = [ target[i][offset + 2], target[i][offset + 3], target[i][offset + 4], target[i][offset + 5] ] predBox = config.predBoxes[k] ebox = encodeBox(config, trueBox, predBox) bboxMasks[i, k, :] = 1 bboxTarget[i, k, 0] = ebox[0] bboxTarget[i, k, 1] = ebox[1] bboxTarget[i, k, 2] = ebox[2] bboxTarget[i, k, 3] = ebox[3] if ( config.gpu ): bboxMasks = bboxMasks.cuda() bboxTarget = bboxTarget.cuda() return bboxMasks, bboxTarget def buildConfTarget(config, confOut, target): batchSize = confOut.size()[0] boxNumber = confOut.size()[1]confOut张量的第二维度的大小,并将其赋值给变量 confTarget = torch.LongTensor(batchSize, boxNumber, config.classNumber) confMasks = torch.ByteTensor(confOut.size()) confMasks.zero_() confScore = torch.nn.functional.log_softmax( Variable(confOut.view(-1, config.classNumber), requires_grad = False) )获得概率分布 confScore = confScore.data.view(batchSize, boxNumber, config.classNumber)confScore张量进行重塑(reshape),使其形状符合指定的维度,并将重塑后的结果赋值给原始的confScore变量。 # positive samples pnum = 0 for i in range(0, batchSize): num = int(target[i][0]) for j in range(0,num): offset = j * 6 k = int(target[i][offset + 6]) cls = int(target[i][offset + 1])读取当前目标的类别。
if cls > 0: confMasks[i, k, :] = 1 confTarget[i, k, :] = cls confScore[i, k, :] = 0 pnum = pnum + 1 else: confScore[i, k, :] = 0 ''' cls = cls * -1 confMasks[i, k, :] = 1 confTarget[i, k, :] = cls confScore[i, k, :] = 0 pnum = pnum + 1 ''' # negtive samples (background) confScore = confScore.view(-1, config.classNumber) confScore = confScore[:, 0].contiguous().view(-1) scoreValue, scoreIndex = torch.sort(confScore, 0, descending=False) for i in range(pnum*3): b = scoreIndex[i] // boxNumber k = scoreIndex[i] % boxNumber if ( confMasks[b,k,0] > 0): break confMasks[b, k, :] = 1 confTarget[b, k, :] = 0 if ( config.gpu ): confMasks = confMasks.cuda() confTarget = confTarget.cuda() return confMasks, confTarget class EzDetectLoss(nn.Module): def __init__(self, config, pretrained=False): super(EzDetectLoss, self).__init__() self.config = config self.confLoss = nn.CrossEntropyLoss() self.bboxLoss = nn.SmoothL1Loss() def forward(self, confOut, bboxOut, target): batchSize = target.size()[0] # building loss of conf confMasks, confTarget = buildConfTarget(self.config, confOut.data, target) confSamples = confOut[confMasks].view(-1, self.config.classNumber) confTarget = confTarget[confMasks].view(-1, self.config.classNumber) confTarget = confTarget[:, 0].contiguous().view(-1) confTarget = Variable(confTarget, requires_grad = False) confLoss = self.confLoss(confSamples, confTarget) # building loss of bbox bboxMasks, bboxTarget = buildbboxTarget(self.config, bboxOut.data, target) bboxSamples = bboxOut[bboxMasks].view(-1, 4) bboxTarget = bboxTarget[bboxMasks].view(-1, 4) bboxTarget = Variable(bboxTarget) bboxLoss = self.bboxLoss(bboxSamples, bboxTarget) return confLoss, bboxLoss
SSD训练与测试代码
from __future__ import print_function import argparse from math import log10 import torch import torch.nn as nn import torch.optim as optim from torch.autograd import Variable from torch.utils.data import DataLoader from voc_dataset import vocDataset as DataSet #from dummy_dataset import dummyDataSet as DataSet from model import EzDetectNet from model import EzDetectConfig from loss import EzDetectLoss # Training settings parser = argparse.ArgumentParser(description='EasyDetect by pytorch') parser.add_argument('--batchSize', type=int, default=16, help='training batch size') parser.add_argument('--testBatchSize', type=int, default=4, help='testing batch size') parser.add_argument('--lr', type=float, default=0.001, help='Learning Rate. Default=0.01') parser.add_argument('--threads', type=int, default=4, help='number of threads for data loader to use') parser.add_argument('--seed', type=int, default=1024, help='random seed to use. Default=123') parser.add_argument('--gpu', dest='gpu', action='store_true') #parser.add_argument('--no-gpu', dest='gpu', action='store_false') parser.set_defaults(gpu=True) opt = parser.parse_args() torch.cuda.set_device(1) print('===> Loading datasets') ezConfig = EzDetectConfig(opt.batchSize, opt.gpu) train_set = DataSet(ezConfig, True) test_set = DataSet(ezConfig, False) train_data_loader = DataLoader(dataset=train_set, num_workers=opt.threads, batch_size=opt.batchSize, shuffle=True) test_data_loader = DataLoader(dataset=test_set, num_workers=opt.threads, batch_size=opt.batchSize) print('===> Building model') mymodel = EzDetectNet(ezConfig, True) myloss = EzDetectLoss(ezConfig) optimizer = optim.SGD(mymodel.parameters(), lr=opt.lr, momentum=0.9, weight_decay=1e-4) #使用随机梯度下降方法 #optimizer = optim.Adam(mymodel.parameters(), lr=opt.lr) if ezConfig.gpu == True: #使用gpu mymodel.cuda() myloss.cuda() def adjust_learning_rate(optimizer, epoch): """每迭代10个epoch,学习率下降0.1倍""" lr = opt.lr * (0.1 ** (epoch // 10)) for param_group in optimizer.param_groups: param_group['lr'] = lr def doTrain(t): mymodel.train() for i, batch in enumerate(train_data_loader): batchX = batch[0] target = batch[1] if ezConfig.gpu: batchX = batch[0].cuda() target = batch[1].cuda() x = torch.autograd.Variable(batchX, requires_grad=False) confOut, bboxOut = mymodel(x) confLoss, bboxLoss = myloss(confOut, bboxOut, target) totalLoss = confLoss*4 + bboxLoss print(confLoss, bboxLoss) print("{} : {} / {} >>>>>>>>>>>>>>>>>>>>>>>>: {}".format(t, i, len(train_data_loader), totalLoss.data[0])) optimizer.zero_grad() totalLoss.backward() optimizer.step() def doValidate(): mymodel.eval() lossSum = 0.0 for i, batch in enumerate(test_data_loader): batchX = batch[0] target = batch[1] if ezConfig.gpu: batchX = batch[0].cuda() target = batch[1].cuda() x = torch.autograd.Variable(batchX, requires_grad=False) confOut, bboxOut = mymodel(x) confLoss, bboxLoss = myloss(confOut, bboxOut, target) totalLoss = confLoss*4 + bboxLoss print(confLoss, bboxLoss) print("Test : {} / {} >>>>>>>>>>>>>>>>>>>>>>>>: {}".format(i, len(test_data_loader), totalLoss.data[0])) lossSum = totalLoss.data[0] + lossSum score = lossSum / len(test_data_loader) print("########:{}".format(score)) return score ####### main function ######## for t in range(50): adjust_learning_rate(optimizer, t) doTrain(t) score = doValidate() if ( t %5 == 0): torch.save(mymodel.state_dict(), "model/model_{}_{}.pth".format(t, str(score)[:4])) import sys from PIL import Image, ImageDraw import torch from torch.autograd import Variable import torchvision.transforms as transforms from torch.utils.data import DataLoader from model import EzDetectConfig from model import EzDetectNet from bbox import decodeAllBox, doNMS ezConfig = EzDetectConfig() ezConfig.batchSize = 1 mymodel = EzDetectNet(ezConfig, True) mymodel.load_state_dict(torch.load(sys.argv[1])) print "finish load model" normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) transformer = transforms.Compose([transforms.ToTensor(),normalize]) img = Image.open(sys.argv[2]) originImage = img img = img.resize((ezConfig.targetWidth, ezConfig.targetHeight), Image.BILINEAR) img = transformer(img) img = img*256 img = img.view(1, 3, ezConfig.targetHeight, ezConfig.targetWidth) print "finish preprocess image" img = img.cuda() mymodel.cuda() classOut, bboxOut = mymodel(Variable(img)) bboxOut = bboxOut.float() bboxOut = decodeAllBox(ezConfig, bboxOut.data) classScore = torch.nn.Softmax()(classOut[0]) bestBox = doNMS(ezConfig, classScore.data.float(), bboxOut[0], 0.15) draw = ImageDraw.Draw(originImage) imgWidth, imgHeight = originImage.size for b in bestBox: draw.rectangle([b[0]*imgWidth, b[1]*imgHeight, b[2]*imgWidth, b[3]*imgHeight]) del draw print "finish draw boxes" originImage.save("1.jpg") print "finish all!"
浙公网安备 33010602011771号