SlideShare a Scribd company logo
1 of 34
 




第7 章
算法实用化
——从身边的例子来看理论、研究的实践投入
                                 [课程]伊藤直也




算法和数据结构的选择十分重要
——适合解决问题的算法和数据结构

    第 7 章讲述算法及其应用的概论。大量数据要在可接受的时
间内处理掉,但有时不论等多长时间也计算不完。相反,使用了
适合问题解决的算法和数据结构的话,原本需要一天计算时间的
任务几秒钟就能完成。
    要想写出高速程序,算法和数据结构的选择是非常重要的。
要处理的数据越大,不同选择之间的差距越显著。
    第 7 章有两个目的,一是让大家感受面对大规模数据时算法
选择的重要性,二是让大家了解如何将算法应用到产品中。我们
会以 Hatena 的各种真实服务的功能为例,讲述最适合的素材。
    第 19 课以算法和算法评测为题,从复杂度记法等与大规模
数据这个主题息息相关的基本事项开始,讲述算法应用到实际产
品中的方法。 20 课和第 21 课以第 5 章后半部分简单介绍过的
      第
“Hatena Dairy 的关键字链接”和“Hatena Bookmark 的文章分
类”的实现为基础,具体介绍算法的实际应用的过程和变迁,以
及在部分实际服务中如何灵活运用算法等内容。


    算法的实用化:
     算法和算法评测(→第 19 课);
     Hatena Diary 的关键字链接(→第 20 课);
     Hatena Bookmark 的文章分类(→第 21 课)。
第 7 章 算法实用化 143




第 19 课
算法和算法评测

数据规模和复杂度的差异

   之前反复说过,要处理的数据越大,算法和数据结构的选择
对速度的影响也就越大。首先看个简单的例子。假设要从数据中
使用线性查找(Linear Search),从头开始依次查找所需数据,
那么如果有 1000 条数据,那就需要反复查找数据直至找到为止,
这个算法最多要进行 1000 次查找。对于 n 条数据要进行 n 次搜
索,因此称为 O(n)算法。
   而“二分查找”(binary search)算法能在 log n 次之内查找
n 条数据,是 O(log n)算法。使用二分查找,1000 条数据最多只
需 10 次就能查找完。
   这个“最大查找次数”可以大致判断计算次数,称为复杂度。
一般来说,复杂度越低,算法就越快。
   n=1000 时,O(n)的最大查找次数为 1000,而 O(log n)为 10,
计算次数差距为 990。n 再大些会怎样呢?若是 100 万条数据,
O(n)需要 100 万次,而 O(log n)只需 20 次。即使是 1000 万条,
O(log n)也只需 24 次。很明显,与 O(n)相比,O(log n)更能承受
数据量的增加。
   请以大规模数据为前提思考一下。数据量较小时,即使使用
O(n)这种简单算法,计算量也不会太大,因此没什么太大问题。
但随着数据量的增加,算法选择的差异就越来越大。在数据搜索
处理中,使用线性查找的话,数据量增大到 1000 条、100 万条、
1000 万条时……显然会成为瓶颈。而解决该瓶颈的方法就是选
择复杂度更低的查找算法,这也是不言而喻的。
144 大规模 Web 服务开发技术


   第 7 章的两个目的
           如本章开始时讲的,第 7 章将以两个目的为前提进行讲解。
   第一个目的就是让大家感受面对大规模数据时算法选择的重要
   性。说到处理大规模数据时算法很重要,看起来好像是换个算法
   实现就万事大吉了,实际上没这么简单。
           算法和数据结构的教科书基本上只会讲到算法的实现,而怎
   样将实现应用到系统中,以及怎样运维,却不会讲述。
           我们希望将大学教科书、论文和最新算法应用到产品中。那
   么,怎样才能把算法应用到产品中呢?这就是第 7 章的另一个目
   的。


   何谓算法?

           讲述 Hatena 的服务之前,首先了解一下算法的基本思路吧。
           “算法”是什么?重新来考虑一下。根据《アルゴリズムイ
   ントロダクション 改訂 2 版 第 1 巻 数学的基礎とデータ構造》①



   (近代科学社、2007 年),算法(algorithm)就是明确定义的
   (well-defined)、以某个值或值的集合为输入(input)、以某个
   值或值的集合为输出(output)的计算步骤。
          ——引用自《アルゴリズムイントロダクション 改訂2版
   第1巻 数学的基礎とデータ構造》(Thomas H.Cormen/Charles
   E.Leiserson/Ronald L. Rivest/Clifford Stein 著、浅野哲夫/岩野和生/
   梅尾博司/山下雅史/和田幸一译,近代科学社,2007 年)第 5 页
           输入适当的值,通过明确定义的计算步骤得到输出值,这就
   是算法。将要查找的数据和被查找的数据作为输入进行查找,得
   到要查找的数据所在的位置,这就是“查找算法”。


                                                                
   ① 译者注:原书为 Introduction to Algorithms, Second Edition,Thomas H.
        Cormen、Charles E.Leiserson、Ronald L. Rivest、Clifford Stein 著。中文
        版为《算法导论》,机械工业出版社,2006 年出版。
第 7 章 算法实用化 145


狭义算法和广义算法
  算法这个词具有狭义和广义两种含义,用途很广泛。
  从数据库提取记录进行适当处理后输出成报表,对于这种平
时随手就能写出的程序,也可以说“用的是什么算法?”这时提
问者想知道的应该是处理(domain logic)的流程。这就是算法
的广义含义。
  而算法的狭义含义,大多是指“针对明确定义的计算问题,
执行已定义的计算步骤”。因此,市面上的算法书并不是讲述业
务应用程序的处理逻辑,而是讲述排序、搜索、散列等计算问题
的解法的。
  第 7 章的内容就是狭义的算法。


学习算法的意义
——计算机资源有限,工程师的通用语言

  CPU、内存等计算机资源是有限的,因此学习算法十分重要。
对于必须解决的问题,如何利用有限的资源解决掉,这种思考方
式也是必须学习的。
  而且,与设计模式同样,算法也是工程师们的通用语言。要
用“使用散列就能解决了”这种语言来沟通,双方都需要正确理
解散列算法是什么。
  学习算法显而易见的好处就是,理解算法后就(可能)可以
解决新的问题。
  例如学习贝叶斯过滤器(Bayesian Filter)之后,就可以编
写数据自动分类之类的程序。利用该算法可以编写垃圾邮件过滤
器。
  有了能用几 MB 的容量保存几亿条数据的数据结构,就能轻
146 大规模 Web 服务开发技术


   松发布由于过大而很难发布的程序。如前一阵子发布的 Google
   日文输入法 ① 就用 LOUDS 这种数据结构将字典数据压缩到
   50MB 左右,这样就能发布巨大的字典。
           而且如前所述,面对大规模数据时,算法特性会对应用程序
   的性能产生巨大影响。学习算法对于把握这种感觉也很有帮助。


               为什么算法知识是必要的?
                计算机资源有限
                   工程师们的通用语言
                   理解算法后(也许)可以解决新问题


   算法评测——复杂度记法

           刚才说过,线性查找的计算量为 O(n),二分查找的复杂度
   为 O(log n)。大多情况下,算法的复杂度可以这样定量评测。算
   法评测一般使用复杂度记法(Order 记法)。
           复杂度记法表示的含义是,当算法的输入大小为 n 时,大致
   需要这么多的计算量。
           花费时间与 n 的大小无关,能在固定时间内完成的处理,其
   复杂度为 O(1)。例如从散列中查找数据,虽然要计算散列函数,
   但散列函数计算不依赖于 n,所以复杂度为 O(1)。而散列搜索中,
   给定键的值(几乎)是唯一的,因此通过键搜索值的处理也是
   O(1) 也依赖于具体实现) 因此,
      (         。    散列搜索整体复杂度为 O(1)②。
           如前所见,线性查找要从开头开始查找,最大要查找 n 次。




                                                                
   ① URL:http://www.google.com/intl/ja/ime/。
   ② 复杂度记法中 O(1)+O(1)不等于 O(2),而是取 O(max(1, 1) = O(1)。
第 7 章 算法实用化 147


某些情况下只需一次就能找到,但复杂度记法并不考虑这种特
殊情况,而是表示平均值或最大值。因此记为 O(n)。二分查找
为 O(log n)。
        像这样用复杂度记法表示各种算法,即可比较算法的性能。
线性查找和二分查找分别为 O(n)和 O(log n),因此二分查找的计
算量比较少①。

各种算法的复杂度记法
        各种算法的复杂度记法中,下述几种计算量经常出现。

             O(1) < O(log n) < O(n) < O(nlog n) < O(n2) < O(n3) … <
             O(nk) < O(2n)

        越往右,复杂度越大。处理大规模数据时,也就是说 n 比较
大时,实用算法也就到 O(n logn)附近。再高,复杂度就会随着 n
的增加而急剧增大,经常导致计算无法结束。
        感觉上而言,O(log n)比 O(n)要快很多,O(n)和 O(n log n)
的差距不是很大,O(n)和 O(n2)之间的鸿沟决定着着计算能否完
成……。
        关于速度问题,如果相当复杂的算法能用 O(n2)的计算完成,
那也可以说“相当快”了,毕竟速度取决于算法中的计算本身。
例如,一般的基于比较的排序算法无论怎么优化,也不可能比
O(n logn)快,这一点在理论上可以证明。因此,排序算法能达到
O(n logn),就可以认为是高速算法。
        复杂度的概念不仅用于表示计算的时间,也可以用于表示空
间。也就是说,复杂度记法不仅可以表示执行时间、操作步骤数,
还可以表示内存使用量等。




                                                             
① 实际上,二分查找要求数据必须预先排序,因此并不能保证一定比线性
     搜索快。
148 大规模 Web 服务开发技术


     上面讲述了算法评测。更为详细的内容,请参考讲解算法的
   书籍。


       算法评测
        复杂度记法
         复杂度
          时间复杂度(执行时间、操作步骤数)
          空间复杂度(内存使用量)


   纸巾能折叠几次?——O(logn)和 O(n)的差距

     刚才说过,线性查找和二分查找相比,数据量变大后,计算
   时间就会出现巨大差距。这里的重点不是复杂度记法本身,而是
   利用复杂度记法比较算法时,对算法间差距的感觉。就是说要掌
   握 O(log n)和 O(n)在 n 增大时,对于复杂度差距的感觉。
     再看个身边的例子。准备一张纸巾,然后将其多次对折。到
   底能对折多少次呢?第一次只需对半折叠即可,完全没问题。第
   二次、第三次、第四次直到第五次应该也没问题。但第六次就比
   较难折了,第七次和第八次完全折不动而不得不放弃。可能有人
   会想“折 100 次肯定没问题!,而实际上 7 次就是极限了。为什
                   ”
   么呢?
     折纸所需的劳动量,依赖于要折的纸张的厚度。假设这个厚
   度最初为 1mm,那么第一次折叠之后就是 2mm。两次折叠后是
   3mm……不对,是 4mm。
     这样,厚度是 1→2→4→8→16→32…这样增加的。可以认
第 7 章 算法实用化 149


为,设折叠次数为 n,计算量就按照 2n 增长。刚才看到,O(2n)
是个相当大的复杂度,因此纸巾在 n=8 时无法折叠也就可以理
解了。
        另外有文章介绍过,厚度为 0.11mm 的卫生纸折叠 25 次后
会达到富士山的高度①。要折叠富士山那么高的纸……一般方法
绝对做不到吧。

对算法中指数增加、对数增加的感觉
        计算量按照指数增加的算法,只需一点点数据量,计算量就
会变得庞大无比。与指数相反,只以对数增加的 O(log n)的算法,
即使数据量变得非常大,也只需一点计算量就能解决,这点也能
直观地理解吧。
        在思考算法复杂度时,这种感觉是最重要的。例如要操作的
数据有 1000 万条,如果能选择对数算法,那么只需几十次计算
就可以了。相反,如果选错了算法,使用 O(n2)或 O(2n)的算法实
现的话,写出的程序即使只有几百条数据,也要浪费相当多资源。


算法和数据结构——千丝万缕的联系

        纵观各种算法书籍,大多都是将算法和数据结构作为一个整
体来讲述。
        数据结构就是数组、树结构等存储或表现对象数据的结构。
        将算法和数据结构作为整体讲述,是因为必须依照算法中的
常用操作选择数据结构。例如,事先将数据保存在适当的树形结




                                                             
① URL:http://gigazine.net/index.php?/news/comments/20100305_fold_half/。
150 大规模 Web 服务开发技术


   构中,大多数情况下搜索会变得很简单,可以降低复杂度。
     第 11 课中已经看到,RDBMS 的索引的实现采用了 B+树这
   种树结构。B+树是个空间上适合外部存储的树结构。利用 B+树
   保存索引,不仅能减少查找所需的操作步骤,还能将磁盘读取次
   数降至最低。因此,RDBMS 的索引一般采用 B+树,同时使用
   适合该数据结构的算法进行查找、插入、排序等操作。
     所以说,算法和数据结构之间存在着千丝万缕的联系。


       算法和数据结构
        数据结构
         数组、树结构、堆……
         根据算法常用操作进行选择
        要根据算法常用操作来选择数据结构


   复杂度和常数项——评测很重要

     计算量的复杂度记法忽略了所有“常数项”。所谓常数项就
   是算法实现中不依赖于输入大小,但却不得不执行的一类处理。
     例如函数调用、函数返回等处理都是常数项,第一次分配变
   量、if 语句分支等也是常数项。简单的实现中,常数项几乎不会
   影响算法的复杂度,但在复杂的实现中,常数项就不可忽略了。
   就算实现不复杂,CPU 缓存是否容易生效、分支预测是否发生
   等计算机结构特点也会有影响,因此常数项可能会导致差距。
     例如,例如排序算法的理论下限为 O(n log n),有几个算法
   的平均复杂度能达到 O(n log n)。但是,同为 O(n log n),一般而
第 7 章 算法实用化 151


言快速排序是最快的。快速排序的特点使得 CPU 缓存容易生效,
这一点比其他算法好得多。这就是常数项较小的例子。
  也就是说,复杂度记法适用于比较算法,但在实现时不应只
考虑复杂度。而且常数项经常取决于实现方法,因此实现时要尽
力减小常数项。

实现时要注意的优化问题
  另一方面希望大家注意,实现某样东西(不仅限于算法)时,
一开始就对常数项进行优化,基本上是错误的。努力减少复杂度
为 O(n2)的算法的常数项,还不如用 O(n log n)的算法来代替,那
样改善效果更好。
  说来说去,还是“评测最重要”。通过评测(benchmark)或
分析(profiling)等手段,正确找出当前程序的问题所在最为重
要。是要更换算法来改善,还是减少常数项来改善,或者是物理
资源不足要更换硬件以改善性能?务必在认真找出问题所在之
后,再设法改善。


应用算法的实际情况——简单就是美

  实际上,高级算法未必是最优解,古典算法有时也不错。进
一步说,与知名算法相比,简单算法更好的情况也不罕见。
152 大规模 Web 服务开发技术


   Hatena Bookmark 的 Firefox 扩展的搜索功能的尝试
       介绍一个 Hatena 中的实际例子。Hatena Bookmark 中有
   Firefox 扩展这个工具,通过它可以将 Hatena Bookmark 与浏览器
   集成,十分方便。该扩展可以针对用户以前保存过的书签数据进
   行增量(incremental)搜索(如图 7.1 所示)。
       我们团队讨论了该搜索功能的实现方法,最后结果是,由于
   增量搜索中搜索动作发生频率很高,而且又是在客户端计算,所
   以计算量必须少。一些人的数据量能达到一万条以上,所以选择
   了 Suffix Array。
       Suffix Array 这个数据结构主要用于文本数据等的高速查
   找。查找本身很快,但必须事先花费很长时间创建数据结构。而
   且,要让 Suffix Array 在应用程序中达到实用程度,如何缩短这




           图 7.1   Hatena Bookmark 的 Firefox 扩展的搜索功能
第 7 章 算法实用化 153


段时间,就成了要解决的课题。当时我们采用了刚刚发现的 IS
方法①解决。
        多次尝试之后,我们用 Firefox 扩展中唯一可用的 JavaScript
语言实现了 IS 方法,但实际结合到应用程序之后并没有达到满
意的效果。尽管速度有所提高,但预处理仍然需要花费很长时间,
用户添加书签时进行的预处理会给机器造成很大负担。
        经过痛苦的选择之后,我们放弃了 Suffix Array,而是使用
Firefox 扩展内部的 SQLite 功能, SQL 的 like 进行部分查找
                        用                 (也
就是线性查找)。这会让数据量大的人搜索速度降低,但也是没
办法的事,只能妥协于这种方法了。
        不过,实际完成后发现,使用上完全没有任何问题。曾经担
心过的几万条数据量的问题,也由于现代计算机的性能提高,查
找上完全没有问题……所以就这样了。

得到的经验
        从这个例子中得到的经验就是,评测和估算非常重要,并且
偶尔尝试一下简单的实现也很重要。为大规模数据进行优化固然
很重要,但从本例中可见,数据较少时优化没有任何意义。而且
它也说明,通过人的感觉来推断数据量多少是不可靠的。


灵活应用第三方实现——CPAN 等

        请不要忘记,大部分常用算法都有已发布的实现,供他人使
用。
        以前面所说的 IS 方法为例,当时 JavaScript 上 Suffix Array




                                                             
① IS 方法(Induced-Sort)是个在线性时间内对 Suffix Array 进行排序的算
     法,于 2009 年提出。
154 大规模 Web 服务开发技术


   并没有好的实现,所以不得不自己编写,但如果是 Perl,那么
   CPAN 上有大量各种算法的开源实现函数库,其他语言也一样。
     灵活运用这一类实现,可以缩短开发时间。话虽如此,我们
   不建议在不了解其内容的情况下使用。必须适当了解它做了哪些
   操作,否则就可能做出错误的选择。
     例如,CPAN 上有大量压缩算法的实现。压缩也有适合不适
   合之分,比如对较短文件有效的算法、花费时间很长但压缩比很
   高的算法、压缩比较低但速度快的算法等,算法的特性各不相同。
   为了正确选择算法,了解一些算法知识没有坏处。在 CPAN 上
   搜索“Algorithm”的结果如图 7.2 所示。
     相反,这些函数库的 API 有时无法满足我们的规格要求,




          图 7.2   在 CPAN 上搜索“Algorithm”的结果
第 7 章 算法实用化 155


或者实现的功能过多。这时可以仅实现必要的地方,既能抑制工
作量,单位成本获得的效果也更高。重点就是平衡性。


通过实例加深感受

  刚才看到了算法及其评测,还有一点点实际应用。认真考虑
理论和实际的平衡最为重要,大家应该多少感受到这种氛围了
吧。


专 栏
数据压缩和速度——提高整体吞吐量的思考方式

  第 6 章的课程讨论了压缩。提到“压缩”,很多人想到的是
将大文件变成小文件的工具吧。Windows 的 ZIP 文件夹、GNU
gzip 等。使用这些工具,能将非常大的文件压缩,但也会消耗机
器的处理能力。因此,应该会有人认为“压缩解压处理负载高,
也就会很慢”吧。
  但是从吞吐量的观点来看,把要处理的数据事先压缩,多数
情况下反而更快。计算机有 CPU 和 I/O 两种负载。为了某个处
理而等待 I/O 时,该处理无法使用 CPU。事先将文件压缩,只会
造成少量 CPU 负载,但却能降低 I/O 负载。因为多数情况下 CPU
空闲而 I/O 繁忙,通过压缩可以让 CPU 负担部分 I/O 的负载,
从而提高整体吞吐量。
  HTTP 的 deflate 压缩通信就是最好的例子。尽管压缩给人司
空见惯的印象,但却是个重要技术。
156 大规模 Web 服务开发技术




   第 20 课
   Hatena Diary 的关键字链接

   什么是关键字链接?

     博客服务 Hatena Diary(http://d.hatena.ne.jp/)支持关键字链
   接,这是个很特别的功能。
     该功能在前面的图 5.3(p.112)的关键字链接截图中已介绍
   过。如图 5.3 所示,写博客时部分关键字会自动加上链接,链接
   目标就是该关键字的解释页面。Wiki 的实现也能给 Wiki 关键字
   自动加链接,这个功能与它很相似。
     被 链 接 的 关 键 字 就 是 用 户 在 Hatena Keyword ( http://k.
   hatena.ne.jp/)上添加的关键字。本书执笔时(2009 年 8 月),
   Hatena Keyword 已有 27 万条以上关键字,用户每天创建的新关
   键字大约有 100 个。
     关键字链接功能就是将输入的文章与 27 万条关键字字典进
   行匹配,将必要的地方替换成链接。链接替换操作实际上只是将
   特定关键字替换成 HTML 链接标签而已,所以问题就是如何对
   文章中的关键字进行文本替换。

     关键字链接的例子

       Hatena Diary 是博客

     →<a href="...">Hatena Diary</a>是<a href="...">博客</a>


   最初的实现

     Hatena Diary 刚刚上线时,该功能并没有花太多功夫,简单
   地采用了正则表达式,将字典中的所有单词用 OR 连接做成正则
第 7 章 算法实用化 157


表达式。
(foo|bar|baz| ...)

     就是这种正则表达式。假设将文本放在$text 变量中,那么
把替换选项和替换字符串看作表达式的 eval 选项进行组合,变
成以下形式就可以了:
use URI::Escape;


$text =~ s/(foo|bar|baz)/&replace_keyword($1)/ge;


sub replace_keyword {
    my $w = shift;
    return sprintf '<a href="/keyword/%s">%s</a>', uri_escape($w), $w;
}



出问题了!——关键字字典越来越大

     由于关键字是用户创建的,刚上线时单词数并不多,从数据
库中取出后直接创建正则表达式以实现关键字链接,这种奢侈的
处理也完全没有问题。但是,随着关键字的数量不断增加,问题
就来了。处理正则表达式要花费很长时间。最耗时的地方有两处:
       编译正则表达式的处理;
       用正则表达式进行模式匹配的处理。
     对于 ,可以预先创建正则表达式并保持在内存或磁盘上,
即通过缓存的方法能够绕过去了。
158 大规模 Web 服务开发技术


      对于问题 ,把完成了关键字链接的正文文本进行缓存等处
   理后,开始时能绕过该问题,但要将新添的关键字反映到关键字
   链接中,必须花费一定时间重新建立缓存,或者从博客服务的特
   性上看,多一半文章的访问量并不大,导致缓存很难生效,所以
   该问题并没有完全解决。


   用模式匹配实现关键字链接的问题

      随着服务的运营,关键字的单词数超过了 10 万条,而且
   Hatena Diary 增加的整体访问量使得关键字链接的处理次数也增
   加了许多,系统终于承受不住了。
      关键字链接计算耗费大量时间的原因在于正则表达式的算
   法。详细情况可以参考正则表达式的书籍,简单来说,正则表达
   式的模式匹配是基于自动机实现的。而且,Perl 的正则表达式实
   现采用的是 NFA(Nondeterministic Finite Automata,非确定型有
   穷自动机)。不仅 Perl,实用的语言大多采用了 NFA 引擎。
      像(foo|bar|baz)这种模式匹配,NFA 会使用一种简单的方
   法,从输入的开头尝试匹配,失败的话就尝试下一个单词,如果
   又失败了,就再次尝试下一个单词。因此, 不匹配就尝试 bar,
                      foo
   如果还不匹配,就尝试 baz……如此循环下去。因此,计算量与
   关键字的个数成比例。
      服务刚开始时,关键字个数很少,相应的计算量也很少,所
   以没有发生什么问题。


   从正则表达式到 Trie——改变匹配的实现方式

      为解决模式匹配带来的复杂度问题,我们把实现方法从正则
   表达式变成了 Trie 树。
第 7 章 算法实用化 159


Trie 入门
   Trie 这种数据结构是树结构的一种。它的特点是,用树结构
将搜索对象数据的公共前缀综合到一起。看个例子就明白了。例
如关键字"ab"、"abcde"、"bc"、"bab"、"d"的 Trie 如图 7.3 所示。
   为了易于理解,这里给各个节点加上了编号。树的边加上字
母,遍历边相当于进行查找。例如依次遍历 0→1→2,就是从根
节点开始依次遍历'a'边、'b'边。而节点 2 上的信息表明'ab'是路径
的终端。沿着'a'→'b'→'c'遍历能到达 8 号节点,但 8 号节点上没
有终端信息,因此'abc'不包含在 Trie 中。
   从关键字中可以看出,"ab"和"abcde"拥有'ab'这个公共前缀,
"bab"和"bc"拥有'b'这个公共前缀。Trie 的特点就是将公共前缀综
合到一起,以避免浪费。


     Trie
        用树结构高效存储字符串集合
         其树结构可以将搜索对象数据的公共前缀综合到一起

               a           b        c         d        e    10
                                2
     0             1                    8          9       abcde
                               ab

               b
                           c
                   3           4


                       a
                                    b    6
           d                   5        bab

                   7
                   b

   图 7.3       Trie 结构(关键字:"ab"、"abcde"、"bc"、"bab"、"d")
160 大规模 Web 服务开发技术


   Trie 结构和模式匹配
       把 Trie 结构当作字典进行模式匹配,其计算量要比正则表
   达式少得多。将输入文本输入到 Trie 中,遍历它的边,如能找
   到终端,就可以认为该单词存在。与(foo|bar|baz)的正则表达式
   相比,公共前缀只需搜索一次即可。
       考虑 hogefoo 这个单词。用该单词对包含 foo、bar、baz 的
   Trie 结构进行遍历。由于不包含 h 字符串,所以不会匹配。接下
   来遍历 oge、ge、e 也一样。然后,用 f 遍历 Trie 时会发现,后
   面有 oo,因此 foo 能匹配。但计算量不会超过 hogefoo 的长度。
       而使用正则表达式进行模式匹配时,首先要用 h 与 foo、bar、
   baz 等比较是否匹配,然后用 o 做同样的操作,如此反复,花费
   的时间与关键字数量呈比例,因此关键字词典越大,两者的差距
   就越明显。


   Aho-Corasick 算法

       实际上,在改善 Hatena Diary 时,我们没有用 Trie 结构进行
   模式匹配,而是采用了更为高速的 Aho-Corasick 算法。
       Aho-Corasick 算法是 Alfred V.Aho 和 Margaret J.Corasick 于
   1975 年在论文《Efficient String Matching: An Aid to Bibliographic
   Search》中提出的古典算法,根据字典创建执行模式匹配的自动
   机,以实现在线性时间内对输入文本进行计算。这种高速算法的
   复杂度与字典大小无关。
       Aho-Corasick 算法利用 Trie 进行模式匹配,但它添加了返回
   的边,匹配失败时可以沿着边返回。如果图示的话,如图 7.4 所
   示。
       例如将"babcdex"输入图中的 Trie,那么找到 bab 的同时也找
   到了 ab。因此,如果能在查找到 bab 之后不要返回开头的节点 0,
第 7 章 算法实用化 161


而是直接移动到节点 2 的话,就能立即找到 ab。而将“节点 6
的下一个是节点 2”这种路径添加到 Trie 中的预处理,就是
Aho-Corasick 算法的主要原理。至于添加这些路径的方法,只需
从 Trie 的根节点进行广度优先搜索,找到合适的节点就能建立,
这种算法也是众所周知的。
        2005 年时我们并没有想到利用 Aho-Corasick 算法进行关键
字链接,后来是工藤拓告诉我们的。工藤拓是きまぐれ日記①
                          (神
经质的日记)的博主,语素分析库 MeCab②的开发者,现在在
Google 参与 Google 日文输入法相关开发。其背景是,实现语素
分析引擎,要将输入的文章与字典中的所有单词进行模式匹配,
这与关键字链接的处理几乎是一样的,而且在自然语言处理中,
这种任务使用 Trie 已成为惯例。
        另外,用 Aho-Corasick 算法实现关键字链接的课题请参见
第 8 章。


                     a                  b                  c          d        e
                                                 2                                  10
           0                  1                 ab              8          9       abcde

                     b
                                        c
                              3                   4
                                                 bc

                                    a
                                                           b     6
                 d                                5             bab

                              7
                              b

                                        图 7.4         Aho-Corasick 算法




                                                             
① URL:http://chasen.org/~taku/blog/archives/2005/09/post_812.html。
② URL:http://mecab.sourceforge.net/。
162 大规模 Web 服务开发技术




               Aho-Corasick 算法
                 复杂度与字典大小无关的高速算法
                   根据字典创建自动机进行模式匹配,对输入文本实现了线
                   性的计算时间


   换成 Regexp::List

           采用 Aho-Corasick 算法后,毫无悬念地解决了关键字链接的
   复杂度问题。之后的一段时间采用了我们自己实现的 Aho-
   Corasick 函数库,但后来又换成了 Regexp::List①这个 CPAN 库。
   Regexp::List 为小饲弹②开发的正则表达式函数库,可以生成基于
   Trie 的正则表达式。
           也就是说,该函数库并非将巨大的正则表达式按照我们最初
   的实现那样用 OR 连接,而是用 Trie 对正则表达式进行优化。利
   用该函数可以将
   qw/foobar fooxar foozap fooza/

           这个正则表达式变换成如下的正则表达式:
   foo(?:[bx]ar|zap?)

           它可以综合公共的前缀和后缀,利用优化后的正则表达式进
   行模式匹配,比用 OR 链接所有单词的尝试次数少得多。减少的
   原因与上述对 Trie 的解释相同。
           利用 Regexp::List 不仅能抑制计算量,而且还能作为正则表
   达式使用。最初简单地采用正则表达式实现时,可以组合各种正




                                                                
   ① URL:http://search.cpan.org/dist/Regexp-List/。
   ② 译者注:小饲弹(Dan Kogai),日本开源软件开发者。文本编码转换
        Perl 模块 Encode.pm 的作者。博客为 http://blog.livedoor.jp/dankogai/。
第 7 章 算法实用化 163


则表达式的选项,还能使用 Perl 语言特有的各种功能,拥有丰
富的灵活性。但改成 Aho-Corasick 算法就失去了这个优势,也
谈不上灵活性了。采用 Regexp::List 可以同时拥有两者的优势。


关键字链接的实现、变迁和考察

     像这样,关键字链接的实现经历了以下过程:巨大的正则表
达式→Aho-Corasick 算法→Regexp:List。从这个过程中可以看出
一些问题。

      最初的简单实现也有一定功效。当初因为简单而采用了
      最简单的正则表达式,实现所需工作量也很少,实现的
      灵活性也很高。因此可以很容易地尝试 Hatena Diary 的关
      键字链接的各种功能,也能根据用户的希望进行修改。
      另一方面,数据量增大后问题就显现出来了。解决该问
      题需要本质的解决方案。通过缓存等表面的修改,可以
      从某种程度上绕过问题,但最终必须解决算法上的根本
      问题。要看清这一点,必须像算法评测中讲过的那样,
      从复杂度观点去寻找问题。

  一开始就采用最优算法并不一定正确,数据量较小时采用简
单方法反而效果较好,必须找出本质问题的解决方法以备数据量
增大……等等,这些面向大规模数据的算法实用化中的各个篇
章,都浓缩在了关键字链接之中。
  更不用说,这段经历导致了 Hatena 重新审视自己的技术水
平。
164 大规模 Web 服务开发技术




   第 21 课
   Hatena Bookmark 的文章分类

   什么是文章分类?

      最后以 Hatena Bookmark 的文章分类为例,看看如何使用特
   定算法解决新问题。前面的图 5.4 p.113)
                   (       的截图已介绍过,Hatena
   Bookmark 提供自动分类功能,将新文章根据文章内容自动分类,
   并按类别呈现给用户。
      例如,可以将文章分类成“科学·学问”和“电脑·IT”两
   类。其他还有“政治·经济”“生活·人生”等共 8 个分类。用
                、
   户向 Hatena Bookmark 提交新文章后,Hatena Bookmark 系统就
   会通过 HTTP 获取文章内容,根据文本内容进行分类,以判断其
   类别。

   用贝叶斯过滤器判断类别
      判断类别需要用到贝叶斯过滤器。课程开头介绍过,贝叶斯
   过滤器可以用于在垃圾邮件过滤器等,所以应该有很多人知道这
   个名字吧。
      贝叶斯过滤器接收文本作为输入,并使用朴素贝叶斯(Naive
   Bayes)算法从概率上判断文章属于哪个类别。其特点是判断未
   知文章时,要利用以前已分类数据的统计信息进行判断。必须事
   先由人提供“正确数据”,告诉贝叶斯过滤器哪篇文章属于哪个
   类别,让它“学习”,最后才能自行进行正确判断。
      这种事先提供学习数据,使计算机能针对未知输入进行某种
   计算的处理,是“机器学习”领域的研究成果。另外,像贝叶斯
第 7 章 算法实用化 165


过滤器这种根据已有文章——即模式——进行分类,属于“模式
识别”领域。灵活应用机器学习、模式识别领域的算法,不仅能
实现自动分类,还能开发别具一格的软件。


机器学习和大规模数据

  像贝叶斯过滤器一样,许多机器学习任务都需要正确数据。
提供正确数据后,学习引擎就能达到甚至超过人类解决特定问题
的准确度。
  虽然贝叶斯过滤器并不需要如此大量的正确数据,但机器学
习任务中,数据量越大,准确度就越高的例子并不罕见。从可扩
展性的观点来看,大规模 Web 服务拥有的大量数据虽然给运维
带来了麻烦,但它们却是研发领域求之不得的数据。

Hatena Bookmark 的相关条目功能
  Hatena Bookmark 具有“相关条目”功能,该功能可以将类
似于某篇文章的关联信息提供给用户。图 7.5 就是 Hatena
Bookmark 针对 Google Chrome 扩展发布的文章给出的相关条目,
可见它提取的有关 Google Chrome 扩展的话题还挺准确。
  关联条目功能,利用了 Hatena Bookmark 中用户手动输入的
4000 多万条“标签”这种分类用文本,并使用文章推荐算法实




          图 7.5   Hatena Bookmark 的关联条目
166 大规模 Web 服务开发技术


   现。文章推荐算法的实现采用了株式会社 Preferred Infrastructure①
   的引擎。
           关联条目功能利用几千万条标签数据取出几条关联文章,这
   对于人类几乎是不可能的任务。像这种从大量数据中取出有意义
   的数据,也许只有拥有大规模数据的 Web 服务才能做到。


   大规模数据和 Web 服务——The Google Way of Science

           说起大规模数据和 Web 服务,就不得不提 Google。大家在
   用 Google 搜索时肯定注意到,它有个“您是不是要找”功能,
   针对错误的查询推荐可能正确的查询。这个“您是不是要找”功
   能,就是将以前的用户搜索记录作为正确数据,学习错误时应当
   如何改正之后,提示出正确数据。Google 非常善于利用收集的
   大量数据并给出反馈。
           其实 Google 这个搜索引擎就是以大量 Web 文档为输入,从
   中提取出有意义的文章,他们自然会深入研究该领域。
           想必大家都知道,最近 Google 利用它的大规模数据,在该
   领域进行了深入研发。Google 拥有全球规模的数据量,利用前
   所未有的数据量做出未知的研究成果,因而受到关注也并不意
   外。
           “The Google Way of Science”②是以前 Wired magazine 的
   Kevin Kelly 的专栏,以“用大量数据和应用数学取代其他一切
   工具”为主旨,考察 Google 的动向。例如有个故事说,
                               “Google
   开发了翻译引擎,对输入模式和词语转换进行学习后,能翻译大




                                                                
   ① URL:http://preferred.jp/。
   ② URL:http://www.kk.org/thetechnium/archives/2008/06/the_google_way.php,
        中文意为“Google 式的科学”
                        (日文 URL:http://memo7.sblo.jp/article/
                     。
        25170459.html)
第 7 章 算法实用化 167


量输入数据,但中文翻译程序的开发者们并不懂中文。”也就是
说,即使不知道理论上“何为正确”,利用应用数学(多数情况
下使用的是统计领域),为机器学习提供前所未有的海量数据,
就能从那个黑盒子中得到正确结果。这个结论足以颠覆以前的科
学常识。
        这个专栏很有意思,而且从今后的研发本质和潮流来看也相
当有趣,强烈推荐读一读。


贝叶斯过滤器的原理

        有点跑题了。回到贝叶斯过滤器的话题上。这里不讲贝叶斯
过滤器的实现,只是简单介绍一下算法运行的原理①。
        刚才说过,贝叶斯过滤器的核心是朴素贝叶斯算法②。朴素
贝叶斯是基于贝叶斯定理的算法。

用朴素贝叶斯进行类别判断
        接下来会讲一些公式。看不太明白也没关系,随便看看就行
了。用朴素贝叶斯进行类别判断,就是给定某篇文档 D,求出该
文档所属概率最高的分类 C 的问题。也就是说,给定文档 D,
求出属于分类 C 的条件概率:

             P(C|D)




                                                             
① 深入到实现方式的说明,请参见我在《WEB+DB PRESS》(Vol.56)上
     连载的《実践アルゴリズム教室》(实践算法学堂)的“第1回:ベイ
     ジアンフィルタ開発に挑戦”
                 (第一回:挑战贝叶斯过滤器开发)。有兴
     趣的人一定要读一读。
② Hatena Bookmark 的分类使用的是由朴素贝叶斯算法进一步发展而来的
     Complement Naive Bayes 算法。
168 大规模 Web 服务开发技术


      设多个分类中概率最高的分类为 C,即为最终选择的分类。
      直接计算条件概率 P(C|D)比较困难,可以利用“贝叶斯定
   理”将其变成可计算的公式。与其说贝叶斯定理云云,倒不如用
   众所周知的数学理论对概率公式变形。变形结果为

        P(C|D) = P(D|C)P(C) / P(D)

      只需求出右边的各个概率 P(D|C)、P(C)、P(D)即可。
      请注意,类别判断所需的并不是具体的概率值,只需比较各
   个分类,求出概率最大者即可。那么,分母 P(D)为文档 D 条件
   的概率,对于所有类别而言该值都相同,比较时可以忽略。
      因此只需考虑以下两个:

        P(D|C)
        P(C)

      要判断类别,只需从学习数据的统计信息中计算出这两个值
   即可。求这两个值实际上非常简单。
      P(C)是某个分类的出现概率,只需事先保存学习数据被保存
   到各个分类中的次数,即可计算。
      至于 P(D|C),可以把文档 D 看成任意单词 W 连续出现的结
   果,那么 P(D|C)可以近似成 P(W1|C) P(W2|C) P(W3|C)…P(Wn|C)。
   这样只需将文档 D 分割成单词,求出每个单词被分类到各个类
   别中的次数,即可求出 P(D|C)的近似值。
第 7 章 算法实用化 169




            贝叶斯公式
             P(B|A)=P(A|B) P(B) / P(A)
            →贝叶斯定理证明了上述概率公式成立。
            直接求 P(B|A) ——即事件 A 发生之后事件 B 发生的概率——
    困难时,这个公式就能派上用场。利用贝叶斯定理变形后,要求
    出 P(B|A),只需求出 P(A|B)、P(B)和 P(A)即可。
            如正文中所示,与其他值比较时,P(A)通常可以忽略,所以
    最后只需求出 P(A|B)和 P(B)即可。



轻而易举实现类别判断
        最后结论是,只需给出正确数据,并保存正确数据被使用的
次数,以及各个单词的出现次数,然后通过朴素贝叶斯算法计算
概率,就能判断出类别。其他数据可以全部抛弃。
        Hatena Bookmark 现在保存的文章数据超过 2000 万条。而
且,邮件过滤器也要对每天收到的邮件进行分类。我们已经看到,
朴素贝叶斯算法只需保存一部分正确数据①,再保存一部分数据
即可,即使面对大规模数据,引擎本身也十分精炼。而且,分类
判断时,只需根据部分正确数据进行(对于计算机而言)简单的
概率计算——也就是四则运算,因此处理速度也很快。
        一说到“分析大量文章的内容,自动判断各个文章的分类”,
会感到根本摸不着头脑,但像这样解释了算法的原理之后,就会
觉得实现相当简单。




                                                             
① 感觉上,Hatena Bookmark 的 8 个分类只需 1000 条左右正确数据,即可
     达到实用程度。
170 大规模 Web 服务开发技术


   算法实用化之路——Hatena Bookmark 的实例

           贝叶斯过滤器的原理出乎意料地简单,实际实现时,主要部
   分只需 100~200 行左右的脚本语言即可完成。算法本身的实现
   很简单。
           下面以 Hatena Bookmark 为例,简单列举一下将贝叶斯过滤
   器实现的类别分类引擎融入产品所需的其他工作。

                分类引擎是用 C++开发的。需要将引擎变成网络服务器。
                编写 Perl 客户端,与该服务器通信并获取结果。由 Web
                应用程序调用。
                为了定期备份学习数据,要给 C++引擎添加数据转储和
                加载功能。
                人工准备 1000 条学习数据。这是必须由人努力完成
                的……
                实现统计,以跟踪判断精度是否足够。将统计画成图表,
                以进行调优。
                考虑冗余化,建立 standby 系统。自动切换功能会消耗很
                多工时,因此只需能从备份系统中加载数据就可以了。
                在 Web 应用程序上准备用户界面。

           差不多就这些。是不是觉得工作量很大啊。
           用 C++编写引擎也是导致工作量稍稍变大的原因,但即使
   用脚本语言编写,实现服务器程序等工作也不会改变。另外,将
   引擎变成服务器以及与 Perl 的 API 交互上,采用了 Apache Thrift①
   这个多语言 RPC 框架。

   实际操作中要考虑的问题很多
           这里并不是想发牢骚,只想告诉大家,算法实用化的路上,
                                                                
   ① URL:http://incubator.apache.org/thrift/。有篇较老的文章讨论过 Thrift,
     位于 《WEB+DB PRESS》      (Vol.46)  我的连载    《Recent Perl World——Thrift
     で多言語 RPC……C++でサーバ、Perl でクライアント》                         (Recent Perl
     World——用 Thrift 实现多语言 RPC……C++写服务器, 写客户端)             Perl        。
     有兴趣的人可以参考。
第 7 章 算法实用化 171


要考虑的实际问题层出不穷。比如本例,需要另外实现服务器程
序时需要特别注意。研发性质的开发,只要核心部分的原型能正
常运行就欢欣鼓舞了,但距离实际应用还有许许多多工作。在开
发现场,维护这些程序的正常运行,以及事先预测工作量等,都
十分重要。


防守姿态和进攻姿态——从文档分类功能说开去

  上面介绍的机器学习、模式识别、数据挖掘等方法的利用目
的,都是从大量数据中提取出有意义的数据,以紧凑方式持有大
规模数据的“特征”以备稍后使用。
  同样是算法,针对大规模数据进行排序、搜索、快速压缩等
算法多用于解决已出现的问题,是“防守”姿态的算法;相反,
机器学习、模式识别等积极地利用大规模数据,使用处理结果给
应用程序增加附加价值,是“进攻”姿态的算法。

储备已有方法
  无论是防守姿态还是进攻姿态,在学习面向大规模数据的算
法的路上,将一定程度的已有方法作为知识储备是非常重要的。
如果不了解 Trie 是何种数据结构、具有何种特性,就不会想到
在关键字链接中使用 Trie;不理解贝叶斯过滤器的原理,文档自
动分类的灵感也不可能出现。
  此外,如前所述,从实现算法到实用化,还有大量必要的附
加工作要做。
  第 7 章的课程占用的篇幅很长。通过该课程,希望大家能切
实体会到面对大量数据时应如何选择算法、应用算法。
172 大规模 Web 服务开发技术




   专 栏
   拼写错误改正功能的制作方法
   ——Hatena Bookmark 的搜索功能

     课程中间我们提到了 Google 的“您是不是要找”功能。课
   程正文中也说过,Google 的搜索查询补全功能应该是将搜索引
   擎日志作为正确数据提供给学习引擎实现的。
     那么,   如果没有大量日志的话,      编写这种程序是不是困难?
   即使没有日志,只要有一定规模的正确数据,也就是说,只要有
   字典,用别的方法也能实现。        将某个特定字典数据作为正确答案
   去修正错误答案,也就是所谓的拼写错误改正功能。
     Hatena Bookmark 的搜索功能就支持这种简单方法实现的拼
   写错误改正功能(如图 F.1 所示)     。
     拼写错误改正功能的实现方法如下。
      正确数据采用 Hatena Keyword 的字典,它拥有 27 万条正
   确数据
      计算用户输入的搜索查询与字典中的语句之间的编辑距
   离,定量衡量错误程度
      以一定的错误程度为基准,        从字典中找出某个单词群作为
   候补正确答案
      将 的候补正确答案以 Hatena Bookmark 的文章中的单词
   使用频率为基准,按照正确的可能性排列
      将使用频率最高的单词作为正确答案,提示给用户
     基本上是这个流程,        就是从字典中查找与输入相似的单词并
   推荐给用户。下面分别仔细地看看每一步。




          图 F.1   Hatena Bookmark 的拼写错误改正功能
第 7 章 算法实用化 173



 27 万条正确答案的字典——将 Hatena Keyword 作为正确
数据字典使用
  这个拼写错误改正程序必须知道什么是正确答案。正确数据
采用了 Hatena Keyword。如果使用仅包含地名的字典,就变成
了地名改正引擎,使用仅包含餐厅名称的字典就成了餐饮改正引
擎,很有意思。
   如果无法自己准备通用字典,可以下载 Wikipedia 等的数据,
用它包含的单词作为字典也可以。

 计算搜索查询与字典中语句的编辑距离,定量衡量错误程度
   所谓编辑距离,就是把一个单词变成另外一个单词所需的编
辑(插入、替换、删除)次数,用它可以定量衡量单词之间的距
离。
   看个例子更容易理解。
  (伊藤直哉,伊藤直也)→1
  (伊藤直,伊藤直也)→1
  (佐藤直哉,伊藤直也)→2
  (佐藤 B 作,伊藤直也)→3
     各个组合的编辑距离如上所示。可见,编辑距离为 1 的单词
相似程度的确比编辑距离为 3 的高。
   众所周知,编辑距离可以用动态规划算法简单、高速地实现,
是动态规划解决问题的代表例。Hatena Bookmark 采用的是由普
通的编辑距离——Levenshtein 距离——发展而来的 Jaro-Winkler
距离。Jaro-Winkler 距离这种定量方法对于靠前的错误单词有较
高的惩罚,这是因为姓名中姓氏经常出错,而名字却不容易写错。
Jaro-Winkler 距离就由这种直感而来。

 以一定的错误程度为基准,从字典中找出某个单词群作为
候补正确答案
   这样,比较输入查询与字典中的单词的编辑距离,获得编辑
距离较小的单词一览。但是,字典中有 27 万条单词,应当避免
174 大规模 Web 服务开发技术


   全部比较。
     我们采用的数据结构,可以先创建了字典的 n-gram 索引,
   仅仅取出与输入语句的 bi-gram 重叠度高的单词。看看图 F.2 就
   很容易理解。
     用这种数据结构,可以预先缩小比较对象,之后再逐个计算
   编辑距离。

    将候补正确答案以文章中的单词使用频率为基准,按照正
   确的可能性排列
     然后将计算出的编辑距离较小的作为候补答案,那么编辑距
   离都是 1、2 这种跳跃的值,因此经常会得到多个候补。输入伊
   藤直弥,就会得到这些正确答案候补:

       伊藤直也
       伊藤直哉
       伊东直也

     哪个才是正确答案呢?
     一个方法是选择搜索空间中出现频率最高的单词作为正确
   答案。Hatena Bookmark 就是这样做的。我们采用文档频率
  (Document Frequency)作为“出现频率最高”的标准。文档频

         用户
                        在 n-gram 索引中匹配两次的单词
         输入
                        也就是与输入“重叠”多的单词




              图 F.2   用 n-gram 索引限定修正候补
第 7 章 算法实用化 175


率就是特定单词在 Hatena Bookmark 中出现在多少篇文章中的
次数。Hatena Bookmark 的其他功能也用到这个数值,因此该值
被保存了下来,用它作为判断依据。
   搜索伊藤直哉而不是伊藤直也的人当然存在,但多数情况下
这种方法是好用的,这就是所谓的启发式(heuristic)吧。

  将使用频率最高的单词作为正确答案提示给用户
   通过上述流程就能找到可能正确的单词,将其作为修正的候
补答案提示给用户。实际上是将 Jaro-Winkler 距离和文档频率相
乘作为分数,然后显示分数大于一定标准的答案。这样,那些不
太像是正确答案的就不会被提示了。
   该方法的详细实现以及代码刊登在《WEB+DB PRESS》
(Vol.51)上我的连载《Recent Perl World——第 19 回:スペル修
正プログラムを作る》(Recent Perl World——第 19 回:创建拼
写修正程序)上,推荐阅读一下。
   当然,从搜索引擎的目的上来看,这种方法实现的拼写修正
功能的有效性并不高,老实说,与 Google 的功能相比,只能期
待这种方法作出些许改善。毕竟,它只能修改英文单词拼写错误
等某种程度上具有正确答案的内容。搜索查询中经常会输入网
络流行语等意想不到的单词,因此以搜索日志等大量“随时更新
的正确答案”为基础进行计算更恰当。

More Related Content

Similar to 大规模Web服务开发技术 样章(第7章)

Interactive Data Language
Interactive Data LanguageInteractive Data Language
Interactive Data Languagesiufu
 
数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社
数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社
数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社pingjiang
 
系統程式 -- 第 1 章 系統軟體
系統程式 -- 第 1 章 系統軟體系統程式 -- 第 1 章 系統軟體
系統程式 -- 第 1 章 系統軟體鍾誠 陳鍾誠
 
软件工程
软件工程软件工程
软件工程bill0077
 
長庚 0511.2011(曾懷恩教授演講)
長庚 0511.2011(曾懷恩教授演講)長庚 0511.2011(曾懷恩教授演講)
長庚 0511.2011(曾懷恩教授演講)noritsai
 
01 课程介绍与计算机系统概述
01 课程介绍与计算机系统概述01 课程介绍与计算机系统概述
01 课程介绍与计算机系统概述Huaijin Chen
 
网管会 一些基础知识
网管会 一些基础知识网管会 一些基础知识
网管会 一些基础知识Jammy Wang
 
软件工程 第七章
软件工程 第七章软件工程 第七章
软件工程 第七章浒 刘
 
R 語言教學: 探索性資料分析與文字探勘初探
R 語言教學: 探索性資料分析與文字探勘初探R 語言教學: 探索性資料分析與文字探勘初探
R 語言教學: 探索性資料分析與文字探勘初探Sean Yu
 
基于Eucalyptus的教育知识服务体系模型研究(1)
基于Eucalyptus的教育知识服务体系模型研究(1)基于Eucalyptus的教育知识服务体系模型研究(1)
基于Eucalyptus的教育知识服务体系模型研究(1)liangxiao0315
 
程式人雜誌 -- 2013年5月號
程式人雜誌 -- 2013年5月號程式人雜誌 -- 2013年5月號
程式人雜誌 -- 2013年5月號鍾誠 陳鍾誠
 
下午技术演讲 Zenny chen
下午技术演讲 Zenny chen下午技术演讲 Zenny chen
下午技术演讲 Zenny chencsdnmobile
 
MapReduce : simplified data processing on large clusters abstract ...
MapReduce : simplified data processing on large clusters abstract  ...MapReduce : simplified data processing on large clusters abstract  ...
MapReduce : simplified data processing on large clusters abstract ...butest
 
MapReduce : simplified data processing on large clusters abstract ...
MapReduce : simplified data processing on large clusters abstract  ...MapReduce : simplified data processing on large clusters abstract  ...
MapReduce : simplified data processing on large clusters abstract ...butest
 
系統程式 -- 第 10 章
系統程式 -- 第 10 章系統程式 -- 第 10 章
系統程式 -- 第 10 章鍾誠 陳鍾誠
 
Java解惑(中文)
Java解惑(中文)Java解惑(中文)
Java解惑(中文)yiditushe
 
软件设计原则、模式与应用
软件设计原则、模式与应用软件设计原则、模式与应用
软件设计原则、模式与应用yiditushe
 
App operationattaobao-velocity2010 bj-final
App operationattaobao-velocity2010 bj-finalApp operationattaobao-velocity2010 bj-final
App operationattaobao-velocity2010 bj-finaliambuku
 

Similar to 大规模Web服务开发技术 样章(第7章) (20)

Interactive Data Language
Interactive Data LanguageInteractive Data Language
Interactive Data Language
 
数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社
数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社
数据结构(用面向对象方法与C++语言描述第二版)殷人昆编著清华大学出版社
 
系統程式 -- 第 1 章 系統軟體
系統程式 -- 第 1 章 系統軟體系統程式 -- 第 1 章 系統軟體
系統程式 -- 第 1 章 系統軟體
 
软件工程
软件工程软件工程
软件工程
 
長庚 0511.2011(曾懷恩教授演講)
長庚 0511.2011(曾懷恩教授演講)長庚 0511.2011(曾懷恩教授演講)
長庚 0511.2011(曾懷恩教授演講)
 
01 课程介绍与计算机系统概述
01 课程介绍与计算机系统概述01 课程介绍与计算机系统概述
01 课程介绍与计算机系统概述
 
网管会 一些基础知识
网管会 一些基础知识网管会 一些基础知识
网管会 一些基础知识
 
软件工程 第七章
软件工程 第七章软件工程 第七章
软件工程 第七章
 
R 語言教學: 探索性資料分析與文字探勘初探
R 語言教學: 探索性資料分析與文字探勘初探R 語言教學: 探索性資料分析與文字探勘初探
R 語言教學: 探索性資料分析與文字探勘初探
 
基于Eucalyptus的教育知识服务体系模型研究(1)
基于Eucalyptus的教育知识服务体系模型研究(1)基于Eucalyptus的教育知识服务体系模型研究(1)
基于Eucalyptus的教育知识服务体系模型研究(1)
 
Storm
StormStorm
Storm
 
Python Basic
Python  BasicPython  Basic
Python Basic
 
程式人雜誌 -- 2013年5月號
程式人雜誌 -- 2013年5月號程式人雜誌 -- 2013年5月號
程式人雜誌 -- 2013年5月號
 
下午技术演讲 Zenny chen
下午技术演讲 Zenny chen下午技术演讲 Zenny chen
下午技术演讲 Zenny chen
 
MapReduce : simplified data processing on large clusters abstract ...
MapReduce : simplified data processing on large clusters abstract  ...MapReduce : simplified data processing on large clusters abstract  ...
MapReduce : simplified data processing on large clusters abstract ...
 
MapReduce : simplified data processing on large clusters abstract ...
MapReduce : simplified data processing on large clusters abstract  ...MapReduce : simplified data processing on large clusters abstract  ...
MapReduce : simplified data processing on large clusters abstract ...
 
系統程式 -- 第 10 章
系統程式 -- 第 10 章系統程式 -- 第 10 章
系統程式 -- 第 10 章
 
Java解惑(中文)
Java解惑(中文)Java解惑(中文)
Java解惑(中文)
 
软件设计原则、模式与应用
软件设计原则、模式与应用软件设计原则、模式与应用
软件设计原则、模式与应用
 
App operationattaobao-velocity2010 bj-final
App operationattaobao-velocity2010 bj-finalApp operationattaobao-velocity2010 bj-final
App operationattaobao-velocity2010 bj-final
 

Recently uploaded

中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,Xin Yun Teo
 
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptxEDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptxmekosin001123
 
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制jakepaige317
 
educ6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptxeduc6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptxmekosin001123
 
EDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptxEDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptxmekosin001123
 
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...黑客 接单【TG/微信qoqoqdqd】
 
泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书
泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书
泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书jakepaige317
 

Recently uploaded (7)

中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
中国文学, 了解王安石变法,熙宁变法,熙盛变法- 中国古代改革的类型- 富国强兵,
 
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptxEDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
EDUC6506(001)_ClassPresentation_2_TC330277 (1).pptx
 
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
哪里可以购买日本筑波学院大学学位记/做个假的文凭可认证吗/仿制日本大学毕业证/意大利语CELI证书定制
 
educ6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptxeduc6506presentationtc3302771-240427173057-06a46de5.pptx
educ6506presentationtc3302771-240427173057-06a46de5.pptx
 
EDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptxEDUC6506_ClassPresentation_TC330277 (1).pptx
EDUC6506_ClassPresentation_TC330277 (1).pptx
 
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
1.🎉“入侵大学入学考试中心修改成绩”来袭!ALEVEL替考大揭秘,轻松搞定考试成绩! 💥你还在为无法进入大学招生系统而烦恼吗?想知道如何通过技术手段更改...
 
泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书
泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书
泽兰应用科学大学毕业证制作/定制国外大学录取通知书/购买一个假的建国科技大学硕士学位证书
 

大规模Web服务开发技术 样章(第7章)

  • 1.   第7 章 算法实用化 ——从身边的例子来看理论、研究的实践投入 [课程]伊藤直也 算法和数据结构的选择十分重要 ——适合解决问题的算法和数据结构 第 7 章讲述算法及其应用的概论。大量数据要在可接受的时 间内处理掉,但有时不论等多长时间也计算不完。相反,使用了 适合问题解决的算法和数据结构的话,原本需要一天计算时间的 任务几秒钟就能完成。 要想写出高速程序,算法和数据结构的选择是非常重要的。 要处理的数据越大,不同选择之间的差距越显著。 第 7 章有两个目的,一是让大家感受面对大规模数据时算法 选择的重要性,二是让大家了解如何将算法应用到产品中。我们 会以 Hatena 的各种真实服务的功能为例,讲述最适合的素材。 第 19 课以算法和算法评测为题,从复杂度记法等与大规模 数据这个主题息息相关的基本事项开始,讲述算法应用到实际产 品中的方法。 20 课和第 21 课以第 5 章后半部分简单介绍过的 第 “Hatena Dairy 的关键字链接”和“Hatena Bookmark 的文章分 类”的实现为基础,具体介绍算法的实际应用的过程和变迁,以 及在部分实际服务中如何灵活运用算法等内容。 算法的实用化: 算法和算法评测(→第 19 课); Hatena Diary 的关键字链接(→第 20 课); Hatena Bookmark 的文章分类(→第 21 课)。
  • 2. 第 7 章 算法实用化 143 第 19 课 算法和算法评测 数据规模和复杂度的差异 之前反复说过,要处理的数据越大,算法和数据结构的选择 对速度的影响也就越大。首先看个简单的例子。假设要从数据中 使用线性查找(Linear Search),从头开始依次查找所需数据, 那么如果有 1000 条数据,那就需要反复查找数据直至找到为止, 这个算法最多要进行 1000 次查找。对于 n 条数据要进行 n 次搜 索,因此称为 O(n)算法。 而“二分查找”(binary search)算法能在 log n 次之内查找 n 条数据,是 O(log n)算法。使用二分查找,1000 条数据最多只 需 10 次就能查找完。 这个“最大查找次数”可以大致判断计算次数,称为复杂度。 一般来说,复杂度越低,算法就越快。 n=1000 时,O(n)的最大查找次数为 1000,而 O(log n)为 10, 计算次数差距为 990。n 再大些会怎样呢?若是 100 万条数据, O(n)需要 100 万次,而 O(log n)只需 20 次。即使是 1000 万条, O(log n)也只需 24 次。很明显,与 O(n)相比,O(log n)更能承受 数据量的增加。 请以大规模数据为前提思考一下。数据量较小时,即使使用 O(n)这种简单算法,计算量也不会太大,因此没什么太大问题。 但随着数据量的增加,算法选择的差异就越来越大。在数据搜索 处理中,使用线性查找的话,数据量增大到 1000 条、100 万条、 1000 万条时……显然会成为瓶颈。而解决该瓶颈的方法就是选 择复杂度更低的查找算法,这也是不言而喻的。
  • 3. 144 大规模 Web 服务开发技术 第 7 章的两个目的 如本章开始时讲的,第 7 章将以两个目的为前提进行讲解。 第一个目的就是让大家感受面对大规模数据时算法选择的重要 性。说到处理大规模数据时算法很重要,看起来好像是换个算法 实现就万事大吉了,实际上没这么简单。 算法和数据结构的教科书基本上只会讲到算法的实现,而怎 样将实现应用到系统中,以及怎样运维,却不会讲述。 我们希望将大学教科书、论文和最新算法应用到产品中。那 么,怎样才能把算法应用到产品中呢?这就是第 7 章的另一个目 的。 何谓算法? 讲述 Hatena 的服务之前,首先了解一下算法的基本思路吧。 “算法”是什么?重新来考虑一下。根据《アルゴリズムイ ントロダクション 改訂 2 版 第 1 巻 数学的基礎とデータ構造》① (近代科学社、2007 年),算法(algorithm)就是明确定义的 (well-defined)、以某个值或值的集合为输入(input)、以某个 值或值的集合为输出(output)的计算步骤。 ——引用自《アルゴリズムイントロダクション 改訂2版 第1巻 数学的基礎とデータ構造》(Thomas H.Cormen/Charles E.Leiserson/Ronald L. Rivest/Clifford Stein 著、浅野哲夫/岩野和生/ 梅尾博司/山下雅史/和田幸一译,近代科学社,2007 年)第 5 页 输入适当的值,通过明确定义的计算步骤得到输出值,这就 是算法。将要查找的数据和被查找的数据作为输入进行查找,得 到要查找的数据所在的位置,这就是“查找算法”。                                                               ① 译者注:原书为 Introduction to Algorithms, Second Edition,Thomas H. Cormen、Charles E.Leiserson、Ronald L. Rivest、Clifford Stein 著。中文 版为《算法导论》,机械工业出版社,2006 年出版。
  • 4. 第 7 章 算法实用化 145 狭义算法和广义算法 算法这个词具有狭义和广义两种含义,用途很广泛。 从数据库提取记录进行适当处理后输出成报表,对于这种平 时随手就能写出的程序,也可以说“用的是什么算法?”这时提 问者想知道的应该是处理(domain logic)的流程。这就是算法 的广义含义。 而算法的狭义含义,大多是指“针对明确定义的计算问题, 执行已定义的计算步骤”。因此,市面上的算法书并不是讲述业 务应用程序的处理逻辑,而是讲述排序、搜索、散列等计算问题 的解法的。 第 7 章的内容就是狭义的算法。 学习算法的意义 ——计算机资源有限,工程师的通用语言 CPU、内存等计算机资源是有限的,因此学习算法十分重要。 对于必须解决的问题,如何利用有限的资源解决掉,这种思考方 式也是必须学习的。 而且,与设计模式同样,算法也是工程师们的通用语言。要 用“使用散列就能解决了”这种语言来沟通,双方都需要正确理 解散列算法是什么。 学习算法显而易见的好处就是,理解算法后就(可能)可以 解决新的问题。 例如学习贝叶斯过滤器(Bayesian Filter)之后,就可以编 写数据自动分类之类的程序。利用该算法可以编写垃圾邮件过滤 器。 有了能用几 MB 的容量保存几亿条数据的数据结构,就能轻
  • 5. 146 大规模 Web 服务开发技术 松发布由于过大而很难发布的程序。如前一阵子发布的 Google 日文输入法 ① 就用 LOUDS 这种数据结构将字典数据压缩到 50MB 左右,这样就能发布巨大的字典。 而且如前所述,面对大规模数据时,算法特性会对应用程序 的性能产生巨大影响。学习算法对于把握这种感觉也很有帮助。 为什么算法知识是必要的? 计算机资源有限 工程师们的通用语言 理解算法后(也许)可以解决新问题 算法评测——复杂度记法 刚才说过,线性查找的计算量为 O(n),二分查找的复杂度 为 O(log n)。大多情况下,算法的复杂度可以这样定量评测。算 法评测一般使用复杂度记法(Order 记法)。 复杂度记法表示的含义是,当算法的输入大小为 n 时,大致 需要这么多的计算量。 花费时间与 n 的大小无关,能在固定时间内完成的处理,其 复杂度为 O(1)。例如从散列中查找数据,虽然要计算散列函数, 但散列函数计算不依赖于 n,所以复杂度为 O(1)。而散列搜索中, 给定键的值(几乎)是唯一的,因此通过键搜索值的处理也是 O(1) 也依赖于具体实现) 因此, ( 。 散列搜索整体复杂度为 O(1)②。 如前所见,线性查找要从开头开始查找,最大要查找 n 次。                                                               ① URL:http://www.google.com/intl/ja/ime/。 ② 复杂度记法中 O(1)+O(1)不等于 O(2),而是取 O(max(1, 1) = O(1)。
  • 6. 第 7 章 算法实用化 147 某些情况下只需一次就能找到,但复杂度记法并不考虑这种特 殊情况,而是表示平均值或最大值。因此记为 O(n)。二分查找 为 O(log n)。 像这样用复杂度记法表示各种算法,即可比较算法的性能。 线性查找和二分查找分别为 O(n)和 O(log n),因此二分查找的计 算量比较少①。 各种算法的复杂度记法 各种算法的复杂度记法中,下述几种计算量经常出现。 O(1) < O(log n) < O(n) < O(nlog n) < O(n2) < O(n3) … < O(nk) < O(2n) 越往右,复杂度越大。处理大规模数据时,也就是说 n 比较 大时,实用算法也就到 O(n logn)附近。再高,复杂度就会随着 n 的增加而急剧增大,经常导致计算无法结束。 感觉上而言,O(log n)比 O(n)要快很多,O(n)和 O(n log n) 的差距不是很大,O(n)和 O(n2)之间的鸿沟决定着着计算能否完 成……。 关于速度问题,如果相当复杂的算法能用 O(n2)的计算完成, 那也可以说“相当快”了,毕竟速度取决于算法中的计算本身。 例如,一般的基于比较的排序算法无论怎么优化,也不可能比 O(n logn)快,这一点在理论上可以证明。因此,排序算法能达到 O(n logn),就可以认为是高速算法。 复杂度的概念不仅用于表示计算的时间,也可以用于表示空 间。也就是说,复杂度记法不仅可以表示执行时间、操作步骤数, 还可以表示内存使用量等。                                                               ① 实际上,二分查找要求数据必须预先排序,因此并不能保证一定比线性 搜索快。
  • 7. 148 大规模 Web 服务开发技术 上面讲述了算法评测。更为详细的内容,请参考讲解算法的 书籍。 算法评测 复杂度记法 复杂度 时间复杂度(执行时间、操作步骤数) 空间复杂度(内存使用量) 纸巾能折叠几次?——O(logn)和 O(n)的差距 刚才说过,线性查找和二分查找相比,数据量变大后,计算 时间就会出现巨大差距。这里的重点不是复杂度记法本身,而是 利用复杂度记法比较算法时,对算法间差距的感觉。就是说要掌 握 O(log n)和 O(n)在 n 增大时,对于复杂度差距的感觉。 再看个身边的例子。准备一张纸巾,然后将其多次对折。到 底能对折多少次呢?第一次只需对半折叠即可,完全没问题。第 二次、第三次、第四次直到第五次应该也没问题。但第六次就比 较难折了,第七次和第八次完全折不动而不得不放弃。可能有人 会想“折 100 次肯定没问题!,而实际上 7 次就是极限了。为什 ” 么呢? 折纸所需的劳动量,依赖于要折的纸张的厚度。假设这个厚 度最初为 1mm,那么第一次折叠之后就是 2mm。两次折叠后是 3mm……不对,是 4mm。 这样,厚度是 1→2→4→8→16→32…这样增加的。可以认
  • 8. 第 7 章 算法实用化 149 为,设折叠次数为 n,计算量就按照 2n 增长。刚才看到,O(2n) 是个相当大的复杂度,因此纸巾在 n=8 时无法折叠也就可以理 解了。 另外有文章介绍过,厚度为 0.11mm 的卫生纸折叠 25 次后 会达到富士山的高度①。要折叠富士山那么高的纸……一般方法 绝对做不到吧。 对算法中指数增加、对数增加的感觉 计算量按照指数增加的算法,只需一点点数据量,计算量就 会变得庞大无比。与指数相反,只以对数增加的 O(log n)的算法, 即使数据量变得非常大,也只需一点计算量就能解决,这点也能 直观地理解吧。 在思考算法复杂度时,这种感觉是最重要的。例如要操作的 数据有 1000 万条,如果能选择对数算法,那么只需几十次计算 就可以了。相反,如果选错了算法,使用 O(n2)或 O(2n)的算法实 现的话,写出的程序即使只有几百条数据,也要浪费相当多资源。 算法和数据结构——千丝万缕的联系 纵观各种算法书籍,大多都是将算法和数据结构作为一个整 体来讲述。 数据结构就是数组、树结构等存储或表现对象数据的结构。 将算法和数据结构作为整体讲述,是因为必须依照算法中的 常用操作选择数据结构。例如,事先将数据保存在适当的树形结                                                               ① URL:http://gigazine.net/index.php?/news/comments/20100305_fold_half/。
  • 9. 150 大规模 Web 服务开发技术 构中,大多数情况下搜索会变得很简单,可以降低复杂度。 第 11 课中已经看到,RDBMS 的索引的实现采用了 B+树这 种树结构。B+树是个空间上适合外部存储的树结构。利用 B+树 保存索引,不仅能减少查找所需的操作步骤,还能将磁盘读取次 数降至最低。因此,RDBMS 的索引一般采用 B+树,同时使用 适合该数据结构的算法进行查找、插入、排序等操作。 所以说,算法和数据结构之间存在着千丝万缕的联系。 算法和数据结构 数据结构 数组、树结构、堆…… 根据算法常用操作进行选择 要根据算法常用操作来选择数据结构 复杂度和常数项——评测很重要 计算量的复杂度记法忽略了所有“常数项”。所谓常数项就 是算法实现中不依赖于输入大小,但却不得不执行的一类处理。 例如函数调用、函数返回等处理都是常数项,第一次分配变 量、if 语句分支等也是常数项。简单的实现中,常数项几乎不会 影响算法的复杂度,但在复杂的实现中,常数项就不可忽略了。 就算实现不复杂,CPU 缓存是否容易生效、分支预测是否发生 等计算机结构特点也会有影响,因此常数项可能会导致差距。 例如,例如排序算法的理论下限为 O(n log n),有几个算法 的平均复杂度能达到 O(n log n)。但是,同为 O(n log n),一般而
  • 10. 第 7 章 算法实用化 151 言快速排序是最快的。快速排序的特点使得 CPU 缓存容易生效, 这一点比其他算法好得多。这就是常数项较小的例子。 也就是说,复杂度记法适用于比较算法,但在实现时不应只 考虑复杂度。而且常数项经常取决于实现方法,因此实现时要尽 力减小常数项。 实现时要注意的优化问题 另一方面希望大家注意,实现某样东西(不仅限于算法)时, 一开始就对常数项进行优化,基本上是错误的。努力减少复杂度 为 O(n2)的算法的常数项,还不如用 O(n log n)的算法来代替,那 样改善效果更好。 说来说去,还是“评测最重要”。通过评测(benchmark)或 分析(profiling)等手段,正确找出当前程序的问题所在最为重 要。是要更换算法来改善,还是减少常数项来改善,或者是物理 资源不足要更换硬件以改善性能?务必在认真找出问题所在之 后,再设法改善。 应用算法的实际情况——简单就是美 实际上,高级算法未必是最优解,古典算法有时也不错。进 一步说,与知名算法相比,简单算法更好的情况也不罕见。
  • 11. 152 大规模 Web 服务开发技术 Hatena Bookmark 的 Firefox 扩展的搜索功能的尝试 介绍一个 Hatena 中的实际例子。Hatena Bookmark 中有 Firefox 扩展这个工具,通过它可以将 Hatena Bookmark 与浏览器 集成,十分方便。该扩展可以针对用户以前保存过的书签数据进 行增量(incremental)搜索(如图 7.1 所示)。 我们团队讨论了该搜索功能的实现方法,最后结果是,由于 增量搜索中搜索动作发生频率很高,而且又是在客户端计算,所 以计算量必须少。一些人的数据量能达到一万条以上,所以选择 了 Suffix Array。 Suffix Array 这个数据结构主要用于文本数据等的高速查 找。查找本身很快,但必须事先花费很长时间创建数据结构。而 且,要让 Suffix Array 在应用程序中达到实用程度,如何缩短这 图 7.1 Hatena Bookmark 的 Firefox 扩展的搜索功能
  • 12. 第 7 章 算法实用化 153 段时间,就成了要解决的课题。当时我们采用了刚刚发现的 IS 方法①解决。 多次尝试之后,我们用 Firefox 扩展中唯一可用的 JavaScript 语言实现了 IS 方法,但实际结合到应用程序之后并没有达到满 意的效果。尽管速度有所提高,但预处理仍然需要花费很长时间, 用户添加书签时进行的预处理会给机器造成很大负担。 经过痛苦的选择之后,我们放弃了 Suffix Array,而是使用 Firefox 扩展内部的 SQLite 功能, SQL 的 like 进行部分查找 用 (也 就是线性查找)。这会让数据量大的人搜索速度降低,但也是没 办法的事,只能妥协于这种方法了。 不过,实际完成后发现,使用上完全没有任何问题。曾经担 心过的几万条数据量的问题,也由于现代计算机的性能提高,查 找上完全没有问题……所以就这样了。 得到的经验 从这个例子中得到的经验就是,评测和估算非常重要,并且 偶尔尝试一下简单的实现也很重要。为大规模数据进行优化固然 很重要,但从本例中可见,数据较少时优化没有任何意义。而且 它也说明,通过人的感觉来推断数据量多少是不可靠的。 灵活应用第三方实现——CPAN 等 请不要忘记,大部分常用算法都有已发布的实现,供他人使 用。 以前面所说的 IS 方法为例,当时 JavaScript 上 Suffix Array                                                               ① IS 方法(Induced-Sort)是个在线性时间内对 Suffix Array 进行排序的算 法,于 2009 年提出。
  • 13. 154 大规模 Web 服务开发技术 并没有好的实现,所以不得不自己编写,但如果是 Perl,那么 CPAN 上有大量各种算法的开源实现函数库,其他语言也一样。 灵活运用这一类实现,可以缩短开发时间。话虽如此,我们 不建议在不了解其内容的情况下使用。必须适当了解它做了哪些 操作,否则就可能做出错误的选择。 例如,CPAN 上有大量压缩算法的实现。压缩也有适合不适 合之分,比如对较短文件有效的算法、花费时间很长但压缩比很 高的算法、压缩比较低但速度快的算法等,算法的特性各不相同。 为了正确选择算法,了解一些算法知识没有坏处。在 CPAN 上 搜索“Algorithm”的结果如图 7.2 所示。 相反,这些函数库的 API 有时无法满足我们的规格要求, 图 7.2 在 CPAN 上搜索“Algorithm”的结果
  • 14. 第 7 章 算法实用化 155 或者实现的功能过多。这时可以仅实现必要的地方,既能抑制工 作量,单位成本获得的效果也更高。重点就是平衡性。 通过实例加深感受 刚才看到了算法及其评测,还有一点点实际应用。认真考虑 理论和实际的平衡最为重要,大家应该多少感受到这种氛围了 吧。 专 栏 数据压缩和速度——提高整体吞吐量的思考方式 第 6 章的课程讨论了压缩。提到“压缩”,很多人想到的是 将大文件变成小文件的工具吧。Windows 的 ZIP 文件夹、GNU gzip 等。使用这些工具,能将非常大的文件压缩,但也会消耗机 器的处理能力。因此,应该会有人认为“压缩解压处理负载高, 也就会很慢”吧。 但是从吞吐量的观点来看,把要处理的数据事先压缩,多数 情况下反而更快。计算机有 CPU 和 I/O 两种负载。为了某个处 理而等待 I/O 时,该处理无法使用 CPU。事先将文件压缩,只会 造成少量 CPU 负载,但却能降低 I/O 负载。因为多数情况下 CPU 空闲而 I/O 繁忙,通过压缩可以让 CPU 负担部分 I/O 的负载, 从而提高整体吞吐量。 HTTP 的 deflate 压缩通信就是最好的例子。尽管压缩给人司 空见惯的印象,但却是个重要技术。
  • 15. 156 大规模 Web 服务开发技术 第 20 课 Hatena Diary 的关键字链接 什么是关键字链接? 博客服务 Hatena Diary(http://d.hatena.ne.jp/)支持关键字链 接,这是个很特别的功能。 该功能在前面的图 5.3(p.112)的关键字链接截图中已介绍 过。如图 5.3 所示,写博客时部分关键字会自动加上链接,链接 目标就是该关键字的解释页面。Wiki 的实现也能给 Wiki 关键字 自动加链接,这个功能与它很相似。 被 链 接 的 关 键 字 就 是 用 户 在 Hatena Keyword ( http://k. hatena.ne.jp/)上添加的关键字。本书执笔时(2009 年 8 月), Hatena Keyword 已有 27 万条以上关键字,用户每天创建的新关 键字大约有 100 个。 关键字链接功能就是将输入的文章与 27 万条关键字字典进 行匹配,将必要的地方替换成链接。链接替换操作实际上只是将 特定关键字替换成 HTML 链接标签而已,所以问题就是如何对 文章中的关键字进行文本替换。 关键字链接的例子 Hatena Diary 是博客 →<a href="...">Hatena Diary</a>是<a href="...">博客</a> 最初的实现 Hatena Diary 刚刚上线时,该功能并没有花太多功夫,简单 地采用了正则表达式,将字典中的所有单词用 OR 连接做成正则
  • 16. 第 7 章 算法实用化 157 表达式。 (foo|bar|baz| ...) 就是这种正则表达式。假设将文本放在$text 变量中,那么 把替换选项和替换字符串看作表达式的 eval 选项进行组合,变 成以下形式就可以了: use URI::Escape; $text =~ s/(foo|bar|baz)/&replace_keyword($1)/ge; sub replace_keyword { my $w = shift; return sprintf '<a href="/keyword/%s">%s</a>', uri_escape($w), $w; } 出问题了!——关键字字典越来越大 由于关键字是用户创建的,刚上线时单词数并不多,从数据 库中取出后直接创建正则表达式以实现关键字链接,这种奢侈的 处理也完全没有问题。但是,随着关键字的数量不断增加,问题 就来了。处理正则表达式要花费很长时间。最耗时的地方有两处: 编译正则表达式的处理; 用正则表达式进行模式匹配的处理。 对于 ,可以预先创建正则表达式并保持在内存或磁盘上, 即通过缓存的方法能够绕过去了。
  • 17. 158 大规模 Web 服务开发技术 对于问题 ,把完成了关键字链接的正文文本进行缓存等处 理后,开始时能绕过该问题,但要将新添的关键字反映到关键字 链接中,必须花费一定时间重新建立缓存,或者从博客服务的特 性上看,多一半文章的访问量并不大,导致缓存很难生效,所以 该问题并没有完全解决。 用模式匹配实现关键字链接的问题 随着服务的运营,关键字的单词数超过了 10 万条,而且 Hatena Diary 增加的整体访问量使得关键字链接的处理次数也增 加了许多,系统终于承受不住了。 关键字链接计算耗费大量时间的原因在于正则表达式的算 法。详细情况可以参考正则表达式的书籍,简单来说,正则表达 式的模式匹配是基于自动机实现的。而且,Perl 的正则表达式实 现采用的是 NFA(Nondeterministic Finite Automata,非确定型有 穷自动机)。不仅 Perl,实用的语言大多采用了 NFA 引擎。 像(foo|bar|baz)这种模式匹配,NFA 会使用一种简单的方 法,从输入的开头尝试匹配,失败的话就尝试下一个单词,如果 又失败了,就再次尝试下一个单词。因此, 不匹配就尝试 bar, foo 如果还不匹配,就尝试 baz……如此循环下去。因此,计算量与 关键字的个数成比例。 服务刚开始时,关键字个数很少,相应的计算量也很少,所 以没有发生什么问题。 从正则表达式到 Trie——改变匹配的实现方式 为解决模式匹配带来的复杂度问题,我们把实现方法从正则 表达式变成了 Trie 树。
  • 18. 第 7 章 算法实用化 159 Trie 入门 Trie 这种数据结构是树结构的一种。它的特点是,用树结构 将搜索对象数据的公共前缀综合到一起。看个例子就明白了。例 如关键字"ab"、"abcde"、"bc"、"bab"、"d"的 Trie 如图 7.3 所示。 为了易于理解,这里给各个节点加上了编号。树的边加上字 母,遍历边相当于进行查找。例如依次遍历 0→1→2,就是从根 节点开始依次遍历'a'边、'b'边。而节点 2 上的信息表明'ab'是路径 的终端。沿着'a'→'b'→'c'遍历能到达 8 号节点,但 8 号节点上没 有终端信息,因此'abc'不包含在 Trie 中。 从关键字中可以看出,"ab"和"abcde"拥有'ab'这个公共前缀, "bab"和"bc"拥有'b'这个公共前缀。Trie 的特点就是将公共前缀综 合到一起,以避免浪费。 Trie 用树结构高效存储字符串集合 其树结构可以将搜索对象数据的公共前缀综合到一起 a b c d e 10 2 0 1 8 9 abcde ab b c 3 4 a b 6 d 5 bab 7 b 图 7.3 Trie 结构(关键字:"ab"、"abcde"、"bc"、"bab"、"d")
  • 19. 160 大规模 Web 服务开发技术 Trie 结构和模式匹配 把 Trie 结构当作字典进行模式匹配,其计算量要比正则表 达式少得多。将输入文本输入到 Trie 中,遍历它的边,如能找 到终端,就可以认为该单词存在。与(foo|bar|baz)的正则表达式 相比,公共前缀只需搜索一次即可。 考虑 hogefoo 这个单词。用该单词对包含 foo、bar、baz 的 Trie 结构进行遍历。由于不包含 h 字符串,所以不会匹配。接下 来遍历 oge、ge、e 也一样。然后,用 f 遍历 Trie 时会发现,后 面有 oo,因此 foo 能匹配。但计算量不会超过 hogefoo 的长度。 而使用正则表达式进行模式匹配时,首先要用 h 与 foo、bar、 baz 等比较是否匹配,然后用 o 做同样的操作,如此反复,花费 的时间与关键字数量呈比例,因此关键字词典越大,两者的差距 就越明显。 Aho-Corasick 算法 实际上,在改善 Hatena Diary 时,我们没有用 Trie 结构进行 模式匹配,而是采用了更为高速的 Aho-Corasick 算法。 Aho-Corasick 算法是 Alfred V.Aho 和 Margaret J.Corasick 于 1975 年在论文《Efficient String Matching: An Aid to Bibliographic Search》中提出的古典算法,根据字典创建执行模式匹配的自动 机,以实现在线性时间内对输入文本进行计算。这种高速算法的 复杂度与字典大小无关。 Aho-Corasick 算法利用 Trie 进行模式匹配,但它添加了返回 的边,匹配失败时可以沿着边返回。如果图示的话,如图 7.4 所 示。 例如将"babcdex"输入图中的 Trie,那么找到 bab 的同时也找 到了 ab。因此,如果能在查找到 bab 之后不要返回开头的节点 0,
  • 20. 第 7 章 算法实用化 161 而是直接移动到节点 2 的话,就能立即找到 ab。而将“节点 6 的下一个是节点 2”这种路径添加到 Trie 中的预处理,就是 Aho-Corasick 算法的主要原理。至于添加这些路径的方法,只需 从 Trie 的根节点进行广度优先搜索,找到合适的节点就能建立, 这种算法也是众所周知的。 2005 年时我们并没有想到利用 Aho-Corasick 算法进行关键 字链接,后来是工藤拓告诉我们的。工藤拓是きまぐれ日記① (神 经质的日记)的博主,语素分析库 MeCab②的开发者,现在在 Google 参与 Google 日文输入法相关开发。其背景是,实现语素 分析引擎,要将输入的文章与字典中的所有单词进行模式匹配, 这与关键字链接的处理几乎是一样的,而且在自然语言处理中, 这种任务使用 Trie 已成为惯例。 另外,用 Aho-Corasick 算法实现关键字链接的课题请参见 第 8 章。 a b c d e 2 10 0 1 ab 8 9 abcde b c 3 4 bc a b 6 d 5 bab 7 b 图 7.4 Aho-Corasick 算法                                                               ① URL:http://chasen.org/~taku/blog/archives/2005/09/post_812.html。 ② URL:http://mecab.sourceforge.net/。
  • 21. 162 大规模 Web 服务开发技术 Aho-Corasick 算法 复杂度与字典大小无关的高速算法 根据字典创建自动机进行模式匹配,对输入文本实现了线 性的计算时间 换成 Regexp::List 采用 Aho-Corasick 算法后,毫无悬念地解决了关键字链接的 复杂度问题。之后的一段时间采用了我们自己实现的 Aho- Corasick 函数库,但后来又换成了 Regexp::List①这个 CPAN 库。 Regexp::List 为小饲弹②开发的正则表达式函数库,可以生成基于 Trie 的正则表达式。 也就是说,该函数库并非将巨大的正则表达式按照我们最初 的实现那样用 OR 连接,而是用 Trie 对正则表达式进行优化。利 用该函数可以将 qw/foobar fooxar foozap fooza/ 这个正则表达式变换成如下的正则表达式: foo(?:[bx]ar|zap?) 它可以综合公共的前缀和后缀,利用优化后的正则表达式进 行模式匹配,比用 OR 链接所有单词的尝试次数少得多。减少的 原因与上述对 Trie 的解释相同。 利用 Regexp::List 不仅能抑制计算量,而且还能作为正则表 达式使用。最初简单地采用正则表达式实现时,可以组合各种正                                                               ① URL:http://search.cpan.org/dist/Regexp-List/。 ② 译者注:小饲弹(Dan Kogai),日本开源软件开发者。文本编码转换 Perl 模块 Encode.pm 的作者。博客为 http://blog.livedoor.jp/dankogai/。
  • 22. 第 7 章 算法实用化 163 则表达式的选项,还能使用 Perl 语言特有的各种功能,拥有丰 富的灵活性。但改成 Aho-Corasick 算法就失去了这个优势,也 谈不上灵活性了。采用 Regexp::List 可以同时拥有两者的优势。 关键字链接的实现、变迁和考察 像这样,关键字链接的实现经历了以下过程:巨大的正则表 达式→Aho-Corasick 算法→Regexp:List。从这个过程中可以看出 一些问题。 最初的简单实现也有一定功效。当初因为简单而采用了 最简单的正则表达式,实现所需工作量也很少,实现的 灵活性也很高。因此可以很容易地尝试 Hatena Diary 的关 键字链接的各种功能,也能根据用户的希望进行修改。 另一方面,数据量增大后问题就显现出来了。解决该问 题需要本质的解决方案。通过缓存等表面的修改,可以 从某种程度上绕过问题,但最终必须解决算法上的根本 问题。要看清这一点,必须像算法评测中讲过的那样, 从复杂度观点去寻找问题。 一开始就采用最优算法并不一定正确,数据量较小时采用简 单方法反而效果较好,必须找出本质问题的解决方法以备数据量 增大……等等,这些面向大规模数据的算法实用化中的各个篇 章,都浓缩在了关键字链接之中。 更不用说,这段经历导致了 Hatena 重新审视自己的技术水 平。
  • 23. 164 大规模 Web 服务开发技术 第 21 课 Hatena Bookmark 的文章分类 什么是文章分类? 最后以 Hatena Bookmark 的文章分类为例,看看如何使用特 定算法解决新问题。前面的图 5.4 p.113) ( 的截图已介绍过,Hatena Bookmark 提供自动分类功能,将新文章根据文章内容自动分类, 并按类别呈现给用户。 例如,可以将文章分类成“科学·学问”和“电脑·IT”两 类。其他还有“政治·经济”“生活·人生”等共 8 个分类。用 、 户向 Hatena Bookmark 提交新文章后,Hatena Bookmark 系统就 会通过 HTTP 获取文章内容,根据文本内容进行分类,以判断其 类别。 用贝叶斯过滤器判断类别 判断类别需要用到贝叶斯过滤器。课程开头介绍过,贝叶斯 过滤器可以用于在垃圾邮件过滤器等,所以应该有很多人知道这 个名字吧。 贝叶斯过滤器接收文本作为输入,并使用朴素贝叶斯(Naive Bayes)算法从概率上判断文章属于哪个类别。其特点是判断未 知文章时,要利用以前已分类数据的统计信息进行判断。必须事 先由人提供“正确数据”,告诉贝叶斯过滤器哪篇文章属于哪个 类别,让它“学习”,最后才能自行进行正确判断。 这种事先提供学习数据,使计算机能针对未知输入进行某种 计算的处理,是“机器学习”领域的研究成果。另外,像贝叶斯
  • 24. 第 7 章 算法实用化 165 过滤器这种根据已有文章——即模式——进行分类,属于“模式 识别”领域。灵活应用机器学习、模式识别领域的算法,不仅能 实现自动分类,还能开发别具一格的软件。 机器学习和大规模数据 像贝叶斯过滤器一样,许多机器学习任务都需要正确数据。 提供正确数据后,学习引擎就能达到甚至超过人类解决特定问题 的准确度。 虽然贝叶斯过滤器并不需要如此大量的正确数据,但机器学 习任务中,数据量越大,准确度就越高的例子并不罕见。从可扩 展性的观点来看,大规模 Web 服务拥有的大量数据虽然给运维 带来了麻烦,但它们却是研发领域求之不得的数据。 Hatena Bookmark 的相关条目功能 Hatena Bookmark 具有“相关条目”功能,该功能可以将类 似于某篇文章的关联信息提供给用户。图 7.5 就是 Hatena Bookmark 针对 Google Chrome 扩展发布的文章给出的相关条目, 可见它提取的有关 Google Chrome 扩展的话题还挺准确。 关联条目功能,利用了 Hatena Bookmark 中用户手动输入的 4000 多万条“标签”这种分类用文本,并使用文章推荐算法实 图 7.5 Hatena Bookmark 的关联条目
  • 25. 166 大规模 Web 服务开发技术 现。文章推荐算法的实现采用了株式会社 Preferred Infrastructure① 的引擎。 关联条目功能利用几千万条标签数据取出几条关联文章,这 对于人类几乎是不可能的任务。像这种从大量数据中取出有意义 的数据,也许只有拥有大规模数据的 Web 服务才能做到。 大规模数据和 Web 服务——The Google Way of Science 说起大规模数据和 Web 服务,就不得不提 Google。大家在 用 Google 搜索时肯定注意到,它有个“您是不是要找”功能, 针对错误的查询推荐可能正确的查询。这个“您是不是要找”功 能,就是将以前的用户搜索记录作为正确数据,学习错误时应当 如何改正之后,提示出正确数据。Google 非常善于利用收集的 大量数据并给出反馈。 其实 Google 这个搜索引擎就是以大量 Web 文档为输入,从 中提取出有意义的文章,他们自然会深入研究该领域。 想必大家都知道,最近 Google 利用它的大规模数据,在该 领域进行了深入研发。Google 拥有全球规模的数据量,利用前 所未有的数据量做出未知的研究成果,因而受到关注也并不意 外。 “The Google Way of Science”②是以前 Wired magazine 的 Kevin Kelly 的专栏,以“用大量数据和应用数学取代其他一切 工具”为主旨,考察 Google 的动向。例如有个故事说, “Google 开发了翻译引擎,对输入模式和词语转换进行学习后,能翻译大                                                               ① URL:http://preferred.jp/。 ② URL:http://www.kk.org/thetechnium/archives/2008/06/the_google_way.php, 中文意为“Google 式的科学” (日文 URL:http://memo7.sblo.jp/article/ 。 25170459.html)
  • 26. 第 7 章 算法实用化 167 量输入数据,但中文翻译程序的开发者们并不懂中文。”也就是 说,即使不知道理论上“何为正确”,利用应用数学(多数情况 下使用的是统计领域),为机器学习提供前所未有的海量数据, 就能从那个黑盒子中得到正确结果。这个结论足以颠覆以前的科 学常识。 这个专栏很有意思,而且从今后的研发本质和潮流来看也相 当有趣,强烈推荐读一读。 贝叶斯过滤器的原理 有点跑题了。回到贝叶斯过滤器的话题上。这里不讲贝叶斯 过滤器的实现,只是简单介绍一下算法运行的原理①。 刚才说过,贝叶斯过滤器的核心是朴素贝叶斯算法②。朴素 贝叶斯是基于贝叶斯定理的算法。 用朴素贝叶斯进行类别判断 接下来会讲一些公式。看不太明白也没关系,随便看看就行 了。用朴素贝叶斯进行类别判断,就是给定某篇文档 D,求出该 文档所属概率最高的分类 C 的问题。也就是说,给定文档 D, 求出属于分类 C 的条件概率: P(C|D)                                                               ① 深入到实现方式的说明,请参见我在《WEB+DB PRESS》(Vol.56)上 连载的《実践アルゴリズム教室》(实践算法学堂)的“第1回:ベイ ジアンフィルタ開発に挑戦” (第一回:挑战贝叶斯过滤器开发)。有兴 趣的人一定要读一读。 ② Hatena Bookmark 的分类使用的是由朴素贝叶斯算法进一步发展而来的 Complement Naive Bayes 算法。
  • 27. 168 大规模 Web 服务开发技术 设多个分类中概率最高的分类为 C,即为最终选择的分类。 直接计算条件概率 P(C|D)比较困难,可以利用“贝叶斯定 理”将其变成可计算的公式。与其说贝叶斯定理云云,倒不如用 众所周知的数学理论对概率公式变形。变形结果为 P(C|D) = P(D|C)P(C) / P(D) 只需求出右边的各个概率 P(D|C)、P(C)、P(D)即可。 请注意,类别判断所需的并不是具体的概率值,只需比较各 个分类,求出概率最大者即可。那么,分母 P(D)为文档 D 条件 的概率,对于所有类别而言该值都相同,比较时可以忽略。 因此只需考虑以下两个: P(D|C) P(C) 要判断类别,只需从学习数据的统计信息中计算出这两个值 即可。求这两个值实际上非常简单。 P(C)是某个分类的出现概率,只需事先保存学习数据被保存 到各个分类中的次数,即可计算。 至于 P(D|C),可以把文档 D 看成任意单词 W 连续出现的结 果,那么 P(D|C)可以近似成 P(W1|C) P(W2|C) P(W3|C)…P(Wn|C)。 这样只需将文档 D 分割成单词,求出每个单词被分类到各个类 别中的次数,即可求出 P(D|C)的近似值。
  • 28. 第 7 章 算法实用化 169 贝叶斯公式 P(B|A)=P(A|B) P(B) / P(A) →贝叶斯定理证明了上述概率公式成立。 直接求 P(B|A) ——即事件 A 发生之后事件 B 发生的概率—— 困难时,这个公式就能派上用场。利用贝叶斯定理变形后,要求 出 P(B|A),只需求出 P(A|B)、P(B)和 P(A)即可。 如正文中所示,与其他值比较时,P(A)通常可以忽略,所以 最后只需求出 P(A|B)和 P(B)即可。 轻而易举实现类别判断 最后结论是,只需给出正确数据,并保存正确数据被使用的 次数,以及各个单词的出现次数,然后通过朴素贝叶斯算法计算 概率,就能判断出类别。其他数据可以全部抛弃。 Hatena Bookmark 现在保存的文章数据超过 2000 万条。而 且,邮件过滤器也要对每天收到的邮件进行分类。我们已经看到, 朴素贝叶斯算法只需保存一部分正确数据①,再保存一部分数据 即可,即使面对大规模数据,引擎本身也十分精炼。而且,分类 判断时,只需根据部分正确数据进行(对于计算机而言)简单的 概率计算——也就是四则运算,因此处理速度也很快。 一说到“分析大量文章的内容,自动判断各个文章的分类”, 会感到根本摸不着头脑,但像这样解释了算法的原理之后,就会 觉得实现相当简单。                                                               ① 感觉上,Hatena Bookmark 的 8 个分类只需 1000 条左右正确数据,即可 达到实用程度。
  • 29. 170 大规模 Web 服务开发技术 算法实用化之路——Hatena Bookmark 的实例 贝叶斯过滤器的原理出乎意料地简单,实际实现时,主要部 分只需 100~200 行左右的脚本语言即可完成。算法本身的实现 很简单。 下面以 Hatena Bookmark 为例,简单列举一下将贝叶斯过滤 器实现的类别分类引擎融入产品所需的其他工作。 分类引擎是用 C++开发的。需要将引擎变成网络服务器。 编写 Perl 客户端,与该服务器通信并获取结果。由 Web 应用程序调用。 为了定期备份学习数据,要给 C++引擎添加数据转储和 加载功能。 人工准备 1000 条学习数据。这是必须由人努力完成 的…… 实现统计,以跟踪判断精度是否足够。将统计画成图表, 以进行调优。 考虑冗余化,建立 standby 系统。自动切换功能会消耗很 多工时,因此只需能从备份系统中加载数据就可以了。 在 Web 应用程序上准备用户界面。 差不多就这些。是不是觉得工作量很大啊。 用 C++编写引擎也是导致工作量稍稍变大的原因,但即使 用脚本语言编写,实现服务器程序等工作也不会改变。另外,将 引擎变成服务器以及与 Perl 的 API 交互上,采用了 Apache Thrift① 这个多语言 RPC 框架。 实际操作中要考虑的问题很多 这里并不是想发牢骚,只想告诉大家,算法实用化的路上,                                                               ① URL:http://incubator.apache.org/thrift/。有篇较老的文章讨论过 Thrift, 位于 《WEB+DB PRESS》 (Vol.46) 我的连载 《Recent Perl World——Thrift で多言語 RPC……C++でサーバ、Perl でクライアント》 (Recent Perl World——用 Thrift 实现多语言 RPC……C++写服务器, 写客户端) Perl 。 有兴趣的人可以参考。
  • 30. 第 7 章 算法实用化 171 要考虑的实际问题层出不穷。比如本例,需要另外实现服务器程 序时需要特别注意。研发性质的开发,只要核心部分的原型能正 常运行就欢欣鼓舞了,但距离实际应用还有许许多多工作。在开 发现场,维护这些程序的正常运行,以及事先预测工作量等,都 十分重要。 防守姿态和进攻姿态——从文档分类功能说开去 上面介绍的机器学习、模式识别、数据挖掘等方法的利用目 的,都是从大量数据中提取出有意义的数据,以紧凑方式持有大 规模数据的“特征”以备稍后使用。 同样是算法,针对大规模数据进行排序、搜索、快速压缩等 算法多用于解决已出现的问题,是“防守”姿态的算法;相反, 机器学习、模式识别等积极地利用大规模数据,使用处理结果给 应用程序增加附加价值,是“进攻”姿态的算法。 储备已有方法 无论是防守姿态还是进攻姿态,在学习面向大规模数据的算 法的路上,将一定程度的已有方法作为知识储备是非常重要的。 如果不了解 Trie 是何种数据结构、具有何种特性,就不会想到 在关键字链接中使用 Trie;不理解贝叶斯过滤器的原理,文档自 动分类的灵感也不可能出现。 此外,如前所述,从实现算法到实用化,还有大量必要的附 加工作要做。 第 7 章的课程占用的篇幅很长。通过该课程,希望大家能切 实体会到面对大量数据时应如何选择算法、应用算法。
  • 31. 172 大规模 Web 服务开发技术 专 栏 拼写错误改正功能的制作方法 ——Hatena Bookmark 的搜索功能 课程中间我们提到了 Google 的“您是不是要找”功能。课 程正文中也说过,Google 的搜索查询补全功能应该是将搜索引 擎日志作为正确数据提供给学习引擎实现的。 那么, 如果没有大量日志的话, 编写这种程序是不是困难? 即使没有日志,只要有一定规模的正确数据,也就是说,只要有 字典,用别的方法也能实现。 将某个特定字典数据作为正确答案 去修正错误答案,也就是所谓的拼写错误改正功能。 Hatena Bookmark 的搜索功能就支持这种简单方法实现的拼 写错误改正功能(如图 F.1 所示) 。 拼写错误改正功能的实现方法如下。 正确数据采用 Hatena Keyword 的字典,它拥有 27 万条正 确数据 计算用户输入的搜索查询与字典中的语句之间的编辑距 离,定量衡量错误程度 以一定的错误程度为基准, 从字典中找出某个单词群作为 候补正确答案 将 的候补正确答案以 Hatena Bookmark 的文章中的单词 使用频率为基准,按照正确的可能性排列 将使用频率最高的单词作为正确答案,提示给用户 基本上是这个流程, 就是从字典中查找与输入相似的单词并 推荐给用户。下面分别仔细地看看每一步。 图 F.1 Hatena Bookmark 的拼写错误改正功能
  • 32. 第 7 章 算法实用化 173 27 万条正确答案的字典——将 Hatena Keyword 作为正确 数据字典使用 这个拼写错误改正程序必须知道什么是正确答案。正确数据 采用了 Hatena Keyword。如果使用仅包含地名的字典,就变成 了地名改正引擎,使用仅包含餐厅名称的字典就成了餐饮改正引 擎,很有意思。 如果无法自己准备通用字典,可以下载 Wikipedia 等的数据, 用它包含的单词作为字典也可以。 计算搜索查询与字典中语句的编辑距离,定量衡量错误程度 所谓编辑距离,就是把一个单词变成另外一个单词所需的编 辑(插入、替换、删除)次数,用它可以定量衡量单词之间的距 离。 看个例子更容易理解。 (伊藤直哉,伊藤直也)→1 (伊藤直,伊藤直也)→1 (佐藤直哉,伊藤直也)→2 (佐藤 B 作,伊藤直也)→3 各个组合的编辑距离如上所示。可见,编辑距离为 1 的单词 相似程度的确比编辑距离为 3 的高。 众所周知,编辑距离可以用动态规划算法简单、高速地实现, 是动态规划解决问题的代表例。Hatena Bookmark 采用的是由普 通的编辑距离——Levenshtein 距离——发展而来的 Jaro-Winkler 距离。Jaro-Winkler 距离这种定量方法对于靠前的错误单词有较 高的惩罚,这是因为姓名中姓氏经常出错,而名字却不容易写错。 Jaro-Winkler 距离就由这种直感而来。 以一定的错误程度为基准,从字典中找出某个单词群作为 候补正确答案 这样,比较输入查询与字典中的单词的编辑距离,获得编辑 距离较小的单词一览。但是,字典中有 27 万条单词,应当避免
  • 33. 174 大规模 Web 服务开发技术 全部比较。 我们采用的数据结构,可以先创建了字典的 n-gram 索引, 仅仅取出与输入语句的 bi-gram 重叠度高的单词。看看图 F.2 就 很容易理解。 用这种数据结构,可以预先缩小比较对象,之后再逐个计算 编辑距离。 将候补正确答案以文章中的单词使用频率为基准,按照正 确的可能性排列 然后将计算出的编辑距离较小的作为候补答案,那么编辑距 离都是 1、2 这种跳跃的值,因此经常会得到多个候补。输入伊 藤直弥,就会得到这些正确答案候补: 伊藤直也 伊藤直哉 伊东直也 哪个才是正确答案呢? 一个方法是选择搜索空间中出现频率最高的单词作为正确 答案。Hatena Bookmark 就是这样做的。我们采用文档频率 (Document Frequency)作为“出现频率最高”的标准。文档频 用户 在 n-gram 索引中匹配两次的单词 输入 也就是与输入“重叠”多的单词 图 F.2 用 n-gram 索引限定修正候补
  • 34. 第 7 章 算法实用化 175 率就是特定单词在 Hatena Bookmark 中出现在多少篇文章中的 次数。Hatena Bookmark 的其他功能也用到这个数值,因此该值 被保存了下来,用它作为判断依据。 搜索伊藤直哉而不是伊藤直也的人当然存在,但多数情况下 这种方法是好用的,这就是所谓的启发式(heuristic)吧。 将使用频率最高的单词作为正确答案提示给用户 通过上述流程就能找到可能正确的单词,将其作为修正的候 补答案提示给用户。实际上是将 Jaro-Winkler 距离和文档频率相 乘作为分数,然后显示分数大于一定标准的答案。这样,那些不 太像是正确答案的就不会被提示了。 该方法的详细实现以及代码刊登在《WEB+DB PRESS》 (Vol.51)上我的连载《Recent Perl World——第 19 回:スペル修 正プログラムを作る》(Recent Perl World——第 19 回:创建拼 写修正程序)上,推荐阅读一下。 当然,从搜索引擎的目的上来看,这种方法实现的拼写修正 功能的有效性并不高,老实说,与 Google 的功能相比,只能期 待这种方法作出些许改善。毕竟,它只能修改英文单词拼写错误 等某种程度上具有正确答案的内容。搜索查询中经常会输入网 络流行语等意想不到的单词,因此以搜索日志等大量“随时更新 的正确答案”为基础进行计算更恰当。