公开笔记

Embedding

覆盖文本向量化全流程技术,包含传统统计类向量化(BOW 词袋模型、TF-IDF)、经典深度学习词嵌入(Word2Vec、GloVe)、预训练嵌入模型(BERT),详解各技术原理、优缺点、实现工具、代码实现与应用场景

发布于 更新于

BOW

把文本看作 “一堆词的集合”,只统计词的出现频率,完全忽略词的顺序、语法、语义。CountVectorizer(纯词频)、TfidfVectorizer(带权重的词频)都属于 BOW 体系。

创建 BOW 的步骤:

  1. 第一步,将文本分词成句子。
  2. 第一步中分词的句子又进一步分词了单词。
  3. 删除所有停用词或标点符号。
  4. 将所有单词转换为小写。
  5. 创建词语的频率分布图。

TF

为了更好地理解文本向量化的基础逻辑,我们可以结合词袋模型(BOW)的例子辅助理解(词袋模型是早期的文本向量化方法,常被用作基础对比):

假设我们有 4 条灾难相关的推文文本:

  • ‘kind true sadly’
  • ‘swear jam set world ablaze’
  • ‘swear true car accident’
  • ‘car sadly car caught up fire’

我们可以用 scikit-learn 中的 CountVectorizer 工具实现词袋模型:它会自动对文本进行分词、构建全局词汇表,再将每条文本转换为 “词频向量”—— 向量的每一位对应词汇表中的一个词,值则是该词在当前文本中的出现次数。

Embedding_1767781039022

最终输出的词频矩阵结构为:

  • 行:对应每条文档(这里是 4 条推文);
  • 列:对应全局词汇表中的所有唯一词;
  • 值:对应词在当前文档中的出现次数。

所以,词袋模型的本质是构建全局词汇表 → 把每条文本映射为 “词频向量”。

类似地,我们可以将 CountVectorizer 应用到更大规模的训练数据(比如 11370 条推文),得到对应的词频矩阵,再结合目标变量训练后续的机器学习 / 深度学习模型。(CountVectorizer 处理后的词频矩阵是模型的 “输入特征”,目标变量是模型的 “学习目标”,二者结合才能完成分类 / 回归任务)

TF-IDF

TF-IDF 词频 - 逆文档频率,基于统计方法,用于查找文本中词语的相关性,文本可以是单个文档,也可以是称为语料库的各种文档。一个词在当前文档中出现得越频繁(TF 高),且在所有文档中出现得越稀少(IDF 高),这个词对当前文档的重要性 / 代表性就越高。

TF-IDF 算法可应用于解决较为简单的自然语言处理和机器学习问题,例如信息检索、停用词去除、关键词提取和基础文本分析。然而,它无法有效地捕捉词序列中的语义含义。

为了创建 TF-IDF 向量,可以使用 Scikit-learn 的 TF-IDF Vectorizer(TfidfVectorizer)。行代表每个文档,列代表词汇表,tf-idf(i,j) 的值通过上述公式计算得出。所得矩阵可与目标变量一起用于训练机器学习/深度学习模型。

Embedding_1767778631320

词袋模型与 TF-IDF 面临的挑战

在词袋模型(BOW)中,向量的维度等于词汇表的规模 —— 若向量中大部分元素为 0,最终会形成一个稀疏矩阵。这种稀疏性会从计算效率和信息利用两方面,增加模型构建的难度。

此外,词袋模型还有两个核心缺陷:一是无法体现词与词之间的语义关联,二是完全忽略词的顺序。这些问题也让后续的词嵌入技术面临更多挑战,具体表现为:

  • 参数规模过大:高维的输入向量会导致神经网络需要学习的权重参数数量剧增;
  • 语义与语序缺失:既无法捕捉词之间的关联,也不考虑词在文本中的出现顺序;
  • 计算成本高昂:参数规模越大,模型的训练与推理过程会消耗更多计算资源。

而 TF-IDF 模型虽然能区分词的重要程度,但并没有解决 “高维稀疏” 的核心问题;同时和词袋模型一样,它也无法捕捉词之间的语义相似性。

Word2Vec

Word2Vec 是谷歌于 2013 年提出的技术,如今已成为解决各类高级自然语言处理(NLP)任务的基础工具之一,其核心作用是训练词嵌入(Word Embedding),而技术底层则基于 “分布假设”。

在这一假设的框架下,Word2Vec 提供了两种实现模型:跳字模型(Skip-gram)与连续词袋模型(CBOW)。

这两种模型本质上是结构简单的浅层神经网络,仅包含输入层、输出层与投影层。它们的核心逻辑是:通过捕捉文本中 “历史与未来的词序信息”,来还原词语所处的语言上下文。

具体训练过程中,模型会迭代遍历大规模文本语料库,学习词语之间的关联关系 —— 其核心假设是:文本中相邻出现的词语,往往具有语义相似性。借助这一特性,Word2Vec 能将语义相近的词语,映射到向量空间中距离较近的嵌入向量上。

在衡量词向量的语义相似度时,Word2Vec 通常采用余弦相似度作为度量指标。余弦相似度的取值等于两个词(或文档)向量之间夹角的余弦值,其含义可简单理解为:

  • 若余弦值为 1,说明两个词语的向量完全重合,语义高度一致;
  • 若夹角为 90°(余弦值为 0),则说明两个词语在上下文语义上没有关联,彼此独立。

简言之,Word2Vec 的核心价值,就是让语义相似的词语,拥有相似的向量表示。

CBOW

词嵌入/连续词袋模型(CBOW)的核心逻辑是:通过多个上下文词作为输入,让神经网络预测中间的目标词。

CBOW 的优势是训练速度快,尤其适合为高频词学习更精准的向量表示。我们可以结合 “窗口大小” 的概念,理解 CBOW 中 “上下文” 与 “当前词” 的关系:

Embedding_1767780611787

在 CBOW 算法中,我们会设定一个 “窗口大小”(如图中窗口大小为 5):窗口中间的词是 “当前词”,其周围(包含前后)的词则构成 “上下文”。CBOW 的任务就是利用这些上下文词,预测出中间的当前词。

具体处理时,每个词会先通过独热编码 (One Hot Encoding) 映射到词汇表对应的编码向量,再输入 CBOW 神经网络。

Embedding_1767780725108

CBOW 的网络结构非常简洁:

  • 输入层:接收上下文词的独热编码向量;
  • 投影层:将所有上下文词的向量做求和(或平均)处理;
  • 输出层:基于词汇表,输出 “当前词” 的概率分布(即预测当前词是什么)。 其中,投影层本质是一个全连接的密集层,负责将输入的上下文信息整合后传递给输出层。

Skip-gram

一种与 CBOW 略有不同的词嵌入技术,因为它不基于上下文预测当前词。相反,它将每个当前词作为输入,输入到一个对数线性分类器和一个连续投影层中。这样,它就能预测当前词前后一定范围内的词。

这种变体仅以一个词作为输入,然后预测与其密切相关的上下文词。这就是它能够高效表示罕见词的原因。

Embedding_1767782527036

代码演示

Word2Vec 的两种核心变体(跳字模型与连续词袋模型),最终目标都是学习模型隐藏层的权重参数 —— 而隐藏层的输出结果,就是我们需要的词嵌入向量。

接下来,我们通过代码演示如何用 Word2Vec 构建自定义词嵌入。

  • 依赖库
Import Libraries
from gensim.models import Word2Vec
import nltk
import re
from nltk.corpus import stopwords
  • 文本预处理 Word2Vec 的输入需要是 “已分词的文本语料”,因此我们先对原始文本做预处理:
#Word2Vec inputs a corpus of documents split into constituent words.
corpus = []
for i in range(0,len(X)):
 tweet = re.sub(“[^a-zA-Z]”,” “,X[i])
 tweet = tweet.lower()
 tweet = tweet.split()
 corpus.append(tweet)

训练完成后,我们可以通过 “查找相似词” 来验证词嵌入的效果(相似词的向量距离更近):

# 查找与“disaster(灾难)”语义最相似的词
model.wv.most_similar('disaster')

执行上述代码后,会得到与 “disaster” 相似的词列表(包含词与对应的相似度):

[
    ('alert', 0.9997072219848633),
    ('military', 0.999704270553589),
    ('murder', 0.9996907501220703),
    ('ash', 0.9996492862701416),
    ('base', 0.9996055364687655),
    ('oil', 0.9996046423912048),
    ('along', 0.99959397315979),
    ('based', 0.9995934963226318),
    ('inundation', 0.9995468854904175),
    ('west', 0.9995380640029907)
]

以 “灾难” 对应的词嵌入向量为例,其输出是一个 100 维的稠密向量(维度由模型训练时的 vector_size 参数指定):

array([-0.5968882, -0.33868956, -0.32643065, ..., 0.68051434, 0.7790246, 0.5617202], dtype=float32)
  • 完整代码
# ===================== 第一步:导入依赖库 =====================
from gensim.models import Word2Vec
import nltk
import re
import pandas as pd
from nltk.corpus import stopwords

# 下载nltk停用词(首次运行需要,后续可注释)
nltk.download('stopwords')

# ===================== 第二步:准备指定的示例数据 =====================
# 替换为你指定的4条文本
data = {
    "text": [
        'kind true sadly',
        'swear jam set world ablaze',
        'swear true car accident',
        'car sadly car caught up fire'
    ],
    "label": [0, 0, 1, 1]  # 模拟标签:0=非灾难,1=灾难相关(仅示例)
}
df = pd.DataFrame(data)
X = df["text"].values  # 文本数据
y = df["label"].values  # 目标变量(训练Word2Vec无需标签,仅用于后续参考)

# ===================== 第三步:文本预处理 =====================
# 初始化停用词表(英文)
stop_words = set(stopwords.words('english'))
corpus = []  # 存储预处理后的分词语料

for i in range(len(X)):
    # 1. 移除非字母字符(保留空格)
    tweet = re.sub("[^a-zA-Z]", " ", X[i])
    # 2. 统一转为小写
    tweet = tweet.lower()
    # 3. 按空格分词
    tweet = tweet.split()
    # 4. 过滤停用词(当前示例无停用词,可保留逻辑)
    tweet = [word for word in tweet if not word in stop_words]
    # 5. 加入语料库
    corpus.append(tweet)

# 打印预处理结果,验证效果
print("=== 预处理后的分词语料 ===")
for i, doc in enumerate(corpus):
    print(f"文本{i+1}{doc}")

# ===================== 第四步:训练Word2Vec模型 =====================
# 初始化并训练Word2Vec
model = Word2Vec(
    sentences=corpus,        # 输入语料(分词后的列表)
    vector_size=100,         # 词嵌入向量维度
    window=3,                # 适配短文本,缩小窗口大小(原5→3)
    min_count=1,             # 最小词频:保留所有词(示例文本词频低)
    sg=0,                    # 模型类型:0=CBOW,1=Skip-gram
    hs=0,                    # 分层softmax:0=关闭
    negative=5,              # 负采样数量
    workers=4,               # 线程数
    epochs=100               # 训练轮数
)

# 保存模型(可选)
model.save("custom_word2vec.model")

# ===================== 第五步:验证词嵌入效果 =====================
print("\n=== 词嵌入效果验证 ===")

# 1. 查找与"accident"(事故)语义相似的词
similar_words = model.wv.most_similar('accident', topn=5)
print(f"\n与'accident'最相似的词:")
for word, similarity in similar_words:
    print(f"  {word}: {similarity:.4f}")

# 2. 获取单个词的嵌入向量(如"fire")
print(f"\n'fire'的词嵌入向量(前10维):")
fire_vec = model.wv["fire"]
print(fire_vec[:10])  # 打印前10维,完整向量是100维

# 3. 计算两个词的语义相似度(如"car"和"accident")
similarity = model.wv.similarity('car', 'accident')
print(f"\n'car'与'accident'的相似度:{similarity:.4f}")

# 4. 额外验证:"sadly"和"fire"的相似度(灾难相关关联)
similarity_sadly_fire = model.wv.similarity('sadly', 'fire')
print(f"'sadly'与'fire'的相似度:{similarity_sadly_fire:.4f}")

# ===================== 第六步:生成文档向量(用于后续模型训练) =====================
# 方法:将文本中所有词的向量取平均,得到文档级向量
def get_doc_vector(text_seg, model):
    """
    将分词后的文本转换为文档向量
    :param text_seg: 分词后的文本列表
    :param model: 训练好的Word2Vec模型
    :return: 文档向量(100维)
    """
    import numpy as np
    vec_list = [model.wv[word] for word in text_seg if word in model.wv.key_to_index]
    if len(vec_list) == 0:
        return np.zeros(100)  # 无有效词时返回全0向量
    return np.mean(vec_list, axis=0)

# 生成所有文本的文档向量
import numpy as np
doc_vectors = np.array([get_doc_vector(seg, model) for seg in corpus])
print(f"\n文档向量形状:{doc_vectors.shape}")  # (4, 100):4个文档,每个100维
print(f"第4条文本(car sadly car caught up fire)的文档向量(前10维):")
print(doc_vectors[3][:10])

GloVe

GloVe 词嵌入方法由 Pennington 等人在斯坦福大学开发,用于自然语言处理。它被称为全局向量,因为该模型直接捕获了全局语料库的统计信息。GloVe 在单词类比和命名实体识别问题上表现出色。

该技术由于采用了更简单的最小二乘代价函数或误差函数,降低了模型训练的计算成本,从而生成了不同且更优的词嵌入。它利用了局部上下文窗口方法(例如 Mikolov 的 skip-gram 模型)和全局矩阵分解方法来生成低维词表示。

潜在语义分析(LSA)是一种基于全局矩阵分解的方法,虽在单词类比任务中表现一般,但它利用统计信息来构建向量结构的思路具有参考性;而 Skip-gram 方法在类比任务中表现更好,却因未利用全局统计数据,无法充分捕捉词的关联信息。

与 Word2Vec(基于局部上下文构建词嵌入)不同,GloVe 聚焦全局上下文生成词嵌入,这是它与 Word2Vec 的核心差异 —— 在 GloVe 中,词与词的语义关系是通过共现矩阵来体现的。

以这两个句子为例:

  • I am a data science enthusiast
  • I am looking for a data science job GloVe 会基于 “窗口大小 = 1”(单词连续出现的次数),构建如下共现矩阵: Embedding_1767784854959 矩阵中的值代表 “对应行的词出现在对应列的词的上下文中的次数”。若语料包含 100 万个不同单词,共现矩阵的规模会达到 100 万 ×100 万。

GloVe 的核心思路是:单词的共现信息,是学习词表示的关键依据。

我们再看斯坦福大学 GloVe 论文中的经典示例:以目标词 “ice” 和 “steam” 为例,统计它们与词汇表中其他词的共现概率,以下是来自一个包含 60 亿词的语料库的一些实际概率:

Embedding_1767785252090

这里的核心公式是:

P(ji)=xijxiP(j|i) = \frac{x_{ij}}{x_i}
  • P(ji)P(j|i) 代表“词 jj 出现在词 ii 的上下文中的概率”。
  • xijx_{ij}:共现矩阵中词 ii 与词 jj 的共现次数,即:xij=共现矩阵[i][j] x_{ij} = \text{共现矩阵}[i][j]
  • xix_i:词 ii 的上下文中出现的所有词的总次数,数学表达式为:xi=kxikx_i = \sum_{k} x_{ik} 假设 k = solid,即与 ice 相关但与 steam 无关的词。预期 Pik / Pjk 比值会很大。类似地,对于与 steam 相关但与 ice 无关的词 k,例如 k = gas,该比值会很小。对于像 water 或 fashion 这样的词,它们要么分别与 ice 和 steam 都相关,要么都与两者都无关,该比值应该接近于 1。

概率比值比原始概率更能区分相关词(例如 solid 和 gas)和不相关词(例如 fashion 和 water)。它也能更好地区分两个相关词。因此,在 GloVe 中,词向量学习的出发点是共现概率的比值,而不是概率本身。

理论就讲到这里,现在开始写代码!

# ===================== 1. 导入所需库 =====================
# nltk:自然语言处理基础库,用于文本预处理
import nltk
# re:正则表达式库,用于文本清洗(移除非字母字符)
import re
# stopwords:nltk内置的停用词库(如the/a/an等无意义词汇)
from nltk.corpus import stopwords
# Glove相关库:Corpus用于构建语料库,Glove用于训练词嵌入
from glove import Corpus, Glove

# ===================== 2. 文本预处理(GloVe输入要求:分词后的文档列表) =====================
# 初始化空列表,用于存储预处理后的分词语料
corpus = []

# 遍历所有文本数据(X为原始文本列表,如推文/句子集合)
for i in range(0, len(X)):
    # 步骤1:移除文本中所有非字母字符(仅保留字母和空格),例如将"storm!!123"转为"storm "
    tweet = re.sub("[^a-zA-Z]", " ", X[i])
    # 步骤2:将所有字符转为小写,统一格式(如"Storm"→"storm")
    tweet = tweet.lower()
    # 步骤3:按空格分词,将字符串转为单词列表(如"heavy storm"→["heavy", "storm"])
    tweet = tweet.split()
    # 步骤4:将预处理后的分词列表加入语料库(若需过滤停用词,可取消下方注释)
    # tweet = [word for word in tweet if not word in set(stopwords.words('english'))]
    corpus.append(tweet)

# ===================== 3. 训练GloVe词嵌入 =====================
# 初始化Corpus对象,用于构建GloVe所需的共现矩阵
corpus_model = Corpus()
# 构建语料库的共现矩阵(核心步骤)
# window=5:上下文窗口大小,即每个词的前后各5个词视为上下文
corpus_model.fit(corpus, window=5)  # 修正原代码:text_corpus→corpus(变量名统一)

# 初始化GloVe模型
# no_components=100:词嵌入向量的维度(常用100/200/300)
# learning_rate=0.05:学习率,控制模型参数更新步长
glove = Glove(no_components=100, learning_rate=0.05)

# 训练GloVe模型(基于共现矩阵)
# corpus.matrix:构建好的共现矩阵
# epochs=100:训练轮数,迭代次数越多拟合效果越好(但易过拟合)
# no_threads=4:训练使用的线程数(根据CPU核心数调整)
# verbose=True:打印训练过程(如每轮损失值)
glove.fit(corpus_model.matrix, epochs=100, no_threads=4, verbose=True)

# 将语料库的词汇字典添加到GloVe模型中(关联单词与索引,方便后续查询)
glove.add_dictionary(corpus_model.dictionary)

# ===================== 4. 验证词嵌入效果:查找相似词 =====================
# 查找与"storm"语义最相似的10个词(返回词+相似度)
# number=10:指定返回相似词的数量
similar_words = glove.most_similar("storm", number=10)
# 打印结果,直观查看相似词
print("与'storm'最相似的10个词:")
for word, similarity in similar_words:
    print(f"  {word}: {similarity:.4f}")

单词元组及其预测概率列表:

Embedding_1767786166345

BERT

BERT 是一种基于 Transformer 模型的自然语言处理(NLP)算法,包含两个核心变体:参数规模为 1.1 亿的 BERT-Base,以及参数达 3.4 亿的 BERT-Large。

它的核心优势是依赖注意力机制,生成与上下文强关联的高质量词嵌入:在训练过程中,输入会逐层经过 BERT 的网络结构,而每一层的注意力机制都能同时捕捉 “当前词左右两侧的语义关联”,这让它的词表示更贴合文本的实际语境。

相比词袋、Word2Vec 等传统方法,BERT 是更先进的技术 —— 它基于维基百科、海量语料预训练得到通用模型,再通过 “微调” 就能适配特定任务的数据集,因此生成的词嵌入效果更优。目前它在语言翻译等 NLP 任务中应用广泛。

它在语言翻译任务中有着广泛的应用。

Embedding_1767786679503

总结

词嵌入技术是训练 GRU、LSTM、Transformer 等深度学习模型的基础,这些模型在情感分类、命名实体识别、语音识别等 NLP 任务中都取得了显著效果。

各类文本表示技术的典型应用场景总结如下:

  • 词袋模型:文本特征初步提取
  • TF-IDF:信息检索、关键词提取
  • Word2Vec:语义分析类任务
  • GloVe:词语类比、命名实体识别
  • BERT:语言翻译、问答系统
← 返回 Notes

Contact

Contact Me

Leave a message here. The form sends directly from the browser to a form delivery service and then to my email.

Messages are delivered to lzx744008464@gmail.com.