news 2026/9/23 23:29:04

CAIL2019相似案例匹配第二名方案详解:从数据清洗到BERT双塔精排

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CAIL2019相似案例匹配第二名方案详解:从数据清洗到BERT双塔精排

简介:法研杯2019相似案例匹配第二名解决方案,内含CAIL2020/2021司法考试赛道冠军团队代码与文档,面向法律NLP、机器学习及司法AI方向的开发者和参赛者,直击法律文本相似度匹配这一典型场景。压缩包共22个文件,包含6个Python脚本、3个Shell脚本、3个Dockerfile、3个Markdown文档等,整体仅192KB;Python脚本覆盖模型训练、预测与数据处理,Dockerfile用于复现运行环境,Markdown记录方案设计与实验结果,结构紧凑且便于对照学习。已有266人学习。整套方案完整呈现了从分词、特征工程、BERT类预训练模型到模型调优与融合的实践链路,同时随附数据集与说明文档,可直接复现榜单第二名的处理思路,也可迁移至相似案例检索、辅助判决等司法AI应用中。

1. 法研杯2019相似案例匹配为什么值得细读:这份第二名方案解决什么问题

CAIL2019 法研杯的相似案例匹配赛道,任务看起来非常聚焦:给定三篇裁判文书 A、B、C,其中 A 与 B、A 与 C 各组成一对候选,让模型判断哪一对才是语义上真正匹配的案例。做过之后才知道,裁判文书动辄几千字,所谓“匹配”不只是案由相同,而是争议焦点、法条引用、裁判结论的综合相似,越做越觉得这是一道被低估的法律 NLP 硬骨头。当年榜单第二名的方案,价值不在模型堆得多大,而在于把数据清洗、三元组训练目标、难例挖掘、在线评测对齐整条链路完整梳理了一遍,附带的标注数据集和文档更是把复现周期从三个月压缩到一两周。这篇笔记就把这条链路从头拆一遍,适合正在做 CAIL 历届赛道、法律文本匹配或长文档相似度的人照着实践。

2. 把任务和数据拆开:三元组结构、评测公式、数据集的真实样子

动手写模型之前,先把数据和评测吃透。这套方案附带的数据集和文档里,最容易被忽略的一点是:相似案例匹配不是一个普通文本分类任务,而是互斥三元组下的相对比较任务。很多人把它当分类跑,准确率长期卡在 73% 附近,问题往往出在这个任务理解上。

2.1 每条样本是一个互斥三元组,不是一对文书

官方数据的每条样本由四部分组成:A 是 query 文书,B 和 C 是两份候选文书,label 表示 A 和哪一份更匹配。严格来说,这叫二路互斥三元组。

{ "id": "SCM000312", "a": "原告张某诉被告李某民间借贷纠纷一案,本院认为,双方借贷关系成立,被告应偿还本金及利息……", "b": "原告王某诉被告陈某买卖合同纠纷一案,本院认为,合同合法有效,被告应支付货款……", "c": "原告刘某诉被告赵某民间借贷纠纷一案,本院认为,双方借贷关系不成立,驳回原告诉讼请求……", "label": 0 }

label 为 0 表示 A 和 B 匹配,为 1 表示 A 和 C 匹配。这个互斥结构决定了模型只有两个输出候选,没有“都不匹配”的第三态。B 和 C 之所以看起来都跟 A 沾边,是官方刻意设计的:干扰项常与 A 同属一个大案由,但裁判结论不一致,或者事实相似但引用的法条不同。

这个设计直接影响建模思路:单看 A 与 B 的绝对相似度、A 与 C 的绝对相似度都不够,必须让两个候选的分数在同一个目标函数里互相比较。我第一次直接算两个 pair 的余弦相似度再各自排序,验证集准确率只有 74%,改成显式相对比较后才过 80%。差距不在模型复杂度,而在任务形式的理解上。

2.2 加载数据时的三个习惯

围绕三元组结构,数据加载阶段有三个习惯我保留至今。它们看着不起眼,但每一条都能省掉后面的排查时间:

def load_scm_data(path): samples = [] with open(path, "r", encoding="utf-8") as f: for line in f: obj = json.loads(line.strip()) samples.append({ "id": obj["id"], "a": obj["a"], "b": obj["b"], "c": obj["c"], "label": obj.get("label", None), }) return samples

第一,一次读入完整三元组,不要拆成两个独立 pair 存成两份数据。拆开会让模型训练时看不见另一个候选的存在,学习目标就从“AB 比 AC 更匹配”降级成“AB 是否匹配”,后者是依赖阈值的主观判断,前者才是官方标签真正定义的相对比较。

第二,test 集可能没有 label 字段,读取时用 get 方法兜底,避免加载到一半报 KeyError。第三,用 id 做去重。同一篇文书出现在多个三元组是正常的,但如果同一份文本被重复写进同一份集合,模型会对特定字符串过拟合,本地指标虚高,官网上线就现原形。

2.3 评测指标:准确率是主指标,别被虚高的分数骗了

CAIL2019 相似案例匹配赛道的主评测指标是三元组级准确率,评测代码本质上就是统计预测与标签的比对正确率。

def scm_accuracy(y_true, y_pred): correct = 0 for label, pred in zip(y_true, y_pred): if label == pred: correct += 1 return correct / len(y_true)

代码没有可调参数,但指标形式是相对比较,预测必须是 0 或 1。真正埋坑的是样本不均衡:官方整体上正负样本均衡,但落到特定案由子集里会有偏斜,比如部分案由下 label=0 的占比可能到 63%。只看整体准确率,模型只需把这些样本全预测成 0 就能刷分。所以我本地验证时额外记录二分类 F1 和正类召回率,当 F1 显著低于准确率 10 个点以上,说明模型偏向高频标签,需要重新平衡训练目标。

2.4 数据划分:按三元组分案由切,别按单篇文书切

官方把数据拆成了训练集、验证集和测试集,但本地复现常常需要再从训练集里划一份开发集做快速迭代。划分的核心原则是:以三元组为最小单位,避免同一篇文书跨集合出现。

我的常见做法是:

  1. 统计每个三元组的案由大类,作为分层标签;
  2. 按 8:1:1 在训练数据上划分 train/dev/valid;
  3. 切分后检查 dev 里是否有与 train 重合的文书文本,有就人工剔除;
  4. 切分逻辑封装成函数,固定 seed,保证每次实验数据顺序可追溯。

以文本去重为例,如果没做这一步,train 里出现了 dev 中同一段“本院认为”,模型等于提前偷看了答案,本地验证虚高 2 个点以上,最后在官方测试集上都会还回去。文件层面,我习惯把处理结果落成三份 tsv,每行五列:id、text_a、text_b、text_c、label,valid 和 test 的 label 位用 -1 占位。这样换模型时加载逻辑不用动,错误也容易被发现。

3. 从零复现第二名方案:预处理细节、基线模型到 BERT 精排的完整路径

这一章按“先跑通再调优”的顺序展开:先做文本清洗和段落加权,再用 TextCNN 把 pipeline 完整跑通,最后换 BERT 双塔精排。每一阶段都有明确的验收标准,出了问题好定位。

3.1 文书清洗的两个原则:去噪声、保语义

裁判文书来自结构化文本,但转存过程中会混入页眉页脚、案号、编号和冗余空白。清洗时我遵循两个原则:去掉不表达语义的格式噪声,保留可能作为案由和法条线索的实体词。

import re def clean_legal_doc(text: str) -> str: text = re.sub(r"\s+", "", text) text = re.sub(r"[【】\[\]]", "", text) text = re.sub(r"^案号[::].*", "", text) return text

这段正则依次做三件事:压缩所有空白、去除方括号、去掉开头的案号行。案号行虽然能当特征用,但它更像数据版本标记,同一个案子在不同版本里编号可能不同,保留反而干扰泛化。至于“原告”“被告”这类词,我刻意保留:它们承载当事人角色,对跨文书实体对齐有隐性帮助。清洗最怕把这类词也一并删掉,让模型看到的是缺胳膊少腿的文书。

清洗后的下一层是分段加权。裁判文书的语义密度分布极不均匀,“本院认为”之后的段落是裁判理由核心,第一段往往是当事人信息列表,前后信息量差距很大。常见做法是用锚点词把文书切成若干段,把段落序号作为 segment id 拼进输入。这层处理对 TextCNN 的增益尤其明显,能在基线上提升约 2 个点,因为 CNN 没有注意力机制,必须靠输入侧喂显式位置信号。

3.2 基线模型:TextCNN 负责验证数据管道

所有复现的第一步,我都建议先用类 TextCNN 模型跑通全流程。这也是方案文档里强调的工程节奏:先确认数据没问题,再上复杂模型。TextCNN 训练快,能在半天内暴露字段错位、label 反转、集合重叠等数据问题。

class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=200, num_filters=256, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in (2, 3, 4) ]) self.classifier = nn.Linear(num_filters * 3, num_classes) def forward(self, x): emb = self.embedding(x).transpose(1, 2) pooled = [] for conv in self.convs: out = torch.relu(conv(emb)) pooled.append(torch.max_pool1d(out, out.size(2)).squeeze(2)) feats = torch.cat(pooled, dim=1) return self.classifier(feats)

卷积核窗口取 2、3、4,对应中文语境里的二字词、三字词、四字词片段,max-pooling 取每个窗口在序列上的最强响应,最后拼成 768 维句向量再过全连接。embed_dim 200、num_filters 256 是效果与显存占用比较平衡的组合,padding_idx=0 保证 padding 位不参与训练。

这里的关键前提是输入用“字”而不是“词”做基本单位。中文分词在法律文本上很容易翻车,“民间借贷纠纷”一旦被错误切成“民间借 / 贷纠 / 纷”,CNN 的局部 n-gram 特征就被破坏了。我直接按字切分,字符级 n-gram 天然覆盖这些词组,避开分词错误这个天花板。TextCNN 的定位不是冲刺高分,而是用十几分钟一个 epoch 的速度,把数据管道的验收工作做完,确认无误后再上 BERT。

3.3 精排模型:BERT 双塔共享权重与差向量打分

第二步用 BERT 双塔结构做精排。我对比过 A+B+C 三输入拼接和双塔两种方案:拼接方案能让 self-attention 直接看到三篇文书的交互,但序列长度接近三倍,训练与推理都慢,而且 A 的向量无法复用;双塔方案牺牲部分交互,换来速度,并支持缓存重复出现的 A。

最终选双塔共享权重,再补一组差向量特征,用交互信息弥补结构短板:

from transformers import AutoModel, AutoTokenizer class SCMBert(nn.Module): def __init__(self, model_name="bert-base-chinese"): super().__init__() self.encoder = AutoModel.from_pretrained(model_name) self.scorer = nn.Linear(768 * 3, 1) def encode(self, input_ids, attention_mask): outputs = self.encoder(input_ids=input_ids, attention_mask=attention_mask) return outputs.last_hidden_state[:, 0] def forward(self, a_ids, a_mask, b_ids, b_mask, c_ids, c_mask): a_vec = self.encode(a_ids, a_mask) b_vec = self.encode(b_ids, b_mask) c_vec = self.encode(c_ids, c_mask) ab_feat = torch.cat([a_vec, b_vec, a_vec - b_vec], dim=-1) ac_feat = torch.cat([a_vec, c_vec, a_vec - c_vec], dim=-1) ab_score = self.scorer(ab_feat) ac_score = self.scorer(ac_feat) return torch.cat([ab_score, ac_score], dim=-1)

编码器取最后一层 [CLS] 向量作为整篇文书表示。差向量 a_vec - b_vec 把两个文书的语义差异直接暴露给打分层:如果只用拼接,模型要从 1536 维里隐式还原差异信息,收敛慢且容易学偏;差向量作为显式特征让打分层直接把注意力放在差异上。共享权重意味着 B 和 C 共用同一个编码器,不会因候选位置学到两套打分词。

推理时 A 的向量可以缓存。测试集里同一篇 A 常对应多个三元组,第一次编码后把 a_vec 存下来,后续直接复用,整体推理时间能省接近一半。需要留意的是,缓存只在一对多的场景有效,如果测试集 A 几乎唯一,收益就很小。

3.4 损失函数与训练参数:照着这张表先跑

损失函数推荐交叉熵,把 ab_score 和 ac_score 拼成两个 logits 和 label 做二分类。不用 triplet loss,理由很实际:三元组里 label 明确指代“哪个候选更匹配”,交叉熵梯度形式直观,调试也容易定位。

参考训练参数表:

参数推荐值说明
max_len256先跑通再考虑加长,长文本收益不高
batch_size1616GB 显存跑双塔不爆显存
epoch3BERT 微调轮次,再多容易过拟合
learning_rate2e-5主干用 2e-5,打分层可放宽到 1e-4
warmup_ratio0.1前 10% 步数预热,让打分层先稳住
weight_decay0.01只约束非 LayerNorm 参数
max_grad_norm1.0梯度裁剪,防长文书样本带飞梯度

max_len 是第一个要动手调的参数。裁判文书是“两头重、中间长”,前 256 字覆盖案由和当事人,后 256 字覆盖裁判理由结尾,但“本院认为”段落在第 512 字之外也很常见。我把 3.1 的分段加权和截断结合:先定位“本院认为”,如果超出截断窗口,就把文书尾部的裁判理由接到窗口里。这种有偏截断比简单截前 256 字稳定可靠得多,是原方案比较细节的一处处理。

warmup_ratio 0.1 意味着前 10% 的训练步数把学习率从 0 线性升到 2e-5。这个参数必须开,因为打分层是随机初始化,一上来就大学习率会把预训练分布破坏掉。我关掉 warmup 做过对照实验,loss 降得很慢,准确率掉了 2 个点。max_grad_norm 从 1.0 起步,遇到个别超长文书样本时能防止一个 batch 把参数整体带偏。

3.5 难例挖掘:为什么它比调参更值钱

训练完成后,把 dev 集上预测错误或 logits 差值小于 0.1 的样本收集起来,重新加入训练集,再微调一轮。这就是难例挖掘在相似案例匹配上的具体用法。

原因在于官方数据里的 B、C 大多是普通干扰项,模型很容易学对;真正难的是那些 B、C 都和 A 高度相似的样本。把难例挑出来重训,等于让模型把有限的训练容量花在最难区分的边界上。方案里专有一个难例池,配合双塔结构做二次训练。我复现时发现难例挖掘的收益甚至超过把 max_len 从 256 调到 512,这是性价比最高的单项优化。

4. 避坑与排查:复现相似案例匹配方案时的 5 个高频坑

复现榜单方案最常见的问题不是模型跑不起来,而是跑起来之后指标不对劲。这一章按“现象、原因、解决”的顺序写五条经验,每一条都能对应到具体排查动作,省得你在黑匣子里瞎猜。

4.1 训练集 99%,验证集只有 74%:模型把长度差学成了捷径

现象:训练 loss 降得很快,验证准确率停滞在 74% 上下。抽样看错误样例,发现模型把字数更多的候选一律判为匹配;把 B、C 长度故意对调,预测跟着对调。

原因:三元组 B、C 的长度分布并不均匀,label 和长度差之间有偶然相关性。BERT 对长序列后半段关注天然不足,长度特征被模型当成最简单可靠的分类信号。这是数据集偏置进了模型,不是过拟合。

解决:先做统计检验,计算长度差与 label 的相关系数。

import numpy as np lens_b = [len(s["b"]) for s in train_samples] lens_c = [len(s["c"]) for s in train_samples] labels = [s["label"] for s in train_samples] corr = np.corrcoef(np.array(lens_b) - np.array(lens_c), np.array(labels))[0, 1] print(corr)

相关系数超过 0.2 就说明数据里有明显的长度捷径。我的处理是把 B、C 截断到同一固定长度,同时把长度差作为显式特征打进打分层,让模型看得见这个维度而不是隐式瞎猜。处理后验证集回升约 3 个点。这个坑的标志性信号就是“训练异常好、验证拉胯”,下次遇到先查长度分布,别急着换更大的模型。

4.2 加大 max_len 后训练时间翻倍,准确率反而下降

现象:把 max_len 从 256 加到 512,想给模型多喂上下文,结果准确率掉了近 2 个点,训练时间从 5 小时变成 11 小时。

原因:文书中间段落大量是事实描述,篇幅长但语义权重低。序列加长后,这堆噪声把 [CLS] 的注意力稀释了,模型更难聚焦“本院认为”和“判决结果”。同时超出预训练长度范围的位置编码能力本就变弱,加长输入的增益没有想象中多。

解决:用锚点截断替代无脑加长。先定位“本院认为”,如果超出 max_len 就把窗口后移,让尽可能多的裁判理由进输入;如果锚点本身靠前,再考虑保留前 256 字加后 256 字。这个方案在 dev 上比单纯前 512 字高约 1 个点,训练开销还不增加。从此我得到一个习惯:长文本任务先看信息密度分布再定截断策略,别把钱花在序列长度上。

4.3 两个候选得分太接近,预测全靠猜

现象:ab_score 和 ac_score 的差值集中在 0.05 以内,dev 上低置信度样本占三成,这部分准确率接近随机,整体指标被拖住。

原因:双塔结构里 A、B、C 三篇文书编码相互独立,遇到 B、C 都和 A 很像的样本时,打分层拿到的差向量几乎一样,模型分不出细微差别。这是结构层面的信息瓶颈,不是 loss 没收敛。

解决:三管齐下。第一,打分层特征里加入 b_vec - c_vec,让模型直接比较两个候选的语义差异;第二,把 dev 低置信度样本送回训练集做难例挖掘;第三,把 A 与 B、A 与 C 的 BM25 分数作为外部特征拼进差向量后面,让打分层参考传统检索信号。做完这三步,低置信度样本占比明显下降。遇到这种情况我不再折腾 warmup 和学习率,结构问题用结构手段解决。

4.4 固定 seed 之后结果还是对不上,先分清是 bug 还是噪声

现象:同一个脚本在 A 机器上跑出 85%,换到 B 机器同样 seed 只跑出 83.5%,逐字对齐代码也找不到问题。

原因:GPU 型号不同,浮点运算归约顺序不同,池化、LayerNorm 的结果存在微小差异,这在 BERT 微调任务里几乎无法彻底消除。此外 transformers 版本变更会导致分词结果和位置编码细节变化,也是复现不一致的高频来源。

解决:把 torch、transformers、CUDA 版本和 GPU 型号记进实验配置。验证方案有效性时允许 0.5 到 1 个点的波动,方向一致就算复现成功。做严格模型对比时必须保持同一环境、同一 batch_size、同一数据顺序,不要跨机器直接对比。我把环境版本写进项目 README 的习惯,就是从这类翻车里养成的。

4.5 线上评测比本地低 2 个百分点,三个隐式差异叠加

现象:本地 dev 85%,提交官方测试集只有 82.8%,代码反复检查没有 bug。

原因:三个隐式差异叠加。一是官方测试集的案由分布与本地 dev 不完全一致,冷门案由占比更高;二是本地 dev 是随机切分,部分样本与训练集相似度过高,指标被捧高;三是提交前用 train+dev 重新训练,模型没“见过”测试集分布,分数回落到真实水平。

解决:dev 划分从随机抽样改成按案由分层抽样,让本地分布尽量接近官方。提交前记录纯 train 训练的模型指标作为基准,再上报 train+dev 重训的版本,两个数字一起看。本地与官方的差距稳定在 2 个点以内说明划分合理;超过 3 个点优先怀疑案由分层没做对,而不是模型问题。

5. 从相似案例匹配迁移到 CAIL 司法考试赛道:两次关键改造与验证

CAIL 司法考试赛道和法研杯相似案例匹配,任务包装完全不同,内部结构却高度同源。司法考试每个选择题都可以改写成:一条案情加一组选项,正确答案本质上是与案情最匹配的选项,错误选项就是干扰项,和三元组里的 B、C 互斥结构几乎一样。压缩包里 CAIL2020/2021 司法考试赛道冠军团队的资料,正是把这套相似匹配的骨架迁移到了新任务上。

5.1 先看司法考试赛道和相似案例匹配的差异在哪

司法考试赛道包含单选、多选、判断和案例分析等题型,输入是一段案情描述,输出是选项字母。难点集中在三个方面:选项数量不固定,多选题答案不止一个;部分题目依赖具体法条原文,需要外部知识;案情和选项之间常有指代关系,比如“本案中张某的行为构成什么罪”,选项里的“张某”要和案情中的“张某”对齐。

和 SCM 比,最大差异有两个。第一,候选数量从 2 变成动态 N,输出不再是二分类;第二,任务从“一对一的匹配”变成“案情加法条到选项是否成立”的推理过程。但底层的语义表征可以复用:SCM 训练出的编码器已经学会法律文书哪部分重要、案由怎么区分、裁判理由怎么定位,这些知识迁移到司法考试赛道直接可用。

5.2 改造一:固定二分类改成多选项打分

把 SCM 的输出从两个候选改成 N 个选项。案情当 query,每个选项单独过编码器,复用差向量打分结构,再按题型决定损失函数。

def score_options(case_vec, option_vecs): scores = [] for opt_vec in option_vecs: feat = torch.cat([case_vec, opt_vec, case_vec - opt_vec], dim=-1) scores.append(model.scorer(feat).squeeze(-1)) return torch.stack(scores)

scores 长度必须和选项数一致。单选题用 softmax 加多分类损失,让选项共享同一个概率空间,模型必须从所有选项里挑一个;多选题用 sigmoid 加二分类损失,每个选项独立判断成立与否。这里最容易翻车的是单选题也当多选题处理,模型对每个选项独立输出概率,选项之间的排斥关系完全丢失。SCM 的双塔编码器可以平滑承接这个改造,因为选项的编码方式和 B、C 完全一致,只换了打分层的输出结构。

5.3 改造二:接入法条检索,给模型外部知识

司法考试题目经常直接考法条,比如“根据《刑法》相关规定,下列行为构成盗窃罪的是哪一选项”。这类题不能靠语义相似度硬猜,必须先定位相关法条。常见做法是构建法条库索引,用 BM25 召回候选法条,再拼到案情后面一起喂给 BERT。

from rank_bm25 import BM25Okapi corpus = [list(law_text) for law_text in law_db] bm25 = BM25Okapi(corpus) def retrieve_laws(question, top_k=3): query_tokens = list(question) scores = bm25.get_scores(query_tokens) top_ids = scores.argsort()[-top_k:][::-1] return [law_db[i] for i in top_ids]

BM25 用字符级切分,因为“盗窃”“数额较大”这类法律关键词本身就是紧凑词组,字符级匹配比词级更直接。注意这一步只负责召回,不判断哪条法规真正适用,适用性判断交给 BERT 对拼接长序列的推理。候选法条控制在 3 条以内、每条 150 字以内,否则输入截断会把案情原文挤出去。这个“检索召回加精排判定”的级联设计,是司法考试赛道的通用打法,和 SCM 里“先检索候选再精排匹配”一脉相承。

5.4 迁移策略:先冻结主干微调打分层,再放开主干

从 SCM 直接微调到司法考试,最怕灾难性遗忘。SCM 数据量大、训练充分,编码器对法律文本分布拟合得很好;司法考试数据量相对小,一上来就全量微调,模型会快速忘记 SCM 学到的案由和裁判理由结构,综合指标反而下降。

我的迁移节奏分两个阶段。第一阶段冻结 BERT 主干,只训练打分层,持续一个 epoch,让打分层先适应多选项输出和司法考试的标签分布;第二阶段放开主干,用 1e-5 的学习率继续微调,比 SCM 主训练低一半。两阶段之间存一个检查点,第二阶段如果 dev 下降就回退检查点重来。用这个策略,SCM 编码器的大部分知识能保留下来,司法考试赛道指标比直接全量微调高出 2 到 3 个点。这套“检索加精排加两阶段微调”的框架,就是冠军团队方案的核心骨架。任务换了,骨干没换。

6. 一个验证技巧:用反例互换检查三元组模型的判别力

模型做完、指标达标,很多人直接提交。我多做一个步骤:把测试集里每个三元组的 B 和 C 互换位置,重新过一遍模型,观察预测结果是否也反过来。

def swap_check(model, sample): orig_pred = predict_one(model, sample) swapped = { "a": sample["a"], "b": sample["c"], "c": sample["b"], } swapped_pred = predict_one(model, swapped) return orig_pred, swapped_pred

如果模型真的学到了“A 与 B 更匹配”这类语义,那么 B、C 互换后预测标签应该从 0 变 1,或者从 1 变 0。如果互换后标签还是原来那个,说明模型在靠位置偏差或者长度特征做判断,也就是常说的“学废了”。这个技巧不需要额外标注,把已有数据复制一份就能自查,是我每次提交前必做的最后一关。

实际操作里会有 1% 到 2% 的样本互换后预测不变,这些是真正的难例,大概率因为 B 和 C 与 A 的相似度太接近。这个比例超过 5%,就得回到第 4 章的难例挖掘重新处理训练集;低于 2% 就可以放心提交。阈值来自我自己的复现经验,不同数据集可以重新统计,但比单看准确率可靠得多。

做相似案例匹配这一路下来,我最大的习惯是先验证模型有没有学歪,再谈准确率提升。指标好看不代表判别方向正确,尤其在三元组这种相对比较任务里,方向错了,调参调得再好也是白费。希望这个反例互换的小技巧也能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 23:22:22

PRQL 的 Elixir 绑定:使用 Rustler NIF 在 Elixir 中编译 PRQL 查询

后端 【免费下载链接】prql PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement 项目地址: https://gitcode.com/gh_mirrors/pr/prql 点击查看 免费下载 本指南围绕 PRQL 仓库中的 Elixir 语言绑定(位…

作者头像 李华
网站建设 2026/9/23 23:17:04

黑翅鸢算法优化CNN-BiLSTM-Attention的客流量预测实战

简介:这是一份基于黑翅鸢算法BKA-CNN-BiLSTM-Attention的客流量预测Matlab实现,面向计算机、电子信息工程、数学等专业的学生,可用于课程设计、期末大作业与毕业设计。代码采用参数化编程,注释清晰,附赠可直接运行的案…

作者头像 李华
网站建设 2026/9/23 23:16:05

佛山壁挂炉维修电话|不点火不供暖就近上门检修|欧米到家客服电话

📝 文章简介佛山家庭使用壁挂炉时,常见问题包括不点火、不出热水、地暖或暖气片不热、故障代码、水压下降、漏水、风机异响、频繁启停等。欧米到家提供壁挂炉检测、维修、清洗保养、采暖调试及配件更换建议服务,覆盖佛山各区:禅城…

作者头像 李华
网站建设 2026/9/23 23:04:10

SegGIS v2.0视频教程:GIS实战技巧与场景化学习指南

1. 项目概述:SegGIS v2.0视频教程的价值定位SegGIS作为地理信息系统(GIS)领域的重要工具,其2.0版本在功能扩展和用户体验上都有显著提升。这套视频教程的诞生,源于我观察到大量GIS从业者在面对新版软件时普遍存在的三个…

作者头像 李华