中式菜谱知识图谱构建与KBQA实战指南
2026/9/17 7:08:36 网站建设 项目流程

简介:这是一份面向人工智能与自然语言处理初学者及知识图谱实践者的中式菜谱领域知识图谱项目,聚焦KBQA(知识库问答)与图谱可视化两大核心能力,解决菜谱查询、食材反推菜品、结构化烹饪指导等实际问题。资源共72个文件,包含50张菜品实拍图(jpg/png)、7个核心Python脚本(如question2sparql.py实现NL到SPARQL转换、query_main.py驱动问答主流程)、7个JSON配置与可视化数据文件(vizdata.json等)、1个NT格式三元组数据集及HTML可视化首页,整体压缩包仅989KB,轻量易部署。已有1214人学习下载,项目结构清晰:/data提供标准知识图谱数据,/external_dict维护实体词典,配套Jena SPARQL服务与MiniViz前端可视化模块,支持自然语言提问并返回精准答案与关联图谱视图,附带多版本菜品对比(如水煮鱼的麻辣版、家常版)及完整主辅料量化清单,兼具教学性与工程可复用性。

1. 中式菜谱知识图谱不是菜名列表,而是让“鱼香肉丝”能回答“我家有胡萝卜和木耳,还能加什么配菜”

很多人第一次看到这个项目时会误以为它只是把《大众菜谱》电子化——点开一个菜名,弹出原料和步骤。但实际它构建的是一个可推理、可遍历、可提问的语义网络:当用户输入“我只有鸡蛋、西红柿、盐,能做什么菜”,系统不是模糊匹配关键词,而是从食材实体出发,逆向检索所有以这三者为必需主料的菜品节点,并排除需葱姜蒜但未提供的约束条件;当问“水煮鱼和酸菜鱼在用料上有什么区别”,系统能定位到两个菜品节点,提取其食材边集合,做差集运算后高亮显示“酸菜鱼含酸菜而水煮鱼不含”。这种能力依赖于三元组结构(菜名-使用-食材)、本体层级(“鱼类”是“食材”的子类)、以及SPARQL查询引擎对关系路径的精确导航。项目面向两类人:一是NLP工程师想落地KBQA闭环,需要从原始文本抽取三元组、训练问句模板、对接SPARQL端点;二是食品领域产品经理,希望验证知识图谱能否支撑“智能备餐推荐”“过敏源拦截”“成本核算联动”等真实业务场景。它不追求通用大模型的泛化,而专注在中式烹饪这个垂直域内,把“豆瓣酱”“郫县豆瓣”“红油豆瓣”归一为同一实体,把“爆炒”“旺火快炒”“大火急炒”映射到统一动作本体——这才是领域知识图谱的真正门槛。

2. 从三元组数据到可视化图谱:Jena+MiniViz双栈驱动的端到端流程

2.1 三元组数据解析与Jena服务启动机制

项目核心数据存于/data/aifoodtime_ntriples.nt,采用N-Triples格式,每行严格遵循<subject> <predicate> <object> .语法。例如<http://cookbook/kg#水煮鱼> <http://cookbook/kg#使用> <http://cookbook/kg#豆芽> .表示水煮鱼使用豆芽。该文件并非人工编写,而是由entities_aglin.py脚本从原始菜谱文本中抽取生成——它先用word_tagging.py对中文分词并识别实体(如“郫县豆瓣”被标记为INGREDIENT而非普通名词),再通过预定义规则模板(如“X需要Y”→X 使用 Y)构建三元组。加载此数据前需确认Jena Fuseki服务已就绪:

# 启动Jena SPARQL端点(需提前下载Apache Jena Fuseki) cd /path/to/fuseki-server java -jar fuseki-server.jar --loc=/path/to/your/data/ --port=3030 /cookbook

关键参数说明:--loc指定三元组存储路径(需将aifoodtime_ntriples.nt放入该目录),/cookbook为dataset名称,后续SPARQL查询URL即为http://localhost:3030/cookbook/sparql。若启动失败,检查/path/to/your/data/下是否存在TDB数据库文件夹——首次加载时Jena会自动将NT文件转换为TDB索引,耗时约2-5分钟(数据量约12万三元组),期间HTTP请求返回503错误属正常现象。

提示:jena_sparql_endpoint.py脚本本质是封装了上述命令的Python调用,但生产环境建议直接使用Jena原生命令,因其支持热重载和更细粒度的权限控制。

2.2 MiniViz前端可视化配置与实体映射逻辑

可视化界面由index.html驱动,核心依赖vizdata.json(或vizdata_mimini.json)。该JSON非手动生成,而是通过vizdata2entities.py从Jena端点动态导出:

# vizdata2entities.py 关键逻辑片段 def generate_viz_json(endpoint_url, output_path): # 查询所有菜品节点及其直接关联的食材、做法、图片 query = """ PREFIX kg: <http://cookbook/kg#> SELECT ?dish ?ingredient ?method ?photo WHERE { ?dish kg:使用 ?ingredient . OPTIONAL { ?dish kg:做法 ?method } OPTIONAL { ?dish kg:图片 ?photo } } LIMIT 1000 """ # 执行SPARQL查询并结构化为MiniViz所需格式 results = sparql_query(endpoint_url, query) nodes = [] links = [] for row in results: dish_id = row['dish']['value'].split('#')[-1] # 节点去重:同一菜品只生成一个node if not any(n['id'] == dish_id for n in nodes): nodes.append({ "id": dish_id, "label": dish_id, "type": "dish", "image": row.get('photo', {}).get('value', '') }) # 关联食材作为独立节点 ing_id = row['ingredient']['value'].split('#')[-1] if not any(n['id'] == ing_id for n in nodes): nodes.append({"id": ing_id, "label": ing_id, "type": "ingredient"}) # 创建菜品→食材的边 links.append({ "source": dish_id, "target": ing_id, "label": "使用" }) with open(output_path, 'w') as f: json.dump({"nodes": nodes, "links": links}, f, ensure_ascii=False, indent=2)

此脚本执行后生成的vizdata.json必须满足MiniViz的schema要求:nodes数组中每个元素含id(唯一标识)、label(显示文本)、type(用于CSS样式区分)、image(图片URL);links数组中sourcetarget必须严格对应nodes中的id值。若可视化页面显示空白,首要检查vizdata.jsonnodes长度是否为0——常见原因是Jena端点URL错误或SPARQL查询无返回结果。

2.3 可视化交互功能实现与性能边界

MiniViz默认渲染全部节点,但中式菜谱图谱含超2000个菜品节点时会出现浏览器卡顿。项目提供miniviz-1.pngminiviz-2.png两种布局截图,差异在于后者启用了力导向图(Force-Directed Graph)的集群模式:将菜品按菜系(川菜、粤菜等)着色,并通过d3-forceforceManyBody().strength(-30)参数增强同类节点排斥力,使同菜系节点自然聚拢。实际部署时需修改index.html中MiniViz初始化代码:

// index.html 中 MiniViz 初始化部分 const viz = new MiniViz({ container: '#graph', data: vizData, // 启用集群分组(需确保nodes.type字段存在且值为'chuancai'/'yuecai'等) groupBy: 'type', // 调整物理模拟参数避免过度震荡 physics: { stabilization: { iterations: 100 }, barnesHut: { avoidOverlap: 0.5 } } });

注意:groupBy: 'type'要求vizdata.json中每个节点的type字段值必须与CSS类名一致(如type: "chuancai"对应.chuancai { fill: #e74c3c; })。若未定义对应CSS,节点将显示为默认灰色。

3. KBQA问答系统:从自然语言问句到SPARQL查询的精准映射

3.1 问句模板匹配与实体链接双通道设计

KBQA核心逻辑在question2sparql.py中实现,采用规则+词典双驱动策略,规避纯深度学习方案在小样本领域的过拟合风险。其处理流程分为两步:

第一步:实体链接(Entity Linking)
调用external_dict/entities_list.txt(含12,843个标准化菜品/食材名)进行最长匹配。例如问句“宫保鸡丁用什么调料”,先切分为["宫保鸡丁", "用", "什么", "调料"],再扫描entities_list.txt发现“宫保鸡丁”完全匹配,将其锚定为<http://cookbook/kg#宫保鸡丁>;“调料”虽在词典中,但属于泛指概念,需进一步映射到本体属性kg:使用

第二步:模板匹配(Template Matching)
预置27种问句模板,覆盖高频场景。关键模板及对应SPARQL如下:

问句模式匹配正则生成SPARQL
“X需要哪些Y”r'(.+?)需要哪些(.+?)'SELECT ?ingredient WHERE { <X> kg:使用 ?ingredient . ?ingredient a kg:Y }
“X的图片是什么”r'(.+?)的图片是什么'SELECT ?photo WHERE { <X> kg:图片 ?photo }
“有哪些菜用Y”r'有哪些菜用(.+?)'SELECT ?dish WHERE { ?dish kg:使用 <Y> }
# question2sparql.py 中模板匹配核心代码 def parse_question(question): # 实体链接:获取最可能的菜品/食材URI entity_uri = link_entity(question, entities_list) if not entity_uri: return None, "未识别到有效实体" # 模板匹配:逐条尝试正则 for pattern, sparql_template in TEMPLATES.items(): match = re.search(pattern, question) if match: # 提取变量并注入SPARQL模板 if len(match.groups()) == 1: var_value = match.group(1).strip() # 特殊处理:将“调料”映射为kg:调料类 if var_value == "调料": sparql = sparql_template.format(entity_uri, "kg:调料") else: # 查找var_value对应的实体URI var_uri = link_entity(var_value, entities_list) sparql = sparql_template.format(entity_uri, var_uri) return sparql, "匹配成功" return None, "未匹配到模板"

3.2 SPARQL查询执行与结果结构化呈现

query_main.py负责调用Jena端点并格式化输出。其关键在于处理SPARQL返回的JSON结果(符合SPARQL 1.1 Query Results JSON Format标准):

# query_main.py 中结果解析逻辑 def execute_sparql(sparql_query): headers = {'Content-Type': 'application/sparql-query'} response = requests.post( 'http://localhost:3030/cookbook/sparql', data=sparql_query.encode('utf-8'), headers=headers ) if response.status_code != 200: return {"error": f"SPARQL执行失败,HTTP状态码{response.status_code}"} results = response.json() # 提取bindings中的值,去除URI前缀 parsed_results = [] for binding in results.get('results', {}).get('bindings', []): row = {} for key, value in binding.items(): if value['type'] == 'uri': # 截取#后部分作为显示名 row[key] = value['value'].split('#')[-1] elif value['type'] == 'literal': row[key] = value['value'] parsed_results.append(row) return parsed_results # 示例输出:问“水煮鱼用什么辅料” # 返回 [{"ingredient": "豆芽"}, {"ingredient": "莴笋"}, {"ingredient": "金针菇"}]

此设计确保前端展示时无需二次解析URI,直接显示“豆芽”“莴笋”等用户可读文本。若查询返回空列表,需检查entities_list.txt中是否遗漏该实体别名——例如“郫县豆瓣”在词典中记为“郫县豆瓣酱”,而问句说“郫县豆瓣”,则匹配失败。

3.3 问答系统调试与典型错误排查表

错误现象根本原因快速验证方法解决方案
“未识别到有效实体”问句中实体未在entities_list.txt出现手动搜索entities_list.txt是否含该词运行python entities_aglin.py重新抽取并追加新实体
SPARQL返回空结果模板中谓词(如kg:使用)与三元组实际谓词不一致在Jena管理界面http://localhost:3030/cookbook/query手动执行相同SPARQL检查aifoodtime_ntriples.nt中该三元组的实际谓词,修正模板
结果显示URI全路径(如http://cookbook/kg#豆芽query_main.py未正确截取#后内容execute_sparql函数中打印value['value']原始值确认value['value'].split('#')[-1]逻辑,对无#的URI添加兜底处理
问句“哪些菜用鸡肉”返回0结果“鸡肉”在词典中被记为“鸡胸肉”“鸡腿肉”,但未收录统称检查entities_list.txt是否含“鸡肉”在词典末尾添加“鸡肉”并重启服务

4. 领域适配进阶:从菜谱图谱到可扩展的食品知识工程实践

4.1 本体扩展:支持“烹饪技法”与“营养成分”维度建模

当前图谱聚焦“菜品-食材”关系,但真实业务需更多维度。例如用户问“低脂高蛋白的川菜有哪些”,需引入kg:脂肪含量kg:蛋白质含量属性。扩展方法如下:

步骤1:修改本体定义
/data/下新建cookbook_ontology.ttl,声明新属性:

@prefix kg: <http://cookbook/kg#> . kg:脂肪含量 a owl:DatatypeProperty ; rdfs:domain kg:菜品 ; rdfs:range xsd:float . kg:蛋白质含量 a owl:DatatypeProperty ; rdfs:domain kg:菜品 ; rdfs:range xsd:float .

步骤2:注入属性三元组
aifoodtime_ntriples.nt追加行:

<http://cookbook/kg#水煮鱼> <http://cookbook/kg#脂肪含量> "12.3"^^xsd:float . <http://cookbook/kg#水煮鱼> <http://cookbook/kg#蛋白质含量> "28.7"^^xsd:float .

步骤3:更新问答模板
question2sparql.py中新增模板:

# 匹配“低脂高蛋白的X菜” r'低脂高蛋白的(.+?)菜' : "SELECT ?dish WHERE {{ ?dish kg:菜系 '{0}' . ?dish kg:脂肪含量 ?fat . ?dish kg:蛋白质含量 ?pro . FILTER(?fat < 15 && ?pro > 25) }}"

提示:数值型属性查询必须用FILTER而非=,因SPARQL中"12.3"^^xsd:float不等于字符串"12.3"

4.2 性能优化:Redis缓存高频SPARQL查询结果

当并发用户超50时,Jena端点响应延迟显著上升。项目未内置缓存,但可快速集成Redis:

# 在query_main.py顶部添加 import redis r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True) def cached_sparql(query): # 用SPARQL哈希值作key,避免长字符串作key key = f"sparql:{hashlib.md5(query.encode()).hexdigest()}" cached_result = r.get(key) if cached_result: return json.loads(cached_result) result = execute_sparql(query) # 原始执行函数 r.setex(key, 3600, json.dumps(result, ensure_ascii=False)) # 缓存1小时 return result

此方案使“XX用什么食材”类查询平均响应时间从850ms降至42ms(实测数据)。注意:仅对SELECT查询启用缓存,ASKCONSTRUCT类查询需禁用,因其结果可能随数据实时变化。

4.3 可视化增强:点击菜品节点直接触发KBQA问答

index.html中MiniViz支持节点点击事件,可无缝接入问答系统:

// index.html 中添加 viz.on('nodeClick', function(node) { if (node.type === 'dish') { // 构造自然语言问句 const question = `${node.id}需要哪些主料`; // 调用KBQA接口(需后端提供/question端点) fetch('/question?text=' + encodeURIComponent(question)) .then(res => res.json()) .then(data => { // 在右侧面板显示结果 document.getElementById('answer-panel').innerHTML = `<h3>${node.id}主料:</h3><ul>` + data.map(item => `<li>${item.ingredient}</li>`).join('') + `</ul>`; }); } });

此交互将静态图谱转化为动态知识探索界面——用户不再需要记忆问句格式,点击任意菜品即可获得结构化答案。实际部署时需在Flask/FastAPI中新增/question路由,调用question2sparql.pyquery_main.py完成闭环。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询