资讯动态

Django音乐推荐系统实战:从数据模型到在线召回

发布时间:2026/10/13 15:17:31 来源:尧图企业网站定制
简介这份资源是一套基于Django框架实现的音乐推荐系统完整源码面向具备Python与Web开发基础、希望深入理解协同过滤推荐算法的开发者与学习者。项目采用传统MVC架构内置UserCF、ItemCF与LFM三种推荐算法并以ItemCF作为主推荐策略核心思路是当多个用户同时偏好项目A与项目B时判定两者具有较高相似度再通过相似度公式完成推荐计算。资源包共83个文件以49个py源码文件为主体辅以14个html模板、4个css与3个js前端资源以及png、jpg图片和txt、md说明文档整体约2.1MB涵盖recommend、forum、authen等应用模块与数据可视化相关代码。目前已有547人学习下载。借助这套代码读者可完整梳理Django项目结构、推荐算法落地流程与前后端交互方式并参考README与依赖配置快速运行调试适合作为课程设计、毕业设计或推荐系统入门实践的参考方案。1. 从零搭一套能跑起来的音乐推荐Django 在这个场景里到底扛了什么活很多人第一次听到「基于 Django 的音乐推荐」脑子里冒出来的画面是协同过滤、矩阵分解、深度学习排序模型然后顺手把 Django 归到「只是个写网页的框架」那一类。真动手做过一轮就会发现推荐效果好不好模型只占一部分剩下的大头全在工程链路上用户行为怎么落库、候选歌怎么召回、推荐结果怎么在请求周期内返回、冷启动用户第一次进来给他推什么。Django 在这里不是配角它承担的是「把离线算出来的推荐结果稳定、可缓存、可解释地送到前端」这件事同时还要扛住用户行为埋点的写入压力。这套方案适合谁如果你手上有一个中小规模的曲库几千到几十万首日活不算夸张团队里没有专门的推荐工程岗那用 Django 做主体框架、把推荐逻辑拆成「离线批算 在线召回」两层是性价比很高的路径。它不适合一上来就对标大厂级实时推荐但对绝大多数自建音乐站、校园电台、垂直音乐社区来说够用而且可控。下面我按实际搭建顺序把选型理由、表结构、召回逻辑、缓存策略和踩过的坑一条条讲清楚。2. 数据模型与行为埋点推荐系统的地基怎么打推荐系统的上限在数据模型定下来的那一刻就基本锁死了。我见过太多项目模型换了一茬又一茬效果就是上不去最后发现问题出在行为表设计得太糙——只记了「谁听了什么歌」没记时长、没记跳过、没记播放来源导致后面想区分「真喜欢」和「误点」都做不到。2.1 三张核心表用户、歌曲、行为先看最小可用的模型设计。用户表直接用 Django 自带的AbstractUser扩展歌曲表存元数据行为表是推荐的全部原料。# models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): # 扩展字段偏好标签用 JSON 存避免频繁改表 taste_tags models.JSONField(defaultdict, blankTrue) class Song(models.Model): title models.CharField(max_length200, db_indexTrue) artist models.CharField(max_length200, db_indexTrue) # 音频特征向量离线用音频模型抽好后存进来 feature_vector models.JSONField(nullTrue, blankTrue) # 标签如 [民谣, 舒缓, 女声] tags models.JSONField(defaultlist) duration models.PositiveIntegerField(default0) # 秒 play_count models.PositiveIntegerField(default0) class PlayRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) song models.ForeignKey(Song, on_deletemodels.CASCADE) # 关键记录实际播放时长用来判断是否真的听完 played_seconds models.PositiveIntegerField(default0) # 行为类型play / skip / like / collect action models.CharField(max_length16, defaultplay) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue) class Meta: indexes [ models.Index(fields[user, -created_at]), models.Index(fields[song, -created_at]), ]逻辑说明PlayRecord是整条推荐链路的燃料。played_seconds这个字段是血泪经验——没有它你无法区分「点开三秒就划走」和「完整听完」而这两类行为在偏好建模里权重天差地别。action字段把播放、跳过、点赞、收藏统一到一张表查询时用action过滤即可避免建四张表做 union。参数说明feature_vector用 JSON 存是为了灵活但如果你的曲库超过十万首、且要做向量相似度检索建议换成专门的向量字段或外接向量库JSON 字段做全表扫描会拖垮数据库。taste_tags同理小规模够用规模上来要拆成独立的标签关联表。2.2 埋点接口别让写行为拖垮主站行为写入是高频操作一个用户听歌过程中可能产生十几条记录。如果每条都同步写主库高峰期数据库连接会被打满。常见做法是走一个轻量的异步接口先写缓存队列再批量落库。# views.py import json from django.core.cache import cache from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt csrf_exempt def report_play(request): if request.method ! POST: return JsonResponse({ok: False}, status405) data json.loads(request.body) # 先塞进 Redis 列表key 按天分片避免单个 key 过大 key fplay_queue:{data[user_id]} cache.lpush(key, json.dumps({ song_id: data[song_id], seconds: data.get(seconds, 0), action: data.get(action, play), })) # 控制单用户队列长度防止异常客户端刷爆内存 cache.ltrim(key, 0, 999) return JsonResponse({ok: True})逻辑说明接口只做一件事——把行为快速塞进 Redis 列表立刻返回。真正的落库交给一个定时任务比如每 30 秒跑一次的 management command从队列里批量取出、批量bulk_create。这样主请求周期极短用户端无感。参数说明ltrim保留最近 1000 条是防御性设计正常用户一天产生不了这么多但异常客户端可能疯狂上报截断能保护内存。key按user_id分片而不是全局一个 key是为了让批量落库时能按用户聚合也避免单 key 热点。注意批量落库时一定要做去重和幂等。网络重试会导致同一条行为上报两次如果不去重热门歌曲的播放量会被刷虚进而污染推荐权重。3. 推荐召回离线批算 在线拼装的两层结构推荐系统最容易翻车的地方是把「算推荐」和「返回推荐」混在一个请求里。用户点开首页后端现场跑一遍协同过滤几百毫秒起步曲库一大直接超时。正确姿势是拆成两层离线把每个用户的候选集算好存起来在线只做过滤和排序。3.1 离线批算基于物品相似度的召回对中小曲库物品协同过滤ItemCF比用户协同过滤更稳因为歌曲的相似关系比用户的相似关系更稳定且可以预先算好。核心思路是两首歌被同一批用户喜欢它们就相似。# management/commands/build_recall.py from django.core.management.base import BaseCommand from django.core.cache import cache from collections import defaultdict from music.models import PlayRecord class Command(BaseCommand): def handle(self, *args, **options): # 只取「有效播放」时长超过 30 秒或标记为 like/collect records PlayRecord.objects.filter( played_seconds__gte30 ).values_list(user_id, song_id) # 构建 用户 - 歌曲集合 user_songs defaultdict(set) for uid, sid in records: user_songs[uid].add(sid) # 构建 歌曲 - 用户集合倒排 song_users defaultdict(set) for uid, songs in user_songs.items(): for sid in songs: song_users[sid].add(uid) # 计算物品相似度用余弦的简化版交集 / 并集开方 import math sim defaultdict(dict) song_ids list(song_users.keys()) for i, s1 in enumerate(song_ids): u1 song_users[s1] for s2 in song_ids[i1:]: u2 song_users[s2] inter len(u1 u2) if inter 0: continue score inter / math.sqrt(len(u1) * len(u2)) if score 0.05: # 阈值过滤避免长尾噪声 sim[s1][s2] score sim[s2][s1] score # 为每个用户生成 topN 召回写入缓存 for uid, songs in user_songs.items(): scores defaultdict(float) for sid in songs: for cand, s in sim.get(sid, {}).items(): if cand not in songs: # 过滤已听 scores[cand] s top sorted(scores.items(), keylambda x: -x[1])[:200] cache.set(frecall:{uid}, [sid for sid, _ in top], timeout86400)逻辑说明先过滤出「有效播放」这是保证相似度质量的前提。然后构建倒排索引两两计算相似度。这里用交集除以并集开方是余弦相似度在集合场景下的等价简化比直接算 Jaccard 更能反映共现强度。最后为每个用户聚合候选并取 top200 存缓存。参数说明played_seconds__gte30这个阈值要按你的曲库调整短歌 30 秒可能已经过半长歌 30 秒只是前奏更稳妥的做法是按歌曲时长的百分比来判定。相似度阈值0.05是经验值太低会引入噪声太高召回数量不够建议先用一批数据跑出来看分布再定。timeout86400表示召回结果缓存一天离线任务每天凌晨重跑一次即可。3.2 在线拼装过滤、打散、补冷启动在线接口拿到离线召回后不能直接返回要做三件事过滤掉用户最近听过的、把同歌手的歌打散、给新用户补冷启动内容。# views.py from django.core.cache import cache from django.db.models import Q from music.models import Song, PlayRecord def get_recommend(user, limit20): # 1. 取离线召回 recall_ids cache.get(frecall:{user.id}, []) # 2. 过滤最近 24 小时听过的 recent PlayRecord.objects.filter( useruser, created_at__gtetimezone.now() - timedelta(hours24) ).values_list(song_id, flatTrue) recent set(recent) candidates [sid for sid in recall_ids if sid not in recent] # 3. 打散同一歌手最多连续出现 2 首 songs Song.objects.in_bulk(candidates[:100]) result, artist_count [], {} for sid in candidates: song songs.get(sid) if not song: continue if artist_count.get(song.artist, 0) 2: continue result.append(song) artist_count[song.artist] artist_count.get(song.artist, 0) 1 if len(result) limit: break # 4. 冷启动召回不足时用热门歌补齐 if len(result) limit: hot Song.objects.order_by(-play_count)[:limit - len(result)] result.extend(hot) return result逻辑说明in_bulk一次性把候选歌曲查出来避免循环里逐条查库。打散逻辑用artist_count控制同歌手连续出现次数这是提升听感的关键——推荐列表里连着五首同一个人的歌用户会觉得系统很蠢。冷启动补齐用全站热门虽然粗糙但有效。参数说明candidates[:100]只取前 100 个候选做在线处理是为了控制单次请求的数据库压力离线召回存了 200 个留了余量。limit20是一屏的数量按前端实际展示调整。打散阈值2可以调曲库歌手集中度高的话放宽到 3。4. 避坑与排查那些让推荐效果原地踏步的细节这一章是我踩过的坑合集每条都按「现象 → 原因 → 解决」写你对照自己的项目排查。4.1 现象新用户首页永远推那几首热门歌原因冷启动逻辑只用了play_count排序而热门歌的播放量是历史累积的新歌永远排不上来导致推荐列表固化。解决热门补齐时引入时间衰减用「近 7 天播放量」而不是总播放量。可以在Song上加一个weekly_play_count字段由定时任务每天重算或者查询时用PlayRecord按时间窗口聚合。这样新歌有机会冒头列表也不会一成不变。4.2 现象离线任务跑一次要几个小时跑完还占满内存原因相似度计算用了双重循环遍历所有歌曲对曲库上万首就是上亿次比较且sim字典全量驻留内存。解决两个方向。一是对歌曲做聚类或分桶只在同桶内计算相似度把 O(n²) 降到可控范围二是相似度算完立刻分批写库或写缓存不要全量攒在内存里。如果曲库确实大考虑用稀疏矩阵库如 scipy.sparse替代 Python 字典。4.3 现象推荐接口 P99 延迟忽高忽低偶尔飙到秒级原因在线拼装时对每个候选歌曲都做了一次数据库查询候选多的时候就是 N1 查询。另外缓存击穿时多个请求同时去查库。解决用in_bulk批量查这是最基本的。缓存层面召回结果设置合理的过期时间并在过期前用后台任务主动刷新避免同一时刻大量请求同时穿透到数据库。对超热用户行为特别多的可以单独做缓存预热。4.4 现象用户反馈「推的歌我都不喜欢」但离线指标看着还行原因离线评估用的是「命中率」这类指标但没考虑用户的实际听歌场景。比如用户早上听轻音乐、晚上听摇滚单一偏好向量无法表达这种时段差异。解决把行为按时间段分桶早中晚各维护一份召回结果在线按请求时间选择对应的召回集。这个改动不大但体感提升明显。另一个方向是引入「场景标签」让用户在设置里选当前心情或场景直接切换召回策略。4.5 现象行为表数据量暴涨查询越来越慢原因PlayRecord只增不删且没有做冷热分离历史数据全堆在一张表里。解决按时间分区或者定期把超过 90 天的行为归档到历史表。推荐计算主要用近期行为历史数据对实时推荐的贡献有限。归档时保留聚合后的统计结果如用户对某歌手的累计偏好分原始明细可以清理。5. 让推荐结果可解释一个提升信任度的小技巧推荐系统有个玄学问题用户不知道你为什么推这首歌就会觉得「这系统在乱推」。解决成本最低的办法是在推荐结果里附上一句解释比如「因为你听过 XX」或「喜欢 XX 的人也在听」。这个功能实现起来不复杂但对留存的影响比调模型参数大得多。具体做法是在离线召回阶段顺便记录每个候选歌曲的「来源歌曲」——也就是哪个已听歌曲贡献了最多的相似度分数。在线返回时把来源歌曲的名字带上。# 离线阶段记录来源 for uid, songs in user_songs.items(): scores defaultdict(float) source {} # 候选 - 贡献最大的来源歌曲 for sid in songs: for cand, s in sim.get(sid, {}).items(): if cand not in songs: scores[cand] s if cand not in source or s source[cand][1]: source[cand] (sid, s) top sorted(scores.items(), keylambda x: -x[1])[:200] # 缓存里同时存 id 和来源 cache.set(frecall:{uid}, [ {song_id: sid, from: source[sid][0]} for sid, _ in top ], timeout86400)逻辑说明source字典记录每个候选歌曲得分最高的来源歌曲。在线接口拿到后查一下来源歌曲的标题拼成「因为你听过《XX》」展示。这个解释不一定完全准确但用户看到具体歌名信任感会明显提升。参数说明来源只保留贡献最大的那一首多了反而啰嗦。如果候选歌曲有多个来源且分数接近可以随机选一个增加解释的多样性。验证这套方案有没有效果别只看离线指标。上线后做 A/B 对比一组带解释、一组不带看点击率和次日留存。我自己的经验是解释带来的点击率提升通常在 5% 到 15% 之间比换一个更复杂的召回模型来得快。最后说个习惯每次改推荐逻辑我都会先在一个小流量用户群上跑三天确认没有异常再全量。推荐系统最怕的就是「离线指标涨了线上体感崩了」小流量验证是唯一的后悔药。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑