news 2026/10/1 7:38:59

基于Python的旅游景点推荐系统:协同过滤算法与Web应用实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python的旅游景点推荐系统:协同过滤算法与Web应用实战

简介:基于Python的旅游景点推荐系统毕业设计项目,面向计算机、信息管理等专业需要完成毕业设计、期末大作业或课程设计的高校学生,也适合希望掌握推荐系统开发全流程的初学者。整个项目围绕旅游景点数据采集、特征处理、推荐算法实现与前端交互展示展开,代码注释详实,关键模块如用户登录、景点管理、个性化推荐等均有清晰逻辑,下载后按说明部署即可运行,无需复杂环境配置。资源包共712个文件,约41兆,构成上以Python后端脚本、Vue前端页面、JavaScript脚本、CSS样式、SVG图标及GIF动图为主,辅以SQL数据库脚本、TXT说明文档、批处理启动脚本和论文文档,覆盖了从环境初始化、数据导入到系统启动运行的完整环节,另保留部分备份文件便于排查调整。目前已有526人学习下载,作者标注为98分项目,导师认可度较高,完整代码与数据库配合论文材料,适合用于快速理解推荐系统项目结构并作为课设或毕设参考。

1. 基于 Python 的旅游景点推荐系统:不是所有毕设都叫推荐系统

每到毕业季,总能看到大量“旅游景点推荐系统”的选题,但大多数做出来的东西只是一个登录注册加景点列表,点开详情页,所谓的“推荐”就是按点击量排个序。这种方案拿去做系统演示没问题,答辩一追问“你的推荐算法在哪里”,就露馅了。真正的基于 Python 的旅游景点推荐系统,核心在于用用户的历史行为数据去计算“这个人接下来还想去哪”,它不是景点查询系统,也不是后台管理系统,而是一个有明确输入、计算逻辑和反馈闭环的推荐引擎。

能做成的方案,通常是把协同过滤的主体逻辑用 Python 手写出来——不走第三方推荐库——再配上 MySQL 存取用户行为,最后用 Flask 或 Django 把结果暴露成 Web 接口,跑通一条“用户打分 → 相似度计算 → TopN 列表生成 → 结果写库”的完整链路。这套做法覆盖了毕设要求的算法体现、数据库设计和论文支撑,适合计算机科学、软件工程、大数据相关专业的学生。

2. 先搞懂推荐算法选型:为什么主流毕设都选协同过滤而不是深度学习

2.1 基于内容的推荐与协同过滤的取舍

旅游景点推荐这个场景有一个天然特点:景点是低频消费品,一个人一年可能只去几个地方,但每个去过的地方都有强烈的偏好信号。基于内容的推荐需要把景点拆成特征向量——比如门票价格、地理位置、景点类型、热度等级——这听起来合理,但落地时你会发现景点特征标注工作量极大,而且“用户喜欢山”不代表“用户喜欢所有山”,场景、季节、同行人都会影响决策。

协同过滤走的是另一条路:不关心景点长什么样,只看“用户和用户之间”“景点和景点之间”的行为相似性。它的假设是,跟你行为相似的人喜欢的地方,你大概率也喜欢。这种思路在旅游推荐里其实非常贴合实际需求——朋友推荐往往比算法推荐更可靠,而协同过滤恰恰是在模拟这个“口碑传播”的过程。

毕设选用协同过滤还有一个现实理由:它能用小巧的数据集支撑完整的算法流程。深度学习方案虽然听起来高级,但需要足够多的行为日志和算力资源,在毕设周期内很难跑出可信结果,而且答辩时解释难度也更高。

2.2 基于用户的协同过滤:手写相似度计算与 TopN 生成

基于用户的协同过滤分三步:构建用户-景点评分矩阵,计算用户间相似度,为目标用户找最近邻并生成推荐。评分矩阵的构建方式有两种——用户主动打分和系统根据行为自动折算;毕设里最简单可靠的做法是让用户在 Web 页面上直接给景点打分,1 到 5 分,同时在用户每次查看景点详情时自动记录一次行为日志,折算成 0.5 分的隐式加分。这样可以避开“数据稀疏”这个答辩必问的问题。

相似度计算是整个系统的核心代码,手写不要用 sklearn 的 pairwise_distances,那样答辩时讲不清楚。常见的做法是直接实现余弦相似度:

import math def cosine_similarity(vec_a, vec_b): """ 计算两个用户向量的余弦相似度 vec_a: 用户a对所有景点的评分列表, 未评分填0 vec_b: 用户b对所有景点的评分列表, 未评分填0 返回: 相似度值, 范围[-1, 1], 越接近1表示越相似 """ if len(vec_a) != len(vec_b): raise ValueError("两个向量的维度必须一致") dot = sum(a * b for a, b in zip(vec_a, vec_b)) norm_a = math.sqrt(sum(a * a for a in vec_a)) norm_b = math.sqrt(sum(b * b for b in vec_b)) if norm_a == 0 or norm_b == 0: return 0.0 return dot / (norm_a * norm_b)

这个函数的逻辑并不复杂:分子是两个向量对应位置相乘后累加,得到的是投影方向上的重合程度;分母是两个向量的模长相乘,作用是消除打分尺度差异——有的人习惯全打 5 分,有的人全打 3 分,模长归一化之后这种个人偏好就被抹掉了。return 0.0 的那个判断必须写,因为总有新用户一条评分记录都没有,向量全 0 时除零会直接让程序崩溃。

拿到相似度之后,下一步是生成推荐列表,这里有一个参数需要专门调试——最近邻数量 K。K 太小,推荐的依据只有一两个人的偏好,偶然性太大;K 太大,把相似度很低的用户也拉进来,推荐结果会趋于平庸。对于毕设的数据规模(几百个用户,几十个景点),K 通常取 10 到 20,具体多少需要通过实验对比来定,这个对比过程本身就是论文的实验章节素材。

2.3 基于物品的协同过滤:为什么它更适合景点推荐

基于用户的协同过滤有一个直觉上的问题:用户数量是不断增长的,但活跃用户往往高度集中,新用户的评分一进来就要全量重算相似度矩阵,性能吃不消。基于物品的协同过滤则完全不同——它计算的是景点与景点之间的相似度,而景点的数量远比用户少,而且景点的相似度矩阵不需要频繁更新,可以离线算好存进数据库,线上直接查询。

景点推荐这个场景特别适合基于物品的协同过滤,因为用户会对一个景点产生“一次性评分”——去过就评,不会反复评同一个地方。物品相似度的计算方式类似于“买了 A 的人还买了 B”,用在旅游场景里就是“去过西湖的人还去过灵隐寺”。把这种关联挖掘出来,系统就可以在景点详情页里做“喜欢这个景点的人也喜欢”的推荐栏目,效果直观且容易解释。

毕设的做法是同时实现这两种算法,然后在论文里做一个对比实验,看哪条路线的推荐精度更高、覆盖面更广。这种“双算法对照”的设计是非常标准的毕设加分项,既能展示工作量,又能在答辩时引导老师把提问方向从“对不对”引向“哪个更好,为什么”。

2.4 混合推荐:给答辩留一张“工程化”底牌

协同过滤的冷启动问题非常明显:新用户没有任何行为数据,系统无法为他推荐;新景点没有任何评分记录,它永远不会出现在推荐列表里。毕设答辩时,老师几乎必然问到“新用户怎么办”,应对方案就是混合推荐。

混合策略通常是把基于流行度的推荐作为兜底。在新用户注册后的第一屏,不要返回协同过滤结果,而是返回景点热度排行榜——按访问量降序排列。等到用户产生了第一条评分行为,再切回协同过滤。这个策略在代码层面只需要一个判断,但体现的是对推荐系统实际落地问题的理解,比单纯堆算法更能体现工程意识。具体做法是:在推荐接口的入口处检查当前用户的评分记录条数,少于 3 条走流行度分支,大于等于 3 条走协同过滤分支。这里的阈值 3 是一个经验值,你完全可以在论文里写“本系统通过实验对比发现当评分记录达到 3 条时,协同过滤的效果才稳定超过流行度推荐”,这就变成了一个可辩驳的实验结论。

3. 用 Python 搭建推荐引擎:从数据表设计到核心算法落地

3.1 MySQL 建表:为什么评分表要单独拆出来

旅游景点推荐系统的数据结构不算复杂,核心表也就四张,但表之间的关系一定要理清楚。用户表存账号信息,景点表存景点基本信息,评分表存用户对景点的打分记录,这是标准的三表结构。

-- 用户表: 存储登录账号与基本信息 CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) UNIQUE NOT NULL COMMENT '登录名', password VARCHAR(255) NOT NULL COMMENT '密码(哈希后存储)', nickname VARCHAR(50) DEFAULT NULL COMMENT '昵称', register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 景点表: 存储景点描述信息与基础属性 CREATE TABLE attraction ( attraction_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '景点ID', name VARCHAR(100) NOT NULL COMMENT '景点名称', city VARCHAR(50) NOT NULL COMMENT '所在城市', category VARCHAR(30) DEFAULT NULL COMMENT '景点类型(自然/人文/主题乐园等)', description TEXT COMMENT '景点简介', avg_score DECIMAL(2,1) DEFAULT 0 COMMENT '综合评分(冗余字段,定期汇总)', visit_count INT DEFAULT 0 COMMENT '访问次数,用于热度推荐' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='景点表'; -- 评分表: 用户对景点的评分记录, 联合主键防止重复评分 CREATE TABLE rating ( rating_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '评分ID', user_id INT NOT NULL COMMENT '评分用户ID', attraction_id INT NOT NULL COMMENT '被评景点ID', score TINYINT NOT NULL COMMENT '评分1-5分', rating_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '评分时间', UNIQUE KEY uk_user_attraction (user_id, attraction_id), CONSTRAINT fk_rating_user FOREIGN KEY (user_id) REFERENCES user(user_id), CONSTRAINT fk_rating_attraction FOREIGN KEY (attraction_id) REFERENCES attraction(attraction_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户评分表';

这里面有一个每个人第一次做毕设都会踩的坑:把评分直接放在用户表或者景点表里。这样看起来能省一条表,但实际上把“一个用户评多个景点”的多对多关系硬塞进了单行记录里,每次查推荐都要做烦琐的字符串拆分。评分表单独拆出来是这一类推荐系统的基本要求,数据库课程设计里的第三范式在答辩时也讲得清楚。

代码里的 COMMENT 是关键,建表语句带上注释,论文的数据表设计部分可以直接引用,不用二次加工。TINYINT 类型而不是 INT 来存评分,是因为评分范围只有 1 到 5,用 TINYINT 就够了,占 1 个字节,这个细节在答辩时随口说出来,显得你对数据库设计有实际经验。joint primary key 加了 UNIQUE KEY (user_id, attraction_id),这是防止同一个用户对同一个景点重复打分。

3.2 喂数据给算法:PyMySQL 读取评分矩阵

数据表建好了,接下来是把 MySQL 里的数据读出来,构造成算法需要的用户-景点评分矩阵。这一步是“数据库”和“推荐算法”两个模块之间的桥梁,最常翻车的地方在于 SQL 写法和数据格式的匹配。用 PyMySQL 连接数据库时,常见的做法是先写一个独立的数据库工具模块,统一管理连接和数据读取:

import pymysql db_config = { "host": "localhost", "user": "root", "password": "your_password", "database": "travel_db", "charset": "utf8mb4", "cursorclass": pymysql.cursors.DictCursor } def load_rating_matrix(): """ 从rating表加载所有评分记录, 构建用户-景点评分矩阵 返回: dict, {user_id: {attraction_id: score}} """ matrix = {} sql = """ SELECT user_id, attraction_id, score FROM rating ORDER BY user_id, attraction_id """ connection = pymysql.connect(**db_config) try: with connection.cursor() as cursor: cursor.execute(sql) rows = cursor.fetchall() for row in rows: uid = row["user_id"] aid = row["attraction_id"] score = row["score"] if uid not in matrix: matrix[uid] = {} matrix[uid][aid] = score finally: connection.close() return matrix

这里的 DictCursor 是很实用的一个配置,fetchall 返回的是字典列表而不是元组列表,可以用列名直接取值,代码可读性高很多。full table scan 在这个毕设场景里是可接受的,因为数据量撑死几千条,但如果你的毕设要求数据量大一些,可以在 rating 表的 user_id 字段上加索引,SQL 执行效率会好很多。

3.3 用户相似度计算:矩阵转置与邻居选取

拿到评分矩阵后,计算用户相似度需要先把矩阵补全成完整维度——每个用户有一条记录都要对所有景点有一个评分值,没评过的用 0 填充,再逐对计算 Cosine 相似度。这里不能照搬 2.2 节里的那个独立函数,你必须先做一个矩阵对齐,否则不同用户评过不同景点,向量长度都不一样,根本没法算。

class UserBasedCF: def __init__(self, rating_matrix): self.rating_matrix = rating_matrix self.user_ids = list(rating_matrix.keys()) self.attraction_ids = self._collect_attractions() self.similarity_matrix = {} def _collect_attractions(self): """收集所有出现过的景点ID并排序, 保证向量维度有序""" aids = set() for ratings in self.rating_matrix.values(): aids.update(ratings.keys()) return sorted(aids) def _build_vector(self, user_id): """将某个用户的评分记录展开为固定维度的向量""" vector = [] ratings = self.rating_matrix.get(user_id, {}) for aid in self.attraction_ids: vector.append(ratings.get(aid, 0)) return vector def calc_all_similarities(self): """计算所有用户两两之间的相似度, 存入相似度矩阵""" for uid_a in self.user_ids: self.similarity_matrix[uid_a] = {} vec_a = self._build_vector(uid_a) for uid_b in self.user_ids: if uid_a == uid_b: continue vec_b = self._build_vector(uid_b) sim = cosine_similarity(vec_a, vec_b) self.similarity_matrix[uid_a][uid_b] = sim

这个类的设计逻辑就是“实时计算、用内存换代码清晰度”,每次请求都全量重算一遍,性能不是最优,但毕设的体量没有任何压力,而且答辩时你讲起来非常顺畅——从评分矩阵到向量展开,再到相似度矩阵,每一步都有代码对应。如果你想优化,可以加一个缓存:相似度矩阵算好后用 pickle 存成文件,只有评分表发生变更时才重新计算。这个优化能写进论文的性能优化章节,工作量不大但表述价值高。

3.4 生成 TopN 推荐列表:加权打分与排除已评景点

有了相似度矩阵,下一步为指定用户生成推荐列表。基本逻辑是:找到与目标用户最相似的 K 个用户,把这 K 个用户评分过的景点收集起来,按加权分数排序,过滤掉目标用户已经评过的,取前 N 个返回。加权分数的计算方式是有讲究的。

def recommend(self, user_id, k=10, n=5): """ 基于用户协同过滤为用户生成景点推荐 user_id: 目标用户ID k: 最近邻数量 n: 推荐结果数量 返回: [(attraction_id, score), ...] 按分数降序 """ if user_id not in self.similarity_matrix: return [] # 取相似度最高的K个用户 neighbors = sorted( self.similarity_matrix[user_id].items(), key=lambda x: x[1], reverse=True )[:k] # 排除目标用户已经评过的景点 rated = set(self.rating_matrix.get(user_id, {}).keys()) # 统计候选景点的加权分数 scores = {} for neighbor_id, sim in neighbors: neighbor_ratings = self.rating_matrix.get(neighbor_id, {}) for aid, score in neighbor_ratings.items(): if aid in rated: continue # 以相似度作为权重, 累加邻居的评分 scores[aid] = scores.get(aid, 0) + sim * score # 按加权分数降序排序并返回前N个 sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) return sorted_scores[:n]

这里要注意的是累加方式。业界标准做法有两种:一种是纯累加,即 scores[aid] += sim * score,分数会随着邻居数增多而无上限;另一种是取均值,即除以所有相似度之和做归一化。在毕设规模下,纯累加会偏向那些被更多邻居评过的热门景点,取均值则偏向小众但高分的景点。建议两种都实现,在论文里做一个对比,说明哪种方式在你们的数据集上效果更好,这又是一个实验点。

3.5 Flask 接口封装:把推荐结果暴露给前端

推荐算法跑通只是第一步,毕设必须能演示,所以要把推荐结果封装成 HTTP 接口,让前端页面能调用。用 Flask 写一个推荐接口是最直接的做法。

from flask import Flask, jsonify, request app = Flask(__name__) @app.route("/api/recommend/<int:user_id>", methods=["GET"]) def api_recommend(user_id): """ 推荐接口, 接收用户ID, 返回推荐景点列表 参数: k(最近邻数, 默认10), n(推荐数量, 默认5) """ k = request.args.get("k", default=10, type=int) n = request.args.get("n", default=5, type=int) matrix = load_rating_matrix() cf = UserBasedCF(matrix) cf.calc_all_similarities() result = cf.recommend(user_id, k=k, n=n) # 把景点ID换成景点详细信息 detail_list = [] for aid, score in result: detail = get_attraction_detail(aid) detail["recommend_score"] = round(score, 4) detail_list.append(detail) return jsonify({"code": 0, "data": detail_list}) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5000)

这个接口每次请求都会重新从数据库加载评分矩阵并全量计算相似度,在演示阶段没有性能问题。参数 k 和 n 通过 URL query string 暴露出来,演示时可以直接在浏览器地址栏调整参数看效果变化,这让答辩现场的“参数敏感性分析”变得非常直观。有一个值得注意的细节是 host="0.0.0.0",这样在虚拟机里跑服务时,宿主机浏览器也能直接通过 IP 访问,避免演示时出现“浏览器打不开”的尴尬。

4. 榜单生成的调参逻辑:不要小看 K 值和 N 值对结果的影响

4.1 K 值选择:从查全率与查准率的摇摆说起

很多毕设代码里,K 值就是个拍脑袋写死的常量。这样做不是不行,但答辩时被问到“为什么选 10”就卡住了。K 值对推荐结果的影响可以用一句话概括——K 值越小,结果越依赖少数几个高度相似的邻居,推荐的景点越个性化但覆盖面越窄;K 值越大,个性化程度下降,结果越来越接近热门榜。

在旅游推荐场景里,这种影响有具体表现。K=5 时,如果用户的最近邻都集中在某个城市,推荐结果很可能全部是那座城市的景点,相当于把“推荐”做成了“同城搜索”;K=30 时,相似度 0.1 的邻居也被拉进来,他们的行为贡献基本是噪声,推荐结果开始趋同于热门榜,失去了“个性化”的意义。经验区间是 K 取 10 到 20,但你需要用实验数据支撑这个选择。具体的实验方法后面避坑章会说,这里先记住一个结论:K 值的选取要与数据集规模匹配,用户总量越少,K 越要往小取。

4.2 相似度计算的边界:评分分布偏差如何影响推荐质量

余弦相似度的一个隐藏问题是它不减去用户的平均评分偏差。举个例子:用户 A 只去过三个地方,全打 5 分;用户 B 去过二十个地方,也全打 5 分。两人在评分尺度上完全不同,但余弦相似度计算出的相似度可能很高,因为他们的向量方向接近。这个问题在学术界的标准解法是调整余弦相似度,即把每个用户的评分减去自身的平均分后再计算。

def adjusted_cosine_similarity(vec_a, vec_b): """ 调整余弦相似度: 先减去均分, 再计算余弦 优势: 消除用户打分尺度差异的影响 适用: 用户评分集中在3-5分时效果更稳定 """ avg_a = sum(vec_a) / len(vec_a) avg_b = sum(vec_b) / len(vec_b) adj_a = [x - avg_a for x in vec_a] adj_b = [x - avg_b for x in vec_b] dot = sum(a * b for a, b in zip(adj_a, adj_b)) norm_a = math.sqrt(sum(a * a for a in adj_a)) norm_b = math.sqrt(sum(b * b for b in adj_b)) if norm_a == 0 or norm_b == 0: return 0.0 return dot / (norm_a * norm_b)

你可能已经发现了,这段代码跟 2.2 节里的普通余弦相似度相比,只多了两行“减去均值”的操作。但这恰恰是两种算法在效果上的分水岭。一个实际场景:假设用户 A 是个“老好人”,什么景点都打 5 分;用户 B 非常挑剔,去过很多景点但只给 3 分。普通余弦相似度计算出来两人相似度偏高,会把 B 不喜欢的景点错推给 A。而调整余弦相似度先减去各自的平均分,A 的所有评分都变成 0,B 的评分都在均值附近波动,两人的相似度会回落到合理水平。在毕设论文里,把两种相似度的实验结果都跑出来,分析差异原因,这一点就能写出上千字。

4.3 推荐数量 N 值:接口返回多少条才合适

N 值看起来不像 K 值那么关键,但它同样影响用户体验。返回到推荐列表的长度,直接影响前端页面的展示密度和“推荐体验”。N 取 3 时,用户觉得推荐太敷衍;N 取 20 时,用户的滑动成本变高,而且因为数据规模小,后排的推荐质量明显下降。毕设演示推荐的 N 取 5 到 8 即可,理由也很简单:旅游推荐的场景不像电商,用户不会一次性看完 20 个地方再决定去哪,5 个推荐足够展示算法效果。

N 值的另一个影响是评测指标——下一章会讲到精确率和召回率,N 值不同,这两个指标天然会此消彼长。所以写论文时,一定要把实验过程中 N 值的设置写清楚,否则“准确率 80%”这种数据就没有对照物,答辩老师一眼就能看出问题。

4.4 冷启动处理:给新用户和新景点各留一条路

前面混合推荐部分提了一句冷启动,这节展开讲一下代码层面怎么落地。新用户的处理策略已经在推荐接口里做了分支,即评分少于 3 条时返回热门榜。这个逻辑要说细致一点,是“查询评分记录数量”这个动作不要每次请求都去全表扫描 rating 表,否则数据量上去后接口会变慢。在一个 user_id 上统计 count 需要用索引,PyMySQL 查询时直接 count(*) where user_id=? 就行,MySQL 的优化器会走主键索引,效率足够。新景点的冷启动更难处理——没有评分就永远不会被推荐,“有去无回”。常见做法是在后台管理页面给新景点一个人工预设的基础分,或者定时任务里按景点描述的词频相关度做一个初始推荐池。毕设周期里做人工预设基础分就够了,管理员录入景点时表单里加一个“初始评分”字段,默认给 2.5 分,这样新景点至少能进候选集。

5. 避坑指南:基于 Python 的旅游景点推荐系统从开发到答辩的 5 个典型翻车点

5.1 建表语句里字段类型定义错误导致中文乱码

现象:系统跑起来后,景点名称和城市字段在页面上显示为“???”,但数据库命令行里查询却是正常中文。

原因:MySQL 建表时没有给表和连接指定字符集。很多初学者在可视化工具里建库时已经选了 utf8mb4,但代码里连接数据库时没有带上 charset="utf8mb4",导致连接层使用默认的 latin1 字符集。

解决:在建库时执行 ALTER DATABASE xxx CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,同步把建表语句里的 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 加上,代码里 PyMySQL 连接参数也要带上 charset="utf8mb4"。这三处必须保持一致,缺一个都会乱码。

5.2 评分表的联合唯一索引失效导致重复评分

现象:通过前端页面给同一个景点连续点击两次“评分”,第一次提交成功,第二次却报错或数据覆盖。

原因:执行 INSERT 时没有处理重复数据。虽然在 rating 表设计了 (user_id, attraction_id) 联合唯一索引,但代码里用的是“先 SELECT 再决定 INSERT 还是 UPDATE”的逻辑,两次并发请求同时执行 SELECT 都没查到记录,就都走了 INSERT 分支,后插入的触发唯一约束报错。

解决:改用 INSERT ... ON DUPLICATE KEY UPDATE 语句,让数据库的原子操作处理这种边界,而不是靠应用层的查重逻辑。这在金融级系统里是标准做法,毕设代码里直接写成这样,体现的是工程严谨性。如果不想让旧评分被覆盖,而是保留最高一次评分,可以写成 UPDATE score = MAX(score, VALUES(score)),但推荐还是用 ON DUPLICATE KEY UPDATE score=VALUES(score) 保持最近一次评分的直觉语义。

5.3 用户相似度矩阵重算导致接口响应时间越来越长

现象:演示时第一次请求推荐接口响应很快,第二次就慢了一倍,到后面越来越慢,好像系统在“退化”。

原因:UserBasedCF 类在每次请求时都重新 load_rating_matrix 并全量 calc_all_similarities。数据量小时没问题,但演示过程中你一边讲解一边不断点击评分,评分表数据持续增加,每次请求的重算成本也在走高,花一两分钟才返回结果。

解决:把相似度矩阵的计算结果做缓存,评分表数据没有变化时直接用缓存。最简单的方式是构建矩阵时记录一个“数据版本号”——用 rating 表的 MAX(rating_time) 作为版本标识,每次请求时先查询版本号与缓存中的版本号是否一致,一致则直接复用内存中的相似度矩阵,不一致才重算。这个优化在毕设代码里实现起来只要十几行,但答辩时讲出来效果非常好,解决的是真实系统最常见的痛点。

5.4 推荐结果里全是已经去过的景点

现象:某个用户的推荐列表里出现了他 3 个月前已经评分过的景点,用户反馈“推荐了个寂寞”。

原因:生成推荐列表时没有过滤掉已评分景点。这个问题最容易出现在你换数据集测试时——如果你的评分矩阵来自一份爬取的公开数据,很多记录的 user_id 并不是从 0 开始连续编号,前端传入的 user_id 可能对应的是一个本身就不存在完整行为记录的用户。

解决:在 recommend 方法内部,一定要有 rated 集合过滤代码,而且在接口层做一个防御检查:如果传入的 user_id 在相似度矩阵里查不到,直接返回热门榜作为兜底,而不是返回空列表。这个兜底逻辑不仅防丢人,还能让答辩演示更流畅。

5.5 论文里的准确率数据没有明确评测口径

现象:论文里写了一句话“本系统推荐准确率达到 85%”,答辩老师追问“准确率怎么算的”,只能支支吾吾说不清楚。

原因:评测指标定义混乱,没有说清是精确率、召回率还是 F1。毕设里最常见的错误是把“推荐列表里有多少条是用户喜欢的”当成准确率,但这本质上只是精确率的一种特例,样本划分不明确时数据没有意义。

解决:论文实验章节必须明确评测方案——把评分数据集按 8:2 切分为训练集和测试集,训练集用来计算相似度矩阵和生成推荐,测试集里用户真实评过分的景点作为“标准答案”,然后统计推荐命中率。这一整套评测代码其实很简单,但在毕设里极少有人认真做,做完就是降维打击。

6. 推荐效果的量化验证:五个指标、一份对比实验与一组截图

做到这一步,系统的功能链路已经完整,但还差一项工作——用数据证明你的推荐“有用”。这不是给评委看热闹,而是给论文的实验章节提供素材。毕设阶段推荐系统常用的评测指标有五项,不需要全用,选两三项能说明问题即可。

精确率衡量“推荐出来的东西里用户真的喜欢的比例”,计算公式是命中数除以推荐数。召回率衡量“用户喜欢的景点有多少被推荐出来了”,命中数除以测试集里用户实际评分的景点总数。F1 是两者的调和平均。还有两项被你 K 值决定的指标:覆盖率衡量推荐结果涉及了多少不同景点,多样性衡量推荐列表内部差异有多大。在论文里跑一张表就够了,表格格式大致是——不同 K 值下,精确率、召回率和覆盖率各是多少,最后标注当 K=15 时 F1 达到最高值。三列数据加上一段分析,实验章节就撑起来了。

验证脚本也值得保存下来,以后新增了评分数据可以直接重新跑一遍。建议用命令行脚本的方式管理,功能是从数据库分别读取训练集和测试集,调用同一个 UserBasedCF 类,输出评测结果;这样每次调完参数再跑一次脚本,得到新数据之后更新论文中的表格,整个过程全部可控。脚本里加一行对运行时间的打印,顺手把性能数据也放进论文章节。

最后把推荐结果页面截图保存下来——一张新用户的热门推荐截图、一张多次评分后的个性化推荐截图、一张 K 值分别为 5 和 20 时的结果对比截图。截图放在论文实验章节里,比任何文字描述都有说服力。整个方案做完,我习惯性会把源代码压缩包和数据库 SQL 文件分开放进“code”和“doc”两个目录,数据库 SQL 文件里顺带写上测试数据插入脚本,这样换一台电脑部署时不用手动敲数据,省去大量重复劳动。希望帮到你。

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

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

Mac mini M6 32G大模型实测:算力、TPS与端云决策全解析

最近总有人问我同一句话&#xff1a;32G内存的Mac mini M6跑大模型&#xff0c;到底行不行&#xff1f;这里的“行”往往包含三层意思&#xff1a;能不能装上跑起来&#xff0c;每秒能蹦几个字&#xff0c;以及有了它之后还要不要买云端API。说白了就是三个字——算力、TPS、端…

作者头像 李华
网站建设 2026/10/1 7:38:50

基于资源的约束委派攻击,红队高频攻击链路

基于资源的约束委派攻击&#xff0c;红队高频攻击链路 免责声明&#xff1a;本文内容仅用于授权红队演练、企业 AD 安全自查、安全学习研究&#xff0c;严禁在未授权的域环境执行 RBCD、AD 属性篡改、Kerberos 票据伪造等操作。未经授权对计算机信息系统进行渗透测试属于违法行…

作者头像 李华
网站建设 2026/10/1 7:37:22

独享代理IP vs 共享代理IP:有什么区别?如何选择?

1. 引言在爬虫采集、数据挖掘、账号注册、广告验证等场景中&#xff0c;代理IP几乎是绕不开的基础设施。而挑选代理IP时&#xff0c;最先遇到的抉择往往就是&#xff1a;独享代理IP还是共享代理IP&#xff1f;两者价格差异明显&#xff0c;使用体验也大不相同。本文将从原理、性…

作者头像 李华
网站建设 2026/10/1 7:35:41

OpenHarmony I2C驱动开发实战:从协议原理到排障技巧

做OpenHarmony设备开发&#xff0c;从传感器、屏幕到各种外设&#xff0c;八成会遇到I2C。尤其是你想在开发板上接个环境光传感器、姿态传感器的时候&#xff0c;跑一版I2C驱动&#xff0c;反复读不到数据、偶尔死锁、时序不稳&#xff0c;这种问题我想不少人都遇到过。这篇内容…

作者头像 李华
网站建设 2026/10/1 7:34:56

位移运算的物理本质:从CPU寄存器搬运到嵌入式性能优化

1. 这不是“<<”和“>>”&#xff0c;这是CPU在你眼皮底下直接搬数据的物理动作很多人第一次看到a << 3或b >> 2&#xff0c;下意识觉得这是个“数学运算”&#xff0c;顶多联想到乘除法——这恰恰是理解位移操作最大的认知陷阱。我带过几十个刚学C/C的…

作者头像 李华