用户画像驱动的混合推荐系统实战:LFM+LightFM动态路由

发布时间:2026/9/16 10:49:16
用户画像驱动的混合推荐系统实战:LFM+LightFM动态路由 简介本资源是一套面向计算机专业本科生的毕业设计级用户画像推荐系统实现基于Python开发融合协同过滤与内容特征的混合推荐算法依托豆瓣电影公开数据集构建完整推荐流程。适用于毕设选题、课程设计及推荐系统入门实践兼顾代码复现与算法理解需求。压缩包共13个文件含3个ZIP分卷MapleMovie.z01/z02/z03及主ZIP、2个PNG效果截图、2份Markdown说明文档项目说明与部署说明、1个CSV原始数据文件、1个DAT模型缓存文件、1个properties配置文件及1个application.properties整体大小144.43MB结构清晰模块分离明确。已有37人下载学习资源提供完整可运行源码、详细设计文档、环境配置指引及远程技术支持覆盖从数据预处理、用户画像构建、混合算法实现到结果可视化全流程小白可直接部署进阶者亦可基于现有框架拓展算法或迁移至其他数据集。1. 这不是又一个“协同过滤内容推荐”的拼凑项目而是一套可落地的用户画像驱动型混合推荐流水线很多毕设级推荐系统把“混合”二字写在 README 里实际代码里却只调用sklearn.metrics算个 RMSE连用户行为序列都没建模。这个豆瓣电影项目不同它用真实爬取的 Top250 影片元数据含导演、类型、年代、评分分布、模拟生成的 5000 用户观影记录含时间戳、评分、标签偏好构建了三层画像体系——基础属性层年龄/性别/活跃时段、行为模式层偏好类型强度、冷热门敏感度、评分偏移倾向、关系图谱层相似用户聚类 ID、影人追随路径。混合策略不是简单加权而是按场景路由新用户走基于影人共现的热度传播算法老用户触发 LFM LightFM 双模型在线融合中间穿插基于用户画像聚类的 fallback 推荐池。整个流程封装为UserProfileEngine和HybridRecommender两个核心类所有模块支持热插拔替换。适合计算机专业本科生做毕设答辩演示也足够让刚入行的数据工程师看懂工业级推荐链路如何从零搭起。2. 用户画像构建从原始行为日志到结构化特征向量的三阶段清洗与编码2.1 数据源解析与字段语义对齐项目中DATA/目录下包含三个关键文件movies.csv1287 行含movie_id,title,genres,director,year,rating_count、ratings.csv约 12 万条user_id,movie_id,rating,timestamp、users.csv5000 行user_id,age,gender,occupation,signup_date。注意ratings.csv中timestamp是 Unix 时间戳单位秒需转换为datetime后提取hour_of_day和day_of_week字段——这直接影响后续活跃时段画像建模import pandas as pd from datetime import datetime ratings pd.read_csv(DATA/ratings.csv) ratings[datetime] pd.to_datetime(ratings[timestamp], units) ratings[hour_of_day] ratings[datetime].dt.hour ratings[day_of_week] ratings[datetime].dt.dayofweek # Monday0, Sunday6提示movies.csv中genres字段为|分隔字符串如Action|Comedy|Sci-Fi不能直接用get_dummies()。必须先str.split(|)再explode()展开否则会导致稀疏矩阵维度爆炸。这是豆瓣数据特有的坑很多初学者在此卡住。2.2 基础画像层静态属性与统计特征工程users.csv提供的age和gender属于强标识字段但需做区间离散化处理。项目采用如下分段逻辑符合国内人口统计习惯年龄区间编码值业务含义0-171青少年18-242大学在校生25-343职场新人35-444成熟用户455高龄活跃用户对应实现代码def encode_age(age): if age 17: return 1 elif age 24: return 2 elif age 34: return 3 elif age 44: return 4 else: return 5 users[age_group] users[age].apply(encode_age) users[gender_code] users[gender].map({M: 0, F: 1})更关键的是行为统计特征每个用户对各类型电影的平均评分、观看频次、类型覆盖率即看过多少种类型 / 总类型数。这里需先关联ratings与movies获取每条评分对应的类型# 构建 movie-genre 映射表避免重复 explode movie_genres movies[[movie_id, genres]].copy() movie_genres[genres] movie_genres[genres].str.split(|) movie_genres movie_genres.explode(genres) # 关联评分与类型 rating_with_genre ratings.merge(movie_genres, onmovie_id, howleft) # 按用户类型聚合 user_genre_stats rating_with_genre.groupby([user_id, genres]).agg({ rating: [mean, count], movie_id: nunique }).reset_index() user_genre_stats.columns [user_id, genre, avg_rating, watch_count, unique_movies]2.3 行为模式层时序敏感特征与偏好强度量化单纯统计平均分会掩盖用户对冷门影片的特殊偏好。项目引入偏好强度系数Preference Intensity Index, PII$$ \text{PII}{u,g} \frac{\text{avg_rating}{u,g} - \mu_g}{\sigma_g} \times \log(\text{watch_count}_{u,g} 1) $$其中 $\mu_g$、$\sigma_g$ 是全局该类型电影的平均分和标准差。该公式同时惩罚低频高分刷分嫌疑和高频低分泛看用户突出稳定高价值偏好。计算代码需分步执行# 全局类型统计 global_genre_stats rating_with_genre.groupby(genres)[rating].agg([mean, std]).reset_index() global_genre_stats.columns [genre, global_mean, global_std] # 合并并计算 PII user_genre_enhanced user_genre_stats.merge(global_genre_stats, ongenre, howleft) user_genre_enhanced[PII] ( (user_genre_enhanced[avg_rating] - user_genre_enhanced[global_mean]) / (user_genre_enhanced[global_std] 1e-8) # 防除零 * np.log(user_genre_enhanced[watch_count] 1) ) # 每用户取 PII 最大值作为该用户最强偏好类型 user_top_preference user_genre_enhanced.loc[ user_genre_enhanced.groupby(user_id)[PII].idxmax() ][[user_id, genre, PII]]2.4 关系图谱层基于行为相似性的用户聚类与影人追随建模项目未使用传统 K-Means而是采用DBSCAN 余弦相似度构建用户关系图。相似度计算基于用户-类型偏好向量维度类型总数值PIIfrom sklearn.cluster import DBSCAN from sklearn.metrics.pairwise import cosine_similarity # 构建用户-类型稀疏矩阵类型列表来自 movies.csv 的所有唯一 genres all_genres sorted(movie_genres[genres].unique()) user_genre_matrix pd.pivot_table( user_genre_enhanced, indexuser_id, columnsgenre, valuesPII, fill_value0 ).reindex(columnsall_genres, fill_value0) # 计算相似度并聚类 similarity_matrix cosine_similarity(user_genre_matrix) clustering DBSCAN(eps0.6, min_samples5, metricprecomputed).fit(1 - similarity_matrix) user_genre_matrix[cluster_id] clustering.labels_ # 输出聚类结果到 DATA/user_clusters.csv user_genre_matrix.reset_index().to_csv(DATA/user_clusters.csv, indexFalse)注意eps0.6是经过网格搜索确定的阈值范围 0.4~0.8过小导致碎片化聚类过大则全归为一类。项目application.properties中已预设此参数无需手动调整。3. 混合推荐引擎LFM 与 LightFM 的双模型协同调度机制3.1 LFM 模型训练隐语义模型的负采样与收敛控制项目采用implicit库实现 LFMLatent Factor Model而非surprise——因前者支持 GPU 加速且对隐式反馈如观影行为更鲁棒。关键参数设置如下参数名值说明factors64隐向量维度过高易过拟合过低丢失特征iterations30迭代次数项目实测 25~35 为最优区间regularization0.01L2 正则强度防止权重爆炸alpha40置信度缩放因子将隐式反馈转为置信度训练代码需显式构造用户-物品交互矩阵import implicit from scipy.sparse import coo_matrix # 构建 COO 矩阵用户ID, 电影ID, 观看次数 user_item_coo coo_matrix( (ratings[rating], (ratings[user_id], ratings[movie_id])), shape(ratings[user_id].max() 1, ratings[movie_id].max() 1) ) # 训练模型 model_lfm implicit.als.AlternatingLeastSquares( factors64, iterations30, regularization0.01, alpha40, use_gpuTrue # 若 CUDA 可用 ) model_lfm.fit(user_item_coo)3.2 LightFM 模型融合用户画像与物品元数据的双通道嵌入LightFM 的核心优势在于能同时输入用户侧特征如age_group,gender_code,cluster_id和物品侧特征如director,year,genres。项目将users.csv和movies.csv中的分类字段进行 one-hot 编码后拼接from lightfm import LightFM from lightfm.data import Dataset dataset Dataset() dataset.fit( usersusers[user_id].unique(), itemsmovies[movie_id].unique(), item_featuresmovies[[genres, director, year]].values.flatten(), # 特征展平 user_featuresusers[[age_group, gender_code, cluster_id]].values.flatten() ) # 构建交互矩阵与特征矩阵 interactions, weights dataset.build_interactions(ratings[[user_id, movie_id]].values) user_features dataset.build_user_features( users[[user_id, age_group, gender_code, cluster_id]].values ) item_features dataset.build_item_features( movies[[movie_id, genres, director, year]].values ) # 训练 LightFM model_lightfm LightFM(losswarp, no_components64, learning_rate0.05) model_lightfm.fit( interactions, user_featuresuser_features, item_featuresitem_features, sample_weightweights, epochs30, num_threads4 )3.3 混合调度策略基于用户冷启动状态的动态路由混合不是简单加权而是根据用户画像实时判断推荐策略新用户user_id未出现在ratings.csv中 或 观看记录 5 条→ 启用PopularityRouter返回当前集群内热门影片按rating_count排序 导演追随链如用户 cluster_id3则推荐 cluster_id3 用户最常看的导演的其他作品老用户观看 ≥ 5 条→ 启动双模型融合$$ \text{score}_{i} 0.7 \times \text{LFM_score}_i 0.3 \times \text{LightFM_score}_i $$但仅对 LFM 得分 top-100 和 LightFM 得分 top-100 取并集后加权避免长尾噪声调度逻辑封装在HybridRecommender.recommend()方法中def recommend(self, user_id, n_items10): if self.is_new_user(user_id): # 实现见 UserProfileEngine.is_new_user() return self.popularity_router(user_id, n_items) # 获取双模型预测 lfm_scores self.model_lfm.recommend(user_id, user_item_coo[user_id], N100) lightfm_scores self.model_lightfm.predict( user_ids[user_id] * 100, item_idslist(range(len(movies))), user_featuresself.user_features, item_featuresself.item_features ) # 取并集并加权 candidate_set set(lfm_scores[0]) | set(np.argsort(lightfm_scores)[-100:]) final_scores {} for item_id in candidate_set: lfm_score lfm_scores[1][lfm_scores[0].tolist().index(item_id)] if item_id in lfm_scores[0] else 0 lightfm_score lightfm_scores[item_id] final_scores[item_id] 0.7 * lfm_score 0.3 * lightfm_score return sorted(final_scores.items(), keylambda x: x[1], reverseTrue)[:n_items]4. 系统部署与效果验证从本地调试到 Flask API 封装的全流程4.1 环境配置与依赖安装适配主流开发环境项目要求 Python ≥ 3.8依赖清单见requirements.txt。特别注意以下三点implicit库需提前安装 Microsoft Visual C Build ToolsWindows或build-essentialUbuntu否则编译失败lightfm依赖cython建议先pip install cython再装 lightfmpandas必须 ≥ 1.5.0旧版本不支持explode()对空字符串的鲁棒处理一键安装命令Linux/macOS# 创建独立环境推荐 conda create -n douban-rec python3.9 -y conda activate douban-rec # 安装核心依赖 pip install -r requirements.txt # 验证关键库版本 python -c import implicit; print(implicit:, implicit.__version__) python -c import lightfm; print(lightfm:, lightfm.__version__)4.2 本地服务启动与 API 测试项目提供app.py作为 Flask 入口启动后监听http://localhost:5000。关键端点如下端点方法功能示例请求/api/recommendPOST获取用户推荐列表curl -X POST http://localhost:5000/api/recommend -H Content-Type: application/json -d {user_id: 123}/api/user/profileGET查询用户画像详情curl http://localhost:5000/api/user/profile?user_id123/api/movie/detailGET获取电影元数据curl http://localhost:5000/api/movie/detail?movie_id1启动服务前需确保DATA/下所有文件就位然后执行export FLASK_APPapp.py export FLASK_ENVdevelopment flask run --host0.0.0.0 --port50004.3 推荐效果验证多维度评估指标计算脚本项目附带evaluate.py支持三种评估模式Holdout 验证随机保留 20% 评分作为测试集计算 Recall10、NDCG10Temporal Split按时间戳切分最后 20% 时间窗口为测试集检验时序鲁棒性A/B Test 模拟对比混合推荐 vs 单一 LFM 推荐的点击率提升运行示例# 执行 Holdout 验证 python evaluate.py --method holdout --k 10 # 输出示例 # Recall10: 0.3241 # NDCG10: 0.2876 # Coverage (items recommended / total items): 0.612评估脚本核心逻辑是重采样用户-物品对避免数据泄露def holdout_split(ratings, test_ratio0.2): # 按 user_id 分组每组内随机抽样保证每个用户都有测试样本 test_samples [] train_ratings ratings.copy() for user_id, group in ratings.groupby(user_id): n_test max(1, int(len(group) * test_ratio)) test_idx group.sample(nn_test, random_state42).index test_samples.extend(test_idx.tolist()) test_set ratings.loc[test_samples] train_set ratings.drop(test_samples) return train_set, test_set5. 进阶技巧快速适配新数据源与模型热更新机制5.1 替换豆瓣数据为自定义数据集的三步法若需接入企业内部观影日志如 CSV 格式含user_id, movie_id, watch_duration, like_flag只需修改data_loader.py中的load_data()函数def load_data(): # 原豆瓣加载逻辑注释掉 # ratings pd.read_csv(DATA/ratings.csv) # 新数据源加载示例企业日志 enterprise_log pd.read_csv(DATA/enterprise_watch_log.csv) # 映射字段watch_duration 600 秒视为有效评分like_flag1 为显式正反馈 ratings enterprise_log[ (enterprise_log[watch_duration] 600) | (enterprise_log[like_flag] 1) ].copy() ratings[rating] np.where( enterprise_log[like_flag] 1, 5.0, # 显式点赞记为满分 np.clip(enterprise_log[watch_duration] / 3600, 1, 5) # 按观看时长映射为1~5分 ) return ratings注意新数据必须保证user_id和movie_id为整数且从 0 开始连续编号否则矩阵索引会错位。可用ratings[user_id] ratings[user_id].rank(methoddense).astype(int) - 1重映射。5.2 模型热更新无需重启服务的增量训练接口app.py中已预留/api/model/update端点支持接收新评分数据并触发增量训练。关键在于implicit的partial_fit()方法app.route(/api/model/update, methods[POST]) def update_model(): new_ratings request.json.get(ratings) # 格式: [{user_id: 1, movie_id: 101, rating: 4.5}] new_df pd.DataFrame(new_ratings) # 构造增量矩阵仅新增交互 new_coo coo_matrix( (new_df[rating], (new_df[user_id], new_df[movie_id])), shapemodel_lfm.user_factors.shape[0], model_lfm.item_factors.shape[0] ) # 增量训练仅更新相关用户/物品的隐向量 model_lfm.partial_fit_users(new_df[user_id].unique()) model_lfm.partial_fit_items(new_df[movie_id].unique()) return jsonify({status: success, updated_users: len(new_df[user_id].unique())})调用方式curl -X POST http://localhost:5000/api/model/update \ -H Content-Type: application/json \ -d {ratings: [{user_id: 123, movie_id: 456, rating: 4.0}]}5.3 推荐结果可解释性增强生成用户-物品匹配理由为满足毕设答辩中的“为什么推这个”需求项目在recommend()返回结果中附加explanation字段。例如{ movie_id: 101, title: 肖申克的救赎, score: 0.92, explanation: 您所在用户群Cluster #3对该导演作品平均评分高出全局均值 1.2 分您过去对 剧情 类型的偏好强度PII3.8显著高于其他类型 }实现逻辑在UserProfileEngine.get_explanation()中def get_explanation(self, user_id, movie_id): cluster_id self.user_clusters.loc[user_id, cluster_id] director self.movies.loc[movie_id, director] user_genre_pii self.user_genre_pii[ (self.user_genre_pii[user_id] user_id) (self.user_genre_pii[genre] self.movies.loc[movie_id, genres].split(|)[0]) ][PII].iloc[0] return f您所在用户群Cluster #{cluster_id}对该导演作品平均评分高出全局均值 {self.director_bias[director]:.1f} 分 \ f您过去对 {self.movies.loc[movie_id, genres].split(|)[0]} 类型的偏好强度PII{user_genre_pii:.1f}显著高于其他类型该字段直接注入 API 响应无需前端额外开发即可展示推荐依据。本文还有配套的精品资源点击获取