PyTorch入门--手写数字识别项目

概述

本文整理自BiliBli的《孔工码字》, 这是一个很好的视频号。讲的非常好,整理在这里,自己学习
他的Gitee地址:https://gitee.com/kongfanhe
本文通过手写数字识别项目来学习如何搭建训练神经网络。

PyTorch框架

在这个项目里,我们使用PyTorch框架,它是由Facebook开发的机器学习工具,
和其他框架比较,它的特点是代码简洁,符合人类思维,上手快, 可以用很少的代码完成机器学习任务。

MNIST数据集

在本项目中,我们使用MNIST手写数字图片数据集,它包含7万张图片(6万张训练图集和1万张测试集)

训练集用来调整神经网络的参数,测试集用来评估神经网络性能。
MNIST每张图片大小28*28像素,灰度值范围在0~255之间,每张图片配有一个标记,标记图片的真实值

讨论:什么是神经网络,它是怎样识别图片的

假设有1张5*5的黑白图片,值是7
如何设计神经网络,识别这个值?

训练过程

分层

首先将像素重新排列成1维阵列, 共25个像素, 构成神经网路的第0层节点,每个节点值为0或1

接下来构造神经网络的第1层节点。这1层的节点值由前一层计算得到。
计算方法:假设第0层节点的数值是x0-0,x0-1,x0-2,直到x0-24, 则第1层第0个节点的数值x1-0如下计算:

用求和公式简化一下:


这里每组(a,b)参数, 对应1条连接线, 字母i表示前1层的节点序号。
同理,可计算其他节点的数值。

字母j表示这1层节点的序号。
类似的构建2,3,4层网络
节点传播公式,也做相应的扩展

这里的字母k和k+1表示网络层数。
图像信息通过节点传播公式,一直传播到网络最后1层,
最后1层也叫输出层,公式中的所有(a,b),都是网络参数。

输出层

输出层上刚好有10个节点,对应10个数字。让每个节点,对应1种可能。

节点上的数值是概率, 第n个节点,表示这张图片是数字n的概率。

softmax归一化:让输出层的每个节点取值0~1之间,总和为1

我们观察到公式中的a和b是任意的,所以输出层节点上的数值也是任意的。
概率的取值范围是0~1,10个概率加在一起,等于1.
我们需要做1次数学变换, 让输出层的每个节点取值0~1之间,总和为1.
如何操作?
首先用自然常数e对每个X做指数运算,让每个数变成正数, 再求和,并用这个总和当分母,得到一组0~1之间且总和为1的数组

这个变换叫softmax归一化。
我们的网络,通过softmax归一化,得到1组看起来像概率的数值。

训练:缩小差值

但它还不是真的概率,如何让输出层具有真是的概率含义?“训练
现在,让网络对提供的图片进行计算,因为网络参数是随机的,所以输出也是随机的。
假设输出的概率分布如下:

我们知道图片代表数字7, 不符合理想情况下的概率分布,我们的目标是缩小差值。

如何缩小,调整网络参数(a,b)
这里有很多中算法,比如梯度下降算法,ADAM算法等等。
不管怎样,a,b是可以求解的
神经网络问题,变成了最优化问题。
几万张图片,重复改过程几万次,直到获得一组合适的网络参数。
这样,这个神经网络就具备了预测能力。
以上过程就是训练。

回顾:

  • 将图像转换为1为像素阵列,输入到神经网络
  • 通过节点间的计算公式,图像信息传播到输出层
  • 通过softmax归一化, 得到了一个概率分布
  • 再通过大量图像数据的训练,不断调整网络参数,让概率分布接近真实值
    所以,神经网络的本质就是一个数学函数, 训练的过程就是调整函数中的参数

batch

每次用1张图片调整参数,效率很低,通常会把几张图片打包成一个批次,一起发给神经网络来调整参数。

这个批次,叫1个batch。

激活函数:非线性转换

我们再次观察一下这个神经网络,

所有节点间计算都是线性的,所有网路的总输入和总输出,也是线性的。
但对很多问题,输入输出存在着非线性。
一个线性函数,无论怎么样调整,都无法模拟出非线性行为。
需要在网络中,引入一些非线性。做法很简单,在每个节点数值上,套上一个非线性函数f(),也叫激活函数。
激活函数有很多选择,常见的激活函数有对数函数,双曲函数,整流函数等。

本项目中,我们使用整流函数。
它在x<0的范围内,输出y=0, 在x>0的范围内,输出y=x。
换个角度理解:当x>0时,节点好像被激活了一样,这也是激活函数名称的由来

代码

安装依赖库

pip install numpy torch torchvision torchaudio matplotlib

代码

import torch
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import MNIST
import matplotlib.pyplot as plt


# 定义一个Net类, 是神经网络的主题
class Net(torch.nn.Module):

    def __init__(self):
        super().__init__()
        # 包含4个全连接层
        # 输入为28*28的图像
        # 中间三层放了64个节点
        self.fc1 = torch.nn.Linear(28*28, 64)
        self.fc2 = torch.nn.Linear(64, 64)
        self.fc3 = torch.nn.Linear(64, 64)
        # 输出为10个数字类别
        self.fc4 = torch.nn.Linear(64, 10)
    
    # 定义了前向传播过程
    # x是图像输入
    # 每层传播中
    #     先做全连接线性计算: self.fc*
    #     再套上一个激活函数: torch.nn.functional.relu
    def forward(self, x):
        x = torch.nn.functional.relu(self.fc1(x))
        x = torch.nn.functional.relu(self.fc2(x))
        x = torch.nn.functional.relu(self.fc3(x))
        # 输出层通过softmax归一化:
        #      log_softmax是为了提高计算的稳定性,在log_softmax之外,又套上了一个对数运算
        x = torch.nn.functional.log_softmax(self.fc4(x), dim=1)
        return x

# 导入数据
def get_data_loader(is_train):
    # 首先定义数据转换类型:ToTensor
    #      Tensor就是一个多维数组, 中文叫张量,是常见概念
    to_tensor = transforms.Compose([transforms.ToTensor()])
    
    # 下载MNIST数据集
    # 第一个参数是下载目录
    # is_train: 导入训练集还是测试集
    data_set = MNIST("", is_train, transform=to_tensor, download=True)
    # batch_size=15: 一个批次包含15张图片
    # shuffle=True:表示数据是随机打乱的
    # 最后,返回数据加载器
    return DataLoader(data_set, batch_size=15, shuffle=True)

# 评估神经网络的识别正确率
def evaluate(test_data, net):
    n_correct = 0
    n_total = 0
    with torch.no_grad():
        # 从测试集中按批次去除数据
        # 计算神经网络的预测值, 与结果进行比较, 累加正确预测的数量
        # argmax函数计算一个税额中最大值的序号, 也就是预测的手写结果
        for (x, y) in test_data:
            outputs = net.forward(x.view(-1, 28*28))
            for i, output in enumerate(outputs):
                if torch.argmax(output) == y[i]:
                    n_correct += 1
                n_total += 1
    # 返回正确率
    return n_correct / n_total


def main():
    # 导入训练集和测试集
    train_data = get_data_loader(is_train=True)
    test_data = get_data_loader(is_train=False)
    
    # 初始化神经网络
    net = Net()
    
    # 打印初始网络的正确率
    print("initial accuracy:", evaluate(test_data, net))
    # 训练神经网络, 是pytorch的固定写法
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    # epoch:有时需要在一个数据集上反复训练神经网络几个轮次,可以提高数据集的利用率
    # 每个轮次就是一个epoch
    for epoch in range(2):
        for (x, y) in train_data:
            # 初始化
            net.zero_grad()
            # 正向传播
            output = net.forward(x.view(-1, 28*28))
            # 计算插值
            #     nll_loss: 是一个对数损失函数, 是为了匹配前面log_softmax的对数运算
            loss = torch.nn.functional.nll_loss(output, y)
            # 反向误差传播
            loss.backward()
            # 优化网络参数
            optimizer.step()
        print("epoch", epoch, "accuracy:", evaluate(test_data, net))

    # 训练完成后,随机抽取3张图像,显示网络的预测结果
    for (n, (x, _)) in enumerate(test_data):
        if n > 3:
            break
        predict = torch.argmax(net.forward(x[0].view(-1, 28*28)))
        plt.figure(n)
        plt.imshow(x[0].view(28, 28))
        plt.title("prediction: " + str(int(predict)))
    plt.show()


if __name__ == "__main__":
    main()

执行结果

第1次启动时,会下载MNIST数据集

扩展: 在28*28的画布上手写数字进行识别:

借助 Python 的 tkinter 库来创建图形用户界面(GUI),结合 PIL 库处理手写图像

import torch
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import MNIST
import matplotlib.pyplot as plt
import tkinter as tk
from PIL import Image, ImageDraw

# 定义一个Net类, 是神经网络的主题
class Net(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # 包含4个全连接层
        # 输入为28*28的图像
        # 中间三层放了64个节点
        self.fc1 = torch.nn.Linear(28*28, 64)
        self.fc2 = torch.nn.Linear(64, 64)
        self.fc3 = torch.nn.Linear(64, 64)
        # 输出为10个数字类别
        self.fc4 = torch.nn.Linear(64, 10)

    # 定义了前向传播过程
    # x是图像输入
    # 每层传播中
    #     先做全连接线性计算: self.fc*
    #     再套上一个激活函数: torch.nn.functional.relu
    def forward(self, x):
        x = torch.nn.functional.relu(self.fc1(x))
        x = torch.nn.functional.relu(self.fc2(x))
        x = torch.nn.functional.relu(self.fc3(x))
        # 输出层通过softmax归一化:
        #      log_softmax是为了提高计算的稳定性,在log_softmax之外,又套上了一个对数运算
        x = torch.nn.functional.log_softmax(self.fc4(x), dim=1)
        return x

# 导入数据
def get_data_loader(is_train):
    # 首先定义数据转换类型:ToTensor
    #      Tensor就是一个多维数组, 中文叫张量,是常见概念
    to_tensor = transforms.Compose([transforms.ToTensor()])

    # 下载MNIST数据集
    # 第一个参数是下载目录
    # is_train: 导入训练集还是测试集
    data_set = MNIST("", is_train, transform=to_tensor, download=True)
    # batch_size=15: 一个批次包含15张图片
    # shuffle=True:表示数据是随机打乱的
    # 最后,返回数据加载器
    return DataLoader(data_set, batch_size=15, shuffle=True)

# 评估神经网络的识别正确率
def evaluate(test_data, net):
    n_correct = 0
    n_total = 0
    with torch.no_grad():
        # 从测试集中按批次去除数据
        # 计算神经网络的预测值, 与结果进行比较, 累加正确预测的数量
        # argmax函数计算一个税额中最大值的序号, 也就是预测的手写结果
        for (x, y) in test_data:
            outputs = net.forward(x.view(-1, 28*28))
            for i, output in enumerate(outputs):
                if torch.argmax(output) == y[i]:
                    n_correct += 1
                n_total += 1
    # 返回正确率
    return n_correct / n_total

def main():
    # 导入训练集和测试集
    train_data = get_data_loader(is_train=True)
    test_data = get_data_loader(is_train=False)

    # 初始化神经网络
    net = Net()

    # 打印初始网络的正确率
    print("initial accuracy:", evaluate(test_data, net))
    # 训练神经网络, 是pytorch的固定写法
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    # epoch:有时需要在一个数据集上反复训练神经网络几个轮次,可以提高数据集的利用率
    # 每个轮次就是一个epoch
    for epoch in range(2):
        for (x, y) in train_data:
            # 初始化
            net.zero_grad()
            # 正向传播
            output = net.forward(x.view(-1, 28*28))
            # 计算插值
            #     nll_loss: 是一个对数损失函数, 是为了匹配前面log_softmax的对数运算
            loss = torch.nn.functional.nll_loss(output, y)
            # 反向误差传播
            loss.backward()
            # 优化网络参数
            optimizer.step()
        print("epoch", epoch, "accuracy:", evaluate(test_data, net))

    # 创建一个Tkinter窗口
    root = tk.Tk()
    root.title("手写数字识别")

    # 创建一个28x28的画布
    canvas_width = 280
    canvas_height = 280
    canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg="white")
    canvas.pack()

    # 绘制表格
    for i in range(1, 28):
        canvas.create_line(i * 10, 0, i * 10, canvas_height, fill="gray")
        canvas.create_line(0, i * 10, canvas_width, i * 10, fill="gray")

    # 创建一个PIL图像用于保存手写数据
    image = Image.new("L", (280, 280), 0)
    draw = ImageDraw.Draw(image)

    # 鼠标拖动事件处理函数
    def paint(event):
        x1, y1 = (event.x - 5), (event.y - 5)
        x2, y2 = (event.x + 5), (event.y + 5)
        canvas.create_oval(x1, y1, x2, y2, fill="black", width=10)
        draw.ellipse([x1, y1, x2, y2], fill=255, width=10)

    canvas.bind("<B1-Motion>", paint)

    # 识别按钮点击事件处理函数
    def recognize():
        # 调整图像大小为28x28,使用Image.LANCZOS替代Image.ANTIALIAS
        resized_image = image.resize((28, 28), Image.LANCZOS)
        # 将图像转换为Tensor
        to_tensor = transforms.ToTensor()
        tensor_image = to_tensor(resized_image).view(-1, 28*28)
        # 进行预测
        with torch.no_grad():
            output = net.forward(tensor_image)
            predict = torch.argmax(output)
        # 显示预测结果
        result_label.config(text=f"预测结果: {int(predict)}")

    # 清除按钮点击事件处理函数
    def clear():
        canvas.delete("all")
        draw.rectangle([0, 0, 280, 280], fill=0)
        result_label.config(text="预测结果: ")
        # 重新绘制表格
        for i in range(1, 28):
            canvas.create_line(i * 10, 0, i * 10, canvas_height, fill="gray")
            canvas.create_line(0, i * 10, canvas_width, i * 10, fill="gray")

    # 创建识别按钮
    recognize_button = tk.Button(root, text="识别", command=recognize)
    recognize_button.pack()

    # 创建清除按钮
    clear_button = tk.Button(root, text="清除", command=clear)
    clear_button.pack()

    # 创建显示预测结果的标签
    result_label = tk.Label(root, text="预测结果: ")
    result_label.pack()

    root.mainloop()

if __name__ == "__main__":
    main()

三轮训练的预测正确率不高啊


多轮训练的正确率:

另一个版本的代码

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import tkinter as tk
from PIL import Image, ImageDraw

# 定义数据预处理
transform = transforms.Compose([
    transforms.ToTensor(),  # 将图像转换为张量
    transforms.Normalize((0.1307,), (0.3081,))  # 归一化处理
])

# 加载训练集和测试集,返回数据加载器
trainset = torchvision.datasets.MNIST(root='./data', train=True,
                                      download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=64, shuffle=True)

testset = torchvision.datasets.MNIST(root='./data', train=False,
                                     download=True, transform=transform)

testloader = DataLoader(testset, batch_size=64, shuffle=False)

# 定义卷积神经网络模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.fc1 = nn.Linear(20 * 4 * 4, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))
        x = self.pool(torch.relu(self.conv2(x)))
        x = x.view(-1, 20 * 4 * 4)
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# 初始化模型、损失函数和优化器
model = SimpleCNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

# 训练模型
def train_model(model, trainloader, criterion, optimizer, epochs):
    model.train()
    for epoch in range(epochs):
        running_loss = 0.0
        for i, data in enumerate(trainloader, 0):
            inputs, labels = data
            optimizer.zero_grad()

            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            running_loss += loss.item()
            if i % 200 == 199:
                print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 200:.3f}')
                running_loss = 0.0

# 测试模型
def test_model(model, testloader):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for data in testloader:
            images, labels = data
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Accuracy of the network on the 10000 test images: {100 * correct / total}%')

# 训练和测试模型
train_model(model, trainloader, criterion, optimizer, epochs=5)
test_model(model, testloader)

# 创建一个Tkinter窗口
root = tk.Tk()
root.title("手写数字识别")

# 创建一个28x28的画布(实际显示为280x280以方便手写)
canvas_width = 280
canvas_height = 280
canvas = tk.Canvas(root, width=canvas_width, height=canvas_height, bg="white")
canvas.pack()

# 绘制表格
for i in range(1, 28):
    canvas.create_line(i * 10, 0, i * 10, canvas_height, fill="gray")
    canvas.create_line(0, i * 10, canvas_width, i * 10, fill="gray")

# 创建一个PIL图像用于保存手写数据
image = Image.new("L", (280, 280), 0)
draw = ImageDraw.Draw(image)

# 鼠标拖动事件处理函数
def paint(event):
    x1, y1 = (event.x - 5), (event.y - 5)
    x2, y2 = (event.x + 5), (event.y + 5)
    canvas.create_oval(x1, y1, x2, y2, fill="black", width=10)
    draw.ellipse([x1, y1, x2, y2], fill=255, width=10)

canvas.bind("<B1-Motion>", paint)

# 识别按钮点击事件处理函数
def recognize():
    # 调整图像大小为28x28
    resized_image = image.resize((28, 28), Image.LANCZOS)
    # 应用与训练数据相同的预处理
    input_tensor = transform(resized_image).unsqueeze(0)
    # 进行预测
    model.eval()
    with torch.no_grad():
        output = model(input_tensor)
        _, predicted = torch.max(output.data, 1)
    # 显示预测结果
    result_label.config(text=f"预测结果: {predicted.item()}")

# 清除按钮点击事件处理函数
def clear():
    canvas.delete("all")
    draw.rectangle([0, 0, 280, 280], fill=0)
    result_label.config(text="预测结果: ")
    # 重新绘制表格
    for i in range(1, 28):
        canvas.create_line(i * 10, 0, i * 10, canvas_height, fill="gray")
        canvas.create_line(0, i * 10, canvas_width, i * 10, fill="gray")

# 创建识别按钮
recognize_button = tk.Button(root, text="识别", command=recognize)
recognize_button.pack()

# 创建清除按钮
clear_button = tk.Button(root, text="清除", command=clear)
clear_button.pack()

# 创建显示预测结果的标签
result_label = tk.Label(root, text="预测结果: ")
result_label.pack()

root.mainloop()
posted @   荣--  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
点击右上角即可分享
微信分享提示