200 条训练序列、100 条测试序列,每条 10 个位置、2 类标签、4 种观测符号。 相邻位置的标签相同的比例是 0.9117——这就是「序列」两个字的全部内容。 同一套发射表,只把转移表从「抹平」换成「数出来」:逐位置准确率 80.20% 抬到 88.70%, 整条全对 11.00% 抬到 48.00%,孤立单点翻转从 130 个降到 7 个。 序列对数似然平均每条多 3.371 nat(96 条变好、4 条变差), 而理论上限是 5.683 nat——差在那些真的该切换的位置上。
到第 10 章为止,所有方法都默认样本之间互相独立:每个点算自己的分,谁也不看谁。 对着一张图片、一行表格,这个假设还凑合; 对着一句话、一段语音、一串心电图,它就彻底错了——相邻位置的标签强相关。 这一节先把「序列」这件事量出来,再拆掉「独立」这个假设,看它值多少。 顺便会发现:拆掉之后模型变成一个生成模型,它得为观测本身建模——那件事既贵又不必要, 于是下一节改成条件模型。
一、序列和一堆点有什么不同
先造一份「真序列」:两类隐状态,四种观测符号,两个状态的发射分布几乎相反; 每步有 0.90 的概率留在原状态。 把状态序列当标签、符号序列当观测,就得到一个典型的序列标注问题。 数据造出来第一件事是量一句话:相邻位置的标签相同的比例。 训练集 0.9117,测试集 0.9033。 这个数就是序列结构存在的证据——如果标签各自独立,它应该等于两类占比的平方和, 也就是 0.5 上下。 而一个逐位置分类的分类器完全看不到这件事。 它每次只看一个位置的观测,判完就走,两个相邻位置的判断之间没有任何约束。 实测它给出 80.20% 的逐位置准确率——不算差,可整条序列全对的只有 11.00%。 11.00% 这个数怎么理解? 10 个位置、80% 每位的准确率,如果错误彼此独立,整条全对的概率是 0.8 的 10 次方,约 10.7%。 实测 11.00%,和这个估计几乎一模一样。 这说明它的错误确实是「各错各的」——而真实的标签序列根本不是这样错的。 一个更直观的读数:孤立的单点翻转。 也就是某个位置和左右两边都不一样。逐位置分类给出 130 个, 而真实标签里这种形态几乎不出现(实测 0 个下界,因为真模型一变就是一整段)。 130 个孤点说明它把观测噪声当成了信号。
二、把「独立」这个假设拆掉
标准做法是一阶马尔可夫链: 标签序列的概率等于「第一个标签的概率」乘上「每个标签给前一个标签的条件概率」; 观测的概率等于「每个位置的观测给该位置标签的条件概率」。 两者相乘,就得到一条序列的联合概率 P(x, y)。 这个模型叫隐马尔可夫模型(HMM):标签是隐的,观测是显的。 它的参数就三张表:初始概率(2 个)、转移概率(2 乘 2)、发射概率(2 乘 4)。 为了把「序列结构值多少」量干净,我把对照做成了「同一套发射表,只换转移表」: 对照组把转移表的每一行都填成标签的边际概率,等于宣告「下一个标签和上一个无关」; 实验组老老实实数出转移。 这样两组之间唯一的差别就是那 4 个数,别的一模一样。 结果:转移抹平的一组,联合对数似然平均每条 负 17.681;完整的一组 负 14.311。 差 3.371 nat。 100 条测试序列里 96 条变好、4 条变差,中位数 3.170。 为什么会有 4 条变差?因为真模型每步有 10% 的概率切换状态。 碰上「真的该切换」的那一步,完整模型反而要为它付一次低概率的钱; 而抹平的模型对切换根本不敏感,谁都不罚。 所以「序列模型一定更好」这句话,在单条序列的层面上是不成立的——它只在平均意义上成立。 3.371 这个数还可以和理论上限对一下。 如果一条序列的 9 次转移全都走「粘性」那一边,每一步能多赚 log(0.912 除以 0.485), 9 步就是 5.683 nat。 实测 3.371 明显低于它, 差的那部分就是真切换的位置——那里完整模型每步要亏 log(0.088 除以 0.485),约 1.7 nat。 一笔账算下来,赚的比亏的多,净赚 3.371。 注意这里能直接比对数似然,是因为两个都是合法的生成模型,都在给同一个联合分布 P(x, y) 打分。 换成「P(y 给 x)」就不能这么比了——那要先各自算归一化常数, 而 HMM 的常数得把所有标签序列枚举一遍。
三、序列模型的三个动作
一个序列模型要做三件事,缺一不可: 一是建模,也就是写出 P(x, y) 怎么分解;二是学习,也就是从数据里把三张表估出来; 三是预测,也就是给定观测,找出最可能的标签序列。 这一节做的是第一件和第二件里最省事的那条路。 学习用的是「监督地数」:训练数据里标签是已知的,初始、转移、 发射三张表直接数出来再归一化就行,一次都不用迭代。 如果有标签的序列拿不到(只有观测),那就得用 EM 算法——那是另一门课的内容,这里不展开。 预测那件事这一节只做了一半。 Demo 里做了「逐位置取最大」:先算每个位置的后验边际,再各取一次最大。 这个做法把转移算进去了(所以准确率 88.70%),但它仍然是「每个位置单独决定」。 真正「整条最优」的做法叫 Viterbi,留到 11.3 讲。 顺便看清一个现象:逐位置分类、转移抹平的 HMM,这两行的四个数字一模一样。 这不是巧合。 转移抹平之后,每个位置的最优标签只由它自己的观测决定,邻居的分数对两个候选都是同一个常数, 抵消掉了。 所以「抹平转移」就等于「放弃序列」:形式上还是一条链,信息量和一个位置一个位置地判完全相等。
四、生成模型的两个毛病
第一个毛病:它必须给观测建模。 这里观测是四种符号,写一张 2 乘 4 的发射表就行; 真实的序列分类里观测常常是高维向量(一帧语音、一个词的词向量),那就得假设一个密度函数。 密度假设错了,整个模型都歪——而且你不会知道是假设错了还是数据不够。 第二个毛病:参数被「概率」这两个字绑住。 三张表每一行都必须加起来等于 1,这是硬约束。 想加一个新特征——「这个词的结尾是不是 ing」「这个位置和前两个位置是否同标签」—— 就得重新设计整个生成过程,让它仍然是一个合法的概率模型。 很多特征之间的相关性根本没法在一个生成过程里表达清楚。 而分类任务其实只要 P(y 给 x)。 它不需要知道「这段观测本身有多常见」,只需要知道「在这段观测下,哪种标签更可能」。 把 P(x) 整个扔掉,直接给 P(y 给 x) 建模,就能绕开上面两个毛病: 不用给观测假设密度,也不用受「每行和为 1」的约束(归一化交给一个叫 Z(x) 的常数去做)。 这就是下一节的条件随机场。 它的代价也很明确:那个 Z(x) 不好算,每一步都要把所有标签序列过一遍。
五、动手 Demo:序列数据的模型化:去掉「标签和上一位置无关」这个假设,每条序列多 3.371 nat
按真模型造 200 条训练序列、100 条测试序列:每条 10 个位置,2 类标签,4 种观测符号, 转移概率 0.90/0.10。 先量一下标签的粘性,然后用三种做法解码——逐位置独立分类、转移表抹平的 HMM、 完整 HMM(逐位置取最大与整条最优),比较逐位置准确率、整条全对率、预测出来的粘性、孤立单点翻转数。 最后算两种 HMM 的序列级联合对数似然,量出「序列结构」到底值多少。
def fit_hmm(X, Y, sm=1.0): # 监督地数三张表
st = np.bincount(Y[:, 0], minlength=2) + sm
tr = np.full((2, 2), sm); em = np.full((2, 4), sm)
for i in range(len(X)):
for t in range(X.shape[1]):
em[Y[i, t], X[i, t]] += 1.0
if t:
tr[Y[i, t - 1], Y[i, t]] += 1.0
return st / st.sum(), tr / tr.sum(1, keepdims=True), em / em.sum(1, keepdims=True)
# 对照组:把转移表每行都填成标签的边际概率,等于「下一个标签和上一个无关」
py = np.bincount(Y.ravel(), minlength=2) + 1.0
py = py / py.sum()
tr_flat = np.tile(py, (2, 1))
实跑输出:
--------------------------------------------------------------------
序列数据的模型化:去掉「标签和上一位置无关」这个假设,每条序列多 3.371 nat
--------------------------------------------------------------------
数据 200 条训练序列、100 条测试序列,每条 10 个位置
标签 两类,按真模型有强粘性:同一状态留在原地的概率 0.90
观测 四种符号,两类的发射分布几乎相反
相邻同标签比例(训练) 0.9117
相邻同标签比例(测试) 0.9033
**这个 0.90 就是「序列」两个字的全部内容:标签不是各自独立的,它在相邻位置上强烈地跟着前一个走。**
**一个逐位置分类的分类器完全看不到这件事——它每次只看一个位置的观测。**
三种做法摆在一起(同一套发射表,区别只在转移这一项):
做法 逐位置准确率 整条全对 预测出来的粘性 孤立单点翻转
-------------------------------------------------------------------------------
逐位置独立分类 80.20% 11.00% 0.6522 130
转移表抹平的 HMM 80.20% 11.00% 0.6522 130
完整 HMM,逐位置取最大 88.70% 48.00% 0.9022 7
完整 HMM,整条最优 87.80% 49.00% 0.9189 0
**第一行和第二行的数字一模一样,这不是巧合。**
**把转移表抹平之后,每个位置的最优标签只由它自己的观测决定,邻居的分数对所有候选都是同一个常数——所以三种解码退化成同一个结果。**
**换句话说,「抹平转移」就等于「放弃序列」:形式上还是条链,信息量和一个位置一个位置地判完全相等。**
**完整 HMM 那一行:准确率从 80.20% 抬到 88.70%,整条全对从 11.00% 抬到 48.00%。**
**孤立的单点翻转从 130 个降到 7 个:逐位置分类最大的毛病就是会在序列中间插一个突兀的短段,那在真实标签序列里几乎不可能出现。**
**预测出来的粘性 0.9022 对真值 0.9033——把转移算进去之后,模型自己就学会了「标签会连着走」。**
再看序列级别的对数似然(越大越好,都是同一份测试集上的联合概率):
模型 每条序列平均 每个位置平均 说明
------------------------------------------------------------
转移表抹平 -17.681 -1.768 等于把标签当独立
完整 HMM -14.311 -1.431 建模了转移
差 +3.371 +0.337 就是「序列」值多少
**平均每条序列多出 3.371 nat,换算到每个位置是 0.337 nat。**
**100 条测试序列里有 96 条变好、4 条变差,中位数 +3.170。**
**不是每条都变好:真模型每步有 10% 的概率切换状态,碰上「真的该切换」的那一步,完整模型反而要为它付一次低概率的钱。**
**如果每条序列的 9 次转移全走粘性那一边,提升的理论上限是 9 乘 log(0.912 除以 0.485),也就是 5.683 nat。实测 3.371 低于它,差的就是那些真切换的位置。**
**注意这里比的是联合概率 P(x,y),两种模型都是合法的生成模型,所以可以直接比。**
**换成「P(y 给 x)」就没法这么比了——那需要各自算归一化常数,而 HMM 的常数要枚举所有标签序列。下一节 CRF 正是绕开这件事的办法。**
最后看一眼 HMM 的两个毛病:
**一是它必须给观测建模。这里观测是四种符号,还好;真实的序列分类里观测常是高维向量,就得假设一个密度,假设错了整个模型都歪。**
**二是它的参数被「概率」这两个字绑住:每行必须加起来等于 1,想加一个新特征(比如「这个词的结尾是什么」)就得重新设计整个生成过程。**
**分类任务其实只要 P(y 给 x),根本不需要 P(x)。把 P(x) 扔掉,就得到了下一节的条件随机场。**
三张表的归一化方向不要搞错:初始概率对状态求和,转移概率对「下一个状态」那一维求和,
发射概率对「观测符号」那一维求和。
所以是 st.sum()、tr.sum(1)、em.sum(1),三处都不一样。
搞错一处不会报错,只会让某一行加起来不等于 1,然后对数似然悄悄偏掉。
sm=1.0 这个平滑不能省。
数据是有限的,四种符号乘两个状态一共 8 个格子,某些格子可能一次都没出现。
计数为 0 时概率为 0,取对数就是负无穷,整条序列的似然直接崩。
加 1 平滑(拉普拉斯平滑)让每个格子至少有一份虚拟计数,代价是概率被稍微拉平。
对照组那三行是这一节的关键设计:它没有另写一个模型,
只是把转移表「抹平」——每行都填标签的边际概率。
这样两组共用同一张发射表、同一个初始表,差别精确地只剩转移这一项。
如果对照组另起炉灶(比如另写一个逐位置分类器),那比出来的就不只是「序列结构值多少」,
还混进了「两种参数估计方法的差别」,结论就说不清了。
「孤立单点翻转」这个指标是自己写的,不是标准指标,但它是这一节最有说服力的读数。
因为它直接对应「这个结果像不像一条真实的标签序列」。
130 个对 7 个,任何人一眼就明白差别在哪。
比较联合对数似然时,两个模型必须在同一份测试集上、而且都是对同一个联合分布 P(x, y) 打分。
这一条这里满足了,所以可以直接比大小。
下一节的 CRF 给的是条件概率 P(y 给 x),和这里没有可比的公共尺度——不能拿这两个数比。
要说明:这份数据是我按已知的 HMM 造的,所以「完整 HMM 表现好」有一半是主场优势。
换成真实的语料,真模型未必是一阶马尔可夫链,那时完整 HMM 的优势会缩小,
而 CRF 那种「只要特征、不假设生成过程」的做法反而更稳。
30 秒实验
- 把真模型的转移改成 0.60/0.40(粘性变弱),重跑。 「序列」还值多少 nat?理论上限 5.683 会变成多少?
- 把序列长度从 10 加到 30,重跑。 每条序列的提升会随长度线性增长吗?逐位置准确率会变吗?为什么?
- 把观测符号从 4 种加到 8 种,并让两类发射分布更接近,重跑。 逐位置准确率先崩还是整条全对率先崩?三种做法的排序会变吗?
常见误区
- 「序列分类就是把每个位置单独分类,最后拼起来」。 拼起来的结果可能根本不是一条合理的序列。 实测逐位置准确率 80.20%,整条全对只有 11.00%,孤立单点翻转 130 个; 把转移算进去之后变成 88.70%、48.00%、7 个。 「每个位置都对」和「整条对」是两件事。
- 「隐马尔可夫模型是序列分类的唯一办法」。 它只是生成式模型里最经典的一种。 条件随机场(下一节)、循环神经网络、以及各种基于注意力机制的模型都能做序列标注。 它们的分野不在「是不是序列模型」,而在「建模的是 P(x, y) 还是 P(y 给 x)」。
- 「粘性是模型假设出来的,所以是人为加进去的约束」。 粘性是先从数据里量出来的:相邻同标签比例 0.9117。 模型只是把这个事实记住了,没有凭空加东西。 真正人为的是「用一阶马尔可夫链」这个结构选择——它假设「只看前一个位置就够了」。 这个假设在语言里常常不够(远距离依赖很常见),那才是需要更复杂模型的地方。
- 「对数似然大就说明分类准」。 不一定。联合似然关心的是 P(x, y),也就是「这段观测和这段标签一起出现有多常见」; 分类只要 P(y 给 x)。 一个模型可以在 P(x) 上下了很大功夫,让联合似然很好看,可 P(y 给 x) 一点没变好。 这个错位正是下一节要把 P(x) 扔掉的原因。
自测三问(点开对答案)
1. 为什么「把转移表抹平」和「逐位置独立分类」的结果一模一样? —— **因为抹平之后,每个位置的分数里属于邻居的那一项对所有候选标签都是同一个常数。** **比较两个候选时,常数抵消掉了,剩下的只有这个位置自己的观测分数。** **于是「在整条链上做动态规划」退化成了「每个位置各取一次最大」。** **实测四个数字完全相同(80.20%、11.00%、0.6522、130),就是这件事的证明。** 2. 为什么逐位置准确率只涨了 8 个点,整条全对率却涨了 4 倍多? —— **因为整条全对要求 10 个位置全对,它对单点错误是「一票否决」。** **逐位置 80% 意味着平均每条序列错 2 个位置;** **哪怕错得再分散,整条全对的概率也只有 0.8 的 10 次方,约 11%。** **逐位置 88.7% 时,平均每条错 1.13 个,整条全对的概率就上到接近一半。** **标签粘性帮了大忙:错误不再是 10 次独立试验,而是成段出现的,** **所以「一条全错在同一个地方」的情况变多,「每条都错一点点」的情况变少。** 3. 生成模型的两个毛病是什么?为什么条件模型能绕开? —— **一是必须给观测 P(x) 建模,高维观测就得假设密度,假设错了全盘皆错。** **二是参数被「每行和为 1」绑住,想加任意特征就得重新设计生成过程。** **条件模型直接给 P(y 给 x) 建模:不需要 P(x),所以不用假设密度;** **归一化交给 Z(x),所以权重可以自由取值,想加什么特征就加什么特征。** **代价是 Z(x) 必须算出来,而它要对所有标签序列求和——这就是下一节的技术难点。**生成模型要给观测建模,还要被「每行和为 1」绑住。 下一节把这些全扔掉,直接给「给定观测时标签的概率」建模,权重想取多大取多大。 换来的是自由,付出去的是一个叫 Z(x) 的归一化常数——它得把所有标签序列过一遍, 于是动态规划从「可选」变成了「必须」。
- ☐ (本节完,下一节 11.2 11.2)