一、LeNet-5介绍
LeNet-5用于手写数字和机器打印字符图片识别的神经网络。
LeNet-5 的网络结构图,它接受32 × 32大小的数字、字符图片,经过第一个卷积层得到[b, 28,28,6] 形状的张量,经过一个向下采样层,张量尺寸缩小到[b,14,14,6] ,经过第二个卷积层,得到[b,10,10,16] 形状的张量,同样经过下采样层,张量尺寸缩小到[b,5,5,16]],在进入全连接层之前,先将张量打成[b,400]的张量,送入输出节点数分别为 120、 84 的 2 个全连接层,得到 [b,84] 的张量,最后通过 Gaussian connections 层
基于 MNIST 手写数字图片数据集训练 LeNet-5 网络,并测试其最终准确度。将输入?形状由32 × 32调整为28 × 28,然后将 2 个下采样层实现为最大池化层,Gaussian connections 层替换为输出维度为10的全连接层。
二、LeNet-5实现
1.数据集加载以及数据集的预处理
# 预处理
def preprocess(x, y):# x :[-1,1]x = 2 * tf.cast(x, dtype=tf.float32) / 255 - 1y = tf.cast(y, dtype=tf.int32)return x, y# 加载数据集
(x, y), (x_text, y_text) = datasets.mnist.load_data()# 创建batch
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.map(preprocess).shuffle(1000).batch(128)test_db = tf.data.Dataset.from_tensor_slices((x, y))
test_db = test_db.map(preprocess).batch(128)# 获取下一个batch
sample = next(iter(test_db))
mnist数据集从网上下载,并将它分为训练集核测试集。得到的训练集数据类型为Numpy,需要将他们转换为张量类型。为了防止出现梯度弥撒,将x从[0,255]标准化到[-1,1]之间。
tf.data.Dataset.from_tensor_slices的作用将输入的张量或者Numpy类型的第一个维度看做样本的个数,沿前第一个维度将tensor(Numpy格式会自动转为tensor)切片,实现了输入张量的自动化切片,这里x的形状[60k, 28,28], 所以会切成60k张图片。然后将整个数据集分为128小样本的批处理,每一次循环处理一个batch
2.网络模型构建
网络结构如下:
使用Sequential容器,生成Sequential类的一个实例
# 建立网络
network = Sequential([layers.Conv2D(6, kernel_size=3, strides=1), # 第一个卷积层, 6个3x3卷积核layers.MaxPool2D(pool_size=2, strides=2), # 高宽各减半的池化层layers.ReLU(),layers.Conv2D(16, kernel_size=3, strides=1), # 第一个卷积层, 6个3x3卷积核layers.MaxPool2D(pool_size=2, strides=2), # 高宽各减半的池化层layers.ReLU(),layers.Flatten(), # 打平层,方便全连接层处理layers.Dense(120, activation='relu'),layers.Dense(60, activation='relu'),layers.Dense(10)
])
# build 一次网络模型,给输入 X 的形状,其中 4 为随意给的 batchsz
network.build(input_shape=(4, 28, 28, 1))
# 统计网络信息
network.summary()
在卷积层与全连接层不能之间相连,全连接层需要传入的数据必须是一维的,所以需要使用Flatten将多维数据打平成一维数据。成员函数build, summary完成网络权值,偏置和输入维度的初始化与网络模型参数状况的输出
3.网络装配
在训练网络时,一般的流程是通过前向计算获得网络的输出值, 再通过损失函数计算网络误差,然后通过自动求导工具计算梯度并更新,同时间隔性地测试网络的性能。
所以,在完成网络模型的搭建后,需要指定网络使用的优化器对象、 损失函数类型, 评价指标等设定,这一步称为装配
# 创建损失函数类
criteon = losses.CategoricalCrossentropy(from_logits=True)# w = w - lr * grad
# 学习率的设置,更新参数
optimizers = optimizers.Adam(learning_rate=1e-3)
优化器主要使用apply_gradients方法传入变量和对应梯度从而来对给定变量进行迭代
4.计算梯度,代价函数并更新参数
with tf.GradientTape() as tape:# 插入通道维度 =>[b,28,28,1]x = tf.expand_dims(x, axis=3)# 向前计算,获得10类的概率分布,[b,784]-> [b,10]out = network(x)# 真实标签 one-hot 编码, [b] => [b, 10]y_onehot = tf.one_hot(y, depth=10)# 计算交叉熵损失函数,标量loss = criteon(y_onehot, out)# 自动计算梯度grads = tape.gradient(loss, network.trainable_variables)# 自动跟新参数optimizers.apply_gradients(zip(grads, network.trainable_variables))
在使用自动求导功能计算梯度,需要将向前计算过程放置在tf.GradientTape()环境中, 利用GradientTape对象的gradient()方法自动求解参数的梯度, 并利用optimizers对象更新参数
由于卷积层需要输出数据的通道数量,卷积核的深度必须和输入图像的深度一样,因此需要增加第3个维度
5.测试
# 测试集,记录预测正确的数量,总样本数量total_correct, total = 0, 0for x, y in test_db:# 插入通道维数x = tf.expand_dims(x, axis=3)# 前向计算,获得 10 类别的预测分布, [b, 784] => [b, 10]out = network(x)# 将输出结果归一化处理,得到和为1的概率prob = tf.nn.softmax(out, axis=1) # [0,1]# 找到对应维度最大值的索引位置pred = tf.argmax(prob, axis=1)pred = tf.cast(pred, dtype=tf.int32)# pred:[b]# y: [b]# correct: [b], True(1): equal; False(0): not equalcorrect = tf.equal(pred, y)correct = tf.reduce_sum(tf.cast(correct, dtype=tf.int32))# 预测对的数量total_correct += int(correct)# 统计预测样本总数total += x.shape[0]# accprint('acc:', total_correct / total)
在测试阶段,由于不需要记录梯度信息,代码一般不需要写在 with tf.GradientTape() as tape 环境中。前向计算得到的输出经过 softmax 函数后,代表了网络预测当前图片输入?属于类别?的概率?(?标签是?|?), ? ∈ 9 。通过 argmax 函数选取概率最大的元素所在的索引,作为当前?的预测类别,与真实标注?比较,通过计算比较结果中间 True 的数量并求和来统计预测正确的样本的个数,最后除以总样本的个数,得出网络的测试准确度
网络训练了2个epoch的准确率可以达到0.98,多训练几次可能还会高一些。
三、完整程序
# -*- codeing = utf-8 -*-
# @Time : 11:07
# @Author:Paranipd
# @File : LeNet-5-test.py
# @Software:PyCharmimport os
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets, layers, optimizers, Sequential, losses # 数据集, 网络层, 分类器, 容器os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # 去掉不必要的报错# 预处理
def preprocess(x, y):# x :[-1,1]x = 2 * tf.cast(x, dtype=tf.float32) / 255 - 1y = tf.cast(y, dtype=tf.int32)return x, y# 加载数据集
(x, y), (x_text, y_text) = datasets.mnist.load_data()# 创建batch
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.map(preprocess).shuffle(1000).batch(128)test_db = tf.data.Dataset.from_tensor_slices((x, y))
test_db = test_db.map(preprocess).batch(128)# 获取下一个batch
sample = next(iter(test_db))# 建立网络
network = Sequential([layers.Conv2D(6, kernel_size=3, strides=1), # 第一个卷积层, 6个3x3卷积核layers.MaxPool2D(pool_size=2, strides=2), # 高宽各减半的池化层layers.ReLU(),layers.Conv2D(16, kernel_size=3, strides=1), # 第一个卷积层, 6个3x3卷积核layers.MaxPool2D(pool_size=2, strides=2), # 高宽各减半的池化层layers.ReLU(),layers.Flatten(), # 打平层,方便全连接层处理layers.Dense(120, activation='relu'),layers.Dense(60, activation='relu'),layers.Dense(10)
])
# build 一次网络模型,给输入 X 的形状,其中 4 为随意给的 batchsz
network.build(input_shape=(4, 28, 28, 1))
# 统计网络信息
network.summary()# 创建损失函数类
criteon = losses.CategoricalCrossentropy(from_logits=True)# w = w - lr * grad
# 学习率的设置,更新参数
optimizers = optimizers.Adam(learning_rate=1e-3)def main():# Step4.loopfor epoch in range(50):for step, (x, y) in enumerate(train_db):with tf.GradientTape() as tape:# 插入通道维度 =>[b,28,28,1]x = tf.expand_dims(x, axis=3)# 向前计算,获得10类的概率分布,[b,784]-> [b,10]out = network(x)# 真实标签 one-hot 编码, [b] => [b, 10]y_onehot = tf.one_hot(y, depth=10)# 计算交叉熵损失函数,标量loss = criteon(y_onehot, out)# 自动计算梯度grads = tape.gradient(loss, network.trainable_variables)# 自动跟新参数optimizers.apply_gradients(zip(grads, network.trainable_variables))if step % 100 == 0:print(epoch, step, 'loss', float(loss))# 测试集,记录预测正确的数量,总样本数量total_correct, total = 0, 0for x, y in test_db:# 插入通道维数x = tf.expand_dims(x, axis=3)# 前向计算,获得 10 类别的预测分布, [b, 784] => [b, 10]out = network(x)# 将输出结果归一化处理,得到和为1的概率prob = tf.nn.softmax(out, axis=1) # [0,1]# 找到对应维度最大值的索引位置pred = tf.argmax(prob, axis=1)pred = tf.cast(pred, dtype=tf.int32)# pred:[b]# y: [b]# correct: [b], True(1): equal; False(0): not equalcorrect = tf.equal(pred, y)correct = tf.reduce_sum(tf.cast(correct, dtype=tf.int32))# 预测对的数量total_correct += int(correct)# 统计预测样本总数total += x.shape[0]# accprint('acc:', total_correct / total)if __name__ == "__main__":main()