Featured image of post Pytorch 实现手写数字识别

Pytorch 实现手写数字识别

手写数字识别关键代码

流程和目标

流程:

准备数据,这些需要准备Dataloader

构建模型,这里可以使用torch构造一个深层的神经网络

模型的训练-学习

模型的保存,保存模型,后续持续使用

模型的评估,使用测试集,观察模型的好坏

目标

知道如何使用Pytorch完成神经网络的构建

知道Pytorch中激活函数的使用方法

知道Pytorch中torchvision.transforms中常见图形处理函数的使用

理解CNN的结构

了解CNN的构成方法

知道如何训练模型和如何评估模型

初步理解神经网络,理解深度学习

1、加载数据集

MNIST 手写数字识别数据集中的图像是一个28*28 的灰度图像。我们通过 pytorch 的内置函数将 MNIST 下载并读到内存中。将训练和测试数据集下载到同目录下的data文件夹下。

其中shuffle参数为是否打乱原有数据顺序

2、预处理数据

由于pytorch读取数据集mnist中的图像时默认使用python中的PIL图像 (P: 8像素,I: 整型,L: 黑白) python images libray, #现在使用pip install Pillow,所以我们首先需要把PIL图像转化为更加适合pytorh计算使用的图像张量,其次需要把原始 - 255之间的像素值通过归一化处理0~1之间的值,这两步预处理的目的都是欲使数据在神经网络中运算更高效。

transforms.ToTensor() 作用就是将PIL中28x28的灰度图像转化为tensor张量其维度为1x28x28 (CxWxH) 其中1的含义为单通道(彩色图像时调整为三通道)

transforms.Normalize([0.1307],[0.3081]) 其中,0.1307和0.3081是mnis数据集的均值和标准差,因为mnist数据值都是灰度图,所以图像的通道数只有一个,因此均值和标准差各一个。使用minst数据集的均值和标准差将数据标准化处理。

使用 Compose方法即是将两个操作合并一起

主要步骤

1、流程

2、重要概念

image-20231201205514249

channel——通道数(1/3)

enumerate——枚举

species ——种类

batch_size——是指在深度学习中用于训练的每个批次(batch)中包含的样本数量。

shuffle=True 是指在每个epoch开始之前,对数据进行随机重排序。在深度学习中,通常会在训练模型之前对数据进行洗牌操作,以确保模型不会受到输入样本顺序的影响。这可以帮助提高模型的泛化能力,并且防止模型过度拟合训练数据。

print(img_batch.shape) 是用来打印当前 batch 的图像数据的形状(shape)。这可以帮助你了解当前 batch 中有多少张图片以及它们的尺寸。通常情况下,输出会是一个四维张量,形状为 (batch_size, channels, height, width),其中:

  • batch_size 表示当前 batch 中有多少张图片;
  • channels 表示图像的通道数,比如 RGB 图像通道数为 3;
  • heightwidth 分别表示图像的高度和宽度。

permute(1,2,0) 会将原始张量的维度顺序从 (dim0, dim1, dim2) 调整为 (dim1, dim2, dim0)。它的维度顺序从(channels,width,height )就变成了(height, width, channels)

3.自建数据集进行图像分类整体思路

1.将自己所找图片按照一定的规则命名后,放到文件夹中。
2.使用glob方法获取所有数据文件路径
3.创建DataSet类的子类Mydataset,用于后续通过路径读入数据,并易于后续相应处理操作
4.通过Transforms.Compose()方法,对图片数据进行统一处理,并转换成Tensor格式
5.创建Mydatasetpro类,调用相关方法,获得{‘图片名’:标签}和{标签:‘图片名’}字典
6.统计索引数量,按照百分比,划分出训练集和测试集
最后得到:训练集数据、测试集数据、训练集标签、测试集标签

参考文章

4.常用术语

data set 数据集——简称ds

data labels数据标签——简称dl

learning_rate学习率——简称lr

学习率(lr)和动量(momentum)是训练神经网络时非常重要的超参数。

  1. 学习率(lr):学习率控制了每次迭代中参数更新的幅度。较高的学习率可能导致优化过程不稳定甚至无法收敛,而较低的学习率可能使优化过程缓慢且容易陷入局部最优解。因此,选择合适的学习率对于训练模型至关重要。
  2. 动量(momentum):动量是一种在参数更新中考虑之前梯度对当前梯度的影响的技术。它有助于加速收敛并减少震荡,特别是在处理非凸优化问题时效果显著。

5.了解神经网络

nn.Conv2d(3, 32, 3, 1, 1),

3是输入通道数(图片是n维) in_channels=3

32是通道数(可自己定义,数越大特征越精细)out_channels=32

输出通道是自己定义的

3是卷积核大小(数越大,新图片越小) kernel_size=3

1是步长 stride=1

1是填充 padding=1

6. 深度学习方法

深度学习通过使用多层神经网络直接从原始图像数据中学习特征,已成为手写数字识别的主流方法。
卷积神经网络(CNN):
LeNet:早期成功的CNN架构之一。
AlexNet、VGGNet:更深的网络结构,更复杂的学习能力。
ResNet:引入残差学习,允许更深的网络结构。
数据增强:通过旋转、缩放、裁剪等手段生成更多训练数据,提高模型的泛化能力。
迁移学习:使用在大型数据集上预训练的模型,并对其进行微调以适应特定任务。

7. loss.item

loss.item()实际上是获取loss张量的标量值,而不是计算损失的结果。

loss是通过调用损失函数(loss_fn)计算得到的。具体来说,你使用了交叉熵损失函数(nn.CrossEntropyLoss)。当你调用loss_fn(outputs, labels)时,它会将模型的预测输出(outputs)和真实标签(labels)作为输入,并计算出一个包含损失值的张量。

然后,通过调用loss.item(),你可以获取这个张量中的标量值。loss.item()方法返回一个Python数值,表示了loss张量中的值。这个标量值就是损失值,代表了模型在当前训练批次上的损失

所以,loss.item()是用于获取损失张量的标量值的方法,它不是损失函数本身的结果。损失函数的结果是一个张量,而loss.item()用于提取这个张量中的标量值。

8. 画每一轮的损失值图像

#主要语句
train_losses = []

train_losses.append(loss.item())

plt.figure()
plt.plot(train_losses, label='Training Loss')
plt.xlabel('epoch')
plt.ylabel('Loss')
plt.title('Training Loss over epoch')
plt.legend()
plt.show()
print('Finished Training')

writer.close()

完整语句

#损失
train_losses = []

# 添加tensorboard可视化工具
writer = SummaryWriter("logs")
start_time = time.time()
for i in range(epoch):
    print("------第{}轮训练开始------".format(i + 1))

    net.train()  # 设置模型为训练模式,只有 Dropout 和 BatchNorm 关心 self.training 标志。
    for data in train_dataloader:
        imgs, labels = data
        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
        outputs = net(imgs)
        labels = torch.tensor(labels, dtype=torch.long)
        loss = loss_fn(outputs, labels)
        optimizer.zero_grad()  # 梯度清零
        loss.backward()  # 反向传播
        optimizer.step()  # 优化器对参数更新
        total_train_step = total_train_step + 1
        if total_train_step % 40 == 0:  # 每训练40次进行打印
            end_time = time.time()
            print("训练次数:{},loss:{}".format(total_train_step, loss.item()))
            print("耗时:{}".format(end_time - start_time))
            start_time = time.time()
            writer.add_scalar("train_loss", loss.item(), total_train_step)  #写入日志

    net.eval()  # 设置模型为评估/推理模式
    # 评估模型(测试)
    total_test_loss = 0
    total_accuracy = 0  # 正确率
    with torch.no_grad():
        for data in test_dataloader:
            imgs, labels = data
            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
            outputs = net(imgs)
            labels = torch.tensor(labels, dtype=torch.long)
            loss = loss_fn(outputs, labels)
            total_test_loss = total_test_loss + loss.item()

            # 计算正确率
            accuracy = (outputs.argmax(1) == labels).sum()
            total_accuracy = 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 = total_test_step + 1
    train_losses.append(loss.item())  # 将每次迭代得到的损失值添加到列表中
torch.save(net, "net100.pth")

plt.figure()
plt.plot(train_losses, label='Training Loss')
plt.xlabel('epoch')
plt.ylabel('Loss')
plt.title('Training Loss over epoch')
plt.legend()
plt.show()
print('Finished Training')

writer.close()

9. 展示预测图片(每行五个)

# 计算行数和列数
num_images = len(names)
num_rows = (num_images + 4) // 5  # 向下取整
num_cols = min(num_images, 5)

# 创建一个大的图像来显示预测结果
fig, axes = plt.subplots(num_rows, num_cols, figsize=(20, 4*num_rows))
# 遍历测试图片
for i, name in enumerate(names):
    data_class = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']  # 按文件索引顺序排列
    image_path = os.path.join(root_path, name)
    image = Image.open(image_path)
    transform = transforms.Compose([transforms.Resize((28, 28)),
                                    transforms.ToTensor()])
    image = transform(image)

    model = Net()  # 创建模型实例
    model = torch.load("net100.pth", map_location=DEVICE)  # 加载模型参数
    model.to(DEVICE)
    image = image.unsqueeze(0)  # 添加批次维度
    image = image.to(DEVICE)

    model.eval()
    with torch.no_grad():
        output = model(image)
    predicted_class = torch.argmax(output, dim=1).item()  # 获取预测结果的索引
    predicted_label = data_class[predicted_class]  # 根据索引获取对应的标签

    # 将图像和预测结果添加到大的图像中
    ax = axes[i // 5, i % 5] if num_rows > 1 else axes[i % 5]
    ax.set_title("Predicted: {}".format(predicted_label))
    ax.axis('off')
    ax.imshow(image.squeeze().permute(1, 2, 0).cpu())  # 转换为CPU上的张量,并调整维度顺序

# 隐藏多余的子图
for j in range(num_images, num_rows * num_cols):
    axes[j // 5, j % 5].axis('off')

plt.tight_layout()  # 调整子图布局
plt.show()

10. python_除法运算符

在Python中,除法运算符有三种形式:///%

  1. /:这是常规的除法运算符,它返回两个操作数的浮点数商。例如,7 / 2 的结果是 3.5。

  2. //:这是整数除法运算符,它返回两个操作数的整数商,向下取整到最接近的整数。例如,7 // 2 的结果是 3,因为 3 是最接近 3.5 的整数。

  3. %:这是取模运算符,它返回除法的余数。例如,7 % 2 的结果是 1,因为 7 除以 2 的余数是 1。

下面是一个示例,演示了这些运算符的使用:

a = 7
b = 2

# 浮点数除法
result1 = a / b
print(result1)  # 输出: 3.5

# 整数除法
result2 = a // b
print(result2)  # 输出: 3

# 取模运算
result3 = a % b
print(result3)  # 输出: 1

总结一下:

  • / 运算符执行常规除法并返回浮点数结果。
  • // 运算符执行整数除法并返回向下取整的整数结果。
  • % 运算符返回除法的余数。

11. Loss和错误率的区别

损失(Loss)和错误率(Error Rate)是在机器学习和深度学习中常用的两个指标,用于评估模型的性能。它们有以下区别:

  1. 损失(Loss):损失是一个用于度量模型预测与实际标签之间差异的指标。在训练过程中,模型通过最小化损失来调整自身的参数,以使预测结果更接近真实标签。常见的损失函数包括均方误差(Mean Squared Error)、交叉熵损失(Cross-Entropy Loss)等。损失值越小表示模型的预测与实际标签越接近,训练得越好。

  2. 错误率(Error Rate):错误率是衡量模型在测试集上的错误预测比例。它表示模型在所有预测中错误的比例,通常以百分比形式表示。错误率越低表示模型的性能越好,因为它意味着模型在测试集上的预测结果更准确。

在代码中,损失(Loss)是通过损失函数计算得到的,用于衡量模型在每个样本上的预测与真实标签之间的差异。通过累加每个样本的损失值,可以得到整体测试集上的损失。

错误率(Error Rate)是通过比较模型的预测结果和真实标签来计算的。对于分类任务,我们将模型的预测结果与真实标签进行比较,统计预测错误的数量,并将其除以总预测数量得到错误率。

损失和错误率都是用于评估模型性能的重要指标,但它们关注的角度不同:损失关注模型预测与真实标签的差异程度,而错误率关注模型的错误预测比例。在训练过程中,我们通常通过最小化损失来优化模型,而在测试和评估阶段,我们会关注错误率来衡量模型的准确性。

12. 为什么是64x7x7 ?

class Net(nn.Module): def init(self): super(Net, self).init() self.model = nn.Sequential( nn.Conv2d(3, 32, 3, 1, 1), # 修改卷积核大小为3,填充为1 nn.MaxPool2d(2), # 池化层 nn.Conv2d(32, 64, 3, 1, 1), # 修改卷积核大小为3,填充为1 nn.MaxPool2d(2), nn.Flatten(), nn.Linear(64 * 7 * 7, 64), # 根据输入图片的大小修改全连接层的输入维度 nn.Linear(64, 10) )

池化的结果,经过两次池化,图片变为原图的1/4,尺寸从28x28变成了7x7

在展平层,深度为64,尺寸为7x7

因此,全连接层的输入维度为 64 x 7 x 7,即64个通道,每个通道的尺寸为 7x7。

展平层将这个 3D 特征图张量变换成一个 1D 的特征向量,以便后续的全连接层可以进行处理。 在这里,展平层的作用是将卷积特征图的空间维度降低,同时保留特征通道的维度。 这样,在全连接层中,每个特征通道都可以与其他通道进行连接,以更好地学习特征之间的关系。

13. CNN构建网络

img

Licensed under CC BY-NC-SA 4.0