简介:一套基于协同过滤算法的音乐推荐系统毕设项目,面向计算机科学与软件工程等相关专业正在准备毕业设计的学生,也可供需要练手完整Web开发实战的学习者使用。系统以后端Python为核心,结合Vue前端,实现用户行为采集、相似度计算与个性化推荐,覆盖从数据表设计到交互展示的主要环节。压缩包共564个文件,约23.25MB,其中py文件为推荐与接口逻辑,vue与js构成前端页面,svg/jpg/png为界面素材,docx与sql则提供论文文档和数据库脚本,结构清晰便于对照学习。项目已经导师指导并认可,评审得分98分,源码均本地编译调试可运行,附有安装、运行、初始化数据库等批处理脚本和完整部署教程,可大幅缩短环境搭建时间。目前已有130人浏览学习,适合需要快速上手并完成高质量毕设或课程设计的同学。
1. 为什么做音乐推荐要选协同过滤:一个毕设题目的第一课
你在答辩时大概率会被问第一个问题:“别的推荐算法那么多,为什么用协同过滤?”如果回答“因为大家都在用”,基本就凉了一半。其实这个问题背后隐藏着一个更关键的判断:协同过滤是唯一一种“只需要用户行为、不需要理解音乐内容”的推荐思路。你不需要知道《加州旅馆》是摇滚还是民谣,不需要听懂和弦和采样率,只需要知道“听过这首歌的人,也爱听那首歌”。这让 Python 毕设项目里的协同过滤音乐推荐系统,成为算法门槛适中、数据可得性高、又能讲清楚设计理由的选题。源码、教程加论文的标准配套,也让它在毕设市场里长盛不衰。这个题适合两类人:想快速落地一个效果可见的推荐系统的初学者,以及需要把论文实验部分写得有说服力的毕业生。
2. 协同过滤的选型逻辑:UserCF、ItemCF 和音乐场景的匹配
2.1 音乐行为数据长什么样:user、item、行为强度和时间戳
不管标题里写的是“音乐推荐系统的设计与实现”,你的数据永远绕不开四个字段:用户 ID、歌曲 ID、行为强度、行为时间。这是所有协同过滤的原料,也是论文里数据预处理章节的主角。
常见的数据来源有两种。公开数据集方面,Last.fm 的用户听歌记录是首选,它的典型字段是 user_id、artist_id、play_count,这种三元组结构天生就是为协同过滤准备的;如果你的题目限定在“歌曲”而不是“歌手”,可以按歌曲粒度重新聚合。另一种是自己造数据,用 Python 爬虫抓某平台的歌单和收藏列表,配合时间戳生成 CSV。自己抓的劣势是数据量小、字段不全,但优势是论文可以写“使用真实用户行为数据验证”。
拿到原始数据后,第一步永远是清洗和统计。先看有多少用户、多少首歌、行为矩阵的稀疏度是多少。大多数毕设数据集的稀疏度都在 95% 以上,这是后面所有坑的源头,现在心里有数,后面调参就不会慌。我一般的处理是:
# 用 pandas 快速了解数据分布 python -c " import pandas as pd df = pd.read_csv('data/user_item.csv') print(df.shape) print(df['user_id'].nunique(), df['item_id'].nunique()) print('sparsity:', 1 - df.shape[0] / (df['user_id'].nunique() * df['item_id'].nunique())) "这段命令会告诉你矩阵有多稀疏。如果稀疏度超过 99%,后面计算相似度时一定会遇到大量零值,这是正常现象,不是代码写错了。如果用户数和歌曲数都在一万以内,内存完全不是问题,放心用 DataFrame 直接算。
2.2 UserCF 和 ItemCF 的选型对比:一张表说清区别
协同过滤分两大流派:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。UserCF 先找“和你听歌口味相似的人”,再把那些人听过而你没听过的歌推荐给你;ItemCF 则反过来,先看你听过哪些歌,再找到“和这些歌相似的歌”推荐给你。
| 对比维度 | UserCF | ItemCF |
|---|---|---|
| 核心假设 | 相似用户有相似偏好 | 相似物品会被同一用户消费 |
| 计算对象 | 用户相似度矩阵 | 物品相似度矩阵 |
| 实时性 | 用户新行为后要重新找邻居 | 物品相似度可离线算好,在线查表 |
| 可解释性 | “和你口味相似的张三也在听” | “因为你听过《XX》,推荐《XX》” |
| 冷启动倾向 | 新用户无行为时完全失效 | 新歌无行为时完全失效 |
| 典型适用场景 | 新闻、社交、短视频 | 电商、音乐、视频点播 |
这张表不是摆设,论文的“技术选型”章节可以直接扩展成表格加文字分析。你需要记住最核心的一条区别:UserCF 的用户相似度矩阵在用户量大的时候算不动,而且用户口味会变,今天和你相似的人明天就可能不爱听民谣了;ItemCF 的歌曲相似度相对稳定,《加州旅馆》和《Hotel California》的共现关系不会因为某个用户注销而改变。
音乐场景还有一个微妙的地方:用户听歌的“口味稳定期”很短,可能这周沉迷后摇,下周改听蒸汽波。如果你用 UserCF,一个用户的口味刚变化,系统还没反应过来,就已经在用旧邻居推荐了。ItemCF 按物品组织,只要你最新听过的几首歌能映射到相似物品,推荐就能跟上变化。
2.3 为什么音乐场景默认从 ItemCF 起步
我做这类项目时,基本默认先把 ItemCF 跑通,再考虑要不要对比 UserCF。原因不只是上面的实时性,还有工程实现上的差距。
ItemCF 的相似度矩阵是 item×item 的,维度取决于歌曲数量。一个典型的毕设数据集里有五千首歌,相似度矩阵就是五千乘五千,用 numpy 算一次只需要几百毫秒;而 UserCF 的用户相似度矩阵是三万用户乘三万用户,光存储就超过 7 GB,内存直接爆掉。如果你的项目只有几百个用户的数据,UserCF 也许能跑,但论文里写“系统支持海量用户”时就站不住脚。
另外,音乐推荐的解释性非常重要。ItemCF 的推荐理由可以轻易表述为“因为你听过 A,所以推荐相似的 B”,这种理由用户能看懂,论文里的案例展示也很好写。UserCF 的解释要绕一层“相似用户”,如果那个用户其实只是碰巧和你有几首重合,解释就很牵强。所以我的建议是:主模型用 ItemCF,UserCF 作为对照实验放在论文里,这说明你做了完整的方案对比,而不是只会一种算法。
3. 用 Pandas 实现 ItemCF:数据清洗、相似度矩阵和 TopN 推荐
3.1 环境准备与数据目录:pandas + numpy 就够了
很多新手在 python 安装教程和环境配置上先卡了一周,其实这个项目完全不需要深度学习框架,CPU 上跑 pandas 和 numpy 就够了。我一般用 VSCode 配好 python 解释器,再装两个依赖包就开写。
# 创建虚拟环境并安装依赖 python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install pandas numpy flask这里没有装 scikit-surprise,是因为这个库封装得太狠,你很难在论文里讲清楚每一步在算什么。用 pandas 手写,反而能把“构建共现矩阵”“计算相似度”“生成推荐”三个步骤讲得明明白白。Flask 是为了最后演示用,现在装好后面不用再折腾。
目录结构保持简单,论文里画系统架构图也方便:
music_reco/ ├── data/ │ └── user_item.csv # user_id, item_id, play_count, ts ├── src/ │ ├── data_loader.py # 数据读取与清洗 │ ├── item_cf.py # 相似度与推荐核心逻辑 │ └── evaluate.py # 离线评估 └── app.py # Flask 演示接口数据文件格式固定为四列:user_id、item_id、play_count、ts(Unix 时间戳)。如果你的原始数据没有时间戳,就按行号当时间用,评估时也能排序,只是精度差一点。
3.2 加载数据并构造隐式反馈权重:播放次数不是评分
音乐推荐里的行为数据绝大多数是隐式反馈:用户不会给每首歌打分,系统只知道他播放了多少次、是否收藏、是否跳过。播放次数是强度信号,但不能直接当评分用。原因很简单:一首歌被循环播放二十次,不代表用户对它的喜爱是只播一次的歌的二十倍,可能只是因为这首歌长、适合当背景音。
常用的做法是对播放次数取对数压缩:
import pandas as pd import numpy as np df = pd.read_csv("data/user_item.csv") df.columns = ["user_id", "item_id", "play_count", "ts"] # 清理掉异常数据:播放次数为 0 或负数是脏数据 df = df[df["play_count"] > 0].copy() # 关键一步:把播放次数压缩为隐式反馈权重 df["weight"] = 1 + np.log1p(df["play_count"])np.log1p是log(1 + x),作用是把播放次数从 1 到几百的区间压缩到 1 到 6 左右。这样《难忘今宵》被循环一百次,权重只是 5.6,而不是 100。权重后面会直接参与相似度计算和推荐打分,量纲不压缩的话,热门歌曲会把整个模型带偏。这里还有一个细节:log1p里加 1 是为了让播放次数为 0 的数据不会变成负无穷,虽然我们上面已经过滤掉了 0,但留着无害。
3.3 余弦相似度矩阵:一行矩阵乘法算出邻居表
ItemCF 的数学基础是物品向量的余弦相似度。把每个物品看成“用户维度的向量”,向量第 i 位是这个物品被用户 i 交互的权重。余弦相似度衡量两个向量方向上的重合程度,正好适合隐式反馈数据——用户不喜欢某个歌手,不会专门去“减一分”,而是表现为没有行为,余弦相似度天然把这种缺失当成零,不对零作惩罚。
from collections import defaultdict # 构建物品-用户矩阵:行是歌曲,列是用户,值是交互权重 pivot = pd.crosstab( df["item_id"], df["user_id"], values=df["weight"], aggfunc="sum" ).fillna(0) # 对每个物品向量做 L2 归一化 vec = pivot.values norm = np.sqrt((vec ** 2).sum(axis=1)) norm[norm == 0] = 1 # 防止除零 vec_norm = vec / norm[:, None] # 余弦相似度 = 归一化后的物品向量点积 sim_matrix = vec_norm @ vec_norm.T sim_df = pd.DataFrame(sim_matrix, index=pivot.index, columns=pivot.index)逻辑说明:pd.crosstab会把 (item, user, weight) 三元组展开成二维矩阵,缺失值填 0。余弦相似度在两两向量都做了 L2 归一化后,等价于一次矩阵乘法,这比显式循环每一个物品对要快一个数量级。sim_df的行和列都是歌曲 ID,sim_df.loc["A", "B"]就是 A、B 两首歌的相似度。
参数说明:这个方法的空间复杂度是 O(物品数²)。五千首歌时矩阵是 5000×5000,float64 约占 200 MB,完全能接受;但如果是五万首歌,矩阵就超过 20 GB,必须改成“只保留每个物品的 Top-K 邻居”的稀疏存储方式。毕设数据量通常到不了这个规模,但你在论文里要提一句“本实现适用于中小规模数据集,大规模场景需改用稀疏邻居表”,这是加分项,不是扣分项。
3.4 生成 TopN 候选:为什么必须截断候选集
相似度矩阵算好了,推荐就是查表加聚合。但这里有个新手最容易犯的错误:对用户听过的每一首歌取全部相似物品,然后合并排序。这样做的问题在于,相似度矩阵里大量接近零的微弱信号会混进来,把真正强的邻居淹没。而且计算量会随历史长度线性爆炸。我一般先取用户最近 20~50 首历史歌曲,每首歌只取相似度最高的前 50 个邻居,聚合成候选集合,再做排序。
def recommend(user_id, recent_k=20, neighbor_k=50, top_n=10): history = df[df["user_id"] == user_id].sort_values("ts") if history.empty: return popular_items[:top_n] # 冷启动兜底,后续章节详解 recent_items = history["item_id"].tail(recent_k).tolist() seen = set(history["item_id"]) scores = defaultdict(float) for item in recent_items: if item not in sim_df.index: continue # 用户对这首歌的交互强度 user_w = history[history["item_id"] == item]["weight"].iloc[0] # 相似度最高的 neighbor_k 个邻居 neighbors = sim_df[item].sort_values(ascending=False) neighbors = neighbors.drop(labels=[item])[:neighbor_k] for sim_item, s in neighbors.items(): if sim_item in seen: continue scores[sim_item] += s * user_w ranked = sorted(scores.items(), key=lambda p: p[1], reverse=True) return [item for item, _ in ranked[:top_n]]这段代码有三个参数值得反复调:recent_k是取用户多长的历史,取太短会丢失长期偏好,取太长会把早期兴趣也带进来;neighbor_k是每个物品的邻居数量,取太大会引入弱相似关系,取太小又可能漏掉强关联;top_n是最终返回的推荐条数,论文实验里通常固定为 10 或 20。注意drop(labels=[item])是把自己从邻居列表里去掉,否则相似度为 1 的自身会被推荐出来,这是新手最容易忽略的 bug。
推荐打分用的是“相似度 × 用户对历史物品的权重”累加。这不是唯一公式,也可以只累加相似度,或者用相似度的平方,但前者在大多数数据集上的表现更平稳。你把这一段逻辑写在论文里,评审是看得懂且挑不出毛病的。
4. 推荐效果怎么评估:留一法切分、三个必看指标和调参路径
4.1 评估要用时间切分:随机划分会让结果虚高
推荐系统的评估和普通分类任务不一样,它有一个隐含前提:不能用未来预测过去。如果你随机从数据里抽 20% 当测试集,那么用户 1 月 1 日听过的歌可能出现在训练集里,而他 1 月 2 日听的歌在测试集里——模型在训练时其实已经见过“下一首歌”的相似邻居了,评估结果会虚高到离谱。
正确做法是时间切分,也就是按用户把最后一次交互行为当作测试集,其余数据当训练集。这种“留一法”评估的是系统在真实世界里的表现:用户听完一首歌后,你推荐给他的一批歌里,有没有他实际会去听的下一首。
df_sorted = df.sort_values(["user_id", "ts"]) # 每个用户留下最后一次行为作为测试集 test_df = df_sorted.groupby("user_id").tail(1).copy() train_df = df_sorted.drop(test_df.index).copy() print(f"train: {train_df.shape[0]} rows, test: {test_df.shape[0]} rows")逻辑说明:groupby("user_id").tail(1)取每个用户时间戳最晚的一条记录。这样一个人只贡献一条测试样本,评估结果代表“用户的下一首歌能否被推荐出来”。如果你只有很少的用户,比如几十个,留一法的测试集太小,可以改成每个用户留最后 20% 的行为,但要保证这些行为在时间上都晚于训练集。
注意:切分前必须先按 user_id 和 ts 排序,否则
tail(1)取到的不是最晚的一条,而是 DataFrame 里的任意最后一条。
4.2 三个核心指标:Precision、Recall 和覆盖率
论文里最常出现的三个指标是 Precision@K、Recall@K 和覆盖率。它们回答的问题完全不同:推荐列表里有多少是用户真正听的;用户真正听的歌有多少被推荐出来了;系统是否只会推那几十首热门歌。
def evaluate(train_df, test_df, top_n=10): hits = 0 total_test = 0 rec_items_all = set() matched_all = set() for uid, group in test_df.groupby("user_id"): target = group["item_id"].iloc[0] recs = recommend(uid, top_n=top_n) if not recs: continue total_test += 1 rec_items_all.update(recs) if target in recs: hits += 1 matched_all.add(target) precision = hits / (len(test_df) * top_n) recall = hits / len(test_df) coverage = len(rec_items_all) / train_df["item_id"].nunique() return {"precision": precision, "recall": recall, "coverage": coverage}这里 Precision 和 Recall 的算法在“每个用户只有一条测试记录”的设定下做了一些简化:Precision 算的是所有推荐条目中命中的比例,分母是测试用户数乘 top_n;Recall 算的是多少用户命中了他们真正听的下一首歌。如果你的测试集每个用户有多条记录,就要改成标准的分子分母定义,论文里要写清楚,避免评审挑刺。
覆盖率的含义容易被忽略。一个只推荐周杰伦和 Taylor Swift 的系统,命中率可能还不错,但它对长尾歌曲完全不友好。覆盖率低说明推荐列表被头部歌曲垄断,这在音乐场景里几乎等于失败。你在调参时如果发现覆盖率不到 10%,就要警惕热门歌曲霸榜的问题,具体解法放在下一个章节。
4.3 调参顺序:先调邻居数,再调历史长度
ItemCF 调参有个不成立但实用的经验:先固定结果长度 K,再调 neighbor_k,最后调 recent_k,不要同时改两个参数。否则你根本说不清效果变好是因为哪个改动带来的。
我的调参路径一般是这样的,先写一个参数扫描脚本:
for neighbor_k in [10, 20, 50, 100]: for recent_k in [10, 20, 50]: # 用训练集重建模型,在测试集上算 recall@10 # 记录到结果表 print(f"recent_k={recent_k}, neighbor_k={neighbor_k}, recall={recall:.4f}")跑完一轮后,你会看到某个参数组合的 Recall 明显高于其他组合。常见的规律是:neighbor_k从 10 涨到 50 时 Recall 快速上升,再往上涨就开始下降或不涨;recent_k的影响相对平缓,因为它只影响用户历史窗口的大小,各用户的收听长度不一样,这个参数的作用会被平均掉。我建议先定recent_k=20,把主要精力放在neighbor_k上,找到最佳点后再微调recent_k。
论文里的实验表一般按这个格式做:
| 模型 | Precision@10 | Recall@10 | 覆盖率 |
|---|---|---|---|
| Popularity 基线 | 0.021 | 0.083 | 0.012 |
| UserCF | 0.032 | 0.117 | 0.074 |
| ItemCF | 0.038 | 0.139 | 0.096 |
Popularity 基线是必做的,它直接把全部用户上一个周期最热门的歌推给所有人。这个基线不需要任何算法,但能让你的模型对比有了“打赢的参照物”。如果你的 ItemCF 连 Popularity 都打不过,先别急着换算法,大概率是相似度矩阵里混入了太多低质量邻居,把neighbor_k调小再试。
5. 避坑指南:音乐推荐系统最容易翻车的 5 个环节
5.1 冷启动:新用户请求推荐时列表为空
现象:演示系统里新注册一个用户,调用推荐接口返回空列表,或者直接报 KeyError。老用户一切正常,新用户完全瘫痪。
原因:协同过滤完全依赖用户历史行为,一个没有任何收听记录的用户,在物品向量空间里是一个全零向量,相似度算出来都是零。另外,推荐函数里如果直接用df[df["user_id"] == uid],查不到记录时 history 为空,.tail(recent_k)返回空列表,于是 scores 为空,最终返回空列表。
解决:冷启动不是 ItemCF 能解决的,必须做兜底策略。最简单可靠的是热度推荐(Popularity),即全局播放权重最高的前 N 首歌:
popular_items = ( df.groupby("item_id")["weight"] .sum() .sort_values(ascending=False) .index .tolist() )然后在recommend函数里判断,如果用户没有历史行为,直接返回popular_items[:top_n]。这个兜底既能让系统演示不尴尬,也能在论文里写成“针对冷启动问题的缓解策略”。更进一步的做法是基于歌手和流派做内容匹配,但毕设做到热度兜底已经够了。
5.2 热门歌曲霸榜:覆盖率怎么都提不上去
现象:推荐列表里全是热门歌曲,覆盖率不到 10%,长尾歌曲一次都没被推荐过。评估指标里 Recall 还行,但展示页面一看就觉得“没什么推荐感”。
原因: ItemCF 在计算相似度时,热门歌曲和大量歌曲都有共现关系,因此它们天然成为很多歌曲的 Top 邻居。再加上用户历史里本来就大概率听过热门歌,热门歌被推荐的概率就更高。这是协同过滤的“马太效应”,不是 bug,却是推荐质量的大敌。
解决:一个常用技巧是对相似度做流行度惩罚,让热门歌曲在成为邻居时被打折。做法是在相似度矩阵上按歌曲的流行度降权:
# 每首歌被多少用户听过,作为流行度 item_pop = df.groupby("item_id")["user_id"].nunique() # 流行度惩罚因子:越热门的歌权重越低 scale = 1 / np.log1p(item_pop) sim_df = sim_df.multiply(scale, axis=0).multiply(scale, axis=1)np.log1p依然是为了压缩量纲,流行度一万和十万的差距在对数后变成 9 和 11,惩罚力度温和而有效。这个操作会让和热门歌曲的相似度整体下降,长尾歌曲的邻居排位相对上升。执行完再算覆盖率,通常能翻一倍以上。
注意:这个惩罚要在相似度计算完成后、推荐之前执行,不能改原始交互数据,否则会影响其他指标。
5.3 相似度全是 0:稀疏矩阵和索引缺失
现象:跑sim_df.loc["song_A", "song_B"]时返回 0,或者直接报 KeyError;再一看,很多歌曲根本没有邻居。
原因:分两种。第一种是数据太稀疏,两首歌没有任何共同用户,余弦相似度本来就是 0,这是正常的。第二种是你把 ItemCF 算好的 sim_df 用在另一份数据上,或者 crosstab 时列顺序发生了变化,导致sim_df[item]取到错误列甚至报错。还有一种隐蔽情况是历史歌曲里有新歌 ID,不在相似度矩阵的索引里。
解决:写推荐函数时,永远先做存在性判断:
if item not in sim_df.index: continue对于邻居全是 0 的歌曲,一个实用的策略是丢弃这首歌的邻居贡献,而不是强行做归一化。因为余弦相似度已经是 0~1 的值,没有归一化的必要。如果你在意“有些歌永远得不到推荐”,可以在论文里说明“对无邻居物品使用基于歌曲风格的内容推荐兜底”,实现不复杂,但能显著提升覆盖率的理论上限。
5.4 播放次数当成评分用:隐式反馈的常见误用
现象:推荐结果里出现大量“老歌”和“神曲”,用户明明只是循环播放了几次,系统就把它当成强烈喜好推荐相似歌曲。
原因:播放次数是隐式反馈,里面掺杂了太多非偏好因素。有人听歌是为了当背景音、助眠、学外语,这些场景下的播放次数和“喜欢”完全不是一回事。如果直接把播放次数当评分用,那些被循环播放的“学习材料”会被错误地当成口味标签,推荐结果自然跑偏。
解决:除了前面讲的1 + log1p(play_count)压缩外,还可以对播放次数做封顶处理,比如超过 30 次的统一按 30 算。如果你有跳过、切歌等负反馈数据,建议显式地给这些行为一个负权重,比如-0.5,能在很大程度上修正隐式反馈的偏差。论文里要强调“本文使用对数压缩后的隐式反馈权重,而非原始播放次数”,这句话显示了你的思考深度,评审印象分会明显不同。
5.5 评估结果虚高:训练集里混进了未来数据
现象:离线评估显示 Recall@10 达到 0.4,看起来非常漂亮,但上线或者人工抽查时效果远没有这么好。再次检查发现“测试集的歌曲在训练集里出现过”。
原因:这是数据划分不规范导致的。前面强调过时间切分,但很多人做交叉验证时仍然用train_test_split(X, test_size=0.2, random_state=42)这种随机划分。协同过滤的核心是“看图说话”,随机划分会把同一首歌同时放进训练集和测试集,模型在训练时已经学会了这首歌和哪些歌相似,测试时当然一猜一个准,相当于考试时翻书。
解决:严格使用时间维度的划分。我的习惯是排序后按用户分组,每个用户最后一条行为去测试,之前全部进训练。另一个容易忽略的细节是:测试集的歌曲如果从没在训练集出现过,推荐结果里必然没有它,这也会拉低指标。你可以选择过滤掉这些“冷门测试歌”,但要保证训练集和测试集的歌曲空间一致,否则评估的是另一种冷启动场景,论文里要明确写出来。
6. 从离线到可演示:接口封装、效果验证和长尾调优
6.1 Flask 包装一个推荐接口
毕设答辩需要现场演示,跑 Jupyter Notebook 展示不够直观,用 Flask 包一个 HTTP 接口是常见做法。核心代码很薄,推荐逻辑全部复用前面的recommend函数:
from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/api/recommend") def api_recommend(): uid = request.args.get("uid") top_n = int(request.args.get("k", 10)) items = recommend(uid, top_n=top_n) return jsonify({"user_id": uid, "items": items, "count": len(items)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)启动后浏览器访问/api/recommend?uid=123&k=10就能拿到 JSON 格式的推荐列表。注意debug=False,否则 VSCode 调试器会干扰 Flask 的重载逻辑。接口返回的歌曲 ID 可以在前端映射成歌名和封面,这一步放在论文的“系统展示”部分会很有说服力。
6.2 论文里的实验对比表怎么生成
不用 Excel 手填,直接从评估函数输出生成 Markdown 表格:
results = [] for model_name, model_func in [("Popularity", pop_recommend), ("UserCF", user_cf_recommend), ("ItemCF", item_cf_recommend)]: metrics = evaluate(model_func) results.append(f"| {model_name} | {metrics['precision']:.4f} | {metrics['recall']:.4f} | {metrics['coverage']:.4f} |")把三行结果拼到表头后面,复制进论文就完成了“实验对比”章节。我每次做这个步骤都会提醒自己:不要为了指标好看而隐藏模型参数,论文附录里附上实验参数表,虽然多写几行字,却是答辩时最稳的防御。
6.3 一个长尾优化:给行为加时间衰减
最后一个进阶操作:用户最近的行为比三个月前的行为更能代表当前口味。做法是给交互权重乘一个时间衰减因子:
df["ts"] = pd.to_numeric(df["ts"], errors="coerce") current_ts = df["ts"].max() # 计算每个行为距离最新行为的天数 df["days_ago"] = (current_ts - df["ts"]) / (24 * 3600) # 半衰期 30 天:30 天前的行为权重减半 df["weight"] = df["weight"] * np.exp(-df["days_ago"] / 30.0)这段代码要在构造相似度矩阵前执行,后面的流程完全不用改。exp(-days/30)的含义是 30 天前行为的权重约为现在的 0.37 倍,90 天前只剩 0.05 倍。这个参数很敏感,半衰期设太短会让模型只认识用户最近一周听的歌,设太长又失去衰减意义。我习惯了先设 30 天,再根据数据时间跨度调整。
我当年做这个题的时候犯过最蠢的一个错:把冷启动用户直接返回空列表,答辩老师当场让我注册个新账号试试,场面一度很尴尬。后来我养成了一个习惯,任何推荐函数先问自己一句“如果这个用户今天刚注册,会发生什么”。这个习惯帮我躲过了很多坑。如果你也打算做这个方向,希望这篇笔记能让你少走一段弯路,希望帮到你。
本文还有配套的精品资源,点击获取