ニューラルネットワークで決定木のようなモデルを作成する

## IF文を微分可能にする

ニューラルネットワークで決定木のようなモデルを作成したい。そのためには微分可能で学習可能なパラメータを持つIF文が必要。

## IF文の構造

以下のような記述で`statement`が`True`の場合には`task_for_true`を実行し、`False`の場合には`task_for_false`を実行する。

```python
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)$を適用していると見ることができる。

[![ステップ関数](https://qiita-image-store.s3.amazonaws.com/0/103085/3042104f-41a7-3265-fb33-e61729e92b54.png)](https://gist.github.com/ScenesK/74b8716cb6c8dc0b9025e3745144d2d8)

このままではこの関数は微分できないが、LSTMのゲートなどを参考にするとこの関数を近似して微分可能にする方法がわかる。単純にシグモイド関数通すだけだけど。

$
\displaystyle f( x) =\frac
    {1}
    {1+e^{-x}}
$

[![シグモイド関数](https://qiita-image-store.s3.amazonaws.com/0/103085/b2962fed-7d78-70e5-980f-e03f62304f66.png)](https://gist.github.com/ScenesK/11d7973ce90cf7239912bc8facc4511f)

もう少し元の関数に近づけるためには$x$に大きな値の係数$s$をかける。

$
\displaystyle f( x) =\frac
    {1}
    {1+e^{-sx}}
$

[![係数のあるシグモイド関数](https://qiita-image-store.s3.amazonaws.com/0/103085/bf37bcb7-9ce1-413e-5970-778e25dc743b.png)](https://gist.github.com/ScenesK/4843fd19c31b8d41970e1d028fbe1203)

さらに、閾値を$0$以外にするために$x$を$x-t$にする。

$
\displaystyle f( x) =\frac
    {1}
    {1+e^{-s( x-t)}}
$

[![平行移動したシグモイド関数](https://qiita-image-store.s3.amazonaws.com/0/103085/74fbf530-9541-bd9d-5b3c-e6b16cbed8fe.png)](https://gist.github.com/ScenesK/a7623610cf9af9a9837703f0c5b9a705)

以上より、`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))$

[![シグモイド関数の微分](https://qiita-image-store.s3.amazonaws.com/0/103085/345cd7b5-ed76-b182-ee63-7cbd9b85d172.png)](https://gist.github.com/ScenesK/47fcfe700bef1561b16c234781452bb1)

`a > b`の場合と同様に変形すると、よりそれらしくなる。

$\displaystyle f( x) =sigmoid( s( x-t))( 1-sigmoid( s( x-t)))$

[![平行移動したシグモイド関数の微分](https://qiita-image-store.s3.amazonaws.com/0/103085/fce8cfb8-f6d4-c146-d073-9a1884b63cc8.png)](https://gist.github.com/ScenesK/103f01f8e40a49315a2c411df0fb7c03)

## 決定木をニューラルネットワークで表現する

処理の手順は以下のようになる。

1. 条件文に適用する$x$を選ぶ
1. 閾値より上の値と下の値に分割する
1. それぞれの値に対して処理を適用する

### 変数を選択する

サンプル数$N$、特徴の数$K$のデータセット$X$から$k$列目の変数だけを選ぶ。

これは、`a == b`のところで扱ったように、シグモイド関数の微分に$0, 1, \dots , K-1$を代入したベクトルを$X$にかけて、目的とする変数以外を$0$にする。

```python
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$のところは考えなくていいので、実際上の問題はない。

```python
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$にも条件文を適用しておく。

```python
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
```

## 実装

そのうちやる。