はじめに
今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #29 ~深層学習超入門編05~
次の記事:Python初心者の備忘録 #31 ~深層学習超入門編07~
今回は重みの初期化、応用的なOptimizerについてまとめております。
■学習に使用している資料
Udemy:②米国AI開発者がやさしく教える深層学習超入門第二弾【Pythonで実践】
■重みの初期化
- 重みの初期化はNNの学習において非常に重要
※全て0、または同じ値に初期化することはできない(対称性の問題) - 重みを適切に初期化をすることで、勾配消失/爆発の問題を軽減する
▶勾配消失/勾配爆発問題
勾配消失 (Vanishing Gradient)
- 層が深くなるにつれ勾配が非常に小さくなり、重みの更新が行われない
勾配爆発(Exploding Gradient)
- 層が深くなるにつれ勾配が非常に大きくなり、重みの更新が大きくなり値が発散してしまう
▶Xavier初期化
- 各層の入力と出力の分散が等しく保たれるようにする
- 活性化関数がシグモイド関数やtanh関数の場合、中心部分で線形に近く有効
※ReLU系の場合、中心部分が線形ではないのでXavier初期化は最適ではない( -> 現代ではほぼ使われない)

※$W^{[l]}~N(\mu, \sigma^2)$:平均$\mu$、分散$\sigma^2$の正規分布からランダム抽出
※※$W^{[l]}~u(a, b)$:a~bの間の値を同じ確率で抽出(一様分布)
▶Kaiming(He)初期化
- ReLUを考慮していないXavierの初期化を改良し、ReLUに対応させたもの
-> 非常に一般的に使われている
PythonでXavier初期化とKaiming初期化
- Xavier初期化、およびKaiming初期化をした重み行列を複数回順伝播しても、出力が消失/爆発しないことを確認する
from functools import partial
import matplotlib.pyplot as plt
import numpy as np
import torch
from torch.nn import init
from torch.nn import functional as F
from torch.utils.data import DataLoader
from torch import nn, optim
import torchvision
from torchvision import transforms
%load_ext autoreload
%autoreload 2
import utils
X = torch.randn(60, 30)
for i in range(50): # 数字をあげていくと結果もさらに大きくなっていく
W = torch.randn(30, 30) # * 0.01 #重みが小さいと今度は消失する
X = X @ W.T
""" Xの出力結果:10の35乗と爆発している
tensor([[ 1.5184e+35, -1.7022e+36, -1.2551e+36, ..., 3.0901e+35,
-2.3487e+35, 2.1645e+34],
[ 2.6464e+36, -3.6710e+36, -1.0742e+36, ..., -4.8010e+34,
-9.9407e+35, -1.2655e+36],
[ 4.9870e+35, -1.5429e+36, -8.0832e+35, ..., 3.8149e+35,
-5.0151e+35, 7.4230e+33],
..,
[-1.2402e+36, 7.6386e+35, -3.9677e+35, ..., -3.0030e+35,
5.2264e+35, 4.2677e+34],
[ 9.6340e+35, -1.4272e+36, -4.5870e+35, ..., 3.7332e+34,
-3.7366e+35, -4.2797e+35],
[ 1.2169e+36, -1.4359e+36, -3.6430e+35, ..., -3.7414e+35,
-1.8559e+35, -7.8265e+35]])
"""
X = torch.randn(60, 30)
n_in, n_out = 30, 30
# 正規分布: N(0, 2/(n_in+n_out))
std = torch.sqrt(torch.tensor(2. / (n_in + n_out)))
# 一様分布: U(-sqrt(6/((n_in+n_out))), sqrt(6/((n_in+n_out))))
limit = torch.sqrt(torch.tensor(6./(n_in + n_out)))
for i in range(50):
# 正規分布
# W = torch.randn(n_out, n_in) * std
# 一様分布
W = torch.rand(n_out, n_in)*2*limit - limit
X = X @ W.T
print(X.mean(), X.std()) # -> tensor(0.0022) tensor(0.3088)
""" Xの出力結果:消失/爆発していない
tensor([[ 0.1376, 0.1937, 0.2161, ..., -0.0641, 0.1980, 0.2853],
[ 0.0264, 0.1679, -0.1516, ..., 0.0121, -0.1168, 0.2115],
[ 0.1682, 0.1805, 0.1737, ..., 0.0313, 0.0983, 0.1244],
..,
[ 0.2490, 0.4636, 0.0394, ..., -0.0249, 0.0310, 0.5654],
[-0.1959, -0.2771, -0.2534, ..., -0.0381, -0.2811, -0.2617],
[-0.0081, -0.0019, 0.0338, ..., -0.0802, 0.0399, 0.1365]])
"""
X = torch.randn(60, 30)
n_in, n_out = 30, 30
# 正規分布: N(0, 2/(n_in))
std = torch.sqrt(torch.tensor(2. / n_in))
# 一様分布: U(-sqrt(6/(n_in)), sqrt(6/(n_in)))
limit = torch.sqrt(torch.tensor(6./n_in))
for i in range(50):
W = torch.randn(n_out, n_in) * std
# W = torch.rand(n_out, n_in)*2*limit - limit
X = X @ W.T
X = torch.clamp(X, min=0.) # ReLU
print(X.mean(), X.std()) # -> tensor(0.4366) tensor(0.7427)
""" Xの出力結果:消失/爆発していない
tensor([[7.9577e-01, 3.1131e-01, 2.1504e+00, ..., 0.0000e+00, 0.0000e+00,
2.4906e+00],
[2.1334e-01, 6.8831e-02, 4.7841e-01, ..., 0.0000e+00, 0.0000e+00,
5.9416e-01],
[2.0947e-01, 6.7180e-02, 4.5081e-01, ..., 0.0000e+00, 0.0000e+00,
5.5637e-01],
..,
[2.0467e-01, 6.5977e-02, 4.8415e-01, ..., 0.0000e+00, 0.0000e+00,
6.0053e-01],
[2.5607e-01, 7.4780e-02, 5.5565e-01, ..., 2.0172e-03, 0.0000e+00,
7.0523e-01],
[1.6209e-01, 5.9801e-02, 3.5403e-01, ..., 0.0000e+00, 0.0000e+00,
4.2128e-01]])
"""
PytorchでKaiming初期化
-
torch.nn.init.kaiming_normal_()関数を使って、対象の層の重みを初期化する-
tensor:初期化対象となる重み(layer.weight)
-
- 学習前のmodelをイテレーションし、それぞれの層を初期化する
- 実行する際に、
nn.Linearやnn.Conv2dなど層を指定して初期化する
- 実行する際に、
-
nn.Linearとnn.Conv2dのデフォルトの重みの初期値は以下のとおり
conv = nn.Conv2d(1, 8, kernel_size=3, stride=2, padding=1) # default std= np.sqrt(1/3*n_in*k*k)
# Kaiming初期化
init.kaiming_normal_(conv.weight) # kaiming std= np.sqrt(2/n_in*k*k)
print(conv.weight.mean(), conv.weight.std())
# tensor(-0.0177, grad_fn=<MeanBackward0>) tensor(0.4742, grad_fn=<StdBackward0>)
# 標準偏差が sqrt(2/(3*3))と近しいことを確認
np.sqrt(2/9) # -> 0.4714045207910317
Batch norm + Kaiming init(バッチ正規化とKaiming初期化)
# model準備
def get_conv_model():
return nn.Sequential(
# 1x28x28
nn.Conv2d(1, 4, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(4), # batch norm
nn.ReLU(),
# 4x14x14
nn.Conv2d(4, 8, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(8), # batch norm
nn.ReLU(),
# 8x7x7
nn.Conv2d(8, 16, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(16), # batch norm
nn.ReLU(),
# 16x4x4
nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(32), # batch norm
nn.ReLU(),
# 32x2x2 -> GAP -> 32 x 1 x 1
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(32, 10)
# 10
)
conv_model = get_conv_model()
# kaiming初期化
for layer in conv_model:
if isinstance(layer, nn.Linear) or isinstance(layer, nn.Conv2d):
init.kaiming_normal_(layer.weight)
# データ準備
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,) )
])
train_dataset = torchvision.datasets.FashionMNIST('./fmnist_data', train=True, download=True, transform=transform)
val_dataset = torchvision.datasets.FashionMNIST('./fmnist_data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=1024, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=1024, num_workers=4)
# 学習
opt = optim.SGD(conv_model.parameters(), lr=0.6)
act_stats = utils.ActivationStatistics(conv_model)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
# 描画
act_stats.plot_statistics()
■応用的なOptimizer
今まで使用していたOptimizerであるSGDには下記のような問題点があり、それらを解決するために様々なOptimizerが開発されている
- 局所最適解や鞍点に比較的お陥りやすい(何も対策をしていない)
- 学習率への依存度が高い
- 適切な学習率を設定するのが難しい
- 損失関数の形状によっては収束速度が極端に遅くなる場合がある
▶Momentum
- SGDは損失関数が谷間形状にある場合収束が遅くなる
- Momentumでは、物理学の概念を使って重みの更新に前回の更新量も考慮することで谷間状でも収束速度を向上させる
▶指数移動平均(EMA:Exponential Moving Average)
- 過去のデータに対して指数関数的に減衰する重みを使って平均を取る
-> 最近のデータの方が、古いデータよりも影響を持つようにしている
PythonでEMAを実装
- y = sin(x)に対してEMAを計算し描画してみる
Optimizer
import numpy as np
import matplotlib.pyplot as plt
import torch
from torch.nn import init
from torch.nn import functional as F
from torch.utils.data import DataLoader
from torch import nn, optim
import torchvision
from torchvision import transforms
%load_ext autoreload
%autoreload 2
import utils
x = np.linspace(0, 2*np.pi, 10)
y = np.sin(x)
# EMA
ema = 0
alpha = 0.8
results = []
for y_i in y:
ema = alpha*ema + (1-alpha)*y_i
results.append(ema)
# 描画
plt.plot(x, y, 'o')
plt.plot(x, y, '-', label='sin(x)')
plt.plot(x, results, label='EMA')
plt.legend()
PythonでMomentum Optimizerを実装
- 以前のOptimizerをMomentumで改良する
以前のOptimizer
# 以下のクラスは第一部のOptimizerのセクションから
class Optimizer():
def __init__(self, parameters, lr=0.03):
self.parameters = list(parameters)
self.lr = lr
def step(self):
with torch.no_grad():
for param in self.parameters:
param -= self.lr * param.grad
def zero_grad(self):
for param in self.parameters:
if param.grad is not None:
param.grad.zero_()
# 改良したopt
class MomentumOptimizer():
def __init__(self, parameters, lr=0.03, momentum=0.9):
self.parameters = list(parameters)
self.lr = lr
self.momentum = momentum
self.v = [torch.zeros_like(param) for param in self.parameters]
def step(self):
with torch.no_grad():
for param, v in zip(self.parameters, self.v):
v[:] = self.momentum * v + param.grad
param -= self.lr * v
def zero_grad(self):
for param in self.parameters:
if param.grad is not None:
param.grad.zero_()
PytorchでMomentum
-
torch.optim.SGDのmomentum引数に減衰係数(β)を指定する
# 動作確認
conv_model = utils.get_conv_model()
# データ準備
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,) )
])
train_dataset = torchvision.datasets.FashionMNIST('./fmnist_data', train=True, download=True, transform=transform)
val_dataset = torchvision.datasets.FashionMNIST('./fmnist_data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=1024, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=1024, num_workers=4)
# SDGで学習
opt = optim.SGD(conv_model.parameters(), lr=0.03)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
"""
epoch: 0: train error: 1.4674616445929318, validation error: 0.9250866413116455, validation accuracy: 0.692083865404129
epoch: 1: train error: 0.7235312532570403, validation error: 0.6280175507068634, validation accuracy: 0.7778738856315612
epoch: 2: train error: 0.5578220926098905, validation error: 0.5409298360347747, validation accuracy: 0.8068060398101806
"""
# Momentumで学習
opt = optim.SGD(conv_model.parameters(), lr=0.03, momentum=0.9)
# opt = MomentumOptimizer(conv_model.parameters(), lr=0.03, momentum=0.9)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
"""
epoch: 0: train error: 1.3806309134273205, validation error: 0.8245054423809052, validation accuracy: 0.7198341846466064
epoch: 1: train error: 0.6624414738962205, validation error: 0.602623051404953, validation accuracy: 0.785443240404129
epoch: 2: train error: 0.5251485599299609, validation error: 0.5096647053956985, validation accuracy: 0.8168845653533936
"""
MomentumのほうがAccuracyの学習が少し早くなっているのがわかる
▶RMSProp(Root Mean Square Propagation)
- 各パラメータに対する学習率を動的に調整する
- 更新(勾配)が大きいパラメータに対しては学習率を低くする
- 逆に更新が小さいパラメータに対しては学習率を高くする
- これにより、学習の収束を早める
- 勾配の二乗の移動平均を使って学習率を調整する
- 勾配の多きさを捉えるために二乗(非負)
PythonでRMSPropを実装
class RMSPropOptimizer():
def __init__(self, parameters, lr=0.03, beta=0.9, eps=1e-8):
self.parameters = list(parameters)
self.lr = lr
self.beta = beta
self.eps = eps
self.sqr_avg = [torch.zeros_like(param) for param in self.parameters]
def step(self):
with torch.no_grad():
for param, sqr_avg in zip(self.parameters, self.sqr_avg):
sqr_avg[:] = self.beta * sqr_avg + (1-self.beta) * param.grad**2
param -= self.lr * param.grad/(torch.sqrt(sqr_avg)+self.eps)
def zero_grad(self):
for param in self.parameters:
if param.grad is not None:
param.grad.zero_()
PytorchでRMSProp
-
torch.optim.RMSpropクラス-
alpha:減衰係数β -
lr:学習率 -
eps:微小数値
-
conv_model = utils.get_conv_model()
# opt = optim.SGD(conv_model.parameters(), lr=0.03, momentum=0.9)
# opt = RMSPropOptimizer(conv_model.parameters(), lr=0.03, beta=0.9)
opt = optim.RMSprop(conv_model.parameters(), lr=0.03, alpha=0.9)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
"""
epoch: 0: train error: 0.7473576877076747, validation error: 0.9195898830890655, validation accuracy: 0.7195332407951355
epoch: 1: train error: 0.45726819260645724, validation error: 0.5333310753107071, validation accuracy: 0.8110929548740387
epoch: 2: train error: 0.39645103579860624, validation error: 0.5383158534765243, validation accuracy: 0.8049884378910065
"""
▶バイアス補正
- EMAでは通常、初期のイテレーションでは勾配とモメンタムを過小評価してしまう
(真の移動平均と乖離してしまう=バイアス) - バイアス補正を行うことで、過小評価を緩和する
- 指数移動平均に対して、$\frac{1}{1-\beta^t}$を掛けることでバイアス補正をかける
RMSPropにバイアス補正を追加してみる
# バイアス補正追加バージョン
class RMSPropOptimizer():
def __init__(self, parameters, lr=0.03, beta=0.9, eps=1e-8):
self.parameters = list(parameters)
self.lr = lr
self.beta = beta
self.eps = eps
self.sqr_avg = [torch.zeros_like(param) for param in self.parameters]
self.t = 0 # カウント用
def step(self):
self.t += 1
with torch.no_grad():
for param, sqr_avg in zip(self.parameters, self.sqr_avg):
sqr_avg[:] = self.beta * sqr_avg + (1-self.beta) * param.grad**2
sqr_avg_corrected = sqr_avg / (1-self.beta**self.t)
param -= self.lr * param.grad/(torch.sqrt(sqr_avg_corrected)+self.eps)
def zero_grad(self):
for param in self.parameters:
if param.grad is not None:
param.grad.zero_()
# 動作確認
conv_model = utils.get_conv_model()
opt = RMSPropOptimizer(conv_model.parameters(), lr=0.03, beta=0.9)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
"""
epoch: 0: train error: 0.7658421902333276, validation error: 0.536114689707756, validation accuracy: 0.8063855230808258
epoch: 1: train error: 0.4590813982284675, validation error: 0.5572960704565049, validation accuracy: 0.7916035532951355
epoch: 2: train error: 0.3966816507153592, validation error: 0.42308476865291594, validation accuracy: 0.8431600749492645
"""
epoch0のAccracyが少し良くなっているし、学習の進みもよくなっているのがわかる
Adam(Adaptive Moment Estimation)
- MomentumとRMSPropを組み合わせたOptimizerで非常に一般的に使われる
※「迷ったらこれを使う」ぐらいよく使われる
PythonでAdamをスクラッチ実装
- 学習率, $\beta_1$, $\beta_2$, $\varepsilon$を引数にとるOptimizerのクラスを実装する
class AdamOptimizer():
def __init__(self, parameters, lr=0.03, beta1=0.9, beta2=0.999, eps=1e-8):
self.parameters = list(parameters)
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.v = [torch.zeros_like(param) for param in self.parameters]
self.s = [torch.zeros_like(param) for param in self.parameters]
self.t = 0 # カウント用
def step(self):
self.t += 1
with torch.no_grad():
for param, v, s in zip(self.parameters, self.v, self.s):
# 1次モーメント
v[:] = self.beta1 * v + (1-self.beta1) * param.grad
v_corrected = v / (1-self.beta1**self.t)
# 2次モーメント
s[:] = self.beta2 * s + (1-self.beta2) * param.grad**2
s_corrected = s / (1-self.beta2**self.t)
# パラメータ更新
param -= self.lr * v_corrected/(torch.sqrt(s_corrected)+self.eps)
def zero_grad(self):
for param in self.parameters:
if param.grad is not None:
param.grad.zero_()
conv_model = utils.get_conv_model()
opt = AdamOptimizer(conv_model.parameters(), lr=0.03, beta1=0.9, beta2=0.999)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
"""
epoch: 0: train error: 0.6832892950308525, validation error: 0.5199526458978653, validation accuracy: 0.8087810933589935
epoch: 1: train error: 0.39591438739986745, validation error: 0.4033509731292725, validation accuracy: 0.8530532538890838
epoch: 2: train error: 0.34351947802608296, validation error: 0.60826216340065, validation accuracy: 0.7851442933082581
"""
PytorchでAdam
-
torch.optim.Adamクラス-
params:モデルのパラメータ -
lr:学習率 -
betas:$\beta_1$, $\beta_2$ (デフォルトでは0.9, 0.999) -
eps:$\varepsilon$(デフォルトでは1e-8)
※$\beta_1$, $\beta_2$, $\varepsilon$はデフォルト値を使うことがほとんど
-
conv_model = utils.get_conv_model()
# PytorchのAdam
opt = optim.Adam(conv_model.parameters(), lr=0.03)
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, 3)
"""
epoch: 0: train error: 0.7002141192807989, validation error: 0.530146399140358, validation accuracy: 0.8095264673233032
epoch: 1: train error: 0.40020737557087915, validation error: 0.4139019697904587, validation accuracy: 0.8463149726390838
epoch: 2: train error: 0.3545065911139472, validation error: 0.43631928861141206, validation accuracy: 0.8417789399623871
"""












