KDOC 198: 『ゼロから作るDeep Learning』
この文書のステータス
- 作成
- 2024-07-31 貴島
- レビュー
- 2024-08-01 貴島
概要
ゼロから作るDeep Learningは、深層学習を実装して学ぶ本。
メモ
実行の前提環境: https://github.com/kijimaD/zerodeep1 を ~/Project
下に展開してから実行する。
- パーセプトロンの限界は、このように 1 本の直線で分けた領域だけしか表現できない点にある(p30)
- パーセプトロンの素晴らしさは、“層を重ねる”ことができる点にある(p31)
- 行列Aと行列Bの積を計算するとき、Aの列数とBの行数を同じ値にする必要がある
- A(2x3) と B(3x4) みたいに
- 一般的に回帰問題では恒等関数を、分類問題ではソフトマックス関数を使う(p66)
- 計算グラフにおけるレイヤの実装(例: 乗算、加算…)は簡単に行うことができる。それらを使えば複雑な微分の計算を求めることができる(p141)
import numpy as np x = np.array([-1.0, 1.0, 2.0]) print(x) y = x > 0 print(y) z = y.astype(int) print(z)
[-1. 1. 2.] [False True True] [0 1 1]
import numpy as np import matplotlib.pylab as plt def step_function(x): return np.array(x > 0, dtype=int) x = np.arange(-5.0, 5.0, 0.1) y = step_function(x) plt.plot(x, y) plt.ylim(-0.1, 1.1) # plt.show()
import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) print(sigmoid(-5)) print(sigmoid(-1)) print(sigmoid(0)) print(sigmoid(1)) print(sigmoid(5))
0.0066928509242848554 0.2689414213699951 0.5 0.7310585786300049 0.9933071490757153
import numpy as np t = np.array([1.0, 2.0, 3.0]) print(1.0 + t) print(1.0 / t)
[2. 3. 4.] [1. 0.5 0.33333333]
import numpy as np def rel(x): return np.maximum(0, x) print(rel(-1)) print(rel(0)) print(rel(1)) print(rel(100))
0 0 1 100
import numpy as np A = np.array([10, 20, 30, 40]) print(A) print(np.ndim(A)) print(A.shape) print(A.shape[0]) print("================") B = np.array([[10, 20, 30, 40], [10, 20, 30, 40]]) print(B) print(np.ndim(B)) print(B.shape) print(B.shape[0])
[10 20 30 40] 1 (4,) 4 ================ [[10 20 30 40] [10 20 30 40]] 2 (2, 4) 2
import numpy as np A = np.array([[1, 2], [3, 4]]) A.shape B = np.array([[5, 6], [7, 8]]) B.shape print(np.dot(A, B))
[[19 22] [43 50]]
import numpy as np A = np.array([[1, 2], [3, 4]]) A.shape B = np.array([[7, 8], [5, 6]]) B.shape print(np.dot(A, B))
[[17 20] [41 48]]
import numpy as np X = np.array([1, 2]) print(X) W = np.array([[1, 3, 5], [2, 4, 8]]) print(W) print(X.shape) print(W.shape) print(np.dot(X, W))
[1 2] [[1 3 5] [2 4 8]] (2,) (2, 3) [ 5 11 21]
import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) X = np.array([1.0, 0.5]) W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) B1 = np.array([0.1, 0.2, 0.3]) print(X.shape) print(W1.shape) print(B1.shape) A1 = np.dot(X, W1) + B1 Z1 = sigmoid(A1) print(A1) print(Z1)
(2,) (2, 3) (3,) [0.3 0.7 1.1] [0.57444252 0.66818777 0.75026011]
import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) def identity_function(x): return x def init_network(): network = {} network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) network['b1'] = np.array([0.1, 0.2, 0.3]) network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) network['b2'] = np.array([0.1, 0.2]) network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]]) network['b3'] = np.array([0.1, 0.2]) return network # 入力から出力方向への伝達処理 def forward(network, x): W1, W2, W3 = network['W1'], network['W2'], network['W3'] b1, b2, b3 = network['b1'], network['b2'], network['b3'] a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) a3 = np.dot(z2, W3) + b3 y = identity_function(a3) return y network = init_network() x = np.array([1.0, 0.5]) y = forward(network, x) print(y)
[0.31682708 0.69627909]
import numpy as np a = np.array([0.3, 2.9, 4.0]) exp_a = np.exp(a) # 指数関数 sum_exp_a = np.sum(exp_a) # 指数関数の和 y = exp_a / sum_exp_a print(exp_a) print(sum_exp_a) print(y)
[ 1.34985881 18.17414537 54.59815003] 74.1221542101633 [0.01821127 0.24519181 0.73659691]
import numpy as np a = np.array([1010, 1000, 990]) result = np.exp(a) / np.sum(np.exp(a)) print(result)
[nan nan nan]
import numpy as np a = np.array([1010, 1000, 990]) c = np.max(a) result = np.exp(a-c) / np.sum(np.exp(a-c)) print(result)
[9.99954600e-01 4.53978686e-05 2.06106005e-09]
import numpy as np def softmax(a): c = np.max(a) exp_a = np.exp(a - c) # オーバーフロー対策 sum_exp_a = np.sum(exp_a) y = exp_a / sum_exp_a return y a = np.array([0.3, 2.9, 4.0]) y = softmax(a) print(y) print(np.sum(y))
[0.01821127 0.24519181 0.73659691] 1.0
ソフトマックス関数の出力の総和は1になる。この性質のおかげでソフトマックス関数の出力を確率として解釈できる。
import numpy as np y = np.array([1, 2, 1, 0]) t = np.array([1, 2, 0, 0]) print(y==t)
[ True True False True]
import numpy as np def sum_squared_error(y, t): return 0.5 * np.sum((y-t)**2) # 「2」を正解とする t = [0,0,1,0,0,0,0,0,0,0] # 「2」の確率が最も高い場合 y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] print(sum_squared_error(np.array(y), np.array(t))) # 「7」の確率が最も高い場合 y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] print(sum_squared_error(np.array(y), np.array(t)))
0.09750000000000003 0.5975
import numpy as np def cross_entropy_error(y, t): delta = 1e-7 # 微細な値を追加してマイナス無限大を発生させないようにする return -np.sum(t * np.log(y + delta)) # 「2」を正解とする t = [0,0,1,0,0,0,0,0,0,0] # 「2」の確率が最も高い場合 y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] print(cross_entropy_error(np.array(y), np.array(t))) # 「7」の確率が最も高い場合 y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] print(cross_entropy_error(np.array(y), np.array(t)))
0.510825457099338 2.302584092994546
import numpy as np print(np.random.choice(60000, 10))
[ 2811 41200 8006 1524 57277 54382 27135 35842 18590 13150]
def numerical_diff(f, x): h = 1e-50 # ごく小さい値 return (f(x+h) - f(x)) / h
import numpy as np print(np.float32(1e-50))
0.0
def numerical_diff(f, x): h = 1e-4 # 丸め誤差をさける return (f(x+h) - f(x-h)) / (2*h) # 中心差分で誤差を減らせる
- 極小な差分によって微分を求めることを数値微分という。数式の展開によって微分を求めることを解析的に微分を求めるなどという(p99)
def function_1(x): return 0.01*x**2 + 0.1*x import numpy as np import matplotlib.pylab as plt x = np.arange(0.0, 20.0, 0.1) y = function_1(x) plt.xlabel("x") plt.ylabel("f(x)") plt.plot(x, y) plt.show()
def function_2(x): return x[0]**2 + x[1]**2 import numpy as np import matplotlib.pylab as plt # x = np.arange(0.0, 20.0, 0.1) # y = function_2(x) # plt.xlabel("x") # plt.ylabel("f(x)") # plt.plot(x, y) # plt.show() # xとyの範囲を設定 x = np.linspace(-5, 5, 100) y = np.linspace(-5, 5, 100) # メッシュグリッドを作成 X, Y = np.meshgrid(x, y) # 関数の値を計算 Z = function_2([X, Y]) # プロットを作成 fig = plt.figure() ax = fig.add_subplot(111, projection='3d') ax.plot_surface(X, Y, Z, cmap='viridis') # グラフのラベルを設定 ax.set_xlabel('X axis') ax.set_ylabel('Y axis') ax.set_zlabel('Z axis') ax.set_title('3D plot of function_2') # グラフを表示 plt.show()
import numpy as np def function_2(x): return x[0]**2 + x[1]**2 # 勾配を計算する def numerical_gradient(f, x): h = 1e-4 # 極小値 grad = np.zeros_like(x) # 勾配を格納する。xと同じ形状の配列を生成する for idx in range(x.size): print("idx:", idx) tmp_val = x[idx] # 元の値を保持する x[idx] = tmp_val + h fxh1 = f(x) # 極小値を入れて計算する x[idx] = tmp_val - h fxh2 = f(x) # 極小値を入れて計算する grad[idx] = (fxh1 - fxh2) / (2*h) # 中心差分 x[idx] = tmp_val # 値を元に戻す return grad print(numerical_gradient(function_2, np.array([3.0, 4.0]))) print("========") print(numerical_gradient(function_2, np.array([0.0, 2.0]))) print("========") print(numerical_gradient(function_2, np.array([-3.0, 4.0])))
idx: 0 idx: 1 [6. 8.] ======== idx: 0 idx: 1 [0. 4.] ======== idx: 0 idx: 1 [-6. 8.]
import numpy as np # 数値微分 def numerical_gradient(f, x): h = 1e-4 grad = np.zeros_like(x) # 勾配を格納する。xと同じ形状の配列を生成する for idx in range(x.size): tmp_val = x[idx] x[idx] = tmp_val + h fxh1 = f(x) x[idx] = tmp_val - h fxh2 = f(x) grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 前後にずらした値を元に戻す return grad # 勾配降下 # lr -> learning rate def gradient_descent(f, init_x, lr=0.01, step_num=100): # 引数で渡された値が変わらないようにコピーする x = init_x for i in range(step_num): grad = numerical_gradient(f, x) x -= lr * grad # 勾配の分更新する print(i, " x: ", x, "\tgrad: ", grad) return x def function_2(x): return x[0]**2 + x[1]**2 init_x = np.array([-3.0, 4.0]) print("init_x: ", init_x) print("gradient_descent: ", gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=10))
init_x: [-3. 4.] 0 x: [-2.4 3.2] grad: [-6. 8.] 1 x: [-1.92 2.56] grad: [-4.8 6.4] 2 x: [-1.536 2.048] grad: [-3.84 5.12] 3 x: [-1.2288 1.6384] grad: [-3.072 4.096] 4 x: [-0.98304 1.31072] grad: [-2.4576 3.2768] 5 x: [-0.786432 1.048576] grad: [-1.96608 2.62144] 6 x: [-0.6291456 0.8388608] grad: [-1.572864 2.097152] 7 x: [-0.50331648 0.67108864] grad: [-1.2582912 1.6777216] 8 x: [-0.40265318 0.53687091] grad: [-1.00663296 1.34217728] 9 x: [-0.32212255 0.42949673] grad: [-0.80530637 1.07374182] gradient_descent: [-0.32212255 0.42949673]
- 損失関数を重みで微分することで、各重みが損失関数にどの程度影響を与えるかを知ることができる
- 勾配(微分の結果)は、損失関数の値がもっとも急速に変化する方向とその大きさを示す。重みをどの方向にどれだけ調整すれば損失関数を最小化できるかを示す
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch04.gradient_simpleset import simpleNet import numpy as np net = simpleNet() print("net.W: ", net.W) x = np.array([0.6, 0.9]) p = net.predict(x) print("p: ", p) print("argmax: ", np.argmax(p)) # 最大値のインデックス t = np.array([0, 0, 1]) # 正解ラベル print("loss: ", net.loss(x, t))
net.W: [[ 0.66771825 -0.03691929 1.8614051 ] [-1.38471091 -0.62661547 0.3531814 ]] p: [-0.84560886 -0.58610549 1.43470632] argmax: 2 loss: 0.21090872143605693
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch04.gradient_simpleset import simpleNet from ch04.gradient import numerical_gradient import numpy as np x = np.array([0.6, 0.9]) t = np.array([0, 0, 1]) net = simpleNet() print("net.W: ", net.W) f = lambda w: net.loss(x, t) # 損失関数を計算する関数 dW = numerical_gradient(f, net.W) print("dW: ", dW)
net.W: [[-2.09839831 -1.31760955 -0.01513779] [-0.14700285 -0.98933336 -0.75867432]] dW: [[ 0.15952377 0.11941069 -0.27893446] [ 0.23928565 0.17911604 -0.41840169]]
- 勾配は、損失関数の値をもっとも減らす方向を示す(p113)
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch04.two_layer_net import TwoLayerNet net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) print(net.params['W1'].shape) print(net.params['b1'].shape) print(net.params['W2'].shape) print(net.params['b2'].shape)
(784, 100) (100,) (100, 10) (10,)
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch04.two_layer_net import TwoLayerNet import numpy as np net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) x = np.random.rand(100, 784) # ダミーの入力データ(100 枚分) y = net.predict(x)
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch04.two_layer_net import TwoLayerNet import numpy as np # 入力画像は 28x28, 分類は10クラス分 net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) x = np.random.rand(100, 784) # ダミーの入力データ (100 枚分) t = np.random.rand(100, 10) # ダミーの正解ラベル(100 枚分) grads = net.numerical_gradient(x, t) # 勾配を計算 print(grads['W1'].shape) print(grads['b1'].shape) print(grads['W2'].shape) print(grads['b2'].shape)
(784, 100) (100,) (100, 10) (10,)
- 誤差逆伝播法を使って求めた勾配の結果は、数値微分による結果とほぼ同じになるが、高速に処理することができる(p117)
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch05.layer_naive import MulLayer apple = 100 # 単価 apple_num = 2 # 個数 tax = 1.1 # 消費税 # layer mul_apple_layer = MulLayer() mul_tax_layer = MulLayer() # forward apple_price = mul_apple_layer.forward(apple, apple_num) price = mul_tax_layer.forward(apple_price, tax) print(price)
220.00000000000003
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch05.layer_naive import MulLayer apple = 100 # 単価 apple_num = 2 # 個数 tax = 1.1 # 消費税 # layer mul_apple_layer = MulLayer() mul_tax_layer = MulLayer() # forward # 最後の値のクラス変数x, yをセットする apple_price = mul_apple_layer.forward(apple, apple_num) # りんごの合計価格 = 単価 * 個数 price = mul_tax_layer.forward(apple_price, tax) # 合計価格 = りんごの合計価格 * 税 # backward # backwardは値が2つに分かれるので返り値は2つある dprice = 1 dapple_price, dtax = mul_tax_layer.backward(dprice) # 引数は順伝播の際の出力変数に対する微分 dapple, dapple_num = mul_apple_layer.backward(dapple_price) # 引数は順伝播の際の出力変数に対する微分 print("dapple: ", dapple) print("dapple_num: ", dapple_num) print("dtax: ", dtax)
dapple: 2.2 dapple_num: 110.00000000000001 dtax: 200
import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from ch05.layer_naive import MulLayer, AddLayer apple = 100 apple_num = 2 orange = 150 orange_num = 3 tax = 1.1 # layer mul_apple_layer = MulLayer() mul_orange_layer = MulLayer() add_apple_orange_layer = AddLayer() mul_tax_layer = MulLayer() # forward apple_price = mul_apple_layer.forward(apple, apple_num) orange_price = mul_orange_layer.forward(orange, orange_num) all_price = add_apple_orange_layer.forward(apple_price, orange_price) price = mul_tax_layer.forward(all_price, tax) print("price", price) # backward dprice = 1 dall_price, dtax = mul_tax_layer.backward(dprice) dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) dorange, dorange_num = mul_orange_layer.backward(dorange_price) dapple, dapple_num = mul_apple_layer.backward(dapple_price) print("dapple_num: ", dapple_num) print("dapple: ",dapple) print("dorange: ",dorange) print("dorange_num: ",dorange_num) print("dtax", dtax)
price 715.0000000000001 dapple_num: 110.00000000000001 dapple: 2.2 dorange: 3.3000000000000003 dorange_num: 165.0 dtax 650
- 計算グラフの考え方をニューラルネットワークに適用する(p141)
- 活性関数として使われるReLUを、計算グラフのレイヤとして見る。順伝播時の入力である x が 0 より大きければ、逆伝播は上流の値をそのまま下流に流す。逆に、順伝播時に x が 0 以下であれば、逆伝播では下流への信号はそこでストップする(p141)
import numpy as np x = np.array([[1.0, -0.5], [-2.0, 3.0]]) print("x: ", x) mask = (x <= 0) print("mask: ", mask)
x: [[ 1. -0.5] [-2. 3. ]] mask: [[False True] [ True False]]
import numpy as np X_dot_w = np.array([[0, 0, 0], [10, 10, 10]]) B = np.array([1, 2, 3]) print("X_dot_w", X_dot_w) print("X_dot_w + B", X_dot_w + B)
X_dot_w [[ 0 0 0] [10 10 10]] X_dot_w + B [[ 1 2 3] [11 12 13]]
import numpy as np dY = np.array([[1, 2, 3], [4, 5, 6]]) print("dY: ", dY) dB = np.sum(dY, axis=0) print("dB: ", dB)
dY: [[1 2 3] [4 5 6]] dB: [5 7 9]
- 数値微分が実践的に必要とされる場面もある。誤差逆伝播法の実装の正しさを確認するとき(p161)
- ニューラルネットワークを行う処理をレイヤという単位で実装した。これらのレイヤには、forward と backward というメソッドが実装されており、データを順方向と逆方向に伝播することで、重みパラメータの勾配を効率的に求められる(p163)
- AdaGrad は、過去の勾配を 2 乗和としてすべて記録する。そのため、学習を進めれば進めるほど、更新度合いは小さくなる(p173)
- 重みの初期値を 0 にすると、正しい学習が行えない。誤差逆伝播法において、すべての重みの値が均一に(同じように)更新されてしまうため(p179)
- 隠れ層のアクティベーション(活性化関数の後の出力データ)の分布を観察することで多くの知見が得られる(p179)
- シグモイド関数の出力が 0 に近づくにつれて(または 1 に近づくにつれて)、その微分の値は 0 に近づく。そのため、0 と 1 に偏ったデータ分布では、逆伝播での勾配の値がどんどん小さくなって消える。これは勾配消失(gradient vanishing)と呼ばれる問題である(p180)
- 活性化関数によって、効果的な初期値が異なる。適度な広がりが必要。重みの初期値を適切に設定すれば、各層のアクティベーションの分布は適度な広がりを持ち、学習がスムーズに行える(p185)
- Batch Normは各層でのアクティベーションの分布を適度な広がりを持つように調整する(p187)
- ハイパーパラメータの調整用のデータは、一般に検証データ(validation data)と呼ぶ
- 全結合層の問題点は、データの形状が“無視”されてしまうこと。たとえば入力データが画像の場合、画像は通常、縦・横・チャンネル方向の3次元の形状である。全結合層に入力するときには、3次元のデータを1次元のデータにする必要がある(p207)
- 全結合層は、形状を無視して、すべての入力データを同等のニューロン(同じ次元のニューロン)として扱うので、形状に関わる情報を生かせない。畳み込み層は形状を維持する。画像の場合、入力データを3次元のデータとして受け取り、同じく3次元のデータとして、次の層にデータを出力する(p207)
- CNNでは、畳み込み層の入出力データを特徴マップということがある(p207)
- 幅1のパディングとは、周囲を幅1ピクセルの0で埋めることを言う。パディングを行う理由は、出力サイズを調整するため(p210)
- フィルターを適用する位置の間隔をストライドという。ストライドを大きくすると出力サイズは小さくなる(p211)
- 画像の場合、縦・横方向に加えてチャンネル方向も合わせた3次元のデータを扱う必要がある(p214)
- フィルターの重みデータは4次元のデータとして(output_channel, input_channel, height, width)という順に書く。たとえばチャンネル数3、サイズ5x5のフィルターが5個ある場合は(20, 3, 5, 5)と書く
- プーリングは、縦・横方向の空間を小さくする演算。たとえば、2x2の領域を1つの要素に集約するような処理をし、空間サイズを小さくする(p219)
- 大きな行列にまとめて計算することは、コンピュータで計算するうえで多くの恩恵がある。たとえば、行列計算のライブラリ(線形代数ライブラリ)などは、行列の計算実装が高度に最適化されており、大きな行列の掛け算を高速に行うことができる。そのため、行列の計算に帰着させることで、線形代数ライブラリを有効に活用できる(p223)
import numpy as np import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from common.util import im2col x1 = np.random.rand(1, 3, 7, 7) col1 = im2col(x1, 5, 5, stride=1, pad=0) print("col1: ", col1.shape) x2 = np.random.rand(10, 3, 7, 7) # 10個のデータ col2 = im2col(x2, 5, 5, stride=1, pad=0) print("col2: ", col2.shape)
col1: (9, 75) col2: (90, 75)
import numpy as np import sys, os sys.path.append(os.environ['HOME'] + "/Project/zerodeep1") from common.util import im2col class Convolution: def __init__(self, w, b, stride=1, pad=0): self.W = W self.b = b self.stride = stride self.pad = pad def forward(self, x): FN, C, FH, FW = self.W.shape N, C, H, W = x.shape out_h = int(1 + (H + 2*self.pad - FH) / self.stride) out_w = int(1 + (W + 2*self.pad - FW) / self.stride) col = im2col(x, FH, FW, self.stride, self.pad) col_W = self.W.reshape(FN, -1).T out = np.dot(col, col_W) + self.b out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) return out
- ディープラーニングは層を深くしたディープなニューラルネットワークである、という(p241)
- 手書き数字という比較的単純な問題に対しては、ネットワークの表現力をそこまで高める必要がないと考えられる。そのため、層を深くすることの恩恵が少ないと言える。大規模な一般物体認識では、問題が複雑になるため、層を深くすることが認識精度の向上に大いに貢献することが分かる(p245)
関連
- KDOC 195: 『ディープラーニングがわかる数学入門』。ディープラーニングの入門つながり