引言上篇文章从零构建大模型六为极简 GPT 添加本地预训练能力实现了完整的训练 → 保存 → 加载 → 推理/续训闭环这是大模型开发中的标准工作流。目前还遗留几个问题我们实际使用了一个很简短的训练数据集所以模型效果必然不是很好。可以直接加载训练好模型的参数比如GPT系列模型参数具体要怎么加载呢实际大模型文本生成过程中可以更灵活或者准确的输出具体的内容比如对于有确定答案的问题每次输出结果都一致而对于文本创作类的输出却可以保持灵活性这是怎么做到的呢。本文将继续介绍遗留问题。加载预训练模型参数首先下载参数量为1.24亿的GPT-2模型文件接下来将权重加载到GPT模型中并保存模型下述代码中大部分操作是为了将GPT-2模型参数适配到我们自己的GPTModel上。Tips后续所有操作包括微调、推理必须使用qkv_biasTrue的配置否则load_state_dict会因参数数量不匹配而失败。import numpy as np import torch.nn import json import os import tensorflow as tf from ch04_mini_gpt.gpt_model import GPTModel # 定义 GPT-2 124M 模型的标准配置与 OpenAI GPT-2 124M 对齐 GPT_CONFIG_124M { vocab_size: 50257, # BPE 分词器的词汇表大小 context_length: 1024, # 模型支持的最大上下文长度词元数量 emb_dim: 768, # 词嵌入和隐藏层的维度 n_heads: 12, # 多头注意力机制中的头数 n_layers: 12, # Transformer 块的层数 drop_rate: 0.1, # 全局 dropout 率部分实现中用于统一设置 drop_emb: 0.1, # 词嵌入层的 dropout 率 drop_att: 0.1, # 注意力权重的 dropout 率 drop_ff: 0.1, # 前馈网络的 dropout 率 qkv_bias: False, # 查询(Q)、键(K)、值(V)投影是否使用偏置初始设为 False后续根据权重调整 } def load_gpt2_params_from_tf_ckpt(ckpt_path, settings): 从 OpenAI 提供的 TensorFlow GPT-2 checkpoint 中加载参数 并将其组织为便于映射到 PyTorch 模型的嵌套字典结构。 参数: ckpt_path: TensorFlow checkpoint 路径 settings: 从 hparams.json 加载的模型超参数如 n_layer 返回: params: 结构化参数字典包含 wte, wpe, blocks[...], g, b 等 # 初始化参数字典为每个 Transformer 层预留一个空字典 params {blocks: [{} for _ in range(settings[n_layer])]} # 遍历 checkpoint 中的所有变量 for name, _ in tf.train.list_variables(ckpt_path): # 加载变量并移除冗余的单例维度如 (1, 768) → (768,) variable_array np.squeeze(tf.train.load_variable(ckpt_path, name)) # 去掉变量名前缀 model/例如 model/wte → [wte] variable_name_parts name.split(/)[1:] # 确定当前变量应存入的字典位置 target_dict params # 如果变量属于某一层如 h0, h1, ...则进入对应 block if variable_name_parts[0].startswith(h): layer_number int(variable_name_parts[0][1:]) # 提取层索引如 h3 → 3 target_dict params[blocks][layer_number] # 逐级深入嵌套字典如 attn/c_attn/w → params[blocks][i][attn][c_attn] for key in variable_name_parts[1:-1]: target_dict target_dict.setdefault(key, {}) # 将数组赋值给最后一级键如 w 或 b last_key variable_name_parts[-1] target_dict[last_key] variable_array return params # 指定 GPT-2 124M 模型文件所在目录 model_dir os.path.join(gpt2, 124M) # 获取最新的 TensorFlow checkpoint 路径 tf_ckpt_path tf.train.latest_checkpoint(model_dir) # 加载模型超参数如 n_vocab, n_ctx, n_embd, n_layer, n_head 等 settings json.load(open(os.path.join(model_dir, hparams.json), r, encodingutf-8)) # 从 checkpoint 中加载所有参数到结构化字典 params load_gpt2_params_from_tf_ckpt(tf_ckpt_path, settings) # 创建新的配置复制原始配置并将 qkv_bias 设为 True因为 OpenAI 的 GPT-2 使用了 QKV 偏置 NEW_CONFIG GPT_CONFIG_124M.copy() NEW_CONFIG.update({qkv_bias: True}) # 实例化 PyTorch 版 GPT 模型使用更新后的配置 gpt GPTModel(NEW_CONFIG) # 设置为评估模式关闭 dropout 等训练相关行为 gpt.eval() def assign(left, right): 辅助函数检查 PyTorch 参数张量 left 与 NumPy 数组 right 的形状是否匹配 若匹配则将 right 转换为可训练的 PyTorch Parameter 并返回。 用于安全地将预训练权重赋值给模型参数。 if left.shape ! right.shape: raise ValueError(fShape mismatch. left: {left.shape}, right: {right.shape}) return torch.nn.Parameter(torch.tensor(right)) def load_weights_into_gpt(gpt, params): 将从 TensorFlow checkpoint 加载的 OpenAI GPT-2 权重 映射并加载到自定义的 PyTorch GPTModel 实例中。 # 加载词嵌入token embedding和位置嵌入position embedding权重 gpt.pos_emb.weight assign(gpt.pos_emb.weight, params[wpe]) gpt.tok_emb.weight assign(gpt.tok_emb.weight, params[wte]) # 遍历每一个 Transformer 块共 12 层 for b in range(len(params[blocks])): # 注意力模块权重加载 # OpenAI 的 c_attn 是一个合并的 (768, 2304) 矩阵包含 Q、K、V 三部分 q_w, k_w, v_w np.split(params[blocks][b][attn][c_attn][w], 3, axis-1) # 注意PyTorch 线性层权重是 (out_features, in_features)需转置 gpt.trf_blocks[b].att.W_query.weight assign(gpt.trf_blocks[b].att.W_query.weight, q_w.T) gpt.trf_blocks[b].att.W_key.weight assign(gpt.trf_blocks[b].att.W_key.weight, k_w.T) gpt.trf_blocks[b].att.W_value.weight assign(gpt.trf_blocks[b].att.W_value.weight, v_w.T) # 加载 Q、K、V 的偏置项同样被合并存储 q_b, k_b, v_b np.split(params[blocks][b][attn][c_attn][b], 3, axis-1) gpt.trf_blocks[b].att.W_query.bias assign(gpt.trf_blocks[b].att.W_query.bias, q_b) gpt.trf_blocks[b].att.W_key.bias assign(gpt.trf_blocks[b].att.W_key.bias, k_b) gpt.trf_blocks[b].att.W_value.bias assign(gpt.trf_blocks[b].att.W_value.bias, v_b) # 加载注意力输出投影层c_proj的权重和偏置 gpt.trf_blocks[b].att.out_proj.weight assign( gpt.trf_blocks[b].att.out_proj.weight, params[blocks][b][attn][c_proj][w].T ) gpt.trf_blocks[b].att.out_proj.bias assign( gpt.trf_blocks[b].att.out_proj.bias, params[blocks][b][attn][c_proj][b] ) # 前馈网络MLP权重加载 # 第一个线性层c_fc gpt.trf_blocks[b].ff.layers[0].weight assign( gpt.trf_blocks[b].ff.layers[0].weight, params[blocks][b][mlp][c_fc][w].T ) gpt.trf_blocks[b].ff.layers[0].bias assign( gpt.trf_blocks[b].ff.layers[0].bias, params[blocks][b][mlp][c_fc][b] ) # 第二个线性层c_proj gpt.trf_blocks[b].ff.layers[2].weight assign( gpt.trf_blocks[b].ff.layers[2].weight, params[blocks][b][mlp][c_proj][w].T ) gpt.trf_blocks[b].ff.layers[2].bias assign( gpt.trf_blocks[b].ff.layers[2].bias, params[blocks][b][mlp][c_proj][b] ) # LayerNorm 权重加载注意OpenAI 使用 scaleg, shiftb gpt.trf_blocks[b].norm1.scale assign( gpt.trf_blocks[b].norm1.scale, params[blocks][b][ln_1][g] ) gpt.trf_blocks[b].norm1.shift assign( gpt.trf_blocks[b].norm1.shift, params[blocks][b][ln_1][b] ) gpt.trf_blocks[b].norm2.scale assign( gpt.trf_blocks[b].norm2.scale, params[blocks][b][ln_2][g] ) gpt.trf_blocks[b].norm2.shift assign( gpt.trf_blocks[b].norm2.shift, params[blocks][b][ln_2][b] ) # 最终 LayerNorm 和输出头 gpt.final_norm.scale assign(gpt.final_norm.scale, params[g]) gpt.final_norm.shift assign(gpt.final_norm.shift, params[b]) # 输出头权重与词嵌入权重共享GPT-2 的标准做法 gpt.out_head.weight assign(gpt.out_head.weight, params[wte]) # 自动选择设备GPU 或 CPU device torch.device(cuda if torch.cuda.is_available() else cpu) # 执行权重加载将 OpenAI 的 TF 权重注入到 PyTorch 模型中 load_weights_into_gpt(gpt, params) # 将模型移动到指定设备 gpt.cpu() # 保存加载好的预训练权重为 PyTorch 格式仅保存 state_dict便于后续快速加载 torch.save(gpt.state_dict(), gpt2_124M_pretrained.pth) print(Pretrained GPT-2 (124M) weights saved to gpt2_124M_pretrained.pth)生成一致性 vs. 多样性如何控制输出行为大模型在推理时的行为主要由解码策略控制而非模型结构本身。常见的策略包括Greedy Search贪心搜索每一步都选择概率最高的 token。结果确定、重复性强适合有唯一答案的任务如问答、代码补全。Beam Search保留多个候选序列兼顾局部最优与全局连贯性常用于翻译、摘要等任务。Sampling采样Top-k Sampling从概率最高的 k 个 token 中采样。Top-p (Nucleus) Sampling从累积概率超过 p 的最小 token 集合中采样。可配合 temperature 调节温度低 → 输出更确定温度高 → 更具创造性。贪心搜索上篇文章中我们编写了用于生成文本的函数#用于生成文本的函数 def generate_text_simple(model, idx, max_new_token, context_size): #idx是当前文本的索引数组形状(batch,n_tokens) for _ in range(max_new_token): idx_cond idx[:, -context_size:] #将当前文本截断至支持的长度。如果大语言模型仅支持5个词元单词时文本长度为10则只有最后5个词元会被用作输入文本 with torch.no_grad(): logits model(idx_cond) logits logits[:, -1, :] #只关注最后一个输出的内容形状从(batch, n_tokens, vocab_size)变为(batch, vocab_size) probas torch.softmax(logits, dim-1) #形状为(batch, vocab_size) idx_next torch.argmax(probas, dim-1, keepdimTrue) #形状为(batch, 1)贪婪解码选概率最大的 idx torch.cat((idx, idx_next), dim1) #将计算出的下一个字符的索引添加到索引数组中idx的形状为(batch, n_tokens1) return idx可以加载上一小节中的GPT-2模型并调用该函数生成文本import tiktoken import torch from ch04_mini_gpt.gpt_model import GPTModel, generate_text_simple from ch05_train_model.generate_text import text_to_token_ids, tokens_ids_to_text GPT_CONFIG_124M { vocab_size: 50257, #词汇表大小被BPE分词器使用的词汇表 context_length: 1024, #上下文长度模型通过位置嵌入能够处理的最大输入词元数量 emb_dim: 768, #嵌入维度大小可以将每个词元转化为768维度的向量 n_heads: 12, #注意力头数 n_layers: 12, #层数transformer块数量 drop_rate: 0.1, #dropout率 表示有10%的隐藏单元被随机丢弃防止过拟合 drop_emb: 0.1, #dropout率 表示有10%的隐藏单元被随机丢弃防止过拟合 drop_att: 0.1, #dropout率 表示有10%的隐藏单元被随机丢弃防止过拟合 drop_ff: 0.1, #dropout率 表示有10%的隐藏单元被随机丢弃防止过拟合 qkv_bias: True, #查询-键-值偏置 } model GPTModel(GPT_CONFIG_124M) device torch.device(cuda if torch.cuda.is_available() else cpu) model.load_state_dict(torch.load(gpt2_124M_pretrained.pth, map_locationdevice)) model.to(device) model.eval() tokenizer tiktoken.get_encoding(gpt2) token_ids generate_text_simple( modelmodel, idxtext_to_token_ids(I am, tokenizer), max_new_token25, context_sizeGPT_CONFIG_124M[context_length] ) print(Output text:\n, tokens_ids_to_text(token_ids, tokenizer))多次调用发现输出每次都如下I am not a fan of the idea of a big-budget movie. I think its a waste of money. I可以看出输出的文本是连续并且固定的这是因为生成的词元每次都从词汇表中选取概率最大的进行输出即贪心解码策略。怎么样可以使模型输出更具有随机性呢温度采样这个过程是通过温度缩放来实现的。温度缩放是指将模型输出的 logits未归一化的预测分数 除以一个大于 0 的温度参数 T再通过 softmax 转换为概率分布。温度大于1会导致词元概率更加均匀分布温度小于1会导致概率大的概率取值更高温度等于1则保持原始的概率分布只需要修改generate_text_simple即可。def generate_text_with_temperature(model, idx, max_new_token, context_size, temperature): for _ in range(max_new_token): idx_cond idx[:, -context_size:] with torch.no_grad(): logits model(idx_cond) logits logits[:, -1, :] if temperature ! 1.0: #对logits采用温度缩放 logits logits / temperature probs torch.softmax(logits, dim-1) #转化为概率 #argmax 只关心哪个位置最大温度通常不改变最大值的位置因此引入torch.multinomial # 只有当概率分布具有一定平坦度即多个 token 有显著非零概率时采样才会产生多样化的输出。 idx_next torch.multinomial(probs, num_samples1) #用 torch.multinomial 从概率中采样 idx torch.cat((idx, idx_next), dim1) #将计算出的下一个字符的索引添加到索引数组中idx的形状为(batch, n_tokens1) return idx将temperature调整为1.5。可以看到文本输出变为I am frightened—let that hurt iii West. HOd., DS 132 SonrekHubh ECH, of Thebe(将temperature调整为0.5。可以看到文本输出变为I am not going to be able to afford it. I am going to have to pay for it. I am going to have to输出不再是只选概率最大的词元实现了灵活性的控制Top-k采样上一节采用温度缩放的概率采样方法来增加输出的多样性。较高的温度下会导致词元预测概率分布更均匀从而产生多样化的输出因为它降低了模型重复选择最可能词元的可能性。但是他有时候会导致语法不正确或者完全无意义的输出。为解决低质量词元污染输出的问题需要引入Top-k 采样策略即只从概率最高的 k 个词中采样彻底排除低质量候选。其本质是用-inf替换未选择的logits。其实现如下def generate_text_with_top_k(model, idx, max_new_token, context_size, top_k): for _ in range(max_new_token): idx_cond idx[:, -context_size:] with torch.no_grad(): logits model(idx_cond) logits logits[:, -1, :] # (batch, vocab_size) # 直接在 logits 上做 top-k更高效、更稳定 top_k_logits, top_k_indices torch.topk(logits, top_k, dim-1) # 将非 top-k 的 logits 设为 -inf这样 softmax 后概率为 0 logits_filtered torch.full_like(logits, float(-inf)) logits_filtered.scatter_(dim-1, indextop_k_indices, srctop_k_logits) # 对过滤后的 logits 做 softmax 得到合法概率分布 probs torch.softmax(logits_filtered, dim-1) # 采样 idx_next torch.multinomial(probs, num_samples1) idx torch.cat((idx, idx_next), dim1) return idx调用该函数五次输出文本生成结果如下Output text: I am not going back, he said. We have a great team here. We have a great group here. The Output text: I am a very strong believer in the power of the individual, and the importance of the individual. The individual is the only Output text: I am not a fan of this game, I have played it many times and I will never play it again. I have played it Output text: I am not going to say that the government has not done a good job in this regard. I am just going to say that the Output text: I am not a doctor, I am not a doctor, I am not a doctor, he said. I am just a person可见使用top-k也可以实现更灵活输出的目的。Top-p采样在Top-k采样中k 是固定的但模型的“置信度”是动态变化的固定 k 无法适应模型在不同上下文下的置信度变化。因此可以引入Top-p采样其核心思想是不再固定数量 k而是固定累积概率阈值 p如 p0.9然后选择最小的词集合使其累积概率 ≥ p。具体实现如下def generate_text_with_top_p(model, idx, max_new_token, context_size, top_p): for _ in range(max_new_token): idx_cond idx[:, -context_size:] with torch.no_grad(): logits model(idx_cond) logits logits[:, -1, :] # (batch, vocab_size) # Step 1: 对 logits 进行 softmax 得到概率分布 probs torch.softmax(logits, dim-1) # Step 2: 对概率按从大到小排序 sorted_probs, sorted_indices torch.sort(probs, descendingTrue, dim-1) # Step 3: 计算累积概率 cumulative_probs torch.cumsum(sorted_probs, dim-1) # Step 4: 找到第一个使得累积概率 top_p 的位置 # 将超过 top_p 的部分设为 True需要被 mask 掉 sorted_indices_to_remove cumulative_probs top_p # Step 5: 但要保留至少一个 token即使它自己就 top_p # 所以将第一个位置最大概率的 mask 设为 False sorted_indices_to_remove[..., 1:] sorted_indices_to_remove[..., :-1].clone() sorted_indices_to_remove[..., 0] False # Step 6: 构建原始索引上的 mask indices_to_remove torch.zeros_like(probs, dtypetorch.bool) indices_to_remove.scatter_(dim-1, indexsorted_indices, srcsorted_indices_to_remove) # Step 7: 将要移除的 token 的 logit 设为 -inf logits_masked torch.where(indices_to_remove, float(-inf), logits) # Step 8: 重新计算 softmax 概率只在保留的 token 上 probs torch.softmax(logits_masked, dim-1) # Step 9: 采样 idx_next torch.multinomial(probs, num_samples1) idx torch.cat((idx, idx_next), dim1) return idx组合策略通用生成函数Top-k、Top-p 和 Temperature 并不是互相替代的关系可以组合使用实现更灵活的生成控制。三者的核心作用如下表所示方法控制什么解决什么问题Temperature调节概率分布的“尖锐度”控制确定性 vs 创造性Top-k限制候选词数量固定 k过滤低质量尾部噪声Top-p限制候选词集合动态 p自适应地保留合理多样性可以看几个典型的使用场景任务类型目标TemperatureTop-kTop-p说明事实问答 / 选择题答案唯一、准确、可复现0.1 – 0.35 – 20可选不启用极低温度接近贪心搜索确保一致性代码补全 / 生成语法正确、符合上下文惯例0.2 – 0.610 – 50不启用 或 0.9Top-k 可过滤非法 token如变量名外的乱码机器翻译 / 摘要忠实原文、流畅、简洁0.5 – 0.8可选0.85 – 0.92平衡忠实性与语言自然度新闻 / 报告写作正式、连贯、信息准确0.7 – 0.930 – 500.9 – 0.95允许适度变体避免机械重复故事 / 小说创作有创意、情节合理、语言生动0.8 – 1.050推荐0.9 – 0.95主流配置兼顾多样性与合理性诗歌 / 歌词生成富有韵律、意象新颖0.9 – 1.1可选0.92 – 0.98提高创造性接受一定非常规表达开放域对话聊天机器人自然、有趣、不重复0.8 – 1.0500.9 – 0.95避免“答非所问”或胡言乱语头脑风暴 / 创意发散突破常规、高多样性1.1 – 1.5100 或不用0.95 – 0.99⚠️ 可能产生不合理内容需人工筛选防止重复 / 循环打破确定性死循环≥ 0.7≥ 20≥ 0.85禁用 贪心搜索最后我们实现一个具有top-k和temperature的文本生成函数def generate(model, idx, max_new_token, context_size, temperature1.0, top_kNone, eos_idNone): batch_size idx.shape[0] assert batch_size 1, 当前实现仅支持 batch_size1单条文本生成 for _ in range(max_new_token): # 截取最近的 context_size 个词元作为模型输入 idx_cond idx[:, -context_size:] with torch.no_grad(): logits model(idx_cond) # 只取最后一个时间步的 logits形状变为 (batch_size, vocab_size) logits logits[:, -1, :] # 步骤 1: 应用温度缩放仅在 temperature 0 且 ≠1 时 if temperature 0.0 and temperature ! 1.0: logits logits / temperature # 步骤 2: 如果启用了 Top-k 采样则过滤掉低概率词元 if top_k is not None: top_k_logits, top_k_indices torch.topk(logits, top_k, dim-1) logits_filtered torch.full_like(logits, float(-inf)) logits_filtered.scatter_(dim-1, indextop_k_indices, srctop_k_logits) logits logits_filtered # 步骤 3: 根据 temperature 决定是贪心还是采样 if temperature 0.0: # 贪心直接对 logits 取 argmax无需 softmax idx_next torch.argmax(logits, dim-1, keepdimTrue) else: # 采样先 softmax 得到概率分布再 multinomial 采样 probs torch.softmax(logits, dim-1) idx_next torch.multinomial(probs, num_samples1) # 步骤 4: 检查是否生成了结束符eos_id若是则提前终止 if eos_id is not None and idx_next.item() eos_id: break # 将新生成的词元拼接到序列末尾 idx torch.cat((idx, idx_next), dim1) return idx总结本文解决了两个关键问题成功加载 GPT-2 124M 官方预训练权重到自定义 PyTorch 模型避免从零训练实现多种文本生成策略贪心、温度采样、Top-k、Top-p并构建支持 temperature top_k 的通用生成函数支持按需控制输出通过合理组合解码参数我们就能让同一个模型既“准确”又“有创意”为后续微调与应用奠定基础。在下一篇文章中我们将基于加载好的 GPT-2 预训练模型进行下游任务微调Fine-tuning实现文本分类的微调让大模型真正为你所用