第十二章 深度学习基础 案例: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%
相关推荐
右耳朵猫AI1 小时前
GitHub周趋势2026W22 | AI编程工具、知识图谱、自托管、AI代理、代码智能
人工智能·github·ai编程
lqqjuly1 小时前
MLA — 多头潜在注意力深度解析
深度学习·神经网络·算法
Black蜡笔小新1 小时前
企业AI算力工作站DLTM深度学习推理工作站零代码私有化重塑企业AI落地新模式
人工智能·深度学习
2601_959480151 小时前
Moneta Markets亿汇:“比特币反弹走势仍脆弱”
人工智能
没事别瞎琢磨1 小时前
六、输出捕获与截断
人工智能·node.js
嘉子的秃头日记2 小时前
TRO 2026|轮椅也能“猜到”用户想往哪走?
大数据·人工智能·机器学习
2601_957190902 小时前
极致裸眼沉浸!飞行影院重塑文旅游玩新体验
大数据·人工智能·旅游
Meinianda2 小时前
我用Agent 使用瑞幸官方MCP下了一单:过程全记录,优缺点分析
人工智能
没事别瞎琢磨2 小时前
七、敏感路径预检——Protected Paths
人工智能·node.js