第8讲 循环神经网络 RNN
时间序列、文本、依赖问题 && Time Series, Text, Dependency Problems
在现实生活中,我们会遇到很多跟时间序列相关的问题,比如一个公司的交易情况或者收入情况等
我们还会遇到和上下文相关的文本处理问题,体现在每个字
的含义根据在句中位置的不同而有不同的含义
这就是在编译原理中,一个很重要的概念——有限状态机(Finite State Machines)。
有限状态机可以模拟很多事情,我们每天都在用的编译器其实就是个有限状态机的应用实例,它能够接收一行行输入的程序语言来产生各种各样的行为,那么在此时此刻它的行为是什么样的,这就是由有限状态机来决定的。
序列问题 Sequence Problems
序列问题分为以下好几类情况
如何区分一对一 (one to one) 和一对多 (one to many) 问题呢?比如,我们输入一张小猫图片
,最后输出
pic
-><0.1, 0.2, 0.1, 0.3, 0.3, 0.1, 0.3>
—— one to one,因为输出的这些值之间没有相关性;pic
-><这是一个哈基米>
—— one to many,输出值 (每个字) 之间具有相关性,一个字是否出现、出现在什么位置,与前后文本相关。
还有多对一 (many to one) 问题
<今天不开心>
->-1
—— Negative值<今天很不开心>
->-3
—— Negative值
多对多 (many to many) 问题
<Knowledge is power>
-><知识就是力量>
——前后都有依赖关系
也就是说,生活中的所有依赖问题/序列问题都可以抽象成上述几类。
用PyTorch搭建全连接神经网络并解决一个序列问题
我们现在有这样的一些数据,是一些公司连续两月的营收(数据集后续上传至github)
我们希望能对某公司未来的营收进行预测。
import pandas as pd
timeseries_revenue = pd.read_csv('time_serise_revenue.csv')
timeseries_revenue = timeseries_revenue.drop(timeseries_revenue.columns[0], axis=1)
timeseries_revenue
我们选其中一个公司看看
timeseries_revenue.sample()
import matplotlib.pyplot as plt
plt.plot(timeseries_revenue.sample().values[0])
假设我们现在已知10天的营收,要预测第11天的营收
一般我们会这么设置训练样本
每个
x
是前10天的营收,target数据y
是第11天的数据
但实际工作中,为了更好的效果,我们会这么设置
每个
x
是第i天到第i+9这十天的数据,target数据y
是第i+1到第i+10的数据
搭建网络
接下来我们就用PyTorch框架来搭建一个简单的深度学习网络。
import torch
import torch.nn as nn
import numpy as np
class FullyConnected(nn.Module): # 可以看到每个Module都是PyTorch的一个子类
def __init__(self, x_size, hidden_size, output_size):
super(FullyConnected, self).__init__() # 调用了父类方法进行初始化(还记得我们之前手搓神经网络时的定义吗)
self.hidden_size = hidden_size
self.linear_with_tanh = nn.Sequential(
nn.Linear(x_size, self.hidden_size),
nn.Tanh(),
nn.Linear(self.hidden_size, self.hidden_size),
nn.Tanh(),
nn.Linear(self.hidden_size, output_size)
)
def forward(self, x):
yhat = self.linear_with_tanh(x)
return yhat
这是一个全连接的网络 (fully connected) 这里要注意的就是要正确设置中间层的层数,比如我们的输入维度是n*10
,输出也是n*10
,中间一共4层,那么就可以将中间层分别设置为10*8
、8*8
、8*9
和9*10
fc_model = FullyConnected(x_size=10, hidden_size=8, output_size=10)
前向传播
下面我们做个简单的实验,看看这个神经网络是否能将[1, 2, … , 10]前向传播后输出一个1*10
的结果
some_test = np.array([[i for i in range(10)]])
fc_model(torch.from_numpy(some_test).float()) # 注意这里要将np数组转换成一个TENSOR张量
这就是随机设置W
时前向传播得到的数据,确实是1*10
的大小。
Python 的魔法方法
__call__
:这里可能会有人觉得奇怪,为什么实例化一个fc_model之后就会自动调用forward呢?
原因就是在 PyTorch 的nn.Module
类中,实际上重写了__call__
方法,以便在模型对象上调用时能够执行forward
方法。下面是一个
__call__
方法使用的经典例子class FindPartner: def __init__(self, name): self.name = name def __call__(self, another): return self.say_hello(another) def say_hello(self, another): print('{}快来,我是{}'.format(another, self.name)) findpartner = FindPartner('老刘') findpartner('老张') ### Output: ### 老张快来,我是老刘
梯度下降
接下来我们开始梯度下降,试图通过100次迭代将0~9
拟合成1~10
from torch import optim
criterion = nn.MSELoss()
optimizer = optim.SGD(fc_model.parameters(), lr=0.01)
losses = []
for iter_ in range(100):
inputs = torch.from_numpy(some_test).float() # 0 ~ 9
targets = torch.from_numpy(some_test+1).float() # 1 ~ 10
outputs = fc_model(inputs)
loss = criterion(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses.append(loss)
losses
能看到loss值一直在下降
# 使用detach()方法移除梯度信息,然后再转换为NumPy数组
losses_np = [loss.detach().numpy() for loss in losses]
plt.plot(losses_np)
fc_model(torch.from_numpy(np.array([0,1,2,3,4,5,6,7,8,9])).float())
最后输入的1~9
非常接近1~10
了。(不过此时输入其他数字都会输出1~10
,原因是训练数据太少了发生了过拟合)
训练
理解了PyTorch每步做了什么,我们把真实数据送进去训练看看。
首先定义一个生成训练集的函数,做法是从DataFrame中随机选择一行,并从该行中随机选择一个起始列索引begin_column,然后返回从该列开始的连续sample_size个值以及其后的sample_size个值。
import random
def sample_from_table(sample_size, dataframe):
sample_row = dataframe.sample().values[0]
begin_column = random.randint(0, len(sample_row) - sample_size - 1) # 这么取值是为了防止越界
return (sample_row[begin_column: begin_column + sample_size],
sample_row[begin_column + 1: begin_column + sample_size + 1])
fc_model = FullyConnected(x_size=10, hidden_size=3, output_size=10)
fc_model = fc_model.double()
criterion = nn.MSELoss()
optimizer = optim.SGD(fc_model.parameters(), lr=0.01)
n_epochs = 100
n_iters = 50
# losses = []
losses = np.zeros(n_epochs) # 每个epoch记录一个loss
for epoch in range(n_epochs):
for iter_ in range(n_iters):
inputs_, targets_ = sample_from_table(50, timeseries_revenue)
# 这里我们一次送入5批数据进行训练,这样可以加快训练速度
inputs = torch.from_numpy(np.array([inputs_[:10],
inputs_[10:20],
inputs_[20:30],
inputs_[30:40],
inputs_[40:50]],
dtype=np.double))
targets = torch.from_numpy(np.array([targets_[:10],
targets_[10:20],
targets_[20:30],
targets_[30:40],
targets_[40:50]],
dtype=np.double))
outputs = fc_model(inputs.double())
loss = criterion(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses[epoch] += loss
if iter_ % 10 == 0:
plt.clf()
plt.ion()
plt.title("Epoch {}, iter {}".format(epoch, iter_))
plt.plot(torch.flatten(outputs.detach()),'r-',linewidth=1,label='Output') # 拟合得到结果
plt.plot(torch.flatten(targets),'c-',linewidth=1,label='Label') # 目标结果
plt.plot(torch.flatten(inputs),'g-',linewidth=1,label='Input') # 输入数据(目标结果沿时间轴平移-1)
plt.draw()
plt.pause(0.05)
下面是第一轮epoch前10次迭代的结果,可以看到Output
基本是乱来的
但是到了第40轮epoch结束,似乎学得有点样子了
训练结束时,模型基本已经能够预测出下一天的营收了(效果其实还不错了,注意看纵轴标度和最开始不一样)。
我们打印一下loss值看看
losses[-10:]
plt.plot(losses)
可以调一下学习率和模型层数看看能不能有更低的loss。
效果不好的情况
RNN 并不适用于解决那些 “此刻(t)数据与之前(0~t-1)数据没有明显关系的问题”。
这里我们导入一个稍微复杂一点的数据集——time_serise_sale,是每天的销售额
timeseries_sales = pd.read_csv('time_serise_sale.csv')
timeseries_sales = timeseries_sales.drop(timeseries_sales.columns[0], axis=1)
timeseries_sales
plt.plot(timeseries_sales.sample().values[0])
如果我们再用上面的模型对它进行预测,会发现模型预测得很“挣扎”
我们也是打印一下loss看看
plt.plot(losses)
别被大趋势所呈现出的“假象”所迷惑,事实上第10次epoch后我们的梯度下降是没卵用的
plt.plot(losses[10:])
也就是说,传统的全连接网络在处理上述问题时是存在缺陷的。
RNN 循环神经网络
循环神经网络
说了这么多终于,来到了我们这篇文章的主角——RNN 循环神经网络。
在传统神经网络中,我们这么定义中间层
y
=
σ
(
w
x
+
b
)
y=\sigma(wx+b)
y=σ(wx+b)
如果x
是一个2*4的向量,那么就有
(
1
2
3
4
5
6
7
8
)
˙
(
a
1
a
2
a
3
a
4
a
5
a
6
a
7
a
8
)
\begin{pmatrix} 1&2&3&4\\ 5&6&7&8 \end{pmatrix} \dot{} \begin{pmatrix} a_1&a_2\\a_3&a_4\\ a_5&a_6\\a_7&a_8 \end{pmatrix}
(15263748)˙
a1a3a5a7a2a4a6a8
但是在这个计算过程中,我们并没有把前后层之间的依赖关系纳入考虑。
那么怎么将前后关系纳入考量呢?
网络结构
有人就发明了下面这种网络结构,从而让计算x^(t)
的时候将x^(t-1)
的信息也记录下来
其中,x^(t)
就可以理解为上面所说第9天的数据,那么x^(t-1)
和x^(t+1)
就分别是第8天和第10天的。
数学表示
如果从数学上表示这种关系,那么
-
先前我们这么定义神经网络:
y t = σ ( W x t + b ) y t + 1 = σ ( W x t + 1 + b ) y_t = \sigma(Wx_t+b)\\ y_{t+1} = \sigma(Wx_{t+1}+b) yt=σ(Wxt+b)yt+1=σ(Wxt+1+b) -
现在我们重新定义神经网络:
- Elman networks
h t = σ h ( W h x t + U h h t − 1 + b h ) y t = σ y ( W y h t + b y ) h_t = \sigma_h(W_hx_t+U_hh_{t-1}+b_h)\\ y_t = \sigma_y(W_yh_t+b_y) ht=σh(Whxt+Uhht−1+bh)yt=σy(Wyht+by)
或者
- Jordan networks
h t = σ h ( W h x t + U h y t − 1 + b h ) y t = σ y ( W y h t + b y ) h_t = \sigma_h(W_hx_t+U_hy_{t-1}+b_h)\\ y_t = \sigma_y(W_yh_t+b_y) ht=σh(Whxt+Uhyt−1+bh)yt=σy(Wyht+by)
这两种定义方式都通过
h_t
的计算包含了依赖关系,前者是在计算y_t
的时候包含了上一次的权重h_{t-1}
,后者包含了y_{t-1}
。从全过程来看,理论上此刻的y_t
将与先前所有的x
都相关。 - Elman networks
训练
我们首先定义一个简单的循环神经网络
参数
n_layers
指的是有几层循环神经网络叠加,下图是一个两层叠加的图例
class RecurrentModel(nn.Module):
def __init__(self, x_size, hidden_size, n_layers, batch_size, output_size):
super(RecurrentModel, self).__init__()
self.hidden_size = hidden_size
self.n_layers = n_layers
self.batch_size = batch_size
self.rnn = nn.RNN(x_size, hidden_size, n_layers, batch_first=True)
self.out = nn.Linear(hidden_size, output_size)
def forward(self, inputs, hidden=None):
hidden = self.init_hidden()
output, hidden = self.rnn(inputs.float(), hidden.float())
output = self.out(output.float())
# 记得循环神经网络需要返回y和h两个值用于下一步计算
return output, hidden
def init_hidden(self):
hidden = torch.zeros(self.n_layers, self.batch_size, self.hidden_size, dtype=torch.double) # 这里需要注意一下顺序
return hidden
接着,参考全连接网络一样构建训练代码,为了比较时的公平,将hidden_size
也设置成8层
PyTorch中的
unsqueeze
函数:用于在张量(Tensor)的特定维度上增加一个新的维度,作用是改变张量的形状,使其维度增加。
unsqueeze
接受一个整数参数,该参数指定在哪个维度上插入新的维度。例如,如果我们有一个一维张量tensor
,形状为(n,)
,那么可以使用unsqueeze
将其变成一个二维张量,形状为(n, 1)
,如下所示:import torch # 创建一个一维张量 tensor = torch.tensor([1, 2, 3]) # 使用unsqueeze在第二维度上插入一个新维度 new_tensor = tensor.unsqueeze(1) print(new_tensor) ### Output: ### tensor([[1], ### [2], ### [3]])
在上述示例中,
unsqueeze(1)
在第二维度上插入了一个新维度,将原始一维张量变成了一个二维张量。或者我们在第二个维度上插入新的维度torch.from_numpy(np.array([[1,2,3], [2,3,4]])).unsqueeze(2) ### Output: ### tensor([[[1], ### [2], ### [3]], ### ### [[2], ### [3], ### [4]]], dtype=torch.int32)
unsqueeze
函数在处理输入数据的形状时非常有用,特别是在深度学习中,因为有些模型需要特定形状的输入数据。在下面代码中,
unsqueeze(2)
的作用是将形状为(batch_size, seq_length)
的二维张量变成了形状(batch_size, seq_length, 1)
的三维张量,这是因为 RNN 模型通常需要三维的输入,其中一个维度表示时间步(sequence length),用于告诉模型现在进行到时间步中的哪一步(注意理解 RNN 网络需要一个一个节点顺序输入,而不是像全连接网络一样以1*10的向量为单位整个输入)。
# fc_model = FullyConnected(x_size=10, hidden_size=3, output_size=10)
x_size = 1
hidden_size = 8
n_layers = 2
batch_size = 5
seq_length = 10 # 我们期望获得的数据长度
n_sample_size = 50 # 一次输入的数据个数
output_size = 1
rnn_model = RecurrentModel(x_size, hidden_size, n_layers, int(n_sample_size/seq_length), output_size)
rnn_model = rnn_model.float() # 这里的模型要和下面inputs的类型对应上
criterion = nn.MSELoss()
optimizer = optim.SGD(rnn_model.parameters(), lr=0.01)
n_epochs = 100
n_iters = 50
# losses = []
losses = np.zeros(n_epochs) # 每个epoch记录一个loss
hidden = rnn_model.init_hidden()
# hidden = None
for epoch in range(n_epochs):
for iter_ in range(n_iters):
inputs_, targets_ = sample_from_table(50, timeseries_sales)
# 这里我们一次送入5批数据进行训练,这样可以加快训练速度
inputs = torch.from_numpy(np.array([inputs_[:10],
inputs_[10:20],
inputs_[20:30],
inputs_[30:40],
inputs_[40:50]],
dtype=np.float32)).unsqueeze(2)
targets = torch.from_numpy(np.array([targets_[:10],
targets_[10:20],
targets_[20:30],
targets_[30:40],
targets_[40:50]],
dtype=np.float32)).unsqueeze(2)
outputs, _ = rnn_model(inputs.double(), hidden.double())
loss = criterion(outputs, targets.float())
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses[epoch] += loss
if iter_ % 10 == 0:
plt.clf()
plt.ion()
plt.title("Epoch {}, iter {}".format(epoch, iter_))
plt.plot(torch.flatten(outputs.detach()),'r-',linewidth=1,label='Output') # 拟合得到结果
plt.plot(torch.flatten(targets),'c-',linewidth=1,label='Label') # 目标结果
plt.plot(torch.flatten(inputs),'g-',linewidth=1,label='Input') # 输入数据(目标结果沿时间轴平移-1)
plt.draw()
plt.pause(0.05)
看看结果,这是最开始
才第9个epoch就学得有点样子了
同样是第40轮的表现,相比于全连接模型的“挣扎”,RNN 的表现要好太多
最后学成了这样
我们再来看看loss的下降
plt.plot(losses)
同样放大检查一下
plt.plot(losses[10:])
的确是在有效下降。
注:对于第一个简单的数据集,RNN 的效果也会更好,基本10个epoch就能搞定了。
改进
事实上,当我们在做文本翻译的问题时,一个单词的含义不仅和其前面一个字 (所有字) 有关,还跟其后面一个字 (所有字) 有关。
所以我们在使用 RNN 的时候,还可以把后一个x
值包含到当前节点的输入里(尽管它实在后一时刻产生的)。这时我们的 RNN 就从Stacked RNN
变成了Bidirectional RNN
,PyTorch中也内置了bidirectional
参数用于双向传播。
挑战 The Challenge of Long-Term Dependencies
RNN 固然有很多优点,但也会随之而来一些问题。
Vanishing, Exploding Problem
比如上一篇文章我们提到了梯度消失与梯度爆炸 (Gradient Vanishing & Gradient Exploding)
由于 RNN 的结构在求偏导时很容易出现类似上图这种情况,结果就是偏导的连乘中有乘数很大 (小),那么得到的结果就会很大 (小),导致梯度爆炸即loss突然变得很大 (梯度消失即loss一直不更新)。
解决的办法上一篇文章页详细说过,有
- BN 结,即批标准化 (Batch Normalization):相当于一个放大器,将过小的损失进行放大
- 梯度裁剪 (Gradient Clipping):直接将过大的梯度值变小
等等。
LSTM 长短期记忆神经网络
LSTM 全称 Long Short Term Memory,直译过来就是长短期记忆,但根据我们之前的讲解很容易理解,它的能力其实是——既能处理长序列、又能处理短序列。
LSTM 的出现其实就是为了解决深度很深的模型容易出现梯度消失和梯度爆炸这一问题的。
这个问题是通过
门(Gate)
来解决的,这里的Gate更像是“阀门”:用于控制多少信息流到下一层的一种结构。Gate: Information Control
- Converge faster
- Detect long-term dependencies
- Gates are a way to optionally let information through. They are composed out of sigmoid neural net layer and a pointwise multiplication operation.
这是一般的 RNN 结构
这是 LSTM 的结构
LSTM 原理
我们来解释一下 LSTM 到底做了什么
f
t
=
σ
(
W
f
⋅
[
h
t
−
1
,
x
t
]
+
b
f
)
i
t
=
σ
(
W
i
⋅
[
h
t
−
1
,
x
t
]
+
b
i
)
C
t
~
=
t
a
n
h
(
W
c
⋅
[
h
t
−
1
,
x
t
]
+
b
c
)
C
t
=
f
t
∗
C
t
−
1
+
i
t
∗
C
t
~
O
t
=
σ
(
W
o
[
h
t
−
1
,
x
t
]
+
b
o
)
h
t
=
O
t
∗
t
a
n
h
(
C
t
)
f_t = \sigma(W_f\cdot[h_{t-1}, x_t]+b_f)\\ i_t = \sigma(W_i\cdot[h_{t-1}, x_t]+b_i)\\ \widetilde{C_t} = tanh(W_c\cdot[h_{t-1}, x_t]+b_c)\\ C_t = f_t*C_{t-1}+i_t*\widetilde{C_t}\\ O_t = \sigma(W_o[h_{t-1}, x_t]+b_o)\\ h_t = O_t*tanh(C_t)
ft=σ(Wf⋅[ht−1,xt]+bf)it=σ(Wi⋅[ht−1,xt]+bi)Ct
=tanh(Wc⋅[ht−1,xt]+bc)Ct=ft∗Ct−1+it∗Ct
Ot=σ(Wo[ht−1,xt]+bo)ht=Ot∗tanh(Ct)
-
Step-01: Decide how much past data it should remember
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)
这里的f_t
指的是forget gate
,决定前一步 (previous time step) 中删掉哪个不重要的信息。可以看到
f_t
是一个sigmoid函数的输出值,也就是说它的范围在0~1。sigmoid表示——留多少信息,其他 sigmoid同理。 -
Step-02: Decide how much this unit adds to current state
i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) C t ~ = t a n h ( W c ⋅ [ h t − 1 , x t ] + b c ) i_t = \sigma(W_i\cdot[h_{t-1}, x_t]+b_i)\\ \widetilde{C_t} = tanh(W_c\cdot[h_{t-1}, x_t]+b_c) it=σ(Wi⋅[ht−1,xt]+bi)Ct =tanh(Wc⋅[ht−1,xt]+bc)
这里的i_t
指的是input gate
,根据信息的重要程度决定哪些信息在当前步 (current time step) 可以通过 (let through)。至此,我们就能看懂这个式子了
C t = f t ∗ C t − 1 + i t ∗ C t ~ C_t = f_t*C_{t-1}+i_t*\widetilde{C_t} Ct=ft∗Ct−1+it∗Ct
其中,f_t
的范围是01,`i_t`的范围是01,~C_t
的范围是-1~1。之所以~C_t
用tanh函数是因为tanh表示——对下一步的影响 (可能是负的),其他 tanh同理。于是就有,
现在的
C_t
= 上一次的C_{t-1}
×遗忘门 (决定删掉过去多少)
+ 这次计算出的~C_t
×输入门 (决定保留这次多少)
。 -
Step-03: Decide what part of the current cell state makes it to the output
O t = σ ( W o [ h t − 1 , x t ] + b o ) h t = O t ∗ t a n h ( C t ) O_t = \sigma(W_o[h_{t-1}, x_t]+b_o)\\ h_t = O_t*tanh(C_t) Ot=σ(Wo[ht−1,xt]+bo)ht=Ot∗tanh(Ct)
这里的O_t
指的是output gate
,允许那些通过的信息 (passed information) 去影响当前步 (current time step) 的输出。
我将各自的参数以及输出结果都在下图中标注出来了,对照一下公式应该还是很清晰的。
所以回到最开始的问题,为什么 LSTM 能够解决梯度消失和梯度爆炸的问题?
这里可以从两方面进行解释:
- 选择性过滤:LSTM 只保留了重要的数据,而过滤掉了那些不重要的,因此在求偏导的时候数据会更加“正常”。
- 微观解释:在求导时我们涉及到求
∂C_t/∂C_{t-1}
,也正是在这个过程中容易梯度爆炸或消失。但是在 LSTM 中会发现求导结果就是f_t
(注意f_t
在每步中是由W_f
拟合得到的),假如C_{t-1}
对C_t
影响很大,那么f_t
就会接近1,反之f_t
就会接近0。这样一来,遗忘门f_t
的值就是偏导结果,影响大结果就是1,影响小结果就是0表示不更新,从而避免了在一连串的求偏导中部分极端值影响最终偏导结果的情况。
Peephole Connection
We let the gate layers look at the cell state
当然 LSTM 不止这一种结构,一种叫Peephole Connection的结构是这么设计的
也就是额外将C_{t-1}
和C_t
加入了三个gates的计算中 (*)
∗
f
t
=
σ
(
W
f
⋅
[
C
t
−
1
,
h
t
−
1
,
x
t
]
+
b
f
)
∗
i
t
=
σ
(
W
i
⋅
[
C
t
−
1
,
h
t
−
1
,
x
t
]
+
b
i
)
C
t
~
=
t
a
n
h
(
W
c
⋅
[
h
t
−
1
,
x
t
]
+
b
c
)
C
t
=
f
t
∗
C
t
−
1
+
i
t
∗
C
t
~
∗
O
t
=
σ
(
W
o
[
C
t
,
h
t
−
1
,
x
t
]
+
b
o
)
h
t
=
O
t
∗
t
a
n
h
(
C
t
)
*\space f_t = \sigma(W_f\cdot[\boldsymbol{C_{t-1}}, h_{t-1}, x_t]+b_f)\\ *\space i_t = \sigma(W_i\cdot[\boldsymbol{C_{t-1}}, h_{t-1}, x_t]+b_i)\\ \widetilde{C_t} = tanh(W_c\cdot[h_{t-1}, x_t]+b_c)\\ C_t = f_t*C_{t-1}+i_t*\widetilde{C_t}\\ *\space O_t = \sigma(W_o[\boldsymbol{C_t}, h_{t-1}, x_t]+b_o)\\ h_t = O_t*tanh(C_t)
∗ ft=σ(Wf⋅[Ct−1,ht−1,xt]+bf)∗ it=σ(Wi⋅[Ct−1,ht−1,xt]+bi)Ct
=tanh(Wc⋅[ht−1,xt]+bc)Ct=ft∗Ct−1+it∗Ct
∗ Ot=σ(Wo[Ct,ht−1,xt]+bo)ht=Ot∗tanh(Ct)
RNN的改进版:GRU
Simpler and fewer parameters
GRU 全称 Gated Recurrent Unit,它之所以能用到更少的参数是因为遗忘门f_t
和输入门i_t
之间存在一定关系——遗忘门f_t
越大表示对过去记忆保留越多,输入门i_t
越大表示对当前结果保留越多,因此我们完全可以设置f_t = 1 - i_t
。
因此,GRU 的构造如下
满足
r
t
=
σ
(
W
r
⋅
[
h
t
−
1
,
x
t
]
)
z
t
=
σ
(
W
z
⋅
[
h
t
−
1
,
x
t
]
)
h
t
~
=
t
a
n
h
(
W
⋅
[
r
t
∗
h
t
−
1
,
x
t
]
)
h
t
=
(
1
−
z
t
)
∗
h
t
−
1
+
z
t
∗
h
t
~
r_t = \sigma(W_r\cdot[h_{t-1}, x_t])\\ z_t = \sigma(W_z\cdot[h_{t-1}, x_t])\\ \widetilde{h_t} = tanh(W\cdot[r_t*h_{t-1}, x_t])\\ h_t = (1-z_t)*h_{t-1}+z_t*\widetilde{h_t}\\
rt=σ(Wr⋅[ht−1,xt])zt=σ(Wz⋅[ht−1,xt])ht
=tanh(W⋅[rt∗ht−1,xt])ht=(1−zt)∗ht−1+zt∗ht
其中,r_t
对应是遗忘门,z_t
对应的是输入门(当然在这里如何对应已经无所谓了)。
可以看到,GRU 减少了一个参数W
(要知道这可是一个矩阵),模型参数减少的结果就是:
参数少了
->所需要的数据就少了
->同样的数据下模型拟合得就快了
->下降的时候模型变简单了
->更不容易过拟合
也就是训练得又快又好。
RNN 模型的应用 RNN Applications
RNN 可以被应用在以下方面
-
预测问题 Prediction Problems
-
语言 (情感) 判断&文本生成 Language Modelling and Generating Text
NLP
-
机器翻译 Machine Translation
-
语音识别 Speech Recognition
-
基于图片生成描述 Generating Image Descriptions
Language Caption (2015)
-
视频标签 Video Tagging
-
文本总结 Text Summarization
-
客服中心对话分析 Call Center Analysis
-
音乐生成 Music composition