简介:一套基于Python的智能旅游推荐系统毕业设计项目,面向计算机相关专业学生及Python开发者,可同时用于毕业设计、课程设计与实战练手。项目采用前后端分离结构,前端以HTML、Vue、CSS为主,包含丰富的JS交互脚本与SVG图标资源;后端基于Python 3.7,配合MySQL数据库及SQL脚本,提供完整的数据库初始化数据;系统功能覆盖旅游线路推荐、景点信息展示、用户管理等模块,界面简洁美观,流程操作便捷,具备较高的实际应用价值。压缩包共797个文件,约20.19MB,涵盖164个JS脚本、53个Vue组件、60张JPG/PNG图片、45个Python源文件、2个SQL数据库脚本,以及安装与运行批处理文件,内含安装.bat、运行.bat等辅助脚本,可直接配置环境运行,目录结构清晰,便于快速定位源码、前端页面、数据库和配置文档。项目已经严格调试,可直接运行,已有132人学习下载;通过这套项目可掌握旅游推荐系统的完整业务逻辑、前后端数据交互方式及Python Web项目从配置到部署的全流程,同时可借助附带数据库脚本和Navicat可视化工具快速还原环境,适合作为毕业设计答辩或作品集素材。
1. 智能旅游推荐系统在毕设里到底做什么
做毕设选“基于Python的智能旅游推荐系统”的人,十个里有八个不是奔着旅游去的,而是这套题能把大学四年学的东西串成一条完整链路:爬数据或造数据、MySQL建表、pandas清洗、协同过滤算法、Flask写接口、前端展示。老师看到的是一个从数据库到推荐结果再到网页演示的闭环,你自己也能在答辩时讲清楚每个环节。
这个系统核心解决的其实是典型的“信息过载”问题:用户面对几百上千个景点不知道去哪,系统根据他之前的浏览、收藏和评分行为,帮他过滤掉不合适的选项,直接给出TopN推荐。适合Python基础已经过关、但缺少一个完整项目经验的同学,也适合想快速把协同过滤从公式变成能跑代码的人。它不追求算法多前沿,重要的是把“用户-景点-评分”这条数据链路做扎实。
2. 推荐算法选型:为什么协同过滤是这套题的默认主干
2.1 先看用户行为形态,再决定UserCF还是ItemCF
智能旅游推荐系统里最常见的做法是协同过滤,核心思想只有一句话:让相似的人或相似的东西互相背书。
UserCF(基于用户的协同过滤)找的是“和你口味相似的人”,把这些人去过而你没去过的地方推给你。ItemCF(基于物品的协同过滤)找的是“和你喜欢过的景点相似的景点”,比如你收藏过西湖,系统把西溪湿地、灵隐寺这一类同样主打自然人文的景点推给你。
选型有一个很朴素的判断标准:用户数量大、物品内容更新快、用户兴趣容易变化的场景用UserCF,比如新闻App;物品集合相对稳定、用户兴趣比较长久的场景用ItemCF,比如图书、旅游景点。毕设里用户往往只有几百个,评分几千条,景点表撑死几百行,这种情况下ItemCF有两个肉眼可见的好处:
- 景点相似度矩阵可以离线算好,接口只需要做查表,响应快,演示不卡顿。
- 推荐结果可解释性强,能说出“因为你去过XX,所以推荐XX”,答辩时特别好讲。
所以我一般会默认把ItemCF当主干,UserCF留作对比实验。对比实验在毕业论文里是个加分项,哪怕最终效果差不多,至少说明你两种方法都推演过。
2.2 基于内容的推荐:专门补冷启动这个硬伤
协同过滤有一个天生短板:新注册的用户没有任何行为记录,系统不知道该给他推什么;新录入的景点没有任何人评分,它就永远沉在数据库底部。这个现象业内叫冷启动。
旅游推荐系统尤其怕冷启动,因为用户第一次打开页面时通常只有一个“当前城市”的信息,连登录都没有,这时候你不可能指望协同过滤。
常见的做法是加一层基于内容的推荐,核心思路是:用景点本身的属性算相似度,而不是用用户行为。属性可以取城市、主题类型、门票区间、游玩时长这几个字段。城市必须单独对待,因为旅游和买书不一样,推荐一个从北京跑到三亚的景点很可能没有实际意义——用户根本没有那个出行计划。
一个最朴素的特征相似度计算可以写成这样。
# 基于内容的景点相似度:只取城市和主题做加权 def content_similarity(a, b): sim = 0.0 if a['city'] == b['city']: sim += 0.5 if a['theme'] == b['theme']: sim += 0.3 if abs(a['price'] - b['price']) / max(a['price'], b['price'], 1) < 0.2: sim += 0.2 return sim这里把城市权重提到0.5,是因为在旅游推荐里地域属性是硬约束,主题和价格只是软偏好。如果反过来,城市权重太低,相似度矩阵里就会混进去大量跨省但同主题的景点,推荐结果看起来“很智能”,实际上用户根本用不上。
基于内容的推荐不用动用户行为表,数据直接从景点表读属性就能算,非常适合做冷启动兜底。它的缺点是推荐结果容易同质化,翻来覆去都是同一类地方,所以通常是和协同过滤混合使用,而不是替代。
2.3 混合推荐:加权融合比纯算法更耐看
毕设答辩时,老师最常问的问题之一就是:“你这个推荐和淘宝首页的有啥区别?”你如果回答“我用了某个纯算法”,其实很难站住脚。电商推荐系统里没有哪个是只靠一种算法打天下的,都是混合策略。
针对旅游推荐系统,我常用的方案是加权融合:协同过滤得分占0.7,内容相似度得分占0.3,两者产生冲突时以协同过滤为主,内容相似度负责把冷门但属性接近的景点补进来。如果用户行为太少,连协同过滤都算不出可靠结果,就直接降级到“当前城市的热门榜”。
这个降级策略不是随便想的,它对应一个关键工程问题:算法的置信度。行为数据只有三五条时,算出来的相似度全是噪声,与其硬推不如让热门榜顶上。热门榜本身也是一个推荐策略,而且是最稳的底线。
混合推荐的排序逻辑可以理解为两轮:召回阶段先把城市不匹配的景点剔除,再用协同过滤得分和内容相似度加权算总分,最后按总分截断取TopN。
3. 数据库设计与旅游数据准备:数据造不好,算法全是空中楼阁
3.1 三张核心表怎么建,才不会被答辩老师挑毛病
旅游推荐系统的数据模型不复杂,核心就是三张表:用户表、景点表、评分表。很多毕设翻车就翻在评分表上,要么没有唯一键导致同一用户对同一景点重复评分,要么外键随意设置导致造数据时各种报错。先看一份可以直接落地的建表SQL。
CREATE DATABASE travel_rec CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE travel_rec; CREATE TABLE user ( uid INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, gender TINYINT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sight ( sid INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL, theme VARCHAR(50), price DECIMAL(10,2) DEFAULT 0, longitude DECIMAL(10,6), latitude DECIMAL(10,6), rating_cache DECIMAL(3,1) DEFAULT 0.0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE rating ( rid INT PRIMARY KEY AUTO_INCREMENT, uid INT NOT NULL, sid INT NOT NULL, score TINYINT NOT NULL COMMENT '1-5分', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_uid_sid(uid, sid), CONSTRAINT fk_rating_user FOREIGN KEY (uid) REFERENCES user(uid), CONSTRAINT fk_rating_sight FOREIGN KEY (sid) REFERENCES sight(sid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE INDEX idx_rating_sid ON rating(sid); CREATE INDEX idx_rating_uid ON rating(uid);几个容易被忽略的设计点:
UNIQUE KEY uk_uid_sid(uid, sid)非常关键。它从数据库层面保证同一个用户对一个景点只产生一条评分,后面不管数据导入多少次、脚本多跑几遍,都不会把矩阵撑出重复值。score TINYINT用1到5的整数而不是浮点数。评分是用户的离散情绪表达,10分制看着精细,实际上用户根本分不清7分和8分的区别,反而加大造数据难度。- 景点表里的
rating_cache是冗余字段,存景点平均分。推荐排序或展示热门榜时直接查这个字段,不用每次实时聚合AVG(score),几百条数据虽然无所谓,但这个习惯对后续扩展有价值。 - 外键建议只在 rating 表上设
fk_rating_user和fk_rating_sight,用户表和景点表之间不要乱加外键。因为真实业务里修改主键的情况很少,外键一旦参与导入,批量写数据的顺序就会被约束,容易报错。
如果你在vscode里配置Python环境,记得装pymysql和sqlalchemy这两个库,连接MySQL时能省掉很多手工操作。
3.2 造数据是门玄学:怎么生成“像真人”的评分记录
多数毕设项目拿不到真实的旅游平台数据,第一反应是写几个for循环随机生成评分。这里有个坑:如果按均匀分布随机打1到5分,造出来的评分矩阵“太平均了”,协同过滤跑出来的推荐结果几乎等于瞎蒙,因为用户之间的差异太小。
真实场景里有几条统计规律可以模拟:热门景点获得的评分多且分数偏高;冷门景点评分少且分数方差大;大部分用户只评过几个景点,只有少数重度用户会评几十个。这种“幂律分布”特征才是推荐算法能抓到规律的前提。
import random import pandas as pd random.seed(42) # 假设sight表里有500个景点:前50个设为热门 all_sights = list(range(1, 501)) hot_sights = set(random.sample(all_sights, 50)) rows = [] for uid in range(1, 201): # 用power分布模拟用户活跃度:多数用户评3-20条,少数评很多 n_ratings = int(random.power(4) * 20) + 3 rated_sights = random.sample(all_sights, min(n_ratings, len(all_sights))) for sid in rated_sights: # 热门景点基准分给4.0,冷门给2.8,再加高斯噪声 base = 4.0 if sid in hot_sights else 2.8 score = int(min(5, max(1, round(base + random.gauss(0, 1.2))))) rows.append((uid, sid, score, uid + 1000000)) ratings = pd.DataFrame(rows, columns=['uid', 'sid', 'score', 'timestamp']) ratings.to_csv('ratings.csv', index=False, encoding='utf-8-sig')重点看random.power(4)这个参数。幂律分布的指数调得越高,数据长尾越明显:指数为4时,大部分用户落在3到10条评分的区间,少数用户能评到20条以上。这个比例和真实推荐场景比较接近。如果直接把random.randint(1, 30)拿来用,造出来的数据会均匀得像个假数据集,算法跑出来不如直接按热度排序,答辩时经不起追问。
热门景点基准分给4.0而不是5.0,也有讲究。真实的用户永远不会把好评全部给到满分,总有人因为天气、排队、预期落差给热门景点打低分。加上random.gauss(0, 1.2)的噪声之后,热门景点的平均分落在3.8到4.2之间,冷门景点平均分在2.5到3.2之间,这样的区分度才能让算法学到“热门的就是更受欢迎”这个信号。
3.3 把CSV灌进MySQL:用Python比Navicat粘贴更省心
数据文件有了,接下来要导入MySQL。很多同学习惯打开Navicat手动导入,但CSV字段类型、编码、主键冲突的问题会在图形界面里被吞掉,报错信息也不直观。我更建议直接在Python里完成导入,顺带可以做一次简单的数据校验。
import pandas as pd from sqlalchemy import create_engine ratings = pd.read_csv('ratings.csv', encoding='utf-8-sig') ratings.columns = ['uid', 'sid', 'score', 'timestamp'] engine = create_engine( 'mysql+pymysql://root:123456@localhost:3306/travel_rec?charset=utf8mb4', pool_size=5, pool_recycle=3600 ) with engine.begin() as conn: ratings.to_sql('rating', conn, if_exists='append', index=False, chunksize=1000)这里有几个参数值得说明:
encoding='utf-8-sig'是为了处理Windows下CSV文件常见的BOM头。用普通的utf-8读取时,第一列列名会变成带不可见前缀的字符串,导入后字段名对不上,报错很难排查。mysql+pymysql://是SQLAlchemy连接MySQL的标准驱动写法。URL里的charset=utf8mb4必须显式声明,否则即使数据库建表时用了utf8mb4,连接层的字符集可能还是latin1,中文导入后就变成乱码。if_exists='append'表示追加写入。如果反复跑这个脚本,会触发前面建表时设的唯一键约束,重复数据会被拒绝而不是变成脏数据。这也是我把唯一键放在最前面的原因。chunksize=1000让pandas分批写入,避免一次性拼一个超大的INSERT语句把数据库连接拖死。几千行数据可能感觉不到差异,但如果你把评分规模扩到几万条,就会明白这个参数的价值。
导入完成后顺手检查一下数据量。
SELECT COUNT(*) FROM rating; SELECT uid, COUNT(*) AS cnt FROM rating GROUP BY uid ORDER BY cnt DESC LIMIT 10;第二句SQL是检查“用户评分数量分布”是否还有长尾特征。如果所有用户都评了差不多的数量,说明造数脚本的幂律设计失败了,趁早回去改数据,别等算法跑完才发现推荐结果全是热门榜。
4. 核心代码落地:相似度计算、推荐生成与Web接口
4.1 把评分表变成算法能吃的矩阵
推荐算法不直接认SQL表,它需要的是一个二维矩阵:行是用户,列是景点,单元格是评分,没有评分的位置填0。pandas里用pivot_table一行就能完成。
import pandas as pd from scipy.sparse import csr_matrix from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://root:123456@localhost:3306/travel_rec?charset=utf8mb4') df = pd.read_sql('SELECT uid, sid, score FROM rating', engine) # 行=用户,列=景点 rating_matrix = df.pivot_table(index='uid', columns='sid', values='score').fillna(0) sparse_matrix = csr_matrix(rating_matrix.values)pivot_table执行完后,rating_matrix.index是全量用户ID,rating_matrix.columns是全量景点ID。后面所有推荐结果都要用这两个索引来还原成真实的UID和景点SID,不要自己另建一套ID映射,否则接口返回时容易对不上号。
这里我特意把fillna(0)和csr_matrix都写出来,是因为它们各有各的用途。fillna(0)方便你print出来直观检查数据,比如确认某一行几个非零值;csr_matrix才是真正送去算相似度的对象,它只存储非零元素,几百个用户几百个景点可能看不出差别,但当你加数据到几万条时,稠密矩阵的内存开销会是稀疏矩阵的几十倍。
4.2 用余弦相似度计算景点之间的关系
景点相似度有多种算法,皮尔逊相关系数、余弦相似度、杰卡德相似系数都有人用。在评分数据稀疏的场景下,余弦相似度是最稳的选择,因为它只关心两个景点在共同被评过的用户上方向是否一致,不关心评分绝对高低。实现上用矩阵乘法代替双重循环,速度完全够用。
import numpy as np def cosine_similarity(item_matrix): # item_matrix: shape = (n_sights, n_users) item_matrix = item_matrix.T norm = np.sqrt(np.sum(item_matrix ** 2, axis=1)).reshape(-1, 1) norm[norm == 0] = 1e-6 normalized = item_matrix / norm return normalized.dot(normalized.T)这段代码里有个细节值得注意:把原始矩阵转置成“每一行是一个景点”再计算。为什么要转置?因为协同过滤最终要的是“景点相似度矩阵”,它的第 i 行第 j 列表示景点 i 和景点 j 的相似度。如果你拿用户评分矩阵直接算,得到的是用户相似度,方向就反了。
norm[norm == 0] = 1e-6是防御性写法。当某个景点没有任何人评分时,它的行全是0,除零会产生NaN,而NaN会污染整个矩阵——一个NaN经过矩阵乘法扩散出去,所有和它有关的相似度全部变成NaN,后续排序直接崩。给它一个极小的归一化值,至少保证结果是一个有限数。
4.3 生成推荐列表:加权求和加上城市过滤
相似度矩阵算完之后,推荐生成就是一个查表和排序的过程:遍历用户已经评分的景点,找出每个景点最相似的邻居景点,用“相似度 × 用户对该景点的评分”作为邻居景点的得分,累加后排序取TopN。
from collections import defaultdict def recommend(uid, rating_matrix, sim_matrix, top_n=10): if uid not in rating_matrix.index: return [] # 交给上层兜底热门榜 user_ratings = rating_matrix.loc[uid] rated_sights = user_ratings[user_ratings > 0].index.tolist() if len(rated_sights) < 5: return [] # 行为太少,协同过滤结果不可信 scores = defaultdict(float) for s1 in rated_sights: s1_idx = rating_matrix.columns.get_loc(s1) # 取该景点最相似的30个邻居 neighbor_idx = np.argsort(sim_matrix[s1_idx])[::-1][:30] for s2_idx in neighbor_idx: s2 = rating_matrix.columns[s2_idx] if s2 in rated_sights: continue sim = sim_matrix[s1_idx][s2_idx] if sim > 0.1: scores[s2] += sim * user_ratings[s1] # 城市过滤在排序后执行:实际项目中在这里按city字段排除 ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [sid for sid, _ in ranked[:top_n]]几个参数是用教训换来的:
- 邻居数量取30,不是取全部。全部邻居参与计算会把大量相似度极低的景点噪声累加进分数里,TopN结果反而被拉偏。30这个值在几百个景点的数据集上表现比较稳定,你要是数据量大可以提到50。
sim > 0.1是过滤阈值。相似度低于0.1的邻居基本就是噪声,参与加权只会让推荐结果变平庸。- 行为少于5条直接返回空列表。这个策略配合后续接口层的热门榜兜底,才能保证每个用户打开页面都有内容可看。
4.4 用Flask把推荐结果包装成交互接口
算法跑通后还差最后一步:让用户在浏览器里触发推荐。最常见的方式是用Flask提供两个接口,一个渲染页面,一个返回JSON数据。
from flask import Flask, request, jsonify, render_template app = Flask(__name__) # 启动时把相似度矩阵算好,接口内不做重计算 SIM_MATRIX = cosine_similarity(sparse_matrix) @app.route('/') def index(): return render_template('index.html') @app.route('/api/recommend/<int:uid>') def recommend_api(uid): recs = recommend(uid, rating_matrix, SIM_MATRIX) if not recs: # 冷启动兜底:返回当前城市热门景点 recs = get_hot_sights_by_city(city='杭州', top_n=10) return jsonify({ 'uid': uid, 'sights': [{'sid': sid, 'name': sight_name_map[sid]} for sid in recs] }) app.run(debug=True, port=5000)注意SIM_MATRIX是模块导入时一次性算好的,而不是在每次请求里重新算。一个几百乘几百的相似度矩阵,双循环计算也就几十毫秒,但如果以后数据量变大,每次请求都重算会直接把接口拖垮。把重计算放到启动阶段,是这类小型推荐项目最实用的性能优化,没有之一。
返回JSON时还有一个隐藏坑:sim_matrix里的元素是numpy类型,jsonify序列化np.int64或np.float64会直接抛TypeError。所以我在组装返回结构时用int()包了一层,避免序列化踩坑。
4.5 前端展示:透明度比炫酷更重要
推荐系统的前端不需要很复杂,但要把“推荐理由”展示出来。最简单的做法是推荐列表里加一行小字:因为你去过西湖、灵隐寺,所以为你推荐西溪湿地。
这个设计不仅对用户有说服力,更重要的是答辩时有话可讲。老师问“你这个推荐为什么靠谱”,你指着界面的推荐理由说:这是ItemCF的产物,系统根据你历史评分景点的相似邻居加权计算得出。可解释性直接体现“我懂算法原理”,比一个只会吐景点列表的黑匣子强太多。
5. 避坑:旅游推荐系统从开发到答辩的常见问题与排查
5.1 相似度矩阵全是0,推荐列表永远为空
现象:控制台打印sim_matrix全部是0,调用推荐接口返回空列表,前端什么都没有。
原因:最常见的是评分矩阵过于稀疏。如果200个用户只产生了800条评分,每个用户平均只评了4个景点,景点两两之间几乎没有被同一个人评过,余弦相似度算出来没有共同非零项,自然全是0。
解决:要么把造数脚本的用户评分数量下限提高,保证每个用户至少评10个景点;要么在推荐函数里加一个最少评分阈值,我上面代码里len(rated_sights) < 5就是干这个的。数据稀疏不是靠算法能硬扛的,先把数据密度提上来再谈算法效果。
5.2 中文景点名导入MySQL后全部变成问号
现象:sight表里插入“西湖”,查询出来是???,前端页面显示乱码。
原因:三层字符集有一层没对齐。数据库建库时用了utf8mb4,但连接串没加charset=utf8mb4,pymysql默认用latin1发送数据;或者CSV文件读取用了encoding='utf-8'但文件本身带BOM头。
解决:连接串显式加上?charset=utf8mb4;CSV读取统一用utf-8-sig;建库语句用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。这三层对齐后,中文基本不会再出问题。如果还乱码,检查MySQL服务端默认字符集,在my.cnf里设置character-set-server=utf8mb4。
5.3 推荐结果同质化严重,来来回回都是同一类景点
现象:用户收藏过一个自然风光景点,推荐列表里全是自然风光,同一个城市里扎堆出现,看着不智能。
原因:ItemCF天然存在“马太效应”,相似度高的景点总是更容易被推荐,多样性和用户真实需求被忽略了。旅游场景尤其特殊:用户出游一次可能会同时考虑人文、美食、购物、亲子,单一类型的推荐不符合实际。
解决:在排序后加一个多样性重排。常见做法是对结果按主题做配额,比如Top10里最多只允许4个同主题景点,优先级从高到低依次排。更简单的一种是轮盘策略:每取一个景点,检查与已选景点平均相似度,超过阈值就跳过,换下一个。
5.4 Flask接口报错 TypeError: Object of type int64 is not JSON serializable
现象:浏览器访问/api/recommend/1直接白屏,控制台报TypeError。
原因:numpy会把pandas里的整数变成np.int64,把小数变成np.float64。Flask的jsonify不认识这些类型,序列化时直接抛异常。
解决:组装返回数据时显式转换类型。
sights = [{ 'sid': int(sid), 'name': str(sight_name_map[sid]), 'score': float(scores.get(sid, 0.0)) } for sid in recs]5.5 导入评分数据时外键约束报错 IntegrityError
现象:运行ratings.to_sql()时报IntegrityError: (pymysql.err.IntegrityError) Cannot add or update a child row。
原因:造数脚本生成的uid或sid在 user 表和 sight 表里不存在。很多同学先跑造数脚本生成评分,再手动往用户景点表里灌数据,两边ID范围对不上,外键直接拒绝写入。
解决:严格按“先导user、再导sight、最后导rating”的顺序操作。造数脚本里用random.sample(range(1, 501), n)时,先确认sight表的主键最大值和最小值正好覆盖这个范围。我习惯在造数前先执行一条SELECT MAX(sid), MIN(sid) FROM sight;把范围查出来再写脚本。
6. 把系统从“能跑”做到“能答辩”:离线评估与冷启动技巧
推荐系统最容易被答辩老师抓到的问题就是:你自己怎么知道推荐结果好不好?如果答不上来,说明系统缺少评估环节。毕设阶段不需要上在线A/B测试,用离线评估就够了,方法叫留一法:把每个用户的最后一条评分藏起来,用剩下的数据给这个用户推荐,再看被藏起来的那条评分有没有出现在推荐列表里。
def evaluate(rating_matrix, sim_matrix, top_n=10): hit = 0 total = 0 for uid in rating_matrix.index: user_ratings = rating_matrix.loc[uid] rated = user_ratings[user_ratings > 0] if len(rated) < 2: continue # 藏起最后一条评分 test_sid = rated.index[-1] train = rating_matrix.copy() train.loc[uid, test_sid] = 0 recs = recommend(uid, train, sim_matrix, top_n=top_n) if test_sid in recs: hit += 1 total += 1 return hit / max(total, 1)配合Precision@10(推荐列表里有多少是用户真正感兴趣的)和Recall@10(用户感兴趣的景点有多少被推荐出来)两个指标,就能说明系统有效性。毕设项目里精度达到0.1到0.2之间已经算及格,不用追求夸张的数字,重点是你敢把评估过程讲清楚。
最后一个实用技巧:冷启动状态下,先用“当前城市热门榜”扛住首页。用户一进入系统,根据IP或手动选择的城市返回热门景点,等用户产生三五个评分行为后,再切换到协同过滤推荐。这个策略简单、有效、还容易演示,比硬上一个复杂模型实在得多。
我每一版改完相似度阈值或邻居数量,都会重跑一遍留一法评估,再手动打开页面用几个测试账号点一遍。推荐系统这东西有点玄学,光看离线指标不许,真实页面里有没有内容才是最后的底线。希望这些步骤能帮你把这个项目做成一个站得住脚的毕设,也希望你答辩顺利。
本文还有配套的精品资源,点击获取