资讯动态

基于机器学习的音乐推荐系统:从特征工程到冷启动实践

发布时间:2026/9/13 5:23:19 来源:尧图企业网站定制
简介基于机器学习构建的音乐推荐系统毕业设计项目面向计算机相关专业正在准备毕设的学生及需要项目实战练习的学习者也可作为课程设计、期末大作业参考。项目经导师指导并通过评审得分98分。压缩包共1106个文件约74.47MB其中包含189个JavaScript文件用于前端交互、94个Java源文件实现后端业务逻辑、82个HTML页面及57个CSS文件构建设计界面还有68个Jar包提供运行依赖同时附带数据库文件与多个时间段的访问日志方便模拟真实用户行为并复现实验场景。功能涵盖用户登录、音乐展示、推荐结果生成等常见模块推荐算法部分提供可扩展的代码实现便于调整对比不同策略。压缩包内还有完整文档说明覆盖系统架构、推荐算法流程、部署运行方式等内容目录结构清晰便于逐模块对照学习。已有224人学习下载。借助这套完整项目的源代码与配套文档读者可快速掌握从数据准备、特征处理、模型训练到推荐结果展示的完整链路还可参考毕业设计论文的写法与项目答辩思路。配套文档对环境搭建、数据集处理、模型评估等常见问题作了说明能有效降低上手门槛项目提供完整可运行的前后端代码与相关配置适合无经验者从零启动也方便有基础的读者在现有框架上替换模型或增加推荐策略作为机器学习应用方向的实战参考。1. 基于机器学习的音乐推荐系统把“猜你喜欢”拆成能复现的工程打开任意一款音乐 App首页那个“每日推荐”背后的模型本质上和标题里这套“机器学习-音乐推荐系统”要做的事是同一件事从用户行为数据里学习兴趣再把候选歌曲按兴趣排序输出。很多人以为推荐系统难在算法拿到项目却发现真正的门槛在数据组织、离线评估和冷启动这三块。这篇文章顺着一套可落地的音乐推荐系统源代码结构往下拆覆盖从原始播放日志到训练样本、从协同过滤到隐语义模型、从评估指标到新用户兜底的完整路径。适合正在做课程设计、准备推荐系统面试或接手第一个推荐模块的工程师。2. 音乐推荐系统的数据准备与特征工程把“听歌记录”变成模型能吃的样本推荐系统的上限由数据质量决定。音乐场景里原始数据最常见的形式是一张播放日志表字段形如user_id, song_id, play_ts, play_seconds, duration_seconds, is_collect。直接拿这张表喂模型是不行的要先把行为翻译成“正样本/负样本/特征”否则后面所有工作都建立在沙地上。2.1 先分清三类行为数据显式反馈、隐式反馈与上下文特征音乐 App 里用户很少主动打分绝大多数反馈是隐式的。做一个音乐推荐系统时我会先把行为分成三类再决定每类怎么进入训练样本。数据类型常见字段在推荐中的含义常见陷阱显式反馈rating、like、收藏用户主动表达偏好信号强数据稀疏绝大多数用户从不打分隐式反馈播放时长、skipping、循环播放反映真实行为量大“没播放”不等于“不喜欢”需要额外负采样上下文特征时间戳、时段、设备类型、地理位置决定推荐的场景相关性容易泄漏未来信息按时间切分要注意隐式反馈里信息量最大的是播放时长占比play_seconds / duration_seconds。一首歌听到 90% 以上说明用户确实喜欢听到 5 秒就切说明排序靠前的这首歌触怒了用户。这两个信号在构造正负样本时权重不同。2.2 构造训练集的核心问题正样本少、负样本靠猜、类别不平衡音乐推荐的数据分布非常偏斜头部歌曲占了绝大多数播放量长尾歌曲的交互次数极少直接训练会让模型只学会推荐热门歌。常见做法是引入负采样。我会对每一条正样本从用户从未交互过的歌曲里随机采样 1 到 2 首作为负样本。正负比例用 1:1 时模型偏保守用 1:2 时偏激进一般先设 1:2 再观察指标变化。时间维度上要按play_ts排序把每个用户前 80% 的行为划进训练集、后 20% 划进验证集这能避免“用未来预测过去”的时序泄漏问题。2.3 数据预处理的最小源代码从原始日志到训练特征下面这段代码完成三件事读入播放日志、计算收听时长占比、构造带负采样的训练文件。这是一个音乐推荐系统项目里最常见的清洗流程放在src/data_prepare.py。import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder # 原始播放日志 log pd.read_csv(data/listening_log.csv) log[ratio] log[play_seconds] / (log[duration_seconds] 1e-6) # 正样本 听完大部分且播放超过30秒或主动收藏 log[is_positive] ( ((log[ratio] 0.8) (log[play_seconds] 30)) | (log[is_collect] 1) ).astype(int) # 丢弃完全没有信号的记录 log log[log[play_seconds] 0].copy() # 将用户和歌曲映射到连续整数ID方便后续喂给矩阵分解 user_enc LabelEncoder() item_enc LabelEncoder() log[user_id] user_enc.fit_transform(log[user_id].values) log[song_id] item_enc.fit_transform(log[song_id].values) # 只保留正样本用于后续负采样 pos log[log[is_positive] 1][[user_id, song_id, play_ts]].copy() # 每个正样本配2个随机负样本 n_items log[song_id].nunique() rng np.random.default_rng(42) neg_samples [] for row in pos.itertuples(): for _ in range(2): neg_item rng.integers(0, n_items) neg_samples.append((row.user_id, neg_item, row.play_ts, 0)) negs pd.DataFrame(neg_samples, columns[user_id, song_id, play_ts, label]) negs negs[~negs.set_index([user_id, song_id]).index.isin( log.set_index([user_id, song_id]).index )] train pd.concat([pos.assign(label1), negs], ignore_indexTrue) train.to_parquet(data/train_samples.parquet)逻辑并不复杂。ratio是收听时长占比用来判断用户是否真的听完了歌曲is_positive用两条规则取并集避免单一条目误判。负采样用rng.integers随机生成不存在的“用户-歌曲”对再通过isin排除掉那些实际交互过但没被标为正样本的记录。注意随机种子固定为 42这是为了保证后面训练时每次复现负样本一致。参数上正负采样比 1:2 和最短收听时长 30 秒是两个最值得调的常量我会先把它们写成配置项而不是硬编码。3. 从相似度到矩阵分解音乐推荐系统的三条算法路线与最小实现数据准备好之后下一步是选算法。音乐推荐系统最常见的建模路径有三条基于物品的协同过滤ItemCF、基于用户的协同过滤UserCF和矩阵分解。顺序上建议先跑通 ItemCF再升级到矩阵分解这样对比时能清楚看到每一步提升了什么。3.1 基于物品的协同过滤ItemCF最快落地的第一版ItemCF 的核心假设简单直接用户喜欢某首歌是因为他之前听过的歌里有和它相似的单曲。相似度不来自歌曲的音频特征而来自“被同一批用户共同消费”的共现关系。常见实现是给每首歌计算一个惩罚后的余弦相似度from collections import defaultdict import math def build_item_similarity(train, top_k50): # train: list of (user_id, song_id) item_items defaultdict(lambda: defaultdict(float)) user_items defaultdict(set) for user, item in train: user_items[user].add(item) item_pop defaultdict(int) for user, items in user_items.items(): for item in items: item_pop[item] 1 # 共现矩阵加流行度惩罚 for user, items in user_items.items(): items list(items) for i in range(len(items)): for j in range(i 1, len(items)): a, b items[i], items[j] w 1.0 / (1.0 math.log1p(item_pop[a] item_pop[b])) item_items[a][b] w item_items[b][a] w # 归一化并截断保留每首歌最相似的top_k sim {} for a, related in item_items.items(): if not related: continue max_w max(related.values()) top sorted(related.items(), keylambda x: -x[1])[:top_k] sim[a] [(b, w / max_w) for b, w in top] return simitem_pop保存每首歌的交互次数1.0 / (1.0 log(item_pop))是惩罚项。这样做是因为 Coldplay 这种大热歌会和太多歌曲共现不惩罚会导致推荐结果全部偏向头部。归一化让相似度落在 0 到 1 之间方便后续做召回分数计算。top_k50控制了内存占用歌曲总数几十万时只保存每首歌相似度最高的 50 首矩阵规模从 O(N^2) 降到 O(N * 50)。3.2 基于用户的协同过滤UserCF适合用户量小的冷启动替代UserCF 是“和你口味相似的人也在听什么”在用户规模小、兴趣分化不明显的场景下效果往往不差。但它的扩展性比较差每来一个推荐请求都要实时计算该用户和其他人的相似度用户量百万级之后延迟会很难看。我的建议是音乐推荐系统第一版优先 ItemCFUserCF 只在做新用户冷启动或作为召回通道补充时使用。3.3 隐语义模型与矩阵分解把用户和歌曲同时映射到低维空间ItemCF 只能表达“显式共现”学不到潜在偏好。比如喜欢民谣的用户里有人偏陈鸿宇、有人偏赵雷ItemCF 靠歌曲共现能覆盖一部分但无法发现“这两个用户其实共享同一个隐因子——安静的男声吉他”。矩阵分解要解决的问题就是把用户和歌曲都映射到一个 K 维隐空间让用户向量和歌曲向量的内积逼近真实偏好。目标函数是带正则化的平方误差minimize sum((r_ui - u_u^T v_i)^2 lambda * (||u_u||^2 ||v_i||^2))其中r_ui是训练样本里的标签0 或 1u_u是用户隐向量v_i是歌曲隐向量K 是隐因子维度。3.4 一个可运行的矩阵分解源代码numpy 随机梯度下降import numpy as np class MatrixFactorization: def __init__(self, n_users, n_items, k32, lr0.01, reg0.02, epochs8): self.u np.random.normal(scale0.1, size(n_users, k)) self.v np.random.normal(scale0.1, size(n_items, k)) self.lr lr # 学习率取值0.005~0.02 self.reg reg # L2正则系数控制隐因子范数 self.epochs epochs # 迭代轮数过小欠拟合过大过拟合 def fit(self, pairs, labels): # pairs: (user_idx, item_idx) # labels: 0/1 for epoch in range(self.epochs): idx np.random.permutation(len(labels)) losses [] for ii in idx: u_id, i_id pairs[ii][0], pairs[ii][1] pred np.dot(self.u[u_id], self.v[i_id]) err labels[ii] - pred # 交替更新用户向量和歌曲向量 self.u[u_id] self.lr * (err * self.v[i_id] - self.reg * self.u[u_id]) self.v[i_id] self.lr * (err * self.u[u_id] - self.reg * self.v[i_id]) losses.append(err ** 2) print(fepoch {epoch 1}, mse{np.mean(losses):.6f})训练时把第 2 节做好的train_samples.parquet读进来构造pairs与labels两个数组即可。学习率设 0.01、正则 0.02、隐因子 K32 是音乐推荐比较稳的起点。K 太小学不出复杂偏好太大容易在稀疏样本上过拟合。判断过拟合的标志是训练集 MSE 持续下降验证集 Hit Rate 却不再上升这时优先加大reg而不是减小epochs。4. 模型训练与评估从源代码到 Hit Rate、NDCG 指标输出很多推荐项目跑通训练就停了但只有把评估做对才知道改一个参数到底是变好还是变坏。音乐推荐里我最常用的离线指标是三个Hit RateK、RecallK、NDCGK。4.1 离线评估的三个核心指标指标计算方式在音乐推荐里的解读注意点Hit RateK测试集真实播放歌曲是否出现在推荐 TopK用户有没有被推荐到想听的歌K 通常取 10 或 20RecallK测试集所有真实播放歌曲中被命中多少推荐系统对用户兴趣的覆盖能力需要和 Hit Rate 一起看NDCGK命中位置越靠前得分越高排序质量越靠前价值越大只关注 TopK 内的位置折扣我会明确提醒不要用 RMSE 作为音乐推荐的核心指标。用户不会给一首歌打 4 分还是 5 分他只会关心推荐列表里前 10 首有没有想听的。排序对了分数偏差大一点完全不影响体验。4.2 训练、预测和评估的完整代码下面的代码把第 3 节的矩阵分解封装成可复现实验输出 Hit Rate10 和 NDCG10import pandas as pd import numpy as np from collections import defaultdict train pd.read_parquet(data/train_samples.parquet) test pd.read_parquet(data/test_positive_samples.parquet) n_users max(train[user_id].max(), test[user_id].max()) 1 n_items max(train[song_id].max(), test[song_id].max()) 1 pairs train[[user_id, song_id]].values labels train[label].values.astype(np.float32) mf MatrixFactorization(n_users, n_items, k32, lr0.01, reg0.02, epochs8) mf.fit(pairs, labels) def recommend(user_id, item_ids, top_k10): scores mf.v[item_ids] mf.u[user_id] # 所有候选歌曲的内积打分 top np.argsort(-scores)[:top_k] return item_ids[top] def evaluate(test_df, top_k10): hit_total 0 ndcg_total 0.0 user_item_map defaultdict(list) for row in test_df.itertuples(): user_item_map[row.user_id].append(row.song_id) for user_id, truth_items in user_item_map.items(): # 候选集 测试集出现的所有歌曲正式场景应替换为召回结果 candidates np.unique(test_df[song_id].values) rec recommend(user_id, candidates, top_k) hit set(truth_items) set(rec) hit_total len(hit) 0 for rank, item in enumerate(rec): if item in set(truth_items): ndcg_total 1.0 / np.log2(rank 2) return hit_total / len(user_item_map), ndcg_total / len(user_item_map) hr, ndcg evaluate(test, top_k10) print(fHitRate10{hr:.4f}, NDCG10{ndcg:.4f})评估逻辑里最值得注意的问题是候选集范围。上面代码用全部测试歌曲作为候选这会让指标虚高因为实际线上召回先筛掉了大量不相关歌曲。更贴近生产的做法是为每个用户构建一个“召回池”比如他听过的歌手下的所有歌曲或者 ItemCF 相似度 Top50 的歌曲集合测试时只允许从召回池里选。4.3 让实验可复现的五个细节第一固定随机种子。负采样和模型参数初始化都要用同一个seed42。第二负样本数量写进配置文件而不是散落在代码里。第三评估时按时间切分测试集不要随机切分。第四记录每次实验的 git commit 和超参方便回滚。第五对 ItemCF 和矩阵分解跑同样的评估函数否则两个模型之间无法对比。5. 冷启动与混合推荐让推荐系统照顾到没有历史行为的新用户新用户没有任何播放记录UserCF 和 ItemCF 都失效矩阵分解也只会返回热门兜底。这是所有音乐推荐系统上线时最先被吐槽的点。5.1 冷启动到底“冷”在哪里新用户冷启动的本质是没有行为特征可以利用。缓解方法有两种思路一是注册时主动收集偏好比如让用户选 3 个喜欢的歌手二是用内容特征做泛化从歌曲本身的属性去推断用户可能喜欢什么。5.2 用内容特征打破冷启动歌手、流派与音频特征给每首歌维护一份画像表字段包括artist_id, genre, release_year, duration再统计歌手和流派的平均热度。这样即使没有用户行为也能根据用户选择的歌手生成一个粗糙但可用的推荐列表。def cold_start_recommend(artist_ids, item_profile, top_n20): # 用户选了3个歌手返回这些歌手的高热度歌曲 selected item_profile[item_profile[artist_id].isin(artist_ids)] selected selected.sort_values(global_popularity, ascendingFalse) return selected[song_id].head(top_n).tolist()这个函数只有十几行但它是冷启动阶段的保底逻辑。global_popularity可以用全站播放量归一化得到注意要按时间窗统计避免太久远的歌曲一直占据前列。5.3 混合推荐架构加权融合或两阶段“召回 排序”线上常用的混合策略是两阶段召回阶段把 ItemCF、矩阵分解、内容推荐的结果各取一定数量合并得到几百首候选排序阶段再用更精细的特征如最近播放时间、歌手热门度、流派匹配度重新打分。冷启动用户的召回通道以内容推荐和全局热门为主老用户则以协同过滤和矩阵分解为主。5.4 把冷启动策略接进主推荐流程的接口设计主流程不外乎三步查用户行为记录、选召回源、排序。行为记录充足时走 ItemCF MF不足时走内容推荐 热门兜底。接口里要注意加超时保护推荐服务不能因为召回计算太慢拖垮整个接口。6. 上线前必跑的一个体检脚本推荐多样性与覆盖率检查离线指标能反映“准不准”但不能完全反映“好不好听”。一个推荐系统如果一直推荐同一歌手的歌Hit Rate 可能很高用户却很快就会腻。所以我在上线前会跑一个多样性检查脚本计算每个用户推荐结果里独特的歌手数和流派数再求全局均值。def diversity_report(rec_lists, item_profile): artist_counts [] genre_counts [] for user_id, songs in rec_lists.items(): meta item_profile[item_profile[song_id].isin(songs)] artist_counts.append(meta[artist_id].nunique()) genre_counts.append(meta[genre].nunique()) return { unique_artist_mean: np.mean(artist_counts), unique_genre_mean: np.mean(genre_counts), top10_item_cover: len(set( s for lst in rec_lists.values() for s in lst)) / item_profile[song_id].nunique() }这个脚本的输入是推荐接口输出每行一个用户的 Top20 列表输出三个数字用户平均听到多少个不同歌手、多少个不同流派、整个推荐列表覆盖了曲库里的多大比例。一般我会把“连续 3 天 unique_genre_mean 4”当成告警条件说明推荐结果已经收窄到少数几个流派需要调整召回源的配比。和离线指标一样这组检查也可以放进 CI 每天跑一次把推荐质量变化挡在上线之前。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价