【フレームワークを使用せず】ゼロから作る物体検出AIモデル
【No.13】ニューラルネットワークの実装(重みの初期値)
1. 本記事について
本記事は、これまでの内容(ニューラルネットワークの理解その1〜4)を踏まえ、ニューラルネットワークをpythonプログラムに落とし込んでいきます。
本記事はこれまで実装してきた処理を使用して、簡単なニューラルネットワーク(NN)を実装していきます。
(前回は、3層のNN構築-学習と推論について解説しました。)
ニューラルネットワークの実装(入力層・Affine前半〜3層のNN構築-学習と推論)を見てから、本記事を見ることをお勧めします。
注意事項として、本シリーズではpythonの基礎的な解説、numpyの基礎的な解説は割愛させていただきます。特に、numpy配列やshape等頻出なので、その辺の知識が不十分の場合は、そちらの習得を先に行うことをお勧めします。
本シリーズを進めていくにあたり、参考にさせていただく書籍があります。オライリー・ジャパンから出版されている「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」です。
本記事は、上記書籍を参考にさせていただいております。
2. 重みの初期値について
今までの初期値
本シリーズでは、重みWの初期値にnp.random.randn()を使用してきました。これは平均が0、標準偏差が1になる正規分布、つまり標準正規分布に従う乱数を生成するモジュールです(正規分布についての詳細は割愛させていただきます)。
試しに3*10の乱数を生成してみます。
import numpy as np
W = np.random.randn(3, 10)
print(W)
[[-0.12544686 0.57144151 1.88362859 0.70056525 0.23098165 -0.99772044
-0.21884406 -0.45430165 1.08211236 -0.75986041]
[-1.89337295 0.04786452 -0.35648205 0.84824475 -1.96454444 -0.74182467
0.0921315 -2.10421522 -0.08645403 0.00844682]
[ 0.85481545 -0.3008695 -1.68461672 -0.19279451 -1.03557514 -0.07139938
-0.24212245 0.28209722 1.03226392 0.68117471]]
図で確認すると以下になります。数値を10000個生成します。
import numpy as np
W = np.random.randn(10000)
plt.hist(W, bins=50)
plt.show()

統計などでよく見る図だと思います。
前回迄使用していたWは、これらに0.01を掛け、値を小さくしています。なぜかというと、前回実装したNNでいうと、0.01を掛けたほうが出力がいい感じになるからです。
前回で使用した3層のNNクラスについて、重みの初期化を0.01掛けで行っています。
試しに、0.01を外した場合の出力値とロスを確認します。
コードは前回で使用したものを踏襲し、weight_init_std = 1とします。
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
# 3層の全結合NN
class NeuralNetwork1:
def __init__(self):
# 重み・バイアスの初期化
#weight_init_std = 0.01
weight_init_std = 1
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]
y = nn.predict(x)# 順伝播
loss_calc = nn.loss_func(y, t)# ロス
print(f'y: {y}')
print(f'loss: {loss_calc}')
print()
if i == 4:
break
y: [[-4248.71146093 956.09146874 -31.2839652 ]]
loss: 9487568.542132823
y: [[-1272.43415859 648.35073992 705.58568932]]
loss: 1268001.4166913137
y: [[-736.75252609 516.16927946 -96.94708595]]
loss: 409414.32070214767
y: [[-1806.40450696 2013.81330447 -182.42581711]]
loss: 3673896.910082736
y: [[-1811.8248297 1142.51821832 34.97224315]]
loss: 2293498.057035498
出力値、ロス共に非常に大きな値になってしまいました。この後の処理でオーバーフロー及びアンダーフローの元になりかねません。つまりあまり良い初期値でないということになります。
上記の様な数値にならない様に、最初に重みに0.01を掛けていい感じにしていたということです。
以上が、いままで使用してきた重みの初期値です。
次に「Heの初期値」を解説します。
Heの初期値
Heの初期値について解説する前に、0.01掛けのデメリットをお伝えします。
0.01掛けは、層があまり深くないネットワークでは有効ですが、層が深くなると出力値が偏ってしまう傾向があります。
試しに、6層の簡易ネットワークと、出力の結果を以下に示します。
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
)
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()# ネットワークをインスタンス化
def LeakyReLU(x):
return np.where(x > 0, x, 0.1*x)
activations = {}
x = train_box[0][0]
weight_init_std = 0.01
for i in range(6):
if i == 0:
w = weight_init_std * np.random.randn(784, 500)
elif i == 1:
x = activations[i-1]
w = weight_init_std * np.random.randn(500, 100)
elif i in (2, 3, 4):
x = activations[i-1]
w = weight_init_std * np.random.randn(100, 100)
else:
x = activations[i-1]
w = weight_init_std * np.random.randn(100, 3)
a = np.dot(x, w)# Affine順伝播の計算
z = LeakyReLU(a)# 活性化関数
activations[i] = z# 出力を保存
# 作図
for i, z in activations.items():
plt.subplot(1, len(activations), i+1)
plt.hist(z.flatten(), bins=30, range=(-0.5, 0.5))
plt.xticks([-0.5, 0.0, 0.5])
if i != 0:
plt.yticks([], [])
plt.title(str(i+1))
plt.show()

図2を見ると、2層目以降出力値が0.0付近に偏ってしまっているのがわかります。
これは、活性化関数がReLUの時に生じやすいです。理由としては、ReLUは負の数を0にして返すので、出力が0になる場合が多いためです。
今回活性化関数にLeaky ReLUを使用しています。Leaky ReLUも0近傍に出力が集中します。
この状態を回避するために、Heの初期値を使用します。
Heの初期値は、前層のノード数を\(n\)として、重みに\(\sqrt{\frac{2}{n}}\)を掛けます。
今回で言うと、1層目は\(n=784\)、2層目は\(n=500\)、3〜6層目は\(n=100\)となります。
先ほどの簡易ネットワークに、Heの初期値を取り入れてみます。
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
)
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()# ネットワークをインスタンス化
def LeakyReLU(x):
return np.where(x > 0, x, 0.1*x)
activations = {}
x = train_box[0][0]
for i in range(6):
if i == 0:
w = np.sqrt(2 / 784) * np.random.randn(784, 500)
elif i == 1:
x = activations[i-1]
w = np.sqrt(2 / 500) * np.random.randn(500, 100)
elif i in (2, 3, 4):
x = activations[i-1]
w = np.sqrt(2 / 100) * np.random.randn(100, 100)
else:
x = activations[i-1]
w = np.sqrt(2 / 100) * np.random.randn(100, 3)
a = np.dot(x, w)# Affine順伝播の計算
z = LeakyReLU(a)# 活性化関数
activations[i] = z# 出力を保存
# 作図
for i, z in activations.items():
plt.subplot(1, len(activations), i+1)
plt.hist(z.flatten(), bins=30, range=(-0.5, 0.5))
plt.xticks([-0.5, 0.0, 0.5])
if i != 0:
plt.yticks([], [])
plt.title(str(i+1))
plt.show()

図3を見ると、図2に比べ0の偏りが少なくなっており、その分ある程度分布が保たれていることがわかります。
図2では各層の重みに0.01を掛けていましたが、図3では1層目に\(\sqrt{\frac{2}{784}}\fallingdotseq0.0505\)、2層目に\(\sqrt{\frac{2}{500}}\fallingdotseq0.6325\)、3〜6層目に\(\sqrt{\frac{2}{100}}\fallingdotseq0.1414\)を重みに掛けています。
こうすることで、出力の偏りを抑えることができます。
因みに、Heの初期値は基本的に活性化関数がReLUの場合に有効とされています。
他には、活性化関数がシグモイドの場合はXavierの初期値というものを使用するのが有効とされています。
本シリーズでは活性化関数は基本的にLeaky ReLUを使用するので、初期値にHeの初期値を指定していきます。
3. まとめ
今回はニューラルネットワークの実装(重みの初期値)について解説しました。
次回も是非みてみてください!
4. 参考文献
斎藤康毅 著 「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」
Link
Search