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

# 前段

## 識別モデルと生成モデル
- 機械学習(深層学習)モデルは入力･出力の目的で分類できる
- 識別(discriminative, backward)
    - データを目的のクラスに分類する
    - データ→クラス
    - $ p(C_k \vert \boldsymbol{x}) $を求める
        - あるデータ$\boldsymbol{x}$が与えられたという条件のもとでクラス$C_k$である確率
- 生成(generative, forward)
    - 特定のクラスのデータを生成する
    - クラス→データ
    - $ p(\boldsymbol{x} \vert C_k) $を求める
        - あるクラス$C_k$に属するという条件のもとでのデータ$\boldsymbol{x}$の分布
- 識別モデル
    - 具体的なモデルの例
        - 決定木
        - ロジスティック回帰
        - SVM
        - ニューラルネットワーク、など
    - 高次元から低次元に圧縮することになる
        - 必要な学習データは少ない
    - 画像認識などに利用される
- 生成モデル
    - 具体的なモデルの例
        - 隠れマルコフモデル
        - ベイジアンネットワーク
        - 変分オートエンコーダー(VAE)
        - 敵対的生成ネットワーク(GAN)、など
    - 低次元から高次元に拡張することになる
        - 必要な学習データは多い
    - 画像の超解像やテキスト生成などに利用される
- 識別器(Classifier)の開発アプローチ
    - 生成モデル
        - $ p(\boldsymbol{x} \vert C_k) $を推定した上で、ベイズの定理で、$ p(C_k \vert \boldsymbol{x}) $を求める
    - 識別モデル
        - $ p(C_k \vert \boldsymbol{x}) $を推定する
    - 識別関数
        - 入力値$ \boldsymbol{x} $を直接クラスに写像(変換)する関数$ f(\boldsymbol{x}) $を推定
        - 確率は計算されない(決定的な識別)

## 万能近似定理(普遍性定理、Universal Approximation Theorem)
- 有限個のユニットを持つ一層の隠れ層で構成されるfeed-forward networkは、一定の条件のもと、任意の連続関数を近似することが出来る
    - ニューラルネットワークの表現力の理論的裏付け


# ニューラルネットワーク
- 全体像
    - 入力層
    - 中間層
    - 出力層
    - 出力層から出力を得る、この出力と訓練データを比較し、誤差の最小化を図る
- ニューラルネットワークは回帰にも分類にも利用できる(万能近似定理)
    - 回帰: 連続する実数値を取る関数の近似
    - 分類: 離散的な結果を予想するための分析
- 一般的に中間層が4つ以上あるニューラルネットワークを深層学習という

## Section 1: 入力層〜中間層
- 入力が$ \boldsymbol{x} = [x_1, x_2, \cdots, x_n]^T $、重みが$ \boldsymbol{W} = [w_1, w_2, \cdots, w_n]^T $とした場合、中間層への入力$u$は、
    - $ u = w_1x_1 + w_2x_2 + \cdots + w_nx_n + b $
    - $ = \boldsymbol{W}\boldsymbol{x} + b $

## Section 2: 活性化関数
- ニューラルネットワークにおいて、次の層への出力の大きさを決める非線形の関数
    - 入力値の値によって、次の層への信号のON/OFFや強弱を定める働きをもつ
- 中間層用の活性化関数
    - ステップ関数
        - $ f(x) = \begin{cases} 1 &ensp; (x \geqq 0) \\\\ 0 &ensp; (x < 0) \end{cases} $
        - 単純パーセプトロンと同じ動作
        - $x=0$で微分できないため、NNでは用いられない
        ```python
        # ステップ関数
        def step_function(x):
            if x>=0:
                return 1
            else:
                return 0
        ```
    - シグモイド関数(ロジスティック関数)
        - $ f(x) = \dfrac{1}{1+e^{-x}} $
        - ステップ関数と似ているが、滑らかなので微分ができる
        - 勾配消失問題があるため、利用は減っている
        ```python
        # シグモイド関数（ロジスティック関数）
        def sigmoid(x):
            return 1/(1 + np.exp(-x))
        ```
    - ReLU関数(正規化線形関数、ランプ関数)
        - $ f(x) = \begin{cases} x &ensp; (x > 0) \\\\ 0 &ensp; (x \leqq 0) \end{cases} $
        - 正規化機能を持たないため、勾配消失問題が起きにくく、形も簡単であるため、今最も使われている
        ```python
        # ReLU関数
        def relu(x):
            return np.maximum(0, x)
        ```

## Section 3: 出力層
- 出力層用の活性化関数
    - 中間層(閾値の前後で信号の強弱を調整)と異なり、出力層では信号の大きさ(比率)はそのままに変換する
    - 分類問題の場合、出力層の出力は0~1の範囲に限定し、総和を1とする必要がある
    - 恒等写像
        - 何もしない
        - $ f(u) = u $
    - シグモイド関数(ロジスティック関数)
        - $ f(x) = \dfrac{1}{1+e^{-x}} $
    - ソフトマックス関数
        - 出力を正規化して、確率として解釈する際に用いられる
        - $ f(\boldsymbol{i}, \boldsymbol{u}) = \dfrac{e^{u_i}}{\sum_{k=1}^Ke^{u_k}} $
            - Kが分類結果のクラス数
        ```python
        # ソフトマックス関数
        def softmax(x):
            # ミニバッチ処理用
            if x.ndim == 2:
                x = x.T
                x = x - np.max(x, axis=0)
                y = np.exp(x) / np.sum(np.exp(x), axis=0)
            return y.T
            # 一般用
            x = x - np.max(x) # オーバーフロー対策
            return np.exp(x) / np.sum(np.exp(x))
        ```
    - tanh関数(ハイパボリックタンジェント関数,双曲線正接関数)
        - -1.0～1.0の数値を出力するという特性を生かして、主に出力層で用いられる
        - $ f(u) = \dfrac{e^u - e^{-u}}{e^u + e^{-u}} $
        - 導関数は$ f'(u) = 1 - f(u)^2 $
        ```python
        # tanh関数
        def tanh(x):
            return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
        ```
- 出力層用の誤差関数
    - 二乗和誤差
        - $ E_n(w) = \dfrac{1}{2} \displaystyle \sum_{j=1}^{J}(y_j - d_j)^2 = \dfrac{1}{2} \vert\vert (y - d) \vert\vert ^2 $
        
        ```python
        # 平均二乗誤差
        def mean_squared_error(d, y):
            return np.mean(np.square(d - y)) / 2
        ```
        - yが先でもdが先でも同じ(2乗するので)
    - 交差エントロピー誤差
        - 分類問題に用いられる
        - $ E_n(\boldsymbol{w}) = - \displaystyle \sum_{i=1}^I d_i \log y_i $
        ```python
        # クロスエントロピー
        def cross_entropy_error(d, y):
            if y.ndim == 1:
                d = d.reshape(1, d.size)
                y = y.reshape(1, y.size)
                
            # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
            if d.size == y.size:
                d = d.argmax(axis=1)
                     
            batch_size = y.shape[0]
            return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size
        ```
        - +1e-7はlogの対象が0になって-∞に発散するのを防ぐために加算

||回帰|二値分類|多クラス分類|
|:--|:-:|:-:|:-:|
|活性化関数|恒等写像|シグモイド関数|ソフトマックス関数|
|誤差関数|二乗誤差|交差エントロピー|交差エントロピー|


## Section 4: 勾配降下法
- 勾配降下法
    - 誤差$ E(\boldsymbol{w}) $を最小化するパラメーター$ \boldsymbol{w} $を学習する
    - $ \boldsymbol{w^{t+1}} = \boldsymbol{w^{t}} - ε∇E $
        - ここで、$ ∇E = \dfrac{∂E}{∂\boldsymbol{w}} = [\dfrac{∂E}{∂w_1} \cdots \dfrac{∂E}{∂w_M}]$
    - 学習率$ ε $
        - 学習率が大きすぎると、最適地点を通り過ぎて発散する可能性がある
        - 学習率が小さすぎると、収束までに時間がかかってしまう
        - また局所極小解で収束する可能性がある
    - アルゴリズムとして以下がある
        - Momentum
        - AdaGrad
        - Adadelta
        - Adam (一番よく使われる)
- 確率的勾配降下法
    - $ \boldsymbol{w^{t+1}} = \boldsymbol{w^{t}} - ε∇E_n $
        - ここで$ E_n $はランダムに抽出したサンプルの誤差
    - 全サンプルではなく、ランダムに抽出したサンプルの誤差で学習する
        - データが冗長な場合の計算コストの軽減
        - 望まない局所極小解に収束するリスクの軽減
        - オンライン学習ができる
- ミニバッチ勾配降下法
    - $ \boldsymbol{w^{t+1}} = \boldsymbol{w^{t}} - ε∇E_t $
        - ここで$ E_t $は、ランダムに分割したデータの集合(ミニバッチ)$D_t$に属するサンプルの平均誤差
    - 確率的勾配降下法のメリットを損なわず、計算機の計算資源を有効利用できる
        - GPUを利用したSIMD(Single Instruction Mutli Data)

- 誤差勾配の計算: 数値微分
    - プログラムで微小な数値を生成し擬似的に微分を計算する一般的な手法
    - $ \dfrac{∂E}{∂w_m} \approx \dfrac{E(w_m + h) - E(w_m - h)}{2h} $
    - ただし、これは順伝播の計算を繰り返し行う必要があり、負荷が高い → 誤差逆伝播法を利用する

## Section 5: 誤差逆伝播法
- 算出された誤差を、出力層側から順に微分し、前の層前の層へと伝播
    - 最小限の計算で各パラメータでの微分値を解析的に計算する手法
- 計算例
    - $ E(y) = \dfrac{1}{2} \displaystyle \sum_{j=1}^{J}(y_j - d_j)^2 = \dfrac{1}{2} \vert\vert (y - d) \vert\vert ^2 $
        - 誤差関数は二乗誤差関数
    - $ y = u^{(L)} $
        - 出力層の活性化関数として恒等写像を採用
    - $ u^{(l)} = w^{(l)}z^{(l-1)} + b^{(l)} $
        - 総入力の計算
    - 求める微分は以下の通り
    - $ \dfrac{∂E}{∂w_{ji}^{(2)}} = \dfrac{∂E}{∂y} \dfrac{∂y}{∂u} \dfrac{∂u}{∂w_{ji}^{(2)}} $
        - 微分の連鎖律
    - $ \dfrac{∂E}{∂y} \dfrac{∂y}{∂u} \dfrac{∂u}{∂w_{ji}^{(2)}} = (y - d)·[0 \cdots z_i \cdots 0]^T $
    - $ = (y_j - d_j)z_i $
        - $ \dfrac{∂E}{∂y} = y - d $
        - $ \dfrac{∂y}{∂u} = 1 $ (恒等写像の微分)
        - $ \dfrac{∂u}{∂w_{ji}^{(2)}} = [0 \cdots z_i \cdots 0]^T $
            - $ w_{ji} $で微分すると、掛ける対象の$ z_i $のみ残る 

# 色々と補足
## 入力層の設計
- 入力としてとり得るデータ
    - 連続する実数
    - 確率
    - フラグ値
        - one-hotベクトル
- 入力層として取るべきでないデータ
    - 欠損値が多いデータ
    - 誤差の大きいデータ
    - 出力そのもの、出力を加工した情報
    - 連続性の無いデータ(背番号とか）
    - 無意味な数が割り当てられているデータ
        - one-hotベクトルのような形にすると良い
- 欠損値の扱い
    - ゼロで詰める（ただし弊害もあり)
    - 欠損値を含む集合を除外
    - 入力として採用しない
- 数値の正規化(Normalization)・標準化(Standardization)
    - 正規化: 0〜1にスケールする
    - 標準化: 平均0、分散1(ルートである標準偏差も0)にスケールする

## 過学習
- 機械学習での話と同じ
- ドロップアウト
    - 過学習を防ぐために、一定割合のノードを不活性化させながら学習を行う
        - 重みを0にする

## 開発環境
- プロセッサ
    - CPU
    - GPU
    - FGPA
    - ASIC(TPU)

## データ集合の拡張(Dataset Augmentation)
- 学習データが不足するときに人工的にデータを作り水増しする手法
    - 分類タスク(画像認識)に効果が高い
- 様々な手法がある
    - オフセット、ノイズ、ドロップアウト、回転、拡大・縮小など
- 様々な変換を組み合わせて水増しデータを生成
- データ拡張の結果、データセット内で混同するデータが発生しないよう注意
    - 数字の9を180度回転させると6と混同する
- 中間層へのノイズ注入で様々な抽象化レベルでのデータ拡張が可能
- データ拡張の効果と性能評価
    - データ拡張を行うとしばしば劇的に汎化性能が向上する。
    - ランダムなデータ拡張を行うときは学習データが毎度異なるため再現性に注意。
- データ拡張とモデルの捉え方
    - 一般的に適用可能なデータ拡張(ノイズ付加など)はモデルの一部として捉える
    - 特定の作業に対してのみ適用可能なデータ拡張(クロップなど)は入力データの事前加工として捉える
        - 例: 不良品検知の画像識別モデルに製品の一部だけが拡大された画像は入力されない

## CNNで扱えるデータの種類
- CNNでは次元間で繋がりのあるデータを扱える

## 特徴量の転移
- Transfer Learning and Domain Adaptation
- 深層学習モデルは、入力層に近い側の特徴量抽出とタスクに固有な処理の組み合わせと考えることができる
    - 特徴量抽出は学習コストが高く、必要なデータも大量
    - タスク固有処理は学習コストが低く、必要なデータも少量
- この中で、特徴量抽出だけを行うことも可能
    - プリトレーニング: 教師なし学習で特徴量抽出器を作る
    - VGG(画像)、BERT(テキスト)
- 逆に学習済みの重みを活用したファインチューニングや転移学習というアプローチも存在
    - ファインチューニング: ベースモデル重みを再学習
    - 転移学習: ベースモデルの重みを固定
- 現在はプリトレーニングを活用し、その上で転移学習をすることが一般的
    - トータルでのコストを低く、性能を上げることができる

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

# 確認テスト
- ディープラーニングは結局何をやろうとしているのか
    - 明示的なプログラムの代わりに多数の中間層を持つニューラルネットワークを用い、入力値から目的とする出力値に変換するを数学モデルを構築する
    - 入力層から入力されたデータに中間層で重み付けをして、出力層で確率を得る
    - 中間層での重み付けの学習を行い、最終的に学習されたモデルを利用し識別を行う(識別モデルの場合)
- 最適化の対象
    - 最終目的は出力値だが、実際に学習するのは重み$w$とバイアス$b$

- 中間層への入力をPythonで書く
    ```Python
    u = np.dot(x, W) + b
    ```

- 線形と非線形
    - 線形は加法性: $ f(x+y) = f(x) + f(y) $、斉次性: $ f(kx) = kf(x) $を満たす

- 中間層の出力をPythonで書く
    ```Python
    # シグモイド関数はfunctionsで定義されている
    z = functions.sigmoid(u)
    ```

- 二乗誤差において、引き算でなく二乗する意味、また1/2の意味
    - 引き算だとプラスとマイナスで打ち消しあってしまう
    - 二乗することで必ずプラスになる
    - 二乗の微分で2が降りるため、1/2と打ち消し合う
        - 本質的な意味はない

- ソフトマックス関数のソースコードを解説する
    ```python
    # ソフトマックス関数
    def softmax(x):
        # ミニバッチ処理用
        if x.ndim == 2:
            x = x.T
            # オーバーフロー対策のため、xからxの最大値を引く(結果は変わらない)
            x = x - np.max(x, axis=0)
            # 計算式通り
            y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T
        # 一般用
        # オーバーフロー対策のため、xからxの最大値を引く(結果は変わらない)
        x = x - np.max(x)
        # 計算式通り
        return np.exp(x) / np.sum(np.exp(x))
    ```

- 交差エントロピー誤差関数のソースコードの解説
    ```python
    # クロスエントロピー
    def cross_entropy_error(d, y):
        if y.ndim == 1:
            d = d.reshape(1, d.size)
            y = y.reshape(1, y.size)
            
        # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
        if d.size == y.size:
            d = d.argmax(axis=1)
                 
        batch_size = y.shape[0]
        # 数式通りに計算して戻す
        # +1e-7はlogの対象が0になって-∞に発散するのを防ぐために加算
        return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size
    ```

- 勾配降下法のソースコード
    - $ \boldsymbol{w^{t+1}} = \boldsymbol{w^{t}} - ε∇E $
    ```Python
    for key in ('W1', 'W2', 'b1', 'b2'):
        network[key]  -= learning_rate * grad[key]
    ```
    - $ ∇E = \dfrac{∂E}{∂\boldsymbol{w}} = [\dfrac{∂E}{∂w_1} \cdots \dfrac{∂E}{∂w_M}]$
    ```Python
    grad = backward(x, d, z1, y)
    ```

- オンライン学習
    - 学習データが入ってくるたびに都度パラメーターを更新し、学習を進めていく方法
        - これに対し、バッチ学習は一度に全ての学習データを使ってパラメーター更新を行う
        - ミニバッチは、学習データの一部を使う

- 誤差逆伝播法の実装

    - $ \dfrac{∂E}{∂y} $

    ```Python
    delta2 = functions.d_mean_squared_error(d, y)
    ```

    
    - $ \dfrac{∂E}{∂y} \dfrac{∂y}{∂u} $
        - 上に同じ($ \dfrac{∂y}{∂u} $が1なので)

    - $ \dfrac{∂E}{∂y} \dfrac{∂y}{∂u} \dfrac{∂u}{∂w_{ji}^{(2)}} $

    ```Python
    # z1は順伝播で以下のように生成される
    # z1, y = forward(network, x)
    grad['W2'] = np.dot(z1.T, delta2)
    ```