机器学习-音乐艺人流行趋势预测

一、数据背景

经过7年的发展与沉淀,目前阿里音乐拥有数百万的曲库资源,每天千万的用户活跃在平台上,拥有数亿人次的用户试听、收藏等行为。在原创艺人和作品方面,更是拥有数万的独立音乐人,每月上传上万个原创作品,形成超过几十万首曲目的原创作品库,如此庞大的数据资源库对于音乐流行趋势的把握有着极为重要的指引作用。

本次大赛以阿里音乐用户的历史播放数据为基础,期望研究人员可以通过对阿里音乐平台上每个阶段艺人的试听量的预测,挖掘出即将成为潮流的艺人,从而实现对一个时间段内音乐流行趋势的准确把控。

数据来源:音乐流行趋势预测数据集_数据集-阿里云天池

二、模型介绍

矩阵分解算法

矩阵分解的核心思想是将用户和项目映射到同一个低维隐空间中,通过隐向量的内积来刻画用户对项目的潜在偏好。这种隐语义模型假设,用户对艺人的喜爱程度并非由某个显式特征直接决定,而是受到若干隐含因子的共同影响,例如音乐风格、演唱方式、情感表达等。

传统协同过滤算法(基准模型)

基于用户的协同过滤(UserCF)是最早被广泛应用的推荐算法之一,其基本假设是:具有相似试听历史的用户会对未来听到的艺人表现出相似的偏好。具体地,对于目标用户u,算法首先计算用户u与其他所有用户在用户-艺人交互矩阵上的相似度,常用的相似度度量有余弦相似度和皮尔逊相关系数。

评估函数

本次实验用了RMSE、MAE。

三、实验过程

1.前期导入库

复制代码
import warnings
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import cosine
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error

warnings.filterwarnings("ignore")
plt.rcParams["font.family"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False

DATA_PATH = r" "   # 请修改为你的实际路径
CF_K = 30
PMF_D_LIST = [10, 20, 30]
TRAIN_MONTH = 6
TEST_MONTH = 1
NDCG_K1 = 10
NDCG_K2 = 20
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)

建议用镜像源安装。

2.数据清洗

复制代码
# 读取无表头的Excel,手动赋予列名
df = pd.read_excel(DATA_PATH, header=None)
df.columns = ['user_id', 'song_id', 'gmt_create', 'action_type', 'Ds']
print(f"原始行数: {len(df)}")
print("前3行原始数据:")
print(df.head(3))

# 重命名 song_id -> artist_id
df.rename(columns={'song_id': 'artist_id'}, inplace=True)

print("\naction_type 原始类型:", df['action_type'].dtype)
print("action_type 唯一值:", df['action_type'].unique()[:10])
# 强制转换为整数(如果是字符串则转换)
df['action_type'] = pd.to_numeric(df['action_type'], errors='coerce')
print("转换后 action_type 类型:", df['action_type'].dtype)
print("转换后唯一值:", df['action_type'].unique()[:10])

# 只保留播放行为(action_type == 1)
df = df[df['action_type'] == 1].copy()
print(f"过滤播放行为后行数: {len(df)}")

if len(df) == 0:
    raise Exception("错误:没有播放行为数据,请检查 action_type 列的值")

# 添加播放次数
df['play_count'] = 1

# 时间转换
df['action_time'] = pd.to_datetime(df['gmt_create'], unit='s')
invalid_time = df['action_time'].isna().sum()
if invalid_time > 0:
    print(f"警告:有 {invalid_time} 行时间戳无效,将被丢弃")
    df = df.dropna(subset=['action_time'])

print(f"有效时间数据行数: {len(df)}")

# 提取年月
df['year_month'] = df['action_time'].dt.to_period('M')
print(f"时间范围: {df['year_month'].min()} 至 {df['year_month'].max()}")
print("各月份数据量:")
print(df['year_month'].value_counts().sort_index())

这里要注意确保 action_type 是整数类型

3.过滤低频交互

复制代码
user_counts = df.groupby('user_id')['play_count'].sum()
artist_counts = df.groupby('artist_id')['play_count'].sum()

min_user_plays = 5
min_artist_plays = 5

keep_users = user_counts[user_counts >= min_user_plays].index
keep_artists = artist_counts[artist_counts >= min_artist_plays].index

before = len(df)
df = df[df['user_id'].isin(keep_users) & df['artist_id'].isin(keep_artists)]
print(f"过滤后剩余行数: {len(df)} (原{before}行)")
print(f"剩余用户数: {df['user_id'].nunique()}")
print(f"剩余物品数: {df['artist_id'].nunique()}")

4.时间窗口划分

复制代码
print("\n===== 板块4:时间窗口划分 =====")
df = df.sort_values('action_time').reset_index(drop=True)
all_months = sorted(df['year_month'].unique())
print("数据中的所有月份:", all_months)
print(f"总月份数: {len(all_months)}")

if len(all_months) < TRAIN_MONTH + TEST_MONTH:
    print(f"警告:总月份数 {len(all_months)} 不足 {TRAIN_MONTH+TEST_MONTH},将调整训练集月份数")
    if len(all_months) <= TEST_MONTH:
        raise Exception("错误:数据月份数不足,无法分出测试集,请检查时间转换或使用更多数据。")
    TRAIN_MONTH = len(all_months) - TEST_MONTH
    print(f"调整后训练月份数: {TRAIN_MONTH}")

train_months = all_months[:TRAIN_MONTH]
test_months = all_months[TRAIN_MONTH:TRAIN_MONTH+TEST_MONTH]

df_train = df[df['year_month'].isin(train_months)]
df_test = df[df['year_month'].isin(test_months)]

print(f"训练集月份: {train_months}, 样本数: {len(df_train)}")
print(f"测试集月份: {test_months}, 样本数: {len(df_test)}")

5.构建交互矩阵

复制代码
def build_interaction_matrix(df, user2idx=None, artist2idx=None):
    if user2idx is None:
        users = df['user_id'].unique()
        artists = df['artist_id'].unique()
        user2idx = {u: i for i, u in enumerate(users)}
        artist2idx = {a: i for i, a in enumerate(artists)}
        R = np.zeros((len(users), len(artists)), dtype=np.float32)
    else:
        R = np.zeros((len(user2idx), len(artist2idx)), dtype=np.float32)
    grouped = df.groupby(['user_id', 'artist_id'])['play_count'].sum()
    for (u, a), val in grouped.items():
        if u in user2idx and a in artist2idx:
            R[user2idx[u], artist2idx[a]] = val
    return R, user2idx, artist2idx

print("\n===== 板块5:构建训练矩阵 =====")
R_train_raw, user2idx, artist2idx = build_interaction_matrix(df_train)
print(f"训练矩阵形状: {R_train_raw.shape}")
print(f"非零元素数: {np.count_nonzero(R_train_raw)}")

# 对数变换 + 归一化
R_log = np.log1p(R_train_raw)
scaler = MinMaxScaler()
R_train = scaler.fit_transform(R_log)
sparsity = (1 - np.count_nonzero(R_train) / R_train.size) * 100
print(f"训练矩阵稀疏度: {sparsity:.2f}%")
print(f"矩阵内存占用: {R_train.nbytes / 1024**2:.2f} MB")

print("\n构建测试矩阵(使用训练集的索引)")
R_test_raw, _, _ = build_interaction_matrix(df_test, user2idx, artist2idx)
R_test_log = np.log1p(R_test_raw)
R_test = scaler.transform(R_test_log)
print(f"测试矩阵形状: {R_test.shape}")
print(f"测试集非零元素数: {np.count_nonzero(R_test_raw)}")

6.评估指标函数

复制代码
def calc_rmse(y_true, y_pred):
    return np.sqrt(mean_squared_error(y_true, y_pred))

def calc_mae(y_true, y_pred):
    return mean_absolute_error(y_true, y_pred)

def calc_ndcg(rank_list, k):
    if len(rank_list) == 0:
        return 0.0
    rank_list = np.array(rank_list)[:k]
    dcg = np.sum(rank_list / np.log2(np.arange(2, len(rank_list)+2)))
    idcg = np.sum(np.sort(rank_list)[::-1] / np.log2(np.arange(2, len(rank_list)+2)))
    return dcg / idcg if idcg != 0 else 0.0

def evaluate_model(pred_mat, true_mat):
    mask = true_mat > 0
    if not np.any(mask):
        return {"RMSE": 0.0, "MAE": 0.0, "NDCG@10": 0.0, "NDCG@20": 0.0}
    y_true = true_mat[mask]
    y_pred = pred_mat[mask]
    rmse = calc_rmse(y_true, y_pred)
    mae = calc_mae(y_true, y_pred)
    
    ndcg10_list = []
    ndcg20_list = []
    n_items = true_mat.shape[1]
    for a in range(n_items):
        pred_rank = pred_mat[:, a]
        ndcg10_list.append(calc_ndcg(pred_rank, NDCG_K1))
        ndcg20_list.append(calc_ndcg(pred_rank, NDCG_K2))
    return {
        "RMSE": rmse,
        "MAE": mae,
        "NDCG@10": np.mean(ndcg10_list),
        "NDCG@20": np.mean(ndcg20_list)
    }

7:协同过滤模型

复制代码
class UserCF:
    def __init__(self, R, k):
        self.R = R
        self.k = k
        self.n_user = R.shape[0]
        self.sim = self._calc_user_similarity()
    def _calc_user_similarity(self):
        sim = np.zeros((self.n_user, self.n_user))
        for i in range(self.n_user):
            for j in range(i+1, self.n_user):
                s = 1 - cosine(self.R[i], self.R[j])
                sim[i, j] = sim[j, i] = s
        return sim
    def predict(self):
        pred = np.zeros_like(self.R)
        for u in range(self.n_user):
            neighbors = np.argsort(self.sim[u])[::-1][1:self.k+1]
            if len(neighbors) > 0:
                pred[u] = np.mean(self.R[neighbors], axis=0)
        return pred

class ItemCF:
    def __init__(self, R, k):
        self.R = R
        self.k = k
        self.n_item = R.shape[1]
        self.sim = self._calc_item_similarity()
    def _calc_item_similarity(self):
        sim = np.zeros((self.n_item, self.n_item))
        for i in range(self.n_item):
            for j in range(i+1, self.n_item):
                s = 1 - cosine(self.R[:, i], self.R[:, j])
                sim[i, j] = sim[j, i] = s
        return sim
    def predict(self):
        pred = np.zeros_like(self.R)
        for a in range(self.n_item):
            neighbors = np.argsort(self.sim[a])[::-1][1:self.k+1]
            if len(neighbors) > 0:
                pred[:, a] = np.mean(self.R[:, neighbors], axis=1)
        return pred

8:概率矩阵分解 PMF

复制代码
class PMF:
    def __init__(self, R, latent_dim, lr=0.01, reg=0.01, epochs=30):
        self.R = R
        self.n_user, self.n_item = R.shape
        self.d = latent_dim
        self.lr = lr
        self.reg = reg
        self.epochs = epochs
        self.U = np.random.normal(0, 0.1, (self.n_user, self.d))
        self.V = np.random.normal(0, 0.1, (self.n_item, self.d))
    def train(self, verbose=True):
        for epoch in range(self.epochs):
            pred = self.U @ self.V.T
            grad_U = -2 * (self.R - pred) @ self.V + 2 * self.reg * self.U
            grad_V = -2 * (self.R - pred).T @ self.U + 2 * self.reg * self.V
            self.U -= self.lr * grad_U
            self.V -= self.lr * grad_V
            if verbose and (epoch+1) % 10 == 0:
                loss = np.sum((self.R - pred)**2) + self.reg*(np.sum(self.U**2)+np.sum(self.V**2))
                print(f"  PMF d={self.d}, epoch {epoch+1}/{self.epochs}, loss={loss:.2f}")
    def predict(self):
        return self.U @ self.V.T

9:训练 UserCF、ItemCF 和 PMF 网格搜索

复制代码
t0 = time.time()
usercf = UserCF(R_train, CF_K)
pred_usercf = usercf.predict()
time_usercf = time.time() - t0
res_usercf = evaluate_model(pred_usercf, R_test)
print(f"UserCF 耗时 {time_usercf:.2f}s, RMSE={res_usercf['RMSE']:.4f}")

print("\n===== 训练 ItemCF =====")
t0 = time.time()
itemcf = ItemCF(R_train, CF_K)
pred_itemcf = itemcf.predict()
time_itemcf = time.time() - t0
res_itemcf = evaluate_model(pred_itemcf, R_test)
print(f"ItemCF 耗时 {time_itemcf:.2f}s, RMSE={res_itemcf['RMSE']:.4f}")

print("\n===== 训练 PMF 并搜索最佳维度 =====")
best_res = None
best_pred = None
best_d = None
pmf_results = {}
for d in PMF_D_LIST:
    t0 = time.time()
    pmf = PMF(R_train, d, epochs=30)
    pmf.train()
    pred = pmf.predict()
    cost = time.time() - t0
    res = evaluate_model(pred, R_test)
    pmf_results[d] = (res, cost, pred)
    print(f"d={d} 完成, RMSE={res['RMSE']:.4f}, 耗时{cost:.2f}s")
    if best_res is None or res['RMSE'] < best_res['RMSE']:
        best_res = res
        best_pred = pred
        best_d = d

10:结果

复制代码
result_df = pd.DataFrame({
    "模型": ["UserCF", "ItemCF", f"PMF(d={best_d})"],
    "RMSE": [res_usercf["RMSE"], res_itemcf["RMSE"], best_res["RMSE"]],
    "MAE": [res_usercf["MAE"], res_itemcf["MAE"], best_res["MAE"]],
    "NDCG@10": [res_usercf["NDCG@10"], res_itemcf["NDCG@10"], best_res["NDCG@10"]],
    "NDCG@20": [res_usercf["NDCG@20"], res_itemcf["NDCG@20"], best_res["NDCG@20"]],
    "耗时(s)": [time_usercf, time_itemcf, pmf_results[best_d][1]]
})
print(result_df.round(4))

11:可视化

复制代码
# PMF 参数影响图
d_vals = list(pmf_results.keys())
rmse_vals = [pmf_results[d][0]['RMSE'] for d in d_vals]
ndcg_vals = [pmf_results[d][0]['NDCG@10'] for d in d_vals]

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(d_vals, rmse_vals, 'o-', color='red')
axes[0].set_xlabel('Latent Dimension d')
axes[0].set_ylabel('RMSE')
axes[0].set_title('PMF RMSE vs d')
axes[0].grid(True)

axes[1].plot(d_vals, ndcg_vals, 's-', color='blue')
axes[1].set_xlabel('Latent Dimension d')
axes[1].set_ylabel('NDCG@10')
axes[1].set_title('PMF NDCG@10 vs d')
axes[1].grid(True)
plt.tight_layout()
plt.show()

# 模型对比柱状图
models = ['UserCF', 'ItemCF', f'PMF(d={best_d})']
rmse_all = [res_usercf['RMSE'], res_itemcf['RMSE'], best_res['RMSE']]
ndcg_all = [res_usercf['NDCG@10'], res_itemcf['NDCG@10'], best_res['NDCG@10']]

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].bar(models, rmse_all, color=['#1f77b4', '#ff7f0e', '#2ca02c'])
axes[0].set_ylabel('RMSE (lower better)')
axes[0].set_title('模型 RMSE 对比')

axes[1].bar(models, ndcg_all, color=['#1f77b4', '#ff7f0e', '#2ca02c'])
axes[1].set_ylabel('NDCG@10 (higher better)')
axes[1].set_title('模型 NDCG@10 对比')
plt.tight_layout()
plt.show()
相关推荐
鲜于言悠9054 小时前
Jev爆火:不生成文字,直接输出概率的决策模型
人工智能
美团技术团队4 小时前
如何在“不能出错”的系统里使用 AI Coding?分布式存储实践与边界
人工智能
枫叶丹44 小时前
AI 提出科学假设之后:从数据筛选到实验验证
人工智能·chatgpt·开源·agent·codex
kaixin_啊啊4 小时前
【零基础学AI】第 19 章——用分类模型识别类别
人工智能·分类·数据挖掘
郝学胜-神的一滴4 小时前
AI 编程智能体 17:四步搞定专属AI智能体
大数据·人工智能·python·神经网络·算法·langchain·强化学习
vx153027823625 小时前
CDGA|AI时代,数据治理的位置下沉了吗?
大数据·人工智能·cdga·数据治理
程序员梅雨5 小时前
【从零开始学习数据仓库 | 第一篇】:维度建模理论
大数据·数据仓库·学习·阿里云·数据分析
微尘hjx5 小时前
【StrongSort 03】多轨迹算法StrongSort改进点
人工智能·算法·yolo·视觉检测·ultralytics·strongsort·轨迹算法
openKylin5 小时前
openKylin亮相 2026 麒麟软件教育医疗创新应用大会
人工智能·开源·openkylin
梦帮科技5 小时前
QML-04-QGraph-Diff:量子非欧图扩散生成模型与 SE(3) 等变分子晶格逆向合成完整技术指南(上部分)
人工智能·算法·自动化·量子计算·傅里叶分析