资讯详情

混合聚类推荐算法实战:Canopy+K-means+协同过滤源码解析

📅 2026/10/22 3:05:22 | 华诺云谱 👁 阅读
混合聚类推荐算法实战:Canopy+K-means+协同过滤源码解析
简介这份资源是面向推荐系统入门与进阶学习者的实战项目包围绕豆瓣电影用户数据实现了一套融合Canopy预处理与K-means聚类的混合推荐算法并以协同过滤为基础生成个性化电影推荐。适合具备Python基础、希望理解聚类与推荐算法如何协同工作的开发者可用于课程设计、毕业项目或算法练手。压缩包为rar格式整体约88.88MB上游未提供具体文件清单从项目描述看应包含数据预处理、特征提取、Canopy粗聚类、K-means细化、用户相似度计算与推荐生成等模块的代码与数据。目前已有579人学习下载说明该案例具备一定参考价值。读者可借此掌握从原始观影数据清洗到推荐列表输出的完整链路理解Canopy如何缩小K-means计算范围、协同过滤如何基于相似用户行为补全推荐结果并可将这套混合策略迁移到电商、音乐、新闻等场景形成可复用的算法思路与排错经验。1. 混合聚类推荐算法从豆瓣数据到个性化推荐这份源码包能跑通什么如果你手头有一份用户-电影评分矩阵想快速搭一个能出推荐结果的聚类方案又不想从零推导公式那这份「GitHub_混合聚类推荐算法.rar」值得拆开看看。它用豆瓣电影用户数据做底子把 Canopy 预处理和 K-means 核心聚类串起来再挂上协同过滤做推荐生成整条链路是完整的。适合两类人一是刚接触推荐算法的学生或转行者想找一个能跑通、能改参数的实战项目二是已经会调 Scikit-learn但没试过把聚类和协同过滤拼在一起用的开发者。它解决的核心问题是在大规模用户数据上直接用 K-means 跑得慢、初始中心敏感而 Canopy 先粗筛一遍能把计算范围压下来推荐结果也更稳。下面按「数据怎么进、聚类怎么跑、推荐怎么出、坑在哪」的顺序拆。2. 数据预处理与特征提取豆瓣评分矩阵怎么变成算法能吃的格式2.1 原始数据的典型结构和清洗动作豆瓣电影用户数据常见的形式是长表一行一条评分记录字段包括用户 ID、电影 ID、评分值、时间戳。直接丢给聚类算法是不行的因为 Canopy 和 K-means 处理的是数值向量不是原始日志。我一般会先做三件事去重、去缺失、把评分转成用户-电影矩阵。去重是因为同一用户对同一电影可能有多条记录保留最新一条去缺失是剔除评分空值转矩阵是把长表 pivot 成「行是用户、列是电影、值是评分」的宽表。这个宽表就是后续所有算法的输入。import pandas as pd import numpy as np # 读取原始评分数据假设字段为 user_id, movie_id, rating, timestamp raw pd.read_csv(douban_ratings.csv) # 去重同一用户对同一电影保留最新时间戳的记录 raw raw.sort_values(timestamp).drop_duplicates( subset[user_id, movie_id], keeplast ) # 去缺失评分字段不能为空 raw raw.dropna(subset[rating]) # 转成用户-电影矩阵缺失评分填 0 表示未观看 matrix raw.pivot_table( indexuser_id, columnsmovie_id, valuesrating ).fillna(0) print(matrix.shape) # 例如 (5000, 3000)5000 个用户3000 部电影这段代码的逻辑很直白sort_values加drop_duplicates保证每个用户-电影对只有一条记录pivot_table把长表转宽表fillna(0)把没看过的电影标成 0而不是缺失值。参数上要注意keeplast因为豆瓣数据里用户可能改过评分保留最新一次更合理。fillna(0)是个权衡0 表示「未交互」不是「评了 0 分」后续算相似度时要区分开否则会把没看过的电影当成差评。2.2 特征提取从评分矩阵到用户向量宽表本身就可以当特征矩阵用但直接拿几千维的稀疏向量去算距离效果和速度都不好。常见做法是降维或者提取统计特征。我一般会先算每个用户的平均评分、评分标准差、观影数量作为辅助特征如果维度太高再用 TruncatedSVD 把矩阵压到 50 到 100 维。这样每个用户就变成一个低维稠密向量Canopy 和 K-means 跑起来快很多。from sklearn.decomposition import TruncatedSVD # 先算用户统计特征 user_stats pd.DataFrame({ mean_rating: matrix.replace(0, np.nan).mean(axis1), std_rating: matrix.replace(0, np.nan).std(axis1), watch_count: (matrix 0).sum(axis1) }).fillna(0) # 用 SVD 把评分矩阵压到 64 维 svd TruncatedSVD(n_components64, random_state42) user_vectors svd.fit_transform(matrix) # 把统计特征拼上去 user_features np.hstack([user_vectors, user_stats.values]) print(user_features.shape) # (5000, 67)这里replace(0, np.nan)是为了在算均值和标准差时排除未观看的电影否则 0 会拉低平均值。TruncatedSVD的n_components设 64 是个经验值维度太低会丢信息太高降维效果不明显。random_state42保证每次跑结果一致方便调试。拼上统计特征后每个用户向量既有隐语义信息又有行为统计信息聚类时更稳。提示如果数据量不大比如用户数低于 1000可以跳过 SVD直接用原始矩阵加统计特征效果差别不大还省一层调试。3. Canopy 预处理两个阈值 T1、T2 怎么定粗聚类为什么能提速3.1 Canopy 的运作逻辑和阈值选择Canopy 的核心就两个距离阈值T1 和 T2且 T1 T2。算法随机选一个点当 Canopy 中心然后遍历所有点距离小于 T1 的划进这个 Canopy距离小于 T2 的从候选集里删掉因为它已经离中心足够近不需要再当别的 Canopy 的中心。这样一轮下来得到一批松散的 Canopy每个 Canopy 里的点数量不等但都围绕一个中心。T1 决定 Canopy 的覆盖范围T2 决定哪些点被「吸收」掉不再参与后续选中心。常见做法是 T1 取所有点对距离的均值T2 取 T1 的一半但具体要看数据分布。from sklearn.metrics.pairwise import euclidean_distances def canopy_cluster(features, t1, t2): n features.shape[0] canopies [] # 每个 canopy 的中心索引 assigned np.full(n, -1) # 每个点属于哪个 canopy available set(range(n)) while available: # 随机选一个还没被吸收的点当中心 center_idx available.pop() center features[center_idx] distances euclidean_distances(features, center.reshape(1, -1)).ravel() # 距离小于 T1 的划进当前 canopy in_canopy [i for i in available if distances[i] t1] for i in in_canopy: assigned[i] len(canopies) available.discard(i) # 距离小于 T2 的点从候选集删除不再当中心 for i in list(available): if distances[i] t2: assigned[i] len(canopies) available.discard(i) assigned[center_idx] len(canopies) canopies.append(center_idx) return canopies, assigned这段代码里t1和t2是外部传入的euclidean_distances算的是欧氏距离。逻辑上先选中心再把 T1 范围内的点划进来然后把 T2 范围内的点从候选集删掉。注意available.discard(i)在循环里操作集合Python 里不能在遍历集合时直接删所以用list(available)转一下。参数上T1 越大Canopy 越少越大T2 越大被吸收的点越多后续 K-means 的候选中心越少。我一般会先跑几组 T1、T2 看 Canopy 数量和大小分布再定最终值。3.2 粗聚类结果怎么交给 K-meansCanopy 跑完后每个点要么属于某个 Canopy要么被吸收但没明确归属。实际用的时候通常把每个 Canopy 的中心作为 K-means 的初始中心或者把 Canopy 数量作为 K 的参考值。这样 K-means 不用随机初始化收敛更快也不容易陷进局部最优。如果 Canopy 数量太多可以合并距离近的 Canopy如果太少就调小 T1 重新跑。# 假设 user_features 已经准备好 t1 np.percentile(euclidean_distances(user_features[:500]), 60) t2 t1 * 0.5 canopy_centers, canopy_labels canopy_cluster(user_features, t1, t2) print(fCanopy 数量: {len(canopy_centers)}) # 用 Canopy 中心作为 K-means 初始中心 from sklearn.cluster import KMeans kmeans KMeans( n_clusterslen(canopy_centers), inituser_features[canopy_centers], n_init1, random_state42 ) kmeans.fit(user_features)这里t1用距离矩阵的 60 分位数是个启发式做法避免手动试太多轮。t2 t1 * 0.5也是常见比例。KMeans的init直接传 Canopy 中心n_init1因为初始中心已经定了不需要多轮随机初始化。n_clusters等于 Canopy 数量如果 Canopy 太多导致 K 太大可以后续用肘部法或轮廓系数再调。注意Canopy 阶段如果 T1 设得太大Canopy 数量会很少K-means 的 K 就偏小聚类粒度太粗T1 太小则 Canopy 太多K-means 跑得慢。建议先用 60 分位数试再根据轮廓系数微调。4. 协同过滤与推荐生成相似用户怎么找推荐列表怎么排4.1 用户-用户相似度计算和邻居选择聚类完成后每个用户有了一个群体标签。协同过滤阶段我一般会在同一个聚类群体内找相似用户而不是全局找这样相似度计算量小推荐也更贴近群体偏好。相似度用余弦相似度或皮尔逊相关系数都行余弦对评分尺度不敏感皮尔逊对用户评分偏置更鲁棒。豆瓣数据里用户评分尺度差异大我倾向皮尔逊。找邻居时取 Top-NN 一般 20 到 50太少推荐不稳定太多会引入噪声。from sklearn.metrics.pairwise import cosine_similarity def recommend_for_user(user_id, matrix, labels, top_n20, top_k10): user_idx matrix.index.get_loc(user_id) cluster_id labels[user_idx] # 只取同聚类内的用户 same_cluster np.where(labels cluster_id)[0] cluster_matrix matrix.iloc[same_cluster] # 算相似度 sim cosine_similarity(cluster_matrix) sim_df pd.DataFrame(sim, indexsame_cluster, columnssame_cluster) # 找 Top-N 相似用户排除自己 neighbors sim_df[user_idx].drop(user_idx).nlargest(top_n).index # 加权评分相似度乘以邻居评分 neighbor_ratings matrix.iloc[neighbors] weights sim_df.loc[neighbors, user_idx].values.reshape(-1, 1) weighted (neighbor_ratings.values * weights).sum(axis0) / weights.sum() # 排除用户已经看过的电影 seen matrix.iloc[user_idx] 0 scores pd.Series(weighted, indexmatrix.columns) scores scores[~seen].nlargest(top_k) return scores这段代码先根据聚类标签筛出同群体用户再算余弦相似度取 Top-N 邻居然后用相似度加权邻居评分最后排除已看电影取 Top-K 推荐。参数上top_n控制邻居数量top_k控制推荐列表长度。drop(user_idx)是把自己从邻居里去掉否则自己跟自己相似度最高推荐会退化成热门电影。加权评分里weights.sum()做归一化避免邻居数量不同导致分数尺度不一致。4.2 推荐结果评估和冷启动处理推荐生成后得有个评估方式。常见做法是留一部分评分做测试集算 RMSE 或 MAE看预测评分和真实评分的差距。但聚类加协同过滤的方案评估时要注意如果测试用户不在任何聚类里或者聚类内邻居太少推荐质量会掉。冷启动用户评分少于 5 条我一般直接推热门电影不走聚类和协同过滤因为数据太少相似度算不准。from sklearn.metrics import mean_squared_error def evaluate(matrix, labels, test_ratio0.2): # 简单留出法随机选 20% 评分做测试 test_mask np.random.rand(*matrix.shape) test_ratio test_mask matrix.values 0 # 只在有评分的位置评估 true_ratings matrix.values[test_mask] pred_ratings [] for i, j in zip(*np.where(test_mask)): user_id matrix.index[i] movie_id matrix.columns[j] # 用同聚类邻居预测该评分 cluster_id labels[i] same_cluster np.where(labels cluster_id)[0] cluster_matrix matrix.iloc[same_cluster] sim cosine_similarity(cluster_matrix) sim_df pd.DataFrame(sim, indexsame_cluster, columnssame_cluster) neighbors sim_df[i].drop(i).nlargest(20).index weights sim_df.loc[neighbors, i].values ratings matrix.iloc[neighbors][movie_id].values pred (ratings * weights).sum() / weights.sum() if weights.sum() 0 else 0 pred_ratings.append(pred) rmse np.sqrt(mean_squared_error(true_ratings, pred_ratings)) return rmse评估逻辑是随机选测试评分对每个测试评分用同聚类邻居加权预测最后算 RMSE。参数test_ratio控制测试集比例一般 0.2。weights.sum() 0是防止邻居权重全为零导致除零。这个评估方式比较粗糙但能快速看方案有没有大问题。如果 RMSE 明显高于全局均值的标准差说明聚类或相似度计算有问题。提示冷启动用户不要硬套聚类结果直接推热门或按电影类型推效果更稳。聚类标签对评分少的用户本来就不准。5. 避坑与排查Canopy 阈值、K 值、相似度计算里的血泪经验5.1 现象Canopy 数量爆炸或只剩一个原因T1 设得太小每个点都成了 Canopy 中心或者 T1 设得太大第一轮就把所有点吸收完。解决先算距离矩阵的分位数T1 取 50 到 70 分位数之间试T2 取 T1 的 0.3 到 0.5 倍。跑完看 Canopy 数量如果超过用户数的 10%调大 T1如果少于 5 个调小 T1。5.2 现象K-means 收敛后轮廓系数很低原因K 值直接用了 Canopy 数量但 Canopy 数量不一定等于最佳聚类数。解决在 Canopy 数量附近取几个 K 值分别跑 K-means算轮廓系数选最高的。或者用肘部法看惯性下降拐点。不要迷信 Canopy 数量就是 K。5.3 现象推荐结果全是热门电影原因相似度计算时没排除已看电影或者邻居选择时热门电影被高频推荐。解决推荐前用seen matrix.iloc[user_idx] 0过滤掉已看另外可以在加权评分时对热门电影做惩罚比如除以电影被观看次数的对数。5.4 现象同聚类内邻居太少推荐报错或为空原因聚类粒度太细某个群体只有几个用户算相似度时邻居不够。解决设一个最小邻居数比如 5如果同聚类内不够就扩大到相邻聚类或者回退到全局热门推荐。不要硬跑否则推荐列表为空。5.5 现象评估 RMSE 比全局均值预测还差原因聚类标签和评分预测不匹配比如把口味差异大的用户聚到了一起。解决检查特征提取阶段是否把评分尺度差异大的用户混在一起可以先按评分均值分桶再在桶内聚类。另外相似度用皮尔逊代替余弦试试皮尔逊对评分偏置更鲁棒。6. 进阶技巧把聚类标签当特征回灌用增量方式更新推荐跑通基础流程后我一般会做一件事把 K-means 的聚类标签当成新特征拼回用户向量再跑一次协同过滤。这样相似度计算时既考虑评分模式又考虑群体归属推荐结果会更贴。具体做法是user_features np.hstack([user_features, kmeans.labels_.reshape(-1, 1)])然后重新算相似度。注意标签是类别值直接拼进去当数值用会引入虚假的距离关系所以最好做 one-hot 编码或者用嵌入向量代替。另一个技巧是增量更新。用户新评分进来后不需要全量重跑 Canopy 和 K-means可以只更新受影响用户的向量然后重新分配聚类标签。常见做法是维护每个聚类的中心新用户算到各中心的距离归到最近的类如果某个聚类中心偏移超过阈值再触发局部重聚类。这样推荐系统能跟上用户口味变化不用每天全量跑。# 增量更新新用户归到最近聚类 def assign_new_user(new_vector, kmeans_model): distances kmeans_model.transform(new_vector.reshape(1, -1)) return distances.argmin() # 聚类标签 one-hot 后拼回特征 from sklearn.preprocessing import OneHotEncoder enc OneHotEncoder(sparse_outputFalse) labels_onehot enc.fit_transform(kmeans.labels_.reshape(-1, 1)) user_features_aug np.hstack([user_features, labels_onehot])assign_new_user用kmeans_model.transform算新向量到各中心的距离取最近。OneHotEncoder把标签转成独热向量避免数值大小引入虚假距离。sparse_outputFalse保证输出稠密数组方便拼接。这套增量方案适合用户量持续增长的场景全量重跑的成本太高。从那以后我每次做聚类加协同过滤都会先跑一遍 Canopy 看阈值分布再定 K 值最后用增量方式更新。这套流程踩过的坑基本都在上面了希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑