33. 更具多样性的生成策略#

33.1. 介绍#

在上一节中,我们完成了 GPT 模型的训练,但是在后几个评估 Epoch 中,我们发现模型生成的文本是重复的,这说明模型生成内容时少了一些多样性。本小节将介绍如何通过调整生成策略,使模型生成的文本更加多样化。

33.2. 环境配置#

33.2.1. 安装依赖#

!pip install --upgrade dsxllm

33.2.2. 环境版本#

from dsxllm.util import show_version

show_version()
本书愿景:
+------+--------------------------------------------------------+
| Info |                  《动手学大语言模型》                  |
+------+--------------------------------------------------------+
| 作者 |                       吾辈亦有感                       |
| 哔站 |      https://space.bilibili.com/3546632320715420       |
| 定位 | 基于'从零构建'的理念,用实战帮助程序员快速入门大模型。 |
| 愿景 | 若让你的AI学习之路走的更容易一点,我将倍感荣幸!祝好😄 |
+------+--------------------------------------------------------+
环境信息:
+-------------+--------------+------------------------+
| Python 版本 | PyTorch 版本 | PyTorch Lightning 版本 |
+-------------+--------------+------------------------+
|   3.12.12   |    2.10.0    |         2.6.1          |
+-------------+--------------+------------------------+

33.3. 训练 GPT 模型#

import lightning as L

import tiktoken

from dsxllm.gpt.dataset import GPTDataModule
from dsxllm.gpt.model import GPTModel, SampleTestCallback


# 定义GPT模型的配置参数(对应124M参数版本的GPT)
GPT_CONFIG_124M = {
    "vocab_size": 50257,  # 词汇表大小,即模型可以表示的不同token的数量
    "seq_len": 256,  # 上下文长度(原为1024,此处缩短为256),决定模型一次处理的最大序列长度
    "d_model": 768,  # 嵌入维度(embedding dimension),每个token被映射到的向量维度
    "n_heads": 12,  # 注意力头数量(number of attention heads),用于多头注意力机制
    "n_layers": 12,  # 层数,Transformer块的数量
    "drop_rate": 0.1,  # Dropout率,用于防止过拟合的概率值
    "qkv_bias": False,  # 查询-键-值偏置(query-key-value bias),是否在注意力计算中使用偏置项
}

# 超参配置:
num_epochs = 10
batch_size = 4
max_length = GPT_CONFIG_124M["seq_len"]
stride = GPT_CONFIG_124M["seq_len"]


# 1️⃣ 初始化 GPT-2 使用的分词器
tokenizer = tiktoken.get_encoding("gpt2")

# 3️⃣ 加载数据模组
data_filepath = "./dataset/llm_corpus.txt"
datamodule = GPTDataModule(
    batch_size=batch_size,
    tokenizer=tokenizer,
    train_data_file=data_filepath,
    max_length=max_length,
    stride=stride,
)

# 4️⃣ 初始化模型
model = GPTModel(GPT_CONFIG_124M)

# 5️⃣ 初始化训练器
trainer = L.Trainer(
    max_epochs=20,
    log_every_n_steps=3,
    check_val_every_n_epoch=1,
    num_sanity_val_steps=0,
    enable_progress_bar=False,
    callbacks=[SampleTestCallback(tokenizer, context_size=max_length)],
    enable_checkpointing=False,
)

# 6️⃣ 训练模型
trainer.fit(model=model, datamodule=datamodule)
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
💡 Tip: For seamless cloud logging and experiment tracking, try installing [litlogger](https://pypi.org/project/litlogger/) to enable LitLogger, which logs metrics and artifacts automatically to the Lightning Experiments platform.
┏━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃    Name                Type        Params  Mode    FLOPs       In sizes        Out sizes ┃
┡━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ 0 │ token_embedding    │ Embedding  │ 38.6 M │ train │      0 │      [2, 256]    [2, 256, 768] │
│ 1 │ position_embedding │ Embedding  │  196 K │ train │      0 │         [256]       [256, 768] │
│ 2 │ embedding_dropout  │ Dropout    │      0 │ train │      0 │ [2, 256, 768]    [2, 256, 768] │
│ 3 │ transformer_blocks │ Sequential │  113 M │ train │  120 B │ [2, 256, 768]    [2, 256, 768] │
│ 4 │ final_layer_norm   │ LayerNorm  │  1.5 K │ train │      0 │ [2, 256, 768]    [2, 256, 768] │
│ 5 │ lm_head            │ Linear     │ 38.6 M │ train │ 39.5 B │ [2, 256, 768]  [2, 256, 50257] │
└───┴────────────────────┴────────────┴────────┴───────┴────────┴───────────────┴─────────────────┘
Trainable params: 190 M                                                                                            
Non-trainable params: 0                                                                                            
Total params: 190 M                                                                                                
Total estimated model params size (MB): 762                                                                        
Modules in train mode: 186                                                                                         
Modules in eval mode: 0                                                                                            
Total FLOPs: 160 B                                                                                                 
生成测试:
+-------+-------------+----------------+
| Epoch | Start_Token | Generated_Text |
+-------+-------------+----------------+
|   0   |      I      |  I,,,,,,,,,,   |
+-------+-------------+----------------+
***** 【Epoch 0】 Val Avg Loss: 8.6662 *****
***** 【Epoch 0】 Train Avg Loss: 9.8946 *****
生成测试:
+-------+-------------+----------------+
| Epoch | Start_Token | Generated_Text |
+-------+-------------+----------------+
|   1   |      I      |  I, the, the.  |
|       |             |                |
|       |             |                |
|       |             |                |
|       |             |                |
|       |             |                |
+-------+-------------+----------------+
***** 【Epoch 1】 Val Avg Loss: 7.3752 *****
***** 【Epoch 1】 Train Avg Loss: 7.8780 *****
生成测试:
+-------+-------------+----------------------------+
| Epoch | Start_Token |       Generated_Text       |
+-------+-------------+----------------------------+
|   2   |      I      | I, and, and, and, and, and |
+-------+-------------+----------------------------+
***** 【Epoch 2】 Val Avg Loss: 6.5450 *****
***** 【Epoch 2】 Train Avg Loss: 6.3474 *****
生成测试:
+-------+-------------+-----------------------+
| Epoch | Start_Token |     Generated_Text    |
+-------+-------------+-----------------------+
|   3   |      I      | I-------------------- |
+-------+-------------+-----------------------+
***** 【Epoch 3】 Val Avg Loss: 6.4690 *****
***** 【Epoch 3】 Train Avg Loss: 6.8890 *****
生成测试:
+-------+-------------+----------------------------+
| Epoch | Start_Token |       Generated_Text       |
+-------+-------------+----------------------------+
|   4   |      I      | I, the, the, and, and, the |
+-------+-------------+----------------------------+
***** 【Epoch 4】 Val Avg Loss: 6.2576 *****
***** 【Epoch 4】 Train Avg Loss: 4.7287 *****
生成测试:
+-------+-------------+-------------------------------------+
| Epoch | Start_Token |            Generated_Text           |
+-------+-------------+-------------------------------------+
|   5   |      I      | I he had the, and I had a little of |
+-------+-------------+-------------------------------------+
***** 【Epoch 5】 Val Avg Loss: 6.1180 *****
***** 【Epoch 5】 Train Avg Loss: 4.0857 *****
生成测试:
+-------+-------------+--------------------------------------+
| Epoch | Start_Token |            Generated_Text            |
+-------+-------------+--------------------------------------+
|   6   |      I      | I he had been, and I was a little of |
+-------+-------------+--------------------------------------+
***** 【Epoch 6】 Val Avg Loss: 6.1419 *****
***** 【Epoch 6】 Train Avg Loss: 3.3987 *****
生成测试:
+-------+-------------+-------------------------------+
| Epoch | Start_Token |         Generated_Text        |
+-------+-------------+-------------------------------+
|   7   |      I      | I had the sun of the picture. |
|       |             |                               |
|       |             |                               |
|       |             |                               |
+-------+-------------+-------------------------------+
***** 【Epoch 7】 Val Avg Loss: 6.1154 *****
***** 【Epoch 7】 Train Avg Loss: 2.8955 *****
生成测试:
+-------+-------------+-----------------------------+
| Epoch | Start_Token |        Generated_Text       |
+-------+-------------+-----------------------------+
|   8   |      I      | I could the sunlit terrace. |
|       |             |                             |
|       |             |                             |
|       |             |                             |
+-------+-------------+-----------------------------+
***** 【Epoch 8】 Val Avg Loss: 6.2984 *****
***** 【Epoch 8】 Train Avg Loss: 2.1331 *****
生成测试:
+-------+-------------+---------------------------+
| Epoch | Start_Token |       Generated_Text      |
+-------+-------------+---------------------------+
|   9   |      I      | I had the sunlit terrace. |
|       |             |                           |
|       |             |                           |
|       |             |                           |
+-------+-------------+---------------------------+
***** 【Epoch 9】 Val Avg Loss: 6.2883 *****
***** 【Epoch 9】 Train Avg Loss: 1.5858 *****
生成测试:
+-------+-------------+------------------------+
| Epoch | Start_Token |     Generated_Text     |
+-------+-------------+------------------------+
|   10  |      I      | I, the sunlit terrace. |
|       |             |                        |
|       |             |                        |
|       |             |                        |
+-------+-------------+------------------------+
***** 【Epoch 10】 Val Avg Loss: 6.3661 *****
***** 【Epoch 10】 Train Avg Loss: 1.1139 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   11  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 11】 Val Avg Loss: 6.4084 *****
***** 【Epoch 11】 Train Avg Loss: 0.7803 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   12  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 12】 Val Avg Loss: 6.5458 *****
***** 【Epoch 12】 Train Avg Loss: 0.5518 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   13  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 13】 Val Avg Loss: 6.5581 *****
***** 【Epoch 13】 Train Avg Loss: 0.4093 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   14  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 14】 Val Avg Loss: 6.6557 *****
***** 【Epoch 14】 Train Avg Loss: 0.3129 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   15  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 15】 Val Avg Loss: 6.7371 *****
***** 【Epoch 15】 Train Avg Loss: 0.2637 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   16  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 16】 Val Avg Loss: 6.7717 *****
***** 【Epoch 16】 Train Avg Loss: 0.2326 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   17  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 17】 Val Avg Loss: 6.8461 *****
***** 【Epoch 17】 Train Avg Loss: 0.2052 *****
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   18  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 18】 Val Avg Loss: 6.8152 *****
***** 【Epoch 18】 Train Avg Loss: 0.1853 *****
`Trainer.fit` stopped: `max_epochs=20` reached.
生成测试:
+-------+-------------+--------------------------------------------+
| Epoch | Start_Token |               Generated_Text               |
+-------+-------------+--------------------------------------------+
|   19  |      I      | I HAD always thought Jack Gisburn rather a |
+-------+-------------+--------------------------------------------+
***** 【Epoch 19】 Val Avg Loss: 6.8078 *****
***** 【Epoch 19】 Train Avg Loss: 0.1731 *****

33.4. 模型生成中存在的问题#

使用训练好的 GPT 模型多次生成文本,观察模型生成的结果。

from dsxllm.util import print_table
from dsxllm.gpt.model import text_to_token_ids, token_ids_to_text
from dsxllm.gpt.model import greedy_generate

# 将模型移动到 CPU 上进行推理
model.to("cpu")

# 设置模型为评估模式(关闭 dropout 和 batch normalization 的训练行为)
model.eval()

# 初始化 GPT-2 分词器
tokenizer = tiktoken.get_encoding("gpt2")

# 使用训练好的模型生成并打印 5 次文本
generate_samples = []
for i in range(5):
    # 调用 greedy_generate 函数生成文本
    generated_text = greedy_generate(
        model=model,
        start_ids=text_to_token_ids("I", tokenizer),
        max_new_tokens=10,
        context_size=GPT_CONFIG_124M["seq_len"],
        tokenizer=tokenizer,
    )

    # 将生成的 token ID 序列解码为可读文本
    generate_samples.append((i, generated_text))

print_table("模型生成效果", field_names=["生成次数", "生成结果"], data=generate_samples)
模型生成效果:
+----------+--------------------------------------------+
| 生成次数 |                  生成结果                  |
+----------+--------------------------------------------+
|    0     | I HAD always thought Jack Gisburn rather a |
|    1     | I HAD always thought Jack Gisburn rather a |
|    2     | I HAD always thought Jack Gisburn rather a |
|    3     | I HAD always thought Jack Gisburn rather a |
|    4     | I HAD always thought Jack Gisburn rather a |
+----------+--------------------------------------------+

我们以 I 开头生成了 5 次文本,发现每次生成的结果都是 I HAD always thought Jack Gisburn rather a,GPT 模型生成文本时,缺少了一些多样性。因为我们选择下一次生成的 token 时,只选择了概率最高的 token,而没有考虑其他可能的 token。

33.5. 如何增加模型生成的多样性?#

想要提高生成文本的多样性,我们可以使用概率采样策略代替贪心解码策略。

  • 贪心解码策略:模型生成下一个词元时,总是选择概率最高的词元作为下一个词元。

  • 概率采样策略:模型生成下一个词元时,根据概率分数进行采样,而不是选择概率最高的词元作为下一个词元。

下面使用一个示例来演示概率采样策略的效果。

33.5.1. 创建示例数据#

import torch

# 示例数据(请根据你的实际数据替换)
vocab = {
    "动": 0,
    "手": 1,
    "学": 2,
    ":": 3,
    "大": 4,
    "语": 5,
    "言": 6,
    "模": 7,
    "型": 8,
}

# 创建反向词汇表,用于从索引获取词
reverse_vocab = {v: k for k, v in vocab.items()}

# 定义下一个 token 的 logits(模型输出的原始值)
next_token_logits = torch.tensor(
    [4.51, 0.89, -1.90, 2.5, 1.63, -1.62, -1.89, 6.28, 1.79]
)

33.5.2. 贪婪解码策略#

使用贪婪解码策略,模型会在每一步选择概率最高的词元作为下一个词元。

# 使用贪婪解码选择 10 次
for i in range(10):
    next_token_index = torch.argmax(next_token_logits)
    # 根据索引获取对应的词
    next_token_word = reverse_vocab[next_token_index.item()]
    print(f"【贪婪解码策略】第 {i + 1} 次选择:索引={next_token_index.item()}, 词='{next_token_word}'")
【贪婪解码策略】第 1 次选择:索引=7, 词='模'
【贪婪解码策略】第 2 次选择:索引=7, 词='模'
【贪婪解码策略】第 3 次选择:索引=7, 词='模'
【贪婪解码策略】第 4 次选择:索引=7, 词='模'
【贪婪解码策略】第 5 次选择:索引=7, 词='模'
【贪婪解码策略】第 6 次选择:索引=7, 词='模'
【贪婪解码策略】第 7 次选择:索引=7, 词='模'
【贪婪解码策略】第 8 次选择:索引=7, 词='模'
【贪婪解码策略】第 9 次选择:索引=7, 词='模'
【贪婪解码策略】第 10 次选择:索引=7, 词='模'

33.5.3. 概率采样策略#

使用概率采样策略,模型会根据每个词元的概率分布随机选择下一个词元(概率越高的词越有可能被选择,但是概率低的词也有被选择的可能)。这种策略可以引入更多的随机性,使生成的文本更加多样化。

# 使用概率采样选择 10 次
for i in range(10):
    next_token_index = torch.multinomial(torch.softmax(next_token_logits, dim=-1), num_samples=1)
    next_token_word = reverse_vocab[next_token_index.item()]
    print(f"【概率采样策略】第 {i + 1} 次选择:索引={next_token_index.item()}, 词='{next_token_word}'")
【概率采样策略】第 1 次选择:索引=0, 词='动'
【概率采样策略】第 2 次选择:索引=7, 词='模'
【概率采样策略】第 3 次选择:索引=7, 词='模'
【概率采样策略】第 4 次选择:索引=7, 词='模'
【概率采样策略】第 5 次选择:索引=0, 词='动'
【概率采样策略】第 6 次选择:索引=7, 词='模'
【概率采样策略】第 7 次选择:索引=7, 词='模'
【概率采样策略】第 8 次选择:索引=0, 词='动'
【概率采样策略】第 9 次选择:索引=7, 词='模'
【概率采样策略】第 10 次选择:索引=7, 词='模'

虽然使用概率采样策略代替贪婪解码策略让模型生成的文本具有了多样性。但是,它还是会以较大的概率选择最可能的词元,会导致生成的文本过于相似。这时可以通过一个称为温度缩放的方法,进一步控制候选词分布和选择过程。

33.6. 温度缩放#

为了控制模型生成的文本的多样性,我们可以使用温度缩放(temperature scaling)技术,通过调整温度参数 \(\tau\) 来改变模型输出的分布,让模型在生成文本时更加随机。

温度缩放的计算公式:

\[ \text{softmax}(x_i) = \frac{e^{x_i / \tau}}{\sum_j e^{x_j / \tau}} \]

33.6.1. 带有温度缩放的Softmax 的代码实现#

def softmax_with_temperature(logits, temperature):
    # 对 logits 按温度系数进行缩放,温度越低,概率分布越集中(更“自信”);温度越高,概率分布越均匀(更“随机”)
    scaled_logits = logits / temperature

    # 使用 softmax 函数将缩放后的 logits 转换为概率分布
    return torch.softmax(scaled_logits, dim=0)

33.6.2. 使用不同的温度系数缩放词元的概率分布#

# 示例数据
vocab = {
    "动": 0,
    "手": 1,
    "学": 2,
    ":": 3,
    "大": 4,
    "语": 5,
    "言": 6,
    "模": 7,
    "型": 8,
}

# 定义下一个 token 的 logits(模型输出的原始值)
next_token_logits = torch.tensor(
    [4.51, 0.89, -1.90, 2.5, 1.63, -1.62, -1.89, 6.28, 1.79]
)

# 温度值列表
temperatures = [1, 0.5, 2]  # 分别表示原始温度、低温度(多样性越低)和高温度(多样性越高)

# 计算不同温度下的概率分布
scaled_probas = [softmax_with_temperature(next_token_logits, T) for T in temperatures]

print(scaled_probas)
[tensor([1.3968e-01, 3.7411e-03, 2.2978e-04, 1.8716e-02, 7.8410e-03, 3.0403e-04,
        2.3209e-04, 8.2005e-01, 9.2015e-03]), tensor([2.8174e-02, 2.0210e-05, 7.6243e-08, 5.0582e-04, 8.8781e-05, 1.3348e-07,
        7.7783e-08, 9.7109e-01, 1.2226e-04]), tensor([0.2186, 0.0358, 0.0089, 0.0800, 0.0518, 0.0102, 0.0089, 0.5297, 0.0561])]

33.6.3. 可视化不同的温度系数对词元概率分布的影响#

import torch
import plotly.express as px
import pandas as pd

# 构建 DataFrame 用于 Plotly 可视化
data = []
for i, T in enumerate(temperatures):
    for idx, (key, value) in enumerate(vocab.items()):
        data.append({
            "词汇": key,
            "概率": scaled_probas[i][idx].item(),
            "温度系数": f"温度系数 = {T}"
        })

df = pd.DataFrame(data)

# 使用 Plotly 绘制分组柱状图
fig = px.bar(
    df,
    x="词汇",
    y="概率",
    color="温度系数",
    barmode="group",
    title="不同温度下的词汇概率分布",
    labels={"词汇": "词汇", "概率": "概率值"},
)

# 调整图表尺寸以适应notebook显示
fig.update_layout(
    width=1000,  # 设置图表宽度
    height=500,  # 设置图表高度
    legend=dict(
        orientation="h",  # 水平排列图例
        yanchor="bottom",
        y=1.02,
        xanchor="right",
        x=1
    )
)

# 显示图表
fig.show()

从结果中我们可以看到:

  • 较高的温度值会让下一个词元的概率分布更均匀,从而产生更多样化的输出,因为它降低了模型重复选择最可能词元的可能性。

  • 较低的温度值会让下一个词元的概率分布更集中,从而产生更确定的输出,因为它增加了模型选择最可能词元的可能性。

虽然概率采样允许模型探索概率较低但可能更具创造性和趣味性的生成路径。然而,这种方法的一个缺点是,它有时会导致语法不正确或完全无意义的输出。所以需要使用 Top-k 的方式缩小候选词的范围,保证生成的文本符合语法规则。

33.7. Tok-K 采样#

Top-K 采样是一种广泛应用于大语言模型(LLM)文本生成的关键解码策略,其核心目标是在生成文本的多样性与连贯性之间取得平衡。与基础的贪心解码(总是选择概率最高的词元)相比,Top-K 采样通过引入可控的随机性,有效避免了输出文本的单调和重复。其基本思想是:在模型为下一个词元生成的概率分布中,仅保留概率值排名前K的候选词元,然后从这个缩小的候选池中进行随机采样,从而既允许较高概率的词元有机会被选中以保持连贯性,又避免了极低概率、不合理词元的干扰。

Top-K 采样的实现过程可以分解为以下几个清晰步骤:

  • 获取概率分布:模型根据当前上下文,为词汇表中的所有可能词元计算一个原始概率分布。

  • 筛选 Top-K 词元:将所有词元按其概率值从高到低排序,并仅保留排名前K位的词元。

  • 重新归一化概率:将筛选出的 K 个词元的概率值进行重新归一化处理,使其概率之和为 1,构成一个新的、用于采样的概率分布。

  • 随机采样:依据这个新的概率分布,使用随机采样选择一个词元作为输出。

在代码实现中,类似因果注意力掩码,使用负无穷大(-inf)来屏蔽非 Top-K 词元。

33.7.1. Top-K 采样的代码实现#

# 获取前 k 个概率最大的 token 的 logits 和位置
top_k = 3
top_logits, top_pos = torch.topk(next_token_logits, top_k)

print(f"Top {top_k} logits:", top_logits)
print(f"Top {top_k} positions:", top_pos)

# 添加掩码:处理 logits,将其中不符合条件的值替换为负无穷大(经Softmax转化为概率时,其概率为0)
new_logits = torch.where(
    condition=next_token_logits < top_logits[-1],  # 条件:筛选出小于 top_logits[-1] 的 logits 值
    input=torch.tensor(float('-inf')),  # 如果条件为 True,则将值替换为负无穷大 (-inf)
    other=next_token_logits  # 如果条件为 False,则保留原始的 next_token_logits 值
)

# 计算原始的下一个词元概率和经过 Top-k 限制的下一个词元概率
next_token_probas = torch.softmax(next_token_logits, dim=0)
topk_probas = torch.softmax(new_logits, dim=0)

print_table("Top-k 采样效果", field_names=["Information", "Value"], data=[
    ["原始预测得分", [f"{logit:.4f}" for logit in next_token_logits.tolist()]],
    ["原始预测概率", [f"{logit:.4f}" for logit in next_token_probas.tolist()]],
    ["Top-3 采样得分", [f"{score:.4f}" for score in new_logits.tolist()]],
    ["Top-3 采样概率", [f"{prob:.4f}" for prob in topk_probas.tolist()]],
])
Top 3 logits: tensor([6.2800, 4.5100, 2.5000])
Top 3 positions: tensor([7, 0, 3])
Top-k 采样效果:
+----------------+-----------------------------------------------------------------------------------------------+
|  Information   |                                             Value                                             |
+----------------+-----------------------------------------------------------------------------------------------+
|  原始预测得分  | ['4.5100', '0.8900', '-1.9000', '2.5000', '1.6300', '-1.6200', '-1.8900', '6.2800', '1.7900'] |
|  原始预测概率  |   ['0.1397', '0.0037', '0.0002', '0.0187', '0.0078', '0.0003', '0.0002', '0.8201', '0.0092']  |
| Top-3 采样得分 |         ['4.5100', '-inf', '-inf', '2.5000', '-inf', '-inf', '-inf', '6.2800', '-inf']        |
| Top-3 采样概率 |   ['0.1428', '0.0000', '0.0000', '0.0191', '0.0000', '0.0000', '0.0000', '0.8381', '0.0000']  |
+----------------+-----------------------------------------------------------------------------------------------+

33.8. 改进生成解码策略#

使用温度系数和 Top-k 采样改进 GPT 的生成解码策略:

  • 温度系数:多样性

  • Top-k 采样:连贯性

def generate(model, start_ids, max_new_tokens, context_size, tokenizer, temperature=0.0, top_k=None, eos_id=None):
    """
    使用温度系数和Top-k采样的更具多样性的生成文本。

    Args:
        model: 语言模型(应处于 eval 模式)
        start_ids (torch.Tensor): 输入的提示 token ID 序列,形状为 (batch_size, seq_len)
        max_new_tokens (int): 要生成的最大新 token 数量
        context_size (int): 模型支持的最大上下文长度
        tokenizer: 用于将 token IDs 解码为文本的分词器
        temperature (float): 温度系数,用于控制生成的多样性(默认0.0表示不使用温度系数)
        top_k (int): Top-k采样的参数,仅保留前k个最大的logits值(默认None表示不使用Top-k采样)
        eos_id (int): 结束 token ID,用于判断是否停止生成(默认None表示不使用结束 token)

    Returns:
        str: 生成的完整文本(包含原始提示)
    """

    model.eval()
    generated_ids = start_ids.to(model.device)

    # generated_ids 是当前上下文中的 Token IDs 数组
    for _ in range(max_new_tokens):
        # 1️⃣ 如果当前序列长度超过上下文窗口,仅保留最后 context_size 个 token 作为输入
        # 例如,如果 LLM 仅支持 3 个 token,而上下文大小为 10,则仅使用最后 3 个 token 作为上下文
        input_context = generated_ids[:, -context_size:]

        # 2️⃣ 使用模型预测每个位置的下一个 Token 在词表上的预测得分,形状为 (batch, seq_len, vocab_size)
        with torch.no_grad():
            logits = model(input_context)

        # 3️⃣ 仅关注最后一个时间步,取最后一个时间步的预测得分作为下一个词的预测,(batch, context_size, vocab_size) 变为 (batch, vocab_size)
        logits = logits[:, -1, :]

        # 4️⃣ 新增:使用Top-k采样过滤logits
        if top_k is not None:
            # 保留前k个最大的logits值
            top_logits, _ = torch.topk(logits, top_k)
            min_val = top_logits[:, -1]  # 获取第k大的值作为阈值
            # 将小于阈值的logits替换为负无穷(在softmax中会被视为概率为0)
            logits = torch.where(logits < min_val, torch.tensor(float('-inf')).to(logits.device), logits)


        # 5️⃣ 新增:应用温度系数缩放
        if temperature > 0.0:
            # 新增:应用温度系数缩放
            logits = logits / temperature  # 对logits进行温度缩放

            # 应用softmax函数得到概率分布,形状为 (batch_size, vocab_size)
            probs = torch.softmax(logits, dim=-1)

            # 从概率分布中随机采样下一个token,形状为 (batch_size, 1)
            idx_next = torch.multinomial(probs, num_samples=1)
        else:
            # 否则与之前相同:取logits最高的词汇表索引,形状为 (batch_size, 1)
            idx_next = torch.argmax(logits, dim=-1, keepdim=True)  

        # 如果遇到序列结束标记并且指定了eos_id,则提前停止生成
        if idx_next == eos_id:
            break

        # 与之前相同:将采样的索引追加到序列中,形状为 (batch_size, num_tokens+1)
        generated_ids = torch.cat((generated_ids, idx_next), dim=1)

    # 将 token IDs 转换为文本
    return token_ids_to_text(generated_ids, tokenizer)

33.9. 使用改进的生成解码策略#

from dsxllm.util import print_table
from dsxllm.gpt.model import text_to_token_ids, token_ids_to_text
from dsxllm.gpt.model import greedy_generate

# 将模型移动到 CPU 上进行推理
model.to("cpu")

# 设置模型为评估模式(关闭 dropout 和 batch normalization 的训练行为)
model.eval()

# 初始化 GPT-2 分词器
tokenizer = tiktoken.get_encoding("gpt2")

# 使用训练好的模型生成并打印 5 次文本
generate_samples = []
for i in range(5):
    # 调用 generate 函数生成文本
    generated_text = generate(
        model=model,
        start_ids=text_to_token_ids("I", tokenizer),
        max_new_tokens=10,
        context_size=GPT_CONFIG_124M["seq_len"],
        tokenizer=tokenizer,
        temperature=3, # 温度系数,控制生成结果的多样性
        top_k=5, # Top-k采样,只保留前5个概率最大的token
    )

    # 将生成的 token ID 序列解码为可读文本
    generate_samples.append((i, generated_text))

print_table("模型生成效果", field_names=["生成次数", "生成结果"], data=generate_samples)
模型生成效果:
+----------+-----------------------------------------------+
| 生成次数 |                    生成结果                   |
+----------+-----------------------------------------------+
|    0     |   I HAD always thought Jack Gisburn rather a  |
|    1     | I could the prism of began to go tears I felt |
|    2     |   I HAD always thought Jack Gisburn rather a  |
|    3     |   I more than if I'd never touched a brush."  |
|    4     |     I HAD always, through, and, pushed one    |
+----------+-----------------------------------------------+

33.10. 答疑讨论#