Chainerを使ってTweetの感情分析をしてみる(第一回)


## 修正履歴
**2016/7/18 : filter後のサイズの計算式に間違いがあったので修正。**


## 概要
近年様々なところでAIとか深層学習(Deep Learning)等々が騒がれてきています。
いろいろとライブラリも増えてきて試しやすい雰囲気になってきたかと思います。
そこで、[Chainer](http://chainer.org)を使ってTweetの感情分析をしてみようと思います。
[Theanoでの記事](http://qiita.com/hogefugabar/items/93fcb2bc27d7b268cbe6)を参照しつつ似たようなことをChainerでやってみようということです。

ただ、これまでsklearnなどの機械学習ライブラリに慣れてきている人は、少し使いにくいので、その辺も合わせて解決できれば。

結構私の独自理解が入っているので、間違いがあるやもしれませんが、そのへんは指摘いただくとありがたいです。

## 第一回の内容

* sklearnとChainerの違い
* ChainerでのMLP実装のサンプル
* ChainerでのCNNを理解する

## 環境

Mac OSX Yosemite 10.10.15
Python 2.7
CPU Intel Core i5 2.6GHz
メモリ 8GB

(そんな装備で大丈夫か？→わからない)

## 準備

### Chainerのインストール

```bash
pip install chainer
```

## sklearnとChainer

sklearnでは

```python
model = (SVMとかRandomForestとか)
model.fit(x_train,y_train)
y_p = model.predict(x_test)
```

とかで、簡単にできたわけです。

ここで、x_trainは大きさ$ N × M $の行列、y_trainは長さ$ N $の教師ベクトル(0,1とか)になります。$N$はサンプルサイズ、$M$は特徴量の数です。x_testはx_trainと列数の同じ(つまり特徴量の大きさが同じ)テストデータになります。

一方Chainerでは、このように「fit」「predict」みたいなメソッドがあるわけではなく、自分で作るしかありません。

例えば多層パーセプトロン(MLP)では以下のように[実装](http://qiita.com/lucidfrontier45/items/0568d0d9e2c125e72734)するようです。

Baseクラスを以下のように、

```python
# -*- coding: utf-8 -*-

from chainer import FunctionSet, Variable, optimizers
from chainer import functions as F
from sklearn import base
from abc import ABCMeta, abstractmethod
import numpy as np
import six


class BaseChainerEstimator(base.BaseEstimator):
    __metaclass__= ABCMeta  # python 2.x
    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, train=True):
        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):
        batchsize = 100
        N = len(y_data)
        for loop in range(self.n_iter):
            perm = np.random.permutation(N)
            sum_accuracy = 0
            sum_loss = 0
            for i in six.moves.range(0, N, batchsize):
                x_batch = x_data[perm[i:i + batchsize]]
                y_batch = y_data[perm[i:i + batchsize]]
                x = Variable(x_batch)
                y = Variable(y_batch)
                self.optimizer.zero_grads()
                yp = self.forward(x)
                loss = self.loss_func(yp,y)
                loss.backward()
                self.optimizer.update()
                sum_loss += loss.data * len(y_batch)
                sum_accuracy += F.accuracy(yp,y).data * len(y_batch)
            if self.report > 0 and loop % self.report == 0:
                print('loop={}, train mean loss={} , train mean accuracy={}'.format(loop, sum_loss / N,sum_accuracy / N))

        return self

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

class ChainerClassifier(BaseChainerEstimator, base.ClassifierMixin):
    def predict(self, x_data):
        return BaseChainerEstimator.predict(self, x_data).argmax(1) #argmaxは行列の行の中で最大になるインデックスを返す。つまりクラスは0から1,2としていかないといけない

    def predict_proba(self,x_data):
        return BaseChainerEstimator.predict(self, x_data)

```

その上で、MLPのクラスをChainerClassifierを継承する形で、


```python
class MLP3L(ChainerClassifier):
    """
    3-Layer Perceptron
    """
    def _setup_network(self, **params):
        network = FunctionSet(
            l1=F.Linear(params["input_dim"], params["hidden_dim"]),
            l2=F.Linear(params["hidden_dim"], params["hidden_dim"]),
            l3=F.Linear(params["hidden_dim"], params["n_classes"]),
        )
        return network

    def forward(self, x, train=True):
        h1 = F.dropout(F.relu(self.network.l1(x)),train=train)
        h2 = F.dropout(F.relu(self.network.l2(h1)),train=train)
        y = self.network.l3(h2)
        return y

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

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

と実装します。

これでsklearnと同じように「fit」「predict (predict_proba)」が使えます。

x_dataはnumpy.float32型、y_dataはnumpy.int32型でないといけないようです。
(fit内部でChainerのVariableにキャストしています)


さて、上記のMLPであれば上記のx_dataはsklearnと同じように大きさ$ N × M $の行列でいいです。が、これを例えば畳み込みニューラルネットワーク(CNN)とかに拡張しようとすると、いきなり問題が生じます。

CNNは画像処理で主に用いられるので、インプットが２次元になっており、これにバッチサイズ(サンプルサイズ)を加えると、３次元のx_dataにしないといけません。(チャネル?という概念があって、実際は４次元テンソルになる)

## ChainerでのCNNサンプルを解読する。

サンプルとして[こちら](http://aidiary.hatenablog.com/entry/20151007/1444223445)のコードを使わせていただきました。

使用しているMNISTの画像は$28 × 28$です。

```python
model = chainer.FunctionSet(conv1=F.Convolution2D(1, 20, 5),
								conv2=F.Convolution2D(20, 50, 5),  
                            l1=F.Linear(800, 500),
                            l2=F.Linear(500, 10))
                            
def forward(x_data, y_data, train=True):
    x, t = chainer.Variable(x_data), chainer.Variable(y_data)
    h = F.max_pooling_2d(F.relu(model.conv1(x)), 2)
    h = F.max_pooling_2d(F.relu(model.conv2(h)), 2)
    h = F.dropout(F.relu(model.l1(h)), train=train)
    y = model.l2(h)
    if train:
        return F.softmax_cross_entropy(y, t)
    else:
        return F.accuracy(y, t)                                                        

```

F.Convolution2Dのリファレンスをみてみると、

![Kobito.SCEMO4.png](https://qiita-image-store.s3.amazonaws.com/0/54539/91a4f44c-487f-0e2b-c80e-efa6b517d7c2.png "Kobito.SCEMO4.png")

となっており、第1引数にin_channels、第2引数にout_channels、第3引数にksize(Filterサイズ)を入れるようになっています。in_channelsはRGBとかで3とかにするらしいですが、1で試すとしていて、out_channelsは出力チャネル数ですが、たぶん、フィルターの違いで20種類の画像を作るってことかな？と勝手に理解。ksizeは5になっていますので、フィルターが$5×5$のフィルターということになります。

### 畳み込みとプーリング処理を行った後の特徴量のサイズ

(2016/7/18修正 ここから)

~~畳み込み処理では、フィルターサイズを$F$として、画像サイズを$S×S$とすると、フィルター後の画像サイズはパディングとかを入れない場合特徴マップサイズを$S_f × S_f$として、[ここの記事](http://aidiary.hatenablog.com/entry/20151108/1446952402)によると~~

```math
S_f = S - 2 × [F/2]
```

~~となります。$[]$は小数点以下切捨てです。~~

**どうやら、試してみると違うようで、というかChainerのDocumentにも書いてあった。**

```math
S_f = S - F + 1
```

でいい。移動平均と同じですよねーそうですよねー。
以前の式は、フィルターサイズが奇数だとうまくいくけど、偶数だとだめ。

あと、プーリング処理で、Maxプーリングを使う場合と、Averageプーリングを使う場合で端っこの処理が違ってくる。
試した感じ、Averageプーリングでは、プーリングサイズで対象のサイズを割った余りが出る場合は計算できないが、Maxプーリングでは計算してしまう。
そのため、その辺を注意しないといけない。

(2016/7/18修正 ここまで)

つまり、今回の例でみると、

畳み込み1回目で

```math
S_{f1} = 28 - 2 × [5/2] = 24
```

で、forward関数の中で、Maxプーリングを行っているので、プーリング後のサイズを$S_{p1} × S_{p1}$として、

```math
S_{p1} = 24 / 2 = 12
```

で、畳み込み2回目で

```math
S_{f2} = 12 - 2 × [5/2] = 8
```

で、forward関数の中で、Maxプーリングを行っているので、プーリング後のサイズを$S_{p2} × S_{p2}$として、

```math
S_{p2} = 8 / 2 = 4
```

となります。

つまり最終的なインプットとなる特徴量の次元は、出力枚数が50となっているので、

```math
M = 50 × 4 × 4 = 800
```

となり、第1層の

```python
l1=F.Linear(800, 500)
```

の第1引数と合います。(Chainerでは間違えていると正解を教えてくれるらしいです)

### forwardに投げる前の準備

さて、モデルを定義したのち、forward関数にx_dataを投げるわけですが、問題はまだあって、Convolutionを行う場合は、以下のリファレンスから4次元テンソルを投げないといけません。(Parametersのxを参照)

![Kobito.sKeAcV.png](https://qiita-image-store.s3.amazonaws.com/0/54539/166d6c10-9ddb-df2b-220f-107bca215c7e.png "Kobito.sKeAcV.png")

$n$はバッチサイズ(サンプルサイズ)、$c_I$はチャネル数、$h$と$w$はそれぞれ画像の縦横サイズです。

上記サンプルコードをみてみると、以下のようにreshapeを使って4次元テンソルに変換しています。

```python
X_train = X_train.reshape((len(X_train), 1, 28, 28))
```

今回、Variable型の状態からreshapeしたいなぁと思って調べてみるとChainerの関数として同じ物が定義されています。

![Kobito.StweJ1.png](https://qiita-image-store.s3.amazonaws.com/0/54539/3b19cbbb-2997-4d7b-0abc-6744a3c07bff.png "Kobito.StweJ1.png")

これを使います。

## 次回

* EmbedIDの特性を調べる
* 自然言語処理での畳み込みニューラルネットワーク