实验十一、使用PyTorch训练MNIST数据集上的卷积神经网络

标题第1关:使用PyTorch建立网络模型

任务描述
本关任务:使用 PyTorch 建立网络模型。

相关知识
为了完成本关任务,你需要掌握:

PyTorch 基础;
使用 PyTorch 建立网络模型。
PyTorch简介
进行深度学习研究离不开深度学习框架。目前主流的深度学习框架包括 Google 发布的 TensorFlow、Facebook 发布的 PyTorch 和 Amazon 发布的 MXNet 等。根据深度学习框架的运行方式,通常可以分为静态图设计和动态图设计两种。在静态图设计中,计算图的构建在网络模型的计算之前,对于用户定义的网络模型,深度学习框架会先进行一个“编译”的过程,将网络模型转化成计算图,之后的计算都根据这个计算图来进行。因为计算图在网络模型计算之前已经确定,因此静态图设计可以预先对计算图进行一系列的优化,因此往往具有更好的效率;但是,计算图不能在网络计算的过程中改变,损失了一定的灵活性,同时使得开发和调试变得困难。另一方面,在动态图设计中,计算图的构建在网络模型的计算过程中动态生成,因此非常灵活,开发和调试都非常方便,与一般的 Python 程序无异。但是,因为网络模型计算过程中还要构建计算图,因此效率上会比静态图差一些。通常来说,静态图设计在工业界更为流行,而动态图设计更受到学术界的青睐。

PyTorch 是 Facebook 推出的深度学习框架,自从问世以来,凭借其出色的灵活性和高性能深受欢迎。PyTorch 采用的是动态图设计,因此使用 PyTorch 进行深度学习研究非常高效且灵活。PyTorch 的学习是一个非常庞大的工程,PyTorch 的官方文档和官方指南是学习 PyTorch 的非常好的资料,希望同学们在学习过程中更多的借鉴上面的资料。

使用PyTorch建立网络模型的方法
PyTorch 的基础数据结构是torch.Tensor,tensor 即张量,可以理解为一个多维数组,与我们之前学习计算图时提到的数据节点相对应。而我们之前学习的计算节点,则与torch.autograd.Function相对应。如果将计算节点与其附属的参数放在一起,则构成了torch.nn.Module。如果你仔细阅读了上面的资料,相信你对这些并不陌生。

在 PyTorch 中,torch.nn.Module是我们定义网络模型时最常使用的类。顾名思义,该类定义了一个网络模块。一个网络模型可能由许多网络模块组成,例如卷积层就是一个最基本的网络模块,这在 PyTorch 中由torch.nn.Conv2d给出,这就是一个torch.nn.Module的子类。而整个网络模型更是可以看作是一个最大的网络模块,因此也需要使用torch.nn.Module来定义。在使用torch.nn.Module时,需要实现其构造函数__init__和前向传播函数forward。在构造函数中,需要定义其参数以及子模块。在前向传播函数中,需要实现其前向传播的过程。torch.nn.Module本身是 callable 的,你可以直接对其进行调用,调用时会执行forward方法。值得注意的是,你不需要考虑反向传播的过程,因为 PyTorch 会通过 autograd 机制自动的进行反向传播,前提是你使用的所有计算都是通过 torch 提供的库及其扩展实现的。下面给出了一个感知机的实现,在构造函数中,定义了感知机的权重和偏置,在前向传播函数中,定义了其前向传播的计算:

from torch import nn
class Percepton(nn.Module):
def init(self):
super().init()
self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
self.bias = nn.Parameter(torch.zeros(10))
def forward(self, xb):
return xb @ self.weights + self.bias > 0
使用PyTorch建立TinyNet网络模型
在本实训中,你需要使用 PyTorch 实现一个用于 MNIST 手写数字识别任务的 TinyNet 网络模型。MNIST 数据集包含 60000 张训练图片和 10000 张测试图片,每张图片是一个 28×28 的黑白图像,每张包含一个手写数字。因为数字是从 0-9,所以这个任务可以看作是一个有 10 个类别的分类任务。因为是黑白图像,所以输入图片只有一个通道,是一个 (B,1,28,28) 的 Tensor,其中 B 是 batch size。下图展示了一部分 MNIST 数据集中的数据。

图1
图1 MNIST数据集
你可以自行设计 TinyNet 的结构,这里给出一种参考设计,包含两个卷积层和两个全连接层,整个结构如下表:

序号 类型 参数 输出特征图大小
0 输入 (B, 1, 20, 20) -
1 卷积层 输出通道32,卷积核大小3x3,步长1,填充1 (B, 32, 20, 20)
2 激活函数 ReLU (B, 32, 20, 20)
3 卷积层 输出通道64,卷积核大小3x3,步长1,填充1 (B, 64, 20, 20)
4 激活函数 ReLU (B, 64, 20, 20)
5 池化层 最大值池化,池化窗口2x2,步长2,填充0 (B, 64, 10, 10)
6 Dropout p=0.25 (B, 64, 10, 10)
7 全连接层 输出神经元128 (B, 128)
4 激活函数 ReLU (B, 128)
6 Dropout p=0.5 (B, 128)
7 全连接层 输出神经元10 (B, 10)
需要注意的是,在 6-7 层之间,需要进行一次 flatten 操作使得 Tensor 的形状能够满足全连接层的要求。

编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,使用 PyTorch 建立网络模型。

测试说明
在本实训中,因为网络模型的初始化是随机的,因此只要你的网络模型的输出的形状是正确的即可,网络模型的训练和有效性验证我们留到下一关进行。

开始你的任务吧,祝你成功!

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


class Net(nn.Module):
    def __init__(self):
        ########## Begin ##########
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)

        ########## End ##########

    def forward(self, x):
        ########## Begin ##########
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output

        ########## End ##########


标题第2关:使用PyTorch训练网络模型

任务描述
本关任务:使用 PyTorch 训练网络模型。

相关知识
为了完成本关任务,你需要掌握:

使用 PyTorch 载入数据;
使用 PyTorch 训练网络模型。
使用PyTorch载入数据
训练网络模型首先需要准备数据。在 PyTorch 中,载入数据需要用到两个类,分别是torch.utils.data.Dataset和torch.utils.data.DataLoader。前者用来定义一个数据集,需要实现__init__、__getitem__和__len__三个方法。__init__方法用来初始化这个数据集,__getitem__用来根据索引返回数据集中的一个数据,__len__用来返回数据集中数据的总个数。在定义好数据集之后,可以用torch.utils.data.DataLoader来对数据集进行读取。其工作原理为:torch.utils.data.DataLoader会先调用数据集的__len__获取数据集大小,之后按照其本身的参数设置,按照随机或者非随机的方式调用数据集的__getitem__方法来获取单个的数据,最后通过collate_fn来将多个数据组合成一个 batch。更多细节可以参考 PyTorch 的官方文档。

在本实训中,我们将用到 MNIST 数据集。这是计算机视觉领域的一个常用数据集,因此 PyTorch 已经内置了其实现。我们将会直接使用这个实现。如果你想了解实现的细节,可以参考官方实现进行学习。

在使用torch.utils.data.Dataset和torch.utils.data.DataLoader构建好 data loader 之后,就可以用通过迭代的方法来获取数据:

import torch
from torchvision import datasets, transforms
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
data_root = ‘path/to/mnist/data’
kwargs = {‘num_workers’: 1, ‘pin_memory’: True}
dataset = datasets.MNIST(data_root, train=True, download=False, transform=transform)
train_loader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True, **kwargs)
for data, target in train_loader:
# train network code
使用PyTorch训练网络模型
在解决了数据的问题之后,下一步我们来解决模型的训练。在上一关中,我们学习了对每个torch.nn.Module,都要实现一个forward方法,并且知道torch.nn.Module是 callable 的,对其进行调用会直接调用 forward 方法。通过这些操作,可以实现网络的前向传播,即预测部分。

但是,对于网络的训练来说,还缺了很多部分。首先是损失函数。因为我们的目标是 MNIST 数据集上的分类任务,所以我们使用交叉熵,你可以通过torch.nn.CrossEntropy或者torch.nn.functional.cross_entropy来调用。在计算了 loss 之后,下一步需要进行的就是反向传播,这里可以通过对 loss 调用backward方法来实现。

然后你还需要一个优化器,我们之前学习的随机梯度下降就是一种优化器。PyTorch 提供了一系列不同的优化器,在torch.optim中,包括 SGD、Adam、AdaDelta、RMSProp 等,因为其背后的原理复杂,这里不做过多的介绍,希望了解更多的同学可以阅读这篇文章。为了方便你的实现,我们推荐使用 AdaDelta 来完成你的实训,并且设置学习率为 0.1。这里需要注意一个细节,在反向传播之前,需要调用 optimizer.zero_grad() 来清空之前 batch 计算过的梯度,防止出错。最后,调用 optimizer.step() 就可以。

在网络模型的训练过程中,学习率并不是一成不变的,通常会随着训练的进行进行衰减。这就需要使用torch.optim.lr_scheduler来对学习率进行处理。同样的,PyTorch 也提供了很多处理学习率的方法,这里我们推荐使用 StepLR,每个 epoch 学习率衰减为之前的 0.7 倍。同样的,在每个 epoch 的最后,调用 lr_scheduler.step() 就可以实现对学习率的调节。

总结一下,网络的训练分为以下步骤:

定义损失函数、优化器、学习率调节器;

载入数据,对于每个 batch,进行以下操作:

a. 清空之前的梯度;
b. 前向传播;
c. 计算 loss;
d. 反向传播;
e. 优化器更新;

每个 epoch 结束时,学习率调节器更新。

编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,使用 PyTorch 实现并训练网络模型。首先,你需要重新实现一个网络模型,这个网络模型你可以自行设计,也可以采用上一关中的推荐模型。之后,你需要实现训练代码,对网络模型进行训练。

在实现训练代码时,你需要完成train函数,测试代码会根据你实现的网络模型类,创建一个网络模型实例,然后将 data loader 以及训练的总 epoch 数传入 train 函数,你需要对网络模型训练 num_epoch 个 epoch 之后返回。

测试说明
平台会对你编写的代码进行测试。我们会对你训练完成的网络模型进行测试,跑通即可通关。

开始你的任务吧,祝你成功!

from __future__ import print_function
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.optim.lr_scheduler import StepLR


class Net(nn.Module):
    def __init__(self):
        ########## start ##########
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(9216, 128)
        self.fc2 = nn.Linear(128, 10)
    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = F.max_pool2d(x, 2)
        x = self.dropout1(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.dropout2(x)
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output

        ########## end ##########


def train(args, model, device, train_loader, optimizer, epoch):
    ########## start ##########
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step() 
    ########## end ##########
        if batch_idx % args.log_interval == 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()))
            if args.dry_run:
                break


def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item()  # sum up batch loss
            pred = output.argmax(dim=1, keepdim=True)  # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    test_acc = 100. * correct / len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        test_acc))
    if test_acc > 95:
        print("success!")

def main():
    # Training settings
    parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
    parser.add_argument('--batch-size', type=int, default=64, metavar='N',
                        help='input batch size for training (default: 64)')
    parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
                        help='input batch size for testing (default: 1000)')
    parser.add_argument('--epochs', type=int, default=1, metavar='N',
                        help='number of epochs to train (default: 14)')
    parser.add_argument('--lr', type=float, default=1.0, metavar='LR',
                        help='learning rate (default: 1.0)')
    parser.add_argument('--gamma', type=float, default=0.7, metavar='M',
                        help='Learning rate step gamma (default: 0.7)')
    parser.add_argument('--no-cuda', action='store_true', default=False,
                        help='disables CUDA training')
    parser.add_argument('--dry-run', action='store_true', default=False,
                        help='quickly check a single pass')
    parser.add_argument('--seed', type=int, default=1, metavar='S',
                        help='random seed (default: 1)')
    parser.add_argument('--log-interval', type=int, default=10, metavar='N',
                        help='how many batches to wait before logging training status')
    parser.add_argument('--save-model', action='store_true', default=False,
                        help='For Saving the current Model')
    args = parser.parse_args()
    use_cuda = not args.no_cuda and torch.cuda.is_available()

    torch.manual_seed(args.seed)

    device = torch.device("cuda" if use_cuda else "cpu")

    train_kwargs = {'batch_size': args.batch_size}
    test_kwargs = {'batch_size': args.test_batch_size}
    if use_cuda:
        cuda_kwargs = {'num_workers': 1,
                       'pin_memory': True,
                       'shuffle': True}
        train_kwargs.update(cuda_kwargs)
        test_kwargs.update(cuda_kwargs)

    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
        ])
    dataset1 = datasets.MNIST('/data/workspace/myshixun/data', train=True, download=True,
                       transform=transform)
    dataset2 = datasets.MNIST('/data/workspace/myshixun/data', train=False,
                       transform=transform)
    train_loader = torch.utils.data.DataLoader(dataset1,**train_kwargs)
    test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs)

    model = Net().to(device)
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)

    scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)
    for epoch in range(1, args.epochs + 1):
        train(args, model, device, train_loader, optimizer, epoch)
        test(model, device, test_loader)
        scheduler.step()

    if args.save_model:
        torch.save(model.state_dict(), "mnist_cnn.pt")


if __name__ == '__main__':
    main()

你可能感兴趣的:(python,深度学习)