[PyTorch] 基于Python和PyTorch的MNIST的手写数字数据集的分类

原文地址为:https://blog.csdn.net/qq_41683065/article/details/91374189

讲解

MNIST的介绍
好比编程入门有 Hello World,机器学习入门有 MNIST 。

MNIST的官方网址:THE MNIST DATABASE of handwritten digits
而本博客中MNIST的介绍部分的图片来自 MNIST介绍

简单来说,MNIST是一个入门级的计算机视觉数据集,它包含各种手写数字图片。
其中训练集包括60000张图片,测试集包括10000张图片。

训练集和测试集的划分非常重要。在机器学习模型设计时必须有一个单独的测试集(不用于训练而是用来评估这个模型的性能),才能更容易把模型推广到其他数据集上(泛化)。
在这里插入图片描述
MNIST的每一张图片包含 28X28 个像素点。我们可以用一个数组来表示这张图片:
在这里插入图片描述
我们把这个数组展开成一个向量,长度是 28x28 = 784。如何展开这个数组(数字间的顺序)不重要,只要保持各个图片采用相同的方式展开。从这个角度来看,MNIST数据集的图片就是在784维向量空间里面的点, 并且拥有比较部分。

须导入的函数库

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms

检查 pytorch 的版本

print(torch.__version__)                      

定义超参数

BATCH_SIZE=512  	# batch_size即每批训练的样本数量
EPOCHS=20			# 循环次数

# 让torch判断是否使用GPU,即device定义为CUDA或CPU
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu")    

batch_sizeepoch 的意义详见本人的另一篇博客 神经网络中的 batchepoch,有很详细的描述。所以在这里就不讲了。

设定超参数DEVICE的意义:
在网络实例化的时候,用.to(DEVICE)将实例化的网络放到相应的CPU或GPU上。

下载 MNIST的数据集

# 训练集
train_loader = torch.utils.data.DataLoader(                 # vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网。
        datasets.MNIST('data', train=True, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor(),       # 图像转化为Tensor
                           transforms.Normalize((0.1307,), (0.3081,))       # 标准化
                       ])),
        batch_size=BATCH_SIZE, shuffle=True)            # shuffle() 方法将序列的所有元素随机排序

# 测试集
test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('data', train=False, transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=BATCH_SIZE, shuffle=True)            # shuffle() 方法将序列的所有元素随机排序

关于batch_size,在本人的另一篇博客 神经网络中的 batchepoch专门讲过,算是非常基本和重要的概念。

定义网络
下面我们定义一个网络。

网络首先包含两个卷积层conv1conv2

之后接着两个全连接层(Linear)fc1fc2得到输出。
需要特别注意:Linear需要接收的是展平后的多维的卷积成的特征图(借用view()函数) 。

最后输出10个维度,作为0-9的标识来确定是哪个数字

建议:
最好将每一层的输入和输出维度都作为注释标注出来,这样后面阅读代码的会方便很多。

class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        # 128x28
        self.conv1=nn.Conv2d(1,10,5)         # 10, 24x24
        self.conv2=nn.Conv2d(10, 20,3)       #128, 10x10
        self.fc1=nn.Linear(20*10*10, 500)
        self.fc2=nn.Linear(500, 10)
    def forward(self, x):
        in_size=x.size(0)		# in_size 为 batch_size(一个batch中的Sample数)
        # 卷积层 -> relu -> 最大池化
        out = self.conv1(x)     # 24
        out = F.relu(out)
        out = F.max_pool2d(out, 2, 2)  # 12
        #卷积层 -> relu -> 多行变一行 -> 全连接层 -> relu -> 全连接层 -> sigmoid
        out = self.conv2(out)  # 10
        out = F.relu(out)
        out = out.view(in_size, -1)     # view()函数作用是将一个多行的Tensor,拼接成一行。
        out = self.fc1(out)
        out = F.relu(out)
        out = self.fc2(out)
        # softmax
        out = F.log_softmax(out, dim=1)
        # 返回值 out
        return out

卷积层函数nn.Conv2d()的参数意义,详见本人的另一篇博客 卷积函数 and 解卷积函数,里面对in_channelout_channelin_sizeout_size有详细的讲解。

然后我们按整个流程来走一遍前向传播

网络结构 函数
卷积层 self.conv1()
激活函数 F.relu()
最大池化层 F.max_pool2d()
卷积层 self.conv2()
激活函数 F.relu()
全连接层预处理 out = out.view(in_size, -1)
线性层 self.fc1()
激活函数 F.relu()
线性层 self.fc2()
变为概率 out = F.log_softmax(out, dim=1)

特征提取开始
第一个卷积层self.conv1=nn.Conv2d(1,10,5) :
其参数意义为:

  • 输入通道为 1 (输入图像是灰度图)
  • 输出通道为 10(10分类问题,所以需要用到的卷积核就有 10 种)
  • 卷积核 kernel_size5×5

24输出维度 = 28输入维度 - 5卷积核size + 1
所以输出 shape 为:10 × 24 × 24

第一个激活函数out = F.relu(out)
输出维度不变仍为 10 × 24 × 24

第一个最大池化层out = F.max_pool2d(out, 2, 2)
该最大池化层在 2x2 空间里向下采样。
12输出维度 = 24输入维度 / 2。
所以输出 shape 为:10 × 12 × 12

第二个卷积层self.conv2=nn.Conv2d(10, 20,3)
其参数意义为:

  • 输入通道为 10 (第一个最大池化层的输出通道数)
  • 输出通道为 20 (需要用到的卷积核就有 20 种)
  • 卷积核kernel_size为 3×3

10输出维度 = 12输入维度 - 3卷积核size + 1
所以输出 shape 为:20 × 10 × 10

第二个激活函数out = F.relu(out)
特征提取结束
输出前的数据预处理

因为全连接层Linear的输出为最后的输出,而全连接层Linear要求的输入为展平后的多维的卷积成的特征图(特征图为特征提取部分的结果)(这一点在介绍网络结构的时候专门拿黄色强调了)

out = out.view(in_size, -1)

in_size(即batch_size)个Sample拉成一维。-1表示列自适应。(关于Samplebatchbatch_size的讲解见本人的另一篇博客 神经网络中的 batchepoch)。
输出前的数据预处理结束
输出即全连接层

第一个全连接层self.fc1=nn.Linear(20*10*10, 500)
输入维度为 20 * 10 * 10= 2000
设定的输出维度为 500 × 1

第三个激活函数out = F.relu(out)
输出维度不变,仍为 500 × 1

第二个全连接层self.fc2=nn.Linear(500, 10)
输入维度为 500 × 1
输出维度设定为 10 × 1(因为是一个10分类的问题,所以最后要变成 10 × 1

第三个激活函数out = F.log_softmax(out, dim=1)
F.log_softmax()将数据的范围改到[0, 1]之内,表示概率。
输出维度仍为 10 × 1,其值可以视为概率。

return(out)

输出即全连接层结束

网络实例化

我们网络将实例化,加入输入输出接口,实例化后使用.to(DEVICE)方法将网络移动到CPUGPU。(DEVICE见超参数设定)

model = ConvNet().to(DEVICE)

定义训练函数

将训练的所有操作都封装到train函数中
在训练模型时直接调用train函数即可。

train函数需要传入的参数有;

  • model——实例化的网络
  • device——决定tensor运算的位置
  • train_loader——训练集数据
  • optimizer——优化器
  • epoch——训练次数

第二行的model.train()将模型转为训练模式,因为一些模块在trainevaluation表现不同(比如Dropout

定义 训练函数 ,将训练的所有操作都封装到train函数中

def train(model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)       # CPU转GPU
        optimizer.zero_grad()               # 优化器清零
        output = model(data)                # 由model,计算输出值
        loss = F.nll_loss(output, target)   # 计算损失函数loss
        loss.backward()                     # loss反向传播
        optimizer.step()                    # 优化器优化
        if(batch_idx+1)%30 == 0:            # 输出结果
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))
# -------------------------------------------------------------

enumerate()函数的讲解,可见本人的另一篇博客 索引序列函数:enumerate() / enumerate(sequence, start=0)。
简单来说这是个enumerate()用于for循环的例子。
enumerate(train_loader)train_loader组合成了一个索引序列,为 数据 增加了 数据下标。
for batch_idx, (data, target) in enumerate(train_loader):这一行中,
我们通过batch_idx来获取数据下标,用data获得待处理的数据(由手写数字的image转成的,被标准化的tensor,详见训练数据集下载的part),用target获取实际值。最后由for循环遍历整个train_loader

定义测试函数
将测试的所有操作都封装到test函数中
在测试模型时直接调用test函数即可。

test函数需要传入的参数有;

  • model——实例化的网络
  • device——决定tensor运算的位置
  • test_loader——训练集数据
# ---------------------测试函数------------------------------
# 测试的操作也一样封装成一个函数
def test(model, device, test_loader):
    test_loss = 0                           # 损失函数初始化为0
    correct = 0                             # correct 计数分类正确的数目
    with torch.no_grad():           # 表示不反向求导(反向求导为训练过程)
        for data, target in test_loader:    # 遍历所有的data和target
            data, target = data.to(device), target.to(device)   # CPU -> GPU
            output = model(data)            # output为预测值,由model计算出
            test_loss += F.nll_loss(output, target, reduction='sum').item()     ### 将一批的损失相加
            pred = output.max(1, keepdim=True)[1]       ### 找到概率最大的下标
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset) 	# 总损失除数据集总数
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))
# ---------------------------------------------------------------

with torch.no_grad()表示不反向求导。训练过程需要反向求导(去更新优化模型参数),测试过程不需要反向求导。

for data, target in test_loader:为遍历整个test_loader,分别用datatarget 获取待处理的tensor和实际值。

correct += pred.eq(target.view_as(pred)).sum().item()中的.view_as(pred)是改变shape的函数(详细见本人的另一篇博客 更改矩阵形状——.reshape(m,n).view(m,n)view_as(tensor))。
在这里target.view_as(pred)targetshape变成了和pred一样(shape统一了才能用.eq()比较)

主函数

进行网络的训练和测试。

# 下面开始训练,这里就体现出封装起来的好处了,只要写两行就可以了
# 整个数据集只过一遍
for epoch in range(1, EPOCHS + 1):
    train(model, DEVICE, train_loader, optimizer, epoch)
    test(model, DEVICE, test_loader)

range(1, EPOCHS + 1)表示执行EPOCHS次。

全部源代码

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms   
# torchvision是独立于pytorch的关于图像操作的一些方便工具库。
# vision.datasets : 几个常用视觉数据集,可以下载和加载
# vision.models : 流行的模型,例如 AlexNet, VGG, ResNet 和 Densenet 以及训练好的参数。
# vision.transforms : 常用的图像操作,例如:数据类型转换,图像到tensor ,numpy 数组到tensor , tensor 到 图像等。
# vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网

print(torch.__version__)                        # 检查 pytorch 的版本

# 定义一些超参数
BATCH_SIZE=512                                  # batch_size即每批训练的样本数量
EPOCHS=20                                       # 循环次数
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu")     # 让torch判断是否使用GPU,即device定义为CUDA或CPU

# 下载 MNIST的数据集
# 训练集
train_loader = torch.utils.data.DataLoader(                 # vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网。
        datasets.MNIST('data', train=True, download=True,
                       transform=transforms.Compose([
                           transforms.ToTensor(),       # 图像转化为Tensor
                           transforms.Normalize((0.1307,), (0.3081,))       # 标准化(参数不明)
                       ])),
        batch_size=BATCH_SIZE, shuffle=True)            # shuffle() 方法将序列的所有元素随机排序

# 测试集
test_loader = torch.utils.data.DataLoader(
        datasets.MNIST('data', train=False, transform=transforms.Compose([
                           transforms.ToTensor(),
                           transforms.Normalize((0.1307,), (0.3081,))
                       ])),
        batch_size=BATCH_SIZE, shuffle=True)            # shuffle() 方法将序列的所有元素随机排序


# 下面我们定义一个网络,网络包含两个卷积层,conv1和conv2,
# 然后紧接着两个线性层作为输出,
# 最后输出10个维度,这10个维度我们作为0-9的标识来确定识别出的是那个数字

# 这里建议大家将每一层的输入和输出维度都作为注释标注出来,这样后面阅读代码的会方便很多
class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        # 128x28
        self.conv1=nn.Conv2d(1,10,5)         # 10, 24x24
        self.conv2=nn.Conv2d(10, 20,3)       #128, 10x10
        self.fc1=nn.Linear(20*10*10, 500)
        self.fc2=nn.Linear(500, 10)
    def forward(self, x):
        in_size=x.size(0)		# in_size 为 batch_size(一个batch中的Sample数)
        # 卷积层 -> relu -> 最大池化
        out = self.conv1(x)     # 24
        out = F.relu(out)
        out = F.max_pool2d(out, 2, 2)  # 12
        #卷积层 -> relu -> 多行变一行 -> 全连接层 -> relu -> 全连接层 -> sigmoid
        out = self.conv2(out)  # 10
        out = F.relu(out)
        out = out.view(in_size, -1)     # view()函数作用是将一个多行的Tensor,拼接成一行。
        out = self.fc1(out)
        out = F.relu(out)
        out = self.fc2(out)
        # softmax
        out = F.log_softmax(out, dim=1)
        # 返回值 out
        return out
        
# 我们实例化一个网络,实例化后使用“.to”方法将网络移动到GPU
model = ConvNet().to(DEVICE)

# 优化器我们也直接选择简单暴力的Adam
optimizer = optim.Adam(model.parameters())


# 定义 训练函数 ,我们将训练的所有操作都封装到train函数中
def train(model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)       # CPU转GPU
        optimizer.zero_grad()               # 优化器清零
        output = model(data)                # 由model,计算输出值
        loss = F.nll_loss(output, target)   # 计算损失函数loss
        loss.backward()                     # loss反向传播
        optimizer.step()                    # 优化器优化
        if(batch_idx+1)%30 == 0:            # 输出结果
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))
# -------------------------------------------------------------

# ---------------------测试函数------------------------------
# 测试的操作也一样封装成一个函数
def test(model, device, test_loader):
    test_loss = 0                           # 损失函数初始化为0
    correct = 0                             # correct 计数分类正确的数目
    with torch.no_grad():           # 表示不反向求导(反向求导为训练过程)
        for data, target in test_loader:    # 遍历所有的data和target
            data, target = data.to(device), target.to(device)   # CPU -> GPU
            output = model(data)            # output为预测值,由model计算出
            test_loss += F.nll_loss(output, target, reduction='sum').item()     ### 将一批的损失相加
            pred = output.max(1, keepdim=True)[1]       ### 找到概率最大的下标
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset) 	# 总损失除数据集总数
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))
# ---------------------------------------------------------------

# 下面开始训练,这里就体现出封装起来的好处了,只要写两行就可以了
# 整个数据集只过一遍
for epoch in range(1, EPOCHS + 1):
    train(model, DEVICE, train_loader, optimizer, epoch)
    test(model, DEVICE, test_loader)
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值