基于协同过滤的商品推荐系统实战:从原理到Python实现与避坑指南
简介这是一份面向计算机类毕业设计的推荐系统完整项目资料包基于协同过滤算法并引入Word2Vec/Doc2Vec嵌入方法解决物品冷启动问题。资料围绕基于用户与基于物品两种推荐维度展开先通过标签词向量化计算物品相似度再对相似度排序得到TOP-N相似用户或物品生成推荐结果并过滤用户已有记录或明确不感兴趣的物品适合需要完成课程设计、毕业论文或求职项目的学习者。压缩包共1160个文件约25.98MB以HTML/CSS/JS前端页面、Java/JSP后端逻辑、SQL/DB数据库脚本、Python脚本、MP4演示视频及论文文档为主要构成目录结构便于按开发模块对照学习。目前已有65人学习下载。附带完整源码、环境配置说明、操作录屏、论文与文档还包含前端页面、样式、脚本、后端接口与数据库脚本形成的可运行闭环可帮助快速掌握协同过滤推荐系统的工程实现、冷启动处理思路与毕业设计答辩要点。1. 基于协同过滤的商品推荐系统为什么是毕业设计里最稳妥的选题每年毕业季推荐系统方向最热门的选择之一就是协同过滤算法。一个购物网站商品推荐系统电影、音乐、图书都能套进同一个框架因为这三类场景本质相同用户对物品产生行为系统根据历史行为预测下一次选择。这个标题的完整魅力在于它不只是算法而是 python 源码、文档说明、演示视频和论文一套齐活对应本科毕设里最经典的算法 系统 论文三件套也适合想在短时间内跑通推荐系统全流程的入门者。和其他毕设题目比它的优势是数据来源明确、算法边界清晰、演示效果直观一台普通笔记本就能完成全部开发。下面我按自己的实现顺序先把算法原理和选型讲清楚再给出可用代码和避坑记录最后聊怎么把它做出答辩亮点。2. 先选对算法再写代码UserCF 与 ItemCF 的取舍、相似度计算与数据形态很多人在写第一行代码前就直接进入造轮子阶段结果做成一个调包侠项目答辩时算法原理一问三不知。推荐系统最重要的是先建立直觉协同过滤的核心假设是物以类聚人以群分如果两个用户的历史行为高度重合他们有相似偏好如果两个商品经常被同一批人购买它们就是相似商品。顺着这两句话才有后面所有代码的合理性。2.1 UserCF 与 ItemCF 怎么选购物网站场景下优先哪边基于用户的协同过滤UserCF找的是和我兴趣相近的用户把这些人买过的东西推荐给我基于物品的协同过滤ItemCF找的是和我买过的商品相似的商品直接推荐相似的物品。两种思路在商品推荐里都能跑但实际效果差别很大。对比维度UserCFItemCF相似度计算对象用户与用户物品与物品典型适用场景新闻资讯、短视频、社交内容电商、图书、电影、音乐计算粒度用户数大在线找相似用户开销高物品数相对少相似度可离线预计算用户兴趣漂移敏感用户偏好变化快相对稳健物品属性稳定推荐可解释性和你相似的人买了…偏弱你喜欢的电影的相似电影偏强对购物网站而言用户量通常远大于商品量而且商品电影、歌曲、图书的属性相对稳定不会因为几天不登录就变味所以 ItemCF 是更稳的起点。它还有两个实际好处一是物品相似度矩阵可以提前离线算好演示时响应快二是因为你喜欢 A所以推荐与 A 相似的 B这个理由在答辩时一目了然评委不需要理解复杂概念就能接受。顺带回答一个高频疑问矩阵分解比如 SVD、ALS也是协同过滤的进阶形态效果往往更好但它属于隐因子模型需要用迭代训练去逼近用户和物品的隐藏向量。对本科毕设来说基于相似度的 ItemCF 更可控、更容易解释也更容易应对评委你为什么用这个方法的追问。矩阵分解可以作为论文里的改进方向提一句而不是作为主线。2.2 三种相似度计算方式余弦、皮尔逊、杰卡德的取舍确定 ItemCF 之后下一个决策是用什么公式衡量两个物品相似。最常见的三种方式各有适用场景我直接给出了对比。余弦相似度cos(θ) (A·B) / (|A||B|)把用户对物品的评分当作向量计算夹角余弦。它适合显式评分数据缺点是没有对评分标准差异做处理——一个新用户习惯打 5 分另一个习惯打 3 分两个人实际口味相近但分值差很多余弦会低估这种相似。皮尔逊相关系数在余弦的基础上先把每个向量减掉均值消除用户自身的打分偏差然后再计算余弦。公式是 r Σ((x-x̄)(y-ȳ)) / sqrt(Σ(x-x̄)² · Σ(y-ȳ)²)。在图书、音乐这类有明确 1~5 分评级的场景里皮尔逊通常比纯余弦稳健。杰卡德相似度J(A,B) |A ∩ B| / |A ∪ B|只关心两个物品是否被共同交互过不关心交互的具体分数。它天然适合电商里的加购、购买这类只有发生了/没发生两种状态的隐式反馈数据。下面给出用 NumPy 手写的三种相似度函数方便在做对比实验时直接切换。import numpy as np def cosine_sim(a, b): 余弦相似度输入两个等长评分向量返回相似度 norm_a np.linalg.norm(a) norm_b np.linalg.norm(b) if norm_a 0 or norm_b 0: return 0.0 return np.dot(a, b) / (norm_a * norm_b) def pearson_sim(a, b): 皮尔逊相关系数先各自中心化消除用户打分尺度差异 a_centered a - a.mean() b_centered b - b.mean() if np.linalg.norm(a_centered) 0 or np.linalg.norm(b_centered) 0: return 0.0 return np.dot(a_centered, b_centered) / (np.linalg.norm(a_centered) * np.linalg.norm(b_centered)) def jaccard_sim(a, b): 杰卡德相似度把非零值视为已交互算交集占并集比例 a_bin (a 0).astype(int) b_bin (b 0).astype(int) inter np.sum((a_bin 1) (b_bin 1)) union np.sum((a_bin 1) | (b_bin 1)) return inter / union if union 0 else 0.0三个函数都用浮点数返回0 表示完全不相似1 表示完全重叠。参数 a、b 是等长的物品向量长度等于用户数。购物网站的数据大多是隐式反馈买了、收藏了、加购了没有评分这种情况下皮尔逊和余弦的效果差异不大反而杰卡德更直接它把共同购买人数和总购买人数的比例关系用最直观的方式算出来参数少、好解释。在实际项目里我一般先跑杰卡德如果数据是显式评分再切到皮尔逊对比。2.3 数据应该长什么样从行为日志到共现矩阵推荐系统里有一句经验拿到一个数据集先花一半时间把它整理成能用的形态。购物网站上用户几乎不会主动打4 分、5 分真实日志是点击、加购、下单数据表长这样user_iditem_idbehavior_typeratingtimestamp10012001click0170000000110012001buy1170000000510022003buy11700000012这张行为表是三种推荐场景的统一入口换成电影 ID 就是电影推荐换成书籍 ISBN 就是图书推荐换成歌曲 ID 就是音乐推荐。算法不关心物品长什么样只关心用户和哪些物品发生过交互。接下来要把这张表旋转成用户-物品矩阵行是用户列是物品值是评分或行为标记。这个矩阵有两个致命特点极其稀疏大多数格子是 0且规模随用户数和物品数乘积增长。1000 个物品的相似度矩阵是 1000×1000用 float64 存储约 8MB看似不大但如果商品数到 10 万 SKU就要 80GB完全不可能直接建矩阵。稀疏性带来的问题我在第 4 章展开讲现在只需要形成这个直觉数据整理阶段的目标就是把原始日志变成干净的 user-item 矩阵再从这个矩阵里找出物品间的关联。3. 用 Python 实现协同过滤推荐系统从 CSV 到 Top-N 推荐再到 Web 接口原理讲完下面是可以直接复现的实现。代码依赖只有 pandas、numpy、Flask 三个库我刻意没有引入任何重型框架方便看清楚协同过滤的每一步在干什么。如果拿到的毕设套件里有源码和文档说明建议先对照这里的结构理解自己的项目数据在哪个文件、相似度矩阵怎么生成、推荐结果从哪里来。3.1 准备一份能跑的数据三种推荐场景共用同一张行为表先造一份小型样例数据。这里用电影场景举例user 1~20 对 item 1~30 产生交互行为被简化成 rating未来想换音乐、图书只需要把 item_id 换成对应的歌曲或书本 ID。import pandas as pd import numpy as np np.random.seed(42) rows [] for uid in range(1, 21): # 每个用户随机看 5~15 部电影 for iid in np.random.choice(range(1, 31), 10, replaceFalse): rating 4.0 if np.random.rand() 0.3 else 3.0 timestamp int(np.random.rand() * 1000000) rows.append([uid, int(iid), rating, timestamp]) df pd.DataFrame(rows, columns[user_id, item_id, rating, timestamp]) df.to_csv(user_behavior.csv, indexFalse) print(df.shape)这里有几个参数值得说明np.random.seed(42) 固定随机种子保证每次生成的样例数据一致便于调试每个用户固定产生 10 条交互故意做成有人没看过某些电影的稀疏形态rating 只取 3.0 和 4.0 两个值模拟看过但没打细分的情况。如果想用真实数据从公开数据集或自己的订单表里导出同样的四列即可核心是必须有 user_id、item_id、timestamp 三列rating 可以缺失缺失时后续代码会当作隐式反馈处理。3.2 核心算法构建物品相似度矩阵与 Top-N 推荐这是整套系统的核心包含两个关键方法build_item_sim_matrix 构建物品间相似度recommend_for_user 为指定用户生成推荐列表。为了更贴近购物网站的真实行为这里默认用杰卡德相似度计算物品关联。import pandas as pd import numpy as np def build_item_sim_matrix(df, min_inter2): 基于行为表构建物品相似度矩阵。 min_inter: 两个物品至少要有多少个共同用户才计算相似度过滤噪声 # 只保留有交互的记录自动过滤掉行为类型里无意义的行 df df[df[rating] 0] # 旋转成用户-物品矩阵行是用户列是物品 user_item df.pivot_table(indexuser_id, columnsitem_id, valuesrating).fillna(0) items list(user_item.columns) sim_rows [] for i in range(len(items)): for j in range(i 1, len(items)): item_a, item_b items[i], items[j] vec_a user_item[item_a].values vec_b user_item[item_b].values # 杰卡德交集 / 并集 inter np.sum((vec_a 0) (vec_b 0)) union np.sum((vec_a 0) | (vec_b 0)) if inter min_inter: continue sim inter / union if union else 0.0 sim_rows.append((item_a, item_b, float(sim))) return pd.DataFrame(sim_rows, columns[item_a, item_b, sim]) def recommend_for_user(df, sim_df, uid, top_n10): 给指定用户推荐 top_n 个物品 # 用户已经交互过的物品不再重复推荐 interacted set(df[df[user_id] uid][item_id]) score {} # 遍历用户交互过的每个物品去找它们的相似物品 for item in interacted: pairs sim_df[(sim_df[item_a] item) | (sim_df[item_b] item)] for _, row in pairs.iterrows(): cand row[item_b] if row[item_a] item else row[item_a] if cand in interacted: continue # 累加候选物品的相似度得分 score[cand] score.get(cand, 0) row[sim] recs sorted(score.items(), keylambda x: x[1], reverseTrue)[:top_n] return [int(i) for i, _ in recs] df pd.read_csv(user_behavior.csv) sim_df build_item_sim_matrix(df, min_inter2) print(sim_df.sort_values(sim, ascendingFalse).head(5)) print(对用户 1 的推荐结果:, recommend_for_user(df, sim_df, uid1, top_n10))build_item_sim_matrix 的两层循环是朴素的 O(N²) 实现物品数量在几千以内时足够用min_inter 参数是一个有效的噪声过滤器——两个物品如果共同用户太少计算出的相似度没有统计意义默认设为 2实际项目中按数据集规模调整稀疏数据可降到 1稠密数据建议提到 5。recommend_for_user 的推荐逻辑是经典的相似度累加用户看过的每部电影都会给它的相似电影贡献分数最后按总分排序取前 10。这种简单实现有个缺陷是热门候选物品分数虚高改进方案放在第 4 章避坑部分。3.3 用 Flask 包一层 Web 接口让推荐结果可以被演示和调用毕设答辩必须有可演示的东西。光有算法脚本还不够评委更愿意看到输入一个用户 ID 就能在浏览器里返回推荐结果。用 Flask 包一层最小的 HTTP 接口几十行内就能实现。from flask import Flask, jsonify, request import pandas as pd import numpy as np app Flask(__name__) # 启动时加载数据并预计算相似度矩阵避免每次请求都重算 df pd.read_csv(user_behavior.csv) sim_df build_item_sim_matrix(df, min_inter2) app.route(/recommend, methods[GET]) def recommend(): GET /recommend?user_id1 返回该用户的推荐列表 uid int(request.args.get(user_id, 1)) recs recommend_for_user(df, sim_df, uid, top_n10) return jsonify({ user_id: uid, recommendations: [{item_id: item} for item in recs] }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这段代码有两个值得注意的设计一是相似度矩阵在服务启动时只算一次而不是每次请求都重建这在购物网站的真实场景里对应离线计算在线召回的经典架构二是接口返回值是 JSON方便后面接前端页面或小程序。host 设为 0.0.0.0 是为了在局域网内用手机或另一台电脑访问演示效果比只在本机访问好得多。实际使用时把 item_id 翻译成商品名、封面、价格再拼接一个简单的 HTML 列表就是一个观感完整的购物网站商品推荐系统。debugTrue 在毕设演示时开着方便排查但千万别在生产环境里开。4. 协同过滤最常见的 5 个坑现象、原因与排查方法协同过滤核心代码不过十几行但真正在数据集上跑起来问题一个接一个。下面这 5 个坑是我在实现和帮人调试这类系统时碰到频率最高的每个都按现象→原因→解决展开希望你在答辩前就排查掉。4.1 相似度矩阵全为 0推荐列表永远为空现象构建完 sim_df 后打印出来全是 0推荐接口返回空数组控制台也没有报错。原因最常见的是数据格式不一致。比如行为表里的 item_id 是字符串1而 pivot_table 生成的列名是整数 1交集计算永远找不到匹配对象相似度全部是 0。另一个原因是数据太稀疏两个物品根本没有任何共同用户杰卡德公式里并集为 0 时直接被跳过。解决先做数据体检——打印出 sim_df 的样本量、交互记录的行数、物品的 ID 类型确认是否匹配。再检查交集分布每个物品平均和几个物品有交集如果平均值小于 2说明行为数据不足需要换数据集或补充行为来源。第三统一 ID 类型建议所有 ID 在读取时统一用astype(str)处理避免 int 和 str 混用这个隐藏地雷。df[user_id] df[user_id].astype(str) df[item_id] df[item_id].astype(str) sim_df build_item_sim_matrix(df)4.2 推荐结果全是热门商品个性化极差现象每个用户拿到的推荐列表都差不多永远是那几部豆瓣高分电影或几个爆款商品完全看不出用户画像的差异。原因这是 ItemCF 的固有偏差不是 bug。热门商品和所有物品都有高共现度所以它们的相似度累加分数天然碾压长尾商品。算法本意是和你历史的交集最大但在数据层面变成了和所有人交集最大。解决加一个热门惩罚因子削弱爆款在最终分数里的优势。常见做法是最终得分除以候选商品流行度的对数流行度用该物品的总交互次数表示。def get_popularity(df): 计算每个物品的总交互次数 return df[item_id].value_counts().to_dict() def recommend_for_user_penalized(df, sim_df, uid, top_n10, alpha0.3): interacted set(df[df[user_id] uid][item_id]) popularity get_popularity(df) score {} for item in interacted: pairs sim_df[(sim_df[item_a] item) | (sim_df[item_b] item)] for _, row in pairs.iterrows(): cand row[item_b] if row[item_a] item else row[item_a] if cand in interacted: continue # 除以 log(1 流行度)爆款受到压制 score[cand] score.get(cand, 0) row[sim] / np.log(1 popularity.get(cand, 1)) return sorted(score.items(), keylambda x: x[1], reverseTrue)[:top_n]alpha 参数没有直接用实际运行时我一般把 alpha 当作是否启用惩罚的开关在实验里对比开和不开的推荐结果选差异明显的方案写进论文。注意np.log(1 popularity)中的 1 是为了防止流行度为 0 时除零错误。4.3 冷启动新用户和新物品没有推荐结果现象给一个没有任何历史行为的用户 ID 调接口返回空数组新上架的商品永远没有曝光机会。这在购物网站的运营视角是致命的——新品得不到推荐位新用户看到的是空白页。原因协同过滤完全依赖历史行为没有历史就没有输入这是算法的本质缺陷。图论里管这叫孤立节点问题新节点没有与任何已有节点相连的边。解决用兜底策略做混合推荐。对无历史用户直接返回全局热门榜 Top-N保证接口永远有输出对无历史行为的物品基于内容属性电影的类型、导演图书的分类、出版社音乐的歌手、曲风计算内容相似度找同类物品做关联。这两种方案实现成本低在论文里作为缓解冷启动的混合策略单独成节是加分项。4.4 数据量一涨内存直接爆掉现象启动代码后程序卡死或长时间没响应最终报 MemoryError。这在把样例数据换成真实数据集时几乎必现。原因两层循环 O(N²) 的时间复杂度加上稠密矩阵 O(N²) 的空间复杂度。N 是物品数当物品数量到 1 万以上时两两计算要执行上亿次循环存一个 1 万×1 万的 float64 矩阵就要 800MB内存根本扛不住。解决三个方向同时做。第一用 scipy.sparse.csr_matrix 存用户-物品交互矩阵只存非零元素把存储压到原来的百分之一以下。第二给相似度矩阵加就近截断每个物品只保留相似度最高的 K 个邻居K 通常取 20~50相当于把稠密矩阵改成稀疏邻接表。第三在计算时跳过交互次数过低的物品对min_inter 参数从 2 提到 5连计算量一起减。这三步做完几万物品的数据量在普通笔记本上能平稳运行。from scipy.sparse import csr_matrix # 将稠密 DataFrame 转成稀疏矩阵数据量大时使用 user_item df.pivot_table(indexuser_id, columnsitem_id, valuesrating).fillna(0) sparse_matrix csr_matrix(user_item.values) print(sparse_matrix.shape, 非零元素数:, sparse_matrix.nnz)4.5 离线评估切分方式不对指标虚高到离谱现象把用户-物品矩阵随机划分成训练集和测试集跑完计算精确率结果高到 60%、70%答辩时被评委追问一下数据划分方式就露馅了。原因随机划分忽略了行为的时间顺序。同一个用户未来的观影记录被当成了训练数据模型相当于偷看了测试集答案这在推荐系统评估里是大忌。推荐系统处理的是时序行为今天的模型只能用昨天的数据来预测。解决必须按时间切分。做法是对每个用户把行为按 timestamp 升序排列前 70% 作为训练集后 30% 作为测试集相似度矩阵只用训练集构建测试集只用来计算推荐结果是否命中了用户真实行为。这样评估出来的指标才是可信的答辩时也能理直气壮地讲清楚评估逻辑。5. 加分的第二阶段混合推荐、离线评估与毕设呈现前面四章把协同过滤本身讲透了。但对毕设来说光有基础算法还不够能体现你真的懂的往往是后面这些加分项。5.1 混合推荐用热度兜底把冷启动问题填上在第 4 章冷启动的基础上给出完整做法。推荐接口返回前做一个优先级判断有历史行为的走 ItemCF 计算没历史行为的直接返回热度榜。app.route(/recommend, methods[GET]) def recommend_mixed(): uid int(request.args.get(user_id, 1)) # 设备行为检查 has_history len(df[df[user_id] uid]) 0 if not has_history: # 热度榜兜底返回全局交互次数最多的前10个商品 hot_items df[item_id].value_counts().index[:10].astype(int).tolist() return jsonify({user_id: uid, strategy: hot, recommendations: hot_items}) recs recommend_for_user(df, sim_df, uid, top_n10) return jsonify({user_id: uid, strategy: itemcf, recommendations: recs})策略字段 strategy 的返回是个很妙的设计演示时能直观地向评委展示系统根据用户情况选择了不同策略也方便你调试时确认走的哪条路径。5.2 写进论文的评估精确率、召回率、覆盖率怎么算评估实验必须有代码。下面的 evaluate 函数按时间切分计算三个在推荐系统论文里最常用的离线指标精确率推荐列表中真正被用户看过的比例、召回率用户真正看过的东西里被推荐覆盖的比例、覆盖率推荐系统总共推了多少物品数值越高说明长尾发掘能力越强。def evaluate(df, sim_df, top_n10): 按时间切分评估每个用户前70%训练后30%测试 precisions, recalls [], [] all_items set(df[item_id]) recommended_items set() for uid, group in df.groupby(user_id): group group.sort_values(timestamp) cut int(len(group) * 0.7) train, test group.iloc[:cut], group.iloc[cut:] if len(test) 0: continue recs recommend_for_user(train, sim_df, uid, top_ntop_n) test_items set(test[item_id]) hit len(set(recs) test_items) precisions.append(hit / top_n) recalls.append(hit / len(test_items)) recommended_items.update(recs) return { precision: float(np.mean(precisions)), recall: float(np.mean(recalls)), coverage: len(recommended_items all_items) / len(all_items) }代码里的关键细节是 recommend_for_user(train, sim_df, uid) 传的是训练集 train而 sim_df 也必须用训练集构建不能把全量数据的相似度矩阵拿来做评估否则又变成数据泄露。top_n 参数建议分别取 5、10、20 跑三组实验输出一条随 top_n 变化的曲线图放论文里图表一放实验分就有了。精确率、召回率和覆盖率这三个指标互相牵制论文里不需要追求每一项都高关键是把指标随参数变化的趋势讲清楚。5.3 文档说明、演示视频和论文怎么组织才像回事标题里带了源码、文档说明、演示视频和论文很多同学拿到这些材料后不知道怎么下手。我的建议是先做减法再补东西第一周把源码跑通理解代码结构替换数据跑出自己的推荐结果第二周写文档说明重点记录环境怎么配、代码各部分功能是什么、怎么更换数据集第三周拍演示视频最标准的流程是打开系统首页→输入一个用户ID→展示推荐结果→切换另一个不同行为的用户→展示推荐列表的差异第四周写论文按照系统分析→系统设计→系统实现→系统测试的结构推进把第 3 章的三张核心图行为表结构、相似度矩阵结果、推荐接口返回截图放进去评估实验独立成节。这几周的过程里我印象最深的一类现象是大部分翻车不是在算法实现上而是在环境配置、数据格式和时间切分上它们比模型本身更消耗时间。所以别急着改模型先把基础链路反复跑通调参是实验做出来的不是设计出来的。希望这些经验能帮你少走弯路做出一份经得起答辩提问的协同过滤推荐系统。本文还有配套的精品资源点击获取