论文分享-->推荐系统-->deepfm

本次要总结分享的是 推荐/CTR 领域内著名的deepfm 论文,参考的代码tensorflow-DeepFM,该论文方法较为简单,实现起来也比较容易,该方法在工业界十分常用。

论文动机及创新点

  • 在deepfm提出之前,现有的模型很难很好的提取低阶和高阶的交互特征,或者需要足够丰富的人工特征工程才能进行。

  • 一些特性交互很容易理解,可以由专家(对业务逻辑很了解的人)设计。然而,大多数其他的特征交互都隐藏在数据中,难以识别先验信息(例如,经典的关联规则“尿布和啤酒”是从数据中挖掘出来的,而不是由专家发现),这只能通过机器学习自动获取。即使对于易于理解的交互特征,专家似乎也不太可能对它们进行穷尽式的建模,尤其是当特征数量很大的时候。

    所以对于许多数据挖掘类比赛,特征工程的工作量几乎占到工作量的95%以上,大部分甚至一些优秀选手,首先一股脑的把所能想到的特征都使用上,然后根据效果做些适当特征选择。当选取的特征效果的确很好时,把构建这些特征的思路包装成某一个听起来很高逼格的”方法论“。

  • 很容易想到,有没有什么办法,能让模型能端到端的进行特征学习呢?从而避免繁杂的人工特征工程过程。deepfm论文里就是基于这一动机,将fm模型和DNN模型联合起来进行训练,其中fm模型可能捕捉到一些低阶的交互特征,而DNN模型捕捉一些高阶模型。该联合模型可以进行端到端的训练学习。

  • Deepfm模型中的Deep部分和fm部分共享embedding,极大减少了需要学习的参数,使其训练过程很有效率。

  • 和以往的CTR模型相比,Deepfm模型效果最好。

模型结构

在这里插入图片描述
y ^ = s i g m o i d ( y F M + y D N N ) \hat{y}=sigmoid(y_{FM}+y_{DNN}) y^=sigmoid(yFM+yDNN)
上图为DeepFM的网络结构图,由左边的FM模型和右边的DNN模型组成,两个子模型共享下方的输入embedding。

输入数据

假定有 n n n 个样本,每个样本由 ( X , y ) (X, y) (X,y) 组成,其中

  • y ∈ ( 1 , 0 ) y \in {(1,0)} y(1,0)
  • X X X m m m 个特征组成,其中包含了 类别型特征 和 数值型特征组成,每个特征可理解为一个 f i e l d field field

    其中 类别 型特征可用one-hot编码表示,数值型特征用其本身数值或者离散化在one-hot表示

  • 这里定义 f i e l d _ s i z e field\_size field_size 表示特征个数; f e a t u r e _ s i z e feature\_size feature_size 表示 (数值特征个数+类别特征取值个数); k k k 表示 e m b e d d i n g _ s i z e embedding\_size embedding_size

    类别特征one-hot后向量长度即为取值个数

FM部分

这里定义两个参数矩阵

  • feature_bias:shape为 [ f e a t u r e _ s i z e , 1 ] [feature\_size,1] [feature_size,1] 的一阶参数矩阵
  • feature_embeddings:shape为 [ f e a t u r e _ s i z e , k ] [feature\_size,k] [feature_size,k] 的二阶参数矩阵
  • 这里用 f e a t u r e _ s i z e feature\_size feature_size,是为了对于类别型特征每个取值都能有向量表征
  • <> 表示内积
    在这里插入图片描述
    不得不说:这篇论文里面的网络图都画的好丑

y F M = < w , x > + ∑ j 1 = 1 d ∑ j 2 = j 1 + 1 d < V i , V j > x j 1 ⋅ x j 2 y_{FM}=<w,x>+\sum_{j_1=1}^d\sum_{j_2=j_1+1}^d <V_i,V_j>x_{j_1} \cdot x_{j_2} yFM=<w,x>+j1=1dj2=j1+1d<Vi,Vj>xj1xj2

上式中 第一项 < w , x > <w,x> <w,x> 表示提取一阶特征,第二项表示提取二阶交叉特征;每个样本在类别型 特征上只有一个取值。

  • d 表示 f i e l d _ s i z e field\_size field_size,第二项是在不同field之间做二阶交叉特征计算。

  • 对于一阶特征中的参数 w w w 表示从feature_bias参数矩阵中lookup得到一个参数,样本中每个特征都能得到一个对应向量(长度为1)。

  • 二阶特征中, V i , V j V_i,V_j Vi,Vj 分别表示 x j 1 , x j 2 x_{j_1},x_{j_2} xj1,xj2 的隐向量,可以从 feature_embeddings参数矩阵中lookup得到(长度为k)。
    在这里插入图片描述

    这里可以理解将数值特征也embedding成向量,一个数值特征只对应一个embedding向量,而一个类别特征的不同取值则对应不同向量,但向量长度均为k,对应论文里说:即使不同的field长度不一样(one-hot向量长度不一样),但是都能embedding成长度相同的向量。

二阶交叉特征推导:
在这里插入图片描述
上式中, n n n 表示 f i e l d _ s i z e field\_size field_size v i v_i vi 表示隐向量从 feature_embeddings (shape为[feature_size,k]) 参数矩阵中lookup得到的参数,那么对于第 i i i f i e l d field field,其得到的 v i v_i vi shape 为 [ 1 , k ] [1,k] [1k],因此 v i , f v_{i,f} vi,f 表示 v i v_i vi f f f个分量,对于类别型特征, x i x_i xi 非0即1。

由上面分析可知,每个输入特征都有对应的 v i v_i vi 参数向量对应。

Deep部分

在这里插入图片描述
这部分更容易理解了,就是个DNN网络,模型输入为上图中的 Dense_Embeddings:
α ( 0 ) = { e 1 , e 2 , e 3 , . . . , e m } \alpha^{(0)}=\{e_1,e_2,e_3,...,e_m\} α(0)={e1,e2,e3,...,em}
α ( l + 1 ) = σ ( W ( l ) α ( l ) + b ( l ) ) \alpha^{(l+1)}=\sigma(W^{(l)}\alpha^{(l)}+b^{(l)}) α(l+1)=σ(W(l)α(l)+b(l))

注意:FM与Deep部分共享输入的embedding feature,也就是他们共同影响 Dense_Embeddings。

代码分析

这部分参考的是 tensorflow-DeepFM

数据预处理

该部分对数据集中特征进行了编号,一个连续特征用一个编号,类别特征不同取值用不同编号

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(表示取到了该编号的类别值)。

定义DeepFM模型超参数

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为 [ f e a t u r e _ s i z e , 1 ] [feature\_size,1] [feature_size,1] 的一阶参数矩阵
feature_embeddings:shape为 [ f e a t u r e _ s i z e , k ] [feature\_size,k] [feature_size,k] 的二阶参数矩阵

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,这是现在非常常见的做法。
  • 论文讲到该方法可以一定程度避免人工特征工程,从模型看的确做到了无脑交叉,模型自动学习各种交叉的权重。
  • 该方法的出发点其实很好理解,对每个特征做embedding,然后交叉,让模型去学习embedding,那么对于类别特征如何embedding呢? 如果直接one-hot太稀疏,所以模型里增加了 embedding 层,将高维稀疏输入向量压缩成低维稠密向量,使得类别特征中每个取值都有对应的参数向量可以表征。
  • 对模型来说,输入即可以是各种ID特征,也可以是经过人工特征工程后提取的特征

参考资料

  • https://github.com/ChenglongChen/tensorflow-DeepFM
  • https://arxiv.org/abs/1703.04247
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值