本文要介绍的是FNN模型,出自于张伟楠老师于2016年发表的论文《Deep Learning over Multi-field Categorical Data》。
论文提出了两种深度学习模型,分别叫做FNN(Factorisation Machine supported Neural Network)和SNN(Sampling-based Neural Network),本文只介绍FNN模型。
其实学习FNN模型之前,强烈建议先学习FM模型,因为FNN模型其实可以看做是由一个FM模型和一个MLP组成的。
FM的引入是为了高效地将输入稀疏特征映射到稠密特征,从而加快FNN模型的训练。
介绍
用户行为预测在许多网页级应用上发挥着重要的作用,比如网页搜索、推荐系统、赞助搜索、以及广告展示等。
在在线广告中,举个例子,对目标用户群体的定位能力是区别于传统线下广告的关键优势。所有的定位技术,都依赖于预测是否特定的用户认为这个广告是相关的,给出用户在特定的场景中点击的概率。
目前大部分的CTR预测都是线性模型,如逻辑回归,朴素贝叶斯,FTRL逻辑回归和贝叶斯逻辑回归等。所有的这些都是基于使用one-hot编码的大量稀疏特征。线性模型简单,有效,但是性能偏差,因为无法学习到特征之间的相互关系。
非线性模型可以通过特征间的组合提高模型的能力。如FMs,将二值化的特征映射成连续的低维空间,通过内积获取特征间的相互关系。GBDT梯度提升树,通过树的构建过程,自动的学习特征的组合。
然而,这些方法并不能利用所有可能的组合。此外,许多模型仍然需要依靠手工进行特征工程,来决定如何进行特征的组合。另一个问题是,已有的CTR模型在对复杂数据间的潜在的模式上的表达能力是非常有限的。所以,它们的泛化能力是非常受限的。
深度学习在CV和NLP上取得了很大的成功,在非监督的训练中,神经网络可以从原始的特征中学习到高维的特征表示,这个能力也可以用在CTR上。在CTR中,大部分的输入特征是来自各个领域的,而且是离散的类别特征。比如用户所在的城市信息(London,Paris,Beijing),设备类型(PC,Mobile),广告类别(Sports, Electronics)等等,并且特征之间的相互依赖是未知的。
因此,我们抱着极大地兴趣想了解一下,深度学习方法是如何在大规模的多特征域的离散类别数据上通过学习特征表示来提高CTR任务的估计准确度的。然而,大规模的输入特征空间需要调整大量的参数,这毫无疑问在计算上是非常昂贵的。与物理世界的图像或者音频数据不同,在推荐系统或者在线搜索等系统中,输入数据都是及其稀疏的。
举个例子,假如我们有100万个二进制输入特征,以及100个隐层,那么这大概需要1亿个连接才能构建第一层神经网络。
模型
先直接给出论文中的FNN模型图,如下:
FNN模型图
上图的右侧已经标明了每一层的含义,下面从模型自顶向下的角度详细解释一下。
- 输出层
模型的输出是一个实数作为预测的CTR,即特定用户在指定上下文的条件下点击给定广告的概率。
这里给出FM的方程:
为了进一步展示FM与FNN的关系,这里再补上一张图加以说明:
FM和FNN Embedding层各参数的对应关系
使用预训练的FM来初始化FNN模型的第一层参数可以有效地学习特征表示,并且绕开了高维二值输入带来的计算复杂度高问题。更进一步,隐含层的权重(除了FM层)可以通过预训练的RBM来进行初始化。FM的权重可以通过SGD来进行更新,我们只需要更新那些不为0的单元,这样可以减少大量的计算。通过预训练FM层和其他的层进行初始化之后,再通过监督学习的方法进行微调,使用交叉熵的损失函数:
使用反向传播的链式法则,FNN模型(包含FM)的权重可以被高效地更新,举个例子,FM层的权重可以通过下述公式来进行更新:
FNN模型分析
FNN模型的特点:
- 采用FM预训练得到的隐含层及其权重作为神经网络的第一层的初始值,之后再不断堆叠全连接层,最终输出预测的点击率。
- 可以将FNN理解成一种特殊的embedding+MLP,其要求第一层嵌入后的各特征域特征维度一致,并且嵌入权重的初始化是FM预训练好的。
- 这不是一个端到端的训练过程,有贪心训练的思路。而且如果不考虑预训练过程,模型网络结构也没有考虑低阶特征组合。
FNN模型的优缺点:
-
优点
1.引入DNN对特征进行更高阶组合,减少特征工程,能在一定程度上增强FM的学习能力,这种尝试为后续深度推荐模型的发展提供了新的思路。 -
缺点
1.两阶段训练模式,在应用过程中不方便,且模型能力受限于FM表征能力的上限。
2.FNN专注于高阶组合特征,但是却没有对低阶特征进行建模。
完整源码&技术交流
技术要学会分享、交流,不建议闭门造车。一个人走的很快、一堆人可以走的更远。
文章中的完整源码、资料、数据、技术交流提升, 均可加知识星球交流群获取,群友已超过2000人,添加时切记的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。
方式①、添加微信号:mlc2060,备注:来自 获取推荐资料
方式②、微信搜索公众号:机器学习社区,后台回复:推荐资料
部分代码
模型部分代码:
import torch
import torch.nn as nn
from BaseModel.basemodel import BaseModel
class FNN(BaseModel):
def __init__(self, config, dense_features_cols, sparse_features_cols):
super(FNN, self).__init__(config)
# 稠密和稀疏特征的数量
self.num_dense_feature = dense_features_cols.__len__()
self.num_sparse_feature = sparse_features_cols.__len__()
# FNN的线性部分,对应 ∑WiXi
self.embedding_layers_1 = nn.ModuleList([
nn.Embedding(num_embeddings=feat_dim, embedding_dim=1)
for feat_dim in sparse_features_cols
])
# FNN的Interaction部分,对应∑∑<Vi,Vj>XiXj
self.embedding_layers_2 = nn.ModuleList([
nn.Embedding(num_embeddings=feat_dim, embedding_dim=config['embed_dim'])
for feat_dim in sparse_features_cols
])
# FNN的DNN部分
self.hidden_layers = [self.num_dense_feature + self.num_sparse_feature*(config['embed_dim']+1)] + config['dnn_hidden_units']
self.dnn_layers = nn.ModuleList([
nn.Linear(in_features=layer[0], out_features=layer[1])\
for layer in list(zip(self.hidden_layers[:-1], self.hidden_layers[1:]))
])
self.dnn_linear = nn.Linear(self.hidden_layers[-1], 1, bias=False)
def forward(self, x):
# 先区分出稀疏特征和稠密特征,这里是按照列来划分的,即所有的行都要进行筛选
dense_input, sparse_inputs = x[:, :self.num_dense_feature], x[:, self.num_dense_feature:]
sparse_inputs = sparse_inputs.long()
# 求出线性部分
linear_logit = [self.embedding_layers_1[i](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])]
linear_logit = torch.cat(linear_logit, axis=-1)
# 求出稀疏特征的embedding向量
sparse_embeds = [self.embedding_layers_2[i](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])]
sparse_embeds = torch.cat(sparse_embeds, axis=-1)
dnn_input = torch.cat((dense_input, linear_logit, sparse_embeds), dim=-1)
# DNN 层
dnn_output = dnn_input
for dnn in self.dnn_layers:
dnn_output = dnn(dnn_output)
dnn_output = torch.tanh(dnn_output)
dnn_logit = self.dnn_linear(dnn_output)
# Final
y_pred = torch.sigmoid(dnn_logit)
return y_pred
注意这里实现的FNN模型跟论文中的并不完全一样。论文中描述的FNN模型的第一层的参数是通过预训练好的FM来初始化的,因此模型需要分为两个阶段来训练。这里为了简化,直接使用了两个Embedding层来代替FM中应该学习得到的参数,使得网络可以以端到端的方式训练,简化代码实现。
测试部分代码:
import torch
from FNN.network import FNN
import torch.utils.data as Data
from DeepCrossing.trainer import Trainer
from Utils.criteo_loader import getTestData, getTrainData
fnn_config = \
{
'embed_dim': 8, # 用于控制稀疏特征经过Embedding层后的稠密特征大小
'dnn_hidden_units': [128, 128],
'num_epoch': 150,
'batch_size': 64,
'lr': 1e-3,
'l2_regularization': 1e-4,
'device_id': 0,
'use_cuda': False,
'train_file': '../Data/criteo/processed_data/train_set.csv',
'fea_file': '../Data/criteo/processed_data/fea_col.npy',
'validate_file': '../Data/criteo/processed_data/val_set.csv',
'test_file': '../Data/criteo/processed_data/test_set.csv',
'model_name': '../TrainedModels/FNN.model'
}
if __name__ == "__main__":
####################################################################################
# FNN 模型
####################################################################################
training_data, training_label, dense_features_col, sparse_features_col = getTrainData(fnn_config['train_file'], fnn_config['fea_file'])
train_dataset = Data.TensorDataset(torch.tensor(training_data).float(), torch.tensor(training_label).float())
test_data = getTestData(fnn_config['test_file'])
test_dataset = Data.TensorDataset(torch.tensor(test_data).float())
fnn = FNN(fnn_config, dense_features_cols=dense_features_col, sparse_features_cols=sparse_features_col)
print(fnn)
####################################################################################
# 模型训练阶段
####################################################################################
# # 实例化模型训练器
trainer = Trainer(model=fnn, config=fnn_config)
# 训练
trainer.train(train_dataset)
# 保存模型
trainer.save()
####################################################################################
# 模型测试阶段
####################################################################################
fnn.eval()
if fnn_config['use_cuda']:
fnn.loadModel(map_location=lambda storage, loc: storage.cuda(fnn_config['device_id']))
fnn = fnn.cuda()
else:
fnn.loadModel(map_location=torch.device('cpu'))
y_pred_probs = fnn(torch.tensor(test_data).float())
y_pred = torch.where(y_pred_probs>0.5, torch.ones_like(y_pred_probs), torch.zeros_like(y_pred_probs))
print("Test Data CTR Predict...\n ", y_pred.view(-1))
在criteo数据集上的部分测试结果,输出的是每一个测试数据的点击率预估: