为了赋予计算机以人类的理解能力与逻辑思维,诞生了人工智能(Artificial Intelligence, AI)这一学科。在实现人工智能的众多算法中,机器学习是发展较为快速的一支。机器学习的思想是让机器自动的从大量的数据中学习出规律,并利用该规律对未知的数据做出预测。在机器学习算法中,深度学习是特指利用深度神经网络的结构完成训练和预测的算法。
机器学习是实现人工智能的途径之一,而深度学习则是机器学习的算法之一。如果把人工智能比喻成人类的大脑,机器学习则是人类通过大量数据来认知学习的过程,而深度学习则是学习过程中非常高效的一种算法。
机器学习
机器学习是实现人工智能的重要途径,也是最早发展起来的人工智能算法。与传统的基于规则设计的算法不同,机器学习的关键在于从大量的数据中找出规律,自动的学习出算法所需的参数。
机器学习最重要的就是数据,根据使用数据形式,可以分为三大类:监督学习(Supervised Learning)、无监督学习(Unsupervised Learning)与强化学习(Reinforcement Learning)。
深度学习
深度学习是机器学习的技术分支之一,主要是通过搭建深层的人工神经网络(Artificial Neural Network)来进行知识的学习,输入数据通常较为复杂、规模大、维度高。深度学习可以说是机器学习问世以来最大的突破之一。
视觉是人类最为重要的感知系统,大脑皮层中近一半的神经元与视觉有关系。计算机视觉是研究如何使机器学会“看”的学科。在很长的一段时间内,计算机视觉的发展都是基于规则与人工设定的模板,很难有鲁棒的语义理解。真正将计算机视觉的发展推向高峰的,是深度学习的爆发,由于视觉图像丰富的语义性与图像的结构性,计算机视觉也是当前人工智能发展最为迅速的领域之一。
在计算机视觉众多的技术领域中,物体检测是一项非常基础的任务,图像分割、物体追踪、关键点检测等通常都要依赖于物体检测。此外,由于每张图像中物体的数量、大小及姿态各不相同,也就是非结构化的输出,这是与图像分类非常不同的一点,并且物体时常会有遮挡截断,物体检测技术也极富挑战性。
物体检测技术,通常是指在一张图像中检测出物体出现的位置及对应的类别。
传统的物体检测通常分为区域选取、特征提取与特征分类3个阶段。
深度学习时代的物体检测发展过程如下所示,深度神经网络大量的参数可以提取出鲁棒性和语义性更好的特征,并且分类器性能也更优越。
在物体检测算法中,物体边框从无到有,边框变化的过程在一定程度上体现了检测是一阶的还是两阶的。
Anchor是一个划时代的思想,最早出现在Faster RCNN中,其本质是一系列大小宽高不等的先验框,均匀的分布在特征图上,利用特征去预测这些Anchors的类别,以及与真实物体边框存在的偏移。Anchor相当于给物体检测提供了一个梯子,使得检测器不至于直接从无到有的预测物体,精度往往较高,常见算法有Faster RCNN和SSD等。
当然,还有一部分无锚框的算法,思路更为多样,直接通过特征预测边框位置的方法,如YOLO v1等。
对于一个检测器,需要制定一定的规则来评价其好坏,从而选择需要的检测器。对于图像分类任务来讲,由于其输出是很简单的图像类别,因此很容易通过判断分类正确的图像数量来进行衡量。
物体检测模型的输出是非结构化的,事先并无法得知输出物体的数量,位置大小等,因此物体检测的评价算法就稍微复杂一些。对于具体的某个物体来讲,可以从预测框与真实框的贴合程度来判断检测的质量,通常使用IOU(Intersection of Union)来量化贴合程度。
由于图像中存在背景与物体两种标签,预测框也分为正确与错误,因此在评测时会产生以下4种样本。
对于一个检测器,通常使用mAP(mean Average Precision)来评价一个模型的好坏,AP指的是一个类别的检测精度,mAP是多个类别的平均精度。评测需要每张图片的预测值与标签值,对于某一个实例,二者包含的内容分别如下:
在预测值与标签值的基础上,AP的具体计算过程如下图所示,首先将所有的预测框按照得分从高到低进行排序(因为得分越高的边框其对于真实物体的概率往往越大),然后从高到低遍历预测框。
类别的评测过程
for c in classes:
#通过类别作为关键字,得到每个类别的预测、标签集总标签数。
dects = det_boxes[c]
gt_class = gt_boxes[c]
npos = num_pos[c]
#利用得分作为关键字,对预测框按照得分从高到低排序
dects = sorted(dects, KeyboardInterrupt=lambda conf: conf[5], reversed=True)
#设置两个与预测边框长度相同的列表,标记是True Positive还是False Positive
TP = np.zeros(len(dects))
FP = np.zeros(len(dects))
#对某一个类别的预测框进行遍历
for d in range(len(dects)):
#将IOU默认为最低
iouMax = SystemError.float_info.min
#遍历与预测框同一图像中的同一类别的标签,计算IOU
if dects[d][-1] in gt_class:
for j in range(len(gt_class[dects[d][-1]])):
iou = Evaluator.iou(dects[d][-4], gt_class[dects[d][-1]][j][:4])
if iou > iouMax:
iouMax = iou
jmax = j #记录与预测有最大IOU的标签
#如果最大IOU大于阈值,并且没有被匹配过,则赋予TP
if iouMax >= cfg['iouThreshold']:
if gt_class[dects[d][-1]][jmax][4] == 0:
TP[d] = 1
gt_class[dects[d][-1]][jmax][4] = 1 #标记为匹配过
#如果被匹配过,赋予FP
else:
FP[d] = 1
#如果对应图像中没有该类别的标签,赋予FP
else:
FP[d] = 1
#利用Numpy的cumsum()函数,计算累计的FP与TP
acc_FP = np.cumsum(FP)
acc_TP = np.cumsum(TP)
rec = acc_TP / npos #得到每个点的Recall
prec = np.divide(acc_TP, (acc_FP + acc_TP)) #得到每个点的Precision
#利用Recall与Precision进一步计算得到AP
[ap, mpre, mrec, ii] = Evaluator.CalculateAveragePrecision(rec, prec)
PyTorch 是Facebook研究人员于2017你那退出的深度学习框架。
Linux诞生于1991年,是一个免费使用与自由传播的类Unix操作系统,现在使用的更多的是Linux发行版系统,即将Linux内核与应用软件做了打包,并进行依赖管理。Linux内核是独立的程序,每个发行版本维护自己修改的内核。
基本目录结构
Linux的思想是“一切都是文件”,数据与程序都是以文件形式存在,甚至是主机与外围众多设备之间的交互也抽象成对文件的操作。为统一各大Linux发行版本对目录文件的定义,FHS结构在1994年对Linux根目录做了统一规范,必须包含boot、lib、home、usr和opt等文件。
home
默认的用户目录,包含了所有用户的目录与数据,建议设置较大的磁盘空间。
环境变量
通俗讲是指操作系统执行程序时默认设定的参数,如各种可执行文件、库的路径等。
环境变量有系统级与用户级之分。系统级的环境变量是每一个登录到系统的用户都要读取的变量,可通过以下两个文件进行设置:
用户级环境变量是指针对当前登录用户设定的环境变量,可以通过两个文件进行设置。
可以在终端使用echo来查看当前的环境变量
echo $PYTHONPATH
变量与对象
对象
内存中存储数据的实体,有明确的类型。Python中的一切都是对象,函数也属于对象。
变量
指向对象的指针,对对象的引用。作为弱类型语言,Python中的变量没有类型。
变量存在深拷贝与浅拷贝的区别,不可变对象无论深/浅拷贝,其地址都是一样的,而可变对象则存在3中情况:
a = 1 #这里1为对象,a是指向对象1的变量
a = 'hello' #变量a可以指向任意的对象,没有类型限制
a
#对于不可变对象,所有指向该对象的变量在内存中共用一个地址
a = 1
b = 1
c = a + 0
id(a) == id(b) and id(a) == id(c) #用id()函数来查看3个变量的内存地址
#如果修改了不可变对象的变量的值,则原对象的其他变量不变;
#如果修改了可变对象的变量,则相当于可变对象被修改,其他变量也会发生变化
a = 1
b = a
b = 2 #由于a 与b指向的1都是不可变对象,因此改变b的值与a没有关系
a
c = [1]
d = c
d.append(2) #c与d指向的是相同的可变对象,d的操作是原地修改
c
#实现拷贝需要首先引入copy模块
import copy
a = [1, 2, [1, 2]]
b = a #直接复制,变量前后没有隔离
c = copy.copy(a) #浅拷贝
d = a[:] #相当于浅拷贝,与c相同
e = copy.deepcopy(a) #深拷贝,前后两个变量完全隔离
a.append(3)
a[2].append(3)
a,b
c
d
e
作用域
Python程序在创建、访问、改变一个变量时,都是在一个保存该变量的空间内进行的,这个空间为命名空间,也叫作用域。Python的作用域是静态的,变量被赋值、创建的位置决定了其被访问的范围,即变量的作用域由其所在的位置决定。
a = 1 #a 为全局变量
def local(): # local也是全局变量,在全局作用域中
b = 2 # b为局部变量
#Python中,使用一个变量时并不严格要求必须预先声明这个变量,但是在真正使用这个变量之前,必须被绑定到某个内存对象(被定义、赋值)中。
#这种变量名的绑定将在当前作用域中引入新的变量,同时屏蔽外层作用域中的同名变量。
a = 1
def local():
a = 2 #由于Python不需要预先声明,因此在局部作用域引入了新的变量,而没有修改全局
local()
print(a) #这里的a值仍为1
#想要实现局部修改全局变量,通常由两种办法,增加global等关键字,或者使用list和dict等可变对象的内置函数
a = 1
b = [1]
def local():
global a #使用global关键字,表明在局部使用的是全局的a变量
a = 2
b.append(2) #对于可变对象,使用内置函数则会修改全局变量
local()
print(a) #这里a的值已经被改变为2
print(b) #这里输出的是[1, 2]
Python的作用域从内而外,可以分为Local(局部)、Enclosed(嵌套)、Global(全局)及Built-in(内置)4种,变量的搜索遵循LEGB原则,如果一直搜索不到则会报错。
高阶函数
在编程语言中,高阶函数是指接受函数作为输入或者输出的函数。对于Python而言,函数是一等对象,即可以赋值给变量、添加到集合中、传参到函数中,也可以作为函数的返回值。
#Python 中的变量可以指向函数
f = abs
f(-1)
#map()函数可以将一个函数映射作用到可迭代的序列中,并返回函数输出的序列
def f(x):
return x * x
map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) #将定义的函数f依次作用于列表的各个元素
map(str, range(4))
#reduce()函数与map()函数不同,其输入的函数需要传入两个参数。
#reduce()的过程是先使用输入函数对序列中的前两个元素进行操作,得到的结果再和第三个元素进行运算,直到最后一个元素。
#reduce的计算过程:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2),x3),x4)
from functools import reduce #需要从functools引入reduce函数
def f(x, y):
return x * 10 + y
reduce(f, [1, 3, 5, 7, 9])
#filter()函数的作用主要是通过输入函数可对迭代序列进行过滤,并返回满足过滤条件的可迭代序列。
def is_odd(n):
return n % 2 == 0
filter(is_odd, [1, 2, 3, 4, 5, 6, 9, 10, 15]) #对奇偶过滤,保留偶数
#sorted()函数可以完成对可迭代序列的排序。与列表本身自带的sort()函数不同,sorted()函数返回的是一个新的列表。
#sorted()函数可以传入关键字key来指定排序的标准,参数reverse代表是否反向。
sorted([3, 5, -87, 0, -21], key=abs, reverse=True) #绝对值排序,并且为反序
#对于一些简单的逻辑函数,可以使用lambda匿名表达式来取代函数的定义,这样可以节省函数名称的定义,以及简化代码的可读性等。
add = lambda x, y : x + y #使用lambda实现add函数
add(1, 3)
map(lambda x: x + 1, [1, 2, 3, 4, 5, 6, 7, 8, 9]) #lambda实现元素加1操作
迭代器与生成器
迭代器(Iterator)与生成器(Generator)是Python最强大的功能之一,尤其是在处理大规模数据序列时,会带来诸多便利。在检测模型训练时,图像与标签数据的加载通常就是利用迭代生成器实现的。
迭代器不要求事先准备好整个迭代过程中的所有的元素,可以使用next()来访问元素。Python中的容器,如list、dict和set等,都属于可迭代对象,对于这些容器,可以使用iter()函数封装成迭代器。
x = [1, 2, 3]
y = iter(x)
z = iter(x)
next(y), next(y), next(z) #迭代器之间相互独立
实际上,任何实现了__iter__()和__next__()方法的对象都是迭代器,其中__iter__()方法返回迭代器本身,next()方法返回容器中的下一个值。for循环本质上也是一个迭代器的实现,作用于可迭代对象,在遍历时自动调动next()函数来获取下一个元素。
生成器是迭代器的一种,可以控制循环遍历的过程,实现一边循环一边计算,并使用yield来返回函数数值,每次调用到yield会暂停。生成器迭代的序列可以不是完整的,从而可以节省出大量的内存空间。
有多种创建迭代器的方法,最简单的是使用生成器表达式,与list很相似,只不过使用括号。
a = (x for x in range(10)) #利用()括号实现一个简单的生成器
next(a), next(a)
使用yield关键字来创建一个生成器。
def f():
yield 1
yield 2
yield 3
f1 = f()
print([next(f1) for i in range(2)])
使用生成器可以便捷的实现斐波那契额数列的生成。
def fibonacci():
a = [1, 1]
while True:
a.append(sum(a)) #往列表里添加下一个元素
yield a.pop(0) #取出第0个元素,并停留在当前执行点
for x in fibonacci():
print(x)
if x > 50:
break #仅打印小于50的数字
版本管理:Git
Git是一个开源的分布式版本控制系统,用于高效敏捷开发工程项目。有了Git,开发人员就不必将不同版本的文件复制成不同副本,而是可以通过Git系统的版本控制来完成,尤其是在多人协同开发时,会提供诸多便利。
Git支持Linux、Mac OS、 Widows等系统,下面以Ubuntu系统为例。
#安装Git
sudo apt install git
#git config指令配置邮箱与密码信息
git config --global user.name 'your name'
git config --global user.email 'your [email protected]'
#
使用git init指令初始化一个Git仓库,执行后会在当前目录中生成一个.git目录,其中保存了所有有关Git的数据,但切勿手动更改.git里的文件。
Git根据文件的存在位置,有工作区、暂存区与版本库区3个概念:
当多人协同开发或者需要有多个不同的版本时,就需要用到Git的分支管理功能了,这是Git极为强大与重要的功能之一。版本管理主要有以下4个指令。
git branch branchname #创建一个名为branchname的分支
git checkout branchname #切换到branchname的分支,git branch也可以查看当前的Git分支
git checkout -b branchname #创建一个名为branchname的分支,并切换到该分支,
git merge #合并分支
GitHub是一个基于Git的代码托管平台,是目前最为流行的代码托管服务,已拥有超过400万个项目,如果想要使用GitHub,首先需要在其官网注册一个账号,并使用创建仓库的命令创建名为repository的仓库。
为了能将自己本地的代码提交到GitHub上,需要添加GitHub账号可以识别的密钥。
ssh-keygen -t rsa -C '[email protected]'
执行上述指令后,一路按回车键,会在~/.ssh文件夹下生成id_rsa.pub文件,复制里面的密钥并粘贴到GitHub账号的SSH Keys里,即可实现本地与GitHub仓库的配对。
完成配对后,可以使用git clone命令将远程仓库拉取到本地。
git clone username@host:/path/to/repository
在本地进行代码的开发,并以此执行git add、git commit命令后,使用git push可以实现将本地版本仓库内的代码推到GitHub上。
git push origin master
高效编辑器:Vim
在Linux终端中,使用vim filename 命令即可打开一个Vim环境。Vim有3种基本模式。
Python调试器:pdb
在命令行中加入pdb模块来启动Python程序,这种方式会从程序第一行开始就进入交互环境,适用于较小的程序调试。
Python3 -m pdb test.py
在更大型的工程里,使用插入点的方式进行调试。将set_trace()函数放到代码中的任何地方,执行程序时都会在此处产生一个断点,尤其是在使用PyTorch这种极度Python化的框架时,使用极为方便。
import pdb
pdb.set_trace()
网页可视化:Jupyter
Jupyter Notebook是一个基于Web应用的交互式笔记本,使用者可以方便的在Web端与Python程序进行交互,以及进行数据的可视化分析。
分屏工具:Terminator
Terminator安装
sudo apt install terminator
任务托管:Screen
Screen是一款由GNU开发的软件,可用于多个命令行终端之间的自由切换与管理。即使网络断开,只要Screen本身没有停止,其内部执行的会话将一直保留。
sudo apt install screen
Tensor,即张量,是PyTorch中的基本操作对象,可以看做是包含单一数据类型元素的多维矩阵。
Tensor数据类型
Tensor在使用时可以有不同的数据类型,官方给了7种CPU Tensor类型与8种GPU Tensor类型,在使用时可以根据网络模型所需的精度与显存容量,合理的选取。16位半精度浮点是专为GPU上运行的模型设计的,以尽可能的节省GPU显存占用,但这种节省显存空间的方式也缩小了所能表达数据的大小。PyTorch中默认的数据类型是torch.FloatTensor,即torch.Tensor等同于torch.FloatTensor。
PyTorch可以通过set_default_tensor_type函数设置默认使用的Tensor类型,在局部使用完后如果需要其他类型,则还需要重新设置回所需的类型。
torch.set_default_tensor_type('torch.DoubleTensor')
对于Tensor之间的类型转换,可以通过type(new_type)、type_as()、int()等多种方式进行操作,尤其是type_as()函数,在后续模型学习中可以看到,想要保持Tensor之间的类型一致,只需要使用type_as()即可,并不需要明确具体是哪种类型。
#创建新Tensor,默认类型为torch.FloatTensor
a = torch.Tensor(2, 2)
a
#使用int()/float()/double()等直接进行数据类型转换
b = a.double()
b
#使用type()函数
c = a.type(torch.DoubleTensor)
c
#使用type_as()函数
d = a.type_as(b)
d
#最基础的Tensor()函数创建方法,参数为Tensor的每一维大小
a = torch.Tensor(2, 2)
a
b = torch.DoubleTensor(2, 2)
b
#使用Python的list序列进行创建
c = torch.Tensor([[1, 2], [3, 4]])
c
#使用zeros()函数,所有元素均为0
d = torch.zeros(2, 2)
d
#使用ones()函数,所有元素均为1
e = torch.ones(2, 2)
e
#使用eye()函数,对角线元素为1,不要求行列数相同,生成二维矩阵
f = torch.eye(2, 2)
f
#使用random()函数,生成随机数矩阵
g = torch.randn(2, 2)
g
#使用arange(start, end, step)函数,表示从start到end,间距为step,一维向量
h = torch.arange(1, 6, 2)
h
#使用randperm(num)函数,生成长度为num的随机排列向量
j = torch.randperm(4)
j
#PyTorch0.4中增加了torch.tensor()方法,餐是可以为python的list、Numpy的ndarray等
k = torch.tensor([1, 2, 3])
k
对于Tensor的维度,可使用Tensor.shape()或者size()函数查看每一维的大小,两者等价。
a = torch.randn(2, 2)
a.shape #使用shape查看Tensor的维度
a.size() #使用size()函数查看Tensor维度
查看Tensor中的元素总个数,可使用Tensor.numel()或者Tensor.nelement()函数,两者等价。
#查看Tensor中总的元素个数
a.numel()
a.nelement()
Tensor的组合与分块
组合与分块是将Tensor相互叠加或者分开。组合操作是指将不同的Tensor叠加起来,主要有torch.cat()和torch.stack()两个函数。cat即concatenate,是指沿着已有的数据的某个维度进行拼接,操作后数据的总维度不变,在进行拼接时,除了拼接的维度之外,其他维度必须相同。而torch.stach()函数指新增维度,病案照指定的维度进行叠加。
#创建两个2x2的Tensor
a = torch.Tensor([[1, 2], [3, 4]])
a
b = torch.Tensor([[5, 6], [7, 8]])
b
#以第一维进行拼接,则变成4x2的矩阵
torch.cat([a, b], 0)
#以第二维进行拼接,则变成2x4的矩阵
torch.cat([a, b], 1)
#以第0维进行stack,叠加的基本单位为序列本身,即a与b,因此输出[a,b],输出维度为2x2x2
torch.stack([a, b], 0)
#以第1维进行stack,叠加的基本单位为每一行,输出维度为2x2x2
torch.stack([a, b], 1)
#以第2维进行stack,叠加的基本单位为每一行的每一个元素,输出维度为2x2x2
torch.stack([a, b], 2)
分块则是与组合相反的操作,指将Tensor分割成不同的子Tensor,主要有torch.chunk()雨torch.split()两个函数,前者需要指定分块的数量,后者需要指定每一块的大小,以整型或者list来表示。
a = torch.Tensor([[1, 2, 3],[4, 5, 6]])
a
#使用chunk,沿着第0维进行分块,一共分两块,因此分割成两个1x3的Tensor
torch.chunk(a, 2, 0)
#沿着第1维进行分块,因此分割成两个Tensor,当不能整除时,最后一个的维数会小于前面的
#因此第一个Tensor为2x2,第二个为2x1
torch.chunk(a, 2, 1)
#使用split,沿着第0维分块,每一块维度为2,由于第一维维度总共为2,因此相当于没有分割
torch.split(a, 2, 0)
#沿着第1维分块,每一块维度为2,因此第一个Tensor为2x2,第二个为2x1
torch.split(a, 2, 1)
#split也可以根据输入的list进行自动分块,list中的元素代表了每一个块占的维度
torch.split(a, [1, 2], 1)
Tensor的索引与变形
索引操作与Numpy非常类似,主要包含小标索引、表达式索引、使用torch.where()与Tensor.clamp()的选择性索引。
a = torch.Tensor([[0,1],[2,3]])
a
#根据下标进行索引
a[1]
a[0, 1]
#选择a中大于0的元素,返回和a相同大小的Tensor,符合条件的置1,否则置0
a > 0
#选择符合条件的元素并返回,等价于torch.masked_select(a, a>0)
a[a > 0]
#选择非0元素的坐标,并返回
torch.nonzero(a)
#torch.where(condition, x, y)满足condition的位置输出x,否则输出y
torch.where(a > 1, torch.full_like(a, 1), a)
#对Tensor元素进行限制可以使用clamp()函数,示例如下,限制最小值为1,最大值为2
a.clamp(1, 2)
变形操作则是指改变Tensor的维度,以适应在深度学习的计算中,数据维度经常变换的需求,是一种十分重要的操作。
view()、resize()和reshape()函数
view()、resize()和shape()函数可以在不改变Tensor数据的前提下任意改变Tensor的形状,必须保证调整前后的元素总数相同,并且调整前后共享内存,三者的作用基本相同。
a = torch.arange(1, 5)
a
#分别使用view()/resize()/reshape()函数进行维度变换
b = a.view(2, 2)
b
c = a.resize(4, 1)
c
d = a.reshape(4, 1)
d
#改变了b/c/d的一个元素,a也跟着改变了,说明两者共享内存
b[0, 0] = 0
c[1, 0] = 0
d[2, 0] = 0
a
如果想要直接改变Tensor的尺寸,可以使用resize_()的原地操作函数。在resize_()函数中,如果超过了原Tensor的大小则重新分配内存,多出部分置0,如果小于原Tensor大小则剩余的部分仍然会隐藏保留。
c = a.resize_(2, 3)
c
#操作之后a也跟着改变了
a
transpose()和permute()函数
transpose()函数可以将指定的两个维度的元素进行转置,而permute()函数则可以按照给定的维度进行维度变换。
a = torch.randn(2, 2, 2)
a
#将第0维和第1维的元素进行转置
a.transpose(0,1)
#按照第2,1,0的维度顺序重新进行元素排列
a.permute(2, 1, 0)
squeeze()和unsqueeze()函数
在实际的应用中,经常需要增加或减少Tensor的维度,尤其是维度为1的情况,这时候可以使用squeeze()与unsqueeze()函数,前者用于去除size为1的维度,后者是将指定的维度的size变为1.
a = torch.arange(1, 4)
a.shape
#将第0维变为1,因此总的维度为1、3
a.unsqueeze(0).shape
#第0为如果是1,则去掉该维度,如果不是1则不起任何作用
a.unsqueeze(0).squeeze(0).shape
expand()和expand_as()函数
有时需要采用复制元素的形式来扩展Tensor的维度,expand()函数将size为1的维度复制扩展为指定大小,也可以使用expand_as()函数指定为示例Tensor的维度。
a = torch.randn(2, 2, 1)
a
#将第2维的维度由1变为3,则复制该维的元素,并扩展为3
a.expand(2, 2, 3)
在进行Tensor操作时,有些操作如transpose()、permute()等可能会把Tensor在内存中变得不连续,而有些操作如view()等式需要Tensor内存连续的,这种情况下需要使用contiguous()操作先将内存变为连续的。
Tensor的排序与取极值
排序函数sort(),选择沿着指定维度进行排序,返回排序后的Tensor及对应的索引位置。max()与min()函数则是沿着指定维度选择最大与最小元素,返回该元素及对应的索引位置。
a = torch.randn(3, 3)
a
#按照第0维即按行排序,每一列进行比较,True代表降序,False代表升序
a.sort(0, True)[0]
a.sort(0, True)[1]
#按照第0维即按行选取最大值,即将每一列的最大值选取出来
a.max(0)
对于Tensor的单元素数学运算,如abs()、sqrt()、log()、pow()和三角函数等,都是逐元素操作(element-wise),输出的Tensor形状与原始Tensor形状一致。
Tensor的自动广播机制与向量化
不同形状的Tensor进行计算时,可自动扩展到较大的相同形状,再进行计算。广播机制的前提是任一个Tensor至少有一个维度,且从尾部遍历Tensor维度时,两者维度必须相等,其中一个要么是1要么不存在。
a = torch.ones(3, 1, 2)
b = torch.ones(2, 1)
#从尾部遍历维度,1对应2,2对应1,3对应不存在,因此满足广播条件,最后求和后的维度为[3,2,2]
(a + b).size()
c = torch.ones(2, 3)
#a 与c最后一维的维度为2对应3,不满足广播条件,因此报错
(a + c).size()
向量化操作是指可以在同一时间进行批量地并行计算,例如矩阵运算,以达到更好的计算效率的一种方式。在实际使用时,应尽量使用向量化直接对Tensor操作,避免低效率的for循环对元素逐个操作,尤其是在训练网络模型时,如果有大量的for循环,会极大的影响训练的速度。
Tensor的内存共享
PyTorch提供了一些原地操作运算,即in-place operation,不经过复制,直接在原来的内存上进行计算。对于内存的共享,主要有如下3种情况。
通过Tensor初始化Tensor
直接通过Tensor来初始化另一个Tensor,或者通过Tensor的组合、分块、索引、变形操作来初始化另一个Tensor,则这两个Tensor共享内存。
a = torch.randn(2, 2)
a
#用a初始化b,或者用a的变形操作初始化c,这三者共享内存,一个边,其余的也改变了。
b = a
c = a.view(4)
b[0, 0] = 0
c[3] = 4
a
原地操作符
PyTorch对于一些操作通过加后缀“”实现了原地操作,如add()和resize_()等,这种操作只要被执行,本身的Tensor则会被改变。
a = torch.Tensor([[1, 2], [3, 4]])
a
#add_()函数使得a也改变了
b = a.add_(a)
a
#resize_()函数使得a也发生了改变
c = a.resize_(4)
a
Tensor与NumPy转换
Tensor与NumPy可以高效的进行转换,并且转换前后的变量共享内存。在进行PyTorch不支持的操作时,甚至可以曲线救国,将Tensor转换为NumPy类型,操作后再转为Tensor。
a = torch.randn(2, 2)
a
#Tensor转为NumPy
b = a.numpy()
b
#NumPy转为Tensor
c = torch.from_numpy(b)
c
#Tensor转为list
d = a.tolist()
d
基本数据Tensor可以保证完成前向传播,想要完成神经网络的训练,接下来还需要进行反向传播与梯度更新,而PyTorch提供了自动求导机制autograd,将前向传播的计算记录成计算图,自动完成求导。
Tensor的自动求导:Autograd
自动求导机制记录了Tensor的操作,以便自动求导与反向传播。可以通过requires_grad参数来创建支持自动求导机制的Tensor。
import torch
a = torch.randn(2, 2, requires_grad=True)
requires_grad参数表示是否需要对该Tensor进行求导,默认为False;设置WieTrue则需要求导,并且依赖于该Tensor的之后的的所有节点都需要求导。
Tensor有两个重要的属性,分别记录了该Tensor的梯度与经历的操作。
import torch
a = torch.randn(2, 2, requires_grad=True)
b = torch.randn(2, 2)
#可以看到默认的Tensor是不需要求导的,设置requires_grad为True后则需要求导
a .requires_grad, b.requires_grad
#也可以通过内置函数requires_grad()将Tensor变为需要求导
b.requires_grad_()
b.requires_grad
#通过计算生成的Tensor,由于依赖的Tensor需要求导,因此c也需要求导
c = a + b
c.requires_grad
#a与b是自己创建的,grad_fn为None,而c的grad_fn则是一个Add函数操作
a.grad_fn, b.grad_fn, c.grad_fn
d = c.detach()
d.requires_grad
计算图
计算图是PyTorch对于神经网络的具体实现形式,包括每一个数据Tensor及Tensor之间的函数function。
Autograd的基本原理是随着每一步Tensor的计算操作,逐渐生成计算图,并将操作的function记录在Tensor的grad_fn中。在前向计算完成后,只需对根节点进行backward函数操作,即可从当前根节点自动进行反向传播与梯度计算,从而得到每一个叶子节点的梯度,梯度计算遵循链式求导法则。
import torch
#生成3个Tensor变量,并作为叶节点
x = torch.randn(1)
w = torch.ones(1, requires_grad=True)
b = torch.ones(1, requires_grad=True)
#自己生成的,因此都为叶节点
x.is_leaf, w.is_leaf, b.is_leaf
#默认是不需要求导,关键字赋值为True后则需要求导
x.requires_grad, w.requires_grad, b.requires_grad
#进行前向计算,由计算生成的变量都不是叶节点
y = w * x
z = y + b
y.is_leaf, z.is_leaf
#由于依赖的变量有需要求导的,因此y和z都需要求导
y.requires_grad, z.requires_grad
#grad_fn记录生成该变量经过了什么操作,如y是Mul,z是Add
y.grad_fn
z.grad_fn
#对根节点调用backward()函数,进行梯度反传
z.backward(retain_graph=True)
w.grad
b.grad
Autograd注意事项
PyTorch的Autograd机制可以使得其可以灵活的进行前向传播与梯度计算,在实际使用时,需要注意以下3点。
PyTorch提供了集成度更高的模块化接口torch.nn,该接口构建与Autograd之上,提供了网络模组、优化器和初始化策略等一系列功能。
nn.Module类
nn.Module是PyTorch提供的神经网络类,并在类中实现了网络各层的定义及前向计算与反向传播机制。在实际使用时,如果想要实现某个神经网络,只需继承nn.Module,在初始化中定义模型结构与参数,在函数forward()中编写网络前向过程即可。
#perception.py
import torch
from torch import nn
#首先建立一个全连接的子module,继承nn.Module
class Linear(nn.Module):
def __init__(self, in_dim, out_dim):
super(Linear, self).__init__() #调用nn.Module的构造函数
#使用nn.Parameter来构造需要学习的参数
self.w = nn.Parameter(torch.randn(in_dim, out_dim))
self.b = nn.Parameter(torch.randn(out_dim))
#在forward中实现前向传播过程
def forward(self, x):
x = x.matmul(self.w) #使用Tensor.matmul实现矩阵相乘
y = x + self.b.expand_as(x) #使用Tensor.expand_as()来保证矩阵形状一致
return y
#构建感知机类,继承nn.Module,并调用了Linear的子module
class Perception(nn.Module):
def __init__(self, in_dim, hid_dim, out_dim):
super(Perception, self).__init__()
self.layer1 = Linear(in_dim, hid_dim)
self.layer2 = Linear(hid_dim, out_dim)
def forward(self, x):
x = self.layer1(x)
y = torch.sigmoid(x)
y = self.layer2(y)
y = torch.sigmoid(y)
return y
import torch
from perception import Perception #调用上述模块
#实例化一个网络,并赋值全连接中的维数,最终输出二维代表了二分类
perception = Perception(2, 3, 2)
#可以看到perception中包含上述定义的layer1与layer2
perception
#named_parameters()可以返回学习参数的迭代器,分别为参数名与参数值
for name, parameter in perception.named_parameters():
print(name, parameter)
#随机生成数据,注意这里的4代表了样本数为4,每个样本有两维
data = torch.randn(4, 2)
data
#将输入数据传入perception.perception()相当调用perception中的forward()函数
output = perception(data)
output
nn.Parameter函数
在类的__init__()中需要定义网络学习的参数,在此使用nn.Parameter()函数定义了全连接中的w和b,这是一种特殊的Tensor的构造方法,默认需要求导,即requires_grad为True。
forward()函数与反向传播
forward()函数用来进行网络的前向传播,并需要传入相应的Tensor,上例中perception(data)即时直接调用了forward()。在具体底层实现中,perception.call(data)将类的实例perception变成了可调用对象perception(data),而在perception.call(data)中主要调用了forward()函数。
多个Module的嵌套
在Module的搭建时,可以嵌套包含子Module,提升代码的复用性。在实际的应用中,PyTorch也提供了绝大多数的网络层,如全连接、卷积网络中的卷积、池化等,并自动实现前向与反向传播。
nn.Module与nn.function库
nn.functional也提供了很多网络层与函数功能,但与nn.Module不同的是,利用nn.funtional定义的网络层不可自动学习参数,还需要使用nn.Parameter封装。nn.functional的设计初衷是对于一些不需要学习参数的层,如激活层、BN(BatchNormlization)层。
nn.Sequential()模块
当模型只是简单的前馈网络时,即上一层的输出直接作为下一层的输入,这时可以采用nn.Sequential()模块来快速搭建模型,而不必手动在forward()函数中一层一层的前向传播。
class Perception(nn.Module):
def __init__(self, in_dim, hid_dim, out_dim):
super(Perception, self).__init__()
#利用nn.Sequential()快速搭建网络模块
self.layer = nn.Sequential(
nn.Linear(in_dim, hid_dim),
nn.Sigmoid(),
nn.Linear(hid_dim, out_dim),
nn.Sigmoid())
def forward(self, x):
y = self.layer(x)
return y
from perception_sequential import Perception
model = Perception(100, 1000, 10).cuda() #构建类的实例,并表明在CUDA上
#打印model结构,会显示Sequential中每一层的具体参数配置
model
input = torch.randn(100).cuda()
output = model(input)
output.shape
损失函数
在深度学习中,损失反映模型最后预测结果与实际真值之间的差距,可以用来分析训练过程的好坏、模型是否收敛等,在PyTorch中,损失函数可以看做是网络的某一层而放到模型定义中,单在实际使用时更偏向于作为功能函数而放到前向传播过程中。
from torch import nn
import torch.nn.functional as F
#设置标签,由于是二分类,一共有4个样本,因此标签维度为4,每个数为0或1两个类别
label = torch.Tensor([0, 1, 1, 0]).long()
#实例化nn中的交叉熵损失类
criterion = nn.CrossEntropyLoss()
#调用交叉熵损失
loss_nn = criterion(output, label)
loss_nn
#由于F.cross_entropy是一个函数,因此可以直接调用,不需要实例化,两者求得的损失值相同
loss_functional = F.cross_entropy(output, label)
loss_functional
优化器nn.optim
nn.Module模块提供了网络骨架,nn.functional提供了各式各样的损失函数,而Autograd又自动实现了求导与反向传播机制,nn.optim提供了进行模型优化、加速收敛的模块。
nn.optim中包含了各种常见的优化算法,包括随机梯度下降算法SGD(Stochastic Gradient Desent,随机梯度下降)、Adam(Adaptive Moment Estimation)、AdaGrad、RMSProp。
SGD方法
梯度下降(Gradient Descent)是迭代法中的一种,是指沿着梯度下降的方向求解极小值,一般可用于求解最小二乘问题。在深度学习中,当前更常用的是SGD算法,以一个小批次(Mini Batch)的数据为单位,计算一个批次的梯度,然后反向传播优化,并更新参数。
SGD参数的两个优点:
SGD缺点:
Adam方法
Adam利用梯度的一阶矩与二阶矩动态的估计调整每一个参数的学习率,是一种学习率自适应算法。
Adam的优点在于,经过调整后,每一次迭代的学习率都在一个确定范围内,使得参数更新更加平稳。此外,Adam算法可以使模型更快收敛,尤其适用于一些深层网络,或者神经网络较为复杂的场景。
from torch import optim
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
optimizer = optim.Adam([var1, var2], lr=0.0001)
#mlp.py
from torch import nn
class MLP(nn.Module):
def __init__(self, in_dim, hid_dim1, hid_dim2, out_dim):
super(MLP, self).__init__()
#通过Sequential快速搭建三层的感知机
self.layer = nn.Sequential(
nn.Linear(in_dim, hid_dim1),
nn.ReLU(),
nn.Linear(hid_dim1, hid_dim2),
nn.ReLU(),
nn.Linear(hid_dim2, out_dim),
nn.ReLU())
def forward(self, x):
x = self.layer(x)
return x
import torch
from mlp import MLP
from torch import optim
from torch import nn
#实例化模型,并赋予每一层的维度
model = MLP(28*28, 300, 200, 10)
model #打印model的结构,由3个全连接层构成
#采用SGD优化器,学习率为0.01
optimizer = optim.SGD(params=model.parameters(), lr=0.01)
data = torch.randn(10, 28*28)
output = model(data)
#由于是10分类,因此label元素从0到9,一共10个样本
label = torch.Tensor([1, 0, 4, 7, 9, 3, 4, 5, 3, 2]).long()
label
#求损失
criterion = nn.CrossEntropyLoss()
loss = criterion(output, label)
loss
optimizer.zero_grad()
#损失的反向传播
loss.backward()
#利用优化器进行梯度更新
optimizer.step()
#对于model中需要单独赋予学习率的层,如special层,则使用‘lr’关键字单独赋予
optimizer = optim.SGD([{
'params': model.special.parameters(), 'lr': 0.001}, {
'params': model.base.parameters()}, lr=0.0001])
模型是神经网络训练优化后得到的成果,包含了神经网络骨架及学校得到的参数。
网络模型库:torchvision.models
对于深度学习,torchvision.models库提供了众多经典的网络结构与预训练模型,利用这些模型可以快速搭建舞台检测网络,不需要逐层手动实现。torchvision包与PyTorch相互独立,需要通过pip指令安装。
from torch import nn
from torchvision import models
#通过torchvision.model直接调用VGG16网络结构
vgg = models.vgg16()
#VGG16的特征层包括13个卷积、13个激活函数ReLU、5个池化一共31层
len(vgg.features)
#VGG16的分类层包括3个全连接、2个ReLU、2个Dropout,一共7层
len(vgg.classifier)
#可以通过出现的属性直接索引每一层
vgg.classifier[-1]
#也可以选取某一部分,如下代表了特征网络的最后一个卷积模组
vgg.features[24:]
加载预训练模型
对于计算机视觉任务,重新训练一个新的模型是比较复杂的,并且不容易调整,因此,Fine-tune(微调)是一个常用的选择。即利用别人在一些数据集上训练好的预训练模型,在自己的数据集上训练自己的模型。
直接利用torchvision.models中自带的预训练模型,只需要在使用时赋予pretrained参数为True即可。
from torch imort nn
from torchvision import models
#通过torchvision.model直接调用VGG16的网络结构
vgg = models.vgg16(pretrained=True)
想要使用自己本地预训练模型,或者之前训练过的模型,则可以通过model.load_state_dict()函数操作。
import torch
from torch import nn
from torchvision import models
#通过torchvision.model直接调用VGG16的网络结构
vgg = models.vgg16()
state_dict = torch.load('your model path')
#利用load_state_dict,遍历预训练模型的关键字,如果出现在了VGG中,则加载预训练参数
vgg.load_state_dict({
k: v for k,v in state_dict_items() if k in vgg.state_dict()})
对于不同的检测任务,卷积网络的前两三层的作用是非常类似的,都是提取图像的边缘信息等,因此未来保证模型训练中能够更加稳定,一般会固定预训练网络的前两三个卷积层而不进行参数的学习。
for layer in range(10):
for p in vgg[layer].parameters():
p.requires_grad = False
模型保存
参数的保存通过torch.save()函数实现,可保存对象包括网络模型、优化器等,而这些对象的当前状态数据可以通过自身的state_dict()函数获取。
torch.save({
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'model_path.pth'})
主流公开数据集
数据加载
PyTorch将数据集的处理过程标准化,提供了Dataset基本的数据类,并在torchvision中提供了众多数据变换函数,数据加载的具体过程主要分3步。
继承Dataset类
对于数据集的处理,PyTorch提供了torch.utils.data.Dataset这个抽象类,在使用时只需要继承该类,并重写__len__()和__getitem__()函数,即可以方便地进行数据集的迭代。
from torch.utils.data import Dataset
class my_data(Dataset):
def __init__(self, image_path, annotation_path, transform=None):
#初始化,读取数据集
def __len__(self):
#获取数据集的总大小
def __getitem__(self, id):
#对于指定的id,读取该数据并返回
#对该类进行实例化
dataset = my_data('your image path', 'your annotation path') #实例化该类
for data in dataset:
print(data)
数据变换与增强:torchvision.transforms
PyTorch提供了torchvision.transforms工具包,可以方便的进行图像缩放、裁剪、随机翻转、填充及张量的归一化等操作,操作对象是PIL的Image或者Tensor。如果需要进行多个变换功能,可以利用transforms.Compose将多个变换整合起来,并且在实际使用时,通常会将变换操作集成到Dataset继承类中。
from torchvision import transforms
#将transforms集成到Dataset类中,使用Compose将多个变换整合到一起
dataset = my_data('your image path ', 'your annotation path', transforms=transforms.Compose([transforms.Resize(256) #将图像最短边缩小至256,宽高变量不变
#以0.5的概率随机翻转指定的PIL图像
transforms.RandomHorizontalFlip()
#将PIL图像转为Tensor,元素区间从[0,255]归一化到[0,1]
transform.ToTensor()
#进行mean与std为0.5的标准化
transforms.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])
]))
继承DataLoader类
经过前两步已经获取每一个变换后的样本,但是仍然无法进行批量处理、随机选取等操作,因此需要torch.utils.data.Dataloader类进一步封装,该类需要4个参数,第1个参数是之前继承的Dataset实例,第2个参数是批量batch的大小,第3个参数是是否打乱数据参数,第4个参数是使用几个线程来加载数据。
from torch.utils.data import Dataloader
#使用DataLoader进一步封装Dataset
dataloader = Dataloader(dataset, batch_size=4, shuffle=True, num_workers=4)
DataLoader是一个可迭代对象,对该实例进行迭代即可用于训练过程。
data_iter = iter(dataloader)
for step in range(iters_per_epoch):
data = next(data_iter)
#将data用于训练网络即可
GPU加速
PyTorch为数据在GPU上运行提供了非常便利的操作。首先可以使用torch.cuda.is_available()来判断当前环境下GPU是否可用,其次是对于Tensor和模型,可以直接调用cuda()方法将数据转移到GPU上运行,并且可以输入数字来指定具体转移到哪块GPU上运行。
import torch
from torchvision import models
a = torch.randn(3,3)
b = models.vgg16()
#判读当前GPU是否可用
if torch.cuda.is_available():
a = a.cuda()
#指定将b转移到变换为1的GPU上
b = b.cuda(1)
#使用torch.device()来指定使用哪一个GPU
device = torch.device('cuda: 1')
c = torch.randn(3, 3, device=device, requires_grad=True)
指定使用哪一块GPU的方法:
#method1
export CUDA_VISIBLE_DEVICES=2 python train.py
#method2
import torch
torch.cuda.set_device(1)
在工程应用中,通常使用torch.nn.DataParallel(model, device_ids)函数来处理多GPU并行计算的问题。
多GPU处理的实现方式是,首先将模型加载到主GPU上,然后复制模型到各个指定的GPU上,将输入数据按batch的维度进行划分,分配到每个GPU上度量进行前向计算,再将得到的损失求和反向传播更新单个GPU上独立进行前向计算,再将得到的损失求和并反向传播更新单个GPU上的参数,最后将更新后的参数复制到各个GPU上。
model_gpu = nn.DataParalle(model, device_ids=[0,1])
output = model_gpu(input)
数据可视化
TensorBoardX
TensorBoardX是专门为PyTorch开发的一套数据可视化工具,功能与TensorBoard相当,支持曲线、图片、文本和计算图等不同形式的可视化,而且使用简单。
#安装
pip install tensorboardX
#训练脚本中,创建记录对象与数据的添加
from tensorboardx import SummaryWriter
#创建writer对象
writer = SummaryWriter('logs/tmp')
#添加曲线,并且可以使用'/'进行多级标题的指定
writer.add_scalar('loss/total_loss', loss.data[0], total_iter)
writer.add_scalar('loss/rpn_loss', rnp_loss.data[0], total_iter)
#TensorBoard在终端中开启Web服务
tensorboard --logdir=log/tmp/
Visdom
Visdom由Facebook团队开发,是一个非常灵活的可视化工具,可用于多种数据的创建、组织和共享,支持NumPy、Torch与PyTorch数据,目的是促进远程数据的可视化,支持科学实验。
#安装
pip install visdom
#开启visdom服务
python -m visdom.server
#demo
import torch
import visdom
#创建visdom客户端,使用默认端口8097,环境为first,环境的作用是对可视化的空间进行区分
vis = visdom.Visdom(env='first')
#vis对象有text()、line()和image()等函数,其中的win参数代表了显示的窗格(pane)的名字
vis.text('first visdom', win='text1')
#在此使用append为真来进行增减text,否则会覆盖之前的text
vis.text('hello pytorch', win='text1', append=True)
#绘制y=-i^2+20xi+1的曲线,opts可以进行标题、坐标轴标签等的配置
for i in range(20):
vis.line(X=torch.FloatTensor([i]), Y=torch.FloatTensor([-i**2+20*i+1]), opts={
'title': 'y=-x^2+20x+1'}, win='loss', update='append')
#可视化一张随机图片
vis.image(torch.randn(3, 256, 256), win='random_image')
#打开浏览器,输入网址即可看到可视化的结果
当前的物体检测算法虽然各不相同,但第一步通常是利用卷积神经网络处理输入图像,生成深层的特征图,然后再利用各种算法完成区域生成与损失计算,这部分卷积神经网络是整个检测算法的‘骨架’,也被称为Backbone。
卷积层
卷积是分析数学中的一种运算,在深度学习中使用的卷积运算通常是离散的。作为卷积神经网络中最基础的组成部分,卷积的本质是用卷积核的参数来提取数据的特征,通过矩阵点乘运算与求和运算来得到结果。
from torch import nn
#使用torch.nn中的Conv2d()搭建卷积层
conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=1, dilation=1, groups=1, bias=True)
#查看卷积核的基本信息,本质上是一个Module
conv
#通过.weight与.bias查看卷积核的权重与偏置
conv.weight.shape
conv.bias.shape
#输入特征图,需要注意特征必须是四维,第一维作为batch数,即使是1也要保留
input = torch.ones(1, 1, 5, 5)
output = conv(input)
#当前配置的卷积核可以使输入和输出的大小一致
input.shape
output.shape
对于torch.nn.Conv2d()来说,传入的参数含义如下:
在实际使用中特征图的维度通常都不是1,假设输入特征图维度为 m × w i n × h i n m \times w_{in} \times h_{in} m×win×hin,输出特征图维度为 n × w o u t × h o u t n \times w_{out} \times h_{out} n×wout×hout,则卷积核的维度为 n × m × k × k n \times m \times k \times k n×m×k×k,在此产生的乘法操作次数为 n × w o u t × h o u t × m × k × k n \times w_{out} \times h_{out} \times m \times k \times k n×wout×hout×m×k×k。
激活函数层
神经网络如果仅仅是有线性的卷积运算堆叠组成,则其无法形成复杂的表达空间,也就很难提取出高语义的信息,因此还需要加入非线性的映射,又称为激活函数,可以逼近任意的非线性函数,以提升整个神经网络的表达能力。在物体检测任务中,常用的激活函数有Sigmoid/ReLU/Softmax函数。
Sigmoid函数
Sigmoid型函数又称为Logistic函数,模拟了生物的神经元特性,即当神经元获得的输入信号累计超过一定的阈值后,神经元被激活而处于兴奋状态,否则处于抑制状态。
σ ( x ) = 1 1 + e x p ( − x ) \sigma(x) = \frac{1}{1 + exp(-x)} σ(x)=1+exp(−x)1
Sigmoid函数将特征压缩到了(0,1)区间,0端对应抑制状态,而1对应激活状态,中间部分梯度较大。
#引入torch.nn模块
import torch
from torch import nn
input = torch.ones(1,1,2,2)
input
sigmoid = nn.Sigmoid() #使用nn.Sigmoid()实例化sigmoid
sigmoid(input)
ReLU函数
为了缓解梯度消失现象,修正线性单元(Rectified Linear Unit, ReLU)被引入到神经网络中。由于其优越的性能与简单优雅的实现,ReLU已经成为目前卷积神经网络中最为常用的激活函数之一。
R e L U ( x ) = m a x ( 0 , x ) = { 0 , if x < 0 x , if x ≥ 0 ReLU(x) = max(0, x) = \begin{cases} 0, & \text{if x < 0} \\ x, & \text{if x $\geq$ 0} \end{cases} ReLU(x)=max(0,x)={ 0,x,if x < 0if x ≥ 0
在小于0的部分,值与梯度皆为0,而在大于0的部分中导数保持为1,避免了Sigmoid函数中梯度接近于0导致的梯度消失问题。ReLU函数计算简单,收敛快,并在众多卷积网络中验证了其有效性。
import torch
from torch import nn
input = torch.randn(1, 1, 2, 2)
input
#nn.ReLU()可以实现inplace操作,即可以直接将运算结果覆盖到输入中,以节省内存
relu = nn.ReLU(inplace=True)
relu(input) #可以看出大于0的值保持不变,小于0的值被置为0
LeakyReLU函数
ReLU激活函数虽然高效,但是其将负区间所有的输入都强行置为0,LeakyReLU函数优化了这一点,在负区间内避免了直接置0,而是赋予很小的权重。
L e a k y R e L U ( x ) = m a x ( 1 a i x , x ) = { 1 a i x if x < 0 x if x ≥ 0 LeakyReLU(x) = max(\frac{1}{a_i}x, x)= \begin{cases} \frac{1}{a_i}x & \text{if x < 0} \\ x & \text{if x $\geq$ 0} \end{cases} LeakyReLU(x)=max(ai1x,x)={ ai1xxif x < 0if x ≥ 0
其中 a i a_i ai代表权重,即小于0的值被缩小的比例。
import torch
from torch import nn
input = torch.randn(1, 1, 2, 2)
input
#利用nn.LeakyReLU()构建激活函数,并且其为0.04,即ai为25,True代表in-place操作
leakyrelu = nn.LeakyReLU(0.04, True)
leakyrelu(input) #从结果看大于0的值保持不变,小于0的值被以0.04的比例缩小。
虽然从理论上讲,LeakyReLU函数的使用效果应该要比ReLU函数好,但是从大量实验结果来看并没有看出其效果比ReLU好。
Softmax函数
在物体检测中,通常需要面对多个物体分类问题,虽然可以使用sigmoid函数来构造多个二分类器,但比较麻烦,多物体类别较为常用的分类器是softmax函数。
在具体的分类任务中,softmax函数的输入往往是多个类别的得分,输出则是每个类别对应的概率,所有类别的概率取值都是在0-1之间,且和为1。
S i = e V i ∑ j C e V j S_i = \frac{e^{V_i}}{\sum_j^Ce^{V_j}} Si=∑jCeVjeVi
其中 V i V_i Vi表示第 i i i个类别的得分, C C C代表分类的类别总数,输出 S i S_i Si为第 i i i个类别的概率。
import torch.nn.functional as F
score = torch.randn(1, 4)
score
#利用torch.nn.functional.softmax()函数,第二个参数表示按照第几个维度进行Softmax计算。
F.softmax(score, 1)
池化层
在卷积网络中,通常会在卷积层之间增加池化(Pooling)层,以降低特征图的参数量,提升计算速度,增加感受野,是一种降采样操作。池化是一种较强的先验,可以使模型更关注全局特征而非局部出现的位置,这种降维的过程可以保留一些重要的特征信息,提升容错能力,并且还能在一定程度上起到防止过拟合的作用。
在物体检测中,常用的池化有最大值池化(Max Pooling)与平均值(Average Pooling)。池化层哟两个主要的输入参数,即核尺寸kernel_size与步长stride。
import torch
from torch import nn
#池化主要需要两个参数,第一个参数代表池化区域大小,第二个参数表示步长
max_pooling = nn.MaxPool2d(2, stride=2)
aver_pooling = nn.AvgPool2d(2, stride=2)
input = torch.randn(1, 1, 4, 4)
input
#调用最大值池化与平均值池化,可以看到size从[1,1,4,4]变为[1,1,2,2]
max_pooling(input)
aver_pooling(input)
Dropout层
在深度学习中,当参数过多而训练样本又比较少时,模型容易产生过拟合现象。过拟合是很多深度学习乃至机器学习算法的通病,具体表现在训练集上预测准确率高,而在测试集上准确率大幅度下降。Dropout算法可以比较有效的缓解过拟合现象的发生,起到一定正则化的效果。
Dropout的基本思想,在训练时,每个神经元以概率p保留,即以1-p的概率停止工作,每次前向传播保留下来的神经元都不同,这样可以使得模型不太依赖于某些局部特征,泛化性强。在测试时,为了保证相同的输出期望值,每个参数还要乘以p。当然还有另外一种计算方式称为Inverted Dropout,即在训练时将保留下的神经元乘以1/p,这样测试时就不需要再改变权重。
Dropout防止过拟合的原因解释:
多模型的平均
不同的固定神经网络会有不同的过拟合,多个平均则有可能让一些相反的拟合抵消掉,而Dropout每次都是ongoing的神经元失活,可以看做是多个模型的平均,类似于多数投票取胜的策略。
减少神经元间的依赖
由于两个神经元不一定同时有效,因此减少了特征之间的依赖,迫使网络学习有更为鲁棒的特征,因为神经网络不应该对特定的特征敏感,而应该从众多特征中学习更为共同的规律,这也起到了正则化的效果。
生物进化
Dropout类似于性别在生物进化中的,物种为了适应环境变化,在繁衍时取雄性和雌性的各一半基因进行组合,这样可以适应更复杂的新环境,避免了单一基因的过拟合,当环境发生变化时也不至于灭绝。
Dropout被广泛应用到全连接层中,一般保留概率设置为0.5,而在较为稀疏的卷积网络中则一般使用BN层来正则化模型,使得训练更稳定。
import torch
from torch import nn
# PyTorch将元素置为0来实现Dropout层,第一个参数为置0概率,第二个参数是否原地操作
dropout = nn.Dropout(0.5, inplace=False)
input = torch.randn(2, 64, 7, 7)
output = dropout(input)
BN层
为了追求更高的性能,卷积网络被设计得越来越深,然而网络却变得难以训练收敛于调参。原因在于,浅层参数的微弱变化经过多层线性变换与激活函数后会被放大,改变了每一层的输入分布,造成深层的网络需要不断调整以适应这些分布变化,最终导致模型难以训练收敛。
由于网络中参数变化导致的不节点数据分布发生变化的现象被称作ICS(Internal Covariate Shift)。ICS现象容易使训练过程陷入饱和区,减慢网络的收敛。ReLU从激活函数角度出发,在一定程度上解决了梯度饱和的现象,而BN则从改变数据分布的角度避免了参数陷入饱和区。由于BN层优越的性能,其已经是当前卷积网络中的标配。
BN层首先对每一个batch的输入特征进行白化操作,即去均值方差过程。假设一个batch的输入数据为 x : B = { x 1 , x 2 , . . . , x m } x:B=\{x_1, x_2,...,x_m\} x:B={ x1,x2,...,xm},首先求该batch数据的均值与方差。
μ B ← 1 m ∑ i = 1 m x i \mu_B \leftarrow \frac{1}{m}\sum_{i=1}^mx_i μB←m1i=1∑mxi
σ B 2 ← 1 m ∑ i = 1 m ( x i − μ B ) 2 \sigma_B^2 \leftarrow \frac {1}{m}\sum_{i=1}^m(x_i - \mu_B)^2 σB2←m1i=1∑m(xi−μB)2
m m m代表batch的大小, μ B \mu_B μB 为批处理数据的均值, σ B 2 \sigma_B^2 σB2为批处理数据的方差。利用求得的均值与方差进行去均值方差操作。
x ^ i ← x i − μ B μ B 2 + ∈ \hat{x}_i \leftarrow \frac{x_i - \mu_B}{\sqrt{\mu_B^2+\in}} x^i←μB2+∈xi−μB
白化操作可以使输入的特征分布具有相同的均值与方差,固定了每一层的输入分布,从而加速网络的收敛。然而,白化操作虽然从一定程度上避免了梯度饱和,但也限制了网络中数据的表达能力,浅层学到的参数信息会被白化操作屏蔽掉,因此,BN层在白化操作后又增加了一个线性变换操作,让数据尽可能的恢复本身的表达能力。
y i ← γ x ^ i + β y_i \leftarrow \gamma \hat{x}_i + \beta yi←γx^i+β
γ \gamma γ和 β \beta β是新引进的可学习参数,最终的输出为 y i y_i yi。
BN层可以看做是增加了线性变换的白化操作,在实际工程中被证明了能够缓解神经网络难以训练的问题。BN层的优点主要哟以下3点:
在测试时,由于是对单个样本进行测试,没有batch的均值与方差通常做法是在训练时将每一个batch的均值与方差都保留下来,在测试时使用所有训练样本均值与方差的平均值。
from torch import nn
#使用BN个需要传入一个参数为num_features,即特征的通道数
bn = nn.BatchNorm2d(64)
#eps为公司中的$\in$,momentum为均值方差的动量,affine为添加科学习是
bn
input = torch.randn(4, 64, 224, 224)
output = bn(input)
#BN层不改变输入、输出的特征大小
output.shape
GN(Group Normalization)从通道方向计算均值与方差,使用更为灵活有效,避开了batch大小对归一化 影响。GN将特征图的通道分为很多个组,对每一个组内的参数做归一化,而不是batch。在特征图中,不同的通道代表了不同的意义,例如形状、边缘和纹理等,这些通道并不是完全独立的分布,而是可以放到一起进行归一化分析。
全连接层
全连接层(Fully Connected Layers)一般连接到卷积网络输出的特征图后边,特点是每一个节点都与上下层的所有节点相连,输入与输出都被延展成一维向量,因此从参数量来看全连接层的参数量是最多的。
在物体检测算法中,卷积网络的主要作用是从局部到整体的提取图像的特征,而连接层则用来将卷积抽象出的特征图进一步映射到特定维度的标签空间,以求取损失或者输出预测结果。
import torch
from torch import nn
#第一维表示一共有4个样本
input = torch.randn(4, 1024)
linear = nn.Linear(1024, 4096)
output = linear(input)
input.shape
全连接层的缺点是参数量庞大。以VGGNet为例,其第一个全连接层的输入特征维为 7 × 7 × 12 = 25088 7\times7\times12=25088 7×7×12=25088个节点,输出特征是大小为4096的一维向量,由于输出层的每一个点都来自于上一层所有点权重相加,因此这一层的参数量为 25088 × 4096 ≈ 1 0 8 25088\times4096 \approx10^8 25088×4096≈108。相比之下,VGGNet最后一个卷积层的卷积核大小为 3 × 3 × 512 × 512 ≈ 2.4 × 1 0 6 3\times3 \times 512\times 512 \approx 2.4 \times 10^6 3×3×512×512≈2.4×106,全连接层的参数量是一个卷积层的40多倍。
大量的参数会导致模型网络应用部署困难,并且其中存在着大量的数据冗余, 容易发生过拟合现象。在很多场景中, 使用全局平均池化(Global Average Pooling, GAP)来取代全连接层, 这种思想最早见于NIN(Network in Network)网络中,总体上,使用GAP有如下好处:
深入理解感受野
感受野(Receptive Field)是指
特征图上的点能看到的输入图像的区域,即特征图上的点是由输入图像中感受野大小区域的计算得到的。
卷积层和池化层都会影响感受野,而激活函数层通常对于感受野没有多大影响,对于一般的卷积神经网络感受野的计算公式如下:
R F l + 1 = R F l + ( k − 1 ) × S l RF_{l+1} = RF_l + (k - 1) \times S_l RFl+1=RFl+(k−1)×Sl
S l = ∏ i = 1 l s t r i d e i S_l = \prod _{i=1}^l stride_i Sl=i=1∏lstridei
其中, R F l + 1 RF_l+1 RFl+1与 R F l RF_l RFl分别代表第 l + 1 l+1 l+1层与第 l l l层的感受野, k k k代表第 l + 1 l+1 l+1层的卷积核的大小, S l S_l Sl代表第 l l l层的步长之积。注意,当前层的步长并不影响当前个 感受野。
通过上述公式求取出的感受野通常很大, 而实际的有效感受野(Effective Receptive Field)往往小于理论感受野。从上图开也看出,虽然第三层的感受野是 7 × 7 7\times7 7×7,但是输入层边缘点的使用次数明显比中间点要少,因此做出的感受野是 7 × 7 7\times7 7×7,因此作出的贡献不同,经过多层的卷积堆叠之后,输入层对于特征图点做出的贡献分布呈高斯分布形状。
空洞卷积
空洞卷积最初是为解决图像分割问题而提出的。常见的图像分割算法通常使用池化层来增大增大感受野,同时也小了特征图尺寸,然后再利用上采样还原图像尺寸。特征图缩小再放大的过程造成了精度上的损失,因此需要有一种操作可以在增加感受野的同时保持特征图的尺寸不变,从而替代池化与上采样操作,在这种需求习下,空洞卷积就产生了。
在物体检测的发展中,空洞卷积也发挥了重要的作用。因为虽然物体检测不要求逐像素的检测,但是保持特征图的尺寸较大,对于小物体的检测及物体的定位来说也是至关重要的。
空洞卷积,顾名思义就是在卷积核中间带有一些洞,跳过一些元素进行卷积。
空洞卷积在不增加参数量的前提下,增大了感受野。假设空洞卷积的卷积核大小为 k k k,空洞数为 d d d,则其等效卷积核大小 k ′ k' k′计算公式如下:
k ′ = k + ( k − 1 ) ( d − 1 ) k' = k + (k - 1)(d - 1) k′=k+(k−1)(d−1)
在计算感受野时,只需将原来的卷积核大小 k k k更换为 k ′ k' k′即可。
空洞卷积在不引人额外参数的前提下可以任意扩大感受野,同时保持特征图分辨率不变。这一点在分割与检测任务中十分有用,感受野的扩大可以检测 体,而特征图分辨率不变使得物体定位十分精准。
from torch import nn
#定义普通卷积,默认dilation为1
conv1 = nn.Conv2d(3, 256, 3, stride=1, padding=1, dilation=1)
conv1
#定义dilation为2的卷积,打印卷积后会有dilation的参数
conv2 = nn.Conv2d(3, 256, 3, stride=1, padding=1, dilation=2)
conv2
VGGNet将卷积网络进行了改良,探索 网络深度与性能的关系,用更小的卷积核与更深的网络结构,取得了较好的效果,成为卷积结构发展史上较为重要的一个网络。
VGGNet网络结构一共有6个不同的 ,最常用的是VGG16。VGGNet采用了5个卷积与三个全连接层,最后使用Softmax做分类。VGGNet有一个显著的特点:每次经过池化层(maxpool)后特征图的尺寸减小一倍,而通道数则增加一倍(最后IG池化层除外)。
VGGNet中,使用的卷积核基本都是 3 × 3 3 \times 3 3×3,而且很多地方出现了多个 3 × 3 3 \times 3 3×3堆叠的现象,这种结构的优点在于,首先从感受野来看,两个 3 × 3 3 \times 3 3×3卷积核与一个 5 × 5 5 \times 5 5×5卷积核是一样的;其次同等感受野时, 3 × 3 3 \times 3 3×3卷积核的参数量更少。更为重要的是,两个 3 × 3 3 \times 3 3×3卷积核的非线性能力要比 5 × 5 5 \times 5 5×5卷积核强,因为其拥有两个激活函数,可大大提高卷积网络的学习能力。
#vgg.py
from torch import nn
class VGG(nn.Module):
def __init__(self, num_classes=1000):
super(VGG, self).__init__()
layers = []
in_dim = 3
out_dim = 64
#循环构造卷积层,一共有13个卷积层
for i in range(13):
layers += [nn.Conv2d(in_dim, out_dim, 3, 1, 1), nn.ReLU(inplace=True)]
in_dim = out_dim
#在第2/4/7/10/13个卷积层后增加池化层
if i == 1 or i == 3 or i == 6 or i == 9 or i == 12:
layers += [nn.MaxPool2d(2,2)]
#第10个卷积后保持和前边的通道数一致,都为512,其余加倍
if i != 9:
out_dim *= 2
self.features = nn.Sequential(*layers)
#VGGNet的3个全连接层,中间有ReLU与Dropout层
self.classifier = nn.Sequential(
nn.Linear(512 * 7 *7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(),
nn.Linear(4096, num_classes),)
def forward(self, x):
x = self.features(x)
#这里是将特征图的维度从[1, 512, 7, 7]变到[1, 512*7*7]
x = x.view(x.size(0), -1)
x = self.classifier(x)
x = self.classifier(x)
return x
import torch
from vgg import VGG
#实例化VGG类,在此设置输出分类数为21,并转移到GPU上
vgg = VGG(21).cuda()
input = torch.randn(1, 3, 224, 224).cuda()
input.shape
score = vgg(input)
score.shape
#也可以单独调用卷积模块,输出最后一层的特征图
features= vgg.features(input)
features.shape
#打印出VGGNet的卷积层,5个卷集组一共30层
vgg.features
#打印出VGGNet的3个全连接层
vgg.classifier
一般来说,增加网络的深度与宽度可以提升网络的性能,但是这样做也会带来参数量的大幅度增加,同时较深的网络需要较多的数据,否则容易产生过拟合现象。除此之外,增加神经网络额深度容易带来梯度消失现象。Inception v1(又名GoogLeNet)网络较好的解决了这个问题。
Inception v1网络是一个精心设计的22层卷积网络,并提出了具有良好局部特征的Inception模块,即对特征并行地执行多个大小不同的卷积运算与池化,最后再拼接到一起。由于 1 × 1 1\times1 1×1、 3 × 3 3\times3 3×3和 5 × 5 5\times5 5×5的卷积核运算对应不同的特征图区域,因此这样做可以得到更好的图像表征信息。
Inception模块使用了三个不同大小的卷积核进行卷积运算,同时还有一个最大值池化,然后将这4部分级联起来(通道拼接),送入下一层。
为了进一步降低网络参数量,Inception又增加了多个 1 × 1 1\times1 1×1的卷积模块。这种 1 × 1 1\times1 1×1模块可以先将特征图降维,再送给 3 × 3 3\times3 3×3和 5 × 5 5\times5 5×5大小的卷积核,由于通道数的降低,参数量也有了较大的减少。
Inception v1网络一共有9个Inception模块,共有22层,在最后的Inception模块处使用了全局平均池化。为了避免深层网络训练时带来的梯度消失问题,引入了两个辅助的分类器,在第3个与第6个Inception模块后输出后执行softmax并计算损失,在训练时和最后的损失一并回传。
Inception v1的参数量是AlexNet的 1 12 \frac{1}{12} 121,VGGNet的 1 3 \frac{1}{3} 31,适合处理大规模数据,尤其是对于计算资源有限的平台。
#inceptionv1.py
import torch
from torch import nn
import torch.nn.functional as F
#首先定义一个包含conv与ReLU的基础卷积类
class BasicConv2d(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, padding=0):
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=padding)
def forward(self, x):
x = self.conv(x)
return F.relu(x, inplace=True)
class Inceptionv1(nn.Module):
def __init__(self, in_dim, hid_1_1, hid_2_1, hid_2_3, hid_3_1, hid_3_5, out_4_1):
super(Inceptionv1, self).__init__()
#下面分别是4个子模块各自的网络定义
self.branch1x1 = BasicConv2d(in_dim, hid_1_1, 1)
self.branch3x3 = nn.Sequential(
BasicConv2d(in_dim, hid_2_1, 1),
BasicConv2d(hid_2_1, hid_2_3, 3, padding=1))
self.baranch5x5 = self.Sequential(
BasicConv2d(in_dim, hid_3_1, 1),
BasicConv2d(hid_3_1, hid_3_5, 5, padding=2))
self.branch_pool = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
BasicConv2d(in_dim, out_4_1, 1))
def forward(self, x):
b1 = self.branch1x1(x)
b2 = self.branch3x3(x)
b3 = self.branch5x5(x)
b4 = self.branch_pool(x)
#将这四个模块沿着通道方向进行拼接
output = torch.cat((b1, b2, b3, b4), dim=1)
return output
import torch
from inceptionv1 import Inceptionv1
#网络实例化,输入模块通道数,并转移到GPU上
net_inceptionv1 = Inceptionv1(3, 64, 32, 64, 64, 96, 32).cuda()
net_inceptionv1
input = torch.randn(1, 3, 256, 256).cuda()
input.shape
output = net_inceptionv1(input)
output.shape
在Inception v1的基础上,Inception v2进一步通过分解与正则化实现更高效的计算,增加了BN层,同时利用两个级联的 3 × 3 3\times3 3×3卷积取代了Inception v1版本中的 5 × 5 5\times5 5×5卷积,这种方式即减少了卷积参数量,也增加了网络的非线性能力。
#inceptioinv2.py
import torch
from torch import nn
import torch.nn.functional as F
#构建基础的卷积模块,与Inception v2的基础模块相比,增加了BN层。
class BasicConv2d(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, padding=0):
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=padding)
self.bn = nn.BatchNorm2d(out_channels, eps=0.001)
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
return F.relu(x, inplace=True)
class Inceptionv2(nn.Module):
def __init__(self):
super(Inceptionv2, self).__init__()
self.branch1 = BasicConv2d(192, 96, 1, 0) #对应1x1卷积分支
#对应1x1卷积与3x3卷积分支
self.branch2 = nn.Sequential(
BasicConv2d(192, 48, 1, 0),
BasicConv2d(48, 64, 3, 1))
#对应1x1卷积、3x3卷积与3x3卷积分支
self.branch3 = nn.Sequential(
BasicConv2d(192, 64, 1, 0),
BasicConv2d(64, 96, 3, 1),
BasicConv2d(96, 96, 3, 1))
#对应3x3平均池化与1x1卷积分支
self.branch4 = nn.Sequential(
nn.AvgPool2d(3, stride=1, padding=1, count_include_pad=False),
BasicConv2d(192, 64, 1, 0))
#前向过程,将4个分支进行torch.cat()拼接起来
def forward(self, x):
x0 = self.branch1(x)
x1 = self.branch2(x)
x2 = self.branch3(x)
x3 = self.branch4(x)
out = torch.cat((x0, x1, x2, x3), 1)
return out
import torch
from inceptionv2 import Inceptionv2
net_inceptionv2 = Inceptionv2().cuda()
net_inceptionv2
input = torch.randn(1, 192, 32, 32).cuda()
input.shape
output = net_inceptionv2(input) #将输入传入实例的网络
output.shape #输出特征图的通道数为96+64+96+64=320
更进一步,Inception v2将 n × n n\times n n×n的卷积运算分解为 1 × n 1\times n 1×n与 n × 1 n\times1 n×1两个卷积,这种计算成本降低33%。
此外,Inception v2还将模块中的卷积核变得更宽而不是更深,形成第三个模块,以解决表征能力瓶颈的问题。Inception v2网络正是由上述的三个不同类型的模块组成的,其计算也更加有效。
Inception v3在Inception v2的基础上,使用了RMSProp优化器,在辅助的分类器部分增加了 7 × 7 7\times7 7×7的卷积,并且用了标签平滑技术。
Inception v4则是将Inception的思想与残差网络进行了结合,显著提升了训练速度与模型准确率。
VGGNet与Inception出现后,学者们将卷积网络不断加深以寻求更优越的性能,随着网络的加深,网络却越来越难以训练,一方面会产生梯度消失现象;另一方面越深的网络返回的梯度相关性会越来越差,接近于白噪声,导致梯度更新也接近于随机扰动。
ResNet的思想在于引入了一个深度残差框架来解决梯度消失问题,即让卷积网络去学习残差映射,而不是期望每一个堆叠层的网络都完整的去拟合潜在的映射(拟合函数)。对于神经网络,如果期望的网络最终映射为 H ( x ) H(x) H(x),非残差网络需要直接拟合输出 H ( x ) H(x) H(x),而残差网络需要引入一个shortcut分支,将需要拟合的映射变为残差 F ( x ) : H ( x ) − x F(x):H(x)-x F(x):H(x)−x。ResNet给出的假设是:相较于直接优化潜在映射 H ( x ) H(x) H(x),优化残差映射 F ( x ) : H ( x ) − x F(x):H(x)-x F(x):H(x)−x更为容易。
在ResNet中,残差模块称为Bottleneck,有18层、34层、50层、101层和152层的网络层数版本。ResNet-50的网络架构如下所示,最主要部分在于中间经历了4个大的卷积组,而这4个卷积组分别包含了3、4、6这个3个Bottleneck模块。最后经过一个全局平均池化使得特征图大小变为 1 × 1 1\times 1 1×1,然后进行1000维的全连接,最后经过Softmax输出分类得分。
由于 F ( x ) + x F(x)+x F(x)+x是逐通道进行相加,因此根据两者是否通道数相同,存在两种Bottleneck结构。对于通道数不同的情况,比如每个卷积组的第一个Bottleneck,需要利用 1 × 1 1\times1 1×1卷积对 x x x进行Downsample操作,将通道数变为相同,再进行加操作。对于相同的情况下,两者可以直接进行相加。
#resnet_bottleneck.py
import torch.nn as nn
class Bottleneck(nn.Module):
def __init__(self, in_dim, out_dim, stride=1):
super(Bottleneck, self).__init__()
#网络堆叠层是由1x1/3x3/1x1这3个卷积组成的,中间包含BN层。
self.bottleneck = nn.Sequential(
nn.Conv2d(in_dim, in_dim, 1, bias=False),
nn.BatchNorm2d(in_dim),
nn.ReLU(inplace=True),
nn.Conv2d(in_dim, in_dim, 3, stride, 1, bias=False),
nn.BatchNorm2d(in_dim),
nn.ReLU(inplace=True),
nn.Conv2d(in_dim, out_dim, 1, bias=False),
nn.BatchNorm2d(out_dim),)
self.relu = nn.ReLU(inplace=True)
#Downsample部分是由一个包含BN层的1x1卷积组成
self.downsample = nn.Sequential(
nn.Conv2d(in_dim, out_dim, 1, 1),
nn.BatchNorm2d(out_dim),)
def forward(self, x):
identity = x
out = self.bottleneck(x)
identity = self.downsample(x)
#将identity(恒等映射)与网络堆叠层输出进行相加,并经过ReLU后输出
out += identity
out = self.relu(out)
return out
import torch
from resnet_bottleneck import Bottleneck
#实例化Bottleneck,输入通道数为64, 输出通道数为256,对应第一个卷积组的第一个Bottleneck
bottleneck_1_1 = Bottleneck(64, 256).cuda()
bottleneck_1_1
input = torch.randn(1, 64, 56, 56).cuda()
output = bottleneck_1_1(input) #将输入送到Bottleneck结构中
input.shape
output.shape #相比输入,输出的特征图分辨率没有变,而通道数变为4倍。
ResNet通过前层与后层的“短路连接”(shortcuts),加强了前后层之间的信息流通,在一定程度上缓解了梯度消失现象,从而可以将神经网络搭建得很深。更进一步,DenseNet《Densely Connected Convolutional Networks》最大化了这种前后层信息交流,通过建立前面所有层与后面层的密集连接,实现了特征在通道维度上的复用,使其可以在参数与计算量更少的情况下实现比ResNet更优的性能。
DenseNet网络由多个Dense Block与间的卷积池化组成,核心就在Dense Block中。Dense Block中的黑点代表一个卷积层,其中的多条黑线代表数据的流动,每一层的输入由前面的所有卷积层的输出组成。注意这里使用了通道拼接(concatnate)操作,而非ResNet的逐元素相加操作。
DenseNet的网络结构有两个特性:
关于Block,需要注意以下4个细节:
DenseNet网络的优势主要体现在两个方面:
#densenet_block.py
import torch
from torch import nn
import torch.nn.functional as F
#实现一个Bottleneck的类,初始化需要输入通道数与GrowthRate这两个参数
class Bottleneck(nn.Module):
def __init__(self, nChannels, growthRate):
super(Bottleneck, self).__init__()
#通常1x1卷积的通道数为GrowthRate的4倍。
interChannels = 4 * growthRate
self.bn1 = nn.BatchNorm2d(nChannels)
self.conv1 = nn.Conv2d(nChannels, interChannels, kernel_size=1, bias=False)
self.bn2 = nn.BatchNorm2d(interChannels)
self.conv2 = nn.Conv2d(interChannels, growthRate, kernel_size=3, padding=1, bias=False)
def forward(self, x):
out = self.conv1(F.relu(self.bn1(x)))
out = self.conv2(F.relu(self.bn2(out)))
#将输入x同时计算的结果out进行通道拼接
out = torch.cat((x, out), 1)
return out
class Denseblock(nn.Module):
def __init__(self, nChannels, growthRate, nDenseBlocks):
super(Denseblock, self).__init__()
layers = []
#将每一个Bottleneck利用nn.Sequential()整合起来,输入通道数需要线性增长
for i in range(int(nDenseBlocks)):
layers.append(Bottleneck(nChannels, growthRate))
nChannels += growthRate
self.denseblock = nn.Sequential(*layers)
def forward(self, x):
return self.denseblock(x)
import torch
from densenet_block import Denseblock
#实例化DenseBlock,包含6个Bottleneck
denseblock = Denseblock(64, 32, 6).cuda()
#查看denseblock的网络结构,由6个Bottleneck组成
denseblock
input = torch.randn(1, 64, 256, 256).cuda()
output = denseblock(input) #将输入传入到denseblock结构中
output.shape #输出通道数为:224+32=64+32*6=256
为了增强语义性,传统的物体检测模型通常只在深度卷积网络的最后一个特征图上进行后续操作,而这一层对应的下采样率(图像缩小的倍数)通常比较大,造成小物体在特征图上的有效信息较少,小物体的检测性能会急剧下降,这个问题被称为多尺度问题。
解决多尺度问题的关键在于如何提取多尺度的特征。传统的方法有图像金字塔(Image Pyramid),主要思路是将输入图像做成多个尺度,不同尺度的图像生成不同尺度的特征,这种方法简单而有效,但是非常耗时,计算量也很大。
卷积神经网络不同层 大小与语义信息不同,本身就类似一个金字塔结构。FPN(Feature Pyramid Network)方法融合了不同层的特征,较好的改善了多尺度检测问题。
FPN的总体架构主要包含自下而上网络、自上而下网络、横向连接与卷积融合4个部分。
对于实际物体检测算法,需要在特征图上进行RoI(Region of Interests,感兴趣区域)提取,而FPN有4个输出的特征图,选择哪一个特征图上面的特征也是问题。FPN给出的解决方法是,对于不同大小的RoI,使用不同额特征图,大尺度的RoI在深层的特征图上进行提取,如P5,小尺度的RoI在浅层的特征图上进行提取,如P2。
FPN将深层的语义信息传到底层,来补充浅层的语义信息,从而获得了高分辨率、强语义的特征,在小物体检测、实例分割等领域有着非常不俗的表现。
#fpn.py
import torch.nn as nn
import torch.nn.functional as F
import math
#ResNet的基本Bottleneck类
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, in_planes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.bottleneck = nn.Sequential(
nn.Conv2d(in_planes, planes, 1, bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes, planes, 3, stride, 1, bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes, self.expansion * planes, 1, bias=False),
nn.BatchNorm2d(self.expansion * planes),)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x):
identity = x
out = self.bottleneck(x)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
#FPN的类,初始化需要一个list,代表ResNet每一个阶段的Bottleneck的数量
class FPN(nn.Module):
def __init__(self, layers):
super(FPN, self).__init__()
self.inplanes = 64
#处理输入的C1模块
self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(3, 2, 1)
#搭建自下而上的C2、C3、C4、C5
self.layer1 = self._mame_layer(64, layers[0])
self.layer2 = self._make_layer(128, layers[1], 2)
self.layer3 = self._make_layer(256, layers[2], 2)
self.layer4 = self._make_layer(512, layers[3], 2)
#对C5减少通道数,得到P5
self.toplayer = nn.Conv2d(2048, 256, 1, 1, 0)
#3x3卷积融合特征
self.smooth1 = nn.Conv2d(256, 256, 3, 1, 1)
self.smooth2 = nn.Conv2d(256, 256, 3, 1, 1)
self.smooth3 = nn.Conv2d(256, 256, 3, 1, 1)
#横向连接,保证通道数相同
self.latlayer1 = nn.Conv2d(1024, 256, 1, 1, 0)
self.latlayer2 = nn.Conv2d(512, 256, 1, 1, 0)
self.latlayer3 = nn.Conv2d(256, 256, 1, 1, 0)
#构建C2到C5,注意区分stride值为1和2的情况
def _make_layer(self, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != Bottleneck.expansion * planes:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, Bottleneck.expansion * planes, 1, stride, bias=False),
nn.BatchNorm2d(Bottleneck.expansion * planes))
layers = []
layers.append(Bottleneck(self.inplanes, planes, stride, downsample))
self.inplanes = planes * Bottleneck.expansion
for i in range(1, blocks):
layers.append(Bottleneck(self.inplanes, planes))
return nn.Sequential(*layers)
#自上而下的采样模块
def _upsample_add(self, x, y):
_, _, H, W = y.shape
return F.upsample(x, size=(H, W), mode='bilinear') + y
def forward(self, x):
#自下而上
c1 = self.maxpool(self.relu(self.bn1(self.conv1(x))))
c2 = self.layer1(c1)
c3 = self.layer2(c2)
c4 = self.layer3(c3)
c5 = self.layer4(c4)
#自上而下
p5 = self.toplayer(c5)
p4 = self._upsample_add(p5, self.latlayer1(c4))
p3 = self._upsample_add(p4, self.latlayer2(c3))
p2 = self._upsample_add(p3, self.latlayer3(c2))
#卷积融合,平滑处理
p4 = self.smooth1(p4)
p3 = self.smooth2(p3)
p2 = self.smooth3(p2)
return p2, p3, p4, p5
import torch
from fpn import FPN
#利用listai初始化FPN网络
net_fpn = FPN([3, 4, 6, 3]).cuda()
net_fpn.conv1 #查看FPN的第一个卷积层
net_fpn.bn1 #查看FPN的第一个BN层
net_fpn.relu #查看FPN的第一个ReLU层
net_fpn.maxpool #查看FPN的第一个池化层,使用最大值池化
net_fpn.layer1 #查看FPN的第一个layer,即前面的C2,包含了3个Bottleneck
net_fpn.layer2 #查看fpn的layer2,即上面的C3,包含了4个Bottleneck
net_fpn.toplayer #1x1的卷积,以得到p5
net_fpn.smooth1 #对p4信息平滑的卷积层
net_fpn.latlayer1 #对c4进行横向处理的卷积层
input = torch.randn(1, 3, 224, 224).cuda()
output = net_fpn(input)
#返回的p2, p3, p4, p5这4个特征图通道数相同,但特征图尺寸递减
output[0].shape
output[1].shape
output[2].shape
output[3].shape
VGGNet和ResNet虽然从各个角度出发提升了物体检测性能,但是都是为ImageNet的图像分类任务而设计的。而图像分类与物体in层两个任务天然存在着落差,分类任务侧重于全图的特征提取,深层的特征图分辨率很低;而物体检测需要定位出物体位置,特征图分辨率不宜过小,因此造成以下两个缺陷:
旷视科技提出的物体检测网络结构DetNet,引入了空洞卷积,使得模型兼具较大感受野与较高分辨率,同时避免了FPN多次上采样,实现了较好的检测效果。
DetNet的网络结构如图所示,选择性能优越的ResNet-50作为基础结构,并保持前4个stage与ResNet-50相同,具体的细节结构有以下3点:
DetNet这种精心设计的结构,在增加感受野的同时,获得了较大的特征图尺寸,有利于物体的定位。与此同时,由于各Stage的特征图尺寸相同,避免了上采样,一定程度上降低了计算量,又利于小物体的检测。
DetNet与ResNet两者的基本思想都是卷积堆叠层与恒等映射的相加,区别在于DetNet使用了空洞数为2的3x3卷积,这样使得特征图尺寸保持不变,而ResNet是使用了步长为2的3x3卷积。B相比于A,在恒等映射部分增加了一个1x1卷积,这样做可以区分开不同 Stage,并且实验发现这种做法对于特征金字塔式的检测非常重要。
#detnet_bottleneck__.py
from torch import nn
class DetBottleneck(nn.Module):
#初始化时extra为False时为Bottleneck A,为True时为Bottleneck B
def __init__(self, inplanes, planes, stride=1, extra=False):
super(DetBottleneck, self).__init__()
#构建连续3个卷积层的Bottleneck
self.bottleneck = nn.Sequential(
nn.Conv2d(inplanes, planes, 1, bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=2, dilation=2, bias=False),
nn.BatchNorm2d(planes),
nn.ReLU(inplace=True),
nn.Conv2d(planes, planes, 1, bias=False),
nn.BatchNorm2d(planes),)
self.relu = nn.ReLU(inplace=True)
self.extra = extra
#Bottleneck B的1x1卷积
if self.extra:
self.extra_conv = nn.Sequential(
nn.Conv2d(inplanes, planes, 1, bias=False),
nn.BatchNorm2d(planes))
def forward(self, x):
#对于Bottleneck B来讲,需要对恒等映射增加卷积处理,与ResNet类似
if self.extra:
identity = self.extra_conv(x)
else:
identity = x
out = self.bottleneck(x)
out += identity
out = self.relu(out)
return out
import torch
from detnet_bottleneck import DetBottleneck
#完成一个stage5、即B-A-A的机构,stage4输出通道数为1024
bottleneck_b = DetBottleneck(1024, 256, 1, True).cuda()
bottleneck_b #查看Bottleneck B的结构,带有extra的卷积层
bottleneck_a1 = DetBottleneck(256, 256).cuda()
bottleneck_a1 #查看Bottleneck A1的结构
bottleneck_a2 = DetBottleneck(256, 256).cuda()
bottleneck_a2 #查看Bottleneck A2的结构,与Bottleneck A1相同
input = torch.randn(1, 1024, 14, 14).cuda()
#将input作为某一层的特征图,依次传入Bottleneck B、A1、A2三个模块
output1 = bottleneck_b(input)
output2 = bottleneck_a1(output1)
output3 = bottleneck_a2(output2)
#三个Bottleneck输出的特征图大小完全相同
output1.shape, output2.shape, output3.shape
开山之作:RCNN
RCNN延续传统物体检测的思想,将物体检测当作分类问题处理,即先提取一系列的候选区域,然后对候选区域进行分类。具体过程主要有4步:
RCNN虽然显著提升了物体检测的效果,但仍然存在3个较大的 。
端到端:Fast RCNN
在RCNN后,SPPNet算法解决了重复卷积计算与固定输出尺度的两个问题,但仍然存在RCNN的其他弊端。Fast RCNN算法不仅训练的步骤可以实现端到端,而且算法基于VGG16网络,在训练速度上比RCNN快了近9倍,在测试速度上快了213倍, 并在VOC 2012数据集上达到了68.4%的检测率。
Fast RCNN算法框架如下所示,相比RCNN,主要有3点改进:
Fast RCNN算法虽然取得了显著的效果,但在该算法中,Selective Search需要消耗2-3秒,而特征提取仅需要0.2秒,因此这种区域生成方法限制了Fast RCNN算法的发挥空间,这为Faster RCNN算法提供了改进方向。
走向实时:Faster RCNN
Faster RCNN算法最大的创新点在于提出了RPN(Region Proposal Network)网络,利用Anchor机制将区域生成与卷积网络联系在一起,将检测速度提升到17fps/sec,并在VOC2012测试集上实现了70.4%的检测结果。
Anchor可以看做是图像上很多固定大小与宽高的方框,由于需要检测的物体本身也都是一个个大小宽高不同的方框,因此Faster RCNN将Anchor当作强先验的知识,接下来只需要将Anchor与真实物体进行匹配,进行分类与位置的微调即可。相比没有Anchor的物体检测算法,这样的先验无疑降低了网络收敛的难度,再加上一系列的工程优化,使得Faster RCNN达到了物体检测中的一个高峰。
jwyang/faster-rcnn.pytorch
从功能模块来讲,Faster RCNN算法的基本流程主要包括4部分:特征提取网络、RPN模块、RoI Pooling(Region of Proposal)模块与RCNN模块,虚线内表示仅仅在训练时才有的步骤。
从整个过程可以看出,Faster RCNN是一个 两阶的算法,即RPN与RCNN,这两步都需要计算损失,只不过前者还要为后者提供较好的感兴趣区域。
RPN部分的输入输出:
理解Anchor
Faster RCNN先提供一些先验的边框,然后再去筛选与修正,这样在Anchor的基础上做物体检测要比从无到有的直接拟合物体边框容易一些。
Anchor的本质是在原图大小上的一系列的矩形框,但Faster RCNN将这一系列的矩形框和feature map进行了关联。具体做法是,首先对feature map进行3x3的卷积操作,得到的每一个点的维度是512维,这512维的数据对应着原始图片上的很多不同的大小与宽高区域的特征,这些区域的中心点都相同。如果下采样率为默认的16,则每一个点的坐标乘以16即可得到对应的原图坐标。
为适应不同物体的大小与宽高,默认在每一个点上抽取了9种Anchors,具体Scale为{8, 16, 32},Ratio为{0.5, 1, 2},将这9种Anchors的大小反算到原图上,季的第不同的原始Proposal。由于feature map大小为37x50,因此一共有37x50x9=16650个Anchors。通过分类网络与回归网络得到每一个Anchor的前景背景概率和偏移量,前景背景概率用来判断Anchor是前景的概率,回归网络则是将偏移量作用到Anchor上是的Anchor更接近于真实物体坐标。
RPN的真值与预测量
对于物体检测任务,模型需要预测每一个物体的类别及其出现的位置,即类别、中心点坐标x与y、宽w与高h这5个量。由于有Anchor这个先验框,RPN可以预测Anchor的类别作为预测边框的类别,并且可以预测真实的边框相对于Anchor的偏移量,而不是直接预测边框的中心点坐标x与y、宽高w与h。
如下图所示,输入图像中有3个Anchors与两个标签,从位置来看,Anchor A、C分别和标签M、N有一定的重叠,而Anchor B位置更像是背景。
RPN卷积网络
为了实现上述的预测,RPN搭建了如下所示的网络结构。具体实现时,在feature map上首先用3x3的卷积进行更深的特征提取,然后利用1x1的卷积分别实现分类网络和回归网络。
在物体检测中,通常将有物体的位置称为前景,没有物体的位置称为背景。在分类网络分支中,首先使用1x1卷积输出18x37x50的特征,由于每个点默认有9个Anchors,并且每个Anchor只预测其属于前景还是背景,因此通道数为18。利用torch.view()函数将特征映射到2x333x75,这样第一维仅仅是一个Anchor的前景背景得分,并送到Softmax函数中进行概率计算,得到的特征再变换到18x37x50的维度,最终输出的是每个Anchor属于前景与背景的概率。
在回归分支中,利用1x1卷积输出36x37x50的特征,第一维的36包含9个Anchor的中心点横纵坐标及宽高这4个量相对于真值的偏移量。RPN的网络部分代码如下:
def forward(self, base_feat, im_info, gt_boxes, num_boxes):
#输入数据的第一维是batch值
batch_size = base_feat.size(0)
#首先利用3x3卷积进一步融合特征
rpn_conv1 = F.relu(self.RPN_Conv(base_feat), inplace=True)
#利用1x1卷积得到分类网络,每个点代表Anchor的前景背景得分
rpn_cls_score = self.RPN_cls_score(rpn_conv1)
#利用reshape与softmax得到Anchor的前景背景概率
rpn_cls_score_reshape = self.reshape(rpn_cls_score, 2)
rpn_cls_prob_reshape = F.softmax(rpn_cls_score_reshpae, 1)
rpn_cls_prob = self.reshape(rpn_cls_prob_reshape, 18)
#利用1x1卷积得到回归网络,每一点代表Anchor的偏移
rpn_bbox_pred = self.RPN_bbox_pred(rpn_conv1)
RPN真值的求取
RPN分类与回归网络得到的是模型的预测值,为了计算预测的损失,还乤得到分类与偏移预测的真值,具体指的是每一个Anchor是否对应这真实物体,以及每一个Anchor对应物体的真实偏移值。
Anchor生成
与前面Anchor 的生成过程一样,可以得到37x50x9=16650个Anchor。按照这种方式生成的Anchor会有一些边界在图像边框外,因此需要把这部分超过图像边框的Anchor过滤掉。
def forward(self, input):
......
#利用Numpy首先得到原图上 中心点坐标,并利用contiguous保证内存连续
shifts = torch.from_numpy(np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose())
shifts = shifts.contiguous().type_as(rpn_cls_score).float()
......
#调用基础Anchor生成所有Anchors
self._anchors = self._anchors.type_as(gt_boxes)
all_anchors = self._anchors.view(1, A, 4) + shifts.view(K, 1, 4)
......
#保留边框内的Anchors
inds_inside = torch.nonzero(keep).view(-1)
anchors = all_anchors[inds_inside, :]
Anchor与标签的匹配
为了计算Anchor的损失,在生成Anchor之后,还需要得到每个Anchor的类别,由于RPN的作用是建议框生成,而非详细的分类,因此只需要区分正样本与负样本,即每个Anchor是属于正样本还是负样本。
计算每一个Anchor与每一个标签的IOU,会得到一个IOU矩阵,具体的判断标准如下:
def forward(self, input):
#生成标签向量,对应每一个Anchor的状态,1为正,0为负,初始化为-1
labels = gt_boxes.new(batch_size, inds_inside.size(0)).fill_(-1)
#生成IoU矩阵,每一行代表一个Anchor,每一列代表 标签
overlaps = bbox_overlaps_batch(anchors, gt_boxes)
#对每一行求最大值,返回的第一个为最大值,第二个为最大值的位置。
max_overlaps, argmax_overlaps = torch.max(overlaps, 2)
#对每一列取最大值,返回的是每一个标签对应的IoU 最大值
gt_max_overlaps,_ = torch.max(overlaps, 1)
#如果一个Anchor最大的IOU小于0.3,视为负样本
labels[max_overlaps < 0.3] = 0
#与所有Anchors的最大IOU为0的标签要过滤掉
gt_max_overlaps[gt_max_overlaps == 0] = 1e-5
#将与标签有最大IoU的Anchor赋予正样本
keep = torch.sum(overlaps.eq(gt_max_overlaps.view(batch_size, 1, -1).expand_as(overlaps)), 2)
if torch.sum(keep) > 0:
labels[keep > 0] = 1
#如果一个Anchor最大的IOU大于0.7,视为正样本
labels[max_overlaps >= 0.7] = 1
Anchor的筛选
由于Anchor的总数量接近于2万,并且大部分Anchor的标签都是背景,如果都计算损失的话则正、负样本失去了均衡,不利用网络的收敛。RPN默认选择256个Anchors进行损失的计算,其中最多不超过128个的正样本。如果数量超过了限定值,则进行随机选取。这里的256和128可以根据实际情况进行调整。
def forward(self, input):
for i in range(batch_size):
#如果正样本数量太多,则进行下采样随机选取
if sum_fg[i] > 128:
fg_inds = torch.nonzeros(labels[i] == 1).view(-1)
rand_num = torch.from_numpy(np.random.permutaion(fg_inds.size(0))).type_as(gt_boxes).long()
disable_inds = fg_inds[rand_num[:fg_inds.size(0) - num_fg]]
labels[i][disable_inds] = -1
#负样本同上
求解回归偏移真值
回归部分的偏移量真值需要利用Anchor与对应的标签求解得到,得到的偏移量的真值后,将其保存在bbox_targets中。还需要求解两个权值矩阵bbox_inside_weights和bbox_outside_weights,前者用来设置正样本回归的权重,正样本设置为1,负样本设置为0,因为负样本对应的是背景,不需要进行回归;后者的作用是平衡RPN分类损失与回归损失的权重,在此设置为1/256。
def forward(self, input):
#选择每一个Anchor对应最大IoU的标签进行偏移计算
bbox_targets = _compute_targets_batch(anchors, gt_boxes.view(-1, 5)[argmax_overlaps.view(-1), :].view(batch_size, -1, 5))
#设置两个权重向量
bbox_inside_weights[labels==1] = 1
num_examples = torch.sum(labels[i] >= 0)
bbox_outside_weights[labels == 1] = 1.0 / examples.item()
bbox_outside_weights[labels == 0] = 1.0 / examples.item()
筛选Proposal得到RoI
训练时生成的Proposal数量为2000个,仍然由很多背景框,完全可以针对Proposal进行进一步筛选,过程与RPN中筛选Anchor的过程类似,利用标签与Proposal构建IoU矩阵,通过与标签的重合程度选出256个正样本。
具体实现时,首先计算Proposal与所有的物体标签的IOU矩阵,然后根据IoU矩阵的值来筛选出符合条件的正负样本。筛选标准如下:
上述步骤得到每一个RoI对应的类别与偏移量真值,为了计算损失,还需要计算每一个RoI的预测量。
VGGNet网络提供了整张图像的feature map,因此可以利用feature map,将每一个RoI区域对应的特征提取出来,然后接入一个全连接网络,分别预测其RoI的分类与偏移量。
由于RoI是由各种大小宽高不同的Anchors经过偏移修正、筛选等过程生成的,因此其大小不一且带有浮点数,然而后续相连的全连接网络要求输入特征大小维度固定,这就需要有一个模块,能够把各种维度不同的RoI变换到维度相同 特征,以满足后续全连接网络的要求,于是RoI Pooling就产生了。
对RoI进行池化的思想在SPPNet中就已经出现,只不过在Fast RCNN中提出的RoI Pooling算法利用了最近邻插值算法将池化过程进行了简化,而在随后的MaskRCNN中进一步提出了RoI Align的算法,利用双线性插值,进一步提升了算法精度。
假如当前RoI大小为332x332,使用VGGNet的全连接层,其所需的特征向量维度为512x7x7,由于目前的特征图通道数为512,Pooling的过程就是如何获得7x7大小区域的特征。
在经过RoI Pooling层之后,特征被池化到了固定的维度,因此接下来可以利用全连接网络进行分类与回归预测量的计算。在训练阶段,最后需要计算预测量与真值的损失并反传优化,而在前向测试阶段,可以直接将预测量加到RoI上,并输出预测量。
审视Faster RCNN
Faster RCNN的缺点及改进方向:
特征融合:HyperNet
卷积神经网络的特点是,深层的特征体现了强语义特征,有利于进行分类与识别,而浅层的特征分辨率高,有利于进行目标的定位。原始的Faster RCNN方法仅仅利用了单层的feature map(例如VGGNet的conv5-3),对于小尺度目标的检测较差,同时高IoU阈值时,边框定位的精度也不高。
HyperNet方法认为单独一个feature map层的特征不足以覆盖RoI的全部特性,因此提出了一个精心设计的网络结构,融合了浅、中、深3个层次的特征,取长补短,在处理好区域生成的同时,实现了较好的物体检测效果。
HyperNet提出的特征提取网络结构如上图所示,以VGGNet作为基础网络,分别从第1/3/5个卷积组后提取特征,这3个特征分别对应着浅层、中层与深层的信息。然后,对浅层的特征进行最大化池化,对深层的特征进行反卷积,使得二者的分辨率都为原图大小的1/4,与中层的分辨率相同,方便进行融合。得到3个特征图后,再接一个5x5的卷积以减少特征通道数,得到通道数为42的特征。
在三层的特征融合前,需要先经过一个LRN(Local Response Normalization)处理,LRN层借鉴了神经生物 中的侧抑制概念,即激活的神经元抑制周围的神经元,在此的作用是增加泛化能力,做平滑处理。
最后将特征沿着通道数维度拼接到一起,3个通道数为42的特征拼接一起后形成通道数为126的特征,作为最终输出。
HyperNet融合了多层特征的网络有如下3点好处:
HyperNet实现了一个轻量化网络来实现候选区域生成。具体方法是,首先在特征图上生成3万个不同大小与宽高的候选框,经过RoI Pooling获得候选框的特征,再接卷积及相应的分类回归网络,进而可以得到预测值,结合标签就可以筛选出合适的Proposal。可以看出,这里的实现方法与Faster RCNN的RPN方法很相似,只不过先进行了RoI Pooling,再选择候选区域。
HyperNet后续的网络与Faster RCNN也基本相同,接入全连接网络完成最后的分类与回归。不同的地方是,HyperNet先使用了一个卷积降低通道数,并且Dropout的比例从0.5调整到了0.25。
由于提前使用了RoI Pooling,导致众多候选框特征都要经过一遍此 Pooling层,计算量较大,为了加速,可以在Pooling前使用一个3×3卷积 降低通道数为4,这种方法在大幅度降低计算量的前提下,基本没有精 度的损失。
总体来看,HyperNet最大的特点还是提出了多层融合的特征,因 此,其检测小物体的能力更加出色,并且由于特征图分辨率较大,物体 的定位也更精准。此外,由于其出色的特征提取,HyperNet的Proposal 的质量很高,前100个Proposal就可以实现97%的召回率。
值得注意,HyperNet使用到了反卷积来实现上采样,以扩大尺寸。 通常来讲,上采样可以有3种实现方法:双线性插值、反池化 (Unpooling)与反卷积。反卷积也叫转置卷积,但并非正常卷积的完 全可逆过程。具体实现过程是,先按照一定的比例在特征图上补充0, 然后旋转卷积核,再进行正向的卷积。反卷积方法经常被用在图像分割 中,以扩大特征图尺寸。
实例分割:Mask RCNN
Mask RCNN的网络 与Faster RCNN非常类似,但有3点区别:
网络结构详细介绍:
特征提取网络
ResNet的FPN基础网络是一个多层特征结合的结构,包含了自下而上、自上而下及横向连接3个部分,这种结构可以将浅层、中层、深层的特征融合起来,使得特征同时具备语义性与强空间性。
原始的FPN会 P2、P3、P4与P5,4个阶段的特征图,但在Mask RCNN中又增加了一个P6。将P5进行最大值池化即可得到P6,目的是获得更大感受野的特征,该阶段仅仅用在RPN网络中。从合适尺度的特征图中切出RoI,大的RoI对应到高语义的特征图中,如P5,小的RoI对应到高分辨率的特征图中,如P3。这样的分配方法保证了大的RoI从高语义的特征图上产生,有利于检测大尺度物体,小的RoI 高分辨率的特征图上产生,有利于小物体的检测。
RoI Align部分
Faster RCNN原始使用的RoI Pooling存在两次取整的操作,导致RoI选取出的特征与 最开始回归出的位置有一定的偏差,称之为不匹配问题,严重影响了检测或者分割的准确度。
RoI Align取消了取整操作,而是保留所有的浮点,然后通过双线性插值的方法获得多个采样点的值,再将多个采样点进行最大值的池化,即可得到该点最终的值。
由于使用了采样点与保留浮点的操作,RoI Align获得了更好的性能。
损失任务设计
得到感兴趣区域的特征后,Mask RCNN增加了Mask分支来进行图像分割,确定每一个像素具体属于哪一个类别。具体实现时,采用了FCN的网络结构,利用卷积与反卷积构建端到端的网络,最后对每一个像素分类,实现了较好的分割效果。
全卷积网络:R-FCN
Faster RCNN在RoI Pooling后采用了全连接网络来得到分类与回归的预测,这部分全连接网络占据了整个网络结构的大部分参数,而目前越来越多的全卷积网络证明不使用全连接网络效果会更好,以适应各种输入尺度的图片。
如果直接去掉RoI Pooling后的全连接,直接连接到分类与回归的网络中,通过实验发现这种方法检测的效果很差,因为基础的卷积网络是针对分类设计的,具有平移不变性,对位置不敏感,而物体检测对位置敏感。
FCN算法(Region-based Fully Convolutional Networks)利用位置敏感得分图(position-sensitive score maps)实现了对位置的敏感,并且采用了全卷积网络,大大减少了网络的参数量。
首先R-FCN采用了ResNet-101网络 Backbone,并在原始的100个卷积层后增加了一个1x1卷积,将通道数降低为1024。
为了增大后续特征图的尺寸,R-FCN将ResNet-101的下采样率从32降到了16。在第5个卷积组里将卷积的步长从2变为1,同时在这个阶段的卷积使用空洞数为2的空洞卷积以扩大感受野。降低步长增加空洞卷积可以在保持特征图尺寸的同时,增大感受野。
在特征图上进行1x1卷积,可以得到位置敏感图,其通道数为 k 2 ( c + 1 ) k^2(c+1) k2(c+1)。c代表物体类别,需要再加上背景这一类别。k的含义是将RoI划分为 k 2 k^2 k2个区域。
在RPN提供一个感兴趣区域后,对应到位置敏感得分图上,首先将RoI划分为kxk个网格,在Pooling时首先选取其所在区域的对应位置的特征,最终形成一个c+1维的kxk特征图。
再对这个c+1维的kxk特征进行通道求和,即可得到c+1维的向量,最后进行softmax即可完成RoI的分类预测。
对于RoI的位置回归,则与分类很相似,只不过位置敏感得分图的通道数为 k 2 ( c + 1 ) k^2(c+1) k2(c+1),而回归的敏感回归图的通道数为 k 2 × 4 k^2\times 4 k2×4,安装相同的方法进行Pooling,可形成通道数为4的kxk特征,求和可得到1x4的向量,即为回归的预测。
由于R-FCN去掉了全连接层,并且整个网络都是共享计算的,因此速度很快。此外,由于位置敏感得分图的存在,引入了位置信息,因此R-FCN的检测效果也更好。
级联网络:Cascade RCNN
得到RoI后,Faster RCNN通过RoI与标签的IoU值来判断该RoI是正样本还是负样本,默认的IoU阈值为0.5,这个阈值是一个超参数,对于检测的精度有较大影响。
阈值越高,选出的RoI会更接近真实物体,检测器的定位会更加准确,但此时符合条件的RoI会变少,正、负样本会更加不均衡,容易导致训练过拟合;另一方面,阈值越低,正样本会更多,有利于模型训练,但这时误检也会增多,从而增大了分类的误差。
对于阈值的问题,通过实验可以发现两个现象:
Cascade RCNN算法通过级联多个检测器来不断优化结果,每个检测器都基于不同的IoU阈值来界定正负样本,前一个检测器的输出作为后一个检测器 输入,并且检测器越靠后,IoU的阈值越高。
对于物体检测任务,Faster RCNN算法采用两阶的检测架构,首先利用RPN网络进行感兴趣区域生成,然后再对该区域进行类别的分类与位置的回归,这种方法虽然提升了精度但是限制了检测速度,YOLO算法利用回归的思想,使用一阶网络直接完成了物体检测,速度快,但是精度明显下降。
SSD(Single Shot Multibox Detector)算法借鉴了Faster RCNN与YOLO的思想,在一阶网络的基础上使用了固定框架进行区域生成,并利用了多层的特征信息,在速度与检测精度上都有了一定的提升。
算法流程如上图所示,输入图像首先经过VGGNet的基础网络,在此之后又增加了几个卷积层,然后利用3x3的卷积核在6个大小与深浅不同的特征层上进行预测,得到预选框的分类与回归预测值,最后直接预测出结果,或者求得网络损失。
SSD的数据增强整体流程包括光学变换与几何变换两个过程。光学变换包括亮度和对比度等随机调整,可以调整图像像素值的大小,并不会改变图像尺寸;几何变换包括扩展、裁剪和镜像等操作,主要负责进行尺度上的变化,最后再进行去均值操作。大部分操作都是随机的过程,尽可能保证数据的丰富性。
通过卷积网络得到了所有PriorBox的预测值与边框位置,为了得到最终的结果,还需要进行边框的匹配及损失计算。首先按照一定的原则,对所有的PriorBox赋予正、负样本的标签,并确定对应的真实物体标签,以方便后续损失的计算;有了对应的真值后,即可计算框的定位损失,这部分只需要正样本即可;同时,为了克服正、负样本的不均衡,进行难样本挖掘,筛选出数量是正样本3倍的负样本;最后计算筛选出的正、负样本的类别损失,完成整个网络前向计算的全过程。
SSD算法的优点:
SSD算法的缺点:
特征融合:DSSD
SSD采用多尺度的特征图预测物体,具有较大感受野的特征图检测大尺度物体,较小感受野特征图检测小尺度物体。这样会存在一个问题,即当小感受野特征图检测小物体时,由于语义信息不足,导致小物体检测效果差。
解决该语义信息不足的方法通常是深浅的特征图融合,DSSD正是从这一角度出发,提出了一套针对SSD多尺度预测的特征融合方法,改进了传统的上采样方法,并且可以适用于多种基础Backbone结构。
对于深浅层的特征融合,通常有3种方法:
SSD利用了感受野与分辨率不同的6个特征图进行后续分类与回归网络的计算,DSSD保留了6个特征图,但对这6个特征图进一步进行了融合处理,然后将融合后的结果送人后续分类与回归网络。
具体的做法是,将深层的特征图直接用作分类与回归,接着,该特征经过一个反卷积模块,并与更浅一层的特征进行逐元素相乘,将输出的特征用于分类与回归计算。类似的,继续将该特征与浅层特征进行反卷积与融合,共计输出6个融合后的特征图,形成一个沙漏式的结构,最后给分类与回归网络做预测。
在得到特征图后,DSSD也改进了分类与回归预测模块。SSD的预测模块是直接使用3x3卷积,而DSSD则包含一个残差单元,主路和旁路进行逐元素相加,然后再接到分类与回归的预测模块中。
DSSD的算法将深层的特征融合到了浅层特征图中,提升了浅层特征的语义性,提升了模型的性能,尤其是对于小物体的检测。
彩虹网络:RSSD
RSSD要解决的SSD的两个问题:
RSSD(Rainbow SSD)一方面利用分类网络增加了不同特征图之间的联系,减少了重复框的出现,另一方面提出了一种全新的深浅特征融合的方法,增加了特征图的通道数,大幅度提升了检测效果。
ARM部分
ARM部分首先经过一个VGG16或者ResNet-101的基础网络,然后在多个特征图上对应不同大小宽高的Anchors,并用卷积网络来提取这些Anchors的特征,进一步可以求得每一个Anchor的分类与回归损失。
ARM有两点作用:
TCB部分
TCB模块主要用于完成特征的转换操作。TCB模块首先对ARM中的每一个特征图进行转换,然后将深层的特征图融合到浅层的特征图中。
ODM部分
ODM基本采取了SSD多层特征图的网络结构,与SSD相比,有两个优势:
Faster RCNN算法利用了两阶结构,先实现感兴趣区域的生成,再进行精细的分类与回归,虽出色的完成了物体检测任务,但也限制了其速度,在更追求速度的实际应用场景下,应用起来仍存在差距。
YOLO系列的算法用回归的思想,使用一阶网络直接完成了分类与位置定位两个任务,速度极快。加速了物体检测在工业界的应用,开辟了物体检测算法的另一片天地。
YOLOv1使用一阶结构完成了物体检测任务,直接预测物体的类别与位置,没有RPN网络,也没有类似于Anchor的预选框,因此速度很快。
网络结构
YOLO一利用了卷积神经网络进行了特征提取,该结构与GoogLeNet模型有些类似,在该结构中,输入图像 尺寸固定为448x448,经过24个卷积层与两个全连接层后,最后输出额特征图大小为7x7x30.
特征图的意义
YOLO v1的网络结构并无太多创新之处,其精髓主要在最后7x7x30大小的特征图中。YOLO v1将输入图像划分成7x7的区域,每一个区域对应于最后特征图上的一个点,该点的通道数为30,代表了预测的30个特征。
YOLO v1在每一个区域内预测两个边框,这样整个图上一共预测7x7x2=98个框,这些边框大小与位置各不相同,基本上可以覆盖整个图上可能出现的物体。
如果一个物体的中心落在了某个区域内,则该区域就负责检测该物体。将该区域的两个框与真实物体框进行匹配,IoU更大的框负责回归真实物体框。
最终的预测特征由类别概率、边框的置信度及边框的位置组成。
YOLO v1 并没有先验框,而是直接在每个区域预测框的大小与位置,是一个回归问题。这样做能够成功检测的原因在于,区域本身就包含了一定的位置信息,另外被检测物体的尺度在一个可以回归的范围内。
一个区域内的两个边框共用了一个类别预测,在训练时会选取与物体IoU更大的一个边框,在测试时会选取置信度更高的一个边框,另一个会被舍弃,因此整张图最多检测出49(7x7区域)个物体。
YOLO v1采用了物体类别与置信度分开的预测方法,这一点与Faster RCNN不同,Faster RCNN将背景也当作了一个类别,共计21种,在类别预测中包含了置信度的预测。
损失计算
通过卷积网络得到每个边框的预测值后,为了进一步计算网络训练的损失,还需要确定每一个边框是对应着真实物体还是背景框,即区分开正、负样本。
当一个真实物体的中心落在了某个区域内时,该区域就负责检测该物体。将与真实物体有最大IoU的边框设为正样本,这个区域的类别真值为该真实物体的类别,该边框的置信度真值为1.
除了上述被赋予正样本的边框,其余边框都为负样本。负样本没有类别损失与边框位置损失,只有置信度损失,其真值为0.
优点
YOLO v1利用了回归的思想,使用轻量化的一阶n概率同时完成了物体的定位与分类,处理速度极快,可以达到45FPS,当使用更轻量级的网络时甚至可以达到155FPS。
缺点
由于每一个区域默认只有两个边框做预测,并且只有一个类别,因此YOLO v1有检测限制,会导致模型对于小物体,以及靠的特别近的物体检测效果不好。
由于没有类似于Anchor的先验框,模型对于新的或者不常见宽高比例的物体检测效果不好。
由于下采样率较大,边框的检测精度不高。
在损失函数中,大物体的位置损失权重与小物体的位置损失权重是一样的,这会导致同等比例的位置误差,大物体的损失会比小物体大。小物体的损失在总损失中占比较小,会带来物体定位的不准确。
YOLO v2相比于YOLO v1预测更加准确,速度更快,识别的物体类别也更多。
网络结构的改善
YOLO v2对于基础网络结构进行多种优化,提出了一个全新的网络结构DarkNet。原始的DarkNet拥有19个卷积层与5个池化层,在增加了一个Passthrough层后一共有22个卷积层,精度与VGGNet相当,但浮点运算量只有VGGNet的1/5左右,因此速度极快。
先验框的设计
YOLO v2吸收了Faster RCNN的优点,设置了一定数量的预选框,使得模型不需要直接预测物体尺度与坐标,只需要预测先验框到真实物体的偏移,降低了预测难度。
关于先验框,YOLO v2使用了聚类的算法来确定先验框的尺度,并且优化了后续的偏移计算方法。
正、负样本与损失函数
正负样本的选取,首先将预测的位置偏移量作用到先验框上,得到预测框的真实位置。
如果一个预测框与所有真实物体的最大IoU小于一定阈值(默认为0.6)时,该预测框视为负样本。
每一个真实物体的中心点落在了某个区域内,该区域就负责检测该物体。将与该物体有最大IoU的预测框视为正样本。
确定正负样本后,计算网络损失。
训练技巧
优点
使用了先验框、特征融合等方法,同时利用 了多种训练技巧,使得模型在保持极快速度的同时大幅度提升了检测的 精度。
缺点
YOLO v3吸收了当前优秀的检测框架的思想,如残差网络和特征融合等,称为DarkNet-53,但速度并没有之前的版本快,在保证实时性的前提下追求检测的精度。YOLO v3提供了一个更轻量化的网络tiny-DarkNet,在模型大小与速度上,实现了SOTA(state of art)的效果。
DarkNet-53的新特性:
多尺度预测
YOLO v3输出了3个大小不同的特征图,从上到下分别对应深层、中层、浅层的特征。深层的特征图尺寸小,感受野大,有利于检测大尺度物体,而浅层的特征图则与之相反,更便于检测小尺度物体,类似于FPN结构。
YOLO v3依然使用了预选框Anchor,由于特征图数量不再是一个,因此匹配方法也要相应的进行改变。使用聚类的算法得到9种不同大小宽高的先验框,然后按照下表进行先验框的分配,这样,每一个特征图上的一个点只需要预测3个先验框,而不是YOLO v2中的5个。
YOLO v3的基础网络更像是SSD与FPN的结合。YOLO v3默认使用了COCO数据集,一共有80个物体类别,因此一个先验框需要80维的类别预测值、4个位置预测及1个置信度预测,3个预测框一共需要3x(80+5)=255维,也就是每一个特征图的预测通道数。
softmax改为Logistic
Softmax函数输出的多个类别预测之间会相互抑制,只能预测出一个类别,而Logistic分类器相互独立,可以实现多类别的预测。
YOLO v3优缺点:
对于轻量化的网络设计,较为流形的有SqueezeNet、MobileNet及ShuffleNet等结构。SqueezeNet采用了精心设计的压缩再扩展的结构,MobileNet使用了效率更高的深度可分离卷积,ShuffleNet提出了通道混洗的操作,进一步降低了模型的计算量。
SqueezeNet使用Fire Module,输入特征尺寸为HxW,通道数为M,经过Squeeze层与Expand层,然后进行融合处理。
深度可分离卷积
深度可分离卷积(Depthwise Separable Convolution),将卷积的过程分为逐通道卷积与逐点1x1卷积两步。虽然扩展为两步,但减少了冗余计算,总体上计算量有了大幅度降低。
MobileNet v2
MobileNet v2吸收了残差结构取代了原始的卷积堆叠方式,提出了Inverted Residual Block结构,根据卷积的步长,在步长为1时使用了残差连接,融合 方式为逐元素相加。
为了降低计算量,当前先进的卷积网络通常在3x3卷积之前增加一个1x1卷积,用于通道间的信息流通与降维。然而在ResNet、MobileNet等高性能的网络中,1x1卷积却占用了大量的计算资源。
ShuffleNet v1 从优化网络结构的角度出发,利用组卷积与通道混洗(Channel Shuffle)的操作有效降低了1x1逐点卷积的计算量,是一个极为高效的轻量化网络。
通道混洗
当前先进的轻量化网络大都使用深度可分离卷积或者组卷积,以降低网络的计算量,但这两种操作都无法改变特征的通道数,因此需要使用1x1卷积。逐点的1x1卷积有两个特性:
ShuffleNet v2
原有的一些轻量化方法在衡量模型性能时,通常使用浮点运算量FLOPs(Floating Point Operations)作为主要指标。FLOPs是指模型在进行一次前向传播时所需的浮点计算次数,其单位为FLOP,可以用来衡量模型的复杂度。除此之外,还有两个重要的指标:内存访问时间(Memory Access Cost, MAC)与网络的并行度。
分析影响网络运行速度,提出建立高性能网络的4个基本规则:
当前的物体检测算法为了保证召回率,对于同一个真实物体往往会有多于1个的候选框输出。由于多余的候选框会影响检测精度,因此需要利用NMS过滤掉重叠的候选框,得到最佳的预测输出。
NMS基本过程
物体检测网络通常在最后增加一个非极大值抑制操作,即NMS,将重复冗余的预测去掉。
量化指标:
NMS简约实现方法:
NMS方法简单但是在更高的物体检测需求下,存在如下缺陷:
抑制得分:SoftNMS
NMS方法虽然有效过滤了重复框,但也容易将本属于两个物体框中得分低的框抑制掉,从而降低了召回率。
NMS的计算公式:
Soft NMS对于IoU大于阈值的边框,没有将其得分直接置0,而是降低该边框的得分,利用边框的得分 IoU来确定新的边框得分,如果当前边框与边框Mde IoU超过设定阈值时,边框的得分呈线性的衰减。但是这并不是一个连续的函数,当一个边框与M的重叠IoU超过阈值时,其得分会发生跳变,对检测结果产生较大的波动。
SoftNMS的重置函数:
Soft NMS的计算复杂度与NMS相同,是一种更为通用的非极大值抑制方法,可以将NMS看作Soft NMS的二值化特例。当然Soft NMS也是一种贪心算法,并不能保证找到最优的得分重置映射。Soft NMS在不影响前向速度的前提下,能够有效提升物体检测精度。
加权平均:Softer NMS
NMS与Soft NMS算法都使用了预测分类置信度作为衡量指标,即假定分类置信度越高的边框,其位置也更为精准。具有高分类置信度的边框其位置并不是最精准的。因此,位置的置信度与分类置信度并不是强相关的关系,直接使用分类置信度作为NMS的衡量指标并非是最佳选择。Softer NMS新增加了一个定位置信度的预测,使得高分类置信度的边框位置变得更加准确,从而有效提升了检测的性能。
Softer NMS对预测边框与真实物体做了两个分布假设:
基于这两个假设,Softer NMS提出了一种基于KL散度的边框回归损失函数KL Loss。KL散度是用来衡量两个概率分布的对称性衡量,KL散度越接近于0,则两个概率分布越相似。
KL Loss是最小化预测边框的高斯分布与真实物体的delta分布之间的KL散度。即预测边框分布越接近于 物体分布,损失越小。
描述边框的预测分布,除了预测位置之外,还需要预测边框的标准差,Softer NMS的预测结构:
边框的标准差 被看做边框的位置置信度,因此Softer NMS利用该标准差也改善了NMS过程。具体过程大体与NMS相同,只不过利用标准差改善了高得分边框的位置坐标,从而使其更为精准。
定位置信度:IoU-Net
在当前的物体检测算法中,物体检测的分类与定位通常被两个分支预测。对于候选框的类别,模型给出了 类别预测,可以作为分类置信度,然而对于定位而言,回归模块通常只预测了一个边框的转换关系,而缺失了定位的置信度,即框的位置准不准,并没有一个预测结果。
定位置信度的缺失,只能将分类的预测值作为边框排序的依据,分类预测值高的边框不一定拥有与真实框最接近的位置,因此这种标准不平衡可能会导致更为准确的边框被抑制掉。
IoU-Net增加了一个预测候选框与真实物体之间的IoU分支,改善了NMS过程,提升了检测器的性能。
当前主流的物体检测算法都是将物体检测当做分类问题来考虑,先使用先验框或者RPN等生成感兴趣的区域,再对该区域进行分类与回归位置。基于分类思想的物体检测算法存在样本不均衡的问题,因而会降低模型的训练效率与检测精度。
缓解方法:
在线难样本挖掘:OHEM
OHEM(Online Hard Example Mining)难样本挖掘方法,先让模型收敛于当前的工作数据集,然后固定该O型,在数据集中去除简单的样本,添加一些当前非判断的样本,精细新的训练,这样的交替训练可以使得模型性能达到最优。
专注难样本:Focal Loss
过拟合现象的本质是模型学习到了训练数据自身的特性,非全局的特性。
小物体由于其尺寸较小,可利用的特征有限,这使得其检测较为困难,当前的检测算法对于小物体并不友好。
降低下采样率与空洞卷积
去掉Pooling层hUI减小感受野,空洞卷积在不改变网络分辨率的基础上增加了网络的感受野。但是采用空洞卷积也不能保证修改后与修改前的感受野完全相同,但能够最大限度的使感受野在可接受的误差内。
Anchor设计
多尺度训练
当前的多尺度训练(Multi Scale Training, MST)通常是指设置几种不同的图片输入尺度,训练时从多个尺度中随机选取一种尺度,将输入图片缩放到该尺度并送人网络中,是一种简单又有效的提升多尺度物体检测的方法。虽然次迭代时都是单一尺度的,但每次都各不相同,增加了网络的鲁棒性,又不至于增加过多的计算量。
特征融合
尺度归一化:SNIP
经典的克服多尺度检测的方法:
SNIP使用了类似于MST的多尺度训练方法,构建了3个尺度的图像金字塔,但在训练时,只对指定范围内的Proposal进行反向传播,而忽略掉过大或者过小的Proposal。
三叉戟:TridentNet
不同尺度的检测性能与感受野呈正相 关,即大的感受野对于大物体更友好,小的感受野对于小物体更友好。
拥挤与遮挡会带来物体的信息缺失,物体的部分 区域是不可见的,边界模糊,容易造成检测器的误检 漏检,从而降低检测性能。
改进NMS:因为NMS对行人遮挡检测影响较大,可以改进NMS提升遮挡检测的性能。
增加语义信息:遮挡会造成行人部分信息缺失,因此可以尝试引入额外的特征,如分割信息、梯度和边缘信息等。
划分多个part处理:由于行人之间的形状较为相似,因此可以利用该先验信息,将行人按照不同部位,如头部、上身、手臂等划分为多个part进行单独处理,然后再综合考虑,可以在一定程度上缓解遮挡带来的整体信息缺失。
与其他标签的排斥:RepGT
RepGT损失设计思想是为了让当前预测框尽可能的远离周围的标签物体,周围标签物体指的是,除了预测框本身要回归的物体之外,与该预测框有最大IoU的物体标签。
与其他预测框的排斥:RepBox
让预测框尽可能的远离周围预测框,降低两者之间的IoU,从而避免属于两个物体的预测框,其中一个被NMS抑制掉。
根据图像中的物体标签,可以将最后的预测框划分为多个组。假设有g个物体,则划分为同组和不同组:同组之间的预测框回归的是同一个物体标签,不同组之间的预测框对应的是不同的物体标签。
对于不同组之间的预测框,希望他们之间的重叠区域越小越好,可以使用IoU来衡量重叠区域。用Smooth优化函数最小化IoU。
OR-CNN
精度与速度的权衡
卷积网络的可解释性与稳定性
卷积网络个并没有我们想象的那么稳定,有时会存在一些怪异的现象。例如,房间里的大象就针对检测器的鲁棒性做了实验,将数据集中某图像中的一只大象移植到了别的图像中,不断的平移,观察检测结果:
目前物体的检测是间矩形边框内的所有特征考虑在内,并且较大的感受野使得最终的RoI会包含边框外较多的特征,而这两种本不属于物体的特征会影响检测的性能。
拥有池化等操作,卷积网络对于微小的位移、变形等具有一定的鲁棒性。当图像中的物体发生肉眼难以辨别的微小平移时,检测置信度发生了巨大的变化。当网络中拥有了具有一定步长的降采样操作后,图像必须平移降采样的整数倍后,才会体现出平移的不变性。
训练:微调还是随机初始化
当前较为通用的训练检测模型的方法是,先在ImageNet数据集上进行预训练,然后在利用自己的数据进行微调,由于ImageNet的预训练模型可以共享,很多情况下并不需要自己亲自去训练,因此这种方法可以大大缩短模型收敛的时间。
ImageNet的预训练并不是必要的,如果有足够的计算资源与数据,完全可以使用随机初始训练方法。预训练的方式可以加速训练过程,并且适用于自己的数据集规模比较小的情况,但不一定能提升最终的检测精度。
考虑物体间关系的检测
通过特征与几何之间的交互,对物体关系进行建模,能够有效提升检测的性能。
Anchor的检测算法中边框的变化,对于二阶的算法,第一阶段RPN会对Anchor进行有效的筛选,生成更有效的精准的Proposal,送人第二阶段,最终得到预测的边框。一阶的算法相当于把固定的Anchor当做了Proposal,通过高效的特征与正、负样本的控制,直接预测出了物体。
Anchor-Free算法已经达到了与基于Anchore的检测器相同甚至更好的检测效果。
基于角点的检测:CornerNet
将传统的预测边框思想转化为预测边框的左上角与右下角两个角点问题,然后再对属于同一个边框的角点进行组合。
检测中心点:CenterNet
将物体检测问题变成一个关键点的估计问题,通过预测物体的中心点位置及对应物体的长与宽,实现当前检测精度与速度最好的权衡。
CenterNet思想与网络:
锚框自学习:Guided Anchoring
Guided Anchoring主要分为锚框预测与特征自适应两部分,其中锚框 部分负责预测Anchor出现的位置与形状,而特征自适应模块可以将Anchor作用到特征图上,从而提取出更有针对性的特征。
锚框预测:Anchor Generation
Guided Anchoring利用网络特征自动的预测了Anchor的分布,其中包含Anchor出现的位置与形状两个特征。Guided Anchoring使用了与CenterNet预测物体类似的思想,采用了两个分支来预测Anchor。
Guided Anchoring在每一个点上采样了9组不同宽高的边框,来取代预测的Anchor,进而 匹配关系,确定优化对象。
特征自适应:Feature Adaption
然而,Guided Anchoring中的Anchor形状不一,特征图事先并无法 得到其需要预测的Anchor的形状,但是后续却要预测该Anchor的类别与 位置偏移,这里就存在Anchor与表征Anchor的特征不适配的问题。
为了解决该问题,Guided Anchoring在Anchor预测后增加了一个特 征自适应层(Feature Adaption),具体实现过程中, Feature Adaption使用了一个3×3可变形卷积作用于特征图上,以适配 Anchor的形状。
与普通可变形卷积不同,这里的偏置取自于预测的Anchor,具体是 利用一个1×1卷积作用于预测的Anchor宽高,实现极为巧妙。从功能上 理解,这里的特征自适应有些类似于RoI Pooling层。