动手学PyTorch | (39) 小批量随机梯度下降

目录

1. 小批量梯度下降

2. 读取数据

3. 从0开始实现

4. 简洁实现

5. 小结


1. 小批量梯度下降

在每一次迭代中,梯度下降使用整个训练数据集来计算梯度,因此它有时也被称为批量梯度下降 (batch gradient descent)。而随机梯度下降在每次迭代中只随机采样一个样本来计算梯度。正如我们在前几节中所看到的,我们还可以在每轮迭代中随机均匀采样多个样本来组成一个⼩批量,然后使用这个⼩批量来计算梯度。下⾯就来描述小批量随机梯度下降。

设⽬标函数f(x):R^d->R.在迭代开始前的时间步设为0。该时间步的⾃变量记为x_0 \in R^d,通常由随机初始化得到。在接下来的每⼀个时间步t>0中,⼩批量随机梯度下降随机均匀采样⼀个由训练数据样本索引组成的⼩批量B_t .我们可以通过重复采样(sampling with replacement)或者不重复采样(sampling without replacement)得到一个小批量中的各个样本。前者允许同一个⼩小批量中出现重复的样本(当不同类别的样本数不均衡时,经常使用,在一个mini-batch中多采样类别较少的样本),后者则不允许如此,且更常见。对于这两者间的任一种方式,都可以使⽤:

动手学PyTorch | (39) 小批量随机梯度下降_第1张图片

来计算时间步t的⼩批量 B_t上目标函数位于x_{t-1}处的梯度g_t.这里|B|代表批量⼤小,即小批量中样本的个数,是一个超参数。同随机梯度一样,重复采样所得的⼩批量随机梯度g_t也是对梯度\bigtriangledown f(x_{t-1})的⽆偏估计。给定学习率\eta_t > 0,⼩批量随机梯度下降对自变量的迭代如下:

基于随机采样得到的梯度的方差在迭代过程中无法减小,因此在实际中,(小批量)随机梯度下降的学习率可以在迭代过程中⾃我衰减,例如\eta_t = \eta t^ \alpha(\alpha=-1,-0.5)  \eta_t = \eta \alpha^t(\alpha=0.95)或者每迭代若干次后将学习率衰减一次。如此一来,学习率和(⼩批量)随机梯度乘积的⽅差会减小。⽽梯度下降(batch)在迭代过程中一直使用⽬标函数的真实梯度,⽆须⾃我衰减学习率。

⼩批量随机梯度下降中每次迭代的计算开销为O(|B|) 。当批量⼤小为1时,该算法即为随机梯度下降; 当批量⼤小等于训练数据样本数时,该算法即为梯度下降。当批量较小时,每次迭代中使用的样本少, 这会导致并行处理和内存使用效率变低。这使得在计算同样数目样本的情况下比使用更⼤批量时所花时间更多。当批量较⼤时,每个小批量梯度里可能含有更多的冗余信息。为了得到较好的解,批量较大时⽐批量较⼩时需要计算的样本数⽬可能更多,例如增大迭代周期数。

 

2. 读取数据

本章里我们将使用⼀个来⾃NASA的测试不同⻜机机翼噪音的数据集来⽐较各个优化算法。我们使⽤该数据集的前1,500个样本和5个特征,并使用标准化对数据进行预处理。

%matplotlib inline
import numpy as np
import time
import torch
from torch import nn, optim
import sys
sys.path.append(".") 
import d2lzh_pytorch as d2l

print(torch.__version__)
def get_data_ch7():  # 可以把本函数保存在d2lzh_pytorch包中方便以后使用
    data = np.genfromtxt('./data/airfoil_self_noise.dat', delimiter='\t')
    data = (data - data.mean(axis=0)) / data.std(axis=0) # 标准化
    return torch.tensor(data[:1500, :-1], dtype=torch.float32), \
           torch.tensor(data[:1500, -1], dtype=torch.float32) # 前1500个样本(每个样本5个特征)

features, labels = get_data_ch7()
features.shape

3. 从0开始实现

(线性回归的从零开始实现)中已经实现过小批量随机梯度下降算法。我们在这里将它的输⼊参数变得更加通用,主要是为了了⽅便后⾯介绍的其他优化算法也可以使⽤同样的输入。具体来说,我们添加了⼀个状态输入并将超参数放在字典里。此外,我们将在训练函数里对各个⼩批量样本的损失求平均,因此优化算法里的梯度不需要除以批量⼤小(已经有系数 1/batch_size)。

def sgd(params, states, hyperparams):
    for p in params:
        p.data -= hyperparams['lr'] * p.grad.data

下⾯实现⼀个通用的训练函数,以⽅便后⾯介绍的其他优化算法使用。它初始化⼀个线性回归模型,然后可以使⽤⼩批量随机梯度下降以及后续小节介绍的其他算法来训练模型。

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def train_ch7(optimizer_fn, states, hyperparams, features, labels,
              batch_size=10, num_epochs=2):
    # 初始化模型
    net, loss = d2l.linreg, d2l.squared_loss
    
    w = torch.nn.Parameter(torch.tensor(np.random.normal(0, 0.01, size=(features.shape[1], 1)), dtype=torch.float32),
                           requires_grad=True)
    b = torch.nn.Parameter(torch.zeros(1, dtype=torch.float32), requires_grad=True)

    def eval_loss():
        return loss(net(features, w, b), labels).mean().item()

    ls = [eval_loss()]
    data_iter = torch.utils.data.DataLoader(
        torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)
    
    for _ in range(num_epochs):
        start = time.time()
        for batch_i, (X, y) in enumerate(data_iter):
            l = loss(net(X, w, b), y).mean()  # 使用平均损失
            
            # 梯度清零
            if w.grad is not None:
                w.grad.data.zero_()
                b.grad.data.zero_()
                
            l.backward()
            optimizer_fn([w, b], states, hyperparams)  # 迭代模型参数
            if (batch_i + 1) * batch_size % 100 == 0:
                ls.append(eval_loss())  # 每100个mini-batch记录下当前训练误差
    # 打印结果和作图
    print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
    d2l.set_figsize()
    d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
    d2l.plt.xlabel('epoch')
    d2l.plt.ylabel('loss')

当批量⼤小为样本总数1,500时,优化使用的是梯度下降。梯度下降的1个迭代周期(epoch)对模型参数只迭代(更新)1 次。可以看到6次迭代后⽬标函数值(训练损失)的下降趋向了平稳。

def train_sgd(lr, batch_size, num_epochs=2):
    train_ch7(sgd, None, {'lr': lr}, features, labels, batch_size, num_epochs)

train_sgd(1, 1500, 6)

动手学PyTorch | (39) 小批量随机梯度下降_第2张图片

当批量⼤小为1时,优化使用的是随机梯度下降。为了简化实现,有关(⼩批量)随机梯度下降的实验中,我们未对学习率进⾏行⾃我衰减,⽽是直接采用较小的常数学习率。随机梯度下降中,每处理⼀个样本会更新⼀次⾃变量(模型参数),一个迭代周期里会对⾃变量进行1,500次更新。可以看到,目标函数值的下降在1个迭代周期后就变得较为平缓。

train_sgd(0.005, 1)

动手学PyTorch | (39) 小批量随机梯度下降_第3张图片

虽然随机梯度下降和梯度下降在一个迭代周期里都处理了1,500个样本,但实验中随机梯度下降的一个迭代周期耗时更多。这是因为随机梯度下降在⼀个迭代周期里做了更多次的⾃变量迭代,⽽且单样本的梯度计算难以有效利用⽮量计算(带来的加速效果)。

当批量⼤小为10时,优化使用的是⼩批量随机梯度下降。它在每个迭代周期(epoch)的耗时介于梯度下降和随机梯度下降的耗时之间。

train_sgd(0.05, 10)

动手学PyTorch | (39) 小批量随机梯度下降_第4张图片

 

4. 简洁实现

在PyTorch里可以通过创建optimizer实例来调用优化算法。这能让实现更简洁。下⾯实现一个通⽤的训练函数,它通过优化算法的函数optimizer_fn和超参数optimizer_hyperparams来创建optimizer实例。

# 这里第一个参数是优化器函数而不是优化器的名字
# 例如: optimizer_fn=torch.optim.SGD, optimizer_hyperparams={"lr": 0.05}
def train_pytorch_ch7(optimizer_fn, optimizer_hyperparams, features, labels,
                    batch_size=10, num_epochs=2):
    # 初始化模型
    net = nn.Sequential(
        nn.Linear(features.shape[-1], 1)
    )
    loss = nn.MSELoss()
    optimizer = optimizer_fn(net.parameters(), **optimizer_hyperparams)

    def eval_loss():#自带的mse没有除以2
        return loss(net(features).view(-1), labels).item() / 2

    ls = [eval_loss()]
    data_iter = torch.utils.data.DataLoader(
        torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)

    for _ in range(num_epochs):
        start = time.time()
        for batch_i, (X, y) in enumerate(data_iter):
            # 除以2是为了和train_ch7保持一致, 因为squared_loss中除了2
            l = loss(net(X).view(-1), y) / 2 
            
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
            if (batch_i + 1) * batch_size % 100 == 0:
                ls.append(eval_loss())
    # 打印结果和作图
    print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
    d2l.set_figsize()
    d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
    d2l.plt.xlabel('epoch')
    d2l.plt.ylabel('loss')
train_pytorch_ch7(optim.SGD, {"lr": 0.05}, features, labels, 10)

动手学PyTorch | (39) 小批量随机梯度下降_第5张图片

 

5. 小结

1)小批量随机梯度每次随机均匀采样⼀个⼩批量的训练样本来计算梯度。

2)在实际中,(小批量)随机梯度下降的学习率可以在迭代过程中自我衰减。

3)通常,小批量随机梯度在每个迭代周期的耗时介于梯度下降和随机梯度下降的耗时之间。

 

你可能感兴趣的:(动手学PyTorch,动手学PyTorch,小批量梯度下降)