keras深度学习实战(27)——循环神经详解与实现(代码片段)

盼小辉丶 盼小辉丶     2023-03-17     194

关键词:

Keras深度学习实战(27)——循环神经详解与实现

0. 前言

我们已经学习了多种将文本表示为向量的方法,并且学习了如何利用这些向量表示进行情感分类。但这种方法的缺点之一是没有考虑单词的顺序,例如,使用这类方法时,句子 A is faster than B 与句子 B is faster than A 具有相同的含义表示,因为这两个句子中的词完全相同,但由于它们的词序不同,实际上应具有不同的含义。在需要保留单词顺序的情况下,循环神经网络 (Recurrent neural networks, RNN) 将会派上用场。

1. 循环神经网络 (Recurrent Neural Network, RNN) 架构简介

1.1 传统文本处理方法的局限性

当我们要在给定事件序列的情况下预测下一个事件时,循环神经网络 (Recurrent Neural Network, RNN) 非常有用。一个简单的示例是,预测 This is an _____ 横线上的单词。
传统的文本分析技术解决该问题的方式通常需要对每个单词进行编码,同时为潜在的新单词提供附加索引:

This: 1,0,0,0
is: 0,1,0,0
an: 0,0,1,0

然后,将短语编码为:

This is an: 1,1,1,0

创建训练数据集:

Input --> 1,1,1,0
Output --> 0,0,0,1

最后,用输入和输出建立模型。

但该模型的主要缺点之一是当输入句子中的单词顺序改变时,输入表示并不会改变,例如,无论是 this is an,或者 an is this,它们的表示均为 1, 1, 1, 0
但是,我们知道单词改变顺序后其含义并不同,因此不能用的相同形式表示,因此这需要我们使用不同的体系结构,句子中的每个单词都需要按照文本顺序输入到不同的输入框中,因此,句子的结构得以被保留。例如,this 输入第一个框中,is 输入第二个框中,an 输入第三个框中,输出框中将输出预测值。

1.2 RNN 架构简介

可以将 RNN 视为一种内存保存的机制,如果网络能够提供一个单独的内存变量,每次提取词向量的特征并刷新内存变量,直至最后一个输入完成,此时的内存变量即存储了所有序列的语义特征,并且由于输入序列之间的先后顺序,使得内存变量内容与序列顺序紧密关联。RNN 架构可视化如下:

右侧的网络是左侧的网络的展开后的结果。右侧的网络在每个时刻接受当前时刻输入以及上一时刻网络状态,并在每个时刻提取一个输出。
在每个时刻 t t t,网络层接受当前时刻的输入 x t x_t xt 和上一个时刻的网络状态向量 h t − 1 h_t−1 ht1,根据网络内部运算逻辑 h t = f θ ( h t − 1 , x t ) h_t=f_\\theta(h_t-1,x_t) ht=fθ(ht1,xt) 计算得到当前时刻的新状态向量 h t h_t ht,并写入内存状态中。在每个时刻,网络层均有输出 o t o_t ot o t = g Φ ( t ) o_t = g_\\Phi(t) ot=gΦ(t),即根据网络的当前时刻状态向量计算后输出。
网络循环接受序列的每个特征向量 x t x_t xt,并刷新内部状态向量 h t h_t ht,同时形成输出 o t o_t ot。这种网络结构就是循环神经网络 (Recurrent Neural Network, RNN) 结构。

1.3 RNN 内存存储机制

如前一小节所示,我们需要内存存储器存储中间状态,在文本分析相关应用中,下一个单词不仅取决于前一个单词,而是取决于要预测的单词的完整上下文。
由于我们需要根据前面的单词预测下一个单词,因此需要一种方式将它们保留在内存中,以便我们可以更准确地预测下一个单词。此外,我们按单词出现的顺序存储内存;也就是说,与离预测单词较远的单词相比,最近出现的单词通常在预测时更有用。

2. 从零开始构建 RNN

2.1 模型分析

在节中,我们使用一个简单示例从零开始构建循环神经网络 (Recurrent neural networks, RNN) ,以便对 RNN 如何解决顺序事件问题有更深入的了解。
RNN 与典型的神经网络结构类似,但在其基础上进行了一些修改,以便在当前时刻考虑先前时刻的网络状态。接下来,我们将使用一个简单示例来介绍构建 RNN 模型的相关细节。
我们考虑如下所示的示例文本:This is an example。给定两个单词的序列,我们任务是预测第三个单词。因此,输入、输出数据构建如下:

inputoutput
this, isan
is, anexample

根据以上分析,构建 RNN 的策略如下:

  • 对单词进行独热编码
  • 确定输入的最大长度
  • 将长度不足的输入填充到最大长度,以使所有输入都具有相同的长度
  • 处理输入和输出数据,拟合 RNN 模型

2.2 使用 Python 从零开始构建 RNN 模型

在本节中,我们实现以上 RNN 模型策略。

(1) 首先,定义模型输入和输出:

#define documents
docs = ['this, is','is an']
# define class labels
labels = ['an','example']

(2) 预处理数据集,以便可以将其传递到 RNN 模型:

from collections import Counter
counts = Counter()
for i,review in enumerate(docs+labels):
    counts.update(review.split())
words = sorted(counts, key=counts.get, reverse=True)
vocab_size=len(words)
word_to_int = word: i for i, word in enumerate(words, 1)

在以上代码中,我们识别给定数据集中的所有不重复单词及其对应的频率,并为每个单词分配一个 ID。输出 word_to_int 的值如下:

print(word_to_int)
# 'is': 1, 'an': 2, 'this,': 3, 'example': 4

(3) 获取输入和输出单词及其对应的 ID

encoded_docs = []
for doc in docs:
    encoded_docs.append([word_to_int[word] for word in doc.split()])
encoded_labels = []
for label in labels:
    encoded_labels.append([word_to_int[word] for word in label.split()])
print('encoded_docs: ',encoded_docs)
print('encoded_labels: ',encoded_labels)
# encoded_docs:  [[3, 1], [1, 2]]
# encoded_labels:  [[2], [4]]

在以上代码中,我们将输入句子的每个单词 的 ID 附加到列表中,并同样,将输出的每个单词的 ID 附加到列表中。

(4) 编码输入时需要注意的另一个因素是输入长度。在情感分析中,输入文本的长度可能因评论而异。但神经网络期望输入大小是固定的。为了解决这个问题,我们在输入上进行填充。填充确保所有输入的编码长度相似。虽然在此简单示例中,两个输入的长度都是 2,但在实践中,我们很可能会遇到输入长度不同的情况。我们可以按以下方式执行填充:

from keras.preprocessing.sequence import pad_sequences
max_length = 2
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='pre')

在以上代码中,我们将 encoding_docs 传递给 pad_sequences 函数,该函数确保所有输入数据具有相同的长度——等于 maxlen 参数值,对于那些长度小于 maxlen 的输入,将这些数据以 0 作为填充以使其长度达到 maxlen,且填充发生在原始编码向量输入的左侧。

(5) 创建输入数据后,对输出数据进行预处理,以便可以将其用于监督模型训练。对输出标签进行独热编码:

from keras.utils import to_categorical
one_hot_encoded_labels = to_categorical(encoded_labels, num_classes=5)
print(one_hot_encoded_labels)
# [[0. 0. 1. 0. 0.] [0. 0. 0. 0. 1.]]

可以看到,给定输出值 (encoded_labels)2, 4,则输出向量为 [[0. 0. 1. 0. 0.] [0. 0. 0. 0. 1.]]

(6) 接下来,构建 RNN 模型。
RNN 期望输入的形状为 (batch_size, time_steps, features_per_timestep)。因此,首先将输入 padded_docs 整形为以下格式:

padded_docs = padded_docs.reshape(2,2,1)

需要注意的是,实际情况下,我们使用每个单词的单词嵌入作为输入,但是,但此示例的目的只是为了了解 RNN 的构建细节,因此直接使用独热编码。
使用 SimpleRNN方 法初始化 RNN 模型:

from keras.models import Sequential
from keras.layers import SimpleRNN, Dense
# define the model
embed_length=1
max_length=2
model = Sequential()
model.add(SimpleRNN(1,activation='tanh', return_sequences=False,input_shape=(max_length,embed_length),unroll=True))

在以上代码中,使用 return_sequences 参数指定我们是否要在每个时间步获取网络中间状态值,当 return_sequences=False 时,表示我们只在最后一个时刻输出值。通常,在多对一任务中,有多个输入(每个时刻需要一个输入),但仅在最后一个时刻输出一个值,return_sequences 将为 false,例如,给定前五天的股价,预测第六天的股价。如果我们需要在每个时刻中获取网络中间状态,则将 return_sequences 设置为 True,例如,在机器翻译中有多个输入,且需要多个输出。
RNN 的输出连接到输出层:

model.add(Dense(5, activation='softmax'))

输出层有 5 个神经单元,因为有五种可能的输出类别,输出中有 5 个值,其中每个值对应于它属于 ID 0ID 4 的单词概率。

(7) 编译模型:

model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
model.summary()

模型架构的简要信息输入如下:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 1)                 3         
_________________________________________________________________
dense (Dense)                (None, 5)                 10        
=================================================================
Total params: 13
Trainable params: 13
Non-trainable params: 0
_________________________________________________________________

在训练模型之前,让我们了解 SimpleRNN 层中的参数。SimpleRNN 的输出形状为 (None, 1)。输出形状中的 None 代表 batch_sizebatch_size 可以为任何数值。1 即为我们为 SimpleRNN 指定的输出维度。
接下来,我们介绍为什么在 simpleRNN 层中参数数量为 3。由于每个时刻只有一个输入特征,且输出也为一个值,则只需要一个权重值。如果输出值维度为 40,则需要 40 个参数。除了将输入连接到输出值的 1 个权重之外,还需要 1 个偏置项。最后 1 个参数用于连接来自上一个时刻的网络中间状态与当前时刻的网络状态,因此,共计有 3 个参数。

(8) 拟合模型以预测输出:

import numpy as np

model.fit(padded_docs, np.array(one_hot_encoded_labels),epochs=100)

在第一个输入数据样本上提取预测输出:

print(model.predict(padded_docs[0].reshape(1,2,1)))

提取的输出如下:

[[0.16531141 0.06173846 0.2435546  0.2741682  0.25522736]]

2.3 验证 RNN 模型输出

训练模型后,我们通过模拟前向计算了解 RNN 的工作原理,即提取模型的权重,使用 NumPy 通过前向传播过程检查预测值。
(1) 检查模型权重:

print(model.weights)

输出的模型权重结果如下所示:

[<tf.Variable 'simple_rnn/simple_rnn_cell/kernel:0' shape=(1, 1) dtype=float32, numpy=array([[1.1845653]], dtype=float32)>, <tf.Variable 'simple_rnn/simple_rnn_cell/recurrent_kernel:0' shape=(1, 1) dtype=float32, numpy=array([[0.09947798]], dtype=float32)>, <tf.Variable 'simple_rnn/simple_rnn_cell/bias:0' shape=(1,) dtype=float32, numpy=array([0.10056799], dtype=float32)>, <tf.Variable 'dense/kernel:0' shape=(1, 5) dtype=float32, numpy=
array([[ 0.8152295 , -0.09143085,  0.78972566, -0.13355458,  0.01479324]],
      dtype=float32)>, <tf.Variable 'dense/bias:0' shape=(5,) dtype=float32, numpy=
array([-0.09761976, -0.09680502,  0.09468284, -0.09676518,  0.09872623],
      dtype=float32)>]

在以上输出中,simple_rnnkernel 将输入连接到 RNN 网络中,recurrent_kernel 将上一个时时刻的网络状态连接到当前时刻的网络状态。dense 层中的 kernelbias 表示将 RNN 层连接到最终输出层。提取权重:

model.get_weights()

(2) 将输入传递到 RNN 层:

print(padded_docs[0])

输出值为:

[[3]
 [1]]

可以看到,第 1 个时刻输入为 3,第 2 个时刻输入为 1。初始化第 1 个时刻输入:

input_t0 = padded_docs[0][0]

(3)1 个时刻的输入值乘以将输入连接到 RNN 层的权重,然后加上偏置值:

input_t0_kernel_bias = input_t0*model.get_weights()[0] + model.get_weights()[2]

(4) 将以上的输出传递到 tanh 激活函数,来计算第 1 时刻网络状态值:

hidden_layer0_value = np.tanh(input_t0_kernel_bias)

(5) 计算第 2 个时刻的网络状态值,输入的值为 1

input_t1 = padded_docs[0][1]

(6)2 个时刻的输入通过权重和偏置时的输出值如下:

put_t1_kernel_bias = input_t1*model.get_weights()[0] + model.get_weights()[2]

可以看到,无论在任何时刻,输入的权重和偏置值均保持不变。

(7) 各个时刻的网络中间状态值计算如下:

h ( t ) = Φ h ( z h ( t ) ) = Φ h ( W x h x ( t ) + W h h h ( t − 1 ) + b h ) h^(t)=\\Phi_h(z_h^(t))=\\Phi_h(W_xhx^(t)+W_hhh^(t-1)+b_h) h(t)=Φh(zh(t))=Φh(Wxhx(t)+Whhh(t1)+bh)

其中 Φ Φ Φ 是网络所用的激活函数,通常使用 tanh 激活函数。
输入层到 RNN 中间状态的计算包括两个部分:

  • 输入层值和 kernel 权重相乘
  • 上一时间步的网络中间状态和 recurrent_kernel 权重相乘

给定时刻的网络中间状态值的最终结果需要首先计算以上两部分的和,然后通过 tanh 激活函数得到最终结果:

input_t1_recurrent = hidden_layer0_value*model.get_weights()[1]

total_input_t1 = input_t1_kernel_bias + input_t1_recurrent

output_t1 = np.tanh(total_input_t1)

(8) 将最后一个时刻的输出传递到全连接层:

final_output = output_t1*model.get_weights()[3] + model.get_weights()[4]

model.get_weights() 方法的第 4 和第 5 个输出对应于从 RNN 层到输出层的连接。

(9) 通过 softmax 激活获得最终输出:

print(np.exp(final_output)/np.sum(np.exp(final_output)))

输出结果如下,可以看到通过网络的输入前向传播计算得到的输出与 model.predict 函数得到的输出结果相同。

[[0.27254263 0.12260658 0.32298458 0.11813989 0.16372632]]

小结

循环神经网络 (Recurrent neural networks, RNN) 是一种十分重要的神经网络模型,基本已经成为机器翻译、语音助手、序列视频分析等应用的标准处理手段。由于其具有记忆特性,可以处理上下文环境有关系的序列数据,因此在自然语言处理领域中有着广泛的应用。本文首先介绍了 RNN 的核心思想及其架构,然后从零开始实现了一个 RNN 模型,并通过前向计算验证了 RNNkeras深度学习实战——使用循环神经网络构建情感分析模型(代码片段)

Keras深度学习实战——使用循环神经网络构建情感分析模型0.前言1.使用循环神经网络构建情感分析模型1.1数据集分析1.2构建RNN模型进行情感分析相关链接0.前言在《循环神经详解与实现》一节中,我们已经了解循环神经网络(Recurr... 查看详情

keras深度学习实战——使用循环神经网络构建情感分析模型(代码片段)

Keras深度学习实战——使用循环神经网络构建情感分析模型0.前言1.使用循环神经网络构建情感分析模型1.1数据集分析1.2构建RNN模型进行情感分析相关链接0.前言在《循环神经详解与实现》一节中,我们已经了解循环神经网络(R... 查看详情

keras深度学习实战(20)——神经风格迁移详解(代码片段)

Keras深度学习实战(20)——神经风格迁移详解0.前言1.神经风格迁移原理2.模型分析3.使用Keras实现神经风格迁移小结系列链接0.前言在DeepDream图像生成算法的学习中,我们通过修改像素值试图使神经网络中卷积核的激... 查看详情

keras深度学习实战——卷积神经网络详解与实现(代码片段)

Keras深度学习实战(7)——卷积神经网络详解与实现0.前言1.传统神经网络的缺陷1.1构建传统神经网络1.2传统神经网络的缺陷2.使用Python从零开始构建CNN2.1卷积神经网络的基本概念2.2卷积和池化相比全连接网络的优势3.使用... 查看详情

keras深度学习实战(26)——文档向量详解(代码片段)

Keras深度学习实战(26)——文档向量详解0.前言1.文档向量基本概念2.神经网络模型与数据集分析2.1模型分析2.2数据集介绍3.利用Keras构建神经网络模型生成文档向量小结系列链接0.前言在《从零开始构建单词向量》一节中,我们... 查看详情

keras深度学习实战——批归一化详解(代码片段)

Keras深度学习实战(5)——批归一化详解0.前言1.批归一化基本概念2.在神经网络训练过程中使用批归一化小结系列链接0.前言批归一化是神经网络中关键性技术之一,使用批归一化可以大幅加快网络的收敛,同时... 查看详情

keras深度学习实战(23)——dcgan详解与实现(代码片段)

Keras深度学习实战(23)——DCGAN详解与实现0.前言1.使用DCGAN生成手写数字图像2.使用DCGAN生成面部图像2.1模型分析2.2从零开始实现DCGAN生成面部图像小结系列链接0.前言在生成对抗网络(GenerativeAdversarialNetworks,GAN)一节中,我们使用... 查看详情

keras深度学习实战——卷积神经网络的局限性(代码片段)

Keras深度学习实战(9)——卷积神经网络的局限性0.前言1.卷积神经网络的局限性2.情景1——训练数据集图像尺寸较大3.情景2——训练数据集图像尺寸较小4.情景3——在训练尺寸较大的图像时使用更大池化小结系列链接0.... 查看详情

keras深度学习实战(22)——生成对抗网络详解与实现(代码片段)

Keras深度学习实战(22)——生成对抗网络详解与实现0.前言1.生成对抗网络原理2.模型分析3.利用生成对抗网络生成手写数字图像小结系列链接0.前言生成对抗网络(GenerativeAdversarialNetworks,GAN)使用神经网络生成与原始图像集... 查看详情

keras深度学习实战(29)——长短时记忆网络详解与实现(代码片段)

Keras深度学习实战(29)——长短时记忆网络详解与实现0.前言1.RNN的局限性2.LSTM模型架构详解2.1LSTM架构2.2LSTM各组成部分与计算流程3.从零开始实现LSTM3.1LSTM模型实现3.2验证输出小结系列链接0.前言长短时记忆网络(LongShortTer... 查看详情

keras深度学习实战(29)——长短时记忆网络详解与实现(代码片段)

Keras深度学习实战(29)——长短时记忆网络详解与实现0.前言1.RNN的局限性2.LSTM模型架构详解2.1LSTM架构2.2LSTM各组成部分与计算流程3.从零开始实现LSTM3.1LSTM模型实现3.2验证输出小结系列链接0.前言长短时记忆网络(LongShortTer... 查看详情

keras深度学习实战——使用长短时记忆网络构建情感分析模型(代码片段)

Keras深度学习实战——使用长短时记忆网络构建情感分析模型0.前言1.构建LSTM模型进行情感分类1.1数据集分析1.2模型构建2.构建多层LSTM进行情感分类相关链接0.前言我们已经学习了如何使用循环神经网络(Recurrentneuralnetworks,RNN)构建... 查看详情

keras深度学习实战(19)——使用对抗攻击生成可欺骗神经网络的图像(代码片段)

Keras深度学习实战(19)——使用对抗攻击生成可欺骗神经网络的图像0.前言1.对抗攻击简介2.对抗攻击模型分析2.1模型识别图像流程2.2对抗攻击流程3.使用Keras实现对抗攻击小结系列链接0.前言近年来,深度学习在图像... 查看详情

keras深度学习实战(23)——dcgan详解与实现(代码片段)

Keras深度学习实战(23)——DCGAN详解与实现0.前言1.使用DCGAN生成手写数字图像2.使用DCGAN生成面部图像2.1模型分析2.2从零开始实现DCGAN生成面部图像小结系列链接0.前言在生成对抗网络(GenerativeAdversarialNetworks,GAN)一节中,... 查看详情

keras深度学习实战(10)——迁移学习(代码片段)

Keras深度学习实战(10)——迁移学习0.前言1.迁移学习1.1迁移学习原理1.2ImageNet数据集介绍2.利用预训练VGG16模型进行性别分类2.1VGG16架构2.2微调模型2.3错误分类的图片示例小结系列链接0.前言在《卷积神经网络的局限性》... 查看详情

keras深度学习实战(20)——deepdream模型详解(代码片段)

Keras深度学习实战(20)——DeepDream模型详解0.前言1.DeepDream的技术原理2.DeepDream模型分析3.DeepDream算法实现3.1数据加载与预处理3.2DeepDream生成模型小结系列链接0.前言在《对抗样本生成》一节中,我们通过略微修改输入... 查看详情

keras深度学习实战(13)——目标检测基础详解(代码片段)

Keras深度学习实战(13)——目标检测基础详解0.前言1.目标检测概念2.创建自定义目标检测数据集2.1windows2.2Ubuntu2.3MacOS3.使用选择性搜索在图像内生成候选区域3.1候选区域3.2选择性搜索3.3使用选择性搜索生成候选区域4.交并... 查看详情

keras深度学习实战(18)——语义分割详解(代码片段)

Keras深度学习实战(18)——语义分割详解0.前言1.语义分割基本概念2.模型与数据集分析2.1模型训练流程2.2模型输出3.实现语义分割模型3.1加载数据集3.2模型构建与训练小结系列链接0.前言在《使用U-Net架构进行图像分割》... 查看详情