AI 音辨世界:艺术小白的我,靠这个AI模型,速识音乐流派选择音乐 ⛵


?? 作者:韩信子@ShowMeAI
?? 数据分析实战系列:https://www.showmeai.tech/tutorials/40
?? 机器学习实战系列:https://www.showmeai.tech/tutorials/41
?? 本文地址:https://www.showmeai.tech/article-detail/309
?? 声明:版权所有,转载请联系平台与作者并注明出处
?? 收藏ShowMeAI查看更多精彩内容

只要给到足够的相关信息,AI模型可以迅速学习一个新的领域问题,并构建起很好的知识和预估系统。比如音乐领域,借助于歌曲相关信息,模型可以根据歌曲的音频和歌词特征将歌曲精准进行流派分类。在本篇内容中 ShowMeAI 就带大家一起来看看,如何基于机器学习完成对音乐的识别分类。

本篇内容使用到的数据集为 ??Spotify音乐数据集,大家也可以通过 ShowMeAI 的百度网盘地址快速下载。

?? 实战数据集下载(百度网盘):公众号『ShowMeAI研究中心』回复『实战』,或者点击 这里 获取本文 [18]音乐流派识别的机器学习系统搭建与调优 『Spotify 音乐数据集

? ShowMeAI官方GitHub:https://github.com/ShowMeAI-Hub

我们在本篇内容中将用到最常用的 boosting 集成工具库 LightGBM,并且将结合 optuna 工具库对其进行超参数调优,优化模型效果。

关于 LightGBM 的模型原理和使用详细讲解,欢迎大家查阅 ShowMeAI 的文章:

??图解机器学习算法(11) | LightGBM模型详解

??机器学习实战(5) | LightGBM建模应用详解

本篇文章包含以下内容板块:

  • 数据概览和预处理
  • EDA探索性数据分析
  • 歌词特征&数据降维
  • 建模和超参数优化
  • 总结&经验

?? 数据概览和预处理

本次使用的数据集包含超过 18000 首歌曲的信息,包括其音频特征信息(如活力度,播放速度或调性等),以及歌曲的歌词。

我们读取数据并做一个速览如下:

import pandas as pd
# 读取数据
data = pd.read_csv("spotify_songs.csv")
# 数据速览
data.head()
# 数据基本信息
data.info()

字段说明如下:

字段 含义
track_id 歌曲唯一ID
track_name 歌曲名称
track_artist 歌手
lyrics 歌词
track_popularity 唱片热度
track_album_id 唱片的唯一ID
track_album_name 唱片名字
track_album_release_date 唱片发行日期
playlist_name 歌单名称
playlist_id 歌单ID
playlist_genre 歌单风格
playlist_subgenre 歌单子风格
danceability 舞蹈性描述的是根据音乐元素的组合,包括速度、节奏的稳定性、节拍的强度和整体的规律性,来衡量一首曲目是否适合跳舞。0.0的值是最不适合跳舞的,1.0是最适合跳舞的。
energy 能量是一个从0.0到1.0的度量,代表强度和活动的感知度。一般来说,有能量的曲目给人的感觉是快速、响亮。例如,死亡金属有很高的能量,而巴赫的前奏曲在该量表中得分较低。
key 音轨的估测总调。用标准的音阶符号将整数映射为音高。例如,0=C,1=C?/D?,2=D,以此类推。如果没有检测到音调,则数值为-1。
loudness 轨道的整体响度,单位是分贝(dB)。响度值是整个音轨的平均值,对于比较音轨的相对响度非常有用。
mode 模式表示音轨的调式(大调或小调),即其旋律内容所来自的音阶类型。大调用1表示,小调用0表示。
speechiness 言语性检测音轨中是否有口语。录音越是完全类似于语音(如脱口秀、说唱、诗歌),属性值就越接近1.0。
acousticness 衡量音轨是否为声学的信心指数,从0.0到1.0。1.0表示该曲目为原声的高置信度。
instrumentalness 预测一个音轨是否包含人声。越接近1.0该曲目就越有可能不包含人声内容。
liveness 检测录音中是否有听众存在。越接近现场演出数值越大。
valence 0.0到1.0,描述了一个音轨所传达的音乐积极性,接近1的曲目听起来更积极(如快乐、欢快、兴奋),而接近0的曲目听起来更消极(如悲伤、压抑、愤怒)。
tempo 轨道的整体估计速度,单位是每分钟节拍(BPM)。
duration_ms 歌曲的持续时间(毫秒)
language 歌词的语言语种

原始的数据有点杂乱,我们先进行过滤和数据清洗。

# 数据工具库
import pandas as pd
import re

# 歌词处理的nlp工具库
import nltk
from nltk.corpus import stopwords
from collections import Counter
# nltk.download('stopwords')

# 读取数据
data = pd.read_csv("spotify_songs.csv")
# 字段选择
keep_cols = [x for x in data.columns if not x.startswith("track") and not x.startswith("playlist")]
keep_cols.append("playlist_genre")
df = data[keep_cols].copy()
# 只保留英文歌曲
subdf = df[(df.language == "en") & (df.playlist_genre != "latin")].copy().drop(columns = "language")


# 歌词规整化,全部小写
pattern = r"[^a-zA-Z ]"
subdf.lyrics = subdf.lyrics.apply(lambda x: re.sub(pattern, "", x.lower()))

# 移除停用词
subdf.lyrics = subdf.lyrics.apply(lambda x: ' '.join([word for word in x.split() if word not in (stopwords.words("english"))]))

# 查看歌词中的词汇出现的频次

# 连接所有歌词
all_text = " ".join(subdf.lyrics)
# 统计词频
word_count = Counter(all_text.split())
# 如果一个词在200首以上的歌里都出现,则保留,否则视作低频过滤掉
keep_words = [k for k, v in word_count.items() if v > 200]
# 构建一个副本
lyricdf = subdf.copy().reset_index(drop=True)
# 字段名称规范化
lyricdf.columns = ["audio_"+ x if not x in ["lyrics", "playlist_genre"] else x for x in lyricdf.columns]
# 歌词内容
lyricdf.lyrics = lyricdf.lyrics.apply(lambda x: Counter([word for word in x.split() if word in keep_words]))
# 构建词汇词频Dataframe
unpacked_lyrics = pd.DataFrame.from_records(lyricdf.lyrics).add_prefix("lyrics_")
# 缺失填充为0
unpacked_lyrics = unpacked_lyrics.fillna(0) 
# 拼接并删除原始歌词列
lyricdf = pd.concat([lyricdf, unpacked_lyrics], axis = 1).drop(columns = "lyrics")
# 排序
reordered_cols = [col for col in lyricdf.columns if not col.startswith("lyrics_")] + sorted([col for col in lyricdf.columns if col.startswith("lyrics_")])
lyricdf = lyricdf[reordered_cols]

# 存储为新的csv文件
lyricdf.to_csv("music_data.csv", index = False)

主要的数据预处理在上述代码的注释里大家可以看到,核心步骤概述如下:

  • 过滤数据以仅包含英语歌曲并删除“拉丁”类型的歌曲(因为这些歌曲几乎完全是西班牙语,所以会产生严重的类不平衡)。
  • 通过将歌词设为小写、删除标点符号和停用词来整理歌词。计算每个剩余单词在歌曲歌词中出现的次数,然后过滤掉所有歌曲中出现频率最低的单词(混乱的数据/噪音)。
  • 清理与排序。

?? EDA探索性数据分析

和过往所有的项目一样,我们也需要先对数据做一些分析和更进一步的理解,也就是EDA探索性数据分析过程。

EDA数据分析部分涉及的工具库,大家可以参考ShowMeAI制作的工具库速查表和教程进行学习和快速使用。
??数据科学工具库速查表 | Pandas 速查表
??图解数据分析:从入门到精通系列教程

首先我们检查一下我们的标签(流派)的类分布和平衡

# 分组统计
by_genre = data.groupby("playlist_genre")["audio_key"].count().reset_index()
fig, ax = plt.subplots()

# 绘图
ax.bar(by_genre.playlist_genre, by_genre.audio_key)
ax.set_ylabel("Number of Observations")
ax.set_xlabel("Genre")
ax.set_title("Observations per Class")
ax.set_ylim(0, 4000)

# 每个柱子上标注数量
rects = ax.patches
for rect in rects:
    height = rect.get_height()
    ax.text(
        rect.get_x() + rect.get_width() / 2, height + 5, height, ha="center", va="bottom"
    )

存在轻微的类别不平衡,那后续我们在交叉验证和训练测试拆分时候注意数据分层(保持比例分布) 即可。

# 把所有字段切分为音频和歌词列
audio = data[[x for x in data.columns if x.startswith("audio")]]
lyric = data[[x for x in data.columns if x.startswith("lyric")]]
# 让字段命名更简单一些
audio.columns = audio.columns.str.replace("audio_", "")
lyric.columns = lyric.columns.str.replace("lyric_", "")

?? 歌词特征&数据降维

我们的机器学习算法在处理高维数据的时候,可能会有一些性能问题,有时候我们会对数据进行降维处理。

降维的本质是将高维数据投影到低维子空间中,同时尽可能多地保留数据中的信息。关于降维大家可以查看 ShowMeAI 的算法原理讲解文章 ??ShowMeAI 的文章:

??机器学习实战(5) | LightGBM建模应用详解

下面我们会基于Optuna这个工具库对 LightGBM 的超参数进行调优,我们需要在 param 定义超参数的搜索空间,在此基础上 Optuna 会进行优化和超参数的选择。


# 建模
from sklearn.model_selection import StratifiedKFold
import lightgbm as lgb
from optuna.integration import LightGBMPruningCallback

# 定义目标函数
def objective(trial, X, y):    
    # 候选超参数
    param = {**fixed_params,
        'boosting_type': 'gbdt',
        'num_leaves': trial.suggest_int('num_leaves', 2, 3000, step = 20),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.2, 0.99, step = 0.05),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.2, 0.99, step = 0.05),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        "n_estimators": trial.suggest_int("n_estimators", 200, 5000),
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3),
        "max_depth": trial.suggest_int("max_depth", 3, 12),
        "min_data_in_leaf": trial.suggest_int("min_data_in_leaf", 5, 2000, step=5),
        "lambda_l1": trial.suggest_float("lambda_l1", 1e-8, 10.0, log=True),
        "lambda_l2": trial.suggest_float("lambda_l2", 1e-8, 10.0, log=True),
        "min_gain_to_split": trial.suggest_float("min_gain_to_split", 0, 10),
        "max_bin": trial.suggest_int("max_bin", 200, 300),
    }
    
    # 构建分层交叉验证
    cv = StratifiedKFold(n_splits = 5, shuffle = True)
    # 5组得分
    cv_scores = np.empty(5)
    
    # 切分为K个数据组,轮番作为训练集和验证集进行实验
    for idx, (train_idx, test_idx) in enumerate(cv.split(X, y)):
        # 数据切分
        X_train_cv, X_test_cv = X.iloc[train_idx], X.iloc[test_idx]
        y_train_cv, y_test_cv = y[train_idx], y[test_idx]

        # 转为lightgbm的Dataset格式
        train_data = lgb.Dataset(X_train_cv, label = y_train_cv, categorical_feature="auto")
        val_data = lgb.Dataset(X_test_cv, label = y_test_cv,  categorical_feature="auto",
                              reference = train_data)
        
        # 回调函数
        callbacks = [
            LightGBMPruningCallback(trial, metric = "f1_score"),
                     # 间歇输出信息
                    lgb.log_evaluation(period = 100),
                     # 早停止,防止过拟合
                    lgb.early_stopping(50)]

        # 训练模型
        model = lgb.train(params = param,  train_set = train_data,
                          valid_sets = val_data,   
                          callbacks = callbacks,
                          feval = lgb_f1_score # 自定义评估准则
                         )
        
        # 预估
        preds = np.argmax(model.predict(X_test_cv), axis = 1)
        # 计算f1-score
        cv_scores[idx] = f1_score(y_test_cv, preds, average = "macro")

    return np.mean(cv_scores)

?? 超参数优化

我们在上面定义完了目标函数,现在可以使用 Optuna 来调优模型的超参数了。

# 超参数优化
import optuna

# 定义Optuna的实验次数
n_trials = 200
# 构建Optuna study去进行超参数检索与调优
study = optuna.create_study(direction = "maximize", # 最大化交叉验证的F1得分
                            study_name = "LGBM Classifier",
                           pruner=optuna.pruners.HyperbandPruner())
func = lambda trial: objective(trial, X_train, y_train)
study.optimize(func, n_trials = n_trials)

然后,我们可以使用 ??Optuna 的可视化模块不同超参数组合的性能进行可视化查看。例如,我们可以使用 plot_param_importances(study) 查看哪些超参数对模型性能/影响优化最重要。

plot_param_importances(study)

我们也可以使用 plot_parallel_coordinate(study)查看尝试了哪些超参数组合/范围可以带来高评估结果值(好的效果性能)。

plot_parallel_coordinate(study)

然后我们可以使用 plot_optimization_history 查看历史情况。

plot_optimization_history(study)

在Optuna完成调优之后:

  • 最好的超参数存储在 study.best_params 属性中。我们把模型的最终参数 params 定义为 params = {**fixed_params, **study.best_params} 即可,如后续的代码所示。
  • 当然,你也可以缩小搜索空间/超参数范围,进一步做精确的超参数优化。
# 最佳模型实验
cv = StratifiedKFold(n_splits = 5, shuffle = True)
# 5组得分
cv_scores = np.empty(5)

# 切分为K个数据组,轮番作为训练集和验证集进行实验
for idx, (train_idx, test_idx) in enumerate(cv.split(X, y)):
    # 数据切分
    X_train_cv, X_test_cv = X.iloc[train_idx], X.iloc[test_idx]
    y_train_cv, y_test_cv = y[train_idx], y[test_idx]

    # 转为lightgbm的Dataset格式
    train_data = lgb.Dataset(X_train_cv, label = y_train_cv, categorical_feature="auto")
    val_data = lgb.Dataset(X_test_cv, label = y_test_cv,  categorical_feature="auto",
                          reference = train_data)
    
    # 回调函数
    callbacks = [
        LightGBMPruningCallback(trial, metric = "f1_score"),
                 # 间歇输出信息
                lgb.log_evaluation(period = 100),
                 # 早停止,防止过拟合
                lgb.early_stopping(50)]

    # 训练模型
    model = lgb.train(params = {**fixed_params, **study.best_params},  train_set = train_data,
                      valid_sets = val_data,   
                      callbacks = callbacks,
                      feval = lgb_f1_score # 自定义评估准则
                     )
    
    # 预估
    preds = np.argmax(model.predict(X_test_cv), axis = 1)
    # 计算f1-score
    cv_scores[idx] = f1_score(y_test_cv, preds, average = "macro")

?? 最终评估

通过上述过程我们就获得了最终模型,让我们来评估一下吧!


# 预估与评估训练集
train_preds = model.predict(X_train)
train_predictions = np.argmax(train_preds, axis = 1)
train_error = f1_score(y_train, train_predictions, average = "macro")

# 交叉验证结果
cv_error = np.mean(cv_scores)

# 评估测试集
test_preds = model.predict(X_test)
test_predictions = np.argmax(test_preds, axis = 1)
test_error = f1_score(y_test, test_predictions, average = "macro")

# 存储评估结果
results = pd.DataFrame({"n_components": n_components,
                        "reduction_method": reduction_method,
                        "train_error": train_error,
                        "cv_error": cv_error,
                        "test_error": test_error,
                        "n_trials": n_trials
                       }, index = [0])

我们可以实验和比较不同的降维方法、降维维度,再调参查看模型效果。如下图所示,在我们当前的尝试中,PCA降维到 400 维产出最好的模型 ——macro f1-score 为66.48%。

?? 总结

在本篇内容中, ShowMeAI 展示了基于歌曲信息与文本对其进行『流派』分类的过程,包含对文本数据的处理、特征工程、模型建模和超参数优化等。大家可以把整个pipeline作为一个模板来应用在其他任务当中。

参考资料

  • ?? 图解数据分析:从入门到精通系列教程:https://www.showmeai.tech/tutorials/3
  • ?? 数据科学工具库速查表 | Pandas 速查表:https://www.showmeai.tech/article-detail/101
  • ?? 图解机器学习算法 | 降维算法详解:https://www.showmeai.tech/article-detail/198
  • ?? 图解机器学习算法 | LightGBM模型详解:https://www.showmeai.tech/article-detail/195
  • ?? 机器学习实战 | LightGBM建模应用详解:https://www.showmeai.tech/article-detail/205
  • ?? Optuna 的可视化模块
  • ?? Akiba,T., Sano, S., Yanase, T., Ohta, T., & Koyama, M. (2019, July). Optuna: A next-generation hyperparameter optimization framework. In Proceedings of the 25th ACM SIGKDD international conference on knowledge discovery & data mining (pp. 2623–2631).
  • ?? Autoencoder Feature Extractions
  • ?? Kaggler’s Guide to LightGBM Hyperparameter Tuning with Optuna in 2021
  • ?? You Are Missing Out on LightGBM. It Crushes XGBoost in Every Aspect