当前位置:   article > 正文

pytorch-3.Tutorials-Learning Pytorch-Deep Learning with PyTorch:WHAT IS TORCH.NN REALLY?_multiplying with

multiplying with


注意:这一节内容是一个整体,底下每个标题只是一个步骤

建议这部分内容作为notebook来运行,而不是作为 script运行。点击页面顶部的link“Download Notebook”就可以下载Notebook。
在这里插入图片描述
Pytorch提供了设计好的modules and classes: torch.nn , torch.optim , Dataset , and DataLoader来帮助用户生成和训练神经网络。为了充分利用这些modules and classes的功能,同时针对自己的问题去定制他们,用户就需要真正理解这些modules and classes能做什么。
为了方便理解,我们首先不借助于any features from these models,在MNIST data set上训练一个基础的神经网络。我们仅仅使用最基础的Pytorch Tensor功能。
然后,我们incrementally循序渐进地从torch.nn, torch.optim, Dataset, or DataLoader中添加feature,这样逐个展示each piece在做什么和each piece在如何工作使得代码更加简洁和灵活。
这里假设你已经安装了PyTorch,并且熟悉基本的tensor操作。 其实Numpy array的操作和PyTorch tensor基本一致。

1.MNIST data setup数据设置

这里,我们使用经典MNIST数据集,MNIST数据集是手写数字的黑白图像。
这里我们使用 pathlib(part of the Python 3 standard library:Python 3的标准程序库)处理路径,用requests(程序库)下载数据集。用这些包的时候,用“import modules”,所以可以看到每一步都做了什么。

from pathlib import Path
import requests

DATA_PATH = Path("data") #指定下载位置“data"
PATH = DATA_PATH / "mnist" #"data"目录下的子目录”mnist"

PATH.mkdir(parents=False, exist_ok=True) #落实上面的指定,生成下载路径-双重目录
#Path.mkdir(mode=0o777,parents=False, exist_ok=False),在给定的路径下创建一个新的目录。
#如果路径早已存在,那么将抛出FileExistsError.
#parents为真,该路径如果确是父目录且父目录是必须的,就会新建父目录。
#parents为假(默认值),之前设定的父目录缺失,会抛出FileNotFoundError:[WinError 3] 系统找不到指定的路径。: 'd\\mnist'
#exist_ok为假(默认值),并且目标目录已存在,将会抛出FileExistError。

URL = "http://deeplearning.net/data/mnist/" #下载地址
FILENAME = "mnist.pkl.gz" #指定下载文件名称

if not (PATH / FILENAME).exists(): #如果PATH / FILENAME(路径下的文件)不存在
        content = requests.get(URL + FILENAME).content #res = requests.get(url,params=params,headers=headers)
        (PATH / FILENAME).open("wb").write(content) #open(name[, mode[, buffering]])
        #mode : mode 决定了打开文件的模式:只读,写入,追加等,wb:以二进制格式打开一个文件只用于写入
        #write() 方法用于向文件中写入指定字符串
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这个数据在numpy array格式中,并且用pickle保存,pickle是python用来serializing data序列化数据的特殊格式。
在这里插入图片描述

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")       
  • 1
  • 2
  • 3
  • 4
  • 5

每个图片大小是28*28,并且被存储为a flattened row of length 784 (=28x28)–长度为784的扁平行。
我们首先将其reshape为2维。

from matplotlib import pyplot
%matplotlib inline
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray") #x_train[0]:按索引取值,第0行,第一个样本数据
print(x_train.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

PyTorch使用torch.tensor,而不是numpy arrays,所以我们将数据由numpy arrays转换为torch.tensor,这样可以转到GPU计算。

import torch

x_train, y_train, x_valid, y_valid = map(torch.tensor, (x_train, y_train, x_valid, y_valid)) #map,映射

n, c = x_train.shape #后面会用到
##print(n,c)
# UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or
# sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).
# 从一个tensor中复制构成元素,建议使用sourceTensor.clone().detach()或者sourceTensor.clone().detach().requires_grad_(True),
#不建议使用torch.tensor(sourceTensor)
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())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2.Neural net from scratch (no torch.nn)从无到有神经网络(不用torch.nn)

首先只用 PyTorch tensor operations来生成一个模型。假设,你已经熟悉神经网络的基础了,如果你不熟悉,你可以在 course.fast.ai—网址为https://course.fast.ai/学习神经网络的基础。

PyTorch提供了生成 random or zero-filled tensors(随机值或0值tensors)的方法,借此我们可以生成一个a simple linear model(简单线性模型)的权重和偏置。我们需要在 PyTorch中设定gradient。 PyTorch要记录在 tensor上所做的operations,这样可以在back-propagation期间自动计算gradient。

对于weights,在初始化之后,我们设定requires_grad ,这样做的原因是我们不想让让那个步骤包含在gradient中。
注意:a trailling _ 表示执行代替之前值得操作)。
注意:We are initializing the weights here with Xavier initialisation (by multiplying with 1/sqrt(n)).初始化weights,通过乘以1/sqrt(n),这个方法来自于:Xavier Glorot 和Yoshua Bengio的论文—Understanding the difficulty of training deep feedforward neural networks。

import math

#初始化weights,bias,就是赋予初值
weights = torch.randn(784, 10) / math.sqrt(784) #math.sqrt平方根--初始化weights,通过乘以1/sqrt(n)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

多亏PyTorch的自动计算梯度的能力,我们可以使用任意standard Python function标准的Python函数(or callable object)作为模型。
所以,我们写一个plain matrix multiplication and broadcasted addition普通的矩阵乘法和广播加法去生成一个simple linear model。我们也需要一个activation function激活函数,所以我们使用 log_softmax函数。虽然PyTorch提供了内置写好的损失函数loss functions,激活函数activation functions…等等,但是你依然可以使用纯python写自己的。PyTorch甚至可以为你的函数自动生成fast GPU 或vectorized CPU 向量化cpu代码。

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

def model(xb):
    return log_softmax(xb @ weights + bias)
    #@ 符号可以替代dot product operation矩阵乘法,表示矩阵乘法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们可以在一个 batch of data(批处理数据,一批数据,这个例子中一批用64 images)。这里是一个前向传播。
注意:这里的预测值不会比随机生成的效果更好,这是因为这里的权重也是随机生成的。

bs = 64  # batch size

xb = x_train[0:bs]  # a mini-batch from x #索引只有一维,则默认为列方向--索引行
preds = model(xb)  # predictions
preds[0], preds.shape #preds的行数和xb相同
print(preds[0], preds.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

正如你看见的,preds tensor不仅包含 tensor values,也包含gradient function。之后我们在backprop.中会用到。
现在,我们实现negative log-likelihood负对数似然函数,将其作为the loss function,这里依然只用standard Python:

#实现loss function
def nll(input, target):
    return -input[range(target.shape[0]), target].mean()
#上面:
loss_func = nll
  • 1
  • 2
  • 3
  • 4
  • 5

用我们的random model随机模型来核对/检查loss,所以在backprop pass反向传播之后,我们能看到模型是否有改进。

#使用loss function,将preds和yb代入
yb = y_train[0:bs]
print(loss_func(preds, yb))
  • 1
  • 2
  • 3

现在去实现一个函数来计算模型的准确率。如何评价预测结果:每一个预测结果中,如果最大值的索引和目标值匹配正确,那么预测就是正确的。
预测结果中最大值的索引就是预测的数字

#实现accuracy函数
def accuracy(out, yb):
    preds = torch.argmax(out, dim=1) #维度是1,是行方向,每一列的值
    print(preds) #预测结果中最大值的索引就是预测的数字,将其和目标值作比较
    return (preds == yb).float().mean()
#这里这样做是因为,神经网络输出的preds是64*10矩阵,10就对应0-*数字的预测值,所以torch.argmax(out, dim=1)输出结果也就是预测值。
#而yb本身就是64*1矩阵,表明了是那个数字。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

检查模型的准确率,这样可以看到当loss降低时准确率是否提高了。

#使用accuracy,将preds和yb代入
print(accuracy(preds, yb))
  • 1
  • 2

用循环来计算梯度,以此来更新模型参数
我们运行一个 training loop训练循环,For each iteration对每次迭代,要实现如下功能:

  • select a mini-batch of data (of size bs)选择一个小批量数据(bs大小)
  • use the model to make predictions用模型去预测
  • calculate the loss计算损失
  • loss.backward() updates the gradients of the model, in this case, weights and bias.用loss.backward() 更新模型梯度,权重和偏置。

我们现在用这些梯度来更新权重和偏置。做这些要在torch.no_grad()context manager(上下文管理器)中,因为我们不想在下一次计算梯度时候,让之前的操作缓存被记录下来,造成多次梯度累加。查看更多关于PyTorch’s Autograd 如何记录操作的信息:https://pytorch.org/docs/stable/notes/autograd.html

我们设置梯度为0,以此为下一次循环做好准备。否则,我们的梯度会记录下发生的所有操作的运行记录(loss.backward() 操作就会将梯度值添加到上一次已经存储下的梯度值中,而不是代替上一次存储下的梯度值中)。

注意:你可以使用standard python debugger标准的python调试器去逐句调试PyTorch code,这允许你在每一步检查不同的变量值。在下面:取消批注set_trace()去尝试。

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): #所有数据遍历完一次是为一个epoch
    for i in range((n - 1) // bs + 1): #((n - 1) // bs)+1,目的是按照bs大小将n个数据划分成组
    # "//"表示整数除法,返回不大于结果的一个最大的整数。" / "  表示浮点数除法,返回浮点结果
        #         set_trace()
        start_i = i * bs #随着i的变化,数据的开始索引处在变化
        end_i = start_i + bs #结束索引,这样数据大小总是bs个
        #结果是:i从0开始,将n个数据按照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(): 
        #torch.no_grad()context manager(上下文管理器)中,这样不会在下一次计算梯度时候,让之前的操作缓存被记录下来造成梯度累加
            weights -= weights.grad * lr #更新weights
            bias -= bias.grad * lr #更新bias
            weights.grad.zero_() #梯度清零
            bias.grad.zero_() #梯度清零
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

通过上面的循环过程我们生成并训练了一个 minimal neural network(a logistic regression一个逻辑回归,这里没有隐藏层)。
检查loss and accuracy ,比较现在得到的结果和之前得到的。希望达到,loss减小,accuracy提升。

#循环结束后,训练结束,model中的weights和bias都更新了,现在检测一下更新后的model的loss和accuracy
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
  • 1
  • 2

3.Using torch.nn.functional

现在,我们要 refactor our code重构代码,重构的代码要做到和之前相同的事情。这里我们开始使用PyTorch’s nn classes从而使得代码更准确更简洁。
从现在开始,以后的每一步,我们应该使得代码变得更加短小,更容易理解,更灵活。

第一步也是最容易的一步是使用来自torch.nn.functional(惯例上–使用方法为import torch.nn.functional as F)的函数来代替我们手写的activation and loss functions激活函数和损失函数。这个torch.nn.functional模块包含torch.nn library中的所有函数。除了许多loss and activation functions以外,torch.nn.functional模块中也包括生成neural nets的其他函数(例如:pooling functions,还有做卷积的函数,线性全连接层函数)。

如果你用negative log likelihood loss and log softmax activation,那么 Pytorch也提供一个简单函数F.cross_entropy。F.cross_entropy将negative log likelihood loss和log softmax activation结合在一起。因而我们甚至可以将激活函数从模型中移除。

import torch.nn.functional as F

loss_func = F.cross_entropy

def model(xb):
    return xb @ weights + bias #@符号表示矩阵乘法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意:我们在模型中不再调用log_softmax,下面确定一下:loss and accuracy和之前我们自己定义的函数功能是一样的。

#调用新的loss函数并跟之前做对比
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
#这里的结果和没使用torch.nn.functional之前的结果一样,因为上次训练完成后,weights和bias都没变化
  • 1
  • 2
  • 3

4.Refactor using nn.Module用nn.Module重构代码

下一步,我们使用nn.Module和nn.Parameter来生成更简洁的训练循环体。我们subclass nn.Module继承nn.Module父类(nn.Module本身是一个类并且能记录状态)。
我们要生成一个类,这个类持有weights, bias, and method for the forward step.
nn.Module包含许多attributes and methods属性和方法(例如:.parameters() and .zero_grad())。

注意:nn.Module是一个PyTorch的特有概念,这是一个我们会常用的类。nn.Module不能和python中module模块的概念混淆,module模块是python中能被 imported的代码包,而nn.Module是PyTorch的一个特定包的名称。

#现在继承nn.Module,自己定义一个类。重新生成简单线性模型--包括:权重和偏置,前向传播方法。
#模型中的weights和bias初始化了,重新随机生成
from torch import nn

class Mnist_Logistic(nn.Module): #自定义Mnist_Logistic类,继承nn.Module父类
    def __init__(self):
        super().__init__()
        self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784)) 
        #torch.randn(784, 10) / math.sqrt(784)初始化,随机生成参数。
        self.bias = nn.Parameter(torch.zeros(10))

    def forward(self, xb):
        return xb @ self.weights + self.bias
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

因为我们现在正在用一个项目对象而不是仅仅用一个函数,所以我们首先需要instantiate our model实例化模型。

#使用类的方法:实例化模型
model = Mnist_Logistic()
  • 1
  • 2

现在用跟之前一样的方式计算损失loss。注意:nn.Module 对象使用方式和函数一样,但是behind the scenes在幕后,Pytorch会自动调用forward method。

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

在之前,training loop中,我们通过名字和分别手动归零每个参数的梯度来更新每个参数的值,就像下面这样:

#可运行,但是会出错,因为没有循环求解梯度的机制,所以不能更新参数
with torch.no_grad():
    weights -= weights.grad * lr
    bias -= bias.grad * lr
    weights.grad.zero_()
    bias.grad.zero_()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但是现在,我们使用model.parameters()和model.zero_grad(),他们都是由PyTorch专门为nn.Module定义的,去代替上面的“通过名字和分别手动归零每个参数的梯度来更新每个参数的值”的步骤,这样更简洁,且更少发生forgetting some of our parameters丢失参数的错误。model.parameters()保存所有的可学习参数,model.zero_grad()将之前计算出的梯度归零。
尤其是当我们构建更复杂的模型时候:

#不可运行,会出错,因为没有循环求解梯度的机制,所以不能更新参数,这里仅是举例,下一步是可运行代码
with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr #内置model.parameters(),将所有参数保存在model.parameters()中
    model.zero_grad() #内置model.zero_grad()将所有参数归零
  • 1
  • 2
  • 3
  • 4

我们wrap our little training loop in a fit function(将训练循环机制定义到fit函数中),下面的可以运行:

n, c = x_train.shape
bs = 64  # batch size
lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

下面我们double-check复核loss是否降低了.0

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

5.Refactor using nn.Linear用nn.Linear重构代码

我们不用手动定义和初始化self.weights,self.bias,计算xb@ self.weights + self.bias,而是用Pytorch的类nn.Linear来构建linear layer(全连接层-线性变换层)。Pytorch包含许多类的predefined layers(层),这些predefined layers可以简化我们的代码。

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

    def forward(self, xb):
        return self.lin(xb)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

instantiate our model实例化模型,并且计算loss,用跟之前相同的方法:

model = Mnist_Logistic()
print(loss_func(model(xb), yb))
  • 1
  • 2

仍然可以用跟之前一样的fit方法来更新参数:

fit()

print(loss_func(model(xb), yb))
  • 1
  • 2
  • 3

6.Refactor using optim用optim重构代码

Pytorch有一个 package—torch.optim.,这个package有各种各样的optimization algorithms优化算法,We can use the step method from our optimizer to take a forward step我们可以从optimizer中调用step method迭代法来做迭代,这样就避免手动更新参数:

之前的手动optimization step优化步骤代码如下:

# previous manually coded optimization step
#下面代码不能运行
with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()
  • 1
  • 2
  • 3
  • 4
  • 5

代替上面代码的是下面,非常简单:

opt.step()
opt.zero_grad()
  • 1
  • 2

optim.zero_grad()重置梯度值为0,我们要在迭代下一个minibatch数据集去计算梯度之前就去调用optim.zero_grad():

from torch import optim
  • 1

我们定义了一个 function去生成模型和优化器model and optimizer,以便以后可以重复调用:

n, c = x_train.shape
bs = 64  # batch size
lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

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

model, opt = get_model()
print(loss_func(model(xb), yb))
# 训练之前的loss

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))
# 训练之后的loss
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

7.Refactor using Dataset用Dataset重构代码

PyTorch有一个 abstract Dataset class抽象数据类。这个Dataset数据集可以是anything任何对象,并且有__len__ function(被Python’s standard len function调用)和 getitem function两个函数作为对dataset内部进行索引的方法。
This tutorial walks through a nice example —本教程走通了一个例子,创建了一个自定义的FacialLandmarkDataset类作为Dataset的子类。

PyTorch的TensorDataset是一个封装了tensors的数据集(Dataset wrapping tensors)。通过定义Dataset的长度和索引方法,就可以给我们迭代,索引,沿着tensor第一维做切片的方法。在训练时候,这将使我们更容易地在同一条直线上访问自变量和因变量。(access both the independent and dependent variables in the same line as we train)

from torch.utils.data import TensorDataset
  • 1

x_train和y_train可以被组合在一个单一TensorDataset中,这样可以方便地做迭代和切片:

train_ds = TensorDataset(x_train, y_train)
  • 1

之前,我们分别迭代x和y的minibatches :代码如下:

#下面代码不可运行
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
  • 1
  • 2
  • 3

现在,我们将上面分开的两步组合到一起,代码如下:

xb,yb = train_ds[i*bs : i*bs+bs]
  • 1
n, c = x_train.shape
bs = 64  # batch size
lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

8.Refactor using DataLoader用DataLoader重构代码

Pytorch的DataLoader负责数据加载批量的管理。你可以从任意Dataset. DataLoader生成DataLoader,从而使得在batches数据批上做迭代更容易。这样就不用train_ds[ibs : ibs+bs]去做迭代了,DataLoader 自动给了我们每个minibatch。

from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)#生成了迭代器
  • 1
  • 2
  • 3
  • 4

之前,我们在batches (xb, yb) 上做迭代的代码如下:

#代码可以运行,但是单独运行没用,需要放到更新参数的总代码结构中
for i in range((n-1)//bs + 1):
    xb,yb = train_ds[i*bs : i*bs+bs]
    pred = model(xb)
  • 1
  • 2
  • 3
  • 4

现在,循环更整洁了,(xb, yb)可从data loader中自动加载。

from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)#生成了迭代器

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

bs = 64  # batch size
lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
    for xb, yb in train_dl: #train_dl是迭代器
        pred = model(xb)
        loss = loss_func(pred, yb)

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

print(loss_func(model(xb), yb)) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

多亏Pytorch的nn.Module, nn.Parameter, Dataset, and DataLoader包,我们的训练循环的代码量现在更小,更容易理解了。
接下来我们添加必要的基础功能来生成实践中更有效的模型。

9.Add validation添加验证

在第一部分,我们仅仅尝试去设置一个合理的训练循环用来应用到训练数据上。事实上,你还应该总是有一个validation set,验证数据集,目的是确认你是否过拟合了。

Shuffling the training data混排训练数据集非常有助于阻止batches批次和过拟合之间的相关性。
另一方面,无论我们shuffle验证数据集与否,validation loss验证loss都是完全相同的。既然shuffling 花费了额外的时间,那么shuffle the validation data就没有意义。

我们将使用的验证数据集的batch size批次大小将是training set的两倍大。这样设置的原因是,validation set不需要backpropagation 反向传播,占用很少内存(不需要存储梯度)。我们这样做可以使用a larger batch size 从而更快计算loss。

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) #shuffle默认为False
  • 1
  • 2
  • 3
  • 4
  • 5

在每一个epoch的最后,计算和打印出 validation loss

注意:我们会总是在训练之前调用model.train(),在inference推理之前调用model.eval(),因为他们都用在了layers中(比如:nn.BatchNorm2d and 和nn.Dropout )去确保在不同阶段有适当的行为。eval是evaluation的简写,评估评价之意。
model是继承了父类nn.Module,nn.Module中有自己定义的许多方法。

model, opt = get_model()

bs = 64  # batch size
lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
    model.train() #在训练之前调用model.train()
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward() #反向传播,loss相对于参数的梯度
        opt.step() #opt内置更新参数方法
        opt.zero_grad() #梯度清零

    model.eval() #在inference推理之前调用model.eval()评估
    with torch.no_grad():
        valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)
    # 计算valid_dl验证数据集迭代器的loss
    print(epoch, valid_loss / len(valid_dl))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

10.Create fit() and get_data()生成fit() 和get_data()–整合loss和更新参数的过程到一个函数中

我们现在重构代码。we go through a similar process twice of calculating the loss for both the training set and the validation set(我们对训练集和验证集进行了两次类似的损失计算),现在我们它变成自己的函数,将对训练集和验证集进行的两次损失计算整合到一个函数中–loss_batch,loss_batch可以为每个 batch计算loss。

在自定义的函数中,我们将optimizer传递进来运行training set,并且用来反向传播。对于验证集,我们不传递优化器,所以这个函数不能运行验证集的反向传播。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

fit()运行必要的操作来训练模型并计算每一个epoch的训练集和验证集的loss。

import numpy as np

bs = 64  # batch size
lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_dl: #计算train_dl中每个batch_size的loss
            loss_batch(model, loss_func, xb, yb, opt) #结果返回loss.item(), len(xb)

        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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

get_data()返回training和validation sets的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_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) #shuffle默认为False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

现在,我们的获取data loaders和将数据fit进模型的过程只用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)
  • 1
  • 2
  • 3

你可以用基础的三行代码去训练种类繁多的模型。我们能用他们去训练一个convolutional neural network (CNN)。

11.Switch to CNN构建CNN

我们现在用三个卷积层convolutional layers去构建我们的神经网络。none of the functions in the previous section(前一节) assume anything about the model form(形式),因为前一节中的函数都没有假定模型形式,所以我们可以用它们去训练一个CNN,而不用做任何修改。

我们将用 Pytorch的预定义的Conv2d class作为卷积层。我们定义CNN为3层卷积层。每个卷积层后面跟一个ReLU层。最后,我们实现一个average pooling均值池化层。

注意:PyTorch的view()是numpy’s 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) #将一维数据转为4维数据,成为多维矩阵
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4) #池化,卷积核为4*4
        return xb.view(-1, xb.size(1))

lr = 0.1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Momentum 是stochastic gradient descent (SGD)的的一个变种,Momentum可以takes previous updates into account as well and generally leads to faster training考虑之前的更新并且做更快的训练。

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

fit(epochs, model, loss_func, opt, train_dl, valid_dl)
  • 1
  • 2
  • 3
  • 4

12.nn.Sequential包装模型

torch.nn还有另一个我们能用来简化代码的类:nn.Sequential。
一个Sequential对象以顺序的形式(n a sequential manner)运行每个包含在它里面的 modules。

我们需要custom layer from a given function从给定函数中定义一个自定义层。比如, PyTorch没有view layer,因而我们就要自己为网络生成一个。当我们用nn.Sequential定义一个网络时,Lambda可以生成一个层。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

用Sequential生成模型仅仅需要:
定义好nn.Sequential中的module后,就会自动进行forward计算。

model = nn.Sequential(
    Lambda(preprocess), #用Lambda调用定义好的函数,生成层
    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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

13.Wrapping DataLoader包装DataLoader

我们在前面构造的CNN是相当简洁的,但是它只能运行MNIST,因为我们假设:

  • 输入是2828 long vector—2828的行一维向量行向量
  • CNN最终的输出是44大小的网格(因为我们假设average
    pooling kernel大小是4
    4)
    所以,CNN中的设定也是根据假设来定的

我们现在放弃这两个假设,为了使得我们的模型可以运行任意的2d single channel image2维单通道图像。
首先,我们移除initial Lambda layer ,但是同时将data preprocessing数据预处理层移进generator中:

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)) #yield出产,产出

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

下一步,我们用nn.AdaptiveAvgPool2d来代替nn.AvgPool2d。
nn.AdaptiveAvgPool2d运行我们去定义我们想要的output tensor的大小。
因而,我们的模型可以运行任意大小的input。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

尝试运行模型:

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

14.Using your GPU使用GPU

如果你可以利用一个CUDA-capable GPU(你也可以从大部分运提供商那里以$0.50/hour的价格租用一个),CUDA-capable GPU可以加速你的代码。
首先检查你的GPU可以在Pytorch中工作:

print(torch.cuda.is_available())
  • 1

然后为GPU创建一个device object装置对象:

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

现在,我们更新一下preprocess,以达到移动batches到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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后,我们移动我们的模型到GPU:

model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
  • 1
  • 2

你会发现模型运行得更快了:

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

15.Closing thoughts结论

我们现在有了一个通用的data pipeline数据管道和training loop,你可以用这个data pipeline和training loop训练多种类型的模型。

还有需要你需要添加的东西,比如:data augmentation数据扩充, hyperparameter tuning超参数调优, monitoring training监督训练, transfer learning迁移学习, and so forth。
These features are available in the fastai library, which has been developed using the same design approach shown in this tutorial, providing a natural next step for practitioners looking to take their models further.这些专题都可以在 fastai library中找到,fastai library已经使用与本教程相同的设计方法发展了前面提到的那些专题功能,为希望进一步发展他们的模型的开发者提供了很自然的下一步。

现在我们总结一下torch.nn, torch.optim, Dataset, and DataLoader。

  • torch.nn
    Module:创建一个可调用,行为类似于一个函数的对象,但它也可以包含状态(如神经网络层权重)。它知道它包含什么参数,并且可以使它们所有的梯度归零,循环它们来进行权重更新。
    Parameter:一个张量的包装器,它告诉一个 Module它有在反向传播期间需要更新的权值。只有带有requires_grad属性集的张量才会更新。
    functional:一个模块(使用通常为import nn.Module.functional as F),它包含激活函数、损失函数等,以及层的无状态版本(即没有设置参数的层,如卷积层和线性层)。
  • torch.optim:包含优化器,例如SGD,可以在backward step期间更新权重。
  • Dataset:一个带有 len and a __getitem__abstract interface of objects 抽象接口对象,包含Pytorch提供的类,比如TensorDataset。
  • DataLoader:获取任何数据集并创建一个迭代器,该迭代器返回批量数据。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/577776
推荐阅读
相关标签
  

闽ICP备14008679号