阿里这篇文章,说白了就是如何将用户的行为序列抽象出一个特征,这里我称之为行为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
核心思想就是,将用户行为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源码,没注意训练集和测试集构建方法是不同的,导致我绕了很大一圈,一直在纠结代码结构,所以看源码,掌握核心思想那部分的编程就好,毕竟数据是活的,思想是固定的。
今天是因为疫情最后一天在家办公了,整理一下在家期间看的东西,慢慢发上来。
最后希望上班后老大能善待我们呀…
码字不易,随手点赞,感谢感谢。