文章目录

  • 1、引言
  • 2、导入数据集(所思所想)
  • 2.1 代码
  • 2.2 运行结果
  • 2.3 查看数据集
  • 2.4 展示样本图片
  • 2.4.1 随机展示图片
  • 2.4.2 展示0-9数字图片
  • 3、搭建网络
  • 3.1 BP神经网络之前馈神经网络模型
  • 3.1.1 前馈神经网络模型代码(BP.py)
  • 3.1.2 模块解析
  • 3.2 卷积神经网络模型
  • 3.2.1 卷积神经网络模型代码(CNN.py)
  • 3.2.2 模型解释
  • 3.3 两个神经网络模型的主程序
  • 3.3.1 完整代码(Network.py)
  • 3.3.2 代码结构解析
  • 3.3.3 训练结果[注意:训练的epoch过大容易造成过拟合]
  • 3.3.3.1 BP神经网络(训练15次epoch)
  • 3.3.3.2 CNN神经网络(训练15次epoch)
  • 3.4 目录结构
  • 4、测试
  • 4.1 介绍
  • 4.2 代码
  • 4.3 实验结论


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目录下:

CNN算法训练集要好久_python


       但是出现了下面的警告:

CNN算法训练集要好久_官网_02


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数据集介绍的官网

CNN算法训练集要好久_官网_03


       我们可以发现在代码中,MNIST类的五个参数,我们用了四个。其中我们用transform参数将PIL图片对象转换了Tensor(张量)。然后按Ctrl点击我们import 的 ToTensor,进入transforms.py,我们来看看ToTensor类的作用:

CNN算法训练集要好久_官网_04


       我们可以看到它是将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。

CNN算法训练集要好久_CNN算法训练集要好久_05


       从上面的图片我们可以看到,输入的图片为三通道时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的像素大小

CNN算法训练集要好久_神经网络模型_06

       通过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()

       结果:

CNN算法训练集要好久_官网_07

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()

       结果:

CNN算法训练集要好久_神经网络模型_08

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。其中,通过矩阵相乘来缩放维度,CNN算法训练集要好久_pytorch_09的二维矩阵乘CNN算法训练集要好久_pytorch_10 变为CNN算法训练集要好久_pytorch_11

CNN算法训练集要好久_CNN算法训练集要好久_12

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模块,详见官网

CNN算法训练集要好久_神经网络模型_13


       参考知乎~

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:

CNN算法训练集要好久_官网_14

       第14-15次epoch:

CNN算法训练集要好久_pytorch_15


       最终精确度达到88.1%。

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

       第1次epoch:(下面这个Warning据说是pytorch1.9.0bug,直接忽略)

CNN算法训练集要好久_pytorch_16


       第14-15次epoch:

CNN算法训练集要好久_官网_17


       最终精确度达到94.5%。

3.4 目录结构

CNN算法训练集要好久_python_18

4、测试

4.1 介绍

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

CNN算法训练集要好久_CNN算法训练集要好久_19


       我会将我自己标注的测试图片(整个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图像处理来增强图像特征。对比两个模型,对于下面这张图:

CNN算法训练集要好久_官网_20


       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。

CNN算法训练集要好久_python_21