線形回帰を最尤法で解いてみる(Keras on TensorFlow)

## 記事の目的

線形回帰を最小二乗法で解いた結果と最尤法で解いた結果が一致することに触れた記事は色々あるが、それを理解してさらに実装できるようになるとどう応用できるのかイメージしにくいのではないかと思ったので。
線形回帰を最尤法で解く実装(Keras on TensorFlow)と応用例を簡単に示してみる。

## 実行環境

Python 3.6
TensorFlow 1.12
TensorFlow Probability 0.5

## 最小二乗法

![ols.png](https://qiita-image-store.s3.amazonaws.com/0/103085/45256d87-117a-0fc5-44dd-896eff377e05.png)

直線$y=f(x)$からデータ点$(x,y)$までの縦の長さ(赤い線)が最小になるように$f(x)$を決めるのが最小二乗法。  
正確には
$$\sum_{i=1}^{n}\left(y_{i}-f(x_{i})\right)^{2}$$
を最小にする。

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import figaspect

np.random.seed(1234)

x_min, x_max = -3, 3
coef = 2
sigma = 1


def f(x): return coef * x


x = np.arange(x_min + 1, x_max)
residual = np.random.normal(scale=sigma, size=x.size)
y = f(x) + residual

fig = plt.figure(figsize=figaspect(1))
ax = fig.gca()
ax.plot((x_min, x_max), (f(x_min), f(x_max)), c='k', label='$f(x)$')
ax.scatter(x, y, c='k', label=r'$(x,\ y)$')
ax.vlines(x, f(x), y, color='r', label='$error$')
ax.legend(loc='upper left')
plt.setp(ax, xlim=(x_min, x_max), xticks=(), yticks=())
plt.show()
```

## 最尤法

![likelihood.png](https://qiita-image-store.s3.amazonaws.com/0/103085/d0735f07-7000-6893-f31f-fa348f407398.png)

直線$y=f(x)$と正規分布(青い線)に従う誤差$\epsilon$を考える。データ点$(x,y)$が$f(x)+\epsilon$から再現される確率/確率密度$p(y\ |\ x)$を考える。この確率(密度)を尤度といい、尤度(赤い線の長さ)が最大になるように$f(x)$を決めるのが最尤法。

正確には
$$\prod_{i=1}^{n}p\left(y_{i}|x_{i}\right)$$
を最大にする。

```python
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
from matplotlib.figure import figaspect

np.random.seed(1234)

x_min, x_max = -3, 3
coef = 2
sigma = 1


def f(x): return coef * x


x = np.arange(x_min + 1, x_max)
y_hat = f(x)
residual = np.random.normal(scale=sigma, size=x.size)
y = y_hat + residual
error = np.linspace(- sigma * 3, sigma * 3, 100).reshape((-1, 1))
dist = norm(scale=sigma)

fig = plt.figure(figsize=figaspect(1))
ax = fig.gca()
ax.plot((x_min, x_max), (f(x_min), f(x_max)), c='k', label='$f(x)$')
ax.scatter(x, y, c='k', label=r'$(x,\ y)$')
lines = ax.plot(x + dist.pdf(error), y_hat + error, c='b')
plt.setp(lines[0], label=r'$\epsilon \sim \mathcal{N}( 0,\ \sigma )$')
ax.hlines(y, x, x + dist.pdf(residual), color='r', label=r'$p(y\ |\ x)$')
ax.legend(loc='upper left')
plt.setp(ax, xlim=(x_min, x_max), xticks=(), yticks=())
plt.show()
```

## コード

TensorFlow Probabilityをインストール。(なくても確率密度関数を自分で書くなら不要)

`$ pip install tensorflow-probability`

実際のコードは以下の通り。
初心者向けにKerasでやってみたけど、正直[ここ](http://kyleclo.github.io/maximum-likelihood-in-tensorflow-pt-1/)のように生TensorFlowのほうが楽かな。損失関数に予測値と正解ラベル以外渡せないので、ネットワーク内で負の対数尤度を計算して、損失関数はその値をそのまま利用。出力は予測用と損失関数用で2つ用意。
$\sigma$を正にするため、変数は$log\sigma$にして、指数関数を通す。

```python
import numpy as np
from sklearn.datasets import make_regression
import tensorflow.keras.backend as K
from tensorflow.keras import initializers
from tensorflow.keras.layers import Input, Dense, Layer
from tensorflow.keras.models import Model
import tensorflow_probability as tfp
tfd = tfp.distributions

x, y = make_regression(n_features=1, noise=10, random_state=1234)


class NegativeLogLikelihood(Layer):

    def __init__(self, output_dim, **kwargs):
        self.output_dim = output_dim
        super().__init__(**kwargs)

    def build(self, input_shape):
        self.log_sigma = self.add_weight(
            'log_sigma', shape=(1,), initializer=initializers.Zeros)
        self.sigma = K.exp(self.log_sigma)
        super().build(input_shape)

    def call(self, inputs):
        y_true = inputs[0]
        y_pred = inputs[1]
        dist = tfd.Normal(loc=y_pred, scale=self.sigma)
        return -1.0 * dist.log_prob(y_true)

    def compute_output_shape(self, input_shape):
        return (input_shape[0],)


def identity_loss(y_true, loss):
    return loss


input_x = Input(shape=(1,))
input_y = Input(shape=(1,))
out = Dense(1)(input_x)
loss = NegativeLogLikelihood(1)([input_y, out])
model = Model(inputs=[input_x, input_y], outputs=[out, loss])
model.compile(optimizer='adam', loss=identity_loss, loss_weights=[0., 1.])

model.fit([x, y.reshape((-1, 1))], [y, y], epochs=int(3e5), batch_size=y.size)
```

## 可視化

![result.png](https://qiita-image-store.s3.amazonaws.com/0/103085/6820c22a-e4c3-d1cf-1332-e870c4581427.png)

```python
import matplotlib.pyplot as plt
from matplotlib.figure import figaspect

coef, intercept = model.layers[1].get_weights()
coef = coef.ravel()
sigma = np.exp(model.layers[-1].get_weights()).ravel()


def f(x): return coef * x + intercept


fig = plt.figure(figsize=figaspect(1))
ax = fig.gca()
ax.scatter(x, y, c='k', alpha=0.3)
xlim = np.array(ax.get_xlim())
y_hat = f(xlim)
ax.plot(xlim, y_hat, c='k')
ax.fill_between(xlim, y_hat-sigma, y_hat+sigma,
                color='b', alpha=0.5, label=r'$\sigma$ range')
ax.fill_between(xlim, y_hat-3*sigma, y_hat+3*sigma,
                color='b', alpha=0.3, label=r'$3\sigma$ range')
ax.legend()
plt.setp(ax, xlim=xlim, xticks=(), yticks=())
plt.show()
```

## 応用例

- 正規分布の部分をt分布に変えて、裾の広い分布に対応したり、少しだけ外れ値に強いロバスト回帰
- RNNなどと組み合わせて[ガウス過程](https://scikit-learn.org/stable/auto_examples/gaussian_process/plot_gpr_noisy_targets.html#sphx-glr-auto-examples-gaussian-process-plot-gpr-noisy-targets-py)みたいに時系列データの区間推定(予測値だけでなく、予測の信頼性≒確信度)
 - RNNに分布のパラメータも予測させるので、たぶん実装はこっちのほうが楽