SentencePieceを形態素解析のように使えるAPIを公開した

2017/4/13追記：taku910さん本人から助言をいただきました。詳細はコメント欄を見てください。本記事は追って修正します。
2017/4/18追記：公開しているAPIの方は修正しました。Unigramの方はまだモデル構築中です（5日間回しても終わらず）。
2017/4/26追記：Unigramの方もできました（12日間回してやっと終わった）。本件の詳細は[別の記事](http://qiita.com/keigohtr/items/fcbc3b34b05d220bd5e7)で投稿しました。

## はじめに
※無理やりWebAPIにしたので、間違ってたら指摘してください。
Googleが[SentencePiece](https://github.com/google/sentencepiece)を公開しました。NMT (Neural Machine Translation／ニューラル機械翻訳) で有効性が確認されているアプローチです。今回はそれを形態素解析のように使える[WebAPI](https://apitore.com/store/apis/details?id=37)にしてみました。無料で使えるので使ってみてください。

<img src="http://blog.apitore.com/wp-content/uploads/2017/04/amarec-20170412-235607.png" alt="" width="760" height="344" class="aligncenter size-full wp-image-975" />

## API
- [SentencePiece API](https://apitore.com/store/apis/details?id=37)

## サンプルコード
- [Javaサンプル](https://github.com/keigohtr/apitore-sdk-java/tree/master/sentencepiece-response)

## 関連記事
- [本家Github](https://github.com/google/sentencepiece)
- [工藤さんによる解説](http://qiita.com/taku910/items/7e52f1e58d0ea6e7859c)
- [本投稿の元記事](http://blog.apitore.com/2017/04/13/api-sentencepiece/)

## 実装内容
簡単に解説すると、

1. 日本語Wikipediaの記事にSentencePieceをかける
1. SentencePieceの出力であるvocabファイルをmecab-ipadicの辞書形式に整形する
1. [kuromoji](https://github.com/atilika/kuromoji)で辞書ごとコンパイルする

もう少し詳細に説明します。

### 1. 日本語Wikipediaの記事にSentencePieceをかける

手元にあった日本語Wikipediaのデータが20160915のdumpで少し古いですが、こいつを元データにしました。いくつか手を加えて整形しています。やったことを羅列すると

1. dumpは[wp2txt](https://github.com/yohasebe/wp2txt)でテキスト化して、全部を一つのファイルにガッシャンコ
1. 40文字以下の行は削除
1. 行先頭が「Image」「File」「イメージ」「ファイル」は削除
1. ファイルの行をランダムに入れ替える
1. 先頭から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](https://apitore.com/)は完全なJavaで実装していますが、SentencePieceはC++で書かれています。「ラッパー書くのは面倒だし、C++でWebAPIとかよくわからんし・・・ということで、ここは無理やり実現するしかない！SentencePieceも形態素解析みたいなもんでしょ」ってことで、普段からお世話になっているJavaの形態素解析器[kuromoji](https://github.com/atilika/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](https://github.com/atilika/kuromoji)で辞書ごとコンパイルする

あとはコンパイルするだけです。kuromojiを通常通りにコンパイルします。kuromojiに内包されるテストコードは絶対に通らないので、テストは削除してしまいましょう。


## 実際に使ってみる
APIは[こちら](https://apitore.com/store/apis/details?id=37)で公開しています。APIコールまでの準備（API登録、アクセストークン発行、サンプル実行）は[こちら](https://apitore.com/about/usage/user)を参考にしてください。

APIの入出力などの仕様は[こちら](https://api.apitore.com/swagger?id=37)で公開しています。一応ここにも書くと、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](https://apitore.com/store/apis/details?id=37)にしてみました。翻訳で使うのもモチロンそうですし、sec2secに使えるってことなので標準語-方言変換とかもできそうです。私は極性判定に使ってみようと思っています。今はWord2Vec結果をRNN+LSTMしていますが、SentencePieceの単語欠片でone-hotベクトル作ってRNN+LSTMって何か良さげじゃないですか？