动手深度学习——训练详解



文章目录

  • 动手深度学习——训练详解
  • 前言
  • 一、线性模型
  • 1.线性回归
  • 二、分类精准度
  • 1.定义accuracy函数:
  • 2.精度函数
  • 3.训练
  • 4.画图
  • 5.train_ch3函数
  • 三、卷积网络训练
  • 1.精度计算
  • 2.训练模型
  • 总结



前言

  • 在刚刚入门深度学习的大坑时,怎么完成一次训练往往是第一道门槛。而完成一次训练大概有两个难点,其一是数据集的搭建,这个主要涉及dataset和dataloader的使用,后续会更如何构造一个可以训练的数据集;其二就是训练的步骤。
  • 而训练步骤其实非常简单,现在看来,本人在此徘徊许久还是因为把深度学习代码想的太难了,没有认真的好好看看,好好打打代码,所以在此整理下从线性模型到CNN的模型的训练过程。

本文参照李沐老师的动手深度学习课程的相关内容


一、线性模型

1.线性回归

import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
################数据读入#################
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
def load_array(data_arrays, batch_size, is_train=True):  #@save
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
################模型确定#################
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
loss = nn.MSELoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

通过这个简单的案例,就可以得到一个大概的训练框架。

  1. 数据集准备,将数据准备成一个dataloader,dataloader中需要指明数据集,batch_size等参数
  2. 确定好网络结构、损失函数、训练器等部分
  3. epoch利用循环控制:(1)计算当前网络权重下的正向输出结果和实际结果的损失;(2)梯度置0,防止梯度累计;(3)反向传播;(4)梯度下降(最经典的训练四步走)
  4. 每个epoch结束输出损失。

二、分类精准度

在李沐大神的动手深度学习的配套代码中,chapter_linear-networks章节的softmax-regression-scratch第一次引入了train_ch3训练函数,该函数引入了分类精确度及画图的具体过程

1.定义accuracy函数:

代码如下(示例):

def accuracy(y_hat, y):  #@save
    """计算预测正确的数量"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

函数的思路很简单:(1)判预测结果是否是二维矩阵(二维矩阵的原因:一个维度是需要预测的数据记录数量,一个维度是预测的各个类别的概率);(2)如果是,我们默认概率最大的那个类别是预测类别;(3)由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量;(4)求和
函数返回的结果是预测正确的数量,如果要得到正确率,需要/数据总量,即accuracy(y_hat, y) / len(y)

2.精度函数

class Accumulator:  #@save
    """累加器"""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
        
def evaluate_accuracy(net, data_iter):  #@save
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module):
        net.eval()  # 将模型设置为评估模式,只能进行前向传播
    metric = Accumulator(2)  # 正确预测数、预测总数
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

从evaluate_accuracy函数进入聊下代码思路:

  • 如果是nn的net,置为只能forward的状态
  • 定义一个累加器(累加器的每一个维度自定义,Accumulator只做单纯的累加)
  • 每一轮dataiter进行累加正确数和总数的累加
  • 相除就是准确率

注意:Accumulator是一个完全自由,完全自定义的类,千万不要认为Accumulator(2)的含义就是正确预测数、预测总数,它仅仅是一个累加器罢了。
Accumulator中做重要的两个函数分别是__init__,add。下面简单讲述下两个函数的具体意义与实现方式:

  • init:定义了一个数组,n表示的是希望的维度
  • add:这个函数的实现非常简洁,*args作为传参需要和data同长,zip(x,y)将x,y中的元素依次拿出,配对打包,形成(x[n],y[n])付给a,b,完成累加。

3.训练

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

将训练四步走和评估结合即可

4.画图

class Animator:  #@save
    """在动画中绘制数据"""
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
                 ylim=None, xscale='linear', yscale='linear',
                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
                 figsize=(3.5, 2.5)):
        # 增量地绘制多条线
        if legend is None:
            legend = []
        d2l.use_svg_display()
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        # 使用lambda函数捕获参数
        self.config_axes = lambda: d2l.set_axes(
            self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # 向图表中添加多个数据点
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        display.display(self.fig)
        display.clear_output(wait=True)

不重要,知道参数意义即可

5.train_ch3函数

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """训练模型(定义见第3章)"""
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc

这样各个代码的意义就很清楚了:

  1. 定义画图工具
  2. 得到每个epoch的训练矩阵
  3. 计算准确度
  4. 画图

三、卷积网络训练

由于卷积网络所需算力较大,因此需要使用gpu,因此和train_ch3不同的点在于,需要将tensor移至gpu上运算。

1.精度计算

def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
    """使用GPU计算模型在数据集上的精度"""
    if isinstance(net, nn.Module):
        net.eval()  # 设置为评估模式
        if not device:
            device = next(iter(net.parameters())).device
    # 正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(X, list):
                # BERT微调所需的(之后将介绍)
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)
            y = y.to(device)
            metric.add(d2l.accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

和之前的evaluate_accuracy相比只差在to(device)

2.训练模型

#@save
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    """用GPU训练模型(在第六章定义)"""
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on', device)
    net.to(device)
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = d2l.Timer(), len(train_iter)
    
    for epoch in range(num_epochs):
        # 训练损失之和,训练准确率之和,样本数
        metric = d2l.Accumulator(3)
        net.train()
        
        for i, (X, y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            print(y_hat)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            
            with torch.no_grad():
                metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
            timer.stop()
            train_l = metric[0] / metric[2]
            train_acc = metric[1] / metric[2]
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches,
                             (train_l, train_acc, None))
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
        
    print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(device)}')
  • 仔细看下代码内容,和添加了分类精度后的训练步骤是十分相似的,定义好各个要素,四步走训练,累加器累加,计算准确度等参数,放入画图工具画图。
  • 唯一的区别就在于,所有的tensor都放在了gpu上。

需要注意的是,在调用训练函数时,使用到了try_gpu()函数
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

其中的try_gpu()代码如下:

def try_gpu(i=0):  #@save
    """如果存在,则返回gpu(i),否则返回cpu()"""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

实际上也只是完成了gpu的选择。

总结

本人才疏学浅,整理过程中必然会有一定的纰漏以及错误,请各位移步B站李沐大神动手深度学习pytorch版进行学习。