语言模型原理与实战:从N-gram到Transformer的演进与工程选型
做NLP这几年我有个很深的感受很多人张口闭口“语言模型”“大模型”但真把一个问题扔过去——“用一句话说清楚语言模型在做什么”——能讲明白的其实没几个。语言模型的核心任务听起来非常朴素给定前面几个词预测下一个词。就是这个朴素到有点不起眼的“接话”游戏撑起了输入法联想、机器翻译、文本续写、语音识别排序、智能客服意图识别这一整片应用。这篇文章我想从一个从业者的视角把语言模型这件事从底到顶捋一遍。不绕开数学但尽量用直觉讲清楚不回避代码但重点放到设计思路和踩坑经验上。无论你是刚入 NLP 的学生还是已经用预训练模型在做业务开发的工程师应该都能从里面拿到点东西。1. 语言模型到底在解决什么问题一个普通却撑起半个NLP的“接话”任务1.1 从“接话”开始理解语言模型我在带新人入门时最常用的类比就是“接话”。你发一句消息“今天天气真”正常人会接上“不错”“好”“热”之类的话。语言模型做的就是这件事根据已有的词预测下一个最合理的词。形式化一点语言模型要学的其实是整个序列的概率分布。给定一句话里面出现的词序列 ( w_1, w_2, ..., w_T )一个语言模型能计算出这句话整体出现的概率 ( P(w_1, w_2, ..., w_T) )。借助链式法则这个联合概率可以拆成[ P(w_1, w_2, ..., w_T) \prod_{t1}^T P(w_t | w_1, w_2, ..., w_{t-1}) ]也就是说整句话的概率等于“在每个时间点上给定前面所有词、预测当前词的概率”连乘起来。看起来就是一次次接话但一旦模型学会了这种条件概率分布它就能回答两类问题给一段文本打分判断像不像人话以及生成一段文本不断取预测结果喂回去。这两件事就是 NLP 里几乎所有上层应用的“水电煤”。1.2 为什么一个预测任务能撑起一门学科的基础很多人会低估这个接话任务的分量觉得“这不就是个自动补全吗”。但你要看它的下游应用面文本生成机器翻译、摘要、对话、文章续写最朴素的实现路径都是语言模型逐词生成只不过在不同任务里输入和输出的组织方式不一样。文本判断与标注给一句话打分、判断情绪、判断实体边界这些看起来不是“生成”的任务也可以借助语言模型知识来提取特征。从预训练时代起语言模型学到的语义表示就成了分类、序列标注等任务的初始化底座。语音识别声学模型给出多个候选音节或候选词序列语言模型负责从中选出概率更高的那一组。这也是语言模型最早的大规模工业应用之一。异常检测网络钓鱼邮件、垃圾文本、机器人刷屏等内容往往有着和正常文本差异很大的语言分布语言模型打的“困惑度”分数可以直接当异常分数用。换句话说语言模型是 NLP 世界的一把通用钥匙。后面所有复杂的模型结构、训练技巧、推理优化最终服务的都还是“把上下文编码好把下一个词预测准”这个总目标。2. 从统计计数到神经网络语言模型的两次范式跃迁2.1 N-gram时代计数、马尔可夫假设与稀疏性的老问题最早的语言模型靠的是数数。这是整个领域里我非常怀念也觉得很值得理解的一个时代因为它的数学直觉极其干净。假设我们现在要预测“今天天气真____”的下一个词。传统 N-gram 的做法是去一个很大的语料库里面数“今天天气真不错”出现了多少次、“今天天气真好”出现了多少次、“今天天气真热”出现了多少次然后找到出现次数最多的那个。这就是统计语言模型。但是语言里词和词的组合实在太多了指望记住所有完整的句子是不可能的于是人们引入马尔可夫假设当前词只依赖前 n-1 个词而不依赖整个历史。于是公式变成[ P(w_t | w_1, ..., w_{t-1}) \approx P(w_t | w_{t-n1}, ..., w_{t-1}) ]n 取 2 就叫做 bigram取 3 叫 trigram。工业界用得比较多的通常也就是 trigram 到 5-gram 之间再往上数据稀疏问题就会彻底爆炸。这里最容易踩的坑就是稀疏性。假设词表大小是 5 万理论上 trigram 的组合数是 5 万的 3 次方这个量级根本没有语料能覆盖。也就是说无论你用多少数据训练总会有大量三元组从未出现过程序一算概率直接得 0整句话概率变成 0这在真实系统里是不可接受的。解决办法是平滑技术。最简单的加 1 平滑也叫拉普拉斯平滑就是给所有没出现过的组合一个很小的基础概率[ P(w_n | w_{n-2}, w_{n-1}) \frac{count(w_{n-2}, w_{n-1}, w_n) 1}{count(w_{n-2}, w_{n-1}) V} ]V 是词表大小。它避免了零概率但也有明显副作用当词表很大时每个没见过的组合都要分走一瓢概率平滑系数稍微调大一点见过的组合概率也会被压得很低。所以后来才有一堆更聪明的平滑手段比如回退模型和插值模型低阶统计量来兜底高阶统计量的稀疏区间。N-gram 时代还有一个致命局限它判断两个词是否相关只看字面是否相同看不出“猫”和“猫科动物”在语义上是近亲。这个局限要等神经网络语言模型来解。2.2 神经网络语言模型从离散计数到连续向量神经网络语言模型的革命性变化就是把每个词从一个人为编号的离散符号变成了一个稠密的实数向量。这个向量学出来之后语义相近的词在向量空间里会相互靠近。于是模型不再需要死记硬背“今天天气真”后面接哪个词最多而是学会了在向量空间里把上下文组合成一个语义状态再从这个状态去预测下一个词。神经语言模型第一次把“泛化”真正带进了语言建模。哪怕“今晚温度真高”这句话完全没在训练语料里出现过只要模型见过“天气——温度”“不错——高”这样的近义替换就能给你的新句子一个合理的概率估计。早期结构里循环神经网络RNN和它的变体 LSTM 是绝对主角。它们的想法非常直观把文本按顺序读进去每读一个词就更新一次“记忆状态”读到最后这个状态里沉淀了整句话的信息。这个机制在今天看来简直到处是坑——顺序计算太慢、长距离信息容易在传递过程中被稀释——但它们是连接统计时代和注意力时代的关键桥梁。2.3 长距离依赖问题为什么“看得远”这么难真正促使整个领域转向注意力的是长距离依赖问题。别听名字玄乎其实特别容易理解有些概率关系需要跨很远的距离才能建立。举个例子“那栋红砖墙的房子已经很旧了我不确定政府会不会考虑保护___。”空里要填“它”还是“他”取决于 20 个词之前的“房子”。统计语言模型在这种情况下几乎无能为力因为它的窗口就那么几个词。RNN 理论上可以记住所有历史实际上梯度在反向传播中会不断相乘错误信号传不了太远就消失或者爆炸了。LSTM 用门控机制缓解了一点这个问题但也只是从“完全记不住”改善到“能记住一些中距离线索”。那时候研究者心里其实已经明白语言建模真正需要的是让每个位置都能直接“看到”全局的相关信息而不是靠一条链子把信息一段一段传过去。这个需求最终催生了注意力机制而基于注意力的 Transformer 架构把语言模型送进了新时代。3. 困惑度Perplexity别只当数字看语言模型评估的关键与陷阱3.1 困惑度到底在算什么评估语言模型最常用的指标是困惑度Perplexity符号记作 PPL。它和模型给整个测试集的平均负对数似然紧密相关公式通常写成[ PPL \exp\left(-\frac{1}{N}\sum_{t1}^N \log P(w_t | w_{t})\right) ]看着吓人直觉其实特别好懂。假设一个模型在某个位置的预测概率分布在 100 个词上完全均匀那它的困惑度就是 100。也就是说困惑度可以理解为“模型在每一步平均面临多少个等概率的候选词”。困惑度 20相当于模型每一步平均只需要从 20 个左右的选择里蒙一个这已经相当不错了困惑度 5000说明模型基本在乱猜或者遇到了和自己训练分布差很远的文本。做实验的时候我一般会同时看两个数字验证集困惑度和训练集困惑度。如果验证集困惑度降不下去而训练集已经降得很低模型多半在背训练数据泛化没跟上。反过来如果两边都高那就是欠拟合要么模型容量不够要么数据太乱要么训练步数没跑够。3.2 一个真实项目里的困惑度对照去年我带着一个模拟项目练手任务是在某领域新闻语料上训练一个字符级语言模型。语料大约 3 亿字符字符表 5000 个左右。对比下来有几个直观数据模型形态验证集困惑度备注5-gram 加1平滑300很多低频字组合完全没见过平滑给的基础概率太低LSTM隐层 5122层80 左右字向量能从上下文中学到规律明显优于n-gramTransformer相对位置编码6层隐层51250 左右收敛更快长距离信息保持更好这个差距在真实文本生成里非常明显。困惑度 300 的模型生成出来的字和字之间经常前言不搭后语困惑度降到 50 后至少局部上下文能保持通顺。当然这些都还在字符级模型的范畴放到子词级或大规模预训练模型上数值会再低一个量级。3.3 困惑度低不代表生成质量一定好这是我最想提醒大家的一点。困惑度衡量的是“模型对测试数据的概率拟合程度”而不是“文本流畅度”或“事实正确率”。我见过好几个项目团队辛辛苦苦把困惑度从 45 压到 30兴冲冲拿模型去生成文案结果出来的文本平淡乏味翻来覆去就是那几个高频句式。原因不复杂困惑度只鼓励模型给出高概率答案而高概率的结果往往是“安全”的常见表达多样性天然不足。模型可以变成一个“平庸但稳”的接话者但未必是“有趣且信息量足”的写手。所以生产环境里我从来不把困惑度当成唯一指标。评测一个生成模型至少还要看内容多样性用 n-gram 覆盖率或生成样本间的相似度来量化。事实一致性如果存在外部知识要检查生成内容是否和知识库矛盾。人工偏好拿几组生成结果做盲评真实用户反馈比任何自动指标都可靠。负面指标模型是否倾向于输出脏话、偏见内容、仇恨言论这些在困惑度里完全看不出来。4. Transformer与预训练时代语言模型如何变成NLP基础设施4.1 自注意力机制带来的两个关键改变Transformer 的核心是自注意力Self-Attention。它的想法也很朴素序列里每一个位置都去计算和其他所有位置的关联权重然后用这种权重把其他位置的信息加权融合过来。想让“房子”这个词去关注 20 个词之前的“红砖墙”不需要像 RNN 那样一点一点传一次注意力计算就能把两端的表示揉到一起。这件事带来了两个关键改变。第一是并行计算。RNN 必须按时间顺序一个词一个词算Transformer 的所有位置可以同时做注意力计算在大规模 GPU 集群上训练效率提升了几个数量级。这也是为什么当年那批大规模语言模型纷纷选择 Transformer 作为底座的核心原因——不是因为它数学上更美而是因为它训得动。第二是全局依赖建模。注意力的感受野是整个序列不管是 20 个词还是 2000 个词只要位置编码还有信息量模型就有机会建立任意距离的联系。代价是注意力矩阵的大小随序列长度平方增长所以实际使用中要用窗口注意力、稀疏注意力等方式去折衷。4.2 预训练微调范式的底层逻辑Transformer 把语言模型做大之后大家发现了一个特别美妙的性质与其为每个任务训练一个全新模型不如先用海量通用文本把语言模型训练好这个阶段叫预训练然后针对具体任务用一小部分标注数据做一下适配这个阶段叫微调。为什么这套组合拳能成立因为语言模型任务本身提供了一个极其强大的自监督信号——文本天然就是标签下一段话是谁写的不重要重要的是模型可以从任何语料上无休止地学习语义、语法、常识和世界知识。预训练语言模型做分类任务时就在语言模型之上接一个简单的分类头做命名实体识别时就在每个位置接一个标签预测头。底座已经通晓语言新任务只需要学一层很薄的映射。这套范式大幅降低了 NLP 任务的开发门槛也是今天所有大规模语言模型产品的基础操作。4.3 理解型与生成型一个内核两种用法进入预训练时代后语言模型按照建模方式分成了两条技术路线理解型和生成型。理解型双向/掩码语言模型的做法是在训练时随机遮住一部分词让模型同时看左右两边的上下文去还原被遮住的词。由于信息来自双向它学到的表示对语义理解任务更有优势——做分类、实体抽取、阅读理解都特别合适。生成型单向自回归语言模型的做法是严格只允许看左边的词然后预测右边的词。这一点和“语言模型预测下一个词”的原始定义完全一致所以天然适合续写、翻译、摘要、对话等生成任务。两条路线没有绝对优劣。当前比较一致的工程做法是你要做哪种任务就优先从对应形态的预训练模型出发。分类优先选双向结构生成优先选自回归结构。硬要用一个文本生成模型去做意图分类也不是不行但通常效率低、效果差属于拿大炮打蚊子。5. 零基础动手实现一个迷你语言模型从语料到训练闭环5.1 语料准备与样本构造先把数据喂对不上手实操语言模型永远是空中楼阁。我建议第一次动手就从一个小型字符级 Transformer 开始整个训练环路能在一张消费级显卡上跑完把原理看明白后再去接触那些工业级预训练模型。第一个要处理的是数据。字符级模型的词表就是去重后的字符集一般中文几千个英文几十个很省内存。但注意字符级模型学起来会比较慢长距离依赖也更难建立所以它适合做教学和理解不适合直接上生产。构造样本时用滑动窗口切文本# 字符级语言模型训练样本构造伪代码框架无关 text 深度学习让语言模型获得了巨大的发展 char_to_id {ch: idx for idx, ch in enumerate(set(text))} ids [char_to_id[ch] for ch in text] block_size 16 # 上下文长度 batch [] for i in range(len(ids) - block_size): x ids[i:i block_size] # 输入前16个字符 y ids[i 1:i block_size 1] # 输出后移一位的16个字符 batch.append((x, y))这里有个特别容易犯的低级错误输入和输出序列千万别错位。y必须是x整体右移一位而不是把x复制一遍。很多人第一次跑语言模型 loss 不降三分之一是数据对齐问题。5.2 模型结构与训练循环原理级实现迷你语言模型的结构可以非常简练就是词嵌入、位置编码、几层自注意力、一个输出映射到词表然后接 softmax 和交叉熵损失。# 迷你语言模型训练循环的核心步骤伪代码框架无关 # config: vocab_size5000, block_size16, n_layer4, n_head4, n_embd256 # 1. 输入编码 tok_emb embedding(x) # [batch, block_size, n_embd] pos_emb position_embedding(arange(block_size)) h tok_emb pos_emb # 位置编码是加性融合 # 2. 多层自注意力 前馈 for _layer in range(n_layer): h h multi_head_attention(norm(h)) h h feed_forward(norm(h)) # 3. 输出投影到词表 logits output_projection(norm(h)) # [batch, block_size, 5000] # 4. 交叉熵损失目标y同样右移对齐 loss cross_entropy(logits.view(-1, vocab_size), y.view(-1))选择超参数的经验值我一般这样起步嵌入维度 256、4 层 Transformer、4 个头、上下文长度 128。批次大小在显存允许范围内尽量取大比如 64。优化器用带权重衰减的 Adam 变体初始学习率设在 1e-3 到 3e-4 之间跑上二三十轮看趋势。这里说明一下嵌入维度太小模型容量不足以学到深层次规律太大会让训练变慢并容易过拟合。256 是最不容易出错的起点。头数乘上头维度要等于嵌入维度这是 Transformer 实现里很常见的维度对齐约束。5.3 训练中我踩过的四个典型坑一路实操下来有几个坑几乎每个人都会遇到。第一loss 直接变成 NaN。这时候八成是学习率太大尤其是用了某些分层学习率策略或者权重衰减没设好。先把学习率降到原来的十分之一跑一下排除这个因素再看数据里有没有包含无穷大或者普通文本里混入的乱码。第二loss 不降反升。先检查训练数据是不是在每一个 batch 里都被打乱了。语言模型极度依赖上下文顺序如果你在 batch 内做了整句随机打乱还能想一想如果连字符顺序都随机打乱了模型当然学不到任何规律。第三上下文窗口越长训练越慢但效果未必更好。窗口翻倍注意力计算量约翻四倍。最稳妥的做法是先用短窗口跑通确认现象合理后再逐步加长别一上来就追求长上下文。第四过拟合在字符级模型上特别快。几千字符的小语料模型很容易把整个语料背下来。验证集困惑度不再下降时及时停训或者加 dropout。6. 生产环境下的选型心得别让理论指标骗了你6.1 按任务类型选模型的决策框架真正落到生产项目里语言模型的选型没那么玄学。我的经验是分三步走先明确任务形态是生成式还是理解式再评估数据量和算力预算最后用一个小型基线模型跑通端到端流程再决定要不要换更大的模型。拿几个常见场景举例电商评论情感分类数据量中等任务本身是判别式的用中小型预训练模型加一个分类头微调通常几天内就能拿到可用效果完全不必要用大生成模型。智能客服答案生成需要生成自然语句对流畅度要求高那就要用生成式模型。如果答案能映射到固定知识库建议先做检索再让模型基于检索片段做摘要幻觉问题会少很多。代码自动补全这个场景对长距离依赖要求很高需要比较大的上下文窗口同时对延迟敏感一般要用针对代码语料做过增量训练的专用模型并配合量化加速。6.2 上下文长度、参数量与成本的三角权衡很多团队一上来就想上“最能打”的大模型。但你必须算清楚三件事上下文长度、参数量、推理成本。这三个变量互相制约。上下文长度直接影响单次推理能接受多少输入。像长文档问答场景输入动辄几千词模型上下文太短就得到处切块拼接效果大打折扣。但上下文窗口增大注意力矩阵平方膨胀GPU 显存和每 token 的推理延迟都会明显上升。参数量的作用更直接模型越大学到知识和规律的上限越高但训练和推理成本也越高。企业在真实业务里经常采用“底座大模型 蒸馏小模型”的组合方案先用大模型处理最难的请求把它的输出作为训练数据蒸馏一个小模型用来承接绝大多数高频简单请求成本能降一到两个数量级。6.3 部署阶段的几个通用优化手段调用延迟和成本优化我的建议按顺序排查先看能不能做批量推理把多个请求拼成一个 batch 一起过模型吞吐立刻上一个台阶再看能不能用量化把权重从高精度压到低精度显存占用和计算量都下降但要先在评测集上验证效果损失是否可以接受最后才考虑模型裁剪和蒸馏这属于手术级改造需要重新训练周期最长。另一个容易被忽略的点是缓存设计。生成式任务往往会有大量共享前缀比如客服系统里同一个开场白之后接不同问题如果框架支持前缀缓存重复的中间计算可以直接复用推理延迟会大幅下降。我在好几个项目里都用这招把单次请求耗时压缩了接近一半。做语言模型这些年我最大的体会是技术演进快得吓人但评判标准始终没变。不管模型多大、多新真正要回答的还是那三个问题——能不能接好话能不能在任务上完成任务能不能在成本范围内稳定上线。想清楚这三件事再大的模型也只是工具箱里的一把工具。