本次要总结分享的是 推荐/CTR 领域内著名的deepfm[1] 论文,参考的代码tensorflow-DeepFM[2],该论文方法较为简单,实现起来也比较容易,该方法在工业界十分常用。
「建议在非深色主题下阅读本文,pc端阅读点击文末左下角“原文链接”,体验更佳」
@
- 论文动机及创新点
- 模型结构
- 输入数据
- FM 部分
- Deep 部分
- 代码分析
- 数据预处理
- 定义 DeepFM 模型超参数
- 构图
- 总结
论文动机及创新点
- 在 deepfm 提出之前,现有的模型很难很好的提取低阶和高阶的交互特征,或者需要足够丰富的人工特征工程才能进行。
- 一些特性交互很容易理解,可以由专家(对业务逻辑很了解的人)设计。然而,大多数其他的特征交互都隐藏在数据中,难以识别先验信息(例如,经典的关联规则“尿布和啤酒”是从数据中挖掘出来的,而不是由专家发现),这只能通过机器学习自动获取。即使对于易于理解的交互特征,专家似乎也不太可能对它们进行穷尽式的建模,尤其是当特征数量很大的时候。
❝所以对于许多数据挖掘类比赛,特征工程的工作量几乎占到工作量的 95%以上,大部分甚至一些优秀选手,首先一股脑的把所能想到的特征都使用上,然后根据效果做些适当特征选择。当选取的特征效果的确很好时,把构建这些特征的思路包装成某一个听起来很高逼格的”方法论“。 ❞
- 很容易想到,有没有什么办法,能让模型能端到端的进行特征学习呢?从而避免繁杂的人工特征工程过程。deepfm 论文里就是基于这一动机,将 fm 模型和 DNN 模型联合起来进行训练,其中 fm 模型可能捕捉到一些低阶的交互特征,而 DNN 模型捕捉一些高阶模型。该联合模型可以进行端到端的训练学习。
- Deepfm 模型中的 Deep 部分和 fm 部分共享 embedding,极大减少了需要学习的参数,使其训练过程很有效率。
- 和以往的 CTR 模型相比,Deepfm 模型效果最好。
模型结构
在这里插入图片描述
上图为 DeepFM 的网络结构图,由左边的 FM 模型和右边的 DNN 模型组成,两个子模型共享下方的输入 embedding。
输入数据
假定有
个样本,每个样本由
组成,其中
由
个特征组成,其中包含了 「类别」型特征 和 「数值」型特征组成,每个特征可理解为一个
。
❝其中 类别 型特征可用 one-hot 编码表示,数值型特征用其本身数值或者离散化在 one-hot 表示 ❞
- 这里定义
表示特征个数;
表示 (数值特征个数 类别特征「取值」个数);
表示
❝类别特征 one-hot 后向量长度即为取值个数 ❞
FM 部分
这里定义两个参数矩阵
- feature_bias:shape 为
的一阶参数矩阵
- feature_embeddings:shape 为
的二阶参数矩阵
- <> 表示内积
不得不说:这篇论文里面的网络图都画的好丑
上式中 第一项<w,x> 表示提取一阶特征,第二项表示提取二阶交叉特征;每个样本在类别型 特征上只有一个取值。
- d 表示
,第二项是在不同 field 之间做二阶交叉特征计算。
- 对于一阶特征中的参数
表示从 feature_bias 参数矩阵中 lookup 得到一个参数,样本中每个特征都能得到一个对应向量(长度为 1)。
- 二阶特征中,
分别表示
的隐向量,可以从 feature_embeddings 参数矩阵中 lookup 得到(长度为 k)。
在这里插入图片描述
❝这里可以理解将数值特征也embedding成向量,一个数值特征只对应一个embedding向量,而一个类别特征的不同取值则对应不同向量,但向量长度均为k,对应论文里说:即使不同的field长度不一样(one-hot向量长度不一样),但是都能embedding成度相同的向量。 ❞
二阶交叉特征推导:
上式中,
表示
,
表示隐向量从 feature*embeddings (shape 为[feature_size,k]) 参数矩阵中 lookup 得到的参数,那么对于第
个
,其得到的
shape 为
,因此
表示
第
个分量,对于类别型特征,
非 0 即 1。
由上面分析可知,每个输入特征都有对应的
参数向量对应。
Deep 部分
这部分更容易理解了,就是个 DNN 网络,模型输入为上图中的 Dense_Embeddings:
注意:FM 与 Deep 部分共享输入的 embedding feature,也就是他们共同影响 Dense_Embeddings。
代码分析
这部分参考的是 tensorflow-DeepFM[3]
数据预处理
该部分对数据集中特征进行了编号,一个连续特征用一个编号,类别特征不同取值用不同编号
代码语言:javascript复制def gen_feat_dict(self):
if self.dfTrain is None:
dfTrain = pd.read_csv(self.trainfile)
else:
dfTrain = self.dfTrain
if self.dfTest is None:
dfTest = pd.read_csv(self.testfile)
else:
dfTest = self.dfTest
df = pd.concat([dfTrain, dfTest])
self.feat_dict = {}
tc = 0
for col in df.columns:
if col in self.ignore_cols:
continue
if col in self.numeric_cols:
# map to a single index
self.feat_dict[col] = tc
tc = 1
else:
us = df[col].unique()
self.feat_dict[col] = dict(zip(us, range(tc, len(us) tc)))
tc = len(us)
self.feat_dim = tc
由上述代码可以看出 feat_dim 就是我们前面定义的 feature_size
代码语言:javascript复制 def parse(self, infile=None, df=None, has_label=False):
assert not ((infile is None) and (df is None)), "infile or df at least one is set"
assert not ((infile is not None) and (df is not None)), "only one can be set"
if infile is None:
dfi = df.copy()
else:
dfi = pd.read_csv(infile)
if has_label:
y = dfi["target"].values.tolist()
dfi.drop(["id", "target"], axis=1, inplace=True)
else:
ids = dfi["id"].values.tolist()
dfi.drop(["id"], axis=1, inplace=True)
# dfi for feature index
# dfv for feature value which can be either binary (1/0) or float (e.g., 10.24)
dfv = dfi.copy()
for col in dfi.columns:
if col in self.feat_dict.ignore_cols:
dfi.drop(col, axis=1, inplace=True)
dfv.drop(col, axis=1, inplace=True)
continue
if col in self.feat_dict.numeric_cols:
dfi[col] = self.feat_dict.feat_dict[col]
else:
dfi[col] = dfi[col].map(self.feat_dict.feat_dict[col])
dfv[col] = 1.
# list of list of feature indices of each sample in the dataset
Xi = dfi.values.tolist()
# list of list of feature values of each sample in the dataset
Xv = dfv.values.tolist()
if has_label:
return Xi, Xv, y
else:
return Xi, Xv, ids
由上面代码可以看出:dfi 表示特征的编号,对于一个类别特征,不同取值其编号不同;dfv 表示该特征值,对于数值型特征就是该值本身,类别特征全是 1(表示取到了该编号的类别值)。
定义 DeepFM 模型超参数
代码语言:javascript复制class DeepFM(BaseEstimator, TransformerMixin):
def __init__(self, feature_size, field_size,
embedding_size=8, dropout_fm=[1.0, 1.0],
deep_layers=[32, 32], dropout_deep=[0.5, 0.5, 0.5],
deep_layers_activation=tf.nn.relu,
epoch=10, batch_size=256,
learning_rate=0.001, optimizer_type="adam",
batch_norm=0, batch_norm_decay=0.995,
verbose=False, random_seed=2016,
use_fm=True, use_deep=True,
loss_type="logloss", eval_metric=roc_auc_score,
l2_reg=0.0, greater_is_better=True):
assert (use_fm or use_deep)
assert loss_type in ["logloss", "mse"],
"loss_type can be either 'logloss' for classification task or 'mse' for regression task"
self.feature_size = feature_size # M=数值型特征个数 类别型特征取值个数,就是feat_dim
self.field_size = field_size # F=特征个数
self.embedding_size = embedding_size # K=embedding_size
self.dropout_fm = dropout_fm
self.deep_layers = deep_layers
self.dropout_deep = dropout_deep
self.deep_layers_activation = deep_layers_activation
self.use_fm = use_fm
self.use_deep = use_deep
self.l2_reg = l2_reg
self.epoch = epoch
self.batch_size = batch_size
self.learning_rate = learning_rate
self.optimizer_type = optimizer_type
self.batch_norm = batch_norm
self.batch_norm_decay = batch_norm_decay
self.verbose = verbose
self.random_seed = random_seed
self.loss_type = loss_type
self.eval_metric = eval_metric
self.greater_is_better = greater_is_better
self.train_result, self.valid_result = [], []
self._init_graph()
构图
feature_bias:shape 为
的一阶参数矩阵 feature_embeddings:shape 为
的二阶参数矩阵
代码语言:javascript复制def _init_graph(self):
self.graph = tf.Graph()
with self.graph.as_default():
tf.set_random_seed(self.random_seed)
self.feat_index = tf.placeholder(tf.int32, shape=[None, None],
name="feat_index") # None * F
self.feat_value = tf.placeholder(tf.float32, shape=[None, None],
name="feat_value") # None * F
self.label = tf.placeholder(tf.float32, shape=[None, 1], name="label") # None * 1
self.dropout_keep_fm = tf.placeholder(tf.float32, shape=[None], name="dropout_keep_fm")
self.dropout_keep_deep = tf.placeholder(tf.float32, shape=[None], name="dropout_keep_deep")
self.train_phase = tf.placeholder(tf.bool, name="train_phase")
self.weights = self._initialize_weights()
# model
self.embeddings = tf.nn.embedding_lookup(self.weights["feature_embeddings"],
self.feat_index) # None * F * K
feat_value = tf.reshape(self.feat_value, shape=[-1, self.field_size, 1])
self.embeddings = tf.multiply(self.embeddings, feat_value) ## 供下面的FM和Deep部分使用
# ---------- first order term ----------
self.y_first_order = tf.nn.embedding_lookup(self.weights["feature_bias"], self.feat_index) # None * F * 1
self.y_first_order = tf.reduce_sum(tf.multiply(self.y_first_order, feat_value), 2) # None * F
self.y_first_order = tf.nn.dropout(self.y_first_order, self.dropout_keep_fm[0]) # None * F
# ---------- second order term ---------------
# sum_square part
self.summed_features_emb = tf.reduce_sum(self.embeddings, 1) # None * K
self.summed_features_emb_square = tf.square(self.summed_features_emb) # None * K
# square_sum part
self.squared_features_emb = tf.square(self.embeddings)
self.squared_sum_features_emb = tf.reduce_sum(self.squared_features_emb, 1) # None * K
# second order
self.y_second_order = 0.5 * tf.subtract(self.summed_features_emb_square, self.squared_sum_features_emb) # None * K
self.y_second_order = tf.nn.dropout(self.y_second_order, self.dropout_keep_fm[1]) # None * K
# ---------- Deep component ----------
self.y_deep = tf.reshape(self.embeddings, shape=[-1, self.field_size * self.embedding_size]) # None * (F*K)
self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[0])
for i in range(0, len(self.deep_layers)):
self.y_deep = tf.add(tf.matmul(self.y_deep, self.weights["layer_%d" %i]), self.weights["bias_%d"%i]) # None * layer[i] * 1
if self.batch_norm:
self.y_deep = self.batch_norm_layer(self.y_deep, train_phase=self.train_phase, scope_bn="bn_%d" %i) # None * layer[i] * 1
self.y_deep = self.deep_layers_activation(self.y_deep)
self.y_deep = tf.nn.dropout(self.y_deep, self.dropout_keep_deep[1 i]) # dropout at each Deep layer
# ---------- DeepFM ----------
if self.use_fm and self.use_deep:
concat_input = tf.concat([self.y_first_order, self.y_second_order, self.y_deep], axis=1)
elif self.use_fm:
concat_input = tf.concat([self.y_first_order, self.y_second_order], axis=1)
elif self.use_deep:
concat_input = self.y_deep
self.out = tf.add(tf.matmul(concat_input, self.weights["concat_projection"]), self.weights["concat_bias"])
这里需要注意:本实现代码中,「也对连续特征也直接做了 embedding」,用的时候也可以把连续特征改为 deep 侧的直接输入,另外针对多值离散特征这里也没有处理。
总结
- 该方法将 FM(捕捉低级特征)和 Deep(捕捉高级特征)进行端到端的联合训练,并且共享输入 embedding,这是现在非常常见的做法。
- 论文讲到该方法可以一定程度避免人工特征工程,从模型看的确做到了无脑交叉,模型自动学习各种交叉的权重。
Reference
[1]
deepfm: https://arxiv.org/abs/1703.04247
[2]
tensorflow-DeepFM: https://github.com/ChenglongChen/tensorflow-DeepFM
[3]
tensorflow-DeepFM: https://github.com/ChenglongChen/tensorflow-DeepFM
代码语言:javascript复制