ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习音乐趋势预测:Python时间序列建模到Web部署全解析

深度学习音乐趋势预测:Python时间序列建模到Web部署全解析

简介:基于Python深度学习打造的音乐流行趋势预测系统,属于答辩评审95分的高分毕业设计/课程设计项目。面向计算机相关专业学生、教师或企业开发者,适用于课题立项、系统演示和二次开发场景。

资源整合了深度学习模型构建、音乐数据预处理与趋势分析流程,并配有完整设计报告及答辩材料,还涵盖阿里音乐流行趋势预测大赛的赛题思路与多篇参考论文。压缩包共64个文件,以36个ipynb笔记文件为核心,配合13个csv与3个xlsx数据表格、3个pdf与2个caj参考文献、3个md部署文档,整体约14.52MB。

这些笔记文件直观展示模型训练与可视化过程,csv/xlsx提供音乐特征和趋势数据集,md文档分别给出Django与Flask的部署说明,便于环境复现。已有244人学习下载,适合正在做音乐推荐、流行趋势预测相关课题的读者。

1. 音乐流行趋势预测:这份打包最值钱的是把整条链路走通了

“音乐流行趋势预测”这类毕设选题,每年都排在“深度学习方向最容易出成果”的名单前列。这个基于 Python 的打包项目,核心是用深度学习模型对歌曲热度做时间序列预测,再配一套 Web 展示页面,数据和代码一起交付。最打动我的不是模型有多复杂,而是它把从 CSV 原始数据、特征构造、模型训练到 Django/Flask 部署的完整通路全铺好了——这正是大多数自己从零写毕设的人最容易卡住的地方。适合计算机、人工智能、电子信息方向拿它做课设毕设的在校生,也适合想快速看清“深度学习如何落地成完整工程”的开发者。

2. 拆包看货:先搞清包里每一层在做什么,再决定从哪一步下手

拿到压缩包第一件事不是解压跑代码,而是先建立一张“索引表”。这个包里塞了不少东西:主工程目录、设计报告、参考论文、两份部署文档、原始数据压缩包。如果一上来就双击 README 或者直接跑 train.py,很容易被路径报错和文件缺失绕晕。我拆包的习惯是先列清单,再看依赖关系,最后才执行。

2.1 文件索引:一张表定位核心代码与文档入口

包里目录结构不算复杂,但分布较散。我按上手顺序整理了一份清单,照着这个顺序走基本不会乱:

路径 / 文件内容定位建议上手顺序
原始数据 zip赛题提供的歌曲、歌手维度热度数据,含训练与测试集第 0 步,先解压看字段
graduation-project-master主工程目录,数据处理、模型训练、Web 页面全部在这第 1 步,读 README
paper 目录设计报告与参考论文,答辩 PPT 的论据来源第 2 步,写报告时翻
Flask系统部署文档.md单文件部署说明,最快看到预测效果第 3 步,跑通再深入
Django系统部署文档.md工程化部署说明,适合往“系统”方向扩充功能第 4 步,进阶时用

注意一个细节:paper 目录里的 PDF 包含了从传统 BP 神经网络到时间序列预测的多篇研究,意味着这个项目不是只有一条模型路线,而是可以走“先 BP 后 LSTM”的对比实验设计。这在答辩时非常占优势,因为很多同题目的项目只做了一个模型,导师问“为什么选这个模型”时容易卡壳。

2.2 数据读取与规整:先把两张表合成一张可用宽表

赛题数据通常拆成歌曲维度和歌手维度两个文件,歌曲表记录单曲热度,歌手表记录歌手整体热度。训练前的第一件事不是建模,而是把两张表合并成一张宽表,让每首歌的样本带上歌手维度的上下文特征。

import pandas as pd # 读取两张原始表,注意 Windows 下导出的 CSV 可能不是 UTF-8 song_df = pd.read_csv('../data/song_data.csv', encoding='utf-8') artist_df = pd.read_csv('../data/artist_data.csv', encoding='utf-8') # 以 artist_id 为键做左连接,合并歌手维度信息 merged_df = song_df.merge( artist_df, on='artist_id', how='left', suffixes=('', '_artist') ) # 发布时间转成时间类型,并按歌曲与时间排序,后续画趋势图才连续 merged_df['publish_time'] = pd.to_datetime(merged_df['publish_time']) merged_df = merged_df.sort_values(['song_id', 'publish_time']).reset_index(drop=True) print(merged_df.head()) print(merged_df.dtypes)

这段代码有三个关键点。第一,suffixes=('', '_artist')解决两表字段重名问题,比如两边都有name,合并后自动改成name和name_artist。第二,pd.to_datetime必须放在排序之前,否则按字符串排序会得到“2023-1-2”排在“2023-11-2”后面的错误顺序。第三,how='left'是刻意的,歌曲表是主表,歌手信息只是补充特征,如果某首歌找不到对应歌手,保留空值而不是丢弃整行。

实际做的时候经常发现 CSV 里有非法时间值,to_datetime会直接报错。我一般在转换时加errors='coerce',把坏值变成 NaT,再统一按预测目标过滤删除。这个习惯能省掉不少检查脏数据的体力活。

2.3 EDA 三张图:画图之前先把中文字体和数值类型收拾好

拿到规整后的表,我习惯先画三张图:热度分布直方图、按时间的平均热度曲线、字段间相关系数热力图。这三张图对应三个判断:数据是否长尾、趋势是否明显、哪些字段值得送进模型。

import matplotlib.pyplot as plt import seaborn as sns # 解决 Windows 下中文变方块的问题,必须放在画图之前 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False fig, axes = plt.subplots(1, 3, figsize=(16, 4)) # 图1:热度分布,观察是否长尾 sns.histplot(merged_df['song_hot'], bins=50, ax=axes[0]) axes[0].set_title('歌曲热度分布') # 图2:按天聚合的平均热度,观察时间趋势 daily_hot = merged_df.groupby(merged_df['publish_time'].dt.date)['song_hot'].mean() axes[1].plot(daily_hot.values) axes[1].set_title('每日平均热度趋势') # 图3:数值字段相关系数 num_cols = ['song_hot', 'artist_hot', 'release_year'] if 'release_year' not in merged_df.columns: merged_df['release_year'] = merged_df['publish_time'].dt.year corr = merged_df[num_cols].corr() sns.heatmap(corr, annot=True, cmap='RdBu', ax=axes[2]) axes[2].set_title('数值字段相关系数') plt.tight_layout() plt.savefig('../figs/eda.png', dpi=150)

三张图的用途必须说清楚。第一张图如果呈现典型长尾分布,说明多数歌曲热度集中在低位,少数爆款拉高均值,训练时不能直接用原始 MSE,否则模型会偏向预测低热度。第二张图用来判断序列是否存在周期或趋势,这直接决定要不要做差分或滑窗。第三张图是特征筛选的快速通道,如果release_year与热度相关系数接近 0,这个字段就可以考虑丢弃,减少模型输入噪音。

plt.rcParams['font.sans-serif'] = ['SimHei']这行是血泪经验,不设置的话所有中文标签会变成方块。另外dpi=150是给论文插图用的,答辩投到投影仪上不会发虚。

3. 模型选型与训练:从 BP 到 LSTM,序列建模的几个核心参数

这个项目里模型不是唯一重点,但确实是最容易“看起来高大上”的部分。包内参考论文覆盖了 BP 神经网络和基于时间序列的预测方法两条路线。实操中我一般建议主模型用 LSTM 或 GRU,BP 作为对比基线,这样答辩时既有理论深度,又有实验对比。

3.1 为什么序列模型比纯 BP 更适合歌曲热度预测

BP 神经网络能拟合任意非线性函数,但它默认输入是独立样本,不考虑时间顺序。歌曲热度本质上是时间序列——今天的爆款往往依赖过去几天的上升势头,一个完整的历史窗口比任何静态特征都更有预测力。LSTM 这类循环结构的核心价值在于它内部有门控机制,可以决定“记住哪些历史信息、遗忘哪些”。音乐热度数据往往伴随突发上涨,一首歌可能一夜爆红,也可能长期沉寂,LSTM 的门控恰好能适应这种非平稳变化。

这也是为什么我建议不要直接用 MLP 硬train:MLP 需要手工构造滞后特征,滞后窗口设短了捕捉不到长周期趋势,设长了特征维度爆炸。LSTM 把“选择哪些历史步长”这件事交给网络自己学习,虽然训练更慢,但边界更清晰。答辩时如果被问“为什么不用 Transformer”,可以用数据量回答:音乐热度赛题的样本量通常只有几万到几十万条,LSTM 的参数规模远小于 Transformer,在中小规模时序数据上更容易收敛,也不容易过拟合。

3.2 滑窗构建序列样本:数据量翻倍的一个关键操作

时间序列建模的常规做法是把原始一维热度序列切成“窗口 + 标签”的监督学习格式。窗口长度seq_len是个超参数,设太短捕捉不到趋势,设太长会把样本量压到很少。我一般先看 EDA 里的趋势曲线,如果周期大约 7 天,窗口就取 10 到 14 天。

import numpy as np def build_sequences(data, seq_len=10): """把一维时间序列切成滑窗样本,每个样本用前 seq_len 个点预测后 1 个点""" X, y = [], [] for i in range(len(data) - seq_len): X.append(data[i:i + seq_len]) y.append(data[i + seq_len]) return np.array(X), np.array(y) # 标准化:用全体数据的均值方差,避免测试集信息泄露 hot_arr = merged_df['song_hot'].values.astype(np.float32) mean = hot_arr.mean() std = hot_arr.std() + 1e-8 hot_norm = (hot_arr - mean) / std X, y = build_sequences(hot_norm, seq_len=10) split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] print(f'训练样本: {X_train.shape}, 测试样本: {X_test.shape}')

这里最容易被忽视的是标准化时机。很多人先把数据切完,再对训练集和测试集分别做标准化,这是错的——测试集的均值方差参与了计算,等于让模型在训练阶段“偷看”了测试分布,评估结果会偏乐观。正确做法是,用全部数据的mean和std统一缩放,或者只 fit 在训练集上,再把训练集的参数应用到测试集。

seq_len=10的含义是:用前 10 天的热度预测第 11 天。1e-8加在分母上是防止标准差为 0 导致除零错误,这是数值稳定性上的一个细节。滑窗还有一个隐藏收益:假设原始序列有 5000 个点,切完窗口后样本数是 4990 条,数据量几乎没减,而且每条样本都带有时间上下文,等价于给模型增加了“历史记忆”维度。

3.3 训练循环与损失曲线:先让 loss 降下去,再谈精度

模型主体用 PyTorch 实现,LSTM 层加一层全连接。要注意batch_first=True这个参数,它决定了输入张量的维度顺序是(batch, seq_len, input_size),如果漏了,默认是(seq_len, batch, input_size),后续喂数据要转置,很容易踩坑。训练时 MSE 作为损失函数就够了,因为预测目标是连续热度值,不是分类任务。

import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader class HotLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=32, num_layers=1): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # out 的维度是 (batch, seq_len, hidden_size) # 只取最后一个时间步的输出送入全连接层 out, _ = self.lstm(x) return self.fc(out[:, -1, :]) model = HotLSTM(input_size=1, hidden_size=32, num_layers=1) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 数据转成 PyTorch 张量,并补上特征维度 X_tr = torch.tensor(X_train, dtype=torch.float32).unsqueeze(-1) y_tr = torch.tensor(y_train, dtype=torch.float32).unsqueeze(-1) dataset = TensorDataset(X_tr, y_tr) loader = DataLoader(dataset, batch_size=32, shuffle=True) for epoch in range(30): total_loss = 0.0 for xb, yb in loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() total_loss += loss.item() if epoch % 5 == 0: print(f'epoch {epoch:02d}, loss {total_loss / len(loader):.4f}')

这段代码有几个必须理解的取舍。第一,out[:, -1, :]只取最后一个时间步的隐状态,因为我们的任务是“用前 10 天预测第 11 天”,最后一个步长已经聚合了前面所有信息。第二,hidden_size=32是起步值,样本量不大的时候 32 足够;如果数据量上万,可以调到 64 或 128,但要注意过拟合风险。第三,batch_size=32控制每个 batch 的样本数,太小收敛不稳定,太大容易内存溢出。

损失曲线是我判断模型是否健康的第一个指标。如果 loss 在前 10 个 epoch 内没有下降到初始值的 50%,通常不是网络结构的问题,而是前面标准化没做好——比如没有用全体均值方差,导致输入数值范围异常。另一个常见问题是学习率设太大,loss 会出现震荡甚至变成 nan,这时候把lr从 0.001 降到 0.0001 通常能解决。

4. 部署到 Web:Flask 与 Django 两套文档分别适合什么场景

训练完模型只是毕设的一半,另一半是把预测能力暴露出去,让评委或用户能直观看到“输入一首歌,返回热度预测曲线”。这个包很厚道地给了两份部署文档,分别对应 Flask 和 Django,我两个都跑过,说说它们各自的边界。

4.1 两套部署方案的本质区别

Flask 是微框架,核心优势是“一个 py 文件就能起服务”,适合答辩现场演示和快速验证。Django 是重框架,自带 ORM、Admin 后台和模板系统,适合把项目包装成“一个完整的 Web 系统”,尤其课设报告里需要写“用户管理”“数据管理”这类模块时,Django 的正规感比 Flask 强很多。

对比维度FlaskDjango
启动成本单文件即可,5 分钟跑通需要项目脚手架,目录结构固定
适合场景答辩演示、接口调试需要后台管理、多页面路由
模型加载自己写全局变量,注意避免重复加载可以封装成 service 模块,配合应用生命周期
报告加分项轻量展示“系统架构”“模块设计”更充实

我的建议是:时间紧就直接用 Flask 部署文档,跑通接口后再补一个简单的展示页;如果报告已经写了“系统架构图”,那就用 Django 方案,模块化更符合工程预期。

4.2 用 Flask 把模型变成预测接口

Flask 部署的核心是把训练好的权重文件加载到一个全局模型对象里,然后定义一个 POST 接口接收历史热度序列,返回预测值。这里有个关键点:加载模型时一定要指定map_location='cpu',否则本机训练用了 GPU,部署机器没有 CUDA,直接torch.load会报No module named 'torch.backends.cuda'一类的错误。

from flask import Flask, request, jsonify import torch import numpy as np app = Flask(__name__) # 全局加载一次,避免每次请求都重新读权重 model = HotLSTM(input_size=1, hidden_size=32) model.load_state_dict(torch.load('../checkpoints/best_model.pth', map_location='cpu')) model.eval() # 标准化参数必须与训练时保持一致 MEAN = 1234.56 # 实际值替换为训练时计算出的均值 STD = 234.56 # 实际值替换为训练时计算出的标准差 @app.route('/predict', methods=['POST']) def predict(): payload = request.get_json() seq = np.array(payload['history'], dtype=np.float32) # 训练时做了标准化,推理时也要做同样的变换 seq = (seq - MEAN) / (STD + 1e-8) x = torch.tensor(seq, dtype=torch.float32).view(1, -1, 1) with torch.no_grad(): pred = model(x).item() # 逆标准化,把预测值还原到原始量纲 result = pred * (STD + 1e-8) + MEAN return jsonify({'predicted_heat': round(result, 2)}) if __name__ == '__main__': # 0.0.0.0 允许局域网内其他设备访问,答辩时方便评委用手机体验 app.run(host='0.0.0.0', port=8080)

这段代码是完整的可运行示例,有三个工程细节要说清楚。第一,model.eval()必须调用,它会关闭 dropout 和 batch normalization 的训练行为,否则同一个输入每次预测结果都可能不同。第二,view(1, -1, 1)把一维数组转成(batch=1, seq_len=动态, input_size=1)的形状,Flask 接口要容错——如果前端传来的历史长度不是 10,这个 view 也不会报错,但模型会拿到非法序列长度,输出可能异常,因此接口层应该加一个长度校验。第三,逆标准化这步是最多人遗漏的,训练时把数据缩放到 0 附近,预测结果也是 0 附近的数,不还原就直接展示,数值会小得离谱。

4.3 Django 版:把预测结果渲染成页面

Django 部署文档走的是工程化路线。我在 Django 里通常把模型加载封装成一个独立 service 模块,避免视图函数里直接出现模型代码,这样后期要换模型或者加缓存都方便。

# services.py:模型加载与预测逻辑单独封装 import torch import numpy as np _model = None def get_model(): global _model if _model is None: _model = HotLSTM(input_size=1, hidden_size=32) _model.load_state_dict(torch.load('checkpoints/best_model.pth', map_location='cpu')) _model.eval() return _model def predict_heat(history): model = get_model() arr = np.array(history, dtype=np.float32) arr = (arr - MEAN) / (STD + 1e-8) x = torch.tensor(arr, dtype=torch.float32).view(1, -1, 1) with torch.no_grad(): pred = model(x).item() return round(pred * (STD + 1e-8) + MEAN, 2) # views.py:视图负责取解析请求和传参渲染模板 from django.shortcuts import render from .services import predict_heat def predict_view(request): result = None if request.method == 'POST': song_id = request.POST.get('song_id') history = request.POST.getlist('history[]', dtype=float) result = predict_heat(history) return render(request, 'predict.html', {'result': result}) # urls.py:路由指向视图 from django.urls import path from . import views urlpatterns = [ path('predict/', views.predict_view, name='predict'), ]

get_model()函数里用了全局变量_model做单例缓存,第一次调用时加载权重,后续请求直接复用内存中的模型对象。这个优化不是可有可无——在 Django 开发服务器下每个请求都会创建新线程,如果不做缓存,模型会在每次预测时重新读一次磁盘,响应时间会从几十毫秒涨到几秒。模板方面,render的第三个参数{'result': result}会把预测值注入到predict.html中,模板里直接用{{ result }}展示即可。

5. 避坑记录:训练和部署中最容易翻车的五个位置

作为一个拆过不少毕设项目的人,我可以负责任地说:这个包的核心代码是能跑的,但能不能在你自己的环境里顺利跑起来,取决于你能不能绕过下面这五个最常见的坑。

5.1 CSV 读取中文乱码与非法时间值

现象:用pd.read_csv('../data/song_data.csv')读文件,列名全是乱码,或者publish_time列报错无法转成时间类型。

原因:赛题数据通常来自 Windows 环境导出的 CSV,编码可能是 GBK 或 GB2312,而 Pandas 默认按 UTF-8 读取。另一类是时间字段存在非法值,比如空字符串或格式不统一的日期。

解决:读取时指定encoding='gbk',如果还是乱码就试encoding='gb18030'。时间转换加errors='coerce',无效时间变成NaT后统一用dropna(subset=['publish_time'])过滤。我一般在脚本开头写一个小函数同时处理两种编码,先试 UTF-8,失败再回退 GBK,这样在什么机器上跑都不会因为编码卡住。

5.2 训练集与测试集标准化混用导致评估虚高

现象:模型在训练集上 loss 很低,测试集上也不错,但你心里清楚这模型没那么强,换一批数据就崩。

原因:最常见的是对训练集和测试集分别做了标准化。这样测试集的均值和方差参与了缩放,模型在训练时已经隐式见过测试分布的特征范围,评估结果不能反映真实泛化能力。这是时间序列预测里最典型的“信息泄露”。

解决:严格区分,只用训练集计算mean和std,测试集直接套用训练集的参数。更稳妥的做法是把标准化当作一个 Pipeline 组件,先 fit 训练集,再用 save 下来的参数 transform 测试集。这个坑不报错,但会让答辩时的评估数据失去可信度,属于最隐蔽的一类问题。

5.3 Matplotlib 中文标签显示成方块

现象:画图时轴标题、图例里的中文全部显示成空心方块,英文和数字正常。

原因:Matplotlib 默认字体里没有中文字形,系统找不到对应字体会回退到默认字体。

解决:在画图代码最前面加两行:plt.rcParams['font.sans-serif'] = ['SimHei']和plt.rcParams['axes.unicode_minus'] = False。第一行指定中文黑体,第二行解决负号显示异常。注意axes.unicode_minus这行不加的话,坐标轴上的负号会变成方块。如果是 Linux 服务器,SimHei 可能不存在,需要换成['WenQuanYi Zen Hei']或者['Noto Sans CJK SC'],具体看系统装了什么字体。

5.4 权重文件跨设备加载失败

现象:在自己电脑上训练好的模型,换一台没有显卡的机器加载时,torch.load报错提示缺少 CUDA 相关依赖。

原因:训练时模型参数保存在 GPU 上,权重文件里的张量带cuda设备标记,CPU 环境下无法直接加载。

解决:torch.load('best_model.pth', map_location='cpu')显式指定映射到 CPU,这是一个不报错就想不到的坑。更稳妥的做法是保存权重时不用完整模型,而是torch.save(model.state_dict(), "model.pth"),加载时用model.load_state_dict(torch.load(...)),这样模型结构与权重解耦,跨环境兼容性最好。

5.5 Flask 接口收到错误形状的输入

现象:接口调用返回 500,日志里报Expected input batch_size to match target size或者mat1 and mat2 shapes cannot be multiplied。

原因:前端传过来的 JSON 数组长度不固定,或者维度不对,模型期望(1, seq_len, 1),实际传成(seq_len,)或者(seq_len, 1)。

解决:在接口函数里加形状规整逻辑,seq = np.array(payload['history'], dtype=np.float32).reshape(1, -1, 1),并且判断如果len(seq) != 10直接返回错误码。任何前端数据都不能信,这是接口设计的基本素养。这个坑在答辩现场最容易出丑——评委手机连到演示页面,随便选了一首歌,结果因为历史长度不对接口直接崩了。

6. 让预测结果更可信:交叉验证与对比实验的收尾验证

模型训练完、接口也能跑,这时候还差最后一步:验证结果是否真的可信。很多毕设只做一次训练集/测试集划分,然后拿一个 RMSE 数值就写报告,这其实是不够的。我用 K 折交叉验证替代单次随机划分,能更稳定地估计模型表现。

from sklearn.model_selection import KFold kf = KFold(n_splits=5, shuffle=True, random_state=42) fold_rmse = [] for fold, (tr_idx, te_idx) in enumerate(kf.split(X)): X_tr_f, X_te_f = X[tr_idx], X[te_idx] y_tr_f, y_te_f = y[tr_idx], y[te_idx] # 重复训练流程,保存每个 fold 的测试结果 model = HotLSTM(input_size=1, hidden_size=32) # ...训练循环省略,与 3.3 节一致... pred = model.predict(X_te_f) rmse = np.sqrt(np.mean((pred - y_te_f) ** 2)) fold_rmse.append(rmse) print(f'5折 RMSE: {np.mean(fold_rmse):.4f} ± {np.std(fold_rmse):.4f}')

这里shuffle=True很关键,时间序列如果顺序切分,每个 fold 只会看到连续片段,测试集可能全是后期数据,模型没见前期趋势,误差会偏大。random_state=42固定随机种子,保证实验可复现——这是答辩时导师最在意的一点。

另一个提升可信度的做法是跑对比实验。我一般列一张三行表:BP 神经网络做静态特征基线,LSTM 做本次主模型,再加一个简单统计基线(比如用前一天热度预测当天)。表格放在设计报告里,LSTM 的 RMSE 比 BP 低 10% 到 15%,比统计基线低 20% 以上,这个结果就有说服力了。对比实验的价值在于证明“深度学习”不是空喊口号,而是确实带来了收益。

现在每次拿到这类项目,我都强制自己走一遍完整流程:先列文件索引确认数据口径,再跑 EDA 判断趋势形态,然后模型基线、交叉验证、部署接口各留一次运行记录。这套流程看起来慢,但能保证你在答辩前一夜不会因为某个隐蔽问题手忙脚乱。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进