简介:这是一份面向深度学习与音乐推荐方向研究者和开发者的课程设计项目,基于Django框架实现了一套结合自动编码器与卷积神经网络的音乐推荐系统,可应用于毕业设计、课设演示或算法复现。资源共226个文件,压缩包约95MB,涵盖Python源码、模型文件、音频数据、前端页面、系统文档与研究论文等资料,结构清晰便于按模块查阅。目前已有175人学习下载,适合具备一定Python和深度学习基础的读者深入学习。项目中实现了随机梯度下降、K近邻、协同过滤、词袋模型和词向量等关键技术,并尝试将内容特征与协同过滤结合为紧耦合模型;配套数据集与算法说明可帮助读者完整走通数据处理、特征提取、模型训练到音乐推荐的全流程,对研究混合推荐机制具有较高参考价值。
1. 深度学习音乐推荐系统(Django)的落地路线
多数人拿到"基于深度学习的音乐推荐方法研究系统(django)"这类项目,第一反应是去调模型结构,但把训练好的推荐引擎接进 Web 服务、让用户能看到"猜你喜欢"那一栏之后,才发现真正的瓶颈在数据流水线和模型服务的衔接上。这个标题拆开看,是两条技术线的交汇:深度学习的向量化召回与排序,Django 的模型定义、ORM 查询和 REST 接口。适合两类人:一类是刚结束深度学习课程、想拿推荐系统落地一个完整项目的研究生,另一类是已经写过 Django 业务系统、希望引入深度模型但不知道从哪一层切入的工程师。读完之后你会有能力自己决定哪一层用深度学习、哪一层继续用规则。
2. 深度学习音乐推荐:模型结构与特征工程
2.1 为什么先打通矩阵分解,再引入深度模型
推荐系统领域有一个常见误区:拿到用户行为数据就直接套深度模型,结果在训练集上指标很好看,线上却连规则版协同过滤都打不过。音乐推荐的交互数据是典型的隐式反馈——播放、跳过、收藏、重复播放,没有评分,稀疏度和噪声都很高。深度模型虽然能拟合复杂非线性关系,但数据量不够时反而过拟合到几种头部歌曲上。
我的做法是先把矩阵分解作为基线和冷启动兜底。矩阵分解把用户与物品的交互矩阵拆成两个低维矩阵的乘积,每个用户和每首歌对应一个固定维度的隐向量。在音乐场景里,这个隐向量可以粗略理解为"口味向量",维度通常取 64 或 128。矩阵分解的优点是训练稳定、收敛快,线上做点积就是一次循环,吞吐量高。
深度模型的引入要解决矩阵分解解决不了的问题:特征交叉和内容特征。矩阵分解只看到"谁和谁交互过",看不到歌曲的音频特征、歌词主题、发布时间以及用户的听歌时段。深度学习可以把这些信息编码进同一个向量空间,让推荐结果不只是"和你口味像的人听的歌",还带上了"你最近常听的风格"这类时间敏感信号。
2.2 可复现的深度推荐结构:Embedding + MLP
我在这里不罗列十种模型,只给出一个在音乐推荐场景下最稳定、改造成本最低的结构:Embedding 层把离散特征映射为稠密向量,拼接后过两层 MLP 输出得分。这个结构在论文里通常叫 Wide & Deep 或 Deep Crossing 的简化版,区别在于特征交叉的处理方式。
import torch import torch.nn as nn class MusicDeepRec(nn.Module): def __init__(self, num_users, num_items, num_genres, embed_dim=64): super().__init__() self.user_emb = nn.Embedding(num_users, embed_dim) self.item_emb = nn.Embedding(num_items, embed_dim) self.genre_emb = nn.Embedding(num_genres, embed_dim) # 用户、歌曲、风格三者向量拼接后过两层全连接 self.mlp = nn.Sequential( nn.Linear(embed_dim * 3, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, user_id, item_id, genre_id): u = self.user_emb(user_id) i = self.item_emb(item_id) g = self.genre_emb(genre_id) # 拼接之后过 MLP,输出一个非归一化的交互得分 concat_vec = torch.cat([u, i, g], dim=-1) return self.mlp(concat_vec).squeeze(-1)这段代码里最值得注意的地方是squeeze(-1)。self.mlp的输出形状是(batch, 1),squeeze(-1)把它压成(batch,),后面接 BCEWithLogitsLoss 时形状才对得上。很多人第一次跑报错,就是漏了这一步,Loss 计算时对不上维度。
Dropout(0.3)的位置也有讲究。放在第一个线性层和 ReLU 之后,作用是在特征交叉之前随机丢弃一部分神经元,防止模型记住个别用户的极端行为。音乐推荐里有个典型现象:少量用户大量刷某一风格的全部作品,如果不做 dropout,模型很容易学成"这个用户只推这个风格"。
embed_dim的选择和交互数据量直接相关。用户数一万、歌曲数五万、交互记录五十万条量级时,64 维表现最稳。把维度加到 256,模型参数量会涨到 64 维的 16 倍,但 AUC 提升通常不到 1 个百分点,训练时间却翻几倍。所以建议是:先 64 起步,训练收敛后看验证集指标再决定要不要加。
2.3 音乐特征的取舍与归一化
音乐推荐的特征工程,我一般只保留三类,类别不要贪多:
| 特征类别 | 具体特征 | 编码方式 | 说明 |
|---|---|---|---|
| 用户侧 | user_id, 注册天数, 每日听歌时长 | one-hot + 数值归一化 | 体现活跃度差异 |
| 物品侧 | item_id, 歌曲时长, 发布时间 | one-hot + 数值归一化 | 发布时间影响热度 |
| 交互侧 | 最近7天播放次数, 收藏数 | 数值归一化 | 短期兴趣信号 |
音频特征(MFCC、色度图)在论文里经常出现,但实际项目中采样率、特征维度、实时提取管道都会增加一整套工程负担。如果训练数据不是直接从音频文件生成的,建议第一版不要碰音频特征,先把文本特征(歌名、歌词关键词)和元数据(语种、年代)加进去就足够了。
特征处理的另一个关键动作是归一化。数值型特征不归一化直接进模型,会导致 MLP 前几层的梯度被大数值特征主导。常见做法是用 sklearn 的StandardScaler:
from sklearn.preprocessing import StandardScaler # 假设 features 是 shape (N, 5) 的 numpy 数组 scaler = StandardScaler() scaled = scaler.fit_transform(features)注意fit_transform只在训练集上使用,验证集和测试集用scaler.transform复用同一套均值和标准差。顺手对全量数据做fit_transform会造成信息泄露,因为验证集的分布信息已经进入了训练时的归一化参数,评估出来的指标会比线上真实表现略高。
3. Django 端的推荐数据建模与服务封装
3.1 用 Django ORM 定义用户、歌曲和交互日志
Django 在这个项目中承担的角色是数据持久化、业务逻辑和接口暴露。数据模型的设计直接决定了推荐系统能拿到什么特征。
# music/models.py from django.db import models from django.contrib.auth.models import User class Song(models.Model): title = models.CharField(max_length=200) artist = models.CharField(max_length=100, db_index=True) genre = models.CharField(max_length=50, db_index=True) duration = models.IntegerField() # 单位秒 release_year = models.IntegerField(null=True) embedding = models.JSONField(default=list) # 可选:直接存训练好的向量 class Meta: indexes = [ models.Index(fields=['genre', 'release_year']), ] class Interaction(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='interactions') song = models.ForeignKey(Song, on_delete=models.CASCADE, related_name='interactions') play_count = models.IntegerField(default=1) liked = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'song')这三个模型是最小可用集合:User是 Django 自带的用户模型,Song存歌曲元数据,Interaction记录用户对每首歌的行为。embedding字段要不要存,取决于做不做在线向量召回——如果推荐服务每次都要实时计算用户向量与全量歌曲向量的相似度,把歌曲向量缓存到数据库里会快很多;如果只在夜间批量预计算推荐结果,这个字段可以省略。
Interaction用unique_together约束用户与歌曲唯一,每次更新时做update_or_create而不是直接create:
from django.db.models import F Interaction.objects.update_or_create( user=user, song=song, defaults={'play_count': F('play_count') + 1} )这段代码用F()表达式做自增,避免先查再写的竞态条件。高并发场景下,两个请求同时读到play_count=5,各自加一后写回 6,实际应该计两次,用F()可以把自增操作下推到数据库执行,不会丢更新。
3.2 把训练好的深度模型封装为推荐服务
模型训练好之后,不能把 PyTorch 的模型对象直接丢给 Django 的视图函数。常见做法是单独建一个services/目录,把推荐逻辑和视图层分开。
# services/recommender.py import torch import numpy as np from django.conf import settings from music.models import Song class RecommenderService: def __init__(self, model_path): self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 这里按之前定义的 MusicDeepRec 结构先实例化,再加载权重 self.model = MusicDeepRec(num_users=..., num_items=..., num_genres=...) self.model.load_state_dict(torch.load(model_path, map_location=self.device)) self.model.eval() def recommend_for_user(self, user_id, top_n=10): # 实体化之前,把歌曲特征拼成批量张量 song_data = Song.objects.values_list('id', 'genre_id') song_ids = [row[0] for row in song_data] genre_ids = [row[1] for row in song_data] user_tensor = torch.LongTensor([user_id] * len(song_ids)).to(self.device) item_tensor = torch.LongTensor(song_ids).to(self.device) genre_tensor = torch.LongTensor(genre_ids).to(self.device) with torch.no_grad(): scores = self.model(user_tensor, item_tensor, genre_tensor) # scores 是按歌曲顺序排列的得分数组 top_indices = np.argsort(scores.cpu().numpy())[::-1][:top_n] return [song_ids[i] for i in top_indices]这段代码把歌曲全量做成三个张量,一次完成前向传播,而不是在 Python 里逐条调用模型。音乐推荐场景下歌曲数量即使到十万,一次前向传播的耗时也远小于万次循环。self.model.eval()在推理前必须调用,它会关闭 dropout 和 batch norm 的训练行为。很多人训练时指标正常,部署后结果漂移,十有八九是漏了eval()。
recommend_for_user里还有一个细节:每一轮请求都做全量推理,性能仍有瓶颈。不要试图在视图层优化这一点,正确的承接方式是缓存推荐结果,或者夜间批量预计算,这两种方案都会在第 5 章展开。
3.3 用 REST 接口暴露推荐能力
Django 提供两种接口方式:纯视图函数 + JsonResponse,或者 Django REST Framework。中小型项目建议直接使用 DRF,省去手写序列化和参数校验的时间。
# api/views.py from rest_framework.views import APIView from rest_framework.response import Response from services.recommender import RecommenderService recommender = RecommenderService(model_path=settings.RECOMMEND_MODEL_PATH) class RecommendView(APIView): def get(self, request): user_id = request.user.id if not user_id: return Response({'error': 'unauthorized'}, status=401) top_n = int(request.query_params.get('top_n', 10)) top_n = max(1, min(top_n, 50)) # 限制范围,避免大拓扑垮数据库 song_ids = recommender.recommend_for_user(user_id, top_n=top_n) return Response({'song_ids': song_ids})RecommenderService在模块加载时实例化一次,避免每个请求都重新加载模型权重。模型文件大的话,加载时间动辄几百毫秒,每个请求都加载一次会让接口性能极差。
top_n的限制也有实际意义。推荐接口被前端频繁调用,如果不限制范围,一次请求返回几百个歌曲 ID 对带宽、序列化和前端渲染都是浪费。实践中用户最多翻到第 50 个就足够。
4. 音乐推荐模型训练、评估与调参
4.1 训练数据的组织与负采样
隐式反馈数据的训练集构造和显式评分不同。用户只告诉系统"我听过哪首歌",没有说"我不喜欢哪首歌"。负样本要人工构造,这个过程叫负采样。
import random def sample_negatives(user_positive_songs, all_song_ids, num_neg=4): positives = set(user_positive_songs) candidates = [sid for sid in all_song_ids if sid not in positives] return random.sample(candidates, num_neg)负采样策略直接影响模型表现。全局随机采样最简单,但会让模型学到"热门歌不好"这种错误信号;只采样热门歌做负样本,模型又容易过度惩罚流行度。一个折中的方案是全量随机采样与热门负样本按 7:3 混合采样,同时把歌曲的流行度作为特征喂给模型,让模型自己学出流行度与交互概率之间的关系。
拿到正负样本后,将整个数据集组织成 PyTorch 的 Dataset 和 DataLoader,训练循环保持常规写法:
from torch.utils.data import Dataset, DataLoader import torch.nn as nn class InteractionDataset(Dataset): def __init__(self, user_ids, item_ids, genre_ids, labels): self.user_ids = user_ids self.item_ids = item_ids self.genre_ids = genre_ids self.labels = labels def __len__(self): return len(self.labels) def __getitem__(self, idx): return ( torch.LongTensor([self.user_ids[idx]]), torch.LongTensor([self.item_ids[idx]]), torch.LongTensor([self.genre_ids[idx]]), torch.FloatTensor([self.labels[idx]]) ) criterion = nn.BCEWithLogitsLoss()BCEWithLogitsLoss在内部计算 sigmoid 和交叉熵,比手动加 Sigmoid 再算 BCE 数值更稳定。PyTorch 官方推荐这种方式。
4.2 离线评估指标怎么选
离线评估推荐系统,只看准确率容易误导。音乐推荐的评估指标我习惯分成两组,同时报告:
| 指标组 | 指标 | 计算方式 | 适用场景 |
|---|---|---|---|
| 排序质量 | AUC, GAUC | 按用户分组计算排序能力 | 衡量模型是否把用户喜欢的歌排前面 |
| 覆盖率 | Recall@K | 统计推荐结果覆盖的歌曲多样性 | 衡量推荐列表是不是只推头部热门歌 |
AUC 适合做模型选型,但不适合衡量推荐列表的最终体验。两组模型 AUC 数值相同,覆盖率可能差别很大——一个只推排行榜前五十的歌,另一个覆盖了长尾歌曲,对用户留存的影响完全不同。
from sklearn.metrics import roc_auc_score # y_true 是每个样本的真实标签(0/1) # y_score 是模型输出的得分 # 按用户分组计算 AUC,再取平均得到 GAUC def compute_gauc(df, user_col='user_id', label_col='label', score_col='score'): user_groups = df.groupby(user_col) aucs = [] for _, group in user_groups: if len(group) < 2 or group[label_col].nunique() < 2: continue aucs.append(roc_auc_score(group[label_col], group[score_col])) return sum(aucs) / len(aucs) if aucs else 0roc_auc_score要求同一组里同时存在正样本和负样本,如果负采样做得不充分,很多用户组会被跳过,GAUC 算出来虚高。所以负采样时每用户至少保证 2 个负样本,是评估能跑起来的前提。
4.3 三个优先调的参数
第一个是学习率。Embedding + MLP 这种结构,初始学习率设在1e-3附近。如果 loss 震荡,把学习率除以 10 继续观察。PyTorch 里配合ReduceLROnPlateau调度器,验证集指标连续几个 epoch 不涨时自动降学习率。
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='max', factor=0.5, patience=3 )mode='max'表示监控的指标越高越好,这里监控验证集 GAUC。patience=3表示连续 3 个 epoch 不提升才降学习率,太小的 patience 会导致学习率过早下降、模型停在前一个局部点。
第二个是负样本数量。num_neg在 2 到 8 之间调整。数量太少,模型区分力不够;数量太多,训练数据膨胀,每 epoch 耗时变长,而且负样本比例严重偏离真实场景。我用得最多的是 4,效果稳定。
第三个是 embedding 维度。之前说过从 64 开始,数据量过百万级再考虑 128。维度不是越大越好,过大的 embedding 维度在隐式反馈场景下容易把模型训练成记忆用户行为,而不是泛化出用户兴趣。
5. Django 推荐系统收尾:冷启动、缓存和模型更新
5.1 冷启动场景的处理
新用户没有任何交互历史,Embedding 层没有更新过,模型输出的得分没有参考价值。常见的兜底方案是按歌曲的全局热度排序,热度可以用播放次数做归一化后作为最终分数。
# services/cold_start.py def hot_songs(top_n=20): from django.db.models import Count return ( Song.objects .annotate(interaction_count=Count('interactions')) .order_by('-interaction_count')[:top_n] )除了热度兜底,还可以做基于注册时选择的偏好标签来生成初始推荐列表,比如让用户勾选喜欢的三到五个歌手,把这些歌手的歌曲按热度排序后混合,作为首次登录的推荐列表,效果通常好过纯全局热度。
5.2 用 Redis 缓存推荐结果
推荐的耗时不在于数据库查询,而在于模型推理。线上每一秒可能有上百个请求,每个请求都跑一次模型前向传播不现实。用 Redis 缓存推荐结果,缓存 key 按recommend:{user_id}:{top_n}设计,过期时间设为 15 分钟。
import redis import json cache = redis.Redis(host='localhost', port=6379, db=0) def get_recommendations_with_cache(user_id, top_n=10): key = f"recommend:{user_id}:{top_n}" cached = cache.get(key) if cached: return json.loads(cached) song_ids = recommender.recommend_for_user(user_id, top_n=top_n) cache.setex(key, 900, json.dumps(song_ids)) return song_idssetex的过期时间是 900 秒,对应 15 分钟。缓存过期后,下一个请求触发一次模型推理并重新写缓存,对数据库和模型服务的压力都控制在可接受范围内。
5.3 批量预计算与模型热更新
如果歌曲数量在十万量级,在线逐用户推理仍然吃不住。更稳妥的做法是每天晚上跑一个定时任务,为所有活跃用户批量计算推荐列表,结果写入数据库。这一步可以用 Celery 定时任务实现,也可以用 shell + manage.py 自定义命令实现。前者有任务队列、失败重试和监控界面;后者足够处理万级用户场景,少一层组件就少一个故障点。
模型更新时,把旧缓存清掉,重新触发预计算,用户请求立刻走新模型。模型文件的版本管理建议用时间戳命名,部署脚本里固定到具体路径,这样回滚时只需要改配置目录。另外分布式推理环境下 PyTorch 的浮点数运算在不同设备上可能会有细微偏差,建议在模型服务层统一用同一精度(float32)加载,避免召回列表出现个位数级别的跳动。
本文还有配套的精品资源,点击获取