add fifth chapter

This commit is contained in:
skindhu 2024-11-09 19:27:15 +08:00
parent fad1f3f200
commit 70023771d1
6 changed files with 549 additions and 78 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
Image/figure5.17.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

View File

@ -6,12 +6,33 @@
+ **从OpenAI加载预训练权重** + **从OpenAI加载预训练权重**
-----
- [5.1 生成式文本模型的评估](#51-生成式文本模型的评估)
- [5.1.1 使用 GPT 生成文本](#511-使用-gpt-生成文本)
- [5.1.2 文本生成损失的计算](#512-文本生成损失的计算)
- [5.1.3 计算训练集和验证集的损失](#513-计算训练集和验证集的损失)
- [5.2 训练 LLM](#52-训练-llm)
- [5.3 通过解码策略控制生成结果的随机性](#53-通过解码策略控制生成结果的随机性)
- [5.3.1 Temperature scaling](#531-temperature-scaling)
- [5.3.2 Top-k 采样](#532-top-k-采样)
- [5.3.3 对文本生成函数进行调整](#533-对文本生成函数进行调整)
- [5.4 在 PyTorch 中加载和保存模型权重](#54-在-pytorch-中加载和保存模型权重)
- [5.5 从 OpenAI 加载预训练权重](#55-从-openai-加载预训练权重)
- [5.6 总结](#56-总结)
-----
在之前的章节中,我们实现了数据采样、注意力机制,并编写了 LLM 的架构。本章的核心是实现训练函数并对 LLM 进行预训练,详见图 5.1。 在之前的章节中,我们实现了数据采样、注意力机制,并编写了 LLM 的架构。本章的核心是实现训练函数并对 LLM 进行预训练,详见图 5.1。
<img src="../Image/chapter5/figure5.1.png" width="75%" /> <img src="../Image/chapter5/figure5.1.png" width="75%" />
如图5.1所示,我们还将学习基本的模型评估技术,以衡量生成文本的质量,这是在训练过程中优化大语言模型的必要条件。此外,我们将讨论如何加载预训练权重,以便为接下来的微调提供坚实的基础。 如图5.1所示,我们将继续学习基本的模型评估技术,以衡量生成文本的质量,这对于在训练过程中优化 LLM 是非常必要的。此外,我们将讨论如何加载预训练权重,以便为接下来的微调提供坚实的基础。
> [!NOTE] > [!NOTE]
> >
@ -23,7 +44,7 @@
## 5.1 生成式文本模型的评估 ## 5.1 生成式文本模型的评估
本章开篇,我们将基于上一章的代码设置 LLM 进行文本生成,并讨论如何对生成文本质量进行评估的基本方法。我们在本节以及本章剩余部分讨论的内容已在图5.2中概述 本章开篇,我们将基于上一章的代码设置 LLM 进行文本生成,并讨论如何对生成文本质量进行评估的基本方法。而本章剩余部分的内容请参考图5.2
<img src="../Image/chapter5/figure5.2.png" width="75%" /> <img src="../Image/chapter5/figure5.2.png" width="75%" />
@ -33,7 +54,7 @@
### 5.1.1 使用 GPT 生成文本 ### 5.1.1 使用 GPT 生成文本
在本节中,我们会先通过对 LLM 的设置简要回顾一下第四章中实现的文本生成过程。在开始这项工作之前,我们首先使用第 4 章中的 GPTModel 类和 GPT_CONFIG_124M 配置字典初始化 GPT 模型,在本章的后续章节会对其进行评估和训练。 在本节中,我们会先通过对 LLM 的设置简要回顾一下第四章中实现的文本生成过程。在开始这项工作之前,我们首先使用第 4 章中的 GPTModel 类和 GPT_CONFIG_124M 配置字典初始化 GPT 模型,以便在后续章节对其进行评估和训练:
```python ```python
import torch import torch
@ -55,17 +76,15 @@ model.eval()
#B 将 dropout 设置为 0 是一种常见的做法 #B 将 dropout 设置为 0 是一种常见的做法
``` ```
在 GPT_CONFIG_124M 配置字典中我们唯一的调整是将上下文长度context_length减少到 256 个 token。此项调整降低了模型训练的计算需求使得可以在普通笔记本电脑上进行训练。 之前定义的 GPT_CONFIG_124M 配置字典中我们唯一的调整是将上下文长度context_length减少到 256 个 token。此项调整降低了模型训练的计算需求使得可以在普通笔记本电脑上进行训练。
参数量为 1.24 亿的 GPT-2 模型最初被配置为可处理最多 1024 个 token。本章结束时我们将更新上下文大小设置并加载预训练权重使模型能够支持 1024-token 的上下文长度。 参数量为 1.24 亿的 GPT-2 模型最初被配置为可处理最多 1024 个 token。本章结束时我们将更新上下文大小设置并加载预训练权重使模型能够支持 1024-token 的上下文长度。
在 GPT 模型实例中,我们使用了上一章介绍的 `generate_text_simple` 函数,并引入了两个实用函数 `text_to_token_ids``token_ids_to_text`,用于在文本和 token 表示之间进行转换。这是本章将会用到的一个技术。图 5.3 展示了这个过程,以便更清楚地说明。 我们通过前一章节中介绍的 generate_text_simple 函数来使用 GPTmodel 实例同时引入了两个实用函数text_to_token_ids 和token_ids_to_text。这些函数简化了文本与 token 表示之间的转换,本章中我们将多次使用这种技术。图 5.3 可以帮助我们更清楚地理解这一过程。
我们通过前一章节中介绍的 generate_text_simple 函数来使用 GPTmodel 实例同时引入了两个实用函数text_to_token_ids 和token_ids_to_text。这些函数简化了文本与 token 表示之间的转换,本章中我们将多次使用这种技术。为了更清楚地理解这一过程,图 5.3 展示了这一流程,在深入代码之前,我们先通过图示帮助理解。
<img src="../Image/chapter5/figure5.3.png" width="75%" /> <img src="../Image/chapter5/figure5.3.png" width="75%" />
图 5.3 展示了使用 GPT 模型生成文本的三个主要步骤。首先,分词器将输入文本转换为一系列 token ID在第 2 章中已有讨论)。然后,模型接收这些 token ID 并生成对应的 logits即词汇表中每个 token 的概率分布,具体见第 4 章)。最后,将 logits 转换回 token ID分词器将其解码为人类可读的文本,完成从文本输入到文本输出的循环。 图 5.3 展示了使用 GPT 模型生成文本的三个主要步骤。首先,分词器将输入文本转换为一系列 token ID在第 2 章中已有讨论)。然后,模型接收这些 token ID 并生成对应的 logits即词汇表中每个 token 的概率分布,具体见第 4 章)。最后,将 logits 转换回 token ID分词器将其解码为可读的文本完成从文本输入到文本输出的循环。
我们通过代码来实现上述过程: 我们通过代码来实现上述过程:
@ -102,7 +121,7 @@ Output text:
Every effort moves you rentingetic wasnم refres RexMeCHicular stren Every effort moves you rentingetic wasnم refres RexMeCHicular stren
``` ```
从输出可以看出,模型尚未生成连贯的文本,因为它还没有经过训练。为了定义文本的‘连贯性’或‘高质量’,我们需要实现一种数值方法来评估生成的内容。这一方法将帮助我们在训练过程中监并提升模型的性能。 从输出可以看出,模型尚未生成连贯的文本,因为它还没有经过训练。为了定义文本的‘连贯性’或‘高质量’,我们需要实现一种数值方法来评估生成的内容。这一方法将帮助我们在训练过程中监并提升模型的性能。
接下来将介绍如何计算生成内容的损失度量,该损失值会作为训练进展和效果的指示器。此外,在后续关于微调 LLM 的章节中,我们将探讨更多评估模型质量的方法。 接下来将介绍如何计算生成内容的损失度量,该损失值会作为训练进展和效果的指示器。此外,在后续关于微调 LLM 的章节中,我们将探讨更多评估模型质量的方法。
@ -110,7 +129,7 @@ Output text:
### 5.1.2 文本生成损失的计算 ### 5.1.2 文本生成损失的计算
本节将探讨如何通过计算‘文本生成损失’来数值化评估训练过程中生成的文本质量。我们将通过一个实际示例逐步讲解这一主题,首先简要回顾第 2 章的数据加载方式以及第 4 章的`generate_text_simple`函数如何生成文本。 本节将探讨如何通过计算‘文本生成损失’来数值化评估训练过程中生成的文本质量。在通过一个实际示例逐步讲解这一主题之前,先简要回顾第 2 章的数据加载方式以及第 4 章的`generate_text_simple`函数如何生成文本。
图 5.4 展示了从输入文本到 LLM 生成文本的整体流程,该流程通过五个步骤实现。 图 5.4 展示了从输入文本到 LLM 生成文本的整体流程,该流程通过五个步骤实现。
@ -118,24 +137,23 @@ Output text:
图 5.4 展示了第 4 章中`generate_text_simple`函数内部的本生成过程。在后续章节中计算生成文本的质量损失之前,我们需要先执行这些初始步骤。 图 5.4 展示了第 4 章中`generate_text_simple`函数内部的本生成过程。在后续章节中计算生成文本的质量损失之前,我们需要先执行这些初始步骤。
在图 5.4 展示的文本生成过程中,为了便于在一页中展示图像,我们使用了包含 7 个 token 的小型词汇表。然而GPTModel 实际上使用了包含 50,257 个的大型词汇表因此在接下来的代码中token ID 的范围为 0 到 50,256而不是图示中的 0 到 6。 为了便于在一页中展示图像,我们图中的示例仅使用了包含 7 个 token 的小型词汇表。然而GPTModel 实际上使用了包含 50,257 个 token 的大型词汇表因此在接下来的代码中token ID 的范围为 0 到 50,256而不是图示中的 0 到 6。
图 5.4 为了简洁仅展示了一个文本示例 'every effort moves'。在接下来的代码示例中,我们将实现图 5.4 中的步骤,并使用两个输入示例 'every effort moves' 和 'I really like' 作为 GPT 模型的输入。 此外,图 5.4 为了简洁仅展示了一个文本示例 'every effort moves'。在接下来的代码示例中,我们将实现图 5.4 中的步骤,并使用两个输入示例 'every effort moves' 和 'I really like' 作为 GPT 模型的输入。
考虑两个输入样本,它们已经被转换为 token ID对应图 5.4 中的步骤 1 考虑两个输入样本,它们已经被转换为 token ID对应图 5.4 中的步骤 1
```python ```python
inputs = torch.tensor([[16833, 3626, 6100], # ["every effort moves", inputs = torch.tensor([[16833, 3626, 6100], # ["every effort moves",
[40, 1107, 588]]) # "I really like"] [40, 1107, 588]]) # "I really like"]
Matching these inputs, the `targets` contain the token IDs we aim for the model to # Matching these inputs, the `targets` contain the token IDs we aim for the model to produce:
produce:
targets = torch.tensor([[3626, 6100, 345 ], # [" effort moves you", targets = torch.tensor([[3626, 6100, 345 ], # [" effort moves you",
[107, 588, 11311]]) # " really like chocolate"] [107, 588, 11311]]) # " really like chocolate"]
``` ```
需要注意的是,目标值是输入数据,但向前偏移了一个位置。我们在第 2 章实现数据加载器时已介绍过这一概念。这种偏移策略对于教会模型预测序列中的下一个 token 至关重要。 需要注意的是,目标值中展示的是输入数据向前偏移了一个位置。我们在第 2 章实现数据加载器时已介绍过这一概念。这种偏移策略对于教会模型预测序列中的下一个 token 至关重要。
当我们将两个输入示例(每个包含三个 token输入模型以计算它们的 logit 向量后,再应用 Softmax 函数将这些 logit 值转换为概率分,这对应于图 5.4 中的步骤 2 接着我们将两个输入示例(每个示例样本包含三个 token输入模型以计算它们的 logit 向量,再应用 Softmax 函数将这些 logit 值转换为概率分,这对应于图 5.4 中的步骤 2
```python ```python
with torch.no_grad(): #A with torch.no_grad(): #A
@ -154,7 +172,7 @@ torch.Size([2, 3, 50257])
第一个数字 2 表示输入中的两个样本(行),即批次大小。第二个数字 3 表示每个样本包含的 token 数量。最后一个数字表示嵌入维度的大小,通常由词汇表大小决定,前面章节已讨论。 第一个数字 2 表示输入中的两个样本(行),即批次大小。第二个数字 3 表示每个样本包含的 token 数量。最后一个数字表示嵌入维度的大小,通常由词汇表大小决定,前面章节已讨论。
通过 softmax 函数将 logits 转换为概率后,第 4 章的 generate_text_simple 函数会将概率分进一步转换回文本,这一过程在图 5.4 的步骤 3 到步骤 5 中进行了展示。 通过 softmax 函数将 logits 转换为概率后,第 4 章的 generate_text_simple 函数会将概率分进一步转换回文本,这一过程在图 5.4 的步骤 3 到步骤 5 中进行了展示。
接下来,通过对概率得分应用 `argmax` 函数,可以得到对应的 token ID实现步骤 3 和 步骤 4 接下来,通过对概率得分应用 `argmax` 函数,可以得到对应的 token ID实现步骤 3 和 步骤 4
@ -163,13 +181,14 @@ token_ids = torch.argmax(probas, dim=-1, keepdim=True)
print("Token IDs:\n", token_ids) print("Token IDs:\n", token_ids)
``` ```
假设我们有 2 个输入样本,每个样本包含 3 个 token。在对概率分应用 argmax 函数后(对应图 5.4 的第 3 步),会得到 2 组输出,每组包含 3 个预测的 token ID 假设我们有 2 个输入样本,每个样本包含 3 个 token。在对概率分应用 argmax 函数后(对应图 5.4 的第 3 步),会得到 2 组输出,每组包含 3 个预测的 token ID
```python ```python
Token IDs: Token IDs:
tensor([[[16657], # First batch tensor([[[16657], # First batch
[ 339], [ 339],
[42826]], [42826]],
[[49906], # Second batch [[49906], # Second batch
[29669], [29669],
[41751]]]) [41751]]])
@ -180,19 +199,18 @@ tensor([[[16657], # First batch
```python ```python
print(f"Targets batch 1: {token_ids_to_text(targets[0], tokenizer)}") print(f"Targets batch 1: {token_ids_to_text(targets[0], tokenizer)}")
print(f"Outputs batch 1: {token_ids_to_text(token_ids[0].flatten(), tokenizer)}") print(f"Outputs batch 1: {token_ids_to_text(token_ids[0].flatten(), tokenizer)}")
When we decode these tokens, we find that these output tokens are quite different from #When we decode these tokens, we find that these output tokens are quite different from the target tokens we want the model to generate:
the target tokens we want the model to generate:
Targets batch 1: effort moves you Targets batch 1: effort moves you
Outputs batch 1: Armed heNetflix Outputs batch 1: Armed heNetflix
``` ```
模型生成的文本与目标文本不同,因为它尚未经过训练。接下来,我们将通过‘损失’来数值化评估模型生成文本的质量(详见图 5.4)。这不仅有助于衡量生成文本的质量,还为实现训练函数提供了基础,训练函数主要通过更新模型权重来改善生成文本的质量。 可以看到,模型生成的文本与目标文本不同,因为它尚未经过训练。接下来,我们将通过‘损失’来数值化评估模型生成文本的质量(详见图 5.5)。这不仅有助于衡量生成文本的质量,还为实现训练函数提供了基础,训练函数主要通过更新模型权重来改善生成文本的质量。
<img src="../Image/chapter5/figure5.5.png" width="75%" /> <img src="../Image/chapter5/figure5.5.png" width="75%" />
文本评估过程的一部分(如图 5.5 所示)是衡量生成的 token 与正确预测目标之间的差距。本章后面实现的训练函数将利用这些信息来调整模型权重,使生成的文本更接近(或理想情况下完全匹配)目标文本。 文本评估过程的一部分(如图 5.5 所示)是衡量生成的 token 与正确预测目标之间的差距。本章后面实现的训练函数将利用这些信息来调整模型权重,使生成的文本更接近(或理想情况下完全匹配)目标文本。
模型训练的目标是提高正确目标 token ID 所在位置的 softmax 概率,如图 5.6 所示。接下来的部分中,我们还会将该 softmax 概率作为评价指标,用于对模型生成的输出进行数值化评估:正确位置上的概率越高,模型效果越好。 换句话说,模型训练的目标是提高正确目标 token ID 所在位置的 softmax 概率,如图 5.6 所示。接下来的部分中,我们还会将该 softmax 概率作为评价指标,用于对模型生成的输出进行数值化评估:正确位置上的概率越高,模型效果越好。
<img src="../Image/chapter5/figure5.6.png" width="75%" /> <img src="../Image/chapter5/figure5.6.png" width="75%" />
@ -219,7 +237,7 @@ Text 1: tensor([7.4541e-05, 3.1061e-05, 1.1563e-05])
Text 2: tensor([1.0337e-05, 5.6776e-05, 4.7559e-06]) Text 2: tensor([1.0337e-05, 5.6776e-05, 4.7559e-06])
``` ```
训练 LLM 的目标是最大化这些概率值,使其尽量接近 1。这样可以确保 LLM 始终选择目标 token —— 即句中的下一个词,作为生成的下一个 token。 训练 LLM 的目标是最大化这些概率值,使其尽量接近 1。这样可以确保 LLM 始终选择目标 token —— 即句中的下一个词,作为生成的下一个 token。
> [!NOTE] > [!NOTE]
> >
@ -229,7 +247,7 @@ Text 2: tensor([1.0337e-05, 5.6776e-05, 4.7559e-06])
> >
> 反向传播需要一个损失函数,该函数用于计算模型预测输出与实际目标输出之间的差异(此处指与目标 token ID 对应的概率)。这个损失函数用于衡量模型预测与目标值的偏差程度。 > 反向传播需要一个损失函数,该函数用于计算模型预测输出与实际目标输出之间的差异(此处指与目标 token ID 对应的概率)。这个损失函数用于衡量模型预测与目标值的偏差程度。
在本节接下来的部分中,我们将针对`target_probas_1`和`target_probas_2`的概率得分计算损失。图 5.7 展示了主要步骤。 在本节剩余内容中,我们将针对`target_probas_1`和`target_probas_2`的概率得分计算损失。图 5.7 展示了主要步骤。
<img src="../Image/chapter5/figure5.7.png" width="75%" /> <img src="../Image/chapter5/figure5.7.png" width="75%" />
@ -250,13 +268,13 @@ tensor([ -9.5042, -10.3796, -11.3677, -11.4798, -9.7764, -12.2561])
> [!TIP] > [!TIP]
> >
> **个人思考:** 在继续接下的计算之前,我们首先来探讨一下,对数在损失函数的应用中到底有什么作用。 > **个人思考:** 在继续接下的计算之前,我们首先来探讨一下,对数在损失函数的应用中到底有什么作用。
> >
> 1. **为什么要用概率的对数** > 1. **为什么要用概率的对数**
> >
> 在 LLM 中概率得分通常是小于1的数例如0.1、0.05等直接用这些数进行计算和优化可能会面临一些问题。比如如果多个概率相乘结果会变得非常小甚至接近0。这种情况称为“数值下溢”Numerical Underflow可能导致计算不稳定。 > 在 LLM 中概率得分通常是小于1的数例如0.1、0.05等直接用这些数进行计算和优化可能会面临一些问题。比如如果多个概率相乘结果会变得非常小甚至接近0。这种情况称为“数值下溢”Numerical Underflow可能导致计算不稳定。
> >
> 我们有三个概率值分别为0.2、0.1和0.05。如果我们计算这些值的乘积,结果是: > 假设我们有三个概率值分别为0.2、0.1和0.05。如果我们计算这些值的乘积,结果是:
> >
> $$ 0.2×0.1×0.05=0.001 $$ > $$ 0.2×0.1×0.05=0.001 $$
> >
@ -301,7 +319,7 @@ print(avg_log_probas)
tensor(-10.7940) tensor(-10.7940)
``` ```
训练的目标是通过更新模型权重,使平均对数概率尽可能接近 0,这将在 5.2 节中实现 训练的目标是通过更新模型权重,使平均对数概率尽可能接近 0(将在 5.2 节中实现)
然而,在深度学习中,常见做法并不是直接将平均对数概率推向 0而是通过将负平均对数概率降低至 0 来实现。负平均对数概率就是平均对数概率乘以 -1这与图 5.7 的第 6 步相对应: 然而,在深度学习中,常见做法并不是直接将平均对数概率推向 0而是通过将负平均对数概率降低至 0 来实现。负平均对数概率就是平均对数概率乘以 -1这与图 5.7 的第 6 步相对应:
@ -329,12 +347,12 @@ print(neg_avg_log_probas)
```python ```python
print("Logits shape:", logits.shape) print("Logits shape:", logits.shape)
print("Targets shape:", targets.shape) print("Targets shape:", targets.shape)
The resulting shapes are as follows: # The resulting shapes are as follows:
Logits shape: torch.Size([2, 3, 50257]) Logits shape: torch.Size([2, 3, 50257])
Targets shape: torch.Size([2, 3]) Targets shape: torch.Size([2, 3])
``` ```
可以看到logits 张量是三维的批量大小、token 数量和词汇表大小。而 targets 张量是二维的:批量大小和 token 数量 可以看到logits 是个三维张量批量大小、token 数量和词汇表大小)。而 targets 是个二维张量(批量大小和 token 数量)
在 PyTorch 中使用交叉熵损失函数时,我们需要将这些张量展平,以便在批量维度上进行合并: 在 PyTorch 中使用交叉熵损失函数时,我们需要将这些张量展平,以便在批量维度上进行合并:
@ -377,7 +395,7 @@ tensor(10.7940)
> >
> `Perplexity`可通过 `perplexity = torch.exp(loss)` 计算,对先前计算的损失值应用此公式将返回 `tensor(48725.8203)` > `Perplexity`可通过 `perplexity = torch.exp(loss)` 计算,对先前计算的损失值应用此公式将返回 `tensor(48725.8203)`
> >
> `Perplexity`通常比原始损失值更具可解释性,因为它表示了模型在每一步生成中,对有效词汇量的不确定程度。在这个例子中,困惑度可以理解为模型在词汇表中的 47,678 个单词或 token 中,不确定该选择哪个作为下一个生成的 token。 > `Perplexity`通常比原始损失值更具可解释性,因为它表示了模型在每一步生成中,对有效词汇量的不确定程度。在这个例子中,`Perplexity`可以理解为模型在词汇表中的 47,678 个单词或 token 中,不确定该选择哪个作为下一个生成的 token。
在本节中,我们对两个小文本输入进行了损失计算,以便更直观地说明损失函数的计算过程。下一节将把损失计算应用于整个训练集和验证集。 在本节中,我们对两个小文本输入进行了损失计算,以便更直观地说明损失函数的计算过程。下一节将把损失计算应用于整个训练集和验证集。
@ -429,13 +447,13 @@ Tokens: 5145
<img src="../Image/chapter5/figure5.9.png" width="75%" /> <img src="../Image/chapter5/figure5.9.png" width="75%" />
出于可视化的需要,图 5.9 将最大长度设置为 6。然而在实际数据加载器中我们将最大长度设置为 LLM 支持的 256 个 token 的上下文长度,使得模型在训练时可以看到更长的文本。 出于可视化的需要,图 5.9 将最大长度设置为 6。然而在实际数据加载器中我们将最大长度设置为 LLM 支持的 256 个 token 的上下文长度,使得模型在训练时可以看到更长的文本。
> [!NOTE] > [!NOTE]
> >
> **处理变长输入的训练** > **处理变长输入的训练**
> >
> 在训练模型时,我们使用大小相似的数据块来保证训练过程的简便和高效。然而,在实践中,使用变长的输入进行训练有助于提升 LLM 的泛化能力,使其在应用时能够适应不同类型的输入。 > 在训练模型时,我们可以使用大小相似的数据块来保证训练过程的简便和高效。然而,在实践中,使用变长的输入进行训练往往有助于提升 LLM 的泛化能力,使其在应用时能够适应不同类型的输入。
为了实现图 5.9 中的数据划分与加载,我们首先定义一个 `train_ratio`,用于将 90% 的数据用于训练,剩余 10% 用于在训练期间进行模型评估: 为了实现图 5.9 中的数据划分与加载,我们首先定义一个 `train_ratio`,用于将 90% 的数据用于训练,剩余 10% 用于在训练期间进行模型评估:
@ -473,7 +491,7 @@ val_loader = create_dataloader_v1(
) )
``` ```
在前面的代码示例中,由于数据集较小,我们使用了较小的批量大小以降低计算资源的消耗。实际训练 LLM 时,批量大小达到 1,024 或更高并不少见。 在前面的代码示例中,由于数据集较小,我们使用了较小的批量以降低计算资源的消耗。实际训练 LLM 时,批量大小达到 1,024 或更高并不少见。
为了确认数据加载器是否正确创建,可以通过遍历这些数据加载器来检查: 为了确认数据加载器是否正确创建,可以通过遍历这些数据加载器来检查:
@ -505,9 +523,9 @@ Validation loader:
torch.Size([2, 256]) torch.Size([2, 256]) torch.Size([2, 256]) torch.Size([2, 256])
``` ```
可以看到,训练集中共有 9 个批,每批包含 2 个样本,每个样本有 256 个 token。由于只分配了 10% 的数据用于验证,因此验证集中只有 1 个批,包含 2 个样本。 可以看到,训练集中共有 9 个批,每批包含 2 个样本,每个样本有 256 个 token。由于只分配了 10% 的数据用于验证,因此验证集中只有 1 个批,包含 2 个样本。
和我们的预期一致输入数据x和目标数据y的形状相同即批大小 × 每批的 token 数量),因为目标数据是将输入数据整体向后偏移一个位置得到的,正如第 2 章讨论的那样。 和我们的预期一致输入数据x和目标数据y的形状相同即批大小 × 每批的 token 数量),因为目标数据是将输入数据整体向后偏移一个位置得到的,正如第 2 章讨论的那样。
接下来我们实现一个工具函数,用于计算由训练和验证加载器返回的批量数据的交叉熵损失: 接下来我们实现一个工具函数,用于计算由训练和验证加载器返回的批量数据的交叉熵损失:
@ -723,7 +741,7 @@ over from Monte Carlo; and Mrs. Gis
根据训练过程中的输出结果,训练损失显著下降,从 9.558 降到 0.762模型的语言能力大幅提升。在训练初期模型仅能在起始上下文后添加逗号如“Every effort moves you,,,,,,,,,,,,”或重复单词“and”。而在训练结束时模型能够生成符合语法的文本。 根据训练过程中的输出结果,训练损失显著下降,从 9.558 降到 0.762模型的语言能力大幅提升。在训练初期模型仅能在起始上下文后添加逗号如“Every effort moves you,,,,,,,,,,,,”或重复单词“and”。而在训练结束时模型能够生成符合语法的文本。
训练集损失类似我们可以看到验证集损失在开始时较高9.856),随后在训练过程中下降。但它始终未能像训练集损失那样低,在第 10 个 epoch 后保持在 6.372。 训练集损失类似我们可以看到验证集损失在开始时较高9.856),随后在训练过程中下降。但它始终未能像训练集损失那样低,在第 10 个 epoch 后保持在 6.372。
在更详细地讨论验证集损失之前,我们先创建一个简单的图表,将训练集和验证集损失并排展示: 在更详细地讨论验证集损失之前,我们先创建一个简单的图表,将训练集和验证集损失并排展示:
@ -759,7 +777,7 @@ plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
> [!TIP] > [!TIP]
> >
> **个人思考:** 让我们基于 LLM 的原理来探讨下为什么在一个较小的数据集上进行多轮训练,容易产生过拟合的现象? > **个人思考:** 让我们基于 LLM 的原理来探讨下为什么在一个较小的数据集上进行多轮训练,容易产生过拟合的现象?
> >
> 1. **模型容量与数据集大小的匹配问题** > 1. **模型容量与数据集大小的匹配问题**
> + 大语言模型具有极高的参数容量,通常包含数百万甚至数十亿个参数。如此巨大的参数空间可以高度灵活地适应数据,使得模型能够“记住”每个样本的具体特征 > + 大语言模型具有极高的参数容量,通常包含数百万甚至数十亿个参数。如此巨大的参数空间可以高度灵活地适应数据,使得模型能够“记住”每个样本的具体特征
@ -788,7 +806,7 @@ plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
## 5.3 通过解码策略控制生成结果的随机性 ## 5.3 通过解码策略控制生成结果的随机性
本节将介绍文本生成策略(也称为解码策略),用于生成更具原创性的文本。首先,我们将简要回顾前一章中的`generate_text_simple`函数,该函数已在本章前面用于生成和打印样本。然后,我们会讲解两种改进方法:`temperature scaling`和 `top-k sampling`。 本节将介绍文本生成策略(也称为解码策略),用于生成更具原创性的文本。首先,我们将简要回顾前一章中的`generate_text_simple`函数,该函数已在本章前面用于生成和打印样本。然后,我们会讲解两种改进方法:`temperature scaling`和 `top-k 采样`。
首先,我们将模型从 GPU 转移回 CPU因为相对较小的模型在推理时不需要使用 GPU。另外在训练结束后我们会将模型切换到评估模式以关闭 dropout 等随机组件: 首先,我们将模型从 GPU 转移回 CPU因为相对较小的模型在推理时不需要使用 GPU。另外在训练结束后我们会将模型切换到评估模式以关闭 dropout 等随机组件:
@ -802,10 +820,10 @@ model.eval()
```python ```python
tokenizer = tiktoken.get_encoding("gpt2") tokenizer = tiktoken.get_encoding("gpt2")
token_ids = generate_text_simple( token_ids = generate_text_simple(
model=model, model=model,
idx=text_to_token_ids("Every effort moves you", tokenizer), idx=text_to_token_ids("Every effort moves you", tokenizer),
max_new_tokens=25, max_new_tokens=25,
context_size=GPT_CONFIG_124M["context_length"] context_size=GPT_CONFIG_124M["context_length"]
) )
print("Output text:\n", token_ids_to_text(token_ids, tokenizer)) print("Output text:\n", token_ids_to_text(token_ids, tokenizer))
``` ```
@ -855,7 +873,7 @@ next_token_logits = torch.tensor(
) )
``` ```
`generate_text_simple` 函数中,通过 softmax 函数将 logits 转化为概率,并通过 argmax 函数得到生成的 token 的 ID最后通过逆词汇表将其映射回文本可以回顾上一章 接着`generate_text_simple` 函数中,通过 softmax 函数将 logits 转化为概率,并通过 argmax 函数得到生成的 token 的 ID最后通过逆词汇表将其映射回文本可以回顾上一章
```python ```python
probas = torch.softmax(next_token_logits, dim=0) probas = torch.softmax(next_token_logits, dim=0)
@ -907,10 +925,7 @@ print_sampled_tokens(probas)
def softmax_with_temperature(logits, temperature): def softmax_with_temperature(logits, temperature):
scaled_logits = logits / temperature scaled_logits = logits / temperature
return torch.softmax(scaled_logits, dim=0) return torch.softmax(scaled_logits, dim=0)
Temperatures greater than 1 result in more uniformly distributed token probabilities, #Temperatures greater than 1 result in more uniformly distributed token probabilities, and Temperatures smaller than 1 will result in more confident (sharper or more peaky) distributions. Let's illustrate this by plotting the original probabilities alongside probabilities scaled with different temperature values:
and Temperatures smaller than 1 will result in more confident (sharper or more peaky)
distributions. Let's illustrate this by plotting the original probabilities alongside
probabilities scaled with different temperature values:
temperatures = [1, 0.1, 5] #A temperatures = [1, 0.1, 5] #A
scaled_probas = [softmax_with_temperature(next_token_logits, T) for T in temperatures] scaled_probas = [softmax_with_temperature(next_token_logits, T) for T in temperatures]
x = torch.arange(len(vocab)) x = torch.arange(len(vocab))
@ -933,9 +948,9 @@ plt.show()
<img src="../Image/chapter5/figure5.14.png" width="75%" /> <img src="../Image/chapter5/figure5.14.png" width="75%" />
当 temperature 取 1 时logits 在传递给 softmax 函数之前会除以 1计算概率得分。这意味着temperature 为 1 时相当于不进行任何缩放。在这种情况下,模型将根据原始的 softmax 概率,通过 PyTorch 中的多项式采样函数来选择 token。 当 temperature 取 1 时logits 在传递给 softmax 函数之前会除以 1计算概率得分。这意味着temperature 为 1 时相当于不进行任何缩放。在这种情况下,模型将根据原始的 softmax 概率,通过 PyTorch 中的`multinomial`函数来选择 token。
如图 5.14 所示,当 temperature 设置为非常小的值(如 0.1)时,生成的分布会更加尖锐,因此多项式函数几乎总是选择最可能的 token这里是 forward其行为接近 argmax 函数。相反,当 temperature 设置为 5 时,生成的分布更接近均匀分布,其他 token 被选中的频率更高。这种情况下生成的文本多样性增加但也更可能出现无意义的内容。例如temperature 设置为 5 时,模型生成类似 每一份努力都会带你吃披萨 的文本概率大约为 4%。 如图 5.14 所示,当 temperature 设置为非常小的值(如 0.1)时,生成的分布会更加尖锐,因此`multinomial`函数几乎总是选择最可能的 token这里是 forward其行为接近 argmax 函数。相反,当 temperature 设置为 5 时,生成的分布更接近均匀分布,其他 token 被选中的频率更高。这种情况下生成的文本多样性增加但也更可能出现无意义的内容。例如temperature 设置为 5 时,模型生成类似 every effort moves you pizza 的文本概率大约为 4%。
> [!TIP] > [!TIP]
> >
@ -946,7 +961,7 @@ plt.show()
> $$ P\left(x_{i}\right)=\frac{\exp \left(\frac{z_{i}}{T}\right)}{\sum_{j} \exp \left(\frac{z_{j}}{T}\right)} $$ > $$ P\left(x_{i}\right)=\frac{\exp \left(\frac{z_{i}}{T}\right)}{\sum_{j} \exp \left(\frac{z_{j}}{T}\right)} $$
> >
> 1. **当 T>1** > 1. **当 T>1**
> 所有 logits 被除以 T缩放后差异变小。由于 exponentiation 函数的敏感性较高,这意味着 logits 值的差异被“压平”使得最优词的概率降低而其他次优词的概率提高。输出的概率分布变得更加均匀再结合multinomial函数可以使生成结果更加多样化但同时也降低了生成结果的确定性。 > 所有 logits 被除以 T缩放后差异变小。由于 exp 函数的敏感性较高,这意味着 logits 值的差异被“压平”使得最优词的概率降低而其他次优词的概率提高。输出的概率分布变得更加均匀再结合multinomial函数可以使生成结果更加多样化但同时也降低了生成结果的确定性。
> >
> 2. **当 T<1** > 2. **当 T<1**
> >
@ -960,20 +975,476 @@ plt.show()
### 5.3.2 Top-k 采样
在前一节中,我们实现了一种结合`temperature scaling`的概率采样方法来增加生成内容的多样性。我们发现,较高的 temperature 值会使下一词的概率分布更均匀,从而降低模型反复选择最可能词的概率,这样可以生成更多样化的内容,使生成过程探索那些概率较低但可能更有趣和创意的路径。不过,这种方法的一个缺点是,有时会导致生成语法不正确或完全不合逻辑的内容,比如 "every effort moves you pizza"。
在本节中,我们引入了另一种称为`top-k 采样`的概念,当与概率采样和`temperature scaling`结合使用时,可以提升文本生成效果。
在 top-k 采样中,我们可以将采样限制在最有可能的前 k 个 token 内,并通过将其他 token 的概率设为零,将它们排除在选择之外,如图 5.15 所示。
<img src="../Image/chapter5/figure5.15.png" width="75%" />
如图 5.15 所示,将所有未选中的 logits 替换为负无穷(-inf这样在计算 Softmax 时,非 top-k 的 token 的概率为 0剩下的概率之和为 1。细心的读者可能记得我们在第 3 章的因果注意力模块中使用过这种掩码技巧。)
接下来让我们通过代码实现 Figure 5.15 中描述的 top-k 过程,首先选出 logits 值最大的那些 token
```python
top_k = 3
top_logits, top_pos = torch.topk(next_token_logits, top_k)
print("Top logits:", top_logits)
print("Top positions:", top_pos)
```
```python
Top logits: tensor([6.7500, 6.2800, 4.5100])
Top positions: tensor([3, 7, 0])
```
接下来,我们应用 PyTorch 的 where 函数,将非 top-3 的 token 的 logit 值设为负无穷大(-inf
```python
new_logits = torch.where(
condition=next_token_logits < top_logits[-1], #A
input=torch.tensor(float('-inf')), #B
other=next_token_logits #C
)
print(new_logits)
#A 识别出小于 top 3 最小值的 logits
#B 将这些较小的 logits 赋值为负无穷大
#C 保留所有其他 token 的原始 logits
```
执行代码,得到以下用于预测下一个 token 的 logits (在 9 个 token 的词汇表中):
```python
tensor([4.5100, -inf, -inf, 6.7500, -inf, -inf, -inf, 6.2800, -inf])
```
最后,应用 softmax 函数将其转化为下一词的概率分布:
```python
topk_probas = torch.softmax(new_logits, dim=0)
print(topk_probas)
```
可以看到,通过 top-3 方法得到的结果是三个非零的概率得分:
```python
tensor([0.0615, 0.0000, 0.0000, 0.5775, 0.0000, 0.0000, 0.0000, 0.3610, 0.0000])
```
我们现在可以应用`temperature scaling` 和`multinomial`函数来进行概率采样,从这 3 个非零概率得分中选择下一个 token。在下一节中我们将通过修改文本生成函数来实现此操作。
### 5.3.3 对文本生成函数进行调整
前两节介绍了两种增加 LLM 生成文本多样性的概念:`temperature scaling`和`top-k 采样`。本节中,我们将这两个概念整合并加入到之前用于生成文本的`generate_simple`函数中,从而创建一个新的`generate`函数:
```python
# Listing 5.4 A modified text generation function with more diversity
def generate(model, idx, max_new_tokens, context_size,
temperature=1.0, top_k=None, eos_id=None):
for _ in range(max_new_tokens): #A
idx_cond = idx[:, -context_size:]
with torch.no_grad():
logits = model(idx_cond)
logits = logits[:, -1, :]
if top_k is not None: #B
top_logits, _ = torch.topk(logits, top_k)
min_val = top_logits[:, -1]
logits = torch.where(
logits < min_val,
torch.tensor(float('-inf')).to(logits.device),
logits
)
if temperature > 0.0: #C
logits = logits / temperature
probs = torch.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
else: #D
idx_next = torch.argmax(logits, dim=-1, keepdim=True)
if idx_next == eos_id: #E
break
idx = torch.cat((idx, idx_next), dim=1)
return idx
#A For循环与之前相同获取logits仅关注最后的时间步
#B 在新步骤中通过top-k采样过滤logits
#C 在新步骤中应用temperature scaling
#D 在未使用temperature scaling时执行贪婪的下一个token选择
#E 如果遇到序列结束token且指定了eos_id则提前停止生成
```
现在来看看这个新的`generate`函数的实际效果:
```python
torch.manual_seed(123)
token_ids = generate(
model=model,
idx=text_to_token_ids("Every effort moves you", tokenizer).to(device),
max_new_tokens=15,
context_size=GPT_CONFIG_124M["context_length"],
top_k=25,
temperature=1.4
)
print("Output text:\n", token_ids_to_text(token_ids, tokenizer))
```
生成的文本如下:
```python
Output text:
Every effort moves you stand to work on surprise, a one of us had gone with random-
```
正如我们所见,当前生成的文本与之前在 5.3 节开头用 `generate_simple` 函数生成的文本有很大不同(例如那句"Every effort moves you know," was one of the axioms he laid...!"),而后者是模型从训练集中记忆的一段话。
> [!NOTE]
>
> **练习 5.2**
>
> 尝试不同的 temperature 和 top-k 设置。根据你的观察,你能想到哪些应用场景适合较低的 temperature 和 top-k 设置吗?反之,哪些应用场景适合较高的 temperature 和 top-k 设置?(建议在本章末加载 OpenAI 的预训练权重后,再次进行此练习)
> [!NOTE]
>
> **练习 5.3**
>
> generate 函数有哪些不同的设置组合可以强制生成确定性行为,即禁用随机采样,使其输出始终一致,类似于 generate_simple 函数?
>
> 到目前为止,我们已介绍了如何预训练 LLM 并使用其生成文本。本章最后两节将讨论如何保存和加载训练好的 LLM以及如何加载 OpenAI 的预训练权重。
## 5.4 在 PyTorch 中加载和保存模型权重
在本章中,我们讨论了如何数值化评估训练进度,以及从零开始预训练 LLM。尽管模型和数据集都相对较小这次练习依然展示了预训练 LLM 的高昂成本。因此,能够保存 LLM 以避免每次在新会话中使用时都重新训练显得尤为重要。
如图 5.16 的章节概览所示,本节将介绍如何保存和加载预训练模型。然后,在接下来的部分中,我们将从 OpenAI 加载一个更强大的预训练 GPT 模型到我们的 GPTModel 实例中。
<img src="../Image/chapter5/figure5.16.png" width="75%" />
幸运的是,保存 PyTorch 模型相对简单。推荐的做法是保存模型的 `state_dict`(状态字典),这是一个字典,用于将模型的每一层映射到其对应的参数上,可以通过 `torch.save` 函数来实现,代码如下所示:
```python
torch.save(model.state_dict(), "model.pth")
```
在以上代码中,`model.pth`是用于保存 `state_dict` 的文件名。`.pth` 是 PyTorch 文件的惯用扩展名,但实际上也可以使用其他扩展名。
使用 `state_dict` 保存模型权重后,可以将权重加载到新的 GPTModel 模型实例中,具体操作如下:
```python
model = GPTModel(GPT_CONFIG_124M)
model.load_state_dict(torch.load("model.pth"))
model.eval()
```
正如第 4 章所讨论的dropout 通过在训练过程中随机“丢弃”某些神经元,以防止模型过拟合。然而,在推理阶段,我们不希望随机丢弃网络中学到的任何信息。通过使用 `model.eval()`,模型会切换到推理阶段的评估模式,从而禁用 dropout 层。
如果计划稍后继续预训练模型(例如使用本章之前定义的 train_model_simple 函数),那么建议同时保存优化器状态。
AdamW 等自适应优化器会为每个模型参数存储额外信息。AdamW 使用历史数据动态调整每个模型参数的学习率。没有这些信息时,优化器会重置,模型可能无法有效学习,甚至无法正确收敛,进而失去生成连贯文本的能力。可以使用 `torch.save` 保存模型和优化器的状态,方法如下:
```python
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
},
"model_and_optimizer.pth"
)
```
接下来,我们可以按以下步骤恢复模型和优化器的状态:首先通过 `torch.load` 加载保存的数据,然后使用 `load_state_dict` 方法恢复状态:
```python
checkpoint = torch.load("model_and_optimizer.pth")
model = GPTModel(GPT_CONFIG_124M)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1)
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
model.train();
```
> [!NOTE]
>
> **练习 5.4**
>
> 保存权重后,在新的 Python 会话中加载模型和优化器,使用 train_model_simple 函数继续进行 1 个 epoch 的预训练。
## 5.5 从 OpenAI 加载预训练权重
之前,我们为了教学目的,使用有限的数据集(包含一本短篇小说集)训练了一个小型 GPT-2 模型,这样可以专注于讲解 LLM 的基本原理,而无需耗费大量时间和计算资源。
OpenAI 公开了 GPT-2 模型的权重,使我们不必投入数十万甚至数百万美元自行在大规模语料上重新训练模型。
在本节的余下部分,我们将把这些权重加载到 GPTModel 类中,并利用该模型进行文本生成。这里的权重是指存储在 PyTorch 的 Linear 和 Embedding 层的 `.weight`属性中的权重参数(在训练模型时,我们可以通过`model.parameters() `访问这些权重)。
在后续章节中,我们将复用这些预训练权重,对模型进行微调以用于文本分类任务,并遵循类似 ChatGPT 的指令。
请注意OpenAI 最初使用 TensorFlow 来保存 GPT-2 的权重,因此在 Python 中加载这些权重需要安装 TensorFlow。另外以下代码将使用进度条工具 tqdm 来跟踪下载进度,也需要提前安装。
请在终端中执行以下命令来安装所需的库:
```python
pip install tensorflow>=2.15.0 tqdm>=4.66
```
由于下载代码篇幅较长,主要是样板代码,因此本章不会浪费篇幅详细讨论。读者可以直接从本章的在线资源库下载 `gpt_download.py` 模块:
```python
import urllib.request
url = (
"https://raw.githubusercontent.com/rasbt/"
"LLMs-from-scratch/main/ch05/"
"01_main-chapter-code/gpt_download.py"
)
filename = url.split('/')[-1]
urllib.request.urlretrieve(url, filename)
```
接下来,在将此文件下载到本地目录后,建议读者简单查看文件内容,确保文件已正确保存并包含有效的 Python 代码。
我们现在可以从 `gpt_download.py` 文件中导入 `download_and_load_gpt2` 函数,从而将 GPT-2 的架构设置settings和权重参数params加载到 Python 会话中:
```
from gpt_download import download_and_load_gpt2
settings, params = download_and_load_gpt2(model_size="124M", models_dir="gpt2")
Executing the proceeding code downloads the following 7 files associated with the 124M
parameter GPT-2 model:
checkpoint: 100%|███████████████████████████| 77.0/77.0 [00:00<00:00, 63.9kiB/s]
encoder.json: 100%|█████████████████████████| 1.04M/1.04M [00:00<00:00, 2.20MiB/s]
hprams.json: 100%|██████████████████████████| 90.0/90.0 [00:00<00:00, 78.3kiB/s]
model.ckpt.data-00000-of-00001: 100%|███████| 498M/498M [01:09<00:00, 7.16MiB/s]
model.ckpt.index: 100%|█████████████████████| 5.21k/5.21k [00:00<00:00, 3.24MiB/s]
model.ckpt.meta: 100%|██████████████████████| 471k/471k [00:00<00:00, 2.46MiB/s]
vocab.bpe: 100%|████████████████████████████| 456k/456k [00:00<00:00, 1.70MiB/s]
```
> [!NOTE]
>
> **最新下载说明**
>
> 如果下载代码无法正常工作,可能是由于网络连接不稳定、服务器问题,或者 OpenAI 共享 GPT-2 模型权重的方式发生了变化。请访问本章节的在线代码库https://github.com/rasbt/LLMs-from-scratch以获取更新的操作说明。如有其他问题也可在 Manning 论坛中提问。
代码执行完成后,查看 `settings``params` 的内容:
```python
print("Settings:", settings)
print("Parameter dictionary keys:", params.keys())
```
输出如下:
```python
Settings: {'n_vocab': 50257, 'n_ctx': 1024, 'n_embd': 768, 'n_head': 12, 'n_layer': 12}
Parameter dictionary keys: dict_keys(['blocks', 'b', 'g', 'wpe', 'wte'])
```
settings` 和 `params` 都是 Python 字典。`settings` 字典存储了 LLM 的架构设置,与我们之前手动定义的 `GPT_CONFIG_124M` 设置类似;`params` 字典则包含实际的权重张量。注意,我们只打印了字典的键,因为打印整个权重内容会占用太多屏幕空间。不过,我们可以通过`print(params)` 打印整个字典,或使用特定的字典键选择对应张量进行查看,例如嵌入层的权重:
```python
print(params["wte"])
print("Token embedding weight tensor dimensions:", params["wte"].shape)
```
token 嵌入层的权重如下所示:
```python
[[-0.11010301 ... -0.1363697 0.01506208 0.04531523]
[ 0.04034033 ... 0.08605453 0.00253983 0.04318958]
[-0.12746179 ... 0.08991534 -0.12972379 -0.08785918]
...
[-0.04453601 ... 0.10435229 0.09783269 -0.06952604]
[ 0.1860082 ... -0.09625227 0.07847701 -0.02245961]
[ 0.05135201 ... 0.00704835 0.15519823 0.12067825]]
Token embedding weight tensor dimensions: (50257, 768)
```
我们通过 `download_and_load_gpt2(model_size="124M", ...)` 加载了最小的 GPT-2 模型权重。此外OpenAI 还提供了更大规模模型的权重,包括 "355M"、"774M" 和 "1558M" 等。尽管模型规模不同,但其整体架构是相同的,如图 5.17 所示。
<img src="../Image/chapter5/figure5.17.png" width="75%" />
如图 5.17 所示,不同大小的 GPT-2 模型在总体架构上保持一致,但注意力头和 Transformer 模块等组件的重复次数以及嵌入维度大小有所不同。本章的剩余代码也会兼容这些更大的模型。
在将 GPT-2 模型的权重加载到 Python 后,我们还需要将这些权重从 `settings``params` 字典转移到 GPTModel 实例中:
```python
# First, we create a dictionary that lists the differences between the different GPT model sizes, as explained in Figure 5.17:
model_configs = {
"gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},
"gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},
"gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},
"gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},
}
# Suppose we are interested in loading the smallest model, "gpt2-small (124M)". We can use the corresponding settings from the model_configs table able to update our full-length GPT_CONFIG_124M we defined and used earlier throughout the chapter as follows:
model_name = "gpt2-small (124M)"
NEW_CONFIG = GPT_CONFIG_124M.copy()
NEW_CONFIG.update(model_configs[model_name])
```
细心的读者可能记得,我们之前设置的 token 长度是 256但 OpenAI 的原始 GPT-2 模型使用的是 1,024 的 token 长度,因此我们需要相应地更新 NEW_CONFIG:
```python
NEW_CONFIG.update({"context_length": 1024})
```
此外OpenAI 在多头注意力模块的线性层中使用了偏置向量以实现查询query、键key和值value矩阵的计算。偏置向量在现代 LLM 中已不再常用,因为它们对提升模型性能没有帮助,因而不再必要。然而,由于我们使用的是预训练权重,为了保持一致性,仍需启用这些偏置向量:
```python
NEW_CONFIG.update({"qkv_bias": True})
# We can now use the updated NEW_CONFIG dictionary to initialize a new GPTModel instance:
gpt = GPTModel(NEW_CONFIG)
gpt.eval()
```
默认情况下GPTModel 实例会使用随机权重进行预训练。而使用 OpenAI 的模型权重的最后一步是将 `params` 字典中加载的权重覆盖这些随机权重。
为此,我们首先来定义一个简单的`assign`工具函数,用于检查两个张量或数组(左侧和右侧)的维度或形状是否一致,并将右侧张量作为可训练的 PyTorch 参数返回:
```python
def assign(left, right):
if left.shape != right.shape:
raise ValueError(f"Shape mismatch. Left: {left.shape}, Right: {right.shape}")
return torch.nn.Parameter(torch.tensor(right))
```
接下来,我们定义一个名为 `load_weights_into_gpt` 的函数,用于将 `params` 字典中的权重加载到 GPT 模型实例中:
```python
# Listing 5.5 Loading OpenAI weights into our GPT model code
import numpy as np
def load_weights_into_gpt(gpt, params):
gpt.pos_emb.weight = assign(gpt.pos_emb.weight, params['wpe']) #A
gpt.tok_emb.weight = assign(gpt.tok_emb.weight, params['wte'])
for b in range(len(params["blocks"])): #B
q_w, k_w, v_w = np.split( #C
(params["blocks"][b]["attn"]["c_attn"])["w"], 3, axis=-1)
gpt.trf_blocks[b].att.W_query.weight = assign(
gpt.trf_blocks[b].att.W_query.weight, q_w.T)
gpt.trf_blocks[b].att.W_key.weight = assign(
gpt.trf_blocks[b].att.W_key.weight, k_w.T)
gpt.trf_blocks[b].att.W_value.weight = assign(
gpt.trf_blocks[b].att.W_value.weight, v_w.T)
q_b, k_b, v_b = np.split(
(params["blocks"][b]["attn"]["c_attn"])["b"], 3, axis=-1)
gpt.trf_blocks[b].att.W_query.bias = assign(
gpt.trf_blocks[b].att.W_query.bias, q_b)
gpt.trf_blocks[b].att.W_key.bias = assign(
gpt.trf_blocks[b].att.W_key.bias, k_b)
gpt.trf_blocks[b].att.W_value.bias = assign(
gpt.trf_blocks[b].att.W_value.bias, v_b)
gpt.trf_blocks[b].att.out_proj.weight = assign(
gpt.trf_blocks[b].att.out_proj.weight,
params["blocks"][b]["attn"]["c_proj"]["w"].T)
gpt.trf_blocks[b].att.out_proj.bias = assign(
gpt.trf_blocks[b].att.out_proj.bias,
params["blocks"][b]["attn"]["c_proj"]["b"])
gpt.trf_blocks[b].ff.layers[0].weight = assign(
gpt.trf_blocks[b].ff.layers[0].weight,
params["blocks"][b]["mlp"]["c_fc"]["w"].T)
gpt.trf_blocks[b].ff.layers[0].bias = assign(
gpt.trf_blocks[b].ff.layers[0].bias,
params["blocks"][b]["mlp"]["c_fc"]["b"])
gpt.trf_blocks[b].ff.layers[2].weight = assign(
gpt.trf_blocks[b].ff.layers[2].weight,
params["blocks"][b]["mlp"]["c_proj"]["w"].T)
gpt.trf_blocks[b].ff.layers[2].bias = assign(
gpt.trf_blocks[b].ff.layers[2].bias,
params["blocks"][b]["mlp"]["c_proj"]["b"])
gpt.trf_blocks[b].norm1.scale = assign(
gpt.trf_blocks[b].norm1.scale,
params["blocks"][b]["ln_1"]["g"])
gpt.trf_blocks[b].norm1.shift = assign(
gpt.trf_blocks[b].norm1.shift,
params["blocks"][b]["ln_1"]["b"])
gpt.trf_blocks[b].norm2.scale = assign(
gpt.trf_blocks[b].norm2.scale,
params["blocks"][b]["ln_2"]["g"])
gpt.trf_blocks[b].norm2.shift = assign(
gpt.trf_blocks[b].norm2.shift,
params["blocks"][b]["ln_2"]["b"])
gpt.final_norm.scale = assign(gpt.final_norm.scale, params["g"])
gpt.final_norm.shift = assign(gpt.final_norm.shift, params["b"])
gpt.out_head.weight = assign(gpt.out_head.weight, params["wte"]) #D
#A 将模型的位置嵌入和token 嵌入的权重设置为 params 中指定的值
#B 遍历模型中的每个 Transformer 模块
#C 使用 np.split 函数将注意力和偏置权重分为三等份,分别用于查询、键和值组件
#D OpenAI 的原始 GPT-2 模型在输出层中复用了 token 嵌入的权重,以减少参数总量,这一概念称为权重共享
```
`load_weights_into_gpt` 函数中,我们需要将 OpenAI 实现中的权重与自定义的 GPTModel 实现进行精确匹配。举个例子OpenAI 将第一个 Transformer 模块的输出投影层权重存储在 `params["blocks"][0]["attn"]["c_proj"]["w"]` 中。而在我们的实现中,这个权重对应于 `gpt.trf_blocks[b].att.out_proj.weight`,其中 `gpt` 是一个 GPTModel 实例。
在开发 `load_weights_into_gpt` 函数时,由于 OpenAI 的命名规范和我们的略有不同,我们进行了大量的尝试。幸运的是,`assign` 函数会在张量维度不匹配时发出警告。此外,如果这个函数有错误,我们会发现生成的 GPT 模型无法生成连贯的文本,从而识别出问题。
我们暂时不在实际操作中尝试 `load_weights_into_gpt`,而是直接将 OpenAI 模型的权重加载到我们自己的 `GPTModel` 实例 `gpt` 中:
```python
load_weights_into_gpt(gpt, params)
gpt.to(device)
```
如果模型加载成功,就可以使用之前的 `generate` 函数生成新文本:
```python
torch.manual_seed(123)
token_ids = generate(
model=gpt,
idx=text_to_token_ids("Every effort moves you", tokenizer),
max_new_tokens=25,
context_size=NEW_CONFIG["context_length"],
top_k=50,
temperature=1.5
)
print("Output text:\n", token_ids_to_text(token_ids, tokenizer))
```
生成的文本如下:
```python
Output text:
Every effort moves you toward finding an ideal new way to practice something!
What makes us want to be on top of that?
```
我们可以确认模型权重已正确加载,因为模型能够生成连贯的文本;在这个过程中,哪怕一个小错误都会导致模型生成失败。
在接下来的章节中,我们将进一步使用该预训练模型,并对其进行微调,使其能够进行文本分类和指令执行。
> [!NOTE]
>
> **练习 5.5**
>
> 使用 OpenAI 预训练权重的 GPT 模型在The Verdict数据集上计算训练集和验证集的损失。
> [!NOTE]
>
> **练习 5.6**
>
> 建议读者尝试不同规模的 GPT-2 模型,例如最大规模的 1558M 参数模型,并与本章加载的 124M 模型的生成效果进行比较。
## 5.6 总结
+ 大语言模型在生成文本时,逐个生成 token。
+ 默认情况下,模型通过将输出转换为概率分数,并选择其中概率最高的 token 来生成下一个 token这种方式称为“贪心解码”。
+ 通过概率采样和`temperature scaling`,可以影响生成文本的多样性和连贯性。
### + 训练集和验证集的损失可以用来评估 LLM 在训练过程中生成文本的质量。
+ 预训练 LLM 的过程就是通过调整模型权重来最小化训练损失。
+ LLM 的训练循环是深度学习中的标准流程,通常使用交叉熵损失和 AdamW 优化器。
+ 在大规模文本数据集上预训练 LLM 非常耗费时间和资源,因此可以加载 OpenAI 提供的开源预训练权重,作为自行预训练模型的替代方案。