« 2006年03月 | メイン | 2006年07月 »

2006年05月28日

デストラクタで longjmp

MeCab のエラー処理のため、デストラクタで longjmp する凶悪なプログラムを書いたのですが、やはり自分自身の解放がうまくいってないようです。VC7 は、ご丁寧にメモリーリークの可能性がありますと言ってくれます。
struct Bar {                                                                                                                                                         
   ~Bar() {                                                                                                                                                          
      std::cout << "Bar deleted" << std::endl;                                                                                                                       
   }                                                                                                                                                                 
};                                                                                                                                                                   
                                                                                                                                                                     
struct Foo {                                                                                                                                                         
   jmp_buf *jmp_;                                                                                                                                                    
   Bar bar;                                                                                                                                                          
   Foo(jmp_buf *jmp): jmp_(jmp) {}                                                                                                                                   
   ~Foo() { longjmp(*jmp_, 1); }                                                                                                                                     
};                                                                                                                                                                   
                                                                                                                                                                     
int main(int argc, char **argv) {                                                                                                                                    
   jmp_buf jmp;                                                                                                                                                      
   if (setjmp(jmp) == 1) {                                                                                                                                           
      std::cout << "retrun 0" << std::endl;                                                                                                                          
      return 0;                                                                                                                                                      
   } else {                                                                                                                                                          
      Foo foo(&jmp);                                                                                                                                                 
   }                                                                                                                                                                 
}         
foo のデストラクタで longjmp が呼ばれ setjmp(jmp) == 1 のところに大域ジャンプします。これで return 0 となってプログラムは終了するのですが、Bar のデストラクタは呼ばれません。つまり Foo は開放されない

考えてみれば当たり前なのですが、こういうときはどうすればいいのだろう。こういう変なことはやらないほうが いいというのはごもっともな指摘ですが..

投稿者 taku : 15:02 | コメント (13) | トラックバック

2006年05月22日

MeCab: Perl のコードを駆逐

MeCab の 辞書コンパイラは一部 Perl で書かれています。その理由として、実装が楽だという理由もありますが、品詞等の素性から内部状態への変換ルール(rewrite.def) を Perl で書くようにユーザに強制していたと理由があります。しかし、開発する上でいろいろ問題がでてきました。

- CSV の parse, コマンドライン解析など、共通して使えるはずのコードが分散している。
- 速度が要求されるもの (ダブル配列の変換等)は、C++ で書かれたバックエンドプログラムが動くのですが、そのツールとの通信に pipe をつかっててかっこ悪い。
- バイナリ辞書の書き込み/読み込みが perl と C++ の二つに分散されているため、メンテナンス性が悪い
- Windows 環境では Perl をインストールしないと辞書の再構築が難しい


連休ぐらいから、休日を利用して Perl のコードを駆逐する作業をすすめた結果、ようやく大枠の移行が終了しました。目に見える違いといえば、辞書のコンパイル速度が4~5倍速くなりました。

/usr/local/libexec/mecab/mecab-dict-index  41.68s user 5.38s system 96% cpu 48.559 total
../src/mecab-dict-index  9.72s user 1.05s system 96% cpu 11.125 total

投稿者 taku : 01:11 | コメント (9) | トラックバック

2006年05月03日

MeCab: 字種に基づくわかち書き

前回の N-gram に引き続き、字種に基づく分かち書きを MeCab だけで実現してみます。
日本語ほど字種が多い言語はありません。ひらがな、カタカナ、漢字、アルファベット、数字、記号..などなど。これらはわかち書きをする上で非常に重要な情報です。MeCab + ipadic の場合、未知語は字種に基づく発見的な手法 (heuristics) で切り出しています。
今回は、辞書はまったく使わず、この字種情報だけで分かち書きをしてみます。単純に「同じ字種のものをまとめて出力する」といった塩梅です。

例によって、MeCab の辞書の構成のドキュメントはこちらにあります。基本的に
1. dic.csv (辞書ファイル)
2. matrix.def (連接ファイル)
3. char.def (文字種ファイル)
4. unk.def (未知語処理)
5. dicrc

から辞書が構成されます。今回も辞書そのものを使わないので、 dic.csv には適当なダミー単語を入れておきます。
dic.csv
_______,0,0,0,*


次に char.def です。これは mecab-ipadic-2.7.0-20060408.tar.gz にある char.def を修正して使います。char.defは UCS2 のどのコードポイントがどの文字種に対応するのかという定義を与えます。文字種そのものの定義はそのまま使い、各文字種に対する未知語処理定義を変更します。ファイルの最初のほうにある各文字種に対する未知語処理定義を以下のように変更します。
DEFAULT        0 1 0  # DEFAULT is a mandatory category!
SPACE          0 1 0
KANJI          0 1 0
SYMBOL         0 1 0
NUMERIC        0 1 0
ALPHA          0 1 0
HIRAGANA       0 1 0
KATAKANA       0 1 0
KANJINUMERIC   0 1 0
GREEK          0 1 0
CYRILLIC       0 1 0

すべての未知語処理を "0 1 0" にしています。これは 「1) 辞書に単語があるときは未知語処理を動かさない, 2) 同じ字種でまとめて単語とする 3) 0 文字までの部分文字列を単語として追加する。」という意味になります。 とくに 2) の部分が大事で、これにより同じ字種でまとめる動作が実現されます。

次に unk.def. これは未知語に対してどんな品詞を割り当てるかを定義します。辞書のフォーマットとまったく同じですが、単語の部分に char.def で定義した文字クラスを書きます。今回の例では、品詞情報を使わないので、"*" を出力するだけにします。
DEFAULT,0,0,0,*
SPACE,0,0,0,*

matrix.def. 連接は使わないので、1x1 のマトリックスにします。
1 1
0 0 0

最後に dicrc。ほとんどデフォルトです。
cost-factor = 800
bos-feature = BOS/EOS

ここまでそろったら辞書をコンパイルしてみます。
/usr/local/libexec/mecab/mecab-dict-index -f euc-jp -c euc-jp

実際に解析させてみましょう。
% echo "MeCabを使って文字種によるワカチガキを実現してみた。" | mecab -d. -Owakati
MeCab  を 使 って 文字種 による ワカチガキ を 実現 してみた 。 

文字の種類ごとにまとめられています。

まとめ:
MeCab は辞書を使わない分かち書きをいくつかサポートしています。基本的に、文字種の定義とそれに対する未知語処理の定義で記述します。もちろん記述力としては正規表現にくらべれば乏しいですが、これまでの形態素解析器の未知語処理はほとんどの場合ハードコーディングされていたので、それにくらべれば進歩していると思います。

投稿者 taku : 20:02 | コメント (90) | トラックバック

MeCab を使って N-gram を取り出す。

Senna や HyperEstraier といった最近の検索システムでは n-gram インデックスが使われることが多くなってきました。正確には文字 n-gram ですが、(単語 n-gramとの対比) ようするに、テキスト中の n 以下までのすべての部分文字列を取り出して index に使う処理のことを言います。
n-gram の取り出しは、すごく簡単で、プログラミングしてもたいした量にはなりませんが、ここはあえて MeCab だけでやってみたいと思います。
まず、mecab-0.91 (src/tokenizer.cpp) に以下のパッチを当てます。(もしくは最新の CSVからソースを拾ってきます) 実際この記事を書くにあたって見つけたバグです。
4c4
<   $Id: tokenizer.cpp,v 1.13 2006/05/03 07:56:28 taku-ku Exp $;
---
>   $Id: tokenizer.cpp,v 1.12 2006/02/07 20:08:48 taku-ku Exp $;
251d250
<       if (begin3 > end) break;

MeCab の辞書の構成のドキュメントはこちらにあります。基本的に
1. dic.csv (辞書ファイル)
2. matrix.def (連接ファイル)
3. char.def (文字種ファイル)
4. unk.def (未知語処理)
5. dicrc

から辞書が構成されます。n-gram の場合は、辞書そのものを使わないので、 dic.csv は空にします。とはいっても空の辞書は受け付けてくれないので、適当なダミー単語を入れておきます。
dic.csv
_______,0,0,0,*

"_________" がダミー単語ですが、テキストに現れそうにない文字列を適当に入れておきます。0,0,0 は左文脈id, 右文脈id, 単語生起コストですが、それぞれ 0 にします。最後の "*" は出力に相当します。
次に char.def です。
DEFAULT 0 0 3
SPACE   0 1 0
0x0020 SPACE

すべての字種を DEFAULT というクラスにマッピングしています。 "0 0 3" というのは、この字種に対する未知語の動作方針です。具体的には 1)常に未知語処理を動かす0/1 2) 同じ字種でまとめる 0/1 3) 何文字までの単語を未知語として登録する 0,1,2...) となっています。"0 0 3" の意味するところは、3-gramまでの部分文字列を未知語として取り出しなさいという意味になります。
次に unk.def. これは未知語に対してどんな品詞を割り当てるかを定義します。辞書のフォーマットとまったく同じですが、単語の部分に char.def で定義した文字クラスを書きます。今回の例では、品詞情報を使わないので、"*" を出力するだけにします。
DEFAULT,0,0,0,*
SPACE,0,0,0,*

matrix.def. 連接は使わないので、1x1 のマトリックスにします。
1 1
0 0 0

最後に dicrc。ほとんどデフォルトです。
cost-factor = 800
bos-feature = BOS/EOS

ここまでそろったら辞書をコンパイルしてみます。
/usr/local/libexec/mecab/mecab-dict-index -f euc-jp -c euc-jp

解析対象の文字コードにあわせて -c オプションを選択する必要があります。(辞書の文字コードに従って適切な マルチバイト文字列処理を行うため)
実際に解析させてみましょう。注意点としては、"-a (--all-morphs)" オプションを使って、すべての単語候補を出力してください。さもないと、uni-gram のみが出力されます。
% echo 東京特許許可局 | mecab -d . -a
東京特  *
東京    *
東      *
京特許  *
京特    *
京      *
特許許  *
特許    *
特      *
許許可  *
許許    *
許      *
許可局  *
許可    *
許      *
可局    *
可      *
局      *
EOS

すべての 3文字以下の部分文字列が出力されているはずです。
何に使うのかという話がありますが、n-gramの統計をお手軽に取りたいとき簡単に利用できると思います。

投稿者 taku : 17:01 | コメント (23) | トラックバック

Text::MeCab が公開された

http://search.cpan.org/~dmaki/Text-MeCab-0.02/
http://d.hatena.ne.jp/lestrrat/20060502

DMAKI氏による MeCab の Perl モジュールが CPANにアップロードされたようです。SWIG で生成されたものより高速に動作するようです。こんなに差が出るとは正直驚きです。 MeCab::Node の iterator をまわして要素を取りだす処理は、SWIG の場合 tie hash になったり、 正直遅いと想像していたのですが、お見事です。

実用上はほとんど問題にならないと思いますが、いくつか問題があります。

まず、MeCab の入力文字列は、内部では一切コピーされず、文字列へのポインタのみを操作して解析が行われます。MeCab::Node::surface は、その文字列へのポインタを格納しています。つまり、
MeCab::Tagger *tagger = MeCab::createTagger("");
const char *s = "今日もしないとね";
MeCab::Node *node = tagger->parse(s);
s = "すもももももももものうち";  // 入力文字列を書き換える
for (MeCab::Node *n = node; n; n = n->next) {
   n->surface; // s をさしてたはずだけど、いずこへ?? 
}
のようなコードは思ったように動きません。n->surface が s をさしているからです。 これを回避するには、
1. プログラマが注意する
2. -C オプションで起動する
の方法があります。 -C オプションは内部で文字列をコピーして処理を行います。若干遅くなりますが、安全になります。

この話はスクリプトバインディングにも当てはまります。Text::MeCab の場合以下のコードはうまく動きません。ゴミ文字列が出力されます。
use Text::MeCab;                                                                           
$mecab = Text::MeCab->new("");                                                                                                               
my $s = "太郎は次郎が持っている本を花子に渡した。"; 
my $node = $mecab->parse($s); 
$s = undef;                                                                           
for (; $node; $node = $node->next) {                                                                               
    print $node->surface, "\n";                                                                                      
}

これを回避するには、-C オプションを使う必要があります。SWIG で作った MeCab モジュールは -C 付きで起動されます。うろ覚えですが、Java と C# は バインディングと GC の同期が取れないので、MeCab::Tagger::parse を呼んだ瞬間に GC されてしまうという事態が起きました。どーしようもないので、泣く泣く -C オプションを追加しました。

もうひとつ、細かい話ですが、MeCab::Node は、MeCab::Tagger が保持している MeCab::Node インスタンスへの参照にすぎません。そのため、以下のようなコードは意図したとおりに動きません。
use Text::MeCab;
$mecab = Text::MeCab->new("");
my $s = "太郎は次郎が持っている本を花子に渡した。";
my $s2 = "すもももももももものうち。";
my $node = $mecab->parse($s);
my $node2 = $node;
$node = $mecab->parse($s2);

for (; $node2; $node2 = $node2->next) {
    print $node2->surface, "\n";
}

$node2 に「太郎は花子に...」の解析結果を入れたはずなのに、実際に出力させてみると「すもも...」の 結果がはいります。これは、MeCab::Tagger が保持している参照先のデータが書き換わったからです。

MeCab 0.80 時代の SWIG は、内部でリファレンスカウントっぽいことをやって、この問題を回避していましたが、SWIGのインタフェイスがあまりにも複雑になってしまったので、0.90からはやめています。

これらが実用上大きな問題になることはほとんどないと思いますが、注意する必要があります。 とくにスクリプト言語は、「誰がどのデータを保持しているのか」ということ気にしなくていいので、それが表面化すると思わぬ落とし穴になってしまいます。

投稿者 taku : 04:31 | コメント (9) | トラックバック

2006年05月01日

mecab-0.91

mecab 0.91をリリースしました。 http://mecab.sourceforge.jp/
    *  Windows 環境で文字列の最後が半角スペースの時に落ちるバグの修正
    * 連接表の前件と後件のサイズが異なるときに正しく解析できないバグ の修正
    * mecab-dict-index に -f オプションを追加し, CSV の文字コードをユー ザが指定できるようにした
    * 一部の API関数が export されていない問題の修正
    * CRFの学習を pthread を使って並列に行えるようにした (experimental)
    * ユーザ辞書が作成できない問題の修正
    * example ディレクトリに MeCabの応用例を追加 (unittest)
    * その他細いバグの修正 

mecab-skkserv も同時リリースしましたので、こちらもよろしくお願いします。

投稿者 taku : 00:39 | コメント (13) | トラックバック