关键词:深度学习、神经网络、Pytorch、OpenCV

一、单个手写数字的识别

步骤一:模型设计

1. 初始模型设计

因为数字有0、1、2、3、4、5、6、7、8、9,所以我们可以把他当作是一个图片十分类任务来处理,因此我结合之前已有的学习基础,设计了一个如下的模型来完成这个任务:

img

这个模型是一个典型的卷积神经网络(CNN),原本是被用来处理CIFAR-10数据集。该模型包含多层卷积层、池化层、展平层和全连接层。至于具体参数例如输入尺寸、卷积核大小、维度等参数,可以根据手写数字的数据集来做具体调整,使其更加符合本次实验的要求。

下面我将对这个模型的每一层做一个简单的介绍:

  1. 输入层
    • 输入的图像尺寸为 3x32x32(3 表示 RGB 三个通道,32x32 表示图像尺寸)。
    • 当然在本次任务中,这个尺寸可能会变化,我将在后续进行说明。
  2. 第一层卷积层
    • 32 个 5x5 卷积核,步幅为 1,填充为 2。
    • 输出特征图尺寸:32x32x32。
  3. 第一层池化层
    • 2x2 最大池化层,步幅为 2。
    • 输出特征图尺寸:32x16x16。
  4. 第二层卷积层
    • 32 个 5x5 卷积核,步幅为 1,填充为 2。
    • 输出特征图尺寸:32x16x16。
  5. 第二层池化层
    • 2x2 最大池化层,步幅为 2。
    • 输出特征图尺寸:32x8x8。
  6. 第三层卷积层
    • 64 个 5x5 卷积核,步幅为 1,填充为 2。
    • 输出特征图尺寸:64x8x8。
  7. 第三层池化层
    • 2x2 最大池化层,步幅为 2。
    • 输出特征图尺寸:64x4x4。
  8. 展平层
    • 将多维的特征图展平成一维向量。
    • 输出向量长度:1024。
  9. 全连接层1
    • 线性层,输入维度 1024,输出维度 64。
  10. 全连接层2
  • 线性层,输入维度 64,输出维度 10。
  • 最后的张量对应十个数字,做一个归一化,每个维度的值反应了他识别为这个数字的概率。

下面是使用pytorch实现这个模型,我定义了一个类,他继承了nn.model,并提供了常见的两种写法,最后进行了正确性的测试以及可视化输出:

# 用于构建CIFAR10的模型,如上图所示
# 顺便介绍sequential的用法

import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.tensorboard import SummaryWriter


class GuoHangJiang(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()

        # 不引入sequential的写法
        # 第一层卷积部分
        # 每一步卷积的padding与stride都需要靠计算得出
        self.conv1 = Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2)
        # 第二层池化部分
        self.maxpool1 = MaxPool2d(kernel_size=2)
        # 第三层卷积部分
        self.conv2 = Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2)
        # 第四层池化部分
        self.maxpool2 = MaxPool2d(kernel_size=2)
        # 第五层卷积部分
        self.conv3 = Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2)
        # 第六层池化部分
        self.maxpool3 = MaxPool2d(kernel_size=2)
        # 第七层展平部分
        self.flatten = Flatten()
        # 第八层线性层
        # 图片中少了一层介绍,仔细观察
        self.linear1 = Linear(in_features=1024, out_features=64)
        # 第九层线性层
        self.linear2 = Linear(in_features=64, out_features=10)

        # 引入sequential的写法
        self.model1 = Sequential(
            Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
            MaxPool2d(kernel_size=2),
            Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
            MaxPool2d(kernel_size=2),
            Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            MaxPool2d(kernel_size=2),
            Flatten(),
            Linear(in_features=1024, out_features=64),
            Linear(in_features=64, out_features=10)
        )

    def forward(self, x):
        # x = self.conv1(x)
        # x = self.maxpool1(x)
        # x = self.conv2(x)
        # x = self.maxpool2(x)
        # x = self.conv3(x)
        # x = self.maxpool3(x)
        # x = self.flatten(x)
        # x = self.linear1(x)
        # x = self.linear2(x)
        x = self.model1(x)
        return x

ghj = GuoHangJiang()
print(ghj)

# 用于测试模型是否正确
input = torch.ones(64, 3, 32, 32)
output = ghj(input)
print(output.shape)

# 除了print可以用来可视化,tensorboard也可以用来可视化
writer = SummaryWriter('logs_seq')
writer.add_graph(ghj, input)
writer.close()

我们运行一下看一下图片张量在这个模型中是怎样变化的:

(pytorch) PS D:\Python\learn_pytorch> & C:/Users/67093/.conda/envs/pytorch/python.exe d:/Python/learn_pytorch/ShuziShibie.py
GuoHangJiang(
  (model1): Sequential(
    (0): Conv2d(3, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))   
    (1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (2): Conv2d(32, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))  
    (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (4): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))  
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Flatten(start_dim=1, end_dim=-1)
    (7): Linear(in_features=1024, out_features=64, bias=True)
    (8): Linear(in_features=64, out_features=10, bias=True)
  )
)
torch.Size([64, 10])

整个流程是非常的清晰且直观的,用来解决手写数字的识别问题绰绰有余。

2. 针对手写数字识别的模型改进

在本次任务中,我将选用MNIST数据集,它是一个包含手写数字的广泛使用的数据集,非常适合用来进行图像分类任务。MNIST数据集包括60,000张训练图像和10,000张测试图像,每张图像都是28x28像素的灰度图像,并且标签为0到9的数字。

要将原本适用于CIFAR-10数据集的模型设计改为适用于MNIST数据集,需要做以下调整:

  1. 输入通道数由3改为1,因为MNIST是灰度图像。
  2. 输入图像尺寸由32x32改为28x28,因为MNIST图像尺寸是28x28。

以下是改进的模型代码:

import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.tensorboard import SummaryWriter

class GuoHangJiang(nn.Module):
    def __init__(self):
        super(GuoHangJiang, self).__init__()

        # 使用sequential的写法
        self.model1 = Sequential(
            Conv2d(in_channels=1, out_channels=32, kernel_size=5, stride=1, padding=2),
            MaxPool2d(kernel_size=2),
            Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
            MaxPool2d(kernel_size=2),
            Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            MaxPool2d(kernel_size=2),
            Flatten(),
            Linear(in_features=64*3*3, out_features=64),
            Linear(in_features=64, out_features=10)
        )

    def forward(self, x):
        x = self.model1(x)
        return x

ghj = GuoHangJiang()
print(ghj)

# 用于测试模型是否正确
input = torch.ones(64, 1, 28, 28)
output = ghj(input)
print(output.shape)

# 除了print可以用来可视化,tensorboard也可以用来可视化
writer = SummaryWriter('logs_seq')
writer.add_graph(ghj, input)
writer.close()

测试结果:

(pytorch) PS D:\Python\learn_pytorch> & C:/Users/67093/.conda/envs/pytorch/python.exe d:/Python/learn_pytorch/ShouXieShiBie.py
GuoHangJiang(
  (model1): Sequential(
    (0): Conv2d(1, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))   
    (1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (2): Conv2d(32, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))  
    (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (4): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))  
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Flatten(start_dim=1, end_dim=-1)
    (7): Linear(in_features=576, out_features=64, bias=True)
    (8): Linear(in_features=64, out_features=10, bias=True)
  )
)
torch.Size([64, 10])

下面是代码运行实例截图:

image-20240701120119799

步骤二:模型可视化训练

我们引入上一步设计的模型,对其进行训练,下面是我对其完整训练流程的介绍:

1. 数据准备

import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torch import nn
from torch.utils.tensorboard import SummaryWriter

# 加载MNIST数据集并进行预处理
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

train_data = torchvision.datasets.MNIST(root='dataset', train=True, transform=transform, download=True)
test_data = torchvision.datasets.MNIST(root='dataset', train=False, transform=transform, download=True)

train_data_size = len(train_data)
test_data_size = len(test_data)
print("训练数据集的长度为:{},测试数据集的长度为:{}".format(train_data_size, test_data_size))

train_dataloader = DataLoader(train_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=False)

这部分代码加载MNIST数据集,并应用了标准化变换。transforms.Normalize((0.5,), (0.5,)) 将图像数据标准化到均值为0.5、标准差为0.5。数据加载器(DataLoader)用于将数据分批次加载到训练和测试中。

2. 模型定义

# 搭建神经网络
# 借鉴之前写过的一个MNIST模型
from model import GuoHangJiang

# 创建网络模型
ghj = GuoHangJiang()

这一部分导入并实例化了之前定义的神经网络模型 GuoHangJiang

3. 损失函数和优化器

# 创建损失函数
loss_fn = nn.CrossEntropyLoss()

# 创建优化器
learning_rate = 1e-2
optimizer = torch.optim.SGD(ghj.parameters(), lr=learning_rate)

这部分代码创建了交叉熵损失函数和随机梯度下降(SGD)优化器。学习率设置为0.01。

4. 训练参数和TensorBoard

# 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 10

# 添加tensorboard可视化
writer = SummaryWriter("logs_train")

这里设置了训练过程中的一些参数,包括总训练步数、总测试步数和训练轮数。还使用TensorBoard记录训练过程中的损失,以便可视化。

5. 训练和测试过程

for i in range(epoch):
    print("----------第{}轮训练开始----------".format(i + 1))

    ghj.train()
    for data in train_dataloader:
        imgs, targets = data
        outputs = ghj(imgs)
        loss = loss_fn(outputs, targets)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_train_step += 1
        writer.add_scalar("train_loss", loss.item(), total_train_step)

        if total_train_step % 100 == 0:
            print("训练次数:{}, Loss:{}".format(total_train_step, loss.item()))

    ghj.eval()
    total_test_loss = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            outputs = ghj(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss += loss.item()
    print("整体测试集上的Loss:{}".format(total_test_loss))
    writer.add_scalar("test_loss", total_test_loss, total_test_step)
    total_test_step += 1

    torch.save(ghj, "ghj_{}.pth".format(i + 1))
    print("第{}轮训练结束,模型已保存".format(i + 1))

writer.close()

这部分代码进行模型训练和测试:

  • 每轮训练开始时,打印当前轮数。
  • 设置模型为训练模式 (ghj.train())。
  • 遍历训练数据进行前向传播、计算损失、反向传播和参数更新。
  • 每训练100次打印一次训练损失。
  • 设置模型为评估模式 (ghj.eval())。
  • 禁用梯度计算 (with torch.no_grad()),遍历测试数据计算总测试损失。
  • 打印整体测试集上的损失。
  • 保存当前训练轮次的模型参数。
  • 关闭TensorBoard写入器。

6. 训练结果展示

下面是运行的时的部分输出展示:

(pytorch) PS D:\Python\learn_pytorch> & C:/Users/67093/.conda/envs/pytorch/python.exe d:/Python/learn_pytorch/ShouXieShiBie_train.py
训练数据集的长度为:60000,测试数据集的长度为:10000
GuoHangJiang(
  (model1): Sequential(
    (0): Conv2d(1, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))   
    (1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (2): Conv2d(32, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))  
    (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (4): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))  
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Flatten(start_dim=1, end_dim=-1)
    (7): Linear(in_features=576, out_features=64, bias=True)
    (8): Linear(in_features=64, out_features=10, bias=True)
  )
)
torch.Size([64, 10])
----------第1轮训练开始----------
训练次数:100, Loss:2.150526762008667
训练次数:200, Loss:1.1308094263076782
训练次数:300, Loss:0.6747239828109741
训练次数:400, Loss:0.3342771828174591
训练次数:500, Loss:0.23235106468200684
训练次数:600, Loss:0.4987734258174896
训练次数:700, Loss:0.35505443811416626
训练次数:800, Loss:0.27889004349708557
训练次数:900, Loss:0.3870258927345276
整体测试集上的Loss:26.561247029341757
第1轮训练结束,模型已保存
----------第2轮训练开始----------
训练次数:1000, Loss:0.27520230412483215
训练次数:1100, Loss:0.04403237998485565
训练次数:1200, Loss:0.12688487768173218
训练次数:1300, Loss:0.15233315527439117
训练次数:1400, Loss:0.1719307005405426
训练次数:1500, Loss:0.18218977749347687
训练次数:1600, Loss:0.13730388879776
训练次数:1700, Loss:0.07805894315242767
训练次数:1800, Loss:0.27868708968162537
整体测试集上的Loss:14.277361426735297
第2轮训练结束,模型已保存
----------第3轮训练开始----------
训练次数:1900, Loss:0.051657937467098236
训练次数:2000, Loss:0.08964638411998749

可以发现训练速度较慢,于是我们对代码进行小修改,使用GPU进行训练。

7. 使用GPU进行训练

以下是使用GPU进行训练的完整代码,我们还添加了正确率的计算:

import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torch import nn
from torch.utils.tensorboard import SummaryWriter

# 检查是否有可用的GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if device.type == 'cuda':
    print(f"使用的GPU是: {torch.cuda.get_device_name(0)}")
else:
    print("没有可用的GPU,使用CPU")

# 加载MNIST数据集并进行预处理
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

train_data = torchvision.datasets.MNIST(root='dataset', train=True, transform=transform, download=True)
test_data = torchvision.datasets.MNIST(root='dataset', train=False, transform=transform, download=True)

train_data_size = len(train_data)
test_data_size = len(test_data)
print("训练数据集的长度为:{},测试数据集的长度为:{}".format(train_data_size, test_data_size))

train_dataloader = DataLoader(train_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=False)

# 搭建神经网络
from ShouXieShiBie_model import GuoHangJiang

# 创建网络模型
ghj = GuoHangJiang()

# 将模型转移到GPU
guj = ghj.to(device)

# 创建损失函数
loss_fn = nn.CrossEntropyLoss()
loss_fn = loss_fn.to(device)

# 创建优化器
learning_rate = 1e-2
optimizer = torch.optim.SGD(ghj.parameters(), lr=learning_rate)

# 设置训练网络的一些参数
total_train_step = 0
total_test_step = 0
epoch = 30

# 添加tensorboard可视化
writer = SummaryWriter("logs_train")

for i in range(epoch):
    print("----------第{}轮训练开始----------".format(i + 1))

    ghj.train()
    for data in train_dataloader:
        imgs, targets = data
        # 将数据转移到GPU
        imgs, targets = imgs.to(device), targets.to(device)
        outputs = ghj(imgs)
        loss = loss_fn(outputs, targets)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_train_step += 1
        writer.add_scalar("train_loss", loss.item(), total_train_step)

        if total_train_step % 100 == 0:
            print("训练次数:{}, Loss:{}".format(total_train_step, loss.item()))

    ghj.eval()
    total_test_loss = 0
    total_accuracy = 0  # 为了计算整体测试集上的准确率,需要记录预测正确的样本数量
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            # 将数据转移到GPU
            imgs, targets = imgs.to(device), targets.to(device)
            outputs = ghj(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss += loss.item()
            # 计算当前批次的准确率,具体计算方法见test.py的解释以及对应的视频链接
            accuracy = (outputs.argmax(1) == targets).sum()
            total_accuracy += accuracy
    print("整体测试集上的Loss:{}".format(total_test_loss))
    print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size))
    writer.add_scalar("test_loss", total_test_loss, total_test_step)
    writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
    total_test_step += 1

    torch.save(ghj, "ghj_{}.pth".format(i + 1))
    print("第{}轮训练结束,模型已保存".format(i + 1))

writer.close()

我们进行30轮训练,下面是部分输出展示:

pytorch) PS D:\Python\learn_pytorch> & C:/Users/67093/.conda/envs/pytorch/python.exe d:/Python/learn_pytorch/ShouXieShiBie_train_GPU.py

使用的GPU是: NVIDIA GeForce RTX 3070 Laptop GPU

训练数据集的长度为:60000,测试数据集的长度为:10000
GuoHangJiang(
  (model1): Sequential(
    (0): Conv2d(1, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (2): Conv2d(32, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (4): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Flatten(start_dim=1, end_dim=-1)
    (7): Linear(in_features=576, out_features=64, bias=True)   
    (8): Linear(in_features=64, out_features=10, bias=True)  
  )
)
torch.Size([64, 10])
----------第1轮训练开始----------
训练次数:100, Loss:2.0744383335113525
训练次数:200, Loss:0.9707316756248474
训练次数:300, Loss:0.5561598539352417
训练次数:400, Loss:0.3622879683971405
训练次数:500, Loss:0.44760021567344666
训练次数:600, Loss:0.18201066553592682
训练次数:700, Loss:0.09421295672655106
训练次数:800, Loss:0.26845136284828186
训练次数:900, Loss:0.15463638305664062
整体测试集上的Loss:31.788333610631526
整体测试集上的正确率:0.9311999678611755
第1轮训练结束,模型已保存
----------第2轮训练开始----------
训练次数:1000, Loss:0.09018311649560928
训练次数:1100, Loss:0.16740190982818604
训练次数:1200, Loss:0.09253757447004318
训练次数:1300, Loss:0.3073056638240814
训练次数:1400, Loss:0.2571570873260498
训练次数:1500, Loss:0.12080878019332886
训练次数:1600, Loss:0.3311219811439514
训练次数:1700, Loss:0.22441141307353973
训练次数:1800, Loss:0.033919304609298706
整体测试集上的Loss:16.8317426412832
整体测试集上的正确率:0.9659000039100647
第2轮训练结束,模型已保存
----------第3轮训练开始----------
训练次数:1900, Loss:0.02228640764951706
训练次数:2000, Loss:0.11637557297945023
训练次数:2100, Loss:0.06177337095141411

8. 训练可视化及结果查询

我们打开tensorboard查看训练的可视化过程如下:

image-20240701133000889 image-20240701133012390 image-20240701133027234

上一步中我们存储了每次训练好的模型(如下),我们讲使用第30轮的模型来完成实验的后续部分。

image-20240701122905770

下面是代码实录:

image-20240701133245619

步骤三:模型测试

我们取用第30轮的模型参数,给出一张手写图片来进行测试,图片如下:

image-20240701123511047

我们给出下面的测试代码,看模型能否识别这个为数字2:

import torch
import torchvision
from PIL import Image
from torch import nn

image_path = "./imgs/shuzi2.png"
# 打开是PIL类型
image = Image.open(image_path).convert('L')  # 转换为灰度图像
print(image)

# 为了适应我们的模型,需要对图片进行预处理
transform = torchvision.transforms.Compose(
    [torchvision.transforms.Resize((28, 28)),
     torchvision.transforms.ToTensor(),
     torchvision.transforms.Normalize((0.5,), (0.5,))]
)
image = transform(image)
print(image.shape)


# 加载网络模型
from ShouXieShiBie_model import GuoHangJiang


# 如果模型原来是使用gpu训练的,这时候就需要告诉电脑使用cpu进行测试,要不然会报错
model = torch.load("ghj_30.pth", map_location=torch.device('cpu'))
print(model)

# 要求输入的image是一个四维的,但现在是torch.Size([1, 28, 28]),少了一个batch size的维度
image = torch.reshape(image, (1, 1, 28, 28))

# 下面这几行不要忘记了,养成一个良好的代码习惯
model.eval()
with torch.no_grad():
    output = model(image)

print(output)
# 告知当前是哪个数字
print("预测的结果为:{}".format(torch.argmax(output, 1).item()))

发现识别错误:

tensor([[-0.7378, -3.9288,  3.1303,  5.3542, -2.3251, -1.9398,  0.1089, -7.0653,
         10.1833, -2.1943]])
预测的结果为:8

问题描述

我们把诸如下面的1-9的数字都测试一遍,发现都识别为8,8的张量特别的大!!

image-20240701134118384image-20240701134125511image-20240701134133131image-20240701134140824

问题解决

经过我的分析发现,mnist数据集使用的样本都是黑色背景,白色数字

而我们做测试、实际用起来,肯定都是白色背景,黑色数字,所以模型就会把白色的背景部分当作是数字,我们简单考虑以下,去掉数字以后剩下的背景确实是跟8最相近的!

为了验证我们的猜想,我们使用一张黑色背景白色数字的图片来做测试如下:

image-20240701134922905
tensor([[-10.6956,  -4.9937,  -5.2039,  -0.2524,  10.5677,  -2.5184, -16.6606,
           5.5232,  -0.6070,  19.9935]])
预测的结果为:9

可以看到问题迎刃而解!并且9对应的张量非常大,效果非常好!

那么如何彻底解决这个问题呢,我们只需要在代码的

image = Image.open(image_path).convert('L')  # 转换为灰度图像

这个部分加一个颜色黑白反转即可!

image = Image.open(image_path).convert('L')  # 转换为灰度图像
# 黑白反转
image = torchvision.transforms.functional.invert(image)

然后再进行测试,输入黑字1,结果如下:

tensor([[ 1.7799, 10.0625, -0.3011, -3.6549, -0.6460, -4.5650, -0.0875,  1.8749,
         -2.2461, -1.0116]])
预测的结果为:1

现在,我们就彻底完成了单个手写数字的识别!

步骤四:封装模型

将上述代码封装成一个函数,接受一张图片的路径,返回预测的数字:

import torch
import torchvision
from PIL import Image
from torch import nn

def predict_digit(image_path):
    # 打开图片并转换为灰度图像
    image = Image.open(image_path).convert('L')  
  
    # 黑白反转
    image = torchvision.transforms.functional.invert(image)
  
    # 预处理图片
    transform = torchvision.transforms.Compose(
        [torchvision.transforms.Resize((28, 28)),
         torchvision.transforms.ToTensor(),
         torchvision.transforms.Normalize((0.5,), (0.5,))]
    )
    image = transform(image)
  
    # 加载预训练模型
    from ShouXieShiBie_model import GuoHangJiang
    model = torch.load("ghj_30.pth", map_location=torch.device('cpu'))
  
    # 添加batch size维度
    image = torch.reshape(image, (1, 1, 28, 28))
  
    # 预测
    model.eval()
    with torch.no_grad():
        output = model(image)
  
    # 返回预测结果
    return torch.argmax(output, 1).item()

# 使用示例
image_path = "./imgs/shuzi1.png"
predicted_digit = predict_digit(image_path)
print("预测的结果为:{}".format(predicted_digit))
(pytorch) PS D:\Python\learn_pytorch> & C:/Users/67093/.conda/envs/pytorch/python.exe d:/Python/learn_pytorch/ShouXieShiBie_function.py
预测的结果为:1

二、一段手写数字的识别

步骤一:图片分割

目标:能将不止一行的数字大图分割成一个数字一个小图,譬如我们使用下面一幅于渊龙同学的手写笔记,目标是写一个算法能将其分为多个小图,并且按照书写顺序从左到右,从上到下进行分割并依次编号。

已上传的图片

我们使用OpenCV来完成这部分的图像处理,设计这么一个函数:

输入

  • image_path (str): 输入图片的文件路径。这个路径指向包含手写数字的图像文件。
  • output_dir (str): 输出目录的路径。这个目录用于保存分割后的数字图片。如果目录不存在,会自动创建。

输出

  • 没有返回值,但会在指定的输出目录中保存分割后的数字图片。每个图片文件命名为 digit_{i}.png,其中 {i} 是数字图片的索引。

代码如下:

import cv2
import os

def split_image_into_digits(image_path, output_dir):
    # 加载图片
    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # 反转颜色
    image = cv2.bitwise_not(image)

    # 二值化
    _, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # 查找轮廓
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 创建一个目录存储分割后的数字图像
    os.makedirs(output_dir, exist_ok=True)

    # 遍历轮廓并保存每个数字图像
    for i, contour in enumerate(contours):
        # 获取轮廓的边界框
        x, y, w, h = cv2.boundingRect(contour)

        # 提取数字图像
        digit_image = image[y:y+h, x:x+w]

        # 保存图像
        cv2.imwrite(f"{output_dir}/digit_{i}.png", digit_image)

    print("数字图像已保存至目录:", output_dir)

# split_image_into_digits使用示例
image_path = "./imgs/shuzi_da.png"
output_dir = "./imgs_output"
split_image_into_digits = split_image_into_digits(image_path, output_dir)

问题描述

分割效果很差,且没有按照顺序排序,75个数字被识别出来了一百多个数字,且成功率很低:

image-20240701160248084

问题解决

  1. 去噪处理:使用中值滤波(cv2.medianBlur)来去除图像噪点。难点在于找一个很好的去噪参数,这里取为13。
  2. 排序轮廓:在提取轮廓的边界框后,通过sorted函数按照y坐标和x坐标进行排序,确保从左到右、从上到下的顺序。难点在于从上到下不能单单以y值作为排序依据,因为我们手写的时候不能确保一行的字的起始y值是一样的,所以要给出一定的浮动,这个浮动值也比较难调,这里取230

下面是改进后的代码:

import cv2
import numpy as np
import os

def split_image_into_digits(image_path, output_dir):
    # 加载图片
    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # 反转颜色
    image = cv2.bitwise_not(image)

    # 去噪处理
    image = cv2.medianBlur(image, 13)

    # 二值化
    _, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # 查找轮廓
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 创建一个目录存储分割后的数字图像
    os.makedirs(output_dir, exist_ok=True)

    # 提取轮廓的边界框
    bounding_boxes = [cv2.boundingRect(contour) for contour in contours]

    # 排序边界框,首先按y坐标排序,再按x坐标排序,并考虑一定的浮动
    def sort_key(box):
        x, y, w, h = box
        return (y // 230, x)  # 使用y // 10给出一定的浮动

    bounding_boxes = sorted(bounding_boxes, key=sort_key)

    # 遍历边界框并保存每个数字图像
    for i, (x, y, w, h) in enumerate(bounding_boxes):
        # 提取数字图像
        digit_image = image[y:y+h, x:x+w]

        # 保存图像
        cv2.imwrite(f"{output_dir}/digit_{i}.png", digit_image)

    print("数字图像已保存至目录:", output_dir)
  
 
# split_image_into_digits使用示例
image_path = "./imgs/shuzi_da.png"
output_dir = "./imgs_output"
split_image_into_digits = split_image_into_digits(image_path, output_dir)

经检查,所有数字都按照顺序被分割了出来🎉

image-20240701162427658

步骤二:小图识别及可视化展示

将封装好的模型函数图片分割函数组合起来,即可实现一段手写数字的识别,我们采取可视化的方式进行直观展示。

问题描述

识别率很低,模型对于下面这种会先拉伸成28*28的正方形,从而导致识别正确率下降。

image-20240701163750007

问题解决

在分割时就进行白色填充。填充规则为正方形的长宽变为28的整数倍,并且数字的上下起码要空出18的距离。

ps. 如果文字太过贴边会降低识别正确率,就譬如下面这张图很容易识别成1(白色的为图片轮廓,黑色的不属于图片部分):

image-20240701164432812
    # 遍历边界框并保存每个数字图像,并进行黑白反转和填充
    for i, (x, y, w, h) in enumerate(bounding_boxes):
        # 提取数字图像
        digit_image = image[y:y+h, x:x+w]

        # 黑白反转
        digit_image = cv2.bitwise_not(digit_image)

        # 将图像填充到正方形,使数字居中
        size = max(w, h)
        square_image = np.ones((size, size), dtype=np.uint8) * 255  # 创建一个白色正方形背景
        x_offset = (size - w) // 2
        y_offset = (size - h) // 2
        square_image[y_offset:y_offset+h, x_offset:x_offset+w] = digit_image

        # 保存图像
        cv2.imwrite(f"{output_dir}/digit_{i}.png", square_image)

下面是完整的整合代码:

import torch
import torchvision
from PIL import Image
from torch import nn
import cv2
import os
import numpy as np
import matplotlib.pyplot as plt

# 该函数用于预测图片中的单个数字
def predict_digit(image_path):
    # 打开图片并转换为灰度图像
    image = Image.open(image_path).convert('L')  
  
    # 黑白反转
    image = torchvision.transforms.functional.invert(image)
  
    # 预处理图片
    transform = torchvision.transforms.Compose(
        [torchvision.transforms.Resize((28, 28)),
         torchvision.transforms.ToTensor(),
         torchvision.transforms.Normalize((0.5,), (0.5,))]
    )
    image = transform(image)
  
    # 加载预训练模型
    from ShouXieShiBie_model import GuoHangJiang
    model = torch.load("ghj_18.pth", map_location=torch.device('cpu'))
  
    # 添加batch size维度
    image = torch.reshape(image, (1, 1, 28, 28))
  
    # 预测
    model.eval()
    with torch.no_grad():
        output = model(image)
  
    # 返回预测结果
    return torch.argmax(output, 1).item()


# 该函数负责将一张大图中的多个数字分割成小图片
def split_image_into_digits(image_path, output_dir):
    # 加载图片
    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
  
    # 检查图片是否加载成功
    if image is None:
        raise ValueError(f"无法加载图像,请检查路径是否正确: {image_path}")
  
    # 反转颜色
    image = cv2.bitwise_not(image)

    # 去噪处理
    image = cv2.medianBlur(image, 13)

    # 二值化
    _, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    # 查找轮廓
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 创建一个目录存储分割后的数字图像
    os.makedirs(output_dir, exist_ok=True)

    # 提取轮廓的边界框
    bounding_boxes = [cv2.boundingRect(contour) for contour in contours]

    # 排序边界框,首先按y坐标排序,再按x坐标排序,并考虑一定的浮动
    def sort_key(box):
        x, y, w, h = box
        return (y // 230, x)  # 使用y // 10给出一定的浮动

    bounding_boxes = sorted(bounding_boxes, key=sort_key)

    # 遍历边界框并保存每个数字图像,并进行白色填充
    for i, (x, y, w, h) in enumerate(bounding_boxes):
        # 提取数字图像
        digit_image = image[y:y+h, x:x+w]

        # 计算填充后的正方形边长,并确保上下至少有18像素的间距
        target_size = max(w, h + 36)
        target_size = ((target_size + 27) // 28) * 28  # 调整为28的整数倍

        # 创建一个新的正方形图像,并将数字图像居中放置
        padded_image = np.ones((target_size, target_size), dtype=np.uint8) * 0
        y_offset = (target_size - h) // 2
        x_offset = (target_size - w) // 2
        padded_image[y_offset:y_offset+h, x_offset:x_offset+w] = digit_image

        # 反转颜色,恢复为黑字白底
        padded_image = cv2.bitwise_not(padded_image)

        # 保存图像
        cv2.imwrite(f"{output_dir}/digit_{i}.png", padded_image)

    print("数字图像已保存至目录:", output_dir)

# 该函数负责对大图中的所有数字进行识别,并以可视化方式展示
def recognize_and_visualize(image_path, output_dir):
    split_image_into_digits(image_path, output_dir)
  
    digit_images = sorted(os.listdir(output_dir), key=lambda x: int(x.split('_')[1].split('.')[0]))
    predictions = []
  
    for digit_image in digit_images:
        digit_path = os.path.join(output_dir, digit_image)
        prediction = predict_digit(digit_path)
        predictions.append(prediction)
  
    # 可视化结果,每行显示10个数字
    num_digits = len(predictions)
    num_cols = 10
    num_rows = (num_digits + num_cols - 1) // num_cols
  
    fig, axes = plt.subplots(num_rows, num_cols, figsize=(num_cols, num_rows))
    axes = axes.flatten()
  
    for i, digit_image in enumerate(digit_images):
        digit_path = os.path.join(output_dir, digit_image)
        img = Image.open(digit_path).convert('L')
        axes[i].imshow(img, cmap='gray')
        axes[i].set_title(str(predictions[i]))
        axes[i].axis('off')
  
    for i in range(num_digits, len(axes)):
        axes[i].axis('off')
  
    plt.tight_layout()
    plt.show()

# 示例调用
image_path = "./imgs/shuzi_da.png"
output_dir = "./imgs_output"
recognize_and_visualize(image_path, output_dir)

运行结果如下:

image-20240701170209507

至此,成功实现了一段手写数字的识别。