Igo : Common Lisp版

Javaで作成していた形態素解析器のCommon Lisp版も作成(cl-igo)。
バイナリ辞書はJavaで作成したものを使用するようにし、辞書の読み込み・形態素解析部分だけをcommon lispで実装した。
ユニコード文字列に対応している処理系なら、多分動くはず...。※ 確認済み処理系: sbcl-1.0.32, clisp-2.42

以下、簡単な使い方と計時。

使い方

まだプロジェクトページには何もドキュメントを書いていないので、代わりにここで使い方などを簡単に説明しておく。

;;;; sbcl-1.0.32

;;;;;;;;;;;;;;;;;
;;;; インストール 
;; 上記リンクから、cl-igo-0.1.0.tar.gzをダウンロードしておく

;; asdf-installを使ってインストール
> (require :asdf-install)
> (asdf-install:install "cl-igo-0.1.0.tar.gz")

;;;;;;;;;;;;;;;;;
;;;; 辞書読み込み
> (require :igo)

;; 読み込み: 結構時間が掛る
> (defvar *tagger* (igo:load-tagger "Javaで作成したバイナリ辞書があるディレクトリ"))

;;;;;;;;;;;;;;;
;;;; 形態素解析
;; 形態素解析
> (igo:parse *tagger* "すもももももももものうち")
     ;; ((表層形 素性文字列 開始位置)) 形式のリストが返る
--> (("すもも" "名詞,一般,*,*,*,*,すもも,スモモ,スモモ" 0) ("も" "助詞,係助詞,*,*,*,*,も,モ,モ" 3)
     ("もも" "名詞,一般,*,*,*,*,もも,モモ,モモ" 4) ("も" "助詞,係助詞,*,*,*,*,も,モ,モ" 6)
     ("もも" "名詞,一般,*,*,*,*,もも,モモ,モモ" 7) ("の" "助詞,連体化,*,*,*,*,の,ノ,ノ" 9)
     ("うち" "名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ" 10))

;; 分かち書き
> (igo:wakati *tagger* "すもももももももものうち")
--> ("すもも" "も" "もも" "も" "もも" "の" "うち")

parse関数が返す結果は、(辞書読み込み時に)若干カスタマイズが可能。

;;;; IPA辞書フォーマットに特化したカスタマイズ 
;; load-tagger関数の第二引数には、素性文字列のパース(マッピング)関数が渡せる  ※ デフォルトはidentity関数
> (setf *tagger* (igo:load-tagger "..." igo:*ipadic-feature-parser*))

> (igo:parse *tagger* "すもももももももものうち")
--> (("すもも" (:名詞 :一般 :* :* :* :* "すもも" "スモモ" "スモモ") 0)
     ("も" (:助詞 :係助詞 :* :* :* :* "も" "モ" "モ") 3)
     ("もも" (:名詞 :一般 :* :* :* :* "もも" "モモ" "モモ") 4)
     ("も" (:助詞 :係助詞 :* :* :* :* "も" "モ" "モ") 6)
     ("もも" (:名詞 :一般 :* :* :* :* "もも" "モモ" "モモ") 7)
     ("の" (:助詞 :連体化 :* :* :* :* "の" "ノ" "ノ") 9)
     ("うち" (:名詞 :非自立 :副詞可能 :* :* :* "うち" "ウチ" "ウチ") 10))

> (funcall igo:*ipadic-feature-parser* "A,B,C,D,E,F,G,H,I")
--> (:A :B :C :D :E :F "G" "H" "I")

;;;; 素性文字列の最初の一文字目以外は不要な場合
> (setf *tagger* (igo:load-tagger "..." (lambda (feature) (subseq feature 0 1))))

> (igo:parse *tagger* "すもももももももものうち")
--> (("すもも" "名" 0) ("も" "助" 3) ("もも" "名" 4) ("も" "助" 6) ("もも" "名" 7) ("の" "助" 9)
     ("うち" "名" 10))

計時

前回と条件を合わせて計時。

計時用プログラム

(require :igo)

(defvar *tagger* (igo:load-tagger "/path/to/dic-dir/"))

#+SBCL (sb-ext:gc :full t) 

;; 行読み込みマクロ
(defmacro each-file-line ((line filepath &rest keys) &body body)
  `(with-open-file (#1=#:in ,filepath ,@keys)
     (let (,line)
       (loop WHILE (setf ,line (read-line #1# nil nil nil)) DO
         (progn ,@body)))))

;; 行読み込み時間
(time
  (each-file-line (line "/path/to/kokoroX256")))

;; 解析時間
(time
  (each-file-line (line "/path/to/kokoroX256")
    (igo:wakati *tagger* line)))

結果

総処理時間(1)行読み込み時間(2)1 - 2
MeCab28.311s0.216s28.095s
Igo(Java)33.901s0.996s32.905s
cl-igo(sbcl)36.084s*10.933s35.151s
残念ながら、この中ではcommon lisp版が一番遅かった。
Java版より速くなることを期待して作っていたけど、無理だった...。

追記(2010/03/23): cl-igo-0.2.0

開発時の利便性を考慮して、APIを若干修正。

> (require :igo)

> (igo:load-tagger "/path/to/dic-dir") 
--> #<IGO::TAGGER {EF9A509}>

;; load-tagger関数呼び出し結果は、igo:*tagger*変数に自動的に保存される
> igo:*tagger*
--> #<IGO::TAGGER {EF9A509}>

;; parse・wakati関数の第二引数にtaggerを渡すように変更。
;; デフォルトでは、igo:*tagger*が使われる。
> (igo:wakati "すもももももももものうち")
--> ("すもも" "も" "もも" "も" "もも" "の" "うち")

;; これは上の式と等価
> (igo:wakati "すもももももももものうち" igo:*tagger*)
--> ("すもも" "も" "もも" "も" "もも" "の" "うち")

というか、ちゃんとしたドキュメントを書かなくちゃいけない...。
まだJavaのバイナリ辞書構築部分も不完全*2だし...。
でも他にもいろいろやりたいことが...。

追記(2010/04/01): cl-igo-0.2.3 + sbcl-1.0.37

見出しの構成で計測を行ったところ、33.962sで処理が終了した。
これで、Java版とほぼ同様の速度*3

*1:追記(2010/04/01): ver-0.2.3では34.422s

*2:エラーハンドリングとかCSVパーサとか

*3:ちなみに、Javaは文字をUTF-16で表現しており、0xFFFF以上のコード値の文字を表すためにはサロゲートペアという仕組みを使っている。対してsbclの場合はUTF-32であり、Javaで作成されたバイナリ辞書を利用する場合、サロゲートペア用の特殊な処理が必要になる。実はその点でsbclは条件的に若干不利。サロゲートペア用の処理を外した場合、終了までの時間は33.687sとなり、Java版より若干速くなった。