正文之前
本文纯粹是为了记笔记而生,同时也算是对官方文档的代码的验证,如果有朋友遇到问题可以来找我讨论。内容是官方文档的精简版。另外,还有一个大佬写的关于paddle的教程:
《PaddlePaddle从入门到炼丹》十一——自定义图像数据集识别
正文
当我们学习编程的时候,编写的第一个程序一般是实现打印"Hello World"。而机器学习(或深度学习)的入门教程,一般都是 MNIST 数据库上的手写识别问题。原因是手写识别属于典型的图像分类问题,比较简单,同时MNIST数据集也很完备。MNIST数据集作为一个简单的计算机视觉数据集,包含一系列如图1所示的手写数字图片和对应的标签。图片是28x28的像素矩阵,标签则对应着0~9的10个数字。每张图片都经过了大小归一化和居中处理。
本教程中,我们从简单的Softmax回归模型开始,带大家了解手写字符识别,并向大家介绍如何改进模型,利用多层感知机(MLP)和卷积神经网络(CNN)优化识别效果。
数据定义:
X是输入:MNIST图片是28×28 的二维图像,为了进行计算,我们将其转化为784维向量,即X=(x0,x1,…,x783)。
Y是输出:分类器的输出是10类数字(0-9),即Y=(y0,y1,…,y9),每一维yi代表图片分类为第ii类数字的概率。
Label 是图片的真实标签:Label=(l0,l1,…,l9)也是10维,但只有一维为1,其他都为0。例如某张图片上的数字为2,则它的标签为(0,0,1,0,…,0)
一、最简单的Softmax回归模型是先将输入层经过一个全连接层得到特征,然后直接通过 softmax 函数计算多个类别的概率并输出[9]。
图为softmax回归的网络图,图中权重用蓝线表示、偏置用红线表示、+1代表偏置参数的系数为1。
在分类问题中,我们一般采用交叉熵代价损失函数(cross entropy loss),公式如下:
二、多层感知机(Multilayer Perceptron, MLP)¶
Softmax回归模型采用了最简单的两层神经网络,即只有输入层和输出层,因此其拟合能力有限。为了达到更好的识别效果,我们考虑在输入层和输出层中间加上若干个隐藏层[10]。
经过第一个隐藏层,可以得到 H1=ϕ(W1X+b1),其中ϕϕ代表激活函数,常见的有sigmoid、tanh或ReLU等函数。
经过第二个隐藏层,可以得到 H2=ϕ(W2H1+b2)。
最后,再经过输出层,得到的Y=softmax(W3H2+b3),即为最后的分类结果向量。
图3为多层感知器的网络结构图,图中权重用蓝线表示、偏置用红线表示、+1代表偏置参数的系数为1。
图3. 多层感知器网络结构图
三、卷积神经网络(Convolutional Neural Network, CNN)¶
在多层感知器模型中,将图像展开成一维向量输入到网络中,忽略了图像的位置和结构信息,而卷积神经网络能够更好的利用图像的结构信息。LeNet-5是一个较简单的卷积神经网络。图4显示了其结构:输入的二维图像,先经过两次卷积层到池化层,再经过全连接层,最后使用softmax分类作为输出层。下面我们主要介绍卷积层和池化层。
图4. LeNet-5卷积神经网络结构
卷积层¶
卷积层是卷积神经网络的核心基石。在图像识别里我们提到的卷积是二维卷积,即离散二维滤波器(也称作卷积核)与二维图像做卷积操作,简单的讲是二维滤波器滑动到二维图像上所有位置,并在每个位置上与该像素点及其领域像素点做内积。卷积操作被广泛应用与图像处理领域,不同卷积核可以提取不同的特征,例如边沿、线性、角等特征。在深层卷积神经网络中,通过卷积操作可以提取出图像低级到复杂的特征。
图5. 卷积层图片
图5给出一个卷积计算过程的示例图,输入图像大小为H=5,W=5,D=3,即5×5大小的3通道(RGB,也称作深度)彩色图像。
这个示例图中包含两(用KK表示)组卷积核,即图中FilterW0 和 FilterW1。在卷积计算中,通常对不同的输入通道采用不同的卷积核,如图示例中每组卷积核包含(D=3))个3×3(用F×F表示)大小的卷积核。另外,这个示例中卷积核在图像的水平方向(WW方向)和垂直方向(H方向)的滑动步长为2(用S表示);对输入图像周围各填充1(用P表示)个0,即图中输入层原始数据为蓝色部分,灰色部分是进行了大小为1的扩展,用0来进行扩展。经过卷积操作得到输出为3×3×2(用Ho×Wo×K表示)大小的特征图,即3×3大小的2通道特征图,其中Ho计算公式为:Ho=(H−F+2×P)/S+1,Wo同理。 而输出特征图中的每个像素,是每组滤波器与输入图像每个特征图的内积再求和,再加上偏置bo,偏置通常对于每个输出特征图是共享的。输出特征图o[:,:,0]中的最后一个−2计算如图5右下角公式所示。
局部连接:每个神经元仅与输入神经元的一块区域连接,这块局部区域称作感受野(receptive field)。在图像卷积操作中,即神经元在空间维度(spatial dimension,即上图示例H和W所在的平面)是局部连接,但在深度上是全部连接。对于二维图像本身而言,也是局部像素关联较强。这种局部连接保证了学习后的过滤器能够对于局部的输入特征有最强的响应。局部连接的思想,也是受启发于生物学里面的视觉系统结构,视觉皮层的神经元就是局部接受信息的。
权重共享:计算同一个深度切片的神经元时采用的滤波器是共享的。例如图5中计算o[:,:,0]o[:,:,0]的每个每个神经元的滤波器均相同,都为W0W0,这样可以很大程度上减少参数。共享权重在一定程度上讲是有意义的,例如图片的底层边缘特征与特征在图中的具体位置无关。但是在一些场景中是无意的,比如输入的图片是人脸,眼睛和头发位于不同的位置,希望在不同的位置学到不同的特征 (参考斯坦福大学公开课)。请注意权重只是对于同一深度切片的神经元是共享的,在卷积层,通常采用多组卷积核提取不同特征,即对应不同深度切片的特征,不同深度切片的神经元权重是不共享。另外,偏重对同一深度切片的所有神经元都是共享的。
池化层¶
图6. 池化层图片
池化是非线性下采样的一种形式,主要作用是通过减少网络的参数来减小计算量,并且能够在一定程度上控制过拟合。通常在卷积层的后面会加上一个池化层。池化包括最大池化、平均池化等。其中最大池化是用不重叠的矩形框将输入层分成不同的区域,对于每个矩形框的数取最大值作为输出层,如图6所示。
常见激活函数介绍¶
- sigmoid激活函数:
- tanh激活函数:
实际上,tanh函数只是规模变化的sigmoid函数,将sigmoid函数值放大2倍之后再向下平移1个单位:tanh(x) = 2sigmoid(2x) - 1 。
- ReLU激活函数: f(x)=max(0,x)
激活函数是用来加入非线性因素的,因为线性模型的表达能力不够。
在下面的代码示例中,我们将深入了解上述内容:
加载 PaddlePaddle 的 Fluid API 包。
from __future__ import print_function # 将python3中的print特性导入当前版本
import os
from PIL import Image # 导入图像处理模块
import matplotlib.pyplot as plt
import numpy
import paddle # 导入paddle模块
import paddle.fluid as fluid
Program Functions 配置
def softmax_regression():
"""
定义softmax分类器:
一个以softmax为激活函数的全连接层
Return:
predict_image -- 分类的结果
"""
# 输入的原始数据,28 * 28 * 1
img = fluid.layers.data(name='img', shape=[1,28,28], dtype='float32')
# 以softmax为激活函数的全连接层,输出层的大小必须为数字的个数10
predict = fluid.layers.fc(
input='img', size=10, act='softmax'
)
return predict
def multilayer_perceptron():
"""
定义多层感知机分类器:
含有两个隐藏层(全连接层)的多层感知器
其中前两个隐藏层的激活函数采用 ReLU,输出层的激活函数用 Softmax
Return:
predict_image -- 分类的结果
"""
# 输入的原始图像数据,大小为28*28*1
img = fluid.layers.data(name='img', shape=[1, 28, 28], dtype='float32')
# 第一个全连接层,激活函数为ReLU
hidden = fluid.layers.fc(input=img, size=200, act='relu')
# 第二个全连接层,激活函数为ReLU
hidden = fluid.layers.fc(input=hidden, size=200, act='relu')
# 以softmax为激活函数的全连接输出层,输出层的大小必须为数字的个数10
prediction = fluid.layers.fc(input=hidden, size=10, act='softmax')
return prediction
def convolutional_neural_network():
"""
定义卷积神经网络分类器:
输入的二维图像,经过两个卷积-池化层,使用以softmax为激活函数的全连接层作为输出层
Return:
predict -- 分类的结果
"""
# 输入的原始图像数据,大小为28*28*1
img = fluid.layers.data(name='img', shape=[1, 28, 28], dtype='float32')
# 第一个卷积-池化层
# 使用20个5*5的滤波器,池化大小为2,池化步长为2,激活函数为Relu
conv_pool_1 = fluid.nets.simple_img_conv_pool(
input=img,
filter_size=5,
num_filters=20,
pool_size=2,
pool_stride=2,
act="relu")
conv_pool_1 = fluid.layers.batch_norm(conv_pool_1)
# 第二个卷积-池化层
# 使用50个5*5的滤波器,池化大小为2,池化步长为2,激活函数为Relu
conv_pool_2 = fluid.nets.simple_img_conv_pool(
input=conv_pool_1,
filter_size=5,
num_filters=50,
pool_size=2,
pool_stride=2,
act="relu")
# 以softmax为激活函数的全连接输出层,输出层的大小必须为数字的个数10
prediction = fluid.layers.fc(input=conv_pool_2, size=10, act='softmax')
return prediction
Train Program 配置
然后我们需要设置训练程序 train_program。它首先从分类器中进行预测。 在训练期间,它将从预测中计算 avg_cost。
注意: 训练程序应该返回一个数组,第一个返回参数必须是 avg_cost。训练器使用它来计算梯度。
def train_program():
"""
配置train_program
:return:
predict -- 分类结果
avg-cost -- 平均损失
acc -- 分类的准确率
"""
# 标签层 label,对应输入图片的类别
label = fluid.layers.data(name='label', shape=[1], dtype='int64')
# predict = softmax_regression() # 取消注释将使用 Softmax回归
# Best pass is 4, testing Avgcost is 0.0630938182697093
# The classification accuracy is 97.91%
# predict = multilayer_perceptron() # 取消注释将使用 多层感知器
# Best pass is 4, testing Avgcost is 0.0609021570576521
# The classification accuracy is 97.99%
predict = convolutional_neural_network() # 取消注释将使用 LeNet5卷积神经网络
# Best pass is 4, testing Avgcost is 0.018020916773774882
# The classification accuracy is 99.31%
# 使用交叉熵函数计算predict和label之间的损失函数
cost = fluid.layers.cross_entropy(input = predict, label = label)
# 计算平均损失
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=predict, label=label)
return predict, [ avg_cost, acc]
Optimizer Function 配置
在下面的 Adam optimizer,learning_rate 是学习率,它的大小与网络的训练收敛速度有关系。
def optimizer_program():
return fluid.optimizer.Adam(learning_rate=0.001)
数据集 Feeders 配置
下一步,我们开始训练过程。paddle.dataset.mnist.train()和paddle.dataset.mnist.test()分别做训练和测试数据集。这两个函数各自返回一个reader——PaddlePaddle中的reader是一个Python函数,每次调用的时候返回一个Python yield generator。
下面shuffle是一个reader decorator,它接受一个reader A,返回另一个reader B。reader B 每次读入buffer_size条训练数据到一个buffer里,然后随机打乱其顺序,并且逐条输出。
batch是一个特殊的decorator,它的输入是一个reader,输出是一个batched reader。在PaddlePaddle里,一个reader每次yield一条训练数据,而一个batched reader每次yield一个minibatch。
# 一个minibatch中有64个数据
BATCH_SIZE = 64
# 每次读取训练集中的500个数据并随机打乱,传入batched reader中,batched reader 每次 yield 64个数据
train_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.mnist.train(), buf_size=500
),
batch_size=BATCH_SIZE
)
# 读取测试集的数据,每次 yield 64个数据
test_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.mnist.test(), buf_size=500
),
batch_size=BATCH_SIZE
)
Event Handler 配置
我们可以在训练期间通过调用一个handler函数来监控训练进度。 我们将在这里演示两个 event_handler 程序。请随意修改 Jupyter Notebook ,看看有什么不同。
event_handler 用来在训练过程中输出训练结果
def event_handler(pass_id, batch_id, cost):
# 打印训练的中间结果,训练轮次,batch数,损失函数
print("Pass %d, Batch %d, Cost %f" % (pass_id,batch_id, cost))
from paddle.utils.plot import Ploter
train_prompt = "Train cost"
test_prompt = "Test cost"
cost_ploter = Ploter(train_prompt, test_prompt)
# 将训练过程绘图表示
def event_handler_plot(ploter_title, step, cost):
cost_ploter.append(ploter_title, step, cost)
cost_ploter.plot()
开始训练
- 可以加入我们设置的 event_handler 和 data reader,然后就可以开始训练模型了。
- 设置一些运行需要的参数,配置数据描述 feed_order 用于将数据目录映射到 train_program
- 创建一个反馈训练过程中误差的train_test
# 该模型运行在单个CPU上
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
# 调用train_program 获取预测值和损失值
prediction, [avg_loss, acc] = train_program()
# 输入的原始图像名称,img 28*28*1
# 标签层,名称为label,对应输入图片的类别标签
# 告知网络传入的数据分为两部分,第一部分是img值,第二部分是label值
feeder = fluid.DataFeeder(feed_list=['img', 'label'], place = place)
# 选择Adam优化器
optimizer = optimizer_program()
optimizer.minimize(avg_loss)
# 训练五轮
EPOCH_NUM = 5
epochs = [epoch_id for epoch_id in range(EPOCH_NUM)]
# 将模型参数保存在save_dirname中
save_dirname = "recognize_digits.inference.model"
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
main_program = fluid.default_main_program()
test_program = main_program.clone(for_test=True)
def tarin_test(train_test_program,
train_test_feed, train_test_reader):
# 将分类准确率存储在acc_set中
acc_set = []
# 将平均损失存储在avg_loss_set中
avg_loss_set = []
# 将测试 reader yield 出的每一个数据传入网络中进行训练
for test_data in train_test_reader():
acc_np, avg_loss_np = exe.run(
program = train_test_program,
feed = train_test_feed.feed(test_data),
fetch_list=[acc, avg_loss]
)
acc_set.append(float(acc_np))
avg_loss_set.append(float(float(avg_loss_np)))
# 获得测试数据上的准确率和损失值
acc_val_mean = numpy.array(acc_set).mean()
avg_loss_val_mean = numpy.array(avg_loss_set).mean()
# 返回平均损失值,平均准确率
return avg_loss_val_mean, acc_val_mean
开始训练
lists = []
step = 0
for epoch_id in epochs:
metric = 0
for step_id, data in enumerate(train_reader()):
metrics = exe.run(
program = main_program,
feed = feeder.feed(data),
fetch_list = [avg_loss, acc]
)
if step % 100 == 0: #每训练100次 打印一次log
event_handler_plot(train_prompt, step, metrics[0])
print("Pass %d, Batch %d, Cost %f" % (step, epoch_id, metrics[0]))
step += 1
metric = metrics[0]
# 测试每个epoch分类的效果
avg_loss_val, acc_val = tarin_test(train_test_program=test_program,
train_test_reader=test_reader,
train_test_feed=feeder)
print("Test with Epoch %d, avg_cost: %s, acc: %s" %(epoch_id, avg_loss_val, acc_val))
event_handler_plot(test_prompt, step, metric)
lists.append((epoch_id, avg_loss_val, acc_val))
# 保存训练好的模型参数用于后续的预测
if save_dirname is not None:
fluid.io.save_inference_model(save_dirname,
["img"], [prediction], exe,
model_filename = None,
params_filename=None)
best = sorted(lists, key=lambda list: float(list[1]))[0]
print('Best pass is %s, testing Avgcost is %s' % (best[0], best[1]))
print('The classification accuracy is %.2f%%' % (float(best[2]) * 100))
Best pass is 3, testing Avgcost is 0.0381211013488268
The classification accuracy is 98.76%
<Figure size 432x288 with 0 Axes>
应用模型
可以使用训练好的模型对手写体数字图片进行分类,下面程序展示了如何使用训练好的模型进行推断。
生成预测输入数据
infer_5.jpeg 是数字 3 的一个示例图像。把它变成一个 numpy 数组以匹配数据feed格式。
def load_image(file):
# 读取图片文件,转化为灰度图
im = Image.open(file).convert('L')
# 将输入图片调整为28*28的高质量图
im = im.resize((28,28), Image.ANTIALIAS)
# 将图片转化为我numpy的数组
im = numpy.array(im).reshape(1,1,28,28).astype(numpy.float32)
#对数据做归一化处理
im = im / 255.0 * 2.0 -1
return im
cur_dir = os.getcwd()
tensor_img = load_image(cur_dir + '/image/infer_5.jpeg')
tensor_img = load_image(cur_dir + '/image/infer_5.jpeg')
Inference 创建及预测
通过load_inference_model来设置网络和经过训练的参数。我们可以简单地插入在此之前定义的分类器。
inference_scope = fluid.core.Scope()
with fluid.scope_guard(inference_scope):
# 使用 fluid.io.load_inference_model 获取 inference program desc,
# feed_target_names 用于指定需要传入网络的变量名
# fetch_targets 指定希望从网络中fetch出的变量名
[inference_program, feed_target_names,
fetch_targets] = fluid.io.load_inference_model(
save_dirname, exe, None, None
)
# 将feed构建成字典 { feed_target_names: feed_target_data}
# 结果将包含一个与fetch——targets对应的数据列表
results = exe.run(inference_program,
feed = {feed_target_names[0] : tensor_img},
fetch_list=fetch_targets)
lab = numpy.argsort(results)
# 打印infer_图片的预测结果
img = Image.open('image/infer_5.jpeg')
plt.show(img)
print("Inference result of image/infer_5.jpeg is: %d" % lab[0][0][-1])
Inference result of image/infer_5.jpeg is: 5
正文之后
附赠我用于测试的图片,讲道理,准确度感人。。。