NLP教程笔记:语言模型的注意力

NLP教程

TF_IDF
词向量
句向量
Seq2Seq 语言生成模型
CNN的语言模型
语言模型的注意力


目录

自然语言模型注意力

如果说在视觉上,机器可以注意到某一个区域,那么在语言上,就是注意到某一个或多个词汇。 如果我们的任务不同,这些注意力可能会想去获取不同区域的词汇。我们举个例子
NLP教程笔记:语言模型的注意力
如果男生代表着一种属性的注意力模型,面对这样一长串销售语言,它注意到的就是关于性能和配置的信息。 你看,如果有了注意力,那么我们的生命是不是被节省了很多。这两个人类模型的目的可以是输出购买意向, 也可以是生成下一句回复销售的对话。我们就拿生成回复来细说一下模型是如何工作的。
NLP教程笔记:语言模型的注意力
首先模型得先通读一下这段文字,毕竟如果没有上下文的信息, 我们也不知道究竟要注意些什么。通读完之后,我们可以得到一个对于这句话的理解,熟悉AI的人应该知道这东西叫句向量。 单独靠一个句向量我们实际上是通过全局信息来生成对话,那么注意力是局部信息,我们可以将全局信息配合局部信息一起来生成每一个回复的词。

所以女生回复的“樱桃红”可以是注意到的“亮樱桃红色”这句销售话术,而“买它”则可能是注意到“千元”和“降价”促成的回复。 所以总结下来,深度学习的注意力,和我们人类的注意力生理机制也有那么异曲同工之处。

Seq2Seq Attention 注意力机制

视觉中,模型注意局部的图片信息,那么语言中,模型注意局部的文字信息。
NLP教程笔记:语言模型的注意力
上图是一个情感分析的简单例子,如果我要区分一句话的积极程度,我可能会注意到这句话中某些词语的积极程度,因为这些词对于我判断积极句子起到了很大作用。 这样的attention注意力就很好理解了。箭头越粗的部分,模型的注意力就越集中。如果是翻译的情景,那注意力就像下面这样
NLP教程笔记:语言模型的注意力
Attention 的方案有很多种,这个方法在 decoder 用“爱”预测时,他会用“爱”这里的 state,结合上 encoder 中所有词的信息,生成一个注意力权重,比如模型会更加注意 encoder 中的爱和莫烦。 然后再结合这个权重,再次把权重施加到 encoder 中的每个 state,这样就有了对于不同 state step 的不一样关注度,注意力的结果(context)也在这里产生。 接着,把 context 和 decoder 那边的信息再次结合,最终输出注意后的答案。
NLP教程笔记:语言模型的注意力
补充一些数学信息,论文中提出了三种计算attention score的方式。 这里的 h t h_t ht​ 是 decoder 见到输入生成的 hidden state, h s ^ \hat{h_s} hs​^​ 是 encoder 见到输入时生成的 hidden state。 意思是decoder每预测一个词, 我都拿着这个decoder现在的信息去和encoder所有信息做注意力的计算。三个公式的不同点就是是否要引入更多的学习参数和变量。

翻译

在这节内容中,还是以翻译为例。延续前几次用到日期翻译的例子, 我们知道在翻译的模型中,实际上是要构建一个Encoder,一个Decoder。这节内容我们就是让Decoder在生成语言的时候,也注意到Encoder的对应部分。

# 中文的 "年-月-日" -> "day/month/year"
"98-02-26" -> "26/Feb/1998"

例子就是将中文的日期形式转换成英文的格式,同时我们也会输出类似这样的注意力图,让我们知道在模型生成某些字的时候,它究竟依据的是哪里的信息。 x轴是中文的年月日,y轴是要生成的英文日月年。
NLP教程笔记:语言模型的注意力

代码

先来看训练过程, 整个训练过程一如既往的简单,生成数据,建立模型,训练模型。数据的生成是提前写好的utils.DateData()功能。

def train():
    # get and process data
    data = utils.DateData(2000)
    model = Seq2Seq(
        data.num_word, data.num_word, emb_dim=12, units=14, attention_layer_size=16,
        max_pred_len=11, start_token=data.start_token, end_token=data.end_token)

    # training
    for t in range(1000):
        bx, by, decoder_len = data.sample(64)
        loss = model.step(bx, by, decoder_len)

最后你能看到它的整个训练过程。最开始预测成渣渣,但是后面预测结果会好很多。你看刚训练几轮其实效果就已经很不错了,可见注意力的强大。

t:  0 | loss: 3.29403 | input:  89-05-25 | target:  25/May/1989 | inference:  00000000000
t:  70 | loss: 0.41608 | input:  03-09-13 | target:  13/Sep/2003 | inference:  13/Jan/2000<EOS>
t:  140 | loss: 0.01793 | input:  92-06-01 | target:  01/Jun/1992 | inference:  01/Jun/1992<EOS>
t:  210 | loss: 0.00309 | input:  23-01-28 | target:  28/Jan/2023 | inference:  28/Jan/2023<EOS>
...
t:  910 | loss: 0.00003 | input:  11-09-13 | target:  13/Sep/2011 | inference:  13/Sep/2011<EOS>
t:  980 | loss: 0.00003 | input:  06-08-10 | target:  10/Aug/2006 | inference:  10/Aug/2006<EOS>

模型构建时,在encoder部分其实和seq2seq的方法是没有什么变化的。 所以下面的代码和seq2seq也没有什么不同之处。

import tensorflow as tf
from tensorflow import keras
import numpy as np
import tensorflow_addons as tfa

class Seq2Seq(keras.Model):
    def __init__(self, ...):
        super().__init__()
        self.units = units

        # encoder
        self.enc_embeddings = keras.layers.Embedding()  # [enc_n_vocab, emb_dim]
        self.encoder = keras.layers.LSTM(units=units, return_sequences=True, return_state=True)

    def encode(self, x):
        o = self.enc_embeddings(x)
        init_s = [tf.zeros((x.shape[0], self.units)), tf.zeros((x.shape[0], self.units))]
        o, h, c = self.encoder(o, initial_state=init_s)
        return o, h, c   

但是在decoder生成后文的时候,decoder要和encoder的结果有很多的联动,会和encoder的o, h, c结果交叉在一起。 我们可以定义一个set_attention(x), 让模型关注到encoder后的信息。同时来让decoder记录从encoder来的信息和获取decoder的初始化state。

class Seq2Seq(keras.Model):
    def __init__(self, ...):
        ...
        self.attention = tfa.seq2seq.LuongAttention(units, memory=None, memory_sequence_length=None)
        self.decoder_cell = tfa.seq2seq.AttentionWrapper(
            cell=keras.layers.LSTMCell(units=units),
            attention_mechanism=self.attention,
            attention_layer_size=attention_layer_size,
            alignment_history=True,   # for attention visualization
        )
        ...

    def set_attention(self, x):
        o, h, c = self.encode(x)
        # encoder output for attention to focus
        self.attention.setup_memory(o)
        # wrap state by attention wrapper
        s = self.decoder_cell.get_initial_state(batch_size=x.shape[0], dtype=tf.float32).clone(cell_state=[h, c])
        return s

首先我们需要在__init__()中先定义好attention的方法和decoder的处理单元。 现在我们已经为decoding做好的准备,来自encoder的信息在set_attention()中被加工好,处理好了。 接下来就是我们训练encoder+decoder的过程。

class Seq2Seq(keras.Model):
    def __init__(self, ...):
        ...
        # decoder
        self.dec_embeddings = keras.layers.Embedding(
            input_dim=dec_v_dim, output_dim=emb_dim,  # [dec_n_vocab, emb_dim]
            embeddings_initializer=tf.initializers.RandomNormal(0., 0.1),
        )
        decoder_dense = keras.layers.Dense(dec_v_dim)

        # train decoder
        self.decoder_train = tfa.seq2seq.BasicDecoder(
            cell=self.decoder_cell,
            sampler=tfa.seq2seq.sampler.TrainingSampler(),   # sampler for train
            output_layer=decoder_dense
        )
        ...

    def train_logits(self, x, y, seq_len):
        s = self.set_attention(x)  
        dec_in = y[:, :-1]   # ignore <EOS>
        dec_emb_in = self.dec_embeddings(dec_in)
        o, _, _ = self.decoder_train(dec_emb_in, s, sequence_length=seq_len)
        logits = o.rnn_output
        return logits

train_logits()训练的步骤很简单,分几步。

  1. 拿到encoder的attention信息和state;
  2. 筛选出标签;
  3. 把标签在decoder中embed;
  4. 拿着所有缓存的 encoded state (attention memory) 和 encoder最后一步产生 state,放入decoder预测,得到所有的由加了注意力的output。
class Seq2Seq(keras.Model):
    def step(self, x, y, seq_len):
        with tf.GradientTape() as tape:
            logits = self.train_logits(x, y, seq_len)
            dec_out = y[:, 1:]  # ignore <GO>
            loss = self.cross_entropy(dec_out, logits)
            grads = tape.gradient(loss, self.trainable_variables)
        self.opt.apply_gradients(zip(grads, self.trainable_variables))
        return loss.numpy()

最后我们加上传统的训练方案,根据logits加label进行误差计算,并反向更新梯度,整个训练过程就完成了。

但是在预测的时候,和训练就不一样了。因为预测时没有标签信息,我们只能基于预测出来的词,再接着做后续的计算。

class Seq2Seq(keras.Model):
    def inference(self, x):
        s = self.set_attention(x)
        done, i, s = self.decoder_eval.initialize(
            self.dec_embeddings.variables[0],
            start_tokens=tf.fill([x.shape[0], ], self.start_token),
            end_token=self.end_token,
            initial_state=s,
        )
        # 记录每一次的预测结果
        pred_id = np.zeros((x.shape[0], self.max_pred_len), dtype=np.int32)

        # 循环获取每一步的预测
        for l in range(self.max_pred_len):
            o, s, i, done = self.decoder_eval.step(
                time=l, inputs=i, state=s, training=False)
            pred_id[:, l] = o.sample_id

        s.alignment_history.mark_used()  # otherwise gives warning 
        return pred_id

所以在预测时,我们使用到了一个Python的循环,不断生成self.max_pred_len这个多个数的预测结果, 然后用numpy array收集起来,作为最后的预测结果。

最后还写了一个可视化的功能 在翻译的时候attention的结果就如下。颜色深的地方就代表越注意。
NLP教程笔记:语言模型的注意力

思考

为什么我们在训练的时候,不把每次decoder出来的词当做下次预测的输入?

训练时,decoder的输入是真实标签信息。预测时,decoder的输入是上一步预测的值。 首先我们说预测的模式,因为缺少真实的label,我们只能拿着上一步预测的值,当成下一步预测的输入,这点非常明确清晰,没毛病。 但是在训练的时候我们为什么不也这么做呢?拿着标签来预测会不会让学习不连贯,效果不好呢?

其实判断一个训练的好坏,并不只是判断训练是否符合逻辑,当然如果用预测(inference)的方式做训练是符合逻辑的,而且的确是可以训练出来一个好结果的。 但是一般我们却不这么用,为什么呢?其实原因就是训练太慢了,如果现在我们训练一个小孩走路,你有两个方案。

  1. 他摔倒了,把它扶起来继续走路;
  2. 他摔倒了,我不管,让它从原地爬起来自己再走。

如果我们的目标是训练这个小孩的自理能力,那么我肯定选第二种,但是我们只想训练他走路的能力,不care他能不能自理,那我肯定选简单粗暴的第一种。 模型也一样,我只想训练他的生成话术的能力,但是我不care它的自动纠错能力,那么我还是直接用true label训练来的更快。

上一篇:树莓派ROS stm32 slam Freertos VFH+A*避障路径规划-智能平衡计划(四)


下一篇:transformer-encoder用于问答中的意图识别