PyTorch 深度学习(GPT 重译)(二)(4)

简介: PyTorch 深度学习(GPT 重译)(二)

PyTorch 深度学习(GPT 重译)(二)(3)https://developer.aliyun.com/article/1485205

我们只需开始一个 requires_grad 设置为 True 的张量,然后调用模型并计算损失,然后在 loss 张量上调用 backward

# In[7]:
loss = loss_fn(model(t_u, *params), t_c)
loss.backward()
params.grad
# Out[7]:
tensor([4517.2969,   82.6000])

此时,paramsgrad 属性包含了相对于每个元素的 params 的损失的导数。

当我们在参数 wb 需要梯度时计算我们的 loss 时,除了执行实际计算外,PyTorch 还会创建带有操作(黑色圆圈)的 autograd 图,如图 5.10 顶部行所示。当我们调用 loss.backward() 时,PyTorch 沿着这个图的反向方向遍历以计算梯度,如图的底部行所示的箭头所示。

图 5.10 模型的前向图和后向图,使用 autograd 计算

累积 grad 函数

我们可以有任意数量的张量,其requires_grad设置为True,以及任意组合的函数。在这种情况下,PyTorch 会计算整个函数链(计算图)中损失的导数,并将其值累积在这些张量的grad属性中(图的叶节点)。

警告!大坑在前方。这是 PyTorch 新手——以及许多更有经验的人——经常会遇到的问题。我们刚刚写的是累积,而不是存储

警告 调用backward会导致导数在叶节点累积。在使用参数更新后,我们需要显式地将梯度清零

让我们一起重复:调用backward会导致导数在叶节点累积。因此,如果backward在之前被调用,损失会再次被评估,backward会再次被调用(就像在任何训练循环中一样),并且每个叶节点的梯度会累积(即求和)在上一次迭代计算的梯度之上,这会导致梯度的值不正确。

为了防止这种情况发生,我们需要在每次迭代时显式地将梯度清零。我们可以很容易地使用就地zero_方法来实现:

# In[8]:
if params.grad is not None:
    params.grad.zero_()

注意 你可能会好奇为什么清零梯度是一个必需的步骤,而不是在每次调用backward时自动清零。这样做提供了更多在处理复杂模型中梯度时的灵活性和控制。

将这个提醒铭记在心,让我们看看我们启用自动求导的训练代码是什么样子,从头到尾:

# In[9]:
def training_loop(n_epochs, learning_rate, params, t_u, t_c):
    for epoch in range(1, n_epochs + 1):
        if params.grad is not None:                # ❶
            params.grad.zero_()
        t_p = model(t_u, *params)
        loss = loss_fn(t_p, t_c)
        loss.backward()
        with torch.no_grad():                      # ❷
            params -= learning_rate * params.grad
        if epoch % 500 == 0:
            print('Epoch %d, Loss %f' % (epoch, float(loss)))
    return params

❶ 这可以在调用 loss.backward()之前的循环中的任何时候完成。

❷ 这是一段有些繁琐的代码,但正如我们将在下一节看到的,实际上并不是问题。

请注意,我们更新params的代码并不像我们可能期望的那样直截了当。有两个特殊之处。首先,我们使用 Python 的with语句在no_grad上下文中封装更新。这意味着在with块内,PyTorch 自动求导机制应该不要关注:即,在前向图中不添加边。实际上,当我们执行这段代码时,PyTorch 记录的前向图在我们调用backward时被消耗掉,留下params叶节点。但现在我们想要在开始构建新的前向图之前更改这个叶节点。虽然这种用例通常包含在我们在第 5.5.2 节中讨论的优化器中,但当我们在第 5.5.4 节看到no_grad的另一个常见用法时,我们将更仔细地看一下。

其次,我们就地更新params。这意味着我们保留相同的params张量,但从中减去我们的更新。在使用自动求导时,我们通常避免就地更新,因为 PyTorch 的自动求导引擎可能需要我们将要修改的值用于反向传播。然而,在这里,我们在没有自动求导的情况下操作,保留params张量是有益的。在第 5.5.2 节中向优化器注册参数时,不通过将新张量分配给其变量名来替换参数将变得至关重要。

让我们看看它是否有效:

# In[10]:
training_loop(
    n_epochs = 5000,
    learning_rate = 1e-2,
    params = torch.tensor([1.0, 0.0], requires_grad=True),  # ❶
    t_u = t_un,                                             # ❷
    t_c = t_c)
# Out[10]:
Epoch 500, Loss 7.860116
Epoch 1000, Loss 3.828538
Epoch 1500, Loss 3.092191
Epoch 2000, Loss 2.957697
Epoch 2500, Loss 2.933134
Epoch 3000, Loss 2.928648
Epoch 3500, Loss 2.927830
Epoch 4000, Loss 2.927679
Epoch 4500, Loss 2.927652
Epoch 5000, Loss 2.927647
tensor([  5.3671, -17.3012], requires_grad=True)

❶ 添加 requires_grad=True 至关重要

❷ 再次,我们使用了标准化的 t_un 而不是 t_u。

结果与我们之前得到的相同。对我们来说很好!这意味着虽然我们能够手动计算导数,但我们不再需要这样做。

5.5.2 自选优化器

在示例代码中,我们使用了普通梯度下降进行优化,这对我们简单的情况效果很好。不用说,有几种优化策略和技巧可以帮助收敛,特别是在模型变得复杂时。

我们将在后面的章节深入探讨这个主题,但现在是介绍 PyTorch 如何将优化策略从用户代码中抽象出来的正确时机:也就是我们已经检查过的训练循环。这样可以避免我们不得不手动更新模型的每个参数的样板繁琐工作。torch模块有一个optim子模块,我们可以在其中找到实现不同优化算法的类。这里是一个简略列表(code/p1ch5/3_optimizers.ipynb):

# In[5]:
import torch.optim as optim
dir(optim)
# Out[5]:
['ASGD',
 'Adadelta',
 'Adagrad',
 'Adam',
 'Adamax',
 'LBFGS',
 'Optimizer',
 'RMSprop',
 'Rprop',
 'SGD',
 'SparseAdam',
...
]

每个优化器构造函数的第一个输入都是参数列表(也称为 PyTorch 张量,通常将requires_grad设置为True)。所有传递给优化器的参数都会被保留在优化器对象内部,因此优化器可以更新它们的值并访问它们的grad属性,如图 5.11 所示。

图 5.11(A)优化器如何保存参数的概念表示。(B)从输入计算损失后,(C)调用.backward会使参数上的.grad被填充。(D)此时,优化器可以访问.grad并计算参数更新。

每个优化器都暴露两个方法:zero_gradstepzero_grad将在构造时将所有传递给优化器的参数的grad属性清零。step根据特定优化器实现的优化策略更新这些参数的值。

使用梯度下降优化器

让我们创建params并实例化一个梯度下降优化器:

# In[6]:
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-5
optimizer = optim.SGD([params], lr=learning_rate)

这里 SGD 代表随机梯度下降。实际上,优化器本身就是一个标准的梯度下降(只要momentum参数设置为0.0,这是默认值)。术语随机来自于梯度通常是通过对所有输入样本的随机子集进行平均得到的,称为小批量。然而,优化器不知道损失是在所有样本(标准)上评估的还是在它们的随机子集(随机)上评估的,所以在这两种情况下算法实际上是相同的。

无论如何,让我们尝试一下我们新的优化器:

# In[7]:
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()
optimizer.step()
params
# Out[7]:
tensor([ 9.5483e-01, -8.2600e-04], requires_grad=True)

在调用step时,params的值会被更新,而无需我们自己操作!发生的情况是,优化器查看params.grad并更新params,从中减去learning_rate乘以grad,与我们以前手动编写的代码完全相同。

准备将这段代码放入训练循环中?不!几乎让我们犯了大错–我们忘记了将梯度清零。如果我们在循环中调用之前的代码,梯度会在每次调用backward时在叶子节点中累积,我们的梯度下降会一团糟!这是循环准备就绪的代码,正确位置是在backward调用之前额外加上zero_grad

# In[8]:
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)
t_p = model(t_un, *params)
loss = loss_fn(t_p, t_c)
optimizer.zero_grad()      # ❶
loss.backward()
optimizer.step()
params
# Out[8]:
tensor([1.7761, 0.1064], requires_grad=True)

❶ 与以前一样,这个调用的确切位置有些随意。它也可以在循环中较早的位置。

太棒了!看看optim模块如何帮助我们将特定的优化方案抽象出来?我们所要做的就是向其提供一个参数列表(该列表可能非常长,对于非常深的神经网络模型是必需的),然后我们可以忘记细节。

让我们相应地更新我们的训练循环:

# In[9]:
def training_loop(n_epochs, optimizer, params, t_u, t_c):
    for epoch in range(1, n_epochs + 1):
        t_p = model(t_u, *params)
        loss = loss_fn(t_p, t_c)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch % 500 == 0:
            print('Epoch %d, Loss %f' % (epoch, float(loss)))
    return params
# In[10]:
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)   # ❶
training_loop(
    n_epochs = 5000,
    optimizer = optimizer,
    params = params,                                # ❶
    t_u = t_un,
    t_c = t_c)
# Out[10]:
Epoch 500, Loss 7.860118
Epoch 1000, Loss 3.828538
Epoch 1500, Loss 3.092191
Epoch 2000, Loss 2.957697
Epoch 2500, Loss 2.933134
Epoch 3000, Loss 2.928648
Epoch 3500, Loss 2.927830
Epoch 4000, Loss 2.927680
Epoch 4500, Loss 2.927651
Epoch 5000, Loss 2.927648
tensor([  5.3671, -17.3012], requires_grad=True)

❶ 很重要的一点是两个参数必须是同一个对象;否则优化器将不知道模型使用了哪些参数。

再次得到与以前相同的结果。太好了:这进一步证实了我们知道如何手动下降梯度!

测试其他优化器

为了测试更多的优化器,我们只需实例化一个不同的优化器,比如Adam,而不是SGD。其余代码保持不变。非常方便。

我们不会详细讨论 Adam;可以说,它是一种更复杂的优化器,其中学习率是自适应设置的。此外,它对参数的缩放不太敏感–如此不敏感,以至于我们可以回到使用原始(非归一化)输入t_u,甚至将学习率增加到1e-1,Adam 也不会有任何反应:

# In[11]:
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-1
optimizer = optim.Adam([params], lr=learning_rate)  # ❶
training_loop(
    n_epochs = 2000,
    optimizer = optimizer,
    params = params,
    t_u = t_u,                                      # ❷
    t_c = t_c)
# Out[11]:
Epoch 500, Loss 7.612903
Epoch 1000, Loss 3.086700
Epoch 1500, Loss 2.928578
Epoch 2000, Loss 2.927646
tensor([  0.5367, -17.3021], requires_grad=True)

❶ 新的优化器类

❷ 我们又回到了将t_u作为我们的输入。

优化器并不是我们训练循环中唯一灵活的部分。让我们将注意力转向模型。为了在相同数据和相同损失上训练神经网络,我们需要改变的只是model函数。在这种情况下并没有特别意义,因为我们知道将摄氏度转换为华氏度相当于进行线性变换,但我们还是会在第六章中这样做。我们很快就会看到,神经网络允许我们消除对我们应该逼近的函数形状的任意假设。即使如此,我们将看到神经网络如何在基础过程高度非线性时进行训练(例如在描述图像与句子之间的情况,正如我们在第二章中看到的)。

我们已经涉及了许多基本概念,这些概念将使我们能够在了解内部运作的情况下训练复杂的深度学习模型:反向传播来估计梯度,自动微分,以及使用梯度下降或其他优化器来优化模型的权重。实际上,并没有太多内容。其余的大部分内容都是填空,无论填空有多广泛。

接下来,我们将提供一个关于如何分割样本的插曲,因为这为学习如何更好地控制自动微分提供了一个完美的用例。

5.5.3 训练、验证和过拟合

约翰内斯·开普勒教给我们一个迄今为止我们没有讨论过的最后一件事,记得吗?他将部分数据保留在一边,以便可以在独立观测上验证他的模型。这是一件至关重要的事情,特别是当我们采用的模型可能近似于任何形状的函数时,就像神经网络的情况一样。换句话说,一个高度适应的模型将倾向于使用其许多参数来确保损失在数据点处最小化,但我们无法保证模型在数据点之外或之间的表现。毕竟,这就是我们要求优化器做的事情:在数据点处最小化损失。毫无疑问,如果我们有一些独立的数据点,我们没有用来评估损失或沿着其负梯度下降,我们很快就会发现,在这些独立数据点上评估损失会产生比预期更高的损失。我们已经提到了这种现象,称为过拟合

我们可以采取的第一步对抗过拟合的行动是意识到它可能发生。为了做到这一点,正如开普勒在 1600 年发现的那样,我们必须从数据集中取出一些数据点(验证集),并仅在剩余数据点上拟合我们的模型(训练集),如图 5.12 所示。然后,在拟合模型时,我们可以在训练集上评估损失一次,在验证集上评估损失一次。当我们试图决定我们是否已经很好地将模型拟合到数据时,我们必须同时看两者!

图 5.12 数据生成过程的概念表示以及训练数据和独立验证数据的收集和使用。

评估训练损失

训练损失将告诉我们,我们的模型是否能够完全拟合训练集——换句话说,我们的模型是否具有足够的容量来处理数据中的相关信息。如果我们神秘的温度计以对数刻度测量温度,我们可怜的线性模型将无法拟合这些测量值,并为我们提供一个合理的摄氏度转换。在这种情况下,我们的训练损失(在训练循环中打印的损失)会在接近零之前停止下降。

深度神经网络可以潜在地逼近复杂的函数,只要神经元的数量,因此参数的数量足够多。参数数量越少,我们的网络将能够逼近的函数形状就越简单。所以,规则 1:如果训练损失不降低,那么模型对数据来说可能太简单了。另一种可能性是我们的数据只包含让其解释输出的有意义信息:如果商店里的好人卖给我们一个气压计而不是温度计,我们将很难仅凭压力来预测摄氏度,即使我们使用魁北克最新的神经网络架构(www.umontreal.ca/en/artificialintelligence)。

泛化到验证集

那验证集呢?如果在验证集中评估的损失不随着训练集一起减少,这意味着我们的模型正在改善对训练期间看到的样本的拟合,但没有泛化到这个精确集之外的样本。一旦我们在新的、以前未见过的点上评估模型,损失函数的值就会很差。所以,规则 2:如果训练损失和验证损失发散,我们就过拟合了。

让我们深入探讨这种现象,回到我们的温度计示例。我们可以决定用更复杂的函数来拟合数据,比如分段多项式或非常大的神经网络。它可能会生成一个模型,沿着数据点蜿蜒前进,就像图 5.13 中所示,只是因为它将损失推得非常接近零。由于函数远离数据点的行为不会增加损失,因此没有任何东西可以限制模型对训练数据点之外的输入。


图 5.13 过拟合的极端示例

那么,治疗方法呢?好问题。从我们刚才说的来看,过拟合看起来确实是确保模型在数据点之间的行为对我们试图逼近的过程是合理的问题。首先,我们应该确保我们为该过程收集足够的数据。如果我们通过以低频率定期对正弦过程进行采样来收集数据,我们将很难将模型拟合到它。

假设我们有足够的数据点,我们应该确保能够拟合训练数据的模型在它们之间尽可能地规则。有几种方法可以实现这一点。一种方法是向损失函数添加惩罚项,使模型更平滑、变化更慢(在一定程度上)更便宜。另一种方法是向输入样本添加噪声,人为地在训练数据样本之间创建新的数据点,并迫使模型尝试拟合这些数据点。还有其他几种方法,所有这些方法都与这些方法有些相关。但我们可以为自己做的最好的事情,至少作为第一步,是使我们的模型更简单。从直觉上讲,一个简单的模型可能不会像一个更复杂的模型那样完美地拟合训练数据,但它可能在数据点之间的行为更加规则。

我们在这里有一些不错的权衡。一方面,我们需要模型具有足够的容量来适应训练集。另一方面,我们需要模型避免过拟合。因此,为了选择神经网络模型的正确参数大小,该过程基于两个步骤:增加大小直到适应,然后缩小直到停止过拟合。

我们将在第十二章中更多地了解这一点–我们将发现我们的生活将是在拟合和过拟合之间的平衡。现在,让我们回到我们的例子,看看我们如何将数据分成训练集和验证集。我们将通过相同的方式对t_ut_c进行洗牌,然后将结果洗牌后的张量分成两部分。

分割数据集

对张量的元素进行洗牌相当于找到其索引的排列。randperm函数正是这样做的:

# In[12]:
n_samples = t_u.shape[0]
n_val = int(0.2 * n_samples)
shuffled_indices = torch.randperm(n_samples)
train_indices = shuffled_indices[:-n_val]
val_indices = shuffled_indices[-n_val:]
train_indices, val_indices           # ❶
# Out[12]:
(tensor([9, 6, 5, 8, 4, 7, 0, 1, 3]), tensor([ 2, 10]))

❶ 由于这些是随机的,如果你的数值与这里的不同,不要感到惊讶。

我们刚刚得到了索引张量,我们可以使用它们从数据张量开始构建训练和验证集:

# In[13]:
train_t_u = t_u[train_indices]
train_t_c = t_c[train_indices]
val_t_u = t_u[val_indices]
val_t_c = t_c[val_indices]
train_t_un = 0.1 * train_t_u
val_t_un = 0.1 * val_t_u

我们的训练循环并没有真正改变。我们只是想在每个时代额外评估验证损失,以便有机会识别我们是否过拟合:

# In[14]:
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
                  train_t_c, val_t_c):
    for epoch in range(1, n_epochs + 1):
        train_t_p = model(train_t_u, *params)        # ❶
        train_loss = loss_fn(train_t_p, train_t_c)
        val_t_p = model(val_t_u, *params)            # ❶
        val_loss = loss_fn(val_t_p, val_t_c)
        optimizer.zero_grad()
        train_loss.backward()                        # ❷
        optimizer.step()
        if epoch <= 3 or epoch % 500 == 0:
            print(f"Epoch {epoch}, Training loss {train_loss.item():.4f},"
                  f" Validation loss {val_loss.item():.4f}")
    return params
# In[15]:
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)
training_loop(
    n_epochs = 3000,
    optimizer = optimizer,
    params = params,
    train_t_u = train_t_un,                          # ❸
    val_t_u = val_t_un,                              # ❸
    train_t_c = train_t_c,
    val_t_c = val_t_c)
# Out[15]:
Epoch 1, Training loss 66.5811, Validation loss 142.3890
Epoch 2, Training loss 38.8626, Validation loss 64.0434
Epoch 3, Training loss 33.3475, Validation loss 39.4590
Epoch 500, Training loss 7.1454, Validation loss 9.1252
Epoch 1000, Training loss 3.5940, Validation loss 5.3110
Epoch 1500, Training loss 3.0942, Validation loss 4.1611
Epoch 2000, Training loss 3.0238, Validation loss 3.7693
Epoch 2500, Training loss 3.0139, Validation loss 3.6279
Epoch 3000, Training loss 3.0125, Validation loss 3.5756
tensor([  5.1964, -16.7512], requires_grad=True)

❶ 这两对行是相同的,除了 train_* vs. val_*输入。

❷ 注意这里没有val_loss.backward(),因为我们不想在验证数据上训练模型。

❸ 由于我们再次使用 SGD,我们又回到了使用归一化的输入。

在这里,我们对我们的模型并不完全公平。验证集真的很小,因此验证损失只有到一定程度才有意义。无论如何,我们注意到验证损失高于我们的训练损失,尽管不是数量级。我们期望模型在训练集上表现更好,因为模型参数是由训练集塑造的。我们的主要目标是看到训练损失和验证损失都在减小。虽然理想情况下,两个损失值应该大致相同,但只要验证损失保持与训练损失相当接近,我们就知道我们的模型继续学习关于我们数据的泛化内容。在图 5.14 中,情况 C 是理想的,而 D 是可以接受的。在情况 A 中,模型根本没有学习;在情况 B 中,我们看到过拟合。我们将在第十二章看到更有意义的过拟合示例。


图 5.14 当查看训练(实线)和验证(虚线)损失时的过拟合情况。 (A) 训练和验证损失不减少;模型由于数据中没有信息或模型容量不足而无法学习。 (B) 训练损失减少,而验证损失增加:过拟合。 © 训练和验证损失完全同步减少。性能可能进一步提高,因为模型尚未达到过拟合的极限。 (D) 训练和验证损失具有不同的绝对值,但趋势相似:过拟合得到控制。

5.5.4 自动微分细节和关闭它

从之前的训练循环中,我们可以看到我们只在train_loss上调用backward。因此,错误只会基于训练集反向传播–验证集用于提供对模型在未用于训练的数据上输出准确性的独立评估。

在这一点上,好奇的读者可能会有一个问题的雏形。模型被评估两次–一次在train_t_u上,一次在val_t_u上–然后调用backward。这不会让自动微分混乱吗?backward会受到在验证集上传递期间生成的值的影响吗?

幸运的是,这种情况并不会发生。训练循环中的第一行评估modeltrain_t_u上产生train_t_p。然后从train_t_p评估train_loss。这创建了一个计算图,将train_t_u链接到train_t_ptrain_loss。当再次在val_t_u上评估model时,它会产生val_t_pval_loss。在这种情况下,将创建一个将val_t_u链接到val_t_pval_loss的单独计算图。相同的张量已经通过相同的函数modelloss_fn运行,生成了不同的计算图,如图 5.15 所示。

图 5.15 显示当在其中一个上调用.backward 时,梯度如何通过具有两个损失的图传播

这两个图唯一共同拥有的张量是参数。当我们在train_loss上调用backward时,我们在第一个图上运行backward。换句话说,我们根据从train_t_u生成的计算累积train_loss相对于参数的导数。

如果我们(错误地)在val_loss上也调用了backward,那么我们将在相同的叶节点上累积val_loss相对于参数的导数。还记得zero_grad的事情吗?每次我们调用backward时,梯度都会累积在一起,除非我们明确地将梯度清零?嗯,在这里会发生类似的事情:在val_loss上调用backward会导致梯度在params张量中累积,这些梯度是在train_loss.backward()调用期间生成的。在这种情况下,我们实际上会在整个数据集上训练我们的模型(包括训练和验证),因为梯度会依赖于两者。非常有趣。

这里还有另一个讨论的要素。由于我们从未在val_loss上调用backward,那么我们为什么要首先构建计算图呢?实际上,我们可以只调用modelloss_fn作为普通函数,而不跟踪计算。然而,构建自动求导图虽然经过了优化,但会带来额外的成本,在验证过程中我们完全可以放弃这些成本,特别是当模型有数百万个参数时。

为了解决这个问题,PyTorch 允许我们在不需要时关闭自动求导,使用torch.no_grad上下文管理器。在我们的小问题上,我们不会看到任何关于速度或内存消耗方面的有意义的优势。然而,对于更大的模型,差异可能会累积。我们可以通过检查val_loss张量的requires_grad属性的值来确保这一点:

# In[16]:
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u,
                  train_t_c, val_t_c):
    for epoch in range(1, n_epochs + 1):
        train_t_p = model(train_t_u, *params)
        train_loss = loss_fn(train_t_p, train_t_c)
        with torch.no_grad():                         # ❶
            val_t_p = model(val_t_u, *params)
            val_loss = loss_fn(val_t_p, val_t_c)
            assert val_loss.requires_grad == False    # ❷
        optimizer.zero_grad()
        train_loss.backward()
        optimizer.step()

❶ 这里是上下文管理器

❷ 检查我们的输出requires_grad参数在此块内被强制为 False

使用相关的set_grad_enabled上下文,我们还可以根据布尔表达式(通常表示我们是在训练还是推理模式下运行)来条件运行代码,启用或禁用autograd。例如,我们可以定义一个calc_forward函数,根据布尔train_is参数,以有或无自动求导的方式运行modelloss_fn

# In[17]:
def calc_forward(t_u, t_c, is_train):
    with torch.set_grad_enabled(is_train):
        t_p = model(t_u, *params)
        loss = loss_fn(t_p, t_c)
    return loss

5.6 结论

我们从一个大问题开始了这一章:机器如何能够从示例中学习?我们在本章的其余部分描述了优化模型以拟合数据的机制。我们选择坚持使用简单模型,以便在不需要的复杂性的情况下看到所有移动部件。

现在我们已经品尝了开胃菜,在第六章中我们终于要进入主菜了:使用神经网络来拟合我们的数据。我们将继续解决相同的温度计问题,但使用torch.nn模块提供的更强大工具。我们将采用相同的精神,使用这个小问题来说明 PyTorch 的更大用途。这个问题不需要神经网络来找到解决方案,但它将让我们更简单地了解训练神经网络所需的内容。

5.7 练习

  1. 重新定义模型为w2 * t_u ** 2 + w1 * t_u + b
  1. 哪些部分的训练循环等需要更改以适应这个重新定义?
  2. 哪些部分对于更换模型是不可知的?
  3. 训练后损失是更高还是更低?
  4. 实际结果是更好还是更差?

5.8 总结

  • 线性模型是用来拟合数据的最简单合理的模型。
  • 凸优化技术可以用于线性模型,但不适用于神经网络,因此我们专注于随机梯度下降进行参数估计。
  • 深度学习可以用于通用模型,这些模型并非专门用于解决特定任务,而是可以自动适应并专门化解决手头的问题。
  • 学习算法涉及根据观察结果优化模型参数。损失函数是执行任务时的错误度量,例如预测输出与测量值之间的误差。目标是尽可能降低损失函数。
  • 损失函数相对于模型参数的变化率可用于更新相同参数以减少损失。
  • PyTorch 中的 optim 模块提供了一系列用于更新参数和最小化损失函数的现成优化器。
  • 优化器使用 PyTorch 的 autograd 功能来计算每个参数的梯度,具体取决于该参数对最终输出的贡献。这使用户在复杂的前向传递过程中依赖于动态计算图。
  • with torch.no_grad(): 这样的上下文管理器可用于控制 autograd 的行为。
  • 数据通常被分成独立的训练样本集和验证样本集。这使我们能够在未经训练的数据上评估模型。
  • 过拟合模型发生在模型在训练集上的表现继续改善但在验证集上下降的情况下。这通常是由于模型没有泛化,而是记忆了训练集的期望输出。

¹ 据物理学家迈克尔·福勒回忆:mng.bz/K2Ej

² 理解开普勒定律的细节并不是理解本章所需的,但你可以在en.wikipedia.org/wiki/Kepler%27s_laws_of_planetary_motion找到更多信息。

³ 除非你是理论物理学家 😉.

⁴ 这个任务——将模型输出拟合为第四章讨论的类型的连续值——被称为回归问题。在第七章和第 2 部分中,我们将关注分类问题。

⁵ 本章的作者是意大利人,所以请原谅他使用合理的单位。

⁶ 权重告诉我们给定输入对输出的影响程度。偏差是如果所有输入都为零时的输出。

⁷ 与图 5.6 中显示的函数形成对比,该函数不是凸的。

⁸ 这个的花哨名称是超参数调整超参数指的是我们正在训练模型的参数,但超参数控制着这个训练的进行方式。通常这些是手动设置的。特别是,它们不能成为同一优化的一部分。

⁹ 或许是吧;我们不会判断你周末怎么过!

¹⁰ 糟糕!现在周六我们要做什么?

¹¹ 实际上,它将跟踪使用原地操作更改参数的情况。

¹² 我们不应认为使用 torch.no_grad 必然意味着输出不需要梯度。在特定情况下(涉及视图,如第 3.8.1 节所讨论的),即使在 no_grad 上下文中创建时,requires_grad 也不会设置为 False。如果需要确保,最好使用 detach 函数。

相关文章
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
24 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
33 0
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
42 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
60 0
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
90 2
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
13天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
55 4
|
13天前
|
XML 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(2)
JavaScript 权威指南第七版(GPT 重译)(六)
60 4
JavaScript 权威指南第七版(GPT 重译)(六)(2)
|
13天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(六)(1)
JavaScript 权威指南第七版(GPT 重译)(六)
27 3
JavaScript 权威指南第七版(GPT 重译)(六)(1)
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(4)
JavaScript 权威指南第七版(GPT 重译)(五)
39 9
|
13天前
|
前端开发 JavaScript 程序员
JavaScript 权威指南第七版(GPT 重译)(五)(3)
JavaScript 权威指南第七版(GPT 重译)(五)
36 8