[Deep-Learning-with-Python]文本序列中的深度学习.md

内容包括:

  • 将文本数据处理成有用的数据表示
  • 循环神经网络
  • 使用1D卷积处理序列数据

深度学习模型可以处理文本序列、时间序列、一般性序列数据等等。处理序列数据的两个基本深度学习算法是循环神经网络和1D卷积(2D卷积的一维模式)。

文本数据

文本是最广泛的序列数据形式。可以理解为一系列字符或一系列单词,但最经常处理的是单词层面。自然语言处理的深度学习是应用在单词、句子或段落上的模式识别;就像计算机视觉是应用在像素上的模式识别。

就像其他神经网络一样,深度学习模型不能直接处理原始文本:只能处理数值型张量。文本向量化是指将文本转换成数值型张量的过程。有多种处理方式:

  • 将文本分割成单词,将每个单词转换成一个向量;
  • 将文本分割成字符,将每个字符转换成一个向量;
  • 抽取单词或字符的n-grams,将每个n-grams转换成一个向量;n-grams是多个连续单词或字符的重叠组。

总的来说,可以文本分解的基本的不同单元(单词,字符或n元语法)称为标记,将文本分解为这样的标记的过程称为标记化tokenization。文本向量化过程:对文本使用标记模式,将数值向量和生成的token联系起来。这些向量打包成序列张量,送到深度学习网络中。将向量和token对应方式有多种,比如one-hot encoding for tokens和token embedding(word embedding).

单词、字符的one-hot编码

将token向量化最常见、最基本的方法是one-hot编码。 单词级别one-hot编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np

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

token_index = {}#对应关系token-ids
for sample in samples:
for word in sample.split():
if word not in token_index: #字典中不存在token时,添加token
token_index[word] = len(token_index)+1 #不使用0

max_length = 10#处理单句的最大长度
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]:#如果句子长度过长,截断;j句子第j单词
index = token_index.get(word)#字典中位置
results[i,j,index] = 1.

字符级one-hot编码

1
2
3
4
5
6
7
8
9
10
11
12
13
import string

samples = ['The cat sat on the mat.', 'The dog ate my homework.']
characters = string.printable#包含所有可打印字符的字符串
token_index = dict(zip(characters,range(1,len(characters)+1)))#id-character;1计数

max_length = 50#处理单个句子的字符最大长度
results = np.zeros(shape=(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.

Keras内置有文本单词级和字符集one-hot编码函数,从原始文本数据开始处理。推荐使用这些函数,因为它们考虑了许多重要的特性,比如忽略字符串中的个别特殊字符,只考虑数据集中最常见的N个单词(避免处理非常大的输入向量空间)。 Keras内置函数的单词级one-hot编码

1
2
3
4
5
6
7
8
9
10
11
12
13
from keras.preprocessing.text import Tokenizer

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

tokenizer = Tokenizer(num_words=1000)#考虑1000个最常见的单词
tokenizer.fit_on_texts(samples)#生成word index

sequences = tokenizer.texts_to_sequences(samples)#将文本转换成下标列表

one_hot_results = tokenizer.texts_to_matrix(samples,mode='binary')#文本直接转换为one-hot编码,向量

word_index= tokenizer.word_index#学到的word index对应关系
print('Found %s unique tokens.' % len(word_index))

单热编码的变体是单热哈希编码—当词汇表中的唯一token数量太大而无法明确处理时,可以使用该技巧。可以将单词散列为固定大小的向量,而不是为每个单词显式分配索引并在字典中保留这些索引的引用。这通常使用非常轻量级的散列函数来完成。这种方法的主要优点是它不需要维护一个明确的单词索引,这可以节省内存并允许数据的在线编码(可以在看到所有可用数据之前立即生成token向量)。这种方法的一个缺点是它容易受到哈希冲突的影响:两个不同的词可能最终会有相同的哈希值,随后任何查看这些哈希值的机器学习模型都无法区分这些词。当散列空间的维度远大于被散列的唯一token的总数时,散列冲突的可能性降低。

1
2
3
4
5
6
7
8
9
10
samples = ['The cat sat on the mat.', 'The dog ate my homework.']

dimensionality = 1000#hash空间维度
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
results[i, j, index] = 1.

word embeddings

将向量与单词相关联的另一种流行且有效的方法是使用密集单词向量,也称为词嵌入。通过单热编码获得的向量是二进制的,稀疏的(主要由零组成),并且具有非常高的维度(与词汇表中的单词数相同的维度),词嵌入是低维浮点向量(即密集向量,与稀疏向量相反).与通过单热编码获得的单词向量不同,词嵌入是从数据中学习的。在处理非常大的词汇表时,通常会看到256维,512维或1,024维的单词嵌入。另一方面,单热编码字通常导致向量维度是20000或更大(在这种情况下捕获20000token的词汇标)。因此,词嵌入将更多信息打包到更少的维度中。

词嵌入有两种获得方式:

  • 学习词嵌入和关注的主要任务(例如文档分类或情绪预测)联合起来。在此设置中,从随机单词向量开始,然后以与神经网络权重相同的方式学习单词向量;
  • 加载到模型词嵌入中,这些词是使用不同的机器学习任务预先计算出来的,而不是正在尝试解决的任务。这些被称为预训练词嵌入。

通过Embedding网络层学习词嵌入向量

将密集向量与单词相关联的最简单方法是随机选择向量。这种方法的问题在于产生的嵌入空间没有结构:例如,accurate和exact的单词最终可能会有完全不同的嵌入,即使它们在大多数句子中都是可互换的。深度神经网络难以理解这种嘈杂的非结构化嵌入空间。 更抽象的说,词向量之间的几何关系应该反映这些单词之间的语义关系。词嵌入意味着将自然语言映射到几何空间中。比如,在适合的嵌入空间中,希望将同义词嵌入到相似的单词向量中;一般来说,期望任意两个单词向量之间的几何距离(例如L2距离)与相关单词之间的语义距离相关(意思不同的单词嵌入在远离彼此相关,而相关的词更接近)。除了距离之外,可能希望嵌入空间中的特定方向有意义。 是否有一些理想的单词嵌入空间可以完美地映射人类语言,并且可以用于任何自然语言处理任务?可能,但尚未计算任何类型的东西。此外,没有人类语言这样的东西—有许多不同的语言,它们不是同构的,因为语言是特定文化和特定语境的反映。但更务实的是,良好的词汇嵌入空间在很大程度上取决于你的任务:英语电影评论情感分析模型的完美词汇嵌入空间可能与英语法律的文档分类模型的完美嵌入空间有所不同,因为某些语义关系的重要性因任务而异。

因此,在每个新任务中学习新的嵌入空间是合理的。幸运的是,反向传播使这很容易,而Keras使它变得更加容易。它是关于学习图层的权重:Embedding嵌入图层。

1
2
3
from keras.layers import Embedding

embedding_layer = Embedding(1000,64)#嵌入层需要至少两个参数:tokens个数eg1000,嵌入层维度eg64

Embedding嵌入层最好的理解方法是看成一个字典:将整数下标(代表一个某个单词)映射到一个稠密向量上。它将整数作为输入,它在内部字典中查找这些整数,并返回相关的向量。

Embedding网络层接收一个2D整数张量为输入,形状(samples,sequence_length),其中每个实体是整数的序列。它可以嵌入可变长度的序列:例如,可以在前面的示例批次中输入嵌入层,其中包含形状(32,10)(32个序列长度为10的批次)或(64,15)(64个序列长度15的批次)。但是,批处理中的所有序列必须具有相同的长度(因为需要将它们打包到单个张量中),因此比其他序列短的序列应该用零填充,并且应该截断更长的序列。 网络层返回一个3D浮点类型张量,形状(samples, sequence_length, embedding_dimensionality).这样的3D张量可以用RNN或1D卷积层处理。 当实例化一个Embedding网络层时,权重(内部字典的token向量)和其他网络层类似,随机初始化。在训练过程中,这些词向量通过反向传播逐渐改动,将空间结构化为下游模型可以利用的东西。一旦完全训练,嵌入空间将显示许多结构 —一种专门针对正在训练模型的特定问题的结构。 在IMDB电影评论语义分析任务上,应用词嵌入。首先,在电影评论中取最常见的10000个单词,然后将每条评论长度限制为20个单词。网络将会学习到10000个单词的8维词嵌入空间,将每个输入的整数序列(2D)转换成嵌入层序列(3D浮点张量),平铺成2D张量,添加一个Dense层做分类。

1
2
3
4
5
6
7
8
9
10
from keras.datasets import imdb
from keras 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)

使用Embedding层分类

1
2
3
4
5
6
7
8
9
10
from keras.models import Sequential
from keras.layers import Flatten,Dense

model = Sequential()
model.add(Embedding(10000,8,input_length=maxlen))
model.add(Flatten())
model.add(Dense(1,activation='sigmoid'))
model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])

history = model.fit(x_train,y_train,epochs=10,batch_size=32,validation_split=0.2)

验证集上的准确率为76%左右,考虑到每条评论只有20个单词,这个结果也可以接受。注意仅仅将embedded嵌入序列平铺,然后在单层全连接网络上训练,导致模型将输入序列的每个单词分割开来看,没有考虑句子的结构以及单词之间的关系。最好在嵌入序列的顶部添加循环层或1D卷积层,以学习将每个序列作为一个整体考虑在内的特征。

使用预训练词嵌入

有时,只有很少的训练数据,无法单独使用数据来学习特定的任务的词嵌入,怎么办?可以从预先计算的嵌入空间中加载嵌入向量,而不是学习想要解决的问题的词嵌入向量,这些嵌入空间是高度结构化的并且展示了有用的属性 - 它捕获了语言结构的一般方面。在自然语言处理中使用预训练单词嵌入的基本原理与在图像分类中使用预训练的卷积网络大致相同:没有足够的数据可用于自己学习真正有用的特征,但期望获得所需的特征相当通用—即常见的视觉特征或语义特征。在这种情况下,重用在不同问题上学习的特征是有意义的。

这样的词嵌入通常使用词出现统计(关于在句子或文档中共同出现的词的观察),使用各种技术来计算,一些涉及神经网络,一些不涉及。Bengio等人最初探讨了以无人监督的方式计算密集,低维度的文字嵌入空间的想法。在21世纪初期,发布了最著名和最成功的词汇嵌入方案之后:Word2vec算法,它开始在研究和行业中广泛应用,由Tomas Mikolov于2013年在谷歌开发. Word2vec维度捕获具体语义属性,例如性别。 可以在Keras嵌入层中下载和使用各种预嵌入的字嵌入数据库。 Word2vec就是其中之一。另一种流行的称为全球向量词表示GloVe,由斯坦福大学的研究人员于2014年开发。该嵌入技术基于对词共现统计矩阵进行因式分解,已经为数以百万计的英语token提供了预先计算的嵌入,这些嵌入是从维基百科数据和通用爬网数据中获得的。

整合:原始文本到词嵌入

下载IMDB原始数据集 地址,解压缩。

处理原始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os

imdb_dir = './imdb'#数据集地址
train_dir = os.path.join(imdb,'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)

数据分词tokenizing 文本向量化,划分训练集和验证集。因为预训练的单词嵌入对于几乎没有可用训练数据的问题特别有用(否则,任务特定的嵌入表现可能超过它们),将添加限制:将训练数据限制为前200个样本。因此,在查看了200个示例之后,对电影评论进行分类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np

maxlen = 100#单个句子最大长度
training_samples = 200#训练集数据量
validation_samples = 10000#验证集数据量
max_words = 10000#字典长度

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)#生成tokens字典
sequences = tokenizer.texts_to_sequences(texts)#将多个文档转换为字典对应下标的list表示,shape为(文档数,每条文档的长度)
word_index = tokenizer.word_index#word-id字典
print('Found %s unique tokens' % len(word_index))

data = pad_sequences(sequences, maxlen=maxlen)#将每个序列padding成相同长度

labels = np.asarray(labels)
print('Shape of data tensor:',data.shape)
print('Shape of label tensor:', labels.shape)

indices = np.arange(data.shape[0])#打乱shuffle
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+validaion_samples]

下载GLOVE词嵌入向量 地址;2014英语维基百科,822MB zip文件,名字‘glove.6B.zip’,包括100维的嵌入向量,40万个单词。 预处理嵌入向量 读取txt文件,构建一个映射关系:单词–词向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
glove_dir = './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_dims),其中其中每个条目i包含参考词索引(在tokenization期间构建)中索引i的单词的embedding_dim维的向量。请注意,索引0不应代表任何单词或标记 - 它是占位符。 预处理GloVe词向量

1
2
3
4
5
6
7
8
embedding_dims = 100

embedding_matrix = np.zeros((max_words,embedding_dims))
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

模型定义

1
2
3
4
5
6
7
8
9
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'))

加载预训练的词向量到Embedding网络层中

1
2
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False

“Freeze”冻住网络层–不可训练。

模型训练验证

1
2
3
4
5
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')

模型训练集和验证集上的准确率、损失值变化

该模型很快就开始过度拟合—训练样本数量很少。出于同样的原因,验证准确性具有很大的差异。

请注意,结果可能会有所不同:因为训练样本很少,性能很大程度上取决于选择的200个样本—而且是随意选择。 也可以训练相同的模型,而无需加载预训练的单词嵌入,也不冻结嵌入层。在这种情况下,您将学习输入tokens的特定于任务的嵌入,当大量数据可用时,这通常比预训练的词嵌入更强大。

不用预训练词嵌入训练相同的网络模型

1
2
3
4
5
6
7
8
9
10
11
12
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.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))

训练集和验证集准确率、损失值变化

验证准确性在50%内停滞。因此,在这种情况下,预训练的单词嵌入优于共同学习的嵌入。

测试集上评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test, y_test)#准确率在56%左右。

小结

  • 将原始数据转换成网络可以处理的张量;
  • 在Keras模型中使用Embedding网络层;
  • 在自然语言处理的小数据集问题上使用预训练的词向量提高模型准确率。

循环神经网络Recurrent neural networks[RNN]

到目前为止,所见过的所有神经网络的一个主要特征,例如全连接的网络和卷积网络,就是它们没有“记忆能力”。显示给它们的每个输入都是独立处理的,输入之间没有任何状态。使用此类网络,为了处理序列或时间序列的数据,必须立即向网络显示整个序列:将其转换为单个数据点。例如,在IMDB示例中所做的:整个电影评论被转换为单个大型向量并一次处理。这种网络称为前馈网络。

相比之下,当你正在阅读现在的句子时,你正在逐字处理它 - 或者更确切地说,通过眼睛扫视 - 同时记住之前的事物;这使你能够流畅地表达这句话所传达的意义。生物智能逐步处理信息,同时保持其处理内部模型,根据过去的信息构建,并随着新信息的不断更新而不断更新。

递归神经网络(RNN)采用相同的原理,尽管是极其简化的版本:它通过迭代序列元素并维持包含与迄今为止所见内容相关信息的状态来处理序列。 实际上,RNN是一种具有内部循环的神经网络. 在处理两个不同的独立序列(例如两个不同的IMDB评论)之间重置RNN的状态,因此仍然将一个序列视为单个数据点:网络的单个输入。 更改的是,数据点不再在一个步骤中处理;相反,网络内部循环遍历序列元素 为了使这些循环loop和状态state的概念清晰,用Numpy实现一个小的RNN的前向传递。该RNN将一系列向量作为输入,您将其编码为2D张量大小(timesteps, input_features)。它在时间步长上循环,并且在每个时间步长,它在t处考虑其当前状态,在t处考虑输入,形状(input_features, ),并将它们组合起来以获得t处的输出。然后,将设置下一步的状态为此前一个输出。对于第一个时间步,未定义前一个输出;因此,目前没有状态。所以,把状态初始化为零向量称为网络的初始状态。

伪代码 V1

1
2
3
4
state_t = 0 #初始t:0
for input_t in input_sequence:#序列元素迭代
output_t = f(input_t,state_t)#输出为当前输入和当前状态相关
state_t = output_t#下一刻的状态为上一刻状态的输出

可以具体化函数f:将输入和状态转换为输出—参数化为两个矩阵W和U以及偏置向量。类似于前馈网络中全连接层操作的转换。

V2

1
2
3
4
state_t = 0
for input_t in input_sequences:
output_t = activation(dot(W,input_t)+dot(U,state_t)+b)
state_t = output_t

V3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import numpy as np

timesteps = 100
input_features = 32
output_features = 64

inputs = np.zeros((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.concatenate(successive_outputs,axis=0)

总之,RNN是一个for循环,它重用循环的前一次迭代期间计算的结果,仅此而已。当然,适合这个定义有许多不同的RNN - 这个例子就是其中之一最简单的RNN。RNN的特征在于它们的阶跃函数,例如在这种情况下的以下函数: $output_t = np.tanh(np.dot(W,input_t)+np.dot(U,state_t)+b)$

注意:在该示例中,最终输出是2D张量的形状(timesteps,output_features),其中每个时间步长是时间t处的循环的输出结果。输出张量中的每个时间步t包含关于输入序列中的时间步长0到t的信息 - 关于整个过去。因此,在许多情况下,不需要这个完整的输出序列;你只需要最后一个输出(循环结束时的output_t),因为它已经包含有关整个序列的信息。

Keras 循环网络层

上面numpy编码实现的是Keras网络层—SimpleRNN网络层:

1
from keras.layers import SimpleRNN

有一个小区别是:SimpleRNN网络层处理序列小批量,而不是一个简单的numpy序列,意味着输入形状为(batch_size, timesteps, input_features),不是(timesteps, input_features). 和Keras的其他循环网络类似,SimpleRNN有两种运行方式:返回每个时间步的输出结果序列集,3D张量,形状(batch_size, timesteps, output_features);返回每个输入序列的最终输出结果,2D张量,形状(batch_size, output_features). 两种方式通过参数return_sequences 控制。 使用SimpleRNN,返回最后时间步的输出结果:

1
2
3
4
5
6
from keras.models import Sequential
from keras.layers import Embedding,SimpleRNN

model = Sequential()
model.add(Embedding(10000,32))
model.add(SimpleRNN(32))

返回全部的状态序列state:

1
2
3
model = Sequential()
model.add(Embedding(10000,32))
model.add(SimpleRNN(32,return_sequences=True))

有时候,需要将几个循环网络层依次相连,增加网络模型的特征表示能力。同时,为了返回所有的输出序列,必须获得所有的中间网络层结果。

1
2
3
4
5
6
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))

使用循环网络处理IMDB数据集。 数据集处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from keras.datasets import imdb
from keras.preprocessing import sequence

max_fetaures = 10000
maxlen = 500
batch_size = 32
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=maxlen)
x_test = sequence.pad_sequences(x_test,maxlen=maxlen)

print('x_train shape:', x_train.shape)
print('x_test shape:',x_test.shape)

模型训练

1
2
3
4
5
6
7
8
9
from keras.layers import 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(x_train,y_train,epochs=10,batch_size=128,validation_split=0.2)

训练接、验证集上准确率、损失值变化

简单的SimpleRNN验证集上准确率最高85%左右,主要问题在于输入序列只考虑前500个单词,而不是整个完整序列。SimpleRNN不擅长处理长序列,如文本。常用其他循环网络处理。

LSTM和GRU网络层

SimpleRNN并不是Keras唯一的循环网络层,还有LSTM和GRU。实际应用时,通常不使用SimpleRNN,因为SimpleRNN过于简单,无法实际使用。SimpleRNN有一个主要问题:虽然它理论上应该能够在时间t保留有关输入的信息[这些信息在很多时间之前看到],但在实践中,这种长期依赖性是不可能学习到的。 这是由于梯度消失问题,类似于非循环网络(前馈网络)所观察到的:当不断向网络添加层时,网络最终变得无法处理。LSTM和GRU层旨在解决梯度消失问题。

LSTM,Long Short-Term Memory,SimpleRNN的变种:它增加了一种跨多个时间步携带信息的方法。 想象一下,传送带与正在处理的序列平行运行。序列中的信息可以在任何时候跳到传送带上,运输到稍后的时间步,并在需要时完好无损地跳下。这基本上就是LSTM所做的事情:它为以后保存信息,从而防止旧信号在处理过程中逐渐消失。

为了详细了解这一点,让我们从SimpleRNN单元格开始。因为有很多权重矩阵,所以用单词o(Wo和Uo)索引单元格中用于输出的W和U矩阵。 SimpleRNN

在此图片中添加一个跨时间步长传输信息的附加数据流。不同的时间步长Ct各不相同,其中C代表Carry。此信息将对单元格产生以下影响:它将与输入连接和循环连接相结合(通过全连接转换:带有权重矩阵的点积,然后是偏置加法和激活函数),它将影响被发送到下一个时间步的状态(通过激活函数和乘法运算)。从概念上讲,信息数据流是一种调制下一个输出和下一个状态的方法。 LSTM 微妙之处:计算Ct数据流的下一个值的方式。涉及三种不同的转变。这三种都具有SimpleRNN单元的形式:

1
y = activation(dot(state_t,U)+dot(input_t,W)+b)

但三种转换方式都有自己的权重矩阵,用i, f, k 对三种方式索引。

1
2
3
4
5
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)

通过组合i_t,f_t和k_t获得新的carray状态(next c_t)。

1
c_t+1 = i_t * k_t + c_t * f_t

如果想直觉性地了解,可以解释每个操作的意图。例如,可以说乘以c_t和f_t是故意忘记carry数据流中无关信息的一种方法;同时,i_t和k_t提供有关当前的信息,用新信息更新carry轨道。但是这些解释并没有多大意义,因为这些操作实际上做的是由参数化的权重决定的;并且权重以端到端的方式学习,从每轮训练开始—不可能将这个或那个操作归功于特定目的。RNN单元格的规范确定了假设空间—在训练期间搜索良好模型配置的空间 - 但它不能确定单元格的作用;这取决于单元格权重。(如全连接网络确定假设空间,全连接权重系数决定每次转换操作)。具有不同重量的相同单元可以做非常不同的事情。因此,构成RNN单元的操作组合可以更好地解释为对空间搜索的一组约束,而不是工程意义上的设计

对于研究人员来说,‘ 如何实现RNN单元的问题’似乎选择约束方式, 最好留给优化算法(如遗传算法或强化学习过程),而不是人类工程师。在未来,这就是构建网络的方式。总之,不需要了解LSTM单元的特定架构。LSTM单元的作用:允许以后重新注入过去的信息,从而解决消失梯度问题。

LSTM例子

IMDB数据集上使用LSTM.网络模型和SimpleRNN架构类似。设置LSTM网络层输出维度,其他为默认设置。Keras默认参数设置,不需要微调即可取得很好的效果。

1
2
3
4
5
6
7
8
9
from keras.layers import LSTM

model = Sequential()
model.add(Embedding(10000,32))
model.add(LSTM(32))
model.add(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)

训练集、验证集损失值、准确率变化 验证集上准确率在89%左右。比SimpleRNN结果好很多,因为梯度消失问题对LSTM影响很小。但是这种结果对于这种计算密集型方法并不具有开创性。为什么LSTM表现不佳?一个原因是没有 调整超参数,例如嵌入维度或LSTM输出维度。另一种可能是缺乏正则化。但主要原因是分析评论的长期结构(LSTM擅长什么)对情绪分析问题没有帮助。通过查看每个评论中出现的单词以及频率,可以很好地解决这样一个基本问题。这就是第一个全连接的方法。 但是有更难的自然语言处理问题在那里,LSTM的优势将变得明显:特别是问答和机器翻译

小结

  • RNN结构,如何工作?
  • LSTM
  • Keras LSTM处理序列数据

循环神经网络的高级应用

  • 循环网络Dropout:缓解过拟合
  • stacking 循环网络:增加模型特征表示能力;
  • 双向循环网络:以不同的方式向循环网络提供相同的信息,提高准确性并减少遗忘。

温度预测问题

到目前为止,所涵盖的唯一序列数据是文本数据,例如IMDB数据集和路透社数据集。但是,除了语言处理之外,序列数据还存在于更多问题中。比如德国耶拿马克斯普朗克生物地球化学研究所气象站记录的天气时间序列数据集

在该数据集中,记录几年内每10分钟14种不同的度量(例如空气温度,大气压,湿度,风向等)。原始数据可以追溯到2003年,但这个例子仅限于2009 - 2016年的数据。该数据集非常适合学习使用数值时间序列。使用它来构建一个模型,该模型将最近的一些数据作为输入过去(几天的数据点)并预测未来24小时的气温。

下载地址

观察数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os

data_dir = '/home/gao/datasets/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))

一共有420551条记录,每行是一个时间步:日期和14个和天气相关的记录值。headers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
["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)"]

将数据转换成Numpy数组:

1
2
3
4
5
6
import numpy as np

float_data = np.zeros((len(lines),len(headers)-1))
for i,line in enumerate(lines):
values = [float(x) for x in line.split(',')[1:]]
float_data[i,:] = values

每10分钟一条记录,1天144条数据。画图显示前10天温度变化情况。

1
2
3
4
from matplotlib.pyplot as plt

temp = float_data[:, 1]
plt.plot(range(1440), temp[:1440])

如果在过去几个月的数据中尝试预测下个月的平均温度,由于数据的年度可靠周期性,问题将很容易。但是,在几天的时间内查看数据,温度看起来更加混乱。这个时间序列是否可以在日常范围内预测?

准备数据

问题的确切表述如下:给定的数据可以追溯到回溯时间步长(时间步长为10分钟)并按步骤时间步长采样,能预测延迟时间步长的温度吗?

  • lookback:720,查看过去5天数据;
  • steps:6,每小时进行一次数据采样;
  • delay:144,将来24小时的预测。

开始之前需要:

  1. 将数据预处理为神经网络可以处理的格式。数据已经是数字,因此不需要进行任何向量化。但是数据中的每个时间序列都有不同的取值范围(例如,温度通常介于-20和+30之间,但是以mbar为单位测量的大气压力大约为1,000)。 独立标准化每个时间序列,以便它们都以相似的比例获取小值。
  2. 编写一个Python生成器,它接收当前浮点数据数组,并从最近的过去产生批量数据,以及将来的目标温度。因为数据集中的样本是高度冗余的(样本N和样本N + 1将具有共同的大多数时间步长,明确分配每个样本将是浪费的。相反,您将使用原始数据动态生成样本。

通过减去每个时间序列的平均值并除以标准差来预处理数据。将使用前200,000个步骤作为训练数据,因此仅计算此部分数据的平均值和标准差。 数据标准化

1
2
3
4
mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std

数据生成器生成元组形式,(samples,targets),samples是输入数据的一个批量,targets是对应的温度标签数组。参数:

  • data:原始浮点数组数据;
  • lookback:输入数据查看的历史数据长度;
  • delay:预测将来数据的长度;
  • min_index和max_index:数据数组中的索引,用于分隔要绘制的时间步长timesteps,对于保留一部分数据以进行验证以及另一部分用于测试非常有用;
  • shuffle:是否打乱顺序;
  • batch_size:批量容量大小;
  • step: 用于对数据进行采样的时间段(以时间步长为单位)。将其设置为6,以便每小时绘制一个数据点。

数据生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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]))
targets = np.zeros((len(rows),))
for j,row in enumerate(rows):
indeices = range(rows[j]-lookback,rows[j],step)
samples[j] = data[indices]
targets[j] = data[rows[j]+delay][1]
yield samples, targets

使用generator生成器生成训练集、验证集和测试集。训练集在前200000条数据上,验证集在之后的100000条数据上,测试集在剩下数据集上。 准备训练集、验证集和测试集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
test_steps = (len(float_data) - 300001 - lookback)

常识性的非机器学习baseline

在开始使用黑盒深度学习模型来解决温度预测问题之前,先尝试一种简单的常识性方法。它将作为一个完整性检查,它将建立一个你必须击败的baseline,以证明更先进的机器学习模型的用处。当你正在接近尚未知解决方案的新问题时,这些常识baseline会很有用。一个典型的例子是不平衡的分类任务,其中一些类比其他类更常见。如果数据集包含90%的A类实例和10%B类实例,则采用常识方法分类任务是在呈现新样本时始终预测“A”。这样的分类器总体上是90%准确的,因此任何基于学习的方法都应该超过这个90%的分数以证明有用性。有时,这些基本baseline可能难以击败。

在这种情况下,可以安全地假设温度时间序列是连续的(明天的温度可能接近今天的温度)以及具有每日周期的周期性。因此,常识性的方法是始终预测从现在起24小时的温度将等于现在的温度。 使用平均绝对误差(MAE)度量来评估这种方法:

1
np.mean(np.abs(preds-targets))

验证

1
2
3
4
5
6
7
8
9
10
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均值,1方差,所以不能立即说明0.29的意义。0.29xtemperature_std转换为平均绝对误差2.57。 MAE转摄氏度误差

1
celsius_error = 0.29*std[1]

平均绝对误差很大。用深度学习解决问题。

机器学习方法

以同样的方式在尝试机器学习方法之前建立常识baseline是有用的,在研究复杂且计算成本高昂的模型(如RNN)之前尝试简单,廉价的机器学习模型(例如小型,全连接的网络)是有用的。这是确保解决问题的任何进一步复杂性是合法的并带来实际好处的最佳方法

下面显示了一个全连接的模型,该模型从展平数据开始,然后通过两个Dense网络层运行。注意最后一个Dense网络层缺少激活函数,这是回归问题的典型特征。使用MAE作为损失。因为使用常识方法评估完全相同的数据并使用完全相同的度量标准,结果可以直接进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

训练集、验证集上损失值变化

一些验证损失接近无学习基线,但不可靠。这表明首先要有这个baseline的优点:事实证明超过baseline并不容易。常识baseline包含许多机器学习模型无法访问的有价值信息。

RNN baseline

第一个全连接的方法做得不好,但这并不意味着机器学习不适用于这个问题。之前的方法首先使时间序列变平,从输入数据中删除了时间概念。数据是一个序列,因果关系和秩序很重要。尝试循环序列处理模型 - 它应该是这种序列数据的完美拟合,因为它利用了数据点的时间排序,与第一种方法不同。 使用GRU网络层(Gated recurrent unit)。GRU层使用与LSTM相同的原理工作,但它们有些简化,因此运行成本更低(尽管可能没有LSTM那么多的特征表示能力)。计算代价和特征表示能力之间的这种权衡在机器学习中随处可见。

1
2
3
4
5
6
7
8
9
10
11
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.结果好很多。超越常识baseline,展示了机器学习的价值,与此类任务中的序列扁平化全密集网络相比,循环网络的优越性。

RNN Dropout

从训练和验证曲线可以看出,该模型过度拟合:训练和验证损失在几个epochs之后开始显著不同。已经熟悉了一种解决这种现象的经典技术:Dropout,它会随机将一个图层的输入单元归零,以便打破该图层所暴露的训练数据中的偶然相关性。但如何在循环网络中使用Dropout? 在2015年,Yarin Gal作为他关于贝叶斯深度学习的博士论文的一部分,确定了循环网络使用dropout的正确方法:应该在每个时间步应用相同的dropout mask(相同的丢弃单位模式),而不是从时间步长到时间步长随机变化的dropout mask。更重要的是,为了规范由GRU和LSTM等循环网络层形成的特征表示,应将时间上恒定的dropout mask应用在网络层的内部循环激活值上。在每个时间步使用相同的dropout mask允许网络在时间上正确地传播其学习误差;时间上随机的dropout mask会破坏错误信号,不利于学习过程。 GRU + Dropout训练

1
2
3
4
5
6
7
8
9
10
11
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])))#dropout输入数据上应用;recurrent_dropout循环单元上应用
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个epochs,不再过度拟合。但是,尽管评估分数更稳定,但最佳分数并不比以前低很多。

循环网络层stack [堆叠]

网络模型不再过拟合,但特征表示能力成为新的瓶颈。可以增加网络模型的深度。回想一下通用机器学习工作流程的描述:增加网络容量通常是一个好主意,直到过度拟合成为主要障碍(假设已经采取基本步骤来缓解过度拟合,例如使用dropout)。 增加模型容量通常可以增加网络层神经元数目或者增加网络层数。循环层堆叠是构建更强大的循环网络的经典方法:例如,目前Google Translate背后就是七个大型LSTM层的堆栈。

要在Keras中将重复层叠加在彼此之上,所有中间层应返回其完整的输出序列(3D张量),而不是在最后一个时间步的输出,指定return_sequences = True。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

结果有所改善,但不太明显。

双向RNN

双向RNN是一种常见的RNN变体,可以在某些任务上提供比常规RNN更高的性能。它经常用于自然语言处理 - 你可以称之为自然语言处理深度学习方法中的“瑞士军刀”。 RNN特别依赖于顺序/时间:它们按顺序处理其输入序列的时间步长,改组或反转时间步长可以完全改变RNN从序列中提取的特征表示。双向RNN利用RNN的顺序敏感性:使用两个常规RNN,例如GRU和LSTM层,每个层在一个方向上处理输入序列(按时间顺序和反时间顺序),然后合并它们的特征表示。通过双向处理序列,双向RNN可以捕获单向RNN可能忽略的特征模式。

值得注意的是,RNN层按时间顺序处理序列(较早的时间步长)可能是一个随意的假设。至少,这是迄今为止没有试图提出质疑的决定。如果按照反时间顺序处理输入序列,RNN的表现是否足够好?但在自然语言处理中,理解句子中一个单词的意思并不依赖于在句子中的位置。在反向IMDB数据集上使用LSTM。 准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from keras.datasets import imdb
from keras.preprocessing import sequence
from keras import layers
from keras.models import Sequential

max_features = 10000
maxlen = 500

(x_train,y_train),(x_test,y_test)=imdb.load_data(num_words=max_features)

x_train = [x[::-1] for x in x_train]#翻转顺序
x_test = [x[::-1] for x in x_test]

x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)

model = Sequential()

model.add(layers.Embedding(max_features, 128))
model.add(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)

获得的性能几乎与按时间顺序的LSTM相同。值得注意的是,在这样的文本数据集中,逆序处理与时间顺序处理一样有效,证实了这样的假设:虽然词序在理解语言方面很重要,但使用的顺序并不重要。重要的是,在逆向序列上训练的RNN将比在原始序列上训练的RNN学习不同的特征表现形式。在机器学习中,不同但有用的表示总是值得利用,它们越不同越好:它们提供了一个新的查看数据的角度,捕获其他方法遗漏的数据的各个方面,可以帮助提高任务的性能。 双向RNN利用这一想法来改进按时间顺序的RNN的性能。它以两种方式查看其输入序列,获得可能更丰富的表示,并捕获仅由时间顺序版本遗漏的特征模式。 Keras中实现双向RNN需要使用Bidirectional网络层,接受一个循环网络层作为参数。Bidirectional网络层生成第二个相同的循环网络,其中一个网络层用来处理顺序输入数据,另一个处理逆序输入数据。 双向RNN训练

1
2
3
4
5
6
7
8
9
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)

表现比单向LSTM好一点。模型很快过拟合,双向参数是单向LSTM的两倍。

深度学习更像是一门艺术,而不是一门科学。可以提供指导,说明在特定问题上可能起作用或不合作的内容,但最终,每个问题都是独一无二的;你必须根据经验评估不同的策略。目前还没有任何理论可以提前告诉你应该采取哪些措施来最佳地解决问题。你必须迭代

小结

  • 在处理新问题时,最好先为选择的度量标准建立常识baseline。如果没有baseline可以击败,无法判断是否正在取得实际进展。[baseline参考物]
  • 在复杂模型之前尝试简单的模型,以证明额外的消耗。有时一个简单模型将成为最佳选择。
  • 当处理时序问题的数据时,循环网络非常适合。
  • 要将dropout与循环网络一起使用,应该使用时间常数drpoout mask和循环dropout mask。这些内置于Keras循环网络层中,因此所要做的就是使用循环网络层的dropout和recurrent_dropout参数。
  • 堆叠的RNN提供比单个RNN层更多的特征表示能力。但需要的计算能力也大大增加,并不值得。虽然他们在复杂问题(例如机器翻译)上效果更好,它们可能并不总是与更小,更简单的问题相关。
  • 双向RNN,它以两种方式查看序列,对自然语言处理问题很有用。当最近的过去数据比序列的开始数据提供更多信息时,表现情况并不理想。

卷积网络处理序列数据

1D卷积网络可以在某些序列处理问题上与RNN竞争,通常计算成本很低。最近,通常与扩张内核一起使用的1D convnets已经成功用于音频生成和机器翻译上。除了这些特定的成功之外,人们早就知道小型1D卷积网络可以为RNN提供快速替代方案,用于简单的任务,例如文本分类和时间序列预测。

序列数据上的1D卷积

2D卷积在每个小patch上进行卷积操作,和2D卷积类似,1D卷积在局部1D Patch(连续子序列)上进行卷积操作。

这样的一维卷积可以识别序列中的局部特征模式。因为对每个patch执行相同的输入变换,所以在句子中的某个位置处学习的模式稍后可以在不同的位置被识别,使得1D卷积平移不变(对于时间转换)。 例如,使用大小为5的卷积窗口的1D卷积处理字符序列应该能够学习长度为5或更小的单词或单词片段,并且它应该能够在输入序列的任何上下文中识别这些单词。

序列数据的1D池化 2D池化操作具有1D等效形式:从输入提取1D patch(子序列)并输出最大值(最大池化)或平均值(平均池化)。与2D convnets一样,这用于减少1D输入(子采样)的长度。

实现一维卷积 Keras中使用Conv1D网络层[和Conv2D网络层类似]。接收3D张量,形状(samples,time,features),返回相同形状的3D张量。卷积窗口是时间周上的1D卷口,输入张量的axis1。 使用Conv1D处理IMDB数据集 数据处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 (samples 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)

1D convnets的结构与2D对应方式相同:它们由一堆Conv1D和MaxPooling1D层组成,以全局池层或Flatten层结束[将3D输出转换为2D输出],允许将一个或多个Dense层添加到模型中以进行分类或回归。 一个不同之处在于,可以负担得起使用带有1D convnets的更大卷积窗口。对于2D卷积层,3×3卷积窗口包含3×3 = 9个特征向量;但是对于1D卷积层,大小为3的卷积窗口仅包含3个特征向量。因此,可以轻松使用尺寸为7或9的1D卷积窗口。 IMDB 1D卷积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.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上的运行时间更快(速度与确切配置有很大关系)。此时,可以重新训练此模型,epochs(8个)并在测试集上运行。这是一个令人信服的证明:一维卷积可以在字级情感分类任务中为循环网络提供快速,廉价的替代方案。

使用CNN和RNN处理长序列数据

由于1D convnets独立处理输入patch,因此它们对时间步长的顺序不敏感,这与RNN不同。当然,为了识别长期模式,可以堆叠许多卷积层和池化层,从而上层将看到原始输入的长块 - 但这仍然是诱导顺序敏感性的相当弱的方式。证明这一弱点的一种方法是在温度预测问题上尝试一维卷积,其中顺序敏感性是产生良好预测的关键。 天气数据上的1D卷积训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
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.4左右,比卷积baseline还差。这是因为convnet在输入时间序列中的所有地方查找模式,并且不知道它看到的模式的时间位置(朝向开头,朝向末尾,等等)。因为在这个特定预测问题的情况下,更新近的数据点应该与旧数据点的重要性不同,所以convnet无法产生有意义的结果。而IMDB数据,与正面或负面情绪相关联的关键字模式是独立于在输入句子中找到它们的位置的信息。

将convnet的速度和优点与RNN的顺序敏感性相结合的一种策略是使用1D convnet作为RNN之前的预处理步骤。当你处理特别长时间无法用RNN实际处理的序列时,这种方法是特别有用的,例如具有数千步的序列数据。convnet会将长输入序列转换为更短(下采样)的更高级别特征序列。抽取出来的特征序列作为RNN的输入数据。 在时间序列数据集上使用这种方法实验。 数据准备

1
2
3
4
5
6
7
8
9
10
11
12
13
step = 3
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

1D卷积+GRU网络层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.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网络好,但运行速度更快。数据集是原来的两倍,在这种情况下似乎没有太大的帮助,但对其他数据集可能很重要。

小结

  • 通常,1D convnets的结构非常类似于计算机视觉领域的2D卷积网络层:它们由Conv1D层和MaxPooling1D层组成,以全局池化操作或展平操作结束。
  • 由于RNN对于处理非常长的序列消耗非常昂贵,但是1D convnets相对较少,因此在RNN之前使用1D convnet作为预处理步骤,缩短序列并提取RNN处理的有用特征表示可能是个好主意

##

  • 可以将RNN用于时间序列回归(“预测未来”),时间序列分类,时间序列中的异常检测以及序列标记(例如识别句子中的名称或日期);
  • 可以使用1D convnets进行机器翻译(序列到序列卷积模型,如SliceNet),文档分类和拼写纠正;
  • 如果全局顺序对序列数据很重要,那么最好使用循环网络来处理它。时间序列通常就是这种情况,其中最近的过去可能比遥远的过去更具信息性。
  • 如果全局顺序没有根本意义,那么1D convnets同样有效并且消耗更低。这通常是文本数据的情况,其中在句子开头找到的关键字与在结尾处找到的关键字一样有意义。
您的支持就是我更新的最大动力!谢谢!