1.基本原理
"N-gram分布距离"的核心思想是:将两个字符串各自拆解成若干个长度为 n 的连续片段(即n-gram),然后通过比较这些片段的频率分布来计算它们的差异程度。这属于一种"无对齐"的字符串比较方法。
一句话定义
n-gram 分布距离 = 比较两个文本在"局部序列模式"上的统计分布是否一致
"这些局部片段(n-gram)出现的概率分布像不像?"
n-gram指的是文本中连续的n个字符或词汇(如单字符的1-gram、双字符的2-gram(bigram)、三字符的3-gram(trigram))。n-gram分布距离的核心思想是:
两个文本的相似程度,取决于它们的n-gram集合的出现频率分布差异------分布越接近,距离越小,相似度越高。
常见的n-gram分布距离包括余弦距离、Jaccard距离、KL散度、卡方距离 等,其中基于频率的余弦距离 和基于集合的Jaccard距离是最常用的两种形式。
2.算法步骤
(1)n-gram的两种类型
| 类型 | 定义 | 示例(文本:"苹果手机") | 适用场景 |
|---|---|---|---|
| 字符级n-gram | 以字符为单位切分连续n个字符 | 1-gram:{苹,果,手,机};2-gram:{苹果,果手,手机} | 短文本、拼写纠错、跨语言文本匹配 |
| 词汇级n-gram | 以分词后的词汇为单位切分连续n个词汇 | 1-gram:{苹果,手机};2-gram:{苹果手机} | 长文本、语义匹配、文档相似度计算 |
(2)n-gram分布的表示
对文本提取n-gram后,通常转换为两种向量形式:
- 布尔向量(存在型):仅记录n-gram是否出现(对应集合,用于Jaccard距离);
- 频率向量(计数型) :记录每个n-gram的出现次数/频率(对应分布,用于余弦、KL散度等)。
对每个 n-gram 计数并归一化:
pT(g)=count(g)∑g′count(g′)p_T(g) = \frac{\text{count}(g)}{\sum_{g'} \text{count}(g')}pT(g)=∑g′count(g′)count(g)
得到一个 离散概率分布。
步骤3:选择距离度量,计算分布差异
根据需求选择距离指标,计算两个频率向量的差异:
- Jaccard距离(基于布尔向量):1 - 交集大小/并集大小 = 1 - 1/5 = 0.8;
- 余弦距离(基于频率向量):1 - 余弦相似度 = 1 - (1×1)/(√3×√3) = 1 - 1/3 ≈ 0.667。
| 距离 | 本质 |
|---|---|
| L1 / L2 | 几何差异 |
| Cosine | 分布方向 |
| KL / JS | 信息损失 |
| Jaccard | 模式重叠 |
| Wasserstein | 质量迁移 |
3.优缺点适用场景
| 特点 | 说明 |
|---|---|
| ✅ 核心优点 | 捕捉局部模式 :对字符串的局部连续变化(如拼写错误、词缀变化)比较敏感 计算相对高效 :与动态规划类的编辑距离相比,计算复杂度通常更低,尤其在大规模文本预处理中 无需对齐 :不要求逐字符/词匹配,对词序调换有一定容忍度。 可调粒度:通过 n 控制敏感度(n=2~5 最常用)。 |
| ❌ 主要缺点 | 忽略全局语义 :本质上仍是基于表面形式的统计,不理解深层语义。 忽略全局结构 :较长的n-gram才能捕捉较长依赖,但会导致特征空间爆炸和稀疏性问题。 维度灾难:n值增大时,所有可能n-gram的数量呈指数级增长。 |
| 🛠️ 典型使用场景 | 文本分类与聚类 :通过比较文档的n-gram分布来度量相似性,用于新闻分类、作者识别等。 拼写检查与模糊匹配 :因为对局部修改(增删改)敏感,可用于单词纠错或模糊搜索。 生物信息学 :用于比较DNA、RNA或蛋白质序列,进行同源性分析或分类。 剽窃检测:通过比较文本片段的n-gram分布来识别相似或重复内容。。 |
4.主流库推荐
| 实现组合 | 核心库 | 适用场景 | 性能 | 易用性 | 核心优势 |
|---|---|---|---|---|---|
| NLTK + SciPy/NumPy | NLTK(n-gram提取)、SciPy(距离计算) | 通用文本处理、小数据量 | 中 | 高 | 接口友好,学习成本低 |
| RapidFuzz + 自定义n-gram | RapidFuzz(距离计算)、自定义Python函数(n-gram提取) | 批量文本匹配、高性能需求 | 极快 | 中 | 距离计算基于C++,批量处理高效 |
| spaCy + scikit-learn | spaCy(分词/ngram)、sklearn(向量/距离) | 词汇级n-gram、工业级NLP任务 | 快 | 中 | 支持词性标注、实体识别等预处理 |
| Polyfuzz | Polyfuzz(集成n-gram+匹配) | 文本模糊匹配的端到端任务 | 快 | 高 | 开箱即用,支持多种匹配策略 |
| 手动实现(Python) | 纯Python函数 | 学习场景、定制化需求 | 慢 | 高 | 灵活可 |
5.使用
场景1:通用文本处理(字符/词汇级n-gram,小数据量)→ 首选NLTK + SciPy
NLTK提供了便捷的n-gram提取工具,SciPy/NumPy可高效计算距离,是学习和小数据量场景的首选。
使用示例:
python
import nltk
from nltk.util import ngrams
import numpy as np
from scipy.spatial.distance import cosine
# 下载nltk的必要数据(首次使用需执行)
# nltk.download('punkt')
# 1. 定义文本和n值
s1 = "kitten"
s2 = "sitting"
n = 2 # 二元字符n-gram
# 2. 提取字符级n-gram
def get_ngram_features(text, n):
# 生成n-gram列表
ngram_list = list(ngrams(text, n))
# 构建n-gram到频率的字典
ngram_freq = {}
for gram in ngram_list:
key = ''.join(gram)
ngram_freq[key] = ngram_freq.get(key, 0) + 1
return ngram_freq
# 提取特征
freq1 = get_ngram_features(s1, n)
freq2 = get_ngram_features(s2, n)
# 3. 构建统一的n-gram向量
all_ngrams = list(set(freq1.keys()) | set(freq2.keys()))
vec1 = np.array([freq1.get(gram, 0) for gram in all_ngrams])
vec2 = np.array([freq2.get(gram, 0) for gram in all_ngrams])
# 4. 计算余弦距离(n-gram分布距离的一种)
cos_sim = 1 - cosine(vec1, vec2)
cos_dist = cosine(vec1, vec2)
print(f"n-gram余弦相似度:{cos_sim:.4f},余弦距离:{cos_dist:.4f}")
# 输出:n-gram余弦相似度:0.3651,余弦距离:0.6349
场景2:批量文本匹配(高性能需求)→ 首选RapidFuzz + 自定义n-gram
RapidFuzz的C++底层实现了多种距离计算(如Jaccard、余弦),结合自定义的n-gram提取函数,可实现批量文本的高效匹配。
使用示例:
python
import rapidfuzz
from rapidfuzz import distance
from nltk.util import ngrams
# 1. 提取n-gram并转换为集合(用于Jaccard距离)
def get_ngram_set(text, n):
ngram_list = list(ngrams(text, n))
return set([''.join(gram) for gram in ngram_list])
# 2. 定义文本和候选列表
query = "北大"
candidates = ["北京大学", "北京师范大学", "清华大学", "南京大学"]
n = 2 # 二元字符n-gram
# 3. 批量计算n-gram的Jaccard距离
matches = []
query_ngram = get_ngram_set(query, n)
for cand in candidates:
cand_ngram = get_ngram_set(cand, n)
# 计算Jaccard相似度
sim = distance.Jaccard.similarity(list(query_ngram), list(cand_ngram))
matches.append((cand, sim))
# 排序输出
matches.sort(key=lambda x: x[1], reverse=True)
print("批量匹配结果:", matches)
# 输出:[('北京大学', 0.5), ('北京师范大学', 0.2), ('清华大学', 0.0), ('南京大学', 0.0)]
场景3:工业级NLP任务(词汇级n-gram)→ 首选spaCy + scikit-learn
spaCy提供了更专业的分词和文本预处理功能,适合词汇级n-gram的提取,结合scikit-learn的向量和距离计算,可处理复杂的NLP任务。
使用示例:
python
import spacy
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# 加载spaCy的中文模型(首次使用需下载:python -m spacy download zh_core_web_sm)
nlp = spacy.load("zh_core_web_sm")
# 1. 定义文本
texts = ["苹果手机的性能很好", "苹果电脑的性能不错", "华为手机的续航很强"]
# 2. 分词函数(基于spaCy)
def tokenize(text):
doc = nlp(text)
return [token.text for token in doc if not token.is_stop and not token.is_punct]
# 3. 提取词汇级2-gram并构建频率向量
vectorizer = CountVectorizer(ngram_range=(2, 2), tokenizer=tokenize)
X = vectorizer.fit_transform(texts)
# 4. 计算余弦相似度(分布距离)
sim_matrix = cosine_similarity(X)
print("词汇级n-gram余弦相似度矩阵:\n", sim_matrix)
# 输出:[[1. 0.28867513 0. ]
# [0.28867513 1. 0. ]
# [0. 0. 1. ]]
场景4:端到端文本模糊匹配→ 首选Polyfuzz
Polyfuzz集成了n-gram、TF-IDF、词嵌入等多种特征,可直接实现文本的模糊匹配,开箱即用。
使用示例:
python
from polyfuzz import PolyFuzz
# 1. 定义查询和候选
query = ["北大"]
candidates = ["北京大学", "北京师范大学", "清华大学", "南京大学"]
# 2. 使用n-gram模型匹配
model = PolyFuzz("NGram")
model.match(query, candidates)
# 3. 输出结果
print(model.get_matches())
# 输出:包含匹配的文本、相似度等信息