Coursera / Improving Deep Neural Networks: Hyperparameter tuning, Regularization and Optimization 受講メモ
Deep Learning Specialization の 2つめのコース
前回: Coursera / Neural Networks and Deep Learning 受講メモ - たにしきんぐダム
データの正規化、様々な正則化手法、gradient descent with momentum や Adam などによる学習の高速化、batch-normalization、gradient checking、ハイパーパラメータチューニングの基本的な手法など。振り返ってみるとかなり盛り沢山な内容だった。jupyter notebook で手を動かすのは本当に勉強になる。
- Bais-Variance tradeoff
- 機械学習モデルを作るサイクル
- Regularization
- Normalizing input
- Vanishing/Exploding Gradient Problem
- Weight Initialization
- Gradient Checking による back-propagation の debug
- mini-batch
- Exponentially Weighted Average
- Optimization
- Hyper-parameter tuning
- Batch Normalization
- softmax (今更?)
- Tensorflow
Bais-Variance tradeoff
- bias: 学習データセットから学習したモデルの予測と、テスト(真の)データセットとの差
- high bias: underfitting
- low bias: うまいことテストデータセットを予測できるモデルになってる
- variance: 学習データセットによるモデルの分散
- high variance: overfitting
- low variance: 学習データに異常な値が含まれていたりしても安定した学習モデルが生成できる状態
- low bias かつ low variance が目指すべきモデル。
- high bias かつ high variance は、一部の学習データにoverfitした結果モデルとしての精度も微妙みたいな最悪な状態
機械学習モデルを作るサイクル
- high bias な学習モデルができる
- high variance なモデルができる
Regularization
正則化項の追加
コスト関数に結合係数による重みを加えることで、結合係数が大きくなりすぎることを防ぐ。
そもそも層や素子の数が大きくなりすぎるとモデルの複雑度(variance)が大きくなり、overfitting しやすくなるという話がある。
極端な話では、結合係数が大きくなることを防げると
- 各素子や、output layer への input が 0 に近い値になる。activation function が sigmoid 等の場合は input が 0 に近いほど、関数の挙動は線形に近くなる。
- activation function が線形だと、等価的に浅い neural network でも同等の neural network が表現できることを考えて、深いネットワークだけどモデルの複雑度は浅めのニューラルネットとほぼ等価。
- ほとんどの素子の出力も0に近くなるので、各層の素子の数がすくなるするのと似たような効果が期待できる。
ただし ||w||_F
は frobenius norm (mはサンプル数、n^[l] はレイヤlの素子数)
- pros
- モデルが正確(early-stoppingと違って、overfittingを防ぐ機構がコスト関数本体に含まれる)
- cons
- コスト関数が単調減少しなくなる
- ハイパラが増える (regularization の係数)、DNNのような複雑なモデルでは一つハイパラが増えると他のハイパラの試行回数も増えることになるので結構深刻。
Dropout Regularization
Neural Network 特有の regularization 手法
- (学習の過程で)各レイヤの素子をランダムに消して(dropout)学習を進める。(1epochごとにどの素子をdropoutするかは更新する)
- inverted dropout
- 各レイヤに
keep_prob
というパラメータを与える 1 - keep_prob
の確率でそのレイヤのある素子をdropoutする(入力をすべて0にする)- レイヤへのinputにはそれぞれ
1/keep_prop
をかけてあげる。- (dropoutした素子からの出力を補い、最終的なコスト関数が without dropout なモデルとだいたい一致させるため
- back-propagation時も同様(同じ
- 各レイヤに
- dev/test時はdropoutしない(モデルの挙動にランダム性を取り入れたくないよね)
その他の正則化手法
- data-augumentation
- 例えば画像なら、画像をflipするとか角度を変えるとか、文字画像ならぼかしを入れるとかして、fake new example を作り出す。
- より多くの種類のデータを与えることで regularize につながる。
- early-stopping
- 学習を回していくともちろん training set での loss function の値はどんどん減っていくが、学習しすぎて overfit していくとそのうち test set での loss function は大きくなっていくはず。
- early-stopping ではいくつかのパラメータを定めて、test set での loss function がもはや減っていないと判断できたらその時点で学習を打ち切りするというやり方。
- early-stopping は loss function を最小化するという目的と相反することを同時にやる羽目になるので、問題の切り分けが難しくなる事がある。
- 後半で学ぶ batch-normalization も Regularization の役割を(少し)果たす。
Normalizing input
- 一部のパラメータのスケールが、他のパラメータより大きい場合、そのスケールの大きいパラメータによるgradientに対する寄与が大きくなり、そのパラメータに関する方向にばかりパラメータの更新が進んでしまう。
- 入力を正規化することで、特定のパラメータによる寄与が他のパラメータの更新を抑えてしまうということがなくなり、効率よくコスト関数を最小化できるようになる。
- 入力値を標準正規分布上に変換する。
- sklearn だと sklearn.preprocessing.StandardScaler がやってくれるやつ。
Vanishing/Exploding Gradient Problem
- Vanishing Gradient Problem
- Exploding Gradient Problem
What's causing the vanishing gradient problem? Unstable gradients in deep neural nets
解決策
- activation function に ReLU (0未満での微分は0、0より大きい入力での微分は常に1) などを利用することで回避できる。(vanishing gradient のみ)
- weight initialization を工夫する(次の項で詳しく)
- 各種regularizationによって結合係数が大きくなることを防ぐことによって Exploding Gradient Problem を防ぐことにもつながる。
Weight Initialization
- 重みをすべて0にしてしまうと、各レイヤの層同士のシンメトリを脱出できず、実質各レイヤ1素子の表現力の低いニューラルネットと同等のモデルができてしまうし、学習は進まない。
- 初期のweightが大きいと、exploding gradient problem に繋がりやすい
- 初期weightは小さなランダム値が好ましい
- どれくらい小さくするのが適切なのか?
He Initialization というのが(隠れ層のactivation functionにReLUを使った場合の) デファクトな weight initialization 手法。He Initialization はランダム値に対して以下の値をかけたものを初期値にする。
$$ \sqrt{\frac{2}{\text{dimension of the previous layer}}} $$
↓↓どうしてこれが望ましいのか非常にわかりやすく解説されていた↓↓
- この値で初期化してやれば、入力に対する出力(逆伝播では反対)の分散が1となり、層を重ねても入力層付近でgradientが極端に小さくなったり大きくなったりすることがないことが期待できるというのが理由って感じ?
- glorat の一様分布は activation function が活性化関数が原点対称かつ原点周りで線形であることを仮定して議論している。
- ReLU を使った場合で計算してみたら He normal 分布でやると分散を抑えられることがわかった。
Gradient Checking による back-propagation の debug
back-prop を自前で実装しないといけないときのデバッグ方法
微分はこう書けるので、εに 1e-7
などの極小値を入れて、だいたいの微分値(gradapprox)を計算
$$ \frac{\partial J}{\partial \theta} = \lim_{\varepsilon \to 0} \frac{J(\theta + \varepsilon) - J(\theta - \varepsilon)}{2 \varepsilon} $$
計算した gradapprox と 実際の gradient を比較(分母をそれぞれのL2ノルムの和で割っているのは正規化のため)
$$ difference = \frac {\mid\mid grad - gradapprox \mid\mid_2}{\mid\mid grad \mid\mid_2 + \mid\mid gradapprox \mid\mid_2} $$
- このdifferenceが小さければ(1e-7など)gradientの計算は合ってそうだし、大きければ何かが間違っていることがわかる。
- gradient checking はコストが大きいので、小さいデータセットで動作確認するときだけ動かすようにしよう。
mini-batch
stochastic gradient descent と batch gradient descent と mini-batch gradient descent
batch gradient descent
すべての学習データを使ってコスト関数を計算し、そこからgradientを計算しパラメータを更新する。
- pros
- 後述する stochastic gradient descent と比べて、大量のデータを使って勾配を計算するので、パラメータの更新がデータに対してstable
- パラメータ更新回数が少ないので効率的(メモリは大量に食うが...)
- cons
- (データが多い場合は)大量のデータを一度に読み込むことになるので大量にメモリを消費する。あまり現実的ではない
- 新しい学習データが追加されたときは、再度すべてのデータを使ってパラメータ更新をしないといけない。
- 現実的には新たなデータが追加されるたびに学習回す必要はなくて、直近データなどで数日に1度くらい学習を回してモデルを更新するので十分だろうが
- パラメータの初期値によって局所最適解に陥りやすい。
- すべてのデータを利用するのでグラフの形は一定。初期値や学習率によっては局所最適解にすぐ陥ってしまう。
stochastic gradient descent
学習データ1つずつを使ってコスト関数を計算し、パラメータ更新を行う
- pros
- 読み込むデータは一つずつなのでメモリ使用量が抑えられる。
- 新しいデータをすぐにモデルに反映させることができる。
- 読み込まれる学習データによってグラフの形は異なる(確率的っていうのはそういうことなのか)ので、局所最適解に陥りにくい。
- ある学習データで局所最適解に陥っても、別の学習データではgradientが0でなくなり学習が進む場合もある。
- cons
- データの数だけパラメータを更新するのでコスト(計算時間)が大きい
- たった一つのデータでパラメータを更新していくので、high-varianceなモデルになりがち
- たとえばある学習データが外れ値な場合はめちゃくちゃな方向にパラメータを更新することも...
mini-batch gradient descent
すべての学習データを 32, 64, 128, 256 などの小さいグループに分割し、グループごとにコストの計算、パラメータ更新を行う。
batch gradient descent の学習の安定性と、stochastic gradient descent のランダム性・メモリ効率の良いとこ取りをした手法
- pros
- 1度に読み込むデータが少ないのでメモリ使用量が抑えられる。
- stochastic gradient descent より学習が効率的(パラメータ更新回数が少ないので)
- ひとまとまりのデータごとにパラメータ更新するので、安定して学習が進む。
- 入力データにランダム性があるので、局所最適解に(batch gradientより)陥りにくい。
- cons
- mini-batch size というハイパラが増える
tf.keras とか見てると default は 32 なんだなぁ(小並感)
tf.keras.Model | TensorFlow Core v2.3.0
Exponentially Weighted Average
概要
gradient descent with momentum などの optimization アルゴリズムを学ぶために exponentially weighted average について勉強する。 Exponentially Weighted Average は効率的に移動平均を計算するアルゴリズム。
移動平均を計算するよりもメモリ上に保持しておくべきデータが1つしかないのでメモリ効率が良い。
$$ \theta_1, \theta_2, \theta_3, ... $$
というようなデータ列があったとき (βはハイパラで0~1)
$$ v_0 = 0 $$
$$ v_t = \beta v_{t-1} + (1-\beta) \theta_t $$
v_t
は、だいたい t - 1/(1-beta)
から t
までのデータの移動平均みたいな値になる。 というのも例えば第t項は
(この辺からはてなブログのmathjaxがうまく変換できなくなってきた)
というようにt項からm個前のデータの係数は β * (1-β)^m
というようになっている。ここで
$$ \lim_{\epsilon \to 0} (1-\epsilon)^{1/\epsilon} = 1/e $$
なので、εにβを当てはめると(βは1以下という成約しかないので lim に対して当てはめるのはいささか乱暴だが)、mが1/(1-β)
の時点で係数は β * 1/e
に割と近い値になるはず。これより前の項は無視できるほど小さいと考える(乱暴では)と v_t
は θ_t ~ θ_{t - (1/1-β)}
の移動平均と同じくらいの値になる。
例えばβ=0.9とすると、だいたい直近10個のデータの移動平均が取れることになる。
bias correction
exponentially weight average は最初の方の平均値 (v_1
や v_2
など)では、食わせるデータ数が 1/(1-β)
より少ないので、本来の求めたい移動平均より小さい値になってしまう。そこで最初の方の平均値には修正を入れて値をスケールさせる。
(tはt番目のデータという意味、tが大きくなるにつれて β^t
は0に近づく。
Optimization
学習の収束を早めるために、ある方向でのパラメータの更新ステップを加速させたり、学習率を学習ステップや方向ごとに良い感じに調整する手法の数々。
- 以下の2つの資料が非常にわかりやすかった。
Gradient descent with momentum
各パラメータ更新ステップで dJ/dW
(以下 dW
と表記) を計算する。普通のgradient descent では w = w - α * dW
と更新するが
$$ v_{dw}^{(0)} = 0 $$
で初期化し、t番目のステップではまず普段どおりdWを計算した後、以下のようにmomentum付きの更新値を計算し
普段どおりパラメータ更新
$$ w = w - \alpha v_{dw}^{(t)} $$
式を見るとわかるように、微分値のexponentially weighted averageを使ってパラメータを更新している。
つまり 1/(1-β)
前までの微分値によって、更新値がブーストされた(momentumを持った)値になっており、終盤の更新式が微小になり収束が遅くなってしまう問題を避けることができる。
bias-correction しても良いけれど、ステップの最初の方の値が小さくなってしまうことに対してあまり関心はないので別にやらなくても良い。
RMSprop(Root Mean Square prop)
(ここに来る前に本当は adagrad が出てきてその進化系(?)がRMSprop / Adagrad)
- まずadagradで「それぞれの要素(方向)の学習率を違うようにするとよいのでは?」という発想になる。
- 毎回大きな勾配を持つ方向では学習率大きくしたくないし、毎回ほぼ0な勾配の場合は小さな勾配でも大きくステップを刻みたい。
- adagradは過去のすべてのステップでの勾配の二乗和のルートで学習率を割る手法。その結果分母は単調減少。
- 最適化の後半では学習率は極小になり学習が進まなくなる。という欠点があった。
- この単調減少性を解決するために、momentumのように過去のいくつかのデータだけを使って学習率を調整するというのがRMSprop
以下のように初期化し
$$ s_{dw}^{(0)} = 0 $$
element-wise に以下の更新式を計算。微分の二乗の移動平均
パラメータの更新は以下のように二乗の移動平均のルートで学習率を調整してやる。(εはsが0だった場合の0除算を避けるため)
$$ w = w - \frac{\alpha}{\sqrt{s_{dw}} + \epsilon} dW $$
Adam Optimization
Momentum と RMSprop を合体させた手法
$$ v_{dw}^{(0)} = 0 $$
$$ s_{dw}^{(0)} = 0 $$
それぞれの値は以下のように更新
momentum では bias correction はしなかったが、今回はs,vを0で初期化しているため、最初の方のステップでの値が非常に小さくなってしまう。これを避けるためにbias correctionをやっておく。
パラメータの更新
他にも最近はEveとかadaboundとかいろんな手法が提案されてるらしい...
Learning rate decay
最初は勢いよくstep大きくパラメータ更新、終盤は小刻みにstepして収束しやすいというlearning_rateがベスト。epochを重ねるごとにlearning_rateを小さくしていく手法。(adamみたいなadaptiveな方法で十分なんじゃないの???)
やり方は色々
$$ \alpha = \frac{1}{1 + decayrate * epoch} \alpha_0 $$
$$ \alpha = 0.95^{epoch} \alpha_0 $$
$$ \alpha = \frac{k}{\sqrt{epoch}} \alpha_0 $$
などなど
Hyper-parameter tuning
GridSearch
それぞれのハイパーパラメータの候補を指定し、exhaustieにすべての組み合わせに対してscoreを計算する。
param_grid = [ {'C': [1, 10, 100, 1000], 'kernel': ['linear']}, {'C': [1, 10, 100, 1000], 'gamma': [0.001, 0.0001], 'kernel': ['rbf']}, ] search = GridSearchCV(model, param_grid, cv=5) search.fit()
簡単なモデルならともかく、DNNになるとハイパラの数もめちゃくちゃ多くなってくるので、exhaustiveにやるのは効率が悪い。
random
すべての組み合わせに対して試すのではなく、グリッドの中からランダムに組み合わせをpickしてcross-validationでscoreを計算する。
ランダムな値にはdistributionを与えることもできる。パラメータによっては、極小な値から大きめの値までの一様分布ではなく、loguniformな分布を与えて、多様なスケールの値からサーチすると適切なハイパーパラメータを掘り当てやすい。どのパラメータにどういう値を与えるのが適切かどうかはモデルに対する理解が問われそうだ。
from sklearn.utils.fixes import loguniform distribution = { 'C': loguniform(1e0, 1e3), 'gamma': loguniform(1e-4, 1e-3), 'kernel': ['rbf'], 'class_weight':['balanced', None] } clf = RandomizedSearchCV(model, distributions, random_state=0)
scipy.stats.loguniform — SciPy v1.5.2 Reference Guide
ベイズ(やってない)
気になる!
Batch Normalization
概要
入力データのnormalizationはやっていたが、入力データだけでなくすべての層のデータをmini-batch ごとにするという手法。activation function 前の線形結合に対して normalize する場合と、activation function 後の出力どちらに対してnormalizeを行うかは議論があるが、前者のほうがよく使われるそう。
あるレイヤーからの activation function 適用前の出力が以下のようだったとき(mはmini-batchでのサンプル数、それぞれそのレイヤの素子数次元をもつベクトル)。普通にmini-batchごとに正規化する。
すべてのレイヤからの出力を必ずしも平均0, 分散1にしたいわけではないかもしれない。そこで gamma と beta という学習可能なパラメータを導入し、正規分布をずらすことができるようにする。(back-prop時には重みWに加えてgammaとbetaの微分も計算してパラメータ更新するようにする)。
あまりまだちゃんと理解できていないのだけれど、ニューラルネットの層が深くなると層を重ねていくごとに入力データの分布がどんどん全く別物に変わってしまって(内部の共変量シフト(Internal Covariate Shift))、その結果深いレイヤではアルゴリズムが対応できなくなる問題がある。batch-normalizationはそれぞれのレイヤで正規化を施すので、入力データの分布が大きく変わってしまうことを防ぎcovariate shiftを防ぐらしい。けどあんまりピンときてない。
得られる効果
- 学習の高速化
- 普通のinputデータのnormalizationと同様に、コスト関数の形がよりシンメトリックな形になるので、
- 重みの初期値選択の重要性が減る
- コスト関数の形がシンメトリックなものになるので、どこからスタートしてもだいたい同じ最適解に収束しやすくなる
- 正則化の効果が少しある
テスト時
トレーニング実行時は mini-batch ごとに平均や分散を計算していたが、テスト時などは入力データは1度に1つだけ、どうデータをnormalizeすればよいのか? 何らかの方法で平均分散を推定する必要がある。
様々な方法があるが、ひとつは exponentially weighted average を使って mini-batch ごとの平均分散を利用して入力値や中間値を正規化する手法が使われる。
softmax (今更?)
multi-class classification のために softmax が出てきたけど今更感。前回のブログでやったな Coursera / Neural Networks and Deep Learning 受講メモ - たにしきんぐダム
Tensorflow
やっとこさ DL Framework が出てきた。assignment では tensorflow v1.4 を利用。v2になって結構API変わったという話も聞くのでちまちまドキュメントも調べつつ勉強する
課題では tensorflow v1 を使って DNN を構築するのだけれど、基本的な概念やv1とv2の違いの部分だけメモしておく。
tf.keras と keras
なんかドキュメント見てたら(v2から) tf.keras
とかいう namespace ができていて keras の API が使えるようになっているっぽいけど、tf.keras
と keras の違いは何? (standalone) keras は tensoflow や theano とかいろんな DL framework をバックエンドに持つラッパーだったんじゃ?
- tensorflow 2.0 から keras API が tensorflow に integrate された
- tensorflow 向けの最適化がいろいろ実装されている、standalone keras には実装されてないものも
- standalone keras にあったすべての機能が tf.keras に実装されているわけではない、やっていきます
- standalone keras 自体の開発は継続される
tensors / variables
- tensor は numpy の ndarray のようなもので、あらゆる次元のconstantはtensor
- Introduction to Tensors | TensorFlow Core
rank_1_tensor = tf.constant([2.0, 3.0, 4.0])
- tensorflow 内で変数を定義するときは
tf.Variable
を使う。- 自動微分のためにもそういうモデルないといけない & 値のCPU/GPU配置最適化のため?
- Introduction to Variables | TensorFlow Core
- Introduction to Gradients and Automatic Differentiation
- tf v1 ではVariableを定義したら
tf.global_variable_initializer()
を明示的に実行する必要があったがv2で不要になった
tf.keras
では pre-defined な loss-function も使える(tfでもある?)- tf.keras.Model | TensorFlow Core v2.3.0 の compile メソッドに
loss
というtf.keras.losses.Loss
のサブクラスを与える - Module: tf.keras.losses | TensorFlow Core v2.3.0
- tf.keras.Model | TensorFlow Core v2.3.0 の compile メソッドに
y_hat = tf.constant(36, name='y_hat') y = tf.constant(39, name='y') loss = tf.Variable((y - y_hat)**2, name='loss')
内部的にはこれによって computation graph を構築している。
(Computation) Graphs and Functions (v1 では Session)
構築した computation graph に値を与えて実行する。
tensorflow v1 では session object を生成し(これを open したタイミングで各種リソースが CPU/GPU に割り当てられる)実行というのを明示的におこなる必要があった。(compuitation graph は TensorBoard | TensorFlow とか使って可視化することも)
# tensoflow v1 init = tf.global_variables_initializer() with tf.Session() as session: session.run(init) print(session.run(loss))
- tensorflow v2 では
@tf.function
というデコレータさえ定義すれば良くなった。 - また v1 では、関数に与える変数などは
tf.placeholder
で定義して、session.run
時にfeed_dict
で値を与える必要があった(明示的にcomputation graphを与えるため)が、v2では@tf.function
でデコレートした関数の引数について勝手にautographがcomputation graphを構築してくれるので不要
# tensorflow v2 def linear_layer(x): return 2 * x + 1 @tf.function def deep_net(x): return tf.nn.relu(linear_layer(x)) deep_net(tf.constant((1, 2, 3)))
computation graph の構築やコンパイルの部分は面白いなと思ったのでもうちょっと掘り下げて勉強してみたい。