ボルツマンマシン
この前のこれ
の記事を書いていこうと思います。
はじめに
動画のこいつは何者なのか、とかいう質問が出てくると思いますが最初に答えます。
人類に限りなく近いゴリラです。
本題に入りましょう、今回のモデルは情報分野だと「ボルツマンマシン」とか「ホップフィールドネットワーク」とか言われてるやつです。
物理科の民は「え、ただのイジングモデルじゃん・・・。」ってなるやつです。
この辺の説明は後でやります。
流れとしては、説明書いてコードはって結果はって終わり!ってなると思います。
各モデルの説明
上で出てきたモデルの説明をしていきます。
物理の話から情報分野に下る流れでいきます。
・イジングモデル(Ising-model)
統計物理とかやった人はわかると思いますが、磁性体のモデルの中で一番(?)簡単なやつです。
まず、棒磁石を思い浮かべてください。おそらく大勢の人は赤と青に塗られ、それぞれにN、Sと書いてあるやつを思い浮かべると思います。
こいつらを半分に切ったらどうなるでしょう?
答えは「長さが半分になった棒磁石が二つできる」です。知ってる人も知らない人もいると思いますが・・・。
では、それぞれもう半分にしたらどうなるでしょう?
もう簡単だと思いますが「さらに半分になった棒磁石が四つできる」ということになります。
これを繰り返していくと、これ以上分割できない「磁石の最小単位」にたどり着くことがわかると思います。
これが「スピン」です。
このスピンは矢印に例えられ、矢印がみんなで同じ方向を向いていると全体としてN極だとかS極だとかが生まれるわけです。
イジングモデルというやつもこれをモデル化したもので、矢印は上か下かのどちらかしか向きません。
全部上に向いていればその方向がN極で、逆はS極ということになります。
大体こんなイメージです。全部上向いてますね
この時矢印(スピン)がどこを向きたいかと言うものを指定しなければいけません。
それがハミルトニアン(エネルギー関数)です。今回の場合
こう書けます。本来は量子力学の波動関数の重なりがどうとか議論して積分したりして得られるものですが、直感的にも良さそうなことがすぐにわかります。
またSはベクトルで書いていますがイジングモデルにおいては±1しかとりません(上か下か)、あとsummationはあらゆるijの組で取ることにしています。
例えばJijが全部+1の場合を考えましょう。しかし、どういう状態が「答え」でしょうか?
現実世界というのは基本的に万物が全体としてエネルギー安定な方向へ向かっています。高いところから物を落とすと下に落ちるのもそうです。
何が言いたいかというと、このエネルギー関数は現実では常に最小化される状態(向き)になるということです。(厳密には自由エネルギーが最小化)
全てがJij=+1となる時は簡単で全てが上、または下に揃えばエネルギーが最小になることがわかると思います。
これはスピンが何個ある時でも同じです。(N>2) 磁石は全体として同じ方向を向いているので、こういったJijによって振る舞いが決まっていそう、ということも納得できると思います。
例ではJijが全て等しい時を考えましたが、Jijが色々な値を取るとそれぞれの矢印がもはやどういった向きをとれば良いのかわからないと思います。実際この場合は解析的に解くことはできません。
・ホップフィールドネットワーク(Hopfield-network)/ボルツマンマシン(Boltzmann-machine)
まずはホップフィールド ネットワークについてです。これはもうイジングモデルそのものです「スピンをJijと言う結合定数で結んでいる」と考えるとなんとなくネットワーク感も出てくるのではないでしょうか。具体的には下図のようになっています。
Jijの値が有限=繋がっている(繋がってないところは0)と考えるとイジングモデルはこのようになり、情報分野ではホップフィールド ネットワークと呼ばれます。
また、CNNなどは順方向に情報が伝わっていき、ニューロンが次々と発火していくような向きを持った構造になっています。しかしこのモデルは向きを持たない無向グラフの一種です。
次にボルツマンマシンについてですが、ホップフィールド とほとんど等価です。何が違うのかと言うと、「温度」の概念が取り込まれていることです。
温度を取り込む利点として、記憶を想起させる時に便利とかはあると思いますが、他にはあるんでしょうかねえ〜〜勉強してないのでよくわかりません。
・結局何が違うのか
結局一緒なんじゃん、となると思います。はっきり言うと
結局一緒
です。本質的には何も変わりません。
じゃあ情報と物理で何が違うかと言うと「問題設定」です。
物理では、物質構造や現象から「Jijを予め決めてスピンの向きを決める」のに対して
情報では基本的に「スピンの向きからJijの組みを推定する」と言う問題を解きます。
逆問題の関係になっているわけですね。
例えば今回の話だと「2値画像(1ピクセル=1スピン)からJijを推定(学習)」し、それを今度は物理の問題として「与えられたJijの組みに対してのスピンの向きを決める(学習画像を想起する)」と言う二段階になっています。
学習・アルゴリズム
ここまででイメージがついたと思うので(思うので)実装に入ります。
正直実装にあたって触れていない重要なことがあるんですが、1つの記事に書ききれないのでいくつか天下り的に紹介します。気になったら調べてください。
学習方法
ホップフィールド における二値画像の学習方法は古くから知られていて「Hebb則」ってやつを使います。
Jij:i番目とj番目のピクセルの結合定数
N:総ピクセル数
p:覚えさせたいp番目の画像
S:2値をとる学習させたい画像のピクセル
こんな感じです。今回の設定だと与えた画像のセットから決定論的に決まるので推定とかではないですが、立派な学習です。
また、Jiiなど添え字が同じやつは0にします。(自分自身への結合は0ということ)
想起方法
これは簡単に言うと「乱数で1つのピクセルの向きを決めて、それがエネルギーを下げる方向ならば採用する」というのを繰り返すモンテカルロ法の1種です。ボルツマンマシンの文脈では少し間違ったことを言っていますが、だいたいあってるのであとは調べてください。
もしかしたらこれ単体の記事を書くかもしれません。
実装
ではこれらの概念を使って実際に実装します。
import numpy as np import cv2 def local_E(spin,i,j,img,img2,img_size):#ローカルな1スピンまわりのエネルギー計算 E = 0 for k in range(img_size): for l in range(img_size): N = img_size**2 #2値(0 or 255)の画像からいちいち直接Jijを計算する。この際0は-1となり、255は1となるように調節 #予めJijの配列を作成しても良いが、添え字を考えるのがめちゃくちゃ面倒 #画像が二枚なので手打ちですが、画像自体をリスト化して、for回すのが賢いと思います tmp = 2*img[i][j]/255-1 #1枚目画像 tmp2 = 2*img[k][l]/255-1 tmp3 = 2*img2[i][j]/255-1 #2枚目画像 tmp4 = 2*img2[k][l]/255-1 jij = (tmp*tmp2+tmp3*tmp4)/N #Hebb則からJijを算出 if i==k and j==l: jij = 0.0 #自分自身への結合は0 E = -1*jij*spin[i][j]*spin[k][l] + E #p番目の画像の全ピクセルに渡ってsummationをとる return E #hyper parameter img_size = 82 #画像の大きさ(正方形を想定) border = 105 #2値にする際のボーダーライン、お好みで調整 N = img_size**2 #総ピクセル数 seeds = 11 #乱数シード sweep = 10 #画像の更新回数(sweep*N回更新) temp = 0.06 #温度、小さな値だと綺麗に元画像が思い出せるが、計算コストがかかる #1枚目画像読み込み img = cv2.imread("pipo.png") img = cv2.resize(img, (img_size, img_size)) #一応指定したサイズと形にリサイズ img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) #グレースケール ret, img = cv2.threshold(img, border, 255, cv2.THRESH_BINARY_INV) #2値化 #二枚目読み込み、上と同様 img2 = cv2.imread("ash.jpg") img2 = cv2.resize(img2, (img_size, img_size)) img2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY) ret, img2 = cv2.threshold(img2, border, 255, cv2.THRESH_BINARY_INV) #2値画像をgazoフォルダに吐き出し(フォルダなどは適当に変えてください) cv2.imwrite('./gazo/pipo.png', img) cv2.imwrite('./gazo/ash.png', img2) #スピン配列(画像を思い出させる用)を作成 spin = np.zeros((img_size,img_size)) tmp_image = np.zeros((img_size,img_size),dtype = int) #+1-1を適当に配置 for i in range(img_size): spin[i] = np.random.choice([-1,1],img_size) #どのスピン(ピクセル)を更新するかを決めるための配列 xy = np.zeros((img_size),dtype = int) for i in range(img_size): xy[i] = i np.random.seed(seeds) for M_i in range(sweep): ran = np.random.rand(img_size**2) k = 0 #画像の吐き出し用 for i in range(img_size): for j in range(img_size): #x座標とy座標をランダムにチョイス(普通にやる場合は順番に全部更新する) x = np.random.choice(xy,1)[0] y = np.random.choice(xy,1)[0] E_old = local_E(spin,x,y,img,img2,img_size) #1つのスピンが感じるエネルギーを計算 spin[x][y] = - spin[x][y] #向きを反転させてみる E_new = local_E(spin,x,y,img,img2,img_size) #更新後の1スピン周りのエネルギーを計算 dE = E_new - E_old #更新前後のエネルギー差を計算 #本来更新することで系全体のエネルギーが下がるか上がるかを計算しなくてはならない #しかし、差分をとるため、注目しているスピンまわりだけ計算すれば十分 if ran[k] > np.exp(-dE/temp): #乱数が更新採択確率を上回ればreject spin[x][y] = - spin[x][y] #元に戻す if k%50 == 0: #50回更新するごとに画像を吐き出し(gif用) for i in range(img_size): for j in range(img_size): tmp_image[i][j] = int((spin[i][j] * 255 + 255)/2) #スピンを0,255の2値画像に変換 new_image_path = './gazo/'+str(M_i) +'_'+str(k)+ '.png'; cv2.imwrite(new_image_path, tmp_image) #書き込み k += 1
ざっとこんなもんです
色々書き足りていないところはありますが、上のプログラムをいい感じに書き直せばもっとたくさんの画像を記憶できるでしょう。
ただ、画像同士がほぼ直交していないとたくさんの画像は記憶できないようです。
結果
上のプログラムだと二枚記憶させているので、実は初期配置によっては
違う人が出てきます。
まとめ
一番簡単な画像記憶をやりましたが、どうでしょうか。
多分説明が足らないところがあると思います。モチベがあればここに付け足すか別の記事で補完したいですね(やらない)
画像記憶以外でもボルツマンマシンですぐできそうなこと見つけたら実装してみようと思います。