1. 前言
我们都知道文字与文字之间是有关联的,也就是一个数据与另一个数据之间存在关系。
传统的神经网络并不能做到这点,看起来也像是一种巨大的弊端。例如,假设你希望对电影中的每个时间点的时间类型进行分类。传统的神经网络应该很难来处理这个问题:使用电影中先前的事件推断后续的事件。循环神经网络RNN解决了这个问题。
RNN是被用来设计解决序列性数据的,类似于文本,音频,视频等文件。循环神经网络使用序列中的先前信息生成当前输出。理论上,RNN能够对任何长度的序列数据进行处理。但是在实践中,为了降低复杂性往往假设当前的状态只与前面的几个状态相关。
2. 应用场景
- 文本分析与生成
自然语言处理
RNN可用于词性标注、命名实体识别、句子解析等任务。通过捕获文本中的上下文关系,RNN能够理解并处理语言的复杂结构。
机器翻译
RNN能够理解和生成不同语言的句子结构,使其在机器翻译方面特别有效。
文本生成
利用RNN进行文本生成,如生成诗歌、故事等,实现了机器的创造性写作。 - 语音识别与合成
语音到文本
RNN可以用于将语音信号转换为文字,即语音识别(Speech to Text),理解声音中的时序依赖关系。
文本到语音
RNN也用于文本到语音(Text to Speech)的转换,生成流畅自然的语音。 - 时间序列分析
股票预测
通过分析历史股票价格和交易量等数据的时间序列,RNN可以用于预测未来的股票走势。
气象预报
RNN通过分析气象数据的时间序列,可以预测未来的天气情况。 - 视频分析与生成
动作识别
RNN能够分析视频中的时序信息,用于识别人物动作和行为模式等。
视频生成
RNN还可以用于视频内容的生成,如生成具有连续逻辑的动画片段。
3. RNN经典结构图
从下面的图表中我们能够发现在这段神经网络中,读取某个输入
x
t
x_t
xt,并输出一个值
h
t
h_{t}
ht,循环允许将信息从网络的一个步骤传递到下一个步骤
我们把这段连接展开之后就可以发现,他可以看作是同一神经网络的多个复制
为了帮助更好的理解,可以查看下方的例子:
我们有一段句子,我们把这段句子放进RNN中进行训练
“My class is the best class.”
-
在初始T0时间,第一步是将单词 “My” 输入网络。RNN 生成一个输出。
-
在T1时,然后在下一步中,我们输入单词 “class” 和上一步的激活值。现在 RNN 同时具有单词 “My” 和 “class” 的信息。
-
这个过程一直持续到句子中的所有单词都被输入。
可以查看下面的动画以进行可视化和理解。
4. 公式解释
RNN主要是由输入层,Hidden layer, 输出层来组成,跟一般的神经网络很相似,不过Hidden layer 他是不断指向自身,表示数据的循环更新。
前向传播
RNN的工作原理可以通过下面的数学方程式表示:
其中,
h
t
h_t
ht 表示在时间
t
t
t 的隐藏层状态,
x
t
x_t
xt 表示在时间
t
t
t的输入,
y
t
y_t
yt 表示在时间
t
t
t 的输出,tanh是激活函数,
c
c
c,
b
b
b是偏置层
补充说明:
在计算时,每一步使用的参数
U
U
U、
W
W
W、
b
b
b都是一样的,也就是说每个步骤的参数都是共享的,因为他们是在同一个向量中,但是只是时间不一样
训练与反向传播
与其他形式的神经网络类似,RNN 模型需要经过训练才能在一组输入通过后产生准确且理想的输出。
在训练过程中,对于每条训练数据,我们都会有一个相应的真实标签,或者简单地放置一个我们希望模型输出的“正确答案”。 当然,在我们通过模型传递输入数据的前几次,我们不会获得等于这些正确答案的输出。 然而,在收到这些输出后,我们在训练期间要做的是计算该过程的损失,它衡量模型的输出与正确答案的差距。 使用这个损失,我们可以计算反向传播的损失函数的梯度。
使用我们刚刚获得的梯度,我们可以相应地更新模型中的权重,以便将来使用输入数据进行的计算将产生更准确的结果。 这里的权重是指在前向传播过程中与输入数据和hidden state相乘的权重矩阵。 计算梯度和更新权重的整个过程称为反向传播。 与前向传播相结合,反向传播会一次又一次地循环,每次修改权重矩阵值以挑选出数据模式时,模型的输出都会变得更加准确。
尽管看起来好像每个 RNN Cell都使用不同的权重,但所有权重实际上是相同的,因为 RNN 单元在整个过程中基本上被重复使用。 因此,只有输入数据和前向hidden state在每个时刻都是唯一的。
5.拓展
RNN 其实有多种结构,如下所示:
每个矩形代表一个向量,箭头代表函数(如矩阵乘法)。 输入向量为红色,输出向量为蓝色,绿色向量表示 RNN 的状态(即将详细介绍)。
从左到右:
(1) one to one
没有 RNN 的 Vanilla 处理模式(就是最普通的神经网络模型),从固定大小的输入到固定大小的输出(例如图像分类)。
(2) one to many
序列输出(例如,图像标题处理需要一张图像并输出一个由单词组成的句子)。
(3) many to one
序列输入(例如情感分析,将给定句子分为表达积极情感或消极情感的句子)。
实际上,我们只在最后一个h上进行输出变换就可以了
(4) many to many
序列输入和序列输出(例如机器翻译:RNN 读取英语句子,然后输出法语句子)。
这里的输入、输出为不等长的序列。
这种结构是Encoder-Decoder,也叫Seq2Seq,是RNN的一个重要变种。原始的n-to-n的RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。为此Encoder-Decoder结构先将输入数据编码成一个上下文语义向量c:
语义向量c可以有多种表达方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c,还可以对最后的隐状态做一个变换得到c,也可以对所有的隐状态做变换。
拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。Decoder的RNN可以与Encoder的一样,也可以不一样。具体做法就是将c当做之前的初始状态h0输入到Decoder中:
还有一种做法是将c当做每一步的输入:
(这个其实也是后面的变种分支,之后会继续讲解)
(5) many to many
同步序列输入和输出(例如视频分类,我们希望给视频的每一帧贴上标签)。 请注意,在每种情况下,长度序列都没有预先指定的限制,因为循环变换(绿色)是固定的,我们可以随意应用。
是最经典的RNN结构,上文讲的就是这种RNN结构,输入、输出都是等长的序列数据。
6. RNN的局限:长期依赖(Long-TermDependencies)问题
RNN的关键点之一就是他们可以用来连接先前的信息到当前的任务上,例如使用过去的视频段来推测对当前段的理解。如果RNN可以做到这个,他们就变得非常有用。但是真的可以么?答案是,还有很多依赖因素。
有时候,我们仅仅需要知道先前的信息来执行当前的任务。例如,假设一个语言模型尝试根据前一个单词预测下一个单词。如果我们试图预测 “the clouds are in the sky” 中的最后一个词,我们不需要任何进一步的上下文——很明显下一个词将是 sky。在这样的场景中,相关的信息和预测的词位置之间的间隔是非常小的,RNN可以学会使用先前的信息。
但在某些情况下,我们需要更多的背景信息。考虑尝试预测文本中的最后一个单词。比如我们试着去预测“I grew up in France…I speak fluent French”最后的词“French”。最近的信息表明,下一个词可能是一种语言的名称,但如果我们想缩小语言的范围,我们是需要先前提到的离当前位置很远的“France”的上下文。这说明相关信息和当前预测位置之间的间隔就肯定变得相当的大。
不幸的是,在这个间隔不断增大时,RNN会丧失学习到连接如此远的信息的能力。理论上,RNN 绝对能够处理这种 “长期依赖性”。人类可以仔细为他们选择参数来解决这种形式的玩具问题。可悲的是,在实践中,RNN 似乎无法学习它们。
7. RNN数学逻辑代码复现
RNN的简单计算
作为一个类,RNN 的API由一个单步函数组成:
rnn = RNN()
y = rnn.step(x) # x is an input vector, y is the RNN's output vector
RNN 类有一些内部状态,每次调用 step 时都会更新这些内部状态。 在最简单的情况下,这种状态由单个隐向量 h 组成。下面是 Vanilla RNN 中步进函数的实现:
class RNN:
# ...
def step(self, x):
# update the hidden state
self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
# compute the output vector
y = np.dot(self.W_hy, self.h)
return y
深入学习, RNN 是一种神经网络,如果你戴上深度学习的帽子,开始像叠薄饼一样堆叠模型,一切都会单调地变得更好(如果方法正确的话)。 例如,我们可以如下组建一个双层递归网络:
y1 = rnn1.step(x)
y = rnn2.step(y1)
以下是一个模拟的RNN算法编写,一般用于理解内部的算法运算,后续可以直接写RNN类来调用执行
"""
Minimal character-level Vanilla RNN model. Written by Andrej Karpathy (@karpathy)
BSD License
"""
import numpy as np
# 数据输入/输出(I/O)
data = open('input.txt', 'r').read()
# should be simple plain text file
chars = list(set(data)) #把数据存入到列表中
data_size, vocab_size = len(data), len(chars)
print 'data has %d characters, %d unique.' % (data_size, vocab_size)
char_to_ix = { ch:i for i,ch in enumerate(chars) }
ix_to_char = { i:ch for i,ch in enumerate(chars) }
#创建从字符到索引的映射char_to_ix和从索引到字符的映射ix_to_char
# hyperparameters
hidden_size = 100 # size of hidden layer of neurons
seq_length = 25 # number of steps to unroll the RNN for
learning_rate = 1e-1
# model parameters
Wxh = np.random.randn(hidden_size, vocab_size)*0.01 # input to hidden
Whh = np.random.randn(hidden_size, hidden_size)*0.01 # hidden to hidden
Why = np.random.randn(vocab_size, hidden_size)*0.01 # hidden to output
bh = np.zeros((hidden_size, 1)) # hidden bias
by = np.zeros((vocab_size, 1)) # output bias
def lossFun(inputs, targets, hprev):
"""
inputs,targets are both list of integers.
hprev is Hx1 array of initial hidden state
returns the loss, gradients on model parameters, and last hidden state
"""
xs, hs, ys, ps = {}, {}, {}, {}
hs[-1] = np.copy(hprev)
#保存每个时间步的隐藏状态。初始状态hs[-1]是传入的上一隐藏状态hprev
loss = 0
# forward pass 前向传播
for t in xrange(len(inputs)):
xs[t] = np.zeros((vocab_size,1))
# encode in 1-of-k representation
#用one-hot编码编译字符
xs[t][inputs[t]] = 1
hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh) # hidden state
ys[t] = np.dot(Why, hs[t]) + by # unnormalized log probabilities for next chars
ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t]))
# 应用softmax计算概率分布
#probabilities for next chars
loss += -np.log(ps[t][targets[t],0])
# softmax (cross-entropy loss)
# 计算交叉熵损失,累加到总损失中
# backward pass: compute gradients going backwards
"""
从最后一个时间步开始向后遍历,计算每个参数的梯度:
对输出概率进行调整以反映实际观察到的目标字符。
通过链式法则计算各层的梯度。
通过tanh反向传递错误。
对梯度应用梯度裁剪,以防止梯度爆炸。
"""
dWxh, dWhh, dWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)
dbh, dby = np.zeros_like(bh), np.zeros_like(by)
dhnext = np.zeros_like(hs[0])
for t in reversed(xrange(len(inputs))):
dy = np.copy(ps[t])
dy[targets[t]] -= 1
# backprop into y. see http://cs231n.github.io/neural-networks-case-study/#grad if confused here
dWhy += np.dot(dy, hs[t].T)
dby += dy
dh = np.dot(Why.T, dy) + dhnext # backprop into h
dhraw = (1 - hs[t] * hs[t]) * dh # backprop through tanh nonlinearity
dbh += dhraw
dWxh += np.dot(dhraw, xs[t].T)
dWhh += np.dot(dhraw, hs[t-1].T)
dhnext = np.dot(Whh.T, dhraw)
for dparam in [dWxh, dWhh, dWhy, dbh, dby]:
np.clip(dparam, -5, 5, out=dparam) # clip to mitigate exploding gradients
return loss, dWxh, dWhh, dWhy, dbh, dby, hs[len(inputs)-1]
def sample(h, seed_ix, n):
"""
sample a sequence of integers from the model
h is memory state, seed_ix is seed letter for first time step
这个函数用于生成文本样本,模拟如何在实际应用中使用训练好的RNN模型
"""
x = np.zeros((vocab_size, 1))
x[seed_ix] = 1
ixes = []
for t in xrange(n):
h = np.tanh(np.dot(Wxh, x) + np.dot(Whh, h) + bh)
y = np.dot(Why, h) + by
p = np.exp(y) / np.sum(np.exp(y))
ix = np.random.choice(range(vocab_size), p=p.ravel())
x = np.zeros((vocab_size, 1))
x[ix] = 1
ixes.append(ix)
return ixes
n, p = 0, 0
mWxh, mWhh, mWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)
mbh, mby = np.zeros_like(bh), np.zeros_like(by) # memory variables for Adagrad
smooth_loss = -np.log(1.0/vocab_size)*seq_length # loss at iteration 0
#这个初始值基本上是在假设模型在开始时对每个字符的预测都是随机的
while True:
# prepare inputs (we're sweeping from left to right in steps seq_length long)
if p+seq_length+1 >= len(data) or n == 0:
hprev = np.zeros((hidden_size,1)) # reset RNN memory
p = 0 # go from start of data
inputs = [char_to_ix[ch] for ch in data[p:p+seq_length]]
targets = [char_to_ix[ch] for ch in data[p+1:p+seq_length+1]]
# sample from the model now and then
if n % 100 == 0:
sample_ix = sample(hprev, inputs[0], 200)
txt = ''.join(ix_to_char[ix] for ix in sample_ix)
print '----\n %s \n----' % (txt, )
# forward seq_length characters through the net and fetch gradient
loss, dWxh, dWhh, dWhy, dbh, dby, hprev = lossFun(inputs, targets, hprev)
smooth_loss = smooth_loss * 0.999 + loss * 0.001
#每次计算出的新损失 loss 会以小部分(0.001)加入到 smooth_loss 中,而 smooth_loss 的大部分(0.999)保留其原有值。这种方法称为指数加权平均(Exponential Moving Average),是一种常见的序列数据平滑技术,用于减少噪声并捕捉趋势。
if n % 100 == 0: print 'iter %d, loss: %f' % (n, smooth_loss) # print progress
# perform parameter update with Adagrad
for param, dparam, mem in zip([Wxh, Whh, Why, bh, by],
[dWxh, dWhh, dWhy, dbh, dby],
[mWxh, mWhh, mWhy, mbh, mby]):
mem += dparam * dparam
param += -learning_rate * dparam / np.sqrt(mem + 1e-8) # adagrad update
p += seq_length # move data pointer
n += 1 # iteration counter
文献参考
前两篇是RNN非常经典的博客,强推
Understanding LSTM Networks
The Unreasonable Effectiveness of Recurrent Neural Networks
Medium: RNN vs GRU vs LSTM
知乎:初学者入门,使用pytorch构建RNN网络
csdn: RNN详解(Recurrent Neural Network)