【机器学习入门】使用pytorch实现手写数字识别

系列文章目录

第1章 专家系统
第2章 决策树
第3章 神经元和感知机
识别手写数字——感知机
第4章 线性回归
第5章 逻辑斯蒂回归和分类
第5章 支持向量机
第6章 人工神经网络(一)
第6章 人工神经网络(二) 卷积和池化



一、使用PyTorch的优点

  1. 支持硬件加速计算。

计算机有限的算力是神经网络早期发展受限的原因之一。很多实际问题需要神经网络有足够的规模和深度,仅仅采用CPU 的计算能力、训练需要的时间将远远超出人们可以接受的程度。在神经网络的计算过程中,向量和矩阵运算出现的频率很高,这些运算在CPU 上只能逐个元素依次串行计算,而在GPU上则可以进行批量并行计算,计算速度大幅提高。

  1. 提供常用神经网络模块,帮助我们快速构建神经网络。

这得益于神经网络计算单元的模块化。不同网络结构通常是有限的常用单元的组合,将常用单元模块化,构建神经网络的过程就变成了"搭积木",我们只要选择合适的模块,调整参数,然后拼接组合,就可以实现复杂的网络结构。比如,卷积层、全连接层、池化层就是常用的神经网络模块,大部分软件包都提供这些模块。

  1. 实现自动求导。

训练神经网络实际上就是利用梯度下降法求解模型参数的最优化问题。这个过程离不开计算损失函数的梯度,也就是求误差对模型参数的导数。早期的神经网络软件包侧重于神经网络的模块化,当我们需要利用基本运算和数学函数构建自定义的模块时,实现求导过程比前向计算困难得多。自动求导功能把我们从这个困境中解脱出来。当前主流的软件包能够把所有基本运算和函数都实现为可求导的,然后利用导数运算法则自动计算它们组合后的导数。

pytorch 的安装这里就暂时省略啦

二、手写数字识别

通过识别手写数字的例子学习一下如何使用PyTorch搭建神经网络模型。
手写数字识别是一个图像分类问题,可以用较小规模网络完成,很适合作为新手实践项目。处理自然图片的任务通常需要更大规模的网络,这意味着网络的层数大大增加,每层的卷积核数量也大大增加,从而才能描述和分辨自然图像中丰富而且复杂的特征,为数以千计的不同类别物体视觉特征提供具有表达力的中间表示。但是,无论多么复杂的网络基本结构都是相似的,处理图像的核心模块都是卷积层和全连接层。

网络的设计

实验采用PyTorch软件包附带的MNIST 手写数字数据集。数据集中的手写数字图像是灰度图片,只有一个通道,尺寸是28x28像素。我们首先用一个卷积核尺寸为3x3的二维卷积层对它进行处理。假设这一层有8个卷积核,那么处理后的结果是8个28x28的通道。然后用2x2的池化层将数据降采样为14x14x8个值。
下面开始第2个卷积层,这里我们会看到如何处理多通道的数据。其实,如果输入图像是彩色的,在第1个卷积层就面临多通道数据了(比如RGB图像有3个通道的数据)。我们仍然采用3x3的卷积核,由于输入有8个通道,那么每个卷积核的权值数量就是3x3x8。第2层卷积核数量通常要大于前一层,因为每个卷积核所表示的图像范围增大了,因此需要更多卷积核来表示更多不同的局部特征。这里使用了 16个卷积核,再次经过2x2的池化层,得到了7x7x16个输出值。
在全部卷积层结束之后,我们要用全连接层实现分类。为了确保能够处理非线性数据,采用两个全连接层。第1个全连接层有64个单元,它们每一个都有7x7x16个输入,因此,这个全连接层有64x7x7x16个权值。第2个全连接层产生用于分类的输出,每个表示一个数字,因此有10个单元,这一层需要640个权值。
注意每个卷积操作后都有一个激活函数,全连接层前也有一次激活函数的操作,而第2个全连接层不再使用激活函数,直接用于SoftMax分类以及交叉熵损失函数。
在这里插入图片描述

import torch
import torch.nn as nn
import torch.nn.functional as F

class MnistNet(nn.Module):
    def __init__(self):
        super(MnistNet, self).__init__()
        # 在构造函数中 定义组成网络的各层模块
        # 首先定义第 1 个 二维卷积层
        # 第1层具有1个输入通道,8个输出通道,也就是8个卷积核
        # 每个卷积核大小为3*3
        # 输入各边补齐1行(或1列),以便输出尺寸与输入一致
        self.conv1 = nn.Conv2d(1,8, kernel_size=3, padding=1)
        # 第2个卷积层承接上一层的输出, 即8个输入通道
        # 这一层有16个输出通道,即16个卷积核
        self.conv2 = nn.Conv2d(8,16,kernel_size=3,padding=1)

        # 下面定义全连接层
        # 第1个全连接层承接卷积层的输出
        self.fc1 = nn.Linear(7*7*16, 64)
        # 第2个全连接层 产生最终10个类别
        self.fc2 = nn.Linear(64, 10)

    def forward(self, x):
        # 每个卷积层使用ReLU激活函数
        # 并使用Max Pooling进行压缩
        x = F.max_pool2d(F.relu(self.conv1(x)),2)
        x = F.max_pool2d(F.relu(self.conv2(x)),2)
        # 将卷积层的输出拉平为 长度为7*7*16 的向量
        # 作为全连接层的输入
        x = x.view(-1, 7*7*16)
        x = F.relu(self.fc1(x))
        # 第2个 全连接层不再使用激活函数
        # 直接用于 Softmax分类 以及 交叉熵误差计算
        return self.fc2(x)

# 构造测试输入
# 输入总是包含一个批次的若干样本
# 第1维 是样本编号, 第2维是通道编号
# 第3、4维是图像的高度和宽度
# 下面的随机输入包含一个样本,一个通道
x = torch.rand((1,1,28,28))
net = MnistNet()
y = net(x)
# 输出一个1*10的张量
print(y)

关于上面代码的一些解释:
在这里插入图片描述

在这里插入图片描述
下面是一个例子,展示了如何在自定义的 MnistNet 类中使用 super() 来调用 nn.Module 的构造方法:

import torch.nn as nn

class MnistNet(nn.Module):
    def __init__(self):
        # 首先调用父类的构造方法
        super(MnistNet, self).__init__()
        # 然后定义子类的特定层和参数
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        # 定义前向传播逻辑
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2(x), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

在这个例子中,MnistNet 类继承自 nn.Module。在 MnistNet 的构造方法中,首先使用 super(MnistNet, self).init 调用父类的构造方法,然后定义了子类特有的层和参数。这样,MnistNet 类就可以使用 nn.Module 提供的所有功能,并且可以添加自己的特定行为。

nn.Conv2d是PyTorch中用于创建二维卷积层的类。创建一个nn.Conv2d层时,你需要传入以下参数:
in_channels (int):输入图像的通道数。例如,对于RGB图像,通道数为3。
out_channels (int):卷积层输出的通道数,也就是卷积核(过滤器)的数量。
kernel_size (int or tuple):卷积核的大小。可以是一个整数,表示卷积核的高度和宽度都是这个值;也可以是一个形如(height, width)的元组,分别指定卷积核的高度和宽度。
stride (int or tuple, optional):卷积步长。默认值为1。可以是一个整数或形如(stride_height, stride_width)的元组。步长决定了卷积核滑动的间隔。
padding (int or tuple or string, optional):输入的每一边添加的零填充的数量。可以是一个整数或形如(pad_height, pad_width)的元组,或者是一个字符串,如’same’(某些框架支持),表示自动计算padding使得输出尺寸与输入尺寸相同。默认值为0。
dilation (int or tuple, optional):卷积核元素之间的间距。默认值为1。可以是一个整数或形如(dilation_height, dilation_width)的元组。
groups (int, optional):将输入和输出通道分组,以便每组进行独立的卷积。默认值为1,表示不分组,即所有通道一起卷积。
bias (bool, optional):如果设置为True,则向输出添加一个可学习的偏置项。默认值为True。
padding_mode (string, optional):指定填充策略。默认为’zeros’,表示使用零填充。其他值如’reflect’、'replicate’或’circular’等,表示不同的填充方式。

nn.Linear是PyTorch中用于创建全连接层(线性层)的类。创建一个nn.Linear层时,你需要传入以下参数:
in_features (int):输入特征的数量。这是指输入数据中每个样本的维度,例如,如果你的输入是一个大小为(batch_size, in_features)的张量,那么in_features就是这个维度的值。
out_features (int):输出特征的数量。这是全连接层的输出维度,即该层将映射到的下一个层的输入维度。
除了这两个必需的参数之外,nn.Linear没有其他参数,因为它只实现了线性变换(即y = x * weight + bias),其中weight是层的参数,bias是可选的偏置项。
bias (bool, optional):是否添加可学习的偏置项。默认情况下,bias被设置为True,这意味着层将会有一个偏置参数。如果你设置bias=False,则层不会学习偏置参数。
创建nn.Linear层后,你可以通过其weight和bias属性访问和操作这些参数。例如,你可以打印权重查看其形状,或者在训练过程中更新它们。
这是一个简单的例子,展示了如何使用nn.Linear创建一个全连接层:

import torch.nn as nn

# 创建一个全连接层,输入特征数为10,输出特征数为5
linear_layer = nn.Linear(in_features=10, out_features=5)

# 打印层的权重和偏置
print(linear_layer.weight)
print(linear_layer.bias)

F.max_pool2d 是 PyTorch 中的一个函数,用于执行最大池化操作,这是一种常用的局部区域下采样技术。最大池化通过从输入的特征图中提取最大值来减少数据的空间维度(高度和宽度),同时保留重要的特征信息。这有助于减少计算量,并且可以提供一定程度的平移不变性。
下面是 F.max_pool2d 函数的参数说明:
input (Tensor):输入的四维张量,形状为 (batch_size, channels, height, width),代表一批具有多个通道的图像。
kernel_size (int or tuple):池化窗口的大小。可以是一个整数,表示高度和宽度都是这个值;也可以是一个形如 (height, width) 的元组,分别指定高度和宽度。
stride (int or tuple, optional):池化时的步长。可以是一个整数或形如 (stride_height, stride_width) 的元组。默认值为 kernel_size。
padding (int or tuple or string, optional):输入的每一边添加的零填充的数量。可以是一个整数或形如 (pad_height, pad_width) 的元组,或者是一个字符串,如 ‘circular’ 或 ‘same’(某些框架支持),表示不同的填充方式。默认值为 0。
dilation (int or tuple, optional):池化窗口中元素之间的间距。默认值为 1,表示没有间距。
ceil_mode (bool, optional):当 padding 或 dilation 导致池化窗口大小不匹配时,是否使用 ceil 函数向上取整计算输出尺寸。默认值为 False。
return_indices (bool, optional):是否返回每个输出元素最大值的索引。这在某些应用中可能有用,例如在语义分割中。默认值为 False。
F.max_pool2d 函数返回两个值:池化后的输出张量和(如果启用了 return_indices)每个输出元素最大值的索引张量。

在PyTorch中,x.view方法是一种改变张量形状(shape)的操作。这个方法不接受任何参数,而是返回一个新的张量,其数据与原始张量x相同,但具有不同的形状。view方法通常用于重新排列张量的维度或者将多维数据展平(flatten)为一维或二维。
x.view(-1, 7716)是一个将张量x重新塑形的操作。这里的-1是一个非常特殊的参数,它告诉PyTorch自动计算这个维度的大小,以便保持张量中元素的总数不变。

训练数据的处理

机器学习模型寻找数据中的统计规律,数据噪声越少、偏差越小,产生的模型就越准确。因此,我们要为数据集准备准确的标注(比如样本的类标签)。实际数据常常是不规则的、充满噪声的,甚至是有偏差的,比如各种类别的样本数量不均衡,图像有各种不同尺寸,样本标签有错误或者系统性偏差。这些情况都要在准备数据的阶段进行处理。
这里我们使用PyTorch软件包提供的数据集,大大简化了准备数据的工作。但是,我们仍然需要进行适当配置,比如,将图片读取为张量数据,将图片中的像素灰度值进行归一化,避免神经网络权值因为图片像素值大幅变化而产生扰动。
另外,我们通常将样本分批输入神经网络进行训练。每次训练一个样本不仅效率低下,而且不能产生正确的梯度信号。每个批次中应该包含各种不同类别的样本,避免出现"盲人摸象"的情况,使训练方向过于偏向某个类别的局部最优。同时PyTorch的数据加载器可以帮助我们将样本顺序打乱,防止样本顺序产生相关性,使训练过程陷入循环。

'''
准备训练数据
'''

import torchvision

# 由于数据集是图片, 需要将图片转化为张量
# 然后进行归一化, 将像素值调整到适合训练的范围
# 下面的数据变换完成这两个操作
data_transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize(mean=[0.5],std=[0.5])
])

# 下载MNIST数据集
# 将数据集保存在下面的路径中
data_path = 'data'
# 数据集包含训练集 和 测试集两部分数据
train_data = torchvision.datasets.MNIST('data',train=True,transform=data_transform,download=True)
test_data = torchvision.datasets.MNIST('data',train=False,transform=data_transform)

# 从数据集构造加载器
# 加载器每批次提取10个样本
# 乱序加载样本,避免顺序相关性
train_loader = torch.utils.data.DataLoader(train_data,batch_size=10,shuffle=True)
test_loader = torch.utils.data.DataLoader(test_data,batch_size=10,shuffle=True)

# 实验一下,读取第一批样本,看加载器是否在正常工作
train_input,labels = next(iter(train_loader))
print(train_input.shape)
# 输入的尺寸为10 x 1 x 28 x 28 包含10个样本
print(labels.shape)
# 目标输出的尺寸为10 ,包含10个标签

对以上代码的具体解释如下:

  1. 数据转换
    使用torchvision.transforms.Compose创建一个组合的数据转换流程。
    torchvision.transforms.ToTensor()将PIL图像或NumPy数组转换为PyTorch张量。
    torchvision.transforms.Normalize(mean=[0.5],std=[0.5])对张量进行归一化处理,其中mean是归一化时的均值,std是标准差。这里将所有通道的均值和标准差都设置为0.5,意味着数据会被缩放到均值为0.5,标准差为0.5的范围内。
    创建好 data_transform 之后,你可以将它作为参数传递给数据集加载器(如 torchvision.datasets 中的类),以便在数据加载过程中自动应用这些转换操作。这使得数据预处理过程简洁、高效,并且易于管理。
  2. 下载MNIST数据集
    使用torchvision.datasets.MNIST下载并加载MNIST数据集。MNIST是一个包含手写数字的图像数据集,常用于训练各种图像处理系统。
    data_path = 'data’指定了数据集的保存路径。
    train_data和test_data分别用于训练集和测试集的数据加载。
    train=True表示加载训练集,train=False表示加载测试集。
    transform=data_transform指定了之前创建的数据转换流程。
    download=True表示如果数据集没有被下载过,将会自动下载。
  3. 构造数据加载器
    构造数据加载器是深度学习训练过程中的一个重要步骤,它负责将数据集组织成批次并提供给模型进行训练。在PyTorch中,这是通过torch.utils.data.DataLoader类来实现的,它会批量加载数据,并可以选择是否打乱数据顺序。
    batch_size=10指定了每个批次的样本数量。
    shuffle=True表示在每个epoch开始时,会随机打乱数据顺序,这有助于提高模型的泛化能力。
  4. 读取第一批样本
    使用next(iter(train_loader))读取训练数据加载器的第一批数据。
    train_input是输入数据,其形状为(10, 1, 28, 28),表示有10个样本,每个样本是28x28像素的灰度图像,通道数为1。
    labels是对应的标签,其形状为(10,),表示有10个标签。

训练神经网络模型

首先检查是不是可以使用GPU设备进行加速计算,如果装备了GPU,则要把网络模型和训练数据都放置在GPU上计算。
然后,准备计算输出误差的损失函数和优化器。这是一个多分类问题,可以使用交叉熵损失函数。对于优化器,使用最基本的随机梯度下降优化器。

# 开始训练之前, 选择使用的计算设备
# 默认为CPU, 如果检测到了GPU ,可以使用GPU加速计算
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("device:",device)

# 准备网络模型
net = MnistNet()
# 将网络模型放在计算设备上, 如果使用CPU可以忽略这一步
net = net.to(device)
# 准备交叉熵损失函数
loss_func = torch.nn.CrossEntropyLoss()
# 准备随机梯度下降优化器, 设置学习率为0.001
optimizer = torch.optim.SGD(net.parameters() ,0.001)

下面是对这段代码的详细解释:

  1. 选择计算设备:
    device = torch.device(“cuda:0” if torch.cuda.is_available() else “cpu”):这行代码首先检查是否有可用的GPU。torch.cuda.is_available()函数会返回一个布尔值,指示系统是否有NVIDIA CUDA支持的GPU。如果有,device变量将被设置为"cuda:0",这表示使用第一个CUDA设备(通常是系统中唯一的GPU)。如果没有检测到GPU,device将被设置为"cpu",表示使用CPU进行计算。
    print(“device:”, device):打印出选择的计算设备。
  2. 准备网络模型:
    net = MnistNet():这行代码创建了一个MnistNet类的实例,这是一个用于手写数字识别的神经网络模型。这个模型的定义应该在代码的其他部分。
    net = net.to(device):将模型移动到之前选择的计算设备上。如果使用GPU,这一步是必要的,因为PyTorch要求所有的张量和模型都在同一个设备上(CPU或GPU)才能进行计算。如果使用CPU进行计算,这一步可以忽略,因为模型默认就在CPU上。
  3. 准备损失函数:
    loss_func = torch.nn.CrossEntropyLoss():这行代码创建了一个交叉熵损失函数的实例。交叉熵损失函数是分类任务中常用的损失函数,它可以衡量模型预测的概率分布与真实标签的概率分布之间的差异。
  4. 准备优化器:
    optimizer = torch.optim.SGD(net.parameters(), 0.001):这行代码创建了一个随机梯度下降(SGD)优化器的实例,并将其与模型的参数关联起来。0.001是学习率的值,它控制着优化过程中参数更新的步长。优化器负责更新模型的权重,以最小化损失函数。

补充:
net.parameters() 是 PyTorch 中的一个方法,它的作用是返回模型 net 中所有可训练参数(权重和偏置)的迭代器。这些参数是神经网络中需要通过训练数据进行学习和调整的数值。
当你创建一个神经网络模型的实例时,比如 net = MnistNet(),模型中的每一层都会自动创建相应的参数。例如,全连接层(nn.Linear)会创建权重矩阵和偏置向量,卷积层(nn.Conv2d)会创建卷积核的权重和偏置等。
调用 net.parameters() 时,你会得到一个生成器(generator),它按顺序产生模型中每一层的参数。这个生成器通常用于以下场景:
传递给优化器:当你创建一个优化器实例时,比如 optimizer = torch.optim.SGD(net.parameters(), lr=0.001),你需要将模型的参数传递给优化器。优化器会使用这些参数来更新模型的权重和偏置,以最小化损失函数。
打印或检查模型参数:你可以使用 net.parameters() 来检查模型中的参数数量、大小或内容。这对于调试和理解模型结构很有帮助。
应用参数更新:在训练循环中,优化器会使用 net.parameters() 来获取参数,并根据计算出的梯度来更新它们。
冻结部分参数:在某些训练场景中,你可能希望冻结模型的一部分参数,只训练另一部分。通过 net.parameters(),你可以轻松地迭代并选择性地冻结或解冻参数。
net.parameters() 是一个非常有用的工具,它使得管理和操作神经网络中的参数变得更加方便和高效。在实际应用中,它是训练和调整神经网络模型的关键部分。

下面可以开始训练模型。MNIST数据集包含60000张训练图片和10000张测试图片。
在训练过程中,我们仅采用训练图片来计算误差和梯度信号,更新网络模型的权值。避免训练样本数量过多,使模型陷入过拟合。
如何避免过拟合呢?
我们可以设计一个“信号”,告诉我们训练可以到此为止,继续进行下去,在训练集上的误差也许还会进一步减小,但是真实样本的分类准确度没有进一步上升的空间了,反而有可能随着过拟合而下降。
而测试集(或者验证集)的作用就是为我们提供一个这样的信号,这些样本没有加入训练过程作为产生梯度信号的数据来源,它们是模型没有见过的新样本。模型在这些新样本上的分类性能,可以作为模型在真实样本空间中的泛化能力的估计,帮助我们观察模型是否还有提升空间,还是已经过拟合。

# 设置训练循环次数
epochs = 3
for i in range(epochs):
    # 枚举训练集中的数据
    for index, (train_input, labels) in enumerate(train_loader):
        # 将网络置于训练状态
        # 某些层在训练和测试时行为不同, 比如批归一化层 和 Dropout层
        net.train(True)
        # 将数据放在计算设备上
        train_input = train_input.to(device)
        labels = labels.to(device)
        # 计算输出
        output = net(train_input)
        # 计算误差
        loss = loss_func(output, labels)
        # 清空优化器, 计算梯度
        optimizer.zero_grad()
        loss.backward()
        # 用梯度优化模型
        optimizer.step()

        # 每训练一段时间观察测试误差
        # 如果观察到测试误差不再继续降低 或者 训练误差开始低于测试误差
        # 说明出现过拟合, 应该停止训练
        if index % 1000 == 0:
            # 将网络置为测试状态
            net.train(False)
            test_input, test_labels = next(iter(test_loader))
            test_output = net(test_input)
            test_loss = loss_func(test_output, test_labels)
            # 输出训练误差 和 测试误差
            print('{0} 训练误差:{1:.4f} 测试误差{2:.4f}'.format(
                index,loss.item(), test_loss.item()
            ))

  1. 设置训练循环次数
    epochs = 3:定义了训练模型的总迭代次数,也就是遍历整个训练集的次数。这里设置为3次。
  2. 训练循环
    for i in range(epochs):外层循环控制训练的轮数(epoch)。
    枚举训练集中的数据:
    for index, (train_input, labels) in enumerate(train_loader):内层循环遍历每一个批次的数据。enumerate 同时提供了批次的索引 index 和数据 (train_input, labels)。
  3. 设置网络为训练状态
    net.train(True):将模型设置为训练模式。这会影响某些层的行为,如Dropout和BatchNorm。
  4. 将数据移动到计算设备
    train_input = train_input.to(device) 和 labels = labels.to(device):将输入数据和标签移动到之前选择的计算设备(CPU或GPU)。
  5. 计算模型输出
    output = net(train_input):通过模型传递输入数据,得到模型的预测输出。
  6. 计算误差(损失)
    loss = loss_func(output, labels):使用损失函数计算模型输出和真实标签之间的误差。
  7. 梯度清零并反向传播
    optimizer.zero_grad():清除之前的梯度信息,这是为了在每次迭代时开始新的梯度计算。
    loss.backward():通过损失值进行反向传播,计算每一层参数的梯度。
  8. 优化模型参数
    optimizer.step():根据计算得到的梯度更新模型的参数。
  9. 观察测试误差
    if index % 1000 == 0:每隔1000个批次,进行一次测试误差的计算和输出。
    net.train(False):将模型设置为评估模式,这会改变Dropout和BatchNorm等层的行为。
    test_input, test_labels = next(iter(test_loader)):获取测试集的下一个批次数据。
    test_output = net(test_input):计算测试数据的输出。
    test_loss = loss_func(test_output, test_labels):计算测试误差。
    print 语句输出当前批次的索引、训练误差和测试误差。

关于这部分代码,我不理解的两个点:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这里发现去掉梯度清零的语句后,拟合效果真的有下降!

在这里插入图片描述

三、保存模型进行预测

这一部分在上面训练好模型的代码基础上,保存模型,并上传一张自己的手写数字图片,将使用pillow将图片转成灰度图片后,再转换为代入我们训练好的网络模型所需要的数据格式,用保存好的模型来预测识别自己上传的手写数字。

保存模型

# 保存模型
# 保存模型的状态字典
torch.save(net.state_dict(), 'models/mnist_model.pth')

加载模型,创建实例

# 创建模型实例
pre_net = MnistNet()
# 加载保存的状态字典
pre_net.load_state_dict(torch.load('models/mnist_model.pth'))
# 将模型放在合适的设备上
pre_net = net.to(device)

处理图片,代入模型预测

一共准备0-9的10张数字图片,存放在imagedata目录中,为png格式,首先需要将它们转换成灰度图片,宽和高要转为28*28的张量,才可以放入模型中处理。
在以下的代码中,我们将处理好的灰度图片使用plt绘制出来,并在每个数字图片的上面标注它的预测类别。

import os
import matplotlib.pyplot as plt
from PIL import Image
import torchvision.transforms as transforms
import torch

# 假设 imgedata 目录下有十张图片,文件名为 number0.png\number1.png...
img_folder = '\data\imagedata'
img_files = [f'number{str(i).zfill(1)}.png' for i in range(10)]

# 定义预处理转换操作
preprocess = transforms.Compose([
    transforms.Resize((28, 28)),  # 调整图像大小
    transforms.Grayscale(num_output_channels=1),  # 转换为灰度
    transforms.ToTensor(),  # 转换为张量
    transforms.Normalize((0.5,), (0.5,))  # 归一化
])

# 处理每张图片并显示
fig, axes = plt.subplots(2, 5, figsize=(10, 4))  # 创建一个2x5的子图
for i, file in enumerate(img_files):
    # 读取图片
    image_path = os.path.join(img_folder, file)
    image = Image.open(image_path)

    # 预处理图片
    input_tensor = preprocess(image).unsqueeze(0)  # 增加一个批次维度

    # 使用模型进行预测
    with torch.no_grad():  # 在预测时不需要计算梯度
        output = pre_net(input_tensor)
        _, predicted = torch.max(output, 1)  # 获取预测的最可能类别

    # 转换为 NumPy 数组并调整通道顺序
    processed_image = input_tensor.squeeze(0).cpu().numpy()  # 移除批次维度
    processed_image = processed_image.transpose(1, 2, 0)  # 调整通道顺序为 HWC

    # 显示图片
    axes[i // 5, i % 5].imshow(input_tensor.squeeze(0).cpu().numpy().transpose(1, 2, 0), cmap='gray')
    axes[i // 5, i % 5].axis('off')  # 关闭坐标轴

    # 打印预测结果
    axes[i // 5, i % 5].set_title(f'Predicted: {predicted.item()}')

# 调整子图间距
plt.subplots_adjust(wspace=0.5, hspace=0.5)
plt.show()

代码 processed_image = input_tensor.squeeze(0) 的作用是移除张量 input_tensor 的第一个维度,也就是批次维度。在 PyTorch 中,当你使用数据加载器(DataLoader)来批量加载数据时,每个批次的数据都会被封装在一个张量中,这个张量的形状通常会包含一个额外的维度,用于表示批次大小(batch size)。
例如,如果你的批次大小为 10,并且你的图像数据是 28x28 像素的灰度图像,那么每个批次的张量形状将会是 [10, 1, 28, 28]。这里,第一个维度(维度索引为 0)的值是 10,表示有 10 个图像样本在这个批次中。squeeze 方法会移除这个维度,将张量的形状从 [10, 1, 28, 28] 变为 [1, 28, 28]。
在处理单个图像时,我们通常不需要批次维度,因为我们只处理一个样本。因此,我们使用 squeeze 方法来移除这个维度,这样我们就可以得到一个干净、没有批次维度的张量,可以用于图像显示或其他操作。

要注意的是,调整通道顺序是因为在使用 matplotlib 的 imshow 函数显示图像时,它期望图像数据的格式是高度(Height)、宽度(Width)在前,通道(Channels)在后的三维数组,即 HWC 格式。这与 PyTorch 中的张量格式不同,PyTorch 通常使用通道在前的格式,即 CHW。
transpose(1, 2, 0) 方法的作用是重新排列数组的轴,将通道轴(第0轴)移动到最后,将高度轴(第1轴)和宽度轴(第2轴)保持不变。这样,原始的 CHW 格式张量就被转换为了 HWC 格式的 NumPy 数组,可以被 imshow 正确显示。

预测结果如下所示,
在这里插入图片描述
准确度很低,可见模型还是过拟合了,回顾上面训练网络的代码,我们发现并没有实现当训练误差小于测试误差时,自动停止训练,即早停的功能。接下来,我们进行改进,看一下加入这个设计能不能有效提高模型在新样本上的泛化能力。

四、增加早停逻辑的训练过程

如果在训练过程中发现测试误差不再降低,或者训练误差开始低于测试误差,这可能是过拟合的信号。在这种情况下,你可能需要采取措施来减少过拟合,例如:

  • 增加数据集的大小或使用数据增强。
  • 减少模型的复杂度。
  • 使用正则化技术,如权重衰减(L2正则化)或Dropout。
  • 提前停止训练(早停)。

鉴于前几种方法还没听说过,所以这里看一下能否在训练过程中增加早停的逻辑,使模型提高泛化性能。
注意这里,我们只改动训练模型的代码,其余部分没有改动。

测试误差连续不下降时早停

# 设置训练循环次数
epochs = 3
best_test_loss = float('inf')  # 最佳测试误差,初始化为无穷大
patience = 1  # 允许测试误差不下降的连续次数

for i in range(epochs):
    train_loss = 0.0  # 训练误差累计值
    train_counter = 0  # 连续测试误差不下降的计数器
    # 枚举训练集中的数据
    for index, (train_input, labels) in enumerate(train_loader):
        # 将网络置于训练状态
        # 某些层在训练和测试时行为不同, 比如批归一化层 和 Dropout层
        net.train(True)
        # 将数据放在计算设备上
        train_input = train_input.to(device)
        labels = labels.to(device)
        # 计算输出
        output = net(train_input)
        # 计算误差
        loss = loss_func(output, labels)
        # 清空优化器, 计算梯度
        optimizer.zero_grad()
        loss.backward()
        # 用梯度优化模型
        optimizer.step()
        # 累计训练误差
        train_loss += loss.item() * train_input.size(0)

        # 每训练一段时间观察测试误差
        # 如果观察到测试误差不再继续降低 或者 训练误差开始低于测试误差
        # 说明出现过拟合, 应该停止训练
        # 将网络置为测试状态
        if index % 1000 == 0 or index == len(train_loader) - 1:
            # 将网络置为测试状态
            net.train(False)
            test_loss = 0.0
            with torch.no_grad():
                for test_input, test_labels in test_loader:
                    test_input = test_input.to(device)
                    test_labels = test_labels.to(device)
                    test_output = net(test_input)
                    test_loss += loss_func(test_output, test_labels).item() * test_input.size(0)

            # 计算平均测试误差
            test_loss /= len(test_loader.dataset)

            # 输出训练误差 和 测试误差
            print(
            f'Epoch [{i + 1}/{epochs}], Batch [{index + 1}/{len(train_loader)}], Train Loss: {train_loss / (index + 1):.4f}, Test Loss: {test_loss:.4f}')

            #   检查是否需要早停
            if test_loss < best_test_loss:
                best_test_loss = test_loss
                train_counter = 0  # 重置计数器
            else:
                train_counter += 1

            # 如果连续耐心次数的测试误差没有改善,则提前停止训练
            if train_counter >= patience:
                print(f'Stopping training early due to no improvement in test loss after {patience} consecutive epochs.')
                break
             # 更新训练误差累计值
    train_loss = 0.0

为了篇幅起见,代码不做过多解释啦。直接看一下这样训练后得到的模型效果:
在这里插入图片描述
预测结果,还是。。。emmm
在这里插入图片描述

训练误差小于测试误差时早停

# 设置训练循环次数
epochs = 3
for i in range(epochs):
    # 枚举训练集中的数据
    for index, (train_input, labels) in enumerate(train_loader):
        # 将网络置于训练状态
        # 某些层在训练和测试时行为不同, 比如批归一化层 和 Dropout层
        net.train(True)
        # 将数据放在计算设备上
        train_input = train_input.to(device)
        labels = labels.to(device)
        # 计算输出
        output = net(train_input)
        # 计算误差
        loss = loss_func(output, labels)
        # 清空优化器, 计算梯度
        optimizer.zero_grad()
        loss.backward()
        # 用梯度优化模型
        optimizer.step()

        # 每训练一段时间观察测试误差
        # 如果观察到测试误差不再继续降低 或者 训练误差开始低于测试误差
        # 说明出现过拟合, 应该停止训练
        if index % 1000 == 0:
            # 将网络置为测试状态
            net.train(False)
            test_input, test_labels = next(iter(test_loader))
            test_output = net(test_input)
            test_loss = loss_func(test_output, test_labels)
            # 输出训练误差 和 测试误差
            print('{0} 训练误差:{1:.4f} 测试误差{2:.4f}'.format(
                index,loss.item(), test_loss.item()
            ))
            if loss.item() < test_loss.item():
                print("Stopping training early due to train_loss < test_loss!")
                break

在这里插入图片描述
在这里插入图片描述
Emm,看来模型还是没有改进。咱也不知道为啥,这。。。那我们接着学习吧,看看以后有没有什么办法。。。


总结

今天学习的内容是以手写数字识别任务为例,展示了使用PyTorch框架构建和训练一个简单的神经网络模型的完整流程。

  1. 数据预处理:
    定义了数据转换操作,包括将图像转换为张量和归一化处理。
    下载并加载了MNIST数据集,分为训练集和测试集。
    创建了数据加载器,用于批量加载数据并在每个epoch开始时打乱数据顺序。
  2. 模型定义:
    使用MnistNet类定义了一个神经网络模型,该类继承自nn.Module。
    MnistNet类中包含了两个卷积层和两个全连接层,以及前向传播逻辑。
    调用super(MnistNet, self).__init__来确保父类nn.Module的构造方法被执行,这是初始化模型参数和设置模型设备的重要步骤。
  3. 训练设置:
    选择了计算设备,优先使用GPU(如果可用)。
    初始化了损失函数CrossEntropyLoss和优化器SGD,设置了学习率。
  4. 训练循环:
    设置了训练的轮数(epochs)。
    在每个epoch中,遍历训练数据加载器中的所有批次。
    将模型设置为训练模式,并将数据移动到计算设备上。
    执行前向传播,计算模型输出和损失。
    清零梯度,执行反向传播计算梯度,并使用优化器更新模型参数。
  5. 性能监控:
    每隔一定数量的批次,将模型设置为评估模式,并在测试集上计算测试误差。
    打印出训练误差和测试误差,以监控模型的性能。

并且,我们使用PIL处理图片,在新的手写图片上测试了一下模型的性能,虽然很差,但是也学习到了一些图片处理成张量的知识。
加油哇。

  • 8
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值