続き
コードは書籍記載のgithubのそれを見ると一発です。
6章 学習に関するテクニック
パラメータの更新や、重みの初期値、正則化、などなどモデルや訓練データが優秀でもパラメータ決定のプロセスをミスると期待した効果が得られません。それらのテクニックの話
パラメータの更新
式よりコードを見た方がわかりやすい。
SGD
確率的勾配降下法。微分した値に学習率を乗じるだけのシンプルな構成。実装が簡単な反面探索が非効率になる場合がある。
$$ W\leftarrow W-\eta\frac{\partial L}{\partial W} $$
class SGD: def __init__(self, lr=0.01): self.lr = lr def update(self, params, grads): for key in params.keys(): params[key] -= self.lr * grads[key]
すべての微分値に一定のlrを乗じて、元のパラメータに減算しているだけ。
Momentum
加速する項がついている最適化。同じ方向(同じ符号)への更新が加速していきます。逆に方向(符号)が変わると減速します。すり鉢を転がる玉のイメージのようです。
$$ v\leftarrow \alpha v-\eta\frac{\partial L}{\partial W}\\
W\leftarrow W+v $$
class Momentum: def __init__(self, lr=0.01, momentum=0.9): self.lr = lr self.momentum = momentum self.v = None def update(self, params, grads): if self.v is None: self.v = {} for key, val in params.items(): self.v[key] = np.zeros_like(val) for key in params.keys(): self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] params[key] += self.v[key]
速度成分の初期値はゼロで、同じ方向に更新が続くと加速します。長いこと加速してるとgradの符号が変わってもそっちに転がらないこともあります。
AdaGrad
学習係数を徐々に減衰させていくやり方です。この減衰の方法を各パラメータそれぞれに変えていくやり方です。大きく変化したパラメータの学習率は減らし、小さな変化しか示さないパラメータはあまり減衰させないような考え方です。ちなみにこの式の丸の中の点はアダマール積というそうです。
$$ h\leftarrow h+\frac{\partial L}{\partial W}\bigodot\frac{\partial L}{\partial W}\\
W\leftarrow W-\eta\frac{1}{\sqrt{h}}\frac{\partial L}{\partial W} $$
class AdaGrad: def __init__(self, lr=0.01): self.lr = lr self.h = None def update(self, params, grads): if self.h is None: self.h = {} for key, val in params.items(): self.h[key] = np.zeros_like(val) for key in params.keys(): self.h[key] += grads[key] * grads[key] params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
減衰率hはgradの二乗和。つまり符号は関係なく変化量の総和を保持しています。これの逆数を乗じています。変化の総量が大きいものは学習率が減っていきます。ルートをとっているのは二乗だと行き過ぎってことでしょう。
RMSProp
AdaGradは過去すべてを同じように加算していますが、これは昔のものほど重みを小さく(忘れる)ようにする方法もあるようです。
for key in params.keys(): self.h[key] *= self.decay_rate # decay_rate = 0.99 self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key] params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
Adam
MomentumとAdaGradの融合。解説はなく、雰囲気はなんとなく…。コメントアウトされてる等価コードの方がわかりやすいかな。
# beta1=0.9, beta2=0.999 がおすすめらしい for key in params.keys(): # self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key] # self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2) self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key]) self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key]) params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
mの成分がMomentumかな。同じ符号の変化が続けば加速する成分。vがAdaGradで変化量が大きければ減速する成分
重みの初期値
重みを全ゼロ(全部同一の値)はあかんよ。同じ重みってことは数あるニューロンが全部同じ反応をするということになるので。最適化が進みません。基本ランダムです。
アクティベーション分布
各ニューロンの出力のヒストグラムをとって、値の分布を調べてみると、どれだけニューロンが活発に動いているかがわかってきます。シグモイドを使ったネットワークで初期値をランダムで開始すると0や1付近に偏った分布が出てくることがあります。これはシグモイドの傾きが小さい場所で停滞している勾配消失の状態といえます。またほかの乱数を使うと今度は0.5付近にかたよります。これもせっかく数あるニューロンが似たような応答をしているという意味では表現力を発揮しきれていません。
そこでXavierの初期値ってのを使うととても均一になるようです。各層のノードの数に応じてガウス分布の標準偏差をいじってやるという方法です。具体的にはノード数のルートの逆数を標準偏差にするとよいそうです。
ReLUの初期値
このXavierの初期値は線形な活性化関数が前提になっているのでReLUには通じません。ので今度はHeの初期値ってのを使うとよいそうです。
Batch Normalization
特徴としては
学習を早く進行させることができる
初期値にそれほど依存しない
過学習を抑制する
いいこと尽くしです。これは意図としては前述のアクティベーション分布を強制的に広げる(正規化する)層です。名前の通り処理単位は学習時のミニバッチで、このミニバッチ単位で正規化します。ミニバッチ単位で平均0、分散1になるように正規化します。
もちろんこいつにもbackwardはあり、この計算はだいぶややこしいです。コード見てもイマイチよくわかりませんでした…。
正則化
過学習の課題を解決させるためのお話です。
過学習
要因としては
パラメータが多く表現力が高い
訓練データが少ない
があります。直感的に理解できます。
Weight decay
重みWが大きくなりすぎることに対してペナルティを与えるという手法です。過学習がパラメータが大きくなることに由来することが多いからだそうです。
このペナルティはWの二乗和(L2ノルム)をLossに加えることです。そしてよくわからないのがgradientの際には誤差伝播法で求めたWの勾配それぞれに対して一様にその微分を加算するとよいそうです。コードもそうなっています。
# Loss for idx in range(1, self.hidden_layer_num + 2): W = self.params['W' + str(idx)] weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2) return self.last_layer.forward(y, t) + weight_decay # gradient for idx in range(1, self.hidden_layer_num+2): grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db
Dropout
学習段階でニューロンをランダムに削除しながら学習する手法です。学習の度に消去するニューロンはランダムで、推論時は学習時に消去した割合ですべてのニューロンを活性化させます。(0.15のドロップアウト率であれば85%のニューロンを活性化させる)
class Dropout: def __init__(self, dropout_ratio=0.5): self.dropout_ratio = dropout_ratio self.mask = None def forward(self, x, train_flg=True): if train_flg: self.mask = np.random.rand(*x.shape) > self.dropout_ratio return x * self.mask else: return x * (1.0 - self.dropout_ratio) def backward(self, dout): return dout * self.mask
Falseのマスクを順伝播の際に作り、逆伝播の際はそのマスクを使って信号を止めています。
アンサンブル学習
複数のモデルを個別に学習させ、推論時にはその複数の出力を平均するという手法です。
Dropoutは学習時にニューロンをランダムに消去する(似て非なるモデル)ので、疑似的にこのアンサンブル学習をしていることになり、似た効果が得られます。
ハイパーパラメータの検証
重みやバイアス以外にも多くのパラメータが存在します。これらをハイパーパラメータと呼びこれは学習で最適化されるものではないので、最適なものを見つけ出す必要があります。
検証データ
データセットはこれまで訓練データとテストデータの2つで検討していましたが、このハイパーパラメータを最適化するにあたってもう一つ検証データを用意する必要があります。というのもテストデータで最適化させると、今度はテストデータに対して過学習してしまいます。なのでもう一つ別のデータセットを用意する必要があります。
ハイパーパラメータの最適化
結構泥臭い。いい結果が出てくるパラメータの範囲を狭めていって見つけていきます。この範囲は10のべき乗オーダーでざっくりと狭めていきます。1回の学習に時間がかかるのである程度のエポック数を小さくした状態で探すのがよいようです。
ステップ0 ハイパーパラメータのの範囲を設定する
ステップ1 設定された範囲からランダムにサンプリングする
ステップ2 小エポックで学習し、検証データを使って精度評価をする
ステップ3 ステップ1と2を繰り返し、範囲を狭める
コメント