Chainer を用いたリカレントニューラル言語モデル作成のサンプルコード解説に挑戦してみた

#リカレントニューラルネットワーク言語モデル
リカレントニューラルネットワーク言語モデル（ニューラル言語モデル）についてChainerのコードを使って解説してみました。

![スライド1.JPG](https://qiita-image-store.s3.amazonaws.com/0/10496/080447a3-8fd5-9cb3-8e33-5107c4f0ac5e.jpeg)

##GPUについて

言語処理はすごく時間がかかるのでGPU設定をおススメしています。
しかし一概に使えば良いということではなく下記のような設定では有効に働きます。
詳細な仕組みの中身を知りたい方は下記をご覧ください。
http://www.kumikomi.net/archives/2008/06/12gpu1.php?page=1

＊得意
行列計算
メモリにシーケンシャルにアクセスし、かつ条件分岐の無い計算（演算密度の高い処理）に強い。

＊苦手
二分探索
メモリにランダムアクセスし、かつ条件分岐が多い。

##GPUドライバ設定

AWSでのGPU設定は下記のサイトを参考に行いました。

http://tleyden.github.io/blog/2014/10/25/cuda-6-dot-5-on-aws-gpu-instance-running-ubuntu-14-dot-04/

```shell-session
apt-get update && apt-get install build-essential
```

Cudaのインストーラーを取得

```shell-session
wget http://developer.download.nvidia.com/compute/cuda/6_5/rel/installers/cuda_6.5.14_linux_64.run
```

Cudaのインストーラーのみ取得

```shell-session
chmod +x cuda_6.5.14_linux_64.run
mkdir nvidia_installers
./cuda_6.5.14_linux_64.run -extract=`pwd`/nvidia_installers
```

image-extractを取得

```shell-session
sudo apt-get install linux-image-extra-virtual
```

再起動

```shell-session
reboot
```

ファイルを作成

```shell-session
vi /etc/modprobe.d/blacklist-nouveau.conf
```

nouveauとlbm-nouveauの起動しないように設定

```
blacklist nouveau
blacklist lbm-nouveau
options nouveau modeset=0
alias nouveau off
alias lbm-nouveau off
```

Kernel Nouveauを起動しないように設定

```
 echo options nouveau modeset=0 | sudo tee -a /etc/modprobe.d/nouveau-kms.conf
```

カーネル起動時にあらかじめメモリに展開することでファイルシステムを構成する設定をしてから再起動

```
update-initramfs -u
reboot
```
カーネルのソースを取得

```shell-session
apt-get install linux-source
apt-get install linux-headers-3.13.0-37-generic
```

NVIDIAのドライバをインストール

```shell-session
cd nvidia_installers
./NVIDIA-Linux-x86_64-340.29.run
```

下記のエラー対応

```
NVIDIA driver install - Error: Unable to find the kernel source tree
```

```
sudo apt-get install linux-headers-`uname -r`
```

下記のコマンドでドライバがインストールされたかを確認する。

```shell-session
nvidia-smi
Wed Aug  5 07:48:36 2015
+------------------------------------------------------+
| NVIDIA-SMI 340.29     Driver Version: 340.29         |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GRID K520           Off  | 0000:00:03.0     Off |                  N/A |
| N/A   54C    P0    80W / 125W |    391MiB /  4095MiB |     99%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Compute processes:                                               GPU Memory |
|  GPU       PID  Process name                                     Usage      |
|=============================================================================|
|    0      8013  python                                               378MiB |
+-----------------------------------------------------------------------------+
```

上記のGPUに振られている番号がGPUのIDである。これは後でChainerを動作させるときに使用する。


##Pythonの設定

Python3の設定を行いました。

下記のコマンドを実行して事前に必要なものをインストールしておく

```shell-session

apt-get update
apt-get install gcc gcc++ kmod perl python-dev
sudo reboot
```

pip導入手順
https://pip.pypa.io/en/stable/installing.html

Pyenv導入手順
https://github.com/yyuu/pyenv

```shell-session

pip install virtualenv

pyenv install 3.4

virtualenv my_env -p = ~/.pyenv/versions/3.4.0/bin/python3.4

```

requirement.txtの設定を行いました。

```shell-session
numpy
scikit-learn
Mako
six
chainer
scikit-cuda
```

必要なライブラリをインストール

```shell-session
pip install -r requirement.txt
```

下記から"install-headers"をダウンロードしてくる。

https://android.googlesource.com/toolchain/python/+/47a24ea6662f20c8e165d541ab6facdf009bfee4/Python-2.7.5/Lib/distutils/command/install_headers.py

PyCudaのインストール

```shell-session
wget https://pypi.python.org/packages/source/p/pycuda/pycuda-2015.1.2.tar.gz
tar zxvf pycuda-2015.1.2.tar.gz
cd pycuda-2015.1.2
./configure.py
make
make install
```

##Chainer リカレントニューラル言語モデル　サンプルコード

コード構成図

![スライド2.JPG](https://qiita-image-store.s3.amazonaws.com/0/10496/c7dfebdc-9b43-dc7a-6d2c-d1415ab845a5.jpeg)

###各種ライブラリをインポート

```py

#!/usr/bin/env python
"""Sample script of recurrent neural network language model.

This code is ported from following implementation written in Torch.
https://github.com/tomsercu/lstm

"""
import argparse
import math
import sys
import time

import numpy as np
import six

import chainer
from chainer import cuda
import chainer.functions as F
from chainer import optimizers

````

###初期設定

・引数でGPUを使用するかどうかを判断しています。
・GPUが指定されればcudaを実行するように設定されています。
・学習回数、ユニット数、確率的勾配法に使用するデータの数、学習に使用する文字列の長さ、勾配法で使用する敷居値を設定しています。

```py

parser = argparse.ArgumentParser()
parser.add_argument('--gpu', '-g', default=-1, type=int,
                    help='GPU ID (negative value indicates CPU)')
args = parser.parse_args()
mod = cuda if args.gpu >= 0 else np

n_epoch = 39   # number of epochs
n_units = 650  # number of units per layer
batchsize = 20   # minibatch size
bprop_len = 35   # length of truncated BPTT
grad_clip = 5    # gradient norm threshold to clip

```

###データ保持

予めdownload.pyでダウンロードしたファイルをプログラムに読ませる処理を関数化しています。

・最終文字を改行から\<eos>に変更しています。\<eos>は最終文字を表す明示的な物なので改行コードを置き換えて明示的に最終文字にしています。
・文字データを確保するための行列を定義しています。
・データを単語をキー、長さを値とした辞書データにして行列データセットに登録しています。

学習データ
validデータ
テストデータ

上記をそれぞれ行列データとして保持しています。

```py
# Prepare dataset (preliminary download dataset by ./download.py)
vocab = {}


def load_data(filename):
    global vocab, n_vocab
    words = open(filename).read().replace('\n', '<eos>').strip().split()
    dataset = np.ndarray((len(words),), dtype=np.int32)
    for i, word in enumerate(words):
        if word not in vocab:
            vocab[word] = len(vocab)
        dataset[i] = vocab[word]
    return dataset

train_data = load_data('ptb.train.txt')
valid_data = load_data('ptb.valid.txt')
test_data = load_data('ptb.test.txt')
print('#vocab =', len(vocab))

```
###リカレントニューラル言語モデル

RNNLM(リカレントニューラル言語モデルの設定を行っています）
・辞書データを、入力ユニット数分のデータに変換する処理（潜在ベクトル空間への変換）を行っています。
・出力が4倍の理由はLSTMを使用しているため、出力、入力制御、忘却、出力制御を行うために用いられています。
・初期のパラメータを-0.1〜0.1の間で与えています。
・GPUの引数が一つ以上ならば、cudaの初期化とモデルをGPUに適用させる処理を行っています。

```py

# Prepare RNNLM model
model = chainer.FunctionSet(embed=F.EmbedID(len(vocab), n_units),
                            l1_x=F.Linear(n_units, 4 * n_units),
                            l1_h=F.Linear(n_units, 4 * n_units),
                            l2_x=F.Linear(n_units, 4 * n_units),
                            l2_h=F.Linear(n_units, 4 * n_units),
                            l3=F.Linear(n_units, len(vocab)))
for param in model.parameters:
    param[:] = np.random.uniform(-0.1, 0.1, param.shape)
if args.gpu >= 0:
    cuda.init(args.gpu)
    model.to_gpu()

```
###学習関数の設定

1ステップ前方処理関数：学習データ、ラベル、状態を与える

・GPU指定があれば、cuda用のデータに変換する
・学習用データを作成（特徴量データ、ラベルデータ）
・特徴ベクトルはBag of wordsの形式なので潜在ベクトル空間に圧縮する
・過学習をしないようにランダムに一部のデータを捨て、過去の状態のも考慮した第一の隠れ層を作成
・LSTMに現在の状態と先ほど定義した隠れ層を付与して学習し、隠れ層と状態を出力
・2層目も1層目と同様の処理を行う
・ラベルは3層目の処理で出力された値を使用する。

状態の初期化：確率的勾配法に必要なデータを与え、学習データと認識させる

各隠れ層と値を通すか判断するcの値を0で必要なデータ分初期化する

```py
# Neural net architecture


def forward_one_step(x_data, y_data, state, train=True):
    if args.gpu >= 0:
        x_data = cuda.to_gpu(x_data)
        y_data = cuda.to_gpu(y_data)
    x = chainer.Variable(x_data, volatile=not train)
    t = chainer.Variable(y_data, volatile=not train)
    h0 = model.embed(x)
    h1_in = model.l1_x(F.dropout(h0, train=train)) + model.l1_h(state['h1'])
    c1, h1 = F.lstm(state['c1'], h1_in)
    h2_in = model.l2_x(F.dropout(h1, train=train)) + model.l2_h(state['h2'])
    c2, h2 = F.lstm(state['c2'], h2_in)
    y = model.l3(F.dropout(h2, train=train))
    state = {'c1': c1, 'h1': h1, 'c2': c2, 'h2': h2}
    return state, F.softmax_cross_entropy(y, t)


def make_initial_state(batchsize=batchsize, train=True):
    return {name: chainer.Variable(mod.zeros((batchsize, n_units),
                                             dtype=np.float32),
                                   volatile=not train)
            for name in ('c1', 'h1', 'c2', 'h2')}

```
更新幅を1に設定して確率的勾配法による最適化を行うように設定している。

```py

# Setup optimizer
optimizer = optimizers.SGD(lr=1.)
optimizer.setup(model.collect_parameters())

```

###評価関数の設定

評価の関数
1：状態の初期化と損失の初期化
2：データのサイズ分確率的勾配法に基づいて更新していく
3：損失の平均値をexp関数に与えて返す。（低い値でも0にならないのが特徴で正の値だと急激に増えるため損失が大きい時には有効に働く）


```py

# Evaluation routine


def evaluate(dataset):
    sum_log_perp = mod.zeros(())
    state = make_initial_state(batchsize=1, train=False)
    for i in six.moves.range(dataset.size - 1):
        x_batch = dataset[i:i + 1]
        y_batch = dataset[i + 1:i + 2]
        state, loss = forward_one_step(x_batch, y_batch, state, train=False)
        sum_log_perp += loss.data.reshape(())

    return math.exp(cuda.to_cpu(sum_log_perp) / (dataset.size - 1))

```

###学習処理を始める前の設定

・学習データのサイズを取得
・ジャンプの幅を設定（順次学習しない）
・パープレキシティを0で初期化
・最初の時間情報を取得
・初期状態を現在の状態に付与
・状態の初期化
・損失を0で初期化

```py

# Learning loop
whole_len = train_data.shape[0]
jump = whole_len // batchsize
cur_log_perp = mod.zeros(())
epoch = 0
start_at = time.time()
cur_at = start_at
state = make_initial_state()
accum_loss = chainer.Variable(mod.zeros(()))
print('going to train {} iterations'.format(jump * n_epoch))
```

###確率的勾配法を用いた学習

確率的勾配法を用いて学習している。
一定のデータを選択し損失計算をしながらパラメータ更新をしている。
逐次尤度の計算も行っている。

```py
for i in six.moves.range(jump * n_epoch):
    x_batch = np.array([train_data[(jump * j + i) % whole_len]
                        for j in six.moves.range(batchsize)])
    y_batch = np.array([train_data[(jump * j + i + 1) % whole_len]
                        for j in six.moves.range(batchsize)])
    state, loss_i = forward_one_step(x_batch, y_batch, state)
    accum_loss += loss_i
    cur_log_perp += loss_i.data.reshape(())

```
・バックプロパゲーションでパラメータを更新する。
・truncateはどれだけ過去の履歴を見るかを表している。
http://kiyukuta.github.io/2013/12/09/mlac2013_day9_recurrent_neural_network_language_model.html#recurrent-neural-network
・L2正規化をかけている。

```py

    if (i + 1) % bprop_len == 0:  # Run truncated BPTT
        optimizer.zero_grads()
        accum_loss.backward()
        accum_loss.unchain_backward()  # truncate
        accum_loss = chainer.Variable(mod.zeros(()))

        optimizer.clip_grads(grad_clip)
        optimizer.update()
```

学習データのパープレキシティの計算（9999回に1回行う）し、表示する。


```py

    if (i + 1) % 10000 == 0:
        now = time.time()
        throuput = 10000. / (now - cur_at)
        perp = math.exp(cuda.to_cpu(cur_log_perp) / 10000)
        print('iter {} training perplexity: {:.2f} ({:.2f} iters/sec)'.format(
            i + 1, perp, throuput))
        cur_at = now
        cur_log_perp.fill(0)
```

バリデーションデータのパープレキシティの計算（バリデーションの幅によって異なる）し表示する

```py

    if (i + 1) % jump == 0:
        epoch += 1
        print('evaluate')
        now = time.time()
        perp = evaluate(valid_data)
        print('epoch {} validation perplexity: {:.2f}'.format(epoch, perp))
        cur_at += time.time() - now  # skip time of evaluation

        if epoch >= 6:
            optimizer.lr /= 1.2
            print('learning rate =', optimizer.lr)

    sys.stdout.flush()

```

###評価

テストデータに対するパープレキシティの計算

```py

# Evaluate on test dataset
print('test')
test_perp = evaluate(test_data)
print('test perplexity:', test_perp)
```


#参考サイト一覧

言語モデルのカバレージ、パープレキシティの説明

http://marujirou.hatenablog.com/entry/2014/08/22/235215

ディープラーニングフレームワークChainerをEC2のGPUインスタンスで動かす g2.2xlarge instance

http://ukonlly.hatenablog.jp/entry/2015/07/04/210149

Drop Out

http://olanleed.hatenablog.com/entry/2013/12/03/010945

Learning to forget continual prediction with lstm

http://www.slideshare.net/FujimotoKeisuke/learning-to-forget-continual-prediction-with-lstm

Zaremba, Wojciech, Ilya Sutskever, and Oriol Vinyals. "Recurrent neural network regularization." arXiv preprint arXiv:1409.2329 (2014).

Google Mikolov

http://www.rnnlm.org/

Neural Network(NN)を利用したLanguage Model(LM)，つまりNeural Network Language Model(NNLM)の一種であり， Recurrent Neural Network(RNN)を使ったRecurrent Neural Network Language Model(RNNLM)

http://kiyukuta.github.io/2013/12/09/mlac2013_day9_recurrent_neural_network_language_model.html

Long Short-term Memory

http://www.slideshare.net/nishio/long-shortterm-memory

Chainerのptbサンプルを解説しつつ、自分の文章を深層学習させて、僕の文章っぽい文を自動生成させてみる

http://d.hatena.ne.jp/shi3z/20150714/1436832305

RNNLM

http://www.slideshare.net/uchumik/rnnln

スパース推定概観：モデル・理論・応用

http://www.is.titech.ac.jp/~s-taiji/tmp/sparse_tutorial_2014.pdf

正則化学習法における最適化手法

http://imi.kyushu-u.ac.jp/~waki/ws2013/slide/suzuki.pdf

リカレントニューラル言語モデル作成参考
https://github.com/yusuketomoto/chainer-char-rnn

ニューラルネット　自然言語処理
http://www.orsj.or.jp/archive2/or60-4/or60_4_205.pdf

言語モデル作成
http://www.slideshare.net/uchumik/rnnln

自然言語処理プログラミング勉強会n-gram言語モデル
http://www.phontron.com/slides/nlp-programming-ja-02-bigramlm.pdf

Statistical Semantic入門 ~分布仮説からword2vecまで~
http://www.slideshare.net/unnonouno/20140206-statistical-semantics

linux source code
https://github.com/torvalds/linux

なぜGPUコンピューティングが注目を浴びているか - 慶應義塾 http://www.yasuoka.mech.keio.ac.jp/gpu/gpu_0.php

CUDA技術を利用したGPUコンピューティングの実際（前編） ―― グラフィックス分野で磨かれた並列処理技術を汎用数値計算に応用
http://www.kumikomi.net/archives/2008/06/12gpu1.php?page=1

GPGPU
https://ja.wikipedia.org/wiki/GPGPU#.E7.89.B9.E5.BE.B4.E3.81.A8.E8.AA.B2.E9.A1.8C