简介:面向计算机相关专业学生与推荐系统入门研究者的毕业设计资源包,基于Python实现带差分隐私的协同过滤推荐系统,聚焦推荐流程中的用户隐私保护。从差分隐私与协同过滤的理论背景入手,梳理国内外研究现状,并阐述差分隐私相对传统安全模型的优势。随后详解基于用户的协同过滤算法步骤,设计并实现了采用两种相似度计算方式、对推荐结果进行差分隐私加密的完整系统,并在MovieLens两个不同规模数据集上实验比对,探索推荐准确度与隐私保护之间的平衡。包内共77个文件,含60张实验效果图、12个Python源码、2个Word论文文档及配置说明,整体约3.37MB,结构清晰便于查阅。已有922人浏览学习,适合需要完整毕业设计参考,或想复现实验、深入理解隐私保护推荐系统的读者。
1. 带差分隐私的协同过滤推荐:这份Python毕设资源能解决什么
推荐系统每天都在把用户的评分、浏览记录变成推荐结果,但这些原始数据一旦泄露,用户偏好、消费习惯会被直接摊开。传统加密方案不适合这类场景,因为协同过滤的相似度计算要求评分以明文参与运算;差分隐私是更现实的解法:在计算结果上加入受控噪声,让外部无法反推单个用户,同时推荐质量不塌。这份资源就是一套完整的Python实现——基于用户的协同过滤配合差分隐私(拉普拉斯机制),带毕业设计论文稿、完整代码、约六十张实验截图和两个不同规模的MovieLens数据集实验记录。适合正在做毕设或课设的人,也适合想验证隐私预算取值边界的从业者。新手能照着步骤跑通,熟手能直接拿调参路径做对照实验。
2. 拆开资源包:协同过滤选型与差分隐私原理先立住
2.1 解压之后先认清目录结构:哪些文件对应哪个环节
拿到压缩包先别急着跑代码,把目录认一遍。资源里的data目录放着ml-latest-small.zip,这是MovieLens的十万级评分数据集,包含userId、movieId、rating和timestamp四个字段,是后面所有实验的输入;code目录下区分了「中期」和后期测试代码,中期是开发过程中留档的过渡版本,测试用的那部分才是论文数据对应的实现;img目录里那一批编号-哈希命名的png是跑实验时自动导出的结果图,论文稿里的误差曲线和对比柱状图基本都能在这里找到对应版本;论文5.5稿.docx是成稿,格式模版_demo.docx是提交毕业设计时的格式模板。
文件比较多,我按使用顺序排了个清单:
| 路径 | 作用 | 什么时候用 |
|---|---|---|
| README.md | 环境说明、运行顺序 | 第一步,先读 |
| data / ml-latest-small.zip | 小规模实验数据集 | 跑通流程用 |
| code / 中期 | 开发过程中的过渡版本 | 理解算法演进 |
| code / 测试 | 最终实验代码 | 复现论文结果 |
| 论文5.5稿.docx | 完整论文正文 | 对照图表和数值 |
| img / *.png | 实验截图 | 复现后做比对 |
| LICENSE | 授权说明 | 商用前确认条款 |
建议的动手顺序是:先读README确认python环境要求,再解压ml-latest-small.zip跑通一个小实验,最后用测试代码复现论文里的一张表或一张图。img里那些png文件名是自动生成的,不是人为整理的,所以别在里面找「最终版」,要看文件序号和论文里的图表位置对应起来。
2.2 基于用户的协同过滤为什么是这份资源的合理起点
推荐系统可以按推荐逻辑分成好几类:基于内容的推荐需要额外解析物品属性,矩阵分解类方法训练成本高、调参链路长;基于用户的协同过滤只需要评分矩阵,逻辑直接,适合作为隐私保护研究的基础载体。它的流程可以压缩成三步:收集用户偏好、找到相似用户、按相似度加权计算推荐。这份资源在相似度环节给了两种算法——余弦相似度和皮尔逊相关系数,这也是User-CF最经典的两个变体。
余弦相似度看的是两个用户评分向量的方向夹角,只关心评分模式是否一致,不关心整体打分高低。皮尔逊相关系数会先给每个用户减掉自身平均评分,再算夹角,等于把「有的人习惯给高分、有的人习惯给低分」这个干扰去除掉。落实到代码里,两者的选择会影响后面Top-N推荐的质量,尤其是评分尺度差异明显的用户群体。这张表是我拆包时整理出的选型依据:
| 相似度方法 | 核心思路 | 适合情况 | 常见边界问题 |
|---|---|---|---|
| 余弦相似度 | 评分向量夹角 | 大家评分尺度接近 | 零向量会触发除零 |
| 皮尔逊系数 | 去均值后算夹角 | 用户评分尺度差异大 | 评分无波动的用户方差为0 |
选型上我的建议是:数据量大、追求先看到稳定结果,先用余弦;想清晰体现隐私保护前后推荐质量不塌,皮尔逊更容易看出噪声对相似度排序的影响。因为一旦对预测评分加噪,皮尔逊那边因为去均值,噪声的扰动会被部分抵消,曲线变化更平滑,写进论文里解释起来也更顺。
2.3 差分隐私的三个参数:epsilon、敏感度与噪声尺度
差分隐私不是一种具体算法,而是一个保证:对数据集增删任意一条记录,查询结果的概率分布变化被限制在一个可控范围内。这套体系里最常用的实现机制是拉普拉斯机制,给查询结果加上服从拉普拉斯分布的随机噪声,噪声的尺度由两个量决定——全局敏感度Δf和隐私预算epsilon。噪声尺度Scale = Δf / epsilon,这三个参数构成了整份资源实验部分的核心。
epsilon越小,噪声越大,隐私保护越强,但推荐结果的可用性会断崖式下降;epsilon越大,推荐质量越接近无噪声版本,但隐私保护名存实亡。那批截图里最核心的图,就是横轴epsilon、纵轴误差的权衡曲线。敏感度Δf的取值是另一个关键,它取决于你往哪个环节加噪声:如果对预测评分加噪,敏感度上界就是评分区间的差值;如果对相似度加噪,敏感度会跟用户维度挂钩,噪声尺度会显著变大。
提示:课程设计常见做法是往最终预测评分上加拉普拉斯噪声,而不是对相似度矩阵加噪。原因很简单,预测评分的敏感度是固定值,参数好交代,论文里的对照实验也好设计。
第3章我会把这套逻辑落成具体代码。
3. 从评分矩阵到加噪推荐:核心代码的完整落地链路
3.1 载入MovieLens评分数据并构建用户-物品矩阵
数据侧先把ml-latest-small.zip解压,里面ratings.csv的四列分别是userId、movieId、rating、timestamp。第一步是用pandas读进来,再用pivot_table把它转成用户为行、物品为列的矩阵,这个矩阵是后面相似度计算的输入基础。
import pandas as pd import numpy as np # 读入评分数据,四列:用户ID、电影ID、评分、时间戳 ratings = pd.read_csv('data/ml-latest-small/ratings.csv') print(ratings.head()) # 构建用户-物品评分矩阵:行 = 用户,列 = 电影 rating_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0) # 未评分位置填0,参与后续矩阵运算 print(f'用户数: {rating_matrix.shape[0]}, 电影数: {rating_matrix.shape[1]}')pivot_table里index和columns分别固定矩阵的行列,values指明取评分数值,fillna(0)把未评分的位置补成0。这里有个尺度问题:填0会让未评分和打了0分混淆,但MovieLens评分范围是1到5,不存在真实0分,所以0在矩阵里只是「未评分」的占位。常用做法是先这样填零跑通流程,后续如果发现相似度矩阵异常,再考虑只对共同评分项单独计算。
3.2 两种相似度计算与最近邻选择的参数含义
有了评分矩阵,接着算用户相似度。scikit-learn的cosine_similarity和numpy的corrcoef分别对应余弦和皮尔逊两种口径,直接对矩阵调用即可:
from sklearn.metrics.pairwise import cosine_similarity # 方法一:余弦相似度,默认按行计算,行正好是用户 user_sim_cos = cosine_similarity(rating_matrix) # 方法二:皮尔逊相关系数,corrcoef同样按行计算 user_sim_pearson = np.corrcoef(rating_matrix) # 统一处理:对角线上的自身相似度强制置0 np.fill_diagonal(user_sim_cos, 0) np.fill_diagonal(user_sim_pearson, 0)cosine_similarity默认按行计算,这里的行正好是用户;np.corrcoef同样按行处理,参数直接传矩阵即可。fill_diagonal是容易被忽略的一步,如果不把对角线的1清零,最近邻选择时最相似的用户永远是用户自己,预测结果会严重偏向用户已有评分。
选邻居时有个核心参数k,代表取前多少个相似用户参与加权。推荐系统的常规范围是10到50,k太小噪声影响大,k太大把相似度很低的用户也拉进来,预测分数被稀释。这份资源里的k需要自己先跑一遍粗搜索确定,第4章实验设计部分会讲具体做法。
3.3 预测评分、Top-N推荐与差分隐私加噪
预测评分的核心逻辑:对目标用户u和目标电影m,取u最相似的k个用户,找到这些人对m的评分,用相似度做加权平均。下面的函数把整个链路串起来:
def predict_rating(user_pos, movie_col, sim_matrix, rating_matrix, k=20): # 取前k个相似用户,argsort返回索引,[::-1]转成降序 top_k_idx = sim_matrix[user_pos].argsort()[::-1][:k] sims = sim_matrix[user_pos][top_k_idx] ratings_vec = rating_matrix.iloc[top_k_idx, movie_col].values # 当前矩阵里0是未评分占位,负相似度用户会拉偏预测,都要过滤 mask = (ratings_vec > 0) & (sims > 0) if mask.sum() < 2: # 有效邻居太少,退回用户平均分兜底 user_rated = rating_matrix.iloc[user_pos] return user_rated[user_rated > 0].mean() if (user_rated > 0).sum() else 3.0 numerator = (sims[mask] * ratings_vec[mask]).sum() denominator = sims[mask].sum() return numerator / denominatormovie_col这里传的是矩阵的列位置而不是movieId,调用前需要先用列索引映射一下。mask同时过滤掉零值评分和负相似度邻居,是因为负相关的用户评分方向相反,加权进来会把预测值往错误方向拉。兜底逻辑返回用户自身平均分或3.0,对应冷启动场景的常见处理。
差分隐私的加噪放在预测评分之后,加的是拉普拉斯噪声:
def laplace_noise(sensitivity, epsilon): """生成拉普拉斯噪声,sensitivity为敏感度,epsilon为隐私预算""" scale = sensitivity / epsilon return np.random.laplace(0, scale) # epsilon先给个初始值,后面用实验确定 epsilon = 2.0 noisy_score = predict_rating(u_pos, m_col, user_sim, rating_matrix, k=20) noisy_score += laplace_noise(sensitivity=1.0, epsilon=epsilon) noisy_score = np.clip(noisy_score, 1, 5) # 评分必须在1-5区间内敏感度取1.0的前提是预测函数做了相似度归一化,每个用户的评分对结果的边际影响被限制在1以内;如果你直接求平均分,敏感度上界要按评分区间差值的1/k来重新算。clip到最后很重要,拉普拉斯噪声是厚尾分布,偶尔会生成特别大的值,不截断的话一个离群点就能把RMSE拉爆。
注意:加噪这一步必须放在整个预测函数跑完后,不能对相似度矩阵加噪再算预测。两种做法在论文里都能解释,但后者敏感度和用户规模成正比,噪声会大到推荐结果不可用。论文稿里把加噪过程表述为「加密」,本质就是往预测结果上注入受控随机噪声。
4. 隐私预算怎么调:两个数据集上的对照实验设计
4.1 数据规模差异带来的实验策略差异
摘要里明确写了要用两个不同规模的数据集做对照,原因在于差分隐私的噪声不会随数据量自动缩小,但数据量大时单条记录的影响力会被稀释,同样epsilon下噪声对推荐质量的破坏更小。ml-latest-small是十万级评分的小数据集,适合跑通链路和快速调参;更大规模的那份数据用于验证一个大结论——隐私保护成本是否随数据量增长而下降。
实验前先做数据划分,不要用随机切分,推荐系统的评估最好按时间切分:ratings.csv按timestamp排序,前80%做训练集,后20%做测试集。原因是推荐系统面对的是未来的评分,随机切分等于让模型提前看到了「未来」的痕迹,误差会被低估,论文审阅时容易被抓到破绽。切分完还要检查一遍训练集和测试集的用户重叠情况,如果测试集里出现一批训练集完全没有的新用户,说明你的切分边界切得太晚,这批用户只会走兜底分支,测试结果里混进了大量噪声。
4.2 epsilon搜索路径与评估指标口径
评估指标上,MAE和RMSE用来衡量预测误差,Precision@N和Recall@N用来衡量推荐列表的命中质量。做隐私预算实验时,RMSE是最直观的指标,因为噪声直接作用在预测分数上。隐私预算的搜索路径我一般分两轮:先粗扫确定量级,再细扫确定论文要用的推荐值。粗扫网格建议这样设:
| epsilon | 实验目的 |
|---|---|
| 0.1 / 1 / 10 | 看趋势,确认误差随epsilon增大的下降速度 |
| 0.5 / 2 / 4 | 细扫,定位误差曲线从陡峭变平缓的拐点 |
| 16 / 32 | 上界验证,确认接近无噪声水平 |
每个epsilon建议固定随机种子跑3到5次取均值。拉普拉斯噪声是随机变量,单次实验的RMSE波动在0.2左右都算正常,不取均值画出来的曲线是锯齿状的,写论文时根本没有说服力。粗扫跑完,看RMSE曲线在哪一段开始走平,论文里的「隐私保护与推荐质量平衡点」就选在那一带。
调参还有一个容易被忽略的维度:k值。k的选取会影响预测函数对噪声的敏感度,k太小预测方差大,加噪后误差更不稳定;k太大预测被稀释,加噪后误差变化不明显。我一般先把epsilon设成一个中间值如4,跑k=10/20/30/40四组,看哪组baseline的RMSE最低,再用这个k做整个epsilon网格实验,这样能把两个变量的影响分开交代。
4.3 用matplotlib画出权衡曲线并和论文截图对照
曲线图是所有截图里最有信息量的一张,横轴用对数刻度,纵轴是RMSE,把两个数据集的曲线画在同一张图里做对比。python 3.8以上的环境里,matplotlib的处理方式如下:
import matplotlib.pyplot as plt # 中文字体,避免图例出现乱码方块 plt.rcParams['font.sans-serif'] = ['SimHei'] fig, ax = plt.subplots(figsize=(8, 5)) # eps_list 是粗扫+细扫的epsilon列表,rmse_small/rmse_full 是多次实验的均值 ax.plot(eps_list, rmse_small, marker='o', label='ml-latest-small') ax.plot(eps_list, rmse_full, marker='s', label='larger dataset') ax.set_xscale('log') # epsilon跨了两个数量级,线性轴会把小值挤成一团 ax.set_xlabel('epsilon(隐私预算)') ax.set_ylabel('RMSE') ax.legend() ax.grid(alpha=0.3) plt.savefig('epsilon_rmse_curve.png', dpi=300, bbox_inches='tight')x轴用log刻度是最关键的细节,epsilon从0.1到32跨越了三个数量级,线性轴会让小数值全部挤在左侧。dpi=300是为了直接放进论文不糊图,bbox_inches='tight'是防止中文标签被裁掉。跑出来的曲线再和img目录里的截图做数量级对比——趋势一致说明实现正确,如果形状差异很大,优先检查随机种子和敏感度是不是没对齐。在Linux服务器上没有SimHei字体时,把图例标签改成英文或者先安装中文字体,否则plt会一路报警告。
5. 避坑记录:协同过滤加差分隐私的五个实战坑
这一章是拆包过程里最值钱的部分,五个坑我都实际踩过,按现象、原因、解决的顺序说清楚。
5.1 余弦相似度矩阵出现NaN,推荐结果全部退回热门兜底
现象:cosine_similarity跑完后矩阵里一堆NaN,predict_rating里mask.sum()经常小于2,最后推荐的全是评分数量最多的热门电影,个性化完全失效。
原因:fillna(0)之后,如果某个用户或某部电影在训练集里压根没有评分记录,它的向量就是全零向量,余弦相似度计算对零向量做除法,直接产生NaN。
解决:构造矩阵之后先过滤,去掉评分数少于5的用户和电影;相似度算完再统一做一次清理,np.nan_to_num(user_sim, nan=0.0)把NaN清成0,同时fill_diagonal把自身相似度归零。两步做完,相似度矩阵才是干净的。这个检查要固定在每次构建矩阵之后,换数据集时最容易漏。
5.2 小epsilon时RMSE悬崖式暴涨:敏感度设大了
现象:epsilon降到0.5以下时RMSE不是缓升而是暴涨,曲线在0.1到1之间像悬崖一样,论文表格里这块数据完全没法用。
原因:噪声尺度是sensitivity/epsilon,epsilon变小会让噪声急剧放大;另一个隐藏原因是敏感度设错,把Δf直接按评分区间差值4来算,噪声比实际需要的大了四倍。敏感度和你的预测函数绑定,不是抄公式就能定的。
解决:先按预测函数的真实上界重算敏感度,加权平均的预测函数用Δf=1比较合理;epsilon的搜索下限从0.5开始而不是0.1,先跑一轮粗扫观察趋势再决定要不要往小值方向加密。如果你确实需要展示小epsilon下的结果,至少要把敏感度调小后重跑,否则那条悬崖曲线是敏感度错误导致的,不是隐私保护的正常代价。
5.3 两次实验数值对不上:随机种子和训练集划分没固定
现象:同一份代码今天跑RMSE是0.87,明天变成0.79,论文里的对比表填不下去,甚至自己都怀疑代码有bug。
原因:实验有两个随机源,一个是数据集划分时的shuffle,另一个是拉普拉斯噪声本身。两个都不固定,结果自然每次都不一样。
解决:代码开头固定np.random.seed(42),数据切分用random_state参数或按timestamp排序后取前80%;拉普拉斯噪声生成前重新set一次seed,多跑几次取均值后再画曲线。要跟评审解释清楚,每次trial的原始输出文件都留着,而不是只留一个均值。这份资源里img目录那批重复编号的图片,其实就是不同次实验留下的痕迹,别忽视它们。
5.4 皮尔逊系数在评分无波动的用户上报除零
现象:用np.corrcoef算皮尔逊相似度时,Warning提示invalid value encountered in true_divide,个别用户列的相似度全是NaN。
原因:皮尔逊系数分母是两个用户评分向量的标准差乘积,如果某个用户对所有电影打的都是同一个分,方差为0,除法直接除零。
解决:相似度矩阵算完后把NaN统一处理成0,再配合用户评分数量过滤,评分种类小于2的用户直接剔除或兜底到用户均值。这类样本量很少,对整体推荐质量影响可以忽略,但让矩阵产生NaN会导致后续批量运算整段崩溃。检查时别只看聚合指标,把每行NaN数量打印一遍最稳。
5.5 用户ID当数组下标:loc和iloc混用导致错位
现象:预测结果和手算对不上,部分用户的推荐永远落在兜底分支,换一个用户ID顺序结果就乱了。
原因:pivot_table之后的行索引是userId本身,userId在MovieLens里不连续,直接用测试集里的userId当数组下标会错位,越界时pandas还会报IndexError。
解决:建立两层映射,user_pos = {uid: idx for idx, uid in enumerate(rating_matrix.index)},将userId映射为矩阵位置;所有查询统一走这个映射,矩阵取值用iloc,索引查询用loc,不混用。这个映射要写成一个独立函数,每次读数据都重建,别在实验中途手动维护。
6. 复现论文图表:从实验记录到一份能提交的结果
论文稿是5.5稿,说明前面经历了多轮迭代。要让它变成能提交的成果,关键是把img目录里那批哈希命名的截图和论文里的图表对上,再自己亲手复现一遍。哈希文件名是自动导出产生的,当时跑实验的代码在code/test里,论文里跟隐私预算相关的图,基本都能在img里找到对应编号。复现流程我习惯固定成四步:第一步固化环境,把pandas、numpy、scikit-learn、matplotlib的版本写进requirements.txt,防止环境漂移;第二步固定数据划分,按timestamp排序前80%做训练;第三步按epsilon网格循环跑,每次实验保存原始预测输出而不是只存RMSE;第四步把多次试验取均值后存成CSV,再生成曲线图。
# run_experiment 的骨架在 code/test 里,核心逻辑就是 # 训练集预测 + 拉普拉斯加噪 + RMSE计算,封装成函数便于循环 results = [] for eps in [0.5, 1, 2, 4, 8, 16]: for trial in range(5): rmse = run_experiment(eps, seed=42 + trial) results.append({'epsilon': eps, 'trial': trial, 'rmse': rmse}) results_df = pd.DataFrame(results) results_df.groupby('epsilon')['rmse'].mean().to_csv('exp_epsilon_rmse.csv')留原始输出的意义在于,论文审阅阶段被问到「这张图的数值波动是噪声引起的还是代码bug」,你能拿出5次trial的原始记录来回应,而不是重新跑一遍碰运气。从那以后我每做一个差分隐私对照实验,都强制自己走一遍这套流程:先写死种子、再固定数据划分、最后把每次trial的完整输出落盘。论文里的每一张图和表都有原始数据做支撑,复现和答辩都能站得住。希望帮到你。
本文还有配套的精品资源,点击获取