のんびり肉体改造ブログ

30代社会人のトレーニング記録と雑記

ラビットチャレンジレポート(深層学習/前編/その1)

トレーニングにまつわるあれこれを掲載しているブログではありますが,都合によりこのような記事を掲載いたします.
現在一般社団法人ディープラーニング協会のE資格を取得するため,ラビットチャレンジなるプログラムを受講しています.本プログラムにおいてレポート公開が必須になっていることから,こちらのブログにてレポートします.

深層学習

はじめに

生成モデルと識別モデル

識別モデルは変数xを受けて,それがどのクラスに相当するのかを確率的に出力する.一方で,生成モデルはクラスが入力されて,そのクラスを表現するxを確率的に出力する.

言い換えると識別モデルは高次元情報を低次元に,生成モデルは低次元情報を高次元に出力する.

f:id:CivilEng:20211217140751p:plain

万能近似定理

深層ニューラルネットワークが実数空間において,任意の有界な連続関数を近似できる ことを保証するための定理.つまり数値情報であれば何でも近似できることを示した定理.

ニューラルネットワーク

ニューラルネットワークはネットワークに存在するパラメータの最適化を行う.

f:id:CivilEng:20211217143151p:plain

ネットワークの関数は一次関数なので,微分可能.得られた誤差を逆伝播させることで,パラメータの更新を行う.誤差関数には回帰問題の場合はMSE,分類問題の場合はCross Entropyを使う事が多い.

活性化関数

活性化関数に使われる関数はいくつかある.シグモイド関数は0~1の間に収まる関数で,σ(0) = 0.5,dσ/dz = 0.25となる関数.

微分可能でσ(z)/dz = (1 - σ(z))*σ(z)と微分関数がシグモイド関数自身で表現できるのが特徴.しかし,微分値が最大で0.25なので,勾配が消失しやすい欠点がある.

f:id:CivilEng:20211217150057p:plain

RELU関数は現在最もよく使われる活性化関数.勾配消失問題とスパース化に貢献.非常にシンプル.

f:id:CivilEng:20211217150627p:plain

他に,他クラス分類に対応するためのソフトマックス関数などがある.

勾配降下法

勾配降下法は入力したデータから得られたMSEを使って,重みの勾配を更新する.

f:id:CivilEng:20211217152048p:plain

f:id:CivilEng:20211217152936p:plain

ここでεは学習率.εを適切に設定しないと,学習効率に影響するだけでなく,発散して解が得られないことも.

収束性向上のために様々なアルゴリズムが提案されている.

確率的勾配降下法(SGD)

勾配降下法では全サンプルの平均誤差を利用するが,SGDではランダムに抽出したサンプルの誤差を使って重みの更新を行う.

f:id:CivilEng:20211217153513p:plain

Eの下に1つのサンプルの誤差であることを示す,nがついている.

データが冗長な場合の計算コストの低減 / 局所極小解への収束リスク低減 / オンライン学習が可能 などのメリットがある.

Momentum SGD,AdamなどはいずれもSGDからの派生.

誤差逆伝播法

誤差関数の出力値から逆算して,各パラメータの微分値を取得する方法.

f:id:CivilEng:20211217154132p:plain

深層学習の学習テクニック

勾配消失問題

活性化関数

勾配消失問題とは,誤差逆伝播法によりパラメータの勾配を計算する際に,値の小さい微分値が複数登場することで,連鎖律による積が影響して勾配がゼロに近づく問題.

シグモイド関数の場合,微分値の最大値が0.25であることから,層が深くなると急速に勾配が小さくなる.

f:id:CivilEng:20211217154958p:plain

そこで,層が深い場合,活性化関数にはシグモイド関数の代わりにReLU関数が用いられることが多い.

重みの初期値

重みの初期値は重要で,0にすると表現力が低下する一方で,大きいと過学習の要因になる.そこで,Xavierの初期値と呼ばれる,平均0, 分散  \sqrt {n} (nは前層のノード数)の正規分布に従って設定することで,前述のような問題が発生しにくくなると言われている.

バッチ正規化

バッチ単位で活性化関数に値を渡す前に正規化を行う手法.

f:id:CivilEng:20211217160236p:plain

学習率最適化問題

確率的勾配降下法において,SGDでは収束までに時間を要することから様々はアルゴリズムが提案されている.詳細は割愛.

CNN

畳み込み層,プーリング層を組み合わせることで,主に画像から特徴量を抽出して,NNに流す手法. 有名なモデルとしてはAlexNet,VGG,ResNetなど. 詳細は割愛.

コード演習

以下に順伝搬・逆伝搬の動きを考察する.

まずは順伝搬は他クラス分類として誤差の出力まで実施. パラメータを手入力してみたり,一様分布でランダムに入力するなどした.

def init_network():
    print("##### ネットワークの初期化 #####")

    network = {}
    # network['W1'] = np.array([
    #     [0.1, 0.4, 0.7, 1.0, 1.3],
    #     [0.2, 0.5, 0.8, 1.1, 1.4],
    #     [0.3, 0.6, 0.9, 1.2, 1.5]
    # ])
    # network['W2'] = np.array([
    #     [0.1, 0.6, 1.1, 1.6],
    #     [0.2, 0.7, 1.2, 1.7],
    #     [0.3, 0.8, 1.3, 1.8],
    #     [0.4, 0.9, 1.4, 1.9],
    #     [0.5, 1.0, 1.5, 2.0],
    # ])
    network['W1'] = np.random.random_sample((3, 5))
    network['W2'] = np.random.random_sample((5, 4))

    # network['b1'] = np.array([0.1, 0.2, 0.3, 0.4, 0.5])
    # network['b2'] = np.array([0.1, 0.2, 0.3, 0.4])

    network['b1'] = np.random.random_sample((5))
    network['b2'] = np.random.random_sample((4))

    print_vec("重み1", network['W1'] )
    print_vec("重み1_shape", network['W1'].shape)
    print_vec("重み2", network['W2'] )
    print_vec("重み2_shape", network['W2'].shape)
    print_vec("バイアス1", network['b1'] )
    print_vec("バイアス2", network['b2'] )

    return network

# プロセスを作成
# x:入力値
def forward(network, x):
    
    print("##### 順伝播開始 #####")
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    
    # 1層の総入力
    u1 = np.dot(x, W1) + b1

    # 1層の総出力
    z1 = functions.relu(u1)

    # 2層の総入力
    u2 = np.dot(z1, W2) + b2
    
    # 出力値
    y = functions.softmax(u2)
    
    print_vec("総入力1", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", y)
    print("出力合計: " + str(np.sum(y)))
        
    return y, z1

## 事前データ
# 入力値
x = np.array([1., 2., 3.])

# 目標出力
d = np.array([0, 0, 0, 1])

# ネットワークの初期化
network =  init_network()

# 出力
y, z1 = forward(network, x)

# 誤差
loss = functions.cross_entropy_error(d, y)

## 表示
print("\n##### 結果表示 #####")
print_vec("出力", y)
print_vec("訓練データ", d)
print_vec("誤差",  loss)

アウトプットは次の通り.

##### ネットワークの初期化 #####
*** 重み1 ***
[[0.40583541 0.48270291 0.45155913 0.35853483 0.07131417]
 [0.71327806 0.41358175 0.78204041 0.49268012 0.05532167]
 [0.96165765 0.46681934 0.75544826 0.30242782 0.02016309]]

*** 重み1_shape ***
(3, 5)

*** 重み2 ***
[[0.0192056  0.5129644  0.99851308 0.23368795]
 [0.16305207 0.79219082 0.10339846 0.43518898]
 [0.08001238 0.7096225  0.71294513 0.62330949]
 [0.18445616 0.58533622 0.34574588 0.58267313]
 [0.35724645 0.43236777 0.97998781 0.44625945]]

*** 重み2_shape ***
(5, 4)

*** バイアス1 ***
[0.80841537 0.0115861  0.85294493 0.48893631 0.53264631]

*** バイアス2 ***
[0.37119429 0.5206541  0.06986217 0.88695258]

##### 順伝播開始 #####
*** 総入力1 ***
[5.52577985 2.72191053 5.13492966 2.74011483 0.77509309]

*** 中間層出力1 ***
[5.52577985 2.72191053 5.13492966 2.74011483 0.77509309]

*** 総入力2 ***
[ 2.11432156 11.09433034 11.2367553   8.50594054]

*** 出力1 ***
[5.65004066e-05 4.48765883e-01 5.17456929e-01 3.37206877e-02]

出力合計: 1.0

##### 結果表示 #####
*** 出力 ***
[5.65004066e-05 4.48765883e-01 5.17456929e-01 3.37206877e-02]

*** 訓練データ ***
[0 0 0 1]

*** 誤差 ***
3.3896407857551885

次は逆伝搬も含めた例.ノード間を接続する関数は線形なので,非常にシンプルなコードで記述できる.

# ウェイトとバイアスを設定
# ネートワークを作成
def init_network():
    print("##### ネットワークの初期化 #####")

    network = {}
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])

    network['W2'] = np.array([
        [0.1, 0.4],
        [0.2, 0.5],
        [0.3, 0.6]
    ])

    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['b2'] = np.array([0.1, 0.2])
    
    print_vec("重み1", network['W1'])
    print_vec("重み2", network['W2'])
    print_vec("バイアス1", network['b1'])
    print_vec("バイアス2", network['b2'])

    return network

# 順伝播
def forward(network, x):
    print("##### 順伝播開始 #####")

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    
    u1 = np.dot(x, W1) + b1
    z1 = functions.relu(u1)
    u2 = np.dot(z1, W2) + b2
    y = functions.softmax(u2)
    
    print_vec("総入力1", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", y)
    print("出力合計: " + str(np.sum(y)))

    return y, z1

# 誤差逆伝播
def backward(x, d, z1, y):
    print("\n##### 誤差逆伝播開始 #####")

    grad = {}

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    #  出力層でのデルタ
    delta2 = functions.d_sigmoid_with_loss(d, y)
    #  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)
    # b1の勾配
    grad['b1'] = np.sum(delta1, axis=0)
    #  W1の勾配
    grad['W1'] = np.dot(x.T, delta1)
        
    print_vec("偏微分_dE/du2", delta2)
    print_vec("偏微分_dE/du2", delta1)

    print_vec("偏微分_重み1", grad["W1"])
    print_vec("偏微分_重み2", grad["W2"])
    print_vec("偏微分_バイアス1", grad["b1"])
    print_vec("偏微分_バイアス2", grad["b2"])

    return grad
    
# 訓練データ
x = np.array([[1.0, 5.0]])
# 目標出力
d = np.array([[0, 1]])
#  学習率
learning_rate = 0.01
network =  init_network()
y, z1 = forward(network, x)

# 誤差
loss = functions.cross_entropy_error(d, y)

grad = backward(x, d, z1, y)
for key in ('W1', 'W2', 'b1', 'b2'):
    network[key]  -= learning_rate * grad[key]

print("##### 結果表示 #####")    


print("##### 更新後パラメータ #####") 
print_vec("重み1", network['W1'])
print_vec("重み2", network['W2'])
print_vec("バイアス1", network['b1'])
print_vec("バイアス2", network['b2'])

アウトプットは次の通り

*** バイアス2 ***
[0.1 0.2]

##### 順伝播開始 #####
*** 総入力1 ***
[[1.2 2.5 3.8]]

*** 中間層出力1 ***
[[1.2 2.5 3.8]]

*** 総入力2 ***
[[1.86 4.21]]

*** 出力1 ***
[[0.08706577 0.91293423]]

出力合計: 1.0

##### 誤差逆伝播開始 #####
*** 偏微分_dE/du2 ***
[[ 0.08706577 -0.08706577]]

*** 偏微分_dE/du2 ***
[[-0.02611973 -0.02611973 -0.02611973]]

*** 偏微分_重み1 ***
[[-0.02611973 -0.02611973 -0.02611973]
 [-0.13059866 -0.13059866 -0.13059866]]

*** 偏微分_重み2 ***
[[ 0.10447893 -0.10447893]
 [ 0.21766443 -0.21766443]
 [ 0.33084994 -0.33084994]]

*** 偏微分_バイアス1 ***
[-0.02611973 -0.02611973 -0.02611973]

*** 偏微分_バイアス2 ***
[ 0.08706577 -0.08706577]

##### 結果表示 #####
##### 更新後パラメータ #####
*** 重み1 ***
[[0.1002612  0.3002612  0.5002612 ]
 [0.20130599 0.40130599 0.60130599]]

*** 重み2 ***
[[0.09895521 0.40104479]
 [0.19782336 0.50217664]
 [0.2966915  0.6033085 ]]

*** バイアス1 ***
[0.1002612 0.2002612 0.3002612]

*** バイアス2 ***
[0.09912934 0.20087066]