RNN基本概念
如何才能让神经网络记住整个句子来正确预测下一个单词呢?这正是RNN发挥作用的时候。
RNN的输出不仅是基于当前的输入,还会基于先前的隐态。此时可能会好奇为什么不能根据当前输入和之前的输入来预测输出,而必须是当前的输入和先前的隐态。这是因为先前的输入只保存了前一个单词的信息,而先前的隐态捕获了整个句子的信息,即先前的隐态捕获了上下文。因此,基于当前的输入和先前的隐态能够预测输出,而不是根据当前输入和先前输入。
RNN广泛用于诸如机器翻译,情感分析等各种自然语言处理(NLP)任务。另外,还适用于股票市场数据等时序数据。
RNN与普通神经网络的不同之处在于隐态中有一个循环,这表明是如何利用先前的隐态来计算输出的。
如下图所示,输出 y 1 y_1 y1 是根据当前输入 x 1 x_1 x1、当前隐态 h 1 h_1 h1,和先前隐态 h 0 h_0 h0预测的, y 2 y_2 y2同理。这就是RNN的工作原理,是采用当前输入和先前隐态来预测输出的。由于这些稳态可以保存所有已观察过的信息,因此可称为记忆单元。
接下来,从数学角度分析:
- U表示输入到隐态的权重矩阵
- W表示隐态到隐态的权重矩阵
- V表示隐态到输出的权重矩阵
在前向传递中,可计算如下:
h
t
=
ϕ
(
U
x
t
+
W
h
t
−
1
)
h_{t}=\phi\left(U x_{t}+W h_{t-1}\right)
ht=ϕ(Uxt+Wht−1)
即,时刻 t 的隐态=tanh([输入 - 隐态权重矩阵 x 输入] + [ 隐态 - 隐态权重矩阵 x 时刻 t-1 的前一隐态]):
y
^
t
=
σ
(
v
h
t
)
\hat{y}_{t}=\sigma\left(v h_{t}\right)
y^t=σ(vht)
即,时刻 t 的输出=Sigmoid(隐态 - 输出权重矩阵 x 时刻 t 的隐态)
另外,还可以定义损失函数为交叉熵损失,如下:
损失
=
−
y
t
log
y
^
t
=-y_{t} \log \hat{y}_{t}
=−ytlogy^t
总损失
=
−
∑
t
y
t
log
y
t
^
=-\sum_{t} y_{t} \log \hat{y_{t}}
=−∑tytlogyt^
y t y_t yt 为时刻 t 的实际单词, y t ^ \hat{y_{t}} yt^为 t 时刻的预测单词,由于是取整个句子为训练样本,因此总的损失是每个时间步的损失之和。
基于时间的反向传播
可以利用反向传播训练RNN。但在RNN中,由于与所有时间步都相关,因此每个输出的梯度不仅与当前的时间步有关,而且还取决于先前的时间步。将上述过程称为基于时间的反向传播(BPTT) 这基本与反向传播相同,只是应用了RNN。
上图中,
L
1
L_1
L1、
L
2
L_2
L2、
L
3
L_3
L3是在每个时间步的损失。现在,需要在每个时间步计算这些损失相对于权重矩阵U、V和W的梯度。与之前通过在每个时间步的损失之和来计算总的损失一样,在此利用每个时间步的梯度之和来更新权重矩阵。
∂
L
∂
V
=
∑
t
∂
L
t
∂
V
\frac{\partial L}{\partial V}=\sum_{t} \frac{\partial L_{t}}{\partial V}
∂V∂L=∑t∂V∂Lt
但是,该方法有一个问题。梯度计算涉及计算相对于激活函数的梯度。在计算相对于Sigmoid/tanh函数的梯度时,梯度值会非常小。而当经过多个时间步反向传播网络并乘以梯度时,梯度值会变得越来越小。称为梯度消失问题。那么就不能学习长期相关信息,即RNN不能长时间保存信息。
梯度消失不仅发生在RNN中,而且也会在采用Sigmoid/tanh函数且具有多个隐层的其他深度神经网络中出现。如果梯度值大于1,当乘以这些梯度时,会产生一个非常大的值,这称为梯度爆炸问题。
一种解决方法是采用ReLU作为激活函数。另外,使用RNN的改进网络LSTM来解决梯度消失的问题。
LSTM RNN
LSTM是RNN的一种变型,主要用于解决梯度消失问题。只要需要,LSTM就会在记忆中保存信息。实质上是由LSTM替换了RNN单元。
利用LSTM RNN来生成歌词
首先,导入库
import warnings
warnings.filterwarnings('ignore')
import tensorflow as tf
import numpy as np
接着,读取包含歌词的文件
with open("data/ZaynLyrics.txt","r") as f:
data=f.read()
data=data.replace('\n','')
data = data.lower()
可以观察数据内容:
print(data[:50])
然后,在all_chars变量中保存所有字符:
all_chars = list(set(data))
将唯一字符的个数保存在unique_chars中:
unique_chars = len(all_chars)
现在,创建每个字符与其编号之间的对应关系。char_to_ix具有字符到编号的映射,而ix_to_char是编号到字符的映射:
char_to_ix = { ch:i for i,ch in enumerate(all_chars) }
ix_to_char = { i:ch for i,ch in enumerate(all_chars) }
例如:
char_to_ix ['e']
9
ix_to_char[9]
e
接下来,定义一个generate_batch函数来生成输入和目标值。目标值就是移位 i i i次后的输入值
例如:如果 input = [12,13,24] 且移位值为1,则目标值为 [13,24]
def generate_batch(seq_length,i):
inputs = [char_to_ix[ch] for ch in data[i:i+seq_length]]
targets = [char_to_ix[ch] for ch in data[i+1:i+seq_length+1]]
inputs = np.array(inputs).reshape(seq_length, 1)
targets = np.array(targets).reshape(seq_length, 1)
return inputs, targets
接着,定义句子长度、学习率和节点个数,即神经元个数:
seq_length = 25
learning_rate = 0.1
num_nodes = 300
现在开始构建LSTM RNN。 Tensorflow提供了一种构建LSTM单元的BasicLSTMCell() 函数,但需要指定LSTM单元中的单元个数和所采用的激活函数类型。
在此,创建一个LSTM单元,并利用tf.nn.dynamic_rnn函数来构建包含这一单元的RNN,该函数可返回输出值和状态值:
def build_rnn(x):
cell= tf.contrib.rnn.BasicLSTMCell(num_units=num_nodes, activation=tf.nn.relu)
outputs, states = tf.nn.dynamic_rnn(cell, x, dtype=tf.float32)
return outputs,states
接着,创建输入X和目标Y的占位符
X=tf.placeholder(tf.float32,[None,1])
Y=tf.placeholder(tf.float32,[None,1])
将X和Y转换为int型
X=tf.cast(X,tf.int32)
Y=tf.cast(Y,tf.int32)
另外,还需创建对于X和Y的onehot表示,具体如下:
X_onehot=tf.one_hot(X,unique_chars)
Y_onehot=tf.one_hot(Y,unique_chars)
调用build_rnn函数,可由RNN得到输出和状态:
outputs,states=build_rnn(X_onehot)
输出转置:
outputs=tf.transpose(outputs,perm=[1,0,2])
初始化权重和偏置:
W=tf.Variable(tf.random_normal((num_nodes,unique_chars),stddev=0.001))
B=tf.Variable(tf.zeros((1,unique_chars)))
将输出乘以权重并加上偏置来计算最终的输出:
Ys=tf.matmul(outputs[0],W)+B
接着,应用Softmax激活函数,得到概率:
prediction = tf.nn.softmax(Ys)
计算cross_entropy
cross_entropy=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=Y_onehot,logits=Ys))
目标是使得损失最小化,因此,需反向传播网络并执行梯度下降:
optimiser = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
在此,定义一个称为predict的帮助函数,用于根据RNN模型得到下一个预测字符的编号:
def predict(seed,i):
x=np.zeros((1,1))
x[0][0]= seed
indices=[]
for t in range(i):
p=sess.run(prediction,{X:x})
index = np.random.choice(range(unique_chars), p=p.ravel())
x[0][0]=index
indices.append(index)
return indices
设置batch_size、批次数和epochs个数以及shift值,来生成一个批次:
batch_size=100
total_batch=int(total_chars//batch_size)
epoch=1000
shift=0
最后,启动Tensorflow模型,来构建损失:
init=tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for epoch in range(epoch):
print("Epoch {}:".format(epoch))
if shift + batch_size+1 >= len(data):
shift =0
# 通过generate_batch函数获得每个批量的输入目标
# 该函数是通过 shifts 值将输入移位并形成目标值的
for i in range(total_batch):
inputs,targets=generate_batch(batch_size,shift)
shift += batch_size
# 计算损失
if(i%100==0):
loss=sess.run(cross_entropy,feed_dict={X:inputs, Y:targets})
# 获得predict函数得到下一预测字符的符号
index =predict(inputs[0],200)
# 将该序号传给ix_to_char字典,并得到char
txt = ''.join(ix_to_char[ix] for ix in index)
print('Iteration %i: '%(i))
print ('\n %s \n' % (txt, ))
sess.run(optimiser,feed_dict={X:inputs,Y:targets})