简介:一份基于Python实现差分隐私与协同过滤相结合的推荐系统毕业设计资源,适用于计算机相关专业学生、推荐系统研究者及隐私保护技术学习者。内容覆盖推荐系统隐私保护研究背景与国内外现状、协同过滤算法主要步骤、差分隐私概念与常用实现机制,并完成基于用户的协同过滤推荐系统设计,相似度采用两种方式计算,借助差分隐私对推荐结果进行加密,在MovieLens两个不同规模数据集上实验比较,测试并定位可平衡推荐准确度与隐私保护程度的隐私预算。资源共77个文件,以60张png图片记录实验流程与结果,12个py源码实现核心逻辑,另有docx论文/模板、zip数据集、md说明及license,整体压缩包仅3.37MB,轻量但结构完整。已有922人学习,适合作为毕业设计参考、课程项目复现或隐私推荐系统入门材料。
1. 基于Python的差分隐私协同过滤推荐系统:毕业设计从哪下手
如果拿MovieLens跑一个UserCF,完整推荐流程不需要三百行Python。真正让这套系统从“课程作业”变成“毕业设计”的,不是协同过滤本身,而是差分隐私这一层:用户评分一旦进入推荐流程,任何一次预测都可能被反推出某个用户看过什么、打过几分。基于Python做这个题目的价值在于把两件事同时落地——用协同过滤处理稀疏评分矩阵,用差分隐私控制噪声注入的位置和隐私预算ε。适合动手能力一般但想把理论和工程都做出深度的同学,计算量不大,一台笔记本就能跑通。
不少同学拿到这类题目第一反应是去搜免费python源码大全,找到的推荐系统Demo大多没有隐私模块,跑通容易,答辩时却解释不清“噪声加在哪里、为什么这里不加”。这篇笔记从选型、加噪位置、代码骨架到四个实际踩坑,给你一条能直接复现的路,也告诉你怎么把对照实验做得让评委挑不出毛病。
2. 协同过滤做推荐主体:UserCF、ItemCF与矩阵分解怎么选
2.1 最小可用基线:UserCF的评分预测长什么样
UserCF的思路是“跟你喜好相似的人,他们的打分你大概率也认可”。计算用户间相似度可以用皮尔逊相关系数或余弦相似度,然后取相似度最高的k个近邻,用他们的评分加权平均预测目标用户对某个物品的分数。实现起来很短,但它决定了后面差分隐私噪声能放在哪里。
import numpy as np def predict_rating(ratings, user, item, sim_func, k=10): """最小可用的UserCF预测函数 ratings: (n_users, n_items) 二维数组,0代表未评分 sim_func: 接收两个用户评分向量、返回相似度的函数 """ sims = [] for other in range(ratings.shape[0]): if other == user: continue s = sim_func(ratings[user], ratings[other]) if np.isnan(s): continue sims.append((s, other)) sims.sort(key=lambda x: x[0], reverse=True) neighbors = sims[:k] # 只取前k个近邻 num, den = 0.0, 0.0 for s, other in neighbors: if ratings[other, item] > 0: num += s * ratings[other, item] den += abs(s) return num / den if den > 0 else 0.0这段代码里两个参数最值得调:k控制邻居数量,k太小预测方差大,k太大把低相似度用户也拉进来反而拉低精度;sim_func用皮尔逊还是余弦也会显著影响结果。另外函数内部用0表示缺失值,这在演示代码里方便,但真实处理时要注意后面加噪声时别把0值评分当成有效数据一起扰动。
之所以先把裸UserCF写出来,是为了确认推荐主流程的边界在哪里。后续加差分隐私噪声时,你可以选择加在输入评分上、加在预测函数输出上,或者加在最终排序结果上,主流程不熟,后面这三个位置的区别就很难讲清楚。
2.2 为什么不用ItemCF和矩阵分解:三个方案的毕设适用性
ItemCF在电商场景更常见,因为商品数量相对稳定,用户行为稀疏,计算物品间相似度可以离线做,线上响应快。而UserCF在用户量远小于物品量的影视评分场景下效果和可解释性都好,MovieLens这种数据集天然适合UserCF起步。
矩阵分解是另一个常见选项,用SVD或SGD把用户和物品映射到隐向量空间,预测精度上限更高,但落地复杂度也高。特别是做差分隐私时,矩阵分解的核心是梯度下降训练,每轮迭代都要向梯度加噪声,隐私预算要在数百轮训练中分配,还涉及裁剪梯度范数。对毕业设计来说,理论深度够了,但工程返工风险明显更高。
| 方案 | 适用场景 | 加差分隐私难度 | 毕设友好度 |
|---|---|---|---|
| UserCF | 用户少、物品多、评分密集 | 低,噪声可加在输入或输出 | 高,流程透明好解释 |
| ItemCF | 物品少、用户行为稀疏 | 中,物品相似度计算会被扰动 | 中,要处理物品间关系 |
| 矩阵分解 | 评分稀疏且追求精度 | 高,每轮梯度都要加噪声 | 低,隐私预算管理复杂 |
如果是电商级别的评分数据,工业界通常走基于spark的电商系统推荐路线,用分布式计算处理大规模矩阵。但毕设场景一般就是单机Python,没必要把Spark引进来增加部署负担。矩阵分解可以放在实验对比里作为精度上限,但主推系统用UserCF,时间投入和效果回报最均衡。
2.3 用户行为数据为什么需要差分隐私:一个成员推断的例子
很多人把差分隐私理解成“把数据打码”或“随机删几条记录”,这是误区。差分隐私保证的核心是:修改一条数据,查询结果的分布几乎不变。换句话说,攻击者无法通过观察推荐系统的输出,判断某个用户是否真的在数据库里。
一个典型的攻击场景是这样的:攻击者知道某用户在3月给某部电影打了低分。如果攻击者能反复查询该电影的推荐分数,并观察去除这条评分前后输出的差异,就能推断出该用户的行为。带差分隐私的推荐系统会向查询结果注入噪声,让前后差异被噪声掩盖。
这个特性正是推荐系统和隐私保护冲突最激烈的地方:推荐系统越懂用户,推荐越准;但系统越懂用户,从输出反推用户行为的风险越大。差分隐私给了一个数学上可证明的解决方案,这也是这个题目在答辩时最好讲、最体现价值的地方。下一章就把差分隐私的公式和参数拆开讲。
3. 差分隐私算法落到协同过滤:噪声注入的三个位置与隐私预算分配
3.1 ε、敏感度与拉普拉斯机制:三个必须写进论文的数字
差分隐私的定义这里不绕弯:随机算法M满足ε-差分隐私,指的是对任意只差一条数据的两个数据集D和D',M在两者上输出的分布差异被ε控制。ε越小,两条数据在输出上表现得越像,隐私保护越强。
落地时用拉普拉斯机制:对查询结果加上服从Laplace分布的随机噪声,噪声尺度λ = Δf / ε。这里Δf是敏感度,表示为“修改一条数据时,查询结果最大变化多少”。评分为1到5分的场景,一个用户把某条评分从1改成5,变化最大值就是4,所以Δf通常取4。
| 隐私预算 ε | 敏感度 Δf | 噪声尺度 λ = Δf/ε | 保护强度 |
|---|---|---|---|
| 0.1 | 4 | 40 | 很强,但结果几乎不可用 |
| 1.0 | 4 | 4 | 中等,噪声可见但可接受 |
| 5.0 | 4 | 0.8 | 较弱,接近原始结果 |
ε和Δf是写论文时一定要解释清楚的两个数字。很多毕业论文只写“加了一点噪声”,既不定义敏感度也不说明ε取值,评委一问就露馅。建议在方法章节直接用表格列出不同ε下的噪声尺度,让保密性论证落在公式上而不是形容词上。
3.2 噪声注入的三个位置:输入扰动、目标扰动与输出扰动
差分隐私噪声不是随便往哪加都行,位置不同,保护的对象和保护效果完全不同。常见做法有三种:输入扰动、目标扰动和输出扰动。
输入扰动最简单,直接给原始评分矩阵加噪声,然后再跑协同过滤。优点是实现快,但问题是噪声污染了训练数据本身,评分值域被打乱,推荐质量的损失通常最大。目标扰动用在矩阵分解的SGD过程中,对每轮梯度加噪声,保护的是训练过程,但实现复杂,还要在每轮之间分配隐私预算。输出扰动是对最终的预测分数或推荐列表加噪声,只影响输出,不碰原始数据,保护的是“查询结果”,工程上最干净。
| 注入位置 | 实现方式 | 质量损失 | 实现复杂度 |
|---|---|---|---|
| 输入扰动 | 对评分矩阵加拉普拉斯噪声 | 大 | 低 |
| 目标扰动 | 对每轮SGD梯度加噪声 | 中 | 高 |
| 输出扰动 | 对预测分数或TopN结果加噪声 | 小 | 低 |
我一般会建议把输入扰动和输出扰动都做出来,形成对照实验。一个证明“在源头加噪声保护最彻底但代价大”,一个证明“在输出端加噪声用最小损失换到了可用保护”。两组实验放在一起,论文的实验章节立刻有了纵深。
3.3 隐私预算怎么分配:组合定理与ε取值对照
单独一次查询用ε就够了,但推荐系统不可能只查询一次。每次TopN推荐都要读取多个物品的预测分数,这时需要用组合定理核算总隐私损失。简单组合下,执行T次查询,总预算就是T乘以单次ε,线性累加。更精细的做法是使用高级组合定理,总损失近似为Tε加上一个与T的平方根相关的项。
def simple_composition(per_query_eps, query_count): """简单组合:多次查询的隐私损失线性叠加""" return per_query_eps * query_count def advanced_composition(per_query_eps, query_count, delta=1e-5): """高级组合定理,适合论文里展示理论深度""" return (per_query_eps * query_count + per_query_eps * np.sqrt(query_count * np.log(1 / delta)))这两个函数在实验里可以直接用,记录每次推荐调用消耗的ε,最后累加出总预算。答辩时只要拿出这个累计表,就能解释系统运行多轮后还剩多少隐私预算,而不是含糊地说“我们加了噪声所以安全”。
ε怎么选也很有讲究。网上很多方案一上来就设ε=0.1,展示“我们隐私保护极强”,但0.1对应的噪声尺度是40,预测分数全被打乱,推荐结果接近随机,又反过来掩盖了系统本身的能力。正确做法是让ε从0.1到5.0之间取五档,画出一条推荐质量随ε变化的曲线,证明系统在可接受的质量损失范围内达到了可量化的隐私保护。
4. 用Python跑通带差分隐私的UserCF:MovieLens上的最小实战
4.1 环境准备:Python安装、虚拟环境与依赖库一次到位
开始写代码前先把环境定下来。网上python安装教程大多推荐直接装Anaconda,但毕设项目我更建议用虚拟环境,避免后面装包把系统Python搞乱。Windows和Linux都适用以下流程:
cd your_project_dir python -m venv venv source venv/bin/activate # Windows下改为 venv\Scripts\activate pip install numpy pandas scipy scikit-learn matplotlib依赖库只需要这四个:numpy做矩阵运算,pandas读评分数据,scipy处理稀疏矩阵和统计函数,matplotlib最后画隐私预算曲线。scikit-learn在这里只用来做数据切分和评估指标计算,它本身没有现成的协同过滤推荐器,所以主流程自己写。如果你用PyCharm,记得把解释器指到venv目录,后续跑脚本不会出现“找不到numpy”的玄学问题。
4.2 数据读入:从u.data到稀疏评分矩阵
MovieLens 100K是毕设最常用的数据集,文件解压后有u.data和u.item等文件。u.data每行是“用户ID、物品ID、评分、时间戳”,用tab分隔;新版数据集的ratings.csv用逗号分隔。读进来后要构建用户-物品评分矩阵,但用户ID和物品ID不是连续的,需要先做编号映射。
import pandas as pd import numpy as np # MovieLens 100K 的 u.data 是 tab 分隔,无表头 ratings = pd.read_csv('u.data', sep='\t', names=['user_id', 'item_id', 'rating', 'timestamp']) # 最新版数据集是 ratings.csv,逗号分隔,列名不同,改成 sep=',' 即可 print(ratings.head()) print('用户数:', ratings['user_id'].nunique(), '物品数:', ratings['item_id'].nunique())dataframe读好之后构建评分矩阵。最稳定的做法是pandas的crosstab,它会自动处理ID不连续的问题:
user_cat = ratings['user_id'].astype('category') item_cat = ratings['item_id'].astype('category') matrix = pd.crosstab(user_cat, item_cat, values=ratings['rating'], aggfunc='mean').fillna(0).values print('评分矩阵形状:', matrix.shape)注意这里用均值填充是图省事。真实处理时,0代表未评分,后面计算相似度和预测函数时要掩码掉这些位置,否则0分的物品会被当成“打0分”参与相似度计算,结果完全失真。如果数据量到百万条,改用它构建scipy的稀疏矩阵,稠密二维数组会直接把内存撑爆。
4.3 UserCF主体:自己写相似度与预测函数
相似度选择上推荐皮尔逊相关系数,它能消除用户打分尺度的差异。有的用户打分整体偏高,有的整体偏低,皮尔逊先做中心化再算相关性,比余弦更稳。但要注意,两个用户共同评分的物品太少时,相关系数不可靠,所以代码里对共同评分数量做了阈值过滤。
def pearson(a, b): """皮尔逊相关系数,只计算两个用户都评过分的物品""" mask = (a > 0) & (b > 0) if mask.sum() < 3: # 共同评分太少的用户对,直接放弃 return np.nan x, y = a[mask], b[mask] if x.std() == 0 or y.std() == 0: # 评分完全一致时相关系数无定义 return np.nan return np.corrcoef(x, y)[0, 1] def recommend(matrix, user, k=10, top_n=10): """对目标用户生成TopN推荐,过滤掉已评分物品""" preds = np.array([predict_rating(matrix, user, item, pearson, k) for item in range(matrix.shape[1])]) ranked = np.argsort(-preds) # 按预测分数降序 return [item for item in ranked if matrix[user, item] == 0][:top_n]recommend函数里的top_n是最终展示给用户的推荐数量,k是近邻数量。这两个参数一个控制输出列表长度,一个控制预测质量。实验时可以把k验证一遍,典型取值5到20之间;k太小,近邻噪声大;k太大,相似度低的用户也进邻居集合,预测被稀释。答辩时把调参过程作为一小节实验写进去,比只贴最终结果更有说服力。
4.4 注入拉普拉斯噪声:输入扰动与输出扰动两套代码
差分隐私的核心函数不长,但位置敏感。先在两个位置分别实现加噪:输入扰动对已有评分打乱,输出扰动只对预测分数打乱。这里的敏感度Δf采用评分值域上限减下限,即5减1等于4。
def laplace_noise(scale): """生成拉普拉斯噪声,scale = Δf / ε""" return np.random.laplace(0, scale) def perturb_input(matrix, eps, delta_f=4): """输入扰动:只对已有评分的位置加噪声""" noisy = matrix.copy() mask = noisy > 0 noise = np.random.laplace(0, delta_f / eps, size=noisy.shape) noisy[mask] = noisy[mask] + noise[mask] return noisy def perturb_output(pred_scores, eps, delta_f=4): """输出扰动:对预测分数向量加噪声""" noise = laplace_noise(delta_f / eps) return pred_scores + noise输入扰动代码里的关键点是mask = noisy > 0,只扰动真实评分,不碰未评分位置。很多人图省事对整个矩阵加噪声,未评分位置变成了带噪声的虚假评分,后续相似度和预测全部翻车。输出扰动则不用碰原矩阵,只在recommend函数计算出预测分数后调用,加完噪声再排序取TopN。
参数层面,delta_f=4只在评分区间为1到5时成立。如果你的数据是0到5分,敏感度要改为5;如果评分范围是1到10,敏感度是9。把它写死是毕设里最常见的技术债,实验前一定先确认数据集的评分范围。
4.5 评估闭环:RMSE与召回率@K
推荐系统没有评估就是自嗨。评分预测场景看RMSE,衡量预测分数与真实分数的偏差;TopN推荐场景看召回率@K,衡量推荐列表里有多少真实偏好的物品被命中。两者都要和隐私保护强度一起报告。
from sklearn.model_selection import train_test_split train, test = train_test_split(ratings, test_size=0.2, random_state=42) train_matrix = pd.crosstab( train['user_id'].astype('category'), train['item_id'].astype('category'), values=train['rating'], aggfunc='mean' ).fillna(0).values def rmse(pred_scores, true_scores): """预测评分的均方根误差""" return np.sqrt(np.mean((np.array(pred_scores) - np.array(true_scores)) ** 2)) truths = test['rating'].values preds = [] for _, row in test.iterrows(): u, i = row['user_id'], row['item_id'] preds.append(predict_rating(train_matrix, u, i, pearson, k=10)) print('RMSE =', rmse(preds, truths))评估这一步的常见偏差是直接用全量数据训练再评估,结果虚高。正确做法是train_test_split先切出20%做测试集,测试集里的评分在训练矩阵中不可见,预测函数只能依赖训练集里其他用户的评分来推断。把RMSE分别算给裸UserCF、输入扰动、输出扰动三个版本,就能定量说出差分隐私让精度损失了多少。
5. 带DP的推荐系统避坑记录:答辩前最该检查的四个地方
5.1 加了噪声后预测分数出现大量NaN或负数
用拉普拉斯机制加噪后,预测分数偶尔出现负值,严重时直接产生NaN,推荐列表一批一批地空掉。原因有两个:一是输入扰动时对全矩阵加噪声,把未评分位置的0变成非0,相似度计算又把它们当成有效评分,整个预测链被污染;二是拉普拉斯分布本身有厚尾,极端噪声值会让分数超出正常评分区间。
解决:输入扰动必须先掩码,只对真实评分位置加噪声;输出扰动后对预测分数做clip,把它限制在1到5之间。clip不是篡改数据,而是保证噪声不会把推荐结果打到无意义范围,实验里这让RMSE下降非常明显。
5.2 ε设成1但隐私保护效果几乎为零
有的同学跑完实验发现ε=1时推荐结果和原始系统几乎一样,这不是差分隐私失效,而是隐私预算根本没有被正确核算。推荐系统每次运行要查询所有物品的预测分数,这相当于执行了物品数量次查询,如果每查询一次都独立发放噪声,实际总隐私损失就是单次ε乘查询次数,早就不是一个数量级了。
解决:在代码里维护一个隐私预算计数器,每次查询结束后累加调用simple_composition函数计算累计损失。实验报告里把“单次查询ε、查询次数、总隐私预算”三列列出来,既能自证安全,答辩时也拿得出完整推导链。
5.3 同一份数据两次实验结果完全不可复现
差分隐私噪声本质是随机数,但如果连裸UserCF的结果都前后不一致,那就是随机种子没固定,根本到不了噪声这一步。Python和numpy重复运行时,如果不播种,每次生成的数据切分、近邻选择都不一样,实验结果自然对不上。
解决:在脚本开头固定全套随机源:import random; random.seed(42); np.random.seed(42)。如果用了PyTorch做矩阵分解对照实验,还要固定它的torch.manual_seed。差分隐私实验建议把噪声种子单独管理,每个ε档位存一份种子记录,这样后续复现时知道是哪一档噪声序列。
5.4 加了差分隐私后精度全面下降,无法向导师交代
这几乎是每个做DP推荐系统的人都会撞上的墙。输出扰动后的RMSE比原始UserCF高一大截,召回率也掉,很多同学就慌了,想删掉隐私模块。这不是算法选错了,而是没有把隐私预算画成曲线。
解决:至少跑五档ε,从0.1到5.0,横轴是ε,纵轴是RMSE或召回率,把裸模型作为水平参考线画在一张图里。结果通常是一条单调曲线:ε小,噪声大,精度损失大;ε增大,精度逐步恢复到接近原始模型。把这组图放进毕设,直观展示了隐私和效用之间的权衡,评委立刻知道你有完整的实验意识。
6. 把实验设计做成毕设加分项:隐私预算与推荐质量的对照实验技巧
实验章节的常见问题是只报一组最终指标。换成对照实验结构,整章的论证强度完全不同。我习惯做一个三模型对比:裸UserCF作为基线、输入扰动DP-UserCF、输出扰动DP-UserCF,让ε走0.1、0.5、1.0、2.0、5.0五档,每组固定随机种子并记录噪声实例。
| 模型 | ε=0.1 | ε=0.5 | ε=1.0 | ε=2.0 | ε=5.0 |
|---|---|---|---|---|---|
| 裸UserCF | 基线 | 基线 | 基线 | 基线 | 基线 |
| 输入扰动DP | RMSE最高 | 下降 | 下降 | 接近基线 | 接近基线 |
| 输出扰动DP | RMSE较高 | 优于输入扰动 | 接近可用 | 几乎无感 | 几乎无感 |
这张表配合一张折线图,答辩时能直接回答“噪声到底让推荐变差了多少”的问题。还能接着分析:输出扰动为什么优于输入扰动,因为噪声作用在最终分数上,没有污染原始的用户相似度结构,所以同样隐私预算下代价更小。
进阶技巧里有一个很划算的做法:输出扰动时不对每个预测分数单独加噪,而是对TopN候选列表的整体打分加一次噪声,再重新排序。这样噪声对排序顺序的影响更温和,推荐列表的稳定性明显提升,隐私保护依然成立,但用户体验层面的“推荐突然换了个人”的概率低很多。这个小技巧不需要改任何理论框架,只改加噪粒度,值得写进实验讨论小节。
做完整组实验后,我习惯把所有结果导出一张csv,包括ε、随机种子、RMSE、召回率和噪声均值,文件名带日期。这份记录在答辩时救过我一次:评委追问“为什么ε=2时召回率先降后升”,我直接翻出对应种子下的那一档噪声分布,看到那次跑批恰好生成了几个极端大噪声,被噪声支配了排序。不是模型bug,是随机波动,但如果没有记录,这顶帽子就摘不掉了。
差分隐私推荐系统的难点从来不是公式和代码,而是把隐私预算、噪声尺度、推荐质量这三者之间的关系讲成一条可验证的曲线。把上面这套代码跑通,再顶住“你的噪声是不是摆设”的追问,这个毕业设计就成了。希望帮到你。
本文还有配套的精品资源,点击获取