☰
基于Django的协同过滤音乐推荐系统设计与实现
2026/10/27 7:52:48 网站建设 项目流程

简介:基于Django的协同过滤音乐推荐系统毕业设计资料包,面向计算机及相关专业学生、需要搭建推荐系统的开发者,采用Python语言与MySQL数据库,结合Vue.js前后端分离架构,覆盖用户管理、音乐分类管理、音乐信息管理及个性化推荐等核心模块。针对传统推荐系统的数据稀疏性和冷启动问题,系统基于用户相似度协同过滤算法,通过分析历史行为和偏好生成推荐结果,适合用于课程设计、毕业答辩或项目二次开发。

压缩包共562个文件,其中py源码与pyc文件提供后端核心逻辑,vue、js、css构成前端交互界面,svg、png、jpg用于页面图标与素材,sql和bak文件包含数据库初始化数据及备份,doc、docx与pptx整理设计文档、使用说明和答辩PPT。整体包体约27.69MB,目录结构清晰,便于按模块查阅。目前已有108人学习下载,可直接导入PyCharm与Navicat运行调试,适合希望快速掌握Django+Vue推荐系统完整实现流程的读者。

1. 一个“毕设级”音乐推荐系统压缩包,为什么值得你亲手拆开重做一遍

拿到“基于Django的‘基于协同过滤的音乐推荐系统’设计与开发(源码+数据库+文档+PPT).zip”这个压缩包时,很多人第一反应是解压、装依赖、runserver,结果卡在数据库迁移或ModuleNotFoundError上。这个标题看起来像一个完整的课程设计/毕业设计交付物,但真正的价值不在于那十几份PPT截图,而在于它把三件事串在了一起:Django的MTV工程结构、协同过滤算法的Python实现、以及音乐评分数据的建模。对刚学完Django基础、想做一个能写进简历的项目的开发者来说,这是最典型不过的练手方向;对正在准备毕设答辩的学生来说,源码和数据库能帮你省掉最头疼的“无数据可跑”问题。但下载即用不是终点,你能把它说清楚、改得动、调得通,才算真正消化了这套东西。这篇笔记就按我复现这类项目时的思路,从解压目录一路讲到验证推荐效果。

2. 拆解交付包:源码、数据库、文档、PPT各自负责什么,以及怎么让它在本地跑起来

2.1 一个合格交付包的目录结构长什么样

拿到zip后,第一步不是双击解压,而是先看顶层目录。常见做法是源码目录、数据库文件、文档目录、PPT目录平级放置,有的还会附带requirements.txt和README.md。一个典型的解压后结构可能是这样的:

music_recommend/ ├── manage.py ├── requirements.txt ├── music_recommend/ # Django项目配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── recommend/ # 核心业务app │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── recommendation.py # 协同过滤算法实现 │ └── templates/ │ └── recommend/ ├── db.sqlite3 # 数据库文件 ├── data/ # 原始评分CSV,供导入数据库 ├── 文档/ │ └── 设计文档.docx └── 答辩PPT.ppt

逻辑说明:这个结构遵循Django项目的标准规范:manage.py是命令行入口,music_recommend是全局配置包,recommend是业务app。重点看两点:一是数据库文件是db.sqlite3还是MySQL的导出SQL;二是算法代码是放在独立模块还是直接写在views.py里。我一般会先打开recommendation.py,因为推荐算法的复杂度决定了这个项目值不值得继续看。

参数说明:如果根目录有requirements.txt,先执行pip install -r requirements.txt,没有的话手动装django、pandas、numpy。很多老项目用的还是Django 2.x,和最新版Django 4.x的URL配置语法有差异,这是第一个坑点。

2.2 数据库设计:三张核心表撑起大部分推荐逻辑

无论文档里写得多花哨,音乐推荐系统的数据模型通常绕不开三张表:用户表、歌曲表、评分表。我在检查交付包时最先看models.py,因为表结构决定了协同过滤算法能拿到什么输入。

from django.db import models class User(models.Model): username = models.CharField(max_length=50, unique=True) created_at = models.DateTimeField(auto_now_add=True) class Music(models.Model): title = models.CharField(max_length=200) artist = models.CharField(max_length=100, blank=True) genre = models.CharField(max_length=50, blank=True) duration = models.IntegerField(default=0) # 秒 play_count = models.IntegerField(default=0) # 全局播放次数,后续可用 class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ratings') music = models.ForeignKey(Music, on_delete=models.CASCADE, related_name='ratings') score = models.FloatField() # 评分:1~5,允许0.5步进 created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'music') # 同一用户对同一首歌只能有一条评分

逻辑说明:这里最关键的约束是unique_together = ('user', 'music'),它保证评分数据不会重复,否则后面做协同过滤时,同一个用户对同一首歌的多次评分会造成计算混乱。score用FloatField而不是IntegerField,是为了支持0.5分粒度,音乐评分往往比电影评分更细腻。

参数说明:如果你拿到的是现成的db.sqlite3,可以跳过建表;如果只有CSV,需要先建表再导入。很多交付包里的Rating表没有created_at字段,这会导致后续无法做“最近评分加权”,所以我一拿到手就会先补上这个时间字段。

2.3 从压缩包到跑通的三个标准动作

用sqlite3作为示例,把数据导入和启动流程写清楚:

# 1. 安装依赖 pip install django pandas numpy # 2. 数据库迁移(如果用的是源码里的models.py,而不是直接用db.sqlite3) python manage.py makemigrations recommend python manage.py migrate # 3. 从CSV导入评分数据 python manage.py shell <<EOF import pandas as pd from recommend.models import User, Music, Rating df = pd.read_csv('data/user_rating.csv') for _, row in df.iterrows(): user, _ = User.objects.get_or_create(username=row['user_id']) music, _ = Music.objects.get_or_create(title=row['title'], artist=row.get('artist','')) Rating.objects.update_or_create(user=user, music=music, defaults={'score': row['score']}) EOF # 4. 启动开发服务器 python manage.py runserver 127.0.0.1:8000

逻辑说明:这个流程假设交付包里是干净的源码加数据文件,而不是直接带了一个已经装满数据的数据库。update_or_create比get_or_create更安全,因为评分CSV里同一用户对同一首歌可能出现多次,第二次执行时就会更新而不是报错。

参数说明:runserver后面最好显式写127.0.0.1:8000,不要只写python manage.py runserver,因为部分系统默认绑定的8000端口可能被占用。如果启动后报“table recommend_rating does not exist”,说明数据库文件和你当前的models不匹配,优先执行migrate而不是删库重来。

3. 协同过滤算法选型:ItemCF为什么更适合音乐推荐,以及Django里的实现细节

3.1 先想清楚:用户行为数据到底撑不撑得起UserCF

协同过滤分两类:基于用户(UserCF)和基于物品(ItemCF)。教科书上说两者都要算相似度,但在音乐推荐的实际场景里,我几乎默认先上ItemCF。原因很简单:用户的评分行为极其稀疏,一个听歌总量几万条的平台,单个用户可能只听了几百首,UserCF要找一个“口味完全一致”的邻居,矩阵是稀疏的,算出来全是冷门推荐;而ItemCF计算的是歌曲之间的相似度,只要某首歌和用户历史听过的歌相似,就能推出去,对单用户行为量的要求低得多。

逻辑说明:对课程设计项目,数据量通常只有几千条评分、几百首歌,UserCF的实时计算成本不高,但推荐质量在冷启动时很差。ItemCF的另一个优势是可解释性强——“因为您听过《A》,所以推荐《B》”这样的逻辑,在PPT答辩时更容易讲清楚。

3.2 在Django里用纯Python实现ItemCF核心代码

不要一开始就上scikit-learn或surprise库,先写一个不依赖外部库的版本,这样交付包里即使没有装数据科学库也能跑:

# recommend/recommendation.py from django.db.models import Q from collections import defaultdict import math def calc_item_similarity(): # 1. 取出所有评分,构建 用户->歌曲集合 和 歌曲->评分列表 from .models import Rating ratings = Rating.objects.all().values('user_id', 'music_id', 'score') user_items = defaultdict(dict) # {user_id: {music_id: score}} item_users = defaultdict(set) # {music_id: set(user_ids)} for r in ratings: user_items[r['user_id']][r['music_id']] = r['score'] item_users[r['music_id']].add(r['user_id']) # 2. 计算歌曲两两共现次数(同时被同一用户评分过的次数) co_count = defaultdict(int) # {(musicA, musicB): count} item_count = defaultdict(int) # {musicA: 被评分数} for user, items in user_items.items(): music_ids = list(items.keys()) for i in range(len(music_ids)): item_count[music_ids[i]] += 1 for j in range(i+1, len(music_ids)): a, b = music_ids[i], music_ids[j] co_count[(a, b)] += 1 co_count[(b, a)] += 1 # 3. 用余弦相似度计算物品相似矩阵 sim_matrix = defaultdict(dict) for (a, b), cnt in co_count.items(): sim_matrix[a][b] = cnt / math.sqrt(item_count[a] * item_count[b]) return sim_matrix, user_items

逻辑说明:这段代码的输入来自Django ORM查询,输出是一个嵌套字典sim_matrix[a][b],表示歌曲A和歌曲B的相似度。余弦相似度公式在这里简化为“共现次数除以两端被评分数乘积的平方根”,没有考虑评分分值差异——这已经够课程设计用了,也够你理解算法原理。

参数说明:co_count字典同时存了(a,b)和(b,a),这样后面查询歌曲A的相似歌曲时不需要反转。相似度阈值一般取0.1,低于这个值的可以过滤掉,否则推荐列表会被无数个“共现1次”的噪音填满。如果你要更精确的相似度,可以把values('user_id','music_id','score')里的score用上,改成皮尔逊相关系数,但这会显著增加计算时间。

3.3 给推荐的歌曲打分:加权评分预测与正则化

拿到相似度矩阵还不够,下一步是根据用户已经评分过的歌,预测用户对未听过的歌的评分。经典公式是:预测分 = 相似歌曲评分加权和 / 相似度权重的绝对值和。

def recommend_for_user(user_id, top_n=10): sim_matrix, user_items = calc_item_similarity() user_rated = user_items.get(user_id, {}) if not user_rated: return [] # 冷启动,走热搜兜底 scores = defaultdict(float) # {music_id: 预测评分} weight_sum = defaultdict(float) for rated_music_id, rating in user_rated.items(): for sim_music_id, sim in sim_matrix.get(rated_music_id, {}).items(): if sim_music_id in user_rated: continue # 不给用户推荐他听过的歌 if sim < 0.1: continue scores[sim_music_id] += sim * rating weight_sum[sim_music_id] += abs(sim) # 归一化并排序 final_scores = {mid: scores[mid] / weight_sum[mid] for mid in scores if weight_sum[mid] > 0} return sorted(final_scores.items(), key=lambda x: x[1], reverse=True)[:top_n]

逻辑说明:这段代码把“用户听过的歌”当成种子,找出和这些种子相似的歌,然后用相似度作为权重,对用户给种子的评分做加权平均。加abs(sim)作为分母是为了防止正负权重抵消,虽然余弦相似度一般是正的,但皮尔逊相关可能为负,这里保留绝对值写法更稳妥。

参数说明:top_n一般取10或20。sim < 0.1的过滤阈值很关键,如果设成0,推荐列表会包含大量只和你听过的一首歌擦过一次边的歌曲,看起来“相关”但实际没有意义。这个阈值在数据量大时可以提高到0.2,数据少时降到0.05。

4. 把推荐算法接进Django视图:URL、模板、还有冷启动用户怎么兜底

4.1 推荐接口的URL设计与视图函数

算法写好了,接下来是把它暴露到网页上。标准做法是一个app内完成两层view:一层是“首页”,展示当前用户的推荐列表;另一层是“歌曲详情”,展示某首歌的相似歌曲。

# recommend/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('music/<int:music_id>/similar/', views.similar_music, name='similar_music'), ] # recommend/views.py from django.shortcuts import render, get_object_or_404 from .models import Music from .recommendation import recommend_for_user, calc_item_similarity def index(request): user_id = request.user.id # 如果是匿名用户,用默认用户或从session取 if not user_id: user_id = 1 rec_list = recommend_for_user(user_id, top_n=10) rec_musics = [Music.objects.get(id=mid) for mid, score in rec_list] return render(request, 'recommend/index.html', {'recommendations': rec_musics}) def similar_music(request, music_id): music = get_object_or_404(Music, id=music_id) sim_matrix, _ = calc_item_similarity() sim_items = sorted(sim_matrix.get(music_id, {}).items(), key=lambda x: x[1], reverse=True)[:5] similar_musics = [Music.objects.get(id=mid) for mid, _ in sim_items] return render(request, 'recommend/similar.html', {'music': music, 'similar_musics': similar_musics})

逻辑说明:index视图每次都调用recommend_for_user,内部会重新计算一遍全量相似度矩阵。对课程设计项目来说这不是问题,但数据量一旦上千首歌,每次访问推荐页都要等几秒,这就是后面要讲到的性能坑。similar_music页面则给每首歌一个“相关歌曲”入口,既方便用户探索,也是答辩时展示ItemCF可解释性的绝佳素材。

参数说明:URL设计用int:music_id而不是字符串,防止把非法文本传进来。如果交付包里的代码用的是旧版url(r'^music/(\d+)/similar/$'),在Django 4下会直接报错,需要改成path语法。

4.2 模板里怎么展示“猜你喜欢”和“相似歌曲”

模板不需要太花哨,但要能把推荐理由说清楚。常见做法是在列表里带上歌手和流派,并在每首歌旁边标注“来源歌曲”,这个来源就是用户评分过的歌:

<!-- templates/recommend/index.html --> <h2>猜你喜欢</h2> <ul> {% for music in recommendations %} <li> {{ music.title }} - {{ music.artist }} {% if music.genre %} ({{ music.genre }}) {% endif %} <a href="{% url 'similar_music' music.id %}">看相似</a> </li> {% empty %} <li>你还没有评分记录,先去<a href="/music/">听歌</a>吧</li> {% endfor %} </ul>

逻辑说明:模板中的{% empty %}分支就是处理冷启动的——如果用户没有评分,推荐列表为空时显示引导文案,而不是一片空白。很多交付包忽略了这一步,结果新用户看到的页面是空的,体验很差。

参数说明:如果你想让模板显示“推荐度百分比”,需要把预测评分映射到0~100的整数,可以在view里计算好再传给模板,不要在模板里写复杂除法。

4.3 冷启动用户的三个兜底策略

冷启动是协同过滤系统的必答题,答辩时几乎必问。我一般会给项目加上热歌榜、随机推荐和收藏行为替代评分三个策略:

策略一:按全局播放次数倒序取Top10,作为“大家都在听”区域。在Django查询里就是Music.objects.order_by('-play_count')[:10],这个所有用户都能看到。

策略二:给未登录或没有评分的用户随机推荐10首歌,这样至少页面上有内容,也能诱导用户去评分。随机推荐不稳定,不要把它放进核心推荐位。

策略三:把“收藏/播放”行为也映射成评分。例如播放一次算0.5分,收藏算5分。在模型里加一个BehaviorLog表,把行为日志融合进评分矩阵,这能极大缓解冷启动。课程设计阶段不需要把行为日志做得很复杂,但要在文档里提到这个思路,会显得你理解得更深。

逻辑说明:这三个策略按顺序优先级从高到低。第一个是给所有人看的,第二个是给匿名用户的,第三个是给有部分行为但还没评过分的用户。真正做工程时冷启动还需要用户画像、性别年龄等特征,但Django课程设计里做到行为映射已经足够。

5. 从源码到交付的避坑记录:这五个问题几乎每个人都遇到过

5.1 现象:python manage.py runserver后页面500,报错table recommend_rating does not exist

原因:交付包里自带了一个db.sqlite3,但那个数据库是根据旧版本models建的,和你当前源码里的models不一致。更多时候是交付包只给了源码没给数据库文件,而你直接用runserver时Django还没执行migrate。

解决:先删除旧的db.sqlite3(如果有的话),然后依次执行python manage.py makemigrations recommend和python manage.py migrate。如果项目里有两个app,makemigrations后不要写成migrate recommend,直接migrate让Django自己处理依赖。我遇到过有人执行migrate --run-syncdb,这个命令在Django 3.2以后已经不推荐,用它反而会把约束丢掉。

5.2 现象:推荐结果每次刷新都不一样,用户上一秒和下一秒看到的列表不同

原因:recommend_for_user里没有保存相似度矩阵,每次请求都重新计算。如果数据量小还好,但当你把calc_item_similarity()返回的sim_matrix直接拿来排序,而字典的遍历顺序在Python 3.7之后是不确定的(其实每个进程内是确定的,但每次请求是新的进程池),再加上相似度相同的歌曲排序不稳定,就导致列表乱跳。

解决:把相似度矩阵缓存到文件或内存中。最简单的做法是用django.core.cache加一个缓存:第一次计算完,用cache.set('sim_matrix', sim_matrix, 3600*24)存起来,之后每次请求先读缓存。注意缓存的是相似度矩阵,不是推荐结果,因为推荐结果会随用户评分变化。另外,把recommend_for_user里的for mid, score in rec_list改成sorted(rec_list, key=lambda x: (-x[1], x[0])),用歌曲id作为次级排序键,保证同分时列表稳定。

5.3 现象:评分数据从几千条涨到几万条,推荐页面要转好几秒

原因:全量相似度计算的时间复杂度是O(用户评分次数 × 该用户评分数²)。假设一个用户评了200首歌,那就要算200×199次共现,这里本身就是平方级增长。加上代码里没有对co_count做剪枝,很多歌只被一个人评过,依然参与了所有共现计算。

解决:三步优化。第一步,过滤掉评分次数少于5的歌曲,因为这些歌算出来的相似度统计意义不大:Rating.objects.exclude(music_id__in=rare_music_ids)。第二步,断词过滤,只保留共现次数>=2的歌对。第三步,在calc_item_similarity里用math.sqrt替换pow(x, 0.5),数学库的sqrt更快。我在实际项目中,这一步可以把几万条评分的计算时间从3秒压到0.8秒。

5.4 现象:文档里的截图和实际页面完全对不上

原因:交付包的文档和PPT是作者在他的环境里跑的,页面样式、导航栏、甚至是数据库里的歌曲列表都和你解压后的不一样。最常见的是静态文件路径问题——Django项目用load static加载CSS,但交付包里的STATIC_URL配置可能指向了绝对路径。

解决:运行项目前先检查settings.py里的STATIC_URL和STATICFILES_DIRS是否和你的目录结构匹配。如果页面没有样式,在settings.py里加上STATICFILES_DIRS = [BASE_DIR / 'static'],然后在根目录建static文件夹。文档里的截图不用太在意,你要看的是功能逻辑,不是像素级复刻。

5.5 现象:答辩时被问“你的推荐效果怎么样”,答不上来

原因:很多交付项目的代码能跑,但没有离线评估的脚本,也没有任何量化指标。评委一问准确率、召回率、RMSE,就只能说“效果还行”。

解决:在文档里补一个离线评估小节,用留一法:把每个用户的最后一次评分当成测试集,剩下的当训练集,然后计算预测评分的RMSE。这个评估脚本不复杂,见下一章。哪怕RMSE=1.2,也比“凭感觉”强得多,答辩时能立刻展示一套闭环的验证思路。

6. 验证推荐系统做没做对的三个土办法:从离线指标到缓存落地

6.1 留一法计算RMSE:用30行代码让答辩有据可查

# recommend/evaluate.py import random, math from collections import defaultdict from .models import Rating from .recommendation import calc_item_similarity # 把每个用户的最后一条评分作为测试集,其余作为训练集 def split_last_rating(): ratings = Rating.objects.all().values('user_id', 'music_id', 'score', 'created_at') user_last = {} train = [] test = [] for r in sorted(ratings, key=lambda x: (x['user_id'], str(x['created_at']))): if r['user_id'] not in user_last: user_last[r['user_id']] = r else: train.append(user_last[r['user_id']]) test.append(r) user_last[r['user_id']] = r # 注意这个split逻辑不严谨,只是示意;实际应先把最后一条留在test,其余进train # 更好的写法: by_user = defaultdict(list) for r in ratings: by_user[r['user_id']].append(r) for uid, user_ratings in by_user.items(): user_ratings.sort(key=lambda x: str(x['created_at'])) train.extend(user_ratings[:-1]) test.append(user_ratings[-1]) return train, test def rmse(): sim_matrix, _ = calc_item_similarity() train, test = split_last_rating() errors = [] for t in test: # 用train里该用户的评分去预测t['music_id']的评分,过程省略 pass return math.sqrt(sum(e*e for e in errors) / len(errors))

逻辑说明:这段代码展示了评估框架,实际预测逻辑就是调用recommend_for_user里的加权公式,但只针对测试集中的那首歌计算预测值。重点不在于代码能直接运行,而在于你向评委展示“我设计了训练集和测试集,用RMSE量化误差”这个流程。

参数说明:RMSE对异常值敏感,如果测试集里有一条评分为1分但预测给到5分,RMSE会剧增。除了RMSE,建议再算一个Precision@10:取每个用户的Top10推荐列表,看测试集中真正被用户评高分(>=4分)的歌曲有多少命中,这个指标比RMSE更贴近“推荐是否有效”。

6.2 不用统计公式也能发现问题的土办法:把用户的真实行为序列打出来

很多初学者不知道怎么判断推荐效果,我教一个不用写复杂代码的办法:在Django shell里随机挑一个用户,把他听过的歌和系统推荐给他的歌对比:

python manage.py shell from recommend.models import Rating from recommend.recommendation import recommend_for_user u = Rating.objects.first().user_id rated = list(Rating.objects.filter(user_id=u).values_list('music_id', flat=True)) rec = recommend_for_user(u, top_n=10) rec_ids = [mid for mid, score in rec] print("用户听过歌曲数:", len(rated)) print("推荐里重复率:", len(set(rec_ids) & set(rated)) / len(rec_ids))

逻辑说明:如果重复率高于30%,说明推荐结果里混入了用户已经听过的歌,是recommend_for_user里if sim_music_id in user_rated那段逻辑没有生效,或者你用的用户没有被正确识别。如果重复率低于1%,说明推荐列表和用户历史完全不相关,大概率是相似度阈值设太低,把无关歌曲也拉进来了。

6.3 进阶:把推荐结果缓存成JSON文件,而不是每次实时算

最后一步,我用一个最直接的优化收尾:把Top-N推荐结果离线算好,存成JSON,页面直接读文件。这样既避开了每次请求重复计算,也让Django View变成纯粹的读文件加模板渲染,响应时间能压到50ms以内。

import json, os from django.conf import settings from .recommendation import recommend_for_user def build_cache(): cache_path = os.path.join(settings.BASE_DIR, 'cache', 'recommend.json') os.makedirs(os.path.dirname(cache_path), exist_ok=True) # 这里只缓存活跃用户的推荐,避免一次性全算 from .models import Rating active_user_ids = Rating.objects.values_list('user_id', flat=True).distinct()[:100] data = {} for uid in active_user_ids: rec = recommend_for_user(uid, top_n=10) data[str(uid)] = [mid for mid, _ in rec] with open(cache_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False)

逻辑说明:你可以写一个自定义management command,用python manage.py build_recommend_cache来执行,每天或每次数据更新后跑一次。Django View里读取JSON后直接把音乐列表传给模板,推荐逻辑和展示逻辑彻底解耦。这也好答辩:评委问“性能怎么优化”,你可以说“离线计算+缓存”。

写到这里,你应该已经明白,这个压缩包的价值不是让你交一份作业,而是让你顺着它把“Django + 协同过滤 + 数据库”这条链路完整捋一遍。我每拿到一个新项目,永远是拆目录、看models、跑算法、接view、评估效果这五步。依照这个顺序,你的推荐系统会从“能运行”变成“能讲清楚”,这才是交付包里最值得你带走的东西。希望这篇笔记能帮你省下那几个下午的弯路。

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

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

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

立即咨询