PythonClassの関数は、引数だけ更新しても駄目

機械学習 Python R

ニューラルネットワークを書いて遊んでいて気付いた事です。

ニューラルネットワークのコード

シンプルなニューラルネットワークは以下のコードで作れます。

import numpy as np
from collections import OrderedDict
from scipy.special import expit as sigmoid
np.random.seed(0)

class Sigmoid:
    def forward(self,x):
        sig = sigmoid(x)
        return sig

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b     
        self.x = None
        self.original_x_shape = None

    def forward(self, x):
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x
        out = np.dot(self.x, self.W) + self.b
        return out

class SimpleNet:
    def __init__(self, input_size,hidden_size, output_size=1):
        self.params = {}
        self.params['W1'] = np.random.randn(input_size, hidden_size,)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = np.random.randn(hidden_size,output_size)
        self.params['b2'] = np.zeros(output_size)   
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Acrivation'] = Sigmoid()
        self.layers["Affine2"] = Affine(self.params['W2'], self.params['b2'])
        

    def predict(self,x):
        for L in self.layers.values():
            x=L.forward(x)
        return x

例えば、

NN=SimpleNet(input_size=3,hidden_size=5,output_size=1)
x=np.random.randn(1,3)
y=NN.predict(x)

とかで動かす事が出来ます。1どのように値が移っていくか見たい時は、

for L in NN_1.layers.values():
        x=L.forward(x)
        values=np.append(values,x)
import copy
NN_W=lambda W: NN.predict(x[0].reshape(1,-1))
W=np.copy(NN.params["W1"])
NN_1=copy.deepcopy(NN)
W_1=np.copy(W)
h=1e-4
W_1[0,0]+=h
NN_1.params["W1"]=W_1
d=(NN_1.predict(x) - NN.predict(x))/h

とかやれば、values に値が入っています。

はまった問題

ここからが本題です。ニューラルネットワークのパラメーターを決めるのには、勾配を計算するわけですが、数学っぽく、ニューラルネットワークを行列やベクトルの関数
$$\begin{eqnarray}
NN_x (W1,b1,W2,b2 )=W2\cdot (\sigma (W1 \cdot x+b1 ) +b2
\end{eqnarray}$$
とみて勾配を計算したいと思ったわけです。
という訳で、以下のようなコードを書きました。

import copy
NN_W=lambda W: NN.predict(x[0].reshape(1,-1))
W=np.copy(NN.params["W1"])
NN_1=copy.deepcopy(NN)
W_1=np.copy(W)
h=1e-4
W_1[0,0]+=h
NN_1.params["W1"]=W_1
d=(NN_1.predict(x) - NN.predict(x))/h

NN_1 が\(W_1 \)の(1,1)成分を少しだけ変化させたニューラルネットワークの関数のつもりです。なので、気持ちとしては
$$\begin{eqnarray}
d=\frac{ \partial NN_x }{\partial W1_{1,1} }
\end{eqnarray}$$
を計算しているつもりです。勿論この値は0にはなりませんが、上のように計算するとd=0が帰ってきます。

解決策

何故なのか調べます。

NN_1.params["W1"]
"""
array([[ 1.76415235,  0.40015721,  0.97873798,  2.2408932 ,  1.86755799],
       [-0.97727788,  0.95008842, -0.15135721, -0.10321885,  0.4105985 ],
       [ 0.14404357,  1.45427351,  0.76103773,  0.12167502,  0.44386323]])
"""
NN.params["W1"]
"""
array([[ 1.76405235,  0.40015721,  0.97873798,  2.2408932 ,  1.86755799],
       [-0.97727788,  0.95008842, -0.15135721, -0.10321885,  0.4105985 ],
       [ 0.14404357,  1.45427351,  0.76103773,  0.12167502,  0.44386323]])
"""

パラメーター自体はしっかり更新されているようです。2次に、順伝搬させて値をチェックします。

values=np.array([])
x=np.random.randn(1,3)
x_1=np.copy(x)
for L in NN_1.layers.values():
        x=L.forward(x)
        values=np.append(values,x)
        print(x)
values=np.array([])
for L in NN.layers.values():
        x_1=L.forward(x_1)
        values=np.append(values,x)
        print(x_1)
"""
[[1.28753573 1.0656353  1.0642368  1.74685217 1.66825623]]
[[0.78372979 0.74376598 0.74349937 0.85155533 0.84134319]]
[[0.7682275]]
[[1.28753573 1.0656353  1.0642368  1.74685217 1.66825623]]
[[0.78372979 0.74376598 0.74349937 0.85155533 0.84134319]]
[[0.7682275]]
"""

値を見てみると、NNでもNN_1 でも同じ値が返っています。パラメーターの値自体は更新されましたが、関数としては変更されてないみたいです。
解決は、以下のように1行書けば出来ます。

import copy

x=np.random.randn(1,3)
NN_W=lambda W: NN.predict(x[0].reshape(1,-1))
W=np.copy(NN.params["W1"])
NN_1=copy.deepcopy(NN)
W_1=np.copy(W)
h=1e-4
W_1[0,0]+=h
NN_1.params["W1"]=W_1
NN_1.layers["Affine1"]=Affine(NN_1.params['W1'], NN_1.params['b1'])#追加した場所
d=(NN_1.predict(x) - NN.predict(x))/h
d
#array([[0.02287]])

という訳で、関数みたいにクラスを使いたかったら、パラメーター自身でなくて層を更新してあげないといけませんでした。
簡単な解決策は、SimpleNet classのpredict関数を呼び出す毎に、層を更新させる事です。

class SimpleNet:
    def __init__(self, input_size,hidden_size, output_size=1):
        self.params = {}
        self.params['W1'] = np.random.randn(input_size, hidden_size,)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = np.random.randn(hidden_size,output_size)
        self.params['b2'] = np.zeros(output_size)   
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Acrivation'] = Sigmoid()
        self.layers["Affine2"] = Affine(self.params['W2'], self.params['b2'])
        

    def predict(self,x):
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Acrivation'] = Sigmoid()
        self.layers["Affine2"] = Affine(self.params['W2'], self.params['b2'])
        for L in self.layers.values():
            x=L.forward(x)
        return x

これで勾配が計算出来ます。

NN=SimpleNet(input_size=3,hidden_size=5,output_size=1)
x=np.random.randn(1,3)

import copy
NN_W=lambda W: NN.predict(x[0].reshape(1,-1))
W=np.copy(NN.params["W1"])
NN_1=copy.deepcopy(NN)
W_1=np.copy(W)
h=1e-4
W_1[0,0]+=h
NN_1.params["W1"]=W_1
d=(NN_1.predict(x) - NN.predict(x))/h
d
#array([[0.11412811]])

困ったことの簡単な再現

単純なコードで、問題を再現します。問題は一言で言えば、関数の中だけ更新しても駄目という事でした次のような状況でした。

def harinezumi(x):
    if x==None:
        return "harinezumi"
    else:
        return "masamune"
    
class masamune():
    def __init__(self,x=None):
        self.x=x
        self.masa={}
        self.masa["mune"]=harinezumi(self.x)+" kawai"
    def togetoge(self):
        print(self.masa["mune"])
    
masamune=masamune()
masamune.togetoge()
#harinezumi kawai

masamune.x="masamune"
masamune.togetoge()
#harinezumi kawai

最後のmasamune.togetoge()では、masamune kawai と出力されてほしい訳ですが、
masa[“mune”]を更新した訳ではないので欲しい出力が得られなかったという訳です。これを解決するには、togetoge関数を呼び出すたびにmasa[“mune”]を更新すればいい訳です。

class masamune():
    def __init__(self,x=None):
        self.x=x
        self.masa={}
        self.masa["mune"]=harinezumi(self.x)+" kawai"
    def togetoge(self):
        self.masa["mune"]=harinezumi(self.x)+" kawai"
        print(self.masa["mune"])

masamune=masamune()
masamune.x="masamune"
masamune.togetoge()
#masamune kawai

まとめ

  • 困ったことを紹介した
  • 解決策を紹介した
  • 困りごとを再現した
  • クラスの中の関数は、引数だけを更新しても駄目だと分かった。
  1. パラメーターも学習データも無いので、ただ値を返してくれるだけですが。
  2. deepcopyを使わないと、そもそもパラメーターが変更されません。
タイトルとURLをコピーしました