本篇文章参考《Packt.Deep.Learning.with.Keras》 电子版网上一搜就能找到,主要关注于使用RNN进行文本相关处理的流程。
实现效果
以《爱丽丝梦游仙境》的英文版为例,给出i am alice
的开头,网络能实现续写故事的效果,如下:
i am alice , and the project gutenberg-tm electronic work in a low the white rabbit again the gryphon as she co
不过毕竟只是个字符级的生成Demo,效果一般,我们会在后续讨论如何改进效果。
预处理
文本读取
首先从http://www.gutenberg.org/下载alice_in_wonderland.txt
,读出所有文本,消除空行
with open('alice_in_wonderland.txt','r') as fin:
lines=[]
for line in fin:
line=line.strip().lower()
#空行
if(len(line)==0):
continue
lines.append(line)
text=" ".join(lines)
字符编码
这一步是为了把字符转换成网络能看懂的形式
#所有字符的集合
chars=set(c for c in text)
nb_chars=len(chars)
#构建映射表
char2index=dict((c,i) for i,c in enumerate(chars))
index2char=dict((i,c) for i,c in enumerate(chars))
训练集生成
我们文本生成的方式是通过前10个字符生成第11个,然后以第2个到第11个字符再来生成第12个,以此往复。所以训练集的X就是原小说中的10个字符,Y就是第11个。代码如下
SEQLEN=10
STEP=1
input_chars=[]
label_chars=[]
for i in range(0,len(text)-SEQLEN,STEP):
input_chars.append(text[i:i+SEQLEN])
label_chars.append(text[i+SEQLEN])
打印一下看下效果
for i in range(5):
print(input_chars[i],"->",label_chars[i])
anyone an -> y
anyone any -> w
nyone anyw -> h
yone anywh -> e
one anywhe -> r
字符串有了,再用上面生成的映射表转为one-hot编码。以26个字母为例,X的形状即为(data_size,10,26)
,其中的data_size
为有多少条X数据,10
指每条数据由10个字符组成,26
为one-hot编码长度。
这里是手工进行one-hot编码,采用keras.utils.to_categorical
也是可以的。
import numpy as np
# 转Index
# Input:(batch_size=len(input_chars),dim1=SEQLEN, dim2=nb_chars)
X=np.zeros((len(input_chars),SEQLEN,nb_chars),dtype=np.bool)
y=np.zeros((len(label_chars),nb_chars),dtype=np.bool)
for i, input_char in enumerate(input_chars):
for j,ch in enumerate(input_char):
X[i,j,char2index[ch]]=1
y[i,char2index[label_chars[i]]]=1
网络构建
书上写的是用字符串的形式去描述误差,比如loss='categorical_crossentropy'
,不过这样手打容易出错,如果是Jupyter notebook环境的话,可以从keras.losses
里面import一下,也是一样的效果。
from keras import Sequential
from keras.layers import SimpleRNN,Dense,Activation
from keras.activations import softmax
from keras.losses import categorical_crossentropy
from keras.optimizers import RMSprop
HIDDEN_SIZE=128
BATCH_SIZE=128
NUM_ITERATIONS=25
NUM_EPOCHS_PER_ITERATION=1
NUM_PREDS_PER_EPOCH=100
model=Sequential()
model.add(SimpleRNN(HIDDEN_SIZE,return_sequences=False,
input_shape=(SEQLEN,nb_chars),
unroll=True))
model.add(Dense(nb_chars))
model.add(Activation(softmax))
#这里的RMSprop优化器需要构造,所以要加上括号
model.compile(loss=categorical_crossentropy,optimizer=RMSprop())
网络结构如下。需要注意书上对RNN进行讲解的时候会把RNN按时间展开
这里的一个圆圈表示RNN的一层(而不只是一个Cell),包含HIDDEN_SIZE
个RNN Cell(可以理解成抽取HIDDEN_SIZE
个特征)
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_1 (SimpleRNN) (None, 128) 24192
_________________________________________________________________
dense_1 (Dense) (None, 60) 7740
_________________________________________________________________
activation_1 (Activation) (None, 60) 0
=================================================================
文本生成
for iteration in range(NUM_ITERATIONS):
print("_"*50)
print("Iteratipn #:%d"%iteration)
model.fit(X,y,batch_size=BATCH_SIZE,epochs=NUM_EPOCHS_PER_ITERATION)
next_chars="i am alice"
for i in range(NUM_PREDS_PER_EPOCH):
Xtest=np.zeros((1,SEQLEN,nb_chars),dtype=np.bool)
index_list=[char2index[c] for c in next_chars]
for i,ci in enumerate(index_list):
Xtest[0][i][ci]=True
pred=model.predict(Xtest)
pred_char=index2char[np.argmax(pred)]
print(pred_char,end="")
next_chars=next_chars[1:]+pred_char
这里预测出来的字符直接取了概率最大的,但如果概率是0.51
,0.49
这样,对0.49
就不是很公平,也可以采用如下的sample
函数,按概率取出字符以增加文本的多样性。
最后几次迭代效果如下:
Iteratipn #:22
Epoch 1/1
161794/161794 [==============================] - 8s 49us/step - loss: 1.4239
, and the project gutenberg-tm electronic work in a low the white rabbit again the gryphon as she co__________________________________________________
Iteratipn #:23
Epoch 1/1
161794/161794 [==============================] - 8s 51us/step - loss: 1.4177
was a little to see while the this agreement in the door and she was a little to see while the this__________________________________________________
Iteratipn #:24
Epoch 1/1
161794/161794 [==============================] - 8s 50us/step - loss: 1.4101
was a little course were and donations to the queen was a little course were and donations to the q
改进与思考
首先网络从前几个字符预测下一个字符比较类似n-gram语言模型,不过由于是按照字符级别构造的,生成效果当然没有按照单词自然,下篇文章我们采用LSTM来对句子进行单词级别的处理,用于分析情感;另外从结果中可以看到,可能是因为原文中下划线_
经常连续出现,导致最后生成的文本也是连续的下划线,可以在一开始的文本处理中选择剔除掉除字母之外的字符。
应用角度上来看,除了文本的预测,还可以是语音的生成,图像的补全(PixelRNN)等等。
RNN倒是能够简单地模拟大脑对时间序列的记忆模式,但是对记忆比较久远的事情就无能为力了,但是人类却可以做到对很久远的、例如童年时期事情的回忆,这样的模式如何在网络上复现?这样的记忆似乎可以看成是重要程度(对这个人人生观价值观的冲击大小)的影响,对于网络如何衡量?有点像Attention机制。再有就是人类看到一个单词往往会联想到其他与之相关的单词,这些单词组成了一张知识网络,只需要记住其中一个知识就能推导出其他知识,这样的联想相似性倒是可以由Word2Vec推导出来,在网络上可以怎样应用呢?图像可以根据特征相似度提取出相似的图片,例如从一个圆就可以联想到篮球、太阳、胖头鱼什么的,这样的联想对人类来说有什么用可能就是网络应用这些机制所需要思考的。
对RNN来说,每次计算除开X
之外还加上了H(t-1)
,将各个时间点都联系在了一起。人类的记忆似乎也是这样,仅仅从大脑的上一个状态转移到当前状态就行、然而RNN模式对于大脑来说相当于把每个时刻的信息都储存到同一个神经元里面然后再更新,而真实情况应该是不同时间的信息存储在不同细胞里面,然后互相调用。从这个角度上来说感觉Wavenet的模式更像一些。(虽然调用的时候是固定的几个信息结合在一起,但是更深的层能够决定是哪些记忆需要被提取出来)
关于记忆还可以搜索Memory Network
,网络起到的作用相当于CPU,外部的储存器相当于RAM,CPU起到找出RAM地址的作用。详细信息可以参考李宏毅课程的Attention-based Model
。
以阅读理解为例,为了回答一个问题Query,我们需要从文章中找出与这个问题相关的回答句子。下图中的
q
就是我们要查询的RAM的“地址”,也可以说这个q
是对文章中句子的编码组合,通过这个组合找到哪些句子是相关的。假如把文章中的每个句子都用x
来表示,a
就是每个句子的重要程度了,也就是Attention
。Attention机制
Ref: Deep Learning基础--理解LSTM/RNN中的Attention机制
目前采用编码器-解码器 (Encode-Decode) 结构的模型非常热门,是因为它在许多领域较其他的传统模型方法都取得了更好的结果。这种结构的模型通常将输入序列编码成一个固定长度的向量表示,对于长度较短的输入序列而言,该模型能够学习出对应合理的向量表示。然而,这种模型存在的问题在于:当输入序列非常长时,模型难以学到合理的向量表示。
类似于人类的记忆,一大堆记忆涌入的时候是记不住的,只会记住关键的一些信息,Attention机制做的就是找出哪些记忆是需要重点关注的。
“在文本翻译任务上,使用attention机制的模型每生成一个词时都会在输入序列中找出一个与之最相关的词集合。之后模型根据当前的上下文向量 (context vectors) 和所有之前生成出的词来预测下一个目标词。
… 它将输入序列转化为一堆向量的序列并自适应地从中选择一个子集来解码出目标翻译文本。这感觉上像是用于文本翻译的神经网络模型需要“压缩”输入文本中的所有信息为一个固定长度的向量,不论输入文本的长短。”
也就是说,它不再使用固定长度的Encode Vector(上下文向量c
)作为记忆,而使用整个Input序列,Memory不再是被编码的形态,而是类似于人脑多个神经元的形态。每个被翻译出来的单词yi
对于Input序列来说都有一个独立的需要关注的上下文向量ci
,相当于从Memory中找到了和文本翻译最相关的Memory出来(对齐)作为结果。
举个例子,在翻译出machine
的时候不再使用整个机器学习
的被压缩后的上下文向量c
,而是使用其独有的c0
,即重点关注机器
这两个字的上下文向量。
除开文字到文字以外,图像到文字也有相同原理的论文。
这样的Attention有什么用处呢?我们将在后续文章的Encoder-Decoder机制中见到他的威力。