密かに日本語化されたWatson Natural Language Classifier (NLC)でテキスト分類

本記事は、[SoftLayer Bluemix Summit 2015](http://softlayer-bluemix-summit.jp/)のセッション「BluemixでWatsonをつかいたおせ！」の[スライド](http://www.slideshare.net/MikiYutani/softlayer-bluemix-summit-2015-bluemixwatson)後半でお話したWatson Natural Language Classifier (NLC)についての詳細を説明するものです。一点だけSummitの時と異なる点として、何もアナウンスはないのですが、この1ヶ月の間にNLCは日本語対応されたようですので、あらたに日本語でのサンプルを作ってみました。

# Watson Natural Language Classifier (NLC)とは

2015年8月6日、Watsonの新しいAPI、Natural Language Classifier (NLC)が正式リリースされました(英語ではGeneral Availability (GA))。このAPIはユーザが与えたデータによって訓練された分類器(classifier)を用いてテキストを分類する、というものです。クイズ番組_Jeopardy!_で実現されていた質問応答システム(以下、_Jeopardy!_型)はルールベースシステムによって質問テキストの分類を行ってきました(注1)が、NLCは、あらためて深層学習(deep learning)技術によって書き直し(注2)、それをAPIとして切り出し公開したものです。NLCは今後Watsonアプリケーションを開発するうえで重要な役割を果たすAPIであると考えられます。

注1: A. Lally, et al. (2012). *Question analysis: How Watson reads a clue.* IBM Journal of Research and Development, 56(3), 2:1-14. http://ieeexplore.ieee.org/xpl/articleDetails.jsp?arnumber=6177727
注2: Rob Yates (2015). *Introducing the IBM Watson Natural Language Classifier.* IBM Watson Developers Community Blog. https://developer.ibm.com/watson/blog/2015/07/10/the-ibm-watson-natural-language-classifier/

# _Jeopardy!_型Watsonに対するNLCの長所

Watson質問応答システム(以下、Watson QA)の特徴はさまざまな観点から質問を分析し、大量のリソースを使用して数千数万の回答候補を検証、その中からもっとも確信度(confidence)が高い回答候補を選択する、ということにあります。そのひとつの機能として、ユーザの質問がどのようなカテゴリに属する質問であるかについて分析し、確信度を出す、というプロセス(質問分析, question analysis)があります。

たとえば、*When was Mozart born?*という質問について考えてみましょう。Watson QAでは、このテキストを解析するにあたり、Lexical Answer Type (LAT)というカテゴリを選定します。_Jeopardy!_の使用したデータセットでは、この質問に対するLATは`DATE_OF_BIRTH`という値が高い確信度をもつように多数のルールを作成することでチューニングされていました。つまり、_Jeopardy!_型Watson QAの本質は、対象ドメインに合致したLATを恣意的に選択し、適切なLATを選択するためのルールセットをWikipedia、DBPedia、WordNetなどから機械学習によって大量に作成、それを数年かけて細かくチューニングし、並列処理によって反応速度を確保した、というものでした。

しかし、LATはIBM Watsonチームが長い年月をかけて_Jeopardy!_用にチューニングしたものであって、汎用性はありません。たとえば、私たちが新しいドメイン(問題領域)での質問応答システムを構築したいと思うと、新たにこのチューニングを行う必要があります。前述の文献によればこの部分は多くのPrologプログラムで構成されており、一般の利用者がチューニングを行うことは至難であると思われます。

一方、NLCでは、質問分析を深層学習で行うことができます。詳細は公開されていませんが、すでにwikipediaなどの知識に基づいて構成された知識モデルに加えて、私達のドメイン知識を追加することで、自然な分類器を作ることができるようになっているようです。

# NLCを使用する準備

## Bluemix利用環境の準備

まず、以下の環境を準備してください。

* IBM ID (Bluemixアカウント)
* [Cloud Foundry CLI (cfコマンド)](https://github.com/cloudfoundry/cli/releases)

## NLCサービスの概要

Bluemixダッシュボードでは、以下のアイコンがNLCを表しています。

![watson-nlc-icon.png](https://qiita-image-store.s3.amazonaws.com/0/21963/33ff361b-cb5b-1821-a3dc-d3b41dac5cfa.png)

ダッシュボードでアイコンをクリックすると、NLCで選択できるプランの詳細を確認することができます。NLCは正式版(GA)ですので、有償サービスとなっています。ただし、無料枠も設定されており、2015年9月末現在の費用は以下の通りです。

* 無料枠：1インスタンスまで、かつ、API呼び出し1,000件/月まで、かつ、トレーニングイベント4件/月まで
* 無料枠を超えた場合の価格：インスタンス1個あたり2,100円、API呼び出し1件あたり0.3675円、トレーニングイベント1件あたり315円

NLCは以下のページにて詳細な説明やデモを見ることができます。

* [ドキュメント](http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/nl-classifier/)
* [API仕様](http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/apis/#!/natural-language-classifier)
* [デモ](http://natural-language-classifier-demo.mybluemix.net/)

## NLCサービスの作成

それでは、NLCサービスを作成してみましょう。今回はREST APIの使い方を理解するためにcurlを使って実行しますので、アプリケーションは作りません。(ダッシュボードからサービスを作成する場合はAppsに「Leave unbound」を指定します)

CloudFoundry CLI (cfコマンド)を使って以下の手順でサービスを作成します。

```console
# cfの接続先としてBluemixを指定
$ cf api https://api.ng.bluemix.net
(ログ省略)

# cfにログイン
$ cf login -u <ユーザ名>
(ログ省略)

# NLCサービスを確認
# サービスの名前とプランを確認します
$ cf marketplace | grep classifier
natural_language_classifier    standard*                                                                                                                                                                      Natural Language Classifier performs natural language classification on question texts. A user would be able to train their data and the predict the appropriate class for a input question. 

# サービスを作成(サービス名: nlc)
$ cf create-service natural_language_classifier standard nlc
(ログ省略)

# 資格情報(サービスキー)を作成(資格情報名: nlc-credentials)
$ cf create-service-key nlc nlc-credentials
(ログ省略)

# 資格情報の内容を確認(username, password, urlを含むjsonが返ってきます)
$ cf service-key nlc nlc-credentials
Getting key re-credentials for service instance nlc as <mail-address>...

{
 "password": "<password>",
 "url": "<url>",
 "username": "<username>"
}
```

## APIの概要

NLCは以下のメソッドからなるAPIセットです。

|名称|メソッド|意味|
|:--|:--|:--|:--|
|/v1/classifiers|GET|分類器の一覧を表示|
|/v1/classifiers|POST|分類器を作成してデータを投入しトレーニング開始|
|/v1/classifiers/{classifier_id}|GET|分類器のトレーニング状況を確認|
|/v1/classifiers/{classifier_id}/classify|POST|分類器を使用してテキストを分類|
|/v1/classifiers/{classifier_id}/classify|GET|(同上)
|/v1/classifiers/{classifier_id}|DELETE|分類器を削除

NLCでは、まず分類器を生成し、独自CSVデータをアップロードして分類器のトレーニングを行うことからはじまります。分類器のトレーニングが終了すると、はじめて分類器を使用することができます。

このプロセスを図示すると、次のようになります。

![watson-nlc-training-process.png](https://qiita-image-store.s3.amazonaws.com/0/21963/4f62b93f-38dc-f392-0392-6f0aad448f8c.png)

以下では、この流れに沿ってAPIの使い方を説明していきます。

# トレーニングデータの作成

NLCのトレーニングデータのフォーマットはCSVです。

## トレーニングデータのフォーマット

フォーマットの仕様詳細を次に記載します。なお、詳細はまだ適宜変更が入っているようですので、最新の[ドキュメント](https://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/doc/nl-classifier/data_format.shtml)を常に参照するようにしてください。(注3)

* CSVは2カラムから構成され、最初のカラムがトレーニングテキスト、次のカラムが分類クラスを表す
* 文字コードはUTF-8
* 分類クラスはスペース、タブ、改行、コロンを含んではならない
* タブは`\t`、改行は`\r`または`\n`または`\r\n`でエスケープする
* カンマ、ダブルクォート文字を含むときはダブルクォート文字で囲み、さらにダブルクォート文字自体はダブルクォート文字を2個重ねる(ex. `"Example text with ""quotation"""`)
* レコード数は5以上10,000未満
* 1個のトレーニングテキストの長さは1,024文字以下

注3: 2015年8月末の時点では、トレーニングテキストには[A-Za-z0-9_-]以外の文字を含んでは
ならない、という趣旨の制約もありましたが、現在はなくなっています。

## 今回使用したトレーニングデータのプロフィール

今回は、オープンデータの先進的な取り組みを行っている[会津若松市](http://www.city.aizuwakamatsu.fukushima.jp/docs/2009122400048/)が公開しているデータセットを使用します。

* [公共施設マップ](http://www.data4citizen.jp/app/users/openDataTop/show/aizuwakamatsu_map) データ提供元：会津若松市 (注4)
* [会津のまちの駅・道の駅リスト](http://www.data4citizen.jp/app/users/openDataTop/show/o_aizu_station) データ提供元：会津若松市／NPO法人 会津地域連携センター (注4)
* [会津若松市行事予定表](http://www.data4citizen.jp/app/users/openDataTop/show/o_aizu_event) データ提供元：会津若松市 (注4)
* [会津若松市内の観光史跡情報](http://www.data4citizen.jp/app/users/openDataTop/show/O_HISTRIC_SITE) データ提供元：会津若松市／[会津若松観光ナビ](http://www.aizukanko.com/) (注5)

注4: ライセンス: [CC-BY](http://creativecommons.org/licenses/by/2.1/jp/) ![cc-by.png](https://qiita-image-store.s3.amazonaws.com/0/21963/37c6c5d0-a807-ca98-a12b-b9117e62c2e0.png)
注5: ライセンス: [CC-BY-ND](http://creativecommons.org/licenses/by-nd/2.1/jp/) ![cc-by-nd.png](https://qiita-image-store.s3.amazonaws.com/0/21963/0453633e-a3e3-c027-653e-f0f88af10e8e.png)

## トレーニングデータの準備

前述各ページの下部にある「リクエストURL」(/apps/users/...のリンク)を押して全件取得し、順に、以下の名前で保存します。

* 公共施設マップ: aizu-map.json
* 会津のまちの駅・道の駅リスト: o_aizu_station.json
* 会津若松市行事予定表: o_aizu_event.json
* 会津若松市内の観光史跡情報: o_histric_site.json

これらに対して、jqとsedを使って、以下のように、それぞれの施設、まちの駅・道の駅、行事、観光史跡などの名前と説明を抜き出してCSVを作成します。途中でCSVフォーマットに合わせて、空白とタブ文字は削除、カンマとコロンは所謂全角文字に置換しています。

```console
$ cat aizu-map.json | jq -r '.data[] | .name, .memo, .category' | sed 'N;s/\n/;/g;N;s/ //g;s/\t//g;s/,/，/g;s/:/：/g;s/\n/,/;s/;,/,/g' > nlc_map.csv
$ cat o_aizu_station.json | jq -r '.data[] | .name, .faculty, .description, .access, .type' | sed 'N;N;N;s/\n/;/g;N;s/ //g;s/\t//g;s/,/，/g;s/:/：/g;s/\n/,/' > nlc_station.csv
$ cat o_aizu_event.json | jq -r '.data[] | .place, .contact, .title, .description' | sed 'N;N;N;s/\n/;/g;s/ //g;s/\t//g;s/,/，/g;s/:/：/g;s/$/,行事予定/' > nlc_event.csv
$ cat o_histric_site.json | jq -r '.data[] | .name, .name_kana, .catchphrase, .description' | sed 'N;N;N;s/\n/;/g;s/ //g;s/\t//g;s/,/，/g;s/:/：/g;s/$/,観光史跡情報/' > nlc_histric_site.csv

# 全データの結合
$ cat nlc_*.csv > training.csv
```

こうして作成されたtraining.csvは以下のようなファイルになります。

```console
# 先頭5行を表示
$ head -5 training.csv 
鶴ヶ城公園;会津若松市観光課　電話0242-39-1251;鶴ヶ城紅葉ライトアップ;紅葉時期にあわせ、鶴ヶ城公園内をライトアップします。#nr#秋の夜長、光に照らされた幻想的な美しさのなか鶴ヶ城公園を散歩してみてはいかがですか。#nr##nr#開催期間10月中旬〜11月中旬#nr#,行事予定
ブライダルルネッサンス中の島　２階　インペリアルホール#nr#（会津若松市上町２番38号）;新年市民交歓会事務局（総務部総務課総務管財グループ内）#nr#0242-39-1211;新年市民交歓会;　新たな年、新年を迎えるにあたり、参加者の皆様に懇親を深めていただく場として、毎年、同時期に開催している交歓会です。#nr#　事前の申し込みが必要です。#nr#　交歓会当日は会場駐車場の混雑が予想されます。御来場の際は公共交通機関等を利用ください。,行事予定
観閲式・分列行進＝北出丸大通り〜會津風雅堂#nr#　　　　　　式典＝會津風雅堂#nr#行進コース：http：//goo.gl/maps/UpYbk;防災安全課消防防災グループ#nr#電話：0242-39-1227#nr#メール：bosaianzen@tw.city.aizuwakamatsu.fukushima.jp;平成２５年会津若松市消防出初式;市民の皆様の生命と財産を守るために、日夜活躍している市消防団が、新年を迎えるにあたり、統制のとれた行進を披露いたします。,行事予定
城南コミュニティセンター;高齢福祉課　39-1290;いきいきわくわく介護予防教室;いつまでも「いきいき」と元気で、「わくわく」した気持ちを持ち続けることができるように、元気な時から介護予防に取り組みましょう#nr##nr#■日時：平成24年12月4日（火）?平成25年1月29日（火）の毎週火曜日（1月1日は休み）全部で8回#nr#■対象者：おおむね65歳以上で15分程度の運動ができ、要支援及び要介護認定を受けていない方#nr#■内容：体力測定、簡単ストレッチ、バランス体操、脳トレ、食生活や口腔ケアの話等#nr#■定員：25名#nr##nr#※申込み受付は終了しました。,行事予定
広田駅前通り;あいづ商工会　河東#nr#TEL0242-75-3511#nr#Fax0242-75-3779;会津かわひがし八日市;ＪＲ広田駅前通りで開催される、河東地区恒例の初市です。#nr#縁起物・福袋の販売や、地元の商店、商工会等も出店します。#nr#お越しの際は公共交通機関（バス・ＪＲ）をご利用になられますととても便利です。#nr#お車でお越しの場合は、誘導員の指示に従ってください。#nr#路上駐車はご遠慮ください。,行事予定

# 分類クラスを表示
$ cat training.csv | awk -F, '{print$2}' | sort | uniq
まちの駅
医療・保健施設
環境・ごみ
観光史跡情報
観光施設
行事予定
市庁舎
市民センター・公民館
消防署・分署
図書館・文化
雪捨て場
駐車場
道の駅
農業

# データ件数を表示
$ wc training.csv 
  186   357 76122 training.csv
```

# トレーニングの実行

## 分類器の作成とトレーニングの開始

新規に分類器を作成するには、/v1/classifiers (POST)を以下のパラメータで呼び出します。

|名称|種別|意味|
|:--|:--|:--|:--|
|training_data|ファイル|トレーニングデータ(CSV)|
|training_metadata|ファイル|メタデータ ex. `{"language":"ja", "name":"classifier_name"}`|

```
$ curl -s -u "<username>:<password>" -X POST -F training_data=@training.csv -F training_metadata="{\"language\":\"ja\",\"name\":\"aizu\"}" <url>>/v1/classifiers | jq .
{
  "status_description": "The classifier instance is in its training phase, not yet ready to accept classify requests",
  "status": "Training",
  "url": "<url>/v1/classifiers/FAA074-nlc-272",
  "created": "2015-09-24T12:49:01.991Z",
  "language": "ja",
  "name": "aizu",
  "classifier_id": "FAA074-nlc-272"
}
```

とくにエラーが表示されなければ、CSVは正しく認識され、トレーニングが開始されます。レスポンスのjsonの`classifier_id`を見ると、この分類器のIDは`FAA074-nlc-272`であることがわかりますので、以下のAPI呼び出しではこのIDを使います。

## トレーニング終了まで待機

無事にトレーニングが開始された場合、トレーニングが終了するまで待機します。
/v1/classifiers/{classifier_id} (GET)を呼び出すことで、トレーニングの進捗状況を確認することができます。

|名称|種別|意味|
|:--|:--|:--|:--|
|classifier_id|パス|分類器ID|

```console
$ curl -s -u "<username>:<password>" <url>/v1/classifiers/FAA074-nlc-272 | jq .
{
  "status_description": "The classifier instance is in its training phase, not yet ready to accept classify requests",
  "status": "Training",
  "url": "<url>/v1/classifiers/FAA074-nlc-272",
  "created": "2015-09-24T12:49:01.991Z",
  "language": "ja",
  "name": "aizu",
  "classifier_id": "FAA074-nlc-272"
}
```

レスポンスの`status`が`Training`から`Available`になるとトレーニング終了です。今回のトレーニングデータは200件弱ですが、何回かためしたところいずれも15分程度でトレーニングが終了しました。

# 分類

作成した分類器を使用してテキストを分類するには、/v1/classifiers/{classifier_id}/classify (POSTまたはGET)に対象テキストを送ります。以下は、POSTの場合のパラメータです。GETの場合は対象テキストを`--data-urlencode`でエンコードしましょう。

|名称|種別|意味|
|:--|:--|:--|:--|
|classifier_id|パス|分類器ID|
|(body)|(body)|対象テキスト(json) ex. `{"text": "対象テキスト"}`
|Content-Type|ヘッダ|`application/json`

それでは実際に分類を行ってみましょう。
まずは観光史跡に関連するテキストです。

```console
# 分類例[1]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"鶴ケ城はどこですか?\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.8929378028614784,
  "class_name": "観光史跡情報"
}

# 分類例[2]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"白虎隊はどうなった?\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.9196483943838244,
  "class_name": "観光史跡情報"
}

# 分類例[3]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"白虎隊の行進を見に行きたい\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.7007787536657019,
  "class_name": "観光史跡情報"
}

# 分類例[4]
$  curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"山川健次郎とは誰ですか?\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.7512416854062283,
  "class_name": "観光史跡情報"
}
```

「鶴ヶ城」(分類例[1])や白虎隊(分類例[2])はトレーニングデータにも頻繁に含まれているキーワードです。高い確信度で観光史跡情報に分類されました。また、白虎隊が現存するかのような誤った質問(分類例[3])に対しては、確信度が低くなりましたが、観光史跡情報に分類されました。さらに、「山川健次郎」(分類例[4])は会津の出身で東京大学総長になった人ですが、トレーニングデータには含まれていません。しかし、高い確信度で観光史跡情報に分類されています。

次に、公共施設について質問してみます。

```console
# 分類例[5]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"市役所に行きたい\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.29687787899766943,
  "class_name": "市民センター・公民館"
}

# 分類例[6]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"会津若松市役所に行きたい\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.5184845632636725,
  "class_name": "消防署・分署"
}
```

あまりうまく分類されているとはいえません。
行事についてはどうでしょうか。

```console
# 分類例[7]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"商店会のイベントはありますか?\"}" <url>/v1/classifiers/D39290-nlc-1155/classify | jq .classes[0]
{
  "confidence": 0.3150945932414525,
  "class_name": "図書館・文化"
}

# 分類例[8]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"駅前商店会のイベントはありますか?\"}" <url>/v1/classifiers/D39290-nlc-1155/classify | jq .classes[0]
{
  "confidence": 0.5580596011560438,
  "class_name": "まちの駅"
}
```

少しの違いで大きく差がでています。一般名詞(市役所、商店会)のみからなる質問は確信度が低くなる一方、特定(会津若松、駅前)が入った場合は確信度は高くなるものの、肝心の分類が不自然になってしまっています。トレーニングデータに問題がありそうです。

最後に、会津若松とはまるで関係のない質問をしてみます。

```console
# 分類例[9]
$ curl -s -u "<username>:<password>" -X POST -H "Content-Type:application/json" -d "{\"text\":\"冥王星はどこにあるの?\"}" <url>/v1/classifiers/FAA074-nlc-272/classify | jq .classes[0]
{
  "confidence": 0.612980321765063,
  "class_name": "観光史跡情報"
}
```

NLCは必ずいずれかの分類クラスにテキストをあてはめますので、このような質問にも律儀に回答することに注意が必要です。

# 分類器の削除

使い終わった分類器は削除しましょう。

```
$ curl -s -u "<username>:<password>" -X DELETE <url>/v1/classifiers/FAA074-nlc-272 | jq .
{}
```

# まとめ

* NLCは日本語が通るようになりました。
* トレーニングデータ以外にもwikipediaのような一般的な知識を参照しているようにみえますが、そのような手がかりがない質問ではトレーニングデータを準備するにあたってのノウハウが必要となりそうです。