pytorch入门:用CNN/BP实现对MNIST数据集的训练[独家解析]

1、引言

       下面的部分代码参考自pytorch官网,传送门,我认为初学者应该首先从网上一些博客中不断管中窥豹,保持怀疑的心态。接着可以尝试从官网中学习入门,因为官网的一定是相对最标准的。在CNN模型中参考了部分莫烦python在YouTube上的教学。但是,我整合、修改的主程序和模型代码的解析是很全面的。
       下面我将以MNIST数据集为例子,而pytorch官网上是FashionMNIST数据集。这两个数据集的唯一区别是,MNIST是0-9的手写数字灰度图片数据库,而FashionMNIST中的是来自 10 种类别商品的灰度图片数据库。
       在文末我会整合所有分段代码,我将借鉴官网,以我的方式,为你编写和解读代码。环境和配置:Pycharm2020.1.4,Python3.7,pytorch1.9.0,Win10 64位系统。

2、导入数据集(所思所想)

2.1 代码

import torch
import numpy as np
from torchvision import datasets
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt


training_data = datasets.MNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

test_data = datasets.MNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

2.2 运行结果

       MNIST数据集被保存在data目录下:
结果
       但是出现了下面的警告:
警告
UserWarning:给定的 NumPy 数组不可写,而PyTorch 不支持不可写的张量。 这意味着您可以使用Tensor写入底层(推测是不可写的)NumPy 数组,但在将数组转换为Tensor之前,您可能希望复制数组以保护其数据或使其可写。 对于该程序的其余部分,此类警告将被抑制。
       解决办法:根据Warning的提示,将MNIST.py文件中以下代码中的copy=False改为True。这样警告就会消失。

return torch.from_numpy(parsed.astype(m[2], copy=False)).view(*s)

       点击UserWarning前的蓝色下划线链接即可进入MNIST.py,在498行。
       思考: 通过分析MNIST类,我们可以发现MNIST数据集介绍的官网
在这里插入图片描述
       我们可以发现在代码中,MNIST类的五个参数,我们用了四个。其中我们用transform参数将PIL图片对象转换了Tensor(张量)。然后按Ctrl点击我们import 的 ToTensor,进入transforms.py,我们来看看ToTensor类的作用:
ToTensor
       我们可以看到它是将PIL图片对象或者numpy.ndarray转化为tensor。注释里面说:Converts a PIL Image or numpy.ndarray (H x W x C) in the range
[0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0]
        所以transform中ToTensor的作用是将灰度图片中每个像素点0-255压缩到0-1。其中H、W、C分别代表高度,宽度和通道数(深度)。借鉴:H、W、C的理解

import numpy as np
from torchvision.transforms import ToTensor

# 左闭右开,所以右边用256,生成一维数组data
data = np.random.randint(0, 256, size=6)
# H x W x C, 模拟照片的输出格式,模拟RGB三通道照片
img = data.reshape(2, 1, 3)
print(img)
print()
tensor = ToTensor()(img)  # 转换成tensor
print(tensor)

      通过输出结果,理解tensor和numpy的区别:
numpy.ndarry
[ [[ 97 157 189]]
  [[ 81 111 188]] ]
tensor:
tensor([ [ [97],
        [81] ],
      [ [157],
        [111] ],
      [ [189],
        [188] ] ], dtype=torch.int32)
其中,[ [97],
     [81] ]
       对应的就是1个通道,一共有三个通道。其中每1个通道的长为2,宽为1。
通道
       从上面的图片我们可以看到,输入的图片为三通道时mode=‘RGB’,即三原色构成的彩色图片。注意:灰度图片,也即是MNIST训练集中的图片只有一个通道

2.3 查看数据集

       代码:

# 输出训练集中数据和对应标签Tensor的维度
print('训练集')
print('图片', training_data.data.size())
print('标签', training_data.targets.size())

# 输出测试集中数据和对应标签Tensor的维度
print('测试集')
print('图片', test_data.data.size())
print('标签', test_data.targets.size())

       输出:训练集是60000个图片,其中每个图片都是28*28的像素大小

输出

       通过print(type(training_data[0]))可知,training_data中每一个元素是一个元组tuple。

2.4 展示样本图片

2.4.1 随机展示图片

       由于数据集的样本是随机打乱的,所以用随机数生成索引无法找到0-9的图片。

# 展示样本图片
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 4
for i in range(1, 11):
    # item()方法从tensor对象中获取数值,size=(1,)表示只取一个,但是下面的方法刚好取到0-9的九张图片的概率很小
    # 元组中只包含一个元素时,需要在元素后面添加逗号来消除歧义
    sample_idx = torch.randint(len(training_data), size=(1,)).item()
    img, label = training_data[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(str(label))
    plt.axis("off")  # 去掉0-28的坐标轴刻度
    # print(img.shape)
    # squeeze()把shape中为1的维度去掉,将torch.Size([1, 28, 28])变为torch.Size([28, 28]),灰度图像
    plt.imshow(img.squeeze(), cmap="gray")
plt.show()

       结果:
结果1

2.4.2 展示0-9数字图片

       代码(tensor和numpy中没有数组的index方法):

# 展示样本图片
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 4
value = -1
for i in range(1, 11):
    # item()方法从tensor对象中获取数值,size=(1,)表示只取一个,但是下面的方法刚好取到0-9的九张图片的概率很小
    # sample_idx = torch.randint(len(training_data), size=(1,)).item()
    value = value + 1
    # 找列表中是value的索引,返回一个元组,首先是所有行索引,然后是所有列索引,显然只有一行
    sample_idx = np.where(training_data.targets.numpy() == value)[0][1]
    img, label = training_data[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(str(label))
    plt.axis("off")  # 去掉0-28的坐标轴刻度
    # print(img.shape)
    # squeeze()把shape中为1的维度去掉,将torch.Size([1, 28, 28])变为torch.Size([28, 28]),灰度图像
    plt.imshow(img.squeeze(), cmap="gray")
plt.show()

       结果:
结果2

3、搭建网络

3.1 BP神经网络之前馈神经网络模型

3.1.1 前馈神经网络模型代码(BP.py)
from torch import nn

class NeuralNetwork(nn.Module):
    # 类中的方法第一个参数一定是self,代表实例对象
    def __init__(self):
        # NeuralNetwork继承nn.Module,下面这段代码就是对继承自父类nn.Module的属性进行初始化
        super(NeuralNetwork, self).__init__()
        # 展平一个连续范围的维度,输出类型为Tensor
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28 * 28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
        )

    def forward(self, x):
        x = self.flatten(x)
        # logits常用于表示最终的全连接层的输出
        logits = self.linear_relu_stack(x)
        return logits
3.1.2 模块解析

       这个模块以及下面的代码我参考自官网搭建神经网络的教学。首先申明一点,官网的代码由于用于教学,是零散。而我将其整合在一起,进行了一定的修改,并在一些关键地方加上了我的理解。其次,我基本能保证这是全网比较适合入门且上心的博文。
       由于前馈神经网络+误差反向传播(BP)更新参数=BP神经网络。由于nn.Module中没办法实现 def backward(self, x),因为需要在模型外面使用迭代优化算法进行更新参数(BP)的操作。为了严谨起见,我将NeuralNetwork类(前馈神经网络模型)单独放到BP.py文件中,在Network.py主模型(真正的BP神经网络模型)中,import调用即可。这样的目的是为了对应CNN.py(CNN类,包含CNN模型)。我们跑代码,直接运行Network.py即可。
       nn.Sequential 就是一个按照顺序的容器模块。linear_relu_stack就是一个线性计算的栈,包含了三个线性层和两个非线性操作ReLU。其中,通过矩阵相乘来缩放维度, 1 × 784 1 \times784 1×784的二维矩阵乘 784 × 512 784\times512 784×512 变为 1 × 512 1\times512 1×512
线性运算

3.2 卷积神经网络模型

3.2.1 卷积神经网络模型代码(CNN.py)
from torch import nn


class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        # 第一个卷积层,通过(28+2*2-5)/1 + 1 = 28的计算可知,输出的通道数是16,图片的维度为(28/2) * (28/2) = 14 * 14
        self.conv1 = nn.Sequential(
            nn.Conv2d(
                in_channels=1,  # 输入的通道数目
                out_channels=16,  # 输出的通道数目,通道数和卷积核的个数一样
                kernel_size=5,  # 卷积核的尺寸
                stride=1,  # 步长
                padding=2  # 边界补0
            ),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        # 第二个卷积层,上一层的输出为16 * 14 * 14,通过(14+2*2-5)/1 + 1 = 14,经过池化层14/2 = 7,输出的图片维度为32 * 7 * 7
        self.conv2 = nn.Sequential(
            # nn.Conv2d(16, 32, 5, 1, 2),
            # 下面的式子可以简写为上式
            nn.Conv2d(
                in_channels=16,
                out_channels=32,
                kernel_size=5,
                stride=1,
                padding=2
            ),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        # 最后经过一层线性变换
        self.out = nn.Linear(32 * 7 * 7, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        # print(x.shape) , torch.Size([1, 32, 7, 7])
        x = x.view(x.size(0), -1)  # -1表示一个不是很确定的数,我认为这里应该是全连接层
        # print(x.shape), torch.Size([1, 1568])
        output = self.out(x)
        return output
3.2.2 模型解释

       我参考了莫烦Python的代码,并自己做了注释。针对于Conv2d模块,详见官网
知乎
       参考知乎~

3.3 两个神经网络模型的主程序

3.3.1 完整代码(Network.py)
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda
import torchvision.models as models
from BP import NeuralNetwork
from CNN import CNN

training_data = datasets.MNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()
)

test_data = datasets.MNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

# 定义超参数
learning_rate = 1e-3
batch_size = 64
epochs = 20

# dataloader是一个迭代器,每一次迭代返回一个batch的样本数据(train_features)和对应的标签(train_labels)
# 如果shuffle=true时,在我们遍历所有batch后,数据会被打乱
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=batch_size, shuffle=True)


# 检查GPU是否可用
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using {device} device')


# model = NeuralNetwork().to(device)
model = CNN().to(device)

# Initialize the loss function
# nn.CrossEntropyLoss 结合了 nn.LogSoftmax 和 nn.NLLLoss
loss_fn = nn.CrossEntropyLoss()

# 优化器,优化算法我们采用SGD随机梯度下降,模型内部的参数(w,b)已经被初始化好了
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)


# 训练train_dataset
def train_loop(dataloader, model, loss_fn, optimizer):
    # training_data是MNIST对象,train_dataloader.dataset从train_dataloader取出该对象,len方法返回数据集的大小
    size = len(dataloader.dataset)
    # batch 代表从dataloader中抽取出的第几个batch_size,是通过枚举enumerate得到的序号。X是64个image的Tensor,y是对应的标签
    for batch, (X, y) in enumerate(dataloader):
        # Compute prediction and loss
        pred = model(X.to(device))  # pred包含了64个样本的输出,是一个64*10的Tensor
        loss = loss_fn(pred, y.to(device))
        # Back propagation
        optimizer.zero_grad()  # 重置模型参数的梯度,默认情况下梯度会迭代相加
        loss.backward()  # 反向传播预测损失,计算梯度
        optimizer.step()  # 梯度下降,w = w - lr * 梯度。随机梯度下降是迭代的,通过随机噪声能避免鞍点的出现

        if batch % 100 == 0:  # 取余的数值可以自己设置
            loss, current = loss.item(), batch * batch_size  # 我将len(X)替换为了batch_size
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")


# 训练test_dataset
def test_loop(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    test_loss, correct = 0, 0

    # 被with torch.no_grad()包住的代码,不会被跟踪反向梯度计算,也就是grad_fn不会变
    with torch.no_grad():
        for X, y in dataloader:
            pred = model(X.to(device))
            test_loss += loss_fn(pred, y.to(device)).item()
            # 通过将tensor中的布尔值转换为0/1并求和,获得BP模型识别成功的样本图片
            correct += (pred.argmax(1) == y.to(device)).type(torch.float).sum().item()

    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")


if __name__ == '__main__':
    for t in range(epochs):
        print(f"Epoch {t + 1}\n-------------------------------")
        train_loop(train_dataloader, model, loss_fn, optimizer)
        test_loop(test_dataloader, model, loss_fn)
    print("Done!")
    # 保存模型结构以及参数
    # torch.save(model, 'bp_model.pth')
    torch.save(model, 'cnn_model.pth')

3.3.2 代码结构解析

       我认为一共包括下面四个步骤:

  1. 导入数据集
  2. 定义网络结构
  3. 迭代优化模型参数[引入计算图(computational graph)的概念]
  4. 保存并加载模型

       对于BP模型和CNN模型,在上面代码中一共有三个差别:

       1、导入模块的时候

from BP import NeuralNetwork

from CNN import CNN

       2、调用模型的时候

#model = NeuralNetwork().to(device)

model = CNN().to(device)

       3、保存模型的时候

#torch.save(model, ‘bp_model.pth’)

torch.save(model, ‘cnn_model.pth’)

       其中2,3中的任意两个语句在一次运行中只能选择一个,且相互对应,另外一条语句注释掉即可。当然,你也可以用GPU并行运算~在张量运算过程中,我都用了Tensor.to(‘cuda’)[device = ‘cuda’],保证都用GPU进行运算,如果你的电脑带不动,用CPU即可。这一点我认为是官网所忽略的

3.3.3 训练结果[注意:训练的epoch过大容易造成过拟合]
3.3.3.1 BP神经网络(训练15次epoch)

       第1次epoch:
1

       第14-15次epoch:
15
       最终精确度达到88.1%。

3.3.3.2 CNN神经网络(训练15次epoch)

       第1次epoch:(下面这个Warning据说是pytorch1.9.0bug,直接忽略)
在这里插入图片描述
       第14-15次epoch:

在这里插入图片描述
       最终精确度达到94.5%。

3.4 目录结构

目录结构

4、测试

4.1 介绍

       测试的目的是通过自己手写的图片/拍照的图片,让神经网络进行识别。所有格的图片我都通过Windows系统自带的画图工具进行实现。你可以设置任意尺寸的正方形画布(当然别的矩形也没问题,但是为了配合原始数据集中28像素*28像素的,最好是正方形图片)。注意背景最好是全黑。我使用橡皮擦在全黑的背景板上写字~ 如下图数字9所示,这些照片按照标签命名,并放在test文件夹下面。
9
       我会将我自己标注的测试图片(整个test文件夹)放到百度网盘,链接:https://pan.baidu.com/s/1417yBPHSsO5CZv-CuPjRsw
提取码:3ho3~

4.2 代码

import torch
from torch import nn
import torchvision.transforms as transforms
import cv2 as cv  # pip install opencv-python
import matplotlib.pyplot as plt
from PIL import Image
from BP import NeuralNetwork
from CNN import CNN

# 检查GPU是否可用
device = 'cuda' if torch.cuda.is_available() else 'cpu'

model1 = torch.load('bp_model.pth').to(device)
model2 = torch.load('cnn_model.pth').to(device)


def resizeImage(file_path):
    img = Image.open(file_path)
    img1 = img.resize((28, 28), Image.ANTIALIAS)
    img1.save(file_path)
    # 转换成灰度图
    img = cv.imread(file_path, 0)
    plt.imshow(img, cmap="gray")
    plt.show()
    print(img.shape)  # numpy数组格式为(H,W,C)
    img_tensor = transforms.ToTensor()(img)  # tensor数据格式是torch(C,H,W)
    # On convolutional layers,Conv2d layers expect input with the shape:(n_samples, channels, height, width)
    img_tensor = torch.unsqueeze(img_tensor, 0)  # 这一条语句是将三维增加为四维,用BP模型训练的时候可以不需要
    return img_tensor


pred = model2(resizeImage('./test/9-5.png').to(device))
print('神经网络识别手写图片的结果:', pred.argmax(1).item())

4.3 实验结论

       自己画的图片,经过def resizeImage(file_path)函数处理,先将原图转换成28*28的像素,再转变成灰度图,最后用Image.ANTIALIAS参数使图片抗锯齿。
       在0.png-9.png(整数标注,共十张图片),我第一次用BP/CNN训练得到的模型(15次epoch),只能识别0-3,5。只有50%的精确度。当然,这个图片的清晰度不是特别高,我都是一笔带过,没有人为加粗。值得注意的是,带-的图片都是我用画板的橡皮擦从不同角度人为擦除来加强图像特征,当然你也可以用Python图像处理来增强图像特征。对比两个模型,对于下面这张图:
9
       BP模型识别为7,CNN识别为3。咋一看CNN效果还没bp好。但是随着我不断测试,对于4-2.png,如下图所示,BP模型识别为9,CNN识别为4,果然CNN识别能力更好。最后发现,在该网络结构下,CNN仍然识别不了7和9(BP识别不了4,7和9),实际最优的准确率大致为80%。最终,测试得到CNN的准确率比BP好,但我感觉BP就算错也没有CNN离谱hh。
4-2

       要提高识别能力,可以人为增强图像或者改进网络结构在测试集达到更高的精确度。

       断断续续写了一周,如果觉得不错,请三连支持一下。有错误也请指正,感谢!

  • 19
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
非常好的问题!我可以回答这个问题。对于mnist数据集的分类问题,我们可以使用卷积神经网络(CNN)来解决。在PyTorch实现CNN非常简单,你只需要按照以下步骤: 1. 导入需要的库和模块 ``` import torch import torch.nn as nn import torch.nn.functional as F ``` 2. 定义CNN模型 ``` class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5) self.pool = nn.MaxPool2d(kernel_size=2, stride=2) self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5) self.fc1 = nn.Linear(in_features=16 * 4 * 4, out_features=120) self.fc2 = nn.Linear(in_features=120, out_features=84) self.fc3 = nn.Linear(in_features=84, out_features=10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(-1, 16 * 4 * 4) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x ``` 这里我们定义了一个简单的CNN模型,包括两个卷积层和三个全连接层。我们使用ReLU作为激活函数,并在卷积层之间使用max-pooling来减小输出尺寸。 3. 加载mnist数据集 ``` trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transforms.ToTensor()) trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True, num_workers=2) ``` 这里我们使用torchvision库来加载mnist数据集,并使用transforms.ToTensor()将数据转换为PyTorch张量。我们还使用DataLoader来生成批数据,以便进行批量训练。 4. 定义优化器和损失函数 ``` net = Net() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) ``` 我们使用交叉熵损失作为损失函数,并使用随机梯度下降(SGD)作为优化器。 5. 训练网络 ``` for epoch in range(10): # loop over the dataset multiple times running_loss = 0.0 for i, data in enumerate(trainloader, 0): # get the inputs; data is a list of [inputs, labels] inputs, labels = data # zero the parameter gradients optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # print statistics running_loss += loss.item() if i % 2000 == 1999: # print every 2000 mini-batches print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000)) running_loss = 0.0 print('Finished Training') ``` 这里我们使用PyTorch的自动微分来计算梯度,并调用优化器更新权重。我们通过训练数据多次迭代来训练网络。 6. 测试网络 ``` correct = 0 total = 0 with torch.no_grad(): for data in testloader: images, labels = data outputs = net(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print('Accuracy of the network on the 10000 test images: %d %%' % ( 100 * correct / total)) ``` 这里我们使用测试数据集来测试训练过的神经网络的准确度。 希望这个回答能帮到你!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TerryBlog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值