粗读《Python 深度学习》(5)

第六章 深度学习用于文本和序列

本章将介绍使用深度学习模型处理文本(可以将其理解为单词序列或字符序列)、时间序列一般的序列数据。用于处理序列的两种基本的深度学习算法分别是 循环神经网络(recurrent neural network)一维卷积神经网络(1D convnet)

这些算法的应用包括:
1、文档分类和时间序列分类,比如识别文章的主题或书的作者;
2、时间序列对比,比如估测两个文档或两支股票行情的相关程度;
3、序列到序列的学习,比如将英语翻译成法语;
4、情感分析,比如将推文或电影评论的情感划分为正面或负面;
5、时间序列预测,比如根据某地最近的天气数据来预测未来天气。

6.1 处理文本数据

文本是最常用的序列数据之一,可以理解为字符序列或单词序列,但最常见的是单词级处理。应注意,本章的这些深度学习模型都没有像人类一样真正地理解文本,而只是映射出书面语言的统计结构,但这足以解决许多简单的文本任务

与其他所有神经网络一样,深度学习模型不会接收原始文本作为输入,它只能处理数值张量。文本 向量化(vectorize) 是指将文本转换为数值张量的过程。

它有多种实现方法:
1、将文本分割为单词,并将每个单词转换为一个向量。
2、将文本分割为字符,并将每个字符转换为一个向量。
3、提取单词或字符的 n-gram,并将每个 n-gram 转换为一个向量。n-gram 是多个连续单词或字符的集合 (n-gram 之间可重叠)

将文本分解而成的单元(单词、字符或 n-gram)叫作 标记(token),将文本分解成标记的过程叫作 分词(tokenization)

三元语法(3-gram) 为例,将句子 “The cat sat on the mat.” 分解为:

{“The”, “The cat”, “cat”, “cat sat”, “The cat sat”,
“sat”, “sat on”, “on”, “cat sat on”, “on the”, “the”,
“sat on the”, “the mat”, “mat”, “on the mat”}

这样的集合分别叫作 三元语法袋(bag-of-3-grams)词袋 是一种不保存顺序的分词方法(生成的标记组成一个集合,而不是一个序列,舍弃了句子的总体结构)。因此它往往被用于浅层的语言处理模型,而不是深度学习模型。提取 n-gram 是一种特征工程,在使用轻量级的浅层文本处理模型时(比如 logistic 回归和随机森林),n-gram 是一种功能强大、不可或缺的特征工程工具。

所有文本向量化过程都是应用某种分词方案,然后将数值向量与生成的标记相关联。本节将介绍两种主要方法:对标记做 one-hot 编码(one-hot encoding)标记嵌入[token embedding,通常只用于单词,叫作 词嵌入(word embedding)]。

6.1.1 词和字符的 one-hot 编码

1、单词级的 one-hot 编码(简单示例)

import numpy as np

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

""" 采集单词,建立索引 """
token_index = {}   # 构建数据中所有标记的索引
for sample in samples:
    for word in sample.split():   # 返回单词列表
        if word not in token_index:
            token_index[word] = len(token_index) + 1    # 单词作key,索引作value

max_length = 10   # 读取每个样本的前10个词

results = np.zeros(shape=(len(samples),
                          max_length,
                          max(token_index.values()) + 1))   # (samples,words,indexes)

""" 利用索引进行one-hot编码 """
for i, sample in enumerate(samples):
    for j, word in list(enumerate(sample.split()))[:max_length]:   
    # 遍历分词产生的单词列表,生成由(序号,单词)元组组成的列表,切片,在用for循环遍历
        index = token_index.get(word)   # 查找单词对应的索引
        results[i, j, index] = 1.       # one-hot编码

2、字符级的 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   # 读取每个样本的前50个字符
results = np.zeros((len(samples), max_length, max(token_index.keys()) + 1)) 
for i, sample in enumerate(samples):
    for j, character in enumerate(sample):
        index = token_index.get(character)   # 查找字符对应的索引
        results[i, j, index] = 1.            # one-hot编码

3、用 Keras 实现单词级的 one-hot 编码

from keras.preprocessing.text import Tokenizer

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

tokenizer = Tokenizer(num_words=1000)   # 创建分词器实例
tokenizer.fit_on_texts(samples) 

sequences = tokenizer.texts_to_sequences(samples)   # 将字符串转换为整数索引组成的列表

one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')   # 获得one-hot编码结果

word_index = tokenizer.word_index   # 获得单词索引
print('Found %s unique tokens.' % len(word_index))

分词器tokenizer的具体用法可以参考这篇博客

4、使用散列技巧的单词级的 one-hot 编码

one-hot 散列技巧(one-hot hashing trick)是 one-hot 编码的一种变体。这种方法没有为每个单词显式分配一个索引并将这些索引保存在一个字典中,而是将单词散列编码为固定长度的向量,通常用一个非常简单的散列函数来实现。它避免了维护一个显式的单词索引,从而节省内存并允许数据的在线编码,但可能会出现 散列冲突(hash collision),即两个不同的单词可能具有相同的散列值。

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

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]:
        index = abs(hash(word)) % dimensionality   # 将单词散列为0~1000范围内的一个随机整数索引
        results[i, j, index] = 1.

利用hash()获取散列。

6.1.2 使用词嵌入

one-hot 编码得到的向量是二进制的、稀疏的(绝大部分元素都是0)、高维的,而 词嵌入低维的浮点数向量(即密集向量)

获取词嵌入的方法有两种:
1、在完成主任务(比如文档分类或情感预测)的同时学习词嵌入。
2、在不同于待解决问题的机器学习任务上预计算好词嵌入,然后将其加载到模型中。这些词嵌入叫作 预训练词嵌入(pretrained word embedding)

一、利用 Embedding 层学习词嵌入

要将一个词与一个密集向量相关联,最简单的方法就是随机选择向量。这种方法的问题在于,得到的嵌入空间没有任何结构。深度神经网络很难对这种杂乱的、非结构化的嵌入空间进行学习

为了方便神经网络的学习,我们希望词向量之间的几何关系能表示这些词之间的语义关系。向量的方向和距离要与语义和语义距离相对应。
在这里插入图片描述

一个好的词嵌入空间在很大程度上取决于你的任务,因为某些语义关系的重要性因任务而异。

1、将一个 Embedding 层实例化

from keras.layers import Embedding

embedding_layer = Embedding(1000, 64)   # (标记的个数,嵌入的维度)

最好将 Embedding 层理解为一个字典,将整数索引(表示特定单词)映射为密集向量。它接收整数作为输入,并在内部字典中查找这些整数,然后返回相关联的向量。

Embedding 层的输入是一个二维整数张量,其形状为 (samples, sequence_length),每个元素是一个整数序列,返回一个形状为 (samples, sequence_length, embedding_dimensionality) 的三维浮点数张量。

将一个 Embedding 层实例化时,它的权重(即标记向量的内部字典)最开始是随机的,与其他层一样。在训练过程中,利用反向传播来逐渐调节这些词向量,改变空间结构以便下游模型可以利用。一旦训练完成,嵌入空间将会展示大量结构这种结构专门针对训练模型所要解决的问题

将这个想法应用于 IMDB 电影评论情感预测任务。

2、加载 IMDB 数据,准备用于 Embedding 层

from keras.datasets import imdb
from keras.layers import preprocessing 
max_features = 10000   # 作为特征的单词个数
maxlen = 20   # 最大长度

(x_train, y_train), (x_test, y_test) = imdb.load_data(
    num_words=max_features)   # 将数据加载为整数列表

x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)   # 将整数列表转换成形状为(samples,maxlen) 的二维整数张量
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)

3、在 IMDB 数据上使用 Embedding 层和分类器

from keras.models import Sequential 
from keras.layers import Flatten, Dense, Embedding

model = Sequential()
model.add(Embedding(10000, 8, input_length=maxlen))   # Embedding 层激活的形状为 (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)

仅仅将嵌入序列展开并在上面训练一个 Dense 层,会导致模型对输入序列中的每个单词单独处理,而没有考虑单词之间的关系和句子结构。更好的做法是在嵌入序列上添加循环层或一维卷积层,将每个序列作为整体来学习特征。

二、使用预训练的词嵌入

与预训练的卷积神经网络类似,也可以将预训练的词嵌入加载到 Embedding 层中使用。书中介绍了两种预训练的词嵌入:由 Google 的 Tomas Mikolov 于 2013 年开发的 word2vec 算法 和由斯坦福大学的研究人员于 2014 年开发的 GloVe(global vectors for word representation)

6.1.3 整合在一起:从原始文本到词嵌入

一、下载 IMDB 数据的原始文本

1、处理 IMDB 原始数据的标签

import os

imdb_dir = '/Users/fchollet/Downloads/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))
            texts.append(f.read())       # 将文件内容添加到列表中
            f.close()
            if label_type == 'neg':      # 制作标签列表
                labels.append(0)
            else:
                labels.append(1)

二、对数据进行分词

2、对 IMDB 原始数据的文本进行分词

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np

maxlen = 100                 # 一句评论最长100词
training_samples = 200       # 训练集样本数量
validation_samples = 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                 # 单词作key,序号作value的字典(单词不重复)
print('Found %s unique tokens.' % len(word_index))

data = pad_sequences(sequences, maxlen=maxlen)    # 统一长度,(samples, 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]

三、下载 GloVe 词嵌入

书中从 https://nlp.stanford.edu/projects/glove,下载 2014 年英文维基百科的预计算嵌入。这是一个 822 MB 的压缩文件,文件名是 glove.6B.zip,里面包含 400 000 个单词(或非单词的标记)的 100 维嵌入向量。

四、对嵌入进行预处理

对解压后的文件(一个 .txt 文件)进行解析,构建一个将单词(字符串)映射为其向量表示(数值向量)的索引。

3、解析 GloVe 词嵌入文件

glove_dir = '/Users/fchollet/Downloads/glove.6B'

embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'))
for line in f:
    values = line.split()
    word = values[0]    # 单词
    coefs = np.asarray(values[1:], dtype='float32')   # 向量
    embeddings_index[word] = coefs
f.close()

print('Found %s word vectors.' % len(embeddings_index))

接下来,需要构建一个可以加载到 Embedding 层中的嵌入矩阵。它必须是一个形状为 (max_words, embedding_dim) 的矩阵,对于单词索引(在分词时构建)中索引为 i 的单词,这个矩阵的元素 i 就是这个单词对应的 embedding_dim 维向量注意,索引 0 不应该代表任何单词或标记,它只是一个占位符。

4、准备 GloVe 词嵌入矩阵

embedding_dim = 100

embedding_matrix = np.zeros((max_words, embedding_dim))
for word, i in word_index.items():                     # 获取单词和对应的序号
    if i < max_words:
       embedding_vector = embeddings_index.get(word)   # 获取向量
       if embedding_vector is not None:
           embedding_matrix[i] = embedding_vector      # 嵌入矩阵中

五、定义模型

5、模型定义

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()

六、在模型中加载 GloVe 嵌入

Embedding 层只有一个权重矩阵,是一个二维的浮点数矩阵,其中每个元素 i 是与索引 i 相关联的词向量。

6、将预训练的词嵌入加载到 Embedding 层中

model.layers[0].set_weights([embedding_matrix])   # 加载预训练的词嵌入矩阵
model.layers[0].trainable = False    # 冻结该层

七、训练模型与评估模型

7、训练与评估

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')

8、绘制结果

在这里插入图片描述

由于训练样本很少,模型很快就开始过拟合,验证精度的波动也很大,但似乎达到了接近 60%。

9、在不使用预训练词嵌入的情况下,训练相同的模型

在这里插入图片描述

验证精度停留在 50% 多一点。因此,在本例中,预训练词嵌入的性能要优于与任务一起学习的嵌入。

最后,在测试数据上评估模型。

10、对测试集数据进行分词

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))
            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)

11、在测试集上评估模型

model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test, y_test)

6.1.4 小结

1、将原始文本转换为神经网络能够处理的格式:one-hot 编码词嵌入
2、使用 Keras 模型的 Embedding 层来学习针对特定任务的标记嵌入。
3、使用预训练词嵌入在小型自然语言处理问题上获得额外的性能提升。

6.2 理解循环神经网络

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

与之相反,循环神经网络(RNN,recurrent neural network) 会遍历所有序列元素,并保存一个状态(state),其中包含与已查看内容相关的信息。具体过程是:向 RNN 中输入大小为 (timesteps, input_features) 的二维张量;对 时间步(timestep) 进行遍历;在每个时间步,RNN 会考虑 t 时刻的当前状态与 t 时刻的输入[形状为 (input_ features,)],对二者计算得到 t 时刻的输出;然后将下一个时间步的状态设置为上一个时间步的输出。
在这里插入图片描述

1、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               # 输出作为下一次迭代的输入

2、简单 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 = []
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)  # 堆叠成一个(timesteps, output_features) 的二维张量

本例中,最终输出是一个形状为 (timesteps, output_features) 的二维张量,包括所用时刻的输出向量。但 t 时刻的输出向量包含 0~t 时刻的全部信息,所以在大多数情况下只需要最后一个输出向量。

6.2.1 Keras 中的循环层

在 Keras 中,利用 SimpleRNN 层 实现上述功能。SimpleRNN 层能够处理序列批量,它接收形状为 (batch_size, timesteps, input_features) 的输入,而不是 (timesteps, input_features)。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))
>>> model.summary()
_________________________________________________________________
Layer (type)                   Output Shape             Param # 
=================================================================
embedding_22 (Embedding)      (None, None, 32)          320000 
_________________________________________________________________
simple_rnn_10 (SimpleRNN)     (None, 32)                2080 
=================================================================
Total params: 322,080
Trainable params: 322,080
Non-trainable params: 0

可以将多个循环层逐个堆叠,从而提高网络的表达能力。此时,需要让所有中间层都返回完整的输出序列。2080 = 32 × 32 + 32 × 32 + 32

>>> 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)) 
>>> model.summary() 
_________________________________________________________________
Layer (type)                   Output Shape            Param # 
=================================================================
embedding_24 (Embedding)       (None, None, 32)        320000 
_________________________________________________________________
simple_rnn_12 (SimpleRNN)      (None, None, 32)        2080 
_________________________________________________________________
simple_rnn_13 (SimpleRNN)      (None, None, 32)        2080 
_________________________________________________________________
simple_rnn_14 (SimpleRNN)      (None, None, 32)        2080 
_________________________________________________________________
simple_rnn_15 (SimpleRNN)      (None, 32)              2080 
=================================================================
Total params: 328,320
Trainable params: 328,320
Non-trainable params: 0

接下来,将这个模型应用于 IMDB 电影评论分类问题中。

1、准备 IMDB 数据

from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000 
maxlen = 500 
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 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、用 Embedding 层和 SimpleRNN 层来训练模型

from keras.layers import Dense

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、绘制结果
在这里插入图片描述

验证精度只有 85%,低于第 3 章中的测试精度。部分原因在于,输入只考虑了前 500 个单词,而不是整个序列;另一个原因在于,该问题只需要关注单词的情感,而不是顺序,而 SimpleRNN 善于处理时间序列。

6.2.2 理解 LSTM 层和 GRU 层

SimpleRNN 并不是 Keras 中唯一可用的循环层,还有另外两个:LSTMGRU。SimpleRNN 通常过于简化,没有实用价值。SimpleRNN 的最大问题在于,理论上它应该能够记住许多时间步之前见过的信息;但实际上随着时间的推进,会逐渐遗忘较远的记忆。其原因在于 梯度消失问题(vanishing gradient problem)

LSTM 层是 SimpleRNN 层的一种变体,它增加了一种携带信息跨越多个时间步的方法。假设有一条传送带,其运行方向平行于你所处理的序列。序列中的信息可以在任意位置跳上传送带,然后被传送到更晚的时间步,并在需要时原封不动地跳回来。
在这里插入图片描述

图中添加额外的数据流,其中携带着跨越时间步的信息。它在不同的时间步的值叫作 Ct,其中 C 表示携带(carry)

1、LSTM 架构的详细伪代码

""" 输出 """
output_t = activation(dot(state_t, Uo) + dot(input_t, Wo) + dot(C_t, Vo) + bo)

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)

""" 更新记忆流 """ 
c_t+1 = i_t * k_t + c_t * f_t

在这里插入图片描述

如果要更哲学一点,还可以解释每个运算的目的。比如:c_t * f_t 是为了故意遗忘携带数据流中的不相关信息,i_t * k_t 是为了将信息记录到记忆流中。但归根结底,这些解释并没有多大意义,因为这些运算的实际效果是由参数化权重决定的,而权重是以端到端的方式进行学习,每次训练都要从头开始,不可能为某个运算赋予特定的目的

RNN 单元的类型决定了假设空间,即在训练期间搜索良好模型配置的空间,但它不能决定 RNN 单元的作用,那是由单元权重来决定的。同一个单元具有不同的权重,可以实现完全不同的作用。因此,组成 RNN 单元的运算组合,最好被解释为对搜索的一组 约束,而不是一种工程意义上的 设计

6.2.3 Keras 中一个 LSTM 的具体例子

只需指定LSTM 层的输出维度,其他所有参数(有很多)都使用 Keras 默认值。Keras 具有很好的默认值,无须手动调参,模型通常也能正常运行。

1、使用 Keras 中的 LSTM 层

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)

在这里插入图片描述

验证精度达到了 89%,比 SimpleRNN 网络好多了,这主要是因为 LSTM 受梯度消失问题的影响要小得多。可以调节超参数,增加正则化提高精度。

6.2.4 小结

1、循环神经网络(RNN) 的概念及其工作原理。
2、长短期记忆(LSTM) 是什么,为什么它在长序列上的效果要好于普通 RNN。
3、如何使用 Keras 的 RNN 层来处理序列数据。

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

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

6.3.1 温度预测问题

本节将使用一个天气时间序列数据集(由德国耶拿的马克思 • 普朗克生物地球化学研究所的气象站记录,2009—2016 年)来构建模型,输入最近的一些数据(几天的数据点),从而预测 24 小时之后的气温。

1、观察耶拿天气数据集的数据

import os

data_dir = '/users/fchollet/Downloads/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))

在这个数据集里,每行是一个时间步(10 min),记录了一个日期和 14 个与天气有关的值。

2、解析数据

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

3、绘制温度时间序列

from matplotlib import pyplot as plt

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

在这里插入图片描述

6.3.2 准备数据

这个问题的确切表述是:一个时间步是 10 分钟,每 steps 个时间步采样一次数据,给定过去 lookback 个时间步之内的数据,能否预测 delay 个时间步之后的温度?

具体参数值如下:
1、lookback = 720:给定过去 5 天内的观测数据(输入)。
2、steps = 6:观测数据的采样频率是每小时一个数据点。
3、delay = 144:目标是未来 24 小时之后的数据(输出)。

在开始训练之前,需要完成以下两件事:
1、将数据预处理为神经网络可以处理的格式。数据已经是数值型的,所以不需要做向量化。只需要对每个时间序列分别做标准化,让它们在相似的范围内都取较小的值。
2、编写一个 Python 生成器,以当前的浮点数数组作为输入,并从最近的数据中生成数据批量,同时生成未来的目标温度。因为数据集中的样本是高度冗余的(对于第 N 个样本和第 N+1 个样本,大部分时间步都是相同的),所以显式地保存每个样本是一种浪费。
在这里插入图片描述

1、数据标准化

mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std

2、生成时间序列样本及其目标的生成器

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没有变化,后面也没有用到i """
    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),         # batch_size,批量大小
                           lookback // step,   # timesteps,数据点个数
                           data.shape[-1]))    # feature_size,特征值个数
        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

该生成器返回一个元组 (samples, targets),其中 samples 是输入数据的一个批量,targets 是对应的目标温度数组。

生成器的参数如下:

参数作用
data原始数据集,浮点数数组
lookback输入数据应该包括过去多少个时间步。
delay目标应该在未来多少个时间步之后。
min_indexmax_indexdata 数组中的索引,用于界定需要抽取哪些时间步。这有助于保存一部分数据用于验证、另一部分用于测试。
shuffle是否打乱样本的标志参数。
batch_size每个批量的样本数。
step数据采样的周期(单位:时间步,10 min)。

3、准备训练生成器、验证生成器和测试生成器

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   # 查看整个验证集,需要从 val_gen 中抽取的次数
test_steps = (len(float_data) - 300001 - lookback) //batch_size   # 查看整个测试集,需要从test_gen 中抽取的次数

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

首先确定一个基准。一种基于常识的方法就是始终预测 24 小时后的温度等于现在的温度。本节使用平均绝对误差(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()

得到的 MAE 为 0.29。转化成温度的平均绝对误差为 0.29 × temperature_std 摄氏度,即 2.57℃。

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

设计一个基本的机器学习方法,确定基准。

1、训练并评估一个密集连接模型

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)

2、绘制结果
在这里插入图片描述

部分验证损失接近不包含学习的基准方法,但这个结果并不可靠。这也展示了首先建立这个基准方法的优点,事实证明,超越这个基准并不容易。我们的常识中包含了大量有价值的信息,而机器学习模型并不知道这些信息

6.3.5 第一个循环网络基准

该节采用 GRU 层门控循环单元(GRU,gated recurrent unit)层的工作原理与 LSTM 相同。但它做了一些简化,因此运行的计算代价更低(虽然表示能力可能不如 LSTM)。

1、训练并评估一个基于 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)

新的验证 MAE 约为 0.265(在开始显著过拟合之前),反标准化转换成温度的平均绝对误差为 2.35℃。与非机器学习方法相比,误差略微降低。

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

2015 年,Yarin Gal 在博士论文《Uncertainty in deep learning》中确定了在循环网络中使用 dropout 的正确方法:对每个时间步应该使用相同的 dropout 掩码(dropout mask,相同模式的舍弃单元),而不是让 dropout 掩码随着时间步的增加而随机变化。此外,为了对 GRU、LSTM 等循环层得到的表示做正则化,应该将不随时间变化的 dropout 掩码应用于层的内部循环激活(叫作 循环 dropout 掩码)。

Keras 的每个循环层都有两个与 dropout 相关的参数:一个是 dropout,它是一个浮点数,指定该层输入单元的 dropout 比率;另一个是 recurrent_dropout,指定循环单元的 dropout 比率

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.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)

在这里插入图片描述

可以看出,验证集的拟合结果得到了改善,减少了过拟合。

6.3.7 循环层堆叠

上节中的模型已不再过拟合,但似乎遇到了性能瓶颈,所以应该考虑增加网络容量。最简单的方法就是增加每层单元数或增加层数。循环层堆叠(recurrent layer stacking) 是构建更加强大的循环网络的经典方法。

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)

在这里插入图片描述

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

6.3.8 使用双向 RNN

双向 RNN(bidirectional RNN) 是一种常见的 RNN 变体,常用于自然语言处理。
在这里插入图片描述

在 Keras 中将一个双向 RNN 实例化,需要使用 Bidirectional 层,它的第一个参数是一个循环层实例Bidirectional 对这个循环层创建了第二个单独实例,然后使用一个实例按正序处理输入序列,另一个实例按逆序处理输入序列。

1、训练并评估一个双向 LSTM

IMDB 情感分析任务上进行测试。

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)

验证精度略微提升,也更快出向过拟合。

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)

结果与普通 GRU 层差不多一样,这是因为在温度预测任务中所有的预测能力肯定都来自于正序的那一半网络。

6.3.9 更多尝试

1、在堆叠循环层中调节每层的单元个数
2、调节 RMSprop 优化器的学习率
3、尝试使用 LSTM 层代替 GRU 层。
4、在循环层上面尝试使用更大的密集连接回归器,即更大的 Dense 层或 Dense 层的堆叠
5、最后在测试集上运行性能最佳的模型。

6.3.10 小结

1、首先建立一个基于常识的基准。
2、尝试一些简单的模型。
3、如果时间顺序对数据很重要,那么循环网络是一种很适合的方法。
4、在循环网络中使用循环 dropout减小过拟合。
5、与单个 RNN 层相比,堆叠 RNN 的表示能力更加强大,但它的计算代价也更高。

6.4 用卷积神经网络处理序列

时间可以被看作一个空间维度,就像二维图像的高度或宽度。对于文本分类和时间序列预测等简单任务,小型的一维卷积神经网络可以替代 RNN,计算代价通常要小很多,而且速度更快。

6.4.1 理解序列数据的一维卷积

之前介绍的卷积层都是二维卷积,从图像张量中提取二维图块并对每个图块应用相同的变换。按照同样的方法,也可以使用一维卷积,从序列中提取局部一维序列段(即子序列)。
在这里插入图片描述

6.4.2 序列数据的一维池化

二维池化运算在卷积神经网络中用于对图像张量进行空间下采样。一维也可以做相同的池化运算:从输入中提取一维序列段(即子序列),然后输出其最大值(最大池化)或平均值(平均池化)。与二维卷积神经网络一样,该运算也是用于降低一维输入的长度(子采样)。

6.4.3 实现一维卷积神经网络

Keras 中的一维卷积神经网络是 Conv1D 层,其接口类似于 Conv2D,输入是形状为 (samples, time, features) 的三维张量。

1、准备 IMDB 数据

6.2.1 中相似,但不设置batch_size

2、在 IMDB 数据上训练并评估一个简单的一维卷积神经网络

与二维卷积神经网络相比,一维卷积神经网络可以使用更大的卷积窗口。

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 上的运行速度都要更快。

6.4.4 结合 CNN 和 RNN 来处理长序列

一维卷积神经网络分别处理每个输入序列段,所以它对时间步的顺序不敏感(这里所说顺序的范围要大于局部尺度,即大于卷积窗口的大小。用窗口采集后,子序列间就没有了时间顺序。)。为了识别更长期的模式,可以增加层数,这样较深的层能够观察到原始输入中更长的序列段,但这仍然不是一种引入顺序敏感性的好方法。

这是因为该模型并不知道所看到某个模式的时间位置(距开始多长时间,距结束多长时间等)。对于这个具体的预测问题,对最新数据点的解释与对较早数据点的解释应该并不相同,所以卷积神经网络无法得到有意义的结果。

要想结合卷积神经网络的速度和轻量与 RNN 的顺序敏感性,一种方法是在 RNN 前面使用一维卷积神经网络作为预处理步骤。对于那些非常长,以至于 RNN 无法处理的序列,这种方法尤其有用。卷积神经网络可以将长的输入序列转换为高级特征组成的更短序列(下采样)。然后输入到 RNN 中。
在这里插入图片描述

这种方法允许操作更长的序列,所以可以查看更早的数据(通过增大数据生成器的 lookback 参数)或查看分辨率更高的时间序列(通过减小生成器的 step 参数)。这里将 6.3.2 中的 step 减半,提高数据分辨率。

1、为耶拿数据集准备更高分辨率的数据生成器
6.3.2 相似,只将 step 减半。

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,但速度要快很多。

6.4.5 小结

1、一维卷积神经网络在处理时间模式时表现也很好。在自然语言处理任务中,它可以替代 RNN,并且速度更快。
2、一维卷积神经网络的架构与二维卷积神经网络很相似,它将 Conv1D 层MaxPooling1D 层堆叠在一起,最后是一个全局池化运算或展平操作。
3、在 RNN 之前使用一维卷积神经网络作为预处理步骤,这样可以使序列变短,并提取出高级特征交给 RNN 来处理。

小结

1、本章介绍的内容:

1)文本分词词嵌入
2)循环网络
3)循环 dropout堆叠 RNN 层双向 RNN
4)使用一维卷积神经网络来处理序列,结合一维卷积神经网络和 RNN 来处理长序列。

2、可以用 RNN 进行时间序列回归(“预测未来”)、时间序列分类时间序列异常检测序列标记(比如找出句子中的人名或日期)。

3、可以将一维卷积神经网络用于机器翻译(序列到序列的卷积模型,比如
SliceNet)、文档分类拼写校正

4、如果序列数据的整体顺序很重要,那么最好使用循环网络来处理。在时间序列中,最近的数据可能比久远的数据包含更多的信息量

5、如果整体顺序没有意义,那么一维卷积神经网络可以实现同样好的效果,而且计算代价更小。文本数据通常都是这样,在句首发现关键词和在句尾发现关键词一样都很有意义。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猎猫骑巨兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值