推荐系统(五)wide&deep
作者:互联网
推荐系统(五)wide&deep
推荐系统系列博客:
- 推荐系统(一)推荐系统整体概览
- 推荐系统(二)GBDT+LR模型
- 推荐系统(三)Factorization Machines(FM)
- 推荐系统(四)Field-aware Factorization Machines(FFM)
这篇博客主要介绍谷歌于2016年发表在RecSys上的一篇文章,俗话说:谷歌家出品,必属精品。这篇文章提出的模型wide&deep着实对推荐系统领域有着非常大的影响,启发了后面几年推荐系统领域的一些工作,比如:deep&cross,deepFM等。这篇文章也是秉承着G家文章一贯的风格【大道至简,非常关注工程实践】,不像国内某互联网公司某团队的paper简直是概念的堆砌,如果没有概念就造概念,让人看的眼花缭乱,明显是冲着发论文去的。这篇博客主要以下几个点来介绍下wide&deep模型。
一、提出wide&deep的动机
在这篇论文之前,工业界推荐系统主流的模型基本是LR或者普通的DNN(当然也有像FM和树模型及其变种),通常而言,线性模型比如LR比较擅长记忆,而DNN则比较擅长泛化。这里来解释下何为记忆,何为泛化。
- 记忆: 其实就是模型直接学到样本中一些强特征,比如,尿布&瓶酒的例子,如果样本中频繁出现了买了尿布的人同时买了啤酒,也就是说尿布&啤酒共现的频率很高,那么模型可以直接记住这种共现,当做推荐的时候,如果一个人买了尿布,那么应该给他推荐啤酒。
- 泛化: 泛化则是学习特征的传递能力,比如某个训练集为树叶,label为判断输入的东西是否是一个树叶,样本中的树叶基本都是绿色的锯齿状树叶,那么我们希望模型能够有泛化能力,当输入一个泛黄的圆形树叶,模型也能判断出这是个树叶。
因此,为了结合这两种能力,提出了wide&deep模型,wide侧为一个LR模型,负责记忆,deep部分为一个多层全连接网络,负责泛化。
二、wide&deep模型结构
从上图能够非常清晰的看到wide&deep模型包含两个部分,wide部分和deep部分。wide部分为一个线性模型LR,deep部分为一个三层隐藏层[1024,512,256]的DNN。模型网络结构虽然简直直白,但有几个细节需要关注下:
- 训练方式: wide部分和deep部分使用logistics loss连接在一起,通过bp联合训练,而不是单独训练然后做ensemble。
- 优化算法: wide部分采用G家出产的FTRL优化算法(关于FTRL后面会单独写一篇博客),deep部分采用AdaGrad(关于AdaGrad参见本人的博客:深度学习中优化方法——momentum、Nesterov Momentum、AdaGrad、Adadelta、RMSprop、Adam)那么随之而来的2个问题:(a)两个不同的优化算法放在一起怎么优化?(b)为什么LR部分采用ftrl,而deep部分采用AdaGrad。 这两个问题留到博客最后的【一些思考】里再介绍。
- 特征: 在Google play的场景下主要分为连续值特征和类别特征,在这篇论文中,连续值特征被归一化到[0,1]范围内,类别特征则做了embedding,然后直接做了concate,输入到deep网络中。wide部分主要输入的是一些交叉特征,用于记忆。
- 模型训练: 采用热启的方式训练模型,上一次(天级别或者小时级)的模型embedding向量和权重初始化下一个时间的模型(ented a warm-starting system which initializes a new model with the embeddings and the linear model weights from the previous model.)。这一点和我厂的abacus保持一致。这样做的好处显而易见,lifetime learning + Fine Tune。
- 模型更新校验: 每次更新到线上之前要做一次校验,校验待更新的模型是否正常,防止把有问题的模型更新到线上,发生问题。这一点透露着浓浓的工业风,也是一个工业系统必备的环节。
三、一些思考
问题1: wide部分和deep采用联合训练,但wide部分采用FTRL优化算法,deep部分采用AdaGrad优化算法,这个该怎么训练?
这个问题直接看TensorFlow官方代码:https://github.com/tensorflow/tensorflow/blob/r1.11/tensorflow/python/estimator/canned/dnn_linear_combined.py
deep侧:
# deep侧
with variable_scope.variable_scope(
dnn_parent_scope,
values=tuple(six.itervalues(features)),
partitioner=dnn_partitioner) as scope:
dnn_absolute_scope = scope.name
dnn_logit_fn = dnn._dnn_logit_fn_builder( # pylint: disable=protected-access
units=head.logits_dimension,
hidden_units=dnn_hidden_units,
feature_columns=dnn_feature_columns,
activation_fn=dnn_activation_fn,
dropout=dnn_dropout,
input_layer_partitioner=input_layer_partitioner,
batch_norm=batch_norm)
dnn_logits = dnn_logit_fn(features=features, mode=mode)
wide侧:
# wide侧
with variable_scope.variable_scope(
linear_parent_scope,
values=tuple(six.itervalues(features)),
partitioner=input_layer_partitioner) as scope:
linear_absolute_scope = scope.name
logit_fn = linear._linear_logit_fn_builder( # pylint: disable=protected-access
units=head.logits_dimension,
feature_columns=linear_feature_columns,
sparse_combiner=linear_sparse_combiner)
linear_logits = logit_fn(features=features)
loss直接把wide部分的loss和deep部分的loss相加
# loss函数
if n_classes == 2:
head = head_lib._binary_logistic_head_with_sigmoid_cross_entropy_loss( # pylint: disable=protected-access
weight_column=weight_column,
label_vocabulary=label_vocabulary,
loss_reduction=loss_reduction)
else:
head = head_lib._multi_class_head_with_softmax_cross_entropy_loss( # pylint: disable=protected-access
n_classes,
weight_column=weight_column,
label_vocabulary=label_vocabulary,
loss_reduction=loss_reduction)
# Combine logits and build full model.
if dnn_logits is not None and linear_logits is not None:
logits = dnn_logits + linear_logits
elif dnn_logits is not None:
logits = dnn_logits
else:
logits = linear_logits
*BP时,采用不用的优化器优化wide侧和deep侧,这里核心语句在于:train_op = control_flow_ops.group(train_ops),从而做到了可以用不同的优化器来优化两侧。
def _train_op_fn(loss):
"""Returns the op to optimize the loss."""
train_ops = []
global_step = training_util.get_global_step()
if dnn_logits is not None:
train_ops.append(
dnn_optimizer.minimize(
loss,
var_list=ops.get_collection(
ops.GraphKeys.TRAINABLE_VARIABLES,
scope=dnn_absolute_scope)))
if linear_logits is not None:
train_ops.append(
linear_optimizer.minimize(
loss,
var_list=ops.get_collection(
ops.GraphKeys.TRAINABLE_VARIABLES,
scope=linear_absolute_scope)))
# 核心语句,采用group函数
train_op = control_flow_ops.group(*train_ops)
with ops.control_dependencies([train_op]):
return state_ops.assign_add(global_step, 1).op
return head.create_estimator_spec(
features=features,
mode=mode,
labels=labels,
train_op_fn=_train_op_fn,
logits=logits)
问题2: 为什么wide侧采用FTRL,而deep侧采用AdaGrad?
我个人觉得在wide侧采用FTRL,一方面是为了产生稀疏解,毕竟在论文中能够看到wide部分的交叉特征是两个id特征的交叉,这样可以大大缩小模型体积,利于线上部署。另外一方面,由于联合训练,必然会出现wide部分收敛速度远远快于deep部分,应该是谷歌大佬们为了缓解这种情况,因为ftrl和adagrad都是随着梯度的累计,学习率会变小,并且ftrl结合了L1正则和L2正则,使得ftrl收敛速度慢。
注:这一点只是个人理解,如果有大佬有更好的理解,欢迎留言交流。
参考文献
[1]: Wide & Deep Learning for Recommender Systems
标签:wide,linear,推荐,dnn,deep,scope,logits 来源: https://blog.csdn.net/u012328159/article/details/121453763