
1. 从BERT到GPT两种架构的分岔路口到底差在哪很多人学完Transformer的Encoder和Decoder结构之后脑子里还是一团浆糊——BERT和GPT明明都是从Transformer出来的为什么一个擅长做理解、一个擅长做生成这个问题如果不在脑子里彻底掰扯清楚后面看再多论文都是白搭。我自己最开始接触这两个模型的时候也是花了很长时间才把它们的血缘关系理顺。后来我发现真正理解它们差异的关键不在于背结构图而在于搞清楚一个核心问题注意力机制里每个token到底能看到哪些位置的信息。这个能看到谁的问题直接决定了模型的训练方式和能力边界。1.1 一个比喻双向阅读 vs 单向续写你可以把BERT想象成一个做完形填空题的学生。给你一句话今天天气真___你可以同时看到今天天气真这三个词然后综合所有上下文来推断空格里应该填什么。这就是BERT的做法——双向注意力每个位置都能看到左边和右边的所有token。GPT则完全不同。它像一个在写接龙故事的人每次只能看到前面已经写出来的内容然后预测下一个词是什么。写今天的时候只能看到开头写天气的时候能看到今天写真的时候能看到今天天气。这就是单向因果注意力每个位置只能看到自己和左边的token。这个差异看起来很小但它带来的后果是根本性的。BERT因为能看到完整上下文所以特别擅长理解任务——分类、抽取、匹配、问答。GPT因为只能看左边天然适合生成任务——续写、对话、翻译、代码补全。你让BERT去做长文本生成它没有这个训练目标你让GPT去做命名实体识别它也不是不能做但需要额外的微调策略。1.2 注意力掩码一行代码决定模型命运如果你去看HuggingFace的源码会发现BERT和GPT的注意力掩码逻辑差异非常直观。BERT的attention mask就是一个简单的padding mask标记哪些位置是真实token、哪些是填充的。而GPT的attention mask是一个下三角矩阵确保位置i只能attend到位置0到i。# GPT的因果掩码上三角部分被mask掉 import torch seq_len 5 causal_mask torch.tril(torch.ones(seq_len, seq_len)) print(causal_mask) # tensor([[1., 0., 0., 0., 0.], # [1., 1., 0., 0., 0.], # [1., 1., 1., 0., 0.], # [1., 1., 1., 1., 0.], # [1., 1., 1., 1., 1.]])这个下三角矩阵就是GPT的灵魂。它保证了训练时的并行性和推理时的一致性——训练的时候虽然所有token一起喂进去但每个位置只能看到左边的信息和推理时逐token生成的行为完全对齐。BERT没有这个约束所以它的[CLS] token能够聚合整个句子的双向信息特别适合做句子级别的表示。但反过来BERT也没法直接用来做自回归生成因为它在训练时看到了未来的信息推理时没法复现这种条件。1.3 训练目标的本质区别BERT用的是Masked Language ModelMLM随机遮住15%的token让模型去猜。这个目标迫使模型学会从双向上下文推断被遮住的内容本质上是在学习一个填空能力。GPT用的是Causal Language ModelCLM就是标准的语言模型目标——给定前面的token预测下一个token。这个目标看起来更简单但它要求模型学会在只有左侧信息的情况下做出合理预测这恰恰是生成任务需要的核心能力。注意很多人以为BERT的MLM和GPT的CLM只是遮不遮未来的区别其实还有一个关键点——MLM每次只预测被遮住的15%位置而CLM每个位置都要预测。这意味着在相同数据量下GPT的每个token都能贡献训练信号而BERT只有15%的token参与loss计算。这也是为什么GPT系列模型在规模扩大后展现出更强的涌现能力。理解了这些底层差异你就能明白为什么后来会出现T5、BART这种Encoder-Decoder架构——它们试图把双向理解和自回归生成结合起来用Encoder做理解、Decoder做生成。但那是另一个话题了这里先把BERT和GPT这条分岔路走透。2. 把BERT的预训练目标拆开揉碎MLM和NSP到底在学什么BERT的预训练用了两个目标Masked Language ModelMLM和Next Sentence PredictionNSP。很多人对MLM比较熟悉但对NSP的理解往往停留在判断两句话是不是连续的这个层面其实里面有不少值得深挖的细节。2.1 MLM的15%遮罩策略为什么不是全遮BERT论文里有一个很反直觉的设计在被选中的15% token里并不是全部替换成[MASK]。具体来说80%的概率替换成[MASK]10%的概率替换成随机token10%的概率保持不变为什么要这么设计如果全部用[MASK]替换会出现一个严重的问题模型只在看到[MASK]的时候才去学习上下文推断而在微调阶段根本没有[MASK]这个token导致预训练和微调之间存在不一致。加入随机token和保持不变这两种情况就是强迫模型对每个位置都保持警惕——你不能只盯着[MASK]去猜你得对每个token都理解得足够好因为任何一个位置都可能是被动了手脚的。这个设计思路后来被很多模型借鉴比如RoBERTa就发现去掉NSP、用动态遮罩效果更好。# 简化版的MLM遮罩逻辑 import random def mask_tokens(tokens, mask_prob0.15, mask_token[MASK], vocab_size30000): masked_tokens tokens.copy() labels [-100] * len(tokens) # -100表示不计算loss for i, token in enumerate(tokens): if random.random() mask_prob: labels[i] token # 记录原始token作为预测目标 r random.random() if r 0.8: masked_tokens[i] mask_token elif r 0.9: masked_tokens[i] random.randint(0, vocab_size - 1) # else: 10%概率保持不变 return masked_tokens, labels这段代码虽然简化了但核心逻辑是完整的。实际训练中遮罩是动态生成的——每个epoch对同一句话遮的位置都不一样这比原始BERT的静态遮罩效果更好。2.2 NSP的争议它真的有用吗NSP任务的设计初衷是让模型理解句子之间的关系这对问答、自然语言推理这类任务很重要。具体做法是50%的时候给模型两句话是真正连续的50%的时候第二句是从语料里随机抽的让模型判断IsNext还是NotNext。但后来的研究尤其是RoBERTa发现NSP任务可能并没有想象中那么有用甚至可能有害。原因在于随机抽取的下一句来自完全不同的文档主题差异太大模型只需要判断主题是否一致就能完成任务根本没学到真正的句子关系去掉NSP之后RoBERTa在多个下游任务上反而表现更好所以现在很多BERT变体都去掉了NSP改用其他方式比如ALBERT用SOP——Sentence Order Prediction让模型判断两句话的顺序有没有颠倒这个任务更难也更有意义。2.3 预训练数据与词表被忽视的关键因素BERT用的是WordPiece分词词表大小30000。这个选择在当时是合理的但后来大家发现中文场景下WordPiece的效果不如按字分词或者BPE。Google原版的中文BERT就是按字分词的词表大小21128。预训练数据方面原版BERT用了BooksCorpus8亿词和英文维基百科25亿词。这个数据量在今天看来不算大但当时已经是很可观的规模了。数据质量对预训练效果的影响非常大——维基百科的文本相对干净规范而BooksCorpus提供了更丰富的长文本。实操心得如果你要自己预训练一个BERT或者做领域适配数据清洗的重要性怎么强调都不过分。我试过在噪声很大的数据上做继续预训练结果模型学了一堆乱七八糟的模式下游任务反而变差了。后来花了大量时间做去重、去噪、过滤低质量文本效果才回来。2.4 从预训练到微调BERT的两种使用姿势BERT在下游任务上的使用方式主要有两种第一种是特征提取——把BERT当作一个固定的编码器取出[CLS]或者最后一层的hidden states然后接一个简单的分类器。这种方式训练成本低但效果通常不如微调。第二种是微调——在预训练权重的基础上用下游任务的数据更新所有参数。这是BERT论文推荐的方式效果也最好。微调的时候学习率要设得很小通常2e-5到5e-5因为预训练权重已经在一个很好的位置了大步长反而会破坏学到的表示。微调还有一个常见坑灾难性遗忘。如果下游任务数据很少模型很容易过拟合把预训练阶段学到的通用语言知识忘掉。解决办法包括用更小的学习率、加warmup、做layer-wise learning rate decay底层用更小的学习率顶层用正常学习率。3. GPT的自回归生成从语言模型到通用助手GPT系列的发展路线和BERT完全不同。BERT走的是理解路线GPT走的是生成路线。从GPT-1到GPT-2再到GPT-3核心架构变化不大主要是规模在扩大但能力却出现了质的飞跃。3.1 GPT-1用Transformer Decoder做预训练微调GPT-1的架构就是Transformer的Decoder堆叠12层768隐藏维度12个注意力头。它的训练分两步先在大量无标注文本上做语言模型预训练然后在下游任务上做有监督微调。GPT-1的创新点在于它证明了预训练微调这个范式在生成模型上同样有效。但它的规模太小1.17亿参数能力有限当时并没有引起太大轰动。3.2 GPT-2Zero-shot的意外惊喜GPT-2把参数扩大到15亿训练数据用了WebText约40GB的网页文本。真正有意思的是OpenAI发现GPT-2在很多任务上不需要微调就能做——给它一个任务描述它就能生成合理的结果。这个发现很重要因为它暗示了一个方向语言模型本身可能就隐含了多种任务的能力只要规模够大、数据够多这些能力就会被激发出来。GPT-2的zero-shot能力虽然还不稳定但已经让很多人开始重新思考预训练的价值。3.3 GPT-3In-context Learning的威力GPT-3把参数推到1750亿训练数据量也大幅增加。它最令人惊讶的能力是In-context Learning——你不需要更新任何参数只需要在输入里给几个例子模型就能学会新任务。比如你想让GPT-3做情感分类只需要这样写评论这家餐厅的服务态度非常好。 情感正面 评论等了一个小时才上菜太慢了。 情感负面 评论菜品一般但环境还不错。 情感模型就能自动补全中性或者混合。这个过程没有任何梯度更新完全靠前向推理完成。这个能力背后的机制至今没有完全解释清楚。一种观点认为Transformer的注意力机制在前向过程中隐式地实现了某种梯度下降——每一层都在根据上下文调整表示效果上类似于在隐空间做优化。另一种观点认为大规模语言模型在预训练过程中已经学到了大量任务的模式in-context examples只是帮助模型定位到正确的模式。3.4 自回归生成的采样策略Temperature、Top-k和Top-pGPT生成文本的时候每一步都会输出一个词表上的概率分布。怎么从这个分布里选下一个词直接决定了生成文本的质量和多样性。贪心搜索就是每次选概率最大的词。这样生成的文本通常很流畅但很无聊容易陷入重复循环。Temperature是在softmax之前对logits做缩放。温度小于1会让分布更尖锐更保守大于1会让分布更平坦更随机。通常0.7到1.0之间比较合适。Top-k采样是只从概率最高的k个词里采样。k太小会限制多样性k太大会引入低质量词。一般k取40到100。Top-pnucleus采样是动态选择累积概率达到p的最小词集合。比如p0.9就选累积概率到90%的那些词。这种方式比固定k更灵活因为不同位置的分布形状不一样。# 简化的Top-p采样实现 import torch import torch.nn.functional as F def top_p_sampling(logits, p0.9, temperature1.0): logits logits / temperature probs F.softmax(logits, dim-1) sorted_probs, sorted_indices torch.sort(probs, descendingTrue) cumulative_probs torch.cumsum(sorted_probs, dim-1) # 找到累积概率超过p的位置 cutoff (cumulative_probs p).nonzero()[0].item() # 保留前cutoff1个词 sorted_probs sorted_probs[:cutoff 1] sorted_indices sorted_indices[:cutoff 1] # 重新归一化并采样 sorted_probs sorted_probs / sorted_probs.sum() sampled_idx torch.multinomial(sorted_probs, 1) return sorted_indices[sampled_idx]实操心得在实际部署中temperature和top-p的组合需要根据任务调。做事实性问答的时候temperature设低一点0.3-0.5top-p设0.8左右减少胡编乱造。做创意写作的时候temperature可以到0.9-1.0top-p设0.95让输出更多样。我见过有人直接用默认参数跑所有任务结果要么太死板要么太飘调参这一步不能省。4. 选BERT还是GPT场景驱动的决策框架知道了BERT和GPT的原理之后实际工作中最常遇到的问题就是这个任务到底该用哪个。我总结了一个简单的决策框架不一定全面但能覆盖大部分常见场景。4.1 任务类型决定架构选择任务类型推荐架构原因文本分类BERT双向上下文对分类更有利命名实体识别BERT序列标注需要双向信息抽取式问答BERT需要定位答案在原文中的位置文本生成GPT自回归生成是GPT的强项对话系统GPT需要逐token生成回复文本摘要BART/T5Encoder-Decoder兼顾理解和生成机器翻译T5/mBART同上代码补全GPT自回归生成天然适合这个表格不是绝对的。比如BERT也可以做生成只是需要额外的解码策略GPT也可以做分类只是通常不如BERT直接。但在大多数情况下按照任务类型选架构是最省事的。4.2 数据量和计算资源的现实约束BERT-base有1.1亿参数GPT-3有1750亿参数。这个差距意味着什么意味着GPT-3的一次推理可能需要多张高端GPU而BERT-base在一块普通显卡上就能跑。如果你手上的数据只有几千条标注样本微调一个BERT-base通常是最务实的选择。如果你有大量无标注文本想做一个通用助手那可能需要考虑GPT类模型但训练成本是另一个数量级。还有一个容易被忽视的点推理延迟。BERT做分类是一次前向传播延迟很低。GPT做生成需要逐token解码生成长度100的文本就要做100次前向传播延迟高得多。在实时性要求高的场景比如在线客服这个差异很关键。4.3 混合架构取两家之长实际工程中很多系统并不是纯BERT或纯GPT而是混合使用。比如用BERT做意图识别和槽位填充用GPT做回复生成用BERT做检索和排序用GPT做最终答案生成RAG架构用BERT做质量评估过滤GPT生成的低质量内容这种混合架构的好处是各取所长。BERT擅长快速理解和判断GPT擅长灵活生成。把两者串起来往往比单用一个大模型效果更好成本也更可控。4.4 一个容易踩的坑不要用BERT的[CLS]做生成我见过有人试图用BERT的[CLS]向量直接接一个解码器来做生成结果效果很差。原因很简单BERT的[CLS]是在双向注意力下聚合的句子表示它包含了整个句子的信息但没有任何生成顺序的概念。用它来初始化解码器相当于让解码器从一个已经知道答案的状态开始生成这和自回归生成的逻辑是矛盾的。正确的做法是如果你要做生成要么用GPT类的自回归模型要么用BART/T5这种Encoder-Decoder架构让Encoder负责理解、Decoder负责生成两者通过cross-attention连接。不要试图把BERT硬改成生成模型那是事倍功半。5. 从理论到落地我踩过的那些坑理论讲完了说点实际的。我在把BERT和GPT应用到真实项目中的过程中踩过不少坑有些是理解不到位有些是工程细节没注意。这里挑几个有代表性的分享一下。5.1 分词器不匹配导致的效果暴跌有一次我拿一个英文预训练的BERT去做中文任务想着都是语言应该差不多结果效果惨不忍睹。后来才发现英文BERT的WordPiece词表里几乎没有中文字符所有中文都被拆成了[UNK]或者单字模型根本学不到有意义的表示。这个坑的教训是预训练模型的语言必须和目标任务的语言匹配。中文任务就用中文预训练模型比如bert-base-chinese不要想着跨语言迁移能省事。如果确实需要跨语言用多语言BERTbert-base-multilingual-cased但效果通常不如单语言模型。5.2 学习率设太大预训练权重全毁了微调BERT的时候学习率是一个极其敏感的参数。我一开始用1e-3的学习率结果loss直接爆炸模型输出全是乱码。后来降到2e-5才正常收敛。为什么学习率要这么小因为预训练权重已经在一个很好的局部最优附近了大步长会把模型踢出这个区域破坏已经学到的语言知识。通常的建议是BERT微调2e-5到5e-5GPT微调1e-5到3e-5更小因为生成任务更敏感加warmup前10%的step线性增加学习率避免一开始就大步长5.3 序列长度不是越长越好BERT的最大序列长度是512。很多人以为越长越好其实不是。更长的序列意味着更大的计算量注意力是O(n²)复杂度而且如果大部分序列都很短padding到512会浪费大量计算。我的做法是先统计训练数据的序列长度分布取95%分位数作为max_length。比如95%的样本都在128以内那就设max_length128能省不少显存和计算时间。对于少数超长样本可以截断或者用滑动窗口处理。5.4 GPT生成重复内容的几种解法用GPT生成文本时最常见的问题就是重复——模型陷入一个循环反复输出相同的短语。这个问题在temperature设低的时候尤其明显。解决办法有几种Repetition Penalty对已经出现过的token降低其概率。HuggingFace的generate函数支持repetition_penalty参数通常设1.1到1.2。No Repeat N-gram禁止出现重复的n-gram。比如no_repeat_ngram_size3就不会出现连续3个词重复的情况。提高Temperature适当提高温度可以增加随机性减少重复。但太高会导致输出不连贯。Top-p采样比贪心搜索更不容易陷入重复循环。我通常的组合是temperature0.8top_p0.9repetition_penalty1.1no_repeat_ngram_size3。这个配置在大多数场景下表现比较均衡。5.5 显存不够用的几种优化手段BERT-base微调大概需要8-12GB显存batch_size16max_length128。如果显存不够可以尝试梯度累积用小batch_size跑多次前向累积梯度后再更新。效果上等价于大batch但显存占用小。混合精度训练用fp16代替fp32显存减半速度提升。HuggingFace的Trainer默认支持。梯度检查点用时间换空间把中间激活值不保存反向传播时重新计算。显存大幅降低但训练变慢。LoRA/QLoRA只训练少量低秩适配器参数冻结原模型。显存需求极低适合大模型微调。实操心得LoRA是我最近用得最多的微调方式。它只训练原模型0.1%到1%的参数效果却能接近全量微调。而且训练好的LoRA权重只有几MB方便存储和切换。如果你有多任务场景可以给每个任务训练一个LoRA推理时动态加载非常灵活。5.6 评估指标的选择陷阱做分类任务的时候很多人只看准确率Accuracy。但如果数据类别不平衡准确率会严重误导。比如99%的样本是负类模型全预测负类也能有99%准确率但实际毫无用处。更合理的指标是F1分数尤其是macro-F1它同时考虑精确率和召回率。对于生成任务BLEU和ROUGE是常用指标但它们和人类判断的相关性有限。如果条件允许最好加入人工评估。我在一个项目里遇到过这种情况自动指标显示模型A比模型B好但人工评估发现模型B的输出更自然、更有用。后来分析发现模型A只是更擅长抄原文里的短语所以BLEU分数高但实际理解能力不如模型B。这个教训让我明白自动指标只能作为参考不能完全替代人的判断。6. 一些值得关注的延伸方向BERT和GPT的故事远没有结束。围绕这两个架构还有很多值得探索的方向。6.1 参数高效微调用更少的参数做更多的事全量微调一个BERT-base需要更新1.1亿参数存储和部署成本都不低。参数高效微调PEFT的思路是只更新少量参数就能达到接近全量微调的效果。除了前面提到的LoRA还有Adapter、Prefix Tuning、Prompt Tuning等方法。这些方法的共同点是冻结预训练模型的大部分参数只训练少量新增参数或低秩矩阵。好处是训练快、存储小、不容易过拟合。在数据量有限的场景下PEFT往往比全量微调更实用。6.2 检索增强生成让GPT不再胡编GPT的一个大问题是幻觉——它会生成看起来合理但实际错误的内容。检索增强生成RAG的思路是先从知识库检索相关文档然后把文档作为上下文喂给GPT让它基于检索到的内容生成答案。这样做的好处是答案有据可查减少了胡编乱造知识可以随时更新不需要重新训练模型可以引用来源增加可信度。RAG目前是很多问答系统的主流架构BERT通常用来做检索和排序GPT用来做最终生成。6.3 指令微调与对齐让模型更懂人话原始的GPT-3虽然能力强但不太听话——你让它写个摘要它可能给你续写一段故事。指令微调Instruction Tuning就是用大量指令-回答对来训练模型让它学会按照指令行事。在此基础上基于人类反馈的强化学习RLHF进一步让模型的输出更符合人类偏好。这个过程比较复杂但效果显著——它让GPT从会生成文本变成了会按照要求生成有用的文本。6.4 长文本处理突破512和2048的限制BERT的512限制和GPT的2048/4096限制在很多场景下都不够用。长文档理解、多轮对话、代码仓库分析都需要更长的上下文窗口。目前有几条技术路线稀疏注意力只计算部分位置的注意力、滑动窗口注意力每个token只关注局部窗口、线性注意力把O(n²)降到O(n)。这些方法各有取舍有的损失精度有的增加实现复杂度。但整体趋势是上下文窗口在不断扩大这对应用层来说是好消息。6.5 多模态扩展从纯文本到图文音视频BERT和GPT最初都是纯文本模型但它们的架构很容易扩展到多模态。VisualBERT、ViLBERT把图像特征和文本特征一起输入TransformerCLIP用对比学习对齐图像和文本的表示GPT-4V可以直接理解图片内容。多模态的核心挑战在于不同模态的信息如何对齐、如何融合、如何在同一套注意力机制下处理。目前的做法通常是先用各自的编码器把不同模态转成向量序列然后拼接起来送进Transformer。这个方向还在快速发展中值得持续关注。写到这里BERT和GPT这条线的基本脉络就梳理得差不多了。从Transformer的分叉到各自的预训练目标再到实际应用中的选型和踩坑每一块都有很多细节值得深挖。我在实际工作中最大的体会是不要迷信某个架构要根据任务、数据、资源来选。BERT和GPT不是对立的它们只是工具箱里的不同工具用对了场景才能发挥价值。