文章目录
- 1 处理文本数据
- 1.1 单词和字符的one-hot编码
- 1.2 使用词嵌入
- 1.3 从原始文本到词嵌入
- 2 循环神经网络
- 2.1 Keras中的循环层
- 2.2 LSTM层和GRU层
- 2.3 实例:使用 LSTM 进行 IMDB 电影评论分类
- 3 循环神经网络的高级用法
- 3.1 温度预测问题
- 3.2 准备数据
- 3.3 基于常识、非机器学习的基准方法
- 3.4 一种基本的机器学习方法
- 3.5 第一个循环网络基准
- 3.6 使用循环dropout 来降低过拟合
- 3.7 循环层堆叠
- 3.8 使用双向 RNN
- 3.9 更多尝试
- 3.10 小结
- 4 一维卷积神经网络
- 4.1 理解序列数据的一维卷积
- 4.2 序列数据的一维池化
- 4.3 实现一维卷积神经网络
- 4.4 结合 CNN 和 RNN 处理长序列
- 5 本章小结
1 处理文本数据
1. 文本
文本是常用的序列数据之一,可以理解为字符序列和单词序列。
2.文本向量化,指将文本转换为数值张量的过程。
- 将文本分割为单词,并将每个单词转换为一个向量;
- 将文本分割为字符,并将每个单词转换为一个向量;
- 提取单词或字符的 n-gram ,并将每个 n-gram 转换为一个向量。
将文本分解成的单元(单词、字符或n-gram)叫做 「标记」,将文本分解成标记的过程叫做「分词」。
所有文本向量化的过程都是应用某种分词方案,然后将数值向量与标记相关联。这些向量组成序列张量,输入到神经网络模型中。
n-gram 是从句子中提取的 N 个(或更少)连续单词或字符的集合。 这类集合叫做词袋,「袋」指的是标记组成的集合,因此「词袋」是一种不保存顺序的粉刺方法,常常被用于浅层的语言处理模型。
3. 将向量与标记相关联的方法
- 对标记做 one-hot编码
- 标记嵌入
1.1 单词和字符的one-hot编码
1. 方法
将每个单词与唯一的整数索引进行关联,然后将这个整数索引 i 转换为长度为 N 的二进制向量(N是词表大小),这个向量只有第 i 个元素是 1 ,其余元素都是 0。
2.单词级的 one-hot 编码
import numpy as np
samples = ['The cat sat on the mat .', 'The dog ate my homework .']
token_index = {} # 构建数据中所有标记的索引
for sample in samples:
# 利用split方法对样本进行分词
for word in sample.split():
if word not in token_index:
# 为每个单词指定唯一索引
# 不存在索引编号为0的单词
token_index[word] = len(token_index) + 1
# 对样本进行分词,只考虑前max_length个单词
max_length = 10
# 结果保存在results中
results = np.zeros(shape=(len(samples), max_length, max(token_index.values()) + 1))
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = token_index.get(word)
results[i, j, index] = 1.
3. 字符级的 one-hot 编码
import string
samples = ['The cat sat on the mat .', 'The dog ate my homework .']
characters = string.printable # 所有可打印的ASCII字符
token_index = dict(zip(range(1, len(characters) + 1), characters))
max_length = 50
results = np.zeros((len(samples), max_length, max(token_index.keys())))
for i, sample in enumerate(samples):
for j, character in enumerate(sample):
index = token_index.get(character)
results[i, j, index] = 1.
4. 用 Keras 实现单词级的 one-hot 编码
Keras 的内置函数可以对原始文本数据进行「单词级」或「字符级」的 one-hot 编码。
from keras.preprocessing.text import Tokenizer
samples = ['The cat sat on the mat .', 'The dog ate my homework .']
# 创建一个分词器,只考虑前1000个最常见的单词
tokenizer = Tokenizer(num_words=1000)
# 构建单词索引
tokenizer.fit_on_texts(samples)
# 将字符串转换为整数索引组成的列表
sequences = tokenizer.texts_to_sequences(samples)
# 也可以直接得到 one-hot 二进制表示
# 该分词器也支持除one-hot外的其他向量化模式
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
# 找回单词索引
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))
# Found 9 unique tokens.
5. 使用散列技巧的单词级的 one-hot 编码
- 方法
用 散列函数 将单词散列编码为固定长度的向量。 - 优点
避免了维护一个显式的单词索引,从而节省内存,并允许数据的在线编码,适用于唯一标记数量太大的情况。 - 缺点
可能会出现 散列冲突 ,两个不同的单词具有相同的散列值。
samples = ['The cat sat on the mat .', 'The dog ate my homework .']
# 将单词保存为长度为 1000 的向量
# 如果单词数量接近 1000 ,会发生很多散列冲突
dimensionality = 1000
max_length = 10
results = np.zeros((len(samples), max_length, dimensionality))
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
# 将单词散列为 0~1000 范围内的一个随机整数索引
index = abs(hash(word)) % dimensionality
results[i, j, index] = 1
1.2 使用词嵌入
1. 词嵌入介绍
- one-hot 编码得到的向量是二进制的、稀疏的、维度很高的;
- 词嵌入 是低维的浮点数向量(密集矩阵),从数据中学习得到;
2. 使用词嵌入的方法
- 在完成主任务的同时学习词嵌入。一开始是随机的词向量,然后通过对词向量进行学习,与学习神经网络的权重类似。
- 预计算词嵌入 ,然后将其加载到模型中。
3. 利用 Embedding 层学习词嵌入
(1)原理
词向量之间的几何关系应该表示这些词之间的「语义关系」。
一般来说,任意两个词向量之间的几何距离,应该和它们的语义距离 有关,比如同义词应该被嵌入相似的词向量中。
因此,合理的做法是对每个新任务都学习一个新的嵌入空间 。反向传播 让这种学习变得简单,我们需要学习 Embedding层 的权重。
(2)步骤
①将一个 Embedding层 实例化
from keras.layers import Embedding
embedding_layer = Embedding(1000, 64)
「Embedding层」需要两个参数:标记的个数 (1000)和 嵌入的维度 (64)。
将 Embedding层 理解成一个字典,将整数索引(表示特定单词)映射为密集向量。它接受整数作为输入,并在内部字典中查找这些整数,返回相关联的向量。
Embedding层 的输入是一个二维整数张量,其形状为(samples,sequence_length),每个元素都是一个整数序列。它能够嵌入长度可变的序列,不过一批数据中的所有序列必须具有相同的长度,所以较短的序列用 0 填充,较长的序列应该被截断。
Embedding层 返回一个形状为(samples,sequence_length,embedding_dimensionly)的三维浮点数张量。
将一个 Embedding层 实例化的时候,它的权重最开始是随机的。在训练过程中,利用 反向传播 来逐渐调节。
②加载 IMDB数据,准备用于 Embedding层
将 Embedding层 应用于 IMDB电影评论情感预测任务。首先准备数据,将电影评论限制为前 10000 个最常见的单词,然后将评论限制为 20 个单词。对于 10000 个单词,网络将对每个词都学习一个 8 维嵌入,将输入的整数序列(二维整数张量)转换为嵌入序列 (三维浮点数张量),然后将张量展平为二维,在上面训练一个 Dense层 用于分类。
from keras.datasets import imdb
from tensorflow.keras import preprocessing
max_features = 10000 # 作为特征的单词个数
# 在maxlen之后截断文本
maxlen = 20
# 将数据加载为整数列表
(x_train, y_train), (x_test, y_test) = imdb.load_data(
num_words = max_features)
# 将整数列表转换为形状为(samples,maxlen)的二维整数张量
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)
③在IMDB数据上使用 Embedding层 和分类器
from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding
model = Sequential()
# 指定Embedding层的最大输入长度,以便之后将嵌入输入展平
# Embedding层激活的形状为(samples,maxlen,8)
model.add(Embedding(10000, 8, input_length=maxlen))
# 将三维的嵌入张量展平为形如(samples,maxlen * 8)的二维张量
model.add(Flatten())
# 添加分类器
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc'])
model.summary()
history = model.fit(x_train, y_train,
epochs=10,
batch_size=32,
validation_split=0.2)
得到的验证精度约为 76% ,考虑到仅查看每条评论的前 20 个单词,这个结果还是相当不错的。
但是需要注意的是,仅仅将嵌入序列展开并在上面训练一个 Dense层,会导致模型对于输入序列中的每个单词单独处理,没有考虑单词之间的关系和句子结构。
4. 使用预训练的词嵌入
当可用的训练数据很少的时候,可以从预计算的嵌入空间加载嵌入向量,通过词频统计计算得到,具有通用的特征。
比如 Word2vec算法 和 GloVe(词表示全局向量)。
1.3 从原始文本到词嵌入
1. 下载 IMDB 数据的原始样本,并处理 IMDB 原始数据的标签
首先,下载原始 IMDB 数据集并解压,下载地址:http://mng.bz/0tIo
接下来,将训练评论转换为 字符串列表,每个字符串对应一条评论,也可以将评论标签(正面/负面)转换成 labels 列表。
import os
imdb_dir = r'E:\firefoxLoad\aclImdb\aclImdb'
train_dir = os.path.join(imdb_dir, 'train')
labels = []
texts = []
for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)
for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), errors='ignore')
texts.append(f.read())
f.close()
if label_type == 'neg':
labels.append(0)
else:
labels.append(1)
2. 对数据进行分词
对文本进行分词,并将其划分为训练集和验证集。因此「预训练的词嵌入」对训练数据很少的问题特别有用,因此将训练数据限定为前 200 个样本。
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np
maxlen = 100 # 在100个单词后截断评论
training_samples = 200 # 在200个样本上进行训练
validation_samples = 10000 # 在10000个样本上验证
max_words = 10000 # 只考虑前10000个常见单词
tokenizer = Tokenizer(num_words = max_words)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)
word_index = tokenizer.word_index
print('Found %s unique tokens. ' % len(word_index))
data = pad_sequences(sequences, maxlen=maxlen)
labels = np.asarray(labels)
print('Shape of data tensor:', data.shape)
print('Shape of label tensor:', labels.shape)
# 将数据划分为训练集和验证集
# 首先打乱数据,因为原本样本是有序的
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]
x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples : training_samples + validation_samples]
y_val = labels[training_samples : training_samples + validation_samples]
3. 下载 GloVe 词嵌入
下载地址:https://nlp.stanford.edu/projects/glove/
选择 2014 年英文维基百科的预计算嵌入,这是一个 822 MB的压缩文件,文件名是 glove.6B.zip,里面包含 400 000个单词(或非单词的标记)
4. 对嵌入进行预处理
①对解压后的文件进行解析,构建一个将单词(字符串)映射为其向量表示(数值向量)的索引。
glove_dir = r'E:\firefoxLoad\glove.6B'
embedding_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'), errors='ignore')
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embedding_index[word] = coefs
f.close()
print('Found %s word vectors. ' % len(embedding_index))
# Found 399913 word vectors.
②构建一个可以加载到Embedding层的 嵌入矩阵,形状为(max_words,embedding_dim)。对于单词索引(分词时构建)中索引为 i 的单词,这个矩阵的元素 i 就是这个单词对应的 embedding_dim 维向量。注意,索引 0 只是一个占位符,不代表任何单词或标记。
embedding_dim = 100
embedding_matrix = np.zeros((max_words, embedding_dim))
for word, i in word_index.items():
if i < max_words:
embedding_vector = embedding_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
# 嵌入索引找不到的词,其嵌入向量全为0
5. 定义模型1
from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding (Embedding) (None, 100, 100) 1000000
_________________________________________________________________
flatten (Flatten) (None, 10000) 0
_________________________________________________________________
dense (Dense) (None, 32) 320032
_________________________________________________________________
dense_1 (Dense) (None, 1) 33
=================================================================
Total params: 1,320,065
Trainable params: 1,320,065
Non-trainable params: 0
_________________________________________________________________
6. 在模型中加载GloVe嵌入
Embedding层 只有一个权重矩阵,是一个二维的浮点数矩阵,其中每个元素 i 是与 索引 i 相关联的词向量。
此外,需要冻结 Embedding层(将trainable属性设置为False)。
因为如果一个模型的一部分是经过训练的,而另外一部分是随机初始化的,那么在训练期间就不需要更新预训练的部分,避免所保存的信息损失。
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False
7.训练模型与评估模型
(1)编译并训练模型
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(x_train, y_train,
epochs=10,
batch_size=32,
validation_data=(x_val, y_val))
model.save_weights('pre_trained_glove_model.h5')
(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()
8. 模型2:在不使用预训练词嵌入的情况下,训练相同模型
在这种情况下,将会学到针对任务的输入标记的嵌入。如果有大量的可用数据,这种方法通常更加强大。
(1)编译并训练模型
from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(x_train, y_train,
epochs=10,
batch_size=32,
validation_data=(x_val, y_val))
(2)绘制模型随时间的变化情况
9. 在测试数据上评估模型
(1)对测试集数据进行分词
test_dir = os.path.join(imdb_dir, 'test')
labels = []
texts = []
for label_type in ['neg', 'pos']:
dir_name = os.path.join(test_dir, label_type)
for fname in sorted(os.listdir(dir_name)):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), errors='ignore')
texts.append(f.read())
f.close()
if label_type == 'neg':
labels.append(0)
else:
labels.append(1)
sequences = tokenizer.texts_to_sequences(texts)
x_test = pad_sequences(sequences, maxlen=maxlen)
y_test = np.asarray(labels)
(2)加载并评估第一个模型
测试精度达到了50% 。
model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test, y_test)
# [0.8677374720573425, 0.5004799962043762]
2 循环神经网络
1. 密集连接网络和卷积神经网络都 没有记忆 ,单独处理每个输出,在输入与输入之间没有保存任何状态。对于这样的网络,要想处理数据点的序列或时间序列,需要将序列转换成单个数据点,然后一次性处理。这种网络叫做 「前馈网络」。
2. 循环神经网络(RNN,recurrent neutral network)
遍历所有序列元素,并保存一个状态,其中包含已查看内容相关的信息。该模型根据过去信息构建,随着新信息的进入而不断更新。
3. RNN的前向传递
RNN 的输入是一个张量序列,形状为(timesteps,input_features)。
RNN 对时间步(timesteps)进行遍历,在每个时间步,考虑 t 时刻的当前状态与 t 时刻的输入 [ 形状为(input_features,)] ,对二者计算得到 t 时刻的输出。然后,我们将下一个时间步的状态设置为上一个时间步的输出。对于第一个时间步,由于上一个时间步的输出没有意义,所以将状态初始化为一个全零向量,叫做网络的初始状态。
# RNN伪代码
state_t = 0 # t时刻的状态
# 对序列元素进行遍历
for input_t in input_sequence:
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))
# 初始状态 全0向量
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, output_t) + b)
successive_outputs.append(output_t)
# 更新网络的状态,用于下一个时间步
state_t = output_t
# 最终输出形为(timesteps, output_features)的二维张量
final_output_sequence = np.stack(successive_outputs, axis=0)
总之, RNN 是一个 for 循环,重复使用循环前一次迭代的计算结果。 RNN 的特征在于其时间步函数。
本例最终输出一个形为(timesteps,output_features)的二维张量,其中每个时间步是循环在 t 时刻的输出。输出张量中的每个时间步 t 包含输入序列中时间步 0~t 的信息,也就是关于全部过去的信息。因此,在多数情况下,只需要最后一个输出(循环结束时的 output_t ),它已经包含了整个序列的信息。
2.1 Keras中的循环层
1. SimpleRNN层
上面使用 Numpy 的简单实现,对应一个实际的 Keras 层,即 SimpleRNN层。
SimpleRNN 能够处理序列批量,接收形状为(batch_size,timesteps,input_features)的输入。
SimpleRNN 可以在两种不同模式下运行,一种是返回每个时间步连续输出的完整序列,即形状为(batch_size,timesteps,output_features)的三维向量;第二种是只返回每个输入序列的最终输出,即形状为(batch_size,output_features)的二维张量。
通过 return_sequences 构造函数参数来控制。
当 return_sequences = True 的时候,返回完整的状态序列。
为了提高网络的表示能力,可以将多个循环层逐个堆叠,在这种情况下,所有中间层都需要返回完整的输出序列,设置 return_sequences = True 。
2. 实例:IMDB电影评论分类
(1)准备IMDB数据并进行预处理
from keras.datasets import imdb
from keras.preprocessing import sequence
max_features = 10000 # 作为特征的单词个数
maxlen = 500 # 在maxlen之后截断文本
batch_size = 32
print('Loading data...')
(input_train, y_train), (input_test, y_test) = imdb.load_data(
num_words=max_features)
print(len(input_train), 'train sequence')
print(len(input_test), 'test sequence')
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)用一个Embedding层和一个SimpleRNN层来训练一个简单的循环网络
from keras.models import Sequential
from keras.layers import Embedding, Dense, SimpleRNN
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)
(3)绘制训练和验证的损失、精度
在第三章,处理这个数据集的第一个简单方法得到的测试精度是 88%,与这个基准相比,这个小型循环网络的验证精度只有 85%。
原因在于输入只考虑了前500个单词,没有考虑整个序列,获得的信息比基准模型还少;另外,SimpleRNN不擅长处理长序列。
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 accuracy')
plt.legend()
plt.show()
2.2 LSTM层和GRU层
1.SimpleRNN 的缺点:
不可能学到 长期依赖,因为它存在 梯度消失问题,随着层数的增加,网络变得无法训练。
因此提出了 LSTM层 和 GRU层 来解决这个问题。
2. LSTM层
Simple层的一种变体, 增加了一种 「携带信息跨越多个时间步」的方法。
原理:保存信息以便后面使用,防止较早期的信号在处理过程中逐渐消失。
(1)为了了解 LSTM, 从 SimpleRNN 单元开始讨论。
因为有多个权重矩阵,所以对单元中的 W 和 U 两个矩阵添加下标字母o (Wo 和 Uo),表示输出。
(2)向这张图像增加额外的数据流,其中携带着跨越时间步的信息。
它在不同的时间步的值叫做 Ct ,其中 C 表示 携带(carry)。它将与输入连接和循环连接进行运算,从而影响到下一个时间的状态。
(3)讨论携带数据流 Ct 下一个值的计算方法
涉及三个不同变换,它们都有各自的权重矩阵,分别用 i 、 j 和 k 作为下标。
伪代码如下:
output_t = activation(dot(state_t, Uo) + dot(input_t, Wo) + dot(C_t, Vo) + b)
i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi)
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf)
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)
(4)组合三个不同变换,得到新的携带状态
c_t+1 = i_t * k_t + f_t * c_t
2.3 实例:使用 LSTM 进行 IMDB 电影评论分类
from keras.layers import 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)
从训练和验证的损失和精度可以看出, LSTM模型的验证精度已经达到了 89%,并且使用了小数据量。
可以通过调节超参数「嵌入维度、LSTM输出维度 」,正则化 来改进这个模型。
3 循环神经网络的高级用法
3.1 温度预测问题
1. 天气时间序列数据集
由德国耶拿的 马克思·普朗克生物地球化学研究所的气象站记录。
在这个数据集中,每 10 分钟记录 14 个不同的量(比如气温、气压、湿度、风向等),其中包含多年的记录,原始数据可追溯到 2003 年,但本例仅使用 2009 - 2016 年的数据。
(1)观察耶拿天气数据集的数据
从输出可以看出,共有420 551 行数据(每行是一个时间步,记录了一个日期和14个与天气有关的值),输出了下列表头。
import os
data_dir = r'E:\firefoxLoad\jena_climate_2009_2016.csv'
fname = os.path.join(data_dir, 'jena_climate_2009_2016.csv')
f = open(fname, errors='ignore')
data = f.read()
f.close()
lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
print(header)
print(len(lines))
['"Date Time"', '"p (mbar)"', '"T (degC)"', '"Tpot (K)"', '"Tdew (degC)"',
'"rh (%)"', '"VPmax (mbar)"', '"VPact (mbar)"', '"VPdef (mbar)"',
'"sh (g/kg)"', '"H2OC (mmol/mol)"', '"rho (g/m**3)"', '"wv (m/s)"',
'"max. wv (m/s)"', '"wd (deg)"']
420551
(2)将420 551 行数据转换成 Numpy 数组
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:]] # 去掉特征"Date Time"
float_data[i, :] = values
(3)绘制温度时间序列,观察温度每年的周期性变化。
from matplotlib import pyplot as plt
temp = float_data[:, 1] # 温度 "T (degC)"
plt.plot(range(len(temp)), temp)
(4)绘制前 10 天的温度时间序列,因为每 10 分钟记录一个数据,所以每天有 144 个数据点。
从图中可以看到每天的周期性变化,尤其是最后 4 天特别明显。
plt.plot(range(1440), temp[:1440])
3.2 准备数据
问题: 一个时间步是 10 分钟,每 steps 个时间步采样一次数据,给定过去 lookback 个时间步之内的数据,能否预测 delay 个时间步之后的温度?需要用到的参数如下:
- lookback = 720:给定过去 5 天内的观测数据。
- steps = 6:观测数据的采样频率是每小时一个数据点。
- delay = 144 : 目标是未来 24 小时之后的数据。
1. 数据预处理
由于数据已经是数值型的,所以不需要做向量化。
但是数据中的每个时间序列位于不同的范围,所以需要对每个时间序列分别做标准化,让它们在相似的范围内取较小的值。
方法:让每个时间序列减去平均值,再除以标准差
我们使用前 200 000 个时间步作为训练数据,所以只对这部分数据计算平均值和标准差。
mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std
2. 生成时间序列样本及其目标的生成器
该生成器以当前的浮点数数组作为输入,并从最近的数据中生成数据批量,同时生成未来的目标温度。因为数据集中的样本是高度冗余的(对于第 N 个样本和第 N+1 个样本,大部分时间步都相同),所以我们将使用原始数据即时生成样本。
生成器生成了一个元组(samples,targets),其中 samples 是输入数据的一个批量,targets 是对应的目标温度数组。生成器参数如下:
- data:浮点数数据组成的原始数组,需要进行标准化处理;
- lookback:输入数据应该包括过去多少个时间步;
- delay:目标应该在未来多少个时间步之后;
- min_index 和 max_index :data 数组中的索引,用于界定需要抽取哪些时间步。有助于保存一部分数据用于验证,另一部分用于测试。
- shuffle:打乱样本,或者按顺序抽取样本;
- batch_size:每个批量的样本数;
- 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]))
# lookback // step:总的采样数目
# data.shape[-1]:features_num
targets = np.zeros((len(rows),))
# 将数据存入样本和目标
for j, row in enumerate(rows):
# 取出索引
indices = range(rows[j] - lookback, rows[j], step)
samples[j] = data[indices]
# targets即当前时间的delay个时间步的温度
targets[j] = data[rows[j] + delay][1]
yield samples, targets
3. 实例化三个生成器
每个生成器分别读取原始数据的不同时间段:训练生成器读取前 200 000 个时间步,验证生成器读取随后的 100 000 个时间步,测试生成器读取剩下的时间步。
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)
validation_gen = generator(float_data,
lookback=lookback,
delay=delay,
min_index=200001,
max_index=300000,
shuffle=True,
step=step,
batch_size=batch_size)
test_gen = generator(float_data,
lookback=lookback,
delay=delay,
min_index=300001,
max_index=None,
shuffle=True,
step=step,
batch_size=batch_size)
# 为了查看整个验证集,需要从val_steps中抽取多少次
val_steps = (300000 - 200001 - lookback) // batch_size
# 为了查看整个测试集,需要从test_steps中抽取多少次
test_steps = (len(float_data) - 300001 - lookback) // batch_size
3.3 基于常识、非机器学习的基准方法
1. 为什么要建立基于常识的基准方法?
基于常识的基准方法,可以作为合理性检查,还可以建立一个基准,更高级的机器学习模型需要打败这个基准才能表现出其有效性。
2. 本例的基准方法
假设:「温度时间序列是连续的,并且具有每天的周期性变化」。因此,一种基于常识的方法就是 「始终预测 24 小时后的温度等于现在的温度」,使用 平均绝对误差(MAE) 指标来评估。
3. 计算符合常识的基准方法的 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))
evaluate_naive_method()
# 0.28989001791386343
得到的 MAE 为 0.29。因为温度数据被标准化成 均值为 0、标准差为 1,所以无法直接对这个值进行解释,它转化成温度的平均绝对误差为 0.29 * temperature_std 摄氏温度,即 2.57 °C 。
这个平均误差比较大,接下来使用深度学习知识来改进结果。
3.4 一种基本的机器学习方法
1. 为什么要先使用基本的机器学习方法?
在开始研究复杂且计算代价很高的模型(RNN)之前,尝试使用简单且计算代价低的机器学习模型很有用,保证进一步增加问题的复杂度是合理的。
2. 训练并评估一个密集连接模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
# float_data.shape[-1]:feature_nums
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
)
3. 绘制验证和训练的损失曲线
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()
从图中可以看出,部分验证损失接近不包含学习的基准方法,但这个结果并不可靠。同时,这也展示了基准方法很难超越,因为常识中包含大量有价值的信息。
3.5 第一个循环网络基准
虽然第一个 全连接方法 的效果并不好,但这并不意味着机器学习方法不适用。
全连接方法 首先将时间序列展平,从输入数据中删除了 时间 的概念,然而「数据序列的因果关系和顺序」都很重要。
1. 训练并评估基于 GRU 的模型
GRU层 (门控循环单元),工作原理与 LSTM 相同,但是做了一些简化,因此计算代价更低,当然表示能力也不如 LSTM 。
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)
2. 绘制验证和训练的损失曲线
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()
从图中可以看出,在开始显著过拟合之前,MAE 约为 0.265,反标准化转换成温度的平均绝对误差为 2.35 °C ,与最初的 2.57 °C 相比,有所提高,但仍有改进的空间。
3.6 使用循环dropout 来降低过拟合
从上面的例子可以发现,模型出现了 过拟合,验证损失和训练损失明显偏离。
1. dropout
(1)方法
将某一层的输入单元随机设置为 0 ,目的是打破该层训练数据中的偶然相关性。
(2)在「循环网络」中使用 dropout 的正确方法
- 对每个时间步使用 相同 的 dropout 掩码(dropout mask,相同模式的舍弃单元),让网络沿着时间正确传播学习误差;
- 将相同的 dropout 掩码应用于层的 内部循环激活 (叫做循环 dropout 掩码),能够对 GRU、LSTM 等循环层得到的表示做正则化。
(3)嵌入 Keras 循环层
Keras 的每个循环层都有两个与 dropout 相关的参数,一个是 dropout ,它是一个浮点数,指定该层输入单元的 dropout 比率;另一个是 recurrent_dropout ,指定循环单元的 dropout 比率。
2. 训练并评估一个使用 dropout正则化 的基于 GRU 的模型
向 GRU 层中添加 dropout 和循环 dropout ,观察是否能减弱 过拟合 的影响。
因为使用 dropout 正则化的网络需要更长的时间才能完全收敛,所以网络训练轮次增加为原来的 2 倍。
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)
观察训练和验证损失,发现前 30 个轮次不再发生过拟合,并且,评估分数更稳定,但是最佳分数并没有比之前低很多。
3.7 循环层堆叠
解决完过拟合问题,应该考虑增加网络容量,通常做法是 增加每层单元数 或 增加层数 ,「循环层堆叠」是一个很好的例子。
在 Keras 中逐个堆叠循环层,所有中间层都应该返回完整的输出序列(1个 3D 张量),而不是只返回最后一个时间步输出,通过指定 return_sequences = True 来实现。
1. 训练并评估一个使用 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)
从图中可以看出,添加一层 对结果的改进并不显著。因此可以得出两个结论:
- 因为过拟合不是很严重,所以可以放心地增大每层的大小,进一步改进验证损失,但是计算成本很高。
- 添加一层 后模型并没有显著改进, 提高网络能力的回报在逐渐减小。
3.8 使用双向 RNN
1. 双向 RNN 介绍
双向 RNN 利用了 RNN 的顺序敏感性:包含两个普通的 RNN ,每个 RNN 分别沿一个方向对输入序列进行处理(时间顺序和时间逆序),然后将它们的表示合并到一起,得到更加丰富的表示,并捕捉到仅使用正序 RNN 时可能忽略的一些模式。
2. 训练并评估一个双向 LSTM
在 Keras 中将一个双向 RNN 实例化,使用 Bidirectional层 ,第一个参数是一个循环层实例。
Bidirectional 对这个循环层创建了第二个单独实例,然后使用一个实例按正序处理输入序列, 另一个实例按照逆序处理输入序列。
在 IMDB 情感分析上来试一下这种方法。
结论: 这个模型的表现比上一节的普通 LSTM 略好,验证精度超过 89% ,但是它也很快开始过拟合,因为双向层的参数个数是正序 LSTM 的 2 倍。
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',
metics=['acc'])
history = model.fit(x_train, y_train,
epochs,
batch_size=128,
validation_split=0.2)
3. 训练一个双向 GRU
将双向 GRU 模型应用于温度预测任务。
结果: 这个模型的表现和普通 GRU层 差不多,因为所有的预测能力肯定来自正序的那一半网络。
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.Bidirectional(
layer.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)
3.9 更多尝试
为了提高温度预测问题的性能:
- 在堆叠循环层调节每层的单元个数;
- 调节 RMSprop 优化器的学习率;
- 使用 LSTM层 替代 GRU层。
- 在循环层上使用更大的密集连接回归器 ,用更大的Dense层,或者使用Dense层的堆叠;
- 在测试集上运行性能最佳的模型(即验证MAE最小的模型),否则,开发的网络将会对验证集过拟合。
3.10 小结
- 遇到新问题,为选择的指标建立一个基于常识的基准 ,作为新模型的判断标准;
- 在尝试代价较高的模型之前,先尝试简单的模型,证明增加计算代价是有意义的;
- 循环网络很适合处理具有时间顺序的数据;
- 在循环网络中使用 dropout ,应该使用一个不随时间变化的 dropout 掩码与循环 dropout 掩码 ,二者内置于 Keras 的循环层,只需要使用循环层的 dropout 和 recurrent_dropout 参数;
- 堆叠 RNN 的表示能力更强大,但是计算代价也更高;
- 双向 RNN 从两个方向查看一个序列,适用于自然语言处理问题。
4 一维卷积神经网络
4.1 理解序列数据的一维卷积
一维卷积层可以识别序列中的局部模式,具有平移不变性,这是因为对每个序列段执行相同的输入变换,所以在句子中某个位置学到的模式稍后可以在其他位置被识别。
4.2 序列数据的一维池化
从输入中提取一维序列段(子序列),然后输出其最大值(最大池化)或平均值(平均池化),能够降低一维输入的长度(子采样)。
4.3 实现一维卷积神经网络
1. Keras 中的一维卷积神经网络是 Conv1D层 ,接收输入的形状为 (samples,time,features) 的三维张量,并返回类似形状的三维张量。
卷积窗口是时间轴上的一维窗口,时间轴是输入张量的第二个轴。
接下来构建一个简单的两层一维卷积神经网络,并将其应用于 IMDB 情感分类任务。
2. 准备 IMDB 数据
from keras.datasets import imdb
from keras.preprocessing import sequence
max_features = 10000
max_len = 500
print('Loading data...')
(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')
print('Pad sequences (sample x time)')
x_train = sequence.pad_sequences(x_train, maxlen=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
3. 在 IMDB 数据上训练并评估一个简单的一维卷积神经网络
一维卷积神经网络是 Conv1D层 和 MaxPooling1D层 的堆叠,最后是一个 全局池化层 或 Flatten层 ,将三维输出转换为二维输出,这样就可以向模型添加一个或多个 Dense层 ,用于回归或分类。
不过,一维卷积神经网络可以使用更大的卷积窗口。
对于二维卷积层,3 * 3 的卷积窗口包含 3 * 3 = 9 个特征向量;对于一维卷积层,大小为 3 的卷积窗口只包含 3 个卷积向量。因此,可以使用大小等于 7 或 9 的一维卷积窗口。
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.Embedding(max_features, 128, input_length=max_len))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()
model.compile(optimizer=RMSprop(lr=1e-4),
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(x_train, y_train,
epochs=10,
batch_size=128,
validation_split=0.2)
从图中可以看出,验证精度略低于 LSTM,但是在 CPU 和 GPU 上的运行速度更快 。
4.4 结合 CNN 和 RNN 处理长序列
1. 一维卷积神经网络的缺点
对时间步的顺序不敏感,因为一维卷积神经网络分别处理每个输入序列段。
2.结合 CNN 和 RNN 的方法
在 RNN 前面使用一维卷积神经网络作为预处理步骤,将长的输入序列转换为高级特征组成的短序列(下采样),提取的特征组成的序列成为网络中 RNN 的输入。
3. 将上述方法应用于温度预测数据集
为了得到更长的序列,可以查看更早的数据(增大数据生成器的 lookback 参数)或查看分辨率更高的时间序列(减小生成器的step参数)。
在这里将 step 减半,得到时间序列的长度变为之前的两倍,温度数据的采样频率变为每 30 分钟一个数据点。
(1) 为耶拿数据集准备更高分辨率的数据生成器
step = 3 # 之前为6
lookback = 720
delay = 144
train_gen = generator(float_data,
lookback=lookback,
delay=delay,
min_index=0,
max_index=200000,
shuffle=True,
step=step)
val_gen = generator(float_data,
lookback=lookback,
delay=delay,
min_index=200001,
max_index=300000,
step=step)
test_gen = generator(float_data,
lookback=lookback,
delay=delay,
min_index=300001,
max_index=None,
step=step)
val_steps = (300000 - 200001 - lookback) // 128
test_steps = (len(float_data) - 300001 - lookback) // 128
(2)结合一维卷积基和 GRU 的模型
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop
model = Sequential()
model.add(layers.Conv1D(32, 5, activation='relu',
input_shape=(None, float_data.shape[-1])))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GRU(32, dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))
model.summary()
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)
从验证损失来看,这种架构的效果不如只用正则化 GRU ,但是它的速度快很多,查看了两倍的数据量。
5 本章小结
- 学到了以下技术,可以广泛应用于序列数据(从文本到时间序列)组成的数据集。
- 如何对文本分词;
- 什么是词嵌入,如何使用词嵌入;
- 是什么循环网络,如何使用循环网络;
- 如果堆叠 RNN 层和使用双向 RNN ;
- 如何使用一维卷积神经网络来处理序列;
- 如何结合一维卷积神经网络和RNN来处理长序列。