ARTICLE DETAIL

资讯详情

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

基于Vue+SpringBoot的音乐推荐播放器:协同过滤算法与开发实战

基于Vue+SpringBoot的音乐推荐播放器:协同过滤算法与开发实战 毕设做了个“基于Vue SpringBoot的协同过滤音乐推荐播放器”前后端分离、推荐算法、播放器全流程都走通了一遍。这篇文章把我从选题、建表、算法实现到前后端联调踩过的坑、做过的取舍原原本本写出来。项目标题挂着“论文”说明大概率是你毕业设计的一部分所以我会重点讲清楚三件事推荐算法怎么落地、播放数据怎么埋点、论文里哪些技术点值得展开写——这三件事想明白了代码和论文都能顺很多。先说结论整个系统最核心的不是播放器也不是前后端框架而是“协同过滤”这四个字。很多人的毕设挂了个“协同过滤”的标题但论文里却写不出推荐逻辑代码里也看不到相似度计算在哪。所以这篇文章我会把用户协同过滤User-Based CF从原理到代码拆开讲这也是你答辩时最容易拿分的地方。1. 项目整体架构与技术选型思路1.1 为什么选SpringBoot Vue这套前后端分离组合这个组合几乎是近几年Java方向毕设的默认答案原因非常现实SpringBoot把后端那一堆繁琐的配置收编得干干净净你只要关注业务逻辑就行Vue天然支持组件化开发播放器、推荐列表、歌单管理这些界面拆成组件之后维护成本低得多。更关键的一点是前后端分离之后推荐算法模块可以单独抽出来做测试论文里也更容易画出清晰的系统架构图。后端我用的是SpringBoot 2.7.18这个版本非常稳依赖兼容性好网上能搜到的资料也最多。前端用Vue 2 Element UI虽然Vue 3已经出来很久了但Vue 2的生态资料对毕设来说更友好特别是播放器相关的第三方组件Vue 2版本踩坑少。如果你学校要求比较新的技术栈也可以用Vue 3 Element Plus核心思路不变。前后端分离带来的第一个问题就是跨域。我的解决方式是在后端写一个全局CORS配置类放行所有来源开发阶段这样最省事。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意addAllowedOriginPattern(*)配合setAllowCredentials(true)是允许携带Cookie的如果后面你要做登录态这个必须保留。1.2 推荐算法选型为什么选协同过滤而不是内容推荐这是论文里必须解释清楚的一个决策点。音乐推荐的常见思路有两类基于内容的推荐Content-Based和协同过滤Collaborative Filtering。基于内容的推荐逻辑很直白——你喜欢周杰伦我就推荐同风格、同歌手的歌曲它不需要别人的数据但问题在于你需要给歌曲打大量标签而且推荐结果永远是“同类”没有惊喜感。协同过滤则完全换了一个思路我不关心歌曲本身长什么样我只关心“用户之间的行为相似性”。A和B的听歌历史高度重合那A最近收藏的歌B大概率也喜欢。这种逻辑的好处是不需要给歌曲做内容分析推荐结果也会“跳出”用户已有的偏好圈层有一种“懂我”的感觉。在协同过滤内部还有两条路线基于用户的User-Based CF和基于物品的Item-Based CF。我做的是基于用户的协同过滤因为音乐用户的偏好变化比较快而且User-Based CF的算法逻辑更好解释画起流程图来很直观写论文的时候容易展开。1.3 数据库设计推荐算法能不能跑起来表结构决定一半推荐算法吃的是“用户行为数据”所以表结构设计必须围绕行为数据展开。我建了六张核心表用户表、歌曲表、歌手表、评分表、收藏表、播放记录表。CREATE TABLE song ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 歌曲名, singer varchar(50) DEFAULT NULL COMMENT 歌手, album varchar(100) DEFAULT NULL COMMENT 专辑, duration int DEFAULT NULL COMMENT 时长秒, url varchar(255) DEFAULT NULL COMMENT 音频文件地址, cover varchar(255) DEFAULT NULL COMMENT 封面图地址, play_count int DEFAULT 0 COMMENT 播放次数, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;评分表是整个推荐系统的核心我采用了隐式评分和显式评分结合的方式。显式评分是用户主动打星隐式评分是根据播放次数折算的——播放一次计1分播放超过三次计2分手动收藏直接计5分。折算逻辑写在service层这样推荐模块拿到的评分矩阵就有足够的密度不至于太稀疏。2. 协同过滤推荐模块的核心实现2.1 基于用户的协同过滤原理拆解User-Based CF的完整流程可以分成四步构建用户-歌曲评分矩阵、计算用户间相似度、找到目标用户的K个近邻、根据近邻的评分预测目标歌曲得分。这个流程在论文里一定要画成流程图在代码里一定要能对应上。评分矩阵我直接用Map模拟Key是用户IDValue是另一个Map存的是歌曲ID到评分的映射。这样做的原因是毕设项目的数据量一般不会太大用内存计算完全够比引入Spark划算得多。如果你的数据集超过十万条再考虑用数据库临时表来算。相似度计算我用的是皮尔逊相关系数公式如下sim(u, v) Σ((r(u,i) - r̄(u)) * (r(v,i) - r̄(v))) / sqrt(Σ(r(u,i) - r̄(u))² * Σ(r(v,i) - r̄(v))²)皮尔逊相关系数相比余弦相似度最大的优势是它考虑了用户评分的“尺度差异”。有的用户手松听过的歌都给高分有的用户手紧大部分歌只给3分。皮尔逊相关系数会先减去用户的平均分再做计算这样手松手紧的影响就被消除掉了。2.2 相似度计算与评分预测的代码落地核心的推荐服务类我命名为RecommendService所有推荐逻辑都收敛在这个类里。先看相似度计算这步public double pearsonSimilarity(MapLong, Double userRatings1, MapLong, Double userRatings2) { // 找出两个用户共同评分过的歌曲 SetLong commonItems new HashSet(userRatings1.keySet()); commonItems.retainAll(userRatings2.keySet()); if (commonItems.size() 2) { return 0.0; // 共同评分项太少相似度无意义 } double sum1 0, sum2 0; for (Long itemId : commonItems) { sum1 userRatings1.get(itemId); sum2 userRatings2.get(itemId); } double avg1 sum1 / commonItems.size(); double avg2 sum2 / commonItems.size(); double numerator 0, denom1 0, denom2 0; for (Long itemId : commonItems) { double r1 userRatings1.get(itemId) - avg1; double r2 userRatings2.get(itemId) - avg2; numerator r1 * r2; denom1 r1 * r1; denom2 r2 * r2; } if (denom1 0 || denom2 0) { return 0.0; } return numerator / (Math.sqrt(denom1) * Math.sqrt(denom2)); }评分预测我用的是加权平均权重就是用户相似度目标用户对歌曲的预测评分等于近邻用户对该歌曲的评分乘以相似度的加权和public double predictRating(Long userId, Long songId, MapLong, MapLong, Double ratingMatrix, int k) { MapLong, Double targetRatings ratingMatrix.get(userId); double numerator 0; double denominator 0; for (Map.EntryLong, MapLong, Double entry : ratingMatrix.entrySet()) { Long otherUserId entry.getKey(); if (otherUserId.equals(userId)) continue; Double otherRating entry.getValue().get(songId); if (otherRating null) continue; double sim pearsonSimilarity(targetRatings, entry.getValue()); if (sim 0) continue; // 负相关用户直接剔除 numerator sim * otherRating; denominator Math.abs(sim); } return denominator 0 ? 0 : numerator / denominator; }这个小节的两个代码片段就是论文核心算法的直接映射。写论文时不要贴太长代码画伪代码流程图更好但答辩演示时一定要能定位到这两段。2.3 冷启动问题的三种缓解策略协同过滤最怕冷启动新用户没有任何行为记录算法根本算不出推荐。我做了三层防护。第一层是热门推荐兜底——新用户进入首页时先展示播放量最高的Top10歌曲保证页面不空。第二层是登录引导——用户注册后可以选喜欢的歌手和风格这个偏好数据会被写成一条初始评分记录相当于给用户一个“伪历史”。第三层是混合推荐——如果用户只有很少的历史行为我用基于内容的推荐做补充按歌曲风格字段匹配相似歌曲协同过滤负责在用户行为丰富之后逐步接管推荐结果。这三层策略写进论文“推荐算法优化”章节是很好的加分点说明你不是原封不动照搬算法而是考虑了落地场景。3. 播放器核心功能与前后端联调实现3.1 播放器的功能设计与音频处理方案播放器是整个项目中视觉表现力最强、也最容易出效果的部分。我用Vue封装了一个全局播放器组件PlayerBar.vue固定在页面底部切换歌曲时组件不会销毁播放状态可以跨页面保持。这个组件里包含封面旋转动画、播放/暂停按钮、上一首/下一首按钮、进度条拖拽和音量控制五个核心模块。音频文件我统一放在后端的静态资源目录下通过接口返回音频访问路径。这里有一个容易被忽视的问题是音频格式兼容性我在测试中发现浏览器对MP3格式支持最稳定所以原始音频文件全部转码成MP3格式再上传。如果你用WAV格式Chrome播放没问题但部分移动端浏览器容易卡顿。播放器核心代码如下play(song) { this.currentSong song; this.audioSrc song.url; this.isPlaying true; this.resetProgress(); } togglePlay() { if (!this.currentSong) return; if (this.audio.paused) { this.audio.play(); this.isPlaying true; } else { this.audio.pause(); this.isPlaying false; } }3.2 前端进度条拖拽与音频同步的一个小坑进度条拖拽这个功能看着简单但实现的时候有个细节非常关键——不能直接用input的change事件来更新播放进度因为change事件要等拖拽完才触发会导致进度条一顿一顿的。应该用input事件实时更新UI显示同时在change事件里才真正跳转播放位置。下面是正确写法handleProgressInput(e) { const percent e.target.value / 100; this.currentTimePercent percent * this.duration; // 注意这里只更新显示不修改audio.currentTime } handleProgressChange(e) { const percent e.target.value / 100; this.audio.currentTime percent * this.duration; this.currentTimePercent this.audio.currentTime; }这样拖拽过程中画面跟手松手后音频才真正跳转体验会顺滑很多。这个小细节我记得当时的指导老师专门问了是一个很好的答辩讨论点。3.3 行为数据埋点与接口设计推荐系统能不能转起来关键看行为数据有没有持续回流。前端每次切歌、播放完成、收藏、评分都会向后端发送行为日志。播放完成的判断是监听audio的ended事件这个事件触发代表一首歌完整播完比手动记录时间戳可靠得多。this.audio.addEventListener(ended, () { this.recordPlayComplete(this.currentSong.id); this.next(); });后端接口我按资源维度统一命名前端调用很直观功能接口地址请求方式说明获取推荐列表/api/recommend/{userId}GET返回协同过滤计算后的歌曲列表歌曲播放上报/api/behavior/playPOST上报播放行为更新播放次数播放完成上报/api/behavior/completePOST上报完整播放用于隐式评分用户评分/api/rating/submitPOST用户主动给歌曲打分获取热门歌曲/api/song/hotGET冷启动兜底推荐4. 论文写作与答辩准备的几个关键坑4.1 论文中算法章节怎么组织结构图我的建议是不要贪多只深入讲懂一个算法。我当时论文里花大篇幅讲解了协同过滤的原理、公式推导、算法流程、伪代码和效果评估内容扎实但完全在可控范围内。有的同学想把Item-Based CF和User-Based CF都写进去甚至还想写矩阵分解结果每个都写不透答辩时一问细节就露馅。论文里推荐放三张图系统架构图、推荐算法流程图、数据库ER图。系统架构图要体现前后端分离的分层关系算法流程图要完整呈现“构建评分矩阵→计算相似度→找近邻→预测评分→生成推荐列表”这条主线数据库ER图画清楚六张表的关系。4.2 推荐效果不好先别怪算法先查这五件事我调试推荐效果时总结了一套排查顺序你要是觉得“推荐结果根本不相关”按这个顺序查一定能找到原因。第一看评分数据是否稀疏每个用户如果少于五条评分记录协同过滤基本等于瞎猜先去扩充行为数据。第二看相似度计算是不是把超过评分范围的歌曲也算进去了我一开始没过滤用户没听过的歌导致相似度被稀释。第三看冷启动兜底有没有生效新用户是否走的是热门推荐。第四看歌曲数据里是不是混入了脏数据比如音频路径为空、歌手字段为空的记录。第五看前端埋点有没有真正上报用浏览器控制台的Network面板抽查一下行为日志接口的请求频率。4.3 答辩时最容易被追问的三个问题第一个问题必然是“协同过滤和基于内容的推荐有什么区别”。你得能一句话说清楚基于内容看的是东西本身像不像协同过滤看的是人像不像。第二个问题是“你的推荐效果怎么衡量”。我当时准备了两个指标式的回答——离线阶段用准确率和召回率评估在线阶段看推荐列表的点击率和完整播放率。哪怕你只是抽样人工评估也要说成“评估维度包括推荐列表的点击率和播放完成率”。第三个问题是“数据量大了怎么办”。标准答案是内存计算换成分布式计算或者引入Spark MLlib评分矩阵持久化到数据库加上定时离线计算。你不用真的做分布式但要把扩展思路说出来。4.4 前端部署的几个小提示如果学校要求把项目部署到服务器演示前端打包后可以放到后端项目的static目录下统一部署这样只启动一个SpringBoot端口就行不用额外配置Nginx。# 前端打包 npm run build # 将dist目录下的文件复制到后端 static 目录 cp -r dist/* ../backend/src/main/resources/static/后端在application.yml里需要关闭静态资源拦截限制不然打包后的前端路由刷新会404要把路由模式改成hash模式const router new VueRouter({ mode: hash, routes });5. 写在项目复盘之后整个项目从零到一我最深的体会有两点。第一推荐算法不是玄学它高度依赖数据质量——哪怕你没有几千条真实用户数据也要在测试阶段把用户行为数据造得足够真实推荐效果才有说服力。第二前后端分离项目里最难啃的不是代码而是接口约定和状态同步前端播放器在页面切换后要保持播放状态、后端行为数据要准确回传这两个点理顺了整个项目就顺畅了。最后再分享一个小技巧如果你时间紧张可以把“歌曲收藏”和“播放完成”这两个行为作为评分主来源让用户在听歌过程中不知不觉就把推荐数据喂饱了。我在做毕设时发现很多同学在测试推荐效果时手动刷了几百条数据很耗时后来我把播放完成事件绑定到评论弹窗上——完整听完一首歌就会自动弹出一个轻量的“喜欢这首歌吗”提示条用户点击一下就是一次显式评分。这个交互既采集了数据又不打断听歌体验实测下来数据回流效率特别高。你可以参考这个思路在播放器的交互细节上做文章这部分写进论文的创新点里也是很加分的。
返回列表