前言
本节学习卷积神经网络的基础知识
- 卷积层
- 填充和步幅
- 多输入和多输出通道
- 池化层
- 卷积神经网络
1、卷积层
互相关运算
卷积层来源于卷积运算
但通常会用互相关运算
如图所示,0 * 0 + 1 * 1 + 3 * 2 + 4 * 3 = 19
卷积窗口从输⼊数组的最左上⽅开始
按从左往右、从上往下的顺序,依次在输⼊数组上滑动
实现如下
from mxnet import autograd, nd
from mxnet.gluon import nn
def corr2d(X, K): # 本函数已在d2lzh库中有
h, w = K.shape
Y = nd.zeros((X.shape[0] - h + 1, X.shape[1] - 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
二维卷积层
- 卷积层的模型参数包括了卷积核和标量偏差
- 在训练模型的时候,通常先对卷积核随机初始化,然后不断迭代卷积核和偏差
class Conv2D(nn.Block):
def __init__(self, kernel_size, **kwargs):
super(Conv2D, self).__init__(**kwargs)
self.weight = self.params.get('weight', shape=kernel_size)
self.bias = self.params.get('bias', shape=(1,))
def forward(self, x):
return corr2d(x, self.weight.data()) + self.bias.data()
例子
使⽤物体边缘检测中的输⼊数据X和输出数据Y来学习我们构造的核数组K
- ⾸先构造⼀个卷积层,将其卷积核初始化成随机数组
- 接下来在每⼀次迭代中,⽤平⽅误差来⽐较Y和卷积层的输出
- 然后计算梯度来更新权重
conv2d = nn.Conv2D(1, kernel_size=(1, 2))
conv2d.initialize()
X = X.reshape((1, 1, 6, 8)) #(样本, 通道, ⾼, 宽)
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
with autograd.record():
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
l.backward()
# 简单起见,这里忽略了偏差
conv2d.weight.data()[:] -= 3e-2 * conv2d.weight.grad()
if (i + 1) % 2 == 0:
print('batch %d, loss %.3f' % (i + 1, l.sum().asscalar()))
print(conv2d.weight.data().reshape((1, 2))) #查看学习到的核函数
2、填充和步幅
假设输⼊形状是mn,卷积核窗口形状是qp
那么输出形状将会是(m-q+1) * (n-p+1)
填充
在输⼊⾼和宽的两侧填充元素(通常是0元素)
例子
- 创建⼀个⾼和宽为3的⼆维卷积层
- 设输⼊⾼和宽两侧的填充数分别为1
- 给定⼀个⾼和宽为8的输⼊
from mxnet import nd
from mxnet.gluon import nn
# 定义⼀个函数来计算卷积层。它初始化卷积层权重,并对输⼊和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
conv2d.initialize()
# (1, 1)代表批量大小和通道数均为1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
return Y.reshape(Y.shape[2:]) # 排除不关心的前两维:批量和通道
# 注意这里是两侧分别填充1行或列,所以在两侧一共填充2行或列
conv2d = nn.Conv2D(1, kernel_size=3, padding=1)
X = nd.random.uniform(shape=(8, 8))
print(comp_conv2d(conv2d, X).shape)
# 使⽤⾼为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1
conv2d = nn.Conv2D(1, kernel_size=(5, 3), padding=(2, 1))
print(comp_conv2d(conv2d, X).shape)
步幅
每次滑动的⾏数和列数
如图所示,在⾼上步幅为3、在宽上步幅为2的⼆维互相关运算
# 令⾼和宽上的步幅均为2,从而使输⼊的⾼和宽减半
conv2d = nn.Conv2D(1, kernel_size=3, padding=1, strides=2)
print(comp_conv2d(conv2d, X).shape)
# 复杂点的例子
conv2d = nn.Conv2D(1, kernel_size=(3, 5), padding=(0, 1), strides=(3, 4))
print(comp_conv2d(conv2d, X).shape)
3、多输入通道和多输出通道
多输入通道
主要是数据的维度
一个2维通道的如下
- 只需要对每个通道做互相关运算
- 然后通过add_n函数来进⾏累加
import d2lzh as d2l
from mxnet import nd
def corr2d_multi_in(X, K):
# ⾸先沿着X和K的第0维(通道维)遍历。然后使⽤*将结果列表变成add_n函数的位置参数
# (positional argument)来进⾏相加
return nd.add_n(*[d2l.corr2d(x, k) for x, k in zip(X, K)])
多输出通道
每个输出通道上的结果由卷积核在该输出通道上的核数组与整个输⼊数组计算而来
def corr2d_multi_in_out(X, K):
# 对K的第0维遍历,每次同输⼊X做互相关计算。所有结果使⽤stack函数合并在⼀起
return nd.stack(*[corr2d_multi_in(X, k) for k in K])
1*1卷积层
- 通常当作保持⾼和宽维度形状不变的全连接层使⽤
- 可以通过调整⽹络层之间的通道数来控制模型复杂度
如图所示,输⼊通道数为3、输出通道数为2的1*1卷积核的互相关计算
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
Y = nd.dot(K, X) # 全连接层的矩阵乘法
return Y.reshape((c_o, h, w))
4、池化层
- 缓解卷积层对位置的过度敏感性
- 直接计算池化窗口内元素的最⼤值或者平均值
from mxnet import nd
from mxnet.gluon import nn
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = nd.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
步幅、填充、多通道都与卷积层类似
5、卷积神经网络(LeNet)
这里学习下早年Yann LeCun提出的奠基性的卷积神经网络
卷积层块
- 基本单位:卷积层⽤来识别图像⾥的空间模式,接上最⼤池化层⽤来降低卷积层对位置的敏感性
- 卷积层都使⽤5*5的窗口,并在输出上使⽤sigmoid激活函数
- 第⼀个卷积层输出通道数为6,第⼆个卷积层输出通道数为16
- 两个最⼤池化层的窗口形状均为2*2,且步幅为2
全连接层块
- 将小批量中每个样本变平(flatten)
- 3个全连接层,输出个数分别是120、84和10
- 输⼊形状将变成⼆维,其中第⼀维是小批量中的样本,第⼆维是每个样本变平后的向量表⽰,且向量⻓度为通道、⾼和宽的乘积
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn
"""早年的卷积神经网络lenet模型"""
# lenet
net = nn.Sequential()
net.add(nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'),
nn.MaxPool2D(pool_size=2, strides=2),
# Dense会默认将(批量大小, 通道, 高, 宽)形状的输入转换成
# (批量大小, 通道 * 高 * 宽)形状的输入
nn.Dense(120, activation='sigmoid'),
nn.Dense(84, activation='sigmoid'),
nn.Dense(10))
# 构造⼀个⾼和宽均为28的单通道数据样本,并逐层进⾏前向计算查看输出形状
X = nd.random.uniform(shape=(1, 1, 28, 28))
net.initialize()
for layer in net:
X = layer(X)
print(layer.name, 'output shape:\t', X.shape)
结语
对卷积神经网络的基础知识进行了学习
后面将学习几种典型的深度卷积神经网络