如果你也在自学NLP大概率绕不开“语言模型”这四个字。我在第13天系统地碰这个主题时整个人有一种“前面学的东西终于被串起来了”的感觉。之前做词向量、分词、文本分类总觉得知识点是散的等到接触语言模型之后才明白很多任务本质都是在做“概率的选择”。今天这篇笔记就是把那个阶段我的理解、实验过程和踩过的坑完整复盘一遍希望能给正在同一进度上的你一点参考。1. 语言模型到底在学什么1.1 核心任务计算句子出现的概率语言模型的表面任务很简单给一句话一个概率。比如“我今天去跑步”这句话应该比“跑步去我今天”得到更高的分数。因为前者在语法、语序、词语搭配上都更符合自然语言的分布。这听起来像常识但一旦把它数学化问题就立刻变得不平凡。我们没有办法直接统计“所有句子的频次”因为句子是无限的绝大多数句子在语料里出现次数是零。语言模型其实就是在这个“无穷候选”的环境下估计“哪个句子更像人话”的概率分布。这个能力下游用处很大。语音识别里一段语音可能对应“我到教室”和“我到教室”两个读音相近的候选模型要选出语言上更合理的一句。机器翻译里同一个源语言可以对应多个译文语言模型帮助排序。文本生成更直接每一个词的选择都来自语言模型给出的概率。所以它不只是NLP的一个分支而是很多任务共享的底座。1.2 链式法则与“无穷历史”的问题计算整句概率常规做法是用条件概率链式法则拆解。对于句子 W w1, w2, …, wn概率可以写成P(w1..wn) P(w1) × P(w2|w1) × P(w3|w1,w2) × ... × P(wn|w1...w_{n-1})也就是说句子概率等于每个词在给定前文情况下出现概率的乘积。这个公式本身是精确的没有引入任何近似。但麻烦也在这里。要估计“第n个词在它之前所有词之后的概率”需要看到足够长的历史。假设词表有5万个词w1,w2,w3组合就是5万的三次方量级实际语料不可能覆盖。这就是为什么后续所有语言模型都要对“历史长度”做压缩要么假设局部依赖要么用神经网络把历史编码成固定向量。可以说整个语言模型的发展史就是一部“如何更好地近似这个条件概率”的历史。1.3 为什么是NLP的地基语言模型的价值不只在生成。凡是需要判断“两段文本哪个更自然”的场景都可以用语言模型打分凡是需要预测下一个词的任务本质上都是语言模型。命名实体识别、情感分析、句法分析里也存在大量利用上下文预测标签的问题虽然表面任务不同但底层对上下文建模的需求是相通的。从学习角度来说理解语言模型也让我重新看懂了词嵌入训练语言模型时必须把词变成向量这比单独跑一个“词向量任务”更自然因为语言模型本身就是为下一个词优化。后面接触到的注意力机制也都是在为同一个条件概率做更高效的建模。所以我建议不要把它当孤立章节而要当成前面所有知识的汇聚点。2. 统计语言模型从n-gram到平滑技巧2.1 n-gram与马尔可夫假设最早的语言模型走的是统计路线核心思想是“马尔可夫假设”第k个词的概率只依赖前面有限的n-1个词而不是完整历史。这就是n-gram模型。unigramP(wk | 前面所有) ≈ P(wk)没有上下文。bigramP(wk | w1..wk-1) ≈ P(wk | wk-1)只看上一个词。trigramP(wk | w1..wk-1) ≈ P(wk | wk-2, wk-1)看前两个词。n越大理论上捕捉的信息越多但数据也越稀疏。以bigram为例参数数量大约是词表大小的平方trigram就是立方实际语料很难把所有组合都填满。因此选n是一个权衡n太小语言不自然n太大估计不可靠。我记得第一次实现bigram时代码核心就是统计相邻词对的出现次数。训练时遍历所有句子把“词i-1 → 词i”的计数加一。预测时用频率估计概率P(wk | wk-1) count(wk-1, wk) / count(wk-1)这个公式看起来很朴素但它是后面平滑方法的基础。2.2 平滑方法给零概率一个台阶直接使用频率估计会遇到一个尴尬情况如果语料里没出现过词对wk-1, wk概率就是0整句概率也被乘成0。解决思路是“平滑”给没有出现的组合也分配一点点概率把已有组合的概率“让”一点出来。最直观的是拉普拉斯平滑加1平滑P(wk | wk-1) ( count(wk-1, wk) 1 ) / ( count(wk-1) V )其中V是词表大小。分母加V是为了保证所有可能词的概率和仍然是1。你可以想象成每个词对本来就有一个“先验”出现次数只是我们假装它出现了一次这样零概率就消失了。实际项目里不太会用加1因为会过度压低高频词的概率。更常见的是加δ比如0.01、0.1也就是“加δ平滑”。另一个常见技巧是回退如果bigram概率为零就退回去用unigram概率估计而不是直接给0。还有一些更复杂的Kneser-Ney平滑我的理解它是在回退时考虑了“这个词到底对多少不同前文有效”比单纯回退更精细。统计模型中平滑参数通常选在验证集上困惑度最低的值。2.3 困惑度模型好坏的数字标尺有了模型就要评估。语言模型最常用的指标是困惑度perplexity。定义式写出来其实不复杂PPL exp( -1/N × ∑ log P(wi | context) )其中N是测试集中总词数求和遍历每个位置的词。另一个理解方式PPL表示“平均每次预测模型面对的不确定词数”。如果PPL是50等价于模型在50个候选词之间做选择如果所有词等概率且词表大小是5万PPL就是5万是最差情况。所以PPL越小越好。要注意两点第一用困惑度比较不同模型时词表和数据集必须一致。一个词表大的模型天然PPL可能更高因为候选空间更大这不一定代表它效果差。第二困惑度只反映概率分配的准确度不能直接反映生成文本是否流畅、是否有意义。有人训练出一个PPL很低的模型生成结果仍然充满重复就是因为它把概率集中在高频安全词上了这是评估和生成体验之间的落差。2.4 统计模型的边界在哪统计语言模型在数据量足够、n适中的时候效果还可以但有一个硬伤它对“远距离依赖”无能为力。英语里的“he ... was”可能隔着十多个词才出现bigram和trigram都看不到。中文里也有类似情况比如“张三昨天告诉李四明天会来”里的“会来”到底是张三还是李四来需要更远的信息。另一个问题是“语义鸿沟”统计模型把词当成离散符号“猫”和“狗”在模型看来毫无关联但在语义上是两个接近的动物。要让模型理解相似性就需要稠密向量。这就是神经网络语言模型登场的理由。3. 神经网络语言模型演进中的重要节点3.1 神经语言模型的初步格局神经网络语言模型并不直接统计词对而是把每个词映射成一个稠密向量然后用向量运算预测下一个词。基本结构是输入词编码成one-hot向量通过嵌入层变成embedding再拼接或相加上下文向量经过一到两层非线性变换最后接softmax输出词表上的概率分布。这样做的好处是判断“猫和狗哪个更可能出现在‘喂’后面”时模型看到的是两个语义相近的向量输出分布会给它们接近的概率而不是像统计模型那样把没见过的组合都当成0。这个性质很快让它在很多评测上超过了n-gram。如果你当时尝试训练一个小的前馈网络语言模型会发现它其实和词嵌入训练是一体两面的事情投影层权重本身就是词向量。3.2 RNN/LSTM与“记忆”的产生前馈网络上下文窗口有限还是没法处理长句。循环神经网络通过隐藏状态把信息一步步传递下去每来一个词隐藏状态更新一次把之前所有信息压缩成一个固定向量然后基于这个向量预测下一个词。理论上RNN能处理任意长度历史算是对“无穷历史”问题的一种工程近似。但实际操作中RNN对很早以前的信息记忆很差因为梯度在反向传播时不断连乘会指数级缩小或爆炸。LSTM的出现缓解了这个问题它引入“门控”机制让网络决定保留多少历史信息、更新多少新信息。你可以把LSTM的细胞状态想象成一个带闸门的笔记本门控制哪些旧内容被擦掉、哪些新内容被写进去。这样的语言模型可以在句中记住主语性别、单复数、时态等长期线索。这个阶段我开始真正理解“上下文表示”这个说法RNN给每个位置的输出都包含整个前文信息所以语言模型训练出的隐藏状态可以当作上下文特征去做其他任务这也是后来众多模型共享的套路。3.3 自注意力机制为什么能取代顺序模型Transformer的出现很大程度上是为了解决两个问题RNN无法并行计算以及顺序传导带来的长距离信息损耗。自注意力机制不再按词序一步步更新状态而是让任意两个词之间直接计算关联权重。查询Q、键K、值V三个向量来自同一个输入序列通过 Q·K 的结果除以缩放因子再softmax得到注意力分数然后用这个分数加权求和每个位置的V。这样一来序列里每个词都能直接看到所有其他词长依赖问题在机制上被绕开了。注意力计算是并行的训练速度远快于RNN。但因为它不关心词序所以需要额外加入位置编码给每个位置一个特征让模型知道“哪些词在前、哪些词在后”。这个设计让现代预训练语言模型能够在海量语料上训练并继续沿用“预测下一个词”这个朴素目标。很多你在新闻里看到的大规模语言模型底层基本都是这个结构区别只在规模、训练数据和具体训练方式。4. 一次简单的语言模型实验复盘4.1 实验设定与数据处理细节学习语言模型光看书不行我建议每个人都亲手跑一个最小实验。我当时用了一份约15万句的中文语料大概120万字。任务设为“bigram 拉普拉斯平滑”和“三层LSTM”两个版本的对比评价指标统一用困惑度。数据处理有四个关键点很容易出错第一分词要一致。训练和测试必须使用同一套分词方案否则统计结果没有意义。我当时直接用字符级避免分词工具带来的不一致。第二要设置特殊token。句子开头用 结尾用 不在词表里的低频词统一替换成 。这样模型才能知道句子的边界也才能把“下一个词是什么”这个任务的起点定住。第三按句子切分后再打乱句子不能把词随机打散。语言模型依赖上下文顺序打乱词会彻底破坏条件概率结构。第四训练集和测试集不能有重叠文本。我当时从同一批文章里先随机抽10%做测试剩下90%做训练而不是先拼接再切块否则一个段落可能一半在训练一半在测试指标虚高。4.2 统计模型参数怎么调我用bigram模型做基线公式就是前面提过的P(wk | wk-1) ( count(wk-1, wk) δ ) / ( count(wk-1) δ×V )其中δ是一个手工调的平滑参数。当时试了几组值效果差异很大模型平滑参数训练困惑度测试困惑度bigramδ1.0142236bigramδ0.1118187bigramδ0.01103195直觉是δ太大给所有未出现词对分配的概率过多把常见词对的概率压低了δ太小新颖词对的概率又容易极端。所以在小数据集上δ0.1左右往往比常见的加1平滑更合适。如果你的语料更大δ可以进一步调小因为统计次数更可靠不需要过度平滑。另外我还试了trigram。理论上效果应该更好但在我这个数据量下三词组合有大量次数为0回退策略没做好时甚至会低于bigram。这也说明一个道理参数越多的模型对数据和平滑方法要求越高不是n越大就一定越好。4.3 使用循环网络训练时的几个关键选择神经网络部分我用了一个经典结构词嵌入维度64单层LSTM隐藏状态128softmax输出层大小等于词表。词表我限制在2万个高频词其余替换成 。训练用交叉熵损失小批量大小64学习率动态衰减。有一个细节经常被新手忽略目标标签是“下一个词”。训练数据里一个长度为T的句子能生成T个训练样本第i个样本的输入是前i-1个词的序列标签是第i个词。LSTM处理整个句子后每个时间步都输出一个概率分布全部位置一起计算损失也就是teacher forcing。实际上你应该把整个句子的输入放进网络而不是滑动窗口一个个取这样效率高也能利用长距离上下文。训练初期我遇到loss不降的问题后来排查发现是嵌入层初始化范围不对、梯度更新过大。换成小一些的均匀分布初始化后loss很快就开始下降。你可以把模型放到一小段固定文本上看看能否过拟合到接近0如果过拟合不了大概率是代码或梯度配置的问题。4.4 实验结果对比与解读我的LSTM模型在相同测试集上的困惑度最终做到了89左右而最优的bigram只有187。这个差距很直观说明利用长上下文和分布式表示确实能带来显著提升。我把结果整理成下面这样模型参数量训练困惑度测试困惑度bigram 平滑~4万118187trigram 回退~200万96158小LSTM~250万5289注意LSTM参数量比trigram多不少但困惑度低很多说明容量不是唯一因素建模方式更重要。这个实验还让我意识到一个点只看PPL不够。我拿LSTM去生成文本虽然PPL不高但生成结果过一会儿就开始重复比如“我们要把这个问题把这个问题搞清楚”。原因是模型过度倾向高频搭配生成本地合理但缺乏全局规划。后来我用带温度和top-k采样的方式重复现象改善了一些但这是后话。5. 实操中遇到的报错与排查经验5.1 数据稀疏和未登录词的处理处理未登录词时最常见的隐患是词表构建不一致。假设你在训练集上构建了一个词表测试集出现训练中没有的词如果直接忽略测试损失会异常高。正确做法是设置一个词频阈值比如词频小于3的全部替换为 并在词表中加入 这个token。同理分词后的测试数据也要执行同样的替换逻辑。一个容易踩的坑是你在训练时把所有低频词替换成 但预测时某个高频词因为某种原因也在词表外就会输出 导致PPL看起来很低但实际模型根本没学会预测正确词。所以评估时最好统计一下测试集中 占的比例如果超过1%就要复查词表策略。5.2 损失不下降先回到极小样本如果你用神经网络训练语言模型损失卡住不降我的第一个建议是拿一小段训练数据比如几百句话把模型容量不变训练几百步看看能不能把训练损失降到接近0。如果连小样本都过拟合不了说明代码或配置有问题而不是模型容量不够。这个方法能帮你快速区分“实现bug”和“欠拟合”。另一个常见问题是梯度爆炸。LSTM虽然比RNN稳定但在长句子上梯度范数仍可能突然暴涨。加梯度裁剪几乎是无脑收益的操作设定最大梯度范数1.0就能避免大量“loss变成NaN”的情况。5.3 评估结果反直觉顺带聊聊生成体验我试过一种场景训练loss持续下降但测试PPL却在某个epoch后开始上升。这是典型的过拟合解决方法是早停。当时我误以为语言模型不容易过拟合因为任务看起来只是预测下一个词后来发现参数量稍微大一点就能把所有训练句子背下来测试集立刻遭殃。还有一次我把评估代码里的“log概率取平均”写成了“取乘积”导致PPL值小得离谱几乎接近1。我盯着结果高兴了半天直到拿一个随机模型测试才发现评估函数有问题。所以要习惯在正常语料和打乱语料上各算一次PPL作为冒烟测试正常语料的PPL应当明显低于打乱语料如果你的正常PPL比打乱还高一定哪里错了。从生成体验来看语言模型的PPL和“人类觉得顺不顺”并不完全等价。一个模型可能给每个词都很合理的概率但生成时因为局部概率分布太平整句子没有主题。如果你要做生成还得组合使用采样温度、重复惩罚、top-p过滤等手段。这些并不是模型训练阶段的事但会让你对“语言模型到底学到了什么”有更完整的认识。最后再分享一个我自己的习惯学语言模型时我会把每一步都落到“给定上文预测下一个词概率分布”这个具体公式上。统计模型是这样循环网络是这样Transformer也一样。只要抓住这个核心后面理解预训练、微调和各种生成技巧都会顺畅很多。