资讯详情 GPT生成式预训练全解析:从架构原理到微调实践
📅 2026/10/16 17:30:42
1. 生成式预训练到底解决了什么问题先把话说在前面GPT这几年几乎成了大模型的代名词但很多人其实没搞明白它最初提出的生成式预训练到底解决了什么问题。如果你把时间拨回2018年那时候做NLP的主流思路是每个任务都要单独标注数据单独训练模型。情感分类要标几万条评论命名实体识别要标几万条句子问答要标几万组问答对。数据标注贵、周期长而且模型学到的知识无法迁移——你在情感分类上学到的语义理解换到下一个任务上一点都用不上。GPT的创始人思路其实很朴素与其每次都为具体任务找标注数据不如先找一大堆随便什么文本让模型自己从里面学语言规律。这个过程不需要人工标注只需要互联网上源源不断的文字。等模型把语言是什么学明白了再拿少量标注数据让它适配具体任务。这就是生成式预训练的核心逻辑先用无监督方式从大规模文本中学习通用语言表征再通过有监督微调迁移到下游任务。这个思路解决了两层问题。第一层是数据问题高质量标注数据永远是稀缺的而原始文本几乎取之不尽。第二层是泛化问题预训练阶段模型看到的是海量多样化的语言现象所以学到的表征比只在单一任务上训练要通用得多。我的理解是这就像培养一个医生之前先让他系统学完基础医学再针对某一专科做临床实习比起一上来就只学某一个病的诊疗方法基础扎实得多。文章后面会从Transformer解码器架构、两阶段训练目标、规模化的演进路径一直讲到我自己实践微调GPT类模型时的参数选择和踩坑记录。不管你是准备入门LLM方向的开发者还是已经在做NLP想补全底层原理的人这篇应该都能给你一个有体系的答案。2. GPT的核心架构拆解2.1 Transformer Decoder是GPT的唯一骨架GPT的架构本质上就是Transformer的Decoder部分但去掉了编码器-解码器之间的交叉注意力。Transformer最开始是做机器翻译的结构是Encoder-Decoder对称形态Encoder把源语言编码成中间表示Decoder再基于这个表示生成目标语言。GPT的思路是我只需要Decoder自己让它看一段文本预测下一个词反复训练就能学会语言。一个GPT Block内部包含两个核心子层先是掩码多头自注意力Masked Multi-Head Self-Attention然后是前馈神经网络Feed-Forward Network每个子层外面都包着残差连接和层归一化。GPT-1的具体配置是12层、隐层维度768、12个注意力头、参数量1.17亿最大序列长度512词表大小约4万用BPE切分。这套配置放到今天看很小但它把Transformer解码器这个架构验证通了。一个容易被忽略的细节是层归一化的位置。最早的Transformer用的是Post-Norm先算子层再做归一化但后来在大规模模型上大家更倾向于Pre-Norm先归一化再算子层因为它在深层的梯度传播上更稳定训练更平滑。GPT系列在实际实现中逐渐转向了Pre-Norm的变体。这类工程细节直接决定了万卡训练能不能稳定跑下来。2.2 掩码自注意力为什么模型只能看左边自注意力机制本身很简单每个位置的向量会生成Query、Key、Value三组向量然后计算每个位置和其他所有位置的相关性再用相关性加权聚合信息。问题来了如果让模型在看今天天气真好我打算去公园______这句话时同时能看到空格后面实际出现的词那预测就没有意义了——答案已经在输入里了模型只需要抄一遍就行。所以GPT使用因果掩码在计算注意力分数时把未来位置的分数设置为负无穷经过Softmax之后权重变成0。这样每个位置只能看到自己和它左边的所有位置右边一概不可见。这个设计的深层意义是强制模型学会真正的预测能力。为了填对下一个词模型必须理解句子的语法结构、上下文的语义约束、甚至常识和世界知识。用代码展示一下这个掩码长什么样只需要几行就能构造出来import torch seq_len 4 mask torch.triu(torch.ones(seq_len, seq_len) * float(-inf), diagonal1) # tensor([[0., -inf, -inf, -inf], # [0., 0., -inf, -inf], # [0., 0., 0., -inf], # [0., 0., 0., 0.]])在注意力计算里这个掩码直接加到分数矩阵上scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) scores scores mask # 未来位置变成 -inf weights torch.softmax(scores, dim-1) out torch.matmul(weights, V)很多人会问为什么BERT能看到双向上下文效果也很好GPT偏要限制自己只看左边答案是两者目标不同。BERT做的是完形填空掩码语言建模模型需要同时参考左右两侧的上下文这适合理解类任务而GPT要生成文本生成过程天然就是从左往右逐词展开的。如果训练时让模型看双向信息、生成时又不能看未来就产生了训练和推断的不一致。GPT把目标和过程统一起来所以生成起来特别顺。2.3 位置编码与BPE分词两个最不起眼的关键模块Transformer本身对词序是不敏感的——你把猫追老鼠和老鼠追猫拆成同样的词向量输入模型看到的注意力矩阵是一样的不考虑位置信息时。所以必须显式地把位置信息注入进去。GPT-1用了可学习的位置嵌入也就是给每个位置分配一个可训练的向量跟着整个网络一起更新。在小规模模型上这个方法简单直接但在长文本场景下外推能力有限所以后来的模型纷纷改用RoPE旋转位置编码这类相对位置方案能更好地处理训练时没见过的超长序列。分词方面GPT用BPEByte Pair Encoding把文本切成子词单元。BPE的核心思想是数据里出现次数最多的相邻字符对不断合并直到达到预定的词表大小。比如learning可能被切成[learn, ing]unbelievable可能被切成[un, believ, able]。这样有几个好处一是不用担心遇到没见过的词OOV问题二是把高频常用词保留为完整词元、低频词拆成更细的子词词表大小和序列长度之间能取得平衡。GPT-2用的字节级BPE词表是50257这个数字今天在好多开源模型里都能看到。2.4 为什么GPT抛弃Encoder只留Decoder这个问题我经常被问到。既然BERT的Encoder双向注意力在理解任务上表现更好为什么后来所有主流大模型都走Decoder-only路线第一个原因前面说过生成任务和因果语言模型天生匹配。第二个原因是接口统一文本生成是这个模型的唯一出口无论下游要做翻译、写作、摘要还是代码生成本质上都是给定上下文续写文本。你不需要为每个任务设计不同的模型头Prompt就是任务描述本身。这也是GPT系列从GPT-1一路扩展到GPT-4之后各项能力不断涌现的架构基础一个统一的生成接口配一个足够大的Decoder就足以容纳几乎所有的语言能力。第三个原因是规模化的工程便利。Decoder-only结构没有Encoder和Decoder之间的复杂交互流水线并行、张量并行、数据并行都好做得多。到了几千亿参数的规模每多一个组件都是巨大的工程负担。后来像T5这样的Encoder-Decoder模型虽然也取得过不错的效果但在最终的大模型竞赛里Decoder-only用更简单的结构、更稳定的训练过程笑到了最后。3. 两阶段训练法预训练与微调3.1 第一阶段用预测下一个词完成无监督预训练GPT的第一阶段目标是标准的语言模型损失给定前t-1个词最大化第t个词的预测概率。把所有位置的概率加起来取平均就是整个句子的损失。用人话解释模型读完一句话的前半段必须尽可能准确地预测后半段接下来出现的每一个词。听起来简单但这个目标的信息量其实极大。为了把下一个词预测准模型不得不学会语法规则、词的搭配习惯、句子的主题一致性、甚至推理链条。举个例子预测因为下雨所以我带了______时模型必须知道雨和伞之间的常识关联。这些知识不需要任何人标注全部从文本中涌现出来。GPT-1预训练用的数据是BooksCorpus大约5GB的图书文本。选图书而不是随意网页是因为长篇连续文本能给模型提供更长的上下文依赖更有利于学习连贯的语义结构。训练配置是最大序列长度512、batch size按Token数折算初始学习率2e-4左右配合warmup和余弦衰减在8块GPU上训练了一个月。训练损失函数本质上就是交叉熵代码里的实现方式值得看一下from torch import nn # logits: [batch, seq_len, vocab_size] # labels: [batch, seq_len] shift_logits logits[:, :-1, :].contiguous() shift_labels labels[:, 1:].contiguous() loss_fct nn.CrossEntropyLoss() loss loss_fct( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1) )注意labels要右移一位这样每个位置都在预测下一个词。这段逻辑是所有生成式语言模型的损失计算基础。3.2 第二阶段把通用模型搬到具体任务上预训练完成后模型已经是一个语言通才但它不会做任何具体任务。这时需要一个轻量的适配过程——有监督微调。微调的思路不是从零训练而是在预训练好的参数基础上用少量标注数据继续更新。GPT-1对下游任务的适配方式很有代表性。它没有为每个任务重新设计复杂的网络结构而是改变输入的拼接方式让任务本身变成文本序列分类任务[Start] 输入文本 [Extract]输出标签蕴含任务[Start] 前提 [Delim] 假设 [Extract]输出判断相似度任务把两个文本以两种顺序分别拼一次各自过模型再合并特征多选题每个选项单独和题干拼起来过模型对多个选项的结果做Softmax微调阶段的损失函数是带权重的混合L L_task λ * L_lm其中L_task是有监督任务损失L_lm是保留的语言模型损失λ在GPT-1里取0.5。很多人不理解为什么要保留语言模型损失我的理解是预训练学到的语言能力是微调任务的地基如果在微调时完全不约束生成方向模型容易在适配小任务时把通用的语言能力丢掉。加上L_lm相当于时刻提醒模型别忘了你还会说人话这对提升收敛稳定性和泛化性都有帮助。3.3 数据与算力预训练的成本到底有多高从GPT-1到GPT-3数据规模的变化非常夸张。GPT-1用了5GB图书GPT-2用Reddit外链搜集的WebText约40GB文本GPT-3则是在过滤后的Common Crawl、WebText、图书和百科混合语料上训练的原始数据量45TB去重过滤后大约570GB约3000亿Token。简单感受一下如果一个人一秒读100个Token不吃不喝读完这些语料需要上千年。算力需求同样夸张。GPT-3的训练成本在当时估计是数百万美元的算力消耗。对普通团队来说从零预训练大模型基本不现实但理解这个成本结构有实际意义它解释了为什么开源社区更倾向于在已有基座模型上做继续预训练或微调而不是自己重造轮子。即便只是领域继续预训练通常也需要至少几十亿Token的高质量数据才能看到明显效果这是很多团队一上来就踩的坑。4. 从GPT-1到GPT-4规模化的关键路径4.1 代际演进参数和能力的跃迁把GPT系列各代模型的核心参数整理成一张表一眼就能看出规模化的趋势模型参数量层数隐层维度最大序列长度关键能力变化GPT-11.17亿12768512验证两阶段训练可行GPT-215亿4816001024零样本生成能力GPT-31750亿96122882048少样本上下文学习GPT-3.5未完整公开--4096对齐人类偏好GPT-4未完整公开--8K/32K多模态、复杂推理GPT-1到GPT-2是第一个跃迁。参数量增加13倍数据从5GB变成40GB模型突然具备了令人惊讶的文本生成连贯性。GPT-2最著名的例子是输入一段关于美国内战后建造火车隧道的文字模型能续写出合理的历史叙事——没有人教过它历史它只是从海量网页里读过。GPT-2到GPT-3是第二个跃迁。参数量暴涨115倍训练数据扩大到3000亿Token这时出现了一个预训练阶段从未专门训练过的能力——上下文学习In-Context Learning。给GPT-3几个翻译示例它不用更新任何参数就能照着示例的模式继续翻译。这在监督学习框架里几乎无法解释只能归结为规模带来的涌现能力。4.2 Scaling Law为什么变大就是有效OpenAI团队在GPT-2和GPT-3之间做了一系列消融实验结论后来被总结为Scaling Law模型损失随着参数量、数据量、计算量的增大按幂律下降。这意味着要想把损失降下来最稳妥的方式就是同时扩大模型和数据规模而不是绞尽脑汁改架构。但这里有个容易误读的点很多人以为只要把模型做大就行实际上Scaling Law的三个维度模型大小、数据大小、计算量需要同步增长缺一不可。只扩大模型不增加数据模型很快就把数据背完了开始过拟合只增加数据不扩大模型模型容量不够损失下降也会变缓。正确的姿势是在三者之间保持大约等比例的投入。现在很多团队训练大模型失败的常见原因之一就是数据规模没有跟上模型规模。4.3 与BERT的路线之争单向还是双向2018年GPT和BERT几乎同时出现当时BERT在GLUE榜单上全面压过GPT学术圈普遍更看好双向Encoder路线。为什么后来剧情反转了核心在于两者优化的目标不同。BERT用掩码语言建模善于理解但天生不擅长生成——它的训练目标里没有从零续写一句话这个能力要求。GPT用因果语言模型不仅理解而且能生成。当大家发现能生成这件事在统一任务接口上价值巨大时Decoder-only路线的优势就显现了你可以让模型把所有任务都转化成给定提示词续写答案包括翻译、总结、问答、写代码全部一个套路搞定。这个趋势到GPT-3的上下文学习能力出现后更加明显。如今几乎所有的开源大模型包括各种规模的中文模型清一色都是Decoder-only架构。回头看BERT的momentum在理解类任务上保持了几年优势但生成能力最终成了决定通用人工智能路线走向的关键变量。4.4 RLHF让模型从会说到会做人GPT-3能生成流畅的文本但它生成的文本不一定符合人类期望可能语句通顺但内容有害可能看似专业实则胡说八道。这是因为预训练的目标是预测下一个词不是说出用户想听的有用答案。到了ChatGPT这一代OpenAI引入了RLHF基于人类反馈的强化学习让模型对齐人类偏好。RLHF分三步第一步收集人工撰写的理想回答做有监督微调SFT让模型学会问答的基本形态第二步让模型生成多个回答由人类标注好坏训练一个奖励模型RM来预测人类偏好第三步用强化学习算法主要用PPO以奖励模型为信号进一步优化生成策略。这个过程成本很高但效果立竿见影模型不再只是文字概率预测器而是变成能理解用户意图、遵循指令、拒绝不合理要求的对话助手。我个人的理解是预训练给模型装上了语言能力RLHF给模型装上了行为规范。少了任何一环要么能力不行要么行为不可控。这也是为什么现在很多开源模型即便基座能力很强如果不对齐用起来依然会感觉不太聪明。5. 实操自己微调一个GPT类模型5.1 工具选型与环境准备如果你想亲自上手跑一个GPT类模型现在生态已经非常成熟了。首选是Hugging Face的Transformers库加PyTorch配合peft库做参数高效微调再搭配DeepSpeed或Accelerate处理分布式训练。硬件方面如果是微调7B级别模型用LoRA在24GB显存的消费级显卡上就能跑全参数微调至少需要A100/H100这样的80GB级别显存或者多卡并行。我建议新手起步的方式是先用1B-3B的小模型在单卡上跑通整个流程理解了数据格式、损失计算、生成推理之后再上大模型。直接上大模型很容易被各种环境问题淹没反而不利于建立正确的直觉判断。个人经验是本地先拿一个小模型反复实验两三天比盲目租卡跑大模型收获大得多。5.2 数据准备决定微调效果的上限微调效果的上限在数据而不在模型本身。数据准备有几个关键点数量上指令微调通常万条级别起步10万条已经算不错领域继续预训练则需要亿级Token起步质量优先于数量混入垃圾数据会让模型变笨需要去重和过滤重复样本会放大模型对特定句式的偏好指令数据要覆盖任务多样性最好包含问答、写作、摘要、翻译、代码、角色扮演等数据格式上对话式微调通常组织成每轮对话的序列每个角色前后拼接special token来区分。以最基础的指令微调为例一个样本就是指令-回答对转化后在训练时通过损失掩码只计算回答部分的损失避免模型去死记硬背指令的措辞。这一步很多人会忽略导致模型训练损失一直在降但生成质量很差——因为它把输入指令也当成需要预测的内容了。5.3 一个可运行的微调流程下面给出一套基于LoRA的微调最小可运行流程。LoRA的思路是冻结原始权重只训练注入的低秩分解矩阵把可训练参数量降到原来的1%左右适合在有限的硬件条件下微调。from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training model AutoModelForCausalLM.from_pretrained(你的基座模型路径) tokenizer AutoTokenizer.from_pretrained(你的基座模型路径) lora_config LoraConfig( r8, # 低秩矩阵的秩 lora_alpha16, # 缩放系数通常设为r的2倍 target_modules[q_proj, v_proj], # 只对注意力层注入LoRA lora_dropout0.05, biasnone ) model get_peft_model(model, lora_config) training_args TrainingArguments( output_dir./output, per_device_train_batch_size4, gradient_accumulation_steps8, # 等效batch size 4 * 8 32 learning_rate2e-5, num_train_epochs3, lr_scheduler_typecosine, warmup_ratio0.1, fp16True, logging_steps10, save_steps500, ) trainer Trainer(modelmodel, argstraining_args, train_datasetdataset) trainer.train()几个参数的取值逻辑说明一下学习率是微调中最敏感的超参数。全参数微调一般用1e-5到3e-5LoRA微调可以用2e-5到1e-4因为只更新少量参数所以可以稍微激进一点。batch size的选择要结合显存显存不够就调大梯度累积步数。warmup比例0.1的含义是前10%的训练步数里学习率从0线性升到峰值防止模型在开始时被大步长甩到不好的区域。5.4 推理与生成解码参数怎么调微调完成后进入生成阶段生成策略对效果的影响同样不可忽视。GPT模型每次输出一个Token然后在已生成内容上继续预测这个过程叫自回归解码。如果每次都选概率最大的Token贪心解码生成结果往往很单调如果完全按概率随机采样结果又可能混乱。实际中一般用折中方案output model.generate( input_ids, max_new_tokens512, temperature0.7, # 控制随机性越低越保守 top_p0.9, # 只在累积概率前90%的Token里采样 repetition_penalty1.05, # 对重复Token施加惩罚 do_sampleTrue )温度参数是最直观的temp1是原始概率分布越小越高概率Token被选中生成越稳定越大越随机更有发散性。top_p是核采样动态截断概率分布里累积概率达到p的那部分避免采样到极端低概率但无意义的Token。repetition_penalty解决生成死循环问题数值大于1会抑制重复出现的Token。实际使用中没有一个万能组合需要根据任务类型调代码生成更偏向低温度创意写作可以适当提高温度。6. 常见问题与排查心得6.1 训练阶段的典型问题排查训练中遇到的情况比较集中先把高频问题整理成速查表现象可能原因排查方向损失不下降学习率过大/过小先试2e-5-5e-5区间观察前200步曲线损失震荡剧烈batch size太小、数据混入噪音增大batch、清洗数据微调后通用能力下降灾难性遗忘降低学习率、减少epoch、混合通用数据显存溢出序列过长、batch过大梯度累积、混合精度、模型并行生成重复内容采样参数不当、训练数据单一调repetition_penalty、检查数据多样性损失不下降是最常见的起步问题。我一般先排除最蠢的错误数据有没有正确配对、labels有没有右移、tokenizer有没有对齐。如果都没问题再看学习率学习率过大时损失前期下降很快但后面震荡甚至发散学习率过小则损失慢吞吞降不动。还有一个容易被忽略的点不同基座模型的词表大小不同如果把别的模型的tokenizer硬套过来embedding矩阵对不上训练必然出问题。显存溢出OOM基本是每个训练过模型的人都会遇到的。优先手段是梯度累积减小per_device_train_batch_size同时用gradient_accumulation_steps把等效batch补回来。然后是混合精度fp16训练能省一半显存新硬件上用bf16更稳配合DeepSpeed的ZeRO Stage 2可以把优化器状态分散到多卡上。如果单卡还是爆还有一个选项是开启CPU offload不过速度代价较大建议优先考虑减小序列长度。6.2 微调后效果不对的排查思路很多人微调完发现一个诡异现象训练损失很低但生成出来的内容质量很差。这个情况往往不是模型的问题而是数据或评测方式的问题。第一种情况是损失掩码配错了。前面说过指令微调通常只计算回答部分的损失如果掩码没加模型会把指令部分当成也需预测的文本训练时同步死背指令文本表现就是训练损失低、推理效果差。检查方法很简单打印出一个batch的labels把-100的位置标出来看看是不是只有回答部分是有效数字。第二种情况是数据泄漏。训练集和验证集不做去重模型可能在记忆答案而不是学会答题。尤其指令数据来自同一个来源时相似的问法反复出现模型很容易表现出虚高的准确率。应对方式是在数据处理阶段按文本相似度做去重并且确保验证集和训练集来源隔离。第三种情况是评估方式和生成配置不匹配。比如评测时用了temperature1的随机采样结果每次跑出来的指标波动极大或者用了过强的repetition_penalty导致模型在需要重复关键词的任务上被误伤。评测生成模型建议固定随机种子多次采样取平均值并且把生成参数显式记录在实验日志里。6.3 影响范围GPT类模型改变了什么作为收尾前的一个讨论我们可以聊一下GPT这条技术路线的实际影响范围。它改变的绝不仅仅是NLP领域。代码生成工具让程序员写样板代码的效率提升了一个档次写作辅助工具让内容生产的门槛大幅降低对话式搜索改变了人机交互方式多模态扩展让模型能看图说话、理解图表、甚至辅助代码调试。更深远的影响在于它把AI从专才变成了通才——以前一个模型只能做一个任务现在一个模型可以处理几乎所有语言相关的任务。对个人开发者来说最实际的启示是你不需要从零造大模型但你可以基于开源的基座模型用领域数据微调出自己的专用模型。这个流程的成本已经降到了个人可以承担的范围而理解GPT的底层原理——从架构到训练方法——是做好这件事的前提。最后说点我的实际体会这几年跟大模型打交道我最深的体会是动手实践永远比看论文重要。很多人把GPT的原理背得很熟但真正跑起来才发现损失函数右移一位这种细节在实践中能卡你一整天。所以如果你看完这篇文章想动手我建议直接从Hugging Face上挑一个小一点的模型先跑通微调再理解每一步犯错的过程本身就是最好的学习。实际操作中还有一个心得无论模型多大先把数据质量抓起来再谈调参。我见过太多团队在超参数上反复较劲最后发现是数据集里有几千条重复样本或者指令格式不统一。把数据可视化地检查一遍比多试十组参数都管用。最后再分享一个小技巧在做任何微调实验之前先固定一套评测集和评测指标甚至先记录好基座模型在评测集上的表现分数。这样你每次改动都能看到明确的涨跌而不是凭感觉觉得好像变好了。这个习惯在后续所有模型实验里都会帮你省下大量时间。