; oi: 機械学習
ラベル 機械学習 の投稿を表示しています。 すべての投稿を表示
ラベル 機械学習 の投稿を表示しています。 すべての投稿を表示

2017年8月31日木曜日

AI×データ時代に必要な「知覚」能力を補う方法

今更ではあるが、安宅和人氏の著した以下の記事を拝読した。安宅氏の経験に基づいた幅広い視野と深い思考・洞察があり、大変参考になる良記事であった。本記事のみのPDFであれば、800円程度で購入できるので、人工知能やデータ分析に携わる方はもちろん、そうでなく今後の未来予測に関心がある幅広い方々も、是非一度ご覧になっていただきたい。

知性の核心は知覚にある
~AI×データ時代に人間が生み出す価値とは~
(2017年5月号 特集 知性を問う)
http://www.dhbr.net/articles/-/4784

本ポストでは、この安宅氏の記事の内容を自分の言葉でまとめると共に、私が本記事を拝読して考えたことを後半に備忘録として追記しておく。本記事を読むことで、近年流行ってきた様々な手法の意味が再整理できたように思う。

「知覚」は重要である

これからは、幅広く目を向け、得られた情報を統合して、情報の意味合いを理解し(=「知覚」)、解釈した内容を言語として表現できることが重要な意味を持つ。

ますます世の中の問題は複雑化していき、それを解くための方法はより一層高度化し、多様化していく。このような状況下では、解くべき問題を定義する力と、解決手法を領域横断的に組合せる力が必要となる。そして、不完全な情報が複雑に絡み合う状態から、取り組むべき課題を見極め、答えを出すべき問いを定義するという極めて高度な情報処理においても、問いに対して様々な領域の知識や知見を総動員して取捨選択し、意味のある組み合わせを見つけ、革新的な解を出すという情報処理においても、「知覚」する能力は必要不可欠となってくるのだ。

なぜ「知覚」が重要なのだろうか

では、なぜ「知覚」が必要不可欠であり、より重要となってくるのだろうか。
その理由としては、情報を統合して意味合いを理解する能力は、これまでの思考・経験に依存する部分が大きいことが挙げられる。得られた情報のみを基に考える場合、情報が不完全なことが多いため、情報が不足し論理的に導かれる帰結は少ない。よって、これまで知り得た暗黙的なルールや別の前提を踏まえて、情報を補完しながら意味合いを理解することでしか、結論が見出せないことが多くなってくる。

「知覚」の能力を身につけるためには

よって、様々なことを経験し、知覚を鍛えるトレーニングをした方が良い。具体的には、複雑な情報の要素を見極め、性質を理解し、要素間の関係性を把握することである。この訓練は、既知の慣れ親しんだ環境下では難しく、全く新しい環境下において新しい経験を行う際により鍛えられると思われる。その状況を正しく理解しないと適切な行動をとれず、生きていけないからだ。

「知覚」能力を補填するためには

以下は本記事には全く書かれていない内容であり私の考えに過ぎないが、より効率的に「知覚」する方法、「知覚」能力を補填する手段もあるように思う。

たとえば、個人レベルでは、既存のモデル(ことわざや物理モデルなど)を対象にあてはめ、類似点や相違点を認識する、アナロジー(類推)思考を行うことで、意味合いの理解や解釈、次の結果まで予測できる場合がある。また、複雑かつ不完全な情報で構成される対象をシステムと捉え、構造的に理解するシステムズアプローチと呼ばれる方法も、要素の洗い出しや、要素間の関係性の整理に役立ち、情報の意味合いを把握、表現、共有することが容易になる。

一方で、チームレベルでは、デザイン思考ベースのワークショップなどで、他者の知覚情報を上手に取り入れ、チームとしての「知覚」能力を高めることも考えられる。

しかしながら、このようなアプローチのベースには、やはり原体験・経験に基づく「知覚」が重要な意味を持つ。また、これらのアプローチの結果を解釈する上でも、高度な認識能力が要求されることを忘れてはいけない。

2017年7月17日月曜日

今後必要となる量子コンピュータ(量子アニーリング方式)に関する周辺知識まとめ

「量子コンピュータが人工知能を加速する」を読んだ内容と周辺知識を整理した。
近年、注目を浴びている量子コンピュータの実情と今後に興味のある方には、必読の一冊と思う。近いうちにちゃんと勉強する必要が出てくるであろう。


本書は、量子コンピュータの基礎原理の一つである「量子アニーリング」方式の理論を提唱した、西森教授、大関准教授による共著の一冊となっている。

量子アニーリング方式の量子コンピュータは、以前は研究の本流であった量子ゲート方式に比べ、今現在とてつもない脚光を浴びている。

理由を平たくまとめると、解ける問題の幅は狭いが、近年重要な人工知能を下支えする重要な問題に対して適用可能で、従来のコンピュータに比べ、超高速(1億倍)かつ低エネルギー(スパコン京の500分の1)で、比較的安定的に解くことができるのだ。

量子アニーリングとは、組み合わせ最適化問題を解く場合に、量子効果を用いる方法である。まず、目的関数をイジングモデル(2値変数とその関係性の関数)として表現する。この2値変数が上下の向きを持つスピンに相当し、それらが格子状の構造を持っているモデルである。格子状につながっている変数同士は同じ値を保持したほうが、エネルギーが低く安定した状態となる。
次に、このイジングモデルに対して、横磁場をかけることで、スピンを上か下の決まった向きではなく、上下の両方を重ね合わせた状態で持つことが可能となる。これを格子状のすべての変数に対して重ね合わせた状態で持つことで、あらゆる解候補をすべて重ね合わせた状態で持つことに相当する(下図左)。
その後、だんだんと横磁場を弱めていく(上か下かをフィックスさせていく)と同時に、格子状の関係性(相互作用)を強くすることで、重ね合わせの確率分布が変化(下図中央)し、同最適化問題の解を得ることができるのである(下図右)。

Fig.量子アニーリングの概要 (横軸は2値変数の組の取るいろいろな値の組(古典状態),縦軸の黒の曲線は目的関数の値,青の線は各配位の存在確率を表す。)引用元:西森教授の量子アニーリング説明ページ

簡単に、量子アニーリング方式と量子ゲート方式を比較すると以下のような違いとなる。
  • 量子アニーリング方式
    • 組合せ最適化問題に特化
    • 安定:エネルギーが低い状態のみを保持すれば良いため
    • ※カナダのベンチャー起業のD−Wave社が実装した量子コンピュータは同方式
  • 量子ゲート方式 
    • 理論的には汎用的に利用可能
    • 不安定:重ね合わせの状態を保持する必要があるため
詳細は、著者の西森教授による量子アニーリング説明ページをご覧いただきたい。
量子アニーリングの論文やイジングモデルの具体的な定式化例の説明がある。
加えて、下記2つの記事は包括的に量子アニーリングについて説明している
大変有益な記事であった。
  1. 「量子コンピュータが人工知能を加速する」を読んで、数式を使わずにPythonでその概要を説明してみた
    http://qiita.com/onhrs/items/aa0aa181c27743956689
  2. 物理のいらない量子アニーリング入門
    http://blog.brainpad.co.jp/entry/2017/04/20/160000
現実の組み合わせ最適化問題を量子アニーリング方式で扱うためには、イジングモデルという形式に落とし込む必要がある。イジングモデルについては、上記記事にも説明が書かれているが、より直感的な理解を求める方は、以下2つを参照されたい。
  1. Ising Modelを平易に解説してみる
    http://enakai00.hatenablog.com/entry/20150106/1420538321
  2. イジングモデル - KnowledgeBase - 岡山大学理論化学研究室
    http://theochem.chem.okayama-u.ac.jp/wiki/wiki.cgi/exp11?page=%A5%A4%A5%B8%A5%F3%A5%B0%A5%E2%A5%C7%A5%EB
また、私が驚いたのは、D−Wave社のような量子コンピュータのハードウェア企業に加えて、「組み合わせ最適化問題」を量子アニーリング方式で扱う「イジングモデル(量子ビットとその相互作用の組み合わせ)」に変換するのに特化したソフトウェア企業が現れているというのも驚きである。
彼らはhardware-agnostic platforms and services(ハードウェア非依存の基盤とサービス)を適用している。今後、D−Wave社以外の量子アニーリング方式のハードウェアが現れてきても、1QBit社のイジングモデルへの記述方法のナレッジは適用可能である。また、日本独自の量子コンピュータと称される、国立情報学研究所が開発中の「レーザーネットワーク方式」の量子コンピュータもイジングモデルを採用しているため、イジングモデルで記述した問題はそのまま解くことができる。今後、ますます重要な役割を担うであろう。

最後に、現状の日本の量子コンピュータに関する取り組みについては、以下を参照されたい。
  1. 日本独自の量子コンピュータ
    http://itpro.nikkeibp.co.jp/article/COLUMN/20140314/543707/?rt=nocnt
  2. 人工知能に必要な「量子コンピュータ」とは|日本の取り組みと必要スキル
    https://furien.jp/columns/267/
雑多ではあるが、本書を読んで調べた周辺知識のまとめは以上である。
今後も、量子コンピュータ界隈の動向から目を話せないことは間違いない。

2016年12月4日日曜日

今更聞けないEMアルゴリズムの解説〜潜在変数が連続変数の場合のEステップの説明〜

$\boldsymbol{z}_i$が連続変数の場合について、変分法によって確率分布$Q(\boldsymbol{z}_i) $を求める方法も追記しておく。

すなわち、以下を示す。
$$
\begin{eqnarray}
Q(\boldsymbol{z}_i)  &=& P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})
\end{eqnarray}
$$

E-Stepの説明(潜在変数が連続変数の場合)

E-Stepでは、$\boldsymbol{\theta}$固定の下、尤度関数の下界の分布を最大化する。以下で尤度関数を変形し、下界を求める手順を示す。

$$
\begin{eqnarray}
\displaystyle \sum_{ i = 1 }^{ N } \ln P( \boldsymbol{x}_i \mid \boldsymbol{\theta}) &=& \displaystyle \sum_{ i = 1 }^{ N } \ln \displaystyle \int P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta}) d\boldsymbol{z}_i\\
&=& \displaystyle \sum_{ i = 1 }^{ N } \ln \displaystyle \int Q(\boldsymbol{z}_i)\frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)} d\boldsymbol{z}_i\\
&\geq&  \displaystyle \sum_{ i = 1 }^{ N }\displaystyle \int Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}d\boldsymbol{z}_i
\end{eqnarray}
$$

(2)式から(3)式への変形は、$\boldsymbol{z}_i$の任意の確率分布$Q(\boldsymbol{z}_i) $でかけて割っただけである。この時、$Q(\boldsymbol{z}_i) $は何ら仮定をおいていないことに注意されたい。
(3)式から(4)式への変形は、Jensen's Inequalityを利用した。

この時、$\boldsymbol{\theta}$固定の下、下界(4)式の最大化を考える場合、変分法を用いれば良い。下界(4)式を$Q(\boldsymbol{z}_i) $の汎関数(関数の形を変化させると値が変化する関数。わかりやすい説明は「物理のかぎしっぽ(変分法1)」を参照されたい。)と捉え、変分法によって極値を求める。

特に、$Q = Q(\boldsymbol{z}_i) $とした時、$\boldsymbol{z}_i, Q$によって決まる、以下のようなシンプルな汎関数を考える。

$$
\begin{eqnarray}
\displaystyle \int f( \boldsymbol{z}_i, Q) d\boldsymbol{z}_i
\end{eqnarray}
$$

このシンプルな汎関数を求めるための、オイラー・ラグランジュ方程式は、以下で表せる。(その他の汎関数のオイラー・ラグランジュ方程式については、「物理のかぎしっぽ(変分法2)」を参照されたい。)

$$
\begin{eqnarray}
\frac{ \partial f }{ \partial Q }
\end{eqnarray}
$$

よって、下界(4)式の積分部分に注目し、$\int Q(\boldsymbol{z}_i) d\boldsymbol{z}_i =1$の制約を加えた汎関数は以下となる。

$$
\begin{eqnarray}
\displaystyle \int Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}d\boldsymbol{z}_i - \lambda (1- \int Q(\boldsymbol{z}_i) d\boldsymbol{z}_i )
\end{eqnarray}
$$

これを$Q$で変分すると以下を得る。
$$
\begin{eqnarray}
\ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}+Q(\boldsymbol{z}_i) \cdot \frac{Q(\boldsymbol{z}_i)}{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})} \cdot \left[- \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)^2} \right] + \lambda = 0
\end{eqnarray}
$$
$$
\begin{eqnarray}
\ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}  = -\lambda + 1
\end{eqnarray}
$$
$$
\begin{eqnarray}
Q(\boldsymbol{z}_i) = e^{\lambda - 1}P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})
\end{eqnarray}
$$

$\int Q(\boldsymbol{z}_i) d\boldsymbol{z}_i =1$より、以下を得る。

$$
\begin{eqnarray}
Q(\boldsymbol{z}_i) &=& \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{\int P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta}) d\boldsymbol{z}_i}\\
&=& \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{P( \boldsymbol{x}_i \mid \boldsymbol{\theta}) }\\
&=& P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})
\end{eqnarray}
$$

これは、離散分布を仮定し、EMアルゴリズムのE-STEPを説明した「今更聞けないEMアルゴリズムの解説」の(8)式と合致する。

2016年11月15日火曜日

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

ケーススタディ

前振りが長くなったが、開発したソースコードを用いて実際に「対話ボット」を学習し、実行した結果を示す。

学習データ

学習には以下の日常で頻繁に利用している家族とのLINEトークデータを用いた。
  • 全LINEトークデータ 17,796ペア(in/out)
  • trainデータ 16,017ペア(in/out)#全データの9割
  • devデータ 1,779ペア(in/out)#全データの1割

ディレクトリ(学習開始時)

学習開始時は、以下の3つのpythonファイルと2つのディレクトリを同階層に配置する。また、line_talk_dataディレクトリには、学習データとして作成した4種類のファイルを格納する。(学習後には中間生成物が各フォルダに生成される)

  • chatbot.py
  • data_utils.py
  • seq2seq_model.py
  • line_talk_data #学習データ格納用ディレクトリ
    • line_talk_train.out
    • line_talk_train.in
    • line_talk_dev.out
    • line_talk_dev.in
  • line_talk_train #学習結果のcheckpointデータ格納用ディレクトリ

学習の実行

python chatbot.py

実行後のコンソールを以下に示す。
chatbot$ python chatbot.py 
Preparing LINE talk data in line_talk_data
Creating vocabulary line_talk_data/vocab40000.out from data line_talk_data/line_talk_train.out
Creating vocabulary line_talk_data/vocab40000.in from data line_talk_data/line_talk_train.in
Tokenizing data in line_talk_data/line_talk_train.out
Tokenizing data in line_talk_data/line_talk_train.in
Tokenizing data in line_talk_data/line_talk_dev.out
Tokenizing data in line_talk_data/line_talk_dev.in
Creating 3 layers of 256 units.
Created model with fresh parameters.
Reading development and training data (limit: 0).
global step 100 learning rate 0.5000 step-time 0.66 perplexity 8820.34
  eval: bucket 0 perplexity 3683.12
  eval: bucket 1 perplexity 4728.98
  eval: bucket 2 perplexity 4118.81
  eval: bucket 3 perplexity 5504.88
以下のように、ある程度収束してきたところで、学習を切り上げる。 今回は以下のスペックのMac book pro にて8時間程度学習を行った。

CPU : 2.9 GHz Intel Core i5
メモリ: 16 GB 1867 MHz DDR3

この時、line_talk_trainディレクトリの中に、学習ステップに応じた .ckptファイルが蓄積されている。対話実行時に、蓄積された最新の.ckptファイルはリストアされる。
global step 17000 learning rate 0.3812 step-time 0.49 perplexity 30.66
  eval: bucket 0 perplexity 29.40
  eval: bucket 1 perplexity 45.61
  eval: bucket 2 perplexity 44.65
  eval: bucket 3 perplexity 85.85
global step 17100 learning rate 0.3812 step-time 0.51 perplexity 30.04
  eval: bucket 0 perplexity 113.59
  eval: bucket 1 perplexity 55.54
  eval: bucket 2 perplexity 39.45
  eval: bucket 3 perplexity 45.94

対話の実行(--decode)

python chatbot.py --decode

--decodeオプションをつけて実行することで、対話モードで実行できる。 以下に「対話ボット」の対話例を示す。 最新の.ckptファイルをリストアされ、対話モードに入っていることが確認出来る。
chatbot$ python chatbot.py --decode
Reading model parameters from line_talk_train/chatbot.ckpt-17600
> 

以下に対話例を示す。 ([Morpho]タグ行)は入力の形態素解析の結果を出力している。
> 今日は何時に帰る?
([Morpho]:今日 は 何 時 に 帰る ?)
00 : 00 くらい か なー !
> 今から帰りまーす
([Morpho]:今 から 帰り ま ー す)
今 から ます !
> おつかれー
([Morpho]:お つかれ ー)
[ スタンプ ]
> 退社しましたー!
([Morpho]:退社 し まし た ー !)
00 : 00 くらい か なー !
> お腹空いたのでゆっくり食べてます〜
([Morpho]:お腹 空い た ので ゆっくり 食べ て ます 〜)
たいしゃ

そこそこの対話ができていることがわかる。
「今日は何時に帰る?」という質問に対して、時刻を返答しようとしていることがわかる。パターンとしては上手く学習できているが、 実際に情報がないので正しい時刻を返せていないのは残念であるが。
「ゆっくり食べてます〜(先にご飯を食べています)」という情報に対して、遅れて「たいしゃ」したというのも、日常でよくあるやりとりを上手く学習できているといえる。
また、LINEトークのテキストデータのみを学習しているため、一部の返答が[スタンプ]になってしまっているのも仕方がない結果であろう。

まとめ

LINEトーク履歴のテキストデータを用いた「対話ボット」を開発してみた結果、個人のLINEトークの返し方を学習し、それっぽい回答をしてくれることがわかった。翻訳と違い、「良い」「悪い」の基準が極めて曖昧なため、評価が難しいのは事実であるが、可能性を感じる結果にはなった。

今後は、データ量を増加させ、時系列を加味して対話データの生成するなど、学習データの洗練を行いたい。また、今回ハイパーパラメータについても、計算量削減のため、デフォルトの設定よりもだいぶ小さい値を用いている。データ量の増加に合わせて、ハイパーパラメータの調整も併せて行うことで、より自然な対話ができるようになると思われる。

参考



話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

2. 「対話ボット」学習ロジックの実装


参考2:Tensorflow: Sequence-to-Sequence Models に含まれる、models/rnn配下の以下の3つのソースコードを基に、「対話ボット」に必要な修正を加える。実行時のメインメソッドを含む translate.py をchatbot.pyとして修正を加えた。


No.srcdescription
1translate/seq2seq_model.pyNeural translation sequence-to-sequence model.
2translate/data_utils.pyHelper functions for preparing translation data.
3translate/translate.pyBinary that trains and runs the translation model.

学習ロジック関連で、プログラムに修正を加えた点は以下だけである。
2. data_utils.py に対して「(LINEトーク履歴から作成した)学習用データ」の読み込み部分にprepare_line_talk_dataメソッドを作成
3. chatbot.py に対してdata_utils.py内のprepare_line_talk_dataメソッドを呼び出すように修正

3. 「対話ボット」対話ロジックの実装

対話ロジック関連で、プログラムに修正を加えた点は以下だけである。
3. chatbot.py に対して、Decode時の日本語の形態素解析処理の追加

英仏翻訳のためのsequence-to-sequenceモデルに対して、これだけの修正を加えるだけで、「対話ボット」として利用可能となる。

最終的に、以下の3つのソースコードとdataディレクトリを同階層に配備するだけで動作する。

ソースコードを以下に示す。
[Source Code : data_utils.py]
# Copyright 2016 y-euda. All Rights Reserved.
# The following modifications are added based on tensorflow/models/rnn/translate/data_utils.py.
# - prepare_line_talk_data() is created to load LINE talk data text file.
#
#==============================================================================
# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Utilities for downloading data from WMT, tokenizing, vocabularies."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import gzip
import os
import re

from tensorflow.python.platform import gfile
import tensorflow as tf

# Special vocabulary symbols - we always put them at the start.
_PAD = b"_PAD"
_GO = b"_GO"
_EOS = b"_EOS"
_UNK = b"_UNK"
_START_VOCAB = [_PAD, _GO, _EOS, _UNK]

PAD_ID = 0
GO_ID = 1
EOS_ID = 2
UNK_ID = 3

# Regular expressions used to tokenize.
_WORD_SPLIT = re.compile(b"([.,!?\"':;)(])")
_DIGIT_RE = re.compile(br"\d")

def gunzip_file(gz_path, new_path):
  """Unzips from gz_path into new_path."""
  print("Unpacking %s to %s" % (gz_path, new_path))
  with gzip.open(gz_path, "rb") as gz_file:
    with open(new_path, "wb") as new_file:
      for line in gz_file:
        new_file.write(line)

def basic_tokenizer(sentence):
  """Very basic tokenizer: split the sentence into a list of tokens."""
  words = []
  for space_separated_fragment in sentence.strip().split():
    words.extend(_WORD_SPLIT.split(space_separated_fragment))
  return [w for w in words if w]


def create_vocabulary(vocabulary_path, data_path, max_vocabulary_size,
                      tokenizer=None, normalize_digits=True):
  """Create vocabulary file (if it does not exist yet) from data file.

  Data file is assumed to contain one sentence per line. Each sentence is
  tokenized and digits are normalized (if normalize_digits is set).
  Vocabulary contains the most-frequent tokens up to max_vocabulary_size.
  We write it to vocabulary_path in a one-token-per-line format, so that later
  token in the first line gets id=0, second line gets id=1, and so on.

  Args:
    vocabulary_path: path where the vocabulary will be created.
    data_path: data file that will be used to create vocabulary.
    max_vocabulary_size: limit on the size of the created vocabulary.
    tokenizer: a function to use to tokenize each data sentence;
      if None, basic_tokenizer will be used.
    normalize_digits: Boolean; if true, all digits are replaced by 0s.
  """
  if not gfile.Exists(vocabulary_path):
    print("Creating vocabulary %s from data %s" % (vocabulary_path, data_path))
    vocab = {}
    with gfile.GFile(data_path, mode="rb") as f:
      counter = 0
      for line in f:
        counter += 1
        if counter % 100000 == 0:
          print("  processing line %d" % counter)
        line = tf.compat.as_bytes(line)
        tokens = tokenizer(line) if tokenizer else basic_tokenizer(line)
        for w in tokens:
          word = _DIGIT_RE.sub(b"0", w) if normalize_digits else w
          if word in vocab:
            vocab[word] += 1
          else:
            vocab[word] = 1
      vocab_list = _START_VOCAB + sorted(vocab, key=vocab.get, reverse=True)
      if len(vocab_list) > max_vocabulary_size:
        vocab_list = vocab_list[:max_vocabulary_size]
      with gfile.GFile(vocabulary_path, mode="wb") as vocab_file:
        for w in vocab_list:
          vocab_file.write(w + b"\n")

def initialize_vocabulary(vocabulary_path):
  """Initialize vocabulary from file.

  We assume the vocabulary is stored one-item-per-line, so a file:
    dog
    cat
  will result in a vocabulary {"dog": 0, "cat": 1}, and this function will
  also return the reversed-vocabulary ["dog", "cat"].

  Args:
    vocabulary_path: path to the file containing the vocabulary.

  Returns:
    a pair: the vocabulary (a dictionary mapping string to integers), and
    the reversed vocabulary (a list, which reverses the vocabulary mapping).

  Raises:
    ValueError: if the provided vocabulary_path does not exist.
  """
  if gfile.Exists(vocabulary_path):
    rev_vocab = []
    with gfile.GFile(vocabulary_path, mode="rb") as f:
      rev_vocab.extend(f.readlines())
    rev_vocab = [line.strip() for line in rev_vocab]
    vocab = dict([(x, y) for (y, x) in enumerate(rev_vocab)])
    return vocab, rev_vocab
  else:
    raise ValueError("Vocabulary file %s not found.", vocabulary_path)


def sentence_to_token_ids(sentence, vocabulary,
                          tokenizer=None, normalize_digits=True):
  """Convert a string to list of integers representing token-ids.

  For example, a sentence "I have a dog" may become tokenized into
  ["I", "have", "a", "dog"] and with vocabulary {"I": 1, "have": 2,
  "a": 4, "dog": 7"} this function will return [1, 2, 4, 7].

  Args:
    sentence: the sentence in bytes format to convert to token-ids.
    vocabulary: a dictionary mapping tokens to integers.
    tokenizer: a function to use to tokenize each sentence;
      if None, basic_tokenizer will be used.
    normalize_digits: Boolean; if true, all digits are replaced by 0s.

  Returns:
    a list of integers, the token-ids for the sentence.
  """

  if tokenizer:
    words = tokenizer(sentence)
  else:
    words = basic_tokenizer(sentence)
  if not normalize_digits:
    return [vocabulary.get(w, UNK_ID) for w in words]
  # Normalize digits by 0 before looking words up in the vocabulary.
  return [vocabulary.get(_DIGIT_RE.sub(b"0", w), UNK_ID) for w in words]


def data_to_token_ids(data_path, target_path, vocabulary_path,
                      tokenizer=None, normalize_digits=True):
  """Tokenize data file and turn into token-ids using given vocabulary file.

  This function loads data line-by-line from data_path, calls the above
  sentence_to_token_ids, and saves the result to target_path. See comment
  for sentence_to_token_ids on the details of token-ids format.

  Args:
    data_path: path to the data file in one-sentence-per-line format.
    target_path: path where the file with token-ids will be created.
    vocabulary_path: path to the vocabulary file.
    tokenizer: a function to use to tokenize each sentence;
      if None, basic_tokenizer will be used.
    normalize_digits: Boolean; if true, all digits are replaced by 0s.
  """
  if not gfile.Exists(target_path):
    print("Tokenizing data in %s" % data_path)
    vocab, _ = initialize_vocabulary(vocabulary_path)
    with gfile.GFile(data_path, mode="rb") as data_file:
      with gfile.GFile(target_path, mode="w") as tokens_file:
        counter = 0
        for line in data_file:
          counter += 1
          if counter % 100000 == 0:
            print("  tokenizing line %d" % counter)
          token_ids = sentence_to_token_ids(line, vocab, tokenizer,
                                            normalize_digits)
          tokens_file.write(" ".join([str(tok) for tok in token_ids]) + "\n")

def prepare_line_talk_data(data_dir, in_vocabulary_size, out_vocabulary_size, tokenizer=None):
  """Get line talk data into data_dir, create vocabularies and tokenize data.

  Args:
    data_dir: directory in which the data sets will be stored.
    in_vocabulary_size: size of the Input vocabulary to create and use.
    out_vocabulary_size: size of the Output vocabulary to create and use.
    tokenizer: a function to use to tokenize each data sentence;
      if None, basic_tokenizer will be used.

  Returns:
    A tuple of 6 elements:
      (1) path to the token-ids for Input training data-set,
      (2) path to the token-ids for Output training data-set,
      (3) path to the token-ids for Input development data-set,
      (4) path to the token-ids for Output development data-set,
      (5) path to the Input vocabulary file,
      (6) path to the Output vocabulary file.
  """
  # Get line_talk data to the specified directory.
  train_path = os.path.join(data_dir, "line_talk_train")               
  dev_path = os.path.join(data_dir, "line_talk_dev")                     
  
  # Create vocabularies of the appropriate sizes.
  out_vocab_path = os.path.join(data_dir, "vocab%d.out" % out_vocabulary_size ) 
  in_vocab_path = os.path.join(data_dir, "vocab%d.in"  % in_vocabulary_size ) 
  create_vocabulary(out_vocab_path, train_path + ".out", out_vocabulary_size, tokenizer)
  create_vocabulary(in_vocab_path, train_path + ".in", in_vocabulary_size, tokenizer)

  # Create token ids for the training data.
  out_train_ids_path = train_path + (".ids%d.out" % out_vocabulary_size)
  in_train_ids_path = train_path + (".ids%d.in" % in_vocabulary_size)
  data_to_token_ids(train_path + ".out", out_train_ids_path, out_vocab_path, tokenizer)
  data_to_token_ids(train_path + ".in", in_train_ids_path, in_vocab_path, tokenizer)

  # Create token ids for the development data.
  out_dev_ids_path = dev_path + (".ids%d.out" % out_vocabulary_size)
  in_dev_ids_path = dev_path + (".ids%d.in" % in_vocabulary_size)
  data_to_token_ids(dev_path + ".out", out_dev_ids_path, out_vocab_path, tokenizer)
  data_to_token_ids(dev_path + ".in", in_dev_ids_path, in_vocab_path, tokenizer)

  return (in_train_ids_path, out_train_ids_path,
          in_dev_ids_path, out_dev_ids_path,
          in_vocab_path, out_vocab_path)

[Source Code : chatbot.py]
# Copyright 2016 y-euda. All Rights Reserved.
# The following modifications are added based on tensorflow/models/rnn/translate/translate.py.
# - train() is modified to load LINE talk data text file.
# - decode() is modifed for the input sentence in Japanese.
#
# ==============================================================================
# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Binary for training seq2seq models and decoding from them.

Running this program without --decode will download the line talk corpus into
the directory specified as --data_dir and tokenize it in a very basic way,
and then start training a model saving checkpoints to --train_dir.

Running with --decode starts an interactive loop so you can see how
the current checkpoint translates English sentences into French.

See the following papers for more information on neural translation models.
 * http://arxiv.org/abs/1409.3215
 * http://arxiv.org/abs/1409.0473
 * http://arxiv.org/abs/1412.2007
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import math
import os
import random
import sys
import time
import logging

import numpy as np
import tensorflow as tf


import data_utils as data_utils
import seq2seq_model as seq2seq_model

from janome.tokenizer import Tokenizer

tf.app.flags.DEFINE_float("learning_rate", 0.5, "Learning rate.")
tf.app.flags.DEFINE_float("learning_rate_decay_factor", 0.99,
                          "Learning rate decays by this much.")
tf.app.flags.DEFINE_float("max_gradient_norm", 5.0,
                          "Clip gradients to this norm.")
tf.app.flags.DEFINE_integer("batch_size", 4, #64
                            "Batch size to use during training.")
tf.app.flags.DEFINE_integer("size", 256, "Size of each model layer.") #1024
tf.app.flags.DEFINE_integer("num_layers", 3, "Number of layers in the model.") #3
tf.app.flags.DEFINE_integer("en_vocab_size", 40000, "English vocabulary size.") #40000
tf.app.flags.DEFINE_integer("fr_vocab_size", 40000, "French vocabulary size.") #40000
tf.app.flags.DEFINE_string("data_dir", "line_talk_data", "Data directory")#data
tf.app.flags.DEFINE_string("train_dir", "line_talk_train", "Training directory.")#train
tf.app.flags.DEFINE_integer("max_train_data_size", 0,
                            "Limit on the size of training data (0: no limit).")
tf.app.flags.DEFINE_integer("steps_per_checkpoint", 100,#200
                            "How many training steps to do per checkpoint.")
tf.app.flags.DEFINE_boolean("decode", False,
                            "Set to True for interactive decoding.")
tf.app.flags.DEFINE_boolean("self_test", False,
                            "Run a self-test if this is set to True.")
tf.app.flags.DEFINE_boolean("use_fp16", False,
                            "Train using fp16 instead of fp32.")

FLAGS = tf.app.flags.FLAGS

# We use a number of buckets and pad to the closest one for efficiency.
# See seq2seq_model.Seq2SeqModel for details of how they work.
_buckets = [(5, 10), (10, 15), (20, 25), (40, 50)]


def read_data(source_path, target_path, max_size=None):
  """Read data from source and target files and put into buckets.

  Args:
    source_path: path to the files with token-ids for the source language.
    target_path: path to the file with token-ids for the target language;
      it must be aligned with the source file: n-th line contains the desired
      output for n-th line from the source_path.
    max_size: maximum number of lines to read, all other will be ignored;
      if 0 or None, data files will be read completely (no limit).

  Returns:
    data_set: a list of length len(_buckets); data_set[n] contains a list of
      (source, target) pairs read from the provided data files that fit
      into the n-th bucket, i.e., such that len(source) < _buckets[n][0] and
      len(target) < _buckets[n][1]; source and target are lists of token-ids.
  """
  data_set = [[] for _ in _buckets]
  with tf.gfile.GFile(source_path, mode="r") as source_file:
    with tf.gfile.GFile(target_path, mode="r") as target_file:
      source, target = source_file.readline(), target_file.readline()
      counter = 0
      while source and target and (not max_size or counter < max_size):
        counter += 1
        if counter % 100000 == 0:
          print("  reading data line %d" % counter)
          sys.stdout.flush()
        source_ids = [int(x) for x in source.split()]
        target_ids = [int(x) for x in target.split()]
        target_ids.append(data_utils.EOS_ID)
        for bucket_id, (source_size, target_size) in enumerate(_buckets):
          if len(source_ids) < source_size and len(target_ids) < target_size:
            data_set[bucket_id].append([source_ids, target_ids])
            break
        source, target = source_file.readline(), target_file.readline()
  return data_set


def create_model(session, forward_only):
  """Create chat model and initialize or load parameters in session."""
  dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
  model = seq2seq_model.Seq2SeqModel(
      FLAGS.en_vocab_size,
      FLAGS.fr_vocab_size,
      _buckets,
      FLAGS.size,
      FLAGS.num_layers,
      FLAGS.max_gradient_norm,
      FLAGS.batch_size,
      FLAGS.learning_rate,
      FLAGS.learning_rate_decay_factor,
      forward_only=forward_only,
      dtype=dtype)
  ckpt = tf.train.get_checkpoint_state(FLAGS.train_dir)
  if ckpt and tf.gfile.Exists(ckpt.model_checkpoint_path):
    print("Reading model parameters from %s" % ckpt.model_checkpoint_path)
    model.saver.restore(session, ckpt.model_checkpoint_path)
  else:
    print("Created model with fresh parameters.")
    session.run(tf.initialize_all_variables())
  return model

def train():
  """Train a in->out chat model using LINE talk data."""
  # Prepare line talk data.
  print("Preparing LINE talk data in %s" % FLAGS.data_dir)
  in_train, out_train, in_dev, out_dev, _, _ = data_utils.prepare_line_talk_data(
      FLAGS.data_dir, FLAGS.en_vocab_size, FLAGS.fr_vocab_size)

  with tf.Session() as sess:
    # Create model.
    print("Creating %d layers of %d units." % (FLAGS.num_layers, FLAGS.size))
    model = create_model(sess, False)

    # Read data into buckets and compute their sizes.
    print ("Reading development and training data (limit: %d)."
           % FLAGS.max_train_data_size)
    dev_set = read_data(in_dev, out_dev)
    train_set = read_data(in_train, out_train, FLAGS.max_train_data_size)
    train_bucket_sizes = [len(train_set[b]) for b in xrange(len(_buckets))]
    train_total_size = float(sum(train_bucket_sizes))

    # A bucket scale is a list of increasing numbers from 0 to 1 that we'll use
    # to select a bucket. Length of [scale[i], scale[i+1]] is proportional to
    # the size if i-th training bucket, as used later.
    train_buckets_scale = [sum(train_bucket_sizes[:i + 1]) / train_total_size
                           for i in xrange(len(train_bucket_sizes))]

    # This is the training loop.
    step_time, loss = 0.0, 0.0
    current_step = 0
    previous_losses = []
    while True:
      # Choose a bucket according to data distribution. We pick a random number
      # in [0, 1] and use the corresponding interval in train_buckets_scale.
      random_number_01 = np.random.random_sample()
      bucket_id = min([i for i in xrange(len(train_buckets_scale))
                       if train_buckets_scale[i] > random_number_01])

      # Get a batch and make a step.
      start_time = time.time()
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          train_set, bucket_id)
      _, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
                                   target_weights, bucket_id, False)
      step_time += (time.time() - start_time) / FLAGS.steps_per_checkpoint
      loss += step_loss / FLAGS.steps_per_checkpoint
      current_step += 1

      # Once in a while, we save checkpoint, print statistics, and run evals.
      if current_step % FLAGS.steps_per_checkpoint == 0:
        # Print statistics for the previous epoch.
        perplexity = math.exp(float(loss)) if loss < 300 else float("inf")
        print ("global step %d learning rate %.4f step-time %.2f perplexity "
               "%.2f" % (model.global_step.eval(), model.learning_rate.eval(),
                         step_time, perplexity))
        # Decrease learning rate if no improvement was seen over last 3 times.
        if len(previous_losses) > 2 and loss > max(previous_losses[-3:]):
          sess.run(model.learning_rate_decay_op)
        previous_losses.append(loss)
        # Save checkpoint and zero timer and loss.
        checkpoint_path = os.path.join(FLAGS.train_dir, "chatbot.ckpt")
        model.saver.save(sess, checkpoint_path, global_step=model.global_step)
        step_time, loss = 0.0, 0.0
        # Run evals on development set and print their perplexity.
        for bucket_id in xrange(len(_buckets)):
          if len(dev_set[bucket_id]) == 0:
            print("  eval: empty bucket %d" % (bucket_id))
            continue
          encoder_inputs, decoder_inputs, target_weights = model.get_batch(
              dev_set, bucket_id)
          _, eval_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
                                       target_weights, bucket_id, True)
          eval_ppx = math.exp(float(eval_loss)) if eval_loss < 300 else float(
              "inf")
          print("  eval: bucket %d perplexity %.2f" % (bucket_id, eval_ppx))
        sys.stdout.flush()

#--decode --data_dir line_talk_data --train_dir line_talk_data
def decode():
  with tf.Session() as sess:
    # Create model and load parameters.
    model = create_model(sess, True)
    model.batch_size = 1  # We decode one sentence at a time.

    # Load vocabularies.
    en_vocab_path = os.path.join(FLAGS.data_dir,
                                 "vocab%d.in" % FLAGS.en_vocab_size)
    fr_vocab_path = os.path.join(FLAGS.data_dir,
                                 "vocab%d.out" % FLAGS.fr_vocab_size)
    en_vocab, _ = data_utils.initialize_vocabulary(en_vocab_path)
    _, rev_fr_vocab = data_utils.initialize_vocabulary(fr_vocab_path)

    # Decode from standard input.
    sys.stdout.write("> ")
    sys.stdout.flush()
    sentence = sys.stdin.readline()
    t = Tokenizer()
    tokens = t.tokenize(sentence.decode('utf-8')) 
    sentence = ' '.join([token.surface for token in tokens]).encode('utf-8') 
    print('([Morpho]:'+ sentence +')')

    while sentence:
      # Get token-ids for the input sentence.
      token_ids = data_utils.sentence_to_token_ids(tf.compat.as_bytes(sentence), en_vocab)
      # Which bucket does it belong to?
      bucket_id = len(_buckets) - 1
      for i, bucket in enumerate(_buckets):
        if bucket[0] >= len(token_ids):
          bucket_id = i
          break
      else:
        logging.warning("Sentence truncated: %s", sentence) 

      # Get a 1-element batch to feed the sentence to the model.
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          {bucket_id: [(token_ids, [])]}, bucket_id)
      # Get output logits for the sentence.
      _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs,
                                       target_weights, bucket_id, True)
      # This is a greedy decoder - outputs are just argmaxes of output_logits.
      outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
      # If there is an EOS symbol in outputs, cut them at that point.
      if data_utils.EOS_ID in outputs:
        outputs = outputs[:outputs.index(data_utils.EOS_ID)]
      # Print out French sentence corresponding to outputs.
      print(" ".join([tf.compat.as_str(rev_fr_vocab[output]) for output in outputs]))
      print("> ", end="")
      sys.stdout.flush()
      sentence = sys.stdin.readline()
      t = Tokenizer()
      tokens = t.tokenize(sentence.decode('utf-8')) 
      sentence = ' '.join([token.surface for token in tokens]).encode('utf-8') 
      print('([Morpho]:'+ sentence +')')


def self_test():
  """Test the translation model."""
  with tf.Session() as sess:
    print("Self-test for neural translation model.")
    # Create model with vocabularies of 10, 2 small buckets, 2 layers of 32.
    model = seq2seq_model.Seq2SeqModel(10, 10, [(3, 3), (6, 6)], 32, 2,
                                       5.0, 32, 0.3, 0.99, num_samples=8)
    sess.run(tf.initialize_all_variables())

    # Fake data set for both the (3, 3) and (6, 6) bucket.
    data_set = ([([1, 1], [2, 2]), ([3, 3], [4]), ([5], [6])],
                [([1, 1, 1, 1, 1], [2, 2, 2, 2, 2]), ([3, 3, 3], [5, 6])])
    for _ in xrange(5):  # Train the fake model for 5 steps.
      bucket_id = random.choice([0, 1])
      encoder_inputs, decoder_inputs, target_weights = model.get_batch(
          data_set, bucket_id)
      model.step(sess, encoder_inputs, decoder_inputs, target_weights,
                 bucket_id, False)

def main(_):
  if FLAGS.self_test:
    self_test()
  elif FLAGS.decode:
    decode()
  else:
    train()

if __name__ == "__main__":
  tf.app.run()


話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)


話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

開発のために必要なこと

テキストベースの「対話ボット」の開発にあたって、最近劇的な精度向上によって話題になっているGoogle翻訳のベースの技術として利用されている、Googleが開発したライブラリである「Tensorflow」の”encoder-decoder sequence-to-sequence model”を用いる。Tensorflowの利用にあたっては、C++とPythonのAPIが用意されているが、現時点(2016/11/14)では、Python APIが最も開発が進んでおり、手軽なため、以後Pythonで開発する事を前提とする。

ちなみにTensorflowは、 Apache 2.0 open source license の下で、OSSとして公開されており、本モデル以外にも、チュートリアルと共に様々なDNN応用例が実装されている。

参考1:「Google翻訳が進化!? 精度が向上したと話題に(ディープラーニングによる新翻訳システムが導入されたとみられています。)2016年11月12日 10時23分 更新」
http://nlab.itmedia.co.jp/nl/articles/1611/12/news021.html

Fig. Google翻訳にるArtificial Intelligenceの日本語訳結果

参考2:Tensorflow: Sequence-to-Sequence Models
https://www.tensorflow.org/versions/r0.11/tutorials/seq2seq/index.html#sequence-to-sequence-models

参考2:Tensorflow: Sequence-to-Sequence Models のチュートリアルでは、同モデルを用いて英仏翻訳を行う例が紹介されている。英語の単語のシーケンスを入力とし、フランス語の単語のシーケンスを出力するモデルである。この構造を「対話ボット」に適用する場合、以下の作業が必要となる。

1. 「対話ボット」学習データの整備
2. 「対話ボット」学習ロジックの実装
3. 「対話ボット」対話ロジックの実装

次に各作業の詳細について説明する。

1.「対話ボット」学習データの整備

チュートリアルの英仏翻訳学習用のデータは、対訳データと呼ばれるデータである。これは、英語の文章に対して、翻訳結果となるフランス語の文章が対となった形で、それぞれのファイルに格納され、各行が対をなしているデータである。また、英語もフランス語も各単語が半角スペースで区切られ、単語のシーケンスとなっている。

これに対して、今回対象とする対話ボットは、LINEトーク履歴データ(日本語)を用いた「対話ボット」であるが故に以下が必要となる。

・対話を形成する日本語文対の作成

LINE のトークデータは以下の手順で簡単にテキストデータとしてエクスポート可能である。

 (iPhoneの場合)出力対象のトーク画面→「設定」→「トーク履歴を送信」

エクスポートされたテキストデータは、以下のような形式である。

 ファイル名 :[LINE]トーク相手名.txt

ex1.[LINE]田中太郎.txt

 ファイル内容:HH:MM¥tトーク者¥t発話内容

ex2.
08:40 太郎 おはようー
08:40 太郎 ございます!
08:41 太郎 [スタンプ]
09:00 花子 今日は起きれたんだね。
09:02 太郎 [スタンプ]
 :  :  :

このファイルを用いて入力文と出力文のペアを作成する。

LINEトークの特徴として、一方的に短文を何度も発話する場合が往々にしてあるため、シーケンスの単位として、連続して発話した一連の内容を1単位とした。簡単のため、発話時刻の間隔は考慮せず、連続で発話したすべてをまとめて一つの単位とした。

ex3.
太郎 おはようーございます![スタンプ]
花子 今日は起きれたんだね。
太郎 [スタンプ]
 : :

また、発話の順序性については、本来は発話時刻を考慮してセッションを考え、対話となっているペア生成する必要がある。しかし、今回は上の処理で一連の発話内容を1単位とした後、最初の発話者を入力担当、次の発話者を出力担当と割り振り、順々にペアを生成した。

ex4. 入力部分
おはようーございます![スタンプ]
[スタンプ]
 : 

ex5. 出力部分
今日は起きれたんだね。
 : 

・日本語文の分かち書き

日本語や中国語のような文章中に区切り文字が存在しない文章は、明示的なシーケンスを表現するために、形態素解析を行い、意味のある単位(=形態素)で文を分解する必要がある。

今回はPure Pythonで開発された形態素解析エンジンであるJanome(蛇の目)を用いる。

Janome (0.2.8)
http://mocobeta.github.io/janome/

以下にサンプルコードを示す。

from janome.tokenizer import Tokenizer
    with open('../data/line_talk.in', mode = 'w') as fw:
        t = Tokenizer()
        for line in inputs:
            tokens = t.tokenize(line) 
            line = ' '.join([token.surface for token in tokens]).encode('utf-8') + '\n'            
            fw.write(line)

分かち書きを行った入力部分、出力部分をそれぞれファイルに格納する。

ex6. line_talk.in
おはようーございます ! [スタンプ]
[スタンプ]
 : 

ex7. line_talk.out
今日 は 起き れ た ん だ ね 。
 : 

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

どのような「対話ボット」を作るか?

「いわゆるAI」がみたすべき重要な要件としては、知性や人格を感じることにある。画像認識の分野における一般物体認識の圧倒的な技術革新は、とても役に立つ高性能な機械という印象を脱しえない。今回は、「何の役にも立たなくても良いが、人格を感じるにはどうするか?」にフォーカスしてみようと思う。

そのように考えていった時に、現時点で最も簡単にコンピュータが自然に人格を表現する方法は文字列によるコミュニケーションであろう。対話の仕方はいくつかあるが、対話時の表情や発された言葉の音声などの複合的な情報を用いた対話の方がより人間らしさを感じることは言うまでもないが、人工知能で再現することはそのメディアの分だけコストがかかる。

よって今回は、最もシンプルに内容だけで人格を感じさせられる可能性がある対話ボットであるテキストによる対話ボットを考える。つまるところ、これは以下のチューリングテストで想定している「機械」そのものである。

人間の判定者が、一人の(別の)人間と一機の機械に対して通常の言語での会話を行う。このとき人間も機械も人間らしく見えるように対応するのである。これらの参加者はそれぞれ隔離されている。判定者は、機械の言葉を音声に変換する能力に左右されることなく、その知性を判定するために、会話はたとえばキーボードとディスプレイのみといった、文字のみでの交信に制限しておく。判定者が、機械と人間との確実な区別ができなかった場合、この機械はテストに合格したことになる。


(出典:Wikipedia https://ja.m.wikipedia.org/wiki/チューリング・テスト )

また、対象とする対話は、人格を感じさせられることを狙っているため、複数人の大量の対話履歴データを用いて学習させるよりも、ある特定の個人の対話履歴を用いて学習させることが望ましい、と考えた。

そこで、最も頻繁に利用しているコミュニケーションツールであるLINEの履歴データの活用を考えた。私のLINEトーク履歴データを用いて「対話ボット」を学習させ、LINE上で行なわれているコミュニケーションを再現できれば、私という人格をある程度再現できたことに相当する。

今回のやるべきことを整理すると「LINEのトーク履歴データを学習し、ある個人の人格を感じさせられるテキストベースの『対話ボット』を開発すること」である。

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(1)

「人工知能といえば」



最近のAIの隆盛はすさまじい。アカデミアにおける研究活動は勿論のこと、政府、企業におけるAI関連組織の編成や、AI関連予算のニュースも数多く取り沙汰されている。

一応、機械学習をかじったことがある身としては、AIと持て囃されている技術の大部分が、データから何かを統計的に学習するという点で従来の機械学習となんら変わりはなく見えてしまい、真新しさはほとんど感じない。

唯一の大きな違いとしては、計算機パワーの増大や、多種多様な学習データの増加によって、ディープラーニングという技術が、画像認識などの特定領域において従来技術に圧勝し、脚光を浴びたことであり、これこそがブームの正体だと思っていた。

しかしながら、先日友人と何気なく人工知能の会話になった際に「AIといったら、SiriやWatsonみたいなやつでは?」と言われてしまった。

文理を問わず、機械学習や最適化について、これまで学んだことの無い多くの人からしてみれば、AIの指し示す領域は、やはり「知性や人格をそこに感じる存在」なのである。SF映画の影響を色濃く受けていると考えられるが、人間の代替であり、延いては感情を兼ね備えて友人や敵になりうる存在としても想起されるようだ。そのAIの中で利用されている要素技術が、単なる統計だろうと、機械学習だろうと、ディープラーニングだろうと全く関係ないのである。

このような世間が考える「いわゆるAI」と「技術領域としてのAI」に乖離を感じたため、今回は「いわゆるAI」に主眼をおいて機械学習を活用してみようと思うに至った。

そこで、題材として選んだのが「対話ボット」である。

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(2)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(3)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(4)

話題のTensorFlow・LINEトーク履歴を用いて対話ボットを作ってみた(5)


2016年10月11日火曜日

代表的な共役事前分布について

本稿では、代表的な共役事前分布の一覧を示す。

まずはじめに事後分布、尤度、事前分布の関係は以下の通りである。

事後分布 $\propto$ 尤度 $\times$ 事前分布

共役事前分布とは、事後分布の関数形と事前分布の関数形が同じになるような、尤度に対する事前分布のことである。

<参照>ベイズ推定における共役事前分布の重要性について

以下に、代表的な共役事前分布を示す。

ベータ分布

尤度関数が二項分布、ベルヌーイ分布の場合、ベータ分布が共役事前分布となる。

尤度関数:ベルヌーイ分布、二項分布


ベルヌーイ分布

$P(x\mid \lambda) = \lambda^x(1-\lambda)^{(1-x)}$ for $ x \in \{0,1\} $
Takes a single parameter $\lambda \in [0,1] $

二項分布

$P(x\mid \lambda, n) = {}_n \mathrm{ C }_x \lambda^x(1-\lambda)^{(n-x)}$
${}_n \mathrm{ C }_x = \frac{ n! }{ x! ( n - x )! }$
Takes parameters $\lambda \in [0,1]$ and $n \geq 0, n \in \mathbb{ Z }$

共役事前分布:ベータ分布

$$Beta(\lambda|a,b) = \frac{\Gamma(a+b)}{\Gamma(a)\Gamma(b)}\lambda^{a-1}(1-\lambda)^{b-1} $$

ディリクレ分布


尤度関数がカテゴリカル分布あるいは多項分布の場合、ディリクレ分布が共役事前分布となる。

尤度関数:カテゴリカル分布、多項分布


カテゴリカル分布

$P(x\mid \boldsymbol{\lambda}) = \displaystyle \prod_{ i = 0 }^K \lambda_i^{x_i} $
Takes $K$parameters $\lambda_i \in [0,1] $ where $\displaystyle \sum_{ i = 1 }^{ K } \lambda_i = 1$

多項分布

$$
P(x\mid \boldsymbol{\lambda}, n) =
\begin{cases}
\frac{ n! }{ x_1! x_2! \cdots x_k! } \displaystyle \prod_{ i = 1 }^K \lambda_i^{x_i} & (when \sum_{ i = 1 }^{ K } x_i = n ) \\
0 & ( otherwise )
\end{cases}
$$
Takes $K+1$ parameters $n$ where  $n \gt 0$, and $\lambda_1,...,\lambda_K$ where $\lambda_i \in [0,1] $ and $\displaystyle \sum_{ i = 1 }^{ K } \lambda_i = 1$

共役事前分布:ディリクレ分布

$$Dir(\boldsymbol{\lambda}|\boldsymbol{a})=\frac{\Gamma(\sum_{ i = 1 }^{ K } a_i)}{\prod_{ i = 1 }^{ K }\Gamma(a_i)}\prod_{ i = 1 }^{ K } \lambda_i^{a_i-1}$$
Takes $K$parameters $a_1,...,a_K$ where $a_i gt 0$
Takes $K$supports $\lambda_1,...,\lambda_K$ where $\lambda_i \in [0,1] $ and $\displaystyle \sum_{ i = 1 }^{ K } \lambda_i = 1$

その他

その他、有名な尤度関数と共役事前分布の関係としては以下のようなものがある。

【事後分布】正規分布, 【尤度関数】正規分布 ,【共役事前分布】正規分布
【事後分布】ガンマ分布, 【尤度関数】ポアソン分布 ,【共役事前分布】ガンマ分布
【事後分布】ガンマ分布, 【尤度関数】正規分布 ,【共役事前分布】ガンマ分布


2016年1月24日日曜日

今更聞けないリプレゼンター定理の解説


定義

リプレゼンター定理(Representer theorem)とは、
「損失関数が$\boldsymbol{\omega}^{ \mathrm{ T } }\boldsymbol{\phi}(\boldsymbol{x}_i)$(パラメータ$\boldsymbol{\omega}$と特徴ベクトルの積)の関数として表現できるとする。この損失関数に正則化項を加えて最適化する問題において、その正則化項が$\lambda\boldsymbol{\omega}^{ \mathrm{ T } }\boldsymbol{\omega}$という形をしていれば、その最適解$\hat{\boldsymbol{\omega}}$は$\boldsymbol{\phi}(\boldsymbol{x}_i)$で張られる空間に存在する」
というものである。

この定義だけでは理解し難いので、具体例を記しておく。

例えば、以下の(1)のような二乗誤差関数の最小化問題を考えた場合に、重みの最適解$\hat{\boldsymbol{\omega}}$は(2)の形で表せることを意味する。
$$
\begin{eqnarray}
\hat{\boldsymbol{\omega}} &=& arg\min_\boldsymbol{\omega}\displaystyle \sum_{i} (y_i - \boldsymbol{\omega}^{ \mathrm{ T } }\boldsymbol{\phi}(\boldsymbol{x}_i))^2+\lambda\boldsymbol{\omega}^{ \mathrm{ T } }\boldsymbol{\omega}\\

\hat{\boldsymbol{\omega}} &=& \sum_{i}\alpha_i\boldsymbol{\phi}(\boldsymbol{x}_i)
\end{eqnarray}
$$

証明

では、なぜ重みの最適解は(2)の形で表現できるのであろうか。
最適解が$\boldsymbol{\phi}(\boldsymbol{x}_i)$で張られる空間に存在しない場合、つまり重み$\boldsymbol{\omega}$が、以下の(3)(4)の形で表現できた場合を考える。
$$
\begin{eqnarray}
\boldsymbol{\omega} &=& \boldsymbol{\omega_0}+\boldsymbol{\xi}\\
\boldsymbol{\omega_0} &=& \sum_{i}\alpha_i\boldsymbol{\phi}(\boldsymbol{x}_i)
\end{eqnarray}
$$
ただし、$\boldsymbol{\xi}$はすべての$\boldsymbol{\phi}(\boldsymbol{x}_i)$に直交する。

この時、最適化問題は以下の(5)式のようになる。
損失関数の部分については、$\boldsymbol{\xi}$が$\boldsymbol{\phi}(\boldsymbol{x}_i)$に直交するため、影響を与えない。対して、正則化項の部分は$\|\boldsymbol{\xi}\|^2 \geq 0$が残る。よって、(5)式は(6)式と同値である。
$$
\small{
\begin{eqnarray}
\displaystyle \sum_{i} (y_i -  (\boldsymbol{\omega_0}+\boldsymbol{\xi})^{ \mathrm{ T } }\boldsymbol{\phi}(\boldsymbol{x}_i))^2+\lambda(\boldsymbol{\omega_0}+\boldsymbol{\xi})^{ \mathrm{ T } }(\boldsymbol{\omega_0}+\boldsymbol{\xi})\\
\Leftrightarrow\displaystyle \sum_{i} (y_i -  \boldsymbol{\omega_0}^{ \mathrm{ T } }\boldsymbol{\phi}(\boldsymbol{x}_i))^2+\lambda(\|\boldsymbol{\omega_0}\|^2+\|\boldsymbol{\xi}\|^2)
\end{eqnarray}
}
$$
さて、最小化を考えた場合、正則化項の$\|\boldsymbol{\xi}\|^2$の増加分を最小にするためには、$\boldsymbol{\xi}$が$\boldsymbol{0}$となる。よって、重みの最適解$\hat{\boldsymbol{\omega}}$は以下の式(7),式(8)で表され、式(2)で表せることが証明できた。

$$
\begin{eqnarray}
\hat{\boldsymbol{\omega}} &=& \boldsymbol{\omega_0}\\
&=& \sum_{i}\alpha_i\boldsymbol{\phi}(\boldsymbol{x}_i)
\end{eqnarray}
$$

何が嬉しいか

最適解が式(2)で表現できて何が嬉しいのだろうか。
$\boldsymbol{\alpha}$の次元はサンプリングされたデータ数$N$と一致する。
また、$\boldsymbol{\omega}$は特徴ベクトルの次元数$d$と一致する。

非線形な分類を可能にするため、データ$x_i$は多くの場合、高次元の特徴ベクトル空間$\boldsymbol{\phi}(\boldsymbol{x}_i)$に写像される(無限次元の特徴ベクトル空間への写像の場合もある)。

最適化を考えた場合、変数は小さい方が簡単である。
$\boldsymbol{\omega}$を直接最適化することももちろん理論的には可能であるが、超高次元な特徴ベクトル空間の場合には、そのパラメータの最適化は現実的ではない。そこで、リプレゼンター定理を用いて、データ数の数だけの変数を持つパラメータ$\boldsymbol{\alpha}$を最適化することで、計算量を削減すること(現実的に解くこと)が可能となる。

使い方

以上のことを踏まえて、ケースに最適化するパラメータを変化させると良いだろう。

$d \gg N$の場合:サンプリングデータの線形結合係数 $\alpha$(次元数$d$)(リプレゼンター定理の利用)
$d \ll N$の場合:元々の重み係数$\omega$(次元数$N$)

以上

2015年7月7日火曜日

今更聞けないEMアルゴリズムの解説

今更ながらEMアルゴリズムとは何かについて、調査・勉強したのでまとめておく。Andrew氏のレクチャノートとパターン認識と機械学習下巻第9章を参考にした。

EMアルゴリズムの目的

観測変数$\boldsymbol{x}$と観測できない潜在変数$\boldsymbol{z}$を含む確率モデルの尤度関数$P( \boldsymbol{x} \mid \boldsymbol{\theta})$ を最大化するパラメータ$\boldsymbol{\theta}$ を見つけることである。

比較的複雑な観測変数の(周辺)分布$P( \boldsymbol{x})$を、より扱いやすい観測変数と潜在変数の同時分布$P( \boldsymbol{x}, \boldsymbol{z})$によって表すことができることがある(例えば混合正規分布などがそれに相当する)。このように、モデルを簡単に扱うために潜在変数$\boldsymbol{z}$を導入し、EMアルゴリズムの効果的な適用を図るケースがしばしばある。

EMアルゴリズムの解説

E-Stepの説明

E-Stepでは、$\boldsymbol{\theta}$固定の下、尤度関数の下界の分布を最大化する。以下で尤度関数を変形し、下界を求める手順を示す。

$$
\begin{eqnarray}
\displaystyle \sum_{ i = 1 }^{ N } \ln P( \boldsymbol{x}_i \mid \boldsymbol{\theta}) &=& \displaystyle \sum_{ i = 1 }^{ N } \ln \displaystyle \sum_{\boldsymbol{z}_i} P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})\\
&=& \displaystyle \sum_{ i = 1 }^{ N } \ln \displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i)\frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\\
&\geq&  \displaystyle \sum_{ i = 1 }^{ N }\displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}
\end{eqnarray}
$$

(1)式から(2)式への変形は、$\boldsymbol{z}_i$の任意の確率分布$Q(\boldsymbol{z}_i) $でかけて割っただけである。
(2)式から(3)式への変形は、Jensen's Inequalityを利用した。以下の(4)式を参照されたい。$\ln$が凹関数なので、期待値を関数に入れた値のほうが、関数に入れた値の期待値より大きい。今回の場合、$\frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}$を値と捉え、$Q(\boldsymbol{z}_i) $によって期待値を算出することを考える。

$$
\begin{eqnarray}
 \ln E_{\boldsymbol{z}_i 〜 Q(\boldsymbol{z}_i) } \left[ \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\right]
&\geq& E_{\boldsymbol{z}_i 〜 Q(\boldsymbol{z}_i) } \ln \left[ \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\right]
\end{eqnarray}
$$

(2)式から(3)式の変形は、$Q(\boldsymbol{z}_i)$がどのような確率分布であっても成立する。
尤度関数最大化の目的を考えると、$\boldsymbol{\theta}$固定の下、下界(3)式を最大化、つまり(3)の等号の成立を図るのが自然であろう。
Jensen's Inequalityの等号の成立条件は、凸関数が線形関数の場合か、狭義凸関数(線形ではない)の場合は、凸関数の中身が1点分布となる場合である。つまり以下を満たす。

$$
\begin{eqnarray}
\displaystyle \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)} &=& const
\end{eqnarray}
$$

また、$\sum_{\boldsymbol{z}_i }Q(\boldsymbol{z}_i) =1$より、以下が成り立つ。

$$
\begin{eqnarray}
Q(\boldsymbol{z}_i)  &=& \displaystyle \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{\sum_{\boldsymbol{z}_i }P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}\\
&=&  \displaystyle \frac{P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})P( \boldsymbol{x}_i \mid \boldsymbol{\theta})}{P( \boldsymbol{x}_i \mid \boldsymbol{\theta})}\\
&=& P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})
\end{eqnarray}
$$

すべての$i$について(8)式を実施することが、E-Stepにて行うことである。

(補足)E-Stepのその他の説明

(3)式の下界の各々$i$について、以下のように式変形する。
$$
\begin{eqnarray}
\ln P( \boldsymbol{x}_i \mid \boldsymbol{\theta})
&\geq& \displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\\
&=& \displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})P( \boldsymbol{x}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\\
&=&  \ln P( \boldsymbol{x}_i \mid \boldsymbol{\theta}) + \displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\\
&=& \displaystyle \ln P( \boldsymbol{x}_i \mid \boldsymbol{\theta}) - KL(Q(\boldsymbol{z}_i)||P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta}))
\end{eqnarray}
$$

(10)式から(11)式への変形は、$\boldsymbol{z}_i $に依存しない$P( \boldsymbol{x}_i \mid \boldsymbol{\theta})$を前に出し、$Q(\boldsymbol{z}_i)$の$\boldsymbol{z}_i$に関する積分が1になることによる。

さて、下界である右辺の最大化を考えた場合、(12)式第2項$Kullback–Leibler$ divergenceの最小化が必要となる。$KL$ divergence $\geq 0$より、  $KL(Q(\boldsymbol{z}_i)||P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})) = 0$となる$Q(\boldsymbol{z}_i)$を求めることと同義となる。よって、$Q(\boldsymbol{z}_i) = P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta})$を求めることとなり、これは(8)式と一致する。

また、$\boldsymbol{z}_i$が連続変数の場合のE-Stepについては、以下に変分法を用いた説明を加えたので、参照していただきたい。
今更聞けないEMアルゴリズムの解説〜潜在変数が連続変数の場合のEステップの説明〜

M-Stepの説明

今度は、$Q(\boldsymbol{z}_i)$を固定し、$\boldsymbol{\theta}$を動かし、尤度関数の下界分布の最大化を図る。

$$
\begin{eqnarray}
\hat{\theta}&=&\mathop{\arg\,\max}\limits_\boldsymbol{\theta}
\displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i) \ln \frac{P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})}{Q(\boldsymbol{z}_i)}\\
&=& \mathop{\arg\,\max}\limits_\boldsymbol{\theta}
\displaystyle \sum_{\boldsymbol{z}_i} Q(\boldsymbol{z}_i) \ln P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})\\
&=& \mathop{\arg\,\max}\limits_\boldsymbol{\theta}
\displaystyle \sum_{\boldsymbol{z}_i} P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta}_{old}) \ln P( \boldsymbol{x}_i, \boldsymbol{z}_i \mid \boldsymbol{\theta})
\end{eqnarray}
$$

(13)式から(14)式の変形は$\boldsymbol{\theta}$によって影響のない部分を排除した。
(15)式はE-Stepで求めた、$Q(\boldsymbol{z}_i) = P( \boldsymbol{z}_i \mid \boldsymbol{x}_i, \boldsymbol{\theta}_{old}) $を代入した。

このように、E-Stepにて潜在変数の事後確率を求め、その事後確率によってM-Stepにて期待値計算を行い、目的のパラメータを更新する。このプロセスを収束するまで繰り返す。イメージを下図に示す。

パターン認識と機械学習 下 (ベイズ理論による統計的予測)  第9章 図9.14


以上、EMアルゴリズムの全貌である。複雑な分布を簡単な分布の構成として扱うことで、簡単に尤度関数の最適化ができるようになった。EMアルゴリズム適用時のポイントは、何を潜在変数の分布として、何を観測変数として扱うのかというモデル化であろう。卑近な混合正規分布への適用例をしっかりと学び、その類推で応用していくのが簡単に思える。

EMアルゴリズムを拡張した変分推論については、また今度。

参考文献
パターン認識と機械学習 下 (ベイズ理論による統計的予測)  第9章, C.M. ビショップ (著), 元田 浩, 栗田 多喜夫, 樋口 知之, 松本 裕治, 村田 昇 (監訳)

2015年7月1日水曜日

ベイズ推定における共役事前分布の重要性について

ベイズ推定における事後確率計算量


$$P(x^* \mid \boldsymbol{x}) = \displaystyle \int P(x^* \mid \boldsymbol{\lambda})  P(\boldsymbol{\lambda} \mid \boldsymbol{x}) d \boldsymbol{\lambda}$$

ベイズ推定の際は、予測をする場合に事後確率によって重み付けをとるため、全てのパラメーターに対する事後確率を覚えておくか、解析的に計算できるようにしておく必要がある。


現実的には、全てのパラメータの事後確率を覚えておくことは不可能なので、解析的に計算しておくか、近似的に計算することになる。

そこで、共役事前分布の登場である。

共役事前分布を用いれば, 事後分布が閉じた形で計算できるため、計算が簡単になる。具体的には、事後分布を求める際に、尤度と事前分布の積が、ある確率分布*定数$\kappa$だとわかる場合、evidence(分母) と定数$\kappa$が同じにならねばならない。なぜなら、左辺の事後確率分布はあらゆる点において0以上1以下で全区間積分すると1になる正しい確率分布であるので、右辺も同様に正しい確率分布荷なる必要がある。ある確率分布(パラメータ未定)が出現しているため、その前の定数はevidence(分母)とキャンセルされる必要があるのである。

$$
\begin{eqnarray}
P(\boldsymbol{\lambda} \mid \boldsymbol{x}) &=& \frac{ \prod_{ i = 1 }^N P(x_i \mid \boldsymbol{\lambda})P(\boldsymbol{\lambda})}{ P(\boldsymbol{x})} \\
&=& \frac{ \prod_{ i = 1 }^N Cat_{x_i}( \boldsymbol{\lambda})Dir_{\boldsymbol{\lambda}}(\boldsymbol{\alpha})}{ P(\boldsymbol{x})} \\
&=& \frac{ \kappa (\boldsymbol{x}, \boldsymbol{\alpha}) Dir_{\boldsymbol{\lambda}}(\boldsymbol{\tilde{\alpha}})}{ P(\boldsymbol{x})}
\end{eqnarray}
$$

上の例では、多項分布($Cat(x)$についてはココ(基本的な確率分布のまとめ)を参照。)とその共役事前分布であるディリクレ分布の掛け合わせによる事後分布の導出を示している。このとき$\kappa$と$P(\boldsymbol{x})$はキャンセルする必要があり、結果的にディリクレ分布のパラメータ$\boldsymbol{\tilde{\alpha}}$が決まれば事後分布がわかるわけである。

その他、パラメーターを介した周辺化の積分計算(予測*事後確率)を行う際に、確率分布が出現し、積分の中の計算が1になる。よって、定数部分の演算だけで観測点からの予測が可能となる点で、共役事前分布は強力である。

$$
\begin{eqnarray}

P(x^* \mid \boldsymbol{x}) &=& \displaystyle \int P(x^* \mid \boldsymbol{\lambda})  P(\boldsymbol{\lambda} \mid \boldsymbol{x}) d \boldsymbol{\lambda}\\
&=& \int Cat_{x^*}( \boldsymbol{\lambda})Dir_{\boldsymbol{\lambda}}(\boldsymbol{\tilde{\alpha}}) d \boldsymbol{\lambda}\\
&=& \int \kappa (x^*,  \boldsymbol{\tilde{\alpha}}) Dir_{\boldsymbol{\lambda}}(\boldsymbol{\breve{\alpha}}) d \boldsymbol{\lambda}\\
&=& \kappa (x^*,  \boldsymbol{\tilde{\alpha}})
\end{eqnarray}
$$

上の例では、各パラメータ$ \boldsymbol{\lambda}$の下の$x^*$の確率分布が多項分布、事後確率分布がディリクレ分布の場合の予測時の導出を示している。定数部分$\kappa$だけ積分の前にもっていくことができ、ディリクレ分布は積分すると1になるので、結果、定数部分$\kappa$が残るわけである。

以上、ベイズ推定における共役事前分布の重要性について述べた。
(代表的な共役事前分布の例はこちら

しかし、MCMCと呼ばれるサンプリング技法が成熟した経緯もあり、共役でない自由な事前分布を用いたとしても近似的に事後分布を求めることで、ベイズ推定可能となっている。詳しくは後日投稿する。

2015年6月28日日曜日

今更聞けない基本的な確率分布のまとめ

忘れがちな以下の4つの確率分布についてまとめておく。

ベルヌーイ分布 (Bernoulli distribution)

確率 $\lambda$で 1 を、確率 $1-\lambda$ で 0 をとる、離散確率分布である。

$P(x\mid \lambda) = \lambda^x(1-\lambda)^{(1-x)}$ for $ x \in \{0,1\} $

Takes a single parameter $\lambda \in [0,1] $

カテゴリカル分布 (Categorical distribution

ベルヌーイ分布を一般化した確率分布で、二値ではなく、$K$値の場合をとる離散確率分布である。
※ベルヌーイ分布はカテゴリカル分布のカテゴリ数が2の場合ともいえる。
※ どういうわけか、日本語Wikipediaにはカテゴリカル分布の記事は存在しないため、多項分布と混乱されやすい。

$$P(x\mid \boldsymbol{\lambda}) = \displaystyle \prod_{ i = 0 }^K \lambda_i^{x_i} $$

Takes $K$parameters $\lambda_i \in [0,1] $ where $\displaystyle \sum_{ i = 1 }^{ K } \lambda_i = 1$

二項分布 (Binomial distribution)

n 個の独立なベルヌーイ試行の「成功」の数の確率分布であり、各試行の「成功」確率$\lambda$は同じである。
※ベルヌーイ分布は二項分布における試行回数が1回の場合ともいえる。

$$
P(x\mid \lambda, n) = {}_n \mathrm{ C }_x \lambda^x(1-\lambda)^{(n-x)}
$$
$${}_n \mathrm{ C }_x = \frac{ n! }{ x! ( n - x )! }$$

多項分布 (Multinomial distribution)

二項分布を一般化した確率分布である。多項分布では、各試行の結果は固定の有限個($K$個)の値をとる。
※カテゴリカル分布は多項分布の試行回数が1回の場合ともいえる。
※二項分布は多項分布のK=2の場合である。

$$
P(x\mid \boldsymbol{\lambda}, n) =
  \begin{cases}
     \frac{ n! }{ x_1! x_2! \cdots x_k! } \displaystyle \prod_{ i = 1 }^K \lambda_i^{x_i}   & (when \sum_{ i = 1 }^{ K } x_i = n ) \\
    0 & ( otherwise )
  \end{cases}
$$

多項分布の例

SASブログより、100足の靴下を取り出す場合($n=100$)、何色の靴下を何回抽出するかという分布例を以下に示す。靴下の色は(黒、茶、白)の三種類であり($K=3$)。それぞれ確率は$\lambda_{black}=0.5,\lambda_{brown}=0.2,\lambda_{white}=0.3$である。


4つの分布の関係性

最後に、以上4つの分布の関係性をまとめると、以下の図になる。カテゴリ数と試行回数によって、最も一般化されたのが多項分布である。このように関連づけて4つの分布を覚えておけば忘れない、、に違いない。

$$
\require{AMScd}
\begin{CD}
Bernoulli(K=2,n=1) @>{K>2}>> Categorical(K>2,n=1)\\
@V{n>1}VV {} @VV{n>1}V\\
Binomial(K=2,n>1)  @>>{K>2}> Multinomial(K>2,n>1)
\end{CD}
$$

$$
\diamondsuit K:カテゴリ数\\
\diamondsuit n:試行回数
$$