E資格の勉強③深層学習day1
目次
1 本投稿の目的
深層学習(day1)に関する学習のまとめ
2 Section1:入力層~中間層
入力層 → 中間層 → 出力層
【要点】
入力層:
ニューラルネットワークが入力を受け取る部分。
任意の数の変数(ノード)を受け取る。(x1,x2,・・xN)と表現。
中間層:
入力層→出力層の間にある層。
※入力層からの最初の出力は中間層(1層目)が受け取る。
この時、各変数に個別に設定された重み(w1,w2・・wN)を掛けた合計値(+バイアス項もある)を入力とする。
入力に対して活性化関数を実施した値を、次の層(中間層or出力層)に渡す。
【考察結果】
確認テスト1:各変数に動物分類の実例を入れてみよう。
入力層:
x1,x2,・・xN → 身長(cm)、体重(g)、耳の長さ(mm)。
中間層の入力:
上記に各重み(w)を掛けた値の合計(総入力)
※ポイント:重みは要素の重要度に依存する。重要度(高)の要素ほど重みが大きくなる。
W =[W1,W2,W3,W4]
X =[W1,W2,W3,W4]
u = W1X1 + W2X2 + W3X3 + W4X4 + b ← この数式(行列の掛け算+b)
(回答)
import numpy as np
u = np.dot(x, W) + b # np.dot(行列のドット積)
確認テスト3:中間層の出力を定義しているソースを抜き出せ。
(回答)
# 2層の総入力
u2 = np.dot(z1, W2) + b2
# 2層の総出力
z2 = functions.relu(u2)
【実装演習】
演習ソース
# 重み
W = np.array([
[0.1, 0.2, 0.3],
[0.2, 0.3, 0.4],
[0.3, 0.4, 0.5],
[0.4, 0.5, 0.6]
])
print_vec("重み", W)
# バイアス項
b = np.array([0.1, 0.2, 0.3])
print_vec("バイアス", b)
# 入力値
x = np.array([1.0, 5.0, 2.0, -1.0])
print_vec("入力", x)
# 総入力
u = np.dot(x, W) + b
print_vec("総入力", u)
# 中間層出力
z = functions.sigmoid(u)
print_vec("中間層出力", z)
実行結果
*** 重み ***
[[0.1 0.2 0.3]
[0.2 0.3 0.4]
[0.3 0.4 0.5]
[0.4 0.5 0.6]]
*** バイアス ***
[0.1 0.2 0.3]
*** 入力 ***
[ 1. 5. 2. -1.]
*** 総入力 ***
[1.4 2.2 3. ]
*** 中間層出力 ***
[0.80218389 0.90024951 0.95257413
→ 総入力 =「入力」と「重み(W)」のドット積 + バイアス項
中間層出力 = シグモイド関数(総入力)
となっている事を確認できる。
※シグモイド関数:入力を「0~1の範囲」の数値に変換する関数。後述するのでここでは詳しく記述しない。
3 Section2:活性化関数
入力層 → 中間層 → 出力層
【要点】
活性化関数:値を非線形に変換する関数。出力を次の層に渡す前に使用する。
Section1で行っていた「 W1X1 + W2X2 + W3X3 + W4X4 + b・・」は線形の処理。これを線形の出力に変換する事が目的。
中間層で利用する活性化関数。
・ステップ関数(現在はほぼ利用されていない)
ある一定の点を境に、出力が変わる関数。(0 or 1)
数式:f(u) = 1/ (1+ e^-w)
値を0~1の範囲に変換する関数。ネイピア数を使用しており、微分が容易にできる。
・ReLU関数
ある一定の点を境に、出力が変わる関数。
ある一点を満たしていない = 0
ある一点を満たす = 0~1の範囲
出力層で利用する活性化関数。※「出力層の活性化関数」を参照。
・ソフトマックス関数
・恒等写像
【考察結果】
確認テスト1:線形と非線形の違いを図に書いて簡易に説明せよ。
線形:直線としてグラフに表示できる。
非線形:直線としてグラフに表示できない。※厳密には「線形が満たす条件を満たしていない」もの。
※グラフの作成には以下を使用した。
確認テスト2:配布されたソースコードの該当する箇所を抜き出せ。
※活性化関数の使用箇所
(回答)
y = functions.sigmoid(u2)
【実装演習】
演習ソース
# 重み
W = np.array([
[0.1, 0.2, 0.3],
[0.2, 0.3, 0.4],
[0.3, 0.4, 0.5],
[0.4, 0.5, 0.6]
])
# バイアス項
b = np.array([0.1, 0.2, 0.3])
# 入力値
x = np.array([1.0, 5.0, 2.0, -1.0])
# 総入力
u = np.dot(x, W) + b
print_vec("総入力", u)
# 中間層出力
z = functions.sigmoid(u)
print_vec("中間層出力", z)
実行結果
*** 総入力 ***
[1.4 2.2 3. ]
*** 中間層出力 ***
[0.80218389 0.90024951 0.95257413
→「総入力」と「中間層出力」の違いに注目。
シグモイド関数により、値の大小関係を維持したまま、値が0~1の範囲に変換されている。
4 Section3:出力層
入力層 → 中間層 → 出力層
【要点】
出力層:
最終的なデータを出力する層。
「中間層の出力 → 次の層の入力に使用するための値」であるが、
「出力層の出力 → 人間が使用したい値」
分類問題の場合は、該当の分類である確率であったり、
回帰問題の場合は、想定される値(数値) となる。
誤差関数:
出力層の出力と、正解(ラベル)の誤差を比較する関数。
※学習で使用した誤差関数は二乗和誤差。全ての誤差を2乗したものの総和 / 2
出力層の活性化関数:
中間層の活性化関数とは異なる。(中間層とは出力する値の使い方が異なるため)
・恒等写像
回帰問題の場合に使用する。値を何も加工しない。
※ 確率などでなく、値を知りたい(出力させたい)ため、加工する必要が無い。
使用する誤差関数は2乗和誤差。
分類問題(2クラス)の場合に使用する。値を0~1の範囲に変換する関数。
使用する誤差関数はクロスエントロピー。
・ソフトマックス関数
分類問題(3クラス)の場合に使用する。値を0~1の範囲に変換する、かつ全確率の総和を1とする関数。
使用する誤差関数はクロスエントロピー。
【考察結果】
★誤差関数について★
確認テスト1:なぜ、引き算でなく2乗するか。また、「/2」はどういう意味を持つか。
(回答:2乗する理由)
符号の違いの考慮を無くすため。2乗すると、値は2乗されが、ラベルと出力の純粋な距離を取得できる。
(回答:「/2」する理由)
誤差から重みとバイアス項を修正する(誤差逆伝搬)のときに、微分を簡単にするため。本質的な意味はない。
★活性化関数について★
確認テスト1:ソフトマックス関数の数式に該当するソースコードを示し、1行ずつ処理の説明をせよ。
(ソフトマックス関数の数式)
※数式取得元は以下。
(回答 ※「★」=追加した説明コメント)
# ソフトマックス関数
def softmax(x):
# ★:ミニバッチの場合(概ね実装は同じ)
if x.ndim == 2:
x = x.T
x = x - np.max(x, axis=0)
y = np.exp(x) / np.sum(np.exp(x), axis=0)
return y.T
# ★:ミニバッチ以外の場合
x = x - np.max(x) # オーバーフロー対策
# ★:np.exp(x) → 数式の分母にあたる部分
# ★:np.sum(np.exp(x)) → 数式の分子にあたる部分
return np.exp(x) / np.sum(np.exp(x))
★誤差関数について★
確認テスト1:交差エントロピーの式に該当するソースコードを示し、1行ずつ処理の説明をせよ。
(回答 ※「★」=追加した説明コメント)
def cross_entropy_error(d, y):
if y.ndim == 1:
d = d.reshape(1, d.size)
y = y.reshape(1, y.size)
# 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
if d.size == y.size:
d = d.argmax(axis=1)
batch_size = y.shape[0]
# ★:重要なのは「-np.sum(np.log(y[np.arange(batch_size), d] + 1e-7))」
# ★:y:ラベル(正解:1、不正解:0) d:nnの出力
# ★:1e-7:対数関数の答が「-∞」になることを防止するための値。
# ★:最終的に値を「-」することでlogの「-」を正数に変換する。
return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size
【実装演習】
演習ソース:
誤差関数(2乗和誤差)の実装
# 平均二乗誤差
def mean_squared_error(d, y):
return np.mean(np.square(d - y)) / 2
誤差関数を使用
# 入力値
x = np.array([1., 2.])
network = init_network()
y, z1 = forward(network, x)
# 目標出力
d = np.array([2., 4.])
# 誤差
loss = functions.mean_squared_error(d, y)
## 表示
print_vec("出力", y)
print_vec("訓練データ", d)
print_vec("誤差", loss)
実行結果
*** 出力 ***
[1.02 2.29]
*** 訓練データ ***
[2. 4.]
*** 誤差 ***
0.9711249999999999
→yとdの誤差量が「誤差」として出力される。
5 Section4:勾配降下法
入力層 → 中間層 → 出力層
入力層 ← 中間層 ← 出力層 ※重みやバイアス項の修正に関係する。
【要点】
勾配降下法:
深層学習※のそもそも目的 → 学習結果を通じて、重さ(W)やバイアス項(B)を「最も誤差が発生しない値 = 傾きが0になる値」に修正すること。
※深層学習 = 層が4層以上存在するNN。= 中間層が2層以上存在するNN。
勾配降下法は、重さ(W)やバイアス項(B)を修正するための方法。
学習率:
誤差を反映させるときの割合。適切な値に設定する事が重要。
→大きすぎる場合:振り子を強く振ったように、振り幅がどんどん大きくなる =発散してしまう。
→小さすぎる場合:振り子を弱く振ったように、振り幅は小さい(最適解にゆっくり近づく。)が、ふり幅が弱いと、「誤差が小さいが最適解でない場所(=局所解)」から動けなくなる可能性がある。
勾配降下法の種類:
エポックごとに、学習データから一部をランダムに選んで学習する。
確率的勾配降下法は、このような事が無い。また、オンライン学習(後述の確認テストを参照)も利用可能。
・バッチ勾配降下法:
全データを一括学習 → 誤差を一気に修正する方法。
※データの追加時、全データを再度使用する必要がある。
・ミニバッチ勾配降下法:
データを一定単位のミニバッチにまとめる。
ミニバッチ単位で学習を行い、全ミニバッチの誤差の平均を用いて誤差を修正する。
→ バッチ勾配降下法は「1命令1データ」 = 並列実行が不可能であるが、
ミニバッチ勾配降下法は「1命令複数データ」 = 並列実行が可能。
【考察結果】
確認テスト1:オンライン学習とは何か。2行でまとめよ。
(回答)
学習データの追加時に、都度パラメータを修正して、学習する方法。
※全データを一気に使用するバッチ学習では行えない。(毎回全データを使用することになり、現実的でない)
確認テスト2:勾配降下法の以下数式の意味を図に書いて説明せよ。
「W(t+1) = W(t) - ε∇Et」
(回答)
※Wに関する説明なので、バイアス項(B)についての説明は省略している。
―――――――――――――――――――――――――
エポック| 重み
―――――――――――――――――――――――――
t | Wt →→↓
| ↓ - ε∇Et (※誤差×学習率 = 改善)
t+1 | Wt+1 ← ※前回のWに改善を行った値=次エポックのW
・ | ・
t+n | Wt+n
―――――――――――――――――――――――――
※考察
この説明については「超AI入門講座」の「スタートテスト(一般)」で見た記憶がある。万一分からない場合、「超AI入門講座」にて復習する事にする。
6 Section5:誤差逆伝搬法
入力層 → 中間層 → 出力層
入力層 ← 中間層 ← 出力層 ※重みやバイアス項の修正に関係する。
【要点】
数値微分で更新量を求めると、入力層側ほど、更新量を求めるのが大変。
出力層側の同じ計算を何回も行うことになる・・。
誤差逆伝搬では、一度求めた更新量を再利用して、更新量を簡単に、高速に求める事ができる。(微分の連鎖率を使用する)
※この説明については「超AI入門講座」で分かりやすい解説がある。
連鎖率はプログラム関係ない数学の知識。以下が参考になる。
※このサイトは数学に関して何かとお勧めである。
【考察結果】
確認テスト1:誤差逆伝搬法では不要な再帰処理を避けることができる。既に行った計算方法を保持しているソースコードを抽出せよ。
(回答 ※「★」=追加した説明コメント)
# 誤差逆伝播
def backward(x, d, z1, y):
print("\n##### 誤差逆伝播開始 #####")
grad = {}
W1, W2 = network['W1'], network['W2']
# 出力層でのデルタ
delta2 = functions.d_sigmoid_with_loss(d, y) ★ここ。「delta2」★
# b2の勾配
grad['b2'] = np.sum(delta2, axis=0)
# W2の勾配
grad['W2'] = np.dot(z1.T, delta2)
# 中間層でのデルタ
delta1 = np.dot(delta2, W2.T) * functions.d_relu(z1) ★「delta2」を流用★
→delta2を流用して、中間層の更新量の取得に使用している。
これにより、計算量を「層の数-1」に抑えることができる。
単純な数値微分では、計算量は「(層の数+1) × 層数/2」になる。
※いわゆるガウスの足し算。
①∂E/∂y ∂y/∂u
(回答)
delta2 = functions.d_mean_squared_error(d, y)
②∂E/∂y ∂y/∂u ∂u/∂w2ji
(回答)
# W2の勾配
grad['W2'] = np.dot(z1.T, delta2)
→上記は微分の連鎖例なので「/」に注意。割り算でない。「a/b」→「aをbで微分した物」という意味。あとは、重み(W)とバイアス項(B)の微分を行う際、偏微分を行っていることに注意。(分からないなら、「超AI入門講座」を再確認する)