[阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本
[阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本
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模型框架,集成了深度学习流行的所有模型,适合学推荐系统模型的人参考。
这个项目主要是对目前的一些基于深度学习的点击率预测算法进行了实现,如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(
) + 1) for feat in sparse_features + ['cate_id', 'brand']]
dense_feature_list = [SingleFeat(feat, 1) for 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=(64, 16),
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=(200, 80),
dnn_activation='relu',
att_hidden_units=(64, 16), 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=(64, 16), 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([2, 1])(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([2, 1])(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=(80, 40), 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([2, 1])(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([2, 1])(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 库的简单介绍
[阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本的更多相关文章
- [论文阅读]阿里DIEN深度兴趣进化网络之总体解读
[论文阅读]阿里DIEN深度兴趣进化网络之总体解读 目录 [论文阅读]阿里DIEN深度兴趣进化网络之总体解读 0x00 摘要 0x01论文概要 1.1 文章信息 1.2 基本观点 1.2.1 DIN的 ...
- [阿里DIN] 深度兴趣网络源码分析 之 整体代码结构
[阿里DIN] 深度兴趣网络源码分析 之 整体代码结构 目录 [阿里DIN] 深度兴趣网络源码分析 之 整体代码结构 0x00 摘要 0x01 文件简介 0x02 总体架构 0x03 总体代码 0x0 ...
- [阿里DIN] 深度兴趣网络源码分析 之 如何建模用户序列
[阿里DIN] 深度兴趣网络源码分析 之 如何建模用户序列 目录 [阿里DIN] 深度兴趣网络源码分析 之 如何建模用户序列 0x00 摘要 0x01 DIN 需要什么数据 0x02 如何产生数据 2 ...
- ------ Tor(洋葱路由器)匿名网络源码分析——主程序入口点(一)------
--------------------------------------------------------<概览> tor 的源码包可以从官网下载,可能需要预先利用其它FQ软件才能访 ...
- ConcurrentHashMap源码分析(JDK8版本<转载>)
注:本文源码是JDK8的版本,与之前的版本有较大差异 转载地址:http://blog.csdn.net/u010723709/article/details/48007881 ConcurrentH ...
- ConcurrentHashMap源码分析_JDK1.8版本
在jdk1.8中主要做了2方面的改进 改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数 ...
- STL 源码分析 (SGI版本, 侯捷著)
前言 源码之前,了无秘密 algorithm的重要性 效率的重要性 采用Cygnus C++ 2.91 for windows cygwin-b20.1-full2.exe 下载地址:http://d ...
- [论文阅读]阿里DIN深度兴趣网络之总体解读
[论文阅读]阿里DIN深度兴趣网络之总体解读 目录 [论文阅读]阿里DIN深度兴趣网络之总体解读 0x00 摘要 0x01 论文概要 1.1 概括 1.2 文章信息 1.3 核心观点 1.4 名词解释 ...
- spark源码分析以及优化
第一章.spark源码分析之RDD四种依赖关系 一.RDD四种依赖关系 RDD四种依赖关系,分别是 ShuffleDependency.PrunDependency.RangeDependency和O ...
随机推荐
- DVWA-文件包含-目录遍历学习笔记
参考文献资料: https://www.cnblogs.com/s0ky1xd/p/5823685.html https://www.cnblogs.com/yuzly/p/10799486.html ...
- 开发你的第一个NCS(Zephyr)应用程序
Nordic有2套并存的SDK:老的nRF5 SDK和新的NCS SDK,两套SDK相互独立,大家选择其中一套进行开发即可.一般而言,如果你选择的芯片是nRF51或者nRF52系列,那么推荐使用nRF ...
- CVE-2017-12149 JBOOS反序列化漏洞复现
一.漏洞描述 2017年8月30日,厂商Redhat发布了一个JBOSSAS 5.x 的反序列化远程代码执行漏洞通告.该漏洞位于JBoss的HttpInvoker组件中的 ReadOnlyAccess ...
- easyui框架 jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding= ...
- hive向es推送数据
第一步:首先要保证网络是通的,很多公司里子网遍布,要和运维和工程侧同事确认好网络是通的,es的地址可以通过curl es地址的方式测试一下. 第二步:下载需要的jar包,必须的是es-hadoop的包 ...
- 一文掌握XSS
目录 XSS跨站脚本攻击 1.什么叫跨站脚本攻击? 2.XSS跨站脚本攻击的原理 3.XSS跨站脚本攻击的目的是什么? 4.XSS跨站脚本攻击出现的原因 5.XSS跨站脚本攻击的条件 1.有输入有输出 ...
- git分支的创建与分支之间合并的底层原理
开发一个版本,采用的发布流程: (1).从master的最新代码拉取一个开发分支,在上面进行开发(这里假设开发分支为dev) (2).在开发分支上不断地进行提交版本,期间,master也会有因为其他版 ...
- Android stdio使用时遇到的一些问题
(1)android stdio加载布局时 Exception raised during rendering: com/android/util/PropertiesMap ...
- ASP.NET Core路由中间件[2]: 路由模式
一个Web应用本质上体现为一组终结点的集合.终结点则体现为一个暴露在网络中可供外界采用HTTP协议调用的服务,路由的作用就是建立一个请求URL模式与对应终结点之间的映射关系.借助这个映射关系,客户端可 ...
- 每日一个linux命令5 -- rm
rm命令.rm是常用的命令,该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除.对于链接文件,只是删除了链接,原有文件均保持不变. rm是一个危险的命 ...