学習レポート: 深層学習(Day 2)

# 深層モデルのための学習テクニック
- 中間層を増やした時に生じる問題への対応策

## 勾配消失問題
- 誤差逆伝播法では、計算結果（=誤差）から微分を逆算することで、不要な再帰的計算を避けて微分を算出できる
    - ただし、中間層が増えると、勾配消失問題によってうまく行かないケースが出てくる
- 勾配消失問題: 誤差逆伝播法が下位層(入力層に近い層)に進んでいくに連れて、勾配がどんどん緩やかになっていくため、勾配降下法による更新では下位層のパラメータがほとんど変わらず、訓練は最適値に収束しなくなる
    - 微分の連鎖率において、小さい(1未満の)値を複数回かけていくと、結果の勾配が0に近付いていってしまう
        - シグモイド関数の微分結果は最大でも0.25
- 勾配消失の解決法
    - 活性化関数の選択
        - ReLU関数
            - 今最も使われている活性化関数

            - $ f(x) = \begin{cases} x &ensp; (x > 0) \\\\ 0 &ensp; (x \leqq 0) \end{cases} $
                - 微分は$ f'(x) = \begin{cases} 1 &ensp; (x > 0) \\\\ 0 &ensp; (x \leqq 0) \end{cases} $ となるため、$ 1 &ensp; (x > 0) $の領域では勾配消失問題を回避し、同時に、$ 0 &ensp; (x \leqq 0) $の領域では不要なパラメーターをつぶすことによってスパース化に貢献
    - 重みの初期値設定
        - 多くの場合は重みを乱数で初期設定する
            - 均一な見方ではなく、多様な見方をできるようにする
            - もともと標準正規分布(平均が0で分散が1)を用いることが一般的だった
        - Xavier初期化では、標準正規分布を前の層のノード数の平方根で割った値を用いる
            - そうすることによって、活性化関数の出力が0 or 1に極端に偏らず、表現力を保てる
            - シグモイド関数など、S字カーブ形状の活性化関数の場合
        - Heの初期化では、前の層のノード数の平方根で割った値に$ \sqrt{2} $をかける
            - ReLU関数など、S字カーブ形状でない活性化関数の場合に有効
    - バッチ正規化(Batch Normalization)
        - ミニバッチ単位で、入力値のデータを正規化する(平均0, スケール1の分布にする)
            - 初期値への依存が減る、勾配消失が起こりにくくなる、外れ値が少なくなり過学習が起こりにくくなるなど、学習が安定し、結果的に速く学習できる
            - 内部共変量シフトの対策となる
        - 活性化関数に値を渡す前後に、バッチ正規化の処理を孕んだ層を加える
        - 数学的記述
            1. $ μ_t = \dfrac{1}{N_t} \displaystyle \sum_{i=1}^{N_t}x_{ni} $
            1. $ σ_t^2 = \dfrac{1}{N_t} \displaystyle \sum_{i=1}^{N_t}(x_{ni} - μ_t)^2 $
            1. $ \hat{x_{ni}} = \dfrac{x_{ni} - μ_t}{\sqrt{σ_t^2 + θ}} $
            1. $ y_{ni} = γx_{ni} + β $
            - 1で平均を、2で分散を求める
            - 3で正規化(θはバイアス項)
            - 4はNNが扱いやすいように、定数倍(γ)したり、定数項(β)を足す
        - バッチ単位で正規化するバッチ正規化以外に、あるサンプルのH×W×C全てのpixelを正規化するレイヤー正規化、各サンプルの各チャネルごとに正規化するインスタンス正規化という手法もある
        - (実装演習の結果)万能薬という訳ではない模様

## 学習率最適化手法(Optimizer)
- 学習率最適化手法を利用して最適な学習率を見出す
    - 学習率の値が大きい場合、発散してしまい、最適解にいつまでもたどり着かない
    - 学習率の値が小さい場合、発散することはないが、小さすぎると収束するまでに時間がかかる
        - また大域局所最適値に収束しづらくなる
- 基本的な考え方
    - 初期の学習率を大きく設定し、徐々に学習率を小さくしていく
    - パラメーター毎に学習率を可変させる
- モメンタム
    - 通常の勾配降下法(誤差をパラメータで微分したものと学習率の積を減算する)に、モメンタム項(前回の移動幅に慣性パラメーターをかけたもの)を加える
        - 局所的最適解にはならず、大域的最適解となる
        - 谷間についてから最も低い位置(最適値)にたどり着くまでの時間が長い(動き続けてしまう)
- AdaGrad
    - 誤差をパラメータで微分したものと再定義した学習率の積を減算する
        - 勾配の緩やかな斜面に対して、最適値に近づける
        - 反面、学習率が徐々に小さくなるので、鞍点問題を引き起こすことがある
            - 勾配が緩やかなところで動きにくくなってしまう
- RMSProp
    - AdaGradの改良版
    - 誤差をパラメータで微分したものと再定義した学習率の積を減算する
        -　αの概念を入れ、古い学習率は軽視するようになる(Decaying)
        -　局所的最適解にはならず、大域的最適解となる
        -　ハイパーパラメーターの調整が必要な場合が少ない
- Adam
    - もっとも使われている現在のところ最強のOptimizer
    - モメンタムの過去の勾配の指数関数的減衰平均
とRMSProp の過去の勾配の2乗の指数関数的減衰平均の両方を受け継いだ最適化アルゴリズム
        - モメンタムとRMSProp両方のメリットを受け継ぐ
- 以下は各手法のソースコード
    ```Python
        # SGD
        for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
            network.params[key] -= learning_rate * grad[key]
    
        # Momentum
        for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
            if i == 0:
                v[key] = np.zeros_like(network.params[key])
            v[key] = momentum * v[key] - learning_rate * grad[key]
            network.params[key] += v[key]
    
        # AdaGrad
        for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
            if i == 0:
                h[key] = np.zeros_like(network.params[key])
            h[key] += grad[key] * grad[key]
            network.params[key] -= learning_rate * grad[key] / (np.sqrt(h[key]) + theta)
    
        # RMSProp
        for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
            if i == 0:
                h[key] = np.zeros_like(network.params[key])
            h[key] *= decay_rate
            h[key] += (1 - decay_rate) * grad[key] * grad[key]
            network.params[key] -= learning_rate * grad[key] / (np.sqrt(h[key]) + 1e-7)
    
        # Adam
        learning_rate_t  = learning_rate * np.sqrt(1.0 - beta2 ** (i + 1)) / (1.0 - beta1 ** (i + 1))
        for key in ('W1', 'W2', 'W3', 'b1', 'b2', 'b3'):
            if i == 0:
                m[key] = np.zeros_like(network.params[key])
                v[key] = np.zeros_like(network.params[key])
    
            m[key] += (1 - beta1) * (grad[key] - m[key])
            v[key] += (1 - beta2) * (grad[key] ** 2 - v[key])
            network.params[key] -= learning_rate_t * m[key] / (np.sqrt(v[key]) + 1e-7)
            
    ```

## 過学習
- テスト誤差と訓練誤差とで学習曲線が乖離すること
- 原因
    - パラメーター数が多い
    - パラメータの値が適切でない
    - ノードが多い、等
    - つまり、入力データ量に対し、ネットワークの自由度(層数、ノード数、パラメーターの値…)が高い
- 正則化(Regularization): ネットワークの自由度(層数、ノード数、パラメーターの値…)を制約する
    - 正則化によって過学習を抑制する
- L1, L2正則化
    - 重みが大きい値をとることで、過学習が発生することがあるが、誤差に対して正則化項を加算することで重みを抑制する
        - 過学習がおこりそうな重みの大きさ以下で重みをコントロールし、かつ重みの大きさにばらつきを出す
    - $ E_n(w) + \dfrac{1}{p}λ \vert \vert x \vert \vert _p $

        ```Python
        weight_decay += weight_decay_lambda * np.sum(np.abs(network.params['W' + str(idx)]))
        
        loss = network.loss(x_batch , d_batch) + weight_decay
        ```

        - 誤差関数(損失関数)にpノルム(距離)を加える(=コスト関数)
        - ここで、pノルムは以下の通り、
        - $ \vert \vert x \vert \vert _p = ( \vert x_1 \vert ^p + \cdots + \vert x_n \vert ^p ) ^ {\dfrac{1}{p}} $

        ```Python
        np.sum(np.abs(network.params['W' + str(idx)]))
        ```

        - $ p = 1 $の場合、L1ノルムはマンハッタン距離、これを用いるのがL1正則化(ラッソ回帰)
            - 微分したrate * np.sign(param)が誤差の勾配に加算される
        - $ p = 2 $の場合、L2ノルムはユークリッド距離、これを用いるのがL2正則化(リッジ回帰)
            - 微分したrate * 2 * paramが誤差の勾配に加算される(ただし実際には2はrateに吸収可能なので、rate * paramと実装される)
    - 特にL1正則化は(角があるため)重みが0のパラメーターが出やすい、結果的にスパース化につながる
    - L1正則化とL2正則化を組み合わせることもできる(Elastic Net)
- ドロップアウト
    - ランダムにノードを削除して学習させる
        - 過学習の課題であるノードの数が多い、への対策
        - データ量を変化させずに、異なるモデルを学習させていると解釈できる

## マルチタスク学習
- マルチタスク学習とは複数の問題を単一のモデルでまとめて解く手法
    - アンサンブル学習も同様
    - これに対比されるのがシングルタスク学習
- 共有層とタスク固有層で構成される
    - 共有層で特徴量を抽出などを行い、その結果を用いてタスク固有層でそのタスク固有の解を得る
- メリット
    - モデル数が少なくて済む
    - 各タスクの相乗効果により、それぞれのタスクで性能向上が期待できる
    - 学習時間や総パラメータ数を減らせる
- 注意点
    - 単一のタスクだけに着目するとより多くのメモリ・演算量が必要
    - 学習の難易度が高い
- バギング
    - 並列に複数の学習器を構築し、それぞれの学習器の結果を用いて総合的な出力結果を求める手法
        - 回帰の場合：出力結果は平均値
        - 分類の場合：出力結果は多数決
    - バギングの流れ
        1. 学習データから決められた回数分のデータを抽出し、それぞれデータセットを作る
        1. データセットからモデルを作る
        1. 1~2を決められた回数分繰り返す
        1. 作成した学習器から結果を構築する
    - バギングと決定木を組み合わせた手法がランダムフォレスト
    - メリット
        - データの一部を訓練データとして使うことで、バリアンス(予測値のばらつき度合い)を抑えることができ、それにより過学習を抑えることができる
        - それぞれ並列で計算できるため、処理時間が抑えることができる
    - 注意点
        - データを使いすぎるとバリアンスが高くなり、過学習の原因となる
        - 訓練データが似通った特徴の場合、テストデータによる検証時に精度向上が期待できない場合がある
- ブースティング
    - バギングが並列なのに対し、ブースティングは直列的に学習
    - ブースティングの流れ
        1. データを決められた回数分抽出して学習器を作成する
        1. 学習器で生成した誤った結果と正解のサンプルを比較する
        1. 誤り率と重要度を学習器ごとに計算し、全体の重みを調整する
    - データの誤り度合いが大きいほど重みが大きくなる
    - メリット
        - 一つ前の学習器で生成したデータを再利用するため、バギングより精度向上が期待できる
    - 注意点
        - バリアンスが高くなりやすく、過学習が起こりやすい
- スタッキング
    - 様々なアルゴリズムを使用して精度を上げる手法
    - スタッキングの流れ
        1. 学習器にランダムフォレストや勾配ブースティングなどを様々な計算法を使って、複数のモデルを用意する
        1. モデルから予測値を出力する
        1. 予測値をまとめたメタモデルを作成する
        1. メタモデルから最終予測値を作成する
    - メリット
        - 様々な計算方法を目的に合わせて使用できるので、精度が上がりやすくなる
    - 注意点
        - 様々な計算方法を目的に合わせて使用する必要があるため、手間がかかる


## 畳み込みニューラルネットワーク(CNN)
- 1979年に福島邦彦によって提唱された視神経を模したニューラルネットワーク、ネオコグニトロンが出発点
    - ネオコグニトロンは、複数の種類の細胞から構成され、その中で最も重要な細胞は「S細胞」(単純型細胞)および「C細胞」(複雑型細胞)と呼ばれる
    - S細胞は特徴抽出を担い、これに対しC細胞は位置ずれを許容する、微小変位（local shift）といったこれらの特徴の変形はC細胞に委ねられている
    - S細胞層はCNNでの畳み込み層、C細胞層はCNNでのプーリング層に相当する
- 次元間で繋がりのあるデータを扱える
    - 特に画像処理に活用されるが、音声などにも活用が可能
- 構造
    - 入力層
    → 畳み込み層(複数)
    → プーリング層
    → 畳み込み層(複数)
    → プーリング層
    → 全結合層
    → 出力層
    - 〜プーリング層までは、次元の繋がりを維持したまま特徴量を抽出する
    - 全結合層〜で人間が欲しい結果を求める
    - LeNetの例
        - 32*32ドットの画像を入力し、最終的に10種類に分類する
        - Input (32, 32)
            → Convolution (28, 28, 6)
            → Subsampling (14, 14, 6)
            → Convolution (10, 10, 16)
            → Subsampling (5, 5, 16)
            → Full connection (120,)
            → Full connection (84,)
            → Gausstan connections: Output (10,)
- 畳み込み層(Convolution)
    - 画像の場合、縦、横、チャンネル(RGB)の3次元のデータをそのまま学習し、次に伝えることができる
        - これに対し、全結合層の場合、縦、横、チャンネル(RGB)の3次元のデータが1次元のデータとして処理されるため、チャンネル間の関連性が学習に反映されない
    - Σ(入力値 * フィルター) = 出力値1
        - フィルターは全結合でいう重み($ \boldsymbol{w} $)
            - フィルターのパラメーターが学習対象
        - フィルターをずらしながら出力値1を得ていく
        - フィルターは(N, N)なので、周囲との繋がりが維持されている(=　次元間の繋がりが保たれている)
        - 入力が(M, M)の場合、基本的には出力は((M-N+1, M-N+1)になる
            - 以下のパディングやストライドがない場合
            - 逆に出力を(M, M)に維持するために、入力の周囲にパディングを行うという処理もある
                - パディングでは、固定値0を埋めたり、最も近い場所の値を埋める
        - 出力はフィルターの枚数(チャンネル)分得られる
        - フィルターを1ますずつずらすのではなく、Xますずつずらすこともある(ストライド)
            - 例えば、2ますずつずらすのが、2ストライド
                - ストライドが増えると、出力のサイズが小さくなる
        - $ O_H = \dfrac{画像の高さ + 2×パディング高さ - フィルターの高さ }{ストライド} + 1 $
        - $ O_W = \dfrac{画像の幅 + 2×パディング幅 - フィルターの幅}{ストライド} + 1 $
    - 活性化関数(出力値1 + バイアス) = 出力値2
    - 実装に際しては、入力を、フィルター適用のために、並び替えた行列を用意しておくことが一般的
        - それによって、フィルターの重みと直接掛け算(内積)ができる
        - サンプルプログラムのim2col(image to column)の処理
            - im2colでは、B(バッチサイズ)×C(チャンネル)数分の入力画像Ih(入力画像高さ)×Iw(入力画像幅)を、行がC×Fh(フィルタ高さ)×Fw(フィルタ幅)、列がB×Oh(出力画像高さ)×Ow(出力画像幅)の二次元配列にする
            - 行がM(フィルタ数)、列がC×Fh×Fwと整形したフィルタに上の二次元配列をかけることで、行がM、列がB×Oh×Owの二次元配列が一気に得られる
- プーリング層
    - 畳み込み層のように(N, N)の領域を切り出し、ずらしながら処理をする
        - しかし、畳み込み層と異なり、重みはない
        - つまり、学習対象となるパラメーターは存在しない
    - プーリング層では対象領域のMax値(Maxプーリング)または平均値(Averageプーリング)を取得する
        - 他にはLpプーリングというものが存在
    - 畳み込み層ではフィルターの枚数によってチャンネル数が変わったが、プーリング層では一般的にチャンネル数は変わらない

- AlexNet
    - Input (224, 224, 3)
        → Convolution(11, 11) → (55, 55, 96)
        → Max pooling(5, 5) → (27, 27, 256)
        → Max pooling(3, 3) → (13, 13, 384)
        → Convolution(3, 3) → (13, 13, 384)
        → Convolution(3, 3) → (13, 13, 256)
        → Full connection (4096,)
        → Full connection (4096,)
        → Output (1000,)
    - 畳み込み層から全結合層へ
        - Flatten: 単純に横に並べる　(13, 13, 256) → (43264,)
        - Global Max Pooling: 各チャンネルの最大値を採用する　(13, 13, 256) → (256,)
        - Global Average Pooling: 各チャンネルの平均値を採用する　(13, 13, 256) → (256,)
        - Global Max PoolingやGlobal Average Poolingは効率がいい割に性能が良い
    - (4096,)の全結合層の出力にドロップアウトを使用し、過学習を防いでいる

# 参考文献
- [ディープラーニング入門 Chainer チュートリアル](https://tutorials.chainer.org/ja/tutorial.html)
- ディープラーニングE資格エンジニア問題集
- ゼロから作るDeep Learning
- 機械学習のエッセンス -実装しながら学ぶPython,数学,アルゴリズム- (Machine Learning)

# 確認テスト
- 連鎖律の問題
    - $ z = t^2 $
    - $ t = x + y $
    - $ \dfrac{dz}{dx} = \dfrac{dz}{dt} \dfrac{dt}{dx} $
    - $ = 2t·1 $
    - $ = 2(x + y) $

- シグモイド関数の微分の最大値
    - 0.25 (@ 入力が0の時)

- 重みの初期値を0にする弊害
    - 重みにばらつきがなくなり、多様な見方ができなくなる
    - 多数の重みを持つ意味がなくなる
    - 結果的に学習がうまく進まない

- バッチ正規化の効果
    - 初期値への依存が減る、勾配消失が起こりにくくなる、外れ値が少なくなり過学習が起こりにくくなるなど、学習が安定し、結果的に速く学習できる

- Optimizerの特徴
    - モメンタムはモメンタム項を加える。大域的最適解となりやすいが、最後まで動き続けてしまう
    - AdaGradは、誤差をパラメータで微分したものと再定義した学習率の積を減算する。勾配の緩やかな斜面に対して、最適値に近づけるが、勾配が緩やかなところで動きにくくなってしまう
    - RMSPropは、誤差をパラメータで微分したものと再定義した学習率の積を減算する。大域的最適解が得やすく、ハイパーパラメーターの調整が必要な場合が少ない
    - Adamは、モメンタムとRMSPropの良いとこどり。現在のところ最強

- リッジ回帰におけるハイパーパラメーター
    - ハイパーパラメーターを大きな値に設定すると、全ての重みが限りなく0に近付く
    - ハイパーパラメーターを0に設定すると、単純な線形回帰となる

- L1正則化のグラフ
    - L1はマンハッタン距離なので、角がある
    - 角があるが故に重み0が出せ、モデルをスパース化しやすい

- サイズ6×6の入力画像を、サイズ2×2のフィルタで畳み込んだ時の出力画像のサイズ
    - ストライド1、パディング1
    - (6+2*1-2)/1 + 1 = 7
    - 7×7