2019年XNUMX月至XNUMX月,举办了社交网络提要排名竞赛
SNA 黑客马拉松
这是第三次以该名称举办黑客马拉松。 它由社交网络ok.ru组织,任务和数据分别与该社交网络直接相关。
在这种情况下,SNA(社交网络分析)更正确地理解为不是对社交图的分析,而是对社交网络的分析。
- 2014 年,任务是预测一个帖子会获得多少点赞。
- 2016年——VVZ任务(也许你很熟悉),更接近于社交图谱的分析。
- 2019 年,根据用户喜欢帖子的可能性对用户的 feed 进行排名。
2014年我不敢说,但2016年和2019年,除了数据分析能力之外,还需要大数据的处理能力。 我认为正是机器学习和大数据处理问题的结合吸引了我参加这些比赛,我在这些领域的经验帮助我获胜。
机器学习训练营
2019年在平台举办比赛
比赛于7月3日线上开始,共设XNUMX个任务。 任何人都可以在网站上注册、下载
任务
源数据提供用户 ID (userId) 和帖子 ID (objectId)。 如果向用户显示帖子,则数据包含一行,其中包含 userId、objectId、用户对此帖子的反应(反馈)以及一组各种功能或图片和文本的链接。
用户身份 | 对象标识 | 所有者 ID | 反馈 | 图片 |
---|---|---|---|---|
3555 | 22 | 5677 | [已点赞、已点击] | [哈希1] |
12842 | 55 | 32144 | [不喜欢] | [哈希2,哈希3] |
13145 | 35 | 5677 | [点击,分享] | [哈希2] |
测试数据集包含类似的结构,但缺少反馈字段。 任务是预测反馈字段中“喜欢”反应的存在。
提交文件的结构如下:
用户身份 | 排序列表[objectId] |
---|---|
123 | 78,13,54,22 |
128 | 35,61,55 |
131 | 35,68,129,11 |
该指标是用户的平均 ROC AUC。
更详细的数据描述可以在以下位置找到:
线上舞台
线上阶段,任务分为3部分
线下阶段
离线阶段,数据包含所有特征,而文本和图像稀疏。 数据集中的行数增加了 1,5 倍,而数据集中的行数已经很多了。
解决问题
由于我是在工作中做简历的,所以我从“图像”任务开始了这次比赛的旅程。 提供的数据包括 userId、objectId、ownerId(发布帖子的组)、创建和显示帖子的时间戳,当然还有该帖子的图像。
基于时间戳生成多个特征后,下一个想法是采用在 ImageNet 上预训练的神经元的倒数第二层,并将这些嵌入发送到 boosting。
结果并不令人印象深刻。 我想,来自 imagenet 神经元的嵌入是无关紧要的,我需要制作自己的自动编码器。
花了很多时间,结果却没有改善。
特征生成
处理图像需要花费很多时间,所以我决定做一些更简单的事情。
正如您立即看到的,数据集中有几个分类特征,为了不打扰太多,我只使用了 catboost。 解决方案非常棒,无需任何设置,我立即就到达了排行榜的第一行。
有相当多的数据,并且以 parquet 格式布局,因此我毫不犹豫地采用了 scala 并开始在 Spark 中编写所有内容。
比图像嵌入提供更多增长的最简单的功能:
- objectId、userId 和ownerId 在数据中出现的次数(应与流行度相关);
- userId 从ownerId 看到了多少帖子(应与用户对群组的兴趣相关);
- 有多少个唯一的 userId 查看了ownerId 的帖子(反映了群组受众的规模)。
从时间戳可以获取用户观看提要的一天中的时间(早上/下午/晚上/晚上)。 通过组合这些类别,您可以继续生成特征:
- 晚上userId登录了多少次;
- 该帖子最常在什么时间显示(objectId)等等。
所有这些都逐渐改善了指标。 但训练数据集的大小约为20M记录,因此添加特征大大减慢了训练速度。
我重新思考了使用数据的方法。 虽然数据是依赖时间的,但我并没有看到“未来”有任何明显的信息泄露,不过,为了以防万一,我将其分解如下:
提供给我们的训练集(二月和三月的两周)分为两部分。
该模型根据过去 N 天的数据进行训练。 上述聚合是基于所有数据(包括测试)构建的。 与此同时,出现了可以构建目标变量的各种编码的数据。 最简单的方法是重用已经创建新功能的代码,并简单地向其提供不会接受训练且目标 = 1 的数据。
因此,我们得到了类似的特征:
- userId在群组ownerId中看过帖子多少次;
- userId对群组ownerId中的帖子点赞了多少次;
- userId 喜欢的来自ownerId 的帖子的百分比。
也就是说,事实证明 平均目标编码 数据集的一部分,用于分类特征的各种组合。 原则上,catboost 还会构建目标编码,从这个角度来看没有任何好处,但是,例如,可以计算喜欢该组中帖子的唯一用户的数量。 同时,主要目标也达到了——我的数据集减少了好几倍,并且可以继续生成特征。
虽然 catboost 只能根据喜欢的反应构建编码,但反馈还有其他反应:转发、不喜欢、不喜欢、点击、忽略,可以手动完成编码。 我重新计算了各种聚合并消除了重要性较低的特征,以免数据集膨胀。
那时我已经以遥遥领先的优势位居第一。 唯一令人困惑的是图像嵌入几乎没有增长。 这个想法就是把一切都奉献给 catboost。 我们对 Kmeans 图像进行聚类并得到一个新的分类特征 imageCat。
以下是手动过滤和合并从 KMeans 获得的簇后的一些类。
基于 imageCat 我们生成:
- 新的分类功能:
- userId 最常查看哪张 imageCat;
- 哪个imageCat最常显示ownerId;
- userId 最常喜欢哪张 imageCat;
- 各种计数器:
- imageCat查看了多少个唯一的userId;
- 大约 15 个相似特征加上如上所述的目标编码。
文本
图像比赛的结果很适合我,我决定尝试一下文字。 我以前没怎么处理过文本,愚蠢的是,我在 tf-idf 和 svd 上度过了这一天。 然后我看到了 doc2vec 的基线,它正是我所需要的。 稍微调整 doc2vec 参数后,我得到了文本嵌入。
然后我简单地重用了图像的代码,其中我用文本嵌入替换了图像嵌入。 结果,我在文字比赛中获得了第二名。
协同系统
还有一场比赛我还没有用棍子“戳”过,从排行榜上的AUC来看,这场比赛的结果应该对线下舞台的影响最大。
我获取了源数据中的所有特征,选择了分类特征,并计算了与图像相同的聚合,除了基于图像本身的特征之外。 仅仅将它放入 catboost 就让我获得了第二名。
catboost 优化的第一步
一个第一名和两个第二名让我很高兴,但大家都知道我没有做任何特别的事情,这意味着我可能会失去位置。
比赛的目标是对用户内的帖子进行排名,而这段时间我一直在解决分类问题,即优化错误的指标。
我将举一个简单的例子:
用户身份 | 对象标识 | 预测 | 基本事实 |
---|---|---|---|
1 | 10 | 0.9 | 1 |
1 | 11 | 0.8 | 1 |
1 | 12 | 0.7 | 1 |
1 | 13 | 0.6 | 1 |
1 | 14 | 0.5 | 0 |
2 | 15 | 0.4 | 0 |
2 | 16 | 0.3 | 1 |
让我们做一个小的重新安排
用户身份 | 对象标识 | 预测 | 基本事实 |
---|---|---|---|
1 | 10 | 0.9 | 1 |
1 | 11 | 0.8 | 1 |
1 | 12 | 0.7 | 1 |
1 | 13 | 0.6 | 0 |
2 | 16 | 0.5 | 1 |
2 | 15 | 0.4 | 0 |
1 | 14 | 0.3 | 1 |
我们得到以下结果:
是否提供样品 | AUC | 用户1 AUC | 用户2 AUC | 平均曲线下面积 |
---|---|---|---|---|
选项1 | 0,8 | 1,0 | 0,0 | 0,5 |
选项2 | 0,7 | 0,75 | 1,0 | 0,875 |
如您所见,提高整体 AUC 指标并不意味着提高用户内的平均 AUC 指标。
加速
在“协作系统”竞赛在线阶段结束前 5 分钟,Sergey Shalnov 将我移至第二名。 我们一起走更远的路。
线下阶段准备
我们凭借 RTX 2080 TI 显卡保证了在线阶段的胜利,但 300 卢布的主要奖金,甚至很可能是最后的第一名,都迫使我们工作了两周。
事实证明,Sergey 也使用了 catboost。 我们交换了想法和特点,我了解到
查看报告让我想到我们需要将所有参数恢复为默认值,并且只有在修复一组功能之后才能非常仔细地进行设置。 现在,一次训练大约需要 15 个小时,但一个模型的速度比在带有排名的集成中获得的速度要好。
特征生成
在协作系统竞赛中,大量特征被评估为对模型很重要。 例如, 审计权重_spark_svd - 最重要的标志,但没有关于它的含义的信息。 我认为根据重要特征来计算各种聚合是值得的。 例如,按用户、按组、按对象计算平均auditweights_spark_svd。 使用没有进行训练且target = 1的数据也可以计算出同样的结果,即平均值 审计权重_spark_svd 按用户按他喜欢的对象。 除此以外的重要标志 审计权重_spark_svd,有好几个。 这里是其中的一些:
- 审计权重Ctr性别
- 审计权重CtrHigh
- 用户所有者计数器创建喜欢
例如,平均 审计权重Ctr性别 根据userId结果发现这是一个重要的特征,就像平均值一样 用户所有者计数器创建喜欢 通过用户Id+所有者Id。 这应该已经让您认为您需要理解这些字段的含义。
同样重要的特征是 审核权重点赞数 и 审核权重显示计数。 将其中一个除以另一个,就得到了一个更重要的特征。
数据泄露
竞赛和生产建模是非常不同的任务。 在准备数据时,很难考虑到所有细节并且不传达有关测试中目标变量的一些重要信息。 如果我们正在创建生产解决方案,我们将在训练模型时尽量避免使用数据泄漏。 但如果我们想赢得竞争,那么数据泄露就是最好的功能。
研究了数据可以看出,根据objectId值 审核权重点赞数 и 审核权重显示计数 变化,这意味着这些特征的最大值的比率将比显示时的比率更好地反映转换后的情况。
我们发现的第一个泄漏是 auditweightsLikesCountMax/auditweightsShowsCountMax.
但如果我们更仔细地观察数据呢? 让我们按演出日期排序并得到:
对象标识 | 用户身份 | 审核权重显示计数 | 审核权重点赞数 | 目标(被喜欢) |
---|---|---|---|---|
1 | 1 | 12 | 3 | 可能不会 |
1 | 2 | 15 | 3 | 也许是吧 |
1 | 3 | 16 | 4 |
当我发现第一个这样的例子时,我感到很惊讶,结果证明我的预测并没有实现。 但是,考虑到对象内这些特征的最大值有所增加,我们并不懒惰,决定寻找 审核权重显示计数下一个 и 审核权重点赞数下一个,即下一时刻的值。 通过添加一个功能
(auditweightsShowsCountNext-auditweightsShowsCount)/(auditweightsLikesCount-auditweightsLikesCountNext) 我们迅速跳跃。
通过查找以下值可以使用类似的泄漏 用户所有者计数器创建喜欢 在 userId+ownerId 内,例如, 审计权重Ctr性别 在 objectId+userGender 内。 我们发现了 6 个类似的泄漏字段,并从中提取了尽可能多的信息。
到那时,我们已经从协作特征中挤出了尽可能多的信息,但没有回到图像和文本竞赛。 我有一个好主意想检查一下:直接基于图像或文本的特征在相关竞赛中能给出多少分数?
图片和文字比赛都没有出现泄漏,但那时我已经返回了默认的 catboost 参数,清理了代码并添加了一些功能。 总计为:
解 | 很快 |
---|---|
最大图像 | 0.6411 |
最多无图像 | 0.6297 |
第二名成绩 | 0.6295 |
解 | 很快 |
---|---|
最多有文字 | 0.666 |
最大无文本 | 0.660 |
第二名成绩 | 0.656 |
解 | 很快 |
---|---|
最大程度的协作 | 0.745 |
第二名成绩 | 0.723 |
很明显,我们不太可能从文本和图像中榨取太多东西,在尝试了一些最有趣的想法后,我们停止了与它们的合作。
协作系统中进一步生成的功能并没有增加,我们开始排名。 在在线阶段,分类和排名集成给我带来了小幅提升,结果是因为我对分类训练不足。 包括 YetiRanlPairwise 在内的所有误差函数都没有产生接近 LogLoss 的结果(0,745 与 0,725)。 QueryCrossEntropy 还抱有希望,但无法启动。
线下阶段
离线阶段,数据结构保持不变,但有细微变化:
- 标识符 userId、objectId、ownerId 被重新随机化;
- 一些标志被移除,一些标志被重新命名;
- 数据增加了约1,5倍。
除了列出的困难之外,还有一个很大的优点:团队分配了一台带有 RTX 2080TI 的大型服务器。 我很喜欢htop很长时间了。
只有一个想法——简单地复制已经存在的东西。 在花了几个小时在服务器上设置环境后,我们逐渐开始验证结果是否可重现。 我们面临的主要问题是数据量的增加。 我们决定稍微减少负载并设置catboost参数ctr_complexity=1。 这会稍微降低速度,但我的模型开始工作,结果很好 - 0,733。 Sergey 与我不同,他没有将数据分成两部分并对所有数据进行训练,尽管这在在线阶段给出了最好的结果,但在离线阶段却遇到了很多困难。 如果我们采用生成的所有功能并尝试将它们推入 catboost,那么在线阶段什么都不起作用。 Sergey 做了类型优化,例如,将 float2 类型转换为 float64。
这些结果足以获胜,但我们隐藏了我们的真实速度,无法确定其他团队是否也在做同样的事情。
战斗到最后一刻
Catboost 调整
我们的解决方案被完全复制,我们添加了文本数据和图像的特征,所以剩下的就是调整 catboost 参数。 Sergey 在 CPU 上进行了少量迭代的训练,而我在 ctr_complexity=1 的 CPU 上进行了训练。 还剩一天,如果您只是添加迭代或增加 ctr_complexity,那么到早上您可以获得更好的速度并步行一整天。
在离线阶段,通过简单地选择不是网站上的最佳解决方案,可以很容易地隐藏速度。 我们预计在提交结束前的最后几分钟排行榜会发生巨大变化,因此决定不再停止。
从Anna的视频中我了解到,要提高模型的质量,最好选择以下参数:
- 学习率 — 默认值是根据数据集的大小计算的。 增加learning_rate需要增加迭代次数。
- l2_leaf_reg — 正则化系数,默认值3,最好从2到30中选择。减小该值会导致过拟合增加。
- 装袋温度 - 将随机化添加到样本中对象的权重中。 默认值为 1,其中权重从指数分布中得出。 减小该值会导致过度拟合的增加。
- 随机强度 — 影响特定迭代中分割的选择。 random_strength 越高,选择低重要性分割的机会就越高。 在随后的每次迭代中,随机性都会降低。 减小该值会导致过度拟合的增加。
其他参数对最终结果的影响要小得多,所以我没有尝试选择它们。 在 ctr_complexity=1 的 GPU 数据集上进行一次迭代训练需要 20 分钟,并且缩减数据集上所选的参数与完整数据集上的最佳参数略有不同。 最后,我对 30% 的数据进行了大约 10 次迭代,然后对所有数据进行了大约 10 次迭代。 结果是这样的:
- 学习率 我比默认增加了40%;
- l2_leaf_reg 保持原样;
- 装袋温度 и 随机强度 减少到0,8。
我们可以得出结论,该模型在默认参数下训练不足。
当我看到排行榜上的结果时,我非常惊讶:
是否提供样品 | 1模型 | 2模型 | 3模型 | 合奏 |
---|---|---|---|---|
无需调音 | 0.7403 | 0.7404 | 0.7404 | 0.7407 |
带调音 | 0.7406 | 0.7405 | 0.7406 | 0.7408 |
我自己得出的结论是,如果不需要快速应用模型,那么最好将参数的选择替换为使用非优化参数的多个模型的集合。
Sergey 正在优化数据集的大小以在 GPU 上运行。 最简单的选择是切断部分数据,但这可以通过多种方式完成:
- 逐渐删除最旧的数据(二月初),直到数据集开始适合内存;
- 删除重要性最低的特征;
- 删除只有一个条目的 userId;
- 仅保留测试中的 userId。
最终,将所有选项组合成一个整体。
最后的合奏
到最后一天深夜,我们已经建立了一个模型集合,结果为 0,742。 一夜之间,我用 ctr_complexity=2 启动了模型,而不是 30 分钟,而是训练了 5 个小时。 凌晨 4 点才开始统计,我制作了最后一个合奏,在公共排行榜上的得分为 0,7433。
由于解决问题的方法不同,我们的预测并没有很强的相关性,这使得整体有了很好的增长。 为了获得良好的集成,最好使用原始模型预测预测(prediction_type='RawFormulaVal')并设置scale_pos_weight=neg_count/pos_count。
在网站上你可以看到
其他方案
许多团队都遵循推荐系统算法的规范。 我不是这个领域的专家,无法评估它们,但我记得两个有趣的解决方案。
尼古拉·阿诺欣的解决方案 。 Nikolay 作为 Mail.ru 的员工,没有申请奖品,因此他的目标不是实现最大速度,而是获得易于扩展的解决方案。- 评审团获奖团队的决定基于
这篇文章来自脸书 ,无需手动工作即可实现非常好的图像聚类。
结论
我印象最深刻的是:
- 如果数据中有分类特征,并且您知道如何正确进行目标编码,那么最好尝试 catboost。
- 如果您正在参加比赛,您不应该浪费时间选择学习率和迭代之外的参数。 更快的解决方案是制作多个模型的集成。
- Boostings 可以在 GPU 上学习。 Catboost 在 GPU 上可以非常快速地学习,但它会占用大量内存。
- 在思路的开发和测试过程中,最好设置一个小的rsm~=0.2(仅限CPU)和ctr_complexity=1。
- 与其他团队不同,我们的模型整体有了很大的提高。 我们只是交换想法并用不同的语言写作。 我们采用了不同的方法来分割数据,我认为每种方法都有自己的错误。
- 目前尚不清楚为什么排名优化的表现比分类优化差。
- 我获得了一些处理文本的经验,并了解了推荐系统的制作方式。
感谢组织者给予我们的情感、知识和奖品。
来源: habr.com