Pytorch实战(1)-Mnist手写数据集 (作为实战入门)

主要是使用LeNet进行手写数字识别。

一、介绍LeNet网络

  • 结构图

  • 详细展开

它的输入尺寸是32×32。

  • 重点介绍一下卷积层、池化层和全连接层
卷积层:通过卷积运算,可以使原信号特征增强,并且降低噪音。
# ------------------------------------------------------------------
解析代码:nn.Conv2d(1, 6, 5, stride=1, padding=2)
第一个参数:输入图片的维数(灰度为1,彩色图像为3)
第二个参数:使用5*5大小的过滤器6个
第三个参数:卷积核(过滤器)大小为5*5
第四个参数:stride为每次移动卷积核的步长为1
第五个参数:padding为填充

如上代码, 对于mnist内的图片大小为28*28,而LeNet网络的输入大小为32*32
          根据padding的大小,将mnist填充为32*32
          经过卷积层(32 - 5 + stride)输出的特征图大小为28*28*6
# ----------------------------------------------------------------

池化层(下采样层):利用图像局部相关性的原理,对图像进行子抽样,可以1.减少数据处理量同时保留有用信息,2.降低网络训练参数及模型的过拟合程度
# ------------------------------------------------------------------
解析代码:nn.ReLU(True)
         nn.MaxPool2d(2, 2)
激活后池化,使用2*2大小的过滤器,步长为2,padding=0.


如上代码,输入28*28*6大小的特征图,池化后,输出14*14*6大小的特征图
# ----------------------------------------------------------------

全连接层:
# ------------------------------------------------------------------
解析代码:nn.Linear(400, 120)
每个单元与上一次的全部5*5*16(400)个单元直接进行全连接。120*(400+1)=48120个可训练参数。如同经典神经网络,全连接层层计算输入向量和权重向量之间的点积,再加上一个偏置。
# ----------------------------------------------------------------

输出层:
# ------------------------------------------------------------------
输出层由欧式径向基函数(Euclidean Radial Basis Function)单元组成,每类一个单元,每个有84个输入。 
换句话说,每个输出RBF单元计算输入向量和参数向量之间的欧式距离。
输入离参数向量越远,RBF输出的越大。
用概率术语来说,RBF输出可以被理解为F6层配置空间的高斯分布的负log-likelihood。
给定一个输式,损失函数应能使得F6的配置与RBF参数向量(即模式的期望分类)足够接近。
# ----------------------------------------------------------------

  • LeNet网络的特点

           1)、每个卷积层包含三部分:卷积、池化和非线性激活函数

            2)、使用卷积提取空间特征

            3)、降采样(Subsample)的平均池化层

            4)、双曲正切(tanh)和S型(Sigmoid)的激活函数,MLP作为最后的分类器

            5)、层与层之间的稀疏连接减少计算复杂度

二、加载数据集

   在这之前先看一下数据预处理:

直接看https://pytorch.org/docs/stable/torchvision/transforms.html#

from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets

# 下载训练集 MNIST 手写数字训练集
train_dataset = datasets.MNIST(
    root='./mnist', train=True, transform=transforms.ToTensor(), download=True)

test_dataset = datasets.MNIST(
    root='./mnist', train=False, transform=transforms.ToTensor())

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

# -------------------------------------------------------------------------------
torchvision.datasets.MNIST(
    root,
    train = True,
    transform = None,
    target_transform = None,
    download = False )

root(string):保存数据集的根目录
train(bool):如果为true,则从training.pt中创建数据集,否则从test.pt中创建数据集
transform(可调用的):一个方法或者transform,它接受一个PIL图像并返回transform,例如 
 transforms.RandomCrop
download(bool):如果true,则从网上下载数据集并放在根目录下,如果已经下载了就不会再次下载。
target_transform :接受目标并对其进行转换的函数/转换
# -----------------------------------------------------------------------------------
DataLoader(
    dataset,  # 数据集
    batch_size=1,  # int,每个批次要加载的样本数,默认为1
    shuffle=False,  # bool,设置为true,则每个epoch,reshuffle数据,默认为false
    sampler=None,  # sample,定义数据集中提取样本的策略,如果指定,则shuffle必须为false
    batch_sampler=None,  # 类似sampler,但是一次返回一批索引。互斥有batch_size,shuffle, 
                         # sampler,和drop_last。
    num_workers=0,   # int,用于数据加载的子进程数。0表示将在主进程中加载数据,默认值为0
    collate_fn=None, # 合并样本列表以形成张量的小批量
    pin_memory=False, 
    drop_last=False, 
    timeout=0,
    worker_init_fn=None)
# --------------------------------------------------------------------------------------

三、定义LeNet网络模型

# 定义LeNet网络
class Cnn(nn.Module):
    # nn.Moudle子类的函数必须在构造函数中执行父类的构造函数
    # 下式等价与nn.Moudle.__init__(self)
    def __init__(self, in_dim, n_class):
        super(Cnn, self).__init__()
        # super用法:Cnn继承父类nn.Model的属性,并用父类的方法初始化这些属性
        self.conv = nn.Sequential(
            # padding=2保证输入输出尺寸相同(参数依次是:输入维度,输出深度,ksize,步长,填充)
            # 卷积层‘1’表示输入图片为单通道,‘6’表示输出通道数
            # ‘5’表示卷积核为5*5
            nn.Conv2d(in_dim, 6, 5, stride=1, padding=2),
            nn.ReLU(True),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(6, 16, 5, stride=1, padding=0),
            nn.ReLU(True),
            nn.MaxPool2d(2, 2))

        # 全连接层
        self.fc = nn.Sequential(
            nn.Linear(400, 120),
            nn.Linear(120, 84),
            nn.Linear(84, n_class))

    # 前向传播
    def forward(self, x):
        out = self.conv(x)
        # print(out.size()):torch.Size([128, 16, 5, 5])
        # 而经过卷积池化后的特征图大小为5*5*16
        # 但是全连接层的输入为400,所有要将out的大小改为400
        # 使用view
        out = out.view(out.size(0), -1)
        # 此时out的size为400,由于批次为128,所有128*400
        out = self.fc(out)
        # 返回out的size为128*10
        return out

# 实例化LeNet网络
model = Cnn(1, 10)  # 图片大小为28*28,图片维度为1,最终输出的是10类

四、定义损失函数和优化器

criterion = nn.CrossEntropyLoss()  # 交叉熵损失

# 随机梯度下降法SGD,指定要调整的参数和学习率
# learning_rate = 1e-2    # 学习率
optimizer = optim.SGD(model.parameters(), lr=learning_rate)

五、训练阶段

num_epoches = 20        # 遍历训练集的次数
for epoch in range(num_epoches):
    running_loss = 0.0  # 运行损失
    running_acc = 0.0  # 精度

    # ----------------------------------------------
    # 对enumerate(iterator, start)的解释:
    # numerate()用于将可迭代、可遍历的数据对象组合为一个索引序列,同时列出数据和数据下标。
    # e2 = enumerate(list, 4)
    # for i in e2:
    #    print(i)
    # 输出结果:
    # (4, 'A')
    # (5, 'B')
    # (6, 'C')
    # (7, 'D')
    # ----------------------------------------------
    # 遍历训练集的每一张图片
    for i, data in enumerate(train_loader, 1):
        img, label = data

        #-------------------------------
        # cuda
        #if use_gpu:
            #img = img.cuda()
            #label = label.cuda()
        # ------------------------------
        
        # 将img和label转换为Variable
        img = Variable(img)
        label = Variable(label)

        # 前向传播
        out = model(img)
        # 计算损失
        loss = criterion(out, label)  # 计算交叉熵损失
        running_loss += loss.item() * label.size(0)  # 
        _, pred = torch.max(out, 1)  # 预测最大值所在位置的标签,即预测的数字。维度设为1
        num_correct = (pred == label).sum()  # 预测正确的数目
        accuracy = (pred == label).float().mean()
        running_acc += num_correct.item()  # 统计预测正确的总数

        # 反向传播
        # grad在反向传播过程中是累加的,这意味着每次运行反向传播,梯度都会累加之前的梯度, 
        # 所以反向传播之前需把梯度清零。
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()  # 更新参数
    print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
        epoch + 1, running_loss / (len(train_dataset)), running_acc / (len(train_dataset))))

 

六、测试阶段

    # model.train() :启用 BatchNormalization 和 Dropout
    # model.eval() :不启用 BatchNormalization 和 Dropout
    # 训练阶段需要启用,而测试阶段不需要。
    model.eval()  # 模型评估
    eval_loss = 0  # 评估损失
    eval_acc = 0  # 评估精度

    for data in test_loader:
        img, label = data
        
        # -------------------------------------------------
        # if use_gpu:
        #     img = Variable(img, volatile=True).cuda()
        #     label = Variable(label, volatile=True).cuda()
        # else:
        #     img = Variable(img, volatile=True)
        #     label = Variable(label, volatile=True)
        # --------------------------------------------------
        
        # 开始测试图片
        out = model(img)
        loss = criterion(out, label)
        eval_loss += loss.item() * label.size(0)

        _, pred = torch.max(out, 1)  # 预测最大值所在位置的标签,即预测的数字。维度设为1
        num_correct = (pred == label).sum()  # 预测正确的数目
        eval_acc += num_correct.item()  # 统计预测正确的数目
    print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(
        test_dataset)), eval_acc / (len(test_dataset))))

七、保存和加载模型

  • 保存模型

      保存整个模型的结构和参数,保存对象为model
      torch.save(model,’./model.pth’)
      保存对象的参数,保存的对象是模型的状态model.state_dict()
      torch.save(model.state_dict(),’./model_state.pth)

# 保存模型
# 将状态为state_dict()的模型保存在当前目录下,命名为cnn.pth
torch.save(model.state_dict(), './cnn.pth')
  • 加载模型

      加载模型结构和参数
      load_model=torch.load('model.pth')
      加载模型参数信息,需要先导入模型结构,然后通过model.load_state_dic(torch.load(‘model_state.pth’))导入

# 加载模型
load_model = torch.load('cnn.pth')

八、总结

  • 在处理一个数据集时,首先要查看该数据集的结构,每张图片的大小。
  • 设置好超参数,比如学习率、训练次数等。
  • 了解采用网络的整体框架,知道每一步的流程。
  • 要知道训练模型与测试模型之间的区别。
  • 测试模型时,不需要网络的后面两层。(视情况而定)
  • 在后向传播之前要记得梯度清零。
  • 熟悉所采用的损失函数,为什么要采用损失函数。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值