本文是学习文档,很多地方直接引用原文了:
https://www.52nlp.cn/%E8%BE%BE%E8%A7%82%E6%95%B0%E6%8D%AE%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E7%9A%84query%E8%87%AA%E5%8A%A8%E7%BA%A0%E9%94%99%E6%8A%80%E6%9C%AF%E5%92%8C%E6%9E%B6%E6%9E%84%E8%AF%A6%E8%A7%A3
https://juejin.cn/post/7490490874847068210
https://zhuanlan.zhihu.com/p/506668995
搜索功能几乎是现在大小系统中必不可缺的功能。不仅仅是Google,百度这种搜索引擎,很多垂直细分领域的搜索需求也很旺盛,比如电商选品网站的产品搜索,文学网站的小说搜索,影视网站的剧名搜索等等。
搜索系统最基本最核心的功能是信息检索,找到含有关键字的数据或文档,然后按照一定排序将结果给出。但都2025年了,如果你的搜索功能只是个大白框,一点拓展功能都没有,用户绝对是没有办法忍受的。对于一个成熟的搜索系统,怎样提高用户体验也非常重要。常见的一些功能有自动补全,拼写纠错,相似词推荐等等。
这里主要讲了下其中的拼写纠错(Error Correction)
拼写纠错
什么是拼写纠错?有必要吗
比如我想搜黄子韬这个明星,但我拼错了,百度会提示我正确的人名并进行搜索,这就是拼写纠错
拼写纠错重要吗?重要!
- 人的记忆很奇妙,偶尔记住错误的词汇、拼写时卡壳,这是一种完全正常,且是大脑记忆机制的普遍现象。有相关的研究反映,人的大脑依赖“记忆捷径”(如读音联想、常见拼写规则等等),例如将“necessary”拼错为“neccessary”(正确应为1个c),因为英语中双写c更常见。
- 错误检索还浪费了大量的资源,尤其是对于百度,Google这种体量的搜索系统,即使使用拼写纠错后,纠错率只有1%,也是不小的提升,更重要的是提升了用户的体验
背景
纠错技术从上世纪就有了,发展了很长时间,即使现在,也有不少的科研人员从事这方面的研究,可见这项技术的难度。(其实也很好理解,语每一门语言都经历了几百年,甚至几千年的长期演变和发展,形成了一套复杂的文法和句法规则。而且用词的要求本身就很高,差之毫厘,谬以千里)
在2000年以前,业界主要依靠长期积累的纠错规则和纠错词典来进行纠错,比如微软的文档编辑产品WORD即采用这种方法,随着机器学习技术的发展,纠错问题受到了学术界和工业界越来越多的关注,其中有两大主流方法:一种解决思路是将语言错误归类,然后采用Maxent(最大熵模型)、SVM(支持向量机)等分类方法对这些类别进行重点识别;另外一种思路是借鉴统计及其翻译(SMT)的思想,将语言纠错等价为机器翻译的过程,即错误文本翻译为正确文本,并随之出现了一系列的优化方法。
复杂的方法略过,只说下常规的纠错流程,分为错误检测、候选召回、纠错排序三个关键步骤
第一步 错误检测:识别潜在拼写问题
错误分类
对于英文,最基本的语义元素是单词,因此拼写错误主要分为两种
- 非词错误(Non-word Error),指单词本身就是拼错的,比如将“happy”拼成“hbppy”,“hbppy”本身不是一个词。
- 真词错误(Real-word Error),指单词虽拼写正确但是结合上下文语境确是错误的,比如“two eyes”写成“too eyes”,“too”在这里是明显错误的拼写。
对于中文,最小的语义单元是字,往往不会出现错字的情况,因为现在用户几乎都是通过输入法输入设备,不像手写汉字也许会出错。虽然汉字可以单字成词,但是两个或以上的汉字组合成的词却是更常见的语义元素,这种组合带来了类似英文的Non-word Error,比如“洗衣机”写成“洗一机”,虽然每个字是对的,但是整体却不是一个词,也就是所谓的错误词,但这种比较少见。
汉字也有类似Real-word Error的问题,比如暴力行业,暴力和行业都是正确的词,但是两个连在一起却有问题,这种才是日常中文使用中频繁出现的问题,因此很多情况下中文纠错实际上是短语纠错问题。这种情况下,我们把中文常见错误总结分为三类:
用词错误,由于输入法等原因导致的选词错误,其主要表现为音近,形近等,比如上面提过的“暴力行业”实际上应该是“暴利行业”
文法/句法错误,该类错误主要是由于对语言不熟悉导致的如多字、少字、乱序等错误,比如“两人家户”实际上应该是“两户人家”;
知识类错误,该类错误可能由于对某些知识不熟悉导致的错误,比如“芈月传”写成“米月传”
错误判断
字典比对法
- 原理:将输入文本拆分为单词/词组,与预设词典比对。若词不在词典中,则标记为可疑错误
- 局限:无法处理拼写正确但语义错误的词(如“peace”误写为“piece”),且依赖词典覆盖度
统计和上下文建模(这部分就很难了,我也不太懂,简单看看)
通过语言模型构建字符串的概率分布p(W),假设*p(W)*是字符串作为句子的概率,则概率由下边的公式计算
P ( w 1 w 2 … w n ) = ∏ i P ( w i ∣ w 1 w 2 … w i − 1 ) P\left(w_{1} w_{2}\ldots w_{n}\right)=\prod_{i} P\left(w_{i} \mid w_{1}w_{2}\ldots w_{i-1}\right) P(w1w2…wn)=i∏P(wi∣w1w2…wi−1)
P(w₁w₂...wₙ)
表示词序列w₁
到wₙ
的联合概率,∏ᵢ P(wᵢ | w₁w₂...wᵢ₋₁)
表示每个词wᵢ
基于其前面所有词w₁
到wᵢ₋₁
的条件概率的乘积举个例子,it’s a fine day today,day出现的概率,依赖于it’s + a + fine的条件概率乘积
上面的概率公式有一个很大的问题,如果w的词过多,那么排列组合起来,可能会有上千亿总组合,但是实际上绝大多数的组合不会出现,于是根据马尔科夫假设,一个词只和他前面n-1个词相关性最高,这就是n元语法模型。
P ( w 1 w 2 … w n ) ≈ ∏ i P ( w i ∣ w i − k … w i − 1 ) P\left(w_{1} w_{2}\ldots w_{n}\right)≈\prod_{i} P\left(w_{i}\mid w_{i-k}\ldots w_{i-1}\right) P(w1w2…wn)≈i∏P(wi∣wi−k…wi−1)
区别在于不在取前面所有词计算概率了,只取k个词
再简单一点,k=1时,就是 P ( w 1 w 2 … w n ) ≈ ∏ i P ( w i ∣ w i − 1 ) P\left(w_{1} w_{2}\ldots w_{n}\right)≈\prod_{i} P\left(w_{i}\mid w_{i-1}\right) P(w1w2…wn)≈i∏P(wi∣wi−1)
若句中某词的概率显著低于候选词,则判定为错误。如“I ate a pair”中“pair”概率远低于“pear”。
深度学习和混合方法(不懂略过)
第二步 候选召回:找到可能的正确词
编辑距离(Levenshtein距离)
莱文斯坦算法思想是计算从A字符串转为B字符串的最小操作(插入、删除、替换)次数,该操作次数越少,说明两个字符串越相似。
举个例子,单词:playlet -> playlist 所需操作:
playle -> playli 一次替换playli -> playlis 一次插入playlis -> playlist 一次插入
共需要三次操作。
当然这只是一个简单的例子,如果是复杂的字符串转换,就不能简单的列出来去计算这个操作次数了。通常使用动态规划的方式去计算这个操作次数,其核心思想是将大字符串拆为小字符串,计算较短字符串的编辑距离。
统计指出,80%的错误词的编辑距离是1,并且几乎所有的错误的编辑距离在2以内。
最长公共子串长度
和Levenshtein距离类似,区别是只允许增加、删除字符两个编辑操作。
playlet和playlist的最长公共子串是playlt,6
Levenshtein和最长公共子串长度,从两个截然相反的角度,分析字符串的相似程度。Levenshtein距离的大小,表示两个字符串差异的大小;而公共最长子串的大小,表示两个字符串相似的程度大小。
构建专门的候选库
- 构建“错误→正确”映射库,比如“连衣群→连衣裙”,可以用评率或概率辅助
- 结合同音词(拼音特征)、形近字(字形结构)、专业术语库(如医学名词)扩展候选集
语言模型或AI辅助
比如上文说的n元语法模型,分析上下文语义,把概率较高的词汇找出来,生成连贯的短语候选。
实战
这里简单实现字典+Levenshtein算法查找出候选词,不复杂,直接让AI写都很OK,简单快速过一遍即可。
我这里随便找了个字典wordnet_dict.json
import json
import re
import time
# 1. 加载字典
with open('wordnet_dict.json', 'r') as f:
wordnet_list = json.load(f)
wordnet_set = set(word.lower() for word in wordnet_list) # 小写化
# 2. 编辑距离计算
def edit_distance(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1): dp[i][0] = i
for j in range(n + 1): dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
cost = 0 if s1[i - 1] == s2[j - 1] else 1
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)
return dp[m][n]
# 3. 错误检测与纠正
def spell_check(text, wordnet_set, max_distance=2):
tokens = re.split(r'\W+', text) # 按非单词字符分词
misspelled = {}
unique_words = set(tokens)
for word in unique_words:
word_lower = word.lower()
if word_lower not in wordnet_set:
# 生成候选词(限制编辑距离≤2)
candidates = []
for dict_word in wordnet_set:
if abs(len(dict_word) - len(word_lower)) > max_distance:
continue
dist = edit_distance(word_lower, dict_word)
if dist <= max_distance:
candidates.append((dict_word, dist))
# 按编辑距离排序
candidates.sort(key=lambda x: x[1])
# misspelled[word] = [c[0] for c in candidates[:5]] # 返回前5个候选
misspelled[word] = [c[0] for c in candidates] # 返回所有候选
return misspelled
# 测试
start_time = time.time()
text = "I am writting an artical"
results = spell_check(text, wordnet_set)
end_time = time.time()
print("Time used:", end_time - start_time)
print(json.dumps(results))
- 我用到的字典中大约有147306个词,执行耗时约1.56 s,结果如下:
{
"writting": [
"witting",
"writing",
"writhing",
"ratting",
"whiting",
"wetting",
"pitting",
"printing",
"spitting",
"hitting",
"shitting",
"writings",
"knitting",
"waiting",
"fitting",
"written",
"sitting",
"rioting",
"wilting",
"drifting",
"rotting"
],
"artical": [
"critical",
"tical",
"urtica",
"actinal",
"martial",
"partial",
"farcical",
"vatical",
"arrival",
"metical",
"nautical",
"optical",
"cortical",
"article",
"vertical",
"acritical",
"apical",
"arnica",
"artisan",
"tactical",
"attica"
]
}
- 可以看到,距离在2内的词有很多,我该选哪一个?这就要靠第三步,纠错排序了
第三步 纠错排序 选择最佳候选
候选词太多了,我们需要按照一定的排序规则,把排名最高的候选词作为最佳纠错结果返回。
- 对于单个词汇来说,排序规则可以使用词频,距离等多种特征,候选词按照这些特征规则进行排序,返回权重较高的词即可
- 对于短语或者一句话,纠错就困难起来了,每个词存在上下文关系约束,这就需要通过模型或者组合策略来解决了
噪声信道模型
最早是香农为了模型化信道的通信问题,在信息熵概念上提出的模型。如果大学学计算机的,一定听过信噪比,香农公式啥的。(我忘光了),而噪声信道模型简单理解就是,给定一个输入(这个输入可能是错误的),通过解码处理,找到真正的最有可能的输入。
w ^ = argmax w ∈ V P ( w ∣ x ) = argmax w ∈ V P ( x ∣ w ) P ( w ) P ( x ) = argmax w ∈ V P ( x ∣ w ) P ( w ) \begin{aligned} \hat{w} & = \underset{w\in V}{\operatorname{argmax}} P(w \mid x) \\ & = \underset{w\in V}{\operatorname{argmax}} \frac{P(x \mid w)P(w)}{P(x)} \\ & = \underset{w\in V}{\operatorname{argmax}} P(x \mid w)P(w) \end{aligned} w^=w∈VargmaxP(w∣x)=w∈VargmaxP(x)P(x∣w)P(w)=w∈VargmaxP(x∣w)P(w)
- 其中*p(x|w)*是正确的词编辑成为错误词x的转移概率
- 再翻译下就是: P(正确词∣错误词)=P(错误词∣正确词)×P(正确词)
- 需要统计大量的正确词和错误词汇来计算转移矩阵的概率(语料库),再结合这个模型计算出概率最大的候选词
- 关于这个方法,我的理解是:错误词变成正确词的概率我们很难知道,但是正确词变成错误词的概率是可以通过大规模的语料算出来的,把问题逆转下,就能得出答案
Deep & Wide模型
推荐系统和机器学习领域的经典模型架构,抖音推荐模型应该就用到了这个。
Deep & Wide模型是由 Google 在 2016 年提出的一种混合推荐系统架构,旨在结合线性模型的记忆能力和深度神经网络的泛化能力。其核心思想是通过联合训练同时优化两部分模型,以提升推荐系统的准确性和多样性,在纠错里,我们可以简单理解成:
- Deep层:学习上下文语义表示
- Wide层:综合字形、拼音、词频、用户点击反馈等特征计算相似度
利用搜索session
搜索系统中的session分析也能够为纠错服务。搜索session指的是用户在某一个时间段内的搜索行为,如果把搜索日志按照时间排序,对于某一个用户的搜索日志来说,可以看到用户的搜索行为是分段的。每段之间往往有较为明显的间隔,每一段我们称为一个搜索session。
一般来说,用户在一个session内的搜索行为都是为了解决一个问题,因此在此session内用户输入的query往往都是相关的。而这个可以辅助找到用户真正想要找到的词!
个性化与动态优化
利用用户画像进行辅助,如果一个用户专注于短剧,那么短剧相关的词汇优先级就很高。类似的还有地域,年龄等等,都可以用来辅助
包推荐:SpellChecker
PySpellChecker这个包主要支持英语,其他语言需额外配置词典,直接看代码:
from spellchecker import SpellChecker
spell = SpellChecker()
text = "I am writting an artical"
misspelled = spell.unknown(text.split())
for word in misspelled:
corrections = spell.candidates(word)
print(f"Misspelled: {word}, Correction: {corrections}")
最后输出:
Misspelled: writting, Correction: {'gritting', 'writing', 'writhing', 'witting'}
Misspelled: artical, Correction: {'vertical', 'attica', 'tactical', 'arica', 'nautical', 'atrial', 'radical', 'partial', 'critical', 'arnica', 'arnicas', 'cortical', 'metical', 'artisan', 'farcical', 'actinal', 'apical', 'arrival', 'optical', 'martial', 'tical', 'vortical', 'article'}
常见函数
函数 | 用途 | 示例 |
---|---|---|
SpellChecker() |
创建拼写检查器实例,支持多语言(如language='es' 设置西班牙语) |
spell = SpellChecker(language='es') |
unknown(word_list) |
返回输入单词列表中所有拼写错误单词的集合 | misspelled = spell.unknown(words) |
correction(word) |
返回单个错误单词的最可能正确拼写 | best_guess = spell.correction("pythn") |
candidates(word) |
返回错误单词的所有候选正确拼写(集合) | candidates = spell.candidates("exmple") |
word_frequency.add() |
添加自定义词汇到词典 | spell.word_frequency.add("PyTorch") |
word_frequency.load_words() |
批量加载自定义词汇列表 | spell.word_frequency.load_words(["TensorFlow", "NumPy"]) |
原理
- Levenshtein距离
- 概率词典与词频统计
不难发现SpellChecker原理其实和我上文中的代码很类似,但只要执行以下,就会发现,SpellChecker快了非常非常多!只能说不要低估了工业级库的优化深度。我AI了下,大致的改良地方有:
- SpellChecker会直接生成错误词的编辑距离2以内的所有可能词,而不是把字典中的所有词进行一遍距离变换,我让AI优化了下:
import json
import re
import time
# 1. 加载字典
with open('wordnet_dict.json', 'r') as f:
wordnet_list = json.load(f)
wordnet_set = set(word.lower() for word in wordnet_list) # 小写化
def generate_candidates(word, max_distance=2):
candidates = set()
n = len(word)
# 编辑距离=1的操作(原代码)
for i in range(n + 1):
for char in 'abcdefghijklmnopqrstuvwxyz':
# 插入
candidates.add(word[:i] + char + word[i:])
# 替换(需防止索引越界)
if i < n:
candidates.add(word[:i] + char + word[i + 1:])
# 删除
if i < n:
candidates.add(word[:i] + word[i + 1:])
# 换位
for i in range(n - 1):
candidates.add(word[:i] + word[i + 1] + word[i] + word[i + 2:])
# ▼▼▼ 新增递归逻辑 ▼▼▼
if max_distance > 1:
new_candidates = set()
for cand in candidates:
# 递归生成距离-1的候选
new_candidates |= generate_candidates(cand, max_distance - 1)
candidates |= new_candidates # 合并结果
return {cand for cand in candidates if cand in wordnet_set}
# 3. 错误检测与纠正
def spell_check(text, wordnet_set, max_distance=2):
tokens = re.split(r'\W+', text) # 按非单词字符分词
misspelled = {}
unique_words = set(tokens)
for word in unique_words:
word_lower = word.lower()
if word_lower not in wordnet_set:
# 生成候选词(限制编辑距离≤2)
misspelled[word] = list(generate_candidates(word_lower, max_distance=max_distance))
return misspelled
# 测试
start_time = time.time()
text = "I am writting an artical"
results = spell_check(text, wordnet_set)
end_time = time.time()
print("Time used:", end_time - start_time)
print(json.dumps(results))
返回的结果一样,但是速度是0.08s,无与伦比的提升。
- 原本的代码:时间复杂度:
O(N×M×L²)
(N=错误词数,M=词典大小,L=单词长度) - 新的方法:时间复杂度:
O(N×3ᴸ)
(L通常≤10,当L=10的时候,3ᴸ=59049,即使这种情况也比字典小(字典约十几万词))
{
"artical": [
"nautical",
"arrival",
"martial",
"optical",
"vertical",
"radical",
"cortical",
"atrial",
"tical",
"critical",
"vatical",
"acritical",
"article",
"partial",
"actinal",
"farcical",
"attica",
"tactical",
"apical",
"metical",
"arnica",
"artisan",
"urtica"
],
"writting": [
"wilting",
"writhing",
"drifting",
"knitting",
"printing",
"rioting",
"spitting",
"pitting",
"sitting",
"whiting",
"witting",
"shitting",
"wetting",
"fitting",
"hitting",
"waiting",
"writings",
"written",
"writing",
"ratting",
"rotting"
]
}
- 引入了词频排序,高频词优先
- 引入缓存机制
- 预过滤词典
结论:学习的时候可以手撸代码,实际使用还是直接用包!
总结下
- 上面写了很多,其实还是很浅,实际上纠错系统仅数据部分,就可能需要进行log统计,数据库信息统计,百科数据抓取,常用字典整理,语言模型使用等等工作
- 真的有这方面需求,可以直接找大模型平台上的现成产品+自己的一些数据资料,应该比较合适
画外音1 拼音搜索
国内普遍使用拼音输入法,无论是百度还是用的一些打字软件,我们输入拼音,甚至是首拼,就能知道我们想要的词汇,这是怎么实现的?
原理其实类似,总结下就是找到可能的词汇,再排序展示给用户
找到可能得词汇
汉字-拼音映射表:维护一个庞大的汉字与拼音对应词库(如“新”对应“xin”),覆盖常见词汇、专有名词及用户高频搜索词,词库包含多音字选项(如“重”对应“zhong/chong”),并支持首字母简拼(如“新华字典” → “xhzd”)
分词与音节切分:系统将用户输入的拼音串拆解为有效音节(如“xinhuazid” → “xin-hua-zi-d”),并允许容错
模糊匹配技术:基于各种算法,前缀树也好,Levenshtein距离也好,找到可能的候选词
排序展示给用户
- 权重动态更新机制:基于全网搜索热度,高频词会被优先映射,提升联想权重
- 上下文与语义分析:基于自然语言处理(NLP)推测用户意图
- 个性化适配:结合历史搜索记录,用户画像等等,优化排序
画外音2 推荐算法
在搜索的时候,我们经常会搜不到结果,实际上就是库里面没有这个产品的数据,这个时候,往往会推荐一些其他的类似产品给我们。
之前我对推荐的思路是,通过标签查找相关的产品。比如,用户搜索王者荣耀,我先分析下王者荣耀的标签,手游,在线竞技,腾讯,MOBA……然后再从这些标签的产品中找到同类产品推荐给用户。但是问题来了,既然我的库里都没有王者荣耀这个产品,那我又怎么知道这个产品的标签呢?我既然不知道标签,那怎么给用户推荐呢?
直到我看到了Jaccard算法,后来又去专门搜了推荐相关的知识!
Jaccard算法
原理
Jaccard算法的原理比较简单,对于A、B两个集合,其集合交集(共同元素)和集合并集(所有唯一元素)的比值越大,那么两个集合的相似度就越高。
使用到字符串相似度计算中,就是计算A、B两个字符串分词后的集合,其字符串集合相似度高,相当于A、B字符串相似度高
使用场景
- 文本相似度计算
- 兴趣标签匹配、兴趣推荐(A用户买了5个商品,B用户买了3个,C用户买了6个,计算这几个用户分别的Jaccard ,给相似度高的用户推荐他没买过但是相似用户买过的商品)
协同过滤
最早的推荐系统核心是"协同过滤",这个词第一次听到会让人感觉懵逼,但原理其实很简单:
假设小明看了视频A、B、C,小红看了视频A、C、D。系统发现小明和小红都看了A和C,那么可能会认为:
- 应该把视频D推荐给小明(因为跟他喜好相似的小红喜欢D)
- 应该把视频B推荐给小红(因为跟她喜好相似的小明喜欢B)
这就是"协同过滤"的基本思想:找到与你兴趣相似的用户,然后把他们喜欢的内容推荐给你。需要注意的是:在这个过程里,系统完全不需要知道视频内容是什么,只需要知道"谁看了什么"。
系统会不断地将喜好相似的用户自动归为一组,就像把爱看同类节目的观众凑到一起。一旦你加入了某个"兴趣圈子",系统就会把圈内其他人看过、而你还没接触的内容推荐给你。更巧妙的是,当你刷视频时的每一次点赞、评论、驻足观看,都在悄悄更新你的兴趣画像,让系统重新评估你应该属于哪些圈子。今天你可能因为关注美食视频而加入"吃货联盟",明天又因为看了几个旅行视频而被划入"旅行爱好者"的行列。这个过程不断循环,推荐内容也随之动态调整,让你总能看到与当前兴趣相符的新内容。
不过现在已经进入了深度学习和大数据时代,自然不会再用协同过滤这种原始的方法了!比如抖音用到过的Wide&Deep模型,双塔召回模型等等
随着AI能力的发展,我们现在已经能够比较轻松的对图片/视频/文字里面的内容进行分析了,这时候就可以对资源本身打上标签了,甚至计算向量,直接把你喜欢的内容推荐给你
如果你对推荐系统感兴趣,可以看看这个:
https://95152.douyin.com/article/15358?enter_from=channel_page&channel=transparency%EF%BC%8C