rnnlm.py


・ライブラリのインポート

#!/usr/bin/env python

import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable, \
                        optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L

・ファイル読み込み、前処理

def load_data(filename):
    global 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(train_path)
eos_id = vocab['<eos>']
  • words :文章を読み込み、文末に<eos>タグを追加、行頭と行末の改行コードを削除する
  • vocab :単語を重複無しで追加し、インデックスを付けて保存
  • dataset :単語のインデックス番号を配列として保存

・クラス定義

--init--

class MyRNN(chainer.Chain):
    def __init__(self, v, k):
        super(MyRNN, self).__init__(
            embed = L.EmbedID(v, k), # Word embedding layer
            H  = L.Linear(k, k), # L1 layer
            W = L.Linear(k, v), # L2 layer
        )

--init--部は、モデルのセット時に1度だけ用いられる。
v,kを引数として受取り、レイヤを定義する。

  • input layer : 語彙数、len{vocab}より、439(?)
  • hidden layer : dembより100
  • output layer : input layer と同じ

__call__

    def __call__(self, s):
        accum_loss = None
        v, k = self.embed.W.data.shape 
        h = Variable(np.zeros((1,k), dtype=np.float32)) # define variable h [ it means x = Variable(x_data, dtype=np.float32) ] 
        for i in range(len(s)): # iterate word in sentenc s = [0,1,2,3] -> 4 iterate

            next_w_id = eos_id if (i == len(s) - 1) else s[i+1] # if next word is in here, else next word is <eos> 
            tx = Variable(np.array([next_w_id], dtype=np.int32)) # tx is next word ID ( = ground truth data ) 
            x_k = self.embed(Variable(np.array([s[i]], dtype=np.int32))) #x_k ==Wx  
            h = F.tanh(x_k + self.H(h)) # sekf.H(h) is W'h
            loss = F.softmax_cross_entropy(self.W(h), tx)
            accum_loss = loss if accum_loss is None else accum_loss + loss

        return accum_loss, x_k, self.W(h)  

__call__部は、モデルの定義完了後に実行される。

v,k: __init__で定義したembedレイヤの重みWの行列を定義。v:行、k:列の次元に対応している。
h:中間層のユニット数を定義、中間層は今回は100ユニットとしている。
sには受け取った文章のインデックスが、単語ごとに配列として格納されている。これをfor i in range(len(s)):ループ内で単語ごとに分割し、処理を行う。

・for内の処理

next_w_idに$t+1$ステップでの出現単語のインデックス番号を格納する。
もし次の単語が存在しない場合、<eos>のインデックスが格納される。
その後、txとして変数化される。x_kでは、今ステップで参照している単語のインデックスを変数化し、word embeddingを行う。

・word embedding

  • 辞書の該当単語IDを取得し、そのID部分のみが1、それ以外を0とするような1-hotベクトル$x_t$を作成する。
  • また、コード内のh (=$h_t$)は、$t-1$ステップの$h_{t-1}$と$x_k$を用いて以下の式で求まる。加えて$W_{embed}$を用いて$x_k$(=x_k)を得る。$$x_t:dim(x_t)=len(vocab)$$
  • $$x_k = W_{embed} * x_k, dim(x_k)=demb$$$$h_t=tanh(W_{embed} * x_t + W_h * h_{t-1})$$
  • $$=tanh(x_k+ W_h*h_{t-1})$$

F.softmax_cross_entropy(self.W(h), tx)により、モデルの尤度が求まる。詳細は以下を参照
交差エントロピー:neuralnetの日記
 

・メモ

観測データ$self.W(h).data=z$, 教師データ$t_x=t$とすると
$$z=[0.166479, 0.060454, 3.714621, ..., -0.407060], sum(z)≠ 1$$$$y =softmax(z) = \frac{exp(z_i)}{\sum_iexp(z_i)}, sum(y)=1$$であり、$softmax()$を通す事で、総和が1になるように正規化される。(=確率とみなせる)
但し、クラスが多い場合、値が極めて小さくなり、アンダーフローの可能性が発生する。logを通す事で、値をそこそこ大きく取る事ができる(?)
クロスエントロピーを通す事で、モデルの損失を測ることができる。$t,y$について$$t=[0, 0, ..., 0, 1, 0, ... , 0], len(y)=len(t)=len(vocab)$$
$$crossentropy(y,t) = -\sum_it_i*log_2(y_i)=\sum_i\frac{t_i}{log_2(y_i)}$$

教師データ$t$は1-hotベクトルであるから、正解クラス$t_i=t_k$以外の値は0となる。よって、式は以下のように変形可能となる。$$crossentropy(y',t)=\frac{t_k}{log_2(y'_k)}$$

モデルの損失を文章単位で累積し、累積誤差accum_lossを返す。

main

for epoch in range(1):
    s = []    
    for pos in range(25): # train_data = [0, 1, 2, 3, 4, 5, 6, 3, 7, 8, 9, 3]
        id = train_data[pos]
        s.append(id)        
        if (id == eos_id):
            ## s = [0,1,2,3] ( index '3' is <eos>  )

            loss, embedlist,tmp = model(s) ## loss.data.size=1, embed.data.size=100

            model.zerograds()
            loss.backward()
            optimizer.update()
            s = []                
        if (pos % 100 == 0):
            print(pos, "/", len(train_data)," finished")
    outfile = "myrnn-" + str(epoch) + ".model"
    serializers.save_npz(outfile, model)

for epoch in range(n): エポックの繰り返し回数n
for pos in range(len(train_data)): 文章のwordの数だけ繰り返す