本次要总结分享的是 推荐/CTR 领域内著名的deepfm[1] 论文,参考的代码tensorflow-DeepFM[2],该论文方法较为简单,实现起来也比较容易,该方法在工业界十分常用。
「建议在非深色主题下阅读本文,pc端阅读点击文末左下角“原文链接”,体验更佳」
@
❝所以对于许多数据挖掘类比赛,特征工程的工作量几乎占到工作量的 95%以上,大部分甚至一些优秀选手,首先一股脑的把所能想到的特征都使用上,然后根据效果做些适当特征选择。当选取的特征效果的确很好时,把构建这些特征的思路包装成某一个听起来很高逼格的”方法论“。 ❞
在这里插入图片描述
上图为 DeepFM 的网络结构图,由左边的 FM 模型和右边的 DNN 模型组成,两个子模型共享下方的输入 embedding。
假定有
个样本,每个样本由
组成,其中
由
个特征组成,其中包含了 「类别」型特征 和 「数值」型特征组成,每个特征可理解为一个
。
❝其中 类别 型特征可用 one-hot 编码表示,数值型特征用其本身数值或者离散化在 one-hot 表示 ❞
表示特征个数;
表示 (数值特征个数+类别特征「取值」个数);
表示
❝类别特征 one-hot 后向量长度即为取值个数 ❞
这里定义两个参数矩阵
的一阶参数矩阵
的二阶参数矩阵
不得不说:这篇论文里面的网络图都画的好丑
上式中 第一项<w,x> 表示提取一阶特征,第二项表示提取二阶交叉特征;每个样本在类别型 特征上只有一个取值。
,第二项是在不同 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。
由上面分析可知,每个输入特征都有对应的
参数向量对应。
这部分更容易理解了,就是个 DNN 网络,模型输入为上图中的 Dense_Embeddings:
注意:FM 与 Deep 部分共享输入的 embedding feature,也就是他们共同影响 Dense_Embeddings。
这部分参考的是 tensorflow-DeepFM[3]
该部分对数据集中特征进行了编号,一个连续特征用一个编号,类别特征不同取值用不同编号
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
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(表示取到了该编号的类别值)。
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 为
的二阶参数矩阵
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 侧的直接输入,另外针对多值离散特征这里也没有处理。
[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