E_P(w) = - Σ w^T Φ(x_n) t_nここで Σは誤分類されたパターン全体を走るとします。 すると、w^T Φ(x_n) ≧ 0 のとき t_n は「間違って」 -1 になっているし、w^T Φ(x_n) < 0 のときは t_n は「間違って」 +1 になっているため、E_P(w) は常に0以上であることがわかります。 これを最小化するために、「誤分類された x_n 1つずつに対して」次の式を使って逐次的に最小値に近づけていきます。 イメージとしては、ニュートン法(多次元版)って感じです。 このあたり、PRML には 3.1.3 節を見ろ、と書いてますが、5.2.4 節もあわせて見るといいかもしれません。
w^(γ+1) = w^(γ) - η▽E_P(w) = w^(γ) + φ(x_n) t_nややこしそうに書いてますが、アバウトにぶっちゃけると「ハズレのときは、そのハズレが出てしまった特徴ベクトルを重みベクトルに足すか引くかする」ということです。 こんな方法では最小値に本当に近づくのかどうか必ずしもわからないんですが、「解が存在する場合は」この計算を有限回繰り返すことで解が得られます、というのが「パーセプトロンの収束定理」で、Perceptron のキモです。 ね、ほんとにそんなんでうまく学習できるんですか? という気になるでしょう。
x_0 = (0, 0), t_0 = -1 x_1 = (1, 0), t_1 = -1 x_2 = (0, 1), t_2 = -1 x_3 = (1, 1), t_3 = +1これを学習できるのか、手で計算してみましょう。 特徴関数 φ は x_n そのままでもいいんですが、「φ_0 はバイアスでしょ、jk」と PRML にもあるので、次のように定義します。
φ( x_0 ) = (1, 0, 0) φ( x_1 ) = (1, 1, 0) φ( x_2 ) = (1, 0, 1) φ( x_3 ) = (1, 1, 1)これより重みベクトル w も3次元、初期値は w^(0) = (0, 0, 0) となります。 さて、ここから計算していきますが、誤分類ベクトルは本来ランダムに選ぶべきところ、ここでは少々恣意的に選んでいくことにします(理由は後述)。
w^(1) = w^(0) + φ(x_0) t_0 = (0, 0, 0) - (1, 0, 0) = (-1, 0, 0)ますます不安……この調子で大丈夫? ■Step 2 x_3 は w^T Φ(x_3) = -1 + 0 + 0 < 0 となり t_3 = +1 と合致しません。よって
w^(2) = w^(1) + φ(x_3) t_3 = (-1, 0, 0) + (1, 1, 1) = (0, 1, 1)■Step 3 x_1 は w^T Φ(x_1) = 0 + 1 + 0 ≧ 0 となり t_1 = -1 と合致しません。よって
w^(3) = w^(2) + φ(x_1) t_1 = (0, 1, 1) - (1, 1, 0) = (-1, 0, 1)■Step 4 x_2 は w^T Φ(x_2) = -1 + 0 + 1 ≧ 0 となり t_2 = -1 と合致しません。よって
w^(4) = w^(3) + φ(x_2) t_2 = (-1, 0, 1) - (1, 0, 1) = (-2, 0, 0)■Step 5 x_3 は w^T Φ(x_3) = -2 + 0 + 0 < 0 となり t_3 = +1 と合致しません。よって
w^(5) = w^(4) + φ(x_3) t_3 = (-2, 0, 0) + (1, 1, 1) = (-1, 1, 1)■Step 6 x_1 は w^T Φ(x_1) = -1 + 1 + 0 ≧ 0 となり t_1 = -1 と合致しません。よって
w^(6) = w^(5) + φ(x_1) t_1 = (-1, 1, 1) - (1, 1, 0) = (-2, 0, 1)■Step 7 x_3 は w^T Φ(x_3) = -2 + 0 + 1 < 0 となり t_3 = +1 と合致しません。よって
w^(7) = w^(6) + φ(x_3) t_3 = (-2, 0, 1) + (1, 1, 1) = (-1, 1, 2)■Step 8 x_2 は w^T Φ(x_2) = -1 + 0 + 2 ≧ 0 となり t_2 = -1 と合致しません。よって
w^(8) = w^(7) + φ(x_2) t_2 = (-1, 1, 2) - (1, 0, 1) = (-2, 1, 1)
#include <algorithm>を追加すれば VC++ でもコンパイルできました。
$ sort -R news20.binary > news20.random $ head -15000 news20.random > news20.train $ tail -4996 news20.random > news20.test実際に中谷が興味があるのは自然言語処理なので、NLP らしいデータはまた別途用意して試してみたいと思います。 まずはパーセプトロン。
$ ./oll_train P news20.train news20.model.p -C=2.0 -b=1.0 -I=1015000 例の学習データを用いて、Perceptron(P) で10回繰り返して学習(-I=10)、結果を news20.model.p に保存します。 学習手法は先頭の "P" を "AP" などに変更することで変えられます。そのあたり、コマンドラインの仕様はプロジェクトページで見てください。 「同じデータを10回繰り返して学習する」というあたりが、収束(の可能性)がある線形識別器ならでは、でしょうか。 Naive Bayes とかだと同じデータで10回学習なんて考えられないですから。
$ ./oll_test news20.test news20.model.p Accuracy 94.235% (4708/4996) (Answer, Predict): (p,p):2425 (p,n):35 (n,p):253 (n,n):2283学習結果を用いて、テストデータを分類させてみて、その正解率を出しています。 (p|n,p|n) は positive|negative を positive|negative に分類した件数で、正解率は 94%。 プロジェクトページの例と大きく違っていないので、一応動いていることが確認できました。 これで自分で勉強&実装したものとのベンチマークがとれますね。 次は、Perceptron を理解するために、「手で Perceptron を計算」してみます。]]>
q_0(t) = Σf^2 - (Σf)^2 / n_0
q_1(t) = Σf^2 - (Σf)^2 / n_1
git://github.com/shuyo/iir.git
$ ./fselect.rb -n 1000 -s -tq0 4million.corpus
ruby hac.rb 4million.corpus.q0
# without feature selection $ ./hac.rb 4million.corpus ------------------------- TOBIN'S PALM || | || +---- BETWEEN ROUNDS || | |+------------------ THE SKYLIGHT ROOM || | | || | +------- THE FURNISHED ROOM || | | || +----------- AN UNFINISHED STORY || | | |+------------- THE GREEN DOOR || | | | +--------- THE CALIPH, CUPID AND THE CLOCK || | | | +------ BY COURIER || | | +-------------- SPRINGTIME A LA CARTE || | +------------------- AN ADJUSTMENT OF NATURE || +--------------------- THE BRIEF DEBUT OF TILDY |+----------------------- THE GIFT OF THE MAGI | | | | +-- SISTERS OF THE GOLDEN CIRCLE | | | +---------- MEMOIRS OF A YELLOW DOG | | +----------------- THE ROMANCE OF A BUSY BROKER | +-------------------- A COSMOPOLITE IN A CAFE | +---------------- MAN ABOUT TOWN | | | +--- FROM THE CABBY'S SEAT | | +-------- LOST ON DRESS PARADE | +------------ MAMMON AND THE ARCHER +------------------------ A SERVICE OF LOVE | | +----- THE COMING-OUT OF MAGGIE | | +- AFTER TWENTY YEARS | +--------------- THE COP AND THE ANTHEM +---------------------- THE LOVE-PHILTRE OF IKEY SCHOENSTEIN # q_0/1000 words $ ./hac.rb 4million.corpus.q0 ------------------------- TOBIN'S PALM || | | | | +-------- THE CALIPH, CUPID AND THE CLOCK || | | | | +--- BY COURIER || | | | +----------- BETWEEN ROUNDS || | | | | +----- THE FURNISHED ROOM || | | | +--------- THE SKYLIGHT ROOM || | | +--------------- THE ROMANCE OF A BUSY BROKER || | +------------------ A COSMOPOLITE IN A CAFE || +-------------------- AN ADJUSTMENT OF NATURE |+----------------------- THE GIFT OF THE MAGI | | | | +---- SISTERS OF THE GOLDEN CIRCLE | | | +------------- AN UNFINISHED STORY | | +------------------- MAN ABOUT TOWN | | | | | | | +-- FROM THE CABBY'S SEAT | | | | | | +------ LOST ON DRESS PARADE | | | | | +---------- MEMOIRS OF A YELLOW DOG | | | | +------------ THE GREEN DOOR | | | +-------------- MAMMON AND THE ARCHER | | +----------------- SPRINGTIME A LA CARTE | +--------------------- THE BRIEF DEBUT OF TILDY +------------------------ A SERVICE OF LOVE | | +------- THE COMING-OUT OF MAGGIE | | +- AFTER TWENTY YEARS | +---------------- THE COP AND THE ANTHEM +---------------------- THE LOVE-PHILTRE OF IKEY SCHOENSTEIN # q_1/1000 words $ ./hac.rb 4million.corpus.q1 ------------------------- TOBIN'S PALM || | | | | | ||+- BETWEEN ROUNDS || | | | | | |+-- THE FURNISHED ROOM || | | | | | +--- THE SKYLIGHT ROOM || | | | | +-------- BY COURIER || | | | +----------- THE CALIPH, CUPID AND THE CLOCK || | | +---------------- MEMOIRS OF A YELLOW DOG || | +------------------ THE COMING-OUT OF MAGGIE || +--------------------- THE ROMANCE OF A BUSY BROKER |+----------------------- A COSMOPOLITE IN A CAFE | | || | || +--------- MAN ABOUT TOWN | | || | |+------------- AN UNFINISHED STORY | | || | | +------- LOST ON DRESS PARADE | | || | +-------------- MAMMON AND THE ARCHER | | || +----------------- AN ADJUSTMENT OF NATURE | | || | +------ THE BRIEF DEBUT OF TILDY | | || +------------ SPRINGTIME A LA CARTE | | || +---- THE GREEN DOOR | | |+------------------- FROM THE CABBY'S SEAT | | +-------------------- THE LOVE-PHILTRE OF IKEY SCHOENSTEIN | +---------------------- A SERVICE OF LOVE | +--------------- THE COP AND THE ANTHEM | +----- AFTER TWENTY YEARS +------------------------ THE GIFT OF THE MAGI +---------- SISTERS OF THE GOLDEN CIRCLE
http://ivoca.nsdev1/api/progresses/[iVoca アカウント名]この URL にアクセスすることで、熱心に学習している5件のブックについて、日々の学習履歴が json 形式で得られます(フォーマットについては後日解説します……すいません)。 なお、従来「サイボウズ(R) ネットID」をご利用いただいていた方のログインID(メールアドレス)とパスワードは、上の注意書きの通りそのまま iVoca アカウントのほうに移行させていただいていますので、「ログイン」をクリックした後、iVoca アカウントのログインフォームにてメールアドレスとパスワードを入れてログインしてください。 mixi OpenID でご利用いただいていた方はそのまま引き続きご利用いただけますし、setting メニューにて iVoca アカウントも設定して、併用することも出来るようになっています。 そして、今回の目玉機能は iKnow! 対応です。 iKnow の API を利用して、iKnow の「学習アイテムリスト」を iVoca で学ぶ(遊ぶ)ことができるようになりました。 これにより iKnow の素晴らしい問題リストを iVoca で覚えられるようになっただけでなく、iKnow にて音声データのある単語はちゃんとゲーム中に発音してくれるようになっています。 iVoca で遊べる iKnow リストの一例: ここに挙げたリスト以外にも、iVoca のユーザであればどなたでも、iKnow のどのリストでも iVoca で学べる(遊べる)ように登録することができます。 iVoca の iKnow メニュー から、iKnow のリスト画面の URL ( http://www.iknow.co.jp/lists/.... という形式のもの) を指定するか、同じ画面の iKnow リスト検索を使って利用したいリストを探してください。 なお、初めて iVoca にて利用するリストの場合には、学習データ初期化のための処理が10秒~1分程度かかります。 これほど素晴らしいデータを API で提供してくれているセレゴさんには、本当に感謝します。ありがとうございます。 これを第1弾として、逆に iVoca のブック(単語帳)を iKnow に登録する機能や、iVoca と iKnow の学習データを一元化して見せる機能とか、まずは iVoca でざっと覚えて、特に苦手な単語だけを選んで iKnow のリストを自動的に作るとかとか、いろいろ夢はふくらむんですが、順番にちょっとずつ、ですね。 まずは「問題の長さをもうちょっと長くできると嬉しい」という要望をいただいているのに、まだほうったらかしなので、こちらをやっつけるところからでしょうかね。 なお発音機能は Flash Player にて、バックエンドで iKnow アイテム情報を順次取得して実現しています。そのため、音声を発音するようになるまで少し遅れたり、ごく一部の単語のみしか発音してくれない、という状態になる場合があります。iKnow が混雑する時間帯は特に起きやすいようです。 改善を図りたいと考えてはいますが、あらかじめご了承ください。 ]]>
git://github.com/shuyo/iir.git前回作った corpus パックも commit してありますので、 clone すればいきなり動く、はず。
git clone git://github.com/shuyo/iir.git cd iir/hac ruby hac.rb 4million.corpusおのおの手元でちょこちょこ改変して試してみるには CodeRepos より git の方が向いてるんじゃあないかなあと思ったんですが、git まだ使いこなせてないのでなんか色々間違ってるかも。 実装の説明に行く前に、まず前回の naive HAC (=single-link clustering) はなぜ inefficient だったか、という話を簡単に。 クラスタを近い(=類似度が高い)順に結合していくのが HAC の基本ですが、そうなると当然「クラスタ同士の類似度」を計算しなくてはならなくなります。 一番素直で簡単のだと、「クラスタ間で最も近い(類似度が高い)ベクトル同士の類似度を、クラスタ間の類似度とする」という方法があるのですが(これが single-link clustering)、これが「大きいクラスタほど有利」な計算方法であることは明らかでしょう。 このため、「大きいクラスタが、対象を一つずつ拾っていってしまう」という現象(chaining)が発生してしまい、クラスタリングとして役に立たなくなってしまうことがあるわけです。 それを解消するための方法として、IIR の 17.2~17.4 では single-link clustering 以外に complete-link clustering, centroid, group-average など、チェインニングが起きにくい「クラスタ同士の類似度」を紹介しています。これらの詳細については、ここでは省きますね。 前回も紹介した「クラスタリングとは (クラスター分析とは)」にもこれらの方法が載っています。ただし、こちらは類似度として(ユークリッド)距離を使用しているので、IIR の cosine similarity とは大小が逆だったり、距離の場合に有効な「ウォード法」が紹介されていたりします。 また、IIR 17.2.1 では、前回の naive HAC の計算量は Θ(N^3) だったのですが、priority queue を導入すると Θ(N^2 log N) にできるよ、という話をしています。 それらを合わせたアルゴリズムの pseudo code が掲載されているので、今回はそれに沿って実装してみるわけです。 ]]> 実装 まず。 前回は余計なキャッシュとかして追いかけにくいコードになってしまって反省なので、今回は「対象な2値ハッシュ」のようなものをこしらえてみました。
a = SymmetricHash.new a[1, 2] = 3 puts a[2, 1] # => 3このように、キーとして2つの値を取りつつ、それを入れ替えたものも同じ値へのアクセスになる、というもの。 前回より読みやすくなってると思います。 あと、priority queue ですが……それっぽいインターフェースの「自動ソート配列」で簡易にやっつけました。手抜きです(汗)。 誰かが高速な priority queue の実装を教えてくれるのを期待(苦笑)。 HAC のキモ、IIR の pseudo code にちょうど対応する部分のソースです。
def pq_hac(docs) sim = SymmetricHash.new # 類似度 clusters = Hash.new # クラスタに属するドキュメント。centroid などの計算に使用 pqueue = Hash.new # priority queue # calcurate similarity & priority queue docs.each do |d1| t1 = d1[:title] clusters[t1] = [d1] pqueue[t1] = PriorityQueue.new docs.each do |d2| next if d1 == d2 v = sim[t1, d2[:title]] unless v sim[t1, d2[:title]] = v = calc_sim(d1[:vector], d2[:vector]) end pqueue[t1].insert( [v, d1, d2] ) end end # hac merged = [] # マージ列。dendrogram 生成に使用 while docs.size > 1 # 類似度最大のクラスタの組を取得 maxsim = [0] docs.each do |d1| t1 = d1[:title] maxsim = pqueue[t1].max if pqueue[t1].max[0] > maxsim[0] end v, d1, d2 = maxsim t1, t2 = d1[:title], d2[:title] # マージ merged << [t1, t2, v] docs.delete(d2) clusters[t1] += clusters[t2] pqueue[d1[:title]].clear # ←★問題有り!★(後述) docs.each do |d| next if d==d1 t = d[:title] pqueue[t].delete_if_3rd(d1) pqueue[t].delete_if_3rd(d2) v = yield sim, clusters, d, d1, d2 # clustering algorism はブロックで与える pqueue[t].insert( [v, d, d1] ) pqueue[t1].insert( [v, d1, d] ) end end merged end今回は pseudo code とかなり1対1に対応するので、見比べやすいです。 ポイントは、clustering algorism はブロックで与えられるようにしているところ。 clustering algorism を色々切り替えて楽しめるようになっています。
# complete_link clusters = pq_hac(docs) do |sim, clusters, d, d1, d2| t, t1, t2 = d[:title], d1[:title], d2[:title] v = sim[t, t1] v = sim[t, t2] if v > sim[t, t2] v end # centroid centroid = Proc.new do |sim, clusters, d, d1, d2| calc_sim(calc_centroid(clusters[d1[:title]]), calc_centroid(clusters[d[:title]])) end clusters = pq_hac(docs, ¢roid)
------------------------- TOBIN'S PALM || |||||| | | | | | |+- BETWEEN ROUNDS || |||||| | | | | | +-- THE FURNISHED ROOM || |||||| | | | | +---- THE SKYLIGHT ROOM || |||||| | | | +------- AN UNFINISHED STORY || |||||| | | +--------- LOST ON DRESS PARADE || |||||| | +----------- MAMMON AND THE ARCHER || |||||| +-------------- MEMOIRS OF A YELLOW DOG || |||||+---------------- AN ADJUSTMENT OF NATURE || ||||| | +------ THE BRIEF DEBUT OF TILDY || ||||| +------------ SPRINGTIME A LA CARTE || ||||| +----- THE GREEN DOOR || ||||+----------------- BY COURIER || |||+------------------ A COSMOPOLITE IN A CAFE || ||| +------------- MAN ABOUT TOWN || ||+------------------- THE LOVE-PHILTRE OF IKEY SCHOENSTEIN || |+-------------------- THE ROMANCE OF A BUSY BROKER || +--------------------- FROM THE CABBY'S SEAT |+----------------------- THE GIFT OF THE MAGI | +-------- SISTERS OF THE GOLDEN CIRCLE +------------------------ A SERVICE OF LOVE +---------------------- THE COMING-OUT OF MAGGIE | | +--- AFTER TWENTY YEARS | +---------- THE COP AND THE ANTHEM +--------------- THE CALIPH, CUPID AND THE CLOCK前回の naive HAC の結果よりは改善の兆しが見られますね。 "AFTER TWENTY YEARS" と "THE COP AND THE ANTHEM" が独立したクラスタに属しているあたりなんか、ちょっとは進歩したな、という感じです。 でも、まだまだ不満足。 これはやっぱり 8000次元もあるせいでしょうか……(次元の呪い、というやつ?)。 少なくとも「8000次元もあるせい」で、これがデータ由来の自然な結果なのかどうかを検証するのが難しくなっているので、次回は feature selection で次元を落とすのを試してみるべきですね。
require 'rubygems' require 'stemmer' docs = Array.new terms = Hash.new ARGV.each do |path| # ファイルを開いて本文を取得 file = path.dup file = $1 if path =~ /\/([^\/]+)$/ text = if path =~ /\.zip$/i file.sub!(/\.zip$/i, ".txt") `unzip -cq #{path} "*.txt"` else open(path){|f| f.read} end text = extract_gutenberg_body(text) # 短編ごとに分割 list = text.split(/^[IVX]+\s*\.?$/)[1..-1] list = text.split(/^\n{4}$/) if list.size<=1 list.each do |x| next unless x =~ /^(.+)$/ title = $1 words = x.scan(/[A-Za-z]+(?:'t)?/) next if words.size < 1000 words.each do |word| word = word.downcase.stem # 単語の原形(簡易版) terms[word] ||= Hash.new(0) terms[word][docs.size] += 1 end docs << {:title=>title, :n_words=>words.size} end end open('corpus', 'w'){|f| Marshal.dump({:docs => docs, :terms => terms}, f) }extract_gutenberg_body() は Project Gutenberg のテキストデータから本文を抽出する の抽出コードを呼び出すものです(上記サンプルコードでは省略)。 これに The Four Million by O. Henry などからダウンロードした zip アーカイブされた ascii ファイルを渡すと、各短編の抽出と使用語の解析が行われて corpus という marshal ファイルに出力されます(全ての Gutenberg ドキュメントに対応するわけではもちろんありません……)。
$ ./extract_corpus.rb 2776.zip 2777.zip ...使用語については原形に戻してます。精度が良くなることを期待するのと(必ずしも上がるわけではないのですが)、計算時間を減らすため。 サンプルコードでは stemmer モジュールを使って簡便に済ませてしまってます。 手元のコードでは、ちょっとでも精度を上げようと、ここに書いてあるやり方で Linguistics モジュールも組み合わせつつ、さらに手作り辞書とかごにょごにょしてます。 Pathtraq のカテゴリ分類での経験からすると、ここの精度が最終的に結構効いてくるんだろうなあ、という予感はします……まあ今のところは技術検証なのでシビアにやる必要はないですけど。 データの準備は本題ではないので、このへんで。 データだけでこんなに面倒じゃあ、検証とか無理無理……と思われると嬉しくないので、ここで作った corpus ファイルをさっくり公開します。 O.Henry の短編 109 データのダウンロード これを利用するには以下のように書けばOK。
data = open('corpus'){|f| Marshal.load(f) } docs = data[:docs] terms = data[:terms] #docs = [{:title=>"THE GIFT OF THE MAGI", :n_words=>2105}, ...] #terms = {"cat"=>{49=>2, 22=>1, 55=>2, 39=>1, 50=>1, ..}, ...}docs には短編のタイトルとその語数が格納されています。 terms は Hash 形式の inverted index になっていて、キーの term に対して、ドキュメントID とそのドキュメントにおけるその term の出現回数(= term frequency) の Hash を返します(ドキュメントID は docs におけるインデックス)。 term には原形の語幹が入っています。 この形式のデータがあれば、いきなりいろいろ遊べますよね!
# tf-idf を計算(一番オーソドックスな式) def calc_tdidf(tf, df, n_docs) tf * Math.log(n_docs / df) end # similarity(ドキュメントの類似度、内積) を計算 def calc_sim(v1, v2) sim = 0 v1.each_with_index {|x, i| sim += x * v2[i] } sim end # similarity(ドキュメントの類似度) を計算 class Similarity def initialize @memo = Hash.new # ドキュメント名に対して結果をキャッシュ end def calc(d1, d2) key = [d1[:title], d2[:title]] @memo[key] || @memo[key.reverse] || (@memo[key] = calc_sim(d1[:vector], d2[:vector])) end def include?(pair, title) # ペアに含まれていれば片割れを返す return pair[0] if pair[1] == title return pair[1] if pair[0] == title end # 2つのクラスタをマージ(inefficient なのはこいつのせい) def merge(d1, d2) @memo.each do |key, value| if (title2 = include?(key, d1[:title])) && title2 != d2[:title] value2 = @memo[[title2, d2[:title]]] || @memo[[d2[:title], title2]] @memo[key] = value2 if value2 > value end end end end # corpus を読み込み data = open(ARGV[0] || 'corpus'){|f| Marshal.load(f) } docs = data[:docs] terms = data[:terms] # 軸 #axes = terms.keys # all terms axes = terms.keys.select{|t| n=terms[t].size; n>1 && n<docs.size } # 2~n_docs-1 # 各ドキュメントの特徴ベクトルを計算 docs.each_with_index do |doc, doc_id| l2 = 0 v = axes.map do |term| rev_index = terms[term] docfreq = rev_index.size termfreq = rev_index[doc_id] x = calc_tdidf(termfreq, docfreq, docs.size) l2 += x * x x end l = Math.sqrt(l2) doc[:vector] = v.map{|x| x / l} # 単位ベクトルに end # HAC algorism(ここが本体) sim = Similarity.new clusters = [] while docs.size > 1 maxsim = 0 maxsim_pair = nil docs.each do |d1| docs.each do |d2| break if d1==d2 s = sim.calc(d1, d2) if maxsim < s maxsim = s maxsim_pair = [d1, d2] end end end clusters << [maxsim_pair[0][:title], maxsim_pair[1][:title], maxsim] docs.delete(maxsim_pair[1]) sim.merge maxsim_pair[0], maxsim_pair[1] end # dendrogram を出力 tree = Tree.new(clusters) puts treeIIR に掲載されている pseudo code では 13行しかないんですけどね(苦笑)。 しかもこれでも dendrogram を出力するための Tree クラスはアルゴリズムに直接関係ない&でかいので省略してあるんですよ……(後掲してあるのでご安心を)。 細かい説明は後にして、まずは実行。 以下のような dendrogram が得られます(テキストだから枝分かれの表現が貧弱ですが)。
------------------------- THE BRIEF DEBUT OF TILDY ||||||||| || | +--- AN ADJUSTMENT OF NATURE ||||||||| || +-------- A COSMOPOLITE IN A CAFE ||||||||| |+------------- FROM THE CABBY'S SEAT ||||||||| | +----------- THE GREEN DOOR ||||||||| | +------- SPRINGTIME A LA CARTE ||||||||| +-------------- THE FURNISHED ROOM ||||||||| | | | +---- BETWEEN ROUNDS ||||||||| | | | +- TOBIN'S PALM ||||||||| | | +------ AN UNFINISHED STORY ||||||||| | +---------- THE SKYLIGHT ROOM ||||||||| +------------ LOST ON DRESS PARADE ||||||||+---------------- MAN ABOUT TOWN |||||||+----------------- MEMOIRS OF A YELLOW DOG ||||||+------------------ THE CALIPH, CUPID AND THE CLOCK |||||| +--------- THE COP AND THE ANTHEM |||||+------------------- MAMMON AND THE ARCHER ||||+-------------------- BY COURIER |||+--------------------- AFTER TWENTY YEARS ||| | +-- THE COMING-OUT OF MAGGIE ||| +--------------- A SERVICE OF LOVE ||+---------------------- SISTERS OF THE GOLDEN CIRCLE || +----- THE GIFT OF THE MAGI |+----------------------- THE LOVE-PHILTRE OF IKEY SCHOENSTEIN +------------------------ THE ROMANCE OF A BUSY BROKER109 ドキュメントだとダイアグラムが大きすぎるので、短編集 The Four Million に含まれる 25 編のクラスタリング結果です。 本当は閾値も出したいんだけど、それは宿題ということで(Graphviz で書けるかなぁ?)。 この結果を見ても、コーパスの性質を知らないと「???」でしょうけれど(だからもっとわかりやすいデータをって!)。 それでもクラスタリングとしては到底満足できない結果であることはわかります。 例えば左から3列目までの枝分かれを見ることで、全体を4つのクラスタに分けることが出来ますが、「1個、1個、2個、21個」というクラスタリングになってしまいます。本当に1個しか含まれないクラスタが存在すべきデータならともかく(1つだけイタリア語とかw)、そうでないならこの割り方は不自然です。 このように枝分かれが階段状になってしまうことを「チェイニング効果」というそうですが、それが起きてしまっているわけですね。 ここで実装した simple unefficient な HAC ではチェイニング効果が発生しやすいことはわかっていたんですが(だから inefficient!)、それが確認できた、ということですね。 17.2 以降の efficient な HAC ではそれが抑えられるはずなので、次回はそちらに挑戦します。
class Tree def initialize(clusters) @tree = Hash.new clusters.each do |d1, d2, s| branch d1, d2 end @lines = Array.new @index = Hash.new gen_lines @tree.values[0] clusters.each do |d1, d2, s| connect d1, d2 end end def get_tree(d) if @tree.key?(d) @tree.delete(d) else d end end def branch(d1, d2) @tree[d1] = [get_tree(d1), get_tree(d2)] end def gen_lines(branch) if branch.instance_of?(Array) gen_lines branch[0] gen_lines branch[1] else @lines << "- " + branch @index[branch] = @lines.size - 1 end end def connect(d1, d2) i1 = @index[d1] i2 = @index[d2] i1, i2 = i2, i1 if i2 < i1 @lines.each_with_index do |line, i| if i==i1 @lines[i] = "-" + line elsif i==i2 @lines[i] = "+" + line elsif i>i1 && i<i2 @lines[i] = "|" + line elsif line =~ /^-/ @lines[i] = "-" + line else @lines[i] = " " + line end end end def to_s @lines.join("\n") end end]]>
猫,cat 犬,dog新しく日本語入力の問題を作ってもらう場合、答えがひらがなやカタカナなら、今まで英単語を書いていたところにそのままその答えを書いてください。
子,ね 丑,うし 寅,とら
鹿島,アントラーズ 川崎,フロンターレ 名古屋,グランパス漢字が入っている場合は、漢字の読みを教えてあげる必要があります。 漢字の部分を "/" で区切って、その読みを ":" の後ろに書いてください。 送りがなのように漢字とかなが混じっている場合は、漢字とかなの間も同じく "/" で分けてください。かなの方はそのまま残しておけばOKです。
he,彼:かれ she,彼:かの/女:じょ think,考:かんが/える red car,赤:あか/い/車:くるま
「吾輩は猫である」の作者,夏:なつ/目:め/漱:そう/石:せき 金閣寺を建立した,足:あし/利:かが/義:よし/満:みつ 弘法大師,空:くう/海:かい複雑なようですが、慣れれば難しくないですよ。 実は、この機能は単に漢字を変換しなくても入力できる機能ではなくて、「入力文字列に自由な表示文字列を割り当てる機能」なのです。 だから、漢字部分は何文字でも書けますし、それこそ漢字じゃなくても大丈夫。読みもかなだけじゃあなくて iVoca で使えるものならアルファベットや記号もOK、と自由度が高いので、こんなこともできます。
二十歳:はたち 竜:ドラグ/破斬:スレイブ 禁書目録:index ♂:おす]]>
var spkr = new ActiveXObject('SAPI.SpVoice'); spkr.Speak("Hello iVoca!");