
简介基于用户画像与协同过滤算法的音乐推荐系统是一套以Python语言实现的毕业设计项目源码主要面向计算机、通信、人工智能、自动化等专业的学生与从业者可用于课程设计、大作业或毕业设计参考。项目采用Django框架搭建融合用户画像构建与协同过滤推荐逻辑涵盖后台推荐模型、前端播放页面、数据库及部署配置等模块能够帮助学习者完整理解推荐系统的工程落地过程。压缩包共包含76个文件以Python源码30个py为核心辅以HTML/CSS/JavaScript页面文件、字体图标、音频样例、SQLite数据库及Docker部署配置等整体大小12.84MB目录结构清晰便于按需检索与二次开发。代码已经过调试测试运行有保障作者在毕业答辩中获得98分高分。目前已有155人学习下载适合希望从零搭建音乐推荐系统或深入理解用户画像、协同过滤算法实践的人群。1. 先说人话这个毕设题到底要你做什么每年毕业季都能看到不少拿音乐推荐系统当课题的同学题目里同时挂着协同过滤算法和用户画像两块招牌。其实这个组合在工业界并不是同一个模块——用户画像负责“认识用户”协同过滤负责“找到用户还没听过但会喜欢的歌”两者拼起来才是一个完整的推荐闭环。但很多毕设只做协同过滤把画像当 PPT 里的一张架构图答辩时一被追问底层数据从哪来、怎么做特征就直接翻车。这个项目的本质是用 Python 把这两块都落了地跑通一个看得见、能演示、敢被问的推荐系统。这篇笔记不绕弯子先拆透用户画像和协同过滤算法单独实现时各自的关键点再给出一份可以直接抄的代码骨架把数据集、相似度计算、评估指标和常见的毕设答辩追问点一并讲清楚。适合正在写推荐系统方向毕业论文、又不想只停在概念层面的同学也适合想快速把协同过滤跑起来看效果的工程师。2. 把用户画像做成可计算的东西特征设计是地基2.1 画像不是“画个人像”是给用户算一组可比较的向量很多同学一听到用户画像就去设计性别、年龄、职业这类人口统计学字段这是在给自己挖坑。你手上的数据如果是爬来的音乐行为日志根本没有这些字段就算有对推荐结果的影响也很弱。音乐场景里的用户画像真正起作用的是行为统计特征和兴趣分布特征。我从日志里归一类典型的输入格式给你看这是毕设项目里常见的数据源头字段示例说明user_idU10086用户唯一标识song_idS2048歌曲唯一标识play_count23播放次数like1 / 0是否主动收藏或点赞skip0 / 1是否跳过timestamp1621234567行为发生时间基于这种日志构建画像分三层走。第一层是聚合统计总播放量、平均播放时长、下载/收藏比例、活跃天数。第二层是偏好分布用户播放过的歌曲中歌手、语种、风格各占多少比例——这一步要结合歌曲元数据表去 join。第三层是时间偏好周末活跃还是工作日活跃晚上偏爱哪种节奏的歌曲。这一个特征向量不是给 SQL 工整地算一张表而是直接输出一个 Python dict方便后续抽进协同过滤做加权和召回。下面我直接给出画像构建代码输入行为日志输出每个用户的画像向量。import pandas as pd import numpy as np from collections import defaultdict def build_user_profile(log_df, song_meta_df): log_df: 用户行为日志含 user_id, song_id, play_count, like, skip, timestamp song_meta_df: 歌曲元数据含 song_id, artist, genre, language, bpm 返回: dict, keyuser_id, value画像特征 dict # 合并行为与歌曲元数据 df log_df.merge(song_meta_df, onsong_id, howleft) profiles {} for user, group in df.groupby(user_id): total_play group[play_count].sum() if total_play 0: continue profile {} # 基础统计特征 profile[total_play] total_play profile[unique_songs] group[song_id].nunique() profile[like_ratio] group[like].mean() profile[skip_ratio] group[skip].mean() # 偏好分布特征歌手 artist_play group.groupby(artist)[play_count].sum() profile[top_artist] artist_play.idxmax() if not artist_play.empty else None profile[artist_diversity] (artist_play 0).sum() / max(len(artist_play), 1) # 偏好分布特征语种 lang_play group.groupby(language)[play_count].sum() total_lang lang_play.sum() profile[lang_zh_ratio] lang_play.get(中文, 0) / total_lang if total_lang 0 else 0 profile[lang_en_ratio] lang_play.get(英文, 0) / total_lang if total_lang 0 else 0 profile[lang_jp_ratio] lang_play.get(日语, 0) / total_lang if total_lang 0 else 0 # 时间偏好 hour pd.to_datetime(group[timestamp], units).dt.hour profile[night_active] ((hour 22) | (hour 5)).mean() profiles[user] profile return profiles上面代码有个关键点所有画像特征都来自统计而非主观打分。top_artist 用播放量求和取最大artist_diversity 用歌手指数量除以歌手出现组数night_active 用行为时间戳落在 22 点到次日 5 点的比例。为什么要这样如果是你自己拍脑袋给用户打“喜欢摇滚 0.8”这种分整个画像就变成了不可复现的黑匣子答辩时导师问一句“这个权重怎么定的”就直接卡住。统计特征每一步都能解释权重也可以说是频率占比底层逻辑硬得多。2.2 画像怎么协同过滤结合做加权不做拼接有的毕设方案把画像特征往用户 ID 后面一拼直接当协同过滤的输入矩阵。这个做法不算错但意义很有限——协同过滤算的是“用户-物品”共现关系画像特征是用户侧属性两者放在同一维度时相似度计算的语义会被冲淡。我一般推荐的做法是先算画像相似度再作为 UserCF 的权重项或辅助召回来源后面第 4 章给出真正的代码。画像在这里还有另一个实际用途——解决音乐推荐的新用户冷启动。新注册用户没行为数据协同过滤直接算不出来。很多毕设无视这个问题但答辩时老师一定会问。做法是先收集用户在注册页选的歌手/语种偏好然后在新用户跟老用户之间用画像特征做最近邻匹配找到“画像最像”的一批老用户用他们的历史播放列表作为候选推荐池。这一块能不能落地直接决定了项目有没有超出“只会调包”的加分项。2.3 画像特征要处理的两个细节窗口第一个是类别特征不能直接当数值用。artist 字段是字符串lang 字段是离散类别把它们变成比率特征后才是连续数值。建议在进入相似度计算前做一次归一化否则 total_play 动辄上千like_ratio 只在 0 到 1 之间盘桓余弦相似度基本被前者带着走。常见的做法是 min-max 归一化。第二个是时间衰减。用户半年前狂听周杰伦最近两周只听陈奕迅画像里 top_artist 如果还是周杰伦推荐结果就会偏旧。我一般在统计播放量时按时间做衰减代码实现并不复杂import time def time_decay_weight(timestamp, half_life_days30): 半衰期 30 天越新的行为权重越高 age_days (time.time() - timestamp) / 86400.0 return 0.5 ** (age_days / half_life_days)时间衰减这个细节放在毕设里很加分。因为它是最容易“讲出工业感”的一笔——哪怕是 5 分钟的口头展示也能说清楚“为什么这个推荐结果没有停留在用户的三个月前”。参数 half_life_days 是调参入口答辩时能自然引出“不同场景该设多大”的讨论。3. 协同过滤算法选哪头UserCF 还是 ItemCF先别急着抄代码3.1 这两个算法在音乐场景里的天然差异协同过滤算法听上去是一个词实际分支开后是两条路UserCF 找“和我像的人”把他们听的歌推给我ItemCF 找“和我听过的歌像的歌”把它们推给我。两者在音乐推荐里的表现差异非常大。音乐消费有一个特性——用户的兴趣相对稳定但曲库更新极快。新歌上架没有播放记录ItemCF 算不出相似物品UserCF 却能通过用户行为把新歌推给老用户的朋友。所以很多音乐平台的核心链路是 UserCF 兜底 ItemCF 精细排序。毕业论文里把这个差异讲明白比把两个算法都背一遍有用得多。但 UserCF 也有一个硬伤用户量大的时候每算一次推荐都要实时找出最相似的 K 个用户计算量是用户数的平方级。所以工业界做 UserCF 一般是离线把相似用户矩阵算好线上只查表。毕设规模不需要考虑性能但代码结构最好一开始就写成“离线预计算 在线查表”的形式至少逻辑上成立。3.2 UserCF 最小可运行实现从无到有完整代码直接上一个能跑的最小实现。输入是用户行为日志输出是给指定用户返回的 Top-N 歌曲列表。import numpy as np import pandas as pd from scipy.sparse import csr_matrix from sklearn.metrics.pairwise import cosine_similarity def build_user_item_matrix(log_df): 构建用户-物品稀疏矩阵 users sorted(log_df[user_id].unique()) items sorted(log_df[song_id].unique()) user_idx {u: i for i, u in enumerate(users)} item_idx {s: i for i, s in enumerate(items)} rows log_df[user_id].map(user_idx).values cols log_df[song_id].map(item_idx).values data log_df[play_count].values.astype(float) matrix csr_matrix((data, (rows, cols)), shape(len(users), len(items))) return matrix, users, items, user_idx, item_idx def user_cf_recommend(log_df, target_user, top_k_users10, top_n_songs20, alpha0.6): target_user: 目标用户 ID top_k_users: 取最相似的 K 个用户 alpha: 惩罚热门物品的系数越大越打压热门 matrix, users, items, user_idx, item_idx build_user_item_matrix(log_df) # 计算用户相似度矩阵 sim_matrix cosine_similarity(matrix) # shape: (n_users, n_users) if target_user not in user_idx: return [] # 新用户冷启动交给画像模块 target_pos user_idx[target_user] target_vec matrix[target_pos] # 取最相似的 K 个用户排除自己 sim_scores list(enumerate(sim_matrix[target_pos])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) sim_scores [s for s in sim_scores if s[0] ! target_pos][:top_k_users] # 候选物品加权累加 reco_scores defaultdict(float) for neighbor_pos, sim_score in sim_scores: neighbor_vec matrix[neighbor_pos] # 只取邻居听过但目标用户没听过的歌 candidate_items [i for i in range(len(items)) if neighbor_vec[0, i] 0 and target_vec[0, i] 0] for i in candidate_items: # 惩罚热门物品alpha 越大热度越高的歌权重越低 item_popularity matrix[:, i].sum() weight sim_score / (item_popularity ** alpha) reco_scores[items[i]] weight * neighbor_vec[0, i] # 取 Top-N top_items sorted(reco_scores.items(), keylambda x: x[1], reverseTrue) return [song_id for song_id, _ in top_items[:top_n_songs]]代码里的核心逻辑在三个地方。第一相似度矩阵直接用了 sklearn 的 cosine_similarity它内部帮你做了稀疏矩阵的归一化不需要手写余弦公式——手写双层循环在用户量上千后就会卡到怀疑人生。第二候选集合只取邻居听过且目标用户没听过的歌这是协同过滤的基本前提如果不去掉已听过的歌推荐结果就是用户播放列表的重复。第三权重算式里加了 item_popularity 的惩罚项这是控制“推荐结果被全网热门歌曲霸榜”的关键。alpha 这个参数值得单独说。设 0 表示不惩罚热门后果是推荐列表永远被最热门的几十首歌霸占每个用户的推荐结果几乎一样答辩现场演示时一点惊喜感都没有。设太大比如 1.5会走向另一个极端太冷门的结果又不符合用户预期。我自己的经验是 0.4 到 0.8 之间做一次网格搜索按下一章的评估指标挑最优值而不是拍脑袋填 0.5。3.3 ItemCF 实现的关键倒排 Spark 风格思维ItemCF 在很多教科书的推荐是“先算物品相似度矩阵再根据用户历史物品取相似物品”。但直接在代码里做通常写成这样def item_cf_recommend(log_df, target_user, top_k_items10, top_n_songs20): matrix, users, items, user_idx, item_idx build_user_item_matrix(log_df) target_vec matrix[user_idx[target_user]] # 用户听过的歌 listened [i for i in range(len(items)) if target_vec[0, i] 0] if not listened: return [] # 计算物品相似度这里用余弦 item_matrix matrix.T # 转置后每行是物品的“用户播放向量” item_sim_matrix cosine_similarity(item_matrix) # 对用户每首听过的歌取最相似的 K 首 reco_scores defaultdict(float) for i in listened: sim_scores list(enumerate(item_sim_matrix[i])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) sim_scores [s for s in sim_scores if s[0] ! i][:top_k_items] for j, sim in sim_scores: if target_vec[0, j] 0: # 跳过已听 item_popularity matrix[:, j].sum() reco_scores[items[j]] sim / (item_popularity ** alpha) return [sid for sid, _ in sorted(reco_scores.items(), keylambda x: x[1], reverseTrue)[:top_n_songs]]和 UserCF 相比ItemCF 这一版最容易被忽略的问题是物品相似度矩阵是稠密的。物品数量超过 5000 时5000×5000 的相似度矩阵会吃掉大量内存。虽然毕设数据量小但仍然建议只用“被同一用户共同播放过的物品对”去构建相似矩阵。也就是说做一次倒排索引——每个用户播放过的所有歌曲两两组合统计共现次数而不是全量算余弦。这个思路也是 Spark 里 ALS 的常见数据预处理方式写进论文的“实现细节”一节会很出彩。4. 把算法拼起来跑通最小闭环数据、训练与验证4.1 没有现成数据怎么办构造一份带边界条件的测试集做毕设最常见的尴尬是——论文要求用真实数据但找不到合适的。公开的 Last.fm 数据集、NetEase 音乐评论数据集在国内网络环境下不是容易拿全的更麻烦的是拿到的数据表结构和我们设计的画像字段不一定对齐。我的建议是走两条路一是先自己构造一个小规模模拟集验证代码路径通不通二是再尝试找公开数据集做行为数据的扩展。构造模拟集时不要随机硬造我带边界条件地写import random import pandas as pd def generate_test_log(n_users500, n_songs2000, n_records20000): 构造模拟行为日志含冷启动用户与热门歌曲 random.seed(42) records [] # 生成几条真正的热门歌曲会被大量用户播放 hot_songs [fS{i} for i in range(1, 11)] for _ in range(n_records): user fU{random.randint(1, n_users)} if random.random() 0.15: song random.choice(hot_songs) # 15% 概率选热门歌 else: song fS{random.randint(1, n_songs)} play_count random.randint(1, 50) like 1 if random.random() 0.2 else 0 skip 1 if random.random() 0.3 else 0 ts random.randint(1600000000, 1700000000) records.append([user, song, play_count, like, skip, ts]) df pd.DataFrame(records, columns[user_id, song_id, play_count, like, skip, timestamp]) # 特意保留 10 个无行为的新用户用来测冷启动 cold_users [fU{i} for i in range(n_users - 9, n_users 1)] existing set(df[user_id].unique()) return df, [u for u in cold_users if u not in existing]这段模拟数据有两个心思在里头。第一15% 的概率直接生成热门歌曲的播放记录这样后面你能亲眼看到不惩罚热门时推荐结果有多单调演示时能形成对比。第二故意留出几个完全没行为的新用户用来验证冷启动分支是不是真的能工作而不是假装没有这个场景。4.2 离线评估指标不要只盯着准确率很多毕设里的评估用准确率这在推荐系统里其实很容易造假。想象一个场景给你 100 首推荐你只听过其中 3 首准确率是 3%。如果我只推荐那首全网最火的歌你大概率听过准确率会虚高——但这样的推荐对你没有任何意义。所以在评估推荐系统时我会同时看三个指标准确率PrecisionN、召回率RecallN和覆盖率Coverage。def evaluate(log_df, train_df, test_df, recommend_func, top_n20): 简化版离线评估 # 训练集上用算法推荐测试集上验证 hit 0 total 0 test_users test_df[user_id].unique() for u in test_users: if u not in train_df[user_id].unique(): continue reco recommend_func(train_df, u, top_n_songstop_n) if not reco: continue real set(test_df[test_df[user_id] u][song_id]) hit len(set(reco) real) total len(reco) precision hit / total if total 0 else 0 # 覆盖率 all_reco set() for u in test_users[:200]: # 取前 200 个用户估算 all_reco.update(recommend_func(train_df, u, top_n_songs20)) total_items set(train_df[song_id].unique()) coverage len(all_reco total_items) / len(total_items) if total_items else 0 return precision, coverage注意这个 evaluate 结构和“准确率”的区别它统计的是 Top-20 推荐列表里有多少首歌被用户在测试期真实播放了对应的其实是 Precision20——先看推荐列表命中多少再看列表长度跟真实播放比例的关系。Recall 请自己补一行hit / len(real)。覆盖率看到的是推荐系统有没有“敢推荐非热门歌”这在音乐场景中是很重要的体验指标。4.3 评分预测和 Top-N 推荐到底哪个适合做主线协通过滤的教科书版本喜欢写评分预测预测用户对未听歌曲的打分再按预测分排序。但真实的音乐推荐场景几乎不采用这个逻辑——因为用户“没有点击”和“不喜欢”是两回事不能把未行为都当 0 分。毕设如果主线做成评分预测评估时就会面对“预测得分 4.2但用户真实行为是 0”的尴尬。我强烈建议主线做成 Top-N 推荐。数据结构、评估方式、演示流程都要更自然答辩时也可以更理直气壮地说“音乐平台推荐的目标不是预测你喜欢什么而是把你喜欢的东西翻出来给你”。5. 避坑手册毕设推荐系统最容易翻车的五个场景5.1 现象所有用户的推荐结果一模一样如果训练完成后的推荐 Top-10 列表换两个用户对比发现重合度超过 70%基本可以断定全是热门歌曲在霸榜。原因很简单热门歌曲播放基数大在用户相似度加权后天然占据高分加上没有对热门作任何惩罚低频歌曲永远挤不进候选集。解决办法有两个方向同时做。一是在候选排序环节加入item_popularity ** alpha的惩罚项alpha 从 0.4 起步试着加大二是计算用户相似度时统一把播放次数做一次 log1p 变换削弱“疯狂刷播放的极端用户”的影响。我在第 3 章的代码里已经把这两处都写进去了。5.2 现象用户画像算出来权重 0 和 1完全拉不开差距很多同学在画像模块里把偏好算成 0/1喜欢这个歌手就是 1不喜欢就是 0。结果所有用户的画像做成 one-hot 编码后余弦相似度差距非常小“相似用户”找得极不靠谱。原因是对播放强度、收藏比例这类连续特征没有做“无量纲化”处理。播放了 3 次和播放了 30 次在强偏好面前被一视同仁了。解决方法是先对每个连续特征做 min-max 或 z-score 归一化再把归一化后的向量传入相似度计算。我见过一个比较稳妥并容易出效果的做法——画像特征进相似度前都先做一次sklearn.preprocessing.StandardScaler。5.3 现象用全部数据训练再用全部数据评估指标高得吓人但现场演示一塌糊涂这是典型的“自己骗自己”的翻车现场。训练集和测试集重叠时你推荐的是用户已经听过的歌评估的 Precision 当然高。但这种系统放到真实环境里用户点开推荐列表发现全是自己播放历史里的歌没有任何惊喜。标准做法是按时间切分用前 80% 的用户行为做训练后 20% 做测试。我在第 4 章的 evaluate 函数里已经把这一点写明白——凡是出现在训练集里的用户才参与测试评估。代码里也特地在模拟数据阶段留下了无行为的冷启动用户在评估时得把它们单独拆出来验证冷启动分支不能混在整体指标里。5.4 现象代码运行到物品相似度计算时内存直接爆掉用余弦相似度计算 2000 首歌之间的相似矩阵瓶颈不在矩阵本身而在 sklearn 的cosine_similarity会将稀疏矩阵转为稠密矩阵再计算内存瞬间吃满。用户数超过 3000、歌曲数超过 10000 时常见的 16G 内存笔记本基本必挂。一个务实的做法是认怂直接改用手写的“共现物品对”相似度只计算同时出现在同一用户播放列表中的物品对而不会计算所有物品之间的相似度。这个策略让矩阵的稀疏性帮你省掉 95% 的无效计算。如果坚持要用全量相似度矩阵请在调用cosine_similarity前先对行做一次抽样减负——保留播放次数最高的前 3000 首歌。5.5 现象新用户进系统推荐模块直接返回空列表不少同学的代码在build_user_item_matrix阶段就给新用户分配了矩阵位置但target_vec全为 0cosine_similarity算出来相似用户全为 0最后 Top-N 推荐直接为空。Demo 时一旦现场注册一个新账号演示就是最尴尬的几秒钟。解决思路前面已经说过——把画像模块前置收集新用户注册时勾选的歌手/语种偏好用画像最近邻找到相似的老用户借用他们的历史播放列表做候选池。至少在论文里写清楚这一层而不是假装这个问题不存在。6. 让这个项目真正有增量价值混合推荐与冷启动闭环到这一步为止你已经有了画像模块、UserCF、ItemCF 和评估代码。但如果答辩到此为止只能算“复现了教科书”还不到“能拿得出手的毕设”的标准。真正能让评委点头的是把两个算法用某种方式融合起来。我常用的混合方案是画像召回 协同过滤重排的两段式结构。第一段用户有行为时走 UserCF/ItemCF 的强关联召回新用户只走画像相似用户召回。第二段把两类结果统一放进来用画像特征里算出的 artist_diversity 和 night_active 做插值调整——例如夜间活跃用户优先把相似用户里播放时间集中在夜间的歌曲排序提前。def hybrid_recommend(log_df, profiles, target_user, top_n20): 画像召回 UserCF 融合的混合推荐示例 reco user_cf_recommend(log_df, target_user, top_k_users10, top_n_songstop_n) if not reco: # 冷启动利用画像相似用户召回 if target_user not in profiles: return [] sim_users find_similar_users_by_profile(profiles, target_user, top_k10) reco collect_songs_from_users(log_df, sim_users, top_ntop_n) return rerank_with_profile(reco, profiles.get(target_user, {}))这里面find_similar_users_by_profile是按照第 2 章的画像特征做余弦相似度计算collect_songs_from_users是把相似老用户播放过、目标用户没播放过的歌曲汇总。混合这块代码不需要封装得太厚重能把“冷启动不是无解”这件事演示出来就足够了。最后说一句我自己的习惯。每次调整相似度阈值、K 值、alpha 或衰减半衰期后我不会只盯整体指标还会随手点开三五个用户的推荐结果看一遍——指标只能告诉你“平均变好了”只有肉眼看列表才能发现热门霸榜、推荐越来越窄这类结构性问题。这也是你做毕设时最值得培养的习惯。这个方向做到这个深度答辩时不管导师问到数据处理、算法选型、评估方式还是冷启动你都有材料能接住。希望帮到你。本文还有配套的精品资源点击获取