TensorFlow深度学习实战——利用词嵌入实现垃圾邮件检测

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_scoreconfusion_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,574SMS 记录,其中 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 表示。使用 TensorFlowtokenizer 将每个 SMS 文本转换为单词序列,然后使用 tokenizerfit_on_texts() 方法创建词汇表;然后,使用 texts_to_sequences()SMS 消息转换为整数序列。最后,由于网络只能处理固定长度的整数序列,调用 pad_sequences() 函数用零填充较短的 SMS 消息;数据集中最长的 SMS 消息有 189tokens (词元)。在许多应用程序中,可能存在一些异常长的序列,可以通过设置 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 万词向量),提供 50d100d200d300d 的向量 (glove-wiki-gigaword-50, glove-wiki-gigaword-100, glove-wiki-gigaword-200, glove-wiki-gigaword-300),另一种是在 Twitter 上训练的(基于 270 亿词元的 120 万词向量),提供 25d50d100d200d 的向量 (glove-twitter-25, glove-twitter-50, glove-twitter-100, glove-twitter-200),较小的嵌入尺寸能够更大程度的压缩输入,因而近似程度也更高
  • fastText:基于 Wikipedia 2017UMBC 网络语料库和 statmt.org 新闻数据集( 160 亿词元)训练的带有子词信息的 100 万词向量 (fastText-wiki-news-subwords-300)
  • ConceptNet Numberbatch:一种集成嵌入,利用 ConceptNet 语义网络、释义数据库 (paraphrase database, PPDB)、Word2VecGloVe 作为输入,生成 600 维的词向量

(1) 在本节中,我们选择在 Gigaword 语料库上训练的 300GloVe 嵌入。为了保持模型的规模较小,我们只希望考虑存在于词汇表中的单词的嵌入,为词汇表中的每个单词创建一个较小的嵌入矩阵。矩阵中的每一行对应一个单词,而该行本身则是该单词对应的嵌入向量:

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() 调用。训练 3epochs 后,评估模型在测试集上的表现,并打印模型在测试集上的准确率和混淆矩阵。然而,对于不平衡数据,即使使用了类别权重,模型仍然可能会总是预测占多数的类别。因此,通常根据每个类别报告准确率,以确保模型有效地学习区分每个类别,这可以通过混淆矩阵轻松完成,通过将每行的对角元素除以该行所有元素的总和,其中每行对应一个标签类别:

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]

由于数据集较小,且模型相对简单。我们仅需进行少量训练( 3epochs),便能取得非常好的结果(验证集准确率高达 90% 以上)。在以上三种情况下,网络都取得了优异的准确率,准确预测了 1,111 条非垃圾邮件和 169 个垃圾邮件。

下图显示了三种情况下模型训练过程中验证准确率变化的差异:

从零开始学习嵌入的情况下,第一个 epoch 结束时,验证准确率为 0.93,在接下来的两个 epochs 中,提升至 0.98。使用固定的预训练嵌入的情况下,网络从预训练嵌入中获得了一定的优势,在第一个 epoch 结束时达到了 0.95 的验证准确率,然而,由于嵌入权重不允许改变,它不能调整嵌入适应垃圾短信检测任务,在第 3epoch 结束时,其验证准确率是三种情况中最低的。进行微调的情况与使用固定的预训练嵌入的情况类似,同样具有起始优势,但可以调整嵌入以适应任务,因此在三种情况中学习速度最快。进行微调的情况在第 1epoch 结束时具有最高的验证准确率,并在第 2epoch 结束时就达到了与从零开始学习嵌入的情况在第 3epoch 结束时相同的验证准确率。

相关链接

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)------词嵌入技术详解

相关推荐
梦姐的编程日志几秒前
从研究动机视角对无监督图像去雾论文的分类
图像处理·人工智能·深度学习·算法·计算机视觉
Y1nhl3 分钟前
搜广推校招面经六十二
人工智能·pytorch·python·算法·机器学习·推荐算法·搜索算法
进取星辰40 分钟前
PyTorch 深度学习实战(28):对比学习(Contrastive Learning)与自监督表示学习
人工智能·深度学习
阿珊和她的猫43 分钟前
AIGC 与 Agentic AI:生成式智能与代理式智能的技术分野与协同演进
人工智能·aigc
飞凌嵌入式44 分钟前
从DeepSeek到Qwen,AI大模型的移植与交互实战指南
人工智能·aigc·嵌入式
不吃香菜?1 小时前
OpenCV图像处理基础到进阶之高阶操作
图像处理·人工智能·opencv
GiantGo1 小时前
深度学习中常见的专业术语汇总
深度学习·名称解释
沐雪架构师1 小时前
LLaMA Factory微调后的大模型在vLLM框架中对齐对话模版
人工智能
不吃香菜?1 小时前
opencv图像处理之指纹验证
人工智能·opencv·计算机视觉
AIGC大时代1 小时前
DeepSeek学术仿写过程中如何拆解框架?
人工智能·chatgpt·智能写作·deepseek·aiwritepaper