ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习的音乐推荐系统实战:从数据清洗到矩阵分解与GRU模型部署

基于深度学习的音乐推荐系统实战:从数据清洗到矩阵分解与GRU模型部署 简介一套基于深度学习的音乐推荐系统毕业设计/课程作业资源包面向计算机相关专业学生覆盖从音乐数据处理、模型构建到后端部署的完整实现流程。资源共543个文件包含Java/JSP业务代码、XML配置、SQL脚本、前端CSS/JS及lrc歌词文本等压缩包仅5.44MB轻量但结构完整便于本地运行与二次开发。项目融合Python深度学习思路与C高效处理理念涉及协同过滤、RNN/CNN等模型并包含API服务实现适合用于毕设答辩演示或课程设计提交。目前已有190人下载学习可配合描述中的技术要点快速理解推荐系统从数据清洗、特征提取到模型训练与部署的各个环节是提升综合工程能力的实用参考资料。1. 基于深度学习的音乐推荐系统毕设资源包里真正值得复用的部分拿到一份「基于深度学习的音乐推荐系统」毕设资源大多数人第一反应是先把 demo 跑起来结果发现推荐列表里永远是那几首热门歌曲根本谈不上个性化。这份资源包不太一样的地方在于它把从数据处理到模型部署的完整链路都带上了Python 数据清洗、TensorFlow/PyTorch 模型构建、Flask 后端接口外加一套 Bootstrap animate.css 的前端展示页。适合正在做毕业设计、课程作业或者想快速搭一个能演示的推荐服务的同学照着主线走一遍能避开不少弯路。2. 数据预处理与特征工程把原始播放日志变成可训练的样本集2.1 播放日志和歌曲元数据先对齐再清洗推荐系统的数据基础通常分三块用户行为表谁在什么时间听了什么歌、歌曲元数据表歌曲ID、艺术家、专辑、流派、用户属性表注册时间、年龄等可选字段。在这套资源里数据格式会以 CSV 或者 JSON 给出处理时第一件事不是建模而是把三张表的 ID 字段对齐。常见做法是先把用户行为表的主键user_id song_id去重同一个用户一天内反复听同一首歌只保留一条避免后面对样本数量产生误判。清洗时还要做两个方向的过滤用户维度和歌曲维度。如果一个用户总共只听过三五首歌他的行为数据学不出稳定的偏好如果一首歌只有一两次播放记录它的向量也缺乏统计意义。我一般会把阈值定为用户至少 20 次播放、歌曲至少 10 次播放这两个数值不固定但面对中小规模数据集几万到几十万条记录时过滤后通常能留下 80% 以上的有效交互。过滤完了别忘了重新编码 ID因为 Pandas 里读进来的 ID 可能是字符串后续 Embedding 层需要连续整数索引。2.2 Pandas 清洗代码与负样本采样逻辑接下来这块是实操中最容易翻车的地方按顺序贴三段代码。先看基本清洗和 ID 重编码import pandas as pd import numpy as np # 读入三张表 interactions pd.read_csv(user_play_log.csv) songs pd.read_csv(songs.csv) users pd.read_csv(users.csv) # 去重同一用户同一首歌只保留一条记录 interactions interactions.drop_duplicates( subset[user_id, song_id], keepfirst ) # 过滤低频用户与冷门歌曲 user_counts interactions[user_id].value_counts() song_counts interactions[song_id].value_counts() keep_users user_counts[user_counts 20].index keep_songs song_counts[song_counts 10].index interactions interactions[ interactions[user_id].isin(keep_users) interactions[song_id].isin(keep_songs) ].reset_index(dropTrue) # 重新编码为连续整数ID供Embedding使用 interactions[uid] interactions[user_id].astype(category).cat.codes interactions[sid] interactions[song_id].astype(category).cat.codes这段代码里 drop_duplicates 的 keepfirst 表示保留最早一条如果你的日志里有播放时长字段更合理的做法是按时长取最大值那一行。阈值 20 和 10 不是硬性标准数据量大的时候可以适当提高比如用户至少 50 次播放、歌曲至少 20 次播放过滤后样本更干净但数量会缩水需要根据任务平衡。然后是负样本采样。协同过滤和后续的序列模型训练时正样本是「用户听了某首歌」但模型需要知道「哪些歌用户不喜欢」这部分就是负样本。最简单的采样方式是对每个用户从他没有听过的歌曲里随机抽样数量和正样本 1:1。all_sids set(interactions[sid].unique()) neg_rows [] # 按用户分组做负采样 for uid, group in interactions.groupby(uid): played set(group[sid]) candidates list(all_sids - played) # 每个用户抽取与正样本等量的负样本 neg np.random.choice(candidates, sizelen(group), replaceFalse) for sid in neg: neg_rows.append({uid: uid, sid: sid, label: 0}) # 合并正负样本 pos_rows interactions[[uid, sid]].copy() pos_rows[label] 1 dataset pd.concat([pos_rows, pd.DataFrame(neg_rows)], ignore_indexTrue) dataset dataset.sample(frac1, random_state42).reset_index(dropTrue)负样本数量的选择会直接影响推荐结果。1:1 是最常见的起点如果发现模型把几乎每首歌都预测成高评分说明负样本太少可以把比例提高到 1:3 甚至 1:5。这里的 np.random.choice 每次运行结果都不同做实验时要固定 random_state否则前后两次训练的数据分布不一样指标对比没有意义。2.3 什么时候值得引入 C 做数据处理摘要里提到 C 用于高效数据处理这条在毕设场景里需要注意边界。Python Pandas 处理百万级以内的记录没问题但如果是千万级用户行为日志Pandas 的 groupby 会变得很慢内存占用也高。常见做法是把数据预处理阶段的重活按时间排序、滑窗切分、去重写成 C 小工具或者直接用 PySpark而不是在 Python 里硬扛。但作为毕业设计除非数据量真的到了千万级否则 C 引入带来的工程复杂度会挤占模型调优的时间不划算。一个折中方案是先用 Python 写好完整流程并跑通小数据再在文档里说明「大规模场景下可以考虑将数据清洗模块用 C 重写」答辩时这个点能体现工程思考又不会让自己陷进 C 编译调试的坑。这套资源里 process.bat 就是用来批处理启动数据的把它理解成工程化的一环就行。3. 推荐模型选型与构建从协同过滤到 RNN 序列推荐的升级路径3.1 为什么毕设建议从矩阵分解起步音乐推荐相关的深度学习方法很多协同过滤、矩阵分解、CNN、RNN、注意力机制都能用但毕设场景下不建议一上来就套 Transformer。原因有两个一是音乐行为数据量通常不够大几万用户几万首歌的规模复杂模型很容易过拟合二是答辩时评委问「为什么选这个模型」矩阵分解的显式公式和可解释性能让你把推荐原理讲得很清楚而注意力模型讲清楚「Q/K/V 怎么算出来的」对本科毕设来说性价比不高。推荐系统的核心假设是相似的听众会喜欢相似的音乐。矩阵分解把这个假设建模成两个低维向量一个用户向量、一个歌曲向量二者点积得到预测评分。这背后没有黑匣子训练过程每一步都可解释。3.2 PyTorch 实现矩阵分解模型先实现一个带偏置项的矩阵分解模型import torch import torch.nn as nn class MatrixFactorization(nn.Module): def __init__(self, num_users, num_items, embed_dim64): super().__init__() self.user_emb nn.Embedding(num_users, embed_dim) self.item_emb nn.Embedding(num_items, embed_dim) # 偏置项捕捉用户/歌曲自身的评分倾向 self.user_bias nn.Embedding(num_users, 1) self.item_bias nn.Embedding(num_items, 1) def forward(self, user_ids, item_ids): u self.user_emb(user_ids) i self.item_emb(item_ids) pred (u * i).sum(dim-1) pred pred self.user_bias(user_ids).squeeze() pred pred self.item_bias(item_ids).squeeze() return predEmbedding 维度 embed_dim 是第一个关键参数。64 维是稳妥起点数据量大可以调到 128数据量小建议 32。为什么这么说维度越高表达力越强但训练参数也越多在几十万条交互数据的规模下128 维以上很容易见过拟合。偏置项的作用是建模「有些用户天生评分高、有些歌曲天生容易被听」这类全局因素不加偏置的模型预测值会过于依赖向量点积收敛后误差通常更大。配合矩阵分解还需要一个训练数据迭代器。常见的做法是把样本构造成三元组uid, sid, label用 TensorDataset 包起来from torch.utils.data import TensorDataset, DataLoader uid torch.tensor(dataset[uid].values, dtypetorch.long) sid torch.tensor(dataset[sid].values, dtypetorch.long) label torch.tensor(dataset[label].values, dtypetorch.float32) train_ds TensorDataset(uid, sid, label) train_loader DataLoader(train_ds, batch_size256, shuffleTrue)这里 batch_size256 是常见默认值。如果显存不够先降到 128 而不是去调小模型如果训练速度慢先升到 512 试一下。shuffleTrue 保证每个 epoch 内样本顺序不同防止模型学到批次顺序的假关联。3.3 用 GRU 捕捉用户听歌的顺序节奏矩阵分解的局限在于它把用户的所有行为压缩成一个静态向量忽略了你先听摇滚再听民谣的顺序信息。RNN 系列的模型可以捕捉这种时序模式毕设里最常用的实现是 GRU因为它比 LSTM 少一个门控训练更快参数更少在小数据上不容易过拟合。如果你的播放日志里有时间戳把每个用户的听歌序列按时间排序切成固定长度的滑窗就能训练序列模型。class GRURecommender(nn.Module): def __init__(self, num_items, embed_dim64, hidden_dim128, num_layers1): super().__init__() self.item_emb nn.Embedding(num_items, embed_dim) self.gru nn.GRU( embed_dim, hidden_dim, num_layersnum_layers, batch_firstTrue ) self.out nn.Linear(hidden_dim, num_items) def forward(self, seq): # seq: [batch, seq_len]值是歌曲ID emb self.item_emb(seq) # [batch, seq_len, embed_dim] _, hidden self.gru(emb) # hidden: [1, batch, hidden_dim] last_hidden hidden[-1] # [batch, hidden_dim] logits self.out(last_hidden) # [batch, num_items] return logits这个模型把用户最近听过的 N 首歌编码成一个隐藏状态向量然后通过全连接层映射到所有歌曲的得分。得分最高的几首歌就是下一首推荐候选。hidden_dim128 是我在这个规模下通常用的值改到 256 不一定带来指标提升但训练时间会明显变长。关于全连接层输出维度等于 num_items 的问题当歌曲总数超过十万这个线性层的参数量会非常大常见做法是限制候选集只对用户最可能感兴趣的几百首歌曲计算得分或者用采样 softmax。4. 训练与调参实战loss 曲线、早停与超参数边界4.1 按用户切分数据防止信息泄漏模型训练前先要切分数据集。这里有个毕设里非常常见的错误直接对全部样本随机切分同一个用户的听歌记录会同时出现在训练集和验证集。这样验证集指标会虚高因为模型已经看过该用户的偏好答辩时评委一旦追问「你的验证集用户是不是在训练集里出现过」答不上来就很被动了。正确做法是按用户维度切分把所有用户随机分成三份80% 用户的全部记录进训练集10% 用户进验证集10% 用户进测试集。users dataset[[uid]].drop_duplicates() users users.sample(frac1, random_state42).reset_index(dropTrue) n_train int(len(users) * 0.8) n_val int(len(users) * 0.1) train_users users.iloc[:n_train][uid] val_users users.iloc[n_train:n_train n_val][uid] test_users users.iloc[n_train n_val:][uid] train_set dataset[dataset[uid].isin(train_users)] val_set dataset[dataset[uid].isin(val_users)] test_set dataset[dataset[uid].isin(test_users)]切分完之后看一眼三份数据的正负样本比例如果偏差过大要在各自集合里补采样。这个步骤很多人会跳过但它直接影响后续早停判断是否可靠。验证集里正样本太少loss 波动就会很大早停很容易误判。4.2 训练循环与早停实现矩阵分解和 GRU 的训练循环大同小异区别只在数据格式。以矩阵分解为例import torch.optim as optim model MatrixFactorization( num_usersdataset[uid].nunique(), num_itemsdataset[sid].nunique(), embed_dim64 ) criterion nn.BCEWithLogitsLoss() optimizer optim.Adam(model.parameters(), lr1e-3) def train_epoch(model, loader, optimizer, criterion): model.train() total_loss 0.0 for u, s, label in loader: optimizer.zero_grad() pred model(u, s) loss criterion(pred, label) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) def evaluate(model, loader, criterion): model.eval() total_loss 0.0 with torch.no_grad(): for u, s, label in loader: pred model(u, s) loss criterion(pred, label) total_loss loss.item() return total_loss / len(loader)有一处细节容易忽略模型输出的是未经 sigmoid 的 logit所以损失函数用 BCEWithLogitsLoss而不是 BCELoss。BCEWithLogitsLoss 内部会把 sigmoid 和 BCE 合并计算数值稳定性更好。如果你在训练代码里发现 loss 一直在 0.69 附近徘徊ln2 的值说明模型输出接近 0大概率是负样本比例失衡或者学习率不合适。早停的实现要让验证集参与决策best_loss float(inf) patience 3 bad_epochs 0 for epoch in range(30): tr_loss train_epoch(model, train_loader, optimizer, criterion) val_loss evaluate(model, val_loader, criterion) print(fepoch {epoch:02d} | train {tr_loss:.4f} | val {val_loss:.4f}) if val_loss best_loss: best_loss val_loss bad_epochs 0 torch.save(model.state_dict(), best_model.pt) else: bad_epochs 1 if bad_epochs patience: print(early stop at epoch, epoch) breakpatience3 的意思是连续 3 个 epoch 验证集 loss 没有下降就停。毕设场景下 30 个 epoch 上限够用了如果你的模型在 30 轮内还没有收敛趋势通常不是训练轮数不够而是前面的数据清洗或负采样有问题。4.3 超参数边界表embedding 维度、学习率与 batch size把常用的调参范围整理成一张表训练时对照着选超参数推荐范围初始值调参方向embed_dim32 / 64 / 12864数据量大且欠拟合就往 128 调过拟合就回 32hidden_dim (GRU)64 / 128 / 256128优先级低于 embed_dim不是瓶颈就先不动learning_rate1e-4 ~ 3e-31e-3loss 震荡就降收敛太慢且 loss 高可以试 3e-3batch_size128 / 256 / 512256显存不够降训练慢且 loss 平滑可升负样本比例1:1 ~ 1:51:1推荐全是热门歌时往 1:3 调patience3 / 53验证集 loss 波动大时升到 5这里特别说一下学习率。Adam 里 1e-3 是通用起点但 loss 出现「前期下降、后期震荡不降」时常见做法是先调低学习率而不是换优化器。SGD 在这个任务里收敛速度明显慢于 Adam不建议作为首选。如果你想体现对优化器的理解可以在文档里说明「使用 Adam 的默认超参数 beta10.9、beta20.999但将 weight_decay 设为 1e-5 来抑制过拟合」这个细节比换优化器更能体现调试能力。5. 避坑指南音乐推荐系统最常见的五个翻车点5.1 训练 loss 不降预测结果接近随机现象训练好几个 epochloss 一直不下降或者卡在 0.69 附近偶尔下降一点又弹回去。原因最常见的是学习率过大导致 loss 震荡其次是模型没有做归一化导致梯度爆炸还有可能是标签类型不对label 用整数 0/1 喂给 BCEWithLogitsLoss但张量类型是 longPyTorch 不会报错但计算梯度会出现问题。解决先把学习率降到 1e-4 试跑 20 个 epoch看 loss 有没有稳定的下降趋势同时检查 label 张量是否转了 floatmodel 输入输出维度是否匹配。如果还不行打印一个 batch 的预测值看是不是集中在 0 附近如果是检查 Embedding 初始化和正负样本比例。5.2 推荐结果全是热门歌个性化程度很差现象无论给哪个用户推荐top 列表里都是那几首播放量最高的歌。原因热门歌本身在训练集中出现频率高模型学到的向量范数偏大点积得分天然偏高。如果负样本采样不足模型会更倾向于把所有歌都预测成正样本。还有一个常见原因是评价指标只看准确率而热门歌恰好能把准确率拉高。解决负样本比例从 1:1 提高到 1:3训练时对热门歌曲做降采样或者用「已在历史中播放的歌曲屏蔽」策略推荐时将用户已经听过的歌曲排除答辩时用覆盖率指标配合精确率一起说明证明推荐结果不只是热门榜。5.3 数据集全部加载进内存训练到一半进程被杀现象训练跑到第二个 epochCPU 内存占用率逼近 100%进程直接被操作系统杀掉。原因代码把所有用户的交互序列一次性构造成了 Python 列表或 DataFrame加上 PyTorch 的 DataLoader 加载时又拷贝了一份内存翻倍。几万用户没问题到了几十万用户就撑不住了。解决改用 PyTorch 的 Dataset 类每次只加载一个批次的数据滑动窗口切分不要提前全部生成而是在getitem里动态切。这类改动工作量不大但能把数据规模上限提升一个数量级。答辩时这也是一个值得讲的工程点。5.4 Flask 后端和前端页面联调时接口 404现象模型训练好之后启动 Flask 服务打开前端页面点击某个用户发起推荐请求浏览器 Network 面板显示 404。原因前端静态资源的路径没有配置对Flask 默认的 static_folder 在项目根目录的 static 下而资源包里 CSS 文件放在 assets 目录或者路由装饰器里的路径和前端 fetch 的 URL 对不上。解决启动 Flask 时显式指定 static_folder 参数比如 app Flask(name, static_folder../frontend)前端 fetch 请求的路径要和后端路由完全一致最好打印请求 URL 逐一比对。这个坑在联调阶段至少能消耗半天时间建议先把接口用 curl 或者 Postman 验证一遍再对接前端。5.5 离线指标很高实际听感却很差现象验证集上精确率 70% 以上但人工点开推荐列表发现推荐的歌曲风格单一或者全是老歌。原因离线指标计算的是「模型在已观察数据上的预测能力」而用户真实偏好是会漂移的。如果训练数据里老歌占多数模型自然偏向推荐老歌。另一个原因是用同一批用户做验证集和测试集指标虚高。解决验证集按时间切分用「前 80% 时间段的用户行为训练后 20% 时间段做验证」这样评估的是模型的预测未来能力而不是记忆能力。这是我花了很长时间才琢磨明白的一点离线指标与实际体验的差距本质上是数据分布假设的差距不解决这个调再多的 embedding 维度都是白搭。6. 部署与验证把模型变成 Flask API用离线指标量化推荐效果6.1 Flask 推荐接口与前端对接训练好的模型需要提供服务。最常见做法是 Flask 封装一个 GET 接口传入用户 ID 返回推荐歌曲列表。注意加载模型时要先实例化模型结构再 load_state_dict否则会报参数不匹配。from flask import Flask, request, jsonify app Flask(__name__, static_folder../frontend, static_url_path) # 读取歌曲ID到歌曲信息的映射 song_info pd.read_csv(songs.csv).set_index(sid) song_info[title] song_info[title].fillna(unknown) def recommend(user_id, top_k10): # 这里用矩阵分解模型给出的向量内积计算得分 all_sids torch.arange(len(song_info)) uid_tensor torch.tensor([user_id] * len(all_sids)) with torch.no_grad(): scores model(uid_tensor, all_sids) top_idx scores.topk(top_k).indices.tolist() return [song_info.loc[i, title] for i in top_idx] app.route(/api/recommend/int:user_id) def api_recommend(user_id): songs recommend(user_id) return jsonify({user_id: user_id, songs: songs})这段代码的关键点在于接口里一次对所有歌曲打分然后取 topk这个操作在几万首歌的规模下毫秒级完成完全够用。如果歌曲数上了百万就要先粗筛候选集再做精排序不过毕设场景基本不会遇到。6.2 用 Precisionk 和覆盖率验证效果部署完还要有数据支撑。三个指标最常用Precisionk 评估推荐列表里有多少被用户实际听过Recallk 评估用户听过的歌有多少进了推荐列表覆盖率评估推荐列表涉及了多少比例的不同歌曲。def precision_at_k(recommended, actual, k10): rec set(recommended[:k]) act set(actual) return len(rec act) / k def recall_at_k(recommended, actual, k10): rec set(recommended[:k]) act set(actual) return len(rec act) / len(act) if act else 0.0 def coverage(recommended_all, num_items): rec_items set() for rec in recommended_all: rec_items.update(rec) return len(rec_items) / num_items指标算完还要会解释。Precisionk 高不代表系统好如果热门榜本身就能拿到 0.3 的精确率你的模型要做到 0.4 以上才有说服力覆盖率低说明推荐集中在头部歌曲这时候要回到第 5 章的负采样和降采样策略去调。这套资源包里从前端展示到后端接口的代码框架都齐了我自己的习惯是每跑通一个阶段就把当时的 loss 曲线和指标截图存下来答辩 PPT 里按时间顺序放出来比只贴一张最终结果有说服力得多。从那以后我每次训练推荐模型都强制走一遍按用户切分验证集、负采样比例检查、冷启动用户回退三个固定动作能挡住至少一半的翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表