Android+SpeechRecognizer+Dialogflowで自由度の高い音声アシスタントを実現

# 最初に
本記事は[ex-mixi Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ex-mixi)の第一日目の記事です。

# 音声アシスタントの縛り
GoogleもAlexaもClovaもSiriも有名ですね。
私は全部使ったことないですが、どれも非常に優秀で完成されたものだと思います。
しかしそれぞれGoogleのサービスを使うのか、Amazonのサービスを使うのか、など縛りがあります。
例えば音楽を再生するときは、GoogleならGoogle Play Music, AlexaならAmazon Musicなど。
「俺はあらゆるサービスをアップルに統一してるんだ！」という場合は問題ないかもしれませんが、そうじゃない場合は、やはり要件によっては自由にプラットフォームを選びたいですよね。
そこで今回は各処理を分割して、自由度の高い音声アシスタントを実現してみます。

# 要件
いろいろ実現可能ですが、まずは下記を考えてみます。

「私を甲子園につれてって！」といえば地図アプリが開かれてナビゲーションがスタートする。

# プロセス
本件では、音声認識にAndroidのSpeechRecognizer、自然言語処理にDialogflow、それ以降の処理は要件によりいろいろ使っています。
実はSpeechRecognizerのみを使って、アプリ側である程度の辞書を定義してよしなに処理することで案外性能のいいボイスコマンド音声認識は可能なのですが、やはり自然言語の処理をするとなると難しくなります。
特に高度なボイスコマンドの処理をする場合、複数のキーワードを種類別にピンポイントで認識する必要が出てきます。
Dialogflowではそのあたりの言語処理をバッチリやってくれます。

## AndroidのSpeech Recognizer
AndroidのSpeechRecognizerを採用した理由として、音声認識制度が非常に高いのはもちろん、音声認識時点での結果をログとして持っておくことができることです。
SpeechRecognizerの使い方の説明は、結構前ですが[Android Speech Recognizerを使いこなす](https://qiita.com/KAKKA/items/d15cb9187fd1bcca8770)に書いたのでそちらを見てもらうとして、
ここでは、音声認識結果の一番もっともらしい結果一つをStringで保持していると仮定します。
そしてこのString自体をDialogflowに送信してやります。

## Dialogflow
このの説明が非常に大変なのですが、ざっくりと流れだけ追って説明します。
Dialogflowへのインプットは、アプリからの音声認識結果の文字列、アウトプットは、どの種類のコマンドでどういったパラメータを持っているかという情報です。

### Intentの作成
Agentをポチッと作ってやって、最初にやることはIntentの作成です。
Intentとは簡単にいうと、その文章はどの種類の欲求している文章なのかです。例えば本件の例でいうとナビゲーションの欲求と音楽再生の欲求があるわけですから、
Navigationという名前のIntentとMusic.Playという名前のIntentを作成してやります。

次にIntentの設定ですが、本件ではContextsとEventsは設定の必要がありません。
Training Phraseに下記のように教師データとなる文章を設定します。
下記例は英語ですが、日本語だと「公園に行きたい」、「公園に連れて行って」、「東京駅までナビゲーション」、「私を甲子園につれてって！」などの教師データを入力します。
<img width="735" alt="スクリーンショット 2018-12-01 0.17.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/36200/eb93d1ec-f228-0f6c-0713-099362382f79.png">
そしてTraining Phrasesの下のAction and Parametersです。
ここには抽出したいパラメータを設定します。上記の画像では「park」という文字列が黄色背景になっており、教師データとしての文章の中で目的地の文字列を抽出するように設定しています。
ENTITYの「@sys.any」は[公式ドキュメント](https://dialogflow.com/docs/reference/system-entities)に定義されていますが、Dialogflowではデフォルトでキーワード抽出する辞書のようなものを持っており、中でもanyはかなり抽象度の高いEntityです。
一番上のテキストボックスに定義されている「navigation」という文字列は下記レスポンスに出てくるactionです。
<img width="744" alt="スクリーンショット 2018-12-01 0.19.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/36200/734e4549-5d33-f898-eb14-53cdace8619d.png">

ドキュメントを見た方はわかるかと思いますが、@sys.locationという住所を抽出するentityも用意されています。
今回これを使わないのは、解析結果が下記のように構造化されており使いづらいという点と、住所でもない自由なキーワードも目的地として利用したいからです。スターバックスとか、コンビニとかそういうキーワードも立派な目的地なので、今回の要件としては@sys.locationをふさわしくありません。
なので抽象度の高い目的地としてキーワードを認識させた後に、その文字列をGoogle Places APIなのでスポット検索してやるほうが良いです。

```
{"country":"日本国",
"admin-area":"東京都",
"street-address":"六本木ヒルズ森タワー 六本木6-10-1",
"subadmin-area":"港区",
"zip-code":"106-6126"}
```

簡単なものでIntentの設定はこんなに簡単にできてしまいます。
Music.Playはもう少し複雑になりますし、ハマるポイントもありますが、方針としては同じです。
この件に関しては後述します。


### 認識結果
あとはAPI叩いてやるだけです。
上記のとおり、「私を甲子園につれてって！」という文字列を送信してやると、下記のようにnavigationのintentが検出され、「私の家」という目的地もパラメータにアサインされています。

```
{
  "queryText": "私を甲子園につれてって！",
  "action": "navigation",
  "parameters": {
    "DestinationName": "甲子園"
  },
  ...
}
```

その後、「甲子園」という文字列を例えばGoogle Places APIなどでスポット検索し、ナビゲーションアプリに投げてやるだけです。

# 何がうれしいのか？
この状態だと基本的すぎて存在価値がないように思えますが、これをどんどん応用させていくことでかなり汎用性の高い音声アシスタントをつくることが可能になります。

## ユーザーがナビゲーションアプリを指定してきた場合に対応
ご存知の通りナビゲーションアプリは優秀なものが複数あります。
Wazeでナビゲーションしたい場合などは、ユーザーは「Wazeで巣鴨獅子王までナビゲーション」みたいな感じに言うでしょう。
ちなみに実際にユーザーがなんと言ってきたかはDialogflow上のコンソールの「Training」または「History」メニューで見ることができます。そういった発言が見つかった場合は教師データとして登録し、Agentを学習させてやります。
ナビゲーションアプリ名のように、ある程度数が少ないものに関してはEntitiesに定義してやり、そのEntityをAction and parameterに指定してやると良いです。
こういった教師データをふよしていく作業をアノテーションといい、機械学習において骨が折れる作業の一つであります。
ABEJA社ではアノテーションを行う専門の人材、アノテーターがおり、メルカリ社でも専門の人材を募集しておられるようです。特に本件の場合、多言語対応するときに自分では作業が不可能になりますので、アノテーターが必須になります。

話がそれましたが、上記のように新たに教師データを付与してやると、「Wazeで巣鴨獅子王までナビゲーション」の解析結果として、例えばAppNameというパラメータにWazeという文字列が入るようになります。
じゃああとはアプリ側でナビゲーションアプリの指定してナビゲーションスタートすれば完了です。

## 音楽再生も汎用性が高くなる
音楽再生は特に自由度が高くなります。
アプリ名、曲名、アーティスト名、ジャンル名、そして音楽アプリ名などを検出できるようにし、アプリ側でどの音楽アプリで何を再生するのかを決めると良いわけです。
この音楽アプリでは見つからない、なんてことになった場合はYouTubeで再生することもできます。

## 音声認識結果の揺れを吸収
チャットボットとは違い、本件は音声認識も利用します。しかし音声認識の結果は100%ユーザーが言おうとしたものと同じではありません。よってどうしてもその認識精度によって結果に揺れが出てきます。
わかりやすい例でいうと英語で"Launch Calendar"というと、音声認識結果は、特に日本人が発音するときには"Lounge Calendar"や"Lunch Calendar"となってしまうことがあります。
この音声認識結果の揺れ自体をDialogflowで学習してしまえば、頑健な音声ボイスコマンド認識ができるようになります。

## 潜在的な音声コマンドや言い回しパターンが増やせる
予め、ナビゲーションをスタートするにはこう言って下さい、といったチュートリアルやガイドは準備したとしても、ユーザーは変わった言い回しのコマンドを言ってきます。日々アノテーションを行っていると、実際にどんなコマンド、言い回しがニーズがあるのかが見えてきます。これを教師データとして学習させてやることで、さらにAgentはの認識精度が高くなります。

## Hot word検出もオリジナルのものを
ボイスコマンド音声認識をスタートさせるためのホットワード認識というものを自分で作ることが可能になります。
AndroidのSpeechRecognizerは連続音声認識は対応していないので、ホットワード検出には別途実装が必要になります。
一例として[こちらの記事](https://qiita.com/KAKKA/items/1cbaa4adaf06b6439f2c)で紹介しております。
OK Googleとかhey Alexaとかhey Siriとかいう必要もありません。
「あんたほんまなにいうてんの！」などのオリジナルのホットワードでアシスタントを呼びましょう。

# おわりに
全て詳細に書くととても書ききれない量になってしまいますので、概要だけで自重しておきます。
また[DroidKaigi 2019](https://droidkaigi.jp/2019/accepted)において、本件を深掘りして詳しくじっくり話す予定であります。スライドは[Speaker Deck](https://speakerdeck.com/kakka)に掲載予定です。
それでは皆さん良いお年を。