本节讲深度学习用于文本和序列
用于处理序列的两种基本的深度学习算法分别是循环神经网络(recurrent neural network)和一维卷积神经网络(1D convnet)
与其他所有神经网络一样,深度学习模型不会接收原始文本作为输入,它只能处理数值张量。文本向量化(vectorize)是指将文本转换为数值张量的过程。它有多种实现方法
- 将文本分割为单词,并将每个单词转换为一个向量
- 将文本分割为字符,并将每个字符转换为一个向量
- 提取单词或字符的 n-gram,并将每个 n-gram 转换为一个向量。n-gram 是多个连续单词或字符的集合(n-gram 之间可重叠)
将文本分解而成的单元(单词、字符或 n-gram)叫作标记(token),将文本分解成标记的过程叫作分词(tokenization)。所有文本向量化过程都是应用某种分词方案,然后将数值向量与生成的标记相关联。这些向量组合成序列张量,被输入到深度神经网络中
n-gram 是从一个句子中提取的 N 个(或更少)连续单词的集合。这一概念中的“单词”也可以替换为“字符”
The cat sat on the mat 分解为二元语法(2-gram)的集合
{"The", "The cat", "cat", "cat sat", "sat", "sat on", "on", "on the", "the", "the mat", "mat"}
分解为三元语法(3-gram)的集合
{"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-2-grams)及三元语法袋(bag-of-3-grams)。这里袋(bag)这一术语指的是,我们处理的是标记组成的集合。这一系列分词方法叫作词袋(bag-of-words)。词袋是一种不保存顺序的分词方法,因此它往往被用于浅层的语言处理模型,而不是深度学习模型
将向量与标记相关联的方法
对标记做 one-hot 编码(one-hot encoding)与标记嵌入[token embedding,通常只用于单词,叫作词嵌入(word embedding)]
one-hot 编码是将标记转换为向量的最常用、最基本的方法
它将每个单词与一个唯一的整数索引相关联,然后将这个整数索引 i 转换为长度为 N 的二进制向量(N 是词表大小),这个向量只有第 i 个元素是 1,其余元素都为 0 (也可以进行字符级的 one-hot 编码)
Keras one-hot编码Demo
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)
# 找回单词索引
word_index = tokenizer.word_index
print(word_index)
# 将字符串转换为整数索引组成的列表
sequences = tokenizer.texts_to_sequences(samples)
print("转换成的索引序列 ", sequences)
text = tokenizer.sequences_to_texts(sequences)
print("转会的文本 ", text)
# 得到 one-hot 二进制表示
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
one_num = 0
for items in one_hot_results:
for item in items:
if item == 1:
one_num += 1
print("1的数量为 ", one_num)
print(one_hot_results)
结果
one-hot 编码的一种变体是所谓的 one-hot 散列技巧(one-hot hashing trick),如果词表中唯
一标记的数量太大而无法直接处理,就可以使用这种技巧
将单词散列编码为固定长度的向量,通常用一个非常简单的散列函数来实现
这种方法的主要优点在于,它避免了维护一个显式的单词索引,从而节省内存并允许数据的在线编码,缺点就是可能会出现散列冲突
词嵌入
one-hot 编码得到的向量是二进制的、稀疏的、维度很高的(维度大小等于词表中的单词个数),而词嵌入是低维的浮点数向量。与 one-hot 编码得到的词向量不同,词嵌入是从数据中学习得到的。常见的词向量维度是 256、512 或 1024(处理非常大的词表时)。与此相对,onehot 编码的词向量维度通常为 20 000 或更高。因此,词向量可以将更多的信息塞入更低的维度中
获取词嵌入有两种方法
- 在完成主任务(比如文档分类或情感预测)的同时学习词嵌入。在这种情况下,一开始是随机的词向量,然后对这些词向量进行学习,其学习方式与学习神经网络的权重相同
- 在不同于待解决问题的机器学习任务上预计算好词嵌入,然后将其加载到模型中。这些词嵌入叫作预训练词嵌入(pretrained word embedding)
利用 Embedding 层学习词嵌入
词嵌入的作用应该是将人类的语言映射到几何空间中,我们希望任意两个词向量之间的几何距离)应该和这两个词的语义距离有关。可能还希望嵌入空间中的特定方向也是有意义的
Embedding 层的输入是一个二维整数张量,其形状为 (samples, sequence_length),它能够嵌入长度可变的序列,不过一批数据中的所有序列必须具有相同的长度
简单Demo
from keras.datasets import imdb
from keras import preprocessing
from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding
import matplotlib.pyplot as plt
max_features = 10000
maxlen = 20
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features, path='E:\\study\\dataset\\imdb.npz')
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)
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'])
model.summary()
history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_split=0.2)
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()
结果
当可用的训练数据很少,以至于只用手头数据无法学习适合特定任务的词嵌入,你可以从预计算的嵌入空间中加载嵌入向量,而不是在解决问题的同时学习词嵌入。有许多预计算的词嵌入数据库,你都可以下载并在 Keras 的 Embedding 层中使用,word2vec 就是其中之一。另一个常用的是 GloVe(global vectors for word representation,词表示全局向量)
没有足够的数据来自己学习真正强大的特征,但你需要的特征应该是非常通用的,比如常见的视觉特征或语义特征
新闻情感分类Demo,使用GloVe预训练词
import os
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np
from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense
import matplotlib.pyplot as plt
imdb_dir = 'E:\\study\\dataset\\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)
# 对 IMDB 原始数据的文本进行分词
maxlen = 100
training_samples = 200
validation_samples = 10000
max_words = 10000
tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)
word_index = tokenizer.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]
# 解析 GloVe 词嵌入文件
glove_dir = 'E:\\study\\models\\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))
# 准备 GloVe 词嵌入矩阵(max_words, embedding_dim)
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
# 模型定义
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()
# 将预训练的词嵌入加载到 Embedding 层中,并冻结
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False
# 训练与评估
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')
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()
# 对测试集数据进行分词
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)
数据下的时间太长放弃了,233