简介:这是一份面向计算机类毕业设计的推荐系统完整项目资料包,基于协同过滤算法并引入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)找的是"和我买过的商品相似的商品",直接推荐相似的物品。两种思路在商品推荐里都能跑,但实际效果差别很大。
| 对比维度 | UserCF | ItemCF |
|---|---|---|
| 相似度计算对象 | 用户与用户 | 物品与物品 |
| 典型适用场景 | 新闻资讯、短视频、社交内容 | 电商、图书、电影、音乐 |
| 计算粒度 | 用户数大,在线找相似用户开销高 | 物品数相对少,相似度可离线预计算 |
| 用户兴趣漂移 | 敏感,用户偏好变化快 | 相对稳健,物品属性稳定 |
| 推荐可解释性 | "和你相似的人买了…"偏弱 | "你喜欢的电影的相似电影"偏强 |
对购物网站而言,用户量通常远大于商品量,而且商品(电影、歌曲、图书)的属性相对稳定,不会因为几天不登录就变味,所以 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_id | item_id | behavior_type | rating | timestamp |
|---|---|---|---|---|
| 1001 | 2001 | click | 0 | 1700000001 |
| 1001 | 2001 | buy | 1 | 1700000005 |
| 1002 | 2003 | buy | 1 | 1700000012 |
这张行为表是三种推荐场景的统一入口:换成电影 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, replace=False): 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", index=False) 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_inter=2): """基于行为表构建物品相似度矩阵。 min_inter: 两个物品至少要有多少个共同用户才计算相似度,过滤噪声""" # 只保留有交互的记录,自动过滤掉行为类型里无意义的行 df = df[df["rating"] > 0] # 旋转成用户-物品矩阵,行是用户,列是物品 user_item = df.pivot_table(index="user_id", columns="item_id", values="rating").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_n=10): """给指定用户推荐 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(), key=lambda x: x[1], reverse=True)[:top_n] return [int(i) for i, _ in recs] df = pd.read_csv("user_behavior.csv") sim_df = build_item_sim_matrix(df, min_inter=2) print(sim_df.sort_values("sim", ascending=False).head(5)) print("对用户 1 的推荐结果:", recommend_for_user(df, sim_df, uid=1, top_n=10))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_inter=2) @app.route("/recommend", methods=["GET"]) def recommend(): """GET /recommend?user_id=1 返回该用户的推荐列表""" uid = int(request.args.get("user_id", 1)) recs = recommend_for_user(df, sim_df, uid, top_n=10) return jsonify({ "user_id": uid, "recommendations": [{"item_id": item} for item in recs] }) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)这段代码有两个值得注意的设计:一是相似度矩阵在服务启动时只算一次,而不是每次请求都重建,这在购物网站的真实场景里对应"离线计算,在线召回"的经典架构;二是接口返回值是 JSON,方便后面接前端页面或小程序。host 设为 0.0.0.0 是为了在局域网内用手机或另一台电脑访问演示,效果比只在本机访问好得多。实际使用时把 item_id 翻译成商品名、封面、价格,再拼接一个简单的 HTML 列表,就是一个观感完整的"购物网站商品推荐系统"。debug=True 在毕设演示时开着方便排查,但千万别在生产环境里开。
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_n=10, alpha=0.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(), key=lambda x: x[1], reverse=True)[: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(index="user_id", columns="item_id", values="rating").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_n=10) return jsonify({"user_id": uid, "strategy": "itemcf", "recommendations": recs})策略字段 strategy 的返回是个很妙的设计,演示时能直观地向评委展示系统"根据用户情况选择了不同策略",也方便你调试时确认走的哪条路径。
5.2 写进论文的评估:精确率、召回率、覆盖率怎么算
评估实验必须有代码。下面的 evaluate 函数按时间切分,计算三个在推荐系统论文里最常用的离线指标:精确率(推荐列表中真正被用户看过的比例)、召回率(用户真正看过的东西里被推荐覆盖的比例)、覆盖率(推荐系统总共推了多少物品,数值越高说明长尾发掘能力越强)。
def evaluate(df, sim_df, top_n=10): """按时间切分评估:每个用户前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_n=top_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 章的三张核心图(行为表结构、相似度矩阵结果、推荐接口返回截图)放进去,评估实验独立成节。
这几周的过程里,我印象最深的一类现象是:大部分翻车不是在算法实现上,而是在环境配置、数据格式和时间切分上,它们比模型本身更消耗时间。所以别急着改模型,先把基础链路反复跑通,调参是实验做出来的,不是设计出来的。希望这些经验能帮你少走弯路,做出一份经得起答辩提问的协同过滤推荐系统。
本文还有配套的精品资源,点击获取