之前一篇笔记中介绍了rnn的结构,前后向传播,并且通过python、tensorflow、keras实现了rnn,一番比较之后,笔者还是建议直接上手keras作为深度学习的工具,其他的工具了解就行了,对于原理其实也不必深究尤其是那些推导,只要你能用keras建模,能取得不错的效果就好了。但是这个梯度消失和梯度爆炸的问题,真的是很难去解决,然而世界不乏大神,一个神奇的东西出现了,那就是lstm,他为什么能解决梯度爆炸,那是因为他把梯度传导的那一相乘的一坨,变成了softmax函数,要么是0要么是1,是0就不传递,是1就尽量保持原状了,减少了梯度爆炸的产生。那么具体是怎么做到的呢,我们下面来看一下。
1 LSTM结构
直白点说,LSTM对原来RNN的细胞单元进行了更改,但并没有更改结构。LSTM增加了三个门用来控制信息传递和最后的结果计算,三个门分别教遗忘门、输入门、输出门,含义很明确了,遗忘门就是决定之前传来的数据哪些留下、哪些抛弃,输入门就是决定哪些东西能够输入,和原来的RNN有类似之处,这个门还有一个重要的使命,那就是对信息向下传递,大部分人称它为细胞状态更新,最后一层为输出门,顾名思义就是决定什么东西能输出。在LSTM中传递给下一层的是细胞状态ht,输出的也是ht,这个还需要大家注意一下。具体通过图示来看。
![](https://img.haomeiwen.com/i12070706/7b82a9ef33b3baf9.png)
1.1 遗忘门
遗忘门会读取前一刻的输出h(t-1)和这一刻的输入x(t),通过sigmoid函数生成一个0-1数,来决定哪个留下,哪个丢弃。这个遗忘门呢是个概率要乘到状态传递函数Ct上。公式如下:![](https://img.haomeiwen.com/i12070706/55a47407ae8b698c.png)
这里出现可第一个参数Wf,维度我不需多说,(hidden_dim+input_dim, output_dim)。
1.2 输入门
输入门也读取前一刻的输出和这一刻的输入,通过sigmoid函数生成一个概率来决定什么东西输入。当然如前所述,这里还有一个任务就是传递信息到下一层,输出需要更新细胞状态的中间变量,然后更新Ct。来看公式:
![](https://img.haomeiwen.com/i12070706/2d88808ab0745b7b.png)
这里出现了两个新的需要训练的参数Wi和WC,维度和Wf一样。接下来我们来更新细胞状态,公式如下:
![](https://img.haomeiwen.com/i12070706/ee420906a3ef271f.png)
这个状态跟新也是一个递归,跟前边所有的细胞状态有关,但是所有的变量我们都算出来了,只需要更新就行,这部分很好没有新的参数出现。
1.3 输出门
输出门要控制最后的输出了,当然也是读取了前一刻的输出和这一刻的输入,生成一个概率,然后乘上tanh激活后的细胞状态Ct就是这一刻的输出啦。公式如下:![](https://img.haomeiwen.com/i12070706/aa63d1ae6dcaabdb.png)
这里又出现了一个参数Wo,维度和之前都是相同的。
![](https://img.haomeiwen.com/i12070706/766437810265acb9.png)
好了,又多了一个参数V,这个东西维度(hidden_dim,output_dim),主要考虑一下输出,参数维度就好理解了。
分解以后,这个结构就很清楚了,大家对于LSTM千万不要死记硬背,跟着我的思路去想,拆开来看,这样就很简单了,而且有几个参数也是一目了然了。
2.LSTM的前后向传播
这里前向传播我就不讲了,根据上一篇RNN的原理,大家按照公式相乘相加就可以,初始的上一层输出定义为0向量就行。这里重点讲一下反向传播,这个也是解决梯度消失和梯度爆炸的关键。
这个LSTM和RNN的区别在于,多了一个细胞传递状态Ct,多了一个变量那么求导自然是多了一步,我们也需要对Ct求导,那么定义的求导中间变量就变成两个了,一个是对最后的ht,另外一个自然是对Ct了,这个不难理解吧。
再来说损失函数,损失函数是累加的,分两部分来看吧,当前输出的损失Lt和上一层传递下来的损失Lt+1,这个东西是导致RNN梯度消失的罪魁祸首啊,根据链式法则,求导的不断相乘叠加(有人说sigmoid和tanh的导数值就在0-1之间),不一会就趋近于0。然后推导的时候先求最后一层的导数,然后向前推导,推导的难点在于h(t+1)对h(t)求导,关系可谓错综复杂,太难了我也不想算了,大家直接看看刘建平的博客,你也不要纠结他怎么算的,你看看结论结论好了反向传播求导公式。最后对遗忘门参数求导的公式如下:
![](https://img.haomeiwen.com/i12070706/a9a0f5725fb9c696.png)
这是一个双重加法,首先那个就是一个累加,外边还要套一个累加。可能大家还是不理解到底LSTM是如何解决梯度消失的,那么我们直接看相乘的那一堆东西,无非就是时刻状态的累乘,首先是RNN,公式长这样:
![](https://img.haomeiwen.com/i12070706/2ee5c79e3b4107fd.png)
然后是LSTM,公式长这样:
![](https://img.haomeiwen.com/i12070706/eb0685602d46cfe3.png)
然后结论就一目了然了吧,第一个是一堆小于1的数相乘,第二个就不一样了,就是简单理解为尽量控制的数不是那么小,这样累乘也不会太小,其实起到梯度消失问题破解的关键就是他的结构,让误差传播的时候别那下降的那么快。再具体的理由初学者不用去深究了,如果工作中实在需要,那么你再去学习吧,不过这个真的非常难,推荐的方法是截断,还有就是前后设置传播距离。
3.LSTM的keras实现
为什么选择keras实现呢,因为keras简单啊。笔者会专门写一篇文章专门从宏观上讲讲keras怎么使用,当然当现实应用中遇到了比较重要的知识点,我也会写文章进行说明,原则就是用通俗的语言帮助大家理解。因为我在看大部分人写的文章时候发现他们都是照搬别人的话,自己理解不理解还不好说,我们再去看这样的文章自然看的是云里雾里,久而久之我们就要从入门到放弃了。跟上一篇讲RNN一样,我只讲思路,不给数据,如果需要你们可以拿到数据后把我的代码改改。
这里额外说一下keras中lstm要求的数据格式,如果选择tensorflow那么要求输入是三维张量,即[样本数,步长(可以理解为预测几期后的数据,一般都是1),特征数(隐藏细胞个数)]
import nltk
import numpy as np
from collections import Counter
from sklearn.model_selection import train_test_split
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.layers.recurrent import LSTM
from keras.layers.embeddings import Embedding
maxlen = 0
sample_num = 0
i = 0
MAX_FEATURE = 2000
MAX_WORD_LEN = 40
HIDDEN_SIZE = 64
EMBEDDING_SIZE = 128
BATCH_SIZE = 32
NUM_EPOCHS = 10
word_counter = Counter()
with open('/Users/elliot/Desktop/pythonlearn/python/rnn/lstm/train.txt') as f:
for line in f:
word = []
wtmp = line[1:].strip()
wtmp = nltk.word_tokenize(wtmp)
for wordtmp_1 in wtmp:
word.append(wordtmp_1)
wordcurrent = len(word)
if wordcurrent > maxlen:
maxlen = wordcurrent
for word_1 in word:
word_counter[word_1] += 1
sample_num += 1
vocab_size = min(MAX_FEATURE, len(word_counter)) + 2
word2index = {x[0]: i + 2 for i, x in enumerate(word_counter.most_common(MAX_FEATURE))}
word2index['UNK'] = 0
word2index['0'] = 1
index2word = {x: i for i, x in word2index.items()}
X = np.empty((sample_num, MAX_WORD_LEN), dtype=int)
y = np.zeros(sample_num)
i
with open('/Users/elliot/Desktop/pythonlearn/python/rnn/lstm/train.txt') as f:
for line in f:
label, sentence = line.strip().split('\t')
y[i] = int(label)
wtmp = nltk.word_tokenize(sentence)
j = 0
tmp = []
if len(wtmp) >= MAX_WORD_LEN:
for word in wtmp:
if j <= MAX_WORD_LEN - 1:
if word in word2index.keys():
tmp.append(word2index[word])
else:
tmp.append(word2index['UNK'])
else:
break
j = j + 1
else:
for word in wtmp:
if word in word2index.keys():
tmp.append(word2index[word])
else:
tmp.append(word2index['UNK'])
j += 1
for k in range(MAX_WORD_LEN - len(wtmp)):
tmp.append(word2index['0'])
j += 1
X[i, :] = tmp[:]
i += 1
Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, test_size=0.2, random_state=42)
model = Sequential()
model.add(Embedding(vocab_size, EMBEDDING_SIZE, input_length=MAX_WORD_LEN))
model.add(LSTM(HIDDEN_SIZE, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(Xtrain, ytrain, BATCH_SIZE, NUM_EPOCHS, validation_data=(Xtest, ytest))
score, acc = model.evaluate(Xtest, ytest, batch_size=BATCH_SIZE)
print('Test score is %.3f, accuracy is %.3f' % (score, acc))
print('{} {} {}'.format('预测', '真实', '句子'))
for i in range(5):
randidx = np.random.randint(len(Xtest))
xtest = Xtest[randidx].reshape(1, 40)
ylabel = ytest[randidx]
ypred = model.predict(xtest)[0][0]
sent = "".join([index2word[x] for x in xtest[0] if x != 0 and x != 1])
print('{} {} {}'.format(int(round(ypred)), int(ylabel), sent))
INPUT_SEN = ['I love reading.', 'You are so boring.']
X_INPUT = np.empty((len(INPUT_SEN), MAX_WORD_LEN), dtype=list)
i = 0
for sen in INPUT_SEN:
wtmp = nltk.word_tokenize(sen)
j = 0
tmp = []
if len(wtmp) >= MAX_WORD_LEN:
for word in wtmp:
if j <= MAX_WORD_LEN - 1:
if word in word2index.keys():
tmp.append(word2index[word])
else:
tmp.append(word2index['UNK'])
else:
break
j = j + 1
else:
for word in wtmp:
if word in word2index.keys():
tmp.append(word2index[word])
else:
tmp.append(word2index['UNK'])
j += 1
for k in range(MAX_WORD_LEN - len(wtmp)):
tmp.append(word2index['0'])
j += 1
X_INPUT[i, :] = tmp[:]
i += 1
labels = [int(round(x[0])) for x in model.predict(X_INPUT)]
label2word = {1: '积极', 0: '消极'}
for i in range(len(INPUT_SEN)):
print('{} {}'.format(label2word[labels[i]], INPUT_SEN[i]))
好像大部分时间都花在数据整理上了,真正建立模型的部分很简单了,想上手深度学习的各位亲先把python基础知识练好,什么numpy、pandas是必须的啊,我简书里也会写系列文章,请大家持续关注。
当然LSTM也有变体,很多论文都对LSTM结构进行了改进,但万变不离其宗,大家用的时候直接导入包。其中有一个双向LSTM比较好用,keras里就是加入一层Bidirectional,简单的很啊。
网友评论