Introduction GPU

Chainer とはニューラルネットの実装を簡単にしたフレームワークです。

  • 今回は言語の分野でニューラルネットを適用してみました。

  • 今回は言語モデルを作成していただきます。

言語モデルとはある単語が来たときに次の単語に何が来やすいかを予測するものです。

言語モデルにはいくつか種類があるのでここでも紹介しておきます。

  • n-グラム言語モデル
    • 単語の数を単純に数え挙げて作成されるモデル。考え方としてはデータにおけるある単語の頻度に近い
  • ニューラル言語モデル

    • 単語の辞書ベクトルを潜在空間ベクトルに落とし込み、ニューラルネットで次の文字を学習させる手法
  • リカレントニューラル言語モデル

    • 基本的なアルゴリズムはニューラル言語モデルと同一だが過去に使用した単語を入力に加えることによって文脈を考慮した言語モデルの学習が可能となる。ニューラル言語モデルとは異なり、より古い情報も取得可能

以下では、このChainerを利用しデータを準備するところから実際に言語モデルを構築し学習・評価を行うまでの手順を解説します。

  1. 各種ライブラリ導入
  2. 初期設定
  3. データ入力
  4. リカレントニューラル言語モデル設定
  5. 学習を始める前の設定
  6. パラメータ更新方法(確率的勾配法)
  7. 言語の予測

もしGPUを使用したい方は、以下にまとめてあるのでご参考ください。

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

1.各種ライブラリ導入

Chainerの言語処理では多数のライブラリを導入します。


In [ ]:
import time
import math
import sys
import pickle
import copy
import os
import re

import numpy as np
from chainer import cuda, Variable, FunctionSet, optimizers
import chainer.functions as F

`導入するライブラリの代表例は下記です。

  • numpy: 行列計算などの複雑な計算を行なうライブラリ
  • chainer: Chainerの導入

2.初期設定

  • 学習回数、ユニット数、確率的勾配法に使用するデータの数、学習に使用する文字列の長さ、勾配法で使用する敷居値、学習データの格納場所、モデルの出力場所を設定しています。

In [ ]:
#-------------Explain7 in the Qiita-------------
n_epochs    = 30
n_units     = 641
batchsize   = 200
bprop_len   = 40
grad_clip   = 0.3
gpu_ID      = 0
data_dir = "data_hands_on"
checkpoint_dir = "cv"
xp = cuda.cupy if gpu_ID >= 0 else np
#-------------Explain7 in the Qiita-------------

3.データ入力

学習用にダウンロードしたファイルをプログラムに読ませる処理を関数化しています

  • 学習データをバイナリ形式で読み込んでいます。
  • 文字データを確保するための行列を定義しています。
  • データを単語をキー、長さを値とした辞書データにして行列データセットに登録しています。

学習データ、単語の長さ、語彙数を取得しています。 上記をそれぞれ行列データとして保持しています。


In [ ]:
# input data
#-------------Explain1 in the Qiita-------------
def source_to_words(source):
    line = source.replace("¥n", " ").replace("¥t", " ")
    for spacer in ["(", ")", "{", "}", "[", "]", ",", ";", ":", "++", "!", "$", '"', "'"]:
        line = line.replace(spacer, " " + spacer + " ")
    
    words = [w.strip() for w in line.split()]
    return words

def load_data():
    vocab = {}
    print ('%s/angular.js'% data_dir)
    source = open('%s/angular_full_remake.js' % data_dir, 'r').read()
    words = source_to_words(source)
    freq = {}
    dataset = np.ndarray((len(words),), dtype=np.int32)
    for i, word in enumerate(words):
        if word not in vocab:
            vocab[word] = len(vocab)
            freq[word] = 0
        dataset[i] = vocab[word]
        freq[word] += 1

    print('corpus length:', len(words))
    print('vocab size:', len(vocab))
    return dataset, words, vocab, freq
#-------------Explain1 in the Qiita-------------

if not os.path.exists(checkpoint_dir):
    os.mkdir(checkpoint_dir)

train_data, words, vocab, freq = load_data()

for f in ["frequent", "rarely"]:
    print("{0} words".format(f))
    print(sorted(freq.items(), key=lambda i: i[1], reverse=True if f == "frequent" else False)[:50])

4.リカレントニューラル言語モデル設定

RNNLM(リカレントニューラル言語モデルの設定を行っています)

  • EmbedIDで行列変換を行い、疎なベクトルを密なベクトルに変換しています。
  • 出力が4倍の理由は入力層、出力層、忘却層、前回の出力をLSTMでは入力に使用するためです。
  • 隠れ層に前回保持した隠れ層の状態を入力することによってLSTMを実現しています。
  • ドロップアウトにより過学習するのを抑えています。
  • 予測を行なうメソッドも実装しており、入力されたデータ、状態を元に次の文字列と状態を返すような関数になっています。
  • モデルの初期化を行なう関数もここで定義しています。

In [ ]:
#-------------Explain2 in the Qiita-------------
class CharRNN(FunctionSet):

    def __init__(self, n_vocab, n_units):
        super(CharRNN, self).__init__(
            embed = F.EmbedID(n_vocab, n_units),
            l1_x = F.Linear(n_units, 4*n_units),
            l1_h = F.Linear(n_units, 4*n_units),
            l2_h = F.Linear(n_units, 4*n_units),
            l2_x = F.Linear(n_units, 4*n_units),
            l3   = F.Linear(n_units, n_vocab),
        )
        for param in self.parameters:
            param[:] = np.random.uniform(-0.08, 0.08, param.shape)

    def forward_one_step(self, x_data, y_data, state, train=True, dropout_ratio=0.7):
        x = Variable(x_data, volatile=not train)
        t = Variable(y_data, volatile=not train)

        h0      = self.embed(x)
        h1_in   = self.l1_x(F.dropout(h0, ratio=dropout_ratio, train=train)) + self.l1_h(state['h1'])
        c1, h1  = F.lstm(state['c1'], h1_in)
        h2_in   = self.l2_x(F.dropout(h1, ratio=dropout_ratio, train=train)) + self.l2_h(state['h2'])
        c2, h2  = F.lstm(state['c2'], h2_in)
        y       = self.l3(F.dropout(h2, ratio=dropout_ratio, train=train))
        state   = {'c1': c1, 'h1': h1, 'c2': c2, 'h2': h2}

        return state, F.softmax_cross_entropy(y, t)

    def predict(self, x_data, state):
        x = Variable(x_data, volatile=True)

        h0      = self.embed(x)
        h1_in   = self.l1_x(h0) + self.l1_h(state['h1'])
        c1, h1  = F.lstm(state['c1'], h1_in)
        h2_in   = self.l2_x(h1) + self.l2_h(state['h2'])
        c2, h2  = F.lstm(state['c2'], h2_in)
        y       = self.l3(h2)
        state   = {'c1': c1, 'h1': h1, 'c2': c2, 'h2': h2}

        return state, F.softmax(y)

def make_initial_state(n_units, batchsize=50, train=True):
    return {name: Variable(np.zeros((batchsize, n_units), dtype=np.float32),
            volatile=not train)
            for name in ('c1', 'h1', 'c2', 'h2')}
#-------------Explain2 in the Qiita-------------

RNNLM(リカレントニューラル言語モデルの設定を行っています)


In [ ]:
# Prepare RNNLM model
model = CharRNN(len(vocab), n_units)

if gpu_ID >= 0:
    cuda.check_cuda_available()    
    cuda.get_device(gpu_ID).use()
    model.to_gpu()
optimizer = optimizers.RMSprop(lr=2e-3, alpha=0.95, eps=1e-8)
optimizer.setup(model)

5.学習を始める前の設定

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

In [ ]:
whole_len    = train_data.shape[0]
jump         = whole_len // batchsize
epoch        = 0
start_at     = time.time()
cur_at       = start_at
state        = make_initial_state(n_units, batchsize=batchsize)
cur_log_perp = 0
if gpu_ID >= 0:
    accum_loss   = Variable(cuda.zeros(()))
    for key, value in state.items():
        value.data = cuda.to_gpu(value.data)
else:
    accum_loss   = Variable(xp.zeros((), dtype=np.float32))

6.パラメータ更新方法(ミニバッチ)

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

  • 適宜学習データのパープレキシティも計算している

  • バックプロパゲーションでパラメータを更新する。

  • truncateはどれだけ過去の履歴を見るかを表している。
  • optimizer.clip_gradsの部分でL2正則化をかけている。
  • 過学習を抑えるために学習効率を徐々に下げている。

In [ ]:
for i in range(int(jump * n_epochs)):
    #-------------Explain4 in the Qiita-------------
    x_batch = np.array([train_data[(jump * j + i) % whole_len]
                        for j in range(batchsize)])
    y_batch = np.array([train_data[(jump * j + i + 1) % whole_len]
                        for j in range(batchsize)])

    if gpu_ID >= 0:
        x_batch = cuda.to_gpu(x_batch)
        y_batch = cuda.to_gpu(y_batch)

    state, loss_i = model.forward_one_step(x_batch, y_batch, state, dropout_ratio=0.7)
    accum_loss   += loss_i
    cur_log_perp += loss_i.data

    if (i + 1) % bprop_len == 0:  # Run truncated BPTT
        now = time.time()
        cur_at = now
        # print('{}/{}, train_loss = {}, time = {:.2f}'.format((i + 1)/bprop_len, jump, accum_loss.data / bprop_len, now-cur_at))

        optimizer.zero_grads()
        accum_loss.backward()
        accum_loss.unchain_backward()  # truncate
        accum_loss = Variable(np.zeros((), dtype=np.float32))
        if gpu_ID >= 0:
            accum_loss = Variable(cuda.zeros(()))
        else:
            accum_loss = Variable(np.zeros((), dtype=np.float32))

        optimizer.clip_grads(grad_clip)
        optimizer.update()
        
    if (i + 1) % 10000 == 0:
        perp = math.exp(cuda.to_cpu(cur_log_perp) / 10000)
        print('iter {} training perplexity: {:.2f} '.format(i + 1, perp))
        fn = ('%s/charrnn_epoch_%i.chainermodel' % (checkpoint_dir, epoch))
        pickle.dump(copy.deepcopy(model).to_cpu(), open(fn, 'wb'))
        cur_log_perp = 0

    if (i + 1) % jump == 0:
        epoch += 1

    #-------------Explain4 in the Qiita-------------

    sys.stdout.flush()

7.言語の予測

  • 学習したモデルを取得
  • モデルからユニット数を取得
  • 最初の空文字を設定

In [ ]:
# load model
#-------------Explain6 in the Qiita-------------
model = pickle.load(open("cv/charrnn_epoch_22.chainermodel", 'rb'))
#-------------Explain6 in the Qiita-------------
n_units = model.embed.W.shape[1]

if gpu_ID >= 0:
    cuda.check_cuda_available()
    cuda.get_device(gpu_ID).use()
    model.to_gpu()
# initialize generator
state = make_initial_state(n_units, batchsize=1, train=False)
if gpu_ID >= 0:
    for key, value in state.items():
        value.data = cuda.to_gpu(value.data)
        
# show vocababury
ivocab = {}
ivocab = {v:k for k, v in vocab.items()}
  • 学習したモデルを利用して文字の予測を行なう。
  • 予測で出力された文字と状態を次の入力に使用する。

In [ ]:
# initialize generator
index = np.random.randint(0, len(vocab), 1)[0]
sampling_range = 5

prev_char = np.array([0], dtype=np.int32)
if gpu_ID >= 0:
    prev_char = cuda.to_gpu(prev_char)

for i in range(1000):
    if ivocab[index] in ["}", ";"]:
        sys.stdout.write(ivocab[index] + "\n")
    else:
        sys.stdout.write(ivocab[index] + " ")
    
    #-------------Explain7 in the Qiita-------------
    state, prob = model.predict(prev_char, state)
    index = np.argmax(cuda.to_cpu(prob.data))
    #index = np.random.choice(prob.data.argsort()[0,-sampling_range:][::-1], 1)[0]
    #-------------Explain7 in the Qiita-------------
    
    prev_char = np.array([index], dtype=np.int32)
    if gpu_ID >= 0:
        prev_char = cuda.to_gpu(prev_char)
print

In [ ]:


In [ ]: