介绍
大语言模型,比如GPT(生成式预训练Transformer),是旨在一次生成一个词(或词元)的大型深度神经网络架构。
然而,尽管这些模型规模很大,但架构并不那么复杂,因为许多组件是重复的
下图是类GPT大语言模型的自顶向下的示意图,除了嵌入层,还包含一个或多个Transformer块,这些块中包括之前实现的掩码多头注意力模块:

GPT-2与GPT-3
主要关注 GPT-2,因为OpenAI已经公开了该预训练模型的权重。GPT-2是学习实现大语言模型的更好选择,因为能够在单台笔记本电脑上运行
GPT-3在模型架构上与GPT-2基本相同,只是将参数量从GPT-2的15亿扩展到了1750亿,并且训练的数据量更多了。GPT-3需要依赖GPU集群来完成训练和推断。
根据Lambda实验室的说法,在单个V100数据中心GPU上训练GPT-3需要355年,在消费级RTX 8000GPU上则需要665年。
代码实现1-配置参数
通过以下Python字典指定小型GPT-2模型的配置
GPT_CONFIG_124M = {
"vocab_size": 50257, # 词汇表大小
"context_length": 1024, # 上下文长度
"emb_dim": 768, # 嵌入维度
"n_heads": 12, # 注意力头的数量
"n_layers": 12, # 层数
"drop_rate": 0.1, # dropout率
"qkv_bias": False # 查询-键-值偏置
}
参数解释:
在GPT_CONFIG_124M字典中,使用简洁的变量名来提高代码的可读性并避免代码过长。
vocab_size:表示会被BPE分词器使用的由
50257个单词组成的词汇表。context_length:指的是模型通过位置嵌入能够处理的最大输入词元数量。
emb_dim:表示嵌入维度大小,可以将每个词元转化为768维的向量。
n_heads:表示多头注意力机制中注意力头的数量。
n_layers:表示模型中的Transformer块数量,接下来的讨论中将介绍。
drop_rate:表示dropout机制的强度(
0.1表示有10%的隐藏单元被随机丢弃),以防止过拟合。qkv_bias:指的是是否在多头注意力机制的线性层中添加一个偏置向量,用于查询、键和值的计算。遵循现代大语言模型的做法,在一开始禁用它,在加载OpenAI的预训练GPT-2权重时,会再解释。
解释
通过该配置,实现一个GPT占位架构(DummyGPTModel),如下图所示。从全局上理解各个部分如何协同工作,以及构建完整的GPT模型架构还需要编写哪些其他组件。
编写GPT架构的步骤:
- 首先从GPT主干入手,创建一个占位符架构;
- 然后实现各个核心组件;
- 最后将它们组装成Transformer块,形成完整的GPT架构

代码实现2-封装类
DummyGPTModel
DummyGPTModel 类基于PyTorch的神经网络模块(nn.Module)定义了一个简化版的类GPT模型。DummyGPTModel类中的模型架构包括词元和位置嵌入、dropout、一系列Transformer块(DummyTransformerBlock)、最终层归一化(DummyLayerNorm)和线性输出层(out_head)。配置信息通过一个Python字典(比如之前创建的 GPT_CONFIG_124M )传入
forward 方法描述了数据在模型中的处理流程:
- 首先计算输入索引的词元和位置嵌入
- 然后应用dropout
- 接着通过Transformer块处理数据
- 再应用归一化
- 最后使用线性输出层生成logits
import torch
import torch.nn as nn
class DummyGPTModel(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"])
# 使用占位符替换 TransformerBlock
self.trf_blocks = nn.Sequential(
*[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])]
)
# 使用占位符替换层归一化
self.final_norm = DummyLayerNorm(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
x = self.drop_emb(x)
x = self.trf_blocks(x)
x = self.final_norm(x)
logits = self.out_head(x)
return logits
DummyTransformerBlock
作为Transformer块,将在以后开发
class DummyTransformerBlock(nn.Module):
"""
一个简单的占位符类,稍后将被真正的TransformerBlock替换
"""
def __init__(self, cfg):
super().__init__()
def forward(self, x):
"""
这个代码块不执行任何操作,只返回其输入
:param x:
:return:
"""
return x
DummyLayerNorm
作为 归一化层,将在以后开发
class DummyLayerNorm(nn.Module):
"""
一个简单的占位符类,稍后将被真正的层归一化替换
"""
def __init__(self, normalized_shape, eps=1e-5):
"""
这里的参数只是为了模仿层归一化的接口
:param normalized_shape:
:param eps:
"""
super().__init__()
def forward(self, x):
return x
代码实现3-准备输入数据
准备输入数据并初始化一个新的GPT模型,以展示其使用方法。基于对分词器的实现
如下图,展示输入数据如何被分词、嵌入,并输入到GPT模型中,在GPT模型中的流入和流出过程:

代码
使用 tiktoken 分词器对包含两个文本输入的批次进行分词处理,以供GPT模型使用:
# 准备输入数据
import tiktoken
tokenizer = tiktoken.get_encoding("gpt2")
batch = []
txt1 = "Every effort moves you"
txt2 = "Every day holds a"
batch.append(torch.tensor(tokenizer.encode(txt1)))
batch.append(torch.tensor(tokenizer.encode(txt2)))
batch = torch.stack(batch, dim=0)
print("batch:\n", batch)
执行结果,以下是这两个文本的词元ID序列:
tensor([[6109, 3626, 6100, 345],
[6109, 1110, 6622, 257]])
代码实现4-初始化DummyGPTModel
初始化参数量为 1.24亿 的 DummyGPTModel 实例,并将分词后的批次数据传递给它:
# 初始化参数量为 `1.24亿` 的 `DummyGPTModel` 实例
print("\n\n初始化参数量为 `1.24亿` 的 `DummyGPTModel` 实例-----")
# 设置随机种子,复现数据
torch.manual_seed(123)
# 创建 DummyGPTModel 对象
model = DummyGPTModel(GPT_CONFIG_124M)
# 传入分词后的批次数据
logits = model(batch)
print("Output shape:", logits.shape)
print("logits:\n", logits)
执行结果:
初始化参数量为 `1.24亿` 的 `DummyGPTModel` 实例-----
Output shape: torch.Size([2, 4, 50257])
logits:
tensor([[[-1.2034, 0.3201, -0.7130, ..., -1.5548, -0.2390, -0.4667],
[-0.1192, 0.4539, -0.4432, ..., 0.2392, 1.3469, 1.2430],
[ 0.5307, 1.6720, -0.4695, ..., 1.1966, 0.0111, 0.5835],
[ 0.0139, 1.6755, -0.3388, ..., 1.1586, -0.0435, -1.0400]],
[[-1.0908, 0.1798, -0.9484, ..., -1.6047, 0.2439, -0.4530],
[-0.7860, 0.5581, -0.0610, ..., 0.4835, -0.0077, 1.6621],
[ 0.3567, 1.2698, -0.6398, ..., -0.0162, -0.1296, 0.3717],
[-0.2407, -0.7349, -0.5102, ..., 2.0057, -0.3694, 0.1814]]],
grad_fn=<UnsafeViewBackward0>)
解释1
输出张量包含两行,对应于两个文本样本。每个文本样本由 4 个词元组成,如下:
txt1 = "Every effort moves you"
txt2 = "Every day holds a"
解释2
每个词元是一个 50 257 维的向量,与分词器的词汇表大小一致,如下:

解释3
嵌入的维度为 50 257 ,因为这些维度中的每一维代表词汇表中的一个唯一词元。
解释4
在实现后处理代码时,将把这些 50 257 维的向量转换回 词元ID,然后将它们 解码为单词。