ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医学搜索Query相关性判断:基于ERNIE与RoBERTa的NLP赛题工程实现

医学搜索Query相关性判断:基于ERNIE与RoBERTa的NLP赛题工程实现

简介:这是一份面向计算机相关专业在校学生、老师及开发者的天池医学搜索查询相关性判断赛题完整实现,基于自然语言处理与深度学习技术,覆盖数据增强、模型训练、评估与推理等关键环节,适合作为毕业设计、课程设计、作业或竞赛入门的高分参考。压缩包共三十个文件,包含八个脚本、七个配置文件、五个工程设置文件,以及日志、表格数据、说明文档等;脚本对应数据增强、训练评估、模型选择等流程,配置文件可灵活调整实验参数,整体约370KB,目录按数据、预训练模型、代码模块清晰划分,便于快速定位与二次开发。资源内置ERNIE、RoBERTa等预训练模型的调用示例,使用说明与英文文档一并提供。目前已有120人学习下载,代码经充分测试,答辩评审平均分达96分,下载后可联系作者获取远程教学或运行指导,帮助快速跑通完整深度学习赛题方案。

1. 医学搜索 Query 相关性判断:为什么说这是最适合课设的 NLP 赛题

医学搜索场景里,用户输入的 Query 和候选文档之间往往字面不重合但语义强相关,这个赛题要求模型判断一段 Query 与一篇医学文档的相关性等级,本质是文本匹配分类任务。这份深度学习课设资源,是一套跑通并拿到高分排名的完整工程实现:data 目录放着原始赛题数据,pretrain_models 里备好了 ERNIE 和两套 RoBERTa 预训练基座,code 目录下是数据增强、训练评估、推理测试的全套脚本。NLP 入门课设做到这个程度,既有模型选型的思考空间,又有实打实的调参路径。适合计算机、人工智能方向的同学用来做课程设计或毕设主线。下面我从数据开始,把整条链路拆开讲。

2. 赛题与数据处理:先定义“相关”,再谈模型

2.1 任务拆解:Query 与文档的相关性不是字符串匹配

这个赛题的输入是双文本结构:一边是用户输入的医学搜索 Query,一边是候选文档。候选文档来源很杂,疾病百科条目、药品说明书、临床指南摘要都有,有些文档标题就一句话,有些正文长到几百字。模型要输出的,是一个人工标注的相关性等级。这里的关键点是,标注者眼里的“相关”和我们直觉上的“相关”并不完全一致。

常见做法是分成三档:0 表示完全无关,1 表示部分相关,2 表示完全相关。这种分档对医学场景尤其重要,因为很多 Query 和文档属于“沾边但没答到点上”。比如 Query 是“高血压 饮食禁忌”,文档讲的是“高血压患者的运动建议”,你说它完全无关吧,它确实在聊高血压;你说它完全相关吧,它压根没回答饮食问题。模型真正要学的,就是这种既模糊又有规律可循的中间态。

如果图省事,用 Word2Vec 词向量取平均或者 TF-IDF 余弦相似度,你会很快撞上字面匹配的天花板。“咳嗽 吃什么药”和“儿童咳嗽的护理与用药指南”,两者重合了“咳嗽”和“药”,但 Query 的重点是治疗药物,文档的落点是护理和用药,字面分偏高,人工标注却倾向于中等相关。反过来,“感冒 流鼻涕”和“普通感冒的主要症状表现”几乎没有字符重合,语义上却是强相关。这些 case 靠传统特征很难区分,预训练模型微调成了主流方案。

所以这份资源从一开始就选择了 ERNIE 和 RoBERTa 两条预训练路线,而不是从词向量开始堆特征。你当然可以换别的基座模型,但保持项目和原脚本一致,能少走很多对分和对齐的弯路。我自己的经验是,先把数据读明白、把标签分布摸清楚,再谈模型选型,否则后面所有调整都像在开盲盒。

2.2 数据标注体系与标签分布

data 目录下存放原始赛题数据,组织形式是 JSON 行式文件,每行一条样本。样本字段一般包含 Query、候选文档和人工标注的 label。训练集规模不算大,几万条的量级,这对预训练模型微调来说偏紧张,所以资源里专门写了 data_augment.py 做数据增强,后面第 4 章会展开讲。

先看标签分布,医疗搜索场景有个特点:完全无关的样本往往占比不高,因为候选文档在构造时已经做过一轮筛选。大量样本集中在“部分相关”和“完全相关”这两档。这个分布直接影响训练策略。如果直接用默认的 CrossEntropyLoss,模型会往样本量大的类别偏,导致“完全无关”这一类召回率很低,整体分数被拖下来。

我一般会在训练前先跑一次统计脚本,看每个 label 的样本数。如果发现类别不均衡,就把类别权重传进 Loss:

# 按标签频数计算权重,抑制多数类对梯度的主导 import math from collections import Counter labels = [sample['label'] for sample in train_samples] counter = Counter(labels) total = sum(counter.values()) class_weights = [] for c in sorted(counter.keys()): freq = counter[c] / total # 常见做法是取对数逆频率,避免少数类权重过大 class_weights.append(1.0 / math.log(1.0 + freq))

这段代码的作用,是把样本占比转成每个类别的 Loss 缩放系数。比如“完全无关”只占 10%,它的权重就会比占 40% 的“完全相关”大几倍。注意 class_weights 的类别顺序必须和模型输出头的 num_labels 一致,否则权重会套错类别。有同学在这上面栽过跟头,loss 一直不收敛,最后发现是类别权重和标签编号错位了一个位置。

另外,官方数据通常只给训练集和测试集,验证集要自己从训练集里切。资源里 utils.py 读取数据时没有写死切分比例,留了参数。常见做法是 9:1 切分,并且固定随机种子,保证每次复现切出来的验证集完全一致。如果不固定种子,模型早停的位置也会跟着漂,你很难判断超参调整到底是变好了还是只是运气变了。

2.3 预处理流程:从原始 JSON 到训练样本

# utils.py:读取原始 JSON 行并构造统一的 sample 结构 import json from transformers import BertTokenizer def load_samples(data_path): samples = [] with open(data_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue item = json.loads(line) samples.append({ 'query': item['query'], 'doc': item['document'], 'label': int(item['label']) }) return samples def tokenize_samples(samples, tokenizer, max_len=128): input_ids, token_type_ids, attention_masks, labels = [], [], [], [] for s in samples: encoded = tokenizer.encode_plus( s['query'], s['doc'], max_length=max_len, truncation='longest_first', padding='max_length', return_tensors='pt' ) input_ids.append(encoded['input_ids'].squeeze(0)) token_type_ids.append(encoded['token_type_ids'].squeeze(0)) attention_masks.append(encoded['attention_mask'].squeeze(0)) labels.append(s['label']) return input_ids, token_type_ids, attention_masks, labels

这段代码把原始文本变成了模型输入张量。tokenizer 必须和预训练模型配套,ERNIE 和 RoBERTa 虽然都走 BertTokenizer 家族,但词表不同,不能混用。资源里 pretrain_models 下面分了三个子目录,分别对应 ERNIE、RoBERTa-wwm-ext-large 和 RoBERTa-large 句子对版本,加载哪一个,tokenizer 就必须指向哪一个目录。

encode_plus 里有几个参数值得认真说。max_length=128 表示把 Query 和 Document 拼接后的序列统一截断到 128 个 token,超出部分丢弃。truncation='longest_first' 表示拼接超长时,优先从较长的那一段里裁掉多余内容,对 Query 短、文档长的样本最友好。padding='max_length' 把所有样本补齐到同一长度,方便组成 batch 做张量运算。

如果你显存够用,把 max_length 从 128 提到 256,通常能带来小幅提升,因为长文档能保留更多有效信息,代价是训练时间和显存占用同步上涨。我一般会先拿小数据跑一次,确认显存峰值能接受,再决定要不要加长。

预处理里还有一个容易被忽略的点:Token 顺序。Query 在前还是 Document 在前,会直接影响微调效果。常见做法是 Query 作为第一个句子输入,Document 作为第二个句子,对应 token_type_ids 里的 0 和 1。预训练模型在做下一句预测任务时,对第一个句子的语义主体会更敏感。资源里 encode_plus 第一个位置传的是 query,遵循的就是这个约定。改代码时务必保持这个顺序,否则复现效果会偏离你预期。

3. 模型选型与微调策略:ERNIE、RoBERTa 与长文本对的取舍

3.1 为什么赛道主流是预训练模型而不是词向量加权

文本匹配任务的传统做法是给 Query 和文档分别算向量,然后算余弦相似度。词向量加权也好,TF-IDF 加权也好,本质都是把句子的语义压缩成一个固定的向量。问题在于,医学 Query 往往很短,一个词的变化就能改变整个意图,而固定向量难以捕捉这种细粒度差异。

预训练模型的做法完全不同。它把 Query 和 Document 拼接成一对输入,在 Transformer 的每一层里让两个文本互相做 Attention。这意味着最终判断相关性时,模型能看到 Query 里哪个词和文档里哪个词产生了强关联,而不是把两边各压缩成一个向量再比相似度。ERNIE、RoBERTa 这些模型在中文语料上做过大规模预训练,对医学领域的词汇表达已经有了基础语义理解,微调时只需要少量标注数据就能适应具体任务。

具体到这份资源,pretrain_models 下备好了三个基座:ERNIE、RoBERTa-wwm-ext-large、RoBERTa-large 句子对版本。这三个模型在文本匹配任务上的表现差异,主要来自预训练目标和模型规模。ERNIE 侧重在预训练阶段融入词汇、句法、语义多层次信息,对中文医学实体更敏感。RoBERTa 系列则是更大的模型参数加上更充分的训练数据,泛化能力更强,但显存占用和推理耗时也更大。

3.2 ERNIE 与 RoBERTa 的差异及适用场景

先看 ERNIE。它的核心特点是预训练时把实体词和短语作为掩码对象,模型对实体边界有更强的感知。在医学搜索场景里,Query 里出现“高血压”“糖尿病”这类实体词时,ERNIE 比普通 BERT 更容易抓住实体所在的位置。如果你的显存有限,机器只能跑 base 级别模型,ERNIE 是不错的起步选择。

再看 RoBERTa-wwm-ext-large。这个模型是 RoBERTa 的中文变体,用了全词掩码策略,预训练时把完整词的所有子词一起遮蔽,而不是只遮挡一个子词。对中文这种分词边界相对清晰的语言,全词掩码让模型学到了更稳定的词级语义,在句子对分类任务上通常比原始 BERT 高一到两个点。

RoBERTa-large 句子对版本则是主打双句输入的 large 模型,参数规模比 base 大很多。在这个赛题里它能带来更高的分数上限,但训练显存需求也最大。我自己的经验是,如果单卡显存 24GB,large 模型配合 128 的序列长度勉强能放下一批 8 个样本,再多就要走梯度累积了。

三个模型怎么选?我的建议是:先用 ERNIE 跑通整个训练预测链路,确认代码无误,再切到 RoBERTa-wwm-ext-large 提分。如果机器资源允许,最后用 RoBERTa-large 句子对版本跑一套完整结果,参与模型集成。这个顺序能让你在有限的时间里,既拿到可用的提交结果,又留出做实验的余地。

3.3 训练超参数配置:batch size、学习率与序列长度

# run_ernie.py:ERNIE 基座的微调配置 from transformers import BertConfig, BertForSequenceClassification num_labels = 3 # 对应预处理里统计的标签数 config = BertConfig.from_pretrained( 'pretrain_models/ERNIE', num_labels=num_labels ) model = BertForSequenceClassification.from_pretrained( 'pretrain_models/ERNIE', config=config )

这段代码的作用,是从本地预训练目录加载权重,同时把模型输出头改造成分类任务所需的 num_labels 个类别。注意 from_pretrained 的第一个参数是本地目录路径,不是模型仓库名。资源里 pretrain_models/ERNIE 目录下必须有 config.json 和权重文件,缺一样都会加载报错。

微调阶段我习惯把学习率设在 2e-5 到 3e-5 之间,batch size 在 16 到 32 之间。预训练模型已经收敛到了很好的语义表示,学习率太高会把学到的知识冲掉,太低又收敛太慢。RoBERTa-large 因为参数多,学习率可以适当降到 1e-5 到 2e-5。序列长度这块,128 是速度与效果的平衡点,256 会带来小幅提升但训练时间明显拉长。

Warmup 比例一般取 10%,让模型在训练初期用很小的学习率稳定几步,再逐步上升到峰值,最后线性衰减到零。如果不设 warmup,经常会出现前几个 step 的 loss 剧烈震荡,后面才慢慢稳定,浪费掉一部分训练时间。

3.4 三份训练脚本的定位差异

资源里 code 目录下有三个入口脚本:run_ernie.py、run_large_roberta_wwm_ext.py、run_large_roberta_pair.py。名字已经把定位说清楚了,分别对应三个预训练基座。三个脚本共用了 train_eval.py 里的训练评估主流程,区别只在模型加载和部分超参。

run_large_roberta_pair.py 的 pair 体现在输入侧:它把 Query 和 Document 构造成句子对,用 token_type_ids 区分两个句子,这种输入方式和第 2 章预处理那段代码是对应的。如果换了输入格式,比如把两段文本拼接好再喂进去,token_type_ids 的设置就要同步改,否则模型无法区分哪部分是 Query、哪部分是 Document。

训练脚本里还应该关注权重保存逻辑。常见的正确做法是每个 epoch 结束后在验证集上算一次指标,如果比历史最优好,就覆盖保存 best_model.bin。如果不做这个判断、只保存最后一轮权重,可能因为最后的训练阶段已经过拟合,导致提交效果反而更差。资源里 train_eval.py 在这个位置的处理逻辑,是值得拿到别的任务里复用的。

注意:ERNIE 与 RoBERTa 的词表不通用,切换基座时,tokenizer 的 vocab 路径要和模型权重严格对应,这是最容易踩的一个坑。

4. 从数据增强到训练评估:把代码跑通的完整链路

4.1 数据增强脚本的逻辑与边界

data_augment.py 的存在,是因为医学搜索赛题的数据量往往不够用。几万条训练样本对预训练模型微调来说,复现次数多了就很容易过拟合。数据增强的思路,是在保持标签语义不变的前提下,让模型看到更多样的表达方式。

常见做法有三种。第一种是同义词替换,把 Query 里的疾病名、症状词替换成同义表达,比如“高血压”换“血压高”,“咳嗽”换“咳”。第二种是随机插入,在样本中间插入无意义但有语法功能的词。第三种是对文档做回译式改写,机器翻译到英文再翻译回中文,生成新的表述。这三种方法里,回译质量最高但耗时也最长,同义词替换最省事但要小心医学实体词不能乱换。

# data_augment.py:基于同义词表的轻量数据增强 import random def synonym_replace(text, synonym_dict, replace_prob=0.3): chars = list(text) for i, ch in enumerate(chars): if ch in synonym_dict and random.random() < replace_prob: chars[i] = random.choice(synonym_dict[ch]) return ''.join(chars)

这段代码的逻辑很简单,遍历文本中的每个字符,如果命中同义词表,就以 replace_prob 的概率替换成同义字。注意这里的同义词表需要你自己根据医学场景维护,比如“高”和“升高”、“疼”和“痛”。replace_prob 一般控制在 0.2 到 0.3 之间,太高会把句子改得不通顺,反而引入噪声。

数据增强有一个边界要特别注意:只能增强训练集,不能增强验证集和测试集。验证集要保持原始标注,才能真实反映模型在原始数据分布上的表现。如果验证集也做了增强,你评估出来的分数会虚高,放到官方测试集上大概率对不上。

4.2 训练评估主线:train_eval.py 的模块拆解

train_eval.py 是整个工程的核心,负责训练循环、验证评估、模型保存三件事。读取数据、切分验证集、构造 DataLoader,这些都在前面几步完成,然后进入标准的 PyTorch 训练循环。

# train_eval.py:训练循环骨架 from transformers import AdamW, get_linear_schedule_with_warmup optimizer = AdamW(model.parameters(), lr=2e-5) total_steps = len(train_loader) * epochs scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps ) for epoch in range(epochs): model.train() for batch in train_loader: outputs = model( input_ids=batch['input_ids'], attention_mask=batch['attention_mask'], token_type_ids=batch['token_type_ids'], labels=batch['labels'] ) loss = outputs.loss loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad()

这段代码对应的是 Transformers 库接口。AdamW 是带权重衰减的 Adam 优化器,权重衰减能限制模型参数过大,缓解过拟合。学习率 2e-5 是为了微调预训练权重,不建议超过 5e-5。get_linear_schedule_with_warmup 把学习率先从小升到大,再到后期线性降下来,一共走 total_steps 步。

训练循环里有一个细节:token_type_ids 对 ERNIE 和 RoBERTa 这类使用绝对位置编码的模型不是必须的,但对句子对任务建议保留,它告诉模型前一段是 Query、后一段是 Document。如果不传这个参数,部分模型会把拼接后的序列当成一个连续句子,相关性判断时少了边界信息,效果会打折扣。

训练结束后,train_eval.py 会在验证集上计算准确率或 F1,按验证分数决定要不要保存当前权重。常见做法是只保存验证集上分数最高的那一次,覆盖掉之前的权重文件。这就是模型早停的基础,避免训练到最后过拟合带来的分数回退。

4.3 三个模型入口脚本的配置与执行

三个入口脚本,对应三种基座模型,执行时的前置条件基本一致:先把 pretrain_models 下对应目录的权重文件准备好,再把数据路径指到正确位置。

# 三种模型的启动方式,逐行执行 python run_ernie.py # ERNIE 基座,先跑通链路 python run_large_roberta_wwm_ext.py # RoBERTa-wwm-ext-large,提分主力 python run_large_roberta_pair.py # RoBERTa-large 句子对版本,参与集成

第二个脚本在单卡上跑时,建议先把 batch size 调小,如果显存不够,它会直接报 CUDA out of memory。第三个脚本跑之前要确认对应的 pretrain 目录下权重确实是 PyTorch 格式,有些开源权重是 TensorFlow 格式导出的,加载前需要先做一次格式转换,否则模型结构对不上,训练出来的分数会非常奇怪。

4.4 测试脚本与推理一致性验证

test.py 负责加载训练好的模型,对测试集做预测并输出提交文件。这里的核心问题是,训练时的数据预处理和测试时必须是同一套逻辑。比如训练时 Query 在前 Document 在后,测试时就不能调换顺序;训练时序列长度截断到 128,测试时也要按 128 截断。

# test.py:加载最佳权重并预测测试集 model.eval() predictions = [] with torch.no_grad(): for batch in test_loader: outputs = model( input_ids=batch['input_ids'], attention_mask=batch['attention_mask'], token_type_ids=batch['token_type_ids'] ) logits = outputs.logits preds = torch.argmax(logits, dim=-1) predictions.extend(preds.cpu().numpy().tolist())

这段代码用 torch.no_grad() 关闭梯度计算,节省显存和推理时间。argmax 把模型输出的三分类 logits 变成标签预测。如果你要提交概率文件,就把 argmax 换成 softmax,直接输出每个类别的概率分数,集成的时候会用得上。

推理一致性验证是一个好习惯。我一般会在测试集上跑两次,第一次用训练好的最佳权重,第二次用最后一轮权重,对比两次预测结果的差异。如果差异很大,说明训练还没收敛或权重保存逻辑有问题。如果两次几乎一致,那模型状态基本稳定,可以放心提交。

5. 避坑指南:预训练模型加载、显存与提交成绩不一致

5.1 预训练模型加载失败

现象:运行 run_ernie.py 时,在 from_pretrained 那一步直接抛 OSError,提示目标目录下找不到 config.json 或找不到权重文件。

原因:预训练目录结构不完整。from_pretrained 方法要求目录下至少有 config.json 和一个权重文件,比如 pytorch_model.bin 或 model.safetensors。有些模型包下载下来是压缩包,解压后目录嵌套了一层,直接指向外层目录当然加载不到。

解决:先去 pretrain_models/ERNIE 目录下确认文件列表。如果发现权重被包在下一层子目录里,就把子目录里的内容移到外层。移动时注意区分 config.json 和权重文件的命名,有些中文模型权重叫 model.ckpt,需要先转换成 pytorch_model.bin 才能被正常识别。这一步做完再重跑,加载就能通过。

5.2 损失不下降

现象:训练开始后,loss 在前几百步一路下滑,但到某个位置之后几乎不再变化,验证集分数也卡住不动。

原因:典型原因是学习率设置过大,模型在预训练权重的基础上反复震荡,始终跳不出局部区域。另一个常见原因是类别权重和标签编号错位,比如把“完全无关”的权重套到了“完全相关”的标签上,梯度方向本身就是错的。

解决:把学习率从 3e-5 降到 1e-5,如果 loss 开始缓慢下降,说明不是学习率的问题。用第 2 章里写的类别权重计算逻辑重新生成 class_weights,并打印出 label 和 weight 的对应表,人工核对一遍。确认这两项没问题后,再把 warmup 比例从 10% 提到 20%,给模型一个更平滑的起步,loss 曲线通常会变得更稳定。

5.3 验证集与测试集成绩不一致

现象:本地验证集上准确率 92%,提交到官方测试集上只有 88%,分差明显。

原因:最常见的是验证集切分时没有固定随机种子,导致每次跑出来的验证集分布不一样,模型保存的位置也不是对准同一个分布的。另一个原因是数据泄漏,比如在做数据增强时把增强后的样本也放进了验证集,模型已经见过这些文本,评估分自然虚高。

解决:在切分验证集之前,先调用 random.seed(42) 或 numpy.random.seed(42),把随机源固定下来。检查 data_augment 的调用范围,确保增强只作用于训练集。数据泄漏这块,除了增强样本,还要注意有没有把 Query 或 Document 完全相同的样本同时放进了训练集和验证集,如果存在,应该按文本去重后重新切分。

5.4 显存不足(OOM)

现象:换到 RoBERTa-large 后,程序在第一个 batch 就报 CUDA out of memory,小模型一切正常。

原因:large 模型的参数量是 base 的三倍以上,激活内存也相应膨胀,同样的 batch size 直接超显存。

解决:先把 batch size 降到 4 甚至 2,如果还是 OOM,就启用梯度累积。梯度累积的意思是每几个小 batch 才做一次参数更新,等效增大了 batch size,但显存占用不变。具体做法是把 train_eval.py 里的 loss.backward() 改成累积逻辑,每隔 accumulation_steps 个 batch 才执行 optimizer.step()。如果显存依然不够,就把序列长度从 128 降到 96,对短 Query 场景影响很小。

5.5 数据增强翻车

现象:用了同义词替换之后,验证集分数不升反降,而且训练集 loss 一直低于验证集 loss,差距越来越大。

原因:同义词表里混入了医学术语的不当替换。比如把“高血压”替换成“血压高”没问题,但把“糖尿病足”拆开替换成“糖尿病”和“足”,语义就偏了。还有随机插入操作产生了不通顺的句子,模型学到的是错误的语言规律,而不是增强后的语义鲁棒性。

解决:先停用数据增强,用原始数据重跑一遍作为基线,验证分数是否因为增强而下降。如果是,就把同义词表里的医学实体词全部列入保护名单,禁止替换。替换概率从 0.3 降到 0.1,让增强样本只占训练集的小比例。回译增强如果成本太高,就只对 Query 部分做,不对长文档做,效果和成本的平衡更划算。

6. 进阶技巧:概率平均集成与阈值搜索,把分数再往上拉一档

前五章讲的是把单模型链路跑通的完整过程。到了这个阶段,你想再往上提分,最有效的不是继续调参,而是把三个模型的结果做集成。常用的集成方式是概率平均:把每个模型对测试集样本输出的三分类概率分别取平均,再按平均后的概率取 argmax。

# 集成逻辑:两个模型概率平均 def ensemble_predict(probs_ernie, probs_roberta, weights=(0.5, 0.5)): probs = weights[0] * probs_ernie + weights[1] * probs_roberta return probs.argmax(axis=1)

weights 参数控制两个模型的融合比例。ERNIE 和 RoBERTa 如果分数接近,用 0.5 和 0.5 就能稳定获得提升。如果明显一个大一个小,我一般会把分数高的一方权重调到 0.6 到 0.7。三个模型一起做概率平均也可以,但要注意大模型占比过高会把集成结果拉偏,必要时在验证集上做一次小范围权重搜索,比拍脑袋定比例靠谱。

阈值搜索也是容易被忽略的一环,默认 argmax 不一定是最好。你可以先用验证集计算出每个类别的概率分布,再按照验证集分数最优的原则搜索一组阈值。先选好阈值后,再用它们去预测测试集。

从那以后,我每次提交前都强制走一遍集成验证:先确认三个模型的最佳权重都保存完整,再加载测试集依次输出概率文件,最后在验证集上比对单模型和集成的准确率,确认集成收益为正再提交。这一步帮我避开了好多次因为单模型过拟合导致的分数虚高。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进