ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Django的协同过滤音乐推荐系统实现与部署

基于Django的协同过滤音乐推荐系统实现与部署 简介这是一套基于PythonDjangoMySQL实现的协同过滤在线音乐推荐系统源码面向计算机相关专业正在准备毕业设计的学生以及需要项目实战练习的初级开发者。系统完整覆盖用户登录注册、音乐列表展示、在线播放、收藏订阅与个性化推荐等核心模块推荐部分采用协同过滤算法可根据用户历史行为生成音乐推荐适合作为毕业设计、课程设计或期末大作业的参考项目。资源包共76个文件含30个Python文件、8个HTML页面、9个CSS样式、7个JavaScript脚本以及字体、音频、图片和SQLite数据库等辅助文件整体约12.84MB。目录按Django项目结构组织包含settings、urls、views、models及推荐算法模块便于理解前后端交互和推荐流程。已有231人参与学习下载。项目经导师指导并获高分评价代码完整、可运行附带数据库备份与Docker部署配置适合希望快速上手并深入理解推荐系统实现的学习者直接使用和二次开发。1. 为什么在线音乐推荐选择协同过滤而不是内容召回音乐推荐和电商推荐有个本质区别用户自己都说不清下一秒想听什么。你在歌单里点了三首民谣系统如果只按歌手或标签给你推同类很快会陷入信息茧房。这个项目用到的协同过滤Collaborative Filtering走的是另一条路——不分析歌曲本身有什么特征只看“和你行为相似的人都在听什么”。这种思路在毕业设计里拿高分恰恰因为它把推荐系统的核心矛盾讲清楚了数据稀疏时如何用群体行为弥补个体偏好。项目技术栈是 Python Django MySQL推荐核心算法放在recommend.py里前端用 Django 模板渲染数据层默认 SQLite 但可以直接切 MySQL。整套代码适合两类人一是计算机专业正在做毕设、需要“算法有实现 系统能演示”的学生二是想快速上手推荐系统工程落地的开发者。下面先拆数据模型再讲算法实现最后给评测和部署方案全程按可复现的标准来。2. Django 数据模型与用户-音乐评分矩阵的落库设计2.1 项目文件结构与 MVC 分层拿到源码包后先看根目录结构整个 Django 项目的组织方式对理解推荐流程很关键music/ # 主应用 ├── models.py # 用户、歌曲、评分、订阅等数据模型 ├── recommend.py # 协同过滤推荐算法核心 ├── views.py # 视图函数处理推荐请求 ├── decorators.py # 登录校验等装饰器 ├── subscribe.py # 订阅/收藏相关逻辑 ├── migrations/ # 数据库迁移文件 └── templates/ # play.html / list.html / user.html 等页面 project/ # Django 项目配置 ├── settings.py # 数据库、静态文件等配置 ├── urls.py # 路由映射 └── wsgi.py / asgi.py docker-compose.yml # MySQL Django 容器编排这个结构是典型的 Django MTV 模式但有一处需要特别注意recommend.py被放在music应用内部相当于算法层直接依赖 ORM。项目里models.py定义了音乐、用户、评分三张核心表它们共同构成了协同过滤所需的“用户-物品评分矩阵”。2.2 核心数据模型User、Music、Rating 表结构进入music/models.py核心模型代码如下from django.contrib.auth.models import User from django.db import models class Music(models.Model): title models.CharField(max_length200, verbose_name歌曲名) artist models.CharField(max_length100, verbose_name歌手) genre models.CharField(max_length50, verbose_name流派, db_indexTrue, blankTrue) duration models.IntegerField(default0, verbose_name时长(秒)) play_count models.IntegerField(default0, verbose_name播放次数) # 注意字段没有用 FileField歌曲文件按 URL 或 ID 关联 # 原因见下文 2.3 说明 url models.URLField(blankTrue, verbose_name音频链接) def __str__(self): return f{self.title} - {self.artist} class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) music models.ForeignKey(Music, on_deletemodels.CASCADE, related_nameratings) score models.FloatField(default0.0, verbose_name评分) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, music) # 一个用户对一首歌只能评一次这段代码里有两个隐含设计点。第一genre字段加了db_indexTrue因为后续按流派做协同推荐的近邻过滤时这是高频查询条件。第二Rating表用unique_together约束避免同一用户对同一首歌曲反复评分导致矩阵出现异常重复项。如果你在自己的项目里用的是FloatField存评分正好符合协同过滤的输入要求——后续算法里可以直接把这个字段转成矩阵数值。注意项目目录里还有个db.sqlite3.bak文件这是带测试数据的备份库。开发阶段直接用 SQLite 跑通流程正式部署再切 MySQL下面讲具体切换步骤。2.3 从 SQLite 切换到 MySQL 的完整步骤源码里requirements.txt是基于 Django 3.x 的切 MySQL 需要额外装驱动。推荐使用mysqlclient安装命令pip install mysqlclient然后在project/settings.py中替换数据库配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: music_recommend, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, } } }注意NAME指定的数据库要提前创建好CREATE DATABASE music_recommend CHARACTER SET utf8mb4;。OPTIONS里的utf8mb4是必须的否则中文歌曲名在写入时会报Incorrect string value错误。执行迁移和数据导出# 生成迁移文件并应用到 MySQL python manage.py makemigrations music python manage.py migrate # 把 SQLite 里的业务数据导出为 JSON python manage.py dumpdata --databasesqlite3 --indent 2 \ music_data.json # 导入到 MySQL注意 settings.py 已切换为 MySQL 配置 python manage.py loaddata music_data.json这里有第三方库和系统库的区别。dumpdata导出时会包含auth.user、sessions等 Django 内置表loaddata导入时会自动处理外键依赖顺序。但如果你之前 SQLite 里的id序列和 MySQL 的自增主键冲突导入后用TRUNCATE清掉涉及的外键表再重导一遍。字段层面遇到contenttypes表重复数据删掉django_content_type表再执行migrate也会自动重建。3. 协同过滤相似度计算与 Top-N 推荐的 recommend.py 实现3.1 ItemCF 与 UserCF 的选型音乐场景为什么要用 ItemCF协同过滤分两大类UserCF 找“与你相似的用户”ItemCF 找“与这首歌相似的音乐”。在音乐推荐场景里我强烈建议用 ItemCF理由有三音乐库的规模通常是百万级但活跃用户远小于物品数UserCF 需要实时计算用户相似度矩阵代价太高音乐消费有很强的“社群性”但 ItemCF 推荐结果更稳定不会因为你偶尔点了首周杰伦就彻底改变推荐列表新歌上架后只要有人听ItemCF 就能立即给出“喜欢 A 的人也喜欢 B”的关联UserCF 很难及时捕捉。实现时通常先建一个“用户-歌曲评分矩阵”再算歌曲之间的余弦相似度。但直接暴力算N x N矩阵在百万元素量级下内存根本扛不住所以要加过滤条件。项目里genre字段正好派上用场——先按同流派圈定候选集再做细粒度相似度计算。3.2 中心化评分与余弦相似度为什么要减去均值再算先看相似度计算的核心代码。项目recommend.py中数据处理部分非常关键我把完整逻辑还原出来import math from collections import defaultdict from django.db.models import Avg from music.models import Music, Rating def build_user_item_matrix(genreNone): 构建用户-歌曲评分矩阵返回 {user_id: {music_id: score}} genre 参数用于按流派过滤缩小候选集 qs Rating.objects.select_related(music, user).all() if genre: qs qs.filter(music__genregenre) matrix defaultdict(dict) for r in qs: matrix[r.user_id][r.music_id] r.score return matrix def normalize_matrix(matrix): 对每个用户的评分离中心化减去个人均值 消除不同用户打分宽严差异有人全打4分有人全打2分 norm defaultdict(dict) for uid, items in matrix.items(): avg sum(items.values()) / len(items) if items else 0 for mid, score in items.items(): norm[uid][mid] score - avg return norm这里的关键步骤是normalize_matrix。协同过滤里如果直接用原始评分算相似度一个习惯打高分的人和一个习惯打低分的人即使口味完全相同余弦相似度也可能很低。减去个人均值后评分离中心化分数变为了“相对个人喜好的偏差”这才是有意义的比较维度。build_user_item_matrix返回的结构是{user_id: {music_id: score}}外层字典的 key 是用户 ID值是内层字典。defaultdict(dict)的好处是当写入matrix[user_id][music_id]时如果user_id不存在会自动创建空字典不需要手动判断键是否存在。3.3 ItemCF 核心基于共同评分用户的歌曲相似度构建完矩阵下一步是计算歌曲之间的相似度。两种常见做法先看基于共同用户的 Jaccard 相似度和余弦相似度def compute_item_similarity(matrix, genreNone): 计算歌曲相似度矩阵 做法遍历每个用户的评分列表对同一用户评分过的歌曲两两配对 # 记录每首歌被哪些用户评分过 music_users defaultdict(set) for uid, items in matrix.items(): for mid in items: music_users[mid].add(uid) # 统计两首歌的共同评分用户数 co_occur defaultdict(lambda: defaultdict(int)) music_norm defaultdict(float) # 每首歌的评分向量模长平方 for uid, items in matrix.items(): mids list(items.keys()) # 对同一用户下的歌曲两两组合 for i in range(len(mids)): for j in range(i 1, len(mids)): mi, mj mids[i], mids[j] co_occur[mi][mj] 1 co_occur[mj][mi] 1 item_sim defaultdict(dict) for mi, related in co_occur.items(): for mj, cnt in related.items(): # 余弦相似度共同用户数 / sqrt(用户数(mi) * 用户数(mj)) denom math.sqrt(len(music_users[mi]) * len(music_users[mj])) if denom 0: item_sim[mi][mj] cnt / denom return item_sim这段代码有三个容易出错的地方。第一for i in range(len(mids))内层从i1开始避免重复配对自己和自己第二music_users统计的是每首歌的评分用户集合在分母计算中用来归一化否则热歌会获得不成比例的相似度优势第三item_sim是嵌套字典item_sim[mi][mj]的值就是“歌曲 mi 与歌曲 mj 的相似度”范围为 0~1。实际工程里可以直接用numpy把矩阵转成稀疏矩阵再算但项目为了展示算法原理用的是纯 Python 实现这在数据量几千到几万条时完全够用。真正需要优化的是co_occur的遍历它是最耗时的部分——用户数越多、每人的歌单越长两次循环的次数就越多。后续优化可以考虑只保留每个用户评分最高的前 N 首歌参与配对丢弃长尾评分。3.4 生成 Top-N 推荐列表加权打分与过滤策略算完相似度推荐列表的生成逻辑如下def recommend_for_user(user_id, top_n10, genreNone): 基于 ItemCF 生成 Top-N 推荐 matrix build_user_item_matrix(genre) norm normalize_matrix(matrix) item_sim compute_item_similarity(matrix, genre) # 用户已评分的歌曲排除这些避免重复推荐 rated set(matrix.get(user_id, {}).keys()) # 候选歌曲得分累加 scores defaultdict(float) for mid, score in norm.get(user_id, {}).items(): if score 0: continue # 跳过低于个人均值的歌曲 for sim_mid, sim_val in item_sim.get(mid, {}).items(): if sim_mid in rated: continue scores[sim_mid] sim_val * abs(score) # 按得分排序返回 Top-N 歌曲 ID 列表 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [mid for mid, _ in ranked[:top_n]]推荐逻辑的核心是累加加权得分。sim_val * abs(score)的含义是用户对某首歌的偏好程度越高从这首歌关联出去的相似歌曲获得的推荐权重也越大。if sim_val 0.2这类过滤条件虽然代码里没写实践中建议加上——相似度低于 0.2 的两首歌基本没有关联价值反而会拉低推荐质量。top_n和genre是两个可调参数。top_n控制推荐列表长度播放页一般展示 10~20 首genre为 None 时全局推荐指定时则做“相似流派内的定向推荐”。注意genre过滤时所有基于该流派构建的矩阵在做recommend_for_user时都要传入一致的值否则用户评分歌单与候选歌单不在一个空间内。3.5 冷启动处理用户没有评分时怎么推荐协同过滤最大的坑是冷启动。新注册用户没有任何评分norm.get(user_id, {})返回空字典推荐结果直接为空。项目里的兜底方案是当推荐列表为空时按play_count降序返回热门歌曲。def recommend_for_user(user_id, top_n10, genreNone): scores ... if not scores: qs Music.objects.filter(play_count__gt0) if genre: qs qs.filter(genregenre) return list(qs.order_by(-play_count)[:top_n].values_list(id, flatTrue)) ...这个逻辑很多推荐系统都在用专业术语叫fallback strategy。热门兜底不是最好的推荐但至少让页面不空给产品留出引导用户打分的时间窗口。如果你的环境里没有play_count数据也可以换成按duration或最近发布时间排序。4. 推荐视图、模板渲染与 Docker 部署全链路4.1 视图函数如何串起推荐流程与播放页面推荐算法写好之后需要通过视图函数暴露给前端。music/views.py中关键视图如下from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from music.recommend import recommend_for_user from music.models import Music login_required def index(request): 首页推荐默认展示 12 首推荐歌曲 user request.user rec_ids recommend_for_user(user.id, top_n12) rec_musics Music.objects.filter(id__inrec_ids) # 维持推荐顺序filter id__in 会打乱顺序 order {mid: i for i, mid in enumerate(rec_ids)} rec_musics sorted(rec_musics, keylambda m: order[m.id]) recent_ratings user.ratings.order_by(-created_at)[:5] return render(request, index.html, { rec_musics: rec_musics, recent_ratings: recent_ratings, }) login_required def play(request, music_id): 播放页展示歌曲信息 插入播放记录 music get_object_or_404(Music, pkmusic_id) # 播放一次就记录一条隐性评分2.0 分 # 作为协同过滤的补充输入这是隐式反馈的核心思想 Rating.objects.update_or_create( userrequest.user, musicmusic, defaults{score: 2.0} ) music.play_count 1 music.save(update_fields[play_count]) # 从同流派下找相似歌曲 similar_ids recommend_for_user(request.user.id, top_n8, genremusic.genre) similar_musics Music.objects.filter(id__insimilar_ids) return render(request, play.html, { music: music, similar_musics: similar_musics, })这段代码里最值得学的是“隐性反馈转评分”的设计。用户点击播放并没有明确打分但视图里把一次播放自动记成2.0分这个数据会进入下一次recommend_for_user的评分矩阵。update_or_create是 Django ORM 提供的方法它会先查user music这条记录是否存在存在则更新score不存在则创建新记录。注意两个排序细节一是Music.objects.filter(id__inrec_ids)返回的记录顺序不保证与rec_ids一致原因在于 SQL 的IN子句默认没有顺序保证所以用order字典重新排序二是similar_musics的排序同样存在这个问题但相似推荐结果偶尔乱序影响不太大因为本来就是一组松散关联的歌。4.2 模板渲染推荐列表与播放页的动态展示项目templates目录下有play.html、list.html、index.html、base.html等模板。base.html定义了公共框架其他模板继承它。看index.html的关键片段{% extends base.html %} {% block content %} div classrecommend-grid {% for music in rec_musics %} div classmusic-card a href{% url play music.id %} h4{{ music.title }}/h4 p classartist{{ music.artist }}/p span classgenre-tag{{ music.genre }}/span /a /div {% empty %} p暂无推荐先去听几首歌吧。/p {% endfor %} /div {% endblock %}{% for music in rec_musics %}遍历视图传过来的推荐列表{% empty %}是 Django 模板内置语法处理空列表场景这样后端不用单独判断空推荐逻辑。渲染逻辑很简单但要注意url标签的使用——必须先在project/urls.py里给播放页路由命名playfrom django.urls import path from music import views urlpatterns [ path(, views.index, nameindex), path(play/int:music_id/, views.play, nameplay), path(user/, views.user, nameuser), path(sign_in/, views.sign_in, namesign_in), path(sign_up/, views.sign_up, namesign_up), ]int:music_id是路径转换器它把 URL 中的play/12/的12转成 Python 整数传给视图的music_id参数。如果转换失败比如传了字母Django 会返回 404避免视图层面再做类型校验。4.3 Docker 一键部署MySQL 与 Django 容器编排项目根目录的docker-compose.yml是部署层的杀手锏内容示例version: 3.8 services: db: image: mysql:8.0 container_name: music_mysql restart: always environment: MYSQL_DATABASE: music_recommend MYSQL_ROOT_PASSWORD: root123456 TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql # 数据持久化防止容器删除后数据丢失 command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci web: build: . container_name: music_django restart: always command: sh -c python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - 8000:8000 depends_on: - db environment: - DB_HOSTdb - DB_NAMEmusic_recommend - DB_USERroot - DB_PASSWORDroot123456 volumes: mysql_data:这里需要注意三点。第一mysql:8.0镜像默认认证插件是caching_sha2_password如果 Django 连不上在docker-compose.yml的command里追加--default-authentication-pluginmysql_native_password可兼容旧客户端。第二command里的makemigrations migrate每次容器启动都执行因为 Django 迁移是幂等的重复执行不会产生副作用这种方式比手动进容器执行命令更适合演示。第三volumes: - .:/code把宿主机代码挂载进容器本地改代码容器立即生效不用重新 build。启动命令docker-compose up -d --build docker-compose logs -f web # 查看 Django 启动日志--build参数强制重建镜像确保Dockerfile里pip install -r requirements.txt的依赖是最新的。logs -f可以跟踪日志输出看到Starting development server at http://0.0.0.0:8000/说明启动成功浏览器访问http://localhost:8000即可。5. 评测指标、冷启动处理与调参验证的实战技巧5.1 离线评估命中率与覆盖率怎么算推荐系统上线前要先做离线评测。评分数据已经存在Rating表里可以直接把数据集按 8:2 划分成训练集和测试集。常见的评估指标是precisionk和recallk对应推荐列表的准确性和完整性。实现思路import random from collections import defaultdict def split_data(rating_qs, test_ratio0.2): test_set defaultdict(set) train_set defaultdict(set) for r in rating_qs: if random.random() test_ratio: test_set[r.user_id].add(r.music_id) else: train_set[r.user_id].add(r.music_id) return train_set, test_set def precision_at_k(recommend_func, test_set, k10): 对每个测试用户生成 Top-K 推荐计算与测试集重叠比例 hit 0 total 0 for uid, test_items in test_set.items(): recs recommend_func(uid, top_nk) recs set(recs) hit len(recs test_items) total min(k, len(test_items)) return hit / total if total else 0注意precisionk的分母min(k, len(test_items))——如果测试集里某用户只有 3 条记录k10时最多只能命中 3 条分母应该用实际可命中的最大值。这个细节很多入门实现会忽略导致指标虚高。recommend_func需要传入一个函数引用方便在评测时替换不同的推荐实现。5.2 调参实战权重系数与相似度阈值recommend_for_user的top_n可以直接传参调整但相似度阈值sim_val目前是硬编码在算法里的。建议在compute_item_similarity返回前做一次过滤def compute_item_similarity(matrix, min_sim0.1): ... item_sim {} for mi, related in co_occur.items(): filtered {mj: cnt / denom for mj, cnt in related.items() if cnt / denom min_sim} if filtered: item_sim[mi] filtered return item_simmin_sim的取值直接影响推荐效果。设得太低如 0.05推荐列表会出现大量无关噪声设得太高如 0.5推荐结果过于集中覆盖率下降。实践中常见的做法是把min_sim从 0.1 起步步长 0.05 递增分别跑评测精度选一个在precisionk和recallk之间均衡的值。项目里的replace_genre_lang.py脚本可以批量清洗genre字段的中文/英文混排这一步对相似度计算的影响比想象中大——如果“流行”和“Pop”同时存在协同过滤会把它们当成两个完全不同的流派处理导致候选集分裂。5.3 线上避坑与日志埋点最后说几个实际部署才会遇到的坑。第一评分数据稀疏时矩阵会很大但有效数据很少推荐响应可能秒级返回。优化方案是给Rating表的user_id和music_id加联合索引db_indexTrue放在user和music字段上确保 SQL 层面快速定位。第二recommend_for_user在每次请求时都全量重建相似度矩阵如果数据量到十万级就会明显变慢。常见做法是把相似度矩阵缓存到 Redis每天凌晨定时任务重算一次。第三播放页的update_or_create会频繁写库建议在play视图里加cache_page(60)装饰器同一首歌 60 秒内的重复播放只走缓存。日志埋点方面Django 默认的runserver控制台会输出每次请求的 SQL 语句开发期排查推荐效率问题很有用。生产部署时建议加django-debug-toolbar它能以面板形式展示每个请求执行了多少条 SQL、哪些查询最慢对定位select_related漏用导致的 N1 查询问题非常有效。到此这套基于 Django MySQL 的协同过滤音乐推荐系统从数据模型到算法实现、再到部署评测已经完整跑通。你在自己的环境里按 2.3 节切库、按 3.4 节改推荐参数、按 5.2 节跑一轮评测就能看到 precisionk 随min_sim变化的曲线这就是推荐系统调参的全部乐趣。本文还有配套的精品资源点击获取
返回列表