今天的模型是DeepFM,这算是一个非常经典的模型了。在介绍这个模型之前先针对之前模型的不足进行一个小总结,这也是DeepFM模型提出来的一个原因。
CTR预测任务中, 高阶特征和低阶特征的学习都非常的重要。 推荐模型我们也学习了很多,基本上是从最简单的线性模型(LR), 到考虑低阶特征交叉的FM, 到考虑高度交叉的神经网络,再到两者都考虑的W&D组合模型。 其实这些模型又存在着自己的问题,也是后面模型不断需要进行改进的原理,主要有下面几点:
所以DeepFM也就应运而生了,老规矩先看一下知识脉络图:
DeepFM是2017年哈工大和华为公司联合提出的一个模型,用一句话来进行概括那就是将W&D中的wide部分由LR换成了FM。其实就是这么简单,但是仅仅知道这个还是远远不够的,这篇论文中还存在着很多细节,以及对推荐系统的理解。
首先先来看一下DeepFM具体的模型图吧:
这个模型也是两部分组成, 左边的FM+右边的DNN, 结构也不是很复杂,和W&D和DCN 模型长得都很像, DNN部分都没改,主要是Wide部分, W&D采用了LR, Deep&Cross采用了一个Cross的交叉网络,而这里采用了FM, 后两者都是针对于W&D的wide不具备自动特征组合能力的缺陷进行改进的。DeepFM的运算过程也比较简单, 左边的FM和右边的DNN共享相同的Embedding层的输入(这个要和W&D进行区分, 不单单是FM替换Wide那么简单,模型的输入模式上也进行了改进), 左侧的FM对不同特征域的Embedding进行了两两交叉(这里的Embedding向量当成了原FM的特征隐向量,也就是输入,对于FM的参数还需要通过梯度下降求解), 右边的DNN对特征Embedding进行了深度交叉, 最后将FM的输出与Deep部分的输出一起送入最后的输出层,参与最后的目标拟合, 公式如下:
上面还有个小细节不知道注意到了没有,就是红线和黑线的区别, 上面的黑线表示的是embedding这里的这些参数是通过深度神经网络这边进行更新的, 而更新好了之后, FM这端直接拿过来用, 也就是红线这部分是直接拿过来用就行了,不同进行参数更新这样的操作了。
关于输入,包括离散的分类特征域(如性别、地区等)和连续的数值特征域(如年龄等)。分类特征域一般通过one-hot或者multi-hot(如用户的浏览历史)进行处理后作为输入特征;数值特征域可以直接作为输入特征,也可以进行离散化进行one-hot编码后作为输入特征。对于每一个特征域,需要单独的进行Embedding操作,因为每个特征域几乎没有任何的关联,如性别和地区。而数值特征无需进行Embedding。与Wide&Deep不同的是,DeepFM中的Wide部分与Deep部分共享了输入特征,即Embedding向量。
Embedding layer长这样:
这个感觉不必多说了, 和FNN一样,这里依然是一个标准的FM模型,负责特征之间的低阶交互过程,FM的输出是Addition单元和Inner Product units的加和, Addition单元反映1阶特征各自的影响, 而Inner product代表2阶特征交互的影响。
与FNN不同, FM这里的隐向量参数是直接和神经网络的参数一样,都是当做学习参数一块学习的,这样就省去了FM的预训练过程,而是以端到端方式训练整个网络。 且这种训练方式还有个好处,就是作者发现通过高阶和低阶交互特征一块来进行反向传播更新参数反而会使得模型表现更佳,当然,这个也依赖于共享Embedding输入的策略。
问题一:什么是特征交互,为什么要进行特征交互?
问题二:为啥人工特征工程有挑战性?
第三个就是作者在这里对之前的PNN, FNN和W&D与自己提出的DeepFM进行了对比:
这里再简单的总结一下:
FNN模型: 预训练的方式增加了开销,模型能力受限于FM表征能力的上限,且只考虑了高阶交互
PNN模型:IPNN的内积计算非常复杂, OPNN的外积近似计算损失了很多信息,结果不稳定, 且同样忽视了低阶交互
W&D模型:虽然是考虑到了低阶和高阶交互,兼顾了模型的泛化和记忆,但是Wide部分输入需要专业的特征工程经验,作者这里还举了个例子,比如用户安装应用和应用推荐中曝光应用的交叉,这个需要一些强的业务经验。
所以DeepFM同时考虑了上面的这些问题, 用FM换掉了W&D的LR,并Wide部分和Deep部分通过低阶和高阶特征交互来影响特征表示,从而更精确地对特征表示进行建模的策略共享了特征Embedding, 解决了上面的问题。 一个小图进行总结:
下面依然是工业上的一些使用经验, 这个模型也是工业上常用的模型:
import torch
import torch.nn as nn
import torch.nn.functional as F
import warnings
warnings.filterwarnings('ignore')
class FM(nn.Module):
"""FM part"""
def __init__(self, latent_dim, fea_num):
"""
latent_dim: 各个离散特征隐向量的维度
input_shape: 这个最后离散特征embedding之后的拼接和dense拼接的总特征个数
"""
super(FM, self).__init__()
self.latent_dim = latent_dim
# 定义三个矩阵, 一个是全局偏置,一个是一阶权重矩阵, 一个是二阶交叉矩阵,注意这里的参数由于是可学习参数,需要用nn.Parameter进行定义
self.w0 = nn.Parameter(torch.zeros([1, ]))
self.w1 = nn.Parameter(torch.rand([fea_num, 1]))
self.w2 = nn.Parameter(torch.rand([fea_num, latent_dim]))
def forward(self, inputs):
# 一阶交叉
first_order = self.w0 + torch.mm(inputs, self.w1) # (samples_num, 1)
# 二阶交叉 这个用FM的最终化简公式
second_order = 1 / 2 * torch.sum(
torch.pow(torch.mm(inputs, self.w2), 2) - torch.mm(torch.pow(inputs, 2), torch.pow(self.w2, 2)),
dim=1,
keepdim=True
) # (samples_num, 1)
return first_order + second_order
class Dnn(nn.Module):
"""Dnn part"""
def __init__(self, hidden_units, dropout=0.):
"""
hidden_units: 列表, 每个元素表示每一层的神经单元个数, 比如[256, 128, 64], 两层网络, 第一层神经单元128, 第二层64, 第一个维度是输入维度
dropout = 0.
"""
super(Dnn, self).__init__()
self.dnn_network = nn.ModuleList(
[nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_units[:-1], hidden_units[1:]))])
self.dropout = nn.Dropout(dropout)
def forward(self, x):
for linear in self.dnn_network:
x = linear(x)
x = F.relu(x)
x = self.dropout(x)
return x
class DeepFM(nn.Module):
def __init__(self, feature_columns, hidden_units, dnn_dropout=0.):
"""
DeepFM:
:param feature_columns: 特征信息, 这个传入的是fea_cols
:param hidden_units: 隐藏单元个数, 一个列表的形式, 列表的长度代表层数, 每个元素代表每一层神经元个数
"""
super(DeepFM, self).__init__()
self.dense_feature_cols, self.sparse_feature_cols = feature_columns
# embedding
self.embed_layers = nn.ModuleDict({
'embed_' + str(i): nn.Embedding(num_embeddings=feat['feat_num'], embedding_dim=feat['embed_dim'])
for i, feat in enumerate(self.sparse_feature_cols)
})
# 这里要注意Pytorch的linear和tf的dense的不同之处, 前者的linear需要输入特征和输出特征维度, 而传入的hidden_units的第一个是第一层隐藏的神经单元个数,这里需要加个输入维度
self.fea_num = len(self.dense_feature_cols) + len(self.sparse_feature_cols) * self.sparse_feature_cols[0][
'embed_dim']
hidden_units.insert(0, self.fea_num)
self.fm = FM(self.sparse_feature_cols[0]['embed_dim'], self.fea_num)
self.dnn_network = Dnn(hidden_units, dnn_dropout)
self.nn_final_linear = nn.Linear(hidden_units[-1], 1)
def forward(self, x):
dense_inputs, sparse_inputs = x[:, :len(self.dense_feature_cols)], x[:, len(self.dense_feature_cols):]
sparse_inputs = sparse_inputs.long() # 转成long类型才能作为nn.embedding的输入
sparse_embeds = [self.embed_layers['embed_' + str(i)](sparse_inputs[:, i]) for i in
range(sparse_inputs.shape[1])]
sparse_embeds = torch.cat(sparse_embeds, dim=-1)
# 把离散特征和连续特征进行拼接作为FM和DNN的输入
x = torch.cat([sparse_embeds, dense_inputs], dim=-1)
# Wide
wide_outputs = self.fm(x)
# deep
deep_outputs = self.nn_final_linear(self.dnn_network(x))
# 模型的最后输出
outputs = F.sigmoid(torch.add(wide_outputs, deep_outputs))
return outputs
hidden_units = [128, 64, 32]
dnn_dropout = 0.
model = DeepFM(fea_cols, hidden_units, dnn_dropout)
参考:翻滚的小强