卷积层的优势:一方面,卷积层保留输入形状,使图像的像素在高和宽两个方向上的相关性均可能被有效识别;另一方面,卷积层通过滑动窗口将同一卷积核与不同位置的输入重复计算,从而避免参数尺寸过大。
(一)LeNet模型
LeNet分为卷积层和全连接层两部分。
卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。
卷积层块的输出形状为(批量大小, 通道, 高, 宽)。当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平(flatten)。
LeNet架构由两个系列的CONV => TANH => POOL层集组成,后跟一个完全连接的层和softmax输出。
这个网络架构已经成为标准的“模板”:堆栈式卷积和池化层,以一个或多个全连接层作为网络的末端。
input:批量大小为1,一个通道,大小为28×28的灰度图像
Conv2D:第一层是一个卷积层,输出通道数为6,卷积核大小为(5,5)
MaxPoolD:第二层是最大池化层,池化层的大小为(2,2),步幅为(2,2)
Conv2D:第三层也是一个卷积层,输出通道数为16(这是因为输入尺寸变小了,所以需要增通道数来使得两个卷积层的参数尺寸类似),卷积核大小为(5,5)
MaxPoolD:第四层是最大池化层,池化层的大小为(2,2),步幅为(2,2)
Dense:全连接层把四维变成二维,第一维是批量大小,第二维是每个样本变平后的向量表示,具体转化过程可见多维输入输出 (三)1×1卷积层
采用的是激活函数是过去比较常用的sigmoid。
import d2lzh as d2l
import mxnet as mx
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn
import time
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))
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)
我们可以分析验证一下模型是否是正确的:参考‘’填充和步幅‘’一节的计算方法
input:1×28×28
kernel_size=:6×1×5×5
conv1_output:6×(28-5+1)×(28-5+1)
pool1_output:6×((24-2+2)/2)×((24-2+2)/2)
kernel_size=:16×6×5×5
conv1_output:16×(12-5+1)×(12-5+1)
pool1_output:16×((8-2+2)/2)×((8-2+2)/2)
dense0:(1,120)
dense1:(1,84)
dense2:(1,10)
计算结果和输出结果一致。
可以看到,在卷积层块中输入的高和宽在逐层减小。卷积层由于使用高和宽均为5的卷积核,从而将高和宽分别减小4,而池化层则将高和宽减半,但通道数则从1增加到16。全连接层则逐层减少输出个数,直到变成图像的类别数10。
(二)获取数据和训练模型
因为卷积神经网络计算比多层感知机要复杂,建议使用GPU来加速计算。
首先在d2lzh中定义了函数try_gpu()来使用GPU加速。
def try_gpu(): # 本函数已保存在d2lzh包中方便以后使用
try:
ctx = mx.gpu() # 尝试在gpu(0)中创建NDArray,成功就使用gpu(0)
_ = nd.zeros((1,), ctx=ctx)
except mx.base.MXNetError:
ctx = mx.cpu() # 不成功仍然使用cpu(0)
return ctx
ctx = try_gpu()
ctx
然后定义评价准确率的函数evaluate_accuracy():
部分参数的意义可见softmax回归的从零开始
def evaluate_accuracy(data_iter, net, ctx):
acc_sum, n = nd.array([0], ctx=ctx), 0
for X, y in data_iter:
# 如果ctx代表GPU及相应的显存,则利用函数as_in_context()将数据复制到显存上
X, y = X.as_in_context(ctx), y.as_in_context(ctx).astype('float32')
acc_sum += (net(X).argmax(axis=1) == y).sum() # 所有计算准确的数据
n += y.size # 参与运算的总数据
return acc_sum.asscalar() / n # 算出比例
定义函数train_ch5(),但是本次实现用的是简洁实现,利用了Gluon。
def train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx,num_epochs):
print('training on', ctx) # 在ctx上进行训练
loss = gloss.SoftmaxCrossEntropyLoss()
#利用Gluon提供的包括softmax运算和交叉熵损失计算的函数,它的数值稳定性更好。
for epoch in range(num_epochs): # 在每一次运算周期内实现下列操作:
train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time() # 初始化
for X, y in train_iter:
X, y = X.as_in_context(ctx), y.as_in_context(ctx) # 利用Gpu运算,将数据复制到显卡中
with autograd.record():
y_hat = net(X) # 利用net对X进行softmax回归运算得到y_hat
l = loss(y_hat, y).sum() # 计算损失函数,求和成标量
l.backward() # 对损失求梯度,以训练参数
trainer.step(batch_size)
# step() 进行一步参数更新。应该在autograd.backward()之后调用,并且在record()范围之外。与sgd功能类似。
y = y.astype('float32')
train_l_sum += l.asscalar() # 计算损失总数
train_acc_sum += (y_hat.argmax(axis=1) == y).sum().asscalar() # 计算训练中准确的数据
n += y.size #总数
test_acc = evaluate_accuracy(test_iter, net, ctx) #测试
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
'time %.1f sec'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc,
time.time() - start))
lr, num_epochs = 0.9, 5 # 手动设定学习率和学习周期
net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier())
#Xavier()返回一个初始化器,执行权重的“xavier”初始化。这个初始值设定项被设计成在所有层中保持梯度的比例大致相同。
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, num_epochs)
对比一下CPU学习和GPU学习的速度:
CPU:
GPU:
GPU确实提速很多的。