转战pytorch(2)——“简单”训练

前言

上一章中我们主要介绍了pytorch的一些常用的层和一些定义,本章中,我们主要介绍一个简单的训练的例子,以最常见的MNIST数据集为基础。但是值得我们注意的,我们已经熟悉神经网络训练了,因此,本章的主要注意力集中在,在pytorch中如何合适的将整个神经网络表达出来,包括数据预处理,模型搭建,训练以及测试过程。

基于以上,本章的主要讲解在如何组织每个部分,而不是训练的具体内容。

1. 无需pytorch也可以做神经网络

这里,我们假设你并没有这些数据。下面是一个准备数据的原始样例,你可以在这个样例里进行数据的下载、查看以及加载,并且还提供了非pytorch的简单的训练。

1.1 数据准备(代码片段1)

代码片段1是用来进行数据准备的,因此主要进行了数据的下载和数据加载。因此本代码片段需和下面的代码片段放在同一文件里才能够正常运行。

#——————————————————————————数据下载——————————————————————————————
from pathlib import Path
import 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)
# ——————————————————————————————数据加载——————————————————————————————————
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")
# ————————————————————————————数据查看——————————————————————————————————
# matplotlib inline
from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)
import torch
#——————————————————————————————————数据转为tensor以便处理————————————————————————————————
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())

1.2 非pytorch训练(代码片段2)

下面是一个非pytorch训练的过程,这里提供的是整个训练过程的展示,并不适合直接使用,因为接下来将会将它改为pytorch版本。但是,有了这些过程,你能够对于神经网络是如何运行的有一个深入的了解。

#————————————————————————————初始化参数——————————————————————
import math
weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
#————————————————————————————————定义模型及函数————————————————————————
def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)
#——————————————————————————尝试运行测试————————————
bs = 64  # 一批数据个数
xb = x_train[0:bs]  # 从x获取一小批数据
preds = model(xb)  # 预测值
preds[0], preds.shape
print(preds[0], preds.shape)
#——————————————————————————定义损失函数————————————
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))
#——————————————————————————定义评估函数————————————
def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()
print(accuracy(preds, yb))
#——————————————————————————完整训练过程————————————
lr = 0.5  # 学习率
epochs = 2  # 训练的轮数

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():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()
print(loss_func(model(xb), yb), accuracy(model(xb), yb))

2. 融入pytorch内置函数

在本阶段,我们主要将1.2中的代码片段2进行pytorch版本的改进。因为pytorch已经编写了很多内置的模块,供我们使用,我们没有必要再重复造轮子了。当然,如果你真的有所创新,你也可以通过最基础的代码创造出属于你自己的轮子!

在代码上,主要改进有以下3点:

  1. 使用内置的损失函数F.cross_entropy,代替log_softmax和nll函数。由于pytorch的特性,使得它的交叉熵损失函数是另外两个函数的结合,因此这里我们可以融合在一起,如果不清楚的,可以参见上一篇《清点装备》。
  2. 使用Module模块使得模型构建更加整洁。这也是pytorch的重要组成部分,我们能够编写的模型或者子模型,都可以继承此类,并只需要编写初始化和前向传播即可,这个在我们上一章中也已经讲了,这里不赘述。
  3. 使用fit函数优化训练过程。这是一个需要重点注意的,在pytorch中,没有封装好一个训练的模块,也是和keras最大的不同。这样的好处是,你可以对于训练在任何时候进行任何操作,因为它们本质上都是代码,代码都是可以操作的。这里我们给出例子。但是这里还不是最终版本,而是一个中间的改良版本。
#————————————————————————————初始化模型(改动2)——————————————————————
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()
#——————————————————————————尝试运行测试————————————
bs = 64  # 一批数据个数
xb = x_train[0:bs]  # 从x获取一小批数据
preds = model(xb)  # 预测值
preds[0], preds.shape
print(preds[0], preds.shape)
#——————————————————————————定义损失函数(改动1)————————————
import torch.nn.functional as F
loss_func = F.cross_entropy

yb = y_train[0:bs]
print(loss_func(preds, yb))
#——————————————————————————定义评估函数————————————
def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()
print(accuracy(preds, yb))
#——————————————————————————完整训练过程(改动3)————————————
lr = 0.5  # 学习率
epochs = 2  # 训练的轮数
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), accuracy(model(xb), yb))

3. 进一步封装和改进

在本阶段,我们主要将2中的代码片段进行进一步的改进。之所以分开,是因为此阶段将会在2的基础上再做一些更加细致的变动。在代码上,将会有以下改进:

  1. 使用内置的神经网络层nn.Linear代替原始写法构建模型。在pytorch里提供给了我们很多常用的层,我们只需要调用即可,这里在上一篇中也已经提到了一些。(工欲善其事必先利其器,这样才能够看的不糊涂)
  2. 使用内置的优化算法包torch.optim代替原始写法优化模型。pytorch也提供给了我们一些算法优化包,也就是反向传播过程的封装,这样我们就不用自己去写那么复杂的优化函数了。
#————————————————————————————初始化模型(改动1,2)——————————————————————
from torch import nn
from torch import optim
lr = 0.5  # 学习率
class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)
    def forward(self, xb):
        return self.lin(xb)

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

model, opt = get_model()
print(loss_func(model(xb), yb))
#——————————————————————————尝试运行测试————————————
bs = 64  # 一批数据个数
xb = x_train[0:bs]  # 从x获取一小批数据
preds = model(xb)  # 预测值
preds[0], preds.shape
print(preds[0], preds.shape)
#——————————————————————————定义损失函数(改动1)————————————
import torch.nn.functional as F
loss_func = F.cross_entropy

yb = y_train[0:bs]
print(loss_func(preds, yb))
#——————————————————————————定义评估函数————————————
def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()
print(accuracy(preds, yb))
#——————————————————————————完整训练过程(改动2)————————————
epochs = 2  # 训练的轮数
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()
        opt.step()
        opt.zero_grad()
fit()
print(loss_func(model(xb), yb), accuracy(model(xb), yb))

4. 将实验做完整

之前都是在实验的主要部分,即模型的搭建和训练上进行改进。这一节则主要针对数据的存储与读取以及增加验证集让实验更加完整。
因此,在本阶段,我们主要将3中的代码片段进行进一步的改进。在代码上,将会有以下改进:

  1. 使用内置的数据加载包Dataset和Dataloader代替原始写法加载数据。Dataset和Dataloader是非常好的标准化输入输出工具,如果我们能够好好利用它,能够使得我们的代码更具有可读性,而且更具有适用性。
  2. 使用增加验证集使得实验更加完整。

由于这两个部分的更改在同一个代码部分,因此我们分两个部分展示。

4.1 使用Dataset和Dataloader进行加载数据

关于Dataset,下面是官网的介绍:

Pytorch包含一个Dataset抽象类。Dataset可以是任何东西,但它始终包含一个__len__函数(通过Python中的标准函数len调用)和一个用来索引到内容中的__getitem__函数。 这篇教程以创建Dataset的自定义子类FacialLandmarkDataset为例进行介绍。
PyTorch中的TensorDataset是一个封装了张量的Dataset。通过定义长度和索引的方式,是我们可以对张量的第一维进行迭代,索引和切片。这将使我们在训练中,获取同一行中的自变量和因变量更加容易。

也就是说,Dataset能够帮助我们更好的存储、管理我们的数据。而Dataloader则是让我们在训练时,能够更好的利用Dataset来获取我们的数据。由于篇幅有限,我们只展示需要修改的部分。

#——————————————————————————完整训练过程(改动)————————————
epochs = 2  # 训练的轮数
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
def fit():
    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()
fit()

4.2 增加验证集

如果不使用验证集的话,就很难知道我们学习的效果如何。这就像是当我们学习完课堂的例题了。在训练集上的学习效果只是相当于我们理解例题的程度,而课后练习才是能够更为公平的评价我们的学习质量。

#——————————————————————————完整训练过程(改动)————————————
epochs = 2  # 训练的轮数
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader

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)
def fit():
    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))
fit()

5. 使得学习和反向传播过程更加简便

由于上一阶段,我们已经针对其训练过程进行了一些改进,在本阶段,我们主要将在原有的基础上使得整个过程更加简便。
因此,在代码上,将会有以下改进:

  1. 规范loss传播,我们将loss的传播过程独立出来,这样更容易控制训练和验证过程。
  2. 规范fit过程,我们将新的loss传播应用到fit函数中,使得代码更简洁明了。
#————————————————————————分离出loss传播(改动1)————————————————————————————————————
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)
#——————————————————————————完整训练过程(改动2)————————————
epochs = 2  # 训练的轮数
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
valid_ds = TensorDataset(x_valid, y_valid)

def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )
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)

#——————————————————————真正的训练过程——————————————————————————
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)

5.2 完全展示

到这里,我们基本的代码都已经修改完毕。最后,真正的训练过程只有3行代码,就能够解决我们的所有的问题。现在让我们来看一看重构后的所有代码组合在一起的形态吧!(这个代码是真正可以运行的!)

import torch
from torch import optim
import numpy as np
from torch import nn
lr = 0.5  # 学习率
bs = 64  # 一批数据个数
epochs = 2  # 训练的轮数
import torch.nn.functional as F
loss_func = F.cross_entropy
class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)
    def forward(self, xb):
        return self.lin(xb)

def get_model():
    model = Mnist_Logistic()
    return model, optim.SGD(model.parameters(), lr=lr)
def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )

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)
# 步骤3: 训练模型
def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        # 步骤3.1 训练过程
        model.train()
        for xb, yb in train_dl:
            loss_batch(model, loss_func, xb, yb, opt)
        # 步骤3.2 评估过程
        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)
#------------------主程序部分
from pathlib import Path
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
import pickle
import gzip
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
FILENAME = "mnist.pkl.gz"
# 步骤1:获取数据
with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
train_ds = TensorDataset(x_train, y_train)
valid_ds = TensorDataset(x_valid, y_valid)
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
# 步骤2:构建模型
model, opt = get_model()
# 步骤3:模型训练
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

透过完整的代码,我们发现其实整个过程我们都已经通过自己的双手完全的实现了,这样当我们在写新的模型和实验的时候,只需要从这个骨架进行微调即可。总的来说步骤如下:

  1. 获取数据。此步骤从文件中将数据变为模型可以识别的即可。
  2. 构建模型。这里我们构建属于我们自己的模型。
  3. 训练模型。这个阶段只需要训练评估模型即可。

这样我们就可以基于我们的框架将其应用到不同的模型、不同的场景了。

6. 框架多样性

6.1 使用卷积神经网络

这里我们使用卷积神经网络加入到我们写的模型之中。利用卷积神经网络,我们就可以编辑更为强大和更规范的神经网络。我们构建的3层卷积神经网络如下:

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))

然后,更新我们的获取模型的函数,并且使用随机梯度下降法的变种Momentum,它将前面步骤的更新也考虑在内,通常能够加快训练速度。

def get_model():
	lr = 0.1
    model = Mnist_CNN()
	opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
    return model, opt

更改过的程序仍然不够准确,但是能够明显感受到它运行的过程变慢了。这正是由于卷积神经网络参数更多的缘故。

6.2 使用序贯模型

如果我们有一个模块,这个模块是一个一个接一个流程,和keras一样可以使用序贯模型封装起来。下面是一个例子。

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)
def get_model():
	lr = 0.1
    model = nn.Sequential(
    Lambda(preprocess), # 像keras里的输入
    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)
    return model, opt

6.3 适用性更广——封装Dataloader

接下来要将另一个更重要的事情了,就是如何在数据加载时,就把数据处理好,这就涉及到Dataloader的封装。这是重点,可以使得我们能够做更多我们曾经没做过的事情。注意看好了,接下来的骚操作。

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)

可以看到,经过WrappedDataLoader函数,我们可以将数据的格式在进入模型之前,重新定义一下,使得它符合模型的输入格式。

6.4 更快的训练——使用GPU

pytorch并不默认使用GPU模式,而需要你手工决定数据和模型是否在GPU上,使用以下代码来进行一个适配。

dev = torch.device(
    "cuda") if torch.cuda.is_available() else torch.device("cpu")
def preprocess(x, y):
    return x.view(-1, 1, 28, 28).to(dev), y.to(dev) # 将数据放置到GPU上
model.to(dev) # 将模型放置到GPU上

7. 小结

本章我们主要利用一个例子梳理了利用pytorch构建一个完整的神经网络,详细介绍了各个部分,以及我们能改进的各项工作。总的来说,总结如下:

  1. 获取数据至本机
  2. 读取数据至内存
  3. 将数据转换为模型适配的形式
  4. 构建模型
  5. 进行训练
  6. 评估、测试实验结果

通过本章,我们能够清楚的知道每个部分使用pytorch该如何编写了,在接下来,我们就要跟上时代的步伐,了解最新的模型了!

发布了232 篇原创文章 · 获赞 547 · 访问量 51万+

猜你喜欢

转载自blog.csdn.net/qq_35082030/article/details/104343587