这是我看完很多博客和视频以后对LSTM和Attention Model的一个理解和总结,有一些公式的地方感觉在数学上不能严格的推导出来,就直接贴了流程图。自己能推导出来的,我用白话文字解释了公式的意思,尽量避免用一些难理解的词,有的地方举了些例子,如果不妥的话烦请指正。

1. LSTM(长短时记忆网络)

之前讲过了RNN循环神经网络,能解决神经网络中信息保存的问题。但是RNN还不够好,因为它存在一个长期依赖的问题,如果我需要依赖一个很久以前的网络层上的神经元信息,RNN会无法连接到很远的神经元的能力。即记忆容量有限。RNN和LSTM,它们各自的结构几乎一样。有细微区别。

如果你的矩阵的维度是固定的,它只能容纳一定的信息,当你给它的序列信息特别长,内容特别多的时候,它是无法通过多层之后还能保留下来的,换言之它记不住的。

RNN:

已知输入St,每个单元cell;每个cell做的事情是一样的:输入St,经过cell,对St更新,输出Ot。

LSTM:

LSTM的cell进行了优化,需要记住的东西会一直往下传,不需要的没用的内容不会记住,会被gate截断掉,以释放空间。关于LSTM说的最清楚的一个博客是英文版的,译文被CSDN老哥翻译了,我用了它的一些图,但内容没用他的,是自己总结的。如下,可以拿去参考:

https://www.cnblogs.com/wangduo/p/6773601.html?utm_source=itdadao&utm_medium=referral

 

原本的RNN的输入输出是这样的:

RNN的cell的内部结构是这样的:

记忆更新的部分圈起来了。而LSTM的cell的内部结构复杂了些:

下面的小图标分别是:

  • Neural network layer:神经网络层,可以看作一个个的非线性处理模块。
  • Pointwise operation:逐点运算,逐点运算举个逐点乘法的例子就是:0.5×[1., 2., 3.] = [0.5, 1.0, 1.5]。
  • Vector transfer:信息传递方向。
  • Concatenate:这个很好理解,信息的汇合。
  • Copy:与上面对应,信息的复制。

还有:ht是输出。

LSTM的关键:cell state 即细胞的状态。先讲它如何运作,再讲如何控制。

如何运作?我们只截取这一小部分,就是从上一时刻的记忆到这一时刻的记忆的一个过程。

就是它记忆的信息就在这条传送带上从后往前传,传送的时候会发生一些信息的交互,信息就在这上面一直保存。我可以在这条传送带上取值,也可以在上面输入值。 Ct是memory,也就是记忆。

如何控制cell state?我们会通过一个叫‘门’gate的东西来处理它,会给信息进行一个选择性的放行,来去除或者增加信息。先放结论:它包含着什么?包含一个sigmoid+一个pointwise。

先看sigmoid,它的输出结果会得出一个概率p,是0-1的一个值。我要做的就是一个信息的变更,到底让不让这部分信息,(前提:我的脑容量就这么大,只能记忆这么多东西,再多我记不住了)让他们接着往下存下去,还是说这部分记忆就没用了就更新了?(这里补充一下,忘记信息之后要更新)肯定要有个东西来控制它,这里我们控制的就是一个sigmoid,描述每个部分有多少量可以通过。这个概率可以和任何一个东西相乘,表示我允许你多大的量可以通过。0表示不需任何量通过,1表示允许任何量通过。

然后再看,LSTM是怎么样用这些门一起去串出来这样一个东西的。

第一步,我会决定从cell state中丢掉什么信息,这里有个‘忘记门’。比如我一开始填我导的职称是副教授,后面我导升成教授了,我好像下次填职称的时候要填教授了,所以我需要用忘记门来忘记旧的信息(忘记她是副教授),更新新的信息(下次再填的时候就填教授)。

所以我会用上一时刻的输出ht-1(这个编辑器输出不了下标啊???t-1是下标,大家凑合看)和现在的输入xt,来一起去决定我以多大的程度来忘记这个内容。б是个sigmoid。输出一个0-1的值,也就是ft要以多大的概率留下这个信息,这个式子产出一个概率值。W和b都是参数,会训练得到的。

第二步,我们把一部分信息用忘记门过滤掉了,就应该补一部分新的信息过来到cell state中,也就是刚才说的传送带上加什么信息。

需要两个值,it和Ct,它们一个是概率,一个是记忆。具体解释一下:it也是范围0-1的一个概率值p,it做的事就是对目前为止学到的所有信息做一个过滤,它的概率值表明,现在学到的哪部分新知识可以更新我之前的记忆的,也就是拿它过滤本次记忆。而本次学到的所有知识就是Ct。要把本次学到的信息,放到之前学到的所有信息中。所以用it这个概率,对Ct做一个过滤,补充到之前学习到的信息中。

第三步,Ct-1是旧的记忆,ft是旧的记忆的通过率,就是我们的gate门。it是本次信息的筛选器,Ct带波浪的是本次学到的知识。

从旧的概率里,以一个0-1的概率值,去保留下来我要的一部分信息,同时我把本次学习的信息筛选出一部分补到我之前学习的知识体系里,形成了现在的知识体系。(2014年出现了GRU,它简化了LSTM的运算,把忘记门和输入门合成了一个更新门,还有其他一些微小的改动,在这不详细总结了)

第四步,Ct是我更新以后之前所有学到的全部知识,(而我解决当前需解决的问题的时候,只需要某些知识,我只是要把我需要的知识筛选出来,于是我拿一个ot去筛选),ot依旧是0-1之间的概率p,它会从Ct所有知识里头Ct去筛选出来解决当前问题的信息,然后给出结果。

所以整个LSTM的更新和RNN是非常像的。唯一的区别是:我先确定我丢掉什么,我再确定我要添加什么,再把它加上,我再根据最后的记忆来产出结果。

讲完了LSTM的步骤,下面是要讨论的核心问题:为什么LSTM比RNN要有优势,能够缓解长时依赖的问题。

LSTM里比较重要的几个问题:

1.为什么用sigmoid,为什么不用relu?为什么不用tanh?

Sigmoid会饱和,确实,可能会带来问题。注意看公式,0-1才可以起到筛选的作用,如果用relu信息就会全过去了,信息它可能会爆炸,每层都会变大,下一层更大,就做不到把每层的信息都压缩到一个共同的范围了。

Relu是做不到的,它大于0的就全过去了,信息会越来越多。

用tanh的话,它双曲正切你想一下图像,它可能会出现一个负值,如-0.2,或者-0.5之类的,就会否定掉之前的全部的。

2.为什么LSTM比RNN更能解决长时依赖?

RNN怎么修改St的? St=tanh(WXt+USt-1)这是个复合函数,复合函数求偏导是连乘的形式,我用的是双曲正切,双曲正切在x偏大和偏小的时候斜率是接近于0的,所以接近于0会让反向传播的时候,St约等于0,rnn什么都学不到了,因为没有梯度传回来了,也就是没有梯度来校正参数W了。

而什么时候会出现≈0呢,就是链路非常长的时候,就可能带来梯度消失,也就是梯度弥散。这个时候求导的链式法则是让无数东西的梯度相乘,当有一个≈0时,整个式子的结果就会≈0,当我的链路越长时,越有可能出现≈0的情况。  

看LSTM,LSTM不是复合函数,是两个函数求和,f(x)+g(x)求偏导的话,得到的是两个偏导的和,即使有一个≈0,它不会导致整体约等于0。所以我的梯度往回传的时候是沿着两条轴往回传的。梯度不是顺着一条轴往回传的,LSTM最大的变化就是把RNN的连乘变成了求和,因此,不再会严重地出现梯度消失问题,这意味着即使时间再远,我应该也是学得到的。

以上的公式都不是必须只能这样,有很多变种(基本上没差异),但这篇总结写的是标准的那一种。

LSTM有很多很多的变种,举一个例子,有人会想,我去推断我要以多大程度去计算我要忘记什么东西,为什么只用上一次的输出,为什么不用之前的记忆Ct-1?其实就有这样的变种,可以把记忆的信息也拿进来和权重相乘。这是已经存在的一个变种了。(这里注意一个地方,向量拼接了以后,如果已存在一个1×100的向量,如果拼了3个这样的向量,它就是一个3×100的向量了,所以在运算的时候W的维度是需要改的)。RNN做损失函数的时候不用每个时间轴的损失都加起来,只求最后一个位置的损失就可以了。Google的大佬大概尝试了上万种RNN架构,发现并非所有任务上LSTM都表现得最好,所以它的变种不是拿一个就能随便用的。

 

2.Attention Model(注意力机制)

Attention Model的本质:

这个方法的灵感是来自人类本身的:我们的视觉在感知眼前场景的时候,不会每次都把一个场景内的所有东西全部看一遍,而是只看自己想看的那个东西。假如我想看狗,当我看到一次狗以后,我再在类似的别的地方看到狗时,我自然就会盯着那个狗看,而不太在意别的东西了。也就是,当我们学习到:这个场景中,在某一部分总是出现我最想看的那个东西,以后再学习时(再在相似的场景中学习时),我就会把注意力放到这部分上,尽量不去看其他部分,以提高效率。

它最核心的操作就是一串权重参数,要从序列中学习到每一个元素的重要程度,然后按重要程度将元素合并。权重参数就是一个注意力分配的系数,给哪个元素分配多少注意力。

大多数的方法都用Encoder-Decoder框架作为一个引子来引出了Attention Model,拿一个Encoder-Decoder的分心模型例子来说,假如在做翻译,比如输入的是英文Tom chase Jerry,它会生成中文单词:“汤姆”,“追逐”,“杰瑞”。

在翻译“杰瑞”这个中文单词的时候,这三个英文单词对翻译“杰瑞”的贡献都一样,这就不是我想要的,因为我想要“Jerry”啊,它才是翻译的关键,但是这个模型是无法体现这一点的,它没有引入Attention Model。这句话就仨词,还算短,没有出错,但如果特别长的话,基本上每个单词自身的信息量就消失了,也就必定丢失很多细节的信息了,效率也将大打折扣

如果引入Attention Model的话,注意力机制会给这三个单词分配不同的注意力大小,体现出来每个单词对翻译当前信息的影响程度,注意这里是每个,因为一句话的所有元素都得分配上。如,Tom:0.2,Chase:0.1,Jerry:0.7。所有注意力权重求和∑为1。这个数字也就是学习当前信息时,注意力机制分配给每个单词的注意力大小。引入了新的信息,因此会提高效率。

Attention函数生成注意力分配系数,它是用来得到attention value的。比较主流的Attention框架如图:

Q是给定目标中的某个元素Query,K是source中构成元素的(Key,Value)中的一部分,也就是Key,通过计算Q和各个K的相关性,得到每个K对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。(有几篇博客写到这里貌似有错,把Value加权求和理解成了把Value相加,正确的应该是对每个Value和它的权重相乘,再求和。即∑ω×Value)

Attention函数的工作步骤总结: 1.Q与K进行相似度计算得到权值;2.对上部权值归一化;3.用归一化的权值与V加权求和。此时加权求和的结果就为注意力值。具体计算过程如图:

还有一点,Attention Model它不是一个单独的模型,只是增加了新的信息,它的变种也没有提出一个新的网络层之类的定义来,所以它不应该被叫注意力模型,应该叫注意力机制,或者说给用到它的模型起个名叫用到了它的模型:Attention-based Model,基于注意力机制的模型。

单层、多层Attention Model以及它的变形和应用。对于某一个时序上的任务,如果能针对它提出合理的注意力机制,就可以提出更多的Attention Model变形。

附上一个@BabY虎子老师用Keras写的双向LSTM + Attention + Dropout实验,数据集是MNIST:

https://blog.csdn.net/u010041824/article/details/78855435

# mnist attention
import numpy as np
np.random.seed(1337)
from keras.datasets import mnist
from keras.utils import np_utils
from keras.layers import *
from keras.models import *
from keras.optimizers import Adam

TIME_STEPS = 28
INPUT_DIM = 28
lstm_units = 64

# data pre-processing
(X_train, y_train), (X_test, y_test) = mnist.load_data('mnist.npz')
X_train = X_train.reshape(-1, 28, 28) / 255.
X_test = X_test.reshape(-1, 28, 28) / 255.
y_train = np_utils.to_categorical(y_train, num_classes=10)
y_test = np_utils.to_categorical(y_test, num_classes=10)
print('X_train shape:', X_train.shape)
print('X_test shape:', X_test.shape)

# first way attention
def attention_3d_block(inputs):
    #input_dim = int(inputs.shape[2])
    a = Permute((2, 1))(inputs)
    a = Dense(TIME_STEPS, activation='softmax')(a)
    a_probs = Permute((2, 1), name='attention_vec')(a)
    #output_attention_mul = merge([inputs, a_probs], name='attention_mul', mode='mul')
    output_attention_mul = multiply([inputs, a_probs], name='attention_mul')
    return output_attention_mul

# build RNN model with attention
inputs = Input(shape=(TIME_STEPS, INPUT_DIM))
drop1 = Dropout(0.3)(inputs)
lstm_out = Bidirectional(LSTM(lstm_units, return_sequences=True), name='bilstm')(drop1)
attention_mul = attention_3d_block(lstm_out)
attention_flatten = Flatten()(attention_mul)
drop2 = Dropout(0.3)(attention_flatten)
output = Dense(10, activation='sigmoid')(drop2)
model = Model(inputs=inputs, outputs=output)

# second way attention
# inputs = Input(shape=(TIME_STEPS, INPUT_DIM))
# units = 32
# activations = LSTM(units, return_sequences=True, name='lstm_layer')(inputs)
#
# attention = Dense(1, activation='tanh')(activations)
# attention = Flatten()(attention)
# attention = Activation('softmax')(attention)
# attention = RepeatVector(units)(attention)
# attention = Permute([2, 1], name='attention_vec')(attention)
# attention_mul = merge([activations, attention], mode='mul', name='attention_mul')
# out_attention_mul = Flatten()(attention_mul)
# output = Dense(10, activation='sigmoid')(out_attention_mul)
# model = Model(inputs=inputs, outputs=output)

model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
print(model.summary())

print('Training------------')
model.fit(X_train, y_train, epochs=10, batch_size=16)

print('Testing--------------')
loss, accuracy = model.evaluate(X_test, y_test)

print('test loss:', loss)
print('test accuracy:', accuracy)

再附上一个我参考学习的@mpk_no1老师自己用keras实现的简单的静态Attention Model层的代码:

https://blog.csdn.net/mpk_no1

from keras import backend as K
from keras.engine.topology import Layer
from keras import initializers, regularizers, constraints
 
class Attention_layer(Layer):
    """
        Attention operation, with a context/query vector, for temporal data.
        Supports Masking.
        Follows the work of Yang et al. [https://www.cs.cmu.edu/~diyiy/docs/naacl16.pdf]
        "Hierarchical Attention Networks for Document Classification"
        by using a context vector to assist the attention
        # Input shape
            3D tensor with shape: `(samples, steps, features)`.
        # Output shape
            2D tensor with shape: `(samples, features)`.
        :param kwargs:
        Just put it on top of an RNN Layer (GRU/LSTM/SimpleRNN) with return_sequences=True.
        The dimensions are inferred based on the output shape of the RNN.
        Example:
            model.add(LSTM(64, return_sequences=True))
            model.add(AttentionWithContext())
        """
 
    def __init__(self,
                 W_regularizer=None, b_regularizer=None,
                 W_constraint=None, b_constraint=None,
                 bias=True, **kwargs):
 
        self.supports_masking = True
        self.init = initializers.get('glorot_uniform')
 
        self.W_regularizer = regularizers.get(W_regularizer)
        self.b_regularizer = regularizers.get(b_regularizer)
 
        self.W_constraint = constraints.get(W_constraint)
        self.b_constraint = constraints.get(b_constraint)
 
        self.bias = bias
        super(Attention_layer, self).__init__(**kwargs)
 
    def build(self, input_shape):
        assert len(input_shape) == 3
 
        self.W = self.add_weight((input_shape[-1], input_shape[-1],),
                                 initializer=self.init,
                                 name='{}_W'.format(self.name),
                                 regularizer=self.W_regularizer,
                                 constraint=self.W_constraint)
        if self.bias:
            self.b = self.add_weight((input_shape[-1],),
                                     initializer='zero',
                                     name='{}_b'.format(self.name),
                                     regularizer=self.b_regularizer,
                                     constraint=self.b_constraint)
 
        super(Attention_layer, self).build(input_shape)
 
    def compute_mask(self, input, input_mask=None):
        # do not pass the mask to the next layers
        return None
 
    def call(self, x, mask=None):
        uit = K.dot(x, self.W)
 
        if self.bias:
            uit += self.b
 
        uit = K.tanh(uit)
 
        a = K.exp(uit)
 
        # apply mask after the exp. will be re-normalized next
        if mask is not None:
            # Cast the mask to floatX to avoid float64 upcasting in theano
            a *= K.cast(mask, K.floatx())
 
        # in some cases especially in the early stages of training the sum may be almost zero
        # and this results in NaN's. A workaround is to add a very small positive number to the sum.
        # a /= K.cast(K.sum(a, axis=1, keepdims=True), K.floatx())
        a /= K.cast(K.sum(a, axis=1, keepdims=True) + K.epsilon(), K.floatx())
        print a
        # a = K.expand_dims(a)
        print x
        weighted_input = x * a
        print weighted_input
        return K.sum(weighted_input, axis=1)
 
    def compute_output_shape(self, input_shape):
        return (input_shape[0], input_shape[-1])

本部分的汤姆追杰瑞的例子,参考了CSDN转载的中科院软件所张俊林老师的讲解:

https://blog.csdn.net/TG229dvt5I93mxaQ5A6U/article/details/78422216

还看了大量Coursera上和deeplearning.ai上的视频和CSDN博客,不一一列出了,表达感谢。

还有一些表达的太过于口语化的地方,明天再改正一下。

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐