【天池经典打榜赛】赛道一-淘宝穿衣穿搭赛
分享一下参加淘宝穿衣穿搭赛过程中的技术复盘。这是一个极具挑战性的多模态推荐任务
1. 赛题初探
1.1 比赛背景与任务
赛题的核心目标是:给定一件商品(如一件上衣),预测出最能与其组成“时尚套餐”的搭配商品(如裤子、鞋子、配饰)。
这听起来很像传统的“猜你喜欢”或“以图搜图”,但深入分析后我发现完全不同:
- 输入:单个商品(图像 + 文本 + 类目)。
- 输出:200 个互补商品列表。
- 核心难点:完全冷启动(Complete Cold Start)。
1.2 数据集处理
在对数据进行初步探索时,我发现:
- 测试集包含 5,462 个待预测商品。
- 训练集中有 23,105 个专家搭配套餐,涉及 60,916 个商品。
- 重叠度为 0。这意味着测试集里的每一个商品,在训练集中从未出现过。
这意味着协同过滤(Collaborative Filtering)、Item2Vec、GraphSAGE 等依赖 ID 交互历史的算法在这里完全失效。我必须构建一个纯粹基于**内容理解(Content-Based)**的模型。
1.3 评估指标
比赛使用平滑后的 MAP@200:
这个公式对排名的敏感度比传统 MAP 更温和,但依然要求我们将正确答案排在 Top 20 甚至 Top 50 以内。
2. 数据处理与清洗流程
面对 50 万量级的商品库和 50 万张图片,高效的数据流是训练的基础。
2.1 原始数据解析
原始数据是松散的文本格式。我编写了健壮的解析器(matchset_parser.py),处理了以下结构:
- Matchsets:
coll_id item_list,其中item_list包含了以分号分隔的搭配部位(Slot)和以逗号分隔的替补商品。 - Items:
item_id cat_id terms,其中terms是脱敏后的分词序列。
2.2 图像与文本预处理
- 图像:我扫描了 4 个分散的图片目录,建立了
item_id -> path的全局索引。预处理阶段,我将图片统一 Resize 到 224x224,并使用 ImageNet 的均值方差进行归一化。 - 文本:对
terms序列进行了截断(Max Length=50)和 Padding,构建了包含 20,002 个词的词汇表。
3. 特征工程:构建多模态表达
既然无法利用 ID 信息,特征工程就决定了模型的上限。
3.1 视觉特征 (Visual)
我选择了经典的 ResNet-50 作为骨干网络。并没有使用全连接层的输出,而是提取了 avgpool 层的 2048维 向量。这层特征包含了丰富的纹理、颜色和形状信息,是判断“这件衣服长什么样”的关键。
3.2 文本特征 (Textual)
虽然文本是脱敏的数字序列,但它依然蕴含了关键的语义(如“韩版”、“纯棉”)。我使用了一个 128 维的 Embedding Layer,结合简单的 Self-Attention 机制来聚合标题信息。
3.3 类目特征 (Categorical)
这是我后期优化的关键。类目 ID 不仅仅是一个属性,它是搭配规则的语法。我为其分配了独立的 64 维 Embedding 空间,让模型学习类目之间的隐式关系。
3.4 特征融合 (Attention Fusion)
为了让模型自动决定“看图”还是“看字”,我设计了一个 Attention Fusion 模块,动态计算各模态的权重,生成最终的 Item Representation。
4. 模型架构与演进
我选择了经典的 双塔架构 (Two-Tower Architecture),因为它能够高效地处理大规模检索任务。
4.1 第一版模型:严重的“自恋”倾向
最初,我训练了一个标准的双塔模型,Loss 仅仅是最大化正样本与随机负样本的距离。
结果令我大跌眼镜:模型给出的推荐列表里,90% 都是同类目商品!
- 查询:一件红上衣。
- 推荐:200 件红上衣。
原因分析:在冷启动场景下,模型发现“视觉相似”是最容易学的捷径。它还没学会“搭配”,只学会了“找同款”。
4.2 改进架构:GNN 的尝试与放弃
为了引入结构信息,我尝试了 GraphSAGE。但在完全冷启动场景下,测试节点是孤立的,无法聚合邻居信息,GNN 退化成了 MLP。这条路走不通。
4.3 最终突破:同类目惩罚与专家先验
我意识到必须强迫模型学习“类目互斥”规则。我在架构外围引入了两个强力补丁:
- 训练端:Hard Negative Sampling,强制采样同类目作为负样本。
- 推理端:专家先验过滤。
5. 训练策略的优化
5.1 损失函数的改造
我重写了 BPR Loss,加入了针对性的惩罚项:
1 | class BPRLoss(nn.Module): |
这个改动立竿见影,强迫模型将同类目商品的得分压低。
5.2 优化器与调度
- AdamW: 配合
weight_decay=0.05,强正则化防止模型死记硬背训练集。 - ReduceLROnPlateau: 配合
patience=8,给模型足够的耐心去跳出局部最优。
6. 推理阶段的“硬核”优化
仅仅靠模型学是不够的,我在推理阶段引入了基于规则的后处理,这是提升分数的杀手锏。
6.1 类目共现矩阵
我统计了训练集中所有专家搭配的 类目转移概率 。例如,如果 上衣(368) 有 20% 的概率搭配 裤子(111),而只有 0.01% 的概率搭配 鞋子(50)。
6.2 强制过滤 (Hard Filtering)
在生成推荐列表时,我执行了以下逻辑:
- 同类目一票否决:查询商品和候选商品同类目?直接剔除!
- 白名单机制:候选商品的类目不在专家的“高频搭配圈”里?剔除!
这一策略将推荐结果的同类目占比从 90% 降到了 0%,推荐列表瞬间变得合理且“懂行”。
7. 可视化与交互
为了不让算法变成黑盒,我用 FastAPI 开发了一个可视化界面。
- 功能:随机浏览商品、点击查看详情、实时生成推荐。
- 价值:通过可视化,我一眼就能看出“推荐了太多同款”这个问题,从而快速定位到了“自恋”的病根。
8. 总结与反思
这次比赛让我深刻体会到:
- 数据决定上限:在动手写模型前,必须彻底分析数据的分布(尤其是冷启动情况)。
- 不要迷信复杂模型:GNN 很强,但在孤立节点面前无能为力;反而是简单的“规则过滤”解决了大问题。
- End-to-End 的思考:算法工程师不能只管训练 Loss,必须关注最终的业务指标(MAP)和输出结果的逻辑合理性。