ニューラルネットワークを書いて遊んでいて気付いた事です。
ニューラルネットワークのコード
シンプルなニューラルネットワークは以下のコードで作れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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 |
例えば、
1 2 3 |
NN=SimpleNet(input_size=3,hidden_size=5,output_size=1) x=np.random.randn(1,3) y=NN.predict(x) |
とかで動かす事が出来ます。1どのように値が移っていくか見たい時は、
1 2 3 4 5 6 7 8 9 10 11 12 |
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}$$
とみて勾配を計算したいと思ったわけです。
という訳で、以下のようなコードを書きました。
1 2 3 4 5 6 7 8 9 |
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が帰ってきます。
解決策
何故なのか調べます。
1 2 3 4 5 6 7 8 9 10 11 12 |
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次に、順伝搬させて値をチェックします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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行書けば出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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関数を呼び出す毎に、層を更新させる事です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 |
これで勾配が計算出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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]]) |
困ったことの簡単な再現
単純なコードで、問題を再現します。問題は一言で言えば、関数の中だけ更新しても駄目という事でした次のような状況でした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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”]を更新すればいい訳です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 |
まとめ
- 困ったことを紹介した
- 解決策を紹介した
- 困りごとを再現した
- クラスの中の関数は、引数だけを更新しても駄目だと分かった。