1.什么是RNN

RNN(Recurrent Neural Network)循环神经网络,是用来专门处理序列数据的神经网络。百度百科关于时间序列数据的定义是这样的:时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化状态或程度。这是时间序列数据的定义,当然这里也可以不是时间,比如文字序列,但总归序列数据有一个特点——后面的数据跟前面的数据有关系。

RNN的出现是为了解决全连接神经网络不能联系上下文去训练模型的缺点。有一个NLP很常见的问题命名实体识别,举个例子,有下面两句话:

第一句:I like eating apple!

第二句:The Apple is a great company!

现在我们要给apple打标签,假设现在有很多已经标记好的数据供我们训练模型。假如我们使用全连接神经网络,把apple对应的特征向量输入到网络中,那么输出结果中我们想让正确的标签概率值最大,来训练模型,我们的训练集中有的apple的标签是苹果有的是公司,这将导致训练的准确度取决于训练集中哪个标签的个数比较多,这显然是我们不愿看到的。

但如果让我们去判断,那么一定是不能只看apple这个词的,是要联系上下文的。但全连接神经网络是不能做到这一点的,于是就有了循环神经网络。

2.RNN为什么能处理序列数据

这是RNN的结构决定的。

rnn时间序列预测MATLAB代码 rnn处理时间序列_rnn时间序列预测MATLAB代码

 这是网上很经典的一张RNN的结构图,他每层不只有一个神经元,不看矩阵w,把他展开可以是这样的:

rnn时间序列预测MATLAB代码 rnn处理时间序列_二进制数_02

这就是一个全连接神经网络。u就是一个3×4的矩阵,s的维度是4,v是4×3的矩阵。

如果按照第一张图的w展开他也可以变成这样:

rnn时间序列预测MATLAB代码 rnn处理时间序列_rnn时间序列预测MATLAB代码_03

 这是不同时刻输入x对应输出的图,这体现了RNN的很重要的一个思想:权重共享。即不同时刻输入的x每次使用的权重矩阵是一样的,每一部分都是如此。

有了这个图我们就可以解释RNN是如何处理序列数据的了,其关键之处在于循环核,也就是图中的Ws矩阵,他能记住上次隐藏层输出的特征,并将他传递给下一次输入。

我们来看RNN的前向传播的公式:

rnn时间序列预测MATLAB代码 rnn处理时间序列_数据_04

rnn时间序列预测MATLAB代码 rnn处理时间序列_数据_05

 隐藏层的输出不仅与本次输入有关还和上一时刻隐藏层的输出有关,这样网络便能记住不同时刻的特征,于是他便能处理序列数据。

3.RNN的反向传播

RNN对于每个时刻输入的x都会产生一个损失函数值,总的损失函数是每个时刻的叠加,我们只以某个时刻为例来推导他的反向传播公式,实际是用各个时刻的损失函数的和来求梯度的。

以第三幅图为例,在t时刻(使用平方损失函数),假设t=3。

损失函数为:

rnn时间序列预测MATLAB代码 rnn处理时间序列_数据_06

 y为真实的输出,各个参数的梯度为:

rnn时间序列预测MATLAB代码 rnn处理时间序列_数据_07

rnn时间序列预测MATLAB代码 rnn处理时间序列_数据_08

rnn时间序列预测MATLAB代码 rnn处理时间序列_损失函数_09

 以Wx的梯度为例,因为S3不仅和本层输入有关,还和S2有关而S2又和S1有关,所以Wx的梯度会有三部分,Ws也是一样。

于是我们便可以推出任意时刻Wx的梯度为:

rnn时间序列预测MATLAB代码 rnn处理时间序列_损失函数_10

 Ws与之类似,只不过把Ws替换一下,这样便可以用梯度下降法进行更新参数了。

4.RNN存在的问题

梯度消失和梯度爆炸是RNN存在的两个问题,他们是如何产生的哪?

我们看上面推出的任意时刻Wx的梯度就会发现中间的连乘的部分每次Sj对Sj-1求导都会产生一个Ws和一项损失函数的导数(因为先要对损失函数整体求导),如果连乘的项过多那么就会有Ws的n次方的项产生如果Ws大于或小于1就会让这个梯度本身很大或很小,也就是产生梯度爆炸或梯度消失。

当然这里说的消失并不是整个梯度都没有了,而是当t较大时(就是后面输入的x)的梯度没有了,这也限制了RNN的学习能力,就是他不能学习远距离的依赖关系。

我们也不能忽略了求导过程中损失函数的导数这一项,他的大小同样会加重梯度消失或爆炸。

如何解决梯度消失或梯度爆炸?

梯度消失:可以使用不同的损失函数比如ReLU,他求导后的值总为1(对于大于0的值),还有leakrelu、elu函数等,或者改变RNN本身的结构,这就是接下来我们要学习的LSTM(Long Short Term Memory, 长短期记忆网络 )和GRU(Gated Recurrent Unit networks,门控循环单元网络)

梯度爆炸:梯度裁剪,权值正则化。

参考文章:

史上最详细循环神经网络讲解(RNN/LSTM/GRU) - 知乎


5.代码实例

只是自己对代码进行了重新理解,并整理。

本代码的任务是用RNN来实现一个八位二进制数的加法运算。

import copy, numpy as np
np.random.seed(0)

# compute sigmoid nonlinearity
def sigmoid(x):
    output = 1/(1+np.exp(-x))
    return output

# convert output of sigmoid function to its derivative
def sigmoid_output_to_derivative(output):
    return output*(1-output)

首先是导入需要的库,并定义sigmoid函数及其导数,这在梯度下降时会用到。

# training dataset generation
int2binary = {}
binary_dim = 8

largest_number = pow(2,binary_dim)
binary = np.unpackbits(
    np.array([range(largest_number)],dtype=np.uint8).T,axis=1)          #将uint8数组的元素解压缩为二进制值输出数组。
for i in range(largest_number):
    int2binary[i] = binary[i]


# input variables
alpha = 0.1
input_dim = 2
hidden_dim = 16
output_dim = 1

然后是定义本次任务的参数,binary_dim二进制的位数为八位,最大的十进制数为2的8次方,并存储所有的八位二进制数。

# initialize neural network weights
synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1  #生成input_dim,hidden_dim大小的矩阵,值为0-1
synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1
synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1
 
synapse_0_update = np.zeros_like(synapse_0)                  #构造全零的矩阵,大小和参数的一样
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)

初始化权重矩阵,和梯度矩阵。

# generate a simple addition problem (a + b = c)
    a_int = np.random.randint(largest_number/2) # int version
    a = int2binary[a_int] # binary encoding

    b_int = np.random.randint(largest_number/2) # int version
    b = int2binary[b_int] # binary encoding

    # true answer
    c_int = a_int + b_int
    c = int2binary[c_int]
    
    # where we'll store our best guess (binary encoded)
    d = np.zeros_like(c)

    overallError = 0
    
    layer_2_deltas = list()
    layer_1_values = list()
    layer_1_values.append(np.zeros(hidden_dim))

随机初始化要运算的数,和他的真实输出。d是一个存储预测输出的数组。layer_1_values存储隐藏层的输出,layer_2_deltas表示输出层对于参数求导时的整体梯度,也即上面式子中L对O的梯度。

# moving along the positions in the binary encoding
    for position in range(binary_dim):
        #每次取八位二进制数的一位进行运算
        # generate input and output
        X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]])
        y = np.array([[c[binary_dim - position - 1]]]).T
        '''print("!!!!!!")
        print(X)
        print(y)'''
        # hidden layer (input ~+ prev_hidden)
        layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h))

        # output layer (new binary representation)
        layer_2 = sigmoid(np.dot(layer_1,synapse_1))

        # did we miss?... if so, by how much?
        layer_2_error = y - layer_2
        layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2))
        overallError += np.abs(layer_2_error[0])
    
        # decode estimate so we can print it out
        d[binary_dim - position - 1] = np.round(layer_2[0][0])
        
        # store hidden layer so we can use it in the next timestep
        layer_1_values.append(copy.deepcopy(layer_1))               #保存隐藏层的输出
        '''print("?????")
        print(layer_1_values)'''
    future_layer_1_delta = np.zeros(hidden_dim)

每次循环只取二进制数的一位进行运算,X表示二进制加数和被加数的两位,y表示真实和的一位。layer_1和layer_2表示隐藏层和输出层的输出值,layer_2_error表示误差。然后将误差的整体导数添加到layer_2_deltas中,统计一下整体误差,循环结束后初始化隐藏层的梯度。

for position in range(binary_dim):
        
        X = np.array([[a[position],b[position]]])
        layer_1 = layer_1_values[-position-1]
        prev_layer_1 = layer_1_values[-position-2]        #取一项的原因是因为这一项的值已经包含之前的了
        '''print("!!!!!!")
        print(prev_layer_1)'''
        # error at output layer
        layer_2_delta = layer_2_deltas[-position-1]
        # error at hidden layer
        layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)

        # let's update all our weights so we can try again
        synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
        synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
        synapse_0_update += X.T.dot(layer_1_delta)
        
        future_layer_1_delta = layer_1_delta
    

    synapse_0 += synapse_0_update * alpha
    synapse_1 += synapse_1_update * alpha
    synapse_h += synapse_h_update * alpha    

    synapse_0_update *= 0
    synapse_1_update *= 0
    synapse_h_update *= 0

开始梯度下降更新参数,需要更新三个矩阵synapse_0,synapes_1,synapes_h。在计算梯度时较为复杂的就是synapes_h的梯度,他需要在计算时向前传播梯度,也即上边总结的最终梯度的连乘的那一部分。其实代码中就变得简单了,他只是一个数,我们用prev_layer_1来表示,但你可能会说,为什么他只有隐藏层的一项如果是最后一个输入的不应该有前面n个隐藏层的输出吗?其实每一个prev_layer_1的数值就已经包含前面的隐藏层的输出了,因为在计算layer_1时就已经包含了。

其余梯度的计算和上边推导的公式是一致的,只是他计算的是这一层的整体的梯度,我们公式里只是一个参数的梯度。其实每一个参数的求导在代码里都不会向前去寻找n个梯度,因为当前的值已经包括之前的梯度了。

# print out progress
    if(j % 1000 == 0):
        print ("Error:" + str(overallError))
        print ("Pred:" + str(d))
        print ("True:" + str(c))
        out = 0
        for index,x in enumerate(reversed(d)):
            out += x*pow(2,index)
        print (str(a_int) + " + " + str(b_int) + " = " + str(out))
        print ("------------")

rnn时间序列预测MATLAB代码 rnn处理时间序列_机器学习_11

 

最后就是输出:误差,预测的二进制的结果,真实的二进制的结果,十进制的初始化的两个数和他们相加的预测结果的表达式。

最后再给出总的代码:

#为啥会有一个损失值骤降的过程?
import copy, numpy as np
np.random.seed(0)

# compute sigmoid nonlinearity
def sigmoid(x):
    output = 1/(1+np.exp(-x))
    return output

# convert output of sigmoid function to its derivative
def sigmoid_output_to_derivative(output):
    return output*(1-output)


# training dataset generation
int2binary = {}
binary_dim = 8

largest_number = pow(2,binary_dim)
binary = np.unpackbits(
    np.array([range(largest_number)],dtype=np.uint8).T,axis=1)          #将uint8数组的元素解压缩为二进制值输出数组。
for i in range(largest_number):
    int2binary[i] = binary[i]
    #print(binary[i])

# input variables
alpha = 0.1
input_dim = 2
hidden_dim = 16
output_dim = 1


# initialize neural network weights
synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1  #生成input_dim,hidden_dim大小的矩阵,值为0-1
synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1
synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1
 
synapse_0_update = np.zeros_like(synapse_0)                  #构造全零的矩阵,大小和参数的一样
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)

# training logic
for j in range(10000):
    
    # generate a simple addition problem (a + b = c)
    a_int = np.random.randint(largest_number/2) # int version
    a = int2binary[a_int] # binary encoding

    b_int = np.random.randint(largest_number/2) # int version
    b = int2binary[b_int] # binary encoding

    # true answer
    c_int = a_int + b_int
    c = int2binary[c_int]
    
    # where we'll store our best guess (binary encoded)
    d = np.zeros_like(c)
    #print(type(d))
    overallError = 0
    
    layer_2_deltas = list()
    layer_1_values = list()
    layer_1_values.append(np.zeros(hidden_dim))
    
    # moving along the positions in the binary encoding
    for position in range(binary_dim):
        #每次取八位二进制数的一位进行运算
        # generate input and output
        X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]])
        y = np.array([[c[binary_dim - position - 1]]]).T
        '''print("!!!!!!")
        print(X)
        print(y)'''
        # hidden layer (input ~+ prev_hidden)
        layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h))

        # output layer (new binary representation)
        layer_2 = sigmoid(np.dot(layer_1,synapse_1))

        # did we miss?... if so, by how much?
        layer_2_error = y - layer_2
        layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2))
        overallError += np.abs(layer_2_error[0])
    
        # decode estimate so we can print it out
        d[binary_dim - position - 1] = np.round(layer_2[0][0])
        
        # store hidden layer so we can use it in the next timestep
        layer_1_values.append(copy.deepcopy(layer_1))               #保存隐藏层的输出
        '''print("?????")
        print(layer_1_values)'''
    future_layer_1_delta = np.zeros(hidden_dim)
    
    for position in range(binary_dim):
        
        X = np.array([[a[position],b[position]]])
        layer_1 = layer_1_values[-position-1]
        prev_layer_1 = layer_1_values[-position-2]        #取一项的原因是因为这一项的值已经包含之前的了
        '''print("!!!!!!")
        print(prev_layer_1)'''
        # error at output layer
        layer_2_delta = layer_2_deltas[-position-1]
        # error at hidden layer
        layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)

        # let's update all our weights so we can try again
        synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
        synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
        synapse_0_update += X.T.dot(layer_1_delta)
        
        future_layer_1_delta = layer_1_delta
    

    synapse_0 += synapse_0_update * alpha
    synapse_1 += synapse_1_update * alpha
    synapse_h += synapse_h_update * alpha    

    synapse_0_update *= 0
    synapse_1_update *= 0
    synapse_h_update *= 0
    
    # print out progress
    if(j % 1000 == 0):
        print ("Error:" + str(overallError))
        print ("Pred:" + str(d))
        print ("True:" + str(c))
        out = 0
        for index,x in enumerate(reversed(d)):
            out += x*pow(2,index)
        print (str(a_int) + " + " + str(b_int) + " = " + str(out))
        print ("------------")