東方創想話で楽しい字句解析
突然東方創想話を解析したいなーと思い立ち、それを簡単にする為のライブラリやツールを作ってみました。皆さんにも紹介してみたく記事も書いてみました。
※この記事はMac OSX環境を前提にしています。また、Ruby初心者向けに配慮した書き方をしている為、中級者以上の方にはつまらないと思う部分が多いでしょう。
東方創想話をRubyで扱う
それでは手始めに東方創想話から最新版の作品集を取得するスクリプトを書いてみましょう!私の作ったsosowa-rubyという、Rubyから東方創想話にアクセスするためのgemをインストールしてください。
$ gem install sosowa
これで準備は完了です!それではsosowa-rubyの使い方を理解するために簡単なコードを書いてみましょう。
Ruby 1.9.xを導入していない方へ
Macには最初からRubyがインストールされていますが、残念ながらバージョンが古く今回の場合は不適切です。 しかし、幸いにもMac OSXには驚くほど簡単にRubyを導入するための手段が用意されています。
複数のRubyを管理するためのツールであるRVMを導入します。 RVMは$HOME/.rvmに全てのRubyに関連するパッケージがインストールされるので、お使いのMacの環境を汚すことはありません。
$ curl -L https://get.rvm.io | bash -s stable --ruby $ rvm use 1.9.2 --default
以上のコマンドを実行するだけでRVMと安定版のRuby 1.9.2(2012/7/25 現時点)がインストールされます。 これでRVMとRuby 1.9.xは用意出来ました。
1. 作品集を取得する
#!/usr/bin/env ruby
# coding: utf-8
# test1.rb
require "sosowa"
latest_log = Sosowa.get
ss = latest_log[0]
puts "タイトル: #{ss.title}"
puts "作者: #{ss.author}"
順を追って解説しましょう。
# coding: utf-8
はPythonで言うところの# -*- coding: utf-8 -*-
と同等の意味を持ち、スクリプトがUTF-8で記述されていることを明示的に表しています。Ruby 1.9.x以降ではこれが無いとエラーになります。require "sosowa"
でsosowa-rubyをインポートして使える状態にしています。Sosowa.get
はSosowa.get :log => 0
と同じ意味であり、最新版の作品集を得ることが出来ます。ss = latest_log[0]
で作品集の先頭のSSをss
変数に格納しています。
それでは実行結果を見てみましょう。
$ ruby test1.rb
タイトル: 小鳥
作者: くじらのたましい
ばっちりですね!SSのタイトルと作者を出力するだけの簡単なスクリプトですが、これでsosowa-rubyで作品集を取得するための基本的な使い方を理解して頂けたと思います。
どのような属性値(titleやauthorなど)が見れるかを ss.params
で確認してみましょう。
直接指定して取得するには?
作品集番号を直接指定して取得する方法も用意されています。以下のように書くだけです。
log_55 = Sosowa.get :log => 55
2. SSを取得する
最初に書いたスクリプトにいくつか書き足して今度はSSの内容を取得してみましょう。
#!/usr/bin/env ruby
# coding: utf-8
# test2.rb
require "sosowa"
latest_log = Sosowa.get
index = latest_log[0]
ss = index.fetch
puts ss.text
おや?となった方に解説しておきますと、latest_log[0]
で返ってくるのは Sosowa::Index
というクラスのインスタンスであり、SS自体ではありません。
SSの内容を扱いたい場合には更に index.fetch
というメソッドを呼び出して実体となる Sosowa::Novel
を取得する必要があります。
それでは実行結果を見てみましょう。
$ ruby test2.rb
!キャラ崩壊注意!<br><br><br><br><br><br>
キラキラと、月の光が湖に反射しているのがよく見えた。<br>
やや風が強い夜であるから、
(省略)
思ったとおり、SSのテキストを取得出来ましたね!
Sosowa::Novelにどのような属性値(titleやauthorなど)が含まれているかを ss.params
で確認してみましょう。
直接指定して取得するには?
もちろんSSもkeyを指定して直接取得することが出来ます。以下のように書くだけです。
ss = Sosowa.get :log => 171, :key => 1342872411
これでsosowa-rubyで作品集とSSを取得する方法を理解して頂けたと思います。
3. SSのテキストを品詞別に分解する
ここからが本題です!これからMeCabと、それをRubyから扱うためのmecab-rubyライブラリを頻繁に使うことになるのでここでHomebrewというMac OSX向けのモダンなパッケージ管理ツールを使ったインストール手順を紹介しておきたいと思います。
Homebrewをインストールする
以下のコマンドでインストール出来ます。FinkやMacPortsは削除しておきましょう。
/usr/bin/ruby -e "$(/usr/bin/curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"
MeCabをインストールする
Homebrewでささっとインストールしてしまいましょう。
$ brew install mecab
$ brew install mecab-ipadic
MeCab Rubyバインディングをインストールする
ruby -v
でRubyのバージョンがRVMでインストールした1.9.xになっていることを確認してください。
$ cd tmp
$ wget http://mecab.googlecode.com/files/mecab-ruby-0.994.tar.gz
$ tar zxvf mecab-ruby-0.994.tar.gz
$ cd mecab-ruby-0.994
# extconf.rbを開いて`mecab-config`の前の行に`$LIBS += '-L/usr/local/Cellar/mecab/0.994/lib'`を代入して保存
$ ruby extconf.rb
$ make
$ make install
mecab-modernをインストールする
mecab-rubyをよりRubyらしい記法で扱えるようにするためのラッパーを作りました!
私の解説では基本的にmecab-modern
でラッピングしたものを使用しているのでこちらもインストールしてください。
$ gem install mecab-modern
東方MeCab辞書をインストールする
MeCabで使用するIPA辞書には当然ですが東方Projectの用語は収録されていません。これでは創想話コーパスを解析しても射命丸が射|命|丸
と分解されてしまい解析の精度が低くなってしまいます。
その問題を解決するために東方MeCab辞書を作りました!
以下の手順を踏むことで東方MeCab辞書をユーザー辞書としてデフォルトで読み込むようになります。
$ cd /tmp
$ git clone git://github.com/oame/thdic-mecab.git
$ cd thdic-mecab
$ rake build
$ rake install
以上で必要な環境が整いました。それでは早速コードを書いてみましょう!
#!/usr/bin/env ruby
# coding: utf-8
# test3.rb
require "mecab-modern"
require "sosowa"
mecab = MeCab::Tagger.new
text = Sosowa.get.sample.fetch.text.plain
tokens = mecab.parseToNode(text)
tokens.each do |token|
puts token.feature
end
これも順を追って解説しましょう。
MeCab::Tagger.new
でMeCabをインスタンス化します。Sosowa.get.sample.fetch.text.plain
は左から読むと理解しやすいでしょう。Sosowa.get
で最新版の作品集を取得しsample
でその中からランダムに一つを選びfetch
でSSを取得してtext
でSSの内容を取得してplain
で改行コード等を取り除いています。mecab.parseToNode(text)
でランダムなテキストをトークナイズして品詞別に分解します。- イテレータを使い、
tokens
の全ての要素のMeCabフォーマットを出力します。
これを実行すると以下のような出力が得られるはずです!
$ ruby test3.rb
(省略)
名詞,固有名詞,人名,一般,*,*,ムラサ,ムラサ,ムラサ,東方Project
助詞,格助詞,一般,*,*,*,に,ニ,ニ
記号,括弧開,*,*,*,*,「,「,「
感動詞,*,*,*,*,*,ねえ,ネエ,ネー
名詞,一般,*,*,*,*,星,ホシ,ホシ
記号,読点,*,*,*,*,、,、,、
名詞,一般,*,*,*,*,いっしょ,イッショ,イッショ
助詞,格助詞,一般,*,*,*,に,ニ,ニ
名詞,一般,*,*,*,*,野球,ヤキュウ,ヤキュー
助詞,並立助詞,*,*,*,*,や,ヤ,ヤ
名詞,一般,*,*,*,*,ろう,ロウ,ロー
記号,一般,*,*,*,*,!,!,!
記号,括弧閉,*,*,*,*,」,」,」
助詞,格助詞,引用,*,*,*,と,ト,ト
動詞,自立,*,*,五段・ワ行促音便,未然形,誘う,サソワ,サソワ
動詞,接尾,*,*,一段,連用形,れる,レ,レ
記号,読点,*,*,*,*,、,、,、
名詞,一般,*,*,*,*,ボール,ボール,ボール
ばっちりですね!
4. TF-IDF法を利用した特徴語抽出
創想話の最新版から適当なSSのテキストを取得してMeCab(+ 東方MeCab辞書)を用いて代表キーワード候補を選出し、TF-IDF法による特徴語抽出をしてみましょう。 その前に、DFを求めるために全ドキュメントから任意の値が含まれているドキュメントの数を調べなければなりません。創想話の検索機能は残念ながら正確なヒット数を返してくれないので今回はSSさがすよ!を利用してヒット数を調べましょう。そのためにRubyからSSさがすよ!にアクセスするライブラリを作りました。
SSさがすよ!のRubyバインディング(ugigi-ruby)をインストール
$ gem install ugigi
必要なライブラリが揃ったところでコードを書きましょう。代表キーワード候補はシンプルに名詞のみに絞って選出しました。
#!/usr/bin/env ruby
# coding: utf-8
# test4.rb
require "mecab-modern"
require "kconv"
require "sosowa"
require "ugigi"
mecab = MeCab::Tagger.new
novel = Sosowa.get.sample.fetch
puts "-"*30
puts novel.title
puts "作者: #{novel.author.name}"
puts "-"*30
text = novel.plain
tf = {}
n = 15699.0 #=> 創想話に登録されているSSの数
puts "代表キーワード候補を抽出中..."
tokens = mecab.parseToNode(text)
tokens.each do |token|
next unless token.feature =~ /名詞/
tf[token.surface] ||= 0
tf[token.surface] += 1
end
puts "代表キーワード候補数: #{tf.size}"
tfidf_list = []
tf.each do |e|
print "TF: #{e[0]} ... \t"
df = Ugigi.total_count(:free => e[0], :sswp => 0, :compe => 0)
if df == 0
print "N/A\n"
tfidf_list << [e[0], 0]
next
end
print "DF: #{df} \t"
tfidf = e[1] * Math.log(n/df)
print "TF-IDF: #{tfidf}\n"
tfidf_list << [e[0], tfidf]
end
tfidf_list = tfidf_list.sort{|a, b| b[1] <=> a[1]}
puts "集計終わり!"
10.times do |n|
l = tfidf_list[n]
puts "#{n+1}. #{l[0]} \tTF-IDF: #{l[1]}"
end
解説するのが面倒なので実行結果を見てフィーリングで理解してください(投)
$ ruby test4.rb
$ ------------------------------
プラスチックハート
作者: 猫井はかま
------------------------------
代表キーワード候補を抽出中...
代表キーワード候補数: 362
TF: 蓄音機 ... DF: 54 TF-IDF: 5.668986525680752
TF: 曲 ... DF: 4248 TF-IDF: 2.6075340186465024
TF: 蝉 ... DF: 491 TF-IDF: 6.923052888901011
TF: 声 ... DF: 13775 TF-IDF: 0.382079814671732
TF: 夏 ... DF: 3464 TF-IDF: 4.523373907688101
TF: 季節 ... DF: 2897 TF-IDF: 5.059618723427027
TF: 静けさ ... DF: 709 TF-IDF: 3.0941150457128983
TF: 制御 ... DF: 1127 TF-IDF: 2.6306560582052496
(省略)
TF: 札 ... DF: 2323 TF-IDF: 1.9073558394746168
TF: どちら ... DF: 5526 TF-IDF: 1.040751066761664
TF: </ ... DF: 11 TF-IDF: 7.2600752994466555
TF: > ... DF: 1610 TF-IDF: 2.273981114266517
集計終わり!
1. 芳香 TF-IDF: 236.26274459938386
2. —— TF-IDF: 35.857217044481985
3. ロボット TF-IDF: 30.04396190056222
4. 青娥 TF-IDF: 20.50148431636708
5. 心臓 TF-IDF: 19.8994053046905
6. span TF-IDF: 17.92964678337016
7. 鼓動 TF-IDF: 17.474439985140872
8. 僵尸 TF-IDF: 17.118716567153832
9. 作り物 TF-IDF: 16.810597827549298
10. 蓄音器 TF-IDF: 14.520150598893311
なんとなく、SSの内容がわかる気がします…。芳香がロボットを作って青娥の心臓をその中に押し込めてサイボーグ化させようと画策するも作り物に心が芽生えるわけもなくただ意味不明な単語を呟くだけの蓄音機になってしまった…的なバッドエンドなのでしょう、きっと(わかりません) 特徴語を抽出することでSSのタグ付けを自動化するツールを作ることもできますし、アイディア次第でもっと面白いツールをつくることが出来るでしょう!
次回予告
CaboChaを使って創想話テキストから主語-述語関係を抽出して遊びます。