行业资讯
AI 音乐推荐的质量评估:协同过滤与内容理解的混合策略
AI 音乐推荐的质量评估协同过滤与内容理解的混合策略一、推荐系统一直推荐和你听过的类似的歌用户听了一个月的《卡农》变奏纯协同过滤推荐的信息茧房效应在音乐领域尤其致命。用户今天听了一首钢琴曲协同过滤推荐的全是钢琴曲——用户永远发现不了爵士、电子、世界音乐这些他可能感兴趣的相邻领域。协同过滤只看和你相似的人也喜欢什么无法理解音乐的内容特征——这首歌的节奏、和声、乐器配置、情感色彩。混合策略Hybrid可以打破这个僵局。协同过滤提供社会化推荐集体智慧的发现内容理解提供音乐学推荐歌曲固有特征的相似性。两者的结合让推荐既有流行度信号多数人喜欢的歌又有惊喜度信号你从没听过但旋律特征和你喜欢的歌接近的歌。二、底层机制与原理剖析混合推荐的三个层次层次一独立评分后加权融合。协同过滤给候选歌曲一个协同得分内容理解给一个特征相似得分最终的推荐得分 α × 协同得分 (1-α) × 内容得分。α 权重可调——偏重大众喜好用高 α偏重音乐学匹配用低 α。层次二特征增强型协同过滤。协同过滤中 item 之间的相似度不只看被多少人共同收听还看音乐特征有多相似。如果用户喜欢 A 歌推荐 B 不仅因为喜欢 A 的人也喜欢 B还因为 B 的 BPM、调性、乐器配置和 A 接近。这解决了冷启动问题——新歌没有被足够多人听过协同过滤无法计算相似度但内容特征可以。层次三多样性重排序MMR。无论哪种推荐算法给出的 Top 10可能前 3 首都非常相似。MMRMaximum Marginal Relevance做二次排序在保证与用户兴趣相关的前提下最大化推荐列表的多样性。如果前 3 首都是钢琴曲MMR 会往后压。三、生产级代码实现 音乐混合推荐引擎 协同过滤ALS 内容特征CLAP 嵌入 MMR 多样性重排序 import numpy as np from typing import List, Dict, Tuple, Optional from dataclasses import dataclass from sklearn.metrics.pairwise import cosine_similarity import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) dataclass class SongFeatures: 歌曲的内容特征 song_id: str # 基础特征 bpm: float # 节奏 key: int # 调性 (0C, 1C#, ..., 11B) mode: int # 0minor, 1major energy: float # 能量 (0-1) danceability: float # 舞曲性 (0-1) acousticness: float # 声学性 (0-1) instrumentalness: float # 器乐性 (0-1) # 深度特征CLAP embedding 128 维向量 embedding: Optional[np.ndarray] None # 标签特征 genres: List[str] None moods: List[str] None class HybridMusicRecommender: 混合音乐推荐器 三阶段推荐 1. 协同过滤召回基于 ALS 的 user-item 矩阵 2. 内容特征召回基于音频嵌入 元数据的相似度 3. MMR 多样性重排序 def __init__(self, cf_weight: float 0.6): cf_weight: 协同过滤的权重0-1 0.6 表示 60% 协同 40% 内容 self.cf_weight cf_weight # 协同过滤模型实际使用 implicit/ALS 库 self.user_factors: Optional[np.ndarray] None # (n_users, latent_dim) self.item_factors: Optional[np.ndarray] None # (n_items, latent_dim) self.item_id_to_idx: Dict[str, int] {} self.idx_to_item_id: Dict[int, str] {} # 内容特征存储 self.song_features: Dict[str, SongFeatures] {} def add_song_features(self, features: SongFeatures): 注册歌曲的内容特征 self.song_features[features.song_id] features def recommend(self, user_id: str, user_history: List[str], top_n: int 10, diversity_lambda: float 0.5) - List[Tuple[str, float]]: 为用户生成推荐列表 参数: user_id: 用户 ID user_history: 用户最近听过的歌曲 ID 列表 top_n: 返回的推荐数量 diversity_lambda: MMR 多样性参数0纯相关性, 1纯多样性 # 候选池排除用户已听过的歌 candidates set(self.song_features.keys()) - set(user_history) if not candidates: return [] # 1. 协同过滤得分 cf_scores self._compute_cf_scores(user_id, list(candidates)) # 2. 内容特征得分 ca_scores self._compute_content_scores(user_history, list(candidates)) # 3. 融合 fused_scores {} all_candidates set(cf_scores.keys()) | set(ca_scores.keys()) for song_id in all_candidates: cf cf_scores.get(song_id, 0.0) ca ca_scores.get(song_id, 0.0) # 归一化到 [0, 1] fused_scores[song_id] self.cf_weight * cf (1 - self.cf_weight) * ca # 按融合得分排序 ranked sorted(fused_scores.items(), keylambda x: x[1], reverseTrue) # 4. MMR 多样性重排序 diversified self._mmr_rerank( ranked, user_history, top_n, diversity_lambda ) return diversified def _compute_cf_scores(self, user_id: str, candidates: List[str]) - Dict[str, float]: 协同过滤得分 从 ALS 模型中取 user vector × item vectors if self.user_factors is None or self.item_factors is None: return {} user_idx self._get_user_idx(user_id) if user_idx is None: return {} user_vec self.user_factors[user_idx] # (latent_dim,) scores {} for song_id in candidates: item_idx self.item_id_to_idx.get(song_id) if item_idx is not None: item_vec self.item_factors[item_idx] scores[song_id] float(np.dot(user_vec, item_vec)) # 归一化 if scores: max_score max(scores.values()) if max_score 0: scores {k: v / max_score for k, v in scores.items()} return scores def _compute_content_scores(self, user_history: List[str], candidates: List[str]) - Dict[str, float]: 内容特征得分 基于用户历史歌曲的特征计算候选歌曲的相似度 if not user_history: return {} # 收集用户历史中所有歌曲的特征 history_features [] for song_id in user_history: if song_id in self.song_features: history_features.append(self.song_features[song_id]) if not history_features: return {} # 计算用户的平均特征向量多种特征的融合 user_avg_embedding None if any(f.embedding is not None for f in history_features): embeddings [f.embedding for f in history_features if f.embedding is not None] if embeddings: user_avg_embedding np.mean(embeddings, axis0) scores {} for song_id in candidates: if song_id not in self.song_features: continue candidate_f self.song_features[song_id] score_parts [] # 1. 深度嵌入相似度基于 CLAP embedding if user_avg_embedding is not None and candidate_f.embedding is not None: sim float(cosine_similarity( [user_avg_embedding], [candidate_f.embedding] )[0][0]) score_parts.append(0.5 * sim) # 50% 权重 # 2. 基础音频特征相似度 basic_sim self._compute_basic_feature_similarity( history_features, candidate_f ) score_parts.append(0.3 * basic_sim) # 30% 权重 # 3. 流派/情绪标签重叠度 tag_sim self._compute_tag_similarity(history_features, candidate_f) score_parts.append(0.2 * tag_sim) # 20% 权重 scores[song_id] sum(score_parts) return scores def _compute_basic_feature_similarity(self, history: List[SongFeatures], candidate: SongFeatures) - float: 计算基础音频特征的相似度 if not history: return 0.0 similarities [] for h in history: # BPM 相似度差值在 ±10 BPM 内视为接近 bpm_diff abs(h.bpm - candidate.bpm) bpm_sim max(0, 1 - bpm_diff / 40) # 40 BPM 为可接受差异 # 调性相似度同调性1, 关系调0.7, 远关系调0.3 key_dist min(abs(h.key - candidate.key), 12 - abs(h.key - candidate.key)) key_sim 1 - key_dist / 6 # Mode 相似度同 mode1不同0 mode_sim 1.0 if h.mode candidate.mode else 0.0 # 能量相似度 energy_sim 1 - abs(h.energy - candidate.energy) # 综合相似度 total_sim 0.15 * bpm_sim 0.25 * key_sim 0.1 * mode_sim 0.5 * energy_sim similarities.append(total_sim) return np.mean(similarities) if similarities else 0.0 def _compute_tag_similarity(self, history: List[SongFeatures], candidate: SongFeatures) - float: 标签重叠度 if not history: return 0.0 # 收集历史中所有流派和情绪标签 hist_genres set() hist_moods set() for h in history: if h.genres: hist_genres.update(h.genres) if h.moods: hist_moods.update(h.moods) cand_genres set(candidate.genres or []) cand_moods set(candidate.moods or []) # Jaccard 相似度 genre_sim 0.0 if hist_genres and cand_genres: genre_sim len(hist_genres cand_genres) / len(hist_genres | cand_genres) mood_sim 0.0 if hist_moods and cand_moods: mood_sim len(hist_moods cand_moods) / len(hist_moods | cand_moods) return 0.5 * genre_sim 0.5 * mood_sim def _mmr_rerank(self, ranked: List[Tuple[str, float]], user_history: List[str], top_n: int, lambda_param: float) - List[Tuple[str, float]]: MMR (Maximum Marginal Relevance) 多样性重排序 算法每次迭代选择相关性 × λ - 与已选集合的最大相似度 × (1-λ)最高的候选 if len(ranked) top_n: return ranked[:top_n] selected [] remaining list(ranked) while len(selected) top_n and remaining: mmr_scores [] for i, (song_id, relevance) in enumerate(remaining): if not selected: mmr_score lambda_param * relevance else: # 计算与已选集合的最大相似度 max_sim max( self._compute_song_similarity(song_id, sel_id) for sel_id, _ in selected ) mmr_score lambda_param * relevance - (1 - lambda_param) * max_sim mmr_scores.append((i, mmr_score)) # 选择 MMR 得分最高的候选 best_idx, _ max(mmr_scores, keylambda x: x[1]) selected.append(remaining.pop(best_idx)) return selected def _compute_song_similarity(self, song_a: str, song_b: str) - float: 两首歌的相似度基于内容特征 fa self.song_features.get(song_a) fb self.song_features.get(song_b) if not fa or not fb: return 0.0 if fa.embedding is not None and fb.embedding is not None: return float(cosine_similarity([fa.embedding], [fb.embedding])[0][0]) return self._compute_basic_feature_similarity([fa], fb) def _get_user_idx(self, user_id: str) - Optional[int]: 获取用户在矩阵中的索引需要与 ALS 训练时的映射一致 idx user_id if isinstance(idx, str): return self.item_id_to_idx.get(user_id) return idx if 0 idx len(self.user_factors) else None # --------------------------------------------------------------------------- # 使用示例 # --------------------------------------------------------------------------- if __name__ __main__: recommender HybridMusicRecommender(cf_weight0.6) # 注册歌曲特征 recommender.add_song_features(SongFeatures( song_idsong_1, bpm120, key0, mode1, energy0.7, danceability0.8, acousticness0.2, instrumentalness0.1, genres[pop, electronic], moods[energetic], )) recommendations recommender.recommend( user_iduser_001, user_history[song_1], top_n5, diversity_lambda0.4, ) for song_id, score in recommendations: print(f {song_id}: {score:.3f})四、边界分析与架构权衡协同过滤的冷启动新歌没有用户交互数据协同过滤无法推荐。内容特征可以填补——用音乐学特征做推荐新用户也没有历史数据——用热门 多样性的组合做首批推荐首次交互后快速切换到混合模型多样性 vs 点击率MMR 会增加多样性但可能降低点击率推荐了一些不太相关但多样的歌λ 参数的调节建议新用户 λ0.2偏重相关性先建立信任老用户 λ0.5平衡相关和新颖A/B 测试对比不同 λ 下的次日留存率而非当日点击率特征质量的鸿沟CLAP 嵌入的质量高度依赖训练数据。如果训练集以西方流行音乐为主对中国民乐的嵌入质量可能很差解决方案领域适配——用少量中国音乐数据 fine-tune CLAP 模型五、总结音乐推荐的混合策略用协同过滤获得社会信号用内容理解突破信息茧房。加权融合α0.6 协同 0.4 内容是起步方案MMR 重排序解决推荐列表的多样性问题。关键设计CF 负责大众喜欢什么内容特征负责音乐学上什么和这首歌相似。新歌用内容特征兜底冷启动新用户用热门多样作为入门推荐。
郑州网站建设
网页设计
企业官网