论文阅读:Wide & Deep Learning for Recommender Systems

一、模型引入原因

在CTR应用中,常用的模型可以分为两类:线性模型、非线性模型
这两者各有利弊:

1、线性模型

优点
利用手工构造的交叉组合特征来使线性模型具有“记忆性”,使模型记住共现频率较高的特征组合,且可解释性强。(Memorization能力强)(这俩名词在后面解释)
缺点
首先,特征工程需要耗费太多精力。其次,因为模型是强行记住这些组合特征的,所以对于未曾出现过的特征组合,权重系数为0,无法进行泛化。(Generalization能力差)

2、DNN结构(非线性)

优点
基于Embedding的方式能够有效提高模型的泛化能力。(Generalization能力强)
缺点
基于Embedding的方式可能因为数据长尾分布(什么是长尾分布可参考这篇博客),导致长尾的一些特征值无法被充分学习,其对应的Embedding vector是不准确的,会造成模型泛化过度,模型记忆性差。(Memorization能力差)

3、Memorization与Generalization

先来看一下原论文中对此的定义
原论文下载地址

Memorization can be loosely defined as learning the frequent co-occurrence of items or features and exploiting the correlation available in the historical data.
Generalization, on the other hand, is based on transitivity of correlation and explores new feature combinations that have never or rarely occurred in the past.

翻译一下就是:
记忆可以大致定义为学习物品或特征的高频共现,并利用历史数据中的相关性。
另一方面,泛化是基于相关的传递性,并探索那些从未或很少发生过新的特征组合。

直白的说就是
Memorization就是模型能够从历史数据中学习到高频共现的特征组合的能力。(这是线性模型的优势)
Generalization代表模型能够利用相关性的传递性去探索历史数据中从未出现过的特征组合。(这是DNN结构的优势)

Memorization趋向于更加保守,推荐用户之前有过行为的items。
相比之下,generalization更加趋向于提高推荐系统的多样性(diversity)。

Wide&Deep模型,就是将线性模型与DNN很好的结合起来,在提高模型泛化能力的同时,兼顾模型的记忆性。(wide就是线性部分,deep就是DNN部分)

二、模型结构

在这里插入图片描述
图中最左边是模型的Wide部分,这个部分可以使用广义线性模型(例如LR)来替代。所以Wide&Deep其实是一类模型的统称,将LR换成FM同样也是一个Wide&Deep模型(要注意这并不是DeepFM模型)。模型的Deep部分则是一个简单的基于Embedding的全连接网络,结构与FNN一致。

1、wide部分

这部分呢其实就是一个广义的线性模型
在这里插入图片描述
在这里插入图片描述
其中, X = [ x 1 , x 2 , x 3...... x d ] {X=[x1,x2,x3......xd]} X=[x1,x2,x3......xd]是d维的特征向量,
ϕ ( X ) = [ ϕ 1 ( X ) , ϕ 2 ( X ) , ϕ 3 ( X ) . . . . . . ϕ k ( X ) ] {ϕ(X)=[ϕ1(X),ϕ2(X),ϕ3(X)......ϕk(X)]} ϕ(X)=[ϕ1(X),ϕ2(X),ϕ3(X)......ϕk(X)]是k维特征转化函数向量。
其中最常用的特征转换函数便是特征交叉函数,定义为
在这里插入图片描述
当且仅当 xi 是第 k 个特征变换的一部分时,cki=1 。否则为0。
举一个网上关于该模型对常见的例子来说:对于二值特征,一个特征交叉函数为 And(gender=female,language=en) ,这个函数中只涉及到特征 female 与 en ,所以其他特征值对应的 cki=0 ,即可忽略。当样本中 female 与 en 同时存在时,该特征交叉函数为1,否则为0。这种特征组合可以为模型引入非线性。

2、DNN部分

在这里插入图片描述
DNN部分就是一个简单的全连接网络:
在这里插入图片描述
其中   a l {\ a}^l  al   b l {\ b}^l  bl   W l {\ W}^l  Wl、f分别代表第L层的输入、偏置项、参数项与激活函数。

3、Wide&Deep部分

在这里插入图片描述
Wide与Deep侧都准备完毕之后,对两部分输出进行简单 加权求和 即可作为最终输出。对于简单二分类任务而言可以定义为:
在这里插入图片描述
其中,WTwide[X,ϕ(X)] 为Wide输出结果,WTdeep 为Deep侧作用到最后一层激活函数输出的参数,Deep侧最后一层激活函数输出结果为 a(lf) ,b 为全局偏置项,σ 为 sigmoid 激活函数 。

三、工程实现

在这里插入图片描述
Google使用的pipeline如下,共分为三个部分:Data Generation、Model Training与Model Serving。

1、Data Generation

本阶段负责对数据进行预处理,供给到后续模型训练阶段。内容包括用户数据收集、样本构造。
对于类别特征,首先过滤掉低频特征,然后构造映射表,将类别字段映射为编号,即token化。
对于连续特征可以根据其分布进行离散化,论文中采用的方式为等分位数分桶方式,然后再放缩至[0,1]区间。

2、Model Training

对Google paly场景,作者构造了如下结构的Wide&Deep模型。在Deep侧,连续特征处理完之后直接送入全连接层,对于类别特征首先输入到Embedding层,然后再连接到全连接层,与连续特征向量拼接。在Wide侧,作者仅使用了用户历史安装记录与当前候选app作为输入。
在这里插入图片描述
作者采用这种“重Deep,轻Wide”的结构完全是根据应用场景的特点来的。Google play因为数据长尾分布,对于一些小众的app在历史数据中极少出现,其对应的Embedding学习不够充分,需要通过Wide部分Memorization来保证最终预测的精度。
作者在训练该模型时,使用了5000亿条样本,这也说明了Wide&Deep并没有那么容易训练。为了避免每次从头开始训练,每次训练都是先load上一次模型的得到的参数,然后再继续训练。有实验说明,类似于FNN使用预训练FM参数进行初始化可以加速Wide&Deep收敛。

3、Model Serving

在实际推荐场景,并不会对全量的样本进行预测。而是针对召回阶段返回的一小部分样本进行打分预测,同时还会采用多线程并行预测,严格控制线上服务时延。

四、实验结果与总结

在这里插入图片描述
优缺点:
优点在第一部分已经介绍过了,缺点也很明显,那就是Wide侧的特征工程仍无法避免。

五、代码实现

class WideDeep(object):
    def __init__(self, vec_dim=None, field_lens=None, dnn_layers=None, wide_lr=None, l1_reg=None, deep_lr=None):
        self.vec_dim = vec_dim
        self.field_lens = field_lens
        self.field_num = len(field_lens)
        self.dnn_layers = dnn_layers
        self.wide_lr = wide_lr
        self.l1_reg = l1_reg
        self.deep_lr = deep_lr

        assert isinstance(dnn_layers, list) and dnn_layers[-1] == 1
        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 inference(self):
        with tf.variable_scope('wide_part'):
            w0 = tf.get_variable(name='bias', shape=[1], dtype=tf.float32)
            linear_w = [tf.get_variable(name='linear_w_%d'%i, shape=[self.field_lens[i]], dtype=tf.float32) for i in range(self.field_num)]
            wide_part = w0 + tf.reduce_sum(
                tf.concat([tf.reduce_sum(tf.multiply(self.x[i], linear_w[i]), axis=1, keep_dims=True) for i in range(self.field_num)], axis=1),
                axis=1, keep_dims=True) # (batch, 1)
        with tf.variable_scope('dnn_part'):
            emb = [tf.get_variable(name='emb_%d'%i, shape=[self.field_lens[i], self.vec_dim], dtype=tf.float32) 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
            in_node = self.field_num * self.vec_dim
            for i in range(len(self.dnn_layers)):
                out_node = self.dnn_layers[i]
                w = tf.get_variable(name='w_%d' % i, shape=[in_node, out_node], dtype=tf.float32)
                b = tf.get_variable(name='b_%d' % i, shape=[out_node], dtype=tf.float32)
                in_node = out_node
                if out_node != 1:
                    x = tf.nn.relu(tf.matmul(x, w) + b)
                else:
                    self.y_logits = wide_part + 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))

        # set optimizer
        self.global_step = tf.train.get_or_create_global_step()

        wide_part_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='wide_part')
        dnn_part_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='dnn_part')

        wide_part_optimizer = tf.train.FtrlOptimizer(learning_rate=self.wide_lr, l1_regularization_strength=self.l1_reg)
        wide_part_op = wide_part_optimizer.minimize(loss=self.loss, global_step=self.global_step, var_list=wide_part_vars)

        dnn_part_optimizer = tf.train.AdamOptimizer(learning_rate=self.deep_lr)
        # set global_step to None so only wide part solver gets passed in the global step;
        # otherwise, all the solvers will increase the global step
        dnn_part_op = dnn_part_optimizer.minimize(loss=self.loss, global_step=None, var_list=dnn_part_vars)

        self.train_op = tf.group(wide_part_op, dnn_part_op)

参考:
https://www.cnblogs.com/yinzm/p/11878831.html
https://mp.weixin.qq.com/s/6UMr3EXoBNwbpfrCOGth_Q
https://blog.csdn.net/yujianmin1990/article/details/78989099?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值