PyTorch学习笔记(八) ---- torch.nn 到底是什么?

2022-10-19 12:15:57

转载请注明作者和出处:http://blog.csdn.net/john_bh/


PyTorch 提供设计优雅的模块和类torch.nntorch.optimDatasetDataLoader 来帮助您创建和训练神经网络。 为了充分利用它们的功能并针对您的问题对其进行自定义,需要真正地了解他们的工作。 为了建立这种理解,将首先在 MNIST 数据集上训练基本神经网络,而无需使用这些模型的任何功能; 最初只会使用最基本的 PyTorch 张量功能。 然后,将一次从torch.nntorch.optimDatasetDataLoader中逐个添加一个功能,确切地显示每个功能,以及如何使代码更简洁或更灵活。

1. MNIST 数据设置

将使用经典的 MNIST 数据集,该数据集由手绘数字的黑白图像组成(介于 0 到 9 之间)。使用 pathlib 处理路径(Python 3 标准库的一部分),并使用请求下载数据集。 只会在使用模块时才导入它们,因此可以确切地看到正在使用模块的每个细节。

from pathlibimport Pathimport requests

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

PATH.mkdir(parents=True, exist_ok=True)

URL="http://deeplearning.net/data/mnist/"
FILENAME="mnist.pkl.gz"if not(PATH / FILENAME).exists():
        content= requests.get(URL + FILENAME).content(PATH / FILENAME).open("wb").write(content)

该数据集为 numpy 数组格式,并已使用 pickle(一种用于序列化数据的 python 特定格式)存储。

import pickleimportgzip

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

每个图像为 28 × 28 28 \times 2828×28,并存储被拍平长度为 784 ( = 28 × 28 ) 784(= 28 \times 28)784(=28×28的向量。 来看一个; 需要先将其重塑为 2d。

from matplotlibimport pyplotimport numpy as np

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

输出:

torch.Size([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)

2. 从零开始的神经网络(无 torch.nn)

PyTorch 提供了创建随机或零填充张量的方法,这里将使用它们为简单的线性模型创建权重和偏差。这些只是常规张量,还有一个特殊的附加值:告诉python 它们需要的梯度。这使PyTorch 记录了在张量上完成的所有操作,因此它可以在反向传播时自动地计算梯度

对于权重,在初始化之后设置requires_grad ,因为不希望该步骤包含在梯度中。 (请注意,PyTorch 中的尾随_表示该操作是就地执行的。)

  1. Xavier 初始化
    在这里用 Xavier 初始化(通过乘以 1 / sqrt(n))来初始化权重。

    import math
    
    weights= torch.randn(784, 10) / math.sqrt(784)
    weights.requires_grad_()
    bias= torch.zeros(10, requires_grad=True)
  2. 线性模型和激活函数
    由于 PyTorch 具有自动计算梯度的功能,可以将任何标准的 Python 函数(或可调用对象)用作模型! 因此,编写一个简单的矩阵乘法和广播加法来创建一个简单的线性模型。 还需要激活函数,因此将编写并使用 l o g _ s o f t m a x log\_softmaxlog_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)

    在上面,@代表点积运算。 将对一批数据(在这种情况下为 64 张图像)调用函数。 这是一个前向传播。

    请注意,由于我们从随机权重开始,因此在这一阶段,我们的预测不会比随机预测更好。

    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)

    输出结果:

    tensor([-2.8144, -2.9565, -2.1889, -2.8592, -1.6188, -1.8370, -2.1526, -2.5953,
            -2.4119, -2.5154], grad_fn=<SelectBackward>) torch.Size([64, 10])

    可以看出,preds张量不仅包含张量值,还包含梯度函数。 稍后将使用它进行反向传播。

  3. 损失函数
    实现负对数似然作为损失函数(同样,只能使用标准 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.4812, grad_fn=<NegBackward>)
  4. 计算准确性
    还实现一个函数来计算模型的准确性。 对于每个预测,如果具有最大值的索引与目标值匹配,则该预测是正确的。

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

    检查一下随机模型的准确性,以便我们可以看出随着损失的增加,准确性是否有所提高。

    print(accuracy(preds, yb))

    输出结果:

    tensor(0.0781)
  5. 训练
    运行一个训练循环。 对于每次迭代:选择一个小批量数据(大小为bs);使用模型进行预测;计算损失;loss.backward()更新模型的梯度,在这种情况下为weights和bias。

    现在,使用这些梯度来更新权重和偏差。 在torch.no_grad()上下文管理器中执行此操作,因为我们不希望在下一步的梯度计算中记录这些操作。

    然后,将梯度设置为零,以便为下一个循环做好准备。 否则,我们的梯度会记录所有已发生操作的运行记录(即loss.backward() 将梯度添加到已存储的内容中,而不是替换它们)

    可以使用标准的 python 调试器逐步浏览 PyTorch 代码,从而可以在每一步检查各种变量值。 取消注释以下set_trace()即可尝试。

    from IPython.core.debuggerimport set_trace
    
    lr= 0.5# learning rate
    epochs= 2# how many epochs to train forfor epochin range(epochs):for iin 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_()

    这样完全从头开始创建并训练了一个最小的神经网络(在这种情况下,是逻辑回归,因为没有隐藏的层)!

    检查损失和准确性,并将其与我们之前获得的进行比较。 希望损失会减少,准确性会增加,而且确实如此。

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

    输出结果:

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

3. 重构

3.1 使用 torch.nn.functional

现在,将重构代码,使其与以前相同,只是将开始利用 PyTorch 的nn类使其更加简洁和灵活。 从这里开始的每一步,都应该使代码中的一个或多个:更短,更易理解和/或更灵活。

第一步也是最简单的步骤,就是用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

请注意,不再在model函数中调用log_softmax。 确认损失和准确性与以前相同:

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

输出结果:

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

3.2. 使用 nn.Module 进行重构

接下来,使用nn.Module和nn.Parameter进行更清晰,更简洁的训练循环。 将nn.Module子类化(它本身是一个类并且能够跟踪状态)。 在这种情况下,我们要创建一个类,该类包含前进步骤的权重,偏差和方法。nn.Module具有许多我们将要使用的属性和方法(例如.parameters()和.zero_grad())。

nn.Module(大写 M)是 PyTorch 的特定概念,也是我们将经常使用的一个类。 nn.Module不要与(小写m)模块的 Python 概念混淆,该模块是可以导入的 Python 代码文件。

from torchimport 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.3484, grad_fn=<NllLossBackward>)

以前,在训练循环中,必须按名称更新每个参数的值,并手动将每个参数的 grads 分别归零,如下所示:

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 pin model.parameters(): p -= p.grad * lr
    model.zero_grad()

我们将把小的训练循环包装在fit函数中,以便稍后再运行。

def fit():for epochin range(epochs):for iin 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 pin model.parameters():
                    p -= p.grad * lr
                model.zero_grad()

fit()

仔细检查一下我们的损失是否下降了:

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

输出结果:

tensor(0.0838, grad_fn=<NllLossBackward>)

3.3. 使用 nn.Linear 重构

继续重构我们的代码。 代替手动定义和初始化self.weights和self.bias并计算xb @ self.weights + self.bias,我们将对线性层使用 Pytorch 类 nn.Linear ,这将为我们完成所有工作。 Pytorch 具有许多类型的预定义层,可以大大简化我们的代码,并且通常也可以使其速度更快。

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.3402, grad_fn=<NllLossBackward>)

仍然可以使用与以前相同的fit方法。

fit()

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

输出结果:

tensor(0.0819, grad_fn=<NllLossBackward>)

3.4. 使用优化重构

Pytorch 还提供了一个包含各种优化算法的软件包torch.optim。 我们可以使用优化器中的step方法采取向前的步骤,而不是手动更新每个参数。

这就是我们将要替换之前手动编码的优化步骤:

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

我们只需使用下面的代替:

opt.step()
opt.zero_grad()

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

from torchimport 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 epochin range(epochs):for iin 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.2361, grad_fn=<NllLossBackward>)
tensor(0.0818, grad_fn=<NllLossBackward>)

3.5. 使用数据集进行重构

PyTorch 有一个抽象的Dataset 类。 数据集可以是具有__len__函数(由 Python 的标准len函数调用)和具有__getitem__函数作为对其进行索引的一种方法。 本教程演示了一个不错的示例,该示例创建一个自定义FacialLandmarkDataset类作为Dataset的子类。

PyTorch 的 TensorDataset 是一个数据集包装张量。 通过定义索引的长度和方式,这也为我们提供了沿张量的一维进行迭代,索引和切片的方法。 这将使我们在训练的同一行中更容易访问自变量和因变量。

from torch.utils.dataimport TensorDataset

x_train和y_train都可以合并为一个TensorDataset,这将更易于迭代和切片。

train_ds= TensorDataset(x_train, y_train)

以前,我们不得不分别遍历 x 和 y 值的迷你批处理:

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 epochin range(epochs):for iin 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.0805, grad_fn=<NllLossBackward>)

3.6. 使用 DataLoader 进行重构

PyTorch 的DataLoader 负责批次管理。可以从任何Dataset创建一个DataLoader。DataLoader使迭代变得更加容易,不必使用train_ds[i*bs : i*bs+bs] ,DataLoader 会自动为我们提供每个小批量。

from torch.utils.dataimport DataLoader

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

以前,我们的循环遍历批处理(xb,yb),如下所示:

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

现在,我们的循环更加简洁了,因为(xb,yb)是从数据加载器自动加载的:

for xb,ybin train_dl:
    pred= model(xb)
model, opt= get_model()for epochin range(epochs):for xb, ybin 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.0804, grad_fn=<NllLossBackward>)

4. 添加基本功能

得益于 Pytorch 的nn.Modulenn.ParameterDatasetDataLoader,我们的训练循环现在变得更小,更容易理解。 现在,让我们尝试添加在实践中创建有效模型所需的基本功能。

4.1. 添加验证

在第 3 节中,只是试图建立一个合理的训练循环以用于我们的训练数据。 实际上,应该具有验证集,以便识别是否过度拟合。

打乱训练数据顺序对于防止批次与过度拟合之间的相关性很重要。 另一方面,无论我们是否打乱验证集,验证损失都是相同的。 由于打乱顺序需要花费更多时间,因此打乱验证集数据顺序没有任何意义。

我们将验证集的批次大小设为训练集的两倍。 这是因为验证集不需要反向传播,因此占用的内存更少(不需要存储渐变)。 利用这一优势来使用更大的批量,并更快地计算损失。

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.BatchNorm2d和nn.Dropout之类的图层会使用它们,以确保这些不同阶段的行为正确。)

model, opt= get_model()for epochin range(epochs):
    model.train()for xb, ybin 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, ybin valid_dl)

    print(epoch, valid_loss / len(valid_dl))

输出结果:

0 tensor(0.3143)
1 tensor(0.3289)

4.2. 创建 fit()和 get_data()

现在,将自己进行一些重构。 由于我们经历了两次相似的过程来计算训练集和验证集的损失,因此我们将其设为自己的函数loss_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)

fit运行必要的操作来训练我们的模型,并计算每个时期的训练和验证损失。

import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):for epochin range(epochs):
        model.train()for xb, ybin 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, ybin valid_dl])
        val_loss= np.sum(np.multiply(losses, nums)) / np.sum(nums)

        print(epoch, val_loss)

get_data返回用于训练和验证集的数据加载器。

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

现在,我们获取数据加载器和拟合模型的整个过程可以在 3 行代码中运行:

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.34373447265625
1 0.31163097116947175

可以使用这些基本的 3 行代码来训练各种各样的模型。

5. 切换到 CNN

现在,我们将构建具有三个卷积层的神经网络。 由于上一节中的所有函数都不包含任何有关模型组合的内容,因此我们将能够使用它们来训练 CNN,而无需进行任何修改。

我们将使用 Pytorch 的预定义 Conv2d 类作为我们的卷积层。 我们定义具有 3 个卷积层的 CNN。 每个卷积后跟一个 ReLU。 最后,我们执行平均池化。(请注意,view是 numpy 的reshape的 PyTorch 版本)

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.32315067479610443
1 0.2475940626323223

6. nn.Sequential

torch.nn还有另一个灵活的类,可以用来简化我们的代码:SequentialSequential对象以顺序方式运行其中包含的每个模块。 这是编写神经网络的一种简单方法。

要利用此优势,我们需要能够从给定的函数轻松定义自定义层。 例如,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
  • 作者:john_bh
  • 原文链接:https://blog.csdn.net/john_bh/article/details/108281292
    更新时间:2022-10-19 12:15:57