Elixirで弱々しいAI#1「MeCabで文章パース」

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

AIには、[「強いAI」と「弱いAI」](https://ja.wikipedia.org/wiki/%E5%BC%B7%E3%81%84AI%E3%81%A8%E5%BC%B1%E3%81%84AI)という分類がありますが、Elixirと、MeCabやWikipedia API等を使って、「弱いAI」の中でも、更に貧弱ゥな「弱々しいAI」（笑）を何回かに分けて作ってみます

ザックリとした仕様としては、こんな感じですかね :speech_balloon: 

- 対話して、言われた文脈を何となく解釈して、それとない返事を返す
- 聞いた言葉から受けた印象から、感情のようなものが揺らぎ、返事が変化する
- 足りない知識は、Wikipediaに取得しに行き、当たり障り無い感じで引用する

なんとも頭の悪いAIになりそうな気配しかしませんが、アレコレしているうちに魂みたいなものが宿るか、[哲学的ゾンビ](https://ja.wikipedia.org/wiki/%E5%93%B2%E5%AD%A6%E7%9A%84%E3%82%BE%E3%83%B3%E3%83%93)ができあがるかも知れない

ひとまず今回は、MeCabで日本語の文章をパースして、ワンパターンな意味解釈をするところまで作ります :white_check_mark: 

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

# ElixirとMeCabのインストール

まず、Elixirをインストールします
下記からダウンロードしてインストールするか、yumやapt、Homebrew等でインストールしてください

　Elixirダウンロード： http://elixir-lang.org/install.html

Dockerを使うのであれば、Dockerをあらかじめインストールした上で、Elixirイメージを持ってきます

　Dockerダウンロード： https://www.docker.com/get-docker
　　※Windows版やMac版は、Stableが調子悪い（docker pullが失敗する）ため、そのときはEdge版で治ることが多い

```
> docker pull trenpixster/elixir
> docker run -it trenpixster/elixir /bin/bash
# uname -a
Linux 8abfc1bd7754 4.9.12-moby #1 SMP Tue Feb 28 12:11:36 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
```

次に、MeCabを下記からダウンロードしてインストールするか、yum等でインストールしてください

　MeCabダウンロード： http://taku910.github.io/mecab/#download

ちなみに、Windows版MeCabは、「C:\Program Files (x86)\MeCab\」以外の場所にインストールすると、次回以降で大変な思いをすることになるので、インストール先は変えないほうが無難です

あと、辞書の文字コードは、「UTF-8」を選択してください

# Elixirプロジェクト作成、MeCabモジュール導入／修正

Elixirプロジェクトを作成します

```
mix new mini_ai
cd mini_ai
```

MeCabをElixirで利用する準備として、mix.exsにMeCabを追加します

```elixir:mix.exs
defmodule MiniAi.Mixfile do
    use Mix.Project
	…
	defp deps do
		[
			{ :mecab, "~> 1.0" }, 
		]
	end
	…
```

MeCabモジュールを取得します（要ネット接続）

```
mix deps.get
```

WindowsネイティブのElixirを利用する場合、そのままのコンソールでは、Elixirが扱うUTF-8を表示できないため、事前に切り替えておきます

```
chcp 65001
```

また、Windowsでは動かないMeCabモジュール内のUNIX依存コードがあるため、以下パスのファイルを修正する必要があります

```elixir: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:          """
```

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

```elixir:deps/mecab/lib/mecab.ex　※修正後
 98:        x when x == nil or x == false ->
 99:          "echo #{str} | mecab #{mecab_option}"
```

なお、deps.getで取得したモジュールは、上記のようなコード書き換えを行っても自動的にはリビルドされないため、もし_build/dev/lib/mecabフォルダが作られている場合は削除しておきます（後述の手順でリビルドが走ります）

# MeCabでのパースを実装

lib/配下にmini_ai.exというファイルができているので、以下の内容に書き換えます

```elixir:lib/mini_ai.ex
defmodule MiniAi do
	def listen( message \\ "あなたは弱々しいAIですか？" ) do
		Mecab.parse( message )
	end
end
```

コンソールからの日本語入力に不自由しそうなので、今回はコード内に日本語文章を入れることにしました
（どこかの回で自由入力可能な状態にもっていきます）

これで準備は整いましたので、iex（Elixirのインタラクティブシェル）を起動し、ビルドします

```
iex -S mix
Eshell V8.0  (abort with ^G)
==> mecab
Compiling 1 file (.ex)
Generated mecab app
==> mini_ai
Compiling 1 file (.ex)
Generated mini_ai app
Interactive Elixir (1.4.2) - press Ctrl+C to exit (type h() ENTER for help)
iex>
```

iexから実装したコードを呼び出すと、MeCabでパースされ、文章が「単語毎のMapのリスト」として分解されます

```elixir
iex> MiniAi.listen()
[%{"conjugation" => "", "conjugation_form" => "", "lexical_form" => "あなた",
   "part_of_speech" => "名詞", "part_of_speech_subcategory1" => "代名詞",
   "part_of_speech_subcategory2" => "一般",
   "part_of_speech_subcategory3" => "", "pronunciation" => "アナタ\r",
   "surface_form" => "あなた", "yomi" => "アナタ"},
 %{"conjugation" => "", "conjugation_form" => "", "lexical_form" => "は",
   "part_of_speech" => "助詞", "part_of_speech_subcategory1" => "係助詞",
   "part_of_speech_subcategory2" => "", "part_of_speech_subcategory3" => "",
   "pronunciation" => "ワ\r", "surface_form" => "は", "yomi" => "ハ"},
 %{"conjugation" => "", "conjugation_form" => "", "lexical_form" => "AI",
   "part_of_speech" => "名詞",
   "part_of_speech_subcategory1" => "固有名詞",
   "part_of_speech_subcategory2" => "一般",
   "part_of_speech_subcategory3" => "", "pronunciation" => "アイ\r",
   "surface_form" => "AI", "yomi" => "アイ"},
 %{"conjugation" => "基本形", "conjugation_form" => "特殊・デス",
   "lexical_form" => "です", "part_of_speech" => "助動詞",
   "part_of_speech_subcategory1" => "", "part_of_speech_subcategory2" => "",
   "part_of_speech_subcategory3" => "", "pronunciation" => "デス\r",
   "surface_form" => "です", "yomi" => "デス"},
 %{"conjugation" => "", "conjugation_form" => "", "lexical_form" => "か",
   "part_of_speech" => "助詞",
   "part_of_speech_subcategory1" => "副助詞／並立助詞／終助詞",
   "part_of_speech_subcategory2" => "", "part_of_speech_subcategory3" => "",
   "pronunciation" => "カ\r", "surface_form" => "か", "yomi" => "カ"},
 %{"conjugation" => "", "conjugation_form" => "", "lexical_form" => "？",
   "part_of_speech" => "記号", "part_of_speech_subcategory1" => "一般",
   "part_of_speech_subcategory2" => "", "part_of_speech_subcategory3" => "",
   "pronunciation" => "？\r", "surface_form" => "？", "yomi" => "？"},
 %{"conjugation" => "", "conjugation_form" => "", "lexical_form" => "",
   "part_of_speech" => "", "part_of_speech_subcategory1" => "",
   "part_of_speech_subcategory2" => "", "part_of_speech_subcategory3
```

ここから、各単語を拾うには、以下のようなコードで、「単語毎のMapのリスト」のうち、単語だけを抽出し続け、それをリストに追加していきます

```elixir:lib/mini_ai.ex
defmodule MiniAi do
	def listen( message \\ "あなたはAIですか？" ) do
		Mecab.parse( message )
		|> get_words( [] )
	end
	def get_words( [ %{ "surface_form" => word } | tail ], words ), do: get_words( tail, words ++ [ word ] )
	def get_words( [], words ), do: words
end
```

コード修正したら、iexの中でrecompile()とすると、リビルドされます

```elixir
iex> recompile()
iex> MiniAi.listen()
["あなた", "は", "AI", "です", "か", "？", ""]
```

# 意味解釈（ごくカンタンなもの）

キチンとした意味解釈は、次回行うとして、今回は、「AI」という単語が出現したらAIのウンチクを語り、「AI」が無ければそっけない返答をする．．．そんなウザいAIに仕上げて、今回は完了としましょう :disappointed: 

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

リビルドして、実行すると、ウンチクを語ります

```elixir
iex> recompile()
iex> MiniAi.listen
あなた「あなたはAIですか？」
貧弱AI「はい、そうです。「弱いAI」は思考できませんが、「強いAI」は人のように思考できるんですよ。」
:ok
```

文章から「AI」が無くなると、まるで興味無い返答になります :cold_sweat: 

```elixir
iex> recompile()
iex> MiniAi.listen
あなた「あなたはマシーンですか？」
貧弱AI「ふーん( ´,_ゝ｀)」
:ok
```

[次回は、もう少し、頭良い感じに](http://qiita.com/piacere/items/ea4ab52191a049898df1)しましょう :hearts:

---
# p.s.

:stars::stars: [6/8（木）開催の《fukuoka.ex #1》発足MeetUp、満員御礼です。ご来訪を楽しみにお待ちしております](https://techjin.connpass.com/event/56497/) 
（※立ち見席とか検討するので、ダメ元でよろしければ、引き続きお申込みどうぞ）
[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/fa13ccf5-9cc6-5c0f-fde3-64f12380bb5c.png)](https://techjin.connpass.com/event/56497/)

---