Tech-Draft

rubyとかscalaとかjavascriptとか?AndroidやiOSもチマチマと。

NodeとMeCabを用いたtweet連想ゲーム

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の17日目です。
ちょっと目先を変えて、MeCab形態素解析Wikipedia見出し辞書を利用したtweet連想ゲームについて書きます。

はじめに

このMeCab形態素解析を用いたtweet連想ゲームはもともと、1日目を書かれている@hakoberaさんが主催されているNode塾講義その3 Node.js を使ったコードをもくもく書く会で作ったものです。せっかくなのでリファクタリングして公開しますデス。

そもそもMeCabって?

MeCab京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース 形態素解析エンジンです. 言語, 辞書,コーパスに依存しない汎用的な設計を 基本方針としています. パラメータの推定に Conditional Random Fields (CRF) を用 いており, ChaSenが採用している 隠れマルコフモデルに比べ性能が向上しています。また、平均的に ChaSen, Juman, KAKASIより高速に動作します. ちなみに和布蕪(めかぶ)は, 作者の好物です.

http://mecab.sourceforge.net/

だそうです。自然言語形態素に分解して、各語の原型や品詞、活用形などを教えてくれます。
今回は「出発単語 → 単語をtwitter searchで検索 → 得られた本文を形態素解析 → 名詞を抽出し、ランダムに次の検策語を決定 → 単語をtwitter searchで検索・・・」という処理の形態素解析部分でMeCabを使います。

MeCabIPA辞書をインストール

最新バージョンをutf-8で使いたいので、http://mecab.sourceforge.net/#installあたりを参考に、現時点の最新版0.98をソースからインストールします。

$ wget http://sourceforge.net/projects/mecab/files/mecab/0.98/mecab-0.98.tar.gz
$ tar zxfv mecab-0.98.tar.gz
$ cd mecab-0.98/
$ ./configure --with-charset=utf8
$ make
$ sudo make install
$ sudo ldconfig
$ wget http://sourceforge.net/projects/mecab/files/mecab-ipadic/2.7.0-20070801/mecab-ipadic-2.7.0-20070801.tar.gz
$ tar zxfv mecab-ipadic-2.7.0-20070801.tar.gz
$ cd mecab-ipadic-2.7.0-20070801/
$ make
$ sudo make install

nodeのMeCabバインディングのインストール

探してみたらやっぱり、 Node用MeCabバインディング あと非同期版つくろうとして失敗した話 - mizchi logがありました。ありがとう、偉大な先達たち!
ただ紹介されているコードのままだと、いくつか問題があるため、index.jsに修正を加えました。実際のソースコードは、nmatsui/node-mecab - GitHubから取得してください。

  • requireするディレクトリの問題で0.6系のnodeで動かない => 0.4系の「build/default」が存在しない場合、 0.6系の「build/Release」からrequireする
  • MeCabインスタンス化する際にオプションが指定できない => オプションをつけたMeCabインスタンスを生成できる公開メソッドを追加する
$ git diff
diff --git a/index.js b/index.js
index f00c7ed..639f5a9 100644
--- a/index.js
+++ b/index.js
@@ -1,7 +1,24 @@
-var MeCab = new require('./build/default/mecab');
+var MeCab
+try {
+  MeCab = new require('./build/default/mecab');
+}
+catch (e) {
+  if (e.message.match('Cannot find module')) {
+    MeCab = new require('./build/Release/mecab');
+  }
+  else {
+    throw e;
+  }
+}
+
 exports.MeCab = MeCab;
 
 var nomal = new MeCab.Tagger();
+
+exports.options = function(opt) {
+  nomal = new MeCab.Tagger(opt);
+}
+
 exports.parse = function(text) {
   var buf, i, row, _i, _len;
   row = nomal.parse(text).split("\n");

実験実験!

$ git clone git://github.com/nmatsui/node-mecab.git
$ cd node-mecab/
$ npm install .
$ npm link
$ cd ../tweet-associate/
$ npm link mecab
$ node
> mecab = require('mecab');
{ MeCab: { Tagger: [Function: Tagger] },
  parse: [Function] }
> console.log(mecab.parse('すもももももももものうち'));
[ [ 'すもも', '名詞', '一般', '*', '*', '*', '*', 'すもも', 'スモモ', 'スモモ' ],
  [ 'も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ' ],
  [ 'もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ' ],
  [ 'も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ' ],
  [ 'もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ' ],
  [ 'の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ' ],
  [ 'うち', '名詞', '非自立', '副詞可能', '*', '*', '*', 'うち', 'ウチ', 'ウチ' ] ]
undefined
> 

おけ!

tweet連想ゲーム 初版

ということでMeCabのNodeバインドを用い、とある単語を含むtweetを検索し、その本文に含まれる単語から次のtweetを探し出すシンプルなコンソールアプリを書いてみました。
ただあまりにヘンな単語で検索しても楽しくないので、次の単語を連想する際に

  • 検索する単語の品詞は名詞のみ
  • 前回検索した単語と同じ単語は使わない
  • 1文字の単語は使わない
  • アルファベット、数字、記号のみの単語は使わない

という縛りを入れています。

#!/usr/bin/env nod
var http   = require('http');
var qs     = require('querystring');
var colors = require('colors');
var argv   = require('optimist').argv;
var mecab  = require('mecab');

var initialWord = argv.w || "javascript";

function findNoun(text, searchWord) {
  var nouns = mecab.parse(text).filter(function(morpheme) {
                  return typeof morpheme[0] != 'undefined' &&
                         morpheme[1] == '名詞'   &&
                         morpheme[0].length >= 2 &&
                         morpheme[0] != searchWord &&
                         !/^[!-~]+$/.test(morpheme[0])
                }).map(function(morpheme) {
                  return morpheme[0];
                });
  return nouns[Math.floor(Math.random()*nouns.length)];
}

function tweetAssociate(searchWord) {
  http.get({
    host: "search.twitter.com",
    port: 80,
    path: "/search.json?" + qs.stringify({
      q   : searchWord,
      rpp : "10",
      lang: "ja"
    })
  }, function(response) {
    var body = "";
    response.on('data', function(data) {
      body += data;
    });
    response.on('end', function() {
      var results = JSON.parse(body).results;
      if (results.length == 0) {
        console.log(searchWord + "のようなマニアックな単語はダメ!");
        setTimeout(function() { tweetAssociate(initialWord) }, 1000);
      }
      else {
        var tweet = results[Math.floor(Math.random()*results.length)];
        var nextWord = findNoun(tweet.text, searchWord);
        console.log(searchWord.bold.red + " => " + nextWord.bold.blue  + "\n");
        console.log("@" + tweet.from_user);
        console.log(tweet.text.replace(searchWord, searchWord.bold.red)
                              .replace(nextWord,   nextWord.bold.blue));
        console.log("====");
        setTimeout(function() { tweetAssociate(nextWord) }, 5000);
      }
    });
  });
}

tweetAssociate(initialWord);
$ node ./tweet-associate. -w 連想

って感じで起動すると、"連想"という単語から出発してtweet連想ゲームが勝手に進行します。

tweet連想ゲーム Wikipedia見出し辞書追加版

上記の初版でも動きますが、連想する単語はかなり短いものばかりです。固有名詞はほとんど抽出できませんし、複合語もうまく連想できません。そもそもMeCabの辞書に載っていないのですから、そりゃ無理ってものです。
そこでMeCab: 単語の追加方法MeCab の辞書構造と汎用テキスト変換ツールとしての利用を参考に、Wikipediaの見出し語をMeCabユーザー辞書として追加しましょう。このテキストファイルには、12/17時点で125万語ほどの見出し語が収集されています。

$ mkdir dic
$ cd dic
$ wget http://dumps.wikimedia.org/jawiki/latest/jawiki-latest-all-titles-in-ns0.gz
$ gunzip jawiki-latest-all-titles-in-ns0.gz

取得したWikipedia見出し語を、MeCab辞書として取り込める形式へ変換します。全て一般名詞扱いのため、活用などなく簡単です。
ただし重要なのは、語のコストです。なるべく長いキーワードを優先したいので、長いキーワードほど小さな値になるようにします。

#!/usr/bin/env ruby
# -*- encoding: utf-8 -*-
ARGF.each do |word|
  word.chomp!
  if word.length >= 2 && 
     /^[!-~]+$/.match(word).nil? &&
     /^.*(,|").*$/.match(word).nil?
    cost = [-36000, -400 * word.length**1.5].max.to_i
    puts "#{word},0,0,#{cost},名詞,一般,*,*,*,*,#{word},*,*"
  end
end

このスクリプトでWikipedia見出し語辞書の元ネタを作り、MeCabが認識できるバイナリ形式へコンパイルします。このWikipedia見出し語辞書を有効にしてmecabを実行してみると・・・

$ ./conv.rb jawiki-latest-all-titles-in-ns0 > wikipedia.csv
$ /usr/local/libexec/mecab/mecab-dict-index -d /usr/local/lib/mecab/dic/ipadic -u wikipedia.dic -f utf-8 -t utf-8 wikipedia.csv
$ mecab -u wikipedia.dic
すもももももももものうち
すもももももも	名詞,一般,*,*,*,*,すもももももも,*,*
もも	名詞,一般,*,*,*,*,もも,*,*
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

と、ノーマルなIPA辞書とは違いすももももも - Wikipediaを名詞として認識するようになりました。

nodeから追加辞書を指定して実行する場合、optionsメソッドを使います。

$ node 
> mecab = require('mecab');
{ MeCab: { Tagger: [Function: Tagger] },
  options: [Function],
  parse: [Function] }
> mecab.options("-u dic/wikipedia.dic");
undefined
> console.log(mecab.parse('すもももももももものうち'));
[ [ 'すもももももも', '名詞', '一般', '*', '*', '*', '*', 'すもももももも', '*', '*' ],
  [ 'もも', '名詞', '一般', '*', '*', '*', '*', 'もも', '*', '*' ],
  [ 'の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ' ],
  [ 'うち', '名詞', '非自立', '副詞可能', '*', '*', '*', 'うち', 'ウチ', 'ウチ' ] ]
undefined
> 

最後に、コンソールアプリを修正します。といっても、コマンドラインからMeCab用のオプションを取れるようにするだけです。

$ diff -u tweet-associate.js tweet-associate2.js 
--- tweet-associate.js	2011-12-17 13:09:32.166163004 +0900
+++ tweet-associate2.js	2011-12-17 15:47:59.896162998 +0900
@@ -6,7 +6,9 @@
 var mecab  = require('mecab');
 
 var initialWord = argv.w || "javascript";
-
+if (argv.u) {
+  mecab.options("-u " + argv.u);
+}
 function findNoun(text, searchWord) {
   var nouns = mecab.parse(text).filter(function(morpheme) {
                   return typeof morpheme[0] != "undefined" &&

では、実行してみましょう。なんとなく、長い複合語もとれるようになったと思います。

$ node ./tweet-associate2.js -w ゲーム -u `pwd`/dic/wikipedia.dic

最後に

MeCab楽しいですね!
今回は次の単語を連想する際に名詞をランダムに選んでいましたが、元の検索語とのレーベンシュタイン距離や単語長を元に「イイカンジの単語」を選ぶようにするともっと面白いかもしれません。が、力尽きました・・・orz

明日は、砂波幹雄さんです!