くますきIT日記

IT系資格、競技プログラミングの情報を書いていきます。

E資格の勉強③深層学習day1

目次

1 本投稿の目的

深層学習(day1)に関する学習のまとめ

2 Section1:入力層~中間層

入力層 → 中間層 → 出力層

【要点】

入力層:

 ニューラルネットワークが入力を受け取る部分。

 任意の数の変数(ノード)を受け取る。(x1,x2,・・xN)と表現。

中間層: 

 入力層→出力層の間にある層。

 ※入力層からの最初の出力は中間層(1層目)が受け取る。

 この時、各変数に個別に設定された重み(w1,w2・・wN)を掛けた合計値(+バイアス項もある)を入力とする。

 入力に対して活性化関数を実施した値を、次の層(中間層or出力層)に渡す。

 

【考察結果】

確認テスト1:各変数に動物分類の実例を入れてみよう。

入力層:

 x1,x2,・・xN → 身長(cm)、体重(g)、耳の長さ(mm)。

中間層の入力:

 上記に各重み(w)を掛けた値の合計(総入力)

 ※ポイント:重みは要素の重要度に依存する。重要度(高)の要素ほど重みが大きくなる。

 

確認テスト2:以下数式をPythonで書け。

 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:線形と非線形の違いを図に書いて簡易に説明せよ。

 

線形:直線としてグラフに表示できる。

f:id:you_it_blog:20211012233852p:plain

非線形:直線としてグラフに表示できない。※厳密には「線形が満たす条件を満たしていない」もの。

f:id:you_it_blog:20211012234114p:plain

※グラフの作成には以下を使用した。

www.geogebra.org

 

確認テスト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行ずつ処理の説明をせよ。

(ソフトマックス関数の数式)

f:id:you_it_blog:20211013110236p:plain

※数式取得元は以下。

ja.wikipedia.org

(回答 ※「★」=追加した説明コメント)

# ソフトマックス関数
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) dnnの出力
# ★: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)を修正するための方法。

  

学習率: 

 誤差を反映させるときの割合。適切な値に設定する事が重要。

  →大きすぎる場合:振り子を強く振ったように、振り幅がどんどん大きくなる =発散してしまう。

  →小さすぎる場合:振り子を弱く振ったように、振り幅は小さい(最適解にゆっくり近づく。)が、ふり幅が弱いと、「誤差が小さいが最適解でない場所(=局所解)」から動けなくなる可能性がある。

 

勾配降下法の種類:

確率的勾配降下法(SGD):

 エポックごとに、学習データから一部をランダムに選んで学習する。

 確率的勾配降下法は、このような事が無い。また、オンライン学習(後述の確認テストを参照)も利用可能。

・バッチ勾配降下法:

 全データを一括学習 → 誤差を一気に修正する方法。

 ※データの追加時、全データを再度使用する必要がある。

・ミニバッチ勾配降下法:

 バッチ学習と確率的勾配降下法(SGD)の混合のような手法。

 データを一定単位のミニバッチにまとめる。

 ミニバッチ単位で学習を行い、全ミニバッチの誤差の平均を用いて誤差を修正する。

 → バッチ勾配降下法は「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入門講座」で分かりやすい解説がある。

連鎖率はプログラム関係ない数学の知識。以下が参考になる。

※このサイトは数学に関して何かとお勧めである。

manabitimes.jp

 

 

 

 

【考察結果】

確認テスト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」になる。

 ※いわゆるガウスの足し算。

mosipro.com

 

確認テスト2: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入門講座」を再確認する)