月別アーカイブ: 2015年4月

gensimとword2vec

何かと話題のword2vecですが、自分も使ってみようと思って試行錯誤したことを書いてみます。間違いなどあればぜひコメントください。

GoogleのC実装

https://code.google.com/p/word2vec/にある、C言語による実装は独立したアプリケーションで、Apache 2.0ライセンスで提供されています。

ビルドした状態でword2vecコマンドを使って学習、モデルを作成し、distance等のコマンドからモデルをもとにデータを出力します。以下はword2vecを引数なしで実行したときの実行例です。

./word2vec -train data.txt -output vec.txt -size 200 -window 5 \
 -sample 1e-4 -negative 5 -hs 0 -binary 0 -cbow 1 -iter 3

各オプションの意味は以下になります。

  • train: 学習対象のテキストデータ(コーパス)
  • size: 次元数(デフォルトは100)
  • window: skip-gramの対象とする単語の最大数を指定(多分)デフォルトは5
  • sample: 単語を無視する際の頻度の閾値(デフォルトは1e-3)
  • threads: 処理するスレッド数
  • hs: Hierarchical Softmax(階層的Softmax)を使う(1)/使わない(0)
  • negative: ネガティブサンプリングに用いる単語数
  • binary: 出力結果をバイナリ形式にする(1)/テキスト形式(0)
  • cbow: Continuous Bag-of-Words 連続単語集合モデルを使う(1)/使わない(0)
  • iter: 学習する回数(イテレーション)の指定。デフォルトは5

CBOWとSkip-gramについてはFiS Projectの記事がわかりやすいです。階層的SoftmaxとネガティブサンプリングについてはnishioさんによるQiitaの記事が詳しいです。

gensim(Python)実装

gensimはPythonとCを組み合わせて記述された、文書解析ソフトウェアパッケージです。ライセンスはLGPL3+で、ソースはhttps://github.com/piskvorky/gensimで公開されています。

もともと複数の計算モデルを扱えるようになっていたようなのですが、2013年9月ごろにword2vecが追加されたようです。webで紹介される実装はこちらの方が多いようです。

Google実装はサンプル実装という面が強く、コマンドからできることに限りがあります。それに比べるとgensimはできることが多い(任意の数の単語のpositive/negativeな演算ができる)のですが、モデル構築にメモリをより使用します。

1GBのテキストをコーパスとしてgensimのword2vecに与えたところ、メインメモリ8GBのマシンでスラッシングを起こしてしまいました。Google実装であれば、1GB程度のテキストはオンメモリで処理できるようです。また、POSIX threadによるマルチスレッド処理にも対応しています。

幸いなことに、gensimのWord2VecではGoogle実装のモデルを読み込ませることができます。大きなデータを扱う際には、これらを組み合わせるとよい感じです。

from gensim.models import word2vec
model = word2vec.Word2Vec.load_word2vec_format("model.bin", binary=True)

pythonで処理したモデルもファイルに保存することができます。

from gensim.models import word2vec

model = word2vec.Word2Vec.load_word2vec_format("model.bin", binary=True)
model.save("gensim-model.bin")
#model.load("gensim-model.bin")

gensimでは、trainメソッドで後から追加学習させることもできるのですが、既に構築したモデルに存在しない単語は扱えない(語彙が増やせない)ようです。これに気付かず、分割したテキストデータをちょっとづつ学習させるというようなことをやらせようとしてはまりました。

結局、大きなコーパスを取り扱う際にはC実装でモデルを構築して、それをgensimで読み込むという使い方をするのが良いようです。

その他参考にした資料

追記(2015/5/20)

gensimでword2vecの処理がシングルスレッドでしかできないような表現でしたが、Word2Vecの引数にworker=2等とすることでマルチコア動作することに気付きました。それでも処理速度はGoogle実装の方が早いとは思いますが、記録として残しておきます。

LanguageToolのgrammer.xml

先日の東京エリアDebian勉強会ハックタイムで、もう少しLanguageToolをみて行きました。いくつか誤解があったのと、gitのコードを見ていて気づいたことを記録します。

私が参照していたJapanese.javaがgrammar.xmlでカバーできないケースをソースコードレベルで対応するものだと勘違いしていたのですが、ここは単にテキストの分割と品詞情報の付加を行うための処理を記述するもののようでした。

grammar.xmlはもっと柔軟性が高いようで、LanguageTool Wikiに高度な処理を行う方法も記述されています。一方でやはりgrammar.xmlだけで対応しきれないケースはJavaで書けることも記述されています。

今回追加しようと考えていたのは、長音符「-」の直前の文字がひらがな、カタカナ以外のケースです。基本的にそれはありえないので、LanguageToolが指摘すべき問題とすべきでしょう。

Doc-ja Wikiに書いた方法で-tオプションを利用すれば、そのようなケースの時に「-」はどのような品詞情報が付加されるかを調べることができます。

<br /><br />$ echo "実行時のダイナミック隣家ーの挙動" |  \<br /> java -jar languagetool-commandline.jar -t -l ja-JP -c UTF-8<br />Working on STDIN...<br />&lt;S&gt; 実行[実行/名詞-サ変接続]時[時/名詞-接尾-副詞可能]の[の/助詞-連体化]ダイナミ ック[ダイナミック/名詞-形容動詞語幹]隣家[隣家/名詞-一般]ー[ー/未知語]の[の/助詞-連体化]挙動[挙動/名詞-一般,&lt;/S&gt;]<br />Time: 527ms for 0 sentences (0.0 sentences/sec)<br />

単独の長音符「ー」は「未知語」と解釈されることがわかりました。単純にtokenがーでPOSが未知語である情報が検出されたら警告すればよさそうです。

しかし現状のLanguageToolはmasterのバージョンが3.0となっており、grammar.xml自体の文法も変わっていることに気付きました。したがって、Doc-ja Wikiの説明も今後は修正が必要になります。

ということを把握したところで先日は時間がなくなってしまいました。grammar.xmlの解説だけでも結構ボリュームがあるので、これを翻訳できると自分を含めうれしい人はいるかなあと思いました。

日本語も扱える文章チェッカーLanguageTool(実用レベルとはいっていない)

以前から注目しているソフトウェアとして、LanguageToolがあります。Javaで書かれた文法チェッカーで、コマンドラインやFirefoxアドオン、Libre/OpenOfficeのプラグインとして動作します。

以前簡単な紹介をおこなったり、使い方をDoc-ja Wikiに書いたりしてみました。このころはversion 2.6でしたが、現在の最新版は2.9です。

残念ながら、日本語の対応状況については今も芳しくありません。日本語のルールの数はslideshareの資料では23種類でしたが、version 2.9の段階では43種類と増えてはいるものの、まだまだ不足している状態です。

$ cd LanguageTool-2.9
$ grep 'rule id=' ./org/languagetool/rules/ja/grammar.xml|wc -l
43

単語ベースでのルールも少ないのですが、コードベースで対応すべきケースについては現状皆無です(https://github.com/languagetool-org/languagetool/blob/master/languagetool-language-modules/ja/src/main/java/org/languagetool/language/Japanese.java)。

先日、こちらにコードを追加しないと対応できなさそうな実例が出てきたので、それを考えてみようと思います。

以前grammer.xmlのプルリクエストを送った時は、パッケージ全体をビルドする必要がなかったので、ビルド環境がそろっていなかったことに気付いていませんでした。

ビルドにはmavenが必要なので、Debian wheezy上でapt-get install mavenを行ってbuild.shをたたいてみたのですが…

$ ./build.sh languagetool-standalone clean package |& tee log
Running: mvn --projects languagetool-standalone --also-make clean package
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] languagetool-parent
[INFO] LanguageTool Style and Grammar Checker Core
[INFO] English module for LanguageTool
[INFO] Persian module for LanguageTool
[INFO] French module for LanguageTool
(略)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.308s
[INFO] Finished at: Fri Apr 17 16:07:19 JST 2015
[INFO] Final Memory: 10M/112M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.2:compile (default-compile) on project languagetool-core: Fatal error compiling: 1.7 は無効な VM バージョンです。 -> [Help 1]
[ERROR]

JVMが古すぎると怒られてしまいました。ということでsid環境で試したところ、無事ビルドできました。続きは明日の東京エリアDebian勉強会でもやってみようと思います。

KAKASIの写経とバグ修正

ここのところKAKASIのコードをいじっています。昔から再実装したいと考えていたのですが、コードを十分理解していない部分もあったので、目視でソースを見ながらエディタで書き写す「写経」的なことをやってみました。

写経の過程でいくつかの潜在的なバグが発見でき、本体にコミットすることもできました。内容は以下です。

それ以外にもいくつかコミットがありますが、直接的なバグではないもの(即値の代入、不適切なマクロ定数の利用、実害のないロジックミス)ばかりです。

それ以外にも問題はありましたが、それらは修正に関して検討が必要なものだったので、Tracにチケットを作る程度にとどめています。

写経したKAKASIのコードも一応gitにおいてあります。ただし動作検証は行っていません。あくまで実験として行ったものです。環境変数で辞書のディレクトリを指定すればおそらく動作はするはずです。

作業に関して、ソースの可読性を高めるため以下のことを念頭に行いました。

  • K&Rスタイルの廃止
  • 単純なマクロ定義だった定数をいくつかenum化
  • 配列定数として埋め込んでいたいくつかのテーブルを外部ファイル化
  • 文字エンコーディング定数でマジックナンバーだったものをマクロ化(dict.cのみ)
  • CMakeの採用

特に文字エンコーディングがらみはかなりわかりやすくなったのではないかと思います。旧来のコードの一部を引用してみます。

    while(*p != '\0') {
        if (*p == '\033') {
            if ((p[1] == '$') &&
                ((p[2] == '@') || (p[2] == 'B'))) {
                kanji = 1;
                p += 2;

ESC $ BというのはISO-2022-JPでG0をJIS X 0208に指定するためのエスケープシーケンスですが、これをマクロで以下のように書き換えました。

    while (*p != NULLCHAR) {
        if (*p == ESCAPE) {     /* ESC (0x1b) */
            if ((p[1] == G0_JISX0208_1ST) &&
                ((p[2] == G0_JISC6226_2ND) || (p[2] == G0_JISX0208_2ND))) {
                kanji = 1;
                p += 2;

コメントがなくても意味が理解しやすくなったと思っています。まだ道半ばではありますが、ゆくゆくはfrom scratchなコードを起こしたいと思っています。

tty emacsで表示できない絵文字をゲタで代替表示

普段emacsをputtyからUTF-8で使うことが多いのですが、最近よくとある絵文字の表示に遭遇すると表示が崩れるという事態にあったので、なんとかできないかと対処してみました。

前提として、https://github.com/kachie/emacs.d/blob/master/init.d/w32-init.elと同様の設定をしています。この設定では、いくつかの文字をputtyのUTF-8設定で正しく表示できるようなutf-8-for-puttyというエンコーディングを定義しています。その中でcp5022x.elという機種依存文字を表示させるためのelispも使われているのですが、その中身を見ながら見よう見まねで書いてみました。

変換テーブルの定義

置き換えの対象となる絵文字のテーブルを自作します。

(define-translation-table
  'emoji-to-ascii
  '((#x1F440 . #x3013))) ;; eyes -> geta

#x1F440は絵文字の目玉で、これをゲタ(〓 #x3013)に置き換えるテーブルです。

エンコーディングの定義

utf-8-for-puttyエンコーディングの定義部分を修正します。

(apply 'define-coding-system 'utf-8-for-putty
       "UTF-8 (translate jis to cp932)"
       :encode-translation-table
;;     (get 'japanese-ucs-jis-to-cp932-map 'translation-table) ;; original
       '(emoji-to-ascii (get 'japanese-ucs-jis-to-cp932-map 'translation-table))
       (coding-system-plist 'utf-8))

表示幅の定義

オリジナルのset-east-asian-ambiguous-widthテーブルに目玉のコードポイントを追加します。

(defun set-east-asian-ambiguous-width (width)
  (while (char-table-parent char-width-table)
    (setq char-width-table (char-table-parent char-width-table)))
  (let ((table (make-char-table nil)))
    (dolist (range
             '(#x00A1 #x00A4 (#x00A7 . #x00A8) #x00AA (#x00AD . #x00AE)
(中略)
;;                    #xFFFD   ;; original
                      #xFFFD #x1F440 ;; 変換前のコードポイントを指定
                      ))
      (set-char-table-range table range width))
    (optimize-char-table table)
    (set-char-table-parent table char-width-table)
    (setq char-width-table table)))
(set-east-asian-ambiguous-width 2)

これで自分が望む出力が得られるようになりました。

非tty Emacsの場合

今回の目的には合いませんでしたが、画像が表示できる環境ではemoji.elが良いようです。こちらを使うと、絵文字がイメージとして表示されます。キャリア別の絵文字も扱えるようで、なかなか高機能です。


 

将軍の名前を補完するshogun-completion

かつて、どこかで「プログラマが徳川将軍の名前を覚えられないのはshellで補完できないからだ」という言説を見かけ、それを解消するべくbashで徳川幕府の将軍名を補完できるshogun-completionというものを作成しました。

当時課題だった問題2点に対応すべく、このたび改良版を公開しました。

鎌倉幕府対応

以前の実装では徳川幕府のみにしか対応していなかったので、オプションにkamakuraを入力した上で補完をすると、鎌倉幕府の歴代将軍を補完できるように修正しました。

shogunコマンドの実装

以前の実装は実在しないshogunコマンドに対する補完、という実装だったのですが、このたびshogunコマンドも実装しました。開発言語にはgolangを用いています。ごーしょーぐんとでも呼んでください。

shogunコマンドの引数に将軍名を入力すると、何代目であるかの情報を出力します。これで以前の実装では対応できなかった、「当該将軍が何代目であるかわからない」という問題にも対応できました。

bashにおける補完の限界

実のところ、zshの補完であればもっと高度なことができます。将軍名を補完する際にそれが何代目であるかの情報を表示するといったことも可能です。

じゃあbashでできないかというと、頑張ればできそうではあります。”bash autocompletion: add description for possible completions“というStackOverflowのやり取りに一例がありました。以下はそのコードの転載です。

_telnet() {
  COMPREPLY=()
  local cur
  cur=$(_get_cword)
  local completions="10.10.10.10 - routerA
10.10.10.11 - routerB
10.20.1.3 - routerC"

  local OLDIFS="$IFS"
  local IFS=$'\n'
  COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
  IFS="$OLDIFS"
  if [[ ${#COMPREPLY[*]} -eq 1 ]]; then #Only one completion
    COMPREPLY=( ${COMPREPLY[0]%% - *} ) #Remove ' - ' and everything after
  fi
  return 0
}
complete -F _telnet -A hostnames telnet

ローカル変数completionsに行区切りで補完したい情報を代入しています。その結果を、IFSに改行のみを設定したうえで、compgenに処理させています。これで、入力にマッチする補完候補のみが配列としてCOMPREPLYに代入されます。さらに配列の数が1つだった場合、文字列” – “以下の部分を切り取って返します。その結果、この例では入力したいIPアドレスのみが実際には補完される、というわけです。

このアプローチを自分も試してみたのですが、bashのバージョンのせいか、IFS=”\n”とした状態でもcompgenの出力全体を代入された1個の配列にしかなりませんでした。IFSを使わない方法も試してみたのですが、思うように複数個の配列にできなかったのでそこで断念しました。あと、当該記事にも書かれていますが、IFSの変更は影響が大きいのでむやみにやらない方がいいようです。