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

在本系列的第十八篇(https://www.jianshu.com/p/73b6f5d00f46)中,我们介绍了阿里的深度兴趣网络(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的模型结构如下:

DIN

DIEN的模型结构如下:

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单元的表达式如下:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容