2017/4/13追記:taku910さん本人から助言をいただきました。詳細はコメント欄を見てください。本記事は追って修正します。
2017/4/18追記:公開しているAPIの方は修正しました。Unigramの方はまだモデル構築中です(5日間回しても終わらず)。
2017/4/26追記:Unigramの方もできました(12日間回してやっと終わった)。本件の詳細は別の記事で投稿しました。
はじめに
※無理やりWebAPIにしたので、間違ってたら指摘してください。
GoogleがSentencePieceを公開しました。NMT (Neural Machine Translation/ニューラル機械翻訳) で有効性が確認されているアプローチです。今回はそれを形態素解析のように使えるWebAPIにしてみました。無料で使えるので使ってみてください。
API
サンプルコード
関連記事
実装内容
簡単に解説すると、
- 日本語Wikipediaの記事にSentencePieceをかける
- SentencePieceの出力であるvocabファイルをmecab-ipadicの辞書形式に整形する
- kuromojiで辞書ごとコンパイルする
もう少し詳細に説明します。
1. 日本語Wikipediaの記事にSentencePieceをかける
手元にあった日本語Wikipediaのデータが20160915のdumpで少し古いですが、こいつを元データにしました。いくつか手を加えて整形しています。やったことを羅列すると
- dumpはwp2txtでテキスト化して、全部を一つのファイルにガッシャンコ
- 40文字以下の行は削除
- 行先頭が「Image」「File」「イメージ」「ファイル」は削除
- ファイルの行をランダムに入れ替える
- 先頭から1,700,000行(約600MB)を使ってSentencePiece実行
なんでこんなことをしているかと言うと、SentencePieceはメモリを大量に必要とするようで、私の手元のPC(メモリ16GB)では全文(約2GB)を入力できませんでした。色々と試行錯誤したところ、16GBのPCでは約600MBが限界みたいです。その代わり、メモリにデータが乗りさえすれば処理は早いです。
SentencePieceは以下のコマンドで実行しています。今更ですが、実行環境はWindows10にCygwinを入れて実行しました。本家のREADMEにあった必要ライブラリはCygwinでそれっぽいのを入れました(バージョンが一部合っていないですが、動いたことは動いた)。
$ ./spm_train --input=input.txt --model_prefix=output --vocab_size=8000 --model_type=bpe
出力はvocabファイルとmodelファイルです。vocabファイルは8000行の単語欠片です。
2. SentencePieceの出力であるvocabファイルをmecab-ipadicの辞書形式に整形する
さて、WebAPIで公開するための準備をします。WebAPIのレスポンスは「単語欠片」と「単語欠片ID」の組を配列で返すことにします。「単語欠片ID」は例えば機械学習でOne-Hotベクトルを作るときに使ってください。もちろん、使わなくても良いです。
さて、実装検討です。私が運営するWebAPIのマーケットプレイスApitoreは完全なJavaで実装していますが、SentencePieceはC++で書かれています。「ラッパー書くのは面倒だし、C++でWebAPIとかよくわからんし・・・ということで、ここは無理やり実現するしかない!SentencePieceも形態素解析みたいなもんでしょ」ってことで、普段からお世話になっているJavaの形態素解析器kuromojiを流用することにしました。kuromojiは有名なmecabのJava版です。そしてmecabはSentencePieceを作った工藤さんの研究技術です。つながってますね!
というわけで今回は、SentencePieceの出力を新しい辞書として既存のkuromojiに追加する形を取りました。その代わりに、いくつか工夫をしておきます。辞書の形式はこんな感じです。
#表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音
され,1,1,1,SPWORD,1,*,*,*,*,*,*,*
「表層形」がSentencePieceの出力である『単語欠片』です。「コスト」を『1』にする、ここがポイントです。「コスト」を『1』にすれば、ほぼ間違いなく形態素解析時にSentencePieceの単語欠片が選択されます。念のため、文脈の連接コストを定義するmatrix.def
で品詞の接続コストもすべて1に変更しておきます。こうすることで「文脈を考慮せず、ひたすらSentencePieceの単語欠片をつないでいく」ことが出来ます。文脈を気にする必要がなくなったので「文脈ID」は何でも良いです。今回は「文脈ID」を『1』としました。
「品詞細分類1」には『単語欠片ID』を振りました。『単語欠片ID』はSentencePieceの出力8000語に対して私がふったユニークなIDです(つまり全部で8000個のID、番号は1~8000までを使う)。SentencePieceの「品詞」は『SPWORD』としました。この品詞は、SentencePieceの対象外の語を見つけるために使います。少し説明すると、SentencePieceの単語欠片は学習データが元になっています。当然ながら学習データで一度も現れていない文字は扱いようがありません。その未知の文字を従来のkuromojiで検出することにしました。(未知の文字はほぼ間違いなく『未知語』に分類されると思いますが)品詞が『SPWORD』じゃないときは、単語欠片IDを『0』としました。これで未知文字も扱えます。
3. kuromojiで辞書ごとコンパイルする
あとはコンパイルするだけです。kuromojiを通常通りにコンパイルします。kuromojiに内包されるテストコードは絶対に通らないので、テストは削除してしまいましょう。
実際に使ってみる
APIはこちらで公開しています。APIコールまでの準備(API登録、アクセストークン発行、サンプル実行)はこちらを参考にしてください。
APIの入出力などの仕様はこちらで公開しています。一応ここにも書くと、APIレスポンスの仕様はこんな感じです。入力はテキストです。
{
"endTime": "string",
"log": "string",
"processTime": "string",
"startTime": "string",
"tokens": [
{
"token": "string",
"wid": 0
}
]
}
実際の使用例を見てみます。「吾輩は猫である。名前はまだない。」を入力してみました。たしかに、通常の形態素解析とは若干異なりますね。
"tokens": [
{
"wid": 5578,
"token": "吾"
},
{
"wid": 5386,
"token": "輩"
},
{
"wid": 472,
"token": "は"
},
{
"wid": 5643,
"token": "猫"
},
{
"wid": 11,
"token": "である"
},
{
"wid": 3796,
"token": "。"
},
{
"wid": 2002,
"token": "名前"
},
{
"wid": 472,
"token": "は"
},
{
"wid": 1914,
"token": "まだ"
},
{
"wid": 26,
"token": "ない"
},
{
"wid": 3796,
"token": "。"
}
]
続いて「WRYYYYYYYYYY!最高にハイってやつだアアア」。見事にバラッバラに分解されています。
"tokens": [
{
"wid": 829,
"token": "W"
},
{
"wid": 589,
"token": "R"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 3032,
"token": "Y"
},
{
"wid": 0,
"token": "!"
},
{
"wid": 799,
"token": "最高"
},
{
"wid": 2689,
"token": "に"
},
{
"wid": 646,
"token": "ハイ"
},
{
"wid": 9,
"token": "って"
},
{
"wid": 3880,
"token": "や"
},
{
"wid": 3888,
"token": "つ"
},
{
"wid": 3914,
"token": "だ"
},
{
"wid": 1726,
"token": "ア"
},
{
"wid": 1726,
"token": "ア"
},
{
"wid": 1726,
"token": "ア"
}
]
最後に「「恐怖」を克服することが「生きる」こと」を入力してみます。なかなか特徴的なセグメントしますね。
"tokens": [
{
"wid": 648,
"token": "「"
},
{
"wid": 5092,
"token": "恐"
},
{
"wid": 5725,
"token": "怖"
},
{
"wid": 3846,
"token": "」"
},
{
"wid": 2163,
"token": "を"
},
{
"wid": 5711,
"token": "克"
},
{
"wid": 4840,
"token": "服"
},
{
"wid": 543,
"token": "することが"
},
{
"wid": 648,
"token": "「"
},
{
"wid": 2859,
"token": "生き"
},
{
"wid": 3798,
"token": "る"
},
{
"wid": 3846,
"token": "」"
},
{
"wid": 12,
"token": "こと"
}
]
おわりに
SentencePieceをWebAPIにしてみました。翻訳で使うのもモチロンそうですし、sec2secに使えるってことなので標準語-方言変換とかもできそうです。私は極性判定に使ってみようと思っています。今はWord2Vec結果をRNN+LSTMしていますが、SentencePieceの単語欠片でone-hotベクトル作ってRNN+LSTMって何か良さげじゃないですか?