ニューラルネットワークを理解する #11 「誤差逆伝播法③」

 前回までで誤差逆伝播法については理解できたと思うのでこれを実装していきましょう。

前回までのまとめ

  • 誤差逆伝播法とは微分を効率的に計算できる手法である
  • 計算グラフによって可視化するとわかりやすい
  • 途中の計算結果を共有することで効率化がなされる

誤差逆伝播法の実装

今回までのをベースにしつつ少し変更箇所があるので一から書いていきましょう。

まずデータセットの準備。

import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf

import tensorflow_datasets as tfds
mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 1次元に変換
x_train_fla=np.array([x.flatten() for x in x_train])
x_test_fla=np.array([x.flatten() for x in x_test])
# 0~1へ変換
x_train_fla_norm=x_train_fla/255.0
x_test_fla_norm=x_test_fla/255.0
# One-Hot
num_classes = 10
y_train_one_hot = np.eye(num_classes)[y_train]
y_test_one_hot = np.eye(num_classes)[y_test]


今回からレイヤという概念を導入します。

これがあることで途中の計算結果を保持できるようにします。

Affineレイヤ

これはニューラルネットワークの重みとバイアスを使った計算処理のレイヤです。

class Affine:
  def __init__(self, W, b):
      self.W =W
      self.b = b
      
      self.x = None
      self.original_x_shape = None
      # 重み・バイアスパラメータの微分
      self.dW = None
      self.db = 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

  def backward(self, dout):
      dx = np.dot(dout, self.W.T)
      self.dW = np.dot(self.x.T, dout)
      self.db = np.sum(dout, axis=0)
      
      dx = dx.reshape(*self.original_x_shape)  # 入力データの形状に戻す(テンソル対応)
      return dx

Sigmoidレイヤ

活性化関数のSigmoid関数をレイヤにしたものです。

def sigmoid(x):
    return 1 / (1 + np.exp(-x))    
class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

Softmaxと損失関数のレイヤ

出力層のSoftmaxと損失関数を合わせたレイヤです。


def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

def softmax(x):
    x = x - np.max(x, axis=-1, keepdims=True)
    return np.exp(x) / np.sum(np.exp(x), axis=-1, keepdims=True)
    
class SoftmaxWithLoss:
  def __init__(self):
      self.loss = None
      self.y = None # softmaxの出力
      self.t = None # 教師データ

  def forward(self, x, t):
      self.t = t
      self.y = softmax(x)
      self.loss = cross_entropy_error(self.y, self.t)
      
      return self.loss

  def backward(self, dout=1):
      batch_size = self.t.shape[0]
      if self.t.size == self.y.size: # 教師データがone-hot-vectorの場合
          dx = (self.y - self.t) / batch_size
      else:
          dx = self.y.copy()
          dx[np.arange(batch_size), self.t] -= 1
          dx = dx / batch_size
      
      return dx

レイヤに対応したニューラルネットワーク

from collections import OrderedDict

def init_layer_network(input_size, hidden_size, output_size, weight_init_std=0.01):
  # 重みの初期化
  network={}
  network['params']={}
  network['params']['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
  network['params']['b1'] = np.zeros(hidden_size)
  network['params']['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
  network['params']['b2'] = np.zeros(output_size)

  # レイヤの生成
  network['layers'] = OrderedDict()
  network['layers']['Affine1'] = Affine(network['params']['W1'], network['params']['b1'])
  network['layers']['Sigmoid1'] = Sigmoid()
  network['layers']['Affine2'] = Affine(network['params']['W2'], network['params']['b2'])

  network['lastLayer'] = SoftmaxWithLoss()

  return network

def predict(layer_network, x):
  for layer in layer_network['layers'].values():
    x = layer.forward(x)
  return x

def loss(layer_network, x, t):
  y = predict(layer_network, x)
  return layer_network['lastLayer'].forward(y, t)
  
def gradient(layer_network, x, t):
  # forward
  loss(layer_network, x, t)

  # backward
  dout = 1
  dout = layer_network['lastLayer'].backward(dout)
  
  layers = list(layer_network['layers'].values())
  layers.reverse()
  for layer in layers:
      dout = layer.backward(dout)

  # 設定
  grads = {}
  grads['W1'], grads['b1'] = layer_network['layers']['Affine1'].dW, layer_network['layers']['Affine1'].db
  grads['W2'], grads['b2'] = layer_network['layers']['Affine2'].dW, layer_network['layers']['Affine2'].db

  return grads
  
def accuracy(layer_network, x, t):
  y = predict(layer_network,x)
  y = np.argmax(y, axis=1)
  if t.ndim != 1 : t = np.argmax(t, axis=1)
  
  accuracy = np.sum(y == t) / float(x.shape[0])
  return accuracy

実際に学習させていきましょう。

layer_network=init_layer_network(784, 50, 10, weight_init_std=0.01)

iters_num = 100  # 繰り返しの回数を適宜設定する
train_size = x_train.shape[0]
batch_size = 10

learning_rate = 0.5

train_loss_list = []
train_acc_list = []
test_acc_list = []

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train_fla_norm[batch_mask]
    y_batch = y_train_one_hot[batch_mask]
    
    # 勾配
    grad = gradient(layer_network, x_batch, y_batch)
    
    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        layer_network['params'][key] -= learning_rate * grad[key]
    
    loss_ = loss(layer_network, x_batch, y_batch)
    train_loss_list.append(loss_)

    train_acc = accuracy(layer_network, x_train_fla_norm, y_train_one_hot)
    test_acc = accuracy(layer_network,x_test_fla_norm, y_test_one_hot)
    train_acc_list.append(train_acc)
    test_acc_list.append(test_acc)
    print(train_acc,test_acc)


めっちゃ早くなってる!
1分くらいしかかからなかった。
でも学習の精度は落ちていますね。
学習率も少し上げないとうまく学習されませんでした。(なぜ?)


markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()


Reluにしてみると精度も変わらなかった。
Sigmoidだと勾配消失問題が起きているのかな?(誤差逆伝搬法だとそうなるのか?)

まあでもこれで30分の一以上に早くできたのですごいですね。

コメント