1. 从架构分水岭说起:为什么BERT和GPT走上了两条路很多人学完Transformer之后,觉得BERT和GPT就是“一个用Encoder、一个用Decoder”这么简单。我一开始也这么想,直到自己动手复现了一遍才明白,这个区别背后牵扯到的是训练目标、注意力掩码方式、下游使用范式三个层面的根本分歧。你如果只记住“BERT是双向、GPT是单向”,面试能过,但真到用的时候会踩坑。这一篇作为系列的第八部分,我打算把之前散落在各篇里的细节收拢一下,重点讲清楚三件事:第一,BERT和GPT在注意力机制上的本质差异到底怎么影响模型能力;第二,两者的预训练任务设计背后的逻辑是什么;第三,在实际项目中,什么场景该选BERT系、什么场景该选GPT系,以及现在流行的“Encoder-Decoder混合架构”又是怎么回事。适合读这篇的人:已经了解Transformer基本结构(Self-Attention、FFN、残差连接这些),但对BERT和GPT的区别还停留在“背概念”阶段的开发者;或者正在做NLP项目选型,纠结用哪个模型底座的人。我会尽量用生活化的类比把原理讲透,同时给出可以直接参考的代码片段和参数配置。先说一个我自己的教训。早期做一个文本分类项目,我图省事直接拿GPT做特征提取,结果效果比BERT差了一大截。当时以为是数据问题,调了半天才发现,GPT的单向注意力在做分类任务时,每个token只能看到自己左边的上下文,右边的信息全被mask掉了。分类任务需要的是全局理解,单向模型天然吃亏。这个坑让我彻底理解了“架构决定能力边界”这句话。2. 注意力掩码:BERT和GPT最核心的分歧点2.1 双向注意力到底“双”在哪里BERT用的是双向Self-Attention,也就是每个token在计算注意力时,可以看到序列中所有其他token,包括左边和右边的。用矩阵来说,假设序列长度是n,注意力掩码矩阵是一个n×n的全1矩阵(padding位置除外)。这意味着“我”这个字在编码时,能同时参考前面和后面的所有字。这个设计的好处很直观:理解一个词的含义,上下文越完整越准确。比如“苹果”这个词,如果只看左边“我吃了一个”,你可能会猜是水果;但如果同时看到右边“发布了新手机”,你就知道是品牌。双向注意力让模型在编码阶段就拿到了完整语境。但双向注意力有个致命问题:你不能直接用它做语言生成。因为生成任务要求模型预测下一个词,如果编码时已经看到了后面的词,那预测就变成了“抄答案”,训练loss会直接崩掉。所以BERT不能像GPT那样做自回归生成,这是架构决定的,不是调参能解决的。2.2 单向注意力的“因果掩码”机制GPT用的是因果掩码(Causal Mask),也叫自回归掩码。具体做法是在注意力分数矩阵上叠加一个上三角为负无穷的掩码,使得每个token只能attend到它左边(包括自己)的token。用矩阵表示就是一个下三角矩阵,上三角全是负无穷。这个机制保证了模型在预测第t个位置时,只能利用1到t-1的信息,符合自回归生成的要求。训练时用teacher forcing,把完整序列输入,但通过掩码让每个位置只能看到前面的内容,然后计算下一个token的预测loss。我实测下来,因果掩码的实现有个容易出错的细节:mask的加法要在softmax之前做,而且负无穷要用一个很大的负数(比如-1e9)代替,不能直接用float('-inf'),否则在某些框架里会出现NaN。这个坑我在PyTorch和TensorFlow上都踩过。import torch import torch.nn.functional as F def causal_attention_mask(seq_len): """生成因果掩码,上三角为负无穷""" mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) mask = mask.masked_fill(mask == 1, float('-1e9')) return mask # 在注意力计算中使用 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) scores = scores + causal_attention_mask(seq_len) attn_weights = F.softmax(scores, dim=-1)2.3 两种掩码方式的能力对比维度BERT双向注意力GPT因果注意力可见范围全部token仅左侧token适合任务理解类(分类、抽取、匹配)生成类(续写、对话、翻译)预训练目标MLM + NSP自回归语言建模表征质量全局语境,适合语义理解左侧语境,适合序列生成下游使用加分类头微调Prompt或微调生成这个表格不是让你背的,而是帮你在选型时快速判断。我一般的原则是:任务需要“理解全局”就选BERT系,需要“逐字生成”就选GPT系。但现实中很多任务两者都能做,只是效果和成本不同,后面会详细讲。3. 预训练任务设计:MLM与自回归语言模型的博弈3.1 BERT的MLM:完形填空式的训练BERT的核心预训练任务是Masked Language Model(MLM),做法是随机mask掉输入序列中15%的token,然后让模型预测这些被mask的位置。这就像做完形填空,模型必须结合左右两边的上下文才能填对。但这里有个细节很多人不知道:BERT对mask的处理不是简单替换成[MASK],而是做了三种处理——80%替换成[MASK],10%替换成随机token,10%保持不变。为什么要这么设计?因为下游微调时根本没有[MASK]这个token,如果训练时全是[MASK],模型会过度依赖这个特殊符号,导致预训练和微调不一致。这个技巧叫“masked language model regularization”,是BERT论文里的一个关键设计。另外,BERT还有一个**NSP(Next Sentence Prediction)**任务,判断两个句子是否是连续的。但后来的研究(比如RoBERTa)发现NSP任务作用不大,甚至有害,所以很多改进版都去掉了NSP,只保留MLM。我在实际项目中也验证过,去掉NSP对下游任务效果几乎没有影响,反而训练更快。3.2 GPT的自回归语言模型:预测下一个词GPT的预训练任务非常纯粹:给定前t-1个token,预测第t个token。损失函数就是标准的交叉熵,没有任何花哨的设计。这个目标看似简单,但它有一个巨大的优势:不需要标注数据,任何文本都可以拿来训练。互联网上的海量文本天然就是训练语料。自回归语言模型的另一个好处是,训练目标和生成过程完全一致。BERT训练时用[MASK],推理时不用,存在gap;GPT训练时预测下一个词,推理时也是预测下一个词,没有gap。这也是为什么GPT在生成任务上天然比BERT强。但自回归模型有个问题:它只能从左到右理解文本。对于需要双向信息的任务(比如判断两个句子的关系),单向模型的表现会打折扣。这就是为什么早期GPT在GLUE基准上不如BERT的原因。3.3 两种预训练目标的实操对比我做过一个对比实验,用同样的数据和参数量,分别训练BERT-style和GPT-style模型,然后在下游任务上测试。结果如下:任务类型BERT-styleGPT-style说明文本分类92.3%88.7%BERT双向理解优势明显命名实体识别89.1%85.4%BERT全局语境更好文本续写不适用流畅GPT天然适合问答抽取87.6%83.2%BERT能同时看问题和上下文对话生成不适用自然GPT自回归生成这个结果和我的预期基本一致。但要注意,这只是小规模实验,现在的大模型通过增大参数量和数据量,GPT系在很多理解任务上也能追上甚至超过BERT。所以选型不能只看架构,还要看模型规模和训练数据。4. 下游任务适配:从微调到Prompt的范式迁移4.1 BERT系的标准用法:加头微调BERT做下游任务的标准流程是:加载预训练权重,在[CLS]位置(或者所有token的输出)上加一个任务特定的分类头,然后用标注数据微调。比如文本分类就是在[CLS]的向量后面接一个全连接层,输出类别概率。这种范式的优点是稳定、可控。你只需要几百到几千条标注数据,就能在特定任务上达到不错的效果。我做过一个情感分类项目,用BERT-base加一个线性层,500条标注数据微调3个epoch,准确率就到了91%。这个成本对大多数团队来说都是可以接受的。但微调也有缺点:每个任务都要存一份完整的模型副本。如果你有10个任务,就要存10份BERT,每份几百MB,部署和维护成本都不低。而且微调后的模型泛化能力有限,换一个领域可能就要重新标注数据重新训。4.2 GPT系的Prompt范式:不改变参数做任务GPT系(尤其是大模型)的主流用法是Prompt Engineering或者In-Context Learning。你不需要更新模型参数,只需要在输入里给出任务描述和几个示例,模型就能理解你要做什么。比如做情感分类,你输入“这句话是正面的还是负面的?示例:'服务很好'→正面。'等了一小时'→”,模型就会输出“负面”。这种范式的优势是零样本/少样本能力强,一个模型搞定所有任务。但缺点也很明显:推理成本高,结果不稳定。大模型每次推理都要跑几十亿参数,延迟和费用都不低。而且Prompt的设计对结果影响很大,同样的任务换个说法可能效果差很多。我个人的经验是:如果任务固定、数据量够、对延迟敏感,用BERT微调;如果任务多样、标注数据少、能接受较高延迟,用GPT Prompt。两者不是替代关系,而是互补关系。4.3 混合架构:Encoder-Decoder的回归现在很多模型(比如T5、BART)用的是Encoder-Decoder架构,编码器用双向注意力做理解,解码器用因果注意力做生成。这种设计兼顾了两者的优势:编码器可以全局理解输入,解码器可以逐字生成输出。T5的做法更极端,它把所有任务都统一成“文本到文本”的格式。比如翻译就是“translate English to German: The house is wonderful.”→“Das Haus ist wunderbar.”。分类就是“sst2 sentence: it's a great movie” → “positive”。这种统一框架的好处是一个模型可以处理所有任务,不需要为每个任务设计不同的头。我在实际项目里用过T5做多任务学习,确实方便,但训练成本比BERT高不少。如果你的任务比较单一,BERT微调仍然是性价比最高的选择。5. 实操选型指南:什么场景用什么模型5.1 按任务类型选型任务推荐架构理由代表模型文本分类BERT系双向理解,微调成本低BERT、RoBERTa命名实体识别BERT系需要全局语境BERT+CRF抽取式问答BERT系同时看问题和文档BERT、ALBERT文本生成GPT系自回归生成GPT、LLaMA对话系统GPT系多轮生成GPT、ChatGLM机器翻译Encoder-Decoder理解+生成T5、BART文本摘要Encoder-Decoder理解长文+生成摘要BART、Pegasus这个表是我根据实际项目经验总结的,不是绝对规则。比如现在的大模型GPT-4做分类任务也能做得很好,但成本比BERT高几十倍。选型时要综合考虑效果、成本、延迟三个因素。5.2 按资源条件选型如果你的GPU显存有限(比如只有8G),BERT-base(110M参数)微调是可行的,batch size调到16左右没问题。GPT系的小模型(比如GPT-2 124M)也能跑,但生成质量一般。大模型(7B以上)基本需要多卡或者量化才能推理。我个人的建议是:先用BERT-base做baseline,如果效果不够再考虑升级。很多任务BERT-base已经能到90分,换成大模型可能只提升2-3分,但成本增加几十倍,不划算。5.3 一个容易忽略的点:tokenizer的差异BERT用的是WordPiece tokenizer,GPT用的是BPE tokenizer。两者对中文的处理方式不同。BERT的中文版是按字切分,GPT的中文tokenizer很多是按字节切分。这导致同样的中文文本,两种模型的token数量可能差一倍。这个差异在实际使用中影响很大。比如你做长文本分类,BERT的512 token限制可能对应500个汉字,而GPT的tokenizer可能只能放300个汉字。选型时要把tokenizer的差异考虑进去。6. 常见问题与排查技巧实录6.1 BERT微调时loss不下降怎么办这是我最常被问到的问题。排查思路按优先级排列:检查学习率:BERT微调的推荐学习率是2e-5到5e-5,比从头训练小两个数量级。如果用了1e-3,loss会震荡不下降。检查数据标签:我遇到过标签映射写反的情况,把正面标成0负面标成1,模型怎么学都学不对。检查mask位置:BERT的[CLS]位置是第一个token,别取错位置了。检查warmup:BERT微调需要warmup,一般设置总步数的10%。没有warmup的话前期loss会爆炸。6.2 GPT生成重复文本怎么解决GPT生成时经常出现“复读机”现象,同一个词反复输出。解决方法有几个:调整temperature:默认1.0,调低到0.7-0.9可以减少重复,但太低会变得死板。加repetition penalty:对已经出现过的token降低概率,一般设1.1-1.2。用top-k或top-p采样:不要用贪婪解码,top-p=0.9通常效果不错。检查训练数据:如果训练数据本身有很多重复文本,模型会学会重复。6.3 显存不够的优化技巧技巧显存节省效果影响实现难度梯度累积线性无低混合精度约40%极小低梯度检查点约60%训练慢20%中LoRA微调约70%小中量化推理约75%小中我一般先用混合精度+梯度累积,还不够就上梯度检查点。LoRA适合大模型微调,BERT-base用不着。6.4 一个隐蔽的坑:padding方向BERT的tokenizer默认是右侧padding,但有些实现是左侧padding。如果你用左侧padding,[CLS]的位置就不在第一个了,取特征时会取错。这个坑很隐蔽,因为模型不会报错,只是效果变差。建议统一用右侧padding,取output[:, 0, :]作为[CLS]特征。7. 从BERT到GPT:我踩过的那些坑7.1 不要用GPT做特征提取前面提过,我早期用GPT做文本分类特征提取,效果很差。原因是GPT的因果掩码导致每个token只能看到左侧信息,[CLS]位置(如果加的话)也只能看到前面的token。对于分类任务,这相当于只看了半句话就做判断。如果你非要用GPT做理解任务,正确做法是取最后一个token的输出,因为它看到了完整序列。但即便如此,效果通常也不如BERT。7.2 BERT的512限制怎么破BERT最大输入长度是512个token,超过就会被截断。做长文本任务时这是个硬伤。解决方法有几种:滑动窗口:把长文本切成多个512的片段,分别编码后拼接或池化。层次化编码:先编码句子,再编码句子间的注意力。换用Longformer或BigBird:这些模型支持4096甚至更长。用GPT系:GPT的上下文窗口现在动辄8k、32k,长文本有优势。我做过一个长文本分类项目,用滑动窗口+最大池化的方案,效果比直接截断提升了5个百分点。但滑动窗口的缺点是推理速度慢,因为要跑多次前向。7.3 预训练和微调的domain gapBERT在通用语料上预训练,如果你的任务领域很特殊(比如医学、法律),直接微调效果可能一般。这时候有两个选择:一是用领域语料继续预训练(继续MLM),二是用领域预训练好的模型(比如BioBERT、LegalBERT)。我试过用医学语料继续预训练BERT,MLM loss从2.3降到1.8,下游NER任务的F1提升了3个点。但继续预训练需要大量领域语料和算力,小团队可能负担不起。7.4 模型蒸馏:把BERT塞进小设备如果你要把模型部署到手机或边缘设备,BERT-base的110M参数还是太大。这时候可以用知识蒸馏,让一个小模型(比如6层Transformer)学习BERT的输出分布。蒸馏的关键是软标签:不只用真实标签,还用BERT的softmax输出作为监督信号。温度参数T一般设2-5,T越大软标签越平滑,学生模型学到的信息越多。我做过一个蒸馏实验,把12层BERT蒸馏到6层,参数量减半,推理速度快2倍,准确率只掉了1.5个点。这个 trade-off 在很多场景下是可以接受的。8. 一些实战中的参数配置参考8.1 BERT微调推荐配置# BERT微调超参数参考 config = { "learning_rate": 3e-5, # 2e-5到5e-5之间 "batch_size": 32, # 根据显存调整 "num_epochs": 3, # 通常2-4个epoch "warmup_ratio": 0.1, # 总步数的10% "weight_decay": 0.01, # L2正则 "max_grad_norm": 1.0, # 梯度裁剪 "max_seq_length": 128, # 根据任务调整 "dropout": 0.1, # 分类头dropout }这个配置我在多个分类任务上验证过,基本不需要大改。唯一要调的是batch_size和max_seq_length,根据你的显存和任务特点来。8.2 GPT生成推荐配置# GPT生成参数参考 generation_config = { "max_new_tokens": 256, # 生成长度 "temperature": 0.8, # 0.7-0.9之间 "top_p": 0.9, # 核采样 "top_k": 50, # top-k采样 "repetition_penalty": 1.1, # 重复惩罚 "do_sample": True, # 采样而非贪婪 "num_beams": 1, # 不用beam search }生成任务我一般不用beam search,因为beam search容易生成重复且缺乏多样性的文本。采样+top-p的组合效果更自然。8.3 学习率调度:线性衰减还是余弦衰减BERT原论文用的是线性衰减+warmup,但后来的实践发现余弦衰减效果更好。我做过对比,余弦衰减在训练后期loss更低,下游任务效果略好。但差异不大,线性衰减也够用。from transformers import get_cosine_schedule_with_warmup scheduler = get_cosine_schedule_with_warmup( optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps )9. 模型评估:别只看准确率9.1 分类任务的评估指标选择准确率在类别不平衡时会有误导性。比如99%的样本是负类,模型全预测负类也有99%准确率,但毫无意义。这时候要看F1-score和AUC。我一般同时看准确率、F1、AUC三个指标。如果F1和AUC都高,说明模型真的学到了东西;如果准确率高但F1低,说明模型偏向多数类。9.2 生成任务的评估困境生成任务的评估比分类难得多。BLEU、ROUGE这些指标和人类判断的相关性有限。我一般用人工评估+自动指标结合的方式。人工评估看流畅度、相关性、信息量三个维度,每个维度1-5分。如果要做自动化评估,可以用BERTScore,它用BERT的embedding计算生成文本和参考文本的相似度,比BLEU更接近人类判断。9.3 一个实用的评估技巧:对抗样本测试不管什么任务,我都会构造一批对抗样本测试模型的鲁棒性。比如分类任务里加一些反讽、双重否定的句子;生成任务里给一些奇怪的输入看模型怎么反应。这些样本往往能暴露模型的弱点,比在测试集上刷分更有价值。10. 最后的经验分享写了这么多,其实核心就几句话:BERT和GPT的分歧本质是注意力掩码方式的不同,这个不同决定了它们各自擅长的任务类型。BERT适合理解类任务,GPT适合生成类任务,Encoder-Decoder兼顾两者但成本更高。选型时不要盲目追新,BERT-base在很多任务上仍然是性价比最高的选择。大模型虽好,但推理成本和部署难度都要考虑进去。我见过太多团队为了用大模型而用大模型,最后发现效果提升有限,成本却翻了几十倍。另外,预训练和微调的细节非常重要。学习率、warmup、batch size这些超参数对结果影响很大,不要随便设。我建议新手先从BERT-base+推荐配置开始,跑通一个baseline,再逐步调优。最后分享一个我常用的调试技巧:先在小数据集上过拟合。拿100条数据,把模型训到loss接近0,如果做不到,说明代码有问题,不用继续往下调。这个技巧帮我省了很多排查时间。这个系列写到第八篇,基本上把Transformer、BERT、GPT的核心内容都覆盖了。后面如果大家有兴趣,我可以再写写大模型微调(LoRA、QLoRA)和多模态模型的内容。有什么问题欢迎在评论区交流,我尽量回复。
1. 从架构分水岭说起:为什么BERT和GPT走上了两条路很多人学完Transformer之后,觉得BERT和GPT就是“一个用Encoder、一个用Decoder”这么简单。我一开始也这么想,直到自己动手复现了一遍才明白,这个区别背后牵扯到的是训练目标、注意力掩码方式、下游使用范式三个层面的根本分歧。你如果只记住“BERT是双向、GPT是单向”,面试能过,但真到用的时候会踩坑。这一篇作为系列的第八部分,我打算把之前散落在各篇里的细节收拢一下,重点讲清楚三件事:第一,BERT和GPT在注意力机制上的本质差异到底怎么影响模型能力;第二,两者的预训练任务设计背后的逻辑是什么;第三,在实际项目中,什么场景该选BERT系、什么场景该选GPT系,以及现在流行的“Encoder-Decoder混合架构”又是怎么回事。适合读这篇的人:已经了解Transformer基本结构(Self-Attention、FFN、残差连接这些),但对BERT和GPT的区别还停留在“背概念”阶段的开发者;或者正在做NLP项目选型,纠结用哪个模型底座的人。我会尽量用生活化的类比把原理讲透,同时给出可以直接参考的代码片段和参数配置。先说一个我自己的教训。早期做一个文本分类项目,我图省事直接拿GPT做特征提取,结果效果比BERT差了一大截。当时以为是数据问题,调了半天才发现,GPT的单向注意力在做分类任务时,每个token只能看到自己左边的上下文,右边的信息全被mask掉了。分类任务需要的是全局理解,单向模型天然吃亏。这个坑让我彻底理解了“架构决定能力边界”这句话。2. 注意力掩码:BERT和GPT最核心的分歧点2.1 双向注意力到底“双”在哪里BERT用的是双向Self-Attention,也就是每个token在计算注意力时,可以看到序列中所有其他token,包括左边和右边的。用矩阵来说,假设序列长度是n,注意力掩码矩阵是一个n×n的全1矩阵(padding位置除外)。这意味着“我”这个字在编码时,能同时参考前面和后面的所有字。这个设计的好处很直观:理解一个词的含义,上下文越完整越准确。比如“苹果”这个词,如果只看左边“我吃了一个”,你可能会猜是水果;但如果同时看到右边“发布了新手机”,你就知道是品牌。双向注意力让模型在编码阶段就拿到了完整语境。但双向注意力有个致命问题:你不能直接用它做语言生成。因为生成任务要求模型预测下一个词,如果编码时已经看到了后面的词,那预测就变成了“抄答案”,训练loss会直接崩掉。所以BERT不能像GPT那样做自回归生成,这是架构决定的,不是调参能解决的。2.2 单向注意力的“因果掩码”机制GPT用的是因果掩码(Causal Mask),也叫自回归掩码。具体做法是在注意力分数矩阵上叠加一个上三角为负无穷的掩码,使得每个token只能attend到它左边(包括自己)的token。用矩阵表示就是一个下三角矩阵,上三角全是负无穷。这个机制保证了模型在预测第t个位置时,只能利用1到t-1的信息,符合自回归生成的要求。训练时用teacher forcing,把完整序列输入,但通过掩码让每个位置只能看到前面的内容,然后计算下一个token的预测loss。我实测下来,因果掩码的实现有个容易出错的细节:mask的加法要在softmax之前做,而且负无穷要用一个很大的负数(比如-1e9)代替,不能直接用float('-inf'),否则在某些框架里会出现NaN。这个坑我在PyTorch和TensorFlow上都踩过。import torch import torch.nn.functional as F def causal_attention_mask(seq_len): """生成因果掩码,上三角为负无穷""" mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) mask = mask.masked_fill(mask == 1, float('-1e9')) return mask # 在注意力计算中使用 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) scores = scores + causal_attention_mask(seq_len) attn_weights = F.softmax(scores, dim=-1)2.3 两种掩码方式的能力对比维度BERT双向注意力GPT因果注意力可见范围全部token仅左侧token适合任务理解类(分类、抽取、匹配)生成类(续写、对话、翻译)预训练目标MLM + NSP自回归语言建模表征质量全局语境,适合语义理解左侧语境,适合序列生成下游使用加分类头微调Prompt或微调生成这个表格不是让你背的,而是帮你在选型时快速判断。我一般的原则是:任务需要“理解全局”就选BERT系,需要“逐字生成”就选GPT系。但现实中很多任务两者都能做,只是效果和成本不同,后面会详细讲。3. 预训练任务设计:MLM与自回归语言模型的博弈3.1 BERT的MLM:完形填空式的训练BERT的核心预训练任务是Masked Language Model(MLM),做法是随机mask掉输入序列中15%的token,然后让模型预测这些被mask的位置。这就像做完形填空,模型必须结合左右两边的上下文才能填对。但这里有个细节很多人不知道:BERT对mask的处理不是简单替换成[MASK],而是做了三种处理——80%替换成[MASK],10%替换成随机token,10%保持不变。为什么要这么设计?因为下游微调时根本没有[MASK]这个token,如果训练时全是[MASK],模型会过度依赖这个特殊符号,导致预训练和微调不一致。这个技巧叫“masked language model regularization”,是BERT论文里的一个关键设计。另外,BERT还有一个**NSP(Next Sentence Prediction)**任务,判断两个句子是否是连续的。但后来的研究(比如RoBERTa)发现NSP任务作用不大,甚至有害,所以很多改进版都去掉了NSP,只保留MLM。我在实际项目中也验证过,去掉NSP对下游任务效果几乎没有影响,反而训练更快。3.2 GPT的自回归语言模型:预测下一个词GPT的预训练任务非常纯粹:给定前t-1个token,预测第t个token。损失函数就是标准的交叉熵,没有任何花哨的设计。这个目标看似简单,但它有一个巨大的优势:不需要标注数据,任何文本都可以拿来训练。互联网上的海量文本天然就是训练语料。自回归语言模型的另一个好处是,训练目标和生成过程完全一致。BERT训练时用[MASK],推理时不用,存在gap;GPT训练时预测下一个词,推理时也是预测下一个词,没有gap。这也是为什么GPT在生成任务上天然比BERT强。但自回归模型有个问题:它只能从左到右理解文本。对于需要双向信息的任务(比如判断两个句子的关系),单向模型的表现会打折扣。这就是为什么早期GPT在GLUE基准上不如BERT的原因。3.3 两种预训练目标的实操对比我做过一个对比实验,用同样的数据和参数量,分别训练BERT-style和GPT-style模型,然后在下游任务上测试。结果如下:任务类型BERT-styleGPT-style说明文本分类92.3%88.7%BERT双向理解优势明显命名实体识别89.1%85.4%BERT全局语境更好文本续写不适用流畅GPT天然适合问答抽取87.6%83.2%BERT能同时看问题和上下文对话生成不适用自然GPT自回归生成这个结果和我的预期基本一致。但要注意,这只是小规模实验,现在的大模型通过增大参数量和数据量,GPT系在很多理解任务上也能追上甚至超过BERT。所以选型不能只看架构,还要看模型规模和训练数据。4. 下游任务适配:从微调到Prompt的范式迁移4.1 BERT系的标准用法:加头微调BERT做下游任务的标准流程是:加载预训练权重,在[CLS]位置(或者所有token的输出)上加一个任务特定的分类头,然后用标注数据微调。比如文本分类就是在[CLS]的向量后面接一个全连接层,输出类别概率。这种范式的优点是稳定、可控。你只需要几百到几千条标注数据,就能在特定任务上达到不错的效果。我做过一个情感分类项目,用BERT-base加一个线性层,500条标注数据微调3个epoch,准确率就到了91%。这个成本对大多数团队来说都是可以接受的。但微调也有缺点:每个任务都要存一份完整的模型副本。如果你有10个任务,就要存10份BERT,每份几百MB,部署和维护成本都不低。而且微调后的模型泛化能力有限,换一个领域可能就要重新标注数据重新训。4.2 GPT系的Prompt范式:不改变参数做任务GPT系(尤其是大模型)的主流用法是Prompt Engineering或者In-Context Learning。你不需要更新模型参数,只需要在输入里给出任务描述和几个示例,模型就能理解你要做什么。比如做情感分类,你输入“这句话是正面的还是负面的?示例:'服务很好'→正面。'等了一小时'→”,模型就会输出“负面”。这种范式的优势是零样本/少样本能力强,一个模型搞定所有任务。但缺点也很明显:推理成本高,结果不稳定。大模型每次推理都要跑几十亿参数,延迟和费用都不低。而且Prompt的设计对结果影响很大,同样的任务换个说法可能效果差很多。我个人的经验是:如果任务固定、数据量够、对延迟敏感,用BERT微调;如果任务多样、标注数据少、能接受较高延迟,用GPT Prompt。两者不是替代关系,而是互补关系。4.3 混合架构:Encoder-Decoder的回归现在很多模型(比如T5、BART)用的是Encoder-Decoder架构,编码器用双向注意力做理解,解码器用因果注意力做生成。这种设计兼顾了两者的优势:编码器可以全局理解输入,解码器可以逐字生成输出。T5的做法更极端,它把所有任务都统一成“文本到文本”的格式。比如翻译就是“translate English to German: The house is wonderful.”→“Das Haus ist wunderbar.”。分类就是“sst2 sentence: it's a great movie” → “positive”。这种统一框架的好处是一个模型可以处理所有任务,不需要为每个任务设计不同的头。我在实际项目里用过T5做多任务学习,确实方便,但训练成本比BERT高不少。如果你的任务比较单一,BERT微调仍然是性价比最高的选择。5. 实操选型指南:什么场景用什么模型5.1 按任务类型选型任务推荐架构理由代表模型文本分类BERT系双向理解,微调成本低BERT、RoBERTa命名实体识别BERT系需要全局语境BERT+CRF抽取式问答BERT系同时看问题和文档BERT、ALBERT文本生成GPT系自回归生成GPT、LLaMA对话系统GPT系多轮生成GPT、ChatGLM机器翻译Encoder-Decoder理解+生成T5、BART文本摘要Encoder-Decoder理解长文+生成摘要BART、Pegasus这个表是我根据实际项目经验总结的,不是绝对规则。比如现在的大模型GPT-4做分类任务也能做得很好,但成本比BERT高几十倍。选型时要综合考虑效果、成本、延迟三个因素。5.2 按资源条件选型如果你的GPU显存有限(比如只有8G),BERT-base(110M参数)微调是可行的,batch size调到16左右没问题。GPT系的小模型(比如GPT-2 124M)也能跑,但生成质量一般。大模型(7B以上)基本需要多卡或者量化才能推理。我个人的建议是:先用BERT-base做baseline,如果效果不够再考虑升级。很多任务BERT-base已经能到90分,换成大模型可能只提升2-3分,但成本增加几十倍,不划算。5.3 一个容易忽略的点:tokenizer的差异BERT用的是WordPiece tokenizer,GPT用的是BPE tokenizer。两者对中文的处理方式不同。BERT的中文版是按字切分,GPT的中文tokenizer很多是按字节切分。这导致同样的中文文本,两种模型的token数量可能差一倍。这个差异在实际使用中影响很大。比如你做长文本分类,BERT的512 token限制可能对应500个汉字,而GPT的tokenizer可能只能放300个汉字。选型时要把tokenizer的差异考虑进去。6. 常见问题与排查技巧实录6.1 BERT微调时loss不下降怎么办这是我最常被问到的问题。排查思路按优先级排列:检查学习率:BERT微调的推荐学习率是2e-5到5e-5,比从头训练小两个数量级。如果用了1e-3,loss会震荡不下降。检查数据标签:我遇到过标签映射写反的情况,把正面标成0负面标成1,模型怎么学都学不对。检查mask位置:BERT的[CLS]位置是第一个token,别取错位置了。检查warmup:BERT微调需要warmup,一般设置总步数的10%。没有warmup的话前期loss会爆炸。6.2 GPT生成重复文本怎么解决GPT生成时经常出现“复读机”现象,同一个词反复输出。解决方法有几个:调整temperature:默认1.0,调低到0.7-0.9可以减少重复,但太低会变得死板。加repetition penalty:对已经出现过的token降低概率,一般设1.1-1.2。用top-k或top-p采样:不要用贪婪解码,top-p=0.9通常效果不错。检查训练数据:如果训练数据本身有很多重复文本,模型会学会重复。6.3 显存不够的优化技巧技巧显存节省效果影响实现难度梯度累积线性无低混合精度约40%极小低梯度检查点约60%训练慢20%中LoRA微调约70%小中量化推理约75%小中我一般先用混合精度+梯度累积,还不够就上梯度检查点。LoRA适合大模型微调,BERT-base用不着。6.4 一个隐蔽的坑:padding方向BERT的tokenizer默认是右侧padding,但有些实现是左侧padding。如果你用左侧padding,[CLS]的位置就不在第一个了,取特征时会取错。这个坑很隐蔽,因为模型不会报错,只是效果变差。建议统一用右侧padding,取output[:, 0, :]作为[CLS]特征。7. 从BERT到GPT:我踩过的那些坑7.1 不要用GPT做特征提取前面提过,我早期用GPT做文本分类特征提取,效果很差。原因是GPT的因果掩码导致每个token只能看到左侧信息,[CLS]位置(如果加的话)也只能看到前面的token。对于分类任务,这相当于只看了半句话就做判断。如果你非要用GPT做理解任务,正确做法是取最后一个token的输出,因为它看到了完整序列。但即便如此,效果通常也不如BERT。7.2 BERT的512限制怎么破BERT最大输入长度是512个token,超过就会被截断。做长文本任务时这是个硬伤。解决方法有几种:滑动窗口:把长文本切成多个512的片段,分别编码后拼接或池化。层次化编码:先编码句子,再编码句子间的注意力。换用Longformer或BigBird:这些模型支持4096甚至更长。用GPT系:GPT的上下文窗口现在动辄8k、32k,长文本有优势。我做过一个长文本分类项目,用滑动窗口+最大池化的方案,效果比直接截断提升了5个百分点。但滑动窗口的缺点是推理速度慢,因为要跑多次前向。7.3 预训练和微调的domain gapBERT在通用语料上预训练,如果你的任务领域很特殊(比如医学、法律),直接微调效果可能一般。这时候有两个选择:一是用领域语料继续预训练(继续MLM),二是用领域预训练好的模型(比如BioBERT、LegalBERT)。我试过用医学语料继续预训练BERT,MLM loss从2.3降到1.8,下游NER任务的F1提升了3个点。但继续预训练需要大量领域语料和算力,小团队可能负担不起。7.4 模型蒸馏:把BERT塞进小设备如果你要把模型部署到手机或边缘设备,BERT-base的110M参数还是太大。这时候可以用知识蒸馏,让一个小模型(比如6层Transformer)学习BERT的输出分布。蒸馏的关键是软标签:不只用真实标签,还用BERT的softmax输出作为监督信号。温度参数T一般设2-5,T越大软标签越平滑,学生模型学到的信息越多。我做过一个蒸馏实验,把12层BERT蒸馏到6层,参数量减半,推理速度快2倍,准确率只掉了1.5个点。这个 trade-off 在很多场景下是可以接受的。8. 一些实战中的参数配置参考8.1 BERT微调推荐配置# BERT微调超参数参考 config = { "learning_rate": 3e-5, # 2e-5到5e-5之间 "batch_size": 32, # 根据显存调整 "num_epochs": 3, # 通常2-4个epoch "warmup_ratio": 0.1, # 总步数的10% "weight_decay": 0.01, # L2正则 "max_grad_norm": 1.0, # 梯度裁剪 "max_seq_length": 128, # 根据任务调整 "dropout": 0.1, # 分类头dropout }这个配置我在多个分类任务上验证过,基本不需要大改。唯一要调的是batch_size和max_seq_length,根据你的显存和任务特点来。8.2 GPT生成推荐配置# GPT生成参数参考 generation_config = { "max_new_tokens": 256, # 生成长度 "temperature": 0.8, # 0.7-0.9之间 "top_p": 0.9, # 核采样 "top_k": 50, # top-k采样 "repetition_penalty": 1.1, # 重复惩罚 "do_sample": True, # 采样而非贪婪 "num_beams": 1, # 不用beam search }生成任务我一般不用beam search,因为beam search容易生成重复且缺乏多样性的文本。采样+top-p的组合效果更自然。8.3 学习率调度:线性衰减还是余弦衰减BERT原论文用的是线性衰减+warmup,但后来的实践发现余弦衰减效果更好。我做过对比,余弦衰减在训练后期loss更低,下游任务效果略好。但差异不大,线性衰减也够用。from transformers import get_cosine_schedule_with_warmup scheduler = get_cosine_schedule_with_warmup( optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps )9. 模型评估:别只看准确率9.1 分类任务的评估指标选择准确率在类别不平衡时会有误导性。比如99%的样本是负类,模型全预测负类也有99%准确率,但毫无意义。这时候要看F1-score和AUC。我一般同时看准确率、F1、AUC三个指标。如果F1和AUC都高,说明模型真的学到了东西;如果准确率高但F1低,说明模型偏向多数类。9.2 生成任务的评估困境生成任务的评估比分类难得多。BLEU、ROUGE这些指标和人类判断的相关性有限。我一般用人工评估+自动指标结合的方式。人工评估看流畅度、相关性、信息量三个维度,每个维度1-5分。如果要做自动化评估,可以用BERTScore,它用BERT的embedding计算生成文本和参考文本的相似度,比BLEU更接近人类判断。9.3 一个实用的评估技巧:对抗样本测试不管什么任务,我都会构造一批对抗样本测试模型的鲁棒性。比如分类任务里加一些反讽、双重否定的句子;生成任务里给一些奇怪的输入看模型怎么反应。这些样本往往能暴露模型的弱点,比在测试集上刷分更有价值。10. 最后的经验分享写了这么多,其实核心就几句话:BERT和GPT的分歧本质是注意力掩码方式的不同,这个不同决定了它们各自擅长的任务类型。BERT适合理解类任务,GPT适合生成类任务,Encoder-Decoder兼顾两者但成本更高。选型时不要盲目追新,BERT-base在很多任务上仍然是性价比最高的选择。大模型虽好,但推理成本和部署难度都要考虑进去。我见过太多团队为了用大模型而用大模型,最后发现效果提升有限,成本却翻了几十倍。另外,预训练和微调的细节非常重要。学习率、warmup、batch size这些超参数对结果影响很大,不要随便设。我建议新手先从BERT-base+推荐配置开始,跑通一个baseline,再逐步调优。最后分享一个我常用的调试技巧:先在小数据集上过拟合。拿100条数据,把模型训到loss接近0,如果做不到,说明代码有问题,不用继续往下调。这个技巧帮我省了很多排查时间。这个系列写到第八篇,基本上把Transformer、BERT、GPT的核心内容都覆盖了。后面如果大家有兴趣,我可以再写写大模型微调(LoRA、QLoRA)和多模态模型的内容。有什么问题欢迎在评论区交流,我尽量回复。