第十二章 深度学习基础 案例:MLP实现银行单据手写数字识别

案例:MLP实现银行单据手写数字识别

案例背景

在本案例中,我们将使用PyTorch和Torchvision构建机器学习模型(特别是神经网络)来执行图像分类任务。我们使用MLP模型来构建手写数字识别模型。本案例中使用的数据集是著名的MNIST数据集,这是一个由手写数字0到9组成的28x28黑白图像数据集。

数据读取与划分

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data as data

import torchvision.transforms as transforms
import torchvision.datasets as datasets

from sklearn import metrics
from sklearn import decomposition
from sklearn import manifold
from tqdm.notebook import trange, tqdm
import matplotlib.pyplot as plt
import numpy as np

import copy
import random
import time

为了确保我们得到可重复的结果,我们为Python、Numpy和PyTorch设置了固定的随机种子

python 复制代码
SEED = 1234

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

我们要做的第一件事是加载数据集。

这将自动下载MNIST数据集的训练集,并将其保存在名为".data"的文件夹中。如果该文件夹不存在,它将创建该文件夹。

python 复制代码
ROOT = '.data'

train_data = datasets.MNIST(root=ROOT,
                            train=True,
                            download=True)

接下来,我们要对数据进行"规范化"。这意味着我们希望它的均值为0,标准差为1。

我们为什么要这么做?标准化我们的数据可以让我们的模型训练得更快,也可以帮助它们避免局部极小值,即训练更可靠。

我们通过减去平均值并除以数据集的标准偏差来规范化数据。首先,我们需要计算平均值和标准差。

:重要的是,平均值和标准偏差只计算在训练集上,而不是测试集上。我们不希望使用任何来自测试集的信息,只应该在计算测试损失时查看它。

为了计算平均值和标准差,将它们转换为浮点数,然后使用内置的' mean '和' std '函数分别计算平均值和标准差。图像数据的值在0-255之间,我们希望在0-1之间缩放,因此我们除以255。

python 复制代码
mean = train_data.data.float().mean() / 255
std = train_data.data.float().std() / 255

我们使用如下数据增强的变换:

  • RandomRotation - randomly rotates the image between (-x, +x) degrees, where we have set x = 5. Note, the fill=(0,) is due to a bug in some versions of torchvision.
  • RandomCrop - this first adds padding around our image, 2 pixels here, to artificially make it bigger, before taking a random 28x28 square crop of the image.
  • ToTensor() - this converts the image from a PIL image into a PyTorch tensor.
  • Normalize - this subtracts the mean and divides by the standard deviations given.

The first two transformations have to be applied before ToTensor as they should both be applied on a PIL image. Normalize should only be applied to the images after they have been converted into a tensor. See the Torchvision documentation for transforms that should be applied to PIL images and transforms that should be applied on tensors.

我们有两个变换列表,一个是训练变换,一个是测试变换。训练集的变换是人为地为我们的模型创建更多的例子来进行训练。我们不以同样的方式扩充我们的测试数据,因为我们需要一组一致的例子来评估我们的最终模型。然而,测试数据仍然应该是规范化的。

python 复制代码
train_transforms = transforms.Compose([
                            transforms.RandomRotation(5, fill=(0,)),
                            transforms.RandomCrop(28, padding=2),
                            transforms.ToTensor(),
                            transforms.Normalize(mean=[mean], std=[std])
                                      ])

test_transforms = transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize(mean=[mean], std=[std])
                                     ])

我们用上述变换来变换我们的数据集

python 复制代码
train_data = datasets.MNIST(root=ROOT,
                            train=True,
                            download=True,
                            transform=train_transforms)

test_data = datasets.MNIST(root=ROOT,
                           train=False,
                           download=True,
                           transform=test_transforms)

我们可以看看数据集中的一些图像,看看我们在处理什么。下面的函数绘制一个正方形的图像网格。

python 复制代码
def plot_images(images):

    n_images = len(images)

    rows = int(np.sqrt(n_images))
    cols = int(np.sqrt(n_images))

    fig = plt.figure()
    for i in range(rows*cols):
        ax = fig.add_subplot(rows, cols, i+1)
        ax.imshow(images[i].view(28, 28).cpu().numpy(), cmap='bone')
        ax.axis('off')

让我们加载25张图片。这些将通过我们的变换进行处理,因此将被随机旋转和裁剪。

查看应用了转换的数据是一个很好的实践,这样可以确保它们看起来合理。

例如,水平或垂直翻转数字是没有意义的。

python 复制代码
N_IMAGES = 25

images = [image for image, label in [train_data[i] for i in range(N_IMAGES)]]

plot_images(images)

此外,我们创建了一个验证集,取训练集的10%。

python 复制代码
VALID_RATIO = 0.9

n_train_examples = int(len(train_data) * VALID_RATIO)
n_valid_examples = len(train_data) - n_train_examples

使用' random_split '函数随机抽取训练集的10%作为验证集。剩下的90%将作为训练集。

python 复制代码
train_data, valid_data = data.random_split(train_data,
                                           [n_train_examples, n_valid_examples])

现在我们可以简单地用上面的测试转换覆盖验证集的转换来替换它。

由于验证集是训练集的"子集",如果我们改变其中一个的变换,那么默认情况下Torchvision将改变另一个的变换。为了防止这种情况发生,我们对验证数据进行了"deep_copy"。

python 复制代码
valid_data = copy.deepcopy(valid_data)
valid_data.dataset.transform = test_transforms

接下来,我们将为每个训练/验证/测试集定义一个"DataLoader"。我们可以对这些图像进行迭代,它们将生成一批图像和标签,我们可以使用这些图像和标签来训练我们的模型。

我们只需要打乱我们的训练集,因为它将用于随机梯度下降,我们希望每个batch在epoch之间是不同的。由于我们不使用验证或测试集来更新我们的模型参数,因此它们不需要被打乱。

理想情况下,我们希望使用最大的batch。这里的64相对较小,如果我们的硬件可以处理它,可以适当地增加batch的大小。

python 复制代码
BATCH_SIZE = 64

train_iterator = data.DataLoader(train_data,
                                 shuffle=True,
                                 batch_size=BATCH_SIZE)

valid_iterator = data.DataLoader(valid_data,
                                 batch_size=BATCH_SIZE)

test_iterator = data.DataLoader(test_data,
                                batch_size=BATCH_SIZE)

模型搭建

我们选择两隐藏层的MLP模型

python 复制代码
class MLP(nn.Module):
    def __init__(self, input_dim, output_dim):
        super().__init__()

        self.input_fc = nn.Linear(input_dim, 250)
        self.hidden_fc = nn.Linear(250, 100)
        self.output_fc = nn.Linear(100, output_dim)

    def forward(self, x):

        # x = [batch size, height, width]

        batch_size = x.shape[0]

        x = x.view(batch_size, -1)

        # x = [batch size, height * width]

        h_1 = F.relu(self.input_fc(x))

        # h_1 = [batch size, 250]

        h_2 = F.relu(self.hidden_fc(h_1))

        # h_2 = [batch size, 100]

        y_pred = self.output_fc(h_2)

        # y_pred = [batch size, output dim]

        return y_pred, h_2

实例化模型,并设置正确的输入和输出维度来定义模型

python 复制代码
INPUT_DIM = 28 * 28
OUTPUT_DIM = 10

model = MLP(INPUT_DIM, OUTPUT_DIM)

我们还可以创建一个小函数来计算模型中可训练参数(权重和偏差)的数量------假设我们的所有参数都是可训练的。

python 复制代码
def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

粗略地计算一下我们的参数个数

第一层有784个神经元连接到250个神经元,所以784*250个加权连接加上250个偏置项。

第二层有250个神经元连接到100个神经元,250*100个加权连接加上100个偏置项。

第三层有100个神经元连接到10个神经元,100*10个加权连接加上10个偏差项。

784 ⋅ 250 + 250 + 250 ⋅ 100 + 100 + 100 ⋅ 10 + 10 = 222 , 360 784 \cdot 250 + 250 + 250 \cdot 100 + 100 + 100 \cdot 10 + 10 = 222,360 784⋅250+250+250⋅100+100+100⋅10+10=222,360

python 复制代码
print(f'The model has {count_parameters(model):,} trainable parameters')
复制代码
The model has 222,360 trainable parameters

模型训练与评估

我们将定义优化策略。这是我们将使用的算法来更新模型的参数,以计算数据上的loss。

python 复制代码
optimizer = optim.Adam(model.parameters())

选用softmax作为损失函数

python 复制代码
criterion = nn.CrossEntropyLoss()

有GPU就用GPU,没有的话问题不大。

python 复制代码
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
python 复制代码
model = model.to(device)
criterion = criterion.to(device)

模型准确率定义

python 复制代码
def calculate_accuracy(y_pred, y):
    top_pred = y_pred.argmax(1, keepdim=True)
    correct = top_pred.eq(y.view_as(top_pred)).sum()
    acc = correct.float() / y.shape[0]
    return acc

然后我们就可以定义过程开始训练了

python 复制代码
def train(model, iterator, optimizer, criterion, device):

    epoch_loss = 0
    epoch_acc = 0

    model.train()

    for (x, y) in tqdm(iterator, desc="Training", leave=False):

        x = x.to(device)
        y = y.to(device)

        optimizer.zero_grad()

        y_pred, _ = model(x)

        loss = criterion(y_pred, y)

        acc = calculate_accuracy(y_pred, y)

        loss.backward()

        optimizer.step()

        epoch_loss += loss.item()
        epoch_acc += acc.item()

    return epoch_loss / len(iterator), epoch_acc / len(iterator)

评估步骤

python 复制代码
def evaluate(model, iterator, criterion, device):

    epoch_loss = 0
    epoch_acc = 0

    model.eval()

    with torch.no_grad():

        for (x, y) in tqdm(iterator, desc="Evaluating", leave=False):

            x = x.to(device)
            y = y.to(device)

            y_pred, _ = model(x)

            loss = criterion(y_pred, y)

            acc = calculate_accuracy(y_pred, y)

            epoch_loss += loss.item()
            epoch_acc += acc.item()

    return epoch_loss / len(iterator), epoch_acc / len(iterator)

定义一下时间函数,用来计算一轮要多长时间

python 复制代码
def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs

我们终于准备好训练了!

在每个epoch中,我们计算训练损失和准确度,然后是验证损失和准确度。然后,我们检查所实现的验证损失是否是我们所见过的最好的验证损失。如果是,我们保存模型的参数(称为"state_dict")。

python 复制代码
EPOCHS = 10

best_valid_loss = float('inf')

for epoch in trange(EPOCHS):

    start_time = time.monotonic()

    train_loss, train_acc = train(model, train_iterator, optimizer, criterion, device)
    valid_loss, valid_acc = evaluate(model, valid_iterator, criterion, device)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut1-model.pt')

    end_time = time.monotonic()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. Acc: {valid_acc*100:.2f}%')
复制代码
HBox(children=(HTML(value=''), FloatProgress(value=0.0, max=10.0), HTML(value='')))



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 01 | Epoch Time: 0m 11s
	Train Loss: 0.413 | Train Acc: 87.24%
	 Val. Loss: 0.155 |  Val. Acc: 95.09%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 02 | Epoch Time: 0m 11s
	Train Loss: 0.171 | Train Acc: 94.76%
	 Val. Loss: 0.106 |  Val. Acc: 96.79%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 03 | Epoch Time: 0m 11s
	Train Loss: 0.135 | Train Acc: 95.82%
	 Val. Loss: 0.086 |  Val. Acc: 97.06%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 04 | Epoch Time: 0m 11s
	Train Loss: 0.117 | Train Acc: 96.38%
	 Val. Loss: 0.084 |  Val. Acc: 97.26%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 05 | Epoch Time: 0m 11s
	Train Loss: 0.105 | Train Acc: 96.66%
	 Val. Loss: 0.072 |  Val. Acc: 97.62%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 06 | Epoch Time: 0m 11s
	Train Loss: 0.096 | Train Acc: 97.02%
	 Val. Loss: 0.070 |  Val. Acc: 97.83%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 07 | Epoch Time: 0m 11s
	Train Loss: 0.091 | Train Acc: 97.10%
	 Val. Loss: 0.092 |  Val. Acc: 97.12%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 08 | Epoch Time: 0m 11s
	Train Loss: 0.085 | Train Acc: 97.35%
	 Val. Loss: 0.082 |  Val. Acc: 97.48%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 09 | Epoch Time: 0m 10s
	Train Loss: 0.083 | Train Acc: 97.40%
	 Val. Loss: 0.058 |  Val. Acc: 98.32%



HBox(children=(HTML(value='Training'), FloatProgress(value=0.0, max=844.0), HTML(value='')))



HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=94.0), HTML(value='')))


Epoch: 10 | Epoch Time: 0m 11s
	Train Loss: 0.077 | Train Acc: 97.66%
	 Val. Loss: 0.071 |  Val. Acc: 97.74%

然后,我们加载获得最佳验证损失的模型的参数,然后使用这些参数在测试集上评估我们的模型。

python 复制代码
model.load_state_dict(torch.load('tut1-model.pt'))

test_loss, test_acc = evaluate(model, test_iterator, criterion, device)
复制代码
HBox(children=(HTML(value='Evaluating'), FloatProgress(value=0.0, max=157.0), HTML(value='')))

我们的模型在测试集上达到98%的准确率。

python 复制代码
print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
复制代码
Test Loss: 0.046 | Test Acc: 98.45%
相关推荐
吴佳浩2 小时前
AI 工程师知识地图:模型格式、框架、部署工具一次讲明白
人工智能·aigc·ai编程
IT_陈寒2 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
码农胖大海2 小时前
AI额度不够用的解决方案
人工智能
后端小肥肠2 小时前
小红书虚拟商品怎么做?我先用 Skill 跑通了壁纸品类
人工智能·aigc·agent
feiyu_gao2 小时前
从零搭建个人 AI 工作台:一个管理者的 3 个月实验
人工智能·aigc·团队管理
Lihua奏4 小时前
从单核到多核:CPU为什么不能再只靠提频变快
深度学习
程序员cxuan4 小时前
一句话,让你用上 GPT-5.6
人工智能·后端·程序员
机器之心4 小时前
AI圈刚开始谈Loop Engineering,两位95后博士已经盯上了人类闭环数据
人工智能·openai
澄旭4 小时前
一文讲清 MCP:AI 应用连接外部世界的标准协议
人工智能
机器之心4 小时前
不只DeepSeek,阶跃等开源JetSpec:大模型解码提速近10倍
人工智能·openai