0%

『面经』深度学习基础

简介

一些关于深度学习的基础知识。


基本网络结构

卷积

img

  • 假设输入为 \((B, C, H, W)\), 卷积核为 \((C_{in}, C_{out}, ks_1, ks_2)\), 则有 \(C_{in} \times C_{out}\) 个不同参数的 \(ks_1 \times ks_2\) 的卷积核、
  • 需要了解怎么计算输出特征图的大小(自行理解,强行记忆不太好)
    • 对大小为 \(H \times W\) 的 feature map,考虑卷积核大小为 \(h \times w\), 填充(padding) 为 \(a\), 步长(stride) 为 \(b\), 则:\(\left\{\begin{array}{c}H^{\prime}=\frac{H+2 * a-h}{b}+1 \\ W^{\prime}=\frac{W+2 * a-w}{b}+1\end{array}\right.\)
  • 局部连接:不是全连接,而是使用size相对input小的kernel在局部感受视野内进行连接(点积运算)
  • 权值共享:在一个卷积核运算中,每次都运算一个感受视野,通过滑动遍历的把整个输入都卷积完成,而不是每移动一次就更换卷积核参数

1x1卷积

作用:

  1. 降维和升维
  2. 不同通道信息交互(cross-channel correlations and spatial correlations)
    • \(1 \times 1\) 卷积其实是对不同 channel 间的信息进行了线性组合
    • 深度可分离卷积(depth-wise seperable convolution)实现了对cross-channel correlation和spatial correlation的彻底解耦
  3. 增加非线性特征:后接非线性激活函数

池化

image-20230708144530420

作用:

  1. 特征不变形:池化操作是模型更加关注是否存在某些特征而不是特征具体的位置。
  2. 特征降维:池化相当于在空间范围内做了维度约减,从而使模型可以抽取更加广范围的特征。同时减小了下一层的输入大小,进而减少计算量和参数个数。
  3. 在一定程度上防止过拟合,更方便优化。

池化的大小计算和卷积类似

特征融合

特征融合有两种常用方式:

  1. \(add\) 操作,例如 ResNet 中的残差连接
  2. \(concate\) 操作,例如 UNet 中的连接

联系:对于 \(Concat\) 的操作,通道数相同且后面带卷积的话,\(add\) 等价于\(concat\) 之后对应通道共享同一个卷积核,采用 \(add\) 操作,我们相当于加入一种先验。当两个分支的特征信息比较相似,可以用 \(add\) 来代替 \(concat\),这样可以更节省参数量。

区别:

  • 对于 \(Concat\) 操作而言,通道数的合并,也就是说描述图像本身的特征增加了,而每一特征下的信息是没有增加。
  • 对于 \(add\) 层更像是信息之间的叠加。这里有个先验,\(add\) 前后的 \(tensor\) 语义是相似的。

注意力机制

image-20230708144834321

这张图主要是针对于机器翻译中用的,在翻译的时候,每一个输出\(Query\)需要于输入\(Key\)的各个元素计算相似度,再与\(Value\) 进行加权求和~

对于\(CV\)领域中,我们一般都是用矩阵运算了,不像\(NLP\)中的任务,需要按照时刻进行,\(CV\)中的任务,就是一个矩阵运算。

用公式来概括性地描述就是: \[ \text { Attention }(\text { Q, S })= \text { Similarity }\left(\text { Q}, K \right) * \text { V } \] 划重点,我们有不同的方法来衡量相似度,这里我们主要有以下几种方案来衡量相似度: \[ \begin{array}{r} \text { 点积: Similarity }\left(\text { Q }, \mathrm{K}\right)=\text { Q } \cdot \mathrm{K} \\ \text { Cosine相似性: } \text { Similarity }\left(\text { Q }, K\right)=\frac{\text { Q } \cdot K}{\mid \text { Q }|\cdot| K \mid} \\ M L P \text { 网络: Similarity }\left(\text { Q, } K \right)=M L P\left(\text { Q }, K\right) \end{array} \] 当有了相似度之后,我们需要对其进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布,越重要的部分,越大!越不重要的部分,越小。我们采用\(Softmax\)为主,当然也有一些使用\(Sigmoid\)这样来进行运算,都是ok的~

因此,这个权重\(Mask\)的值可以这么计算: \[ Mask = \operatorname{softmax}\left(\frac{Q K^{T} }{\sqrt{d_{k} }}\right) \] 其中\(\sqrt{d_{k} }\)表明将数据进行下缩放,防止过大了。

最后就是得到\(Attention\)的输出了: \[ \text { Attention }(Q, K, V)=Mask * V \]

Non-local Attention

image-20230708145106003

输入特征\(x\),通过\(1 * 1\)卷积来得到\(Key,Query,Value\),这里的三个矩阵是不同的,因此上文中是假设 相同。

其中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Self_Attn(nn.Module):
""" Self attention Layer"""
def __init__(self,in_dim,activation):
super(Self_Attn,self).__init__()
self.chanel_in = in_dim
self.activation = activation

self.query_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//8 , kernel_size= 1)
self.key_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//8 , kernel_size= 1)
self.value_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim , kernel_size= 1)
self.gamma = nn.Parameter(torch.zeros(1))

self.softmax = nn.Softmax(dim=-1) #
def forward(self,x):
"""
inputs :
x : input feature maps( B X C X W X H)
returns :
out : self attention value + input feature
attention: B X N X N (N is Width*Height)
"""
m_batchsize,C,width ,height = x.size()
proj_query = self.query_conv(x).view(m_batchsize,-1,width*height).permute(0,2,1) # B X CX(N)
proj_key = self.key_conv(x).view(m_batchsize,-1,width*height) # B X C x (*W*H)
energy = torch.bmm(proj_query,proj_key) # transpose check
attention = self.softmax(energy) # BX (N) X (N)
proj_value = self.value_conv(x).view(m_batchsize,-1,width*height) # B X C X N

out = torch.bmm(proj_value,attention.permute(0,2,1) )
out = out.view(m_batchsize,C,width,height)

out = self.gamma*out + x
return out,attention

代码看上去还是比较容易懂得,主要就是 \(torch.bmm()\) 函数,它可以将纬度为 \(B \times N \times C\) 矩阵与 \(B \times C \times N\) 的矩阵相乘的到 \(B \times N \times N\) 的矩阵。再使用 \(Softmax\) 来得到归一化之后的矩阵,结合残差,得到最后的输出!

CBAM

\(CBAM\)\(Channel\) \(Attention\)\(Spatial\) \(Attention\)组合而成。

image-20230708151931479

其中的\(Channel\) \(Attention\) 模块,主要是从C x H x w 的纬度,学习到一个C x 1 x 1的权重矩阵。

论文中的图如下:

image-20230708151924403

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ChannelAttentionModule(nn.Module):
def __init__(self, channel, reduction=16):
super(ChannelAttentionModule, self).__init__()
mid_channel = channel // reduction
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)

self.shared_MLP = nn.Sequential(
nn.Linear(in_features=channel, out_features=mid_channel),
nn.ReLU(inplace=True),
nn.Linear(in_features=mid_channel, out_features=channel)
)
self.sigmoid = nn.Sigmoid()

def forward(self, x):
avgout = self.shared_MLP(self.avg_pool(x).view(x.size(0),-1)).unsqueeze(2).unsqueeze(3)
maxout = self.shared_MLP(self.max_pool(x).view(x.size(0),-1)).unsqueeze(2).unsqueeze(3)
return self.sigmoid(avgout + maxout)

当然,我们可以使用\(Query,Value, Key\)的形式来对它进行修改成一个统一架构,只要我们可以学习到一个在通道纬度上的分布矩阵就好。

如下方伪代码,\(key, value, query\) 均为\(1 * 1\)卷积生成。

1
2
3
4
5
6
7
8
9
# key: (N, C, H, W)
# query: (N, C, H, W)
# value: (N, C, H, W)
key = key_conv(x)
query = query_conv(x)
value = value_conv(x)

mask = nn.softmax(torch.bmm(key.view(N, C, H*W), query.view(N, C, H*W).permute(0,2,1)))
out = (mask * value.view(N, C, H*W)).view(N, C, H, W)

对于\(Spatial\) \(Attention\),如图所示:

image-20230708151908154

参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
class SpatialAttentionModule(nn.Module):
def __init__(self):
super(SpatialAttentionModule, self).__init__()
self.conv2d = nn.Conv2d(in_channels=2, out_channels=1, kernel_size=7, stride=1, padding=3)
self.sigmoid = nn.Sigmoid()

def forward(self, x):
avgout = torch.mean(x, dim=1, keepdim=True)
maxout, _ = torch.max(x, dim=1, keepdim=True)
out = torch.cat([avgout, maxout], dim=1)
out = self.sigmoid(self.conv2d(out))
return out

采用\(Query, Key, Value\)的框架来进行改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
key = key_conv(x)
query = query_conv(x)
value = value_conv(x)

b, c, h, w = t.size()
query = query.view(b, c, -1).permute(0, 2, 1)
key = key.view(b, c, -1)
value = value.view(b, c, -1).permute(0, 2, 1)

att = torch.bmm(query, key)

if self.use_scale:
att = att.div(c**0.5)

att = self.softmax(att)
x = torch.bmm(att, value)

x = x.permute(0, 2, 1)
x = x.contiguous()
x = x.view(b, c, h, w)

激活函数


损失函数


优化器


正则化

Dropout

目前来说,\(Dropout\)有两种。第一种就是传统的\(Dropout\)方案。另一种,就是我们的吴恩达老师所讲的\(Inverted\) \(Dropout\)了。 这两种方案本质上没什么区别,在实现的过程中是有区别的,接下来我们会详细进行比较。

这里先给出其数学公式:

\(Training\) \(Phase\) : \[ \mathbf{y}=f(\mathbf{W} \mathbf{x}) \circ \mathbf{m}, \quad m_{i} \sim \operatorname{Bernoulli}(p) \] \(Testing\) \(Phase\) : \[ \mathbf{y}=(1-p) f(\mathbf{W} \mathbf{x}) \]

首先,看下\(Dropout\)论文中给出的图像,了解下\(Dropout\)究竟干了个啥。

img

概括来说:Dropout提供了一种有效地近似组合指数级的不同经典网络架构的方法。

\(Dropout\)应用到神经网络中,相当于从该网络中采样一些子网络。这些子网络由所有在\(Dropout\)操作后存活下来的单元节点组成。如果一个神经网络有\(n\)个节点,则能够产生\(2^{n}\)中可能的子网络。在测试阶段,我们不是直接将这些指数级的子网络显式的取平均预测,而是采用一种近似的方法:仅使用单个神经网络,该网络的权重是先前训练的网络权重乘以失活概率\(p\)。这样做可以使得在训练阶段隐藏层的期望输出(在随机丢弃神经元的分布)同测试阶段是一致的。这样可以使得这\(2^{n}\)个网络可以共享权重。

  1. \(Inverted\) \(Dropout\)

先看下\(Inverted\) \(Dropout\)的实现代码,假设,我们的输入是\(x\)\(p\)表示随机丢弃的概率, \(1-p\)表示的是神经元保存的概率。则\(Inverted\) \(Dropout\)的实现过程如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
def dropout(x, p):
if p < 0. or p >1.
# 边界条件,在写代码的时候,一定要仔细!!!p为随机丢弃的概率
raise Exception("The p must be in interval [0, 1]")
retain_prob =1. -p
#我们通过binomial函数,生成与x一样的维数向量。
# binomial函数就像抛硬币一样,每个神经元扔一次,所以n=1
# sample为生成的一个0与1构成的mask,0表示抛弃,1表示保留
sample =np.random.binomial(n=1, p=retain_prob, size=x.shape)
x *= sample # 与0相乘,表示将该神经元Drop掉
x /= retain_prob
return x

这里解释下,为什么在后面还需要进行 x/=retain_prob 的操作?

假设该层是输入,它的期望是\(a\),在不使用\(Dropout\)的时候,它的期望依旧是\(a\)。如果该层进行了\(Dropout\), 相当于有\(p\)的概率被丢弃,\(1-p\)的概率被保留,则此层的期望为\((1-p) * a * 1+ p * a * 0 = (1-p) * a\),为了保证输入与输出的期望一致,我们需要进行代码中\(x /= retain\_prob\)这一步。

  1. 传统\(Dropout\)

对于传统的\(Dropout\),在训练的时候,我们不需要进行\(x /= retain\_prob\)的这一步,直接进行神经元\(Drop\)操作。此时,假设输入\(x\)的期望是\(a\),则此时的输出期望为\((1-p)*a\)。我们在测试的时候,整个神经元是保留的,因此输出期望为\(a\)。为了让输入与输出的期望一致,则在测试的阶段,需要乘以\((1-p)\),使其期望值保持\((1-p)*a\)

传统的dropout和Inverted-dropout虽然在具体实现步骤上有一些不同,但从数学原理上来看,其正则化功能是相同的,那么为什么现在大家都用Inverted-dropout了呢?主要是有两点原因:

  • 测试阶段的模型性能很重要,特别是对于上线的产品,模型已经训练好了,只要执行测试阶段的推断过程,那对于用户来说,推断越快用户体验就越好了,而Inverted-dropout把保持期望一致的关键步骤转移到了训练阶段,节省了测试阶段的步骤,提升了速度。

  • dropout方法里的 \(p\)是一个可能需要调节的超参数,用Inverted-dropout的情况下,当你要改变 \(p\) 的时候,只需要修改训练阶段的代码,而测试阶段的推断代码没有用到 \(p\) ,就不需要修改了,降低了写错代码的概率。

DropConnect

与 dropout 类似但对象是网络权重而不是特征图

这里给出一个Github上面针对卷积核的2D DropConnect操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.conv import _ConvNd,_pair

class DropConnectConv2D(_ConvNd):
def __init__(self, in_channels, out_channels, kernel_size, stride=1,
padding=0, dilation=1, groups=1,
bias=True, padding_mode='zeros', p=0.5):
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
super(DropConnectConv2D, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
False, _pair(0), groups, bias, padding_mode)
self.dropout = nn.Dropout(p)
self.p = p

def _conv_forward(self, input, weight):
if self.padding_mode != 'zeros':
return F.conv2d(F.pad(input, self._reversed_padding_repeated_twice, mode=self.padding_mode),
weight, self.bias, self.stride,
_pair(0), self.dilation, self.groups)
return F.conv2d(input, weight, self.bias, self.stride,
self.padding, self.dilation, self.groups)

def forward(self, input):
return self._conv_forward(input, self.dropout(self.weight) * self.p)

if __name__=='__main__':
conv = DropConnectConv2D(1,1,3,1,bias=False).train()
conv.weight.data = torch.ones_like(conv.weight)

a = torch.ones([1,1,3,3])
print(a)
print(conv(a))

上面的代码,我们其实只需要主要看下\(self.dropout(self.weight) * self.p\)这么一部分代码。

Spatial Dropout

考虑特征图的相邻位置有强关联,所以一次性 drop 整个通道的特征图

Stochastic Depth

这个 drop 的是 ResNet 的模块

image-20230708170713189

若网络总共有 \(L\)\(block\),我们给每个\(block\)都加上了一个概率 \(p_{l}\)

在训练时: 根据\(p_{l}\) 用一个\(bernoulli\)随机变量生成每个\(block\)的激活状态 \(b_{l}\),最终把 \(ResNet\)\(bottleneck\) \(block\),从\(H_{l}=\operatorname{ReL} U\left(f_{l}\left(H_{l-1}\right)+idtentity\left(H_{l-1}\right)\right)\)调整成了\(H_{l}=\operatorname{ReLU}\left(b_{l} f_{l}\left(H_{l-1}\right)+idtentity\left(H_{l-1}\right)\right)\)

其中,当\(b_{l}=0\)时,表明这个\(block\)未被激活,此时\(H_{l}=\operatorname{ReL} U\left(identity\left(H_{l-1}\right)\right)\)。特别地是。其中\(p_{l}\)是从\(p_{0}=1\)线性衰减到\(p_{L}=0.5\),即\(p_{l}=1-\frac{l}{L}\left(1-p_{L}\right)\)

在预测的时候:

\(block\)被定义为: \(H_{l}^{T e s t}=\operatorname{ReL} U\left(p_{l} f_{l}\left(H_{l-1}^{\text {Test } }\right)+identity\left(H_{l-1}^{\text {Test } }\right)\right)\)相当于将\(p_{l}\)与该层的残差做了一个权值融合了。

个人觉得这样\(Drop\)有以下两个好处

  • 这种引入随机变量的设计有效的克服了过拟合使模型有了更好的泛化能力。这种\(Drop\)的方式,本质上一种模型融合的方案。由于训练时模型的深度随机,预测时模型的深度确定,事实上是在测试时把不同深度的模型融合了起来。
  • 以往的\(Dropout\)或者\(DropConnect\)都主要是在全连接层进行,这里是对整个网络进行\(Drop\)的。

这里给出一个参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class BottleNeck(nn.Module):
def __init__(self, in_channels, out_channels, stride):
super(BottleNeck, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, stride=stride, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=(out_channels * 4), kernel_size=1),
nn.BatchNorm2d((out_channels * 4)),
nn.ReLU(inplace=True)
)
self.relu = nn.ReLU(inplace=True)
self.downsample = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=(out_channels * 4), kernel_size=1, stride=stride),
nn.BatchNorm2d((out_channels * 4))
)

def forward(self, x, active):
if self.training:
if active == 1:
print("active")
identity = x
identity = self.downsample(identity)
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x + identity
x = self.relu(x)
return(x)
else:
print("inactive")
x = self.downsample(x)
x = self.relu(x)
return(x)
else:
identity = x
identity = self.downsample(identity)
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.prob * x + identity
x = self.relu(x)
return(x)

L1与L2正则化

正则化(\(Regularization\)) 是机器学习中对原始损失函数引入惩罚项,以防止过拟合或提高模型泛化性能的一类方法的统称。所谓惩罚是指对损失函数中的某些参数做一些限制。此时目标函数变成了原始损失函数+惩罚项,常用的正则项一般有两种,英文称作 \(l_{1}−norm\)\(l_{2}−norm\),中文称作 \(L1\) 正则化和 \(L2\) 正则化,或者 \(L1\) 范数和 \(L2\) 范数(实际是 \(L2\) 范数的平方)。

对于线性回归模型,使用 \(L1\) 正则化的模型叫做 \(Lasso\) 回归,使用 \(L2\) 正则化的模型叫做 \(Ridge\) 回归(岭回归)。

L1正则化

假设带有\(L1\)正则化的目标函数为:

\[ J=J_0 + ||W||_1 = J_0 + \alpha\sum|w|\ \ \ \ \ \ \ \ \ (1) \]

其中,\(J_0\)为原始的损失函数,\(\alpha \sum |w|\)为L1正则化项,\(\alpha\)为正则化系数,\(w\) 表示特征的系数(\(x\)的参数),可以看到正则化项是对系数做了限制。\(L1\) 正则化是指权值向量 \(w\) 中各个元素的绝对值之和,通常表示为 \(\| w \|_1\)

\(L1\) 范数符合拉普拉斯分布,是不完全可微的。表现在图像上会有很多角出现。这些角和目标函数的接触机会远大于其他部分。就会造成最优值出现在坐标轴上,因此就会导致某一维的权重为\(0\) ,产生稀疏权重矩阵,进而防止过拟合。

\(L1\)正则化项相当于对原始损失函数\(J_0\)做了一个约束。我们令\(L = \alpha\sum|w|\),那么整个目标函数可以写成: \[ J= J_0 + L \ \ \ \ \ (2) \]

我们的目的就是求出在约束条件\(L\)下,\(J_0\)取最小值的解。为了方便理解,我们考虑二维的情况,此时\(L = |w_1| + |w_2|\)

L1正则化图示

图中等高线是 \(J_0\) 的等高线,黑色菱形是 \(L\) 函数的图形。图中当等高线 \(J_0\)\(L\) 图形首次相交的地方就是最优解。上图中 \(J_0\)\(L\) 在一个顶点处相交,这个顶点就是最优解 \(w^∗\)

拓展到多维,\(L\) 函数就会有很多突出的角(二维情况下四个,多维情况下更多),\(J_0\) 与这些角接触的概率远大于与 \(L\) 其它部位接触的概率(这是很直觉的想象,突出的角比直线的边离等值线更近),而在这些角的位置上使很多权重为 \(0\)。所以在最优解处,\(L1\) 正则化就可以产生稀疏模型,进而可以用于特征选择。

\(\alpha\)正则化系数,可以控制 \(L\) 图形的大小,\(\alpha\)越小,\(L\) 图形越大,\(\alpha\)越大,\(L\) 图形越小。

\(L1\)正则化对所有参数的惩罚力度都一样,可以让一部分权重变为 \(0\),去除某些特征(权重为 \(0\) 则等效于去除),因此产生稀疏模型。

那么稀疏模型有什么好处呢?

稀疏化正则化项一个最重要的优势就在于实现特征的自动选择。所谓稀疏性,说白了就是模型的很多参数是 \(0\)。通常机器学习中特征数量很多,例如文本处理时,如果将一个词组(\(term\))作为一个特征,那么特征数量会达到上万个(\(bigram\))。但是只有少数特征对该模型有贡献,绝大部分特征是没有贡献的。在最小化目标函数时,需要考虑这些额外的特征,虽然能获得更小的训练误差,但在预测阶段,模型会考虑这些无用的特征,从而可能干扰模型的正确预测。

这种模型就是所谓的泛化性能不强,有过拟合的嫌疑。如果通过稀疏化正则化项得到一个稀疏模型,很多参数是\(0\),此时我们就可以只关注系数是非零值的特征。这相当于对模型进行了一次特征选择,只留下一些比较重要的特征,提高模型的泛化能力,降低过拟合的可能。这就是稀疏模型与特征选择的关系。

L2正则化

假设带有\(L2\)正则化的目标函数为: \[ J = J_0 + ||w||^2_2 = J_0+\alpha \sum w^2 \ \ \ \ \ \ \ \ \ (3) \]\(L1\)正则化,\(w\) 表示特征的系数( \(x\) 的参数),可以看到正则化项是对系数做了限制。\(L2\)正则化是指权值向量 \(w\) 中各个元素的平方和然后再求平方根(可以看到 \(Ridge\) 回归的\(L2\)正则化项有平方符号),通常表示为 \(\| w \|_2\)

\(L2\) 范数符合高斯分布,是完全可微的。和 \(L1\) 相比,图像上为一个圆。一般最优值不会在坐标轴上出现。在最小化正则项时,参数不断趋向于 \(0\),但并不是 \(0\)

如下图:

L2正则化图示

相比于\(L1\)正则化,\(L2\)正则化的函数 \(L\)\(J_0\) 第一次相交的地方出现在具有稀疏性的位置的概率就变得非常小了。这就从直观上来解释了为什么\(L1\)正则化能产生稀疏性,而\(L2\)正则化不能产生稀疏性的原因了。

\(L2\)正则化的作用:主要是为了防止过拟合

拟合过程中通常都倾向于让权值尽可能小,最后构造一个所有参数都比较小的模型。因为一般认为参数值小的模型比较简单,泛化能力强,能适应不同的数据集,也在一定程度上避免了过拟合现象。可以设想一下对于一个线性回归方程,若参数很大,那么只要数据偏移一点点,就会对结果造成很大的影响;但如果参数足够小,数据偏移得多一点也不会对结果造成什么影响,专业一点的说法是抗扰动能力强。

越是复杂的模型,越是尝试对所有样本进行拟合,包括异常点。这就会造成在较小的区间中产生较大的波动,这个较大的波动也会反映在这个区间的导数比较大。只有越大的参数才可能产生较大的导数。因此参数越小,模型就越简单。

为什么\(L2\)正则化能够得到值很小的参数???

我们通过线性回归,来看一下\(L2\)正则化解决过拟合问题。

假设要求解的参数为\(\theta\)\(h_{\theta}(x)\) 是假设函数。线性回归一般使用平方差损失函数。单个样本的平方差是\((h_{\theta}(x) - y)^2\),如果考虑所有样本,损失函数是对每个样本的平方差求和,假设有 \(m\) 个样本,线性回归的损失函数如下, \[ J(\theta) = \frac{1}{2m} \sum^m_{i=1} (h_{\theta}(x^{(i)}) - y^{(i)})^2 \ \ \ \ \ \ \ \ (4) \]

其梯度下降算法公式为: \[ \theta_j = \theta_j - \alpha \frac{1}{m}[\sum_{i=1}^m(h_{\theta}(x^{(i)}-y^{(i)})x_j^{(i)}] \ \ \ \ \ \ \ \ (5) \]

加入\(L2\)正则化后,其损失函数为 \[ J(\theta) = \frac{1}{2}\sum^m_{i=1}((h_{\theta}(x^{(i)}) - y^{(i)})^2 + \lambda\sum^m_{i=1}\theta_j^2) \ \ \ \ \ \ \ \ (6) \] 其梯度下降算法公式为: \[ \theta_j = \theta_j - (\alpha \frac{1}{m}[\sum_{i=1}^m(h_{\theta}(x^{(i)}-y^{(i)})x_j^{(i)}] + \lambda \theta_j)=\theta_j(1-\alpha\frac{\lambda}{m}) - (\alpha \frac{1}{m}\sum_{i=1}^m(h_{\theta}(x^{(i)}-y^{(i)})x_j^{(i)}) \ \ \ \ \ \ \ \ (7) \] 可以看到,由于学习率 \(\alpha > 0, \lambda >0\),且这两个值一般都是很小的正数,所以 \(0< 1-\alpha\frac{\lambda}{m} < 1\),所以每次 \(\theta\) 在更新的时候都会减小,\(\lambda\) 越大,衰减的越快,这也是\(L2\)正则化可以获得更小的权重值的原因。

正如在线性回归中的应用,\(L2\) 正则化就是在损失函数中加入一个 \(L2\) 范数和一个超参数 \(\lambda\)\(L2\)范数用 \(\|w\|^2\) 这种符号表示,它的意思是对于向量 \(w\) 中的各个数先求平方再加和。线性回归中加入的对于 \(\theta_j\) 求平方和就是一个 \(L2\) 范数。超参数\(\lambda\) 则用于控制参数惩罚的程度。

我们在举个例子,来展示\(L2\)正则化如何解决过拟合的现象

来源:吴恩达机器学习课程

将上述公式分为两部分,左边部分即为原始的损失函数,右边部分为 \(L2\) 正则化项(注意:正则化项中不包含\(\theta_0\))。\(\lambda\) 为超参数,是人为设定的。为了最小化整个损失函数,那么就要减小 \(\theta_1\) ~ \(\theta_n\) 的值。对于上图中的那种过拟合状态,加入正则项后,\(\theta_1\) ~ \(\theta_n\)减小,也就是使得权重衰减,这样就会降低高阶项对于整个函数的影响,使得估计函数变得比较平滑。

可以想象一种极端的情况,如果\(\lambda\) 为无穷大,那么 \(\theta_1\) ~ \(\theta_n\) 趋近于0,那么整个式子就只剩一个\(\theta_0\),为一条和 y 轴垂直的直线,这种状态为严重的欠拟合状态。可以看到,当\(\lambda\)为0时,即为原来的状态,此时过拟合。所以会有一个恰当的\(\lambda\)使得模型处于既不过拟合又不欠拟合的状态。

在未加入\(L2\)正则化发生过拟合时,拟合函数需要顾忌每一个点,最终形成的拟合函数波动很大,在某些很小的区间里,函数值的变化很剧烈,也就是某些 \(w\) 值非常大。为此,\(L2\) 正则化的加入惩罚了权重变大的趋势,逼迫所有 \(w\) 尽可能趋向零但不为零(\(L2\)正则化的导数趋于零),导致权重较为平滑。

直观理解

假设只有一个参数为\(w\),损失函数为\(L(w)\),分别加上\(L1\)正则项和\(L2\)正则项后有:

\[ J_{L1}(w)=L(w) +\lambda|w| \\ J_{L2}(w)=L(w)+\lambda w^{2} \] 这里,假设\(L(w)\)在0处的导数值为\(d_{0}\),即: \[ \left.\frac{\partial L(w)}{\partial w}\right|_{w=0}=d_{0} \] 这时,可以推导使用\(L1\)正则和\(L2\)正则时的导数。

当引入\(L2\)正则项,在\(0\)处的导数:\(\left.\frac{\partial J_{L 2}(w)}{\partial w}\right|_{w=0}=d_{0}+2 \times \lambda \times w=d_{0}\)

引入\(L1\)正则项,在\(0\)处的导数: \[ \begin{array}{l} \left.\frac{\partial J_{L 1}(w)}{\partial w}\right|_{w=0^{-} }=d_{0}-\lambda \\ \left.\frac{\partial J_{L 1}(w)}{\partial w}\right|_{w=0^{+} }=d_{0}+\lambda \end{array} \] 可见,引入\(L2\)正则时,损失函数在0处的导数仍是\(d_{0}\) ,无变化。

而引入\(L1\)正则后,损失函数在\(0\)处的导数有一个突变。从\(d_{0}-\lambda\)\(d_{0}+\lambda\)。若\(d_{0}-\lambda\)\(d_{0}+\lambda\)异号,则在\(0\)处会是一个极小值点。因此,优化时,很可能优化到该极小值点上,即\(w=0\)处。

当然,这里只解释了有一个参数的情况,如果有更多的参数,也是类似的。因此,用L1正则更容易产生稀疏解。

先验概率看L1更稀疏

假设,我们的数据数据是稀疏的,不妨就认为它来自某种\(laplace\)分布。其中\(laplace\)的概率密度函数图像如下图所示:

再看看\(laplace\)分布的概率密度函数: \[ f(x \mid \mu, b)=\frac{1}{2 b} \exp \left(-\frac{|x-\mu|}{b}\right) \] 如果取对数,剩下的是一个一次项\(|x-u|\),这就是\(L1\)范式。所以用\(L1\)范式去正则,就假定了你的数据是稀疏的\(laplace\)分布。

总结

  • \(L1\)正则化项是模型各个参数的绝对值之和。\(L2\)正则化项是模型各个参数的平方和的开方值。
  • \(L1\)正则化可以使部分权重为\(0\),产生稀疏权值矩阵,即产生一个稀疏模型,可以用于特征选择;一定程度上,\(L1\)也可以防止过拟合,当\(L1\)的正则化系数很小时,得到的最优解会很小,可以达到和\(L2\)正则化类似的效果。
  • \(L2\)正则化通过权重衰减,可以使所有的权重趋向于\(0\),但不为\(0\),导致模型权重参数较小且较为平滑,防止模型过拟合(\(overfitting\));
  • \(L2\)正则化的效果是对原最优解的每个元素进行不同比例的放缩;\(L1\)正则化则会使原最优解的元素产生不同量的偏移,并使某些元素为\(0\),从而产生稀疏性。

标签平滑

标签平滑(Label Smoothing):在训练时即假设标签可能存在错误,避免“过分”相信训练样本的标签。当目标函数为交叉熵时,这一思想有非常简单的实现,称为标签平滑(Label Smoothing)。

为了达到这个目标,我们很自然想到的方法是:在每次迭代时,并不直接将(\({x_i}\),\({y_i}\))放入训练集,而是设置一个错误率 \(\epsilon\),以 1-\(\epsilon\) 的概率将(\({x_i}\),\({y_i}\))代入训练,以 \(\epsilon\) 的概率将(\({x_i},1-{y_i}\))代入训练。这样,模型在训练时,既有正确标签输入,又有错误标签输入,可以想象,如此训练出来的模型不会“全力匹配”每一个标签,而只是在一定程度上匹配。这样,即使真的出现错误标签,模型受到的影响就会更小。

具体实现来说:对于标签为 \(1\) 的类别我们将标签设置为 \(1 - \epsilon\),对于标签为 \(0\) 的类别我们将标签设置为 \(\epsilon\).

下面我们给出在使用标签平滑时的 \(softmax\) 损失的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def cross_entropy_loss(preds, target, reduction):
logp = F.log_softmax(preds, dim=1)
loss = torch.sum(-logp * target, dim=1)
if reduction == 'none':
return loss
elif reduction == 'mean':
return loss.mean()
elif reduction == 'sum':
return loss.sum()
else:
raise ValueError(
'`reduction` must be one of \'none\', \'mean\', or \'sum\'.')

# one-hot编码
def onehot_encoding(labels, n_classes):
return torch.zeros(labels.size(0), n_classes).to(labels.device).scatter_(
dim=1, index=labels.view(-1, 1), value=1)

def label_smoothing(preds, targets,epsilon=0.1):
#preds为网络最后一层输出的logits
#targets为未one-hot的真实标签
n_classes = preds.size(1)
device = preds.device

onehot = onehot_encoding(targets, n_classes).float().to(device)
targets = onehot * (1 - epsilon) + torch.ones_like(onehot).to(
device) * epsilon / n_classes
loss = cross_entropy_loss(preds, targets, reduction="mean")
return loss

技巧

增加BatchSize

在Backbone不变的情况下,若显存有限,如何增大训练时的batch size?

  • 使用inplace操作,比如relu激活函数,我们可以使用inplace=True
  • 每次循环结束时候,我们可以手动删除loss,但是这样的操作,效果有限。
  • 使用float16混合精度计算,据有关人士测试过,使用apex,能够节省将近50%的显存,但是还是要小心mean与sum会溢出的操作。
  • 训练过程中的显存占用包括前向与反向所保存的值,所以在我们不需要bp的forward的时候,我们可以使用torch.no_grad()。
  • 如使用将batchsize=32这样的操作,分成两份,进行forward,再进行backward,不过这样会影响batchnorm与batch size相关的层。yolo系列cfg文件里面有一个参数就是将batchsize分成几个sub batchsize的。
  • 使用pooling,减小特征图的size,如使用GAP等来代替FC等。
  • optimizer的变换使用,理论上,显寸占用情况 sgd < momentum < adam,可以从计算公式中看出有额外的中间变量。
  • 梯度累加,需要适当学习率,对 BN 也有显著影响

总的来说最有效的还是混合精度梯度累加


模型

反向传播(BP)

反向传播(Back Propagation)是一种网络学习方法,主要特定是信号前向传递,误差反向传播,通过不断调节网络权重值,使得网络的最终输出与期望输出尽可能接近,以达到训练的目的。

举个简单的例子:

image-20230706154720417

\(x\)是网络的输入,\(y\) 是网络的输出,\(w\) 是网络学习到的参数。我们的目标是通过更新 \(w\) 寻找一个 \(f_w(x) = y\) ,即: \[ \min_w \sum_x \| f_w(x) - y \|^2 \]\(E = \sum_x \| f_w(x) - y \|^2\) 记为损失项,我们的目标是寻找能使损失 \(E\) 最小的参数 \(w\)。具体而言,常用的经典方法为梯度下降法,即随机生成一个 \(w\) 然后向着梯度下降的方向更新优化: \[ w^{+}=w-\eta \cdot \frac{\partial E}{\partial w} \] 这里 \(w\) 是一个随机初始化的权重,\(\frac{\partial E}{\partial w}\) 是表示当前误差对权重\(w\)的梯度。\(\eta\) 是表示的学习率,通常不会很大,都是0.01以下的值,用来控制更新的步长。

链式求导应该很简单就不说了。


评价指标

IOU

目标检测中的IOU

假设,我们有两个框,\(rec1\)\(rec2\),我们要计算其\(IOU\)。其中\(IOU\)的计算公式为,其交叉面积\(Intersection\)除以其并集\(Union\)

\(IOU\)的数学公式为: \[ I o U=\frac{S_{rec1} \cap S_{rec2} }{S_{rec1} + S_{rec2} - S_{rec1} \bigcap S_{rec2} } \]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def compute_iou(rec1, rec2):
"""
computing IoU
param rec1: (y0, x0, y1, x1) , which reflects (top, left, bottom, right)
param rec2: (y0, x0, y1, x1) , which reflects (top, left, bottom, right)
return : scale value of IoU
"""
S_rec1 =(rec1[2] -rec1[0]) *(rec1[3] -rec1[1])
S_rec2 =(rec2[2] -rec2[0]) *(rec2[3] -rec2[1])
#computing the sum area
sum_area =S_rec1 +S_rec2
#find the each edge of interest rectangle
left_line =max(rec1[1], rec2[1])
right_line =min(rec1[3], rec2[3])
top_line =max(rec1[0], rec2[0])
bottom_line =min(rec1[2], rec2[2])
#judge if there is an intersect
if left_line >=right_line or top_line >=bottom_line:
return 0
else:
intersect =(right_line -left_line) +(bottom_line -top_line)
return intersect /(sum_area -intersect)

这里我们主要讨论下这个if判断,我们以横轴 \(x\) 方向为例,其中对 \(y\) 纵轴方向是一样的,我们来判断两个框重合与否。其中 \(x_{0}\)\(rec1\) 左上角的 \(x\) 坐标,\(x_{1}\)\(rec1\) 右下角的 \(x\) 坐标。\(A_{0}\)\(rec2\) 的左上角 \(x\) 坐标,\(A_{1}\)\(rec2\) 的右下角 \(x\) 坐标。

语义分割中的IOU

\[ IOU = \frac{\text { target } \bigwedge \text { prediction } }{target \bigcup prediction} \]

1
2
3
4
5
6
7
8
9
10
11
12
13
def compute_ious(pred, label, classes):
'''computes iou for one ground truth mask and predicted mask'''
ious = [] # 记录每一类的iou
for c in classes:
label_c = (label == c) # label_c为true/false矩阵
pred_c = (pred == c)
intersection = np.logical_and(pred_c, label_c).sum()
union = np.logical_or(pred_c, label_c).sum()
if union == 0:
ious.append(float('nan'))
else
ious.append(intersection / union)
return np.nanmean(ious) #返回当前图片里所有类的mean iou

其中,对于 \(label\)\(pred\) 有多种形式。如识别目标为 4 类,那么 \(label\) 的形式可以是一张图片对应一份 \(mask[0,1,2,3,4]\),其中 \(0\) 为背景,我们省略,则 \(class\) 可以为 \([1,2,3,4]\)。也可以是对应四份二进制 \(mask[0,1]\), 这四层 \(mask\) 的取值为 \(0/1\)\(class\)\([1]\)

NMS

Non-Maximum-Suppression(非极大值抑制): 当两个 box 空间位置非常接近,就以 score更高的那个作为基准,看 IOU 即重合度如何,如果与其重合度超过阈值,就抑制 score 更小的 box,只保留 score 大的就 Box,其它的 Box 就都应该过滤掉。对于 NMS 而言,适合于水平框,针对各种不同形状的框,会有不同的 NMS 来进行处理。

参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import numpy as np

def NMS(dets, thresh):
"""Pure Python NMS baseline."""
# tl_x,tl_y,br_x,br_y及score
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]

#计算每个检测框的面积,并对目标检测得分进行降序排序
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]

keep = [] #保留框的结果集合
while order.size > 0:
i = order[0]
keep.append(i)  #保留该类剩余box中得分最高的一个
# 计算最高得分矩形框与剩余矩形框的相交区域
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])

#计算相交的面积,不重叠时面积为0
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h

#计算IoU:重叠面积 /(面积1+面积2-重叠面积)
ovr = inter / (areas[i] + areas[order[1:]] - inter)

#保留IoU小于阈值的box
inds = np.where(ovr <= thresh)[0]
order = order[inds + 1] #注意这里索引加了1,因为ovr数组的长度比order数组的长度少一个

return keep

SoftNMS

说到 Soft NMS,首先需要了解传统 NMS 有哪些缺点。其主要缺点包括如下:

  • 物体重叠:有一个最高分数的框,如果使用 NMS 的话就会把其他置信度稍低,但是表示另一个物体的预测框删掉(由于和最高置信度的框 overlap 过大)
  • 所有的 bbox 都预测不准:不是所有的框都那么精准,有时甚至会出现某个物体周围的所有框都标出来了,但是都不准的情况
  • 传统的 NMS 方法是基于分类分数的,只有最高分数的预测框能留下来,但是大多数情况下 IoU 和分类分数不是强相关,很多分类标签置信度高的框都位置都不是很准

Soft NMS 主要是针对 NMS 过度删除框的问题。Soft-NMS 吸取了 NMS 的教训,在算法执行过程中不是简单的对 IoU 大于阈值的检测框删除,而是降低得分。算法流程同 NMS 相同,但是对原置信度得分使用函数运算,目标是降低置信度得分。其算法步骤如下:

image-20230727154116260

红色的部分表示原始 NMS 算法,绿色部分表示 Soft-NMS 算法,区别在于,绿色的框只是把\(s_{i}\)降低了,而不是把\(b_{i}\)直接去掉,极端情况下,如果\(f\)只返回\(0\),那么等同于普通的NMS。

\(b_{i}\)为待处理 BBox 框,\(\mathcal{B}\)为待处理 BBox 框集合,\(s_{i}\)\(b_{i}\)框更新得分,\(N_{t}\)是 NMS 的阈值,\(D\) 集合用来放最终的 BBox,\(f\)是置信度得分的重置函数。\(b_{i}\)\(\mathcal{M}\)的 IOU越大,\(b_{i}\)的得分\(s_{i}\)就下降的越厉害。

\(f\)函数是为了降低目标框的置信度,满足条件,如果\(b_{i}\)\(\mathcal{M}\)的 IoU 越大,\(f(iou(\mathcal{M}, bi))\)就应该越小, Soft-NMS提出了两种\(f\)函数:

经典的 NMS 算法将 IOU 大于阈值的窗口的得分全部置为 0,可表述如下: \[ s_i = \left\{\begin{matrix} s_i, &iou(\mathcal{M},b_i) < N_t\\ 0, &iou(\mathcal{M}, b_i) \geq N_t \end{matrix}\right. \] 论文中置信度重置函数有两种形式改进,一种是线性加权的\[ s_i = \left\{\begin{matrix} s_i, &iou(\mathcal{M},b_i) < N_t\\ s_i(1-iou(\mathcal{M},b_i)), &iou(\mathcal{M}, b_i) \geq N_t \end{matrix}\right. \] 一种是高斯加权形式: \[ s_{i}=s_{i} e^{-\frac{\mathrm{iou}\left(\mathcal{M}, b_{i}\right)^{2}}{\sigma}}, \forall b_{i} \notin \mathcal{D} \] SoftNMS算法的优点如下:

  • 该方案可以很方便地引入到object detection算法中,不需要重新训练原有的模型;

  • soft-NMS在训练中采用传统的NMS方法,可以仅在推断代码中实现soft-NMS

  • NMS 是 Soft-NMS特殊形式,当得分重置函数采用二值化函数时,Soft-NMS 和 NMS 是相同的。soft-NMS 算法是一种更加通用的非最大抑制算法。

这里提供一个\(github\) 中的\(Cython\)代码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
def cpu_soft_nms(np.ndarray[float, ndim=2] boxes, float sigma=0.5, float Nt=0.3, float threshold=0.001, unsigned int method=0):
cdef unsigned int N = boxes.shape[0]
cdef float iw, ih, box_area
cdef float ua
cdef int pos = 0
cdef float maxscore = 0
cdef int maxpos = 0
cdef float x1,x2,y1,y2,tx1,tx2,ty1,ty2,ts,area,weight,ov

for i in range(N):

# 在i之后找到confidence最高的框,标记为max_pos
maxscore = boxes[i, 4]
maxpos = i

tx1 = boxes[i,0]
ty1 = boxes[i,1]
tx2 = boxes[i,2]
ty2 = boxes[i,3]
ts = boxes[i,4]

pos = i + 1
# 找到max的框
while pos < N:
if maxscore < boxes[pos, 4]:
maxscore = boxes[pos, 4]
maxpos = pos
pos = pos + 1

# 交换max_pos位置和i位置的数据
# add max box as a detection
boxes[i,0] = boxes[maxpos,0]
boxes[i,1] = boxes[maxpos,1]
boxes[i,2] = boxes[maxpos,2]
boxes[i,3] = boxes[maxpos,3]
boxes[i,4] = boxes[maxpos,4]

# swap ith box with position of max box
boxes[maxpos,0] = tx1
boxes[maxpos,1] = ty1
boxes[maxpos,2] = tx2
boxes[maxpos,3] = ty2
boxes[maxpos,4] = ts

tx1 = boxes[i,0]
ty1 = boxes[i,1]
tx2 = boxes[i,2]
ty2 = boxes[i,3]
ts = boxes[i,4]
# 交换完毕

# 开始循环
pos = i + 1

while pos < N:
# 先记录内层循环的数据bi
x1 = boxes[pos, 0]
y1 = boxes[pos, 1]
x2 = boxes[pos, 2]
y2 = boxes[pos, 3]
s = boxes[pos, 4]

# 计算iou
area = (x2 - x1 + 1) * (y2 - y1 + 1)
iw = (min(tx2, x2) - max(tx1, x1) + 1) # 计算两个框交叉矩形的宽度,如果宽度小于等于0,即没有相交,因此不需要判断
if iw > 0:
ih = (min(ty2, y2) - max(ty1, y1) + 1) # 同理
if ih > 0:
ua = float((tx2 - tx1 + 1) * (ty2 - ty1 + 1) + area - iw * ih) #计算union面积
ov = iw * ih / ua #iou between max box and detection box

if method == 1: # linear
if ov > Nt:
weight = 1 - ov
else:
weight = 1
elif method == 2: # gaussian
weight = np.exp(-(ov * ov)/sigma)
else: # original NMS
if ov > Nt:
weight = 0
else:
weight = 1

boxes[pos, 4] = weight*boxes[pos, 4]

# if box score falls below threshold, discard the box by swapping with last box
# update N
if boxes[pos, 4] < threshold:
boxes[pos,0] = boxes[N-1, 0]
boxes[pos,1] = boxes[N-1, 1]
boxes[pos,2] = boxes[N-1, 2]
boxes[pos,3] = boxes[N-1, 3]
boxes[pos,4] = boxes[N-1, 4]
N = N - 1
pos = pos - 1

pos = pos + 1

keep = [i for i in range(N)]
return keep

SofterNMS

Softer NMS 的改进:

  • 针对分类置信度和框的 IoU 不是强相关的问题,构建一种 IoU 的置信度,来建模有多大把握认为当前框和 GT 是重合的。

  • 针对所有的框单独拿出来都不准的问题,文章中提出一种方法,根据 IoU 置信度加权合并多个框优化最终生成框。

Softer-NMS 文章对预测框建模,以下公式中 \(x\) 表示偏移前的预测框,\(x_{e}\) 表示偏移后的预测框,输出的 \(x_{g}\) 表示 GT 框,使用高斯函数对预测框建模: \[ P_{\Theta}(x)=\frac{1}{2 \pi \sigma^{2}}e^{-\frac{(x-x_{e})^2}{2 \sigma^{2}}} \] 对于 GT 框建模:使用\(delta\)分布(即标准方差为\(0\)的高斯分布极限)。 \[ P_{D}(x)=\delta\left(x-x_{g}\right) \] 对于\(delta\)分布,当\(\sigma\)越小,其函数图像就会越瘦高,同时,当\(\sigma\)越小,表示网络越确定,可以使用\(1-\sigma\)就可以作为网络的置信度。

同时,论文使用\(KL\)散度来最小化\(Bounding\) \(box\) \(regression\) \(loss\)。既\(Bounding\) \(box\)的高斯分布和\(ground\) \(truth\)的狄拉克\(delta\)分布的\(KL\)散度。直观上解释,\(KL\) \(Loss\)使得\(Bounding\) \(box\)预测呈高斯分布,且与\(ground\) \(truth\)相近。而将包围框预测的标准差看作置信度。

\(faster\) \(rcnn\)中添加了\(softer\) \(nms\)之后的示意图如图所示:

多加了一个\(\sigma\)预测,也就是\(box\) \(std\),而\(Box\)的预测其实就是上面公式中的\(x_{e}\)

因此,整个计算过程如下:

  1. 计算\(x_{e}\)\(x\)的2范数距离和\(\sigma\)计算出\(P_{\theta}(x)\).
  2. 通过\(x_{g}\)\(x\)的2范数距离算出\(P_{D}\).
  3. 使用\(P_{D}\)\(P_{\theta}\)计算\(KLs\)散度作为\(loss\),最小化\(KLLoss\)

关于坐标回归的损失函数: \[ \begin{array}{l} L_{r e g}=D_{K L}\left(P_{D}(x) \| P_{\Theta}(x)\right) \\ =\int P_{D}(x) \log \frac{P_{D}(x)}{P_{\Theta}(x)} d x \\ =-\int P_{D}(x) \log P_{\Theta}(x) d x+\int P_{D}(x) \log P_{D}(x) d x \\ =-\int P_{D}(x) \log P_{\Theta}(x) d x+H\left(P_{D}(x)\right) \\ =-\log P_{\Theta}\left(x_{g}\right)+H\left(P_{D}(x)\right) \\ =\frac{\left(x_{g}-x_{e}\right)^{2}}{2 \sigma^{2}}+\frac{1}{2} \log \left(\sigma^{2}\right)+\frac{1}{2} \log (2 \pi)+H\left(P_{D}(x)\right) \end{array} \] 而后面两项是与\(x_{e}\)无关,可以去掉~ \[ L_{\text {reg }}=\alpha\left(\left|x_{g}-x_{e}\right|-\frac{1}{2}\right)-\frac{1}{2} \log (\alpha+\epsilon) \] 因此,计算过程如下图所示:

网络预测出来的结果是\(x1_{i}, y1_{i}, x2_{i}, y2_{i}, \sigma{x1_{i}}, \sigma{x2_{i}}, \sigma{x3_{i}}, \sigma{x4_{i}}\)。前面四个为坐标,而后面四个是坐标的\(\sigma\)

上表中的蓝色的是Soft-NMS,只是降低了 \(\mathcal{S}\) 的权值。重点看绿色的,绿字第一行表示拿出所有与 \(\mathcal{B}\)\(IoU\) 大于 \(N_{t}\) 的框(用 \(idx\) 表示),然后将所有这些框做一个加权,\(\mathcal{B}[idx]/\mathcal{C}[idx]\) 其实是 \(\mathcal{B}[idx] * 1/\mathcal{C}[idx]\),后者是置信度 \(\frac{1}{\sigma^{2}}\),并使用 \(sum\) 做了归一化。需要注意的是,Softer-NMS 算法中,\(\mathcal{B}\) 是不变的,Softer-NMS只调整每个框的位置,而不筛选框。


一些问题


参考资料

--- ♥ end ♥ ---

欢迎关注我呀~