深度学习基础模型算法原理及编程实现–06.循环神经网络
前面介绍的全连接神经网络和卷积神经网络,都只能单独的取处理一个个的输入,前一个输入和后一个输入是完全没有关系的。但在处理序列信息时,前后输入是相关的,如语义理解需要将前后的词语贯联起来;视频处理也需要前后多帧图像的分析。这时就需要引入深度学习领域中循环神经网络(Recurrent Neural Network)来解决问题。正如全连接及卷积神经网络可以处理大小可变的图像,循环神经网络也可以处理长度变化的序列问题。
循环神经网络简介
RNN在自然语言处理中应用的较多,比如语言模型,通过给定一句话前面的部分,预测接下来最有可能的一个词是啥。在语音转文本(STT)的应用中,声学模型输出的结果可能是若干个可能的候选词,这时就需要语言模型从中选择一个最可能的词;图像到文本的识别(orc)也是类似的。
使用RNN之前,语言模型主要采用N-Gram,N是一个正整数,表示一个词出现的概率只与前面N个词汇相关。而一个词的出现可能与前面整个句子的语境都相关,且句子的长度可能很长,从而只能增加N;但N的增大会导致训练模型大小呈指数级增加。
RNN理论上可以往前(后)看任意多个词。循环神经网络种类繁多,。。。
7.2 循环神经网络的设计原理
说到这里你可能一头雾水,不知道为什么循环神经网络的节点要设计成这样,这里给大家一个直观的理解,之前介绍的全连接和卷积都是前向网络,然后通过隐藏层向前传播到输出层。在前馈网络中,输入没有顺序可言。
可用下面的公式表示上图前向神经网络的计算方法:
循环神经网络就是为了解决序列问题而提出的,可以简单将其理解为由多个前向网络并排组成的网络,并且隐藏层之间存在信息传递(路由),其网络结构示意如下图所示。
为什么要这么做呢,这里举一个例子:预测steep一词中的下一个字母,为了简化问题,可假设整个字母表由 S、T、E 和 P 组成,从正常的前馈网络开始,传入字母S期望的输出为T,传入T期望获得E,现在传入E,在这个词中E后面跟另一个E或P,而这里显示的网络没有足够的信息来决定预测哪一个字母是正确的结果。要解决这个问题,需要获得关于字母历史序列的信息,一个很显然的方法是将上一步的隐藏层输出路由到下一个隐藏层的输入中,即将隐藏层输出信息做为下一隐藏层的输入信息的一部分。所以现在,当网络看到E时,由于隐藏层之间信息的路由,网络知道字母序列在前面看到了S、T和E,所以下一个字母应该为E,就不会在E和P之间迷惑了,这就是循环神经网络。
7.2 循环神经网络模型
7.2.1 RNN VS CNN
(1) CNN单个隐藏层中的节点之间是没有相连关系的,但RNN中是有的。
(2) CNN中隐藏层输入仅包含当前的输入,而RNN隐藏层的输入不仅包含当前的输入,还会对前面输入的信息进行记忆并应用于当前的输出计算
7.2.2 单向循环神经网络单个节点
单向循环神经网络隐藏层中的神经元节点模型可表示如下:
从上式可以看出RNN的输出受前面所有时刻输出信息的影响,这就说明了为什么循环神经网络各个节点会对前面输入的信息进行记忆并应用于当前输出的原因。
7.2.3 双向循环神经网络单个节点
对语言模型而言,如果想对某个有上下文的句子填入某个词,是要同时观察前面和后面的信息的,因此需要双向神经网络。双向神经网络隐藏层中第k层第t个时刻的神经元节点模型可表示如下:
7.2.4 循环神经网络
将上面的双向循环神经网络隐藏层扩展成 层 个时刻的深度循环神经网络模型,当 ,即隐藏层深度大于等于2时,循环神经网络又可称为深度神经网络,其模型可表示如下:
7.3 循环神经网络的训练
循环神经网络的训练算法:BPTT(Backpropagation through time,随时间反向传播)
BPTT算法是针对循环层的训练算法,它的基本原理和BP算法是一样的,也包含同样的三个步骤:
1. 前向计算每个神经元的输出值;
2. 反向计算每个神经元的误差项值,它是误差函数E对神经元j的加权输入的偏导数;
3. 计算每个权重的梯度。
最后再用随机梯度下降算法更新权重。
7.3.1 前向计算
前向计算在式(8.6)中已经描述过了,只需将其拆分开来即可。
7.3.1.1 输入层
7.3.1.2 隐藏层
7.3.1.3 输出层
7.3.2 误差项计算
BTPP算法将第l层t时刻的误差项值沿两个方向传播,一个方向是沿隐藏层深度方向将其传递到上一层网络,这部分只误差项只和权重矩阵U有关;另一个是方向是将其沿时间轴传递到初始时刻,这部分误差项只和权重矩阵W有关。
7.3.2.1 误差项沿时间轴的传递规律
7.3.2.2 误差项沿网络深度的传递规律
7.3.2.2.1 隐藏层沿网络深度的误差项
7.3.2.2.2 输出层误差项
7.3.3 权重矩阵梯度更新
7.4 梯度爆炸和消失
基本的循环神经网络不能较好的处理较长序列,因为其在训练过程中很容易发生梯度爆炸和梯度消失,这导致训练时梯度不能在较长序列中一直传递下去,从而使RNN无法捕捉到长距离的影响。
为什么会产生梯度爆炸和消失的现象呢?由式(7.15)可知,
式中β为矩阵模的上界,如果N-n很大,也就是历史信息较长的话,β大于1或小于1会分别导致梯度爆炸及消失。通常梯度爆炸更容易处理,因为梯度爆炸时会报NaN错误,可设置梯度阈值来限制较大梯度;而梯度消失较难处理一些。通常有三种方法处理梯度消失问题:
1.合理的初始化权重值。初始化权重,使每个神经元尽可能不要取极大或极小值,以躲开梯度消失的区域。
2.使用relu代替sigmoid和tanh作为激活函数。
3.使用其他结构的RNNs,比如长短时记忆网络(LTSM)和Gated Recurrent Unit(GRU)。
7.5 小节
至此,我们讲完了基本的循环神经网络、它的训练算法:BPTT。由于基本的循环神经网络存在梯度爆炸和梯度消失问题,并不能真正的处理好长距离的依赖(虽然有一些技巧可以减轻这些问题),但真正得到广泛的应用的是循环神经网络的一个变体:长短时记忆网络。它内部有一些特殊的结构,可以很好的处理长距离的依赖。下面给出了数字加法的RNN实现(python版本),由于该问题比较简单,只需一个隐藏层就可以得到较好的结果,但为了验证文中公式的准确性,分别考虑了隐藏层为1-4层的情况。运行结果如下:
详细代码如下:
#-*- coding: UTF-8 -*-
from RNN import *
import numpy as np
binary_dim = 8
alpha = 0.1
rnn = RNN(binary_dim, 2, 16, 1, SigmoidActivator)
largest_number = pow(2,binary_dim)
for j in range(20000):
a_int = np.random.randint(largest_number/2)
a_bin = np.unpackbits(np.array([[a_int]],dtype=np.uint8).T,axis=1)
b_int = np.random.randint(largest_number/2)
b_bin = np.unpackbits(np.array([[b_int]],dtype=np.uint8).T,axis=1)
X = np.vstack((a_bin,b_bin)).T
c_int = a_int + b_int
y_bin = np.unpackbits(np.array([[c_int]],dtype=np.uint8).T,axis=1)[0]
pre_bin = rnn.forward(X[range((X.shape[0]-1),-1,-1),:])
err = rnn.backward(y_bin[range((y_bin.shape[0]-1),-1,-1)])
rnn.gradeUpdata(alpha)
if(j%100 == 0):
out = 0
# for index,x in enumerate(reversed(pre_bin)):
for index,x in enumerate(pre_bin):
out += x*pow(2,index)
if(np.abs(out - a_int - b_int) < 1e-5):
print('%6d' %(j), 'step err:%0.3f %5d' %(err[0], a_int), '+%5d' %(b_int), '=%5d' %(int(out)), '_right')
else:
print('%6d' %(j), 'step err:%0.3f %5d' %(err[0], a_int), '+%5d' %(b_int), '=%5d' %(int(out)), '_wrong')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#-*- coding: UTF-8 -*-
from activators import *
import numpy as np
#alpha = 0.1
class NodeNet(object):
def __init__(self, iInputDim, iHiddenDim):
self.m_dOutputArray = np.zeros([1, iHiddenDim])
self.m_dDeltaArray = np.zeros([1, iHiddenDim])
self.m_dInputArray = np.zeros([1, iInputDim])
class OnewayRNNLayer(object):
def __init__(self, iNodeNum, iInputDim, iHiddenDim, activator):
self.m_NodeSet = [NodeNet(iInputDim, iHiddenDim) for _ in range(iNodeNum)]
self.m_iNodeNum = iNodeNum
self.m_iInputDim = iInputDim
self.m_iHiddenDim = iHiddenDim
self.m_WForPreTime = 2*np.random.random([iHiddenDim, iHiddenDim])-1
# self.m_WForPreTime = np.random.normal(0, 1/np.sqrt(iHiddenDim*iHiddenDim), [iHiddenDim, iHiddenDim])
self.m_WForPreLayer = 2*np.random.random([iInputDim, iHiddenDim])-1
# self.m_WForPreLayer = np.random.normal(0, 1/np.sqrt(iInputDim*iHiddenDim), [iInputDim, iHiddenDim])
self.m_WGradForPreTime = np.zeros_like(self.m_WForPreTime)
self.m_WGradForPreLayer = np.zeros_like(self.m_WForPreLayer)
self.m_activator = activator
self.m_inputArray = np.array([iNodeNum, iInputDim])
def forward(self, inputArray):
self.m_inputArray = inputArray
preTimeArray = np.zeros([1, self.m_iHiddenDim])
self.m_outArray = np.zeros([self.m_iNodeNum,self.m_iHiddenDim])
for i in range(self.m_iNodeNum):
self.m_NodeSet.m_dOutputArray = self.m_activator.forward(np.dot(preTimeArray,self.m_WForPreTime) + np.dot(inputArray.reshape(1,inputArray.shape[1]),self.m_WForPreLayer))
self.m_outArray[i,:] = self.m_NodeSet.m_dOutputArray
preTimeArray = self.m_NodeSet.m_dOutputArray
return self.m_outArray
def backward(self, inputDelta, WGradForPreLayer):
nextTimeArray = np.zeros([1, self.m_iHiddenDim])
delta = []
for i in range(self.m_iNodeNum-1,-1,-1):
# delta.append(np.multiply(np.dot(nextTimeArray, self.m_WForPreTime.T) + np.dot(inputDelta, self.m_WGradForPreLayer.T), self.m_activator.backward(self.m_NodeSet.m_dOutputArray)))
delta.append(np.multiply(np.dot(nextTimeArray, self.m_WForPreTime.T) + np.dot(inputDelta, WGradForPreLayer.T), self.m_activator.backward(self.m_NodeSet.m_dOutputArray)))
nextTimeArray = delta[-1]
delta.reverse() #delta是从后向前求的,而其在list中是依次添加的,所以需逆序后才是按原有时间序列排序的delta
self.m_WGradForPreTime = np.zeros_like(self.m_WForPreTime)
self.m_WGradForPreLayer = np.zeros_like(self.m_WForPreLayer)
for i in range(self.m_iNodeNum-1,-1,-1):
if(i != 0):
self.m_WGradForPreTime += np.dot(self.m_NodeSet[i-1].m_dOutputArray.T, delta)
elif(i == 0):
self.m_WGradForPreTime += np.dot(np.zeros_like(self.m_NodeSet[0].m_dOutputArray).T, delta) #由于被加项是0矩阵,因此该项可以去掉
# self.m_WGradForPreTime += np.dot(self.m_NodeSet.m_dOutputArray.T, delta)
self.m_WGradForPreLayer += np.dot(np.array([self.m_inputArray]).T, delta)
return delta
def gradeUpdata(self, alpha):
self.m_WForPreTime -= alpha*self.m_WGradForPreTime
self.m_WForPreLayer -= alpha*self.m_WGradForPreLayer
class RNN(object):
def __init__(self, iNodeNum, iInputDim, iHiddenDim, iOutputDim, activator):
self.m_outputActivator = activator
self.m_layers = [
OnewayRNNLayer(iNodeNum, iInputDim, iHiddenDim, activator),
# OnewayRNNLayer(iNodeNum, iHiddenDim, iHiddenDim*2, activator),
# OnewayRNNLayer(iNodeNum, iHiddenDim*2, iHiddenDim, activator),
# OnewayRNNLayer(iNodeNum, iHiddenDim, iHiddenDim, activator)
];
self.m_dOutputArray = np.zeros([iNodeNum, iOutputDim])
self.m_WOutputLayer = 2*np.random.random([iHiddenDim, iOutputDim])-1
# self.m_WOutputLayer = np.random.normal(0, 1/np.sqrt(iHiddenDim*iOutputDim), [iHiddenDim, iOutputDim])
self.m_iOutputNum = iOutputDim
self.m_iNodeNum = iNodeNum
def forward(self, inputArray):
for i in range(len(self.m_layers)):
inputArray = self.m_layers.forward(inputArray)
for i in range(self.m_layers[-1].m_iNodeNum):
self.m_dOutputArray = self.m_outputActivator.forward(np.dot(self.m_layers[-1].m_NodeSet.m_dOutputArray, self.m_WOutputLayer))
return np.round(self.m_dOutputArray)
def backward(self, labelArray):
err = 0
deltaA = np.zeros([self.m_iNodeNum, self.m_iOutputNum])
self.m_WGradOutputLayer = np.zeros_like(self.m_WOutputLayer)
for i in range(self.m_layers[-1].m_iNodeNum):
deltaOutput = self.m_dOutputArray - labelArray
deltaA = np.dot(deltaOutput, self.m_layers[-1].m_activator.backward(self.m_dOutputArray))
self.m_WGradOutputLayer += self.m_layers[-1].m_NodeSet.m_dOutputArray.T * deltaA #矩阵相城用dot, 矩阵与数相乘直接用*
err += np.abs(deltaOutput)
inputDelta = deltaA
hlay = self.m_layers[0]
nextTimeArray = np.zeros([1, hlay.m_iHiddenDim])
delta = []
for i in range(hlay.m_iNodeNum-1,-1,-1):
delta.append(np.multiply(np.dot(nextTimeArray, hlay.m_WForPreTime.T) + np.dot(inputDelta, self.m_WOutputLayer.T) , hlay.m_activator.backward(hlay.m_NodeSet.m_dOutputArray)))
# delta.append(np.multiply(np.dot(nextTimeArray, hlay.m_WForPreTime.T) + np.dot(inputDelta, self.m_WGradForPreLayer.T), hlay.m_activator.backward(hlay.m_NodeSet.m_dOutputArray)))
#注意,隐藏层的最后一层用到输出层中的权重矩阵
nextTimeArray = delta[-1]
delta.reverse() #delta是从后向前求的,而其在list中是依次添加的,所以需逆序后才是按原有时间序列排序的delta
hlay.m_WGradForPreTime = np.zeros_like(hlay.m_WForPreTime)
hlay.m_WGradForPreLayer = np.zeros_like(hlay.m_WForPreLayer)
for i in range(hlay.m_iNodeNum-1,-1,-1):
if(i != 0):
hlay.m_WGradForPreTime += np.dot(hlay.m_NodeSet[i-1].m_dOutputArray.T, delta)
elif(i == 0):
hlay.m_WGradForPreTime += np.dot(np.zeros_like(hlay.m_NodeSet[0].m_dOutputArray).T, delta) #由于被加项是0矩阵,因此该项可以去掉
hlay.m_WGradForPreTime += np.dot(hlay.m_NodeSet.m_dOutputArray.T, delta)
hlay.m_WGradForPreLayer += np.dot(np.array([hlay.m_inputArray]).T, delta)
if(len(self.m_layers)>=2):
for i in range(len(self.m_layers)-2,-1,-1):
delta = self.m_layers.backward(delta, self.m_layers[i+1].m_WForPreLayer)
return err
def gradeUpdata(self, alpha):
self.m_WOutputLayer -= alpha*self.m_WGradOutputLayer
for i in range(len(self.m_layers)):
self.m_layers.gradeUpdata(alpha)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
由于个人也是刚刚系统的接触深度学习,有不对的地方还希望广大同学告诉我,希望大家多多指导。
python版本:https://pan.baidu.com/s/1qZLJ7Gg
C++版本:待上传
---------------------
【转载】
作者:drilistbox
原文:https://blog.csdn.net/drilistbox/article/details/79720859
|
|