NJUNMT学习

一、模型定义

NJUNMT中的模型分为Sequence2Sequence和Transformer两种。两种模型总体上都可以看做是Encoder-Decoder模型,但使用的具体encoder和decoder的类型存在不同。Seq2Seq模型使用RNN,而Transformer不使用RNN,依赖于attention机制。

模型.jpg

1. Sequence2Sequence

这一部分介绍NJUNMT中seq2seq模型的主要构成部件。
seq2seq模型配置示例:

#njunmt/example_configs/toy_seq2seq.yml
model: njunmt.models.SequenceToSequence
model_params:
  '''是否对所有fflayer进行归一化'''
  fflayers.layer_norm: true 
  '''源语言embedding维度'''
  embedding.dim.source: 8
  '''目标语言embedding维度'''
  embedding.dim.target: 8
  modality.params:
    multiply_embedding_mode:
    '''各层是否共享embedding和softmax权重?'''
    share_embedding_and_softmax_weights: false
     '''logits的保留率 1.0为不进行dropout'''
    dropout_logit_keep_prob: 1.0
    '''损失函数'''
    loss: crossentropy
    timing:
'''初始激活值'''
  initializer: random_uniform
  ...

1.1 Embedding

Embedding(词嵌入)到底是什么
神经网络中的embedding层的参数是一个矩阵,跟整个模型一起训练得来。矩阵乘以某个词的独热向量得到这个词的词向量表示。

NJUNMT中用的是经过n次封装后的tf.nn.embedding_lookup()
详解TF中的Embedding操作!

1.1 Encoder

seq2seq模型的encoder配置示例:

model_params:
  ...
  """指定encoder类型"""
  encoder.class: njunmt.encoders.BiUnidirectionalRNNEncoder
  encoder.params:
    rnn_cell:
      """指定rnn cell类型,可以是LSTMCell/GRUCell"""
      cell_class: GRUCell
      cell_params:
      """每个cell中神经元的数量"""
      num_units: 9
      """input 的保留率 1.0为不进行dropout"""
      dropout_input_keep_prob: 0.9
      """input 的保留率 1"""
      dropout_state_keep_prob: 1.0
      """RNN层数"""
      num_layers: 2
    ...

seq2seq模型使用RNN encoder。njunmt/encoders/rnn_encoder.py中定义了三种encoder类型:
UnidirectionalRNNEncoder:单向多层RNN
StackBidirectionalRNNEncoder:双向多层RNN
BiUnidirectionalRNNEncoder:底层为双向RNN,上层为双向多层RNN
*疑问:为什么要使用这样的RNN架构?

Encoder类均包含encode()方法:

def encode(inputs, sequence_length, **kwargs):
    outputs, states = tf.nn.dynamic_rnn(...)
    return (outputs, final_states)

tf.nn.dynamic_rnn()方法运行已创建RNN cell,dynamic在于不同batch传入的sequence_length可以不同,每一个batch内动态进行padding.

Sequence2Sequence类中的_encode()方法:
对input进行embedding,然后调用Encoder的encode()方法

    def _encode(self, input_fields):
        features = self._input_to_embedding_fn(...)
        encoder_output = self._encoder.encode(features, feature_length)
        return encoder_output

1.2 Bridge

衔接encoder final state和decoder initial state的东西。
njunmt/utils/bridges.py中定义了4种Bridge:
ZeroBridge:decoder initial state 为全0
PassThroughBridge:直接将encoder final state传给decoder
InitialStateBridge:用一层全连接网络对encoder final state进行映射再传给decoder
Variable Bridge:通过学习决定decoder initial state

1.3 Decoder

seq2seq模型使用RNN decoder。njunmt/decoders/rnn_decoder.py中定义了三种decoder类型:
SimpleDecoder:多层RNN,无Attention机制
AttentionDecoder:多层RNN,使用concat(decoder_input, attention_context)作为输入
CondAttentionDecoder:底层是使用decoder_input作为输入的单层RNN,上层是使用attention_context作为输入的多层RNN

Decoder类的decode()方法主要是调用了dynamic_decode()方法:

def dynamic_decode(decoder,
                   encoder_output,
                   bridge,
                   helper,
                   target_to_embedding_fn,
                   outputs_to_logits_fn,
                   parallel_iterations=32,
                   swap_memory=False,
                   **kwargs):
     ...
    '''一些初始化,包括把target input 变成embedding'''

    with tf.variable_scope(decoder.name):
        '''调用decoder.prepare()方法:
        1. 用bridge初始化decoder的states,
        2. 从encoder_output获得attention(如果需要)'''
        initial_cache = decoder.prepare(encoder_output, bridge, helper)  
        if decoder.mode == ModeKeys.INFER:
            assert "beam_size" in kwargs
            beam_size = kwargs["beam_size"]
            initial_cache = stack_beam_size(initial_cache, beam_size)

    """while_loop中的循环体"""
    def body_traininfer(time, inputs, cache, outputs_ta,
                        finished, *args):

        with tf.variable_scope(decoder.name):
            '''调用decoder.step()方法'''
            outputs, next_cache = decoder.step(inputs, cache)
        outputs_ta = nest.map_structure(lambda ta, out: ta.write(time, out),
                                        outputs_ta, decoder_output_remover.apply(outputs))
        inner_loop_vars = [time + 1, None, None, outputs_ta, None]
        sample_ids = None
        if decoder.mode == ModeKeys.INFER:
            log_probs, lengths = args[0], args[1]
            bs_stat_ta = args[2]
            predicted_ids = args[3]
            with tf.variable_scope(self.name):
                decoder_top_features = self.merge_top_features(ret_val)
            logits = outputs_to_logits_fn(decoder_top_features)
            # sample next symbols
            sample_ids, beam_ids, next_log_probs, next_lengths \
                = helper.sample_symbols(logits, log_probs, finished, lengths, time=time)
            predicted_ids = gather_states(tf.reshape(predicted_ids, [-1, time + 1]), beam_ids)

            next_cache["decoding_states"] = gather_states(next_cache["decoding_states"], beam_ids)
            bs_stat = BeamSearchStateSpec(
                log_probs=next_log_probs,
                beam_ids=beam_ids)
            bs_stat_ta = nest.map_structure(lambda ta, out: ta.write(time, out),
                                            bs_stat_ta, bs_stat)
            next_predicted_ids = tf.concat([predicted_ids, tf.expand_dims(sample_ids, axis=1)], axis=1)
            next_predicted_ids = tf.reshape(next_predicted_ids, [-1])
            next_predicted_ids.set_shape([None])
            inner_loop_vars.extend([next_log_probs, next_lengths, bs_stat_ta, next_predicted_ids])

        next_finished, next_input_symbols = helper.next_symbols(time=time, sample_ids=sample_ids)
        next_inputs = target_to_embedding_fn(next_input_symbols, time + 1)

        next_finished = tf.logical_or(next_finished, finished)
        inner_loop_vars[1] = next_inputs
        inner_loop_vars[2] = next_cache
        inner_loop_vars[4] = next_finished
        return inner_loop_vars

    loop_vars = [initial_time, initial_inputs, initial_cache,
                 initial_outputs_ta, initial_finished]

    if decoder.mode == ModeKeys.INFER:  
        ...'''增加一些infer需要的参数'''

    '''while(cond(args)) loop(body(args))
    cond: tf.logical_not(tf.reduce_all(initial_finished))
    tf.logical_not:布尔值(组成的张量)not运算
    tf.reduce_all:计算张量在维度上的逻辑和(按某轴向全部取and)
    finished为布尔张量 记录什么东西结束了
    循环到所有finished标记为真为止'''
    conf = lambda *args: tf.logical_not(tf.reduce_all(args[4]))
    res = tf.while_loop(
        conf,
        body_traininfer,
        loop_vars=loop_vars,
        parallel_iterations=parallel_iterations,
        swap_memory=swap_memory)

    ...'''从res中取出final_outputs'''

    if decoder.mode == ModeKeys.INFER:
        ...'''return final_outputs和一些state'''

    return final_outputs

decoder output-(merge)->logits-(softmax)->
*这部分比较复杂,还有很多没看懂

Sequence2Sequence类中的_decode()方法:

    def _decode(self, encoder_output, input_fields):

        if '''mode == TRAIN or EVAL''':
            ...'''准备label和helper'''
        else:  ''' mode == INFER'''
            ...'''准备helper'''

        '''调用Decoder的decode()方法'''
        decoder_output, decoding_res = self._decoder.decode(
            encoder_output, bridge, helper,
            self._target_to_embedding_fn,
            self._outputs_to_logits_fn,
            beam_size=...)
        return decoder_output, decoding_res

1.3 Attention

如果不使用注意力机制,那么decoder在每一个时间片使用的都是同一个上下文向量(Context),即encoder output。
在有注意力机制的seq2seq模型中,针对decoder的每个时间片,有不同的Context,每个Context分配给encoder隐藏层状态(h)的各时间片的权重不同。


context计算公式.png

注意力机制使得decoder第i个时间片的输出不仅仅关注相对应的encoder第j个时间片的隐藏层状态,还可以将前后的隐藏层状态以一定权重输入进来。

上图中的α即attention weight,根据encoder的隐藏层状态(h)和decoder的隐藏层状态(H)得到。在不同种类的注意力机制中,attention weight有不同的计算方法。

1.3.1 加法注意力

BahnandauAttention中使用的是加法注意力:用一个全连接层得到attention score(logits,即下图中的e),然后softmax得到attention weight(下图中的α)

image.png

另一种常见的字母表达方式:把上式中decoder隐藏层状态H替换为Q(query),encoder隐藏层状态h替换为K(keys),即为:

attention_weight = softmax(tanh(W1*Q+W2*K))

1.3.2 乘法注意力

其他注意力模型中用到的是乘法注意力:乘法注意力不用使用一个全连接层,所以空间复杂度占优;另外由于乘法可以使用优化的矩阵乘法运算,所以计算上也一般占优。


乘法注意力.png

把上式中decoder隐藏层状态H替换为Q(query),encoder隐藏层状态h替换为K(keys),即为:

attention_weight = softmax(dot_product(Q, K))

Attention的看这里不要看上面
[深度概念]·Attention机制实践解读

#in encoder
EO, _ = encoder(...)
attention_values = EO
attention_keys = FC(EO)

#in decoder
attention_query = cell_output
#in attention.build()
query = FC(attention_query)
keys = attention_keys
memory = attention_values
score = att_fn(query, keys)#att_fn可以是加法乘法等
attention_weight = softmax(score)
context = attention_weight * memory
#then use context to infer
image.png

关于注意力的计算详细:
提出Transformer的《Attention is all you need》论文原文
《attention is all you need》解读

2. Transformer

Transformer模型.png

Transformer模型简介

*以下来自:Transformer for NMT
总的来说,模型由encoder&decoder两部分组成,左侧的Stage 2,3构成一层的encoder,右侧的Stage 2,3,4构成一层decoder, 在每一层中的每个Stage称为一个子层(sublayer). 在transformer中,encoder和decoder同样可以堆叠N个来构建deeper model,论文中使用了6个encoders和6个decoders. 除此之外再加入输入embedding,位置编码(positional encoding)和输出的dense layer,就是完整的transformer. 下面我们具体地分析各部分结构。

*Transformer很复杂,还没仔细看

二、运行过程

2.1 一些TF基础知识

tensorflow中的Graph(图)和Session(会话)的关系(大盘鸡与红烧肉)

TensorFlow 中的几个关键概念:Tensor,Operation,Graph,Session

tensorflow中的Session()和run()
Hook? tf.train.SessionRunHook()介绍

*一个相似完整流程参考
[tf]使用attention机制进行NMT

NJUNMT
bin/train.py中实例化了一个TrainingExperiment对象,带入模型配置运行训练。

    training_runner = TrainingExperiment(
        model_configs=model_configs)

    training_runner.run()

TrainingExperiment的run()方法定义:

#njunmt/nmt_experiment.py
 def run(self):
        """ 建立源语言与目标语言词汇表"""
        vocab_source = Vocab(...)
        vocab_target = Vocab(...)
        eval_dataset = {
            "vocab_source": vocab_source,
            "vocab_target": vocab_target,
            "features_file": self._model_configs["data"]["eval_features_file"],
            "labels_file": self._model_configs["data"]["eval_labels_file"]}

        config = tf.ConfigProto()
        config.gpu_options.allow_growth = True
        config.allow_soft_placement = True

        """model_fn()建立模型"""
        estimator_spec = model_fn(...)
        train_ops = estimator_spec.train_ops
        hooks = estimator_spec.training_hooks

       """build training session"""
        sess = tf.train.MonitoredSession(
            session_creator=tf.train.ChiefSessionCreator(
                scaffold=tf.train.Scaffold(),
                checkpoint_dir=None,
                master="",
                config=config),
            hooks=tuple(hooks) + tuple(build_eval_metrics
                                       (self._model_configs, eval_dataset,
                                        model_name=estimator_spec.name)))

        ...
        """somehow 得到处理好的train data"""
        train_data = ...

        eidx = [0, 0]
        update_cycle = [self._model_configs["train"]["update_cycle"], 1]#[目标cycle数,当前cycle数]

        def step_fn(step_context):
            step_context.session.run(train_ops["zeros_op"])
            try:
                while update_cycle[0] != update_cycle[1]:
                    data = train_data.next()
                    step_context.session.run(
                        train_ops["collect_op"], feed_dict=data["feed_dict"])
                    update_cycle[1] += 1
                data = train_data.next()
                update_cycle[1] = 1
                return step_context.run_with_hooks(
                    train_ops["train_op"], feed_dict=data["feed_dict"])
            except StopIteration:
                eidx[1] += 1

        while not sess.should_stop():
            if eidx[0] != eidx[1]:
                tf.logging.info("STARTUP Epoch {}".format(eidx[1]))
                eidx[0] = eidx[1]
            sess.run_step_fn(step_fn)

*疑问:这里的sess.run_step_fn(step_fn)是怎么运行的?tf.train.MonitoredSession()类中没有查到run_step_fn()方法

关于tf.train.MonitoredSession()
官方文档给的定义是:
Session-like object that handles initialization, recovery and hooks.
是一个处理初始化,模型恢复,和处理Hooks的类似于Session的类。

关于这里的Experiment,training_hooks都还缺乏了解

关于tf.estimator
Estimator对象包装由model_fn指定的模型,其中,给定输入和其他一些参数,返回需要进行训练、计算,或预测的操作.
EstimatorSpec是定义model_fn返回的类(继承自namedtuple),用于初始化Estimator

#njunmt/model/model_builder.py
class EstimatorSpec(
    namedtuple('EstimatorSpec', ['name', 'input_fields',
                                 'predictions', 'loss', 'train_ops',
                                 'training_chief_hooks', 'training_hooks'])):

"""创建NMT模型,根据train,evaluation,inference三种mode具体有不同的返回内容"""
def model_fn(...):
    ...
    """创建模型实例"""
    model = eval(model_str)(
        params=model_configs["model_params"],...)
    ...
    """调用各Model类的build()方法"""
    def _build_model():
        ...
        if mode == ModeKeys.INFER:
            # model_output is prediction
            return _input_fields, _model_output
        elif mode == ModeKeys.EVAL:
            # model_output = (loss_sum, weight_sum), attention
            return _input_fields, _model_output[0], _model_output[1]
        elif mode == ModeKeys.TRAIN:  # mode == TRAIN
            ..."""something for train"""
            return _input_fields, _loss, grads
        ...

    model_returns = parallelism(_build_model)
    input_fields = model_returns[0]
    if mode == ModeKeys.INFER:
        ..."""something for inference"""
        return EstimatorSpec(...)

    if mode == ModeKeys.EVAL:
        ... """something for evaluation"""
        return EstimatorSpec(...)

    assert mode == ModeKeys.TRAIN
    ..."""something for train"""
    return EstimatorSpec(...)

三、调参基础

机器学习中的正则化(Regularization)
Normalization(归一化)
《理解Dropout》分享
Seq2Seq中的beam search算法
Tensorflow中优化器--AdamOptimizer详解

激活函数.png

TensorFlow之RNN:堆叠RNN、LSTM、GRU及双向LSTM
[深度概念]·Attention机制实践解读
变形金刚”为何强大:从模型到代码全面解析Google Tensor2Tensor系统

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354