
简介面向知识图谱与语义网方向的学习者压缩包内整合了从理论到实践的多层次资料覆盖自然语言处理、机器学习、深度学习在知识提取、知识表示、知识存储与知识检索中的应用也适合参与北京地区学习小组或开源项目实践的开发者使用。资源共77个文件以SQL数据库脚本、Markdown笔记、PDF讲义、Jupyter Notebook实验、Python脚本及JSON配置为主整体约27.49MBSQL用于图数据库或关系型存储落地ipynb与py提供NLP、关系抽取和知识图谱构建的代码示例PDF和MD则聚焦本体建模、语义分析与课程讲义。内容兼顾经典课程与项目实战既有斯坦福CS224d深度学习与自然语言处理的作业测验及中译讲义帮助理解词向量、神经网络与反向传播也有知识表示、正则表达式、PostgreSQL应用等补充材料以及kg-beijing等开源项目代码与说明便于按“理论—实验—工程”路径系统学习。压缩包目录按class/week等模块组织可快速定位不同阶段内容。目前已有69人浏览学习适合希望系统建立知识图谱认知框架并动手实践的读者。1. 知识图谱与语义网的完整链路从文本抽取到图查询的落地路径拿到一个名为「知识图谱与语义网_自然语言处理_机器学习_深度学习_知识提取_知识表示_知识存储_知识检索_图数据库_本体建模」的打包文件我一般不会急着解压看代码而是先顺着这个文件名的顺序把整套技术栈在脑子里过一遍语义网给出了知识表示的框架NLP和深度学习负责从文本里抽实体与关系图数据库承担存储检索则是面向用户的出口。这套链路每一步都有成熟的开源方案但串起来时坑特别多。本文按构建流程走一遍覆盖知识提取、表示、存储、检索四个关键环节适合正在组学习小组、准备从零搭知识图谱的同学也适合评估已有文档库能否改造成知识图谱的工程师。北京地区的学习小组做开源项目时最容易卡住的不是某一个算法而是管线里各环节的衔接。2. 从原始文本到三元组信息抽取与深度学习的任务分工2.1 先分清知识提取的三个子任务实体识别、关系抽取、属性抽取知识图谱构建的第一步是把非结构化文本变成结构化三元组。这里涉及三类信息抽取任务命名实体识别NER找出「北京」「泥浆泵」「李雷」这类实体并标注类型关系抽取判断实体之间是否存在「位于」「属于」「发生」等语义关系属性抽取补充实体的属性值比如「额定功率110kW」。三者优先级不同。我一般先做 NER因为实体是关系抽取的前提属性抽取可以放到后半程用规则补充即可。很多学习小组上来就训练一个大而全的信息抽取模型结果标注数据不够、评估指标混乱最后连一个可靠实体集都拿不出来。词法层面还有实体消歧和指代消解两个难点。「苹果」可以是水果也可能是公司「北京交通大学」和「北京交大」指向同一实体这类问题不做图谱里就会积累大量同义节点。常见做法是构建别名表再配合实体链接工具做合并这个话题放到第 3 章本体建模时一起讲。2.2 机器学习与深度学习模型怎么搭从 CRF 到预训练模型的选择信息抽取的模型选择要看你的标注数据量。下表列出几个可行路线路线模型需要标注量典型场景劣势规则词典正则、AC 自动机0术语固定的垂直领域泛化差、维护成本高传统机器学习CRF、HMM数千句小规模、特征可控特征工程耗人力深度学习BiLSTM-CRF1 万句以上通用文本、跨领域对标注完整性敏感预训练微调BERT、RoBERTa、ERNIE3000 句起中文长尾实体显存占用大、推理慢垂直领域比如石油设备、法律文书、医疗病历里的术语识别效果排序通常是预训练微调 BiLSTM-CRF CRF 词典规则但推理速度和可解释性排序正好相反。学习小组一般建议先做规则兜底再用预训练模型提升边界识别两者通过一个投票机制融合能明显减少实体边界切错的问题。2.3 用 HuggingFace 跑通一个最小实体抽取下面这段代码能在本地快速验证预训练模型的 NER 效果中文场景用uer/roberta-base-finetuned-cluener2020-chinesefrom transformers import AutoTokenizer, AutoModelForTokenClassification, pipeline model_id uer/roberta-base-finetuned-cluener2020-chinese tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForTokenClassification.from_pretrained(model_id) # aggregation_strategysimple 把子词拼回整词输出实体边界 ner pipeline( token-classification, modelmodel, tokenizertokenizer, aggregation_strategysimple ) text 泥浆泵 2PN-630 缸套磨损后更换了缸套组件 for ent in ner(text): print(ent[word], ent[entity_group], round(ent[score], 2))aggregation_strategysimple是真正影响输出质量的参数不设置时返回的是 BPE 子词级别的预测一个词可能被切成两三段设置为simple后会按边界合并直接给出完整实体。entity_group返回的是实体类型CLUENER2020 数据集的标签主要是地址、书名、公司、游戏、政府、电影、姓名、组织、职位、景点等常见类别。如果领域术语分布差异大建议用自己的 3000 句标注语料微调模型下载方式不变训练目标改成AutoModelForTokenClassification加一个线性分类头。实体抽取完还要做关系抽取。最小可用的做法是管道式先 NER 得到实体对再用一个文本分类模型判断这对实体属于哪类关系。需要注意管道式方案的误差会传播——实体识别错一个词后面的关系分类全部失效这就是为什么工业界更倾向用联合抽取模型一次输出三元组。2.4 关系抽取的常见做法与数据组织关系抽取的最小 demo可以用sentence-transformers配合聚类实现也可以用基于触发词的规则兜底。这里给一个轻量规则策略import re TRIGGER_PATTERNS { 位于: [r位于(.?)[。]], 属于: [r属于(.?)[。]], 发生于: [r(?:发生|出现|产生)(.?)(?:故障|问题)[。]], } def extract_relation(sentence: str): for relation, patterns in TRIGGER_PATTERNS.items(): for pat in patterns: matched re.search(pat, sentence) if matched: return relation, matched.group(1) return None, None sentence 泥浆泵出现缸套磨损故障位于钻井平台B区 rel, obj extract_relation(sentence) print(rel, obj) # 发生于 缸套磨损位于 钻井平台B区规则触发词适合术语高度固定的领域能作为冷启动阶段的标注工具——用规则抽出的三元组直接进数据库同时抽样人工修正积累成训练数据。等数据量达到几千条再切换到「BERT 关系分类头」这种监督方案。这里的核心参数是触发词表质量和句子的切分粒度。一句话里往往包含多组三元组比如「泥浆泵位于 B 区且发生了缸套磨损」按句号切分后仍可能包含两个关系规则抽取需要按「每个关系一个窗口」的方式重新切分窗口大小通常设为 20 到 30 个字符太小截断宾语太大会串到相邻句子。3. 本体建模与知识表示决定图谱能否推理的 Schema 设计3.1 语义网的底子RDF 三元组与属性图两种表示路线知识图谱的表层是三元组主语、谓语、宾语底层是知识表示方案。语义网架构里最经典的是 RDF 和 OWL它们解决的问题是「让机器也能理解知识的语义」。RDF 把一切陈述表达为实体, 属性/关系, 值/实体三元组OWL 在此基础上提供了subClassOf、equivalentClass、transitiveProperty等逻辑构造让机器可以做简单推理。工程实现层面要做一个路线选择比较维度RDF/OWL 三元组属性图LPG代表技术RDF4J、Jena、VirtuosoNeo4j、NebulaGraph查询语言SPARQLCypher、Gremlin推理能力内置 OWL 推理支持规则推理弱需自己实现图算法存储类型三元组库、关系表图数据库专用存储适用场景开放数据、跨库互操作、语义检索业务系统、实时关系查询、图计算语义网社区推动的是第一条路线强调「开放」与「共享」不同系统用同样的 URI 和本体数据就能互通。而实际做企业知识图谱的人多数选 Neo4j 这类属性图因为查询直观、运维简单还支持一个节点带多个属性值。学习小组如果目标是理解语义网原理建议两条路都走一遍先用 Jena 加载一个 OWL 本体做推理验证再把同一批数据灌进 Neo4j 做性能对比。3.2 本体建模顺序先定类层次再定属性最后补关系约束很多人在建模阶段直接按文本里的名词定义「节点类型」比如设备、故障、人员、位置结果类层次混乱、属性散落。我建议按下面的顺序来定义类Class从领域核心概念出发建立父类与子类关系比如「设备」下有「泥浆泵」「顶驱」「故障」下有「机械故障」「电气故障」定义属性Property给每个类补充数据属性比如泥浆泵的额定功率、生产厂商、安装日期定义对象属性Object Property描述类与类之间的关系比如「设备-发生-故障」「故障-涉及-部件」同时补充定义域和值域约束补约束和公理比如一个泥浆泵最多属于一个钻井平台这就是基数约束。一个容易犯的错是把属性值当成独立实体。例如「缸套磨损」是故障类型的一个值不应该设计成机械故障类的子类否则查询「有哪些机械故障」时返回的是「缸套磨损」「密封失效」这样的实例语义就乱了正确做法是把缸套磨损定义为故障类的一个个体再让它的faultType属性取值为机械故障。这类设计问题只在查询阶段暴露改起来代价极大。3.3 用 rdflib 把本体写进图里Python 生态里最常用的开源工具是rdflib可以在几行代码内定义一个 OWL 本体并写进 Turtle 文件from rdflib import Graph, Namespace, URIRef, Literal, RDF, RDFS, OWL g Graph() EX Namespace(http://example.org/drilling#) g.bind(ex, EX) # 定义类 g.add((EX.Equipment, RDF.type, OWL.Class)) # 设备 g.add((EX.MudPump, RDF.subClassOf, EX.Equipment)) # 泥浆泵是设备的子类 g.add((EX.Fault, RDF.type, OWL.Class)) # 故障 # 定义对象属性并声明定义域、值域 g.add((EX.hasFault, RDF.type, OWL.ObjectProperty)) g.add((EX.hasFault, RDFS.domain, EX.Equipment)) g.add((EX.hasFault, RDFS.range, EX.Fault)) # 添加数据属性并声明类型 g.add((EX.ratedPower, RDF.type, OWL.DatatypeProperty)) g.add((EX.ratedPower, RDFS.domain, EX.MudPump)) # 创建实例 pump_01 URIRef(EX pump_01) fault_01 URIRef(EX fault_01) g.add((pump_01, RDF.type, EX.MudPump)) g.add((fault_01, RDF.type, EX.Fault)) g.add((pump_01, EX.hasFault, fault_01)) # 序列化为 Turtle后面可以灌入 Jena 做推理 g.serialize(ontology.ttl, formatturtle)这段代码里有几个关键约定RDFS.domain和RDFS.range声明了关系的定义域与值域查询引擎和推理机可以据此做一致性检查RDF.subClassOf建立了类层次支持「查询所有设备」时自动把泥浆泵、顶驱都算进来OWL.DatatypeProperty与OWL.ObjectProperty的区别是前者指向字面量数值、日期、字符串后者指向另一个实体。用图数据库存储时这些语义约束会下降为节点标签和关系类型所以设计阶段的严谨程度决定了查询阶段能问多少层问题。3.4 领域本体的可复用模板给学习小组做石油钻井领域图谱时可以直接复用下面这套类结构类常见属性关联关系设备额定功率、出厂编号、安装时间位于→井场发生→故障故障现象描述、原因、处理建议涉及→部件发生于→设备部件规格型号、材质属于→设备易损→故障井场地理坐标、投产日期包含→设备配备→人员人员岗位、资质负责→设备操作→工序这套模板的关键在于「故障」与「部件」之间的关系。一个故障可能由多个部件导致一个部件也可能引发多类故障多对多关系在属性图里直接建模为两个方向的边但建议只保留一条主边并增加方向属性避免查询时反复过滤重复路径。4. 知识存储图数据库 Neo4j 的建模映射与导入优化4.1 为什么知识图谱最终都落到图数据库三元组可以直接存关系表或键值库但知识图谱的查询模式天然是多跳也就是从一个实体出发沿着边找相邻节点的邻居深度通常 2 到 5 跳。关系表实现多跳查询要连续 JOIN深度超过 3 层性能急剧下降图数据库把邻接关系存为指针式结构遍历的代价只与子图大小相关与全图规模无关。Neo4j 的社区版足够支撑学习项目属性图模型也很直观节点对应实体节点标签对应类关系对应语义边。它的查询语言 Cypher 把图匹配写成人类可读的模式比如「查某台设备发生过的所有故障以及每个故障涉及的部件」只有四行MATCH (p:MudPump {id: pump_01})-[:hasFault]-(f:Fault)-[:involves]-(c:Component) RETURN p.name AS pump, f.name AS fault, c.name AS component这里{id: pump_01}是按属性过滤[:hasFault]限制了关系类型等号右侧是返回字段。Cypher 的模式匹配会在执行计划生成阶段被优化等价关系的路径展开上通常远优于手写 JOIN。4.2 入库前的约束与索引先定义 Schema 再写数据Neo4j 没有强制 Schema但不代表它不需要 Schema。没有唯一约束的场景同样一条数据重复灌入两次就会生成两个 id 不同但内容相同的节点之后的实体对齐和查询全乱。建库第一件事是设置唯一约束和索引// 设备、故障、部件都要求业务主键唯一 CREATE CONSTRAINT entity_id IF NOT EXISTS FOR (e:Entity) REQUIRE e.id IS UNIQUE; // 经常按 name 检索建立索引加速 CREATE INDEX entity_name IF NOT EXISTS FOR (e:Entity) ON (e.name); // 设备按类型过滤的场景很频繁复合索引用上 CREATE INDEX entity_type_name IF NOT EXISTS FOR (e:Entity) ON (e.type, e.name);CREATE CONSTRAINT ... IF NOT EXISTS是幂等操作重复执行不会报错适合把建库脚本放进启动流程里。复合索引的顺序有讲究(e.type, e.name)适合先按类型过滤再按名称匹配的查询如果查询模式是直接按名称搜索则应该反过来建(e.name, e.type)。索引创建后会占用额外写性能图库规模在千万级以下时通常感知不到差异。4.3 从 CSV 批量导入LOAD CSV 与 APOC 的正确姿势实际项目的数据源头多数是关系表或者标注平台导出的 CSV。Neo4j 的LOAD CSV支持批量读取配合MERGE可以做到幂等写入// entities.csv 格式: id,name,type,remark LOAD CSV WITH HEADERS FROM file:///entities.csv AS row MERGE (e:Entity {id: row.id}) ON CREATE SET e.name row.name, e.type row.type, e.remark row.remark ON MATCH SET e.name row.name, e.type row.type;MERGE的行为是「有则匹配无则创建」所以必须保证前面的唯一约束已建立。ON CREATE SET只在新建节点时生效ON MATCH SET在已存在节点上更新字段。数据量超过 50 万行时单条LOAD CSV事务会堆积内存推荐用apoc.periodic.iterate分批提交CALL apoc.periodic.iterate( LOAD CSV WITH HEADERS FROM file:///relations.csv AS row RETURN row, MATCH (a:Entity {id: row.src_id}), (b:Entity {id: row.dst_id}) MERGE (a)-[r:HAS_RELATION {type: row.rel}]-(b), {batchSize: 5000, parallel: true, retries: 3} )第一个参数是前置查询第二个参数是每批次执行的操作batchSize控制单批行数parallel开启并行写入retries处理偶发死锁。注意并行写入模式下MERGE 的事务隔离会造成关系重复创建如果后续发现关系数量不符合预期优先检查是不是parallel: true导致。4.4 存储方案对比与参数调优方案写入速度查询深度适合规模备选场景Neo4j 属性图中5 跳稳定千万级节点企业图谱、反欺诈NebulaGraph高优于 Neo4j亿级节点超大规模图Jena TDB低支持 SPARQL 推理百万级三元组语义网研究、推理验证rel: 关系表高3 跳以上慢百万级临时存储、审计推荐学习小组用 Neo4j 社区版放到 Docker 里分配 4GB 以上堆内存dbms.memory.heap.max_size设置为物理内存一半。导入大批量数据前先关闭dbms.auto_index之外的索引导入完成再创建索引能把写入速度提升数倍。另外给每条边加一个source属性记录文本出处后续做溯源与白名单过滤都方便。5. 知识检索Cypher 精确查询与向量语义检索的混合方案5.1 精确检索Cypher 的路径匹配与变长查询知识图谱检索的第一层需求是「按结构查询」比如「A 设备发生过的所有故障」「和故障 X 相关的所有部件的库存状态」。Cypher 的变长路径在这里非常管用// 查询与 pump_01 相关的所有路径深度1到3 MATCH path (p:MudPump {id: pump_01})-[:hasFault|involves|partOf*1..3]-(x) RETURN path LIMIT 100*1..3表示扩展深度 1 到 3 跳|表示允许的关系类型集合。深度超过 5 跳时需要关注查询性能——Neo4j 的匹配过程会做笛卡尔积扩展深度与耗时呈指数关系。解决办法是限制关系类型或者在中间节点增加标签过滤例如-[:hasFault|involves]-(x:Component)能显著裁剪候选集。5.2 语义检索把自然语言问句转成向量精确检索只能命中与查询条件完全一致的节点。实际使用中用户问的是「泥浆泵异响了怎么办」而图谱里存的是「缸套磨损会导致泥浆泵异响」字面完全不一致。这时需要语义检索——把实体名和自然语言问句都映射到同一个向量空间然后用余弦相似度找近似。最小实现用sentence-transformers加载一个中文向量模型给图谱节点批量生成 embedding存到节点属性里from sentence_transformers import SentenceTransformer model SentenceTransformer(shibing624/text2vec-base-chinese) # 对图谱中的实体描述生成向量写入CSV供neo4j-admin import使用 entities [ {id: pump_01, name: 泥浆泵, desc: 钻井液循环系统的核心设备}, {id: fault_01, name: 缸套磨损, desc: 缸套内径变大导致排量下降} ] for ent in entities: text f{ent[name]} {ent[desc]} vec model.encode(text) ent[embedding] ,.join(str(round(v, 6)) for v in vec) print(entities)节点入库后查询时同样把用户输入编码成向量然后做相似度计算。借助 Neo4j GDS 库的gds.similarity.cosine查询长这样WITH $queryVec AS queryVec MATCH (n:Entity) WHERE n.embedding IS NOT NULL WITH n, gds.similarity.cosine(n.embedding, queryVec) AS score ORDER BY score DESC LIMIT 10 RETURN n.id, n.name, score这里有几个关键参数向量模型选 768 维还是 384 维取决于节点数量和检索时延score的阈值一般设在 0.65 到 0.75低于该值的结果基本不可信。每个节点只存一段描述性文本的 embedding不要把多个属性的 embedding 混合拼接否则向量会失去语义焦点。5.3 混合检索先向量召回再 Cypher 过滤纯语义检索会误召回「异响」的近义词实体所以稳定方案是三阶段流水线召回向量检索 Top 50 候选节点过滤用 Cypher 限定标签、时间范围、来源等结构化条件排序在过滤结果里按相似度得分降序再结合知识图谱的度数连接数加权。# 伪代码混合检索主流程 query_vec model.encode(泥浆泵异响了怎么办) candidates vector_search(query_vec, top_k50) allowed_types [Fault, MudPump, Component] results cypher_filter(candidates, allowed_typesallowed_types, sourceknowledge_base) final rerank(results, weight_sim0.7, weight_degree0.3)这一套的好处是向量检索保证了语义泛化能力Cypher 过滤保证了知识图谱的约束不被打破最终排序由相似度得分和节点在网络中的重要度共同决定避免热门实体长期占据搜索结果前列。5.4 检索效果怎么评估检索效果建议用hitk和MRR两个指标衡量。准备 100 条人工标注的「问题-正确答案」对跑一遍检索看前三名里是否包含正确答案# 给定100个query统计hit3 hit_at_3 0 for query, gold_id in test_set: candidates hybrid_search(query, top_k3) if any(c[id] gold_id for c in candidates): hit_at_3 1 print(hit3 , hit_at_3 / len(test_set) * 100)hit3达到 80% 说明混合检索可用低于 60% 时优先检查向量模型与领域文本的匹配度而不是调阈值。6. 实体对齐与增量更新知识图谱保持干净的两个关键技巧6.1 合并重复实体基于名字相似度的去重流程知识图谱引入多来源数据后最典型的质量问题是同一实体有多个节点例如「北京交通大学」「北京交大」「Beijing Jiaotong University」。常见做法是字段级相似度匹配加人工复核from difflib import SequenceMatcher def entity_dedup_pairs(candidates, threshold0.85): pairs [] for i in range(len(candidates)): for j in range(i 1, len(candidates)): sim SequenceMatcher(None, candidates[i], candidates[j]).ratio() if sim threshold: pairs.append((candidates[i], candidates[j], sim)) return pairsthreshold的取值对结果影响很大0.9 只会合并完全一致的文本0.8 会把「泥浆泵」和「泥浆泵总成」错误合并建议先跑一遍分布再定阈值。回到 Neo4j 里合并操作的顺序永远是先写主节点、再迁移关系、最后删除被合并节点。6.2 用 Cypher 审计图谱质量每轮导入后跑几个固定审计查询把重复节点、孤立节点、缺属性节点数量输出到日志// 同名不同id的重复节点 MATCH (e:Entity) WITH e.name AS name, collect(e.id) AS ids WHERE size(ids) 1 RETURN name, ids LIMIT 20; // 没有任何关系的孤立节点 MATCH (e:Entity) WHERE NOT (e)--() RETURN count(e) AS orphan_count; // 缺少关键属性的实体 MATCH (e:Entity) WHERE e.type Fault AND e.name IS NULL RETURN count(e) AS bad_rows;把这三条查询存成audit.cypher导入后跑一遍结果低于阈值再进入下一步。常见的做法是配合 6.1 节的相似度函数从重复节点里挑出权重最高的一个保留其余节点的关系全部MERGE过去。6.3 增量更新的时间戳策略周期性爬取或人工录入会产生增量数据我一般给每个节点增加updated_at属性和一个全局版本号入库时用apoc.periodic.iterate分批比对只更新字段值发生变化的节点。这样做有两个好处一是查询阶段可用时间范围过滤二是重复导入不会产生全量更新的开销。审计查询里的重复节点数量如果连续三次导入都为 0说明唯一约束和合并流程已经生效。本文还有配套的精品资源点击获取