☰
Python实现差分隐私协同过滤:保护隐私的推荐系统毕设方案
2026/10/3 3:04:16 网站建设 项目流程

简介:这是一份基于Python的带差分隐私协同过滤推荐系统源码与文档,定位为高分毕业设计项目,面向计算机、通信、人工智能、自动化等相关专业学生、教师或从业者,适用于课程设计、期末大作业或毕业设计参考。压缩包共16个文件,以12个Python脚本为主,辅以2个Word文档、1个Markdown说明及1个数据压缩包,整体仅2.27MB,目录结构清晰,便于按代码、论文和测试数据分模块研读。该资源已有69人学习,项目答辩评分达98分,代码均调试通过可直接运行,并选用MovieLens小规模数据集,覆盖数据处理、差分隐私机制与协同过滤算法实现等关键环节。配套文档和分模块代码有助于理解隐私保护推荐系统的构建思路,既可作为新手入门的学习样本,也适合在此基础上扩展功能、进行二次开发。

1. 从“推荐系统”到“高分毕设”之间,差的是一个差分隐私

先说一个反直觉的结论:在推荐系统里,真正让你毕业设计拿高分的,往往不是推荐准确率提升了几个点,而是你证明了自己“知道推荐系统现在最头疼的问题是什么”。协同过滤在学术界被研究了几十年,工业界已经把它跑得滚瓜烂熟,但差分隐私(Differential Privacy, DP)给这个老方向重新划了一条起跑线——在保护用户隐私的前提下,推荐质量不能崩。用Python写一套带差分隐私的协同过滤推荐系统,恰恰是这条线上性价比最高的落地项目,既能展示你懂推荐算法本身,又能展示你懂隐私保护的工程化实现,这正是“高分毕设”最看重的两个加分点。

这套系统解决的具体问题是:当用户评分数据进入推荐模型时,如何让攻击者即使拿到模型参数,也无法反推出某个用户的评分记录。常见的做法是在协同过滤的相似度计算或矩阵分解目标函数中加入满足差分隐私的噪声,在推荐精度和隐私预算之间做权衡。适合的人群很明确:正在做推荐系统相关毕设的学生、想往隐私计算方向转的研究生,以及想快速验证DP思想在推荐场景下可行性的工程师。本文会从数学原理讲到可运行的Python代码,再给你一份可以直接复现的参数清单和踩坑记录。

2. 差分隐私 + 协同过滤:先把“噪声从哪里加”这件事想清楚

2.1 差分隐私的数学定义和两个“可视化”参数

差分隐私的核心承诺可以一句话讲完:在数据集里删除任意一个用户,算法输出结果的概率分布变化很小,小到攻击者无法判断某个用户是否在数据集里。正式定义是,对于两个只差一条记录的数据集 D 和 D',随机算法 M 对所有输出子集 S 满足:

Pr[M(D) ∈ S] ≤ e^ε × Pr[M(D') ∈ S]

这里的 ε 被称为隐私预算(privacy budget),它是整个系统里最需要拧紧的旋钮。ε 越小,隐私保护越强,但为了让结果可用,你需要注入更大的噪声,推荐质量随之下降。而噪声的注入量还由一个参数控制:敏感度 Δf,它表示“删除一个用户后,查询结果最大可能变化多少”。

你不需要把定义背得滚瓜烂熟,但要理解一个工程上的结论:敏感度越小,满足同样 ε 需要的噪声越小,推荐质量损失越小。这就是为什么在实际实现中,聪明的人不会直接对原始评分加噪声,而是先做规范化处理,把敏感度压小再加噪声。这里的两个参数——ε 和敏感度 Δf——就是你毕设论文里最值得画图展示的两个维度。

2.2 在协同过滤的哪个环节加噪声:三种选型和取舍

协同过滤指的是基于用户相似度(User-based CF)或物品相似度(Item-based CF)来做推荐。差分隐私可以注入在这条链路的三个不同位置,常见做法有三种:

  • 对原始评分矩阵加噪声:最简单,但敏感度极高。评分的范围是 1 到 5,删除一个用户可能让整个矩阵的某些统计量剧烈变化,噪声必须注得很大,推荐质量崩溃得快。
  • 对相似度矩阵加噪声:计算完用户/物品相似度后,在相似度值上注入拉普拉斯噪声。敏感度相对可控,实现难度中等,是工程上最常见的折中方案。
  • 对推荐结果加噪声:在最终生成 Top-K 推荐列表时对分数加噪声。隐私保护强度被削弱的可能性较大,因为它不保护中间计算过程对相似度泄露的间接推断。

我一般会建议毕设做第二种:在相似度矩阵上注入满足差分隐私的噪声。理由很直接——第一种太粗暴,第三种说服力不足,评阅老师看一眼你选的加噪位置就能判断你是真的思考过还是只是在套代码。下面整个系统的核心逻辑也以这个选择为主线。

2.3 为什么用拉普拉斯噪声而不是高斯噪声

在 ε 差分隐私体系下,拉普拉斯机制是标准答案,因为它的尾巴更厚,能保证严格的 ε 差分隐私。高斯机制只能做到 (ε, δ) 差分隐私,也就是允许以 δ 的概率打破 ε 的界限,数学上更复杂,在毕设里往往会被老师追问“δ 怎么选、为什么这么选”这类你未必答得上来的问题。

拉普拉斯噪声的概率密度是 Lap(0, Δf/ε),实现时只需要一个核心公式:噪音 = Lap(0, Δf / ε)。在Python里用 numpy.random.laplace(0, scale) 就能生成。这里 scale 就是 Δf/ε。敏感度 Δf 的计算和你的具体算法绑定,在相似度计算场景下,如果你用余弦相似度且评分已经规范化到 0 到 1 区间,敏感度的上界通常可以保守地取 1。这一点在写论文时一定要讲清楚,否则“敏感度为什么取这个值”会成为答辩中的破绽。

3. 用 Python 实现一个带差分隐私的 User-based 协同过滤

3.1 系统结构:四个模块,数据流先理清楚

整个系统拆成四个模块比较合理:数据加载模块、隐私预算管理模块、带噪相似度计算模块、推荐生成与评估模块。数据流是:评分数据 → 分组建模(Group by user)→ 相似度矩阵加噪 → 生成推荐 → 计算 RMSE/MAE 等指标。先放一份最小可运行的目录结构和核心代码。

recsys_dp/ ├── data/ │ └── ratings.csv ├── dp_cf/ │ ├── __init__.py │ ├── load_data.py │ ├── privacy_budget.py │ ├── similarities.py │ └── recommender.py └── run_experiment.py

加载数据模块的核心逻辑是先读入 user_id, item_id, rating 三列,然后交叉表转成用户-物品评分矩阵,缺失值置 0。之所以置 0 而非 NaN,是因为后面计算余弦相似度时,稀疏矩阵的缺失项对点积没有贡献,置 0 最自然。

# load_data.py import pandas as pd import numpy as np def load_ratings(path: str) -> pd.DataFrame: df = pd.read_csv(path) # 确保评分列数值化,缺失评分直接丢弃 df = df.dropna(subset=["rating"]) return df def build_user_item_matrix(df: pd.DataFrame) -> pd.DataFrame: # 透视表:行是用户,列是物品,值是评分 matrix = df.pivot_table( index="user_id", columns="item_id", values="rating" ).fillna(0) return matrix

load_ratings 负责清洗原始数据,dropna 的作用是避免个别用户对个别物品没打分却出现空值导致后续计算崩溃。build_user_item_matrix 把长表转成宽表,fillna(0) 是关键决定——当前目标是用余弦相似度,0 表示“无评分”,它和“评分为 0”的含义冲突,所以需要确保原始数据里没有 0 分记录,否则语义会混乱。

3.2 隐私预算管理:让 ε 可以在多次查询中被追踪

差分隐私里一个绕不开的问题是多次查询带来的隐私损失累积。如果每次相似度计算都消耗 ε,那么连续查询十次后实际隐私保护强度已经远低于预期。因此要做一个简单的隐私预算记账模块,这个模块虽然代码只有几行,但它决定了你系统在“隐私可证明”这一点上的可信度。

# privacy_budget.py class PrivacyBudget: def __init__(self, total_epsilon: float, delta: float = 0.0): self.total_epsilon = total_epsilon self.spent_epsilon = 0.0 self.delta = delta def spend(self, epsilon: float) -> bool: """尝试申请一次查询的预算,超支则拒绝。""" if self.spent_epsilon + epsilon > self.total_epsilon: return False self.spent_epsilon += epsilon return True def remaining(self) -> float: return self.total_epsilon - self.spent_epsilon

这个类的核心价值是让“隐私预算超支”这件事变得可见。很多第一次做差分隐私的同学把 ε 当成了一个静态参数,算完就完了,忽略了多次推荐查询会反复消耗预算。spend 方法返回布尔值,调用方拿到 False 之后可以决定是停止推荐还是用缓存结果,这个机制在毕设答辩演示时非常有说服力。

3.3 核心算法:带拉普拉斯噪声的相似度矩阵

下面是整个系统的核心,User-based CF 的相似度计算,加噪前先对评分行做 L2 归一化来压低敏感度,然后注入拉普拉斯噪声。这里我把敏感度按余弦相似度的性质设为 1,如果你用皮尔逊相关系数,敏感度上界需要重新推导。

# similarities.py import numpy as np import pandas as pd from sklearn.preprocessing import normalize def compute_noisy_similarities(matrix: pd.DataFrame, epsilon: float) -> pd.DataFrame: # 对用户评分向量做 L2 归一化,把每行向量长度变为 1 normalized = normalize(matrix.values, axis=1, norm="l2") # 归一化后余弦相似度等价于点积 sim_matrix = normalized @ normalized.T # 敏感度取 1.0,尺度参数 = 敏感度 / epsilon scale = 1.0 / epsilon noise = np.random.laplace(0, scale, size=sim_matrix.shape) # 噪声矩阵对称化,避免相似度不对称导致推荐排序不稳定 noise = (noise + noise.T) / 2.0 noisy_sim = sim_matrix + noise # 屏蔽掉对角线,避免“自己和自己最相似”干扰推荐 np.fill_diagonal(noisy_sim, 0) return pd.DataFrame(noisy_sim, index=matrix.index, columns=matrix.index)

这段代码的逻辑分三步:第一步用 sklearn 的 normalize 做 L2 归一化,归一化之后向量点积就等于余弦相似度,省去逐对计算的开销;第二步生成拉普拉斯噪声矩阵,关键细节是 (noise + noise.T) / 2,这保证噪声矩阵是对称的,否则用户 A 对 B 的相似度和 B 对 A 的相似度会不一致,直接导致推荐列表在对称性上出现奇怪的问题;第三步把对角线清零,因为自己与自己的相似度永远是 1,不加处理会出现系统永远推荐用户已看过的物品这种低级错误。

参数说明:epsilon 传进来的是本次查询可用预算。scale = 1.0 / epsilon 意味着 ε 越小噪声越大。如果你希望直观观察噪声对推荐质量的影响,可以把 epsilon 从 0.1 到 10 按对数间隔取一组值,跑完实验后画一条“ε-推荐精度”曲线,这几乎是毕设里性价比最高的图表。

3.4 评分预测与推荐生成:Top-K 列表的两种生成方式

相似度矩阵算完之后,给目标用户生成推荐有两种常见路径。第一种是“加权平均预测未知评分”,第二种是“直接拿相似用户的高分物品作为推荐候选”。我建议你的系统里同时实现这两种,并在实验里对比它们对噪声的耐受度。

# recommender.py import numpy as np def predict_score(user_id: int, item_id: int, ratings_matrix, sim_matrix, top_n=20): # 取出与该用户最相似的 top_n 个用户 sim_scores = sim_matrix.loc[user_id].sort_values(ascending=False).head(top_n) # 相似用户对目标物品的评分 item_ratings = ratings_matrix.loc[sim_scores.index, item_id] # 去掉未评分项 mask = item_ratings > 0 if not mask.any(): return 0 numerator = np.dot(np.abs(sim_scores[mask]), item_ratings[mask]) denominator = np.abs(sim_scores[mask]).sum() return numerator / denominator if denominator > 0 else 0 def recommend_topk(user_id: int, ratings_matrix, sim_matrix, k=10, top_n=20): scores = {} for item_id in ratings_matrix.columns: if ratings_matrix.loc[user_id, item_id] > 0: continue # 跳过已评分物品 scores[item_id] = predict_score(user_id, item_id, ratings_matrix, sim_matrix, top_n) return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:k]

predict_score 里的关键操作是只对“相似用户已评分的物品”做加权,未评分的直接 mask 掉,避免零填充值把预测结果往 0 拉。分母用相似度绝对值求和,是因为噪声可能让部分相似度变成负值,如果直接用原始相似度求和,一个负值大噪声很可能让分母趋近 0,造成预测分数爆炸。这里用 np.abs 是一种稳健化处理,也是差分隐私推荐系统里常见的实用技巧。

4. 跑通实验:数据集选择、评估指标与对照组设计

4.1 数据集的三个可选来源和预处理要点

推荐系统最常用的公开数据集是 MovieLens,特别是 ml-1m 和 ml-100k。但要注意:MovieLens 原始评分文件是 u.data 格式,列之间用制表符分隔,没有表头,需要自己命名。如果你用的是新版 ml-latest-small,读入时要处理 CSV 的表头。在毕设场景里,我建议用 ml-latest-small,数据量小、跑得快、包含 600 个用户和 9000 部电影的评分,足以展示算法效果而不至于训练一次要等半小时。

# 加载 MovieLens 数据并按 8:2 切分训练集和测试集 import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("data/ratings.csv") # 保证同一用户既出现在训练集也出现在测试集 train_df, test_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df["userId"])

stratify=df["userId"] 的目的就是按用户分层切分,避免随机切分导致某个用户完全不在训练集里,否则推荐系统对这个用户的冷启动问题会在实验指标上被严重放大。random_state=42 固定随机种子,保证实验可复现。这一点在论文的“实验设置”部分必须写明:所有实验在相同随机种子下运行三遍取平均值。

4.2 评估指标:RMSE 和 Precision@K,以及一个容易被忽略的覆盖率指标

评分预测任务用 RMSE(均方根误差)来评估,公式为 RMSE = sqrt(mean((真实评分 - 预测评分)^2))。推荐列表任务用 Precision@K,比如 K=10 时,看预测的 10 个物品里有多少是测试集中该用户真正评过高分的物品。覆盖率(Coverage)则衡量系统能推荐多少不同物品,差分隐私噪声大的时候推荐结果往往固化在热门物品上,覆盖率会明显下降,这在直观上展示了“隐私保护带来的代价”。

这三项指标合成一个评估模块,输出为一个表格,便于你在毕设报告里直接粘贴。跑实验时采样测试用户的策略也很关键:计算所有用户的指标很慢且容易因为冷启动用户多而拉低指标,常规做法是随机采样 100 个在训练集中评分记录不少于 20 条的用户,保证评估人群处于正常活跃状态。

4.3 对照实验:不加噪声、加噪声、不同 ε 之间的差异

实验设计至少要包含四组:原始 CF(不加噪声)、ε=10、ε=1、ε=0.1。下面给出主实验脚本的骨架,把不同 ε 下的指标打印出来并保存到 CSV。这里用到的经验值是:对于 ml-latest-small 这个规模的数据集,ε 在 1 到 10 之间时推荐精度下降可以控制在 10% 以内;ε 低于 0.5 后 RMSE 会显著恶化。

# run_experiment.py import pandas as pd import numpy as np from dp_cf.load_data import load_ratings, build_user_item_matrix from dp_cf.similarities import compute_noisy_similarities from dp_cf.recommender import predict_score, recommend_topk df = load_ratings("data/ratings.csv") ratings_matrix = build_user_item_matrix(df) results = [] for eps in [100, 10, 1, 0.5, 0.1]: sim = compute_noisy_similarities(ratings_matrix, epsilon=eps) # 采样用户,计算 RMSE 和 Precision@10 sample_users = ratings_matrix.index[:100] # 简化演示,实际需要更合理的采样 rmse_list, prec_list = [], [] for uid in sample_users: test_items = ratings_matrix.loc[uid] predicted = recommend_topk(uid, ratings_matrix, sim, k=10) # 实际评估逻辑需要结合 train/test 切分,这里是演示骨架 pass results.append({"epsilon": eps, "rmse": np.mean(rmse_list), "precision@10": np.mean(prec_list)}) df_results = pd.DataFrame(results) df_results.to_csv("experiment_results.csv", index=False)

这段代码刻意保留了评估逻辑的占位,因为实际评估必须结合 4.1 的 train/test 切分,否则会出现严重的过拟合评估——训练时已经把测试评分参与过相似度计算,预测结果自然好看但没有说服力。很多第一次做这个方向的同学会在这里翻车:直接用全量数据算相似度,再用全量数据评估,RMSE 低到离谱,被老师一问就露馅。

5. 必踩的五个坑:现象、原因与解决方案

5.1 坑一:相似度矩阵不对称导致推荐列表双向不一致

现象:用户 A 的推荐列表里出现物品 X,但用户 B 的推荐列表里却没有基于同样相似度应该出现的物品 Y,算法日志看不出异常。

原因:生成噪声矩阵时没有做对称化处理。numpy.random.laplace 生成的噪声矩阵各元素独立,相加后 sim_ij 和 sim_ji 不一致,虽然绝对值差异可能很小,但 Top-K 排序对微小差异极其敏感。

解决:在 compute_noisy_similarities 里加入噪声矩阵对称化代码,即 (noise + noise.T) / 2,一行代码解决。这是整个项目里性价比最高的一个修复。

5.2 坑二:L2 归一化后评分向量全零导致 NaN

现象:计算相似度时出现 Warning: invalid value encountered in multiply,结果矩阵里出现大量 NaN。

原因:某些用户只对一到两个物品评过分,且评分经过预处理后变成了 0 向量。L2 归一化时 0 向量的范数为 0,除以零产生 NaN。

解决:预处理阶段剔除评分记录少于 5 条的用户。可以在 build_user_item_matrix 之后加一行过滤代码:ratings_matrix = ratings_matrix[ratings_matrix.astype(bool).sum(axis=1) >= 5]。

5.3 坑三:隐私预算超支后算法仍在运行

现象:审计日志显示 spent_epsilon 已经超过 total_epsilon,但推荐请求还在继续返回结果。

原因:隐私预算模块没有被真正接入推荐主流程,只做了记账没有做熔断。

解决:在 recommend_topk 入口处检查 PrivacyBudget.spend 的返回值,如果返回 False,直接返回缓存中的上一轮推荐结果,或者返回空列表并记录日志。这是让你系统在“隐私保护可验证”上站得住脚的关键动作。

5.4 坑四:固定随机种子后实验结果仍然不稳定

现象:相同代码、相同 random_state 跑三遍,RMSE 波动超过 5%。

原因:随机种子只固定了数据的 train/test 切分,没有固定差分隐私噪声生成的随机性。每次调用 compute_noisy_similarities 都会生成新的噪声,即使数据切分完全一致,结果也不一样。

解决:实验脚本最前面设置 np.random.seed(42),或者在生成噪声的接口里传入一个随机种子参数。如果想严谨一点,跑 10 次不同种子实验,报告均值和标准差,这比单次结果更有说服力。

5.5 坑五:预测分数出现远超评分范围的数值

现象:预测分数出现 17 分、-3 分这类明显超出 1-5 评分范围的值。

原因:加权平均时没有对相似度做绝对值处理,一个大的正噪声或负噪声直接放大了加权结果;另外没有对评分做截断。

解决:在 predict_score 的返回值处加一个评分域约束:return max(1.0, min(5.0, raw_score))。这是推荐系统的标准后处理技巧,但在加了差分隐私噪声后这个约束变得尤其重要。

6. 进阶玩法:把 ε 做成分段参数并画出隐私-效用曲线

如果你想让这个毕设再上一个档次,可以从“单一 ε 全表加噪”升级为“按用户活跃度分配不同 ε”。高活跃用户对推荐质量要求高,同时他们的隐私泄露风险本来就更大;低活跃用户则需要更强的保护,数值上应该分配更小的 ε。这个设计在代码上只多一个 epsilon_map,在理念上却是往“个性化差分隐私”这个研究方向靠拢,答辩时非常加分。

# 按用户评分数量分配个性化隐私预算 def assign_epsilon_by_activity(ratings_matrix, min_eps=0.5, max_eps=5.0): user_activity = ratings_matrix.astype(bool).sum(axis=1) max_activity = user_activity.max() # 活动度低的人分配更小的 epsilon,更强保护 epsilon_map = min_eps + (max_eps - min_eps) * (user_activity / max_activity) return epsilon_map

实现时不再对整个相似度矩阵用一个 scale,而是对每个用户的行使用不同的噪声尺度。需要注意对称性问题:行 i 加了噪声后,矩阵第 i 行第 j 列的值和第 j 行第 i 列的值对应的是不同预算下的噪声,直接使用会产生不对称,解决方式是取两次噪声的平均值作为最终相似度。

验证方法建议如下:横轴取 epsilon,纵轴取 RMSE 或 Precision@10,把无噪声基线、均匀 DP、个性化 DP 三条曲线画在同一张图里。预期结果是低 ε 区域个性化 DP 的 RMSE 明显低于均匀 DP,高 ε 区域两者趋同。画完这张图,你的毕设报告核心实验部分就齐了。

最后说一个我每次做这个方向都会提醒自己的习惯:差分隐私模型里,任何一次添加噪声的操作都要能回答“敏感度是多少”。如果你写不出这个值的推导过程,就不要声称自己做的是差分隐私。把隐私预算记账、敏感度推导、噪声对称化这三件事做扎实,比调出一组漂亮的准确率数字重要得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询