diff --git a/README.md b/README.md
index 3123f95..7d2843f 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@
+ [附录A:PyTorch简介](./cn-Book/附录A.PyTorch简介.md)
+ [附录B:参考文献和进一步阅读](./cn-Book/附录B.参考文献和进一步阅读.md)
+ [附录C:习题解答](./cn-Book/附录C.习题解答.md)
++ [附录D:给训练循环添加高级技巧](./cn-Book/附录D.给训练循环添加高级技巧.md)
diff --git a/cn-Book/附录D.给训练循环添加高级技巧.md b/cn-Book/附录D.给训练循环添加高级技巧.md
index 6eb3d69..be7ab62 100644
--- a/cn-Book/附录D.给训练循环添加高级技巧.md
+++ b/cn-Book/附录D.给训练循环添加高级技巧.md
@@ -1,6 +1,12 @@
-在本附录中,我们将增强第 5-7 章中涵盖的预训练和微调过程的训练函数。特别是前三部分内容,将涵盖学习率预热、余弦衰减和梯度裁剪等高级技巧。
+- [D.1 学习率预热](#d1-学习率预热)
+- [D.2 余弦衰减](#d2-余弦衰减)
+- [D.3 梯度裁剪](#d3-梯度裁剪)
+- [D.4 修改后的训练函数](#d4-修改后的训练函数)
-最后一部分将这些技巧整合到第 5 章中开发的训练函数中,并预训练一个大语言模型 (LLM)。
+-----
+在本附录中,我们将增强第 5-7 章中介绍过的预训练和微调过程的训练函数。特别是前三部分内容,将涵盖学习率预热、余弦衰减和梯度裁剪等高级技巧。
+
+最后一部分将这些技巧整合到在第 5 章开发的训练函数中,并预训练一个大语言模型 (LLM)。
为使本附录中的代码自成一体,我们重新初始化了在第5章中训练的模型。
@@ -138,7 +144,7 @@ plt.show()
另一种广泛应用于训练复杂深度神经网络和 LLM 的技术是余弦衰减。此方法在整个训练周期中调整学习率,使其在预热阶段后遵循余弦曲线。
-在其流行的变体中,余弦衰减将学习率降低(或衰减)至接近于零,模仿半个余弦周期的轨迹。余弦衰减中学习率的逐渐降低旨在减缓模型更新其权重的速度。这尤其重要,因为它有助于最大限度地降低在训练过程中越过损失最小值的风险,这对于确保训练在其后期阶段的稳定性至关重要。
+在其流行的变体中,余弦衰减将学习率降低(或衰减)至接近于零,模仿半个余弦周期的轨迹。余弦衰减中学习率的逐渐降低旨在减缓模型更新其权重的速度。这一点非常重要,因为它有助于最大限度地降低在训练过程中越过最小损失值的风险,这对于确保训练在其后期阶段的稳定性至关重要。
我们可以修改上一节中的训练循环模板,通过以下方式添加余弦衰减:
@@ -178,6 +184,8 @@ plt.show()
学习率曲线如图 D.2 所示。
+
+
如图 D.2 所示,学习率以线性预热阶段开始,在前 20 步内增加,直到在 20 步后达到最大值。在 20 步线性预热之后,余弦衰减开始起作用,逐渐降低学习率,直到达到最小值。
@@ -199,6 +207,175 @@ $$G=\left[\begin{array}{ll}
2 & 4
\end{array}\right]$$
-如果我们旨在将这些梯度裁剪到最大范数 1,我们首先计算这些梯度的 L2 范数,即为:
+如果我们的目的是将这些梯度裁剪到最大范数 1,可以首先计算这些梯度的 L2 范数,即为:
+
+$$|G|_{2}=\sqrt{1^{2}+2^{2}+2^{2}+4^{2}}=\sqrt{25}=5$$
+
+鉴于 |G|2 = 5 超过了我们的最大范数 1,我们需缩小梯度以确保它们的范数恰好等于 1。这是通过一个缩放因子实现的,该因子计算为 max_norm/|G|2 = 1/5。因此,调整后的梯度矩阵 G' 变为:
+
+$$G^{\prime}=\frac{1}{5} \times G\left[\begin{array}{ll}
+1 / 1 & 2 / 5 \\
+2 / 5 & 4 / 5
+\end{array}\right\rceil$$
+
+为了演示这个梯度裁剪过程,我们将首先初始化一个新模型并计算一个训练批次的损失,类似于标准训练循环中的过程:
+
+```python
+from previous_chapters import calc_loss_batch
+torch.manual_seed(123)
+model = GPTModel(GPT_CONFIG_124M)
+loss = calc_loss_batch(input_batch, target_batch, model, device)
+loss.backward()
+```
+
+在调用前面代码片段中的 `.backward()` 方法后,PyTorch 计算损失梯度并将它们存储在每个模型权重(参数)张量的 `.grad` 属性中。
+
+为了便于演示,我们可以定义以下 `find_highest_gradient` 函数,在调用 `.backward()` 之后,通过该函数扫描模型权重张量的所有 `.grad` 属性来识别最高的梯度值:
+
+```python
+def find_highest_gradient(model):
+ max_grad = None
+ for param in model.parameters():
+ if param.grad is not None:
+ grad_values = param.grad.data.flatten()
+ max_grad_param = grad_values.max()
+ if max_grad is None or max_grad_param > max_grad:
+ max_grad = max_grad_param
+ return max_grad
+print(find_highest_gradient(model))
+```
+
+以上代码识别出的最大梯度值如下:
+
+```python
+tensor(0.0373)
+```
+
+现在让我们应用梯度裁剪,它可以通过一行代码来实现,并观察它如何影响最大的梯度值:
+
+```python
+torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
+print(find_highest_gradient(model))
+```
+
+在应用最大范数为 1 的梯度裁剪之后,最大的梯度值比之前小得多:
+
+```python
+tensor(0.0166)
+```
+
+在下一节中,我们将把本附录中迄今为止涵盖的所有概念付诸实践,并修改 LLM 训练函数。
+
+
+
+## D.4 修改后的训练函数
+
+在本附录的最后一部分,我们通过添加之前介绍的三个概念来改进我们在第 5 章中使用的 `train_model_simple` 训练函数:线性预热、余弦衰减和梯度裁剪。这些方法都有助于稳定 LLM 训练。
+
+代码如下,相对于 `train_model_simple` 的改动已进行注释:
+
+```python
+from previous_chapters import evaluate_model, generate_and_print_sample
+
+def train_model(model, train_loader, val_loader, optimizer, device, n_epochs,
+ eval_freq, eval_iter, start_context, warmup_steps=10,initial_lr=3e-05, min_lr=1e-6):
+ train_losses, val_losses, track_tokens_seen, track_lrs = [], [], [], []
+ tokens_seen, global_step = 0, -1
+
+ peak_lr = optimizer.param_groups[0]["lr"] #A
+ total_training_steps = len(train_loader) * n_epochs #B
+ lr_increment = (peak_lr - initial_lr) / warmup_steps #C
+
+ for epoch in range(n_epochs):
+ model.train()
+ for input_batch, target_batch in train_loader:
+ optimizer.zero_grad()
+ global_step += 1
+
+ if global_step < warmup_steps: #D
+ lr = initial_lr + global_step * lr_increment
+ else:
+ progress = ((global_step - warmup_steps) /
+ (total_training_steps - warmup_steps))
+ lr = min_lr + (peak_lr - min_lr) * 0.5 * (
+ 1 + math.cos(math.pi * progress))
+
+ for param_group in optimizer.param_groups: #E
+ param_group["lr"] = lr
+ track_lrs.append(lr)
+ loss = calc_loss_batch(input_batch, target_batch, model, device)
+ loss.backward()
+
+ if global_step > warmup_steps: #F
+ torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) #G
+
+ optimizer.step()
+ tokens_seen += input_batch.numel()
+
+ if global_step % eval_freq == 0:
+ train_loss, val_loss = evaluate_model(
+ model, train_loader, val_loader,
+ device, eval_iter
+ )
+ train_losses.append(train_loss)
+ val_losses.append(val_loss)
+ track_tokens_seen.append(tokens_seen)
+ print(f"Ep {epoch+1} (Iter {global_step:06d}): "
+ f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
+
+ generate_and_print_sample(
+ model, train_loader.dataset.tokenizer,
+ device, start_context
+ )
+
+return train_losses, val_losses, track_tokens_seen, track_lrs
+
+
+#A 从优化器检索初始学习率,假设我们将其用作峰值学习率
+#B 计算训练过程中的总迭代次数
+#C 计算预热阶段的学习率增量
+#D 根据当前阶段(预热或余弦退火)调整学习率
+#E 将计算出的学习率应用于优化器
+#F 在预热阶段后应用梯度裁剪以避免梯度爆炸
+#G 与第 5 章中使用的 train_model_simple 函数相比,此行以下的所有内容保持不变
+```
+
+在定义了 `train_model` 函数之后,我们可以使用它来训练模型,用法与第 5 章中的 `train_model_simple` 方法类似:
+
+```python
+torch.manual_seed(123)
+model = GPTModel(GPT_CONFIG_124M)
+model.to(device)
+peak_lr = 5e-4
+optimizer = torch.optim.AdamW(model.parameters(), weight_decay=0.1)
+
+n_epochs = 15
+train_losses, val_losses, tokens_seen, lrs = train_model(
+ model, train_loader, val_loader, optimizer, device, n_epochs=n_epochs,
+ eval_freq=5, eval_iter=1, start_context="Every effort moves you",
+ warmup_steps=10, initial_lr=1e-5, min_lr=1e-5
+)
+```
+
+在 MacBook Air 或类似的笔记本电脑上,训练大约需要 5 分钟才能完成,并打印以下输出:
+
+```python
+Ep 1 (Iter 000000): Train loss 10.934, Val loss 10.939
+Ep 1 (Iter 000005): Train loss 8.529, Val loss 8.843
+Every effort moves you,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+Ep 2 (Iter 000010): Train loss 6.400, Val loss 6.825
+Ep 2 (Iter 000015): Train loss 6.116, Val loss 6.861
+Every effort moves you,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+...
+the irony. She wanted him vindicated--and by me!" He laughed again, and threw back his
+head to look up at the sketch of the donkey. "There were days when I
+Ep 15 (Iter 000130): Train loss 0.101, Val loss 6.707
+Every effort moves you?" "Yes--quite insensible to the irony. She wanted him
+vindicated--and by me!" He laughed again, and threw back his head to look up at the
+sketch of the donkey. "There were days when I
+```
+
+与第 5 章类似,由于数据集非常小,并且我们对其进行了多次迭代,因此模型在几个 epoch 后开始过拟合。然而,我们可以看到该函数正在工作,因为它最小化了训练集损失。
+
+这里鼓励读者在更大的文本数据集上训练模型,并将使用这种更复杂的训练函数获得的结果与第 5 章中使用的 `train_model_simple` 函数获得的结果进行比较。
-$$|G|_{2}=\sqrt{1^{2}+2^{2}+2^{2}+4^{2}}=\sqrt{25}=5$$
\ No newline at end of file