IF文を微分可能にする
ニューラルネットワークで決定木のようなモデルを作成したい。そのためには微分可能で学習可能なパラメータを持つIF文が必要。
IF文の構造
以下のような記述でstatement
がTrue
の場合にはtask_for_true
を実行し、False
の場合にはtask_for_false
を実行する。
if statement:
task_for_true
else:
task_for_false
今回はニューラルネットワークに組み込むのでtask
の部分は$ax+b$のような形になる。
問題はstatement
の部分をどうやってニューラルネットワークで表現するかである。
a > b
のニューラルネットワーク的表現
x = [0, 1, 2]
のようなNumPy配列を考える。x[x > 0]
とすると、[1, 2]
という結果が返ってくる。
このときx > 0
は[False, True, True]
というbool型の配列として評価され、それを元にTrue
に該当する位置の値が返ってきている。また、NumPyのbool型は内部的にはTrue = 1, False = 0
となっているので、x.sum() = 3
のような演算が可能。
以上のことから、x[x > 0]
はx
の各要素に
$
\displaystyle f( x) =\begin{cases}
1 & ( x >0)\
0 & ( x\leqq 0)
\end{cases}
$
という関数$f( x)$を適用していると見ることができる。
このままではこの関数は微分できないが、LSTMのゲートなどを参考にするとこの関数を近似して微分可能にする方法がわかる。単純にシグモイド関数通すだけだけど。
$
\displaystyle f( x) =\frac
{1}
{1+e^{-x}}
$
もう少し元の関数に近づけるためには$x$に大きな値の係数$s$をかける。
$
\displaystyle f( x) =\frac
{1}
{1+e^{-sx}}
$
さらに、閾値を$0$以外にするために$x$を$x-t$にする。
$
\displaystyle f( x) =\frac
{1}
{1+e^{-s( x-t)}}
$
以上より、x > t
という条件文は$
\displaystyle f( x) =\frac
{1}
{1+e^{-s( x-t)}}
$によって表現できることがわかる。$s$が正ならx > t
、負ならx < t
になる。
a == b
のニューラルネットワーク的表現
これもa > b
の場合のように各要素に同じ関数を適用していると考えていくが、元になる関数は
$
\displaystyle f( x) =\begin{cases}
1 & ( x=0)\
0 & ( x\neq 0)
\end{cases}
$
である。これを近似するには、画像のAttentionなどを参考に正規分布のような山型の関数を使うとよい。$0\sim 1$の値を返すこのような関数としてはtanhの微分でもいいのだが、数値計算的に安定するのでここではシグモイド関数の微分を使ってみる。
$\displaystyle f( x) =sigmoid( x)( 1-sigmoid( x))$
a > b
の場合と同様に変形すると、よりそれらしくなる。
$\displaystyle f( x) =sigmoid( s( x-t))( 1-sigmoid( s( x-t)))$
決定木をニューラルネットワークで表現する
処理の手順は以下のようになる。
- 条件文に適用する$x$を選ぶ
- 閾値より上の値と下の値に分割する
- それぞれの値に対して処理を適用する
変数を選択する
サンプル数$N$、特徴の数$K$のデータセット$X$から$k$列目の変数だけを選ぶ。
これは、a == b
のところで扱ったように、シグモイド関数の微分に$0, 1, \dots , K-1$を代入したベクトルを$X$にかけて、目的とする変数以外を$0$にする。
N = 10
K = 5
X = np.random.normal(size=(N, K))
sig = lambda x: 1 / (1 + np.exp(-x))
sig_diff = lambda x: 4 * sig(x) * (1 - sig(x))
x_shift = lambda x, s, t: s * (x - t)
selected_col = X * sig_diff(x_shift(np.arange(K), 100, 1))
ニューラルネットワークで学習させることを考えると、最初から$s$に(絶対値が)大きな値を設定すると勾配が$0$になってしまって学習できないと思うので、初期値としては小さい値でよさそう。
条件によって分割する
これは、a > b
のところで扱ったようにシグモイド関数を使用する。
データセット$X$で考えると面倒なので、抽出された列$k$を表すベクトル$x$で考える。Deep Learningのフレームワークで実装する場合にはサンプル数$N$のところは考えなくていいので、実際上の問題はない。
N = 10
x = np.random.normal(scale=10, size=N)
sig = lambda x: 1 / (1 + np.exp(-x))
x_shift = lambda x, s, t: s * (x - t)
condition = sig(x_shift(x, 10, 0.1))
xx = x.reshape((-1, 1)) * np.array([condition, 1 - condition]).T
これでxx[:, 0]
には条件がTrue
のもの、xx[:, 1]
には条件がFalse
ものが取り出される。ここでは$x$が適切にスケーリングされているなら、$s$は学習可能なパラメータでなく大きな値の定数でもよさそう。
それぞれの値に対して処理を適用する
$N\times K$だった入力データ$X$のshapeは、この時点で$N\times K\times 2$になっている。
このうち、前段階で条件に合致した値を$x_{T}$、合致しなかった値を$x_{F}$とすると、
$
\displaystyle y=\begin{cases}
w_{T} x_{T} +b_{T} & \
w_{F} x_{F} +b_{F} &
\end{cases}
$
のような形にしたい。単純にそれぞれ$w$をかけて$b$を足して、True
の場合とFalse
の場合を合わせてしまうとどちらも同じ$b$を適用するのと変わらないので、$b$にも条件文を適用しておく。
N = 10
x = np.random.normal(scale=10, size=N)
sig = lambda x: 1 / (1 + np.exp(-x))
x_shift = lambda x, s, t: s * (x - t)
true = sig(x_shift(x, 10, 0.1))
condition = np.array([true, 1 - true]).T
xx = x.reshape((-1, 1)) * condition
w = np.random.normal(size=(2, 1))
b = np.random.normal(size=2)
bb = b * condition
y = xx.dot(w) + bb
実装
そのうちやる。