深度学习音乐推荐系统Django落地:从模型训练到REST服务封装

发布时间:2026/9/16 14:09:55
深度学习音乐推荐系统Django落地:从模型训练到REST服务封装 简介这是一份面向深度学习与音乐推荐方向研究者和开发者的课程设计项目基于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_dim64): 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 的StandardScalerfrom 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_length200) artist models.CharField(max_length100, db_indexTrue) genre models.CharField(max_length50, db_indexTrue) duration models.IntegerField() # 单位秒 release_year models.IntegerField(nullTrue) embedding models.JSONField(defaultlist) # 可选直接存训练好的向量 class Meta: indexes [ models.Index(fields[genre, release_year]), ] class Interaction(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameinteractions) song models.ForeignKey(Song, on_deletemodels.CASCADE, related_nameinteractions) play_count models.IntegerField(default1) liked models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, song)这三个模型是最小可用集合User是 Django 自带的用户模型Song存歌曲元数据Interaction记录用户对每首歌的行为。embedding字段要不要存取决于做不做在线向量召回——如果推荐服务每次都要实时计算用户向量与全量歌曲向量的相似度把歌曲向量缓存到数据库里会快很多如果只在夜间批量预计算推荐结果这个字段可以省略。Interaction用unique_together约束用户与歌曲唯一每次更新时做update_or_create而不是直接createfrom django.db.models import F Interaction.objects.update_or_create( useruser, songsong, defaults{play_count: F(play_count) 1} )这段代码用F()表达式做自增避免先查再写的竞态条件。高并发场景下两个请求同时读到play_count5各自加一后写回 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_locationself.device)) self.model.eval() def recommend_for_user(self, user_id, top_n10): # 实体化之前把歌曲特征拼成批量张量 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_pathsettings.RECOMMEND_MODEL_PATH) class RecommendView(APIView): def get(self, request): user_id request.user.id if not user_id: return Response({error: unauthorized}, status401) 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_ntop_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_neg4): 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按用户分组计算排序能力衡量模型是否把用户喜欢的歌排前面覆盖率RecallK统计推荐结果覆盖的歌曲多样性衡量推荐列表是不是只推头部热门歌AUC 适合做模型选型但不适合衡量推荐列表的最终体验。两组模型 AUC 数值相同覆盖率可能差别很大——一个只推排行榜前五十的歌另一个覆盖了长尾歌曲对用户留存的影响完全不同。from sklearn.metrics import roc_auc_score # y_true 是每个样本的真实标签0/1 # y_score 是模型输出的得分 # 按用户分组计算 AUC再取平均得到 GAUC def compute_gauc(df, user_coluser_id, label_collabel, score_colscore): 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, modemax, factor0.5, patience3 )modemax表示监控的指标越高越好这里监控验证集 GAUC。patience3表示连续 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_n20): from django.db.models import Count return ( Song.objects .annotate(interaction_countCount(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(hostlocalhost, port6379, db0) def get_recommendations_with_cache(user_id, top_n10): key frecommend:{user_id}:{top_n} cached cache.get(key) if cached: return json.loads(cached) song_ids recommender.recommend_for_user(user_id, top_ntop_n) cache.setex(key, 900, json.dumps(song_ids)) return song_idssetex的过期时间是 900 秒对应 15 分钟。缓存过期后下一个请求触发一次模型推理并重新写缓存对数据库和模型服务的压力都控制在可接受范围内。5.3 批量预计算与模型热更新如果歌曲数量在十万量级在线逐用户推理仍然吃不住。更稳妥的做法是每天晚上跑一个定时任务为所有活跃用户批量计算推荐列表结果写入数据库。这一步可以用 Celery 定时任务实现也可以用 shell manage.py 自定义命令实现。前者有任务队列、失败重试和监控界面后者足够处理万级用户场景少一层组件就少一个故障点。模型更新时把旧缓存清掉重新触发预计算用户请求立刻走新模型。模型文件的版本管理建议用时间戳命名部署脚本里固定到具体路径这样回滚时只需要改配置目录。另外分布式推理环境下 PyTorch 的浮点数运算在不同设备上可能会有细微偏差建议在模型服务层统一用同一精度float32加载避免召回列表出现个位数级别的跳动。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询