chainerをscikit-learn likeに使えるようにした

# モチベーション
DeepLearningを簡単に書けることで最近話題のChainerだけど、いまいちAPIがまだ洗練されていない気がしたのでscikit-learn likeに使えるようにしてみた。

成果物 -> https://github.com/lucidfrontier45/scikit-chainer , https://pypi.python.org/pypi/scikit-chainer

# 方針
scikit-learnと同じように`model.fit(X, y)`や`model.predict(X)`, `model.score(X, y)`のように使えるようにする。

#実装

そんなに長くないので全部貼っつける。
まずは基底クラス。`sklearn.base.BaseEstimator`に相当します。

```py3
from abc import ABCMeta, abstractmethod
from chainer import FunctionSet, Variable, optimizers
from chainer import functions as F
from sklearn import base


class BaseChainerEstimator(base.BaseEstimator, metaclass=ABCMeta):
    def __init__(self, optimizer=optimizers.SGD(), n_iter=10000, eps=1e-5, report=100,
                 **params):
        self.network = self._setup_network(**params)
        self.optimizer = optimizer
        self.optimizer.setup(self.network.collect_parameters())
        self.n_iter = n_iter
        self.eps = eps
        self.report = report

    @abstractmethod
    def _setup_network(self, **params):
        return FunctionSet(l1=F.Linear(1, 1))

    @abstractmethod
    def forward(self, x):
        y = self.network.l1(x)
        return y

    @abstractmethod
    def loss_func(self, y, t):
        return F.mean_squared_error(y, t)

    @abstractmethod
    def output_func(self, h):
        return F.identity(h)

    def fit(self, x_data, y_data):
        score = 1e100
        x = Variable(x_data)
        t = Variable(y_data)
        for i in range(self.n_iter):
            self.optimizer.zero_grads()
            loss = self.loss_func(self.forward(x), t)
            loss.backward()
            self.optimizer.update()
            d_score = score - loss.data
            score = loss.data
            if d_score < self.eps:
                print(i, loss.data, d_score)
                break
            if self.report > 0 and i % self.report == 0:
                print(i, loss.data, d_score)
        return self

    def predict(self, x_data):
        x = Variable(x_data)
        y = self.forward(x)
        return self.output_func(y).data
```

で、これを継承して回帰と分類の基底クラスを作ります。
基本的には`sklearn.base.RegressionMixin`や`sklearn.base.ClassifierMixin`と混ぜるだけ。

```py3
class ChainerRegresser(BaseChainerEstimator, base.RegressorMixin):
    pass


class ChainerClassifier(BaseChainerEstimator, base.ClassifierMixin):
    def predict(self, x_data):
        return BaseChainerEstimator.predict(self, x_data).argmax(1)
```

# 使用例
LogisticRegressionを試しに実装してみます。

```py3
class LogisticRegression(ChainerClassifier):
    def _setup_network(self, **params):
        return FunctionSet(l1=F.Linear(params["n_dim"], params["n_class"]))

    def forward(self, x):
        y = self.network.l1(x)
        return y

    def loss_func(self, y, t):
        return F.softmax_cross_entropy(y, t)

    def output_func(self, h):
        return F.softmax(h)
```

テストしてみます

```py3
import numpy as np
from skchainer import linear
from scipy import special

x = np.linspace(-10, 10, 10000).astype(np.float32)
p = special.expit(x)
y = np.random.binomial(1, p).astype(np.int32)
x = x.reshape(len(x), 1)
model = linear.LogisticRegression(n_dim=1, n_class=2, report=100).fit(x, y)
print(model.score(x, y))
```

ちゃんと収束して分類できていることが確認できました。

```
0 3.128652334213257 1e+100
100 0.179422065615654 0.000214472
200 0.16965821385383606 4.20064e-05
300 0.1672072559595108 1.36197e-05
332 0.16683495044708252 9.96888e-06
0.9284
```

(追伸)

Multi-Layer Perceptronも作ってみました。

```py
class MultiLayerPerceptron(ChainerClassifier):
    def _setup_network(self, **params):
        print(params)
        network = FunctionSet(
            l1=F.Linear(params["input_dim"], params["hidden_dim"]),
            l2=F.Linear(params["hidden_dim"], params["n_classes"])
        )
 
        return network
 
    def forward(self, x):
        h = F.relu(self.network.l1(x))
        y = self.network.l2(h)
        return y
 
    def loss_func(self, y, t):
        return F.softmax_cross_entropy(y, t)
 
    def output_func(self, h):
        return F.softmax(h)
 
 
if __name__ == "__main__":
    import numpy as np
    from sklearn import datasets
 
    iris = datasets.load_iris()
    x = iris.data.astype(np.float32)
    y = iris.target.astype(np.int32)
 
    input_dim = x.shape[1]
    n_classes = len(set(y))
 
    model = MultiLayerPerceptron(optimizer=optimizers.AdaDelta(rho=0.5),
        input_dim=input_dim, hidden_dim=10, n_classes=n_classes, report=100).fit(x, y)
    print(model.score(x, y))
```