「集合知プログラミング」がなぜこれほど賛美されているのだろうか?

オライリーから出ている「集合知プログラミング」は2007年8月に出版された"Programming Collective Intelligence"の訳本です。私もAmazonで目次をみて、これはいい本であろうと思い、この洋書を結構早い段階で手に入れました。当時私はPythonを触ったことがなかったのですが、この分野はある程度詳しいつもりだったので、Python初学者というスタンスでまずは斜め読みをしました。確かに良さそうな本だと思いました。しかし、Python初心者として実際に本に沿ってコーディングを始めるとそのコードや実行例のひどさに愕然としてしまいました(まぁそんなに大袈裟にいうことでもないでしょうが・・・)。

本の通りにコーディングをしても、実際に同じような結果が得られません。すぐに誤植と分かるものも多いので、計算アルゴリズムの間違いは直せたが、Pythonの使い方の部分での間違いには苦労させられました。また、動的に日々変わるページの解析ならまだしも、静的な数値の計算も本の通りに再現できない。もしくは、本の通りにコーディング・実行しても、エラーになってしまう。悔しいので頑張って本家の正誤表(http://oreilly.com/catalog/9780596529321/errata/)などを見ながら、コーディングしていった。ある意味Pythonの勉強になったが、ちょっと学部生向けの教科書には出来ないと思いました。正誤表もUnconfirmed error reports and comments from readersのみが充実しているので、自分で理解が出来るコメントを取捨選択して組み上げていった記憶があります。その時は2章までで挫折して、残りの章は読み物として読みました。読み物としては確かにざっと見た感じ良いのだが、肝心のアルゴリズム部分は説明の詳細は省かれているところが多くて、ライブラリの使い方のみが書かれていたりする。サポートベクターマシンが使いたいと思い9章を読んでもlibsvmの使い方が書かれているだけでした。ちょっとだけショックでした。

そして、今年7月に訳本が出たのです。当然あまり期待していなかったのですが、いろんなところのBLOGで褒め称えているので、興味が出てきました。きっと誤植もなくなり、色々と改善されたのだと思い学部生の教科書にしようと購入しました。・・・ショックでした。何も変わっていないのです。なぜこの本が絶賛されているのだろう?と不思議でしかたありませんでした。本家のコメントを見てもやはり評価が分かれています(http://oreilly.com/catalog/9780596529321/)。

僕が見たところ、日本のBLOGでは割と絶賛しているものが多いですね。で、なぜこういうことが起きているのかを考えたのですが、この本の対象者には3パターンくらいあるかなぁと思いました。

まず、プログラミング経験者(Python経験者含む)でいろんな集合知プログラミングを経験したいと思っている人。この場合は、プログラミング言語をある程度扱えるので(特にPython経験者なら)、アルゴリズムが分かれば実装できます。つまり、手っ取り早くアルゴリズムを身につけるための学習本として購入する人。しかし、肝心の計算アルゴリズムに関しては特定言語のライブラリに依存して説明していたりするので、完全にこの本だけで集合知プログラミングが身につく訳ではない。しかし他の言語で書いている人たちは、アルゴリズムにも長けている人が割合として多いので、楽しそうにプログラミングをしているBLOGをみます。こういう形ならこれで全然ありだと思います。後半の章は特定のライブラリに依存している書き方なので、大変そうですが・・・。

次に、単純にPython触ってみたいなぁと思っている人。この中にはアルゴリズムに関する知識を持っていると誤植に気づくので直せる。しかし彼らにしても、Pythonの型や文法などの初歩的な所が分かっていたとしても、それ以外の部分でどうしてもつまづいてしまう。なんの説明なしにいきなりreload()を使いはじめたときは一瞬面を喰らいました。コードの主要部分以外は説明がないのは、prefaceにも書いてある通り、同じくオライリーから出ている「初めてのPython」を通読している事を前提としているので、しょうがないのかもしれないです。ですが、それが結果としてハードルを上げてしまいましたね。中級者以上はまだしも初級者にはちょこっとだけ辛い形になっているような気がします。

最後に、プログラミングもアルゴリズムも分からない初学者が、魅力的なテーマを沢山扱っている本だから、手っ取り早く教科書として買うパターン。まぁ、挫折するでしょう。プログラムのミスなのか、計算アルゴリズムのほうが間違っているのかの判断出来ません。この人達を対象にするのなら、ライブラリの使い方のみに言及して、手っ取り早く結果が出せることに重視した方がよかったかと思います。各アルゴリズムの強力さを知って興味を持ったら、それぞれのテーマに関してのみを扱う書籍へと向かわせればいいのですから。

とにかく僕はどの3パターンの人でも、ある程度褒めることはあっても絶賛はないだろう、とそう思いました。にもかかわらずこの本に対する賛美が多いのはきっと、中身の斜め読みと目次の豪華さに目がいってしまい、これはいいと思ってしまった第4のパターンの人たちがほめている結果だと思います。

よく書評をされる方を見かけますが、本当に通読しているのか疑問です。それを鵜呑みにするほうも責任があるとは思いますが、書評はあまり信用せずに参考程度にとどめるのがいいかなぁと思ってしまいました。出来ることなら書評が沢山集まったポータルサイトか何かがあればまだ判断するのに丁度いいのですが、少なくとも自分の手に取って見ないとやっぱり分からない事はとても多いのです。まる。折角なので日記の最後のほうに個人的な正誤表を載せました。

英語版と日本語版の比較

この本を持っていない人が、どのくらいこの本に間違いが多いかを知る方法がある。Google先生に聞けば簡単に出てきてしまうので、ポイントしておいても構わないでしょう。ここに英語の全文pdfが落ちています(http://1a26.com/pdf/Programming/Programming%20Collective%20Intelligence.pdf)。一方、日本語版についてはサンプルPDF(はじめに、2章、35ページ、1MB)が提供されています(http://www.oreilly.co.jp/editors/archives/000188.html)。

この二つを使いながら比較をすることが出来ます。英語版が出たのが日本語版が出るほぼ一年前で、かつ、すでに本家の正誤表に載っているものが殆ど反映されずに日本語版が出てしまったことは、悲しいです。オライリーを信じていただけにショックです。

また注意したいのは英語版ではあっているのに日本語版で間違っている場合と、日本語版ではあっているのに英語版で間違っている場合もあるので、色々気を付けたいところです。

個人的な正誤表 2章

とはいえ、個人的にこの本で扱っているテーマは非常に面白く出来れば学部生相手の教科書に使いたいと思うので、読んで直せた部分だけでも個人的な正誤表を作ってみようと思います。他にも教科書に使っている人がいるのであれば、共有した方がよりいいでしょうというくらいな気持ちで載せてみます。まずは2章からでも始めてみます。

個人的にはpythonインタープリタを起動し、対話的に代入してみたり関数呼んでみたりする場合は、pythonよりもipythonで行った方が楽だと思います。関数名とか変数名はTabで補完出きるし、上矢印で過去実行した文が呼び出せたりするので、打ち込む手間が省けますから。教科書として本に沿っていくと反復練習が発生するので。あと、オライリー本家にこの本で入力することになるソースコードやデータファイルがダウンロード出きるので、それもあわせて手元において勉強するのがベストでしょう。

それでは、間違えを載せておきますね。

p.9 実行部分

で、間違いはp.8の通りにコードを打ち込んでいるのなら、

critics['Toby']['Snakes on a Plane']=4.5

と代入するのはなんか意味ないですよね。すでにそのディクショナリのそのキーの値は4.5なのですから。
次の行で

critics['Toby']

の値を見たなら

{'Snakes on a Plane': 4.5, 'Superman Returns': 4.0, 'You, Me and Dupree': 1.0}

となるべきです。本ではSuperman Returnsがなぜか消えてますね。

p.10 実行部分

TobyとLaSalleの距離を求める時の彼等の座標がいきなり整数値に丸めらてしまっています。しかも、Tobyは(1,4.5)でLaSalleは(2,4)なので、あえて丸めるなら

sqrt(pow(5-4,2)+pow(1-2,2))
1.4142135623730951

となるべきです。本だと座標の値がずれていますよね。
さらに厳密にいえば、

sqrt(pow(4.5-4,2)+pow(1-2,2))
1.1180339887498949

でしょうね。

p.11 上の実行部分

0で割ることが起きるのを防ぐコード部分。これもp.10の続きなのでこれも厳密には間違い。

1/(1+sqrt(pow(4.5-4,2)+pow(1-2,2)))
0.47213595499957939

となるべきかな。

p.11 真ん中のコードを紹介している部分

sum_of_squares=sum([pow(prefs[person1][item]-prefs[person2][item],2) 
                    for item in prefs[person1] if item in prefs[person2]])

の部分は

sum_of_squares=sum([pow(prefs[person1][item]-prefs[person2][item],2) 
                    for item in si])

とした方が理解が出きると思うのですが。Python初学者にリスト内表記を強要している時点で分かりにくいですけどね。Python分かってくるとリスト内表記にしたくなりますが。でも、アルゴリズムを教えたいのなら、あえてネストにしてもいいと思うけど。まぁ、とりあえずリスト内表記にするなら、こっちの方がいいかなと。

さらに、その下に間違いがあります。さっき上で例題を出してまで強調したのに、肝心のコードが間違ってますね。

return 1/(1+sqrt(sum_of_squares))

が正解。

p.11 下の実行部分

ここでいきなりreload()が出てくる。そもそも著者はどのようにコードを編集して、Pythonインタープリタと対話させること想定しているのだろう。どちらにしても、いつrecommendationsをimportしたのか?本には一度も出てきてない。エディタを開きながら本のコードを記入して、別のウィンドウかシェルでpythonを実行するのを想定しているとするならば、始めてモジュールを利用するのだから、

import recommendations
recommendations.sim_distance(recommendations.critics,'Lisa Rose','Gene Seymour')

あるいはcriticsディクショナリとsim_distance関数は両方使うので最初から

from recommendations import *
sim_distance(critics,'Lisa Rose','Gene Seymour')

かなぁと思うのですが?ちなみに前述した通り距離関数にsqrtが入っていないので、結果は本の値ではなく

0.29429805508554946

となるのが正しいはずです。
とにかく、以降のreload()は自分がどのように実行しているかによって、適宜読み替えるのが吉。

p.17 実行部分

二回目のgetRecommendations()は距離関数をsim_distanceに戻したので、前述した箇所を変えていないと値が違っているはずです。正しくは、

getRecommendations(critics,'Toby',similarity=sim_distance)
[(3.457128694491423, 'The Night Listener'),
(2.7785840038149239, 'Lady in the Water'),
(2.4224820423619167, 'Just My Luck')]

じゃないでしょうか。

そもそも2.4と2.7.2は

推薦度の考え方が怪しいような気がします.時間を見つけて検討したい.とりあえず本にあるコードの通りやりたいのなら上記の誤植を直せば試せます.

del.icio.usAPI関連とMovieLensのデータセット

del.icio.usAPIを使った実行部分は今現在のデータをとってくるのでもちろん本の通りの実行結果にはならないので適宜確認しながら行いましょう。
p.28のコードですが、当たり前すぎますが、データセットへのpathを指定していますので、自分がどのディレクトリにMovieLensのデータを展開したのか確認してからコードを変えましょう。

まとめ

2章はpdfがあって見ることが出きるので揚げ足を取るような細かい指摘をしてしまいました。一個一個直すのも面倒だろうので、本家からダウンロードできるソースコードのコメントをはずして間違い箇所を修正したものを以下に貼り付けます。

from math import sqrt

critics = {'Lisa Rose': {'Lady in the Water': 2.5, 
                         'Snakes on a Plane': 3.5,
                         'Just My Luck': 3.0, 
                         'Superman Returns': 3.5, 
                         'You, Me and Dupree': 2.5, 
                         'The Night Listener': 3.0},
           'Gene Seymour': {'Lady in the Water':3.0,
                             'Snakes on a Plane': 3.5,
                            'Just My Luck': 1.5, 
                            'Superman Returns': 5.0, 
                            'The Night Listener': 3.0,
                            'You, Me and Dupree': 3.5},
           'Michael Phillips': {'Lady in the Water':2.5,
                                'Snakes on a Plane': 3.0,
                                'Superman Returns': 3.5, 
                                'The Night Listener': 4.0},
           'Claudia Puig': {'Snakes on a Plane': 3.5,
                             'Just My Luck': 3.0, 
                             'The Night Listener': 4.5,
                             'Superman Returns': 4.0,
                             'You, Me and Dupree': 2.5},
           'Mick LaSalle': {'Lady in the Water':3.0,
                            'Snakes on a Plane': 4.0,
                            'Just My Luck': 2.0,
                            'Superman Returns': 3.0, 
                            'The Night Listener': 3.0,
                            'You, Me and Dupree':2.0},
           'Jack Matthews': {'Lady in the Water':3.0,
                            'Snakes on a Plane': 4.0,
                            'The Night Listener': 3.0,
                            'Superman Returns': 5.0, 
                            'You, Me and Dupree': 3.5},
           'Toby': {'Snakes on a Plane': 4.5,
                    'You, Me and Dupree': 1.0,
                    'Superman Returns': 4.0}
           }

def sim_distance(prefs, person1, person2):
    si={}
    for item in prefs[person1]:
        if item in prefs[person2]:
            si[item]=1

    if len(si)==0: return 0

    '''
    original

    sum_of_squares=sum([pow(prefs[person1][item]-prefs[person2][item],2) 
                        for item in prefs[person1] if item in prefs[person2]])
    '''
    sum_of_squares=sum([pow(prefs[person1][item]-prefs[person2][item],2) 
                        for item in si])

    '''
    original

    return 1/(1+sum_of_squares)
    '''
    return 1/(1+sqrt(sum_of_squares))


def sim_pearson(prefs, p1, p2):
    si={}
    for item in prefs[p1]:
        if item in prefs[p2]: si[item]=1
 
    n=len(si)
    if n==0: return 0

    sum1=sum([prefs[p1][it] for it in si])
    sum2=sum([prefs[p2][it] for it in si])
    
    sum1Sq=sum([pow(prefs[p1][it],2) for it in si])
    sum2Sq=sum([pow(prefs[p2][it],2) for it in si])
    
    pSum=sum([prefs[p1][it]*prefs[p2][it] for it in si])
    
    num=pSum-(sum1*sum2/n)
    den=sqrt((sum1Sq-pow(sum1,2)/n)*(sum2Sq-pow(sum2,2)/n))
    if den==0: return 0
    r=num/den
    return r

def topMatches(prefs,person,n=5,similarity=sim_pearson):
    scores=[(similarity(prefs,person,other),other)
            for other in prefs if other!=person]
    scores.sort()
    scores.reverse()
    return scores[0:n]

def getRecommendations(prefs,person,similarity=sim_pearson):
    totals={}
    simSums={}
    for other in prefs:
        if other==person: continue
        sim=similarity(prefs,person,other)
        
        if sim<=0: continue
    
        for item in prefs[other]:
            if item not in prefs[person] or prefs[person][item]==0:
                totals.setdefault(item,0)
                totals[item]+=prefs[other][item]*sim
                simSums.setdefault(item,0)
                simSums[item]+=sim
                
    rankings=[(total/simSums[item],item) for item, total in totals.items()]
    rankings.sort()
    rankings.reverse()
    return rankings

def transformPrefs(prefs):
    result={}
    for person in prefs:
        for item in prefs[person]:
            result.setdefault(item,{})
            result[item][person]=prefs[person][item]
    return result

def calculateSimilarItems(prefs,n=10):
    result={}
    
    itemPrefs=transformPrefs(prefs)
    c=0
    for item in itemPrefs:
        c+=1
        if c%100==0: print "%d / %d" % (c,len(itemPrefs))
        scores=topMatches(itemPrefs,item,n=n,similarity=sim_distance)
        result[item]=scores
    return result

def getRecommendedItems(prefs,itemMatch,user):
    userRatings=prefs[user]
    scores={}
    totalSim={}

    for (item,rating) in userRatings.items():
        for (similarity,item2) in itemMatch[item]:
            if item2 in userRatings: continue

            scores.setdefault(item2,0)
            scores[item2]+=similarity*rating

            totalSim.setdefault(item2,0)
            totalSim[item2]+=similarity

    rankings=[(score/totalSim[item],item) for item,score in scores.items()]

    rankings.sort()
    rankings.reverse()
    return rankings

def loadMovieLens(path='./data/movielens'):
    movies={}
    for line in open(path+'/u.item'):
        (id,title)=line.split('|')[0:2]
        movies[id]=title
        
    prefs={}
    for line in open(path+'/u.data'):
        (user,movieid,rating,ts)=line.split('\t')
        prefs.setdefault(user,{})
        prefs[user][movies[movieid]]=float(rating)
    return prefs

このソースコードであれば本にあるような事は実行できます。

確認はしたつもりですが、貼り付けるときにミスとかがあって、間違いがあるかもしれませんので、使うときは気をつけてください。

三章以降もそのうち個人的な正誤表を作っていきたいと思います。

関連リンク

随時追加していきたいと思います.

書籍「集合知プログラミング」について パート1

http://d.hatena.ne.jp/naoe/20081130

書籍「集合知プログラミング」について パート2

http://d.hatena.ne.jp/naoe/20081207

書籍「集合知プログラミング」について パート3

http://d.hatena.ne.jp/naoe/20090201

書籍「集合知プログラミング」について パート4

http://d.hatena.ne.jp/naoe/20090222

書籍「集合知プログラミング」について パート5

http://d.hatena.ne.jp/naoe/20090524

書籍「集合知プログラミング」について パート6

まだない。準備中

書籍「集合知プログラミング」について パート7

まだない。準備中

書籍「集合知プログラミング」について パート8

http://d.hatena.ne.jp/naoe/20090614

以降まだ準備中

すみません、まだないです。