666. Make Data Count - Finding Data References | make-data-count-finding-data-references
感谢 Kaggle 和主办方举办此次比赛,事实证明这比我们最初预期的要具有挑战性得多。
我们的解决方案主要由两个步骤组成:
与其他团队类似,我们的检索过程在这里非常直接。我们使用简单的正则表达式和规则来提取 DOI 和 accession IDs 的候选引用,然后与 2024 年 DataCite 年度转储以及 EUPMC 挖掘术语语料库中的一些额外条目进行比较。此外,我们还从 Crossref 2025 转储中提取了论文元数据信息。
这个简单的方法足以获得接近 100% 的 DOI 召回率,同时保持 minimal 的假阳性。虽然我们尝试了不同的方法,但这最终被证明是最可靠的。除此之外,我们还根据我们在训练集和 LB 探测中观察到的情况应用了一些小的启发式规则。
如果我们在 PDF 解析文本中找不到引用,我们也会在 XML 中搜索 accessions 和 DOIs。否则,我们简单地忽略 XML 而仅依赖 PDF。
我们还不得不忽略一些总是假阳性的 accession 和 DOIs,例如:
figshare DOIsGCA_ accessions,这是假阳性的主要来源HGNC:*, rs* 以及其他几种类型最后,如果在同一篇文章中发现多个 accessions 和 DOIs,我们发现忽略 DOI 会在 LB 上带来提升。
一开始我们就知道类型分类是本次比赛中最重要的部分之一。原因是,由于我们使用 F1 分数作为指标,检索部分的 FN 或 FP 仅计为 1 个错误。然而,如果我们获得 TP 样本但错误分类了其类型,我们将得到 2 个错误:1 个 FN 用于缺失的正确类型,1 个 FP 用于错误的预测类型。
我们这里的解决方案主要包括两个步骤:6 折 Deberta-v3 集成和一些启发式规则。
与其他团队类似,我们首先使用了一些在 LB 和 CV 上都证明有效的规则:
dryad DOIs 和 SAMN accessions 都是 primary(主要)剩余的引用随后使用 Deberta-v3 Large 集成进行分类。
我们创建了一个 6 折 StratifiedGroupKFold(按 type 分层并按 article_id 分组),并在每个折上使用二元分类设置(顶部有分类头)训练了一个 deberta-v3 large。以下特征用于生成训练提示:
我们还尝试微调许多 LLM,如带有分类头的 Qwen2.5 7B,但事实证明这在训练过程中非常不稳定。Debertas 不仅更快,而且取得了更好的结果。
我们发现了两个非常重要的技巧来稳定训练:添加梯度裁剪和模型/权重 EMA。后者是一种非常简单的技术,包括通过 SGD/Adam 训练可训练模型,并在每个训练步骤后通过直接平均(加权)两个模型的权重来更新冻结的对应模型。使用 transformers 库,可以通过继承 Trainer 类轻松实现,如下所示:
from typing import Dict
import torch
from ema_pytorch import EMA
import copy
class EMATrainer(Trainer):
def __init__(self, ema_decay=0.9995, ema_update_every=1, *args, **kwargs):
super().__init__(*args, **kwargs)
# 模型设置后初始化 EMA
self.ema_decay = ema_decay
self.ema_update_every = ema_update_every
self.ema = None
def _setup_ema(self):
if self.ema is None:
self.ema = EMA(
self.model,
beta=self.ema_decay,
update_every=self.ema_update_every,
update_after_step=50 # 50 步后开始 EMA
)
def training_step(self, model, inputs, num_items_in_batch = None):
"""重写训练步骤以包含 EMA 更新"""
if self.ema is None:
self._setup_ema()
# 执行正常训练步骤
loss = super().training_step(model, inputs, num_items_in_batch=num_items_in_batch)
# 每一步后更新 EMA
self.ema.update()
return loss
def evaluate(self, eval_dataset=None, ignore_keys=None, metric_key_prefix="eval"):
"""使用 EMA 模型进行评估"""
if self.ema is not None:
# 暂时使用 EMA 模型进行评估
original_model = self.model
self.model = self.ema.ema_model
results = super().evaluate(eval_dataset, ignore_keys, metric_key_prefix)
# 恢复原始模型
self.model = original_model
return results
else:
return super().evaluate(eval_dataset, ignore_keys, metric_key_prefix)
def save_model(self, output_dir=None, _internal_call=False):
"""保存 EMA 模型"""
# 保存 EMA 模型
ema_output_dir = f"{output_dir}/ema_model"
if self.ema is not None and output_dir is not None:
self.ema.ema_model.save_pretrained(ema_output_dir)
else:
self.model.save_pretrained(ema_output_dir)
这两种技术虽然简单,但尽管训练数据集很小且 noisy,它们使训练曲线更加平滑。
在推理时,我们一次运行 2 个 DeBERTa 模型(每个 T4 GPU 上一个),并通过简单平均集成 6 个预测。
在我们的测试中,这个 6 折集成明显优于使用 0 样本模型。拥有分类头的另一个优势是能够随意调整阈值(直接或通过分位数)。
最后,我们要向以下人员致以谢意:
在我看来,像你们这样的人是 Kaggle 如此伟大的原因,也是我多年来能够学到这么多东西的原因。我不知道还有哪个地方的人们愿意在如此竞争激烈的环境中如此乐意分享和帮助彼此。