程序设计实践 双语版3.1---马尔可夫链算法

给我看你的流程图而藏起你的表,我将仍然是莫名其妙。如果给我你的表,那么我将不再要你的流程图,因为它们太明显了。—Frederick P. Brooks, Jr., 《人月神话》

以上从Brooks的经典书中摘录的内容想说的是,数据结构设计是程序构造过程的中心环节。一旦数据结构安排好了,算法就像是瓜熟蒂落,编码也比较容易。

这种观点虽然有点过于简单化,但也不是在哄骗人。在前一章里我们考察了各种基本数据结构,它们是许多程序的基本构件。在这一章中,我们将组合这些结构,要完成的工作是设计和实现一个中等规模的程序。我们将说明被处理的问题将如何影响数据结构,从这里还可以看到,一旦数据结构安排好之后,代码将会如何自然地随之而来。

我们的观点的另一个方面是:程序设计语言的选择在整个设计过程中,相对而言,并不是那么重要。我们将抽象地设计这个程序,然后用C、C++、Java、Awk 和Perl把它写出来。由不同实现之间的比较,可以看出语言在这里能有什么帮助或者妨碍,以及它们并不那么重要的各种情况。程序的设计当然可以通过语言来装饰,但是通常不会为语言所左右。

我们要选择的问题并不是很常见的,但它在基本形式上又是非常典型的:一些数据进去,另一些数据出来,其处理过程并不依赖于多少独创性。

我们准备做的就是产生一些随机的可以读的英语文本。如果随便扔出来一些随机字母或随机的词,结果当然是毫无意义的。例如,一个随机选取字母(以及空格,用作词之间的分隔)的程序可能产生:



没人会觉得这有什么意思。如果以字母在英语里出现的频率作为它们的权重,我们可能得到下面这样的内容:



这个也好不到哪儿去。采用从字典里随机选择词的方式也弄不出多少意思来:



为了得到更好一些的结果,我们需要一个带有更多内在结构,例如包含着各短语出现频率的统计模型。但是,我们怎么才能得到这种统计呢?

我们当然可以抓来一大堆英语材料,仔细地研究。但是,实际上有一种更简单也更有意思的方法。这里有一个关键性的认识:用任何一个现成的某种语言的文本,可以构造出由这个文本中的语言使用情况而形成的统计模型。通过该模型生成的随机文本将具有与原文本类似的统计性质。

3.1 马尔可夫链算法

完成这种处理有一种非常漂亮的方法,那就是使用一种称为马尔可夫链算法的技术。我们可以把输入想像成由一些互相重叠的短语构成的序列,而该算法把每个短语分割为两个部分:一部分是由多个词构成的前缀,另一部分是只包含一个词的后缀。马尔可夫链算法能够生成输出短语的序列,其方法是依据(在我们的情况下)原文本的统计性质,随机性地选择跟在前缀后面的特定后缀。采用三个词的短语就能够工作得很好——利用连续两个词构成的前缀来选择作为后缀的一个词:

设置w1和w2为文本的前两个词

输入w1和w2

循环:

随机地选出w3,它是文本中w1w2的后缀中的一个

打印w3

把w1和w2分别换成w2和w3

重复循环为了说明问题,假设我们要基于本章开头的引语里的几个句子生成一些随机文本。这里采用的是两词前缀:



下面是一些输入的词对和跟随它们之后的词:输入前缀跟随的后缀词

处理这个文本的马尔可夫算法将首先打印出Show your,然后随机取出flowcharts或table。如果选中了前者,那么现在前缀就变成your flowcharts,而下一个词应该是and或will。如果它选取tables,下一个词就应该是and。这样继续下去,直到产生出足够多的输出,或者在找后缀时遇到了结束标志。

我们的程序将读入一段英语文本,并利用马尔可夫链算法,基于文本中固定长度的短语的出现频率,产生一段新文本。前缀中词的数目是个参数,上面用的是2。如果将前缀缩短,产生出来的东西将趋向于无聊词语,更加缺乏内聚力;如果加长前缀,则趋向于产生原始输入的逐字拷贝。对于英语文本而言,用两个词的前缀选择第三个是一个很好的折衷方式。看起来它既能重现输入的风味,又能加上程序的古怪润饰。什么是一个词?最明显的回答是字母表字符的一个序列。这里我们更愿意把标点符号也附着在词后,把“words”和“words.”看成是不同的词,这样做将有利于改进闲话生成的质量。加上标点符号,以及(间接的)语法将影响词的选择,虽然这种做法也可能会产生不配对的引语和括号。我们将把“词”定义为任何实际位于空格之间的内容,这对输入语言并没有造成任何限制,但却将标点符号附到了词上。许多语言里都有把文本分割成“空白界定的词”的功能,

这个功能也很容易自己实现。

根据这里所采用的方法,输出中所有的词、所有的两词短语以及所有三个词的短语都必然

是原来输入中出现过的,但是,也会有许多四个词或更多个词的短语将被组合产生出来。下面

几个句子是由我们在本章里将要开发的程序生成的,给它提供的文本是艾尼思特·海明威的《太阳照样升起》的第4章:



我们很幸运,在这里标点符号没出问题。实际中却未必总能这样。

3.2 数据结构的选择

有多少输入需要处理?程序应该运行得多快?要求程序读完一整本书并不是不合理的,因此我们需要准备对付输入规模n=100 000个词甚至更多的情况。输出将包括几百甚至几千个词,而程序的运行应该在若干秒内完成,而不是几分钟。假定输入文本有100 000个词,n已经相当大了,因此,如果还要求程序运行得足够快,这个算法就不会太简单。

马尔可夫算法必须在看到了所有输入之后才能开始产生输出。所以它必须以某种形式存储整个输入。一个可能的方式是读完整个输入,将它存为一个长长的字符串。情况的另一方面也很明显,输入必须被分解成词。如果另用一个指向文本中各词的指针数组,输出的生成将很简单:产生一个词,扫描输入中的词,看看刚输出的前缀有哪些可能的后缀,然后随机选取一个。但是,这个方法意味着生成每个词都需要扫描100 000个输入词。1000个输出就意味着上亿次字符串比较。这样做肯定快不了。

另一种可能性是存储单个的词,给每个词关联一个链表,指出该词在文本中的位置。这样就可以对词进行快速定位。在这里可以使用第2章提出的散列表。但是,这种方式并没有直接触及到马尔可夫算法的需要。在这里最需要的是能够由前缀出发快速地确定对应的后缀。

我们需要有一种数据结构,它能较好地表示前缀以及与之相关联的后缀。程序将分两部分,第一部分是输入,它构造表示短语的整个数据结构;第二部分是随后的输出,它使用这个数据结构,生成随机的输出。这两部分都需要(快速地)查询前缀:输入过程中需要更新与前缀相关的后缀;输出时需要对可能后缀做随机选择。这些分析提醒我们使用一种散列结构,其关键码是前缀,其值是对应于该前缀的所有可能后缀的集合。

为了描述的方便,我们将假定采用二词前缀,在这种情况下,每个输出词都是根据它前面的一对词得到的。前缀中词的个数对设计本身并没有影响,程序应该能对付任意的前缀长度,但给定一个数能使下面的讨论更具体些。我们把一个前缀和它所有可能后缀的集合放在一起,称其为一个状态,这是马尔可夫算法的标准术语。

对于一个特定前缀,我们需要存储所有能跟随它的后缀,以便将来取用。这些后缀是无序的,一次一个地加进去。我们不知道后缀将会有多少,因此,需要一种能容易且高效地增长的数据结构,例如链表或者动态数组。在产生输出的时候,我们要能从关联于特定前缀的后缀集合中随机地选出一个后缀。还有,数据项绝不会被删除。

如果一个短语出现多次,那么又该怎么办?例如,短语“might appear twice”可能在文本里出现两次,而“might appear once”只出现了一次。这个情况有两种可能的表示方式:或者在“might appear”的后缀表里放两个“twice”;或者是只放一个,但还要给它附带一个计数值为2的计数器。我们对用或不用计数器的方式都做过试验。不用计数器的情况处理起来比较简单,因为在加入后缀时不必检查它是否已经存在。试验说明这两种方式在执行时间上的差别是微不足道的。

总结一下:每个状态由一个前缀和一个后缀链表组成。所有这些信息存在一个散列表里,以前缀作为关键码。每个前缀是一个固定大小的词集合。如果一个后缀在给定前缀下的出现多于一次,则每个出现都单独包含在有关链表里。

下面的问题是如何表示词本身。最简单的方法是把它们存储为独立的字符串。在一般文本里总是有许多反复出现的词,如果为单词另外建一个散列表有可能节约存储,因为在这种情况下每个词只需要存储一次。此外,这样做还能加快前缀散列的速度,因为这时每个词都只有一个惟一地址,可以直接比较指针而不是比较词里的各个字符。我们把这种做法留做练习。下面采用每个词都分开存放的方式。

补充阅读:

隐藏在设计模式后面的基本思想是:大部分程序所采用的不过是很少几种不同的设计结构,与此类似,实际上也只有不多的几种基本数据结构。说的远一点,这与我们在第1章讨论过的编码习惯用法也是很相像的。这方面的经典参考文献是Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides的《设计模式:可重用面向对象软件的要素》(Design Pattern: Elements of Reusable Object-Oriented Software,Addison-Wesley,1995)。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值