背景
前馈网络、CNN对序列数据只能采用固定长度作为输入,但是,句子、音频等数据,输入固定长度(虽然可以通过输入填充到固定大小来克服)。这两种网络仍比序列模型RNN表现更差,因为传统模型不理解输入的上下文。
1. 循环神经网络Recurrent Neural Network
循环神经网络,时间步的每个节点从前前一个节点获取输入,并不断反馈循环。
表示形式:
a
t
=
f
(
h
t
−
1
,
x
t
)
,
g
(
x
)
=
t
a
n
h
x
,
a
t
=
g
(
W
h
h
⋅
h
t
−
1
+
W
x
h
⋅
x
t
)
h
t
=
W
h
y
⋅
a
t
\mathbf a_t = f(\mathbf h_{t-1}, \mathbf x_t),\\ \mathbf g(\mathbf x) = tanh \mathbf x,\\ \mathbf a_t = g(\mathbf W_{hh} \cdot \mathbf h_{t-1} + \mathbf W_{xh} \cdot \mathbf x_t)\\ \mathbf h_t = \mathbf W_{hy} \cdot \mathbf a_t
at=f(ht−1,xt),g(x)=tanhx,at=g(Whh⋅ht−1+Wxh⋅xt)ht=Why⋅at
2. 长短时神经网络LSTM
2.1 遗忘门
f
t
=
σ
(
W
f
⋅
[
h
t
−
1
,
x
t
]
+
b
f
)
f_t = \sigma (W_f \cdot [h_{t-1}, x_t] + b_f)
ft=σ(Wf⋅[ht−1,xt]+bf)
输入的是,当前时刻的信息和历史隐状态的信息,经过sigmoid函数,值靠近0代表遗忘,值靠近1代表保持。
2.2 输入门及更新门
i
t
i_t
it 是遗忘门类似,值接近0代表遗忘,值接近1代表保持;
C
^
t
\hat{C}_{t}
C^t为当前输入的细胞状态,经过tanh函数进行压缩.
由sigmoid的输出决定tanh的输出中哪部分信息是重要的。
值得注意的是,这里记忆状态是使用哈达玛积,不是矩阵相乘,而是相应元素相乘!
C t C_t Ct 为更新后当前时刻的细胞状态,一部分由之前细胞状态与遗忘门进行点乘操作,加上输入门的信息,这两部分都考虑到了上一时刻的隐层状态 h t − 1 h_{t-1} ht−1。
疑惑:这里为什么使用 tanh
激活函数? 更新门就是,tanh
激活函数和 sigmoid
激活函数后的矩阵逐位相乘。
LSTM管理一个内部状态向量,当我们添加某个函数的输出时,其值应该能够增加或减少。 Sigmoid输出总是非负的;该州的价值只会增加。 tanh的输出可以是正的或负的,允许状态的增加和减少。
2.3 输出门
同理,这里求隐层状态也是用了哈达玛积(element-wise),
当前时刻输出为
o
t
o_t
ot, 与前一时刻学习到的信息
h
t
−
1
h_{t-1}
ht−1和 当前时刻信息
x
t
x_t
xt 有关。
输出
o
t
o_t
ot和经过tanh变换的细胞状态
C
t
C_t
Ct,作为 当前时刻的信息
h
t
h_t
ht.
包含细胞状态的都用 t a n h tanh tanh激活函数。
3. 门控循环单元GRU
GRU比LSTM训练时间更加短,同时也可以记住长期依赖。
z
t
z_t
zt代表更新门操作;
r
t
r_t
rt代表重置门操作,
h
t
h_t
ht代表 t 时刻隐层信息。
GRU只有更新门
z
t
z_t
zt 和重置门
r
t
r_t
rt,没有输出门和细胞状态。
更新门决定有多少过去信息用来利用,重置门决定多少过去信息丢弃。
思想就是充分考虑前一时刻隐层状态信息
h
t
−
1
h_{t-1}
ht−1,和当前时刻的信息
h
^
t
\hat{h}_t
h^t,这两部分通过权重
z
t
z_t
zt来调节。
一般来说,GRU计算快速,计算精度略低于LSTM。
4. 双向RNN
双向RNN的出发点是,有时模型需要读取序列数据的上下文来消除歧义,特别是在文本领域。
双向RNN的重复模块是传统RNN,LSTM或者GRU单元,并且有一种在时间上前向学习过程和向后学习过程。
堆叠LSTM
5. 实践
简单模拟RNN实现,
import copy, numpy as np
np.random.seed(0)
# compute sigmoid nonlinearity
def sigmoid(x):
output = 1/(1+np.exp(-x))
return output
# convert output of sigmoid function to its derivative
def sigmoid_output_to_derivative(output):
return output*(1-output)
# training dataset generation
int2binary = {}
binary_dim = 8
largest_number = pow(2,binary_dim)
binary = np.unpackbits(
np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
for i in range(largest_number):
int2binary[i] = binary[i]
# input variables
alpha = 0.1
input_dim = 2
hidden_dim = 16
output_dim = 1
# initialize neural network weights
synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1
synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1
synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1
synapse_0_update = np.zeros_like(synapse_0)
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)
# training logic
for j in range(10000):
# generate a simple addition problem (a + b = c)
a_int = np.random.randint(largest_number/2) # int version
a = int2binary[a_int] # binary encoding
b_int = np.random.randint(largest_number/2) # int version
b = int2binary[b_int] # binary encoding
# true answer
c_int = a_int + b_int
c = int2binary[c_int]
# where we'll store our best guess (binary encoded)
d = np.zeros_like(c)
overallError = 0
layer_2_deltas = list()
layer_1_values = list()
layer_1_values.append(np.zeros(hidden_dim))
# moving along the positions in the binary encoding
for position in range(binary_dim):
# generate input and output
X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]])
y = np.array([c[binary_dim - position - 1]]).T
# hidden layer (input ~+ prev_hidden)
layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h))
# output layer (new binary representation)
layer_2 = sigmoid(np.dot(layer_1,synapse_1))
# did we miss?... if so, by how much?
layer_2_error = y - layer_2
layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2))
overallError += np.abs(layer_2_error[0])
# decode estimate so we can print it out
d[binary_dim - position - 1] = np.round(layer_2[0][0])
# store hidden layer so we can use it in the next timestep
layer_1_values.append(copy.deepcopy(layer_1))
future_layer_1_delta = np.zeros(hidden_dim)
for position in range(binary_dim):
X = np.array([[a[position],b[position]]])
layer_1 = layer_1_values[-position-1]
prev_layer_1 = layer_1_values[-position-2]
# error at output layer
layer_2_delta = layer_2_deltas[-position-1]
# error at hidden layer
layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)
# let's update all our weights so we can try again
synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
synapse_0_update += X.T.dot(layer_1_delta)
future_layer_1_delta = layer_1_delta
synapse_0 += synapse_0_update * alpha
synapse_1 += synapse_1_update * alpha
synapse_h += synapse_h_update * alpha
synapse_0_update *= 0
synapse_1_update *= 0
synapse_h_update *= 0
# print out progress
if(j % 1000 == 0):
print("Error:" + str(overallError))
print ("Pred:" + str(d))
print ("True:" + str(c))
out = 0
for index,x in enumerate(reversed(d)):
out += x*pow(2,index)
print (str(a_int) + " + " + str(b_int) + " = " + str(out))
print ("------------")
Example 2 搭建RNN:
# coding: utf-8
import numpy as np
import tensorflow as tf
from tensorflow.python import debug as tf_debug
from matplotlib import pyplot as plt
'''超参数'''
num_steps = 10
batch_size = 200
num_classes = 2
state_size = 16
learning_rate = 0.1
'''生成数据
就是按照文章中提到的规则,这里生成1000000个
'''
def gen_data(size=1000000):
X = np.array(np.random.choice(2, size=(size,)))
Y = []
'''根据规则生成Y'''
for i in range(size):
threshold = 0.5
if X[i-3] == 1:
threshold += 0.5
if X[i-8] == 1:
threshold -=0.25
if np.random.rand() > threshold:
Y.append(0)
else:
Y.append(1)
return X, np.array(Y)
'''生成batch数据'''
def gen_batch(raw_data, batch_size, num_step):
raw_x, raw_y = raw_data
data_length = len(raw_x)
batch_patition_length = data_length // batch_size # ->5000
data_x = np.zeros([batch_size, batch_patition_length], dtype=np.int32) # ->(200, 5000)
data_y = np.zeros([batch_size, batch_patition_length], dtype=np.int32) # ->(200, 5000)
'''填到矩阵的对应位置'''
for i in range(batch_size):
data_x[i] = raw_x[batch_patition_length*i:batch_patition_length*(i+1)]# 每一行取batch_patition_length个数,即5000
data_y[i] = raw_y[batch_patition_length*i:batch_patition_length*(i+1)]
epoch_size = batch_patition_length // num_steps # ->5000/5=1000 就是每一轮的大小
for i in range(epoch_size): # 抽取 epoch_size 个数据
x = data_x[:, i * num_steps:(i + 1) * num_steps] # ->(200, 5)
y = data_y[:, i * num_steps:(i + 1) * num_steps]
yield (x, y) # yield 是生成器,生成器函数在生成值后会自动挂起并暂停他们的执行和状态(最后就是for循环结束后的结果,共有1000个(x, y))
def gen_epochs(n, num_steps):
for i in range(n):
yield gen_batch(gen_data(), batch_size, num_steps)
'''定义placeholder'''
x = tf.placeholder(tf.int32, [batch_size, num_steps], name="x")
y = tf.placeholder(tf.int32, [batch_size, num_steps], name='y')
init_state = tf.zeros([batch_size, state_size])
'''RNN输入'''
x_one_hot = tf.one_hot(x, num_classes)
rnn_inputs = tf.unstack(x_one_hot, axis=1)
'''定义RNN cell'''
with tf.variable_scope('rnn_cell'):
W = tf.get_variable('W', [num_classes + state_size, state_size])
b = tf.get_variable('b', [state_size], initializer=tf.constant_initializer(0.0))
def rnn_cell(rnn_input, state):
with tf.variable_scope('rnn_cell', reuse=True):
W = tf.get_variable('W', [num_classes+state_size, state_size])
b = tf.get_variable('b', [state_size], initializer=tf.constant_initializer(0.0))
return tf.tanh(tf.matmul(tf.concat([rnn_input, state],1),W) + b)
'''将rnn cell添加到计算图中'''
state = init_state
rnn_outputs = []
for rnn_input in rnn_inputs:
state = rnn_cell(rnn_input, state) # state会重复使用,循环
rnn_outputs.append(state)
final_state = rnn_outputs[-1] # 得到最后的state
#cell = tf.contrib.rnn.BasicRNNCell(num_units=state_size)
#rnn_outputs, final_state = tf.contrib.rnn.static_rnn(cell=cell, inputs=rnn_inputs,
#initial_state=init_state)
#rnn_outputs, final_state = tf.nn.dynamic_rnn(cell=cell, inputs=rnn_inputs,
#initial_state=init_state)
'''预测,损失,优化'''
with tf.variable_scope('softmax'):
W = tf.get_variable('W', [state_size, num_classes])
b = tf.get_variable('b', [num_classes], initializer=tf.constant_initializer(0.0))
logits = [tf.matmul(rnn_output, W) + b for rnn_output in rnn_outputs]
predictions = [tf.nn.softmax(logit) for logit in logits]
y_as_list = tf.unstack(y, num=num_steps, axis=1)
losses = [tf.nn.sparse_softmax_cross_entropy_with_logits(labels=label,logits=logit) for logit, label in zip(logits, y_as_list)]
total_loss = tf.reduce_mean(losses)
train_step = tf.train.AdagradOptimizer(learning_rate).minimize(total_loss)
'''训练网络'''
def train_rnn(num_epochs, num_steps, state_size=4, verbose=True):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
#sess = tf_debug.LocalCLIDebugWrapperSession(sess)
training_losses = []
for idx, epoch in enumerate(gen_epochs(num_epochs, num_steps)):
training_loss = 0
training_state = np.zeros((batch_size, state_size)) # ->(200, 4)
if verbose:
print('\nepoch', idx)
for step, (X, Y) in enumerate(epoch):
tr_losses, training_loss_, training_state, _ = \
sess.run([losses, total_loss, final_state, train_step], feed_dict={x:X, y:Y, init_state:training_state})
training_loss += training_loss_
if step % 100 == 0 and step > 0:
if verbose:
print('第 {0} 步的平均损失 {1}'.format(step, training_loss/100))
training_losses.append(training_loss/100)
training_loss = 0
return training_losses
training_losses = train_rnn(num_epochs=5, num_steps=num_steps, state_size=state_size)
print(training_losses[0])
plt.plot(training_losses)
plt.show()
6. 总结
双向LSTM模型一般具有较高的性能。
Q: LSTM 参数如何计算?
A:设 LSTM 输入维度为 x_dim, 输出维度为 y_dim,那么参数个数 n 为:
n = 4 * ((x_dim + y_dim) * y_dim + y_dim)
Q:pytorch 中LSTM W, b 如何初始化?
均匀分布:
Q:LSTM如何处理处理变长序列?
以pytorch为例:压缩序列torch.nn.utils.rnn.pack_padded_sequence()
pack_padded_sequence(...)
-- 参数列表:
-- input: 有 <PAD> 的 batch 序列
-- lengths: input 中每个序列的长度
-- batch_first: 如果为True, input 必须是 [batch_size, seq_len, input_size]
-- enforce_sorted: 如果为True, 那么 input 中的序列需要按照 长度递减排列
-- 返回值:
一个 PackedSequence 对象
Q: 为什么LSTM解决了梯度消失的问题
A:
梯度消失的原因:在多层网络中,影响梯度大小的因素主要有两个:权重和激活函数的偏导。深层的梯度是多个激活函数偏导乘积的形式来计算,如果这些激活函数的偏导比较小(小于1)或者为0,那么梯度随时间很容易vanishing;相反,如果这些激活函数的偏导比较大(大于1),那么梯度很有可能就会exploding。因而,梯度的计算和更新非常困难。
解决方案:
使用一个合适激活函数,它的梯度在一个合理的范围。LSTM使用gate function,有选择的让一部分信息通过。gate是由一个sigmoid单元和一个逐点乘积操作组成,sigmoid单元输出1或0,用来判断通过还是阻止,然后训练这些gate的组合。所以,当gate是打开的(梯度接近于1),梯度就不会vanish。并且sigmoid不超过1,那么梯度也不会explode。
LSTM的效果:1、当gate是关闭的,那么就会阻止对当前信息的改变,这样以前的依赖信息就会被学到。2、当gate是打开的时候,并不是完全替换之前的信息,而是在之前信息和现在信息之间做加权平均。所以,无论网络的深度有多深,输入序列有多长,只要gate是打开的,网络都会记住这些信息。
(‘梯度消失’其实在DNN和RNN中意义不一样,DNN中梯度消失指的是误差无法传递回浅层,导致浅层的参数无法更新;而RNN中的梯度消失是指较早时间步所贡献的更新值,无法被较后面的时间步获取,导致后面时间步进行误差更新的时候,采用的只是附近时间步的数据。)
如果根据BPTT把梯度的结构展开,会包含连乘项:
该值范围在0~1之间,但是在实际参数更新中,可以通过控制bias比较大,使得该值接近于1;在这种情况下,即使通过很多次连乘的操作,梯度也不会消失,仍然可以保留"长距"连乘项的存在。即总可以通过选择合适的参数,在不发生梯度爆炸的情况下,找到合理的梯度方向来更新参数,而且这个方向可以充分地考虑远距离的隐含层信息的传播影响。
RNN:
最近开通了个公众号,主要分享深度学习相关内容,包含NLP,推荐系统,风控等算法相关的内容,感兴趣的伙伴可以关注下。
公众号相关的学习资料会上传到QQ群596506387,欢迎关注。
reference: