続き。ニューラルネットワークのパラメータがなぜ最適に自動的に更新されるかを説明した章になります。
コードは書籍記載のgithubのそれを見ると一発です。
4章 ニューラルネットワークの学習
損失関数という指標を使って、より汎化性能のよいモデルへの重みパラメータを更新させていきます。
データから学習する
重みのパラメータがとても多いので、人の経験と勘による手動によるパラメータ更新ではなく、データを使った自動パラメータ更新の仕組みが必要になります。このために多くの正解データが必要になります。
例えば手書きの5を認識しようとしたとき、膨大な正解データからその特徴となる特徴量を見つけ出し、その評価をすることで「5である。」と判断することになります。
いわゆる機械学習(SVM,KNNなど)の分野ではこの特徴量は何等か人間が与えていますが、これすら自動にしてしまうのがニューラルネットワークになります。
ここでは大きく訓練データとテストデータの2つのデータに分けて、説明されていますが、後述のハイパーパラメータ決定のプロセスではもう一つのデータセット(検証データ)が出てくるので、最終的には3つのデータに分けることになります。
とはいえ大きく訓練データで鍛えて、テストデータで汎化性能を確認する。といった流れで学習を進めます。
損失関数
正解ラベル(one-hot)
正解ラベルを1としてそれ以外を0であらわす表現をone-hot表現と呼びます。例えば手書きの2のone-hotの正解データは3番目(0オリジン)の値だけ1の
t = [0,0,1,0,0,0,0,0,0,0]
と表されます。
2乗和誤差
$$ E=\frac{1}{2}\sum_k(y_{k}-t_{k})^2 $$
def sum_squared_error(y, t): return 0.5 * np.sum((y-t)**2)
この誤差和はバッチサイズ方向にも誤差積算しています。
交差エントロピー誤差
$$ E=-\sum_kt_{k}\log{y_{k}} $$
def cross_entropy_error(y, t): return -np.sum(t * np.log(y + 1e-7))
式がややこしい。これは先のソフトマックス関数を活性化関数として最終層に入れたときに、この式を使ってLossを計算すると、微分したときに大変便利になる。という前提で、こういうものだと思って使います。one-hotベクトルをイメージするとlog1が0なのでまぁtとyの誤差が小さいほど小さい。という動きは想像できます。
1e-7はlog0のマイナス無限大発散を抑制するものです。
ミニバッチ学習
データが多すぎると更新がなかなか進まないので、膨大なデータから一部ランダムにデータを抽出して、細かい単位で更新、学習していく手法。
batch_size = 100 batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask]
先のバッチと同様に損失関数側もバッチ対応させないといけない。
def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) y = y.reshape(1, y.size) # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換 if t.size == y.size: t = t.argmax(axis=1) batch_size = y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
この実装がまたややこしい。正解ラベルのベクトルの値は1なのでtの掛け算も省略されてます。
なぜ損失関数を設定するのか?
認識精度を指標にしてしまうと、微分したときに0となることが多く更新が進まない。認識率だと、これがあってたけどこれは間違ってた、みたいな状況で数字が変化しない。なのでパラメータを微小変化させたときしっかり値が連続的に変化する損失関数を設定するとよい。ってことのようです。
数値微分
勾配法では勾配情報を使って更新の方向を決めます。微分値で勾配を求めます。xを微小に増やした結果と微小に減らした結果の差分から勾配を求めることを数値微分と呼ぶようです。
$$ \frac{df(x)}{dx}=\lim_{h \rightarrow 0}\frac{f(x+h)-f(x-h)}{2h} $$
def numerical_diff(f, x): h = 1e-4 return (f(x+h) - f(x-h)) / (2*h)
偏微分
これが多変数(xがベクトル)になった場合はいわゆる偏微分が必要になります。ある変数で微分しようとした場合にその変数以外は定数とみなし微分してやればよいだけなので、コードとしては
def _numerical_gradient(f, x): h = 1e-4 # 0.0001 grad = np.zeros_like(x) for idx in range(x.size): tmp_val = x[idx] x[idx] = float(tmp_val) + h fxh1 = f(x) # f(x+h) x[idx] = tmp_val - h fxh2 = f(x) # f(x-h) grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 値を元に戻す return grad
これでよさそうです。
勾配
偏微分で求まるのはある地点において関数の値を減らす向きを示すだけであって、関数の値を最も減らす方向では必ずしもない。ということです。重要ポイントだそうです。
勾配法
今現在の値に対して、勾配に学習率lrを乗じたものを減算するものです。考え方としてはシンプルです。
$$ x_0=x_0-\eta\frac{\partial f}{\partial x_0} $$
def gradient_descent(f, init_x, lr=0.01, step_num=100): x = init_x for i in range(step_num): grad = numerical_gradient(f, x) x -= lr * grad return x
この例は学習率0.01で100回微分して更新を終えるものです。
これをニューラルネットワークの重みWに対して行うことで学習ができます。
class simpleNet: def __init__(self): self.W = np.random.randn(2,3) def predict(self, x): return np.dot(x, self.W) def loss(self, x, t): z = self.predict(x) y = softmax(z) loss = cross_entropy_error(y, t) return loss x = np.array([0.6, 0.9]) t = np.array([0, 0, 1]) net = simpleNet() f = lambda w: net.loss(x, t) # wはダミー引数 dW = numerical_gradient(f, net.W)
学習アルゴリズムの実装
ステップ1 ミニバッチ
訓練データからランダムに一部のデータを選び出す。この塊をミニバッチと呼ぶ
ステップ2 勾配の算出
ミニバッチの損失関数を減らすために、各重みパラメータの勾配を求める。勾配は損失関数の値を最も減らす方向を示す。
ステップ3 パラメータの更新
重みパラメータを勾配方向に微小量だけ更新する。
ステップ4 繰り返す
ステップ1~3を繰り返す。
この手順を、無作為に選ばれたデータを使用することから確率的勾配降下法と呼ばれ、SGDと記述されることが多いようです。
テストデータで評価
学習を進めると損失関数の値は徐々に下がっていきますが、これは訓練データに対してであって必ずしもすべてのデータでそうなっているとは限りません。のでテストデータで評価します。
ここでエポックという単位が登場します。これはミニバッチ学習を複数回行うことですべての訓練データを使い切った回数として定義されるようです。20000データのうち100のミニバッチであれば200回更新すれば1エポック。
このサンプルは1エポック毎に精度チェックをしています。
# データの読み込み (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True) network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) iters_num = 10000 # 繰り返しの回数を適宜設定する train_size = x_train.shape[0] batch_size = 100 learning_rate = 0.1 train_loss_list = [] train_acc_list = [] test_acc_list = [] # 1エポックあたりの繰り返し数 iter_per_epoch = max(train_size / batch_size, 1) for i in range(iters_num): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] # 勾配の計算 #grad = network.numerical_gradient(x_batch, t_batch) grad = network.gradient(x_batch, t_batch) # パラメータの更新 for key in ('W1', 'b1', 'W2', 'b2'): network.params[key] -= learning_rate * grad[key] loss = network.loss(x_batch, t_batch) train_loss_list.append(loss) # 1エポック毎に認識精度を計算 if i % iter_per_epoch == 0: train_acc = network.accuracy(x_train, t_train) test_acc = network.accuracy(x_test, t_test) train_acc_list.append(train_acc) test_acc_list.append(test_acc) print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))
コメント