Pytorch: torch.nn 模块与网络组成单元
Copyright: Jingmin Wei, Pattern Recognition and Intelligent System, School of Artificial and Intelligence, Huazhong University of Science and Technology
文章目录
本教程不商用,仅供学习和参考交流使用,如需转载,请联系本人。
torch.nn 为 Pytorch 的核心模块,包含已准备好的层,损失函数,优化器等等。损失函数和优化器在下一个教程,深度神经网络搭建中会详细讲解,这一部分主要讲解不同的层的原理,参数计算和使用方式。
不同的网络层,构成了神经网络的基本单元。一个神经网络,主要由如下的一种,或者多种层进行排列组合,得到完整的模型,不同层的功能不尽相同:
卷积层:离散运算,卷积网络最基础的组成部分,用来提取数据的特征。主要分为普通卷积,空洞卷积,转置卷积等等。
池化层:用来降低特征图参数量,提高计算速度,增加感受野,是一种降采样操作。常见的有最大值池化和平均值池化。
激活函数层:非线性映射,以形成更复杂的表达空间,提高高语义信息,增加网络的表达能力。常用的有 Sigmoid, Tanh, ReLU, LeakyReLU, PReLU, RReLU, Softplus, Softmax 等等。
循环层:具有反馈结构的网络层,用来构成循环网络。
Droupout 层:使神经元按概率 p 部分停止工作,防止过拟合,起到一定的正则化效果。
Batch Normalization(BN) 层:伴随网络加深,浅层参数的微弱变化经过多层变换后会被放大,改变了每一层的输入分布,导致模型难以收敛。因此 BN 层,从改变数据分布的角度避免参数陷入饱和区,有效缓解了梯度消失问题,使模型更稳定。
Group Normalization(GN) 层:与 BN 层在 batch 维度操作不同,GN 层主要在通道维度进行分组(group)操作。
全连接层:一般连接在网络最后面,输入输出被延展成一维向量,用于最后的分类预测或者回归输出。
import torch
import torch.nn as nn
import torch.nn.functional as F
from matplotlib import pyplot as plt
from PIL import Image
import numpy as np
import cv2
Convolution Layer-卷积层
离散运算,卷积网络最基础最核心的组成部分,用来提取数据的特征。
详细的二维卷积运算过程请自行翻阅信号相关资料。
方法原型说明:
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1,
padding=0, dilation=1, groups=1,
bias=True, padding_mode='zeros')
参数说明和默认参数设置如下:
in_channels, # 输入图像的通道数D_1
out_channels, # 输出特征映射的数量,即卷积核(滤波器)个数K
kernel_size, # 卷积核尺寸F
stride=1, # 卷积的滑动步长S,一般为1,如果大于1,则输出特征图尺寸会小于输入特征图
padding=0, # 边缘填充的数量P
dilation=1, # 卷积核元素的步幅,可调整空洞卷积的大小
groups=1, # 从输入到输出的阻塞连接数,可实现组卷积,进而达到稀疏连接
bias=True, # 是否添加偏置
padding_mode='zeros' # 填充模式,默认是0填充,其他常用的还有边缘填充
卷积层计算方法
假设某层输入为输入大小为 W 1 × H 1 × D 1 W_1 × H_1 × D_1 W1×H1×D1 , 滤波器个数为 K K K , 尺寸为 F F F , S t r i d e Stride Stride 为 S S S , 零填充为 P P P 。
卷积后得到的特征图大小是 W 2 × H 2 × D 2 W_2 × H_2 × D_2 W2×H2×D2 , 这里:
W 2 = W 1 − F + 2 P S + 1 , H 2 = H 1 − F + 2 P S + 1 , D 2 = F W_2=\frac{W_1-F+2P}{S}+1,\ H_2=\frac{H_1-F+2P}{S}+1, D_2=F W2=SW1−F+2P+1, H2=SH1−F+2P+1,D2=F
卷积层的参数量是:
( F ⋅ F ⋅ D 1 ) ⋅ K + K (F\cdot F\cdot D_1)\cdot K + K (F⋅F⋅D1)⋅K+K
我们搭建一个具体的卷积层来进行分析:
假设现在有 20 20 20 张( b a t c h _ s i z e = 20 batch\_ size=20 batch_size=20 ) 大小为 W × H = 1920 ⋅ 1080 W\times H=1920\cdot1080 W×H=1920⋅1080 的 R G B RGB RGB 图(因此深度 D = 3 D=3 D=3 ),且每个像素点的值为 ( 255 , 255 , 255 ) (255, 255, 255) (255,255,255) ,即全部都是黑色。
input_data = torch.full((20, 3, 1920, 1080), fill_value = 255, dtype=torch.float)
即输入的特征图参数为 W 1 × H 1 × D 1 = 1920 ⋅ 1080 ⋅ 3 W_1\times H_1\times D_1=1920\cdot 1080\cdot 3 W1×H1×D1=1920⋅1080⋅3 。则卷积层的 i n _ c h a n n e l s ( D 1 ) = 3 in\_ channels(D_1)=3 in_channels(D1)=3 已经被唯一确定,不可修改。
输出的特征图参数为 W 2 × H 2 × D 2 W_2\times H_2\times D_2 W2×H2×D2 ,由卷积核和输入数据共同决定。
卷积层输出要求:我们规定,这层卷积的滤波器个数 o u t _ c h a n n e l s ( K ) = 16 out\_ channels(K)=16 out_channels(K)=16,滤波器大小( k e r n e l _ s i z e kernel\_ size kernel_size ) 为 F × F = 3 × 3 F\times F=3\times3 F×F=3×3。填充 s t r i d e ( S ) = 2 stride(S)=2 stride(S)=2 ,填充默认是 0 0 0 填充,大小为 z e r o _ p a d d i n g ( P ) = 1 zero\_ padding(P)=1 zero_padding(P)=1 ,不存在稀疏连接 g r o u p s = 1 groups=1 groups=1 , 不设置空洞卷积 d i l a t i o n = 1 dilation=1 dilation=1 ,存在偏置 b i a s = T r u e bias=True bias=True。
这一层的参数量是:
( F ⋅ F ⋅ D 1 ) ⋅ K + K = 3 ⋅ 3 ⋅ 3 ⋅ 16 + 16 = 448 (F\cdot F\cdot D_1)\cdot K + K=3\cdot3\cdot3\cdot16+16=448 (F⋅F⋅D1)⋅K+K=3⋅3⋅3⋅16+16=448
# 根据输出要求搭建一个卷积层对象
my_conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3,
stride=2, padding=1, dilation=1, groups=1, bias=True)
# 查看信息
my_conv
Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
注:在 t o r c h s u m m a r y torchsummary torchsummary 可视化方法中,表格的显示 i n p u t _ s i z e = D 1 × W 1 × H 1 input\_size = D_1\times\ W_1\times H_1 input_size=D1× W1×H1。
通过 .weight 和 .bias 能查看卷积核的权重和偏置。
我们注意,nn.Module 库定义的网络层会自动学习参数,比如 Fully-connected, Convolution, Pooling 层等等,适合于定义需要学习参数的层。因此下面参数的自动求导参数为 True 。
nn.functional 库定义的网络层不会自动学习参数,使用针对于 Activation, Batch Normalization 层,而且需要用nn.Parameter 封装,可用于定义不需要学习参数的层。
# print(my_conv.weight)
print(my_conv.weight.shape)
torch.Size([16, 3, 3, 3])
print(my_conv.bias)
print(my_conv.bias.shape)
Parameter containing:
tensor([-0.1697, 0.1827, 0.0775, 0.0341, 0.0827, -0.0182, 0.0954, 0.1548,
-0.1561, -0.0243, 0.0243, 0.0184, 0.0530, 0.0241, -0.0035, -0.0584],
requires_grad=True)
torch.Size([16])
前文输入数据时,我们已经强调过,特征必须为四维,即 b a t c h _ s i z e × D 1 × W 1 × H 1 = 20 ⋅ 3 ⋅ 1920 ⋅ 1080 batch\_ size\times D_1\times W_1\times H_1=20\cdot3\cdot1920\cdot1080 batch_size×D1×W1×H1=20⋅3⋅1920⋅1080
input_data.shape
torch.Size([20, 3, 1920, 1080])
然后我们调用卷积核对输入数据进行卷积:
output_feature = my_conv(input_data)
# 计算输出特征图的大小
output_feature.shape
torch.Size([20, 16, 960, 540])
即输出特征图的尺寸 b a t c h _ s i z e × D 2 × W 2 × H 2 = 20 ⋅ 16 ⋅ 960 ⋅ 540 batch\_ size\times D_2\times W_2\times H_2=20\cdot16\cdot960\cdot540 batch_size×D2×W2×H2=20⋅16⋅960⋅540
我们利用一开始说的参数计算的公式推导,来计算输出特征图的尺寸是否正确:
W 2 = W 1 − F + 2 P S + 1 = 1920 − 3 + 1 2 + 1 = 960 H 2 = H 1 − F + 2 P S + 1 = 1080 − 3 + 1 2 + 1 = 540 D 2 = K = 16 W_2=\frac{W_1-F+2P}{S}+1=\frac{1920-3+1}{2}+1=960\\ H_2=\frac{H_1-F+2P}{S}+1=\frac{1080-3+1}{2}+1=540\\ D_2=K=16 W2=SW1−F+2P+1=21920−3+1+1=960H2=SH1−F+2P+1=21080−3+1+1=540D2=K=16
可以看到,是完全匹配的。
实例:打开图像和提取边缘
img = Image.open("Lenna.tif")
# 转为灰度图
imggray = np.array(img.convert('L'), dtype = np.float32)
img.size
(512, 512)
# 图像可视化
plt.figure(figsize=(6, 6))
plt.imshow(imggray, cmap=plt.cm.gray)
plt.axis('off')
plt.show()
# 将 512*512 的数组,转为 1*1*512*512 的张量
high, width = imggray.shape
img_tensor = torch.from_numpy(imggray.reshape((1, 1, 512, 512)))
img_tensor.shape
torch.Size([1, 1, 512, 512])
卷积,提取图像轮廓
# 定义边缘检测卷积核,并将维度处理为 1*1*5*5
kersize = 5
ker = torch.ones(kersize, kersize, dtype=torch.float32) * -1
ker[2, 2] = 24
ker = ker.reshape((1, 1, kersize, kersize))
# 设置卷积核:灰度图in_channels=1, out_channels=2, kernel_size=5*5, 不添加偏置
conv2d = nn.Conv2d(1, 2, (kersize, kersize), bias=False)
conv2d.weight.data[0] = ker
# 卷积
out = conv2d(img_tensor)
# 维度压缩
out = out.data.squeeze()
out.shape
torch.Size([2, 508, 508])
# 可视化卷积结果
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(out[0], cmap=plt.cm.gray) # 使用的为边缘特征提取卷积核
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(out[1], cmap=plt.cm.gray) # 使用的卷积核为随机数,结果与原始图类似
plt.axis('off')
plt.show()
Pooling Layer-池化层
降维,对数据进一步浓缩,缓解内存压力。池化是一种降采样操作,是一种较强的先验,可以使模型关注全局特征而非局部出现的位置,能保留一些重要的特征信息,提升容错,防止过拟合。
主要分为平均值池化和最大值池化。详细的二维池化运算过程请自行翻阅信号相关资料。
方法原型说明:
torch.nn.MaxPool2d(kernel_size, stride, padding,
dilation, ceil_mode, return_indices)
kernel_size, # 池化窗口大小
stride, # 步长
padding, # 边缘填充0的数量
dilation, # 控制窗口的步幅
ceil_mode, # 为True,计算输出信号大小时会向上取整
return_indices # 为True会返回输出最大值的索引
池化层计算方法
假设某层输入为输入大小为 W 1 × H 1 × D 1 W_1 × H_1 × D_1 W1×H1×D1 , 池化窗口尺寸为 F F F , S t r i d e Stride Stride 为 S S S 。
池化后得到的特征图大小是 W 2 × H 2 × D 2 W_2 × H_2 × D_2 W2×H2×D2 , 这里:
W 2 = W 1 − F S + 1 , H 2 = H 1 − F S + 1 , D 2 = D 1 W_2=\frac{W_1-F}{S}+1,\ H_2=\frac{H_1-F}{S}+1, D_2=D_1 W2=SW1−F+1, H2=SH1−F+1,D2=D1
池化层的参数量为 0 0 0 !
使用上例卷积操作后得到的特征图,经过卷积得到的数据大小为 b a t c h _ s i z e × D × W × H = 20 ⋅ 16 ⋅ 960 ⋅ 540 batch\_ size\times D\times W\times H=20\cdot16\cdot960\cdot540 batch_size×D×W×H=20⋅16⋅960⋅540 。
设置池化窗口大小 k e r n e l _ s i z e ( F ) = 2 kernel\_ size(F)=2 kernel_size(F)=2 ,步长 s t r i d e ( S ) = 2 stride(S)=2 stride(S)=2 。
max_pooling = nn.MaxPool2d(2, stride=2)
aver_pooling = nn.AvgPool2d(2, stride=2)
input = output_feature # 使用上例卷积操作后得到的特征图
output_feature2 = max_pooling(input)
output_feature2.shape
torch.Size([20, 16, 480, 270])
output_feature3 = aver_pooling(input)
output_feature3.shape
torch.Size([20, 16, 480, 270])
即输出特征图的尺寸 b a t c h _ s i z e × D × W × H = 20 ⋅ 16 ⋅ 480 ⋅ 270 batch\_ size\times D\times W\times H=20\cdot16\cdot480\cdot270 batch_size×D×W×H=20⋅16⋅480⋅270
根据上述公式:
W 2 = W 1 − F S + 1 = 960 − 2 2 + 1 = 480 , H 2 = H 1 − F S + 1 = 540 − 2 2 + 1 = 270 , D 2 = D 1 = 16 W_2=\frac{W_1-F}{S}+1=\frac{960-2}{2}+1=480,\\ H_2=\frac{H_1-F}{S}+1=\frac{540-2}{2}+1=270, \\ D_2=D_1=16 W2=SW1−F+1=2960−2+1=480,H2=SH1−F+1=2540−2+1=270,D2=D1=16
可以看到,公式计算结果很代码显示的结果也是完全匹配的。
对于上例的卷积图像进行池化操作
# 最大值池化
maxpool2 = nn.MaxPool2d(2, stride = 2)
out2 = maxpool2(out)
# 维度压缩
out2 = out2.data.squeeze()
out2.shape
torch.Size([2, 254, 254])
# 可视化最大值池化结果
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(out2[0], cmap=plt.cm.gray) # 使用的为边缘特征提取卷积核
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(out2[1], cmap=plt.cm.gray) # 使用的卷积核为随机数,结果与原始图类似
plt.axis('off')
plt.show()
# 平均值池化
avgpool2 = nn.AvgPool2d(2, stride = 2)
out3 = avgpool2(out)
# 维度压缩
out3 = out3.data.squeeze()
out3.shape
torch.Size([2, 254, 254])
# 可视化平均值池化结果
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(out3[0], cmap=plt.cm.gray) # 使用的为边缘特征提取卷积核
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(out3[1], cmap=plt.cm.gray) # 使用的卷积核为随机数,结果与原始图类似
plt.axis('off')
plt.show()
# 自适应平均值池化
adaavgpool2 = nn.AdaptiveAvgPool2d(output_size=(100, 100))
out4 = adaavgpool2(out)
# 维度压缩
out4 = out3.data.squeeze()
out4.shape
torch.Size([2, 254, 254])
# 可视化自适应平均值池化结果
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(out4[0], cmap=plt.cm.gray) # 使用的为边缘特征提取卷积核
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(out4[1], cmap=plt.cm.gray) # 使用的卷积核为随机数,结果与原始图类似
plt.axis('off')
plt.show()
Activation Layer-激活函数层
以形成更复杂的表达空间,提高高语义信息,增加网络的表达能力。
nn.Sigmoid(Logistic Function)
模拟生物神经元特性,即当神经元获得的输入超过一定阈值后,神经元会处于兴奋状态,否则处于抑制状态。
Sigmoid 函数作为激活函数的优点是连续、单调、可导且具有非线性特点。但其缺点是非中心对称,输出不是 0 均值的,同时它的导数函数曲线见右图,x 变化很小的范围内,导数才有值且不大,在神经网络应用中,训练过程使用的是反向传播算法,通过链式法则回传的梯度不断相乘,因此,Sigmoid 函数作为激活函数时会导致梯度消失问题,尤其在网络比较深时会达不到训练效果。
σ ( x ) = 1 1 + e − x \sigma(x)=\frac{1}{1+e^{-x}} σ(x)=1+e−x1
input = torch.ones(1, 1, 2, 2)
sigmoid = nn.Sigmoid()
sigmoid(input)
tensor([[[[0.7311, 0.7311],
[0.7311, 0.7311]]]])
双曲正切-Tanh
双曲正切函数作为激活函数的优点是中心对称、单调、连续、可导且具有非线性特点。但其缺点,x 变化更小的范围内,导数才有值且不大于 1,在神经网络应用中,训练过程使用的是反向传播算法,通过链式法则回传的梯度不断相乘,因此,双曲正切函数作为激活函数时同样会导致梯度消失问题,尤其在网络比较深时会达不到训练效果。
t a n h ( x ) = e x − e − x e x + e − x tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}} tanh(x)=ex+e−xex−e−x
input = torch.ones(1, 1, 2, 2)
tanh = nn.Tanh()
tanh(input)
tensor([[[[0.7616, 0.7616],
[0.7616, 0.7616]]]])
修正线性单元-ReLU
ReLU(Rectified Linear Unit) 有着优越的性能和简单优雅的实现,是最常用的激活函数之一。
R e L U ( x ) = max ( 0 , x ) = { 0 i f x < 0 x i f x ⩾ 0 ReLU(x)=\max(0, x)= \begin{cases} 0\quad if\ x<0 \\ x\quad if\ x\geqslant0 \end{cases} ReLU(x)=max(0,x)={0if x<0xif x⩾0
ReLU 函数作为激活函数的优点是梯度不会饱和,解决了梯度消失问题,没有指数计算,计算复杂度低。它的缺点是非中心对称,负数部分的梯度为 0,导致相应参数不会更新。
input = torch.randn(1, 1, 2, 2)
input
tensor([[[[-0.3578, 0.7376],
[-1.4350, -1.3372]]]])
inplace=True 表示直接将运算结果覆盖到输入中,以节省内存。
relu = nn.ReLU(inplace=True)
relu(input)
tensor([[[[0.0000, 0.7376],
[0.0000, 0.0000]]]])
带泄露单元的ReLU-Leaky ReLU
优化的 ReLU 版本,让负区间避免直接全部置 0 ,而是赋予很小的权重,即一定程度上更新负数部分的相应参数。
L e a k y R e L U ( x ) = max ( 1 a i x , x ) = { 1 a i x i f x < 0 x i f x ⩾ 0 LeakyReLU(x)=\max(\frac{1}{a_i}x, x)= \begin{cases} \frac{1}{a_i}x\quad if\ x<0 \\ x\quad if\ x\geqslant0 \end{cases} LeakyReLU(x)=max(ai1x,x)={ai1xif x<0xif x⩾0
常用 1 a i = 0.1 \frac{1}{a_i}=0.1 ai1=0.1 。
input = torch.randn(1, 1, 2, 2)
input
tensor([[[[ 0.7689, -1.9128],
[ 0.4807, -1.2859]]]])
设 a i = 10 a_i=10 ai=10 ,且存在 in-place 操作:
leakyrelu = nn.LeakyReLU(0.1, inplace=True)
leakyrelu(input)
tensor([[[[ 0.7689, -0.1913],
[ 0.4807, -0.1286]]]])
leakyrelu.negative_slope
0.1
参数化修正线性单元-PReLU
PReLU 可以看作是 Leaky ReLU 的一个变体。在 PReLU 中,负值部分的斜率是根据数据来定的,而非预先定义的。
input = torch.randn(1, 1, 2, 2)
input
tensor([[[[ 2.1989, -0.4237],
[-0.2249, 0.0554]]]])
prelu = nn.PReLU()
prelu(input)
tensor([[[[ 2.1989, -0.1059],
[-0.0562, 0.0554]]]], grad_fn=<PreluBackward>)
# 查看此时的a_i
prelu.weight.data.tolist()[0]
0.25
随机修正线性单元-RReLU
RReLU 也是 Leaky ReLU 的一个变体。在 RReLU 中,负值的斜率在训练中是随机的,在之后的测试中就变成了固定的了。RReLU 的亮点在于,在训练环节中,aji是从一个均匀的分布U(I,u)中随机抽取的数值。形式上来说,我们能得到以下结果:
R R e L U ( x ) = { a j i x j i i f x < 0 x j i i f x ⩾ 0 w h e r e a j i ∼ U ( l , u ) , l < u a n d l , u ∈ [ 0 , 1 ) RReLU(x)= \begin{cases} a_{ji}x_{ji}\quad if\ x<0 \\ x_{ji}\quad if\ x\geqslant0 \end{cases}\quad where\ a_{ji}\sim U(l,u), l<u\ and\ l,u\in[0,1) RReLU(x)={ajixjiif x<0xjiif x⩾0where aji∼U(l,u),l<u and l,u∈[0,1)
-
Leaky ReLU中的 a i a_i ai 是固定的。
-
PReLU中的 a i a_i ai 是根据数据变化的。
-
RReLU中的 a j i a_{ji} aji 是一个在一个给定的范围内随机抽取的值,这个值在测试环节就会固定下来。
input = torch.randn(1, 1, 2, 2)
input
tensor([[[[ 0.8843, -0.6539],
[-0.5519, 2.1376]]]])
设置一个 a j i a_{ji} aji 随机数在 0~0.5 之间的 RReLU:
rrelu = nn.RReLU(0, 0.5, True)
rrelu(input)
tensor([[[[ 0.8843, -0.0285],
[-0.0311, 2.1376]]]])
ReLU 的平滑近似-Softplus
其实它就是 Logistic-Sigmoid 函数,加 1 是为了保证非负性,可以看做 max ( 0 , x ) \max(0,x) max(0,x) 的平滑。
s o f t p l u s ( x ) = log ( 1 + e x ) softplus(x)=\log(1+e^x) softplus(x)=log(1+ex)
input = torch.randn(1, 1, 2, 2)
input
tensor([[[[-0.4834, 0.1555],
[ 0.1482, 0.8827]]]])
softplus = nn.Softplus()
softplus(input)
tensor([[[[0.4804, 0.7739],
[0.7700, 1.2289]]]])
可视化几种激活函数图像
x = torch.linspace(-6, 6, 100)
sigmoid = nn.Sigmoid()
y1 = sigmoid(x)
tanh = nn.Tanh()
y2 = tanh(x)
relu = nn.ReLU()
y3 = relu(x)
leakyrelu = nn.LeakyReLU(0.1)
y4 = leakyrelu(x)
prelu = nn.PReLU()
y5 = prelu(x)
rrelu = nn.RReLU(0, 0.5)
y6 = rrelu(x)
softplus = nn.Softplus()
y7 = softplus(x)
plt.figure(figsize = (14, 6))
plt.subplot(2, 4, 1)
plt.plot(x.data.numpy(), y1.data.numpy(), 'r-')
plt.title('Sigmoid')
plt.grid()
plt.subplot(2, 4, 2)
plt.plot(x.data.numpy(), y2.data.numpy(), 'r-')
plt.title('Tanh')
plt.grid()
plt.subplot(2, 4, 3)
plt.plot(x.data.numpy(), y3.data.numpy(), 'r-')
plt.title('ReLU')
plt.grid()
plt.subplot(2, 4, 4)
plt.plot(x.data.numpy(), y4.data.numpy(), 'r-')
plt.title('Leaky ReLU(ai='+str(leakyrelu.negative_slope)+')')
plt.grid()
plt.subplot(2, 4, 5)
plt.plot(x.data.numpy(), y5.data.numpy(), 'r-')
plt.title('PReLU(ai='+str(prelu.weight.data.tolist()[0])+')')
plt.grid()
plt.subplot(2, 4, 6)
plt.plot(x.data.numpy(), y6.data.numpy(), 'r-')
plt.title('RReLU')
plt.grid()
plt.subplot(2, 4, 7)
plt.plot(x.data.numpy(), y7.data.numpy(), 'r-')
plt.title('Softplus')
plt.grid()
多分类-Softmax
多个激活函数 one verse one(OVO) 叠加,过于麻烦,因此 Softmax 设计出用于解决多分类问题,一般可放在网络的最后部分。输入为多个类别的得分,输出为每一个类别对应的判定概率,所有的类别取值在 [ 0 , 1 ) [0,1) [0,1) 之间,且总和为 1 1 1 。
设 V i V_i Vi 为第 i i i 个类别得分, C C C 表示类别总数, S i S_i Si 为第 i i i 个类别的概率。
S i = e V i ∑ j C e V j S_i=\frac{e^{V_i}}{\sum_j^Ce^{V_j}} Si=∑jCeVjeVi
该函数在 torch.nn.functional 库中:
score = torch.randn(1, 4)
score
tensor([[-0.1559, 1.0321, -0.6477, 0.5218]])
F.softmax(score, 1)
tensor([[0.1457, 0.4781, 0.0891, 0.2870]])
Dropout Layer
有效解决过拟合现象,起到一定的正则化效果。
Dropout 使得训练时,每个神经元以概率 p p p 保留,概率 1 − p 1-p 1−p 停止工作,这样让模型不太依赖于局部特征,泛化推理能力更强。
在测试的时候,为了保证相同的输出期望值,每个参数还要乘以 p p p 。
另一种方式称为 Inverted Dropout ,即在训练时将保留下的神经元乘以 1 p \frac{1}{p} p1 ,这样测试的时候就不需要再改变权重了。
Dropout为什么可以防止过拟合,可以从以下方面解释。
-
多模型的平均:不同的固定神经网络会有不同的过报合,多个取平均则有可能让一些相反的拟合抵消掉,而Dropout每次都是不同的神经元失活,可以看做是多个模型的平均,类似于多数投票取胜的策略。
-
减少神经元间的依赖:由于两个神经元不一-定同时有效, 因此减少了特征之间的依赖,迫使网络学习有更为鲁棒的特征,因为神经网络不应该对特定的特征敏感,而应该从众多特征中学习更为共同的规律,这也起到了正则化的效果。
-
生物进化: Dropout类似于性别在生物进化中的角色,物种为了适应环境变化,在繁衔时取雄性和雌性的各一半基因进行组合, 这样可以适应更复杂的新环境,避免了单一基因的过拟合,当环境发生变化时也不至于灭绝。
设置 p = 0.5 p=0.5 p=0.5 , i n p l a c e = T r u e inplace=True inplace=True 直接将运算结果覆盖到输入中,以节省内存。
dropout = nn.Dropout(0.5, inplace=True)
input = torch.randn(1, 1, 4, 4)
output = dropout(input)
output
tensor([[[[ 0.0972, -1.2529, -0.0000, -0.0000],
[-0.8804, -0.0000, 0.2065, 0.0000],
[-0.0000, 0.0000, 2.3399, -0.0000],
[ 2.6936, -0.0000, -0.0000, 1.4938]]]])
在较为稀疏的卷积网络中,Dropout 的效果就不那么明显了。因此,可以使用 BN 层来正则化模型。
Batch Normalization Layer
伴随网络加深,浅层参数的微弱变化经过多层变换后会被放大,改变了每一层的输入分布,导致模型难以收敛。
BN 层,从改变数据分布的角度避免参数陷入饱和区,有效缓解了梯度消失问题,使模型更稳定。
由网络中参数变化现象,导致的内部节点数据分布发生变化的现象,称做 ICS(Internal Covariate Shit) 。ICS 容易使训练过程陷入饱和区,减慢网络的收敛。
ReLU 从激活函数的角度出发,在一定程度上解决了梯度饱和的现象。而 BN 层则从改变数据分布的角度避免了参数陷入饱和区。由于 BN 层优越的性能,其已经是当前卷积网络中的“标配” 。
BN 层首先对每一个 batch 的输入特征进行白化操作,即去均值方差过程。假
设一个 batch 的输入数据为
x
:
B
=
{
x
1
,
⋯
,
x
m
}
x: B=\{x_1,\cdots, x_m\}
x:B={x1,⋯,xm} 首先求该batch数据的均值与方差:
μ B ← 1 m ∑ i = 1 m x i σ B 2 ← 1 m ∑ i = 1 m ( x i − μ B ) 2 \mu_B\leftarrow\frac{1}{m}\sum_{i=1}^mx_i\\ \sigma_B^2\leftarrow\frac{1}{m}\sum_{i=1}^m(x_i-\mu_B)^2 μB←m1i=1∑mxiσ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{\sigma_B^2+\epsilon}} xi^←σB2+ϵxi−μB
白化操作可以使输入的特征分布具有相同的均值与方差,固定了每一层的输入分布,从而加速网络的收敛。
然而,白化操作虽然从一定程度上避免了梯度饱和,但也限制了网络中数据的表达能力,浅层学到的参数信息会被白化操作屏蔽掉,因此,BN层在白化操作后又增加了一个线性变换操作,让数据尽可能地恢复本身的表达能力,如下式所示:
y i ← γ x i ^ + β y_i\leftarrow\gamma\hat{x_i}+\beta yi←γxi^+β
公式中, γ \gamma γ 与 β \beta β 为新引进的可学习参数,最终的输出为 y i y_i yi 。
BN 层可以看做是增加了线性变换的白化操作,在实际工程中被证明了能够缓解神经所网络难以训练的问题。
BN 层的优点主要有以下3点:
-
缓解梯度消失,加速网络收敛。BN 层可以让激活函数的输入数据落在非饱和区,缓解了梯度消失问题。此外,由于每一层数据的均值与方差都在一定范围内,深层网络不必去不断适应浅层网络输入的变化,实现了层间解耦,允许每一层独立学习,也加快了网络的收敛。
-
简化调参,网络更稳定。在调参时,学习率调得过大容易出现震荡与不收敛,BN 层则抑制了参数微小变化随网络加深而被放大的问题,因此对于参数变化的适应能力更强,更容易调参。
-
防止过拟合。BN 层将每一个 batch 的均值与方差引入到网络中,由于每个 batch 的这两个值都不相同,可看做为训练过程增加了随机噪音,可以起到一定的正则效果,防止过拟合。
在测试时,由于是对单个样本进行测试,没有 batch 的均值与方差。因此通常做法是在训练时将每 batch 的均值与方差都保留下来,在测试时使用所有训练样本均值与方差的平均值。
bn = nn.BatchNorm2d(64)
# eps为公式中的\epsilon,momentum为均值方差的动量,affine为添加科学系参数
bn
BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
input = torch.randn(4, 64, 224, 224)
output = bn(input)
# BN 不改变特征大小
output.shape
torch.Size([4, 64, 224, 224])
Group Normalization Layer
尽管 BN 层取得了巨大的成功,但仍有一定的弊端,主要体现在以下两点:
-
由于是在 batch 的维度进行归一化, BN 层要求较大的 batch 才能有效地工作,而物体检测等任务由于占用内存较高,限制了 batch 的大小,这会限制 BN 层有效地发挥归一化功能。
-
数据的 batch 大小在训练与测试时往往不一样。在训练时般采用滑动来计算平均值与方差,在测试时直接拿训练集的平均值与方差来使用。这种方式会导致测试集依赖于训练集,然而有时训练集与测试集的数据分布并不致。
因此,我们能不能避开 batch 来进行归一化呢? 答案是可以的,最新的工作 GN(Group Normalization) 从通道方向计算均值与方差,使用更为灵活有效,避开了 batch 大小对归一化的影响。
具体来讲,GN 先将特征图的通道分为很多个组,对每一个组内的参数做归一化,而不是 batch 。 GN 之所以能够工作的原因,是在特征图中,不同的通道代表了不同的意义,例如形状、边缘和纹理等,这些不同的通道并不是完全独立地分布,而是可以放到一起进行归一化分析。
input = torch.randn(4, 64, 224, 224)
# 分组数自己设置,通道数量=输入数据的深度D
gn = nn.GroupNorm(num_groups=8, num_channels=64)
gn
GroupNorm(8, 64, eps=1e-05, affine=True)
output = gn(input)
output.shape
torch.Size([4, 64, 224, 224])
RNN Layer-循环层
用于设置 RNN 中的反馈结构。
方法原型说明:
torch.nn.RNN(mode, input_size, hidden_size,
num_layers=1, bias=True, batch_first=False,
dropout=0., bidirectional=False)
mode,
input_size, # 输入x的特征数
hidden_size, # 隐含层的特征数
num_layers=1, # 网络的层数
bias=True, # 为True则使用偏置权重
batch_first=False, # 为True,则输入和输出的shape应该是[batch_size, time_step, feature]
dropout=0., # 若值非零,则除了最后一层,其他层输出都会套上一个dropout层
bidirectional=False # 为True则会形成一个双向的RNN
Fully-connected Layer-全连接层
一般连接在卷积网络输出特征图的最后面,用于最后的分类。特点是每个节点都和上下层的所有节点相连,输入输出被延展成一维向量。
卷积主要是局部到整体地提取图像特征,而全连接层则用于将卷积抽象出的特征图进一步映射到特定维度的标签空间,以求的损失或者输出预测结果。
方法原型说明:
torch.nn.Linear(in_features, out_features, bias=True)
in_features, # 输入样本的特征数量
out_features, # 输出样本的特征数量
bias=True # 为True表示会学习偏置
input = torch.randn(4, 1024)
linear = nn.Linear(1024, 4096)
output = linear(input)
print(input.shape)
print(output.shape)
torch.Size([4, 1024])
torch.Size([4, 4096])
Global Average Pooling-全局平均池化层
全连接层会导致参数量非常庞大,这也是我们为什么从 FCN 过渡到 CNN 的主要原因之一。
大量参数会导致模型的应用部署困难,且存在参数冗余,也容易过拟合。因此我们可以使用全局平均池化层(Global Average Pooling)来取代全连接层,这个思想最早出现在 NIN(Network in Network) 网络中,其优势在于:
-
利用池化实现降维,极大减少网络参数量。
-
将特征提取和分类合二为一,一定程度上防止过拟合。
-
去除了全连接层,可以实现任意图像尺度的输入。
感受野与 Dilated Convolution(空洞卷积)
感受野
感受野(Receptive Field)是指特征图上的某个点能看到的输入图像的区域,即特征图上的点是由输入图像中感受野大小区域的计算得到的。
举个简单的例子,如图所示为一个三层卷积网络,每一层的卷积核为 3 × 3 3\times3 3×3 ,步长为 1 1 1 ,可以看到第层对应的感受野是 3 × 3 3\times3 3×3 ,第二层是 5 × 5 5\times5 5×5 ,第三层则是 7 × 7 7\times7 7×7 。
卷积层和池化层都会影响感受野,而激活函数层通常对于感受野没有影响。对于一般的卷积神经网络,感受野可由下两式计算得出。
R F l + 1 = R F l + ( K − 1 ) × S l S l = ∏ l = 1 l S t r i d e i RF_{l+1}=RF_l+(K-1)\times S_l\\ S_l=\prod_{l=1}^lStride_i RFl+1=RFl+(K−1)×SlSl=l=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 ,但是输入层中边缘点的使用次数明显比中间点要少,因此做出的贡献不同。经过多层的卷积堆叠之后,输入层对于特征图点做出的贡献分布呈高斯分布形状。
理解感受野是理解卷积神经网络工作的基础,尤其是对于使用 Anchor 作为强先验区域的物体检测算法,如 Faster RCNN 和 SSD ,如何设置 Anchor 的大小,Anchor应该对应在特征图的哪一层,都应当考虑感受野。通常来讲,Anchor的大小应该与感受野相匹配,尤其是有效的感受野,过大或过小都不好。
在卷积网络中,有时还需着要计算特征图的大小, 一般可以按照下式进行计算:
n o u t = n i n + 2 P + K S + 1 n_{out}=\frac{n_{in}+2P+K}{S}+1 nout=Snin+2P+K+1
其中, n i n n_{in} nin 与 n o u t n_{out} nout 分别为输入特征图与输出特征图的尺寸, P P P 代表这一层的 p a d d i n g padding padding 大小, K K K 代表这一层的卷积核 k e r n e l _ s i z e kernel\_ size kernel_size 大小, S S S 为步长。
Dilated Convolution-空洞卷积
空洞卷积最初是为解决图像分割的问题而提出的。常见的图像分割算法通常使用池化层来增大感受野,同时也缩小了特征图尺寸,然后再利用上采样(转置卷积)还原图像尺寸。
特征图缩小再放大的过程造成了精度上的损失,因此需要有一种操作可以在增加感受野的同时保持特征图的尺寸不变,从而替代池化与上采样操作。在这种需求下,空洞卷积就诞生了。
在近几年的物体检测发展中,空洞卷积也发挥了重要的作用。因为虽然物体检测不要求逐像素地检测,但是保持特征图的尺寸较大,对于小物体的检测及物体的定位来说也是至关重要的。
空洞卷积,顾名思义就是卷积核中间带有一些洞, 跳过一些元素进行卷积。 在此以 3 × 3 3\times3 3×3 卷积为例,其中,左图是普通的卷积过程,在卷积核紧密排列在特征图上滑动计算,而中图代表了空洞数为 2 2 2 的空洞老积,可以看到,在特征图上每 2 2 2 行或者 2 2 2 列送取元素与卷积核卷积。类似地,右图代表了空洞数为 3 3 3 的空洞卷积。
在代码实现时,它有一个概外的超参数 dilation rate 表示空洞数,普通卷积的 dilation rate 默认为 1 1 1 ,中图和右图的 dilation rate 为 3 3 3 。
在图中,同样的 3 × 3 3\times3 3×3 卷积,却可以起到 5 × 5 , 7 × 7 5\times5,7\times7 5×5,7×7 等卷积的效果,可以看出,空洞卷积在不下增加参数量的前提下,增大了感受野。假设空洞卷积的卷积核大小为 K K K ,空洞数为 d d d ,则其等效卷积核大小 K ′ K' K′ 计算如式所示:
K ′ = K + ( K − 1 ) × ( d − 1 ) K'=K+(K-1)\times(d-1) K′=K+(K−1)×(d−1)
在计算感受野时,只需要将原来的卷积核大小 K K K 更换为 K ′ K' K′ 即可。
空洞卷积的优点显而易见,在不引入额外参数的前提下可以任意扩大感受野,同时保持特征图的分辨率不变。这点在分割与检测任务中十分有用, 感受野的打大可以检测大物体,而特征图分辨率不变使得物体定位更加精准。
# 普通卷积
conv1 = nn.Conv2d(3, 256, 3, stride=1, padding=1, dilation=1)
# 空洞数为2的卷积
conv2 = nn.Conv2d(3, 256, 3, stride=1, padding=1, dilation=2)
print(conv1)
print(conv2)
Conv2d(3, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
Conv2d(3, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), dilation=(2, 2))
当然,空洞卷积也有自己的一些缺陷, 主要表现在以下 3 3 3 个方面:
-
网格效应(Gridding Effect):由于空洞卷积是一种稀疏的采样方式,当多个空洞卷积叠加时,有些像素根本没有被利用到,会损失信息的连续性与相关性,进而影响分割、检测等要求较高的任务。
-
远距离的信息没有相关性:空洞卷积采取了稀疏的采样方式,导致远距离卷积得到的结果之间缺乏相关性,进而影响分类的结果。
-
不同尺度物体的关系:大的 dilation rate 对于大物体分割与检测有利,但是对于小物体则有弊无利,如何处理好多尺度问题的检测,是空洞卷积设计的重点。
对于上述问题,有多篇文章提出了不同的解决方法,典型的有图森未来提出的 HDC(Hybrid Dilated Convolution) 结构。该结构的设计准则是堆叠卷积的 dilation rate 不能有大于 1 1 1 的公约数,同时将 dilation rate 设置为类似于 [ 1 , 2 , 5 , 1 , 2 , 5 ] [1,2,5,1,2,5] [1,2,5,1,2,5] 这样的锯齿类结构。此外各 dilation rate 之间还需要满足一个 数学公式,这样可以尽可能地覆盖所有空洞,以解决网格效应与远距离信息的相关性问题。
具体的利用空洞卷积解决图像识别问题请看之后的相关教程。