十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+协同过滤:基于照片分享数据的旅游景点推荐系统实战

Python+协同过滤:基于照片分享数据的旅游景点推荐系统实战 简介基于Python开发的照片分享旅游景点推荐系统是一套面向毕业设计、课程设计与项目开发的完整资料包。该系统将照片分享与景点推荐相结合覆盖数据采集、处理、分析与前端展示等常见开发环节并涵盖数据预处理、特征工程、推荐算法与结果展示等模块适合计算机相关专业学生、程序员及数据分析爱好者参考学习。压缩包内共238个文件包含Python源码py/pyc、表格数据csv、前端页面html/css/js、说明文档docx/md以及图像资源jpg/svg/png等结构清晰方便按需查阅与二次开发。资源包大小约170.04MB目前已有158人学习下载。项目源码经过严格测试可直接运行并提供完整的项目文档与数据分析过程帮助使用者理解推荐系统从数据预处理、特征构建到结果展示的完整链路配套资料可支撑课程设计或毕业设计的说明撰写也适合在此基础上扩展新的功能模块。1. 基于 Python 的照片分享旅游景点推荐系统从相册里挖出下一站目的地“用户手机相册里存着大量旅行照片每一张照片都隐含一条地域信息——经纬度、时间、拍摄主题。如果按用户历史照片推算偏好再去比对其他用户的足迹就会拿到一套可解释的推荐因子。”这就是“基于 Python 开发的照片分享旅游景点推荐系统”的核心思路。它并不是一个普通相册项目而是把照片当成行为数据用户上传照片 → 系统提取 EXIF/GPS → 关联景点 → 构建用户-景点评分矩阵 → 用协同过滤推荐新目的地。这个选题对毕业设计和课程设计都很有利因为它同时覆盖后端 Web 开发、数据库建模、推荐算法和数据分析可视化四条能力线每一条都能独立验收。2. 系统架构与数据模型照片、用户、景点和评分如何汇入推荐链路2.1 照片分享到景点推荐的完整业务闭环在项目拆分时我一般先画一条数据流用户上传照片到后端后端保存文件的同时读取 EXIF 里的 GPS 坐标再用逆地理编码把经纬度换算成城市或景点名随后写入照片记录。系统按用户对景点的访问频率和照片数量换算成隐式评分评分数据汇入推荐模块最终输出候选景点列表给 Web 前端展示。这个闭环完全可以在本地离线跑通不需要对接任何第三方地图大平台答辩演示时非常稳。这里有一个容易被初稿忽略的设计点照片分享和景点推荐应当共用同一张评分数据源而不是各建一套表。很多课程设计会把照片跟评分分开运行起来才发现照片本身的数量、停留时间、点赞数恰恰是隐式评分最稳定的来源。显式评分用户主动打星数据量少冷启动问题严重所以工程上推荐做法是把“访问过没访问过”和“拍照数量”换算成 15 档评分这样评分矩阵能尽快被填满协同过滤算法才有东西可算。2.2 四张核心表的设计与字段解释建模我直接采用 SQLAlchemy演示环境用 SQLite 就够跑想往正式文档上靠就换 MySQL。以下是最小可运行的 4 张表结构表名核心字段作用userid, username, created_at用户信息对应推荐算法的用户维度photoid, user_id, spot_id, file_path, upload_time, lat, lon照片主体外链用户和景点spotid, name, city, category, description景点字典最终推荐对象ratingid, user_id, spot_id, score, create_time显示或隐式评分唯一约束为 (user_id, spot_id)对应 SQLAlchemy 模型的核心片段如下from datetime import datetime from app import db class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) create_time db.Column(db.DateTime, defaultdatetime.now) class Spot(db.Model): __tablename__ spot id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(128), nullableFalse) city db.Column(db.String(64), indexTrue) category db.Column(db.String(64)) description db.Column(db.Text) class Photo(db.Model): __tablename__ photo id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) spot_id db.Column(db.Integer, db.ForeignKey(spot.id)) file_path db.Column(db.String(256)) lat db.Column(db.Float) lon db.Column(db.Float) upload_time db.Column(db.DateTime, defaultdatetime.now) class Rating(db.Model): __tablename__ rating id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) spot_id db.Column(db.Integer, db.ForeignKey(spot.id)) score db.Column(db.Integer, default1) # 1~5隐式评分 create_time db.Column(db.DateTime, defaultdatetime.now)这里有几个选型参数值得说明。score 定成 Integer 而不是 Float是因为隐式评分从次数分桶而来整数档位能让评分矩阵更干净也方便后续做余弦相似度计算。lat 和 lon 用 Float 而不用 String是为了给后续半径查询和聚类做准备。每张表都保留 create_time后面的数据分析模块需要按时间线聚合用户行为。Spot.city 增加 indexTrue因为城市筛选是最高频的查询路径不加索引会导致全表扫描数据量到几千条时就会有卡顿感。2.3 推荐算法选型为什么毕业设计首选用户协同过滤我常被问这个项目能不能上深度学习。能但对课程设计和毕业设计来说评估的核心是算法可解释加工程可跑通。深度学习在照片推荐场景有两个硬伤一是训练样本通常只有几百到几千条远不够模型收敛二是算出来的推荐结果很难向答辩老师解释“为什么推了这个景点”。协同过滤算法旅游推荐系统是行业里被验证最充分的路线数据需求低、效果可量化、代码量适中。三种主流方案的取舍方案优点缺点适用阶段用户协同过滤 UserCF可解释性强新数据可实时参与推荐新用户冷启动差毕业设计首选物品协同过滤 ItemCF稳定性高不受用户口味漂移影响需要计算物品间关联权重中期扩展基于内容或深度学习能直接利用照片视觉特征依赖标注数据和训练环境作为创新点加分本系统采用 UserCF用户上传一张照片后他就自动进入某个“相似人群”集合中。用余弦相似度衡量用户之间的近似程度复杂度 O(n²)用户数在 1000 以内完全扛得住。选型理由定下来之后后面每个函数才能对应回真实业务问题。3. Python 实现照片 EXIF 解析、评分矩阵构建与协同过滤落地3.1 项目目录结构让源码和项目文档从一开始就规范毕业设计评分通常会看源码完整性和项目文档可读性所以目录在第一次 commit 时就按交付结构来组织travel-recommend/ ├── app/ │ ├── __init__.py │ ├── models.py │ ├── photo_parser.py │ ├── recommender.py │ └── router.py ├── data/ │ ├── raw/ # 照片原图和初始 CSV │ └── processed/ # 清洗后的 rating.csv ├── analysis/ │ ├── eda.py # 数据分析 │ └── visualize.py # 可视化图表 ├── docs/ │ ├── README.md │ ├── 需求分析.md │ └── 接口说明.md ├── requirements.txt └── run.py把照片解析独立成 photo_parser.py而不是写进 Flask 路由里这样后端和数据分析两边都能复用同一个解析函数。很多项目做到一半会发现数据分析又重新解析了一遍照片重复劳动。3.2 用 Pillow 解析照片 EXIF 与 GPS 坐标照片的经纬度藏在 EXIF 里而 EXIF 的 GPS 字段以“度分秒”元组存储。封装一个提取函数只对外返回十进制度数方便后续逆地理编码和距离计算from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS def parse_gps_from_photo(path): img Image.open(path) exif img.getexif() if not exif: return None gps {} for tag_id, value in exif.items(): tag_name TAGS.get(tag_id, tag_id) if tag_name GPSInfo: for key, val in value.items(): gps[GPSTAGS.get(key, key)] val if GPSLatitude not in gps or GPSLongitude not in gps: return None lat _dms_to_decimal(gps[GPSLatitude]) lon _dms_to_decimal(gps[GPSLongitude]) # 南纬和西经需要取负值 if gps.get(GPSLatitudeRef) S: lat -lat if gps.get(GPSLongitudeRef) W: lon -lon return {lat: round(lat, 6), lon: round(lon, 6)} def _dms_to_decimal(dms): d, m, s dms return d m / 60.0 s / 3600.0逻辑说明Pillow 的 getexif 返回原始数值字典TAGS 把数字 tag 映射成字段名。GPSLatitudeRef 是 N 或 SGPSLongitudeRef 是 E 或 W只转数值不判断方向会让一部分照片落到错误半球这是常见隐藏 bug。保留 6 位小数对应约 0.1 米精度做城市级景点匹配绰绰有余。如果照片没有 GPS降级策略是从上传表单里读用户填写的城市字段或从文件名里包含的地点关键词做一次简单映射。不要让“照片无 GPS”直接导致上传接口报 500。3.3 构建用户-景点评分矩阵并计算相似度隐式评分换算放在数据预处理层推荐模块只消费已经成型的 score 列。换算逻辑统计同一用户在同一景点的照片数量用四分位数映射到 15 档import pandas as pd def build_rating_from_photos(photo_df): grp photo_df.groupby([user_id, spot_id]).size().reset_index(namephoto_count) # 用分位数切分为 1~5 档隐式评分 grp[score] pd.qcut(grp[photo_count], q5, labels[1, 2, 3, 4, 5]) return grp[[user_id, spot_id, score]]参数说明qcut 的 q5 表示把照片次数等频切成 5 段。即使大多数用户的照片张数集中在 1 到 2 张qcut 依然会按排序位置均匀划分不会像 cut 那样直接报区间错误。labels 必须和 q 数量对齐否则 pandas 会生成 object 类型列后续 cosine_similarity 会报类型错误。接下来是核心的协同过滤代码import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_matrix(ratings_df): # 用户-景点二维矩阵未访问填 0 matrix ratings_df.pivot_table( indexuser_id, columnsspot_id, valuesscore, fill_value0 ) return matrix def usercf_recommend(user_id, ratings_df, k5, top_n10): matrix build_matrix(ratings_df) sim cosine_similarity(matrix) sim_df pd.DataFrame(sim, indexmatrix.index, columnsmatrix.index) if user_id not in sim_df.index: return [] # 新用户返回空由上层做热门兜底 # 取相似度最高的 k 个用户 neighbors sim_df[user_id].drop(user_id).sort_values(ascendingFalse).head(k) # 加权汇总相似用户访问过的景点 scores {} for neighbor, weight in neighbors.items(): for spot_id in matrix.columns: if matrix.loc[user_id, spot_id] ! 0: continue # 跳过该用户已访问过的景点 rating matrix.loc[neighbor, spot_id] if rating 0: scores[spot_id] scores.get(spot_id, 0) rating * weight return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n]三个参数最值得调k 是最近邻数量控制推荐结果的多样性k 太小推荐列表会被单一用户主导建议在 5 到 20 之间对比效果top_n 根据前端展示位数量设置fill_value0 代替 dropna是为了保证 Sklearn 的 cosine_similarity 能在稀疏矩阵上正常工作零向量之间的相似度会被计算为 0不会产生 NaN这比手写内积实现更省心。3.4 用 Flask 把推荐模块封装成可演示接口推荐逻辑独立成函数后路由层就非常薄from flask import Flask, request, jsonify app Flask(__name__) app.route(/api/recommend/int:user_id) def recommend(user_id): top_n request.args.get(top_n, default10, typeint) result usercf_recommend(user_id, load_ratings(), k5, top_ntop_n) spot_names [id_to_name[s] for s, _ in result] return jsonify({user_id: user_id, spots: spot_names})把评分加载放进 load_ratings 函数里统一做缓存避免每次请求都从 SQLite 重新读一遍全表。之后如果要换 FastAPI 或 Django只需要替换路由层推荐算法不用动。4. 数据分析用 pandas 和 matplotlib 验证推荐系统的有效性与用户特征4.1 数据加载与预处理从照片表生成特征宽表写推荐代码之前容易忽略一个步骤数据分析的质量取决于预处理阶段能否把照片表、用户表、景点表合并成一张宽表。我常用的做法import pandas as pd photos pd.read_csv(data/processed/photos.csv) ratings pd.read_csv(data/processed/ratings.csv) spots pd.read_csv(data/processed/spots.csv) # 宽表每行一条照片记录带上景点名称、城市、上传月份 wide photos.merge(spots[[id, name, city, category]], left_onspot_id, right_onid, howleft) wide[upload_time] pd.to_datetime(wide[upload_time]) wide[year_month] wide[upload_time].dt.to_period(M) missing wide[[city, category]].isnull().sum() print(missing)这里比较关键的参数是 dt.to_period(M)它返回月份周期对象后续 groupby 按月聚合时会按时间顺序排序不会出现字符串月份排序错乱。merge 用 howleft 保留照片主表如果照片匹配不上景点应该回头查数据录入问题而不是静默丢行。4.2 景点热度分析与用户行为画像景点热度用两个指标照片数量和平均评分。两者结合能识别出“高热度但低评分”的景点这类景点正是推荐系统调参时应该降权的对象。hot wide.groupby(name).agg( photo_count(id, count), avg_score(score, mean) ).reset_index() hot[heat_bucket] pd.qcut(hot[photo_count], q4, labels[低, 中, 高, 超高])热度分层的作用是后续采样权重可以按层配置。例如最终推荐展示时从“超高”热度层只抽 20%给“中”和“低”热度层留出 40% 名额避免协同过滤的头部效应把少数热门景区反复推给所有人。这个策略在论文里对应推荐系统的覆盖率评估是一个很好的加分点。4.3 用 matplotlib 做 Python 数据分析与可视化用户地域与时间偏好我会出至少三张图热门景点照片数 Top15 柱状图、投稿量的月度趋势折线图、按城市聚合的照片热力图。matplotlib 足够应对不需要引入太重的前端可视化库import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Noto Sans CJK SC] # 处理中文 top15 hot.sort_values(photo_count, ascendingFalse).head(15) plt.figure(figsize(10, 5)) plt.bar(top15[name], top15[photo_count]) plt.title(热门景点照片数 Top15) plt.xticks(rotation45) plt.tight_layout() plt.savefig(docs/top15.png, dpi150)两个高频踩坑参数中文字体必须配置 font.sans-serif否则图上显示方块savefig 设置 dpi150 是文档出版的最低质量要求用默认 100 会在缩放后显得发糊。4.4 推荐效果评估划分训练测试集衡量准确率与召回率推荐系统不能只用“看起来合理”来验收我会按时间维度切分数据用前 80% 的评分记录训练后 20% 作为测试集再算精确率和召回率from sklearn.model_selection import train_test_split train, test train_test_split(ratings_df, test_size0.2, random_state42) def evaluate(usercf_recommend, train, test, k5, top_n10): hit 0 total 0 for uid in test[user_id].unique(): recs usercf_recommend(uid, train, kk, top_ntop_n) rec_ids {rid for rid, _ in recs} test_spot_ids set(test[test[user_id] uid][spot_id]) if test_spot_ids: hit len(rec_ids test_spot_ids) total len(test_spot_ids) recall hit / total if total else 0 precision hit / (len(train[user_id].unique()) * top_n) return precision, recall这里的 precision 是简化算法按训练集用户总数和 top_n 的乘积做分母。如果论文需要更严格的 Precisionk应该对每个用户分别计算“命中数除以 k”再取平均。random_state42 保证答辩时每次跑出来的评估结果一致避免现场翻车。5. 项目交付与进阶把协同过滤替换成图像特征推荐的 3 个关键改动推荐逻辑调通之后项目已经具备毕业答辩的完整形态。交付源码时有几个细节必须注意data/processed 目录下的数据产物不要 commit 进 Git提前配好 .gitignorerequirements.txt 固定 Pillow、flask、pandas、scikit-learn 的主版本号run.py 做成一个入口脚本完成建库、导入照片、启动 Web 服务三个动作让评分老师双击就能跑。排障环节最容易踩的坑集中在四个场景直接对照处理现象原因对策EXIF 解析返回空图片被截图或社交软件压缩过元数据被剥离用原始相机文件测试避免用微信转发后的图推荐结果全为空新用户没有相似邻居对 user_id 不在矩阵里的情况做热门景点兜底评分全是 0fill_value0 的零值语义混淆对评分矩阵做均值中心化后再算相似度matplotlib 中文乱码系统缺中文字体设置plt.rcParams[font.sans-serif] [SimHei]想继续把项目从协同过滤升级成“照片加景点”的双向推荐系统可以引入预训练的 ResNet 提取照片视觉向量再计算用户相册之间的嵌入相似度。这样推荐逻辑会从“找相似人群去过的景点”演变成“找拍照风格最像的用户去过哪里”两种结果在 Web 端用两个 tab 展示答辩时就可以完整讲述“协同过滤加内容嵌入”的混合推荐方案。最后的验证技巧是加一个 /debug 路由直接输出每个用户的 top-3 候选以及候选来源权重用于在答辩现场实时展示“为什么推荐了杭州而不是苏州”。把当时的 debug 截图放进项目文档能让推荐过程完全可解释这比任何口头描述都有说服力。本文还有配套的精品资源点击获取
返回列表