【E資格】深層学習①

I 深層学習Day1

 ニューラルネットワークとは様々な情報(入力)から内部(中間層)で重みとバイアスの調整を実施して最終的な結果(出力)を出すネットワークのことである。もう少し詳細を見てみるとある動物に対して、体長や耳の大きさ、足の長さなどの情報から犬なのか猫なのかを出力するイメージである。
 ニューラルネットワークは大きく分けて二つのことが可能である。「回帰」と「分類」である。回帰では連続する実数値を取る関数の近似を行い、分類では離散的な結果を予想することを指す。以降では、回帰問題・分類問題について、構成する各層についての詳細や学習方法も含めて整理する。

確認テスト NN全体像①

ディープラーニングは、結局何をやろうとしているのか2行以内で述べよ。また、次の中のどの値の最適化が最終目的か。全て選べ。
①入力値[X] ②出力値[Y] ③重み[W] ④バイアス[b] ⑤総入力[u] ⑥中間層入力[z] ⑦学習率[ρ]


★解答
 DLの目的:「多層の中間層をもつニューラルネットワークを用いて、入力値を出力値に変換する数学モデルを構築する」
 最適化するパラメータ:④バイアス[b] ⑥中間層入力[z]

確認テスト NN全体像②

 次のネットワークを図にせよ。
入力層:2ノード1層/中間層:3ノード2層/出力層:1ノード1層


★解答

1. 入力層〜中間層

 入力層から中間層の処理では2つの処理を行う。まず、複数の入力に対応する重みをかけたものを足し合わせて最終的にバイアスを足したものを中間層に渡す。中間層では入力層から受け取った値に活性化関数に通して次の層へと値を渡す。簡潔に言うのであれば、「入力である複数の情報に重みをかけて一つにまとめ、中間層で活性化関数に通す」といった手順である。数式で表現すると、入力をx、重みをw、バイアスをb、中間層での出力をz、総入力をuとすると

$$\begin{eqnarray}
u &=& w_1x_1+w_2x_2+\cdot\cdot\cdot+b\\
&=&{\bf W\cdot x}+{\bf b} (1-1)
\end{eqnarray}$$

となる。図示すると以下のようになる。

実装演習

入力層から中間層の処理部分のソースコードについて記載する。

import numpy as np
from common import functions

#重み行列の定義
#ここではわかりやすいように、全ての値を定義した。実際は乱数などを用いる。
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
# 中間層出力
z = functions.sigmoid(u)
#common/functions.py sigmoid(x)
def sigmoid(x):
    return 1/(1 + np.exp(-x))

確認テスト

確認テスト 入力層〜中間層①

入力層〜中間層の図式に動物分類の実例を入れる。


★解答

確認テスト 入力層〜中間層②

(1-1)式をPythonで記載する。


u1 = np.dot(x, W1) + b1
確認テスト 入力層〜中間層③

中間層の出力を定義しているソースを抜き出す。


★解答

# 中間層出力
z = functions.sigmoid(u)

2. 活性化関数

 活性化関数とは、ニューラルネットワークにおいて、次の層への出力の大きさを決める非線形の関数である。入力値に応じて、次の層への信号のON/OFFや強弱を定める働きをもつ。
 以下に、中間層と出力層でよく用いられる活性化関数を示す。

  • 中間層用の活性化関数(本章で説明
    • ReLU関数
    • シグモイド(ロジスティック)関数
    • ステップ関数
  • 出力層用の活性化関数(3. 出力層に詳細を記載
    • ソフトマックス関数
    • 恒等写像
    • シグモイド(ロジスティック)関数

 ステップ関数は出力は常に1か0の階段状の関数であるが、線形分離可能なものしか学習できないため、用いられることは少ない。
 シグモイド関数については機械学習でも用いたが、

$$ f(u) = \frac{1}{1+e^{-\mu}}$$

となる関数である。信号の強弱を伝えられるため、ニューラルネットワーク普及のきっかけとなったが、大きな値では出力の変化が微小なため、勾配消失問題を引き起こすことがあった。
 RELU関数は

$$ f(x) = \left\{
\begin{array}{ll}
x & (x \gt 0) \\
0 & (x \leq 0)
\end{array}
\right. $$

となる関数である。これは今最も用いられている活性化関数であり、勾配消失問題の回避とスパース化に貢献することで良い成果をもたらしている。

実装演習

ここでは順伝播の中での活性化関数の利用についての実装結果を示す。ただし、重みなどの初期化は別途実装していることを仮定している。また、numpyと関数定義(/common/functions.py)をインクルードしている。

def forward(network, x):
    #重みとバイアスをネットワークに用意
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    u1 = np.dot(x, W1) + b1  # 1層の総入力
    z1 = functions.relu(u1)  # 1層の総出力
    
    u2 = np.dot(z1, W2) + b2  # 2層の総入力
    z2 = functions.relu(u2)   # 2層の総出力

    u3 = np.dot(z2, W3) + b3  # 出力層の総入力
    y = u3                    # 出力層の総出力
    
    return y, z1, z2
# ./common/functions.py relu(x)
def relu(x):
    return np.maximum(0, x)

確認テスト

確認テスト 活性化関数①

線形と非線形の違いを図に書いて簡易に説明せよ。


★解答
ざっくりとした言い方としては、グラフが直線的であるか否か。具体的には関数fが線形である場合以下の2点を満たす。

加法性:f(x+y) = f(x) + f(y)
斉次性:f(kx) = kf(x)

図示すると以下のようになる。

確認テスト 活性化関数②

中間層の出力である、以下の数式に該当するPythonコードをかけ。

$$ {\bf z} = f({\bf u}) $$

ただし、ソースコード


★解答

#中間層の出力
z1 = functions.sigmoid(u)
#functions.sigmoid
def sigmoid(x):
    return 1/(1 + np.exp(-x))

3. 出力層

 出力層ではNNから出力されるあたいと教師データ(正解値)をもとに誤差を計算する。誤差を求める際には二乗誤差関数や交差エントロピーが用いられる。前者は回帰問題の場合、後者は分類問題に対してよく用いられる。二乗誤差関数は、NNの出力をy、教師データをdとすると

$$ E_n(w) = \frac{1}{2}\sum_{j=1}^J(y_j-d_j)^2=\frac{1}{2}||({\bf y}-{\bf d})||^2 $$

と表記される。一方で交差エントロピーは

$$ E_n(w) = \sum_{i=1}^l(-d_i \log y_i) $$

と表記される。各々の意味については確認テストにて説明する。

 次に、出力層でよく用いられる活性化関数について説明する。恒等写像については出力をそのまま写像する処理である。ソフトマックス関数は

$$f(i,u) = \frac{e^{u_i}}{\sum_{k=1}^Ke^{u_k}} $$

である。この関数の意味についても確認テストで説明する。

実装演習

活性化関数や誤差関数については、ライブラリを使用する場合は関数名を指定するだけで実行できることと、直接関数を記述する際も関数をまとめたソースを作って使用することが多い。そのため、ここではPythonで各種活性化関数を記載する。NNに実装する際は「出力=活性化関数名(入力)」または「誤差=誤差関数名(教師、出力)」とすればよい。

import numpy as np
# シグモイド関数(ロジスティック関数)
def sigmoid(x):
    return 1/(1 + np.exp(-x))

# ReLU関数
def relu(x):
    return np.maximum(0, x)

# ソフトマックス関数
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) # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x))

# 平均二乗誤差
def mean_squared_error(d, y):
    return np.mean(np.square(d - y)) / 2

# クロスエントロピー
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]
    return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size

確認テスト

確認テスト 出力層①

二乗誤差関数について
・なぜ、引き算ではなく二乗するか述べよ
・1/2にはどういう意味をもつか述べよ


★解答
・引き算では負の誤差を正の誤差を足しあわした際に打ち消してしまうため、二乗をとる。
・極端な場合1/5でも1/13でもなんでも良いが、5.で説明する誤差逆伝播法では誤差関数の微分を行うため、このときにファクターが1/2×2=1で計算が簡単になるため

確認テスト 出力層②

ソフトマックス関数について、ソースコードを示し、処理の説明をせよ。

$$ f(i, u) = \frac{e^{u_i}}{\sum_{k=1}^K e^{u_k}} $$


★解答

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) # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x))

if文部分についてはミニバッチを考慮した際に、入力に入ってくるxの形が変わってくるため分岐している。softmax関数として、本質的な意味をもつのは最後のreturn文部分。3クラス以上の時に全クラスを足したら1になるような確率を出力するため、分母でsumをとっている。

確認テスト 出力層③

交差エントロピーについてソースコードを示し、処理の説明をせよ。

$$ E_n({\bf w}) = -\sum_{i=1}^I d_i\log y_i $$


★解答

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]
    return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size

本質的な部分は最後のreturn文。最初のif文は出力が1次元の場合の処理、入れ子になっているif文はコメント文にも記載しているが、教師データがone-hot-vectorの場合の処理。return 文にで数式と同じように、dlog(y)を足し合わせている。微小な値を足しているのは数値計算の便宜上。

4. 勾配降下法

 深層学習の目的は学習を通して誤差を最小にするネットワークを作成することである。つまり、誤差を最小にするパラメータ(重み:w)を見つけることが目的である。これを行う手法が勾配効果法である。勾配降下法では以下の式でパラメータの最適化を行う。

$$\begin{eqnarray}
{\bf w}^{(t+1)}&=&{\bf w}^{(t)}-\epsilon\nabla E\\
\nabla E &=& \frac{\partial E}{\partial {\bf w}}=\left[\frac{\partial E}{\partial w_1}\cdot\cdot\cdot\frac{\partial E}{\partial w_M}\right]
\end{eqnarray}$$

ただしεは学習率を表す。学習率の値によって学習の効率が大きく異なる。学習率が大きい場合、最小値に辿り着かずに発散してしまい、小さすぎる場合は発散することはないが、収束するまでに時間がかかってしまったり、局所最適解に留まってしまったりする。学習率の決定や収束性向上のための有名なアルゴリズムを以下に列挙する(詳細はII深層学習Day2で説明する)

  • Momentum
  • AdaGrad
  • Adadelta
  • Adam

以下ではサンプルの取り方を工夫した勾配効果法の種類について二つ説明する。
 まず確率的勾配降下法(SGD)はランダムに抽出したサンプルの誤差を用いる手法である。(単純な勾配降下法は全サンプルの平均誤差を用いている)

$$ {\bf w}^{(t+1)} = {\bf w}^{(t)} - \epsilon\nabla E_n $$

SGDを用いることで、データが冗長な場合の計算コストの軽減を行うことができ、局所極小解に収束するリスクを軽減できる。またオンライン学習が可能になる。
 次に、ミニバッチ勾配降下法について説明する。ミニバッチ勾配降下法はランダムに分割したデータの集合(ミニバッチ)Dt に属するサンプルの平均誤差を用いる手法である。

$$\begin{eqnarray}
{\bf w}^{(t+1)} &=& {\bf w}^{(t)} - \epsilon \nabla E_t\\
E_t &=& \frac{1}{N_t}\sum_{n\in D_t}E_n \\
N_t &=& |D_t|
\end{eqnarray}$$

ミニバッチ勾配降下法ではSGDのメリットを損なわず、計算機の計算資源を有効活用することが可能rとなる。つまりCPUのスレッドの並列化やGPUのSIMD(Single Instruction Multi Data)が可能となる。

実装演習

確率的勾配降下法の一部

#学習部分のみ抜粋(forward, backwardは順伝播、逆伝播を表す)

epoch = 1000 #抽出数
network = init_network() #ネットワークの初期化
#ランダムにサンプル抽出
random_datasets = np.random.choice(data_sets, epoch)

for dataset in random_datasets:
    x, d = dataset['x'], dataset['d']
    z1, y = forward(network, x)
    grad = backward(x, d, z1, y)
    # パラメータに勾配適用
    for key in ('W1', 'W2', 'b1', 'b2'):
        network[key]  -= learning_rate * grad[key]

    # 誤差
    loss = functions.mean_squared_error(d, y)
    losses.append(loss)

ミニバッチ法の一部

#学習部分のみ抜粋

for i in range(iters_num):
    # ランダムにバッチを取得    
    batch_mask = np.random.choice(train_size, batch_size)
    # ミニバッチに対応する教師訓練画像データを取得    
    x_batch = x_train[batch_mask]
    # ミニバッチに対応する訓練正解ラベルデータを取得する
    d_batch = d_train[batch_mask]


    
    z1, y = forward(network, x_batch)
    grad = backward(x_batch, d_batch, z1, y)

    # パラメータに勾配適用
    for key in ('W1', 'W2', 'b1', 'b2'):
        network[key]  -= learning_rate * grad[key]

確認テスト

確認テスト 勾配降下法①

パラメータの最適化部分に対応するPythonソースコードを記載せよ。


★解答

network[key] -= learning_rate * grad[key]
grad = backward(x, d, z1, y)

ただし、networkは重み、バイアスを記載したネットワークを表し、grad[]は誤差逆伝播を表す。

確認テスト 勾配降下法②

オンライン学習とは何か。


モデルに一度に全てのデータを与えて学習するのではなく、少しずつデータを与えて学習する手法。データが与えられるたびにパラメータの更新が可能である。

確認テスト 勾配降下法③

ミニバッチ勾配降下法の以下の数式の意味を図に書いて説明せよ。

$$ {\bf w}^{(t+1)} = {\bf w}^{(t)} - \epsilon \nabla E_t $$


★解答

5. 誤差逆伝播法

誤差逆伝播法は計算結果(=誤差)から微分を逆算することで、不要な再帰的計算を避けて微分を算出する手法である。以下の図のように、逆伝播を考える際には微分の連鎖率を用いることで、再帰計算を防ぐことが可能となる。

実装演習

誤差逆伝播に係るソースコードを記載する(関数化している)

#誤差逆伝播関数(入力, 教師, 中間層, 出力層)
#出力として、パラメータの勾配を返す
def backward(x, d, z1, y):

    grad = {}

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    #dE/du2
    delta2 = functions.d_sigmoid_with_loss(d, y)
    grad['b2'] = np.sum(delta2, axis=0)
    grad['W2'] = np.dot(z1.T, delta2)
    #dE_du2
    delta1 = np.dot(delta2, W2.T) * functions.d_relu(z1)
    grad['b1'] = np.sum(delta1, axis=0)
    grad['W1'] = np.dot(x.T, delta1)
        
    return grad

確認テスト

確認テスト 誤差逆伝播法①

誤差逆伝播法では不要な再帰的処理を避けることができる。すでに行った計算結果を保持しているソースコードを記載せよ。


★解答

# 出力層でのデルタ
delta2 = functions.d_sigmoid_with_loss(d, y)
# 中間層でのデルタ
delta1 = np.dot(delta2, W2.T) * functions.d_relu(z1)

delta1でdelta2の計算結果を用いることで再帰処理を減らしている。

確認テスト 誤差逆伝播法②

以下の数式に対応するソースコードを記述せよ。

$$
\frac{\partial E}{\partial {\bf y}}\frac{\partial{\bf y}}{\partial {\bf u}}, \\
\frac{\partial E}{\partial {\bf y}}\frac{\partial{\bf y}}{\partial {\bf u}}\frac{\partial{\bf u}}{\partial w}$$


delta2 = functions.d_mean_squared_error(d, y)
grad['W2'] = np.dot(z1.T, delta2)

ただし、ここで用いられるz1は以下で生成される。

z1, y = forward(network, x)

II 深層学習Day2

 ここでは深層モデルのための学習手法(学習率の最適化、過学習の抑制)と、畳み込みNNについてまとめる。

1. 勾配消失問題

 勾配消失問題は誤差逆伝播法が下位層に進んでいくにつれて、勾配が徐々に緩やかになっていく問題である。このため、パラメータの更新時に下位層ではパラメータの値はほとんど変わらず最適値に収束しなくなる。

 勾配消失問題を引き起こす代表的な活性化関数はシグモイド関数である。関数の特徴として大きな値では出力の変化が微小であることや、シグモイド関数の微分の最大が0.25であることが理由である。

 ここでは勾配消失問題の解消方法として以下の3つ観点で説明する。

  • 活性化関数の選択
  • 重みの初期値設定
  • バッチ正規化

 まず、勾配消失問題の回避によく使われる活性化関数としてReLU関数がある。

$$ f(x) = \left\{
\begin{array}{ll}
x & (x \gt 0) \\
0 & (x \leq 0)
\end{array}
\right. $$

関数の微分が1か0であるため、重みが伝わるか伝わらないかの選択がシビアに行われる。これにより、勾配消失問題の回避とスパース化に貢献することで良い成果をもたらしている。

 次に、重みの初期値設定としてXavierとHeについて説明する。Xavierでは初期値を設定する際の活性化関数としてReLU関数、シグモイド関数、双曲線正接関数が用いられる。実際の初期値の設定方法としては「標準正規分布/sqrt(前の層のノード数)」とする。特徴的なのは分母の前の層のノード数の平方根を用いている部分である。例えば、標準正規分布で重みを初期化すると、出力は0か1に寄ってしまう。この場合は微分値は0に近く、勾配消失問題を引き起こしてしまう。一方でXavierなら出力は0か1に極端にはよらなくなり、勾配消失問題の回避となる。

 Heでは初期値を設定する活性化関数としてReLU関数を用いる。考え方としてはXavierに似ているが、Heでは「標準正規分布の重み×sqrt(2/前の層のノード数) 」とする。ReLU関数を活性化関数に用いる場合では、標準正規分布で重みを初期化すると、出力はほぼ0になってしまう。一方で、Heでは0には比較的偏りはするものの、1に向かっても表現力をもつため、勾配消失問題の回避策となる。

 最後にバッチ正規化について説明する。バッチ正規化とは、ミニバッチ単位で、入力値のデータの偏りを抑制する手法である。実装方法は、活性化関数に値を渡す前後に、バッチ正規化の処理を含んだ層を加える。バッチ正規化の数学的記述について以下に示す。1.〜3.までは統計の正規化であり、4.で変倍・移動させるのが特徴である。4.においてγはスケーリングパラメータ、βはシフトパラメータと呼ばれる。

$$\begin{eqnarray}
1. ミニバッチの平均 &:& \mu_t = \frac{1}{N_t}\sum^{N_t}_{i=1}x_{ni}\\
2. ミニバッチの分散 &:& \sigma_t^2 = \frac{1}{N_t}\sum^{N_t}_{i=1}(x_{ni}-\mu_t)^2\\
3. ミニバッチの正規化 &:& \hat{x}_{ni} = \frac{x_{ni}-\mu_t}{\sqrt{\sigma_t^2+\theta}}\\
4. 変倍\cdot移動&:& y_{ni}=\gamma x_{ni}+\beta
\end{eqnarray}

実装演習

mnistを利用した学習について、重みの初期値設定でXavierを利用した場合とHeを利用した場合の比較を行う。(源泉データ:http://yann.lecun.com/exdb/mnist/
XavierとHeでは重みの初期化と活性化関数以外は共通しているため、以下のソースではHeについて示している。Xavierを適用する場合はコメントアウトを外してHe部分をコメントアウトすれば良い。

#Xavier
from typing import AsyncContextManager
import numpy as np
from data.mnist import load_mnist
from PIL import Image
import pickle
from common import functions
import matplotlib.pyplot as plt

#mnistのロード
(x_train, d_train), (x_test, d_test) = load_mnist(normalize = True, one_hot_label = True)
train_size = len(x_train)

input_layer_size = 784 #入力層サイズ
hidden_layer_1_size = 40 #中間層サイズ
hidden_layer_2_size = 20 #中間層サイズ
output_layer_size = 10   #出力層サイズ

iters_num =  2000 #繰り返し数
batch_size = 100 #ミニバッチサイズ
lerning_rate = 0.1 #学習率
plot_interval = 10 #描写頻度

#ネットワークの初期化
#重みの初期化を考えるならここが重要
def init_network():
    network = {}
    
    ##Xavier
    #network['W1'] = np.random.randn(input_layer_size, hidden_layer_1_size) / (np.sqrt(input_layer_size))
    #network['W2'] = np.random.randn(hidden_layer_1_size, hidden_layer_2_size) / (np.sqrt(hidden_layer_1_size))
    #network['W3'] = np.random.randn(hidden_layer_2_size, output_layer_size) / (np.sqrt(hidden_layer_2_size))

    ## He
    network['W1'] = np.random.randn(input_layer_size, hidden_layer_1_size) / (np.sqrt(input_layer_size)) * np.sqrt(2)
    network['W2'] = np.random.randn(hidden_layer_1_size, hidden_layer_2_size) / (np.sqrt(hidden_layer_1_size)) * np.sqrt(2)
    network['W3'] = np.random.randn(hidden_layer_2_size, output_layer_size) / (np.sqrt(hidden_layer_2_size)) * np.sqrt(2)      


    network['b1'] = np.zeros(hidden_layer_1_size)
    network['b2'] = np.zeros(hidden_layer_2_size)
    network['b3'] = np.zeros(output_layer_size)

    return network

#順伝搬
def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    ##Xavier
    #hidden_f = functions.sigmoid

    ##He
    hidden_f = functions.relu

    u1 = np.dot(x, W1) + b1
    z1 = hidden_f(u1)
    u2 = np.dot(z1, W2) + b2
    z2 = hidden_f(u2)
    u3 = np.dot(z2, W3) + b3
    y  = functions.softmax(u3)

    return z1, z2, y

#誤差逆伝播
def backward(x ,d, z1, z2, y):
    grad = {}

    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
   
    ##Xavier
    #hidden_d_f = functions.d_sigmoid

    ##He
    hidden_d_f = functions.d_relu

    delta3 = functions.d_softmax_with_loss(d, y)
    grad['b3'] = np.sum(delta3, axis = 0)
    grad['W3'] = np.dot(z2.T, delta3)
    delta2 = np.dot(delta3, W3.T) * hidden_d_f(z2)
    grad['b2'] = np.sum(delta2, axis = 0)
    grad['W2'] = np.dot(z1.T, delta2)
    delta1 = np.dot(delta2, W2.T) * hidden_d_f(z1)
    grad['b1'] = np.sum(delta1, axis = 0)
    grad['W1'] = np.dot(x.T, delta1)

    return grad

# パラメータの初期化
network = init_network()
accuracies_train = []
accuracies_test = []

#正解率
def accuracy(x, d):
    z1, z2, y = forward(network, x)
    y = np.argmax(y, axis = 1)
    if d.ndim != 1 : d = np.argmax(d, axis = 1)
    accuracy = np.sum(y == d) / float(x.shape[0])
    
    return accuracy

#学習
for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    d_batch = d_train[batch_mask]

    z1, z2, y = forward(network, x_batch)
    grad = backward(x_batch, d_batch, z1, z2, y)

    ###正解率の変化
    if (i+1)%plot_interval==0:
        accr_test = accuracy(x_test, d_test)
        accuracies_test.append(accr_test)

        accr_train = accuracy(x_batch, d_batch)
        accuracies_train.append(accr_train)
    
    ###パラメータに勾配適用
    for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
        network[key] -= lerning_rate * grad[key]

#後処理
lists = range(0, iters_num, plot_interval)
plt.plot(lists, accuracies_train, label = "training set")
plt.plot(lists, accuracies_test, label = "test set")
plt.legend()
plt.title("accuracy (He)")
plt.xlabel("count")
plt.ylabel("accuracy")

plt.savefig("He.png")
plt.clf()

学習の経過は以下の図のようになる。

XavierをHeのどちらも問題なく学習が進んでいる。Heの方が、このデータセットの場合は学習速度が早いことがわかる。

バッチ正規化については、用いるクラスの抜粋を以下に示す。

import numpy as np
from collections import OrderedDict
from common import layers
from data.mnist import load_mnist
import matplotlib.pyplot as plt
from multi_layer_net import MultiLayerNet
from common import optimizer

# バッチ正則化 layer
class BatchNormalization:
    '''
    gamma: スケール係数
    beta: オフセット
    momentum: 慣性
    running_mean: テスト時に使用する平均
    running_var: テスト時に使用する分散
    '''
    def __init__(self, gamma, beta, momentum=0.9, running_mean=None, running_var=None):
        self.gamma = gamma
        self.beta = beta
        self.momentum = momentum
        self.input_shape = None

        self.running_mean = running_mean
        self.running_var = running_var  
        
        # backward時に使用する中間データ
        self.batch_size = None
        self.xc = None
        self.std = None
        self.dgamma = None
        self.dbeta = None

    def forward(self, x, train_flg=True):
        if self.running_mean is None:
            N, D = x.shape
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)
                        
        if train_flg:
            mu = x.mean(axis=0) # 平均
            xc = x - mu # xをセンタリング
            var = np.mean(xc**2, axis=0) # 分散
            std = np.sqrt(var + 10e-7) # スケーリング
            xn = xc / std
            
            self.batch_size = x.shape[0]
            self.xc = xc
            self.xn = xn
            self.std = std
            self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * mu # 平均値の加重平均
            self.running_var = self.momentum * self.running_var + (1-self.momentum) * var #分散値の加重平均
        else:
            xc = x - self.running_mean
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))
            
        out = self.gamma * xn + self.beta 
        
        return out

    def backward(self, dout):
        dbeta = dout.sum(axis=0)
        dgamma = np.sum(self.xn * dout, axis=0)
        dxn = self.gamma * dout
        dxc = dxn / self.std
        dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis=0)
        dvar = 0.5 * dstd / self.std
        dxc += (2.0 / self.batch_size) * self.xc * dvar
        dmu = np.sum(dxc, axis=0)
        dx = dxc - dmu / self.batch_size
        
        self.dgamma = dgamma
        self.dbeta = dbeta

        return dx    

確認テスト

確認テスト 勾配消失問題①

連鎖率の原理を使い、dz/dxを求めよ。

$$\begin{eqnarray}
z&=&t^2\\
t&=& x+y
\end{eqnarray}$$


★解答

$$ \frac{dz}{dx}=2(x+y) $$

確認テスト 勾配消失問題②

シグモイド関数を微分した時、入力値が0の時に最大値をとる。その値として正しいものを選択肢から選べ。
(1)0.15, (2)0.25, (3) 0.35, (4)0.45


★解答
シグモイド関数をσ(x)とすると、その微分はσ(x)×(1-σ(x))となる。σ(0)=0.5であることと、シグモイド関数の微分はx=0で最大となることから答えは(2)である。

確認テスト 勾配消失問題③

重みの初期値に0を設定すると、どのような問題が発生するか。簡潔に説明せよ。


★解答
重みの初期値を0とすると、全ての重みが均一に更新されるため、複数の層を用いている利点がなくなる。

確認テスト 勾配消失問題④

一般的に考えられるバッチ正則化の効果を2点挙げよ。


★解答
・中間層の重みの更新が安定化することで、学習スピードがあがる。
・正規化によって、学習データの極端なばらつきを少なくするため、過学習を防ぐ。

2. 学習率最適化手法

 深層学習では、学習を通して誤差を最小にするネットワークを作成するために勾配降下法を用いてパラメータを最適化している。この時に学習率の値によって学習のスピードが大きく変わる。学習率の値が大きいを最適値にたどりつかずに発散してしまい。一方で学習率が小さいと収束に時間がかかり、大域局所最適値に収束しづらい。ここでは以下の4つの学習率最適化手法について説明する。

  • モメンタム
  • AdaGrad
  • RMSProp
  • Adam

 モメンタムは、誤差をパラメータで微分したものと学習率の積を減算したのちに、現在の重みを減算した値と慣性の積を加算する。坂を転がりながら最適解を探しているとしたら、直前のスピードが早ければ次の一歩も大きく、遅ければ次の一歩も小さいようなイメージである。

$$ \begin{eqnarray}
V_t = \mu V_{t-1}-\epsilon\nabla E\ \ \ (\mu:慣性)\\
{\bf w}^{(t+1)}={\bf w}^{(t)}+V_t
\end{eqnarray}$$

モメンタムのメリットとして、局所最適化にはなりにくく、大域的最適解となる。谷間についてから最適値に到達するまでの時間は短いことが挙げられる。

 次にAdaGradについて説明する。AdaGradは誤差をパラメータで微分したものと再定義した学習率の積を減算する手法である。大まかには、これまでの重みの勾配量を経験として導入する手法である。

$$\begin{eqnarray}
h_0 &=& \theta \\
h_t = h_{t-1}+(\nabla E)^2\\
{\bf w}^{(t+1)}&=&{\bf w}^{(t)}-\epsilon\frac{1}{\sqrt{h_t}+\theta}\nabla E
\end{eqnarray}$$

AdaGradのメリットは勾配の緩やかな斜面に対して、最適解に近づける点がある。一方で、学習率が徐々に小さくなるために、鞍点問題を引き起こしやすいことが課題である。

 次にRMSPropについて説明する。RMSPropは誤差をパラメータで微分したものと再定義した学習率の積を減算する手法である。

$$\begin{eqnarray}
h_t &=& \alpha h_{t-1}+(1-\alpha)(\nabla E)^2\\
{\bf w}^{(t+1)}&=&{\bf w}^{(t)}-\epsilon\frac{1}{\sqrt{h_t}+\theta}\nabla E
\end{eqnarray}$$

ここでαは前回までの勾配情報を加味する割合を表す。RMSPropのメリットは局所最適解にはならず、大域的最適解になることと、ハイパーパラメータの調整が必要な場合が少ないことが挙げられる。

 最後にAdamはモメンタムの過去勾配の指数関数的元帥平均とRMSPropの過去の勾配の二乗の指数関数的減衰平均の二つを組み合わせた最適化アルゴリズムである。ここまでで述べたモメンタムとRMSPropを組み合わせたのみなので、実装演習で中身を確認する。

実装演習

Momentum, AdaGrad. RMSProp, Adamについてそれぞれの実装結果を以下に示す。学習率最適化部分以外は共通しているため、コメントアウト部分を外せば全ての場合について調べることが可能である。

import sys, os
import numpy as np
from collections import OrderedDict
from common import layers
from data.mnist import load_mnist
import matplotlib.pyplot as plt
from multi_layer_net import MultiLayerNet


# データの読み込み
(x_train, d_train), (x_test, d_test) = load_mnist(normalize=True, one_hot_label=True)

use_batchnorm = False

network = MultiLayerNet(input_size=784, hidden_size_list=[40, 20], output_size=10, activation='sigmoid', weight_init_std=0.01,
                       use_batchnorm=use_batchnorm)

iters_num = 1000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01
# 慣性(Momentum 以外では不要)
momentum = 0.9
# decay_late (RMSProp以外では不要)
decay_late = 0.8
# beta(Adam 以外では不要)
beta1 = 0.9
beta2 = 0.999

train_loss_list = []
accuracies_train = []
accuracies_test = []

plot_interval=10

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    d_batch = d_train[batch_mask]

    # 勾配
    grad = network.gradient(x_batch, d_batch)
    if i == 0:
#Momentum        v = {}
#AdaGrad, RMSProp        h = {}
        m = {}
        v = {}
    learning_rate_t = learning_rate * np.sqrt(1.0 - beta2 ** (i+1)) / (1.0 - beta1 ** (i+1))
    for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
        if i == 0:
#Momentum            v[key] = np.zeros_like(network.params[key])
#AdaGrad            h[key] = np.full_like(network.params[key], 1e-4)
#RMSProp            h[key] = np.zeros_like(network.params[key])
            m[key] = np.zeros_like(network.params[key])
            v[key] = np.zeros_like(network.params[key])

#Momentum        v[key] = momentum * v[key] - learning_rate * grad[key]
#Momentum        network.params[key] += v[key]
#AdaGrad        else:
#AdaGrad            h[key] += np.square(grad[key])
#AdaGrad        network.params[key] -= learning_rate * grad[key] / (np.sqrt(h[key]))
#RMSProp        h[key] *= decay_late
#RMSProp        h[key] += (1 - decay_late) * np.square(grad[key])
#RMSProp        network.params[key] -= learning_rate * grad[key] / (np.sqrt(h[key]) + 1e-7 )
        m[key] += (1-beta1) * (grad[key] - m[key])
        v[key] += (1-beta2) * (grad[key] ** 2 - v[key])
        network.params[key] -= learning_rate_t * m[key] / (np.sqrt(v[key]) + 1e-7)

        loss = network.loss(x_batch, d_batch)
        train_loss_list.append(loss)
        
    if (i + 1) % plot_interval == 0:
        accr_test = network.accuracy(x_test, d_test)
        accuracies_test.append(accr_test)        
        accr_train = network.accuracy(x_batch, d_batch)
        accuracies_train.append(accr_train)
        
        
lists = range(0, iters_num, plot_interval)
plt.plot(lists, accuracies_train, label="training set")
plt.plot(lists, accuracies_test,  label="test set")
plt.legend()
plt.title("accuracy (Adam)")
plt.xlabel("count")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
# グラフの表示
plt.savefig("Adam.png")

以下が精度に対するプロット結果である。

上記より、現状のハイパーパラメータの設定では、MomentumとAdaGradについてはうまく学習できていないことがわかる。一方でRMSPropとAdamは学習自体はうまくいっているが、RMSPropでは若干過学習になりつつあると思われる。AdamについてはRMSPropよりは学習精度がよいことがわかる。ただし、ハイパーパラメータの設定にもよるため、必ずしもAdamがよいとは限らないが、MomentumとRMSPropの良いところを取り入れているので、学習率の最適化については比較的良い結果をもたらす。


確認テスト

確認テスト 学習率最適化手法①

モメンタム・AdaGrad・RMSPropの特徴をそれぞれ簡潔に説明せよ。


★解答
モメンタム:前回の重みの影響を加味する点
AdaGrad:これまでの重みの勾配量を経験として導入する点
RMSProp:前回までの勾配情報をある程度加味する点

3. 過学習

 過学習とはテスト誤差と訓練誤差で学習曲線が乖離している状態である。この原因としてはネットワークの自由度(層数、ノード数、パラメータ数など)が高いことが原因である。例えば入力値のデータ数が少ないのに、NNが大きい場合である。ここでは過学習の対策として、正則化(L1正則化、L2正則化)とドロップアウトについて説明する。

 過学習の原因として重みが大きい値を取ることが挙げられる。つまり過大評価をしている状態になり、一部の値に極端な反応を示している状態である。この解決策は以下2点である。

  • 過学習が起こりそうな重みを大きさ以下で重みをコントロールする
  • 重みの大きさにばらつきを出す

この2点の観点のもと、まずはL1、L2正則化について説明する。正則化はネットワークの自由度へ制約を与える手法である。一般的には①誤差関数にpノルムを加え、②pノルムを計算することで正則化を行う。

$$\begin{eqnarray}
①&:&E_n({\bf w}) + \frac{1}{p}\lambda || x ||_p\\
②&:&||x||_p = (|x_1|^p+...+|x_n|^p)^{1/p}
\end{eqnarray}$$

p=1の場合をL1正則化(ラッソ正則化)、p=2の場合をL2正則化(リッジ正則化)という。簡単には距離を用いて、重みの大きさを制御しているイメージである。p1ノルムの場合はマンハッタン距離、p2ノルムの場合はユークリッド距離である。

 次に、ドロップアウトについて説明する。過学習はノードの数が多いことも一つの要因であった。そこでランダムにノードを削除して学習をすることで過学習を抑制する。さらにドロップアウトを行うことで、データ量を変化させずに異なるモデルを学習させることが可能になる。ただし、学習対象のデータが増えたとも解釈できるため、学習時間は増えてしまう。

実装演習

L1, L2正則化とドロップアウトを組み合わせた実装結果を以下に示す。L1, L2正則化については双方記載している。

import numpy as np
from collections import OrderedDict
from common import layers
from data.mnist import load_mnist
import matplotlib.pyplot as plt
from multi_layer_net import MultiLayerNet
from common import optimizer

class Dropout:
    def __init__(self, dropout_ratio=0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None

    def forward(self, x, train_flg=True):
        if train_flg:
            self.mask = np.random.rand(*x.shape) > self.dropout_ratio
            return x * self.mask
        else:
            return x * (1.0 - self.dropout_ratio)

    def backward(self, dout):
        return dout * self.mask

(x_train, d_train), (x_test, d_test) = load_mnist(normalize=True)

# 過学習を再現するために、学習データを削減
x_train = x_train[:300]
d_train = d_train[:300]

# ドロップアウト設定 ======================================
use_dropout = True
dropout_ratio = 0.1
# ====================================================

network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10, use_dropout = use_dropout, dropout_ratio = dropout_ratio)

iters_num = 1000
train_size = x_train.shape[0]
batch_size = 100
learning_rate=0.01

train_loss_list = []
accuracies_train = []
accuracies_test = []
hidden_layer_num = network.hidden_layer_num

plot_interval=10

# 正則化強度設定 ======================================
weight_decay_lambda=0.002
# =================================================

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    d_batch = d_train[batch_mask]

    grad = network.gradient(x_batch, d_batch)
    weight_decay = 0
    
    for idx in range(1, hidden_layer_num+1):
        grad['W' + str(idx)] = network.layers['Affine' + str(idx)].dW + weight_decay_lambda * np.sign(network.params['W' + str(idx)])
#L2正則化        grad['W' + str(idx)] = network.layers['Affine' + str(idx)].dW + weight_decay_lambda * network.params['W' + str(idx)]
        grad['b' + str(idx)] = network.layers['Affine' + str(idx)].db
        network.params['W' + str(idx)] -= learning_rate * grad['W' + str(idx)]
        network.params['b' + str(idx)] -= learning_rate * grad['b' + str(idx)]        
        weight_decay += weight_decay_lambda * np.sum(np.abs(network.params['W' + str(idx)]))
#L2正則化        weight_decay += 0.5 * weight_decay_lambda * np.sqrt(np.sum(network.params['W' + str(idx)] ** 2))


    loss = network.loss(x_batch, d_batch) + weight_decay
    train_loss_list.append(loss)        
        
    if (i+1) % plot_interval == 0:
        accr_train = network.accuracy(x_train, d_train)
        accr_test = network.accuracy(x_test, d_test)
        accuracies_train.append(accr_train)
        accuracies_test.append(accr_test)
                
lists = range(0, iters_num, plot_interval)
plt.plot(lists, accuracies_train, label="training set")
plt.plot(lists, accuracies_test,  label="test set")
plt.legend()
plt.title("accuracy (Dropout + L1)")
plt.xlabel("count")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.savefig("overfitting(Dropout+L1).png")

このパラメータセットではまだ過学習であるとも取れる。学習時間とのトレードオフにはなるが、ドロップアウト率を増やしてもよいかもしれない。


確認テスト・例題チャレンジ

確認テスト 過学習①

機械学習で使われる線形モデルの正則化は、モデルの重みを制限することで可能となる。線形モデルの正則化手法の中にリッジ回帰という手法があり、その特徴として正しいものを選択せよ。
(a)ハイパーパラメータを大きな値に設定すると、全ての重みが限りなく0に近づく
(b)ハイパーパラメータを0に設定すると、非線形回帰となる
(c)バイアス項についても、正則化される。
(d)リッジ回帰の場合、隠れ層に対して正則化項を加える


★解答:(d)

確認テスト 過学習②

下図について、L1正則化を表しているグラフはどちらか。


★解答:右図

例題チャレンジ 過学習①

★解答:(4)(L2ノルムは||param||2なので、その微分であるparamが答え。)

例題チャレンジ 過学習②

★解答:(3) (L1ノルムは|param|なので、勾配は符号のみ)

例題チャレンジ 過学習③

★解答:(4) (imageの形式が(縦幅、横幅、チャンネル)なのでそれに合わせてreturnする)

4. 畳み込みニューラルネットワークの概念

 畳み込みニューラルネットワーク(CNN)は画像に対してよく使われるNNであるが、画像以外にも次元的に繋がりのあるデータに対して用いることが可能である。全体の流れとしては

入力→畳み込み→畳み込み→プーリング→•••→全結合→出力

のようになる。以下では、畳み込み層・プーリング層・全結合層についてそれぞれ説明する。

 まず、畳み込み層は3次元の空間情報も学習可能な層のことを指す。処理としては

入力×フィルター→出力値+バイアス→活性化関数→出力

ということを行う。フィルターは重みのようなものであり、周りの情報(次元的つながり)を保持しながら学習を進めるためのものである。畳み込み層ではパディングという操作がよく行われる。パディングは畳み込みをしても入出力の画像サイズを変えないための処理であり、2つの方法がある。

  • ゼロパディング(入力画像の周囲を0で埋める)
  • 最も近い数字で入力の画像の周囲を埋める

まとめると、パディングではひとまわり大きい画像を作り、フィルター前後で画像サイズが変わらないようにしている。
 次に、ストライドについて説明する。ストライドはフィルターを動かすメモリ数を表しており、ストライド数が大きいほど、出力の画像サイズが小さくなる。ストライドを考慮してフィルターを通したのちの画像の出力サイズは以下のようになる。

$$\begin{eqnarray}
O_H&=&\frac{画像高さ+2\times パディング-フィルター高さ}{ストライド}+1\\
O_W&=&\frac{画像幅+2\times パディング-フィルター幅}{ストライド}+1
\end{eqnarray}$$

 次にプーリング層について説明する。プーリング層では重みは用いず、単純なサイズの圧縮(特微量抽出)を行う。よく用いるプーリングの方法としては以下の二種類がある。

  • Max Pooling : 対象領域の最大値を取得する。
  • Average Pooling : 対象領域の平均値を取得する。

 最後に全結合層について説明する。全結合層では重みとバイアスの処理を行う。通常のNNでは次元情報が失われるのに対して、畳み込みを行うことで、全結合の直言までは次元を保持した特微量を抽出した学習を行うことができる。

実装演習

画像データを2次元配列に変換する際にストライドとパディングの処理を行う。

#画像データを2次元配列に格納(ストライド・パディング処理含む)
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    # N: number, C: channel, H: height, W: width
    N, C, H, W = input_data.shape
    out_h = (H + 2 * pad - filter_h)//stride + 1
    out_w = (W + 2 * pad - filter_w)//stride + 1

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
    
    col = col.transpose(0, 4, 5, 1, 2, 3) # (N, C, filter_h, filter_w, out_h, out_w) -> (N, filter_w, out_h, out_w, C, filter_h)    
    
    col = col.reshape(N * out_h * out_w, -1)
    return col

#逆変換
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    # N: number, C: channel, H: height, W: width
    N, C, H, W = input_shape
    # 切り捨て除算    
    out_h = (H + 2 * pad - filter_h)//stride + 1
    out_w = (W + 2 * pad - filter_w)//stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2) # (N, filter_h, filter_w, out_h, out_w, C)

    img = np.zeros((N, C, H + 2 * pad + stride - 1, W + 2 * pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]

次に、畳み込みを行う(この時にバイアスを加える)

class Convolution:
    # W: フィルター, b: バイアス
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
        # 中間データ(backward時に使用)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # フィルター・バイアスパラメータの勾配
        self.dW = None
        self.db = None

    def forward(self, x):
        # FN: filter_number, C: channel, FH: filter_height, FW: filter_width
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        # 出力値のheight, width
        out_h = 1 + int((H + 2 * self.pad - FH) / self.stride)
        out_w = 1 + int((W + 2 * self.pad - FW) / self.stride)
        
        # xを行列に変換
        col = im2col(x, FH, FW, self.stride, self.pad)
        # フィルターをxに合わせた行列に変換
        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)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        # dcolを画像データに変換
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

プーリング層の処理(MaxPooling)

class Pooling:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        self.pool_h = pool_h
        self.pool_w = pool_w
        self.stride = stride
        self.pad = pad
        
        self.x = None
        self.arg_max = None

    def forward(self, x):
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h) / self.stride)
        out_w = int(1 + (W - self.pool_w) / self.stride)
        
        # xを行列に変換
        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        # プーリングのサイズに合わせてリサイズ
        col = col.reshape(-1, self.pool_h*self.pool_w)
        
        # 行ごとに最大値を求める
        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        # 整形
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

CNN全体の処理

lass SimpleConvNet:
    # conv - relu - pool - affine - relu - affine - softmax
    def __init__(self, input_dim=(1, 28, 28), conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
                 hidden_size=100, output_size=10, weight_init_std=0.01):
        filter_num = conv_param['filter_num']        
        filter_size = conv_param['filter_size']
        filter_pad = conv_param['pad']
        filter_stride = conv_param['stride']
        input_size = input_dim[1]
        conv_output_size = (input_size - filter_size + 2 * filter_pad) / filter_stride + 1
        pool_output_size = int(filter_num * (conv_output_size / 2) * (conv_output_size / 2))

        # 重みの初期化
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
        self.params['b1'] = np.zeros(filter_num)
        self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
        self.params['b2'] = np.zeros(hidden_size)
        self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b3'] = np.zeros(output_size)

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Conv1'] = layers.Convolution(self.params['W1'], self.params['b1'], conv_param['stride'], conv_param['pad'])
        self.layers['Relu1'] = layers.Relu()
        self.layers['Pool1'] = layers.Pooling(pool_h=2, pool_w=2, stride=2)
        self.layers['Affine1'] = layers.Affine(self.params['W2'], self.params['b2'])
        self.layers['Relu2'] = layers.Relu()
        self.layers['Affine2'] = layers.Affine(self.params['W3'], self.params['b3'])

        self.last_layer = layers.SoftmaxWithLoss()

    def predict(self, x):
        for key in self.layers.keys():
            x = self.layers[key].forward(x)
        return x
        
    def loss(self, x, d):
        y = self.predict(x)
        return self.last_layer.forward(y, d)

    def accuracy(self, x, d, batch_size=100):
        if d.ndim != 1 : d = np.argmax(d, axis=1)
        
        acc = 0.0
        
        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            td = d[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == td) 
        
        return acc / x.shape[0]

    def gradient(self, x, d):
        # forward
        self.loss(x, d)
        
        # backward
        dout = 1
        dout = self.last_layer.backward(dout)
        layers = list(self.layers.values())
        
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grad = {}
        grad['W1'], grad['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
        grad['W2'], grad['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grad['W3'], grad['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grad

以上を組み合わせた実行部分

from common import optimizer

# データの読み込み
(x_train, d_train), (x_test, d_test) = load_mnist(flatten=False)

print("データ読み込み完了")

# 処理に時間のかかる場合はデータを削減 
x_train, d_train = x_train[:5000], d_train[:5000]
x_test, d_test = x_test[:1000], d_test[:1000]


network = SimpleConvNet(input_dim=(1,28,28), conv_param = {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1},
                        hidden_size=100, output_size=10, weight_init_std=0.01)

optimizer = optimizer.Adam()

iters_num = 1000
train_size = x_train.shape[0]
batch_size = 100

train_loss_list = []
accuracies_train = []
accuracies_test = []

plot_interval=10



for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    d_batch = d_train[batch_mask]
    
    grad = network.gradient(x_batch, d_batch)
    optimizer.update(network.params, grad)

    loss = network.loss(x_batch, d_batch)
    train_loss_list.append(loss)

    if (i+1) % plot_interval == 0:
        accr_train = network.accuracy(x_train, d_train)
        accr_test = network.accuracy(x_test, d_test)
        accuracies_train.append(accr_train)
        accuracies_test.append(accr_test)
        

lists = range(0, iters_num, plot_interval)
plt.plot(lists, accuracies_train, label="training set")
plt.plot(lists, accuracies_test,  label="test set")
plt.legend(loc="lower right")
plt.title("accuracy")
plt.xlabel("count")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.savefig("cnn.png")

実行結果は以下のようになる。

確認テスト

確認テスト CNNの概念①

サイズ6×6の入力画像を、サイズ2×2のフィルタで畳み込んだ時の出力画像のサイズを答えよ。ただし、ストライドとパディングは1とする


★解答
出力画像のサイズは7×7となる。(公式を使っても良いし、この程度なら絵に書いてもよい)

5. 最新のCNN

 ここではAlexNetについて説明する。AlexNetは2012年のコンペティションで1位を取得したモデルである。AlexNet以前では人間が特微量を指定して学習を行なっていたが、AlexNetでは特微量の人間による設計は不要になった。現在ではAlexNet自体はそれほど使われていないが、最新のモデルでも基本的な考え方は同じである。以降では、全体の流れは以下の図のとおりである。

5層の畳み込み層とプーリング層があり、全結合層は3層となっている。図には記載されていないが、全結合層部分でドロップアウトが使用されている。また、画像データの少なさを解決するために、既存の画像データを反転・回転して学習データの数を増やしていた。

実装演習

import chainer
import chainer.functions as F
import chainer.links as L

class AlexNet(chainer.Chain):

    insize = 227

    def __init__(self):
        super(AlexNet, self).__init__()
        with self.init_scope():
            self.conv1 = L.Convolution2D(None,  96, 11, stride=4)
            self.conv2 = L.Convolution2D(None, 256,  5, pad=2)
            self.conv3 = L.Convolution2D(None, 384,  3, pad=1)
            self.conv4 = L.Convolution2D(None, 384,  3, pad=1)
            self.conv5 = L.Convolution2D(None, 256,  3, pad=1)

            self.fc6 = L.Linear(None, 4096)
            self.fc7 = L.Linear(None, 4096)
            self.fc8 = L.Linear(None, 1000)

    def forward(self, x, t):
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.fc6(h)))
        h = F.dropout(F.relu(self.fc7(h)))
        h = self.fc8(h)

        loss = F.softmax_cross_entropy(h, t)
        chainer.report({'loss': loss, 'accuracy': F.accuracy(h, t)}, self)
        return loss

コメント

タイトルとURLをコピーしました