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说明: 同一特征在不同域有不同的向量表示')

参考资料


相关推荐
江畔柳前堤18 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore19 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考21 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX21 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面1 天前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号1 天前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽1 天前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI1 天前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者1 天前
征程6工具链模型X86推理方式说明
算法
地平线开发者1 天前
【征程6】校准量化中HistogramObserver解析
算法