AutoRec
AutoRec可以说是深度学习时代最古老、最简单、最容易理解的模型。该模型模拟无监督学习中用到的数据表征和压缩工具AutoEncoder, 自然地,其模型也可以写成重建函数形式:
其中 f(*) 和 g(*) 分别为输出层和隐藏层的激活函数。
跟AutoEncoder类似地,其损失函数为
不过考虑到对模型参数的限制,比如加入L2正则,损失函数变化为:
一般如原论文中一样,把AutoRec用于协同过滤(Collaborative Filtering). 对于条目协同过滤,AutoRec的模型定义(PyTorch)如下:
class Autorec(nn.Module):
def __init__(self,args, num_items):
super(Autorec, self).__init__()
self.args = args
#self.num_users = num_users
self.num_items = num_items
self.hidden_units = args.hidden_units
self.lambda_value = args.lambda_value
self.encoder = nn.Sequential(
nn.Linear(self.num_items, self.hidden_units),
nn.Sigmoid()
)
self.decoder = nn.Sequential(
nn.Linear(self.hidden_units, self.num_items),
)
def forward(self,torch_input):
encoder = self.encoder(torch_input)
decoder = self.decoder(encoder)
return decoder
对应地,在损失函数中需要对模型参数加正则:
def loss(self, decoder, input, optimizer, mask_input):
cost = 0
temp2 = 0
cost += ((decoder - input) * mask_input).pow(2).sum()
rmse = cost
for i in optimizer.param_groups:
for j in i['params']:
# print(type(j.data), j.shape,j.data.dim())
if j.data.dim() == 2:
temp2 += torch.t(j.data).pow(2).sum()
cost += temp2 * self.lambda_value * 0.5
return cost, rmse
对于条目协同过滤I-AutoRec来说,输入是用户对所有物品的评分向量,但是由于输入向量是稀疏的,所以在计算loss中要加入上面程序段中的mask.
在I-AutoRec的推理过程中,输入是某个用户的评分条目列表,得到是对应这个条目列表的相似条目列表:
reqArtList = ['Item1','Item3','Item111']
reqID = []
for i in range(len(reqArtList)):
reqID.append(itemList.index(reqArtList[i]))
tVec = np.zeros((1,numItems))
for i in range(len(reqID)):
tVec[0,reqID[i]]=1
tdata = torch.FloatTensor(tVec).cuda()
pre = rec(tdata).detach().cpu().numpy()[0]
a=pre.argsort()[-30:][::-1]
preDict = {}
for i in range(len(a)):
if a[i] not in reqID:
preDict[itemList[a[i]]] = pre[a[i]]
sortedDict = {k:v for k,v in sorted(preDict.items(),key = lambda item: item[1], reverse=True)}
topIDs = list(sortedDict.keys())
' '.join(topIDs)
Deep Crossing
Deep Crossing模型的应用场景是微软搜索引擎Bing中的搜索广告推荐场景。用户在搜索引擎中输入搜索词之后,搜索引擎除了会返回相关结果,还会返回与搜索词相关的广告。尽可能地增加搜索广告的点击率,准确地预测广告点击率,并以此作为广告排序的指标之一,是非常重要的工作,也是Deep Crossing模型的优化目标。
微软使用的特征如下表所示,这些特征可以分为三类:一类是可以被处理成one-hot或者multi-hot向量的类别型特征,包括用户搜索词(query)、广告关键词(keyword)、广告标题(title)、落地页(landing page)、匹配类型(match type);一类是数值型特征,微软称其为计数型特征,包括点击率、预估点击率(click prediction);一类是需要进一步处理的特征,包括广告计划(campaign)、曝光计划(impression)、点击样例(click)等。
类别型特征可以通过one-hot或multi-hot编码生成特征向量,数值型特征则可以直接拼接进特征向量中,在生成所有输入特征的向量表达后,Deep Crossing模型利用该特征向量进行CTR预估。深度学习网络的特点是可以根据需求灵活地对网络结构进行调整,从而达成从原始特征向量到最终的优化目标的端到端的训练目的。
模型结构:
整个模型包含四种结构:Embedding,Stacking,Residual Unit,Scoring Layer。
论文中使用的目标函数为 :
在实际应用中,可以灵活替换为其他目标函数。
下面对各层结构进行分析:
1.1 Embedding & Stacking
Embedding的主要目的是将高维稀疏特征转化为低维稠密特征,其公式化定义为:
其中 X_j 代表输入的第 个特征Field,并且已经过one-hot编码表示, 分别表示对应的模型参数。与前几篇paper介绍的Embedding过程不同的是,DeepCrossing加上了偏置项 。公式中的 操作等价于使用 激活函数。
尽管可以通过分Field的操作,减少Embedding层的参数量,但是由于某些 高基数特征 的存在,如paper中提到的CampaignID,其对应的 仍然十分庞大。为此作者提出,针对这些高基数特征构造衍生特征,具体操作如下。根据CampaignID的历史点击率从高到低选择Top1000个,编号从0到999,将剩余的ID统一编号为1000。同时构建其衍生特征,将所有ID对应的历史点击率组合成1001维的稠密矩阵,各个元素分别为对应ID的历史CTR,最后一个元素为剩余ID的平均CTR。通过降维引入衍生特征的方式,可以有效的减少高基数特征带来的参数量剧增问题。
经过Embedding之后,直接对所有的X进行拼接Stacking 。作者将特征embedding为256维,但是对于本身维度低于256的特征Field,无需进行Embedding,直接送入Stacking层,如上图中的 所示。
1.2 Residual Unit
公式定义为:
1.3 Scoring Layer
使用logloss作为目标函数,可以灵活改用其他函数表示。
题外话:Early Crossing vs. Late Crossing
在paper中,作者针对特征交叉的时间点先后的问题进行试验对比。在DeepCrossing中,特征是在Embedding之后就开始进行交叉,但是有一些模型如DSSM,是在各类特征单独处理完成之后再进行交叉计算
实验:MovieLens100Kdataset
class DeepCrossing(object):
def __init__(self, vec_dim=None, field_lens=None, lr=None, residual_unit_num=None, residual_w_dim=None, dropout_rate=None, lamda=None):
self.vec_dim = vec_dim
self.field_lens = field_lens
self.field_num = len(field_lens)
self.lr = lr
self.residual_unit_num = residual_unit_num
self.residual_w_dim = residual_w_dim
self.dropout_rate = dropout_rate
self.lamda = float(lamda)
self.l2_reg = tf.contrib.layers.l2_regularizer(self.lamda)
self._build_graph()
def _build_graph(self):
self.add_input()
self.inference()
def add_input(self):
self.x = [tf.placeholder(tf.float32, name='input_x_%d'%i) for i in range(self.field_num)]
self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
self.is_train = tf.placeholder(tf.bool)
def _residual_unit(self, input, i):
x = input
in_node = self.field_num*self.vec_dim
out_node = self.residual_w_dim
w0 = tf.get_variable(name='residual_w0_%d'%i, shape=[in_node, out_node], dtype=tf.float32, regularizer=self.l2_reg)
b0 = tf.get_variable(name='residual_b0_%d'%i, shape=[out_node], dtype=tf.float32)
residual = tf.nn.relu(tf.matmul(input, w0) + b0)
w1 = tf.get_variable(name='residual_w1_%d'%i, shape=[out_node, in_node], dtype=tf.float32, regularizer=self.l2_reg)
b1 = tf.get_variable(name='residual_b1_%d'%i, shape=[in_node], dtype=tf.float32)
residual = tf.matmul(residual, w1) + b1
out = tf.nn.relu(residual+x)
return out
def inference(self):
with tf.variable_scope('emb_part'):
emb = [tf.get_variable(name='emb_%d'%i, shape=[self.field_lens[i], self.vec_dim], dtype=tf.float32, regularizer=self.l2_reg) for i in range(self.field_num)]
emb_layer = tf.concat([tf.matmul(self.x[i], emb[i]) for i in range(self.field_num)], axis=1) # (batch, F*K)
x = emb_layer
with tf.variable_scope('residual_part'):
for i in range(self.residual_unit_num):
x = self._residual_unit(x, i)
x = tf.layers.dropout(x, rate=self.dropout_rate, training=self.is_train)
w = tf.get_variable(name='w', shape=[self.field_num*self.vec_dim, 1], dtype=tf.float32, regularizer=self.l2_reg)
b = tf.get_variable(name='b', shape=[1], dtype=tf.float32)
self.y_logits = tf.matmul(x, w) + b
self.y_hat = tf.nn.sigmoid(self.y_logits)
self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
reg_variables = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
if len(reg_variables) > 0:
self.loss += tf.add_n(reg_variables)
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.loss)
PNN
PNN,全称为Product-based Neural Network,认为在embedding输入到MLP之后学习的交叉特征表达并不充分,提出了一种product layer的思想,既基于乘法的运算来体现体征交叉的DNN网络结构,如下图:
输出层
输出层很简单,将上一层的网络输出通过一个全链接层,经过sigmoid函数转换后映射到(0,1)的区间中,得到我们的点击率的预测值:
l2层
根据l1层的输出,经一个全链接层 ,并使用relu进行激活,得到我们l2的输出结果:
l1层
l1层的输出由如下的公式计算:
重点马上就要来了,我们可以看到在得到l1层输出时,我们输入了三部分,分别是lz,lp 和 b1,b1是我们的偏置项,这里可以先不管。lz和lp的计算就是PNN的精华所在了。我们慢慢道来
Product Layer
product思想来源于,在ctr预估中,认为特征之间的关系更多是一种and“且”的关系,而非add"加”的关系。例如,性别为男且喜欢游戏的人群,比起性别男和喜欢游戏的人群,前者的组合比后者更能体现特征交叉的意义。
product layer可以分成两个部分,一部分是线性部分lz,一部分是非线性部分lp。二者的形式如下:
在这里,我们要使用到论文中所定义的一种运算方式,其实就是矩阵的点乘啦:
我们先继续介绍网络结构,有关Product Layer的更详细的介绍,我们在下一章中介绍。
Embedding Layer
Embedding Layer跟DeepFM中相同,将每一个field的特征转换成同样长度的向量,这里用f来表示。
损失函数
使用和逻辑回归同样的损失函数,如下:
Product Layer详细介绍
前面提到了,product layer可以分成两个部分,一部分是线性部分lz,一部分是非线性部分lp。
看上面的公式,我们首先需要知道z和p,这都是由我们的embedding层得到的,其中z是线性信号向量,因此我们直接用embedding层得到:
论文中使用的等号加一个三角形,其实就是相等的意思,你可以认为z就是embedding层的复制。
对于p来说,这里需要一个公式进行映射:
不同的g的选择使得我们有了两种PNN的计算方法,一种叫做Inner PNN,简称IPNN,一种叫做Outer PNN,简称OPNN。
接下来,我们分别来具体介绍这两种形式的PNN模型,由于涉及到复杂度的分析,所以我们这里先定义Embedding的大小为M,field的大小为N,而lz和lp的长度为D1。
2.1 IPNN
IPNN的示意图如下:
IPNN中p的计算方式如下,即使用内积来代表pij:
所以,pij其实是一个数,得到一个pij的时间复杂度为M,p的大小为N*N,因此计算得到p的时间复杂度为N*N*M。而再由p得到lp的时间复杂度是N*N*D1。因此 对于IPNN来说,总的时间复杂度为N*N(D1+M)。文章对这一结构进行了优化,可以看到,我们的p是一个对称矩阵,因此我们的权重也可以是一个对称矩阵,对称矩阵就可以进行如下的分解:
因此:
因此:
从而得到:
可以看到,我们的权重只需要D1 * N就可以了,时间复杂度也变为了D1*M*N。
2.2 OPNN
OPNN的示意图如下:
OPNN中p的计算方式如下:
此时pij为M*M的矩阵,计算一个pij的时间复杂度为M*M,而p是N*N*M*M的矩阵,因此计算p的事件复杂度为N*N*M*M。从而计算lp的时间复杂度变为D1 * N*N*M*M。这个显然代价很高的。为了减少负责度,论文使用了叠加的思想,它重新定义了p矩阵:
这里计算p的时间复杂度变为了D1*M*(M+N)
Wide&Deep
Memorization 和 Generalization是推荐系统很常见的两个概念,其中Memorization指的是通过用户与商品的交互信息矩阵学习规则,而Generalization则是泛化规则。我们前面介绍的FM算法就是很好的Generalization的例子,它可以根据交互信息学习到一个比较短的矩阵 V ,其中 vi 储存着每个用户特征的压缩表示(embedding),而协同过滤与SVD都是靠记住用户之前与哪些物品发生了交互从而推断出的推荐结果,这两者推荐结果当然存在一些差异,我们的Wide&Deep模型就能够融合这两种推荐结果做出最终的推荐,得到一个比之前的推荐结果都好的模型。
可以这么说:Memorization趋向于更加保守,推荐用户之前有过行为的items。相比之下,generalization更加趋向于提高推荐系统的多样性(diversity)。Memorization只需要使用一个线性模型即可实现,而Generalization需要使用DNN实现。
下面是wide&deep模型的结构图,由左边的wide部分(一个简单的线性模型),右边的deep部分(一个典型的DNN模型)。
其实wide&deep模型本身的结构是非常简单的,对于有点机器学习基础和深度学习基础的人来说都非常的容易看懂,但是如何根据自己的场景去选择那些特征放在Wide部分,哪些特征放在Deep部分就需要理解这篇论文提出者当时对于设计该模型不同结构时的意图了,所以这也是用好这个模型的一个前提。
如何理解Wide部分有利于增强模型的“记忆能力”,Deep部分有利于增强模型的“泛化能力”?
-
wide部分是一个广义的线性模型,输入的特征主要有两部分组成,一部分是原始的部分特征,另一部分是原始特征的交互特征(cross-product transformation),对于交互特征可以定义为:
ϕk(x)=d∏i=1xckii,cki∈{0,1}
这个式子什么意思读者可以自行找原论文看看,大体意思就是两个特征都同时为1这个新的特征才能为1,否则就是0,说白了就是一个特征组合。用原论文的例子举例:
AND(user_installed_app=QQ, impression_app=WeChat),当特征user_installed_app=QQ,和特征impression_app=WeChat取值都为1的时候,组合特征AND(user_installed_app=QQ, impression_app=WeChat)的取值才为1,否则为0。
对于wide部分训练时候使用的优化器是带 L1 正则的FTRL算法(Follow-the-regularized-leader),而L1 FTLR是非常注重模型稀疏性质的,也就是说W&D模型采用L1 FTRL是想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0,这就大大压缩了模型权重及特征向量的维度。Wide部分模型训练完之后留下来的特征都是非常重要的,那么模型的“记忆能力”就可以理解为发现"直接的",“暴力的”,“显然的”关联规则的能力。 例如Google W&D期望wide部分发现这样的规则:用户安装了应用A,此时曝光应用B,用户安装应用B的概率大。
-
Deep部分是一个DNN模型,输入的特征主要分为两大类,一类是数值特征(可直接输入DNN),一类是类别特征(需要经过Embedding之后才能输入到DNN中),Deep部分的数学形式如下:
a(l+1)=f(Wla(l)+bl)
我们知道DNN模型随着层数的增加,中间的特征就越抽象,也就提高了模型的泛化能力。 对于Deep部分的DNN模型作者使用了深度学习常用的优化器AdaGrad,这也是为了使得模型可以得到更精确的解。
Wide部分与Deep部分的结合
W&D模型是将两部分输出的结果结合起来联合训练,将deep和wide部分的输出重新使用一个逻辑回归模型做最终的预测,输出概率值。联合训练的数学形式如下:
P(Y=1|x)=δ(wTwide[x,ϕ(x)]+wTdeepa(lf)+b)
4. 操作流程
-
Retrieval :利用机器学习模型和一些人为定义的规则,来返回最匹配当前Query的一个小的items集合,这个集合就是最终的推荐列表的候选集。
-
Ranking:
- 收集更细致的用户特征,如:
- User features(年龄、性别、语言、民族等)
- Contextual features(上下文特征:设备,时间等)
- Impression features(展示特征:app age、app的历史统计信息等)
- 将特征分别传入Wide和Deep一起做训练。在训练的时候,根据最终的loss计算出gradient,反向传播到Wide和Deep两部分中,分别训练自己的参数(wide组件只需要填补deep组件的不足就行了,所以需要比较少的cross-product feature transformations,而不是full-size wide Model)
- 训练方法是用mini-batch stochastic optimization。
- Wide组件是用FTRL(Follow-the-regularized-leader) + L1正则化学习。
- Deep组件是用AdaGrad来学习。
- 训练完之后推荐TopN
- 收集更细致的用户特征,如:
所以wide&deep模型尽管在模型结构上非常的简单,但是如果想要很好的使用wide&deep模型的话,还是要深入理解业务,确定wide部分使用哪部分特征,deep部分使用哪些特征,以及wide部分的交叉特征应该如何去选择。
实战代码:
import warnings
warnings.filterwarnings("ignore")
import itertools
import pandas as pd
import numpy as np
from tqdm import tqdm
from collections import namedtuple
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from utils import SparseFeat, DenseFeat, VarLenSparseFeat
# 简单处理特征,包括填充缺失值,数值处理,类别编码
def data_process(data_df, dense_features, sparse_features):
data_df[dense_features] = data_df[dense_features].fillna(0.0)
for f in dense_features:
data_df[f] = data_df[f].apply(lambda x: np.log(x+1) if x > -1 else -1)
data_df[sparse_features] = data_df[sparse_features].fillna("-1")
for f in sparse_features:
lbe = LabelEncoder()
data_df[f] = lbe.fit_transform(data_df[f])
return data_df[dense_features + sparse_features]
def build_input_layers(feature_columns):
# 构建Input层字典,并以dense和sparse两类字典的形式返回
dense_input_dict, sparse_input_dict = {}, {}
for fc in feature_columns:
if isinstance(fc, SparseFeat):
sparse_input_dict[fc.name] = Input(shape=(1, ), name=fc.name)
elif isinstance(fc, DenseFeat):
dense_input_dict[fc.name] = Input(shape=(fc.dimension, ), name=fc.name)
return dense_input_dict, sparse_input_dict
def build_embedding_layers(feature_columns, input_layers_dict, is_linear):
# 定义一个embedding层对应的字典
embedding_layers_dict = dict()
# 将特征中的sparse特征筛选出来
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns)) if feature_columns else []
# 如果是用于线性部分的embedding层,其维度为1,否则维度就是自己定义的embedding维度
if is_linear:
for fc in sparse_feature_columns:
embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size, 1, name='1d_emb_' + fc.name)
else:
for fc in sparse_feature_columns:
embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size, fc.embedding_dim, name='kd_emb_' + fc.name)
return embedding_layers_dict
def get_linear_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns):
# 将所有的dense特征的Input层,然后经过一个全连接层得到dense特征的logits
concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values()))
dense_logits_output = Dense(1)(concat_dense_inputs)
# 获取linear部分sparse特征的embedding层,这里使用embedding的原因是:
# 对于linear部分直接将特征进行onehot然后通过一个全连接层,当维度特别大的时候,计算比较慢
# 使用embedding层的好处就是可以通过查表的方式获取到哪些非零的元素对应的权重,然后在将这些权重相加,效率比较高
linear_embedding_layers = build_embedding_layers(sparse_feature_columns, sparse_input_dict, is_linear=True)
# 将一维的embedding拼接,注意这里需要使用一个Flatten层,使维度对应
sparse_1d_embed = []
for fc in sparse_feature_columns:
feat_input = sparse_input_dict[fc.name]
embed = Flatten()(linear_embedding_layers[fc.name](feat_input)) # B x 1
sparse_1d_embed.append(embed)
# embedding中查询得到的权重就是对应onehot向量中一个位置的权重,所以后面不用再接一个全连接了,本身一维的embedding就相当于全连接
# 只不过是这里的输入特征只有0和1,所以直接向非零元素对应的权重相加就等同于进行了全连接操作(非零元素部分乘的是1)
sparse_logits_output = Add()(sparse_1d_embed)
# 最终将dense特征和sparse特征对应的logits相加,得到最终linear的logits
linear_logits = Add()([dense_logits_output, sparse_logits_output])
return linear_logits
# 将所有的sparse特征embedding拼接
def concat_embedding_list(feature_columns, input_layer_dict, embedding_layer_dict, flatten=False):
# 将sparse特征筛选出来
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns))
embedding_list = []
for fc in sparse_feature_columns:
_input = input_layer_dict[fc.name] # 获取输入层
_embed = embedding_layer_dict[fc.name] # B x 1 x dim 获取对应的embedding层
embed = _embed(_input) # B x dim 将input层输入到embedding层中
# 是否需要flatten, 如果embedding列表最终是直接输入到Dense层中,需要进行Flatten,否则不需要
if flatten:
embed = Flatten()(embed)
embedding_list.append(embed)
return embedding_list
def get_dnn_logits(dense_input_dict, sparse_input_dict, sparse_feature_columns, dnn_embedding_layers):
concat_dense_inputs = Concatenate(axis=1)(list(dense_input_dict.values())) # B x n1 (n表示的是dense特征的维度)
sparse_kd_embed = concat_embedding_list(sparse_feature_columns, sparse_input_dict, dnn_embedding_layers, flatten=True)
concat_sparse_kd_embed = Concatenate(axis=1)(sparse_kd_embed) # B x n2k (n2表示的是Sparse特征的维度)
dnn_input = Concatenate(axis=1)([concat_dense_inputs, concat_sparse_kd_embed]) # B x (n2k + n1)
# dnn层,这里的Dropout参数,Dense中的参数及Dense的层数都可以自己设定
dnn_out = Dropout(0.5)(Dense(1024, activation='relu')(dnn_input))
dnn_out = Dropout(0.3)(Dense(512, activation='relu')(dnn_out))
dnn_out = Dropout(0.1)(Dense(256, activation='relu')(dnn_out))
dnn_logits = Dense(1)(dnn_out)
return dnn_logits
# Wide&Deep 模型的wide部分及Deep部分的特征选择,应该根据实际的业务场景去确定哪些特征应该放在Wide部分,哪些特征应该放在Deep部分
def WideNDeep(linear_feature_columns, dnn_feature_columns):
# 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)
# 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding
linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns))
# 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
# 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
# Wide&Deep模型论文中Wide部分使用的特征比较简单,并且得到的特征非常的稀疏,所以使用了FTRL优化Wide部分(这里没有实现FTRL)
# 但是是根据他们业务进行选择的,我们这里将所有可能用到的特征都输入到Wide部分,具体的细节可以根据需求进行修改
linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)
# 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)
dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns))
# 在Wide&Deep模型中,deep部分的输入是将dense特征和embedding特征拼在一起输入到dnn中
dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, embedding_layers)
# 将linear,dnn的logits相加作为最终的logits
output_logits = Add()([linear_logits, dnn_logits])
# 这里的激活函数使用sigmoid
output_layer = Activation("sigmoid")(output_logits)
model = Model(input_layers, output_layer)
return model
if __name__ == "__main__":
# 读取数据
data = pd.read_csv('./data/criteo_sample.txt')
# 划分dense和sparse特征
columns = data.columns.values
dense_features = [feat for feat in columns if 'I' in feat]
sparse_features = [feat for feat in columns if 'C' in feat]
# 简单的数据预处理
train_data = data_process(data, dense_features, sparse_features)
train_data['label'] = data['label']
# 将特征分组,分成linear部分和dnn部分(根据实际场景进行选择),并将分组之后的特征做标记(使用DenseFeat, SparseFeat)
linear_feature_columns = [SparseFeat(feat, vocabulary_size=data[feat].nunique(),embedding_dim=4)
for i,feat in enumerate(sparse_features)] + [DenseFeat(feat, 1,)
for feat in dense_features]
dnn_feature_columns = [SparseFeat(feat, vocabulary_size=data[feat].nunique(),embedding_dim=4)
for i,feat in enumerate(sparse_features)] + [DenseFeat(feat, 1,)
for feat in dense_features]
# 构建WideNDeep模型
history = WideNDeep(linear_feature_columns, dnn_feature_columns)
history.summary()
history.compile(optimizer="adam",
loss="binary_crossentropy",
metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])
# 将输入数据转化成字典的形式输入
train_model_input = {name: data[name] for name in dense_features + sparse_features}
# 模型训练
history.fit(train_model_input, train_data['label'].values,
batch_size=64, epochs=5, validation_split=0.2, )