cha_kabuのNotebooks

個人的な機械学習関連勉強のアウトプット置き場です。素人の勉強録なので、こちらに辿り着いた稀有な方、情報はあまり信じない方が身のためです。

ゼロから作るDeep Learning3 フレームワーク編を読む その⑨ステップ46~48

はじめに

以下のシリーズ記事の続きです。

cha-kabu.hatenablog.com

本編

ステップ46 Optimizerによるパラメター更新

最適化手法の基底クラスとなるOptimizerクラスを作成し、それを継承してSGDとMomentumを実装します。

基底クラスのOptimizerクラスの実装はメソッドは多いですが一つ一つがやっていることは単純です。分からなかったのは何故わざわざsetupメソッドでtargetとするModelクラスもしくはLayerクラスを指定するのでしょう?継承先のクラスで引数に与えてやれば良い気もするのですが…pytorchを使っているときはこのsetupメソッドに該当する記述は不要1なので必要性が分からなかったのですが、何か理由あってのことだと思うのでひとまず書籍に従います。参考までに、以下のコードでも動きました。

class SGD(Optimizer):
    def __init__(self, target, lr=0.01): 
        super().__init__()
        self.lr = lr
        self.target = target # オリジナルの実装 
        
    def update_one(self, param):
        param.data -= self.lr * param.grad.data

##### 省略 #####

model = MLP((hidden_size, 1))
optimizer = optimizers.SGD(model,lr) # 書籍ではoptimizers.SGD(lr).setup(model)

##### 省略 #####

また、add_hookメソッドの説明がだいぶあっさりしています(このあとのステップで使うのかも知れません)が、パラメータ更新前に前処理を行う関数を追加するメソッドとのこと。公開されているoptimizerモジュールには以下の三つが実装されています。

WeightDecay

日本語だと重み減衰って言うんですね。過学習対策に使われるもので、損失関数に正則化項(DeZeroではL2ノルム)を加えます。具体的には、損失関数が\displaystyle{L(\Theta)}から以下の形に変化します。※\displaystyle{\Theta}は「すべてのパラメータ」

\displaystyle{L(\Theta)+\lambda\dfrac{1}{2}\sum_{w}||w||^{2}}

\displaystyle{\lambda}はハイパーパラメータでこの値が大きいほど大きな重みをとることにペナルティを与えることになります(≒パラメータが少しずつしか変化しない)。損失関数が変化したことにより、逆伝播する勾配の値も右辺部分を微分した形が残ることになり、以下の様に変化します。

\displaystyle{ w \leftarrow w-\eta \left(\dfrac{\partial L(\Theta)}{\partial w} + \lambda w\right)}

WeightDecayはDezeroでは以下の様に使用することができます。ハイパーパラメター\displaystyle{\lambda}は以下の例の様に、他コンペを見ていても\displaystyle{1e-3}よりは小さい程度の値が設定されていることが多い気がします。

optimizer.add_hook(dezero.optimizers.WeightDecay(1e-4))
ClipGrad

いわゆる「勾配爆発」を食い止めるための機能。取り得る勾配の上限値を指定して、それを超えた場合に上限値で正規化します。具体的には上限値\displaystyle{M}、勾配を\displaystyle{g}とすると、以下の様にします。

\displaystyle{g = \dfrac{M}{||g||}g}
FreezeParam

確証が持てなかったのですが恐らく転移学習をするときによく使うフリーズの機能ですかね?中身は単純で、引数に可変長でLayerクラスを受け取り、パラメターのgradをNoneにします。試していないのですがloss.backward()とoptimizer.update()の間で呼び出して使う感じでしょうか?

こちらはいったん寝かして置き、後半のステップでVGG16の実装があるのでそちらで確認したいと思います。


続いてSGDクラスの実装がされているのですが、まだミニバッチの機能を実装していないので、挙げられている使用例は確率的勾配降下法とは言えず通常の勾配降下法ですよね…?


最後に、SGD以外の最適化手法の実装紹介としてMomenumSGDが紹介されています。コードの見た目は簡単ですが、何を更新しているのかが追わないとパッと理解できなかったのでメモしておきます。

f:id:cha_kabu:20201121232614p:plain

大元はModelインスタンスですが、実際はその中身のParameterインスタンスを追っていくことになります。上図の通りなのですが敢えて言葉で説明すると…

  1. Modelクラスをインスタンス化したmodelをtargetとしてそのインスタンス変数_paramsの中身を確認 → l0,l1の取得
  2. model._paramsに保存されていたLinearインスタンスインスタンス変数の_paramsの中身を確認 → l0,l1それぞれのW,bを取得
  3. 2で確認した各層のパラメータを格納しているParameterインスタンスのオブジェクトidと、そのdataと同じ形状のゼロ行列をディクショナリのインスタンス変数self.vsに格納
  4. 2で確認した各層各パラメータの勾配の値を使ってパラメータ自体の値を更新する

入れ子構造が分けわからなくなりますね。。書籍上は以上ですが、最適化手法の数学的性質、雰囲気だけで流してきたのでいつか別記事でまとめたいと思います。


ステップ47 ソフトマックス関数と交差エントロピー誤差

タイトル通りソフトマックス関数と交差エントロピー誤差を実装します。事前知識あったので特に分かりづらい点はありませんでしたが、数式がすらすら書けるほどは頭に入っていないので一応まとめておきます。

ソフトマックス関数

分類問題の出力層で使われることが多い活性化関数(と言ってよいのか?)です。出力の数値を確率に変換してくれます。数式は以下の通りで、分子は入力の指数関数、分母はすべての入力の指数関数の和。

\displaystyle{



p_{k} = \dfrac{exp(y_{k})}{\sum_{i=1}^{n}exp(y_{i})}

}
交差エントロピー誤差

多値分類問題で使われる損失関数、別名logloss。低い方が良く、完全ランダムな時は以下の計算式から0.693になります。ごつい数式をしていますが、要するに「正解している予測の予測確率のlogをとり合計してマイナスを掛けている」だけです。

\displaystyle{



L=-\dfrac{1}{N}\Sigma_{i=1}^{N}(y_{i}\log p_{i}+(1-y_{i})\log (1-p_{i}))=-\dfrac{1}{N}\Sigma_{i=1}^{N}\log p'_{i}

}

\displaystyle{y_{i}}は正例かどうかを表すラベルで、\displaystyle{p_{i}}は各レコードが正例である予測確率。\displaystyle{p'_{i}}は真の値を予測している確率。正解が\displaystyle{1}の時は\displaystyle{\log p_{i}}を掛けて、正解が\displaystyle{0}の時は\displaystyle{\log (1-p_{i})}を掛けたものの総計をとって\displaystyle{-\frac{1}{N}}倍しています。

突然出てきた「エントロピー」という謎の言葉や、何故対数をとるのか等についてはこちらのサイト様の説明が分かりやすいです。※交差エントロピー誤差についての説明ページではありません。

また、これらの関数は書籍上はpythonの関数を作成する方法defで作成されていますが、ライブラリではFunctionを継承したクラスで実装されています。クラスでは他の関数同様にforwardメソッドで関数的な計算が、backwardメソッドでは逆伝播が行われ値を返します。ソフトマックス関数と交差エントロピー誤差の逆伝播についてはゼロから作るDeepLearning①の巻末付録Aが詳しいです。

ステップ48 多値分類

オリジナルの3値分類を行います。この後DataLoderの実装などがありますが、DeepLearningの学習の流れ全体が分かりやすいのはここだと思うのと、いつもなんとなく写経してしまって分かった気になっているので、学習のループがどう回っているのか視覚的に捉えられる様に表にしました。※コード例のところは一部あまりよろしくない簡素化を行っています。

f:id:cha_kabu:20201121232636p:plain

表の例も他で良く見る形に変えてしまったのですが、書籍の以下のコードは見慣れなくて少し戸惑いました。

sum_loss += float(loss.data) * len(batch_t)

lossの合計に1ループ分のlossを加算する際、バッチデータの長さを掛けています。そしてそのあとループの外では

avg_loss = sum_loss / data_size

lossの合計をデータサイズで割って平均としています。よく見るのは、lossの合計は単純な1ループ分の合計(バッチデータの長さを掛けない)で、そのあとバッチサイズで割るパターンだと思いますが、こちらはバッチ分割時に余りがでる際に対応した形式なのかと思います。確かにこちらの方が正確な平均lossが出せますね。

最後に

さっとまとめて進んでしまいましたが、最適化アルゴリズムと損失関数の数学的な側面はいっつも雰囲気だけで掴んでしまってたぶん良く分かっていないので、いつかちゃんと別記事でまとめる宣言をしてプレッシャーを自分にかけておきます。次回は~ステップ51を予定。


  1. pytorchでは引数にparamsを渡すので修正版のコードの様に引数にModel(もしくはLayer)クラスを渡すのもそもそものお作法とも違うのですが。