下面介绍的网络组件都是 torch.nn.Module 的派生类,所以它们有一些共同的成员函数:
- named_parameters:返回该网络层参数的迭代器。通过这个迭代器可以输出该层的参数名(如 weight 和 bias)和参数值。
- parameters: 返回该网络层参数的迭代器。通过这个迭代器可以输出该层的参数值。
- state_dict: 返回该网络层状态字典的迭代器。状态字典包含网络参数和持久化缓存(persistent buffers),比如 BN 层的状态字典不仅包含参数 weight 和 bias,还包含持久化缓存 running_mean、running_var 和 num_batches_tracked。
1. 卷积层
卷积层的输入必须是 4 维特征,参数有 weight、bias,即权重、偏置。使用 PyTorch 定义卷积层:
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')
1. kernel_size: int 或 tuple 类型,指定卷积核的宽高。
2. int 或 tuple 类型,指定卷积核的在横竖方向上的步长。
3. dilation: int 或 tuple 类型,指定卷积核元素间的距离。默认为 1。用于空洞卷积。
4. groups: int 类型。用于可分离卷积。输出通道必须是该参数的整数倍。
5. padding: int、tuple 或 str 类型,在输入特征四周填充的行数,默认填充 0 行。
5.1 int x: 在上下左右都填充 x 行。
5.2 tuple (x, y): 上下填充 x 行,左右填充 y 行。
5.3 str valid 或 same: 低版本不支持。不填充或填充到使输入输出宽高相同,这两个参数只有在 stride = 1 时有效。
6. padding_mode: str 类型,可以是 zeros、reflect、replicate 或 circular。默认是 zeros。
6.1 zeros: 填充 0。
6.2 reflect: 以边界为对称轴,用边界里面的像素值填充。
6.3 replicate: 用边界像素值填充。
6.4 circular: 把下边界里面的像素值填充到上面,其它三边填充类似。
卷积核的尺寸:分类网络一般使用边长为 5、7 的大卷积核,用于降低特征间的冗余性;重建任务一般使用边长为 3 的小卷积核,用于保留更多的局部特征。卷积核的边长 n 一般为单数,这样的情况下,输入特征四周各有 (n-1)/2 行填充,卷积步长为 1 就可以保证输入输出特征尺寸相等。
1.1 常规卷积
卷积核的通道等于输入特征的通道 in_channels,宽高等于 kernel_size,个数等于输出特征的通道个数 out_channels。
1.2 空洞卷积
感受野内像素不连续的卷积。一个感受野内相邻像素间的距离(单位是像素个数)称为空洞率,常规卷积是空洞率为 1 的空洞卷积。
下面我们根据 DeepLab v2 中的 ASPP 模块详细探讨空洞卷积及其用法。
class ASPP(torch.nn.Module):
def __init__(self, in_channel, out_channel, kernel_size=3, dilations=None):
super(ASPP, self).__init__()
if dilations is None:
dilations = [6, 12, 18]
# 1.确定空洞卷积的填充。
paddings = list()
for dilation in dilations:
equivalent_kernel_size = kernel_size + (kernel_size - 1) * (dilation - 1)
paddings.append((equivalent_kernel_size - 1) // 2)
# 2.创建三个尺度的空洞卷积。
self.atrous_conv0 = torch.nn.Conv2d(in_channel, out_channel, kernel_size, 1, paddings[0], dilation=dilations[0])
self.atrous_conv1 = torch.nn.Conv2d(in_channel, out_channel, kernel_size, 1, paddings[1], dilation=dilations[1])
self.atrous_conv2 = torch.nn.Conv2d(in_channel, out_channel, kernel_size, 1, paddings[2], dilation=dilations[2])
def forward(self, input):
out0 = self.atrous_conv0(input)
out1 = self.atrous_conv1(input)
out2 = self.atrous_conv2(input)
return torch.cat((out0, out1, out2), dim=1)
上面的程序只给出了 ASPP 的主要部分,实际使用 ASPP 时,还需要一些卷积、BN 等层。
我们在第 12、13、14 行创建了 3 个空洞卷积,并且设置卷积核边长为 3。假如用 kernel_size 代表这里设置的卷积核边长,用 dilation 代表空洞率,则空洞卷积的等效卷积核(或称为实际卷积核)边长
e
q
u
i
v
a
l
e
n
t
_
k
e
r
n
e
l
_
s
i
z
e
=
k
e
r
n
e
l
_
s
i
z
e
+
(
k
e
r
n
e
l
_
s
i
z
e
−
1
)
×
(
d
i
l
a
t
i
o
n
−
1
)
equivalent\_kernel\_size = kernel\_size + (kernel\_size - 1) \times (dilation - 1)
equivalent_kernel_size=kernel_size+(kernel_size−1)×(dilation−1)。
步长为 1 就是希望卷积的输入输出尺寸相同,所以 4 个边的填充数
p
a
d
d
i
n
g
=
(
e
q
u
i
v
a
l
e
n
t
_
k
e
r
n
e
l
_
s
i
z
e
−
1
)
/
/
2
padding = (equivalent\_kernel\_size - 1) // 2
padding=(equivalent_kernel_size−1)//2。
1.3 可分离卷积
无论是普通卷积还是空洞卷积,卷积核都是三维的。可分离卷积的通道为 1。
1.4 卷积计算量
假设输入特征的维度是
h
i
n
×
w
i
n
×
c
i
n
h_{in}\times w_{in} \times c_{in}
hin×win×cin,卷积核的维度是
h
k
×
w
k
×
c
i
n
h_{k}\times w_{k} \times c_{in}
hk×wk×cin。假如步长为 1,则卷积核的中心像素点会依次与尺寸为
h
i
n
×
w
i
n
h_{in}\times w_{in}
hin×win 的输入特征截面上的每一个像素点重合。所以卷积次数是
h
i
n
×
w
i
n
h_{in}\times w_{in}
hin×win。
每次卷积的计算包括卷积核的每个参数与输入特征对应相乘,然后求和,所以计算量是
h
k
×
w
k
×
c
i
n
+
1
h_{k}\times w_{k} \times c_{in} + 1
hk×wk×cin+1。于是一个卷积核卷积输入特征的计算量是
h
i
n
×
w
i
n
×
(
h
k
×
w
k
×
c
i
n
+
1
)
h_{in}\times w_{in} \times (h_{k}\times w_{k} \times c_{in} + 1)
hin×win×(hk×wk×cin+1)。如果忽略求和,一个卷积核卷积输入特征的计算量是
h
i
n
×
w
i
n
×
(
h
k
×
w
k
×
c
i
n
)
h_{in}\times w_{in} \times (h_{k}\times w_{k} \times c_{in})
hin×win×(hk×wk×cin),即
输
入
特
征
的
截
面
积
×
卷
积
核
的
截
面
积
×
输
入
特
征
通
道
数
输入特征的截面积\times 卷积核的截面积\times 输入特征通道数
输入特征的截面积×卷积核的截面积×输入特征通道数。
假如输出特征的通道数是
c
o
u
t
c_{out}
cout,即需要
c
o
u
t
c_{out}
cout 个卷积核。于是考虑输入输出特征时,卷积计算量是
输
入
特
征
的
截
面
积
×
卷
积
核
的
截
面
积
×
输
入
特
征
通
道
数
×
输
出
特
征
通
道
数
输入特征的截面积\times 卷积核的截面积\times 输入特征通道数\times 输出特征通道数
输入特征的截面积×卷积核的截面积×输入特征通道数×输出特征通道数。
2. 池化层
1. 自适应池化
data = torch.Tensor([[[11, 12, 14], [21, 22, 24], [31, 32, 34]]])
adapt_max_pool = torch.nn.AdaptiveMaxPool2d(output_size=4)
adapt_avg_pool = torch.nn.AdaptiveAvgPool2d(output_size=4)
y1 = adapt_max_pool(data)
y2 = adapt_avg_pool(data)
3. 激活函数层
激活层没有参数。激活函数必须是非线性的,值域和单调性无要求。
有界的激活函数会导致梯度消失。当特征的值在大概 [-5, 5] 的区间时,Sigmoid 和 tanh 函数的值才有明显变化。所以当特征或权重过大时,如果没有 BN 层,这两个函数很容易饱和,从而难以提取到有效特征。
Sigmoid 和 tanh 函数的导数值大多数小于 1,如果连续的网络层都用这些激活函数,会导致梯度连续下降,从而引起梯度消失。
1. ReLU
梯度值恒为 1,不会造成梯度爆炸。在自变量小于 0 时把结果置 0,有利于网络的稀疏化。计算简便。对于 SGD 优化算法的收敛有巨大的加速作用。
ReLU有很多变种,如ReLU6、Leaky ReLU、ELU。
2. Sigmoid
梯度值域小,容易梯度消失。
3. tanh
tanh:结果满足零均值但计算复杂。
4. Leak ReLU
Faster R-CNN 使用 Leak ReLU 激活函数。
5. Mish
YOLO4 中使用了 Mish 激活函数,它的公式是 M i s h = x ⋅ t a n h ( l n ( 1 + e x ) ) Mish = x \cdot tanh(ln(1+e^x)) Mish=x⋅tanh(ln(1+ex))。
Mish 激活函数是对 ReLU 的改进,它的特点:
- 有下界无上界:最小值约为 0.31,最大值为无穷大。无上界,梯度不会饱和;有下界,强正则化效果。
- 不单调:在任何区间都不单调。和 ReLU 相比,即使输入为负,也能产生较小的负梯度值。
- 无穷阶连续和光滑。
- 计算量大。
- 自门控。
6. hard-Swish
hard-Swish 激活函数在 MobileNet v3 中提出,它的公式是 h − s w i s h = x ⋅ R e L U 6 ( x + 3 ) ÷ 6 h-swish = x \cdot ReLU6(x+3) \div 6 h−swish=x⋅ReLU6(x+3)÷6。
4. BN层
BN 层的输入必须是 4 维 data = [b, c, h, w]。创建 BN 层时至少给出 num_features 参数且这个参数必须是 data.size(1)。
data = torch.Tensor(np.random.randint(0, 10, size=(1, 2, 3, 4)))
bn = torch.nn.BatchNorm2d(num_features=2, eps=1e-5, momentum=0.1, affine=True, track_running_stats=True)
output = bn(data) # output.shape = data.shape.
BN层的参数有 weight 和 bias,还有持久化缓存:
1. running_mean 和 running_var
因为训练数据较多,直接求训练数据的均值、方差计算量过大,所以只能在运行过程中求均值、方差,所以叫做 running_mean、running_var。这两个参数根据一批数据(mini-batch)更新,作为整个训练集的均值、方差。当使用 net.eval() 模式时,BN 层会用这两个参数把测试数据转换成这样的均值和方差的正态分布。
running_mean 和 running_var 是需要学习的参数,它们的初始值是 0 和 1,每次输入数据经过该 BN 层它们都会更新。假设 n、mean、var 分别是输入数据的元素个数、均值、方差,BN层参数 momentum 使用默认值 0.1。更新方法是:
{
r
u
n
n
i
n
g
_
m
e
a
n
=
r
u
n
n
i
n
g
_
m
e
a
n
×
(
1
−
m
o
m
e
n
t
u
m
)
+
m
e
a
n
×
m
o
m
e
n
t
u
m
r
u
n
n
i
n
g
_
v
a
r
=
r
u
n
n
i
n
g
_
v
a
r
×
(
1
−
m
o
m
e
n
t
u
m
)
+
v
a
r
×
m
o
m
e
n
t
u
m
×
n
n
−
1
\left\{ \begin{array}{lr} running\_mean = running\_mean \times (1 - momentum) + mean \times momentum\\ running\_var = running\_var \times (1 - momentum) + var \times momentum \times \frac{n}{n-1} \end{array} \right.
{running_mean=running_mean×(1−momentum)+mean×momentumrunning_var=running_var×(1−momentum)+var×momentum×n−1n
2. num_batches_tracked
mini-batch 数量,也就是训练过程中网络进行了多少轮的输入输出。当 BN 层参数 momentum 为 None 时使该参数计算累计滑动平均值(cumulative moving average),否则计算指数滑动平均值(exponential moving average)。
if self.momentum is None: # use cumulative moving average
exponential_average_factor = 1.0 / self.num_batches_tracked.item()
else: # use exponential moving average
exponential_average_factor = self.momentum
5. Dropout层
Dropout 以概率 p 把输入数据置 0,然后把所有数据乘 1 1 − p \frac{1}{1 - p} 1−p1。如果 inplace = True,数据的操作是 in-place 类型,也就是在输入数据所在内存操作。
data = torch.Tensor(np.random.randint(1, 2, size=(3, 4)))
dropout = torch.nn.Dropout(p=0.5, inplace=False)
output = dropout(data)
Dropout 可以处理 1 维数据,当处理多维数据时,它把每个 2 维平面上的每个元素随机置 0。Dropout2d 处理的数据维度至少是 2 维,它把每个平面上每一行随机置 0。
6. 全连接层
全连接层的输入参数有 in_features、out_features、bias,分别指定每个输入样本的大小、每个输出样本的大小、是否使用偏置项。
全连接层包含的参数有 weight、bias(可选)。全连接层包含的参数个数只与每个样本的输入大小和输出大小有关,与样本数量无关。
data = torch.Tensor(np.random.randint(0, 10, size=(3, 4)))
full_connect = torch.nn.Linear(in_features=4, out_features=5, bias=False)
output = full_connect(data) # output.shape = [3, 5].
parameter_count = sum(x.numel() for x in full_connect.parameters()) # (4 + 1) * 5.
data 是输入,output 是输出。输入包含 3 个样本,输出也会有 3 个样本。
7. 参考
- Mish 激活函数,CSDN