5.1 评估生成文本模型
5.2 训练一个LLM
5.3 控制随机性的解码策略
5.4 在PyTorch中加载和保存模型权重
5.5 从OpenAI加载预训练权重
此前,我们使用一个由一本短篇小说集组成的有限数据集训练了一个小型GPT - 2模型 。这种方法使我们能够专注于基础知识,而无需大量的时间和计算资源。
幸运的是,OpenAI公开分享了他们的GPT - 2模型权重,这样我们就无需自己花费 数万到数十万美元在大型语料库上重新训练模型。那么,让我们将这些权重加载到我 们的GPTModel类中,并使用该模型进行文本生成。这里,weights例如指的是存储在Py Torch的线性层和嵌入层的.weight属性中的权重参数。我们之前在训练模型时通过mod el.parameters()访问过它们。在第6章中,我们将重新使用这些预训练权重来针对文本 分类任务对模型进行微调,并遵循类似于ChatGPT的指令。
请注意,OpenAI最初是通过TensorFlow保存GPT - 2权重的,我们必须安装TensorFl ow才能在Python中加载这些权重。以下代码将使用一个名为tqdm的进度条工具来跟踪 下载过程,我们也必须安装该工具。
你可以安装
pip install tensorflow>=2.15.0 tqdm>=4.66
下载代码比较长,大多是模板代码,不是很有趣。因此,我们没有占用宝贵的篇幅来 讨论从互联网上获取文件的Python代码,而是直接从本章的在线仓库中下载gpt_downl oad.py Python模块:
import urllib.request
url = (
"https://raw.githubusercontent.com/rasbt/"
"LLMs-from-scratch/main/ch05/"
"01_main-chapter-code/gpt_download.py"
)
filename = url.split('/')[-1]
urllib.request.urlretrieve(url, filename)
接下来,将此文件下载到Python会话的本地目录后,你应该简要检查该文件的内容, 以确保其保存正确且包含有效的Python代码。
现在我们可以按如下方式从gpt_download.py文件中导入download_and_load_gpt2函 数,该函数会将GPT - 2的架构设置(settings)和权重参数(params)加载到我们的Py thon会话中:
from gpt_download import download_and_load_gpt2
settings, params = download_and_load_gpt2(
model_size="124M", models_dir="gpt2"
)
执行此代码将下载与124M参数的GPT-2模型相关的以下七个文件:
checkpoint: 100%|███████████████████████████| 77.0/77.0 [00:00<00:00, 63.9kiB/s]
encoder.json: 100%|█████████████████████████| 1.04M/1.04M [00:00<00:00, 2.20MiB/s]
hprams.json: 100%|██████████████████████████| 90.0/90.0 [00:00<00:00, 78.3kiB/s]
model.ckpt.data-00000-of-00001: 100%|███████| 498M/498M [01:09<00:00, 7.16MiB/s]
model.ckpt.index: 100%|█████████████████████| 5.21k/5.21k [00:00<00:00, 3.24MiB/s]
model.ckpt.meta: 100%|██████████████████████| 471k/471k [00:00<00:00, 2.46MiB/s]
vocab.bpe: 100%|████████████████████████████| 456k/456k [00:00<00:00, 1.70MiB/s]
[!NOTE] 注意
如果下载代码对你不起作用,可能是由于网络连接不稳定、服务器问题 ,或者OpenAI共享开源GPT-2模型权重的方式发生了变化。在这种情况下, 请访问本章的在线代码库(https://github.com/rasbt/LLMs-from-scratch)获取替 代和更新的说明,并通过曼宁论坛提出进一步的问题。
假设前面代码执行完毕,我们来检查一下settings和params的内容:
print("Settings:", settings)
print("Parameter dictionary keys:", params.keys())
内容如
Settings: {'n_vocab': 50257, 'n_ctx': 1024, 'n_embd': 768, 'n_head': 12, 'n_layer': 12}
Parameter dictionary keys: dict_keys(['blocks', 'b', 'g', 'wpe', 'wte'])
settings和params都是Python字典。settings字典存储的大语言模型(LLM)架构设置与 我们手动定义的GPT_CONFIG_124M设置类似。params字典包含实际的权重张量。请 注意,我们只打印了字典键,因为打印权重内容会占用太多屏幕空间;不过,我们可 以通过print(params)打印整个字典来检查这些权重张量,也可以通过相应的字典键选择 单个张量,例如嵌入层权重:
print(params["wte"])
print("Token embedding weight tensor dimensions:", params["wte"].shape)
词元嵌入层的权重是
[[-0.11010301 ... -0.1363697 0.01506208 0.04531523]
[ 0.04034033 ... 0.08605453 0.00253983 0.04318958]
[-0.12746179 ... 0.08991534 -0.12972379 -0.08785918]
...
[-0.04453601 ... 0.10435229 0.09783269 -0.06952604]
[ 0.1860082 ... -0.09625227 0.07847701 -0.02245961]
[ 0.05135201 ... 0.00704835 0.15519823 0.12067825]]
Token embedding weight tensor dimensions: (50257, 768)
我们通过download_and_load_gpt2(model_size="124M",...)
设置下载并加载了最小的GP T-2模型的权重。OpenAI还分享了更大模型的权重:3.55亿参数、7.74亿参数和15.58亿 参数。如图5.17所示,这些不同规模的GPT模型的总体架构是相同的,除了不同的架构元素的重复次数不同,嵌入大小也不同。本章的其余代码也与这些更大的模型兼 容。
图5.17 GPT - 2大语言模型有几种不同的模型规模,参数数量从1.24亿到15.58亿不等。其核心架构相同,唯一的区别 在于嵌入尺寸以及诸如注意力头和Transformer块等单个组件的重复次数。
将GPT - 2模型权重加载到Python中后,我们仍需要将它们从设置和参数字典转移到 我们的GPTModel实例中。首先,我们创建一个字典,列出图5.17中不同GPT模型规模 之间的差异:
model_configs = {
"gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},
"gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},
"gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},
"gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},
}
假设我们想加载最小的模型“gpt2 - small(1.24亿参数)”。我们可以使用model_con figs
表中的相应设置来更新我们之前定义并使用的完整长度的GPT_CONFIG_124M
:
model_name = "gpt2-small (124M)"
NEW_CONFIG = GPT_CONFIG_124M.copy()
NEW_CONFIG.update(model_configs[model_name])
细心的读者可能还记得,我们之前使用的是256个词元的长度,但OpenAI最初的GPT - 2模型是使用1024个词元的长度进行训练的,所以我们必须相应地更新NEW_CONFIG
:
NEW_CONFIG.update({"context_length": 1024})
此外,OpenAI在多头注意力模块的线性层中使用偏置向量来实现查询、键和值矩阵的 计算。如今,偏置向量在大语言模型中已不常用,因为它们并不能提升建模性能,因 此是不必要的。然而,由于我们使用的是预训练权重,为保持一致性,我们需要匹配 相关设置并启用这些偏置向量:
NEW_CONFIG.update({"qkv_bias": True})
现在我们可以使用更新后的NEW_CONFIG
字典来初始化一个新的GPTModel
实例:
gpt = GPTModel(NEW_CONFIG)
gpt.eval()
默认情况下,GPTModel
实例会使用随机权重进行预训练初始化。使用OpenAI模型权 重的最后一步是用我们加载到params
字典中的权重覆盖这些随机权重。为此,我们 首先将定义一个小型的assign
实用函数,该函数会检查两个张量或数组(左和右)是 否具有相同的维度或形状,并将右边的张量作为可训练的PyTorch参数返回:
def assign(left, right):
if left.shape != right.shape:
raise ValueError(f"Shape mismatch. Left: {left.shape}, "
"Right: {right.shape}"
)
return torch.nn.Parameter(torch.tensor(right))
接下来,我们定义一个load_weights_into_gpt
函数,该函数将params
字典中的权重加 载到GPTModel
实例gpt
中。
在load_weights_into_gpt
函数中,我们仔细地将OpenAI实现中的权重与我们的GPTM odel
实现进行匹配。举一个具体的例子,OpenAI将第一个Transformer块的输出投影层 的权重张量存储为params["blocks"][0]["attn"]["c_proj"]["w"]
。在我们的实现中,这个 权重张量对应于gpt.trf_blocks[b].att.out_proj.weight
,其中gpt
是一个GPTModel
实例 。
开发load_weights_into_gpt
函数需要大量的猜测工作,因为OpenAI使用的命名规范 与我们的略有不同。不过,如果我们试图匹配两个维度不同的张量,assign
函数会提 醒我们。此外,如果我们在这个函数中犯了错误,我们也会注意到,因为最终得到的 GPT模型将无法生成连贯的文本。
现在让我们实际尝试一下 load_weights_into_gpt
,并将OpenAI模型权重加载到我 们的GPTModel实例 gpt
中:
load_weights_into_gpt(gpt, params)
gpt.to(device)
如果模型加载正确,我们现在就可以使用之前的 generate
函数,用它来生成新的文 本:
torch.manual_seed(123)
token_ids = generate(
model=gpt, idx=text_to_token_ids("Every effort moves you", tokenizer).to(device),
max_new_tokens=25,
context_size=NEW_CONFIG["context_length"],
top_k=50,
temperature=1.5
)
print("Output text:\n", token_ids_to_text(token_ids, tokenizer))
生成的文本如下:
Output text:
Every effort moves you toward finding an ideal new way to practice something!
What makes us want to be on top of that?
我们可以确信模型权重加载正确,因为模型能够生成连贯的文本。这个过程中哪怕一 个小错误都会导致模型无法正常运行。在接下来的章节中,我们将进一步使用这个预 训练模型,并对其进行微调,以实现文本分类和执行指令的功能 。
练习5.5
使用来自OpenAI的预训练权重,在“The Verdict”数据集上计算GPTModel的训练集 和验证集损失。
练习5.6
对不同规模的GPT - 2模型进行实验,例如参数达15.58亿的最大模型,并将其生成的 文本与1.24亿参数的模型生成的文本进行比较。