本次要分享的是 推荐系统/CTR 领域的论文,论文链接Deep Interest Network for Click-Through Rate Prediction,参考的代码链接 DeepInterestNetwork,该论文所提方法比较简单,但是有一些工程细节值得分析下。
文章目录
论文动机及创新点
- 目前基于深度学习的CTR模型,大多都是 embedding&MLP 的方式建模,具体而言,就是将高维稀疏的特征向量压缩成低微稠密向量,并且其长度固定,在此基础上再接个多层感知机来学习特征之间的非线性关系。但是这种将其转换成长度固定向量的方式,较难捕捉用户的多峰兴趣。
- 在电商场景中,用户的兴趣往往是多峰的,即用户可能同时在多个不同商品上都饶有兴趣,而CTR的任务就是从用户历史行为来建模用户兴趣。但是目前常用的将高维稀疏向量压缩成低维的长度固定向量的方式,限制了embedding&MLP 对用户兴趣表征能力,对此有两种解决方式
- 大幅增加固定长度的向量的维度,但是这样也会大幅增加模型需要学习的参数
- 没必要将用户历史特征压缩成 固定长度的向量,因为用户当前的兴趣只和历史行为中某个或者某几个记录有关而已(典型的Attention思想)
- 本论文提出了 一种新的模型:深度兴趣网络(DeepInterest Network,DIN),该模型通过设计局部激活单元(local activation unit),基于用户对某一广告的历史行为,自适应的学习用户兴趣表示,并且提出了两种训练技巧
- mini-batch aware regularizer:大大降低了正则化时的计算量
- data adaptive activation function(Dice): 根据输入数据的分布自适应地调整校正点
模型
- User Profie Feature:用户画像特征
- Goods:商品各种ID特征(例如item_id, cate_id,shop_id),也可以是经过人工特征工程提取的商品特征
- Candidate Ad:同上,候选广告ID特征,也可以是经过人工特征工程提取的广告特征
上图中喂给Embedding Layer的特征既可以是各种ID特征(这样就无需特征工程了),也可以是经由特征工程提取的一些特征。
Activation units
由上图其实也可以看出,这里面用到了NLP里常见的Attention机制,其中:
- Query:candidate Ad
- Key:User Behaviors 中的 Goods 序列
- Value:User Behaviors 中的 Goods 序列
Attention机制数学公式:
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
Attention(Q,K,V)=softmax\left ( \frac{QK^{T}}{\sqrt{d_k}} \right )V
Attention(Q,K,V)=softmax(dkQKT)V
大部分涉及到Attention机制,差不多都遵循上述的计算方式,不同的地方在于 分子中
Q
Q
Q 与
K
T
K^T
KT 是 具体是如何计算的。
值得一提的是,本论文作者在某平台说,他们这样做是自己从实践中摸索出来的,后来才发现和Attention思想不谋而合
那我们看看具体他们这个Activation units怎么做的
也就是Attention中分子是这样计算的:
F
C
(
D
i
c
e
(
[
K
,
Q
,
K
∗
Q
]
)
)
FC(Dice([K, Q, K*Q]))
FC(Dice([K,Q,K∗Q]))
- 表示点乘,实际代码实现里还加了个 Q-K
更具体的Attention计算如下
上市
υ
A
\upsilon_A
υA 表示
c
a
n
d
i
d
a
t
e
_
a
d
candidate\_ad
candidate_ad 的 embedding 向量,
e
i
e_i
ei 表示 用户历史有行为的商品embedding 向量,
a
(
.
)
a(.)
a(.) 表示Activation units 操作。
值得注意的是,论文讲的Attention,分子上并没有做Softmax,他们是这样解释的:如果一个用户,历史行为记录里,90%都是与clothes有关,10%与electronics有关,在推荐时有T-shirt 和 phone 两个候选产品,T-shirt 获取注意力值,理应远大于phone所获取的注意力,如果进行softmax操作,反而缩小了两者差距。
这种去掉softmax的操作真是很玄乎,以我往常的经验,效果好不好真不好说,作者放出的代码里却依然加上了softmax
Mini-batch Aware Regularization
传统的正则化,对所有参数进行正则,而在广告场景,模型训练时,输入的样本都比较稀疏高维,有着数以亿计的参数,直接应用传统的L1和L2正则化不太实际。由此提出了Mini-batch Aware Regularization,具体而言就是,在训练时,只更新每个mini_batch中出现的非零特征对应的参数,相比传统L2正则每次对全量参数更新的方式,节省了大量计算资源和时间。
再来更具体的看看:
上式中,
W
∈
R
D
×
K
W \in R ^{D \times K}
W∈RD×K,其中D表示向量的维度,K表示特征个数,
∣
∣
.
∣
∣
2
2
||.||_2^2
∣∣.∣∣22 表示L2正则化,
n
j
n_j
nj 表示在所有样本中特征j 非零的次数,
I
(
x
j
≠
0
)
I(x_j \neq 0)
I(xj=0) 表示当前样本的第j个特征值是否为0。
上式可以变换如下:
上式中B表示batch个数,
B
m
B_m
Bm 表示第m个batch,进一步变换如下:
上式中
α
m
j
=
m
a
x
(
x
,
y
)
∈
B
m
I
(
x
j
≠
0
)
\alpha_{mj} =max_{(x,y) \in B_m}I(x_j \neq 0)
αmj=max(x,y)∈BmI(xj=0) ,表示当前mini_batch 中是否至少有一个样本在特征j上不等于0。注意是约等于号。
那么在更新参数时:
论文中对这种正则化方式描述比较详细,该方法比较实用,不知为何,作者放出的代码里貌似没有这部分代码实现。
Data Adaptive Activation Function
论文中讲到PReLu激活函数,固定在0处矫正点,可能不适合网络中每层不同分布输入。因此提出了Dice激活函数
- p ( s ) = I ( s > 0 ) p(s)=I(s>0) p(s)=I(s>0)
- E(s):训练时为当前batch的均值,预测时为整个预测级的均值
- Var(s):训练时为当前batch的方差,预测时为整个预测级的方差
利用LSTM建模
因为这里用到了用户历史行为序列,很容易想到用传统的时序网络模型,例如LSTM,然后再配上Attention机制,岂不是更好? 但是论文中提到,这样做几乎没有提升,给出的解释是:”与NLP任务中受语法约束的文本不同,用户历史行为序列可能包含多个并发兴趣。对这些兴趣的快速跳跃和突然结束导致用户行为的序列数据看起来很嘈杂没有序列上规律。“ 这一点结论倒是很令我意外,读这篇论文之前我一直以为用LSTM效果肯定更好,能捕捉到用户在时序上兴趣变化规律。
实验评价指标
论文提到使用GAUC作为实验评价指标,简单回顾下这AUC指标的计算方式
I
(
P
p
,
P
n
)
=
{
1
,
P
p
>
P
n
0.5
,
P
p
=
P
n
0
,
P
p
<
P
n
I(P_p,P_n)=\left\{\begin{matrix} 1 ,& P_{p}>P_{n}\\ 0.5,& P_{p}=P_{n}\\ 0 ,& P_{p}<P_{n} \end{matrix}\right.
I(Pp,Pn)=⎩⎨⎧1,0.5,0,Pp>PnPp=PnPp<Pn
A U C = I ( P p , P n ) M ∗ N AUC=\frac{I(P_p,P_n)}{M*N} AUC=M∗NI(Pp,Pn)
上式为AUC的一种常用的计算方式,其中 P p , P p P_p,P_p Pp,Pp 分别表示模型对正样本预测该类、对负样本的预测概率。M、N分别表示正样本,负样本数量。
从AUC的定义其实可以得到,其本质就是 数据集中,模型对正样本的预测概率大于负样本的预测概率,符合这种条件的正负样本对数。
AUC指标虽然能评估一个分类模型的好坏,但是对于 推荐/CTR 领域,需要关注的很精细一些,需要关注到对每个用户的推荐效果。
上式中auc其实就是gauc,其中n表示用户个数,
A
U
C
i
AUC_i
AUCi 表示对第i个用户的auc值,
i
m
p
r
e
s
s
i
o
n
i
impression_i
impressioni 表示第i个用户点击/浏览的次数。
代码分析
看完论文以后,对于代码实现,我个人觉得有几个点需要关注如何实现的
- 因为论文中提到要从用户历史行为序列对用户兴趣建模,那么对于不同用户,历史行为序列长度可能不一致,代码中具体是如何padding的呢?
- Mini-batch Aware Regularization 具体是怎么实现的呢? 可惜这部分没放出来
数据预处理
train_set = []
test_set = []
for reviewerID, hist in reviews_df.groupby('reviewerID'):
pos_list = hist['asin'].tolist()
def gen_neg():
neg = pos_list[0]
while neg in pos_list:
neg = random.randint(0, item_count-1)
return neg
neg_list = [gen_neg() for i in range(len(pos_list))]
for i in range(1, len(pos_list)):
hist = pos_list[:i]
if i != len(pos_list) - 1:
train_set.append((reviewerID, hist, pos_list[i], 1))
train_set.append((reviewerID, hist, neg_list[i], 0))
else:
label = (pos_list[i], neg_list[i])
test_set.append((reviewerID, hist, label))
random.shuffle(train_set)
random.shuffle(test_set)
由上述代码可知,代码中随机采样非正类作为负样本。
- 对于训练集中某个用户,每次选择其历史序列记录里前i个记录作为样本中的记录序列,第i个记录作为候选ad,并且打上label。
- 对于测试集中某个用户,每个样本都会带上第i个正ad和负ad,以用作后面模型训练过程中的auc/gauc评估。不参与训练
class DataInput:
......................
def __iter__(self):
return self
def next(self):
if self.i == self.epoch_size:
raise StopIteration
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])
i.append(t[2])
y.append(t[3])
sl.append(len(t[1]))
max_sl = max(sl)
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)
关注上述代码里的next函数,该函数控制batch输出数据的逻辑。注意到里面用到sl来存储每个样本 历史序列(hist) 的长度,用max_sl记录每个batch里最大长度,然后用 hist_i=np.zeros初始化 hist,再往里面填充真是hist 商品ID,相当于用0对序列进行了填充,使其长度一致。
Activation units
def attention(queries, keys, keys_length):
'''
queries: [B, H]
keys: [B, T, H]
keys_length: [B]
'''
queries_hidden_units = queries.get_shape().as_list()[-1]
queries = tf.tile(queries, [1, tf.shape(keys)[1]])
queries = tf.reshape(queries, [-1, tf.shape(keys)[1], queries_hidden_units])
din_all = tf.concat([queries, keys, queries-keys, queries*keys], axis=-1)
d_layer_1_all = tf.layers.dense(din_all, 80, activation=tf.nn.sigmoid, name='f1_att', reuse=tf.AUTO_REUSE)
d_layer_2_all = tf.layers.dense(d_layer_1_all, 40, activation=tf.nn.sigmoid, name='f2_att', reuse=tf.AUTO_REUSE)
d_layer_3_all = tf.layers.dense(d_layer_2_all, 1, activation=None, name='f3_att', reuse=tf.AUTO_REUSE)
d_layer_3_all = tf.reshape(d_layer_3_all, [-1, 1, tf.shape(keys)[1]])
outputs = d_layer_3_all
# Mask
key_masks = tf.sequence_mask(keys_length, tf.shape(keys)[1]) # [B, T]
key_masks = tf.expand_dims(key_masks, 1) # [B, 1, T]
paddings = tf.ones_like(outputs) * (-2 ** 32 + 1)
outputs = tf.where(key_masks, outputs, paddings) # [B, 1, T]
# Scale
outputs = outputs / (keys.get_shape().as_list()[-1] ** 0.5)
# Activation
outputs = tf.nn.softmax(outputs) # [B, 1, T]
# Weighted sum
outputs = tf.matmul(outputs, keys) # [B, 1, H]
return outputs
上述代码里,通过对矩阵的tile操作,再直接进行矩阵乘法等实现Attention计算。
- query为候选ad,经过ID和cate_ID的embedding后,得到形状为[Batch_size, Hidden_size]的矩阵
- keys为用户历史行为商品序列,经过ID和cate_ID的embedding后,得到形状为[Batch_size, Time_length,Hidden_size]的矩阵
- keys_length: 记录当前batch内,每个样本的行为序列真实长度。
上述填充的0最终会取到第0号商品的emb,这样是否合理?
再关注下上述代码里的 Mask 部分
paddings = tf.ones_like(outputs) * (-2 ** 32 + 1)
outputs = tf.where(key_masks, outputs, paddings)
这里的操作,相当于把原本填充0的部分,在Attention计算后将其对应位置的Attention值置为一个非常非常小的值。后面再经过sigmoid函数后,这样填充部分对模型的影响很小了?
这一步做的很玄乎,不知道有没有更好的解释
后面还有一些auc,gauc的相关代码,利用测试集,计算得到模型对正负样本的预测概率,然后做差等,比较简单,这里就不详细分析了。
个人总结
- 这篇论文思想比较简单,核心亮点在于将NLP领域内的注意力机制用到推荐/CTR 领域而已
- 工程上讲的比较详细,值得好好看看
- 作者放出的代码与论文里挺多地方不一致,容易让人产生疑惑
- 该方法将用户历史序列记录直接放到一个列表里,并没有利用LSTM等时序模型建模,因为对模型学习来讲,用户的每个历史行为记录都是等价的,没有先后关系
- 对于该论文方法,你可以直接用各种ID作为输入,也可以使用经过人工特征工程提取后特征作为输入。
参考资料
- https://arxiv.org/abs/1706.06978
- https://github.com/zhougr1993/DeepInterestNetwork