FM (Factorization Machines) 学习日记
一、背景与动机
问题:特征组合的困境
在推荐系统、广告点击预测等任务中,特征之间往往存在交互关系:
- 用户 A 喜欢电影 X → 用户 A + 电影 X 组合很重要
- 周末晚上 + 美食类视频 → 点击率高
传统方法的局限:
| 方法 | 问题 |
|---|---|
| 线性模型 | 无法捕捉特征交互 |
| 多项式回归 | 显式组合所有特征,参数爆炸 O(n²) |
| SVM | 核函数计算复杂,稀疏数据效果差 |
FM 的解决方案
用隐向量表示每个特征,通过向量内积隐式建模特征交互,将参数从 O(n²) 降到 O(nk),k 是隐向量维度。
二、核心公式
FM 模型方程
对于输入特征向量 x,FM 预测值为:
ŷ = w₀ + Σ(wᵢ · xᵢ) + ΣΣ(<vᵢ, vⱼ> · xᵢ · xⱼ)
^ ^ ^
偏置 一阶线性项 二阶交互项
分解为三部分:
| 部分 | 含义 | 参数量 |
|---|---|---|
| w₀ | 全局偏置 | 1 |
| Σ(wᵢ · xᵢ) | 线性部分(一阶) | n |
| ΣΣ(<vᵢ, vⱼ> · xᵢ · xⱼ) | 交互部分(二阶) | n × k |
关键创新:内积分解
传统二阶交互需要 wᵢⱼ 参数(O(n²) 个),FM 用隐向量内积替代:
wᵢⱼ ≈ <vᵢ, vⱼ> = Σ(vᵢₓ · vⱼₓ) (x从1到k)
向量内积的定义:
两个向量 a 和 b 的内积(也叫点积),定义为对应元素相乘再求和:
<a, b> = Σ(aₓ · bₓ) = a₁·b₁ + a₂·b₂ + ... + aₖ·bₖ
举例:
a = [0.2, 0.5, 0.3]
b = [0.4, 0.1, 0.2]
<a, b> = 0.2×0.4 + 0.5×0.1 + 0.3×0.2
= 0.08 + 0.05 + 0.06
= 0.19
内积的意义:
- 内积值越大 → 两个向量越"相似"或越"接近同方向"
- 内积值越小 → 两个向量越"不相关"或"方向相反"
- 内积为 0 → 两个向量正交(垂直),完全不相关
在 FM 中,<vᵢ, vⱼ> 表示特征 i 和特征 j 的"匹配程度"或"交互强度"。
好处:
- 参数量:n × k(k 通常取 8-64)
- 即使特征组合从未在训练中出现,也能通过隐向量内积估计交互强度
wᵢ 和 vᵢ 的区别
wᵢ 和 vᵢ 是 FM 模型中不同部分的参数,作用完全不同:
| 参数 | 所属部分 | 维度 | 作用 |
|---|---|---|---|
| wᵢ | 一阶(线性) | 1 个数值 | 单独衡量特征 i 的重要性 |
| vᵢ | 二阶(交互) | k 维向量 | 与其他特征做内积,衡量交互强度 |
在公式中的位置:
ŷ = w₀ + Σ(wᵢ · xᵢ) + ΣΣ(<vᵢ, vⱼ> · xᵢ · xⱼ)
^ ^ ^
偏置 一阶部分(wᵢ) 二阶部分(vᵢ)
- wᵢ 出现在线性项:
wᵢ · xᵢ(特征 i 单独的贡献) - vᵢ 出现在交互项:
<vᵢ, vⱼ>(特征 i 和 j 的交互贡献)
举例说明:
假设预测用户是否会点击某个广告,特征有:
| 特征 | xᵢ(输入值) | wᵢ(一阶权重) | vᵢ(隐向量,k=3) |
|---|---|---|---|
| 用户A | 1 | 0.5 | [0.2, 0.1, 0.3] |
| 广告X | 1 | -0.2 | [0.4, 0.2, 0.5] |
| 周末 | 1 | 0.8 | [0.1, 0.3, 0.2] |
wᵢ 的作用(单独贡献):
一阶部分 = w₀ + w₀·x₀ + w₁·x₁ + w₂·x₂
= w₀ + 0.5×1 + (-0.2)×1 + 0.8×1
= w₀ + 1.1
解读:
- 用户A 本身有点击倾向(w₀=0.5)
- 广告X 本身不太吸引人(w₁=-0.2)
- 周末时段点击率高(w₂=0.8)
vᵢ 的作用(交互贡献):
二阶部分 = <v₀,v₁>·x₀·x₁ + <v₀,v₂>·x₀·x₂ + <v₁,v₂>·x₁·x₂
用户A-广告X 交互 = <v₀,v₁> = 0.2×0.4 + 0.1×0.2 + 0.3×0.5 = 0.23
用户A-周末 交互 = <v₀,v₂> = 0.2×0.1 + 0.1×0.3 + 0.3×0.2 = 0.11
广告X-周末 交互 = <v₁,v₂> = 0.4×0.1 + 0.2×0.3 + 0.5×0.2 = 0.18
解读:
- 用户A 和 广告X 的匹配度 = 0.23(正交互,可能喜欢)
- 用户A 和 周末时段的交互 = 0.11(弱交互)
- 广告X 在周末更吸引人 = 0.18
直观理解:
| 参数 | 类比 |
|---|---|
| wᵢ | 这个特征的"基础分数",不管其他特征是什么 |
| vᵢ | 这个特征的"性格标签",用来和其他特征"配对" |
就像相亲:
- wᵢ = 这个人的基础条件(收入、学历等)
- vᵢ = 这个人的性格特点,用来判断和另一个人是否合拍
两个人合不合拍,取决于 v 向量的内积,而不是各自的 w 值。
为什么需要分开?
如果只用 wᵢ,模型不知道"用户A 喜欢电影X"这种组合信息。
如果只用 vᵢ(没有一阶项),模型就丢失了"周末时段本身点击率就高"这种单独特征的重要性。
两者结合,FM 才能:
- 捕捉单独特征的重要性(wᵢ)
- 捕捉特征组合的交互强度(vᵢ)
三、数学推导:高效计算
原始二阶项计算复杂度 O(n²),但可以优化到 O(nk):
原始形式:
ΣΣ(<vᵢ, vⱼ> · xᵢ · xⱼ) (i从1到n, j从i+1到n)
优化形式:
= 1/2 · Σ[(Σ vᵢₓ · xᵢ)² - Σ(vᵢₓ² · xᵢ²)] (x从1到k)
推导过程
第一步:展开平方项
(Σ vᵢₓ · xᵢ)² = Σ(vᵢₓ² · xᵢ²) + ΣΣ(vᵢₓ · vⱼₓ · xᵢ · xⱼ) (i≠j的部分)
第二步:理解交互项
交互项(j ≠ i)包含 i < j 和 i > j 两种情况,各算一次,正好是原始二阶项的 2 倍:
ΣΣ(vᵢₓ · vⱼₓ · xᵢ · xⱼ) (i≠j) = 2 · ΣΣ(vᵢₓ · vⱼₓ · xᵢ · xⱼ) (i<j)
第三步:得到最终公式
ΣΣ(vᵢₓ · vⱼₓ · xᵢ · xⱼ) (i<j)
= 1/2 · [(Σ vᵢₓ · xᵢ)² - Σ(vᵢₓ² · xᵢ²)]
代码对比
# 原始方式 O(n²) - 不推荐
interaction = 0
for i in range(n):
for j in range(i+1, n):
interaction += np.dot(V[i], V[j]) * x[i] * x[j]
# 高效方式 O(nk) - 推荐
linear_sum = V * x.reshape(-1, 1) # (n, k)
sum_square = np.sum(linear_sum, axis=0) ** 2 # (k,) 先求和再平方
square_sum = np.sum(linear_sum ** 2, axis=0) # (k,) 先平方再求和
interaction = 0.5 * (sum_square - square_sum).sum()
四、为什么 FM 强大
1. 参数效率
假设 n = 1000 个特征,k = 16:
| 方法 | 二阶交互参数 |
|---|---|
| 多项式回归 | 1000 × 1000= 1000,000 |
| FM | 1000 × 16 = 16,000 |
2. 处理稀疏数据
推荐系统中,特征往往高度稀疏(大多数 xᵢ = 0):
- 传统方法:很多 wᵢⱼ 从未训练,无法学习
- FM:即使 (用户A, 电影X) 组合从未出现,各自隐向量在其他组合中已学习,可以推断交互强度
3. 泛化能力
隐向量捕捉特征的"语义":
- 用户 A 的向量 v_A 代表他的偏好模式
- 电影 X 的向量 v_X 代表其类型特征
- 内积 <v_A, v_X> = 偏好匹配度
五、与其他方法对比
| 方法 | 特点 | 适用场景 |
|---|---|---|
| FM | 二阶交互,参数少 | 推荐系统、CTR预估 |
| FFM | 特征分域,向量更精细 | 多领域特征 |
| DeepFM | FM + DNN,同时学低阶和高阶 | 复杂交互模式 |
| Wide&Deep | 线性 + DNN | 通用推荐 |
六、训练与优化
损失函数
根据任务选择:
# 回归任务
loss = MSE(y_true, y_pred)
# 二分类(如点击预测)
loss = BCE(y_true, sigmoid(y_pred))
# 排序任务(推荐)
loss = BPR 或 Pairwise Loss
优化方法
| 方法 | 特点 |
|---|---|
| SGD | 简单有效,适合大规模数据 |
| ALS | 交替最小二乘,理论保证收敛 |
| Adam | 自适应学习率,推荐使用 |
正则化
# L2 正则化
loss += λw · ||w||² + λv · ||V||²
七、PyTorch 实现
import torch
import torch.nn as nn
class FactorizationMachine(nn.Module):
def __init__(self, num_features, k=16):
super().__init__()
# 一阶部分
self.w0 = nn.Parameter(torch.zeros(1)) # 偏置
self.w = nn.Parameter(torch.zeros(num_features)) # 一阶权重
# 二阶隐向量 (每个特征一个k维向量)
self.V = nn.Parameter(torch.randn(num_features, k) * 0.01)
def forward(self, x):
"""
x: (batch_size, num_features) 稀疏或稠密特征
"""
# 一阶线性部分
linear = self.w0 + torch.matmul(x, self.w)
# 二阶交互部分(高效计算)
# x.unsqueeze(2): (batch, n) -> (batch, n, 1)
# self.V.unsqueeze(0): (n, k) -> (1, n, k)
# 结果: (batch, n, k) - 广播元素乘法
interactions = x.unsqueeze(2) * self.V.unsqueeze(0)
# 先求和再平方: (batch, k)
sum_square = torch.sum(interactions, dim=1) ** 2
# 先平方再求和: (batch, k)
square_sum = torch.sum(interactions ** 2, dim=1)
# 二阶项: (batch,)
second_order = 0.5 * torch.sum(sum_square - square_sum, dim=1)
# 最终输出
output = linear + second_order
return output
# 使用示例
num_features = 100
model = FactorizationMachine(num_features, k=16)
# 模拟数据
x = torch.randn(32, num_features) # batch_size=32
y = torch.randint(0, 2, (32,)) # 二分类标签
# 训练
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(100):
pred = model(x)
loss = criterion(pred, y.float())
optimizer.zero_grad()
loss.backward()
optimizer.step()
八、实战:CTR 预估示例
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
from FM import FactorizationMachine
# 模拟广告点击数据
# 特征:用户ID、广告ID、设备类型、时间段、位置...
num_users = 1000
num_ads = 500
num_features = num_users + num_ads + 10 # 其他特征
class FMCTR(nn.Module):
def __init__(self, num_features, k=16):
super().__init__()
self.fm = FactorizationMachine(num_features, k)
def forward(self, x):
return torch.sigmoid(self.fm(x))
# 特征编码(One-Hot 或稀疏表示)
def encode_features(user_id, ad_id, other_features):
"""将离散特征转为稀疏向量"""
x = np.zeros(num_features)
x[user_id] = 1 # 用户特征位
x[num_users + ad_id] = 1 # 广告特征位
x[num_users + num_ads:] = other_features # 其他特征
return x
# 生成模拟训练数据
batch_size = 100
user_ids = np.random.randint(0, num_users, batch_size)
ad_ids = np.random.randint(0, num_ads, batch_size)
other_features = np.random.randn(batch_size, 10)
click_labels = np.random.randint(0, 2, batch_size)
X = torch.tensor(
[encode_features(u, a, o)
for u, a, o in zip(user_ids, ad_ids, other_features)],
dtype=torch.float32
)
y = torch.tensor(click_labels, dtype=torch.float32)
# 训练
model = FMCTR(num_features, k=16)
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
losses = []
for epoch in range(500):
pred = model(X)
loss = criterion(pred, y)
losses.append(loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch {epoch}, Loss: {loss.item():.4f}')
# 绘制loss曲线
plt.figure(figsize=(10, 6))
plt.plot(losses, linewidth=2)
plt.xlabel('Epoch', fontsize=12)
plt.ylabel('Loss', fontsize=12)
plt.title('Training Loss', fontsize=14)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('training_loss.png')
print('Loss曲线保存为 training_loss.png')
# ==================== 预测 ====================
model.eval() # 切换到评估模式(关闭 dropout 等)
with torch.no_grad(): # 不计算梯度,节省内存
# 单个样本预测
test_user_id = 50
test_ad_id = 20
test_other = np.random.randn(10)
test_x = torch.tensor([encode_features(test_user_id, test_ad_id, test_other)], dtype=torch.float32)
click_prob = model(test_x).item()
print(f'\n用户 {test_user_id} 点击广告 {test_ad_id} 的概率: {click_prob:.4f}')
# 批量预测
batch_user_ids = [10, 20, 30]
batch_ad_ids = [5, 15, 25]
batch_other = np.random.randn(len(batch_user_ids), 10)
batch_x = torch.tensor(
[encode_features(u, a, o) for u, a, o in zip(batch_user_ids, batch_ad_ids, batch_other)],
dtype=torch.float32
)
batch_probs = model(batch_x).squeeze()
print('\n批量预测结果:')
for uid, aid, prob in zip(batch_user_ids, batch_ad_ids, batch_probs):
print(f' 用户{uid} - 广告{aid}: {prob:.4f}')
# 推荐场景:为用户推荐广告
def recommend_for_user(user_id, top_k=5):
"""为指定用户推荐 top_k 个广告"""
model.eval()
with torch.no_grad():
# 为所有广告生成预测
other = np.zeros(10) # 使用固定的其他特征
user_probs = []
for ad_id in range(num_ads):
x = torch.tensor([encode_features(user_id, ad_id, other)], dtype=torch.float32)
prob = model(x).item()
user_probs.append((ad_id, prob))
# 按概率排序,返回 top_k
user_probs.sort(key=lambda x: -x[1])
return user_probs[:top_k]
print(f'\n=== 为用户 100 推荐 Top 5 广告 ===')
recommendations = recommend_for_user(100, top_k=5)
for rank, (ad_id, prob) in enumerate(recommendations, 1):
print(f'{rank}. 广告ID {ad_id}: 点击概率 {prob:.4f}')
九、FM 的变体与演进
| 模型 | 年份 | 改进点 |
|---|---|---|
| FM | 2010 | 基础版本 |
| FFM | 2014 | 特征分域,每个特征对不同域有不同向量 |
| DeepFM | 2017 | FM + DNN,并行结构 |
| xDeepFM | 2018 | 压缩交互网络,显式高阶交互 |
| AFM | 2017 | 注意力机制加权交互项 |
FFM 思想
不同领域的特征交互方式不同:
- 用户特征与广告特征的交互
- 时间特征与广告特征的交互
- 设备特征与广告特征的交互
FFM 让每个特征针对不同域有不同的隐向量:
交互强度 = <vᵢ_域j, vⱼ_域i>
参数量:n × 域数量 × k
十、总结
FM 的核心价值
- 高效:用隐向量降维,参数从 O(n²) → O(nk)
- 泛化:稀疏数据下仍能估计未见组合
- 实用:工业界广泛应用(推荐、广告)
适用场景
- 点击率预估(CTR)
- 排序任务
- 评分预测
- 任何需要特征交互的任务
局限
- 只捕捉二阶交互(需 DeepFM 等扩展高阶)
- 隐向量维度 k 需调参
参考资源
- 原论文:Rendle, S. (2010). “Factorization Machines”
- 开源实现:libFM (C++), xLearn, fastFM
- 深入讲解:美团技术博客 FM 系列文章
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)