DIN(Deep Interest Network)深度兴趣网络
在业务场景中,会有大量的用户历史行为信息。DIN模型的创新点就是使用了注意力机制来对用户的兴趣动态模拟,而这个前提就是用户之前有大量的历史行为了,这样我们在预测某个商品广告用户是否点击的时候,可以参考他之前购买过或者查看过的商品。 用户对商品的点击行为实际上很大程度依赖于他的历史行为,究竟用户历史行为中的哪个会对当前的点击预测带来积极的作用。而实际上,对于用户点不点击当前的商品广告,很大程度上是依赖于他的历史行为的。
DIN的动机就是在业务的角度上,我们应该自适应的去捕捉用户的兴趣变化,这样才能较为准确的实施广告推荐;而放到模型的角度,我们应该考虑到用户的历史行为商品与当前商品广告的一个关联性。关联性就使用注意力机制来做到,设计了一个"local activation unit"结构,利用候选商品和历史问题商品之间的相关性计算出权重,这个就代表了对于当前商品广告的预测,用户历史行为的各个商品的重要程度大小,而加入了注意力权重的深度学习网络,就是这次的主角DIN:
DIN 模型结构以及原理
- 特征表示:在用户的序列数据中,用户的浏览商品和加购商品等每个人都是不同长度的,所以一般都使用multi-hot编码来做;
- 基准模型:其实就是Embedding&MLP形式,只是在此基础上加入了注意力的结构;

- Embedding layer:把高维稀疏的输入转成低维稠密向量,每个离散特征下面都会对应着一个embedding词典,维度是D x K,这里的D表示的是隐向量的维度,K表示当前离散特征的唯一取值个数;所以Embeeding矩阵 * one-hot(K x 1矩阵)编码可以得到D x 1的embedding向量,这样就可以得到稀疏特征的稠密向量了。同时,multi-hot编码可以得到一个embedding向量的列表。
- Pooling layer and Concat layer: pooling层的作用是将用户的历史行为embedding最终变成一个定长的向量,因为每个用户历史购买的商品数不一样,所以先pooling,再拼接;
- MLP:全连接层,学习特征之间的各种交互。
- Loss:点击率预测任务,二分类问题,所以这里的损失函数用的交叉熵损失
基准模型的问题:用户行为在和候选商品交互之前已经被pooling和concat了,所以损失了部分信息,尤其是用户行为较多的时候;
解决方法:在当前候选广告和用户的历史行为之间引入注意力的机制,这样在预测当前物品是否点击的时候,让模型更关注于与当前物品相关的那些用户历史产品

DIN就是引入候选物品与用户历史行为的注意力机制local activation unit,这里其实是一个前馈神经网络,输入是用户历史行为商品和当前的候选商品,输出是它俩之间的相关性, 这个相关性相当于每个历史商品的权重,把这个权重与原来的历史行为embedding相乘求和就得到了用户的兴趣表示:

- 需要注意的是:这里加权和不是1,准确的说这里不是权重,而是直接算的相关性分数,也就是在softmax之前的权重,这是为了保留用户的兴趣强度。
DIN 实现
使用movielens数据集,其中会有用户过去对电影评分的一系列行为;这种历史行为是序列性质的特征,并且不同的用户这种历史行为特征长度会不一样,所以我们在放入神经网络的时候会进行padding(不够的填充0),而到具体层上进行运算的时候,会用mask掩码标记出填充位置。
DIN模型的输入特征大致上分为三类:Dense(连续型),Sparse(离散型),VarlenSparse(变长离散型),也就是指的上面的历史行为数据。而不同的类型特征也就决定了后面处理的方式会不同:
- Dense型特征:由于是数值型,这里为每个这样的特征建立input层接收这种输入,然后拼接起来。
- Sparse型特征:把离散型特征通过embedding层转成低维稠密向量,然后拼接起来。这里面的候选商品的embedding向量还要和变长特征进行计算。
- VarlenSparse型特征:一般指的是用户的历史行为特征,变长数据,首先会进行padding操作成等长,然后接受输入,通过embedding层得到各自历史行为的embedding向量,拿着这些向量与上面的候选商品embedding向量进入AttentionPoolingLayer去对这些历史行为特征加权合并,最终得到输出。
最后,把上面三种特征拼接进DNN网络,得到最后的输出结果即可。
python
# DIN网络搭建
def DIN(feature_columns, behavior_feature_list, behavior_seq_feature_list):
"""
这里搭建DIN网络,有了上面的各个模块,这里直接拼起来
:param feature_columns: A list. 里面的每个元素是namedtuple(元组的一种扩展类型,同时支持序号和属性名访问组件)类型,表示的是数据的特征封装版
:param behavior_feature_list: A list. 用户的候选行为列表
:param behavior_seq_feature_list: A list. 用户的历史行为列表
"""
# 构建Input层并将Input层转成列表作为模型的输入
input_layer_dict = build_input_layers(feature_columns)
input_layers = list(input_layer_dict.values())
# 筛选出特征中的sparse和Dense特征, 后面要单独处理
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns))
dense_feature_columns = list(filter(lambda x: isinstance(x, DenseFeat), feature_columns))
# 获取Dense Input
dnn_dense_input = []
for fc in dense_feature_columns:
dnn_dense_input.append(input_layer_dict[fc.name])
# 将所有的dense特征拼接
dnn_dense_input = concat_input_list(dnn_dense_input) # (None, dense_fea_nums)
# 构建embedding字典
embedding_layer_dict = build_embedding_layers(feature_columns, input_layer_dict)
# 离散的这些特特征embedding之后,然后拼接,然后直接作为全连接层Dense的输入,所以需要进行Flatten
dnn_sparse_embed_input = concat_embedding_list(sparse_feature_columns, input_layer_dict, embedding_layer_dict, flatten=True)
# 将所有的sparse特征embedding特征拼接
dnn_sparse_input = concat_input_list(dnn_sparse_embed_input) # (None, sparse_fea_nums*embed_dim)
# 获取当前行为特征的embedding, 这里有可能有多个行为产生了行为列表,所以需要列表将其放在一起
query_embed_list = embedding_lookup(behavior_feature_list, input_layer_dict, embedding_layer_dict)
# 获取历史行为的embedding, 这里有可能有多个行为产生了行为列表,所以需要列表将其放在一起
keys_embed_list = embedding_lookup(behavior_seq_feature_list, input_layer_dict, embedding_layer_dict)
# 使用注意力机制将历史行为的序列池化,得到用户的兴趣
dnn_seq_input_list = []
for i in range(len(keys_embed_list)):
seq_embed = AttentionPoolingLayer()([query_embed_list[i], keys_embed_list[i]]) # (None, embed_dim)
dnn_seq_input_list.append(seq_embed)
# 将多个行为序列的embedding进行拼接
dnn_seq_input = concat_input_list(dnn_seq_input_list) # (None, hist_len*embed_dim)
# 将dense特征,sparse特征, 即通过注意力机制加权的序列特征拼接起来
dnn_input = Concatenate(axis=1)([dnn_dense_input, dnn_sparse_input, dnn_seq_input]) # (None, dense_fea_num+sparse_fea_nums*embed_dim+hist_len*embed_dim)
# 获取最终的DNN的预测值
dnn_logits = get_dnn_logits(dnn_input, activation='prelu')
model = Model(inputs=input_layers, outputs=dnn_logits)
return model

DIEN(Deep Interst Evolving Network)
在推荐场景,用户无需输入搜索关键词来表达意图,这种情况下捕捉用户兴趣并考虑兴趣的动态变化将是提升模型效果的关键。但是大多数的模型(包括DIN)直接使用用户行为作为兴趣,但是用户潜在兴趣一般与用户行为不一定有直接表示关系。DIN模型考虑了用户兴趣,并且强调用户兴趣是多样的,该模型使用注意力机制来捕捉和target item的相关兴趣,这样一来用户的兴趣就会随着目标商品自适应的改变。
DIEN模型原理

DIEN模型的重点就是如何将用户的行为序列转换成与用户兴趣相关的向量,在DIN中是直接通过与target item计算序列中每个元素的注意力分数,然后加权求和得到最终的兴趣表示向量。在DIEN中使用了两层结构来建模用户兴趣相关的向量。
GRU门控循环单元
更新门Zt,重置门Rt
Interest Extractor Layer
使用GRU(Gated Recurrent Unit,门控循环单元)【轻量级的循环神经网络,可以捕捉长距离信息】,为了更好的训练,引入辅助函数:我们用h(1)来预测第二次点击,h(2)来预测第三次点击,并计算loss
引入辅助函数的函数有:
- 辅助loss可以帮助GRU的隐状态更好地表示用户兴趣。
- RNN在长序列建模场景下梯度传播可能并不能很好的影响到序列开始部分,如果在序列的每个部分都引入一个辅助的监督信号,则可一定程度降低优化难度。
- 辅助loss可以给embedding层的学习带来更多语义信息,学习到item对应的更好的embedding。
最后损失函数为:

Interest Evolving Layer
从用户的行为序列,通过GRU+辅助损失建模之后,对用户行为序列中的兴趣进行了提取并表达成了向量的形式,而用户的兴趣会因为外部环境或内部认知随着时间变化,特点如下:
- 兴趣是多样化的,可能发生漂移。 没准用户瞬间兴趣转移
- 虽然兴趣可能会被影响,但是每一种兴趣都有自己的发展过程,我们只关注与target item相关的演进过程。 所以,我们再次使用attention机制,计算target item与用户兴趣(GRU计算出的hidden state)之间的相关系数,然后再次进行计算

DIEN代码
python
def DIEN(feature_columns, behavior_feature_list, behavior_seq_feature_list, neg_seq_feature_list, use_neg_sample=False, alpha=1.0):
# 构建输入层
input_layer_dict = build_input_layers(feature_columns)
# 将Input层转化为列表的形式作为model的输入
input_layers = list(input_layer_dict.values()) # 各个输入层
user_behavior_length = input_layer_dict["hist_len"]
# 筛选出特征中的sparse_fea, dense_fea, varlen_fea
sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns)) if feature_columns else []
dense_feature_columns = list(filter(lambda x: isinstance(x, DenseFeat), feature_columns)) if feature_columns else []
varlen_sparse_feature_columns = list(filter(lambda x: isinstance(x, VarLenSparseFeat), feature_columns)) if feature_columns else []
# 获取dense
dnn_dense_input = []
for fc in dense_feature_columns:
dnn_dense_input.append(input_layer_dict[fc.name])
# 将所有的dense特征拼接
dnn_dense_input = concat_input_list(dnn_dense_input)
# 构建embedding字典
embedding_layer_dict = build_embedding_layers(feature_columns, input_layer_dict)
# 因为这里最终需要将embedding拼接后直接输入到全连接层(Dense)中, 所以需要Flatten
dnn_sparse_embed_input = concat_embedding_list(sparse_feature_columns, input_layer_dict, embedding_layer_dict, flatten=True)
# 将所有sparse特征的embedding进行拼接
dnn_sparse_input = concat_input_list(dnn_sparse_embed_input)
# 获取当前的行为特征(movie)的embedding,这里有可能有多个行为产生了行为序列,所以需要使用列表将其放在一起
query_embed_list = embedding_lookup(behavior_feature_list, input_layer_dict, embedding_layer_dict)
# 获取行为序列(movie_id序列, hist_movie_id) 对应的embedding,这里有可能有多个行为产生了行为序列,所以需要使用列表将其放在一起
keys_embed_list = embedding_lookup(behavior_seq_feature_list, input_layer_dict, embedding_layer_dict)
# 把q,k的embedding拼在一块
query_emb, keys_emb = concat_input_list(query_embed_list), concat_input_list(keys_embed_list)
# 采样的负行为
neg_uiseq_embed_list = embedding_lookup(neg_seq_feature_list, input_layer_dict, embedding_layer_dict)
neg_concat_behavior = concat_input_list(neg_uiseq_embed_list)
# 兴趣进化层的计算过程
dnn_seq_input, aux_loss = interest_evolution(keys_emb, query_emb, user_behavior_length, neg_concat_behavior, gru_type="AUGRU")
# 后面的全连接层
deep_input_embed = Concatenate()([dnn_dense_input, dnn_sparse_input, dnn_seq_input])
# 获取最终dnn的logits
dnn_logits = get_dnn_logits(deep_input_embed, activation='prelu')
model = Model(input_layers, dnn_logits)
# 加兴趣提取层的损失 这个比例可调
if use_neg_sample:
model.add_loss(alpha * aux_loss)
# 所有变量需要初始化
tf.compat.v1.keras.backend.get_session().run(tf.compat.v1.global_variables_initializer())
return model
参考:funrec(datawhalechina.github.io/fun-rec/#/c...),