「ゼロから作るDeep Learning」(斎藤 康毅 著 オライリー・ジャパン刊)を読んでいる時に、参照したサイト等をメモしていきます。 その6の2← → その7
いまだに2層のニューラルネットであれこれやってます。
#1桁の数字を2つ1組にして、その和が9未満か9以上かを、2層ニューラルネットに判定させます
こんなのは、計算させたほうが早そうですが、
P86
ニューラルネットワークの利点は、すべての問題を同じ流れで解くことができる点にあります。たとえば、解くべき問題が「5」を認識する問題なのか、「犬」を認識する問題なのか、それとも、「人の顔」を認識する問題なのかといった詳細とは関係なしに、ニューラルネットワークは与えられたデータをただひたすら学習し、与えられた問題のパターンを発見しようと試みます。
ということなので、あえてやってみました。
少ないデータ量でテストできるはずなので、その処理過程も検証してみようかと。
処理の核となる2層ニューラルネットは、p114のものを流用。
import numpy as np
from common.functions import *
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 重みの初期化
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
# x:入力データ、 t:教師データ
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)
dz1 = np.dot(dy, W2.T)
da1 = sigmoid_grad(a1) * dz1
grads['W1'] = np.dot(x.T, da1)
grads['b1'] = np.sum(da1, axis=0)
return grads
(追記)
このプログラムの predict 定義の最後のところ、出力層の活性化関数のところですが、
p64
出力層で利用する活性化関数は、解く問題の性質に応じて決めます。たとえば、
回帰問題では恒等関数、2 クラス分類問題ではシグモイド関数、多クラス分類
ではソフトマックス関数を使うのが一般的です。出力層の活性化関数について
は、次節で詳しく説明します。
ということなので、
y = sigmoid(a2)
としてもいいのかもしれません。実際に、そのように変更して動かしてみても、ちゃんと学習できました。
訓練データはこんな感じで作成。100通りある組合せのうち、90組を訓練データ、10組をテストデータにしました。
# データ作成
import random
w = np.zeros((100,2), dtype=np.uint8)
for i in range(0,10):
for j in range(0,10):
w[i*10+j][:] = [i, j]
r = list(range(0,100))
random.shuffle(r)
t = np.zeros((100,2), dtype=np.uint8)
l = np.zeros((100), dtype=np.uint8)
j = 0
for i in r:
t[j] = w[i]
if (t[j][0]+t[j][1])<9:
l[j] = 1
else:
l[j] = 0
j += 1
train_mask = np.array(range(90))
x_train = t[train_mask]
t_train = l[train_mask]
batch_mask = np.array(range(90,100))
x_test = t[batch_mask]
t_test = l[batch_mask]
訓練データの内容。2つの数字と、その和が9未満なら1、9以上なら0
[3 5] : 1
[3 0] : 1
[2 0] : 1
[1 3] : 1
[3 3] : 1
[5 1] : 1
[1 9] : 0
[7 5] : 0
・
・
・
[1 5] : 1
[9 7] : 0
[0 1] : 1
[3 4] : 1
[8 6] : 0
[5 6] : 0
# 学習
def to_one_hot(label):
t = np.zeros((label.size, 2))
for i in range(label.size):
t[i][label[i]] = 1
return t
l_train = to_one_hot(t_train)
# ハイパーパラメータ
iters_num = 100
train_size = x_train.shape[0]
batch_size = 10
learning_rate = 0.1
train_loss_list = []
network = TwoLayerNet(input_size=2, hidden_size=3, output_size=2)
for i in range(iters_num):
# ミニバッチの取得
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = l_train[batch_mask]
# 勾配の計算
grad = network.gradient(x_batch, t_batch) # 誤差逆伝播法 速い
# パラメータの更新
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
#学習経過の記録
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
ハイパーパラメータが、
iters_num = 100
batch_size = 10
だと、学習経過の記録が、こんな感じで
学習回数が不足している感じです。
テストデータを判定させると、60%の精度しかありません。
# テストデータで評価
x = x_test
t = t_test
#network は、上の処理で作成したものをそのまま使う
accuracy_cnt = 0
for i in range(len(x)):
y = network.predict(x[i])
p= np.argmax(y)
if p == t[i]:
print(str(x[i]) + " : " + str(p))
accuracy_cnt += 1
else:
print(str(x[i]) + " : " + str(p) + " ←間違い")
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
[9 2] : 0
[9 3] : 0
[2 3] : 0 ←間違い
[1 4] : 0 ←間違い
[6 9] : 0
[3 2] : 0 ←間違い
[9 5] : 0
[4 5] : 0
[6 2] : 0 ←間違い
[7 2] : 0
Accuracy:0.6
ハイパーパラメータを、
iters_num = 1000
batch_size = 10
にすると、90%の精度に。
[9 2] : 0
[9 3] : 0
[2 3] : 1
[1 4] : 1
[6 9] : 0
[3 2] : 1
[9 5] : 0
[4 5] : 0
[6 2] : 0 ←間違い
[7 2] : 0
Accuracy:0.9
iters_num = 10000
batch_size = 10
で、100%の精度に。
[9 2] : 0
[9 3] : 0
[2 3] : 1
[1 4] : 1
[6 9] : 0
[3 2] : 1
[9 5] : 0
[4 5] : 0
[6 2] : 1
[7 2] : 0
Accuracy:1.0
#バッチ件数を増やして、学習回数を減らしてみた
iters_num = 200
batch_size = 50
精度80%
iters_num = 200
batch_size = 80
精度60%
以上から、バッチ件数よりも学習回数が重要らしいことがわかる。
最低2000回くらい必要か?
#バッチ件数を減らしてみた
iters_num = 2000
batch_size = 5
精度90%
学習回数が2000回あれば、学習は進んでいくが、バッチ件数が少なすぎると、ブレが大きくなるようだ。
iters_num = 10000
batch_size = 5
精度90%
少ないデータで学習すると、極端に偏ったデータだけがそろった時に、偏った学習をしてしまうようだ。
#中間層のサイズは?
このデータの場合、1本の直線で区切って判別できるので、それほど複雑な中間層はいらないのではないか? というか、中間層の層の数や層のサイズっていうのは、どうやって決めればいいのだろう?
まあ、悩むよりも実際にやってみたほうがわかりやすいかも。
ということで、バッチサイズ20、学習回数1万回の設定で、層のサイズだけ変えてみた。
##隠れ層のサイズを1にした場合
けっこう、ふつうに学習できているような?
テストデータの推論も100%あってるし。
しかし、データを変えて何回かためしていると、こんな風に収束しなくなることも。
勾配をうまく見つけられなくなったのだろうか?
テストデータを与えて推論させても、50から70%の精度しかない。内容を見ると、すべてを0と判定している。つまり、まともに直線を引けていないようだ。このときと同じデータを、隠れ層のサイズを2以上にしてやると、ちゃんと学習できています。
##隠れ層のサイズを2に
ふつうに学習できている。何回か試行したが、収束しなくなることはなかった。
##隠れ層のサイズを1000に
さすがに、サイズが1000もあると、ムダに計算の時間が長い。しかし、結果はたいして変わらない。
たぶん、隠れ層のサイズは入力や出力の要素数より多くないといけないと思われる。まあ、ふつうに考えても、そうだろうな、と思います。しかし、多ければいいというものでもないようです。いくつがいいのかは、今回のデータは単純すぎるので、よくわかりませんが。
今回は試していませんが、中間層の数は、判定の複雑さによるようです。今回は直線1本で区切れるので、2層でまったく問題ありませんでしたが、これが何本もの線や平面で区切らないといけないなら、層の数を増やさないといけないのだろうな、と思います。
#おまけ fill_between にはまってしまった
5章を読んでも、よくわからないから、未だに4章までのところで、あれこれ遊んでます。
このメモでやっていることをグラフにしてみようと思ったら、matplotlib沼にはまってしまいました。
作ろうとしたグラフは、これ。今回やったことというのは、このグラフの青い部分と赤い部分を分類しているだけ、ということを示したかったわけです。おそらく、画像の識別でやっていることというのもこれと同じことなんだろうと。元データの要素数が違いますが、多次元空間で多次元平面で仕切られた部分のどちらにあるかで判定している、というところは、多分、同じ仕組みなんだろうと、納得できました。
この程度のグラフを描くのに、いろんなところでつまづいた結果、こんなプログラムでなんとかできました。
import numpy as np
import matplotlib.pylab as plt
plt.figure(figsize=(8,8))
plt.xlabel("x",fontsize=30)
plt.ylabel("y",fontsize=30, labelpad=18)
plt.grid(True)
x = np.arange(-1, 11, 1)
y = 9 - x
plt.plot(x, y, color='black')
plt.axhline(y=9, xmin=0.0, xmax=1.0, color="red",linewidth=1,linestyle="dashed")
plt.axvline(x=9, ymin=0.0, ymax=1.0, color="red",linewidth=1,linestyle="dashed")
plt.fill_between(x, y, 9, where=(x*y>=0), color='pink', alpha=.5)
plt.text(4,6,"(x+y)>=9",fontsize=30,color="red")
plt.axhline(y=0, xmin=0.0, xmax=1.0, color="blue",linewidth=1,linestyle="dashed")
plt.axvline(x=0, ymin=0.0, ymax=1.0, color="blue",linewidth=1,linestyle="dashed")
plt.fill_between(x, y, 0, where=(x*y>=0),color='blue', alpha=.25)
plt.text(2,2,"(x+y)<9",fontsize=30,color="blue")
plt.show()
fill_between を使えば、グラフとグラフの間に色を塗れるのはすぐにわかったのですが、x<0の範囲やx>9の範囲にも色を塗ってしまうので、これをどうすれば無くせるかに悩みました。
where=(x>=0) とすると x>9の範囲は塗ってしまいます。
where=(x>=0 and x<=9) や where=(x between 0 and 9)とするとエラーになります。
塗りたくないのは、x<0 と x>9 の範囲ですが、x<0 のとき y>9 、x>9 のとき y<0 ということに気がついて、 where=(x*y>=0) という条件を指定すればいいことに気がつきました。気がつくまで1時間以上かかりました。
#追記
ハイパーパラメータを
iters_num = 10000
batch_size = 10
として学習し、テストの正答が100%になったとき、
network.params['W1']
array([[0.78114325, 0.73202525, 1.01373241],
[0.73870312, 0.69091733, 0.96524673]])
network.params['b1']
array([-6.55019861, -6.12926691, -8.54456424])
となった。
このパラメータをニューラルネットの1層目にあてはめると、
a_1 = 0.78114325x_1 + 0.72870312x_2 - 6.55019861
という式になる。
$a_1=0$となる直線を、$x_1$を横軸、$x_2$を縦軸にした座標上に描くと、
$0.78114325x_1 + 0.72870312x_2 - 6.55019861 = 0$
$0.72870312x_2 = -0.78114325x_1 + 6.55019861$
$x_2 = \frac{-0.78114325}{0.72870312}x_1\ + \frac{6.55019861}{0.72870312}$
$x_2 = -1.057452214x_1 + 8.8671598$
となり、このようなグラフになる。
本来の式とはズレがあるが、そこそこ近い式になるように、重みWが求められています。
直線が a = 0 のとき。赤い部分が a > 0 で、青い部分が a < 0 の場合ということになります。
そして、この a の値をシグモイド曲線にあてはめると
a > 0 のとき、0.5より大きくなり
a < 0 のとき、0.5より小さくなります。
#参考にしたサイト
1.4. Matplotlib: 作図
早く知っておきたかったmatplotlibの基礎知識、あるいは見た目の調整が捗るArtistの話
matplotlib.org