データナード

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

HConvNetの実装

HConvNetは、Convレイヤーを積み上げ、各ConvレイヤーのMax Poolingを結合することでSentence Embeddingを生成する方法です。

概要

f:id:mathgeekjp:20200416110551g:plain
https://research.fb.com/wp-content/uploads/2017/09/emnlp2017.pdf

  • LASERは、エンコーダで複数のBiLSTMを積み上げ、そのMax PoolingをとることでSentence Embeddingを生成します。
  • LASERのエンコーダ部分はLSTMを使っているため、低速です。
  • HConvNetは、BiLSTM-Maxpoolingモデルよりも精度が劣りますが、計算効率が高い可能性があります。

コード

import numpy as np
import tensorflow as tf
import tensorflow.keras.layers as layers
from tensorflow.keras import backend as K
#tf.executing_eagerly()

from tensorflow.keras.mixed_precision import experimental as mixed_precision

#policy = mixed_precision.Policy('mixed_float16')
#mixed_precision.set_policy(policy)


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_multi_conv(encoder, cnn_dim, cnn_depth, enc_act):
    max_pooling_layers = []
    dims = [1024 // cnn_depth for _ in range(cnn_depth)]
    for dim in dims:
        encoder = layers.SeparableConv1D(dim,
                                         kernel_size=3,
                                         strides=1,
                                         padding="same",
                                         activation="relu")(encoder)
        max_pooling_layers.append(layers.GlobalMaxPooling1D()(encoder))
    return layers.Concatenate(name="sentemb")(max_pooling_layers)


def _build_sentemb(bpe_input, max_features, emb_dim1, cnn_dim, cnn_depth,
                   use_mask, enc_act):
    mask_zero = False
    if use_mask:
        mask_zero = True
    bpe_emb1 = layers.Embedding(max_features, emb_dim1,
                                mask_zero=mask_zero)(bpe_input)
    sentemb = _build_multi_conv(bpe_emb1, cnn_dim, cnn_depth, enc_act)
    return sentemb


def _build_decoder_lstm(cat_emb, gru_dim, dec_magn, drate, dec_act):
    decoder = layers.GRU(gru_dim * dec_magn,
                         return_sequences=True,
                         dropout=drate,
                         recurrent_initializer="glorot_uniform",
                         activation=dec_act)(cat_emb)
    return decoder


def _build_catemb(bpe_input, lid_input, sentemb, max_out_features, emb_dim2,
                  max_out_lang, lidemb_dim):
    bpe_emb2 = layers.Embedding(max_out_features, emb_dim2)(bpe_input)
    lid_emb = layers.Embedding(max_out_lang, lidemb_dim)(lid_input)
    sentemb_fx = MyRepeatVector()([sentemb, lid_input])
    cat_emb = layers.Concatenate()([bpe_emb2, sentemb_fx, lid_emb])
    return cat_emb


def _build_output(decoder_lstm, max_out_features):
    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(from_logits=True)


def build_model(max_features=50001,
                max_out_features=8001,
                emb_dim1=320,
                emb_dim2=320,
                cnn_dim=256,
                gru_dim=512,
                cnn_depth=4,
                lidemb_dim=32,
                drate=0.1,
                rdrate=0.0,
                use_custom_loss=False,
                use_clipping=False,
                use_bn=True,
                use_mask=True,
                lr=0.005,
                clpn=1.,
                dec_magn=2,
                dec_relu_magn=2,
                enc_act="relu",
                dec_act="tanh",
                reduction="auto",
                max_out_lang=3):
    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=max_features,
                             emb_dim1=emb_dim1,
                             cnn_dim=cnn_dim,
                             cnn_depth=cnn_depth,
                             use_mask=use_mask,
                             enc_act=enc_act)
    cat_emb = _build_catemb(bpe2,
                            lid,
                            sentemb=sentemb,
                            max_out_features=max_out_features,
                            emb_dim2=emb_dim2,
                            max_out_lang=max_out_lang,
                            lidemb_dim=lidemb_dim)
    decoder_lstm = _build_decoder_lstm(cat_emb=cat_emb,
                                       gru_dim=gru_dim,
                                       dec_magn=dec_magn,
                                       drate=drate,
                                       dec_act=dec_act)
    output = _build_output(decoder_lstm=decoder_lstm,
                           max_out_features=max_out_features)
    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():
    from params.middle import DG, CONFIG
    CALLBACKS = [
        tf.keras.callbacks.ModelCheckpoint(filepath=CONFIG.outname,
                                           save_weights_only=False,
                                           save_best_only=False),
        tf.keras.callbacks.CSVLogger(CONFIG.outlog)
    ]

    gen = DG.data_generate_phase2_grp(DG.data_generate_phase1(), CONFIG.chunk,
                                      10)
    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=CONFIG.steps,
                        epochs=CONFIG.epochs,
                        validation_data=None,
                        callbacks=CALLBACKS)


if __name__ == "__main__":
    main()

# https://github.com/sugiyamath/keras-laser/blob/master/scripts/model_scripts/hconv_model.py 

考察

  • filtersやkernel_sizeをどう設定すればより良い文ベクトルを獲得できるか。keras-tuner等でパラメータチューニングしたほうが良さそう。
  • LASERのように、たくさんの言語を翻訳するようなタスクを解くことで文Embeddingを獲得したい場合、Conv1Dは十分な情報を持つことができるか。
  • 一般に、エンコーダ部分のアーキテクチャを分離すれば、マルチタスク学習できる可能性がある。ただし、マルチタスク学習をしたとしても、sentence retrievalタスクに利用するためには、sentembの部分が文ベクトルとして適切に機能するか(類似度や距離を取れるか)検証する必要がある。
  • 少し試したところ、このアーキテクチャの場合、文ベクトルが奇妙にスパース化される。(cosine similarityなら使えると思うが...)

参考