上一章我们学习了seq2seq,它的本质是由编码器和译码器组合成的模型,我们也介绍了他的两点改进,但是这两点改进都是属于小改进,本章将会学习一种彻底的改进措施Attention方法。

8.1 Seq2Seq存在的问题

上一章学习的译码器无论多上的输入序列它都会将他们转化为一个固定长度的隐藏状态,这样其实很不合理,比如一个1000字的文章和50字的文章都被压缩成一个10个元素构成隐藏状态,这样长文章就会有很多信息丢失,这就是第一点不合理的地方。
在这里插入图片描述
还有就由于前面介绍的LSTM是单向传递的,所以就存在某一个词它包含的单词隐含信息存在不对称性,比如刚才的“猫”向量中含有“吾輩”“は”“猫”这 3 个单词的信息。考虑整体的平衡性,最好均衡地含有单词“猫”周围的信息。在这种情况下,从两个方向处理时序数据的双向RNN(或者双向 LSTM)比较有效。
在这里插入图片描述

8.2 编码器的改进

针对前面提出的两点问题我们需要改进,后者我们的改进如前图我们可以构造双向的LSTM,但是对于一个问题,我们的改进就是针对文本的长度来构建一个隐藏向量的集合,这样我们就能保证不落下单词信息了。
在这里插入图片描述

8.3 解码器的改进

8.3.1 解码器的改进①

针对编码器改进后的输出,我们解码器也需要进行相应的改进,思考我们在进行翻译时,大脑做了什么呢?比如,在将“吾輩は猫である”这句话翻译为英文时,肯定要用到诸如“吾輩 = I”“猫 = cat”这样的知识。也就是说,可以认为我们是专注于某个单词(或者单词集合),随时对这个单词进行转换的。那么,我们可以在 seq2seq 中重现同样的事情吗?确切地说,我们可以让 seq2seq 学习“输入和输出中哪些单词与哪些单词有关”这样的对应关系吗?

在机器翻译的历史中,很多研究都利用“猫 =cat”这样的单词对应关系的知识。这样的表示单词(或者词组)对应关系的信息称为对齐(alignment)。到目前为止,对齐主要是手工完成的,而我们将要介绍的 Attention 技术则成功地将对齐思想自动引入到了 seq2seq 中。这也是从“手工操作”到“机械自动化”的演变。
在这里插入图片描述

从现在开始,我们的目标是找出与“翻译目标词”有对应关系的“翻译源词”的信息,然后利用这个信息进行翻译。也就是说,我们的目标是仅关注必要的信息,并根据该信息进行时序转换。这个机制称为 Attention,是本章的主题。

总结前面说的,就是我们要通过某种计算,找出与解码器输出相关的隐藏状态,比如,当解码器输出“I”时,从 h s \boldsymbol{hs} hs 中选出“吾輩”的对应向量。一下是原理图和代码表示:
在这里插入图片描述

class WeightSum:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None

    def forward(self, hs, a):
        N, T, H = hs.shape

        ar = a.reshape(N, T, 1).repeat(H, axis=2)
        t = hs * ar
        c = np.sum(t, axis=1)

        self.cache = (hs, ar)
        return c

    def backward(self, dc):
        hs, ar = self.cache
        N, T, H = hs.shape

        dt = dc.reshape(N, 1, H).repeat(T, axis=1) # sum的反向传播
        dar = dt * hs
        dhs = dt * ar
        da = np.sum(dar, axis=2) # repeat的反向传播

        return dhs, da

8.3.2 解码器的改进②

前面的改进涉及一个重要权重 a \boldsymbol{a} a,那么我们如何求这个权重就是本节讨论的重点,这个权重的求解我们通过求解码器各个隐藏状态同预测单词的内积然后在正规化进行求解:
在这里插入图片描述
在这里插入图片描述
他的整理运算代码和流程为:

class AttentionWeight:
    def __init__(self):
        self.params, self.grads = [], []
        self.softmax = Softmax()
        self.cache = None

    def forward(self, hs, h):
        N, T, H = hs.shape

        hr = h.reshape(N, 1, H).repeat(T, axis=1)
        t = hs * hr
        s = np.sum(t, axis=2)
        a = self.softmax.forward(s)

        self.cache = (hs, hr)
        return a

    def backward(self, da):
        hs, hr = self.cache
        N, T, H = hs.shape

        ds = self.softmax.backward(da)
        dt = ds.reshape(N, T, 1).repeat(H, axis=2)
        dhs = dt * hr
        dhr = dt * hs
        dh = np.sum(dhr, axis=1)

        return dhs, dh

在这里插入图片描述

8.3.2 解码器的改进③

有了前面两个层只需要将两个层连接起来就成了我们的Attention:
在这里插入图片描述

class Attention:
    def __init__(self):
        self.params, self.grads = [], []
        self.attention_weight_layer = AttentionWeight()
        self.weight_sum_layer = WeightSum()
        self.attention_weight = None

    def forward(self, hs, h):
        a = self.attention_weight_layer.forward(hs, h)
        out = self.weight_sum_layer.forward(hs, a)
        self.attention_weight = a
        return out

    def backward(self, dout):
        dhs0, da = self.weight_sum_layer.backward(dout)
        dhs1, dh = self.attention_weight_layer.backward(da)
        dhs = dhs0 + dhs1
        return dhs, dh

在这里插入图片描述
最后还需要将Attention实现为TimeAttention类:

class TimeAttention:
    def __init__(self):
        self.params, self.grads = [], []
        self.layers = None
        self.attention_weights = None

    def forward(self, hs_enc, hs_dec):
        N, T, H = hs_dec.shape
        out = np.empty_like(hs_dec)
        self.layers = []
        self.attention_weights = []

        for t in range(T):
            layer = Attention()
            out[:, t, :] = layer.forward(hs_enc, hs_dec[:,t,:])
            self.layers.append(layer)
            self.attention_weights.append(layer.attention_weight)

        return out

    def backward(self, dout):
        N, T, H = dout.shape
        dhs_enc = 0
        dhs_dec = np.empty_like(dout)

        for t in range(T):
            layer = self.layers[t]
            dhs, dh = layer.backward(dout[:, t, :])
            dhs_enc += dhs
            dhs_dec[:,t,:] = dh

        return dhs_enc, dhs_dec

8.4 带Attention的Seq2Seq的实现

8.4.1 编码器的实现

class AttentionEncoder(Encoder):
    def forward(self, xs):
        xs = self.embed.forward(xs)
        hs = self.lstm.forward(xs)
        return hs

    def backward(self, dhs):
        dout = self.lstm.backward(dhs)
        dout = self.embed.backward(dout)
        return dout

8.4.2 解码器的实现

我们将Softmax with Loss实现为解码器:
在这里插入图片描述

class AttentionDecoder:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        affine_W = (rn(2*H, V) / np.sqrt(2*H)).astype('f')
        affine_b = np.zeros(V).astype('f')

        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
        self.attention = TimeAttention()
        self.affine = TimeAffine(affine_W, affine_b)
        layers = [self.embed, self.lstm, self.attention, self.affine]

        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads

    def forward(self, xs, enc_hs):
        h = enc_hs[:,-1]
        self.lstm.set_state(h)

        out = self.embed.forward(xs)
        dec_hs = self.lstm.forward(out)
        c = self.attention.forward(enc_hs, dec_hs)
        out = np.concatenate((c, dec_hs), axis=2)
        score = self.affine.forward(out)

        return score

    def backward(self, dscore):
        # 参照源代码

    def generate(self, enc_hs, start_id, sample_size):
        # 参照源代码

8.4.3 Seq2Seq的实现

class AttentionSeq2seq(Seq2seq):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        args = vocab_size, wordvec_size, hidden_size
        self.encoder = AttentionEncoder(*args)
        self.decoder = AttentionDecoder(*args)
        self.softmax = TimeSoftmaxWithLoss()

        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads

8.5 带 Attention 的 seq2seq 的学习

# 读入数据
(x_train, t_train), (x_test, t_test) = sequence.load_data('date.txt')
char_to_id, id_to_char = sequence.get_vocab()
# 反转输入语句
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]

# 设定超参数
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 256
batch_size = 128
max_epoch = 10
max_grad = 5.0

model = AttentionSeq2seq(vocab_size, wordvec_size, hidden_size)
optimizer = Adam()
trainer = Trainer(model, optimizer)

acc_list = []
for epoch in range(max_epoch):
    trainer.fit(x_train, t_train, max_epoch=1,
                batch_size=batch_size, max_grad=max_grad)

    correct_num = 0
    for i in range(len(x_test)):
        question, correct = x_test[[i]], t_test[[i]]
        verbose = i < 10
        correct_num += eval_seq2seq(model, question, correct,
                                    id_to_char, verbose, is_reverse=True)

    acc = float(correct_num) / len(x_test)
    acc_list.append(acc)
    print('val acc %.3f%%' % (acc * 100))


model.save_params()

以上便是整个学习流程,下面看评价:
在这里插入图片描述
可以强调的是Attention层的位置有很多,本章只是说明了一种最好理解的方式,其他的方式还有:
在这里插入图片描述

8.6 Seq2Seq 的深层化和 skip connection

同时为了提高精度我们还可以深化层,同时为了避免纵向的梯度小时,我们有残差连接(skip connection,也称为 residual connection 或 shortcut)法:
在这里插入图片描述
在这里插入图片描述

在时间方向上,RNN 层的反向传播会出现梯度消失或梯度爆炸的问题。梯度消失可以通过 LSTM、GRU 等 Gated RNN 应对,梯度爆炸可以通过梯度裁剪应对。而对于深度方向上的梯度消失,这里介绍的残差连接很有效。

总结

Attention的主要应用有神经机器翻译(Neural Machine Translation),去除RNN主义的Transformer模型和NTM,这里不再说明。

本章主要介绍了Attention,他是能关注输入和输出关系的方法,Attention 扩展了深度学习的可能性。Attention 是一种非常有效的技术,具有很大潜力。在深度学习领域,今后 Attention 自己也将吸引更多的“注意力”。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐