算法的故事 / 第一季第二季目录0 / 30 章已读

第二季 · 第 49 章

在无数条错误路径中寻找真相

《算法的故事》第二季 · 第四篇:穿越噪声的信息——安德鲁·维特比与动态规划解码算法的诞生1966年,美国加利福尼亚州,洛杉矶。加州大学洛杉矶分校的一间教室里,一位年轻的工程学教授正在准备通信理论课程。他的学生需要理解一种特殊的编码。它不是把消息切成若干完全独立的块。而是让编码器记住前面几个输入比特。每当一个新比特到来,编码器都会根据当前输入与之前的状态,输出一组新的编码比特。这项技术叫作:卷积码(Convolutional Code)。它能够为通信加入有结构的冗余信息。对于卫星通信与无线电链路,这种能力具有实际价值。但一旦信道存在噪声,接收端就会遇到非常困难的问题。收到的一串比特,可能在许多位置发生过翻转。而编码器的输出,不仅取决于某个输入比特,还与之前的输入有关。因此,要恢复完整消息,就不能简单地把每一位独立地纠正。接收端需要反过来推断:发送端最有可能依次输入过哪些比特?编码器曾经经过怎样的内部状态?哪一条完整的状态路径,最能解释当前收到的信号?如果把每一种可能的原始消息都列出来,再重新编码并与接收数据比较,那么候选数量会随着消息长度迅速增加。假设原始消息有100位。全部可能的输入序列数量就是:21002^{100}这是一个极其庞大的数字。可是,年轻的教授在准备课程时,注意到一项能够改变整个计算结构的性质。不同的历史路径,可能重新汇合到同一个编码器状态。如果两条路径已经抵达相同的状态,而其中一条与过去接收到的信号明显更不一致,那么较差的那条路径还有必要继续保留吗?对于以完整路径似然为目标、并满足有限状态与可加路径度量条件的问题,答案是:不需要。因为从相同状态出发,未来能够选择的分支完全相同。既然较差的路径已经输在过去,那么它不可能仅靠完全相同的未来选择,反过来击败另一条更优的路径。因此,我们只需要:对每一个可能的当前状态,保留抵达它的最佳历史路径。其他更差的历史路径,可以丢弃。随着新的信号不断到来,我们重复这项操作。直到收到整段消息。最后,从最优终点状态一路回溯,就能恢复整条最可能的原始路径。这便是后来闻名于世的:维特比算法(Viterbi Algorithm)。而那位正在为学生准备课程的年轻教授,就是:安德鲁·詹姆斯·维特比(Andrew J. Viterbi)。这段故事并不是后人凭空添加的课堂传奇。2015年,MIT在采访中记录了维特比本人对发明过程的回忆。他说,自己当时只是希望找到一种更容易向学生解释通信问题的方法。而他在1999年接受IEEE历史中心口述史采访时,也明确指出,这项算法来自他的教学工作。 [Massachusetts Institute of Technology][Engineering and Technology History Wiki]1966年3月,他完成了包含相关研究的论文稿件。1967年4月,论文正式发表。Error Bounds for Convolutional Codes and an Asymptotically Optimum Decoding Algorithm《卷积码的错误界与一种渐近最优译码算法》。发表于《IEEE Transactions on Information Theory》第13卷第2期,第260—269页。 [数字对象标识符]维特比1967年的原始论文。需要注意,论文的重点不仅是介绍一种搜索算法,还包括卷积码错误概率界和译码性能的理论分析。但是,历史还有一个耐人寻味的细节。维特比在1999年的回忆中提到:当时他并没有充分意识到,这项算法未来会具有多大的实际价值。一度还有人认为,没有必要在论文中详细描述它。后来,论文审稿人之一詹姆斯·梅西(James Massey)鼓励他将算法保留下来。 [Engineering and Technology History Wiki]如果这段研究经历只被写成“一个人突然想到了一种更快的算法”,我们就会错过真正重要的过程。一项来自教学需求的数学整理。一次对编码状态结构的重新理解。一篇同时研究理论错误界的论文。以及后来多年不断发展的工程实现。它们共同构成了维特比算法的历史。而我们今天要解决的核心问题,可以写成一句话:当一段受损的观测序列背后存在无数条候选历史,我们怎样不用枚举全部路径,就找到最有可能发生过的那一条?

第一幕:1939年,一个孩子逃离意大利;1958年,他参与了美国第一颗成功卫星的通信工作

1935年3月9日,安德鲁·维特比出生于意大利北部城市贝加莫。他的父亲阿基莱是一名眼科医生。母亲名叫玛丽亚·卢里亚。1938年,墨索里尼的法西斯政权实施针对犹太人的种族法律。维特比的家庭受到迫害。1939年,全家离开意大利,移居美国。他们先在纽约生活,后来搬到波士顿。维特比在波士顿接受教育,进入著名的波士顿拉丁学校。他后来回忆,自己大约十岁时,常常隔着查尔斯河望向麻省理工学院,希望有一天能够在那里学习。[OAC][Engineering and Technology History Wiki]

从数学与电子学走向通信

1952年,维特比进入麻省理工学院。他的研究兴趣逐渐集中于通信与电子工程。1956年获得学士学位。1957年取得硕士学位。同年6月,他加入加州理工学院喷气推进实验室(JPL)。当时,苏联尚未发射第一颗人造卫星Sputnik 1。但导弹控制、无线电通信与远距离信号传输,已经是重要的技术研究方向。维特比参与了扩频通信和相关电子系统的研究。这段经历让他接触到:移位寄存器。伪随机序列。无线电信号。以及远距离遥测。后来,这些技术与他在卷积码方面的研究形成了自然联系。

1958年:Explorer 1

1958年1月31日,美国成功发射Explorer 1。这是美国第一颗成功进入地球轨道的人造卫星。维特比作为JPL团队成员,参与了相关通信与遥测系统的工作。需要准确区分:他并不是独自设计了整颗卫星。也不是在Explorer 1上已经使用了后来才发表的维特比算法。他当时参与的是更早期的通信技术研究。而这些实践经历,帮助他认识到:当无线电信号微弱、受到噪声干扰,或者传输条件发生变化时,通信系统如何可靠恢复信息,是一项非常重要的工程问题。[School of Engineering][Engineering and Technology History Wiki]

1962年:博士论文

维特比随后在南加州大学继续攻读博士。1962年,他获得电气工程博士学位。他的研究继续围绕数字通信、信号理论和相关编码问题展开。1963年,他进入加州大学洛杉矶分校任教。这一变化,看起来只是从工程研究岗位转向大学课堂。但实际上,它为算法的诞生创造了重要条件。因为当他需要将复杂的卷积码译码问题,解释给尚未完全掌握相关理论的学生时,原来那些他已经习以为常的研究方法,突然显得过于复杂。他不得不重新整理这个问题。

1966年:一项来自教学的发现

在1999年的IEEE口述史中,维特比明确表示:他在教授卷积码时,感到一些理论很难讲清楚。于是,他开始寻找更简单的解释方法。最终形成了后来以自己名字命名的算法。这与后来一些技术史中常见的“研究者在实验室里独自苦思,忽然灵光一现”的故事不同。它是一项来自重新理解已有知识的发现。维特比试图回答:为什么这些编码序列之间存在复杂关系?如果从编码器的内部状态出发,能不能把问题变得更清晰?而当状态图出现之后,一项重要性质也逐渐显现。复杂的历史可以很长,但为了决定未来,我们未必需要记住全部历史。只需要保留能够影响未来的状态。以及抵达该状态的最佳历史结果。这一思想,将成为维特比算法的数学核心。

第二幕:上一章用多项式编码,这一章为什么换成移位寄存器?

上一章,我们研究了里德—所罗门码。它将一段消息表示为有限域上的多项式。例如:P(x)=a0+a1x+⋯+ak−1xk−1P(x)=a_0+a_1x+\cdots+a_{k-1}x^{k-1}然后在多个不同位置计算取值,形成带有冗余的编码符号。这是一种典型的分组编码思想。但现在,假设数据源不是一次性提交完整消息。而是不断向外发送比特:u1,u2,u3,…u_1,u_2,u_3,\ldots例如,卫星上的遥测设备不断产生观测数据。或者无线通信设备持续发送一串数字信号。这时,我们可以让编码器保留一小段历史。每到来一个新比特,就根据当前输入和之前的若干输入,产生编码输出。这种编码叫作:卷积码(Convolutional Code)。

一个最简单的教学编码器

我们设计一个拥有两个记忆单元的编码器。记忆单元保存:m1m_1以及:m2m_2它们分别代表前一个和前两个输入比特。当前输入是:utu_t每次输入一个比特,编码器输出两个比特:vt,1=ut⊕m1⊕m2\boxed{v_{t,1}=u_t\oplus m_1\oplus m_2}vt,2=ut⊕m2\boxed{v_{t,2}=u_t\oplus m_2}随后更新内部记忆:(m1′,m2′)=(ut,m1)\boxed{(m_1',m_2')=(u_t,m_1)}这里:⊕\oplus表示异或(XOR)。
图49-1:两级记忆的卷积编码器
m₁m₂uₜ⊕⊕v₁v₂结构示意:每次输入一个新比特,输出两位编码结果
查看 SVG 配图原件
示意图原件 49-006-7ee6f82007e0.svg
49-006-7ee6f82007e0.svg · 点击查看大图
该图表达记忆状态、异或组合和输出关系。具体连线以正文中的两条异或公式为准,并非电路制造用原理图。

这项编码的参数是什么?

每输入:11个比特。输出:22个比特。因此编码率为:R=12\boxed{R=\frac12}这里的:RR是原始信息比特数与编码输出比特数之比。它不是通信信道容量。两者属于不同的概念。编码器需要当前输入,以及前两个输入。所以,它的约束长度为:K=3\boxed{K=3}在常见的八进制生成多项式写法中,这个编码器对应:(7,5)8\boxed{(7,5)_8}因为:78=11127_8=111_258=10125_8=101_2其中:111表示第一个输出使用当前输入和两个历史位。101表示第二个输出使用当前输入与第二个历史位。这是通信理论教材里常见的一种教学卷积码。但不应把这个具体编码器误认为维特比1967年论文中唯一使用的编码结构。

一位输入,为什么会影响后面多个输出?

假设编码器初始状态:m1=0,m2=0m_1=0,\qquad m_2=0现在输入:11输出:v1=1⊕0⊕0=1v_1=1\oplus0\oplus0=1v2=1⊕0=1v_2=1\oplus0=1因此输出:1111然后状态更新:(m1,m2)=(1,0)(m_1,m_2)=(1,0)下一次输入:00输出:v1=0⊕1⊕0=1v_1=0\oplus1\oplus0=1v2=0⊕0=0v_2=0\oplus0=0因此输出:1010现在状态为:(0,1)(0,1)即使输入仍然为0:v1=0⊕0⊕1=1v_1=0\oplus0\oplus1=1v2=0⊕1=1v_2=0\oplus1=1输出仍会受到之前输入1的影响。这就是卷积码的“记忆性”。当前输出,不只取决于当前输入,还取决于一段有限的过去。

第三幕:把10110送进编码器,究竟会发生什么?

现在,我们使用刚才的编码器,处理一段具体消息:10110\boxed{10110}初始状态:0000但为了方便译码时明确终点,我们还要在消息末尾加入两个0。这两个0叫作:终止比特(Tail Bits)。因此,编码器实际收到:1011000\boxed{1011000}最后两个0不是原始消息内容。它们的作用,是让两个记忆单元最终回到:0000状态。对于这个两级记忆编码器,如果输入消息末尾连续添加两个0,最终状态就能回到00。

第一次输入:1

初始状态:0000输入:u=1u=1输出:1111新状态:1010

第二次输入:0

当前状态:1010输入:u=0u=0输出:1010新状态:0101

第三次输入:1

当前状态:0101输入:u=1u=1输出:0000新状态:1010后面四轮继续执行。
图49-2:完整卷积编码过程
时刻输入旧状态输出新状态
11001110
20101001
31010010
41100111
50110101
60011100
70000000
使用速率1/2、约束长度3、生成多项式(7,5)八进制的编码器,初始状态00。最后两位为终止比特。
于是,最终输出为七组:
text
11 10 00 01 01 11 00
连成一串:11100001011100\boxed{11100001011100}这是一段14位编码消息。原始真正的信息内容为:1011010110加上两个终止比特,编码器处理了7个输入位。所以,在包含终止开销的这段短消息中,实际有效信息率是:514\frac5{14}约为35.7%。它低于卷积编码器的名义码率:12\frac12这是因为两位终止比特本身不承载新的原始信息。如果消息很长,这项固定长度的终止开销在总比特数中的比例,就会越来越小。

现在,噪声出现了

假设第三组编码输出:0000在传输过程中,第二个比特发生翻转。接收端看到:0101因此,整段收到的数据变成:
text
11 10 01 01 01 11 00
原始发送:
text
11 10 00 01 01 11 00
第三组只错了一位。但接收端并不知道究竟是哪一位发生错误。它也不知道原始消息是:1011010110如果它只看到第三组:0101甚至可能误以为编码器在那个时刻采取了另一个输入。因为:0101本身也是某些状态转移能够产生的合法输出。这说明,卷积码译码不能只看某一组输出是否“长得合法”。它必须综合考虑:前面经过的状态。当前可能输入。后面接收到的数据。以及整条路径的总一致程度。于是,我们的任务变成:在所有可能的编码器历史中,找到一条输出序列与收到的数据最吻合的路径。这便是维特比算法真正要解决的问题。

第四幕:四个状态,为什么能够代替无限增长的历史?

刚才,我们发现编码器的输出与过去有关。这似乎意味着:如果消息已经传输了十万位,接收端就必须考虑前面全部十万位的历史。但仔细观察编码器。它实际上只记住两个比特:(m1,m2)(m_1,m_2)而每个记忆位只有两种可能:0。1。所以全部可能状态,只有:22=42^2=4种。分别是:00, 01, 10, 11\boxed{00,\ 01,\ 10,\ 11}这是整项算法的第一个重大简化。无论消息已经传输10位。100位。还是100万位。当前编码器的内部状态,都只可能是这四种之一。

为什么只记住两个过去的输入就够了?

因为我们的编码公式只使用:当前输入 utu_t。前一个输入 m1m_1。前两个输入 m2m_2。在下一个时刻,所有更早的信息都不会直接影响输出。所以,对于预测编码器的未来行为,只需要知道当前的两个记忆位。数学上,我们将这种性质称为:有限状态性(Finite-State Property)。在概率模型中,对应的思想与马尔可夫性密切相关。当前状态总结了与未来转移有关的必要历史。

每个状态只有两种下一步选择

假设当前状态是:1010如果下一次输入:u=0u=0那么:v1=0⊕1⊕0=1v_1=0\oplus1\oplus0=1v2=0⊕0=0v_2=0\oplus0=0输出:1010新状态:0101如果下一次输入:u=1u=1那么:v1=1⊕1⊕0=0v_1=1\oplus1\oplus0=0v2=1⊕0=1v_2=1\oplus0=1输出:0101新状态:1111因此,从状态10出发,只有两种合法转移。其他状态也一样。
图49-3:四状态编码器的完整转移表
当前状态输入0 → 输出 / 新状态输入1 → 输出 / 新状态
0000 / 0011 / 10
0111 / 0000 / 10
1010 / 0101 / 11
1101 / 0110 / 11
“输出 / 新状态”遵循前面的(7,5)八进制卷积编码规则。它是后续网格图计算的唯一状态转移依据。

如果我们不知道原来的输入呢?

发送端知道每次输入是0还是1。但接收端不知道。因此,接收端必须同时考虑:当前可能处于哪个状态?下一步可能输入0还是1?如果从起点00出发:第一步有两条可能路径。第二步最多有四条。第三步最多有八条。以此类推。如果消息长度为:TT全部输入序列数量为:2T\boxed{2^T}所以,虽然编码器只有四个内部状态,但不同的历史路径数量依然可能呈指数增长。关键问题就在这里。状态数量很小,不代表历史路径数量也很小。那我们怎样利用只有四个状态这一事实,避免保存所有历史路径?答案是:将相同终点状态的历史路径互相比较。

第五幕:两条历史到达同一个状态,为什么较差的一条永远不必再考虑?

这是整个维特比算法最重要的一幕。设想两条候选历史路径:AA和:BB它们的过去完全不同。但在第 tt 个时刻,都到达了同一个编码器状态:ss假设,经过前面全部观测数据的比较:路径A累计发生:33个比特不一致。路径B累计发生:55个比特不一致。那么,如果我们以后都从同一个状态 ss 出发,并继续采用完全相同的后续输入序列,会发生什么?

共同的未来,意味着共同的后续代价

因为两条路径目前都处于状态:ss所以,只要未来选择相同的输入,它们将产生相同的后续编码输出。因此,未来新增加的错误代价也完全相同。假设未来代价为:cc那么路径A的最终代价是:3+c3+c路径B的最终代价是:5+c5+c显然:3+c<5+c3+c<5+c不管未来发生什么,只要两条路径拥有相同的后续选择,路径B都不可能反败为胜。因此:我们可以永久丢弃路径B,只保留路径A。

什么叫作幸存路径?

对于每个当前状态,我们只保存一条累计代价最小的历史路径。这条路径叫作:幸存路径(Survivor Path)。如果两条路径代价相同,可以按照预先规定的平局规则选择其中之一。它们在当前目标下同样优秀。所以,保留一条就足够找到某个最优解。如果任务要求列出全部并列最优路径,则需要额外保存多个前驱,不能简单丢弃所有平局路径。

这项思想为什么不会丢失真正的最优答案?

可以用反证思路理解。假设我们丢弃了抵达状态 ss 的路径B。原因是还有一条路径A,它们抵达同一时刻的同一状态,但:cost⁡(A)≤cost⁡(B)\operatorname{cost}(A)\le\operatorname{cost}(B)如果某条全局最优路径包含B,那么我们可以把它到达状态 ss 之前的历史替换成A。因为两条历史在同一时刻抵达同一状态,后面完全可以继续执行原来的后续输入。而替换后的总成本不会更高。所以,即使B出现在某个最优路径里,仍然存在一条至少同样优秀、使用A作为前缀的路径。因此,为了找到一个全局最优解,没有必要保留B。这就是维特比算法正确性的核心理由。

从指数数量的历史,压缩为有限数量的最佳前缀

如果时间已经来到:t=100t=100全部输入历史可能有:21002^{100}种。但编码器只有四个状态。因此,我们只需要保存:到状态00的最佳历史。到状态01的最佳历史。到状态10的最佳历史。到状态11的最佳历史。也就是每个状态一条最佳前缀路径。在每个新时刻,重新计算并更新这些幸存路径。
图49-4:同一终点状态只保留最佳历史
s路径A · 代价3路径B · 代价5相同未来
查看 SVG 配图原件
示意图原件 49-010-f04d8218530c.svg
49-010-f04d8218530c.svg · 点击查看大图
如果A和B在同一时刻进入相同状态,那么未来合法转移集合相同。只要路径成本可加,较差的历史前缀就可以丢弃。

这不是碰运气的剪枝

有些搜索算法为了加快速度,会使用启发式估计。它可能根据经验,暂时不探索某些看起来不太有希望的路线。但维特比算法这里进行的剪枝,不是凭感觉。它依赖一项严格的状态合并与最优子结构性质。因此,在模型与代价设定正确、未实施额外近似剪枝的情况下,维特比算法能够准确找到整条序列的最优路径。这个“最优”的含义,需要与我们选定的路径度量保持一致。如果路径代价代表二元对称信道中的比特错误数,那么它寻找的是累计汉明距离最小的合法路径。如果路径分数代表隐藏状态模型的对数联合概率,那么它寻找的是联合概率最大的状态路径。两者使用相同的递推思想,但对应的数学目标不同。这项共同结构,最终就是:动态规划(Dynamic Programming)。

第六幕:贝尔曼的动态规划,怎样变成了维特比的幸存路径?

现在,我们暂时离开通信工程。回到1950年代。美国数学家 理查德·贝尔曼(Richard Bellman),正在研究多阶段决策问题。许多复杂决策并不是一次完成的。而是经历:第一阶段。第二阶段。第三阶段。直到最终目标。如果我们每次都重新枚举过去所有历史,就会产生大量重复计算。贝尔曼发展的动态规划理论,强调了这样一项思想:如果希望得到一条整体最优策略,那么它在达到某个中间状态之后的剩余决策,也必须与该状态下的相关最优决策结构相一致。这种思想通常被称为:最优性原理(Principle of Optimality)。1957年,贝尔曼出版《Dynamic Programming》,系统整理了这一研究领域。但动态规划并不是1967年维特比第一次发明的。维特比算法的特殊价值,是将动态规划的状态递推结构,与卷积码的序列译码问题结合起来。

一条简单的递推公式

现在,设:Dt(s)D_t(s)表示:在处理前 tt 组接收数据之后,所有以状态 ss 结束的合法历史路径中,累计代价最小的数值。假设从状态:s′s'可以转移到:ss而该转移对于第 tt 组观测的代价为:ct(s′,s)c_t(s',s)那么,到达状态 ss 的最优路径,一定来自某个前一时刻的状态。因此:Dt(s)=min⁡s′→s[Dt−1(s′)+ct(s′,s)]\boxed{ D_t(s)= \min_{s'\rightarrow s} \left[ D_{t-1}(s')+c_t(s',s) \right] }这就是在最小化路径代价的形式下,维特比算法的核心递推式。如果我们使用最大化对数似然的形式,则可以写成:Mt(s)=max⁡s′→s[Mt−1(s′)+ℓt(s′,s)]\boxed{ M_t(s)= \max_{s'\rightarrow s} \left[ M_{t-1}(s')+\ell_t(s',s) \right] }其中:ℓt\ell_t代表该转移及当前观测带来的对数似然分数。

这两个公式为什么其实是一回事?

假设一个候选路径的概率是:PP为了找到概率最大的路径,我们可以最大化:log⁡P\log P因为对数是单调递增函数。也可以最小化:−log⁡P-\log P因此:max⁡log⁡P\max\log P与:min⁡(−log⁡P)\min(-\log P)对应同一个最优候选。在二元对称信道、独立翻转概率:0<p<120<p<\frac12的设定下,如果一条合法编码路径与接收序列在 dd 个比特位置上不同,总共比较 NN 个比特,那么:P(y∣c)=(1−p)N−dpdP(y\mid c) = (1-p)^{N-d}p^d取对数:log⁡P(y∣c)=(N−d)log⁡(1−p)+dlog⁡p\log P(y\mid c) = (N-d)\log(1-p)+d\log p整理:=Nlog⁡(1−p)+dlog⁡p1−p=N\log(1-p) +d\log\frac{p}{1-p}由于:p1−p<1\frac{p}{1-p}<1所以:log⁡p1−p<0\log\frac{p}{1-p}<0因此:最大化这条路径的接收似然,等价于最小化它与接收序列之间的汉明距离。这便是为什么,在我们的教学案例里,可以直接采用:每一位相同,代价0。每一位不同,代价1。然后寻找累计代价最小的路径。

2017年,维特比本人怎样解释自己的算法?

在算法发表五十周年之际,维特比于2017年为南加州大学写了一篇说明:The Viterbi Algorithm Demystified。《维特比算法解密》。他使用状态图、转移分数与最优路径度量,给出了简洁的解释。其中最重要的一项递推是:Mj(k)=max⁡i[Mi(k−1)+mij(k)]M_j(k)= \max_i\left[M_i(k-1)+m_{ij}(k)\right]他还特别强调:除了当前的最优状态分数,程序还必须保存导致这些最优分数的历史路径信息,才能在最后恢复完整答案。[School of Engineering]这说明:维特比算法的完整实现,不只是计算一个最小值。还必须记录:最优值究竟来自哪个前驱状态。否则,我们可能知道最佳路径的总分数,却无法恢复真正的原始消息。

第七幕:一组错误的接收数据,怎样在网格图上一步步变回正确消息?

我们现在终于拥有了全部数学工具。首先,有一个拥有四个状态的编码器。其次,每一步输入0或1,都对应一条确定的状态转移。第三,信道可能翻转某些编码输出比特。第四,接收端希望找到累计汉明距离最小的合法状态路径。于是,我们把所有状态按照时间排列。每一列代表一个时刻。每一行代表一个编码器状态。相邻两列之间,使用合法状态转移建立连线。这种图叫作:网格图(Trellis Diagram)。也常译为:篱笆图。

为什么不直接使用普通的状态图?

因为普通状态图只有四个节点:00、01、10、11。状态之间可以反复转移。而维特比算法处理的是一个按时间展开的序列问题。同一个状态:0000在第1时刻。第2时刻。第3时刻。虽然代表同一种编码器记忆状态,但它们对应的观测数据和累计成本不同。因此,我们需要将同一状态在不同时间复制为不同的网格图节点。
图49-5:四状态卷积码的时间展开结构
00011011t=0t=1t=2t=3
查看 SVG 配图原件
示意图原件 49-013-ab0d683102f7.svg
49-013-ab0d683102f7.svg · 点击查看大图
概念图仅画出部分连接,展示“相同状态在不同时间层重复出现”的结构。完整网格图使用前面转移表里的全部合法连接。

第一次接收到:11

我们知道编码器从:0000开始。因此初始代价:D0(00)=0D_0(00)=0其他三个状态在初始时刻不可到达:D0(01)=D0(10)=D0(11)=∞D_0(01)=D_0(10)=D_0(11)=\infty现在,第一组收到:1111如果输入0:从00转移到00。输出:0000与收到的:1111相差两位。所以代价是:22如果输入1:从00转移到10。输出:1111与收到的数据完全相同。代价:00因此,第一轮结束:D1(00)=2\boxed{D_1(00)=2}D1(10)=0\boxed{D_1(10)=0}另外两个状态仍不可达。

第二次接收到:10

现在,需要从已经到达的状态继续扩展。从00出发:输入0,输出00,新状态00,与10相差1位。因此:D2(00)=2+1=3D_2(00)=2+1=3输入1,输出11,新状态10,与10相差1位。因此:D2(10)=2+1=3D_2(10)=2+1=3从10出发:输入0,输出10,新状态01,与10完全相同。因此:D2(01)=0+0=0D_2(01)=0+0=0输入1,输出01,新状态11,与10相差2位。因此:D2(11)=0+2=2D_2(11)=0+2=2最终,第二轮各状态代价为:D2=[3032]\boxed{ D_2= \begin{bmatrix} 3\\ 0\\ 3\\ 2 \end{bmatrix} }这里的顺序是:00, 01, 10, 1100,\ 01,\ 10,\ 11

第三次接收到:01

注意,真正发送的第三组原本是:0000但受到噪声影响,接收端看到:0101现在,我们从第二轮的四个状态继续扩展。其中,到状态00存在两条主要候选路线。第一条来自状态00:历史代价3。新输出00与接收01相差1位。总成本:3+1=43+1=4第二条来自状态01:历史代价0。输入0后输出11,与接收01相差1位。总成本:0+1=10+1=1因此:D3(00)=min⁡(4,1)=1D_3(00)=\min(4,1)=1我们保留第二条路线。丢弃第一条路线。到状态10,同样存在两条可能的前驱。来自00的候选:3+1=43+1=4来自01的候选:0+1=10+1=1因此:D3(10)=1D_3(10)=1其他状态继续计算,得到:D3=[1213]\boxed{ D_3= \begin{bmatrix} 1\\ 2\\ 1\\ 3 \end{bmatrix} }这是一个非常关键的现象。虽然第三组接收数据已经发生错误,但正确的历史路径并没有因此被迫中断。因为程序比较的是:完整路径的累计一致程度,而不是强迫每一组局部观测都必须完美匹配。接下来,随着后面四组数据陆续到来,算法会继续更新全部状态的幸存路径。最终,结合已知的终止状态00,就能够找到最优的完整编码器历史。

本例七轮的累计状态代价

图49-6:每轮幸存路径的最低累计代价
时刻收到00011011
0—0∞∞∞
1112∞0∞
2103032
3011213
4012321
5013132
6111333
7001434
最小化每次输出符号对的汉明距离。初始状态为00,且当前已知两位终止比特使第7轮结束状态必须为00。
最后:D7(00)=1D_7(00)=1说明,从初始00出发,七轮以后回到00的合法路径中,存在一条与接收编码数据仅相差1个比特的路径。通过保存的前驱关系逐步回溯,恢复出输入:1011000\boxed{1011000}去掉末尾两个终止比特:10110\boxed{10110}成功恢复原始消息。但我们还没有真正看到,算法是怎样在每一步保留和丢弃路径的。下一幕,我们将让整张网格图变成一套可以操作的实验。

第八幕:互动实验室——看见每一条幸存路径怎样穿过噪声

现在,我们要把维特比算法真正放进一张可以操作的网格图。实验沿用刚才的四状态卷积编码器。发送端的原始消息为:1011010110补上两个终止比特之后:10110001011000编码输出:
text
11 10 00 01 01 11 00
你可以选择:完全没有噪声。只有一位出错。有两位分布在不同位置的错误。或者受到更严重干扰的接收序列。然后,逐轮观察算法怎样更新四个状态的累计代价。尤其要注意:当两条不同历史路径汇合到同一个状态时,只有累计成本较低的那条,会成为该状态的幸存路径。

实验:逐轮执行维特比译码

图49-7:四状态维特比译码实验台使用速率1/2、生成多项式(7,5)的卷积码。每一轮根据接收符号对更新最小累计汉明距离,保存各状态的最佳前驱。
真实发送的编码序列
11
10
00
01
01
11
00
实际收到的编码序列
11
10
01
01
01
11
00
红色组包含传输中发生的比特翻转,共 1 位。这个真实错误数量仅用于教学核验;译码器并不知道它。
0 / 7
000110110∞∞∞t=0t=1t=2t=3t=4t=5t=6t=7节点数字为该时刻抵达该状态的最低累计错误代价
各状态幸存前驱
当前回溯示例路径
其他候选转移
初始状态编码器从00开始。因此00的累计成本为0,其他状态暂时不可达。选择“下一轮”处理第一组接收数据。
状态最低代价幸存前驱
000尚不可达或初始
01∞尚不可达或初始
10∞尚不可达或初始
11∞尚不可达或初始
图中没有强制所有时刻都有四个可达状态。等代价时,采用固定的状态遍历与输入顺序选择一条幸存路径。最终只在已知终止状态00上回溯,避免将“全状态最优”和“终止状态受限最优”混为一谈。

实验观察一:在第三轮,噪声已经出现

选择:“一位翻转”。然后把进度调到第三轮。你会看到:接收到的第三组编码比特:0101与真正发送的:0000不同。但是,算法并没有在此停止。它继续维护四个状态的最佳累计成本。

实验观察二:到第七轮,为什么可以恢复正确输入?

直接查看完整结果。最终恢复:10110001011000去除终止比特:1011010110累计汉明距离为:11这与我们人为制造的一次比特翻转一致。但请记住:译码器没有事先知道真正发生了几次错误。它只是找出了给定约束下,与接收序列最一致的合法编码路径。

实验观察三:噪声越强,算法不一定越“聪明”

尝试较强干扰案例。算法依然能够找出一条最小代价路径。但这条路径不一定就是发送端真正走过的路径。维特比算法能够保证的,是在指定模型、状态约束与目标函数下,找到最优候选。它不能保证:无论噪声有多严重,原始消息都能正确恢复。这与前两章的汉明码、Reed–Solomon码完全一致。最优译码算法不等于无限纠错能力。

第九幕:1973年,福尼让一项“看起来无法实用的算法”成为经典

现在,我们已经知道维特比算法怎样工作。但如果只把历史写成:“1967年,维特比发表论文,立即发明了今天所有人都熟悉的最优网格图译码器。”就会错过一段重要的后续发展。事实上,这项算法最初的意义、计算成本和最优性解释,都经过了进一步研究。其中一位关键人物是:G·戴维·福尼(G. David Forney Jr.)。

最初,它被用来研究卷积码的错误界

维特比1967年原始论文的题目,并不是简单的:The Viterbi Algorithm。而是:Error Bounds for Convolutional Codes and an Asymptotically Optimum Decoding Algorithm。这意味着,研究的主要背景还包括:分析卷积码的错误概率。推导与编码约束长度有关的理论界。以及研究译码算法的渐近行为。在后来的个人回忆中,维特比承认,自己当时并没有认为这项方法已经具有非常显著的实际价值。因为按当时的估计,它可能需要大量寄存器和存储资源。而1960年代的计算机硬件,很难像今天一样轻易处理复杂的状态递推。[Engineering and Technology History Wiki][CiteSeerX]这不是一句“当年所有人都缺乏远见”的简单故事。它反映了数学理论与工程设备之间的真实差距。一种算法可能拥有十分优美的递推结构。但如果需要保存太多历史路径,或者无法满足实时处理速度,那么它仍然很难立即变成产品。

1967年:福尼提出清晰的网格图表述

福尼在1967年前后的研究中,发展了使用网格图描述卷积编码路径的方式。原本看起来会无限分叉的编码树,经过状态合并,可以表示为一种具有固定数量状态、随时间向前展开的结构。这让维特比算法的工作机制变得更加直观。每个时间层包含有限数量的状态。合法分支将相邻时间层连接起来。多条历史路径到达相同状态后,只保留最佳候选。然后不断向前推进。这便是我们今天最熟悉的Trellis解释方式。

1973年:一篇影响深远的教程论文

1973年3月,福尼在《Proceedings of the IEEE》发表:The Viterbi Algorithm《维特比算法》。第61卷第3期,第268—278页。它是一篇系统性的教程与综述论文。福尼明确将维特比算法解释为:对受到无记忆噪声观测的有限状态马尔可夫过程,估计最优状态序列的一种递推最优方法。他还总结了该算法在数字通信和其他状态估计问题中的应用前景。[CiNii Research][forohistorico.coit.es]这一步的意义很大。因为维特比算法不再只是一个与特定卷积码错误界紧密联系的数学工具。研究人员可以将它理解为一种更加一般的状态路径优化方法。只要我们拥有:有限状态。状态之间的合法转移。能够分解为局部项的路径分数。以及一段观测序列。就可以考虑使用类似的动态规划递推。

从“渐近最优”到“最大似然序列译码”

还有一个容易被误解的历史细节。1967年原始论文题目中的:Asymptotically Optimum是其当时分析和表述的理论背景。后来,研究者认识到,在适当的卷积码模型和完整的网格图最优路径计算下,维特比算法实际上能够执行精确的最大似然序列译码(Maximum-Likelihood Sequence Decoding)。也就是说:对于一个定义明确的模型,它不是仅仅得到一个“接近最佳”的路径。而是得到该模型下,整体观测似然最大的合法路径。2024年发表的一篇航天遥测编码史回顾,也明确区分了1967年算法的最初认识,以及福尼在1973年对网格图和最大似然性质的系统阐述。[PMC]但仍然需要强调:最大似然序列译码,并不意味着一定恢复真实原始消息。如果噪声足够严重,最有可能的候选路径也可能不是实际发送路径。这是统计推断的性质,而不是维特比算法的程序错误。

一位工程师在1968年前后发现了实际实现的希望

维特比在1999年的口述史中,还提到了JPL研究人员 Jerry Heller。他回忆,Heller通过模拟发现,实际译码实现可能不需要当初想象的数千个寄存器。在某些设计里,使用32或64个寄存器的存储结构就可能取得有意义的性能。这项发现帮助人们重新认识了算法的实用潜力。[Engineering and Technology History Wiki]这里不能将32或64简单地理解为:维特比算法永远只需要32个或者64个状态。真正需要保存的状态数量,取决于卷积码的约束长度。而幸存路径存储和回溯深度,又是另一项工程参数。这几个数字不能混为一谈。

从理论论文到可用硬件

维特比在后来继续推动算法的实际应用。他与欧文·雅各布斯等人共同创办了Linkabit。该公司发展了数字通信相关的软件与硬件技术。随着半导体技术进步,原本需要大型设备完成的译码计算,逐渐能够由专用数字电路承担。在1999年的口述史中,维特比回忆,Linkabit在1973至1974年前后开始研发用于维特比算法相关信号处理的集成电路。[Engineering and Technology History Wiki]这段历史再一次说明:一项算法的成熟,往往需要不同研究者长期合作。维特比提出核心思想。福尼完善并推广网格图解释。Heller等研究者帮助验证实际实现的可能性。半导体工程师进一步将这些理论转化为可用的电路。因此,维特比算法真正进入现代通信系统,并不是某一篇论文发表后立即完成的事情。而是一条从数学证明、程序模拟到硬件实现的发展道路。

第十幕:Python实战——完整实现卷积编码与维特比译码

现在,我们把本章的核心算法写成一份可以独立运行的Python程序。我们不使用专业通信编码库。也不依赖复杂的硬件仿真工具。只需要Python标准库。程序包含:卷积码编码。状态转移计算。汉明距离分支度量。维特比动态规划。父节点回溯。以及噪声注入与验证。

先把编码器写出来

使用前面约定的:R=12,K=3,(7,5)8R=\frac12,\quad K=3,\quad(7,5)_8每输入一个比特,输出两个编码比特。
python

def encode_convolutional(bits, terminate=True):
    """
    速率1/2、约束长度K=3的卷积编码器。

    生成多项式:(7, 5) 八进制

    状态包含前两个输入比特:
        (m1, m2)

    输出:
        v1 = u XOR m1 XOR m2
        v2 = u XOR m2
    """

    if set(bits) - {"0", "1"}:
        raise ValueError("输入必须是二进制字符串")

    # 尾部添加两个终止位
    source = bits + ("00" if terminate else "")

    m1, m2 = 0, 0
    output = []

    for char in source:
        u = int(char)

        v1 = u ^ m1 ^ m2
        v2 = u ^ m2

        output.append(f"{v1}{v2}")

        m1, m2 = u, m1

    return output


message = "10110"

encoded = encode_convolutional(message)

print("原始消息:", message)
print("编码序列:", " ".join(encoded))

assert encoded == [
    "11", "10", "00", "01",
    "01", "11", "00",
]
预期输出:
text
原始消息: 10110
编码序列: 11 10 00 01 01 11 00

再建立编码器的状态转移函数

接下来,我们需要知道:从任意状态出发,输入0或1,会生成什么输出?以及抵达什么新状态?
python

STATES = ("00", "01", "10", "11")


def transition(state, input_bit):
    """
    state: 两字符状态,例如"10"
    input_bit: 整数0或1

    返回:
        (next_state, encoded_pair)
    """

    m1 = int(state[0])
    m2 = int(state[1])

    u = input_bit

    v1 = u ^ m1 ^ m2
    v2 = u ^ m2

    next_state = f"{u}{m1}"
    output_pair = f"{v1}{v2}"

    return next_state, output_pair


for state in STATES:
    print(f"\n当前状态: {state}")

    for bit in (0, 1):
        nxt, out = transition(state, bit)

        print(
            f"输入{bit} -> 输出{out} -> 新状态{nxt}"
        )
这段程序能够生成第四幕中的完整状态转移表。请注意,它没有使用原始消息内容。它只根据当前状态和候选输入,计算下一步可能发生什么。这正是接收端进行路径推断时所需要的结构。

然后,实现真正的维特比动态规划

我们将保存两类主要信息。metrics:当前各状态的最佳累计代价。backpointers:每一轮每个状态对应的最佳前驱。每次处理一组收到的编码比特,都比较全部合法状态转移。最后,根据已知终止状态进行回溯。
python

def hamming_distance(a, b):
    if len(a) != len(b):
        raise ValueError("长度不一致")

    return sum(x != y for x, y in zip(a, b))


def viterbi_decode(received, terminated=True):
    """
    维特比硬判决译码。

    参数:
        received: 编码符号对列表,例如
            ["11", "10", "01", ...]
        terminated:
            True  -> 约定终止状态为00
            False -> 从最终可达状态中选最佳者

    返回:
        解码输入序列、最小累计距离、最佳路径状态序列
    """

    INF = float("inf")

    metrics = {
        state: INF
        for state in STATES
    }

    # 编码器起始状态固定为00
    metrics["00"] = 0

    backpointers = []

    for observed in received:
        if len(observed) != 2:
            raise ValueError("每组观测必须为两个比特")

        if set(observed) - {"0", "1"}:
            raise ValueError("观测必须为二进制")

        next_metrics = {
            state: INF
            for state in STATES
        }

        parents = {}

        for state in STATES:
            if metrics[state] == INF:
                continue

            for input_bit in (0, 1):
                next_state, expected = transition(
                    state, input_bit
                )

                branch_cost = hamming_distance(
                    expected, observed
                )

                candidate = (
                    metrics[state] + branch_cost
                )

                if candidate < next_metrics[next_state]:
                    next_metrics[next_state] = candidate

                    parents[next_state] = (
                        state, input_bit
                    )

        metrics = next_metrics
        backpointers.append(parents)

    if terminated:
        state = "00"
    else:
        state = min(
            STATES,
            key=lambda s: metrics[s]
        )

    if metrics[state] == INF:
        raise ValueError("没有合法的终止路径")

    best_cost = metrics[state]

    decoded_reversed = []
    states_reversed = [state]

    # 从终点沿最佳前驱逐步回溯
    for t in range(len(received) - 1, -1, -1):
        previous, input_bit = backpointers[t][state]

        decoded_reversed.append(str(input_bit))

        state = previous
        states_reversed.append(state)

    decoded_full = "".join(
        reversed(decoded_reversed)
    )

    state_path = list(
        reversed(states_reversed)
    )

    return {
        "decoded_full": decoded_full,
        "decoded_data": (
            decoded_full[:-2]
            if terminated
            else decoded_full
        ),
        "cost": best_cost,
        "states": state_path,
    }


# 一次比特翻转
received = [
    "11", "10", "01",
    "01", "01", "11", "00",
]

result = viterbi_decode(
    received,
    terminated=True
)

print("接收序列:", " ".join(received))
print("恢复输入:", result["decoded_full"])
print("原始消息:", result["decoded_data"])
print("最小距离:", result["cost"])
print("状态路径:", " -> ".join(result["states"]))

assert result["decoded_data"] == "10110"
assert result["cost"] == 1
预期输出:
text
接收序列: 11 10 01 01 01 11 00
恢复输入: 1011000
原始消息: 10110
最小距离: 1
状态路径: 00 -> 10 -> 01 -> 10 -> 11 -> 01 -> 00 -> 00
这是整章最重要的程序。它通过遍历每一轮的有限状态转移,找到了整段序列的最优路径。没有枚举全部:27=1282^7=128种输入。而是利用状态合并,在每一轮只保存四个状态的最佳代价和前驱。如果序列增加到100位,它仍然不需要列出全部:21002^{100}种候选。

用暴力枚举验证它真的找到最优路径

我们还可以编写一份只适合短序列的暴力求解器。它枚举全部可能的原始输入。重新编码。计算与接收序列的总汉明距离。然后与维特比算法的结果进行比较。这样,我们就能通过另一种完全不同的计算方法,对程序进行交叉验证。
python

from itertools import product


def brute_force_decode(received):
    """
    只用于短消息验证。

    假设最后两个输入为固定的00终止比特。
    """

    data_length = len(received) - 2

    best_message = None
    best_cost = float("inf")

    for bits in product("01", repeat=data_length):
        message = "".join(bits)

        expected = encode_convolutional(
            message,
            terminate=True
        )

        cost = sum(
            hamming_distance(a, b)
            for a, b in zip(expected, received)
        )

        if cost < best_cost:
            best_cost = cost
            best_message = message

    return best_message, best_cost


received = [
    "11", "10", "01",
    "01", "01", "11", "00",
]

dp = viterbi_decode(received)
bf_message, bf_cost = brute_force_decode(received)

print("维特比:", dp["decoded_data"], dp["cost"])
print("暴力枚举:", bf_message, bf_cost)

assert dp["cost"] == bf_cost
assert dp["decoded_data"] == bf_message

print("两种方法结果一致")
需要注意,如果存在多条代价相同的最优路径,不同算法可能采用不同的平局规则。此时,两个程序的消息输出不一定完全相同。但最小代价仍然应该一致。本例使用的接收序列没有这种影响,因此可以直接比较消息内容。

算法复杂度:为什么它比暴力枚举重要?

设:TT为消息时间步数。SS为编码器状态数量。每个状态有:BB条候选输出分支。那么,动态规划每轮最多检查:SBS B条转移。总计算量约为:O(TSB)\boxed{O(TSB)}对于我们这个二进制输入编码器:B=2B=2因此:O(TS)O(TS)而:S=2K−1S=2^{K-1}所以:O(T2K−1)\boxed{O(T2^{K-1})}如果约束长度:KK固定,那么算法复杂度随序列长度:TT线性增长。这与枚举全部输入序列的:O(2T)O(2^T)存在根本区别。但是,这里还包含一项很重要的限定:维特比算法并没有消除编码器记忆长度带来的指数增长。因为状态数量:2K−12^{K-1}仍然随着约束长度增加而指数增长。因此,如果编码器拥有很多记忆单元,维特比译码器仍然可能需要大量计算和存储。真正的突破是:当状态数有限且适中时,算法不会因为消息本身变长,就必须指数式枚举全部历史。

第十一幕:如果真实状态看不见,维特比算法还能找到什么?

到目前为止,我们主要研究卷积码。它的内部状态由移位寄存器中的比特决定。状态之间的转移规则也是确定的。但是,维特比算法的思想,并不要求状态一定来自电子电路。假设我们研究一台设备。它可能处于两种内部工作状态:稳定。受到干扰。但我们无法直接观察它的全部内部状态。我们只能得到一些外部测量结果。例如:信号清晰。信号模糊。信号清晰。信号清晰。信号模糊。现在,我们想判断:这五次观测背后,设备最可能经历了怎样的内部状态变化?这就进入了另一个重要领域。隐马尔可夫模型(Hidden Markov Model,HMM)。

为什么叫作“隐藏”?

因为真正的内部状态:qtq_t无法被直接观测。我们只能观察某种由状态产生的外部结果:oto_t例如:设备处于稳定状态时,更容易产生清晰信号。处于干扰状态时,更容易产生模糊信号。但两者之间不是绝对关系。稳定状态偶尔也可能产生模糊信号。干扰状态偶尔也可能得到一段清晰的观测。因此,我们必须使用概率来描述这些关系。

一个完整的两状态模型

假设隐藏状态只有两个:S=稳定S=\text{稳定}I=干扰I=\text{干扰}初始状态概率:P(S)=0.7P(S)=0.7P(I)=0.3P(I)=0.3状态转移概率:A=[0.850.150.250.75]A= \begin{bmatrix} 0.85&0.15\\ 0.25&0.75 \end{bmatrix}其中第一行代表从稳定状态出发。第二行代表从干扰状态出发。所以:P(S→S)=0.85P(S\rightarrow S)=0.85P(S→I)=0.15P(S\rightarrow I)=0.15P(I→S)=0.25P(I\rightarrow S)=0.25P(I→I)=0.75P(I\rightarrow I)=0.75同时,观测概率为:
隐藏状态清晰模糊
稳定0.90.1
干扰0.30.7
现在,我们收到五次观测:清晰,模糊,清晰,清晰,模糊\boxed{ \text{清晰,模糊,清晰,清晰,模糊} }我们希望找到最有可能的完整隐藏状态序列。

HMM中的维特比递推

定义:δt(j)\delta_t(j)表示:所有在第 tt 时刻处于状态 jj、并能产生前 tt 次观测的历史路径中,联合概率最大的那条路径所对应的概率。初始:δ1(j)=πjbj(o1)\delta_1(j)=\pi_jb_j(o_1)其中:πj\pi_j是初始状态概率。bj(ot)b_j(o_t)是状态 jj 产生观测 oto_t 的概率。递推为:δt(j)=bj(ot)max⁡i[δt−1(i)aij]\boxed{ \delta_t(j)= b_j(o_t) \max_i \left[\delta_{t-1}(i)a_{ij}\right] }其中:aija_{ij}是从状态 ii 转移到状态 jj 的概率。为了恢复最优历史,我们还保存:ψt(j)=argmax⁡i[δt−1(i)aij]\boxed{ \psi_t(j)= \underset{i}{\operatorname{argmax}} \left[\delta_{t-1}(i)a_{ij}\right] }这与卷积码译码的结构非常相似。只不过,我们不再最小化比特不一致的数量。而是在最大化整条隐藏状态路径与观测序列的联合概率。

结果可能是什么?

对于上述具体参数,维特比算法得到的一条最优隐藏状态路径是:S,S,S,S,I\boxed{S,S,S,S,I}即:稳定。稳定。稳定。稳定。干扰。这看起来有些反直觉。因为第二次观测已经出现“模糊”。为什么模型没有立刻判断进入干扰状态?原因是:稳定状态有85%的概率保持稳定。如果每次看到一次模糊观测就立即切换内部状态,可能需要付出较低的状态转移概率代价。因此,整体概率最大的状态路径,并不一定在每个时刻都选择最容易解释当前单个观测的状态。它同时考虑:初始状态概率。状态转移概率。以及每个状态生成对应观测的可能性。

一个容易忽略的区别:整条最可能路径,不等于每一时刻最可能的状态

对于同一个例子,如果我们采用另一种方法:对每一个时刻,分别计算其隐藏状态的后验边缘概率。然后独立选择概率最大的状态。可能得到:S,I,S,S,IS,I,S,S,I它与维特比的:S,S,S,S,IS,S,S,S,I在第二个时刻不同。为什么?因为逐时刻后验最大化,与整条路径联合概率最大化,是两种不同的优化目标。维特比算法寻找:arg⁡max⁡q1:TP(q1:T∣o1:T)\boxed{ \arg\max_{q_{1:T}} P(q_{1:T}\mid o_{1:T}) }而逐时刻后验状态估计寻找:arg⁡max⁡qtP(qt∣o1:T)\boxed{ \arg\max_{q_t} P(q_t\mid o_{1:T}) }第一个考虑整条路径的联合概率。第二个分别考虑每一个时刻的边缘概率。两者可能不同。更极端的情况下,如果某些状态转移概率为零,逐时刻分别选择出的状态序列甚至可能不是一条合法的状态路径。这项区别,是理解维特比与前向—后向算法之间关系的重要基础。

1989年,一篇经典教程系统解释了它们

1989年2月,劳伦斯·拉比纳(Lawrence R. Rabiner)发表:A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition《隐马尔可夫模型及其语音识别应用教程》。论文系统介绍了HMM的三个基本问题:计算给定观测序列的概率。寻找最优隐藏状态序列。以及根据数据估计模型参数。其中,寻找最佳隐藏状态序列,正是维特比算法的典型应用。[数字对象标识符]这项研究让我们看到:从1967年的卷积码译码,到后来更一般的概率序列推断,维特比算法的数学结构仍然十分稳定。我们不需要重新发明一套完全不同的路径搜索方法。只需要明确:状态是什么?状态转移概率是什么?观测来自哪个概率分布?然后使用相应的最优路径递推。

第十二幕:在语音识别和生命科学中,最难的往往不是一个字符,而是一整条序列

现在,设想一段语音。某个人说了一句话。麦克风记录下随时间变化的声波。我们可以将声音转换为一系列声学特征。但计算机真正希望知道的,可能是:说话者依次发出了哪些语音单位?又说了哪些词语?这些语音单位和词语,无法直接从每一小段声波中独立、准确地确定。因为同一个音素的声学表现,可能随着说话者、语速、上下文和噪声而变化。而相邻音素之间,也存在一定的序列结构。这与我们刚才研究的HMM问题十分相似。

隐藏状态与声学观测

在传统HMM语音识别系统中,可以将某些音素或更细的语音状态表示为隐藏状态。声学特征序列则作为观测。模型根据:状态之间的转移概率。以及各状态产生对应声学特征的概率。寻找与整段语音最一致的隐藏状态路径。维特比算法可以在这种状态网络中,寻找最有可能的合法状态序列。进一步结合词典与语言模型,还可以对候选词语序列进行搜索。但需要注意:传统语音识别里的维特比搜索,不等于现代所有语音识别系统都必须使用HMM。当代语音识别技术还包括CTC、注意力模型、Transducer以及多种神经网络结构。它们的概率建模与解码策略可能明显不同。维特比算法仍然具有广泛的理论和工程价值,但不能将其描述成所有现代语音识别系统不可替代的单一核心。

更早的历史:这项动态规划思想不只被一个领域独立发现

在语音识别的发展史中,研究者们还发现了一个非常有趣的现象。维特比算法所依赖的序列动态规划结构,在多个学科中曾被独立提出或重新发现。例如:1968年,Vintsyuk在语音识别领域研究过相关动态规划方法。1970年,Needleman与Wunsch在生物序列比对方面发展了动态规划算法。此后,Sakoe、Chiba、Sankoff、Wagner与Fischer等研究者,也在不同领域研究过具有相似结构的序列最优化方法。Jurafsky与Martin的《Speech and Language Processing》中特别指出,这项方法具有多次独立发现的历史。[Stanford University]但不能因此简单地说:“这些研究者都在完全独立地发明相同版本的维特比算法。”它们的具体问题、模型、目标函数以及历史发展路线并不完全相同。共同点是:当大量候选历史共享相同的中间状态时,可以通过动态规划,只保留对目标函数有用的最佳前缀信息。

生物序列也可以拥有隐藏结构

现在,再想象一段DNA序列。我们已经获得:A、C、G、T。这些核苷酸符号组成的长序列。但某些生物学结构无法直接通过观察单个字符判断。例如:某一段区域可能具有特定的功能状态。某些序列结构可能对应不同的统计规律。如果我们建立一个HMM,让不同隐藏状态产生不同的核苷酸分布,就可以尝试从观测到的DNA序列中推断最可能的状态路径。维特比算法提供了一种最优路径计算方式。在适当的模型中,它可以用于序列注释、基因结构预测和其他概率序列分析任务。不过,这只是数学工具层面的应用描述。不能据此宣称:维特比算法单独就能够准确识别所有基因。或者理解DNA的全部生物功能。真正的分析效果,还取决于:模型结构。训练数据。生物学假设。以及实验验证。

一项算法,为什么能够从电子电路走到语言和DNA?

因为它真正解决的问题,并不是某种特定的物理介质。它解决的是:在一个具有有限状态依赖结构的序列模型中,如何高效找到得分最高的一条完整状态路径。只要问题具有这种结构,就可以考虑使用维特比式动态规划。这让它成为一项跨学科的经典算法。它的应用范围,并不受最初卷积码研究背景的限制。

第十三幕:第二份Python实验——从观测中寻找最可能的隐藏状态

前面,我们已经实现了卷积码的维特比译码。现在,使用同样的动态规划思想,实现一个隐藏马尔可夫模型中的维特比算法。我们继续使用两种状态:稳定。干扰。以及两种观测:清晰。模糊。

模型参数

初始状态概率:π=[0.70.3]\pi= \begin{bmatrix} 0.7\\ 0.3 \end{bmatrix}转移矩阵:A=[0.850.150.250.75]A= \begin{bmatrix} 0.85&0.15\\ 0.25&0.75 \end{bmatrix}观测概率矩阵:B=[0.90.10.30.7]B= \begin{bmatrix} 0.9&0.1\\ 0.3&0.7 \end{bmatrix}收到的观测:
text
清晰 模糊 清晰 清晰 模糊
这一次,我们使用概率乘法,而不是汉明距离。为了避免长序列连续乘积出现数值下溢,程序会对概率取自然对数。于是:概率连乘。变成对数概率相加。

完整Python程序

python

import math


STATES = ("稳定", "干扰")

# 初始状态概率
START_PROB = {
    "稳定": 0.7,
    "干扰": 0.3,
}

# 状态转移概率
TRANSITION = {
    "稳定": {
        "稳定": 0.85,
        "干扰": 0.15,
    },
    "干扰": {
        "稳定": 0.25,
        "干扰": 0.75,
    },
}

# 观测概率
EMISSION = {
    "稳定": {
        "清晰": 0.9,
        "模糊": 0.1,
    },
    "干扰": {
        "清晰": 0.3,
        "模糊": 0.7,
    },
}


def viterbi_hmm(observations):
    """
    HMM最大联合概率状态路径推断。

    使用对数概率,避免长序列下溢。
    """

    if not observations:
        return []

    # 第一个时刻的最佳路径分数
    scores = {
        state: (
            math.log(START_PROB[state])
            + math.log(EMISSION[state][observations[0]])
        )
        for state in STATES
    }

    backpointers = []

    # 从第二个观测开始递推
    for observation in observations[1:]:
        next_scores = {}
        parent = {}

        for current in STATES:
            candidates = {}

            for previous in STATES:
                candidates[previous] = (
                    scores[previous]
                    + math.log(
                        TRANSITION[previous][current]
                    )
                    + math.log(
                        EMISSION[current][observation]
                    )
                )

            best_previous = max(
                candidates,
                key=candidates.get
            )

            next_scores[current] = (
                candidates[best_previous]
            )

            parent[current] = best_previous

        scores = next_scores
        backpointers.append(parent)

    # 选择最终最可能状态
    final_state = max(
        scores,
        key=scores.get
    )

    # 从末尾逐步回溯
    path = [final_state]

    for parent in reversed(backpointers):
        final_state = parent[final_state]
        path.append(final_state)

    path.reverse()

    return path


observations = [
    "清晰",
    "模糊",
    "清晰",
    "清晰",
    "模糊",
]

best_path = viterbi_hmm(observations)

print("观测序列:", " → ".join(observations))
print("最优状态:", " → ".join(best_path))

assert best_path == [
    "稳定",
    "稳定",
    "稳定",
    "稳定",
    "干扰",
]
预期输出:
text
观测序列: 清晰 → 模糊 → 清晰 → 清晰 → 模糊
最优状态: 稳定 → 稳定 → 稳定 → 稳定 → 干扰

为什么这份程序与卷积码译码如此相似?

我们比较两段代码的核心部分。卷积码:
python
candidate = metrics[state] + branch_cost
选出最小累计代价。HMM:
python
candidate = previous_score + log_probability
选出最大累计对数概率。它们共同的结构是:当前最佳分数=Best⁡(前驱最佳分数+本轮局部分数)\boxed{ \text{当前最佳分数} = \operatorname{Best} \left( \text{前驱最佳分数} + \text{本轮局部分数} \right) }其中:Best 可以代表 min。也可以代表 max。关键在于:整个路径的分数,能够由一系列局部分数组合得到。而当前有限状态,足以决定未来允许怎样继续。这也是维特比算法可以跨越通信、语音和其他序列模型的根本原因。

如果观测概率为零怎么办?

我们当前的模型中,所有使用到的概率都大于零。所以可以直接计算:
python
math.log(p)
但如果某些合法转移或观测概率为零,那么:log⁡0\log0在实数范围内没有有限值。实际程序一般使用:−∞-\infty表示不可能发生的转移。例如:
python
def log_probability(p):
    return math.log(p) if p > 0 else float("-inf")
这不是一个无关紧要的程序细节。如果某种状态转移根本不合法,就不应该因为数值计算错误而重新进入最佳路径。

如果序列特别长怎么办?

对于很长的观测序列,直接连乘许多小于1的概率,容易得到一个极其接近零的数字。在浮点计算中,可能发生下溢。因此,使用对数域是一种常见的实现方式。因为:log⁡(ab)=log⁡a+log⁡b\log(ab)=\log a+\log b而:arg⁡max⁡P=arg⁡max⁡log⁡P\arg\max P = \arg\max\log P只要对数计算正确,最优路径不会因此发生改变。这是数学变换直接改善数值稳定性的一项经典例子。

有一个容易误用的地方

维特比算法在HMM中计算的是:给定观测下的最佳状态序列。它并不直接负责训练:初始状态概率。状态转移概率。观测概率。这些模型参数通常需要根据数据另外估计。例如,传统HMM中经常使用Baum–Welch等方法进行参数学习。所以不能把:维特比译码。前向—后向概率计算。Baum–Welch训练。看作完全相同的算法。它们可能使用相似的动态规划结构,却解决不同问题。

第十四幕:1977年,维特比算法真正走向深空

现在,我们回到维特比算法最初的应用方向。通信。尤其是远距离无线电通信。1977年,美国国家航空航天局发射了旅行者1号和旅行者2号探测器。这两艘探测器将穿越巨大的行星际距离。把来自遥远空间的科学观测数据,发送回地球。但遥远空间通信面临一项无法回避的困难。随着距离增大,地面接收端收到的无线电信号会越来越微弱。噪声则继续存在。因此,怎样在有限发射功率、有限通信带宽和长距离条件下可靠恢复数据,是航天通信系统的核心问题之一。

旅行者任务采用了卷积码与维特比译码

根据NASA喷气推进实验室相关技术历史资料,旅行者任务采用过速率:R=12R=\frac12约束长度:K=7K=7的卷积码,并在地面使用维特比译码技术。这比本章教学使用的:K=3K=3更加复杂。我们的小型编码器只有:23−1=42^{3-1}=4个状态。而:K=7K=7的同类二进制卷积编码器,状态数为:27−1=642^{7-1}=64因此,实际译码器需要维护的状态数量明显增加。但它仍然远小于枚举整段消息全部可能输入序列所需要的候选数量。NASA的深空通信资料还记录,相关最大似然卷积译码器能够在不同数据速率下运行,为后续任务提供重要的遥测恢复能力。[NASA][NASA 技术报告服务器]

64个状态意味着什么?

假设探测器连续传输10000个原始比特。暴力枚举全部可能输入:2100002^{10000}显然不可行。但如果编码器只有64个状态,那么维特比译码器每个时刻只需要处理有限数量的合法转移。在一输入位、两分支的简单结构中,每轮最多评估:64×2=12864\times2=128条候选分支。对于10000个时间步,总分支计算数量约为:1 280 0001\,280\,000这是一个数量级完全不同的问题。当然,真实译码器还有:软信息读取。状态度量更新。幸存路径管理。硬件流水线。缓存。以及其他开销。但最核心的复杂度优势,已经非常清楚。

为什么不直接使用上一章的Reed–Solomon码?

这不是一个“哪一种编码更高级”的问题。Reed–Solomon码与卷积码具有不同的结构与优势。在实际通信系统中,它们还可以组合使用。例如,某些深空通信方案使用卷积码处理内层传输错误,再通过外层Reed–Solomon码增强可靠性。NASA的编码研究历史就明确记录了这样的级联编码结构,在旅行者之后的多项深空任务中得到应用。[NASA 技术报告服务器]这意味着:第48章和第49章,并不是两种互相竞争、只能选择其一的技术。它们可以在同一套系统中承担不同职责。

一段跨越多章的算法接力

图49-8:一条深空数据链路中的不同算法角色
1
原始科学数据来自探测器的测量与科学仪器

2
外层纠错编码例如Reed–Solomon码,增加符号级恢复冗余

3
内层卷积编码根据有限记忆状态生成连续编码比特

受到噪声干扰的无线电信道信号传播和接收过程可能导致错误

5
维特比译码在卷积码网格图中恢复最可能的输入序列

6
外层译码与验证进一步修复或检测残留数据错误
这是某类级联纠错通信系统的概念结构,并非所有旅行者数据链路在所有任务阶段的精确配置。具体采用的外层码、内层码和处理方式随任务及时间变化。

真正重要的,不只是节省计算时间

在深空通信中,重新发送一份数据可能非常昂贵。信号传播存在巨大时延。探测器发射功率受到严格限制。地面天线的接收能力也不是无限的。因此,能够利用数学编码与高效译码,在很弱的信号中尽可能可靠地恢复信息,具有极其重要的意义。从1966年的课堂,到1977年的深空任务,维特比算法开始真正走出理论论文。它不再只是一项解释卷积码性能的数学工具。而成为人类远距离通信基础设施的一部分。

第十五幕:算法会寻找最优路径,但它并不知道“真相”一定是什么

现在,我们已经看到维特比算法在多个领域中的应用。但在结束本章之前,需要讨论它的边界。尤其是一个容易让人误解的词:最优。维特比算法能够找到最优路径。但这句话究竟意味着什么?

第一项局限:模型错了,最优答案也可能错

假设真实信道具有明显的突发错误。但我们却将它建模为每个比特独立翻转的二元对称信道。那么,程序仍然能够运行。它也仍然能够找出该错误模型下,累计汉明距离最小的合法路径。但这条路径,并不一定是真实信道统计规律下最可能的原始消息。因为我们的代价函数可能没有准确反映真实噪声机制。因此:模型内最优≠现实中必然正确\boxed{ \text{模型内最优} \ne \text{现实中必然正确} }这项区别,对通信、语音识别和HMM都成立。

第二项局限:状态数量仍然可能非常大

对于约束长度:KK的二进制卷积码,常见状态数量为:S=2K−1S=2^{K-1}所以,虽然算法对时间步数 TT 的复杂度可以是线性的,但对约束长度K仍然可能是指数级的。例如:K=3⇒S=4K=3\Rightarrow S=4K=7⇒S=64K=7\Rightarrow S=64K=15⇒S=16384K=15\Rightarrow S=16384这意味着:不能无限增加编码器记忆长度,然后期望维特比算法始终拥有很低的计算成本。历史上许多高约束长度编码方案,都需要采用不同的译码技术或特殊硬件设计。

第三项局限:保存幸存路径也需要内存

我们的Python程序保存了每个时刻、每个状态的最佳前驱。如果消息长度为:TT状态数为:SS那么,直接存储全部前驱关系通常需要:O(TS)O(TS)的空间。但实际通信接收器可能需要连续处理很长的数据流。不能每次都无限制地保存全部历史。因此,工程实现会使用:有限回溯深度。幸存路径寄存器。滑动窗口。或者其他内存优化策略。这通常意味着,在延迟、存储与译码性能之间进行权衡。对于我们本章使用的已知长度、已知初始状态与已知终止状态的有限消息,完整回溯是清楚而准确的。但对无限持续的数据流,通常需要采用更加适合实时运行的策略。

第四项局限:硬判决与软判决并不相同

我们刚才的Python实现,将每个接收信号直接解释成:0。或者1。这种做法叫作:硬判决(Hard Decision)。但真实无线电接收器,可能测量到的是连续幅度。例如:一个信号看起来非常接近1。另一个信号介于0和1之间,明显更加不确定。如果我们直接把它们都转换成1,就会丢失这份置信度信息。因此,在合适的通信模型下,可以使用:软判决(Soft Decision)。例如,对于高斯噪声下的BPSK信号,可以根据接收值与候选发送信号的距离,构造分支度量。在常见的独立同方差高斯模型中,最小化某些欧几里得距离平方之和,与最大似然译码具有对应关系。因此,维特比算法并不必须使用汉明距离。它可以使用与实际信道概率相匹配的分支分数。但我们仍然需要明确:软判决能保留更多接收信息,不代表它对任何错误模型都必然拥有同样的收益。具体效果取决于信道模型、量化精度和实际实现。

第五项局限:最佳路径与全部不确定性并不是同一份答案

维特比算法会输出一条最佳路径。但有时,我们不仅希望知道:哪条路径概率最大?还希望知道:第二条路径概率是多少?不同时间点的状态概率各是多少?模型对某个位置是否十分不确定?这些问题,可能需要使用:前向算法。前向—后向算法。后验概率推断。或者其他概率计算方法。维特比算法可以告诉我们最佳候选。但如果研究目标是保留整个概率分布,只输出一条最优路径显然还不够。
图49-9:四种常见序列推断目标
问题典型方法
哪条完整状态路径最可能?Viterbi
这段观测总体有多可能?Forward
每一时刻处于各状态的概率?Forward–Backward
怎样根据数据学习HMM参数?Baum–Welch等方法
这些方法之间并不是谁简单替代谁。而是因为研究目标不同,需要保存和计算的信息也不同。

维特比算法真正保证的是什么?

在适当的有限状态序列模型中,如果:初始条件明确。合法转移准确。路径分数能够按局部项分解。以及我们没有采用额外破坏最优性的近似剪枝。那么,维特比算法能够计算:该模型与目标函数下的最优完整路径\boxed{\text{该模型与目标函数下的最优完整路径}}这就是它最重要的数学保证。但从这一结论,不能直接推出:它能够纠正任意数量的通信错误。它能够完整理解一句话的语义。它能够自动推断一段DNA的真实生物功能。或者它在任何硬件上都是最快的算法。这些问题还需要额外的理论与实验证据。

尾声:最好的路径,并不需要保存所有走过的弯路

让我们再次回到1966年。洛杉矶。一位年轻的教授正在准备通信理论课程。他面对的是卷积码。一种拥有记忆的编码。它让当前的输出,受到之前输入的影响。这项性质能够为通信增加冗余。但也让接收端必须在大量可能的输入历史中,寻找真正发生过的那一条。如果原始消息只有五位,那么全部候选还比较容易列举。如果有一百位:21002^{100}种可能。如果有一千位:210002^{1000}种可能。显然,我们不能把全部候选都逐一重新编码,再寻找最可能的结果。而维特比注意到了另一种结构。编码器真正保存的过去,可能只有有限几个记忆位。因此,大量不同的历史路径,最终会汇合到相同的内部状态。如果两条历史到达同一个状态,其中一条拥有更高的累计代价,那么在相同的未来选择下,它永远不可能击败更好的那条路径。所以,我们没有必要继续保存它。只需要保留:抵达每个状态的最佳历史。然后不断向前递推。最终再通过保存的前驱关系,恢复整条最优路径。这项思想最终可以归结为一行公式:Dt(s)=min⁡s′→s[Dt−1(s′)+ct(s′,s)]\boxed{ D_t(s)= \min_{s'\rightarrow s} \left[D_{t-1}(s')+c_t(s',s)\right] }或者,如果使用最大对数似然分数:Mt(s)=max⁡s′→s[Mt−1(s′)+ℓt(s′,s)]\boxed{ M_t(s)= \max_{s'\rightarrow s} \left[M_{t-1}(s')+\ell_t(s',s)\right] }从形式上看,这是一项非常简洁的动态规划递推。但它改变了一类极其重要问题的计算方式。原本随时间长度指数增长的候选历史,可以被合并成有限状态上的最佳前缀。

从课堂,到卫星

1966年,维特比在教学过程中发展这项算法。1967年,论文正式发表。后来的研究者逐渐明确了它的最大似然性质。福尼发展了网格图解释,并于1973年发表经典教程。Heller等工程研究者,则帮助人们看到了实际实现的可能性。随着集成电路与数字信号处理技术发展,维特比译码器逐渐进入卫星通信、无线通信和其他数字系统。1977年发射的旅行者探测器,也采用了相关卷积编码与译码方案。后来,这项算法又被用于:隐藏马尔可夫模型。语音识别。生物序列分析。以及其他具有有限状态结构的序列推断问题。这不是因为这些问题的物理机制完全相同。而是因为它们共享一项重要的数学结构。过去可能非常漫长。但对于预测未来,当前有限状态可能已经包含了需要的全部相关历史信息。而在每个状态上,我们只需要保留最优前缀。

为什么一项来自通信理论的算法,最终能够跨越许多学科?

因为维特比算法真正解决的,不只是怎样纠正卷积码里的错误。它还回答了一项更加普遍的计算问题。假设我们能够观察一段结果。但产生这段结果的内部过程并不完全可见。我们知道内部状态之间可能怎样转移。也知道每一种状态或转移,可能产生怎样的观测。那么,怎样高效恢复:最有可能发生过的完整历史?这种问题可以出现在许多地方。一段受到噪声污染的无线电信号。一段由隐藏语音单位产生的声学特征。一条具有统计结构的生物序列。甚至一些其他有限状态的时间序列分析任务。当问题的结构符合维特比动态规划的条件时,算法就可以发挥作用。但它始终需要面对一个根本限制:它找到的是模型所定义的最优路径。而不是真实世界无条件保证正确的历史。如果模型错误。如果观测信息不足。如果噪声太严重。或者状态空间设计不合理。那么,即使程序完全正确,输出也可能不符合真实情况。所以,一项优秀算法的力量,不仅来自它能够计算什么。还来自我们清楚知道:它究竟在优化什么。

第四篇的四次接力

《穿越噪声的信息》· 第46—49章回顾
第46章
香农:通信可靠性的数学极限信息熵量化不确定性,信道容量给出在指定模型下可靠传输的理论边界。

第47章
汉明:让计算机定位单比特错误利用重叠校验、伴随式和最小码距,让系统具备明确的单错误纠正能力。

第48章
里德—所罗门:恢复受损的多个符号利用有限域多项式、最小码距和交织,抵抗符号错误、擦除与某些突发损坏。

第49章
维特比:从噪声序列中恢复最可能的路径通过有限状态、局部分数和幸存路径,避免枚举指数数量的历史候选。
这四章,逐渐构成了一条完整的思想路线。香农告诉我们:信息能否可靠传输,以及存在什么理论极限。汉明告诉我们:怎样利用少量冗余,发现并修复特定错误。里德与所罗门告诉我们:怎样用代数结构恢复多个损坏的符号。维特比则告诉我们:当数据的生成过程具有记忆时,怎样在海量候选历史中高效寻找最可能的一条路径。但是,到这里,我们仍然只解决了信息传输问题的一部分。通信不仅会受到噪声影响。它还可能遭遇一种完全不同的困难。太多设备,同时争夺同一条网络。即使每一个数据包本身都能够正确编码,也不代表网络能够顺利传输全部数据。路由器的缓冲区可能被填满。数据包可能被丢弃。发送端可能误以为只是偶然出现了传输失败。然后立即重传。而重传又进一步增加网络负担。最终,一张原本还能工作的网络,可能因为所有人都在拼命发送数据而严重拥堵。这将是第四篇的最后一个问题。

历史档案:维特比算法的诞生与传播

年份事件历史意义
1935年3月9日Andrew J. Viterbi出生于意大利贝加莫后来的通信理论与算法研究者
1939年随家人移居美国逃离法西斯意大利的反犹迫害
1957年加入JPL参与扩频通信与早期航天遥测研究
1958年Explorer 1发射参与相关通信系统工作的科研团队
1962年在南加州大学取得博士学位深入研究数字通信理论
1963年加入UCLA任教教学工作成为算法思想发展的重要背景
1966年3月完成相关论文稿件据1999年本人回忆
1967年4月原始论文正式发表提出后来被称为维特比算法的译码方法
1967年前后Forney发展网格图分析让卷积码路径合并结构更加清晰
1973年Forney发表《The Viterbi Algorithm》系统解释算法及其最大似然状态序列估计意义
1977年Voyager任务发射维特比译码进入重要深空通信应用
1989年Rabiner发表HMM经典教程系统阐述维特比算法在概率序列推断与语音识别中的作用
2008年9月29日维特比获颁美国国家科学奖章表彰其算法及数字通信领域贡献
2017年维特比发表算法五十周年说明再次用状态图与最优路径递推解释算法本质

主要参考文献与一手资料

1967年 · 维特比原始论文Andrew J. Viterbi — Error Bounds for Convolutional Codes and an Asymptotically Optimum Decoding Algorithm《IEEE Transactions on Information Theory》第13卷第2期,第260—269页。用于核查算法最初的卷积码研究背景及论文的准确发表日期和题目。

1999年 · 发明者本人接受IEEE口述史采访IEEE History Center — Oral History: Andrew Viterbi本章最重要的历史依据之一。直接说明算法起源于教学、1966年3月的论文稿件、James Massey作为审稿人的推动,以及Jerry Heller对硬件实现的影响。

1973年 · 福尼的经典教程G. David Forney Jr. — The Viterbi Algorithm《Proceedings of the IEEE》第61卷第3期,第268—278页。系统介绍网格图、有限状态模型、最优序列推断及算法的实际实现。

2017年 · 维特比亲自解释算法Andrew J. Viterbi — The Viterbi Algorithm Demystified发明者本人以最大对数似然、状态路径和最优前驱递推解释算法。适合与本章的动态规划推导对照阅读。

维特比的生平与教育经历MIT News — Inspiring a New Generation of InnovatorsUSC — About Andrew J. Viterbi用于核查早年移民、MIT和JPL经历、UCLA教学、Linkabit及其后来的通信产业贡献。

2024年 · 深空遥测编码历史研究Michael Rice — A History of Channel Coding in Aeronautical Mobile Telemetry and Deep-Space Telemetry发表于《Entropy》。用于核查卷积码、顺序译码、维特比算法和NASA深空通信应用之间的真实技术发展关系。

1989年 · 隐马尔可夫模型经典教程Lawrence R. Rabiner — A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition《Proceedings of the IEEE》第77卷第2期,第257—286页。清楚区分HMM的观测概率计算、最优状态路径和参数学习问题。

美国国家科学基金会 · 维特比奖章资料National Medal of Science — Andrew J. Viterbi官方资料记录了2007年度的奖章表彰及2008年9月29日的颁奖时间,获奖贡献包括维特比算法与CDMA无线通信技术。
**史实与教学重构说明:**本章开篇1966年的教学情景,依据维特比本人1999年口述史和2015年MIT采访组织,具体课堂中的问题与思考过程属于教学性叙事,并非逐字记录。维特比1967年论文所研究的是卷积码及其译码性能,并不是为语音识别或DNA分析专门设计的算法。本章使用的四状态、速率1/2、生成多项式(7,5)的卷积码,是为了讲清维特比动态规划而选用的标准教学模型。互动实验、接收错误位置和HMM观测数据均为现代构造,并非1967年原论文中的真实通信记录。“最优译码”在这里指给定模型、初始终止约束及路径目标函数下的最优候选。它不保证在任意噪声条件下恢复实际原始消息。
下一章预告 · 第50章

《当整个互联网开始堵车:雅各布森与TCP拥塞控制的诞生》

1986年10月,美国加利福尼亚州。互联网仍然远没有今天这样庞大。但一个令人震惊的现象已经出现。某些网络连接原本能够传输每秒数万比特的数据。突然之间,实际吞吐量却急剧下降。数据包被不断丢弃。发送端开始重传。但越是重传,网络越拥堵。最终,一条原本能够工作的网络,几乎陷入瘫痪。在后来发表于1988年的经典论文中,研究者记录了一个惊人的数字。从劳伦斯伯克利实验室到加州大学伯克利分校之间,某条网络连接的实际吞吐量,曾从:32 000 bits/s32\,000\text{ bits/s}下降到:40 bits/s40\text{ bits/s}虽然两个地点相距很近,但网络拥塞使实际有效传输能力下降到了原来的八百分之一左右。 [dr. heap references]研究者范·雅各布森(Van Jacobson)与迈克尔·卡雷尔斯(Michael Karels)开始调查。他们发现,一个重要问题并不是物理链路突然变坏。而是TCP实现对网络拥塞的处理方式,可能进一步加剧拥堵。于是,研究者开始发展新的拥塞控制算法。慢启动。拥塞避免。重传定时器的改进。以及更加合理地控制网络中未确认数据的数量。下一章,我们将追踪1986年的拥塞崩溃与1988年的经典论文:Congestion Avoidance and Control。我们将通过互动实验观察:为什么发送得越快,有时反而传输得越慢?TCP拥塞窗口如何逐步增长?丢包发生时,算法为什么需要后退?以及一项看似简单的“加性增加、乘性减小”原则,怎样帮助许多发送端共享有限的网络资源。前面四章研究怎样让信息穿越噪声;下一章将研究,当每个人都想同时发送信息时,怎样避免整张网络被自己制造的流量压垮。
——《算法的故事》第二季 · 第49章 完