语言模型这玩意儿刚上手的时候很容易陷入一个误区觉得它就是个高级输入法你打几个字它顺着往下编。我刚开始接触那会儿也是这么想的拿个预训练模型跑续写看着它把今天天气接成今天天气真好适合出去玩觉得挺神奇但也就那样了。直到后来真正去拆Transformer的编码器结构、去跑BERT的微调任务才意识到会续写和会理解之间隔着的不是一代模型而是一整套建模范式的转变。这篇是下篇上篇我们聊了语言模型的基本分类和RNN那条技术路线的来龙去脉这篇重点落在理解这件事上——Transformer到底怎么让模型从接话进化到读懂BERT为什么能成为理解类任务的基石以及束搜索这类解码策略在实际生成中到底该怎么调。如果你正在做文本分类、语义匹配、信息抽取这类偏理解的任务或者想搞清楚预训练语言模型和生成式大语言模型到底是不是一回事这篇的内容应该能帮你把这条线捋清楚。1. 从续写到理解语言模型的能力分水岭到底在哪1.1 续写本质上是条件概率的滚动计算很多人第一次接触语言模型都是从给个开头让它往下写开始的。这个过程的数学本质其实很朴素给定前面的词序列预测下一个词的概率分布然后选一个词接上去再把新序列作为输入继续预测。用公式说就是 ( P(w_t | w_1, w_2, ..., w_{t-1}) )每一步都在算这个条件概率。RNN时代就是这么干的隐状态 ( h_t ) 携带历史信息一步步往前滚。问题是这个携带是有损的序列一长前面的信息就被稀释得差不多了。你让它续写一段话开头提了个人名写到第三段它可能就忘了这人叫啥。这不是模型笨是结构决定的——信息在时间步上反复做矩阵乘法梯度该消失的消失该爆炸的爆炸。所以会续写这个能力本质上是在做局部连贯性建模。它能把相邻几个词、一两句话串顺但它对整段文本的全局语义并没有一个稳定的表示。你给它苹果公司发布了新款手机它能接这款手机搭载了最新的芯片接得挺像那么回事。但你问它苹果公司是做什么的它不一定能给你一个准确的概括因为它训练时学的是下一个词是什么不是这段话在说什么。1.2 理解要求的是全局语义的压缩表示会理解就不一样了。理解意味着模型能把一整段文本压缩成一个固定维度的向量这个向量里编码了这段话的核心语义而且这个语义表示要足够稳定换一种说法、换一个语序向量不能跑太远。这就引出了Transformer编码器的核心设计。它不像RNN那样必须按顺序一步步来而是通过自注意力机制让序列里每个位置都能直接看到其他所有位置。一句话里银行这个词到底指金融机构还是河岸编码器可以通过它和存款利率或者河堤水流的注意力权重来判断。这种全局可见性是RNN给不了的。我自己的体会是判断一个模型是在续写还是在理解有个很简单的测试方法给它一段话让它输出这句话的向量表示然后换一种完全不同的表述方式说同一件事看两个向量的余弦相似度。如果相似度很高说明它抓的是语义如果相似度很低说明它抓的是表面词形。BERT在这类测试上的表现和纯生成式模型完全不是一个量级。1.3 编码器与解码器的分工不是随便定的Transformer原论文里 encoder 和 decoder 是配套出现的但后来大家发现这两块可以拆开用而且拆开之后各自擅长的任务非常明确。编码器擅长的是读——把输入序列编码成上下文相关的表示适合分类、匹配、抽取、序列标注这些不需要生成新文本的任务。解码器擅长的是写——自回归地一个词一个词往外蹦适合翻译、摘要、对话这些生成任务。BERT用的是纯编码器GPT系列用的是纯解码器T5这种用的是编码器加解码器的完整结构。这个分工不是拍脑袋定的。编码器的双向注意力让它在编码时能同时利用左右两侧的上下文这对理解任务是天然优势但双向注意力如果直接拿来做生成就会出现偷看答案的问题——预测第t个词的时候已经看到了第t1个词训练和推理对不上。所以生成任务必须用带因果掩码的解码器只能看左边。选模型之前先想清楚你的任务是读还是写。做文本分类硬上GPT系列不是不行但性价比很低做长文本生成硬上BERT那基本是走不通的。2. Transformer编码器的关键部件不是每个设计都只是为了更强2.1 自注意力为什么能替代循环结构自注意力的计算过程拆开看就三步每个输入向量分别投影成Query、Key、Value三个向量用Query和所有Key做点积算注意力分数用softmax归一化后对Value加权求和。这个机制最直接的好处是并行。RNN必须等前一个时间步算完才能算下一个GPU再强也只能干等着。自注意力里每个位置的Q和所有位置的K可以同时算序列长度是1000还是100计算图的深度是一样的。这对训练效率的提升是数量级的。另一个好处是路径长度。RNN里第一个词的信息要传到第100个词中间要经过99次传递每传一次都可能衰减。自注意力里第一个词和第100个词之间只有一次点积操作信息传递路径是O(1)。这就是为什么Transformer能处理长距离依赖而RNN一到长序列就拉胯。但自注意力也不是没有代价。它的计算复杂度是 ( O(n^2) )序列长度翻倍计算量翻四倍。这也是后来各种高效Transformer变体比如Swin Transformer用窗口注意力、Linformer用低秩近似要解决的问题。实际项目里如果你的序列长度经常超过512就得认真考虑注意力复杂度的优化了。2.2 多头注意力不是简单的多来几组多头注意力的做法是把Q、K、V分别投影到多个低维子空间在每个子空间里独立算注意力最后把结果拼起来再投影一次。直觉上的解释是不同的头可以关注不同的模式有的头关注语法关系有的头关注语义相似性有的头关注位置邻近。我实际拆过一些预训练模型的多头注意力权重确实能看到分工。有的头在句子对任务里专门盯着两个句子的对齐关系有的头在序列标注任务里关注相邻词。但这种分工不是人工设计的是训练自己学出来的不同模型、不同层学到的模式都不一样。实现上有个细节容易踩坑多头注意力的每个头维度是 ( d_{model} / h )比如768维、12个头每个头就是64维。拼接之后再做一次线性变换恢复到768维。这个线性变换不能省它负责把多个子空间的信息融合起来。我见过有人为了省参数把这个投影去掉结果模型效果掉得厉害。2.3 位置编码解决的是顺序感问题自注意力本身是位置无关的——把输入序列打乱注意力算出来的结果只是跟着打乱不会因为顺序变了就产生不同的表示。但语言是有顺序的猫追老鼠和老鼠追猫完全是两回事。原始Transformer用的是正弦位置编码用不同频率的sin和cos函数生成位置向量直接加到词嵌入上。这种编码的好处是可以外推到训练时没见过的长度但实际效果一般。后来BERT用的是可学习的位置嵌入每个位置学一个向量效果更稳但没法外推最大长度在预训练时就定死了。现在主流的大模型基本都转向了旋转位置编码RoPE或者相对位置编码。RoPE的思路是把位置信息编码成旋转操作作用在Q和K上这样注意力分数天然就带上了相对位置信息。我在实际项目里对比过RoPE在长文本任务上的表现确实比可学习位置嵌入好一截尤其是序列长度超过训练长度的时候。3. BERT的预训练目标为什么完形填空比预测下一个词更适合理解3.1 掩码语言模型的核心逻辑BERT最核心的预训练任务是Masked Language Model说白了就是完形填空。随机把输入序列里15%的词遮住让模型根据剩下的词预测被遮住的词。这个任务和预测下一个词有本质区别。预测下一个词只能看左边模型学的是单向的语言习惯完形填空能同时看左右两边模型必须真正理解上下文才能填对。比如我今天去___办了张卡遮住的是银行模型看到办了张卡就知道这里大概率是金融机构看到河就知道是河岸。这种双向约束逼着模型学语义而不是学词序。但这里有个工程上的坑预训练时遮了15%的词但微调的时候没有遮词这就导致预训练和微调之间存在不一致。BERT的解决办法是在遮住的15%里80%换成[MASK]标记10%换成随机词10%保持不变。这样模型不能只依赖[MASK]这个标记来定位要预测的位置必须对每个位置都保持警惕。3.2 下一句预测任务的争议与取舍BERT另一个预训练任务是Next Sentence Prediction给两个句子判断第二句是不是第一句的真实下一句。这个任务的初衷是让模型学会句子间的关系对问答、自然语言推理这类任务有帮助。但后来RoBERTa的论文直接把这个任务拿掉了发现去掉之后效果反而更好。原因大概是NSP任务太简单了模型靠一些表面线索就能猜对比如两个句子的话题是否一致、长度是否匹配根本没学到真正的逻辑关系。RoBERTa改用动态掩码和更大的batch size在多个基准上超过了原版BERT。我自己做句子对任务的时候如果数据量够一般会直接上RoBERTa或者Sentence-BERT不太会去纠结NSP。但如果数据量很小NSP预训练过的模型在收敛速度上确实有一点优势这个得看具体场景。3.3 微调阶段的学习率设置比预训练更敏感BERT微调有个经验值学习率用2e-5到5e-5epoch数3到4batch size 16到32。这个范围不是随便定的。学习率太大预训练学到的语言知识会被冲掉模型退化成随机初始化的状态学习率太小微调不动效果上不去。我踩过的一个坑是做小样本分类的时候一开始用了5e-5的学习率训练loss降得很快但验证集准确率一直上不去后来降到2e-5再配合warmup效果才出来。小样本场景下模型很容易过拟合到训练集的表面特征学习率低一点、训练轮数少一点反而更稳。另一个细节是BERT的不同层对任务的贡献不一样。底层学的是词法和句法高层学的是语义和任务相关特征。做分类任务时取[CLS]位置的输出接一个分类头就够了做序列标注时取每个位置的输出接一个线性层。有些论文会尝试对不同层做加权融合但实际提升有限除非你的任务特别复杂。4. 束搜索与解码策略生成任务里最容易被忽视的调参环节4.1 贪心解码为什么不够用生成任务里最朴素的解码方式是贪心每一步都选概率最大的那个词。这个方法快但容易陷入局部最优。比如我今天去了___贪心可能选公园因为公园的概率最高但整句话下来我今天去了医院看病可能才是更合理的序列。贪心解码的另一个问题是重复。模型可能陷入我今天去了公园公园公园这种循环因为每一步选最大概率词的时候前一步的输出又强化了当前步选同一个词的倾向。4.2 束搜索的宽度选择是个权衡束搜索的思路是每一步保留概率最高的k个候选序列最后从这k个里选整体概率最高的。k就是束宽k1就是贪心k越大搜索空间越广但计算量也越大。实际用的时候束宽不是越大越好。我做过一个文本摘要的实验束宽从1加到5ROUGE分数有明显提升但从5加到10提升就很小了推理时间却翻了一倍。一般任务束宽设3到5就够了特别追求生成质量的场景可以到10再往上性价比很低。束搜索还有个问题是倾向于生成短序列因为每一步都在累乘概率序列越长概率越低。解决办法是加长度惩罚把概率和除以序列长度的某个幂次。这个惩罚系数一般设0.6到1.0具体看任务对长度的偏好。4.3 采样策略在开放生成里的作用束搜索适合翻译、摘要这种有明确参考答案的任务但用在对话、故事生成这种开放性任务上会显得很死板生成的东西千篇一律。这时候就得用采样。温度采样是给logits除以一个温度系数再softmax。温度大于1分布更平生成更多样温度小于1分布更尖生成更保守。我一般对话场景用0.7到0.9创意写作可以用到1.0以上。Top-k采样是只从概率最高的k个词里采样避免选到太离谱的词。Top-p采样也叫核采样是累积概率达到p就截断比Top-k更灵活因为候选词的数量会随分布形状变化。实际用的时候Top-p设0.9左右比较稳配合温度0.8生成质量和多样性平衡得比较好。解码策略没有万能参数同一个模型在不同任务上最优的解码配置可能完全不同。建议在验证集上做一个小规模的网格搜索把束宽、温度、Top-p这几个参数组合跑一遍选效果最好的那组。5. 预训练语言模型和生成式大语言模型到底是不是一回事5.1 从参数规模看两者的重叠与分化严格来说预训练语言模型是一个更大的范畴BERT、GPT、T5、RoBERTa都算。生成式大语言模型是其中偏向生成、参数规模特别大的那一类比如GPT-3、GPT-4、LLaMA系列。但大这个字很模糊。BERT-large是3.4亿参数在2018年算大模型现在随便一个开源模型都是70亿参数起步3.4亿连小都算不上。所以更准确的说法是预训练语言模型强调的是预训练微调这个范式生成式大语言模型强调的是大规模生成能力少样本/零样本泛化。5.2 微调范式和提示范式的根本差异BERT时代的典型用法是拿预训练权重在自己的任务数据上微调每个任务一个模型。做情感分类微调一个做命名实体识别微调一个做问答再微调一个。这种范式下模型是任务专用的。GPT-3之后范式变成了提示工程不微调直接给模型几个例子或者一段指令让它自己完成任务。同一个模型换个提示就能做不同的事。这种范式的优势是通用性强劣势是推理成本高而且对提示的写法很敏感。我自己的做法是混合用。如果任务数据充足、对延迟敏感还是微调一个小模型划算如果任务多变、数据少、对延迟不敏感用大模型加提示更灵活。没必要非此即彼。5.3 本地部署大语言模型的实际考量现在本地部署大语言模型的门槛比前两年低多了有llama.cpp、Ollama、vLLM这些工具消费级显卡也能跑起来。但实际部署的时候有几个点得想清楚。显存是硬约束。7B参数的模型FP16精度大概需要14GB显存INT8量化后7GB左右INT4量化后4GB左右。量化会损失一点效果但INT8的损失通常很小INT4在复杂推理任务上会有可感知的下降。推理框架的选择也重要。llama.cpp适合CPU和Apple Silicon量化支持好vLLM适合GPU吞吐量高适合服务化部署Ollama封装得最好上手最快但定制化空间小。我一般做原型验证用Ollama正式部署用vLLM。还有个容易被忽视的点是上下文长度。模型支持的最大长度和实际能用的长度是两回事。很多模型标称支持32K甚至128K但实际用到一半以上时效果会明显下降因为训练时长文本样本本来就少。如果你的任务需要处理长文档最好先拿实际数据测一下别光看标称参数。6. 实操中绕不开的几个工程问题6.1 分词器选择对下游任务的影响BERT用的是WordPiece分词中文场景下基本是按字切偶尔合并一些高频词。这种分词方式对中文其实不算最优因为中文的词边界信息在按字切的时候丢了。后来有些中文预训练模型改用全词掩码就是在分词的时候尽量保持词的完整性效果有提升。实际项目里如果你的领域有很多专有名词或者缩写最好在分词器里加自定义token不然这些词会被切得七零八落模型很难学到它们的语义。加token之后需要resize模型的embedding矩阵新增的token embedding随机初始化微调的时候会跟着更新。6.2 长文本处理的截断与滑窗策略BERT最大长度512超过的部分只能截断。但很多实际任务比如法律文书分类、长评论情感分析文本长度远超512。直接截断会丢信息尤其是关键信息在后面的情况。常见的处理策略有几种。一种是滑窗把长文本切成多个512的片段分别编码后做池化或者注意力融合。另一种是取头尾因为很多文本的核心信息在开头和结尾。还有一种是先用规则或者小模型抽取关键句再送进BERT。我做过一个长文本分类的项目对比下来滑窗加平均池化的效果最稳但推理成本是单段的数倍。如果延迟要求高取头尾加关键句抽取的组合更实用。6.3 模型蒸馏与量化在部署中的取舍BERT-base推理一次大概几十毫秒在GPU上不算慢但如果QPS要求高或者要部署到边缘设备就得考虑压缩。蒸馏是把大模型的知识转移到小模型比如用BERT-base教一个6层的DistilBERT或者4层的TinyBERT。蒸馏后的模型参数量减少40%到60%推理速度提升一倍左右效果损失通常在2到3个百分点。量化是把FP32权重转成INT8模型大小减少75%推理速度提升2到4倍效果损失一般小于1个百分点。实际部署的时候我一般先试量化因为改动小、效果好量化还不够再考虑蒸馏。两者可以叠加但叠加后的效果损失需要仔细评估不是简单相加。7. 一些踩过坑之后才明白的事做语言模型相关项目这几年有几个教训是文档里不会写、但实际会反复遇到的。第一个是数据质量比模型选择重要。我见过太多人花大量时间调模型结构、换预训练权重但训练数据里标错的比例超过10%怎么调都上不去。后来把标注清洗了一遍同样的模型结构F1直接涨了5个点。模型再强也架不住数据在教它学错的东西。第二个是评估指标要和业务目标对齐。学术基准上的准确率、F1是一回事线上业务效果是另一回事。我做过一个意图分类离线F1 0.92上线之后用户投诉率反而上升了因为模型把取消订单和查询订单搞混了而这两个类别的业务代价完全不对等。后来改成加权损失让模型对高代价类别更敏感线上效果才好转。第三个是别迷信大模型。很多任务用小模型加好数据效果不比大模型差推理成本还低一个数量级。我现在的习惯是先用小模型跑一个baseline如果效果离业务要求差得远再考虑上大模型。上来就上大模型很多时候是浪费算力。第四个是版本管理要趁早做。预训练权重、分词器、微调数据、训练配置这些东西一旦开始迭代很快就会乱。我现在的做法是每个实验一个目录配置用yaml存权重和分词器一起打包实验记录写清楚改了啥、效果咋样。看起来麻烦但省去了后面大量这个模型是哪个版本训的的排查时间。语言模型这条线从RNN到Transformer从BERT到GPT技术迭代很快但底层的逻辑其实变化不大怎么更好地表示语义怎么更高效地利用上下文怎么让模型学到的东西和实际任务对齐。把这几件事想清楚具体用哪个模型、哪个框架反而没那么纠结了。