データナード

機械学習と自然言語処理についての備忘録 (旧ナード戦隊データマン)

keras-laserのいくつかの修正

問題点

kerasでlaserを実装中だが、文Embeddingを使ってみると、どんな文ペアでも殆どが1に近い類似度になってしまう。

現状の解決策

build_model関数でモデルの主要な部分を引数から設定できるようにする。

いくつかの仮説

  • 損失関数内でパディング部分も評価してしまっていることが問題。
  • パディングのマスキングに関する問題。
  • sentembを線形変換し、それをデコーダ内のLSTMのinitial_stateにわたす際のなんらかの問題。
  • 活性関数の設定によって生じる問題。
  • クリッピングの有無に関する問題。
  • bilstmのユニット数や深さなどの設定の問題。
  • embeddingレイヤーの次元の問題。
  • 高速化のためにGRUを使ったが、LSTMを使わないことによって生じる問題。
  • dropout率の設定、またはdropoutを入れる部分の問題。
  • デコーダの入力BPEの未来の時間の情報がリークし、過学習している可能性。

問題に関連していると思われる現象

  • val_accuracy, val_lossなどは高い値が出ているのに、文Embeddingとしては機能しない。
  • 文ベクトル内に0.0がたくさん含まれる。
  • 文ベクトルの任意の文ペアの類似度が1に近い値になる。
  • たまに勾配爆発する。

修正版のコード

このコードは、これらの問題を解決するためにいろいろなパラメータを変更しやすくしたものです。

import numpy as np
import tensorflow as tf
import tensorflow.keras.layers as layers
from tensorflow.keras import backend as K
from data_generator4 import data_generate_phase1, data_generate_phase2
tf.executing_eagerly()

MAX_OUT_LANG = 3
BS = 2
STEPS = 100
EPOCHS = 100000
#MAXLEN = 80

CALLBACKS = [
    tf.keras.callbacks.ModelCheckpoint(
        filepath="/root/work/keras-laser/models/model_last_euro.h5",
        save_weights_only=False,
        save_best_only=False),
    tf.keras.callbacks.CSVLogger(
        "/root/work/keras-laser/models/training_euro6.log")
]


class MyRepeatVector(layers.Layer):
    def __init__(self, mask_zero=False, **kwargs):
        super(MyRepeatVector, self).__init__(**kwargs)
        self.mask_zero = mask_zero

    def call(self, inputs):
        x = inputs[0]
        y = inputs[1]
        v = layers.RepeatVector(K.shape(y)[1])(x)
        return v

    def compute_mask(self, inputs, mask=None):
        if not self.mask_zero:
            return None
        output_mask = K.not_equal(inputs[1], 0)
        return output_mask

    def get_config(self):
        config = {'mask_zero': self.mask_zero}
        base_config = super(MyRepeatVector, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))


def _validation_data(gen):
    return next(gen)


def _build_est(sentemb, est_act, gru_dim, dec_magn, use_initial, use_raw_est,
               use_lstm):
    est = None
    if use_initial:
        if use_raw_est:
            assert dec_magn == 2
            est = [sentemb]
            if use_lstm:
                est.append(sentemb)
        else:
            est = [
                layers.Dense(gru_dim * dec_magn, activation=est_act)(sentemb)
            ]
            if use_lstm:
                est.append(
                    layers.Dense(gru_dim * dec_magn,
                                 activation=est_act)(sentemb))
    return est


def _build_multi_bilstm(encoder, gru_dim, gru_depth, use_lstm, use_enc_relu,
                        enc_act):
    for i in range(gru_depth):
        if use_lstm:
            encoder = layers.Bidirectional(
                layers.LSTM(gru_dim,
                            return_sequences=True,
                            recurrent_initializer='glorot_uniform',
                            activation=enc_act))(encoder)
        else:
            encoder = layers.Bidirectional(
                layers.GRU(gru_dim,
                           return_sequences=True,
                           recurrent_initializer='glorot_uniform',
                           activation=enc_act))(encoder)
    if use_enc_relu:
        encoder = layers.TimeDistributed(
            layers.Dense(gru_dim * 2, activation="relu"))(encoder)
    return encoder


def _build_sentemb(bpe_input, max_features, emb_dim1, gru_dim, gru_depth,
                   use_lstm, use_zero_replacer, use_enc_relu, enc_act):
    bpe_emb1 = layers.Embedding(max_features, emb_dim1)(bpe_input)
    encoder = _build_multi_bilstm(bpe_emb1, gru_dim, gru_depth, use_lstm,
                                  use_enc_relu, enc_act)
    x = encoder
    if use_zero_replacer:
        #THIS FEATURE DOESN'T WORK
        a = x * K.cast(K.not_equal(x, 0.0), 'float32')
        b = -1.0 * K.cast(K.equal(x, 0.0), 'float32')
        x = a + b

    sentemb = layers.GlobalMaxPooling1D(name="sentemb")(x)
    return sentemb


def _build_decoder_lstm(cat_emb, est, gru_dim, dec_magn, drate, rdrate,
                        use_lstm, dec_act):
    if use_lstm:
        decoder = layers.LSTM(gru_dim * dec_magn,
                              return_sequences=True,
                              dropout=drate,
                              recurrent_dropout=rdrate,
                              recurrent_initializer="glorot_uniform",
                              activation=dec_act)(cat_emb, initial_state=est)
    else:
        decoder = layers.GRU(gru_dim * dec_magn,
                             return_sequences=True,
                             dropout=drate,
                             recurrent_dropout=rdrate,
                             recurrent_initializer="glorot_uniform",
                             activation=dec_act)(cat_emb, initial_state=est)
    return decoder


def _build_catemb(bpe_input, lid_input, sentemb, max_features, emb_dim2,
                  max_out_lang, lidemb_dim, emdrate, use_emd):
    bpe_emb2 = layers.Embedding(max_features, emb_dim2)(bpe_input)
    lid_emb = layers.Embedding(max_out_lang, lidemb_dim)(lid_input)

    if use_emd:
        bpe_emb2 = layers.SpatialDropout1D(emdrate)(bpe_emb2)

    sentemb_fx = MyRepeatVector()([sentemb, lid_input])
    cat_emb = layers.Concatenate()([bpe_emb2, sentemb_fx, lid_emb])
    return cat_emb


def _build_output(decoder_lstm, gru_dim, dec_relu_magn, max_out_features,
                  use_dense_relu):
    if use_dense_relu:
        decoder_lstm = layers.TimeDistributed(
            layers.Dense((gru_dim * dec_relu_magn),
                         activation="relu"))(decoder_lstm)
    output = layers.TimeDistributed(
        layers.Dense(max_out_features))(decoder_lstm)
    return output


def _build_loss_function(use_custom_loss, max_out_features, reduction="auto"):
    if use_custom_loss:
        loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
            from_logits=True, reduction=reduction)

        def loss_function(real, pred):
            real_fx = tf.reshape(real, [-1])
            pred_fx = tf.reshape(pred, [-1, max_out_features])
            mask = K.not_equal(real_fx, 0)
            loss_ = loss_object(real_fx, pred_fx)
            mask = tf.cast(mask, dtype=loss_.dtype)
            loss_ *= mask
            loss_ = tf.reduce_mean(loss_)
            return loss_

        return loss_function
    else:
        return tf.keras.losses.SparseCategoricalCrossentropy()


def build_model(max_features=50002,
                max_out_features=10001,
                emb_dim1=100,
                emb_dim2=100,
                gru_dim=512,
                gru_depth=2,
                lidemb_dim=8,
                drate=0.1,
                rdrate=0.0,
                emdrate=0.1,
                use_emd=True,
                use_initial=True,
                use_raw_est=False,
                use_lstm=False,
                use_enc_relu=False,
                use_dense_relu=True,
                use_custom_loss=True,
                use_clipping=False,
                use_zero_replacer=True,
                lr=0.002,
                clpn=1.,
                dec_magn=2,
                dec_relu_magn=2,
                bs=BS,
                lrelu_alpha=0.1,
                enc_act="tanh",
                est_act="linear",
                dec_act="tanh",
                reduction="auto",
                max_out_lang=MAX_OUT_LANG):
    bpe1 = layers.Input(batch_shape=(None, None),
                        name="bpe_enc",
                        dtype=tf.int32)
    bpe2 = layers.Input(batch_shape=(None, None),
                        name="bpe_dec",
                        dtype=tf.int16)
    lid = layers.Input(batch_shape=(None, None), name="lid", dtype=tf.uint8)
    sentemb = _build_sentemb(bpe1, max_features, emb_dim1, gru_dim, gru_depth,
                             use_lstm, use_zero_replacer, use_enc_relu,
                             enc_act)
    cat_emb = _build_catemb(bpe2, lid, sentemb, max_features, emb_dim2,
                            max_out_lang, lidemb_dim, emdrate, use_emd)

    est = _build_est(sentemb, est_act, gru_dim, dec_magn, use_initial,
                     use_raw_est, use_lstm)
    decoder_lstm = _build_decoder_lstm(cat_emb, est, gru_dim, dec_magn, drate,
                                       rdrate, use_lstm, dec_act)
    output = _build_output(decoder_lstm, gru_dim, dec_relu_magn,
                           max_out_features, use_dense_relu)
    model = tf.keras.Model([bpe1, bpe2, lid], output)

    adam = tf.keras.optimizers.Nadam(learning_rate=lr)
    if use_clipping:
        adam = tf.keras.optimizers.Nadam(learning_rate=lr, clipnorm=clpn)

    loss_function = _build_loss_function(use_custom_loss, max_out_features,
                                         reduction)

    model.compile(optimizer=adam,
                  loss=loss_function,
                  metrics=[tf.keras.metrics.SparseCategoricalAccuracy()])

    return model


def main():
    gen = data_generate_phase2(data_generate_phase1(), BS)
    #X_val, Y_val = _validation_data(gen)
    model = build_model()

    with open("summary.txt", "w") as f:
        model.summary(print_fn=lambda x: f.write(x + "\n"))
    model.fit_generator(gen,
                        steps_per_epoch=STEPS,
                        epochs=EPOCHS,
                        validation_data=None,
                        callbacks=CALLBACKS)


if __name__ == "__main__":
    main()

カスタムlossについて

損失関数内でパディングラベルを無視するにはどうするか考慮する必要があります。

def _build_loss_function(use_custom_loss, max_out_features):
    if use_custom_loss:
        loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
            from_logits=True, reduction='auto')

        def loss_function(real, pred):
            real_fx = tf.reshape(real, [-1])
            pred_fx = tf.reshape(pred, [-1, max_out_features])
            mask = tf.math.logical_not(tf.math.equal(real_fx, 0))
            loss_ = loss_object(real_fx, pred_fx)
            mask = tf.cast(mask, dtype=loss_.dtype)
            loss_ *= mask
            loss_ = tf.reduce_mean(loss_)
            return loss_

        return loss_function
    else:
        return tf.keras.losses.SparseCategoricalCrossentropy()

Googleの公式のチュートリアルを参考に、上記のコードを追加しました。

https://www.tensorflow.org/tutorials/text/nmt_with_attention#define_the_optimizer_and_the_loss_function

公式との違いは、出力が入力長と同じだけある点です。つまり、(batch_size, output_length, vocab_size) というshapeに対処するように変更したものです。(ただ、損失関数について設計するのをあまりやったことがないため、この方法でいいのか自信がありません。)

追記: LASERの公式の実装

LASERは訓練コードが公開されていませんが、Encoder部分だけpytorchのコードがあるようです。

特に以下の部分が関係するような気はします:

LASER/embed.py at 8b053348af22a0038db495616023a7341a4a614f · facebookresearch/LASER · GitHub

Set padded outputs to -inf so they are not selected by max-pooling

と書かれており、max-poolingによって0が選択されないように0を-infにリプレースしているようです。Kerasでこの部分を実装する方法を探す必要がありそうです。

参考