
做旅游推荐系统我一开始走的弯路就是迷信协同过滤那一套成熟的模型。当时选了个公开的旅游评论数据集用UserCF和ItemCF跑了一版离线AUC看着还行上线一测就露馅了用户在一个城市搜了亲子游系统推回来的全是热门景点什么故宫外滩都来了跟用户诉求完全不沾边。后来我彻底明白了一个道理——旅游是低频、高客单、强语义的决策场景靠相似用户的共现行为根本推不动。真正想把推荐做明白得让系统理解为什么推荐这个而不是谁的浏览记录和谁重了。这也是我把目光转向知识图谱的原因。这篇文章不是科普知识图谱概念而是把我从本体设计、数据构建到模型融合、上线部署的完整过程拆开讲重点是我中的那些坑和最终采用的方案。适合正在做旅游推荐、做毕业设计选了这个方向、或者想在推荐里引入语义关系的朋友参考。1. 传统推荐算法为什么在旅游场景频频失灵1.1 低频消费导致协同过滤的数据基础不成立协同过滤的理论根基是用户-物品交互矩阵足够稠密。电商场景一个用户一年下几十单视频场景一个用户一天刷几十个内容矩阵的稀疏度还在可控范围内。旅游是什么情况一个普通用户一年出行一到三次一次行程可能就选三五天、覆盖十来个POI交互矩阵的稀疏度能到99.99%以上。矩阵这么稀相似用户计算出来的邻居全是弱连接推荐结果自然随机。更麻烦的是旅游的消费是非重复性的。电商里一个用户买过iPhone再推AirPods这个逻辑成立因为它基于同一物理实体的持续需求。但旅游里用户去过杭州千岛湖你不能指望他下个月再去一次千岛湖。协同过滤的UserCF假设相似的人喜欢相似的东西但旅游的场景里用户上一段旅行和下一段旅行之间可能隔了半年兴趣点早就变了。我们当时用ItemCF做出来的推荐结果有一大半是用户已经去过的地方用户点进去看一眼就退出来行为日志里全是点击-立即返回的短停留记录。1.2 旅游推荐的决策链路比电商长得多电商推荐本质上是一个决策终点用户看到一个商品评估价格、评价、款式然后决定买不买。旅游推荐是典型的决策链问题第一步定目的地城市第二步考虑出行时间季节、节假日、天气第三步规划区域内景点组合第四步匹配餐饮、住宿、交通中间还要考虑同行人构成亲子、情侣、父母、预算范围、当地特色属性。任何一个环节没对上推荐结果都是废的。传统协同过滤模型输入是用户ID 物品ID它只能学到这个用户和那个用户行为模式相似完全无法感知用户正在规划亲子游且预算有限这类上下文信号。就算你把时间、季节、同行人当成特征拼进FM或者DeepFM里模型也只能学习这些特征与点击行为之间的统计相关性无法推理出都江堰对亲子游客友好是因为景区内部有观光车路程平缓有水利工程科普教育点这种深层的、结构化的知识。1.3 旅游推荐需要的是语义理解不只是行为拟合我在项目复盘时把旅游推荐对模型的要求归纳成三条语义性推荐理由不只是猜到你要什么而是能说明这个景点为什么适合你。多跳性能从一个实体出发沿着逻辑链条找到关联实体。比如用户搜索过都江堰系统应该能推理都江堰是水利工程→用户可能对人文历史类景观感兴趣→推荐乐山大佛/三峡大坝。强约束性推荐结果必须满足时空约束能结合淡旺季、天气、用户偏好互斥规则比如带老人出行就不该推登山强度大的景点。协同过滤最多满足第二条的一半第一条基本没戏第三条完全做不到。知识图谱的价值正在于此它把实体和关系组织成图结构让推荐算法可以在语义网络上做推理、做路径挖掘、做约束过滤。这不是模型架构上的小修小补而是推荐系统理解用户意图的底层能力升级。2. 旅游知识图谱的构建思路从本体定义到落库2.1 本体设计先定骨架再填肉知识图谱构建最容易翻车的环节就是上来就闷头写爬虫、灌数据结果库里有几千万个节点但关系定义乱成一团后续模型和查询全部抓瞎。我个人的经验是本体设计阶段花的时间比例应该在30%以上这部分的账迟早要还。旅游领域的本体设计核心是回答清楚三个问题有哪些实体类型有哪些关系类型每个实体的关键属性是什么我的第一版本体设计有一个非常惨痛的教训我一开始把餐厅和酒店都归为POI这个类型想在属性层面区分结果图模型里一个POI节点同时挂了人均消费和房价两种属性后面写查询时逻辑极其混乱。后来我拆分成了7类实体结构清晰很多实体类型核心属性说明景点ScenicSpot名称、简介、门票、建议游玩时长、经纬度、热度、等级推荐的核心对象城市City名称、省份、气候带、特色标签承接路线推荐美食Food名称、菜系、人均、代表店铺、适合时段决策链中的餐饮环节酒店Hotel名称、星级、价格区间、距离地标、亲子设施决策链中的住宿环节出行月份TravelMonth月份、季节、温度区间、降水情况时间语义节点主题标签Tag名称、类型连接用户画像与POI用户UserID、偏好标签从交互中抽取推荐起点关系类型我最终精简成13类核心的几条如下位于景点→城市适合季节景点→出行月份相似于景点↔景点基于标签和embedding相似度包含于美食/酒店→城市有标签实体→主题标签偏好用户→主题标签邻近景点↔景点基于地理距离提供景点→体验类型比如研学夜游露营2.2 数据获取与实体对齐最脏最累但决定上限数据源我分了三个渠道。结构化数据主要来自高德、百度的POI开放接口字段质量比较整齐缺点是描述性文本少半结构化数据来自百科网站和旅游攻略站的信息框天然有景点等级建议游玩时间这类键值对适合写规则抽取非结构化数据来自用户游记和马蜂窝评论包含适合带孩子风景绝美但要爬很多台阶这类高质量语义信息但需要NLP处理。实体对齐是图谱构建里最脏的活儿。同一个景点在不同数据源叫法可能完全不同宽窄巷子在一些数据源里写宽窄巷子景区在游记里可能就叫宽窄宽巷子和窄巷子也会被单独提及。我第一版直接按名称精确匹配对齐率不到六成大量重复节点散落在图里。后来方案是名称归一化 地理坐标双验证名称先做归一化全角转半角、去除景区/风景名胜区/国家级等后缀词、统一繁体中文为简体。用归一化名称对齐候选集。对候选集中的实体加上经纬度距离校验阈值200米内和行政区划归属校验。这套流程跑下来对齐率提升到了85%以上。没法对齐的那部分主要是游记里的口语化别名比如兰桂坊在成都语境下指九眼桥酒吧街而不是香港的兰桂坊这类歧义我直接用规则过滤器处理掉兜底策略是在图谱里放别名属性而不是新建实体节点。2.3 存储选型与图模型落地存储方案我同时用了MySQL和图数据库。MySQL存原始的面包屑数据抓来的JSON、清洗前的文本、中间映射表图数据库存清理后、对齐完成的最终图谱。很多人一上来就把所有数据灌进图数据库后面改本体调整关系时哭都哭不出来——原始数据必须有一个与图谱解耦的备份层。图数据库选型方面我测试了Neo4j、NebulaGraph和HugeGraph。项目规模不大几十万节点、百万级关系单机的Neo4j就够用而且Cypher查询语法生态成熟、文档多招人也容易。如果你的节点规模到千万级再考虑NebulaGraph这类分布式方案。这里有个重要经验Neo4j在做大批量初始导入时别用CREATE逐条插入要用neo4j-admin import或者LOAD CSV批量写入。我一开始用Python调驱动逐条插入几十万节点插了快两个小时后来换成LOAD CSV压缩到十几分钟。图谱落库后的一个核心节点是构建用户节点。用户节点的来源是系统日志里的历史交互行为我把用户类型预设成12类亲子、研学、摄影、美食、徒步、自驾、人文、休闲等通过用户的历史行为匹配到对应标签再通过偏好关系挂到图谱上。这一步很关键后面做路径推理和模型训练时用户节点就是整个推荐流程的起点。3. 图谱参与推荐的三种主流姿势以及我推荐的第一版方案3.1 三种融合路线对比嵌入、路径、联合模型知识图谱参与推荐计算的路线基本可以归成三个流派Embedding-based基于图嵌入。首先使用TransE、TransR、RotatE这类平移距离模型或GCN/GAT这类图神经网络为图谱中的节点生成低维稠密向量。然后有两种用法一是直接把节点向量拼进排序模型作为特征二是预先离线算好用户向量和景点向量的相似度作为召回候选。这条路落地容易、性能好但损失了可解释性——嵌入后的向量是隐含表示你没法直接告诉用户为什么推这个。Path-based基于路径。定义一个meta-path模式比如用户→偏好标签→景点→同标签→新景点然后在图谱里做路径检索最终返回符合实例路径的推荐候选。这条路可解释性最强推荐理由是您关注过摄影主题Mystery景点也与摄影相关但路径模式需要人工设计长尾路径的覆盖率是个问题。Joint Model联合模型。代表工作是RippleNet、KGCN、KGAT。核心思路是把知识图谱作为用户-物品交互图的辅助结构用图神经网络或偏好传播机制在图上做端到端学习。这个路线当前效果最好但实现复杂、训练成本高且需要调的数据量级比较大。三者的取舍很直观效果上限联合模型最高工程速度嵌入最快解释性路径最强。但我强烈建议第一版先别冲KGAT从嵌入路线起步把整条链路跑通后续再迭代到联合模型也不迟。3.2 我最推荐的第一版方案TransE嵌入 规则召回 排序重排我最终采用的方案分三段第一段图谱嵌入生成。用RotatE模型对图谱做嵌入训练。选RotatE而不是TransE的原因在于TransE在处理对称关系和一对多关系时有天然缺陷——你想想相似于和邻近这类关系都是对称的TransE会让这两个节点的向量逼近同一个点区分度很差。RotatE用复数空间旋转解决对称/非对称问题更适合旅游图谱里大量存在的对称关系。训练参数我最终固定为embedding维度128margin为3RotatE的评分函数是个复数旋转差margin需要给大一些学习率0.002batch大小256负采样数量128每正样本配对128个负样本。训练完成后每个景点节点得到一个128维向量。第二段语义召回。召回阶段拼接了两路结果。第一路将用户节点向量与全量景点向量做内积相似度计算取Top50。第二路做简单的规则召回——基于硬性条件过滤排除用户已去过的城市、排除不适合当季的景点、排除超出预算的酒店再按热度排序补充Top20候选。两路取并集后进入排序阶段。伪代码大致是def recall(user_id, current_month, budget, excluded_cities): candidates set() # 基于图嵌入的语义召回 user_embedding get_embedding(user_id) all_poi_embeddings get_all_poi_embeddings() semantic_top topk(user_embedding, all_poi_embeddings, k50) candidates.update(semantic_top) # 规则召回不消耗图语义纯业务规则 rule_top rule_based_recall(seasoncurrent_month, budgetbudget, exclude_citiesexcluded_cities, k20) candidates.update(rule_top) return list(candidates)这里有个细节容易踩坑用户节点embedding是用用户历史访问/点击过的景点节点取这些景点向量的平均值当作用户向量的近似。这在图嵌入领域叫transductive mean aggregation效果不完美但简单可靠。如果用户是纯粹的新用户没有任何历史行为我冷启动阶段的处理方式是直接跳过语义召回只走规则召回配热门兜底。第三段排序重排。排序模型选了LightGBM特征分四组统计类特征候选景点热度、用户点击率、图谱语义特征候选景点与用户向量的余弦相似度、两者在图谱上的最短路径长度、业务规则特征是否当季时宜、是否匹配预算、是否在预留城市内、上下文特征当前月份、用户角色类型。重排阶段加了个多样性控制用MMR最大边际相关度lambda设0.6对结果做去重避免一次性推5个同类型古镇。整条线上线后离线推荐命中率比纯ItemCF提升大约22%但更明显的好转其实在线下评测——用户理由反馈里说我推得明白的占比从31%提高到了67%。这说明图谱带来的语义理解能力用户是感知得到的。3.3 用路径推理生成推荐理由并反哺排序可解释推荐是知识图谱路线天然的加分项。排序完成后我对Top N候选中每一条在图谱中回溯一条语义路径作为推荐理由。核心逻辑是用Cypher查询meta-path实例MATCH p (u:User {id: $user_id})-[:偏好]-(t:Tag)-[:有标签]-(poi:ScenicSpot) WHERE poi.id $candidate_id RETURN t.name AS reason_tag, poi.name AS poi_name比如用户有摄影标签候选景点是梯田景区匹配到路径用户→偏好→摄影→有标签→梯田景区生成的推荐理由是您偏好摄影主题梯田景区以日出云海著称。这个理由在UI上直接展示用户理解成本极低。因为图嵌入路线本身没有可解释性所以我用路径回溯的方式补上了这层解释能力在工程上是完全可行的轻量方案。这条路径不仅能用于展示我后来还尝试把它当作排序模型的实时特征路径是否存在、路径条数、最近一条路径的权重和加进去后AUC又涨了约1.8%。4. 落地过程中我踩过的坑和改进方案4.1 新景点没有交互数据怎么办规则兜底不解决问题传统协同过滤遇到冷启动会抓瞎知识图谱能不能解决严谨地回答能解决一部分但需要设计。新景点的图谱节点直接连了城市、标签、适合季节、邻近景点这些关系所以图范式天然有冷启动的骨架——问题在于用户的交互信号还没建立embedding训练里新节点的向量没有足够的邻居信息来来更新。我的处理方式是三层冷启动策略。第一层新景点embedding可用其邻域节点的平均向量初始化比如它连接了城市成都标签亲子邻近大熊猫基地就用这三个节点向量的加权平均作为初始向量。第二层语义召回阶段提高规则召回的比例Top20提到Top40保证新景点有足够曝光机会撑过冷启动期。第三层在用户提交的反馈数据累积过10次交互后点击不计、出发前收藏算1次行程完成后评价算3次再开放图嵌入路线的全量召回避免模型用弱信号污染用户向量。4.2 图谱更新的频率离线全量与在线增量要分开旅游数据具备明显的时效性景点开放状态在变、开放时间在变、热度在变、用户行为在变。但是在系统迭代过程中也要注意避免的一个坑是不用对全图做频繁的全量训练代价太高也没有必要。图嵌入训练我每周离线跑一次全量更新耗时可控制在2小时内但用户节点的更新走增量路径——用户产生新行为后实时更新该用户与标签之间的偏好关系权重同时用最新的加权平均值刷新用户向量。这样既保证了图谱主体景点、城市、实体关系的稳定性又能让用户的个性化信号实时反映到推荐结果里。增量更新这块有个隐藏问题就是用户向量已经参与排序阶段近实时的打分但图嵌入的全量更新每周才一次两者之间会产生偏移。我采用的补偿方式是排序阶段不直接用用户embedding做唯一语义特征而是同时保留一个短期兴趣向量基于最近7天行为计算两个向量拼接送入排序模型。效果比只用一个向量要稳在线指标波动小很多。4.3 推荐质量评估离线指标和用户真实体验的错位最后聊一个最容易被忽视的问题知识图谱推荐系统的评估。纯离线榜单AUC和Recall都有虚高的可能因为图嵌入本身会把结构相似的节点拉得很近离线评测时如果测试集和训练集存在同分布节点分自然好看。真正能判断推荐系统价值的是用户表态率——也就是用户做出明确的积极行为收藏、加入行程、出发与曝光量的比值。我最终把评估指标定为三个层级第一层是基础统计指标AUC、NDCG10、召回率用于版本回归对比第二层是用户明确反馈指标收藏率、入行程率、出行转化率这是衡量推荐是否有用的核心第三层是开放式反馈跟用户聊、看投诉、看搜索联想词变化因为冷启动期的用户需求往往不体现在行为数据里而体现在主动搜索里——用户反复搜索大熊猫基地_门票_价格却没有任何推荐点击说明你的推荐结果里没有信息含量足够的东西。这个三层评估体系上线后团队讨论推荐质量的方向立刻从聊模型结构变成了聊用户意图——这个转向就是知识图谱给项目带来的最大的真实价值。最后说点实在的经验。如果你也想做类似的项目我建议从城市级别的子图谱起步不要一上来就做全国甚至全球。我当时只做了成都、重庆、西安三个城市的数据本体反复调了三轮才定下来图谱质量远比数量重要。另一个经验是别迷信模型图谱本身的结构设计和使用方式比选哪个模型影响大得多——模型只是从图里捞东西的手图的骨架搭得歪了怎么捞都是废的。先跑通嵌入规则的链路把推荐理由生成出来再谈上不上KGCN、KGAT这是我认为性价比最高的路径。