大模型原理:计算文本生成损失(交叉熵损失函数)

介绍

在训练过程中通过计算 文本生成损失,来对 生成的文本质量 进行 数值评估

作用:文本生成损失用于量化模型预测下一个 token 的误差,反向传播更新模型参数,让模型逐步学会符合人类语言规律、贴合任务需求的文本生成能力

整体流程

从输入文本到大语言模型生成文本的整体流程:

对于左侧显示的 3个输入词元 中的每一个,计算包含词汇表中每个词元对应 概率分数的向量。

每个向量中 最高概率分数 的 索引位置,表示 最有可能的下一个 词元ID。这些与最高概率分数相关联的 词元ID 会被选中并映射回表示模型生成的文本

提示: 为了将该图片完整地放在一个页面上,使用包含 7个词元的小词汇表。然而,GPTModel使用的是由 50 257 个单词组成的更大的词汇表。因此,代码中的词元ID范围将是 从0到50 256,而不是 从0到6

准备代码

从 大模型原理:全部代码 复制以下类或函数:

  • MultiHeadAttention
  • GELU
  • FeedForward
  • LayerNorm
  • TransformerBlock
  • GPTModel
  • generate_text_simple

配置

从 大模型原理:修改大模型配置 复制 GPT_CONFIG_124M

实现上图的流程

第一步:准备数据

省略第一步,直接给定下面词元ID

准备输入词元ID

以下两个输入示例,表示已经被映射词元ID(参见上图的第1步):

inputs = torch.tensor([[16833, 3626, 6100],   # ["every effort moves",
                       [40,    1107, 588]])   #  "I really like"]
输出输出词元ID

与上面 输入词元ID 相匹配,targets 是希望模型生成的词元ID:

targets = torch.tensor([[3626, 6100, 345  ],  # [" effort moves you",
                        [1107, 588, 11311]])  #  " really like chocolate"]

提示:targets 是对输入数据的复制,但 向前移动一个位置

第2步:将输出结果转换为概率分数

  1. 将 输入词元ID(包含 3个词元 的两个输入示例) 传给模型,计算得到 logits 向量。

  2. 然后,应用 softmax函数 将这些 logits 转换为 概率分数(probas,参见上图的第2步)

# 启用 eval() 模式,禁用诸如dropout等只在训练期间使用的随机组件
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.eval()

with torch.no_grad():
    logits = model(inputs)

probas = torch.softmax(logits, dim=-1)
print(probas)
print(probas.shape)

概率分数(probas)张量的最终张量维度如下所示:

torch.Size([2, 3, 50257])
  • 第一个数值 2 对应于输入中的两个示例(行),也称为“批次大小”。
  • 第二个数值 3 对应于每个输入(行)中的词元数量。
  • 最后一个数值 50257 对应于嵌入维度,由词汇表大小确定。

第3-4步:将结果概率分数转换回词元ID

通过 softmax函数 将 logits 转换为 概率后,将结果概率分数转换回词元ID(参见上图的第3~4步)。

token_ids = torch.argmax(probas, dim=-1, keepdim=True)
print("Token IDs:\n", token_ids)

考虑到我们有两个包含3个词元的输入批次,将 argmax函数 应用于概率分数(参见上图的第3步)会产生两组输出,每组包含3个预测的词元ID:

第5步:将词元ID转换回文本

第5步会将词元ID转换回文本:

tokenizer = tiktoken.get_encoding("gpt2")

print(f"目标 batch 1: {token_ids_to_text(targets[0], tokenizer)}")
print(f"输出 batch 1: {token_ids_to_text(token_ids[0].flatten(), tokenizer)}")

执行结果:

目标 batch 1:  effort moves you
输出 batch 1:  Armed heNetflix

发现这些输出词元与希望模型生成的目标词元 非常不同

原因

模型 没有经过训练,权重是随机的,所以 生成的是随机文本,与目标文本不同

解决

希望通过 损失指标(参见下图)来量化评估模型生成的文本的性能。这不仅有助于衡量生成的文本的质量,同时也是实现训练函数的一个构建块,将使用它来更新模型的权重,从而改善生成的文本

已经完成了第(1)步,现在准备实现文本评估函数(第(2)步)

文本评估过程的一部分是衡量生成词元与正确预测(目标)之间的偏差程度。

模型训练的目标

在训练之前,模型会生成随机的下一个词元的概率向量。

模型训练的目标 是确保与图中框出的 目标词元ID 对应的 概率值 最大化。即:是增大与正确目标词元ID对应的索引位置的softmax概率,如下图所示。这个 softmax概率 也将用于接下来要实现的评估指标中,以量化评估模型生成的输出:正确位置的概率越高,效果越好。

提示:为了将所有内容放入一张图中,上图展示了 七个词元 的词汇表的 softmax概率。所以,随机值在 1/7 左右,大约等于 0.14。但本文中,用于GPT-2模型的词汇表有 50 257个词元,因此大多数初始概率将在 0.000 02 左右( 1/50 257 )

打印概率分布

对于两个输入文本,可以使用以下代码打印与目标词元对应的初始 softmax概率分数:

text_idx = 0
target_probas_1 = probas[text_idx, [0, 1, 2], targets[text_idx]]
print("文本 1:", target_probas_1)

text_idx = 1
target_probas_2 = probas[text_idx, [0, 1, 2], targets[text_idx]]
print("文本 2:", target_probas_2)

每个批次的 3个目标词元ID 概率分数 如下所示:

文本 1: tensor([7.4540e-05, 3.1061e-05, 1.1563e-05])
文本 2: tensor([1.0337e-05, 5.6776e-05, 4.7559e-06])

训练大语言模型的目标:是 最大化正确词元的可能性,这涉及增大其相对于其他词元的概率。通过这种方式,可以确保大语言模型始终选择目标词元(实质上是句子中的下一个单词)作为它生成的下一个词元。

反向传播

如何最大化与目标词元对应的softmax概率值呢?

更新模型权重,以便模型为想要生成的相应 词元ID 输出更高的值。权重更新是通过一种称为 反向传播 的过程完成的,这是训练深度神经网络的标准技术

损失函数

反向传播需要 损失函数,损失函数 会计算模型的 预测输出(在这里是与 目标词元ID 对应的概率),与 目标值 之间的偏差

计算两个批次的概率分数的损失

计算两个示例批次的概率分数的损失,即 target_probas_1 和 target_probas_2

主要步骤如上图所示:已经完成 第1~3步,计算了与目标张量对应的词元的概率,即: target_probas_1 和 target_probas_2,接下来会继续进行 第4步,对概率分数应用 对数(使用对数的好处,详见链接)

log_probas = torch.log(torch.cat((target_probas_1, target_probas_2)))
print(log_probas)

执行结果:

tensor([ -9.5042, -10.3796, -11.3677, -11.4798,  -9.7764, -12.2561])

通过计算 平均值 将这些对数概率组合成一个单一分数(参见上图的 第5步):

avg_log_probas = torch.mean(log_probas)
print(avg_log_probas)

得到的平均对数概率分数如下所示:

tensor(-10.7940)

目标

目标是通过在 训练过程中更新模型的权重,使 平均对数概率尽可能接近 0 。

为什么平均对数概率尽可能接近0?

概率 p∈(0,1],是与目标张量对应的词元的概率
对数概率 \log_ep:

  • 当 p=1 时,\log_ep = 0,代表模型 100% 确定这个位置是真实 token。

  • 当 0<p<1 时,\log_ep < 0 ,是负数。概率越小,对数概率越负。

然而,在深度学习中,通常的做法 不是 将平均对数概率升至0,而是 将负平均对数概率降至0。负平均对数概率就是平均对数概率乘以 -1,对应于图5-7中的 第6步:

neg_avg_log_probas = avg_log_probas * -1
print(neg_avg_log_probas)

输出 tensor(10.7940)

在深度学习中,将 -10.7940 这个负值转换为 10.7940 的术语称为 交叉熵损失。

PyTorch内置了该函数,即:cross_entropy 函数,该函数可以为我们处理上图中的所有步骤。

交叉熵损失函数

在机器学习和深度学习中,交叉熵损失(详见链接) 是一种常用的度量方式,用于衡量 两个概率分布之间的差异——通常是标签(在这里是数据集中的词元)的真实分布和模型生成的预测分布(例如,由大语言模型生成的词元概率)之间的差异。

查看张量形状

在应用 cross_entropy 函数之前,先回顾 logits张量 和 targets张量 的形状:

print("Logits shape:", logits.shape)
print("Targets shape:", targets.shape)

执行结果:

Logits shape: torch.Size([2, 3, 50257])
Targets shape: torch.Size([2, 3])
  • logits张量 具有3个维度:批处理大小、词元数量和词汇表大小
  • targets张量 则具有两个维度:批处理大小和词元数量。

展平

对于PyTorch中的交叉熵损失函数,通过在 批处理维度 上将它们展平:

logits_flat = logits.flatten(0, 1)
targets_flat = targets.flatten()
print("Flattened logits:", logits_flat.shape)
print("Flattened targets:", targets_flat.shape)

得到的张量维度如下所示:

Flattened logits: torch.Size([6, 50257])
Flattened targets: torch.Size([6])

提示:

  • targets 是目标词元ID
  • logits 是在进入softmax函数以获取概率分数之前的未经缩放的模型输出。

使用 cross_entropy() 函数

先前,应用 softmax函数,选择与目标ID对应的概率分数,并计算了负对数概率的平均值。PyTorch的 cross_entropy() 函数将处理所有这些步骤:

loss = torch.nn.functional.cross_entropy(logits_flat, targets_flat)
print(loss)

得到的损失与上面手动应用图中的各个步骤时获得的损失相同,为 tensor(10.7940)

困惑度

困惑度通常与交叉熵损失一起用来评估模型在诸如语言建模等任务中的性能。它可以提供一种更易解释的方式来理解模型在预测序列中的下一个词元时的不确定性。

困惑度可以衡量模型预测的概率分布与数据集中实际词汇分布的匹配程度。与损失类似,较低的困惑度表明模型的预测更接近实际分布。

困惑度可以通过下面方式计算得出:

perplexity = torch.exp(loss)

在先前计算的损失上应用该公式会得到 tensor(48725.8203)

困惑度通常被认为比原始损失值更易于解释,因为它表示模型在每一步中对于有效词汇量的不确定性。在给定的示例中,这意味着模型不确定在词汇表的48 725个词元中应该生成哪个来作为下一个词元。

为了方便讲解,我们计算了两个短文本输入的损失。接下来,我们将对整个训练集和验证集计算损失。


原文出处:/show_1GW2fyLPhdP0.html