赛程持续大概了两个多月的时间,从临近毕业到工作,算是学生时代最后一个比赛吧,遗憾的是成绩并不是很理想,最后只拿到了国二,到了复赛阶段又是被迫solo的局面。复赛期间体会到了在职人员的不易,白天上班,晚上熬夜打比赛,真的太难了┭┮﹏┭┮。最后,做为学生时代的“终点”,还是在此记录下比赛过程中学习到的知识,感谢周周星的分享以及大佬们无私的开源,努力向前排优秀选手学习。
此次赛题:GitHub地址 作者:Wisley 邮箱:903953316@qq.com GitHub:个人主页
一、赛题描述w
此次比赛基于脱敏和采样后的数据信息,对于给定的一定数量到访过微信视频号“热门推荐”的用户, 根据这些用户在视频号内的历史n天的行为数据,通过算法在测试集上预测出这些用户对于不同视频内容的互动行为(包括点赞、点击头像、收藏、转发等)的发生概率。 本次比赛以多个行为预测结果的加权uAUC值进行评分。
1、数据
数据主要包含两个表,feed_info表与user_action表,详细介绍说明可以查看这里。
- feed_info.csv
Feed信息表包含了视频(简称feed)的基本信息,如authorid (视频号作者ID)、bgm_song_id (背景音乐ID),以及manual_keyword_list (人工标注关键词)等基本ID信息,同时还包括了脱敏的文本信息(比如视频描述),以及音频信息as r、图像识别信息ocr 等均以脱敏的字符串数字表示。同时赛方额外提供了融合了ocr 、asr 、图像、文字的多模态的内容理解特征向量,特征维度为512 维。 - user_action.csv
用户行为表包含了用户在视频号内一段时间内的历史行为数据(包括停留时长、播放时长和各项互动数据),该表的用户对应数据按照时间戳顺序由小到大排列。表中已提供了用户点击行为的标签。
user action表中主要有七种点击行为,其中初赛的最终分数为4个行为(查看评论、点赞、点击头像、转发)的uAUC值的加权平均。复赛的最终分数为7个行为(查看评论、点赞、点击头像、转发、收藏、评论和关注)的uAUC值的加权平均。
2、评价指标
本次比赛采用uAUC作为单个行为预测结果的评估指标,uAUC定义为不同用户下AUC的平均值,计算公式如下:
u
A
U
C
=
1
n
∑
i
=
1
n
A
U
C
i
u A U C=\frac{1}{n} \sum_{i=1}^{n} A U C_{i}
uAUC=n1?i=1∑n?AUCi? 其中,n为测试集中的有效用户数,有效用户指的是对于某个待预测的行为,过滤掉测试集中全是正样本或全是负样本的用户后剩下的用户。AUCi为第i个有效用户的预测结果的AUC(Area Under Curve)。 从计算公式可以看出,我们只针对每个用户的AUC计算并取平均,那么整体排序上,不同用户的行为相互是不受影响的,同时由于是AUC的计算,所以结果与预测概率大小也无关,只与该用户行为预测的排序有关。
3、其他说明
- 比赛全程不允许使用外部数据集。
- 允许使用开源的词典、embedding和预训练模型,以上数据和模型需在2021/07/12日期前开源,且需通过邮件的形式向组委会报备开源链接地址和md5。
- 复赛阶段允许使用初赛阶段的数据集。
二、初赛方案
初赛主要基于deep_ctr[1]的框架进行的,其中,队友近西使用lightgbm机器学习方法,对feed表与action进行特征工程后,将特征送入lgb训练预测。队友kitty是借鉴baseline的深度学习方法,采用deepfm模型将连续特征、离散特征、以及可变长特征(序列文本特征)送入模型预测。两个队友的方案都是单任务预测,最后汇总成多任务的,这部分队友的工作不过多介绍,主要介绍下笔者初赛的方案和思路。
1、训练框架
由于赛题是个多标签预测任务,考虑到复赛会在更大规模的数据上进行,所以一开始便没有采用单任务预测的方式,而是采用了多标签预测的方案。模型在最后的输出层使用MMOE进行多任务输出。
在训练框架的设计上,受GNN异构图的构建启发,笔者单独建立了user表与feed表,其中user表用于存放用户侧的相关特征,feed表用于存放视频侧的相关特征,两个表用字典的方式进行存储,特征名作为key进行索引。我们对feedid按照顺序从0开始重新编码为feedid_node ,每个特征的排序按照重新编码后的顺序排列,因此只要知道feedid_node 的编号以及特征key,就能取出该feed对应的特征。同理userid也重新编码为了userid_node 。我们在构建完两个特征表后,在build模型时,将其放到GPU上。而在模型前向传播时,只需将feedid_node 与userid_node 送进去,根据id 来索引对应的特征,将其拼接即可。这样就大大降低了内存开销。普通的训练方式是将特征与user action 行为表进行拼接,以Sequence batch的方式送入模型训练,这样内存占用的大小与action 的大小以及user与feed特征维度数有关。而我们采取的方式,只与user 的数量与feed 数量与维度有关,与action 的大小无关。由于本赛题的userid 与feedid 是固定数量的(测试集的userid一定出现在训练集中,可能出现的feed赛方已全部给出),所以这样的方式将原本需要的 a*(di+ui) 的内存空间,缩减到只需要 u*ui+d*di 大小的内存空间。其中a表示用户行为数,d表示feed数量,u表示user数量,di表示feed特征维度,ui表示user特征维度。除此之外,对于嵌入特征,可以只保存对应序列id,在模型通过模型内部的embedding矩阵来得到特征向量,可以大大缩减内存和显存的占用,在初赛阶段,我们4096的batcsize 显存占用也只有5g,且显存占用大小与batchsize大小无关。
特征表建立与训练部分示例代码如下:
#构建feed表与user表
feed_data={}
user_data={}
#feed
feed_data['dense']=torch.from_numpy(feeds[dense_features].values.astype('float32'))
feed_data['hash_dense']=torch.from_numpy(np.hstack([dense_arry1,dense_arry2,dense_arry4,dense_arry3]).astype('float32'))
# user
user_feats=['userid','device']
for f in user_feats:
user_info[f]=user_info[f].fillna(0)
for f in ['device']:
gens=LabelEncoder()
user_info[f]=gens.fit_transform(user_info[f])
user_data[f]=torch.from_numpy(user_info[f].values)
# 放到GPU上
for f,d in user_data.items():
user_data[f]=d.to(torch.device('cuda'))
for f,d in feed_data.items():
feed_data[f]=d.to(torch.device('cuda'))
#训练
batch_size=4096
src=train_ratings['userid'].apply(lambda x: userid2nid[x]).values
dst=train_ratings['feedid'].apply(lambda x: feedid2nid[x]).values
for ind in tqdm(range(0,n_pos//batch_size+1)):
batch=batch_index[ind*batch_size:(ind+1)*batch_size]
batch_src=src[batch] # user node
batch_dst=dst[batch] # feed node
logits = model(batch_src,batch_dst)
2、模型结构
由于笔者在校期间,自研过一些GNN相关知识,而最近GNN在推荐领域也是炒的热火朝天,所以在比赛初期一直想尝试使用GNN 的一些模型,这部分模型是基于DGL实现的。
(1) PinSage
PinSage是斯坦福和Pinterest公司合作提出的工业级GCN推荐系统,并将其应用在了Pinterest上,PinSage模型使用随机游走和图卷积来捕获图结构的特征以及节点的特征,以生成节点的嵌入表示。该算法的特点在于通过采样节点的邻居并动态地从采样邻居构建计算图,实现了有效的局部的卷积,从而在训练时不需要在整张图上进行操作。这里我们可以对user节点的feed邻居进行采样,来构建feed节点的子图。采样方式同样是选择随机游走的方式。
关于GCN部分 笔者尝试了GAT、GraphSAGE等 效果都不理想,尝试将学到的node embedding与原始特征的embedding进行合并,通过DNN输出,有比较大的提升,但是线上分数只有0.63左右。后来发现不使用GNN中的embedding反倒分数更高,遂放弃了这个方案。pinsage代码参考
(2)RGCN
在PinSage中,采用二部图进行构图,这本身可以当作一个异质的图神经网络,而不同任务关系就是异质的边关系。因此笔者又尝试构建异质图神经网络,并利用RGCN作为图卷积进行消息聚合。对于不同的边关系(比赛中我们用不同的点击任务构建边),都有一组可学习的参数矩阵对应(图卷积)。异构图RGCN的代码可以参考DGL官方手册示例代码,如下:
import dgl.nn as dglnn
class RGCN(nn.Module):
def __init__(self, in_feats, hid_feats, out_feats, rel_names):
super().__init__()
self.conv1 = dglnn.HeteroGraphConv({
rel: dglnn.GraphConv(in_feats, hid_feats)
for rel in rel_names}, aggregate='sum')
self.conv2 = dglnn.HeteroGraphConv({
rel: dglnn.GraphConv(hid_feats, out_feats)
for rel in rel_names}, aggregate='sum')
def forward(self, graph, inputs):
# inputs are features of nodes
h = self.conv1(graph, inputs)
h = {k: F.relu(v) for k, v in h.items()}
h = self.conv2(graph, h)
return h
经过实验发现,对于比赛数据,将与用户交互的商品作为边,比以点击任务作为边关系效果要好,另外对于卷积操作,普通的图卷积也要优于其他卷积操作,相比Pinsage,异质RGCN的表现要更好,初赛分数能达到0.65左右,经过进一步的调参和特征优化,可以达到0.66左右的分数,但是后续的一周时间里笔者却始终没有继续上分了,所以不得已,还是把注意力放在了传统NN方法上。
(3)Cross Net mix
在不同的业务场景下,CTR任务更多是依赖细致的特征工程。一般来说,最简单的线性模型就是将原始特征进行组合变换,它非常容易理解并且容易扩展,但是表达能力有限。而有效的组合特征通常需要人工不断的探索与尝试。NN方法,则是为了代替费时费力的手工特征,让模型自动探索和组合高阶特征。现有的很多成熟且有效的NN模型,如Deepfm、DIN、AutoInt,都是尝试从不同的方面来提高特征的表达能力,使模型能更有效地挖掘离散、连续、文本序列等特征的组合。理论上,DNN能够在特定平滑假设下以任意的精度逼近任意函数,而在实际中大多数函数并不是任意的,所以DNN能够利用可行的参数量达到很好的效果。通过DNN能够对离散和序列等特征的Embedding向量以及非线性激活函数学习到高阶的特征组合,并且残差网络使得我们能够训练很深的网络。然而隐式的学习了所有的特征组合,对于模型效果和学习效率可能并不都是有利的,而且缺乏一定的可解释性。所以FM和FFM 提出了显式去构造特征的二阶或者更高阶的组合,但是浅层的结构反而限制了特征的表达能力,更高阶的扩展则产生了大量额外的计算开销。并且在比赛中,很多有效的特征组合往往都是低阶的。因此,作者提出了一种高效的方式进行显性的特征组合,Cross Network 每一层的神经元数量都相同而且等于输入向量
X
0
X_0
X0?的维度,每一层都有如下公式所示(都是列向量),其中函数
f
f
f拟合的是
X
l
+
1
X_{l+1}
Xl+1??
X
l
X_l
Xl?的残差。进一步
X
l
+
1
=
X
0
X
l
T
W
l
+
B
l
+
X
l
=
f
(
X
l
,
W
l
,
B
l
)
+
X
l
X_{l+1}=X_{0} X_{l}^{T} W_{l}+B_{l}+X_{l}=f\left(X_{l}, W_{l}, B_{l}\right)+X_{l}
Xl+1?=X0?XlT?Wl?+Bl?+Xl?=f(Xl?,Wl?,Bl?)+Xl? 相比DNN,交叉网络显示地构建了交叉项,且参数量要小很多,同时每一层就是特征的一次交叉组合,模型更具有可解释性。进一步,受MOE结构启发DCN-M模型对DCN进行了改进,将W变成参数矩阵,将矩阵分解至多个子空间,随后通过门控机制来对这些子空间进行融合,其有效地学习显式和隐式特征交叉,使模型高效、简单的同时,增强了表达能力,如下图所示。 在比赛中,我们将稀疏特征与连续特征以及embedding特征拼接后,送入cross net,专家数默认4,交叉网络层数4,最后与DNN的输出拼接,经过一层线性层输出,线上能达到0.669左右的成绩,相比于之前GNN的表现都要好很多。在deep_ctr中可以直接调用:
CrossNetMix(128*3+64,layer_num=4)
笔者通过消融实验,对比deepfm,在相同特征的情况下,cross network相比deepfm有将近一个百的提升。
3、特征构建
这里简单列举下比较有用的特征,一些原始特征输入不过多解释。
1. Target Encode
对feedid、authorid、userid等或进行组合,通过滑窗(前N天)计算label的均值作为编码后的特征。(详细可在代码中查看)
2. 稀疏特征
maunual_keyword_list等序列特征取其中的id作为稀疏特征,或者使用tfidf+svd得到embdding。
3、embding★
这个特征算是整个比赛中的强特了,发现前排很多队伍都是靠这个上分的,类似GNN的思想,通过groupby userid得到feedid序列,当作句子来训练w2v,得到feedid的embedding。这里尝试了分任务embedding,效果远没有直接把所有交互做embedding好。我们认为,比赛方提供的数据可能是已经粗排后的结果,给用户曝光的feed其实在一定程度上表现了用户一定的喜好趋势,同时如果分任务做emb也会造成信息泄露的问题。这里的embedding可以尝试多种的组合,对feedid和userid都可以做,如果显存不够的话可以用svd分解来降维。通过一些实验我们发现最有用的还是通过userid对feedid的emb,同时我们还尝试了deepwalk和node2vec的方式,和groupby的方式差别不大。
三、复赛方案
复赛由于数据量是初赛的10倍,所以大多数选手的主要工作是如何在有限的内存下来复现初赛的方案。笔者初赛时考虑了复赛的情况,所以并不需要对原本的方案做额外的改动。在统计特征方面,由于测试集不可见,所以在做特征的时候,保存了训练集的统计特征分布,用于推断时提供给测试集。在不做任何优化改进时,复赛第一次提交就有0.695的分数。
1、有效尝试
1、feedid emb 的改进,采用sg 和hs 模式分别训练emb最后进行拼接。 2、通过计算相关系数,发现有些任务比较相关,可以采用共享线性层的方式。 3、DNN层使用BN和dropout 增加模型的泛化能力 4、训练上采用 lookahead+admw ,两个epoch即可。 5、 借鉴DIN的结构,增加user的行为序列输入,通过attention pool得到兴趣feed的emb。(这部分可以构建一个新的模型,但是最后分数和cross network是差不多的,可以作为最后的多模融合,详细可以参考deep_ctr中的代码以及笔者git上的代码)。也有前排选手是借鉴transform的结构来做,并取得了不错的效果,但是笔者使用tansformer的效果非常差,等后面开源了可以学习下大佬是怎么做的。
2、无效尝试
1、对action表进行负采样(负样本的定义有多种,但是都不太理想),由于线上数据是非常宝贵的,虽然存在噪声的可能,但是我们没有一个有效的判断噪声的方法,所以如无必要,尽量用全量数据训练是最好的。 2、userid 的embedding,这部分的做法和feedid的emb是一样的,但是效果并不明显,在初赛还有降分的迹象。 3、增加ple层,PLE号称是比MMOE在多任务上有更好的表现,笔者基于torch实现了ple层,线下确实有一点点提升,但是线上结果和mmoe是差不多的,参数量却更大,所以最后放弃了这个方案。 4、 增加各类特征embedding的维度,增加dnn层数、expert数等,均无法提升,反而会有过拟合的风险。
四、赛后思考
(1)在初赛时,统计特征、目标编码等方式,对分数有较为明显的提升,但是在复赛阶段,这部分却没那么重要,笔者在复赛时,意外发现,即使去除所有交互的统计特征,只保留embedding特征,模型依然能达到相当甚至更好的分数。 (2)根据现在已开源的前排方法,笔者分析主要差距是没有对emb做进一步深入尝试,对其他id做emb+svd以及对文本特征做tfidf+svd 会有微妙的效果,我们在一些长文本特征上采用截断的方式也不是很合理,不同特征组合也会相互影响。同时发现在复赛阶段,统计特征可能对模型预测还会有一定的副作用,但是没有进一步去研究和实验。 (3)赛后思考了下,为什么几种GNN的建模都不理想,我们使用的都是消息传递的框架,赛方给的主要是feed侧的特征,那么不管是user node还是feed node 基本都是由feed emb的信息传递产生的,而有的user交互非常少,有的user交互feed特别多,这种不平衡是否会影响节点的表达? 另外GNN本身就存在过平滑的问题,最近的研究发现,GCN中的线性变换会加速模型的退化,是否我们可以简单地只构建一层GCN 进行信息聚合? 本次虽然在最后比赛中并没有使用GNN模型,但是通过groupby userid 对feedid预训练和GNN有异曲同工的作用,而这部分的提升是非常大的,这说明GNN 的结构是非常值得借鉴的,那么实现一个端到端的GNN+DNN的模型,是否会比预训练+dnn的形式要更好更优雅。 (4) 本题用户的行为序列并未表现出很强的时序性,虽然数据按照用户的点击时间进行了排序,但是当使用时序模型如LSTM去提取深度特征时,表现并不好。笔者认为,对于视频推荐的用户主体,点击事件不太受视频的曝光序列影响,它与推荐不同,并没有表现出很强的时序性,比如:上个星期我喜欢并点赞的视频,大概率这星期还会喜欢(但推荐曝光的视频会根据短期的用户兴趣进行推荐)。
以上是笔者自己的一些思考和猜想,欢迎读者讨论发表有趣的观点一起讨论。 在此再次感谢前人无私的开源与分享,我们是站在巨人的肩膀才看的更高更远!
参考文献
【1】DeepCTR: Easy-to-use,Modular and Extendible package of deep-learning based CTR models ,Weichen Shen 【2】 「【Paper】Deep & Cross Network for Ad Click Predictions - 一只背影 - 博客园」- https://www.cnblogs.com/cling-cling/p/9922766.html 【3】 「深度特征工程:[google]DCN-M: Improved Deep & Cross Network for Feature Cross Learning in Web-scale Learning_knight的博客-CSDN博客」- https://blog.csdn.net/u012852385/article/details/109197384 【4】 「Deep and Cross Network原理及实现 Andante」- https://nirvanada.github.io/2017/12/14/DCN/ 【5】 「deepctr.models.dcnmix module — DeepCTR 0.8.7 documentation」- https://deepctr-doc.readthedocs.io/en/latest/deepctr.models.dcnmix.html
|