階層型クラスタの作成
ついに本題に入ります.
今回やること
「集合知プログラミング」の3章で使われている,単語の頻度がファイルになっている"blogdata.txt"を使って,階層型クラスタを作成します.
得られる結果はp.47にあるデンドログラムと同じになるはずですが...
blogdata.txtの読み込み
blogdata.txtはTSV(Tab Separated Values)になっています.
このファイルを読み込み,URLをキーにしたHashに格納します.
def blog_data_from(file) word_counts = {} lines = File.open(file, 'r').readlines # 先頭行を読んで,単語の配列を作る words = lines.shift.chomp.split("\t") words.delete('Blog') lines.each do |line| row = line.chomp.split("\t") url = row.shift counts = row.map { |cell| cell.to_i } word_counts[url] = Hash[*words.zip(counts).flatten] end word_counts end
HierarchicalClusterクラスの作成
階層型クラスタをMy::HierarchicalClusterクラスとして実装します.
以下に全ソースを示します.ちなみにピアソン相関の計算には以前書いたものを使っています.
module My class HierarchicalCluster def initialize(word_counts) @word_counts = word_counts @leafs = word_counts.keys @branches = {} @similarities = {} @tree = Node.new('Root') end attr_reader :tree # 一番距離の近いURL同士を枝葉にしてleafsに戻す # また,作成した枝の平均値を求めてword_countsに保存する # 上記を1つの枝が残るまで,すなわち1つの木になるまで繰り返す def make_tree until @leafs.size == 1 calc_similarities left, right, similarity = closest_leaf update_branches_and_leafs(left, right, similarity) end @tree << @branches[@leafs.to_s] end private # 全ての単語の組み合わせに対して,ピアソン相関値を求める # ただし,格納する際には1から減算して格納する # 相関値のキーはペアを表現するクラス(Pair)で持つ def calc_similarities @leafs.each do |leaf1| @leafs.each do |leaf2| # 自分自身との相関は意味が無いので飛ばす next if leaf1 == leaf2 pair = Pair.new [leaf1, leaf2] unless @similarities[pair] # valuesメソッドを使うと順序が保証されないので,別変数にvalueをそれぞれ格納する leaf1_counts = [] leaf2_counts = [] @word_counts[leaf1].each do |word, count| leaf1_counts << count leaf2_counts << @word_counts[leaf2][word] end @similarities[pair] = 1 - Pearson.calc(leaf1_counts, leaf2_counts) end end end end # 2つの葉がもつword countの平均値を計算する def average_counts_between(leaf1, leaf2) average_counts = {} @word_counts[leaf1].each do |word, count| average_counts[word] = (count + @word_counts[leaf2][word]).quo(2) end average_counts end # leafsに残っている葉同士の相関値から,最も距離の短いものを返す def closest_leaf @similarities.select { |pair, similarity| @leafs.include?(pair.left) and @leafs.include?(pair.right) }.sort_by { |pair, similarity| similarity }.first.flatten end # 枝を作って,branchesに登録する def update_branches_and_leafs(url1, url2, similarity) branch = "#{url1}_#{url2}" # 枝を作る際,葉がURLの場合と枝の場合があるので注意する @branches[branch] = Node.new_branch(branch, (@branches[url1] or url1), (@branches[url2] or url2)) @word_counts[branch] = average_counts_between(url1, url2) @leafs << branch @leafs.delete(url1) @leafs.delete(url2) end end end
ポイント解説
まずはmake_treeを見てください.
ここで全体の処理の流れを記述しています.
def make_tree until @leafs.size == 1 calc_similarities left, right, similarity = closest_leaf update_branches_and_leafs(left, right, similarity) end @tree << @branches[@leafs.to_s] end
@leafsはクラスタ対象が配列になっているもの,と思ってほしいのですが,その@leafsに対して,
それぞれの相関値を計算 (calc_similarities)
最も相関が高い組み合わせを探す (closest_leaf)
クラスタを表現する木に継ぎ足す (update_branches_and_leafs)
という処理を繰り返しています.
しかし,このままではループが終わりません.
そこでupdate_branches_and_leafsメソッドで最も相関の高かったurl1とurl2をbranchとしてくっつけてから削除します.
その代わりに,url1とurl2の枝になっているbranchを挿入します.
def update_branches_and_leafs(url1, url2, similarity) ... @leafs << branch @leafs.delete(url1) @leafs.delete(url2) end
こうすることで@leafsの要素がどんどん減っていき,最終的には1つだけ残るようになります.
階層型クラスタの定義からすれば当たり前なのですが,Pythonですっきりと書かれたソースを読んだ時は感心しました.
メソッドチェーン
今回のテーマとは関係ないのですが,closest_leafを見てください.
少々見づらいかもしれませんが,メソッド呼び出しが途切れずに最後まで続いています,
しかも,連続したメソッド呼び出しで得られた結果を,そのままclosest_leafメソッドの返り値としています.
def closest_leaf @similarities.select { |pair, similarity| @leafs.include?(pair.left) and @leafs.include?(pair.right) }.sort_by { |pair, similarity| similarity }.first.flatten end
このようにメソッドチェーンで記述すると,
やりたい処理を(ほぼ)そのまま書き連ねることができる
余計な一時変数が要らない
という恩恵があり,何でもオブジェクトなRubyだと書いてて気持ちがいいです.
ただ,やりすぎると後で何が書いてあるのか分からなくなるので,程々にしておきましょう.
最後に動作確認
このプログラムを実行すると,デンドログラムがずらずら〜っと表示されます.
require 'pair' require 'node' require 'pearson' cluster = My::HierarchicalCluster.new(blog_data_from('blogdata.txt')) cluster.make_tree cluster.tree.printTree
本に掲載されている結果と同じはずですが,いかがでしょうか?
(とても長いので非表示にしてあります.↓からどうぞ)
Root +----+ |----> gapingvoid: "cartoons drawn on the back of business cards" +----+ |----+ | |----> Schneier on Security | +----> Instapundit.com +----+ |----> The Blotter +----+ |----+ | |----> MetaFilter | +----+ |----> SpikedHumor +----+ |----> Captain's Quarters +----+ |----> Michelle Malkin +----+ |----+ | |----> NewsBusters.org - Exposing Liberal Media Bias | +----+ |----+ | |----> Crooks and Liars | +----> Hot Air +----+ |----> Power Line +----> Think Progress +----+ |----> Andrew Sullivan | The Daily Dish +----+ |----> Little Green Footballs +----+ |----> Eschaton +----+ |----> Daily Kos +----> Talking Points Memo: by Joshua Micah Marshall +----+ |----> 43 Folders +----+ |----> TechEBlog +----+ |----+ | |----> Mashable! | +----> Signum sine tinnitu--by Guy Kawasaki +----+ |----+ | |----+ | |----> Slashdot | +----+ |----> MAKE Magazine +----> Boing Boing | +----+ |----+ | |----> Oilman | +----+ |----> Online Marketing Report +----+ |----> Treehugger +----+ |----> SimpleBits +----+ |----> Cool Hunting +----+ |----> Steve Pavlina's Personal Development Blog +----+ |----+ | |----> Pharyngula | +----> ScienceBlogs : Combined Feed +----+ |----> BuzzMachine +----+ |----> Copyblogger +----+ |----+ | |----> Seth's Blog | +----> The Viral Garden +----+ |----+ | |----> Bloggers Blog: Blogging the Blogsphere | +----+ |----> Sifry's Alerts +----> ProBlogger Blog Tips +----+ |----+ | |----> Scobleizer - Tech Geek Blogger | +----> Valleywag +----+ |----+ | |----> O'Reilly Radar | +----> 456 Berea Street +----+ |----> Lifehacker +----+ |----> Quick Online Tips +----+ |----> Publishing 2.0 +----+ |----> Micro Persuasion +----+ |----> A Consuming Experience (full feed) +----+ |----> John Battelle's Searchblog +----+ |----> Search Engine Watch Blog +----+ |----> Read/WriteWeb +----+ |----> Official Google Blog +----+ |----> Search Engine Roundtable +----+ |----> Google Operating System +----> Google Blogoscoped +----+ |----+ | |----+ | |----+ | |----> Blog Maverick | +----+ |----> Download Squad +----+ |----> CoolerHeads Prevail +----+ |----> Joystiq +----> The Unofficial Apple Weblog (TUAW) | +----+ |----> Autoblog +----+ |----> Engadget +----> TMZ.com | +----+ |----> Matt Cutts: Gadgets, Google, and SEO +----+ |----> PaulStamatiou.com +----+ |----+ | |----> TechCrunch | +----> GigaOM +----+ |----+ | |----> Creating Passionate Users | +----> Techdirt +----+ |----> Joho the Blog +----+ |----+ | |----> Jeremy Zawodny's blog | +----> PerezHilton.com +----+ |----> Joi Ito's Web +----+ |----> ongoing +----+ |----> Joel on Software +----+ |----+ | |----> we make money not art | +----+ |----> plasticbag.org +----+ |----> Signal vs. Noise +----+ |----> kottke.org +----+ |----> Neil Gaiman's Journal +----+ |----+ | |----> The Huffington Post | Raw Feed | +----+ |----> Wonkette +----+ |----> Gawker +----+ |----> Go Fug Yourself +----> The Superficial - Because You're Ugly +----+ |----> Deadspin +----> Gothamist +----+ |----> Kotaku +----> Gizmodo +----+ |----> Shoemoney - Skills to pay the bills +----+ |----> flagrantdisregard +----+ |----> WWdN: In Exile +----+ |----> Derek Powazek +----+ |----> lifehack.org +----> Dave Shea's mezzoblue +----+ |----> Wired News: Top Stories +----+ |----> Bloglines | News +----> Topix.net Weblog