卷积(Convolution)
卷积是卷积神经网络中最常用的操作,卷积到底是如何运算的呢?
我们先从一维来看,如上图所示,有一个滑动的窗口从左到右,从上到下滑动,这个滑动的 3 ∗ 3 3*3 3∗3的矩阵我们就称作卷积核,卷积核里面的数和输入矩阵的数字一一相乘后相加,就得到了一个新的数字,这些新的数字按从左到右、从上到下的顺序排列,就得到了输出矩阵,整个这个过程,我们就叫做卷积。
通过一维的卷积我们可以看到,输入的矩阵内的数字是固定的,输出矩阵的数字其实完全取决于卷积核内的数字,如果我想要输出正确的结果,只能通过修正卷积核。换句话来说,卷积核就是我们需要修正的参数,这一点希望大家能够明确。
对于图像来讲,我们看到的都是三通道的彩色图(RGB格式),也就是说最初的输入是一个3通道的矩阵,比如(3,640,640),对于多通道的矩阵,卷积操作也不难理解。
其实就是将一维的卷积重复了三次,有3个卷积窗口分别滑动得到了三个矩阵,最后将这三个矩阵内的数相加,得到了最后的输出。可以看到上图的输入shape为(3,5,5),卷积核的shape为(3,3,3),输出的shape为(1,3,3)
然而,输入图像往往需要经过多个卷积层才能输出,也就是说,输入的通道数是不固定的,可能是3、16、64等等,对于这些情况如何卷积呢?
如上图所示,有一个通道数很大的输入矩阵,类比我们三通道的卷积方式,其实也不难看出,卷积核的通道数应该等于输入的通道数,这样每一层对应起来,其实就变成了一维的形式,最后把每层的结果相加,就得到了一个一维的输出矩阵。
到这边其实还没有结束,因为除了输入通道以外,我们还希望可以控制输出通道数。具体如何控制输出通道数呢?
就是用Dout个卷积核重复我们上一步的操作,这样的话,输入通道和输出通道都可以是任意的数。
我们可以作出以下的总结,对于一个(Din,Hin,Win)的输入矩阵,经过Dout个(Din,h,w)的卷积核之后,输出为(Dout,Hout,Wout)的矩阵。换句话来说卷积核的通道数等于输入通道数,卷积核的个数等于输出通道数。
实现卷积操作
import paddle
import paddle.nn as nn
# N C H w
input = paddle.rand(shape=[1, 3, 32, 32])
# N:批尺寸
# C:通道数
# H:特征高度
# W:特征宽度
Conv2D_func = nn.Conv2D(in_channels=3, out_channels=16, kernel_size=2, stride=2, data_format="NCHW")
print("输入矩阵的形状:", input.shape)
kernel_weights = Conv2D_func.weight.numpy()
print("卷积核形状:", kernel_weights.shape)
output = Conv2D_func(input)
print("Conv2D的输出矩阵的形状:", output.shape)
输出
输入矩阵的形状: [1, 3, 32, 32]
卷积核形状: (16, 3, 2, 2)
Conv2D的输出矩阵的形状: [1, 16, 16, 16]
池化(Pooling)
除了卷积以外,池化也是一个比较常用的运算操作,池化主要分为两种最大池化和平均池化。
![[7.jpg]]
![[8.jpg]]
上图分别为最大池化和平均池化,池化窗口的大小为(2,2),步长为2,池化就是将这个窗口内的数变成一位数,对于最大池化来说,就是变成窗口内的最大数,平均池化就是变成窗口内所有数的平均值。对于多通道的输入来说,池化和卷积不同,池化就是对每一个通道上都进行这样的操作,最后不需要加在一起,也就是说池化前后的输入输出通道不变,仅仅改变了特征图的大小。
更加直观得来说,池化其实相当于对特征图进行了resize的操作。图像的特征特征不会明显改变,池化可以有效地减少参数量,而且可以有效地防止过拟合。
实现两种池化操作
# 池化
# 1. 最大池化
print("---最大池化---")
MaxPool2D_func = nn.MaxPool2D(kernel_size = 2, stride = 2, padding = 0) # 定义一个池化函数实例
print("输入矩阵的形状:", input.shape)
output = MaxPool2D_func(input)
print("输出矩阵:", output.shape)
# 2. 平均池化
print("---平均池化---")
MaxPool2D_func = nn.AvgPool2D(kernel_size = 2, stride = 2, padding = 0) # 定义一个池化函数实例
print("输入矩阵的形状:", input.shape)
output = MaxPool2D_func(input)
print("输出矩阵:", output.shape)
输出
---最大池化---
输入矩阵的形状: [1, 3, 32, 32]
输出矩阵: [1, 3, 16, 16]
---平均池化---
输入矩阵的形状: [1, 3, 32, 32]
输出矩阵: [1, 3, 16, 16]
归一化(Normalization)
归一化的含义也非常简单,就是把矩阵内所有的数不失真地映射到0-1之间。这样做可以防止某一维或某几维对数据影响过大,一般我们在卷积运算之后紧跟着都需要进行归一化操作。
左图是未归一化的梯度等高图,右图是归一化之后的图,明显左图受到某些维度的数据影响过大,右图梯度下降就平缓很多,相对更容易达到期望的最优解。
实现softmax归一化
# 归一化
print("---归一化---")
batch_normalize_func = nn.Softmax()
output = batch_normalize_func(input)
print("归一化结果:", output)
输出
---归一化---
归一化结果: Tensor(shape=[1, 3, 32, 32], dtype=float32, place=Place(gpu:0), stop_gradient=True,
[[[[0.02945272, 0.02023173, 0.03672620, ..., 0.02727095,
0.04800135, 0.03767298],
[0.02483018, 0.03907410, 0.04241405, ..., 0.02664823,
0.03834482, 0.02681958],
[0.02151689, 0.04063185, 0.02607030, ..., 0.02340001,
0.03528566, 0.03000160],
...,
[0.03189616, 0.03406674, 0.03284206, ..., 0.01868608,
0.04039628, 0.03823650],
[0.02570179, 0.04229375, 0.03318217, ..., 0.03910165,
0.03136663, 0.03478132],
[0.04273928, 0.04915112, 0.02639926, ..., 0.02896717,
0.03779081, 0.02076009]],
[[0.02125637, 0.01911546, 0.03468457, ..., 0.03193741,
0.02249386, 0.03008775],
[0.02613716, 0.03536418, 0.04924056, ..., 0.02179346,
0.03020715, 0.02932096],
[0.02268929, 0.02485034, 0.04595596, ..., 0.05259003,
0.03095243, 0.02420499],
...,
[0.03150102, 0.02546015, 0.02193094, ..., 0.03013777,
0.03983514, 0.04770198],
[0.03008535, 0.03357872, 0.02689368, ..., 0.02260291,
0.01929821, 0.01890717],
[0.04432007, 0.02071708, 0.01900296, ..., 0.04294547,
0.02441577, 0.03218404]],
[[0.05063424, 0.03174394, 0.03208403, ..., 0.04883498,
0.01918886, 0.02432425],
[0.02613127, 0.04466325, 0.01913738, ..., 0.03364428,
0.02955570, 0.02185302],
[0.02892951, 0.01994222, 0.02151663, ..., 0.03105490,
0.02314292, 0.02446281],
...,
[0.02056237, 0.02511812, 0.03405361, ..., 0.03050380,
0.04212487, 0.02005277],
[0.04123520, 0.02985365, 0.03573005, ..., 0.01828455,
0.03955228, 0.01976910],
[0.01794492, 0.03676109, 0.02784664, ..., 0.02514183,
0.01729956, 0.03750264]]]])
激活函数(Activation)
通过以上的讨论,不难发现整个卷积网络都是对矩阵的一些线性化操作,对于一些非线性的情况,是无法精确拟合的,这就需要增加一些函数略微扭曲输出的结果,这些函数也被称为激活函数。
常见的激活函数有很多,例如relu,swish,mish,softmax等。
这些激活函数看似区别不大,对最终的模型表现却有极大的影响。例如有些激活函数是无上下限的,有些函数时间成本很高,这些都会影响到最后模型的速度和精度。
激活函数
# 激活函数relu
print("---激活函数---")
x = paddle.to_tensor([-2.0, -1.0, 0, 1.0, 2.0])
output = F.relu(x)
print("relu", output)
输出
---激活函数---
relu Tensor(shape=[5], dtype=float32, place=Place(gpu:0), stop_gradient=True,
[0., 0., 0., 1., 2.])