nykergoto’s blog

機械学習とpythonをメインに

本のクラスタリングをやってみよう - 吾輩は猫であるに近い本は何なのか

最近理論よりなことばかりやっていたので今回は実際のデータを使った解析をやってみます。

今回使うデータは、読書メーターからクロールさせてもらって作成した、ユーザーに紐付いた読書履歴のデータです。ユーザーごとに [だれの, どんな] 本を読んだかがわかるようなデータになっています。一例は以下のような感じです。

アガサ・クリスティー   おづ まりこ    トマス・H・クック ムア・ラファティ    川口俊和    ジョナサン・オージエ  村田 沙耶香    岡崎 琢磨   米澤 穂信   ピエール・ルメートル  金内 朋子

この人はミステリーが好きなのかもしれませんね。 先の例は作者でしたが、これと同じように本のタイトルも取得しています。 取得した本の数(累積)は 100万冊, ユーザー数は 2500 と気づいたら案外大きいデータセットになっていました。 このうち今回は本のタイトルのデータを使って、本のクラスタリングをやってみたいと思います。

github.com

方法

さてどうやってクラスタリングをやっていくか、ということですが、基本的なアイディアは「読書の履歴にたいする各本は、文章に対する単語の関係と似ているのではないか」という仮説です。

ユーザーは一定の嗜好パターンを持っていて、それに基づいて読む本を決めているはずです。 これは文章がある意味合い(例えば新聞記事とかラブレターとか)を持っていて、その意味合いによって出現する単語の分布が異なる、ということによく似ています。 このことから、文章と単語の関係から単語の意味表現を得たり、クラスタリングを行うような手法は、読書の履歴から本の特徴を得る場合にも用いれるはずです。はず。

以上の仮説に基づいて文章のクラスタ分類でおなじみの LDA (Latent Dirichlet Allocation) と Word2Vec を使います。

LDA

まずは LDA をやってみます。 LDAは文章中に出てくる単語の回数をカウントして文章の特徴とする BoW (Bag of Words) を用いるモデルです。

例えば、僕が文章を書くことを考えてみます。 僕には文章の嗜好や知識の偏りがあるので、野球や数学の話題は出て来るでしょうが、裁縫に関する話題はあまり出てこないように思われます。 このような話題のことをトピックと呼んでいます。 トピックモデルでは「文章には何らかの話題の偏りがある」ということを仮定します。

そして文章中に現れる単語は、文章に紐付けられたトピックのどれかから生成されるものと考えます。すなわち、すべての単語はそれに紐付いたトピックを持っている、と考えます。 またトピックは、どの単語がよく現れるか、また現れにくいかという分布を持っていて、この分布はトピックごとに異なっています。

例えば仮に野球を表すであれば バット, ヒット といった単語の確率が高くなっていて、反対に シュート という単語は低い、といった感じです。 このトピックごとの単語のかたよりも学習していきます。

トピックモデルはグラフィカルモデルの有用さを示すいい例なので数式をたどると面白いかもしれません。僕はベイズとグラフィカルモデル大好きなので、LDA大好きです。 こんな雑な説明でもこれから勉強したい!となった方は、ぜひさとういっせい先生の本をおすすめします。とても丁寧に式を追っていて、数式の気持ちというか「なぜこれをやってるんだろう」を丁寧に説明してくれていて、数式の途中でまいごになることが少なく、おすすめです。

www.amazon.co.jp

結局 LDA をして嬉しいのは何かというか「文章がどのような偏りを持ったトピックで生成されたのか」という情報を知ることが出来るということです。 この情報は先の 野球トピック のようなものに相当しています。 すなわちこちらから何も情報を与えなくても(文章だけ与えれば) バットとヒットは同じ文脈で出てきやすいんだな、というようなクラスの情報を得ることが出来るわけで、それはとてもハッピーですね。

というわけで早速やってみましょう。 feature.py の関数を用いてデータをロードした後に, 10個のトピックに分類してみます。

from features import load_feature, compile_corpus_and_dictionary
from lda import LDATrainer

data = load_feature(root_path="./data/raw/", target="title")
corpus, dictionary = compile_corpus_and_dictionary(data, no_below=5, save_to="checkpoints/dictionary.txt")

lda_trainer = LDATrainer(output_dir="./checkpoints/")
lda_trainer.run(corpus, dictionary, topics=10)  # 学習の実行

# 上位のトピックを保存
lda_trainer.show_topics(save_to_dir="./checkpoints/")

出てきたトピックの分布は以下のようになりました。 10個のトピックをすべて載せてもよいのですが、ちょっと見にくいのと、あとでも述べますがあまりトピックごとに差分が見えなかったので2つだけ載せています。

topic 1

[('阪急電車(幻冬舎文庫)', 0.0013286450514788421),
 ('和菓子のアン(光文社文庫)', 0.0010029719922129846),
 ('永遠の0(講談社文庫)', 0.00095220966510543164),
 ('氷菓(角川文庫)', 0.000931927887614856),
 ('火花', 0.00088232395541623144),
 ('レインツリーの国(新潮文庫)', 0.00087065130728654241),
 ('ビブリア古書堂の事件手帖3~栞子さんと消え…', 0.00083625459565052221),
 ('陽だまりの彼女(新潮文庫)', 0.00078834353368666273),
 ('夜は短し歩けよ乙女(角川文庫)', 0.00076325430885853943),
 ('ぼくは明日、昨日のきみとデートする(宝島社…', 0.00076090024369739853)]

topic2

[('永遠の0(講談社文庫)', 0.0012030737598141335),
 ('阪急電車(幻冬舎文庫)', 0.0011685757490659541),
 ('イニシエーション・ラブ(文春文庫)', 0.0011032171029623371),
 ('舟を編む', 0.00093492775265262469),
 ('西の魔女が死んだ(新潮文庫)', 0.00085344844568903389),
 ('夜のピクニック(新潮文庫)', 0.00084074342930131642),
 ('図書館戦争図書館戦争シリーズ(1)(角…', 0.00079957001036084336),
 ('カラフル(文春文庫)', 0.00078677125722370774),
 ('死神の精度(文春文庫)', 0.00077641049565474061),
 ('ぼくは明日、昨日のきみとデートする(宝島社…', 0.00076590742659798081)]

なんだかどちらのトピックも大差ない内容になってしまっていますね。 本のタイトルをクラスタに分けたいというモチベーションだったので、その意味でこれは失敗になってしまいました。 その後も色々と試行錯誤はしてみた*1のですが、あまり良い結果が得られなかったので LDA は宿題に回して Word2Vec の方に移ります。

Word2Vec

次は word2vec をやってみます。

word2vec は単語の分散表現 (word embedding) を獲得手法と表現されることが多いように、単語が何の意味を持っているのかをベクトルで表したい、ということがモチベーションの手法です。 *2

word2vec には Continuous Bag of Words (CBOW) と skip-gram の二種類がありますが、発想はほとんど同じです。 CBOW では周辺の単語から中央の言葉を予測していて、 skip-gram では中心の単語から周辺の単語の予測をします。そしてどちらの手法もその予測器は隠れ層1の auto-encoder です。 数式にすると中心の単語が入力になる skip-gram の目的関数は以下のようなります。

$$ {\min_{W_1, W_2}} {\rm Loss}(W_1, W_2) = \sum_{i \in {\rm train}} \sum_{j \in {\rm surround(i)}} {\rm CrossEntropy} ( W_2^{T} ( W_1 x_i ), y_j) $$

このとき $x, y \in \mathbb{R}^n$ は単語を表す one-hot-vector で $n$ は文章中の語彙数を表しています。また $W_1, W_2 \in {\mathbb R}^{m \times n}$ は隠れ層の次元数が $m$ の auto-encoder のパラメータです。

CBOW の場合には入力が周辺の単語になりますから、上の数式で $x_i$ と $y_j$ が入れ替わったような形になります。

こうして得られた auto-encoder の隠れ層の部分、すなわちある単語 $x$ にたいして $W_1 x$ で計算される $m$ 次元ベクトルが、単語の情報をより低次元で表現できるベクトル(これを単語の分散表現と呼びます)になっていると解釈できます。

現在はそれにいろいろな改良やアイディアが付け加えられています。 例えば、ロス関数の計算において発生する softmax の計算が語彙の数 $n$ のオーダーで必要なことが計算のボトルネックになっているとして、すべての単語に対する計算をさぼって、近似的に softmax を計算してしまおうというネガティブサンプリングという手法もそのひとつです。 これにより最適化における大幅な速度向上を達成しました。 また、word2vecの入力に文章のidを加えて、文章も分類してしまおうという方法 (doc2vec) も提案されています。

python で word2vec をやりたいときは chainer や keras などのディープなフレームワークを使ってもできますが内部が C で実装されていて速度の早い gensim を用いるのが良いと思います。 特にGPU環境でなければ。(一回 word2vec を実装した時には cpu しかなく全く使い物になりませんでした)

このライブラリ関数やクラスの定義の仕方に若干の癖があるという難点はありますが cpu 環境でも十分実用的な時間に計算できるという点はとても魅力です。 では実際に計算をやってみます。 今回は僕が作成した訓練のための V2WTrainer クラスを用いていますが, 実装見てもらうとわかるように内部で大したことはやっていないので直接 gensim を使うときもこんなのりで出来るんだーという感じで見てもらえれば。

from word_to_vec import W2VTrainer

w2v_trainer = W2VTrainer(output_dir="./checkpoints/")
w2v_trainer.run(data)

結果

Word2Vec では単語のベクトルが得られるので、内積を取ることでその近さを知ることが可能です。よってある単語に最も近い単語、最も遠い単語を計算できます。 今回は単語ではなく本の名前なので、ある本と似た本、全く似ていない本を計算出来ることになります。

ここでようやくタイトルの「吾輩は猫である」が出てきます。吾輩は猫であるも色々とバージョンがある(ハードカバーとか文庫版とか)ので、まずは最も読まれていた新潮文庫版で試します。 吾輩は猫であるとの類似度の計算結果が以下になります。

我輩は猫である(新潮文庫) に近い本・遠い本

近い本

[('三四郎(新潮文庫)', 0.8102374076843262),
 ('こころ(新潮文庫)', 0.7891632914543152),
 ('門(新潮文庫)', 0.7769464254379272),
 ('草枕(新潮文庫)', 0.7562223672866821),
 ('虞美人草(新潮文庫)', 0.7479068636894226),
 ('阿部一族・舞姫(新潮文庫)', 0.7449460625648499),
 ('坊っちゃん(新潮文庫)', 0.7428607940673828),
 ('それから(新潮文庫)', 0.7405833005905151),
 ('行人(新潮文庫)', 0.7404835224151611),
 ('道草(新潮文庫)', 0.7297923564910889)]

遠い本

[('死神姫の再婚-五つの絆の幕間劇-(ビーズ…', 0.13127756118774414),
 ('絶園のテンペスト2(ガンガンコミックス)', 0.1260046362876892),
 ('死神姫の再婚-始まりの乙女と終わりの教師-…', 0.1172419935464859),
 ('信長協奏曲1(ゲッサン少年サンデーコミッ…', 0.09469656646251678),
 ('レベルE(上)(集英社文庫―コミック版)', 0.09304021298885345),
 ('絶園のテンペスト(5)(ガンガンコミックス)', 0.09189711511135101),
 ('信長協奏曲7(ゲッサン少年サンデーコミッ…', 0.09187307208776474),
 ('信長協奏曲2(少年サンデーコミックス)', 0.08920496702194214),
 ('天使1/2方程式1(花とゆめCOMICS)', 0.08579257875680923),
 ('闇の皇太子偽悪の革命家(ビーズログ文庫)', 0.08444569259881973)]

予想以上に綺麗に分離してくれました。 同じ夏目漱石の文庫本が多く並んでいます。 また森鴎外が入っているのも納得できます。 反対の遠い本ではコミックス系が多くなっていて、これも納得できる結果になりました。 LDA よりはうまくいってそう…

いい感じだったので森見登美彦の「四畳半神話大系(角川文庫)」でもやってみました

四畳半神話大系(角川文庫)に近い本・遠い本

近い本

[('夜は短し歩けよ乙女(角川文庫)', 0.8690416216850281),
 ('太陽の塔(新潮文庫)', 0.8665028810501099),
 ('美女と竹林(光文社文庫)', 0.8509087562561035),
 ('有頂天家族(幻冬舎文庫)', 0.8440086841583252),
 ('新釈走れメロス他四篇(祥伝社文庫も…', 0.8257927894592285),
 ('ペンギン・ハイウェイ(角川文庫)', 0.8094362020492554),
 ('([も]3-1)恋文の技術(ポプラ文庫)', 0.8090072274208069),
 ('きつねのはなし(新潮文庫)', 0.7985487580299377),
 ('四畳半王国見聞録(新潮文庫)', 0.7920351028442383),
 ('宵山万華鏡(集英社文庫)', 0.7382686734199524)]

遠い本

[('血界戦線4―拳客のエデン―(ジャンプコ…', 0.07178327441215515),
 ('光とともに…13―自閉症児を抱えて', 0.06594447791576385),
 ('王家の紋章(17)(Princessc…', 0.0623263344168663),
 ('VIVO!1(マッグガーデンコミックス…', 0.05496706813573837),
 ('王家の紋章(15)(Princessc…', 0.05385550856590271),
 ('光とともに…14―自閉症児を抱えて', 0.052545785903930664),
 ('彼氏彼女の事情(15)(花とゆめCOMI…', 0.050349362194538116),
 ('王家の紋章(16)(Princessc…', 0.050084952265024185),
 ('Vassalord.6(マッグガーデンコ…', 0.04914539307355881),
 ('彼氏彼女の事情(16)(花とゆめCOMI…', 0.0489741712808609)]

こちらも上位がすべて森見登美彦氏で埋まっていますね。 同じ作家で固まることがよく起こるというのは、作家によらず、特定の作家の本を読むと同じ作家の本を再び読む、ということが言えるのかもしれません。まあそりゃ当たり前か。 一度気にいると同じ作家の本ばかり買ってしまう、という感覚が可視化された、ということでしょうか。

ここで「ハードカバーの方がコアな客が多いから、同じ作家で固まりやすいのでは(類似度がほぼ1になるとか」と思ったので検証してみます。 ハードカバーの四畳半神話大系の類似度は以下のようになりました。

[('きつねのはなし', 0.7952536344528198),
 ('リンダリンダラバーソール(新潮文庫)', 0.7498961091041565),
 ('新釈走れメロス他四篇', 0.7390938401222229),
 ('夜は短し歩けよ乙女', 0.7354095578193665),
 ('太陽の塔', 0.7339460849761963),
 ('有頂天家族', 0.7336863875389099),
 ('風に舞いあがるビニールシート', 0.7267143130302429),
 ('ぐるぐるまわるすべり台', 0.7245926260948181),
 ('天国はまだ遠く', 0.7150847911834717),
 ('二枚舌は極楽へ行く(FUTABA・NOVE…', 0.7144078016281128)]

「きつねのはなし」は森見登美彦氏ですが、類似度は先よりも落ちています。 二番目の本はしらなかったのでググったのですが大槻ケンヂ氏の本なんですね、知りませんでした。

www.amazon.co.jp

内容紹介
僕らのバンドが、メジャーデビューすることになった! その頃、日本はバンドブームに沸いていた。無名だった若者が、次々とスターになった。ライブ会場は熱狂に満ちた。でも、ブームはいつか終わるものだ。大人たちは、潮が引くように去ってゆく。誰もが時の流れと無縁ではいられないんだ。僕と愛すべきロック野郎たちの、熱くて馬鹿馬鹿しくて切なかった青春を、いま再生する。

普通に面白そう……。 あでもそう思うってことは、ちがう作家だけどその人が面白いと思いそうな本を拾えてるってことなのかもしれないですね。サンプル僕だけであれですが。

文庫版の森見登美彦氏の本も見当たらないのが不思議ですね。これはハードカバーを読む人と、文庫を読む人はそもそも文脈がだいぶ違っていて、あの本はハードカバーだけど今回は文庫までまとう、といった読み方はしないんだなあということが伺えます。

ちなみに吾輩は猫である岩波文庫版になるとだいぶ様相が違っていました

[('「超」文章法(中公新書)', 0.8472597002983093),
 ('無限論の教室(講談社現代新書)', 0.8468811511993408),
 ('天の瞳幼年編〈2〉(角川文庫)', 0.8414252400398254),
 ('日本語の教室(岩波新書)', 0.8320746421813965),
 ('不都合な真実', 0.8299296498298645),
 ('朝の少女(新潮文庫)', 0.8163926601409912),
 ('人生に意味はあるか(講談社現代新書)', 0.8158650398254395),
 ('けものたちは故郷をめざす(新潮文庫あ4…', 0.813538670539856),
 ('DeepLove―アユの物語完全版', 0.812150239944458),
 ('ブラック・ジョーク大全(講談社文庫)', 0.8112159967422485)]

夏目漱石が激減してしまったのが不思議です。岩波文庫でも夏目漱石の本はいくつか出ているのですが…

まとめと今後の課題

  • word2vec の応用の広さすごい
  • 著者のデータでも何かしたい
  • LDA かわいそうなのでなんとかチューニングして結果出してあげたい。ベイズだしね。

*1:トピック数, αの事前分布, 最適化方法などいろいろやったものの徒労に終わる悲しみ

*2:元論文: [1301.3781] Efficient Estimation of Word Representations in Vector Space