news 2026/9/14 16:33:25

用户画像与协同过滤双路音乐推荐系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用户画像与协同过滤双路音乐推荐系统实战

简介:本资源是一个面向人工智能初学者与项目实践者的音乐推荐系统完整工程,聚焦用户画像构建与协同过滤算法融合应用,解决个性化音乐推荐中的冷启动与精度提升问题。压缩包共74个文件,含30个Python核心模块(如recommend.py、models.py、views.py)、前端静态资源(9个CSS、8个HTML、7个JS)及Django配置文件、SQLite数据库、Docker部署脚本(Dockerfile、docker-compose.yml)和KKBox数据预处理脚本(genre_proc.py、replace_genre_lang.py),整体12.84MB,结构清晰,覆盖前后端开发、模型训练与容器化部署全流程。已有363人学习下载。读者可直接复现基于SVD矩阵分解的评分预测流程,深入理解用户画像标签体系与协同过滤的加权融合策略,并获得真实工业级音乐数据集(KKBox挑战赛)的完整处理链路与可运行Web界面,具备教学演示与二次开发双重价值。

1. 这不是“猜你喜欢”的简单升级:用用户画像+协同过滤双路建模,让音乐推荐从泛化走向可解释、可调控

你可能已经试过只用surprise库跑一个SVD模型,输入用户-歌曲评分矩阵,输出 top-N 推荐列表——结果准确率尚可,但运营同学一问“为什么给张三推《夜曲》”,你就只能答“模型算出来的”。这恰恰暴露了纯协同过滤的致命短板:黑箱驱动、冷启动脆弱、无法响应业务规则干预。而本项目标题里明确并列的“用户画像”与“协同过滤”,指向一种工业级推荐系统的真实落地范式:用结构化用户特征锚定长期兴趣,用实时行为序列强化短期偏好,再通过加权融合实现可解释的决策闭环。它不追求学术 SOTA,而是解决“如何让算法工程师能向产品讲清每条推荐背后的逻辑”“如何在新歌上线 24 小时内进入推荐池”“如何对 VIP 用户提升长尾曲库曝光权重”等真实需求。适合正在做课程设计、实习项目或中小厂推荐模块迭代的 Python 工程师——你需要的不是理论推导,而是能直接pip installpython main.py、修改config.yaml就看到效果的最小可行路径。

2. 用户画像子系统:从原始日志到可计算标签的三层构建法

用户画像不是把年龄、性别、地域字段塞进数据库就完事。真正支撑推荐的画像,必须是可量化、可更新、可参与模型计算的向量表示。本项目采用三层递进式构建:基础属性层(静态)、行为序列层(动态)、兴趣聚类层(抽象)。每一层都对应明确的数据源、清洗逻辑和存储格式,避免常见误区——比如把用户最近播放的 10 首歌直接当特征,却不做归一化;或用未去重的设备 ID 当用户主键,导致画像漂移。

2.1 基础属性层:用 Pandas 实现轻量级标签生成

原始数据通常来自埋点日志(CSV/JSON)或业务库(MySQL),字段包括user_id,device_id,register_time,last_login,province,age,gender等。关键动作是去噪+补全+离散化

import pandas as pd import numpy as np # 读取原始用户表(模拟) df_user = pd.read_csv("raw_user_data.csv") # 步骤1:处理缺失值——用众数填充性别,用中位数填充年龄 df_user['gender'] = df_user['gender'].fillna(df_user['gender'].mode()[0]) df_user['age'] = df_user['age'].fillna(df_user['age'].median()) # 步骤2:年龄分段(避免连续值干扰后续树模型) df_user['age_group'] = pd.cut(df_user['age'], bins=[0, 18, 25, 35, 45, 60, 100], labels=['0-18', '19-25', '26-35', '36-45', '46-60', '60+']) # 步骤3:省份映射为经济层级(需业务确认,此处为示例) province_tier = { '广东': 'Tier1', '江苏': 'Tier1', '浙江': 'Tier1', '河南': 'Tier2', '四川': 'Tier2', '湖南': 'Tier2', '青海': 'Tier3', '西藏': 'Tier3', '宁夏': 'Tier3' } df_user['province_tier'] = df_user['province'].map(province_tier).fillna('Unknown') # 输出结构化画像表(供后续 join) df_profile = df_user[['user_id', 'gender', 'age_group', 'province_tier']].copy() df_profile.to_parquet("user_profile.parquet", index=False)

提示pd.cutbinslabels必须严格匹配,否则pd.cut返回NaNmap()对未覆盖键默认返回NaN,务必用fillna()处理,否则后续get_dummies()会报错。

2.2 行为序列层:用时间窗口聚合播放行为

协同过滤依赖用户-物品交互,但原始日志是逐条事件(如play_start,play_end,skip)。需按用户聚合为带权重的行为序列,而非简单统计播放次数:

# 读取播放日志(含时间戳) df_log = pd.read_csv("play_log.csv", parse_dates=['event_time']) df_log = df_log.sort_values(['user_id', 'event_time']) # 定义行为权重(业务规则:完整播放 > 播放 > 跳过) weight_map = {'play_end': 1.0, 'play_start': 0.3, 'skip': -0.5} df_log['weight'] = df_log['event_type'].map(weight_map) # 按用户+歌曲聚合最近7天行为(避免历史噪音) recent_window = df_log[df_log['event_time'] > (df_log['event_time'].max() - pd.Timedelta(days=7))] user_song_weight = recent_window.groupby(['user_id', 'song_id'])['weight'].sum().reset_index() # 生成用户-歌曲交互矩阵(稀疏格式,节省内存) from scipy.sparse import coo_matrix import numpy as np user_ids = user_song_weight['user_id'].astype('category').cat.codes song_ids = user_song_weight['song_id'].astype('category').cat.codes weights = user_song_weight['weight'].values # 构建 COO 矩阵(行=user_id索引,列=song_id索引,值=权重) interaction_matrix = coo_matrix((weights, (user_ids, song_ids)), shape=(len(user_ids.cat.categories), len(song_ids.cat.categories))) # 保存为 .npz 格式(比 CSV 节省 80% 存储,加载快 3x) from scipy.io import savemat savemat("interaction_matrix.npz", {'data': interaction_matrix})

注意coo_matrix是构建稀疏矩阵的高效方式,但不能直接用于 sklearn 模型训练;后续需转为csr_matrixcsc_matrixastype('category')pd.factorize()更安全,避免新用户/新歌曲导致索引错位。

2.3 兴趣聚类层:用层次聚类发现隐式音乐偏好群组

单纯用KMeans对用户向量聚类易受维度灾难影响。本项目采用基于行为相似度的层次聚类(Agglomerative Clustering),先计算用户间 Jaccard 相似度,再聚类:

from sklearn.cluster import AgglomerativeClustering from sklearn.metrics import pairwise_distances import numpy as np # 从 interaction_matrix 提取用户行为向量(二值化:是否播放过) binary_matrix = (interaction_matrix > 0).astype(int).toarray() # 计算 Jaccard 距离(适用于稀疏二值数据) jaccard_dist = pairwise_distances(binary_matrix, metric='jaccard') # 层次聚类(n_clusters=50 是经验值,需根据业务调整) clustering = AgglomerativeClustering( n_clusters=50, metric='precomputed', linkage='average' ) user_clusters = clustering.fit_predict(jaccard_dist) # 保存聚类结果(user_id -> cluster_id 映射) df_cluster = pd.DataFrame({ 'user_id': df_user['user_id'], 'cluster_id': user_clusters[:len(df_user)] # 确保长度一致 }) df_cluster.to_csv("user_clusters.csv", index=False)
参数取值建议说明
n_clusters30~100用户量 <10万用 30,>100万用 80~100;过多导致群组无区分度
linkage'average''single'更稳定,比'complete'更敏感于局部结构
metric'precomputed'必须传入距离矩阵,不可省略

3. 协同过滤引擎:SVD++ 模型的 PyTorch 实现与参数调优

纯 Memory-based CF(如 User-CF)在百万级用户场景下计算开销巨大,而 Matrix Factorization(MF)类模型更易扩展。本项目选择SVD++——它在经典 SVD 基础上显式建模用户隐式反馈(如播放、收藏),比surprise.SVD更贴合音乐场景。我们用 PyTorch 自定义实现,而非调包,以便深度控制正则项、学习率衰减和负采样策略。

3.1 SVD++ 模型定义:显式引入用户行为偏置

SVD++ 的核心公式为:
$$\hat{r}{ui} = \mu + b_u + b_i + q_i^T(p_u + |N(u)|^{-\frac{1}{2}} \sum{j \in N(u)} y_j)$$
其中 $N(u)$ 是用户 $u$ 的隐式行为物品集,$y_j$ 是物品 $j$ 的隐式反馈向量。PyTorch 实现需同时管理p_u,q_i,b_u,b_i,y_j五组参数:

import torch import torch.nn as nn import torch.optim as optim class SVDPP(nn.Module): def __init__(self, n_users, n_items, n_factors=64, dropout=0.1): super().__init__() self.n_users = n_users self.n_items = n_items self.n_factors = n_factors # 显式参数(SVD) self.user_factors = nn.Embedding(n_users, n_factors) self.item_factors = nn.Embedding(n_items, n_factors) self.user_bias = nn.Embedding(n_users, 1) self.item_bias = nn.Embedding(n_items, 1) # 隐式参数(SVD++ 特有) self.item_y = nn.Embedding(n_items, n_factors) # y_j 向量 self.dropout = nn.Dropout(dropout) # 初始化(Xavier 均匀分布) nn.init.xavier_uniform_(self.user_factors.weight) nn.init.xavier_uniform_(self.item_factors.weight) nn.init.xavier_uniform_(self.item_y.weight) def forward(self, user_idx, item_idx, implicit_items=None, implicit_weights=None): # 基础预测 mu = 0.0 # 全局均值(可从训练集计算) b_u = self.user_bias(user_idx).squeeze() b_i = self.item_bias(item_idx).squeeze() p_u = self.user_factors(user_idx) q_i = self.item_factors(item_idx) base_pred = mu + b_u + b_i + torch.sum(p_u * q_i, dim=1) # SVD++ 隐式项:∑ y_j / sqrt(|N(u)|) if implicit_items is not None and len(implicit_items) > 0: y_j = self.item_y(implicit_items) # [batch, seq_len, factors] if implicit_weights is not None: y_j = y_j * implicit_weights.unsqueeze(-1) # 加权求和 y_sum = torch.sum(y_j, dim=1) # [batch, factors] norm_factor = torch.sqrt(torch.tensor(len(implicit_items), dtype=torch.float32)) implicit_term = torch.sum(p_u * (y_sum / norm_factor), dim=1) return base_pred + implicit_term else: return base_pred # 初始化模型(n_users/n_items 来自 interaction_matrix.shape) model = SVDPP( n_users=interaction_matrix.shape[0], n_items=interaction_matrix.shape[1], n_factors=64 )

逻辑说明forward方法中implicit_items是用户近期播放过的歌曲 ID 列表(如[1024, 3056, 789]),implicit_weights是对应权重(如[1.0, 0.8, 0.3])。y_j向量被加权求和后,与用户因子p_u点积,形成对用户长期兴趣的修正项。

3.2 训练循环:负采样 + AdamW + 梯度裁剪

音乐推荐中正样本稀疏(用户只听过极小部分歌曲),必须负采样。本项目采用Uniform Negative Sampling(非热门采样,避免偏差):

def sample_negative(user_id, pos_items, n_neg=5): """为 user_id 采样 n_neg 个负样本(排除已播放歌曲)""" all_items = list(range(interaction_matrix.shape[1])) neg_items = [] while len(neg_items) < n_neg: candidate = np.random.choice(all_items) if candidate not in pos_items: # 确保不采样正样本 neg_items.append(candidate) return neg_items # 训练主循环 optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-5) criterion = nn.MSELoss() for epoch in range(10): model.train() total_loss = 0 for batch in dataloader: # dataloader 每批含 user_id, pos_item_id, implicit_items optimizer.zero_grad() # 获取隐式行为(最近播放的 10 首歌) implicit_items = batch['implicit_items'] # shape: [batch, 10] implicit_weights = batch['implicit_weights'] # shape: [batch, 10] # 正样本预测 pos_pred = model( user_idx=batch['user_id'], item_idx=batch['pos_item_id'], implicit_items=implicit_items, implicit_weights=implicit_weights ) # 负样本预测(每个正样本配 5 个负样本) neg_items = [] for u_id, pos_list in zip(batch['user_id'], batch['pos_items']): neg_items.extend(sample_negative(u_id, pos_list, n_neg=5)) neg_items = torch.tensor(neg_items, dtype=torch.long) neg_pred = model( user_idx=batch['user_id'].repeat_interleave(5), item_idx=neg_items, implicit_items=implicit_items.repeat(1,5).view(-1,10), # 广播 implicit_weights=implicit_weights.repeat(1,5).view(-1,10) ) # BPR Loss(隐式反馈优化目标) loss = -torch.mean(torch.log(torch.sigmoid(pos_pred - neg_pred))) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 防梯度爆炸 optimizer.step() total_loss += loss.item() print(f"Epoch {epoch}, Loss: {total_loss/len(dataloader):.4f}")
关键参数推荐值作用
lr1e-3初始学习率,SVD++ 对 lr 敏感,过高导致震荡
weight_decay1e-5L2 正则,防止过拟合,尤其对user_factors有效
n_factors32~128维度越高表达力越强,但训练慢;64 是平衡点
clip_grad_norm_1.0必须设置,否则 SVD++ 训练易发散

4. 双路融合推荐:用户画像特征注入与在线服务部署

协同过滤给出“相似用户喜欢什么”,用户画像回答“这个用户是谁”。二者不能简单加权平均,而应通过特征交叉+门控机制实现动态融合。本项目采用轻量级FeatureGating模块,在推理时实时计算画像对推荐结果的影响权重。

4.1 特征工程:将画像转化为模型可接受的数值向量

用户画像需与 SVD++ 的嵌入向量对齐维度。这里用One-Hot Encoding+Dense Projection

# 加载画像数据 df_profile = pd.read_parquet("user_profile.parquet") df_cluster = pd.read_csv("user_clusters.csv") # 合并画像(one-hot 编码分类变量) df_feat = df_profile.merge(df_cluster, on='user_id', how='left') categorical_cols = ['gender', 'age_group', 'province_tier', 'cluster_id'] encoded = pd.get_dummies(df_feat[categorical_cols], drop_first=True) # 投影到 64 维(匹配 SVD++ factor size) from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(encoded) # 用线性投影降维(避免高维稀疏) proj_model = LinearRegression() proj_model.fit(X_scaled, np.random.randn(len(X_scaled), 64)) # 伪标签,实际用预训练 user_profile_vec = proj_model.predict(X_scaled) # shape: [n_users, 64] # 保存为 numpy array np.save("user_profile_vectors.npy", user_profile_vec)

注意pd.get_dummies(..., drop_first=True)避免共线性;LinearRegression在此仅作投影工具,实际项目中可用 PCA 或预训练 AutoEncoder 替代。

4.2 FeatureGating 融合层:让画像决定协同过滤的可信度

Gating 机制公式:
$$\text{final_score} = \alpha \cdot \text{cf_score} + (1-\alpha) \cdot \text{profile_score}$$
其中 $\alpha = \sigma(W_g [p_u; v_u] + b_g)$,$v_u$ 是用户画像向量,$\sigma$ 是 sigmoid。PyTorch 实现:

class FeatureGating(nn.Module): def __init__(self, n_factors=64, hidden_dim=32): super().__init__() self.gate_net = nn.Sequential( nn.Linear(n_factors * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), nn.Sigmoid() ) def forward(self, cf_emb, profile_emb): # cf_emb: [batch, 64], profile_emb: [batch, 64] concat = torch.cat([cf_emb, profile_emb], dim=1) # [batch, 128] alpha = self.gate_net(concat) # [batch, 1] return alpha * cf_emb + (1 - alpha) * profile_emb # 在推理时使用 gating = FeatureGating() cf_output = model.user_factors(user_id) # [batch, 64] profile_vec = torch.tensor(user_profile_vec[user_id]) # [batch, 64] fused_emb = gating(cf_output, profile_vec) # [batch, 64] # 与 item_factors 点积得最终分数 item_embs = model.item_factors.weight # [n_items, 64] scores = torch.matmul(fused_emb, item_embs.T) # [batch, n_items] top_k_scores, top_k_indices = torch.topk(scores, k=10, dim=1)

4.3 Flask API 部署:支持实时推荐请求

将模型打包为 REST API,接收user_id返回推荐列表:

from flask import Flask, request, jsonify import torch import numpy as np app = Flask(__name__) # 加载模型与向量 model = torch.load("svdpp_model.pth", map_location='cpu') model.eval() user_profile_vec = np.load("user_profile_vectors.npy") gating = FeatureGating() @app.route('/recommend', methods=['POST']) def recommend(): data = request.json user_id = int(data['user_id']) # 获取用户嵌入 cf_emb = model.user_factors(torch.tensor([user_id])) profile_emb = torch.tensor(user_profile_vec[user_id]).unsqueeze(0) # 融合 fused_emb = gating(cf_emb, profile_emb) # 计算所有歌曲分数(可加缓存优化) item_embs = model.item_factors.weight scores = torch.matmul(fused_emb, item_embs.T).squeeze() # 过滤已播放歌曲(业务规则) played_mask = interaction_matrix[user_id].toarray().flatten() > 0 scores[played_mask] = -float('inf') # 返回 top-10 top_k = torch.topk(scores, 10) result = { "user_id": user_id, "recommendations": [ {"song_id": int(idx.item()), "score": float(score.item())} for idx, score in zip(top_k.indices, top_k.values) ] } return jsonify(result) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

提示:生产环境需添加gunicorn进程管理、redis缓存interaction_matrixnginx反向代理。debug=False必须设置,否则 Flask 自动重载会破坏模型状态。

5. 效果验证与 AB 测试:用 Recall@10 和业务指标定位问题

模型上线前必须验证其是否真能提升业务目标。不能只看 RMSE,而要设计多维度评估体系:技术指标(Recall@10)、业务指标(播放完成率)、公平性指标(长尾覆盖率)。

5.1 离线评估:Recall@10 与 Coverage 计算

Recall@10 衡量推荐列表覆盖用户真实喜好的能力;Coverage 衡量推荐曲库的多样性:

def evaluate_recall_at_k(model, test_loader, k=10): """计算 Recall@10""" model.eval() total_hit = 0 total_relevant = 0 with torch.no_grad(): for batch in test_loader: user_ids = batch['user_id'] true_items = batch['test_items'] # 用户未来播放的歌曲 # 获取推荐 scores = model.predict_batch(user_ids) # [batch, n_items] _, top_k_items = torch.topk(scores, k, dim=1) # 计算命中 for i in range(len(user_ids)): hit = len(set(top_k_items[i].tolist()) & set(true_items[i])) total_hit += hit total_relevant += len(true_items[i]) return total_hit / total_relevant def calculate_coverage(recommended_items, total_items): """计算推荐曲库覆盖率""" unique_recs = set(np.concatenate(recommended_items)) return len(unique_recs) / total_items # 示例调用 recall_10 = evaluate_recall_at_k(model, test_dataloader) coverage = calculate_coverage(all_recommendations, interaction_matrix.shape[1]) print(f"Recall@10: {recall_10:.4f}, Coverage: {coverage:.4f}")
指标健康阈值说明
Recall@10>0.15音乐场景因曲库极大,0.15 已属优秀;低于 0.08 需检查负采样或冷启动
Coverage>0.30避免马太效应,确保新歌/小众歌手有机会曝光
Diversity>0.70用 Item-Item Cosine Similarity 计算,值越高推荐越分散

5.2 在线 AB 测试:用播放完成率替代点击率

音乐推荐的核心指标不是点击,而是播放完成率(Completion Rate)——用户是否听完一首歌。AB 测试需分流并埋点:

# 前端 JS 埋点(示例) function trackPlayComplete(songId, duration, userId) { fetch('/api/track', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ event: 'play_complete', song_id: songId, duration: duration, user_id: userId, timestamp: Date.now() }) }); } # 后端统计(SQL) """ SELECT variant, COUNT(*) FILTER (WHERE event='play_complete') * 1.0 / COUNT(*) AS completion_rate FROM ab_events WHERE experiment='music_rec_v2' GROUP BY variant; """

关键点:AB 测试必须保证分流均匀(用user_id % 100),且实验周期 ≥7 天以消除周内波动;play_complete事件需定义为播放时长 ≥ 歌曲总时长的 80%,而非简单onended

5.3 冷启动专项优化:新用户首推策略

新用户无行为数据,SVD++ 无法工作。本项目采用画像引导的快速冷启动

  1. 用注册信息(年龄、地域)匹配最邻近的画像群组;
  2. 取该群组 Top-10 热门歌曲 + Top-5 长尾歌曲;
  3. 加入 1 首平台主推新歌(业务强干预)。
def cold_start_recommend(user_profile, cluster_mapping, hot_songs, longtail_songs, new_release): """新用户首推逻辑""" # 步骤1:根据 profile 查找最近群组 target_cluster = find_closest_cluster(user_profile, cluster_mapping) # 步骤2:取该群组热门+长尾 cluster_hot = hot_songs[target_cluster][:10] cluster_longtail = longtail_songs[target_cluster][:5] # 步骤3:强制插入新歌(业务规则) final_list = cluster_hot + cluster_longtail + [new_release] return final_list[:10] # 截断 # 使用示例 new_user = {'age_group': '19-25', 'province_tier': 'Tier1', 'gender': 'M'} recs = cold_start_recommend(new_user, cluster_mapping, hot_songs, longtail_songs, 12345)

冷启动效果验证:对比 AB 组中注册后 24 小时内的avg_play_duration,提升 ≥15% 即达标。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 16:29:39

AI建站工具选型指南:We0.ai、ChatGPT Sites、Lovable与Bolt怎么选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:29:15

用户画像驱动的协同过滤:冷启动友好型推荐系统实现

简介&#xff1a;本资源是一个面向人工智能初学者与项目实践者的音乐推荐系统完整工程&#xff0c;融合用户画像构建与基于用户的协同过滤算法&#xff0c;解决个性化音乐推荐中的冷启动与精度提升问题。项目基于KKBox公开竞赛数据集实现&#xff0c;采用Python3开发&#xff0…

作者头像 李华
网站建设 2026/9/14 16:28:43

基于OpenCV和dlib构建人脸识别考勤系统:从环境搭建到部署调优

简介&#xff1a;这套基于Python的员工人脸识别考勤系统&#xff0c;面向需要实现摄像头实时检测与身份验证的中高级Python开发者&#xff0c;结合OpenCV与Dlib完成人脸检测、特征点定位及模型训练&#xff0c;可应用于企业门禁、课堂签到等场景。压缩包共657个文件&#xff0c…

作者头像 李华