资讯动态

FM分解机:从稀疏特征到音乐推荐的交叉特征建模

发布时间:2026/9/14 9:05:57 来源:尧图企业网站定制
简介压缩包收录了一套基于分解机模型的音乐推荐系统完整项目由R语言和Python语言配合实现适合推荐算法学习者、数据科学初学者以及需要快速搭建召回或排序实验的开发者。资源围绕用户物品稀疏交互数据展开能够帮助理解分解机在推荐场景中的应用流程是一份从理论到实践的紧凑样例包内共有十四个文件包括八个R语言脚本、五个Python语言脚本和一个Markdown说明文档整体体积约为十五千字节代码规模小巧结构清晰便于逐行阅读和二次修改。项目目前已有一百四十七人浏览学习。实际内容覆盖数据预处理、分解机模型训练、平均倒数排名评估以及序列推荐和上下文感知推荐等模块说明文档对运行方式和文件组织进行了交代。使用这份项目可以直观理解分解机如何通过特征向量的内积捕捉用户与物品之间的交互关系同时学习R语言与Python语言在数据清洗、建模、结果校验等环节的协同用法适合作为课程设计、论文复现或小型推荐实验的参考起点。1. 拿 #nowplaying 数据集试过之后我才认可 FM 不是老古董做音乐推荐的时候最让人头疼的不是模型不够深而是特征长什么样。用户 ID、歌曲 ID、听歌时段、歌手、风格这些特征单独看都很稀疏但组合起来才是真正的信号深夜三点还在听后摇的人和下午边工作边听盯鞋的人想给他推的东西完全不一样。普通逻辑回归对这种交叉关系无能为力矩阵分解又只认用户和物品两个维度中间的括号没人接。我拿 #nowplaying 这个基于 Twitter 正在听歌状态的数据集跑了一轮用分解机把上下文特征、用户特征、物品特征全部丢进一个模型最后 MRR 比单纯按流行度推荐高了一截。这个项目是 R 做数据预处理、Python 跑 FM 训练的混合结构里面还同时给了上下文和序列两套实验脚本正好适合想复现 FM 推荐又不想从零搭环境的人。2. FM 模型原理与输入特征设计从交互矩阵到交叉特征2.1 分解机是怎么学会特征交互的分解机Factorization Machine本质上是在线性模型后面加了一层二阶交叉项。形式上可以写成y(x) w0 sum(wi * xi) sum(sum(vi, vj * xi * xj))其中vi是特征 i 的隐向量vi, vj是点积。当两个特征同时非零时这个交叉项才被激活。因为特征是 one-hot 或者 multi-hot 编码大部分 xi 都是 0所以实际参与计算的只有那些点击过的特征对这正好让稀疏数据里的信号能互相传递。FM 的巧妙在于没有为每个特征对单独设权重而是共享隐向量。比如用户 A 在深夜听过歌曲 X 学到了「深夜」和「后摇」的关联那么用户 B 在深夜场景就也能复用这层关系哪怕 B 和 X 从没同时出现过。这就是拿 FM 做音乐推荐比纯 LR 强的原因。2.2 项目里的特征空间怎么安排打开 nowplaying-RS-Music-Reco-FM-master你会发现它的特征并不是简单地把用户 ID 和歌曲 ID 拼起来。文件名里的Context_POP_RND、Context_POP_USER、Sequence已经把实验口径分好了。常见做法是把用户 ID、歌曲 ID、歌手 ID、时间槽早中晚、星期几、收听次数等组合成一个高维稀疏向量。下面的表是一个典型的 FM 输入特征设计特征组特征含义编码方式示例用户用户唯一标识one-hot用户 132歌曲歌曲唯一标识one-hot歌曲 7781上下文时段4 段 one-hot深夜上下文星期7 段 one-hot周六上下文是否重复收听二值1交互用户-歌曲历史次数归一化数值0.6这样的特征向量维度可能到百万级别但每条样本非零特征通常不到 20 个。FM 的优势就体现在这个地方维度再高实际计算量也只跟非零个数相关。项目里 train.r 和 load.py 做的事情基本都是把原始日志文件映射成这种特征格式。2.3 为什么这个场景不像 DNN 或矩阵分解那么顺有过深度学习经验的人可能会问为什么不上一个 embedding MLP 的模型。在数据量只有几百万到几千万的推荐场景里DNN 的参数规模很难压住玩一玩就过拟合。矩阵分解只能建模 user-item 二元交互你很难把「周六晚上」这种上下文塞进内积公式硬塞就要把 MF 改造成 SVD 或者 timeSVD改来改去结果就是把 FM 重新实现一遍。FM 的另一个优点是对两种语言支持都很成熟R 里可以自己写梯度更新Python 里可以用 xlearn 或 libfm 直接训练项目里 runFM.py 走的就是 Python 路线。既然你已经在一套代码里同时看到 R 和 Python 脚本说明团队当时看重的是数据处理用 R 顺手、模型训练 Python 生态方便。3. R 脚本预处理与基准实验先跑通 POP_RND 和 POP_USER3.1 train.r 是怎么生成 FM 训练样本的R 脚本在项目里的角色是特征工程和负采样。train.r 大致的逻辑是读取原始日志表把用户 ID、歌曲 ID、上下文时间戳映射成整数索引然后输出 libFM 格式的文本。libFM 格式长这样1 1:1 2:302 3:1 4:1 5:0.6 0 1:1 2:404 3:2 4:1 5:0.2每一行是 label 后跟非零特征格式是index:value从 1 开始计数。下面是一段简化后的 R 预处理代码library(data.table) dt - fread(nowplaying_data.csv) dt - dt[!is.na(user_id) !is.na(song_id)] # 把 user_id 和 song_id 映射为连续的整数 uid_map - unique(dt$user_id) dt[, uid : match(user_id, uid_map)] # 时间槽0-5 深夜, 6-11 上午, 12-17 下午, 18-23 晚上 dt[, time_slot : floor(hour / 6) 1] # 生成 libfm 格式特征起始索引从 1 开始 dt[, feat_str : sprintf(1:%d 2:%d 3:%d, uid, song_id, time_slot)] # 根据历史收听次数构造数值特征 dt[, listen_ratio : listen_count / mean(listen_count)] # 写出训练样本 fwrite(dt[, .(positive, feat_str, sprintf(5:%.3f, listen_ratio))] , fm_train.txt, sep , quote FALSE, col.names FALSE)这里的要点是特征索引必须全局统一R 脚本负责把所有分类变量转换成整数索引后续 Python 训练时不再关心字符串。hour字段要先从时间戳里提取listen_ratio的归一化用均值除一遍避免数值特征尺度太大影响隐向量学习。如果你手上有train.r和test.r两个入口建议先跑一遍 test.r确认特征索引空间和 train 一致否则后面 calcMRR 的结果会完全失真。3.2 Context_POP_RND 和 Context_POP_USER 到底在对比什么项目名里的Context_POP_RND是指上下文感知的流行度 随机推荐基线Context_POP_USER是流行度 用户历史偏好基线。这两套基准的意义是把 FM 模型和各种朴素策略放在同一个评估管线里对比如果 FM 的 MRR 连按收听榜推都打不过那说明特征没对齐或者训练数据有问题。train_POP_RND.r 和 train_POP_USER.r 的差别只在怎么生成负样本RND 模式从所有歌曲里随机抽负样本USER 模式从用户没听过的歌曲里抽样并且给热门歌曲更高概率。Rscript train.r --mode context --baseline pop_rnd Rscript test.r --mode context --baseline pop_rnd Rscript train.r --mode context --baseline pop_user Rscript test.r --mode context --baseline pop_user跑完以后会生成一个验证集预测结果里面包含每个测试用户对应的候选歌曲排名。这种设计把干净的数据处理和复杂的模型训练拆开了你可以先看基准能不能跑通再进入 FM 调参。负样本比例也是在这个环节定的R 脚本里有个negative_ratio参数我一般会设 3 到 5太高会把正样本的 signal 淹没太低模型又学不到负反馈信息。3.3 基准实验的输出怎么看基准脚本最后会打印简单的命中率报告主要包括 Recall20 和 MRR。下表是我跑默认参数时经常看到的对比具体数会因为数据集清洗方式浮动配置特征范围负样本策略预期 MRR20Context_POP_RND只含流行度统计全局随机负采样0.08 - 0.12Context_POP_USER用户历史收听偏好用户导向负采样0.12 - 0.18FM默认用户 歌曲 上下文用户导向负采样0.18 - 0.25如果 FM 跑出来和 POP_USER 差不多先查一件事上下文特征在 FM 里到底有没有被用上。可以在 runFM.py 里临时把时间槽和星期从特征里去掉看 MRR 是否下降如果没有变化说明特征索引在 R 侧就已经错了。4. Python 侧 FM 训练与 MRR 评估从 libFM 格式到推荐排名4.1 runFM.py 的训练流程拆解Python 端接收 R 脚本产出的 libFM 格式文件然后进行模型训练。现在项目里普遍用 xlearn 的 FM 模型接口很干净。下面这个示例对应runFM.py的核心逻辑import xlearn as xl # R 输出的训练文件直接作为 xlearn 输入 fm_model xl.create_fm() fm_model.set_train(fm_train.txt) fm_model.set_validate(fm_valid.txt) param { task: binary, lr: 0.01, lambda: 0.0002, k: 32, epoch: 20, opt: ftrl, eval_metric: auc } fm_model.fit(param, ./fm_model.out)k32是隐向量的维度这个值不是越大越好特征只有几十个非零维度时隐向量设到 64 以上容易引入噪声。opt换成sgd收敛会更慢但更稳ftrl适合处理大规模稀疏特征。训练完以后用同一个模型给验证集打分fm_model.set_test(fm_valid.txt) fm_model.predict(./fm_model.out, ./fm_pred.txt)这里的预测输出就是每条样本的点击概率后面 calcMRR 脚本需要结合候选集顺序把概率转成排名。4.2 calcMRR.py 怎么计算指标MRR 是 Mean Reciprocal Rank对每个测试用户在所有候选歌曲中去找那个真正交互过的目标歌曲取排名倒数后求平均。实现起来不长下面这段代码基本可以抄import numpy as np def calc_mrr(scores_path, ground_truth_path): mrr_sum 0.0 q_count 0 with open(scores_path) as f_score, open(ground_truth_path) as f_truth: cur_user None user_candidates [] def flush(): global mrr_sum, q_count if cur_user is None or len(user_candidates) 0: return rank_list sorted(user_candidates, keylambda x: x[1], reverseTrue) target truth_map.get(cur_user) for rank, (item_id, _) in enumerate(rank_list, 1): if item_id target: mrr_sum 1.0 / rank q_count 1 break truth_map {} for line in f_truth: u, item line.strip().split() truth_map[u] item for line in f_score: user, item, sc line.strip().split() if cur_user ! user: flush() cur_user user user_candidates [] user_candidates.append((item, float(sc))) flush() return mrr_sum / q_count逻辑说明scores_path里每一行是用户、候选物品和 score同一个用户的候选会连续排在一起。维护一个user_candidates列表等用户切换时先排序再在排序结果里找目标歌曲的位置。这里有个容易踩的坑如果候选集中没有目标物品应该跳过该用户而不是记零否则数据不全会严重拉低 MRR。我在实际用的时候还会顺手输出 Recall20毕竟 MRR 只关心第一名位置推荐列表的覆盖率也需要对照看。4.3 调参顺序建议FM 在音乐推荐上的超参数并不需要网格搜索大动干戈按下面的顺序调就行参数推荐范围影响k隐向量维度16 - 48维度太低欠拟合太高过拟合lr学习率0.005 - 0.02太高容易振荡lambda正则化1e-4 - 1e-3缓解稀疏特征过拟合epoch10 - 30看验证集早停negative_ratio3 - 5R 脚本里控制负采样比例每改一个参数都要同时盯训练 AUC 和验证 MRR。有次我把 lambda 设到 0前几个 epoch 的 AUC 升高很快之后就崩了因为用户 ID 的特征太多了几乎每个用户独有的隐向量都被疯狂放大。FM 的正则项比 LR 更重要因为它直接约束隐向量的 L2 范数。5. 从上下文到序列推荐train_seq.r 的进阶玩法与排错5.1 用最近听歌记录构造序列特征Sequence目录下那组 train_seq.r/test_seq.r 是把 FM 从上下文感知扩展到序列感知。做法不复杂把用户最近听过的 10 首歌按时间排序编码成 multi-hot 特征拼到原有特征向量里比如位置 10001 到 10010 分别表示最近第 1 首、第 2 首……这样 FM 就能学到「听了 A 之后紧接着听 B」的转移关系。注意这里不要把每首歌都单独做 one-hot否则 10 首歌就是 10 个独立的 id跟用户历史特征没区别了。# 按用户和收听时间排序构造序列特征 dt - dt[order(user_id, timestamp)] dt[, seq_flag : 1:.N, by user_id] seq_feat - dt[seq_flag 10, .(user_id, seq_feat_str paste(sprintf(1000%d:%d, seq_flag, song_id), collapse ))]写文件时把seq_feat_str追加到原有 libfm 格式后面新特征索引从 100 开始或往前偏移避免和原有索引冲突。训练的时候建议先冻结住 FM 的参数单独把序列特征投进去看训练 loss 是否下降如果完全不动多半是特征索引越界被 libFM 静默丢弃了。5.2 验证集必须切断未来信息序列推荐最容易犯的错是把测试用户最近收听的歌曲混进训练集。在test_seq.r里对每个测试用户在某一时刻截断历史只保留该时刻之前的记录作为特征该时刻之后的首次收听作为目标。按下述思路处理可以避坑把数据集按用户切分用每个用户 80% 时间段的记录做训练20% 最新的记录做验证并且在验证时不使用这 20% 里的歌曲做用户特征。如果验证集 MRR 特别高先检查一遍特征构建函数是否引用到了全局的最后一条记录这个 bug 在团队协作时非常常见。5.3 让 FM 在音乐推荐场景真正落地的小技巧跑完序列特征后可以把 FM 预测出的 top K 歌曲作为候选集再交给下游的排序模型精排而不是让 FM 一杆子插到底。从项目结构看R 做特征工程、Python 做模型训练、calcMRR 做离线评估这套流程本身就是可以复用到别的推荐项目的脚手架。在 R 和 Python 之间传递数据时用data.table::fwrite加sep 别用默认的逗号否则 libFM 格式解析会崩。调参的时候也别把隐向量维度照搬论文里的 20在 #nowplaying 这种规模上32 就是个合理的起点。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价