torch.einsum
(Einstein summation convention,即爱因斯坦求和约定)可以写tensor运算和更高维度tensor写法更加简洁,如用torch.einsum("bij, bjk -> bik", batch_tensor_1, batch_tensor_2)
进行batch内矩阵乘法运算。seq
进行sofjtmax
归一化,即使得seq
维度的元素之和为1。因为对于固定序列中不存在的位置负无穷mask掉了,所以缺失的位置经过softmax
归一化后的数值为0。 Softmax ( x i ) = exp ( x i ) ∑ j exp ( x j ) \operatorname{Softmax}\left(x_i\right)=\frac{\exp \left(x_i\right)}{\sum_j \exp \left(x_j\right)} Softmax(xi)=∑jexp(xj)exp(xi)第一部分和task5差不多,是同一篇论文Comirec-SA用self-attentive method代替之前DR多兴趣抽取层,
Comirec:Controllable Multi-Interest Framework for Recommendation
论文链接:https://arxiv.org/abs/2005.09347
Comirec是阿里发表在KDD 2020上的一篇工作,这篇论文对MIND多行为召回进行了扩展:
假设一个用户集合 u ∈ U u \in \mathcal{U} u∈U 和一个物品集合 i ∈ I i \in \mathcal{I} i∈I, 对于每一个用户, 定义用户序列 ( e 1 ( u ) , e 2 ( u ) , … , e n ( u ) ) \left(e_1^{(u)}, e_2^{(u)}, \ldots, e_n^{(u)}\right) (e1(u),e2(u),…,en(u)), 根据时间先后顺序排序,其中 e t ( u ) e_t^{(u)} et(u) 记录了第 t t t 个物品与用户交互。
Comirec流程:
self-attentive
模块),得到K个interest embeddings;K X N
个item。K X N
个item送入该模块得到N个item(选内积分数最高的N个,和MIND做法一致): f ( u , i ) = max 1 ≤ k ≤ K ( e i ⊤ v u ( k ) ) f(u, i)=\max _{1 \leq k \leq K}\left(\mathbf{e}_i^{\top} \mathbf{v}_u^{(k)}\right) f(u,i)=1≤k≤Kmax(ei⊤vu(k))和1.1说的一样,某用户经过多兴趣提取层后得到K个interest embeddings后,从中找出和target item i(即下面embedding为 e i e_i ei)最接近,即内积最大的一个interest embeeding,方法如下 v u = V u [ : , argmax ( V u ⊤ e i ) ] , \mathbf{v}_u=\mathbf{V}_u\left[:, \operatorname{argmax}\left(\mathbf{V}_u^{\top} \mathbf{e}_i\right)\right], vu=Vu[:,argmax(Vu⊤ei)],
和之前一样是结合负采样的最大似然法(后面代码用少量数据,直接交叉熵损失函数)。给出一个训练样本 ( u , i ) (u, i) (u,i), 用户embedding v u \mathbf{v}_u vu, 和物品embedding e i \mathbf{e}_i ei,则用户与物品交互的似然函数:
P θ ( i ∣ u ) = exp ( v u ⊤ e i ) ∑ k ∈ I exp ( v u ⊤ e k ) P_\theta(i \mid u)=\frac{\exp \left(\mathbf{v}_u^{\top} \mathbf{e}_i\right)}{\sum_{k \in I} \exp \left(\mathbf{v}_u^{\top} \mathbf{e}_k\right)} Pθ(i∣u)=∑k∈Iexp(vu⊤ek)exp(vu⊤ei)
目标为最小化该似然函数:
loss = ∑ u ∈ U ∑ i ∈ I u − log P θ ( i ∣ u ) \text { loss }=\sum_{u \in \mathcal{U}} \sum_{i \in I_u}-\log P_\theta(i \mid u) loss =u∈U∑i∈Iu∑−logPθ(i∣u)
原论文是按照8:1:1划分训练集、验证集、测试集(按照用户划分,更具泛化能力)。
在我们的Comirec-SA中, 我们的特征重要性 (也就是我们学习出来的Attention Score) 是针对序列中每个Item的Attention Score, 在有了Attention Score之后就可以对序列中的|tem进行加权求和得到序列的单一兴趣表征了。单一兴趣建模时的Attention Score计算:
a = softmax ( w 2 T tanh ( W 1 H ) ) T a=\operatorname{softmax}\left(w_2^T \tanh \left(W_1 H\right)\right)^T a=softmax(w2Ttanh(W1H))T
可以把 1.1 1.1 1.1单一兴趣建模中的 w 2 ∈ R d a w_2 \in \mathbb{R}^{d_a} w2∈Rda 扩充至 W 2 ∈ R d a × K W_2 \in \mathbb{R}^{d_a \times K} W2∈Rda×K(可训练化参数), 这里是因为在输入embedding中加入了可训练的position embedding,其维度和item embedding的维度一样都是 d d d。
Attention Score的计算公式就变成:
A = softmax ( W 2 T tanh ( W 1 H ) ) T A=\operatorname{softmax}\left(W_2^T \tanh \left(W_1 H\right)\right)^T A=softmax(W2Ttanh(W1H))T
其中:
这时我们可以通过如下式子得到用户的多兴趣表征:
V u = H A V_u=H A Vu=HA
其中:
class MultiInterest_SA(nn.Layer):
def __init__(self, embedding_dim, interest_num, d=None):
super(MultiInterest_SA, self).__init__()
self.embedding_dim = embedding_dim
self.interest_num = interest_num
if d == None:
self.d = self.embedding_dim*4
self.W1 = self.create_parameter(shape=[self.embedding_dim, self.d])
self.W2 = self.create_parameter(shape=[self.d, self.interest_num])
def forward(self, seq_emb, mask = None):
''' seq_emb : batch,seq,emb mask : batch,seq,1 '''
H = paddle.einsum('bse, ed -> bsd', seq_emb, self.W1).tanh()
mask = mask.unsqueeze(-1)
A = paddle.einsum('bsd, dk -> bsk', H, self.W2) + -1.e9 * (1 - mask)
A = F.softmax(A, axis=1)
A = paddle.transpose(A,perm=[0, 2, 1])
multi_interest_emb = paddle.matmul(A, seq_emb)
return multi_interest_emb
首先来回顾单头和多头的注意力:
自注意力是指键(K)、查询(Q)和值(V)来自同一数据来源,即 K=Q=V。
Transformer 以特定方式执行此操作:
令 x 1 , x 2 , ⋯ , x T x_{1}, x_{2}, \cdots, x_{T} x1,x2,⋯,xT 是 Transformer 编码器的输入向量, 其中 x i ∈ R d x_{i} \in R^{d} xi∈Rd, 则键 ( K ) (K) (K) 、查询 ( Q ) (Q) (Q) 、值 ( V ) (V) (V) 是:
k i = K x i q i = Q x i v i = V x i \begin{aligned} k_{i} &=K x_{i} \\ q_{i} &=Q x_{i} \\ v_{i} &=V x_{i} \end{aligned} kiqivi=Kxi=Qxi=Vxi
令 X = [ x 1 , x 2 , ⋯ , x T ] ∈ R T × d \mathrm{X}=\left[x_{1}, x_{2}, \cdots, x_{T}\right] \in \mathrm{R}^{\mathrm{T} \times \mathrm{d}} X=[x1,x2,⋯,xT]∈RT×d 是输入向量的串联(拼接), 其中:
如果想一次查询(Q)句子中的多个位置:
对于单词 i \mathrm{i} i, self-attention 只 “看” x i T Q T K x j x_{i}^{T} Q^{T} K x_{j} xiTQTKxj 分数高的地方, 即单头注意力, 见上图的(b) 。但是, 若出于不同的原因想要关注不同的 j \mathrm{j} j,则可通过多个 Q 、 K 、 V \mathrm{Q} 、 \mathrm{~K} 、 \mathrm{~V} Q、 K、 V 矩阵来定义多个注意力 “头”:
令: Q l , K l , V l ∈ R d × d / h Q_{l}, K_{l}, V_{l} \in R^{d \times d / h} Ql,Kl,Vl∈Rd×d/h, 其中 h h h 是注意力头的数量, l l l 的范围从 1 到 h h h,则: 每个注意力头独立地计算注意力: output l = softmax ( X Q l K l T X T ) ∗ X V l \text { output }_{l}=\operatorname{softmax}\left(X Q_{l} K_{l}^{T} X^{T}\right) * X V_{l} output l=softmax(XQlKlTXT)∗XVl
其中:output l ∈ R d / h _{l} \in R^{d / h} l∈Rd/h
然后合并所有头的输出: output = Y [output 1 , … , output h ] \text { output }=Y \text { [output }_{1}, \ldots, \text { output }_{h} \text { ] } output =Y [output 1,…, output h ]
其中: Y ∈ R d × d Y \in R^{d \times d} Y∈Rd×d
因此, 每个头都可以 “看” 不同的事物, 并以不同的方式构建值 ( V \mathrm{V} V ) 向量。多头注意力和单头自注意力有相同的计算量。最简单的多头注意力,即两头注意力(如上图的c)。
为了强化推荐item的多样性,作者将item的类别作为衡量多样性的基础。
问题定义:给定从用户 u u u 的 K K K 个兴趣中检索到一个集合 M \mathcal{M} M, 有 K ⋅ N K \cdot N K⋅N 个物品, 找到一个输出集合 S \mathcal{S} S, 有 N N N 个 物品 (Top-N) , 使预定义的值函数 Q ( u , S ) Q(u, \mathcal{S}) Q(u,S)最大化。
(1)多样性:作者使用 g ( i , j ) g(i, j) g(i,j)用来衡量两个Item i, j之间的多样性:
g ( i , j ) = σ ( C A T E ( i ) ≠ C A T E ( j ) ) g(i, j)=\sigma(C A T E(i) \neq C A T E(j)) g(i,j)=σ(CATE(i)=CATE(j))
(2)推荐精度:当推荐结果的多样性较高的时候, 往往推荐的精度就会下降, 这是一个Trade Off,可以构造如下的目标函数(包含多样性指标和推荐精度指标): Q ( u , S ) = ∑ i ∈ S f ( u , i ) + λ ∑ i ∈ S ∑ j ∈ S g ( i , j ) Q(u, \mathcal{S})=\sum_{i \in \mathcal{S}} f(u, i)+\lambda \sum_{i \in \mathcal{S}} \sum_{j \in \mathcal{S}} g(i, j) Q(u,S)=i∈S∑f(u,i)+λi∈S∑j∈S∑g(i,j)
如上面介绍的SA运算:
A = softmax ( W 2 T tanh ( W 1 H ) ) T A=\operatorname{softmax}\left(W_2^T \tanh \left(W_1 H\right)\right)^T A=softmax(W2Ttanh(W1H))T
可以得到 A ∈ R n × K A \in \mathbb{R}^{n \times K} A∈Rn×K, 这时我们可以通过如下式子得到用户的多兴趣表征:
V u = H A V_u=H A Vu=HA
注意:
[256, 20, 4]
的tensor-[256, 20, 1]
的tensor,后者会减在全部(4个)第2维度之上。paddle.einsum
函数在tf和torch中都有,einsum
(Einstein summation convention,即爱因斯坦求和约定)可以写tensor运算和更高维度tensor写法更加简洁。torch版本的多兴趣抽取层SA代码如下。seq
进行sofjtmax
归一化,即使得seq
维度的元素之和为1。因为对于固定序列中不存在的位置负无穷mask掉了,所以缺失的位置经过softmax
归一化后的数值为0。 Softmax ( x i ) = exp ( x i ) ∑ j exp ( x j ) \operatorname{Softmax}\left(x_i\right)=\frac{\exp \left(x_i\right)}{\sum_j \exp \left(x_j\right)} Softmax(xi)=∑jexp(xj)exp(xi)class MultiInterest_SA(nn.Module):
def __init__(self, embedding_dim, interest_num, d=None):
super(MultiInterest_SA, self).__init__()
self.embedding_dim = embedding_dim
self.interest_num = interest_num
if d == None:
self.d = self.embedding_dim*4
# self.W1 = self.create_parameter(shape=[self.embedding_dim, self.d])
# self.W2 = self.create_parameter(shape=[self.d, self.interest_num])
self.W1 = Parameter(torch.Tensor(self.embedding_dim, self.d))
self.W2 = Parameter(torch.Tensor(self.d, self.interest_num))
def forward(self, seq_emb, mask = None):
''' seq_emb : batch,seq,emb mask : batch,seq,1 '''
H = torch.einsum('bse, ed -> bsd', seq_emb, self.W1).tanh()
mask = mask.unsqueeze(-1) # (batch, seq) -> (batch, seq, 1)
# einsum 爱因斯坦求和约定
# (batch_size, seq_len, interest_num) - (batch_size, seq_len, 1)负无穷,
# 两个维度不同可以做减法, 比如[256, 20, 4]-[256, 20, 1],后者会减在4个 全部之上
A = torch.einsum('bsd, dk -> bsk', H, self.W2) + -1.e9 * (1 - mask)
A = F.softmax(A, dim=1)
A = torch.transpose(A, 2, 1)
multi_interest_emb = torch.matmul(A, seq_emb)
return multi_interest_emb
einsum
(Einstein summation convention,即爱因斯坦求和约定)的用法:
c = np.dot(a, b) # 常规
c = np.einsum('ij,jk->ik', a, b) # einsum
c = np.einsum('ijk,jkl->kl', a, b)
import torch
# 1. 张量转置
A = torch.randn(3, 4, 5)
B = torch.einsum("ijk->ikj", A)
print(A.shape, "\n", B.shape, "\n", "======") # (3, 4, 5) ; (3, 5, 4)
# 2. 取对角元素
A = torch.randn(5, 5)
B = torch.einsum("ii->i", A)
print(A.shape, "\n", B.shape, "\n", "======")
# 3. 求和降维
A = torch.randn(4, 5)
B = torch.einsum("ij->i", A)
print(A.shape, "\n", B.shape, "\n", "======")
# 4. 哈达玛积(两个矩阵维度相同)
A = torch.randn(3, 4)
B = torch.randn(3, 4)
C = torch.einsum("ij, ij->ij", A, B)
print(A.shape, "\n", B.shape, "\n", C.shape, "\n", "======")
# 5. 向量内积
A = torch.randn(10)
B = torch.randn(10)
#C=torch.dot(A,B)
C = torch.einsum("i,i->",A,B)
# 6. 向量外积
A = torch.randn(10)
B = torch.randn(5)
#C = torch.outer(A,B)
C = torch.einsum("i,j->ij",A,B)
# 7. 矩阵乘法
A = torch.randn(5,4)
B = torch.randn(4,6)
#C = torch.matmul(A,B)
C = torch.einsum("ik,kj->ij",A,B)
# 8. 张量缩并
A = torch.randn(3,4,5)
B = torch.randn(4,3,6)
#C = torch.tensordot(A,B,dims=[(0,1),(1,0)])
C = torch.einsum("ijk,jih->kh",A,B)
# 9. batch矩阵乘法
batch_tensor_1 = torch.arange(2 * 4 * 3).reshape(2, 4, 3)
batch_tensor_2 = torch.arange(2 * 3 * 4).reshape(2, 3, 4)
torch.bmm(batch_tensor_1, batch_tensor_2) # [2, 4, 4]
torch.einsum("bij, bjk -> bik", batch_tensor_1, batch_tensor_2) # [2, 4, 4]
这部分和之前DR的类时一样的,改动的部分都体现在MultiInterest_SA
上了。
class ComirecSA(nn.Layer):
def __init__(self, config):
super(ComirecSA, self).__init__()
self.config = config
self.embedding_dim = self.config['embedding_dim']
self.max_length = self.config['max_length']
self.n_items = self.config['n_items']
self.item_emb = nn.Embedding(self.n_items, self.embedding_dim, padding_idx=0)
self.multi_interest_layer = MultiInterest_SA(self.embedding_dim,interest_num=self.config['K'])
self.loss_fun = nn.CrossEntropyLoss()
self.reset_parameters()
def calculate_loss(self,user_emb,pos_item):
all_items = self.item_emb.weight
scores = paddle.matmul(user_emb, all_items.transpose([1, 0]))
return self.loss_fun(scores,pos_item)
def output_items(self):
return self.item_emb.weight
def reset_parameters(self, initializer=None):
for weight in self.parameters():
paddle.nn.initializer.KaimingNormal(weight)
def forward(self, item_seq, mask, item, train=True):
if train:
seq_emb = self.item_emb(item_seq) # Batch,Seq,Emb
item_e = self.item_emb(item).squeeze(1)
multi_interest_emb = self.multi_interest_layer(seq_emb, mask) # Batch,K,Emb
cos_res = paddle.bmm(multi_interest_emb, item_e.squeeze(1).unsqueeze(-1))
k_index = paddle.argmax(cos_res, axis=1)
best_interest_emb = paddle.rand((multi_interest_emb.shape[0], multi_interest_emb.shape[2]))
for k in range(multi_interest_emb.shape[0]):
best_interest_emb[k, :] = multi_interest_emb[k, k_index[k], :]
loss = self.calculate_loss(best_interest_emb,item)
output_dict = {
'user_emb': multi_interest_emb,
'loss': loss,
}
else:
seq_emb = self.item_emb(item_seq) # Batch,Seq,Emb
multi_interest_emb = self.multi_interest_layer(seq_emb, mask) # Batch,K,Emb
output_dict = {
'user_emb': multi_interest_emb,
}
return output_dict
任务信息 | 截止时间 | 完成情况 |
---|---|---|
11月14日周一正式开始 | ||
Task01:Paddle开发深度学习模型快速入门 | 11月14、15、16日周三 | 完成 |
Task02:传统序列召回实践:GRU4Rec | 11月17、18、19日周六 | 完成 |
Task03:GNN在召回中的应用:SR-GNN | 11月20、21、22日周二 | 完成 |
Task04:多兴趣召回实践:MIND | 11月23、24、25、26日周六 | 完成 |
Task05:多兴趣召回实践:Comirec-DR | 11月27、28日周一 | 完成 |
Task06:多兴趣召回实践:Comirec-SA | 11月29日周二 | 完成 |
[1] 多兴趣召回实践:Comirec-SA
论文:Controllable Multi-Interest Framework for Recommendation
链接:https://arxiv.org/abs/2005.09347
[2] 推荐场景中召回模型的演化过程. 京东大佬
[3] 原论文作者代码:https://github.com/THUDM/ComiRec/blob/a576eed8b605a531f2971136ce6ae87739d47693/src/train.py
[4] https://github.com/ShiningCosmos/pytorch_ComiRec/blob/main/ComiRec.py
[5] https://wangxiaocs.github.io/
[6] 推荐算法炼丹笔记:阿里序列化推荐算法ComiRec
[7] einsum is all you needed
[8] Understanding PyTorch einsum
[9] 矩阵操作万能函数 einsum 详细解析(通法教你如何看懂并写出einsum表达式)
[10] Sparse-Interest Network for Sequential Recommendation .AAAI 2021
[11] torch_rechub代码复现:https://github.com/datawhalechina/torch-rechub/blob/main/torch_rechub/models/matching/comirec.py
单头注意力机制:
A t t e n t i o n ( Q , K , V ) = Q K T d k ∗ V Attention(Q,K,V)=\frac{QK^T}{\sqrt{d_k}}*V Attention(Q,K,V)=dkQKT∗V
多头注意力机制:
M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h ) ∗ W O MultiHead(Q,K,V)=Concat(head_1,...,head_h)*W^O MultiHead(Q,K,V)=Concat(head1,...,headh)∗WO
其中:
h e a d i = A t t e n t i o n ( Q ∗ W i Q , K ∗ W i K , V ∗ W i V ) head_i=Attention(Q*W_i^Q,K*W_i^K,V*W_i^V) headi=Attention(Q∗WiQ,K∗WiK,V∗WiV)