ニューラルネットワークについて
そもそもの大まかな流れ
- 入力情報に従って、活性化関数の値を次のノードに渡す
- 受け取った全ての情報を重み $w$ とバイアス $b$ を元に線形結合する。
- 線形結合した値を活性化関数に入力して、その値を次のノードに渡す
- 手順2〜3を繰り返して、最終的なノードから値を出力する
- (PNNでは最後のレイヤーは1ノードしか持たないが)最終的な出力から損失関数を計算する:手順1〜6がforward propagation
- 次に損失関数を利用して各分岐における重みを計算する:back propagation
- 手順1〜7で一回のトレーニングが終了する。後はこれを規定回数繰り返して、より正解を上手く記述する重みの集合=ニューラルネットワークを作成する
手順6において一般的には、勾配降下法(損失関数の微分係数を用いて重みを更新する手法)が取られる。そのため、損失関数は微分可能な分布である必要がある。以下では、活性化関数、損失関数、勾配降下法について簡単に記述する。
活性化関数について
一つ前のレイヤーで計算された入力値は、まず各weightとバイアスに従って線形結合される。そしてそれらを活性化関数への引数として、次のレイヤーには活性化関数の出力値を渡す...、を延々と繰り返すのが機械学習。なので活性化関数の重要な意味合いとしては、その数式の形(「なんでexponentialなんだ、なんで分数でxxx...」というのはあまり意味のない議論)ではなく、どういう値をどれだけの範囲に渡って出力するかが重要である。今回使用している二種類の活性化関数についてまとめる。
relu(ランプ関数)
xが0以上の場合に正比例の形になる。シグモイド関数、原点から遠ざかるほど勾配が無くなる(微分係数が0に近づく)ため、一度ユニットが大きな値を持ってしまったら学習が停滞する問題があった。ランプ関数では勾配消失問題が経験的に解消されることが知られている。
f(x) = x~~(x>0)
sigmoid(シグモイド関数)
関数の出力値は0から1の間を取る。
f(x) = \frac{1}{1-e^x}
損失関数(=誤差関数)
ニューラルネットワークの $n$ 次元の出力を損失関数を用いて評価する。概念としては、正解の $n$ 次元の値と比較して、差が小さければ損失関数の値も小さいものとなる。そのため、よいニューラルネットワークは損失関数の出力値が小さくなる。
- binary_crossentropy
- 2クラス分類に使用(0か1か、背景事象か信号事象かという高エネでよく使用する)
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
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枚の画像それぞれにおける損失関数を平均したもの。
L=\frac{1}{N}\sum_{i=1}^{N} l_i
- ミニバッチ学習
- 全データをミニバッチに分割して、ミニバッチ処理毎にモデルを更新する手法
- ミニバッチに含まれるデータ数(=バッチサイズ)処理毎に、損失関数を平均して算出しモデルの更新を行う。そしてその更新されたモデルに従って、次のミニバッチでの学習を開始する。
- 例えば1000枚の画像で、バッチサイズ100枚に分割するとする。このときサブセットは10個存在するので、10回のパラメータ更新を経験する。
先述したように、一般的にはミニバッチ学習手法が広く使用されている。前例でサブセット10個を処理した段階で、1エポック終了という数え方をする。
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
にしただけで、構造は何も変わらない。行がイベントに対応、列が各変数に対応。
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
実際のコーディング
- まず初めに Inputで入力情報の次元数を定義する
- Dense(全結合型ニューラルネットワーク)を、各レイヤー毎に定義する。このタイミングで使用する活性化関数(avitivation function)を定義しておく。
- 隠れレイヤーは次のレイヤーにそのまま出力するので、32次元の出力(今回は[32,32,32]の32次元3層でニューラルネットワークを定義している)。そして最後のレイヤーは1つのノードで、[0,1]を出力させる。
- hidden layerの活性化関数は
relu
で、最後のレイヤーの活性化関数はsigmoid
を使用する。
- hidden layerの活性化関数は
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)$を用いて、各重みを更新していく。
w^{t+1} ← w^{t} - \eta \frac{\partial E(w^{t})}{\partial w^{t}} + \alpha \Delta w^{t}
ここで$\eta$は学習率(学習係数)、$\alpha$はモーメンタムを表している。
sgd = SGD(lr=self.learning_rate, momentum=self.momentum, nesterov=self.nesterov, decay=self.learning_rate_decay)
Kerasを用いたトレーニング
compile
ここまでで述べた知識(それ以上の背景知識も踏まえて)を使って、kerasでニューラルネットワークをトレーニングするには次の工程を取る。まず、モデルを「コンパイル」する必要がある
model.compile(...)
fit
次にコンパイルした後に、fit = 実際のトレーニングを行う。
- batch_size
- それぞれのサブセットに含まれるデータの数をバッチサイズという。
- ex) 10000事象のデータをバッチサイズ10で分割すると、1000個のサブセットが出来上がる。
- verbose
- 0:出力一切なし
- 1:プログレスバーあり
- 2:プログレスバーなし
- callbacks
- エポック終了時に呼び出したい関数のリストを渡す。ex. エポック毎にとある情報をprintする自前の関数、など。
ちなみに
PNNは使用する力学的入力情報とは別に、理論パラメータを入力情報に取るのが特徴である。信号事象のシミュレーションには正しく理論パラメータが存在するが、では背景事象の理論パラメータはどうするか?例えば質量パラメータを入力情報としているのであれば、背景事象のトレーニングの際にはランダムな値を選択してトレーニングする。
参考URL
以下のサイトを大いに参考にさせて頂きました。ありがとうございます。