PyTorch学习笔记(3)

PyTorch学习笔记(3)

文章目录

  • PyTorch学习笔记(3)
    • 0 本文来历
    • 1 PyTorch作为框架,向使用者提供了什么?
    • 2 不用PyTorch框架特性搭建网络
      • 2.1 MNIST数据集的建立
      • 2.2 不使用torch.nn从零建立神经网络
    • 3 开始用PyTorch特性模块替换
      • 3.1 使用torch.nn函数
      • 3.2 使用nn.Module模块
      • 3.3 通过nn.Linear重构
      • 3.4 使用optim重构
      • 3.5 使用Dataset重构
      • 3.6 使用DataLoader重构
    • 4 添加的其他功能
      • 4.1 添加验证集
      • 4.2 创建fit()和get_data()
    • 5 建立一个CNN模型
      • 5.1 切换到CNN模型
      • 5.2 nn.Sequential
      • 5.3 封装DataLoader
    • 6 使用GPU
    • 7 总结

0 本文来历

​ 这是PyTorch学习笔记系列的第三篇,它来自于对PyTorch官网上What is torch.nn really【传送门】一文的学习. 但这篇文章不是对原文的翻译. 笔记的目的是从学习资料中提取出主干架构,从而建构自己的知识体系. 所以笔记是因人而异的,可能只适合于笔记记录者本人,也可能对其他水平相仿佛的人有一定的参考价值. 在此,因笔者水平所限,无法保证内容的正确性和完整性,请读者加以鉴别.

1 PyTorch作为框架,向使用者提供了什么?

​ 笔者的一个理解:所谓框架,Framework,需要搭建起实现某项功能的基本脉络. PyTorch作为深度学习的框架,它也需要向常用的深度学习任务提供实现的基本脉络.

​ 文章提到,PyTorch提供了“优雅设计过的模块和类”,以下划重点:torch.nn, torch.optim, Dataset, DataLoader. 文章为了充分说明这四个模块和类的使用,以MNIST数据集为例,通过这几个典型模块,实现了一个神经网络模型. 所以作为笔记,我们接下来重点记录一下这几个框架在搭建神经网络模型时所提供的便利特性.

2 不用PyTorch框架特性搭建网络

2.1 MNIST数据集的建立

​ 模型不是无源之水,无本之木,需要有数据. MNIST是一个手写体数字集,赫赫有名,不再赘述其基本情况. 文章使用该数据集进行网络的训练和测试. 首先是通过Python3标准库中的pathlib来进行MNIST数据的下载和读取:

from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"

PATH.mkdir(parents=True, exist_ok=True)
# 在当前文件夹下生成/data/mnist/路径,exist_ok表示如果要生成的路径已存在,忽略此错误

URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
FILENAME = "mnist.pkl.gz"

# 如果文件未下载,则利用requests模块来进行访问下载
if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)

​ 使用pickle模块和gzip模块来读取数据集文件:

import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")

​ 注意编码方式为"latin-1", 读写方式为二进制方式读取,即"rb".

​ 用训练集元组(x_train, y_train)和验证集(x_valid, y_valid)来接收读取的数据.

​ 每一幅图片都是 28 × 28 28\times28 28×28的大小,在压缩文件中,它被扁平化保存为一个长度为784( = 28 × 28 =28\times28 =28×28)的向量. 为了显示该图像,需要首先将之转换为2d数据.

from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)

​ 引入matplotlib库,将训练集数据中的第一个(即下标为0)维度改变为(28, 28), 颜色表为"gray"即黑白.输出结果为:

PyTorch学习笔记(3)_第1张图片

(50000, 784)

从输出的元组可以看出,训练集是一个大小为50000,每一条数据有784个数据点构成.

PyTorch使用torch.tensor,而不是用numpy数组,所以需要先转换数据:

import torch

x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())
tensor([[0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.]]) tensor([5, 0, 4,  ..., 8, 4, 8])
torch.Size([50000, 784])
tensor(0) tensor(9)

​ 从输出的结果来看,这一步我们生成了训练集的数据,其中,x_train记录的是图像数据,y_train记录的是图像标签.

​ 通常进行数据建模之前,我们需要对将要分析的数据集有一个大概的了解,包括数据标签的最大最小值等等,在MNIST这个训练集中,标签的最小值是0,最大值是9.

​ 特别要注意的是,x_train的数据并不是看上去的那样全部为0,只是恰好因为图像数据的边角都是灰度为0的纯黑色.

​ 以上,完成对MNIST数据集的读取操作. (每一步都很长,确实很长,但是学习的乐趣正在于此,不是么?)

2.2 不使用torch.nn从零建立神经网络

from scratch1

​ PyTorch提供可以创建随机数或全零0数据的tensor,这使得我们可以为我们的简单线性模型创建权重和偏置. 这是非常常规的tensors,但是有一个特别的地方:我们告诉PyTorch我们需要梯度. 这会导致PyTorch记录对tensor所做的所有操作,这才使得它可以在反向传播之时自动计算梯度.

​ 初始化之后,我们设置requires_grad,因为我们不希望梯度中包含这个步骤.(在PyTorch中,后缀_表示这个操作会对自身起效.

我们初始化权重的策略用的是Xavier initialisation(即乘以1/sqrt(n))

​ 归功于PyTorch自动计算梯度的能力,我们可以使用任何Python标准函数作为模型,所以让我们只写一个最简单最纯粹的矩阵简洁,并广播附加项,创建一个最简单的线性模型. 我们也需要一个激活函数,所以我们写一个log_softmax来使用. 切记:尽管PyTorch提供很多预先定义好的损失函数、激活函数,但你仍然可以用纯粹的python写就自己的损失函数. PyTorch甚至可以自动创建快速的GPU或者向量化的CPU代码.

def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)
  • 注意代码中的特殊用法,其中sum(-1)是在计算矩阵的行和,sum(0)是在计算矩阵的列和.

  • unsqueeze()函数是PyTorch中定义的,和squeeze()一样是对数据进行维度扩展/压缩.

  • unsqueeze(-1)的意思是在tensor的倒数第一维上添加一个维度

  • 在上式中,@表示矩阵乘法.

    ​ 以下代码中出现了深度学习的一个概念,叫作batch,batch原意为一批,这里可以理解为将若干个源数据作为一次输入:

bs = 64  # batch size

xb = x_train[0:bs]  # a mini-batch from x
preds = model(xb)  # predictions
preds[0], preds.shape
print(preds[0], preds.shape)

​ 上述代码中,一个batch的大小为64.

​ model为我们自己定义的一个计算函数,返回由输入和权重做矩阵乘法,再加上偏置,再通过log_softmax函数输出结果. 当然,由于初始化时我们是随机给定的权重和偏置,所以这个预测结果就是一个随机给出的预测值.

输出值:

tensor([-1.9572, -2.1259, -2.1602, -2.1049, -2.4300, -2.1800, -2.6181, -2.5231,
        -2.3852, -2.8833], grad_fn=<SelectBackward>) torch.Size([64, 10])

​ 注意,笔者这里的输出值和官网原文上给出的并不一致,那是因为不同的电脑随机出来的数字有不同的差异.

​ pred这个tensor(由weights、bias计算得来,注意weights和bias均要求PyTorch保留梯度计算)不仅保留了计算结果,而且还保留了梯度. 在反向传播时我们会用到这里保留的梯度.

​ 接下来按负对数似然2来完成损失函数(同样,我们只使用标准python):

def nll(input, target):
    return -input[range(target.shape[0]), target].mean()

loss_func = nll

​ 让我们把输入和输出代入这个损失函数,计算一下损失函数的结果,方便我们在之后的反向传播改进后,提升模型的效果.

yb = y_train[0:bs]
print(loss_func(preds, yb))

输出值:

tensor(2.4143, grad_fn=<NegBackward>)

​ 接下来还需要定义模型计算的准确度,对每一次预测来说,如果预测出的最大的值的序号跟目标值相匹配,则认为预测是准确的.

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

输出值:

tensor(0.0938)

​ 看一下输出值结果,看看在损失函数改善后能不能也有所改善.

​ 我们现在可以跑一下训练循环,对于每一次迭代,我们将要:

  • 选择一个mini-batch的数据(大小为bs)

  • 使用model生成预测值

  • 计算损失函数

  • loss.backward()更新模型的梯度,在此例中,梯度存在weights和bias里

    注意,接下来使用torch.no_grad()文本管理器来执行训练,因为我们不希望这些操作积累到下一次的梯度计算之中.

from IPython.core.debugger import set_trace

lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        #         set_trace()
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        with torch.no_grad():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()
  • set_trace()可以解注释之后,在Python命令行模式下运行按循环次数步进运行程序3

  • 学习率设置为0.5

  • epoch相对于batch来说,是迭代次数,按batch对所有训练集进行划分,把所有训练集样本按batch运算执行完一圈,称为一个epoch

检查一下训练完成后的损失函数值,以及准确度值:

print(loss_func(model(xb), yb), accuracy(model(xb), yb))

输出值:

tensor(0.0795, grad_fn=<NegBackward>) tensor(1.)

​ 这个训练结果可以说是非常理想了,损失函数值下降到了0.0795,准确度计算出来结果为1,说明在最后一组训练验证集中结果全对了.

​ 至此,在没有使用nn等模块的情况下,已经把一整套深度学习训练逻辑搭建了起来.

3 开始用PyTorch特性模块替换

3.1 使用torch.nn函数

​ 从现在开始,我们要练习用PyTorch的特性模块来替换相应的核心代码了:替换后的代码会变得更短、更易理解、更灵活.

​ 最先被替换的是激活函数损失函数,使用的是torch.nn.functional(一般导入后用F来表示).这个模块包含了torch.nn库中的所有函数.除了激活和损失函数之外,你也能从这个模块里找到相应的池化函数. (还有做卷积、线性层等等,但这些最好用库里别的模块来调用).

​ 如果你在使用负对数似然的损失函数和log_softmax的激活函数,PyTorch提供了一个单个的函数F.cross_entropy来连接以上两个函数. 所以我们就能把激活函数从模型里面去掉了.

import torch.nn.functional as F

loss_func = F.cross_entropy

def model(xb):
    return xb @ weights + bias

​ 注意,这段代码运算结束后,loss_func已经发生了变化,没有再去调用log_softmax.

print(loss_func(model(xb), yb), accuracy(model(xb), yb))

​ 再看一下此时的结果:

tensor(0.0795, grad_fn=<NllLossBackward>) tensor(1.)

​ 与更换之前一样.

3.2 使用nn.Module模块

​ 接下来登场的是nn.Modulenn.Parameter,首先,我们从nn.Module(它本身是一个类,并且可以记录状态)派生一个子类. 在这个例子中,我们想创建一个类,保存我们的weights, bias和前向预测的方法. nn.Module包含一系列的属性和方法(就像.parameters().zero_grad()).

nn.Module是一个PyTorch的特别概念,是我们经常要用的一个类. nn.Module与小写的module区别,M使用使用的是大写.

from torch import nn

class Mnist_Logistic(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

​ 注意,我们现在定义的是类,所以在使用它之前还需要实例化:

model = Mnist_Logistic()

​ 现在我们就可以像之前那样计算损失函数了,注意nn.Module对象可以像函数一样调用,但是调用背后,PyTorch将会自动调用forward函数.

print(loss_func(model(xb), yb))

输出:

tensor(2.4129, grad_fn=<NllLossBackward>)

​ 之前我们的训练循环,必须要按照变量名称一个接一个地更新值,并且手动给梯度置0,像下面这样:

with torch.no_grad():
    weights -= weights.grad * lr
    bias -= bias.grad * lr
    weights.grad.zero_()
    bias.grad.zero_()

​ 现在就不必如此了,我们可以使用model.parameters()model.zero_grad()(这二者均为PyTorch为了nn.Module而开发的. 这给我们提供了极大的便利性,尤其是如果我们使用一个更加复杂的模型时:

with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

​ 注意,上例中采用一个简单的循环,把所有的参数都通过学习率更新了一次,且采用model.zero_grad()就把多个参数的重置给完成了.

​ 我们把整个训练循环封装成fit()函数,以便后续再次调用它.

def fit():
    for epoch in range(epochs):
        for i in range((n - 1) // bs + 1):
            start_i = i * bs
            end_i = start_i + bs
            xb = x_train[start_i:end_i]
            yb = y_train[start_i:end_i]
            pred = model(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            with torch.no_grad():
                for p in model.parameters():
                    p -= p.grad * lr
                model.zero_grad()

fit()

​ 让我们再次检查一下,经过训练以后,我们的损失函数变成了什么样子:

print(loss_func(model(xb), yb))

输出值:

tensor(0.0831, grad_fn=<NllLossBackward>)

3.3 通过nn.Linear重构

​ 接下来隆重登场的就是nn.Linear了,当我们想使用线性层时,我们就不需要再去手写xb @ self.weights + self.bias公式了,直接使用一个nn.Linear能让我们的书写更简便,且计算更快.

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)

    def forward(self, xb):
        return self.lin(xb)

​ 同样也需要实例化看一看结果如何:

model = Mnist_Logistic()
print(loss_func(model(xb), yb))

输出值:

tensor(2.3404, grad_fn=<NllLossBackward>)

​ 此时再调用前面定义好的fit()函数:

fit()
print(loss_func(model(xb), yb))

输出值:

tensor(0.0809, grad_fn=<NllLossBackward>)

3.4 使用optim重构

​ PyTorch同时提供了一个包含有多种优化策略的包,即torch.optim,我们也可以使用步进运行的方式,从我们的优化器那里向前做一次预测,取代手动更新每一个参数.

​ 之前我们是这样来完成优化的:

with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

​ 现在我们只需要这样:

opt.step()
opt.zero_grad()

optim.zero_grad()将梯度重置为0,我们需要在下一个minibatch计算梯度之前调用它.

from torch import optim

def get_model():
    model = Mnist_Logistic()
    return model, optim.SGD(model.parameters(), lr=lr)

model, opt = get_model()
print(loss_func(model(xb), yb))

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

​ 此处定义了一个函数,方便将来使用,输出值如下:

tensor(2.3238, grad_fn=<NllLossBackward>)
tensor(0.0826, grad_fn=<NllLossBackward>)

3.5 使用Dataset重构

​ PyTorch有一个抽象Dataset类. 一个Dataset可以是任意一个包含__len__函数和__getitem__函数的类.

​ PyTorch的TensorDataset是一个封装tensors的Dataset. 通过定义长度和迭代器,同时也给我们提供了一种方式来迭代、索引和分片的方式. 这将使我们在训练时更容易在同一行内得到独立和非独立变量.

from torch.utils.data import TensorDataset

x_trainy_train可以在一个TensorDataset中连接在一起,这样更容易迭代和分片.

train_ds = TensorDataset(x_train, y_train)

​ 之前,我们只能分别来进行数据的分批:

xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]

​ 现在我们可以将两步一起做了:

xb,yb = train_ds[i*bs : i*bs+bs]
model, opt = get_model()

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        xb, yb = train_ds[i * bs: i * bs + bs]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

​ 输出值:

tensor(0.0837, grad_fn=<NllLossBackward>)

3.6 使用DataLoader重构

​ PyTorch的DataLoader是用来管理批的工具. 你可以从任意一个Dataset中创建DataLoader. DataLoader让批与批之间的迭代更加容易. DataLoader自动给我们计算每一个minibatch的分片,不再需要使用train_ds[i*bs : i*bs+bs].

from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)

​ 之前我们的循环在批与批之间迭代时是像这样:

for i in range((n-1)//bs + 1):
    xb,yb = train_ds[i*bs : i*bs+bs]
    pred = model(xb)

​ 现在我们的循环要更简洁:

for xb,yb in train_dl:
    pred = model(xb)
model, opt = get_model()

for epoch in range(epochs):
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

输出值:

tensor(0.0819, grad_fn=<NllLossBackward>)

​ 至此,感谢nn.Modulenn.ParameterDatasetDataLoader这几个模块,我们的训练循环已经变短了很多,可读性也更强. 接下来让我们添加一些基本的特征,在实践中会非常有用.

4 添加的其他功能

4.1 添加验证集

​ 在上一个区块里,我们已经建立了一个训练集的循环. 实际上,你还需要有一个验证集,为了测试看看模型是否存在过拟合现象.

​ 对训练集进行随机洗牌也很重要,这样可以防止批与批之间发生过拟合. 另外来说,验证集的损失函数和我们打不打乱顺序没有关系,而且随机洗牌还要花费更多的时间,所以在验证集是没有必要进行洗牌的.

​ 我们的验证集的批大小将定义为训练集的2倍. 这是由于验证集计算时不需要进行反向传播,消耗更少的内存. 我们利用这一点来扩大批的大小,可以更快地得到计算结果.

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)

valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)

​ 我们将在每一个epoch的结尾计算并打印验证集的损失函数值.

​ 请注意,我们始终在训练前调用model.train(),在推理之前调用model.eval(),因为像nn.BatchNorm2dnn.Dropout等层级里,需要保证在不同的情形下这些函数的行为是正确且适当的.

model, opt = get_model()

for epoch in range(epochs):
    model.train()
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

    model.eval()
    with torch.no_grad():
        valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)

    print(epoch, valid_loss / len(valid_dl))

输出值:

0 tensor(0.3311)
1 tensor(0.2978)

4.2 创建fit()和get_data()

​ 我们现在要做一点属于自己的重构了. 我们注意到在计算训练集和验证集时,过程非常接近,所以我们定义自己的loss_batch,用来对于每一个batch计算损失.

​ 我们给训练集传递一个优化器,并使用它来计算反向传播. 对于验证集,我们不传这个优化器,所以这个方法将不会计算反向传播.

def loss_batch(model, loss_func, xb, yb, opt=None):
    loss = loss_func(model(xb), yb)

    if opt is not None:
        loss.backward()
        opt.step()
        opt.zero_grad()

    return loss.item(), len(xb)

​ 注意:通过一个条件判断来区分是训练集还是验证集.本函数返回loss函数的标量和数据集的数据个数.

​ fit运行必须的操作,来训练我们自己的模型,并为每一个epoch计算训练集和验证集的损失.

import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_dl:
            loss_batch(model, loss_func, xb, yb, opt)

        model.eval()
        with torch.no_grad():
            losses, nums = zip(
                *[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
            )
        val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)

        print(epoch, val_loss)

​ 再定义一个get_data函数,为训练集和验证集返回dataloaders:

def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )

​ 现在,封装完毕之后,我们只需要寥寥几行代码就可以完成整个过程了:

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

输出值:

0 0.33169105496406553
1 0.29847450672388076

​ 到此,用这几行代码已经可以训练很多种类型的模型了. 现在让我们看看能不能训练一个CNN.

5 建立一个CNN模型

5.1 切换到CNN模型

​ 我们要建立一个三层卷积网络. 由于上一节中的任何函数都未假定模型窗体,因此我们将能够在不进行任何修改的情况下使用它们来训练CNN.

​ 我们将使用Pytorch的预定义Conv2d类作为我们的卷积层。我们定义一个具有3个卷积层的CNN. 每个卷积后跟一个ReLU。最后,我们执行一个平均池.

class Mnist_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
        self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)

    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4)
        return xb.view(-1, xb.size(1))

lr = 0.1

​ 动量法是随机梯度下降的一种变化形式,它考虑到了以前的更新,通常会使训练更快一些.

model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

输出值:

0 0.37231672590970993
1 0.2393851181805134

5.2 nn.Sequential

torch.nn有另一个可以简化模型的类:Sequential. 一个Sequential对象将会以一种序列的方式,执行其包含的每一个模块. 这里一种写我们神经网络的简单方式.

​ 为了利用这一点,我们需要先简单定义一个经典层. 比如,PyTorch中没有一个察看层,我们需要给我们的网络定义一个. Lambda可以作为我们创建层的定义,等我们使用Sequential时就可以使用它了.

class Lambda(nn.Module):
    def __init__(self, func):
        super().__init__()
        self.func = func

    def forward(self, x):
        return self.func(x)


def preprocess(x):
    return x.view(-1, 1, 28, 28)

​ 于是模型使用Sequential来建立就很简单:

model = nn.Sequential(
    Lambda(preprocess),
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AvgPool2d(4),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

输出值:

0 0.3319808834314346
1 0.23777353531122208

5.3 封装DataLoader

​ 我们的CNN已经搭建成功了,但是它存在一个问题,因为它只对MNIST有效,因为:

  • 它假设输入是 28 × 28 28 \times 28 28×28长的向量

  • 它假设最终的CNN的大小为 4 × 4 4 \times 4 4×4(正因为如此,平均池化层的核,我们才用的这么大)

    如果我们要规避这两个前提,使我们的模型能对任意的2d单通道图像有效. 首先,我们把初始的Lambda层移走,把数据处理过程转入一个生成器:

def preprocess(x, y):
    return x.view(-1, 1, 28, 28), y


class WrappedDataLoader:
    def __init__(self, dl, func):
        self.dl = dl
        self.func = func

    def __len__(self):
        return len(self.dl)

    def __iter__(self):
        batches = iter(self.dl)
        for b in batches:
            yield (self.func(*b))

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

​ 接着,我们可以用nn.AdaptiveAvgPool2d替换nn.AvgPool2d,这样我们可以按照输出tensor的大小来定义,而不是只针对我们的输入,模型可以对任意输入大小有效.

model = nn.Sequential(
    nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    nn.AdaptiveAvgPool2d(1),
    Lambda(lambda x: x.view(x.size(0), -1)),
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

​ 试着训练一下:

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

​ 输出值:

0 0.41792837913036346
1 0.24113577970266342

6 使用GPU

​ 如果你有GPU设备,或者租用了云端GPU,注意是需要支持CUDA的:

print(torch.cuda.is_available())

​ 输出值为:

True

​ 我们可以创建一个device设备对象:

dev = torch.device(
    "cuda") if torch.cuda.is_available() else torch.device("cpu")

​ 注意这是一个适应CPU和GPU两种计算单元的定义.

def preprocess(x, y):
    return x.view(-1, 1, 28, 28).to(dev), y.to(dev)


train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

​ 最后,我们可以把模型迁移到GPU上,看一下输出值:

0 0.2040884757488966
1 0.23871206577420234

7 总结

  • torch.nn
    • Module:创建一个可调用的函数,其行为类似于函数,但也可以包含状态(如神经净层权重)。它知道它包含什么参数,可以归零其所有的梯度,循环通过他们的重量更新,等等.
    • Parameter: 张量的包装器,告诉模块它有在后行期间需要更新的重量。仅更新具有requires_grad属性集的张量.
    • functional:一个模块(通常按惯例导入到 F 命名空间),其中包含激活函数、损耗函数等,以及非有状态的图层版本,如卷积层和线性图层.
  • torch.optim:包含优化器,如 SGD,在反向传播步骤中更新参数权重.
  • Dataset:具有对象__len____getitem__的抽象接口,包括使用 Pytorch 提供的类,如 TensorDataset.
  • DataLoader:接受任何数据集并创建一个返回数据批处理的迭代器.

  1. 固定组合,从头开始,从零开始的意思 ↩︎

  2. 对数计算在定义model时已由log_softmax函数定义所得,因此在这里计算时没有再出现log ↩︎

  3. 这里对命令行调试不太清楚的就不要采用这种方式来进行代码运行查看了,如果解注释这一句,则每次代码执行到这里就会中断,此时,可以通过p来查看变量,通过c来接续执行 ↩︎

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