知识图谱的构建是一个复杂的系统性工程,涉及知识建模、数据获取、知识抽取、知识融合、知识存储、知识推理、知识应用及维护等多个环节。以下是知识图谱构建的详细方法和流程:
一、知识建模:定义知识图谱的模式(Schema)
知识建模是构建知识图谱的第一步,旨在确定知识图谱的结构和语义,定义实体、关系、属性及其层次体系。
1. 确定领域和目标
- 明确应用场景:例如医疗领域的疾病知识图谱、电商领域的商品知识图谱等。
- 确定关键问题:如“需要回答哪些类型的查询?”“需要哪些实体和关系?”
2. 定义实体类型
- 列举领域核心实体:
- 医疗领域:疾病、药品、症状、科室等。
- 电商领域:商品、品牌、类别、用户等。
- 构建实体层次结构(Taxonomy):
- 例如:“疾病”是父类,“传染病”“慢性病”是子类。
3. 定义关系类型
- 常见关系类型:
- 上下位关系(IsA):如“肺癌”→“癌症”。
- 关联关系:如“药品”→“治疗”→“疾病”。
- 属性关系:如“疾病”→“症状”→“咳嗽”。
- 关系的方向性和约束:
- 关系通常是有向的(如“患者”→“患有”→“疾病”)。
- 定义关系的基数(如“一个疾病可以有多个症状”)。
4. 定义属性和数据类型
- 实体属性:如“疾病”的属性包括“发病率”“潜伏期”(数值型)、“症状描述”(文本型)等。
- 关系属性:如“治疗”关系的属性包括“疗程”“有效率”等。
5. 工具选择
- 本体建模工具:
- Protege:可视化界面,支持OWL本体语言,适合学术和小型项目。
- WebProtégé:基于云端的协作建模工具。
- TopBraid Composer:企业级本体建模工具,支持复杂推理。
- Schema表示语言:
- OWL(Web本体语言):用于定义语义关系,支持推理。
- RDFS(资源描述框架模式):轻量级语义模型,适合简单场景。
- JSON Schema/GraphQL Schema:适合动态数据建模和API驱动的应用。
二、数据获取:收集多源异构数据
知识图谱的数据来源多样,需根据实体和关系类型收集相关数据,并处理数据的异构性(如结构化、半结构化、非结构化数据)。
1. 数据类型
- 结构化数据:
- 关系型数据库(SQL):如MySQL中的“商品表”“用户表”。
- 表格数据(Excel/CSV):如药品说明书表格。
- 知识库(公开):如Wikidata、DBpedia、CN-DBpedia。
- 半结构化数据:
- XML/JSON数据:如网页中的JSON接口返回数据。
- 百科页面(如维基百科):通过模板提取结构化信息。
- 非结构化数据:
- 文本数据:新闻报道、医学文献、用户评论等。
- 图像/音频/视频:需结合多媒体分析技术(如OCR、语音识别)转换为文本。
2. 数据获取方法
- 公开数据爬取:
- 使用爬虫工具(如Python的Scrapy、BeautifulSoup)从网站提取数据。
- 注意合规性:遵守网站robots协议,避免侵犯隐私。
- 数据库对接:
- 通过ETL(Extract-Transform-Load)工具从企业内部数据库(如Oracle、SQL Server)抽取数据。
- API接口获取:
- 调用公开API(如Google Knowledge Graph API、百度百科API)获取结构化数据。
- 人工录入/众包:
- 对缺失或高价值数据(如专家经验)进行人工标注,或通过众包平台(如Amazon Mechanical Turk)收集。
三、知识抽取:从数据中提取实体、关系和属性
知识抽取(Knowledge Extraction)是将多源数据转化为结构化知识的核心步骤,需针对不同数据类型采用不同技术。
1. 实体抽取(命名实体识别,NER)
- 目标:从文本中识别具有特定类别的实体(如人名、地名、机构名)。
- 方法:
- 规则-based方法:使用正则表达式、词典匹配(如通过疾病词典识别“糖尿病”)。
- 统计学习方法:
- 模型:CRF(条件随机场)、BiLSTM-CRF、BERT-CRF。
- 工具:spaCy、NLTK、HanLP(中文)。
- 深度学习方法:
- 预训练模型:BERT、RoBERTa、ERNIE(中文),Fine-tune后用于NER。
- 工具:Hugging Face Transformers库。
- 挑战:
- 歧义词(如“苹果”可能指水果或公司)。
- 新词识别(如新兴疾病名称)。
2. 关系抽取(RE)
- 目标:识别实体之间的语义关系(如“药物-治疗-疾病”)。
- 方法:
- 远程监督(Distant Supervision):
- 假设包含相同实体对的句子具有相同关系(如“阿司匹林治疗头痛”→“治疗”关系)。
- 结合知识库(如Wikidata)自动生成训练数据。
- 监督学习方法:
- 模型:CNN、RNN、GNN(图神经网络),或基于预训练模型的分类器(如BERT+分类层)。
- 规则与模式匹配:
- 使用依存句法分析(如“主语-谓语-宾语”结构)提取关系,例如“患者[主语]患有[谓语]肺癌[宾语]”。
- 远程监督(Distant Supervision):
- 工具:Stanza(多语言NLP工具包)、DeepPavlov(关系抽取模型)。
3. 属性抽取
- 目标:提取实体的属性值(如“疾病”的“高发人群”)。
- 方法:
- 从结构化数据中直接映射(如数据库表字段)。
- 从文本中抽取:通过正则表达式(如“年龄:(\d+)岁”)或序列标注模型(将属性值视为实体)。
四、知识融合:消除数据冲突,统一知识表示
知识融合旨在整合多源数据,解决实体歧义、冗余和冲突问题,形成统一的知识图谱。
1. 实体对齐(Entity Alignment)
- 目标:识别不同数据源中表示同一现实实体的节点(如“阿司匹林”在不同数据库中的记录)。
- 方法:
- 基于规则的对齐:
- 匹配实体名称、属性值(如“药品通用名”+“分子式”相同则视为同一实体)。
- 基于嵌入的对齐:
- 将实体编码为低维向量(如TransE、RESCAL模型),计算向量相似度(如余弦距离)。
- 混合方法:结合规则和机器学习模型(如SVM、神经网络)进行对齐。
- 基于规则的对齐:
- 工具:
- Dedupe(Python库,用于重复数据检测)。
- Falcon-AO(实体对齐工具,支持大规模知识图谱)。
2. 冲突消解
- 解决数据不一致问题:
- 优先级策略:指定数据源优先级(如权威数据库>百科>用户生成内容)。
- 投票机制:多个数据源冲突时,取多数一致结果。
- 人工审核:对高风险冲突(如医疗数据)进行人工确认。
3. 知识归一化
- 统一实体命名和属性值格式:
- 实体名:如统一“慢阻肺”和“慢性阻塞性肺疾病”为标准名称。
- 属性值:如日期格式统一为“YYYY-MM-DD”,数值单位统一(如“千克”“kg”统一为“kg”)。
五、知识存储:选择合适的图数据库
知识图谱通常以图结构(节点-边-属性)存储,需根据数据规模和查询需求选择数据库。
1. 图数据库分类
- 属性图数据库:
- 适用场景:中小企业级应用,支持复杂查询和实时更新。
- 代表:Neo4j(最常用)、ArangoDB、OrientDB。
- 特点:使用Cypher查询语言,支持事务和索引。
- RDF数据库:
- 适用场景:学术研究、语义网应用,需严格遵循RDF标准。
- 代表:Apache Jena、Stardog、GraphDB。
- 特点:支持OWL推理,查询语言为SPARQL。
- 分布式图数据库:
- 适用场景:超大规模数据(数十亿节点),如社交网络、推荐系统。
- 代表:Dgraph、JanusGraph、AWS Neptune(支持属性图和RDF)。
- 特点:支持水平扩展,基于Spark或Flink等分布式框架。
2. 存储方案选择
- 小规模场景:Neo4j单机版,易于部署和开发。
- 大规模场景:JanusGraph+Cassandra/Elasticsearch,支持分布式存储和查询。
- 语义推理需求:Stardog或GraphDB,支持OWL本体和SPARQL推理。
六、知识推理:挖掘隐含知识
知识推理通过已有的知识推断出新的关系或实体,解决知识图谱的不完整性问题。
1. 推理方法
- 基于规则的推理:
- 定义逻辑规则,自动推导新关系。
- 示例:若“X是Y的父母,Y是Z的父母”,则“X是Z的祖父母”。
- 工具:Jena的Rule Engine、Drools(业务规则引擎)。
- 基于表示学习的推理:
- 将实体和关系嵌入向量空间(如TransE、RotatE模型),通过向量计算预测缺失关系。
- 示例:通过“药物-治疗-疾病”关系向量,推断“药物A可能治疗疾病B”。
- 工具:PyTorch-BigGraph、OpenKE。
- 基于图神经网络(GNN)的推理:
- 利用图结构信息(如节点邻居特征)预测关系,如GraphSAGE、GAT模型。
- 适用于推荐系统、欺诈检测等场景。
2. 推理应用场景
- 缺失关系补全:预测“基因-疾病”关联关系。
- 不一致性检测:发现知识图谱中的逻辑矛盾(如“某人年龄为-5岁”)。
- 层级关系推导:自动构建实体的上下位关系(如通过“肺癌是一种恶性肿瘤”推断“肺癌IsA肿瘤”)。
七、知识应用:构建智能应用
知识图谱的价值体现在其应用场景中,常见应用包括:
1. 问答系统(QA)
- 流程:
- 用户提问解析(如“糖尿病的症状有哪些?”)。
- 实体识别(“糖尿病”)和关系识别(“症状”)。
- 图查询(在知识图谱中查找“糖尿病”的“症状”属性)。
- 结果整理与自然语言回答。
- 工具:DeepQA(IBM Watson技术栈)、Rasa(结合知识图谱的对话系统)。
2. 推荐系统
- 方法:
- 构建用户-商品-属性的知识图谱,分析用户偏好(如“用户A喜欢品牌B的红色运动鞋”)。
- 通过图算法(如PageRank、协同过滤)推荐相关商品。
- 案例:电商平台通过知识图谱分析商品关联(如“购买手机的用户常买充电器”)。
3. 决策支持
- 应用场景:
- 金融领域:构建企业知识图谱,分析股权结构、关联交易,识别风险。
- 医疗领域:辅助诊断,根据患者症状、病史和知识图谱中的疾病特征推荐诊断方向。
4. 语义搜索
- 与传统搜索的区别:
- 传统搜索:基于关键词匹配(如搜索“肺癌”返回包含该词的网页)。
- 语义搜索:理解查询意图,返回结构化知识(如“肺癌的治疗药物有哪些?”直接列出药物列表)。
- 实现方式:
- 使用SPARQL查询知识图谱,结合自然语言处理将用户查询转换为图数据库查询语句。
八、知识图谱维护与更新
知识图谱需持续维护以保证时效性和准确性,流程包括:
1. 数据监控与增量更新
- 实时/定时爬取:监控数据源变化(如新闻网站更新疾病信息),触发增量抽取。
- 版本控制:记录知识图谱的更新历史,支持回滚(如使用Git或图数据库自带的版本管理功能)。
2. 质量评估
- 指标:
- 完整性:实体覆盖率、关系覆盖率(如知识图谱中是否包含90%以上的已知疾病)。
- 准确性:实体对齐准确率、属性值正确率(通过人工抽样验证)。
- 一致性:无矛盾关系(如“药物A治疗疾病B”与“药物A禁忌疾病B”不能并存)。
- 工具:编写脚本自动检测数据质量(如重复实体、无效关系),生成质量报告。
3. 人工干预与反馈机制
- 建立用户反馈渠道(如APP内的“纠错”按钮),收集用户发现的错误知识。
- 对高优先级领域(如医疗、金融)定期进行专家审核,确保知识可靠性。
九、典型工具链与案例
1. 工具链示例(以医疗知识图谱为例)
- 数据获取:爬虫获取医学指南(非结构化文本)+医院数据库(结构化数据)。
- 知识抽取:
- NER:使用BERT-ERNIE模型识别“疾病”“药品”实体。
- 关系抽取:基于远程监督和依存句法提取“药物-治疗-疾病”关系。
- 知识融合:通过Dedupe对齐不同数据源中的“药品”实体,人工审核冲突数据。
- 存储与推理:Neo4j存储属性图,使用Cypher查询;结合规则引擎推导“并发症”关系。
- 应用:搭建医疗问答系统,支持自然语言查询疾病相关知识。
2. 开源项目与资源
- 通用知识图谱:
- Wikidata:多语言百科知识库,可直接用于构建领域图谱的基础框架。
- DBpedia:从维基百科提取的结构化数据,支持SPARQL查询。
- 工具库:
- PyTorch-BigGraph:Facebook开源的分布式图表示学习库,适合大规模知识图谱。
- Stardog:企业级RDF数据库,支持知识推理和SQL/SPARQL混合查询。
- AIGC辅助工具:如ChatGPT可辅助生成实体关系模板、标注数据等。
以下是结合知识图谱构建方法的实践案例,以**“电影知识图谱”**为例,详细展示从数据获取到应用的全流程,并附具体代码和工具操作示例,帮助理解理论与实践的结合。
十、实践案例:电影知识图谱构建
目标
构建一个包含电影、演员、导演、类型、奖项等信息的知识图谱,支持电影推荐、问答(如“诺兰导演的科幻片有哪些?”)和语义搜索。
(一)、知识建模:定义电影领域Schema
1. 实体类型
实体类型 | 说明 |
---|---|
电影(Movie) | 电影本体,如《星际穿越》 |
演员(Actor) | 参演电影的演员 |
导演(Director) | 执导电影的导演 |
类型(Genre) | 电影类型,如科幻、悬疑 |
奖项(Award) | 电影获得的奖项,如奥斯卡 |
2. 关系类型
关系类型 | 方向 | 说明 |
---|---|---|
主演(starring) | Movie ← Actor | 演员主演某部电影 |
导演(directed_by) | Movie ← Director | 导演执导某部电影 |
属于类型(has_genre) | Movie → Genre | 电影属于某类型 |
获得奖项(won_award) | Movie → Award | 电影获得某个奖项 |
同剧演员(co_star) | Actor ↔ Actor | 演员共同出演同一部电影 |
3. 属性定义
- 电影属性:上映年份(year)、评分(rating)、时长(duration)、剧情简介(description)。
- 演员属性:出生日期(birth_date)、国籍(nationality)。
- 导演属性:代表作(representative_works)。
- 奖项属性:颁奖机构(organizer)、年份(year)、奖项名称(award_name)。
(二)、数据获取:爬取电影数据
1. 数据源选择
- 公开API:TMDB(The Movie Database,提供电影元数据)。
- 网页爬取:豆瓣电影榜(获取用户评分和评论)。
2. 使用Python爬取TMDB数据
import requests
import json
# TMDB API密钥(需在TMDB官网申请)
API_KEY = "your_api_key"
BASE_URL = "https://api.themoviedb.org/3"
def fetch_movies(page=1):
url = f"{BASE_URL}/discover/movie?api_key={API_KEY}&page={page}"
response = requests.get(url)
data = json.loads(response.text)
return data["results"]
# 爬取前10页数据(约200部电影)
movies = []
for page in range(1, 11):
movies += fetch_movies(page)
# 保存为JSON文件
with open("movies.json", "w", encoding="utf-8") as f:
json.dump(movies, f, ensure_ascii=False, indent=2)
3. 数据字段映射
- TMDB返回字段:
title
(电影名)、release_date
(上映日期)、vote_average
(评分)、genres
(类型列表)、cast
(演员列表)、crew
(导演等剧组人员)。 - 清洗后提取关键信息:
def process_movie(movie): processed = { "movie_id": movie["id"], "title": movie["title"], "year": int(movie["release_date"].split("-")[0]) if movie["release_date"] else None, "rating": movie["vote_average"], "genres": [g["name"] for g in movie["genres"]], "actors": [], # 从cast中提取演员 "director": None # 从crew中提取导演 } # 处理演员 for cast_member in movie.get("cast", []): if cast_member["known_for_department"] == "Acting": processed["actors"].append({ "actor_id": cast_member["id"], "name": cast_member["name"], "character": cast_member["character"] }) # 处理导演 for crew_member in movie.get("crew", []): if crew_member["job"] == "Director": processed["director"] = { "director_id": crew_member["id"], "name": crew_member["name"] } return processed processed_movies = [process_movie(m) for m in movies]
(三)、知识抽取:从非结构化数据中补充信息
1. 需求:从豆瓣评论中提取电影剧情关键词(属性扩展)
- 工具:使用Hugging Face的
pipeline
进行文本摘要。from transformers import pipeline summarizer = pipeline("summarization", model="facebook/bart-large-cnn") def extract_plot_keywords(reviews): # 合并多条评论为一段文本 combined_text = " ".join(reviews) # 生成摘要(提取关键剧情信息) summary = summarizer(combined_text, max_length=100, min_length=30, do_sample=False)[0]["summary_text"] return summary.split(", ") # 按逗号分割关键词 # 模拟豆瓣评论数据(实际需爬取) sample_reviews = [ "《星际穿越》探讨了时空穿越和亲情,视觉效果震撼", "剧情烧脑,演员演技在线,尤其是马修·麦康纳的表演" ] plot_keywords = extract_plot_keywords(sample_reviews) print(plot_keywords) # 输出:['时空穿越', '亲情', '视觉效果震撼', '剧情烧脑', '演员演技在线']
(四)、知识融合:对齐实体与消歧
1. 问题:不同数据源中演员名称不一致(如“克里斯蒂安·贝尔” vs “Christian Bale”)
2. 解决方法:基于名称相似度对齐
- 工具:使用
fuzzywuzzy
库计算字符串相似度。from fuzzywuzzy import fuzz def is_same_actor(actor1, actor2): # 忽略大小写,计算模糊匹配分数 score = fuzz.ratio(actor1.lower(), actor2.lower()) return score > 85 # 相似度阈值设为85% # 示例:对齐TMDB中的“Christian Bale”和豆瓣中的“克里斯蒂安·贝尔” score = fuzz.ratio("Christian Bale", "克里斯蒂安·贝尔") print(score) # 输出:89(超过阈值,视为同一实体)
3. 冲突消解:优先采用权威数据源(如TMDB)的ID作为唯一标识
- 为每个实体分配全局唯一ID(如
movie:123
、actor:456
),通过ID关联不同数据源。
(五)、知识存储:使用Neo4j构建图数据库
1. 安装与配置Neo4j
- 下载地址:Neo4j官网,选择社区版(免费)。
- 启动后访问
http://localhost:7474
,默认用户名/密码:neo4j/neo4j
(首次登录需修改密码)。
2. 导入数据到Neo4j
-
方法1:使用Cypher语句批量插入
from neo4j import GraphDatabase uri = "bolt://localhost:7687" driver = GraphDatabase.driver(uri, auth=("neo4j", "your_password")) def create_movie_node(tx, movie): tx.run( "CREATE (m:Movie {movie_id: $movie_id, title: $title, year: $year, rating: $rating})", movie_id=movie["movie_id"], title=movie["title"], year=movie["year"], rating=movie["rating"] ) with driver.session() as session: for m in processed_movies: session.write_transaction(create_movie_node, m)
-
方法2:使用Neo4j的LOAD CSV功能
- 将数据转换为CSV格式(如
movies.csv
、actors.csv
)。 - 在Neo4j浏览器中执行:
LOAD CSV WITH HEADERS FROM "file:///movies.csv" AS row CREATE (m:Movie {movie_id: toInteger(row.movie_id), title: row.title, year: toInteger(row.year), rating: toFloat(row.rating)})
- 将数据转换为CSV格式(如
3. 创建关系
// 创建“主演”关系
MATCH (a:Actor {actor_id: 123}), (m:Movie {movie_id: 456})
CREATE (a)-[:starring]->(m)
// 创建“导演”关系
MATCH (d:Director {director_id: 789}), (m:Movie {movie_id: 456})
CREATE (d)-[:directed_by]->(m)
(六)、知识推理:推导“同剧演员”关系
1. 规则定义
- 若两名演员共同出演同一部电影,则他们之间存在
co_star
关系。
2. 使用Cypher执行推理
// 批量创建同剧演员关系
MATCH (a1:Actor)-[:starring]->(m:Movie)<-[:starring]-(a2:Actor)
WHERE a1.actor_id < a2.actor_id // 避免重复创建双向关系
CREATE (a1)-[:co_star]-(a2)
SET a1.co_star_count = coalesce(a1.co_star_count, 0) + 1,
a2.co_star_count = coalesce(a2.co_star_count, 0) + 1
(七)、知识应用:搭建电影问答系统
1. 需求:回答“诺兰导演的科幻片有哪些?”
2. 实现步骤
-
步骤1:解析用户问题
- 使用
spaCy
识别实体和关系:import spacy nlp = spacy.load("en_core_web_sm") def parse_question(question): doc = nlp(question) entities = [ent.text for ent in doc.ents if ent.label_ in ["PERSON", "WORK_OF_ART"]] relations = [] for token in doc: if token.dep_ == "dobj" and token.head.text == "导演": relations.append("directed_by") elif token.text == "科幻片": relations.append("has_genre") return {"entities": entities, "relations": relations} question = "诺兰导演的科幻片有哪些?" parsed = parse_question(question) print(parsed) # 输出:{"entities": ["诺兰", "科幻片"], "relations": ["directed_by", "has_genre"]}
- 使用
-
步骤2:生成Cypher查询
def generate_cypher(entities, relations): director_name = entities[0] genre = entities[1] return f""" MATCH (d:Director {{name: "{director_name}"}})-[:directed_by]->(m:Movie)-[:has_genre]->(g:Genre {{name: "{genre}"}}) RETURN m.title, m.year, m.rating """ cypher_query = generate_cypher(parsed["entities"], parsed["relations"]) print(cypher_query)
-
步骤3:执行查询并返回结果
with driver.session() as session: result = session.run(cypher_query) movies = [record["m.title"] for record in result] print(f"诺兰导演的科幻片有:{', '.join(movies)}")
(八)、实践工具与资源总结
阶段 | 工具/库 | 作用 |
---|---|---|
数据获取 | requests | 爬取TMDB API数据 |
数据清洗 | pandas | 处理JSON数据,提取关键字段 |
知识抽取 | Hugging Face Transformers | 文本摘要、命名实体识别 |
知识融合 | fuzzywuzzy | 实体名称相似度匹配 |
知识存储 | Neo4j + Cypher | 图数据库存储与查询 |
应用开发 | Flask + spaCy | 搭建问答系统API |
(九)、实践中常见问题与解决方案
- 数据缺失:
- 处理方法:通过多个数据源互补(如同时使用TMDB和豆瓣数据),或使用生成模型(如GPT-3)补全简介等文本信息。
- 性能瓶颈:
- 处理方法:对Neo4j进行索引优化(如为
movie.title
、actor.name
创建索引),或迁移至分布式图数据库(如Dgraph)。
- 处理方法:对Neo4j进行索引优化(如为
- 实体歧义:
- 处理方法:结合实体属性(如出生日期、国籍)辅助消歧,或引入外部知识库(如Wikidata)验证实体唯一性。
(十)、扩展实践建议
- 多模态数据集成:
- 爬取电影海报图片,存储为实体属性,或使用计算机视觉技术提取海报中的文本和视觉特征(如颜色、人物)。
- 推荐系统集成:
- 在知识图谱中添加用户观影记录,通过图算法(如Personalized PageRank)生成个性化推荐。
- 实时更新:
- 使用消息队列(如Kafka)监听数据源变化,触发增量更新流程,保持知识图谱实时性。
十一、挑战与未来趋势
1. 主要挑战
- 数据稀疏性:小众领域(如罕见病)数据不足,难以构建完整图谱。
- 多模态数据处理:图像、视频等非结构化数据的语义理解仍需突破。
- 可解释性:深度学习模型在知识推理中的决策过程难以解释,影响医疗、金融等领域的可信度。
- 实时性要求:部分场景(如实时推荐)需要知识图谱秒级更新,对存储和计算性能要求高。
2. 未来趋势
- AIGC与知识图谱结合:利用大语言模型(LLM)自动生成缺失知识,如通过GPT-4补全罕见病的症状描述。
- 联邦学习在知识融合中的应用:在不共享原始数据的前提下,跨机构联合构建知识图谱(如医疗数据隐私保护)。
- 时空知识图谱:引入时间和空间维度(如“事件-时间-地点”关系),支持动态场景分析(如疫情传播建模)。
- 轻量化知识图谱:针对边缘计算设备(如智能终端),优化存储和推理算法,降低计算资源消耗。
十二、一个完整的知识图谱构建案例
完整的电影知识图谱构建解决方案,包括:
- 数据获取:从TMDB API获取电影、演员、导演和类型数据
- 数据处理:提取实体和关系,进行数据清洗
- 知识抽取:使用transformers从电影概述中提取关键词
- 知识存储:将数据导入Neo4j图数据库,创建节点和关系
- 知识查询:实现简单的问答系统和电影推荐功能
使用前需要:
- 在TMDB官网注册账号并获取API密钥
- 安装Neo4j数据库并启动服务
- 安装必要的Python库:
requests
,neo4j
,transformers
,fuzzywuzzy
,spacy
,tqdm
- 下载spaCy英文模型:
python -m spacy download en_core_web_sm
代码设计考虑了扩展性,可以根据需要添加更多的实体类型、关系和功能,如情感分析、多模态处理等。
完整代码:
import requests
import json
import os
import pandas as pd
from fuzzywuzzy import fuzz
from transformers import pipeline
from neo4j import GraphDatabase
from tqdm import tqdm
import spacy
class MovieKnowledgeGraphBuilder:
def __init__(self, tmdb_api_key, neo4j_uri, neo4j_user, neo4j_password):
"""初始化电影知识图谱构建器"""
self.tmdb_api_key = tmdb_api_key
self.neo4j_driver = GraphDatabase.driver(neo4j_uri, auth=(neo4j_user, neo4j_password))
self.nlp = spacy.load("en_core_web_sm") # 用于问题解析
self.summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
def fetch_movies_from_tmdb(self, num_pages=10, save_path="movies.json"):
"""从TMDB API获取电影数据"""
if os.path.exists(save_path):
print(f"数据已存在,从文件加载: {save_path}")
with open(save_path, "r", encoding="utf-8") as f:
return json.load(f)
movies = []
for page in tqdm(range(1, num_pages + 1), desc="爬取电影数据"):
url = f"https://api.themoviedb.org/3/discover/movie?api_key={self.tmdb_api_key}&page={page}"
response = requests.get(url)
if response.status_code == 200:
page_data = response.json()
movies.extend(page_data.get("results", []))
else:
print(f"请求失败,状态码: {response.status_code}")
with open(save_path, "w", encoding="utf-8") as f:
json.dump(movies, f, ensure_ascii=False, indent=2)
return movies
def process_movie_data(self, movies):
"""处理电影数据,提取实体和关系"""
processed_movies = []
actors_dict = {}
directors_dict = {}
genres_dict = {}
for movie in tqdm(movies, desc="处理电影数据"):
# 提取电影基本信息
processed = {
"movie_id": movie["id"],
"title": movie["title"],
"year": int(movie["release_date"].split("-")[0]) if movie.get("release_date") else None,
"rating": movie["vote_average"],
"overview": movie.get("overview", ""),
"genres": []
}
# 处理电影类型
for genre in movie.get("genres", []):
genre_id = genre["id"]
genre_name = genre["name"]
genres_dict[genre_id] = genre_name
processed["genres"].append(genre_id)
# 获取电影详细信息(包含演员和导演)
movie_details = self._get_movie_details(movie["id"])
# 处理演员
processed["actors"] = []
for cast_member in movie_details.get("cast", [])[:10]: # 取前10位主演
actor_id = cast_member["id"]
actor_name = cast_member["name"]
character = cast_member["character"]
actors_dict[actor_id] = {
"name": actor_name,
"popularity": cast_member.get("popularity", 0),
"gender": cast_member.get("gender", 0)
}
processed["actors"].append({
"actor_id": actor_id,
"character": character
})
# 处理导演
for crew_member in movie_details.get("crew", []):
if crew_member["job"] == "Director":
director_id = crew_member["id"]
director_name = crew_member["name"]
directors_dict[director_id] = {
"name": director_name,
"popularity": crew_member.get("popularity", 0)
}
processed["director"] = director_id
break # 一部电影通常只有一个导演
processed_movies.append(processed)
return {
"movies": processed_movies,
"actors": actors_dict,
"directors": directors_dict,
"genres": genres_dict
}
def _get_movie_details(self, movie_id):
"""获取电影详细信息(包括演员和导演)"""
url = f"https://api.themoviedb.org/3/movie/{movie_id}?api_key={self.tmdb_api_key}&append_to_response=credits"
response = requests.get(url)
if response.status_code == 200:
return response.json()
return {}
def extract_plot_keywords(self, overview):
"""从电影概述中提取关键词"""
if not overview:
return []
# 使用BART模型生成摘要
try:
summary = self.summarizer(overview, max_length=30, min_length=10, do_sample=False)[0]["summary_text"]
# 简单分词作为关键词
return [word.strip() for word in summary.split(",") if word.strip()]
except:
return []
def build_knowledge_graph(self, data):
"""将处理后的数据导入Neo4j图数据库"""
with self.neo4j_driver.session() as session:
# 创建约束,确保唯一性
session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (m:Movie) REQUIRE m.movie_id IS UNIQUE")
session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (a:Actor) REQUIRE a.actor_id IS UNIQUE")
session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (d:Director) REQUIRE d.director_id IS UNIQUE")
session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (g:Genre) REQUIRE g.genre_id IS UNIQUE")
# 导入电影节点
for movie in tqdm(data["movies"], desc="导入电影节点"):
keywords = self.extract_plot_keywords(movie["overview"])
session.run(
"""
MERGE (m:Movie {movie_id: $movie_id})
SET m.title = $title, m.year = $year, m.rating = $rating,
m.overview = $overview, m.keywords = $keywords
""",
movie_id=movie["movie_id"],
title=movie["title"],
year=movie["year"],
rating=movie["rating"],
overview=movie["overview"],
keywords=keywords
)
# 导入演员节点
for actor_id, actor in tqdm(data["actors"].items(), desc="导入演员节点"):
session.run(
"""
MERGE (a:Actor {actor_id: $actor_id})
SET a.name = $name, a.popularity = $popularity, a.gender = $gender
""",
actor_id=actor_id,
name=actor["name"],
popularity=actor["popularity"],
gender=actor["gender"]
)
# 导入导演节点
for director_id, director in tqdm(data["directors"].items(), desc="导入导演节点"):
session.run(
"""
MERGE (d:Director {director_id: $director_id})
SET d.name = $name, d.popularity = $popularity
""",
director_id=director_id,
name=director["name"],
popularity=director["popularity"]
)
# 导入类型节点
for genre_id, genre_name in tqdm(data["genres"].items(), desc="导入类型节点"):
session.run(
"""
MERGE (g:Genre {genre_id: $genre_id})
SET g.name = $name
""",
genre_id=genre_id,
name=genre_name
)
# 创建电影-演员关系
for movie in tqdm(data["movies"], desc="创建电影-演员关系"):
for actor_info in movie["actors"]:
session.run(
"""
MATCH (m:Movie {movie_id: $movie_id})
MATCH (a:Actor {actor_id: $actor_id})
MERGE (a)-[r:ACTED_IN {character: $character}]->(m)
""",
movie_id=movie["movie_id"],
actor_id=actor_info["actor_id"],
character=actor_info["character"]
)
# 创建电影-导演关系
for movie in tqdm(data["movies"], desc="创建电影-导演关系"):
if "director" in movie and movie["director"]:
session.run(
"""
MATCH (m:Movie {movie_id: $movie_id})
MATCH (d:Director {director_id: $director_id})
MERGE (d)-[r:DIRECTED]->(m)
""",
movie_id=movie["movie_id"],
director_id=movie["director"]
)
# 创建电影-类型关系
for movie in tqdm(data["movies"], desc="创建电影-类型关系"):
for genre_id in movie["genres"]:
session.run(
"""
MATCH (m:Movie {movie_id: $movie_id})
MATCH (g:Genre {genre_id: $genre_id})
MERGE (m)-[r:BELONGS_TO]->(g)
""",
movie_id=movie["movie_id"],
genre_id=genre_id
)
# 创建演员-演员合作关系
print("创建演员合作关系...")
session.run(
"""
MATCH (a1:Actor)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(a2:Actor)
WHERE a1.actor_id < a2.actor_id
MERGE (a1)-[r:CO_STARRED_WITH]-(a2)
ON CREATE SET r.movie_count = 1
ON MATCH SET r.movie_count = r.movie_count + 1
"""
)
def answer_question(self, question):
"""基于知识图谱回答问题"""
doc = self.nlp(question)
# 提取实体和关系
entities = []
relations = []
for ent in doc.ents:
entities.append(ent.text)
# 简单规则匹配关系类型
if "导演" in question or "directed" in question.lower():
relations.append("DIRECTED")
if "主演" in question or "acted" in question.lower():
relations.append("ACTED_IN")
if "类型" in question or "genre" in question.lower():
relations.append("BELONGS_TO")
if "合作" in question or "co-starred" in question.lower():
relations.append("CO_STARRED_WITH")
# 基于提取的信息生成Cypher查询
if not entities or not relations:
return "抱歉,我无法理解您的问题。"
# 简单问答模板匹配
if "导演" in question and "电影" in question:
# 问题示例:"诺兰导演的电影有哪些?"
director_name = entities[0]
query = f"""
MATCH (d:Director {{name: $director_name}})-[:DIRECTED]->(m:Movie)
RETURN m.title AS title, m.year AS year, m.rating AS rating
ORDER BY m.rating DESC
"""
params = {"director_name": director_name}
with self.neo4j_driver.session() as session:
result = session.run(query, **params)
movies = [f"{record['title']} ({record['year']}, 评分: {record['rating']})" for record in result]
if movies:
return f"{director_name}导演的电影有:\n" + "\n".join(movies)
else:
return f"抱歉,我没有找到{director_name}导演的电影。"
elif "演员" in question and "电影" in question:
# 问题示例:"莱昂纳多主演的电影有哪些?"
actor_name = entities[0]
query = f"""
MATCH (a:Actor {{name: $actor_name}})-[:ACTED_IN]->(m:Movie)
RETURN m.title AS title, m.year AS year, m.rating AS rating
ORDER BY m.rating DESC
"""
params = {"actor_name": actor_name}
with self.neo4j_driver.session() as session:
result = session.run(query, **params)
movies = [f"{record['title']} ({record['year']}, 评分: {record['rating']})" for record in result]
if movies:
return f"{actor_name}主演的电影有:\n" + "\n".join(movies)
else:
return f"抱歉,我没有找到{actor_name}主演的电影。"
elif "类型" in question and "电影" in question:
# 问题示例:"有哪些科幻电影?"
genre_name = entities[0]
query = f"""
MATCH (m:Movie)-[:BELONGS_TO]->(g:Genre {{name: $genre_name}})
RETURN m.title AS title, m.year AS year, m.rating AS rating
ORDER BY m.rating DESC
LIMIT 10
"""
params = {"genre_name": genre_name}
with self.neo4j_driver.session() as session:
result = session.run(query, **params)
movies = [f"{record['title']} ({record['year']}, 评分: {record['rating']})" for record in result]
if movies:
return f"以下是一些{genre_name}类型的电影:\n" + "\n".join(movies)
else:
return f"抱歉,我没有找到{genre_name}类型的电影。"
elif "合作" in question and len(entities) == 2:
# 问题示例:"莱昂纳多和凯特温斯莱特合作过哪些电影?"
actor1_name = entities[0]
actor2_name = entities[1]
query = f"""
MATCH (a1:Actor {{name: $actor1_name}})-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(a2:Actor {{name: $actor2_name}})
RETURN m.title AS title, m.year AS year
ORDER BY m.year DESC
"""
params = {"actor1_name": actor1_name, "actor2_name": actor2_name}
with self.neo4j_driver.session() as session:
result = session.run(query, **params)
movies = [f"{record['title']} ({record['year']})" for record in result]
if movies:
return f"{actor1_name}和{actor2_name}合作过的电影有:\n" + "\n".join(movies)
else:
return f"抱歉,我没有找到{actor1_name}和{actor2_name}合作过的电影。"
return "抱歉,我还无法回答这类问题。"
def recommend_movies(self, movie_title, limit=5):
"""基于知识图谱推荐相似电影"""
query = """
MATCH (m:Movie {title: $movie_title})-[:BELONGS_TO]->(g:Genre)<-[:BELONGS_TO]-(rec:Movie)
WHERE rec.title <> $movie_title
WITH rec, COUNT(g) AS genre_overlap, COLLECT(g.name) AS genres
ORDER BY genre_overlap DESC, rec.rating DESC
LIMIT $limit
RETURN rec.title AS title, rec.year AS year, rec.rating AS rating, genres
"""
with self.neo4j_driver.session() as session:
result = session.run(query, movie_title=movie_title, limit=limit)
recommendations = []
for record in result:
recommendation = {
"title": record["title"],
"year": record["year"],
"rating": record["rating"],
"genres": record["genres"]
}
recommendations.append(recommendation)
if recommendations:
print(f"基于《{movie_title}》的推荐电影:")
for i, rec in enumerate(recommendations, 1):
print(f"{i}. {rec['title']} ({rec['year']}) - 评分: {rec['rating']}")
print(f" 类型: {', '.join(rec['genres'])}")
return recommendations
else:
print(f"抱歉,没有找到与《{movie_title}》相似的电影。")
return []
def close(self):
"""关闭Neo4j驱动连接"""
self.neo4j_driver.close()
# 使用示例
if __name__ == "__main__":
# 配置信息(请替换为您自己的信息)
TMDB_API_KEY = "your_tmdb_api_key"
NEO4J_URI = "bolt://localhost:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "your_neo4j_password"
# 初始化构建器
builder = MovieKnowledgeGraphBuilder(TMDB_API_KEY, NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD)
# 1. 获取数据
movies = builder.fetch_movies_from_tmdb(num_pages=5)
# 2. 处理数据
processed_data = builder.process_movie_data(movies)
# 3. 构建知识图谱
builder.build_knowledge_graph(processed_data)
# 4. 问答示例
questions = [
"克里斯托弗·诺兰导演的电影有哪些?",
"莱昂纳多·迪卡普里奥主演的电影有哪些?",
"有哪些科幻电影?",
"莱昂纳多·迪卡普里奥和凯特·温斯莱特合作过哪些电影?"
]
for question in questions:
answer = builder.answer_question(question)
print(f"\n问题:{question}")
print(f"回答:{answer}")
# 5. 推荐示例
builder.recommend_movies("Inception", limit=3)
# 关闭连接
builder.close()
总结
知识图谱的构建是一个“迭代优化”的过程,需结合领域特点选择合适的技术方案,并在实践中不断调整建模逻辑、优化抽取算法、提升数据质量。随着AI技术的发展,知识图谱将更深度融合机器学习、多模态处理和边缘计算,成为支撑智能应用的核心基础设施。