
简介一份基于Django的“基于协同过滤的音乐推荐系统”毕业设计完整方案面向计算机专业毕业生及推荐系统开发者。系统采用PythonDjango构建后端MySQL存储数据前端使用Vue.js实现交互界面包含用户管理、音乐分类、音乐信息管理及个性化推荐等模块通过基于用户相似度的协同过滤算法解决数据稀疏性与冷启动问题为音乐平台提供了可落地的智能推荐参考实现。压缩包共562个文件大小约27.69MB主要文件类型包括后端Python源码(.py)、前端Vue组件(.vue)与JavaScript脚本(.js)、数据库SQL脚本(.sql)、项目文档(.docx/.doc)与答辩PPT(.pptx)以及启动配置脚本等目录结构清晰便于按模块学习与二次开发。目前已有108人浏览学习。借助此资源包读者可快速部署运行系统深入理解协同过滤推荐算法在真实项目中的集成方式同时可借鉴其前后端分离架构、数据库设计与文档组织作为毕业设计、课程设计或求职作品的有力参考。1. 为什么说这个协同过滤音乐推荐系统值得你下载每年毕业设计一到三月就会有一批人被「推荐系统」这类课题卡住算法推导能看懂打开别人的源码却不知道文件为什么这么分Django 的 Hello World 会写但把用户登录、音乐管理、收藏行为、推荐列表串成一个完整工程就完全没头绪。这份「基于 Django 的协同过滤音乐推荐系统」就是拿来当脚手架用的——它把基于用户和基于物品两种协同过滤都实现了带完整的数据库文件、设计文档和答辩 PPT项目结构按 Django 的 MVT 规范拆开适合做毕设或课设的本科生、想找个完整 Django 实战项目跟一遍的新手以及想快速在本地跑通推荐流程、验证算法效果的人。2. Django 项目骨架与数据模型设计先把三张表设计明白2.1 项目目录结构Django 的 MVT 组织方式拿到压缩包解压后先别急着pip install然后python manage.py runserver——先看清目录因为后面所有排错都依赖你对「哪个文件该放哪个目录」的判断。典型结构是这样MusicRecommend/ ├── manage.py ├── MusicRecommend/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── music/ # 核心业务应用 │ ├── models.py # 数据模型 │ ├── views.py # 视图函数 │ ├── urls.py # 应用路由 │ ├── admin.py # 后台管理 │ └── migrations/ # 数据库迁移文件 ├── recommend/ # 推荐算法独立模块 │ ├── similar.py # 相似度计算 │ └── recall.py # 候选集生成 ├── static/ ├── templates/ └── db.sqlite3 # 已初始化的数据库这个拆分逻辑建议你保留把推荐算法单独放一个包而不是塞进views.py。原因有两个。第一Django 的惯例是「App 只负责一类业务」推荐算法既不属于用户模块也不属于歌曲模块单独放方便复用答辩时也容易讲清楚代码分层。第二算法模块日后要换实现方案比如从协同过滤换成矩阵分解你只需要改recommend包视图层完全不用动。创建业务应用用python manage.py startapp music这是 Django 的固定动作。重点理解一下migrations目录的机制里面的文件是 Django 自动生成的表结构快照数据库的增删改查全部通过 ORM 完成不需要手动写 SQL。对刚上手的同学我一般建议不要手动去动migrations里的文件改完模型后用makemigrations统一生成新的迁移文件而不是去改旧的。MVT 的运行逻辑一句话就能讲透浏览器请求/music/recommend/路由层urls.py把地址映射到views.py的某个函数函数操作models.py里的模型完成数据库增删改查最后把结果丢给templates/下的模板渲染成 HTML 返回。你在这个工程的任何一处排错其实都在排查这条链路上的某一个环节。2.2 models.py 设计用户、歌曲、行为三张核心表音乐推荐系统的模型不需要特别多核心就是用户、歌曲、用户行为三张表。用户直接复用 Django 内置的User自带认证能力省掉自己写密码校验和 session 逻辑歌曲表和用户行为表要仔细抠字段。先看代码from django.db import models from django.contrib.auth.models import User class Song(models.Model): title models.CharField(max_length100, verbose_name歌名) singer models.CharField(max_length50, verbose_name歌手) genre models.CharField(max_length20, blankTrue, verbose_name流派) play_count models.IntegerField(default0, verbose_name播放次数) cover models.CharField(max_length200, blankTrue, verbose_name封面图URL) file models.CharField(max_length200, verbose_name音频文件路径) class Meta: db_table song verbose_name 歌曲 def __str__(self): return self.title class UserBehavior(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) song models.ForeignKey(Song, on_deletemodels.CASCADE, verbose_name歌曲) score models.FloatField(default0, verbose_name评分) is_favorite models.BooleanField(defaultFalse, verbose_name是否收藏) created_time models.DateTimeField(auto_now_addTrue, verbose_name行为时间) class Meta: db_table user_behavior verbose_name 用户行为 unique_together (user, song) def __str__(self): return f{self.user}-{self.song}字段设计上有三个点值得细说。第一个是外键user和song用 ForeignKey 而不是直接存 ID这样反向查询查某首歌被哪些用户收藏过、admin 后台下拉选择、级联删除都方便。第二个是unique_together它保证同一个用户对同一首歌只有一条行为记录防止用户反复收藏同一首歌时数据重复。实际项目里这里要配合「更新或创建」逻辑而不是直接insert。第三个是评分字段用 FloatField因为协同过滤算法要对评分做归一化和加权运算浮点数能兼容以后改成 10 分制的需求IntegerField 在这里反而限制死了。如果毕设要求必须用 MySQL而不是项目自带的 SQLite只需要改settings.py里的DATABASES配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: music_recommend, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }从 SQLite 切到 MySQL 有个常见的坑SQLite 文件里的数据不会自动同步到 MySQL你必须先在 MySQL 里建好库、设好字符集再重新执行迁移。默认字符集是 latin1 的话中文歌名入库直接变问号这个放到第 5 章专门讲。2.3 数据库迁移实操与数据写入模型改完执行下面两步让表结构落库python manage.py makemigrations music python manage.py migrate第一条命令把models.py的变更生成迁移文件第二条命令把迁移文件应用到数据库。这两个命令是 Django 开发里最常敲的每次改完模型都要执行一遍顺序不要反。如果你是第一次跑一个干净的库建议顺序是把原有的db.sqlite3备份后删掉如果里面已经有过误操作直接删了重来最省事、执行上面两条命令、最后再执行python manage.py createsuperuser创建管理员。需要往库里灌测试数据时我一般在 Django shell 里写循环比在 admin 后台一条条点快得多python manage.py shellfrom music.models import Song songs [ Song(title晴天, singer周杰伦, genre流行, file/music/qingting.mp3), Song(title海阔天空, singerBeyond, genre摇滚, file/music/haikuo.mp3), Song(titleDespacito, singerLuis Fonsi, genre拉丁, file/music/despacito.mp3), ] Song.objects.bulk_create(songs) print(Song.objects.count())这里bulk_create是一次性写入多条记录比循环里一条条save()快一个数量级对毕业设计这种几十条数据无所谓但批量写入的写法在讲代码时是加分项。顺便提醒一个查询集的操作细节UserBehavior.objects.filter(song__id1).delete()删的是行为记录而不是歌曲记录——删除对象时要看清 QuerySet 的过滤条件song__id和song_id在 Django 查询语法里是两种含义前者是跨表查询后者是外键字段名用混了很容易误删数据。3. 协同过滤推荐算法相似度计算与候选集生成的实现3.1 基于用户的协同过滤找口味相似的人推荐系统的核心逻辑用一句话可以概括把用户的历史行为收藏、评分、播放变成特征向量计算实体之间的相似度再把相似实体的偏好推荐给目标用户。难点不在概念而在怎么把相似度算得又快又符合业务直觉。基于用户的协同过滤UserCF分两步走。第一步对每个用户找他「行为最相似」的 K 个邻居用户第二步把邻居们有过正向行为、而目标用户没听过的音乐拿出来按相似度加权排序成 Top-N 列表。它适合用户量大、物品相对稳定、用户兴趣跟随圈子变化的场景——校园音乐站就是典型新生入学后听的歌往往来自同宿舍、同社团同学的推荐。这就是 UserCF 的业务直觉你身边的人在听什么你大概率也会喜欢。相似度计算最常见的做法是余弦相似度。把用户 u 和用户 v 的行为记录看成两个集合余弦相似度等于它们共同收藏歌曲数除以收藏数的几何平均区间落在 0 到 1 之间越接近 1 说明两人的口味越接近# recommend/similar.py from math import sqrt def user_similarity(behaviors): 计算用户两两之间的余弦相似度 behaviors: { user_id: { song_id: score } } 返回: { user_id: { other_user_id: similarity } } # 1. 构建歌 - 用户集合的倒排表 song_to_users {} for user_id, items in behaviors.items(): for song_id in items: song_to_users.setdefault(song_id, set()).add(user_id) # 2. 统计用户两两之间的共同收藏数 cooccurrence {} for song_id, user_ids in song_to_users.items(): for u in user_ids: for v in user_ids: if u ! v: cooccurrence.setdefault(u, {}).setdefault(v, 0) cooccurrence[u][v] 1 # 3. 除以各自收藏数的几何平均得到余弦相似度 similarity {} for u, neighbors in cooccurrence.items(): similarity[u] {} for v, common_count in neighbors.items(): similarity[u][v] common_count / sqrt(len(behaviors[u]) * len(behaviors[v])) return similarity这段代码分三层逻辑。第一步建立倒排表把「用户有哪些歌」翻转为「某首歌被哪些用户收藏」这一步是为了避免后续 O(n²) 的笛卡尔积去遍历所有用户对性能差别在数据量上千以后非常明显。第二步统计共同收藏次数这是整段代码最耗时的部分行为数据一多瓶颈基本都在这。第三步做归一化除以集合大小的乘积开根号让相似度不受用户收藏数量的量级影响——收藏了 500 首歌的用户和只收藏 5 首歌的用户不能因为数量差就天然相似。注意一个细节倒排表里的用户集合用的set所以同一首歌被同一用户反复播放只算一次共同行为这符合「是否听过」的布尔逻辑。如果你要支持评分数据第三步可以换成 Pearson 相关系数把计数器换成评分差值的累加骨架完全不用动。3.2 基于物品的协同过滤用已收藏的音乐找同类ItemCF 是另一个方向不找相似的人找相似的歌。用户收藏了 A系统就算出与 A 最相似的歌曲 B、C把 B、C 推给用户。它在音乐场景通常比 UserCF 更稳因为一首歌的属性风格、歌手、节奏是固定的不像人的兴趣会漂移用户历史积累越久ItemCF 的推荐结果越像「猜你喜欢」而不是「你朋友在听什么」。物品相似度和用户相似度的计算互为镜像def item_similarity(behaviors): 计算歌曲两两之间的余弦相似度 返回: { song_id: { other_song_id: similarity } } # 1. 统计每首歌被多少个不同用户收藏同时统计同现次数 song_count {} song_cooccur {} for user_id, items in behaviors.items(): for s1 in items: song_count[s1] song_count.get(s1, 0) 1 song_cooccur.setdefault(s1, {}) for s2 in items: if s1 ! s2: song_cooccur[s1][s2] song_cooccur[s1].get(s2, 0) 1 # 2. 用共同用户数做余弦归一化 similarity {} for s1, neighbors in song_cooccur.items(): similarity[s1] {} for s2, co_count in neighbors.items(): similarity[s1][s2] co_count / sqrt(song_count[s1] * song_count[s2]) return similarity这段和 UserCF 的差别只在「用户」和「物品」两个词互换外层循环从用户出发遍历他的收藏列表每首歌都和他收藏的其他歌产生一次同现计数分母除以两首歌被收藏次数的几何平均。同一个用户收藏《晴天》和《七里香》各一次这两首歌的同现次数就加一收藏这两首歌的人越多它们的相似度越高。做毕设时建议两种算法都实现然后按下面的维度在答辩 PPT 里对比选型理由对比维度UserCFItemCF推荐逻辑找相似用户推荐相似用户喜欢的歌找相似歌曲推荐与已收藏歌曲相似的歌适合场景用户量小、兴趣跟随圈子变化物品属性稳定、用户历史行为多冷启动表现新用户无行为无法定位相似用户新歌无行为无法参与相似度计算可解释性「和你口味相似的人也喜欢」「因为你收藏了《晴天》」计算热点用户两两相似度随用户数平方增长物品两两相似度随歌曲数平方增长音乐推荐项目我更推荐把 ItemCF 作为主算法、UserCF 作为辅助原因在于可解释性。在推荐页写「因为你收藏了《晴天》所以推荐《七里香》」用户能一秒理解写「因为用户小王和你口味相似」评委大概率追问一句「相似度怎么算的」。ItemCF 的解释路径更短答辩时省很多口舌。3.3 推荐生成与冷启动兜底有了相似度矩阵最后一步是生成推荐列表。下面这段同时兼容 UserCF 和 ItemCF 的相似度矩阵结构接口统一# recommend/recall.py def recommend_for_user(user_id, behaviors, sim_matrix, top_n10): 根据相似度矩阵生成 Top-N 推荐 sim_matrix 可以是 item_similarity 的结果。 interacted set(behaviors.get(user_id, {}).keys()) scores {} # UserCF: 遍历相似用户累加相似度作为候选歌的分数 for other_user, sim in sim_matrix.get(user_id, {}).items(): if sim 0: continue for song_id, score in behaviors.get(other_user, {}).items(): if song_id in interacted: continue scores[song_id] scores.get(song_id, 0) sim * score ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n]生成逻辑有三个原则。第一已交互的歌曲必须过滤否则推荐页会把你收藏过的歌再推一遍演示时显得系统「没干活」。第二分数累加用sim * score而不是简单加 sim这样能兼容评分制数据如果只用布尔收藏score 全是 1加权式也不会出错。第三排序用sorted(..., reverseTrue)在几千条候选数据下完全够用不需要引入额外排序依赖。冷启动是协同过滤自带的短板。新用户没有任何历史行为相似度矩阵里搜不到他推荐结果必然是空列表。最常见的兜底方案是「热门榜补齐」按play_count倒序取前 10 首热门歌塞进推荐接口的返回里页面上标注「热门推荐」。把兜底逻辑写进视图接口而不是写在模板里这样前端只认一个返回结构后续换成混合推荐时改动最小。这个方案不完美但应对毕设演示足够了——老师点开一个没听过歌的新账号看到的不是空页面而是热门歌单体验分直接拉满。4. 把推荐结果送到页面视图、路由与模板渲染4.1 views.py 与 urls.py推荐接口的数据流算法写在recommend包里还得把结果通过 HTTP 送出去。路由层做的是 URL 到视图函数的映射。主项目的urls.py用include把请求分发到 app 的urls.py# MusicRecommend/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(music/, include(music.urls)), ]# music/urls.py from django.urls import path from . import views urlpatterns [ path(recommend/, views.recommend_view, namerecommend), path(song/int:song_id/, views.song_detail, namesong_detail), ]path(song/int:song_id/)里的int:song_id是 URL 转换器Django 会把 URL 里的数字捕获出来并转成 int 传给视图函数。如果不用 int 转换器拿到的就是字符串后面还得手动int()转换一旦 URL 里传了非数字直接 500。路由顺序也有讲究Django 从上往下匹配recommend/要放在song/前面避免前缀相同的路由被跳过去。视图层负责取数、计算、渲染我把流程拆成五步写进代码取当前登录用户、读出所有用户行为、算相似度矩阵、生成推荐列表、渲染模板# music/views.py from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from .models import Song, UserBehavior from recommend.similar import user_similarity from recommend.recall import recommend_for_user login_required def recommend_view(request): # 1. 读出全部行为记录组织成算法要求的嵌套字典 behaviors {} for record in UserBehavior.objects.all(): behaviors.setdefault(record.user_id, {})[record.song_id] record.score # 2. 算相似度矩阵数据量小实时算即可 sim_matrix user_similarity(behaviors) # 3. 生成推荐结果 rec_list recommend_for_user(request.user.id, behaviors, sim_matrix, top_n10) # 4. 兜底冷启动用户返回热门榜 if not rec_list: hot_songs Song.objects.order_by(-play_count)[:10] return render(request, music/recommend.html, {songs: hot_songs, scores: {}}) song_ids [sid for sid, _ in rec_list] scores dict(rec_list) songs Song.objects.filter(id__insong_ids) # 5. 返回模板上下文 return render(request, music/recommend.html, { songs: songs, scores: scores, })这里有一个我踩过的问题Song.objects.filter(id__insong_ids)返回的对象顺序跟song_ids列表顺序不一致。数据库返回顺序由主键或索引决定不是按 IN 条件里的列表顺序。所以如果模板按返回顺序显示排第一的不一定是分数最高的歌。解决办法是保留scores字典模板里用它来控制展示顺序或者把 songs 转成字典后按song_ids重新组装。我一般用后者因为模板里的逻辑越少代码越容易讲清楚。4.2 模板渲染把推荐分数和歌曲卡片展示出来模板层用 Django Template Language 做数据展示。推荐页的核心逻辑是有推荐结果就循环渲染卡片没有结果给空态提示推荐分数保留三位小数显示让用户能感知「推荐度」这个数字的存在!-- templates/music/recommend.html -- div classsong-list {% for song in songs %} div classsong-card h3{{ song.title }}/h3 p{{ song.singer }} · {{ song.genre }}/p span classscore推荐度 {{ scores|default_if_none:0|floatformat:3 }}/span a href{% url song_detail song.id %}去听听/a /div {% empty %} p你还没有足够的历史行为先去多听几首歌推荐会更准。/p {% endfor %} /div{% url song_detail song.id %}是反向解析模板里不硬编码 URL 地址路由地址改一次所有调用它的模板自动跟着变这是 DTL 里值得养成的习惯。floatformat:3把相似度加权的长小数格式化成三位小数不然页面上会显示 0.12345678 这种没法看的一长串。default_if_none:0处理热门榜兜底时 scores 为空字典的情况避免模板取到空值报错。模板里不适合写复杂业务判断。如果你发现模板里{% if %}嵌套超过两层就该考虑把逻辑下沉到视图层。冷启动兜底就是标准例子——判断条件放在视图函数里模板只负责展示结果这样职责边界清晰答辩被追问时也好答。4.3 管理后台不写代码就能维护数据Django 自带的 admin 后台对毕业设计项目非常实用。答辩现场你要演示推荐结果变化如果靠写 SQL 造数据演示节奏会非常僵硬把模型注册进 admin所有的数据库增删改查都变成网页操作# music/admin.py from django.contrib import admin from .models import Song, UserBehavior admin.register(Song) class SongAdmin(admin.ModelAdmin): list_display (title, singer, genre, play_count) search_fields (title, singer) # 顶部的搜索框按歌名/歌手搜 list_filter (genre,) # 右侧按流派过滤 admin.register(UserBehavior) class BehaviorAdmin(admin.ModelAdmin): list_display (user, song, score, is_favorite, created_time) list_per_page 50 # 行为记录多时每页放 50 条list_display决定列表页展示哪些字段列search_fields让搜索走 SQL 的 LIKE 查询list_filter生成侧边栏过滤选项。这些配置不需要写一行前端代码。需要提醒的是admin 后台只对is_staff用户开放创建超级用户之后别急着登——先python manage.py runserver 0.0.0.0:8000把服务拉起来再访问/admin/才有意义。5. 避坑把这份音乐推荐系统跑通前最常见的五个坑5.1 Django 版本太新老项目直接崩现象按说明装好依赖python manage.py runserver控制台立刻报错ImportError: cannot import name url from django.conf.urls或者django.core.exceptions.ImproperlyConfigured。原因老项目urls.py里用的是 Django 2.x 时代的from django.conf.urls import url写法而你的环境里装的是 Django 4.x这个函数早已被移除。网上大部分带毕设性质的 Django 资源都有这个问题下载物自带的依赖说明不兼容新版本环境。解决先看项目里有没有requirements.txt有就按里面钉住的版本装一般这类项目锁的是 Django 2.2 或 3.2。没有的话把urls.py里的url(r^song/(?Psong_id\d)/$, ...)改写成path(song/int:song_id/, ...)兼容性直接解决。我拿到任何 Django 项目第一步都是pip freeze对比版本再决定改代码还是重装环境——这是处理了十来个毕业设计项目攒下的血泪经验。5.2 数据库迁移报「表已存在」越改越乱现象改了models.py加了一个字段执行python manage.py migrate提示Table music_userbehavior already exists或者提示字段已存在但模型定义里没有。原因项目目录里已经有一个初始化过的db.sqlite3文件表结构早就落库你又改了模型或重跑迁移。Django 的迁移是增量式的它只应用「模型与迁移文件不一致」的部分如果数据库、迁移文件、模型三者本来就对不上就会互相矛盾报各种「已存在」或「不存在」的错。解决最省事的办法是把db.sqlite3复制一份备份后删掉然后按顺序重新执行makemigrationsmigrate。如果库里已经造好了要保留的数据用python manage.py migrate music --fake-initial把当前迁移标记为已应用再单独对新增字段做迁移。注意改完表结构后dumpdata导出的 JSON 里字段对不上loaddata也会报错所以先备份 sqlite 文件再动模型是唯一不会后悔的做法。5.3 推荐页空白日志里全是 NaN现象页面能打开但推荐区空无一物切换到 DEBUG 模式后看到ValueError: cannot convert float NaN to integer或float division by zero。原因协同过滤在数学上有个边界情况——两个用户行为集合为空或者某首歌从未被共同收藏余弦公式里0 / sqrt(0 * 0)会得到 NaN。后续排序、渲染遇到 NaN 就直接报错或静默丢弃表现就是推荐结果为空。这属于算法边界问题不是代码写错。解决在相似度函数里给分母加一个极小值common_count / sqrt(len(items_u) * len(items_v) 1e-9)。加 epsilon 不是玄学是数值计算的常规操作行为数据稀疏时也能稳住不会因为一个用户只收藏了一首歌就让整页白屏。同时在生成推荐的循环里过滤掉sim 0和取值为 NaN 的相似度条目双保险。5.4 中文乱码admin 后台显示一排问号现象在 admin 后台新增歌曲中文歌名、歌手名保存后全部变成????页面展示看起来正常但数据库里存的就是问号。原因SQLite 对 UTF-8 支持没问题但如果你按学校要求改用 MySQL建库时默认字符集是 latin1中文直接存不进去。这是「数据库端字符集」和「Django 连接端字符集」两处不一致叠加的结果。解决建库时显式指定字符集CREATE DATABASE music_recommend CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再把settings.py里 DATABASES 的 OPTIONS 加上charset: utf8mb4。utf8mb4 比 utf8 更完整能存 Emoji 和生僻字我所有 Django 项目都直接用 utf8mb4省得以后再改表结构。5.5 创建了超级用户admin 却登不进去现象python manage.py createsuperuser显示创建成功但登录 admin 一直提示「用户名或密码错误」或者登录时直接报no such table: django_session。原因第二种是明确原因——migrate没执行完整Django 的 session 表没建立登录逻辑没法存会话信息。第一种往往也是同一个原因createsuperuser虽然把用户写进了auth_user表但 session 表缺失导致登录流程走不完表现看起来像是密码输入错误。解决先执行python manage.py migrate把 auth、session、admin 相关的系统表全部建好再执行python manage.py createsuperuser重建管理员最后重启服务。如果中途切换过数据库比如从 SQLite 切到 MySQL要确认 MySQL 里有完整的 Django 系统表——只导了业务表、没迁系统表照样卡在这一步。6. 验收推荐算法用三行假数据验证相似度和推荐队列拿到这类带算法的毕设项目我第一件事不是跑页面而是先把算法拆出来用假数据验证。页面可能因为模板报错、路由不通、数据库缺失而失败但如果算法本身输出不对就算页面全绿答辩演示也会露馅。推荐算法不是黑匣子几行手造数据就能把它的行为看清楚。python manage.py shellfrom recommend.similar import user_similarity from recommend.recall import recommend_for_user behaviors { 1: {101: 5, 102: 4, 103: 3}, 2: {101: 4, 102: 5, 104: 2}, 3: {105: 5, 106: 4}, } sim user_similarity(behaviors) print(sim.get(1)) # 期望输出: {2: 0.6666666666666666} recs recommend_for_user(1, behaviors, sim, top_n5) print(recs) # 期望输出: [(104, 1.3333333333333333)]三行假数据能验证三件事。第一用户 1 和用户 2 共同收藏了 101、102 两首歌各自收藏总数都是 3相似度应该是 2 / sqrt(3*3) ≈ 0.6667而用户 3 和用户 1 没有共同歌曲根本不会出现在相似度矩阵里。第二推荐列表中只出现 104 这一首歌101、102 被正确过滤用户 1 已经听过103 来自用户 1 自己所以也被过滤。第三用户 3 不参与推荐他没有与用户 1 的任何共同行为。这套验证应该在拿到项目的第一天就跑一遍比看任何 Django 教程都更能帮你理解 ORM 数据结构和算法输入输出之间的关系。验证算法通过后再看一眼数据库里UserBehavior的数据分布。如果所有用户的行为都集中在同一批歌曲上推荐结果会退化成热门榜这是数据问题不是算法问题——答辩前要主动准备好「数据稀疏」的说明解释清楚为什么推荐列表在某些账号下是空的、兜底热门榜是怎么触发的。算法能跑通只算第一步能解释清楚边界条件才是拿高分的关键。从那以后我每次跑这类毕业设计项目都强制先走一遍三件事核对依赖版本、确认迁移状态、用 shell 假数据验证算法输出非空。上个月帮一个学弟排查推荐页白屏查到最后就是db.sqlite3没删干净导致的迁移错乱——这种问题在毕业设计季几乎天天见提前走完这三步能省掉大半天的排查时间。这套资源的文档和 PPT 是给答辩用的但我觉得最值钱的反而是recommend包里那两个相似度函数把算法单独抽出来验证的思维比任何现成文档都耐用。希望帮到你。本文还有配套的精品资源点击获取