0%

日常学习【天池经典打榜赛】赛道一-淘宝穿衣穿搭赛

【天池经典打榜赛】赛道一-淘宝穿衣穿搭赛

分享一下参加淘宝穿衣穿搭赛过程中的技术复盘。这是一个极具挑战性的多模态推荐任务

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

Score=11+ln(Rank)\text{Score} = \frac{1}{1 + \ln(\text{Rank})}

这个公式对排名的敏感度比传统 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 最终突破:同类目惩罚与专家先验

我意识到必须强迫模型学习“类目互斥”规则。我在架构外围引入了两个强力补丁:

  1. 训练端:Hard Negative Sampling,强制采样同类目作为负样本。
  2. 推理端:专家先验过滤。

5. 训练策略的优化

5.1 损失函数的改造

我重写了 BPR Loss,加入了针对性的惩罚项:

1
2
3
4
5
6
7
8
9
class BPRLoss(nn.Module):
def __init__(self, penalty_weight=2.0):
# ...
def forward(self, pos_scores, neg_scores, neg_cats, anchor_cats):
# ...
# 如果最难负样本与 Anchor 是同类目,Loss 权重翻倍
if is_same_cat:
loss *= self.penalty_weight
return loss

这个改动立竿见影,强迫模型将同类目商品的得分压低。

5.2 优化器与调度

  • AdamW: 配合 weight_decay=0.05,强正则化防止模型死记硬背训练集。
  • ReduceLROnPlateau: 配合 patience=8,给模型足够的耐心去跳出局部最优。

6. 推理阶段的“硬核”优化

仅仅靠模型学是不够的,我在推理阶段引入了基于规则的后处理,这是提升分数的杀手锏。

6.1 类目共现矩阵

我统计了训练集中所有专家搭配的 类目转移概率 P(CatBCatA)P(Cat_B | Cat_A)。例如,如果 上衣(368) 有 20% 的概率搭配 裤子(111),而只有 0.01% 的概率搭配 鞋子(50)

6.2 强制过滤 (Hard Filtering)

在生成推荐列表时,我执行了以下逻辑:

  1. 同类目一票否决:查询商品和候选商品同类目?直接剔除!
  2. 白名单机制:候选商品的类目不在专家的“高频搭配圈”里?剔除!

这一策略将推荐结果的同类目占比从 90% 降到了 0%,推荐列表瞬间变得合理且“懂行”。


7. 可视化与交互

为了不让算法变成黑盒,我用 FastAPI 开发了一个可视化界面。

  • 功能:随机浏览商品、点击查看详情、实时生成推荐。
  • 价值:通过可视化,我一眼就能看出“推荐了太多同款”这个问题,从而快速定位到了“自恋”的病根。

8. 总结与反思

这次比赛让我深刻体会到:

  1. 数据决定上限:在动手写模型前,必须彻底分析数据的分布(尤其是冷启动情况)。
  2. 不要迷信复杂模型:GNN 很强,但在孤立节点面前无能为力;反而是简单的“规则过滤”解决了大问题。
  3. End-to-End 的思考:算法工程师不能只管训练 Loss,必须关注最终的业务指标(MAP)和输出结果的逻辑合理性。
-------------本文结束感谢您的阅读-------------