DMR (Deep Match to Rank) 网络介绍与源码浅析
文章目录
零. 前言 (与正文无关, 请忽略)
对自己之前分析过的文章做一个简单的总结:
- 机器学习基础: LR / LibFM
- 特征交叉: DCN / PNN / DeepMCP / xDeepFM / FiBiNet / AFM
- 用户行为建模: DSIN / DMR / DMIN / DIN
- 多任务建模: MMOE
- Graph 建模: GraphSage / GAT
广而告之
可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中. CSDN 上的阅读体验会更好一些, 地址是: https://blog.csdn.net/eric_1993/category_9900024.html
一. 文章信息
- 论文标题: Deep Match to Rank Model for Personalized Click-Through Rate Prediction
- 论文地址: https://ojs.aaai.org/index.php/AAAI/article/view/5346/5202
- 代码地址: https://github.com/lvze92/DMR
- 发表时间: AAAI 2020
- 论文作者: Ze Lyu, Yu Dong, Chengfu Huo, Weijun Ren
- 作者单位: Alibaba
核心观点
本文提出 DMR (Deep Match to Rank) 算法, 将 Matching 和 Ranking 网络进行联合训练. DMR 主要由 Item-to-item 子网络和 User-to-Item 子网络构成, 两个子网络均尝试捕获 user 和 target item 之间的相关性. 其中 Item-to-Item 子网络采用 Attention 机制计算 target item 和用户历史行为之间的相关性, 而 User-to-Item 子网络通过内积来建模用户和 target item 之间的相关性. 注意在训练 User-to-Item 子网络时, 只靠 click label 提供监督信息是不够的, 因此引入了辅助 Loss, 使用用户历史行为作为 label 来帮助 User-to-Item 网络的学习.
核心观点解读
DMR (Deep Match to Rank) 模型结构如下图:
其主要由 User-to-Item 以及 Item-to-Item 两个子网络组成, 两个子网络均尝试捕获 user 和 target item 之间的相关性.
User-to-Item 子网络
在 User-to-Item 子网络中, 采用 Attention 机制结合 position encoding 来学习用户侧表征, 之后再与 target item 的 embedding 进行内积.
这里需要注意 target item 在 User-to-Item 和 Item-to-Item 子网络中均有出现, 但是它们并不共享 embedding. 其中 Item-to-Item 网络中 target item 对应的 embedding 被称为 input representation, 而 User-to-Item 网络中 target item 对应的 embedding 被称为 output representation.
User-to-Item 子网络中, Attention 权重系数的生成方式为:
a t = z ⊤ tanh ( W p p t + W e e t + b ) α t = exp ( a t ) ∑ i = 1 T exp ( a i ) \begin{aligned} a_{t} &=\boldsymbol{z}^{\top} \tanh \left(\boldsymbol{W}_{p} \boldsymbol{p}_{t}+\boldsymbol{W}_{e} \boldsymbol{e}_{t}+\boldsymbol{b}\right) \\ \alpha_{t} &=\frac{\exp \left(a_{t}\right)}{\sum_{i=1}^{T} \exp \left(a_{i}\right)} \end{aligned} atαt=z⊤tanh(Wppt+Weet+b)=∑i=1Texp(ai)exp(at)
其中 p t ∈ R d p \boldsymbol{p}_{t}\in\mathbb{R}^{d_p} pt∈Rdp 为第 t t t 个 position embedding, e t ∈ R d e \boldsymbol{e}_{t} \in \mathbb{R}^{d_{e}} et∈Rde 为第 t t t 个用户行为对应的特征向量, z ∈ R d h \boldsymbol{z} \in \mathbb{R}^{d_{h}} z∈Rdh 为可学习的参数. 最终的用户表达是对历史行为进行加权求和获得:
u = g ( ∑ t = 1 T ( α t e t ) ) = g ( ∑ t = 1 T ( h t ) ) \boldsymbol{u}=g\left(\sum_{t=1}^{T}\left(\alpha_{t} \boldsymbol{e}_{t}\right)\right)=g\left(\sum_{t=1}^{T}\left(\boldsymbol{h}_{t}\right)\right) u=g(t=1∑T(αtet))=g(t=1∑T(ht))
其中 u ∈ R d v \boldsymbol{u} \in \mathbb{R}^{d_{v}} u∈Rdv 为用户 embedding, g ( ⋅ ) g(\cdot) g(⋅) 为非线性变换, 其输入维度大小为 d e d_e de, 输出维度大小为 d v d_v dv.
在 User-to-Item 网络中, target item 对应的 embedding 为 output representation, 使用 v ′ \boldsymbol{v}^\prime v′ 表示, 它与用户 embedding 的内积可以描述 user 和 item 之间的相关性:
r = u ⊤ v ′ r = \boldsymbol{u}^{\top} \boldsymbol{v}^{\prime} r=u⊤v′
若 r r r 值越大, 越能体现 user emb 和 item emb 之间的相关性, 这样能促进 prediction net 对 ctr 的预估. 但是只靠 click label 来提供监督信息是不够的, 因此引入了辅助网络, 使用用户行为作为 label 来帮助 u2i 网络的学习. 辅助网络的目标是使用前 T − 1 T - 1 T−1 个行为来预测第 T T T 个行为, 那么该问题就转化为了一个 multi-classification 的分类问题. 前 T − 1 T - 1 T−1 个行为 sum pooling 的结果表示为 u T − 1 ∈ R d v \boldsymbol{u}_{T-1} \in \mathbb{R}^{d_{v}} uT−1∈Rdv, 那么用户下一次要点击 item j j j 的概率为:
p j = exp ( u T − 1 ⊤ v j ′ ) ∑ i = 1 K exp ( u T − 1 ⊤ v i ′ ) p_{j}=\frac{\exp \left(\boldsymbol{u}_{T-1}^{\top} \boldsymbol{v}_{j}^{\prime}\right)}{\sum_{i=1}^{K} \exp \left(\boldsymbol{u}_{T-1}^{\top} \boldsymbol{v}_{i}^{\prime}\right)} pj=∑i=1Kexp(uT−1⊤vi′)exp(uT−1⊤vj′)
Loss 函数可以设计为:
L a u x = − 1 N ∑ i = 1 N ∑ j = 1 K y j i log ( p j i ) L_{aux}=-\frac{1}{N} \sum_{i=1}^{N} \sum_{j=1}^{K} y_{j}^{i} \log \left(p_{j}^{i}\right) Laux=−N1i=1∑Nj=1∑Kyjilog(pji)
但显然这是一个计算成本巨大的任务, 因此作者借鉴 Word2Vec 中提出的负采样技术, 重新设计了辅助网络的 Loss 为:
L N S = − 1 N ∑ i = 1 N ( log ( σ ( u T − 1 ⊤ v o ′ ) ) + ∑ j = 1 k log ( σ ( − u T − 1 ⊤ v j ′ ) ) ) \begin{aligned} L_{N S}=&-\frac{1}{N} \sum_{i=1}^{N}\left(\log \left(\sigma\left(\boldsymbol{u}_{T-1}^{\top} \boldsymbol{v}_{o}^{\prime}\right)\right)\right.\\ &\left.+\sum_{j=1}^{k} \log \left(\sigma\left(-\boldsymbol{u}_{T-1}^{\top} \boldsymbol{v}_{j}^{\prime}\right)\right)\right) \end{aligned} LNS=−N1i=1∑N(log(σ(uT−1⊤vo′))+j=1∑klog(σ(−uT−1⊤vj′)))
Item-to-Item 子网络
Item-to-Item 子网络以一种不太直接的方式来建模 user 和 item 之间的相关性, 它采用 Attention 机制学习 target item 与用户行为的相关性, 公式如下:
a ^ t = z ^ ⊤ tanh ( W ^ c e c + W ^ p p t + W ^ e e t + b ^ ) \hat{a}_{t}=\hat{\boldsymbol{z}}^{\top} \tanh \left(\hat{\boldsymbol{W}}_{c} \boldsymbol{e}_{c}+\hat{\boldsymbol{W}}_{p} \boldsymbol{p}_{t}+\hat{\boldsymbol{W}}_{e} \boldsymbol{e}_{t}+\hat{\boldsymbol{b}}\right) a^t=z^⊤tanh(W^cec+W^ppt+W^eet+b^)
其中 e c ∈ R d e \boldsymbol{e}_{c} \in \mathbb{R}^{d_{e}} ec∈Rde 为 target item 对应的 embedding, 注意它和 User-to-Item 子网络中的 embedding 不是共享的. 其他参数不再介绍, 一目了然.
另外, 在 Item-to-Item 网络中, 用户与 item 之间的相似性可以使用 target 与用户历史行为的相似程度之和来表示, 即:
r ^ = ∑ t = 1 T a ^ t \hat{r}=\sum_{t=1}^{T} \hat{a}_{t} r^=t=1∑Ta^t
而用户侧的 embedding 则采用对历史行为的加权求和表示:
α ^ t = exp ( a ^ t ) ∑ i = 1 T exp ( a ^ i ) u ^ = ∑ t = 1 T ( α ^ t e ^ t ) \begin{aligned} \hat{\alpha}_{t} &=\frac{\exp \left(\hat{a}_{t}\right)}{\sum_{i=1}^{T} \exp \left(\hat{a}_{i}\right)} \\ \hat{\boldsymbol{u}} &=\sum_{t=1}^{T}\left(\hat{\alpha}_{t} \hat{\boldsymbol{e}}_{t}\right) \end{aligned} α^tu^=∑i=1Texp(a^i)exp(a^t)=t=1∑T(α^te^t)
最后输入到 MLP 的 embedding 为:
c = [ x p , x t , x c , u ^ , r , r ^ ] \boldsymbol{c}=\left[\boldsymbol{x}_{p}, \boldsymbol{x}_{t}, \boldsymbol{x}_{c}, \hat{\boldsymbol{u}}, r, \hat{r}\right] c=[xp,xt,xc,u^,r,r^]
依次表示 User Profile, Target Item, Context, Item-to-Item 网络中的用户 embedding, User-to-Item 网络中用户和 item 的相似性, Item-to-Item 网络中用户和 item 的相似性
源码分析
源码位于: https://github.com/lvze92/DMR; 在下面分析代码时, 从名字就能看出含义的变量不多介绍.
数据集来自 https://tianchi.aliyun.com/dataset/dataDetail?dataId=56
其中用户历史行为使用:
self.btag_his = tf.cast(self.feature_ph[:, 0:50], tf.int32)
self.cate_his = tf.cast(self.feature_ph[:, 50:100], tf.int32)
self.brand_his = tf.cast(self.feature_ph[:, 100:150], tf.int32)
self.mask = tf.cast(self.feature_ph[:, 150:200], tf.int32)
self.match_mask = tf.cast(self.feature_ph[:, 200:250], tf.int32)
btag
表示用户行为类型, 包括浏览/加购/购买/喜欢.
行为中采用类目以及品牌, 行为个数截断为 50 个, 不足 50 个则采用补 0 操作, 设置 mask
来确定真实行为. 行为序列按照由远及近的顺序进行排序, 比如 mask
可能的值是 [0, 0, ..., 1, 1, 1]
, mask[-1]
表示最近的行为对应的 mask. 在大多数情况下 self.match_mask
和 self.mask
的值是相同的, 但存在一小部分样本这两个值有 diff. 暂时没想明白 match_mask
和 mask
的区别 (数据预处理代码没有提供), 因此假设 self.match_mask
与 self.mask
完全相同, 忽略细节, 不影响对核心算法的理解.
Position Embedding 的生成
代码位于: https://github.com/lvze92/DMR/blob/master/model.py
## 用于 Item-to-Item 网络
self.position_his = tf.range(50)
self.position_embeddings_var = tf.get_variable("position_embeddings_var", [50, other_embedding_size])
self.position_his_eb = tf.nn.embedding_lookup(self.position_embeddings_var, self.position_his) # [T, E']
self.position_his_eb = tf.tile(self.position_his_eb, [tf.shape(self.mid)[0], 1]) # [B*T, E']
self.position_his_eb = tf.reshape(self.position_his_eb, [tf.shape(self.mid)[0], -1, self.position_his_eb.get_shape().as_list()[1]]) # [B,T,E']
## 用于 User-to-Item 网络
self.dm_position_his = tf.range(50)
self.dm_position_embeddings_var = tf.get_variable("dm_position_embeddings_var", [50, other_embedding_size])
self.dm_position_his_eb = tf.nn.embedding_lookup(self.dm_position_embeddings_var, self.dm_position_his) # [T, E']
self.dm_position_his_eb = tf.tile(self.dm_position_his_eb, [tf.shape(self.mid)[0], 1]) # [B*T, E']
self.dm_position_his_eb = tf.reshape(self.dm_position_his_eb, [tf.shape(self.mid)[0], -1, self.dm_position_his_eb.get_shape().as_list()[1]]) # [B,T,E']
## Position Embedding 最后要和用户行为类型 (btag 表示浏览/加购/购买/喜欢等行为)对应的 embedding 进行拼接
self.position_his_eb = tf.concat([self.position_his_eb, self.btag_his_batch_embedded], -1) ## 用于 Item-to-Item 网络, 大小为 [B, T, E3]
self.dm_position_his_eb = tf.concat([self.dm_position_his_eb, self.dm_btag_his_batch_embedded], -1) ## 用于 User-to-Item 网络, 大小为 [B, T, E3]
注意 Position Embedding 最后和用户行为类型对应的 embedding 进行拼接, 其中 self.position_his_eb
用于 Item-to-Item 网络, 而 self.dm_position_his_eb
用于 User-to-Item 网络.
User-to-Item 网络
在 https://github.com/lvze92/DMR/blob/master/model.py 中, 调用:
# User-to-Item Network
with tf.name_scope('u2i_net'):
"""
+ dm_item_vectors 为用户行为 embedding layer, 大小设置为 [K, E1]
+ dm_item_biases 为 bias, 大小设置为 K
+ 注意 K 是所有商品空间的大小
+ deep_match() 函数中传入的参数含义:
该函数参数比较多, 但总结下来可以认为: 前四个参数和求 Attention 系数有关, 而后面 5 个参数跟利用负采样来计算辅助 loss 有关.
- self.item_his_eb 为用户历史行为 emb, 大小为 [B, T, E2]
- self.dm_position_his_eb 为 position embedding, 大小为 [B, T, E3]
- self.mask 和 self.match_mask 大小相等, 均为 [B, T]
- self.cate_his: 用户历史行为, 这里只使用类目特征, 没有使用 brand 特征, 大小为 [B, T], 而上面 self.item_his_eb 则是对类目 emb 和 brand emb 进行了拼接;
之所以只用 cate 而不同时使用 brand, 猜测是进行负采样的时候, 传入的是 dm_item_vectors (含义是用户行为 embedding layers, 里面只包含 cate)
- main_embedding_size: item embedding size, 等于注释中的 E1
- dm_item_vectors: 商品 embedding 空间, 大小为 [K, E1], 用于负采样
- dm_item_biases: 商品 bias 空间, 大小为 [K]
- cate_size: 商品空间大小, 表示 K
+ deep_match() 函数有三个返回值:
- self.aux_loss: 表示辅助 loss
- dm_user_vector: User-to-Item 网络中最终的 user emb, 大小为 [B, E1]
- scores: Attention 权重系数, 大小为 [B, 1, T]
+ self.cate_id: target item 对应的 id, 大小为 [B]
+ dm_item_vec: target item 对应的 emb, 大小为 [B, E1]
+ rel_u2i: 用户 emb 和 target item 对应的 emb 做内积, 得到大小为 [B, 1] 的结果
"""
dm_item_vectors = tf.get_variable("dm_item_vectors", [cate_size, main_embedding_size])
dm_item_biases = tf.get_variable('dm_item_biases', [cate_size], initializer=tf.zeros_initializer(), trainable=False)
# Auxiliary Match Network
self.aux_loss, dm_user_vector, scores = deep_match(self.item_his_eb, self.dm_position_his_eb, self.mask, tf.cast(self.match_mask, tf.float32), self.cate_his, main_embedding_size, dm_item_vectors, dm_item_biases, cate_size)
self.aux_loss *= 0.1
dm_item_vec = tf.nn.embedding_lookup(dm_item_vectors, self.cate_id)
rel_u2i = tf.reduce_sum(dm_user_vector * dm_item_vec, axis=-1, keep_dims=True)
self.rel_u2i = rel_u2i
可以看到, deep_match()
函数返回用户 emb dm_user_vector
后, 和 target item 向量 dm_item_vec
进行内积, 得到相关性 rel_u2i
. 再来看 deep_match
函数的定义, 代码位于: https://github.com/lvze92/DMR/blob/master/utils.py. 分成两个部分阅读, 首先是 position emb 作为 query
def deep_match(item_his_eb, context_his_eb, mask, match_mask, mid_his_batch, EMBEDDING_DIM, item_vectors, item_biases, n_mid):
"""
参数介绍: (前四个参数和求 Attention 系数有关, 而后面 5 个参数跟利用负采样来计算辅助 loss 有关.)
+ item_hist_eb: 用户历史行为 emb, 大小为 [B, T, E2]
+ context_his_eb: position embedding, 大小为 [B, T, E3]
+ mask 和 match_mask: 大小相等, 均为 [B, T]
+ mid_his_batch: (mid 表示 m id, 哈哈) 用户历史行为, 和 item_hist_eb 的区别是, 这里只使用类目特征, 没有使用 brand 特征, 大小为 [B, T]
之所以只用 cate 而不同时使用 brand, 猜测是进行负采样的时候, 传入的 item_vectors 中只包含了 cate 特征 (没包含 brand 特征)
+ EMBEDDING_DIM: item embedding size, 等于注释中的 E1 (看上面一大段代码中的注释)
+ item_vectors: 商品 embedding 空间, 大小为 [K, E1], 用于负采样
+ 商品 bias 空间, 大小为 [K]
+ cate_size: 商品空间大小, 表示 K
"""
## 1. 首先 context_his_eb 表示 position emb, 它用作 query, 和用户行为一起做非线性变换来生成 Attention 权重系数 scores
query = context_his_eb ## [B, T, E3]
query = tf.layers.dense(query, item_his_eb.get_shape().as_list()[-1], activation=None, name='dm_align') ## [B, T, E2]
query = prelu(query, scope='dm_prelu') ## [B, T, E2]
inputs = tf.concat([query, item_his_eb, query-item_his_eb, query*item_his_eb], axis=-1) ## [B, T, E4]
att_layer1 = tf.layers.dense(inputs, 80, activation=tf.nn.sigmoid, name='dm_att_1')
att_layer2 = tf.layers.dense(att_layer1, 40, activation=tf.nn.sigmoid, name='dm_att_2')
att_layer3 = tf.layers.dense(att_layer2, 1, activation=None, name='dm_att_3') ## [B, T, 1]
scores = tf.transpose(att_layer3, [0, 2, 1]) ## [B, 1, T], 生成权重系数
## 结合 mask, 因为后面要使用 softmax 来对权重系数进行归一化, 只有那些真实的历史行为会被用于计算, 而通过补 0 操作得到的行为不用于计算,
## 因此通过设置一个极大的负值 -2**32+1, 来达到目的, 这样 softmax 的结果就约等于 0
bool_mask = tf.equal(mask, tf.ones_like(mask)) # [B, T]
key_masks = tf.expand_dims(bool_mask, 1) # [B, 1, T]
paddings = tf.ones_like(scores) * (-2 ** 32 + 1)
scores = tf.where(key_masks, scores, paddings)
"""
这一步是比较细节的操作, 从论文的公式中不太能看出来. 假设 scores 为: (大小为 [B, 1, T])
[[[1, 2, ...]],
[[3, 4, ...]]]
经过 tile + reshape 等操作后, 得到的结果是: (大小为 [B, T, T])
[
[
[1, 2, ...],
[1, 2, ...],
....
],
[
[3, 4, ...],
[3, 4, ...],
....
],
]
tril 为下三角矩阵:
[
[
[1, 0, 0, ...],
[1, 1, 0, ...],
[1, 1, 1, ...]
],
[
[1, 0, 0, ...],
[1, 1, 0, ...],
[1, 1, 1, ...]
],
]
scores_tile 经过 softmax 对 Attention 系数进行归一化, 最后和 item_his_eb 进行矩阵相乘, 得到大小为 [B, T, E2] 的输出 att_dm_item_his_eb
使用下三角函数的目的是, 每一次只计算前 i 个行为的加权求和结果. 比如对第 T 个行为, 它只和 T-1, T-2, ..., 0 个行为来计算 Attention 的结果.
同时, 下一段将要分析的代码中, user_vector = dnn_layer1[:, -1, :], 其表示最后输出的 user emb, 但它是从 dnn_layer1 中取最后一个结果.
"""
scores_tile = tf.tile(tf.reduce_sum(scores, axis=1), [1, tf.shape(scores)[-1]]) # [B, T*T]
scores_tile = tf.reshape(scores_tile, [-1, tf.shape(scores)[-1], tf.shape(scores)[-1]]) # [B, T, T]
diag_vals = tf.ones_like(scores_tile) # [B, T, T]
# tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense()
tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()
paddings = tf.ones_like(tril) * (-2 ** 32 + 1)
scores_tile = tf.where(tf.equal(tril, 0), paddings, scores_tile) # [B, T, T]
scores_tile = tf.nn.softmax(scores_tile) # [B, T, T]
att_dm_item_his_eb = tf.matmul(scores_tile, item_his_eb) # [B, T, E2]
上面代码阐述了如何利用 position emb 作为 query 来进行 Attention, 下面介绍如何产生 user emb 以及如何做负采样来进行辅助 Loss 的计算, 代码如下:
## Attention 后的结果还要进行非线性变换
dnn_layer1 = tf.layers.dense(att_dm_item_his_eb, EMBEDDING_DIM, activation=None, name='dm_fcn_1') ## [B, T, E1]
dnn_layer1 = prelu(dnn_layer1, 'dm_fcn_1') # [B, T, E1]
## 从 dnn_layer1 中选取最后一个结果作为用户 emb, 它已经对用户的 T 个行为进行了加权求和, 即
user_vector = dnn_layer1[:, -1, :] ## [B, E1]
## 从 dnn_layer1 中选取倒数第二个结果作为 u_{T - 1} 作为辅助 Loss 的正样本 (看模型结构图中 Auxiliary Match Network Loss 中那部分)
## 这里的 match_mask 我没弄明白, 不过大多数情况下, 它和 mask 的值是一样的, 不过它的数值要么是 1, 要么是 0
user_vector2 = dnn_layer1[:, -2, :] * tf.reshape(match_mask, [-1, tf.shape(match_mask)[1], 1])[:, -2, :]
## 调用 tf.nn.sampled_softmax_loss 以及 tf.nn.learned_unigram_candidate_sampler 来进行辅助 Loss 的计算
## 其中 mid_his_batch[:, -1] 表示 e_{T}, 即最后一个行为 (看模型结构图, 图中的 e_{T} 就是 md_his_batch[:, -1], 同时它也是
## Auxiliary Match Network Loss 图中的 v_{o}), 用作辅助 Loss 的正样本
## 而这里还要采用 2000 个行为作为负样本, user_vector2 是辅助 Loss 的 u_{T - 1}.
num_sampled = 2000
loss = tf.reduce_mean(tf.nn.sampled_softmax_loss(weights=item_vectors,
biases=item_biases,
labels=tf.cast(tf.reshape(mid_his_batch[:, -1], [-1, 1]), tf.int64),
inputs=user_vector2,
num_sampled=num_sampled,
num_classes=n_mid,
sampled_values=tf.nn.learned_unigram_candidate_sampler(tf.cast(tf.reshape(mid_his_batch[:, -1], [-1, 1]), tf.int64), 1, num_sampled, True, n_mid)
))
return loss, user_vector, scores
最后 deep_match
函数返回辅助 loss, 用户 emb, 以及 Attention 权重系数 scores.
Item-to-Item 网络
在 https://github.com/lvze92/DMR/blob/master/model.py 中, 调用:
# Item-to-Item Network
with tf.name_scope('i2i_net'):
"""
+ self.item_eb: target item 对应的 emb, 大小为 [B, E2], 拼接了类目和 brand embedding
+ self.item_his_eb: 用户历史行为 emb, 大小为 [B, T, E2]
+ self.position_his_eb: position embedding, 大小为 [B, T, E3]
+ self.mask: 大小为 [B, T]
+ att_outputs: target emb 与 position emb 作为 query, 与历史行为共同用于 Attention 系数的生成, 最后使用 Attention 系数对用户行为进行加权求和,
得到大小为 [B, E2] 的向量
+ alphas: 归一化之后的 Attention 系数, 大小为 [B, 1, T]
+ scores_unnorm: 归一化之前的 Attention 系数, 大小为 [B, 1, T]
+ rel_i2i: 表示 user和 item 相关性的结果, 对 Attention 系数进行累加 (文章公式说了), 大小为 [B, 1]
"""
att_outputs, alphas, scores_unnorm = dmr_fcn_attention(self.item_eb, self.item_his_eb, self.position_his_eb, self.mask)
rel_i2i = tf.expand_dims(tf.reduce_sum(scores_unnorm, [1, 2]), -1)
self.rel_i2i = rel_i2i
self.scores = tf.reduce_sum(alphas, 1)
下面看 dmr_fcn_attention()
函数, 定义在: https://github.com/lvze92/DMR/blob/master/utils.py,
def dmr_fcn_attention(item_eb, item_his_eb, context_his_eb, mask, mode='SUM'):
"""
参数介绍:
+ item_eb: target item 对应的 emb, 大小为 [B, E2], 拼接了类目和 brand embedding
+ item_his_eb: 用户历史行为 emb, 大小为 [B, T, E2]
+ context_his_eb: position embedding, 大小为 [B, T, E3]
+ mask: 大小为 [B, T]
"""
mask = tf.equal(mask, tf.ones_like(mask))
item_eb_tile = tf.tile(item_eb, [1, tf.shape(mask)[1]]) # [B, T*E2]
item_eb_tile = tf.reshape(item_eb_tile, [-1, tf.shape(mask)[1], item_eb.shape[-1]]) # [B, T, E2]
## 当 position emb 不为空时, 它和目标商品的 embedding 进行拼接作为 query, 大小为 [B, T, E2+E3]
if context_his_eb is None:
query = item_eb_tile
else:
query = tf.concat([item_eb_tile, context_his_eb], axis=-1)
## query 经过非线性变换, 变换后的大小为 [B, T, E2]
query = tf.layers.dense(query, item_his_eb.get_shape().as_list()[-1], activation=None, name='dmr_align') ## [B, T, E2]
query = prelu(query, scope='dmr_prelu')
## query 和用户历史行为用于 Attention 系数的计算
dmr_all = tf.concat([query, item_his_eb, query-item_his_eb, query*item_his_eb], axis=-1)
att_layer_1 = tf.layers.dense(dmr_all, 80, activation=tf.nn.sigmoid, name='tg_att_1')
att_layer_2 = tf.layers.dense(att_layer_1, 40, activation=tf.nn.sigmoid, name='tg_att_2')
att_layer_3 = tf.layers.dense(att_layer_2, 1, activation=None, name='tg_att_3') # [B, T, 1]
att_layer_3 = tf.reshape(att_layer_3, [-1, 1, tf.shape(item_his_eb)[1]]) # [B, 1, T]
scores = att_layer_3
## 设置 Mask
key_masks = tf.expand_dims(mask, 1) # [B, 1, T]
paddings = tf.ones_like(scores) * (-2 ** 32 + 1)
paddings_no_softmax = tf.zeros_like(scores)
scores = tf.where(key_masks, scores, paddings) # [B, 1, T]
scores_no_softmax = tf.where(key_masks, scores, paddings_no_softmax)
## 得到归一化后的 Attention 系数, 大小为 [B, 1, T]
scores = tf.nn.softmax(scores)
if mode == 'SUM':
## 使用 Attention 系数对历史行为进行加权求和
output = tf.matmul(scores, item_his_eb) # [B, 1, E2]
output = tf.reduce_sum(output, axis=1) # [B, E2]
else:
## 相当于不进行加权求和
scores = tf.reshape(scores, [-1, tf.shape(item_his_eb)[1]]) ## [B, T]
output = item_his_eb * tf.expand_dims(scores, -1) ## [B, T, E2]
output = tf.reshape(output, tf.shape(item_his_eb)) ## [B, T, E2]
return output, scores, scores_no_softmax
MLP 网络
代码位于: https://github.com/lvze92/DMR/blob/master/model.py, 将各种 embedding 进行拼接 (包括 User-to-Item & Item-to-Item 网络的输出以及 Attention 系数的累加, User Profile 等):
"""
+ user_feat: 用户侧特征, 大小为 [B, C1]
+ item_feat: 目标商品侧特征, 大小为 [B, C2]
+ context_feat: 上下文特征, 大小为 [B, C3]
+ item_his_eb_sum: 历史行为特征进行 sum, 大小为 [B, E2]
+ item_eb * item_hist_eb_sum: 目标商品和历史行为的交叉特征, 大小为 [B, E2]
+ rel_u2i: User-to-Item 网络的输出, 用户 emb 和 item emb 的内积, 大小为 [B, 1]
+ rel_i2i: Item-to-Item 网络的输出, 表示目标商品与用户的相关性, 大小为 [B, 1]
+ att_outputs: Item-to-Item 网络中目标商品和历史行为的 Attention 结果, 大小为 [B, E2]
"""
inp = tf.concat([self.user_feat, self.item_feat, self.context_feat, self.item_his_eb_sum,self.item_eb * self.item_his_eb_sum, rel_u2i, rel_i2i, att_outputs], -1)
self.build_fcn_net(inp) ## MLP 的输出
总结
其实网络结构不是很复杂, 但写博客时发现要形式化描述, 得花很多时间… 深感没有必要, 谨记, 以后写博客尽量从简, 快速分析重点. 将论文中的内容重新说一遍实在没有必要.