做NLP越久越会发现,很多任务看似简单,真正落地的时候全是细节。今年年中某个业务方向忽然提了个需求:把“同一实体的不同叫法”自动找出来。当时我盯着需求文档想了很久,这不就是别名发现模型吗?于是就有了这篇文章里说的这轮初次尝试。整个过程谈不上多高深,但踩坑不少,思路也迭代了好几版,权当一次完整的技术复盘记录。
1. 为什么要做别名发现:一个被反复问到的业务问题
1.1 到底什么叫“别名”
先把我理解的任务边界说清楚。这里的别名,不是指某个人有几个绰号,而是业务数据里同一个实体对象出现了多种名称表达。举个例子,一个商品可能在内部系统里叫“A1001-白”,在前台页面展示成“智能保温杯 白色 500ml”,在用户评价里则被叫做“白色杯子”“那个保温的瓶子”。离线分析系统如果不知道这些名字指向同一个商品,就会把它们当成三个不同实体,导致统计口径分裂、搜索结果召回不全、知识图谱里出现大量孤立节点。
这本质上是一个实体对齐问题,但比传统实体消歧要更轻量,因为只处理“名称层”的对齐,不去理解上下文和文档语义。我的目标很朴素:给定一组候选名称,给出两两之间是不是“同一个东西”的判定和置信度,再把置信度足够的名称聚到一个别名集合里。
1.2 为什么不能用词典硬凑
遇到这个需求,第一反应通常是维护一张别名映射表,把“A1001-白”手工关联到“白色保温杯”,这当然可以,但落到真实数据上就崩了。
真实业务里的名称表达是长尾的,数量级在几十万到上百万,而且新词每天都在产生。两个字写的别名、加了品牌名后缀的促销叫法、错别字、英文中文混写、数字大小写不一致,纯靠人去维护根本不现实。更麻烦的是,别名发现的价值恰恰在于“发现”,也就是找出那些连业务人员都没想到会被关联在一起的叫法。比如某天运营发现“大肚杯”和“900ml吸管杯”在同一次团购活动里被反复一起下单,它们可能就是同一个款式的不同卖法。这种事词典永远追不上。
所以模型化的思路是必然选择:先让模型自动产出候选,再让人只做少量审核。这样既能处理长尾,又能控制人工成本。
2. 数据构造与任务边界:最花时间的其实是这步
2.1 输入输出怎么定义
任务定义方式直接决定后续建模难度。我第一版把模型定义成:对于两个名称字符串 $s_1$ 和 $s_2$,输出一个0到1之间的分数,表示它们指向同一实体的概率。训练方式可以用有监督的二分类,也可以用无监督相似度检索后接阈值判断。
我最后采用了“无监督召回 + 轻监督重排”的两段式结构,后面会细讲。这里先说数据。
项目组拿到的是一批带统一标识的历史数据,比如同一个商品ID下挂了历史上出现过的所有标题、录入名称、简称、平台卖点标签。这个结构非常关键,它天然给出了正样本:同一个ID下的任意两个名称都是别名正例。负样本则从不同ID的名称里随机抽样组合。这个数据设计听起来简单,但实际操作时要非常小心ID的粒度,否则模型会被带偏。
2.2 名称为什么叫“脏数据”
名称文本的脏,脏得超乎预期。一开始我把数据接进来直接做了个字符串正则去重,结果发现大量实际上同一个东西的名称因为标点差异被当成不同的。
举几个真实出现的形态:
- “白色杯子 500ml” 和 “白色杯子500ml” 差一个空格。
- “苹果-12” 和 “苹果12” 中间的分隔符不同。
- “商品:大肚杯” 和 “大肚杯(含吸管)” 后面多了括号说明。
- “黑色 WHITE” 这种英文语义矛盾,纯粹是录入错误。
这些并不罕见,而是占比很高。如果不对文本做标准化,再好的模型也是在垃圾特征上做算术。所以我在数据预处理阶段动了个小手术。
2.3 预处理方案与细节
具体做了四步:
- 统一字符大小写,全角转半角。
- 去掉名称里的停用后缀,如“商品”“新款”“特价”“包邮”等营销归类词。
- 括号内的补充信息单独抽出作为附属特征,不直接参与主体向量计算。
- 数字和单位统一化处理,“500ml”“500ML”“500毫升”都映射到同一个符号位。
这些规则不强,但能显著减少后续相似度计算的噪声。这也算是“数据先行”的常规操作,做模型之前先让文本干净。
3. 初版方案设计:字符级向量与多通道相似度检索
3.1 为什么一开始不直接上预训练模型
有人可能会问,怎么不用BERT之类的模型直接算语义相似度。我理解这种想法,但实际业务里有两个障碍。
第一,名称普遍很短,平均只有6到15个字符,缺少上下文,BERT类的向量在这种输入上优势不明显,反而会有严重的“近义词误伤”。第二,推理速度要求高,上亿对候选如果用深度模型做两两计算,单机根本扛不住。别名发现的数据量往往是O(n²)量级的,任何复杂模型都很难直接全量跑。
所以我把首要目标放在“快速召回大概率别名”上,用轻量特征把候选对从几亿压到几万,再用重排模型或规则精排。这就像搜索引擎的召回和精排,漏斗越往上越要轻。
3.2 名称向量的构建:字符n-gram与加权TF
我用的不是整词向量,而是字符n-gram加权向量。具体做法是,把标准化后的名称分割成所有长度为2和3的连续字符片段,统计每个片段的出现次数,并通过TF-IDF权重转换得到向量。
原理其实很简单,中文里词内部结构不像英文那么规则,但字符n-gram能很好捕捉局部拼写特征。“保温杯”和“保溫杯”错了一个字,2-gram里“保温”“温杯”仍然重合度极高;“吸管杯”在字符级上也能和“大肚吸管杯”产生足够多的交集。相比之下,直接把整段文字丢给语义向量,字符级拼写信息反而被稀释。
代码上大致是这样的:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer(analyzer='char_wb', ngram_range=(2, 3)) X = vectorizer.fit_transform(all_normalized_names)这里用char_wb而不是char,是因为char_wb会在单词边界内生成n-gram,能避免“白色杯”这种中间空格导致的无意义片段。实际效果差异不小,建议用char_wb。
3.3 三个相似度通道加权
光一个字符向量余弦相似度不够,我还加了两个辅助通道,三路打分,最终加权合并。
- 通道A:字符n-gram向量余弦相似度,主要捕捉局部拼写重合。
- 通道B:拼音向量余弦相似度。先把名称转成拼音,再对拼音做字符n-gram。这个通道专门对付“错别字”和“同音字”,比如“苹果”和“苹菓”字符上差一个字,但拼音一样。
- 通道C:编辑距离归一化分数,用最长公共子序列长度除以两个名称的长度和。主要防止短名称之间余弦分数虚高。
最终分数 = 0.5 × 通道A + 0.3 × 通道B + 0.2 × 通道C。这个权重是我在小验证集上调出来的,不是唯一解,但方向很明确:字符拼写最重要,拼音次之,编辑距离只做约束。
3.4 向量检索与候选截断
要避免对所有候选对做两两全算,我利用向量检索的思路:先把上百万名称向量建好索引,然后用每个名称向量去检索topK个近邻,只保留相似度超过粗略阈值的对。
这里的要点是批量计算和分段截断。我先用余弦相似度粗筛召回top100,再用三通道分数精排,最后把低于0.6的候选全部丢掉。实测召回率能保住,运算量却凭空少了好几个量级。给个直观数据,15万名称两两组合是11亿对,用向量检索取top100后只需要计算150万对的精排分数,直接降到原来的1.3%。
4. 从相似度到打分模型:让机器学会“配不配”
4.1 相似度加权有什么缺点
几轮结果看下来,多通道加权方案已经有基本可用度了,但我很快发现它有天花板。
因为权重是人工拍脑袋定的,它只适合抓“字面上很像”的别名,遇到“语义相同但表达完全不同的名称”就开始拉胯。比如“运动水壶 大容量”和“户外便携杯子”,字面没有太多重合,可明眼人都知道它们很可能说的是同一类商品。字符级向量在这类case上天然无能为力。
于是我想给打分加上可学习的参数,让模型从示例里学出“什么叫配”,而不是靠我拿权重试。
4.2 自动构造训练样本
正样本方面,同一商品ID下的名称对就是天然正例。但直接全部拿来训练会有偏差,因为同一ID下也会混入一些只出现过一两次的临时标签,有的实际上根本不是同一个东西;此外,短名称之间的搭配会产生大量“噪声正例”。所以我把正样本做了一步清洗,要求两个名称在该ID下共同出现的次数不低于某个阈值,并且两个名称的相似度粗筛分数都不能太低。
负样本采取“同ID反例 + 跨ID随机负采样”的策略。跨ID采样很简单,从不同ID的名称里随机挑配对,这类负样本占了绝大多数,容易学。但真正的难点是“难负例”:字面上很像,但其实是两个货号不同的实体,比如“白色款500ml”和“白色款450ml”。难负例太少,模型就会过于宽松,把所有相似的都判成正例。所以我额外从top人工标注了一批难负例,把它们的损失函数里权重提高,让模型被迫去区分这些近似但不相同的名称。
4.3 打分模型用什么结构
重排阶段我用的是梯度提升树模型,特征一共十来维:
- 三通道相似度分数。
- 字符2-gram重合率、字符3-gram重合率。
- 编辑距离、最长公共子串长度。
- 长度差、数字部分是否冲突。
- 括号附属信息是否匹配。
- 拼音归一化后的编辑距离。
训练时我只把分数当作相对排序的标准,不要求输出严格概率,验证指标主要看在不同召回深度下的F1。
对比实验我看了一轮:只用人工权重是0.72的F1,加上树模型重排之后能到0.79。两个点听起来不多,但放到上百万对基数上,意味着误判数量级下降不少,质检组那边能明显感觉到提审量变少。
5. 评测、阈值调节与典型踩坑实录
5.1 评测集怎么造才不会自欺欺人
如果没有标准评测集,模型调参就是自嗨。我把评测分为两个层次:离线全自动指标和在线人工抽检。
离线部分,我从历史数据里随机抽了3000对名称人工标注,标注规则很简单:两个名称是不是指同一个实体。标注结果分为三类——确定是、确定不是、存疑。存疑的对不进评测集,避免人为噪声污染指标。最后有效评测集大约2600对,正负比1比3,符合真实分布。
为了测出模型在难负例上的表现,我还专门从top检索结果里挖了高相似但确实不是实体别名的对,作为第二个检测集。这个集合只有大概500对,但每次迭代模型都要单独看它上面的错误率,防止模型为了冲整体指标把“看起来像就当是”的毛病带进来。
5.2 三组典型case拆解
我把测试集上的结果人工看了几遍,发现模型行为可以分成三类。
第一类是明显别名,模型基本不会错,比如“大肚杯400ml”和“400ml大肚杯”之间的词序变化。这类case字符向量余弦相似度直接0.9以上,三通道加权后稳定高分。
第二类是半词序重合加修饰词不同,比如“保温杯弹盖款”和“弹盖保温杯”。这类词序变化靠字符n-gram还能抓住部分,但分数会略降,阈值设在0.72附近时可以捞回不少。
第三类是纯语义型别名,比如“车载吸尘器”和“车载便携吸尘器”还算不困难,真正难的是“洗车神器”和“高压水枪”这种完全不同的词。这类case我目前初版模型表现一般,字面相似度太低,重排模型也很难单独靠名称本身判断。它们正是下一步需要解决的问题。
下面这个表格是测试集上三个子集的F1对比:
| 子集 | 样例特征 | F1 分数 |
|---|---|---|
| 字面强匹配 | 词序不同、字符高度重合 | 0.94 |
| 字面中等匹配 | 存在修饰词差异、局部拼写变化 | 0.81 |
| 语义匹配 | 字面差异大,语义接近 | 0.51 |
5.3 踩坑记录:阈值不是万能的
这一轮最深的体会是,阈值调参解决不了方法论的问题。
一开始我把全局分数阈值调得很严格,F1反而下降,因为大量真别名被挡在门外。改成宽松阈值后,误报率又飙升,质检同事反馈每天要看完上千条坏数据才能捞出几十条有效别名。后来我把阈值拆成两段:全局粗阈值只管召回,后面按不同业务类目分别设精排阈值。效果比全局整一整好得多,因为不同类目的名称分布差异太大。
另一个坑是相似度分数和置信度并不单调。有些case的分数高得吓人,但其实是同一个商品ID下反复出现导致向量过拟合;有些真别名因为名称太短,分数反而只有0.63。刚开始我做了一个很蠢的清洗,把分数低于0.75的候选全删了,结果把一批优质别名也顺手删掉。后来我改用“分数分位数 + 类目局部排序”的方式来决定哪些候选进入人工审核,效果稳定许多。
还有一个细节是跨语言和数字编码的干扰。“iPhone 14”和“苹果14”这种中英混写非常常见,但拼音通道和编辑距离通道都会给出奇怪的高分。我最后对“纯数字+英文品牌词”做了一个单独处理,把英文品牌名映射成统一符号位,再去做字符n-gram,才把这类case理顺。
5.4 更多实战小问题
再补充一些零碎但影响大的点。
- 空格和换行符在名称里经常出现,向量化之前一定要
strip并替换内部空白。 - 全角数字和半角数字必须归一,“36”和“36”肉眼看不出差别,算法却分得很清楚。
- 商品标题里的“新款”“官方”“正品”这类词高频但区分度极低,应该直接进停用词,不然会让“新款保温杯”和“保温杯新款”分数虚高。
- 长尾低频名称之间相似度计算噪声很大,最好对低频词做回退处理,单独走编辑距离通道,而不是硬套字符n-gram向量。
这些都属于经验过滤,加上去之后整体效果提升有限,但每一条都避免了特定场景下的炸裂。
6. 当前方案的边界与下一步的方向
6.1 初版模型的能力上限
说句实话,这个初版方案解决的是“把肉眼可见的别名自动收回来”,还没到“把所有隐藏叫法都挖出来”的程度。
最核心的局限在于,所有相似度信号都来自名称字符串本身,而真实业务里大量别名必须靠上下文才能确认,比如某个名称在某个类目下是别名,放到另一个类目下就变成完全不同的东西。还有一词多义,比如“蓝山”在不同业务里分别指咖啡产地、某种色号、某个地点编号,字符串特征完全无法区分。
另一个明显的瓶颈是低频名称。“一次性出现的孤词”没有足够的n-gram统计,也没有共现关系可依赖,目前方案对它束手无策。这批孤词数量虽然不多,但往往是真正需要被发现的隐藏别名。
另外,初版模型没有对时间维度建模。一个产品改版后名称跟着变化,这种变化轨迹其实可以反过来辅助推断别名关系。比如同一商品ID的历史名称序列是“A款-白色”到“B款-白”,再到“白色版”,中间过渡词能提供强信号,而我的模型完全没用到这个信息。
6.2 我想尝试的几个进化方向
按优先级排序,下一步我准备做三件事。
第一,把历史名称序列建模成序列。不再孤立地比较两两名称,而是把一个实体的名称列表当成时序数据,利用前后名称的共现变化来生成候选,再结合当前的相似度模型做确认。这能直接缓解低频孤词问题。
第二,引入多模态附属信息作为旁路信号。名称只是实体信息的一部分,图片、类目路径、品牌ID、关键属性元组都可以作为“确认侧信息”,当名称相似度模糊时用旁路信息来投票。
第三,做基于中心点的聚类,而不是两两判定的传递闭合。现在我走的是“两两判断后做连通块合并”路线,传递性会带来连锁错误,A和B是别名、B和C是别名,但A和C实际上不是,这类误伤已经在测试集里出现过。改成聚类模型后,能够从全局一致性角度做约束,效果应该会有提升。
这些方向做完一轮,估计这套别名发现模型才能算真正能打的状态。不过就这次初次尝试而言,模型已经从“全靠人盯”变成了“机器捞,人审”,把整体运营效率拉高了不止一个量级。
最后分享一个实操体会:做这类偏底层的NLP任务,不要一上来就选最贵的模型。先把轻量方案跑通,让特征工程、阈值策略和数据管道都稳定下来,再逐步引入更重的模型。数据迭代的收益,往往高于模型结构的收益。