Keras深度学习实践7——循环卷积神经网络

内容参考以及代码整理自“深度学习四大名“著之一《Python深度学习》
查看完整代码,请看: https://github.com/ubwshook/MachineLearning

密集连接网络和卷积神经网络都有一个主要特点,那就是它们都没有记忆。它们单独处理每个输入,在输入与输入之间没有保存任何状态,对于这样的网络,要想处理数据点的序列或时间序列,你需要向网络同时展示整个序列,即将序列转换成单个数据点。

与此相反,当你在阅读的时候,你是一个词一个词地阅读,同时会记住之前的内容。这让你能够动态理解这个句子所传达的含义。生物智能以渐进的方式处理信息,同时保存一个关于所处理内容的内部模型,这个模型是根据过去的信息构建的,并随着新信息的进去而不断更新。

循环神经网络的原理与此相同: 它处理序列的方式,遍历所有序列元素,并保存一个状态(state), 起重工包含与已查看内容相关的信息。实际上,RNN是一类具有内部环的神经网络。在处理两个不同的独立序列之间,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)
简单RNN模型的精度

简单RNN模型的损失

训练结果看看起来不是很理想,有两个原因:1.输入只考虑了500个单词; 2.SimpleRNN不擅长处理长序列,比如文本。所以我们要引入更高级的循环层

二、LSTM和GRU

Keras中还有两个循环层,LSTM和GRU。SimpleRNN的最大问题是,不能学习长期依赖,存在梯度消失问题。随着层数的增加,网络最终无法训练。

LSTM层是SimpleRNN层的一种变体,它增加了一种携带信息跨越多个时间步的方法。假设有一条传送带,其运行方向平行于你所处理的序列。序列中的信息可以在任意位置跳上传送带,然后被传送到更晚的时间步,并在需要的时候原封不动的跳回来。它保存信息以便后面使用,从而防止较早期的信号在处理中逐渐消失。

LSTM原理图

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)
GRU训练损失

这一次的训练结果约为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)
GRU结合dropout训练损失

过拟合现象比之前要好的多,但是结果提升并不大。

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)

训练结果入下:


stack_gru.png

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)

训练结果入下,这个结果不是很好,比基准还要差一些:


gru_bi.png

你可能感兴趣的:(Keras深度学习实践7——循环卷积神经网络)