RNN 神经网络及其变体网络
循环神经网络
在网络模型的不同部分进行权值共享使得模型可以扩展到不同样式的样本,正如 CNN 网络中一个确定好的卷积核模板,几乎可以处理任何大小的图片。将图片中分成多个区域,使用同样的卷积核对每一个区域进行处理,最后可以获得非常好的处理结果。
同样的,循环网络使用的类似的模块( 形式上类似,之后会进行讲解 )对整个序列进行处理,可以将很长的序列进行泛化,得到需要的结果。
因为循环神经网络可以将序列进行较好的处理,且时间同样也是有序数列,在实际应用中,RNN 循环神经网络对于处理时序数据具有天然的优势。
网络的结构
类似于 CNN 神经网络的卷积核概念,在 RNN 网络中也类有类似的概念 “ 计算图 ” 。
计算图其实质上是用来描述一个函数方法的模块,也等同于一个数据处理 “ 盒子 ” 。
每一个计算图有输入和输出的数据,同时在某一时刻的计算图的数据对下一时刻计算图造成影响。
计算图可以将按照序列进行展开成为一个具有深度的链结构,在这个深度结构中,也构成了参数的共享。
其中最为普遍的系统形式为:
利用这个公式可以将每一个序列的元素进行连接处理,将公式进行展开可以得到类似于下面的形式:
进行展开后可以得到序列的第 t 个节点(或第 t 时刻)的处理结果。在式子中有不变的参数 θ ,通过保证θ 使得整个网络可以进行权值共享,每一阶段通过参数将前一阶段的状态导入。
RNN 网络
介绍
RNN 网络模型即是循环神经网络的表现。相比于前面描述的循环网络,RNN 中加入了其他参数的输入。即在某一个节点或某一时刻 t(以下统称为某一时刻 t),输入网络的参数还有外界的信号 x^(t) ,这样将上式进行重写,有:
因为网络的参数通过循环不断地传入网络,这一点类似于人思考的过程,RNN 网络可以保持信息的持久性,这一点在其他的神经网络结构中是不具备的。也正是因为这个特征,RNN 网络可以很好的对时序模块进行处理。
在对于网络的理解,也可以理解成为对于同一个网络,每次传入的参数不同。传入参数的一部分是某一时刻 t 的新参数,另一部分是上一时刻 t-1已经获取到的参数。
RNN 拓扑结构
将 RNN 网络函数
进行展开后可以获得下图所示的链结构:
在图中黄色框标注的部分为网络的一个计算图,也可以将其称作为一个细胞体。同时图中分别用蓝色和橙色标注了两种不同的参数传导方式。先解释一下每一个参数的含义, x^(t) 为t时刻的输入参数,h^(t) 为隐藏层的激活函数,o^(t) 为t时刻的输出参数,L^(t)为网络的损失值, y^(t)为目标函数。一般情况下使用图中蓝色线标注的迭代方式,当整个网络位数很高并且信息非常多时,可以利用橙色算法进行传参。
通过将网络展开,能够较好的看到对于一个序列 x^(t),可以使用 RNN 循环神经网络进行处理,网络具有联想能力,可以将之前的信息加以保存,并在之后的循环中进行调用。
RNN 的记忆功能
在上图中我们可以看到 RNN 展开链的形式,这里利用实例解释一下 RNN 的记忆功能。我们将 RNN 网络的拓扑图进行简化。
假如需要使用 RNN 循环网络对一段话进行预测,“ 我是中国人,我爱中国,我的母语是()” 。对于网络来说,现假设 “ 中国人 ” 信息在 x^(2)输入,括号中的内容在 o^(n)
输出。这里就涉及到联想功能,如果说是一个训练好的网络,输出信息时网络会根据前文的信息进行判断该点的输出应该为一种语言的名字,并通过之前的输入 “ 中国人 ” 判断此处应该是 “ 汉语 ” 。
但是对于 RNN 网络仍然是存在一些局限性的,输出的信息还有很多依赖的因素。在理想情况下,RNN 网络的输出精度并不受循环次数的增加而降低。但是在实际应用中,当输入关键信息的位置和输出信息的位置之间的节点变得非常长之后,RNN 神经网络会丧失链接到判断输出的关键信息位置的能力。在实际的应用中,对于循环神经网络,人们经常应用的是 RNN 网络的变体,例如 LSTM 网络。在 LSTM 网络中没有因为位置远近而降低输出精度的问题。
RNN 变体 LSTM 网络
LSTM 网络,即为长短期记忆网络。因为该网络的结构,该网络适合处理序列中间隔和延时较长的事件。在实际生活中,LSTM 网络已经应用在诸多方面, 2015 年谷歌将其应用在安卓系统语音识别功能中,在苹果手机中 Siri 功能也整合了 LSTM 网络,百度、亚马逊等公司的产品中很多也应用到了 LSTM 网络。
LSTM 细胞体结构
在基础 RNN 网络中,之所以不能解决长期依赖问题,是因为 RNN 处理数据的计算图( 细胞体 )结构简单,只有一个非常简单的结构,比如说只进行一个 Sigmoid函数的数据处理,这使得网络很难对长时间的信息进行记忆,同时网络对于哪些信息需要舍弃,哪些信息需要保留也无法做出很好的判断。
在 LSTM 网络中,通过将细胞体结构复杂化,在算法中加入了判断信息是否有用的处理器。在网络中加入了三道 “ 门 ” ,分别叫做:遗忘门、输入门、输出门。信息进入网络中,网络会根据一定的规则来判断信息是否有用,有用的信息将加以保留,无用的信息将进行遗忘。下面我们将 LSTM 的一个细胞体以简图形式呈现。
sigmo层代表一个输出的权重,表示信息通过的量,其取值范围为[0 1] 。当取值为 0时,代表所有信息都不能通过;当取值为1时,代表所有信息都能通过。
网络通过接受前一个细胞体的两个参数 h^ (t−1)、C^(t−1),以及外界给予的参数 x^(t)
,经过三个门的过滤整合将信息传递给下一个细胞体。上图中黑色和蓝色的线是信息在整个网络中信息的传送带,将信息在网络中不断的传输。
在上图两条线合并的地方表示将信息进行合并(蓝线和黑线不相交),在实际处理中,表示将两个向量进行合并。一条线分开成两条线的地方表示将信息进行复制(不需要考虑颜色),黑色方块代表 LSTM 神经网络中的一层,而圆圈代表将信息进行相应的处理。
胞体中颜色的区分是为了让读者更好理解三个门的概念。第一个门用橙色线进行标注为忘记门;第二个门用绿色线和黄色线标注为输入门;第三个门是用灰色线进行标注为输出门。
遗忘门
遗忘门为网络中的第一个门,网络会对信息进行判断,决定信息的弃留。为什么遗忘门作为网络的第一个门,是因为需要将网络信息进行过滤,放在之后容易对新加入的信息进行错误处理。
在遗忘门中信息经过一个 Sigmoid函数处理得到新的信息 f1^(t),这个网络层决定了信息的保存还是丢弃。
应用到实际应用中,细胞体中可能含有的是 “ 小明 ” 这个人名,我们很希望使用它来预测之后的词语,网络中可能已经获取的信息是 “ 忧伤的 ” ,但经过网络处理中后该词需要进行更新,遗忘门就会将该词进行过滤。
输入门
输入门分为两步进行,首先绿色线的部分需要选择什么值需要进行更新,之后黄色线的部分是确定将什么值进行传输。
绿色线部分:
黄色线部分:
现在我们已经将网络信息中需要更新的旧信息予以遗忘,新信息定位好并获取其内容,之后就是将信息进行整合,更新细胞体状态,并且这里得到细胞体的一部分输出:
输出门
最后是将信息进行输出,输出门所采用的信息是将细胞状态进行处理并和初始信息整合后进行输出。
通过最后输出门的处理,将细胞信息进行了选择,输出了我们需要的那部分信息,比如按照之前的语料信息,“ 小明 ” 现在的状态并不是 “ 忧伤的 ” 而是 “ 快乐的 ” 。这时网络的输出会将 “ 快乐的 ” 这个信息进行输出。
RNN 其他形式变体介绍
Peephole 网络
该网络将细胞体的状态作为网络神经层的一部分输入。整合了细胞状态的输入,使得网络判断可依据的信息变多,这种优化的方法也称作是窥孔优化。
Coupled 网络
在这个网络中,细胞状态的遗忘是相对的,遗忘门遗忘了一些信息之后,相应的在输入门就会生成一些信息。未被遗忘的信息将传输到输入门中,有:
GRU 网络
该网络是简化版的 LSTM 网络,在这个网络中将细胞状态的概念去掉,通过一条输出链在每时刻和输入信息进行整合作为输出并传输到下一时刻。正因为这样的结构,使得 GRU 网络收敛的时间和需要的信息量的要求上都优于 LSTM 网络。
利用 RNN 进行简单的加法运算
实验原理
本实验利用 RNN 变体神经网络层于层之间的网络权值能够进行传递的特点,经过不断的训练将网络权值进行改变,以达到该网络可以准确进行计算的目的。
import numpy as np
import copy
%matplotlib inline
神经网络层的函数实现
在该实验中将会用到两个用于神经网络层的处理函数,sigmoid 函数及其导数。这两个函数用于神经网络层的数据处理,sigmoid 函数即是网络中的激活函数。这个函数中具有一些非常优良的以便神经网络使用的特性,这个函数可以将任意值映射到区间[0 1] 中,方便将数值和概率相挂钩。
通过这个函数将数据进行非线性化处理。以便于网络的非线性记忆功能。同时该网络的导数也很容易计算,通过一点的输出值即可求得其导数: sigmoid*(1-sigmoid)。下面将这两个函数进行定义。
def sigmoid(x):
y = 1/(1+np.exp(-x))
return y
def dersigmoid(x):
y = x*(1-x)
return y
神经网络初始化
网络初始化
将网络的输入层、隐藏层、输出层进行初始化设定,这里我们要预先将神经网络这三层的神经元个数进行设定,以及将网络层与层之间信息交换的权值网络进行初始化设定,同时定义一个和权值网络大小相等的全零网络,用来储存神经网络更新权值的暂存值。
step = 0.1
ipnumber = 2
hdnumber = 32
opnumber = 2
neu_i2h = 2*np.random.random((ipnumber,hdnumber)) - 1
neu_h2o = 2*np.random.random((hdnumber,opnumber)) - 1
neu_h2h = 2*np.random.random((hdnumber,hdnumber)) - 1
neu_i2hN = np.zeros_like(neu_i2h)
neu_h2oN = np.zeros_like(neu_h2o)
neu_h2hN = np.zeros_like(neu_h2h)
这里首先初始化三个随机数值的矩阵,分别代表输入层和隐藏层、隐藏层和输出层、隐藏层和隐藏层之间的连接权值,在之后的操作中,这三个矩阵中的数值不断随着网络的学习进行更改,最终使得网络中的权值能正确的对给定的输入值进行处理。
之后网络设定了三个值为零的大小和权值网络相同的矩阵,为的是进行每一次学习的暂时结果的记录。这些矩阵在每一次循环完之后都会将其重置为零矩阵。
训练数据集初始化
这里便于神经网络的更精确的计算,将输入神经网络的数值转换成二进制数字。
i2b = {}
bdigit = 8
MAXnumber = pow(2,bdigit)
bin = np.unpackbits(np.array([range(MAXnumber)],dtype=np.uint8).T,axis=1)
for i in range(MAXnumber):
i2b[i] = bin[i]
在这里的 bdigit 设定的是二进制数字的最大位数,即设定一个神经网络输入值的取值范围。并设定初始的字典 i2b 。
训练神经网络
训练的次数越多网络拟合效果越好。在这个实验中我们选定了 2000020000 次的训练,本小节下面的所有代码,都是在这个 forfor 循环中进行的。
for i in range(20000):
....
....
设定加法问题
给神经网络出了一个简单的加法问题,目的就是让神经网络通过 2000020000 次的训练能够学会简单的加法运算,当训练好网络之后,网络进行加减运算时并不是通常意义上的加减,而是通过一步步权值进行处理最终得到正确答案的。
a_decimal = np.random.randint(MAXnumber / 2)
b_decimal = np.random.randint(MAXnumber / 2)
c_decimal = a_decimal + b_decimal
a = i2b[a_decimal]
b = i2b[b_decimal]
c = i2b[c_decimal]
首先代码生成了两个随机的数字,并且保证随机数字小于最大数字的一半( 需要保证两者相加仍然在神经网络的处理范围之内 )。之后将这两个数字及其加和转化为二进制数字进行储存。
预测值初始化
将进行预测值的初始化,设定几个数组,将得到的预测值、误差值、输出层导数进行储存。为了之后训练和反向传播进行准备。
# 网络预测值的二进制数组
binary = np.zeros_like(c)
# 误差值
aError = 0
#导数值
oplayer_der = list()
hdlayer_val = list()
hdlayer_val.append(np.zeros(hdnumber))
训练神经网络
在训练神经网络的过程中首先将数据进行转换,之后数据通过层作用计算权值,并最终将误差进行计算并将权值进行储存以便之后的反向传播运算。在这步中只是做一下网络的训练,层于层之间的网络权值并没有改变。
for locate in range(bdigit):
X = np.array([[a[bdigit - locate - 1],b[bdigit - locate - 1]]])
Y = np.array([[c[bdigit - locate - 1]]]).T
hdlayer = sigmoid(np.dot(X,neu_i2h) + np.dot(hdlayer_val[-1],neu_h2h))
oplayer = sigmoid(np.dot(hdlayer,neu_h2o))
oplayer_error = Y - oplayer
oplayer_der.append((oplayer_error)*dersigmoid(oplayer))
aError += np.abs(oplayer_error[0])
binary[bdigit - locate - 1] = np.round(oplayer[0][0])
hdlayer_val.append(copy.deepcopy(hdlayer))
数据读取
X = np.array([[a[bdigit - locate - 1],b[bdigit - locate - 1]]]) #从右向左检索a、b二进制数字
Y = np.array([[c[bdigit - locate - 1]]]).T #检索正确数值
这里将二进制数据中的数字从右往左进行读取,并存入到两个数组之中,便于之后的训练。
网络训练
# 隐藏层
hdlayer = sigmoid(np.dot(X,neu_i2h) + np.dot(hdlayer_val[-1],neu_h2h))
# 输出层
oplayer = sigmoid(np.dot(hdlayer,neu_h2o))
处理隐藏层的函数是将现在隐藏层的传入信号 XX 和输入层与隐藏层的网络权值 neu_i2h 进行矩阵相乘,得到现在时刻网络的传入 np.dot(X,neu_i2h) 。
并和上一时刻网络隐藏层的状态 np.dot(hdlayer_val[-1], neu_h2h) ( 上一时刻网络状态的导数 hdlayer_val[-1] 和隐藏层与隐藏层的网络权值neu_h2h 进行矩阵相乘)进行相加。将加和进行函数处理,得到现在隐藏层的值。
输出层将网络隐藏层得到的值 hdlayer 和隐藏层与输出层的网络权值 neu_h2o 进行矩阵相乘,得到网络的输出值。
误差分析
# 计算误差
oplayer_error = Y - oplayer # 真实误差
oplayer_der.append((oplayer_error)*dersigmoid(oplayer)) # 每时刻导数值
aError += np.abs(oplayer_error[0]) # 累加误差的绝对值
在这个步骤中,首先计算出输出层和真是值的误差 oplayer_error,并将每一次训练输出值的导数进行储存,最后将句对误差进行相加。
进行反向传播
到这一步我们得到了神经网络的输出值,下面需要将神经网络进行反向传播来将网络进行进一步的构建。
Fhdlayer_dels = np.zeros(hdnumber)
for locate in range(bdigit):
X = np.array([[a[locate],b[locate]]])
hdlayer = hdlayer_val[-locate-1]
hdlayer_pre = hdlayer_val[-locate-2]
oplayer_dels = oplayer_der[-locate-1]
hdlayer_dels = (Fhdlayer_dels.dot(neu_h2h.T) + oplayer_dels.dot(neu_h2o.T)) * dersigmoid(hdlayer)
neu_h2oN += np.atleast_2d(hdlayer).T.dot(oplayer_dels)
neu_h2hN += np.atleast_2d(hdlayer_pre).T.dot(hdlayer_dels)
neu_i2hN += X.T.dot(hdlayer_dels)
Fhdlayer_dels = hdlayer_dels
数据检索
X = np.array([[a[locate],b[locate]]]) # 检索数据
hdlayer = hdlayer_val[-locate-1] # 从数据中取出当前隐藏层
hdlayer_pre = hdlayer_val[-locate-2] # 从数据中取出前一个隐藏层
这里反向将数据进行检索,以便于之后的误差的计算。通过前一个循环得到的隐藏层的数组,取出相应的隐藏层的值准备误差运算。
误差计算
# 输出层误差
oplayer_dels = oplayer_der[-locate-1]
# 隐藏层误差
hdlayer_dels = (Fhdlayer_dels.dot(neu_h2h.T) + oplayer_dels.dot(neu_h2o.T)) * dersigmoid(hdlayer)
这里将误差进行统计得到输出层和隐藏层的误差,以便权值进行更新。这里得到了两个网络层的误差,通过这两组误差将权值进行更新。
进行权值更新
neu_i2h += neu_i2hN * step
neu_h2o += neu_h2oN * step
neu_h2h += neu_h2hN * step
neu_i2hN *= 0
neu_h2oN *= 0
neu_h2hN *= 0
得到了所有的值之后,将网络的权值进行更新,即是这一次训练对网络权值造成的影响会在这一步中进行更新。
组合之后的示例代码如下:
for j in range(20000):
a_decimal = np.random.randint(MAXnumber / 2)
b_decimal = np.random.randint(MAXnumber / 2)
c_decimal = a_decimal + b_decimal
a = i2b[a_decimal]
b = i2b[b_decimal]
c = i2b[c_decimal]
binary = np.zeros_like(c)
aError = 0
oplayer_der = list()
hdlayer_val = list()
hdlayer_val.append(np.zeros(hdnumber))
for locate in range(bdigit):
X = np.array([[a[bdigit - locate - 1], b[bdigit - locate - 1]]])
Y = np.array([[c[bdigit - locate - 1]]]).T
hdlayer = sigmoid(np.dot(X, neu_i2h) +
np.dot(hdlayer_val[-1], neu_h2h))
oplayer = sigmoid(np.dot(hdlayer, neu_h2o))
oplayer_error = Y - oplayer
oplayer_der.append((oplayer_error)*dersigmoid(oplayer))
aError += np.abs(oplayer_error[0])
binary[bdigit - locate - 1] = np.round(oplayer[0][0])
hdlayer_val.append(copy.deepcopy(hdlayer))
Fhdlayer_dels = np.zeros(hdnumber)
for locate in range(bdigit):
X = np.array([[a[locate], b[locate]]])
hdlayer = hdlayer_val[-locate-1]
hdlayer_pre = hdlayer_val[-locate-2]
oplayer_dels = oplayer_der[-locate-1]
hdlayer_dels = (Fhdlayer_dels.dot(neu_h2h.T) +
oplayer_dels.dot(neu_h2o.T)) * dersigmoid(hdlayer)
neu_h2oN += np.atleast_2d(hdlayer).T.dot(oplayer_dels)
neu_h2hN += np.atleast_2d(hdlayer_pre).T.dot(hdlayer_dels)
neu_i2hN += X.T.dot(hdlayer_dels)
Fhdlayer_dels = hdlayer_dels
neu_i2h += neu_i2hN * step
neu_h2o += neu_h2oN * step
neu_h2h += neu_h2hN * step
neu_i2hN *= 0
neu_h2oN *= 0
neu_h2hN *= 0
结果输出
在进行 20000次训练之后,我们将最后一次网络的预测值进行输出。同时比较一下输出结果和正确结果的误差大小。
print("Error:" + str(aError))
print("Predicted:" + str(binary))
print("True:" + str(c))
value = 0
for index, x in enumerate(reversed(binary)):
value += x*pow(2, index)
print(str(a_decimal) + " + " + str(b_decimal) + " = " + str(value))
其中 Error 代表我们预测值和真实值在网络运行中产生的总误差, Predicted 是网络的预测值, True 是这个加法问题的真实值,最终通过算式将加法予以呈现。