Translated ['src/AI/AI-llm-architecture/0.-basic-llm-concepts.md', 'src/

This commit is contained in:
Translator 2025-06-08 18:10:36 +00:00
parent d29b4f99f9
commit 574eff4680
10 changed files with 1369 additions and 82 deletions

View File

@ -1,4 +1,4 @@
# 0. 基 LLM 概念
# 0. 基 LLM 概念
## 预训练
@ -10,11 +10,11 @@
- **参数**:参数是神经网络中的**可学习权重和偏差**。这些是训练过程调整的数字以最小化损失函数并提高模型在任务上的性能。LLM 通常使用数百万个参数。
- **上下文长度**:这是用于预训练 LLM 的每个句子的最大长度。
- **嵌入维度**用于表示每个标记或单词的向量大小。LLM 通常使用数十亿维度。
- **嵌入维度**用于表示每个标记或单词的向量大小。LLM 通常使用数十亿维度。
- **隐藏维度**:神经网络中隐藏层的大小。
- **层数(深度)**模型的层数。LLM 通常使用数十层。
- **注意力头数**在变换器模型中这是每层中使用的独立注意力机制的数量。LLM 通常使用数十个头。
- **丢弃率**:丢弃率类似于在训练过程中移除的数据百分比(概率变为 0用于**防止过拟合**。LLM 通常使用 0-20% 之间的
- **丢弃率**:丢弃率类似于在训练过程中移除的数据百分比(概率变为 0用于**防止过拟合**。LLM 通常使用 0-20% 之间的丢弃率
GPT-2 模型的配置:
```json
@ -30,7 +30,7 @@ GPT_CONFIG_124M = {
```
## Tensors in PyTorch
在 PyTorch 中,**tensor** 是一种基本数据结构,作为多维数组,推广了标量、向量和矩阵等概念到更高的维度。张量是数据在 PyTorch 中表示和操作的主要方式,特别是在深度学习和神经网络的背景下。
在 PyTorch 中,**tensor** 是一种基本数据结构,作为多维数组,推广了标量、向量和矩阵等概念到更高的维度。张量是 PyTorch 中数据表示和操作的主要方式,特别是在深度学习和神经网络的背景下。
### Mathematical Concept of Tensors
@ -70,7 +70,7 @@ tensor2d = torch.tensor([[1, 2],
tensor3d = torch.tensor([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
```
### Tensor 数据类型
### 张量数据类型
PyTorch 张量可以存储各种类型的数据,例如整数和浮点数。
@ -133,7 +133,7 @@ result = tensor2d @ tensor2d.T
自动微分的核心是微积分中的 **链式法则**。链式法则指出,如果你有一个函数的复合,复合函数的导数是组成函数导数的乘积。
在数学上,如果 `y=f(u)``u=g(x)`,那么 `y` `x` 的导数为:
在数学上,如果 `y=f(u)``u=g(x)`,那么 `y` 关于 `x` 的导数为:
<figure><img src="../../images/image (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
@ -153,7 +153,7 @@ result = tensor2d @ tensor2d.T
- `y=1.0` 是目标标签。
- `L` 是损失。
我们想要计算损失 `L` 权重 `w` 和偏差 `b` 的梯度。
我们想要计算损失 `L` 关于权重 `w` 和偏差 `b` 的梯度。
**4. 手动计算梯度**
@ -190,36 +190,36 @@ loss.backward()
print("Gradient w.r.t w:", w.grad)
print("Gradient w.r.t b:", b.grad)
```
请提供需要翻译的具体内容。
请提供需要翻译的内容。
```css
cssCopy codeGradient w.r.t w: tensor([-0.0898])
Gradient w.r.t b: tensor([-0.0817])
```
## Backpropagation in Bigger Neural Networks
## 在更大神经网络中的反向传播
### **1.Extending to Multilayer Networks**
### **1. 扩展到多层网络**
在具有多个层的大神经网络中,由于参数和操作数量的增加,计算梯度的过程变得更加复杂。然而,基本原理保持不变:
在具有多个层的大神经网络中,由于参数和操作数量的增加,计算梯度的过程变得更加复杂。然而,基本原理保持不变:
- **Forward Pass:** 通过将输入传递通过每一层来计算网络的输出。
- **Compute Loss:** 使用网络的输出和目标标签评估损失函数。
- **Backward Pass (Backpropagation):** 通过从输出层递归应用链式法则到输入层,计算损失相对于网络中每个参数的梯度。
- **前向传播:** 通过每一层传递输入来计算网络的输出。
- **计算损失:** 使用网络的输出和目标标签评估损失函数。
- **反向传播Backpropagation** 通过从输出层递归应用链式法则,计算损失相对于网络中每个参数的梯度,直到输入层
### **2. Backpropagation Algorithm**
### **2. 反向传播算法**
- **Step 1:** 初始化网络参数(权重和偏置)。
- **Step 2:** 对于每个训练示例,执行前向传播以计算输出。
- **Step 3:** 计算损失。
- **Step 4:** 使用链式法则计算损失相对于每个参数的梯度。
- **Step 5:** 使用优化算法(例如,梯度下降)更新参数。
- **步骤 1** 初始化网络参数(权重和偏置)。
- **步骤 2** 对于每个训练样本,执行前向传播以计算输出。
- **步骤 3** 计算损失。
- **步骤 4** 使用链式法则计算损失相对于每个参数的梯度。
- **步骤 5** 使用优化算法(例如,梯度下降)更新参数。
### **3. Mathematical Representation**
### **3. 数学表示**
考虑一个具有一个隐藏层的简单神经网络:
<figure><img src="../../images/image (5) (1).png" alt=""><figcaption></figcaption></figure>
### **4. PyTorch Implementation**
### **4. PyTorch 实现**
PyTorch 通过其自动求导引擎简化了这个过程。
```python

View File

@ -5,7 +5,7 @@
**Tokenizing** 是将数据(如文本)分解为更小、可管理的部分,称为 _tokens_ 的过程。每个 token 都会被分配一个唯一的数字标识符ID。这是为机器学习模型处理文本做准备的基本步骤尤其是在自然语言处理NLP中。
> [!TIP]
> 这个初始阶段的目标非常简单:**以某种有意义的方式将输入划分为 tokensids**。
> 这个初始阶段的目标非常简单:**以某种合理的方式将输入划分为 tokensids**。
### **How Tokenizing Works**
@ -15,54 +15,54 @@
文本: `"Hello, world!"`\
Tokens: `["Hello", ",", "world", "!"]`
2. **Creating a Vocabulary:**
- 为了将 tokens 转换为数字 ID创建一个 **vocabulary**。这个 vocabulary 列出了所有唯一的 tokens单词和符号并为每个 token 分配一个特定的 ID。
- **Special Tokens:** 这些是添加到 vocabulary 中以处理各种场景的特殊符号
- 为了将 tokens 转换为数字 ID创建一个 **vocabulary**。这个词汇表列出了所有唯一的 tokens单词和符号并为每个分配一个特定的 ID。
- **Special Tokens:** 这些是添加到词汇表中的特殊符号,以处理各种场景
- `[BOS]`(序列开始):表示文本的开始。
- `[EOS]`(序列结束):表示文本的结束。
- `[PAD]`(填充):用于使批次中的所有序列具有相同的长度。
- `[UNK]`(未知):表示不在 vocabulary 中的 tokens。
- `[UNK]`(未知):表示不在词汇表中的 tokens。
- _Example:_\
如果 `"Hello"` 被分配 ID `64``","``455``"world"``78``"!"``467`,那么:\
`"Hello, world!"``[64, 455, 78, 467]`
- **Handling Unknown Words:**\
如果像 `"Bye"` 这样的单词不在 vocabulary 中,它将被替换为 `[UNK]`。\
如果像 `"Bye"` 这样的单词不在词汇表中,它会被替换为 `[UNK]`。\
`"Bye, world!"``["[UNK]", ",", "world", "!"]``[987, 455, 78, 467]`\
_(假设 `[UNK]` 的 ID 是 `987`)_
### **Advanced Tokenizing Methods**
虽然基本的 tokenizer 对简单文本效果很好,但在处理大 vocabulary 和新或稀有单词时存在局限性。高级 tokenizing 方法通过将文本分解为更小的子单元或优化 tokenization 过程来解决这些问题。
虽然基本的 tokenizer 对简单文本效果很好,但在处理大词汇表和新或稀有单词时存在局限性。高级 tokenizing 方法通过将文本分解为更小的子单元或优化 tokenization 过程来解决这些问题。
1. **Byte Pair Encoding (BPE):**
- **Purpose:** 通过将稀有或未知单词分解为频繁出现的字节对,减少 vocabulary 的大小并处理稀有或未知单词。
- **Purpose:** 通过将稀有或未知单词分解为频繁出现的字节对,减少词汇表的大小并处理稀有或未知单词。
- **How It Works:**
- 从单个字符作为 tokens 开始。
- 迭代地将最频繁的 token 对合并为一个单一的 token。
- 继续直到没有更多频繁的对可以合并。
- **Benefits:**
- 消除了对 `[UNK]` token 的需求,因为所有单词都可以通过组合现有的子词 tokens 来表示。
- 更高效和灵活的 vocabulary
- 更高效和灵活的词汇表
- _Example:_\
`"playing"` 可能被 tokenized 为 `["play", "ing"]`,如果 `"play"``"ing"` 是频繁的子词。
2. **WordPiece:**
- **Used By:** 像 BERT 这样的模型。
- **Purpose:** 类似于 BPE它将单词分解为子词单元以处理未知单词并减少 vocabulary 大小。
- **Purpose:** 类似于 BPE它将单词分解为子词单元以处理未知单词并减少词汇表大小。
- **How It Works:**
- 从单个字符的基础 vocabulary 开始。
- 从单个字符的基本词汇表开始。
- 迭代地添加最频繁的子词,以最大化训练数据的可能性。
- 使用概率模型决定合并哪些子词。
- **Benefits:**
- 在拥有可管理的 vocabulary 大小和有效表示单词之间取得平衡。
- 在拥有可管理的词汇表大小和有效表示单词之间取得平衡。
- 高效处理稀有和复合单词。
- _Example:_\
`"unhappiness"` 可能被 tokenized 为 `["un", "happiness"]``["un", "happy", "ness"]`,具体取决于 vocabulary
`"unhappiness"` 可能被 tokenized 为 `["un", "happiness"]``["un", "happy", "ness"]`,具体取决于词汇表
3. **Unigram Language Model:**
- **Used By:** 像 SentencePiece 这样的模型。
- **Purpose:** 使用概率模型确定最可能的子词 tokens 集合。
- **How It Works:**
- 从一组潜在的 tokens 开始。
- 迭代地移除那些对模型的训练数据概率改善最小的 tokens。
- 最终确定一个 vocabulary,其中每个单词由最可能的子词单元表示。
- 最终确定一个词汇表,其中每个单词由最可能的子词单元表示。
- **Benefits:**
- 灵活且可以更自然地建模语言。
- 通常会导致更高效和紧凑的 tokenizations。

View File

@ -0,0 +1,233 @@
# 2. 数据采样
## **数据采样**
**数据采样** 是为训练大型语言模型LLMs如 GPT 准备数据的关键过程。它涉及将文本数据组织成模型用于学习如何根据前面的单词预测下一个单词(或标记)的输入和目标序列。适当的数据采样确保模型有效捕捉语言模式和依赖关系。
> [!TIP]
> 第二阶段的目标非常简单:**对输入数据进行采样,并为训练阶段准备,通常通过将数据集分成特定长度的句子并生成预期的响应。**
### **为什么数据采样很重要**
像 GPT 这样的 LLM 通过理解前面单词提供的上下文来生成或预测文本。为了实现这一点,训练数据必须以模型能够学习单词序列及其后续单词之间关系的方式进行结构化。这种结构化的方法使模型能够概括并生成连贯且上下文相关的文本。
### **数据采样中的关键概念**
1. **标记化:** 将文本分解为称为标记的较小单元(例如,单词、子词或字符)。
2. **序列长度 (max_length)** 每个输入序列中的标记数量。
3. **滑动窗口:** 通过在标记化文本上移动窗口来创建重叠输入序列的方法。
4. **步幅:** 滑动窗口向前移动以创建下一个序列的标记数量。
### **逐步示例**
让我们通过一个示例来说明数据采样。
**示例文本**
```arduino
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
```
**Tokenization**
假设我们使用一个**基本的分词器**,将文本分割成单词和标点符号:
```vbnet
Tokens: ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit."]
```
**参数**
- **最大序列长度 (max_length):** 4 个标记
- **滑动窗口步幅:** 1 个标记
**创建输入和目标序列**
1. **滑动窗口方法:**
- **输入序列:** 每个输入序列由 `max_length` 个标记组成。
- **目标序列:** 每个目标序列由紧接着相应输入序列的标记组成。
2. **生成序列:**
<table><thead><tr><th width="177">窗口位置</th><th>输入序列</th><th>目标序列</th></tr></thead><tbody><tr><td>1</td><td>["Lorem", "ipsum", "dolor", "sit"]</td><td>["ipsum", "dolor", "sit", "amet,"]</td></tr><tr><td>2</td><td>["ipsum", "dolor", "sit", "amet,"]</td><td>["dolor", "sit", "amet,", "consectetur"]</td></tr><tr><td>3</td><td>["dolor", "sit", "amet,", "consectetur"]</td><td>["sit", "amet,", "consectetur", "adipiscing"]</td></tr><tr><td>4</td><td>["sit", "amet,", "consectetur", "adipiscing"]</td><td>["amet,", "consectetur", "adipiscing", "elit."]</td></tr></tbody></table>
3. **结果输入和目标数组:**
- **输入:**
```python
[
["Lorem", "ipsum", "dolor", "sit"],
["ipsum", "dolor", "sit", "amet,"],
["dolor", "sit", "amet,", "consectetur"],
["sit", "amet,", "consectetur", "adipiscing"],
]
```
- **目标:**
```python
[
["ipsum", "dolor", "sit", "amet,"],
["dolor", "sit", "amet,", "consectetur"],
["sit", "amet,", "consectetur", "adipiscing"],
["amet,", "consectetur", "adipiscing", "elit."],
]
```
**可视化表示**
<table><thead><tr><th width="222">标记位置</th><th>标记</th></tr></thead><tbody><tr><td>1</td><td>Lorem</td></tr><tr><td>2</td><td>ipsum</td></tr><tr><td>3</td><td>dolor</td></tr><tr><td>4</td><td>sit</td></tr><tr><td>5</td><td>amet,</td></tr><tr><td>6</td><td>consectetur</td></tr><tr><td>7</td><td>adipiscing</td></tr><tr><td>8</td><td>elit.</td></tr></tbody></table>
**步幅为1的滑动窗口:**
- **第一个窗口 (位置 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **目标:** \["ipsum", "dolor", "sit", "amet,"]
- **第二个窗口 (位置 2-5):** \["ipsum", "dolor", "sit", "amet,"] → **目标:** \["dolor", "sit", "amet,", "consectetur"]
- **第三个窗口 (位置 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **目标:** \["sit", "amet,", "consectetur", "adipiscing"]
- **第四个窗口 (位置 4-7):** \["sit", "amet,", "consectetur", "adipiscing"] → **目标:** \["amet,", "consectetur", "adipiscing", "elit."]
**理解步幅**
- **步幅为1:** 窗口每次向前移动一个标记,导致高度重叠的序列。这可以更好地学习上下文关系,但可能增加过拟合的风险,因为相似的数据点被重复。
- **步幅为2:** 窗口每次向前移动两个标记,减少重叠。这减少了冗余和计算负担,但可能会错过一些上下文细微差别。
- **步幅等于max_length:** 窗口按整个窗口大小向前移动,导致非重叠序列。这最小化了数据冗余,但可能限制模型学习序列间依赖关系的能力。
**步幅为2的示例:**
使用相同的标记文本和 `max_length` 为4:
- **第一个窗口 (位置 1-4):** \["Lorem", "ipsum", "dolor", "sit"] → **目标:** \["ipsum", "dolor", "sit", "amet,"]
- **第二个窗口 (位置 3-6):** \["dolor", "sit", "amet,", "consectetur"] → **目标:** \["sit", "amet,", "consectetur", "adipiscing"]
- **第三个窗口 (位置 5-8):** \["amet,", "consectetur", "adipiscing", "elit."] → **目标:** \["consectetur", "adipiscing", "elit.", "sed"] _(假设继续)_
## 代码示例
让我们通过来自 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch02/01_main-chapter-code/ch02.ipynb) 的代码示例更好地理解这一点:
```python
# Download the text to pre-train the LLM
import urllib.request
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt")
file_path = "the-verdict.txt"
urllib.request.urlretrieve(url, file_path)
with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read()
"""
Create a class that will receive some params lie tokenizer and text
and will prepare the input chunks and the target chunks to prepare
the LLM to learn which next token to generate
"""
import torch
from torch.utils.data import Dataset, DataLoader
class GPTDatasetV1(Dataset):
def __init__(self, txt, tokenizer, max_length, stride):
self.input_ids = []
self.target_ids = []
# Tokenize the entire text
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
# Use a sliding window to chunk the book into overlapping sequences of max_length
for i in range(0, len(token_ids) - max_length, stride):
input_chunk = token_ids[i:i + max_length]
target_chunk = token_ids[i + 1: i + max_length + 1]
self.input_ids.append(torch.tensor(input_chunk))
self.target_ids.append(torch.tensor(target_chunk))
def __len__(self):
return len(self.input_ids)
def __getitem__(self, idx):
return self.input_ids[idx], self.target_ids[idx]
"""
Create a data loader which given the text and some params will
prepare the inputs and targets with the previous class and
then create a torch DataLoader with the info
"""
import tiktoken
def create_dataloader_v1(txt, batch_size=4, max_length=256,
stride=128, shuffle=True, drop_last=True,
num_workers=0):
# Initialize the tokenizer
tokenizer = tiktoken.get_encoding("gpt2")
# Create dataset
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
# Create dataloader
dataloader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=shuffle,
drop_last=drop_last,
num_workers=num_workers
)
return dataloader
"""
Finally, create the data loader with the params we want:
- The used text for training
- batch_size: The size of each batch
- max_length: The size of each entry on each batch
- stride: The sliding window (how many tokens should the next entry advance compared to the previous one). The smaller the more overfitting, usually this is equals to the max_length so the same tokens aren't repeated.
- shuffle: Re-order randomly
"""
dataloader = create_dataloader_v1(
raw_text, batch_size=8, max_length=4, stride=1, shuffle=False
)
data_iter = iter(dataloader)
first_batch = next(data_iter)
print(first_batch)
# Note the batch_size of 8, the max_length of 4 and the stride of 1
[
# Input
tensor([[ 40, 367, 2885, 1464],
[ 367, 2885, 1464, 1807],
[ 2885, 1464, 1807, 3619],
[ 1464, 1807, 3619, 402],
[ 1807, 3619, 402, 271],
[ 3619, 402, 271, 10899],
[ 402, 271, 10899, 2138],
[ 271, 10899, 2138, 257]]),
# Target
tensor([[ 367, 2885, 1464, 1807],
[ 2885, 1464, 1807, 3619],
[ 1464, 1807, 3619, 402],
[ 1807, 3619, 402, 271],
[ 3619, 402, 271, 10899],
[ 402, 271, 10899, 2138],
[ 271, 10899, 2138, 257],
[10899, 2138, 257, 7026]])
]
# With stride=4 this will be the result:
[
# Input
tensor([[ 40, 367, 2885, 1464],
[ 1807, 3619, 402, 271],
[10899, 2138, 257, 7026],
[15632, 438, 2016, 257],
[ 922, 5891, 1576, 438],
[ 568, 340, 373, 645],
[ 1049, 5975, 284, 502],
[ 284, 3285, 326, 11]]),
# Target
tensor([[ 367, 2885, 1464, 1807],
[ 3619, 402, 271, 10899],
[ 2138, 257, 7026, 15632],
[ 438, 2016, 257, 922],
[ 5891, 1576, 438, 568],
[ 340, 373, 645, 1049],
[ 5975, 284, 502, 284],
[ 3285, 326, 11, 287]])
]
```
## 参考文献
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)

View File

@ -2,13 +2,13 @@
## Token Embeddings
在对文本数据进行分词后,为像 GPT 这样的训练大型语言模型LLMs准备数据的下一个关键步骤是创建 **token embeddings**。Token embeddings 将离散的标记(如单词或子词)转换为模型可以处理和学习的连续数值向量。此解释分解了 token embeddings、它们的初始化、使用以及位置嵌入在增强模型对标记序列理解中的作用。
在对文本数据进行分词后,为像 GPT 这样的训练大型语言模型 (LLMs) 准备数据的下一个关键步骤是创建 **token embeddings**。Token embeddings 将离散的标记(如单词或子词)转换为模型可以处理和学习的连续数值向量。此解释分解了 token embeddings、它们的初始化、使用以及位置嵌入在增强模型对标记序列理解中的作用。
> [!TIP]
> 这个第三阶段的目标非常简单:**为词汇表中每个先前标记分配一个所需维度的向量以训练模型。** 词汇表中的每个单词将在 X 维空间中有一个点。\
> 这个第三阶段的目标非常简单:**为词汇表中每个先前标记分配一个所需维度的向量以训练模型。** 词汇表中的每个单词将在 X 维空间中有一个点。\
> 请注意,最初每个单词在空间中的位置是“随机”初始化的,这些位置是可训练的参数(将在训练过程中得到改善)。
>
> 此外,在 token embedding 过程中 **创建了另一层嵌入**,它表示(在这种情况下)**单词在训练句子中的绝对位置**。这样,句子中不同位置的单词将具有不同的表示(含义)。
> 此外,在 token embedding 期间 **创建了另一层嵌入**,它表示(在这种情况下)**单词在训练句子中的绝对位置**。这样,句子中不同位置的单词将具有不同的表示(含义)。
### **What Are Token Embeddings?**

View File

@ -14,7 +14,7 @@
#### 示例:机器翻译
考虑将德语句子 "Kannst du mir helfen diesen Satz zu übersetzen" 翻译成英语。逐字翻译不会产生语法正确的英语句子,因为不同语言之间的语法结构存在差异。注意机制使模型能够在生成输出句子的每个单词时专注于输入句子的相关部分,从而导致更准确和连贯的翻译。
考虑将德语句子 "Kannst du mir helfen diesen Satz zu übersetzen" 翻译成英语。逐字翻译不会产生语法正确的英语句子,因为不同语言之间的语法结构存在差异。注意机制使模型在生成输出句子的每个单词时能够专注于输入句子的相关部分,从而导致更准确和连贯的翻译。
### 自注意力介绍
@ -39,29 +39,30 @@
#### 步骤1计算注意分数
> [!TIP]
> 只需将查询的每个维度值与每个标记的相关维度相乘并加上结果。你将得到每对标记的1个值。
> 只需将查询的每个维度值与每个标记的相关维度相乘并加上结果。你将为每对标记获得1个值。
对于句子中的每个单词,通过计算它们嵌入的点积来计算与 "shiny" 的**注意分数**。
**"Hello"与"shiny"之间的注意分数**
**"Hello" 和 "shiny" 之间的注意分数**
<figure><img src="../../images/image (4) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
**"shiny"与"shiny"之间的注意分数**
**"shiny" 和 "shiny" 之间的注意分数**
<figure><img src="../../images/image (1) (1) (1) (1) (1) (1) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
**"sun"与"shiny"之间的注意分数**
**"sun" 和 "shiny" 之间的注意分数**
<figure><img src="../../images/image (2) (1) (1) (1) (1).png" alt="" width="563"><figcaption></figcaption></figure>
#### 步骤2归一化注意分数以获得注意权重
> [!TIP]
> 不要迷失在数学术语中,这个函数的目标很简单,归一化所有权重,使**它们的总和为1**。\
> 不要迷失在数学术语中,这个函数的目标很简单,归一化所有权重,使**它们的总和为1**。
>
> 此外,**softmax** 函数被使用,因为它通过指数部分强调差异,使得更容易检测有用的值。
应用**softmax函数**到注意分数,以将其转换为总和为1的注意权重。
应用**softmax函数**将注意分数转换为总和为1的注意权重。
<figure><img src="../../images/image (3) (1) (1) (1) (1).png" alt="" width="293"><figcaption></figcaption></figure>
@ -100,16 +101,16 @@
<figure><img src="../../images/image (9) (1) (1).png" alt=""><figcaption></figcaption></figure>
加权嵌入的总和:
加权嵌入和:
`context vector=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]`
**这个上下文向量表示了"shiny"的丰富嵌入,结合了句子中所有单词的信息。**
**这个上下文向量表示了“shiny”的丰富嵌入,结合了句子中所有单词的信息。**
### 过程总结
1. **计算注意分数**:使用目标单词的嵌入与序列中所有单词的嵌入之间的点积。
2. **归一化分数以获得注意权重**应用softmax函数到注意分数以获得总和为1的权重。
2. **归一化分数以获得注意权重**对注意分数应用softmax函数以获得总和为1的权重。
3. **计算上下文向量**:将每个单词的嵌入乘以其注意权重并求和结果。
## 带可训练权重的自注意力
@ -181,7 +182,7 @@ values = torch.matmul(inputs, W_value)
### 代码示例
从 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb) 获取一个示例,您可以查看这个实现我们讨论的自注意力功能的类:
从 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb) 获取一个示例,您可以查看这个实现我们讨论的自注意力功能的类:
```python
import torch
@ -221,11 +222,11 @@ sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))
```
> [!TIP]
> 注意,`nn.Linear` 用于将所有权重标记为训练参数,而不是用随机值初始化矩阵。
> 注意,`nn.Linear` 被用来将所有权重标记为可训练参数,而不是用随机值初始化矩阵。
## 因果注意力:隐藏未来词汇
对于 LLM我们希望模型考虑当前位置信息之前出现的标记,以便 **预测下一个标记**。**因果注意力**,也称为 **掩蔽注意力**,通过修改注意力机制来防止访问未来标记,从而实现这一点。
对于 LLM我们希望模型考虑当前位置信息之前出现的标记,以便**预测下一个标记**。**因果注意力**,也称为**掩蔽注意力**,通过修改注意力机制来防止访问未来标记,从而实现这一点。
### 应用因果注意力掩码

View File

@ -14,7 +14,7 @@ LLM架构示例来自 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04
<figure><img src="../../images/image (3) (1) (1) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31">https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31</a></p></figcaption></figure>
1. **输入(标记化文本)**:该过程以标记化文本开始,该文本被转换为数值表示。
2. **标记嵌入和位置嵌入层**:标记化文本通过**标记嵌入**层和**位置嵌入层**,后者捕捉序列中标记的位置,这对理解单词顺序至关重要。
2. **标记嵌入和位置嵌入层**:标记化文本通过**标记嵌入**层和**位置嵌入层**,后者捕捉序列中标记的位置,这对理解单词顺序至关重要。
3. **变换器块**:模型包含**12个变换器块**,每个块有多个层。这些块重复以下序列:
- **掩蔽多头注意力**:允许模型同时关注输入文本的不同部分。
- **层归一化**:一个归一化步骤,以稳定和改善训练。
@ -23,7 +23,7 @@ LLM架构示例来自 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04
4. **最终输出层**:模型输出一个**4x50,257维的张量**,其中**50,257**表示词汇表的大小。该张量中的每一行对应于模型用于预测序列中下一个单词的向量。
5. **目标**:目标是将这些嵌入转换回文本。具体来说,输出的最后一行用于生成下一个单词,在该图中表示为“前进”。
### Code representation
### 代码表示
```python
import torch
import torch.nn as nn
@ -217,7 +217,7 @@ torch.sqrt(torch.tensor(2.0 / torch.pi)) *
<figure><img src="../../images/image (2) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
> [!TIP]
> 在FeedForward层内的线性层之后使用此函数的目的是将线性数据转换为非线性以便模型能够学习复杂的非线性关系。
> 在FeedForward层内的线性层之后使用此函数的目的是将线性数据转换为非线性,以便模型能够学习复杂的非线性关系。
### **前馈神经网络**
@ -293,7 +293,7 @@ return self.scale * norm_x + self.shift
- **层归一化:** 一种用于对每个批次中单个示例的特征(嵌入维度)进行归一化的技术。
- **组件:**
- **`eps`** 一个小常数(`1e-5`),添加到方差中以防止在归一化过程中出现除以零的情况
- **`eps`** 一个小常数(`1e-5`在归一化过程中添加到方差中以防止除以零。
- **`scale``shift`** 可学习的参数(`nn.Parameter`允许模型对归一化输出进行缩放和偏移。它们分别初始化为1和0。
- **归一化过程:**
- **计算均值(`mean`** 计算输入 `x` 在嵌入维度(`dim=-1`)上的均值,同时保持维度以便广播(`keepdim=True`)。
@ -302,7 +302,7 @@ return self.scale * norm_x + self.shift
- **缩放和偏移:** 将可学习的 `scale``shift` 参数应用于归一化输出。
> [!TIP]
> 目标是确保同一标记的所有维度的均值为0方差为1。这样做的目的是**通过减少内部协变量偏移来稳定深度神经网络的训练**,内部协变量偏移是指由于训练过程中更新参数而导致的网络激活分布的变化。
> 目标是确保同一标记的所有维度的均值为0方差为1。这样做的目的是**通过减少内部协变量偏移来稳定深度神经网络的训练**,内部协变量偏移是指由于训练过程中参数更新而导致的网络激活分布的变化。
### **Transformer 块**
@ -350,7 +350,7 @@ return x # Output shape: (batch_size, seq_len, emb_dim)
- **层的组成:** 结合多头注意力、前馈网络、层归一化和残差连接。
- **层归一化:** 在注意力和前馈层之前应用,以实现稳定的训练。
- **残差连接(快捷方式):** 将层的输入添加到其输出,以改善梯度流并使深层网络的训练成为可能。
- **残差连接(快捷方式):** 将层的输入添加到其输出,以改善梯度流并使深层网络的训练成为可能。
- **丢弃法:** 在注意力和前馈层之后应用,以进行正则化。
#### **逐步功能**
@ -585,8 +585,8 @@ pythonCopy codefinal_layer_norm_params = 2 * 768 = 1,536
```
**b. 输出投影层 (`out_head`)**
- **层:** `nn.Linear(emb_dim, vocab_size, bias=False)`
- **参数:** `emb_dim * vocab_size`
- **层** `nn.Linear(emb_dim, vocab_size, bias=False)`
- **参数** `emb_dim * vocab_size`
```python
pythonCopy codeoutput_projection_params = 768 * 50257 = 38,597,376
```

View File

@ -0,0 +1,943 @@
# 6. 预训练与加载模型
## 文本生成
为了训练一个模型,我们需要该模型能够生成新的标记。然后,我们将生成的标记与预期的标记进行比较,以便训练模型**学习它需要生成的标记**。
如前面的例子中,我们已经预测了一些标记,可以重用该功能来实现这个目的。
> [!TIP]
> 第六个阶段的目标非常简单:**从头开始训练模型**。为此将使用之前的LLM架构并通过定义的损失函数和优化器对数据集进行循环以训练模型的所有参数。
## 文本评估
为了进行正确的训练,需要测量检查获得的预测与预期标记的匹配情况。训练的目标是最大化正确标记的可能性,这涉及到相对于其他标记增加其概率。
为了最大化正确标记的概率,必须修改模型的权重,以使该概率最大化。权重的更新是通过**反向传播**完成的。这需要一个**要最大化的损失函数**。在这种情况下,函数将是**执行的预测与期望预测之间的差异**。
然而它将使用以n为底的对数而不是处理原始预测。因此如果当前对预期标记的预测是7.4541e-05则**7.4541e-05**的自然对数(以*e*为底)大约是**-9.5042**。\
然后对于每个具有5个标记上下文长度的条目例如模型需要预测5个标记前4个标记是输入的最后一个第五个是预测的标记。因此在这种情况下对于每个条目我们将有5个预测即使前4个在输入中模型并不知道这一点因此有5个预期标记因此有5个概率需要最大化。
因此,在对每个预测执行自然对数后,计算**平均值**,去掉**负号**这称为_交叉熵损失_这是**需要尽可能接近0的数字**因为1的自然对数是0
<figure><img src="../../images/image (10) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233">https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233</a></p></figcaption></figure>
衡量模型好坏的另一种方法称为困惑度。**困惑度**是用于评估概率模型预测样本的好坏的指标。在语言建模中,它表示模型在预测序列中的下一个标记时的**不确定性**。\
例如困惑度值为48725意味着在需要预测一个标记时它对词汇表中48,725个标记中的哪个是正确的感到不确定。
## 预训练示例
这是在[https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb)中提出的初始代码,有时会稍作修改。
<details>
<summary>这里使用的先前代码,但在前面的部分中已经解释过</summary>
```python
"""
This is code explained before so it won't be exaplained
"""
import tiktoken
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
class GPTDatasetV1(Dataset):
def __init__(self, txt, tokenizer, max_length, stride):
self.input_ids = []
self.target_ids = []
# Tokenize the entire text
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})
# Use a sliding window to chunk the book into overlapping sequences of max_length
for i in range(0, len(token_ids) - max_length, stride):
input_chunk = token_ids[i:i + max_length]
target_chunk = token_ids[i + 1: i + max_length + 1]
self.input_ids.append(torch.tensor(input_chunk))
self.target_ids.append(torch.tensor(target_chunk))
def __len__(self):
return len(self.input_ids)
def __getitem__(self, idx):
return self.input_ids[idx], self.target_ids[idx]
def create_dataloader_v1(txt, batch_size=4, max_length=256,
stride=128, shuffle=True, drop_last=True, num_workers=0):
# Initialize the tokenizer
tokenizer = tiktoken.get_encoding("gpt2")
# Create dataset
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
# Create dataloader
dataloader = DataLoader(
dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last, num_workers=num_workers)
return dataloader
class MultiHeadAttention(nn.Module):
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
super().__init__()
assert d_out % num_heads == 0, "d_out must be divisible by n_heads"
self.d_out = d_out
self.num_heads = num_heads
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.out_proj = nn.Linear(d_out, d_out) # Linear layer to combine head outputs
self.dropout = nn.Dropout(dropout)
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))
def forward(self, x):
b, num_tokens, d_in = x.shape
keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
queries = self.W_query(x)
values = self.W_value(x)
# We implicitly split the matrix by adding a `num_heads` dimension
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
keys = keys.transpose(1, 2)
queries = queries.transpose(1, 2)
values = values.transpose(1, 2)
# Compute scaled dot-product attention (aka self-attention) with a causal mask
attn_scores = queries @ keys.transpose(2, 3) # Dot product for each head
# Original mask truncated to the number of tokens and converted to boolean
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
# Use the mask to fill attention scores
attn_scores.masked_fill_(mask_bool, -torch.inf)
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
attn_weights = self.dropout(attn_weights)
# Shape: (b, num_tokens, num_heads, head_dim)
context_vec = (attn_weights @ values).transpose(1, 2)
# Combine heads, where self.d_out = self.num_heads * self.head_dim
context_vec = context_vec.reshape(b, num_tokens, self.d_out)
context_vec = self.out_proj(context_vec) # optional projection
return context_vec
class LayerNorm(nn.Module):
def __init__(self, emb_dim):
super().__init__()
self.eps = 1e-5
self.scale = nn.Parameter(torch.ones(emb_dim))
self.shift = nn.Parameter(torch.zeros(emb_dim))
def forward(self, x):
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)
norm_x = (x - mean) / torch.sqrt(var + self.eps)
return self.scale * norm_x + self.shift
class GELU(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return 0.5 * x * (1 + torch.tanh(
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
(x + 0.044715 * torch.pow(x, 3))
))
class FeedForward(nn.Module):
def __init__(self, cfg):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
GELU(),
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
)
def forward(self, x):
return self.layers(x)
class TransformerBlock(nn.Module):
def __init__(self, cfg):
super().__init__()
self.att = MultiHeadAttention(
d_in=cfg["emb_dim"],
d_out=cfg["emb_dim"],
context_length=cfg["context_length"],
num_heads=cfg["n_heads"],
dropout=cfg["drop_rate"],
qkv_bias=cfg["qkv_bias"])
self.ff = FeedForward(cfg)
self.norm1 = LayerNorm(cfg["emb_dim"])
self.norm2 = LayerNorm(cfg["emb_dim"])
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
def forward(self, x):
# Shortcut connection for attention block
shortcut = x
x = self.norm1(x)
x = self.att(x) # Shape [batch_size, num_tokens, emb_size]
x = self.drop_shortcut(x)
x = x + shortcut # Add the original input back
# Shortcut connection for feed-forward block
shortcut = x
x = self.norm2(x)
x = self.ff(x)
x = self.drop_shortcut(x)
x = x + shortcut # Add the original input back
return x
class GPTModel(nn.Module):
def __init__(self, cfg):
super().__init__()
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
self.drop_emb = nn.Dropout(cfg["drop_rate"])
self.trf_blocks = nn.Sequential(
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])
self.final_norm = LayerNorm(cfg["emb_dim"])
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
def forward(self, in_idx):
batch_size, seq_len = in_idx.shape
tok_embeds = self.tok_emb(in_idx)
pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]
x = self.drop_emb(x)
x = self.trf_blocks(x)
x = self.final_norm(x)
logits = self.out_head(x)
return logits
```
</details>
```python
# Download contents to train the data with
import os
import urllib.request
file_path = "the-verdict.txt"
url = "https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch02/01_main-chapter-code/the-verdict.txt"
if not os.path.exists(file_path):
with urllib.request.urlopen(url) as response:
text_data = response.read().decode('utf-8')
with open(file_path, "w", encoding="utf-8") as file:
file.write(text_data)
else:
with open(file_path, "r", encoding="utf-8") as file:
text_data = file.read()
total_characters = len(text_data)
tokenizer = tiktoken.get_encoding("gpt2")
total_tokens = len(tokenizer.encode(text_data))
print("Data downloaded")
print("Characters:", total_characters)
print("Tokens:", total_tokens)
# Model initialization
GPT_CONFIG_124M = {
"vocab_size": 50257, # Vocabulary size
"context_length": 256, # Shortened context length (orig: 1024)
"emb_dim": 768, # Embedding dimension
"n_heads": 12, # Number of attention heads
"n_layers": 12, # Number of layers
"drop_rate": 0.1, # Dropout rate
"qkv_bias": False # Query-key-value bias
}
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.eval()
print ("Model initialized")
# Functions to transform from tokens to ids and from to ids to tokens
def text_to_token_ids(text, tokenizer):
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
return encoded_tensor
def token_ids_to_text(token_ids, tokenizer):
flat = token_ids.squeeze(0) # remove batch dimension
return tokenizer.decode(flat.tolist())
# Define loss functions
def calc_loss_batch(input_batch, target_batch, model, device):
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
logits = model(input_batch)
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
return loss
def calc_loss_loader(data_loader, model, device, num_batches=None):
total_loss = 0.
if len(data_loader) == 0:
return float("nan")
elif num_batches is None:
num_batches = len(data_loader)
else:
# Reduce the number of batches to match the total number of batches in the data loader
# if num_batches exceeds the number of batches in the data loader
num_batches = min(num_batches, len(data_loader))
for i, (input_batch, target_batch) in enumerate(data_loader):
if i < num_batches:
loss = calc_loss_batch(input_batch, target_batch, model, device)
total_loss += loss.item()
else:
break
return total_loss / num_batches
# Apply Train/validation ratio and create dataloaders
train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))
train_data = text_data[:split_idx]
val_data = text_data[split_idx:]
torch.manual_seed(123)
train_loader = create_dataloader_v1(
train_data,
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=True,
shuffle=True,
num_workers=0
)
val_loader = create_dataloader_v1(
val_data,
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=False,
shuffle=False,
num_workers=0
)
# Sanity checks
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the training loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"increase the `training_ratio`")
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the validation loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"decrease the `training_ratio`")
print("Train loader:")
for x, y in train_loader:
print(x.shape, y.shape)
print("\nValidation loader:")
for x, y in val_loader:
print(x.shape, y.shape)
train_tokens = 0
for input_batch, target_batch in train_loader:
train_tokens += input_batch.numel()
val_tokens = 0
for input_batch, target_batch in val_loader:
val_tokens += input_batch.numel()
print("Training tokens:", train_tokens)
print("Validation tokens:", val_tokens)
print("All tokens:", train_tokens + val_tokens)
# Indicate the device to use
if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")
print(f"Using {device} device.")
model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes
# Pre-calculate losses without starting yet
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader
with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
train_loss = calc_loss_loader(train_loader, model, device)
val_loss = calc_loss_loader(val_loader, model, device)
print("Training loss:", train_loss)
print("Validation loss:", val_loss)
# Functions to train the data
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
eval_freq, eval_iter, start_context, tokenizer):
# Initialize lists to track losses and tokens seen
train_losses, val_losses, track_tokens_seen = [], [], []
tokens_seen, global_step = 0, -1
# Main training loop
for epoch in range(num_epochs):
model.train() # Set model to training mode
for input_batch, target_batch in train_loader:
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
loss = calc_loss_batch(input_batch, target_batch, model, device)
loss.backward() # Calculate loss gradients
optimizer.step() # Update model weights using loss gradients
tokens_seen += input_batch.numel()
global_step += 1
# Optional evaluation step
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} (Step {global_step:06d}): "
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
# Print a sample text after each epoch
generate_and_print_sample(
model, tokenizer, device, start_context
)
return train_losses, val_losses, track_tokens_seen
def evaluate_model(model, train_loader, val_loader, device, eval_iter):
model.eval()
with torch.no_grad():
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
model.train()
return train_loss, val_loss
def generate_and_print_sample(model, tokenizer, device, start_context):
model.eval()
context_size = model.pos_emb.weight.shape[0]
encoded = text_to_token_ids(start_context, tokenizer).to(device)
with torch.no_grad():
token_ids = generate_text(
model=model, idx=encoded,
max_new_tokens=50, context_size=context_size
)
decoded_text = token_ids_to_text(token_ids, tokenizer)
print(decoded_text.replace("\n", " ")) # Compact print format
model.train()
# Start training!
import time
start_time = time.time()
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
num_epochs = 10
train_losses, val_losses, tokens_seen = train_model_simple(
model, train_loader, val_loader, optimizer, device,
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
start_context="Every effort moves you", tokenizer=tokenizer
)
end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
# Show graphics with the training process
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import math
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
fig, ax1 = plt.subplots(figsize=(5, 3))
ax1.plot(epochs_seen, train_losses, label="Training loss")
ax1.plot(
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
)
ax1.set_xlabel("Epochs")
ax1.set_ylabel("Loss")
ax1.legend(loc="upper right")
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
ax2 = ax1.twiny()
ax2.plot(tokens_seen, train_losses, alpha=0)
ax2.set_xlabel("Tokens seen")
fig.tight_layout()
plt.show()
# Compute perplexity from the loss values
train_ppls = [math.exp(loss) for loss in train_losses]
val_ppls = [math.exp(loss) for loss in val_losses]
# Plot perplexity over tokens seen
plt.figure()
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
plt.xlabel('Tokens Seen')
plt.ylabel('Perplexity')
plt.title('Perplexity over Training')
plt.legend()
plt.show()
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
},
"/tmp/model_and_optimizer.pth"
)
```
让我们逐步解释
### 文本 <--> ids 转换的函数
这些是一些简单的函数,可以用于将词汇中的文本转换为 ids 及其反向转换。这在文本处理的开始和预测的结束时是必要的:
```python
# Functions to transform from tokens to ids and from to ids to tokens
def text_to_token_ids(text, tokenizer):
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
return encoded_tensor
def token_ids_to_text(token_ids, tokenizer):
flat = token_ids.squeeze(0) # remove batch dimension
return tokenizer.decode(flat.tolist())
```
### 生成文本函数
在前面的部分中,一个函数仅在获取 logits 后获取 **最可能的标记**。然而,这意味着对于每个输入,总是会生成相同的输出,这使得它非常确定性。
以下的 `generate_text` 函数将应用 `top-k``temperature``multinomial` 概念。
- **`top-k`** 意味着我们将开始将所有标记的概率减少到 `-inf`,除了前 k 个标记。因此,如果 k=3在做出决策之前只有 3 个最可能的标记的概率将与 `-inf` 不同。
- **`temperature`** 意味着每个概率将被温度值除以。值为 `0.1` 将提高最高概率与最低概率的比较,而温度为 `5` 的情况下,例如将使其更加平坦。这有助于提高我们希望 LLM 具有的响应变化。
- 在应用温度后,再次应用 **`softmax`** 函数,使所有剩余标记的总概率为 1。
- 最后,函数不再选择概率最大的标记,而是应用 **`multinomial`** 来 **根据最终概率预测下一个标记**。因此,如果标记 1 的概率为 70%,标记 2 的概率为 20%,标记 3 的概率为 10%,那么 70% 的时间将选择标记 120% 的时间将选择标记 210% 的时间将选择标记 3。
```python
# Generate text function
def generate_text(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None):
# For-loop is the same as before: Get logits, and only focus on last time step
for _ in range(max_new_tokens):
idx_cond = idx[:, -context_size:]
with torch.no_grad():
logits = model(idx_cond)
logits = logits[:, -1, :]
# New: Filter logits with top_k sampling
if top_k is not None:
# Keep only top_k values
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)
# New: Apply temperature scaling
if temperature > 0.0:
logits = logits / temperature
# Apply softmax to get probabilities
probs = torch.softmax(logits, dim=-1) # (batch_size, context_len)
# Sample from the distribution
idx_next = torch.multinomial(probs, num_samples=1) # (batch_size, 1)
# Otherwise same as before: get idx of the vocab entry with the highest logits value
else:
idx_next = torch.argmax(logits, dim=-1, keepdim=True) # (batch_size, 1)
if idx_next == eos_id: # Stop generating early if end-of-sequence token is encountered and eos_id is specified
break
# Same as before: append sampled index to the running sequence
idx = torch.cat((idx, idx_next), dim=1) # (batch_size, num_tokens+1)
return idx
```
> [!TIP]
> 有一种常见的替代方法叫做 [**`top-p`**](https://en.wikipedia.org/wiki/Top-p_sampling),也称为核采样,它不是获取具有最高概率的 k 个样本,而是 **按概率对所有结果的 **词汇** 进行排序,并从最高概率到最低概率 **累加**,直到达到 **阈值**
>
> 然后,**只有这些词** 的词汇将根据它们的相对概率被考虑。
>
> 这使得不需要选择一个数量为 `k` 的样本,因为最佳的 k 可能在每种情况下都不同,而是 **只需要一个阈值**
>
> _注意这一改进未包含在之前的代码中。_
> [!TIP]
> 改进生成文本的另一种方法是使用 **Beam search**,而不是在此示例中使用的贪婪搜索。\
> 与贪婪搜索不同,贪婪搜索在每一步选择最可能的下一个词并构建单一序列,**beam search 在每一步跟踪得分最高的 𝑘 k 个部分序列**(称为“光束”)。通过同时探索多种可能性,它在效率和质量之间取得平衡,增加了 **找到更好整体** 序列的机会,这可能由于早期的次优选择而被贪婪方法错过。
>
> _注意这一改进未包含在之前的代码中。_
### Loss functions
**`calc_loss_batch`** 函数计算单个批次预测的交叉熵。\
**`calc_loss_loader`** 获取所有批次的交叉熵并计算 **平均交叉熵**
```python
# Define loss functions
def calc_loss_batch(input_batch, target_batch, model, device):
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
logits = model(input_batch)
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
return loss
def calc_loss_loader(data_loader, model, device, num_batches=None):
total_loss = 0.
if len(data_loader) == 0:
return float("nan")
elif num_batches is None:
num_batches = len(data_loader)
else:
# Reduce the number of batches to match the total number of batches in the data loader
# if num_batches exceeds the number of batches in the data loader
num_batches = min(num_batches, len(data_loader))
for i, (input_batch, target_batch) in enumerate(data_loader):
if i < num_batches:
loss = calc_loss_batch(input_batch, target_batch, model, device)
total_loss += loss.item()
else:
break
return total_loss / num_batches
```
> [!TIP]
> **梯度裁剪**是一种用于增强大型神经网络**训练稳定性**的技术,通过设置梯度幅度的**最大阈值**来实现。当梯度超过这个预定义的`max_norm`时,它们会按比例缩小,以确保对模型参数的更新保持在可管理的范围内,防止出现梯度爆炸等问题,并确保更可控和稳定的训练。
>
> _请注意这一改进未包含在之前的代码中。_
>
> 查看以下示例:
<figure><img src="../../images/image (6) (1).png" alt=""><figcaption></figcaption></figure>
### 加载数据
函数`create_dataloader_v1``create_dataloader_v1`在前面的部分已经讨论过。
从这里可以注意到定义了90%的文本将用于训练而10%将用于验证,两个数据集存储在两个不同的数据加载器中。\
请注意,有时数据集的一部分也会留作测试集,以更好地评估模型的性能。
两个数据加载器使用相同的批量大小、最大长度、步幅和工作线程数在这种情况下为0。\
主要的区别在于每个加载器使用的数据,以及验证器不会丢弃最后一部分数据,也不会打乱数据,因为这对验证目的并不需要。
此外,**步幅与上下文长度一样大**,意味着用于训练数据的上下文之间不会重叠(减少过拟合,但也减少训练数据集)。
此外请注意在这种情况下批量大小为2以将数据分为2个批次主要目的是允许并行处理并减少每个批次的消耗。
```python
train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))
train_data = text_data[:split_idx]
val_data = text_data[split_idx:]
torch.manual_seed(123)
train_loader = create_dataloader_v1(
train_data,
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=True,
shuffle=True,
num_workers=0
)
val_loader = create_dataloader_v1(
val_data,
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=False,
shuffle=False,
num_workers=0
)
```
## Sanity Checks
目标是检查是否有足够的用于训练的 tokens形状是否符合预期并获取有关用于训练和验证的 tokens 数量的一些信息:
```python
# Sanity checks
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the training loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"increase the `training_ratio`")
if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the validation loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"decrease the `training_ratio`")
print("Train loader:")
for x, y in train_loader:
print(x.shape, y.shape)
print("\nValidation loader:")
for x, y in val_loader:
print(x.shape, y.shape)
train_tokens = 0
for input_batch, target_batch in train_loader:
train_tokens += input_batch.numel()
val_tokens = 0
for input_batch, target_batch in val_loader:
val_tokens += input_batch.numel()
print("Training tokens:", train_tokens)
print("Validation tokens:", val_tokens)
print("All tokens:", train_tokens + val_tokens)
```
### 选择用于训练和预计算的设备
以下代码仅选择要使用的设备,并计算训练损失和验证损失(在尚未进行任何训练的情况下)作为起点。
```python
# Indicate the device to use
if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")
print(f"Using {device} device.")
model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes
# Pre-calculate losses without starting yet
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader
with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
train_loss = calc_loss_loader(train_loader, model, device)
val_loss = calc_loss_loader(val_loader, model, device)
print("Training loss:", train_loss)
print("Validation loss:", val_loss)
```
### 训练函数
函数 `generate_and_print_sample` 将获取一个上下文并生成一些标记,以便了解模型在该时刻的表现。这由 `train_model_simple` 在每一步调用。
函数 `evaluate_model` 会根据训练函数的指示频繁调用,用于测量模型训练时的训练损失和验证损失。
然后,主要的函数 `train_model_simple` 实际上是训练模型的函数。它期望:
- 训练数据加载器(数据已分离并准备好进行训练)
- 验证器加载器
- 训练期间使用的 **优化器**:这是将使用梯度并更新参数以减少损失的函数。在这种情况下,如你所见,使用的是 `AdamW`,但还有许多其他选择。
- 调用 `optimizer.zero_grad()` 以在每轮重置梯度,防止它们累积。
- **`lr`** 参数是 **学习率**,决定在优化过程中更新模型参数时采取的 **步长大小**。较 **小** 的学习率意味着优化器对权重进行 **较小的更新**,这可能导致更 **精确** 的收敛,但可能会 **减慢** 训练。较 **大** 的学习率可以加快训练,但 **有可能超出** 损失函数的最小值(**跳过** 损失函数最小化的点)。
- **权重衰减** 通过添加一个额外的项来修改 **损失计算** 步骤,以惩罚大权重。这鼓励优化器找到具有较小权重的解决方案,在良好拟合数据和保持模型简单之间取得平衡,防止机器学习模型过拟合,避免模型对任何单一特征赋予过多重要性。
- 传统优化器如带有 L2 正则化的 SGD 将权重衰减与损失函数的梯度结合。然而,**AdamW**Adam 优化器的一个变体)将权重衰减与梯度更新解耦,从而实现更有效的正则化。
- 用于训练的设备
- 训练轮数:遍历训练数据的次数
- 评估频率:调用 `evaluate_model` 的频率
- 评估迭代:在调用 `generate_and_print_sample` 时评估模型当前状态时使用的批次数
- 开始上下文:调用 `generate_and_print_sample` 时使用的起始句子
- 分词器
```python
# Functions to train the data
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
eval_freq, eval_iter, start_context, tokenizer):
# Initialize lists to track losses and tokens seen
train_losses, val_losses, track_tokens_seen = [], [], []
tokens_seen, global_step = 0, -1
# Main training loop
for epoch in range(num_epochs):
model.train() # Set model to training mode
for input_batch, target_batch in train_loader:
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
loss = calc_loss_batch(input_batch, target_batch, model, device)
loss.backward() # Calculate loss gradients
optimizer.step() # Update model weights using loss gradients
tokens_seen += input_batch.numel()
global_step += 1
# Optional evaluation step
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} (Step {global_step:06d}): "
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")
# Print a sample text after each epoch
generate_and_print_sample(
model, tokenizer, device, start_context
)
return train_losses, val_losses, track_tokens_seen
def evaluate_model(model, train_loader, val_loader, device, eval_iter):
model.eval() # Set in eval mode to avoid dropout
with torch.no_grad():
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
model.train() # Back to training model applying all the configurations
return train_loss, val_loss
def generate_and_print_sample(model, tokenizer, device, start_context):
model.eval() # Set in eval mode to avoid dropout
context_size = model.pos_emb.weight.shape[0]
encoded = text_to_token_ids(start_context, tokenizer).to(device)
with torch.no_grad():
token_ids = generate_text(
model=model, idx=encoded,
max_new_tokens=50, context_size=context_size
)
decoded_text = token_ids_to_text(token_ids, tokenizer)
print(decoded_text.replace("\n", " ")) # Compact print format
model.train() # Back to training model applying all the configurations
```
> [!TIP]
> 为了提高学习率,有几个相关的技术叫做 **linear warmup** 和 **cosine decay.**
>
> **Linear warmup** 的定义是设定一个初始学习率和一个最大学习率,并在每个 epoch 后持续更新。这是因为以较小的权重更新开始训练可以减少模型在训练阶段遇到大且不稳定更新的风险。\
> **Cosine decay** 是一种技术,它在 **warmup** 阶段后 **逐渐减少学习率**,遵循半余弦曲线,减缓权重更新以 **最小化超调** 损失最小值的风险,并确保后期训练的稳定性。
>
> _请注意这些改进未包含在之前的代码中。_
### Start training
```python
import time
start_time = time.time()
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)
num_epochs = 10
train_losses, val_losses, tokens_seen = train_model_simple(
model, train_loader, val_loader, optimizer, device,
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
start_context="Every effort moves you", tokenizer=tokenizer
)
end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"Training completed in {execution_time_minutes:.2f} minutes.")
```
### 打印训练演变
使用以下函数,可以打印模型在训练过程中的演变。
```python
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import math
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
fig, ax1 = plt.subplots(figsize=(5, 3))
ax1.plot(epochs_seen, train_losses, label="Training loss")
ax1.plot(
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
)
ax1.set_xlabel("Epochs")
ax1.set_ylabel("Loss")
ax1.legend(loc="upper right")
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
ax2 = ax1.twiny()
ax2.plot(tokens_seen, train_losses, alpha=0)
ax2.set_xlabel("Tokens seen")
fig.tight_layout()
plt.show()
# Compute perplexity from the loss values
train_ppls = [math.exp(loss) for loss in train_losses]
val_ppls = [math.exp(loss) for loss in val_losses]
# Plot perplexity over tokens seen
plt.figure()
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
plt.xlabel('Tokens Seen')
plt.ylabel('Perplexity')
plt.title('Perplexity over Training')
plt.legend()
plt.show()
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)
```
### 保存模型
如果您想稍后继续训练,可以保存模型和优化器:
```python
# Save the model and the optimizer for later training
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
},
"/tmp/model_and_optimizer.pth"
)
# Note that this model with the optimizer occupied close to 2GB
# Restore model and optimizer for training
checkpoint = torch.load("/tmp/model_and_optimizer.pth", map_location=device)
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(); # Put in training mode
```
或者仅仅是模型,如果您只打算使用它:
```python
# Save the model
torch.save(model.state_dict(), "model.pth")
# Load it
model = GPTModel(GPT_CONFIG_124M)
model.load_state_dict(torch.load("model.pth", map_location=device))
model.eval() # Put in eval mode
```
## 加载 GPT2 权重
有两个快速脚本可以在本地加载 GPT2 权重。对于这两个脚本,您可以在本地克隆仓库 [https://github.com/rasbt/LLMs-from-scratch](https://github.com/rasbt/LLMs-from-scratch),然后:
- 脚本 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py) 将下载所有权重并将格式从 OpenAI 转换为我们 LLM 所期望的格式。该脚本还准备了所需的配置和提示:“每一次努力都在推动你”
- 脚本 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb) 允许您在本地加载任何 GPT2 权重(只需更改 `CHOOSE_MODEL` 变量)并从一些提示中预测文本。
## 参考文献
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)

View File

@ -0,0 +1,110 @@
# 7.1. Fine-Tuning for Classification
## What is
微调是将一个**预训练模型**的过程,该模型已经从大量数据中学习了**通用语言模式**,并将其**调整**以执行**特定任务**或理解特定领域的语言。这是通过在一个较小的、特定任务的数据集上继续训练模型来实现的,使其能够调整参数以更好地适应新数据的细微差别,同时利用其已经获得的广泛知识。微调使模型能够在专业应用中提供更准确和相关的结果,而无需从头开始训练一个新模型。
> [!TIP]
> 由于预训练一个“理解”文本的LLM相当昂贵因此通常更容易和便宜地微调开源的预训练模型以执行我们希望其执行的特定任务。
> [!TIP]
> 本节的目标是展示如何微调一个已经预训练的模型因此LLM将选择给出**给定文本被分类到每个给定类别的概率**(例如,文本是否为垃圾邮件)。
## Preparing the data set
### Data set size
当然为了微调模型您需要一些结构化数据来专门化您的LLM。在[https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb)中提出的示例中GPT2被微调以检测电子邮件是否为垃圾邮件使用的数据来自[https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip](https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip)_。
该数据集包含的“非垃圾邮件”示例远多于“垃圾邮件”示例,因此本书建议**仅使用与“垃圾邮件”相同数量的“非垃圾邮件”示例**因此从训练数据中删除所有额外示例。在这种情况下每种情况有747个示例。
然后,**70%**的数据集用于**训练****10%**用于**验证****20%**用于**测试**。
- **验证集**在训练阶段用于微调模型的**超参数**并做出关于模型架构的决策,有效地通过提供模型在未见数据上的表现反馈来帮助防止过拟合。它允许在不偏倚最终评估的情况下进行迭代改进。
- 这意味着尽管此数据集中包含的数据不直接用于训练,但它用于调整最佳**超参数**,因此该集不能像测试集那样用于评估模型的性能。
- 相比之下,**测试集**仅在模型完全训练并完成所有调整后使用;它提供了对模型在新未见数据上泛化能力的无偏评估。对测试集的最终评估提供了模型在实际应用中预期表现的现实指示。
### Entries length
由于训练示例期望条目(在这种情况下为电子邮件文本)具有相同的长度,因此决定通过添加`<|endoftext|>`的ID作为填充使每个条目与最大条目一样大。
### Initialize the model
使用开源预训练权重初始化模型以进行训练。我们之前已经做过这个,并按照[https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb)的说明,您可以轻松做到这一点。
## Classification head
在这个特定示例中预测文本是否为垃圾邮件我们并不关心根据GPT2的完整词汇进行微调而只希望新模型能够判断电子邮件是否为垃圾邮件1或不是0。因此我们将**修改最终层**使其提供每个词汇的token的概率改为仅提供是否为垃圾邮件的概率就像一个包含2个单词的词汇
```python
# This code modified the final layer with a Linear one with 2 outs
num_classes = 2
model.out_head = torch.nn.Linear(
in_features=BASE_CONFIG["emb_dim"],
out_features=num_classes
)
```
## 参数调整
为了快速微调,最好只微调一些最终参数,而不是所有参数。这是因为已知较低层通常捕捉基本的语言结构和适用的语义。因此,仅**微调最后几层通常就足够且更快**。
```python
# This code makes all the parameters of the model unrtainable
for param in model.parameters():
param.requires_grad = False
# Allow to fine tune the last layer in the transformer block
for param in model.trf_blocks[-1].parameters():
param.requires_grad = True
# Allow to fine tune the final layer norm
for param in model.final_norm.parameters():
param.requires_grad = True
```
## 用于训练的条目
在之前的部分中LLM通过减少每个预测标记的损失进行训练尽管几乎所有预测的标记都在输入句子中只有最后一个是真正预测的以便模型更好地理解语言。
在这种情况下,我们只关心模型是否能够预测该模型是否为垃圾邮件,因此我们只关心最后一个预测的标记。因此,需要修改我们之前的训练损失函数,仅考虑该标记。
这在 [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/ch06.ipynb) 中实现为:
```python
def calc_accuracy_loader(data_loader, model, device, num_batches=None):
model.eval()
correct_predictions, num_examples = 0, 0
if num_batches is None:
num_batches = len(data_loader)
else:
num_batches = min(num_batches, len(data_loader))
for i, (input_batch, target_batch) in enumerate(data_loader):
if i < num_batches:
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
with torch.no_grad():
logits = model(input_batch)[:, -1, :] # Logits of last output token
predicted_labels = torch.argmax(logits, dim=-1)
num_examples += predicted_labels.shape[0]
correct_predictions += (predicted_labels == target_batch).sum().item()
else:
break
return correct_predictions / num_examples
def calc_loss_batch(input_batch, target_batch, model, device):
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
logits = model(input_batch)[:, -1, :] # Logits of last output token
loss = torch.nn.functional.cross_entropy(logits, target_batch)
return loss
```
注意,对于每个批次,我们只对**最后一个预测的标记的logits**感兴趣。
## 完整的GPT2微调分类代码
您可以在[https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/load-finetuned-model.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch06/01_main-chapter-code/load-finetuned-model.ipynb)找到所有微调GPT2以成为垃圾邮件分类器的代码。
## 参考文献
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)

View File

@ -56,7 +56,7 @@ print(model_input + desired_response)
然后,需要对所有输入和期望输出进行批处理以进行训练。为此,需要:
- 对文本进行标记化
- 将所有样本填充到相同的长度通常长度将与用于预训练LLM的上下文长度相同
- 将所有样本填充到相同的长度通常长度将与用于预训练LLM的上下文长度一样大
- 在自定义合并函数中将输入向右移动1以创建期望的标记
- 用-100替换一些填充标记以将其排除在训练损失之外在第一个`endoftext`标记之后,将所有其他`endoftext`标记替换为-100因为使用`cross_entropy(...,ignore_index=-100)`意味着它将忽略目标为-100的情况
- \[可选\] 使用-100掩盖所有属于问题的标记以便LLM仅学习如何生成答案。在应用Alpaca风格时这将意味着掩盖所有内容直到`### Response:`
@ -84,7 +84,7 @@ print(model_input + desired_response)
5. [**SuperGLUE**](https://super.gluebenchmark.com/)**** 在GLUE的基础上SuperGLUE包括更具挑战性的任务旨在对当前模型构成困难。
6. **超越模仿游戏基准(**[**BIG-bench**](https://github.com/google/BIG-bench)**** BIG-bench是一个大规模基准包含200多个任务测试模型在推理、翻译和问答等领域的能力。
7. **语言模型的整体评估(**[**HELM**](https://crfm.stanford.edu/helm/lite/latest/)**** HELM提供了在准确性、鲁棒性和公平性等各种指标上的全面评估。
8. [**OpenAI Evals**](https://github.com/openai/evals)**** OpenAI的开源评估框架允许在自定义和标准化任务上测试AI模型
8. [**OpenAI Evals**](https://github.com/openai/evals)**** OpenAI的开源评估框架允许对AI模型进行自定义和标准化任务的测试
9. [**HumanEval**](https://github.com/openai/human-eval)**** 一系列编程问题,用于评估语言模型的代码生成能力。
10. **斯坦福问答数据集(**[**SQuAD**](https://rajpurkar.github.io/SQuAD-explorer/)**** SQuAD由关于维基百科文章的问题组成模型必须理解文本以准确回答。
11. [**TriviaQA**](https://nlp.cs.washington.edu/triviaqa/)**** 一个大规模的琐事问题和答案数据集,以及证据文档。

View File

@ -1,6 +1,6 @@
# LLM 训练 - 数据准备
**这些是我从非常推荐的书中做的笔记** [**https://www.manning.com/books/build-a-large-language-model-from-scratch**](https://www.manning.com/books/build-a-large-language-model-from-scratch) **以及一些额外的信息。**
**这些是我从非常推荐的书中做的笔记** [**https://www.manning.com/books/build-a-large-language-model-from-scratch**](https://www.manning.com/books/build-a-large-language-model-from-scratch) **以及一些额外的信息。**
## 基本信息
@ -13,7 +13,7 @@
## 1. 分词
> [!TIP]
> 这个初始阶段的目标非常简单:**以某种有意义的方式将输入分割成标记ID。**
> 这个初始阶段的目标非常简单:**以某种有意义的方式将输入划分为标记ID。**
{{#ref}}
1.-tokenizing.md
@ -22,7 +22,7 @@
## 2. 数据采样
> [!TIP]
> 这个第二阶段的目标非常简单:**对输入数据进行采样,并为训练阶段准备数据,通常通过将数据集分成特定长度的句子,并生成预期的响应。**
> 这个第二阶段的目标非常简单:**对输入数据进行采样,并为训练阶段准备,通常通过将数据集分成特定长度的句子,并生成预期的响应。**
{{#ref}}
2.-data-sampling.md
@ -40,11 +40,11 @@
3.-token-embeddings.md
{{#endref}}
## 4. 注意机制
## 4. 注意机制
> [!TIP]
> 这个第四阶段的目标非常简单:**应用一些注意机制**。这些将是许多**重复的层**,将**捕捉词汇表中单词与当前用于训练LLM的句子中其邻居的关系**。\
> 为此使用了许多层,因此许多可训练的参数将捕捉这些信息。
> 这个第四阶段的目标非常简单:**应用一些注意机制**。这些将是许多**重复的层**,将**捕捉词汇表中单词与当前用于训练 LLM 的句子中其邻居的关系**。\
> 为此使用了很多层,因此将有很多可训练的参数来捕捉这些信息。
{{#ref}}
4.-attention-mechanisms.md
@ -53,7 +53,7 @@
## 5. LLM 架构
> [!TIP]
> 这个第五阶段的目标非常简单:**开发完整LLM的架构**。将所有内容整合在一起应用所有层并创建所有函数以生成文本或将文本转换为ID及反向操作。
> 这个第五阶段的目标非常简单:**开发完整 LLM 的架构**。将所有内容整合在一起,应用所有层并创建所有函数以生成文本或将文本转换为 ID 反向操作。
>
> 该架构将用于训练和预测文本。
@ -82,7 +82,7 @@
## 7.1. 分类的微调
> [!TIP]
> 本节的目标是展示如何微调一个已经预训练的模型,以便LLM选择给定文本被分类到每个给定类别的**概率**(例如,文本是否为垃圾邮件)。
> 本节的目标是展示如何微调一个已经预训练的模型,以便 LLM 不再生成新文本,而是给出**给定文本被分类到每个给定类别的概率**(例如,文本是否为垃圾邮件)。
{{#ref}}
7.1.-fine-tuning-for-classification.md