674. Jigsaw - Agile Community Rules Classification | jigsaw-agile-community-rules
首先,我要真诚地感谢 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 判别系统。核心思想是:
在比赛早期,我们发现在线学习导致公共 leaderboard 分数波动非常大,并且有许多意外问题。例如,完全相同的 notebook 有时能运行通过,有时却立即抛出 Exception。这意味着仅依靠每天的五次提交让我们感到非常不安全。
在训练数据上,我们创建了一个按标签分层的 StratifiedKFold 分割,且 val_ratio = 0.1。换句话说,我们对二元目标 rule_violation 进行了分层分割,并取了大约 10% 的样本作为本地验证集。每次我们更改方法时,都会使用三个随机种子上的平均交叉验证分数对其进行评估。我们计算了本地 AUC 并将其作为离线指标。
这个稳定的本地交叉验证分数实际上是所有后续决策的锚点。
官方训练数据存在一个实际问题。相同的 body(评论文本)与相同的 rule(规则文本)配对时,有时在不同地方被标记为不同的 rule_violation 值。换句话说,完全相同的评论和规则对有时被标记为违规,有时被标记为不违规。如果我们直接在这些冲突样本上进行训练,LLM 将学习到“这个问题没有标准答案”,从而导致监督信号发散。
我们的清洗过程是:
"body || rule || subreddit || rule_violation""body || rule || rule_violation",这意味着我们忽略 subreddit(body_trim, rule_trim) 对,如果我们观察到冲突的标签,我们计算 rule_violation 为正的次数与总次数的比例。
这种清洗有两个主要好处:
我们运行了一个小型探测,发现 subreddit 字段在训练数据中通常携带强烈的社会偏差信号。相同的规则和相同的行为可以根据 subreddit 被判断为“违规”或“不违规”。标准、语气和容忍程度完全不同,有时甚至混乱。
对于判别式 LLM,subreddit 很容易成为 shortcut feature(捷径特征)甚至是 leakage feature(泄露特征)。
最终我们决定了以下主要方法。我们从 prompt 中移除了 subreddit,只保留了 Rule: 和 Comment:。
Prompt 基本上看起来像这样:
System:
"判断 Reddit 评论是否违反规则。严格只回答 Yes 或 No。"
User:
Rule: <规则文本>
Comment: <评论文本>
Assistant:
Yes / No
最终我们训练并提交了四个 LoRA 微调版本,涵盖了不同的规模和不同的 LLM 架构:
phi-4-14b 加载 bnb 4-bit 量化qwen3-14b 加载 bnb 4-bitqwen3-8b 加载 bnb 4-bitqwen25-7b-instruct 加载 bnb 4-bit所有这些模型都是使用 unsloth.FastLanguageModel.from_pretrained(...) 加载 4-bit 量化权重,然后通过 LoraConfig 应用 LoRA。我们使用 trl.SFTTrainer 进行一个 epoch 的监督微调。
在最后的冲刺阶段,我们需要端到端地运行所有四个模型。这包括训练、推理、生成 submissionX.csv,然后融合预测。这非常接近 Kaggle 的资源限制。最终提交的总运行时间为 11 小时。
我们做了两件事工程上的事情使其成为可能:
(1) 双 GPU 并行调度
我们编写了一个名为 all_train.sh 的脚本,并将模型分为两组。我们在不同的 CUDA_VISIBLE_DEVICES 下启动它们,以便两个进程可以同时运行。
这种方法使我们能够将总运行时间保持在 Kaggle 仍可接受的范围内。
(2) 现实情况:“相同的 Notebook 有时成功有时崩溃”
我们看到了一个非常令人沮丧的 Kaggle 重运行现象。完全相同的 notebook 版本有时能完成并产生分数,有时却立即抛出 Notebook Threw Exception 或 Time Out。我们的假设是 Kaggle 并不总是为每次提交分配完全相同的底层硬件资源。
我们的缓解策略是:
per_device_train_batch_size = 4 或 8,然后使用 gradient_accumulation_steps 来控制有效的全局 batch size。bs=4 或 bs=per_device_train_batch_size*2,这些都是安全值,而不是 pushed 到极大的 batch sizes。换句话说,我们故意调整脚本以使用大约 80% 到 90% 的可用资源,而不是 pushed 到 100% 并冒着 out of memory 错误的风险。这使得它在波动的环境下更有可能存活下来。
再次感谢 Kaggle、Jigsaw 团队以及所有讨论和分享 baseline 的竞争对手。社区审核和内容政策判断这个领域是真实的、困难的,甚至有点混乱,这正是它值得继续工作的原因。