TensorFlow深度学习实战------利用词嵌入实现垃圾邮件检测
-
- [0. 前言](#0. 前言)
- [1. 构建垃圾邮件检测模型](#1. 构建垃圾邮件检测模型)
-
- [1.1 模型分析](#1.1 模型分析)
- [1.2 获取数据](#1.2 获取数据)
- [1.3 构建嵌入矩阵](#1.3 构建嵌入矩阵)
- [1.4 定义垃圾邮件分类器](#1.4 定义垃圾邮件分类器)
- [2. 训练并评估模型](#2. 训练并评估模型)
- [3. 运行垃圾邮件检测器](#3. 运行垃圾邮件检测器)
- 相关链接
0. 前言
由于大型语料库生成的各种强大嵌入的广泛适用性,使用这些嵌入将文本输入转换为机器学习模型的输入逐渐变成普遍操作。文本可以视为一系列词元 (tokens
),嵌入能够将每个 token
转换为一个密集的固定维度向量。每个 token
都替换为向量,从而将文本序列转换为样本矩阵,每个样本都有固定数量的特征,对应于嵌入的维度。
样本矩阵可以直接用作标准机器学习程序的输入,在本节中,我们将介绍如何在一维卷积神经网络 (Convolutional Neural Network
, CNN
) 中使用该矩阵,实现垃圾邮件检测器将短信 (Short Message Service
, SMS
) 或文本消息分类为非垃圾短信 (ham
) 或垃圾短信 (spam
)。
1. 构建垃圾邮件检测模型
1.1 模型分析
在本节中,我们首先将从零开始学习构建用于垃圾邮件检测任务的嵌入。接下来,介绍如何使用预训练嵌入,类似于计算机视觉中的迁移学习过程。最后,学习如何结合这两种方法,从预训练嵌入开始,网络以此为起点学习自定义嵌入,此过程类似于计算机视觉中的微调。
(1) 首先,导入所需库,并定义超参数:
python
import argparse
import gensim.downloader as api
import numpy as np
import os
import shutil
import tensorflow as tf
from sklearn.metrics import accuracy_score, confusion_matrix
DATA_DIR = "data"
EMBEDDING_NUMPY_FILE = os.path.join(DATA_DIR, "E.npy")
DATASET_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip"
EMBEDDING_MODEL = "glove-wiki-gigaword-300"
EMBEDDING_DIM = 300
NUM_CLASSES = 2
BATCH_SIZE = 128
NUM_EPOCHS = 3
CLASS_WEIGHTS = { 0: 1, 1: 8 }
Scikit-learn
是一个开源的 Python
机器学习工具包,包含许多高效且易于使用的工具,用于数据挖掘和数据分析。在本节中,我们使用两个预定义指标,accuracy_score
和 confusion_matrix
,评估训练后的模型。
1.2 获取数据
模型训练使用公开数据集,来自 UCI
机器学习库中的 SMS
垃圾短信数据集。
(1) 下载文件并解析,生成一组 SMS
消息及其对应的标签列表:
python
def download_and_read(url):
local_file = url.split('/')[-1]
p = tf.keras.utils.get_file(local_file, url, extract=True, cache_dir=".")
labels, texts = [], []
local_file = os.path.join("datasets", "SMSSpamCollection")
with open(local_file, "r") as fin:
for line in fin:
label, text = line.strip().split('\t')
labels.append(1 if label == "spam" else 0)
texts.append(text)
return texts, labels
texts, labels = download_and_read(DATASET_URL)
(2) 数据集包含 5,574
条 SMS
记录,其中 747
条标记为 spam
(垃圾短信),其余 4,827
条标记为 ham
(非垃圾短信)。SMS
记录的文本存储在 texts
变量中,相应的数值标签 (0=ham
, 1=spam
) 存储在 labels
变量中:
python
cat_labels = tf.keras.utils.to_categorical(labels, num_classes=NUM_CLASSES)
(3) 接下来预处理数据,以便网络可以使用。SMS
文本需要以整数序列的形式输入到网络中,其中每个单词由其在词汇表中的对应 ID
表示。使用 TensorFlow
的 tokenizer
将每个 SMS
文本转换为单词序列,然后使用 tokenizer
的 fit_on_texts()
方法创建词汇表;然后,使用 texts_to_sequences()
将 SMS
消息转换为整数序列。最后,由于网络只能处理固定长度的整数序列,调用 pad_sequences()
函数用零填充较短的 SMS
消息;数据集中最长的 SMS
消息有 189
个 tokens
(词元)。在许多应用程序中,可能存在一些异常长的序列,可以通过设置 maxlen
参数限制序列长度。使用这种方式,长度超过 maxlen
个词元的句子将被截断,长度不足 maxlen
个词元的句子将被填充:
python
# tokenize and pad text
tokenizer = tf.keras.preprocessing.text.Tokenizer()
tokenizer.fit_on_texts(texts)
text_sequences = tokenizer.texts_to_sequences(texts)
text_sequences = tf.keras.preprocessing.sequence.pad_sequences(text_sequences)
num_records = len(text_sequences)
max_seqlen = len(text_sequences[0])
print("{:d} sentences, max length: {:d}".format(num_records, max_seqlen))
(4) 将标签转换为独热编码格式,用于损失函数(分类交叉熵)的计算:
python
cat_labels = tf.keras.utils.to_categorical(labels, num_classes=NUM_CLASSES)
(5) tokenizer
通过 word_index
属性提供对创建的词汇表的访问,该属性实际上是一个词汇表单词及其在词汇表中索引位置的字典。我们还构建了反向索引,以便从索引位置找到对应的单词。需要注意的是,除了词汇表中单词外,我们还为填充字符 (PAD
) 创建了键值对:
python
word2idx = tokenizer.word_index
idx2word = {v:k for k, v in word2idx.items()}
word2idx["PAD"] = 0
idx2word[0] = "PAD"
vocab_size = len(word2idx)
print("vocab size: {:d}".format(vocab_size))
(6) 最后,创建数据集对象,以供网络使用。数据集对象可以设置一些属性,例如批大小等。我们使用填充后的整数序列和分类标签构建数据集,打乱数据,将其分割为训练、验证和测试集,并为这三个数据集设置批大小:
python
dataset = tf.data.Dataset.from_tensor_slices((text_sequences, cat_labels))
dataset = dataset.shuffle(10000)
test_size = num_records // 4
val_size = (num_records - test_size) // 10
test_dataset = dataset.take(test_size)
val_dataset = dataset.skip(test_size).take(val_size)
train_dataset = dataset.skip(test_size + val_size)
test_dataset = test_dataset.batch(BATCH_SIZE, drop_remainder=True)
val_dataset = val_dataset.batch(BATCH_SIZE, drop_remainder=True)
train_dataset = train_dataset.batch(BATCH_SIZE, drop_remainder=True)
1.3 构建嵌入矩阵
Gensim
库提供了访问多种预训练嵌入模型的方式,通过运行以下命令查看:
shell
>>> import gensim.downloader as api
>>> api.info()['models'].keys()
dict_keys(['fasttext-wiki-news-subwords-300', 'conceptnet-numberbatch-17-06-300', 'word2vec-ruscorpora-300', 'word2vec-google-news-300', 'glove-wiki-gigaword-50', 'glove-wiki-gigaword-100', 'glove-wiki-gigaword-200', 'glove-wiki-gigaword-300', 'glove-twitter-25', 'glove-twitter-50', 'glove-twitter-100', 'glove-twitter-200', '__testing_word2vec-matrix-synopsis'])
Gensim
库中常用的预训练词嵌入模型包括:
Word2Vec
:包含两个版本 (word2vec-ruscorpora-300
,word2vec-google-news-300
),一种是在Google
新闻上训练的(基于30
亿词元的300
万词向量),另一种是在俄罗斯语料库上训练的GloVe
:包含两个版本,一种是在Gigawords
语料库上训练的(基于60
亿词元的40
万词向量),提供50d
、100d
、200d
和300d
的向量 (glove-wiki-gigaword-50
,glove-wiki-gigaword-100
,glove-wiki-gigaword-200
,glove-wiki-gigaword-300
),另一种是在Twitter
上训练的(基于270
亿词元的120
万词向量),提供25d
、50d
、100d
和200d
的向量 (glove-twitter-25
,glove-twitter-50
,glove-twitter-100
,glove-twitter-200
),较小的嵌入尺寸能够更大程度的压缩输入,因而近似程度也更高fastText
:基于Wikipedia 2017
、UMBC
网络语料库和statmt.org
新闻数据集(160
亿词元)训练的带有子词信息的100
万词向量 (fastText-wiki-news-subwords-300
)ConceptNet Numberbatch
:一种集成嵌入,利用ConceptNet
语义网络、释义数据库 (paraphrase database
,PPDB
)、Word2Vec
和GloVe
作为输入,生成600
维的词向量
(1) 在本节中,我们选择在 Gigaword
语料库上训练的 300
维 GloVe
嵌入。为了保持模型的规模较小,我们只希望考虑存在于词汇表中的单词的嵌入,为词汇表中的每个单词创建一个较小的嵌入矩阵。矩阵中的每一行对应一个单词,而该行本身则是该单词对应的嵌入向量:
python
def build_embedding_matrix(sequences, word2idx, embedding_dim,
embedding_file):
if os.path.exists(embedding_file):
E = np.load(embedding_file)
else:
vocab_size = len(word2idx)
E = np.zeros((vocab_size, embedding_dim))
word_vectors = api.load(EMBEDDING_MODEL)
for word, idx in word2idx.items():
try:
E[idx] = word_vectors.word_vec(word)
except KeyError: # word not in embedding
pass
# except IndexError: # UNKs are mapped to seq over VOCAB_SIZE as well as 1
# pass
np.save(embedding_file, E)
return E
E = build_embedding_matrix(text_sequences, word2idx, EMBEDDING_DIM,
EMBEDDING_NUMPY_FILE)
print("Embedding matrix:", E.shape)
Embedding matrix: (9010, 300)
输出嵌入矩阵的形状为(9010,300),对应于词汇表中的9,010个词元,且预训练GloVe嵌入中有300个特征。
1.4 定义垃圾邮件分类器
接下来,使用一维卷积神经网络 (Convolutional Neural Network
, CNN
) 定义垃圾邮件分类器。
输入是一个整数序列。第一层是嵌入层,将每个输入序列转换为大小为 (embedding_dim
) 的向量。根据运行模式的不同(从零开始学习嵌入,进行迁移学习,还是进行微调),网络中的嵌入层会略有不同。当网络从随机初始化的嵌入权重开始学习 (run_mode == "scratch"
) 时,我们将 trainable
参数设置为 True
。在迁移学习的情况下 (run_mode == "vectorizer"
),设置权重来自于外部嵌入矩阵 E
,将 trainable
参数设置为 False
,因此它不会进行训练。在微调情况下 (run_mode == "finetuning"
),设置嵌入权重来自外部矩阵 E
,并将 trainable
参数设置为 True
。
嵌入层的输出送入卷积层,固定大小为 3
个词元的一维窗口 (kernel_size=3
),也称时间步,使用 256
个卷积核执行 num_filters=256
卷积,以产生每个时间步大小为 256
的向量。因此,输出向量的形状为 (batch_size,time_steps,num_filters)
。
卷积层的输出传递到 1D
空间 dropout
层。空间 dropout
将随机丢弃卷积层输出的整个特征图,这是一种防止过拟合的正则化技术。然后通过全局最大池层,该层从每个时间步的每个卷积核中取最大值,得到形状为 (batch_size,num_filters)
的向量。
dropout
层的输出送入池化层进行展平后,送入一个全连接层,将形状为 (batch_size,num_filters)
的向量转换为 (batch_size,num_classes)
。softmax
激活将(垃圾邮件,非垃圾邮件)的分数转换为概率分布,得到输入邮件为垃圾邮件或非垃圾邮件的概率。
python
class SpamClassifierModel(tf.keras.Model):
def __init__(self, vocab_sz, embed_sz, input_length,
num_filters, kernel_sz, output_sz,
run_mode, embedding_weights,
**kwargs):
super(SpamClassifierModel, self).__init__(**kwargs)
if run_mode == "scratch":
self.embedding = tf.keras.layers.Embedding(vocab_sz,
embed_sz,
input_length=input_length,
trainable=True)
elif run_mode == "vectorizer":
self.embedding = tf.keras.layers.Embedding(vocab_sz,
embed_sz,
input_length=input_length,
weights=[embedding_weights],
trainable=False)
else:
self.embedding = tf.keras.layers.Embedding(vocab_sz,
embed_sz,
input_length=input_length,
weights=[embedding_weights],
trainable=True)
self.dropout = tf.keras.layers.SpatialDropout1D(0.2)
self.conv = tf.keras.layers.Conv1D(filters=num_filters,
kernel_size=kernel_sz,
activation="relu")
self.pool = tf.keras.layers.GlobalMaxPooling1D()
self.dense = tf.keras.layers.Dense(output_sz,
activation="softmax"
)
def call(self, x):
x = self.embedding(x)
x = self.dropout(x)
x = self.conv(x)
x = self.pool(x)
x = self.dense(x)
return x
# model definition
conv_num_filters = 256
conv_kernel_size = 3
model = SpamClassifierModel(
vocab_size, EMBEDDING_DIM, max_seqlen,
conv_num_filters, conv_kernel_size, NUM_CLASSES,
run_mode, E)
model.build(input_shape=(None, max_seqlen))
model.summary()

最后,使用分类交叉熵损失函数和 Adam
优化器编译模型:
python
model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
2. 训练并评估模型
需要注意的是,本节所用的数据集是不平衡的,垃圾短信样本仅有 747
个,而非垃圾短信有 4,827
个。网络如果总是预测占多数的类别,就可以达到接近 87%
的准确率。为了解决这个问题,我们设置类别权重,表明垃圾短信的错误成本是非垃圾短信的 8
倍,这可以通过 CLASS_WEIGHTS
变量实现,并作为额外参数传递给 model.fit()
调用。训练 3
个 epochs
后,评估模型在测试集上的表现,并打印模型在测试集上的准确率和混淆矩阵。然而,对于不平衡数据,即使使用了类别权重,模型仍然可能会总是预测占多数的类别。因此,通常根据每个类别报告准确率,以确保模型有效地学习区分每个类别,这可以通过混淆矩阵轻松完成,通过将每行的对角元素除以该行所有元素的总和,其中每行对应一个标签类别:
python
# train model
model.fit(train_dataset, epochs=NUM_EPOCHS,
validation_data=val_dataset,
class_weight=CLASS_WEIGHTS)
# evaluate against test set
labels, predictions = [], []
for Xtest, Ytest in test_dataset:
Ytest_ = model.predict_on_batch(Xtest)
ytest = np.argmax(Ytest, axis=1)
ytest_ = np.argmax(Ytest_, axis=1)
labels.extend(ytest.tolist())
predictions.extend(ytest.tolist())
print("test accuracy: {:.3f}".format(accuracy_score(labels, predictions)))
print("confusion matrix")
print(confusion_matrix(labels, predictions))
3. 运行垃圾邮件检测器
接下来,我们研究的以下三种情况:
- 网络从零开始学习嵌入
- 使用固定的预训练嵌入,其中嵌入矩阵视为向量化器,将整数序列转换为向量序列
- 从预训练嵌入开始,在训练期间进一步进行微调以适应任务
通过设置 mode
参数的值可以评估每种情况:
shell
$ python spam_classifier --mode [scratch|vectorizer|finetuning]
由于数据集较小,且模型相对简单。我们仅需进行少量训练( 3
个 epochs
),便能取得非常好的结果(验证集准确率高达 90%
以上)。在以上三种情况下,网络都取得了优异的准确率,准确预测了 1,111
条非垃圾邮件和 169
个垃圾邮件。
下图显示了三种情况下模型训练过程中验证准确率变化的差异:

从零开始学习嵌入的情况下,第一个 epoch
结束时,验证准确率为 0.93
,在接下来的两个 epochs
中,提升至 0.98
。使用固定的预训练嵌入的情况下,网络从预训练嵌入中获得了一定的优势,在第一个 epoch
结束时达到了 0.95
的验证准确率,然而,由于嵌入权重不允许改变,它不能调整嵌入适应垃圾短信检测任务,在第 3
个 epoch
结束时,其验证准确率是三种情况中最低的。进行微调的情况与使用固定的预训练嵌入的情况类似,同样具有起始优势,但可以调整嵌入以适应任务,因此在三种情况中学习速度最快。进行微调的情况在第 1
个 epoch
结束时具有最高的验证准确率,并在第 2
个 epoch
结束时就达到了与从零开始学习嵌入的情况在第 3
个 epoch
结束时相同的验证准确率。
相关链接
TensorFlow深度学习实战(1)------神经网络与模型训练过程详解
TensorFlow深度学习实战(2)------使用TensorFlow构建神经网络
TensorFlow深度学习实战(3)------深度学习中常用激活函数详解
TensorFlow深度学习实战(4)------正则化技术详解
TensorFlow深度学习实战(5)------神经网络性能优化技术详解
TensorFlow深度学习实战(6)------回归分析详解
TensorFlow深度学习实战(7)------分类任务详解
TensorFlow深度学习实战(8)------卷积神经网络
TensorFlow深度学习实战(9)------构建VGG模型实现图像分类
TensorFlow深度学习实战(10)------迁移学习详解
TensorFlow深度学习实战(11)------风格迁移详解
TensorFlow深度学习实战(12)------词嵌入技术详解