Python大数据实战(十六):音乐推荐系统——基于协同过滤算法构建个性化歌单引擎

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;} 收集用户行为
找到相似用户/物品
计算并推荐
偏好数据

不分析兴趣标签
通过行为计算相似

非属性相似
基于场景

进行协同推荐

  1. 收集用户行为:汇总能体现用户偏好的行为数据,但不分析具体的兴趣标签
  2. 找到相似用户/物品:通过协同行为计算相似度(非属性相似)
  3. 计算并推荐:基于场景进行协同推荐

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 推荐算法 KNNBasicSVDNMFBaselineOnly
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 下一步优化方向

  1. 引入歌曲内容特征:将歌曲的流派、节奏、歌词等特征融入模型,解决冷启动问题
  2. 使用深度学习:尝试 Neural Collaborative Filtering (NCF) 等深度推荐模型
  3. 实时推荐:结合用户实时行为(播放、跳过、收藏)进行动态推荐
  4. 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 1.1.1 版本编写,不同版本可能存在 API 差异。


本文发表于 2026-07-07,是"Python大数据实战"系列的第 16 篇(完结篇)。

相关推荐
带娃的IT创业者1 小时前
突破算力与安全的边界:深度解析 Mythos AI 的“受信发布”机制与技术影响
人工智能·安全·大语言模型·ai安全·mythos ai·受信发布·ai监管
dreamread1 小时前
2026紫微八字同排工具怎么选:看盘面切换、功能边界和学习路径
人工智能·软件工具·传统文化
耍酷的魔镜1 小时前
核心设计理念:5W2H、JSON-LD 与通用知识图谱
人工智能·json·知识图谱
SunnyDays10111 小时前
Python PDF 转 Markdown 详解(转换整个文档,特定页面,表格,扫描 PDF)
python·pdf 转 markdown·pdf 转 md·扫描 pdf 转 md·pdf 表格转 md
2601_954706492 小时前
云手机 API 自动化实战:Python 批量控机、挂机脚本完整实现
python·智能手机·自动化
星河耀银海2 小时前
大模型安全:对抗攻击与防御方法
人工智能·安全·大模型
xsdick2 小时前
抛弃 OpenClawd吧!我用 Go 打造了企业级 Swarm(蜂群)agent,更智能,更安全、性能快 5 倍、成本直降80%
人工智能·ai·ai编程
冬奇Lab2 小时前
Workflow 系列(10):企业级架构——注册表、组合与治理
人工智能·工作流引擎
冬奇Lab3 小时前
每日一个开源项目(第153篇):AgentsView - 跨 30+ AI 编程工具的会话分析和费用追踪,本地优先,无需账号
人工智能·开源·ai编程