卷积神经网络(Convolutional Neural Networks, CNN)
前言
卷积神经网络是深度学习模型的一种,也是一种计算框架吧,就像【人工智能学习】【六】循环神经网络一样,现在没有纯用CNN做项目的,而是基于CNN的AlxNet,VGG等模型,在后面的文章中会进行介绍。
ImageNet图像识别挑战赛(ImageNet Large Scale Visual Recognition Challenge),在2012年之前参赛模型普遍选用特征+支持向量机(Support Vector Machine,SVM)。在2012年,Alex Krizhevsky和他的导师Geoff Hinton提出的基于卷积神经网络架构的模型,以巨大的优势获得当年的ImageNet图像分类挑战赛的桂冠。
如图所示,2012年较2011年,图像分类的错误率从26%下降到16%,由此掀开了人们对卷积神经网络研究的热潮。以卷积神经网络为架构的模型在随后的ImageNet挑战赛中垄断了所有冠军。2015年,由微软亚洲研究院提出的深度残差神经网络(Deep Residual Networks,ResNet)取得了图像分类错误率3.5%的成绩,使计算机对图像的识别正确率第一次超越了人类。(这句话出自斯坦福cs231n计算机视觉课程)
卷积神经网络以多层感知机(Multi-layer perceptron,MPL)【人工智能学习】【三】多层感知机为基础。20世纪60年代,Hubel和Wiesel在研究猫大脑皮层中基础视觉区域的激活过程时发现,大脑对图像处理的前期,并不是对整体的鱼或老鼠进行处理,而是是基于图像简单的形状结构,例如边缘、形状等。基于这种思想,在复杂的深度神经网络中,有很多用于提取边缘形状的滤波器。
我们先考虑单个神经元节点的情况
H
=
f
(
∑
i
=
1
n
x
i
w
i
j
+
b
h
)
H=f({\sum_{i=1}^n}x_iw_{ij}+b_h)
H=f(i=1∑nxiwij+bh)
当用这个模型来处理图片时,对于一张1000*1000像素的图片,神经网络的隐含层节点也会很多,cs231n里举的例子是如果有1M个隐含层神经元,
1
M
∗
1000
∗
1000
=
1
0
12
1M*1000*1000=10^{12}
1M∗1000∗1000=1012,则一共需要
1
0
12
10^{12}
1012个参数。
局部感知野
卷积神经网络用局部感知野和权值共享的概念降低了神经元节点数,如下图所示
这样把图片的一个区域内的像素,通过矩阵运算和一个神经元节点相连,如果用10*10的局部感知单元与输入图片连接,一共1M个隐含层神经元, 1 M ∗ 10 ∗ 10 = 1 0 8 1M*10*10=10^8 1M∗10∗10=108,参数规模已经从 1 0 12 10^{12} 1012降到 1 0 8 10^8 108。
权值共享
权值连接是指每个神经元节点用一个权重矩阵 W W W来扫描整张图片,得到一张feature map(特征图)。通过添加多个神经元节点,可以学习到多个特征图,每张特征图可以看做原图的不同通道上的表达。所以他的计算方式和之前讲到的线性回归不一样。后面会给出代理样例。
卷积神经网络结构
讲完了一些思想后,让我们抛开上面讲的,来看CNN的结构。
卷积神经网络是一种多层次的网络结构,每一层网络结构都包含许多个相互独立的神经元节点。一个卷积神经网络一般是由一个输入层(Input)、若干卷基层(Convolutions)和池化层(Subsampling)、输出层(Output)组成,其中卷基层用于特征提取,池化层用于图像降维。
输入一张图片,然后经过C1层进行特征提取,S2层图片降维,图片大小降为C1时的一半。再次经过C3层特征提取,S4降维。最终将提取到的所有特征连接成一个一维向量输入到最后的全连接层,经过分类器得到最终的输出。
以LeNet-5为例,一共有7层,输入端像素为32*32。下面是经典的这张图。。
LeNet-5模型一共有7层。有三个卷基层:C1,C3,C5。
C1层有6个55大小的卷积核,可以提取到6个2828大小的特征图(feather map)。这一层可训练的参数为156个,每个滤波器为
5
∗
5
5*5
5∗5个参数,加上一个bias参数,有6个滤波器。有156个参数。一共有1562828=122304个连接。
C3层包括1500权重和16个偏置,有1516个训练参数和151600个连接。
S2层和C3层之间不是全连接Lecun设计了如上图所示的连接方式,不仅减少了权重数量,而且还相互组合由C3提取特征,破坏了网络的对称性。
C5层包含120个feather map,尺寸为
1
∗
1
1*1
1∗1。
LeNet-5模型还包括两个下采样层S2,S4。S2包括6个特征图,特征图的每个单元与上一层
2
∗
2
2*2
2∗2大小的区域连接。S2层每个单元4个参数相加,乘以一个可训练参数加一个可训练偏置作为输出,所以一共有
12
(
6
∗
(
1
+
1
)
=
12
)
12(6*(1+1)=12)
12(6∗(1+1)=12)个可训练参数。每个特征图都与C1中2*2的区域和1个偏置相连接,所以一共有
5880
(
6
∗
5
∗
14
∗
14
=
5880
)
5880(6*5*14*14=5880)
5880(6∗5∗14∗14=5880)个连接。同理,S4有32个可训练参数和156000个连接。
卷积层是将输出的图像与一个可训练的权重矩阵进行点乘,并将结果相加,再加一个偏置作为输出的运算。
下采样层也叫池化层,是将图片缩小规模,以降低网络的计算量,子采样层的一般公式如下:
x
j
l
=
β
j
l
d
o
w
n
(
x
j
l
−
1
)
+
b
j
l
x_j^l=\beta_j^ldown(x_j^{l-1})+b_j^l
xjl=βjldown(xjl−1)+bjl
down()函数是采样方式,有平均池化(就是取被池化区域像素的平均值),最大采样(就是取被池化区域像素的最大值)
全连接层是将所有提取到的特征图经过卷积操作得到一个一维向量,后面接一个【人工智能学习】【二】Softmax与分类模型来实现分类。
卷积神经网络的训练
先来看单节点的反向传播的计算,有助于理解反向传播的思想。
上面的图“激活函数”位置有点偏。。应该放到中间圆圈下面。这个图可以用下面三个公式来表示:
z
=
∑
i
=
1
n
X
W
+
b
z={\sum_{i=1}^nXW+b}
z=i=1∑nXW+b
a
=
f
(
z
)
a=f(z)
a=f(z)
J
=
l
o
s
s
(
z
)
J=loss(z)
J=loss(z)
设样本集
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
…
…
(
x
n
,
y
n
)
}
(
说
明
:
x
和
y
都
是
向
量
)
{\{(x_1,y_1),(x_2,y_2)……(x_n,y_n)\}} (说明:x和y都是向量)
{(x1,y1),(x2,y2)……(xn,yn)}(说明:x和y都是向量)
以二次损失函数为例:
l
o
s
s
=
1
2
(
y
i
ˊ
−
y
i
)
2
loss = \frac{1}{2} (\acute{y^i}-y^i)^2
loss=21(yiˊ−yi)2
其中
y
i
ˊ
\acute{y^i}
yiˊ为预测值,
y
i
y^i
yi为训练集样本中的真实值。
为了更新权值矩阵
W
W
W和偏置
b
b
b,需要求代价函数
J
J
J对于权值矩阵和偏置的偏导
∂
J
∂
w
\frac{∂J}{∂w}
∂w∂J与
∂
J
∂
b
\frac{∂J}{∂b}
∂b∂J。根据链式法则得
∂
J
∂
w
=
∂
J
∂
a
∂
a
∂
z
∂
z
∂
w
\frac{∂J}{∂w}=\frac{∂J}{∂a}\frac{∂a}{∂z}\frac{∂z}{∂w}
∂w∂J=∂a∂J∂z∂a∂w∂z
∂
J
∂
b
=
∂
J
∂
a
∂
a
∂
z
∂
z
∂
b
\frac{∂J}{∂b}=\frac{∂J}{∂a}\frac{∂a}{∂z}\frac{∂z}{∂b}
∂b∂J=∂a∂J∂z∂a∂b∂z
接着到CNN,考虑CNN有池化层局部感知,反向传播从后往前计算,是需要将每一层的feature map向上采样(尺寸要统一嘛)
上采样有mean-pooling和max-pooling,以mean-pooling为例,为了保证敏感度的总和不变,需要均摊到每一项上:
池化层搞定后,别忘了卷积层也有尺寸变化,反向传播计算时这个计算叫反卷积(Backwards Convolution),将残差进行映射。首先将残差矩阵padding一下(就是为了统一尺寸)。
上面试
w
w
w矩阵的反向传播,偏置b就直接链式法则计算了。
多通道
多通道输入
以上处理的实际上是黑白图片(只有一个通道),一般图片具有3个通道:RGB。这里先从感性上认识,输入三通道,在第一层卷积时,就必然要处理3个通道的像素,就需要3倍于原来的feature map。这时候如果不想要多通道输出,就直接做加和了。如果输入是
c
c
c维,卷积核尺寸为
k
h
×
k
w
k_{h}×k_{w}
kh×kw,为每一个通道分配一个
k
h
×
k
w
k_{h}×k_{w}
kh×kw的卷积核,卷积核就变成了
k
h
×
k
w
×
c
k_{h}×k_{w}×c
kh×kw×c大小了。
该图是两通道的例子:
多通道输出
在上面的例子上,最后的输出不做加和。(我觉得这里理解的不是很好)
为啥说感性上的认识呢,因为计算时,输入的变量是将三通道拉直成一维的
x
x
x进行计算的。所以里面还是有很多计算上的细节。
实现
手写实现
构造一个简单的边缘检测的例子。这个例子的数据和标签初始化如下
Y = torch.zeros(6, 7)
X[:, 2: 6] = 0
Y[:, 1] = 1
Y[:, 5] = -1
print(X)
print(Y)
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])
tensor([[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]])
定义二维互相关运算,一般使用 2 ∗ 2 2*2 2∗2的卷积核
import torch
import torch.nn as nn
def corr2d(X, K):
H, W = X.shape
h, w = K.shape
Y = torch.zeros(H - h + 1, W - w + 1)
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
return Y
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
Y = corr2d(X, K)
print(Y)
tensor([[19., 25.],
[37., 43.]])
二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
# 随机初始化
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, x):
# 卷积+偏置
return corr2d(x, self.weight) + self.bias
训练
conv2d = Conv2D(kernel_size=(1, 2))
step = 30
lr = 0.01
for i in range(step):
Y_hat = conv2d(X)
l = ((Y_hat - Y) ** 2).sum()
l.backward()
# 梯度下降
conv2d.weight.data -= lr * conv2d.weight.grad
conv2d.bias.data -= lr * conv2d.bias.grad
# 梯度清零
conv2d.weight.grad.zero_()
conv2d.bias.grad.zero_()
if (i + 1) % 5 == 0:
print('Step %d, loss %.3f' % (i + 1, l.item()))
print(conv2d.weight.data)
print(conv2d.bias.data)
Step 5, loss 4.569
Step 10, loss 0.949
Step 15, loss 0.228
Step 20, loss 0.060
Step 25, loss 0.016
Step 30, loss 0.004
tensor([[ 1.0161, -1.0177]])
tensor([0.0009])
pytorch实现
卷积层的简洁实现
我们使用Pytorch中的nn.Conv2d类来实现二维卷积层,主要关注以下几个构造函数参数:
in_channels (python:int) – Number of channels in the input imag
out_channels (python:int) – Number of channels produced by the convolution
kernel_size (python:int or tuple) – Size of the convolving kernel
stride (python:int or tuple, optional) – Stride of the convolution. Default: 1
padding (python:int or tuple, optional) – Zero-padding added to both sides of the input. Default: 0
bias (bool, optional) – If True, adds a learnable bias to the output. Default: True
X = torch.rand(4, 2, 3, 5)
print(X.shape)
conv2d = nn.Conv2d(in_channels=2, out_channels=3, kernel_size=(3, 5), stride=1, padding=(1, 2))
Y = conv2d(X)
print('Y.shape: ', Y.shape)
print('weight.shape: ', conv2d.weight.shape)
print('bias.shape: ', conv2d.bias.shape)
torch.Size([4, 2, 3, 5])
Y.shape: torch.Size([4, 3, 3, 5])
weight.shape: torch.Size([3, 2, 3, 5])
bias.shape: torch.Size([3])
池化层的简洁实现
我们使用Pytorch中的nn.MaxPool2d实现最大池化层,关注以下构造函数参数:
kernel_size – the size of the window to take a max over
stride – the stride of the window. Default value is kernel_size
padding – implicit zero padding to be added on both sides
X = torch.arange(32, dtype=torch.float32).view(1, 2, 4, 4)
pool2d = nn.MaxPool2d(kernel_size=3, padding=1, stride=(2, 1))
Y = pool2d(X)
print(X)
print(Y)
tensor([[[[ 0., 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.]]]])
tensor([[[[ 5., 6., 7., 7.],
[13., 14., 15., 15.]],
[[21., 22., 23., 23.],
[29., 30., 31., 31.]]]])