ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python美食知识图谱实战:从数据清洗到Neo4j问答系统

Python美食知识图谱实战:从数据清洗到Neo4j问答系统 简介这是一套基于Python构建的知识图谱型美食问答系统源码面向高校学生、教师及初学者用于课程设计、期末大作业或毕业设计实践解决自然语言提问与结构化美食知识检索之间的映射问题。资源共18个文件含6个核心Python模块如question_parser.py、answer_search.py、FoodUI.py等、2个JSON知识库文件food.json等、7个TXT词典文件涵盖菜系、食材、口味等实体、1个README.md说明文档及配套图标与示例图片整体压缩包仅162KB轻量易部署。已有163人学习下载体现其在教学实践中的实用热度。读者可直接运行完整GUI界面深入理解知识图谱构建build_foodgraph.py、问句分类question_classifier.py、语义解析与答案检索全流程代码注释清晰模块职责明确附带词典与数据样例便于二次开发或拓展至其他垂直领域问答系统。1. 这不是个“问答APP”而是一套可复现、可调试、可扩展的美食领域知识图谱工程实践如果你正在赶期末大作业看到“美食问答系统-基于Python知识图谱”这个标题第一反应可能是又一个调用现成API、拼几个关键词就返回菜名的demo错。真正能拿高分、经得起答辩追问的是能说清“为什么用Neo4j而不是SQLite存关系”“怎么把‘宫保鸡丁’和‘花生’‘干辣椒’‘糊辣味’在图里连起来”“用户问‘糖尿病人能吃啥甜点’时系统如何跳过糖分计算直接走本体推理路径”的完整链路。这个源码包的核心价值不在于它能回答几道菜的问题而在于它把知识图谱从概念落地为可运行、可验证、可修改的Python工程——从食材实体抽取、三元组清洗、图数据库建模到SPARQL查询改写、Cypher路径匹配、答案生成模板注入每一步都留有调试入口。适合需要交差但更想真正理解知识图谱构建逻辑的计算机/信息管理专业学生也适合想用最小成本验证领域图谱可行性的初级工程师。2. 用Python构建美食知识图谱从原始数据到Neo4j图数据库的四步闭环构建知识图谱不是先装Neo4j再填数据而是从数据形态反推图结构设计。这个源码包采用“食材-菜系-烹饪技法-营养成分-禁忌人群”五类核心节点用HAS_INGREDIENT、BELONGS_TO_CUISINE、USES_TECHNIQUE、CONTAINS_NUTRIENT、CONTRAINDICATED_FOR六种关系建模比单纯用JSON存菜谱多出3层语义关联能力。下面拆解从零开始的完整流程。2.1 数据准备用pandas清洗非结构化菜谱文本生成标准三元组CSV源码中data/recipe_raw.txt是典型爬虫结果混杂着“【主料】鸡胸肉200g、青椒1个…【做法】切丁→腌制→爆炒…”等非结构化描述。直接硬解析易出错正确做法是分阶段清洗import pandas as pd import re # 步骤1按菜名分割原始文本每道菜以【菜名】开头 with open(data/recipe_raw.txt, r, encodingutf-8) as f: raw_text f.read() recipes re.split(r【菜名】, raw_text)[1:] # 跳过首空段 # 步骤2对每道菜提取结构化字段正则需适配实际格式 triples [] for recipe in recipes: name_match re.search(r(.*?)\n, recipe) if not name_match: continue dish_name name_match.group(1).strip() # 提取主料示例规则实际需根据数据调整 ingredients_match re.search(r【主料】(.*?)\n, recipe) if ingredients_match: for ing in re.split(r[、,;], ingredients_match.group(1)): ing_clean re.sub(r\dg|\d克|\dml|\d毫升, , ing).strip() if ing_clean and len(ing_clean) 1: triples.append([dish_name, HAS_INGREDIENT, ing_clean]) # 步骤3导出为标准三元组CSV头行为subject,predicate,object df_triples pd.DataFrame(triples, columns[subject, predicate, object]) df_triples.to_csv(data/triples_clean.csv, indexFalse, encodingutf-8-sig)提示正则表达式必须针对你的原始数据微调。若原始数据含HTML标签需先用BeautifulSoup清理若存在“辅料”“调料”等多级分类需增加predicate字段映射规则如“辅料”→HAS_AUXILIARY_INGREDIENT避免所有关系都塞进HAS_INGREDIENT导致语义模糊。2.2 图模型设计用Neo4j Browser验证节点与关系的合理性Neo4j不是万能存储错误的图模型会让查询变慢甚至无法执行。该源码采用“中心化菜品节点”设计Dish为根节点所有属性口味、难度、耗时作为其属性而食材、菜系、技法等作为独立节点通过关系连接。这种设计优于把所有信息堆在Dish节点上原因有三① 食材复用率高“鸡蛋”出现在100道菜中单独建节点节省存储② 支持反向查询“哪些菜含花生”只需MATCH (i:Ingredient {name:花生})-[:HAS_INGREDIENT]-(d:Dish) RETURN d.name③ 便于扩展约束如给Ingredient节点加is_allergen: true属性标记过敏原。在Neo4j Browser中执行建模验证命令// 创建约束确保节点唯一性防止重复导入 CREATE CONSTRAINT ON (d:Dish) ASSERT d.name IS UNIQUE; CREATE CONSTRAINT ON (i:Ingredient) ASSERT i.name IS UNIQUE; CREATE CONSTRAINT ON (c:Cuisine) ASSERT c.name IS UNIQUE; // 导入三元组需提前将CSV放至Neo4j import目录 LOAD CSV WITH HEADERS FROM file:///triples_clean.csv AS row MERGE (s:Dish {name: row.subject}) MERGE (o:Ingredient {name: row.object}) CREATE (s)-[:HAS_INGREDIENT]-(o);注意MERGE比CREATE安全但首次导入时若CSV含脏数据如空字符串、重复行会导致约束冲突报错。建议先用pandas去重df_triples.drop_duplicates(inplaceTrue)再检查df_triples[subject].nunique()与df_triples[object].nunique()是否合理例如菜品数应远小于食材数。2.3 关系增强用Jieba词典补全隐含语义关系原始三元组只覆盖显性关系如“麻婆豆腐 HAS_INGREDIENT 豆腐”但用户提问常含隐含逻辑“不吃辣的人能吃啥”需知道“麻婆豆腐 USES_TECHNIQUE 煮”且“煮”属于“温和技法”而“辣”是“口味”属性。源码中utils/relation_enhancer.py通过两步补全词典驱动加载data/cuisine_dict.json含菜系-技法映射表对每道菜自动添加BELONGS_TO_CUISINE关系分词标注用Jieba对菜名分词后匹配预定义词典识别“酸辣”“清蒸”“红烧”等关键词生成HAS_FLAVOR或USES_TECHNIQUE关系。import jieba from jieba import posseg # 加载自定义词典提升“宫保鸡丁”等专有名词识别率 jieba.load_userdict(data/food_dict.txt) def extract_technique(dish_name): # 规则菜名含“蒸”“煮”“炖”→ USES_TECHNIQUE含“辣”“酸”→ HAS_FLAVOR tech_keywords [蒸, 煮, 炖, 焖, 煨] flavor_keywords [辣, 酸, 甜, 咸, 鲜] words posseg.cut(dish_name) for word, flag in words: if word in tech_keywords: return USES_TECHNIQUE, word elif word in flavor_keywords: return HAS_FLAVOR, word return None, None # 对所有菜品批量增强 for dish in dish_names: rel_type, value extract_technique(dish) if rel_type: # 生成新三元组并追加到CSV new_triple [dish, rel_type, value] # ... 写入文件逻辑关键参数说明jieba.load_userdict()路径必须是绝对路径或相对于脚本的正确路径posseg.cut()比cut()多返回词性但此处仅需关键词匹配故用基础分词即可。若发现“水煮鱼”被切为“水/煮/鱼”需在food_dict.txt中加入“水煮鱼”作为整体词条。3. 实现自然语言问答从用户问题到Cypher查询的映射引擎问答系统的核心不是NLP模型而是精准的“问题→图查询”翻译器。该源码放弃BERT等重型模型采用规则模板的轻量方案既保证本科生可理解又满足90%常见问题覆盖。3.1 问题分类器用正则匹配识别5类高频意图用户输入千变万化但美食领域问题高度结构化。源码qa/query_parser.py定义5类意图及对应正则模式意图类型正则示例匹配问题输出槽位INGREDIENT_QUERY.*[含有含.*?][什么CUISINE_QUERY.*[属于是].*?[什么哪].*?[菜系]SUBSTITUTE_QUERY.*[替代代替换].*?[什么NUTRITION_QUERY.*[含富含有没有].*?[蛋白RESTRICTION_QUERY.*[糖尿病人高血压孕妇].*?[能import re INTENT_PATTERNS { INGREDIENT_QUERY: r.*[含|有|含.*?][什么|哪些].*?[食材|原料|材料], CUISINE_QUERY: r.*[属于|是].*?[什么|哪].*?[菜系], SUBSTITUTE_QUERY: r.*[替代|代替|换].*?[什么|啥].*?[食材], NUTRITION_QUERY: r.*[含|富含|有没有].*?[蛋白|钙|糖|铁|维生素], RESTRICTION_QUERY: r.*[糖尿病人|高血压|孕妇|儿童].*?[能|不能].*?[吃] } def classify_intent(question): for intent, pattern in INTENT_PATTERNS.items(): if re.search(pattern, question): return intent return UNKNOWN # 示例调用 print(classify_intent(糖尿病人能吃啥甜点)) # 输出RESTRICTION_QUERY提示正则需用re.search()而非re.match()因用户问题可能以“请问”“我想知道”开头中文字符集要加re.U标志re.search(pattern, question, re.U)否则[含|有]可能匹配失败。3.2 查询生成器将槽位映射为可执行Cypher语句分类后query_generator.py根据意图类型填充Cypher模板。以INGREDIENT_QUERY为例模板为MATCH (d:Dish {name:$dish})-[:HAS_INGREDIENT]-(i:Ingredient) RETURN collect(i.name) AS ingredients其中$dish由正则捕获的菜名填充。关键在于参数化查询——直接拼接字符串易引发Cypher注入如菜名含或{必须用驱动程序的参数绑定机制from neo4j import GraphDatabase class QueryGenerator: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def generate_ing_query(self, dish_name): # 安全参数绑定非字符串拼接 with self.driver.session() as session: result session.run( MATCH (d:Dish {name:$dish_name})-[:HAS_INGREDIENT]-(i:Ingredient) RETURN collect(i.name) AS ingredients, dish_namedish_name # 参数名必须与Cypher中$:xxx一致 ) return result.single()[ingredients] if result.peek() else [] # 使用示例 gen QueryGenerator(bolt://localhost:7687, neo4j, password) print(gen.generate_ing_query(麻婆豆腐)) # [豆腐, 牛肉末, 豆瓣酱, 花椒]注意Neo4j Python驱动要求参数名严格匹配Cypher中的占位符$dish_name且值类型需匹配字符串不能传数字。若查询无结果result.single()返回None需用result.peek()判空避免AttributeError。3.3 答案生成用Jinja2模板注入查询结果输出自然语言直接返回[豆腐,牛肉末]不符合问答体验。templates/answer_templates.j2定义答案模板{%- if ingredients|length 0 -%} 没找到{{ dish }}的食材信息请确认菜名是否正确。 {%- else -%} {{ dish }}的主要食材包括{% for ing in ingredients %}{{ ing }}{% if not loop.last %}、{% endif %}{% endfor %}。 {%- endif -%}渲染代码from jinja2 import Environment, FileSystemLoader env Environment(loaderFileSystemLoader(templates)) template env.get_template(answer_templates.j2) # 渲染答案 answer template.render( dish麻婆豆腐, ingredients[豆腐, 牛肉末, 豆瓣酱, 花椒] ) print(answer) # 输出麻婆豆腐的主要食材包括豆腐、牛肉末、豆瓣酱、花椒。关键配置FileSystemLoader路径必须指向模板文件所在目录{% if not loop.last %}确保列表末尾不加顿号这是中文排版基本要求。4. 高分期末答辩必答三问参数调优、性能瓶颈与可扩展性验证答辩老师最可能追问的不是“怎么跑起来”而是“为什么这样设计”“边界情况怎么处理”“如果数据量翻10倍怎么办”。以下三个实操技巧直击高分要害。4.1 Neo4j内存参数调优解决10万节点查询延迟超2秒问题默认Neo4j配置dbms.memory.heap.initial_size512m在导入5万三元组后简单MATCH查询可能达1.8秒。根源是堆内存不足导致频繁GC。修改conf/neo4j.conf# 将初始和最大堆内存设为物理内存的1/4开发机建议2G dbms.memory.heap.initial_size2g dbms.memory.heap.max_size2g # 启用页面缓存加速图遍历关键 dbms.memory.pagecache.size1g # 关闭不必要日志降低IO压力 dbms.logs.gc.enabledfalse重启Neo4j后同样查询耗时降至0.3秒内。验证方法在Browser中执行EXPLAIN前缀查看执行计划确认NodeByLabelScan步骤已使用PageCache。提示pagecache.size必须小于max_size否则启动失败若修改后Neo4j无法启动检查logs/debug.log中是否有OutOfMemoryError再调低参数。4.2 构建食材同义词库解决“土豆”vs“马铃薯”查询不一致问题用户输入“马铃薯”但图中只有“土豆”节点导致查无结果。源码data/synonym_dict.json定义同义词映射{ 土豆: [马铃薯, 洋芋, 山药蛋], 西红柿: [番茄, 洋柿子], 豆角: [豇豆, 长豆角] }查询前先做标准化import json with open(data/synonym_dict.json, r, encodingutf-8) as f: synonym_map json.load(f) def normalize_ingredient(ing): for standard, variants in synonym_map.items(): if ing in variants or ing standard: return standard return ing # 未匹配则返回原词 # 查询时先标准化 user_input 马铃薯 standard_name normalize_ingredient(user_input) # 返回土豆 # 再用standard_name查询图数据库注意同义词库需持续维护。若发现新词如“西兰花”被用户输入为“绿花菜”必须同步更新JSON文件否则标准化失效。4.3 扩展营养成分推理用Cypher实现“含糖量高→糖尿病人不宜”规则链当前系统对RESTRICTION_QUERY仅返回静态标签如Dish节点有diabetic_safe: false属性。高分方案是动态推理当用户问“糖尿病人能吃啥”系统应遍历所有菜检查其食材是否含高糖成分如“白糖”“蜂蜜”再结合CONTAINS_NUTRIENT关系判断。Cypher实现// 查找所有含高糖食材的菜品 MATCH (d:Dish)-[:HAS_INGREDIENT]-(i:Ingredient)-[:CONTAINS_NUTRIENT]-(n:Nutrient {name:糖}) WHERE n.value 10 // 单位克/100g WITH collect(d.name) AS forbidden_dishes // 返回不在禁用列表中的菜品 MATCH (d:Dish) WHERE NOT d.name IN forbidden_dishes RETURN d.name AS safe_dish LIMIT 10此查询将RESTRICTION_QUERY从“查属性”升级为“实时推理”体现知识图谱核心价值。需确保Nutrient节点有value属性单位统一为克/100g且CONTAINS_NUTRIENT关系已建立。验证技巧在Browser中先执行MATCH (n:Nutrient) RETURN n.name, n.value LIMIT 5确认数据存在若n.value为空需回溯数据清洗步骤在triples_clean.csv中补充营养数值三元组如[白糖, CONTAINS_NUTRIENT, 糖:100]。本文还有配套的精品资源点击获取
返回列表