
简介基于机器学习构建的音乐推荐系统完整毕设资源包面向计算机、人工智能、自动化、电子信息等专业在校生与相关从业者可用于毕业设计、课程设计、作业或项目初期立题演示。资源内代码全部通过运行验证后端以 Java 与 JSP/class/jar 实现推荐算法和业务逻辑前端由 HTML/CSS/JS 搭建交互页面XML 与 properties 文件管理配置DB/CSV 中存放歌曲信息与历史日志另有 JPG/PNG 截图和 README 说明文档方便快速了解项目结构并二次开发。压缩包共 1106 个文件约 74.1MB内容丰富但目录清晰适合从数据预处理、特征工程到模型训练与效果评估的完整流程学习。附带多时段 access_log 日志数据可用于用户行为分析、召回/排序策略验证等扩展研究。已有 580 人学习下载整体完成度与规范性较好对希望以音乐推荐为主题完成毕设或积累工程经验的人有实际参考价值。1. 基于机器学习的音乐推荐系统到底在解决什么从“歌单灰点”到“千人千面”打开音乐App首页推荐里十有八九是周杰伦、林俊杰这些热门歌手你随手点下一首冷门法语电子乐推荐列表却纹丝不动。这就是典型的“流行度霸权”现象也是你拿着十几万条用户播放记录准备做机器学习音乐推荐系统时第一道坎。网上带源代码和文档说明的项目很多但大多跑起来容易换一份数据就翻车原因往往不在模型而在数据处理和评估方式。本文要带你走一遍真实落地路径从原始播放记录到特征工程再到矩阵分解、混合模型训练、参数调优和避坑排查最后给一个可复现的代码骨架。适合正在做课程设计、毕业设计、或者刚入门推荐系统的朋友看完能自己动手跑通一条完整链路。2. 音乐推荐系统的数据与特征工程为什么原始播放记录不能直接喂给模型推荐系统里有一句老话数据和特征决定了上限模型只是逼近这个上限。音乐推荐尤其如此因为用户的“喜欢”是高度隐晦的——听完一段可能只是因为通勤路上懒得切歌。我在日常做机器学习模型训练时第一步从来不是选算法而是先把数据摊开看一遍这一章就是把“机器学习中的数据处理是什么”这个问题落到具体动作上。2.1 数据形态用户-歌曲-播放次数三元组与隐含反馈公开可用的音乐推荐数据最常见的是三元组用户ID、歌曲ID、播放次数。播放次数是典型的隐含反馈用户没有给你打1到5分而是用行为表达兴趣。隐含反馈的数值分布非常难看有人把一首歌循环了一百遍有人听三秒就切歌但数据里只有“播放次数”。这个数字和真实喜好之间有偏差但不能直接丢弃。我一般会先把播放次数做截断处理防止爬虫或异常刷量污染训练集。import pandas as pd # 假设原始列表里有三列user_id, song_id, play_count df pd.read_csv(user_song_plays.csv) # 播放次数下界设为1上界截断到5000避免极端值主导loss df[play_count] df[play_count].clip(lower1, upper5000) # 统计每个用户和每首歌的总播放量用于过滤垃圾数据 user_play_sum df.groupby(user_id)[play_count].transform(sum) song_play_sum df.groupby(song_id)[play_count].transform(sum) # 用户总播放量少于20次说明这个用户只听了两三首歌行为信号不足 # 歌曲总播放量少于5次属于几乎没人听过的噪声数据 df df[(user_play_sum 20) (song_play_sum 5)] print(df.shape)这里的逻辑很清楚过滤不是为了让数据好看而是保证后续矩阵分解能够学到稳定的隐因子。如果保留只被播放过一次的歌模型会把那些稀疏的ID也当成一个隐因子来学结果就是过拟合推理时几乎所有用户都接到同一批冷门歌。另外很多课程设计喜欢直接读入原始数据不做处理最后模型精度看起来很高实际上是因为数据泄漏或者样本极不均衡这一点后面专门讲。2.2 特征提取与处理把“听过”变成模型吃得了的数值除了三元组完整的音乐推荐系统还需要歌曲的元数据比如艺人、专辑、流派、发行年份甚至音频特征。但绝大多数入门项目只给了你三元组这个时候能做的特征工程有两类一类是从交互数据里统计出来的行为特征比如用户听过多少首歌、歌曲被多少用户听过、用户的播放活跃时间段另一类是群体统计特征比如每首歌的平均播放次数、标准差。这些特征在训练基于树的排序模型时特别有用。df[play_count_log] np.log1p(df[play_count]) # 压偏态 user_feat df.groupby(user_id).agg( user_listen_cnt(song_id, count), user_avg_play(play_count_log, mean), user_active_hour_mean(timestamp, lambda x: (x // 3600) % 24 if False else 0) # 示意真实数据有时间戳列 ).reset_index() song_feat df.groupby(song_id).agg( song_listen_cnt(user_id, count), song_avg_play(play_count_log, mean) ).reset_index() df df.merge(user_feat, onuser_id).merge(song_feat, onsong_id)很多初学者忽略了一点用户活跃时间段、听歌时长这些特征能显著改善冷启动下新歌的推荐。因为即使没有足够用户交互歌曲自身的热度和播放时长曲线也能提供先验。另外把播放次数取对数是非常常见的操作否则平均值会被极少数疯狂用户拉高。机器学习模型的梯度在高度偏态的输入上极不稳定尤其是用均方差损失训练时一个1000次播放的记录产生的残差能把损失函数带偏好几个量级。2.3 机器学习中的数据处理与负采样推荐模型的半条命推荐问题本质上是选择问题。你给用户看的候选歌曲可能有几十万首但用户只听过其中的几十首。如果只拿“听过”的数据当正样本模型会学出一个永远输出1的无脑预测器。解决办法是构造负样本也就是“用户没听过的歌”。常见做法是随机采样但纯随机采样有一个问题热门歌曲被采为负样本的概率更高模型会误以为热门歌不好导致推荐结果向冷门歌偏移。更好的做法是混合采样70%从全局随机30%从热门歌曲里选让模型认清“热门但不被该用户喜欢”的情况。def generate_neg_samples(df, n_neg_per_pos3, seed42): rng np.random.RandomState(seed) users df[user_id].unique() songs df[song_id].unique() pos_pairs set(zip(df[user_id], df[song_id])) neg_pairs [] for u in users: interacted set(df.loc[df[user_id] u, song_id]) # 用固定种子采样保证可复现 possible songs[~songs.isin(list(interacted))] chosen rng.choice(possible, sizemin(len(possible), n_neg_per_pos), replaceFalse) neg_pairs.extend([(u, s, 0) for s in chosen]) return neg_pairs注意这里没有把用户所有没听过的歌曲都拿来做负样本那样几十万用户乘几百万歌内存直接爆炸。机器学习中的高效做法是“采样一部分负样本”并且保证同一个人听到的正样本和负样本比例在1:3左右。这样的样本构造方式对后续用逻辑回归或GBDT做CTR预估型推荐特别关键。这个思路在源码里通常表现为一个sample函数你拿到源代码后第一件事就是看它负采样逻辑因为那是作者对数据分布的隐式理解。3. 搭建推荐模型从协同过滤到矩阵分解再到机器学习混合模型有了干净的数据和负样本下一步就是选模型。音乐推荐领域最经典的是协同过滤基于用户行为找相似。再进一步是用矩阵分解做隐因子建模这也是很多源码里默认给出的基线。最后为了处理冷启动通常要加入内容特征做混合。3.1 基于用户的协同过滤最直观的“和你像的人听的歌”基于用户的协同过滤逻辑很直白找到和你播放历史最像的N个用户然后把他们听过而你没听过的歌拿来推荐。这个思路实现起来很简单但有两个明显问题用户数量大时计算用户间相似度过慢用户行为稀疏时相似度不可靠。所以现在的源代码里更多是用基于物品的协同过滤先算歌曲和歌曲之间的相似度再根据你听过的歌去推相似歌曲。from scipy.sparse import csr_matrix from sklearn.metrics.pairwise import cosine_similarity user_idx {u: i for i, u in enumerate(df[user_id].unique())} item_idx {s: j for j, s in enumerate(df[song_id].unique())} rows [user_idx[u] for u in df[user_id]] cols [item_idx[s] for s in df[song_id]] score df[play_count_log].values interaction csr_matrix((score, (rows, cols))) item_item_sim cosine_similarity(interaction.T) # 按物品维度算余弦 # 把相似度矩阵转成DataFrame方便查询 sim_df pd.DataFrame(item_item_sim, indexitem_idx.keys(), columnsitem_idx.keys())这里的关键是用稀疏矩阵存交互记录直接存成二维数组在几十万歌曲规模下就是内存灾难。余弦相似度适合播放次数取对数后的数据因为对数可以减缓热门歌曲的绝对数值优势。但纯物品协同过滤有一个毛病它只依赖行为无法回答“为什么相似”。一首民谣和一首电子乐可能因为都被同一个歌单收录而出现在相似度里但没有语义上的可解释性。这是后面引入内容特征的原因。3.2 矩阵分解与隐因子用机器学习补全用户-歌曲矩阵矩阵分解把用户和歌曲映射到同一个低维隐因子空间。用随机梯度下降去拟合所有已知的“(u,i,r)”三元组让预测评分拟合真实播放次数。这是机器学习音乐推荐系统源码里的核心模块。自己实现一遍能深刻理解参数如何互相影响我建议至少手写一次。import numpy as np class MatrixFactorization: def __init__(self, n_factors20, lr0.01, reg0.02, epochs20): self.n_factors n_factors self.lr lr self.reg reg self.epochs epochs self.user_factors None self.item_factors None def fit(self, user_idx_list, item_idx_list, ratings, n_users, n_items): rng np.random.RandomState(42) # 用小的随机数初始化避免对称性问题 self.user_factors rng.normal(0, 0.1, size(n_users, self.n_factors)) self.item_factors rng.normal(0, 0.1, size(n_items, self.n_factors)) for epoch in range(self.epochs): total_loss 0 for u, i, r in zip(user_idx_list, item_idx_list, ratings): pred np.dot(self.user_factors[u], self.item_factors[i]) err r - pred total_loss err ** 2 # 更新时先记住旧的用户向量算物品梯度时用旧值 u_old self.user_factors[u].copy() self.user_factors[u] self.lr * (err * self.item_factors[i] - self.reg * self.user_factors[u]) self.item_factors[i] self.lr * (err * u_old - self.reg * self.item_factors[i]) if epoch % 5 0: print(fepoch {epoch}, loss {total_loss:.4f})这段代码看起来简单但坑很多。第一u_old必须复制因为同一行代码里self.user_factors[u]更新后再算物品梯度时不能让它跟着变。第二正则化系数reg和损失函数里的残差平方项是一起乘以学习率更新的所以调大reg会拖慢收敛。第三这个纯Python循环在百万级样本下跑一轮要几分钟所以实际源码里会改成向量化计算或者直接用surprise、scipy里的优化器。但原理就是上面这样。3.3 加入内容特征的混合推荐冷启动问题的常见解法矩阵分解能处理用户行为但遇到没有行为的新用户就废了。常见做法是混合推荐把矩阵分解得到的隐因子当作特征再拼上歌曲的文本或属性和用户的统计特征丢进一个逻辑回归或XGBoost模型让模型学要不要给这个用户推荐这首歌。这是从“纯协同过滤”向“机器学习排序”升级的关键一步。# 特征向量示意每个样本一行 # [用户隐因子20维] [歌曲隐因子20维] [用户统计特征] [歌曲统计特征] [交叉特征] feat_dim 20 20 3 3 1 X np.zeros((len(samples), feat_dim)) y np.array([s[label] for s in samples]) # 召回阶段先用矩阵分解粗略过滤一遍只保留用户隐因子与歌曲隐因子点积前100的候选 candidates model_mf.recommend(user_idx, top_k100) # 排序阶段用逻辑回归给候选打精确分 from sklearn.linear_model import LogisticRegression clf LogisticRegression(C1.0, max_iter100) clf.fit(X_train, y_train) # 最终排序时把MF得分和LR得分加权合并 final_score 0.7 * mf_score 0.3 * clf.predict_proba(X)[:, 1]这种两阶段“召回排序”结构是工业级音乐推荐系统的标准做法。源码里如果只有矩阵分解那只是基线如果连交叉特征和逻辑回归都有就值得仔细看。注意召回阶段可以让模型只遍历几千首歌而不是全部这样排序阶段的计算量就受控了。我第一次做的时候把排序模型用在全量歌曲上服务器直接卡死后来改成召回100首再排序延迟降到了原来的百分之一。4. 训练、评估与调参从离线指标到线上体验的关键几步模型写完最需要较真的其实是评估方式。很多源代码里的train_test_split直接随机切这在推荐系统里是要被骂的因为用户行为有强烈的时间先后关系。用后一个月的收听去预测上个月的推荐等于作弊。4.1 数据划分与评估指标RMSE之外还要看PrecisionK推荐系统的离线评估要回答两个问题回归型指标RMSE、MAE衡量预测播放次数准不准排名型指标PrecisionK、RecallK、AUC衡量推荐列表里用户真正想听的比例。实际使用时更看重后者因为用户不会去看你预测的2.3次还是2.5次他只关心列表前10里有没有他想听的歌。代码里要留出专门的评估函数。def precision_at_k(true_songs, rec_songs, k10): rec_set set(rec_songs[:k]) if len(rec_set) 0: return 0 hit len(rec_set set(true_songs)) return hit / len(rec_set) def recall_at_k(true_songs, rec_songs, k10): if len(true_songs) 0: return 0 rec_set set(rec_songs[:k]) return len(rec_set set(true_songs)) / len(true_songs)评估时有一个原则对每个用户从他收听时间最新的若干首歌里取出去重后的歌曲集作为true_songs然后把推荐列表和这个集合对比。不能全部历史歌都算对因为用户早就变口味了。我见过很多项目直接把所有历史播放当成真实标签结果Precision虚高因为用户过去听过的流行歌在训练集里出现得多推荐列表把流行歌排到前面命中率自然高。4.2 必调参数与迭代方向隐因子个数、正则化系数、学习率矩阵分解的四个核心参数是隐因子个数、学习率、正则化系数、迭代轮数。隐因子个数决定了模型能表达多细的“风格”音乐推荐里常见取值是20到100。太小学不出风格差异太大容易过拟合且训练变慢。学习率一般从0.01开始正则化系数从0.02左右开始做网格搜索。我自己的经验是先用一个小的隐因子数跑通验证流程然后每轮只改一个参数不要一上来就网格搜索所有组合。param_grid { n_factors: [20, 50, 80], lr: [0.005, 0.01, 0.02], reg: [0.01, 0.02, 0.05], epochs: [20, 30] } # 用GridSearchCV之前要自定义一个评分函数不能直接用默认的accuracy # 因为推荐场景里label是0/1但多数样本是负样本accuracy会骗你这里特别要强调“负样本占比”。如果正负样本比是1:3一个永远预测“不喜欢”的模型也能有75%的准确率。所以评估时别用accuracy要用AUC、PrecisionK或者对正样本加权。很多机器学习课程设计选题时选了音乐推荐最后报告里写准确率98%十有八九就是这个坑——随机划分后的测试集里负样本太多全预测0都能拿高分。4.3 模型保存与使用把推荐结果变成可复现的服务训练完成后要把模型参数保存下来。矩阵分解保存用户因子矩阵和物品因子矩阵即可这两个都是numpy数组用np.save保存成.npy文件。排序模型用joblib.dump保存。然后写一个predict.py加载模型后对指定用户生成推荐列表。import numpy as np # 假设已经保存了user_factors.npy 和 item_factors.npy user_factors np.load(user_factors.npy, allow_pickleTrue) item_factors np.load(item_factors.npy, allow_pickleTrue) song_ids np.load(song_ids.npy, allow_pickleTrue) def recommend_for_user(user_row_index, n10, excludeNone): scores user_factors[user_row_index] item_factors.T if exclude is not None: scores[exclude] -np.inf return song_ids[np.argsort(scores)[::-1][:n]]注意推荐时一定要排除用户已经听过的歌曲否则你给一个重度用户推荐的全是他自己歌单里的旧歌。另外如果模型里有偏差项保存时也要一并保存否则上线后预测值会有整体偏移。文档说明里如果没写模型如何加载十有八九是要你自己根据训练代码反推保存格式。5. 避坑与常见问题排查跑通demo容易上线难在这些鬼地方这个章节要专门来踩坑。我接过不少拿着开源源码来问问题的人发现90%的失败都不是模型选型问题而是下面几个隐蔽的错误。每条都按“现象→原因→解决”来说。5.1 冷启动新用户和新歌曲什么都没有推荐结果全乱套现象新用户注册后页面上的推荐列表完全是热门老歌和用户刚选择的“喜好歌手”完全无关。原因协同过滤模型只用交互历史做推断新用户行为矩阵里对应行全是0点积结果天然偏向高频物品。解决给冷启动用户提供“种子歌曲”入口让用户先选3首歌然后基于物品协同过滤找到这3首歌的相似歌曲。如果连歌曲ID都没有只能用内容特征按艺人和流派做规则化过滤。源码里通常有一个cold_start.py模块专门处理这种情况。5.2 矩阵稀疏与内存爆炸评分矩阵存成普通二维数组就是找死现象训练到一半Python进程被OOM杀死。原因用户数和歌曲数几十万构建的n_users * n_items密集矩阵要占几百GB内存。解决用scipy.sparse存交互矩阵或者直接走矩阵分解的采样训练方式不构建完整矩阵。我在自己的实践里喜欢把交互三元组拆分成两个稀疏矩阵一个是用户-歌曲播放次数矩阵一个是歌曲特征矩阵。千万不要为了方便把播放次数补成0填充的密集矩阵那是灾难。5.3 数据泄漏与时间穿越用未来数据预测过去指标全在骗人现象离线测试集上RMSE低到0.01模型一上线推荐明显不如随机。原因数据集直接随机划分同一首歌的历史播放被同时分到训练集和测试集模型等于见过答案。解决按时间戳排序前80%时间作为训练集后20%作为测试集。还要确保测试集里的用户历史中不包含训练集之后的记录。代码里可以这样写df df.sort_values(timestamp) cutoff int(len(df) * 0.8) train df.iloc[:cutoff] test df.iloc[cutoff:] # 关键一步训练集中用户ID要重新编码测试集中必须保留用户与歌曲ID的映射如果你的源数据没有时间戳那就用用户ID做分层划分保证同一个用户的所有交互都在同一边不然一个用户的播放记录一半在训练一半在测试模型看到了他的完整听歌历史评估结果没有意义。这个问题在机器学习课程设计里特别常见因为大家习惯用train_test_split(random_state42)但那是对独立同分布数据的假设交互数据不满足。5.4 流行度偏差与马太效应热门歌曲霸榜个性化名存实亡现象推荐列表里排名前20的歌曲永远是那些总播放量最多的。原因主流艺人的歌曲在训练集里出现的次数多模型学到的是“播放量高的歌就是好”而不是“这个用户会喜欢这首歌”。解决调整负采样分布让热门歌成为负样本的概率更高从而压低模型对这种普遍流行的偏爱。另一种做法是在最终排序分数里对歌曲流行度做惩罚final_score predicted_score - 0.2 * np.log1p(song_popularity[item_idx])这里0.2是惩罚系数需要根据验证集调。我见过一个项目不处理这个推荐结果和排行榜几乎一模一样推荐系统成了一个摆设。机器学习模型训练时看到的热门歌曲样本多这只是统计规律不是用户真实兴趣你需要强行修正这个偏差。5.5 源代码与文档说明不匹配复现别人项目最容易翻车的点现象严格按照文档说明运行先报缺少numpy装完又报版本冲突最后发现代码里面调用了文档里根本没写的函数。原因网上不少项目文档是后补的作者自己可能换了环境但代码没同步更新。解决第一先用pip freeze requirements.txt给自己项目锁住环境第二如果是从GitHub拉下来的源代码第一件事看README里的运行命令和requirements.txt没有就把所有import的第三方库都装一遍第三不要默认代码里的数据路径存在自己写一个检查脚本去验证文件是否存在、列名是否一致。这个坑几乎每个做“基于机器学习-音乐推荐系统源代码文档说明”项目的人都会踩别问我怎么知道的。6. 一个可落地的完整代码骨架从数据加载到推荐结果保存最后一章给一个整体骨架把前面所有模块串起来。这个骨架不是严格的工程级代码但足够让你在自己电脑上跑通流程并扩展成课程设计或毕业设计主体。import pandas as pd import numpy as np from scipy.sparse import csr_matrix # 1. 加载数据 df pd.read_csv(user_song_plays.csv) # 2. 过滤、取对数 df df[df[play_count] 0] df[play_count_log] np.log1p(df[play_count]) # 3. 构造ID映射 uid {u: i for i, u in enumerate(df[user_id].unique())} sid {s: j for j, s in enumerate(df[song_id].unique())} df[uid] df[user_id].map(uid) df[sid] df[song_id].map(sid) # 4. 切分按用户切分避免同一用户数据泄漏 users df[uid].unique() rng np.random.RandomState(42) val_users rng.choice(users, sizeint(len(users)*0.2), replaceFalse) train df[~df[uid].isin(val_users)] valid df[df[uid].isin(val_users)] # 5. 训练矩阵分解 mf MatrixFactorization(n_factors40, lr0.01, reg0.02, epochs20) mf.fit(train[uid].values, train[sid].values, train[play_count_log].values, n_userslen(uid), n_itemslen(sid)) # 6. 在验证集上评估Precision10 def evaluate(valid_df, mf, k10): # 对每个验证用户取其最新听到的5首歌作为真实兴趣其余历史作为排除项 # 这里为了演示简单取每个用户在验证集里的最后一条记录做采样 results [] for u in valid_df[uid].unique(): seen set(valid_df.loc[valid_df[uid] u, sid]) scores mf.user_factors[u] mf.item_factors.T scores[list(seen)] -np.inf recs np.argsort(scores)[::-1][:k] # 假设真实兴趣是用户所有听过歌曲中播放次数最高的 true_one valid_df.loc[(valid_df[uid] u) (valid_df[play_count] 1), sid].iloc[0] results.append(1 if true_one in recs else 0) return np.mean(results) # 7. 保存模型参数和映射 np.save(user_factors.npy, mf.user_factors) np.save(item_factors.npy, mf.item_factors) np.save(song_ids.npy, np.array(list(sid.keys())))这个骨架里最值得注意的技巧是我最后会反复检查的一步验证时要把该用户在训练阶段出现过的所有歌曲排除掉否则算出来的Precision是假的。我在初版项目里忘了排除Precision10跑出0.35自查代码时发现是因为用户喜欢听的歌在训练集里出现过太多次热门歌全排在前面。排掉历史交互之后Precision掉到0.12那个数字才反映真实的推荐命中率。矩阵分解训练完之后你还会发现一个问题每轮loss下降很慢甚至不稳定。这时候不要急着加更多数据先检查学习率是不是太大正则化是不是太小。我习惯是先跑5轮看loss在什么量级再调参数或者直接把lr设成0.005reg设成0.05通常比盲目用0.010.02稳定。另一个实用技巧是把每个epoch的验证集Precision10也打印出来而不是只打印训练loss。很多源码只提供训练过程不给验证曲线导致你调参全靠猜。我都是自己写一个小循环在每次epoch结束后跑一遍验证集看Precision有没有上升。如果训练loss降了但验证精度不升那就是过拟合信号该加正则化或者减小隐因子维度。如果你打算把这个项目继续做深下一步可以尝试用surprise库替代自写的矩阵分解因为它的SVD实现会加偏差项效果通常更好。但为了课程设计展示原理自写版本反而更容易讲清楚。我也踩过文档说明里说不支持Python最新版本结果被卡了一周最后发现换回3.8环境就通了。经验是先看setup.py和.github/workflows里的测试环境再决定要不要升级依赖。希望帮到你这篇笔记里的教训都是我自己一遍遍跑出来的音乐推荐系统看似简单做好一群用户和几十万首歌之间的“猜你喜欢”每一步都值得较真。本文还有配套的精品资源点击获取