一、概述
先上图,图来自 李宏毅老师的《Deep Learning for Human Language Processing》课程
核心包括三个部分 Encoder, Attention, Decoder, 其中Attention的实现方式各种各样,也是大家重点研究的对象。 Encoder 和 Decoder的使用和实现方式就比较通用了。并且也都是用到RNN结构。
二、RNN
RNN 实际上和Conv 的本质上是相同, 拿(None,Width, Height, Channel) 结果来说, 就是将 w,h 空间上的信息提取到channel 维度上。换语之,rnn的操作就是将 t 时间维度上的信息,变成有限容量的信息。
每个时刻的输入对应一个 $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 层后的,数据是什么样子的?接图:
数据使用向量表示,并采用欧式距离表示数据相关性, 可视化的结果,就是相关的数据聚合在某个区域。
四、构建模型
上面讲述基础的单元,接下来补充一下, 更大的基础结构,==模型==。
- 编码器模型
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 层,入下图。
以头尾相接的形式,也就是上一个时刻的输出为下一个时刻的输入。变换如下
(None, 1, embeding_dim + content_vector_dim) => (None, 1)
进过N轮后,或者超过最大限制长度,循环结束。输出的shape 经过 concat处理得到 (None, N), 通过查表可得到对应的字符
- Attention
该层是变化最多的层, 经典的有: Luong Attention 和 Bahdanau Attention, 这里我只讲解 Bahdanau Attention
结构图
解码器的 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即可。
️五、注意事项
- 训练文本需要标记 start end 符号, 使得模型知道何时输入开始和输出结束。
- 训练时间比较长,使用小数据确认loss下降后,再去训练。
六、附加
请右键下载后使用