基于Django与协同过滤的歌曲推荐系统完整开发复盘

发布时间:2026/10/12 9:36:58
基于Django与协同过滤的歌曲推荐系统完整开发复盘 先说个可能让你意外的观察搜“基于Django协同过滤算法的歌曲推荐系统”这个组合关键词能找到的方案一大把但绝大多数都停留在一个能注册、能登录、能选几首歌、能返回一个推荐列表的Demo层面。代码能跑你不能深问。比如相似度矩阵怎么算、稀疏评分矩阵怎么办、冷启动用户返回什么、评分数据从哪来很多人一问就会卡壳。这篇博文是我自己从零开发这套系统的完整复盘会讲清楚每个设计决策背后“为什么这么选”目标是让它真正变成能写进简历、能应付课程设计答辩、日后还能往生产系统方向扩展的完整项目。如果你正准备做一个和推荐相关的Web项目或者已经写了一半但总觉得“算法是被硬塞进Django里的”那这篇文章值得你读完整。我不打算只给一份能跑的代码更多是把我踩过的坑和最终采用的工程方案摊开来讲。1. 歌曲推荐这个选题为什么适合做Django实战1.1 这类项目真正练的是什么能力Django本身是一个“快速搭建完整应用”的Web框架很多教程会教你ORM、模板渲染、表单和认证但很少让你认真去考虑数据结构和算法。协同过滤算法单独学又很容易陷进数学推导里不知道数据到底怎么进、怎么出。把两者放在一个项目里刚好补齐了这条完整链路数据从哪来、模型怎么设计、算法怎么算、结果怎么通过API发给前端。我见过不少课程设计和求职项目推荐逻辑就是“找到同一首歌的所有评分求平均然后排个序”这本质上是排行榜不是协同过滤。真正的协同过滤要求你处理用户与物品的关系矩阵、计算相似度、做加权预测。这个过程里牵扯到的数据建模、性能优化、缓存策略才是这个项目真正的价值所在。1.2 为什么是歌曲不是电影、图书或者商品我最早其实想写电影推荐后来发现歌曲场景有一个别人很少提的优势元数据极简。电影推荐需要导演、演员、剧情简介、海报甚至获奖信息准备数据的过程比写算法还痛苦。图书更麻烦一本书的主题归属模糊评分人群和使用场景差异极大。商品推荐则要面对大量属性维度以及价格、促销等动态信息。歌曲不一样一首歌的核心字段就四个歌名、歌手、流派、时长。一套几百首歌的数据集手动构造都能有模有样。更关键的是歌曲的行为数据密集一个小型音乐社区里用户每天点播几十次很常见几百个用户几周就能攒下数万条交互记录。密度越高协同过滤越容易算出有价值的结果。而且推荐结果特别好自检——你听完推荐列表里的“相似歌曲”会马上判断这推荐靠不靠谱不用借助任何评测工具。2. 协同过滤的核心计算从评分矩阵到TopN推荐2.1 先别急着写代码UserCF和ItemCF的区别很多人一听到协同过滤就开始写余弦相似度函数写了半天都不知道自己在做“基于用户”还是“基于物品”。这两者从数据结构上说只差一个转置但业务含义完全不同。算法类型核心假设适用场景工程特点UserCF基于用户和你口味相似的人喜欢的你也可能喜欢用户数远小于物品数、用户画像稳定的社区在线找相似用户成本高实时性差ItemCF基于物品喜欢你听过的歌的人也喜欢另一首歌物品数量稳定、用户行为充足的场景相似度矩阵可离线算好线上直接查表歌曲场景里歌的数量通常比用户数量少一个量级。一个只有几千用户的社区歌曲可能只有几百首到几千首而且一首歌的风格不会频繁变化歌与歌之间的相似关系一周之内都很稳定。这正好是ItemCF的主场。算法可以提前把“歌与歌之间的相似度矩阵”算好用户请求推荐时只做一个矩阵乘法加排序性能非常可观。2.2 一次手算彻底搞懂相似度与预测分数我始终觉得想理解协同过滤与其读十篇公式推导不如亲手算一遍小样本。假设有三个用户和四首歌数据如下用户歌A歌B歌C歌DU153未评分未评分U25未评分42U3未评分354这里“未评分”不参与计算我把它当作占位符。目标是为U1预测对歌C和歌D的偏好分数。先用余弦相似度衡量用户之间的口味相似度。U1的向量是(5, 3)U2在共同维度上只有歌A有评分向量取(5, 0)真正参与余弦计算的只有两个向量的非零共同维度也就是歌A。U2向量在B、C、D上虽然都有值但U1没有评分就不能直接拿来算内积。不过简化起见工程实现里一般会把未评分置为0然后对整个向量算余弦这里我按完整向量算一遍方便你核对数字。向量形式U1 (5, 3, 0, 0)U2 (5, 0, 4, 2)U3 (0, 3, 5, 4)cos(U1, U2) 5×5 / (√34 × √45) ≈ 25 / 39.12 ≈ 0.639cos(U1, U3) 3×3 / (√34 × √50) ≈ 9 / 41.23 ≈ 0.218然后预测U1对歌C的评分score(U1, C) (0.639×4 0.218×5) / (0.639 0.218) ≈ 4.25对歌Dscore(U1, D) (0.639×2 0.218×4) / (0.639 0.218) ≈ 2.50计算结果是歌C明显高于歌D把歌C放进Top1推荐。整个过程里要注意的是0.639这个相似度权重很大因为U2和U1共同喜欢歌AU3虽然也给了歌B高分但它只和U1有一首共同评分所以权重低。这个例子很直观地展示了协同过滤的本质用共同偏好加权投票。2.3 为什么选余弦相似度而不是欧氏距离初学者容易在这里踩坑。欧氏距离衡量的是两个向量之间的绝对距离但评分体系里有一个人习惯打3分算合格、5分算神作另一个人觉得4分以下都是垃圾两人的绝对分差很大欧氏距离会认为他们完全不同。可实际上他们对歌曲的排序偏好可能高度一致。余弦相似度只关心向量的方向把每一行的数值整体缩放之后方向和口味才会显露出来。换句话说一个用户给几首歌打(3, 4, 5)另一个打(4, 5, 6)虽然绝对分差完全不同但余弦相似度会接近1。这一点在真实数据里极其重要因为用户打分的尺度不可能统一。另外真实工程里还会用皮尔逊相关系数做中心化处理等于是先把每个用户的平均分减掉再算相似对“习惯性低分用户”和“习惯性高分用户”的容忍度更高。如果只是做一个课程设计余弦相似度已经完全够用。3. Django数据模型与查询性能推荐系统的底座3.1 三个核心模型这样设计Django模型是整个推荐系统的地基。很多教程会让你把“用户收藏的歌曲”存成一个JSON字段或者多对多关系当时觉得很方便后面做算法时想死的心都有。我的方案是拆成三个模型歌曲表、评分表、以及系统自带的用户表。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, blankTrue) duration models.IntegerField(default0) # 单位秒 play_count models.IntegerField(default0) # 全局播放次数用于热门兜底 class Meta: indexes [ models.Index(fields[artist, genre]), ] def __str__(self): return self.title class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) song models.ForeignKey(Song, on_deletemodels.CASCADE, related_nameratings) score models.FloatField(default0) # 1~5用于协同过滤 created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, song) indexes [ models.Index(fields[user, score]), ]为什么评分必须独立成表而不是在Song表上挂一个用户外键因为推荐系统的核心是“用户-物品”二元关系。一个用户对很多首歌产生很多条评分一首歌也被很多用户评分天然是多对多关系。独立成Rating表之后Django的ORM查询、算法取数、后续加行为日志字段都会轻松很多。unique_together保证同一个用户对同一首歌只有一条评分记录防止前端重复提交或者脚本重复写入。索引里我特意加了(user, score)因为系统最常见的查询是“这个用户评过哪些歌”这个查询会直接作为ItemCF的输入。3.2 灌入数据用脚本模拟真实用户行为没有真实业务数据就得自己造。很多项目死在这一步直接随机生成评分最后算出来的推荐结果完全是一堆噪声。我写的模拟脚本只做三件事。第一给虚拟用户随机分配两个主要流派偏好比如“喜欢摇滚和电子对民谣基本无感”。第二只从用户偏好的流派里挑歌来评分分数服从偏正态分布喜欢的歌打高分一般的歌打中分。第三给每个用户留出10%左右的歌完全不打分模拟真实场景里的“听都没听过”。这样做生成的矩阵虽然不是真实数据但具备真实数据最关键的群聚效应相似的人确实有相似偏好。有了这个特征协同过滤算出来才有意义。如果完全随机打分你再怎么调算法推荐结果都是一堆莫名其妙的歌。这一点在项目演示里特别重要因为评委通常会随机抽查几个用户看推荐列表如果明显有偏好的用户得到的是混乱推荐项目可信度会大打折扣。3.3 推荐结果展示时的N1查询问题Django的ORM用起来很顺手但一个经典问题会在推荐场景里被放大拿到推荐歌曲id列表之后很多人会写for循环逐个查Song表。# 错误示范每首歌都产生一次数据库查询 songs [] for song_id in recommended_ids: songs.append(Song.objects.get(idsong_id))如果推荐10首歌就会出现1次获取推荐列表的查询再加上10次Song查询。请求一多数据库压力立刻上来。正确做法是把id列表一次传给ORM做批量查询from songs.models import Song song_ids [101, 208, 315, 402, 509, 610, 707, 808, 906, 1003] songs Song.objects.filter(id__insong_ids)然后你需要保持顺序时可以在Python里按id重新排一下。别小看这个细节很多“推荐接口越到后面越慢”的案例根因就在这里。4. 推荐引擎的工程化实现离线计算与在线查询4.1 为什么歌曲场景必须离线算相似度矩阵如果用户请求推荐时现算“这首歌和他听过的每一首歌的相似度”那响应时间会非常感人。假设有5000首歌一次用户请求就要做几千次向量点积数据库和Python的循环都会被拖垮。歌曲场景的幸运之处在于歌与歌之间的相似关系是慢变量。一首歌的风格、受众群体在几天内基本不会发生结构性变化。所以工程上一定要把计算拆成两层离线层负责定期构建相似度矩阵在线层只负责查表和排序。离线任务可以每天凌晨跑一次也可以每六小时跑一次。构建结果存成numpy文件或者直接丢进Redis。线上接口读取的是一份已就绪的矩阵不用自己现场算相似度。拿一个中等规模估算5000首歌相似度矩阵是5000×5000也就是2500万个浮点数。用float32存储大约100MB内存单机完全扛得住。所以歌曲推荐这类物品数量可控的场景用离线矩阵方案再合适不过。4.2 ItemCF的核心代码结构我工程里用的不是纯手工for循环而是基于scipy稀疏矩阵和numpy向量化实现。思路是先构建用户-歌曲稀疏矩阵然后计算歌曲之间的余弦相似度矩阵最后用一次矩阵乘法完成推荐打分。import numpy as np from scipy.sparse import csr_matrix from scipy.spatial.distance import cosine def build_item_similarity(user_item_matrix): user_item_matrix: csr_matrix形状为 (n_users, n_items) 返回: item_simnumpy数组形状为 (n_items, n_items) n_items user_item_matrix.shape[1] item_sim np.zeros((n_items, n_items)) # 工程优化版不好读先用直白循环帮助你理解 for i in range(n_items): for j in range(i 1, n_items): vec_i user_item_matrix[:, i].toarray().flatten() vec_j user_item_matrix[:, j].toarray().flatten() # 这一步可以过滤掉都为零的维度避免除零 mask (vec_i 0) (vec_j 0) if mask.sum() 2: continue sim 1 - cosine(vec_i[mask], vec_j[mask]) item_sim[i, j] item_sim[j, i] sim return item_sim def recommend_by_item(user_rating_vec, item_sim, user_rated_songs, top_k10): user_rating_vec: 当前用户的评分向量长度为 n_items item_sim: 由上面函数生成的相似度矩阵 user_rated_songs: 用户已经评过分的歌曲索引集合 scores item_sim.dot(user_rating_vec) # 已经听过的歌不需要再次推荐 scores[list(user_rated_songs)] -np.inf return np.argsort(scores)[-top_k:][::-1].tolist()这段代码的核心就是最后那个矩阵乘法scores item_sim.dot(user_rating_vec)。它等价于遍历用户评过分的每一首歌把它的相似歌曲分数乘以用户对该歌的评分后累加。这就是最经典的ItemCF加权投票也是前面手算例子的向量化版本。注释里我已经留了一个优化点计算余弦相似度时先通过mask过滤掉两边都是零的维度。原因下一章会详细讲这里先记住结论——0在评分矩阵里不代表“不喜欢”它代表“没有反馈”硬算会把相似度拉向0。4.3 一次完整的推荐请求链路从用户打开推荐页面到看到歌曲列表整个链路我在工程里拆成六步请求进入Django视图先通过request.user拿到当前用户。查询Rating表取出该用户所有评分记录。如果一条记录都没有直接走热门歌曲兜底接口返回play_count全局排行。有评分记录就读取离线算好的相似度矩阵内存变量或Redis缓存。把用户评分向量和相似度矩阵做矩阵乘法得到候选歌曲分数再过滤掉已评分的歌。取TopN歌曲id批量查Song表序列化返回给前端。这个链路里真正耗时的地方只有第2步的一次数据库查询和第5步的一次矩阵乘法。十万级数据量下耗时基本在几十毫秒以内对于个人项目和课程设计来说已经是很好的表现。5. 推荐效果翻车的几个经典场景与修正策略5.1 零分和“未评分”被混为一谈这是我自己第一版代码最大的坑。你去看很多开源项目评分矩阵都是把没评过分的格子直接填0然后拿去算余弦相似度。表面上看矩阵很完整算起来也不报错但结果会逐渐变得莫名其妙。原因是用户A没听过歌X并不代表他不喜欢歌X。如果矩阵里把“未评分”和“用户主动打了0分”都存成0计算相似度时这些大量无意义的零维度会被当成“共同偏好”处理。喜欢听周杰伦的用户和喜欢听德云社的用户他们的评分向量里绝大多数元素都是0余弦相似度的分子是共同非零维度上的内积分母却带着双方庞大的零元素计算结果会被无谓地稀释相似度趋近于0。修正方法有两类。第一类是在算余弦时只取两个向量共同非零的维度做内积像4.2节代码里那个mask操作第二类是用稀疏矩阵存储不要手动给所有没评分的格子补0。我从头到尾只保留Rating表里有值的数据构造稀疏矩阵时也只填充有评分的位置。5.2 热门歌曲霸榜需要逆流行度惩罚ItemCF天然有一个毛病热门歌曲的相似度矩阵里它跟谁都“相似”。因为大多数人听过热门歌热门歌与其他歌曲的同现次数极高相似度容易被高估。结果就是不管用户听什么推荐列表都会被头部爆款占掉大部分名额冷门歌曲永远没有出头机会。我在项目里加了一个简单的逆流行度惩罚因子思路和TF-IDF一样一首歌如果出现在太多用户的评分记录里它的权重就降低。修正后的推荐分数变成下面这样[ score(u, s) \sum_{i \in rated(u)} sim(s, i) \cdot r_{u,i} \cdot \frac{1}{\log(2 freq(s))} ]其中freq(s)是歌曲s在全站评分记录里出现的次数。歌曲越热门分母越大被推荐的优先级就越低。加了这一层之后推荐列表里开始出现一些没那么大众但确实和用户口味匹配的歌整体观感和实用性都提升明显。要注意惩罚也不能太狠否则会把热门歌曲完全排除反而走向另一个极端。实际调参的时候我卡在log底数和内层常数上试了好几轮最后采用的方案是freq按整个评分集统计热门度的阈值用全站评分数量的中位数做参照。5.3 冷启动用户和冷启动歌曲的兜底策略冷启动是任何推荐系统都躲不开的问题也是最容易被课程设计忽略的一环。一个刚注册的用户评分表里什么都没有协同过滤拿什么算直接报错或者返回空列表是最差的做法。我做了三层兜底。第一层新用户无评分时返回全站热门歌曲列表按play_count排序第二层如果用户只评过一两首歌相似度矩阵里那首歌的邻居数量不够就结合歌曲的genre标签做同流派补全把流派匹配的歌曲混进候选第三层前端引导用户先给三首歌打分再进入首页推荐。我实测下来的一个经验是让新用户先选三首喜欢的歌再进首页比直接丢一个空白推荐页效果好很多。这不光是算法问题还是产品逻辑问题。三首评分足够让ItemCF产生第一批候选推荐列表就不再是空白的。5.4 评分习惯偏差先中心化再算相似度真实评分数据里有的人打分范围就是4到5有的人更愿意给2到4。前者和后者即使喜欢完全相同的歌余弦相似度也会被绝对评分尺度干扰。处理思路是中心化计算每条评分与该用户平均分的差值再对差值矩阵算相似度。中心化公式[ r{u,i} r{u,i} - \bar{r_u} ]这样每个用户的评分分布都归到以0为中心评分偏差方向相同的人相似度才会高。实现起来很简单算相似度之前把Rating表里的分数减去用户平均分即可。歌曲侧也可以做类似处理比如每首歌减去歌曲的平均分能缓解“全体用户都给高分”的歌曲虚高问题。6. 从单机Demo到可部署服务接口、缓存与扩展方向6.1 用DRF把推荐结果封成接口Django自带视图函数可以直接返回JsonResponse但一旦系统里有好几个数据模型和多个APIDjango REST Framework会让你省很多事。我用了DRF的ModelSerializer和APIView来组织推荐接口。from rest_framework import serializers, views, response from songs.models import Song class SongSerializer(serializers.ModelSerializer): class Meta: model Song fields [id, title, artist, genre, duration] class RecommendView(views.APIView): def get(self, request): user request.user song_ids get_recommendation_for_user(user.id) # 推荐引擎入口 if not song_ids: return response.Response({items: []}) songs Song.objects.filter(id__insong_ids) return response.Response({ items: SongSerializer(songs, manyTrue).data, count: len(song_ids), })选择APIView而不是ViewSet是因为推荐接口逻辑本身不算复杂不需要默认的CRUD范式。APIView里写一个get方法就足够清晰。这里最值得注意的地方还是song查询必须批量进行要避免serializer内部逐条数据库查询。6.2 缓存推荐结果计算一次服务多次推荐结果的缓存设计有个容易忽略的点不是每次请求都重新算一遍完整推荐。单个用户的推荐列表肯定要缓存但相似度矩阵更要缓存。我按两层来做缓存。第一层是相似度矩阵这是全站共享的全局数据离线任务跑完之后写入Rediskey设计成item_sim:v1:{timestamp}这样的带版本号形式方便任务更新时原子切换。第二层是单用户推荐结果key设计成user_rec:{user_id}过期时间设成30分钟。用户每打一次分就在打分视图里主动删除该用户的推荐缓存保证下一次请求能看到最新推荐。from django.core.cache import cache def get_user_recommendation(user_id): cache_key fuser_rec:{user_id} cached cache.get(cache_key) if cached is not None: return cached song_ids compute_recommendation(user_id) cache.set(cache_key, song_ids, timeout60 * 30) return song_ids这个方案在产品原型阶段完全够用。真正上生产后如果用户量变大可以考虑把相似度矩阵直接加载到所有进程的共享内存里再用Celery定时任务做离线重建那就超出本文范围了。6.3 下一步从评分到行为从ItemCF到混合推荐课程设计做到这一步已经可以说是一个闭环项目但真实场景里几乎不会有人手动给歌曲打1到5分。用户在音乐App里的行为是播放、收藏、跳过、完播。这些行为可以被映射成伪评分播放一次1完整听完2收藏5跳过-3。映射完之后的矩阵依然是稀疏的但比手动评分的覆盖面大得多。我自己的一个体会是从“能跑的Demo”往“可用的系统”走最大的变化不是算法变复杂而是数据来源变了。行为数据进来之后推荐频率会远高于评分数据冷启动问题更好解系统的整体推荐质量也会有明显提升。再往后就是混合推荐。ItemCF输出召回候选排序层用一个简单逻辑回归或者XGBoost模型输入特征是歌手重合度、流派重合度、歌曲热门度、用户历史平均评分等最后再做一次多样性重排保证一个页面上不会出现十首同风格的歌。如果你对AI开发感兴趣现在也有AI Agent能帮你生成项目脚手架但骨架可以自动搭推荐算法的核心思路和数据结构必须自己吃透不然出了问题你可能连矩阵的shape都对不上。最后分享一个带人的经验。我每次让新同学上手这个项目都先要求把那三用户四首歌的手算案例徒手算一遍。算过的人后面看任何ItemCF源码都不会懵没算过的人往往会卡在“为什么矩阵乘法一次就能算出加权总分”这一步上。你如果今天刚开始建议也从这个手算开始不要急着复制代码。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询