Recommendation-System-5-DIN

Introduction

在过去的DeepCrossing, Wide&Deep, DeepFM, NFM等模型里面,这些模型都是通过把sparse feature变成embedding的vector,然后把dense的vectors进行特征交叉(feature intersection) 从而学习低阶和更加高阶的特征。Embeding&MLP模型对于这种推荐任务一般有着差不多的固定处理套路,就是大量稀疏特征先经过embedding层, 转成低维稠密的,然后进行拼接,最后喂入到多层神经网络中去。

然而,这些模型都是只考虑现有的特征进行交叉学习,而没有考虑用户的历史信息和过去的行为信息,没有从时间的角度考虑用户的行为变化。而对于历史行为信息挖掘的问题,又有很多不同的模型,比如用于推荐系统的GNN图神经网络, DIN (deep interest network)等。 而这次要介绍的是Deep Interest Network(DIIN)模型,它是2018年阿里巴巴提出来的模型, 该模型基于业务的观察,从实际应用的角度进行改进,相比于之前很多“学术风”的深度模型, 该模型更加具有业务气息。

Motivation

DIN 模型的应用场景是阿里巴巴的电商广告推荐业务, 这样的场景下一般会有大量的用户历史行为信息, 这个其实是很关键的。因而DIN为了解决这个业务场景有一些特点:

  1. Assumption:
    DIN 假设了应用场景里面有大量的用户的历史信息,而这些信息能够提供用户的兴趣和爱好以及购买/浏览等行为的变化。而DIN主要是关注和利用这些历史信息进行信息的挖掘

  2. Novelty:
    基于DIN的assumption下,DIN模型的创新点或者解决的问题就是使用了注意力机制来对用户的兴趣动态模拟, 这样就能猜测出用户的大致兴趣来,这样我们的推荐才能做的更加到位,所以这个模型的使用场景是非常注重用户的历史行为特征(历史购买过的商品或者类别信息)。而和之前学习的DeepFM, wide&deep等模型相比,之前的模型都没有考虑历史信息的问题。

Deep Interest Network (DIN)

由于DIN考虑了用户的历史浏览行为特征,在输入的特征表达里面和过去的模型有出入,所以这个部分先考虑输入特征的表达之后考虑DIN的网络架构

DIN 的特征表达 (feature representation)

在过去的DeepFM和NeuralFM里面它们的特征交叉的方式都以下的形式,要么直接通过embedding投映然后将特征进行直接交叉,要么用神经网络把特征通过DNN的方式进行高阶特征学习 而这些输入的$x_i$的特征是sparse的one-hot vector里面要么是1 表明某个特征有出现,要么0 表示没出现。

Multi-one-hot

但是在考虑到用户历史行为里面,用一个feature来代表用户的历史浏览的item时,它的可以千变万化并且也有很多数量的长度不确定。举个例子:输入的特征是 [age =18, gender = Female, product_cat = book, visited_cat_ids = {book, bag, computer, paper},visited_shop_id={TV,movie} ], 那么这里的 visited_cat_ids 这个list就是过去看过的item的history。而这个list根据用户不同的浏览历史,它的长度和item的值也是不一样的。如果直接把这个feature变成one-hot是难以表达的因为one-hot里面只能有一个1代表有浏览某一个item,但是不能代表多个。

面对这个问题DIN的处理方法是将数据集里面的所有的visit过的items进行合并成一个集合,然后对这个集合变成一个multi-onehot的vector用来表示用户历史浏览过的多个item。 而每个浏览过的item可以通过embedding方法变成对应的dense vector,这样就能得到多个embedding vector。而embedding vector的个数也会因为每个用户浏览的历史的item个数不同而不同。

对multi-onehot 举个例子就像之前的NFM的结构一样,输入的sparse vector里面有多个1,而每个1代表浏览过的item,比如一个multi-onhot的vector里面有10个item因此vector的长度 是10,而 vector每个scalar value对应一个item,如果这个value=1那么这个item就是浏览过的。比如 visited_cat_ids = {book, bag, computer, paper}那么对应book, bag, computer, paper的4个item对应位置的scalar value就是1,其他位置的value就是0。

Pooling for embedding vectors

那么问题又来了,每个用户的embedding的vector的个数不一样,但DNN的输入的shape是固定的,怎么才能把他们这些不同数目的embedding的vectors变成相同大小的特征?

方法就是pooling,把多出来的vectors通过pooling的方法进行concate 拼接的方法,形成一个固定大小的DNN的输入表达形式,如果有连续的feature value也把它的embedding拼接上去,它的公式是
$$
e_i=pooling(e_{i1}, e_{i2}, …e_{ik})
$$

这里的$i$表示第$i$个历史特征组(是历史行为,比如历史的商品id,历史的商品类别id等, 这些特征组都是通过multi-onehot 来表示), 这里的$k$表示对应历史特种组里面用户购买过的商品数量,也就是历史embedding的数量。

特征表达的小总结

  • Dense型特征: 由于是数值型了,这里为每个这样的特征建立Input层接收这种输入, 然后拼接起来先放着,等离散的那边处理好之后,和离散的拼接起来进DNN

  • Sparse型特征: 为离散型特征建立Input层接收输入,然后需要先通过embedding层转成低维稠密向量,然后拼接起来放着,等变长离散那边处理好之后, 一块拼起来进DNN, 但是这里面要注意有个特征的embedding向量还得拿出来用(广告商品的embedding),就是候选商品的embedding向量,这个还得和后面的计算相关性,对历史行为序列加权。

  • VarlenSparse型特征: 这个一般指的用户的历史行为特征,变长数据, 首先会进行padding操作成等长, 然后建立Input层接收输入,然后通过embedding层得到各自历史行为的embedding向量, 拿着这些向量与上面的候选商品embedding向量进入+ AttentionPoolingLayer去对这些历史行为特征加权合并,最后得到输出。

DIN 网络结构

DIN Base model:

DIN 的改进结构如下:

  1. Embedding layer:这个层的作用是把高维稀疏的输入转成低维稠密向量, 每个离散特征下面都会对应着一个embedding词典, 维度是$D\times K$, 这里的$D$表示的是隐向量的维度, 而$K$表示的是当前离散特征的唯一取值个数。 其他离散特征也是同理,只不过上面那个multi-hot编码的那个,会得到一个embedding向量的列表,因为他开始的那个multi-hot向量不止有一个是1,这样乘以embedding矩阵,就会得到一个列表了。通过这个层,上面的输入特征都可以拿到相应的稠密embedding向量了。

  2. pooling layer and Concat layer: pooling层的作用是将用户的历史行为embedding这个最终变成一个定长的向量,因为每个用户历史购买的商品数是不一样的, 也就是每个用户multi-hot中1的个数不一致,这样经过embedding层,得到的用户历史行为embedding的个数不一样多,也就是上面的embedding列表不一样长, 那么这样的话,每个用户的历史行为特征拼起来就不一样长了。 而后面如果加全连接网络的话,我们知道,他需要定长的特征输入。 所以往往用一个pooling layer先把用户历史行为embedding变成固定长度(统一长度),就像之前所说的pooling的公式一样。我们可以考的上面的图片里面Goods1的各个纵向的embedding vector在通过Concat之后变成单个横向的vector,而横向的vector里面的颜色对应之前的embedding的vector,但是长度变小了,这个是因为用了pooling的原因。
    Concat layer层的作用就是拼接了,就是把这所有的特征embedding向量,如果再有连续特征的话也算上,从特征维度拼接整合,作为MLP的输入。

  3. MLP:这个就是普通的全连接,用了学习特征之间的各种交互。

  4. Loss: 由于这里是点击率预测任务, 二分类的问题,所以这里的损失函数用的负的log对数似然:
    $$
    L=-\frac{1}{N} \sum_{(\boldsymbol{x}, y) \in S}(y \log p(\boldsymbol{x})+(1-y) \log (1-p(\boldsymbol{x})))
    $$

这里改进的地方已经框出来了,这里会发现相比于base model, 这里加了一个local activation unit, 这里面是一个前馈神经网络,输入是用户历史行为商品和当前的候选商品, 输出是它俩之间的相关性, 这个相关性相当于每个历史商品的权重,把这个权重与原来的历史行为embedding相乘求和就得到了用户的兴趣表示$v_{U}(A)$, 这个东西的计算公式如下:

$$
\begin{array}{cc}
v_U(A)=f(v_{A}, e_{1}, e_{2}, \ldots, e_{H})\\
=\sum_{j=1}^{H} a(e_{j}, v_{A}) e_{j} \\
=\sum_{j=1}^{H} w_{j} e_{j}\\
\end{array}
$$

这里的${v_{A}, e_{1},e_{2}, \ldots, e_{H}}$是用户$U$的历史行为特征embedding, $v_{A}$表示的是候选广告$A$的embedding向量, $a(e_j, v_A)=w_j$表示的权重或者历史行为商品与当前广告$A$的相关性程度。$a(\cdot)$表示的上面那个前馈神经网络,也就是那个所谓的注意力机制, 当然,看图里的话,输入除了历史行为向量和候选广告向量外,还加了一个它俩的外积操作,作者说这里是有利于模型相关性建模的显性知识。

这里有一点需要特别注意,就是这里的权重加和不是1, 准确的说这里不是权重, 而是直接算的相关性的那种分数作为了权重,也就是平时的那种scores(softmax之前的那个值),这个是为了保留用户的兴趣强度。

Properties

优点

  • 考虑了用户的历史浏览信息,更加贴近真实的业务场景
  • 也用embedding 和特征交叉的方式进行特征的学习和挖掘以及降维

    缺点

  • 考虑用户浏览信息时没有考虑历史的先后顺序,只是把历史的信息和广告的信息特征交叉,没有考虑历史浏览的item的先后关联,比如一个物品被浏览后用户容易浏览下一个相关的物品
  • 用户最新浏览的商品很有可能不在用户过去的浏览的历史里面,这样的话multi-onehot vector的长度就会变化, embedding也要重新训练,并且随着浏览的商品增多,onehot的长度越大,embedding训练也会越困难,在随着时间变化而特征也会变化这也是个问题。所以embedding要定期更新。
  • 一般来说,用户浏览的历史有很多,那样embedding的个数就需要很多。但是对于一些浏览的历史里面,很有可能会存在大量的用户浏览了但是不感兴趣的内容,是否能够把这些内容进行过滤(比如更加浏览的时间来判断是否应该把item加入历史的list里面)从而降低onehot 的大小从而来加速模型的训练?

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 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

Reference

[1] paper: https://arxiv.org/pdf/1706.06978.pdf
[2] datawhale: https://github.com/wenkangwei/team-learning-rs/blob/master/DeepRecommendationModel/DIN.md
[3] https://cloud.tencent.com/developer/article/1164761
[4] DeepCTR github https://github.com/wenkangwei/DeepCTR

Comments