返回列表

2nd place solution

674. Jigsaw - Agile Community Rules Classification | jigsaw-agile-community-rules

开始: 2025-07-23 结束: 2025-10-23 内容安全 数据算法赛
第二名解决方案 - Jigsaw Agile Community Rules

第二名解决方案

作者: ^^_AI (yaoqingjin)

排名: 第 2 名

发布时间: 2025 年 11 月 2 日

团队成员: ^^_AI, Sophia Yang, Jiaming Zhang, Yang, hoatha

首先,我要真诚地感谢 Kaggle 和 Jigsaw 团队组织了这次非常有趣的比赛,并开放了一个真实且具有高价值的问题场景。这项任务不仅仅是一个“分类竞赛”。它更像是直接置身于一个非常嘈杂的真实世界社区审核环境中,那里的规则可能会变化,甚至标签也不总是可信的。

我也要感谢我的队友。在方案设计、实施细节、调试,甚至是笔记本版本管理等琐碎工作方面,我的队友给了我非常直接有效的建议和 sanity checks(合理性检查)。他们帮助我避免浪费大量时间。特别是在最后的冲刺阶段,当我们讨论如何并行化训练和推理,以及如何主动避免 Kaggle 的随机性问题(如 "Notebook Threw Exception" 或 "Time Out")时,这种协作绝对是至关重要的。

我也想解释一下为什么我现在才发布这篇 write-up。在过去的两周里,我的实际工作非常忙碌。白天大部分时间都在赶项目截止日期,只能在周末晚上清理代码和日志。所以这份报告延迟到了现在,周日晚上。


方法概述

jigsaw-llama3-1-8b-instruct-training-one-epoch 基本上是我们方法的起点。非常感谢 @mks2192。它证实了一个非常重要的事实:unsloth 非常适合在 Kaggle 环境中进行在线 LoRA 微调,并且仍然可以在推理时保持非常高的吞吐量。我们后来的管道本质上是在此基础上的迭代演进。

我们最终的提交是一个多模型集成的 LLM 判别系统。核心思想是:

  1. 我们将此任务视为一个指令微调问题,看起来像“LLM 读取一条规则和一条评论,然后只回答 Yes 或 No",采用对话格式。
  2. 我们显式地对嘈杂的标注进行标签清洗和去重,以提高监督信号的可靠性。
  3. 我们故意防止模型依赖 subreddit 字段,因为 subreddit 的偏差和污染非常严重。
  4. 我们使用多个不同的基础大语言模型,来自不同的模型家族且具有不同的大小,并使用 unsloth 在 Kaggle 上用 LoRA 对其进行微调。
  5. 在推理期间,我们通过检查 Yes 和 No 之间的下一个 token 的 logit 差异来获取概率分数。
  6. 我们对所有模型预测应用简单但 robust 的加权融合。

1. 本地验证策略

在比赛早期,我们发现在线学习导致公共 leaderboard 分数波动非常大,并且有许多意外问题。例如,完全相同的 notebook 有时能运行通过,有时却立即抛出 Exception。这意味着仅依靠每天的五次提交让我们感到非常不安全。

在训练数据上,我们创建了一个按标签分层的 StratifiedKFold 分割,且 val_ratio = 0.1。换句话说,我们对二元目标 rule_violation 进行了分层分割,并取了大约 10% 的样本作为本地验证集。每次我们更改方法时,都会使用三个随机种子上的平均交叉验证分数对其进行评估。我们计算了本地 AUC 并将其作为离线指标。

这个稳定的本地交叉验证分数实际上是所有后续决策的锚点。


2. 标签清洗

官方训练数据存在一个实际问题。相同的 body(评论文本)与相同的 rule(规则文本)配对时,有时在不同地方被标记为不同的 rule_violation 值。换句话说,完全相同的评论和规则对有时被标记为违规,有时被标记为不违规。如果我们直接在这些冲突样本上进行训练,LLM 将学习到“这个问题没有标准答案”,从而导致监督信号发散。

我们的清洗过程是:

  1. 我们将每一行压缩为一个 key。我们尝试了两种变体:
    • "body || rule || subreddit || rule_violation"
    • 以及一个更激进的版本:"body || rule || rule_violation",这意味着我们忽略 subreddit
    • 最终我们的主要选择是忽略 subreddit。下面的第 3 节解释了为什么 subreddit 是有问题的。
  2. 对于相同的 (body_trim, rule_trim) 对,如果我们观察到冲突的标签,我们计算 rule_violation 为正的次数与总次数的比例。
    • 我们采取多数投票作为最终标签。
  3. 我们用这个多数标签填充冲突的行。
  4. 然后我们去重。

这种清洗有两个主要好处:

  • 我们获得了更干净的监督信号。LLM 在监督微调期间不再被训练去自相矛盾。
  • 我们获得了更高的每样本质量。在冲突解决和去重之后,数据集大小实际上减少了。然而,每个剩余样本变得更加可信。对于 LoRA 这种低秩适应方法来说,更少但更干净的样本比更多但嘈杂的样本更有价值,特别是因为我们只有训练一个 epoch 的预算。

3. 故意丢弃 Subreddit 特征

我们运行了一个小型探测,发现 subreddit 字段在训练数据中通常携带强烈的社会偏差信号。相同的规则和相同的行为可以根据 subreddit 被判断为“违规”或“不违规”。标准、语气和容忍程度完全不同,有时甚至混乱。

对于判别式 LLM,subreddit 很容易成为 shortcut feature(捷径特征)甚至是 leakage feature(泄露特征)。

最终我们决定了以下主要方法。我们从 prompt 中移除了 subreddit,只保留了 Rule:Comment:

Prompt 基本上看起来像这样:

System:
"判断 Reddit 评论是否违反规则。严格只回答 Yes 或 No。"

User:
Rule: <规则文本>
Comment: <评论文本>

Assistant:
Yes / No

4. 多模型并行微调与集成

4.1 模型家族

最终我们训练并提交了四个 LoRA 微调版本,涵盖了不同的规模和不同的 LLM 架构:

  1. phi-4-14b 加载 bnb 4-bit 量化
  2. qwen3-14b 加载 bnb 4-bit
  3. qwen3-8b 加载 bnb 4-bit
  4. qwen25-7b-instruct 加载 bnb 4-bit

所有这些模型都是使用 unsloth.FastLanguageModel.from_pretrained(...) 加载 4-bit 量化权重,然后通过 LoraConfig 应用 LoRA。我们使用 trl.SFTTrainer 进行一个 epoch 的监督微调。

4.2 Kaggle 上的实际工程问题(GPU 并行和重运行不稳定性)

在最后的冲刺阶段,我们需要端到端地运行所有四个模型。这包括训练、推理、生成 submissionX.csv,然后融合预测。这非常接近 Kaggle 的资源限制。最终提交的总运行时间为 11 小时。

我们做了两件事工程上的事情使其成为可能:

(1) 双 GPU 并行调度
我们编写了一个名为 all_train.sh 的脚本,并将模型分为两组。我们在不同的 CUDA_VISIBLE_DEVICES 下启动它们,以便两个进程可以同时运行。

这种方法使我们能够将总运行时间保持在 Kaggle 仍可接受的范围内。

(2) 现实情况:“相同的 Notebook 有时成功有时崩溃”
我们看到了一个非常令人沮丧的 Kaggle 重运行现象。完全相同的 notebook 版本有时能完成并产生分数,有时却立即抛出 Notebook Threw ExceptionTime Out。我们的假设是 Kaggle 并不总是为每次提交分配完全相同的底层硬件资源。

我们的缓解策略是:

  • 将训练 epoch 数固定为 1。
  • 选择一个“足够高但不会导致 OOM"的 batch size,例如 per_device_train_batch_size = 4 或 8,然后使用 gradient_accumulation_steps 来控制有效的全局 batch size。
  • 在推理期间,我们使用 bs=4bs=per_device_train_batch_size*2,这些都是安全值,而不是 pushed 到极大的 batch sizes。

换句话说,我们故意调整脚本以使用大约 80% 到 90% 的可用资源,而不是 pushed 到 100% 并冒着 out of memory 错误的风险。这使得它在波动的环境下更有可能存活下来。


再次感谢 Kaggle、Jigsaw 团队以及所有讨论和分享 baseline 的竞争对手。社区审核和内容政策判断这个领域是真实的、困难的,甚至有点混乱,这正是它值得继续工作的原因。

同比赛其他方案