news 2026/10/3 4:56:33

知识图谱驱动的旅游景点推荐系统:从Neo4j图谱构建到混合打分实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
知识图谱驱动的旅游景点推荐系统:从Neo4j图谱构建到混合打分实践

简介:基于知识图谱的旅游景点推荐系统Python源码,面向需要完成毕业设计、期末大作业或课程设计的高校学生,也适合希望入门知识图谱与推荐系统结合的开发者。压缩包内有18个文件,主要包含13个py源码脚本(覆盖模型构建、预测、实体关系处理与路径抽取等功能)、3个txt文本、1个pth模型权重文件及1个markdown说明文档,包体仅2.64MB。已有198人学习浏览。源码均经过本地编译可运行,评审分达98分,项目难度适中,且经助教审定,可直接参考或二次开发。整体采用RKGE类知识图谱推荐模型,通过训练好的模型权重、实体处理与路径计算脚本,清晰呈现从图谱构建到推荐结果生成的完整流程,能帮助读者理解知识图谱与推荐系统的结合方式,适合作为课程项目模板或毕业设计基础。

1. 知识图谱驱动的景点推荐:这份 Python 源码包最值钱的不是算法,是图谱

很多人在拿到那份“基于知识图谱的旅游景点推荐系统Python源码”时,第一反应是去找推荐算法那一块,觉得最值钱的是模型代码。实际上这种高分项目最容易被低估的部分恰恰是那张知识图谱本身:实体怎么定义、关系怎么建立、清洗之后有没有冗余节点、入库时用的是 create 还是 merge。推荐算法停在 DataFrame 上也能跑,但一旦换到图数据库,整个推荐逻辑就会变成路径推理,查询方式和边界条件完全不是一回事。

这类源码包解决的核心问题是帮用户滤掉无关景点:用户喜欢西湖,系统应当推导出他可能也喜欢同城市的灵隐寺,或者同类型的江南古镇,而不是靠评分矩阵硬凑。适合的人群既包括正在做课程设计、毕业设计的学生,也包括想从协同过滤切到知识图谱方向的工程师。读完你需要掌握一条完整链路——从本体建模到 Neo4j 入库,再到路径查询、混合打分,以及最后的留一法验证。

2. 图谱建模与数据清洗:先把本体骨架立起来,再谈推荐系统

知识图谱不是把数据塞进 Neo4j 就完事,能拿到高分的项目一定把本体建模想清楚了。本体就是这张图的 schema,它决定了以后查询能不能写得短、推理会不会绕远路。旅游景点这个领域特别适合做图谱,因为实体种类不多、关系相对固定,设计好之后就能形成一张信息量很大的网络。

2.1 本体设计:实体、关系、属性的三层约定,以及为什么先画草稿再写代码

我一般会在写代码前先用纸画三张清单:实体清单、关系清单、属性清单。实体清单是节点的类型,在 Neo4j 里对应 label。旅游景点系统常用的实体有五类:用户、景点、城市、美食,按需再加天气类型,用于做“天气不好时优先推荐室内景点”这类规则。

实体Neo4j Label典型属性说明
用户Useruser_id, age, occupation接收推荐的主体
景点ScenicSpotspot_id, name, rating, ticket_price推荐的目标对象
城市Citycity_id, name景点的地理位置
美食Foodfood_id, name, category用户饮食偏好的关联项
天气WeatherTypeweather_id, type影响景点选择的客观因素

实体之间的关系我通常控制在五种以内,这样查询路径才短,语义也清楚。

头实体关系尾实体含义
UserLIKESScenicSpot用户喜欢某景点
UserVISITSScenicSpot用户去过某景点
ScenicSpotLOCATED_INCity景点位于某城市
CitySERVES_FOODFood城市提供某类美食
ScenicSpotSIMILAR_TOScenicSpot景点之间的语义相似

关系一多,维护成本立刻上升,写 Cypher 时也容易把方向写反。比如 LOCATED_IN 的方向是景点指向城市,如果写成城市指向景点,第 4 章的路径推理就会查不到任何结果。

属性方面要区分两类。第一类是标识属性,比如 spot_id、city_id,全图唯一稳定,删除和更新都靠它。第二类是展示属性,比如 name、rating、ticket_price,只负责展示和排序。很多人容易把 name 当主键用,但爬虫数据里“西湖”和“杭州西湖”常常同时存在,用名字做唯一约束会直接报冲突;稳妥做法是每行数据导入前先生成人造 ID,规则可以是“城市名拼音 + 景点编号”,保证与展示字段彻底分离。

2.2 用 pandas 清洗 CSV:实体去重与文本归一化的四个参数

拿到的原始数据通常来自爬虫抓取或公开整理,比如用 python 爬虫拿景点名称、评分、门票价。常见的问题有三个:字段缺失、同一景点出现多种写法、城市名带不带“市”后缀。清洗这一步直接决定入库节点的数量,做不好后面全是重复节点。

import pandas as pd df = pd.read_csv('attractions_raw.csv', encoding='utf-8') # subset 控制参与去重的列,keep='first' 保留最早出现的记录 df = df.drop_duplicates(subset=['spot_name', 'city_name'], keep='first') # 用 errors='coerce' 把评分成非法值的行转成 NaN,再统一补 0.0 df['rating'] = pd.to_numeric(df['rating'], errors='coerce').fillna(0.0) # 去掉名称两端空格,并把中文全角空格统一成半角 df['spot_name'] = df['spot_name'].astype(str).str.strip().str.replace(' ', ' ') # 城市去掉“市”后缀,保证“北京”和“北京市”合并 df['city_name'] = df['city_name'].astype(str).str.replace('市', '') df.to_csv('attractions_clean.csv', index=False, encoding='utf-8-sig')

这段脚本有四个关键点。drop_duplicates 的 subset 选的是业务唯一键,在没有人造 ID 时用“景点名+城市名”组合;keep='first' 保证重复行不重复入库。rating 字段若是文本里混进“暂无评分”,用 to_numeric 转 NaN 再 fillna,后续排序和打分才不报错。replace(' ', ' ') 处理全角空格,这是中文数据里最隐蔽的问题。输出用 utf-8-sig 而不是 utf-8,是为了让 Neo4j 在 Windows 下读 CSV 时不出现 BOM 乱码。

清洗之后的数据需要补一列 spot_id 和 ciy_id 作为唯一主键,这里直接用行号加前缀就可以。如果这一步省略了 city_id,后面所有城市节点都会退化成用 name 做匹配,一旦城市名写法不统一,图谱里就会出现“北京市”和“北京”两个城市节点,同城推理直接失效。

3. 用 Py2neo 批量写入 Neo4j:从连接检查、节点 merge 到唯一约束

数据准备好之后,下一步是把它写进 Neo4j。整个项目运行在 Python + Neo4j Community 上,中间用 py2neo 做桥接。本地环境先把 Python 装好,再用 pip install py2neo 安装库,版本选 2021.2.3 系列的即可,这个版本和 Neo4j 4.x 的兼容性比较稳。

3.1 安装 py2neo 并检查连通性:bolt 协议、auth 参数与端口选择

安装完成后别急着写批量入库,先用一段最小脚本确认 Python 能连上 Neo4j。

from py2neo import Graph # bolt 是 Neo4j 的二进制协议,auth 传用户名和密码 g = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) # 跑一条最简单的查询,能执行就说明连接没有问题 print(g.run("RETURN 1 AS ok").data())

这里有两个容易卡住的地方。第一个是协议,Neo4j 默认提供 bolt 和 http 两个连接入口,bolt 走 7687 端口,http 走 7474 端口;py2neo 的 Graph 可以自动识别前缀,但建议显式写成 bolt。第二个是 auth,Neo4j 4.x 安装后首次登录会强制让你改默认密码,这一步在 Neo4j 浏览器里完成,改完之后才能在 Python 里用新密码连接。

3.2 用 merge 而不是 create:唯一约束、主键与批量入库

能连通之后就可以写节点了。最 naive 的写法是用 create 逐条插入,但脚本只要重跑两次,图里的重复节点马上失控。正确做法是先建唯一约束,再统一用 merge。

from py2neo import Graph, Node, Relationship, NodeMatcher import pandas as pd g = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) # 先建唯一约束,spot_id 和 city_id 是后续所有 merge 的判重依据 g.run("CREATE CONSTRAINT ON (s:ScenicSpot) ASSERT s.spot_id IS UNIQUE") g.run("CREATE CONSTRAINT ON (c:City) ASSERT c.city_id IS UNIQUE") df = pd.read_csv('attractions_clean.csv', encoding='utf-8-sig') matcher = NodeMatcher(g) for row in df.itertuples(index=False): # merge 而不是 create:主标签 ScenicSpot,主键 spot_id spot = Node("ScenicSpot", spot_id=row.spot_id, name=row.spot_name, rating=row.rating) g.merge(spot, "ScenicSpot", "spot_id") # 城市节点单独处理:先查再建,避免重复 city = matcher.match("City", city_id=row.city_id).first() if city is None: city = Node("City", city_id=row.city_id, name=row.city_name) g.create(city) # 景点与城市之间的关系也走 merge,防止重复关系 g.merge(Relationship(spot, "LOCATED_IN", city))

merge 的第二个参数是主标签,第三个参数是主键。它不像 create 那样无条件新造节点,而是先按主键找,找到了就更新属性,找不到才创建。所以第一次跑和第十次跑,结果都是同一套节点。

city 节点没有直接用 merge,是因为旧版 py2neo 对 merge 在节点和关系上的行为不完全一致,手动“先查再建”更直观,也不会产生多余空节点。整个入库脚本最耗时的不是 Python 循环,而是每次 merge 触发的图查找,所以唯一约束必须提前建立,否则缺少索引会让入库变成全表扫描,几万行数据会慢到无法接受。

4. 路径推理与混合打分:把知识图谱特征和协同过滤揉进同一个推荐公式

图建好以后,推荐的核心就是查询路径。基于知识图谱的推荐与矩阵分解最大的不同在于,它可以利用多跳关系来解释“为什么推这个景点”,而不是只看统计意义上的评分相似度。

4.1 在 Cypher 里写路径查询:从“用户喜欢”到“同城景点”的推理

一条典型的一跳推理是:用户喜欢的所有景点里,找与候选景点位于同一城市的其他景点,统计数量和平均评分,作为知识图谱特征。

MATCH (u:User {user_id: $uid})-[:LIKES]->(a:ScenicSpot)-[:LOCATED_IN]->(c:City) <-[:LOCATED_IN]-(s:ScenicSpot {spot_id: $sid}) RETURN count(DISTINCT a) AS same_city_count, avg(a.rating) AS liked_avg_rating

这个查询能回答两个问题:用户曾经喜欢过多少个和候选景点同城的景点,以及这些历史喜欢景点的平均评分。count 和 avg 不只是简单数字,它们会作为图谱侧的重要特征喂进后面的打分公式。

如果 same_city_count 很高,说明候选景点在用户常去的城市圈里,推荐优先级可以上调;如果 liked_avg_rating 偏低,说明这个城市在用户历史中的评价不好,权重就需要下调。再往后跳一层,把美食和天气也纳入推理,就能覆盖“用户到了陌生城市、按口味找景点”的场景,比如用户喜欢川菜,候选景点所在城市恰好有川菜馆,这类景点在查询里会获得额外加分。

4.2 混合打分函数:图谱特征、协同过滤与热度的权重分配

知识图谱特征很难单独撑起整个推荐,和协同过滤组合是更稳的做法。常见做法是先把路径特征、协同过滤评分和热度分别归一化到 0 到 1,再按权重合并。

def get_graph_features(user_id, candidate_spot_id): query = """ MATCH (u:User {user_id: $uid})-[:LIKES]->(a:ScenicSpot)-[:LOCATED_IN]->(c:City) <-[:LOCATED_IN]-(s:ScenicSpot {spot_id: $sid}) RETURN count(DISTINCT a) AS same_city_count, avg(a.rating) AS liked_avg_rating """ rec = graph.run(query, uid=user_id, sid=candidate_spot_id).data() if not rec: return {"same_city_count": 0, "liked_avg_rating": 0} return rec[0] def hybrid_score(graph_feats, cf_score, popularity): # 同城数量归一化,除以候选数量上限 same_city_norm = min(graph_feats["same_city_count"] / 5.0, 1.0) # 历史平均评分已经是 0-5,除以 5 归一到 0-1 rating_norm = graph_feats["liked_avg_rating"] / 5.0 graph_score = 0.6 * same_city_norm + 0.4 * rating_norm return round(0.4 * graph_score + 0.4 * cf_score + 0.2 * popularity, 4) candidates.sort(key=lambda x: hybrid_score(x["graph_feats"], x["cf_score"], x["popularity"]), reverse=True) top_n = candidates[:10]

权重的取值不要照抄,最好在验证集上调。我的习惯是先把协同过滤权重固定到 0.4,然后调图谱侧权重,观察命中率变化;如果用户行为数据非常稀疏,协同过滤那一项几乎全是默认值,就应该把它的权重调低到 0.2,让图谱路径主导。

热度项 0.2 通常保持不变,它在冷启动阶段兜底,防止推荐结果全是小众冷门景点。popularity 可以简单用 min-max 归一化处理,原始热度建议用“月访问量”或“评分×评论数”计算,注意不要让评分一个字段既进热度又进图谱特征,否则两项会重复加权。

5. 避坑清单:TLS 握手失败、重复节点、中文乱码与稀疏数据的四个翻车现场

这类源码包被下载后在本地跑不起来,十个里有八个卡在环境和编码上,剩下的则死在数据稀疏和重复节点。下面几条都是我做类似项目时实际遇到过的。

5.1 TLS 握手失败:py2neo 默认加密与 Neo4j Community 的矛盾

现象:运行连接脚本时抛错,提示 Server connected but failed to perform TLS handshake。

原因:py2neo 在 2021.2.3 之后的版本默认对 bolt 连接启用加密,而 Neo4j Community 默认没有开 TLS,于是双方在握手阶段直接断开。

解决:在 Graph 构造时显式传 secure=False,或者改走 http://localhost:7474 这个明文端口。代码里使用 bolt://localhost:7687 时,务必带好这个参数,否则排查方向会跑到证书和密钥上去。

5.2 重复节点暴增:create 与 merge 在脚本重跑时的差别

现象:同一个景点在 Neo4j 里查出来有十几个节点,推荐列表里同一个名字反复出现。

原因:入库脚本里用的是 create。create 每次执行都是无条件新建节点,所以每调试一次就多一批节点。

解决:删除整图后重新入库,并把 create 换成 merge,再配上唯一约束。如果数据已经坏了,先用 Cypher 的 MATCH (n) DETACH DELETE n 清空全图再重跑,不要在重复节点上做数据修补。

5.3 中文乱码:CSV 的 UTF-8 与 Neo4j 导入设置不一致

现象:用 load csv 导入时中文变成问号或乱码,Neo4j 浏览器里显示的全是转义字符。

原因:CSV 文件是 UTF-8 无 BOM,而 Neo4j 在 Windows 环境下解析时把它当成了其他编码,导致中文字节被拆坏。

解决:写 CSV 时统一用 utf-8-sig,并在 pandas 读取时显式指定 encoding='utf-8-sig'。如果数据里还有从网页抓下来的换行符或隐藏字符,先 strip 再编码转换,否则这些字符会一并入库,后续查询对不上。

5.4 用户行为稀疏:协同过滤失效时图谱路径是唯一的救星

现象:混合推荐结果接近随机,推荐出来的景点和用户历史行为没有明显关系。

原因:用户行为数据太少,评分矩阵稀疏到协同过滤部分几乎没有信号。

解决:把协同过滤权重调低,同时把图谱侧的“同城喜欢数量”特征权重拉高。冷启动用户在第一轮推荐时,直接回到第 4 章的 Cypher 路径查询,用同城关系和美食关联来解释推荐理由,而不是强行用稀疏矩阵打分。

6. 用留一法验证推荐命中率,并把图谱路径变成推荐理由

推荐做完之后,我习惯先跑一个留一法评估:把每个用户的最后一次行为藏起来,用前面的行为生成 top-n 推荐列表,看能不能命中被藏掉的那个景点。

def evaluate_hit_rate(rec_func, user_history, n=10): hits = 0 total = 0 for user_id, actions in user_history.items(): if len(actions) < 2: continue # 行为太少跳过,避免评估失真 test_spot = actions[-1] # 被藏起来的目标 train_actions = actions[:-1] # 只喂给推荐函数的历史 recs = rec_func(user_id, train_actions, top_n=n) if any(r["spot_id"] == test_spot["spot_id"] for r in recs): hits += 1 total += 1 return round(hits / max(total, 1), 4) hit_rate = evaluate_hit_rate(hybrid_recommend, user_history, n=10) print("hit_rate@10:", hit_rate)

hit_rate@10 在 0.1 到 0.3 之间属于这个领域的正常范围。如果连续两个版本都低于 0.1,问题大概率不在权重,而是图谱路径里的关系方向写反了,或者清洗阶段丢了主键。

验证通过后,把推荐理由落到前端也很有价值。做法是把这个用户的推荐路径返回成三层 JSON,例如“用户→喜欢→西湖→位于→杭州→包含→灵隐寺”,前端就能直接展示“因为你喜欢西湖,所以推荐同在杭州的灵隐寺”。这一步对高分开题答辩特别有用,能直观展示推荐系统的可解释性。

把这一套走完,我最大的体会是整个系统的上限一半以上由图谱质量和数据清洗决定,后面的调参其实是在抢救一个不稳定的底座;把时间花在实体对齐和关系去重上,比纠结权重多 0.05 要划算得多。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 4:56:28

2026工业AI控制系统:云边端协同落地实战指南

1. 项目概述&#xff1a;这不是在造“AI聊天机器人”&#xff0c;而是在重构工业控制的神经中枢“2026 AI工业控制系统&#xff0c;如何搭建&#xff1f;”——看到这个标题&#xff0c;很多人第一反应是点开看个热闹&#xff0c;以为又要讲一遍大模型怎么调参、Agent怎么编排、…

作者头像 李华
网站建设 2026/10/3 4:54:46

拆解Zoom AI架构:联合式方法与多模态集成如何塑造下一代会议智能

开完一场线上会议&#xff0c;系统在会议结束的同时&#xff0c;已经把一份带时间戳的纪要和行动项清单推到了日历里。你甚至不需要手动整理&#xff0c;因为AI已经听完了全场、看过了共享屏幕上的PPT、读取了聊天框里的补充链接&#xff0c;最后把多路信息汇聚成了几段结构化要…

作者头像 李华
网站建设 2026/10/3 4:54:40

从“事后聪明”到智能进化:hindsight如何驱动个人成长与AI系统优化

hindsight&#xff1a;不要把“事后聪明”只当成讽刺&#xff0c;它其实是成长和技术进化的原动力很多人第一次看到“hindsight”这个词&#xff0c;脑子里蹦出来的翻译是“后见之明”“事后诸葛亮”。在中文语境里&#xff0c;它多少带点贬义&#xff0c;好像在说“你早干嘛去…

作者头像 李华
网站建设 2026/10/3 4:53:37

OpenDRIVE中poly3曲线对不齐的数学原理与实战排查指南

做自动驾驶仿真的人&#xff0c;十有八九都被OpenDRIVE里的poly3曲线折磨过。明明按照标准文档写了多项式&#xff0c;车道线在单条reference line上看着也正常&#xff0c;结果多车道一拼、跨路段一接&#xff0c;曲线的末端总差那么几厘米甚至几米&#xff0c;车跑上去方向突…

作者头像 李华
网站建设 2026/10/3 4:53:31

DGM预测程序:离散灰色模型的小样本预测Python实现

简介&#xff1a;这是一份基于离散灰色模型&#xff08;DGM&#xff09;的MATLAB预测程序包&#xff0c;面向需要处理小样本、信息不完全时间序列的经济预测、工程数据分析和环境科学等场景。包内为一个DGM.m脚本&#xff0c;采用最小二乘法对一阶离散灰色模型DGM(1,1)的参数进…

作者头像 李华
网站建设 2026/10/3 4:52:12

CODESYS安装配置完全指南:从下载到跑通第一个软PLC程序

1. 为什么要写这篇教程&#xff1a;CODESYS到底解决了什么问题第一次接触CODESYS的人&#xff0c;多半是听同行提起“这个软件能做软PLC”“写逻辑跟玩一样”。我当时入坑&#xff0c;是因为接手一个项目&#xff0c;客户指定要用支持IEC 61131-3标准的控制器&#xff0c;而且最…

作者头像 李华