Implement a GPT model 5. GPT model & Generating text
GPT model
在我们用代码组装GPT-2模型之前,让我们先看看它的整体结构, 如图4.15所示, 其中包含我们到目前为止所覆盖的所有概念。如我们所见,Transformer Block在GPT模型架构 中多次重复。在124百万参数的GPT-2模型中,它重复了12次,我们通过GPT_CONFIG _124M字典中的n_layers条目来指定。在参数为1,542百万的最大GPT-2模型中,这个Transformer Block重复了48次。
最终Transformer Block输出通过一个最终的层归一化步骤,然后到达线性输出层。该层将变换器的输出映射到高维空间(在这种情况下,50,257个维度,对应模型的词汇大小 ),以预测序列中的下一个标记。

图 4.15 GPT 模型架构的概述,显示数据在 GPT 模型中的流动。
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 logitsNote这个 GPTModel 类的 init 构造函数使用通过 Python 字典 cfg 传入的配置初始化 令牌和位置信息嵌入层。这些嵌入层负责将输入令牌索引转换为密集向量并添加位置信息。
接下来,init 方法创建一个与 cfg 中指定的层数相等的 TransformerBlock 模块的 顺序堆栈。在 transformer 块之后,应用了一个 LayerNorm 层,对 transformer 块的输 出进行标准化,以稳定学习过程。最后,定义了一个不带偏置的线性输出头,将 trans former 的输出投影到分词器的词汇空间中,以为词汇中的每个令牌生成 logits。
FeedForward方法接受一批输入令牌索引,计算它们的嵌入,应用位置嵌入,通过变换器块传递序列,规范化最终输出,然后计算 logits,表示下一个令牌的未规范化概率。我们 将在下一节中将这些 logits 转换为令牌和文本输出。
现在让我们使用我们传递给cfg参数的GPTCONFIG 124M字典初始化124百万参数的GPT模型,并使用我们之前创建的批量文本输入进行喂入:
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
out = model(batch)
print("Input batch:\n", batch)
print("\nOutput shape:", out.shape)
print(out)
## output
Input batch:
tensor([[6109, 3626, 6100, 345], # Token IDs of text1
[6109, 1110, 6622, 257]]) # Token IDs of text2
Output shape: torch.Size([2, 4, 50257])
tensor([[[ 0.3613, 0.4222, -0.0711, ..., 0.3483, 0.4661, -0.2838],
[-0.1792, -0.5660, -0.9485, ..., 0.0477, 0.5181, -0.3168],
[ 0.7120, 0.0332, 0.1085, ..., 0.1018, -0.4327, -0.2553],
[-1.0076, 0.3418, -0.1190, ..., 0.7195, 0.4023, 0.0532]],
[[-0.2564, 0.0900, 0.0335, ..., 0.2659, 0.4454, -0.6806],
[ 0.1230, 0.3653, -0.2074, ..., 0.7705, 0.2710, 0.2246],
[ 1.0558, 1.0318, -0.2800, ..., 0.6936, 0.3205, -0.3178],
[-0.1565, 0.3926, 0.3288, ..., 1.2630, -0.1858, 0.0388]]],
grad_fn=<UnsafeViewBackward0>)在我们继续编码将模型输出转换为文本的函数之前,让我们花更多时间在模型架构 本身上并分析其大小。使用 numel() 方法,即“元素数量”的简称,我们可以收集模 型参数张量中的总参数数量:
total_params = sum(p.numel() for p in model.parameters())
print(f"Total number of parameters: {total_params:,}")
## output
Total number of parameters: 163,009,536之前,我们提到初始化一个具 有1.24亿参数的GPT模型,那么为什么实际参数数量是1.63亿呢?
原因是一个叫做 weight tying 的概念,它在最初的 GPT-2 架构中被使用。这意味着原始的 GPT-2 架构在其输出层中重用了来自the token embedding layer的权重。为了更好地理解,让 我们看看我们通过 GPTModel 之前初始化的标记嵌入层和线性输出层的形状:
print("Token embedding layer shape:", model.tok_emb.weight.shape)
print("Output layer shape:", model.out_head.weight.shape)
## output
Token embedding layer shape: torch.Size([50257, 768])
Output layer shape: torch.Size([50257, 768])从打印输出中可以看到,这两个层的权重张量具有相同的形状.
由于分词器词汇表中有 50,257 行,令牌嵌入和输出层非常大。根据权重绑定,让我们 从总的 GPT-2 模型计数中移除输出层的参数数量:
total_params_gpt2 = total_params - sum(p.numel() for p in model.out_head.parameters())
print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")
## output
Number of trainable parameters considering weight tying: 124,412,160正如我们所见,该模型现在只有1.24亿个参数,匹配了GPT-2模型的原始大小。
权重绑定(weight tying)减少了模型整体的内存占用和计算复杂性。然而,根据我的经验,使用单 独的令牌嵌入和输出层可以获得更好的训练和模型性能;因此,我们在我们的GPTMo del实现中使用了单独的层。现代大型语言模型同样如此。
最后,让我们计算一下我们 GPTModel 对象中 1.63 亿个参数的内存需求:
# Calculate the total size in bytes (assuming float32, 4 bytes per parameter)
total_size_bytes = total_params * 4
# Convert to megabytes
total_size_mb = total_size_bytes / (1024 * 1024)
print(f"Total size of the model: {total_size_mb:.2f} MB")
## output
Total size of the model: 621.83 MB现在我们已经实现了GPTModel架构,并且看到它输出形状为[batch_size, num_tokens, vocab_size]的数值张量,接下来让我们编写代码将这些输出张量转换为文本。
Generating text
我们现在将实现将GPT模型的张量输出转换回文本的代码。在开始之前,让我们简要 回顾一下像LLM这样的生成模型是如何一次生成一个词(或标记)文本的。

图 4.16 LLM 逐步生成文本的过程,每次生成一个标记。
图 4.16 展示了一步步的过程,通过该过程,GPT 模型根据输入上下文生成文本, 例如“你好,我是。”随着每次迭代,输入上下文不断增加,使模型能够生成连贯且 与上下文相符的文本。在第六次迭代时,模型构造了一个完整的句子:“你好,我是 一个准备帮助的模型。”我们已经看到,我们当前的 GPTModel 实现输出形状为 [batc h_size, num_token, vocab_size] 的张量。
现在问题是:GPT 模型如何从这些输出张量生 成文本?

图 4.17 显示了 GPT 模型中文本生成的机制,通过展示令牌生成过程中的单次迭代。
下一词生成过程如图4.17所示,说明了GPT模型在给定输入的情况下生成下一个词 的单个步骤。
在每一步中,模型输出一个矩阵,矩阵中的向量代表潜在的下一个词。 与下一个词对应的向量被提取并通过softmax函数转换为概率分布。在包含结果概率分 数的向量中,找到最高值的索引,这个索引对应于词的ID。然后,将这个词的ID解码 回文本,生成序列中的下一个词。最后,这个词被添加到之前的输入中,形成下一个 子迭代的新输入序列。
这一逐步过程使模型能够顺序生成文本,从最初的输入上下文中构建连贯的短语和句子。
def generate_text_simple(model, idx, max_new_tokens, context_size):
# idx is (batch, n_tokens) array of indices in the current context
for _ in range(max_new_tokens):
# Crop current context if it exceeds the supported context size
# E.g., if LLM supports only 5 tokens, and the context size is 10
# then only the last 5 tokens are used as context
idx_cond = idx[:, -context_size:]
# Get the predictions
with torch.no_grad():
logits = model(idx_cond)
# Focus only on the last time step
# (batch, n_tokens, vocab_size) becomes (batch, vocab_size)
logits = logits[:, -1, :]
# Apply softmax to get probabilities
probas = torch.softmax(logits, dim=-1) # (batch, vocab_size)
# Get the idx of the vocab entry with the highest probability value
idx_next = torch.argmax(probas, dim=-1, keepdim=True) # (batch, 1)
# Append sampled index to the running sequence
idx = torch.cat((idx, idx_next), dim=1) # (batch, n_tokens+1)
return idx这段代码演示了使用PyTorch为语言模型实现生成循环的简单实现。它迭代生成指定数 量的新令牌,裁剪当前上下文以适应模型的最大上下文大小,计算预测,然后根据最 高概率预测选择下一个令牌。
为了编码 generate_text_simple 函数,我们使用 softmax 函数将 logits 转换为概率分 布,从中通过 torch.argmax 确定值最高的位置。softmax 函数是单调的,这意味着它在 转换为输出时保持输入的顺序。因此,实际上,softmax 步骤是多余的,因为 softmax 输出张量中得分最高的位置与 logits 张量中的位置是相同的。
换句话说,我们可以直接对 logits 张量应用 torch.argmax 函数,获得相同的结果。然而,我提供转换的代码 以说明将 logits 转换为概率的完整过程,这可以增加额外的直觉,使得模型生成最可 能的下一个标记,这被称为 greedy decoding。
在下一章中,我们实现GPT训练代码时,将使用额外的采样技术来修改softmax输出 ,以便模型不会总是选择最可能的标记。这在生成的文本中引入了可变性和创造性。

图 4.18 六次标记预测周期的迭代,其中模型将初始标记 ID 的序列作为输入,预测下一个标记,并将该标记附加到 输入序列中以进行下一次迭代。
这个逐个生成令牌 ID 并使用 generate_text_simple 函数将其附加到上下文中的过程 在图 4.18 中进一步说明。(每次迭代的令牌 ID 生成过程在图 4.17 中详细描述。)
我们以迭代的方式生成令牌 ID。例如,在迭代 1 中,模型提供了与“你好,我是”对应 的令牌,预测下一个令牌(ID 为 257,即“一个”),并将其附加到输入中。这个过 程重复进行,直到模型在六次迭代后生成完整句子“你好,我是一个准备帮助的模型 ”。
现在让我们尝试用“你好,我是”作为模型输入的上下文来调用generate_text_simpl e函数。首先,我们将输入上下文编码为令牌ID:
start_context = "Hello, I am"
encoded = tokenizer.encode(start_context)
print("encoded:", encoded)
encoded_tensor = torch.tensor(encoded).unsqueeze(0)
print("encoded_tensor.shape:", encoded_tensor.shape)
## output
encoded: [15496, 11, 314, 716]
encoded_tensor.shape: torch.Size([1, 4])接下来,我们将模型置于.eval()模式。这会禁用像dropout这样的随机组件,这些组件 仅在训练期间使用,并在编码输入张量上使用generate_text_simple函数:
model.eval() # disable dropout
out = generate_text_simple(
model=model,
idx=encoded_tensor,
max_new_tokens=6,
context_size=GPT_CONFIG_124M["context_length"]
)
print("Output:", out)
print("Output length:", len(out[0]))
## output
Output: tensor([[15496, 11, 314, 716, 27018, 24086, 47843, 30961, 42348, 7267]])
Output length: 10
decoded_text = tokenizer.decode(out.squeeze(0).tolist())
print(decoded_text)
##output
Hello, I am Featureiman Byeswickattribute argue正如我们所见,模型生成了无意义的文字,发生了什么?模型无法产生连贯文本的原因是我们尚未对其 进行训练。到目前为止,我们只实现了GPT架构,并用初始随机权重初始化了一个GPT模型实例。
Summary
- 层归一化(Layer normalization)通过确保每一层的输出具有一致的均值和方差来稳定训练。
- Shortcut connections是跳过一个或多个层的连接,通过将一个层的输出直接传递给更深的层,这有助于缓解训练深度神经网络时出现的梯度消失问题,尤其是在训练大规模语言模型(LLMs)时。
- Transformer Block是 GPT 模型的核心结构组件,结合了带有掩蔽的多头注意力模块和使用 GELU 激活函数的全连接前馈网络。
- GPT 模型是具有大量参数的 LLM,通过多个重复的 Transformer Block构建,参数量从数百万到数十亿不等。
- GPT 模型有不同的规模,例如,124百万、345百万、762百万和1,542百万参数,我们可以使用相同的 GPTModel Python 类来实现这些模型。
- GPT 类似的 LLM 的文本生成能力涉及将输出张量解码为可读的文本,通过根据给定的输入上下文一次预测一个标记的方式进行。
- 如果没有训练,GPT 模型会生成不连贯的文本,这凸显了训练对于生成连贯文本的重要性。