Python大数据实战(十六):音乐推荐系统------基于协同过滤算法构建个性化歌单引擎
文章目录
- Python大数据实战(十六):音乐推荐系统------基于协同过滤算法构建个性化歌单引擎
- 前言
- 一、推荐系统概述
- 二、协同过滤算法详解
-
- [2.1 什么是协同过滤](#2.1 什么是协同过滤)
- [2.2 协同过滤的三种实现](#2.2 协同过滤的三种实现)
-
- [2.2.1 基于用户的协同过滤(User-Based CF)](#2.2.1 基于用户的协同过滤(User-Based CF))
- [2.2.2 基于物品的协同过滤(Item-Based CF)](#2.2.2 基于物品的协同过滤(Item-Based CF))
- [2.2.3 基于模型的协同过滤(Model-Based CF)](#2.2.3 基于模型的协同过滤(Model-Based CF))
- 三种方法对比
- [三、Surprise 推荐系统库](#三、Surprise 推荐系统库)
-
- [3.1 Surprise 简介](#3.1 Surprise 简介)
-
- 安装方式
- [Surprise 官方资源](#Surprise 官方资源)
- [3.2 Surprise 核心模块](#3.2 Surprise 核心模块)
- 四、音乐推荐系统实战
-
- [4.1 项目概述](#4.1 项目概述)
- [4.2 数据加载与预处理](#4.2 数据加载与预处理)
-
- [4.2.1 加载 Pickle 数据](#4.2.1 加载 Pickle 数据)
- [4.2.2 读取 Surprise 格式数据](#4.2.2 读取 Surprise 格式数据)
- [4.3 构建相似歌单推荐模型](#4.3 构建相似歌单推荐模型)
-
- [4.3.1 基于 KNN 的协同过滤](#4.3.1 基于 KNN 的协同过滤)
- [4.3.2 查找最相似歌单](#4.3.2 查找最相似歌单)
- [4.3.3 基于歌单相似度的歌曲推荐](#4.3.3 基于歌单相似度的歌曲推荐)
- [4.4 模型评估与优化](#4.4 模型评估与优化)
-
- [4.4.1 使用交叉验证评估](#4.4.1 使用交叉验证评估)
- [4.4.2 网格搜索调参](#4.4.2 网格搜索调参)
- 五、常见问题与解决方案
-
- [5.1 冷启动问题](#5.1 冷启动问题)
- [5.2 数据稀疏性问题](#5.2 数据稀疏性问题)
- 六、总结与展望
-
- [6.1 本文总结](#6.1 本文总结)
- [6.2 下一步优化方向](#6.2 下一步优化方向)
- [6.3 系列文章回顾](#6.3 系列文章回顾)
- 参考链接
前言
"这首歌怎么这么好听?为什么现在才推给我?"
这是每个音乐爱好者都曾有过的体验。当你打开网易云音乐的"每日推荐",30 首歌中总有几首让你惊艳------仿佛算法比你还懂你的耳朵。这背后,就是推荐系统在悄悄工作。
推荐系统早已渗透到我们生活的方方面面:淘宝的"猜你喜欢"、抖音的"刷个不停"、大众点评的"附近美食"......它们的本质,都是在海量数据中精准匹配用户与物品。
本文以网易云音乐真实歌单数据为基础,从推荐系统原理、协同过滤算法、Surprise 库实战到模型评估,完整呈现一个音乐推荐引擎的构建过程。全文超过 3000 字,适合正在学习推荐系统和 Python 数据挖掘的读者。
🎯 本文要点:理解协同过滤的三种实现方式,掌握 Surprise 库的基本用法,学会构建歌单相似度推荐模型。
一、推荐系统概述
1.1 什么是推荐系统
推荐系统(Recommendation System)是利用网站向用户提供商品信息和建议,帮助用户决定应该购买什么产品,模拟销售人员帮助客户完成购买过程的智能系统。
个性化推荐的核心是:根据用户的兴趣特点和历史行为,向用户推荐其可能感兴趣的信息和商品。
推荐系统的核心角色
| 角色 | 说明 | 音乐场景示例 |
|---|---|---|
| 用户(User) | 接受推荐的主体 | 网易云音乐用户 |
| 物品(Item) | 被推荐的客体 | 歌曲、歌单、专辑 |
| 行为(Behavior) | 用户与物品的交互 | 播放、收藏、评论、分享 |
| 评分(Rating) | 用户对物品的偏好程度 | 1-5 星评分、喜欢/不喜欢 |
1.2 推荐系统建模过程
一个完整的推荐系统建模流程通常包含以下阶段:
#mermaid-svg-RmNoDfl7A4V3vVkr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RmNoDfl7A4V3vVkr .error-icon{fill:#552222;}#mermaid-svg-RmNoDfl7A4V3vVkr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RmNoDfl7A4V3vVkr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RmNoDfl7A4V3vVkr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RmNoDfl7A4V3vVkr .marker.cross{stroke:#333333;}#mermaid-svg-RmNoDfl7A4V3vVkr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RmNoDfl7A4V3vVkr p{margin:0;}#mermaid-svg-RmNoDfl7A4V3vVkr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr .cluster-label text{fill:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr .cluster-label span{color:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr .cluster-label span p{background-color:transparent;}#mermaid-svg-RmNoDfl7A4V3vVkr .label text,#mermaid-svg-RmNoDfl7A4V3vVkr span{fill:#333;color:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr .node rect,#mermaid-svg-RmNoDfl7A4V3vVkr .node circle,#mermaid-svg-RmNoDfl7A4V3vVkr .node ellipse,#mermaid-svg-RmNoDfl7A4V3vVkr .node polygon,#mermaid-svg-RmNoDfl7A4V3vVkr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RmNoDfl7A4V3vVkr .rough-node .label text,#mermaid-svg-RmNoDfl7A4V3vVkr .node .label text,#mermaid-svg-RmNoDfl7A4V3vVkr .image-shape .label,#mermaid-svg-RmNoDfl7A4V3vVkr .icon-shape .label{text-anchor:middle;}#mermaid-svg-RmNoDfl7A4V3vVkr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RmNoDfl7A4V3vVkr .rough-node .label,#mermaid-svg-RmNoDfl7A4V3vVkr .node .label,#mermaid-svg-RmNoDfl7A4V3vVkr .image-shape .label,#mermaid-svg-RmNoDfl7A4V3vVkr .icon-shape .label{text-align:center;}#mermaid-svg-RmNoDfl7A4V3vVkr .node.clickable{cursor:pointer;}#mermaid-svg-RmNoDfl7A4V3vVkr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RmNoDfl7A4V3vVkr .arrowheadPath{fill:#333333;}#mermaid-svg-RmNoDfl7A4V3vVkr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RmNoDfl7A4V3vVkr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RmNoDfl7A4V3vVkr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RmNoDfl7A4V3vVkr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RmNoDfl7A4V3vVkr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RmNoDfl7A4V3vVkr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RmNoDfl7A4V3vVkr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RmNoDfl7A4V3vVkr .cluster text{fill:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr .cluster span{color:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RmNoDfl7A4V3vVkr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RmNoDfl7A4V3vVkr rect.text{fill:none;stroke-width:0;}#mermaid-svg-RmNoDfl7A4V3vVkr .icon-shape,#mermaid-svg-RmNoDfl7A4V3vVkr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RmNoDfl7A4V3vVkr .icon-shape p,#mermaid-svg-RmNoDfl7A4V3vVkr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RmNoDfl7A4V3vVkr .icon-shape rect,#mermaid-svg-RmNoDfl7A4V3vVkr .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RmNoDfl7A4V3vVkr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RmNoDfl7A4V3vVkr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RmNoDfl7A4V3vVkr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 📊 数据采集
用户行为日志
🔧 数据预处理
清洗/去重/格式化
🧬 特征工程
用户-物品矩阵构建
🤖 算法建模
协同过滤/矩阵分解
📈 模型评估
RMSE/MAE/召回率
🚀 线上部署
实时推荐服务
各阶段关键任务
| 阶段 | 关键任务 | 产出物 |
|---|---|---|
| 数据采集 | 收集用户行为日志、物品元数据 | 原始数据集 |
| 数据预处理 | 去重、缺失值处理、格式统一 | 清洗后数据集 |
| 特征工程 | 构建 User-Item 评分矩阵 | 特征矩阵 |
| 算法建模 | 训练协同过滤/矩阵分解模型 | 推荐模型 |
| 模型评估 | 离线评估指标计算 | 评估报告 |
| 线上部署 | 模型上线、AB 测试 | 推荐服务 |
二、协同过滤算法详解
2.1 什么是协同过滤
协同过滤(Collaborative Filtering)是推荐系统中最经典、应用最广泛的算法之一。
💡 核心思想:利用兴趣相投、拥有共同经验之群体的喜好来推荐用户感兴趣的信息。个人通过合作机制给予信息回应(如评分)并记录下来,以达到过滤的目的,进而帮助别人筛选信息。
协同过滤的三大特征
| 特征 | 说明 |
|---|---|
| 忽略物品本身属性 | 不依赖歌曲的流派、节奏、歌词等特征 |
| 关注用户行为 | 基于播放记录、收藏、评分等行为数据 |
| 间接推荐 | 通过行为找间接关系,而非直接匹配标签 |
协同过滤推荐的一般步骤
#mermaid-svg-bmczIxSIiVhie0vF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bmczIxSIiVhie0vF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bmczIxSIiVhie0vF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bmczIxSIiVhie0vF .error-icon{fill:#552222;}#mermaid-svg-bmczIxSIiVhie0vF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bmczIxSIiVhie0vF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bmczIxSIiVhie0vF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bmczIxSIiVhie0vF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bmczIxSIiVhie0vF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bmczIxSIiVhie0vF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bmczIxSIiVhie0vF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bmczIxSIiVhie0vF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bmczIxSIiVhie0vF .marker.cross{stroke:#333333;}#mermaid-svg-bmczIxSIiVhie0vF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bmczIxSIiVhie0vF p{margin:0;}#mermaid-svg-bmczIxSIiVhie0vF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bmczIxSIiVhie0vF .cluster-label text{fill:#333;}#mermaid-svg-bmczIxSIiVhie0vF .cluster-label span{color:#333;}#mermaid-svg-bmczIxSIiVhie0vF .cluster-label span p{background-color:transparent;}#mermaid-svg-bmczIxSIiVhie0vF .label text,#mermaid-svg-bmczIxSIiVhie0vF span{fill:#333;color:#333;}#mermaid-svg-bmczIxSIiVhie0vF .node rect,#mermaid-svg-bmczIxSIiVhie0vF .node circle,#mermaid-svg-bmczIxSIiVhie0vF .node ellipse,#mermaid-svg-bmczIxSIiVhie0vF .node polygon,#mermaid-svg-bmczIxSIiVhie0vF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bmczIxSIiVhie0vF .rough-node .label text,#mermaid-svg-bmczIxSIiVhie0vF .node .label text,#mermaid-svg-bmczIxSIiVhie0vF .image-shape .label,#mermaid-svg-bmczIxSIiVhie0vF .icon-shape .label{text-anchor:middle;}#mermaid-svg-bmczIxSIiVhie0vF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bmczIxSIiVhie0vF .rough-node .label,#mermaid-svg-bmczIxSIiVhie0vF .node .label,#mermaid-svg-bmczIxSIiVhie0vF .image-shape .label,#mermaid-svg-bmczIxSIiVhie0vF .icon-shape .label{text-align:center;}#mermaid-svg-bmczIxSIiVhie0vF .node.clickable{cursor:pointer;}#mermaid-svg-bmczIxSIiVhie0vF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bmczIxSIiVhie0vF .arrowheadPath{fill:#333333;}#mermaid-svg-bmczIxSIiVhie0vF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bmczIxSIiVhie0vF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bmczIxSIiVhie0vF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bmczIxSIiVhie0vF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bmczIxSIiVhie0vF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bmczIxSIiVhie0vF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bmczIxSIiVhie0vF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bmczIxSIiVhie0vF .cluster text{fill:#333;}#mermaid-svg-bmczIxSIiVhie0vF .cluster span{color:#333;}#mermaid-svg-bmczIxSIiVhie0vF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bmczIxSIiVhie0vF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bmczIxSIiVhie0vF rect.text{fill:none;stroke-width:0;}#mermaid-svg-bmczIxSIiVhie0vF .icon-shape,#mermaid-svg-bmczIxSIiVhie0vF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bmczIxSIiVhie0vF .icon-shape p,#mermaid-svg-bmczIxSIiVhie0vF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bmczIxSIiVhie0vF .icon-shape rect,#mermaid-svg-bmczIxSIiVhie0vF .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bmczIxSIiVhie0vF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bmczIxSIiVhie0vF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bmczIxSIiVhie0vF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收集用户行为
找到相似用户/物品
计算并推荐
偏好数据
不分析兴趣标签
通过行为计算相似
非属性相似
基于场景
进行协同推荐
- 收集用户行为:汇总能体现用户偏好的行为数据,但不分析具体的兴趣标签
- 找到相似用户/物品:通过协同行为计算相似度(非属性相似)
- 计算并推荐:基于场景进行协同推荐
2.2 协同过滤的三种实现
2.2.1 基于用户的协同过滤(User-Based CF)
核心思想:给某用户推荐物品 = 找到他的相似用户 → 相似用户喜欢的物品 → 推荐给该用户。
"用户A和用户B都喜欢周杰伦和林俊杰,
用户B还喜欢陈奕迅,
那么用户A可能也喜欢陈奕迅。"
实现步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 汇总用户行为 | 收集每个用户的所有行为记录 |
| 2 | 向量化用户行为 | 将行为转化为向量,计算用户间相似度 |
| 3 | 寻找相似用户集合 | 找到与目标用户兴趣相似的用户群体 |
| 4 | 去重推荐 | 筛选相似用户喜欢但目标用户未接触过的物品 |
2.2.2 基于物品的协同过滤(Item-Based CF)
核心思想:给某用户推荐物品 = 计算物品间相似度 → 找到用户喜欢物品的相似物品 → 推荐。
"用户喜欢《晴天》,
喜欢《晴天》的用户也都喜欢《七里香》,
那么推荐《七里香》给该用户。"
实现步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 汇总用户行为 | 收集所有用户行为,以物品为维度构建向量 |
| 2 | 计算物品相似度 | 通过用户行为(非属性)计算物品关联性 |
| 3 | 生成候选集 | 在物品候选集中筛选推荐目标 |
| 4 | 排序推荐 | 按相似度排序,推荐 Top-N |
2.2.3 基于模型的协同过滤(Model-Based CF)
核心思想:使用机器学习算法对用户-物品评分矩阵进行建模,预测用户对新物品的评分。
| 方法 | 描述 | 典型算法 |
|---|---|---|
| 矩阵分解 | 将评分矩阵分解为用户隐向量和物品隐向量 | SVD、SVD++、NMF |
| 聚类 | 对用户或物品进行聚类后推荐 | K-Means |
| 关联规则 | 挖掘物品间的共现关系 | Apriori、FP-Growth |
| 深度学习 | 使用神经网络学习用户和物品的表示 | Neural CF、DeepFM |
三种方法对比
| 维度 | User-Based | Item-Based | Model-Based |
|---|---|---|---|
| 原理 | 找相似用户 | 找相似物品 | 机器学习建模 |
| 冷启动 | 新用户冷启动严重 | 新物品冷启动严重 | 可缓解 |
| 计算复杂度 | O(N²),用户多时慢 | O(M²),物品多时慢 | 取决于模型 |
| 可解释性 | 高("和你有相似口味的人也喜欢") | 高("和你喜欢的歌相似的歌") | 低(黑盒) |
| 适用场景 | 用户数 << 物品数 | 物品数 << 用户数 | 大规模数据 |
三、Surprise 推荐系统库
3.1 Surprise 简介
Surprise(Simple Python RecommendatIon System Engine)是 scikit-learn 系列中的一个推荐系统专用库,提供了丰富的协同过滤算法实现和评估工具。
安装方式
bash
pip install surprise
⚠️ 常见错误 1 :安装时报
Microsoft Visual C++ 14.0 is required
错误信息:
error: Microsoft Visual C++ 14.0 is required.
Get it with "Microsoft Visual C++ Build Tools"
解决方案:
- Windows 用户:下载并安装 Microsoft C++ Build Tools
- 安装时勾选"MSVC v140 - VS 2015 C++ build tools"
- 或者直接使用 Anaconda 环境:
conda install -c conda-forge scikit-surprise
⚠️ 常见错误 2 :导入时报
ModuleNotFoundError: No module named 'surprise'
错误信息:
ModuleNotFoundError: No module named 'surprise'
解决方案 :注意包名是 scikit-surprise,不是 surprise
bash
# 错误方式
pip install surprise
# 正确方式
pip install scikit-surprise
Surprise 官方资源
3.2 Surprise 核心模块
| 模块 | 功能 | 常用类/函数 |
|---|---|---|
| Dataset | 数据加载与处理 | Dataset.load_from_file()、Dataset.load_builtin() |
| Reader | 定义数据格式 | Reader(line_format, sep, rating_scale) |
| Trainset | 训练集构建 | data.build_full_trainset() |
| prediction_algorithms | 推荐算法 | KNNBasic、SVD、NMF、BaselineOnly |
| accuracy | 评估指标 | accuracy.rmse()、accuracy.mae() |
| model_selection | 交叉验证与网格搜索 | cross_validate()、GridSearchCV |
四、音乐推荐系统实战
4.1 项目概述
本项目基于网易云音乐的歌单与歌曲数据,构建音乐推荐系统。核心思路是将"歌单"类比为推荐系统中的"用户"------每个歌单包含若干首歌曲,通过计算歌单之间的相似度,为用户推荐与其已有歌单相似的新歌单。
数据集说明
| 数据文件 | 内容 | 格式 |
|---|---|---|
popular_playlist.pkl |
歌单数据 | Pickle 二进制 |
popular_music_suprise_format.txt |
歌单与歌曲对应关系 | 文本(Surprise 格式) |
popular_song.pkl |
流行歌曲数据 | Pickle 二进制 |
技术栈
| 工具 | 用途 |
|---|---|
| Pandas | 数据加载、清洗、统计分析 |
| NumPy | 数值计算、矩阵操作 |
| Pickle | 二进制数据反序列化 |
| Surprise | 推荐系统建模与评估 |
| Matplotlib | 可视化展示 |
4.2 数据加载与预处理
4.2.1 加载 Pickle 数据
python
import pickle
import pandas as pd
import numpy as np
# 加载歌单数据(二进制格式)
playlist_data = pickle.load(open('popular_playlist.pkl', 'rb'))
print(f"歌单数量: {len(playlist_data)}")
# 加载歌曲数据
song_data = pickle.load(open('popular_song.pkl', 'rb'))
print(f"歌曲数量: {len(song_data)}")
# 查看歌单数据结构
print("歌单数据字段:", playlist_data.columns.tolist())
print(playlist_data.head())
4.2.2 读取 Surprise 格式数据
Surprise 要求数据格式为 user item rating [timestamp],其中:
- user:用户 ID(本项目对应歌单 ID)
- item:物品 ID(本项目对应歌曲 ID)
- rating:评分(本项目对应歌曲在歌单中的权重)
python
from surprise import Dataset, Reader
# 定义数据读取格式
# 格式:歌单ID 歌曲ID 评分
reader = Reader(
line_format='user item rating', # 定义三列格式
sep='\t', # 分隔符为 Tab
rating_scale=(0, 100) # 评分范围 0-100
)
# 从文件加载数据
data = Dataset.load_from_file(
file_path='popular_music_suprise_format.txt',
reader=reader
)
# 构建训练集(使用全部数据)
trainset = data.build_full_trainset()
print(f"训练集用户数(歌单数): {trainset.n_users}")
print(f"训练集物品数(歌曲数): {trainset.n_items}")
print(f"训练集评分数: {trainset.n_ratings}")
4.3 构建相似歌单推荐模型
4.3.1 基于 KNN 的协同过滤
python
from surprise import KNNBasic
from surprise import accuracy
from surprise.model_selection import train_test_split
# 划分训练集和测试集(80% 训练,20% 测试)
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)
# 构建基于用户的 KNN 协同过滤模型
# sim_options 配置相似度计算方式
sim_options = {
'name': 'pearson', # 皮尔逊相关系数
'user_based': True, # True = User-Based, False = Item-Based
'min_support': 3 # 最少共同评分数量
}
model = KNNBasic(sim_options=sim_options)
# 训练模型
model.fit(trainset)
print("模型训练完成!")
# 在测试集上预测
predictions = model.test(testset)
# 评估模型
rmse = accuracy.rmse(predictions)
mae = accuracy.mae(predictions)
print(f"RMSE: {rmse:.4f}")
print(f"MAE: {mae:.4f}")
4.3.2 查找最相似歌单
python
def find_similar_playlists(model, trainset, playlist_id, k=10):
"""
查找与指定歌单最相似的 k 个歌单
参数:
model: 训练好的 KNN 模型
trainset: 训练集
playlist_id: 目标歌单的原始 ID
k: 返回最相似的 k 个歌单
返回:
list: [(相似歌单ID, 相似度), ...]
"""
# 将原始 ID 转换为内部 ID
try:
inner_id = trainset.to_inner_uid(playlist_id)
except ValueError:
print(f"歌单 {playlist_id} 不在训练集中")
return []
# 获取该歌单的 k 个最近邻居
neighbors = model.get_neighbors(inner_id, k=k)
# 转换回原始 ID 并获取相似度
similar_playlists = []
for neighbor_inner_id in neighbors:
neighbor_raw_id = trainset.to_raw_uid(neighbor_inner_id)
similarity = model.sim[inner_id][neighbor_inner_id]
similar_playlists.append((neighbor_raw_id, similarity))
# 按相似度降序排列
similar_playlists.sort(key=lambda x: x[1], reverse=True)
return similar_playlists
# 查找歌单 ID 为 0 的相似歌单
similar = find_similar_playlists(model, trainset, playlist_id=0, k=10)
for pid, sim in similar:
print(f"歌单 {pid} → 相似度: {sim:.4f}")
4.3.3 基于歌单相似度的歌曲推荐
python
def recommend_songs(model, trainset, playlist_id, n=20):
"""
为指定歌单推荐新歌曲
参数:
model: 训练好的 KNN 模型
trainset: 训练集
playlist_id: 目标歌单 ID
n: 推荐歌曲数量
返回:
list: [(歌曲ID, 预测评分), ...]
"""
# 获取目标歌单已有的歌曲集合
try:
inner_uid = trainset.to_inner_uid(playlist_id)
except ValueError:
return []
existing_songs = set()
for (inner_iid, rating) in trainset.ur[inner_uid]:
existing_songs.add(inner_iid)
# 获取所有歌曲中目标歌单没有的歌曲
all_songs = set(range(trainset.n_items))
candidate_songs = all_songs - existing_songs
# 预测评分
predictions = []
for song_inner_id in candidate_songs:
song_raw_id = trainset.to_raw_iid(song_inner_id)
pred = model.predict(playlist_id, song_raw_id)
predictions.append((song_raw_id, pred.est))
# 按预测评分降序排列,取前 n 个
predictions.sort(key=lambda x: x[1], reverse=True)
return predictions[:n]
# 为歌单 0 推荐 20 首歌曲
recommended = recommend_songs(model, trainset, playlist_id=0, n=20)
print("推荐歌曲 Top 20:")
for song_id, score in recommended:
print(f" 歌曲 {song_id} → 预测评分: {score:.2f}")
4.4 模型评估与优化
4.4.1 使用交叉验证评估
python
from surprise.model_selection import cross_validate
# 定义多种算法进行比较
algorithms = {
'KNN_User': KNNBasic(sim_options={'name': 'pearson', 'user_based': True}),
'KNN_Item': KNNBasic(sim_options={'name': 'cosine', 'user_based': False}),
'SVD': SVD(),
'BaselineOnly': BaselineOnly()
}
# 对每种算法进行 5 折交叉验证
results = {}
for name, algo in algorithms.items():
print(f"\n正在评估: {name}")
cv_results = cross_validate(
algo, data,
measures=['RMSE', 'MAE'],
cv=5,
verbose=True
)
results[name] = {
'RMSE': np.mean(cv_results['test_rmse']),
'MAE': np.mean(cv_results['test_mae'])
}
# 打印对比结果
print("\n" + "="*60)
print(f"{'算法':<20} {'RMSE':>10} {'MAE':>10}")
print("="*60)
for name, metrics in results.items():
print(f"{name:<20} {metrics['RMSE']:>10.4f} {metrics['MAE']:>10.4f}")
print("="*60)
4.4.2 网格搜索调参
python
from surprise.model_selection import GridSearchCV
# 定义参数网格
param_grid = {
'n_epochs': [20, 30, 40], # 迭代次数
'lr_all': [0.002, 0.005, 0.01], # 学习率
'reg_all': [0.02, 0.05, 0.1] # 正则化系数
}
# 网格搜索
gs = GridSearchCV(SVD, param_grid, measures=['rmse'], cv=3)
gs.fit(data)
# 输出最优参数
print(f"最优 RMSE: {gs.best_score['rmse']:.4f}")
print(f"最优参数: {gs.best_params['rmse']}")
五、常见问题与解决方案
5.1 冷启动问题
现象:新用户没有任何行为数据,新歌曲也没有被任何用户评分,导致无法进行推荐。
解决方案:
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 热门推荐 | 推荐全局最热门的歌曲 | 新用户 |
| 内容推荐 | 基于歌曲的属性(流派、歌手)推荐 | 新歌曲 |
| 混合推荐 | 结合协同过滤和内容推荐 | 通用 |
| 用户画像 | 引导用户填写偏好标签 | 注册阶段 |
5.2 数据稀疏性问题
现象:用户-物品评分矩阵中大部分元素为空,导致相似度计算不准确。
python
# 计算评分矩阵的稀疏度
n_users = trainset.n_users
n_items = trainset.n_items
n_ratings = trainset.n_ratings
sparsity = 1 - n_ratings / (n_users * n_items)
print(f"评分矩阵稀疏度: {sparsity:.2%}")
解决方案:
- 使用矩阵分解(如 SVD)填充缺失值
- 设置
min_support参数,过滤评分过少的用户或物品 - 引入隐式反馈(播放次数、停留时长等)
六、总结与展望
6.1 本文总结
本文从零开始构建了一个基于协同过滤的音乐推荐系统,主要涵盖:
| 知识点 | 核心内容 |
|---|---|
| 推荐系统原理 | 用户-物品-行为三元关系、建模流程 |
| 协同过滤算法 | User-Based、Item-Based、Model-Based 三种实现 |
| Surprise 库 | 数据加载、模型训练、评估指标 |
| 实战项目 | 网易云音乐歌单相似度计算与歌曲推荐 |
| 模型评估 | RMSE、MAE、交叉验证、网格搜索 |
6.2 下一步优化方向
- 引入歌曲内容特征:将歌曲的流派、节奏、歌词等特征融入模型,解决冷启动问题
- 使用深度学习:尝试 Neural Collaborative Filtering (NCF) 等深度推荐模型
- 实时推荐:结合用户实时行为(播放、跳过、收藏)进行动态推荐
- AB 测试:上线后进行在线评估,对比不同算法的实际效果
6.3 系列文章回顾
| 序号 | 文章 | 核心算法 |
|---|---|---|
| 1 | 二手车价格预测 | XGBoost 回归 |
| 2 | 互联网架构 | 系统设计 |
| 3 | 工资分类预测 | 决策树/随机森林 |
| 4 | 广告点击转化率预测 | 逻辑回归 |
| 5 | 数据仓库Hive讲义 | Hive SQL |
| 6 | 文本分类-自然语言处理 | TF-IDF + 朴素贝叶斯 |
| 7 | 旅游景点票价预测 | 线性回归 |
| 8 | 气象数据分析 | 时序分析 |
| 9 | 申请评分卡 | WOE/IV + 逻辑回归 |
| 10 | 电商大数据购买行为分析 | RFM 模型 |
| 11 | 电影数据分析 | 关联规则 |
| 12 | 航班准点数据分析 | 随机森林 |
| 13 | 论坛项目 | Flask + MySQL |
| 14 | 足球运动员分析 | K-Means 聚类 |
| 15 | 银行客户流失分析 | GBDT |
| 16 | 音乐推荐系统 | 协同过滤 + Surprise |
🎵 预告:至此,"Python大数据实战"系列 16 篇已全部完成!感谢读者一路陪伴。后续将开启新系列,敬请期待。
参考链接
- Surprise 官方文档 --- Simple Python RecommendatIon System Engine
- Surprise GitHub 仓库 --- 源码与示例
- 协同过滤推荐算法综述 --- 知乎专栏深度解析
- 网易云音乐推荐算法探秘 --- 腾讯云开发者社区
- Matrix Factorization Techniques for Recommender Systems --- Netflix Prize 经典论文
📝 免责声明:本文所用数据集仅供学习研究使用,不涉及商业用途。代码示例基于 Surprise 1.1.1 版本编写,不同版本可能存在 API 差异。
本文发表于 2026-07-07,是"Python大数据实战"系列的第 16 篇(完结篇)。