FFM (Field-aware Factorization Machine) 学习日记

背景与动机

问题场景

在 FM 中,每个特征只有一个隐向量:

复制代码
用户 123 → v₁ = [0.2, 0.3, -0.1, ...]
广告 45  → v₂ = [0.1, -0.2, 0.4, ...]

交互: v₁ · v₂ = 0.2×0.1 + 0.3×(-0.2) + ...

问题: 同一个特征在不同域(Field)中应该有不同的表示!

例子说明

域 (Field) 特征 语义
用户域 用户 123 用户偏好
广告域 广告 45 广告属性

FM 的问题:

  • 用户 123 在"用户域"和"广告域"用同一个向量
  • 但语义完全不同!

FFM 的解决:

  • 用户 123 在"用户域"有向量 v₁
  • 用户 123 在"广告域"有另一个向量 v₂
  • 同一特征在不同域有不同表示

FFM vs FM 对比

核心区别

维度 FM FFM
参数量 n × k n × f × k
隐向量数 每个特征 1 个 每个特征 f 个 (f=域数)
交互项 vᵢ · vⱼ vᵢ,ⱼ · vⱼ,ᵢ

公式对比

FM:

复制代码
y = w₀ + Σᵢ wᵢxᵢ + Σᵢⱼ vᵢ · vⱼ xᵢxⱼ  (i < j)

FFM:

复制代码
y = w₀ + Σᵢ wᵢxᵢ + Σᵢⱼ vᵢ,ⱼ · vⱼ,ᵢ xᵢxⱼ  (i < j)

关键区别: FFM 的隐向量依赖于目标域 (Field)


模型原理

Field (域) 的概念

Field 是特征的分组:

复制代码
Field 0: 用户相关特征
  - 用户 ID
  - 用户年龄
  - 用户性别

Field 1: 广告相关特征
  - 广告 ID
  - 广告类型
  - 广告价格

FFM 的交互项

假设有 f 个域:

复制代码
特征 i 在域 j 的隐向量: vᵢ,ⱼ
特征 j 在域 i 的隐向量: vⱼ,ᵢ

交互项: vᵢ,ⱼ · vⱼ,ᵢ

直观理解:

交互 使用的向量 含义
用户 × 广告 v_用户,广告 · v_广告,用户 用户从"广告域角度看" · 广告从"用户域角度看"

参数量计算

FM 参数量:

复制代码
n × k  # n 个特征,每个 k 维

FFM 参数量:

复制代码
n × f × k  # n 个特征,f 个域,每个域内 k 维

示例:

  • 特征数 n = 100
  • 域数 f = 5
  • 隐向量维度 k = 8
模型 参数量 增加倍数
FM 800 1x
FFM 4000 5x

代码实现

PyTorch 实现

python 复制代码
import torch
import torch.nn as nn


class FFM(nn.Module):
    """
    Field-aware Factorization Machine

    核心思想:
        每个特征在每个域都有独立的隐向量
        特征 i 与域 j 交互时,使用 vᵢ,ⱼ
    """

    def __init__(self, feature_dims, field_dims, k=8):
        """
        Args:
            feature_dims: 每个特征的可能取值数
            field_dims: 每个特征所属的域索引
            k: 隐向量维度
        """
        super().__init__()

        self.feature_dims = feature_dims
        self.field_dims = field_dims
        self.num_features = len(feature_dims)
        self.num_fields = max(field_dims) + 1
        self.k = k

        # 一阶权重
        self.w0 = nn.Parameter(torch.zeros(1))
        self.w = nn.Parameter(torch.zeros(self.num_features))

        # ==================== FFM 的核心 ====================
        # 形状: (num_features, num_fields, k)
        # 每个特征在每个域都有一个 k 维向量
        self.V = nn.Parameter(torch.randn(self.num_features, self.num_fields, k) * 0.01)

    def forward(self, x):
        """
        Args:
            x: (batch_size, num_features) 特征索引

        Returns:
            logits: (batch_size, 1) 预测分数
        """
        batch_size = x.shape[0]

        # 一阶线性项
        linear = self.w0 + torch.matmul(x, self.w)

        # ==================== 二阶交互项 ====================
        second_order = torch.zeros(batch_size)

        # 遍历所有特征对
        for i in range(self.num_features):
            for j in range(i + 1, self.num_features):
                # 获取特征 i 和 j 所属的域
                field_i = self.field_dims[i]
                field_j = self.field_dims[j]

                # ==================== FFM 关键 ====================
                # 特征 i 在域 j 的隐向量
                vi_fieldj = self.V[i, field_j]  # (k,)

                # 特征 j 在域 i 的隐向量
                vj_fieldi = self.V[j, field_i]  # (k,)

                # 交互项
                interaction = torch.sum(vi_fieldj * vj_fieldi, dim=-1)  # (batch_size,)

                second_order += x[:, i] * x[:, j] * interaction

        output = linear + second_order.view(-1, 1)
        return output


# ==================== 使用示例 ====================
if __name__ == '__main__':
    # 特征定义
    # Field 0: 用户相关
    # Field 1: 广告相关
    feature_dims = [1000, 5, 3, 500, 4, 2]  # 用户ID, 年龄, 性别, 广告ID, 类型, 价格
    field_dims = [0, 0, 0, 1, 1, 1]         # 前三个是用户域,后三个是广告域

    model = FFM(
        feature_dims=feature_dims,
        field_dims=field_dims,
        k=8
    )

    print('=== FFM 模型结构 ===')
    print(model)

    # 计算参数量
    total_params = sum(p.numel() for p in model.parameters())
    print(f'参数量: {total_params:,}')

    # 生成模拟数据
    batch_size = 32
    x = torch.tensor([
        [torch.randint(0, dim, size=(1,)).item() for dim in feature_dims]
        for _ in range(batch_size)
    ])
    y = torch.randint(0, 2, (batch_size, 1), dtype=torch.float32)

    # 训练
    criterion = nn.BCEWithLogitsLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    print('\n=== 开始训练 ===')
    for epoch in range(100):
        pred = model(x)
        loss = criterion(pred, y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 20 == 0:
            print(f'Epoch {epoch + 1}, Loss: {loss.item():.4f}')

代码详解:交互项计算

FFM 最核心的两行代码:

python 复制代码
# 第一行:计算两个隐向量的内积
interaction = torch.sum(vi_fieldj * vj_fieldi, dim=-1)  # (batch_size,)

# 第二行:累加所有交互
second_order += x[:, i] * x[:, j] * interaction
第一行:计算向量内积

这是什么?

计算两个隐向量的内积(点积)

复制代码
vi_fieldj = [v₁, v₂, v₃, v₄, v₅, v₆, v₇, v₈]  # 特征 i 在域 j 的向量
vj_fieldi = [u₁, u₂, u₃, u₄, u₅, u₆, u₇, u₈]  # 特征 j 在域 i 的向量

interaction = v₁·u₁ + v₂·u₂ + v₃·u₃ + ... + v₈·u₈

内积的含义: 衡量两个向量的相似度/关联强度

为什么这样设计?

  • FFM 的核心:特征 i 在不同域有不同表示
  • 特征 i 与域 j 交互时,用 vi_fieldj
  • 特征 j 与域 i 交互时,用 vj_fieldi
第二行:累加所有交互

这是什么?

将当前特征对 (i, j) 的交互项加到总和中:

复制代码
second_order = Σᵢⱼ (xᵢ · xⱼ · vᵢ,ⱼ · vⱼ,ᵢ)

分解说明:

部分 含义 作用
x[:, i] 特征 i 的值 0 或 1 (one-hot)
x[:, j] 特征 j 的值 0 或 1 (one-hot)
vᵢ,ⱼ · vⱼ,ᵢ 两个隐向量的内积 关联强度

累加过程:

python 复制代码
second_order = 0

# 第一对: (用户ID, 广告ID)
second_order += x[:, 0] * *x[:, 3] * v_用户,广告 · v_广告,用户

# 第二对: (用户ID, 广告类型)
second_order += x[:, 0] * x[:, 4] * v_用户,类型 · v_类型,用户

# ... 继续,直到所有特征对计算完毕
为什么需要 x[:, i] * x[:, j]

这是稀疏特征处理的技巧:

python 复制代码
# one-hot 特征
x = [1, 0, 0, 1, 0]  # 特征 0 和 3 存在

# 交互项计算
x[:, 0] * x[:, 3] = 1 × 1 = 1   # 两个特征都存在,交互有效
x[:, 0] * x[:, 1] = 1 × 0 = 0   # 特征1不存在,忽略

作用:

  • 只计算同时存在的特征对
  • 稀疏特征优化(大多数项是 0)
直观理解:用户点击广告

场景: 用户 123 点击广告 45

特征:

复制代码
特征 0: 用户ID = 123  (在用户域)
特征 3: 广告ID = 45   (在广告域)

计算过程:

复制代码
1. 获取隐向量
   v_用户,广告 = 用户123从"广告域"的角度看   = [0.23, 0.15, ...]
   v_广告,用户 = 广告45从"用户域"的角度看   = [0.67, 0.32, ...]

2. 计算内积 (第一行代码)
   interaction = 0.23×0.67 + 0.15×0.32 + ... = 0.34

3. 累加交互项 (第二行代码)
   second_order += 1 × 1 × 0.34 = 0.34
   (x[:, 0] = 1, x[:, 3] = 1,因为特征存在)


实战应用

域的划分策略

策略 说明 示例
语义域 按语义分类 用户域、广告域、语境域
时间域 按时间划分 实时域、历史域
交叉域 特殊组合 用户×广告域

超参数选择

参数 推荐值 影响
k (隐向量维度) 4-16 过大过拟合
学习率 0.001 标准值
正则化 L2, λ=0.01 防止过拟合

优化技巧

1. 预训练 Embedding

先用 FM 训练,初始化 FFM 的隐向量:

python 复制代码
FFM.V[:, :, :] = FM.V.unsqueeze(1).repeat(1, num_fields, 1)
``''

**2. 稀疏矩阵优化**

对于稀疏特征,只计算非零特征的交互:
```python
# 只遍历非零特征
non_zero_indices = (x > 0).nonzero()

面试常见问题

Q1: FFM 和 FM 的核心区别是什么?

A:

  • FM: 每个特征只有 1 个隐向量
  • FFM: 每个特征在每个域都有独立的隐向量
  • 本质: FFM 考虑了特征在不同上下文中的不同语义

Q2: 为什么 FFM 需要更多参数?

A:

因为每个特征在 f 个域中都有独立表示:

复制代码
FM 参数:  n × k
FFM 参数: n × f × k  # 多了 f 倍

f 是域的数量,通常 3-10 个。

Q3: FFM 什么时候效果比 FM 好?

A:

当满足以下条件时:

  1. 特征有明确的域划分
  2. 同一特征在不同域的语义差异大
  3. 数据量足够(能训练更多参数)

例子: 用户 ID 在"用户域"和"广告域"语义明显不同。

Q4: FFM 可以和 Embedding 结合吗?

A:

可以!有两种方式:

方式 1: 直接 Embedding

python 复制代码
# 为每个特征在每个域创建独立的 embedding
self.embeddings = nn.ModuleList([
    nn.ModuleList([
        nn.Embedding(feature_dims[i], k)
        for _ in range(num_fields)
    ])
    for i in range(num_features)
])

方式 2: FFM 特有的设计

使用传统的 one-hot 特征,FFM 的隐向量矩阵 V 本身就是可学习的。

Q5: FFM 的优化公式?

A:

FFM 无法使用 FM 的优化公式,因为:

  • FM 的二阶项: vᵢ · vⱼ
  • FFM 的二阶项: vᵢ,ⱼ · vⱼ,ᵢ (向量依赖于域)

所以 FFM 必须用双重循环,复杂度 O(n²k)。

Q6: FFM 的推理速度如何优化?

A:

FFM 的推理速度是主要瓶颈,优化方法:

  1. 稀疏优化: 只计算非零特征
  2. 量化: 将 float32 降为 float16
  3. 蒸馏: 用小模型学习 FFM 的知识
  4. 批量推理: 增加 batch size

Q7: 实际应用中如何选择 FM vs FFM?

A:

场景 推荐 原因
数据量小 FM FFM 容易过拟合
无明显域划分 FM FFM 优势不明显
语义域明确 FFM 充分利用域信息
追求极致效果 FFM 通常优于 FM
延迟敏感 FM FFM 推理慢

Q8: FFM 的扩展模型?

A:

1. DeepFFM

  • 类似 DeepFM
  • FFM + DNN

2. Field-weighted FM (FwFM)

  • 引入域权重
  • 区分不同域的重要性

3. Attentional FM (AFM)

  • 引入注意力机制
  • 学习特征交互的重要性

模型演进路线

复制代码
FM (2010)
   ↓ 同一特征只有一个向量
FFM (2016)
   ↓ 每个特征在每个域都有向量
DeepFM (2017)
   ↓ 加入 DNN 捕捉高阶交互
DeepFFM (2018)
   ↓ FFM + DNN
AFM (2017), FwFM (2019)
   ↓ 注意力机制、域权重

快速检查清单

理解 FFM,你应该能回答:

  • 解释 FFM 和 FM 的区别
  • 说明 Field (域) 的概念
  • 计算 FFM 的参数量
  • 理解 vᵢ,ⱼ 和 vⱼ,ᵢ 的含义
  • 知道 FFM 的适用场景
  • 了解 FFM 的局限性(参数多、推理慢)
  • 能实现简单的 FFM 模型

实现案例(CTR预测)

python 复制代码
import torch
import torch.nn as nn


class FFM(nn.Module):
    """
    Field-aware Factorization Machine (FFM)

    核心模型:
        FM 的扩展,每个特征在每个域(Field)都有独立的隐向量

    与 FM 的区别:
        FM:  每个特征只有 1 个隐向量
        FFM: 每个特征在每个域都有独立的隐向量

    交互项:
        FM:  vᵢ · vⱼ
        FFM: vᵢ,ⱼ · vⱼ,ᵢ  (特征 i 在域 j 的向量 · 特征 j 在域 i 的向量)
    """

    def __init__(self, feature_dims, field_dims, k=8):
        """
        Args:
            feature_dims: 每个特征的可能取值数
                          例如: [1000, 5, 3, 500, 4, 2]
            field_dims: 每个特征所属的域索引
                        例如: [0, 0, 0, 1, 1, 1]
                        前3个是用户域,后3个是广告域
            k: 隐向量维度
        """
        super().__init__()

        self.feature_dims = feature_dims
        self.field_dims = field_dims
        self.num_features = len(feature_dims)
        self.num_fields = max(field_dims) + 1
        self.k = k

        # ==================== 一阶部分 ====================
        # 偏置项
        self.w0 = nn.Parameter(torch.zeros(1))

        # 一阶权重: 每个特征一个权重
        self.w = nn.Parameter(torch.zeros(self.num_features))

        # ==================== FFM 核心:隐向量 ====================
        # 形状: (num_features, num_fields, k)
        # 每个特征在每个域都有一个 k 维向量
        self.V = nn.Parameter(torch.randn(self.num_features, self.num_fields, k) * 0.01)

    def forward(self, x):
        """
        前向传播

        Args:
            x: (batch_size, num_features) 特征索引

        Returns:
            logits: (batch_size, 1) 预测分数
        """
        batch_size = x.shape[0]

        # ==================== 一阶线性项 ====================
        # y₁ = w₀ + Σᵢ wᵢxᵢ
        # x 需要转为 float,因为输入是 Long 类型 (索引)
        # matmul 结果形状: (batch_size,),需要 unsqueeze 变成 (batch_size, 1)
        linear = self.w0 + torch.matmul(x.float(), self.w).unsqueeze(1)

        # ==================== 二阶交互项 ====================
        # FFM 的交互项: Σᵢⱼ vᵢ,ⱼ · vⱼ,ᵢ xᵢxⱼ

        second_order = torch.zeros(batch_size)

        # 将 x 转为 float,避免后续类型错误
        x_float = x.float()

        # 遍历所有特征对 (i < j)
        for i in range(self.num_features):
            for j in range(i + 1, self.num_features):

                # 获取特征 i 和 j 所属的域
                field_i = self.field_dims[i]
                field_j = self.field_dims[j]

                # ==================== FFM 关键设计 ====================
                # 特征 i 在域 j 中的隐向量
                # 形状: (k,)
                vi_fieldj = self.V[i, field_j]

                # 特征 j 在域 i 中的隐向量
                # 形状: (k,)
                vj_fieldi = self.V[j, field_i]

                # 两个向量的内积
                # 形状: (batch_size,)
                interaction = torch.sum(vi_fieldj * vj_fieldi, dim=-1)

                # 乘以特征值 xᵢxⱼ
                # 这里 x 是 one-hot 形式,所以 x[:, i] 就是 0 或 1
                second_order += x_float[:, i] * x_float[:, j] * interaction

        # 合并输出
        output = linear + second_order.view(-1, 1)

        return output


# ==================== 使用示例 ====================
if __name__ == '__main__':
    # ==================== 特征定义 ====================
    # Field 0: 用户相关特征
    # Field 1: 广告相关特征

    # 特征: [用户ID, 用户年龄, 用户性别, 广告ID, 广告类型, 广告价格等级]
    feature_dims = [1000, 5, 3, 500, 4, 2]

    # 每个特征所属的域
    #          [用户域, 用户域, 用户域, 广告域, 广告域, 广告域]
    field_dims = [0, 0, 0, 1, 1, 1]

    # 创建 FFM 模型
    model = FFM(
        feature_dims=feature_dims,
        field_dims=field_dims,
        k=8  # 隐向量维度
    )

    print('=== FFM 模型结构 ===')
    print(model)

    # ==================== 参数量分析 ====================
    total_params = sum(p.numel() for p in model.parameters())

    # 计算各部分参数量
    linear_params = 1 + model.num_features  # w0 + w
    ffm_params = model.num_features * model.num_fields * model.k

    print(f'\n参数量分析:')
    print(f'  总参数量: {total_params:,}')
    print(f'  一阶部分: {linear_params:,} (w0 + w)')
    print(f'  FFM 隐向量: {ffm_params:,} (特征×域×k)')
    print(f'    = {model.num_features} × {model.num_fields} × {model.k}')

    # 与 FM 对比
    fm_params = model.num_features * model.k
    print(f'\n与 FM 对比:')
    print(f'  FM 参数量: {fm_params + linear_params:,}')
    print(f'  FFM 参数量: {total_params:,}')
    print(f'  增加倍数: {total_params / (fm_params + linear_params):.2f}x')

    # ==================== 生成训练数据 ====================
    batch_size = 32

    # 生成随机训练样本
    # 每个样本: [用户ID, 年龄, 性别, 广告ID, 类型, 价格]
    x = torch.tensor([
        [torch.randint(0, dim, size=(1,)).item() for dim in feature_dims]
        for _ in range(batch_size)
    ])

    # 生成随机标签
    y = torch.randint(0, 2, (batch_size, 1), dtype=torch.float32)

    # ==================== 训练配置 ====================
    criterion = nn.BCEWithLogitsLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    print(f'\n=== 开始训练 ===')
    print(f'batch_size: {batch_size}')
    print(f'特征数: {model.num_features}')
    print(f'域数: {model.num_fields}')

    # ==================== 训练循环 ====================
    for epoch in range(1000):
        # 前向传播
        pred = model(x)

        # 计算损失
        loss = criterion(pred, y)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (epoch + 1) % 20 == 0:
            print(f'Epoch {epoch + 1:3d}, Loss: {loss.item():.6f}')

    # ==================== 预测示例 ====================
    model.eval()

    with torch.no_grad():
        # 生成测试样本
        test_x = torch.tensor([[
            torch.randint(0, dim, size=(1,)).item() for dim in feature_dims
        ]])

        # 预测
        logits = model(test_x)
        click_prob = torch.sigmoid(logits)

        print(f'\n=== 预测结果 ===')
        print(f'模型输出 (logits): {logits.item():.4f}')
        print(f'点击概率 (sigmoid): {click_prob.item():.4f}')

    # ==================== 可视化隐向量 ====================
    print(f'\n=== 隐向量示例 ===')
    print('特征 0 (用户ID) 在不同域的隐向量:')
    print(f'  在用户域 (Field 0): {model.V[0, 0][:3].tolist()}... (前3维)')
    print(f'  在广告域 (Field 1): {model.V[0, 1][:3].tolist()}... (前3维)')

    print('\n特征 3 (广告ID) 在不同域的隐向量:')
    print(f'  在用户域 (Field 0): {model.V[3, 0][:3].tolist()}... (前3维)')
    print(f'  在广告域 (Field 1): {model.V[3, 1][:3].tolist()}... (前3维)')

    print('\n说明: 同一特征在不同域有不同的向量表示')

参考资料


相关推荐
南宫萧幕1 小时前
HEV能量管理控制算法实战:从MPC/RL理论基础到Simulink闭环建模
算法·matlab·汽车·控制·pid
IT猿手1 小时前
SCI一区:章鱼优化算法(Octopus Optimization Algorithm, OOA)求解23个测试函数,出图丰富,提供完整MATLAB代码
开发语言·算法·matlab
superior tigre1 小时前
739 每日温度
算法·leetcode·职场和发展
忡黑梨1 小时前
eNSP_从直连到BGP全网互通
c语言·网络·数据结构·python·算法·网络安全
Run_Teenage2 小时前
算法:离散化模板
算法
乐迪信息2 小时前
乐迪信息:实时预警,秒级响应:船舶AI异常行为检测算法
大数据·人工智能·算法·安全·目标跟踪
用AI赚一点2 小时前
AI落地不是造大模型:从概念到落地的核心差异
人工智能·深度学习·机器学习
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 15. 三数之和 | C++ 排序+双指针
c++·算法·leetcode
fox_lht2 小时前
第十章 通用集合
开发语言·后端·算法·rust