问题与目标
Token ID 只是词表中的位置编号,数字大小不代表语义距离。要让算法比较文本或让神经网络学习语言规律,还需要把离散符号转换为向量。
本篇比较词袋、TF-IDF、Word2Vec、Embedding 和上下文表示,使用纯 Python 计算 TF-IDF 与余弦相似度,并用 NumPy 展示 Embedding 查表。完成后应能解释每种表示保留了什么、丢失了什么。

从左到右,表示能力逐渐增强,训练与推理成本也随之增加。没有一种表示在所有任务上都更好,简单可解释的统计特征仍然是重要基线。
核心概念
词袋与 TF-IDF
词袋统计词是否出现或出现多少次,不保留词序。句子“节点检查服务”和“服务检查节点”会得到相同向量。
TF-IDF 在词频基础上降低常见词权重、提高区分性词语权重:
TF-IDF(term, document) = TF(term, document) × IDF(term)
IDF(term) = log((文档总数 + 1) / (包含该词的文档数 + 1)) + 1
它适合文本检索、相似度和传统分类基线,但仍不理解词序和一词多义。
Word2Vec 与静态词向量
Word2Vec 根据上下文共现学习稠密向量。Skip-gram 使用中心词预测周围词,CBOW 使用周围词预测中心词。训练完成后,每个词通常只有一个固定向量,因此“苹果发布产品”和“吃一个苹果”中的“苹果”表示相同。
Embedding 是可训练的查表层
Embedding 矩阵形状通常为:
[vocab_size, embedding_dim]
输入 Token ID 后取出对应行。Embedding 可以随机初始化后随任务训练,也可以用预训练词向量初始化。Token ID 42 本身没有距离含义,但第 42 行向量可以通过训练获得语义。
上下文相关表示
Transformer 会结合整段上下文,为同一个 Token 生成不同表示。这解决了静态词向量无法区分语境的问题,但向量依赖模型、输入句子和所在层,不能脱离上下文理解。
可运行实现
下面对四条自创运行消息计算字符二元组 TF-IDF,并找出与查询最相似的文本。字符二元组不依赖中文分词库,适合演示统计表示流程。
import math
from collections import Counter
DOCUMENTS = [
"节点网络延迟升高",
"节点网络连接超时",
"磁盘剩余容量不足",
"批处理任务已经恢复",
]
def bigrams(text: str) -> list[str]:
clean = "".join(text.split())
return [clean[i : i + 2] for i in range(len(clean) - 1)]
def fit_idf(documents: list[str]) -> dict[str, float]:
document_frequency = Counter()
for text in documents:
document_frequency.update(set(bigrams(text)))
count = len(documents)
return {
term: math.log((count + 1) / (frequency + 1)) + 1
for term, frequency in document_frequency.items()
}
def vectorize(text: str, idf: dict[str, float]) -> dict[str, float]:
counts = Counter(bigrams(text))
total = max(sum(counts.values()), 1)
return {
term: count / total * idf.get(term, 0.0)
for term, count in counts.items()
if term in idf
}
def cosine(left: dict[str, float], right: dict[str, float]) -> float:
common = left.keys() & right.keys()
dot = sum(left[key] * right[key] for key in common)
left_norm = math.sqrt(sum(value * value for value in left.values()))
right_norm = math.sqrt(sum(value * value for value in right.values()))
return dot / (left_norm * right_norm) if left_norm and right_norm else 0.0
idf = fit_idf(DOCUMENTS)
document_vectors = [vectorize(text, idf) for text in DOCUMENTS]
query = "网络请求发生超时"
query_vector = vectorize(query, idf)
ranking = sorted(
zip(DOCUMENTS, document_vectors),
key=lambda item: cosine(query_vector, item[1]),
reverse=True,
)
for text, vector in ranking:
print(f"{cosine(query_vector, vector):.3f} {text}")
查询包含“网络”和“超时”片段,因此“节点网络连接超时”通常排在前面。若查询使用完全不同的同义表达,统计表示可能找不到共同片段,这正是它的边界。
Embedding 查表可以用几行 NumPy 表示:
import numpy as np
rng = np.random.default_rng(7)
embedding_table = rng.normal(0, 0.1, size=(8, 4))
token_ids = np.array([2, 5, 2])
vectors = embedding_table[token_ids]
print(vectors.shape) # (3, 4)
print(np.allclose(vectors[0], vectors[2])) # True
相同 Token ID 查到相同初始向量;训练时反向传播会更新被使用的行。随机初始化的向量没有天然语义,不能拿来直接计算可靠的词义相似度。
常见问题与排查
把 Token ID 当作连续数值
ID 只是索引。不能因为 ID 101 和 102 接近,就认为两个 Token 语义接近。相似度应计算向量,而不是编号。
TF-IDF 训练集与测试集分别拟合
IDF 和词表只能在训练数据上拟合,再用于验证集和测试集。分别拟合会造成特征空间不一致,也可能引入数据泄漏。
稀疏向量维度过高
可以设置最小文档频率、最大词表规模或使用字符 n-gram。降维和特征筛选必须在训练数据上确定。
预训练词向量出现未知词
准备明确的 <unk> 策略,也可以退回字符或子词表示。不要用全零向量静默代替而不记录,否则未知词比例过高时难以发现。
小结
文本表示是一组取舍:词袋和 TF-IDF 简单、快速、可解释;Word2Vec 提供静态语义;Embedding 能随任务训练;上下文模型根据句子动态生成表示。选择表示时应同时考虑任务、数据量、推理成本和可解释性。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


