トピックス

特にコンセプトなくいろいろ書きます

ワーキングメモリはいかにして意識的なふるまいを生じさせるのか?5


今度こそやってみよう

 やるやると言っていながら全然やってこなかった確認作業に入りたいと思う。

 ワーキングメモリをもつモデルともたないモデルで架空/現実の識別タスクの実行結果に差が出るか、出るとすればどの程度出るのかを確認する。

 一般的に、架空と現実がどのように弁別されるかを考えてみると、われわれは普通、判断の対象になっているものがどの程度「ありえない」かでそれを区別している。

 対象が完全に「ありえない」ものであればそれは架空のものだと言えるし、よくあることならそれはきっと現実だ(ありえなくはないものが架空のものだったということもありうる[いたずらで食品サンプルを食卓に並べられる可能性は否定できない]けれど、とりあえずそれについては考えないでおく)。

 架空/現実の識別をするためにはその「ありえなさ」を測定する必要がある。

 ありえなさをどう測るかという点について、やり方は色々考えられるが、今回は一般的に考えて最もありうる事象と観察された事象が食い違っている場合を「ありえない」状況として定義しておこうと思う。

 今の説明だとどういうことなのかわからないはずなので、例を挙げたい。

 たとえば、明らかにリンゴにしか見えないものを見せられて、「これはうんこです」と言われたとしても、信じることはきっとできないだろう。

 それは嘘だ(うんこをリンゴそっくりに整形したかも知れないが、確率的に多分、それはない)。

 そのリンゴはうんこではない。

 それは架空の話のはずだ。

 今回の実験ではこの決定方針に従おうと思う。

 つまり、どう考えてもAなはずのものがBだった場合を「ありえない」ものとして、虚構(架空)と判断する。

$$ {X_i} = \begin{cases} 現実(よくある) & if(s_1=リンゴ, s_2=リンゴ) \\ 虚構(ありえない) & if(s_1=リンゴ, s_2=うんこ) \end{cases}\hspace{50pt}(1) $$

 上の式では見た感じ(${s_1}$)がリンゴで実際(${s_2}$)もリンゴだった場合現実と判断(${x} = 現実$)しているけれど、もちろん、見た感じ(${s_1}$)がうんこで実際(${s_2}$)もうんこだった場合も現実という判断(${x} = 現実$)になる。

 ${s_1}$と${s_2}$は実際にコード化する際はダミー変数(それぞれ、$\boldsymbol{s}(リンゴ)=[1,0]$と$\boldsymbol{s}(うんこ)=[0,1]$)とし、ベクトルで扱う。

$$ \boldsymbol {x_i} = (\boldsymbol{s_1^j},\boldsymbol{s_2^j})\hspace{10pt} j:for\ all\ elements $$

 この設定で実験してみる。

 モデルにはニューラルネットワークを使う。

 ワーキングメモリ(情報の収束領域)をもつネットワークともたないネットワークを作ってその挙動を比べてみる。

 イメージ的には以下のように作る。

f:id:N0Z:20210411084837p:plain

 このネットワーク構造をもつものをコードに落とす。  ニューラルネットワークの説明と実装の仕方は他にわかりやすい説明が山ほどあるのでここではしない。  むしろ、それについてはこちらがお世話になっている。  ここでのモデル作りにはたとえば

  を参考にさせてもらっている。  下で作るモデルは「ワーキングメモリがあるパターン」になる。

import numpy as np
import matplotlib.pyplot as plt

# HiddenLayerクラスを作る
class HiddenLayer:
    
    def __init__(self,n_in,n_out):
        self.w = np.random.randn(n_in,n_out)
        self.b = np.random.randn(n_out)
        
    def forward(self,x):
        self.x = x
        u = np.dot(x,self.w) + self.b
        self.y = 1 / (1 + np.exp(-u))
        return self.y
    
    def backward(self,grad_out,eta):
        grad_u = grad_out * (1 - self.y) * self.y
        self.grad_w = np.dot(self.x.T,grad_u)
        self.grad_b = np.sum(grad_u,axis=0)
        self.grad_x = np.dot(grad_u,self.w.T)
        self.w -= eta * self.grad_w
        self.b -= eta * self.grad_b
        return self.grad_x

# OutputLayerクラスを作る
class OutputLayer:
    
    def __init__(self,n_in,n_out):
        self.w = np.random.randn(n_in,n_out)
        self.b = np.random.randn(n_out)
        
    def forward(self,x):
        self.x = x
        u = np.dot(x,self.w) + self.b
        self.y = 1 / (1 + np.exp(-u))
        return self.y
    
    def backward(self,t,eta):
        grad_u = self.y - t
        self.grad_w = np.dot(self.x.T,grad_u)
        self.grad_b = np.sum(grad_u,axis=0)
        self.grad_x = np.dot(grad_u,self.w.T)
        self.w -= eta * self.grad_w
        self.b -= eta * self.grad_b
        return self.grad_x

def forward(network,x):
    for layer in network:
        x = layer.forward(x)
    return x

def backward(network,t,eta):
    for layer in reversed(network):
        t = layer.backward(t,eta)

def calc_cee(x,t):
    cee = - np.sum(t * np.log(x))
    return cee

def calc_acc(network,x,t):
    n = len(x)
    y = forward(network,x)
    classes = []
    for i in range(n):
        cls = []
        for j in range(len(y[i])):
            if y[i][j] > 0.6:
                cls.append(1)
            elif y[i][j] < 0.4:
                cls.append(0)
            else:
                cls.append('unclassified')
        classes.append(cls)
    tp_tf = 0
    uncls = 0
    for i in range(n):
        if 1 in classes[i]:
            idx_y = classes[i].index(1)
            idx_t = t[i].tolist().index(1)
            if idx_y == idx_t:
                tp_tf += 1
        if 'unclassified' in classes[i]:
            uncls += 1
    acc = tp_tf / n
    rate_uncls = uncls / n
    return acc, rate_uncls

def train(network,x,t,eta,interval):
    error_record = []
    index_random = np.arange(len(x))
    np.random.shuffle(index_random)
    epochs = len(x) // interval
    for i in range(epochs):
        index_mb = index_random[i*interval : i*interval+interval]
        x_mb = x[index_mb]
        t_mb = t[index_mb]
        y = forward(network,x_mb)
        a = backward(network,t_mb,eta)
        cee = calc_cee(y,np.array(t_mb))
        error_record.append(cee)
    return error_record

# 各層のノード数を決める
n_in = 4
n_mid = 4
n_out = 2

# 各層をインスタンス化
hidden_layer = HiddenLayer(n_in,n_mid)
output_layer = OutputLayer(n_mid,n_out)

network = [hidden_layer,output_layer]

# データセットを作る
X = [[1,0,1,0],
     [1,0,0,1],
     [0,1,1,0],
     [0,1,0,1]] * 1000
T = [[0,1] ,
     [1,0],
     [1,0] ,
     [0,1]] * 1000
X = np.array(X)
T = np.array(T)
n_data = len(X)

# 実験
interval = 10
eta = 1e-1
errors = []
accs = []
uncls = []

index_random = np.arange(n_data)
np.random.shuffle(index_random)

for i, idx in enumerate(index_random):
    x = X[idx]
    t = T[idx]
    forward(network,x.reshape(1,-1))
    backward(network,t.reshape(1,-1),eta)
    if i % interval == 0:
        y = forward(network,x)
        cee = calc_cee(y,t)
        errors.append(cee)
        acc,un = calc_acc(network,X,T)
        accs.append(acc)
        uncls.append(un)

# 結果表示
x_axis = np.arange(0,n_data// interval,1)
plt.figure()
plt.plot(x_axis,errors)
plt.xlabel('Trials/10')
plt.ylabel('CrossEntoropyLoss')
plt.figure()
plt.plot(x_axis,accs,label='Acc')
plt.plot(x_axis,uncls,label='unclassified')
plt.xlabel('Trials/10')
plt.ylabel('Rate')
plt.legend()
plt.show()

結果、どうなったか?

 上のコードを実行すると、結果はこうなる。 f:id:N0Z:20210411080918p:plain

 結果の説明はとりあえず置いておいて、「ワーキングメモリがないパターン」も試しておきたいと思う。  今度は上のコードのデータセットとノード数を次のように変更して実行してみる。

# データセット
X = [[1,0,1,0],
     [1,0,0,1],
     [0,1,1,0],
     [0,1,0,1]] * 1000
T = [[0,1] ,
     [1,0],
     [1,0] ,
     [0,1]] * 1000
X = np.array(X)
T = np.array(T)
X= X[:,:2]
n_data = len(X)

# 各層のノード数
n_in = 2
n_mid = 4
n_out = 2

 すると、結果こうなる。 f:id:N0Z:20210411092925p:plain  これで結果が出たわけだけれど、この結果からどういう結論が出るかということについては次回追記したい。