1. DeepFM模型:为什么说它是推荐系统的“瑞士军刀”?
如果你做过推荐系统或者广告点击率(CTR)预估,肯定对特征交叉这件事又爱又恨。爱的是,特征交叉往往能带来模型效果的显著提升;恨的是,这事儿太费人工了。回想一下,在传统的逻辑回归(LR)时代,我们是不是得绞尽脑汁地想:“用户的性别和商品品类交叉一下会不会有效?”“用户的历史点击序列和当前曝光item的类别做个组合呢?” 这些全靠业务经验和反复的AB测试来摸索,效率低不说,还很容易遗漏那些隐藏在数据深处的、意想不到的有效组合。
后来有了因子分解机(FM),它算是帮我们解放了一部分生产力。FM能自动学习所有特征的两两交叉(二阶交叉),通过为每个特征学习一个隐向量,用隐向量的内积来表示交叉的权重。这招在稀疏数据下特别管用,因为即使两个特征很少在一条样本里同时出现,FM也能通过它们各自的隐向量学到一些交互信息。但FM也有局限,它主要擅长二阶交叉,对于更高阶、更复杂的特征模式,就显得力不从心了。
再后来,深度学习席卷而来,大家自然想到用深度神经网络(DNN)来捕捉这些高阶的、非线性的特征交互。于是有了FNN(用FM预训练Embedding再喂给DNN)、PNN(在Embedding层后加了个Product层专门做交叉)。但这些模型要么依赖FM预训练,要么只关注高阶交叉,把FM原本擅长的低阶交叉给丢了。
直到2017年,华为和哈工大联合提出的DeepFM模型,才真正优雅地解决了这个问题。我第一次在项目里尝试DeepFM时,感觉就像找到了一把“瑞士军刀”——它把FM和DNN巧妙地结合在了一起,而且设计得非常精妙。
DeepFM的核心思想一句话就能说清:让一个FM组件和一个DNN组件共享同一个输入层和Embedding层,然后让它们各司其职。 FM组件专门负责捕捉低阶(主要是一阶和二阶)的特征交叉,DNN组件则通过多层非线性变换去挖掘高阶的特征交互模式。最后,把这两部分的输出加起来,过一个Sigmoid函数,就得到了最终的预测值。
这个设计有几个让我拍案叫绝的好处: 第一,完全端到端,告别特征工程。模型直接从原始特征(经过One-Hot等编码)学习,不需要我们手动设计任何交叉特征。FM部分自动搞定二阶交叉,DNN部分自动探索高阶交叉。 第二,共享Embedding,高效且一致。FM和DNN共用一套特征Embedding向量。这不仅大大减少了模型参数,更重要的是,它让低阶和高阶特征交互的学习是在同一个语义空间里进行的,相互促进,学到的特征表示更精准。 第三,结构清晰,便于理解和调优。FM和DNN两部分相对独立,我们可以分别分析它们对最终结果的贡献,调试起来也更方便。
我印象很深的是,在一次电商推荐场景的实验中,我们对比了LR、FM和DeepFM。LR需要精心设计几十个交叉特征,FM效果已经不错,但上线DeepFM后,线上AUC直接提升了接近1个百分点。别小看这1个点,在亿级流量的场景下,带来的业务收益是非常可观的。从那以后,DeepFM就成了我们团队CTR模型 baseline 的首选之一。
2. 庖丁解牛:DeepFM模型结构的三层拆解
光说思想不够过瘾,咱们得把DeepFM拆开揉碎了看。它的整体结构可以清晰地分为三层:输入层、共享Embedding层、以及并行的FM层与DNN层。下面我结合一个具体的例子,比如预测用户是否会点击一个游戏广告,来一步步拆解。
第一层:输入层 (Input Layer) 这一层负责接收原始特征。通常我们的特征分为两大类:
- 稀疏特征 (Sparse Features):比如用户ID、游戏ID、用户性别、所在城市、游戏类别等。这类特征取值很多,通常用One-Hot编码,会得到一个非常长且稀疏的向量。
- 稠密特征 (Dense Features):比如用户年龄、游戏历史点击率、当前时间(小时)等。这类特征本身就是数值,可以直接输入,或者经过归一化处理。
假设我们的特征有:
- 稀疏特征:用户ID (100万个)、游戏ID (10万个)、城市 (500个)
- 稠密特征:用户年龄、当前小时
在输入层,稀疏特征会转换成One-Hot编码的索引(比如用户ID=12345,就对应一个索引值),稠密特征则直接是数值。
第二层:共享Embedding层 (Shared Embedding Layer)
这是DeepFM设计最巧妙的地方,也是它高效的关键。对于每一个稀疏特征,我们都会创建一个Embedding矩阵。这个矩阵的大小是 (特征维度, 嵌入维度k)。比如“游戏ID”这个特征有10万个可能取值,嵌入维度k设为10,那么这个Embedding矩阵就是 (100000, 10)。
当一个具体的特征值(比如游戏ID=888)输入时,我们通过查表(Lookup)操作,从对应的Embedding矩阵中取出一个k维的稠密向量。请注意,这个查表操作得到的向量,会同时喂给接下来的FM组件和DNN组件。 这就是“共享”的含义。
对于稠密特征,通常有两种处理方式:一种是直接将其作为一个1维的“嵌入向量”(可以理解为权重)输入到FM的一阶部分;另一种是也可以将其转换成一个k维的向量(通过一个全连接层),然后和稀疏特征的Embedding一起输入到DNN。在经典的DeepFM实现中,通常采用第一种方式,稠密特征不参与Embedding层。
第三层:并行组件层 (Parallel Components) 共享Embedding层的输出,会兵分两路,分别进入FM组件和DNN组件。
-
FM组件 (Factorization Machine Component): 这个部分就是标准的FM模型。它接收所有特征的Embedding向量(对于稠密特征,是其原始值或变换后的值)。FM的输出由两部分组成:
- 一阶部分:就是各个特征的线性加权和,类似于逻辑回归。它学习每个特征单独的权重
w_i。 - 二阶部分:这是FM的精华。它计算所有特征两两之间的交互。但不是暴力计算,而是利用公式变形,用
O(kn)的复杂度高效计算所有特征隐向量的两两内积之和。具体公式我们稍后会在代码部分看到。 FM组件的最终输出就是一阶部分 + 二阶部分。
- 一阶部分:就是各个特征的线性加权和,类似于逻辑回归。它学习每个特征单独的权重
-
DNN组件 (Deep Neural Network Component): 这一路就是把所有特征的Embedding向量(对于稀疏特征)和稠密特征拼接(Concatenate)起来,形成一个长的稠密向量,然后输入到一个多层感知机(MLP)中。这个MLP通常由几层全连接层组成,中间使用ReLU等激活函数引入非线性。DNN通过多层非线性变换,能够自动学习特征之间复杂的高阶组合模式。
最终输出层 (Output Layer) FM组件的输出(一个标量)和DNN组件的输出(也是一个标量)会被直接相加。然后,这个加和的结果被送入一个Sigmoid函数,将值映射到(0,1)区间,作为最终的点击率预估值。
我们可以用一个简单的公式来概括整个模型:
y_hat = sigmoid( y_FM + y_DNN )
其中 y_FM 捕捉显式的低阶特征交互,y_DNN 捕捉隐式的高阶特征交互。两者互补,共同决策。
3. 手把手实现:用PyTorch从零搭建DeepFM
理论说得再漂亮,不如动手写一行代码。接下来,我就用PyTorch带大家一步步实现一个DeepFM模型。我们会基于公开的Criteo广告点击数据集来构建,这个数据集有13个连续特征和26个类别特征,非常适合做CTR预估的练习。
第一步:数据准备与预处理 Criteo数据集很大,我们通常先进行一些必要的预处理:
- 填充缺失值:连续特征用均值填充,类别特征用‘-1’表示缺失。
- 连续特征归一化:使用对数变换(log(x+1))或标准化(减均值除以标准差)来平滑数值分布。
- 类别特征编码:为每个类别特征构建一个词汇表(vocabulary),将字符串或整数映射到一个连续的ID。对于训练集中未出现过的类别(OOV),统一映射到一个特定的ID(比如0)。
- 生成数据加载器:将处理后的特征(稀疏特征ID和稠密特征值)以及标签(0/1)组织起来,用PyTorch的
DataLoader进行批量加载。
这里给出一个简化的数据预处理和加载的框架:
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import numpy as np
class CriteoDataset(Dataset):
def __init__(self, file_path, sparse_features, dense_features):
# 读取数据,假设已经是预处理好的DataFrame
self.data = pd.read_csv(file_path)
self.sparse_features = sparse_features # 列名列表
self.dense_features = dense_features # 列名列表
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
row = self.data.iloc[idx]
# 稀疏特征:转换为LongTensor,是特征在各自词汇表中的索引
sparse_data = [torch.tensor(row[feat], dtype=torch.long) for feat in self.sparse_features]
# 稠密特征:转换为FloatTensor
dense_data = torch.tensor(row[self.dense_features].values.astype(np.float32), dtype=torch.float)
label = torch.tensor(row['label'], dtype=torch.float)
return sparse_data, dense_data, label
# 假设我们已经有了特征字典,将特征值映射到ID
def build_feature_dict(data, features):
feat_dict = {}
for feat in features:
# 这里简单用unique值构建,实际中需要考虑OOV和频次过滤
unique_vals = data[feat].unique()
feat_dict[feat] = {v: i+1 for i, v in enumerate(unique_vals)} # 0留给OOV
feat_dict[feat]['vocab_size'] = len(unique_vals) + 1
return feat_dict
# 使用示例
# sparse_feat_dict = build_feature_dict(train_data, sparse_features)
# 然后在Dataset的__getitem__中,使用这个dict将原始值转换为ID
第二步:实现FM组件
FM组件的核心是高效计算二阶交叉项。我们回顾一下公式,对于二阶部分 sum_{i=1 to n} sum_{j=i+1 to n} <v_i, v_j> x_i x_j,可以通过数学变换优化为 0.5 * sum_{f=1 to k} ( (sum_i v_{i,f} x_i)^2 - sum_i v_{i,f}^2 x_i^2 )。这样复杂度就从 O(kn^2) 降到了 O(kn)。
import torch.nn as nn
import torch.nn.functional as F
class FM(nn.Module):
def __init__(self, sparse_feature_sizes, dense_dim, embed_dim):
"""
sparse_feature_sizes: 字典,key为特征名,value为该特征词汇表大小
dense_dim: 稠密特征的数量
embed_dim: 嵌入向量的维度k
"""
super(FM, self).__init__()
self.sparse_feature_sizes = sparse_feature_sizes
self.embed_dim = embed_dim
self.dense_dim = dense_dim
# 为每个稀疏特征创建Embedding层
self.embedding_layers = nn.ModuleDict()
for feat_name, vocab_size in sparse_feature_sizes.items():
self.embedding_layers[feat_name] = nn.Embedding(vocab_size, embed_dim)
# FM一阶部分:每个稀疏特征一个权重,每个稠密特征一个权重
self.sparse_linear = nn.ModuleDict()
for feat_name in sparse_feature_sizes.keys():
# 每个稀疏特征对应一个标量权重
self.sparse_linear[feat_name] = nn.Embedding(sparse_feature_sizes[feat_name], 1)
# 稠密特征的线性权重
self.dense_linear = nn.Linear(dense_dim, 1, bias=False)
# FM二阶部分:权重已经包含在embedding_layers的向量v_i中了
def forward(self, sparse_inputs, dense_inputs):
"""
sparse_inputs: 字典,key为特征名,value为LongTensor (batch_size, 1)
dense_inputs: FloatTensor (batch_size, dense_dim)
"""
batch_size = dense_inputs.size(0)
# ---------- 一阶部分 ----------
# 稀疏特征一阶项
sparse_linear_terms = []
for feat_name, input_idx in sparse_inputs.items():
# input_idx shape: (batch_size, 1)
# 查表得到权重 (batch_size, 1, 1) -> squeeze成 (batch_size,)
w = self.sparse_linear[feat_name](input_idx).squeeze(1) # (batch_size, 1)
sparse_linear_terms.append(w)
# 稠密特征一阶项
dense_linear_term = self.dense_linear(dense_inputs) # (batch_size, 1)
first_order = torch.sum(torch.cat(sparse_linear_terms, dim=1), dim=1, keepdim=True) + dense_linear_term
# ---------- 二阶部分 ----------
# 获取所有特征的embedding向量
# 对于稀疏特征:通过Embedding层查找
sparse_embeddings = []
for feat_name, input_idx in sparse_inputs.items():
# input_idx shape: (batch_size, 1)
emb = self.embedding_layers[feat_name](input_idx).squeeze(1) # (batch_size, embed_dim)
sparse_embeddings.append(emb)
# 对于稠密特征:我们需要将其“嵌入”到embed_dim维。
# 一种常见做法是使用一个线性层将每个稠密特征映射到embed_dim维,但原论文中稠密特征似乎不参与二阶交叉?
# 实际上,在经典实现中,稠密特征通常只参与一阶部分。为了简化,我们这里假设稠密特征不参与FM二阶交叉。
# 将所有稀疏特征的embedding堆叠起来
# 注意:如果稠密特征要参与,需要先扩展维度并乘上一个权重矩阵
all_embeddings = torch.stack(sparse_embeddings, dim=1) # (batch_size, num_sparse, embed_dim)
# 计算 sum_i v_i * x_i。由于我们的x_i对于One-Hot是1,所以就是sum_i v_i
# 但更通用的写法是考虑特征值x_i(对于稠密特征)。这里我们假设输入已经是embedding,x_i隐含在查表过程中(为1)。
sum_embed = torch.sum(all_embeddings, dim=1) # (batch_size, embed_dim)
sum_embed_square = torch.pow(sum_embed, 2) # (batch_size, embed_dim)
# 计算 sum_i v_i^2
square_embed = torch.pow(all_embeddings, 2) # (batch_size, num_sparse, embed_dim)
square_sum_embed = torch.sum(square_embed, dim=1) # (batch_size, embed_dim)
# 二阶交叉项:0.5 * sum( (sum_i v_i)^2 - sum_i v_i^2 )
second_order = 0.5 * torch.sum(sum_embed_square - square_sum_embed, dim=1, keepdim=True) # (batch_size, 1)
return first_order, second_order, all_embeddings # 返回embedding供DNN使用
第三步:实现DNN组件 DNN部分就是一个标准的全连接网络,它的输入是所有特征Embedding拼接起来的向量。
class DNN(nn.Module):
def __init__(self, input_dim, hidden_dims, dropout=0.5):
"""
input_dim: 输入维度(所有稀疏特征embedding展平后的长度 + 稠密特征数)
hidden_dims: 列表,每层的神经元数量,例如 [256, 128, 64]
dropout: Dropout比率
"""
super(DNN, self).__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(nn.BatchNorm1d(hidden_dim)) # 可选,稳定训练
layers.append(nn.ReLU())
layers.append(nn.Dropout(p=dropout))
prev_dim = hidden_dim
# 输出层,输出一个标量
layers.append(nn.Linear(prev_dim, 1))
self.mlp = nn.Sequential(*layers)
def forward(self, x):
# x shape: (batch_size, input_dim)
return self.mlp(x)
第四步:组装完整的DeepFM模型 现在,我们把FM和DNN组合起来,并确保它们共享Embedding。
class DeepFM(nn.Module):
def __init__(self, sparse_feature_sizes, dense_dim, embed_dim, dnn_hidden_dims):
super(DeepFM, self).__init__()
self.fm = FM(sparse_feature_sizes, dense_dim, embed_dim)
# 计算DNN的输入维度:所有稀疏特征embedding展平 + 稠密特征
num_sparse = len(sparse_feature_sizes)
dnn_input_dim = num_sparse * embed_dim + dense_dim
self.dnn = DNN(dnn_input_dim, dnn_hidden_dims)
# 最终的Sigmoid输出层
self.sigmoid = nn.Sigmoid()
def forward(self, sparse_inputs, dense_inputs):
# 1. FM部分
first_order, second_order, sparse_embeddings = self.fm(sparse_inputs, dense_inputs)
fm_output = first_order + second_order # (batch_size, 1)
# 2. 准备DNN的输入:将稀疏特征embedding展平,并与稠密特征拼接
batch_size = dense_inputs.size(0)
# sparse_embeddings 是一个列表,每个元素是(batch_size, embed_dim)
# 我们需要将其在特征维度上拼接
sparse_embed_flat = torch.cat([emb for emb in sparse_embeddings], dim=1) # (batch_size, num_sparse*embed_dim)
dnn_input = torch.cat([sparse_embed_flat, dense_inputs], dim=1) # (batch_size, dnn_input_dim)
# 3. DNN部分
dnn_output = self.dnn(dnn_input) # (batch_size, 1)
# 4. 结合两部分输出
combined_output = fm_output + dnn_output
# 5. Sigmoid激活得到最终概率
output = self.sigmoid(combined_output)
return output.squeeze() # (batch_size,)
第五步:模型训练与评估 模型搭建好后,就是常规的训练流程:定义损失函数(BCELoss)、优化器(如Adam),然后循环迭代。
def train_one_epoch(model, dataloader, criterion, optimizer, device):
model.train()
total_loss = 0
for batch_idx, (sparse_data, dense_data, labels) in enumerate(dataloader):
# 将数据移动到设备
sparse_inputs = {name: data.to(device) for name, data in zip(sparse_feature_names, sparse_data)}
dense_data = dense_data.to(device)
labels = labels.to(device)
optimizer.zero_grad()
predictions = model(sparse_inputs, dense_data)
loss = criterion(predictions, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
# 初始化模型、损失函数、优化器
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = DeepFM(sparse_feature_sizes, dense_dim=13, embed_dim=10, dnn_hidden_dims=[256, 128, 64]).to(device)
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 训练循环
num_epochs = 10
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
val_auc = evaluate(model, val_loader, device) # 需要实现evaluate函数计算AUC
print(f'Epoch {epoch+1}, Train Loss: {train_loss:.4f}, Val AUC: {val_auc:.4f}')
在实际项目中,你还需要实现评估函数(计算AUC、LogLoss等)、学习率调度、早停等策略。代码里我简化了稠密特征在FM二阶部分和Embedding层的处理,真实的工业级实现会更复杂,需要考虑缺失值、归一化、以及如何将稠密特征也融入Embedding体系。但上面的代码骨架已经清晰地展示了DeepFM的核心实现逻辑。
4. 实战技巧与避坑指南:让DeepFM在你的场景中真正work
纸上得来终觉浅,绝知此事要躬行。根据我多年的实战经验,直接套用上面的代码到生产环境,很可能效果不尽如人意。下面分享几个让DeepFM模型发挥出真正威力的关键技巧和常见“坑点”。
技巧一:特征工程依然重要,但方向变了 DeepFM号称“无需特征工程”,但这更多是指不需要手动构造二阶交叉特征。对于特征本身的处理,依然至关重要。
- 连续特征分桶:这是最常用且有效的一招。像年龄、价格、点击次数这类连续值,直接输入模型效果往往不好。更好的做法是将其离散化成分桶(Bucketization)。例如,将年龄分为“<18”, “18-25”, “26-35”, “>35”几个桶,然后当作类别特征处理,赋予其Embedding。这相当于给模型引入了非线性先验。我通常使用等频分桶或基于业务知识的分桶。
- 序列特征处理:用户历史行为序列(如最近点击的10个商品ID)是强特征。简单的平均池化(Mean Pooling)会损失序列信息。可以尝试使用注意力机制(如Target Attention)来对历史行为加权,或者使用GRU/LSTM来编码序列,将最终状态作为特征输入。不过这会增加模型复杂度,需要权衡。
- 多值类别特征:一个用户可能有多个兴趣标签。处理方式可以是:1) 将多个标签ID分别作为独立的特征输入(但会导致特征维度爆炸);2) 对多个标签的Embedding进行求和或平均池化;3) 使用更复杂的聚合方式,如注意力池化。
技巧二:Embedding维度与初始化
- 维度选择:Embedding维度
k是一个超参数。不是越大越好!对于高频特征(如热门商品ID),较大的k可以容纳更多信息;对于低频特征,太大的k容易过拟合。一个实践经验是,可以按照特征取值频率的平方根或对数来动态设置不同特征的k,但这实现复杂。通常,对所有稀疏特征使用统一的k(如8, 10, 16)并进行网格搜索是更实际的做法。 - 初始化:Embedding层的初始化很重要。默认的均匀分布初始化可能不是最优的。可以尝试使用Xavier或Kaiming初始化。在一些工作中,用预训练的FM模型得到的隐向量来初始化Embedding,能加速收敛,但DeepFM本身是端到端训练,这个收益可能不大。
技巧三:DNN结构设计与训练技巧
- 网络深度与宽度:DNN部分通常3-4层就足够了。过深的网络在CTR任务中容易过拟合,且训练更慢。每层的神经元数量可以从输入层逐渐递减(如512->256->128->64),形成一个“金字塔”结构。使用Dropout(如0.3-0.5)和BatchNorm是防止过拟合、稳定训练的有效手段。
- 激活函数:最常用的是ReLU及其变种(如Leaky ReLU)。输出层一定是Sigmoid,因为我们是二分类概率预测。
- 优化器与学习率:Adam优化器是默认选择,它的自适应学习率特性很友好。学习率可以从1e-3开始尝试,配合学习率衰减(如ReduceLROnPlateau)。对于非常大的数据集,也可以使用带动量的SGD,有时能收敛到更优的点。
- 负采样与样本权重:CTR数据极度不平衡(点击样本很少)。如果正负样本比例悬殊(如1:100),可以考虑对负样本进行采样,或者给正样本更高的损失权重,让模型更关注正例。
常见“坑点”与排查
-
模型不收敛或AUC很低:
- 检查特征输入:确认稀疏特征的ID是否在词汇表范围内,有没有OOV问题。检查稠密特征是否有异常值(如NaN或极大值),做好归一化或截断。
- 检查损失函数:二分类任务用BCELoss,确保预测值在(0,1)内,标签是0或1。
- 降低学习率:这是最常用的手段。先用一个很小的学习率(如1e-4)看loss是否下降。
- 梯度裁剪:如果遇到梯度爆炸,可以设置梯度裁剪(
torch.nn.utils.clip_grad_norm_)。
-
过拟合:
- 增加正则化:除了Dropout,可以在FM的线性权重和DNN的权重上添加L2正则化(权重衰减)。
- 早停:在验证集AUC不再提升时停止训练。
- 简化模型:减少DNN层数、神经元数,或降低Embedding维度。
-
线上服务性能压力:
- Embedding查表是瓶颈:DeepFM的线上推理,大部分时间花在从庞大的Embedding表里查找向量。可以考虑:
- 模型量化:将Embedding和模型权重从FP32转为INT8,能大幅减少内存占用和加速计算。
- 模型裁剪:对Embedding表进行剪枝,移除低频特征对应的行。
- 高性能推理库:使用TensorRT、ONNX Runtime等对模型进行优化和加速。
- Embedding查表是瓶颈:DeepFM的线上推理,大部分时间花在从庞大的Embedding表里查找向量。可以考虑:
一个简单的效果对比实验 在你自己的数据集上,我强烈建议你跑一个简单的对比实验:
- Baseline: 逻辑回归(LR) + 你认为最重要的几个手工交叉特征。
- 对比1: 纯FM模型。
- 对比2: 纯DNN模型(Embedding+MLP)。
- 最终: DeepFM模型。 记录下它们在验证集上的AUC和LogLoss。你很可能观察到:LR < FM ≈ DNN < DeepFM。这个实验能直观地让你感受到特征自动交叉以及高低阶组合的威力。
5. 超越DeepFM:演进与相关模型浅析
DeepFM无疑是一个里程碑式的模型,但它并非终点。了解它的“兄弟姐妹”和后续演进,能帮助你在不同场景下做出更合适的选择。
Wide & Deep vs DeepFM 这是最直接的对比。Wide & Deep的Wide部分是一个线性模型(LR),需要人工设计交叉特征。DeepFM用FM替换了LR,实现了自动二阶交叉。因此,在缺乏特征工程经验或特征组合空间巨大的场景下,DeepFM优势明显。但在一些强业务逻辑的场景,已知少数几个交叉特征极其重要,那么Wide & Deep中手动将这些特征加入Wide部分,可能会带来更稳定、可解释性更强的效果。
xDeepFM:显式高阶交叉 DeepFM的DNN部分学习的是隐式的高阶交叉,我们很难知道它具体学到了什么组合。xDeepFM提出了压缩交互网络(CIN),以一种显式(explicit)且向量级(vector-wise)的方式学习高阶特征交互。CIN的每一层都会与初始的Embedding层进行交叉,生成新的交互特征图。理论上,xDeepFM能更可控地学习有界阶数的特征交互。但CIN计算复杂度较高,在实际大规模应用中需要仔细评估效率。
Deep & Cross Network (DCN) 谷歌提出的DCN模型,其核心是Cross网络。这个网络结构特殊,每一层的输出是上一层的输出与原始输入的交叉(外积)再加上上一层输出本身。通过这种设计,Cross网络可以高效地学习特定阶数的特征交叉。DCN可以看作是Wide部分的一种自动化改进。与DeepFM相比,DCN的Cross网络专注于显式交叉,且交叉方式不同于FM的内积。
AutoInt:用注意力机制学习交叉 AutoInt模型使用多头自注意力机制来学习特征之间的交互。每个特征通过注意力权重与其他所有特征进行交互,模型可以学习到哪些特征组合更重要。这种方法非常灵活,能捕捉复杂的非线性交叉,但计算量相对较大,解释性也弱于FM。
如何选择?—— 实战经验谈
- 首选DeepFM:如果你的团队刚开始从传统模型转向深度学习推荐模型,DeepFM是一个稳健且强大的起点。它平衡了效果、复杂度和实现难度,在大多数CTR/CVR预估任务中都能取得不错的效果。
- 考虑xDeepFM或DCN:当你对模型有更高要求,并且业务场景中特征交叉的逻辑非常复杂、重要,同时你有足够的计算资源时,可以尝试这些更先进的模型。它们在某些数据集上报告了比DeepFM更好的效果。
- 尝试AutoInt:如果你的特征间关系非常复杂,且你怀疑注意力机制能更好地建模这种关系(比如不同特征的重要性高度依赖于上下文),可以实验AutoInt。
- 永远不要忘记线下实验:没有放之四海而皆准的“最佳模型”。最靠谱的方法是在你的具体数据集上,用严格的A/B测试框架去对比这些模型。离线指标(AUC、LogLoss)是参考,但最终一定要看线上AB实验的CTR、CVR、收入等核心业务指标。
模型在不断演进,但核心思想是相通的:如何更高效、更智能地挖掘海量特征中隐藏的关联。DeepFM以其简洁优雅的架构,为我们提供了一个兼具强大能力和实用性的优秀范本。掌握它,就等于握住了打开深度学习推荐系统大门的一把关键钥匙。剩下的,就是在你的业务数据中不断实践、调优和迭代了。记住,好的模型是基础,但贴合业务的数据理解和特征洞察,才是最终效果的保证。
DeepFM实战:从理论到代码实现的全解析&spm=1001.2101.3001.5002&articleId=153250316&d=1&t=3&u=9d1f25dfb4544128b0fb4d3e60b51bdb)
4万+

被折叠的 条评论
为什么被折叠?



