专项练习 · 9 分钟阅读

Python 分词练习:词元、向量与余弦相似度

分词是 AI 流水线第一个可能悄悄丢失信息的地方。这些练习把选择摆到明面上:什么算一个词元、词表如何共享,以及相似度分数能说明什么、不能说明什么。

先规范化,再切分

转小写是简单的规范化步骤,让 Python 和 python 成为同一个词元,但也会抹去人名或代码中可能重要的信息。选定规则后,训练文本和新文本都要一致应用。

import re

def tokenize(text):
    return re.findall(r"[a-z']+", text.lower())

print(tokenize("Python, Python! Text."))

所有文档使用同一规则

共享一个词表

从训练文档构造词表,而不是为每篇新文档单独构造。对词元排序,让维度顺序确定。词表外的词元需要明确策略,例如忽略或映射到未知词元。

docs = ["python text", "text models"]
vocab = sorted({word for doc in docs for word in doc.split()})
index = {word: i for i, word in enumerate(vocab)}
print(index)

稳定的维度

把词元计数成向量

计数向量很容易检查:每个位置表示某个词表词元出现了几次。如果长文档不应自动与所有内容更相似,可以按文档长度归一化。

def vector(text, index):
    counts = [0] * len(index)
    for word in text.split():
        if word in index:
            counts[index[word]] += 1
    return counts

index = {"python": 0, "text": 1}
print(vector("python text text", index))

可以审计的向量

用余弦比较

余弦相似度衡量向量夹角,因此忽略长度。当一篇文档只是更长时,这很有用。但如果两句话用词完全不同,它仍然无法判断语义是否相同。

import math

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(y * y for y in b))
    if not norm_a or not norm_b:
        return 0.0
    return dot / (norm_a * norm_b)

print(round(cosine([1, 1], [2, 2]), 3))
print(round(cosine([1, 0], [0, 1]), 3))

看方向,不看长度

知道词袋漏掉了什么

词袋模型会给 “dog bites man” 和 “man bites dog” 相同向量。字符 n-gram 可以缓解拼写错误,词嵌入可以捕捉分布相似性,但都不能代替在真实任务上的评估。

def bag(text):
    return sorted(text.lower().split())

print(bag("dog bites man") == bag("man bites dog"))

词序消失了

你的练习清单

常见问题

词元一定是一个单词吗?

不一定。生产分词器可能按标点、子词、字节或 Unicode 码点切分。按单词只是教学上的简化。

比较文本一定要用神经嵌入吗?

不一定。词袋和 TF-IDF 是许多搜索与分类任务的强基线。词汇重叠不够时,嵌入才更有价值。

为什么余弦会返回 0.0?

两个向量没有共享的非零维度,或者其中一个向量全为零。保护条件把这个情况显式化。

练习词元与向量

12 道题,覆盖分词、词表、计数、余弦相似度和词汇匹配局限。

打开练习

继续阅读: