1. 为什么BERT至今仍是面试官手里的“照妖镜”如果你最近面过几家像样的公司不管是做推荐、搜索还是对话大概率会被问到BERT。很多人觉得这玩意儿都出来好几年了Transformer都迭代好几轮了怎么还在问我跟你讲面试官问BERT根本不是指望你背出论文里的公式他是在用BERT当“照妖镜”——一个对基础架构理解不透彻的人在BERT的细节追问下三句话就会露馅。我自己带过不少新人也帮朋友做过模拟面试。一个很普遍的现象是大家能把BERT的输入输出说清楚也能说出“用了Transformer Encoder”但一旦问到“为什么BERT的[CLS]能用来做分类”“为什么预训练要搞两个任务”“为什么position embedding是学出来的而不是像Transformer原版那样用三角函数”就开始支支吾吾。这些问题的答案恰恰是区分“调包侠”和“真正理解模型”的分水岭。这篇文章我打算把BERT从里到外拆一遍。不是那种照着论文目录念的拆法而是按照一个面试官可能追问的路径来拆从输入表示到预训练任务从微调策略到变体演进再到实际落地时的坑。每一块我都会告诉你“为什么这么设计”“面试官想听什么”“你该怎么答才显得有深度”。适合正在准备面试的算法工程师也适合想真正搞懂BERT而不是只会from transformers import BertModel的开发者。提示本文默认你已经知道Transformer的基本结构Self-Attention、Multi-Head、FFN、残差连接。如果这些还不熟建议先把Transformer那篇论文过一遍否则后面的内容会有点吃力。2. 输入表示层三个Embedding相加背后藏了多少考点2.1 Token EmbeddingWordPiece到底解决了什么问题BERT用的是WordPiece分词不是整词也不是字符。为什么因为整词会导致词表爆炸英文几十万词OOV问题严重纯字符又会让序列变得很长而且单个字符携带的语义信息太少。WordPiece是一种折中它从字符开始逐步合并那些高频共现的片段最终得到一个大小可控BERT-base是30522的词表。举个例子“playing”可能被切成“play”“##ing”“unhappiness”可能被切成“un”“##happi”“##ness”。注意那个“##”前缀它表示这个子词不是词的开头。面试官如果问你“##是什么意思”你答“表示子词接续”这就到位了。这里有个容易被忽略的细节WordPiece的合并策略是基于似然度的不是简单的频率。具体来说它会选择能让语言模型概率提升最大的那一对相邻片段进行合并。这个点在面试里说出来会让人觉得你确实读过源码或者至少读过相关文档。2.2 Segment Embedding不只是为了区分两句话Segment Embedding只有两个向量EA和EB。第一句话的所有token加EA第二句话的所有token加EB。很多人以为这只是为了告诉模型“这两段话来自不同句子”但更深层的原因是BERT的预训练任务NSPNext Sentence Prediction需要模型理解句子间的关系而Segment Embedding就是给模型提供这个信息的唯一显式信号。你可能会问Self-Attention本身就能让不同位置的token互相看到为什么还需要Segment Embedding因为Attention是位置无关的它不知道哪些token属于句子A哪些属于句子B。如果没有Segment Embedding模型只能靠[SEP]的位置来推断这在高维空间里是很不稳定的。加上Segment Embedding之后模型就有了一个明确的“归属标签”。2.3 Position Embedding为什么BERT选择“学”而不是“算”Transformer原版用的是正弦余弦函数来生成位置编码这是固定的、不需要学习的。但BERT改成了可学习的位置嵌入最大长度512每个位置一个向量。为什么我个人的理解是正弦余弦编码在理论上可以外推到比训练时更长的序列但实际效果并不好因为模型在训练时没见过那些位置组合。而可学习的位置嵌入虽然不能外推超过512就报错但在512以内的表达能力更强模型可以根据任务需要灵活调整每个位置的含义。对于BERT这种“预训练微调”的范式来说固定长度反而不是大问题因为绝大多数下游任务的序列长度都在512以内。面试里如果被问到“为什么不用正弦余弦”你可以从“表达能力”和“实际效果”两个角度答再补一句“后来很多变体比如RoBERTa也沿用了可学习位置嵌入说明这个选择在实践中被验证了”。2.4 三个Embedding相加为什么是相加不是拼接这是一个高频考点。拼接会让维度变成原来的三倍参数量和计算量都上去了而且后续的LayerNorm和Attention需要重新设计维度。相加则保持了维度不变同时让三个信息在同一个空间里融合。你可以理解为每个位置的最终表示是“这个词是什么”“它属于哪句话”“它在什么位置”三个信息的叠加。但相加也有个潜在问题信息会混在一起模型需要自己学会区分。不过Transformer的多头注意力机制本身就有很强的解耦能力不同头可以关注不同的子空间所以这个问题在实践中并不严重。注意BERT的输入还有LayerNorm和Dropout顺序是Embedding相加 → LayerNorm → Dropout。这个顺序在面试里被问到过别答反了。3. 预训练任务MLM和NSP的设计逻辑与争议3.1 Masked Language Model15%的mask率是怎么来的MLM的核心思想是随机遮住输入中的一些token让模型根据上下文预测这些token。这就像做完形填空。但这里有几个非常关键的细节面试官特别喜欢追问。第一为什么是15%这个比例是实验试出来的。太低模型学不到足够的语言知识太高上下文信息太少任务变得太难而且和下游任务下游任务不会mask的差距太大。15%是一个经验上的平衡点。第二15%里面怎么处理BERT的做法是80%替换成[MASK]10%替换成随机词10%保持不变。为什么要这么复杂因为如果100%都替换成[MASK]模型会学到“只要看到[MASK]就预测”的捷径但下游任务里根本没有[MASK]这个token这就导致了预训练和微调之间的不一致。加入随机词和不变词是为了让模型不能偷懒必须真正依赖上下文来预测。第三[MASK] token本身的问题。BERT引入了一个特殊的[MASK] token但这个token在真实文本中不存在。这意味着模型学到的[MASK]位置的表示和正常位置的表示是有偏差的。这个问题后来被RoBERTa指出来了RoBERTa干脆去掉了NSP并且动态改变mask的位置让模型看到更多样的mask模式。3.2 Next Sentence Prediction一个被证明“没那么有用”的任务NSP的任务是给两句话判断第二句是不是第一句的真实下一句。正样本是连续的句子对负样本是随机拼接的句子对。这个任务的初衷是让模型学会句子间的关系这对问答、自然语言推理等任务很重要。但后来RoBERTa的实验表明NSP任务可能并没有那么有用甚至可能有害。RoBERTa去掉了NSP只保留MLM结果在多个任务上反而更好。原因是NSP任务太简单了模型可以通过主题相似度等浅层特征来判断而不是真正理解句子间的逻辑关系。后来ALBERT提出了SOPSentence Order Prediction让模型判断两句话的顺序这个任务更难效果也更好。面试里如果被问到“NSP有什么用”你可以先讲原始设计意图再补一句“后续研究表明NSP可能不是最优选择RoBERTa和ALBERT分别用去掉NSP和SOP来改进”这样既展示了基础知识又展示了对领域进展的关注。3.3 预训练的数据与算力为什么普通人复现不了BERT-base是在BooksCorpus8亿词和英文维基百科25亿词上训练的总共约33亿词。训练用了64块TPU跑了4天。这个算力门槛决定了绝大多数人只能做微调做不了预训练。但这里有个面试常问的点如果给你一个垂直领域比如医疗、法律你会怎么继续预训练答案通常是在领域语料上继续做MLM学习率要小比如1e-5到5e-5训练步数不用太多否则会灾难性遗忘。这个策略在BioBERT、LegalBERT等模型上都被验证过。4. 微调策略怎么把预训练模型用到自己的任务上4.1 分类任务为什么用[CLS]而不是平均池化BERT的输出是一个序列每个token对应一个向量。做句子分类时最直接的做法是取[CLS]位置的向量接一个全连接层。为什么是[CLS]因为[CLS]在预训练时并没有对应的具体token它相当于一个“汇总位”通过Attention机制它可以聚合整个序列的信息。而且NSP任务就是基于[CLS]做的所以模型被训练成把句子级信息编码到[CLS]里。但[CLS]并不是唯一选择。你也可以对所有token的向量做平均池化或者用Attention加权。实际效果上[CLS]通常够用但在某些任务上平均池化可能更好。面试里如果被问到你可以说“BERT原论文用的是[CLS]但后续很多工作发现平均池化在某些任务上更稳定具体用哪个最好在验证集上试一下”。4.2 学习率与层数选择微调不是“全量更新”那么简单微调BERT时学习率通常设得很小比如2e-5、3e-5、5e-5。为什么因为预训练模型已经学到了很好的表示微调只是在这个基础上做“微调”学习率太大会破坏预训练学到的知识。另一个技巧是分层学习率底层靠近输入的层学习率小一点顶层靠近输出的层学习率大一点。因为底层学的是通用语言特征词法、句法这些不需要大改顶层学的是任务相关特征需要更多调整。这个技巧在FastAI等框架里被广泛使用。还有一个问题是微调时要不要冻结某些层如果下游任务数据量很小比如几千条冻结底层、只训练顶层和分类头可以防止过拟合。如果数据量足够大几十万条全量微调通常效果更好。4.3 序列长度与批大小的权衡BERT的最大序列长度是512。如果你的文本超过512怎么办常见做法是截断取前512个token或者分段把文本切成多个512的片段分别过BERT再聚合。截断会丢失信息分段会增加计算量。实际中如果关键信息通常在开头截断就够了如果关键信息可能在任何位置分段更稳妥。批大小方面BERT-base在16G显存的GPU上序列长度128时可以用32的批大小序列长度512时可能只能用到8或16。如果显存不够可以用梯度累积来模拟大批大小。这些工程细节在面试里如果被问到能答出来会显得你有实际落地经验。5. BERT的变体家族面试官想看你知不知道“后来发生了什么”5.1 RoBERTa去掉NSP动态Mask更大数据RoBERTa是BERT的“加强版”主要改动有去掉NSP任务、动态mask每次epoch重新mask而不是预训练时固定mask、更大的batch size、更多的训练数据160G vs BERT的16G。这些改动让RoBERTa在GLUE等基准上全面超过BERT。面试里如果问“BERT有什么改进方向”RoBERTa是最标准的答案。你可以说“RoBERTa证明了NSP不是必需的动态mask比静态mask更有效数据量和训练时长对最终效果影响很大”。5.2 ALBERT参数共享与SOPALBERT的核心是减少参数量。它用了两种技术一是嵌入矩阵分解把大的词嵌入矩阵分解成两个小矩阵二是跨层参数共享所有Transformer层共享同一套参数。这让ALBERT的参数量远小于BERT但效果不降反升。另外ALBERT用SOP替代了NSP。SOP是判断两句话的顺序比NSP更难因为模型不能靠主题相似度来作弊必须理解句子间的逻辑顺序。5.3 DistilBERT知识蒸馏的典型应用DistilBERT用知识蒸馏把BERT-base压缩到6层参数量减少40%速度提升60%但保留了97%的性能。蒸馏时学生模型不仅学习真实标签还学习教师模型的软标签softmax输出的概率分布。这个思路在模型压缩领域非常经典。面试里如果问“怎么把BERT部署到移动端”DistilBERT、TinyBERT、MobileBERT都是可以提的方案。关键是要说出“知识蒸馏”“量化”“剪枝”这些关键词。6. 实际落地中的坑从数据预处理到线上部署6.1 数据预处理tokenizer的坑比你想象的多第一个坑是tokenizer.encode和tokenizer.encode_plus的区别。前者只返回token ids后者返回input_ids、attention_mask、token_type_ids等。做BERT微调时必须用encode_plus或者__call__因为attention_mask是必须的否则padding位置会被Attention看到影响结果。第二个坑是最大长度的设置。如果你设了max_length128但实际数据里有大量超过128的样本截断会丢失信息。建议先统计一下数据的长尾分布再决定截断长度。第三个坑是特殊token的位置。[CLS]必须在开头[SEP]必须在每句话结尾。如果你自己拼接句子忘了加[SEP]模型的效果会大打折扣。6.2 训练时的显存优化梯度累积与混合精度显存不够是常态。除了减小batch size还有两个常用技巧梯度累积和混合精度训练。梯度累积是跑多个小batch把梯度累加起来再更新一次参数相当于模拟了大batch。混合精度是用fp16做前向和反向用fp32保存参数可以节省约一半显存速度也更快。但混合精度有个坑fp16的数值范围小容易溢出。所以通常要用动态损失缩放dynamic loss scaling让损失值在反向传播前放大更新后再缩回去。PyTorch的torch.cuda.amp已经封装好了这些用起来很方便。6.3 线上部署推理速度与模型大小的平衡线上部署BERT时延迟和吞吐是核心指标。如果直接用PyTorch跑单条推理可能几十毫秒QPS上不去。常见的优化手段有ONNX Runtime、TensorRT、量化int8、蒸馏用小模型。具体选哪个取决于你的硬件和延迟要求。还有一个容易被忽略的点tokenizer的速度。Python的tokenizer在大批量推理时可能成为瓶颈。可以用Rust实现的tokenizer比如HuggingFace的tokenizers库速度快很多。7. 面试模拟几个高频追问与参考回答7.1 “BERT为什么用双向而不是单向”这个问题考的是你对语言模型范式的理解。单向模型比如GPT只能看到左边的上下文适合生成任务。双向模型可以看到左右两边适合理解任务。BERT通过Masked Language Model实现了双向因为如果不用mask模型在预测某个词时就能直接看到这个词本身任务就没意义了。7.2 “BERT的Attention和Transformer原版有什么区别”结构上基本一样都是Multi-Head Self-Attention。区别在于BERT用了可学习的位置嵌入而原版用正弦余弦。另外BERT的输入多了Segment Embedding。还有就是BERT的预训练任务和原版Transformer的机器翻译任务完全不同。7.3 “如果让你改进BERT你会怎么做”这个问题没有标准答案但你可以从几个角度答数据层面更多领域数据、动态mask、任务层面用SOP替代NSP、加入对比学习、结构层面参数共享、稀疏Attention、训练层面更大batch、更长训练。关键是说出你的理由而不是堆砌名词。7.4 “BERT和Word2Vec的区别是什么”Word2Vec是静态词向量每个词只有一个向量不管上下文。BERT是动态词向量同一个词在不同句子里有不同的表示。比如“苹果”在“吃苹果”和“苹果手机”里的向量是不一样的。这个区别是BERT能大幅提升NLP任务效果的核心原因。8. 我踩过的几个坑和给你的实用建议第一个坑是学习率设太大。我刚开始微调BERT时用了1e-3的学习率结果loss直接爆炸。后来改成2e-5才正常收敛。记住BERT微调的学习率通常在1e-5到5e-5之间不要超过1e-4。第二个坑是忘了加attention_mask。有一次我手动构造输入只传了input_ids没传attention_mask结果padding位置被Attention看到模型效果差了很多。后来检查了半天才发现这个问题。所以一定要用tokenizer的__call__方法它会自动处理attention_mask。第三个坑是序列长度设得太短。有个任务里关键信息在文本的后半部分但我设了max_length128结果关键信息被截掉了模型怎么调都上不去。后来改成256效果立刻提升。所以一定要先看看数据的长尾分布。第四个坑是没做领域适配。直接用BERT-base做医疗文本分类效果一般。后来在医疗语料上继续做了一轮MLM效果提升了几个点。如果你的领域和通用语料差异很大继续预训练是值得的。提示微调BERT时建议先用小学习率跑几个epoch观察验证集loss。如果loss不降可能是学习率太小如果loss震荡或爆炸可能是学习率太大。找到合适的区间后再跑完整训练。最后说一个我个人的体会BERT的很多设计选择比如15%的mask率、80/10/10的替换策略、可学习位置嵌入都不是理论推导出来的而是实验试出来的。面试官问这些不是要你背数字而是想看你有没有“实验思维”——知道一个设计为什么被选中以及如果换一种设计会有什么后果。这种思维比记住任何公式都重要。
阅读完成 · 觉得有帮助?