做中文语义资源的人大概都遇到过同一个窘境项目里需要一个能查上下位关系的词典翻开公开的同义词词林类资源发现词表规模不够专业领域几乎空白自己想扩人工标注一条X 是一种 Y的成本又不低。两年前我在做一个商品与服务的归一类项目时被这件事卡了很久后来把目光转向了中文维基百科的分类体系——那套由无数编辑者十几年来手工挂上去的分类树本质上就是一份规模巨大、持续更新、带多标签的中文语义词典草稿。问题只在于它没有被整理成词典的样子。这篇内容讲的就是怎么把分类系统整理成中文语义词典这件事。完整链路包括分类图怎么建模、上下位关系怎么抽、同义关系从哪几个地方捡、置信度怎么打分、噪声怎么过滤、最终怎么落成可查询的词典文件。适合正在做知识图谱、问答检索、文本归一、领域词库建设的朋友也适合刚接触开放百科数据、想拿它练手的人。代码不复杂难点在结构判断和噪声处理这两块我会讲得细一些。1. 中文语义词典为什么值得从分类系统里挖1.1 手工词库绕不开的三个现实天花板先说清楚为什么要想别的办法。我接触过的中文语义资源无论规模大小基本都卡在三个地方。第一是规模与覆盖通用词表往往在常用词上做得不错一旦进入垂直领域比如医疗器械、工业零部件、地方特产同义词和上下位关系立刻稀疏得没法用。第二是更新速度新词从出现到进入词库中间隔着一两年是常事而做业务的人往往等不了这么久。第三是关系类型不全不少资源只给同义和少量反义上下位关系是零散的层级链条经常断在中间某一层你想从智能手机一路查到电子产品中间那一跳就没了。这三件事叠加起来的结果是做应用的团队要么自己攒一个领域词表要么在每个下游任务里临时用规则凑。我自己就写过一堆如果词尾是机则归为设备这种补丁式规则维护成本高得离谱加一个新品类就要改一次代码。所以后来我换了个思路找一份已经被人手工整理过层级、而且还在持续维护的中文结构化数据把它当作原料用程序做归一化和关系抽取。开放百科的分类体系正好符合这个条件。1.2 百科分类体系的结构特征恰好就是词典需要的形状维基类平台的组织方式其实很简单每个条目页面底部挂若干个分类标签分类页面本身又可以挂到更上层的分类下面。这个结构对建词典来说有几个天然优势值得逐条说。分类名本身是自然语言短语。不像某些本体库用的是编码或者生造标签这里的分类名就是人话比如中国科幻小说作家乐器制造企业有机化合物。这意味着你从分类名里可以直接读出语义类型甚至能反推出关系的类型是一种属于来自。允许多父节点。一个分类可以同时挂在生物学和教育下面这种多归属恰好对应现实世界的复杂语义比如生物标本既属于生物领域也属于教育工具。对于词典来说多父节点意味着一个词可以获得多条上位路径这对后续的路径打分和消歧很有用。分层粒度有粗有细形成天然梯度。从顶层的大类自然科学社会到极细的叶子分类某年某地成立的某类组织中间有若干层过渡。做语义词典时你既需要粗粒度的高层概念也需要细粒度的具体类这套梯度刚好能覆盖。跨语言链接提供了外部校验。同一个概念往往在多个语言版本里都有对应条目通过语言链接可以把中文标题和外文标题对齐。这个信息在判断同义关系和校验概念是否规范时非常好用后面会专门讲。1.3 先说清楚边界能拿到什么拿不到什么不把边界讲清楚后面的落地一定翻车。从分类体系里比较稳能拿到的是上下位关系is-a、类别归属属于哪个主题域、粗粒度同义同一概念的不同写法、相关关系经常一起出现的概念。这几类关系已经能覆盖知识图谱骨架和检索召回的大部分需求。拿不到的也很明确。精细的词义区分很难一个词在百科里通常只有一个条目多义词要靠消歧义页面或者分类归属来粗分粒度到不了义项级。反义关系基本没有分类体系不表达对立概念。部分-整体关系只有零星几类分类名能覆盖比如XX的组成部分比例很低不能指望。情感、语用层面的信息完全没有。所以我的定位一直是把分类体系当作基础骨架 领域扩展的来源而不是替代精细语义词典。骨架用它细节从别的地方补这样心态就正了也不会在评估阶段发现指标对不上而怀疑方法有问题。2. 把分类系统读成一张图数据落库与结构归一2.1 决定后续质量的字段选择数据处理的第一步是把公开导出的数据库表读进来。维基类平台通常会提供结构化的导出版本核心是几张表页面表、分类链接表、重定向表、跨语言链接表。不要一上来就写抽取逻辑先把这几张表的关系理清楚后面省很多事。表关键字段在建词典中的作用页面表页面 ID、命名空间、标题、是否重定向区分条目节点和分类节点识别重定向页分类链接表来源 ID、目标分类名、链接类型、排序键、时间戳建立条目→分类和分类→父分类两种边重定向表来源页、目标标题抽取别名词、简称、常见错写跨语言链接表来源页、语言代码、外文标题概念对齐辅助同义判断页面链接表来源页、目标标题计算共现抽取相关关系这张表里最容易被人忽略的是链接类型字段。它区分了三种边条目挂分类、子分类挂父分类、文件挂分类。前两种是我们要的第三种直接扔掉。如果不做这个区分文件分类会大量混进图里导致后面统计分类扇出时严重失真。我第一次跑的时候就吃了这个亏某个顶层分类的子节点数统计出来上万查了半天才发现全是图片文件的分类。另一个细节是命名空间。条目和分类在不同的命名空间里分类的命名空间编号是固定的。做图的时候节点类型必须用命名空间来判定不能靠标题里有没有分类两个字这种字符串规则因为条目名里完全可能包含这两个字。2.2 多父节点和环图必须先消毒再谈语义分类图名义上是有向无环图实际数据里是有环的。原因是编辑者会互相挂分类A 分类挂到 B 下面B 又挂回 A形成小环还有自环某个分类把自己挂成了自己的子分类。这些环在人工浏览时无伤大雅但在做层级推理时是灾难传递闭包会直接爆掉。我的处理办法分两步。第一步是找强连通分量用 Tarjan 算法跑一遍把环检测出来。规模上中文分类节点在几十万量级边数在百万量级纯 Python 加字典实现完全跑得动不需要上分布式。第二步是按环的大小分级处理自环直接删边两点环把两条边都降权到 0.3再大的环比如三五个分类互相挂说明这一片分类体系本身有争议整片区域的关系置信度统一打折但不删因为里面往往还有可用的上下位链。降权而不是删除是因为环上挂着的大量正常页面还是有价值的。比如某类设备和某类仪器互相挂这两个分类下面的具体设备条目依然是可靠的叶子节点只是它们之间的父子方向说不清那就两条边都保留但都不作为强证据。这个取舍在做关系抽取时会体现为路径支撑数这个特征后面会讲。2.3 分类名里的三类噪声必须分开处理分类名是自然语言自然语言就有噪声。我把噪声归成三类处理方式完全不同混在一起处理会误伤。噪声类型典型形态处理策略维护型元分类标题含维护模板待清理需要补充等整枝剪掉不下传给子分类时间型分类以年份或年代开头如某年成立某年代保留节点但不作为语义词条输出地域与行政修饰某省某市各国某某剥离修饰前缀地域信息转成属性字段维护型元分类是必须整枝剪掉的。这类分类是平台自己用来管理条目的挂在上面的条目和真实语义毫无关系。剪枝的时候要注意整枝也就是这个分类下面的整棵子树都不要因为它下面挂的子分类同样是维护性质的。判断依据用关键词匹配就够不需要模型。时间型分类的处理要克制。这类分类节点本身是有意义的它记录了事件的年份但它不适合作为语义词典里的概念节点因为某年成立不是一个类别而是一个时间属性。我的做法是保留节点用于图连通性计算但在输出词条时过滤掉避免出现某某公司 是一种 某年成立这种荒谬的上下位关系。地域修饰的处理最有意思。各国某某这种分类在中文百科里非常常见比如各国作家各国河流。这类分类名是集合名词直接作为上位词输出会很别扭。我的做法是把它归一化成单数核心词——各国作家归一成作家同时把各国这个限定信息记成属性。这样输出到词典里就是干净的某国作家 是一种 作家。归一化规则用正则加一张人工维护的后缀映射表覆盖常见的限定成分。2.4 一条能跑通的建图脚本骨架给一段实际用过的代码结构SQLite 直接读内存建图。这段只是骨架重点是结构顺序。import sqlite3, re from collections import defaultdict conn sqlite3.connect(wiki_raw.db) cur conn.cursor() # 1. 读分类节点,命名空间 14 是分类 cat_ids {} for pid, title in cur.execute( SELECT page_id, page_title FROM page WHERE page_namespace 14): cat_ids[pid] title # 2. 读条目节点,命名空间 0 page_ids {} for pid, title, is_redir in cur.execute( SELECT page_id, page_title, page_is_redirect FROM page WHERE page_namespace 0): page_ids[pid] (title, is_redir) # 3. 建边:只保留 subcat 和 page 两种类型 parent_of defaultdict(set) # 分类 - 父分类集合 child_of defaultdict(set) inst_of defaultdict(set) # 页面 - 所属分类集合 for src, tgt, ctype in cur.execute( SELECT cl_from, cl_to, cl_type FROM categorylinks): if ctype subcat and src in cat_ids: parent_of[cat_ids[src]].add(tgt) child_of[tgt].add(cat_ids[src]) elif ctype page and src in page_ids: inst_of[page_ids[src][0]].add(tgt)建完之后立刻做两件事统计每个分类的入度有多少页面挂在它下面和扇出它下面有多少子分类。这两个统计量在后面的置信度打分里是核心特征而且顺手就能发现问题——比如某些分类入度为 0说明它是空壳某些分类扇出上万说明这一片没被整理过。3. 关系抽取从分类路径里抠出上下位、同义与相关3.1 instance-of 和 subclass-of 必须分开算这是整个流程里最容易出错的地方也是很多人第一次做的时候直接把准确率做崩的地方。百科分类里有两种语义完全不同的边条目挂到分类上表达的是这个条目属于这个类分类挂到父分类上表达的是这个类属于更上层的类。这两种边如果混在一起做传递会得到大量错误结论。举个具体的例子。某位作家这个条目挂在科幻作家分类下科幻作家分类挂在作家分类下。正确的推理链路是某位作家 属于 科幻作家实例关系科幻作家 是一种 作家子类关系从而推出 某位作家 是一种 科幻作家。这条链条的每一跳性质不同输出时要分开标注。实际处理里我用了两套结构条目到分类用inst_of字典分类到父分类用parent_of字典。做上下位抽取时只从parent_of里取子类-父类关系因为这层关系可以直接当作上位的候选inst_of里的关系输出时打上实例标记供后面的概念归并使用不直接算作同义词级别的上下位。还有一个坑是分类名的复数性。中文没有词形变化但分类名经常是集合概念比如乐器这个分类下挂的是各种具体乐器条目输出钢琴 是一种 乐器是通顺的但钢琴家这个分类下挂的是具体的人输出某人 是一种 钢琴家就有点怪虽然语义上说得通但作为词典条目读起来别扭。我的处理是对分类名做名词化判断识别出指人指集合指抽象概念三类再决定输出格式。指人的输出成某人是某类人指集合的输出成某物是一种某类物。3.2 分类名模板把某某学家还原成概念分类名的后缀携带着丰富的关系类型信息用正则模板去匹配收益很高。我整理了常用的几组模式实测下来覆盖面不错。PATTERNS [ (r^(.{2,10}?)学家$, person, 研究者), (r^(.{2,10}?)家$, person, 从业者), (r^(.{2,10}?)设备$, device, 设备), (r^(.{2,10}?)制品$, product, 制品), (r^各国(.{2,10})$, collective,集合), (r^(.{2,10}?)史$, topic, 历史主题), (r^(.{2,10}?)理论$, theory, 理论), ]模板的作用不只是识别类型更重要的是反推上位词。比如匹配到某某学家那么上位词大概率是学家或者研究者这比从分类图里爬好几层更可靠。我在实际使用中把模板生成的上位词和图上抽出的上位词做了合并模板命中的给高权重图路径抽出的给中等权重两边一致的时候直接提到最高。模板的另一个用处是拦截错误关系。如果一个分类名匹配到指人模板但它下面挂的是物品条目那说明要么分类挂错了要么条目有问题这类边直接降权或丢弃。这个交叉校验帮我砍掉了一大批脏数据。3.3 同义关系的四个来源可信度差别很大同义关系不能只靠一种来源我实际用了四个按可信度从高到低排。第一是跨语言链接对齐。两个不同的中文标题如果它们的跨语言链接指向同一条外文条目那基本可以判定这两个中文标题指同一个概念。这种关系的准确率在我的抽检里是最高的因为它依平台的结构而非文本相似度。典型场景是同一事物的不同译名、全称与简称并存。第二是重定向关系。重定向页记录的是一种输入这个词应该跳到那个页面的约定绝大部分是别名、简称、常见错写。这条来源量大但噪声也大需要过滤三类跳转到消歧义页的、跳转到列表页的、以及标题本身带括号说明的。带括号说明的比如某某消歧义说明它是一个导航页不是概念本身。第三是分类名括号注释的剥离。不少分类名带括号补充说明格式是核心词说明。剥离括号部分得到核心词再和核心词相同的其他分类合并这能补出一批同义关系。这里要注意中英文括号的混用正则里两种都要覆盖否则会漏掉一大半。第四是同一分类下的高共现近义词。这条最不可靠只能作为候选交给人工复核。具体做法是同一分类下的条目两两计算编辑距离和词向量相似度距离近且相似度高的作为候选。实际跑下来这批候选的准确率不到六成所以我一般只在小范围里用它来发现漏网的同义词不做全量输出。3.4 相关关系用共现统计阈值要按领域调相关关系是词典里最容易被滥用的部分因为它容易算但很难保证有用。我用的是同一分类下的共现频次加 PMI。基本假设很简单两个条目反复出现在同一个分类里说明它们在语义上有关联。指标上除了 PMI我还加了 Jaccard 系数做辅助因为 PMI 对低频词的惩罚不够。两者结合后阈值定在 PMI 大于 3.0 且共现次数大于等于 5这个组合在我测过的几个领域里误报率可以接受。需要提醒的是阈值不能一刀切。大类目比如人物下面的共现关系噪声明显更多因为人物之间的关联太杂而技术类目下的共现关系质量高得多。所以我的做法是按分类层级深度和分类扇出做分段阈值高层分类阈值收紧叶子分类阈值放宽。这个调整看起来琐碎但对最终词典的可用性影响很大。4. 质量闸门置信度打分、抽检与评估4.1 置信度特征的权重设计关系抽出来之后不能直接用必须打分排序再截断。我用的特征不多六七个但每一个都有明确的语义解释。特征含义方向建议权重路径长度从子到父经过几跳越短越可靠0.30支撑路径数有几条不同路径支持同一关系越多越可靠0.25模板命中分类名是否匹配类型模板命中加分0.20跨语言校验是否有外文对应有加分0.10分类扇出惩罚该分类下子节点过多越多越减分-0.10分类入度挂在该分类下的页面数适度加分0.05打分用简单加权求和就够不需要上模型因为特征之间的交互很弱而且加权线性模型的可解释性对后续调参非常重要。我试过用逻辑回归替代效果提升很小反而因为要标注训练集而拖慢了节奏。经验上只要样本量不到万级人工定权重加抽检反馈的迭代效率高得多。有一个组合规则值得单独说当模板命中和路径支撑同时成立且方向一致时置信度直接置顶。这类关系几乎不会错我在抽检里没遇到过反例。反过来路径长度超过 5 跳且没有任何模板支撑的关系即使支撑路径数很多也要谨慎因为长路径上的偏置会被逐层放大。4.2 人工抽检怎么做才不是走过场抽检最怕的是只抽一类样本。我的做法是分层抽样按置信度分高中低三档每档各抽 150 条再加一组专门从长路径里抽的 50 条。这样能得到三条不同的准确率曲线用来判断阈值该定在哪里。标注规范要提前锁死最重要的是明确上下位的判定标准。我的标准是必须能自然地说出X 是一种 Y且不产生明显歧义X 是 Y 的一个实例算通过X 和 Y 相关不通过X 是 Y 的一部分不通过归到部分关系里单独统计。规范不锁死三个人标出来的结果没法比。一致性方面我用三人独立标注加 Kappa 系数低于 0.7 就说明规范有歧义要回去改规范重新标。实际做下来第一轮 Kappa 一般都在 0.6 出头改过两轮规范之后能到 0.78 左右。4.3 和公开资源对齐验证覆盖率而不只是准确率光看抽检准确率不够还要看覆盖率。我的办法是拿一份公开的中文同义词林类资源做基准把它的词条和我们的词典求交集看覆盖比例和在交集上的关系一致率。这两个指标的含义不同。覆盖比例反映词典的规模是否够用实际跑下来通用领域的覆盖在六成上下专业领域会低很多这是预期的。关系一致率反映质量在交集上如果我们的关系方向和基准一致的比例能到八成以上说明整体质量过关不一致的部分要单独看因为公开资源本身也有错不能全信基准。这个对齐过程还有个副产品能发现一批基准里没有但百科里有的新词和领域词这部分恰好是我们想要补的增量。4.4 从图到词典存储结构与查询接口词典最终要能被业务方便地查所以存储结构要围绕查询场景设计。我用三张表纯文本或 SQLite 都能装下。CREATE TABLE lemma ( word_id INTEGER PRIMARY KEY, word TEXT NOT NULL, word_type TEXT, -- person / device / concept ... source TEXT, -- instance / subclass / template freq INTEGER ); CREATE TABLE relation ( head_id INTEGER, tail_id INTEGER, rel_type TEXT, -- hypernym / synonym / related confidence REAL, evidence TEXT -- 路径或来源说明 ); CREATE TABLE alias ( word_id INTEGER, alias TEXT, alias_type TEXT -- redirect / langlink / bracket );evidence这个字段看着多余实际非常有用。调试阶段要追一条关系是怎么来的没这个字段只能回去重跑全流程。上线后做人工复核也靠它标注员能看到依据判断速度快很多。查询接口我给两个最常用的。查一个词的所有上位词一条 join 就够查两个词之间是否存在路径在图上做双向广度优先限制深度 6 跳以内超过就返回空。后者做语义距离计算时用得很多。5. 实操中真正卡住我的四个问题5.1 分类名里的括号和消歧后缀比想象中麻烦括号问题看起来是个小细节实际影响了整个同义关系的质量。中文百科里括号的用法非常不统一半角、全角、中英混排都有而且括号里的内容性质差别很大有的是消歧说明某某消歧义有的是补充限定某某某地有的是原名某某外文名。我的处理规则是分类型判断。括号里含消歧义列表索引这类词的整个标题标记为导航页不参与概念抽取括号里是地名的剥离后保留地名作为属性括号里是外文的剥离后把外文记成别名。用一条正则统一处理会误伤必须按内容分类。这个规则我迭代了三轮才稳定下来。5.2 分词粒度造成的伪同义是我踩得最深的坑早期我用分词工具把分类名切开再做匹配结果生成了一大批荒谬的同义关系。最典型的是计算机科学家被切成计算机加科学家于是计算机和科学家被判定为同一层级的概念直接污染了整片区域。问题的根源是分词工具不理解构词法的层次。计算机科学家是一个整体概念它的构词是计算机科学加家不是计算机加科学家。分词器按统计规律切切错了位置语义就全变了。解决办法是把顺序反过来先用模板正则做整体匹配匹配不上的才退回分词而且分词结果只用于辅助特征不直接生成关系。模板正则的好处是它按语义单位切分不会切错位置。改完之后这类伪同义基本消失了。这件事让我认识到处理领域文本时规则和模板的优先级应该高于通用统计工具。5.3 版本迭代后的增量更新比重建还难数据源会定期更新全量重建一次成本不低但增量更新更容易出错。难点在于分类改名和分类合并一个分类改了名旧名字下面的边不会自动消失新名字下面的边又刚刚建立如果不做映射同一片区域会被算成两份支撑路径数虚高置信度打分全乱。我的做法是维护一张分类名变更映射表靠对比两期的分类节点集合自动生成差异人工确认后合并。对于消失的分类把它的边转移到映射目标上对于新增的分类正常建边。同时保留一份版本快照出问题能回滚。实测下来增量更新的正确率依赖映射表的质量映射表一旦有遗漏后面要花几倍的时间排查。所以我的建议是如果数据量不是特别大宁可全量重建也不要为了省时间做不可靠的增量。全量跑一次的时间成本远低于排查一次增量错误的成本。5.4 一套可直接复现的最小参数清单最后把关键参数集中列一下这些都是我在实际跑通过程中稳定下来的值可以直接拿来当起点再按自己数据的规模微调。参数建议值说明分类入度下限3低于此值的分类视为空壳不产出关系分类扇出上限5000超过视为未整理区域关系降权最大路径长度5超过的路径不参与置信度计算支撑路径数下限2单路径支撑的关系降权处理词条长度范围1 到 12 字过滤超长标题和单字符噪声PMI 阈值3.0相关关系筛选共现次数下限5与 PMI 联合使用抽检样本量每档 150 条三档置信度加长路径组流程顺序上我的建议是先落库建图再做环检测和剪枝然后抽上下位接着补同义最后算相关。这个顺序不能乱因为后面的步骤依赖前面产出的干净图结构。曾经有同事为了赶进度把同义抽取提到剪枝之前结果维护型元分类里的导航页标题全被当成别名混进来了最后只能推倒重来。还有一个小技巧值得分享在整个流程跑完之后随机抽 200 个词条把它们的所有上位路径打印出来用眼睛扫一遍。这个过程不产生任何指标但能直观暴露很多结构性问题比如某一片分类挂得特别浅、某类词条的上位链全是同一个泛化分类。我每次迭代都会做这一步比看报表更快发现问题所在。
阅读完成 · 觉得有帮助?