Chainerでフィードフォワードニューラルネットワークを実装して文書分類する

#はじめに
最近何かと話題のChainerを使って文書のポジネガを判定する2値分類器を実装してみました。
初めてChainerを使ったので、練習用ということで簡単なモデルとなっています。
筆者のようにChainerでディープニューラルネットを実装して何かしてみたいという方向けです。

間違い等はコメント欄で指摘していただけると助かります。

全コードは[こちら](https://github.com/ichiroex/chainer-ffnn)からご参照下さい。

#事前準備
 - chainer, gensim, scikit-learnのインストール

#環境
 - Python 2.7系
 - Chainer 1.6.2.1

#使用するデータの例
使用したデータは、英語の何かのレビューに関する文書です。
各行が一文書に対応しており、文書中の各単語は半角スペースで区切られています。
各行の先頭の数字(e.g. 1, 0)は、ラベルです。

＊データセットは各自用意して下さい。
＊日本語の文書を使用する場合は、MeCabなどで分かち書きを行って下さい。

> 0 each scene drags , underscoring the obvious , and sentiment is slathered on top .
> 0 afraid to pitch into farce , yet only half-hearted in its spy mechanics , all the queen's men is finally just one long drag .
> 1 clooney directs this film always keeping the balance between the fantastic and the believable . . .
> 1 just about the best straight-up , old-school horror film of the last 15 years .

#文書のベクトル化
各文書をニューラルネットワークの入力として扱うために、bag-of-wordsでベクトル化を行います。
ベクトル化には、gensimの関数を利用しました。
詳しくはこちらの記事を参考にして下さい。→  [scikit-learnとgensimでニュース記事を分類する](http://qiita.com/yasunori/items/31a23eb259482e4824e2)

関数`load_data`では、入力データを読み込み、各文書のラベルと単語列を`l.strip().split(" ", 1)`で分割しています。
`target`には文書ラベル、`source`には文書ベクトルをそれぞれ格納し、`dataset`にまとめて戻り値として返します。

`corpora.Dictionary(document_list)`では、各単語を要素とする文書リストのリスト(`document_list`)を渡すことで、単語辞書を作成しています。
本来なら、訓練データのみを使って単語辞書を作成しないといけないのですが、未知語処理を省きたかったので全文書を使って単語辞書を作成しています。

ここで`vocab_size`は、全文書の語彙数であり、文書ベクトルの次元数に対応します。
そのため、今回実装したニューラルネットの入力層のユニット数は、`vocab_size`と等しいです。

```
def load_data(fname):
    source = []
    target = []
    f = open(fname, "r")

    document_list = [] #各行に一文書. 文書内の要素は単語
    for l in f.readlines():
        sample = l.strip().split(" ", 1)        #ラベルと単語列を分ける
        label = int(sample[0])                  #ラベル
        target.append(label)
        document_list.append(sample[1].split()) #単語分割して文書リストに追加

    #単語辞書を作成
    dictionary = corpora.Dictionary(document_list)
    dictionary.filter_extremes(no_below=5, no_above=0.8)
    # no_below: 使われている文書がno_below個以下の単語を無視
    # no_above: 使われてる文章の割合がno_above以上の場合無視

    #文書のベクトル化
    for document in document_list:
        tmp = dictionary.doc2bow(document) #文書をBoW表現
        vec = list(matutils.corpus2dense([tmp], num_terms=len(dictionary)).T[0])
        source.append(vec)

    dataset = {}
    dataset['target'] = np.array(target)
    dataset['source'] = np.array(source)
    print "vocab size:", len(dictionary.items()) #語彙数 = 入力層のユニット数

    return dataset, dictionary
```

#モデルの定義
今回は練習用ということで、簡単なモデルを実装しました。
（さきほどの関数`load_data`から受け取った`dataset`をscikit-learnに入っている関数`train_test_split`を利用して、訓練データとテストデータに分割しています。）

入力層のユニット数`in_units`は、文書ベクトルの次元数(`x_train.shape[1] `)を入れています。
隠れ層(中間層)は、適当に設定して構いません。今回は、デフォルトで500を渡すようにしています。
出力層は、ソフトマックス関数を使うので、ユニット数はラベルのタイプ数である2としています。

```
x_train, x_test, y_train, y_test = train_test_split(dataset['source'], dataset['target'], test_size=0.15)
N_test = y_test.size         # test data size
N = len(x_train)             # train data size
in_units = x_train.shape[1]  # 入力層のユニット数 (語彙数)

n_units = args.units # 隠れ層のユニット数
n_label = 2          # 出力層のユニット数

#モデルの定義
model = chainer.Chain(l1=L.Linear(in_units, n_units),
                      l2=L.Linear(n_units, n_units),
                      l3=L.Linear(n_units,  n_label))
```

#順伝搬
関数`forward`では順伝搬を行います。
入力層->隠れ層、隠れ層->隠れ層の活性化関数には、シグモイド関数を使用しました。

```
def forward(x, t, train=True):
    h1 = F.sigmoid(model.l1(x))
    h2 = F.sigmoid(model.l2(h1))
    y = model.l3(h2)
    return F.softmax_cross_entropy(y, t), F.accuracy(y, t)
```

#学習
全体の流れとしては、
1. 訓練データからバッチを作成
2. 順伝搬
3. 誤差逆伝播
4. パラメータ更新
という感じです。（あまり自信ない…）

各epochで、訓練データに対する誤差・テストデータに対する誤差を計算しています。
また、分類問題なので分類正解率`accuracy`もそれぞれ計算します。

```
# Setup optimizer
optimizer = optimizers.Adam()
optimizer.setup(model)

# Learning loop
for epoch in six.moves.range(1, n_epoch + 1):

    print 'epoch', epoch

    # training
    perm = np.random.permutation(N) #ランダムな整数列リストを取得
    sum_train_loss     = 0.0
    sum_train_accuracy = 0.0
    for i in six.moves.range(0, N, batchsize):

        #perm を使い x_train, y_trainからデータセットを選択 (毎回対象となるデータは異なる)
        x = chainer.Variable(xp.asarray(x_train[perm[i:i + batchsize]])) #source
        t = chainer.Variable(xp.asarray(y_train[perm[i:i + batchsize]])) #target

        model.zerograds()            # 勾配をゼロ初期化
        loss, acc = forward(x, t)    # 順伝搬
        sum_train_loss      += float(cuda.to_cpu(loss.data)) * len(t)   # 平均誤差計算用
        sum_train_accuracy  += float(cuda.to_cpu(acc.data )) * len(t)   # 平均正解率計算用
        loss.backward()              # 誤差逆伝播
        optimizer.update()           # 最適化

    print('train mean loss={}, accuracy={}'.format(
        sum_train_loss / N, sum_train_accuracy / N)) #平均誤差


    # evaluation
    sum_test_loss     = 0.0
    sum_test_accuracy = 0.0
    for i in six.moves.range(0, N_test, batchsize):

        # all test data
        x = chainer.Variable(xp.asarray(x_test[i:i + batchsize]))
        t = chainer.Variable(xp.asarray(y_test[i:i + batchsize]))

        loss, acc = forward(x, t, train=False)

        sum_test_loss     += float(cuda.to_cpu(loss.data)) * len(t)
        sum_test_accuracy += float(cuda.to_cpu(acc.data))  * len(t)

    print(' test mean loss={}, accuracy={}'.format(
        sum_test_loss / N_test, sum_test_accuracy / N_test)) #平均誤差

#modelとoptimizerを保存
print 'save the model'
serializers.save_npz('pn_classifier_ffnn.model', model)
print 'save the optimizer'
serializers.save_npz('pn_classifier_ffnn.state', optimizer)
```

#実行結果
最終的なテストデータに関する分類正解率は`accuracy=0.716875001788`となりました。
しかし、学習が進むにつれてテスト誤差が増加しており、過学習が起きています...

おそらくてきとうにモデルを組んだことが原因でしょう。

```
>python train.py --gpu 1 --data input.dat --units 1000
vocab size: 4442
epoch 1
train mean loss=0.746377664579, accuracy=0.554684912523
 test mean loss=0.622971419245, accuracy=0.706875003874
epoch 2
train mean loss=0.50845754933, accuracy=0.759408453399
 test mean loss=0.503996372223, accuracy=0.761249992996
epoch 3
train mean loss=0.386604680468, accuracy=0.826067760105
 test mean loss=0.506066314876, accuracy=0.769374992698
epoch 4
train mean loss=0.301527346359, accuracy=0.870433726909
 test mean loss=0.553729468957, accuracy=0.774999994785
epoch 5
train mean loss=0.264981631757, accuracy=0.889085094432
 test mean loss=0.599407823756, accuracy=0.766874998808
epoch 6
train mean loss=0.231274759588, accuracy=0.901114668847
 test mean loss=0.68350501731, accuracy=0.755625002086

...

epoch 95
train mean loss=0.0158744356008, accuracy=0.993598945303
 test mean loss=5.08019682765, accuracy=0.717499997467
epoch 96
train mean loss=0.0149783944279, accuracy=0.994261124581
 test mean loss=5.30629962683, accuracy=0.723749995232
epoch 97
train mean loss=0.00772037562047, accuracy=0.997351288256
 test mean loss=5.49559159577, accuracy=0.720624998212
epoch 98
train mean loss=0.00569957431572, accuracy=0.99834455516
 test mean loss=5.67661693692, accuracy=0.716875001788
epoch 99
train mean loss=0.00772406136085, accuracy=0.997240925267
 test mean loss=5.63734056056, accuracy=0.720000002533
epoch 100
train mean loss=0.0125463016702, accuracy=0.995916569395
 test mean loss=5.23713605106, accuracy=0.716875001788
save the model
save the optimizer

```

#おわりに
Chainerを使って、文書分類のためのフィードフォワードニューラルネットワークを実装しました。
過学習が起きないように、モデルの改善を行いたいと思います。

Chainerの勉強用にコードを見てみたい方は、[こちら](https://github.com/ichiroex/chainer-ffnn)からご参照下さい。

#参考ページ
 - [深層学習でアニメ顔を分類する](http://qiita.com/hogefugabar/items/312707a09d29632e7288)
 - [Deep Learning のフレームワーク Chainer を使った画像分類 その4](http://humansandcomputers.blogspot.jp/2015/08/auto-image-classification-w-deep-learning-framework-chainer-4.html)
 - [Chainerで回帰問題をした](http://qiita.com/nzw0301/items/dbeb55bcd00b56b7fc87)