差分隐私协同过滤:推荐系统中的隐私保护设计与实现
简介一套基于Python的带差分隐私的协同过滤推荐系统毕业设计源码与文档说明面向计算机、人工智能、自动化等专业的学生尤其适合需要完成推荐系统方向课程设计或毕业设计的读者。项目将协同过滤算法与差分隐私保护机制相结合覆盖数据预处理、相似度计算、推荐生成与隐私预算调节等关键环节代码均经过调试测试可直接运行并配有论文、中期文档与说明文件方便理解设计思路和快速复现。压缩包共16个文件以12个Python源码文件为主体辅以Word文档、Markdown说明及数据子包整体大小仅2.27MB目录结构清晰便于定向查阅与二次开发。已有69人学习下载适合希望借鉴高分毕设项目、研究推荐系统隐私保护实现或扩展算法功能的读者使用。拿到后可基于现有代码调整数据集、修改差分隐私参数或协同过滤策略灵活适配不同实验场景。1. 为什么毕业设计需要把差分隐私和协同过滤绑在一起想象一个场景你手里是 MovieLens 1M 评分数据导师只给了“推荐系统”四个字。如果只交一个协同过滤模型加 RMSE 曲线答辩翻来覆去讲的都是“数据稀疏怎么处理、相似度怎么算”评委大概率会追问“你的工作和其他人有什么区别”。但如果你能在常规协同过滤之上回答清楚“用户评分隐私不被泄露”这个问题并且给出可复现的主机噪声注入实验这就是一个能撕开分数差距的切入角度。带差分隐私的协同过滤推荐系统本质不是发明一种新推荐算法而是把差分隐私Differential Privacy, DP作为一层可度量的隐私保护壳叠加到已有的协同过滤流程上。它解决的核心矛盾是推荐质量依赖于用户评分数据但评分数据恰恰是最敏感的个人行为信息。用 ε-差分隐私的形式化定义去量化“攻击者从模型输出里反推出某个用户具体评分的难度”让隐私保护变成一组可调参数而不是一句写在文档里的空话。这个题目的实际工作量为数据清洗与矩阵构造、协同过滤基线模型、DP 机制实现、隐私预算与效果的双维评估。工具链只需要 Python、NumPy、Pandas不需要深度学习框架所以非常适合作为周期一学期左右的毕业设计。对于已经有五年以上工程经验的读者这篇文章更值得关注的是敏感度计算和隐私预算分配——这两点是工程落地中真正会踩坑的地方。2. 差分隐私与协同过滤的理论基础从邻域模型到矩阵分解2.1 协同过滤的两条主流路线基于内存与基于模型协同过滤推荐系统的训练数据是用户-物品评分矩阵 R其中 R[u][i] 代表用户 u 对物品 i 的打分。实际系统中这个矩阵极度稀疏评分占比通常低于 5%。处理这个稀疏矩阵业界基本分为两条路线。第一条是基于内存的协同过滤也叫最近邻协同过滤。它直接使用评分矩阵计算用户之间或物品之间的相似度然后再用相似邻居的评分预测目标评分。基于用户的 CF 适合用户数小于物品数的场景基于物品的 CF 在物品数小于用户数的场景中更常见。优点是实现简单、可解释性强缺点是计算复杂度随维度增长较快且对数据稀疏非常敏感。第二条是基于模型的协同过滤主流做法是矩阵分解把评分矩阵分解成用户隐向量矩阵 P 和物品隐向量矩阵 Q使得 R ≈ P·Qᵀ。然后通过最小化平方误差训练隐向量。SVD、SVD、ALS 都属于这一类。矩阵分解的泛化能力通常优于最近邻方法但训练过程是一个迭代优化问题差分隐私的注入点也更加隐蔽。对比维度基于内存的 CF基于模型的 CF矩阵分解实现难度低几百行代码中高需要优化器与正则训练速度无需训练但预测时实时计算训练慢预测快差分隐私注入位置相似度矩阵或评分矩阵梯度、目标函数或模型参数适合毕设的程度高中可解释性强弱我建议毕业设计优先选基于内存的 CF因为 DP 机制最容易解释你可以清晰地指出“噪声加在哪一步、影响了哪个矩阵”这在论文评审时比黑盒模型安全得多。2.2 差分隐私的形式化定义与关键参数ε、敏感度差分隐私的直觉定义是任意一个用户的记录从数据集中被替换为另一条记录算法输出的概率分布不会发生明显变化。形式化地说如果算法 M 满足 ε-差分隐私那么对于任意两个相邻数据集 D 和 D相差一条用户数据以及任意输出子集 S有Pr[M(D) ∈ S] ≤ e^ε · Pr[M(D) ∈ S]这里只需要记住两个参数ε 是隐私预算越小代表隐私保护越强但同时意味着你需要注入更大的噪声敏感度Sensitivity是相邻数据集上查询函数输出差异的上界。对于返回实数值的查询函数 f常用拉普拉斯机制实现 DPM(D) f(D) Lap(Δf / ε)其中 Lap(λ) 表示尺度为 λ 的拉普拉斯噪声Δf 是函数 f 的全局敏感度。工程上最容易犯的错误是把 f 的敏感度当成 1 来处理。比如我们要对用户相似度加噪两个用户在不同评分数量下相似度的取值范围虽然都在 [-1, 1]但敏感度并不直接等于 2而是依赖于评分向量的长度和数据边界。如果你用的是评分矩阵本身比如对每个评分加噪敏感度就是评分数据的范围。MovieLens 评分是 1-5 分所以输入扰动的敏感度 Δf 4噪声尺度就是4/ε。如果对相似度加噪敏感度需要根据相似度函数单独推算这也是第 4 章做调参实验时要重点观察的原因。2.3 把差分隐私注入协同过滤的三种常见位置根据加噪位置的不同业界把 DP 与推荐系统的结合方式分为三种。第一种是输入扰动在计算相似度或训练模型之前直接对原始评分矩阵加噪声。这种做法的好处是后续流程完全不需要修改但代价是噪声会被相似度计算中的均值、方差操作进一步放大导致推荐质量损失明显。第二种是输出扰动把查询函数的输出当作敏感对象。例如先计算用户-用户相似度矩阵再对相似度矩阵的每个元素添加拉普拉斯噪声。这个方案的隐私分析相对容易但在相似度矩阵上施加 DP 时需要先确认相似度函数本身是否满足低敏感度条件——皮尔逊相关系数对单个评分的变化并不稳定敏感度较大这是它的缺陷。第三种是梯度扰动面向基于模型的 CF在每次 SGD 更新时对梯度裁剪并添加高斯噪声也就是 DP-SGD。这是目前深度学习领域的主流做法用在矩阵分解上可以得到更低的效用损失但实现复杂度高需要调裁剪阈值和噪声倍率对于毕设来说时间成本偏高。我的建议是毕设选输出扰动具体落实到“相似度矩阵”上。你可以在论文里用一张图展示三个不同位置加噪的效果差异这本身就是有价值的实验结果。3. 在 Python 中实现带 DP 的协同过滤推荐系统从这一章开始进入代码层面。环境依赖建议先固定版本Python 3.10 或更高版本均可NumPy 不低于 1.24Pandas 不低于 1.5。下面的实现不依赖 sklearn全部用 NumPy 手写方便你逐行理解并改写。3.1 数据准备与训练/测试划分我使用 MovieLens 100K 数据集做示例你可以从公开渠道下载u.data文件。把这个文件放到本地目录后读取数据import pandas as pd import numpy as np # 读取 MovieLens 100K 数据字段顺序为 user_id, item_id, rating, timestamp df pd.read_csv(u.data, sep\t, names[uid, mid, rating, ts]) print(f评分总数: {len(df)}, 用户数: {df.uid.nunique()}, 物品数: {df.mid.nunique()}) # 固定随机种子保证实验可复现 np.random.seed(42) # 按用户分组随机抽样 20% 作为测试集 test_uid df.groupby(uid)[mid].apply( lambda x: np.random.choice(x.index, sizeint(len(x) * 0.2), replaceFalse) ) test_indices np.concatenate(test_uid.values) test_df df.loc[test_indices] train_df df.drop(test_indices)这里有一个关键点不能用全局随机抽样必须按用户抽样。否则有些用户可能完全没有测试样本导致评估指标偏向于活跃用户。按用户抽样能保证每个用户至少有一条测试记录。接着把训练集转成用户-物品矩阵。为了让用户索引和物品索引连续这里做一个 map 映射unique_uids train_df.uid.unique() unique_mids train_df.mid.unique() uid_map {u: i for i, u in enumerate(unique_uids)} mid_map {m: i for i, m in enumerate(unique_mids)} row train_df.uid.map(uid_map).values col train_df.mid.map(mid_map).values data train_df.rating.values.astype(np.float64) R np.zeros((len(uid_map), len(mid_map))) R[row, col] data这个矩阵 R 是后续所有计算的基础。注意它的每一个元素对应一个真实评分在后续添加差分隐私时它就是需要被保护的敏感数据。3.2 先实现一个不带隐私保护的协同过滤基线基于用户的协同过滤预测评分分为三步计算用户间相似度、寻找近邻、加权聚合评分。这里选择皮尔逊相关系数作为相似度度量因为它能消除用户打分尺度差异的影响。def pearson_sim(R): 计算用户-用户皮尔逊相似度矩阵 n_users R.shape[0] # 每个用户的评分均值只考虑有评分的列 mean_ratings np.nanmean(np.where(R 0, R, np.nan), axis1) # 中心化评分矩阵只对非零评分减去用户均值 R_centered np.where(R 0, R - mean_ratings[:, np.newaxis], 0) # 计算向量点积和模长 numerator R_centered R_centered.T denominator np.sqrt(np.einsum(ij,ij-i, R_centered, R_centered)) denominator denominator[:, np.newaxis] 1e-9 sim_matrix numerator / (denominator * denominator.T) np.fill_diagonal(sim_matrix, 0) return sim_matrix预测时对于目标用户 u 和物品 i先取出用户 u 对物品 i 评过分的最近邻用户再用相似度加权平均计算预测值def predict(R, sim_matrix, k40): 基于用户的协同过滤预测评分 n_users, n_items R.shape pred np.zeros_like(R) # 预先计算用户评分均值 user_mean np.array([R[u][R[u] 0].mean() if (R[u] 0).sum() 0 else 3.0 for u in range(n_users)]) for u in range(n_users): # 获取用户 u 已经评分过的物品集合 rated_items np.where(R[u] 0)[0] # 用户 u 对所有其他用户的相似度 sim_u sim_matrix[u].copy() # 只保留相似度 0 的邻居 neighbors np.where(sim_u 0)[0] if len(neighbors) 0: pred[u][rated_items] user_mean[u] continue # 取前 k 个邻居 neighbors neighbors[np.argsort(sim_u[neighbors])[::-1][:k]] w sim_u[neighbors] # 邻居对物品 i 的评分未评分为0 neigh_ratings R[neighbors][:, rated_items] # 减去邻居的用户均值做加权平均后再加回目标用户均值 for idx, i in enumerate(rated_items): valid neigh_ratings[:, idx] 0 if valid.sum() 0: pred[u][i] user_mean[u] continue pred[u][i] user_mean[u] np.dot(w[valid], neigh_ratings[valid, idx] - user_mean[neighbors][valid]) / (np.sum(w[valid]) 1e-9) return pred这段代码里需要注意两个细节。第一非零元素代表已评分这要求评分矩阵中的 0 不能是真实评分否则会被误判为未评分。MovieLens 评分从 1 开始所以是安全的。第二相似度权重加在“邻居评分均值偏差”上而不是直接加权原始评分这是为了消除用户评分偏移对预测的影响。3.3 注入拉普拉斯噪声在相似度矩阵上施加差分隐私基线模型写完后差分隐私版本只需要替换相似度矩阵生成函数。我们选择对皮尔逊相似度矩阵加拉普拉斯噪声也就是输出扰动。关键问题是敏感度怎么算。皮尔逊相似度是两个中心化向量的余弦相似度取值范围是 [-1, 1]。在极端情况下一个用户只有一个评分另一个用户有大量评分这个评分值从 1 变成 5会显著改变相似度。由于评分范围是 1-5中心化后取值范围是 [-2, 2]所以近似取全局敏感度 Δf 4 是合理的工程假设。噪声注入代码如下def dp_similarity(R, epsilon): 在皮尔逊相似度矩阵上添加拉普拉斯噪声 epsilon: 隐私预算越小噪声越大 if epsilon 0: raise ValueError(epsilon 必须大于0) sim pearson_sim(R) sensitivity 4.0 # 评分为 1-5 时的全局敏感度近似值 scale sensitivity / epsilon noise np.random.laplace(0.0, scale, sizesim.shape) # 加上噪声后置信在 [-1, 1] 区间内避免语义混乱 dp_sim np.clip(sim noise, -1.0, 1.0) np.fill_diagonal(dp_sim, 0) return dp_sim使用方式非常简单把predict函数接收的sim_matrix从pearson_sim(R)换成dp_similarity(R, epsilon)。你可以先设置 epsilon1.0 跑一次对比 RMSE 是否明显变差再设 epsilon10.0看效果是否接近原模型。这个控制变量实验就是毕业设计里的核心图表。3.4 代码逻辑与参数说明上面三段代码对应的完整流程是原始评分矩阵 R → 中心化 → 皮尔逊相似度 → 加拉普拉斯噪声 → 裁剪到 [-1, 1] → 预测评分 → 评估。核心参数有三个epsilon隐私预算越小保护越强。推荐先跑一组[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]观察曲线。sensitivity敏感度近似值。如果你用的数据集不是 1-5 分要手动修改。比如评分范围 0-10 分敏感度就是 10。k邻居数量一般在 20-60 之间。DP 噪声会稀释相似度的区分度所以 k 可能需要调大。注意这里对相似度矩阵施加 DP 后本质上保护的是“相似度矩阵”这个输出。这意味着攻击者即使看到相似度矩阵也无法轻易推算出某个用户的具体评分。但这并不是本地差分隐私因为服务器还是能够接触原始数据只是对外发布的模型参数满足了 DP 定义。4. 隐私预算、相似度阈值与评估指标的调参实验4.1 ε 取多大才“既安全又不伤效果”隐私预算 ε 的取值没有绝对标准因为不同领域对“可接受风险”的定义完全不同。在推荐系统场景中业界实验通常把 ε 控制在 1 到 10 之间。ε1 时噪声尺度大但推荐质量往往已经退化到不可用的程度ε10 时噪声影响相对小但隐私保护的意义也会被质疑。建议你跑一组网格实验。这里直接给出评估代码使用 RMSE 和 MAE 两个指标from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_prediction(pred_matrix, test_df, uid_map, mid_map): y_true [] y_pred [] for row in test_df.itertuples(): u uid_map.get(row.uid) m mid_map.get(row.mid) if u is None or m is None: continue y_true.append(row.rating) y_pred.append(pred_matrix[u, m]) rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) return rmse, mae eps_list [0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0] results [] for eps in eps_list: dp_sim dp_similarity(R, epsiloneps) pred predict(R, dp_sim, k40) rmse, mae evaluate_prediction(pred, test_df, uid_map, mid_map) results.append((eps, rmse, mae)) print(feps{eps:.1f}, RMSE{rmse:.4f}, MAE{mae:.4f})运行后你会看到RMSE 曲线在 ε 从 0.1 到 1.0 之间下降非常快从 1.0 到 10.0 逐渐平缓。这说明 ε0.1 到 0.5 之间噪声已经淹没了信号继续缩小 ε 只会让结果随机化。实际能接受的折中区间通常在 ε1.0 到 5.0 之间。以下是实验常见的参考值ε 值RMSEMovieLens 100K隐私保护强度可用性评估0.11.95 左右很强几乎随机不推荐1.01.45 左右中强可作为脱敏方案5.01.05 左右中弱可用但保护有限50.00.95 左右弱接近无噪声基线这些数字是模拟参考具体数值会因 train/test 划分方式不同而变化但下降趋势是稳定的。4.2 相似度度量与邻居数量的选择皮尔逊相关对用户评分偏移不敏感是默认首选。但当你加了拉普拉斯噪声后评分偏移本身也被噪声污染皮尔逊相关的优势会减弱。这时候你可以尝试余弦相似度因为它省去了均值中心化步骤噪声的传递路径更短。实现方式是在pearson_sim中把R - mean替换成R本身其余逻辑不变。邻居数量 k 是影响预测稳定性的第二关键参数。在无噪声情况下k 从 20 提高到 60RMSE 通常会略微下降但在有 DP 噪声的情况下相似度矩阵中的“虚假高相似度邻层”会变多如果 k 太小预测容易被噪声邻居主导。我的经验是把 k 从 40 提高到 80同时观察 RMSE 是否改善。如果改善明显说明噪声干扰已经被平均法则抑制如果没有变化说明噪声已经均匀地扩散到了整条相似度分布中。4.3 评估指标与实验框架毕业设计不能只看 RMSE。推荐系统一般还要关注排序质量例如 PrecisionK 和 RecallK。对于每个测试用户你从“未评分物品”中挑出预测分最高的 10 个物品和测试集中用户真实评分高于 4 的物品求交集。下面给出一个简化版本的评估逻辑def precision_at_k(pred, test_df, uid_map, mid_map, k10, threshold4.0): prec_list [] # 获得每个用户实际喜欢的物品集合评分大于等于4 user_pos_items {} for row in test_df.itertuples(): u uid_map.get(row.uid) if u is None: continue if row.rating threshold: user_pos_items.setdefault(u, []).append(row.mid) # 对每个用户从预测矩阵中取前 k 个物品 for u in user_pos_items: pred_scores pred[u] top_k np.argsort(pred_scores)[::-1][:k] hits len(set(top_k) set(mid_map.get(m) for m in user_pos_items[u])) prec_list.append(hits / k) return np.mean(prec_list)注意这个简化版没有剔除训练集中已经出现过的物品严格实验中需要先排除掉用户训练集中的物品再去取前 k 个预测物品否则评估结果会虚高。这个“物品排重”操作是答辩评委最可能追问的细节。在调参时把 ε 和 k 作为网格搜索的两个维度输出一个表格比单独画两条曲线更有说服力。因为 k 的调整实际上在部分补偿 ε 带来的噪声这组实验能证明一个结论差分隐私的效用损失可以被后处理参数部分回收但不能完全消除。5. 用成员推断攻击模拟验证差分隐私效果推荐系统的隐私保护是否生效不能只靠“我们加了噪声”这句话来说服答辩组。你需要设计一个可量化的实验证明攻击者无法从模型输出中区分某个用户是否在训练集中。这个威胁模型叫做成员推断攻击在差分隐私领域是最常用的验证手段。实现一个简化的成员推断攻击思路是这样的假设攻击者拿到了系统的相似度矩阵或模型参数他想猜某个目标用户 u 的某条评分记录是否被用来训练。在没有 DP 保护时如果目标用户的评分参与了相似度计算那么相似度矩阵中与他相关的行会显著不同于随机噪声攻击者可以利用“某个用户与自己相似度是否异常高”来判断成员资格。模拟实验设计如下随机从训练集中抽取 20 个用户再随机抽取 20 个未出现在训练集中的“伪用户”给他们随机生成一个长度与真实用户接近的评分向量。用同一套 DP 预测函数分别计算这 40 个用户的相似度分布然后设计一个分类器最简单的阈值判断去区分它们。你会发现ε 越小区分准确率越接近 50%也就是随机猜测水平。攻击成功率随 ε 变化的曲线可以这样绘制import numpy as np def membership_attack_success(R, epsilon, n_targets20, trials5): success 0 for _ in range(trials): dp_sim dp_similarity(R, epsilon) # 随机选真实用户 real_uids np.random.choice(R.shape[0], n_targets, replaceFalse) # 生成伪用户评分向量长度约为真实用户平均评分数的1.5倍 pseudo np.random.randint(1, 6, (n_targets, R.shape[1])).astype(np.float64) real_scores [] for u in real_uids: real_scores.append(np.max(dp_sim[u])) pseudo_scores [] for pvec in pseudo: # 简单计算伪用户与所有真实用户的平均相似度作为攻击分数 # 这里用余弦相似度以便直接计算 center_p pvec - pvec[pvec 0].mean() norm_p np.linalg.norm(center_p) 1e-9 sims (center_p R.T) / (norm_p * np.linalg.norm(R, axis1) 1e-9) pseudo_scores.append(np.max(sims)) # 最优阈值直接取两组分数均值的中间点 threshold (np.mean(real_scores) np.mean(pseudo_scores)) / 2 pred_real np.sum(np.array(real_scores) threshold) pred_pseudo np.sum(np.array(pseudo_scores) threshold) success (pred_real pred_pseudo) / (2 * n_targets) return success / trials这个攻击模型并不完美但它能直观展示一个规律当 ε 非常小时真实用户的最大相似度和伪用户的最大相似度分布重叠攻击成功率趋近于 0.5当 ε 极大时真实用户因参与过协同计算而明显更“突出”攻击成功率可能达到 0.8 以上。把这个结果和推荐质量 RMSE 曲线画在同一张双纵轴图上论文的“隐私-效用权衡”就立住了。最后强调一个工程细节每次调用dp_similarity生成带噪相似度矩阵时都必须重新采样噪声而且整个实验流程要保持随机种子一致。差分隐私的数学保证依赖于随机性如果你为了实验复现而固定了噪声那么单次实验结果不具备统计意义。正确做法是针对每个 ε 运行多次实验取攻击成功率的均值与置信区间。这个多轮平均的写法就是答辩现场能让你少被追问两分钟的关键。本文还有配套的精品资源点击获取