Elixirで弱々しいAI#2「構文解釈してオウム返し（ついでにPhoenixでWebアプリ化）」

（この記事は [Elixir (その2)とPhoenix Advent Calendar 2016](http://qiita.com/advent-calendar/2016/elixir2_and_phoenix) 7日目の記事です）

[前回](http://qiita.com/piacere/items/8576ff1079aa28eec804)は、ごくごくカンタンな受け答えをする、AIとはとても呼べない何かを作りました :sweat: 

今回は、多少なりとも、言われたことを文章構成として解釈して、その内容に合わせた返事≒オウム返し、ができるようにしてみましょう

あと、言うことを変えるたびにイチイチrecompile()するのも面倒なので、PhoenixでWebアプリとして作っていきます

なお、本コラム中の「Elixirの書き方」については、あまり細かく説明をしていないので、「ここの書き方が分からない」とか「この処理が何をしているのかよく分からない」等あれば、コメントいただければ、回答します :headphones:

# Phoenixのインストール

ここはあまり大事なところでは無いので、やや説明を手抜きします :checkered_flag:

[Elixir入門「第3回：Phoenix 1.3で高速webアプリ ＆ REST APIアプリをサクッと書いてみる」](https://www.slideshare.net/piacere_ex/elixir3phoenix-13web-rest-api-81219622)のP6～10に、PhoenixのインストールとHTML編集について書いているので、こちらを実施して、Phoenixの使い方を押さえてください

なお、DockerでElixirを入れていない方は、P6先頭のDockerでのElixirイメージ起動は無視してください

# Phoenixプロジェクト作成、GETパラメータの受け渡しと表示

Phoenixプロジェクトを作成し、起動します

```
# mix phx.new web_mini_ai --no-brunch --no-ecto
# cd web_mini_ai
# iex -S mix phx.server
```

ブラウザで以下URLにアクセスすると、上記資料のP7と同じ画面が表示されます

```
http://localhost:4000
```

次に、GETパラメータを受け渡しできるようにするために、index()の第2引数の「_params」を「params」に変更し、更にrenderの最後に、GETパラメータ群を引数として渡すための「, params: params」を追記します

```elixir:lib/web_mini_ai_web/controllers/page_controller.ex
defmodule WebMiniAi.PageController do
  use WebMiniAi.Web, :controller

  def index(conn, params) do
    render conn, "index.html", params: params
  end
end
```

ページ側も書き換えます

```erb:lib/web_mini_ai_web/templates/page/index.html.eex
<p>あなた「<%= @params[ "message" ] %>」</p>
<p>貧弱AI「<%= MiniAi.listen( @params[ "message" ] ) %>」</p>

<form method="GET" action="/">
<input type="text" name="message" size="60" value="">
<input type="submit" value="話しかける">
</form>
```

ブラウザで以下画面が表示されます

![1.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4ccd55ff-83fd-9c1a-0433-e3895bf9d9f4.png)

テキストボックスに何かを入れ、「話しかける」ボタンを押すと、上部の「あなた」のところに入れたメッセージが表示されます

貧弱AIは、まだ喋れません :baby_tone1: 

# 貧弱AIを引っ越しさせる

前回作った、mini_aiプロジェクト配下のlib/mini_ai.exモジュールを、今回プロジェクト配下のlibの下にコピーし、以下のように書き換えます（Webページで表示するので、不要なIO.putsを削除したもの）

```elixir:lib/mini_ai.ex
defmodule MiniAi do
	def listen( message ) do
		case message do
			""  -> ""
			nil -> ""
			_   ->
				Mecab.parse( message )
				|> get_words( [] )
				|> reply
		end
	end
	def reply( words ) do
		case words |> Enum.any?( &( &1 == "AI" ) ) do
			true  -> "はい、そうです。「弱いAI」は思考できませんが、「強いAI」は人のように思考できるんですよ。"
			false -> "ふーん( ´,_ゝ｀)"
		end
	end
	def get_words( [ %{ "surface_form" => word } | tail ], words ), do: get_words( tail, words ++ [ word ] )
	def get_words( [], words ), do: words
end
```

ページ側も、貧弱AIが上記モジュールを使って喋るよう、書き換えます

```erb:lib/web_mini_ai_web/templates/page/index.html.eex
<p>あなた「<%= @params[ "message" ] %>」</p>
<p>貧弱AI「<%= MiniAi.listen( @params[ "message" ] ) %>」</p>
…
```

# 改めてMeCabモジュールのインストール

今回プロジェクトには、まだMeCabモジュールを入れていないため、mix.exsにMeCabを追加します（cowboyのエントリーの後にカンマを入れ忘れないように注意）

```elixir:mix.exs
defmodule WebMiniAi.Mixfile do
　use Mix.Project
  …
  defp deps do
    [{:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     { :mecab, "~> 1.0" }, 
    ]
  end
  …
```

iexを、Ctrl+Cを2回押して抜けた後、モジュールを取得します（要ネット接続）

```
iex>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
# mix deps.get
```

Windowsを使っているなら、MeCabモジュール内のUNIX依存コードを修正します

```:deps/mecab/lib/mecab.ex
 98:        x when x == nil or x == false ->
 99:          """
100:          cat <<EOS.907a600613b96a88c04a | mecab #{mecab_option}
101:          #{str}
102:          EOS.907a600613b96a88c04a
103:          """
```

これを以下のように書き換えます

```
 98:        x when x == nil or x == false ->
 99:          "echo #{str} | mecab #{mecab_option}"
```

6/15追記：_build配下のフォルダを削除しなくても、「mix deps.compile」を使えば、リビルドできるようです
 ↓
_build/dev/lib/mecabフォルダがあれば削除し、リビルドとPhoenixサーバ起動を行います（warningが出ますが、ひとまず動くので気にせずに）

```
# iex -S mix phx.server
Eshell V8.0  (abort with ^G)
Compiling 2 files (.ex)
warning: the underscored variable "params" is used after being set. A leading underscore indicates 
that the value of the variable should be ignored. If this is intended please rename the variable to 
remove the underscore
  web/controllers/page_controller.ex:5
```

話しかけてみましょう

![2.png](https://qiita-image-store.s3.amazonaws.com/0/155423/df836d1d-35b7-8af9-6f4b-ebfaa9f07831.png)

何もロジックは進化していないけど、雰囲気、出てきたんじゃない？ :sweat_smile: 

![3.png](https://qiita-image-store.s3.amazonaws.com/0/155423/6e411af5-4f45-35b2-3a77-7b9ebfa6e585.png)

# 「固有名詞」を捕まえてオウム返しさせる

今は「AI」という固定の単語にしか反応しないため、文中に「固有名詞」が出現した場合に、オウム返しさせるようなロジックに変更してみます

固有名詞は、MeCabの解析中、「part_of_speech_subcategory1」を見ると判定できるため、これを捕まえ、最後の固有名詞でオウム返しさせてみます

```elixir:lib/mini_ai.ex
defmodule MiniAi do
	def listen( message ) do
		case message do
			""  -> ""
			nil -> ""
			_   ->
				Mecab.parse( message )
				|> get_last_proper_noun( "" )
				|> reply
		end
	end
	def reply( words ) do
		case words != "" do
			true  -> "#{words}が好きなんだねー"
			false -> "ふーん( ´,_ゝ｀)"
		end
	end
	def get_words( [ %{ "surface_form" => word } | tail ], words ), do: get_words( tail, words ++ [ word ] )
	def get_words( [], words ), do: words

	def get_last_proper_noun( [ %{ "part_of_speech_subcategory1" => "固有名詞", "surface_form" => word } | tail ], last_word ), do: get_last_proper_noun( tail, word )
	def get_last_proper_noun( [ %{ "part_of_speech_subcategory1" => _ } | tail ], last_word ), do: get_last_proper_noun( tail, last_word )
	def get_last_proper_noun( [], last_word ), do: last_word
end
```

話しかけてみましょう

![4.png](https://qiita-image-store.s3.amazonaws.com/0/155423/2ac454ec-44a8-a8da-e08b-bb19b0db2235.png)

うぅ、なんか「ただの巨人好き」と勘違いされている．．．

という訳で、[次回は、「進撃の巨人」をキチンと固有名詞として理解する現在っ子に進化](http://qiita.com/piacere/items/a21d8901e4ae7b70aee8)させます

あと本当は、CaboChaとか使って、もうちょい頭の良い解析をしたいんだけど、どうもElixirのモジュールが世の中に無いようなので、次回、CaboCha Elixirモジュールも作ってみます :hearts: