尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Django实战:从零构建音乐推荐系统,涵盖相似度计算与Faiss加速
简介这是一套基于Django框架实现的音乐推荐系统完整源码面向具备Python与Web开发基础、希望深入理解协同过滤算法的开发者与学习者。项目采用传统MVC架构内置UserCF、ItemCF与LFM三种推荐算法并以ItemCF作为主推荐策略核心思路是当更多用户同时喜欢项目A与项目B时两者便具备显著相似性相似度通过复数公式计算得出。系统还借助Google Charts图表库实现数据可视化并划分recommend、forum、authen等应用模块配合模板与静态资源组织页面。压缩包共83个文件以49个py源码、14个html模板为主辅以css、js、png、jpg及Pipfile等配置文档整体约2.1MB结构清晰便于二次开发。目前已有547人学习下载适合作为推荐算法入门实践与Django项目参考。1. 从「歌荒」到推荐系统MusicRecommenderSystem 到底在解决什么你有没有过这种体验打开音乐 App首页推荐来来回回就那几首点「换一批」还是老面孔。问题不在歌少而在推荐逻辑太糙。MusicRecommenderSystem 这个标题说的就是用 Django 搭一套能跑起来的音乐推荐服务——不是调个第三方 API 糊弄而是自己管歌曲库、自己算相似度、自己出推荐列表。它适合两类人一是想学推荐系统但被 Spark 那套重型栈劝退的后端开发者二是手里有一批歌曲数据、想快速做个能演示的 Web 服务的同学。核心链路就三步歌曲特征入库、相似度计算、接口返回推荐结果。Django 在这里的角色不是「顺便用一下」而是把 ORM、路由、模板、管理后台全串起来让你不用从零写 HTTP 层。下面按「建库→算相似→出接口→避坑→调优」的顺序拆开讲。2. 歌曲特征怎么存Django 模型设计与数据导入推荐系统的地基是数据模型。模型设计错了后面算相似度时要么字段不够要么查询慢到没法看。这一章先把歌曲、用户、行为三张核心表定下来再走一遍批量导入的流程。2.1 三张核心表Song、UserProfile、ListenRecord常见做法是把歌曲的「内容特征」和「行为特征」分开存。内容特征指流派、语速、能量值这类从音频或元数据里抽出来的属性行为特征指谁听了哪首歌、听了几次、有没有跳过。两者混在一张表里后期做混合推荐时会很别扭。# models.py from django.db import models from django.contrib.auth.models import User class Song(models.Model): title models.CharField(max_length200, db_indexTrue) artist models.CharField(max_length200, db_indexTrue) genre models.CharField(max_length50, db_indexTrue) # 流派用于内容相似度 tempo models.FloatField(default0.0) # 语速 BPM归一化后使用 energy models.FloatField(default0.0) # 能量值 0~1 duration models.IntegerField(default0) # 时长秒 created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [models.Index(fields[genre, tempo])] class ListenRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) song models.ForeignKey(Song, on_deletemodels.CASCADE) play_count models.IntegerField(default1) # 累计播放次数 completed models.BooleanField(defaultFalse) # 是否完整听完 updated_at models.DateTimeField(auto_nowTrue) class Meta: unique_together (user, song) # 同一用户同一首歌只留一条genre和tempo上建索引是因为后面算内容相似度时会按这两个字段做过滤和范围查询。ListenRecord用unique_together约束避免同一用户反复插入同一首歌导致行为矩阵稀疏度失真。completed字段很关键——完整听完和听三秒就切权重完全不同后面算隐式评分时要乘系数。2.2 批量导入别用 create用 bulk_create新手最容易翻车的地方拿个 CSV 循环Song.objects.create()一千首歌能跑几十秒。正确做法是先构造对象列表再一次性bulk_create。# management/commands/import_songs.py import csv from django.core.management.base import BaseCommand from recommender.models import Song class Command(BaseCommand): help 从 CSV 批量导入歌曲 def handle(self, *args, **options): batch [] with open(songs.csv, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: batch.append(Song( titlerow[title].strip(), artistrow[artist].strip(), genrerow[genre].strip().lower(), # 统一小写避免 Rock/rock 分裂 tempofloat(row.get(tempo) or 0), energyfloat(row.get(energy) or 0), durationint(row.get(duration) or 0), )) if len(batch) 500: # 每 500 条落一次库 Song.objects.bulk_create(batch, ignore_conflictsTrue) batch.clear() if batch: Song.objects.bulk_create(batch, ignore_conflictsTrue) self.stdout.write(f导入完成共 {Song.objects.count()} 首)bulk_create的batch_size默认会一次性拼一条大 SQL500 条一批是经验值——太小网络往返多太大 SQL 语句可能超包大小限制。ignore_conflictsTrue让重复数据静默跳过适合增量导入场景。注意genre统一转小写否则「Pop」和「pop」会被当成两个流派相似度计算直接失真。2.3 特征归一化tempo 和 energy 不在一个量纲上tempo可能是 60 到 180energy是 0 到 1。直接算欧氏距离tempo 会完全主导结果。必须在入库后或计算前做归一化。# utils/normalize.py from recommender.models import Song def normalize_features(): songs list(Song.objects.all()) if not songs: return tempos [s.tempo for s in songs] t_min, t_max min(tempos), max(tempos) span (t_max - t_min) or 1.0 # 防止除零 for s in songs: s.tempo_norm (s.tempo - t_min) / span # 映射到 0~1 Song.objects.bulk_update(songs, [tempo_norm])这里用 min-max 归一化而不是 z-score是因为歌曲特征分布通常没有明显高斯形态min-max 更稳。span加or 1.0是防所有歌 tempo 相同导致除零。归一化后的值建议单独存字段避免每次推荐都重算一遍全库。3. 相似度怎么算从余弦相似到混合推荐数据齐了接下来是推荐系统的核心——怎么判断两首歌「像」。这一章讲三种由浅入深的算法以及怎么把它们拼成一个能用的推荐接口。3.1 内容相似度用余弦相似度算歌曲距离把每首歌表示成一个向量[tempo_norm, energy, genre_onehot...]两首歌的相似度就是向量夹角余弦。流派用 one-hot 展开假设有 10 个流派就加 10 维。# recommender/similarity.py import numpy as np from recommender.models import Song def build_feature_matrix(): songs list(Song.objects.all()) genres sorted(set(s.genre for s in songs)) genre_index {g: i for i, g in enumerate(genres)} matrix [] for s in songs: vec [s.tempo_norm, s.energy] [0.0] * len(genres) vec[2 genre_index[s.genre]] 1.0 # one-hot 位置 matrix.append(vec) return songs, np.array(matrix, dtypenp.float32) def cosine_sim_matrix(features): norm np.linalg.norm(features, axis1, keepdimsTrue) norm[norm 0] 1e-9 # 防零向量 normalized features / norm return normalized normalized.T # 矩阵乘法一次算完所有两两相似度normalized normalized.T是整段代码的关键——用矩阵乘法一次性算出 N×N 相似度矩阵比双重循环快两个数量级。norm 0的兜底不能省空特征歌曲会让整个矩阵出 NaN。这个矩阵在歌曲数量几千首时内存完全扛得住上万首就要考虑分块或只算 Top-K。3.2 协同过滤用 ListenRecord 构建用户-物品矩阵内容相似度只看歌本身不看「谁在听」。协同过滤补的就是这块。把ListenRecord转成用户-歌曲评分矩阵play_count和completed加权当隐式评分。# recommender/cf.py import numpy as np from recommender.models import ListenRecord def build_rating_matrix(): records ListenRecord.objects.select_related(user, song).all() users sorted(set(r.user_id for r in records)) songs sorted(set(r.song_id for r in records)) u_idx {u: i for i, u in enumerate(users)} s_idx {s: i for i, s in enumerate(songs)} matrix np.zeros((len(users), len(songs)), dtypenp.float32) for r in records: # 完整听完权重 1.0未听完 0.3再乘播放次数对数 weight 1.0 if r.completed else 0.3 matrix[u_idx[r.user_id], s_idx[r.song_id]] weight * np.log1p(r.play_count) return users, songs, matrixselect_related一次性把关联的 user 和 song 拉出来避免 N1 查询。评分用log1p(play_count)而不是原始次数是因为听 100 次和听 10 次的偏好差距不是线性的取对数能压住极端值。completed的 0.3 系数是经验值你可以按业务调——如果平台鼓励「试听」这个系数可以更低。3.3 混合推荐接口加权融合两路结果单独用内容相似或协同过滤都有短板新歌没行为数据协同过滤推不出来老用户听腻了同流派内容相似度又太窄。混合两者最稳。# recommender/engine.py import numpy as np from recommender.similarity import build_feature_matrix, cosine_sim_matrix from recommender.cf import build_rating_matrix def recommend_for_user(user_id, top_k10, alpha0.6): users, songs, rating build_rating_matrix() if user_id not in users: return [] # 冷启动用户走热门兜底 u users.index(user_id) user_vec rating[u] # 协同过滤得分用户向量与歌曲列向量的点积 cf_scores user_vec rating # 内容得分用户听过的歌的内容相似度加权平均 song_list, feat build_feature_matrix() sim cosine_sim_matrix(feat) listened np.where(user_vec 0)[0] content_scores sim[listened].mean(axis0) if len(listened) else np.zeros(len(song_list)) # 归一化后加权融合 cf_norm cf_scores / (cf_scores.max() or 1) ct_norm content_scores / (content_scores.max() or 1) final alpha * cf_norm (1 - alpha) * ct_norm final[listened] -1 # 已听过的排除 top_idx np.argsort(final)[::-1][:top_k] return [song_list[i] for i in top_idx if final[i] 0]alpha0.6表示协同过滤占六成、内容占四成这个比例在行为数据充足时偏协同、数据稀疏时应该调低。final[listened] -1把听过的歌压到最低避免推荐重复。冷启动用户直接返回空列表由上层走「热门歌曲」兜底逻辑——这一步不能省否则新用户首页直接空白。4. 接口与性能Django 视图层怎么扛住推荐请求算法跑通只是第一步接口响应慢照样没人用。这一章讲视图怎么写、缓存怎么加、查询怎么减。4.1 用 DRF 写推荐接口# recommender/views.py from rest_framework.views import APIView from rest_framework.response import Response from recommender.engine import recommend_for_user from recommender.serializers import SongSerializer class RecommendView(APIView): def get(self, request): user_id request.user.id top_k min(int(request.query_params.get(k, 10)), 50) # 上限 50防刷 songs recommend_for_user(user_id, top_ktop_k) return Response(SongSerializer(songs, manyTrue).data)top_k加min(..., 50)是防止有人传k100000把服务打挂。序列化用 DRF 的SongSerializer只返回前端需要的字段别把整个模型__dict__吐出去。4.2 缓存相似度矩阵别每次请求都重算build_feature_matrix和cosine_sim_matrix是全库扫描每次请求都跑一遍QPS 上到几十就崩。用 Django 缓存框架把矩阵缓存起来定时失效。# recommender/cache_utils.py from django.core.cache import cache import numpy as np from recommender.similarity import build_feature_matrix, cosine_sim_matrix def get_cached_sim_matrix(): cached cache.get(sim_matrix) if cached is not None: return cached songs, feat build_feature_matrix() sim cosine_sim_matrix(feat) cache.set(sim_matrix, (songs, sim), timeout3600) # 1 小时过期 return songs, simtimeout3600意味着新歌入库后最多一小时才进推荐池。如果业务要求实时可以在Song的post_save信号里主动cache.delete(sim_matrix)。缓存的是元组(songs, sim)注意songs是模型实例列表缓存后端要能序列化——Redis 后端没问题本地内存缓存也行。4.3 数据库查询优化select_related 和 only推荐接口里但凡涉及外键不加select_related就是 N1。另外只取需要的字段别SELECT *。# 优化前每首歌都单独查 artist 关联表如果有 # 优化后 songs Song.objects.only(id, title, artist, genre).filter(id__intop_ids)only延迟加载未指定字段减少数据传输量。如果Song表字段很多比如存了音频指纹这个优化能省不少内存。5. 避坑指南推荐系统上线前必须排查的 5 个问题这一章全是血泪经验每条按「现象→原因→解决」写照着排查能省你至少两天。5.1 推荐结果全是同一首歌现象接口返回 10 条8 条是同一首。原因相似度矩阵里某首歌与大量歌曲相似度接近 1argsort后全排在前面。常见于该歌特征向量接近零向量归一化后与其他向量点积异常。解决在cosine_sim_matrix里对零向量做兜底前面代码已加1e-9另外在最终排序前对同一 artist 做去重限制每个歌手最多出现 2 次。5.2 新用户首页空白现象刚注册的用户打开推荐页一条数据没有。原因recommend_for_user里user_id not in users直接返回空列表上层没做兜底。解决加热门歌曲回退逻辑——按ListenRecord全局播放量排序取 Top-K新用户直接返回这批。5.3 缓存和数据库不一致现象后台删了一首歌推荐接口还在返回它。原因相似度矩阵缓存了 1 小时删除操作没触发缓存失效。解决在Song的post_delete信号里cache.delete(sim_matrix)或者把缓存时间缩短到 10 分钟。别指望手动清缓存一定会忘。5.4 bulk_create 后主键为 None现象导入完歌曲想立刻建关联记录发现song.id是None。原因部分数据库后端如某些配置下的 SQLitebulk_create不回填主键。解决导入后重新Song.objects.filter(...)查一遍拿 ID或者改用bulk_create后跟一次refresh_from_db。PostgreSQL 支持RETURNING没这个问题。5.5 相似度计算内存爆掉现象歌曲数到 5 万首时cosine_sim_matrix直接 OOM。原因N×N 矩阵5 万首就是 25 亿个 float32约 10GB。解决不要全量算。改成只算「用户听过的歌」与「候选歌」之间的相似度候选集用 genre 过滤先缩小到几千首再算矩阵。或者上 Faiss 做近似最近邻但那是另一个量级的改造了。6. 进阶技巧用 Faiss 把推荐延迟压到 10ms 以内前面那套 numpy 全量矩阵方案歌曲几千首时够用上万首就吃力。如果你真要把 MusicRecommenderSystem 推到生产环境Faiss 是绕不过去的一步。它把相似度搜索从「算全矩阵」变成「建索引近似查询」延迟能从几百毫秒降到个位数。核心思路把每首歌的特征向量灌进 Faiss 索引查询时拿用户偏好向量去搜最近邻。下面是最小可用示例。# recommender/faiss_index.py import faiss import numpy as np from recommender.similarity import build_feature_matrix def build_faiss_index(): songs, feat build_feature_matrix() feat feat.astype(np.float32) faiss.normalize_L2(feat) # Faiss 内积等价于余弦相似度 dim feat.shape[1] index faiss.IndexFlatIP(dim) # IP Inner Product index.add(feat) return songs, index def query_similar(songs, index, user_vec, top_k10): user_vec user_vec.astype(np.float32).reshape(1, -1) faiss.normalize_L2(user_vec) scores, ids index.search(user_vec, top_k) return [songs[i] for i in ids[0] if i 0]IndexFlatIP是精确内积索引适合几万到几十万向量。再大就换IndexIVFFlat用聚类把搜索范围缩小代价是牺牲一点召回率。normalize_L2必须在add和search前都做否则内积不等于余弦相似度结果会偏。参数上IndexIVFFlat的nlist聚类数一般取sqrt(N)nprobe查询时扫的聚类数取nlist的 1% 到 10%需要拿真实数据测召回率和延迟的平衡点。我一般先用IndexFlatIP跑通确认效果达标后再换 IVF别一上来就调参。验证方法很简单拿同一个用户向量分别用 numpy 全量算和 Faiss 查对比 Top-10 重合度。重合度 90% 以上说明索引参数可接受低于 80% 就得加nprobe。最后说个习惯每次改完相似度逻辑我都会拿一个固定用户跑一遍推荐结果人工看前 20 条像不像。指标再好看推出来全是口水歌用户照样跑。算法是冷的耳朵是热的。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

深入理解epoll机制:从内核结构到高并发网络服务实战排查

深入理解epoll机制:从内核结构到高并发网络服务实战排查

1. 为什么做了这么多年业务,我还要回头抠epoll的细节记得有一年我排查一个内部网关服务,CPU使用率莫名其妙冲到90%多,业务代码逻辑查了个遍都没找到问题,最后才发现是事件通知这块在反复空转。从那以后我养成了一个习惯&#xff1…

📅 2026/10/13 3:52:13
Openclaw 2026.3.7 Docker离线部署方案:镜像锁定与数据卷实战

Openclaw 2026.3.7 Docker离线部署方案:镜像锁定与数据卷实战

Openclaw 2026.3.7 的 Docker 离线部署方案,我前前后后调了两周,终于整理成一套能直接拿到隔离网络里跑的 bundle。这套方案不是简单地把镜像 save 再 load 就完事,而是把版本锁定、数据持久化、服务编排、回滚机制全塞进了几个文件里&#x…

📅 2026/10/13 3:47:12
SpringBoot在线教育平台毕设实战:从架构设计到功能实现与避坑指南

SpringBoot在线教育平台毕设实战:从架构设计到功能实现与避坑指南

1. 项目核心需求与设计思路1.1 在线教育平台的业务场景解析做毕业设计选在线教育平台,几乎是一年四季都不冷门的方向。每个学校的信息类相关专业,总有几个同学做“网课系统”“教学管理平台”“在线学习系统”,但大多数项目做出来都像一个“课…

📅 2026/10/13 3:47:12
MORE NEWS

更多资讯

📰

Oracle 9i补丁9.2.0.8在Windows 32位上的安装与避坑全解析

简介:针对仍需维护 Oracle 9i 旧系统的数据库管理员与运维人员,这份 Windows 32 位安装资源以 p4547809_92080_WINNT 为标识,提供完整安装包,适合搭建、修复、迁移传统数据库环境,也可用于灾备恢复与补丁验证。压缩包共…

📰

Spring Boot + 微信小程序实战:宠物美容预约系统开发详解

很多人问过我,毕设或者实际项目里做一个“预约类”小程序到底怎么下手,今天拿一个真实的例子说说:基于 Spring Boot 微信小程序 的宠物美容预约系统。这套系统做出来后,能覆盖常见的单店或多店预约场景,包括宠物档案、…

📰

Linux块层架构演变:从单队列到blk-mq与io_uring性能优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

图论连通分量详解:从DFS到并查集与Tarjan实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

App版本升级数据表设计与接口实现:从字段拆解到灰度发布

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Hybrid RAG混合检索实战:融合稀疏与稠密检索,彻底解决精确词召回难题

1. 一次线上问答事故:纯向量检索在精确问题上当场翻车做检索增强生成(RAG)的朋友应该都有过这种体验:系统在大多数问题上答得漂亮,可一旦用户抛出带型号、编号、错误码的问题,答案就开始离谱。我去年在给某…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬