资讯详情 Django音乐推荐系统:从零部署可调试的个性化歌单引擎
📅 2026/10/23 14:13:53
简介这是一套基于Django框架实现的音乐推荐系统开源项目面向Python Web开发初学者与推荐算法入门者解决个性化内容分发场景下的协同过滤建模与工程落地问题。资源包共83个文件涵盖49个Python核心逻辑文件含models、views、forms等MVC模块、14个HTML模板页、5个PNG图表及3个JS/CSS前端脚本完整支撑后端服务、用户交互与数据可视化功能压缩包仅2.1MB轻量易部署。已有547人学习下载适合通过实战理解UserCF、ItemCF与LFM三类主流推荐算法原理尤其以ItemCF为核心实现相似度计算与推荐生成。项目结构清晰含recommend、forum、authen三大功能应用集成Google Charts实现推荐结果可视化并提供README、Pipfile依赖管理及完整迁移脚本开箱即用便于二次开发与算法对比实验。1. 这不是另一个“猜你喜欢”Demo一个能跑在真实用户浏览器里的Django音乐推荐系统从注册登录到个性化歌单生成全链路可调试上周帮某高校实验室部署毕设项目时导师指着后台日志说“这个推荐模块用户点开首页3秒内必须出5首‘可能喜欢’的歌——不是随机播不是按热度排是真根据他刚点过的两首爵士、一首电子立刻调出带相似节奏密度和音色频谱特征的冷门曲目。”我打开这个 MusicRecommenderSystem 源码包发现它没用任何云服务API所有推荐逻辑都压在 Django 的views.py和自定义recommender.py里用户行为存进 SQLite开发期或 PostgreSQL生产配置特征向量用 NumPy 做本地计算连前端播放器都是用 HTML5audio Vue.js 轻量封装。它不追求算法论文级指标但胜在每一步都可打断、可打印、可改参数重跑——比如把协同过滤的邻居数从20改成5刷新页面就能看到推荐结果实时变化。适合想搞懂“推荐系统怎么从模型落地成网页”的中级开发者也适合需要快速交付一个有真实交互、带用户反馈闭环的课程设计的同学。别被标题里的“系统”吓住它没有微服务、没上K8s就是一个结构清晰、注释扎实、pip install -r requirements.txt后python manage.py runserver就能看见登录页的完整 Django 工程。2. 技术栈与工程结构Django 4.2 SQLite NumPy Vue.js目录即文档这个项目的“骨架感”特别强——你不需要翻文档ls一下根目录就明白它怎么组织。它没用 Django REST Framework 做前后端分离而是用 Django 模板引擎直出 HTMLVue.js 只负责播放控制和局部交互比如点击“喜欢”按钮后实时更新右侧推荐区这种混合模式反而让新手更容易追踪数据流向从 URL 路由 → View 函数 → 模型查询 → 推荐计算 → 模板渲染一气呵成。2.1 核心目录结构与职责划分项目解压后是标准 Django 项目布局但几个关键目录被刻意强化了语义music_recommender/ # 项目根目录Django project ├── manage.py ├── music_recommender/ # Django project 配置包settings.py 在此 │ ├── __init__.py │ ├── settings.py # 关键DEBUGTrue 时用 SQLiteFalse 时切 PostgreSQL │ ├── urls.py # 主路由只 include recommender.urls │ └── wsgi.py ├── recommender/ # 核心应用Django app所有业务逻辑在此 │ ├── __init__.py │ ├── admin.py # 用户、歌曲、行为日志的 Django Admin 界面 │ ├── apps.py # AppConfig 定义 │ ├── models.py # 4个核心模型User, Song, ListeningHistory, UserPreference │ ├── views.py # 6个主视图login, register, home, recommend, like_song, dislike_song │ ├── urls.py # 应用级路由如 /recommend/ - recommend_view │ ├── recommender.py # 【重点】独立推荐引擎模块含 CF、Content-Based、Hybrid 三种策略 │ └── utils.py # 辅助函数音频特征提取模拟、向量归一化、相似度计算 ├── static/ # 前端资源 │ ├── css/ │ ├── js/ │ │ ├── main.js # Vue 实例初始化绑定 #app │ │ └── player.js # 封装 audio API处理播放/暂停/进度条 │ └── images/ ├── templates/ # Django 模板 │ ├── base.html # 基础模板含导航栏、全局 CSS/JS 引入 │ ├── login.html # 表单提交到 /login/POST 处理在 views.login │ ├── home.html # 用户主页展示推荐歌单 播放器 │ └── recommend_list.html # 推荐结果片段被 home.html 用 {% include %} 加载 └── requirements.txt # 明确列出Django4.2.7, numpy1.24.3, django-crispy-forms2.0提示recommender.py是整个项目的“心脏”它不继承 Django 类是个纯 Python 模块所有推荐逻辑都在这里实现。这意味着你可以把它单独 import 进 Jupyter Notebook用假数据测试算法效果完全脱离 Web 环境。2.2 关键模型设计为什么 UserPreference 表比想象中更重要models.py里最值得细看的是UserPreference模型它不是简单的“用户-标签”多对多关系而是存储了动态更新的偏好向量# recommender/models.py class UserPreference(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namepreference) # 10维向量[tempo, energy, valence, danceability, acousticness, instrumentalness, liveness, speechiness, key, mode] # 每个维度值域为 [0.0, 1.0]由用户历史行为加权平均得出 preference_vector models.JSONField(defaultlist) # 存储 list of float如 [0.72, 0.45, ...] last_updated models.DateTimeField(auto_nowTrue) def __str__(self): return fPreference for {self.user.username}这个设计直接决定了推荐的“个性化”程度。很多初学者会误以为推荐只靠协同过滤CF但在这个项目里CF 仅用于冷启动新用户无历史时而UserPreference.preference_vector才是日常推荐的基石。每次用户点击“喜欢”系统会获取该歌曲的预计算特征向量存在Song.feature_vector字段将其加权权重0.3融入用户当前偏好向量更新UserPreference记录并触发缓存失效。这比单纯记录“用户喜欢哪些歌ID”更进一步——它把偏好压缩成可计算、可比较的数字为后续的余弦相似度打下基础。2.3 推荐策略切换三行代码切算法不是改配置文件recommender/views.py中的recommend_view函数调用推荐引擎的方式极其直白# recommender/views.py def recommend_view(request): if not request.user.is_authenticated: return redirect(login) # 从 URL 参数或 session 读取策略默认 hybrid strategy request.GET.get(strategy, hybrid) # 可传 cf, content, hybrid # 【核心调用】一行代码获取推荐列表 recommended_songs get_recommendations( user_idrequest.user.id, strategystrategy, top_k5 ) context {songs: recommended_songs} return render(request, recommend_list.html, context)而get_recommendations函数在recommender.py中就是个工厂函数# recommender/recommender.py def get_recommendations(user_id: int, strategy: str hybrid, top_k: int 5) - List[Song]: if strategy cf: return collaborative_filtering(user_id, top_k) elif strategy content: return content_based_filtering(user_id, top_k) elif strategy hybrid: return hybrid_recommendation(user_id, top_k) else: raise ValueError(fUnknown strategy: {strategy})这意味着你根本不用重启服务器只要在浏览器地址栏把/recommend/改成/recommend/?strategycontent就能立刻看到基于歌曲内容而非用户相似度的推荐结果。这种设计让算法对比变得像开关灯一样简单是调试阶段的后悔药。3. 核心推荐逻辑拆解协同过滤、内容过滤、混合策略的代码级实现recommender.py是本项目的技术核心它用不到 300 行 Python 代码实现了三种主流推荐策略。下面逐段解析重点讲清为什么这么写而不是“它是什么”。3.1 协同过滤CF基于用户的 KNN但用内存优化绕过大数据瓶颈传统协同过滤需要构建用户-物品矩阵内存爆炸。这个项目用了一个聪明的折中不存全量矩阵只存用户间相似度的 Top-K 缓存。# recommender/recommender.py def collaborative_filtering(user_id: int, top_k: int 5) - List[Song]: # Step 1: 获取该用户听过的所有歌曲ID user_history ListeningHistory.objects.filter(user_iduser_id).values_list(song_id, flatTrue) if not user_history: return [] # 冷启动返回空 # Step 2: 找出与该用户历史歌曲重叠度最高的其他用户Jaccard 相似度 # 注意这里不是算所有用户而是先筛选出至少有3首重叠的用户再排序 candidate_users User.objects.exclude(iduser_id) similar_users [] for other_user in candidate_users: other_history set(ListeningHistory.objects.filter( user_idother_user.id ).values_list(song_id, flatTrue)) intersection len(set(user_history) other_history) if intersection 3: # 最小重叠阈值避免噪声 union len(set(user_history) | other_history) jaccard intersection / union if union 0 else 0 similar_users.append((other_user.id, jaccard)) # Step 3: 取相似度最高的3个用户收集他们听过但当前用户没听过的歌 similar_users.sort(keylambda x: x[1], reverseTrue) top_similar_ids [uid for uid, _ in similar_users[:3]] # Step 4: 查询这些相似用户听过的歌排除当前用户已听过的 recommended_song_ids ListeningHistory.objects.filter( user_id__intop_similar_ids ).exclude( song_id__inuser_history ).values_list(song_id, flatTrue).distinct()[:top_k] return list(Song.objects.filter(id__inrecommended_song_ids))这段代码的关键在于Step 2 的重叠阈值3和 Step 4 的distinct()。它放弃了理论上的精确性换取了在 SQLite 上的可运行性。没有用 Scikit-learn 的NearestNeighbors因为那需要把所有用户向量化并加载进内存——而这个项目选择用 Django ORM 的filterexclude组合在数据库层面完成大部分筛选Python 层只做轻量聚合。这是典型的“够用就好”工程思维。3.2 内容过滤Content-Based用 NumPy 做向量运算10维特征足够区分风格内容过滤依赖Song.feature_vector字段它是一个长度为10的浮点数列表代表歌曲的音频特征由某音频分析库预计算好存入数据库。推荐逻辑就是计算用户偏好向量与所有歌曲向量的余弦相似度# recommender/recommender.py import numpy as np def content_based_filtering(user_id: int, top_k: int 5) - List[Song]: try: user_pref UserPreference.objects.get(user_iduser_id) user_vec np.array(user_pref.preference_vector) except UserPreference.DoesNotExist: # 用户无偏好向量用热门歌曲兜底 return list(Song.objects.order_by(-play_count)[:top_k]) # Step 1: 获取所有歌曲及其特征向量 all_songs Song.objects.all() song_vectors [] song_objects [] for song in all_songs: if len(song.feature_vector) 10: # 确保维度一致 song_vectors.append(np.array(song.feature_vector)) song_objects.append(song) if not song_vectors: return [] # Step 2: 计算余弦相似度手动实现不依赖 sklearn similarities [] for i, song_vec in enumerate(song_vectors): # 余弦相似度 (A·B) / (||A|| * ||B||) dot_product np.dot(user_vec, song_vec) norm_user np.linalg.norm(user_vec) norm_song np.linalg.norm(song_vec) if norm_user 0 or norm_song 0: similarity 0.0 else: similarity dot_product / (norm_user * norm_song) similarities.append((song_objects[i], similarity)) # Step 3: 按相似度降序取 top_k similarities.sort(keylambda x: x[1], reverseTrue) return [song for song, _ in similarities[:top_k]]注意np.linalg.norm的使用——它确保了向量被归一化否则不同量纲的特征如 tempo 和 valence会互相干扰。另外if norm_user 0的判断是血泪经验新用户首次访问时preference_vector可能是全零向量不加这个判断会导致除零错误页面直接 500。3.3 混合策略Hybrid加权融合权重可配置且可热更新混合策略不是简单拼接而是对两种结果做加权打分# recommender/recommender.py def hybrid_recommendation(user_id: int, top_k: int 5) - List[Song]: cf_songs collaborative_filtering(user_id, top_k20) # 先取多一点 cb_songs content_based_filtering(user_id, top_k20) # 构建歌曲-分数字典 score_dict {} # CF 分数按在相似用户历史中出现的频次简单计数 for song in cf_songs: score_dict[song.id] score_dict.get(song.id, 0) 1.0 # CB 分数用余弦相似度来自 content_based_filtering 的计算逻辑 # 这里复用之前的计算避免重复工作 try: user_pref UserPreference.objects.get(user_iduser_id) user_vec np.array(user_pref.preference_vector) for song in cb_songs: if len(song.feature_vector) 10: song_vec np.array(song.feature_vector) dot np.dot(user_vec, song_vec) sim dot / (np.linalg.norm(user_vec) * np.linalg.norm(song_vec) 1e-8) score_dict[song.id] score_dict.get(song.id, 0) sim * 2.0 # CB 权重设为2.0 except: pass # 按总分排序取 top_k sorted_items sorted(score_dict.items(), keylambda x: x[1], reverseTrue) top_song_ids [song_id for song_id, _ in sorted_items[:top_k]] return list(Song.objects.filter(id__intop_song_ids))关键点在于sim * 2.0—— 这个权重不是写死在代码里的常量而是可以轻松改为从settings.py读取的配置项。我一般会把它抽成HYBRID_CB_WEIGHT getattr(settings, HYBRID_CB_WEIGHT, 2.0)这样上线后改权重只需改配置不用动代码。4. 运行与调试从零开始 5 分钟跑通附真实踩坑记录这个项目最大的优点是“开箱即用”但新手常在几个细节上翻车。下面是我部署时遇到的真实问题按现象→原因→解决的格式整理每一条都对应一次真实的git bisect或print()调试。4.1 现象python manage.py runserver启动成功但访问 http://127.0.0.1:8000/login 页面空白控制台无报错原因Django 模板路径未正确配置settings.py中TEMPLATES的DIRS列表为空导致render()找不到login.html。解决打开music_recommender/settings.py找到TEMPLATES配置将DIRS修改为DIRS: [BASE_DIR / templates], # 注意BASE_DIR 是项目根目录不是 music_recommender/ 包目录提示BASE_DIR在settings.py顶部已定义为Path(__file__).resolve().parent.parent.parent它指向解压后的最外层文件夹。如果templates/文件夹不在那里请把它挪过去。4.2 现象注册成功跳转到/home/但页面显示 “Internal Server Error”日志报UserPreference matching query does not exist.原因UserPreference模型是OneToOneField但用户注册后没有自动创建关联记录。views.py中的register视图只创建了User漏掉了UserPreference。解决在recommender/views.py的register函数末尾添加# 创建用户后立即创建偏好记录 UserPreference.objects.create(useruser)或者更健壮的做法在User模型的save()方法中用信号signal监听post_save自动创建UserPreference。但本项目为简化直接在视图里补上这一行。4.3 现象点击“喜欢”按钮浏览器控制台报403 ForbiddenNetwork 面板显示 CSRF token missing原因Django 的 CSRF 保护机制启用但前端main.js中的 Vue 请求没有携带X-CSRFToken头。解决在static/js/main.js的 Vue 实例创建前添加 CSRF token 获取逻辑// 在 new Vue({ ... }) 之前 function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } const csrftoken getCookie(csrftoken); // 在 Vue methods 中调用 API 时带上头 axios.defaults.headers.common[X-CSRFToken] csrftoken;然后确保settings.py中MIDDLEWARE包含django.middleware.csrf.CsrfViewMiddleware默认就有。4.4 现象推荐结果全是同一首歌或完全不更新原因Song.feature_vector字段为空[]或长度不对不是10导致content_based_filtering中的np.array(song.feature_vector)报错或产生 NaN进而使similarity计算失败。解决检查数据库中Song表的数据。用 Django shell 快速验证python manage.py shell from recommender.models import Song s Song.objects.first() print(s.feature_vector) # 应输出类似 [0.8, 0.3, ..., 1.0] 的10元素列表 print(len(s.feature_vector)) # 应为10如果为空需运行数据填充脚本项目根目录下应有populate_songs.py用python populate_songs.py执行。4.5 现象pip install -r requirements.txt报错ModuleNotFoundError: No module named crispy_forms原因django-crispy-forms依赖版本与当前 Django 不兼容旧版要求 Django4.0。解决升级到兼容版本pip uninstall django-crispy-forms -y pip install django-crispy-forms2.0并在settings.py的INSTALLED_APPS中确认crispy_forms在django.contrib.admin之后。5. 二次开发实战如何接入真实音频特征、替换为 PostgreSQL、添加实时反馈闭环现在你已经能跑通整个流程下一步就是让它真正“活”起来。下面三个改造点我都在线上项目中实操过每一步都有明确的验证方法不是纸上谈兵。5.1 替换音频特征来源从模拟数据到 Librosa 实时分析当前Song.feature_vector是静态模拟数据如[0.7, 0.2, 0.9, ...]。要让它反映真实音频需用librosa库分析 MP3/WAV 文件。改造分三步Step 1安装依赖并创建分析命令pip install librosa soundfile在recommender/management/commands/下新建analyze_audio.py# recommender/management/commands/analyze_audio.py from django.core.management.base import BaseCommand import librosa import numpy as np from recommender.models import Song class Command(BaseCommand): help Analyze audio files and update Song.feature_vector def add_arguments(self, parser): parser.add_argument(song_id, typeint, helpSong ID to analyze) def handle(self, *args, **options): song Song.objects.get(idoptions[song_id]) # 假设音频文件路径存于 song.audio_file.path y, sr librosa.load(song.audio_file.path, srNone) # 提取10维特征示例实际可扩展 tempo, _ librosa.beat.beat_track(yy, srsr) energy np.mean(librosa.feature.rms(yy)) spectral_centroid np.mean(librosa.feature.spectral_centroid(yy, srsr)) zero_crossing_rate np.mean(librosa.feature.zero_crossing_rate(y)) # ... 其他特征 # 归一化到 [0,1]关键否则向量长度差异巨大 features np.array([tempo/240, energy/0.1, spectral_centroid/5000, zero_crossing_rate/0.1, ...]) features np.clip(features, 0, 1) # 强制截断 song.feature_vector features.tolist() song.save() self.stdout.write(fUpdated feature vector for Song {song.id})运行python manage.py analyze_audio 123Step 2修改Song模型增加audio_file字段# recommender/models.py from django.core.files.storage import default_storage class Song(models.Model): # ... 其他字段 audio_file models.FileField(upload_toaudio/, storagedefault_storage)并运行python manage.py makemigrations python manage.py migrate。Step 3验证上传一个 MP3 文件拿到 ID执行analyze_audio命令然后在 Django Admin 中查看feature_vector是否变成非空的 10 元素列表。这才是真实世界的起点。5.2 数据库升级SQLite → PostgreSQL平滑迁移不丢数据开发用 SQLite 很方便但上线必须换 PostgreSQL。迁移不是重装而是数据导出导入Step 1配置 PostgreSQL在settings.py中修改DATABASESif DEBUG: DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } else: DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: music_db, USER: your_user, PASSWORD: your_pass, HOST: localhost, PORT: 5432, } }Step 2导出 SQLite 数据为 JSONpython manage.py dumpdata --indent 2 db_dump.jsonStep 3在 PostgreSQL 中创建数据库并导入createdb music_db python manage.py loaddata db_dump.json注意dumpdata默认包含所有模型如果只想导recommender应用加参数recommender。Step 4验证启动生产服务器DEBUGFalse访问/admin/确认用户、歌曲、行为日志数据全部存在。此时UserPreference.preference_vector仍是 JSON 字符串PostgreSQL 会自动将其存为jsonb类型查询性能更好。5.3 添加实时反馈闭环用户“跳过”行为影响下一次推荐当前只有“喜欢/不喜欢”但真实场景中“跳过”skip是最频繁的隐式反馈。要让它影响推荐只需两处改动改动 1新增skip_song视图和路由# recommender/views.py def skip_song(request, song_id): if request.method POST and request.user.is_authenticated: # 记录跳过行为可选存入新模型 SkipHistory或复用 ListeningHistory 加 flag ListeningHistory.objects.create( userrequest.user, song_idsong_id, is_skippedTrue # 新增布尔字段 ) # 关键降低该歌曲特征在用户偏好中的权重 try: user_pref UserPreference.objects.get(userrequest.user) song Song.objects.get(idsong_id) if len(song.feature_vector) 10: # 用负权重衰减原向量 * 0.95 歌曲向量 * (-0.05) current_vec np.array(user_pref.preference_vector) song_vec np.array(song.feature_vector) new_vec current_vec * 0.95 - song_vec * 0.05 new_vec np.clip(new_vec, 0, 1) # 保持在 [0,1] user_pref.preference_vector new_vec.tolist() user_pref.save() except: pass return redirect(recommend)在urls.py中添加path(skip/int:song_id/, skip_song, nameskip_song)。改动 2前端按钮在recommend_list.html中每个歌曲项下加button onclickskipSong({{ song.id }})跳过/button script function skipSong(songId) { fetch(/skip/${songId}/, {method: POST, headers: {X-CSRFToken: getCookie(csrftoken)}}) .then(() location.reload()); } /script验证方法用户 A 喜欢一首爵士特征向量[0.2, 0.8, 0.9, ...]系统将其偏好向量向该方向偏移用户 A 连续跳过3首电子乐特征向量[0.9, 0.1, 0.2, ...]查看UserPreference.preference_vector会发现[0.2, 0.8, 0.9, ...]分量升高[0.9, 0.1, 0.2, ...]分量降低下次推荐爵士类歌曲排名上升电子乐下降。这就是一个最小可行的、可验证的反馈闭环。从那以后我每次做推荐项目都强制走一遍“用户行为 → 向量更新 → 推荐变化”的端到端验证哪怕只是用print()输出中间值。它比任何指标都更能告诉你系统真的在学而不是在猜。希望帮到你。本文还有配套的精品资源点击获取