十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

音乐推荐系统毕设全攻略:协同过滤+Django+可视化

音乐推荐系统毕设全攻略:协同过滤+Django+可视化 每年毕业季总有一批人被选题折磨到头秃音乐推荐系统属于那种“看起来平平无奇、写起来真香”的经典方向。它在用户量、数据量、推荐效果、可视化展示上都有足够多的发挥空间而且既能体现算法功底又能体现工程能力。我带过的学生里用这套思路做出来的系统答辩时几乎没有被问倒过。这篇文章就把这套毕设的完整思路拆开讲技术栈为什么这么选、协同过滤怎么落到代码里、数据仓库和分布式计算在毕设中到底怎么体现以及我踩过的坑和排查经验。不管你手头是急着定方案还是已经写完一半想补细节这篇都能直接拿来当参考。1. 内容整体设计与思路拆解1.1 项目定位为什么选音乐推荐而不是电影推荐很多同学第一反应是电影推荐但电影推荐已经被做烂了评委问的问题也容易被背诵答案。音乐推荐的优势在于数据获取路径清晰、用户行为维度丰富播放、收藏、下载、点赞、跳过而且可以同时用“基于用户的协同过滤”和“基于物品的协同过滤”做对比实验这部分内容很容易扩展成论文里的实验章节。从数据量来看音乐平台天然具备“长尾”特征热门歌曲被少数人反复播放冷门歌曲分布在大量用户的长尾行为中。协同过滤在这种数据分布下能体现的优势和劣势都要比电影数据明显这对毕设来说反而是好事——有现象可分析、有对比可写。从可视化角度音乐推荐系统可以做的图表类型非常丰富歌曲热度排行柱状图、用户活跃时段折线图、风格占比饼图、地域分布地图。Echarts在这些场景下全部覆盖毕设展示环节天然好看不用额外凑页面。1.2 技术栈选型的真实理由这套技术栈看起来是拼凑但实际上每个组件都有明确分工而且非常贴合本科毕设的评分标准。Django承担的是后端业务逻辑和模板渲染。它内置的MTV模式在答辩时非常好讲清楚Model负责和数据库打交道Template负责页面呈现View负责业务处理。加上Django自带Admin后台你可以直接拿后台管理歌曲、用户、评论数据省掉一整套管理端开发的时间把精力留给推荐算法和可视化。Bootstrap的作用是让前端不拉胯。音乐推荐系统的页面不算复杂但好歹有首页、榜单页、推荐页、用户中心几个模块。Bootstrap的栅格系统和现成组件能让页面从“学生作品”直接跨到“像模像样”。如果你愿意再花点时间配一个现成的AdminLTE或SB Admin模板整个系统的高级感立刻不一样。Echarts是可视化的核心输出工具。它跟Django的结合方式其实非常简单后端把统计数据序列化成JSON前端通过Ajax拿数据再渲染图表。Echarts中国地图、折线图、柱状图、饼图这些在毕设里属于“高频但实现成本低”的场景只需要注意几个细节后面我会讲到。它完全不需要后端参与绘图性能压力很小数据量大时只承担数据接口的角色就可以了。协同过滤算法是整个系统的灵魂。它不像深度推荐模型那样需要GPU和大量样本一个几百行代码的Python实现就能跑出不错的效果解释起来也直观跟你有相似品味的人听过的歌你大概率也喜欢。这种“可解释性”对答辩非常关键评委不需要你讲清楚神经网络的反向传播但要能听明白你的推荐逻辑。1.3 系统功能模块划分我建议把系统拆成六个核心模块对应到Django的App结构里users模块用户注册、登录、个人信息维护使用Django自带的认证体系扩展music模块歌曲、歌手、专辑、风格的管理与展示数据来源于爬取或公开数据集behavior模块用户行为日志的记录与查询比如播放、收藏、下载记录recommend模块协同过滤核心算法负责计算相似度、生成推荐列表stats模块统计分析和数据导出给可视化提供JSON数据接口visual模块可视化大屏页面和图表渲染承载Echarts全部内容模块之间通过ORM模型关联Behavior表同时外键关联User和Song。这样设计的好处是答辩时被问到“模块之间如何解耦”可以直接回答通过数据模型解耦算法模块只依赖行为数据和歌曲数据不关心用户界面。2. 核心细节解析与实操要点2.1 协同过滤算法原理、公式与代码落地协同过滤最核心的假设是如果用户A和用户B在历史行为上相似那么A喜欢的物品B也大概率喜欢。实现路径有两种主流方向。基于用户的协同过滤UserCF计算用户之间的相似度找到相似用户集合再推荐相似用户喜欢但当前用户没听过的歌曲。相似度计算最常用的是余弦相似度和皮尔逊相关系数。余弦相似度的公式是similarity(u, v) (u·v) / (|u| * |v|)在音乐推荐场景里u和v是两个用户对歌曲的评分向量。但实际中我们拿到的往往是隐式反馈播放/跳过不是显式评分所以需要做一个转化播放次数 0记为1播放次数为0记为0或者用播放次数本身作为权重。基于物品的协同过滤ItemCF计算歌曲之间的相似度推荐“和你听过的歌相似的歌”。它比UserCF更适合音乐场景因为歌曲数量相对稳定相似度矩阵的更新成本低而且结果更容易解释因为你听过《晴天》所以推荐同样风格、同样歌手或者听歌习惯相似的人常听的《七里香》。我给出的代码实现建议是先构建“用户歌曲”矩阵用Pandas的pivot_table用sklearn的cosine_similarity或手写余弦相似度计算对目标用户遍历其历史歌曲取每首歌的TopN相似歌曲汇总相似度分数过滤掉听过的返回TopN手写一个简化版import math from collections import defaultdict def user_similarity(user_items): # user_items: dict, key是用户ID, value是歌曲ID集合 user_sim defaultdict(dict) for u in user_items: for v in user_items: if u v: continue common user_items[u] user_items[v] if not common: continue sim len(common) / math.sqrt(len(user_items[u]) * len(user_items[v])) user_sim[u][v] sim return user_sim def recommend(user_id, user_items, user_sim, top_n10): # 找出TopK相似用户 sim_users sorted(user_sim.get(user_id, {}).items(), keylambda x: x[1], reverseTrue)[:20] rec_score defaultdict(float) for v, sim in sim_users: for song in user_items[v]: if song not in user_items[user_id]: rec_score[song] sim return sorted(rec_score.items(), keylambda x: x[1], reverseTrue)[:top_n]注意这个版本是教学用的直观写法数据量上千用户、上万歌曲后效率会比较差。毕设场景只要在数据导入时预计算好相似度矩阵并存入数据库推荐请求时直接查表就行没必要实时跑全量计算。2.2 数据仓库分层设计不是堆概念毕设里谈数据仓库多半是为了应对评委那句“你的数据量就这么点跟数据仓库有什么关系”。但这不代表数据仓库是纯凑字数它确实能帮你把零散的日志数据整理成可用的分析基础。我建议在项目中搭建一个轻量分层的处理流程不引入太重的基础设施ODS层原始数据层存储爬虫拿到的原始JSON日志、用户行为原始记录保持原样不动DWD层明细数据层清洗后的明细数据剔除缺字段的记录统一时间格式生成标准化的播放日志表DWS层汇总数据层按用户、歌曲、风格等维度做聚合统计比如每个用户的播放总时长、每首歌的播放次数ADS层应用数据层面向推荐算法和可视化报表的数据比如用户歌曲评分矩阵、歌曲热度排行在Django项目里不一定要引入独立的数仓工具可以把ODS对应原始导入文件DWD对应清洗后的导入脚本DWS和ADS对应数据库中的聚合表。这样既体现了分层思想又不会因为大数据组件环境问题导致项目跑不起来。数据仓库的建模中星型模型是音乐场景的好选择事实表是播放记录维度表是用户表、歌曲表、时间表。用图解释时清晰写SQL时也好写聚合效率高。2.3 Django的MTV模式到底帮你省了什么Django的MTV是答辩必问题但很多学生讲得抽象。我用音乐系统的实际代码来拆解Model模型定义数据结构。比如一个歌曲表class Song(models.Model): title models.CharField(max_length200) artist models.CharField(max_length100) genre models.CharField(max_length50) duration models.IntegerField() play_count models.IntegerField(default0) created_at models.DateTimeField(auto_now_addTrue)View视图处理请求逻辑。推荐接口的视图长这样def recommend_view(request): user request.user rec_list get_recommendations(user.id, top_n20) return JsonResponse({code: 0, data: rec_list})Template模板渲染HTML页面默认用Django模板语言DTL。所有涉及用户输入输出的页面都用DTL做变量替换和循环展示。MTV模式的核心优势在于职责分离前端人员改模板不影响后端逻辑后端调整数据结构不影响页面展示。虽然是毕设单兵作战但这种结构能让你在写到后期时不至于改一个功能牵一发动全身。2.4 分布式计算毕设里的“轻分布式”“分布式计算”放在标题里确实有点吓人。实际做的时候不用真的搭Hadoop集群但要体现分布式思想。一个省力的方案是引入PySpark做离线数据处理。它在本地以local模式运行不需要集群但代码逻辑是完全分布式的用RDD或DataFrame处理用户行为数据时map、reduceByKey这些操作就是分布式计算的经典范式。你可以在论文里写“基于Spark的离线推荐数据处理”实现就是先用Spark做用户行为聚合再把聚合结果写回MySQL供协同过滤使用。另一个更轻的方案是模拟分布式计算把用户行为数据按用户ID哈希分片用Python的多进程Pool并行计算不同分片的相似度最后汇总。这同样体现了分而治之的思想且不容易出环境问题。我的建议是如果你的答辩评委偏向软件工程就讲多进程分片如果评委偏向大数据方向就加Spark的local模式演示。两者都不要做太深点到为止重点是“你清楚自己为什么用分布式以及用在哪里”。3. 实操过程与核心环节实现3.1 环境搭建与项目初始化环境版本我建议固定下来不然过几个月依赖冲突会非常痛苦。Python3.8到3.11均可我测试过3.10最稳3.12太新部分依赖可能还没跟上Django4.2 LTS版本不要用2.x的老版本也不要刚发布的5.xmysqlclient需要提前装好MySQL并安装对应驱动pandas、numpy、scikit-learn推荐算法核心依赖pyecharts或pycharts可以直接用Python生成Echarts配置也可以自己写JSON接口初始化项目django-admin startproject music_system cd music_system python manage.py startapp users python manage.py startapp music python manage.py startapp behavior python manage.py startapp recommend python manage.py startapp stats python manage.py startapp visual记得在settings.py的INSTALLED_APPS里把新增的App全部注册然后执行迁移python manage.py makemigrations python manage.py migrate注意Django 4.x对MySQL的版本有要求MySQL 5.7以下容易报错建议至少用5.7以上8.0更好。遇到MySQLdb is not installed的问题要么装mysqlclient要么在__init__.py里用pymysql做兼容。3.2 推荐模块的完整代码实现推荐模块是整个系统的核心我给出一个可以直接跑通的数据流程从数据清洗到最终推荐列表其中要特别注意评分矩阵是稀疏的情况。首先从数据库里把用户行为数据读出来构造成用户-歌曲矩阵import pandas as pd from behavior.models import PlayRecord def build_matrix(): records PlayRecord.objects.all().values(user_id, song_id, play_count) df pd.DataFrame(list(records)) # 转成透视表NaN表示没有播放过 matrix df.pivot_table(indexuser_id, columnssong_id, valuesplay_count, fill_value0) return matrix接着计算物品相似度from sklearn.metrics.pairwise import cosine_similarity def compute_item_sim(matrix): # 按列计算歌曲之间的余弦相似度 item_sim cosine_similarity(matrix.T) item_sim_df pd.DataFrame(item_sim, indexmatrix.columns, columnsmatrix.columns) return item_sim_df推荐逻辑def recommend_by_item(user_id, matrix, item_sim_df, top_n10): user_vec matrix.loc[user_id] played set(user_vec[user_vec 0].index) scores {} for song_id in played: sim_songs item_sim_df[song_id].sort_values(ascendingFalse).index[1:11] for sim_song in sim_songs: if sim_song in played: continue scores[sim_song] scores.get(sim_song, 0) item_sim_df.loc[song_id, sim_song] sorted_songs sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] return [song[0] for song in sorted_songs]这里有几个细节容易被忽视相似度计算时matrix.T把用户维度变到列是为了得到“歌曲相似歌曲”的矩阵方向别搞反score累加时权重是相似度所以与自己听过的歌相似度越高、贡献越大过滤掉played里的歌曲是必须的否则推荐列表全是你已经听过的很尴尬如果要做基于用户的协同过滤逻辑刚好反过来先算用户相似度再找目标用户的TopK相似用户汇总这些相似用户喜欢但目标用户没听过的歌曲。3.3 数据可视化Echarts与Django接口联动Echarts集成的方式我推荐前后端分离式的JSON接口而不是在Django模板里直接塞大段JavaScript。这样图表配置和数据源分离后期加新图表也只需要新增一个接口。在stats/views.py里写一个歌曲热度Top10接口from django.http import JsonResponse from music.models import Song def song_hot_top10(request): songs Song.objects.order_by(-play_count)[:10] data { names: [s.title for s in songs], counts: [s.play_count for s in songs] } return JsonResponse({code: 0, data: data})前端页面引入Echarts通过fetch拿数据再渲染div idhotChart styleheight: 400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script fetch(/stats/song_hot_top10/) .then(res res.json()) .then(data { var chart echarts.init(document.getElementById(hotChart)); chart.setOption({ title: { text: 歌曲热度Top10 }, tooltip: {}, xAxis: { data: data.data.names, axisLabel: { rotate: 30 } }, yAxis: {}, series: [{ type: bar, data: data.data.counts }] }); }); /script注意x轴如果歌名太长一定要加axisLabel.rotate旋转角度否则全部叠在一起答辩展示时非常难看。Echarts柱状图的柱子如果想用自定义图片官方支持series.itemStyle里配置image可以搜一下具体配置。如果你想在图表上体现更多维度常见的组合是柱状图展示播放量Top10歌曲和Top10歌手折线图展示近30天的用户活跃曲线饼图展示不同风格的占比中国地图展示用户所在省份的热度分布Echarts中国地图需要额外加载地图数据注意不要用旧版内置地图现在官方推荐使用GeoJSON方式注册3.4 Bootstrap前端与可视化大屏拼装前端页面建议先用Bootstrap搭好布局框架再往里面填充Echarts图表。最简单的结构是navbar container-fluid row card的组合。Bootstrap栅格系统响应式能力对毕设足够用了一个col-lg-4 col-md-6的卡片在大屏幕上三列并排在平板上自动降为两列手机上单列竖排。我们采用的管理端模板一般也是基于Bootstrap的改起来成本低。如果你不喜欢Bootstrap默认样式也可以直接用原生CSS覆盖或者引入AdminLTE、SB Admin这类的后端管理模板这类模板天生带侧边栏、顶部导航、卡片统计组件可视化大屏做出来效果会好很多。在页面结构上我推荐做一个独立的大屏页面包含左侧、中间、右侧三个区域中间区域用大号Echarts柱状图或折线图展示核心指标左侧放风格饼图和热门歌手榜右侧放热门歌曲榜和用户活跃趋势这样整体结构感强展示的时候印象分直接拉满。3.5 分布式计算的务实落地方式在毕设里引入分布式计算的目的是体现“海量数据处理思维”讲的时候落脚点应该是工程思路不必过多纠缠集群部署。我采用的方式是先用SQL把30天内的用户行为明细导出来然后写一个Python脚本用进程池按用户ID分片并行统计每个分片的播放TopN再把结果合并。核心代码逻辑大概是from multiprocessing import Pool def process_user_chunk(chunk): # 统计每个用户播放次数最多的10首歌 result chunk.groupby(user_id).apply(lambda x: x.nlargest(10, play_count)) return result def parallel_stat(df, workers4): splits np.array_split(df, workers) with Pool(workers) as pool: results pool.map(process_user_chunk, splits) return pd.concat(results)在论文中描述为“采用并行分片计算将用户行为数据按用户ID划分到多个worker通过reduce操作合并各worker的统计结果”这就体现了分布式的核心拆分、并行、合并。如果你用的是Spark可以在数据处理脚本里引入pysparkfrom pyspark.sql import SparkSession spark SparkSession.builder.appName(musicStat).master(local[*]).getOrCreate() df spark.read.csv(user_behavior.csv, headerTrue) top_songs df.groupBy(user_id).count().orderBy(count, ascendingFalse)Spark的好处是答辩时可以直接展示代码中的groupBy、reduceByKey、分区操作这些都是分布式计算的标志性语法。坏处是环境搭建略麻烦尤其机器内存不够时容易OOM。本地跑的话master用local[2]就够了不要贪多开太多executor。4. 常见问题与排查技巧实录4.1 Django运行中的高频问题Q1启动后报ImproperlyConfigured: mysqlclient 1.4.3 or newer is required这个问题在Windows上特别常见。解决方法是安装新版mysqlclient或者在项目的__init__.py中设置import pymysql pymysql.install_as_MySQLdb()如果还报错检查MySQL版本和字符集配置。Q2Django的StreamingHttpResponse设置content_type和content_disposition怎么不生效有同学做歌曲下载功能时会用到response StreamingHttpResponse(file_stream, content_typeaudio/mpeg) response[Content-Disposition] attachment; filenamesong.mp3StreamingHttpResponse的content_type参数可以正常传给响应头但content_disposition必须以key的方式写入不能直接当成构造参数。文件名里包含中文时建议用filename*UTF-8quote格式编码否则会乱码。Q3Django模板中include、extend的路径总是错模板路径是从templates/目录开始的不要带上绝对路径。建议在settings.py中明确指定每个App的templates目录或者全部统一放在根目录下。最常见的报错是TemplateDoesNotExist先检查文件名和后缀再检查路径拼写。Q4多App开发时迁移顺序混乱如果behavior引用了music的模型先执行python manage.py makemigrations music再执行behavior最后统一migrate。否则容易出现外键关联的表还不存在就建了新表。4.2 Echarts使用中的典型坑Q1Echarts折线图x轴刻度不完整数据对不上比如有12个月的数据x轴只显示了部分刻度。这通常是因为没有设置xAxis.axisLabel.interval的显示策略。解决方法是显式配置xAxis: { type: category, data: months, axisLabel: { interval: 0, // 强制显示全部刻度 rotate: 45 // 数据多时旋转避免重叠 } }Q2Echarts tooltip内容太长不换行毕设展示时如果用户hover显示的信息过宽会撑破页面。解决方法是自定义tooltip的formatter在内容中插入换行tooltip: { formatter: function(params) { return params.name br/播放量 params.value; } }Q3Echarts饼图label line末端的圆点偏移这通常是labels重叠导致的视觉问题。可以调整label: { alignTo: edge, edgeDistance: 10, lineHeight: 15 }或者把labelLine的length调大一些。如果数据项过多直接关闭labelLine只保留标签会更清爽。Q4Vue项目里pxtorem对Echarts没效果如果你是页面里嵌了Vue再引入Echarts移动端适配时pxtorem插件可能不会自动处理canvas内的尺寸。Echarts内部用的是像素值不是CSS rem。解决方式是通过window.devicePixelRatio手动计算图表尺寸或者用resize事件重新触发自适应。4.3 Bootstrap与前端交互问题Q1Bootstrap下拉菜单点击没反应通常是因为没有引入Bootstrap的JS依赖或者jQuery加载顺序不对。记住固定顺序先jQuery再Bootstrap的bundle然后自定义脚本。如果你不想用jQuery只追求静态下拉菜单效果可以直接用CSS实现加一个hover状态显示子菜单但这种方式移动端不友好建议还是引入Bootstrap JS。Q2Bootstrap 5中data-toggle失效Bootstrap 5把data属性从>
返回列表