ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

实体别名归一:用并查集把同一家公司的六种写法合成一个节点

实体别名归一:用并查集把同一家公司的六种写法合成一个节点 做企业文档抽取时几乎每个人都会撞到同一堵墙同一家公司在语料里有六种写法。「苏州华宸」「华宸机械」「苏州华宸机械有限公司」「华宸机械设备有限公司」「hua chen 机械」「华辰机械」——它们在语义上是同一个实体在字符串上却彼此无关。下游做检索增强、做统计聚合、做知识图谱节点合并时六个写法就是六个孤岛一条查询命中其中一个另外五个的引用量与关联字段全部丢失。常见的处理思路有两种。一种是在数据入库前人工建一张对照表写全「标准名 ← 别名列表」另一种是完全交给语义模型用向量相似度做聚类。前者准确但跟不上数据增长后者省事但阈值一调就串味。本文实现的是中间路线先做确定性归一化再用编辑距离与前后缀规则生成候选最后用并查集把候选合并成连通分量。全流程可离线运行不依赖任何模型服务结果稳定可复现。一、先定义清楚什么算同一个在写代码之前得把判定规则说清楚否则阈值永远在改。实践中常用的判定维度有四个| 维度 | 判定示例 | 可自动化程度 | 误判风险 || --- | --- | --- | --- || 字符归一后完全相同 | 全角转半角、去空格后相等 | 高 | 极低 || 前后缀包含 | 「华宸机械」⊂「苏州华宸机械有限公司」 | 高 | 中短串易误含 || 编辑距离占比 | 「华宸机械」vs「华辰机械」距离 1 / 长度 4 | 高 | 中同音字与真别名混淆 || 人工登记别名 | 别名表里显式声明 | 高 | 无 |优先级从上到下人工登记的别名表是最终裁决其余三种只生成候选边。这一点很关键——自动化只负责「把可能相同的摆到一起」负责「判定相同」的必须是可追溯的规则或人。二、归一化先消掉噪声任何字符串比对之前先做归一化。这一步的收益最大、成本最低很多人跳过它才导致后面阈值怎么调都不对。import re import unicodedata _FULL re.compile(r[\uff01-\uff5e]) def normalize(name: str) - str: s unicodedata.normalize(NFKC, name.strip()) s .join(ch.lower() if ch.isascii() else ch for ch in s) s _FULL.sub(lambda m: chr(ord(m.group()) - 0xFEE0), s) s re.sub(r[\s\u3000], , s) s re.sub(r[()【】\[\]·、,.。;:/_-], , s) return s这里刻意没有去掉「有限公司」「股份」「集团」这类词——它们在某些语境下确实区分实体母公司与同名子公司。行政区划词东莞、市、省也保留交给后面的包含关系判定处理。归一化只做一件事把「同一个词的不同写法」压成同一个 key把语义判断留给下一步。三、生成候选边三条规则就够三条规则从不同方向找候选彼此互补。第一条是「归一后相等」直接产出最强边。第二条是「包含关系」短串是长串的子串且短串长度不少于四少于四的串包含概率太高例如「网络」几乎人人名里有。第三条是编辑距离。def edit_distance(a: str, b: str) - int: prev list(range(len(b) 1)) for i, ca in enumerate(a, 1): cur [i] for j, cb in enumerate(b, 1): cur.append(min(prev[j] 1, cur[j - 1] 1, prev[j - 1] (ca ! cb))) prev cur return prev[-1] def cand_edges(names): norm {n: normalize(n) for n in names} keys sorted(norm.items(), keylambda kv: kv[1]) edges [] for i, (n1, k1) in enumerate(keys): for n2, k2 in keys[i 1:]: if k1 k2: edges.append((n1, n2, same)) elif len(k1) 4 and k1 in k2: edges.append((n1, n2, contain)) elif len(k2) 4 and k2 in k1: edges.append((n2, n1, contain)) else: d edit_distance(k1, k2) base min(len(k1), len(k2)) if base and d / base 0.25: edges.append((n1, n2, fedit{d})) return edges0.25这个比例是经验起点不是通用常数。中文企业名归一后长度通常在六到十二个字符允许一到两个字符的差异刚好覆盖漏字与同音替换长度更短的串需要更严格的阈值否则「华南制造」与「华东制造」会被并到一起。四、并查集把散落的候选边收成簇候选边给出来的是两两关系实际要的是「分组」。并查集在这里比图库或邻接表更合适只做合并与查根路径压缩后接近常数时间。class DSU: def __init__(self, items): self.p {x: x for x in items} self.r {x: 0 for x in items} def find(self, x): while self.p[x] ! x: self.p[x] self.p[self.p[x]] x self.p[x] return x def union(self, a, b): ra, rb self.find(a), self.find(b) if ra rb: return if self.r[ra] self.r[rb]: ra, rb rb, ra self.p[rb] ra if self.r[ra] self.r[rb]: self.r[ra] 1合并完之后簇内该选哪个名字当标准名规则也要显式写出来不能靠「取第一个」。推荐三条按序生效的判据长度取最长信息最全通常带行政区划与组织形式等长时取出现频次最高的业务上更常用仍相等时取字典序最小保证结果可复现。def cluster(names, alias_mapNone): dsu DSU(names) for a, b, _ in cand_edges(names): dsu.union(a, b) for std, alts in (alias_map or {}).items(): for alt in alts: if alt in dsu.p and std in dsu.p: dsu.union(std, alt) groups {} for n in names: groups.setdefault(dsu.find(n), []).append(n) pick max(len(g) for g in groups.values()) return {root: sorted(members, keylambda x: (-len(x), x))[0] for root, members in groups.items()}, pick五、一个能直接跑的最小例子把上面几段拼起来即可运行if __name__ __main__: corpus [苏州华宸机械有限公司, 华宸机械, 苏州华宸, hua chen 机械, 华宸机械设备有限公司, 华辰机械, 岭南制造, 江北制造, 某材料公司] root_std, largest cluster(corpus, {华宸机械: [苏州华宸机械有限公司]}) for std in sorted(set(root_std.values())): print(std) print(最大簇规模:, largest)典型输出里六个「华宸」写法会被压进同一簇标准名为最长的那个「苏州华宸机械有限公司」「华辰机械」因编辑距离为一被拉进候选需要人工确认后才能决定是否合并——这一步正是规则与人的分工线算法负责把可疑的摆出来是否放行由登记或复核决定。「岭南制造」与「江北制造」距离为二、归一后长度四比例 0.5超过阈值不合并这是短串场景下阈值应有的表现。六、上线的落地顺序· 第一步先跑归一化把「同串不同写法」这类零风险重复清掉。这一步通常能解决三到五成的问题而且完全不需要判定。· 第二步跑包含与编辑距离候选把结果按簇打印成表交业务方逐簇确认。第一轮确认的工作量最大也最值——确认结论会沉淀成别名表。· 第三步把确认后的别名表落到配置里之后的增量数据只做「查表 归一」不再全量跑相似度。· 第四步加监控。统计每周新出现的「未被任何别名表覆盖且长度不低于四的新串」这些是下一轮要确认的对象。没有这一步别名表会在两个月后重新过期。七、几个真实的坑坑一行政区划同名。「江苏某电子」与「某电子江苏分公司」是两个法人实体包含规则会把它们并到一起。对策是把「分公司」「子公司」这类关系词做成阻断词命中时降级为提示而不是直接合并。坑二英文缩写撞车。两三个字母的 key 冲突极多「XY」既可能是企业代号也可能是产品型号。对策是英文串单独走一套规则长度不足四不进相似度通道。坑三改过名的企业。更名前后是两个名字、同一家主体。这类必须靠人工登记任何算法都推不出来。对策是在数据模型里给实体留「曾用名」字段而不是让检索层去猜。坑四上游字段脏。「苏州华宸机械有限 公司」中间夹一个全角空格或者「华宸机械\n」带换行。归一化里那些看起来多余的字符处理兜住的就是这类情况。生产环境里脏字段的出现频率远高于预想。八、小结实体别名归一的难点不在算法算法就是归一化加编辑距离加并查集百行以内。难点在两件更朴素的事把「什么算同一个」写成可以执行的规则以及把人工确认过的结论沉淀成表让机器下次不必重新猜。前者决定误判率后者决定这套东西能维持多久。两者都做到六个写法才真的变成一个节点。本文由一支长期做企业知识库与文本处理工程的技术团队整理欢迎同行交流指正。
返回列表