推荐系统遇上深度学习 (二十四)-- 深度兴趣进化网络 DIEN 原理及实战!


本文地址:http://www.6aiq.com/article/1547907379260
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出

原文发布于微信公众号 - 小小挖掘机(wAIsjwj)
原文发表时间:2018-11-04

在本系列的推荐系统遇上深度学习 (十八)–探秘阿里之深度兴趣网络(DIN) 浅析及实现中,我们介绍了阿里的深度兴趣网络(Deep Interest Network,以下简称 DIN),时隔一年,阿里再次升级其模型,提出了深度兴趣进化网络(Deep Interest Evolution Network, 以下简称 DIEN,论文地址:https://arxiv.org/pdf/1809.03672.pdf),并将其应用于淘宝的广告系统中,获得了 20.7% 的 CTR 的提升。本篇,我们一同来探秘 DIEN 的原理及实现。

1、背景

在大多数非搜索电商场景下,用户并不会实时表达目前的兴趣偏好。因此通过设计模型来捕获用户的动态变化的兴趣,是提升 CTR 预估效果的关键。阿里之前的 DIN 模型将用户的历史行为来表示用户的兴趣,并强调了用户兴趣的多样性和动态变化性,因此通过 attention-based model 来捕获和目标物品相关的兴趣。虽然 DIN 模型将用户的历史行为来表示兴趣,但存在两个缺点:
1)用户的兴趣是不断进化的,而 DIN 抽取的用户兴趣之间是独立无关联的,没有捕获到兴趣的动态进化性
2)通过用户的显式的行为来表达用户隐含的兴趣,这一准确性无法得到保证。

基于以上两点,阿里提出了深度兴趣演化网络 DIEN 来 CTR 预估的性能。DIEN 模型的主要贡献点在于:
1)模型关注电商系统中兴趣演化的过程,并提出了新的网络结果来建模兴趣进化的过程,这个模型能够更精确的表达用户兴趣,同时带来更高的 CTR 预估准确率。
2)设计了兴趣抽取层,并通过计算一个辅助 loss,来提升兴趣表达的准确性。
3)设计了兴趣进化层,来更加准确的表达用户兴趣的动态变化性。

接下来,我们来一起看一下 DIEN 模型的原理。

2、DIEN 模型原理

2.1 模型总体结构

我们先来对比一下 DIN 和 DIEN 的结构。
DIN 的模型结构如下:

DIEN 的模型结构如下:

可以看到,DIN 和 DIEN 的最底层都是 Embedding Layer,User profile, target AD 和 context feature 的处理方式是一致的。不同的是,DIEN 将 user behavior 组织成了序列数据的形式,并把简单的使用外积完成的 activation unit 变成了一个 attention-based GRU 网络。

2.2 兴趣抽取层 Interest Extractor Layer

兴趣抽取层 Interest Extractor Layer 的主要目标是从 embedding 数据中提取出 interest。但一个用户在某一时间的 interest 不仅与当前的 behavior 有关,也与之前的 behavior 相关,所以作者们使用 GRU 单元来提取 interest。GRU 单元的表达式如下:

这里我们可以认为 ht 是提取出的用户兴趣,但是这个地方兴趣是否表示的合理呢?文中别出心裁的增加了一个辅助 loss,来提升兴趣表达的准确性:

这里,作者设计了一个二分类模型来计算兴趣抽取的准确性,我们将用户下一时刻真实的行为 e(t+1) 作为正例,负采样得到的行为作为负例 e(t+1)’,分别与抽取出的兴趣 h(t) 结合输入到设计的辅助网络中,得到预测结果,并通过 logloss 计算一个辅助的损失:

2.3 兴趣进化层 Interest Evolution Layer

兴趣进化层 Interest Evolution Layer 的主要目标是刻画用户兴趣的进化过程。举个简单的例子:

以用户对衣服的 interest 为例,随着季节和时尚风潮的不断变化,用户的 interest 也会不断变化。这种变化会直接影响用户的点击决策。建模用户兴趣的进化过程有两方面的好处:
1)追踪用户的 interest 可以使我们学习 final interest 的表达时包含更多的历史信息。
2)可以根据 interest 的变化趋势更好地进行 CTR 预测。

而 interest 在变化过程中遵循如下规律:
1)interest drift:用户在某一段时间的 interest 会有一定的集中性。比如用户可能在一段时间内不断买书,在另一段时间内不断买衣服。
2)interest individual:一种 interest 有自己的发展趋势,不同种类的 interest 之间很少相互影响,例如买书和买衣服的 interest 基本互不相关。

为了利用这两个时序特征,我们需要再增加一层 GRU 的变种,并加上 attention 机制以找到与 target AD 相关的 interest。

attention 的计算方式如下:

而 Attention 和 GRU 结合起来的机制有很多,文中介绍了一下三种:

GRU with attentional input (AIGRU)
这种方式将 attention 直接作用于输入,无需修改 GRU 的结构:

Attention based GRU(AGRU)
这种方式需要修改 GRU 的结构,此时 hidden state 的输出变为:

GRU with attentional update gate (AUGRU)
这种方式需要修改 GRU 的结构,此时 hidden state 的输出变为:



2.4 模型试验

文章在公共数据和自己的数据集上都做了实验,并选取了不同的对比模型:

离线实验的结果如下:

DIEN 使用了辅助 loss 和 AUGRU 结构,而 BaseModel + GRU + AUGRU 与 DIEN 的不同之处就是没有增加辅助 loss。可以看到,DIEN 的实验效果远好于其他模型。

3、DIEN 模型实现

本文模型的实现参考代码是:https://github.com/mouna99/dien
本文代码的地址为:https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-DIEN-Demo
本文数据的地址为:https://github.com/mouna99/dien

3.1 数据介绍

根据 github 中提供的数据,解压后的文件如下:
uid_voc.pkl: 用户名对应的 id
mid_voc.pkl: item 对应的 id
cat_voc.pkl:category 对应的 id
item-info:item 对应的 category 信息
reviews-info:用于进行负采样的数据
local_train_splitByUser: 训练数据,一行格式为:label、用户名、目标 item、 目标 item 类别、历史 item、历史 item 对应类别。
local_test_splitByUser: 测试数据,格式同训练数据

3.2 代码实现

本文的代码主要包含以下几个文件:
rnn.py:对 tensorflow 中原始的 rnn 进行修改,目的是将 attention 同 rnn 进行结合。
vecAttGruCell.py: 对 GRU 源码进行修改,将 attention 加入其中,设计 AUGRU 结构
data_iterator.py: 数据迭代器,用于数据的不断输入
utils.py: 一些辅助函数,如 dice 激活函数、attention score 计算等
model.py:DIEN 模型文件
train.py: 模型的入口,用于训练数据、保存模型和测试数据

好了,接下来我们介绍一些关键的代码。

输入数据介绍

输入的数据有用户 id、target 的 item id、target item 对应的 cateid、用户历史行为的 item id list、用户历史行为 item 对应的 cate id list、历史行为的长度、历史行为的 mask、目标值、负采样的数据。

对于每一个用户的历史行为,代码中选取了 5 个样本作为负样本。

self.mid_his_batch_ph = tf.placeholder(tf.int32,[None,None],name='mid_his_batch_ph')
self.cat_his_batch_ph = tf.placeholder(tf.int32,[None,None],name='cat_his_batch_ph')
self.uid_batch_ph = tf.placeholder(tf.int32,[None,],name='uid_batch_ph')
self.mid_batch_ph = tf.placeholder(tf.int32,[None,],name='mid_batch_ph')
self.cat_batch_ph = tf.placeholder(tf.int32,[None,],name='cat_batch_ph')
self.mask = tf.placeholder(tf.float32,[None,None],name='mask')
self.seq_len_ph = tf.placeholder(tf.int32,[None],name='seq_len_ph')
self.target_ph = tf.placeholder(tf.float32,[None,None],name='target_ph')
self.lr = tf.placeholder(tf.float64,[])
self.use_negsampling = use_negsampling
if use_negsampling:
    self.noclk_mid_batch_ph = tf.placeholder(tf.int32, [None, None, None], name='noclk_mid_batch_ph')
    self.noclk_cat_batch_ph = tf.placeholder(tf.int32, [None, None, None], name='noclk_cat_batch_ph')

输入数据转换为对应的 embedding

接下来,输入数据将转换为对应的 embedding:

with tf.name_scope("Embedding_layer"):
    self.uid_embeddings_var = tf.get_variable("uid_embedding_var",[n_uid,EMBEDDING_DIM])
    tf.summary.histogram('uid_embeddings_var', self.uid_embeddings_var)
    self.uid_batch_embedded = tf.nn.embedding_lookup(self.uid_embeddings_var,self.uid_batch_ph)

    self.mid_embeddings_var = tf.get_variable("mid_embedding_var",[n_mid,EMBEDDING_DIM])
    tf.summary.histogram('mid_embeddings_var',self.mid_embeddings_var)
    self.mid_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var,self.mid_batch_ph)
    self.mid_his_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var,self.mid_his_batch_ph)
    if self.use_negsampling:
        self.noclk_mid_his_batch_embedded = tf.nn.embedding_lookup(self.mid_embeddings_var,
                                                                   self.noclk_mid_batch_ph)

    self.cat_embeddings_var = tf.get_variable("cat_embedding_var", [n_cat, EMBEDDING_DIM])
    tf.summary.histogram('cat_embeddings_var', self.cat_embeddings_var)
    self.cat_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.cat_batch_ph)
    self.cat_his_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var, self.cat_his_batch_ph)
    if self.use_negsampling:
        self.noclk_cat_his_batch_embedded = tf.nn.embedding_lookup(self.cat_embeddings_var,
                                                                   self.noclk_cat_batch_ph)
																   

接下来,将 item 的 id 对应的 embedding 以及 item 对应的 cateid 的 embedding 进行拼接,共同作为 item 的 embedding:

self.item_eb = tf.concat([self.mid_batch_embedded,self.cat_batch_embedded],1)
self.item_his_eb = tf.concat([self.mid_his_batch_embedded,self.cat_his_batch_embedded],2)

if self.use_negsampling:
    self.noclk_item_his_eb = tf.concat(
        [self.noclk_mid_his_batch_embedded[:, :, 0, :], self.noclk_cat_his_batch_embedded[:, :, 0, :]], -1)
    self.noclk_item_his_eb = tf.reshape(self.noclk_item_his_eb,
                                        [-1, tf.shape(self.noclk_mid_his_batch_embedded)[1], EMBEDDING_DIM * 2]) # 负采样的item选第一个

    self.noclk_his_eb = tf.concat([self.noclk_mid_his_batch_embedded, self.noclk_cat_his_batch_embedded], -1)

第一层 GRU

接下来,我们要将用户行为历史的 item embedding 输入到 dynamic rnn 中,同时计算辅助 loss:

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)

aux_loss_1 = self.auxiliary_loss(rnn_outputs[:,:-1,:],self.item_his_eb[:,1:,:],self.noclk_item_his_eb[:,1:,:],self.mask[:,1:],stag="gru")
self.aux_loss = aux_loss_1

辅助 loss 的计算其实是一个二分类模型,代码如下:

def auxiliary_loss(self,h_states,click_seq,noclick_seq,mask,stag=None):
    mask = tf.cast(mask,tf.float32)
    click_input = tf.concat([h_states,click_seq],-1)
    noclick_input = tf.concat([h_states,noclick_seq],-1)
    click_prop_ = self.auxiliary_net(click_input,stag=stag)[:,:,0]
    noclick_prop_ = self.auxiliary_net(noclick_input,stag=stag)[:,:,0]
    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_

def auxiliary_net(self,input,stag='auxiliary_net'):
    bn1 = tf.layers.batch_normalization(inputs=input, 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, 2, activation=None, name='f3' + stag, reuse=tf.AUTO_REUSE)
    y_hat = tf.nn.softmax(dnn3) + 0.00000001
    return y_hat

AUGRU

我们首先需要计算 attention 的 score,然后将其作为 GRU 的一部分输入:

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)

接下来,就是 AUGRU 的结构,这里我们需要设计一个新的 VecAttGRUCell 结构,相比于 GRUCell,修改的地方如下:

上图中左侧是 GRU 的源码,右侧是 VecAttGRUCell 的代码,我们主要修改了 call 函数中的代码,在 GRU 中,hidden state 的计算为:

new_h = u * state + (1 - u) * c

AUGRU 中,hidden state 的计算为:

u = (1.0 - att_score) * u
new_h = u * state + (1 - u) * c

代码中给出的 hidden state 计算可能与文中有些出入,不过核心的思想都是,对于 attention score 大的,保存的当前的 c 就多一些。

设计好了新的 GRU Cell,我们就能计算兴趣的进化过程:

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)

得到兴趣进化的结果 final_state2 之后,需要与其他的 embedding 进行拼接,得到全联接层的输入:

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)

全联接层得到最终输出

最后我们通过一个多层神经网络,得到最终的 ctr 预估值:

def build_fcn_net(self,inp,use_dice=False):
    bn1 = tf.layers.batch_normalization(inputs=inp,name='bn1')
    dnn1 = tf.layers.dense(bn1,200,activation=None,name='f1')

    if use_dice:
        dnn1 = dice(dnn1,name='dice_1')
    else:
        dnn1 = prelu(dnn1,'prelu1')

    dnn2 = tf.layers.dense(dnn1,80,activation=None,name='f2')
    if use_dice:
        dnn2 = dice(dnn2,name='dice_2')
    else:
        dnn2 = prelu(dnn2,name='prelu2')

    dnn3 = tf.layers.dense(dnn2,2,activation=None,name='f3')
    self.y_hat = tf.nn.softmax(dnn3) + 0.00000001

    with tf.name_scope('Metrics'):
        ctr_loss = -tf.reduce_mean(tf.log(self.y_hat) * self.target_ph)
        self.loss = ctr_loss
        if self.use_negsampling:
            self.loss += self.aux_loss
        tf.summary.scalar('loss',self.loss)
        self.optimizer = tf.train.AdamOptimizer(learning_rate=self.lr).minimize(self.loss)

        self.accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.round(self.y_hat),self.target_ph),tf.float32))
        tf.summary.scalar('accuracy',self.accuracy)

    self.merged = tf.summary.merge_all()

这样,一个 DIEN 的模型就设计好了,其中的细节还是很多的,希望大家都能动手实现一下!

参考文献

1、https://blog.csdn.net/friyal/article/details/83115900
2、https://arxiv.org/pdf/1809.03672.pdf
3、https://github.com/mouna99/dien


本文地址:http://www.6aiq.com/article/1547907379260
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出