共分散のある多変量ベルヌーイ分布の考察

先の記事で導出した3次元の多変量ベルヌーイ分布の同時確率の形を再掲すると、次のようなものである(記号の説明は、前の記事を参照のこと)。

v_{1}=q_{1}q_{2}q_{3}+q_{3}\sigma_{12}+q_{2}\sigma_{13}+q_{1}\sigma_{23}-\sigma_{123}
v_{2}=q_{1}q_{2}p_{3}+p_{3}\sigma_{12}-q_{2}\sigma_{13}-q_{1}\sigma_{23}+\sigma_{123}
v_{3}=q_{1}p_{2}q_{3}-q_{3}\sigma_{12}+p_{2}\sigma_{13}-q_{1}\sigma_{23}+\sigma_{123}
v_{4}=q_{1}p_{2}p_{3}-p_{3}\sigma_{12}-p_{2}\sigma_{13}+q_{1}\sigma_{23}-\sigma_{123}
v_{5}=p_{1}q_{2}q_{3}-q_{3}\sigma_{12}-q_{2}\sigma_{13}+p_{1}\sigma_{23}+\sigma_{123}
v_{6}=p_{1}q_{2}p_{3}-p_{3}\sigma_{12}+q_{2}\sigma_{13}-p_{1}\sigma_{23}-\sigma_{123}
v_{7}=p_{1}p_{2}q_{3}+q_{3}\sigma_{12}-p_{2}\sigma_{13}-p_{1}\sigma_{23}-\sigma_{123}
v_{8}=p_{1}p_{2}p_{3}+p_{3}\sigma_{12}+p_{2}\sigma_{13}+p_{1}\sigma_{23}+\sigma_{123}

これを一般ベイズ分類器(Naiveではない)の事前確率として利用し、MNISTデータの読み取りをテストすることが次の課題だ。ただ、昨夜布団に入って、ここでの\sigma_{ij}\sigma_{123}は、事前学習で標本値を作ることができるが、各係数の前についている符号はどのように決定するのかが気になった。そのまま寝てしまったが、今朝、考えたので、それをメモしておく。(図の\thetaは、\sigma_{123}のことである)


基本は、正の相関のあるピクセル同士が同じ値を持つ確率は高まる、負の相関の場合は低くなるということである。

そうなると\sigma_{ij}の係数にかかる符号は、次のようにして決まる。まず、それぞれの式の第1の項目は、三つのピクセルの状態と、それらが独立の場合の同時確率を示している。例えば最初の式の場合はq_{1}q_{2}q_{3}で、全てがqということは、すべのピクセルの値が負である状態と言うことである。次に、第二の項は、q_{3}\sigma_{12}で、その符号は正である。この符号が正である理由は、相関の問題になっている第1ピクセルと第2ピクセルが、ともに0で、同じ方向に動いている、連動しているから、正なのである。その第1式は、全てが連動しているので、すべて正なのである。

第2式を見てみよう。その第1項は、q_{1}q_{2}p_{3}である。ピクセルの状態は、第1,2ピクセルガ0で、第3ピクセルが1となっていることがわかる。第2項は、+p_{3}\sigma_{12}であるが、この場合も、第1,2ピクセルは連動しているので正である。第3項は-q_{2}\sigma_{13}で、符号は負だが、これは、第1ピクセルが0で、第3ピクセルは1で、方向が逆になっているので、負なのである。

ちなみに、\sigma_{ij}の前についているのが、p_{i}であるかq_{j}であるかは、第1項の組み合わせの中に入っているものがそのまま踏襲されているだけである。

また\sigma_{123}は、三つのピクセル値の符号によって決まる。すなわち、その式の第1項のqが含まれていれば(-1)をかけ、pが含まれているならば(1)をかけ、その三つの値の掛け算の符号で決まるのである。つまり、第2式のような場合は、(-1)(-1)(1) だから、正である。

すなわち、第1項が決まると、式の形は全て決まってしまうと言うことになる。そうすると、3次多変量ベルヌーイ分布の同時確率の一般形もすぐにかけそうだ。それができると、n次多変量ベルヌーイ分布の一般形もかけることになる。ただ、頭の痛くなるほど複雑になるので、今は、書くのをやめておく。

 

多変量ベルヌー分布の変数間の相関を考慮した同時確率分布を考える

タイトルがやたら難しい!おそらく、3、4日前の自分がこのタイトルを見たら、記事の中身を決して読まなかっただろうと思う(笑)

はじめに:それはベルヌーイ分布だった

最近、何か色々考えたりプログラムしていて、自分のやろうとしていることを、後で、その理論的枠組みを知ることが多くなった。もともと、中野馨氏のアソシアトロン をやっていて、そこでベイズ定理を用いて概念分類をしているのが面白くて、やってみたら、後で、それが「単純ベイズ分類器 Naive Bayes Classifier」というものであることを知って、色々な知識が増えた。それをやったら、今度は、「単純」ではダメだ。ピクセルの変数間の相互関係を考慮しないとMNISTの正解率を84%以上には上げれないと思って、どうしたらいいんだと色々調べたら、そのために必要なことが、実は多変量ベルヌーイ分布の相関を考慮したモデルの問題なのだということを知った。

そもそも、MNISTデータのテスト正解率をあげるのに、なぜ相関を考慮する必要があるかをまず書いておこう。単純ベイズ分類器の場合、一つのピクセルの事前分布を調べるが、その変数の分布は独立であると想定する。MNSITデータは、一つの手書き数字に、28X28=748ピクセルを用いている。Naiveの場合、その第iピクセルが0であるか1であるか(MNISTは、0から255までの数値になっているが、経験的に、これを0,1に変換しても結果はそれほど変わらない)になっているが、その0か1かの値をとる変数x_{i}は他のピクセルの変数とは独立に与えられる確率変数であると想定するのである。しかし、実際は、数字が形をなすということは、ピクセルのどこかの値が1になることと、別のピクセルの値が1になることが、連動していることを意味している。連動しているからこそ、数字として人間が認識できるのである。だから、相関を考慮することはどうしても必要なのである。

また、ピクセルが独立しているなどという想定は、脳の機構とは全く違ったものになる。ニューロンが他のニューロンとは独立に発火するというのは、脳的ではないし、面白くない。相関を考慮するということは、ニューロン同士の関係性をある程度シミュレートすることにもつながる。

というわけで、ここにたどり着いたのである。この「多変量ベルヌー分布の変数間の相関を考慮した同時確率分布の導出」という問題は、一つのピクセルの値をベルヌーイの二値の分布ではなく、正規分布と仮定すること、さらには、多くのピクセルの変数を相関のある多変量正規分布と仮定することよりも、問題が複雑なのではないかと思う。

そもそも、1,0の二値の分布が独立でない場合の同時分布とはなんだろうか、そこが全く掴めなかった。ネットでも、わかりやすく解説しているところがなかなか見つからなかった。しかし、自分の理解した範囲で、以下で解説してみよう。

2変数のベルヌーイ分布:共分散がある場合

MNISTは784ピクセルで1文字を表す。が、ここで目一杯単純化して、あるパターン(MNISTの場合は文字)が2ピクセルで表されたとしよう。一つのピクセルが0か1の値を持つとする。このパターンのもとで、二つのピクセルがどのような値を持つかを考えるのである。最大可能状態なパターンは4つしかなく、それをs_{i}, i=1,2,3,4とし、それらが二つのピクセルの値をそれぞれs_{1}= (0,0), s_{2}=(1,0), s_{3}=(0,1), s_{4}=(1,1)である。一般的に、第1ピクセルの値を確率変数(ベルヌーイ確率変数)としてx_{1}, x_{2}と表すと考えておいても良い。

\begin{array}{c|cc|c}State&x_{1}&x_{2}& \\ \hline s_{1}&0&0&v_{1}\\ s_{2}&1&0&v_{2}\\s_{3}&0&1&v_{3}\\s_{4}&1&1&v_{4} \\ \hline & p_{1} & p_{2} &prob. \end{array}

今、このパターンのもとで、状態s_{i}が現れる確率は、x_{1}, x_{2}の二変数の同時確率であり、それをv_{i}で表すことにしよう。この同時確率は、一般ベイズ分類器の事前確率になるものであり、私たちが求めたいところのものである。つまり、あるパターンのもとで、状態iが現れる確率がわかれば、状態iが現れた元でその原因となるパターンの確率がベイズ定理によって推計することができる、元のパターンを推定することができるわけである。

今、このパターンのもとで、第1ピクセルが1の値を持つ確率をp_{1}としよう。確率演算子Pを用いて、次のように書くこともできる。すなわち、普通の期待値演算子Eを使うとp_{1}=E(x_{1})である。同じく、第2ピクセルが1になる確率をp_{2}としよう。

今もし、二つのピクセルの値の確率変数が、相互に独立だとしたら、先の同時確率は簡単に求めることができる。すなわち、状態s_{1}の場合、二つのピクセルはともにゼロであるから、同時確率v_{1}は、

v_{1}=(1-p_{1})(1-p_{2})

となる。同様に、状態2の場合は、第1ピクセルが1で第2ピクセルは0であるから

v_{2}=p_{1}(1-p_{2})

となる。一般に、

v_{i} = p_{1}^{x_{1}}(1-p_{1})^{(1-x_{1})}p_{2}^{x_{2}}(1-p_{2})^{(1-x_{2})}

と書けば、すべての状態を表していることになる。独立の場合は、これを使えばベイズ分類が可能になり、それは、先の記事で私がやったNaive Bayes Classifierに他ならない。

では、独立性がなく、二つのピクセルの値の間に相関がある場合はどうなるだろうか。それを以下で考えてみよう。

今、第1ピクセルが1を持つ状態は、s_{2}s_{4}であるから、それらの状態の同時確率と、この状態の確率(周辺確率と呼んでもいい)との関係は次のようになる。(括弧つきの数字は、式の番号)

v_{2}+v_{4}=p_{1} \cdots (1)

p_{2}は、二番目のピクセル(確率変数)が1になる確率であり、それは次のようになる。

v_{3}+v_{4}=p_{2} \cdots (2)

さらに、二つの確率変数の間の共分散が存在し、それを\sigma_{12}であらわそう。この時、状態の同時確率と共分散の間には次のような関係が成立する。

p_{1}p_{2}v_{1}-(1-p_{1})p_{2}v_{2}-p_{1}(1-p_{2})v_{2}+(1-p_{1})(1-p_{2})v_{4}=\sigma_{12} \cdots (3)

私と同じくらいの理解力の人間では、上の式を見てすぐには理解できないと思うので、少し説明しよう。

共分散の普通の定義は、確率変数x_{i}x_{j}の共分散\sigma_{ij}は、その期待値をそれぞれp_{i}, p_{j}として、期待値演算子E を用いて、\sigma_{ij}=E(x_{i}-p_{i})(x_{j}-p_{j})と表される。言葉で表せば、\sigma_{ij}は、状態の値 (x_{i}-p_{i})(x_{j}-p_{j})に、この状態が現れる確率をかけたものである。

今、(3)式をvの項が4つあると見て、一番最初の項をみてください。これは状態s_{1}は変数が0,0の値を持っているので、分散の定義では、(0-p_{1})(0-p_{2})$???v_{1}$をかけたもの、すなわち、p_{1}p_{2}v_{1}となったものなのである。他の三つの項も、分散の定義に沿っていることは、お分かりになるだろう。それらの合計が、我々の求める\sigma_{12}に他ならない。

さて、もう一つ式がある。これが一番簡単だ。すなわち、同時確率も確率だから、それらを合計したものは1でなければならないということである。

v_{1}+v_{2}+v_{3}+v_{4}=1 \cdots (4)

我々の目的は、 各状態の同時確率の4つの値v_{1}, v_{2}, v_{3},v_{4}を求めることだが、それに対応する4つの式を得たことになる。当然、 p_{1},p_{2},p_{3},p_{4}および\sigma_{12}は与えられているものとする。実際それは、MNISTの場合、教育用データで標本値を求めることができるので、心配はいらない。

解くための学力は、中学あるいは高校の数学力があればいいだろう。が、4元連立方程式を解くというのはとても面倒くさい。十代の頃は、いつもケアレスミスで、間違ってしまった。でも、やらなければ前に進めないので、やりました。結果は次のようなものとなる。

v_{1}=(1-p_{1})(1-p_{2})+\sigma_{12}
v_{2}=p_{1}(1-p_{2})-\sigma_{12}
v_{3}=(1-p_{1})p_{2}-\sigma_{12}
v_{4}=p_{1}p_{2}+\sigma_{12}

結果を見て、そのわかりやすさに感激した。つまり、共分散のある場合の同時確率は、共分散がない場合、すなわち、相互に独立の場合の同時確率に、分散を足すか引くかしたもの。両方が1ないしは0の時は、共分散を足し、違う場合は引くということだ。つまり、共分散がある状態は同時に現れたり、消えたりする確率が高くなる。

結果を見て、なるほどと思った。

ただ、これだけでは、一般にどうなるかが予想できない。これは2ピクセルの場合である。しかし、MNISTは、784ピクセルもあるのだ。

もちろん、すぐに一般的な場合を考えても良いのかもしれない。計算手順はだいたいわかったので。しかし、やはりそれは無謀だ。まず、3ピクセルの場合をやってみようと思った。

3変数のベルヌーイ分布:共分散がある場合

3変数の場合も、基本同じだろう。と、思った。まず、テーブルを書いておく。

\begin{array}{c|ccc|c}State&x_{1}&x_{2}&x_{3}& \\ \hline s_{1}&0&0&0&v_{1}\\ s_{2}&0&0&1&v_{2}\\ s_{3}&0&1&0&v_{3}\\ s_{4}&0&1&1&v_{4}\\ s_{5}&1&0&0&v_{5}\\ s_{6}&1&0&1&v_{6}\\ s_{7}&1&1&0&v_{7}\\ s_{8}&1&1&1&v_{8}\\ \hline & p_{1} & p_{2} &p_{3} &prob. \end{array}

まず、式を作っていく。一番わかりやすいのは同時確率の合計が1という \sum_{i=1}^{8}v_{i}=1である。さらに、周辺確率p_{1},p_{2},p_{3}と同時確率との関係から、3本の式ができる。また、相関係数\sigma_{12},\sigma_{13},\sigma_{23}と確率の関係から式が3本できる(相関係数は、対象であることに注意)。あれ、7つしかできない。未知数である同時確率は8個あるのに、式が一つ足りないのだ。これには、かなり悩んだ。訳がわからなかったが、いろいろ調べてわかった。

x_{1},x_{2},x_{3}の三つの確率変数の間の同時共分散を考慮しなければならないのである。今この分散を\sigma_{123}としよう。すなわち、\sigma_{123}=E(x_{1}-p_{1})(x_{2}-p_{2})(x_{3}-p_{3})である。

この8本の式を全部書くと、ノート1ページを埋めるくらいになった(笑)。さらに、このサイトに書く元気は湧いてこない。かといって、何も書かないわけにもいかないので、行列表現で、この8元連立方程式を書いておこう。ただし、要素の中の式が長くならないように1-p_{i}=q_{i}, i=1,2,\cdots,8としておく。

\left(\begin{array}{cccccccc}1&1&1&1&1&1&1&1 \\0&0&0&0&1&1&1&1\\0&0&1&1&0&0&1&1\\0&1&0&1&0&1&0&1\\p_{1}p_{2}&p_{1}p_{2}&-p_{1}q_{2}&-p_{1}q_{2}&-q_{1}p_{2}&-q_{1}p_{2}&q_{1}q_{2}&q_{1}q_{2}\\p_{1}p_{3}&-p_{1}p_{3}&p_{1}q_{3}&-p_{1}q_{3}&-q_{1}p_{3}&q_{1}q_{3}&-q_{1}p_{3}&q_{1}q_{3}\\p_{2}p_{3}&-p_{2}q_{3}&-q_{2}p_{3}&q_{2}q_{3}&p_{2}p_{3}&-p_{2}q_{3}&-q_{2}p_{3}&q_{2}q_{3}\\-p_{1}p_{2}p_{3}&p_{1}p_{2}q_{3}&p_{1}q_{2}p_{3}&-p_{1}q_{2}q_{3}&q_{1}p_{2}p_{3}&-q_{1}p_{2}q_{3}&-q_{1}q_{2}p_{3}&q_{1}q_{2}q_{3}\end{array}\right)\left(\begin{array}{c}v_{1}\\v_{2}\\v_{3}\\v_{4}\\v_{5}\\v_{6}\\v_{7}\\v_{8}\end{array}\right)=\left(\begin{array}{c}1\\p_{1}\\p_{2}\\p_{3}\\\sigma_{12}\\\sigma_{13}\\\sigma_{23}\\\sigma_{123}\end{array}\right)

式はできた。解くための必要条件、式の数=未知数の数、も満たしている。が、さすがにこれを記号的に解く気にはなれない。コンピュータに頼るしかない。記号が入っていない、変数以外は実際の数値ならば、EXCElで簡単に解くことができるが、ここでは係数などが記号のままとかなければならないのである。数式を記号のまま処理して、連立方程式を解けるシステム、まず、頭に浮かんだのはMathematicaで、かつてライセンスを取っていた。ほとんど使わなかったが、今更、アップグレードやら何やらの手続きも面倒だ。トライアル期間を利用して解くという手もあるかと思ったが、そもそも使い方がよくわからない。

そこで、Maximaというのが無料で、よく使われているということを知って、これをMacにインストールした(サイト)。すぐに解くことができた。ただし、式が少し間違っていると、答えを出さなかったり、とんでもなく長い答えを出してくるので、正確に式を入れなければならない。

solve([v1+v2+v3+v4+v5+v6+v7+v8=1,
            v5+v6+v7+v8=p1,
            v3+v4+v7+v8=p2,
            v2+v4+v6+v8=p3,
            p1*p2*(v1+v2)-p1*(1-p2)*(v3+v4)-(1-p1)*p2*(v5+v6)+(1-p1)*(1-p2)*(v7+v8)=s12,
            p1*p3*(v1+v3)-p1*(1-p3)*(v2+v4)-(1-p1)*p3*(v5+v7)+(1-p1)*(1-p3)*(v6+v8)=s13,
            p2*p3*(v1+v5)-p2*(1-p3)*(v2+v6)-(1-p2)*p3*(v3+v7)+(1-p2)*(1-p3)*(v4+v8)=s23,
            -p1*p2*p3*v1+p1*p2*(1-p3)*v2+p1*(1-p2)*p3*v3-p1*(1-p2)*(1-p3)*v4
            +(1-p1)*p2*p3*v5-(1-p1)*p2*(1-p3)*v6
            -(1-p1)*(1-p2)*p3*v7+(1-p1)*(1-p2)*(1-p3)*v8=theta]
,[v1,v2,v3,v4,v5,v6,v7,v8]);

(%o19) [[v1 = (- theta) + s23 + p1 ((- s23) + p3 + p2 (1 - p3) - 1) + s13
 + p2 ((- s13) + p3 - 1) + s12 + p3 ((- s12) - 1) + 1, 
v2 = theta + p1 (s23 + p2 p3 - p3) - s23 + p2 (s13 - p3) - s13 + p3 (s12 + 1), 
v3 = theta + p1 (s23 + p2 (p3 - 1)) - s23 + p2 (s13 - p3 + 1) + p3 s12 - s12, 
v4 = (- theta) + s23 + p1 ((- s23) - p2 p3) + p2 (p3 - s13) - p3 s12, 
v5 = theta + p1 (s23 - p3 + p2 (p3 - 1) + 1) + p2 s13 - s13 + p3 s12 - s12, 
v6 = (- theta) + p1 ((- s23) - p2 p3 + p3) - p2 s13 + s13 - p3 s12, 
v7 = (- theta) + p1 (p2 (1 - p3) - s23) - p2 s13 - p3 s12 + s12, 
v8 = theta + p1 (s23 + p2 p3) + p2 s13 + p3 s12]]

上半分に、solveというコマンドで解くための式を定式化している。下半分の、Maximaのコマンドプロンプト(%o19)の後に、 '[[' と ']]'に挟まれてv_{1}からv_{8}までの答えが書き出されている。素晴らしい!!なお、thetaは\sigma_{123}のことである。

簡略化が、不完全なので、ぱっと見よくわからないが、整理すると次のようになる。ただし、q_{i}=1-p_{i}であることに注意する。2次元の場合の自然な拡張になっていることも直ちに読み取ることができる。

v_{1}=q_{1}q_{2}q_{3}+q_{3}\sigma_{12}+q_{2}\sigma_{13}+q_{1}\sigma_{23}-\sigma_{123}
v_{2}=q_{1}q_{2}p_{3}+p_{3}\sigma_{12}-q_{2}\sigma_{13}-q_{1}\sigma_{23}+\sigma_{123}
v_{3}=q_{1}p_{2}q_{3}-q_{3}\sigma_{12}+p_{2}\sigma_{13}-q_{1}\sigma_{23}+\sigma_{123}
v_{4}=q_{1}p_{2}p_{3}-p_{3}\sigma_{12}-p_{2}\sigma_{13}+q_{1}\sigma_{23}-\sigma_{123}
v_{5}=p_{1}q_{2}q_{3}-q_{3}\sigma_{12}-q_{2}\sigma_{13}+p_{1}\sigma_{23}+\sigma_{123}
v_{6}=p_{1}q_{2}p_{3}-p_{3}\sigma_{12}+q_{2}\sigma_{13}-p_{1}\sigma_{23}-\sigma_{123}
v_{7}=p_{1}p_{2}q_{3}+q_{3}\sigma_{12}-p_{2}\sigma_{13}-p_{1}\sigma_{23}-\sigma_{123}
v_{8}=p_{1}p_{2}p_{3}+p_{3}\sigma_{12}+p_{2}\sigma_{13}+p_{1}\sigma_{23}+\sigma_{123}

この同時確率の正しさを検証するものとしては、Jozef L. Teugels氏が1990年にJournal of Multivariate Analysis, Vol.32,誌に発表した論文"Some Representations of the Multivariate Bernoulli and Binominal Distributions" (pp.256-268)がある(サイト)。そこに、私のものと同じように、2次の場合と3次の場合の式が書かれている。ただし、3次の場合の式の並びと、ここでの式の並びは対応していない。それを適切に対応させれば、両者が一致していることを確認できるだろう。実は、先の\sigma_{123}が必要になることは、この論文に教えてもらった。

ここまでくると、一般的な多数のベルヌーイ変数がある場合の同時確率が予想できる。ただし、その一般形を書いた式は、私は見つけていない。いや、書き方が大変なことは予測できる。だからこそ、上記のTeugels氏は、クロネッカー積というものを使わざるを得なかったのだろうと思う。もし、どなたか、多変量ベルヌーイ分布の同時確率の一般系の書き方がわかる方がおられたら是非教えていただきたい(ツイッター @wassiisg まで)。

ただし、共分散を考慮したベルヌーイ分布を使って、一般的な(Naiveではない)ベイズ分類器を作る場合、必ずしも一般式は必要ではないというか、一般的な式のまま計算はできないと思う。どのように使うかは、次以降の記事で書きたい。

 

MNISTの手書き数字データをベイズ推計してみた:Naive Bayes Classifier

この間、アソシアトロンの本を読んだりプログラムを書いたりするうちに、概念形成の場合は、アソシアトロンよりもベイズ推計のほうがパフォーマンスが良くなる可能性に思い当たった。そこで、このさい、この概念形成のベイズ推計モデルを使って、ディープラーニングなどの人工知能の性能評価に使われているMNISTの手書き数字データをベイズモデルで推計してみた。

MNISTデータは、中身を変えずに少し形式を変形して用いた。訓練用の60000個のデータで、事前確率を計算し、それをもとにテストデータ10000個で、テストした。どうせパフォーマンスはぼろぼろだろうと思ったが、84.1%の正解率だった。ディープラーニングだと90%の後半の数字が出るので(私が作ったDeep Learningのプログラムで、MNISTのテストした時には、96%にとどまったが:笑)、それから比べると相当低いが、私は正直、こんなに高い数字が出たことで、ちょう驚いた。どこか、正解を計算に使ったような、とんでもない間違いをしているのではないかと思ったくらいだ。

まず、プログラムが、ディープラーニングよりも無茶苦茶簡単なものになる。プログラムは、以下に置いてある。

https://github.com/toyowa/NaiveBayesClassifier

MNISTのデータは大きすぎて載せていないが、読み込み関数を見れば、どのようなフォーマットか理解できると思う。MNISTデータをテキストファイルとして、わかりやすいフォーマットに変えている。

プログラムの簡単さよりも遥かに大きな驚きは、60000個の学習から10000個のテストを終えるまでに、私の8コアのubuntu18.04で9秒33で終わってしまうのだ。プログラムはg++用のc++で書いている。テストは、4秒弱で終わる。データの読み込み時間がかかっているので、実際の計算はもっと短い。

これは、単純ベイズ分類器(Naive Bayes Classifier)というものだ。単純というのは、変数間の相関を考慮せず、独立であると想定していることである。独立でない場合については、のちに書く予定だ。

理論的な枠組みを示す。単純ベイズ分類器といっても、理論はそんな単純ではない。手書き数字データは、0から9までの10個の数字のラベルがつけられている。このラベルに結びついた事象をA_{s}, s=0,1,\cdots,9であらわす。MNISTでは1ピクセルを0から255の数値でその濃さを表しているが、ここでは正ならば1、負ならば0と変換しておく(多変量ベルヌーイ試行あるいはベルヌーイ分布にしている。これを多変量正規分布でも、試みたが色々不都合があった。機会があれば説明する)。このピクセルのベクトル(あるいは事象)をx^{n} = (x_{i}^{n},i=0,1,\cdots, 783)としよう。nはデータ番号(必要がないときは省略する)、iはピクセル番号である。

このとき、ベイズ定理は次のように表される。

P(x|A_{s})P(A_{s})=P(A_{s}|x)P(x)

今、P(x|A_{s})は、数字ラベルsのときに、ピクセルベクトルxが得られる確率であり、P(A_{s}|x)は、逆に、ベクトルxが得られた時に、それが数字ラベルsに属する確率、P(A_{s})およびP(x)は、それぞれの条件に依存しない全体に対する確率である。

これは次のように変形できる。

式(1)
\begin{array}{lll}P(A_{s}|x) & = & \displaystyle\frac{P(x|A_{s})P(A_{s})}{P(x)} \\ & = & \displaystyle\frac{P(x|A_{s})P(A_{s})}{P(x|A_{0})P(A_{0})+P(x|A_{1})P(A_{1})+,\cdots,+P(x|A_{9})P(A_{9})} \\ & = & \displaystyle\frac{P(x|A_{s})P(A_{s})}{\sum_{s=0}^{9}P(x|A_{s})P(A_{s})}\end{array}

また、ここでは、変数(ピクセル値)の相関がないと想定しているので、P(x|A_{s})は、極めて単純(Naive)な形になり、次のように求められる。

式(2)
\begin{array}{lll}P(x|A_{s}) & = & p_{s0}^{x_{0}}(1-p_{s0})^{1-{x_{0}}}\cdot p_{s1}^{x_{1}}(1-p_{s1})^{1-{x_{1}}}\cdot,\cdots,\cdot p_{s783}^{x_{783}}(1-p_{s783})^{1-{x_{783}}} \\ & = & \prod_{i=0}^{783}p_{si}^{x_{i}}(1-p_{si})^{1-{x_{i}}}\end{array}

ここで、p_{si}は、ラベルがsのときに、ピクセルベクトルxの第i番目の要素x_{i}が1になる確率である。P(x|A_{s})が、各ピクセルが1であったり0で会ったりする同時確率になっていることを示しているのである。すなわち、x_{i}が1であるときはp_{si}そのものになり、0であるときはp_{si}のべき乗x_{i}が0で、値が1になってしまうかわりに1-p_{si}の冪乗1-x_{i}が1になるという算段である。

これを用いて、(1)式の左辺の確率が最大のラベルを求めれた、それがピクセルデータから推計された数字ラベルとなる。

ただ、(2)式が784回もかけるかたちになっていて、途方も無い計算と思われるかもしれない。

計算について、もし、このような784回も積を取る形が望ましく無い場合は、自然対数をとって和のかたちにすることもあり得る。すなわち、われわれの目的は、(1)式において、P(A_{s}|x)を最大にするsを求めることであり、各ラベルで共通する(1)式の分母は本質的役割を果たさない。そこで、(1)式の両辺を自然対数で変換変換して、(1)の右辺の分母だけを比較しても、最もあり得る数字ラベルを求めることは、理論的には全く問題ない。

ただ、実際の計算をすると、どちらも全く同じ答えを出し、自然対数を取る時間が余計にかかるだけであり、実際の計算では元々のかたちを用いている。

元々のかたちでの計算では、教育用データから事前確率を計算し、全てのテストデータを処理するのにMacPro上のUbuntu18.04 g++で8秒弱で計算を終えるのに対して、対数計算を加えると11秒かかった。計算結果は同じである。

なお、学習データは、基本60000個用いたが、1000個くらいから増やしていって、30000個くらいまでは、テストの正解率が上昇し、正解率は84.1%に達し、その後全く増加しなくなる。計算の確実性は高まっているようだが、パフォーマンスは良くならない。

『アソシアトロン』中野馨著の第4章概念形成の計算

表題の著作の第4章、4.1「概念形成」にかかれているサンプルを、プログラムして計算してみた。

まず、ベイズ定理を用いた計算は、次のようなプログラムだ。g++の一番新しいもので、linuxマシンでコンパイルすればいいと思う。私は、ubuntu 18.04 のg++でコンパイルしている。他のものの場合は、変更が必要になるかもしれない。(なお、その後知ったことだが、これは単純ベイズ分類器 Naive Bayes Classifier というものである。後の記事でもっと詳しく説明するだろう)

#include <iostream>
#include <string>
#include <cmath>

using namespace std;

double pxAB(int x[], double pq[]);

int main(){

    int x[16][4] = {
        {  0,  0,  0,  0}, //0  
        {  0,  0,  0,  1}, //1  
        {  0,  0,  1,  0}, //2  
        {  0,  0,  1,  1}, //3  
        {  0,  1,  0,  0}, //4  
        {  0,  1,  0,  1}, //5  
        {  0,  1,  1,  0}, //6  
        {  0,  1,  1,  1}, //7  
        {  1,  0,  0,  0}, //8  
        {  1,  0,  0,  1}, //9  
        {  1,  0,  1,  0}, //10 
        {  1,  0,  1,  1}, //11 
        {  1,  1,  0,  0}, //12 
        {  1,  1,  0,  1}, //13 
        {  1,  1,  1,  0}, //14 
        {  1,  1,  1,  1}  //15 
        };
        
    double PA = 0.5;
    double PB = 0.5;
    double p[4] = {0.6, 0.8, 0.8, 0.6};
    double q[4] = {0.4, 0.2, 0.2, 0.2};    
    
    for(int d=0;d<16;d++){
        double PxA = pxAB(x[d],p); 
        double PxB = pxAB(x[d],q); 
        double probBx = (PxB*PA)/(PxA*PA+PxB*PB); 
        double probAx = 1-probBx; 
        string AB; 
        if(probAx > probBx) 
            AB = " ==> A";
        else 
            AB = " ==> B";
        cout << "No." << d << " PAx = " << probAx << " PBx = " << probBx << AB <<endl;
    }
    
}

double pxAB(int x[], double pq[]){
    double prob = 1;
    for(int i=0;i<4;i++){
        prob = prob*pow(pq[i],x[i])*pow(1-pq[i],1-x[i]);
    }
    return prob;
}

以下のように、上記の本に書かれているとおりの結果が出力された。

No.0 PAx = 0.0204082 PBx = 0.979592 ==> B
No.1 PAx = 0.111111 PBx = 0.888889 ==> B
No.2 PAx = 0.25 PBx = 0.75 ==> B
No.3 PAx = 0.666667 PBx = 0.333333 ==> A
No.4 PAx = 0.25 PBx = 0.75 ==> B
No.5 PAx = 0.666667 PBx = 0.333333 ==> A
No.6 PAx = 0.842105 PBx = 0.157895 ==> A
No.7 PAx = 0.969697 PBx = 0.030303 ==> A
No.8 PAx = 0.0447761 PBx = 0.955224 ==> B
No.9 PAx = 0.219512 PBx = 0.780488 ==> B
No.10 PAx = 0.428571 PBx = 0.571429 ==> B
No.11 PAx = 0.818182 PBx = 0.181818 ==> A
No.12 PAx = 0.428571 PBx = 0.571429 ==> B
No.13 PAx = 0.818182 PBx = 0.181818 ==> A
No.14 PAx = 0.923077 PBx = 0.0769231 ==> A
No.15 PAx = 0.986301 PBx = 0.0136986 ==> A

しかし、アソシアトロンのほうがうまく行かない。原因がわかったかたは、ツイッターの @wassiisg まで教えていただきたい。色々テスト、計算過程の検証をしたが、3個のデータについて、ベイズ推定の場合と一致しない。プログラムは、以下のようなものである。

#include <iostream>
#include <string>
#include <cmath>

using namespace std;

void estimation(int v[], int data[][4]);

int main(){
    int data[10][4] = {
        {  1,  1,  1,  1}, // A
        {  1,  1,  1,  0}, // A
        {  1,  1,  0,  1}, // A
        {  0,  1,  1,  0}, // A
        {  0,  0,  1,  1}, // A
        {  1,  0,  0,  0}, // B
        {  0,  1,  0,  0}, // B
        {  0,  0,  1,  0}, // B
        {  1,  0,  0,  1}, // B
        {  0,  0,  0,  0}  // B
        };

    int test[16][4] = {
        {  0,  0,  0,  0}, //0  B
        {  0,  0,  0,  1}, //1  NON X
        {  0,  0,  1,  0}, //2  B
        {  0,  0,  1,  1}, //3  A
        {  0,  1,  0,  0}, //4  B
        {  0,  1,  0,  1}, //5  NON
        {  0,  1,  1,  0}, //6  A
        {  0,  1,  1,  1}, //7  B
        {  1,  0,  0,  0}, //8  B
        {  1,  0,  0,  1}, //9  NON
        {  1,  0,  1,  0}, //10 NON X
        {  1,  0,  1,  1}, //11 NON
        {  1,  1,  0,  0}, //12 NON X
        {  1,  1,  0,  1}, //13 A
        {  1,  1,  1,  0}, //14 A
        {  1,  1,  1,  1}  //15 A
        };

    int M[4][4];
    for(int i=0;i<4;i++){
        for(int j=0;j<4;j++){
            M[i][j] = 0;
        }
    }
    
    for(int d=0;d<10;d++){
        for(int i=0;i<4;i++){
            for(int j=0;j<4;j++){
                int di = data[d][i];
                int dj = data[d][j];
                if(di == 0) di = -1;
                if(dj == 0) dj = -1;
                M[i][j] = M[i][j] + (di*dj);
            }
        }
    }
    cout << "記憶行列 M " << endl;
    for(int i=0;i<4;i++){
        for(int j=0;j<4;j++){
            cout << M[i][j] << " ";
        }
        cout << endl;
    }
    
    cout << "概念形成 " << endl;
    for(int d=0;d<16;d++){
        cout << "No." << d << ":: "; 
        int v[4];
        for(int j=0;j<4;j++) v[j] = 0;
        for(int i=0;i<4;i++){
            for(int k=0;k<4;k++){ 
                int dk = test[d][k]; 
                if(dk == 0) dk = -1; 
                v[i] += M[i][k]*dk; 
            } 
            if(v[i] > 0){
                v[i] = 1;
                cout << "1 "; 
            }else{
                 v[i] = 0;
                 cout << "0 "; 
            }
        }
        estimation(v, data);
        cout << endl;
    }
    
    return 0;
}

void estimation(int v[], int data[][4]){
    double minidist = 10000;
    int minino = -1;
    for(int d=0;d<10;d++){
        double dis = 0;
        for(int j=0;j<4;j++){
            // ユークリッドの距離
            dis += (double)(data[d][j]-v[j])*(data[d][j]-v[j]);
            /*
            // Hamming の距離
            if(data[d][j]-v[j] < 0){
                dis += v[j]-data[d][j];
            }else{
                dis += data[d][j]-v[j];
            }
            */
        }
        // ユークリッドの距離
        dis = sqrt(dis); 
        // Hamming の距離
        //dis = dis/2;
        if(dis < minidist){
            minino = d;
            minidist = dis;
        }
    }
    if(minino < 5) cout << " ==> A ";
    else cout << " ==> B ";    
    cout << "( " << minino << ", " << minidist << " )";
}

出力結果は以下のようになる。

記憶行列 M 
10 2 -2 4 
2 10 2 0 
-2 2 10 0 
4 0 0 10 
概念形成 
No.0:: 0 0 0 0  ==> B ( 9, 0 )
No.1:: 0 0 0 1  ==> A ( 4, 1 )
No.2:: 0 0 1 0  ==> B ( 7, 0 )
No.3:: 0 0 1 1  ==> A ( 4, 0 )
No.4:: 0 1 0 0  ==> B ( 6, 0 )
No.5:: 0 1 0 1  ==> A ( 2, 1 )
No.6:: 0 1 1 0  ==> A ( 3, 0 )
No.7:: 0 1 1 1  ==> A ( 0, 1 )
No.8:: 1 0 0 0  ==> B ( 5, 0 )
No.9:: 1 0 0 1  ==> B ( 8, 0 )
No.10:: 1 0 1 0  ==> A ( 1, 1 )
No.11:: 1 0 1 1  ==> A ( 0, 1 )
No.12:: 1 1 0 0  ==> A ( 1, 1 )
No.13:: 1 1 0 1  ==> A ( 2, 0 )
No.14:: 1 1 1 0  ==> A ( 1, 0 )
No.15:: 1 1 1 1  ==> A ( 0, 0 )

ベイズ推定の場合と比べると3つのデータだけ違った結果を出している。

CUDA nvprof で、Error: incompatible CUDA driver version の対処法

答え:root権限を与えてnvprofを実行する。

例:(「CUDAプロフェッショナルプログラミング」を読んでいるところなので)

sudo /usr/local/cuda-10.0/bin/nvprof --metrics gld_throughput ./sumMatrix 32 32

ちなみに、私の環境は以下のとおり。

$ nvprof --version
nvprof: NVIDIA (R) Cuda command line profiler
Copyright (c) 2012 - 2018 NVIDIA Corporation
Release version 10.0.117 (21)
$ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2018 NVIDIA Corporation
Built on Sun_Aug_12_21:08:25_CDT_2018
Cuda compilation tools, release 10.0, V10.0.117

デバイスは、Jetpack Xavier です。

Jetson Xavier のセットアップメモ

アソシアトロン でMNISTの手書き数字の認識テストをさせたくてシステムを作った。ただ、6万個の学習データを使って、うまく認識させるためにはベクトルの次元を大きくしなくてはならず、それで学習させると、MacPro の24スレッドをフルに使っても計算に50時間かかる(笑)今それをやらせている。

アソシアトロン の学習とディープラーニングの学習の決定的な違いは、前者が極めて単純な行列計算の膨大な積み重ねになっているところだ。ディープラーニングのバックプロパゲーションというのは、多変数の非線形の最適化で、並列処理が難しい。プロは、うまくやっているのだろうが、相互依存が激しくて、並列処理にふさわしく処理を分割できないのだ。

その点、アソシアトロン は、計算をどこまでも細かく分解できる。

こうなると、GPUの並列処理がうまくいくように思う。ということで、CUDAで並列処理を、久しぶりにやってみようという気になった。Linux用の古いTELSAも持っているし、MAC用のTELSAまで持っている。が、ここは、最新のNVIDIAのJetson Xavier (写真左)を使ってみよう。Xavierは、私が奉職している大学の遠い昔の設立者の方と同じ呼び名である。

今年初めに買ったのだが、セットアップが面倒で、なんども失敗したので放置しておいた。しかし、ここはちゃんと動かさなければならない。

ホストコンピュータは、MacProに入れたVirtualBoxのUbuntu_18.04だ。基本、本家の次のサイトをベースにセットアップすべきだ。
(A) How to Install JetPack
MacのVirtualBoxだから、次のサイトからも色々教えていただいた。
(B) MacBook Pro で Jetson AGX Xavier をセットアップ
本家のサイトのインストラクション通りにやったが、途中でうまくいかなくなった。色々試したので、何が良かったのか、正確にはわからなくなってしまったが、特に大事なポイントだけは記録のために押さえておこう。

大きな流れは、ホストコンピュータにCUDAのツールキットなどのシステムをダウンロードし、デバイスに送信するという二段階になっている。本家の手順に、コメントしておこう。

(1)最初は、本家(A)の手順6のComponent Managerでは、Fullを選んでおけば良い。ここを進むと、インストールが開始されるが、それは、ホストコンピュータに必要なシステムがダウンロードされることとと理解しておく。
(2)次に、デバイスに送り込む手続きに入る。手順9にFlash OSを手順6で選ぶかどうかのことが書かれているが、最初は選んだままで良い。その際、ちゃんとUSBでホストとデバイスを繋いでおかなければならない。VirtualBoxの場合は、通常はMACがUSBを使っているので、Ubuntuに変更しなくてはならないことを忘れないように。
(3)そうすると手順10でネットワークの形式を聞いてくるが、ここは標準で選択されているUSB経由のネットワークにしておけば良い。USB経由のネットワークのIPアドレスが割り振られる。
(4)そうすると、手順14が出てくる。この辺りから混乱してくる。当初のリカバリーモードとは違って、一旦 デバイスのUbuntuを立ち上げた後のリカバリーモードで、違うようなのだ。実際、押すボタンも違う。ここは、手順14に忠実にやる。その際、USBがちゃんと繋げてないとエラーになる。
(5)しかし、ここで途中で止まってしまう現象に出くわし、困った。そこで、サイト(B)の教示に従う。つまり、その時は、一旦、CTL-Cでターミナルを閉じて、終了画面を出させて、ダウンロードしたものを削除せずに、インストールウィンドウも閉じる。そして、再度、デバイスを普通に立ち上げて、そこに割り振られたIPアドレスを確認する。
(6)その後、再度、NVIDAのインストーラを立ち上げる。手順9で、FLASH OSのインストールをしないように、no actionにする。ここがポイント。そうすると、次に、デバイスのIPアドレスとユーザー名とパスワードを聞いてくる。ここで、IPアドレスは先に確認したもの、ユーザー名はnvidia、パスワードもnvidiaと入れていけば、次に、インストールが開始される(!!)。なお、(B)では、nvidiaではなく、ubuntuと書いてあるが、私の場合、ubuntuでは失敗した。そのユーザーはあるが、ここは、基本的ユーザーのnvidiaにしなければならなかった。

全く、なんども失敗し、実に面倒だった。

なお、そのままほっておくと、デバイスはチンチかちんに熱くなる。心配になるので、ホームにある、./jetson_clocks.shをsudoで動かすと、ファンがうるさいほど回ってくれるので、すぐ冷却される。

./jetson_clocks.shは、CPUやGPUのクロックを確認するツールらしい。こちらで少し説明がされている。

また、サンプルは、

$ cd
$ ./jetson_clocks.sh
$ cd /usr/local/cuda/bin
$  sudo –user=nvidia ./cuda-install-samples-10.0.sh  /home/nvidia
$ cd  /home/nvidia/NVIDIA_CUDA-8.0_Samples
$ make -j 8
$ cd  5_Simulations/smokeParticles
$ ./smokeParticles

で実行させることができる。コンパイルには時間がかかるので、注意。実行の様子は、以下のツイッターの動画をみてください。

こちらを参考にさせていただいた。

これから、こちらもまた面倒なのだが、CUDAでC++プログラミングである。

期限切れの無線ルーターを上手に使う

昨夜、事務所(松竹芸能)のネタ見せが角座であった。なんと、無線ルーターにロボットが接続できず、ネタを披露できないというロボットをネガに使い始めて初めての大失態を犯した。多分で、伝説になると思う。

その原因は、先月まで2年間使っていたWIFIのルーター(左)が、契約切れで、新しいものを契約せず、簡易なWIFIルーター(右)を使ったことだ。

おそらく、簡易WIFIの電波が弱く、ちょっとの移動でロボットとの接続が切れてしまったためだと思う。外から、劇場にロボットをルーターと接続したまま持ち込むのだが、その途中に切れてしまったのだ。もっと十分なテストをすべきだった。

帰りながら、前のルーターは、ローカルにロボット同士をつなぐのには、別に契約が切れても大丈夫なのではないかと思いついた。こちらは、ローカルの配信に使う電波は十分に強いだろう。

契約切れでも、案の定、ローカルのロボットもパソコンもスマホも、相互接続を難なく繋げることができた。しかも、インターネットに接続したWIFIにつなぐことができることもわかった。自宅のWIFIモスマホのテザリングも繋ぐことができた。

しかも、こちらの方は電波が強く安定していることは間違いない。これでいける!!

2019年3月7日:結局どちらも使いもにならないことがわかった。一応繋がるが、安定しない。繋がらない状況や接続の切断が発生する

アソシアトロン の原理

人工知能の分野では、ディープラーニングなどの階層的ニューラルネットワークが脚光を浴びている。確かに、驚くべき成果を挙げているのだから、それは当然のことである。しかし、それが人間の脳のニューラルネットワークをシミュレートしているかといえばそうではないだろう。ディープラーニングが、その基礎的パーツとして神経回路網的構造を持っていることは確かだが、人間の脳もそのようにシステマティックに階層化されたネットワーク層を積み重ねているとは到底思えない。

人間の脳は、もっと非構造的システムのはずだ。脳には、領域ごとに違った機能を果たしていることはわかっている。しかし、その領域そのものが莫大な冗長性を持ったものであり、漠然とした機能の瞬間的作用から、人間の意識を想像している感じなのである。

そのように考えていた時、アソシアトロン というものに出会った。実は、私が30年以上前、岩手大学にいた頃、今のディープラーニングにつながるニューラルネットワークを研究していた頃、すでにこのアソシアトロン というものは世に出されていた。名前は知っていたのだ。が、当時の、バックプロぱゲーションなどを実装した並列処理システム、ニューラルネットワークの勢いの中で、真剣に考えてみたいテーマではなかったから、具体的にどのように実装するなどというところまでは全く行かなかった。

しかし、今この時に、改めてその内容を捕まえてみると、とても興味深い。そうだ、人間の脳は、きっとこんな感じなのだと思わせる、単純で、それでいてニューラルネットワークらしい漠然として機能を有している気がしてきた。

改めて、その理論の中身を捉えてみた。それは以下にまとめておいた。

アソシアトロン の原理
アソシアトロン の原理

この原理説明のpdf文書を見ていただければ明らかなように、このアソシアトロン が必ずしもそのものではない情報から記憶を再現できるのは、パターンが、そのパターンの次元倍のネットワークの中に、パターン情報を分散させるからなのである。原理的なアイデアはこれに尽きると思う。

今日のコンピュータ機能の進化した状況の中で、この単純さと優れた機能は改めて見直されるべきだと思う。

prolog化した『羅生門』を二分木検索する

prologのファクト文に変換した「羅生門」をどう使っていくか、使う過程で、さらにprolog化の方法をどう改良していくかが、次の課題となっている。

どう使っていくかの手始めに、先の羅生門の全文二分木化したものを検索するルールを使ってみよう。先の二分木を改良したもの(したがってJAVAプログラムそのものも改良)に、ルールを加えたswi-prolog用のスクリプトをzipで圧縮したものを以下においておく。

rashomon.zip

ダウンロードし、unzipする。冒頭に、検索のprologルールが記載され、その後、羅生門全文の二分木になっている。冒頭のルールだけを再掲すると以下のようなものである。

%%%%%%%%%%%%%%%%%%%%%%%%%
%% 「羅生門」の二分木探索
%%%%%%%%%%%%%%%%%%%%%%%%%
%%% 節(1)
% 右葉の探索
% 見つけたら、その語以外(AおよびB)を取得する
rsearch(S,node(A,B,S),A,B).
% リストの場合も受け入れる
rsearch(S,node(A,B,L),A,B) :- member(S,L). %% リストの場合の処理
% 上で一致しなかったら、左右のノードの内側を調べる
rsearch(S, node(_, Y, _), A, B) :- rsearch(S, Y, A, B).
rsearch(S, node(_, _, Z), A, B) :- rsearch(S, Z, A, B).
% 左葉の探索:基本右葉と同じ
lsearch(S,node(A,S,B),A,B).
lsearch(S,node(A,L,B),A,B) :- member(S,L).
% ここは、右と全く同じになる
lsearch(S, node(_, Y, _), A, B) :- lsearch(S, Y, A, B).
lsearch(S, node(_, _, Z), A, B) :- lsearch(S, Z, A, B).

%%% 検索、表示(2)
% 検索語を右側にした部分文章
right(X) :- rashomon(P,T),rsearch(X, T, A, B),write(P),write(': '),printnode(B),printnode(A),printnode(X),nl.
% 検索語を左側にした部分文章
left(X) :- rashomon(P,T),lsearch(X, T, A, B),write(P),write(': '),printnode(X),printnode(A),printnode(B),nl.

%%% ノードの表示(3)
% 対象がatomならば、そのまま表示
printnode(N) :- atom(N),write(N).
% 対象が空でないリストならば、最初の項の表示
printnode(N) :- [X|_] = N,printnode(X). 
% 対象が空リストならば'/'(半角スラッシュの表示)
printnode(N) :- [] = N,write('/'). %% 空リストでもtrueにする
% 対象がnodeならば、元の言葉の順序で表示(葉がnodeならば再帰的に表示する:ただし、node語がnodeは含まない)
printnode(N) :- node(X,Y,Z) = N,printnode(Y),printnode(X),printnode(Z).

このprologは、二分木の右の葉から語句を検索するものと、左の葉から検索するものの、二つのルールからなっている。

実行例を以下に示そう。

$ swipl ← ターミナルからswirl-prologを起動する
Welcome to SWI-Prolog (threaded, 64 bits, version 7.6.4)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit http://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

1 ?- ['rashomon.swi']. ← 解凍したRashomon.swiを読み込む
true.

2 ?- left(下人). ←  左の葉から、「下人」を検索する、パラグラフ、行番号とともに結果が出力される。1行しか示されないので、次を示すためには; セミコロンを入れる
line_0_1: 下人が羅生門の下で雨やみを待っていた/
line_3_6: 下人は七段/ある/石段の一番上の段に洗い/ざらした/紺の襖の尻を据えて/右の頬に出来た//大きな面皰を気にしながらぼんやり/雨のふるのを眺めていた/
line_4_0: 下人が雨やみ
line_4_1: 下人は雨がやんでも格別/どう/しようと云う/当てはない//
line_4_6: 下人が雨やみ
line_4_6: 下人が行き所がなくて/途方にくれていたと云う/方が適当である//
line_4_7: 下人のSentimentalismeに影響//
line_4_9: 下人は何をおいても差当り明日の暮しをどうにか/しようと云わば/どうにも/ならない事をどうにか/しようととりとめもない/考えをたどりながらさっきから朱雀大路にふる/雨の音を聞くともなく/聞いていたのである/
line_6_3: 下人の考え
line_6_5: 下人は手段
line_7_0: 下人は/大きな嚔をして/それから大儀/そうに立/上った//
line_8_0: 下人は頸をちぢめながら山吹の汗袗に重ねた/紺の襖の肩を高くして門のまわりを見まわした/
line_8_4: 下人はそこで腰にさげた/聖柄の太刀が鞘/走らないように気をつけながら藁草履をはいた/足をその/梯子の一番下の段へふみかけた/
line_9_4: 下人は始め
line_10_0: 下人は守宮のように足音をぬすんで/やっと/急な/梯子を一番上の段まで這うようにして上り//
line_12_0: 下人はそれらの死骸の腐爛/した/臭気に思わず/鼻を掩った/
line_13_0: 下人の眼はその/時/はじめて/その/死骸の中に蹲っている人間を見た//
line_14_0: 下人は六分の恐怖と四分の好奇心とに動かされて暫時は呼吸をするのさえ忘れていた/
line_15_0: 下人の心から
line_16_0: 下人には勿論/何故/老婆が死人の髪の毛を抜くかわからなかった/
line_16_2: 下人にとってはこの/雨の夜にこの/羅生門の上で死人の髪の毛を抜くと云う/事がそれだけで既に/許す/べからざる悪であった/
line_17_0: 下人は両足
line_20_0: 下人は老婆が死骸につまずきながら慌てふためいて/逃げようと行手を塞いで/こう/罵った//
line_20_2: 下人はまたそれを行かすまいと押しもどす//
line_20_5: 下人はとうとう/老婆の腕をつかんで/無理に/そこへ○/じ/倒した//
line_22_0: 下人は老婆
line_22_3: 下人は始めて/明白に/この/老婆の生死が全然/自分の意志に支配れていると云う/事を意識//
line_22_6: 下人は老婆
line_26_0: 下人は老婆の答が存外/平凡なのに失望//
line_29_0: 下人は太刀を鞘におさめて/その/太刀の柄を左の手でおさえながら冷然として/この/話を聞いていた/
line_29_2: 下人の心には/ある勇気が生まれて来た/
line_29_5: 下人は饑死をするか盗人になるかに迷わなかったばかりではない/
line_31_0: 下人は嘲るような声で念を押した//
line_33_0: 下人はすばやく/老婆の着物を剥ぎとった//
line_33_3: 下人は剥ぎとった/檜皮色の着物をわきにかかえて/またたく間に/急な/梯子を夜の底へかけ/下りた//
line_35_0: 下人の行方は誰も知らない/

3 ?- right(下人). % 続いて、右の葉から「下人」を検索する
line_4_5: 今/この/下人
line_15_4: この/下人
line_15_4: 恐らく/下人
line_16_3: 勿論/下人
line_20_1: それでも下人
line_24_0: その/下人
line_24_4: 喘ぎ喘ぎ/下人

左の葉から下人を選択すると、下人についてのアクティブな文章を出力される。右の葉の場合は、もっと、受動的なものとなるが、下人がそのような受動的な扱いをされている文章がより少ないことがわかる。それがどこの文章に当たるかは、冒頭の line_XX_XX の部分でわかる。

どのような文章を引き出すかは、その文章に関するリスト構造と、ルートフレーズをどのように設定してあるかに依存する。

芥川龍之介『羅生門』全文をprolog宣言文に変換した

この間作成したJAVAの変換プログラムで、青空文庫にある芥川龍之介「羅生門」の全文を、二分木のprologの宣言文にした(プログラムは、その後改定されているが、まだgithubには反映されていない)。結果は、ベタでここに載せるのは長すぎるので、以下のzipファイルをダウンロードし解凍して、適当なテキストエディタでご覧ください。

jprolog.swi.zip

具体的な手法などは、前の記事を参照のこと。変換にかかった時間は、1分2.832秒で、許容範囲。

swiprologに読み込にかかる時間は、ほんの一瞬。listingした結果(一部)は以下のような感じだ。

washida:~/Project/Robot/MakeKnowledge/JProlog/data $ swipl -f jprolog.swi 
Welcome to SWI-Prolog (threaded, 64 bits, version 7.6.4)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit http://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

1 ?- listing.

:- dynamic exception/3.
:- multifile exception/3.


plsample19(line0, node(へ, node([], [], どこ), node([], 行く, []))).
plsample19(line1, node([], [], [])).

plsample11(line0, node(あるが, node(の, node(と, 見る, 楼), node(には, 内, node(に, 噂, node([], [聞いた, 聞く], node([], 通り, node(つかの, 幾, node(が, 死骸, node([], 無造作に, [棄てて, 棄てる])))))))), node(の, 火, node(の, 光, node([], 及ぶ, node(が, 範囲, node(より, [思った, 思う], node(ので, 狭い, node(は, 数, node(つとも, 幾, node(ない, [わから, わかる], []))))))))))).
plsample11(line1, node(である, node(ながら, node([], ただ, おぼろげ), node(は, 知れるの, node(に, node([], その, 中), node(と, node(の, 裸, 死骸), node(を, 着物, node([], [着た, 着る], node(とが, 死骸, node(と, ある, node([], いう, 事))))))))), [])).
plsample11(line2, node(いるらしい, node(には, node([], 勿論, 中), node(も, 女, node(も, 男, [まじって, まじる]))), [])).
plsample11(line3, node(だと, node(は, node([], node([], そうして, その), 死骸), node(が, node([], 皆, それ), node([], かつて, node(いた, [生きて, 生きる], 人間)))), node([], 云う, node(さえ, 事実, node(れる, [疑わ, 疑う], node([], ほど, node(を, 土, node([], [捏ねて, 捏ねる], node([], [造った, 造る], node(のように, 人形, node([], node(を, 口, [開いたり, 開く]), node(を, 手, node(して, [延ばしたり, 延ばす], node(の, node([], ごろごろ, 床), node(に, 上, node(いた, [ころがって, ころがる], [])))))))))))))))).
plsample11(line4, node(しかも, [], node(とか, 肩, node(とかの, 胸, node(なっている, 高く, node(に, 部分, node(の, node([], node([], ぼんやり, [した, する]), 火), node(を, 光, node([], [うけて, うける], node(の, node(なっている, 低く, 部分), node(を, 影, node([], 一層, node(しながら, 暗く, node(に, 永久, node(の如く, 唖, node(いた, [黙って, 黙る], [])))))))))))))))).

plsample35(line0, node(の, 下人, node(は, 行方, node(も, 誰, node(ない, [知ら, 知る], []))))).
・・・・・・
・・・・・・
以下延々と続く

使い方のヒントは、例えばこの記事を参考に。