【学习笔记】《Python深度学习》第六章:深度学习用于文本和序列

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来处理长序列。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值