RNN原理解析与实现(Keras)

4,460 阅读10分钟

为什么要使用RNN

前馈神经网络

  • 前馈神经网络是一种最简单的神经网络,是目前应用最广泛、发展最迅速的人工神经网络之一。
  • 各神经元分层排列,第一层是输入层;然后是隐藏层,其中隐藏层可能会有多层;最后一层为输出层。
    • 层与层之间是全连接的,每层的节点之间是无链接的。
    • 每层的神经元只与前一层的神经元相连,只接受前一层的输出,并传给下一层,各层间没有反馈。
    • 每一层的神经元之间都是相互独立的,如输入层的神经元是彼此独立的。
  • 常见的前馈神经网络有单层前馈神经网络、多层前馈神经网络(DNN、多层感知器)、CNN等。

前馈神经网络的缺陷

前馈神经网络只能单独处理一个个的输入,前一个输入与后一个输入之间没有任何关系,如果碰到需要处理输入之间关系的数据,它们将无法正确的预测输入值,比如下列样例:

  • 球的轨迹(移动过程点的集合)

    • 现有一个球在平面上移动,我们想要预测球的移动方向。
    • 如果使用前馈网络,那么我们把点集输入模型,但是由于它只能处理单独一个个的输入,那么我们就无法根据上一个的信息来预测下一个点的方向(即球的移动方向)。
    • 只能根据被动的根据所有的输入(点)的来预测方向,不清楚点位于第几步,就无法正确的预测方向。如下图:我们知道球的轨迹,却不清楚此时球位于哪一点的位置。
  • 自然语言处理

    • 如果我们输入一句话我吃苹果,想要预测它的语义,那么它会被拆成一个个词输入模型
    • 如果我们不知道它们的顺序,随机处理它们,那么我们就无法正确的预测出这句话的语义
    • 因为:苹果吃我苹我吃果我吃苹果是完全不同的语义

RNN的定义

针对上述需要需要按照顺序处理的数据,在原有全连接神经网络的基础上添加了一个时间轴的概念,即诞生了循环神经网络(RNN)

循环神经网络(RNN)是一类用于处理序列数据的神经网络。RNN拥有记忆模块,可以获取以及计算过的信息,记住执行的顺序。即使是同样的输入,如果输入的顺序不同也会产生不同的输出。

RNN的基本结构

神经网络的结构分为输入层、隐藏层、输出层。输入与输出层是处理数据的输入输出的;而隐藏层则对数据进行计算、预测处理。前馈网络与循环网络(RNN)的主要区别就是在隐藏层。

最简前馈神经网络

前馈网络的隐藏层,如果只有一个神经元,则直接处理;如果有多个神经元,每个神经元都单独运行,互不相关,没有任何顺序、引用、传值的关系。

RNN的结构

而RNN的结构则是在前馈网络隐藏层上添加了时间轴的概念,让每一个单元都按照输入的顺序进行处理,上一个处理的输出作为下一个处理的输入与当前的输入一起计算,综合前几个输入的计算结果与当前输入一起进行预测。

RNN的种类(结构的四种形式)

1 to N

这种形式一是只在序列开始把输入信息输入模型计算(左);二是把输入信息作为每个阶段的输入(右),这种结构可以处理如: 输入图像的特征,输出y的序列是一段句子或者从别的类别生成语音;输入一个类别,输出一段描述文字这类问题。

N to 1

输入的是一个序列,输出的是一个单独的值。这种结构常用于处理分类问题,如:输入一段文字判断类别、输入句子判断感情倾向、输入图片判断类别。

N to N

输入和输出序列是等长的。这种可以作为简单的Char RNN可以用来生成文章,诗歌,甚至是代码。

N to M

这种结构又称Encoder-Decoder、Seq2Seq模型,它会将输入数据编码成一个上下文向量c,之后通过c输出预测序列。它广泛应用于机器翻译、文本摘要、阅读理解、对话生成等领域。

RNN的实现

准备数据集,提取特征信息最为输入数据

实现RNN的第一步需要准备数据集,下列是对图像进行分类的数据处理格式

(x_train, y_train) =  dealImage()
# 处理数据格式,将图片数据x转化到-1到1之间(除以像素点最大值255),提高精确度
x_train = x_train.reshape(x_train.shape[0], 56, -1).astype('float') / 255.0
# 转换labels为one hot格式
y_train = np_utils.to_categorical(y_train, num_classes=num_classes)
# 同样的方法加载测试数据
(x_test, y_test) =  dealTestImage()
# deal.py 处理图片分类时的数据预处理,读取图片数据和对应类别
def dealImage():
    result = [(src, label)....] # 路径和类别的对应关系可以保存到本地文件或者数据库中,需要使用的时候在读取
    images = []
    labels = []
    for row in result:
        imsrc = cv2.imread(row[0], 0)
        im = cv2.resize(imsrc, (56, 56), interpolation=cv2.INTER_AREA)  # 转换数据格式(按自己需要的格式转换) 
        imgData = np.array(im) # numpy array化
        images.append(imgData)
        labels.append(row[1])
    images = np.array(images)
    labels = np.array(labels)
    return (images, labels)

搭建神经网络结构(从输入到输出)

根据前向传播算法的理论,搭建RNN神经网络,选择恰当的神经网络的层数、神经元数目

  • 创建模型
    • 创建模型 keras模型分为顺序模型和函数式API模型
    • 顺序模型是多个网络层的线性堆叠,可以帮助我们快速创建一些简单的模型,是我们最常使用的模型
    • 函数式API模型是用来定义复杂模型(多输出模型、有向无环图、具有共享层的模型)的方法
 # 创建模型,当前模型比较简单无复杂的结构,使用顺序模型即可
model = Sequential()
cell_size = 300
  • 搭建神经网络
    • 如果当前层的输入维度与前一层的输出维度一致,可以不写输入维度
    • (如果创建多层)每层SimpleRNN之间必须设置return_sequences=True(默认是False)
    • 激活函数也是神经网络中一个很重的部分。每一层的网络输出都要经过激活函数。
      • 常用激活函数有:softmaxSoftplusRelutanhsigmodhard_sigmoidlinear
      • 经过调试,当前模型的SimpleRNN适合使用tanh函数,输出Dense层适合采用softmax进行分类
# 循环神经网络
model.add(SimpleRNN(
    units=cell_size,  # 输出数据的维度(当前层神经元的数目)
    activation='tanh',  # 激活函数,默认即tanh
    return_sequences=True,
    input_shape=(56, 56)  # 输入数据的维度(shape)
))

model.add(SimpleRNN(units=cell_size, return_sequences=True))
model.add(SimpleRNN(units=cell_size, return_sequences=True))
# 每层SimpleRNN之间必须设置return_sequences=True(默认是False)
# return_sequences 是返回输出序列的最后一个输出(False),还是返回全部序列(True)
# return_sequences=True 表示我们需要完整的编码序列,而不仅仅是最终总结状态

# return_sequences=True 返回的是个多维数组,如果下一层无法接收该种多维数组,则层需要设置为return_sequences=True或者不设置取默认值
model.add(SimpleRNN(units=cell_size))
# 添加全连接层作为输出层  
model.add(Dense(num_classes, activation='softmax'))

大量特征数据输入神经网络,优化神经网络参数,训练模型

使用反向传播算法迭代训练模型,根据每次训练结果优化参数后再次训练直到得到能尽可能好的预测结果的模型

反向传播算法tensorflow与keras都有封装,直接调用方法即可

  • 第一步需要定义模型的优化器、损失函数
    • 机器学习分为构建模型、训练模型两部分。优化器和损失函数是模型训练时两个最重要的参数
    • 优化器是用来更新和计算影响模型训练和输出的参数,是模型的输出逼近或达到最优值,从而最小(大)化损失函数的值
      • 优化器有:SGDAdagradRMSpropAdam
      • 本例采用的是常用的Adam算法
    • 损失函数是用来评估模型好坏程度的,即预测值与真实值的不一致程度
      • 损失函数有msemaemapemslehingecategorical_crossentropy等等
      • 本例采用适合分类的交叉熵categorical_crossentropy函数
# 初始化优化器
adam = Adam(lr=1e-4)
# 定义优化器,loss function,训练过程中计算准确率
model.compile(
    optimizer=adam, # 优化器
    loss='categorical_crossentropy',  # 损失函数 mse 均方差 categorical_crossentropy 交叉熵(多分类)
    metrics=['accuracy'] # 训练和测试期间的模型评估标准
)
  • 传入数据,调用keras模型的方法开始训练
# 开始训练模型,调用model.fit()方法,方法采用时序后向传播算法训练模型
# 以给的数目的轮次训练模型,到第epochs轮结束训练,每轮多个批次,每批次大小batch_size
model.fit(x_train, y_train, batch_size=32, epochs=20)
# 预测模型的损失值和准确率
loss, accuracy = model.evaluate(x_test, y_test)
# 保存模型,以便使用时加载
model.save('myfile/rnn.hdf5')

训练结果

使用模型预测和分类数据

加载已保存的模型,输入待预测的,输出最可能的结果

# 加载模型
model = load_model('myfile/rnn.hdf5')
# 使用模型预测结果
pred = model.predict(data) # data 是numpy的array格式
# 取出可能性最大的值作为预测结果
print([final.argmax() for final in pred])

RNN常见变体

双向循环网络(Bi-RNN)

在处理序列信息时,有时我们不仅需要之前的信息,还需要之后的信息,比如:预测我__苹果这句话所缺失的词是什么,我们就需要根据上下文的词汇一起去预测。而Bi-RNN能很好的解决这个问题,它是一个由两个RNN上下叠加在一起组成的相对较简单的RNN。因此,它的输出由前向RNN和后向RNN共同决定,它当前时刻输出(第t步的输出)不仅仅与之前序列有关,还与之后序列有关。

堆叠循环神经网络(SRNN)

SRNN是在全连接的单层循环神经网络的基础上堆叠形成的深度算法。该网络具有更强大的表达与学习能力,但是复杂性也随之提高,同时需要更多的训练数据。

长短期记忆网络(LSTM)

RNN在处理长期依赖(时间序列上距离较远的节点)时会遇到巨大的困难,因为计算距离较远的节点之间的联系时会涉及雅可比矩阵的多次相乘,会造成梯度消失或者梯度膨胀的现象。 LSTM就是一种为了解决长期依赖问题而提出的特殊RNN,因此,记住长时间的信息是它的基本功能

门控循环单元(GRU)

GRU可以看成是LSTM的变种,GRU把LSTM中的遗忘门和输入们用更新门来替代。 把cell state和隐状态ht进行合并,在计算当前时刻新信息的方法和LSTM有所不同。GRU的构造更简单,在训练数据大的情况下能节省更多时间

参考文献