前言
论文地址:arxiv
代码地址:github
注意,代码第三方复现难免有diff,建议阅读论文~
系列文章
推荐系统——Neural Collaborative Filtering(NMF)
推荐系统——Deep Interest Network for Click-Through Rate Prediction(DIN)
推荐系统——Deep Interest Evolution Network for Click-Through Rate Prediction(DIEN)
推荐系统——Deep Session Interest Network for Click-Through Rate Prediction(DSIN)
推荐系统——Multi-Interest Network with Dynamic Routing for Recommendation at Tmall(MIND)
推荐系统——Behavior Sequence Transformer for E-commerce Recommendation in Alibaba(BST)
持续更新中…
动机
以往的做法一般是把用户的兴趣转成一个单独的embedding,作者认为用户应该有多个兴趣,比如一个人喜欢鞋子又喜欢篮球,就应该有两个兴趣中心。因此本文的主要动机就是对多兴趣进行建模。
网络结构
本文的网络结构画风和DIN简直太像哈哈哈,网络结构也很简单,从输入往上看。
上图为输入的内容,other Feature主要为用户的属性特征,比如性别年龄等,item1-N为用户的序列行为,label那一侧应该是推荐的内容吧就是target item。举个例子,背景是预测新推荐的短视频用户是否点击,那么item1-N就是用户以往点击的N个视频,target item就是当前推荐的短视频,other feature就是用户的年龄性别那些特征。
上图是本文的核心部分,针对图可以看到,N个item变成了5个兴趣中心,其实这部分的实现就是Kmeans哈,这个就是可以简单的理解成用聚类算法把N个item聚成了5个类,类中心就是兴趣中心。但是为啥作者的实现不是Kmeans呢,其实也很简单,sklearn的Kmeans不是用tensor写的,没法传梯度然后反向传播更新模型,作者这里看着就像用tensor简单的实现了Kmeans的功能。
我这里简单的写了下流程:
cjhc^h_jcjh?是输出的兴趣中心,cilc^l_icil?是输入的N个item,SijS_{ij}Sij?是一个可学习的参数,流程如下:
1、首先bijb_{ij}bij?用正态分布初始化
2、根据图示公式计算wijw_{ij}wij?
3、根据图示公式计算zjhz^h_jzjh?
4、根据图示公式计算cjhc^h_jcjh?
5、根据图示公式更新bijb_{ij}bij?
6、重复2-6执行n轮
公式真没啥就是矩阵乘的运算,可以简单的理解成Kmeans就好了。然后这里需要人为的设定聚类的中心,其实是非常不友好的,这也是Kmeans算法比较致命的缺陷,优化的话可以试试DBSCAN这样的聚类算法,根据特征的密度自动确定类中心个数可能更合理一些。
后面一节就是把特征拼接起来过FC层接损失进行分类了,这还多了一个label-aware attention,实现也很简单,就是ReLU出来的特征过两个不同的FC生成K和V,然后K和Q(Q就是target item)点乘,算softmax后计算每个K的权重然后加权到V上了。
Torch测试代码
import torch
import torch.nn as nn
import torch.nn.functional as Fdef sequence_mask(lengths, maxlen=None, dtype=torch.bool):if maxlen is None:maxlen = lengths.max()row_vector = torch.arange(0, maxlen, 1)row_vector = row_vector.to(lengths.device)matrix = torch.unsqueeze(lengths, dim=-1)mask = row_vector < matrixmask.type(dtype)return maskdef squash(inputs):vec_squared_norm = torch.sum(torch.square(inputs), dim=1, keepdim=True)scalar_factor = vec_squared_norm / (1 + vec_squared_norm) / torch.sqrt(vec_squared_norm + 1e-9)vec_squashed = scalar_factor * inputs # element-wisereturn vec_squashedclass RoutingNew(nn.Module):def __init__(self, input_seq_len, input_dim, output_dim, iteration=1, output_seq_len=1):super(RoutingNew, self).__init__()self.iteration = iterationself.output_seq_len = output_seq_lenself.input_seq_len = input_seq_lenself.input_dim = input_dimself.output_dim = output_dimself.B_matrix = nn.init.normal_(torch.empty(1, output_seq_len, input_seq_len), mean=0, std=1)self.B_matrix.requires_grad = Falseself.S_matrix = nn.init.normal_(torch.empty(self.input_dim, self.output_dim), mean=0, std=1)def forward(self, low_capsule, seq_len):## seq: B * 1## low_capsule: B * in_seq_len * in_dim## high_capsule: B * out_seq_len * out_dimB, _, _ = low_capsule.size()assert torch.max(seq_len).item() <= self.input_seq_lenassert self.iteration >= 1seq_len_tile = seq_len.repeat(1, self.output_seq_len) # B * out_seq_lenfor i in range(self.iteration):mask = sequence_mask(seq_len_tile, self.input_seq_len) # B * out_seq_len * in_seq_lenpad = torch.ones_like(mask, dtype=torch.float32) * (-2 ** 16 + 1) # B * out_seq_len * in_seq_lenpad = pad.to(mask.device)B_tile = self.B_matrix.repeat(B, 1, 1) # B * out_seq_len * in_seq_lenB_tile = B_tile.to(mask.device)B_mask = torch.where(mask, B_tile, pad) # B * out_seq_len * in_seq_lenW = nn.functional.softmax(B_mask, dim=-1) # B * out_seq_len * in_seq_lenself.S_matrix = self.S_matrix.to(low_capsule.device)low_capsule_new = torch.einsum('ijk,ko->ijo', (low_capsule, self.S_matrix)) # B * in_seq_len * output_dimhigh_capsule_tmp = torch.matmul(W, low_capsule_new) # B * output_seq_len * output_dimhigh_capsule = squash(high_capsule_tmp) # B * output_seq_len * output_dimwith torch.no_grad(): # 这里一定要截断梯度,不然反向传播这个for循环会报错B_delta = torch.sum(torch.matmul(high_capsule, torch.transpose(low_capsule_new, dim0=1, dim1=2)),dim=0, keepdim=True)B_delta = B_delta.to(mask.device)self.B_matrix = self.B_matrix.to(mask.device)self.B_matrix = B_delta + self.B_matrixreturn high_capsuleif __name__ == '__main__':# 序列长16,输入维度128,输出维度256,迭代1轮,输出的兴趣中心3个model = RoutingNew(input_seq_len=16,input_dim=128,output_dim=256,iteration=1,output_seq_len=3)input = torch.randn((64,16,128))# 因为序列可能不定长,需要给padding的部分mask掉,这里测试就16个全保留seq_mask = torch.ones((64,1))*16output = model(input,seq_mask)print(output.shape) # 64,3,256
效果
这个效果看不太懂,看起来是变好了,但是我在工业数据测试这个模型效果很差…而且类中心设几个合理呢?感觉不是很好用的一个方法。但是呢多个兴趣中心的这个想法是很不错的,有机会想想怎么优化这个方案~太忙了。。哎