小赵带你读论文系列14-阿里妈妈之Deep Interest Network for Click-Through Rate Prediction


欢迎关注公众号,DataDesigner,让我们一起白话机器学习。

前言

鬼才知道我为什么要学管理经济学和管理数学,学不进去了,算了看看论文读读代码放松一下。

这是阿里DIN系列的第一篇文章,文章读起来不是很难,不过工业上实现会考虑时延就很麻烦,主要的工作我觉得像是引入了Attention机制(他自己解释说有点差别)。以往都是根据用户的Profile,Behavior Sequence做一个用户的Embedding,然后根据这个Embedding来和待推荐的商品作比较,最终输出用户点击该商品的概率。阿里觉得不好,第一,这种对待所有商品用户的Embedding都一成不变,这显然不行。第二,单纯扩大Embeddingsize来获取更多的信息,这样会浪费大量的计算成本并且有过拟合现象。以往的推荐形式如下图:

主体内容

第一,阿里妈妈觉得待推荐的物品是否会被点击和用户的历史行为有着显著的关系,那么我们能不能使用用户的历史行为(也代表了用户的兴趣,Deep Interest大概从这里命名)来表征用户对于不同商品的兴趣呢,换句话说,我能不能使得用户对于不同的商品有不同的Embedding呢。然后他就把待推荐物品和用户历史点击行为进行Attention,他死不承认用Attention的一个原因大体就是他的Attention没有做正则化,以及他的Attention weightssum=1。模型构建如下:

第二,他在工业上做了两个优化。第一个优化在每次批处理时,仅使用那些在当前Batch中非零(至少出现过一次)的特征作为正则化项(这式子翻译过来就是对于出现频率高的,给与较小的正则化强度;
3.对于出现频率低的,给予较大的正则化强度。),因为如果使用全特征参数的话,会增大计算量,丢弃的话就会过拟合。公式如下(文中还做了一个近似的操作,如公式2,文中那个λ是个需要手动调节的超参数,不用管):

他在工业上实现的第二个优化就是使用了一个长得非常奇葩的激活函数,这个函数为什么要这么写我还真不知道,原文里说的是分割点应该由数据决定(PRelu==LeakyRelu)而且可以防止w更新缓慢,但看着就像Sigmoid平移了。。对了他给这个激活函数的名字叫做Dice

第三,这个评价函数不像是AUC,后来发现这是推荐中常用的手法,叫做GAUC(如公式1),只需要保证用户点击的排在前面就可以,不是一定要排在第一个,具体可以详见GAUC,而且还有一种新奇的比较模型之间提升关系的方法,我很喜欢,如公式2:

第四,算是对自己的一个提醒,这玩意CaseStudy十分重要啊,字数不够CaseStudy来凑,还能加上几个好看的图片和TSNE降维(颜色越深代表预测的点击概率越大)。

代码实现

仅贴上论文主体框架的代码,本人加了几个注释,方便大家理解一下数据传输的流程。原代码真的是晦涩难懂。。。。命名习惯极差,推荐大家参照PEP8命名规范

import tensorflow as tf

from ..feature_column import SparseFeat, VarLenSparseFeat, DenseFeat, build_input_features
from ..inputs import create_embedding_matrix, embedding_lookup, get_dense_input, varlen_embedding_lookup, \
    get_varlen_pooling_list
from ..layers.core import DNN, PredictionLayer
from ..layers.sequence import AttentionSequencePoolingLayer
from ..layers.utils import concat_func, NoMask, combined_dnn_input


def DIN(dnn_feature_columns, history_feature_list, dnn_use_bn=False,
        dnn_hidden_units=(200, 80), dnn_activation='relu', att_hidden_size=(80, 40), att_activation="dice",
        att_weight_normalization=False, l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, seed=1024,
        task='binary'):

    features = build_input_features(dnn_feature_columns)

    sparse_feature_columns = list(
        filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns)) if dnn_feature_columns else []
    dense_feature_columns = list(
        filter(lambda x: isinstance(x, DenseFeat), dnn_feature_columns)) if dnn_feature_columns else []
    varlen_sparse_feature_columns = list(
        filter(lambda x: isinstance(x, VarLenSparseFeat), dnn_feature_columns)) if dnn_feature_columns else []  # 特征分类

    history_feature_columns = []
    sparse_varlen_feature_columns = []
    history_fc_names = list(map(lambda x: "hist_" + x, history_feature_list))
    for fc in varlen_sparse_feature_columns:
        feature_name = fc.name
        if feature_name in history_fc_names:
            history_feature_columns.append(fc)
        else:
            sparse_varlen_feature_columns.append(fc)

    inputs_list = list(features.values())

    embedding_dict = create_embedding_matrix(dnn_feature_columns, l2_reg_embedding, seed, prefix="") # 为每个特征做embedding

    query_emb_list = embedding_lookup(embedding_dict, features, sparse_feature_columns, history_feature_list,
                                      history_feature_list, to_list=True) # 根据列表查找特征id的embedding
    keys_emb_list = embedding_lookup(embedding_dict, features, history_feature_columns, history_fc_names,
                                     history_fc_names, to_list=True)
    dnn_input_emb_list = embedding_lookup(embedding_dict, features, sparse_feature_columns,
                                          mask_feat_list=history_feature_list, to_list=True)
    dense_value_list = get_dense_input(features, dense_feature_columns)

    sequence_embed_dict = varlen_embedding_lookup(embedding_dict, features, sparse_varlen_feature_columns)
    sequence_embed_list = get_varlen_pooling_list(sequence_embed_dict, features, sparse_varlen_feature_columns,
                                                  to_list=True) # 变长变为定长

    dnn_input_emb_list += sequence_embed_list  # 合并列表

    keys_emb = concat_func(keys_emb_list, mask=True) # Goods对应的列表都concat起来,
    deep_input_emb = concat_func(dnn_input_emb_list)
    query_emb = concat_func(query_emb_list, mask=True)
    hist = AttentionSequencePoolingLayer(att_hidden_size, att_activation,
                                         weight_normalization=att_weight_normalization, supports_masking=True)([
        query_emb, keys_emb]) # 根据Ads和Goods的embedding计算Attention

    deep_input_emb = tf.keras.layers.Concatenate()([NoMask()(deep_input_emb), hist])
    deep_input_emb = tf.keras.layers.Flatten()(deep_input_emb)
    dnn_input = combined_dnn_input([deep_input_emb], dense_value_list)
    output = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn, dnn_dropout, dnn_use_bn, seed=seed)(dnn_input) # 喂入一个Dnn
    final_logit = tf.keras.layers.Dense(1, use_bias=False,
                                        kernel_initializer=tf.keras.initializers.glorot_normal(seed))(output)

    output = PredictionLayer(task)(final_logit)

    model = tf.keras.models.Model(inputs=inputs_list, outputs=output) # 这里写法真不一样,这里直接用Model而不是层返回,有可能为了封装方便。
    return model

当然了trainset和testset的构建很有意思,具体可以参照DIN输入输出,尤其注意的是maxlen=所有用户的所有历史记录中的最长序列,因为一个用户有多个序列,采用最长的进行padding。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Data_Designer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值