内容参考以及代码整理自“深度学习四大名“著之一《Python深度学习》
查看完整代码,请看: https://github.com/ubwshook/MachineLearning
密集连接网络和卷积神经网络都有一个主要特点,那就是它们都没有记忆。它们单独处理每个输入,在输入与输入之间没有保存任何状态,对于这样的网络,要想处理数据点的序列或时间序列,你需要向网络同时展示整个序列,即将序列转换成单个数据点。
与此相反,当你在阅读的时候,你是一个词一个词地阅读,同时会记住之前的内容。这让你能够动态理解这个句子所传达的含义。生物智能以渐进的方式处理信息,同时保存一个关于所处理内容的内部模型,这个模型是根据过去的信息构建的,并随着新信息的进去而不断更新。
循环神经网络的原理与此相同: 它处理序列的方式,遍历所有序列元素,并保存一个状态(state), 起重工包含与已查看内容相关的信息。实际上,RNN是一类具有内部环的神经网络。在处理两个不同的独立序列之间,RNN状态会被重置,你仍可以将一个序列看作单个数据点,即网络的单个输入。真正改变的是,数据点不再是在单个步骤中进行处理,相反,网络内部会对序列元素进行遍历。
我们用Numpy来实现一个简单RNN的前向传递。这个RNN的输入是一个张量序列,我们将其编码为大小为(timesteps, input_features)的二维张量。
"""
纯python实现一个RNN的原理
"""
timesteps = 100 # 输入序列的时间步
input_features = 32 # 输入特征空间维度
output_features = 64 # 输出特征空间维度
inputs = np.random.random((timesteps, input_features)) # 输入数据,此处仅为示意
state_t = np.zeros((output_features,)) # 出事状态为0
# 创建随机的权重矩阵
W = np.random.random((output_features, input_features))
U = np.random.random((output_features, output_features))
b = np.random.random((output_features,))
successive_outputs = []
for input_t in inputs: # 输入形状为(input_features,)的向量
# 由输入和当前状态计算得到输出
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.concatenate(successive_outputs, axis=0)
一、Keras实现简单RNN
keras中简单RNN层使用下面语句:
from keras.layers import SimpleRNN
SimpleRNN层可以批量处理输入序列,接收形状为(batch_size, timesteps, input_feature)。与所有循环层一样SimpleRNN可以再两种模式下运行:一种是返回每个时间步连续输出的完整序列,即形状为(batch_size, timesteps, output_features)的三维张量;另一种是只返回每个输入序列的最终输出,即形状为(batch_size, output_features)的二维张量。这两种模型由return_sequences这个构造参数来控制。下面是一个简单RNN的例子。
1. 数据准备
这个例子使用的是imdb数据集,这个我们在之前使用密集连接网络解决过,如果需要回顾请点击这里。
max_features = 10000 # 作为特征的单词数量,也就是高频出现的10000个词语
maxlen = 500 # 评论500词以上截断
batch_size = 32 # 每个批次的数据个数
print('Loading data...')
(input_train, y_train), (input_test, y_test) = load_local(num_words=max_features)
print(len(input_train), 'train sequences')
print(len(input_test), 'test sequences')
print('Pad sequences (samples x time)')
input_train = sequence.pad_sequences(input_train, maxlen=maxlen) # 填充序列是所有序列都是相同长度
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)
print('input_train shape:', input_train.shape)
print('input_test shape:', input_test.shape)
2.训练简单RNN网络
这个模型第一层是词嵌入层获得词向量(词嵌入回顾),第二层使用简单RNN循环处理数据,第三层使用密集连接构成分类器。
model = Sequential()
model.add(Embedding(max_features, 32)) # 词嵌入
model.add(SimpleRNN(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)
show_acc_results(history)
训练结果看看起来不是很理想,有两个原因:1.输入只考虑了500个单词; 2.SimpleRNN不擅长处理长序列,比如文本。所以我们要引入更高级的循环层
二、LSTM和GRU
Keras中还有两个循环层,LSTM和GRU。SimpleRNN的最大问题是,不能学习长期依赖,存在梯度消失问题。随着层数的增加,网络最终无法训练。
LSTM层是SimpleRNN层的一种变体,它增加了一种携带信息跨越多个时间步的方法。假设有一条传送带,其运行方向平行于你所处理的序列。序列中的信息可以在任意位置跳上传送带,然后被传送到更晚的时间步,并在需要的时候原封不动的跳回来。它保存信息以便后面使用,从而防止较早期的信号在处理中逐渐消失。
LSTM的理解推荐[一篇文章](https://www.jianshu.com/p/4b4701beba92),这里就不展开去讲述。下面我们来看一下keras怎么去实现LSTM,非常简单就是使用LSTM层:
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)
show_acc_results(history)
三、循环神经网络的高级用法
为了提高循环神经网络的性能和泛化能力,我们将依托耶拿气温预测问题来实践3种提高循环神经网络的技巧:
- 循环dropout: 使用dropout来降低过拟合
- 堆叠循环层: 提高循环神经网络的表示能力
- 双向循环层: 将相同的信息以不同的方式呈现给循环网络,可以提高精度并缓解遗忘问题。
1.问题描述和数据准备
数据集是一个天气事件序列数据集,它是德国耶拿地区的气象数据。在这个数据集中,每10分钟记录14个不同的量,比如气温、气压、湿度、风向等。我们将使用这个数据集构造模型,输入最近的一些数据,可以预测24小时之后的气温。
数据集下载地址:https://s3.amazonaws.com/keras-datasets/jena_climate_2009_2016.csv.zip
首先我们解析数据,将各项指标的浮点数数据存放在张量中:
def get_temperature_data():
"""
对原始数据处理
:return: 返回标准化后的浮点数据,和标准差
"""
data_dir = 'E:/git_code/data/jena_climate'
fname = os.path.join(data_dir, 'jena_climate_2009_2016.csv')
f = open(fname)
data = f.read()
f.close()
lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
print(header)
print(len(lines))
float_data = np.zeros((len(lines), len(header) - 1))
for index, line in enumerate(lines):
values = [float(x) for x in line.split(',')[1:]]
float_data[index, :] = values
mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std
return float_data, std
绘制温度曲线:
def show_sequence(float_data):
"""
展示序列图像
:param float_data: 数据序列
:return: 返回浮点数据
"""
temp = float_data[:, 1]
plt.plot(range(len(temp)), temp)
plt.show()
plt.plot(range(1440), temp[:1440])
plt.show()
解决这个问题一些数据需要制定下来:
float_data, std = get_temperature_data() # 获取处理后的数据和标准差
LOOK_BACK = 1440 # 输入数据应该包括过去的多少个时间步
STEP = 6 # 数据采样周期。 设置为6,每小时抽取一个数据点
DELAY = 144 # 目标应该在未来的多少个时间步
BATCH_SIZE = 128 # 每批数据的样本数
VAL_STEPS = (300000 - 200001 - LOOK_BACK) // BATCH_SIZE # 验证数据的批次数量
TEST_STEPS = (len(float_data) - 300001 - LOOK_BACK) // BATCH_SIZE # 测试数据的批次数量
构造数据生成器,来组织数据以便可以给神经网络使用:
def generator(data, lookback, delay, min_index, max_index, shuffle=False, batch_size=128, step=6):
"""
根据参数生成数据,用于进行训练
:param data: 浮点数据组成的原始数据
:param lookback: 输入数据应该包括过去的多少个时间步
:param delay: 目标应该在未来的多少个时间步
:param min_index: data中的索引,用于确定抽取数据的范围
:param max_index: data中的索引,用于确定抽取数据的范围
:param shuffle: 是否打乱样本
:param batch_size: 每批数据的样本数
:param step: 数据采样周期。 设置为6,每小时抽取一个数据点
:return:
"""
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
2.确定训练的比较基准
在开始神经网络的训练之前,我们先可以确定一些基准,更高级的神经网络起码要比这个基准要好。有时候这个基准也是很难被打败的。
这里我们假设24小时后的温度与现在相同,这样的假设预测的平均绝对误差(mae)可以作为基准:
def evaluate_naive_method():
"""
计算一个基准精度,始终预测24小时后的温度与现在的温度相同,以下代码就计算局方绝对误差(MAE)指标来评估这种方法。
:return:
"""
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为0.29, 绝对误差为0.29*std,即2.57摄氏度。
另一个基准是使用密集连接神经网络来处理:
def train_dense(float_data):
"""
密集连接网络预测问题
:param float_data: 标准化后温度数据
:return:
"""
model = Sequential()
model.add(layers.Flatten(input_shape=(LOOK_BACK // 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)
plt_loss(history)
使用密集连接层的损失要比前面一种基准大,可以看出基准并不容易超越。
第三种基准是循环网络基准,使用一层GRU层的循环神经网络。GRU层比LSTM更加简化计算代价更低。
def train_gru(float_data):
"""
使用GRU循环网络预测温度
:param float_data: 标准化后温度数据
:return:
"""
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)
plt_loss(history)
这一次的训练结果约为0.265,比基准好好多了,但训练很快出现了过拟合,仍然可以改进。
3.使用循环dropout来降低拟合
dropout即将某一层输入单元随机设置为0,目的是打破该层训练数据中的偶然相关性。dropout参数用于设置被dropout的单元的比例。
def train_gru_dropout(float_data):
"""
使用GRU循环网络并使用dropout来降低过拟合
:param float_data: 标准化后温度数据
:return:
"""
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)
plt_loss(history)
过拟合现象比之前要好的多,但是结果提升并不大。
4.循环层堆叠
增加了dropout模型不再拟合,但是性能高似乎遇到了瓶颈,我们可以尝试增加网络容量,增加循环层:
def train_stacking_gru(float_data):
"""
使用GRU循环网络 + dropout + 堆叠
:param float_data: 标准化后温度数据
:return:
"""
model = Sequential(float_data)
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)
plt_loss(history)
训练结果入下:
4.双向RNN
双向RNN在某些任务上比普通的RNN要好。他会使用输入的正向和反向都进行训练:
def train_bidirectional_gru(float_data):
"""
使用双向GRU来训练网络
:return:
"""
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)
plt_loss(history)
训练结果入下,这个结果不是很好,比基准还要差一些: