【フレームワークを使用せず】ゼロから作る物体検出AIモデル
【No.12】ニューラルネットワークの実装(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を学習・それを元に推論していきます。

NNの学習

まず、前回実装した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)
              
            

今回の趣旨は、「0, 1, 2とそれぞれ書かれた画像データをNNに渡し、画像認識させる」こととします。なので、まず0、1、2の画像データと教師データを収集することが必要です。

今回はお馴染みMNISTを使用していきます(MNISTの詳しい解説は省略させていただきます)。

0、1、2だけ使用するので、それらの画像データと、対になる教師データを抽出します。

              
              import numpy as np
              import matplotlib.pyplot as plt
              from collections import OrderedDict

              import torch
              import torch.nn as nn
              import torch.nn.functional as F
              import torch.optim as optim
              import torchvision
              import torchvision.transforms as transforms


              #訓練データ
              train_dataset = torchvision.datasets.MNIST(root='./data',
                                                         train=True,
                                                         transform=transforms.ToTensor(),
                                                         download=True# 初回
                                                         #download=False# 2回目以降
                                                         )
              #検証データ
              test_dataset = torchvision.datasets.MNIST(root='./data',
                                                         train=False,
                                                         transform=transforms.ToTensor(),
                                                         download=True# 初回
                                                         #download=False# 2回目以降
                                                         )


              train_box = []
              for i, data in enumerate(train_dataset):
                  if data[1] in (0, 1, 2):
                      t1 = data[0].numpy().copy().reshape(-1, 784)
                      t2 = np.eye(3)[data[1]]
                      train_box.append((t1, t2))

                  if len(train_box) == 5000:
                      break

              test_box = []
              for i, data in enumerate(test_dataset):
                  if data[1] in (0, 1, 2):
                      t1 = data[0].numpy().copy().reshape(-1, 784)
                      t2 = np.eye(3)[data[1]]
                      test_box.append((t1, t2))

                  if len(test_box) == 20:
                      break
              
            

各dataset取得時、初回はdownload=True、2回目以降はdownload=Falseにしています。

訓練データとは学習に使用するデータのことで、検証データとは文字通り学習後の精度検証用に使用するデータのことです。
今回訓練データを5000、検証データを20にしました。

本来は訓練データ:検証データ = 8:2 とか 9:1 が望ましいですが、今回ガチの精度検証はせず、軽く目検のみなので、上記の比率でいきます。

[0, 1, 2]のデータをそれぞれtrain_boxとtest_boxに格納しました。これらから1枚ずつ取り出して学習及び推論を実施していきます。

教師データは、画像データに対応する数字、つまり単なる数値なのですが、この数値をone-hotベクトルにします。
one-hotベクトルとは、例えば数値の範囲が0~2の場合、「0」を[1, 0, 0]と表します。「1」は[0, 1, 0]、「2」は[0, 0, 1]と変換します。

なぜこの様な変換をするかというと、今回のNNの出力ノードは3つで、各ノードの値は、学習前はもちろんそれぞれ適当な数値が出力されるはずです。教師データをono-hotベクトルにすることで、学習が進むにつれ出力がそれぞれ1か0に収束するようになります。

「0」画像の教師データは[1, 0, 0]、「1」画像の教師データは[0, 1, 0]、「2」画像の教師データは[0, 0, 1]となります。

一応画像と教師データをみてみます。

              
              for i, data in enumerate(train_box):
                  x = data[0]
                  t = data[1]

                  print(t)
                  plt.imshow(x.reshape(28, 28), cmap='gray')
                  plt.show()

                  if i == 0:
                      break
              
            
              
              [1. 0. 0.]
              
            
図1 [1. 0. 0.]に対応する画像

これで入力する画像データ、教師データの準備ができました。ここから実際にNNに入力し、学習を実施してみたいと思います。

              
              import numpy as np
              import matplotlib.pyplot as plt
              from collections import OrderedDict

              import torch
              import torch.nn as nn
              import torch.nn.functional as F
              import torch.optim as optim
              import torchvision
              import torchvision.transforms as transforms


              # NN
              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)


              # 訓練データと検証データを取得
              train_dataset = torchvision.datasets.MNIST(root='./data',
                                                         train=True,
                                                         transform=transforms.ToTensor(),
                                                         #download=True
                                                         download=False
                                                         )

              test_dataset = torchvision.datasets.MNIST(root='./data',
                                                         train=False,
                                                         transform=transforms.ToTensor(),
                                                         #download=True
                                                         download=False
                                                         )

              # 0, 1, 2のみを抽出。及び教師データをone-hotベクトル化
              train_box = []
              for i, data in enumerate(train_dataset):
                  if data[1] in (0, 1, 2):
                      t1 = data[0].numpy().copy().reshape(-1, 784)
                      t2 = np.eye(3)[data[1]]
                      train_box.append((t1, t2))

                  if len(train_box) == 5000:
                      break

              test_box = []
              for i, data in enumerate(test_dataset):
                  if data[1] in (0, 1, 2):
                      t1 = data[0].numpy().copy().reshape(-1, 784)
                      t2 = np.eye(3)[data[1]]
                      test_box.append((t1, t2))

                  if len(test_box) == 20:
                      break


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

              # 学習
              for i, data in enumerate(train_box):
                  x = data[0]
                  t = data[1]

                  if i % 100 == 0:
                      print(f'train {i+1}-{i+100}...')

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

                  if i % 200 == 0:
                      print(f'--loss: {loss_calc}')
              
            
              
              train 1-100...
              --loss: 0.501434176031198
              train 101-200...
              train 201-300...
              --loss: 0.0782794788204203
              train 301-400...
              train 401-500...

              省略

              train 4501-4600...
              train 4601-4700...
              --loss: 0.13184783039830777
              train 4701-4800...
              train 4801-4900...
              --loss: 0.004502928226713012
              train 4901-5000...
              
            

学習が完了しました。

余談ですが、おそらく300回目のロス計算で値がかなり小さくなっているみたいなので、このNNは学習データ5000個もいらないかもしれません。その辺グラフ化して、ロスが底をうっているか確認する必要があります(今回はしていませんが)。

NNの推論

学習したNNがどの程度のものか、実際に推論して確かめます。検証方法は目検です(精度検証についても後日記事にしたいと思っています)。

              
              # 上記の学習の続きから書く

              # 推論
              for i, data in enumerate(test_box):
                  x = data[0]
                  t = data[1]

                  y = nn.predict(x)# 順伝播

                  predict = np.argmax(y)

                  print(f'predict: {predict} <-- {y}')
                  plt.imshow(x.reshape(28, 28), cmap='gray')
                  plt.show()
              
            
図2 画像データ20個と予測値の図
              
              predict: 2 <-- [[0.36059703 0.13137184 0.50844576]]
              predict: 1 <-- [[-0.02663263  0.99466058  0.03140036]]
              predict: 0 <-- [[ 0.79300986  0.23215765 -0.02393032]]
              predict: 1 <-- [[-0.06686145  1.08179798 -0.01505955]]
              predict: 0 <-- [[0.74730933 0.1652882  0.08997015]]
              predict: 0 <-- [[0.78678812 0.19816317 0.01809926]]
              predict: 1 <-- [[-0.12694668  1.08833617  0.0394031 ]]
              predict: 0 <-- [[ 0.82427907  0.21126809 -0.03306473]]
              predict: 0 <-- [[0.77275124 0.20889788 0.01981551]]
              predict: 1 <-- [[-0.08340048  1.00862371  0.07528398]]
              predict: 1 <-- [[-0.08169642  1.01635994  0.06542205]]
              predict: 2 <-- [[0.03304641 0.11196785 0.85466351]]
              predict: 1 <-- [[-0.11789704  1.06673833  0.05232005]]
              predict: 2 <-- [[0.07267232 0.02074368 0.90492775]]
              predict: 1 <-- [[-0.08315619  1.06286295  0.02070193]]
              predict: 1 <-- [[-0.03343533  0.96542191  0.06749356]]
              predict: 2 <-- [[0.06231169 0.31565589 0.62024599]]
              predict: 1 <-- [[-0.13759874  0.87797909  0.2593149 ]]
              predict: 2 <-- [[0.03652807 0.12435752 0.84026237]]
              predict: 0 <-- [[0.74064184 0.23156223 0.02766383]]
              
            

矢印の右側が実際の推論値(出力値)で、矢印の左側が最も高い推論値のインデックス、つまり予測値を示しています。

しっかり予測できていることが分かります。
この様に、タスクによっては、3層で重みの数もさほど多くないNNでも十分精度を出すことができます。

今回でNN構築が完了しました。本シリーズでは、これからNNの精度を上げるための手法などを解説、その後、画像データに特化したNN(CNN)の解説・実装を行っていきたいと思います。引き続きよろしくお願い致します。

3. まとめ

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

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

4. 参考文献

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

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

詳しくはこちら

Category

Search