原文地址:(41条消息) 深度学习(十一)RNN入门学习_hjimce的专栏-CSDN博客

1. 简介

RNN(Recurrent Neural Networks)中文名又称之为:循环神经网络,是一类以序列数据为输入,在序列的演进方向上进行递归且所有节点(循环单元)按链式连接的递归神经网络。

循环神经网络具有记忆性,参数共享且图灵完备,因此对序列的非线性特征学习具有一定优势。其在计算机视觉里面用的比较少。RNN在自然语言处理(NLP),例如语音识别,语言建模,机器翻译,语音音频等领域有应用,也被用于各种时间序列预报。引入了卷积神经网络(CNN)构筑的循环神经网络可以处理包含序列输入的计算机视觉问题。

关于RNN能处理时间序列的原因:RNN主要用来处理序列数据,在传统的神经网络模型中,是从输入层到隐藏层再到输出层,层与层之间是全连接的,每层之间的节点是无连接的。但是这种普通的神经网络对于很多问题却无能为力。例如,你要预测句子的下一个单词是什么,一般要用到前面的单词,因为前后单词不是无关的。RNN之所以被称为循环神经网络,即一个序列的输出也与前面的输出有关,就是因为其会对前面的信息进行记忆并应用于当前输入的计算中,即隐藏层之间的节点不再是无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。理论上,RNN能对任何长度的序列数据进行处理,但在实践中,为了降低复杂性往往假设当前的状态只与前面几个状态有关。

2. 循环神经网络原理

首先回顾一下简单的MLP三层神经网络模型:

上面这个图就是最简单的浅层网络模型了,是一个向量,为输入层的值,是一个向量,为隐藏层的值,也是一个向量,为输出层的值。U,V就是我们需要学习的参数了,其中U是输入层到隐藏层的权重矩阵,V是隐藏层到输出层的权重矩阵。上面的图很简单,每层神经元的个数只有一个,我们可以得到如下公式:

(1)隐藏层神经元的激活值为:s=f(u*x+b1);

(2)输出层的激活值为:o=f(v*s+b2);

这就是最简单的三层神经网络模型的计算公式了。上面经过线性变化后,再用了激活函数f进行映射。其实RNN的网络结构图,仅仅是在上面加了一条连接线而已,RNN结构图:

RNN的基础结构,只有一个输入层,隐藏层,输出层,看起来跟传统的浅层神经网络模型差不多,唯一的区别是:上面的隐藏层多了一圈连接线,而这条线就是所谓的循环递归。同时线上还有一个多的参数W,RNN的展开图如下:

我们直接看上图Ot的计算流程。可以看到隐藏层单元st的输入有两个:来自xt的输入、来自st-1的输入,前者是本层的输入,后者是上层的隐藏层激活值输出。于是RNN,t时刻的计算公式如下:

(1)t时刻,隐藏层神经元的激活值为:st=f(U*xt+W*st-1+b1);

(2)t时刻,输出层的激活值为:ot=g(V*st+b2)

可以看到,跟一开始给出的MLP,公式上就差那么一点点。仅仅只是上面的st计算的时候,在函数f变量计算的时候,多个一个w*st-1。

其中(2)是输出层的计算公式,输出层是一个全连接层,其每个节点斗鱼隐藏层每个节点相连。V是输出层的权重矩阵,g是激活函数。(1)是隐藏层的计算公式,它是循环层,U是输入x的权重矩阵,W是上一次的值st-1的输入的权重矩阵,f是激活函数

重点:在上面的符号中,x是输入,h是隐藏单元,O为输出,y为训练集的标签,L 损失函数。注意,隐藏单元h在t时刻的表现不仅受t时刻输入  x  的影响,还受上一时刻 t-1 的状态影响 h(t-1) ,所以体现出记忆网络的特点,此记忆为长时记忆。其中,W,V,U是权值,同一类型的权值相同,尽管在每一个时刻都参与运算,但一个RNN网络中每个类型的权值是不变的,由公式可以看出,U用来与输入x相乘,W与上一个时刻的状态 h(t-1) 相乘,这二者共同构成了此时的单元状态  h(t),V与此时的状态  h(t)  相乘加上权值得到  t  时刻的输出  o(t),最终的模型预测输出为 y(t),其中  y(t)=tanh( o(t) )。

3. 循环神经网络的训练算法

BPTT是针对循环层的训练算法,它的基本原理和BP算法是一样的,包含同样的三个步骤:

(1)前向计算每个神经元的输出值;

(2)反向计算每个神经元的误差项值,它是误差函数E对神经元j的加权输入的偏导数;

   ——将第i层t时刻的误差值沿两个方向传播:

   ——第一个方向是传递到上一层网络,这部分只跟权重矩阵U有关;(相当于把全连接网络旋转90度来看);

   ——第二个方向是沿时间线传递到初始时刻,这部分只跟权重矩阵W有关。

(3)计算每个权重的梯度:最后再用随机梯度下降法更新权重。

4. 语音模型实例

基于RNN的语言模型例子:我们要用RNN做这样一件事情,每输入一个词,循环神经网络就输出截止到目前为止,下一个最有可能的词,如下图所示:

这里写图片描述

步骤一:把词表达为向量的形式

(1)建立一个包含所有词的词典,每个词在词典里有唯一的编号;
(2)任意一个词都可以用一个N维的one-hot向量来表示。

这里写图片描述

这种向量化方法,我们得到了一个高维,稀疏的向量,这之后采用一些降维方法,将高维稀疏的向量转化为低维的稠密向量。

为了输出最可能的词,所以需要计算词典中每个词是当前词的下一个词的概率,再选择概率最大的那一个。

因此,神经网络的输出向量也是一个N维向量,向量中的每个元素对应着词典中相应的词是下一个词的概率:

这里写图片描述

步骤二:采用softmax作为输出层

softmax函数即将输出进行归一化,将输出映射到0~1之间的一个值,即概率,计算方法为:

这里写图片描述

最后得到的结果每个值对应即为该词的输出概率,且和为1。如下图:

这里写图片描述

步骤三:训练

把语料转换成语言模型的训练数据集,即对输出x和标签y进行向量化,y也是一个one-hot 向量:

这里写图片描述

接下来对概率进行建模,一般用交叉熵误差函数作为优化目标,其定义如下:

这里写图片描述

用上面的例子就是:

这里写图片描述

计算过程如下:

这里写图片描述

有了模型,优化目标,梯度表达式,就可以用梯度下降算法进行训练了。

5. 两种改进的RNN

(1)LSTM

LSTM 全称叫 Long Short Term Memory networks,它和传统 RNN 唯一的不同就在与其中的神经元(感知机)的构造不同。传统的 RNN 每个神经元和一般神经网络的感知机没啥区别,但在 LSTM 中,每个神经元是一个“记忆细胞”,细胞里面有一个“输入门”(input gate), 一个“遗忘门”(forget gate), 一个“输出门”(output gate),俗称“三重门”。

这里写图片描述

展开的记忆细胞,上面的黑线是记忆流,下面的黑线是数据流。

工作流如下:在输入门中,根据当前的数据流来控制接受细胞记忆的影响,即通过激活函数产生了一个概率,该概率为接受上一个细胞记忆的概率;接着在遗忘门里,更新这个细胞的记忆和数据流,其中产生了两个值,一个概率一个当前细胞的数据,二者相乘通过激活函数作为记忆与上一个细胞的一部分记忆拼合而成,作为本细胞的记忆输出到下一个细胞,当前的数据经过激活函数作为数据流;最后在输出门输出更新后的记忆,并且产生根据记忆产生更新的数据流,输出。

LSTM 模型的关键之一就在于这个“遗忘门”, 它能够控制训练时候梯度在这里的收敛性(从而避免了 RNN 中的梯度 vanishing/exploding 问题),同时也能够保持长期的记忆性

(2)GRU

GRU也是RNN的改进版本,主要从两个方面进行改良:一是,序列中不同的位置处的单词对当前隐藏层的状态的影响不同,越前面的影响越小,即每个前面状态对当前的影响进行了距离加权,距离越远,权值越小。二是,在产生误差时,误差可能是由某一个或几个单词而引发的,所以应当仅仅对对应的单词weight进行更新。GRU的结构如下图所示,GRU首先根据当前输入单词向量word vector和前一个隐藏层的状态hidden state计算出update gate和reset gate。再根据reset gate、当前word vector以及前一个hidden state计算新的记忆单元内容(new memory content)。当reset gate为1的时候,new memory content忽略之前的所有memory content,最终的memory是之前的hidden state与new memory content的结合。

这里写图片描述

6. 源码实现

下面结合代码,了解RNN在代码层面的实现:

# -*- coding: utf-8 -*-
"""
Created on Thu Oct 08 17:36:23 2015
@author: Administrator
"""
 
import numpy as np
import codecs
 
data = open('text.txt', 'r').read() #读取txt一整个文件的内容为字符串str类型
chars = list(set(data))#去除重复的字符
print chars
#打印源文件中包含的字符个数、去重后字符个数
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) }
print char_to_ix
hidden_size = 100 # 隐藏层神经元个数
seq_length = 20 #
learning_rate = 1e-1#学习率
 
#网络模型
Wxh = np.random.randn(hidden_size, vocab_size)*0.01 # 输入层到隐藏层
Whh = np.random.randn(hidden_size, hidden_size)*0.01 # 隐藏层与隐藏层
Why = np.random.randn(vocab_size, hidden_size)*0.01 # 隐藏层到输出层,输出层预测的是每个字符的概率
bh = np.zeros((hidden_size, 1)) #隐藏层偏置项
by = np.zeros((vocab_size, 1)) #输出层偏置项
#inputs  t时刻序列,也就是相当于输入
#targets t+1时刻序列,也就是相当于输出
#hprev t-1时刻的隐藏层神经元激活值
def lossFun(inputs, targets, hprev):
 
  xs, hs, ys, ps = {}, {}, {}, {}
  hs[-1] = np.copy(hprev)
  loss = 0
  #前向传导
  for t in xrange(len(inputs)):
    xs[t] = np.zeros((vocab_size,1)) #把输入编码成0、1格式,在input中,为0代表此字符未激活
    xs[t][inputs[t]] = 1
    hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh) # RNN的隐藏层神经元激活值计算
    ys[t] = np.dot(Why, hs[t]) + by # RNN的输出
    ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t])) # 概率归一化
    loss += -np.log(ps[t][targets[t],0]) # softmax 损失函数
  #反向传播
  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
    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]
#预测函数,用于验证,给定seed_ix为t=0时刻的字符索引,生成预测后面的n个字符
def sample(h, seed_ix, n):
 
  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)#h是递归更新的
    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 n<20000:
  #n表示迭代网络迭代训练次数。当输入是t=0时刻时,它前一时刻的隐藏层神经元的激活值我们设置为0
  if p+seq_length+1 >= len(data) or n == 0: 
    hprev = np.zeros((hidden_size,1)) # 
    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]]
 
  #当迭代了1000次,
  if n % 1000 == 0:
    sample_ix = sample(hprev, inputs[0], 200)
    txt = ''.join(ix_to_char[ix] for ix in sample_ix)
    print '----\n %s \n----' % (txt, )
 
  # RNN前向传导与反向传播,获取梯度值
  loss, dWxh, dWhh, dWhy, dbh, dby, hprev = lossFun(inputs, targets, hprev)
  smooth_loss = smooth_loss * 0.999 + loss * 0.001
  if n % 100 == 0: print 'iter %d, loss: %f' % (n, smooth_loss) # print progress
  
  # 采用Adagrad自适应梯度下降法,可参看博文:http://blog.csdn.net/danieljianfeng/article/details/42931721
  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) #自适应梯度下降公式
  p += seq_length #批量训练
  n += 1 #记录迭代次数

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐