当前位置: 首页 > 资讯

【原创】循环神经网络入门

论智       2017-11-08

上次我们介绍了神经网络的基本原理,其中举了一个非常简单的用神经网络进行异或运算的例子。这个网络接受两个输入(1或0),最终输出1或0。我们的异或网络有一个特性,当前的输入和以往的输入无关。也就是说,给定输入(1, 0),输出永远是1,至于上一个输入是不是也是(1, 0),还是(0, 1),还是别的什么,完全不影响当前输入的运算结果。从函数式编程的角度来说,这个异或网络是一个纯函数。


实际工程中的神经网络当然不会这么简单,但是基本结构是类似的。接受一个或多个输入,通过多层神经网络的运算,最终输出一个结果。


那么,这样的结构会不会有什么不足呢?


就编程而言,我们知道,纯函数是有很多不足的,对于很多实际问题,副作用是很重要的。一些纯函数编程语言,通过各种绕弯的方式把副作用“藏”起来,但是,相应地,这些语言在建模一些实际问题时就不那么直接了。


同理,某些问题也需要神经网络具备一些副作用。例如,字符识别问题。显然,单个字符的识别,和前后的字符都是相关的。再比如,处理视频,视频的每一帧,和前后帧也是相关的。上面两个例子的共性,是处理输入序列。


那么,我们怎样才能让神经网络记住历史信息呢?


如果是一般的程序语言,那有很多方法。最粗暴的做法就是声明几个全局变量,让这些全局变量记住某些状态。但是,神经网络里并没有全局变量这种东西。那该怎么办呢?


如果我们意识到,全局变量其实是函数的隐含输入, 那么答案其实很简单。我们将上一个状态作为函数的输入,然后除了返回输出值外,额外返回一个新的状态,并将这个状态传递给下一个函数。通过一连串函数的接力,不依赖全局变量,实现了状态的“保存”。


例如,在一般的程序语言中,取随机数的函数大概是这个样子的:


# 用全局变量存储随机数种子

seed = 0


def random(section):

   global seed

   seed = next_random(seed)

   output = process(seed, section)

   return output

在纯函数语言里会变成这样:


# 将状态作为输入传递

def random(section, old_seed):

   seed = next_random(old_seed)

   output = process(seed, section)

   # 额外返回状态

   return (output, seed)

我们看到,在纯函数语言中,状态被转化为一系列函数的输出。类似地,神经网络可以将输入序列转化为状态序列输出。这样的神经网络,称为循环神经网络(Recurrent Neural Network),简称RNN。


rnn = RNN(D, H)

这里的D代表输入序列,H代表状态序列。输入序列D被转化为状态序列H。


h = rnn.next(h0, x)

这里h0是初始状态序列,x是输入序列。返回的输出h为包含时序信息的状态序列。


下一步运算之间使用上一步产生的状态序列:


h = rnn.next(x)

h的具体计算方法为:


h[t] = tanh(Wh(h[t- 1]) + Wx(x[t]) + b)

其中,Wh是记录状态之间连接的矩阵,Wx是记录输入与状态之间连接的矩阵,b是偏置。tanh函数的选取是经验的结果,在实践中发现,tanh的效果比较好。


如果我们把循环神经网络展开的话,我们会得到一串神经网络,每一个网络会把它的输出传递到下一个网络中:


unroll-rnn



展开图清楚地表明了RNN的结构和序列数据的密切关系。另外,我们可以看到,这一串的神经网络,神似前述纯函数语言中通过一串函数传递状态的做法。


以上就是循环神经网络的基本原理。实际工程中,基本不用上面的原生循环神经网络,而是使用经过改进的LSTM(Long Short-Term Memory,长短期记忆网络)。


我们可以看到,原生RNN中状态的计算方法是比较简单的。这就带来一个问题,如果相关的两个输入,在输入序列中的距离较远,那最终的效果就比较差了。当然,有时候,调整一些参数,可以将距离较远的相关输入(长时依赖关系)联系起来,但大多数时候,这很困难。


因此,我们考虑给我们的循环网络加入长期记忆功能。在状态之外,增加一个结构(不妨把这个结构叫做细胞)。然后,通过某些阀门,控制何时遗忘信息,何时将信息保存到细胞内。


LSTM

# LSTM和原生循环网络的总体结构是一致的。

lstm = nn.LSTM(D, H)


# 输入门,决定是否将信息传入细胞

i[t] = sigmoid(ai[t])

# 遗忘门,决定是否遗忘信息

f[t] = sigmoid(af[t])

# 输出门,决定细胞状态中哪些信息将被输出

o[t] = sigmoid(ao[t])

# 使用`tanh`调整输出结果(确保输出值在-1和1之间)

g[t] = tanh(ag[t])


# 细胞状态既取决于上一个时刻的细胞状态,

# 也取决于输入门和遗忘门的作用

c[t] = f[t] * c[t - 1] + i[t] * g[t]

# 状态取决于输出门和细胞状态

h[t] = o[t] * tanh(c[t])

从上面我们可以看到,LSTM和原生RNN的基本原理是一样的,LSTM在RNN的基础上增加了一个细胞状态而已。


机器人网原创文章,未经授权禁止转载。详情见转载须知

本文来自机器人网,如若转载,请注明出处:https://www.jqr.com/news/008527