vlambda博客
学习文章列表

[阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本

[阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本

0x00 摘要0x01 背景1.1 代码进化1.2 Deepctr1.2.1 统一视角1.2.2 模块化1.2.3 框架优点0x2 测试数据2.1 数据集介绍2.2 原始样本骨架raw_sample2.3 广告基本信息表ad_feature2.4 用户基本信息表user_profile2.5 用户的行为日志behavior_log2.6 典型科研场景0x03 目录结构0x04 数据构造4.1 生成采样数据4.2 生成DIEN需要的输入0x05 DIEN模型5.1 train_dien.py5.2 dien.py5.2.1 第二版本5.2.2 输入参数5.2.3 构建向量5.2.4 兴趣进化层5.2.4.1 DynamicGRU 15.2.4.2 auxiliary_loss5.2.4.3 auxiliary_net5.2.4.4 AttentionSequencePoolingLayer5.2.4.5 DynamicGRU 25.2.5 DNN全连接层0xFF 参考

0x00 摘要

DIEN是阿里深度兴趣进化网络(Deep Interest Evolution Network)的缩写。

之前我们对DIEN的源码进行了解读,那是基于 https://github.com/mouna99/dien 中的实现。

后来因为继续看DSIN,发现在DSIN代码https://github.com/shenweichen/DSIN中,也有DIEN的新实现。

于是阅读整理,遂有此文。

0x01 背景

1.1 代码进化

大家都知道,阿里此模型是的演化脉络是:DIN,DIEN,DSIN,……

随之代码就有三个版本。

第一个版本作者直接就说明效率不行,推荐第二个版本 https://github.com/mouna99/dien 。此版本代码就是纯tensorflow代码,一招一式扎扎实实,读起来也顺爽。

第三个版本则是基于Keras与deepctr,同前一版本相比则是从游击队升级为正规军,各种高大上和套路。

1.2 Deepctr

之所以成为正规军,deepctr作用相当大,下面就介绍下。

DeepCtr是一个简易的CTR模型框架,集成了深度学习流行的所有模型,适合学推荐系统模型的人参考。

  • github 传送门

  • 官方文档传送门

这个项目主要是对目前的一些基于深度学习的点击率预测算法进行了实现,如PNN,WDL,DeepFM,MLR,DeepCross,AFM,NFM,DIN,DIEN,xDeepFM,AutoInt等,并且对外提供了一致的调用接口。

1.2.1 统一视角

Deepctr的出现不仅仅降低了广告点击率预测模型的上手难度,方便进行模型对比,也让给了我们机会从这些优秀的源码中学习到构建模型的方式。

DeepCTR的设计主要是面向那些对深度学习以及CTR预测算法感兴趣的同学,使他们可以利用这个包:

  • 从一个统一视角来看待各个模型

  • 快速地进行简单的对比实验

  • 利用已有的组件快速构建新的模型

1.2.2 模块化

DeepCTR通过对现有的基于深度学习的点击率预测模型的结构进行抽象总结,在设计过程中采用模块化的思路,各个模块自身具有高复用性,各个模块之间互相独立。基于深度学习的点击率预测模型按模型内部组件的功能可以划分成以下4个模块:

  • 输入模块

  • 嵌入模块

  • 特征提取模块

  • 预测输出模块

所有的模型都是严格按照4个模块进行搭建的,输入和嵌入以及输出基本都是公用的,每个模型的差异之处主要在特征提取部分。

1.2.3 框架优点

  • 整体结构清晰灵活,linear返回logit,FM层返回logit,deep包含中间层结果,在每一种模型中打包deep的最后一层,判断linear,fm和deep是否需要,最后接入全连接层。

  • 主要用到的模块和架构:keras的Concatenate(list转tensor),Dense(最后的全连接层和dense),Embedding(sparse,dense,sequence),Input(sparse,dense,sequce)还有常规操作:优化器,正则化项

  • 复用了重载了Layer层,重写了build,call,compute_output_shape,compute_mask,get_config

下面我们就开始深入看看最新版本的DIEN。

0x2 测试数据

DIEN使用的是天池的数据,readme中提示下载:

1. Download Dataset [Ad Display/Click Data on Taobao.com](https://tianchi.aliyun.com/dataset/dataDetail?dataId=56)
2. Extract the files into the ``raw_data`` directory

Ali_Display_Ad_Click是阿里巴巴提供的一个淘宝展示广告点击率预估数据集。

2.1 数据集介绍

数据名称 说明 属性
raw_sample 原始的样本骨架 用户ID,广告ID,时间,资源位,是否点击
ad_feature 广告的基本信息 广告ID,广告计划ID,类目ID,品牌ID
user_profile 用户的基本信息 用户ID,年龄层,性别等
raw_behavior_log 用户的行为日志 用户ID,行为类型,时间,商品类目ID,品牌ID

2.2 原始样本骨架raw_sample

从淘宝网站中随机抽样了114万用户8天内的广告展示/点击日志(2600万条记录),构成原始的样本骨架。
字段说明如下:

  • (1) user_id:脱敏过的用户ID;

  • (2) adgroup_id:脱敏过的广告单元ID;

  • (3) time_stamp:时间戳;

  • (4) pid:资源位;

  • (5) noclk:为1代表没有点击;为0代表点击;

  • (6) clk:为0代表没有点击;为1代表点击;

我们用前面7天的做训练样本(20170506-20170512),用第8天的做测试样本(20170513)。

2.3 广告基本信息表ad_feature

本数据集涵盖了raw_sample中全部广告的基本信息。字段说明如下:

  • (1) adgroup_id:脱敏过的广告ID;

  • (2) cate_id:脱敏过的商品类目ID;

  • (3) campaign_id:脱敏过的广告计划ID;

  • (4) customer_id:脱敏过的广告主ID;

  • (5) brand:脱敏过的品牌ID;

  • (6) price: 宝贝的价格

其中一个广告ID对应一个商品(宝贝),一个宝贝属于一个类目,一个宝贝属于一个品牌。

2.4 用户基本信息表user_profile

本数据集涵盖了raw_sample中全部用户的基本信息。字段说明如下:

  • (1) userid:脱敏过的用户ID;

  • (2) cms_segid:微群ID;

  • (3) cms_group_id:cms_group_id;

  • (4) final_gender_code:性别 1:男,2:女;

  • (5) age_level:年龄层次;

  • (6) pvalue_level:消费档次,1:低档,2:中档,3:高档;

  • (7) shopping_level:购物深度,1:浅层用户,2:中度用户,3:深度用户

  • (8) occupation:是否大学生 ,1:是,0:否

  • (9) new_user_class_level:城市层级

2.5 用户的行为日志behavior_log

本数据集涵盖了raw_sample中全部用户22天内的购物行为(共七亿条记录)。字段说明如下:

  • (1) user:脱敏过的用户ID;

  • (2) time_stamp:时间戳;

  • (3) btag:行为类型, 包括以下四种:

类型 说明
ipv 浏览
cart 加入购物车
fav 喜欢
buy 购买
  • (4) cate:脱敏过的商品类目;

  • (5) brand: 脱敏过的品牌词;

这里以user + time_stamp为key,会有很多重复的记录;这是因为我们的不同的类型的行为数据是不同部门记录的,在打包到一起的时候,实际上会有小的偏差(即两个一样的time_stamp实际上是差异比较小的两个时间)。

2.6 典型科研场景

根据用户历史购物行为预测用户在接受某个广告的曝光时的点击概率。

基线
AUC:0.622

0x03 目录结构

代码目录结构如下,前面五个是数据处理,models下面是模型,train_xxx是训练代码。

.
├── 0_gen_sampled_data.py
├── 1_gen_sessions.py
├── 2_gen_dien_input.py
├── 2_gen_din_input.py
├── 2_gen_dsin_input.py
├── config.py
├── config.pyc
├── models
│   ├── __init__.py
│   ├── dien.py
│   ├── din.py
│   └── dsin.py
├── train_dien.py
├── train_din.py
└── train_dsin.py

0x04 数据构造

下面我们分析数据构造部分。

4.1 生成采样数据

0_gen_sampled_data.py  的作用是生成采样数据:

  • 基本逻辑如下:

  • 按照比率用用户中提取采样;

  • 从原始样本骨架raw_sample中提取采样用户对应的数据;

  • 对于用户数据进行去重;

  • 从行为数据中提取采样用户对应的数据;

  • 对于ad['brand']的缺失数据补充-1;

  • 使用 LabelEncoder对特征进行硬编码,将文本特征进行编号:

  • 对ad['cate_id']和log['cate']进行去重合并,然后编码;

  • 对ad['brand']和log['brand']进行去重合并,然后编码;

  • log去除btag列;

  • log去除时间戳非法列;

  • 然后存储成文件;

4.2 生成DIEN需要的输入

2_gen_dien_input.py的作用是生成DIEN需要的输入,主要逻辑是:

获取采样数据中用户session相关文件( 这部分其实DIEN不需要 )。

FILE_NUM = len(
    list(
        filter(lambda x: x.startswith('user_hist_session_' + str(FRAC) + '_din_'), os.listdir('../sampled_data/'))))

遍历文件,把数据放入user_hist_session_

for i in range(FILE_NUM):
    user_hist_session_ 
= pd.read_pickle(
        '../sampled_data/user_hist_session_' + str(FRAC) + '_din_' + str(i) + '.pkl')
    user_hist_session.update(user_hist_session_)
    del user_hist_session_

使用gen_sess_feature_dien来生成session数据,并且分别生成字典,并且用进度条显示。

生成一个dict,每个value是用户行为(cate_id,brand,time_stamp)列表:

sess_input_dict = {'cate_id': [], 'brand': []}
neg_sess_input_dict = {'cate_id': [], 'brand': []}
sess_input_length = []
for row in tqdm(sample_sub[['user''time_stamp']].iterrows()):
    a, b, n_a, n_b, c 
= gen_sess_feature_dien(row)
    sess_input_dict['cate_id'].append(a)
    sess_input_dict['brand'].append(b)
    neg_sess_input_dict['cate_id'].append(n_a)
    neg_sess_input_dict['brand'].append(n_b)
    sess_input_length.append(c)

gen_sess_feature_dien函数获取session数据。

  • 从后往前遍历当前用户的历史session,把每个session中小于时间戳的都取出来。

  • 通过 for e in cur_sess[max(0, i + 1 - sess_max_len):i + 1]] 来取出session中最后sess_max_len个log,

  • 通过 sample 函数来生成负采样数据,

  • 最后得到 'cate_id', 'brand' 两个数据。

def gen_sess_feature_dien(row):
    sess_max_len 
= DIN_SESS_MAX_LEN
    sess_input_dict = {'cate_id': [0], 'brand': [0]}
    neg_sess_input_dict = {'cate_id': [0], 'brand': [0]}
    sess_input_length = 0
    user, time_stamp = row[1]['user'], row[1]['time_stamp']
    if user not in user_hist_session or len(user_hist_session[user]) == 0:

        sess_input_dict['cate_id'] = [0]
        sess_input_dict['brand'] = [0]
        neg_sess_input_dict['cate_id'] = [0]
        neg_sess_input_dict['brand'] = [0]
        sess_input_length = 0
    else:
        cur_sess = user_hist_session[user][0]
        for i in reversed(range(len(cur_sess))):
            if cur_sess[i][2] < time_stamp:
                sess_input_dict['cate_id'] 
= [e[0]
                                              for e in cur_sess[max(0, i + 1 - sess_max_len):i + 1]]
                sess_input_dict['brand'] = [e[1]
                                            for e in cur_sess[max(0, i + 1 - sess_max_len):i + 1]]

                neg_sess_input_dict = {'cate_id': [], 'brand': []}

                for c in sess_input_dict['cate_id']:
                    neg_cate, neg_brand = sample(c)
                    neg_sess_input_dict['cate_id'].append(neg_cate)
                    neg_sess_input_dict['brand'].append(neg_brand)

                sess_input_length = len(sess_input_dict['brand'])
                break
    return sess_input_dict['cate_id'], sess_input_dict['brand'], neg_sess_input_dict['cate_id'], neg_sess_input_dict[
        'brand'], sess_input_length

sample生成负采样数据,就是随机生成 index,然后如果对应的 category 等于 cate_id ,则重新生成index。以此获取一个采样数据。

def sample(cate_id):
    global ad
    while True:
        i 
= np.random.randint(0, ad.shape[0])
        sample_cate = ad.iloc[i]['cate_id']
        if sample_cate != cate_id:
            break
    return sample_cate, ad.iloc[i]['brand']

对于user的缺失数值用 -1 填充;把new_user_class_level重命名;把user 重命名为 'userid';

user = user.fillna(-1)
user.rename(
    columns={'new_user_class_level ''new_user_class_level'}, inplace=True)
sample_sub.rename(columns={'user''userid'}, inplace=True)

对sample_sub, user做连接,对 data, ad 做连接。

data = pd.merge(sample_sub, user, how='left', on='userid', )
data = pd.merge(data, ad, how='left', on='adgroup_id')

对于sparse_features进行硬编码,将文本特征进行编号。

对于dense_features进行标准缩放。

sparse_features = ['userid''adgroup_id''pid''cms_segid''cms_group_id''final_gender_code''age_level',
                   'pvalue_level''shopping_level''occupation''new_user_class_level''campaign_id',
                   'customer']
dense_features = ['price']

for feat in tqdm(sparse_features):
    lbe 
= LabelEncoder()  # or Hash
    data[feat] = lbe.fit_transform(data[feat])
mms = StandardScaler()
data[dense_features] = mms.fit_transform(data[dense_features])

对于 sparse_features和dense_features分别构建SingleFeat,这是deepCtr中构建的namedtuple。

sparse_feature_list = [SingleFeat(feat, data[feat].nunique(
) + 1for feat in sparse_features + ['cate_id''brand']]

dense_feature_list = [SingleFeat(feat, 1for feat in dense_features]
sess_feature = ['cate_id''brand']

对于sparse_feature中的特征,从sess_input_dict和neg_sess_input_dict中获取value,构建sequence,这两个是session数据,就是行为数据。

sess_input = [pad_sequences(
    sess_input_dict[feat], maxlen=DIN_SESS_MAX_LEN, padding='post'for feat in sess_feature]
neg_sess_input = [pad_sequences(neg_sess_input_dict[feat], maxlen=DIN_SESS_MAX_LEN, padding='post'for feat in
                  sess_feature]

对于sparse_feature_list和dense_feature_list中的特征,遍历data中的对应value,构建成model_input。

把sess_input,neg_sess_input 和 [np.array(sess_input_length)]三个构建成sess_lists;

将sess_lists加入到model_input;

model_input = [data[feat.name].values for feat in sparse_feature_list] + \
              [data[feat.name].values for feat in dense_feature_list]
sess_lists = sess_input + neg_sess_input + [np.array(sess_input_length)]
model_input += sess_lists

接下来是把数据存入文件。

pd.to_pickle(model_input, '../model_input/dien_input_' +
             str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl')
pd.to_pickle(data['clk'].values, '../model_input/dien_label_' +
             str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl')
try:
    pd.to_pickle({'sparse': sparse_feature_list, 'dense': dense_feature_list},
                 '../model_input/dien_fd_' + str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl', )
except:
    pd.to_pickle({'sparse': sparse_feature_list, 'dense': dense_feature_list},
                 '../model_input/dien_fd_' + str(FRAC) + '_' + str(DIN_SESS_MAX_LEN) + '.pkl', )

0x05 DIEN模型

具体到模型,可以分为两部分。

  • train_dien.py 负责把模型及相关部分构建完成。

  • dien.py 就是具体模型实现;

5.1 train_dien.py

首先读入数据。

fd = pd.read_pickle('../model_input/dien_fd_' +
                    str(FRAC) + '_' + str(SESS_MAX_LEN) + '.pkl')
model_input = pd.read_pickle(
    '../model_input/dien_input_' + str(FRAC) + '_' + str(SESS_MAX_LEN) + '.pkl')
label = pd.read_pickle('../model_input/dien_label_' +
                       str(FRAC) + '_' + str(SESS_MAX_LEN) + '.pkl')

sample_sub = pd.read_pickle(
    '../sampled_data/raw_sample_' + str(FRAC) + '.pkl')

构建label。

sample_sub['idx'] = list(range(sample_sub.shape[0]))
train_idx = sample_sub.loc[sample_sub.time_stamp < 1494633600'idx'].values
test_idx = sample_sub.loc[sample_sub.time_stamp >= 1494633600'idx'].values

train_input = [i[train_idx] for i in model_input]
test_input = [i[test_idx] for i in model_input]

train_label = label[train_idx]
test_label = label[test_idx]

sess_len_max = SESS_MAX_LEN
BATCH_SIZE = 4096
sess_feature = ['cate_id''brand']
TEST_BATCH_SIZE = 2 ** 14

生成了keras模型,所以后面可以fit,predict。

model = DIEN(fd, sess_feature, 4, sess_len_max, "AUGRU", att_hidden_units=(6416),
             att_activation='sigmoid', use_negsampling=DIEN_NEG_SAMPLING)

keras的基本套路:compile,fit,predict。

model.compile('adagrad''binary_crossentropy',
              metrics=['binary_crossentropy', ])
if DIEN_NEG_SAMPLING:
    hist_ = model.fit(train_input, train_label, batch_size=BATCH_SIZE,
                      epochs=1, initial_epoch=0, verbose=1, )
    pred_ans = model.predict(test_input, TEST_BATCH_SIZE)
else:
    hist_ = model.fit(train_input[:-3] + train_input[-1:], train_label, batch_size=BATCH_SIZE, epochs=1,
                      initial_epoch=0, verbose=1, )
    pred_ans = model.predict(
        test_input[:-3] + test_input[-1:], TEST_BATCH_SIZE)

5.2 dien.py

这里是模型代码,本文核心。因为DIEN总体思想不变,所以我们重点看看与之前第二版本( https://github.com/mouna99/dien )的区别。

5.2.1 第二版本

我们回忆下第二版本 主体代码,作为对比。

class Model_DIN_V2_Gru_Vec_attGru(Model):
    def __init__(self, n_uid, n_mid, n_cat, EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE, use_negsampling=False):
        super(Model_DIN_V2_Gru_Vec_attGru, self).__init__(n_uid, n_mid, n_cat,
                                                          EMBEDDING_DIM, HIDDEN_SIZE, ATTENTION_SIZE,
                                                          use_negsampling)

        # RNN layer(-s)
        with tf.name_scope('rnn_1'):
            rnn_outputs, _ = dynamic_rnn(GRUCell(HIDDEN_SIZE), inputs=self.item_his_eb,
                                         sequence_length=self.seq_len_ph, dtype=tf.float32,
                                         scope="gru1")
            tf.summary.histogram('GRU_outputs', rnn_outputs)

        # Attention layer
        with tf.name_scope('Attention_layer_1'):
            att_outputs, alphas = din_fcn_attention(self.item_eb, rnn_outputs, ATTENTION_SIZE, self.mask,
                                                    softmax_stag=1, stag='1_1', mode='LIST', return_alphas=True)
            tf.summary.histogram('alpha_outputs', alphas)

        with tf.name_scope('rnn_2'):
            rnn_outputs2, final_state2 = dynamic_rnn(VecAttGRUCell(HIDDEN_SIZE), inputs=rnn_outputs,
                                                     att_scores = tf.expand_dims(alphas, -1),
                                                     sequence_length=self.seq_len_ph, dtype=tf.float32,
                                                     scope="gru2")
            tf.summary.histogram('GRU2_Final_State', final_state2)

        inp = tf.concat([self.uid_batch_embedded, self.item_eb, self.item_his_eb_sum, self.item_eb * self.item_his_eb_sum, final_state2], 1)
        self.build_fcn_net(inp, use_dice=True)

然后看看 最新的第三版本。

5.2.2 输入参数

DIEN的输入参数如下,大致可以从注释中知道其意义。

def DIEN(feature_dim_dict, seq_feature_list, embedding_size=8, hist_len_max=16,
         gru_type="GRU", use_negsampling=False, alpha=1.0, use_bn=False, dnn_hidden_units=(20080),
         dnn_activation='relu',
         att_hidden_units=(6416), att_activation="dice", att_weight_normalization=True,
         l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, init_std=0.0001, seed=1024, task='binary')
:

   """Instantiates the Deep Interest Evolution Network architecture.

    :param feature_dim_dict: dict,to indicate sparse field (**now only support sparse feature**)like {'sparse':{'field_1':4,'field_2':3,'field_3':2},'dense':[]}
    :param seq_feature_list: list,to indicate  sequence sparse field (**now only support sparse feature**),must be a subset of ``feature_dim_dict["sparse"]``
    :param embedding_size: positive integer,sparse feature embedding_size.
    :param hist_len_max: positive int, to indicate the max length of seq input
    :param gru_type: str,can be GRU AIGRU AUGRU AGRU
    :param use_negsampling: bool, whether or not use negtive sampling
    :param alpha: float ,weight of auxiliary_loss
    :param use_bn: bool. Whether use BatchNormalization before activation or not in deep net
    :param dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of DNN
    :param dnn_activation: Activation function to use in DNN
    :param att_hidden_units: list,list of positive integer , the layer number and units in each layer of attention net
    :param att_activation: Activation function to use in attention net
    :param att_weight_normalization: bool.Whether normalize the attention score of local activation unit.
    :param l2_reg_dnn: float. L2 regularizer strength applied to DNN
    :param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector
    :param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate.
    :param init_std: float,to use as the initialize std of embedding vector
    :param seed: integer ,to use as random seed.
    :param task: str, ``"binary"`` for  binary logloss or  ``"regression"`` for regression loss
    :return: A Keras model instance.

    """

5.2.3 构建向量

这里构建两种向量,分别是密度向量(Dense Vector)和稀疏向量(Spasre Vector)。密度向量会存储所有的值包括零值,而稀疏向量存储的是索引位置及值,不存储零值,在数据量比较大时,稀疏向量才能体现它的优势和价值。

函数get_input是从输入字典中提取向量。

def get_input(feature_dim_dict, seq_feature_list, seq_max_len):
    sparse_input, dense_input 
= create_singlefeat_inputdict(feature_dim_dict)
    user_behavior_input = OrderedDict()
    for i, feat in enumerate(seq_feature_list):
        user_behavior_input[feat] 
= Input(shape=(seq_max_len,), name='seq_' + str(i) + '-' + feat)
    user_behavior_length = Input(shape=(1,), name='seq_length')
    return sparse_input, dense_input, user_behavior_input, user_behavior_length

遍历feature_dim_dict构建特征字典,每一个item是name:Embedding。其作用是从sparse_embedding_dict中,获取sparse_input中对应的具体输入变量所对应的embedding。

sparse_embedding_dict = {feat.name: Embedding(feat.dimension, embedding_size,
                                              embeddings_initializer=RandomNormal(
                                                  mean=0.0, stddev=init_std, seed=seed),
                                              embeddings_regularizer=l2(
                                                  l2_reg_embedding),
                                              name='sparse_emb_' + str(i) + '-' + feat.name) for i, feat in
                         enumerate(feature_dim_dict["sparse"])}

获取嵌入var,这里每一个embedding_dict[feat]都是一个矩阵。

query_emb_list = get_embedding_vec_list(sparse_embedding_dict, sparse_input, feature_dim_dict["sparse"],
                                        return_feat_list=seq_feature_list)

把这些拼接起来。

query_emb = concat_fun(query_emb_list)
keys_emb = concat_fun(keys_emb_list)
deep_input_emb = concat_fun(deep_input_emb_list)

5.2.4 兴趣进化层

下面开始调用生成兴趣进化层。

hist, aux_loss_1 = interest_evolution(keys_emb, query_emb, user_behavior_length, gru_type=gru_type,
                                      use_neg=use_negsampling, neg_concat_behavior=neg_concat_behavior,
                                      embedding_size=embedding_size, att_hidden_size=att_hidden_units,
                                      att_activation=att_activation,
                                      att_weight_normalization=att_weight_normalization, )

其中:

  • DynamicGRU 相当于 第二版的dynamic_rnn,就是第一层 ‘rnn_1’;

  • auxiliary_loss与第二版几乎一样;

  • auxiliary_net只是最后一步   y_hat = tf.nn.sigmoid(dnn3)  不同;

具体代码如下:

def interest_evolution(concat_behavior, deep_input_item, user_behavior_length, gru_type="GRU", use_neg=False,
                       neg_concat_behavior=None, embedding_size=8, att_hidden_size=(6416), att_activation='sigmoid',
                       att_weight_normalization=False, )
:


    aux_loss_1 = None

    rnn_outputs = DynamicGRU(embedding_size * 2, return_sequence=True,
                             name="gru1")([concat_behavior, user_behavior_length])

    if gru_type == "AUGRU" and use_neg:
        aux_loss_1 = auxiliary_loss(rnn_outputs[:, :-1, :], concat_behavior[:, 1:, :],

                                    neg_concat_behavior[:, 1:, :],

                                    tf.subtract(user_behavior_length, 1), stag="gru")  # [:, 1:]

    if gru_type == "GRU":
        rnn_outputs2 = DynamicGRU(embedding_size * 2, return_sequence=True,
                                  name="gru2")([rnn_outputs, user_behavior_length])
        hist = AttentionSequencePoolingLayer(att_hidden_units=att_hidden_size, att_activation=att_activation,
                                             weight_normalization=att_weight_normalization, return_score=False)([
            deep_input_item, rnn_outputs2, user_behavior_length])

    else:  # AIGRU AGRU AUGRU

        scores = AttentionSequencePoolingLayer(att_hidden_units=att_hidden_size, att_activation=att_activation,
                                               weight_normalization=att_weight_normalization, return_score=True)([
            deep_input_item, rnn_outputs, user_behavior_length])

        if gru_type == "AIGRU":
            hist = multiply([rnn_outputs, Permute([21])(scores)])
            final_state2 = DynamicGRU(embedding_size * 2, gru_type="GRU", return_sequence=False, name='gru2')(
                [hist, user_behavior_length])
        else:  # AGRU AUGRU
            final_state2 = DynamicGRU(embedding_size * 2, gru_type=gru_type, return_sequence=False,
                                      name='gru2')([rnn_outputs, user_behavior_length, Permute([21])(scores)])
        hist = final_state2
    return hist, aux_loss_1
5.2.4.1 DynamicGRU 1

DynamicGRU 相当于 第二版的dynamic_rnn,就是第一层 ‘rnn_1’。

这一层对应架构图中黄色部分,即兴趣抽取层(Interest Extractor Layer),主要组件是 GRU。

主要作用是通过模拟用户的兴趣迁移过程,基于行为序列提取用户兴趣序列。即将用户行为历史的item embedding输入到dynamic rnn(第一层GRU)中。

rnn_outputs = DynamicGRU(embedding_size * 2, return_sequence=True,
                         name="gru1")([concat_behavior, user_behavior_length])
5.2.4.2 auxiliary_loss

辅助loss的计算其实是一个二分类模型,对应论文中:

在这里插入图片描述

auxiliary_loss与第二版几乎一样。

def auxiliary_loss(h_states, click_seq, noclick_seq, mask, stag=None):
    #:param h_states:
    #:param click_seq:
    #:param noclick_seq: #[B,T-1,E]
    #:param mask:#[B,1]
    #:param stag:
    #:return:
    hist_len, _ = click_seq.get_shape().as_list()[1:]
    mask = tf.sequence_mask(mask, hist_len)
    mask = mask[:, 0, :]
    mask = tf.cast(mask, tf.float32)
    # 倒数第一维度concat,其余不变
    click_input_ = tf.concat([h_states, click_seq], -1)
    # 倒数第一维度concat,其余不变
    noclick_input_ = tf.concat([h_states, noclick_seq], -1)
    # 获取正样本最后一个y_hat
    click_prop_ = auxiliary_net(click_input_, stag=stag)[:, :, 0]
    # 获取负样本最后一个y_hat
    noclick_prop_ = auxiliary_net(noclick_input_, stag=stag)[
                    :, :, 0]  # [B,T-1]
    # 对数损失,并且mask出真实历史行为
    click_loss_ = - tf.reshape(tf.log(click_prop_),
                               [-1, tf.shape(click_seq)[1]]) * mask
    noclick_loss_ = - \
                        tf.reshape(tf.log(1.0 - noclick_prop_),
                                   [-1, tf.shape(noclick_seq)[1]]) * mask
    loss_ = tf.reduce_mean(click_loss_ + noclick_loss_)

    return loss_
5.2.4.3 auxiliary_net

auxiliary_net只是最后一步 y_hat = tf.nn.sigmoid(dnn3)  不同。

def auxiliary_net(in_, stag='auxiliary_net'):
    bn1 
= tf.layers.batch_normalization(
        inputs=in_, name='bn1' + stag, reuse=tf.AUTO_REUSE)
    dnn1 = tf.layers.dense(bn1, 100, activation=None,
                           name='f1' + stag, reuse=tf.AUTO_REUSE)
    dnn1 = tf.nn.sigmoid(dnn1)
    dnn2 = tf.layers.dense(dnn1, 50, activation=None,
                           name='f2' + stag, reuse=tf.AUTO_REUSE)
    dnn2 = tf.nn.sigmoid(dnn2)
    dnn3 = tf.layers.dense(dnn2, 1, activation=None,
                           name='f3' + stag, reuse=tf.AUTO_REUSE)
    y_hat = tf.nn.sigmoid(dnn3)
    return y_hat
5.2.4.4 AttentionSequencePoolingLayer

这部分是deepctr完成的,对应第二版本的din_fcn_attention,关于din_fcn_attention可以参见前文[论文解读] 阿里DIEN整体代码结构。

DIEN 中,‘Attention_layer_1’ 层的作用是:通过在兴趣抽取层基础上加入Attention机制,模拟与当前目标广告相关的兴趣进化过程,对与目标物品相关的兴趣演化过程进行建模。即将第一层的输出,喂进第二层GRU,并用attention score(基于第一层的输出向量与候选物料计算得出)来控制第二层的GRU的update gate。

class AttentionSequencePoolingLayer(Layer):
    """The Attentional sequence pooling operation used in DIN.

      Input shape
        - A list of three tensor: [query,keys,keys_length]
        - query is a 3D tensor with shape:  ``(batch_size, 1, embedding_size)``
        - keys is a 3D tensor with shape:   ``(batch_size, T, embedding_size)``
        - keys_length is a 2D tensor with shape: ``(batch_size, 1)``

      Output shape
        - 3D tensor with shape: ``(batch_size, 1, embedding_size)``.

      Arguments
        - **att_hidden_units**:list of positive integer, the attention net layer number and units in each layer.
        - **att_activation**: Activation function to use in attention net.
        - **weight_normalization**: bool.Whether normalize the attention score of local activation unit.
        - **supports_masking**:If True,the input need to support masking.

      References
        - [Zhou G, Zhu X, Song C, et al. Deep interest network for click-through rate prediction[C]//Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining. ACM, 2018: 1059-1068.](https://arxiv.org/pdf/1706.06978.pdf)
    """


    def __init__(self, att_hidden_units=(8040), att_activation='sigmoid', weight_normalization=False,
                 return_score=False,
                 supports_masking=False, **kwargs)
:

        self.att_hidden_units = att_hidden_units
        self.att_activation = att_activation
        self.weight_normalization = weight_normalization
        self.return_score = return_score
        super(AttentionSequencePoolingLayer, self).__init__(**kwargs)
        self.supports_masking = supports_masking

    def build(self, input_shape):
        if not self.supports_masking:
            if not isinstance(input_shape, list) or len(input_shape) != 3:
                raise ValueError('...)
            if len(input_shape[0]) != 3 or len(input_shape[1]) != 3 or len(input_shape[2]) != 2:
                raise ValueError(...)
            if input_shape[0][-1] != input_shape[1][-1] or input_shape[0][1] != 1 or input_shape[2][1] != 1:
                raise ValueError(...)
        else:
            pass
        self.local_att = LocalActivationUnit(
            self.att_hidden_units, self.att_activation, l2_reg=0, dropout_rate=0, use_bn=False, seed=1024, )
        super(AttentionSequencePoolingLayer, self).build(
            input_shape)  # Be sure to call this somewhere!

    def call(self, inputs, mask=None, training=None, **kwargs):
        if self.supports_masking:
            if mask is None:
                raise ValueError(...)
            queries, keys = inputs
            key_masks = tf.expand_dims(mask[-1], axis=1)

        else:
            queries, keys, keys_length = inputs
            hist_len = keys.get_shape()[1]
            key_masks = tf.sequence_mask(keys_length, hist_len)

        attention_score = self.local_att([queries, keys], training=training)
        outputs = tf.transpose(attention_score, (0, 2, 1))

        if self.weight_normalization:
            paddings = tf.ones_like(outputs) * (-2 ** 32 + 1)
        else:
            paddings = tf.zeros_like(outputs)
        outputs = tf.where(key_masks, outputs, paddings)

        if self.weight_normalization:
            outputs = tf.nn.softmax(outputs)
        if not self.return_score:
            outputs = tf.matmul(outputs, keys)
        outputs._uses_learning_phase = attention_score._uses_learning_phase

        return outputs
5.2.4.5 DynamicGRU 2

前面attention的score作为本GRU的一部分输入。

    if gru_type == "AIGRU":
        hist = multiply([rnn_outputs, Permute([21])(scores)])
        final_state2 = DynamicGRU(embedding_size * 2, gru_type="GRU", return_sequence=False, name='gru2')(
            [hist, user_behavior_length])
    else:  # AGRU AUGRU
        final_state2 = DynamicGRU(embedding_size * 2, gru_type=gru_type, return_sequence=False,
                                  name='gru2')([rnn_outputs, user_behavior_length, Permute([21])(scores)])
    hist = final_state2

这部分是deepctr完成的,对应第二版本的GRU,把VecAttGRUCell等迁移到这里。

class DynamicGRU(Layer):
    def __init__(self, num_units=None, gru_type='GRU', return_sequence=True, **kwargs):

        self.num_units = num_units
        self.return_sequence = return_sequence
        self.gru_type = gru_type
        super(DynamicGRU, self).__init__(**kwargs)

    def build(self, input_shape):
        # Create a trainable weight variable for this layer.
        input_seq_shape = input_shape[0]
        if self.num_units is None:
            self.num_units = input_seq_shape.as_list()[-1]
        if self.gru_type == "AGRU":
            self.gru_cell = QAAttGRUCell(self.num_units)
        elif self.gru_type == "AUGRU":
            self.gru_cell = VecAttGRUCell(self.num_units)
        else:
            self.gru_cell = tf.nn.rnn_cell.GRUCell(self.num_units)

        # Be sure to call this somewhere!
        super(DynamicGRU, self).build(input_shape)

    def call(self, input_list):
        """
        :param concated_embeds_value: None * field_size * embedding_size
        :return: None*1
        """

        if self.gru_type == "GRU" or self.gru_type == "AIGRU":
            rnn_input, sequence_length = input_list
            att_score = None
        else:
            rnn_input, sequence_length, att_score = input_list

        rnn_output, hidden_state = dynamic_rnn(self.gru_cell, inputs=rnn_input, att_scores=att_score,sequence_length=tf.squeeze(sequence_length,), dtype=tf.float32, scope=self.name)
        if self.return_sequence:
            return rnn_output
        else:
            return tf.expand_dims(hidden_state, axis=1)

5.2.5 DNN全连接层

现在我们得到了连接后的稠密表示向量,接下来就是利用全连通层自动学习特征之间的非线性关系组合。

于是通过一个多层神经网络,得到最终的ctr预估值,这部分就是一个函数调用。

对应论文中的:

在这里插入图片描述

代码如下:

deep_input_emb = Concatenate()([deep_input_emb, hist])

deep_input_emb = tf.keras.layers.Flatten()(deep_input_emb)
if len(dense_input) > 0:
    deep_input_emb = Concatenate()(
        [deep_input_emb] + list(dense_input.values()))

output = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn,
             dnn_dropout, use_bn, seed)(deep_input_emb)
final_logit = Dense(1, use_bias=False)(output)
output = PredictionLayer(task)(final_logit)

model_input_list = get_inputs_list(
    [sparse_input, dense_input, user_behavior_input])

if use_negsampling:
    model_input_list += list(neg_user_behavior_input.values())

model_input_list += [user_behavior_length]

model = tf.keras.models.Model(inputs=model_input_list, outputs=output)

if use_negsampling:
    model.add_loss(alpha * aux_loss_1)
tf.keras.backend.get_session().run(tf.global_variables_initializer())
return model

至此, Keras 版本分析基本完成。

0xFF 参考

2019-11-06 广告点击率预测:DeepCTR 库的简单介绍

DeepCTR:易用可扩展的深度学习点击率预测算法包

Deepctr框架代码阅读