xDeepFM:显式与隐式特征交互融合
总阅读量 加载中... 次
xDeepFM(eXtreme Deep Factorization Machine)发表于 KDD 2018,论文题为 xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems [1]。
xDeepFM 的核心是压缩交互网络 CIN:在保留字段嵌入向量结构的同时,逐层构造显式高阶交互,并与线性模型、深度网络联合学习,用于点击率预测等精排任务。
1. 背景
点击率预测常常依赖特征组合。例如,用户所在城市、商品类别和访问时段共同影响点击意愿。单独增加某个特征的权重,难以描述这种条件关系。
DeepFM 使用 FM 建模显式二阶交互,并通过深度网络学习隐式高阶关系。深度分支能够表达复杂函数,但没有为每一层指定明确的交互阶数。DCN 则通过递推交叉层构造有界阶数的交互,不过计算建立在拼接向量的坐标上。
xDeepFM 引入 CIN,将一个字段的嵌入向量作为交互单元。它让上一层特征向量与输入字段向量做逐元素乘法,再把大量交互向量压缩为少量输出向量。堆叠多层后,模型可以在受控宽度下构造更高阶的组合。
2. 模型架构
2.1 整体网络结构
xDeepFM架构图
模型包含三个并行分支:
- 线性分支:直接对输入特征加权,学习一阶贡献。
- 压缩交互网络:显式构造向量级高阶交互,对各层输出池化后参与预测。
- 深度网络:将字段嵌入展平,通过多层非线性变换学习隐式关系。
CIN 与深度网络共享输入嵌入,三路分数相加后经过 Sigmoid 得到点击概率。线性分支使用自己的标量权重,不是对嵌入向量简单求和。
2.2 输入层
设输入有
单值类别字段通过查表得到
CIN 的各字段必须具有相同的嵌入维度,才能沿对应坐标相乘。深度分支接收
2.3 压缩交互网络
(1)交互与压缩
令第
其中
计算过程可以分为两步。首先构造交互张量:
然后对字段对所在的两个轴加权求和:
“压缩”指将
(2)向量级交互的含义
同一个字段对的权重
把交互张量变形为
(3)交互阶数如何增加
第一层把两个输入向量相乘,得到二阶交互。第二层再乘一个输入向量,例如:
因此,在不加入偏置和非线性激活的基础 CIN 中,第
这里的阶数针对嵌入坐标的乘积,不等同于包含同样数量的不同字段。求和允许重复字段,例如
此外,高阶系数由多层权重的乘积共同决定,并非所有高阶项都有独立参数。若在交叉层加入非线性激活,就不能继续直接套用有限次多项式的解释;加入偏置也会改变齐次性。下文实现采用无偏置、无激活的交叉层。
2.4 多层池化与输出
对每层的每个向量沿嵌入维度求和:
然后拼接各层池化结果:
池化发生在输出分支,传给下一层的仍是完整矩阵
收集所有层输出,使预测层能够同时使用不同阶数的信号。下面代码将每层全部向量用于继续交互及输出,不额外划分两组通道。
2.5 线性分支与深度分支
设原始编码特征为
对于单值类别字段,可以为每个类别学习一个标量,再跨字段求和。它等价于在独热编码上应用线性模型,无需显式构造稀疏大向量。
深度网络从展平后的嵌入开始:
最终预测为:
三路相加的是分数,而不是分别归一化后的概率。训练时直接使用分数
BCEWithLogitsLoss,预测时再计算 Sigmoid。
2.6 与相关模型的区别
| 模型 | 显式交互部分 | 深度分支 | 主要特点 |
|---|---|---|---|
| FM | 字段嵌入内积构造二阶项 | 无 | 低成本建模二阶关系 |
| DeepFM | FM 二阶项 | 有 | 二阶显式交互与隐式交互共享嵌入 |
| DCN | 拼接向量上的递推交叉 | 有 | 逐层提高显式交互阶数 |
| xDeepFM | 字段向量逐元素乘积与压缩 | 有 | 跨嵌入坐标共享交互权重,汇集多阶输出 |
FM 的交互项是
2.7 参数量与计算成本
无偏置的第
这里不包含共享嵌入、深度分支、线性分支和全局偏置。CIN 交叉权重的数量不依赖
直接构造交互张量时,第
例如
2.8 损失函数
点击率预测使用二元交叉熵:
还可以针对选定参数施加正则化。评估时同时观察 AUC 与 LogLoss,前者反映区分能力,后者衡量概率预测误差。下面示例只实现交叉熵训练,不把模拟数据上的损失作为模型效果结论。
3. 模型实现
3.1 核心代码实现
下面使用 PyTorch 实现基础 CIN 和完整 xDeepFM。为了清楚展示字段结构,输入限定为每个字段一个类别 ID;各字段 ID 从 0 开始,分别使用自己的嵌入表。
(1)压缩交互网络
import torch
import torch.nn as nn
class CIN(nn.Module):
def __init__(self, num_fields, layer_sizes=(32, 32)):
super().__init__()
if num_fields < 1 or not layer_sizes or any(h < 1 for h in layer_sizes):
raise ValueError("字段数与各层宽度必须为正整数")
self.layers = nn.ModuleList()
previous = num_fields
for width in layer_sizes:
# 在所有嵌入坐标上共享相同的通道混合权重。
self.layers.append(nn.Conv1d(
previous * num_fields, width, kernel_size=1, bias=False
))
previous = width
self.output_dim = sum(layer_sizes)
def forward(self, x0):
# x0: [B, M, D];hidden: [B, H, D]
hidden = x0
pooled = []
for layer in self.layers:
products = torch.einsum("bhd,bmd->bhmd", hidden, x0)
products = products.flatten(1, 2) # [B, H*M, D]
hidden = layer(products)
pooled.append(hidden.sum(dim=-1))
return torch.cat(pooled, dim=1)
这里没有在交叉层中加入激活与偏置,与前面的阶数推导保持一致。flatten(1, 2) 将上一层通道和输入字段合并,不能把嵌入坐标也合并到通道中。
(2)完整模型
class XDeepFM(nn.Module):
def __init__(self, field_dims, embedding_dim=16,
cin_sizes=(32, 32), deep_sizes=(128, 64)):
super().__init__()
if not field_dims or any(n < 1 for n in field_dims):
raise ValueError("每个字段的类别数量必须为正整数")
if embedding_dim < 1 or not deep_sizes or any(h < 1 for h in deep_sizes):
raise ValueError("嵌入维度与深度网络宽度必须为正整数")
self.embeddings = nn.ModuleList([
nn.Embedding(n, embedding_dim) for n in field_dims
])
self.linear_embeddings = nn.ModuleList([
nn.Embedding(n, 1) for n in field_dims
])
for embedding in self.embeddings:
nn.init.normal_(embedding.weight, std=0.01)
for embedding in self.linear_embeddings:
nn.init.zeros_(embedding.weight)
self.cin = CIN(len(field_dims), cin_sizes)
self.cin_head = nn.Linear(self.cin.output_dim, 1, bias=False)
layers = []
width = len(field_dims) * embedding_dim
for next_width in deep_sizes:
layers.extend([nn.Linear(width, next_width), nn.ReLU()])
width = next_width
self.deep = nn.Sequential(*layers)
self.deep_head = nn.Linear(width, 1, bias=False)
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, ids):
if ids.ndim != 2 or ids.shape[1] != len(self.embeddings):
raise ValueError("输入形状必须为 [批量大小, 字段数]")
x0 = torch.stack([
embedding(ids[:, i])
for i, embedding in enumerate(self.embeddings)
], dim=1)
linear = torch.stack([
embedding(ids[:, i])
for i, embedding in enumerate(self.linear_embeddings)
], dim=1).sum(dim=1)
cin_score = self.cin_head(self.cin(x0))
deep_score = self.deep_head(self.deep(x0.flatten(1)))
return (linear + cin_score + deep_score + self.bias).squeeze(-1)
三路输出都是 [B, 1],最后仅移除最后一个维度,因此批量大小为 1 时仍返回 [1]。CIN 与深度网络使用同一份 x0,反向传播时共同更新字段嵌入。
(3)前向预测与训练示例
torch.manual_seed(42)
field_dims = [100, 50, 20, 10]
model = XDeepFM(field_dims, embedding_dim=8,
cin_sizes=(16, 16), deep_sizes=(32, 16))
ids = torch.stack([torch.randint(n, (16,)) for n in field_dims], dim=1)
labels = torch.randint(0, 2, (16,)).float()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
model.train()
logits = model(ids)
loss = nn.BCEWithLogitsLoss()(logits, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
probabilities = torch.sigmoid(model(ids))
print("Prediction shape:", probabilities.shape)
print("Training loss:", loss.item())
3.2 实验与工程注意事项
- 固定输入再比较模型:在相同数据划分、字段编码和训练预算下比较 DeepFM、纯 CIN 与 xDeepFM,并通过移除分支检验各部分收益。
- 分别调整深度和宽度:CIN 深度影响交互阶数,宽度影响可保留的交互向量数量;两者都会改变计算成本,增加层数并不保证效果提升。
- 关注中间张量:字段较多时,显式交互张量可能成为显存瓶颈。可以考虑分块收缩或低秩权重,但需要验证结果一致性与真实延迟。
- 检查数值尺度:重复乘法会放大输入尺度差异,也可能让高阶信号衰减。应检查归一化、嵌入初始化、各分支输出和梯度分布。
- 防止数据泄漏:根据业务时间划分训练与验证数据,避免将未来行为、全量统计或标签相关信息引入特征。
4. 总结
xDeepFM 通过 CIN 将显式特征交互从二阶扩展到更高阶。它保留字段向量结构,在对应嵌入坐标上相乘,再用跨坐标共享的权重压缩交互向量,最终汇集不同层的输出。
理解 CIN 的关键在于区分字段轴、嵌入坐标轴和输出通道轴:交互发生在字段向量之间,权重在嵌入坐标上共享,压缩控制输出通道数量。结合线性分支与深度网络后,模型同时学习一阶贡献、显式高阶交互和隐式非线性关系。
5. 参考文献
[1] Lian J, Zhou X, Zhang F, et al. xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems. KDD, 2018.
[2] Guo H, Tang R, Ye Y, et al. DeepFM: A Factorization-Machine based Neural Network for CTR Prediction. IJCAI, 2017.
[3] Wang R, Fu B, Fu G, Wang M. Deep & Cross Network for Ad Click Predictions. AdKDD, 2017.
[4] Rendle S. Factorization Machines. ICDM, 2010.