にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

チャットボットをディープラーニングで作成

チャットボットをディープラーニングで作成

  • アマゾンプライムビデオを見ていたら、涼宮ハルヒの憂鬱が公開されており、懐かしい気持ちで見ていました。私がアニメとSFにハマるきっかけとなった思い出の作品です。
  • この作品をみてSFを調べだし、汎用AIに興味を持つに至りました。そして、高校生の身分でチャットボットを作ろうとしたのですが、当時はプログラミングスキルも、確率場に関する知識もはなかったので、あきらめていたのでした。
  • 今なら簡単なチャットボットなら組むことができるので組んでみて無念を晴らそうと思います。f:id:catindog:20161211205017p:plain

背景

  • 高校生の時の夢を叶えるべく、DeepLearningによるチャットボットを作る
  • GoogleのSeq2Seqではヘルプデスクの文章を学習させることによって、「人間らしい」チャットボットの作成に成功した
  • 人間らしいチャットボットとは、問題解決のタスクではなく、自然な応答を返してくれるようになること

f:id:catindog:20161211205103p:plain

環境

  • 764unitを3層重ねたLSTM, RNN, DeepLarning
  • Windows 10 x64
  • GTX 1050
  • Anaconda(Python 3.5)
  • Chainer latest
  • 学習時間は2時間弱でした
  • Memory 24GByte
  • CPU Corei7

データ・セット

  • 本当は女の子と話すシチュを想像していたのですが、男の子と女の子の会話のペアが取れそうになかったので、アニメのシナリオを利用することにしました
  • アニメや映画のシナリオではキャラAとキャラBが交互に話すスタイルを取っています。そのため、キャラAとキャラBの対話を一つのデータとして学習させることで、チャットボットを作成します

f:id:catindog:20161211205123p:plain

  • このサイトを参考にしたのですが、データ量が少なすぎで、一般的な応答の特徴の獲得までに至っていないようです。膨大なアニメのシナリオレポジトリ、ないでしょうかね。[1]
  • テキスト情報に換算して1.4MByte程度しかなかったです。
使用したアニメのタイトル一覧
 コードギアス 反逆のルルーシュ
デュラララ!!
東のエデン
エルフェンリート
涼宮ハルヒちゃんの憂鬱 & にょろーん☆ちゅるやさん
けいおん!
らき☆すた
涼宮ハルヒの憂鬱
日常
誰も知らない
まどか☆マギカ
狼と香辛料
侵略!イカ娘
天体戦士サンレッド
東方夢想夏郷
...and more

動作確認

λ python chatbot.py --model cv\latest_anime_640.chainermodel --vocabulary data\anime\vocab .bin
INTPUT なんか違わなイカ!?
OUTPUT あんたはかもでゲソ

ちゃんとイカ娘の文脈になる

λ python chatbot.py --model cv\latest_anime_640.chainermodel --vocabulary data\anime\vocab .bin
INTPUT やあ、長門っち、長門っち!
OUTPUT え? ま、ちょっとな、何考えなの何だよ。まだイカ娘のほうがまだ出るのかもしれない

ハルヒの文脈にならない、データが足らないか

λ python chatbot.py --model cv\latest_anime_640.chainermodel --vocabulary data\anime\vocab .bin
INTPUT 死にます
OUTPUT イカ姉ちゃん、がんばってください

がんばる。

λ python chatbot.py --model cv\latest_anime_640.chainermodel --vocabulary data\anime\vocab .bin
INTPUT 雨降るって言ってました?
OUTPUT ねー! なんてやって……

雨の話題にならないですね。収束率は悪くなかったので、データ量とデータ・セットの特性によるかもしれない。

感想

  • 数ギガに及ぶ対話文があれば、一般的な会話の応答が学習できるのではと思いました。実際は1.4Mしかないです。
  • WindowsでのPythonC++などのプログラミング、めっちゃ消耗しませんか。大変でした...
  • エンドレスエイトの戦犯、未だに許せません...

複数の著者をテキスト情報を混ぜ合わせたRNNと長文の改善手法

問題背景

  • RNNによる単純な小説の学習と、学習結果の出力は過去、多くの事前研究で行われており、ある程度、人が書いたらしいと思われる文章が数多く出力されてきた。[1], [2]
  • しかしながら、複数の著者の小説を混在させて評価した例は少ないか無いように思う。
  • 著者を混ぜ合わせて学習した場合、両方の著者の特性を備えたモデルができるのではないかと仮定した。
  • また副次的知見として、Attentionの情報を引数に取らないRNNは一般的に長文に弱いが、もっとかんたんな方法で長文を生成することに成功したので、その時の情報を記す。

提案手法

  • 複数の著者の文章を混ぜ合わせ、学習用データ・セットとする
  • 長文を出力させるために、前の出力内の特徴となる文字を取り出して、一度、LSTMをリセットし、前の文の末尾の文字列のシーケンスを経過したと言う状態を作り出し、連続しない自然な長文を出力させる

f:id:catindog:20161209141815p:plain

図1. 今回使用した長文生成モデル

実験手法

  • 小説家になろうのサイトより、「無職転生 - 異世界行ったら本気だす -」と、「転生したらスライムだった件」と「蜘蛛ですが、なにか?」を2016/12/09まで公開されている情報をもとに、文字粒度のRNNを学習させる
  • 1. 「無職転生 - 異世界行ったら本気だす -」のみで学習させる
  • 2. 3つの作品を順序を保ったまま、学習させる
  • 3. 3つの作品の各行をランダムにシャッフルさせ、学習させる

評価指標

  • 出力結果が自然な文章であり、特定の誰かの文章でないと評価できる場合にtrue、自然な文章でなく、特定の誰かの文章なのだと確信でき場合にfalseの二値として、分類し、1.2.3で各々100文を評価して、スコアを比べる

結果

  • 1. score 56 @3layer,Unit1024
  • 2. score 1 @3layer,Unit1024
  • 3. score 60 @3layer,Unit1024

 以上のような結果となり、2に関してのみ極端に悪いスコアとなった。主な原因は自然な文章が全く生成されないことに起因している。
f:id:catindog:20161209142124p:plain

図2. 1の出力結果。長文である

f:id:catindog:20161209142155p:plain

図3. 3の出力結果。長文で独自の言い回しが見て取れる

考察

  • 複数の著者の文章を単純に順番道理に連結しただけでは、そもそもtrain-lossが下がらず、まともに学習できていないなどの問題があることがわかった。これは、RNNが学習する文章の特徴量がその作者固有のものであると考えられる。一層あたりのRNNUnit数は1024の3層のモデルであったので、もっと層を増やすなどなどの工夫をすれば、正しく学習できる可能性があるが、計算時間を考慮すると一部現実的でない可能性もある。
  • シャッフルした文章を学習しても、正しい文章は出力されるようであるが、文脈構造が弱いように感じられる。順序情報を失ってしまったたであると考えられる。
  • 自然でユニークな文章であるというスコアに関しては1と3で大きな有意差はなく、同一でない文章を作るだけならば、特定の著者や作品に偏っていてもいいかもしれないが、特異的にユニークである文章を3に関しては作成することができた。例を示すと以下のようになる。
 この世界では、リーダーが火炎球(フルメッド)に制御されていたのだ。
 この世界では、リーダーが火炎球(フルファンジーヴァングアーン)を観察した。
 この世界では、リーダーが火炎球(レンドエンド)に相当するハズだ。
 この世界では、リーダーが火炎球(フルフォン)の天敵となった場所に進化したのだ。
 この世界では、リーダーが火炎纏(チェイン)を用いていた。

なんだかよくわからない中二病ワードを効率よくそれっぽく生成することができるようである。これを複数の作者が入ったため、本来存在しない中二病ワードの生成に成功したと捉えられそうである。

レビューのスコア予想問題

背景

  • 商品やサービスを論じるときに、その文脈から定量的にどの程度良かったのか、悪かったのか知ることは難しい
  • 幸いなことにネットには膨大な商品とサービスのレビュー件数が存在し、サービスごとのドメインが異なってもある程度、定量的に文章から良し悪しを把握することができる
  • ある程度、機械学習アルゴリズムにより正しく分離できるのならば、分離したときの素性の重要度等の情報を用いて、文章が星がつきやすい文章なのか、星がつきにくい文章なのか判断する指標として使うことができる
    • 仮説ですが、星がつきやすい文章はポジティブであり、星がつきにくい文章はネガティブだと考えることができるように直感的には感じます

f:id:catindog:20161205181804p:plain

図1. ベクトル化した図の例

f:id:catindog:20161205181847p:plain

図2. 何らかの機械学習アルゴリズムで分離した場合

  • 青のプロットを、星の数が少ないレビューとして、オレンジの部分がレビューの数が多いとすると、以下のような分離面が構築できます。図示しやすいように2次元ですが、実際には60000次元と、かなり高次元でスパースなデータになっています。

手法

  • 規約でスクレイピング等を得に禁じていないサイトなどで、信用できるレビューをパースします。この時、信用できると判断する条件は、ある程度文章量があること、レビュー表示アルゴリズムで、上位に来ていること、レビューに参考になったなどの付加情報を利用します。
  • レビューを形態素解析して、if-idfでベクトル化する。
  • ベクトル化した情報を、機械学習のbinary classificationで分離し、ある程度の精度が出るか確認します
    • 機械学習で分離した結果を利用して、その重みを分離に用いた重要な因子としてみなすことができます。

データ

  • データの配布は禁止されているので、配布することができません。申し訳ありません。
  • 20161205までに集めた13万件のレビューをもとに、分類を行いました。
  • KVSにURL: {stars: 1~ 5, contents: レビュー内容}のように保存することで、データを管理しています。
  • 星の分布には偏りがあり、星が5が圧倒的に多く、星1, 2, 3は少なく、1, 2, 3は足し合わせても、星5の総数には及びません。
    • そこで、星5と星1,2,3の分離問題として捉えて、分離するようにしました。

f:id:catindog:20161205182523p:plain

図3 星の分布@N11000程度の場合

XGBoostによる分類

  • XGBoostはランダムフォレスト等と系譜を同じくするディシジョンツリーの機械学習アルゴリズムです。非常に高い精度で分離できるため、2015年からKaggle等で用いられるようになっているようです。[1]
    • eta: 0.3
    • gamma: 1.0
    • min_child_weight: 1
    • max_depth: 20
$ wc -l differ20161205-2.svm
90233
$ head -n 75000 differ20161205-2.svm > train.txt
$ tail -n 15000 differ20161205-2.svm > test.txt

$ xgboost t.conf
[16:57:31] 75000x93504 matrix with 8267281 entries loaded from train.txt
[16:57:31] 15000x93498 matrix with 1651122 entries loaded from test.txt
….
[17:00:58] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 144 extra nodes, 300 pruned nodes, max_depth=20
[17:00:58] [99] test-error:0.048600 train-error:0.002213
[17:00:58] update end, 112.189 sec in all
  • train errorが4.86%であり、十分、実用に耐えられるレベルで分離できているように見える。

liblinear(ロジスティック回帰)による分類

  • - 最もシンプルな分類であると思われます。収束も早く、このロジスティック回帰に対してどの程度の性能が出るかで性能を確認したりすることもあります。
$ liblinear-train -s 0 train.txt
.............................................*.......................................................
optimization finished, #iter = 1000
WARNING: reaching max number of iterations
Using -s 2 may be faster (also see FAQ)
Objective value = -30.960220
nSV = 24990
$ liblinear-predict test.txt train.txt.model result
Accuracy = 94.12% (14118/15000)
  • 94.12%とXGBoostには及ばないけれど、十分に性能が発揮されているように思われます。学習の結果、素性の重要度の解釈が容易なので、ロジスティック回帰の結果のウェイトを確認します。

ロジスティック回帰の重みの結果

  • 単語一つにつき、一次元というパワープレイをしているので、単語数がたくさんないと正しく学習できないのですが、900,000件もあれば、多分大丈夫です。
  • (注)libsvmとliblinearは、0番目のインデックスは読み込むことができないので、スルーしていますが、本来はこのようなアドホックなことはすべきではありません。

f:id:catindog:20161205183500p:plain

図4 ポジティブ、ネガティブの素性

モデルの定性的評価

  • ロジスティック回帰は非常に単純な式で表現できるため、特徴量の重要度が分かれば自分で計算することができます。

f:id:catindog:20161205183539p:plain

図5. アルファ、ベータに特徴量の重要度が入ります

  • プログラムにしてもわずか数行です。Python2ですが、好きなのを使ってください
tsv = filter(lambda x:x != '', open('stash/star_ranking.tsv').read().split('\n'))
trank = {}
for t_w in tsv:
  t, w = t_w.split(' ')
  trank[t] = float(w)
  idf = json.loads(open(filename + '.idf.json').read())
  m = MeCab.Tagger ("-Owakati")
  for line in sys.stdin:
    score = 0.
    line = line.strip()
    for t in m.parse(line).strip().split(' '):
      if idf.get(t.decode('utf-8')) != None and trank.get(t) != None:
        score += idf.get(t.decode('utf-8')) * trank.get(t)
      res = int(1. / (1. + math.pow(math.e, score*-1 ) ) * 100)
      if res < 50.:
        print("だめっぽい文章", end=" ")
      else:
        print("よいっぽい文章", end=" ")
     print("score =", res)

定性評価

  • 自分で作ったデータセットに対して、予想したとおりの性能を発揮するか確認します。
$ echo "風邪を引いて頭がとても痛いので家で寝ていたいめう。" | python Review.py --mode score --file tmp/differ20161205
だめっぽい文章 score = 44
$ echo "冬のボーナスが出たので新年は新しい家族をむかえてハワイに旅行に行く。" | python Scraper.py --mode score --file tmp/differ20161205
よいっぽい文章 score = 55
  • 大丈夫っぽいですね。

結論

  • 日本語の意味しているところを正しく分類しようとすると、次元数が多すぎて分類するの大変で性能がでないのではないかと危惧していた時代もありましたが、意外とかんたんにできるので、応用の範囲は広そうだと感じました。
  • 単語のtfidfしか見ていないので、文章構造は無視しているので、これを取ろうとすると、また違った仕組みが必要になります。個人的にはRNNなどで分類するのがちかいと思います。LSTMなどの仕組みが長文を正しく認識できるのか、いまいち自信がないのですが…
  • これをセンチメント分析の分類器として使えないかどうか調べているのですが、微妙にレビューと使っている単語の分布が異なっており、専用のデータ・セットの必要性を感じています。[2]

■ 機械学習で絵文字の意味を知る

機械学習で絵文字の意味を知る

絵文字は近年になってiPhone, Androidに搭載されてSMSやTwitterなどで積極的にソーシャルメディアに現れるようになった文字です。
文字自体の成立が比較的新しいということもあり、広辞苑などに詳しい意味が定義されているわけでないです。そのため、脳機能がある程度成熟してしまった青年から壮年の人間にとって新規の言語体系を学ぶのにも等しいコストが発生しているものと思われます。辞書もない今、絵文字の正確な用法と意味を学ぼうとすると、若い世代と積極的にコミュニケーションを取らねばならず、若い世代との認知がズレているために、コミュニケーションそのものが成立しなく、結果として若い世代が発明・進化させた単語を正確な意味で知ることはできないのです。
このブログでは、新たな言葉の意味をWord2Vecなどの周辺分脈を解析して、その意味するところを把握するというアプローチを撮りたいと思っています。
f:id:catindog:20160907114425p:plain
[1] PFI, Slideshare
Word2Vecから周辺に同じような文脈で用いられる単語を表示することで、意味を類推することを可能とさせます。

■ 😇 💗 🙏 😂 🎉 💕 💢 ✋ 😘 ✨😊 <- 意味がよくわからない

 Twitterの開発者サービスに登録するとstreaming apiというもの経由で全ツイートの1%を無料でサンプリングできます!ふとっぱら、ツイッター社♡[4]
 Twitterとか若い子もやっていると仮定して、Twitterに絵文字が使われるとその周辺の文脈を学習して、類似の分布をもつ日本語で置き換えることで、若くない世代にも理解しやすくしましょうというアプローチです。
 💗とかは”愛してます”ぐらいの感情表現であると理解できるのですが、😇ってなんですかね。。。天使?
 わからないので、Streaming APIをぶんぶん回して、たくさんサンプリングしました。
 はい、近しい単語トップ10ドーン☆

文字, 関連度
😌   0.830253481865
😢   0.813541054726
🤗   0.793880105019
🙏   0.774758398533
💞   0.774288773537
💭   0.77075445652
🙌   0.767523646355
🙈   0.75866407156
💦   0.758447051048

 ファッ!?😇の意味を知りたいのに、同じ絵文字が近いとか出てくるのか。。。これは、想像以上にジェネレーションギャップありますわ。奴ら絵文字で会話しとる。
 トップ100まで拡張して、日本語がでるまでひたすら頑張る。

文字, 関連度
こんにちは   0.666822075844
タメ   0.662293732166
頑張る   0.659095883369
ありがと   0.658896565437
元気   0.63748061657

 旧おっさん日本語に限定すると、”こんにちは”とか”頑張る”とか”ありがと”とかポジティブな文脈で使われることが多いようです。なにかポジティブで前向きな感情としておじさん理解しました。
 次、🙏<-これです

タメ   0.707892537117
タグ   0.693758368492
インスタ   0.65028488636
呼びタメ   0.62894743681
仲良く   0.625664234161
…省略…
(・ω・)ノ   0.57857966423
嬉しい   0.571420192719
よろしくお願いします   0.569167256355
お誕生日おめでとう   0.567905783653
ぜひ   0.566062629223
すいません   0.565639853477
おねがい   0.560641229153

 タメの意味すら怪しかったのですが、どうやらタメ口で話すほど、仲良くしましょうって意味らしいです。ということは、このアイコンはハイタッチなのかな? ずっと、ごめんなさいっていう手を合わせて誤っている用法しか知らなかったのですが、ランク的にはずっと低いです。おぢさん、学びました。
 ちなみに主成分分析PCAをかけるとこんなグラフが得られます。
f:id:catindog:20160907114558p:plain
 パソコンのディスプレイは二次元なので、二次元に情報が圧縮されてしまっていますが、用法の分布的にはこんな感じだそうで、日本語だけ追っていくと、”よろしく”とか”仲良くしましょう”っていうポジティブ表現が一番近いのでは無いでしょうか。

■ おっさん vs JK

 あまりにもおっさんとJKでやばい違いが出たので、これは載せざるを得ないと判断しました。
 おっさんの周りを主成分分析します。
f:id:catindog:20160907114718p:plain
 なにこれひどい。文字ばっかりでネガティブなのも多い。

対して、JKを主成分分析します。
f:id:catindog:20160907114742p:plain

 💗💓なんて可愛いんでしょう。絵文字が散らばって華やかです!かわいい。JKになりたい。

■ Word2Vecは別の言語でも同じような分布を取ることがある

 今回のアプローチでは手が回らなかったのですが、未知の言語でも同じような単語の分布を取ることがあるようです。以下の図では、数字とか家とか牛とか豚とか、人間であれば同じような地球上どこでも持ちうる社会的背景を利用して、同じような文脈を取りうるからか、同じような単語の分布になるようです。
f:id:catindog:20160907114822p:plain

 未知の言語体系を持つ宇宙人とかと会話するときなど、必要なんじゃないでしょうか。少なくともSeq2Seqでは教師あり学習で、誰かが未知の言語を習得している必要がありますし、最初のアプローチは統計的?機械学習的アプローチになりそうです。
 絵文字でこれを表現するのは、できなくもないような。JKと話すのは宇宙人と話すようなものだしね。理解する方法としては、あながち間違っていない。

■ 直感的理解

 画像のCNNやRNNに比べて視覚表現で表せられないぶん、よくわからないという声をよく聞きまが、 Qiitaの図がよくわかりやったです。
f:id:catindog:20160907114857p:plain

[3]

■ 数学的理解

 Word2Vecを知って一ヶ月ほど詳しい内部の仕組みを知らずに、概要だけで動かしていたのですが、詳しく理解しないといい加減ダメだろうとarXivを読もうと思い腰を上げたのです。arXivを読む前に、ブログを漁っていたら、すごく詳しいブログを見つけました。
ニューラルネットワークで、別に深い学習というわけではないのです。
詳しい日本語を使った解説は[2]のリンク元が引用元になりますので、chainerやTensorFlowで実装するにはここを見ていただけるといいかと思います。
入力層にOne-hot vectorで単語の列を入力します
中間層がOne-hot vectorと中間層のノードの数を掛けあわせた、テンソルになっています。
f:id:catindog:20160907115034p:plain
f:id:catindog:20160907115049p:plain
f:id:catindog:20160907115110p:plain
出力層がソフトマックス関数で最初にSkip-gram関数でモデル化したものを予想したものです。
f:id:catindog:20160907115138p:plain
f:id:catindog:20160907115151p:plain

■ おまけ 社畜

 おっさんも社畜もno future。JKになりたい
f:id:catindog:20160907115227p:plain