乐趣区

掌握动态规划助你成为优秀的算法工程师

1. 导论


相信很多同学已经在为今年的校招做准备了,随着 AI 的火热,越来越多的同学涌入了算法的行当之中。那去年校招的算法岗是有多火热?在知乎上看到这么一条帖子,先不说内容哈,足足 400w+ 的阅读量啊。

不光是计算机或软件专业的学生,很多电子,通信,自动化等相关专业的同学也吸引了进来。当然,这应该是件好事。但是相当一部分同学,在学习的过程中,尤其是刚入门的时候,可能会有这样一个疑问:算法工程师的算法,为什么不是指《算法导论》中的算法(以下称为经典算法,用以区分),而是指机器学习里的算法。都叫算法(Algorithm),但好像不是一回事啊,两者有什么关系,又有什么区别呢?

本文试图通过动态规划这一经典算法中的重要内容,同时又在机器学习算法中有着广泛的应用,来简单探讨一下这两种“算法”。

2. 动态规划


首先,动态规划(Dynamic Programming)是《算法导论》中的重要章节,同时也是在机器学习算法中有着非常重要应用的一种优化算法。可以说是,无论是否是算法工程师都应该掌握的一种算法。再功利一点说,动态规划也是诸多面试官特别喜欢考的一种题型,下面就带大家稍微温故一下。

按照教材 [1] 的介绍,动态规划通常需要按如下 4 个步骤来进行设计:

刻画一个最优解的结构特征。

递归地定义最优解的值。

计算最优解的值,通常采用自底向上的方法。

利用计算出信息构造一个最优解。

简单点说,动态规划算法的核心就是大问题拆成重复的小问题,同时记住已经解决了的小问题的解。那如果你去网上搜搞 ACM 或者 OI 大神的说法,他们都会说动态规划是一个框,是一种解决问题的思想,而不是某种具体的算法。下面我们详细来看看,时髦的机器学习是怎么往动态规划这个框里填的。

2.1 编辑距离

说到编辑距离(Edit distance),大家可能都比较熟悉。在自然语言处理(Natural Language Processing,又 NLP)中,编辑距离是计算文本相似度的一种基本距离计算方式。简单来说,就是只能通过替换,删除,插入操作,将一串字符串变为另一串字符串的操作数。而求最小编辑距离的过程,就是一个典型的动态规划的过程。

如果计算两个字符串的编辑距离,我们可以看做是父问题,那他的子问题自然就是如何求更小字符串之间的编辑距离。那上面提到,动态规划不仅要将父问题拆分成子问题,更要记下子问题的解,以达到节省空间和时间的效果。

下图可以看出,这个 D 矩阵,就是我们用来存储这个子问题解的空间,假设两个字符串的长度分别为 m 和 n。而 D(i, j)如图所示,则是我们定义的最优解的结构特征,实际表示的就是长度为 i 和长度为 j 的两个子序列之间的最小编辑距离,我们把它从左至右,从下到上填到每个格子里,一直到矩阵的右上角,就可以得到我们要的最小编辑距离。那么最终我们的空间复杂度为 O(mn),而时间复杂度同样为 O(mn),相比采用分治的思想递归地进行求解还是要快很多。(这里篇幅有限,就不赘述代码了,有兴趣的同学可以自行网上搜索)

https://web.stanford.edu/clas…

2.2 动态时间规整

说完 NLP 里的相似度计算,咱们再来看看语音识别里有没有类似的算法用到动态规划呢?答案是肯定的。这个算法就叫动态时间规整(Dynamic time warping),简称 DTW,一听名字是不是感觉就很动态规划。DTW 算法是传统语音识别中的重要方法,起初是用于孤立词的语音识别,判断两段语音是否为同一个单词。实际上,只要是时间序列,如下图[2],都可以用来计算相似度,不局限于语音识别当中,例如股市的交易策略,手势识别等等场景都有应用。

在语音信号中,有一个很大的问题就是,信号长度并不相等,即使是同一个人说同一个单词,也会有语速上的差别。那这个时候基于欧几里得距离(Euclidean Distance)的方法就不奏效了,但是两个时间序列形状上又非常的相似,于是我们就希望可以通过某种对齐的方式来衡量这两个时间序列的相似性。如上图所示,箭头代表了两个时间序列中的相似点。我们用所有相似点之间的距离和来表示这种相似度,称之为规整路径距离。

大家看到这个“棋盘”好像和我们上面讲到的编辑距离中的 D 矩阵很像。没错,假设 n 为序列 A 的长度,m 为序列 B 的长度,D(m, n)就是上面这两个序列的规则路径距离。至于 D(m, n)怎么求?又到了我们的动态规划发挥威力的时候。还是像求最小编辑距离一样,D(i, j)如下式所求,并且由左到右,由下到上填入 D 矩阵中,最终求得的右上角的值就是我们的规整路径距离。时间复杂度依然为 O(mn)。

说到这里,好像动态规划不就是画个矩阵,按照 D(i, j)的计算方法填满矩阵,得到右上角的最终解。嗯,好像也有点道理。那我们接下来继续看看,这样说对不对。

2.3 维特比算法

如果说前面两种算法和机器学习只能算沾边的话,那我们现在要说的维特比算法(Viterbi Algorithm)可以说是动态规划在机器学习当中的典范了,尤其如果是做自然语言处理方向的话,维特比算法更是不可不知,哪怕在如今 deep learning 当道的时代。在自然语言处理中,像分词、词性标注、命名实体识别、输入法等等任务中都有非常重要的应用。

除了前面介绍的计算两个序列之间的距离以外,动态规划还有一个重要的应用场景,就是计算有向无环图(DAG)中两个节点间的最短路径,而维特比算法就是针对篱笆网络(Lattice Network)这一特殊的有向无环图而提出的。


如上图所示,这是一个部分的篱笆网络,中间我们假设有 N 列,每列有 4 个节点,节点之间的权重我们暂时忽略。这个时候,网络的最左边有一个节点为 S,最右端有一个节点为 E。如果我想求 S 到 E 之间的最短路径,理所当然,我们如果穷举出所有的路径进行比较,也就是 4N 条路径,自然可以得到结果,但如果层数很多或者每层的节点数很多的时候,这种方法就显得不够友好了。

既然穷举法太过暴力,自然我们想试试能不能用动态规划来解决。首先,篱笆网络有这么一个特点,就是假设我们从第一列走到最后一列,我们一定会经过其中的第 i 时刻的某个节点。这个当然是显而易见的,但给我们带来了一个好处,那就是当我们计算最终的最短路径时,假设第 i 列有 k 个节点,如果我们已经计算了从开头到第 i 列所有 k 个节点的最短路径,那最终的最短路径一定是经过其中之一。第二,如果说最短路径 P 经过某个节点 xij,那么从起始节点 S 到节点 xij 的这段子路径 Q,一定是从起始节点 S 到 xij 的最短路径,否则总路径 P 也不再是最短路径,这就自相矛盾了。

有了这两个特性,终于可以试试动态规划了。同样我们从最左边的 S 节点出发,到第 1 列的 4 个节点,因为各只有一段距离,那自然这 4 个距离 d(S, x1i)为 S 节点到这 4 个节点的最短距离。当我们走到第 2 列时,根据之前的特性,一定会经过第 1 列的某个节点。此时的 S 节点到第 2 列某个节点的距离则为 d(S, x2j)=d(S, x1i) + d(x1i, x2j)。而第 1 列有 4 个节点,所以 d(S, x2j)应该是取 4 个距离中的最小值,当然在这一过程中,我们计算了 4 次,对于第 2 列的每个节点,我们都去进行如上的计算。所以在从第 1 列走到第 2 列的过程中,我们计算了 4×4 次,更关键的是我们把 d(S, x2j)都要保存下来,作为我们下一次计算的基础。

而这个保存中间结果的过程,很明显地体现出了前文所述的动态规划的特点。接下来,我们继续走到第 3 列,同样的,S 节点到第 3 列某个节点的距离为 d(S, x3k)=d(S, x2j) + d(x2j, x3k)。这个时候我们发现,等式右边的第一项,可以直接取我们刚刚保存的中间结果。对于 d(S, x3k),我们依然是计算 4 次,取最小值保存下来。同样,需要遍历第 3 列的 4 个节点,所以又是 4×4 次计算。也就是说,每往前走 1 列,我们就计算了 4×4 次。以此类推,到最右边的节点 E 的时候,我们需要计算 N×42 次,相比于穷举法的 4N 条路径,这个效率已经是非常大的进步,把指数级的复杂度降低到了多项式级别!

2.4 CYK 算法

这个 CYK 算法大家可能会有点陌生,全名为 Cocke–Younger–Kasami 算法,是以三位作者的姓名共同命名的。这个算法其实是句法分析方向的一个经典算法,因为提出的时间是在基于规则的年代,所以即使是做 NLP 的同行,依然有很多同学并不了解。所以这里给大家多交代一些背景知识。

首先,CYK 算法是基于乔姆斯基(Chomsky)范式的上下文无关语法(Context Free Grammar)。感觉越解释概念越多了哈。简单点说,乔姆斯基范式有两种形式。

这里,ABC 都是非终结符,就是像名词短语(NP),动词短语(VP)等等,x 是终结符,比如单词就是终结符。对于 A 这个非终结符,要么拆分成更小的 2 个非终结符,要么就到此为止,右边是一个单词。例如:”吃药“这个动词短语,就可以按下面的方式进行句法分析。

好了,介绍完了背景知识,我们的任务就是给定一个句法规则的集合,对于任意一个句子,将它按照这个句法规则集合进行解析。下图就是我们的一个句法解析树,也就是最终的一个结果。

对于一次解析来说,如果尝试去解析出所有的结果,那将是指数级的复杂度,而 CYK 算法就是利用动态规划的思想将复杂度降低到了多项式级。假设我们的句子的单词数为 n,我们先画一个如下图的下三角矩阵,横坐标为位置 i,纵坐标为跨度 j。

CYK 算法过程如下:

http://ccl.pku.edu.cn/doubtfi…

其中最关键的就是步骤 2 中的最内层循环,对于一个跨度内所有分割点 k。简单点说,就是在给定了位置和跨度的一个短语中,不断调整中间的分割点位置,使得分割点两边的短语子串能够符合给定的句法规则。当子串符合句法规则时,把对应的结果记录下来,并为后续的解析所用,这就体现了动态规划思想的核心!

2.5 分词

上面我们介绍过维特比算法,在分词任务中有重要的应用,但现在我们又要来提分词了,那这里的动态规划和维特比有什么不一样呢。首先,目前的分词的主流算法里有这么两种,一种是基于字的模型分词算法,还有一种则是基于词典的分词方法。前者,主要有基于隐马尔可夫(HMM)的生成式模型、基于条件随机场(CRF)的判别式模型以及基于神经网络的分词算法,在这些算法的解码部分,都可以使用维特比算法进行解码。那这节要说的动态规划则是基于词典的分词算法里用到的。

基于词典的分词算法是如何工作的呢?举一个简单的例子,” 我来到达观数据 ”。首先,我们根据词典将句子进行简单的匹配,将句中匹配到的词典词和所有的单字组合起来,作为节点,构造成一个分词的词图,如下图所示(边的权重假设都为 1)。那这个图里从 S 节点到 E 节点的每条路径,都代表这一种分词的结果。我们希望正确的分词结果,理应是一条概率最大的的路径,这样才能把一个语言学的问题转化成一个数学的问题,从而让计算机可以辅助我们得到正确的分词结果。

因为句子是顺序的,所以这个词图表现为一个有向无环图。还记得前面讲到的维特比算法是针对特殊的有向无环图来计算最短路径,这里的词图就显得更为一般些。如何求得有向无环图的最短路径呢,这个时候,我们需要先对词图进行拓扑排序,然后再利用动态规划求排序后的词图最大概率路径。拓扑排序的结果如下图:

有了拓扑排序的结果,我们就可以动态规划了。上面我们讲 Viterbi 算法的时候,提到这么一个性质,如果最终的最短路径 P 经过某个节点 i,那么经过这个节点的子路径 Q 一定是起始节点 S 到节点 i 的最短路径,否则最短路径 P 一定有比它更短的路径存在。在这里,我们是求最大概率路径,其实原理是一样的。对于到最终节点 E 的路径 Route(E),有:

实际上,我们可以一直写下去,而这些子路径就是动态规划中的最优子结构,我们只需要再求解这些子结构中的最优解即可。相比于,列举出所有的分词路径,这种计算方法是要快上很多。在计算最大概率的过程中,有不少小 trick。期待后续的达观技术分享为大家详细介绍。

2.6 强化学习策略迭代

说到强化学习,这里不得不首先引用一下 Sutton 在《Reinforcement Learning: An Introduction》关于动态规划的一些表述。

DP provides an essential foundation for the understanding of the methods presented in the rest of this book. In fact, all of these methods can be viewed as attempts to achieve much the same effect as DP, only with less omputation and without assuming a perfect model of the environment.

虽然动态规划在实际的强化学习中,有诸多的限制,但动态规划所具有的理论基础是强化学习必不可少的,可见动态规划之于强化学习的重要意义!

强化学习和大家熟悉的机器学习有着很大的不同,它没有用到标注数据进行 supervise,而且当前的动作可能会影响到后续的结果。既然是动作,肯定会考虑到很多因素。那么在强化学习里,我们需要考虑哪些变量呢?首先肯定是回报了,这里用 R 来表示,不同的回报决定了不同的动作。回报又分为长期的和短期的,就好比做基础算法研究,短期看没什么回报,但长期来说就是技术的护城河。我们通过衰减系数???? 来表示,0≤????<1。除此以外,我们还要看当前所处的状态 S 来决定所做的动作。有了状态的集合,那自然要考虑状态之间的转移关系,就是我们的 P,状态转移矩阵,但因为对于很多实际问题,状态实在太多,所以每个状态只能考虑前一个状态的相关情况,这样就大大简化了问题的复杂度,这种性质我们也叫马尔科夫性。最后,加上我们的一系列动作 A,就有了马尔科夫决策过程(Markov Decision Process, MDP)。

http://www0.cs.ucl.ac.uk/staf…

好了,现在有了这些基本要素了,下面要用这些要素做什么呢?比如机器学习里,我们希望是最小化损失函数。而在强化学习里,我们则是希望最大化的累积回报,比如下棋的时候,只要最终赢了即可,而不是要每步都要吃掉对方一些棋子。这时候,我们就先定义一下这个累积回报,假设从 t 时刻开始到最终时刻 T,并且加上之前提到的衰减系数????,则有:

既然这个 G~t~ 是从 t 时刻到最终状态的累积回报,那就不光是一条路径的累积,而是所有的路径的总和,当状态过多的时候,我们就没法计算。这个时候需要引入一个状态值函数,作为从某状态开始的回报总和的期望。

这个状态值函数就是用来衡量某一个状态的好坏。除了我们上面提到的几个要素以外,我们在某个状态执行什么动作,并不是固定的,那就需要引入新的概念策略????,作为动作关于状态的分布。则上式就变为:

问题又来了,这个期望其实也不太好算,因为其中一个状态的价值函数需要计算后续所有状态的回报。既然是马尔科夫决策过程,那自然会想到用迭代的思想去计算。得到其递归的形式 Bellman 方程,如下式(详细推导过程见 7)

这样,在给定策略的情况下,我们就可以迭代地去求每一个状态的值函数了。从这个更新公式,也可以看出,强化学习里的动态规划将围绕状态值函数的更新这个最优子结构而做文章。

对于从 v1 到最终的 v 的每次迭代 vk,我们利用上式对值函数进行更新,直到收敛于 V (证明略)。这一步就叫做策略评估。这时候,我们可以对于取得最大的动作值函数 q 而得到的新策略,对新的策略进行策略评估步骤。直到我们的策略不再变化,此时得到的策略即为最优策略。

3. 总结


一下子说了那么多涉及到动态规划的机器学习算法,相信读者朋友们还可以列举出更多(欢迎评论区留言)。我们在动态规划这个框里填了那么多机器学习算法,而机器学习算法也非常依赖动态规划的高效率,可谓是你中有我,我中有你。

从上面举的很多例子大家也可以看到,动态规划更侧重于对问题求解的优化,比如求最短距离,实际上是可以通过暴力方法进行求解的,而动态规划则是提高了求解的效率。但机器学习算法可不仅仅是对问题的求解进行优化,它要解决的问题往往没有精确解,同时也没法通过穷举等暴力方法进行求解。需要对问题进行清晰地定义并建模,重点在于学习的过程,基于对训练数据的拟合来预测新的样本。

而说完动态规划,大家很容易又联想到贪心算法,同样也是经典算法中的一个重要的优化方法。比如 word2vec 中的霍夫曼树(Huffman Tree),比如 seq2seq 中需要用到的集束搜索(Beam Search),都是借鉴了贪心算法的思想。前面我们提到了求最短路径问题虽然是动态规划,这实际上又涉及到经典算法中图论里的内容,类似的还有一系列的概率图模型(Probabilistic Graphical Model)。事实上,还有很多机器学习算法运用到了经典算法里的内容,这里就不一一列举了。

总之,对于一名优秀的算法工程师来说,经典算法的掌握必不可少,对于机器学习算法的理解也有很大的帮助。希望大家能够认真掌握,” 举一反三 ”,在接下来的秋招中好好发挥。

参考资料
1.Cormen, Thomas H., Charles E. Leiserson, and Ronald L. Rivest. “ 算法导论.” 第 2 版 机械工业出版社 (2006).

2.Salvador, Stan, and Philip Chan. “FastDTW: Toward Accurate Dynamic Time Warping in Linear Time and Space.”

3. 吴军. 数学之美. 人民邮电出版社, 2012.

4. 宗成庆. 统计自然语言处理. 清华大学出版社, 2013

5. 黄海安. https://www.zybuluo.com/Team/…

6.Wiering, Marco, and Martijn Van Otterlo. “ 强化学习.” 机械工业出版社.

7.http://www0.cs.ucl.ac.uk/staf…

关于作者


杨慧宇:达观数据 NLP 技术专家,负责达观数据底层 NLP 算法效果的优化,以及财务审核相关产品的研发工作。

退出移动版