Python深度学习(循环神经网络)--学习笔记(十三)

6.2 理解循环神经网络

  • 目前见过的所有神经网络(比如密集连接网络和卷积神经网络)都有一个特点,那就是它们都没有记忆。它们单独处理每个输入,在输入与输入之间没有保存任何状态。对于这样的网络,要想处理数据点的序列或时间序列,你需要向网络同时展示整个序列,即将序列转换成单个数据点。例如,在IMDB示例中就是这么做的:将全部电影评论转换为一个大向量,然后一次性处理。这种网络叫作前馈网络(feedforward network)。
  • 与此相反,当你在阅读这个句子时,你是一个词一个词地阅读(或者说,眼睛一次扫视一次扫视地阅读),同时会记住之前的内容。这让你能够动态理解这个句子所传达的含义。生物智能以渐进的方式处理信息,同时保存一个关于所处理内容的内部模型,这个模型是根据过去的信息构建的,并随着新信息的进入而不断更新。
  • 循环神经网络(RNN,recurrent neural network)采用同样的原理,不过是一个极其简化的版本:它处理序列的方式是,遍历所有序列数据,并保存一个状态(state),其中包含与已查看内容相关的信息。实际上,RNN是一类具有内部环的神经网络。在处理两个不同的独立序列(比如两条不同的IMDB评论)之间,RNN状态会被重置,因此,你仍可以将一个序列看作单个数据点,即网络的单个输入。真正改变的是,数据点不再是在单个步骤中进行处理,相反,网络内部会对序列元素进行遍历。
  • 用Numpy来实现一个简单的RNN的前向传递,这个RNN的输入是一个张量序列,将其编码成大小为(timesteps, input_features)的二维张量。它对时间步(timestep)进行遍历,在每个时间步,它考虑 t t t时刻的当前状态与 t t t时刻的输入(形状为(input_features)),对二者计算得到 t t t时刻的输出。然后,我们将下一个时间步的状态设置为上一个时间步的输出。对于第一个时间步,上一个时间步的输出没有定义,所以它没有当前状态。因此,你需要将状态初始化为一个全零向量,这叫作网络的初始状态(initial state)。
# RNN伪代码
state_t = 0 # t时刻的状态
for input_t in input_sequences: # 对序列元素进行遍历
	output_t = f(input_t, state_t)
	state_t = output_t # 前一次的输出变成下一次迭代的状态
  • 函数f:从输入和状态到输出的变换,其参数包括两个句子(W和U)和一个偏置向量。类似于前馈网络中密集连接层所做的变换。
# 更详细的RNN伪代码
state_t = 0
for input_t in input_sequence:
	output_t = activation(dot(W, input_t) + dot(U, state_t) + b)
	state_t = output_t
# 简单RNN的Numpy实现
import numpy as np

timesteps = 100 # 输入序列的时间步数
input_features = 32 # 输入特征空间的维度
output_features = 64 # 输出特征空间的维度

# 输入数据:随机噪声,仅作为示例
inputs = np.random.random((timesteps, input_features))

# 初始状态:全零向量
state_t = np.zeros((output_features, ))

# 创建随机的权重矩阵
W = np.random.random((output_features, input_features))
U = np.random.random((output_features, output_features))
b = np.random.random((output_features,))

successive_outputs = []
# input_t是形状为(input_features, )的向量
for input_t in  inputs:
    output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)
    successive_outputs.append(output_t)
    state_t = output_t

final_output_sequence = np.stack(successive_outputs, axis=0)
print(final_output_sequence.shape)
  • RNN是一个for循环,它重复使用循环前一次迭代的计算结果,仅此而已。当然,你可以构建许多不同的RNN,它们都满足于上述定义。这个例子只是最简单的RNN表述之一。RNN的特征在于其时间步函数,比如前面例子中的函数:
output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)
  • 本例中,最终输出是一个形状为(timesteps, output_features)的二维张量,其中每个时间步是循环在t时刻的输出。输出张量中的每个时间步t包含输入序列中时间步0~t的信息,即关于全部过去的信息。因此,在多数情况下,你并不需要这个所有输出组成的序列,你只需要最后一个输出(循环结束时的output_t),因为它已经包含了整个序列的信息。

6.2.1 Keras中的循环层

  • 上面Numpy的简单实现,对应一个实际的Keras层,即SimpleRNN层。
from keras.layers import SimpleRNN
  • 二者有一点区别:SimpleRNN层能够像其他Keras层一样处理序列批量,而不是像Numpy示例哪有只能处理单个序列。因此,它接收形状为(batch_size, timesteps, input_features)的输入,而不是(timesteps, input_features)。
  • 与Keras中的所有循环层一样,SimpleRNN可以在两种不同的模式下运行:一种是返回每个时间步连续输出的完整序列,即形状为(batch_size, timesteps, output_features)的三维张量;另一种是只返回每个输入序列的最终输出,即形状为(batch_size, output_features)的二维张量。这两种模式由return_sequences这个构造函数参数来控制。
# 只返回最后一个时间步的输出。
from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN

model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32))
print(model.summary())

# 返回完整的状态序列
model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
print(model.summary())
  • 为了提高网络的表示能力,将多个循环层逐个堆叠有时也是很有用的。在这种情况下,你需要让所有中间层都返回完整的输出序列。
# 多个循环层的堆叠
model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32))
print(model.summary())
  • 在第3章,处理这个数据集的第一个简单方法得到的测试精度是88%。不幸的是,与这个基准相比,这个小型网络的表现并不好(验证精度只有85%)。问题的部分原因在于,输入只考虑前500个单词,而不是整个序列,因此,RNN获得的信息比前面的基准模型更少。另一部分原因在于,SimpleRNN不擅长处理长序列,比如文本。

6.2.2 理解LSTM层和GRU层

  • SimpleRNN并不是Keras中唯一可用的循环层,还有另外两个:LSTM和GRU。在实践中总会用到其中之一,因为SimpleRNN通常过于简化,没有实用价值。SimpleRNN的最大问题是,在时刻t,理论上来说,它应该能够记住许多时间步之前见过的消息,但实际上它是不可能学到这种长期依赖的。其原因在于梯度消失(vanishing gradient problem),这一效应类似于在层数较多的非循环网络(即前馈网络)中观察到的效应:随着层数的增加,网络最终变得无法训练。Hochreiter、Schmidhuber和Bengio在20世纪90年代初研究了这一效应的理论原因。LSTM层和GRU层都是为了解决这个问题而设计的。
  • 先来看LSTM层。其背后的长短期记忆(LSTM,long short-term memory)算法由Hochreiter和Schmidhuber在1997年开发,是二人研究梯度消失问题的重要成果。
  • LSTM层是SimpleRNN层的一种变体,它增加了一种携带信息跨越多个时间步的方法。假设有一条传送带,其运行方法平行于你所处理的序列。序列中的信息可以在可以在任意位置跳上传送带,然后被传送到更晚的时间步,并在需要时原封不动地跳回来。这实际上就是LSTM的原理:它保存信息以便后面使用,从而防止较早期的信号在处理过程中逐渐消失。(对于LSTM的具体结构,本文就不详细论述了,推荐一篇文章:https://zhuanlan.zhihu.com/p/32085405,作者对LSTM进行了详尽的讲解、通俗易懂)。

6.2.3 Keras中一个LSTM的具体例子

  • 现在我们来看一个更实际的问题:使用LSTM层来创建一个模型,然后在IMDB数据上训练模型。这网络与前面介绍的SimpleRNN网络类似。你只需指定LSTM层的输出维度,其他所有参数(有很多)都使用Keras默认值。Keras具有很好的默认值,无须手动调参,模型通常也能正常运行。
from keras.layers import Embedding, Dense, LSTM
from keras.models import Sequential

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(input_train, y_train, epochs=10, batch_size=128, validation_split=0.2)

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

6.3 循环神经网络的高级用法

  • 本节将介绍提高循环神经网络的性能和泛化能力的三种高级技巧:(1)循环dropout(recurrent dropout):这是一种特殊的内置方法,在循环层中使用dropout来降低过拟合。(2)堆叠循环层(stacking recurrent layers):这会提高网络的表示能力(代价是更高的计算负荷)。(3)双向循环层(bidirectional recurrent layer):将相同的信息以不同的方式呈现给循环网络,可以提高精度并缓解遗忘问题。

6.3.1 温度预测问题

  • 到目前为止,我们遇到的唯一一种序列数据就是文本数据,比如IMDB数据集和路透社数据集。但除了语言处理,其他许多问题中也都用到了序列数据。在本节的所有例子中,我们将使用一个天气时间序列数据集,它由德国耶拿的马克思普朗克生物地球化学研究所的气象站记录。
  • 在这个数据集中,每10分钟记录14个不同的量(比如气温、气压、温度、风向等),其中包含多年的记录。原始数据可追溯到2003年,但本例仅适用2009-2016年的数据。这个数据集非常适合用来学习处理数值型时间序列。我们将会用这个数据集构建模型,输入最近的一些数据(几天的数据点),可以预测24小时之后的气温。
# 观察耶拿天气数据集的数据
import os

data_dir = "../data"
fname = os.path.join(data_dir, "jena_climate_2009_2016.csv")

f = open(fname, 'r', encoding='utf-8')
data = f.read()
f.close()

lines = data.split('\n')
header = lines[0].split(",")
lines = lines[1:]
print(header)
print(len(lines))

# 解析数据
import numpy as np

float_data = np.zeros((len(lines), len(header) - 1))

for i, line in enumerate(lines):
    values = [float(x) for x in line.split(',')[1:]]
    float_data[i, :] = values

# 绘制温度时间序列
from matplotlib import pyplot as plt

temp = float_data[:, 1] # 温度(单位:摄氏度)
plt.plot(range(len(temp)), temp)

# 绘制前10天的温度时间序列:每10分钟记录一个数据,所以每天有144个数据点
plt.figure()

plt.plot(range(1440), temp[:1440])
plt.show()
  • 如果你想根据过去几个月的数据来预测下个月的平均温度,那么问题很简单,因为数据具有可靠的年度周期性。但从几天的数据来看,温度看起来更混乱一些。

6.3.2 准备数据

  • 这个问题的确切表述如下:一个时间步是10分钟,没steps个时间步采样一次数据,给定过去lookback个时间步之内的数据,能否预测delay个时间步之后的温度?用到的参数值如下:lookback = 720,给定过去5天内的观测数据;(2)steps = 6,观测数据的采样频率是每小时一个数据点;(3)delay = 144,目标是未来24小时之后的数据。(4)将数据预处理为神经网络可以处理的格式。这很简单。数据已经是数值型的,所以不需要做向量化。但数据中的每个时间序列位于不同的范围(比如温度通道位于-20到+30之间,但气压大约在1000毫巴上下)。你需要对每个时间序列分别做标准化,让它们在相似的范围内都取较小的值。(5)编写一个Python生成器,以当前的浮点数数组作为输入,并从最近的数据中生成数据批量,同时生成未来的目标温度。因为数据集中的样本是高度冗余的(对于第 N N N个样本和第 N + 1 N+1 N+1个样本,大部分时间步都是相同的),所以显式地保存每个样本是一种浪费。相反,我们将使用原始数据即时生成样本。
  • 预处理数据的方法是,将每个时间序列减去其平均值,然后除以其标准差。我们将使用前200000个时间步作为训练数据,所以只对这部分数据计算平均值和标准差。
# 数据标准化
mean = float_data[:200000].mean(axis=0)
std = float_data[:200000].std(axis=0)

float_data -= mean
float_data /= std
  • 生成器生成一个元组(samples, targets),其中samples是输入数据的一个批量,targets是对应的目标温度数组。生成器的参数如下:(1)data,浮点数数据组成的原始数组;(2)lookback,输入数据应该包括过去多少个时间步;(3)delay,目标应该在未来多少个时间步之后;min_index和max_index,data数组中的索引,用于界定需要抽取哪些时间步。这有助于保存一部分数据用于验证、另一部分用于测试;(4)shuffle,是打乱样本,还是按顺序抽取样本;(5)batch_size,每个批量的样本数;(6)step,数据采用的周期(单位:时间步),我们将其设为6,为的是每个小时抽取一个数据点。
# 生成时间序列样本及其目标的生成器
def generator(data, lookback, delay, min_index, max_index, shuffle=False, batch_size=128, step=6):
    if max_index is None:
        max_index = len(data) - delay - 1

    i = min_index + lookback
    while 1:
        if shuffle:
            rows = np.random.randint(min_index + lookback, max_index, size=batch_size)
        else:
            if i + batch_size >= max_index:
                i = min_index + lookback

            rows = np.arange(i, min(i+batch_size, max_index))
            i += len(rows)

        samples = np.zeros((len(rows), lookback//step, data.shape[-1]))
        targets = np.zeros((len(rows), ))

        for j, row in enumerate(rows):
            indices = range(rows[j] - lookback, rows[j], step)
            samples[j] = data[indices]
            targets[j] = data[rows[j] + delay][1]

        yield samples, targets
  • 下面,我们使用这个抽象的generator函数来实例化三个生成器:一个用于训练,一个用于验证,还有一个用于测试。每个生成器分别读取原始数据的不同时间段:训练生成器读取前200000个时间步,验证生成器读取随后的100000个时间步,测试生成器读取剩下的时间步。
# 准备训练生成器、验证生成器和测试生成器
lookback = 1440
step = 6
delay = 144
batch_size = 128

train_gen = generator(float_data, lookback=lookback, delay=delay, min_index=0, max_index=200000, shuffle=True, step=step, batch_size=batch_size)

val_gen = generator(float_data, lookback=lookback, delay=delay, min_index=200001, max_index=300000, step=step, batch_size=batch_size)

test_gen = generator(float_data, lookback=lookback, delay=delay, min_index=300001, max_index=None, step=step, batch_size=batch_size)

val_steps = (300000 - 200001 - lookback) // batch_size
test_steps = (len(float_data) - 300001 - lookback) // batch_size

6.3.3 一种基于常识的、非机器学习的基准方法

  • 开始使用黑盒深度学习模型解决温度预测问题之前,我们先尝试一种基于常识的简单方法。它可以作为合理性检查,还可以建立一个基准,更高级的机器学习模型需要打败这个基准才能表现出其有效性。面对一个尚没有已知解决方案的新问题时,这种基于常识的基准方法很有用。一个经典的例子就是不平衡的分类任务,其中某些类别比其他类别更常见。如果数据集中包含90%的类别A实例和10%的类别B实例,那么分类任务的一种基于常识的方法就是对新样本始终预测类别"A"。这种分类器的总体精度为90%,因此任何基于学习的方法在精度高于90%时才能证明其有效性。有时候,这样基本的基准方法可能很难打败。
  • 本例中,我们可以放心地假设,温度时间序列是连续的(明天的温度很可能接近今天的温度),并且具有每天的周期性变化。因此,一种基于常识的方法就是始终预测24小时后的温度等于现在的温度。我们使用平均绝对误差(MAE)指标来评估这种方法。
np.mean(np.abs(preds - targets))
# 计算符合常识的基准方法的MAE
def evaluate_naive_method():
    batch_maes = []
    for step in range(val_steps):
        samples, targets = next(val_gen)
        preds = samples[:, -1, 1]
        mae = np.mean(np.abs(preds - targets))
        batch_maes.append(mae)

    print(np.mean(batch_maes))
    # 将MAE转换成摄氏温度误差
    print(np.mean(batch_maes) * std[1])

evaluate_naive_method()

6.3.4 一种基本的机器学习方法

  • 在尝试机器学习方法之前,建立一个基于常识的基准方法是很有用的;同样,在开始研究复杂且计算代价很高的模型(比如RNN)之前,尝试使用简单且计算代价低的机器学习模型也是很有用的,比如小型的密集连接网络。这可以保证进一步增加问题的复杂度是合理的,并且会带来真正的好处。
# 训练并评估一个密集连接模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Flatten(input_shape=(lookback//step, float_data.shape[-1])))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=val_gen, validation_steps=val_steps)

# 绘制结果
import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
  • 部分验证损失接近不包含学习的基准方法,但这个结果并不可靠。这也展示了首先建立这个基准方法的优点,事实证明,超越这个基准并不容易,我们的常识中包含了大量有价值的信息,而机器学习模型并不知道这些信息。
  • 如果从数据到目标之间存在一个简单且表现良好的模型(即基于常识的基准方法),那为什么我们训练的模型没有找到这个模型并进一步改进呢?原因在于,这个简单的解决方案并不是训练过程所要寻找的目标。我们在模型空间(即假设空间)中搜索解决方案,这个模型空间是具有我们所定义架构的所有两层网络组成的空间。这些网络已经相当复杂了。如果你在一个复杂模型的空间中寻找解决方案,那么可能无法学到简单且性能良好的基准方法,虽然技术上来说它属于假设空间的一部分。通常来说,这对机器学习是一个非常重要的限制:如果学习算法没有被硬编码要求去寻找特定类型的简单模型,那么有时候参数学习是无法找到简单问题的简单解决方案的。

6.3.5 第一个循环网络基准

  • 第一个全连接方法的效果并不好,但这并不意味着机器学习不适用于这个问题。前一个方法首先将时间序列展平,这从输入数据中删除了时间的概念。我们来看一下数据本来的样子:它是一个序列,其中因果关系和顺序都很重要。我们将尝试一种循环序列处理模型,它应该特别适合这种序列数据,因为它利用了数据点的时间序列。
  • 我们将使用Chung等人在2014年开发的GRU层,而不是上一节介绍的LSTM层。门控循环单元(GRU,gated recurrent unit)层的工作原理和LSTM相同。但它做了一些简化,因此运行的计算代价更低。
# 训练并评估一个基于GRU的模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=val_gen, validation_steps=val_steps)

import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

6.3.6 使用循环dropout来降低过拟合

  • 从训练和验证曲线中可以明显看出,模型出现过拟合:几轮过后,训练损失和验证损失就开始显著偏离。我们已经学过降低过拟合的一种经典技术-dropout,即将某一层的输入单元随机设为0,其目的是打破该层训练数据中的偶然相关性。但在循环网络中如何正确地使用dropout,这并不是一个简单的问题。人们早就知道,在循环层前面应用dropout,这种正则化会妨碍学习过程,而不是有所帮助。2015年,在关于贝叶斯深度学习的博士论文中,Yarin Gal确定了在循环网络中使用dropout的正确方法:对每个时间步应该使用相同的dropout掩码(dropout mask,相同模式的舍弃单元),而不是让dropout掩码随着时间步的增加而随机变化。此外,为了对GRU、LSTM等循环层得到的表示做正则化,应该将不随时间变化的dropout掩码应用于层的内部循环激活(叫作循环dropout掩码)。对每个时间步使用相同的dropout掩码,可以让网络沿着时间正确地传播其学习误差,而随时间随机变化的dropout掩码则会破坏这个误差信号,并且不利于学习过程。
  • Yarin Gal使用Keras开展这项研究,并帮助将这种机制直接内置到Keras循环层中。Keras的每个循环层都有两个与dropout相关的参数:一个是dropout,它是一个浮点数,指定该层输入单元的dropout比率;另一个是recurrent_dropout,指定循环单元的dropout比率。我们向GRU层添加dropout和循环dropout,看一下这么做对过拟合的影响。因为dropout正则化的网络总是需要更长的时间才能完全收敛,所以网络训练轮次增加为原来的2倍。
# 训练并评估一个使用dropout
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32, dropout=0.2, recurrent_dropout=0.2, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=val_gen, validation_steps=val_steps)

import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

6.3.7 循环层堆叠

  • 模型不再过拟合,但似乎遇到了性能瓶颈,所以我们应该考虑增加网络容量。回想一下机器学习的通用工程流程:增加网络容量通常是一个好主意,直到过拟合变成主要的障碍(假设你已经采取基本步骤来降低过拟合,比如使用dropout)。只要过拟合不是太严重,那么很可能是容量不足的问题。
  • 增加网络容量的通常做法是增加每层单元数或增加层数、循环层堆叠(recurrent layer stacking)是构建更加强大的循环网络的经典方法,例如,目前谷歌翻译算法就是7个大型LSTM层的堆叠。
  • 在Keras中逐个堆叠循环层,所有中间层都应该返回完整的输出序列(一个3D张量),而不是只返回最后一个时间步的输出。这可以通过指定return_sequence=True来实现。
# 训练并评估一个使用dropout正则化的堆叠GRU模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32, dropout=0.1, recurrent_dropout=0.5, return_sequences=True, input_shape=(None, float_data.shape[-1])))
model.add(layers.GRU(64, activation='relu', dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=val_gen, validation_steps=val_steps)

import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
  • 可以看到,添加一层的确对结果有所改进,但并不显著。我们可以得出两个结论:(1)因为过拟合仍然不是很严重,所以可以放心地增大每层的大小,以进一步改进验证损失。但这么做的计算成本很高;(2)添加一层模型并没有显著改进,所以提高网络能力的回报在逐渐减小。

4.3.8 使用双向RNN

  • 本节介绍的最后一种方法叫作双向RNN(bidirectional RNN)。双向RNN是一种常见的RNN变体,它在某些任务上的性能比普通RNN更好。它常用于自然语言处理,可谓深度学习对自然语言处理的瑞士军刀。
  • RNN特别依赖于顺序或时间,RNN按顺序处理输入序列的时间步,而打乱时间步或反转时间步完全改变RNN从序列中提取的表示。正是由于这个原因,如果顺序对问题很重要(比如温度预测问题),RNN的表现会很好。双向RNN利用了RNN的顺序敏感性:它包含两个普通RNN,比如你已经学过的GRU层和LSTM层,每个RN分别沿一个方向对输入序列进行处理(时间正序和时间逆序),然后将它们的表示合并在一起。通过沿这两个方向处理序列,双向RNN能够捕捉到可能被单向RNN忽略的模式。
# 使用逆序序列训练并评估一个LSTM
from keras.datasets import imdb
from keras.preprocessing import sequence
from keras import layers
from keras.models import Sequential

max_features = 10000 # 作为特征的单词个数
maxlen = 500 # 在这么多单词之后截断文本

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features) # 加载数据

# 将序列反转
x_train = [x[::-1] for x in x_train]
x_test  = [x[::-1] for x in x_test]

# 填充序列
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test,maxlen=maxlen)

model = Sequential()
model.add(layers.Embedding(max_features, 128))
model.add(layers.LSTM(32))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 模型性能与正序LSTM几乎相同。值得注意的是,在这样一个文本数据上,逆序处理的效果与正序一样好,这证实了一个假设:虽然单词顺序对理解语言很重要,但使用哪种顺序并不重要。重要的是,在逆序序列上训练的RNN学到的表示不同于在原始序列上学到的表示,正如在现实世界中,如果时间倒流(你的认识是第一天死去、最后一天出生),那么你的心智模型也会完全不同。在机器学习中,如果一种数据表示不同但有用,那么总是值得加以利用,这种表示与其他表示的差异越大越好,它们提供了查看数据的全新角度,抓住了数据中被其他方法忽略的内容,因此可以提高模型在某个任务上的性能。这是集成(ensmbling)方法背后的直觉。
  • 双向RNN正是利用这个想法来提高正序RNN的性能。它从两个方向查看数据,从而得到更加丰富的表示,并捕捉到仅适用正序RNN时可能忽略的一些模式。
  • 在Keras中将一个双向RNN实例化,我们需要使用Bidirectional层,它的第一个参数是一个循环层实例。Bidirectional对这个循环层创建了两个单独的实例,然后使用一个实例按正序处理输入序列,另一个实例按逆序处理输入序列。
# 训练并评估一个双向LSTM
model = Sequential()
model.add(layers.Embedding(max_features, 32))
model.add(layers.Bidirectional(layers.LSTM(32)))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
# 训练一个双向GRU
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Bidirectional(layers.GRU(32), input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=val_gen, validation_steps=val_steps)

6.3.10 小结

  • 我们在第4章学过,遇到新问题时,最好首先为你选择的指标建立一个基于常识的基准。如果没有需要打败的基准,那么就无法分辨是否取得了真正的进步。
  • 在尝试计算代价较高的模型之前,先尝试一些简单的模型,以此证明增加计算代价是有意义的。有时简单模型就是你的最佳选择。
  • 如果时间顺序对数据很重要,那么循环网络是一种很适合的方法,与那些先将时间数据展平的模型相比,其性能要更好。
  • 想要在循环网络中使用dropout,你应该使用一个不随时间变化的dropout掩码与循环dropout掩码。这二者都内置于Keras的循环层中,所以你只需要使用循环层的dropout和recurrent_dropout参数即可。
  • 与单个RNN层相比,堆叠RNN的表示能力更加强大。但它计算代价也更高,因此不一定总是需要。虽然它在机器翻译等复杂问题上很有效,但在较小、较简单的问题上可能不一定有用。
  • 双向RNN从两个方向查看一个序列,它对自然语言处理问题非常有用。但如果在序列数据中最近的数据比序列开头包含更多的信息,那么这种方法的效果就不明显。

你可能感兴趣的:(深度学习,神经网络,深度学习)