機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

TheanoでLSTM③

前回の続き
kento1109.hatenablog.com

前回は、lstm_layerの内容を見てきた。
このように呼んでいたので、

proj = lstm_layer(tparams, emb, options,
                  prefix=options['encoder'],
                  mask=mask)

projには、lstm_layerの戻り値のhがセットされる。
h文字の長さ×サンプル数×隠れ層(128)の3階テンソル
前回の処理を絵で描くとこうなる。
projには赤線のhがセットされる。
f:id:kento1109:20171126122226p:plain

if options['encoder'] == 'lstm':
        proj = (proj * mask[:, :, None]).sum(axis=0)
        proj = proj / mask.sum(axis=0)[:, None]

proj * mask[:, :, None]アダマール積で、paddingされた(単語が存在しない)要素をゼロで更新する。
その結果を軸0の方向での総和を求める。
(文書の単語の値の総和して1文書1ベクトルとする。)
結果、projは、サンプル数×隠れ層(128)の行列となる。
※今回は、感情分析タスクなので、出力層に1文書で1つの出力としているが、Sequence Taggingの場合、出力層には、各単語の隠れ層の結果を渡す必要がある。

次のdropout_layerの説明は割愛する。

softmax関数で各クラスの事後確率を算出する。

pred = tensor.nnet.softmax(tensor.dot(proj, tparams['U']) + tparams['b'])

proj(サンプル数×128)とtparams['U'](128×2)の内積を取ると、「サンプル数×2」の行列になる。

f_pred_prob = theano.function([x, mask], pred, name='f_pred_prob')

は、pred を出力する関数定義。

f_pred = theano.function([x, mask], pred.argmax(axis=1), name='f_pred')

は、pred の事後確率が最大のクラスの確率を出力する関数定義。

cost = -tensor.log(pred[tensor.arange(n_samples), y] + off).mean()

で正解ラベルと予測の確率の誤差をサンプル数で平均を取って損失関数とする。
これでようやくメインのtrain_lstm関数に戻る。

戻ってきて

if decay_c > 0.:
    decay_c = theano.shared(numpy_floatX(decay_c), name='decay_c')
    weight_decay = 0.
    weight_decay += (tparams['U'] ** 2).sum()
    weight_decay *= decay_c
    cost += weight_decay

これは、weight decay(重み減衰)と呼ばれ、
損失関数にL2正則化項を含め汎化性能向上を図る手法。

f_cost = theano.function([x, mask, y], cost, name='f_cost')

は、損失関数の定義

次に微分計算

grads = tensor.grad(cost, wrt=list(tparams.values()))
f_grad = theano.function([x, mask, y], grads, name='f_grad')

wrtには、微分の値を取るシンボルを指定
tparamsは、全パラメータ(全て共有変数)の辞書
ミニバッチ内ではf_grad関数を直接呼んでおらず、最適化アルゴリズム内でgrads を呼ぶ形で使用している。

次に最適化関数の定義

# optimizer=adadelta
lr = tensor.scalar(name='lr')
f_grad_shared, f_update = optimizer(lr, tparams, grads,
                                    x, mask, y, cost)

デフォルトでは、adadelta関数を呼び出す。
adadeltaは、勾配降下法の最適化アルゴリズムの一つ。
詳細な内容はここでは割愛する。

続いて、検証・テストデータのミニバッチ化

kf_valid = get_minibatches_idx(len(valid[0]), valid_batch_size)
kf_test = get_minibatches_idx(len(test[0]), valid_batch_size)

valid[0]=105,test[0]=500,valid_batch_size=64の場合、
kf_validは、

0 1
0,1,2,...,63 64,65,...,104

のリスト、
kf_test は、

0 1 2 3 4 5 6 7
0,1,2,...,63 64,...,127 128,..., 255 ... ... ... ... 448,..., 499

のリストになる。

また、validFreq ,saveFreq の値は、今回は手動設定しているが、それぞれ「-1」とした場合、

validFreq = len(train[0]) // batch_size  # 1998 // 64 = 31
saveFreq = len(train[0]) // batch_size  # 1998 // 64 = 31

となる。

こっから、いよいよエポック数分の学習が始まる。
まず、訓練データをミニバッチ化するためのインデックスの取得

kf = get_minibatches_idx(len(train[0]), batch_size, shuffle=True)

batch_size=16, shuffle=Trueなので、kf はこんな感じ。(1998までの数値をランダムに16分割)

0 1 2 .. 124
1,124,83,... ... ... ... 1312,10,...

以降は、

for _, train_index in kf:
 ...

内でミニバッチ毎の繰り返し処理
まず、

y = [train[1][t] for t in train_index]
x = [train[0][t]for t in train_index]

で訓練データからミニバッチ単位のサンプルデータを抽出する。

次に、

x, mask, y = prepare_data(x, y)

prepare_dataxの長さをミニバッチ内の最大の長さにそろえる。(不足分はpadding
maskxの各要素が単語かpaddingされた要素か判別するためのもの。

f_grad_shared関数でミニバッチの入力の損失値を計算する。
※ミニバッチループの外で定義した関数(モデル)を呼び出す。

cost = f_grad_shared(x, mask, y)

f_grad_sharedは、adadelta関数を呼び出した戻り値。
f_grad_shared関数のモデルは定義済みで、ここで、値を入れるイメージ。
中身を見ると、

zgup = [(zg, g) for zg, g in zip(zipped_grads, grads)]
rg2up = [(rg2, 0.95 * rg2 + 0.05 * (g ** 2))
         for rg2, g in zip(running_grads2, grads)]
f_grad_shared = theano.function([x, mask, y], cost, updates=zgup + rg2up,
                                name='adadelta_f_grad_shared')

x, mask, yは、ミニバッチの繰り返し処理で与える値
costは、adadelta関数呼び出し時に指定した損失関数
zgup, rg2upには、f_grad_shared関数の呼び出し時、ミニバッチ内の入力に応じた各パラメータの勾配値がセットされる。
それらを元に現在の損失関数の値を計算する。

f_update関数で、各パラメータを更新する。

f_update(lrate)

f_update関数の内容

f_update = theano.function([lr], [], updates=ru2up + param_up,
                           on_unused_input='ignore',
                           name='adadelta_f_update')

lr(学習率)を受け取り、各パラメータを更新する。

次の部分は重要でないので、検証部分まで飛ばす。

train_err = pred_error(f_pred, prepare_data, train, kf)
valid_err = pred_error(f_pred, prepare_data, valid,
                       kf_valid)
test_err = pred_error(f_pred, prepare_data, test, kf_test)

で訓練・検証・テストデータのエラー率(不一致律)を求める。
pred_error関数はエラー率を求める関数。

def pred_error(f_pred, prepare_data, data, iterator, verbose=False):
    valid_err = 0
    for _, valid_index in iterator:
        x, mask, y = prepare_data([data[0][t] for t in valid_index],
                                  numpy.array(data[1])[valid_index],
                                  maxlen=None)
        preds = f_pred(x, mask)
        targets = numpy.array(data[1])[valid_index]
        valid_err += (preds == targets).sum()
    valid_err = 1. - numpy_floatX(valid_err) / len(data[0])

    return valid_err

各ミニバッチ単位で

  • preds:予測クラス
  • targets:実際のクラス
  • valid_err:予測と実際のクラスが一致した数

を求めて、全てのデータのvalid_err(不一致率)を戻り値とする。
後は、早期終了判定やパラメータの保存判定などを行う。
これをミニバッチ毎エポック回数繰り返す。

LSTMは長かったが、内容が割と理解できた気がする。

次は、Denoising Autoencoders を読んでみようと思う。