提出したレポートです。
絶対書きすぎですが、行間を埋めたくなるので仕方ない。
Rabbit Challenge - Stage 3. 深層学習 前編 (Day 2)
1. 勾配消失問題
深層ニューラルネットワークでは、層を深くすることで表現力は向上する一方、学習がうまく進まなくなる問題が生じることがある。
その代表的な例が 勾配消失問題 である。
勾配消失問題とは、誤差逆伝播法によって勾配を計算する際、層を遡るにつれて勾配が小さくなり、入力層付近の重みがほとんど更新されなくなる現象を指す。
この問題が発生すると、理論的には表現力の高い深層モデルであっても、実際には十分な学習が行われない。
本講義では、勾配消失問題を 深層モデルの学習を妨げる主要な要因の一つ として位置づけ、その対策として、
- 活性化関数の選択
- 重みの初期値設定
- 正規化手法の導入
がどのような役割を果たすかを順に確認する。
以下では、それぞれの手法について基本的な定義と数式を整理し、後続の実装演習への準備とする。
1.1 活性化関数:ReLU 関数
ReLU(Rectified Linear Unit)関数は、以下のように定義される活性化関数である。
$$ f(x) = \max(0, x) $$
この関数の微分は次のようになる。
$$ f'(x) = \begin{cases} 1 & (x > 0) \\ 0 & (x \le 0) \end{cases} $$
ReLU 関数は、正の入力に対して微分値が一定であるため、シグモイド関数や tanh 関数と比べて、逆伝播の過程で勾配が小さくなりにくい。
この性質により、活性化関数に起因する勾配消失を緩和し、深い層まで勾配を伝えやすくする点に意義がある。
1.2 重みの初期値設定:Xavier 初期化
重みの初期値が不適切な場合、順伝播や逆伝播の過程で信号や勾配の分散が層を重ねるごとに変化し、学習が不安定になる。
この問題を緩和するために用いられる手法が Xavier 初期化 である。
Xavier 初期化では、重み $W$ を次の分布から初期化する。
$$ W \sim \mathcal{N}\left(0,\ \frac{1}{n_{\text{in}}}\right) $$
または
$$ W \sim \mathcal{U}\left(-\sqrt{\frac{6}{n_{\text{in}}+n_{\text{out}}}},\ \sqrt{\frac{6}{n_{\text{in}}+n_{\text{out}}}}\right) $$
ここで、$n_{\text{in}}$ は入力ユニット数、$n_{\text{out}}$ は出力ユニット数である。
この初期化により、層をまたいでも出力や勾配の分散が大きく変化しないように調整される。
1.3 重みの初期値設定:He 初期化
He 初期化は、ReLU 関数を用いる場合に適した重み初期化手法である。
ReLU 関数では負の入力が $0$ になるため、有効なユニット数が減少する点を考慮している。
He 初期化では、重み $W$ を次の分布から初期化する。
$$ W \sim \mathcal{N}\left(0,\ \frac{2}{n_{\text{in}}}\right) $$
このように分散を大きめに設定することで、ReLU 関数と組み合わせた場合でも、勾配消失が起こりにくくなる。
1.4 バッチ正規化(Batch Normalization)
バッチ正規化は、各層への入力をミニバッチ単位で正規化する手法であり、学習の安定化を目的として用いられる。
ミニバッチ内の入力 $x$ に対して、平均と分散を用いて次のように正規化を行う。
$$ \hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \varepsilon}} $$
ここで、$\mu_B$ と $\sigma_B^2$ はミニバッチ内の平均および分散であり、$\varepsilon$ は数値安定化のための微小値である。
さらに、学習可能なパラメータ $\gamma, \beta$ を用いて次の変換を行う。
$$ y = \gamma \hat{x} + \beta $$
これにより、入力分布の変動を抑えつつ、モデルの表現力を維持することができる。
実装演習
配布コードの実行
配布コード 2_2_2_vanishing_gradient_modified.ipynb の実行結果を載せる。
- シグモイド関数 & ガウス分布初期化
- ReLU 関数 & ガウス分布初期化
- シグモイド関数 & Xavier 初期化
- ReLU 関数 & He 初期化
[try] hidden_size_list の数字を変更してみよう
配布コードの「ReLU 関数 & He 初期化」を使って、隠れ層の数を増加させて観察する。
hidden_size_list を以下のように変化させた。
hidden_size_list = [40, 20] + [20]*4
実行結果としては、accuracy は学習初期から急激に変化するのではなく、反復回数に応じて緩やかに向上した。
これは、He 初期化により各層での勾配のスケールが適切に保たれ、ReLU の特性と相まって学習が安定して進んだためと考えられる。
この結果は、勾配消失を抑制することで深層ネットワークの学習が安定化することを示している。
[try] sigmoid - He と relu - Xavier についても試してみよう
- シグモイド関数 & He 初期化
シグモイド関数と He 初期化を組み合わせた場合でも、学習は安定して進行し、一定の精度に到達した。
しかし、ReLU と He 初期化を用いた場合と比較すると、学習の立ち上がりが遅く、深層化に対する耐性という点では十分とは言えないと考えられる。
このことから、初期化手法と活性化関数の組み合わせが学習挙動に大きく影響することが分かる。
- ReLU 関数 & Xavier 初期化
ReLU 関数と Xavier 初期化を用いた場合、学習初期から accuracy が速やかに向上し、安定して高い精度に到達した。
これは、ReLU により活性化関数由来の勾配消失が抑制され、Xavier 初期化によって勾配の分散が極端に崩れなかったためと考えられる。
確認テスト
連鎖律の原理を使い、dz/dx を求めよ
$$ z = t^2,\quad t = x + y $$
$z$ は $t$ を通じて $x$ に依存しているため、連鎖律より
$$ \frac{dz}{dx} = \frac{dz}{dt} \cdot \frac{dt}{dx} $$
と書ける。
$$ \frac{dz}{dt} = 2t, \frac{dt}{dx} = 1 $$
より、
$$ \frac{dz}{dx} = 2t \cdot 1 = 2(x + y) $$
シグモイド関数を微分した時、入力値が 0 の時に最大値をとる。その値として正しいものを選択肢から選べ
シグモイド関数を
$$ \sigma(x)=\frac{1}{1+e^{-x}} $$
とすると、その微分は
$$ \sigma'(x)=\sigma(x)\bigl(1-\sigma(x)\bigr) $$
で与えられる。
入力値 $x=0$ のとき
$$ \sigma(0)=\frac{1}{2} $$
より、
$$ \sigma'(0)=\frac{1}{2}\left(1-\frac{1}{2}\right)=\frac{1}{4}=0.25 $$
したがって、正しい選択肢は(2)$0.25$ と計算できる。
重みの初期値に 0 を設定すると、どのような問題が発生するか。簡潔に説明せよ
重みの初期値をすべて 0 に設定すると、各ユニットが同じ値・同じ勾配を持つ対称な状態になり、学習によって役割分担が生じなくなる。
その結果、重みが同一に更新され続け、表現力が向上せず学習が進まないという問題が発生する。
一般的に考えられるバッチ正規化の効果を 2 点挙げよ
-
各層の出力分布を正規化することで勾配が安定し、学習が高速かつ安定に進む
-
学習中に入力分布の変動が抑えられるため、勾配消失や勾配爆発を起こしにくくなる
参考図書 / 関連記事
- ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
- p.183 - 184
Xavier 初期化は、活性化関数が線形、あるいは原点付近で線形近似できることを前提として導かれた重み初期化手法である。
sigmoid 関数や tanh 関数は、中央付近では線形関数とみなせる領域を持ち、かつ左右対称な形状をしているため、Xavier 初期化との相性が良いとされている。
一方、ReLU 関数は非対称であり、負の入力を $0$ に切り捨てる特性を持つため、Xavier 初期化をそのまま用いると分散が適切に保たれない。
この問題に対応するため、ReLU 関数に特化して設計された初期化手法が He 初期化である。
また、sigmoid 関数と tanh 関数の違いとして、出力の中心がそれぞれ $(0, 0.5)$ と $(0, 0)$ に位置する点が挙げられる。
一般に、活性化関数が原点対称であることは、勾配の伝播を安定させる上で望ましい性質であることが知られており、この点において tanh 関数は sigmoid 関数よりも有利である。
2. 学習率最適化手法
勾配降下法では、学習率の設定が学習の収束速度や安定性に大きく影響する。
学習率が大きすぎると発散し、小さすぎると学習が極端に遅くなるため、学習率を適切に制御する最適化手法が提案されている。
以下では、代表的な学習率最適化手法について、その更新式を中心に整理する。
2.1 モメンタム(Momentum)
モメンタムは、過去の勾配情報を蓄積し、その慣性を利用して更新を行う手法である。
勾配降下法における振動を抑え、収束を高速化することを目的としている。
更新式は以下の通りである。
$$ \begin{aligned} \boldsymbol{v}_t &= \alpha \boldsymbol{v}_{t-1} - \eta \nabla E(\boldsymbol{w}_t) \\ \boldsymbol{w}_{t+1} &= \boldsymbol{w}_t + \boldsymbol{v}_t \end{aligned} $$
ここで、$\boldsymbol{v}_t$ は速度ベクトル、$\alpha$ はモメンタム係数、$\eta$ は学習率を表す。
2.2 AdaGrad
AdaGrad は、各パラメータごとに学習率を調整する手法であり、頻繁に更新されるパラメータの学習率を小さくする特徴を持つ。
更新式は以下の通りである。
$$ \begin{aligned} \boldsymbol{h}_t &= \boldsymbol{h}_{t-1} + (\nabla E(\boldsymbol{w}_t))^2 \\ \boldsymbol{w}_{t+1} &= \boldsymbol{w}_t - \frac{\eta}{\sqrt{\boldsymbol{h}_t} + \epsilon} \nabla E(\boldsymbol{w}_t) \end{aligned} $$
$\boldsymbol{h}_t$ は過去の勾配の二乗和を表し、$\epsilon$ はゼロ除算を防ぐための微小値である。
2.3 RMSProp
RMSProp は、AdaGrad において 学習が進むにつれて学習率が過度に小さくなる問題を改善した手法である。
過去の勾配の二乗を指数移動平均で保持する。
更新式は以下の通りである。
$$ \begin{aligned} \boldsymbol{h}_t &= \rho \boldsymbol{h}_{t-1} + (1-\rho)(\nabla E(\boldsymbol{w}_t))^2 \\ \boldsymbol{w}_{t+1} &= \boldsymbol{w}_t - \frac{\eta}{\sqrt{\boldsymbol{h}_t} + \epsilon} \nabla E(\boldsymbol{w}_t) \end{aligned} $$
ここで、$\rho$ は減衰率を表す。
2.4 Adam
Adam は、モメンタムと RMSProp を組み合わせた最適化手法であり、現在最も広く用いられている手法の一つである。
一次モーメント(平均)と二次モーメント(分散)の両方を考慮する。
更新式は以下の通りである。
$$ \begin{aligned} \boldsymbol{m}_t &= \beta_1 \boldsymbol{m}_{t-1} + (1-\beta_1)\nabla E(\boldsymbol{w}_t) \\ \boldsymbol{v}_t &= \beta_2 \boldsymbol{v}_{t-1} + (1-\beta_2)(\nabla E(\boldsymbol{w}_t))^2 \end{aligned} $$
バイアス補正を行った後、
$$ \boldsymbol{w}_{t+1} = \boldsymbol{w}_t - \frac{\eta}{\sqrt{\hat{\boldsymbol{v}}_t} + \epsilon}\hat{\boldsymbol{m}}_t $$
としてパラメータを更新する。
実装演習
配布コードの実行
配布コード 2_4_optimizer_after.ipynb の実行結果を載せる。
- SGD
- Momentum
- AdaGrad
- RMSProp
- Adam
[try] 活性化関数と重みの初期化方法を変更して違いを見てみよう
import numpy as np
import matplotlib.pyplot as plt
from data.mnist import load_mnist
from multi_layer_net import MultiLayerNet
##################################################
# 共通設定(必要なら変更)
##################################################
hidden_size_list = [40, 20]
iters_num = 1000
batch_size = 100
learning_rate = 0.01
plot_interval = 10
use_batchnorm = False
seed = 0
##################################################
# データ読み込み(1回だけ)
##################################################
(x_train, d_train), (x_test, d_test) = load_mnist(normalize=True, one_hot_label=True)
train_size = x_train.shape[0]
print("データ読み込み完了")
##################################################
# 1条件を学習して精度推移を返す
##################################################
def run_sgd_experiment(
activation: str,
weight_init_std,
*,
hidden_size_list,
iters_num, batch_size,
learning_rate,
plot_interval,
use_batchnorm,
seed,
):
np.random.seed(seed)
network = MultiLayerNet(
input_size=784,
hidden_size_list=hidden_size_list,
output_size=10,
activation=activation,
weight_init_std=weight_init_std,
use_batchnorm=use_batchnorm
)
accuracies_train = []
accuracies_test = []
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)
# paramsのキーはネットワーク構造に依存するので、存在するものを全部更新する
for key in network.params.keys():
network.params[key] -= learning_rate * grad[key]
if (i + 1) % plot_interval == 0:
accr_test = network.accuracy(x_test, d_test)
accr_train = network.accuracy(x_batch, d_batch)
accuracies_test.append(accr_test)
accuracies_train.append(accr_train)
return accuracies_train, accuracies_test
##################################################
# 6条件を回す(2×3)
##################################################
# initの指定を MultiLayerNet が受け取る形に合わせる
# gauss: 数値(例 0.01)
# Xavier / He: 文字列
init_map = {
"gauss": 0.01,
"xavier": "Xavier",
"he": "He",
}
act_list = ["sigmoid", "relu"]
init_list = ["gauss", "xavier", "he"]
results = {}
for act in act_list:
for init_name in init_list:
w_init = init_map[init_name]
print(f"Running: {act} × {init_name} (weight_init_std={w_init})")
tr, te = run_sgd_experiment(
activation=act,
weight_init_std=w_init,
hidden_size_list=hidden_size_list,
iters_num=iters_num,
batch_size=batch_size,
learning_rate=learning_rate,
plot_interval=plot_interval,
use_batchnorm=use_batchnorm,
seed=seed
)
results[(act, init_name)] = (tr, te)
##################################################
# 2行×3列で描画
##################################################
fig, axes = plt.subplots(2, 3, figsize=(16, 8))
fig.suptitle("Activation × Weight Initialization (SGD)", fontsize=16)
x = list(range(plot_interval, iters_num + 1, plot_interval))
for r, act in enumerate(act_list):
for c, init_name in enumerate(init_list):
ax = axes[r, c]
tr, te = results[(act, init_name)]
ax.plot(x, tr, label="train")
ax.plot(x, te, label="test")
ax.set_title(f"{act} × {init_name}")
ax.set_xlabel("iteration")
ax.set_ylabel("accuracy")
ax.set_ylim(0, 1.0)
ax.legend(loc="lower right")
plt.tight_layout()
plt.show()
活性化関数と重み初期化の組み合わせによる学習挙動を比較した結果、ReLU 関数と He 初期化の組み合わせが最も安定して高い精度に到達した。
一方、sigmoid 関数とガウス初期化では学習がほとんど進まず、 活性化関数と初期化手法の適切な組み合わせが重要であることが確認できた。
[try] バッチ正規化をして変化を見てみよう
前項のコードを使用して、use_batchnorm = True で実行する。
バッチ正規化なしのときに学習が停滞していた条件(特に sigmoid × gauss や relu × gauss)でも accuracy が大きく改善し、学習が安定して進むようになった。
これは、各層の出力(中間表現)をミニバッチ単位で正規化し、平均・分散を揃えることで、層をまたいだ信号のスケール変動が抑えられたためと考えられる。
その結果、勾配の大きさが極端に小さくなる(勾配消失)あるいは不安定になる状況が緩和され、学習が進みやすくなった。
また、バッチ正規化を用いることで、活性化関数や重み初期化に対する依存度が低下している点も確認できる。
バッチ正規化なしでは「ReLU × He」など特定の組み合わせが顕著に優位であったが、バッチ正規化ありでは多くの条件で高い精度に到達しており、初期化の差が相対的に小さくなっている。
以上より、バッチ正規化は学習の安定化と収束の改善に寄与し、重み初期化や活性化関数選択の難しさを一定程度緩和する手法であることが分かった。
確認テスト
モメンタム・AdaGrad・RMSProp の特徴をそれぞれ簡潔に説明せよ
- モメンタム(Momentum)
過去の勾配の移動平均を用いて更新方向に慣性を持たせることで、振動を抑えつつ一貫した方向へ学習を進め、収束を高速化する手法である。
- AdaGrad
各パラメータごとに過去の勾配の二乗和を蓄積し、頻繁に更新されるパラメータほど学習率を小さくすることで、学習率を自動調整する手法である。
- RMSProp
AdaGrad で学習率が極端に小さくなる問題を緩和するため、勾配の二乗の移動平均を用いて学習率を調整し、長期学習でも安定した更新を可能にする手法である。
参考図書 / 関連記事
- ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
- p.170 - 177
最適化の更新経路を図示していたので、コードを書いて図示して比較する。
import numpy as np
import matplotlib.pyplot as plt
##################################################
# 目的関数(細長い谷)と勾配
# f(x, y) = x^2/20 + y^2
##################################################
def f(xy):
x, y = xy
return (x ** 2) / 20.0 + (y ** 2)
def grad_f(xy):
x, y = xy
return np.array([x / 10.0, 2.0 * y])
##################################################
# Optimizers
##################################################
class SGD:
def __init__(self, lr=0.95):
self.lr = lr
def update(self, xy, g):
return xy - self.lr * g
class Momentum:
def __init__(self, lr=0.1, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = np.zeros(2)
def update(self, xy, g):
self.v = self.momentum * self.v - self.lr * g
return xy + self.v
class AdaGrad:
def __init__(self, lr=1.5, eps=1e-7):
self.lr = lr
self.eps = eps
self.h = np.zeros(2)
def update(self, xy, g):
self.h += g * g
return xy - self.lr * g / (np.sqrt(self.h) + self.eps)
class RMSProp:
def __init__(self, lr=0.1, decay=0.9, eps=1e-7):
self.lr = lr
self.decay = decay
self.eps = eps
self.h = np.zeros(2)
def update(self, xy, g):
self.h = self.decay * self.h + (1.0 - self.decay) * (g * g)
return xy - self.lr * g / (np.sqrt(self.h) + self.eps)
class Adam:
def __init__(self, lr=0.3, beta1=0.9, beta2=0.999, eps=1e-7):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.m = np.zeros(2)
self.v = np.zeros(2)
self.t = 0
def update(self, xy, g):
self.t += 1
self.m = self.beta1 * self.m + (1 - self.beta1) * g
self.v = self.beta2 * self.v + (1 - self.beta2) * (g * g)
m_hat = self.m / (1 - self.beta1 ** self.t)
v_hat = self.v / (1 - self.beta2 ** self.t)
return xy - self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
##################################################
# 軌跡生成
##################################################
def run(opt, xy0=np.array([-7.0, 2.0]), steps=30):
xy = xy0.astype(float).copy()
traj = [xy.copy()]
for _ in range(steps):
g = grad_f(xy)
xy = opt.update(xy, g)
traj.append(xy.copy())
return np.array(traj)
##################################################
# 描画
##################################################
optimizers = {
"SGD": SGD(lr=0.95),
"Momentum": Momentum(lr=0.1, momentum=0.9),
"AdaGrad": AdaGrad(lr=1.5),
"RMSProp": RMSProp(lr=0.1),
"Adam": Adam(lr=0.3),
}
x = np.linspace(-10, 10, 400)
y = np.linspace(-10, 10, 400)
X, Y = np.meshgrid(x, y)
Z = (X**2)/20.0 + (Y**2)
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.ravel()
for ax, (name, opt) in zip(axes, optimizers.items()):
ax.contour(X, Y, Z, levels=np.logspace(-1, 2, 20), linewidths=1)
traj = run(opt)
ax.plot(traj[:, 0], traj[:, 1], marker="o", linewidth=1.5)
ax.set_title(name)
ax.set_xlim(-10, 10)
ax.set_ylim(-10, 10)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(True, alpha=0.3)
# 余った1枠を非表示
for i in range(len(optimizers), len(axes)):
axes[i].axis("off")
fig.suptitle("Optimization trajectories (SGD / Momentum / AdaGrad / RMSProp / Adam)", fontsize=14)
plt.tight_layout()
plt.show()
細長い谷形状の目的関数に対して、以下のようなことが分かる。
- SGD は、勾配方向に対してジグザグに進むので非効率
- Momentum は、慣性により振動が抑制され、ジグザグ度合いが軽減されている
- AdaGrad と RMSProp は、学習率を自動調整することで安定した収束を示す
- Adam は、それらを統合した手法として,初期段階から高速かつ安定した最適化経路を描く
3. 過学習
深層学習モデルは表現力が高く、訓練データに対して誤差を小さくすることができる一方で、訓練データに過剰に適合してしまい、未知データで性能が落ちることがある。この現象を 過学習(overfitting) という。これを抑えて、汎化性能を向上させることが正則化の目的である。
深層学習における正則化は、大きく 陽的正則化(explicit regularization) と 陰的正則化(implicit regularization) に分類される。
陽的正則化は損失関数に正則化項を加える方法であり、陰的正則化は学習の進め方(ハイパーパラメータ設定など)そのものが正則化として働く方法である。
3.1 Weight decay と L1/L2 正則化(陽的正則化)
過学習の一因は、モデルが訓練データに合わせて重みを過度に大きくし、複雑な関数(高分散なモデル)を作ってしまうことである。そこで、重みの大きさにペナルティを課すことで、モデルの複雑さを抑える。
L2 正則化(Weight decay)
L2 正則化は、損失関数 $E(\boldsymbol{w})$ に対して重みの二乗和を加える。
$$ E_{\text{reg}}(\boldsymbol{w}) = E(\boldsymbol{w}) + \frac{\lambda}{2}|\boldsymbol{w}|_2^2 = E(\boldsymbol{w}) + \frac{\lambda}{2}\sum_i w_i^2 $$
このとき勾配は
$$ \nabla E_{\text{reg}}(\boldsymbol{w}) = \nabla E(\boldsymbol{w}) + \lambda \boldsymbol{w} $$
となり、勾配降下法による更新は
$$ \boldsymbol{w} \leftarrow \boldsymbol{w} - \eta\left(\nabla E(\boldsymbol{w}) + \lambda \boldsymbol{w}\right) $$
で与えられる。
右辺の $-\eta\lambda \boldsymbol{w}$ が 重みを原点方向へ縮める(減衰させる) ため、L2 正則化は Weight decay と呼ばれる。
これにより、重みが過度に大きくなることが抑えられ、過学習の抑制が期待できる。
L1 正則化
L1 正則化は、損失関数に重みの絶対値和を加える。
$$ E_{\text{reg}}(\boldsymbol{w}) = E(\boldsymbol{w}) + \lambda|\boldsymbol{w}|_1 = E(\boldsymbol{w}) + \lambda\sum_i |w_i| $$
L1 は多くの重みを ちょうど 0 にしやすい(疎性) ため、不要な特徴(結合)を削ってモデルを単純化する方向に働く。
一方、L2 は重みを連続的に小さくし、全体的に滑らかなモデルになりやすい。
3.2 ドロップアウト
ドロップアウトは、学習時に ランダムにノードを削除して学習させる 手法であり、過学習を抑制して汎化性能を向上させる目的で用いられる。陰的正則化の一種である。
各ユニットの出力を確率的に無効化することで、特定ユニットへの依存(共適応)を防ぎ、アンサンブル学習に近い効果を得られる。
学習時のマスク $\boldsymbol{m}$(要素が 0/1)を用いて
$$ \boldsymbol{h}' = \boldsymbol{m}\odot \boldsymbol{h} $$
のように中間層出力 $\boldsymbol{h}$ を部分的に落とす。
推論時は、学習時の期待値と整合するようにスケーリングする(実装流儀により、学習時にスケールする “inverted dropout” を使うことが多い)。
3.3 ドロップコネクト
ドロップコネクトは、ドロップアウトが「ノード(ユニット)を落とす」のに対し、結合(重み)をランダムに落とす 発想の正則化である。陰的正則化の一種である。
形式的には、重み行列 $W$ に対してマスク $M$ を掛け、
$$ \boldsymbol{u} = \boldsymbol{x}(M\odot W) + \boldsymbol{b} $$
のように、学習時だけ結合を確率的に無効化する。
ユニット単位で落とすより粒度が細かく、より強いランダム化として働く場合がある。
3.4 陰的正則化(エポック数・バッチサイズ・学習率)
陰的正則化は、「モデルの損失関数に正則化項を追加する」のではなく、「学習の進め方を決めるパラメータ調整が正則化の役割を果たす」ものであり、代表パラメータとして エポック数・バッチサイズ・学習率 が挙げられている。
- エポック数
訓練データ全てを入力として学習を行う回数。
増やしすぎると訓練誤差は下がり続ける一方で、検証誤差が上がり始める(過学習)ため、学習曲線を見て適切な回数で止める必要がある。
また、検証性能が改善しなくなった時点で学習を終了する 早期終了(アーリーストッピング) は、エポック数を通じた代表的な陰的正則化である。
- バッチサイズ
ミニバッチ学習における 1 バッチのデータ数。
バッチサイズが大きい設定では、大域最適解に収束しにくくなる場合がある。一般に、バッチサイズは勾配推定のノイズ量を変化させ、学習の安定性・汎化に影響する。
- 学習率
重み更新のステップ幅。
学習率が大きすぎると最適解近傍で振動や発散が起こり、小さすぎると学習が進まない。学習率の大きさにより大域最適解へ収束しにくくなる場合がある。
実装演習
配布コードの実行
配布コード 2_5_overfiting.ipynb の実行結果を載せる。
- 過学習
- Weight decay (L2)
- Weight decay (L1)
- ドロップアウト
- ドロップアウト + L1
[try] weigth_decay_lambda の値を変更して正則化の強さを確認しよう
Weight decay (L2) で確認する。
配布コードでは weight_decay_lambda = 0.1 となっているので、少し増やして weight_decay_lambda = 0.15 で実行する。
train / test の accuracy がさらに低下した。
これは正則化が強くなりすぎ、重みが過度に抑制された結果、モデルが十分な表現を学習できなくなったためと考えられる。
この結果から、Weight decay には過学習を抑制しつつ性能を保つ、適切な強さの範囲が存在することが分かる。
[try] dropout_ratio の値を変更してみよう
配布コードでは dropout_ratio = 0.15 となっているので、少し増やして dropout_ratio = 0.2 で実行する。
train / test の accuracy が、全体として低めに推移した。
これはドロップアウトがやや強く働き、学習時に有効なユニット数が減少したことで、特徴表現の学習が十分に進まなかったためと考えられる。
これより、ドロップアウトは過学習を抑制する一方で、強くしすぎると学習そのものを阻害することが確認できた。
[try] optimizer と dropout_ratio の値を変更してみよう
4 種類の optimizer を比較する。
ドロップアウト(dropout_ratio=0.15)を用いた場合でも、optimizer によって収束速度と汎化性能に明確な差が見られた。
SGD は収束が遅く test accuracy も低めである一方、Momentum・AdaGrad・Adam は初期から高速に収束し、test accuracy も高く安定した。
特に Adam は学習初期から安定した性能を示し、ドロップアウト下でも最も効率よく学習できることが確認できた。
確認テスト
下図について、L1 正則化を表しているグラフはどちらか答えよ
右側。
理由として、L1 正則化(Lasso) では制約領域がひし形(ダイヤモンド形)になり、等高線との接点が軸上に来やすいため、重みがちょうど 0 になる スパース解 が得られます。
一方、左側の円形の制約は L2 正則化(Ridge) を表しています。
参考図書 / 関連記事
- ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
- p.197
アンサンブル学習 とは、複数のモデルを個別に学習させ、推論時にそれらの出力を平均や多数決などで統合することで、汎化性能を向上させる手法である。
単一モデルに比べて予測のばらつきが抑えられ、過学習を起こしにくくなることが知られている。
ニューラルネットワークにおける Dropout は、このアンサンブル学習と密接な関係を持つ。Dropout は学習時にニューロンをランダムに無効化することで、毎回異なるネットワーク構造を学習していると解釈できる。
推論時には、Dropout によって無効化された割合を考慮して出力をスケーリングすることで、これら多数のネットワークの平均を近似的に取っている。
このように、Dropout は アンサンブル学習の効果を単一のネットワークで効率的に実現する正則化手法 であり、過学習の抑制と汎化性能の向上に寄与する。
4. 畳み込みニューラルネットワークの概念
全体像
畳み込みニューラルネットワーク(Convolutional Neural Network : CNN) は、主に画像認識などの空間構造を持つデータを扱うために設計された深層学習モデルである。
全結合層(Fully Connected Layer)では入力の位置関係が失われるのに対し、CNN では 空間的な局所構造を保ったまま特徴を抽出できる という特徴を持つ。
CNN は主に以下の層から構成される。
- 畳み込み層(Convolution Layer)
- プーリング層(Pooling Layer)
- (最終段で)全結合層
畳み込み層とプーリング層を交互に重ねることで、画像の局所的特徴から抽象的特徴へと段階的に表現を学習する。
4.1 畳み込み層(Convolution Layer)
畳み込み層は、入力データに対して フィルタ(カーネル) と呼ばれる小さな重み行列を滑らせながら積和演算を行い、特徴マップ(feature map)を生成する層である。
入力が
- 高さ $H$
- 幅 $W$
- チャンネル数 $C$
を持つ場合、入力形状は $H \times W \times C$ と表される。
フィルタは入力の局所領域にのみ接続されるため、全結合層と比べて パラメータ数が大幅に削減される と同時に、局所的な特徴(エッジや模様など)を効率よく捉えることができる。
出力サイズの計算
畳み込み層では、入力サイズ・フィルタサイズ・パディング・ストライドの関係から、出力される特徴マップの空間サイズが決まる。
入力の高さを $H$、幅を $W$、フィルタの高さを $F_H$、幅を $F_W$、パディングを $P$、ストライドを $S$ とすると、出力特徴マップの高さ $O_H$、幅 $O_W$ は次式で与えられる。
$$ O_H = \frac{H + 2P - F_H}{S} + 1 $$
$$ O_W = \frac{W + 2P - F_W}{S} + 1 $$
この式から分かるように、
- パディング $P$ を大きくすると出力サイズは大きくなる
- ストライド $S$ を大きくすると出力サイズは小さくなる
- フィルタサイズ $F_H, F_W$ が大きいほど出力は小さくなる
といった関係がある。
特に、
$$ P = \frac{F_H - 1}{2}, \quad S = 1 $$
と設定すると、出力サイズが入力サイズと等しくなり(same padding)、深い畳み込みネットワークでも空間サイズを保ったまま学習を進めることができる。
4.1.1 バイアス
畳み込み層では、各フィルタごとに 1 つのバイアス が用意される。
畳み込み演算によって得られた値にバイアスを加えた後、活性化関数が適用される。
$$ \text{output} = \text{convolution}(X, W) + b $$
このバイアスにより、出力の全体的なシフトが可能となり、表現力が向上する。
4.1.2 パディング
パディングとは、入力データの周囲に 値(通常は 0)を追加する処理 である。
パディングを行わない場合、畳み込みを重ねるごとに特徴マップのサイズは小さくなる。
パディングを用いることで、
- 出力サイズを入力と同じに保つ
- 画像の端の情報を十分に利用する
といった効果が得られる。
特に、入力と出力の空間サイズを同一に保つ same padding は、深い CNN でよく用いられる。
4.1.3 ストライド
ストライドとは、フィルタを どれだけの間隔で移動させるか を表すパラメータである。
- $S=1$:1 ピクセルずつずらして計算する(最も細かい)
- $S=2$:2 ピクセルずつ飛ばして計算する(出力が間引かれる)
- $S=3$:3 ピクセルずつ…(さらに粗くなる)
ストライドを大きくすると、出力サイズは小さくなり、特徴マップのダウンサンプリング効果 を持つ。
4.1.4 チャンネル
入力データがカラー画像の場合、通常は
- 赤(R)
- 緑(G)
- 青(B)
の 3 チャンネルを持つ。
畳み込み層のフィルタは、入力の全チャンネルにまたがる形で定義される。
例えば、入力が $H \times W \times 3$ の場合、フィルタは $kH \times kW \times 3$ の形状を持つ。
また、フィルタの個数 = 出力チャンネル数 であり、複数のフィルタを用いることで、異なる種類の特徴を同時に抽出できる。
4.2 プーリング層
プーリング層は、特徴マップの空間サイズを縮小する層であり、主に Max Pooling が用いられる。
例えば、$2 \times 2$ の領域から最大値を取り出すことで、
- 特徴マップのサイズ削減
- 計算量の削減
- 小さな位置ずれに対する頑健性の向上
といった効果が得られる。
プーリング層には学習すべきパラメータは存在せず、畳み込み層で抽出された特徴を整理・要約する役割を担う。
実装演習
基本的な CNN
配布コード 2_6_simple_convolution_network_after.ipynb の実行結果を載せる。
Simple Convolution Network を用いた学習では、学習初期から訓練精度・テスト精度ともに急速に向上し、最終的に高い認識精度に到達した。これは、畳み込み層によって画像の局所的特徴を効率よく抽出できたためであり、全結合層のみのネットワークと比べて学習が安定したと考えられる。
また、訓練精度がほぼ 1.0 に近づく一方で、テスト精度も高い値を維持しており、過度な過学習は見られなかった。これは、畳み込み層とプーリング層によるパラメータ削減と空間情報の集約が、汎化性能の向上に寄与していることを示している。
以上より、Simple Convolution Network であっても、画像認識タスクにおいては十分に高い性能を発揮でき、CNN の有効性を確認することができた。
[try] im2col の処理を確認しよう
- 関数内で transpose の処理をしている行をコメントアウトして下のコードを実行してみよう
確認用コード。
import numpy as np
def show_im2col(input_shape, filter_shape, stride, pad):
N, C, H, W = input_shape
FH, FW = filter_shape
x = np.arange(N*C*H*W).reshape(N, C, H, W)
col = im2col(x, FH, FW, stride=stride, pad=pad)
OH = (H + 2*pad - FH)//stride + 1
OW = (W + 2*pad - FW)//stride + 1
print()
print("x.shape =", x.shape)
print("FH,FW =", (FH, FW), " stride =", stride, " pad =", pad)
print("OH,OW =", (OH, OW))
print("col.shape =", col.shape) # 期待: (N*OH*OW, C*FH*FW)
print("col (先頭5行)\n", col[:5])
# ケースA
show_im2col((1,1,4,4), (2,2), stride=1, pad=0)
# ケースB
show_im2col((1,1,4,4), (2,2), stride=1, pad=1)
# ケースC
show_im2col((1,1,6,6), (3,3), stride=2, pad=0)
transpose あり。
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2) stride = 1 pad = 0
OH,OW = (3, 3)
col.shape = (9, 4)
col (先頭5行)
[[ 0. 1. 4. 5.]
[ 1. 2. 5. 6.]
[ 2. 3. 6. 7.]
[ 4. 5. 8. 9.]
[ 5. 6. 9. 10.]]
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2) stride = 1 pad = 1
OH,OW = (5, 5)
col.shape = (25, 4)
col (先頭5行)
[[0. 0. 0. 0.]
[0. 0. 0. 1.]
[0. 0. 1. 2.]
[0. 0. 2. 3.]
[0. 0. 3. 0.]]
x.shape = (1, 1, 6, 6)
FH,FW = (3, 3) stride = 2 pad = 0
OH,OW = (2, 2)
col.shape = (4, 9)
col (先頭5行)
[[ 0. 1. 2. 6. 7. 8. 12. 13. 14.]
[ 2. 3. 4. 8. 9. 10. 14. 15. 16.]
[12. 13. 14. 18. 19. 20. 24. 25. 26.]
[14. 15. 16. 20. 21. 22. 26. 27. 28.]]
transpose なし。
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2) stride = 1 pad = 0
OH,OW = (3, 3)
col.shape = (9, 4)
col (先頭5行)
[[ 0. 1. 2. 4.]
[ 5. 6. 8. 9.]
[10. 1. 2. 3.]
[ 5. 6. 7. 9.]
[10. 11. 4. 5.]]
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2) stride = 1 pad = 1
OH,OW = (5, 5)
col.shape = (25, 4)
col (先頭5行)
[[ 0. 0. 0. 0.]
[ 0. 0. 0. 1.]
[ 2. 3. 0. 4.]
[ 5. 6. 7. 0.]
[ 8. 9. 10. 11.]]
x.shape = (1, 1, 6, 6)
FH,FW = (3, 3) stride = 2 pad = 0
OH,OW = (2, 2)
col.shape = (4, 9)
col (先頭5行)
[[ 0. 2. 12. 14. 1. 3. 13. 15. 2.]
[ 4. 14. 16. 6. 8. 18. 20. 7. 9.]
[19. 21. 8. 10. 20. 22. 12. 14. 24.]
[26. 13. 15. 25. 27. 14. 16. 26. 28.]]
コメントアウト前後で col.shape は同じだが、各行に並ぶ要素の順序が異なっている。
transpose を行うことで、各行が「同一位置の受容野(C×FH×FW)」を表すように並び替えられ、畳み込みを行列積として正しく計算できる。
transpose を外すと並びの意味が崩れ、後段の行列積が畳み込みとして成立しなくなる。
- input_data の各次元のサイズやフィルターサイズ・ストライド・パディングを変えてみよう
確認コード。
import numpy as np
def run_case(input_shape, filter_shape, stride, pad):
N, C, H, W = input_shape
FH, FW = filter_shape
# 中身が分かりやすい連番入力
x = np.arange(N * C * H * W).reshape(N, C, H, W)
col = im2col(x, FH, FW, stride=stride, pad=pad)
OH = (H + 2 * pad - FH) // stride + 1
OW = (W + 2 * pad - FW) // stride + 1
print("===================================")
print(f"x.shape = {x.shape}")
print(f"FH,FW = {filter_shape}, stride = {stride}, pad = {pad}")
print(f"OH,OW = ({OH}, {OW})")
print(f"col.shape = {col.shape}") # (N*OH*OW, C*FH*FW)
print("col (先頭5行)")
print(col[:5])
print()
print("ケース1:基本(基準)")
run_case(
input_shape=(1, 1, 4, 4),
filter_shape=(2, 2),
stride=1,
pad=0
)
print("ケース2:padding の効果")
run_case(
input_shape=(1, 1, 4, 4),
filter_shape=(2, 2),
stride=1,
pad=1
)
print("ケース3:stride の効果")
run_case(
input_shape=(1, 1, 6, 6),
filter_shape=(3, 3),
stride=2,
pad=0
)
print("ケース4:フィルタサイズの効果")
run_case(
input_shape=(1, 1, 6, 6),
filter_shape=(4, 4),
stride=1,
pad=0
)
実行結果。
ケース1:基本(基準)
===================================
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2), stride = 1, pad = 0
OH,OW = (3, 3)
col.shape = (9, 4)
col (先頭5行)
[[ 0. 1. 4. 5.]
[ 1. 2. 5. 6.]
[ 2. 3. 6. 7.]
[ 4. 5. 8. 9.]
[ 5. 6. 9. 10.]]
ケース2:padding の効果
===================================
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2), stride = 1, pad = 1
OH,OW = (5, 5)
col.shape = (25, 4)
col (先頭5行)
[[0. 0. 0. 0.]
[0. 0. 0. 1.]
[0. 0. 1. 2.]
[0. 0. 2. 3.]
[0. 0. 3. 0.]]
ケース3:stride の効果
===================================
x.shape = (1, 1, 6, 6)
FH,FW = (3, 3), stride = 2, pad = 0
OH,OW = (2, 2)
col.shape = (4, 9)
col (先頭5行)
[[ 0. 1. 2. 6. 7. 8. 12. 13. 14.]
[ 2. 3. 4. 8. 9. 10. 14. 15. 16.]
[12. 13. 14. 18. 19. 20. 24. 25. 26.]
[14. 15. 16. 20. 21. 22. 26. 27. 28.]]
ケース4:フィルタサイズの効果
===================================
x.shape = (1, 1, 6, 6)
FH,FW = (4, 4), stride = 1, pad = 0
OH,OW = (3, 3)
col.shape = (9, 16)
col (先頭5行)
[[ 0. 1. 2. 3. 6. 7. 8. 9. 12. 13. 14. 15. 18. 19. 20. 21.]
[ 1. 2. 3. 4. 7. 8. 9. 10. 13. 14. 15. 16. 19. 20. 21. 22.]
[ 2. 3. 4. 5. 8. 9. 10. 11. 14. 15. 16. 17. 20. 21. 22. 23.]
[ 6. 7. 8. 9. 12. 13. 14. 15. 18. 19. 20. 21. 24. 25. 26. 27.]
[ 7. 8. 9. 10. 13. 14. 15. 16. 19. 20. 21. 22. 25. 26. 27. 28.]]
入力サイズ、フィルタサイズ、ストライド、パディングを変更すると、出力サイズ(OH,OW)および im2col によって展開される行列の形状が変化することが確認できた。
特に、パディングは周辺情報の保持に、ストライドは空間的な間引きに対応しており、これらの組み合わせによって畳み込み層の振る舞いが制御されている。
[try] col2imの処理を確認しよう
- im2col の確認で出力した col を image に変換して確認しよう
確認コード。
import numpy as np
##################################################
# 入力データの準備
##################################################
N, C, H, W = 1, 1, 4, 4
x = np.arange(N * C * H * W).reshape(N, C, H, W)
FH, FW = 2, 2
stride = 1
pad = 0
print("x (input image)")
print(x[0, 0])
print()
##################################################
# im2col
##################################################
col = im2col(x, FH, FW, stride=stride, pad=pad)
print("col.shape =", col.shape)
print("col (先頭5行)")
print(col[:5])
print()
##################################################
# col2im
##################################################
x_rec = col2im(col, x.shape, FH, FW, stride=stride, pad=pad)
print("x_rec (col2im result)")
print(x_rec[0, 0])
print()
##################################################
# 重なり回数の確認
##################################################
x_ones = np.ones_like(x)
col_ones = im2col(x_ones, FH, FW, stride=stride, pad=pad)
cnt = col2im(col_ones, x.shape, FH, FW, stride=stride, pad=pad)
print("overlap count map")
print(cnt[0, 0])
実行結果。
col2im により col を画像形状へ戻すと、元の入力と一致しない結果が得られた。
これは、畳み込みにおいて受容野が重なる部分が加算されて復元されるためである。
col2im は im2col の単純な逆変換ではなく、逆伝播計算などで必要な加算処理を含む復元であることが確認できた。
確認テスト
サイズ 6×6 の入力画像を、サイズ 2×2 のフィルタで畳み込んだ時の出力画像のサイズを答えよ。なお、ストライドとパディングは 1 とする。
出力画像のサイズは 7 × 7 。
畳み込み層の出力サイズは
$$ O = \frac{H + 2P - F}{S} + 1 $$
で求められる。
ここで
- 入力サイズ $H = 6$
- フィルタサイズ $F = 2$
- パディング $P = 1$
- ストライド $S = 1$
を代入すると、
$$ O = \frac{6 + 2\times1 - 2}{1} + 1 = 7 $$
となる。
参考図書 / 関連記事
- ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
- p.207
なぜ、全結合層ではなく、畳み込みニューラルネットワークを用いるのか。
全結合層(Affine 層)を用いたニューラルネットワークでは、隣接する層のすべてのニューロンが結合されるため、入力データの形状を考慮せずに処理が行われる。
画像データの場合、本来は「縦・横・チャンネル」という 3 次元の構造を持っているが、全結合層に入力する際にはこれを 1 次元に並べ替える必要があり、空間的な位置関係が失われてしまう。
しかし画像には、近接するピクセル同士が似た値を持つ、チャンネル間に相関があるなど、形状に基づく重要な情報 が含まれている。全結合層では、これらの局所的な構造や距離の概念を捉えることができず、効率的な特徴抽出が難しいという問題がある。
一方、畳み込み層では入力の形状を保ったまま処理を行い、局所領域に対して同じフィルタを適用することで特徴を抽出する。そのため、空間的な構造を活かした学習が可能となり、パラメータ数も全結合層に比べて大幅に削減できる。
このように、畳み込みニューラルネットワークは、画像の持つ構造的特性を適切に扱える点で全結合層よりも優れており、画像認識などのタスクにおいて高い性能を発揮する。
5. 最新の CNN
近年の畳み込みニューラルネットワーク(CNN)は、単に層を深くするだけでなく、過学習の抑制や学習の安定化を重視した設計が行われている。
代表的なモデルである AlexNet では、畳み込み層とプーリング層を重ねる基本構造は LeNet と大きく変わらないものの、活性化関数として ReLU を採用し、全結合層に Dropout を導入することで深いネットワークの学習を可能にした。
このように、最新の CNN は構造そのものの革新というよりも、正則化手法や学習を安定させる工夫を取り入れることで性能を向上させており、実用的な深層モデルへと発展している。
参考図書 / 関連記事
- ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
- p.237 - 238
本書では、LeNet から AlexNet に至る CNN の発展について解説されており、両者のネットワーク構造には本質的な大きな違いがないことが示されている。
一方で、AlexNet では ReLU の導入や Dropout、LRN などの手法が用いられ、より深いネットワークの学習が可能となった点が強調されている。
また、CNN の発展には大量のデータと GPU による高速な並列計算環境の普及が大きく寄与したことが述べられている。
6. [フレームワーク演習] 正則化/最適化
ニューラルネットワークは高い表現能力を持つ一方で、モデルが複雑になりすぎると訓練データに過剰に適合し、汎化性能が低下する問題が生じる。
このような過学習や、学習が不安定になる問題を抑制するために、正則化や正規化といった手法が用いられる。
6.1 過学習が起きる理由
過学習とは、訓練データに対する誤差は小さいにもかかわらず、未知のデータに対する誤差が大きくなる現象である。
学習の進行に伴い、訓練誤差は減少し続けるが、テスト誤差が途中から増加する場合、モデルは訓練データに特化した学習を行っていると考えられる。
過学習が起きる主な原因として、以下が挙げられる。
- パラメータ数やノード数が多く、モデルが複雑すぎる
- パラメータの値が特定の方向に偏っている
- 学習データが不足している、あるいは偏っている
これらはいずれも、モデルの自由度が過剰であることに起因している。
6.2 正則化とは
正則化(Regularization)とは、ネットワークの自由度(層数、ノード数、パラメータの値など)に制約を与え、モデルの複雑さを抑制することで、訓練データへの過剰適合を防ぐ手法である。
正則化を導入することで、訓練サンプルに対する過剰な適合が抑えられ、結果として汎化性能の向上が期待できる。
6.3 パラメータ正則化(L1・L2・Elastic Net)
パラメータ正則化では、誤差関数に正則化項を加えることで、重み(パラメータ)の大きさを制御する。一般に、以下の形で誤差関数が拡張される。
$$ E(\boldsymbol{w}) + \lambda \| \boldsymbol{w} \|_p $$
ここで $p$ によって正則化の種類が決まる。
-
L1正則化(p=1)
重みの絶対値和を抑制し、不要なパラメータを0にしやすい。結果としてスパースなモデルが得られる。
-
L2正則化(p=2)
重みの二乗和を抑制し、パラメータの発散を防ぐ。重み全体を小さく保つ効果がある。
-
Elastic Net
L1正則化とL2正則化を組み合わせた手法であり、それぞれの正則化項の強さはハイパーパラメータによって調整される。
6.4 正則化レイヤー ― Dropout
Dropout は学習時にノードをランダムに無効化し、一部の経路を遮断した状態で学習を行う手法である。これにより、特定のノードや経路への依存が抑えられ、より汎化性能の高いモデルが得られる。
Dropout は、複数の異なるネットワークを学習させていると解釈でき、アンサンブル学習と類似した効果を、単一のネットワークで実現していると考えられる。
6.5 正規化レイヤー(Batch / Layer / Instance 正規化)
正規化レイヤーでは、レイヤー間を流れるデータの分布を正規化し、平均0・分散1となるように調整する。これにより、学習の安定化や収束の高速化が期待できる。
-
Batch正規化(Batch Normalization)
ミニバッチ内の同一チャネルを正規化単位とする。バッチサイズが小さい場合、統計量が不安定になり効果が薄れることがある。
-
Layer正規化(Layer Normalization)
各サンプルごとに、H×W×C全体を正規化単位とする。ミニバッチサイズに依存しない点が特徴である。
-
Instance正規化(Instance Normalization)
各サンプルの各チャネルごとに正規化を行う。Batch正規化においてバッチサイズが1の場合と等価な処理と捉えられる。
実装演習
2_9_regularization.ipynb
本実験では、正則化の適用対象を activity_regularizer から kernel_regularizer に変更した。
activity_regularizer は各層の出力(活性値)に対して制約を与えるのに対し、kernel_regularizer は 重みパラメータそのもの に正則化項を加える。そのため、L1/L2 正則化の本来の目的である「モデルの複雑さ(重みの大きさ)を抑制する」という効果を、より直接的に反映できる。
また、正則化の強さを示す regularization_method_weight を 0.0001 に設定した。
これは、正則化を弱めにかけることで、学習能力を極端に損なうことなく、過学習のみを緩和することを狙った設定である。
- 正則化なし(None)
学習誤差(loss)はエポックの増加とともに単調に減少している一方で、検証誤差(val_loss)は途中から増加している。
これは、モデルが訓練データに過剰に適合し、汎化性能が低下している 典型的な過学習の挙動 である。
正則化を行わない場合、重みが自由に更新されるため、モデルの複雑さが過剰になりやすいことが確認できる。
- L1 正則化
L1 正則化を適用した場合、学習誤差・検証誤差ともに減少はするものの、全体として誤差の値は高めで推移している。
これは、正則化係数を 0.0001 としたことで、L1 正則化の影響が比較的強く働き、重みが積極的に 0 に近づけられた結果、モデルの表現力が抑制されすぎている(アンダーフィッティング気味) 状態であると考えられる。
- L2 正則化
L2 正則化では、学習誤差は着実に減少している一方で、検証誤差は途中から増加している。
正則化なしの場合と比べると過学習の進行はやや緩和されているが、係数 0.0001 では 正則化が十分とは言えず、後半で再び過学習の兆候が現れている。
L2 正則化は重みの発散を抑える効果を持つが、本設定ではその効果が限定的であることが読み取れる。
- L1 + L2 正則化(Elastic Net)
L1L2 正則化では、学習誤差と検証誤差の差が比較的小さく、全体として安定した推移を示している。
L1 によるスパース化と L2 による重み抑制が同時に働くことで、モデルの複雑さが適度に制御され、過学習と学習不足のバランスが最も良い状態 になっていると考えられる。
2_10_layer-normalization.ipynb
Instance Normalization については、TensorFlow Addons が利用できない環境であったこと、また本課題の主目的が「代表的な正規化手法の挙動比較」にあることから、今回は比較対象から除外した。
- 正規化なし(None)
訓練損失(loss)は単調に減少する一方で、検証損失(val_loss)は途中から増加に転じており、訓練データに過度に適合した 過学習 が確認できた。
- Batch Normalization
学習初期における val_loss の低下が最も早く、正規化なしの場合と比較して 学習の安定化および汎化性能の改善 が見られた。
ただし、後半では val_loss が再び増加しており、正規化のみでは過学習を完全に防ぐことはできないことが分かる。
- Layer Normalization
訓練損失は大きく低下したものの、val_loss は早期から増加傾向を示し、汎化性能の改善にはつながらなかった。
CNN 構造では Batch Normalization の方が適している場合が多く、本実験結果もその傾向を支持するものとなった。
2_11_dropout.ipynb
Dropout を使用しない場合、訓練損失(loss)は継続的に減少している一方で、検証損失(val_loss)は途中から増加しており、学習が進むにつれて 過学習が発生している ことが確認できる。
一方、Dropout を適用した場合、訓練損失の減少は緩やかになるものの、検証損失は全体として低い水準で推移し、大きな増加は見られなかった。これは、学習時にランダムにノードを無効化することで、特定の特徴や重みに過度に依存することが抑制され、汎化性能が向上した ためと考えられる。
以上より、Dropout は学習を意図的に難しくする代償として、過学習を効果的に抑制し、検証データに対する性能を安定させる手法であることが、本実験結果からも確認できた。
参考図書 / 関連記事
- ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
この項で扱った正則化および学習安定化手法については、すでに前項までで理論的背景および実装結果を示したとおりであるが、これらはいずれも『ゼロから作る Deep Learning』において体系的に整理されている内容である。
同書では、ニューラルネットワークが高い表現能力を持つ一方で、訓練データへの過適合(過学習)が生じやすいことが指摘されており、その代表的な対策として L1・L2 正則化、Dropout、正規化レイヤーが紹介されている。
特に、重みの大きさを制約する正則化はモデルの自由度を抑制する役割を持ち、Dropout はニューロン間の共適応を防ぐことで汎化性能を向上させる手法として説明されている。
本レポートにおける実装結果でも、正則化や Dropout を導入した場合に、訓練損失と検証損失の乖離が緩和される傾向が確認でき、参考図書で述べられている理論と整合的な挙動が観測された。
また、Batch Normalization などのような正規化手法についても、学習の安定化や収束速度の向上を目的とした技術として位置づけられており、実装演習を通じて、それらの効果と適用条件を実験的に確認することができた。
以上より、この項の内容は参考図書に基づく理論を、実装および結果の考察を通じて具体化したものといえる。
0 件のコメント:
コメントを投稿