CS231n/assignment2/Batch Normalization(バッチ正規化)

今回はStanford Universityが提供するCS231nシリーズのAssignment2第二弾バッチ正規化に挑戦する。そのうちコピペではなく自分の答案を貼り付けられるようになるだろうことを心から願わざるにはいられない今日この頃。

多層ネットワークの学習を楽にする一手法は、SGD+momentumやRMSProp、Adam等のより高度な最適化法を使うことだ。もう一つの手法は、ネットワークが学習しやすいようにネットワークのアーキテクチャを変えることだ。その一つの考え方が、1で近年になって提唱されたbatch normalizationだ。その提案はそんなに複雑なものではない。機械学習手法は、入力データが、zero mean and unit variance(平均0分散1)を持つuncorrelated features(無相関特徴)だとより機能性が上がる傾向にある。ニューラルネットワークを訓練する場合、データをネットワークへフィードする前に、データ特徴を明確にdecorrelate(無相関化)するために、データを前処理することができる。この事が、ネットワークの第一層が、うまく分散しているデータを見ることを確実にする。しかし、入力データを前処理しても、ネットワークの深い層の活性化は、フィードされるデータがネットワークの前の層からの出力なので、無相関化されない可能性が非常に高く、ゼロ平均・単位分散ではなくなる。もっと悪いことに、学習過程の間、ネットワークの各層の特徴分布は、各層の重みが更新されるに連れてシフトしてしまう。2の著者等は、深層ニューラルネットワーク内の特徴分布移動は、多層ネットワークの訓練をより困難なものにしている可能性があることを仮説として取り上げている。この問題を克服するために、3等は、ネットワーク内にバッチ正規化層を挿入することを提唱している。訓練時、バッチ正規化層は各特徴量の平均と標準偏差を推測するためにデータのミニバッチを利用する。これら推定された平均値と標準偏差は、その後、ミニバッチの特徴量を中央化・正規化するのに使われる。この平均・標準偏差の移動平均は訓練中保持され、テスト時にこの移動平均は特徴量を中央化・正規化するのに使われる。

この正規化戦略は、ある特定の層が、時として、平均0または分散1でない特徴を持つことで最適になる可能性があることから、ネットワークの表現力を減衰させる可能性を有している。このために、バッチ正規化層は、各特徴次元のための学習可能シフトとスケールパラメーターをインクルードしている。

# As usual, a bit of setup
from __future__ import print_function
import time
import numpy as np
import matplotlib.pyplot as plt
from cs231n.classifiers.fc_net import *
from cs231n.data_utils import get_CIFAR10_data
from cs231n.gradient_check import eval_numerical_gradient, eval_numerical_gradient_array
from cs231n.solver import Solver

%matplotlib inline
plt.rcParams['figure.figsize'] = (10.0, 8.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

# for auto-reloading external modules
# see http://stackoverflow.com/questions/1907993/autoreload-of-modules-in-ipython

def rel_error(x, y):
    """ returns relative error """
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

%load_ext autoreload
%autoreload 2
# Load the (preprocessed) CIFAR10 data.

data = get_CIFAR10_data()
for k, v in data.items():
    print('%s: ' % k, v.shape)
X_train:  (49000, 3, 32, 32)
y_train:  (49000,)
X_val:  (1000, 3, 32, 32)
y_val:  (1000,)
X_test:  (1000, 3, 32, 32)
y_test:  (1000,)

Batch normalization: Forward

cs231n/layers.pyを開いて、batchnorm_forward関数内にbatch normalization forward passを実装する。それが終わったら、実装をテストするために下記のコードを実行する。

# Check the training-time forward pass by checking means and variances
# of features both before and after batch normalization

# Simulate the forward pass for a two-layer network
np.random.seed(231)
N, D1, D2, D3 = 200, 50, 60, 3
X = np.random.randn(N, D1)
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)
a = np.maximum(0, X.dot(W1)).dot(W2)

print('Before batch normalization:')
print('  means: ', a.mean(axis=0))
print('  stds: ', a.std(axis=0))

# Means should be close to zero and stds close to one
print('After batch normalization (gamma=1, beta=0)')
a_norm, _ = batchnorm_forward(a, np.ones(D3), np.zeros(D3), {'mode': 'train'})
print('  mean: ', a_norm.mean(axis=0))
print('  std: ', a_norm.std(axis=0))

# Now means should be close to beta and stds close to gamma
gamma = np.asarray([1.0, 2.0, 3.0])
beta = np.asarray([11.0, 12.0, 13.0])
a_norm, _ = batchnorm_forward(a, gamma, beta, {'mode': 'train'})
print('After batch normalization (nontrivial gamma, beta)')
print('  means: ', a_norm.mean(axis=0))
print('  stds: ', a_norm.std(axis=0))
Before batch normalization:
  means:  [ -2.3814598  -13.18038246   1.91780462]
  stds:  [27.18502186 34.21455511 37.68611762]
After batch normalization (gamma=1, beta=0)
  mean:  [ 3.10862447e-17 -8.88178420e-18  2.40779618e-17]
  std:  [0.99999999 1.         1.        ]
After batch normalization (nontrivial gamma, beta)
  means:  [11. 12. 13.]
  stds:  [0.99999999 1.99999999 2.99999999]
# Check the test-time forward pass by running the training-time
# forward pass many times to warm up the running averages, and then
# checking the means and variances of activations after a test-time
# forward pass.
np.random.seed(231)
N, D1, D2, D3 = 200, 50, 60, 3
W1 = np.random.randn(D1, D2)
W2 = np.random.randn(D2, D3)

bn_param = {'mode': 'train'}
gamma = np.ones(D3)
beta = np.zeros(D3)
for t in range(50):
    X = np.random.randn(N, D1)
    a = np.maximum(0, X.dot(W1)).dot(W2)
    batchnorm_forward(a, gamma, beta, bn_param)
bn_param['mode'] = 'test'
X = np.random.randn(N, D1)
a = np.maximum(0, X.dot(W1)).dot(W2)
a_norm, _ = batchnorm_forward(a, gamma, beta, bn_param)

# Means should be close to zero and stds close to one, but will be
# noisier than training-time forward passes.
print('After batch normalization (test-time):')
print('  means: ', a_norm.mean(axis=0))
print('  stds: ', a_norm.std(axis=0))
After batch normalization (test-time):
  means:  [-0.03927354 -0.04349152 -0.10452688]
  stds:  [1.01531428 1.01238373 0.97819988]

Batch Normalization: backward

次に、batch normalization用のbackward passを、batchnorm_backward関数内に実装する。バックワードパスを抽出するために、個々の中間ノードを介して、batch normalizationとbackprop用の計算グラフを書き出す必要がある。いくつかの中間層は複数の送信ブランチを持っているかもしれないので、バックワードパスにおいて、これらのブランチの勾配を間違いなく合計するように。それが終わったら、実装をテストするために下記のコードを実行する。

# Gradient check batchnorm backward pass
np.random.seed(231)
N, D = 4, 5
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

bn_param = {'mode': 'train'}
fx = lambda x: batchnorm_forward(x, gamma, beta, bn_param)[0]
fg = lambda a: batchnorm_forward(x, a, beta, bn_param)[0]
fb = lambda b: batchnorm_forward(x, gamma, b, bn_param)[0]

dx_num = eval_numerical_gradient_array(fx, x, dout)
da_num = eval_numerical_gradient_array(fg, gamma.copy(), dout)
db_num = eval_numerical_gradient_array(fb, beta.copy(), dout)

_, cache = batchnorm_forward(x, gamma, beta, bn_param)
dx, dgamma, dbeta = batchnorm_backward(dout, cache)
print('dx error: ', rel_error(dx_num, dx))
print('dgamma error: ', rel_error(da_num, dgamma))
print('dbeta error: ', rel_error(db_num, dbeta))
dx error:  1.70292739451216e-09
dgamma error:  7.420414216247087e-13
dbeta error:  2.8795057655839487e-12

Batch Normalization: alternative backward

講義の中で話したシグモイド・バックワードパス用の2つの異なる実装で、一つの考え方は、簡単なオペレーションと全中間値を介するバックドロップから成る計算グラフを書き出すことだ。もう一つは、紙上で微分を解くことだ。sigmoid function(シグモイド関数)に関しては、紙上でグラディエントを単純化することで、バックワードパス用の非常に単純な公式を導き出せるし、意外なことに、紙上で微分を解いて簡約すればバッチ正規化バックワードパス用の簡潔な式も導ける。それが終わったら、簡略化したバッチ正規化バックワードパスをbatchnorm_backward_altに実装して下記のコードを実行して2つの実装を比較する。2つの実装は、ほぼ同一の計算結果になる必要があるが、代替え実装はやや高速なはずである。

NOTE: この箇所は完全に任意ではあるが、完遂すれば3ポイントを付与する。

np.random.seed(231)
N, D = 100, 500
x = 5 * np.random.randn(N, D) + 12
gamma = np.random.randn(D)
beta = np.random.randn(D)
dout = np.random.randn(N, D)

bn_param = {'mode': 'train'}
out, cache = batchnorm_forward(x, gamma, beta, bn_param)

t1 = time.time()
dx1, dgamma1, dbeta1 = batchnorm_backward(dout, cache)
t2 = time.time()
dx2, dgamma2, dbeta2 = batchnorm_backward_alt(dout, cache)
t3 = time.time()

print('dx difference: ', rel_error(dx1, dx2))
print('dgamma difference: ', rel_error(dgamma1, dgamma2))
print('dbeta difference: ', rel_error(dbeta1, dbeta2))
print('speedup: %.2fx' % ((t2 - t1) / (t3 - t2)))
dx difference:  0.0
dgamma difference:  0.0
dbeta difference:  0.0
speedup: 1.20x

Fully Connected Nets with Batch Normalization

バッチ正規化のための実装を得たので、cs2312n/classifiers/fc_net.pyの中にあるFullyConnectedNetに戻る。バッチ正規化を付け足すために実装を修正する。具体的に言えば、コンストラクターでフラグuse_batchnormがTrueの時、各ReLU nonlinearityの前にバッチ正規化層を挿入する。ネットワークの最後の層からの出力は正規化されてはならない。作業が終わったら、実装の勾配確認をするために下記のコードを走らせる。

HINT: cs231n/layer_utils.pyにあるものに似た追加のヘルパー層を定義するのがいいかもしれない。もしやるなら、cs231n/classifiers/fc_net.pyの中に定義する。

np.random.seed(231)
N, D, H1, H2, C = 2, 15, 20, 30, 10
X = np.random.randn(N, D)
y = np.random.randint(C, size=(N,))

for reg in [0, 3.14]:
    print('Running check with reg = ', reg)
    model = FullyConnectedNet([H1, H2], input_dim=D, num_classes=C,
                            reg=reg, weight_scale=5e-2, dtype=np.float64,
                            use_batchnorm=True)

    loss, grads = model.loss(X, y)
    print('Initial loss: ', loss)

    for name in sorted(grads):
        f = lambda _: model.loss(X, y)[0]
        grad_num = eval_numerical_gradient(f, model.params[name], verbose=False, h=1e-5)
        print('%s relative error: %.2e' % (name, rel_error(grad_num, grads[name])))
    if reg == 0: print()
Running check with reg =  0
Initial loss:  2.2611955101340957
W1 relative error: 1.10e-04
W2 relative error: 2.85e-06
W3 relative error: 3.92e-10
b1 relative error: 2.22e-03
b2 relative error: 2.50e-07
b3 relative error: 4.78e-11
beta1 relative error: 7.33e-09
beta2 relative error: 1.07e-09
gamma1 relative error: 7.47e-09
gamma2 relative error: 2.41e-09

Running check with reg =  3.14
Initial loss:  6.996533220108303
W1 relative error: 1.98e-06
W2 relative error: 2.28e-06
W3 relative error: 1.11e-08
b1 relative error: 1.12e-08
b2 relative error: 2.53e-08
b3 relative error: 2.23e-10
beta1 relative error: 6.32e-09
beta2 relative error: 5.69e-09
gamma1 relative error: 5.94e-09
gamma2 relative error: 4.14e-09

Batchnorm for deep networks

6層ネットワークを、バッチ最適化有り/無しの両方で、1000標本のサブセットで訓練するために下記のコードを実行する。

np.random.seed(231)
# Try training a very deep net with batchnorm
hidden_dims = [100, 100, 100, 100, 100]

num_train = 1000
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

weight_scale = 2e-2
bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, use_batchnorm=True)
model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, use_batchnorm=False)

bn_solver = Solver(bn_model, small_data,
                num_epochs=10, batch_size=50,
                update_rule='adam',
                optim_config={
                  'learning_rate': 1e-3,
                },
                verbose=True, print_every=200)
bn_solver.train()

solver = Solver(model, small_data,
                num_epochs=10, batch_size=50,
                update_rule='adam',
                optim_config={
                  'learning_rate': 1e-3,
                },
                verbose=True, print_every=200)
solver.train()
(Iteration 1 / 200) loss: 2.340974
(Epoch 0 / 10) train acc: 0.107000; val_acc: 0.115000
(Epoch 1 / 10) train acc: 0.313000; val_acc: 0.266000
(Epoch 2 / 10) train acc: 0.396000; val_acc: 0.279000
(Epoch 3 / 10) train acc: 0.485000; val_acc: 0.316000
(Epoch 4 / 10) train acc: 0.525000; val_acc: 0.318000
(Epoch 5 / 10) train acc: 0.595000; val_acc: 0.334000
(Epoch 6 / 10) train acc: 0.637000; val_acc: 0.325000
(Epoch 7 / 10) train acc: 0.666000; val_acc: 0.329000
(Epoch 8 / 10) train acc: 0.695000; val_acc: 0.292000
(Epoch 9 / 10) train acc: 0.776000; val_acc: 0.330000
(Epoch 10 / 10) train acc: 0.766000; val_acc: 0.325000
(Iteration 1 / 200) loss: 2.302332
(Epoch 0 / 10) train acc: 0.129000; val_acc: 0.131000
(Epoch 1 / 10) train acc: 0.283000; val_acc: 0.250000
(Epoch 2 / 10) train acc: 0.316000; val_acc: 0.277000
(Epoch 3 / 10) train acc: 0.373000; val_acc: 0.282000
(Epoch 4 / 10) train acc: 0.390000; val_acc: 0.310000
(Epoch 5 / 10) train acc: 0.434000; val_acc: 0.300000
(Epoch 6 / 10) train acc: 0.535000; val_acc: 0.345000
(Epoch 7 / 10) train acc: 0.530000; val_acc: 0.304000
(Epoch 8 / 10) train acc: 0.628000; val_acc: 0.339000
(Epoch 9 / 10) train acc: 0.654000; val_acc: 0.342000
(Epoch 10 / 10) train acc: 0.714000; val_acc: 0.331000

上で訓練した2ネットワークの結果を視覚化するのに下記のコードを実行する。バッチ正規化を使うことでネットワークが急速に収束するのが分かるはずだ。

plt.rcParams["font.size"] = "15"
plt.subplot(3, 1, 1)
plt.title('Training loss')
plt.xlabel('Iteration')

plt.subplot(3, 1, 2)
plt.title('Training accuracy')
plt.xlabel('Epoch')

plt.subplot(3, 1, 3)
plt.title('Validation accuracy')
plt.xlabel('Epoch')

plt.subplot(3, 1, 1)
plt.plot(solver.loss_history, 'o', label='baseline')
plt.plot(bn_solver.loss_history, 'o', label='batchnorm')

plt.subplot(3, 1, 2)
plt.plot(solver.train_acc_history, '-o', label='baseline')
plt.plot(bn_solver.train_acc_history, '-o', label='batchnorm')

plt.subplot(3, 1, 3)
plt.plot(solver.val_acc_history, '-o', label='baseline')
plt.plot(bn_solver.val_acc_history, '-o', label='batchnorm')
  
for i in [1, 2, 3]:
    plt.subplot(3, 1, i)
    plt.legend(loc='upper center', ncol=4)
plt.gcf().set_size_inches(18, 25)
plt.show()
/root/.pyenv/versions/py365/lib/python3.6/site-packages/matplotlib/cbook/deprecation.py:107: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance.  In a future version, a new instance will always be created and returned.  Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
  warnings.warn(message, mplDeprecation, stacklevel=1)

Batch normalization and initialization

バッチ正規化と重み初期化の相互作用を研究する目的で小さな実験を行う。最初のセルはバッチ正規化有り/無しの両方の8層ネットワークを、重み用の異なる尺度を用いて訓練する。2つ目のセルは、重み初期値スケールに対する訓練精度、検証セット精度、訓練損失をグラフ化する。

np.random.seed(231)
# Try training a very deep net with batchnorm
hidden_dims = [50, 50, 50, 50, 50, 50, 50]

num_train = 1000
small_data = {
  'X_train': data['X_train'][:num_train],
  'y_train': data['y_train'][:num_train],
  'X_val': data['X_val'],
  'y_val': data['y_val'],
}

bn_solvers = {}
solvers = {}
weight_scales = np.logspace(-4, 0, num=20)
for i, weight_scale in enumerate(weight_scales):
    print('Running weight scale %d / %d' % (i + 1, len(weight_scales)))
    bn_model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, use_batchnorm=True)
    model = FullyConnectedNet(hidden_dims, weight_scale=weight_scale, use_batchnorm=False)

    bn_solver = Solver(bn_model, small_data,
                  num_epochs=10, batch_size=50,
                  update_rule='adam',
                  optim_config={
                    'learning_rate': 1e-3,
                  },
                  verbose=False, print_every=200)
    bn_solver.train()
    bn_solvers[weight_scale] = bn_solver

    solver = Solver(model, small_data,
                  num_epochs=10, batch_size=50,
                  update_rule='adam',
                  optim_config={
                    'learning_rate': 1e-3,
                  },
                  verbose=False, print_every=200)
    solver.train()
    solvers[weight_scale] = solver
Running weight scale 1 / 20
Running weight scale 2 / 20
Running weight scale 3 / 20
Running weight scale 4 / 20
Running weight scale 5 / 20
Running weight scale 6 / 20
Running weight scale 7 / 20
Running weight scale 8 / 20
Running weight scale 9 / 20
Running weight scale 10 / 20
Running weight scale 11 / 20
Running weight scale 12 / 20
Running weight scale 13 / 20
Running weight scale 14 / 20
Running weight scale 15 / 20
Running weight scale 16 / 20
Running weight scale 17 / 20
Running weight scale 18 / 20
Running weight scale 19 / 20
Running weight scale 20 / 20
# Plot results of weight scale experiment
best_train_accs, bn_best_train_accs = [], []
best_val_accs, bn_best_val_accs = [], []
final_train_loss, bn_final_train_loss = [], []

for ws in weight_scales:
    best_train_accs.append(max(solvers[ws].train_acc_history))
    bn_best_train_accs.append(max(bn_solvers[ws].train_acc_history))

    best_val_accs.append(max(solvers[ws].val_acc_history))
    bn_best_val_accs.append(max(bn_solvers[ws].val_acc_history))

    final_train_loss.append(np.mean(solvers[ws].loss_history[-100:]))
    bn_final_train_loss.append(np.mean(bn_solvers[ws].loss_history[-100:]))

plt.subplot(3, 1, 1)
plt.title('Best val accuracy vs weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Best val accuracy')
plt.semilogx(weight_scales, best_val_accs, '-o', label='baseline')
plt.semilogx(weight_scales, bn_best_val_accs, '-o', label='batchnorm')
plt.legend(ncol=2, loc='lower right')

plt.subplot(3, 1, 2)
plt.title('Best train accuracy vs weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Best training accuracy')
plt.semilogx(weight_scales, best_train_accs, '-o', label='baseline')
plt.semilogx(weight_scales, bn_best_train_accs, '-o', label='batchnorm')
plt.legend()

plt.subplot(3, 1, 3)
plt.title('Final training loss vs weight initialization scale')
plt.xlabel('Weight initialization scale')
plt.ylabel('Final training loss')
plt.semilogx(weight_scales, final_train_loss, '-o', label='baseline')
plt.semilogx(weight_scales, bn_final_train_loss, '-o', label='batchnorm')
plt.legend()
plt.gca().set_ylim(1.0, 3.5)

plt.gcf().set_size_inches(18, 25)
plt.show()
参考サイトhttps://github.com/

  1. Sergey Ioffe and Christian Szegedy, “Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift”, ICML 2015.
  2. Sergey Ioffe and Christian Szegedy, “Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift”, ICML 2015.
  3. Sergey Ioffe and Christian Szegedy, “Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift”, ICML 2015.