【フレームワークを使用せず】ゼロから作る物体検出AIモデル
【No.11】ニューラルネットワークの実装(3層のNN構築-実装)

1. 本記事について

本記事は、これまでの内容(ニューラルネットワークの理解その1〜4)を踏まえ、ニューラルネットワークをpythonプログラムに落とし込んでいきます。
本記事はこれまで実装してきた処理を使用して、簡単なニューラルネットワーク(NN)を実装していきます。

前回は、3層のNN構築-概要について解説しました。)

ニューラルネットワークの実装(入力層・Affine前半〜3層のNN構築-概要)を見てから、本記事を見ることをお勧めします。

注意事項として、本シリーズではpythonの基礎的な解説、numpyの基礎的な解説は割愛させていただきます。特に、numpy配列やshape等頻出なので、その辺の知識が不十分の場合は、そちらの習得を先に行うことをお勧めします。

本シリーズを進めていくにあたり、参考にさせていただく書籍があります。オライリー・ジャパンから出版されている「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」です。

本記事は、上記書籍を参考にさせていただいております。

2. NNの実装

前回迄実装してきた各処理を使って、簡単なニューラルネットワーク(以下NN)を構築し、実際に学習と推論を行ってみます。

これから行うことは、①NNの概要を決定(前回)、②NNをpythonで構築、③構築したNNで楽手と推論
です。今回は②を行います。

では、実際にNNをpythonで実装していきます。

使用するクラス

まず、前回迄に実装してきた4つのクラスを以下にまとめます。SGDは今回使用しないので省略します。

・Affine(中間層)
・Leaky ReLU(活性化関数)
・loss(SSE)
・Momentum(重み更新)
              
              import numpy as np


              class Affine:
                  # 各値の初期化
                  def __init__(self, W, b):
                      self.W = W
                      self.b = b

                      self.x = None
                      self.dW = None
                      self.db = None

                  # 順伝播
                  def forward(self, x):
                      self.x = x # 順伝播の入力を保持
                      out = np.dot(x, self.W) + self.b

                      return out

                  # 逆伝播
                  def backward(self, dout):
                      dx = np.dot(dout, self.W.T)      # δの計算
                      self.dW = np.dot(self.x.T, dout) # 重みの計算
                      self.db = dout                   # バイアスの計算

                      return dx


              class LeakyReLU:
                  # 各値の初期化
                  def __init__(self, alpha):
                      self.x = None# 逆伝播の時に使う
                      self.alpha = alpha

                  # 順伝播
                  def forward(self, x):
                      self.x = x# 順伝播の入力を保持
                      out = np.where(x > 0, x, self.alpha*x)

                      return out

                  # 逆伝播
                  def backward(self, dout):
                      dx = np.where(self.x > 0, dout*1, dout*self.alpha)# δの計算

                      return dx


              class loss():
                  # 各値の初期化
                  def __init__(self):
                      self.loss = None
                      self.y = None# 逆伝播の時に使う
                      self.t = None# 逆伝播の時に使う

                  # 順伝播
                  def forward(self, y, t):
                      self.y = y# 順伝播のyを保持
                      self.t = t# 教師データを保持
                      self.loss = np.sum((y-t)**2) / 2

                      return self.loss

                  # 逆伝播
                  def backward(self):
                      dx = self.y - self.t

                      return dx


              class Momentum:
                  # 各値の初期化
                  def __init__(self, lr=0.01, momentum=0.9):
                      self.lr = lr
                      self.momentum = momentum# 摩擦
                      self.v = None# 次の更新時に使う

                  def update(self, params, grads):
                      # 初回時はv = 0に指定する
                      if self.v is None:
                          self.v = {}
                          for key, val in params.items():
                              self.v[key] = np.zeros_like(val)# 重みの配列と同一形状

                      # パラメータごとに値を更新
                      for key in params.keys():
                          self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
                          params[key] += self.v[key]
              
            

上記クラスを順次使用していきます。

重みとバイアスの値

重みとバイアスの値を初期化します。

              
              import numpy as np

              # 重みとバイアスの初期化
              weight_init_std = 0.01

              params = {}
              params['W1'] = weight_init_std * np.random.randn(784, 500)
              params['b1'] = np.zeros((1, 500))

              params['W2'] = weight_init_std * np.random.randn(500, 100)
              params['b2'] = np.zeros((1, 100))

              params['W3'] = weight_init_std * np.random.randn(100, 3)
              params['b3'] = np.zeros((1, 3))

              for i in params:
                  print(f'key: {i}, shape of values: {params[i].shape}')
              
            
              
              key: W1, shape of values: (784, 500)
              key: b1, shape of values: (1, 500)
              key: W2, shape of values: (500, 100)
              key: b2, shape of values: (1, 100)
              key: W3, shape of values: (100, 3)
              key: b3, shape of values: (1, 3)
              
            

np.random.randnは平均0、標準偏差1の標準正規分布に従う乱数を生成します。
生成された乱数に対して0.01を掛けています。

重みの初期値に関しては、結構奥が深いので、後に記事にさせていただこうと思います。

とりあえずこのNNは一旦上記で進めます。

中間層の構築

中間層を構築していきます。以下の様にします。

              
              import numpy as np
              from collections import OrderedDict

              layers = OrderedDict()

              layers['Affine1'] = Affine(params['W1'], params['b1'])
              layers['LeakyReLU1'] = LeakyReLU(0.1)

              layers['Affine2'] = Affine(params['W2'], params['b2'])
              layers['LeakyReLU2'] = LeakyReLU(0.1)

              layers['Affine3'] = Affine(params['W3'], params['b3'])

              for i in layers:
                  print(f'key: {i}, values: {layers[i]}')
              
            
              
              key: Affine1, values: <__main__.Affine object at 0x7fab8f387040>
              key: LeakyReLU1, values: <__main__.LeakyReLU object at 0x7fab8f387100>
              key: Affine2, values: <__main__.Affine object at 0x7fab8f387160>
              key: LeakyReLU2, values: <__main__.LeakyReLU object at 0x7fab8f3871c0>
              key: Affine3, values: <__main__.Affine object at 0x7fab8f387220>
              
            

順伝播

順伝播、要は推論を行う関数を構築します。

              
              import numpy as np

              def predict(x):
                  for layer in layers.values():
                      x = layer.forward(x)

                  return x
              
            

入力をxとして、forで各中間層のforward関数に渡しています。

xを指定して結果をみてみます。一応各中間層のshapeもみてみます。

              
              import numpy as np

              def predict(x):
                  for layer in layers.values():
                      x = layer.forward(x)
                      print(f'layer: {layer.__class__.__name__}, x: {x.shape}')

                  return x


              x = np.random.rand(1, 784)
              x = predict(x)

              print()
              print(x)
              
            
              
              layer: Affine, x: (1, 500)
              layer: LeakyReLU, x: (1, 500)
              layer: Affine, x: (1, 100)
              layer: LeakyReLU, x: (1, 100)
              layer: Affine, x: (1, 3)

              [[0.0024374  0.0003439  0.00143628]]
              
            

ロスの計算

ロスの計算処理を構築していきます。といっても、ロス関数のクラスをほぼそのまま使うだけです。

              
              import numpy as np

              def loss_func(x, t):
                  return Loss.forward(x, t)
              
            

関数名をloss_funcにしました(class lossと被るのを防ぐため)。いまいちな名前なので、適当に変更をお願いいたします。
関数内のLossは、loss_func()の外側でインスタンス宣言します。

教師データ(t)を、適当にそれぞれ1、2、3にして、ロスを算出してみます。

              
              import numpy as np

              def loss_func(x, t):
                  return Loss.forward(x, t)

              x = np.random.rand(1, 784)
              t = np.array([1, 2, 3])

              Loss = loss()# loss()をインスタンス化

              x = predict(x)# 順伝播
              loss_calc = loss_func(x, t)# ロス

              print(loss_calc)
              
            
              
              6.993676714274571
              
            

ロスが算出されました。この6.993...をなるべく0に近づける為に、これから重みの変化量を算出し、そこから重みの更新をしていくことになります。

逆伝播

逆伝播で重みの変化量(dW)を算出し、変数に格納していきます。

              
              import numpy as np

              def gradient():
                  dout = Loss.backward()

                  layerss = list(layers.values())
                  layerss.reverse()
                  for layer in layerss:
                      dout = layer.backward(dout)

                  grads = {}# 各重みの勾配を格納
                  grads['W1'] = layers['Affine1'].dW
                  grads['b1'] = layers['Affine1'].db
                  grads['W2'] = layers['Affine2'].dW
                  grads['b2'] = layers['Affine2'].db
                  grads['W3'] = layers['Affine3'].dW
                  grads['b3'] = layers['Affine3'].db

                  return grads
              
            

Loss.backward()のLossは先ほどインスタンス化したloss()です。

各中間層の関数を逆順にし、 backward()関数を実行します。
backward()関数実行によって、各dWとdbが算出されるので、それらの数値をgradsに格納していきます。

実際に処理を実行してみます。

              
              import numpy as np

              def gradient():
                  dout = Loss.backward()

                  layerss = list(layers.values())
                  layerss.reverse()
                  for layer in layerss:
                      dout = layer.backward(dout)

                  grads = {}# 各重みの勾配を格納
                  grads['W1'] = layers['Affine1'].dW
                  grads['b1'] = layers['Affine1'].db
                  grads['W2'] = layers['Affine2'].dW
                  grads['b2'] = layers['Affine2'].db
                  grads['W3'] = layers['Affine3'].dW
                  grads['b3'] = layers['Affine3'].db

                  return grads


                  x = np.random.rand(1, 784)
                  t = np.array([1, 2, 3])

                  Loss = loss()# loss()をインスタンス化

                  x = predict(x)# 順伝播
                  loss_calc = loss_func(x, t)# ロス
                  grads = gradient()# 逆伝播→勾配計算

                  for grad in grads:
                      print(f'grad: {grad}, shape: {grads[grad].shape}')
              
            
              
              grad: W1, shape: (784, 500)
              grad: b1, shape: (1, 500)
              grad: W2, shape: (500, 100)
              grad: b2, shape: (1, 100)
              grad: W3, shape: (100, 3)
              grad: b3, shape: (1, 3)
              
            

重みの更新(学習)

gradsを使って重みを更新していきます。今回は更新関数としてMomentumを使用します。

              
              import numpy as np

              def update_func(params, grads):
                  momentum.update(params, grads)
              
            

関数内のmomentumは、update_func()の外側でインスタンス宣言します。

実際に重み更新を実行してみます。

              
              import numpy as np

              def update_func(params, grads):
                  momentum.update(params, grads)

              x = np.random.rand(1, 784)
              t = np.array([1, 2, 3])

              Loss = loss()# loss()をインスタンス化
              momentum = Momentum()# Momentumをインスタンス化

              x = predict(x)# 順伝播
              loss_calc = loss_func(x, t)# ロス
              grads = gradient()# 逆伝播→勾配計算

              print('before params')
              print(params['W1'][:5])
              update_func(params, grads)# 重みの更新

              print()
              print('after params')
              print(params['W1'][:5])
              
            
              
              before params
              [[-0.00392147 -0.0309712  -0.01221141 ...  0.00014648 -0.0189872
                -0.0044456 ]
              [ 0.00184462  0.01221464  0.00316603 ...  0.00059381 -0.00482479
                0.00367312]
              [-0.00134839  0.00961074 -0.00311371 ... -0.00329958 -0.00207449
                -0.00275487]
              [-0.00141299  0.00885205 -0.0060326  ...  0.00947855 -0.009482
                0.00932961]
              [-0.00425406 -0.00767978  0.00624049 ...  0.00038459 -0.00063964
                -0.01193417]]

              after params
              [[-0.00392336 -0.03097432 -0.01221867 ...  0.00014639 -0.01898411
                -0.00444544]
              [ 0.00183779  0.01220338  0.00313978 ...  0.00059349 -0.0048136
                0.00367371]
              [-0.00135149  0.00960564 -0.0031256  ... -0.00329972 -0.00206942
                -0.0027546 ]
              [-0.00142168  0.00883773 -0.00606597 ...  0.00947814 -0.00946777
                0.00933036]
              [-0.00425487 -0.00768111  0.00623739 ...  0.00038455 -0.00063831
                -0.0119341 ]]
              
            

重みが更新されています。

ネットワークを構築

最後に、これまでの入力〜重みの更新を使用し、NNクラスを実装してみます。

              
              import numpy as np
              from collections import OrderedDict

              class NeuralNetwork1:
                  def __init__(self):
                      # 重み・バイアスの初期化
                      weight_init_std = 0.01
                      self.params = {}
                      self.grads = {}

                      self.params['W1'] = weight_init_std * np.random.randn(784, 500)
                      self.params['b1'] = np.zeros((1, 500))

                      self.params['W2'] = weight_init_std * np.random.randn(500, 100)
                      self.params['b2'] = np.zeros((1, 100))

                      self.params['W3'] = weight_init_std * np.random.randn(100, 3)
                      self.params['b3'] = np.zeros((1, 3))

                      # 中間層の構築
                      self.layers = OrderedDict()

                      self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
                      self.layers['LeakyReLU1'] = LeakyReLU(0.1)

                      self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
                      self.layers['LeakyReLU2'] = LeakyReLU(0.1)

                      self.layers['Affine3'] = Affine(self.params['W3'], self.params['b3'])

                      self.loss = loss()# ロス関数
                      self.update = Momentum()# 更新関数


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

                      return x


                  def loss_func(self, x, t):
                      return self.loss.forward(x, t)


                  def gradient(self):
                      dout = self.loss.backward()

                      layers = list(self.layers.values())
                      layers.reverse()
                      for layer in layers:
                          dout = layer.backward(dout)

                      self.grads['W1'] = self.layers['Affine1'].dW
                      self.grads['b1'] = self.layers['Affine1'].db
                      self.grads['W2'] = self.layers['Affine2'].dW
                      self.grads['b2'] = self.layers['Affine2'].db
                      self.grads['W3'] = self.layers['Affine3'].dW
                      self.grads['b3'] = self.layers['Affine3'].db

                      return self.grads


                  def update_func(self):
                      self.update.update(self.params, self.grads)
              
            

実際に入力して処理を実行してみます。

              
              import numpy as np
              from collections import OrderedDict

              class NeuralNetwork1:
                  def __init__(self):
                      # 重み・バイアスの初期化
                      weight_init_std = 0.01
                      self.params = {}
                      self.grads = {}

                      self.params['W1'] = weight_init_std * np.random.randn(784, 500)
                      self.params['b1'] = np.zeros((1, 500))

                      self.params['W2'] = weight_init_std * np.random.randn(500, 100)
                      self.params['b2'] = np.zeros((1, 100))

                      self.params['W3'] = weight_init_std * np.random.randn(100, 3)
                      self.params['b3'] = np.zeros((1, 3))

                      # 中間層の構築
                      self.layers = OrderedDict()

                      self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
                      self.layers['LeakyReLU1'] = LeakyReLU(0.1)

                      self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
                      self.layers['LeakyReLU2'] = LeakyReLU(0.1)

                      self.layers['Affine3'] = Affine(self.params['W3'], self.params['b3'])

                      self.loss = loss()# ロス関数
                      self.update = Momentum()# 更新関数


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

                      return x


                  def loss_func(self, x, t):
                      return self.loss.forward(x, t)


                  def gradient(self):
                      dout = self.loss.backward()

                      layers = list(self.layers.values())
                      layers.reverse()
                      for layer in layers:
                          dout = layer.backward(dout)

                      self.grads['W1'] = self.layers['Affine1'].dW
                      self.grads['b1'] = self.layers['Affine1'].db
                      self.grads['W2'] = self.layers['Affine2'].dW
                      self.grads['b2'] = self.layers['Affine2'].db
                      self.grads['W3'] = self.layers['Affine3'].dW
                      self.grads['b3'] = self.layers['Affine3'].db

                      return self.grads


                  def update_func(self):
                      self.update.update(self.params, self.grads)


              x = np.random.rand(1, 784)
              t = np.array([1, 2, 3])

              nn = NeuralNetwork1()# ネットワークをインスタンス化

              y = nn.predict(x)# 順伝播
              loss_calc = nn.loss_func(y, t)# ロス
              grads = nn.gradient()# 逆伝播→勾配計算

              print('before params')
              print(nn.params['W1'][:5])
              nn.update_func()# 重みの更新

              print()
              print('after params')
              print(nn.params['W1'][:5])
              
            
              
              before params
              [[-0.00070941  0.00989282 -0.00737298 ...  0.00100678 -0.00224317
                -0.00253225]
              [ 0.00559211  0.01140472 -0.01884446 ...  0.006631    0.00107669
                -0.00779534]
              [ 0.00549107  0.00580318 -0.00859119 ... -0.00111083 -0.01898
                0.0102331 ]
              [-0.01654702 -0.00496326 -0.00104003 ... -0.0066198   0.0016084
                0.00713683]
              [ 0.00904293  0.01121062  0.0080445  ...  0.0268196  -0.01556447
                0.00033156]]

              after params
              [[-0.00071018  0.00989378 -0.00737459 ...  0.00100624 -0.00224486
                -0.00252682]
              [ 0.0055919   0.01140498 -0.01884489 ...  0.00663085  0.00107623
                -0.00779388]
              [ 0.00549029  0.00580415 -0.00859282 ... -0.00111139 -0.0189817
                0.01023861]
              [-0.0165481  -0.00496194 -0.00104227 ... -0.00662056  0.00160605
                0.00714441]
              [ 0.00904189  0.0112119   0.00804234 ...  0.02681886 -0.01556674
                0.00033887]]
              
            

重みが更新されていきます。

今は入力が適当な乱数ですが、これを画像データにして複数枚入力してあげれば、それらに最適化された重み更新が実行されるという形です。

次回は実際に画像データを用意して学習し、画像認識の推論を行ってみます。

3. まとめ

今回はニューラルネットワークの実装(3層のNN構築-実装)について解説しました。

次回も是非みてみてください!

4. 参考文献

斎藤康毅 著 「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」

・物体検出AIの導入
・アノテーションサービス
・手書き計算サイト ZONE++ の運営
・技術ブログ LAB++の運営
   上記をメインにおこなっております

詳しくはこちら

Category

Search