2015年7月8日水曜日

擬似AI チャット ロボットを作成


◆ 目的
チャット形式で会話ができるロボットアプリを作成する。
ここで作成するのはいわゆる人工無能というやつである。文脈を理解しているわけではないのでAIではない。それらしく答えることができるように力技で対応する。
ディープラーニングの発展も目覚ましいが、言語処理に関しては人工無能の域を出ていない。


◆ 実現手段
大きく3つのステップを踏む。

1. ベースとなる例文の読み込み
テキストであればなんでもよい。
ライセンスフリーの小説から会話文をとるのも一案かもしれない。
この後の流れを見てもらえると分かるが、そのまま例文を喋らせるわけではない。


2. 形態要素解析
文章を分解して品詞を特定するために、オープンソースの形態素解析エンジンであるmecabライブラリを利用する。
rubyでコードを書きたいため、そのモジュールとしてnattoを使う。

# yum install mecab mecab-ipadic
# gem install natto


3. マルコフ連鎖
アルゴリズムとしてマルコフ連鎖を使い辞書を作り文脈を作成する。



◆ 会話例
今回はファーストガンダムのセリフを読み込ませてみた。
完成したツールで実際に会話した結果である。

※文節化して組み合わせた文章にはライセンスはない。

YOU: セイラさん いますか
AI : セイラさんは気休めがお上手で
YOU: ガンダムはどこだ
AI : ガンダム、次回大気圏突入
YOU: まじか
AI : ホワイトベースにあげて戦闘準備させるんだぞ
YOU: 頑張れアムロ
AI : しょっ少佐
YOU: シャアか
AI : シャア少佐だって、戦場の戦いで勝って出世した
YOU: 俺だってー
AI : 俺にだってこれくらいはな



◆ コード
githubにアップしている。

短いコードであるが、解読しやすいよう数点補足する。

(形態要素解析)
形態要素解析はmorpheme.rb内で行っている。
先に説明したように、MeCabライブラリ(rubyではnatto)に食わせればよい。

下の例文を読ませた場合の結果を出力させる。

line="こんにちはお元気ですか?"

natto = Natto::MeCab.new
natto.parse(line) do |nt|
  morphemes.push(["#{nt.surface}\t#{nt.feature}"])
end


こんにちは  感動詞,*,*,*,*,*,こんにちは,コンニチハ,コンニチワ
お        接頭詞,名詞接続,*,*,*,*,お,オ,オ
元気      名詞,形容動詞語幹,*,*,*,*,元気,ゲンキ,ゲンキ
です      助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
か        助詞,副助詞/並立助詞/終助詞,*,*,*,*,か,カ,カ
?         記号,一般,*,*,*,*,?,?,?


(マルコフ連鎖を使った辞書)
markov.rbで実装しているマルコフ連鎖が一番のポイントである。

例えば次の2つの会話を読み込ませるたとする。

「あたしはおしゃべりが好きなプログラムの少年です」
「あたしが好きなのはおしゃべりとロボットです」

形態要素化させた頭2つの要素をプレフィックス1と2とし、3つ目をサフィックスとする。
次はそのプレフィックス2とサフィックスを新たにプレフィックス1と2として処理させる。
この処理を繰り返して続けていく。

rubyの連想配列と配列で表現すると以下のようになる。

{
"あたし"=>{"は"=>["おしゃべり"], 
          "が"=>["好き"]}, 

"は"=>{"おしゃべり"=>["が",
                    "と"]}, 

"おしゃべり"=>{"が"=>["好き"],
             "と"=>["ロボット"]},

"が"=>{"好き"=>["な"]}

"好き"=>{"な"=>["プログラム",
               "の"]},

"な"=>{"プログラム"=>["の"], 
       "の"=>["は"]},

"プログラム"=>{"の"=>["少年"]},

"の"=>{"少年"=>["です"], 
      "は"=>["おしゃべり"]},

"少年"=>{"です"=>["%END%"]},

"と"=>{"ロボット"=>["です"]},

"ロボット"=>{"です"=>["%END%"]}
}

これで文法的に正しく、分岐を発生させられる辞書ができたことになる。
同じような例文があると、分岐が重複することも出てくるだろうが、それはそのまま残しておく実装にした。特定の要素の母数が増えれば、それが選択されやすくなり、特異な分岐は選ばれにくくなるが読ませた例文に依存するわけで自然だろうと判断したためである。


(marsialの利用)
毎回新規に例文を読み込ませ、マルコフ連鎖させた連想・配列を用意するのは手間である。
そのまま単純にファイルに書き出して読み込ませるにしても、
少々ややこしい形式で整形しているため解析処理を書くことも苦労するだろう。

そこでmarshalを使い、loadとdumpをさせている(markov.rb)。

def load(file)
  open(file, 'r') do |fp|
    @dic = Marshal::load(fp)
    @starts = Marshal::load(fp)
  end
end

def save(file)
  open(file, 'w') do |fp|
    Marshal::dump(@dic, fp)
    Marshal::dump(@starts, fp)
  end
end



◆ さらなる改良のために
1.
文章の主語、述語、修飾語等の単語間の関係性を特定する係り受け解析を使うと、よりおもしろいことができるだろう。
自然言語処理において、自然に見えるようにする工夫は種々考えられている。
  a) 形態素解析
  b) 構文解析(係受け解析)
  c) 意味解析
  d) 文脈解析

今回はa)だけを使ったが、さらにb)まで踏み込んでもいいだろう。
機械学習アルゴリズムの一つであるSVM(Support Vector Machines)を利用した日本語係り受け解析器としてはCabochaが有名である。

2.
単語をベクトル化して表現するする定量化手法であるWord2Vecを使うことも思いつく。
単語同士の類似度を算出させたり、またそれらを加算・減算処理させて意味を捉えたりとおもしろいことができる。

3.
さらに対象が単語だけだったWord2Vecから、文書にも意味を持たせてベクトルとして捉えるDoc2Vecを扱ってみてもいいだろう。

4.
回答候補のランキング付けにLearning to Rankを使うことも考えられる。

5.
TFIDF(Term Frequency Inverse Document Frequency)を組み込めば重要な単語を判断できるだろう。

6.
ディープラーニング技術をチャット技術にも使うことができる。
参考:ディープラーニング技術を使ったチャットロボットの例