☰
Django音乐推荐系统:从零部署可调试的个性化歌单引擎
2026/10/23 14:13:38 网站建设 项目流程

简介:这是一套基于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 做本地计算,连前端播放器都是用 HTML5<audio>+ 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 模板引擎直出 HTML,Vue.js 只负责播放控制和局部交互(比如点击“喜欢”按钮后实时更新右侧推荐区),这种混合模式反而让新手更容易追踪数据流向:从 URL 路由 → View 函数 → 模型查询 → 推荐计算 → 模板渲染,一气呵成。

2.1 核心目录结构与职责划分

项目解压后是标准 Django 项目布局,但几个关键目录被刻意强化了语义:

music_recommender/ # 项目根目录(Django project) ├── manage.py ├── music_recommender/ # Django project 配置包(settings.py 在此) │ ├── __init__.py │ ├── settings.py # 关键!DEBUG=True 时用 SQLite,False 时切 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 # 明确列出:Django==4.2.7, numpy==1.24.3, django-crispy-forms==2.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_delete=models.CASCADE, related_name='preference') # 10维向量:[tempo, energy, valence, danceability, acousticness, instrumentalness, liveness, speechiness, key, mode] # 每个维度值域为 [0.0, 1.0],由用户历史行为加权平均得出 preference_vector = models.JSONField(default=list) # 存储 list of float,如 [0.72, 0.45, ...] last_updated = models.DateTimeField(auto_now=True) def __str__(self): return f"Preference for {self.user.username}"

这个设计直接决定了推荐的“个性化”程度。很多初学者会误以为推荐只靠协同过滤(CF),但在这个项目里,CF 仅用于冷启动(新用户无历史时),而UserPreference.preference_vector才是日常推荐的基石。每次用户点击“喜欢”,系统会:

  1. 获取该歌曲的预计算特征向量(存在Song.feature_vector字段);
  2. 将其加权(权重=0.3)融入用户当前偏好向量;
  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_id=request.user.id, strategy=strategy, top_k=5 ) 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(f"Unknown strategy: {strategy}")

这意味着你根本不用重启服务器,只要在浏览器地址栏把/recommend/改成/recommend/?strategy=content,就能立刻看到基于歌曲内容(而非用户相似度)的推荐结果。这种设计让算法对比变得像开关灯一样简单,是调试阶段的后悔药。

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_id=user_id).values_list('song_id', flat=True) if not user_history: return [] # 冷启动,返回空 # Step 2: 找出与该用户历史歌曲重叠度最高的其他用户(Jaccard 相似度) # 注意:这里不是算所有用户,而是先筛选出至少有3首重叠的用户,再排序 candidate_users = User.objects.exclude(id=user_id) similar_users = [] for other_user in candidate_users: other_history = set(ListeningHistory.objects.filter( user_id=other_user.id ).values_list('song_id', flat=True)) 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(key=lambda x: x[1], reverse=True) top_similar_ids = [uid for uid, _ in similar_users[:3]] # Step 4: 查询这些相似用户听过的歌,排除当前用户已听过的 recommended_song_ids = ListeningHistory.objects.filter( user_id__in=top_similar_ids ).exclude( song_id__in=user_history ).values_list('song_id', flat=True).distinct()[:top_k] return list(Song.objects.filter(id__in=recommended_song_ids))

这段代码的关键在于Step 2 的重叠阈值(>=3)和 Step 4 的distinct()。它放弃了理论上的精确性,换取了在 SQLite 上的可运行性。没有用 Scikit-learn 的NearestNeighbors,因为那需要把所有用户向量化并加载进内存——而这个项目选择用 Django ORM 的filter+exclude组合,在数据库层面完成大部分筛选,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_id=user_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(key=lambda x: x[1], reverse=True) 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_k=20) # 先取多一点 cb_songs = content_based_filtering(user_id, top_k=20) # 构建歌曲-分数字典 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_id=user_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(), key=lambda x: x[1], reverse=True) top_song_ids = [song_id for song_id, _ in sorted_items[:top_k]] return list(Song.objects.filter(id__in=top_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(user=user)

或者更健壮的做法:在User模型的save()方法中用信号(signal)监听post_save,自动创建UserPreference。但本项目为简化,直接在视图里补上这一行。

4.3 现象:点击“喜欢”按钮,浏览器控制台报403 Forbidden,Network 面板显示 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 不兼容(旧版要求 Django<4.0)。
解决:升级到兼容版本:

pip uninstall django-crispy-forms -y pip install "django-crispy-forms>=2.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', type=int, help='Song ID to analyze') def handle(self, *args, **options): song = Song.objects.get(id=options['song_id']) # 假设音频文件路径存于 song.audio_file.path y, sr = librosa.load(song.audio_file.path, sr=None) # 提取10维特征(示例,实际可扩展) tempo, _ = librosa.beat.beat_track(y=y, sr=sr) energy = np.mean(librosa.feature.rms(y=y)) spectral_centroid = np.mean(librosa.feature.spectral_centroid(y=y, sr=sr)) 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(f"Updated feature vector for Song {song.id}")

运行:python manage.py analyze_audio 123

Step 2:修改Song模型,增加audio_file字段

# recommender/models.py from django.core.files.storage import default_storage class Song(models.Model): # ... 其他字段 audio_file = models.FileField(upload_to='audio/', storage=default_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中修改DATABASES:

if 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 数据为 JSON

python manage.py dumpdata --indent 2 > db_dump.json

Step 3:在 PostgreSQL 中创建数据库,并导入

createdb music_db python manage.py loaddata db_dump.json

注意:dumpdata默认包含所有模型,如果只想导recommender应用,加参数recommender。

Step 4:验证
启动生产服务器(DEBUG=False),访问/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( user=request.user, song_id=song_id, is_skipped=True # 新增布尔字段 ) # 关键:降低该歌曲特征在用户偏好中的权重 try: user_pref = UserPreference.objects.get(user=request.user) song = Song.objects.get(id=song_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, name='skip_song')。

改动 2:前端按钮在recommend_list.html中,每个歌曲项下加:

<button onclick="skipSong({{ song.id }})">跳过</button> <script> function skipSong(songId) { fetch(`/skip/${songId}/`, {method: 'POST', headers: {'X-CSRFToken': getCookie('csrftoken')}}) .then(() => location.reload()); } </script>

验证方法:

  1. 用户 A 喜欢一首爵士(特征向量[0.2, 0.8, 0.9, ...]);
  2. 系统将其偏好向量向该方向偏移;
  3. 用户 A 连续跳过3首电子乐(特征向量[0.9, 0.1, 0.2, ...]);
  4. 查看UserPreference.preference_vector,会发现[0.2, 0.8, 0.9, ...]分量升高,[0.9, 0.1, 0.2, ...]分量降低;
  5. 下次推荐,爵士类歌曲排名上升,电子乐下降。

这就是一个最小可行的、可验证的反馈闭环。从那以后我每次做推荐项目,都强制走一遍“用户行为 → 向量更新 → 推荐变化”的端到端验证,哪怕只是用print()输出中间值。它比任何指标都更能告诉你:系统真的在学,而不是在猜。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询