深度学习与计算机视觉Project1: 用Pytorch实现MNIST手写数字识别

在介绍Pytorch实现MNIST手写数字识别之前,先来了解下训练一个网络的步骤。

一.数据预处理

二.定义网络

三.定义损失函数与优化方式

四.训练模型

五、运行测试集观察效果

六、保存模型

 


 

一、数据预处理

数据处理这一块,PyTorch使用了torchvision来完成数据的处理,只实现了一些数据集的处理,如果处理自己的工程则需要修改增加内容。

import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
# 1、准备数据
# 训练数据
train_data = torchvision.datasets.MNIST(root='../../data',
                                        train=True,
                                        transform=transform,
                                        download=True)

# 创建训练数据迭代器
train_loader = torch.utils.data.DataLoader(dataset=train_data,
                                           batch_size=Config.batch_size,
                                           shuffle=True)

# 测试数据
test_data = torchvision.datasets.MNIST(root='../../data',
                                       train=False,
                                       transform=transform,
                                       download=True)

# 测试数据迭代器(我这个是一次加载完)
test_loader = torch.utils.data.DataLoader(dataset=test_data,
                                          batch_size=Config.batch_size,
                                          shuffle=True)

把原始数据处理为模型使用的数据需要3步:transforms.Compose(),torchvision.datasets(), torch.utils.data.DataLoader()分别可以理解为数据处理格式的定义、数据处理和数据加载。

1.1 transform.Compose()

tansforms.Compose()意思就是将多种变换组合起来,如transforms.Compose([ transforms.CenterCrop(10), transforms.ToTensor(), ]),就先对图片进行中心切割,再转换为张量。(PS:transforms.CenterCrop(size):将给定的PIL.Image进行中心切割,得到给定的sizesize可以是tuple(target_height, target_width)size也可以是一个Integer,在这种情况下,切出来的图片的形状是正方形。)

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])

transforms.ToTensor()将需要被处理的数据转为Tensor类型

transforms.Normalize()数据被转化为Tensor类型后,对其进行减均值(0.1307)和除方差(0.3081),以实现数据的正则化。

1.2 torchvision.datasets()

这是pytorch自带的数据集,可以通过它来下载pytorch已有的数据集:

  • MNIST
  • COCO(用于图像标注和目标检测)(Captioning and Detection)
  • LSUN Classification
  • ImageFolder
  • Imagenet-12
  • CIFAR10 and CIFAR100
  • STL10
rain_data = torchvision.datasets.MNIST(root='../../data',
                                        train=True,
                                        transform=transform,
                                        download=True)

- root : 存放文件 的目录   - train : True = 训练集, False = 测试集  

- download : True = 从互联网上下载数据集,并把数据集放在root目录下. 如果数据集之前下载过,将处理过的数据(minist.py中有相关函数)放在root的目录下则会自动调用已经下载好的数据集。

torchvision.datasets()包含了下面3个函数

  • __init__魔法方法里面进行读取数据文件
  • __getitem__魔法方法进行支持下标访问
  • __len__魔法方法返回自定义数据集的大小,方便后期遍历

_init__(self, root, train=Ture, transform=None, traget_transform=None, download=False):该方法用来初始化类和对数据进行加载(有时需要定义一些开关来防止重复处理)。数据的加载就是针对不同的数据,把其data和label(分为训练数据和测试数据)读入到内存中。

__getitem__(self, index):该方法是把读入的输出传给PyTorch(迭代器的方式)。

1.3 torch.utils.data.DataLoader()

# 创建训练数据迭代器
train_loader = torch.utils.data.DataLoader(dataset=train_data,
                                           batch_size=Config.batch_size,
                                           shuffle=True)

dataloader是加载dataset,并设置其batch_size(单次训练时送入的样本数目),以及shuffle(是否打乱样本顺序,训练集一定要设置shuffle为True,测试集则无强制性规定)

torch.utils.data.DataLoader() Data loder, Combines a dataset and and a sampler, and provides single, or multi-process iterators over the dataset. 就是把合成数据并且提供迭代访问。输入参数有:

  • dataset (Dataset) – 加载数据的数据集。
  • batch_size (int, optional) – 每个batch加载多少个样本(默认: 1)。
  • shuffle (bool, optional) – 设置为True时会在每个epoch重新打乱数据(默认: False).
  • sampler (Sampler, optional) – 定义从数据集中提取样本的策略。如果指定,则忽略shuffle参数。
  • num_workers (int, optional) – 用多少个子进程加载数据。0表示数据将在主进程中加载(默认: 0)
  • collate_fn (callable, optional) –整理数据,把每个batch数据整理为tensor。(一般使用默认调用default_collate(batch))。
  • pin_memory (bool, optional) –针对不同类型的batch进行处理。比如为Map或者Squence等类型,需要处理为tensor类型。
  • drop_last (bool, optional) – 用于处理最后一个batch的数据。因为最后一个可能不能够被整除,如果设置为True,则舍弃最后一个,为False则保留最后一个,但是最后一个可能很小。(默认: False)

torch.utils.data.DataLoader类主要使用torch.utils.data.sampler实现,sampler是所有采样器的基础类,提供了迭代器的迭代(__iter__)和长度(__len__)接口实现,同时sampler也是通过索引对数据进行洗牌(shuffle)等操作。因此,如果DataLoader不适用于你的数据,需要重新设计数据的分批次,可以充分使用所提供的smapler。

二、定义网络

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        """
        Conv2d参数:
        第一位:input channels  输入通道数
        第二位:output channels 输出通道数
        第三位:kernel size 卷积核尺寸
        第四位:stride 步长,默认为1
        第五位:padding size 默认为0,不补
        """
        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 32, 3, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )

        self.conv2 = nn.Sequential(
            nn.Conv2d(32, 64, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )

        self.fc1 = nn.Sequential(
            nn.Linear(64 * 5 * 5, 128),
            nn.BatchNorm1d(128),
            nn.ReLU()
        )

        self.fc2 = nn.Sequential(
            nn.Linear(128, 64),
            nn.BatchNorm1d(64),  # 加快收敛速度的方法(注:批标准化一般放在全连接层后面,激活函数层的前面)
            nn.ReLU()
        )

        self.fc3 = nn.Linear(64, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

这里采用卷积神经网络,当然也可以用其他网络,看个人喜好了。

对于类不太熟悉的,可以先看下这篇博客:https://blog.csdn.net/qq_39314932/article/details/80716295

super类的作用是继承的时候,调用含super的各个的基类__init__函数,如果不使用super,就不会调用这些类的__init__函数,详情参考:https://blog.csdn.net/shiheyingzhe/article/details/83051471

2.1、nn.Sequential

一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数。

2.2、nn.Conv2d()

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)

  • in_channels(int) – 输入信号的通道
  • out_channels(int) – 卷积产生的通道
  • kerner_size(int or tuple) - 卷积核的尺寸
  • stride(int or tuple, optional) - 卷积步长
  • padding(int or tuple, optional) - 输入的每一条边补充0的层数
  • dilation(int or tuple, optional) – 卷积核元素之间的间距
  • groups(int, optional) – 从输入通道到输出通道的阻塞连接数
  • bias(bool, optional) - 如果bias=True,添加偏置

dilation: 用于控制内核点之间的距离
groups: 控制输入和输出之间的连接: group=1,输出是所有的输入的卷积;group=2,此时相当于有并排的两个卷积层,每个卷积层计算输入通道的一半,并且产生的输出是输出通道的一半,随后将这两个输出连接起来。

参数kernel_sizestride,paddingdilation也可以是一个int的数据,此时卷积height和width值相同;也可以是一个tuple数组,tuple的第一维度表示height的数值,tuple的第二维度表示width的数值

卷积运算详情查看:https://blog.csdn.net/weixin_42111770/article/details/80808412

2.3、nn.ReLU()

激活函数(Activation Function),就是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。引入激活函数是为了增加神经网络模型的非线性。没有激活函数的每层都相当于矩阵相乘。就算你叠加了若干层之后,无非还是个矩阵相乘罢了。

如果不用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,这种情况就是最原始的感知机(Perceptron)。如果使用的话,激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中。

ReLU激活函数如下:

从ReLU函数图像可知,它是分段线性函数,所有的负值和0为0,所有的正值不变,这种操作被称为单侧抑制。ReLU函数图像其实也可以不是这个样子,只要能起到单侧抑制的作用,对原图翻转、镜像都可以。

当训练一个深度分类模型的时候,和目标相关的特征往往也就那么几个,因此通过ReLU实现稀疏后的模型能够更好地挖掘相关特征,拟合训练数据。正因为有了这单侧抑制,才使得神经网络中的神经元也具有了稀疏激活性。尤其体现在深度神经网络模型(如CNN)中,当模型增加N层之后,理论上ReLU神经元的激活率将降低2的N次方倍

不用simgoid和tanh作为激活函数,而用ReLU作为激活函数的原因是:加速收敛。因为sigmoid和tanh都是饱和(saturating)的。何为饱和?可理解为把这两者的函数曲线和导数曲线plot出来:他们的导数都是倒过来的碗状,也就是越接近目标,对应的导数越小。而ReLu的导数对于大于0的部分恒为1。于是ReLU确实可以在BP的时候能够将梯度很好地传到较前面的网络。

一般情况下,使用ReLU会比较好

  • 使用 ReLU,就要注意设置 learning rate,不要让网络训练过程中出现很多 “dead” 神经元;
  • 如果“dead”无法解决,可以尝试 Leaky ReLU、PReLU 、RReLU等Relu变体来替代ReLU;
  • 不建议使用 sigmoid,如果一定要使用,也可以用 tanh来替代。

2.4、nn.MaxPool2d()

torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)

对于输入信号的输入通道,提供2维最大池化(max pooling)操作

 

  • kernel_size(int or tuple) - max pooling的窗口大小
  • stride(int or tuple, optional) - max pooling的窗口移动的步长。默认值是kernel_size
  • padding(int or tuple, optional) - 输入的每一条边补充0的层数
  • dilation(int or tuple, optional) – 一个控制窗口中元素步幅的参数
  • return_indices - 如果等于True,会返回输出最大值的序号,对于上采样操作会有帮助
  • ceil_mode - 如果等于True,计算输出信号大小的时候,会使用向上取整,代替默认的向下取整的操作

如果padding不是0,会在输入的每一边添加相应数目0.

参数kernel_sizestride, paddingdilation数据类型: 可以是一个int类型的数据,此时卷积height和width值相同; 也可以是一个tuple数组(包含来两个int类型的数据),第一个int数据表示height的数值,tuple的第二个int类型的数据表示width的数值.

2.5、nn.Linear()

torch.nn.Linear(in_features, out_features, bias=True)

对输入数据做线性变换:y=Ax+b

参数:

  • in_features - 每个输入样本的大小
  • out_features - 每个输出样本的大小
  • bias - 若设置为False,这层不会学习偏置。默认值:True

形状:

  • 输入: (N,in_features)(N,in_features)
  • 输出: (N,out_features)(N,out_features)

变量:

  • weight -形状为(out_features x in_features)的模块中可学习的权值
  • bias -形状为(out_features)的模块中可学习的偏置

2.6、nn.BatchNorm1d()

有关Batch Normlization的具体原理:https://blog.csdn.net/qq_23262411/article/details/100175943

torch.nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1, affine=True) 

对小批量(mini-batch)的2d或3d输入进行批标准化(Batch Normalization)操作

在训练时,该层计算每次输入的均值与方差,并进行移动平均。移动平均默认的动量值为0.1。

在验证时,训练求得的均值/方差将用于标准化验证数据。

参数:

  • num_features: 来自期望输入的特征数,该期望输入的大小为'batch_size x num_features [x width]'
  • eps: 为保证数值稳定性(分母不能趋近或取0),给分母加上的值。默认为1e-5。
  • momentum: 动态均值和动态方差所使用的动量。默认为0.1。
  • affine: 一个布尔值,当设为true,给该层添加可学习的仿射变换参数。

Shape: - 输入:(N, C)或者(N, C, L) - 输出:(N, C)或者(N,C,L)(输入输出相同)

2.7、view

view: 原先tensor中的数据按照行优先的顺序排成一个一维的数据(这里应该是因为要求地址是连续存储的),然后按照参数组合成其他维度的tensor。比如说是不管你原先的数据是[[[1,2,3],[4,5,6]]]还是[1,2,3,4,5,6],因为它们排成一维向量都是6个元素,所以只要view后面的参数一致,得到的结果都是一样的。

详情可以参考:https://blog.csdn.net/york1996/article/details/81949843

三、定义损失函数与优化方式

# 2、建立模型
model = Model()
criterion = nn.CrossEntropyLoss()  # 损失函数
optimizer = optim.Adam(model.parameters(), lr=Config.learnning_rate)  # 优化器

损失函数采用交叉熵损失,越小越好。

交叉熵主要是用来判定实际的输出与期望的输出的接近程度,为什么这么说呢,举个例子:在做分类的训练的时候,如果一个样本属于第K类,那么这个类别所对应的的输出节点的输出值应该为1,而其他节点的输出都为0,即[0,0,1,0,….0,0],这个数组也就是样本的Label,是神经网络最期望的输出结果。也就是说用它来衡量网络的输出与标签的差异,利用这种差异经过反向传播去更新网络参数。

关于CrossEntropyLoss的详解:https://www.jianshu.com/p/6049dbc1b73f

关于NLLloss和CrossEntropyLoss的关系:https://blog.csdn.net/qq_22210253/article/details/85229988

四、训练模型

# 3、训练模型
total_steps = len(train_loader)
for epoch in range(Config.num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        images, labels = Variable(images.cpu()), Variable(labels.cpu())
        outputs = model(images)  # 输出
        loss = criterion(outputs, labels)  # 损失

        optimizer.zero_grad()  # 梯度置零
        loss.backward()  # 反向求导数
        optimizer.step()  # 更新模型参数

        if (i + 1) % Config.print_per_step == 0:
            _, predicted = torch.max(outputs, 1)
            correct = int(sum(predicted == labels))  # 计算预测正确个数
            accuracy = correct / Config.batch_size  # 计算准确率
            print('epochs:{}/{},steps:{}/{},Loss:{},accuracy:{}'.format(epoch + 1, Config.num_epochs,
                                                        i + 1, total_steps,loss,accuracy))

4.1、forward()

前向传播是在构建模型的类中进行定义,用forward()函数,上述代码中

outputs = model(images)表示前向传播,详解请看:https://blog.csdn.net/u011501388/article/details/84062483

前向传播和反向传播的理论:https://blog.csdn.net/qq_16137569/article/details/81449209

4.2、backward()

4.3、Variable

images, labels = Variable(images.cpu()), Variable(labels.cpu())

这里用variable的主要原因是variable可以用来自动求导。

用GPU跑的话改成下面这句:

Variable(x_train.cuda()),Variable(y_train.cuda())

对Variable的理解可以详见:https://blog.csdn.net/qq_19329785/article/details/85029116

五、测试模型

  # 4、测试模型
    total_test = len(test_data)
    test_loss = 0.
    test_correct = 0
    with torch.no_grad():
        for j, (images, labels) in enumerate(test_loader):
            images, labels = Variable(images.cpu()), Variable(labels.cpu())
            outputs = model(images)
            loss = criterion(outputs, labels)
            test_loss += loss * Config.batch_size
            _, predit = torch.max(outputs, 1)
            correct = int((predit == labels).sum())
            test_correct += correct
        accuracy = test_correct/total_test
        loss = test_loss/total_test

        print('Test loss:{},Accuracy:{},'.format(loss, accuracy))

测试模型和训练模型差不多,这里就不做详细介绍了

六、保存模型

torch.save(model.state_dict(), '../../save_model/model.ckpt')

../:意思是返回当前工程文件夹的上一级文件。

更多保存模型操作:https://blog.csdn.net/LXYTSOS/article/details/90639524

七、完整工程

创建model.py文件,代码如下:

import torch
import torch.nn as nn


class Config:
    input_size = 28 * 28
    num_classes = 10
    learnning_rate = 0.001
    num_epochs = 1
    batch_size = 64
    print_per_step = 100


class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        """
        Conv2d参数:
        第一位:input channels  输入通道数
        第二位:output channels 输出通道数
        第三位:kernel size 卷积核尺寸
        第四位:stride 步长,默认为1
        第五位:padding size 默认为0,不补
        """
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        self.conv2 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        self.fc1 = nn.Sequential(
            nn.Linear(in_features=64 * 5 * 5, out_features=128),
            nn.BatchNorm1d(num_features=128),
            nn.ReLU()
        )

        self.fc2 = nn.Sequential(
            nn.Linear(in_features=128, out_features=64),
            nn.BatchNorm1d(num_features=64),  # 加快收敛速度的方法(注:批标准化一般放在全连接层后面,激活函数层的前面)
            nn.ReLU()
        )

        self.fc3 = nn.Linear(64, 10)

    def forward(self, x):
                           #input size: torch.Size([64, 1, 28, 28])
        x = self.conv1(x)  #torch.Size([64, 32, 15, 15]) 64:batch_size,32:out_channels,first_15:height,second_15:width
        x = self.conv2(x)  #torch.Size([64, 64, 5, 5])
        x = x.view(x.size()[0], -1)  #torch.Size([64, 1600]) 每个样本被拉成一维
        x = self.fc1(x)  #torch.Size([64, 128])
        x = self.fc2(x)  #torch.Size([64, 64])
        x = self.fc3(x)  #torch.Size([64, 10])
        return x

创建train.py

import torch
from torch import nn, optim
import numpy as np
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from torch.autograd import Variable
from model import Config, Model

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
# 1、准备数据
# 训练数据
train_data = torchvision.datasets.MNIST(root='../../data',
                                        train=True,
                                        transform=transform,
                                        download=True)

# 创建训练数据迭代器
train_loader = torch.utils.data.DataLoader(dataset=train_data,
                                           batch_size=Config.batch_size,
                                           shuffle=True)

# 测试数据
test_data = torchvision.datasets.MNIST(root='../../data',
                                       train=False,
                                       transform=transform,
                                       download=True)

# 测试数据迭代器(我这个是一次加载完)
test_loader = torch.utils.data.DataLoader(dataset=test_data,
                                          batch_size=Config.batch_size,
                                          shuffle=True)

# 2、建立模型
model = Model()
criterion = nn.CrossEntropyLoss()  # 损失函数
optimizer = optim.Adam(model.parameters(), lr=Config.learnning_rate)  # 优化器

# 3、训练模型
total_steps = len(train_loader)
for epoch in range(Config.num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        images, labels = Variable(images.cpu()), Variable(labels.cpu())
        outputs = model(images)  # 输出
        loss = criterion(outputs, labels)  # 损失

        optimizer.zero_grad()  # 梯度置零
        loss.backward()  # 反向求导数
        optimizer.step()  # 更新模型参数

        if (i + 1) % Config.print_per_step == 0:
            _, predicted = torch.max(outputs, 1)
            correct = int(sum(predicted == labels))  # 计算预测正确个数
            accuracy = correct / Config.batch_size  # 计算准确率
            print('epochs:{}/{},steps:{}/{},Loss:{},accuracy:{}'.format(epoch + 1, Config.num_epochs,
                                                        i + 1, total_steps,loss,accuracy))
    # 4、测试模型
    total_test = len(test_data)
    test_loss = 0.
    test_correct = 0
    with torch.no_grad():
        for j, (images, labels) in enumerate(test_loader):
            images, labels = Variable(images.cpu()), Variable(labels.cpu())
            outputs = model(images)
            loss = criterion(outputs, labels)
            test_loss += loss * Config.batch_size
            _, predit = torch.max(outputs, 1)
            correct = int((predit == labels).sum())
            test_correct += correct
        accuracy = test_correct/total_test
        loss = test_loss/total_test

        print('Test loss:{},Accuracy:{},'.format(loss, accuracy))

torch.save(model.state_dict(), '../../save_model/model.ckpt')

# 显示前20个数据
for i in range(0, 20, 5):
    s_index = 150
    for index in range(5):
        image, label = test_data[i + index]
        image = torch.squeeze(image, 0)
        s_index += 1
        plt.subplot(s_index)
        plt.imshow(image)
        plt.text(0, -1, 'label={}'.format(label))

    plt.show()



本次project主要是调用pytorch的包来熟悉整个框架模型,后续会深入学习每个函数的原理,尽量将其中的算法复现出来。


参考的博客/网站:(参考的博客太多了,有些没有列出来,抱歉了)

1、https://blog.csdn.net/zhpf225/article/details/103766493

2、https://zhuanlan.zhihu.com/p/37857371

3、https://blog.csdn.net/chenxaioxue/article/details/82316908

4、https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch-autograd/#variable

5、https://blog.csdn.net/zhpf225/article/details/103766493

 

 

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值