一、背景与动机

问题:特征组合的困境

在推荐系统、广告点击预测等任务中,特征之间往往存在交互关系

  • 用户 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 的核心价值

  1. 高效:用隐向量降维,参数从 O(n²) → O(nk)
  2. 泛化:稀疏数据下仍能估计未见组合
  3. 实用:工业界广泛应用(推荐、广告)

适用场景

  • 点击率预估(CTR)
  • 排序任务
  • 评分预测
  • 任何需要特征交互的任务

局限

  • 只捕捉二阶交互(需 DeepFM 等扩展高阶)
  • 隐向量维度 k 需调参

参考资源

  • 原论文:Rendle, S. (2010). “Factorization Machines”
  • 开源实现:libFM (C++), xLearn, fastFM
  • 深入讲解:美团技术博客 FM 系列文章
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐