阿里DIN源码之如何建模用户序列(1):base方案

本文深入解析DIN模型,探讨如何通过注意力机制优化用户行为序列特征的抽取,提高推荐系统的精准度。介绍了传统用户行为序列特征抽取方法的局限性,并详细阐述DIN模型如何通过计算相关性模块对序列行为加权,获取更有效的embedding。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阿里这篇文章,说白了就是如何将用户的行为序列抽象出一个特征,这里我称之为行为emb,往常对用户的一组行为序列,都是平等对待,同权pooling,或者加时间衰减。

这篇文章好就好在他深刻的分析了用户行为意图,即用户的每个行为和候选商品的相关性是不同的,以此为契机,利用一个计算相关性的模块(后来也叫attention),对序列行为加权pooling,得到想要的embedding。

网上对DIN源码分享的资料很少,代码原理并没有什么难的,tf代码只要搞懂了维度怎么变化的(我尽量都标注),看明白还是比较简单的,只是如何提升效率就是一门大学问了,这里不讨论。这里主要是自己对源码的一个学习小结,分享出来,一些代码细节都已经趟过了…有不足的地方希望大家指正。

下面包含大量代码,请结合注释享用~~~
文章同步更新于知乎:https://zhuanlan.zhihu.com/p/111173364

数据形式

train_set:

(userid,[13179,17993],28326,1)

[userid,[13179,17993],33333,0]

test_set:

(userid,[13179,17993,28326],(正样本id,负样本id))

中括号括起来的是用户的行为序列对应的itemid。

那么,先讲一下传统的用户行为序列特征的抽取方法,即paper中图2的左侧部分。

Base model

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POvcsuUz-1583415091782)(C:\Users\Admin\AppData\Roaming\Typora\typora-user-images\image-20200305200049279.png)]

核心思想就是,将用户行为item list中的每个item,抽取特征做concat,然后直接做sum pooling,再其他特征输入MLP,最终得到候选商品的得分,也就是用户对候选商品的“喜好”程度。

先说下train.py部分,做的事情就是先用初始模型评估一遍测试集,然后按照batch训练,每1000次评估测试集。

流程很简单:

with tf.Session(config=tf.ConfigProto(gpu_options=gpu_options)) as sess:

  model = Model(user_count, item_count, cate_count, cate_list)
  sess.run(tf.global_variables_initializer())
  sess.run(tf.local_variables_initializer())

  print('test_gauc: %.4f\t test_auc: %.4f' % _eval(sess, model))
  sys.stdout.flush()
  lr = 1.0
  start_time = time.time()
  for _ in range(2):
    random.shuffle(train_set)
    epoch_size = round(len(train_set) / train_batch_size)
    loss_sum = 0.0
    for _, uij in DataInput(train_set, train_batch_size):
      loss = model.train(sess, uij, lr)
      loss_sum += loss

      if model.global_step.eval() % 1000 == 0:
        test_gauc, Auc = _eval(sess, model)
        print('Epoch %d Global_step %d\tTrain_loss: %.4f\tEval_GAUC: %.4f\tEval_AUC: %.4f' %
              (model.global_epoch_step.eval(), model.global_step.eval(),
               loss_sum / 1000, test_gauc, Auc))
        sys.stdout.flush()
        loss_sum = 0.0

      if model.global_step.eval() % 336000 == 0:
        lr = 0.1
    print('Epoch %d DONE\tCost time: %.2f' %
          (model.global_epoch_step.eval(), time.time()-start_time))
    sys.stdout.flush()
    model.global_epoch_step_op.eval()

  print('best test_gauc:', best_auc)
  sys.stdout.flush()

这里面主要有两个类要分别说下:DataInput、Model

首先,DataInput,其实这是一个迭代器,作用就是每次调用返回下一个batch的数据。这段代码涉及到数据如何按照batch划分,以及如何构造一个迭代器。

其中,输入进来的train_set,形式为(userid,[item1,item2],item3,1),分别对应(用户id,行为序列,候选itemid,label)

class DataInput:
    def __init__(self, data, batch_size):

        self.batch_size = batch_size
        self.data = data
        self.epoch_size = len(self.data) // self.batch_size
        if self.epoch_size * self.batch_size < len(self.data):
            self.epoch_size += 1
        self.i = 0
        
    def __iter__(self):
        return self
    def __next__(self):
        if self.i == self.epoch_size:
            raise StopIteration
        # 定义一个batch内的数据
        ts = self.data[self.i * self.batch_size: min((self.i + 1) * self.batch_size,
                                                     len(self.data))]
        self.i += 1
        u, i, y, sl = [], [], [], []
        for t in ts:
            u.append(t[0])  # userid
            i.append(t[2])  # 候选itemid
            y.append(t[3])  # label
            sl.append(len(t[1]))  # sl表示每个序列的真实长度,区别padding
        max_sl = max(sl)

        # padding,填充hist历史访问序列,不够的补0
        hist_i = np.zeros([len(ts), max_sl], np.int64)
        k = 0
        for t in ts:
            for l in range(len(t[1])):
                hist_i[k][l] = t[1][l]
            k += 1

        return self.i, (u, i, y, hist_i, sl)

返回的就是:迭代轮数,(userid,候选itemid,label,已经padding后的行为序列,真实序列长度)。

之后,送入模型训练,也就是train.py中的这一步:

loss = model.train(sess, uij, lr)

很显然,要看核心的模型构建部分了,Model类太长,我拆开来讲。

先看变量设置部分,其中cate_list

self.u = tf.placeholder(tf.int32, [None,]) # [B] user_id
self.i = tf.placeholder(tf.int32, [None,]) # [B] pos_item_id
self.j = tf.placeholder(tf.int32, [None,]) # [B] neg_item_id   【这个变量只用在测试集中】
self.y = tf.placeholder(tf.float32, [None,]) # [B] label
self.hist_i = tf.placeholder(tf.int32, [None, None]) # [B, T]  hist_padding,T是padding长度
self.sl = tf.placeholder(tf.int32, [None,]) # [B]  sample len list 每个样本真实的hist长度
self.lr = tf.placeholder(tf.float64, []) # learning rate
hidden_units = 128

"构建user, item的embedding lookup table "
user_emb_w = tf.get_variable("user_emb_w", [user_count, hidden_units])
item_emb_w = tf.get_variable("item_emb_w", [item_count, hidden_units // 2]) 
item_b = tf.get_variable("item_b", [item_count],
                         initializer=tf.constant_initializer(0.0))

cate_emb_w = tf.get_variable("cate_emb_w", [cate_count, hidden_units // 2]) 
cate_list = tf.convert_to_tensor(cate_list, dtype=tf.int64)

接着就是每个item的分类特征的拼接,这里只用到了itemid和cate类目,两个特征。很简单,直接concat。(真是万能的concat…)

注意id和cate两个特征维度是64,而用户特征(只有id一个)维度是128,最后用户和item各自维度是一样的。

u_emb = tf.nn.embedding_lookup(user_emb_w, self.u)

ic = tf.gather(cate_list, self.i) # item category [B]
i_emb = tf.concat(values = [
    tf.nn.embedding_lookup(item_emb_w, self.i),
    tf.nn.embedding_lookup(cate_emb_w, ic),
], axis=1)  #  [B,64] concat [B,64] = [B,128]
i_b = tf.gather(item_b, self.i)

接着就到了区别于DIN的用户行为序列特征的抽取部分,特征concat后,item list对位相加取平均,标准pooling方法,没做过的可以学习一下。

hc = tf.gather(cate_list, self.hist_i) #  [B,T]
h_emb = tf.concat([
    tf.nn.embedding_lookup(item_emb_w, self.hist_i),
    tf.nn.embedding_lookup(cate_emb_w, hc),
], axis=2) # [B,T,64] concat [B,T,64] = [B,T,192]
#-- sum pooling -------
mask = tf.sequence_mask(self.sl, tf.shape(h_emb)[1], dtype=tf.float32) # [B, T],T中True的范围就是每个用户实际行为序列的长度
mask = tf.expand_dims(mask, -1) # [B, T, 1] 增加一维,用1填充
mask = tf.tile(mask, [1, 1, tf.shape(h_emb)[2]]) # 复制, [B, T, H] ; H = 128
h_emb *= mask # [B, T, H]  到这里, padding的item都变成0; False和任何数相乘结果都是0
hist = h_emb
hist = tf.reduce_sum(hist, 1)  #[B,H]
hist = tf.div(hist, tf.cast(tf.tile(tf.expand_dims(self.sl,1), [1,128]), tf.float32))

这样,就得到了每个用户对应的行为序列embedding,也就是hist变量,这个特征也就抽象出来了。剩下的就是每一个field特征,concat后全部输入MLP,最终得到一个预测值。

有几个点还要说明一下,源码中GAUC和AUC只在评估测试集时用到,而为了计算GAUC,就要知道一个用户内正负样本的排序,所以才有了self.j这个变量,以及对应的测试集样本构造。一个用户有正负样本各一个。

刚开始DIN源码,没注意训练集和测试集构建方法是不同的,导致我绕了很大一圈,一直在纠结代码结构,所以看源码,掌握核心思想那部分的编程就好,毕竟数据是活的,思想是固定的。

今天是因为疫情最后一天在家办公了,整理一下在家期间看的东西,慢慢发上来。

最后希望上班后老大能善待我们呀…

码字不易,随手点赞,感谢感谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值