DSIN 深度 Session 兴趣网络介绍及源码剖析
前言(可以忽略~)
本文介绍 DSIN 网络的基本原理,并对源码进行详细分析,从数据预处理,训练数据生成,模型构建等方面对 DSIN 的完整实现进行详细介绍。
(PS:好久好久没有写文章了,罪过罪过,这段时间发生了太多的事情,似梦如幻,2020 年结尾钟声快要敲响之际,平静终于回归了我的内心,过去的事情不再留恋,2021 年开启新的征程。“星空”我望过了,还差的就是脚踏实地。祝新的一年身体健康,万事如意!)
广而告之
可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;
文章信息
核心观点
对用户历史行为序列进行建模,阿里有几个非常重要的工作,如 DIN (深度兴趣网络,详见博客 DIN 深度兴趣网络介绍以及源码浅析) 以及 DIEN (深度兴趣进化网络, 博客之后完成, Flag~????),然而这些模型并没有考虑到用户历史行为序列中的内在结构,即行为序列可以被划分为多个 Sessions,Session 之间可以反映出用户兴趣的变化。下图是将用户行为进行 Session 划分的一个例子:

图片表示用户点击过的商品,图片下面的数字为点击时间,Session 的划分规则是:如果两个行为之间的间隔超过了 30 分钟,就划分一个 Session;(该规则出自 Airbnb 的一篇论文:Real-time personalization using embeddings for
search ranking at airbnb) 从图中可以清楚的看到,同一个 Session 内的行为非常相似,而 Session 间的兴趣则是多样的。因此,考虑对 Session 进行建模,可以更好的捕获用户动态变化的兴趣。
本文提出的 DSIN 网络主要包含三个核心组成部分:
- Session 兴趣提取层 (Session Interest Extractor Layer): 将用户的序列行为划分为多个 session 后,使用 self-attention 以及 bias-encoding 对每个 session 进行建模;self-attention 可以捕获 session 内各个行为之间的内在联系,进而提取出每个 session 的用户兴趣;
- Session 兴趣交互层 (Session Interest Interacting Layer):提取出用户的 session 兴趣后,session 兴趣之间也是存在联系的,采用 Bi-LSTM(双向 LSTM) 来捕获 session 兴趣间的变化和演进。
- Session 兴趣激活层 (Session Interest Activating Layer):每个 session 兴趣对目标商品的影响不同,采用 Attention 机制刻画目标商品和每个 session 兴趣之间的相关性。
以上是对 DSIN 的概括总结, 下面进行详细的分析.
核心观点解读
DSIN 的网络结构如下图所示:

网络结构图稍显复杂, 我们按照从易到难的顺序进行介绍, 大致可以分为 3 个部分:
- 左下角部分: 将用户侧和目标商品侧的稀疏特征分别转换为 embedding, 设为
和
(其中
表示用户侧稀疏特征的个数,
表示商品侧稀疏特征的个数,
- 右下角部分: DSIN 的核心模块, 对用户历史行为进行 session 划分, 采用 self-attention 与 Bi-LSTM 来分别捕获 session 兴趣序列
和
, 并通过 Attention 机制来学习目标商品 (Target Item) 和 session 兴趣间的相关性程度, 再对两个 session 兴趣序列分别进行加权求和, 得到聚合后的 session 兴趣
(使用浅黄色表示) 以及
- 上半部分: 为经典的 MLP 结构, 输入为用户侧和商品侧的 embedding
,
以及自适应学习的 session 兴趣
,
下面对右下角的部分进行拆解分析, 该部分为 DSIN 的核心模块, 主要由:
- Session Divsion Layer(Session 划分层)
- Session Interest Extractor Layer(Session 兴趣提取层)
- Session Interest Interacting Layer(Session 兴趣交互层)
- Session Interest Activating Layer(Session 兴趣激活层)
四个部分构成. 下面按顺序依次介绍.
Session 划分层 (Session Divsion Layer)
对用户历史行为序列按照时间顺序排序, 如果两个行为之间的间隔超过了 30 分钟,就划分一个 Session; 按该规则将用户行为序列
划分为多个 Sessions
, 其中第
个 Session 定义为
, 其中
为该 Session 中的行为个数,
表示该 Session 中用户的第 
Session 兴趣提取层 (Session Interest Extractor Layer)
划分完 Session 后, 为了捕获 Session 内各个行为的内在关系, 在每个 Session 内应用 multi-head self-attention 机制. 为了刻画 Session 内行为的顺序关系, 类似于 Transorformer 中的 positional encoding, DSIN 介绍了 Bias Encoding
(其中
为用户 Session 的个数,
为一个 Session 内的行为个数, 

其中 

注意
是一个元素值, 表示第
个 Session 内的第
个行为对应的 embedding, 其第
个元素所对应的偏置值 (bias). 另外注意 
这里补充一点, 可能一开始看到这里的时候会有些疑惑, 比如
这个 Tensor 是如何实现的, 毕竟
是三个维度不相等的向量, 它们无法直接相加. 在代码实现时,
为大小等于 (K, 1, 1) 的数组,
为大小等于 (1, T, 1) 的数组, 而
为大小等于 (1, 1, C) 的数组, 利用 Broadcast 操作可以将三者给相加, 最终得到大小为 
用户 Session 与 Bias Encoding 相加后, 得到新的表达
输入到 Multi-Head Self-Attention 模块中. 设 Head 的个数为
, 那么对于第
个 Session
, 其将被划分为
份, 即
, 其中
为
的第
个 Head, 其中
.
第 

其中 

其中
也为线性矩阵,
. 之后对该 Session 中的 

此时
.
Session 兴趣交互层 (Session Interest Interacting Layer)
为了进一步捕获 Session 兴趣间的变化和演进, 作者采用了 Bi-LSTM 模型. 其公式化如下:

其中
为 sigmoid 函数,
分别为 input gate, forget gate, output gate 以及 cell vectors, 它们的大小和
一致. Bi-LSTM 的隐层状态 

其中
表示前向 LSTM 的隐层状态而 
Session 兴趣激活层 (Session Interest Activating Layer)
在得到 Session 兴趣序列后, 由于每个 session 兴趣对目标商品的影响不同,这里采用 Attention 机制来刻画目标商品和每个 session 兴趣之间的相关性。
前面介绍了使用 Multi-Head Self-Attention 与 Bi-LSTM 分别捕获了 session 兴趣序列
和
, 目标商品分别与两个 Session 兴趣序列进行 Attention 的结果为:

其中
为目标商品对应的 embedding; 最后, 将 Attention 得到的结果
,
, 目标商品 embedding
以及用户侧 embedding 
源码分析
下面对 DSIN 的源码进行分析,了解其数据处理,模型构建、训练等实现细节,从而加深对论文核心观点的理解。要解读的源码地址为:https:///shenweichen/DSIN
数据集介绍
DSIN 代码处理的数据集 Ad Display/Click Data on Taobao.com 是阿里巴巴提供的一个淘宝展示广告点击率预估数据集。其主要内容如下:

说明:
-
raw_sample:原始的样本骨架,应该是从展现点击日志中获取的数据,描述了用户与曝光商品(广告)之间的关系,比如是否发生了点击等; -
ad_feature: 描述了广告的基本信息,比如广告 ID,广告计划 ID,品牌等; -
user_profile: 描述了用户的基本信息,如用户 ID, 年龄,性别等; -
raw_behavior_log: 用户的行为日志,描述了用户的历史行为,行为类型主要包含浏览、购买、加购、喜欢(收藏);
数据预处理 – 采样用户
代码地址:https:///shenweichen/DSIN/blob/master/code/0_gen_sampled_data.py
作者贴心地在给代码文件命名时加上了 0_, 1_ 之类的前缀,表明了代码的执行顺序,首先我们需要执行 0_gen_sampled_data.py 对用户进行采样。代码中很多内容是文件的读取,下面我只截取出比较核心的部分:
- 采样用户 (详情见注释)
基本信息读取,并对用户进行下采样,历史行为只考虑浏览行为。
## 读取用户信息表和原始样本骨架
user = pd.read_csv('../raw_data/user_profile.csv')
sample = pd.read_csv('../raw_data/raw_sample.csv')
## FRAC 为采样率,对用户进行下采样;原始样本骨架只考虑采样后的用户
if FRAC < 1.0:
user_sub = user.sample(frac=FRAC, random_state=1024)
else:
user_sub = user
sample_sub = sample.loc[sample.user.isin(user_sub.userid.unique())]
## 读取用户历史行为,这里只考虑浏览行为(log['btag'] == 'pv', 其中 'pv' 表示浏览)
## 历史行为也只考虑采样后的用户
log = pd.read_csv('../raw_data/behavior_log.csv')
log = log.loc[log['btag'] == 'pv']
userset = user_sub.userid.unique()
log = log.loc[log.user.isin(userset)]
## 读取广告基本信息
ad = pd.read_csv('../raw_data/ad_feature.csv')
ad['brand'] = ad['brand'].fillna(-1)
- 用户行为编码
分别对 cate_id 和 brand 进行编码,注意编码从 1 开始计数,这是因为后续会划分 Session,每个 Session 内的行为个数是不同的,为了让 Session 内的行为数为一固定值,那么行为数不足的 Session 就需要补 0,这些 0 可以表示无行为;因此为了方便区分,可以从 1 开始计数。
from sklearn.preprocessing import LabelEncoder
## 分别对 cate_id 和 brand 进行编码
lbe = LabelEncoder()
unique_cate_id = np.concatenate(
(ad['cate_id'].unique(), log['cate'].unique()))
lbe.fit(unique_cate_id)
ad['cate_id'] = lbe.transform(ad['cate_id']) + 1
log['cate'] = lbe.transform(log['cate']) + 1
lbe = LabelEncoder()
unique_brand = np.concatenate(
(ad['brand'].unique(), log['brand'].unique()))
lbe.fit(unique_brand)
ad['brand'] = lbe.transform(ad['brand']) + 1
log['brand'] = lbe.transform(log['brand']) + 1
文件剩余的内容就是将处理后的数据进行保存。OK,现在总结一下该文件的作用:
- 数据处理, 对用户信息表(
user_profile)中的用户进行采样, 并从原始样本骨架(raw_sample)中获取这部分用户对广告的反馈(是否点击等); - 之后结合广告基本信息表 (
ad_feature) 获取广告的基本信息, 主要是brand(品牌) 以及cate_id(商品类目) 信息; - 接下来结合用户行为日志 (
raw_behavior_log), 获取用户的历史浏览行为(代码只考虑浏览行为, 其他三种行为, 如: 加购、购买、喜欢 暂不考虑), 并对历史行为进行编码,浏览行为用(cate_id, brand, time_stamp) 来表示.
数据预处理 – Session 划分
代码位置:https:///shenweichen/DSIN/blob/master/code/1_gen_sessions.py
在 0_gen_sample_data.py 中完成了对用户的采样以及历史行为的预处理,1_gen_sessions.py 文件采用多进程的方式对这部分用户的历史行为进行 Session 划分;
gen_user_hist_sessions 运行入口
函数中我会删去不需要特意分析的代码;
def gen_user_hist_sessions(model, FRAC=0.25):
"""
在 0_gen_sample_data.py 中完成了对用户的采样, 这会采用多进程的方式对这部分用户的历史行为进行 session 的划分; session 的划分标准是: 如果一个用户的前后两次行为的时间差 > 30min, 那么就划分一个 session; 此外如果一个 session 内的行为数不超过 2 个, 该 session 就不保留了.
"""
## 读取用户历史行为,只保留 0503 ~ 0513 这段时间范围内的数据
print("gen " + model + " hist sess", FRAC)
name = '../sampled_data/behavior_log_pv_user_filter_enc_' + str(FRAC) + '.pkl'
data = pd.read_pickle(name)
data = data.loc[data.time_stamp >= 1493769600] # 0503-0513
# 0504~1493856000
# 0503 1493769600
## 读取采样后的用户信息
user = pd.read_pickle('../sampled_data/user_profile_' + str(FRAC) + '.pkl')
## 用户数量较多,采用多进程的方式来进行处理,
n_samples = user.shape[0]
batch_size = 150000
iters = (n_samples - 1) // batch_size + 1
for i in range(0, iters):
target_user = user['userid'].values[i * batch_size:(i + 1) * batch_size]
sub_data = data.loc[data.user.isin(target_user)]
## 对用户进行分组聚合,将同一个用户的所有行为聚合在一起,然后再对这个
## 行为列表进行排序以及 Session 划分,这部分逻辑在 gen_session_list_dsin
## 函数中完成,调用 applyParallel 函数实现多进程处理
df_grouped = sub_data.groupby('user')
user_hist_session = applyParallel(
df_grouped, gen_session_list_dsin, n_jobs=20, backend='multiprocessing')
print("1_gen " + model + " hist sess done")
gen_session_list_dsin 实现划分 Session 的逻辑
根据用户的历史行为来划分 session, 按照前后两次行为的时间间隔是否超过 30min 来进行 session 的划分, 另外只保留行为数超过 2 个的 sessions.
def gen_session_list_dsin(uid, t):
"""
根据用户的历史行为来划分 session, 按照前后两次行为的时间间隔是否超过 30min 来进行 session 的划分, 另外只保留行为数超过 2 个的 sessions
"""
## 对用户行为序列 t 按时间从小到大排序,也就是说,近期的行为排在后面,而较老的行为排在前面
t.sort_values('time_stamp', inplace=True, ascending=True)
last_time = 1483574401 # pd.to_datetime("2017-01-05 00:00:01")
session_list = []
session = []
## row[0] 为样本在表格中的序号, row[1] 为 pandas.Series, 里面的内容包括
## (user, time_stamp, cate, brand)
## 下面的逻辑是计算前后两个行为之间的时间误差,如果 delta > 30min, 那么就划分一个 Session;此外,如果该 Session 中的行为数不多于 2 个,那么就丢弃该 Session.
for row in t.iterrows():
time_stamp = row[1]['time_stamp']
delta = time_stamp - last_time ## 计算和上一个行为的时间误差
cate_id = row[1]['cate']
brand = row[1]['brand']
if delta > 30 * 60: # Session begin when current behavior and the last behavior are separated by more than 30 minutes.
if len(session) > 2: # Only use sessions that have >2 behaviors
session_list.append(session[:])
session = []
session.append((cate_id, brand, time_stamp))
last_time = time_stamp
if len(session) > 2:
session_list.append(session[:])
return uid,
函数最后返回 user_id 以及 session_list, 行为使用 (cate_id, brand, timestamp) 来表示,那么 session_list 可以表示为:
session_list = [
[(c11, b11, t11), (c12, b12, t12), ...],
[(c21, b21, t21), (c22, b22, t22), ...],
.......
]
产生模型训练数据
代码位置:https:///shenweichen/DSIN/blob/master/code/2_gen_dsin_input.py
之后运行 2_gen_dsin_input.py 来产生模型所需要的训练数据。这部分代码相对比较复杂,下面将代码拆分,对各个部分进行分析。
- 读取
1_gen_session.py 产生的用户 session 文件,并统一保存到user_hist_session 字典中。
user_hist_session = {}
FILE_NUM = len(
list(filter(lambda x: x.startswith('user_hist_session_' + str(FRAC) + '_dsin_'),
os.listdir('../sampled_data/'))))
print('total', FILE_NUM, 'files')
for i in range(FILE_NUM):
"""
在 1_gen_sessions.py 中, 划分完 session 后, 最终使用字典来保留 {user_id: session_list}, 即每个用户对应的 session 序列;
这里将所有用户的 session 序列统一保存到 user_hist_session 中
"""
user_hist_session_ = pd.read_pickle(
'../sampled_data/user_hist_session_' + str(FRAC) + '_dsin_' + str(i) + '.pkl') # 19,34
user_hist_session.update(user_hist_session_)
del- 获取采样后的用户, 保存到
sample_sub 中
sample_sub = pd.read_pickle(
'../sampled_data/raw_sample_' + str(FRAC) + '.pkl')
- 将
sample_sub 中的所有用户的 session 汇总,保存到sess_input_dict 中,每个用户只保留DSIN_SESS_COUNT = 5 (定义在config.py 文件中)个 Session. 另外使用sess_input_length_dict 保存每个 session 的真实行为个数,因为在后面的处理过程中,会存在将 session 截断或补 0 的操作,使所有 Session 的长度统一为DSIN_SESS_MAX_LEN = 10.
sess_input_dict = {}
sess_input_length_dict = {}
for i in range(SESS_COUNT):
sess_input_dict['sess_' + str(i)] = {'cate_id': [], 'brand': []}
sess_input_length_dict['sess_' + str(i)] = []
sess_length_list = []
for row in tqdm(sample_sub[['user', 'time_stamp']].iterrows()):
sess_input_dict_, sess_input_length_dict_, sess_length = gen_sess_feature_dsin(
row)
for i in range(SESS_COUNT):
sess_name = 'sess_' + str(i)
sess_input_dict[sess_name]['cate_id'].append(
sess_input_dict_[sess_name]['cate_id'])
sess_input_dict[sess_name]['brand'].append(
sess_input_dict_[sess_name]['brand'])
sess_input_length_dict[sess_name].append(
sess_input_length_dict_[sess_name])
sess_length_list.append(sess_length)其中 gen_sess_feature_dsin 函数得到每个用户的 SESS_COUNT = 5 个 Sessions,并保存在 sess_input_dict_ 中,而 sess_input_dict 用于汇总所有用户的 Sessions。最后得到的 sess_input_dict 形式如下:
## c: cate_id, b: brand
## 假设用户数为 k 个, 每个用户保留 5 个 Session。
## 由于 Session 内的行为数不固定,所以 m,n,q 不一定相等
sess_input_dict = {
'sess_0': {
'cate_id': [[c11, c12, ..., c1m], ## sess_0 中用户1 有 m 个行为
[c21, c22, ..., c2n], ## 用户 2 有 n 个行为
...,
[ck1, ck2, ..., ckq]], ## 用户 k 有 q 个行为
'brand' : [[b11, b12, ..., b1m],
[b21, b22, ..., b2n],
...,
[bk1, bk2, ..., bkq]],
},
'sess_1': {
......
},
.....,
'sess_5': {
.....
},
}
下面再详细介绍 gen_sess_feature_dsin 函数:
其内容主要是从一个用户的 session_list 中保留最近的 5 个 Session,同时要保证 Session 内的行为时间不超过用户和广告发生交互的时间 (否则就不叫历史行为了…)
def gen_sess_feature_dsin(row):
"""
row 中保存着一个用户的 ID 以及对广告进行反馈的时间 time_stamp,
因此在下面的处理中,主要目的是将 time_stamp 之前的行为保留,
同时对每个用户只保留最近的 5 (DSIN_SESS_COUNT)个 Session
"""
sess_count = DSIN_SESS_COUNT ## 5
sess_max_len = DSIN_SESS_MAX_LEN ## 10,该函数没有用到这个变量
sess_input_dict = {}
sess_input_length_dict = {}
for i in range(sess_count):
sess_input_dict['sess_' + str(i)] = {'cate_id': [], 'brand': []}
sess_input_length_dict['sess_' + str(i)] = 0
sess_length = 0
user, time_stamp = row[1]['user'], row[1]['time_stamp'] ## time_stamp 是用户对广告的反馈时间, 历史行为的时间应该要小于这个时间
# 边界情况处理
if user not in user_hist_session:
for i in range(sess_count):
sess_input_dict['sess_' + str(i)]['cate_id'] = [0]
sess_input_dict['sess_' + str(i)]['brand'] = [0]
sess_input_length_dict['sess_' + str(i)] = 0
sess_length = 0
else: # 核心逻辑
## 先确定 sess_0 的结果,再确定 sess_1 ~ sess_4 的结果
valid_sess_count = 0
last_sess_idx = len(user_hist_session[user]) - 1
for i in reversed(range(len(user_hist_session[user]))): ## 从最新的 session 开始处理
cur_sess = user_hist_session[user][i] ## 用户 user 的第 i 个 session, [(cate_id, brand, timestamp), ....]
if cur_sess[0][2] < time_stamp: ## cur_sess[0][2] 表示第一个行为的行为时间, 需要小于 time_stamp
in_sess_count = 1
for j in range(1, len(cur_sess)): ## 这个session中其他行为的时间也应该小于 time_stamp, in_sess_count 统计这个 session 内小于 time_stamp 的行为数
if cur_sess[j][2] < time_stamp:
in_sess_count += 1
if in_sess_count > 2:
## 取该 session 中最近的 sess_max_len(10个) 个行为, 如果 session 内的行为个数(此外还要满足时间<time_stamp这个条件)少于 10 个,
## 那么 index 范围为 [0, in_sess_count];
## 如果 session 中行为数较多, 那么取时间最新的, 行为用 (cate_id, brand, timestamp) 表示, e[0] 表示 cate_id, e[1] 表示 brand
sess_input_dict['sess_0']['cate_id'] = [e[0] for e in cur_sess[max(0,
in_sess_count - sess_max_len):in_sess_count]]
sess_input_dict['sess_0']['brand'] = [e[1] for e in
cur_sess[max(0, in_sess_count - sess_max_len):in_sess_count]]
sess_input_length_dict['sess_0'] = min(
sess_max_len, in_sess_count)
last_sess_idx = i
valid_sess_count += 1
break
## 上一段代码得到最新的 session 作为 sess_0, 下面依次获取 sess_1 ~ sess_4
for i in range(1, sess_count):
if last_sess_idx - i >= 0: ## 一个 session 内至少有两个行为, 在 1_gen_sessions.py 中有这样的设定
cur_sess = user_hist_session[user][last_sess_idx - i]
## 这里获取 session 内行为的代码使用简便的 cur_sess[-sess_max_len:], 而上一段代码使用复杂的
## cur_sess[max(0, in_sess_count - sess_max_len):in_sess_count], 是因为第一个 session 要考虑
## 行为的时间不能超过 time_stamp, 但这里的 session 中所有行为的时间都小于 time_stamp, 所以直接用 cur_sess[-sess_max_len:]
## 处理即可
sess_input_dict['sess_' + str(i)]['cate_id'] = [e[0]
for e in cur_sess[-sess_max_len:]]
sess_input_dict['sess_' + str(i)]['brand'] = [e[1]
for e in cur_sess[-sess_max_len:]]
sess_input_length_dict['sess_' +
str(i)] = min(sess_max_len, len(cur_sess))
valid_sess_count += 1
else: ## 如果用户的 session 个数比较少, 默认用 0 表示
sess_input_dict['sess_' + str(i)]['cate_id'] = [0]
sess_input_dict['sess_' + str(i)]['brand'] = [0]
sess_input_length_dict['sess_' + str(i)] = 0
sess_length = valid_sess_count ## valid_sess_count 记录有效的 session 长度
## sess_input_length_dict 用于记录每个 session 真实的行为长度
return sess_input_dict, sess_input_length_dict,
由于 gen_sess_feature_dsin 函数只处理一个用户的 session_list, 所以其返回结果 sess_input_dict 形如:
sess_input_dict = {
'sess_0': {
'cate_id': [c1, c2, ..., cm],
'brand' : [b1, b2, ..., bm],
},
'sess_1': {
......
},
.....,
'sess_5': {
.....
},
}- 继续介绍主逻辑:下一步读取用户信息以及广告信息,并和原始样本骨架表(raw_sample) 三表进行关联:
user = pd.read_pickle('../sampled_data/user_profile_' + str(FRAC) + '.pkl')
ad = pd.read_pickle('../sampled_data/ad_feature_enc_' + str(FRAC) + '.pkl')
user = user.fillna(-1)
user.rename(
columns={'new_user_class_level ': 'new_user_class_level'}, inplace=True)
sample_sub = pd.read_pickle(
'../sampled_data/raw_sample_' + str(FRAC) + '.pkl')
sample_sub.rename(columns={'user': 'userid'}, inplace=True)
## sample_sub 描述了用户和广告的关联, 要得到 user_id 本身的信息以及 ad 的信息, 需要用
## sample_sub 去关联 user 和 ad 两个表
data = pd.merge(sample_sub, user, how='left', on='userid', )
data = pd.merge(data, ad, how='left', on='adgroup_id')关联后的结果保存到 data 变量中.
- 将稀疏特征和稠密特征都转化为 ID,并记录各个特征空间的大小:
## 这里 sparse_features 大小为 13
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']
## 转换为 id;
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])
## 记录特征空间的大小; SingleFeat 就是一个 namedtuple, 用于记录特征的基本信息
## sparse_feature_list 大小为 13 + 2 = 15
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]
加上 cate_id 和 brand 的话,总共 15 个稀疏特征,以及 1 个稠密特征。注意代码中使用 sparse_feature_list 和 dense_feature_list 记录了各个稀疏和稠密特征的个数,用于后续构建 Embedding Layer (设置 Embedding Layer 的大小)以及 Hash 等。
- 将所有用户各个 Session 中的行为个数统一限制为
DSIN_SESS_MAX_LEN (10个),如果行为个数不足 10 个,那么进行补零操作:
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
sess_feature = ['cate_id', 'brand']
sess_input = []
sess_input_length = []
## 使用 pad_sequences 对所有 session 进行补 0 操作, 使得所有 session 的长度均为 DSIN_SESS_MAX_LEN
## sess_input 为一个大小为 SESS_COUNT * len(sess_feature) 的 list, 里面的元素为 shape 为 (number_of_user, DSIN_SESS_MAX_LEN) 的 numpy 数组.
## SESS_COUNT=5, sess_feature = [cateid, brand]
for i in tqdm(range(SESS_COUNT)):
sess_name = 'sess_' + str(i)
for feat in sess_feature:
sess_input.append(pad_sequences(
sess_input_dict[sess_name][feat], maxlen=DSIN_SESS_MAX_LEN, padding='post'))
sess_input_length.append(sess_input_length_dict[sess_name])
- 构造 DSIN 的输入数据:
## model_input 中每个元素为 shape=(number_of_user,) 的 numpy 数组, model_input 大小为 len(sparse_feature_list) + len(dense_feature_list);
## sess_input 为一个大小为 SESS_COUNT * len(sess_feature) 的 list, 里面的元素为 shape 为 (number_of_user, DSIN_SESS_MAX_LEN) 的 numpy 数组
## np.array(sess_length_list) 为 shape=(number_of_user,) 的 numpy 数组
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 + [np.array(sess_length_list)]
model_input +=
model_input 是一个 list,里面保存着各种 numpy 数组:(注: 图中的
应该等于 sample_sub 的行数)

model_input 的大小为 27,其中包括 15 个稀疏特征,1 个稠密特征,5 个 Session,每个 Session 内用 cate_id 和 brand 表示行为,因此有 5 x 2 个元素,最后再加上 1 个数组表示每个 Session 内真实的行为长度,因此总大小是 15 + 1 + 5 x 2 + 1 = 27.
最后将输入数据,label 以及特征空间大小保存下来,用于后续训练模型。
if not os.path.exists('../model_input/'):
os.mkdir('../model_input/')
pd.to_pickle(model_input, '../model_input/dsin_input_' +
str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
pd.to_pickle(data['clk'].values, '../model_input/dsin_label_' +
str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
## 将稀疏特征和稠密特征的空间大小使用字典保存
pd.to_pickle({'sparse': sparse_feature_list, 'dense': dense_feature_list},
'../model_input/dsin_fd_' + str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
print("gen dsin input done")介绍完数据预处理的部分,下面介绍模型训练代码。
模型训练代码介绍
代码位于:https:///shenweichen/DSIN/blob/master/code/train_dsin.py
数据预处理中得到的 model_input 包含了所有的数据,因此首先需要划分训练集和测试集:
SESS_COUNT = DSIN_SESS_COUNT ## 5, 每个用户的 Session 个数
SESS_MAX_LEN = DSIN_SESS_MAX_LEN ## 10,每个 Session 中的行为个数
fd = pd.read_pickle('../model_input/dsin_fd_' +
str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
model_input = pd.read_pickle(
'../model_input/dsin_input_' + str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
label = pd.read_pickle('../model_input/dsin_label_' +
str(FRAC) + '_' + str(SESS_COUNT) + '.pkl')
sample_sub = pd.read_pickle(
'../sampled_data/raw_sample_' + str(FRAC) + '.pkl')
## 增加 idx 列,表示样本的索引,用于后续训练集和测试集的划分
sample_sub['idx'] = list(range(sample_sub.shape[0]))
## 按时间,将用户与广告发生交互的时间 < 1494633600 的样本当作训练集,
## 其他作为测试集,首先获取到这些样本的索引
train_idx = sample_sub.loc[sample_sub.time_stamp <
1494633600, 'idx'].values
test_idx = sample_sub.loc[sample_sub.time_stamp >=
1494633600, 'idx'].values
得到索引后,再划分模型的输入数据和 label 等:
## 输入也进行划分, 下面两行代码中的 i 均为 numpy 数组
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]
模型训练和预估, loss 采用 binary_crossentropy, 优化方法选择 adagrad. 重点在 DSIN 模型。
sess_count = SESS_COUNT
sess_len_max = SESS_MAX_LEN
BATCH_SIZE = 4096
sess_feature = ['cate_id', 'brand']
TEST_BATCH_SIZE = 2 ** 14
## DSIN 的构建依赖 deepctr 库
model = DSIN(fd, sess_feature, embedding_size=4, sess_max_count=sess_count,
sess_len_max=sess_len_max, dnn_hidden_units=(200, 80), att_head_num=8,
att_embedding_size=1, bias_encoding=False)
model.compile('adagrad', 'binary_crossentropy',
metrics=['binary_crossentropy', ])
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)
print()
print("test LogLoss", round(log_loss(test_label, pred_ans), 4), "test AUC",
round(roc_auc_score(test_label, pred_ans), 4))
下面详细介绍 DSIN 模型。
DSIN 模型代码介绍
代码位置:https:///shenweichen/DSIN/blob/master/code/models/dsin.py
DSIN 的构建依赖 deepctr 库,注意一开始运行代码之前,使用 pip install -r requirements.txt 安装必要的依赖项,deepctr 库最新版本很多接口发生了变化,所以最好是安装 requirements.txt 中指定的版本。
作者给输入参数添加了详细的说明,我再介绍一下:
def DSIN(feature_dim_dict, sess_feature_list, embedding_size=8, sess_max_count=5, sess_len_max=10, bias_encoding=False,
att_embedding_size=1, att_head_num=8, dnn_hidden_units=(200, 80), dnn_activation='sigmoid', dnn_dropout=0,
dnn_use_bn=False, l2_reg_dnn=0, l2_reg_embedding=1e-6, init_std=0.0001, seed=1024, task='binary',
):
"""Instantiates the Deep Session Interest Network architecture.
:param feature_dim_dict: 类似 {'sparse':{'field_1':4,'field_2':3,'field_3':2},'dense':[]} 这样的字典,指明了每个特征空间的大小
:param sess_feature_list: 传入的是 ['cate_id', 'brand'], 用这些表示用户行为
:param embedding_size: 稀疏特征 embedding size 的大小
:param sess_max_count: 每个用户最大的 Session 个数,为 5
:param sess_len_max: 每个 Session 中的行为个数,为 10
:param bias_encoding: bool 值,是否使用 bias encoding
:param att_embedding_size: the embedding size of each attention head
:param att_head_num: attention head 的个数
:param dnn_hidden_units: list, dnn 网络每一隐层的节点个数
:param dnn_activation: dnn 中每一层的激活函数
:param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate.
:param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net
:param l2_reg_dnn: float. L2 regularizer strength applied to DNN
:param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector
:param init_std: float,to use as the initialize std of embedding vector
:param seed: 随机种子
:param task: str, ``"binary"`` for binary logloss or ``"regression"`` for regression loss
:return: A Keras model instance.
"""
在 train_dsin.py 文件中传入到该函数的参数中,需要注意的有:
sess_feature=['cate_id', 'brand']
embedding_size=4
sess_max_count=sess_count ## 5
sess_len_max=sess_len_max ## 10
att_head_num=8
att_embedding_size=1
注意特征的 embedding size 为 4.
代码首先进行数值判断,要保证 Multi-Head Self-Attention 的输出结果 embedding 的维度和输入是相同的。首先,每个稀疏特征都会被映射为大小等于 embedding_size 的向量,而一个行为使用 (cate_id, brand) 来表示,该行为对应的 embedding 大小是这两个稀疏特征对应的 embedding 向量进行 concatenation 得到的,即行为的 embedding 大小为 2 * embedding_size (len(sess_feature_list) == 2)
if (att_embedding_size * att_head_num != len(sess_feature_list) * embedding_size):
raise ValueError(
"len(session_feature_lsit) * embedding_size must equal to att_embedding_size * att_head_num ,got %d * %d != %d *%d" % (
len(sess_feature_list), embedding_size, att_embedding_size, att_head_num))
获取模型的输入:
## sparse_input: sparse_input 是大小为 15 的字典, key 有 userid, group_id 等特征, 而元素均为 shape=[B, 1] 的 tensor,表示每个特征的 id
## user_behavior_input_dict 为大小为 sess_max_count 的字典,key 为 sess_0 ~ sess_1, 每个 session 包含 brand/cid 两种类型的序列
## user_sess_length 表示真实 Session 的长度,虽然每个用户最后都划分了 5 个 Session,
## 但并不是每个用户都有 5 个 Session
sparse_input, dense_input, user_behavior_input_dict, _, user_sess_length = get_input(
feature_dim_dict, sess_feature_list, sess_max_count, sess_len_max)
"""
-- 中途插入 --
其中 get_input 定义如下,我把代码直接贴在这里,方便阅读
"""
from tensorflow.python.keras.layers import Input
def get_input(feature_dim_dict, seq_feature_list, sess_max_count, seq_max_len):
sparse_input, dense_input = create_singlefeat_inputdict(feature_dim_dict)
user_behavior_input = {}
for idx in range(sess_max_count):
sess_input = OrderedDict()
for i, feat in enumerate(seq_feature_list):
sess_input[feat] = Input(
shape=(seq_max_len,), name='seq_' + str(idx) + str(i) + '-' + feat)
user_behavior_input["sess_" + str(idx)] = sess_input
user_behavior_length = {"sess_" + str(idx): Input(shape=(1,), name='seq_length' + str(idx)) for idx in
range(sess_max_count)}
user_sess_length = Input(shape=(1,), name='sess_length')
return sparse_input, dense_input, user_behavior_input, user_behavior_length,
上面代码获取的输入(Input) 有 sparse_input, dense_input, user_behavior_input_dict, user_sess_length, 为了保证我们思维的连续性,我们先暂时略过 DSIN 函数中间部分的细节,直接跳到该函数的末尾部分,看看模型完整的输入是如何构建的,这样可以和前面讲过的model_input (数据预处理那一节)结合起来:
"""
我们直接跳到 DSIN 函数最后的部分,看看模型完整的输入
get_inputs_list 函数大概的作用是将 dict/OrderedDict 转换成 list 输出
"""
sess_input_list = []
for i in range(sess_max_count):
sess_name = "sess_" + str(i)
sess_input_list.extend(get_inputs_list(
[user_behavior_input_dict[sess_name]]))
model_input_list = get_inputs_list([sparse_input, dense_input]) + sess_input_list + [
user_sess_length]
model = Model(inputs=model_input_list, outputs=output)
return
我们可以看到 model_input_list 这个变量,它包含的内容刚好是 [sparse_input, dense_input] + sess_input_list + [user_sess_length], 而如果将它们都展开来看的话,正是:
## sparse_input 大小为 15
sparse_input = [
Input(shape=(1,), name='user_id', dtype),
Input(shape=(1,), name='adgroup_id', dtype),
......,
Input(shape=(1,), name='cate_id', dtype),
Input(shape=(1,), name='brand', dtype),
]
## dense_input 大小为 1
dense_input = [
Input(shape=(1,), name='price', dtype),
]
## sess_input_list 大小为 len(sess_feature_list) * SESS_COUNT = 2 * 5 = 10
## seq_max_len == 10
sess_input_list = [
Input(shape=(seq_max_len,), name='seq_00-cate_id'),
Input(shape=(seq_max_len,), name='seq_01-brand'),
......,
Input(shape=(seq_max_len,), name='seq_40-cate_id'),
Input(shape=(seq_max_len,), name='seq_41-brand'),
]
## user_sess_length 大小为 1,表示真实 Session 的长度
user_sess_length = [
Input(shape=(1,), name='sess_length')
]
如果把上面的所有 Input 按顺序来看,正好可以和下图对应!

model_input 中的数据就通过 Input 输入到模型中了。
下面我们回到模型代码。获取输入数据后,由于输入的是各种特征对应的 ID 值,首先要将它们转换为稠密的 embedding 向量:
from tensorflow.python.keras.layers import (Concatenate, Dense, Embedding,
Flatten, Input)
## sparse_embedding_dict 保存每个稀疏特征对应的 Embedding Layer
## 后面可以使用 embedding_lookup 查找每个特征对应的 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,
mask_zero=(feat.name in sess_feature_list)) for i, feat in
enumerate(feature_dim_dict["sparse"])}
获取目标商品对应的 embedding:
"""
+ query_emb_list 为大小为 2 (等于 len(sess_feature_list)) 的 list, 保存两个 tensor,
+ target item 对应的 cate_id 和 brand
+ feature_dim_dict['sparse'] 记录了每个 field 的空间大小,
用于对这个 field 下的特征值进行 Hash,
+ sparse_embedding_dict 存储了每个 field 对应的 embedding layer,
用于将稀疏特征映射为稠密向量;
+ sparse_input 则保存了每个特征的取值
"""
query_emb_list = get_embedding_vec_list(sparse_embedding_dict, sparse_input, feature_dim_dict["sparse"],
sess_feature_list, sess_feature_list)
"""
-- 中途插入 --
get_embedding_vec_list 定义如下,用于获取特征对应的 embedding, 相当于在 Embedding Layer
做 embedding_lookup, 如果指定了 return_feat_list,那么将只会获取 return_feat_list 中特征对应的 embedding
"""
def get_embedding_vec_list(embedding_dict, input_dict, sparse_fg_list,return_feat_list=(),mask_feat_list=()):
embedding_vec_list = []
for fg in sparse_fg_list:
feat_name = fg.name
if len(return_feat_list) == 0 or feat_name in return_feat_list:
if fg.hash_flag:
## 做 hash 的时候,if mask_zero = True,0 or 0.0 will be set to 0,other value will be set in range[1,num_buckets)
lookup_idx = Hash(fg.dimension,mask_zero=(feat_name in mask_feat_list))(input_dict[feat_name])
else:
lookup_idx = input_dict[feat_name]
embedding_vec_list.append(embedding_dict[feat_name](lookup_idx))
return
query_emb_list 是一个 list,保存着两个大小为 [B, 1, 4] 的 Tensor (embedding_size = 4), 之后将特征进行 concatenation
query_emb = concat_fun(query_emb_list) ## [B, 1, 8]
之后再获取输入到 DNN 中的特征, 仍然是调用 get_embedding_vec_list 实现:
"""
sparse_embedding_dict 保存 embedding layer, sparse_input 保存特征,
feature_dim_dict["sparse"] 保存特征空间大小
传入 mask_feat_list 是想将这些特征中的 0 值直接映射为 0 向量;
"""
deep_input_emb_list = get_embedding_vec_list(sparse_embedding_dict, sparse_input, feature_dim_dict["sparse"],
mask_feat_list=sess_feature_list)
deep_input_emb = concat_fun(deep_input_emb_list) ## [B, 1, 60], 15 个稀疏特征, emb_dim=4
deep_input_emb = Flatten()(NoMask()(deep_input_emb)) ## [B, 60]
获取到 deep_input_emb 后,相当于完成了模型示意图中的左下角部分:

下一步实现 Session Division Layer, 用于将用户行为划分为 Session, 并加上 Bias-Encoding. 但由于在制作数据集的时候已经对行为划分了 Session, 所以在这一步主要内容是将行为转换为 embedding:
"""
tr_input: list, 长度等于 sess_max_count, 每个元素为 [B, 10, 8] 大小的 Tensor,
10 表示 max_session_len, tr_input 的前缀 tr_ 表示 transformer,说明该变量是 Transformer 的输入
"""
tr_input = sess_interest_division(sparse_embedding_dict, user_behavior_input_dict, feature_dim_dict['sparse'],
sess_feature_list, sess_max_count, bias_encoding=bias_encoding)
""""
-- 中途插入 --
sess_interest_division 函数的定义如下:
"""
def sess_interest_division(sparse_embedding_dict, user_behavior_input_dict, sparse_fg_list, sess_feture_list,
sess_max_count,
bias_encoding=True):
tr_input = []
"""
使用 get_embedding_vec_list 获取每个行为对应的 embedding,
行为使用 (cate_id, brand) 表示,而 cate_id 和 brand 对应的 Tensor 大小
均为 [B, 10], 因此 get_embedding_vec_list 得到两个大小为 [B, 10, 4] 的 Tensor,
使用 concat_fun 进行 concat,得到 keys_emb shape=[B, 10, 8]
tr_input 的长度为 sess_max_count=5
"""
for i in range(sess_max_count):
sess_name = "sess_" + str(i)
keys_emb_list = get_embedding_vec_list(sparse_embedding_dict, user_behavior_input_dict[sess_name],
sparse_fg_list, sess_feture_list, sess_feture_list)
keys_emb = concat_fun(keys_emb_list) ## [B, 10, 8]
tr_input.append(keys_emb)
## 加上 BiasEncoding
if bias_encoding:
tr_input = BiasEncoding(sess_max_count)(tr_input)
return
上面代码中用到了论文中介绍的 BiasEncoding,单独介绍一下, 其核心代码段如下:
class BiasEncoding(Layer):
def __init__(self, sess_max_count, seed=1024, **kwargs):
self.sess_max_count = sess_max_count
self.seed = seed
super(BiasEncoding, self).__init__(**kwargs)
def build(self, input_shape):
# Create a trainable weight variable for this layer.
if self.sess_max_count == 1:
embed_size = input_shape[2].value
seq_len_max = input_shape[1].value
else:
embed_size = input_shape[0][2].value
seq_len_max = input_shape[0][1].value
"""
sess_bias_embedding: [sess_max_count, 1, 1]
seq_bias_embedding: [1, seq_len_max, 1]
item_bias_embedding: [1, 1, embed_size]
注意它们的 shape
"""
self.sess_bias_embedding = self.add_weight('sess_bias_embedding', shape=(self.sess_max_count, 1, 1),
initializer=TruncatedNormal(
mean=0.0, stddev=0.0001, seed=self.seed))
self.seq_bias_embedding = self.add_weight('seq_bias_embedding', shape=(1, seq_len_max, 1),
initializer=TruncatedNormal(
mean=0.0, stddev=0.0001, seed=self.seed))
self.item_bias_embedding = self.add_weight('item_bias_embedding', shape=(1, 1, embed_size),
initializer=TruncatedNormal(
mean=0.0, stddev=0.0001, seed=self.seed))
def call(self, inputs, mask=None):
"""
:param concated_embeds_value: None * field_size * embedding_size
:return: None*1
sess_bias_embedding + seq_bias_embedding + item_bias_embedding
三者通过 Broadcast 进行相加
"""
transformer_out = []
for i in range(self.sess_max_count):
transformer_out.append(
inputs[i] + self.item_bias_embedding + self.seq_bias_embedding + self.sess_bias_embedding[i])
return
得到经过 BiasEncoding 处理后的 Session 输入后,相当于实现了模型示意图中的如下部分:

下一步要将其输入到 Multi-Head Self-Attention 中,以学习 Session 内各行为的内在关系, 并学习出对应的 Session 兴趣,这一步相当于实现了 Session Interest Extractor Layer:
Self_Attention = Transformer(att_embedding_size, att_head_num, dropout_rate=0, use_layer_norm=False,
use_positional_encoding=(not bias_encoding), seed=seed, supports_masking=True,
blinding=True)
"""
tr_input 为 list,大小为 5,里面的元素为大小等于 [B, 10, 8] 的 Tensor
Self-Attention 的输出 sess_fea 大小为 [B, 5, 8], 5 为 sess_max_count
提前做个说明,Transformer 中完成每个 Session 的 Multi-Head Self-Attention 后,结果大小
应该是 out=[B, 10, 8], 但最后输出时会做 reduce_mean(out, axis=1, keep_dims=True),
用于生成 Session 兴趣对应的 embedding,大小为 [B, 1, 8], 由于总共有 sess_max_count=5 个 Session,所以最终的输出 sess_fea 大小为 [B, 5, 8]
"""
sess_fea = sess_interest_extractor(
tr_input, sess_max_count, Self_Attention) ## [B, 5, 8]
"""
-- 中途插入 --
sess_interest_extractor 函数的定义如下
"""
def sess_interest_extractor(tr_input, sess_max_count, TR):
"""
tr_input 为 list, 大小为 5,里面的元素大小为 [B, 10, 8]
sess_max_count=5
TR 即 Transformer,实现 Multi-Head Self-Attention
"""
tr_out = []
for i in range(sess_max_count):
tr_out.append(TR(
[tr_input[i], tr_input[i]]))
sess_fea = concat_fun(tr_out, axis=1)
return
Transformer 的核心代码就不贴了,太长了,知道它输入输出大小即可。上述代码完成了模型结构图中如下部分:

下一步使用 Bi-LSTM 学习 Session 兴趣之间的演进,完成了 Session Interest Interacting Layer 的实现:
"""
输入 sess_fea 大小为 [B, 5, 8]
输出 lstm_outputs 大小也为 [B, 5, 8]
"""
lstm_outputs = BiLSTM(len(sess_feature_list) * embedding_size,
layers=2, res_layers=0, dropout_rate=0.2, )(sess_fea)
相当于实现了模型示意图中的:

在得到 Session 兴趣序列后, 由于每个 session 兴趣对目标商品的影响不同,这里采用 Attention 机制来刻画目标商品和每个 session 兴趣之间的相关性,即实现 Session Interest Activating Layer 层:
"""
注意输入为 [query_emb, sess_fea, user_sess_length], 其中 query_emb 为目标商品对应的 embedding,大小为 [B, 8], 而 sess_fea 表示用户兴趣,大小为 [B, 5, 8], user_sess_length 大小为 [B, 1], 表示用户真实的 Session 的长度,在做 Attention 时,用作 Mask,以计算权重系数。
AttentionSequencePoolingLayer 即 DIN 网络中目标商品和历史行为的 Attention,关于 DIN 网络的介绍可以查看: https://zhuanlan.zhihu.com/p/338050940
输出结果 interest_attention_layer 的 shape=[B, 1, 8]
"""
interest_attention_layer: [B, 1, 8], user_sess_length: [B, 1]
interest_attention_layer = AttentionSequencePoolingLayer(att_hidden_units=(64, 16), weight_normalization=True,
supports_masking=False)(
[query_emb, sess_fea, user_sess_length])
"""
同理,这里时 query_emb 和 lstm_outputs 继续 Attention,
lstm_outputs 大小为 [B, 5, 8]
输出结果 lstm_attention_layer 大小为 [B, 1, 8]
"""
lstm_attention_layer = AttentionSequencePoolingLayer(att_hidden_units=(64, 16), weight_normalization=True)(
[query_emb, lstm_outputs, user_sess_length])
以上步骤相当于实现了模型示意图中的:

最后,将输入特征都给 Concat 起来:
## deep_input_emb: [B, 76] 60 + 8 + 8
deep_input_emb = Concatenate()(
[deep_input_emb, Flatten()(interest_attention_layer), Flatten()(lstm_attention_layer)])
## dense_input.values(): [B, 1], 表示 price
## 此时 deep_input_emb: [B, 77]
if len(dense_input) > 0: ## 如果存在稠密特征的话
deep_input_emb = Concatenate()(
[deep_input_emb] + list(dense_input.values()))
相当于实现了:

将 Concat 起来的向量输入到 DNN 中,实现对点击率的预估:
## output: [B, 80], dnn_hidden_units: [200, 80]
output = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn,
dnn_dropout, dnn_use_bn, seed)(deep_input_emb)
output = Dense(1, use_bias=False, activation=None)(output)
## output: [B, 1]
output = PredictionLayer(task)(output)
即实现:

至此,DSIN 的代码介绍完毕了。
总结
文章内容有点多,前前后后写了很久,每天挤点时间不容易,冬日应该冬眠的 ???????????? 后面再分析下 DIEN 的代码,Flag 还是要立的