Apitore blog

Apitoreを運営していた元起業家のブログ

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

はじめに

GoogleがSentencePieceを公開しました。NMT (Neural Machine Translation/ニューラル機械翻訳) で有効性が確認されているアプローチです。今回はそれを形態素解析のように使えるWebAPIにしてみました。

API

サンプルコード

関連記事

実装内容

※無理やりWebAPIにしたので、間違ってたら指摘してください。 簡単に解説すると、

  1. 日本語Wikipediaの記事にSentencePieceをかける
  2. SentencePieceの出力であるvocabファイルをmecab-ipadicの辞書形式に整形する
  3. kuromojiで辞書ごとコンパイルする

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

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

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

  1. dumpはwp2txtでテキスト化して、全部を一つのファイルにガッシャンコ
  2. 40文字以下の行は削除
  3. 行先頭が「Image」「File」「イメージ」「ファイル」は削除
  4. ファイルの行をランダムに入れ替える
  5. 先頭から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って何か良さげじゃないですか?