Parametric Neural Network

# ニューラルネットワークについて
## そもそもの大まかな流れ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137120/922a241d-9c23-c8de-0aaf-1bc3f293952e.png)

1. 入力情報に従って、活性化関数の値を次のノードに渡す
2. 受け取った全ての情報を重み $w$ とバイアス $b$ を元に線形結合する。
3. 線形結合した値を活性化関数に入力して、その値を次のノードに渡す
4. 手順2〜３を繰り返して、最終的なノードから値を出力する
5. （PNNでは最後のレイヤーは1ノードしか持たないが）最終的な出力から損失関数を計算する：手順１〜６がforward propagation
6. 次に損失関数を利用して各分岐における重みを計算する：back propagation
7. 手順１〜７で一回のトレーニングが終了する。後はこれを規定回数繰り返して、より正解を上手く記述する重みの集合＝ニューラルネットワークを作成する

手順６において一般的には、勾配降下法（損失関数の微分係数を用いて重みを更新する手法）が取られる。そのため、損失関数は微分可能な分布である必要がある。以下では、活性化関数、損失関数、勾配降下法について簡単に記述する。

## 活性化関数について
一つ前のレイヤーで計算された入力値は、まず各weightとバイアスに従って線形結合される。そしてそれらを活性化関数への引数として、次のレイヤーには活性化関数の出力値を渡す...、を延々と繰り返すのが機械学習。なので活性化関数の重要な意味合いとしては、その数式の形（「なんでexponentialなんだ、なんで分数でxxx...」というのはあまり意味のない議論）ではなく、どういう値をどれだけの範囲に渡って出力するかが重要である。今回使用している二種類の活性化関数についてまとめる。

### relu（ランプ関数）
xが0以上の場合に正比例の形になる。シグモイド関数、原点から遠ざかるほど勾配が無くなる（微分係数が0に近づく）ため、一度ユニットが大きな値を持ってしまったら学習が停滞する問題があった。ランプ関数では勾配消失問題が経験的に解消されることが知られている。

```math
f(x) = x~~(x>0)
```    

### sigmoid（シグモイド関数）
関数の出力値は０から1の間を取る。

```math
f(x) = \frac{1}{1-e^x}
```
## 損失関数（=誤差関数）
ニューラルネットワークの $n$ 次元の出力を損失関数を用いて評価する。概念としては、正解の $n$ 次元の値と比較して、差が小さければ損失関数の値も小さいものとなる。そのため、よいニューラルネットワークは損失関数の出力値が小さくなる。

- binary_crossentropy
    - 2クラス分類に使用（0か１か、背景事象か信号事象かという高エネでよく使用する）

```math
E(w) = -\sum_n^{N} \left( d_n\log y_n + (1-d_n)\log(1-y_n) \right)
```




## 勾配法
損失関数を用いてどのように重みを更新するか、ニューラルネットワークのキモとなる部分。ここでは一般的なSDGについて説明する。SDGでは損失関数の微分係数を用いて次のトレーニングに使用する重みを計算する。この時に用いるパラメータに

- $\eta$：学習係数、学習率、learning rate
- $\alpha$：モーメンタム
- $h$：減衰率、learning decay rate

```math
w^{t+1} = w^{t} - \eta\frac{1}{\sqrt{h}}\frac{\partial E(w^{t})}{\partial w^{t}} + \alpha\Delta w
```

という上式に従って計算していく。ここで勾配法についての概要レベルの知識として

- 学習係数が大きすぎると、重みの値が $t$ と $t+1$ で大きく異なってしまい、トレーニングが収束しにくくなる
- 学習係数が小さすぎると、重みの更新の度合いも小さくなり、学習に時間が掛かってしまう
- 減衰率を導入することで、学習係数もトレーニングに従って更新していく、という手法を取る

が挙げられる。

## 学習方法
一般的にニューラルネットワークの学習方法は「ミニバッチ学習」を念頭に置いて説明されている。ここではどのタイミングで損失関数を用いてパラメータの更新（=重みの更新=モデルの更新）を行うかについて説明する。

- オンライン学習
    - 入力情報で計算した損失関数を元に、毎回モデルを更新する手法。
    - 例えば1000枚の画像があれば、1000回のパラメータ更新を経験する。
- バッチ学習
    - バッチ単位（=全データを一括にして）でモデルを更新する手法。
    - 例えば1000枚の画像があれば、1回のパラメータ更新を経験する。この時に用いる損失関数は、1000枚の画像それぞれにおける損失関数を平均したもの。

```math
L=\frac{1}{N}\sum_{i=1}^{N} l_i
```

- ミニバッチ学習
    - 全データをミニバッチに分割して、ミニバッチ処理毎にモデルを更新する手法
    - ミニバッチに含まれるデータ数（=バッチサイズ）処理毎に、損失関数を平均して算出しモデルの更新を行う。そしてその更新されたモデルに従って、次のミニバッチでの学習を開始する。
    - 例えば1000枚の画像で、バッチサイズ100枚に分割するとする。このときサブセットは10個存在するので、10回のパラメータ更新を経験する。

先述したように、一般的にはミニバッチ学習手法が広く使用されている。前例でサブセット10個を処理した段階で、１エポック終了という数え方をする。

# PNN
高エネではBDTがよく使用される。小統計に強かったり、基本的にはDTなので極力ブラックボックスにならずに済んだりメリットは多い。粒子識別では既にDNNが使用されたりしているので、ProfileLLのときのように「使わんと損やで」という形でニューラルネットをS/N比改善のために使用することにした。2016年に提唱されているモデルがParametrised Neural Network（PNN）であり、それを一般的なpythonライブラリを用いて構築する。今回使用するライブラリは、

- uproot
    - 高エネ界隈で用いられているROOTフォーマットをpythonでdata frameにするやつ。
- sklearn （scikit-learn、サイキットラーン）
    - pythonの機械学習ライブラリ
- keras
    - TensorFlow上で動作する、ニューラルネットワークのライブラリ

## ROOTファイルを読み込む（高エネ的前段階処理）
### uproot
CERNライブラリROOTフォーマットのデータを読み込むためのPythonモジュール。ROOT Ntupleをpython `DataFrame`にしただけで、構造は何も変わらない。行がイベントに対応、列が各変数に対応。

```python
import uproot
f = uproot.open("data.root")
print(f.keys())
# ['data;1', 'background;1", ....]

f['data'].pandas.df()
#        Btag  EventFlavour  EventNumber  FourBodyMass  Jet0CorrTLV  ...  mass  mass_scaled           sT      sTNoMet  signal    weight  weight_scaled
#entry                                                               ...                                                                              
#9        2.0           8.0     560044.0   1666.098145   542.301636  ...  900     0.352941  #1566.298340  1404.298218       1  0.003898       0.028524
#10       1.0           5.0     560480.0   1606.993896   241.007111  ...  900     0.352941  #1841.925049  1434.105713       1  0.004255       0.031135
#11       2.0           0.0     561592.0   1857.901245   721.780457  ...  900     0.352941  #2444.058105  1910.263306       1  0.002577       0.018855
#15       2.0           5.0     561088.0   1348.327515   174.501556  ...  900     0.352941  #1328.051147  1029.908447       1  0.003360       0.024585

f['data'].pandas.df('EventNumber')
#        EventNumber
#entry              
#0      2.148751e+08
#1      2.143515e+08
#2      6.018242e+07
#3      2.868989e+07
...
```

上記が読み込んですぐのデータフレームで、ここから必要な情報の値だけ（使用する入力情報）をピックアップしたデータフレームを作成する。次の工程で使用するデータフレームのスライス方法について簡単に説明する。
uprootで読み込んだ独自のデータフレームは末尾に `mass_scaled` がいるため、`X[:, :-1]`とスライスをする。これは、「行の全て、列は始めから最後1つ前まで」という意味のスライス方法である。以上を踏まえて、次から核心に移る。

- ```from sklearn.utils import shuffle```
    - テスト・トレーニングデータを分割する際に、ランダムに並び替えてから分割させる方法
    - 何もしなければデータの先頭から順番に分割されてしまう

## 実際のトレーニング工程
### 事前処理（スケール変換）
扱うデータのスケール（=桁数）を揃える必要がある。そこで使用されるのは、sclearnのメソッドであり、今回は外れ値に強い`RobustSclaer`を使用している。そもそも外れ値があると、特徴量の平均・分散が外れ値に大きく影響されてしまうため、標準化がうまく機能しなくなる。データの性質そのままに、機械が扱いやすい情報に焼き直していると考えておこう。

- StandardScaler
    - データの分布を標準化する
- RobustScaler
    - 今回使っている処理はコレ
    - fit_transformで、fit（配列Xの平均と分散を計算）とtransform（）を行い、その配列を格納（X）


### 作成したNNモデル
- keras.layers：レイヤーの性質を定義する
    - Input
    - Dense
        - 全結合ニューラルネットワークレイヤー。全てのパーセプトロン（ノード）が、次のレイヤーのパーセプトロンに繋がっている
        - ポンチ絵で一般的に描かれるようなNNのレイヤーのこと
- keras.model
    - Kerasには二種類のモデルの（Python上での）定義の仕方がある
    - Sequential modelとFunctional API Model

#### 実際のコーディング
1. まず初めに Inputで入力情報の次元数を定義する
2. Dense（全結合型ニューラルネットワーク）を、各レイヤー毎に定義する。このタイミングで使用する活性化関数（avitivation function）を定義しておく。
3. 隠れレイヤーは次のレイヤーにそのまま出力するので、32次元の出力（今回は[32,32,32]の32次元3層でニューラルネットワークを定義している）。そして最後のレイヤーは１つのノードで、[0,1]を出力させる。
    1. hidden layerの活性化関数は `relu` で、最後のレイヤーの活性化関数は `sigmoid`を使用する。

```python
x = Input(shape=(n_input_vars,))
d = x 
for n in self.layer_size:
    d = Dense(n, activation=self.activation)(d)

y = Dense(1, activation="sigmoid")(d)
Model(x, y)
```

### 使用している勾配法

今回使用した勾配法は非常にオーソドックスなSGD（Stochastic Gradient Descent : 確率的勾配降下法)である。以下の式で損失関数$E(w)$を用いて、各重みを更新していく。

```math
w^{t+1} ← w^{t} - \eta \frac{\partial E(w^{t})}{\partial w^{t}} + \alpha \Delta w^{t}
```

ここで$\eta$は学習率（学習係数）、$\alpha$はモーメンタムを表している。

```python
sgd = SGD(lr=self.learning_rate, momentum=self.momentum, nesterov=self.nesterov, decay=self.learning_rate_decay)
```

## Kerasを用いたトレーニング
### compile
ここまでで述べた知識（それ以上の背景知識も踏まえて）を使って、kerasでニューラルネットワークをトレーニングするには次の工程を取る。まず、モデルを「コンパイル」する必要がある

```python
model.compile(...)
```

### fit
次にコンパイルした後に、fit = 実際のトレーニングを行う。

- batch_size
    - それぞれのサブセットに含まれるデータの数をバッチサイズという。
    - ex) 10000事象のデータをバッチサイズ10で分割すると、1000個のサブセットが出来上がる。
- verbose
    - 0：出力一切なし
    - 1：プログレスバーあり
    - 2：プログレスバーなし
- callbacks
    - エポック終了時に呼び出したい関数のリストを渡す。ex. エポック毎にとある情報をprintする自前の関数、など。

### ちなみに
PNNは使用する力学的入力情報とは別に、理論パラメータを入力情報に取るのが特徴である。信号事象のシミュレーションには正しく理論パラメータが存在するが、では背景事象の理論パラメータはどうするか？例えば質量パラメータを入力情報としているのであれば、背景事象のトレーニングの際にはランダムな値を選択してトレーニングする。

# 参考URL
以下のサイトを大いに参考にさせて頂きました。ありがとうございます。

- https://aizine.ai/preprocessing0614/
- https://qiita.com/tokkuman/items/1944c00415d129ca0ee9
- https://note.nkmk.me/python-tensorflow-keras-basics/
- https://products.sint.co.jp/aisia/blog/vol1-4
- https://qiita.com/kenta1984/items/bad75a37d552510e4682