【深度学习】手写数字分类模型

任务介绍

手写数字识别是计算机视觉的一个经典项目,因为手写数字的随机性,使用传统的计算机视觉技术难以找到数字共有特征。在计算机视觉发展的初期,手写数字识别成为一大难题。

从我们之前讲解的视觉任务分类来看,手写数字识别是典型的分类任务,输入一张图片进行十分类。在现实生活中,手写数字识别也有非常多的应用场景。如下图,我们看到的邮编的识别,可以极大地推动产业自动化,使用卷积神经网络实现的精度甚至可以超越人类

本次任务就是想建立一个模型,输入一张手写数字的图片,就能输出一个正确的分类结果。通过这样的一个实战项目,可以很好地帮我们巩固和理解我们之前讲过的卷积、池化等常用操作,也可以温习一下深度学习的基本流程。

请添加图片描述

数据准备

手写数字识别有通用的数据集MNIST,其中包含已经标注好的几万张手写数字,并且分好了训练集和评价集。如果我们对其中的一张图片进行可视化,可以看到这样的画面:

请添加图片描述

图像的shape为(1,28,28),是单通道图,图像的大小仅为28*28,它的标注为7

通常对于一般项目来说,需要自己手写一个Dataloader来依次加载数据,返回图片和标注,供给训练的接口用于训练。这里考虑到我们入门的原因,直接使用写好的API。有兴趣的同学可以自己尝试不使用高级API,自己下载好压缩包手写一下Dataloader。

train_loader = paddle.io.DataLoader(MNIST(mode='train', transform=ToTensor()), batch_size=10, shuffle=True)
valid_loader = paddle.io.DataLoader(MNIST(mode='test', transform=ToTensor()), batch_size=10)

通过上面包装好的API,我们就加载好了训练集评价集,可以供训练接口调用。

网络搭建

准备好数据之后,第二部也就是搭建卷积神经网络,卷积神经网络直接影响着模型的精度,这一步也是最为关键的一个环节。本次实战中,我们默认使用LeNetLeNet是最早的卷积神经网络之一,诞生于1998年,在手写数字识别任务中取得了巨大成功
请添加图片描述

它的网络结构也非常简单,基本上为一个卷积层接着一个池化层,最后通过两个全连接层输出一个[1,10]的矩阵。全连接层我们之前没有介绍过,它通常用于拟合一些批量数据,比如有很多散点,拟合出一条曲线。它的结构如下:
在这里插入图片描述

也就是说每一个输出和前面一层的所有参数都相关,它的数学表达其实就是乘上一个变换矩阵再加上偏差,得到输出矩阵。为什么图像中大量使用卷积层,很少使用全连接层呢?这边留给大家课后自己思考。

LeNet使用Paddle复现代码如下:

import paddle
import numpy as np
from paddle.nn import Conv2D, MaxPool2D, Linear
import paddle.nn.functional as F

# 定义 LeNet 网络结构
class LeNet(paddle.nn.Layer):
    def __init__(self, num_classes=1):
        super(LeNet, self).__init__()
        self.conv1 = Conv2D(in_channels=1, out_channels=6, kernel_size=5)
        self.max_pool1 = MaxPool2D(kernel_size=2, stride=2)
        self.conv2 = Conv2D(in_channels=6, out_channels=16, kernel_size=5)
        self.max_pool2 = MaxPool2D(kernel_size=2, stride=2)
        self.conv3 = Conv2D(in_channels=16, out_channels=120, kernel_size=4)
        self.fc1 = Linear(in_features=120, out_features=64)
        self.fc2 = Linear(in_features=64, out_features=num_classes)
    def forward(self, x):                        #[N,1,28,28] 
        x = self.conv1(x)                        #[N,6,24,24]
        x = F.sigmoid(x)                         #[N,6,24,24]
        x = self.max_pool1(x)                    #[N,6,12,12]
        x = F.sigmoid(x)                         #[N,6,12,12]
        x = self.conv2(x)                        #[N,16,8,8]
        x = self.max_pool2(x)                    #[N,16,4,4]
        x = self.conv3(x)                        #[N,120,1,1]
        x = paddle.reshape(x, [x.shape[0], -1])  #[N,120]
        x = self.fc1(x)                          #[N,64]
        x = F.sigmoid(x)                         #[N,64]
        x = self.fc2(x)                          #[N,10]
        return x

Paddle使用动态图的这种写法非常清晰,定义一个类体,在初始化函数内写好需要使用的层,需要特别注意好输入输出的通道数,卷积核的大小这些参数,如果稍不注意就会出现维度上的错误。在这边定义好之后,我们再写forward函数,forward函数就是之后我们传入图像后真正执行的运算。

为了帮助大家理解,再详细解释一下执行流程。首先我们实例化类体

model = LeNet(num_classes=10)

实例化的时候,类体自动地执行init()初始化函数,init()函数里面又实例化了Conv2D,MaxPool2D,这些其实都是类体,这些类体和LeNet一样,也有init()和forward函数,在初始化函数中都进行了相应的实例化。实例化的过程,并没有真正开始运算,只是定义好了我想要使用的层。

output = model(img)

当我再次运行上面的代码后,相当于调用了这个类体,并且输入了img,这时候类体会自动调用call()函数,那forward函数为什么会执行呢?原因就在于所有的运算都继承了paddle.nn.Layer母类,母类中将forward函数写在了call()里面,那么就相当于调用LeNet这个类的对象的时候,自动调用了forward函数,这时候也就开始了真正的运算过程

整个过程希望大家反复推敲,知道彻底理解为止。不难发现,这样的建立网络的形式,可以不停地嵌套,这是非常清晰的形式,我们之后讲解复杂模型的时候这样的优势就会体现出来。

模型训练

import paddle
import paddle.nn
import paddle.nn.functional as F
import paddle.metric
import paddle.optimizer
from paddle.nn import Conv2D, MaxPool2D, Linear
from model import LeNet

import paddle.io as pio
import paddle.vision.transforms as ptransform
from paddle.vision.datasets import MNIST
import numpy as np


# 定义训练过程
def train(model, optimizer, train_loader, valid_loader) :
    use_gpu = True
    paddle.device.set_device("gpu:0") if use_gpu else paddle.device.set_device("cpu")

    if paddle.in_dynamic_mode() :
        print("dynamic graph...")
    else:
print("static graph...")

print("start training...")
for epoch in range(EPOCH_SIZE) :
    print("------epoch{} start------".format(epoch))
    # 开始训练
    model.train()
    for batch_id, data in enumerate(train_loader()) :
        img = data[0]   # 图像信息
        label = data[1] # 这是数字几?

        # 计算模型输出
        logits = model(img)
        # 计算loss
        loss_func = paddle.nn.CrossEntropyLoss(reduction = "none") # 交叉熵loss
        loss = loss_func(logits, label)
        # 平均loss
        avg_loss = paddle.mean(loss)
# if batch_id % 500 == 0:
        print("epoch: {}, batch_id: {}, loss is: {:.4f}".format(epoch + 1, batch_id, float(avg_loss.numpy())))
        # 反向传播
        avg_loss.backward()
        optimizer.step()
        optimizer.clear_grad()

        # 开始评价
        print("evaluation...")
        model.eval()
        accuracies = []
        losses = []
        for batch_id, data in enumerate(valid_loader()) :
            img = data[0]
            label = data[1]
            # 计算模型输出
            logits = model(img)
            loss_func = paddle.nn.CrossEntropyLoss(reduction = "none")
            loss = loss_func(logits, label)
            acc = paddle.metric.accuracy(logits, label)
            accuracies.append(acc.numpy())
            losses.append(loss.numpy())

            print("[validation]\n accuracy: {:.4f}\n loss: {:.4f}".format(np.mean(accuracies), np.mean(losses)))

		print("------epoch{} finish------\n".format(epoch))

		# 保存模型参数
		paddle.save(model.state_dict(), path = "mnist.pdparams")



# 1. 实例化一个LeNet网络
model = LeNet(num_classes = 10)

# 2. 训练轮次
EPOCH_SIZE = 50

# 3. 定义优化器
optimizer = paddle.optimizer.Momentum(learning_rate = 0.001,
	parameters = model.parameters())

# 4. 定义训练集加载器 每次迭代给出10个数据
train_loader = pio.DataLoader(MNIST(mode = 'train', transform = ptransform.ToTensor()),
	batch_size = 100,
	shuffle = True)
# 5. 定义测试集加载器
valid_loader = pio.DataLoader(MNIST(mode = 'test', transform = ptransform.ToTensor()),
	batch_size = 100)

# 6. 开始训练
train(model, optimizer, train_loader, valid_loader)

训练的代码我们根据学过的知识,就非常清晰。从数据集接口获得数据集,把图像输入到模型中,模型得到一个预测值,使用CrossEntropyLoss损失函数计算预测值和标签真实值的loss,将loss反向发聩给网络参数,最后使用优化器修正参数,降低loss

需要注意的是CrossEntropyLoss损失函数自带softmax,分类问题最后都需要一个softmax激活函数把输出的[1,10]矩阵归到[0,1],并且10个数的和为1,也就代表了这张图片为0-9的概率

模型预测

import numpy as np
import paddle

from paddle.vision.datasets import MNIST
from paddle.vision.transforms import ToTensor
from model import LeNet
import paddle.nn.functional as F

def print_array(arr) :
    for e in arr[0] :
        print("{:.6f}".format(float(e)), end = " ")
        print("")
		
def test_infer(n : int) :
	# 1. 加载推理用的图片
	valid_loader = MNIST(mode = "test", transform = ToTensor())
	image = np.array(valid_loader[n][0])
	
	# # 2. 查看要推理的图片
	# import matplotlib.pyplot as plt
	# plt.imshow(image.squeeze(), cmap = 'gray')
	# plt.show()
	
	# 3. 加载网络并填入训练好的参数
	model = LeNet(num_classes = 10)
	model_dict = paddle.load("mnist.pdparams")
	model.set_state_dict(model_dict)
	if paddle.in_dynamic_mode() :
		print("dynamic graph...")
	else:
		print("static graph...")
		
	# 4. 切换到评价模式
	model.eval()

	# 5. 输入到模型中的矩阵 打印
	x = valid_loader[n][0].reshape((1, 1, 28, 28)).astype("float32")
	print("标签:{}\n输入矩阵:".format(valid_loader[n][1]))
	for col in x[0][0]:
	for e in col :
	if (float(e) == 0.00) :
		print("    ", end = "\t")
	else :
		print("{:.2f}".format(float(e)), end = " ")
		print("")

	# 6. 推理
	result = model(x)
	print("原始输出:")
	print_array(result)

	# 7. softmax
	softmax_result = F.softmax(result)
	print("softmax后:")
	print_array(softmax_result.numpy())

if __name__ == "__main__":
	for i in range(0, 10) :
	    test_infer(i)

训练完模型之后,我们需要加载模型并且预测,这里就挑选了评价集中的一张图片预测,看一下输出的结果是否正确。
在这里插入图片描述

我们使用这样的方法加载模型,最后预测输出:

原始输出:
-1.574104 -0.194709 1.864320 1.968624 -1.509960 -1.423821 -7.800447 9.007638 -1.589992 2.344529 
softmax后:
0.000025 0.000100 0.000788 0.000874 0.000027 0.000029 0.000000 0.996858 0.000025 0.001273 

这也就分别代表0-9的概率,7的概率高达 99.68 % 99.68\% 99.68%,模型输出正确!

导出模型

可以将动态图转为静态图导出:

import paddle
from model import LeNet
import paddle.jit
import paddle.static

# 初始化模型
model = LeNet(num_classes = 10)
state_dict = paddle.load('mnist.pdparams')
model.set_state_dict(state_dict)

# 指定输入规格
input_spec = [paddle.static.InputSpec(shape = [None, 1, 28, 28], dtype = 'float32')]

# 将动态图模型转换为静态图模型并指定 input_spec
model = paddle.jit.to_static(model, input_spec = input_spec)

# 导出模型
paddle.jit.save(model, 'mnist_static_model')

用visualdl工具查看网络结构:

visualdl --logdir=./mnist_static_model/ --model=mnist_static_model.pdmodel

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_宁清

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值