想象你叫小林,是主题公园入口的新人安检员。你的任务听起来简单:给一条长队里的每位游客贴身份牌——普通游客(V)、工作人员(S)、VIP(C)或儿童(K)。可你很快就发现,光看一个人根本不够:穿玩偶服的可能是表演部员工,也可能只是狂热粉丝;举着气球的孩子身边一定跟着大人;工作人员总是三五成群连排出现。你盯着每一个人看,却频频贴错,直到一位老师傅点醒你:“小伙子,贴牌不是一个个孤立决定——你得一边走一边看,前面的人贴了什么,后面的人长什么样,把整条队伍当成一串环环相扣的决定来做。”这句话,正是条件随机场(CRF)的核心直觉。CRF 是序列标注问题的一把利器:输入一串观测(每个人长什么样),输出一串标签(每个人的身份牌),模型既看每个位置自己的特征,又看标签之间的依赖。这篇教程,就跟着小林从“贴牌”出发,把 CRF 的原理、特征、解码和训练一次讲透。

岗位关联

对 AI 从业者而言,CRF 是处理命名实体识别、词性标注等结构化预测任务的必备武器,也是 BiLSTM/BERT+CRF 方案的输出层基石。读懂它,你会更懂“为什么模型不光要看词,还要看标签怎么连”。

目录
  1. 可视化演示
  2. 序列标注问题
  3. 判别式模型
  4. 线性链结构
  5. 特征函数
  6. 维特比解码
  7. 参数训练
  8. CRF与HMM
  9. 应用领域
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示条件随机场CRF的核心机制,通过总览/传输/转换/输出四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

条件随机场CRF:一CRF 一、CRF判别式建模框架 二、线性链CRF结构与概率定义 核心机制可视化 条件随机场CRF整体架构与关键组件

序列标注问题

小林第一天上岗,队长只丢给他一句话:“给每个人贴牌子。”他以为这是道单选题,一个个看脸判断,结果半天签发了三十张错牌——把 VIP 当成了普通游客,把工作人员误认为粉丝,还差点让一个没大人陪同的孩子进了园。他这才醒悟:贴牌是一串决定,不是一个个孤立的决定。后面的牌子会受前面的影响,比如工作人员结伴走,VIP 由引导员带着走,儿童必须有大人牵着。

在计算机的世界里,这串“游客”叫观测序列 x,每个游客的外貌、衣着、行为是 x_i;这串“牌子”叫标签序列 y,每个人的身份牌是 y_i。给一串观测逐个位置贴标签,就是序列标注。小林最初的错误来自只盯单个位置——这正是把序列问题当“独立分类”的陷阱:每个点局部最对,整串却可能错得离谱。

序列标注任务有两个核心成分:单点特征(这个位置长得像什么)与标签依赖(相邻标签之间有什么关系)。任务目标是找到一整串概率最高的标签序列,而不是逐点独立地挑最大概率。对序列标注而言,“整体最优”往往胜过“每个点局部最对”。

小林把这四个字——“整体最优”——刻在工牌背面。从那以后,他不再一个个拍脑袋,而是把整条队伍当作一个整体来决策。这就是序列标注的起点,也是 CRF 登场的舞台。

x=(x1,x2,…,xn),y=(y1,y2,…,yn),y^=arg⁡max⁡yP(y∣x)x = (x_1, x_2, \dots, x_n), \qquad y = (y_1, y_2, \dots, y_n), \qquad \hat{y} = \mathop{\arg\max}_{y} P(y \mid x)
  1. 观测序列 xx 是整条队伍,x_i 是第 i 位游客的外貌与行为特征。
    x=(x1,x2,…,xn)x = (x_1, x_2, \dots, x_n)
  2. 标签序列 yy 是整串身份牌,y_i 是第 i 位游客的最终标签。
    y=(y1,y2,…,yn)y = (y_1, y_2, \dots, y_n)
  3. 条件概率在“看到这条队伍”的前提下,某一串贴法出现的概率。
    P(y∣x)P(y \mid x)
  4. 整体最优从所有可能的贴法中选出概率最大的那串,记为 ŷ。
    y^=arg⁡max⁡yP(y∣x)\hat{y} = \mathop{\arg\max}_{y} P(y \mid x)

判别式模型

小林有两个师傅。编剧型师傅的绝活是“编剧本”:他会设想这批游客是怎么被“生成”出来的——先有队形(标签怎么排),再按每个身份牌子“发射”出相应外貌(穿什么、带什么)。遇到新队伍,他把所有可能剧本演一遍,挑最像的那个。听起来很酷,可惜剧本总得先写好“什么样的人就该长什么样”的设定,可现实里穿制服的也可能是 VIP。

裁判型师傅完全不同:他从不编剧本。他只死磕一件事——看到长相 x 与相邻牌子 y_{i-1}、y_i 的证据,直接判断该贴什么牌。他把成千上万条“如果看到什么组合,就贴什么牌”的经验直接学进脑子里。这种“不生成、只判别”的思路就是判别式模型,CRF 正属于这一类。

生成式模型求解时要绕道:先建模 P(x|y) 与 P(y),再用贝叶斯公式推出 P(y|x);判别式模型直接对 P(y|x) 建模,不描述观测是如何产生的。CRF 是判别式概率图模型:在给定观测序列 x 的条件下,为标签序列 y 定义条件概率分布,把特征线性加权后取指数再归一化。

裁判型师傅后来成了小林最敬重的人——他不预测游客“应该长什么样”,只管“看起来像什么就贴什么牌”,所以能接受各种奇奇怪怪的重叠线索而不崩溃。这个“不编故事”的立场,正是 CRF 相对传统生成式模型的根本优势。

P(y∣x)=1Z(x)exp⁡(∑i=1n∑k=1Kλkfk(yi−1,yi,x,i))\displaystyle P(y \mid x) = \frac{1}{Z(x)} \exp \left( \sum_{i=1}^{n} \sum_{k=1}^{K} \lambda_k f_k(y_{i-1}, y_i, x, i) \right)
  1. 条件概率给定队伍外貌 x 时,贴法 y 的概率。
    P(y∣x)P(y \mid x)
  2. 加权求和对每个位置 i、每条特征规则 k,把权重 λ_k 乘以特征值 f_k 再累加。
    ∑i=1n∑k=1Kλkfk(yi−1,yi,x,i)\sum_{i=1}^{n} \sum_{k=1}^{K} \lambda_k f_k(y_{i-1}, y_i, x, i)
  3. 指数变换把得分变成正数,得分越高概率越大。
    exp⁡(⋅)\exp(\cdot)
  4. 归一化常数对固定的 x,把所有可能贴法的指数得分加起来,保证概率之和为 1。
    Z(x)=∑y′exp⁡(∑i=1n∑k=1Kλkfk(yi−1′,yi′,x,i))\displaystyle Z(x) = \sum_{y'} \exp \left( \sum_{i=1}^{n} \sum_{k=1}^{K} \lambda_k f_k(y'_{i-1}, y'_i, x, i) \right)

线性链结构

小林问师傅:队伍一千个人,每人四种牌子,难道要考虑人和所有人之间的关系?师傅摇头:你观察一下,绝大多数时候,一个人的牌只和紧挨着的前一个人最相关——工作人员一串人连着走,VIP 由引导员单独带,儿童的牌旁边必定有大人。隔得远了,关系就弱到可以忽略。于是画出来的依赖图,就是一条不打岔的链。

这就是“线性链”名字的由来:标签按顺序排成一串,每个标签只与相邻标签有直接连线,其他影响都被折叠进“相邻”里。数学上称作一阶马尔可夫假设:给定前一个标签和整条观测,当前标签的概率只依赖前一个标签,而不依赖更早的历史。

这条链让 CRF 既保留了“标签互相影响”的结构,又避免了完全图那种爆炸式复杂度。分母 Z(x) 的求和也只在链上进行,可以用动态规划高效完成。工程上,词性标注、命名实体识别、分词这些经典序列标注任务,几乎都采用这种线性链结构。

小林看着师傅画的链恍然大悟:原来“互相影响”不等于“人人互联”,一条链足以装下“跟着前面走”的全部智慧。这也是为什么它叫 linear-chain CRF——顺着队伍走,一颗心只牵前一人的手。

P(yi∣y1,y2,…,yi−1,x)=P(yi∣yi−1,x)P(y_i \mid y_1, y_2, \dots, y_{i-1}, x) = P(y_i \mid y_{i-1}, x)
  1. 完整历史条件左边表示 y_i 原本要依赖前面所有标签。
    P(yi∣y1,y2,…,yi−1,x)P(y_i \mid y_1, y_2, \dots, y_{i-1}, x)
  2. 一阶近似右边只保留最近一个标签 y_{i-1} 作为条件。
    P(yi∣yi−1,x)P(y_i \mid y_{i-1}, x)
  3. 链式乘积重复应用假设后,整串标签的联合概率拆成相邻标签转移之积,计算量从指数级降为多项式级。
    ∏i=1nP(yi∣yi−1,x)\prod_{i=1}^{n} P(y_i \mid y_{i-1}, x)

特征函数

光有链还不够,小林手里得有一张“检查清单”。队长发下来的清单上写满“如果…就加分”的条目:看到制服加两分,背着相机的游客加一分半,穿玩偶服又贴着 S 牌加三分……每一条,都是一个“特征”。

在 CRF 里,清单条目被写成特征函数 f_k(y_{i-1}, y_i, x, i)。它接收五个输入:前一个标签、当前标签、整条观测、当前位置,然后回答一个简单的问题——“这条规则命中了吗?”命中返回 1,没命中返回 0。特征函数分两类:只看当前位置与观测关系的叫状态特征(如“第 i 个词是‘公司’→ 标签是 ORG”),还要看前一标签的叫转移特征(如“前一标签是 B-ORG → 当前标签是 I-ORG”)。

实际工程里,特征不是一条条手搓,而是按“模板”批量生成:当前词、前一个词、后一个词、是否大写、词性、前后缀、窗口内共现……一个模板展开就是成百上千条特征。因为特征是“加分项”,CRF 允许特征之间任意重叠,这正是它比朴素生成模型更灵活的关键。

小林把清单越写越厚,但师傅提醒他:清单是死的,权重是活的。每条规则的“分量”要靠数据说话——这就是后面要讲的训练。特征负责“看见”,权重负责“掂量”。

fk(yi−1,yi,x,i)={1,若 yi−1=B-ORG, yi=I-ORG, xi=“公司”0,否则f_k(y_{i-1}, y_i, x, i) =\begin{cases}1, & \text{若 } y_{i-1} = \text{B-ORG},\ y_i = \text{I-ORG},\ x_i = \text{``公司''}\\0, & \text{否则}\end{cases}
  1. 函数签名特征函数接收前一标签 y_{i-1}、当前标签 y_i、观测 x 与位置 i。
    fk(yi−1,yi,x,i)f_k(y_{i-1}, y_i, x, i)
  2. 命中返回 1相邻标签与当前词同时满足规则时,特征值为 1,为得分贡献一个正项。
    yi−1=B-ORG, yi=I-ORG, xi=“公司”y_{i-1} = \text{B-ORG},\ y_i = \text{I-ORG},\ x_i = \text{``公司''}
  3. 未命中返回 0大多数情况下特征不命中,返回 0,对得分没有贡献。
    fk(⋅)=0f_k(\cdot) = 0
  4. 权重 λ_k每条特征配一个可学习权重,训练时学到的就是这些 λ。
    λk⋅fk(⋅)\lambda_k \cdot f_k(\cdot)

维特比解码

上岗第三周,小林面对一条一千人的长队差点崩溃:每人四种牌,一共有 4 的一千次方种贴法,把全宇宙的纸都写满也不够。师傅扔给他一支笔:“别傻算。从左往右走,每走到一个人,只记四种牌各自‘最优到这里的累计得分’和它来自前面哪种牌。走到队尾,从最高分往回倒着捋,一条路就出来了。”

这个“记最优、存前驱、倒着捋”的土办法就是动态规划;在 CRF 里它的正式名字叫维特比解码。它把“全局搜索最优路径”变成“逐位置地保留局部最优”,复杂度从指数级降到 O(n×L²),其中 n 是序列长度,L 是标签数。

维特比不只是“快”,它还保证找到全局最优——因为一阶链的结构保证了每一步的最优子结构可以安全拼接。工程上,预测阶段几乎都用维特比;训练阶段则是另一套算法,见下一节。

小林按师傅的方法走了一遍,一千人的队十几秒就给出了整串标签,准确率还比之前高出一大截。他感叹:原来“全局最优”不必靠蛮力,记好每一步、别贪心走岔路,就能稳稳走到终点。

δi(j)=max⁡yi−1[δi−1(yi−1)+∑k=1Kλkfk(yi−1,yi=j,x,i)]\delta_i(j) = \max_{y_{i-1}} \left[ \delta_{i-1}(y_{i-1}) + \sum_{k=1}^{K} \lambda_k f_k(y_{i-1}, y_i = j, x, i) \right]
  1. 累计得分δ_i(j) 表示走到第 i 位、且第 i 位贴标签 j 时,前 i 位的最优累计得分。
    δi(j)\delta_i(j)
  2. 递推公式它等于“上一位最优累计得分”加上“从上一标签转移到 j 的特征得分”。
    δi−1(yi−1)+∑k=1Kλkfk(yi−1,yi=j,x,i)\delta_{i-1}(y_{i-1}) + \sum_{k=1}^{K} \lambda_k f_k(y_{i-1}, y_i = j, x, i)
  3. 取最大前驱对所有可能的上一标签取最大,并记录谁是赢家,供回溯使用。
    max⁡yi−1[⋅]\max_{y_{i-1}}[\cdot]
  4. 回溯求解走到队尾取最大 δ_n(j),沿记录倒推整条最优标签序列。
    yn∗=arg⁡max⁡jδn(j),yi−1∗=回溯(yi∗)y_n^* = \mathop{\arg\max}_{j} \delta_n(j), \quad y_{i-1}^* = \text{回溯}(y_i^*)

参数训练

清单有了,可每条规则加几分?小林试着拍脑袋:制服加两分?相机加一分?结果预测一塌糊涂。师傅带他翻出三个月的值班记录——上面是每位游客的长相和最终正确贴的牌子。师傅说:让模型照着正确答案调分,这就是训练。

训练的目标很朴素:让模型给“正确标签序列”的概率尽量高。形式上就是最大化训练集上的对数似然。直观地说,如果模型老把穿制服的人判成游客,说明“制服→S”这条特征的权重太低,就往高调;如果某条特征对判断毫无帮助,它的权重自然会被压下去。

实际中常用 L-BFGS 或 SGD 迭代求解,并加入 L2 正则防止过拟合;计算“模型期望”用前向—后向算法在链上高效完成,而不是真的枚举所有路径。注意:维特比用于“预测时找最优路径”,训练时用的是前向—后向,两者分工不同,不可混淆。

小林照着历史记录调了三轮权重,第二周的错误率就降了一半。他算是明白了:特征负责提问,数据负责回答“该给每个问题多大权重”。所谓训练,就是让模型在旧战场上学会新分寸。

L(λ)=∑(x,y)∈Dlog⁡P(y∣x)=∑(x,y)∈D[∑i=1n∑k=1Kλkfk(yi−1,yi,x,i)−log⁡Z(x)]\displaystyle \mathcal{L}(\lambda) = \sum_{(x, y) \in D} \log P(y \mid x)= \sum_{(x, y) \in D} \left[ \sum_{i=1}^{n} \sum_{k=1}^{K} \lambda_k f_k(y_{i-1}, y_i, x, i) - \log Z(x) \right]
∂L∂λk=∑(x,y)∈D[Fk(y,x)−∑y′P(y′∣x) Fk(y′,x)]\displaystyle \frac{\partial \mathcal{L}}{\partial \lambda_k} = \sum_{(x, y) \in D} \left[ F_k(y, x) - \sum_{y'} P(y' \mid x)\, F_k(y', x) \right]
  1. 对数似然目标把每个训练样本的条件概率取对数再求和,这是要最大化的目标。
    ∑(x,y)∈Dlog⁡P(y∣x)\sum_{(x, y) \in D} \log P(y \mid x)
  2. 分子得分项正确标签序列的特征加权得分,训练时希望它尽量大。
    ∑i=1n∑k=1Kλkfk(yi−1,yi,x,i)\sum_{i=1}^{n} \sum_{k=1}^{K} \lambda_k f_k(y_{i-1}, y_i, x, i)
  3. 分母归一化项减去 log Z(x),逼迫模型同时压低“错误贴法”的得分。
    −log⁡Z(x)- \log Z(x)
  4. 真实计数F_k(y, x) 是特征 k 在正确标签序列上的出现次数。
    Fk(y,x)=∑i=1nfk(yi−1,yi,x,i)F_k(y, x) = \sum_{i=1}^{n} f_k(y_{i-1}, y_i, x, i)
  5. 模型期望后一项是特征 k 在当前模型下所有可能路径的期望出现次数。
    ∑y′P(y′∣x) Fk(y′,x)\sum_{y'} P(y' \mid x)\, F_k(y', x)
  6. 梯度方向真实计数高于期望就加大 λ_k,反之减小,反复迭代直到两者平衡。
    λk←λk+η[Fk(y,x)−EP(y′∣x)[Fk]]\lambda_k \leftarrow \lambda_k + \eta \left[ F_k(y, x) - E_{P(y' \mid x)}[F_k] \right]

CRF与HMM

编剧型师傅的“剧本模型”,在教科书里叫隐马尔可夫模型(HMM)。小林曾经很推崇它:它能讲出完整故事——先按概率排标签顺序,再让每个标签“发射”出观测。但它有个致命伤:故事必须假设“观测之间互不影响”。可现实里,打扮、同伴、通道常常纠缠在一起,HMM 编的故事就失真了。

CRF 不编故事,直接把观测的所有线索作为特征“看”进去,因此能轻松表达“重叠且相关”的特征,这是它相对 HMM 的核心优势。代价是:HMM 有现成的生成故事,训练简单;CRF 必须算归一化常数 Z(x) 和特征期望,训练更贵,特征工程也更依赖经验。

选型建议:数据充足、特征重叠严重、追求精度时优先 CRF;资源紧张、需要快速训练时 HMM 也可用。在深度学习时代,常见做法是把 CRF 接在 BiLSTM/BERT 输出之后:神经网络自动学每个位置的发射得分,CRF 用转移特征保证标签序列的合法性,两者互补。

小林最后把两位师傅都请进了工具箱:编故事的 HMM 负责讲解原理,裁判型 CRF 负责上阵实战。开工前他总会问一句:这条队伍的特征纠缠得厉害吗?是的话,就交给 CRF。

P(x,y)=∏i=1nP(yi∣yi−1) P(xi∣yi)(HMM)P(x, y) = \prod_{i=1}^{n} P(y_i \mid y_{i-1})\, P(x_i \mid y_i) \qquad \text{(HMM)}
  1. 转移概率P(y_i | y_{i-1}) 描述标签之间的“队形”偏好。
    P(yi∣yi−1)P(y_i \mid y_{i-1})
  2. 发射概率P(x_i | y_i) 描述每个标签“生成”出长相 x_i 的概率。
    P(xi∣yi)P(x_i \mid y_i)
  3. 独立假设乘积形式隐含“x_i 只看 y_i、与邻居观测无关”,这是 HMM 表达受限的地方;CRF 用特征函数直接建模任意观测线索,绕开了这个假设。
    ∏i=1nP(xi∣yi)\prod_{i=1}^{n} P(x_i \mid y_i)

应用领域

小林成了“贴牌专家”,被调去各种园区支援:游乐场新闻室要把公告里的部门名、人名、日期标出来;语音助手要把用户的指令切成词并标上词性;连生物实验室都请他帮忙,在 DNA 片段上标注“哪些区域是基因”。他惊喜地发现:只要换一套检查清单,同一套“贴牌智慧”哪儿都通用。

在 NLP 里,这套智慧对应的正是三大经典任务:命名实体识别(人名、地名、机构名)、词性标注(名词、动词、形容词)、中文分词(字序列 → 词边界)。三者共同点是标签之间有强依赖(B 后面才有 I,形容词常在名词前),非常适合 CRF。工程实现上,解码得分常拆成“转移得分 + 发射得分”两部分:

实践技巧手记:数据标注常用 BIO 方案(B 开始、I 内部、O 外部)避免歧义;特征模板以窗口(前后各 1-2 词)、词形(大小写、数字、后缀)、词典匹配为主;与深度学习结合时,让 BiLSTM/BERT 输出每个位置的发射得分,CRF 只学转移约束,既省力又稳;最后记得加正则化与早停,防止成千上万个特征把训练集背下来。

多年后小林成了首席安检师,给新人培训时他只讲一句话:任何“一串东西要逐个贴标签、且标签手拉手”的问题,都可以考虑 CRF。从欢乐谷到实验室,这套从队列里悟出的智慧,就是他职业生涯最趁手的一把利器。

S(x,y)=∑i=1n[T(yi−1,yi)+E(yi,xi)]S(x, y) = \sum_{i=1}^{n} \left[ T(y_{i-1}, y_i) + E(y_i, x_i) \right]
  1. 转移得分从 y_{i-1} 到 y_i 的标签搭配偏好,如 B-ORG 后更可能接 I-ORG。
    T(yi−1,yi)T(y_{i-1}, y_i)
  2. 发射得分位置 i 的观测 x_i 对标签 y_i 的支持程度,可来自词频统计或神经网络。
    E(yi,xi)E(y_i, x_i)
  3. 总得分逐位置求和得到整串标签的得分,维特比算法在这个得分上解码。
    S(x,y)=∑i=1n[T+E]S(x, y) = \sum_{i=1}^{n} [T + E]

AI 应用场景

智能理赔中的命名实体识别

阶段:数据标注 + 模型训练与推理

解决的问题:传统词典加规则抽取保险单中的疾病名、药品名,遇到新词或跨行描述就失效。CRF 把“关键词状态特征”和“标签转移依赖”一起学习,能在整句上下文中稳定抽取实体。

具体案例:某保险公司把理赔描述“患者因高血压入院,服用硝苯地平”喂给 BiLSTM+CRF 模型,标签体系用 BIO(B-疾病、I-疾病、I-药品),自动输出“高血压-疾病、硝苯地平-药品”并填充理赔表单字段,审核效率提升约 3 倍。

对话系统中的槽位填充

阶段:特征工程 + 结构化预测

解决的问题:口语化 query 如“帮我订北京到上海的机票”需要抽取出发地与目的地槽位,独立分类器会把两个地名孤立地判类别,而 CRF 的转移特征能强制“出发地→目的地”的顺序合法,减少槽位错乱。

具体案例:某语音助手在机票预订场景用 CRF 抽取出发地、目的地、日期槽位,转移特征约束槽位顺序,槽位抽取错误率比逐词分类下降约 40%。

生物信息中的基因结构标注

阶段:序列建模 + 解码

解决的问题:在 DNA/蛋白质序列上标注“编码区、非编码区、剪接位点”,相邻位点标签强相关;CRF 的链式结构天然匹配碱基序列的局部依赖性,比窗口分类器更少出现“基因片段断裂”的错误标注。

具体案例:某基因预测工具用 CRF 在基因组序列上标注外显子与内含子边界,特征取 k-mer 频次、GC 含量与剪接信号,解码出的基因片段完整性显著优于逐位分类基线。

Python 演示

python112 行
import math  def all_paths(n, L):    """枚举长度为 n 的所有标签路径, 共 L**n 条"""    if n == 0:        return [[]]    return [p + [j] for p in all_paths(n - 1, L) for j in range(L)]  class TinyCRF:    """线性链CRF最小实现: 特征提取 + 全路径规范化 + 梯度上升训练 + 维特比解码"""     def __init__(self, labels):        self.labels = labels          # 标签表, 如 ['O', 'B-PER', 'I-PER']        self.L = len(labels)        self.w = {}                   # 特征名 -> 权重     def feats(self, x, i, y_prev, y_cur):        """位置 i 的特征: 转移特征 + 单词/大写/前词 状态特征"""        f = {}        f['T:' + self.labels[y_prev] + '>' + self.labels[y_cur]] = 1.0        w = x[i]        f['E:' + self.labels[y_cur] + ':w=' + w] = 1.0        if i > 0:            f['E:' + self.labels[y_cur] + ':prev=' + x[i - 1]] = 1.0        if w[0].isupper():            f['E:' + self.labels[y_cur] + ':cap'] = 1.0        return f     def score(self, x, i, y_prev, y_cur):        """特征线性加权得分, 对应公式中的求和项"""        s = 0.0        for k, v in self.feats(x, i, y_prev, y_cur).items():            s += self.w.get(k, 0.0) * v        return s     def probs(self, x):        """用全路径枚举计算 P(y|x), 返回 (路径列表, 概率列表, 归一化常数 Z)"""        paths = all_paths(len(x), self.L)        raws = []        for y in paths:            s = 0.0            for i in range(len(x)):                s += self.score(x, i, y[i - 1] if i > 0 else 0, y[i])            raws.append(s)        m = max(raws)        exps = [math.exp(v - m) for v in raws]        Z = sum(exps)        return paths, [v / Z for v in exps], Z     def gradient(self, x, y):        """单个样本的梯度: 真实特征计数 - 模型期望特征计数"""        grad = {}        paths, ps, Z = self.probs(x)        for path, p in zip(paths, ps):            for i in range(len(x)):                for k, v in self.feats(x, i, path[i - 1] if i > 0 else 0, path[i]).items():                    grad[k] = grad.get(k, 0.0) - p * v        for i in range(len(x)):            for k, v in self.feats(x, i, y[i - 1] if i > 0 else 0, y[i]).items():                grad[k] = grad.get(k, 0.0) + v        return grad     def train(self, data, iters=25, lr=0.05):        """梯度上升最大化对数似然"""        for step in range(iters):            g = {}            for x, y in data:                for k, v in self.gradient(x, y).items():                    g[k] = g.get(k, 0.0) + v / len(data)            for k, v in g.items():                self.w[k] = self.w.get(k, 0.0) + lr * v     def viterbi(self, x):        """解码: 动态规划找最优路径, 对应维特比算法"""        n = len(x)        best = [[0.0] * self.L for _ in range(n)]        back = [[0] * self.L for _ in range(n)]        for j in range(self.L):            best[0][j] = self.score(x, 0, 0, j)        for i in range(1, n):            for j in range(self.L):                cand = [(best[i - 1][p] + self.score(x, i, p, j), p)                        for p in range(self.L)]                best[i][j], back[i][j] = max(cand)        last = max(range(self.L), key=lambda j: best[n - 1][j])        ys = [last]        for i in range(n - 1, 0, -1):            ys.append(back[i][ys[-1]])        ys.reverse()        return ys  def main():    labels = ['O', 'B-PER', 'I-PER']    data = [        (['Tom', 'visited', 'Beijing'], [1, 0, 0]),        (['Alice', 'loves', 'Shanghai'], [1, 0, 0]),        (['Tom', 'Hanks', 'arrived'], [1, 2, 0]),        (['Alice', 'met', 'Tom', 'Hanks'], [1, 0, 1, 2]),    ]    model = TinyCRF(labels)    model.train(data, iters=30, lr=0.1)    test = ['Tom', 'Hanks', 'visited', 'Beijing']    pred = model.viterbi(test)    print('输入:', ' '.join(test))    print('预测:', ' | '.join(labels[i] for i in pred))  if __name__ == '__main__':    main()
目的:用约 30 行核心逻辑实现一个可训练的线性链 CRF——从特征提取、全路径归一化、梯度上升训练到维特比解码,在微型人名识别数据集上跑通“训练—预测”全流程。
  1. f['T:' + self.labels[y_prev] + '>' + self.labels[y_cur]] = 1.0构造转移特征:前一标签与当前标签的搭配,对应“工作人员后面更可能是工作人员”这类规则。
  2. f['E:' + self.labels[y_cur] + ':w=' + w] = 1.0构造状态特征:当前词与标签的对应,对应“这个游客长什么样”。
  3. exps = [math.exp(v - m) for v in raws]先减去最大值再取指数,避免数值溢出,等价于对数似然里的 log-sum-exp 技巧。
  4. grad[k] = grad.get(k, 0.0) - p * v累加“模型期望”的负梯度项,对应训练公式里的期望部分。
  5. best[i][j], back[i][j] = max(cand)维特比递推:记录当前标签的最优累计得分与最优前驱标签。
  6. model.train(data, iters=30, lr=0.1)训练入口:迭代 30 轮梯度上升,让正确标签序列的概率越来越高。

常见误区

误区我觉得 CRF 就是 HMM 换了个写法,反正都是“前一个状态转移到下一个状态、再生成观测”,训练预测套路都一样。

正解两者数学立场完全不同。HMM 是生成式,先建模 P(x,y) = ∏P(y_i|y_{i-1})P(x_i|y_i),隐含“观测独立生成”的强假设;CRF 是判别式,直接建模 P(y|x),特征函数可以是任意重叠的规则,不用虚构“标签如何生成观测”的故事。就像编剧型师傅必须把剧本写得自洽,裁判型师傅只看证据直接判。

为何常见:两者都是链式结构、都带“转移”,表面长得像;教材常把 HMM 当 CRF 的前置课,容易让人以为前者只是后者的旧版。

误区我一直以为训练 CRF 就是反复跑维特比解码,跑着跑着权重就学好了。

正解维特比是推理/解码算法,回答的是“已知权重,找最优标签序列”;训练回答的是“已知标注数据,学权重”,需要最大化对数似然,计算归一化常数 Z(x) 与特征期望(用前向—后向或全路径枚举),再做梯度上升。二者分工就像:先练好眼力(训练),再用眼力扫队(解码)。

为何常见:很多 demo 把“预测→对比→再预测”的循环展示成学习过程,混淆了“评估”与“训练”。

误区CRF 的强项不就是灵活特征吗?我把所有能想到的几千个特征全塞进去,肯定更准。

正解特征过多会导致过拟合:模型把训练集背下来,见了新队伍就露馅。实践上要加 L2 正则、做特征筛选,特征模板讲“覆盖而不冗余”,并用验证集控制特征规模。就像安检清单条目再多,也得靠数据掂量每条权重。

为何常见:“特征函数是 CRF 的灵魂”这句话被错误放大成“特征数量就是模型质量”。

误区BERT 模型那么强,直接把每个词分类就完了,谁还用 CRF 这种老古董?

正解CRF 至今仍广泛用作结构化输出层(BiLSTM-CRF、BERT-CRF)。神经网络给出每个位置的发射得分,CRF 用转移特征约束标签序列的合法性(如 B-ORG 后不能直接接另一个 B-ORG),保证整句解码结果结构合法。在小样本、强规则领域,纯 CRF 依然能打。

为何常见:看到榜单上深度学习霸榜,就误以为所有传统组件都被淘汰;忽略了“结构化约束”是独立于特征提取的稳定需求。

面试题

Q1. 条件随机场 CRF 属于哪一类概率模型?

单选题 · easy

  • A.判别式概率图模型← 正确答案
  • B.生成式概率图模型
  • C.非概率线性分类器
  • D.无监督聚类算法

答案:A。CRF 直接建模条件概率 P(y|x),只关心“看到长相后该怎么贴牌”,不编“游客如何被生成”的故事,因此是判别式模型。B 是 HMM 的路线,C 和 D 与 CRF 的概率图本质不符。

Q2. CRF 在序列标注任务中直接建模的目标是什么?

单选题 · easy

  • A.观测序列的联合分布
  • B.标签序列的条件概率← 正确答案
  • C.标签序列的先验概率
  • D.特征向量的高斯分布

答案:B。CRF 的目标是 P(y|x):给定整条观测序列 x,求标签序列 y 的概率。A 是生成式模型的做法,C 只关心标签本身,D 与 CRF 无关。

Q3. 线性链 CRF 对标签 yi 的依赖做了怎样的简化假设?

单选题 · mid

  • A.只依赖前一个标签← 正确答案
  • B.依赖全部历史标签
  • C.完全不依赖其他标签
  • D.只依赖后一个标签

答案:A。线性链 CRF 采用一阶马尔可夫假设:P(y_i | y_1……y_{i-1}, x) = P(y_i | y_{i-1}, x)。这个“只看前一人的手”的假设把复杂度从指数级降到多项式级。

Q4. 特征函数 f_k(y_{i-1}, y_i, x, i) 的返回值通常是?

单选题 · mid

  • A.返回 0 或 1 的指示值← 正确答案
  • B.返回任意连续实数
  • C.返回标签的编号
  • D.返回归一化的概率

答案:A。标准线性链 CRF 的特征函数是“规则是否命中”的指示器:命中返回 1,未命中返回 0,对应安检清单里“如果…就加分”的条目。

Q5. 归一化常数 Z(x) 在 CRF 中起什么作用?

单选题 · mid

  • A.让所有路径概率之和为 1← 正确答案
  • B.放大特征得分使预测稳定
  • C.统计特征次数以加速训练
  • D.代替解码器直接输出标签

答案:A。Z(x) 对所有可能标签序列的指数得分求和,把得分变成合法概率。没有它,P(y|x) 就不是概率分布,也无法做极大似然训练。

Q6. CRF 相比 HMM 最主要的优势是什么?

单选题 · mid

  • A.可自由设计重叠特征捕捉细节← 正确答案
  • B.训练过程不需要任何标注数据
  • C.参数量一定少于 HMM 模型
  • D.解码速度比 HMM 快一倍以上

答案:A。CRF 不假设观测独立,可以把词形、上下文、词典等重叠特征任意组合进特征函数;HMM 必须假设“标签生成观测”的独立发射,表达力受限。

Q7. 训练 CRF 计算前向变量时常用 log-sum-exp 技巧,主要目的是?

单选题 · extreme

  • A.防止指数运算发生数值溢出← 正确答案
  • B.把解码换成贪心搜索算法
  • C.减少特征函数的调用次数
  • D.让梯度上升变成闭式求解

答案:A。序列较长时,未归一化的指数得分可能巨大或极小,直接 exp 会溢出。log-sum-exp 先提公共最大值再指数,得到数值稳定的概率,是 CRF 训练与推理的标配技巧。

Q8. 下列哪些任务适合使用线性链 CRF?

多选题 · mid

  • A.命名实体识别任务← 正确答案
  • B.词性标注任务← 正确答案
  • C.中文分词任务← 正确答案
  • D.单张图像分类任务

答案:ABC。三者都是“给序列逐个位置贴标签、且标签间强依赖”的结构化预测任务,正是 CRF 的主场;单张图像分类没有序列结构,不属于 CRF 的适用场景。

Q9. 下列哪些信息能被线性链 CRF 的特征函数刻画?

多选题 · hard

  • A.当前词与标签的对应关系← 正确答案
  • B.相邻标签之间的转移搭配← 正确答案
  • C.字符大小写等词形特征← 正确答案
  • D.整句词数的全局统计特征

答案:ABC。特征函数 f_k 可包含当前观测与标签(A)、相邻标签转移(B)、词形(C)。D 是整句级别的全局统计,标准线性链特征函数不做这种整体计算。

Q10. 关于 CRF 的参数训练,下列说法正确的有?

多选题 · hard

  • A.目标是最大化训练集对数似然← 正确答案
  • B.需要计算归一化常数 Z(x)← 正确答案
  • C.梯度中包含模型期望项← 正确答案
  • D.训练阶段直接调用维特比

答案:ABC。训练最大化对数似然,需要算 Z(x) 与特征期望,因此 A、B、C 正确。维特比是“预测时找最优路径”的解码算法,训练阶段用前向—后向,D 错误。