基于LAS模型的聊天机器人解读

一、概述

先上图,图来自 李宏毅老师的《Deep Learning for Human Language Processing》课程

image

核心包括三个部分 Encoder, Attention, Decoder, 其中Attention的实现方式各种各样,也是大家重点研究的对象。 Encoder 和 Decoder的使用和实现方式就比较通用了。并且也都是用到RNN结构。

二、RNN

RNN 实际上和Conv 的本质上是相同, 拿(None,Width, Height, Channel) 结果来说, 就是将 w,h 空间上的信息提取到channel 维度上。换语之,rnn的操作就是将 t 时间维度上的信息,变成有限容量的信息。

image

每个时刻的输入对应一个 $x_t$, RNN是参数共享, 每一个时刻通过的都是同一个神经元。公式如下

一个segment 结束后, 同时有两个输出 y_t, h_t,下一个时刻 h_t 作为新的输入经过 σh 后输出下一个时刻的 y{t+1}, h_{t+1}, 以此类推。

RNN结构看似能完美解决了时间维度上的体征提取, 但同时存在诸多问题,例如:

无法长时记忆,RNN 对于短的序列可以有效记录相关信息,较长序列时,因为特征是通过简单的累加操作,当序列中存在较多相似的特征时,就容易被覆盖。

超参调教困难,RNN 存在递归相乘的结构 h_t = σh(W_h x_t + U_h y{t-1} + b_h),进行倒数运算的时候,就会发现, 存在 W^{t-1} 项, 意味着什么的,超参选取不合理的时候, 在进行反向梯度传播的时候,即便时较小的变动也会造成网络参数的巨大波动,梯度爆炸 , 或者 梯度拟散。

如何解决呢? 使用RNN相应的变种 lstm 或 gru 都可以很好的解决~

三、Embedding 编码

现实世界存在很多具有相关性的数据,以词汇为例: “哈士奇”,“萨摩耶”, “英短”, “加菲”, 如果以简单的 one_hot 进行处理那么上面的数据将被处理成: 000, 100, 010,001, 不经加大了运算量,同时破坏了数据相关性。

那么通过 Embedding 层后的,数据是什么样子的?接图:

image

数据使用向量表示,并采用欧式距离表示数据相关性, 可视化的结果,就是相关的数据聚合在某个区域。

四、构建模型

上面讲述基础的单元,接下来补充一下, 更大的基础结构,==模型==。

  • 编码器模型

class Encoder(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
        super(Encoder, self).__init__()
        self.batch_sz = batch_sz
        self.enc_units = enc_units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = tf.keras.layers.GRU(self.enc_units, return_sequences=True, return_state=True,
                                       recurrent_initializer='glorot_uniform')

    def call(self, x, hidden):
        x = self.embedding(x)
        output, state = self.gru(x, initial_state=hidden)
        return output, state

    def initialize_hidden_state(self):
        return tf.zeros((self.batch_sz, self.enc_units))

一个批次的数据的 shape 通过, embeding层 和 rnn 层后, 相应的shape 的变换为。

i: embeding 层变换

(None, 20) => (None, 20 , embeding_dim)

ii:RNN 层变

RNN 输出两个Tensor, (None, enc_units) 为最后一个经过RNNCell的状态, 可以认为是整句话的语境, (None, 20, enc_uints) 为每个时刻输出的状态。

(None, 20 , embeding_dim) => (None, enc_units), (None, 20, enc_uints)
  • 解码器模型

class Decoder(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
        super(Decoder, self).__init__()
        self.batch_sz = batch_sz
        self.dec_units = dec_units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = tf.keras.layers.GRU(self.dec_units,
                                       return_sequences=True,
                                       return_state=True,
                                       recurrent_initializer='glorot_uniform')
        self.fc = tf.keras.layers.Dense(vocab_size)

        self.attention = BahdanauAttention(self.dec_units)

    def call(self, x, hidden, enc_output):
        # 解码器 hidden 和 编码器 output 输入dao Attention中,hidden (128, 256),  enc_output (128, 20, 256)
        context_vector, attention_weights = self.attention(hidden, enc_output)

        x = self.embedding(x)

        x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)

        output, state = self.gru(x)

        output = tf.reshape(output, (-1, output.shape[2]))

        x = self.fc(output)

        return x, state, attention_weights

解码器的接口在本质上和编码器相似, 不一样的是,解码器的RNN输入是来自上一个 RNN 输出,并且在首次输入的时候都是一个固定的标志符 "start" . 拥有start 标志符还不够, 还需要结合编码器的输出输出的语境。

当前时刻输入 = start标志符 +  语境 vector

i. embeding 层变换

此部分和编码器一摸一样, 不同的是,输入的数量是一个字符表示启动。
(None, 1) => (None, 1, embeding_dim)

ii. 语境结合

content_vector 来自于Atention Model, 下面会讲到。


(None, 1, embeding) + boradcat(context_vector) ==> (None, 1, embeding_dim + content_vector_dim)
iii. RNN 层变换

经过 attention 加持过的 输出,需要再次经过RNN 层,入下图。

image

以头尾相接的形式,也就是上一个时刻的输出为下一个时刻的输入。变换如下

(None, 1, embeding_dim + content_vector_dim) => (None, 1)

进过N轮后,或者超过最大限制长度,循环结束。输出的shape 经过 concat处理得到 (None, N), 通过查表可得到对应的字符

  • Attention

该层是变化最多的层, 经典的有: Luong Attention 和 Bahdanau Attention, 这里我只讲解 Bahdanau Attention

结构图

image

解码器的 t-1 时刻输出 和 编码器全部时刻输出, 经过一个 a_t 变换后,得到 attention_weight 权重, attention_weight 和 编码器全部输出再次经过 一个 c_t变换 等到 t时刻的 content_vector。shape的变化如下注释所示。

class BahdanauAttention(tf.keras.Model):
    def __init__(self, units):
        super(BahdanauAttention, self).__init__()
        self.W1 = tf.keras.layers.Dense(units)
        self.W2 = tf.keras.layers.Dense(units)
        self.V = tf.keras.layers.Dense(1)

    def call(self, query, values):
        # hidden shape == (batch_size, hidden size)
        # hidden_with_time_axis shape == (batch_size, 1, hidden size)
        # we are doing this to perform addition to calculate the score
        # (128, 256) => (128, 1,  256)
        hidden_with_time_axis = tf.expand_dims(query, 1)

        # (128, 20, 256) x (256, 256)  + (128, 1, 256) x (256, 256) => (128, 20, 256) + (128, 20, 256) => (128, 20, 256)
        # fc 层处理 (128, 20, 256) x (256, 1) => (128, 20, 1)
        # score shape == (batch_size, max_length, hidden_size)
        score = self.V(tf.nn.tanh(
            self.W1(values) + self.W2(hidden_with_time_axis)))

        # softmax 层处理到 0 ~ 1
        # attention_weights shape == (batch_size, max_length, 1)
        # we get 1 at the last axis because we are applying score to self.V
        attention_weights = tf.nn.softmax(score, axis=1)

        # (128, 20, 1) x (128, 20, 256) => (128, 20, 256)
        # context_vector shape after sum == (batch_size, hidden_size)
        context_vector = attention_weights * values
        context_vector = tf.reduce_sum(context_vector, axis=1)

        return context_vector, attention_weights

注意上面的变化是某个时刻的变换, 而不是群全部时刻的, 所以如果想的全部时刻的变化, 将生成多个时刻的 content_vector。

四、loss 计算

使用categorical_crossentropy 即可,如果出现梯度拟散, 计算loss的时候,使用batch的loss即可。

️五、注意事项

  1. 训练文本需要标记 start end 符号, 使得模型知道何时输入开始和输出结束。
  2. 训练时间比较长,使用小数据确认loss下降后,再去训练。

六、附加

请右键下载后使用

视频及其代码

你可能感兴趣的:(基于LAS模型的聊天机器人解读)