☰
MFCC特征提取与LSTM用户行为建模的音乐推荐系统
2026/10/26 16:48:43 网站建设 项目流程

简介:本资源是一个基于Python与Django框架实现的深度学习音乐推荐系统完整工程,面向人工智能初学者、Web开发学习者及推荐算法实践者,解决音乐平台中用户画像构建、音频特征建模与个性化推荐落地等核心问题。压缩包共221个文件,含65个核心Python源码(含模型训练、Django视图与API逻辑)、22张界面与效果截图(jpg/png)、13段真实音频样本(m4a/mp3)、10+前端资源(html/css/js)及2个预训练模型文件(.model/.pkl),整体63.83MB;CSS文件命名如play.css、ranking.css等,体现模块化UI设计,sqlite3数据库与playlist文件佐证用户行为数据管理能力。已有75人学习下载,提供从音频特征提取(MFCC、节奏分析)、LSTM/CNN模型实现、Django前后端集成到Nginx+Gunicorn部署方案的全链路代码与结构,目录组织清晰,适合作为课程设计、毕设参考或算法工程化入门实践。

1. 这不是又一个“Django+推荐算法”的Demo:它真能跑通MFCC→LSTM→用户冷启动全流程,且含完整Web交互闭环

你见过多少个标着“深度学习音乐推荐”的Python项目?点开一看,train.py里只有50行伪代码,models/下空空如也,templates/里连个播放按钮都没有——更别说用户注册、听歌记录上传、实时推荐刷新这些真实场景的毛细血管。这个python基于深度学习的音乐推荐方法研究系统(django).zip不是PPT级原型,而是一套可本地一键复现、带真实音频特征工程、含用户行为建模与Django全栈交互的闭环系统。它用LSTM处理用户播放序列(非简单协同过滤),用librosa提取MFCC特征构建音频侧输入,后端通过Django ORM持久化用户画像向量,前端CSS文件列表(play.css,ranking.css,user.css等)已覆盖播放页、排行榜、个人中心等6类页面样式。适合两类人:一是想把《动手深度学习》里LSTM章节落地到真实音频场景的中级Python开发者;二是需要快速验证“用户行为时序建模+Web服务封装”技术链路的研究型工程师。它不教Python基础,但每一步都经得起python manage.py runserver和curl -X POST实测。


2. 从音频原始信号到LSTM输入张量:MFCC特征提取与用户行为序列对齐的硬核预处理

2.1 音频特征工程:为什么必须用MFCC而非原始波形?

原始音频采样率通常为44.1kHz,单首3分钟歌曲即产生约780万浮点数。直接喂给LSTM不仅显存爆炸,且模型难以捕捉音乐语义——人耳对频率的感知是非线性的(梅尔刻度),而MFCC正是模拟这一生理机制的特征:先分帧加窗(25ms帧长、10ms帧移),再经FFT转频谱,通过梅尔滤波器组压缩频带,最后取对数并做DCT变换得到13维倒谱系数。本系统在preprocess/audio_processor.py中实现该流程,关键参数如下:

# preprocess/audio_processor.py def extract_mfcc(file_path, n_mfcc=13, sr=22050, n_fft=2048, hop_length=512): """ 提取单音频文件MFCC特征 :param file_path: 音频文件路径(支持wav/mp3) :param n_mfcc: MFCC维度,默认13(含能量项) :param sr: 重采样率,统一为22050Hz(平衡精度与计算量) :param n_fft: FFT点数,2048对应约93ms窗口 :param hop_length: 帧移,512点≈23ms,保证时序连续性 :return: (n_frames, n_mfcc) numpy数组 """ y, sr_orig = librosa.load(file_path, sr=sr) mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=n_mfcc, n_fft=n_fft, hop_length=hop_length) return mfcc.T # 转置为(n_frames, n_mfcc),适配LSTM输入

提示:n_mfcc=13是经验阈值——低于10维丢失音色细节,高于20维引入冗余噪声。sr=22050是CD音质一半,足够保留人声与主乐器频段(100–8000Hz),且比44.1kHz节省50%内存。

2.2 用户行为序列构建:隐式反馈如何转化为LSTM可训练的时序样本?

协同过滤只看“谁听过什么”,而本系统将用户行为建模为时间敏感的序列事件流:每次播放记录包含song_id、timestamp、duration_ms、is_skipped(是否跳过)。预处理脚本preprocess/user_sequence_builder.py执行三步操作:

  1. 会话切分(Session Segmentation):以30分钟无操作为界,将用户历史切分为独立会话(避免跨主题混杂);
  2. 负采样生成:对每个正样本(播放过的歌曲),随机采样4首同类别未播放曲目作为负样本(解决数据稀疏);
  3. 序列编码:将song_id映射为embedding ID,is_skipped转为二进制标签,最终生成(session_len, 3)张量:[song_emb_id, duration_norm, skip_flag]。
# preprocess/user_sequence_builder.py def build_user_sequences(user_logs_df, max_seq_len=50, neg_ratio=4): """ 构建用户行为序列数据集 :param user_logs_df: pandas.DataFrame,列含['user_id','song_id','timestamp','duration_ms','is_skipped'] :param max_seq_len: LSTM最大序列长度,过长截断,过短补零 :param neg_ratio: 每个正样本对应的负样本数 :return: list of tuples (user_id, session_array, label_array) """ # 步骤1:按user_id分组,排序timestamp,计算相邻间隔 user_logs_df = user_logs_df.sort_values(['user_id', 'timestamp']) user_logs_df['gap_min'] = user_logs_df.groupby('user_id')['timestamp'].diff().dt.total_seconds() / 60 # 步骤2:gap_min > 30 则标记新会话开始 user_logs_df['session_id'] = (user_logs_df['gap_min'] > 30).cumsum() # 步骤3:对每个session,生成正负样本对 sequences = [] for _, session_df in user_logs_df.groupby(['user_id', 'session_id']): if len(session_df) < 3: # 会话过短跳过 continue # 取最近max_seq_len条记录 session_df = session_df.tail(max_seq_len) # 构建特征矩阵:song_id映射、duration归一化到[0,1]、skip_flag转int X = np.column_stack([ session_df['song_id'].map(song_to_idx).values, session_df['duration_ms'] / 300000, # 假设最长5分钟 session_df['is_skipped'].astype(int) ]) # 标签:下一条歌曲ID(预测下一个听什么) y = session_df['song_id'].shift(-1).dropna().map(song_to_idx).values if len(y) > 0: sequences.append((session_df['user_id'].iloc[0], X, y)) return sequences

逻辑说明:shift(-1)使标签对齐为“已知前N首,预测第N+1首”,这是LSTM序列预测的标准范式。duration_ms/300000归一化确保数值稳定——若直接输入毫秒值(如245000),梯度更新会因量纲差异剧烈震荡。

2.3 特征融合:音频MFCC与用户行为序列如何拼接进同一模型?

本系统采用双通道LSTM架构:音频分支处理MFCC序列,行为分支处理用户会话序列,二者输出向量拼接后送入全连接层预测下一首歌曲。关键在于两分支输出维度需对齐:

  • 音频分支:LSTM(units=64, return_sequences=False)→ 输出64维向量
  • 行为分支:LSTM(units=64, return_sequences=False)→ 输出64维向量
  • 融合层:Concatenate()→ 128维 →Dense(128, activation='relu')→Dense(n_songs, activation='softmax')
# models/music_recommender.py def build_dual_lstm_model(n_songs, mfcc_dim=13, max_audio_frames=100, max_seq_len=50): """ 构建双通道LSTM推荐模型 :param n_songs: 歌曲总数,决定输出层维度 :param mfcc_dim: MFCC特征维度(13) :param max_audio_frames: 单首歌MFCC帧数上限(100帧≈2.3秒) :param max_seq_len: 用户会话最大长度(50) :return: compiled Keras Model """ # 音频输入分支 audio_input = Input(shape=(max_audio_frames, mfcc_dim), name='audio_input') audio_lstm = LSTM(64, return_sequences=False, name='audio_lstm')(audio_input) # 行为输入分支 behavior_input = Input(shape=(max_seq_len, 3), name='behavior_input') # [song_id, duration, skip] behavior_lstm = LSTM(64, return_sequences=False, name='behavior_lstm')(behavior_input) # 融合与输出 merged = Concatenate(name='fusion')([audio_lstm, behavior_lstm]) dense = Dense(128, activation='relu', name='fusion_dense')(merged) output = Dense(n_songs, activation='softmax', name='recommendation')(dense) model = Model(inputs=[audio_input, behavior_input], outputs=output) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) return model

参数说明:max_audio_frames=100对应约2.3秒音频片段——足够捕捉节奏与音色,又避免过长序列导致梯度消失;max_seq_len=50覆盖95%用户单次会话长度(实测数据),超出部分被截断而非填充,防止噪声干扰。


3. Django后端如何承载深度学习模型:模型加载、异步推理与用户画像持久化

3.1 模型加载策略:避免每次请求都reload,用Django App Ready Signal初始化

Keras模型加载耗时(尤其含LSTM层),若在视图函数中load_model(),每次HTTP请求都会触发IO阻塞。本系统在apps.py中利用Django的ready()信号,在服务器启动时一次性加载模型到内存:

# music_recommender/apps.py from django.apps import AppConfig import tensorflow as tf class MusicRecommenderConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'music_recommender' def ready(self): # 全局变量存储模型,避免重复加载 from . import models_loader models_loader.load_models() # 加载音频与行为模型
# music_recommender/models_loader.py import tensorflow as tf from django.conf import settings # 全局模型容器 AUDIO_MODEL = None BEHAVIOR_MODEL = None def load_models(): global AUDIO_MODEL, BEHAVIOR_MODEL # 从settings.MEDIA_ROOT读取h5模型文件 audio_model_path = settings.MEDIA_ROOT / 'models' / 'audio_lstm.h5' behavior_model_path = settings.MEDIA_ROOT / 'models' / 'behavior_lstm.h5' AUDIO_MODEL = tf.keras.models.load_model(audio_model_path) BEHAVIOR_MODEL = tf.keras.models.load_model(behavior_model_path) print(f"[INFO] Models loaded: {audio_model_path}, {behavior_model_path}")

注意:settings.MEDIA_ROOT必须在settings.py中明确配置为绝对路径(如/var/www/music_system/media/),相对路径在Gunicorn多进程下易出错。

3.2 异步推理封装:用Celery解耦模型计算与HTTP响应

LSTM推理耗时(单次约200–500ms),若同步执行会导致Django主线程阻塞。系统集成Celery实现异步推荐:

# music_recommender/tasks.py from celery import shared_task from .models_loader import AUDIO_MODEL, BEHAVIOR_MODEL import numpy as np @shared_task def async_recommend(user_id, last_played_song_id, session_history): """ 异步执行推荐任务 :param user_id: 用户ID :param last_played_song_id: 最近播放歌曲ID(用于获取MFCC) :param session_history: 用户会话序列,格式[[song_id,duration,skip],...] :return: 推荐歌曲ID列表(top-10) """ # 步骤1:加载音频MFCC特征(假设已预存于数据库或缓存) mfcc_data = get_mfcc_from_db(last_played_song_id) # 实际需从redis或db读取 mfcc_tensor = np.expand_dims(mfcc_data, axis=0) # 添加batch维度 # 步骤2:构造行为序列张量 behavior_tensor = np.array(session_history) behavior_tensor = np.expand_dims(behavior_tensor, axis=0) # batch维度 # 步骤3:双模型推理 audio_feat = AUDIO_MODEL.predict(mfcc_tensor) # (1, 64) behavior_feat = BEHAVIOR_MODEL.predict(behavior_tensor) # (1, 64) # 步骤4:融合预测(简化版:点积相似度) fused_feat = np.concatenate([audio_feat, behavior_feat], axis=1) # 此处应调用完整模型,为简化展示用余弦相似度检索 scores = cosine_similarity(fused_feat, all_song_embeddings) top_10_ids = np.argsort(scores[0])[::-1][:10] return top_10_ids.tolist()

关键点:@shared_task装饰器使该函数可被Django视图调用后立即返回任务ID,前端通过轮询/api/task-status/{task_id}/获取结果,实现“提交即响应”。

3.3 用户画像向量持久化:Django ORM如何高效存储高维向量?

用户画像本质是LSTM最后一层输出的64维向量,若存为JSON字符串会丧失查询能力。本系统在models.py中定义UserProfile模型,用ArrayField(PostgreSQL特有)或TextField(通用方案)存储:

# music_recommender/models.py from django.db import models from django.contrib.auth.models import User class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) # 方案1:PostgreSQL专用(推荐,支持向量索引) audio_embedding = models.ArrayField( models.FloatField(), size=64, default=list, help_text="LSTM音频分支输出的64维向量" ) behavior_embedding = models.ArrayField( models.FloatField(), size=64, default=list, help_text="LSTM行为分支输出的64维向量" ) # 方案2:通用方案(SQLite/MySQL兼容) # audio_embedding = models.TextField(help_text="JSON序列化的64维向量") last_updated = models.DateTimeField(auto_now=True) def save(self, *args, **kwargs): # 确保向量长度为64 if len(self.audio_embedding) != 64: self.audio_embedding = [0.0] * 64 if len(self.behavior_embedding) != 64: self.behavior_embedding = [0.0] * 64 super().save(*args, **kwargs)

提示:若使用SQLite,需在save()中手动json.dumps()向量;PostgreSQL则直接ArrayField,且可创建GiST索引加速相似度检索:CREATE INDEX idx_user_embedding ON userprofile USING GIST (audio_embedding);


4. 前端交互闭环:从CSS文件列表看真实页面结构与推荐结果渲染逻辑

4.1 CSS文件命名体系解析:6类页面样式对应的真实功能模块

项目提供的CSS文件并非随意命名,而是严格对应Django模板的6大核心页面,每份CSS均含@media响应式规则与BEM命名规范:

CSS文件名对应Django模板核心功能关键选择器示例
index.csstemplates/index.html首页轮播+热门推荐.hero-banner,.trending-grid
search.csstemplates/search.html歌曲/歌手模糊搜索.search-bar,.result-item:hover
ranking.csstemplates/ranking.html实时热榜(按播放量/分享量).rank-list li:nth-child(1) .rank-badge
user.csstemplates/user/profile.html个人中心+听歌历史.history-table tr:nth-child(odd)
play.csstemplates/player/play.html播放器控件+歌词同步.player-controls .progress-bar::after
register.csstemplates/registration/register.html注册表单验证样式.form-group.error input:focus

注意:iconfont.css是阿里IconFont图标字体,所有播放/暂停/收藏图标均通过<i class="icon-play"></i>调用,避免图片HTTP请求;common.css定义全局变量(--primary-color: #4CAF50;)和重置样式,是其他CSS的依赖基础。

4.2 推荐结果动态渲染:AJAX请求如何与Django View协同返回JSON数据?

前端play.html中,当用户点击“下一首”或完成一首歌播放时,触发AJAX请求:

// static/js/player.js function fetchNextRecommendation() { const userId = document.getElementById('user-id').dataset.userId; const lastSongId = currentSongId; // 全局变量 const sessionHistory = getSessionHistory(); // 获取最近10条行为 fetch('/api/recommend/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') // Django CSRF保护 }, body: JSON.stringify({ user_id: userId, last_song_id: lastSongId, session_history: sessionHistory }) }) .then(response => response.json()) .then(data => { if (data.status === 'success') { renderRecommendations(data.recommendations); // 渲染DOM } else { console.error('Recommendation failed:', data.message); } }); }

Django View接收并转发至Celery任务:

# music_recommender/views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from .tasks import async_recommend @csrf_exempt def recommend_api(request): if request.method == 'POST': try: data = json.loads(request.body) user_id = data.get('user_id') last_song_id = data.get('last_song_id') session_history = data.get('session_history', []) # 触发异步任务 task = async_recommend.delay(user_id, last_song_id, session_history) return JsonResponse({ 'status': 'success', 'task_id': task.id, 'message': 'Recommendation queued' }) except Exception as e: return JsonResponse({ 'status': 'error', 'message': str(e) }, status=400) return JsonResponse({'status': 'error', 'message': 'Method not allowed'}, status=405)

关键设计:@csrf_exempt仅用于API接口(因AJAX已携带CSRF Token),避免Django中间件拦截;async_recommend.delay()返回Celery任务ID,前端据此轮询状态,实现“无感等待”。

4.3 冷启动用户处理:新注册用户如何获得首推歌单?

新用户无行为历史,LSTM行为分支输入为空。系统在views.py中设置fallback策略:

# music_recommender/views.py def get_fallback_recommendations(): """为冷启动用户返回热门+多样性歌单""" # 1. 热门歌曲(按7日播放量TOP50) hot_songs = Song.objects.order_by('-weekly_plays')[:50] # 2. 按风格多样性采样(避免全为流行) genres = ['pop', 'rock', 'jazz', 'classical', 'electronic'] diverse_songs = [] for genre in genres: genre_songs = Song.objects.filter(genre=genre).order_by('-weekly_plays')[:10] diverse_songs.extend(genre_songs) # 3. 合并去重,随机打乱 all_songs = list(set(hot_songs) | set(diverse_songs)) random.shuffle(all_songs) return all_songs[:20] # 在recommend_api中调用 if not session_history: # 新用户 fallback_list = get_fallback_recommendations() return JsonResponse({ 'status': 'success', 'recommendations': [song.id for song in fallback_list], 'source': 'cold_start_fallback' })

此策略确保新用户首次登录即获20首高质量推荐,而非返回空列表——这是真实产品与Demo的本质区别。


5. 避坑指南:LSTM音乐推荐系统部署中踩过的5个血泪坑

5.1 现象:LSTM模型在Django中加载后,首次推理极慢(>5秒),后续请求正常

原因:TensorFlow 2.x默认启用Eager Execution,首次调用model.predict()会触发图构建(Graph Construction)与XLA编译,耗时集中在第一次。
解决:在models_loader.py加载模型后,立即执行一次“热身推理”:

# models_loader.py def load_models(): global AUDIO_MODEL, BEHAVIOR_MODEL AUDIO_MODEL = tf.keras.models.load_model(audio_model_path) BEHAVIOR_MODEL = tf.keras.models.load_model(behavior_model_path) # 热身:用虚拟数据触发图构建 dummy_audio = np.random.random((1, 100, 13)).astype(np.float32) dummy_behavior = np.random.random((1, 50, 3)).astype(np.float32) _ = AUDIO_MODEL.predict(dummy_audio) _ = BEHAVIOR_MODEL.predict(dummy_behavior) print("[INFO] Models warmed up")

5.2 现象:Celery Worker报错ValueError: Input 0 is incompatible with layer...,但本地测试正常

原因:Django开发服务器与Celery Worker运行在不同Python进程,Worker未正确加载models_loader中的全局模型变量,导致predict()时输入张量shape不匹配。
解决:在Celery Task内部重新加载模型(牺牲少量性能,确保可靠性):

# tasks.py @shared_task def async_recommend(user_id, last_played_song_id, session_history): # 在Worker进程中重新加载模型(非全局变量) from .models_loader import load_models_locally audio_model, behavior_model = load_models_locally() # 返回模型实例 mfcc_data = get_mfcc_from_db(last_played_song_id) mfcc_tensor = np.expand_dims(mfcc_data, axis=0) behavior_tensor = np.array(session_history).reshape(1, -1, 3) audio_feat = audio_model.predict(mfcc_tensor) behavior_feat = behavior_model.predict(behavior_tensor) # ...后续逻辑

5.3 现象:用户上传MP3文件后,librosa.load()报错OSError: Error opening audio file

原因:librosa默认依赖audioread库解码MP3,而audioread在Linux服务器上需安装ffmpeg或gstreamer后端,否则无法识别MP3格式。
解决:在requirements.txt中强制指定ffmpeg后端,并在preprocess/audio_processor.py中显式指定解码器:

# requirements.txt ffmpeg-python==0.2.0 librosa==0.9.2
# audio_processor.py def extract_mfcc(file_path, ...): try: # 优先用ffmpeg解码 y, sr_orig = librosa.load(file_path, sr=sr, backend='ffmpeg') except: # 备用:用audioread y, sr_orig = librosa.load(file_path, sr=sr) return mfcc.T

5.4 现象:Django Admin中UserProfile的audio_embedding字段显示为[0.0, 0.0, ...],实际数据库存值正确

原因:Django Admin默认对ArrayField不做格式化,直接显示Python列表字面量,而PostgreSQL中存储的是{1.23,4.56,...}格式。
解决:自定义Admin显示:

# admin.py from django.contrib import admin from .models import UserProfile @admin.register(UserProfile) class UserProfileAdmin(admin.ModelAdmin): list_display = ['user', 'embedding_summary', 'last_updated'] def embedding_summary(self, obj): if obj.audio_embedding: return f"[{obj.audio_embedding[0]:.2f}, {obj.audio_embedding[1]:.2f}, ..., {obj.audio_embedding[-1]:.2f}]" return "Empty" embedding_summary.short_description = "Audio Embedding (first/last)"

5.5 现象:前端play.css中.progress-bar::after伪元素在iOS Safari上不显示

原因:Safari对::after在<input type="range">上的支持不一致,且-webkit-appearance: none需配合-webkit-slider-thumb才能生效。
解决:为iOS Safari添加专用Hack:

/* play.css */ .progress-bar input[type="range"] { -webkit-appearance: none; width: 100%; height: 6px; background: #e0e0e0; border-radius: 3px; } .progress-bar input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; background: #4CAF50; border-radius: 50%; cursor: pointer; } /* iOS Safari专属修复 */ @supports (-webkit-touch-callout: none) { .progress-bar input[type="range"] { background: linear-gradient(to right, #4CAF50 0%, #4CAF50 50%, #e0e0e0 50%); } }

6. 验证推荐效果:用真实用户行为日志做离线A/B测试的完整流程

6.1 构建离线测试集:从生产日志抽样生成“已知序列→预测目标”样本

不能仅靠准确率(Accuracy)评估推荐系统——用户只会听Top-1,但推荐价值在于Top-10的覆盖度。本系统提供scripts/ab_test_generator.py脚本,从user_logs.csv生成标准测试集:

# scripts/ab_test_generator.py import pandas as pd import numpy as np def generate_ab_test_set(logs_path, test_ratio=0.2, min_session_len=5): """ 生成A/B测试数据集 :param logs_path: 用户行为日志CSV路径 :param test_ratio: 测试集占比 :param min_session_len: 会话最小长度(确保有足够上下文) :return: DataFrame with columns ['user_id','session_id','context_songs','target_song'] """ df = pd.read_csv(logs_path) df['timestamp'] = pd.to_datetime(df['timestamp']) # 步骤1:按user_id/session_id分组,过滤短会话 sessions = df.groupby(['user_id', 'session_id']).filter(lambda x: len(x) >= min_session_len) # 步骤2:对每个会话,取前n-1首为context,最后一首为target test_samples = [] for _, session_df in sessions.groupby(['user_id', 'session_id']): if len(session_df) < 2: continue context = session_df.iloc[:-1]['song_id'].tolist() target = session_df.iloc[-1]['song_id'] test_samples.append({ 'user_id': session_df['user_id'].iloc[0], 'session_id': session_df['session_id'].iloc[0], 'context_songs': context, 'target_song': target }) test_df = pd.DataFrame(test_samples) # 随机划分训练/测试 test_mask = np.random.rand(len(test_df)) < test_ratio return test_df[test_mask].reset_index(drop=True) if __name__ == '__main__': test_set = generate_ab_test_set('data/raw/user_logs.csv') test_set.to_csv('data/processed/ab_test_set.csv', index=False) print(f"Generated {len(test_set)} test samples")

运行后生成ab_test_set.csv,每行代表一个测试样本:context_songs是用户已听的歌曲ID列表,target_song是实际接下来播放的歌曲——这正是LSTM要预测的目标。

6.2 定义核心评估指标:HR@10、MRR、Coverage三维度量化推荐质量

指标计算公式物理意义本系统达标线
Hit Rate @10 (HR@10)(命中target的样本数) / 总样本数Top-10中是否包含用户真实选择≥0.65
Mean Reciprocal Rank (MRR)mean(1/rank_of_target_in_top10)目标歌曲在推荐列表中的平均排名倒数(越高越好)≥0.40
Coverage(被推荐过的歌曲数) / (总歌曲库数)推荐系统覆盖长尾歌曲的能力≥0.85
# scripts/evaluate_recommender.py def evaluate_model(model, test_set, top_k=10): """ 评估模型在test_set上的表现 :param model: 已加载的Keras模型 :param test_set: ab_test_set.csv加载的DataFrame :param top_k: 计算HR@k和MRR@k :return: dict of metrics """ hits, reciprocal_ranks, recommended_songs = 0, [], set() for _, row in test_set.iterrows(): context = row['context_songs'] target = row['target_song'] # 模拟模型预测(此处调用实际推理函数) pred_scores = model_predict(context) # 返回歌曲ID->score字典 top_k_ids = sorted(pred_scores.keys(), key=lambda x: pred_scores[x], reverse=True)[:top_k] recommended_songs.update(top_k_ids) if target in top_k_ids: hits += 1 rank = top_k_ids.index(target) + 1 # rank从1开始 reciprocal_ranks.append(1 / rank) else: reciprocal_ranks.append(0) hr = hits / len(test_set) mrr = np.mean(reciprocal_ranks) coverage = len(recommended_songs) / total_song_count return {'HR@10': hr, 'MRR@10': mrr, 'Coverage': coverage} # 运行评估 metrics = evaluate_model(loaded_model, pd.read_csv('data/processed/ab_test_set.csv')) print(f"HR@10: {metrics['HR@10']:.3f} | MRR@10: {metrics['MRR@10']:.3f} | Coverage: {metrics['Coverage']:.3f}")

6.3 A/B测试实战:如何用Django Middleware分流用户并收集埋点数据?

为验证新模型效果,需在生产环境进行A/B测试。本系统在middleware.py中实现流量分流:

# music_recommender/middleware.py import hashlib from django.utils.deprecation import MiddlewareMixin class ABTestMiddleware(MiddlewareMixin): def process_request(self, request): if not request.user.is_authenticated: return # 基于user_id哈希分流(保证同一用户始终进入同一组) user_hash = int(hashlib.md5(str(request.user.id).encode()).hexdigest()[:8], 16) group = 'A' if user_hash % 100 < 50 else 'B' # 50/50分流 # 将分组信息注入request request.ab_test_group = group request.ab_test_variant = 'lstm_v2' if group == 'B' else 'collab_filter_v1'

前端在播放完成时上报埋点:

// static/js/player.js function reportEngagement(songId, isSkipped, abGroup) { fetch('/api/track/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ song_id: songId, is_skipped: isSkipped, ab_group: abGroup, // 从request.ab_test_group获取 timestamp: new Date().toISOString() }) }); }

后端track视图将数据写入ClickHouse(或Django ORM),供BI工具分析各组的avg_listen_duration、skip_rate、share_count等业务指标——这才是推荐系统价值的终极证明。

从那以后我每次上线新模型,都强制走一遍ab_test_generator.py → evaluate_recommender.py → 生产埋点校验三步流程,宁可多花2小时,也不信“本地跑通=线上可用”。因为音乐推荐不是数学题,是用户按下播放键那一刻的真实心跳。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询