简介:面向中文自然语言处理预训练模型泛化能力挑战赛的完整解决方案包,聚焦TNEWS新闻分类、OCEMOTION情感分析、OCNLI自然语言推理三大任务,重点解决多标签不均衡数据对模型泛化能力的影响。包内共18个文件,包含6个csv数据文件、4个Python脚本、3个shell训练脚本、2个说明txt及文档、配置文件等,压缩包整体6.47MB,目录结构清晰,便于按任务模块快速定位。已有88人学习使用。方案提供了从数据预处理、模型训练到预测提交的完整工程代码,配套README与环境依赖说明,并附赠说明文件与资源文档,可帮助参赛者或NLP学习者复现比赛流程、理解重采样与权重调整等不均衡处理策略,以及基于预训练模型的下游任务微调方法。
1. 从这份中文 NLP 挑战赛方案说起:三大任务、预训练模型与“多标签不均衡”到底是什么
我在天池上拆过的中文 NLP 项目里,这份方案属于“一个骨架带三个任务”的典型。它围绕 TNEWS 新闻分类、OCEMOTION 情感分析、OCNLI 自然语言推理三个任务,把预训练模型微调、多标签不均衡数据处理、脚本化复现完整串在一起。适合正在打天池中文 NLP 比赛的人,也适合想快速拿一套可跑的深度学习基线去改的从业者。你不需要从零搭训练框架,拿到压缩包后按 README 和脚本往下走,就能把三个任务的训练、预测、提交流程跑通。真正值钱的是里面处理不均衡数据的思路,以及那几个把训练和预测串成一条链的 shell 脚本。
2. 数据拆解与不均衡处理:TNEWS / OCEMOTION / OCNLI 的标签分布、重采样与权重调整
2.1 三大任务的数据结构:先看清每个 csv 在做什么
压缩包里的 data 目录有六个 csv:TNEWS_train1128.csv、OCEMOTION_train1128.csv、OCNLI_train1128.csv,以及对应的 TNEWS_a.csv、OCEMOTION_a.csv、OCNLI_a.csv。从命名看,train1128 应该是经过一轮重采样后的训练集,a 后缀文件可能是增强样本或辅助测试集。我的习惯是拿到数据先不跑模型,先把每个 csv 的列名、行数、标签分布扫一遍。
import pandas as pd for name in ['TNEWS', 'OCEMOTION', 'OCNLI']: df = pd.read_csv(f'data/{name}_train1128.csv') print(name, df.shape) print(df.columns.tolist()) print(df.head(2).to_string())这段代码会打印出每个任务的行数和列名。TNEWS 通常是 text 和 label 两列,新闻分类的标签是主题类别,比如体育、科技、财经;OCEMOTION 是文本情感倾向,一般是正向、负向、中性之类的分类;OCNLI 自然语言推理则会有 premise、hypothesis、label 三列,label 通常是蕴含、矛盾、中立。先打印出来,能避免后面写 dataset 时把列名猜错。
从任务本质看,TNEWS 和 OCEMOTION 都是单标签多分类,OCNLI 是三分类推理任务。但方案名里写的是“多标签不均衡数据处理”,这里要留意:你不一定需要把每个样本打多个标签,而是要处理标签分布不均衡。三个任务的标签数量差异可能很大,TNEWS 的类别数最多,OCEMOTION 的类别数少,OCNLI 是三类,但每类的样本比例可能严重偏斜。这种偏斜会让模型偏向多数类,所以第一步必须是统计标签分布。
from collections import Counter for task in ['TNEWS', 'OCEMOTION', 'OCNLI']: df = pd.read_csv(f'data/{task}_train1128.csv') cnt = Counter(df['label']) total = sum(cnt.values()) print(task, '类别数:', len(cnt)) for label, num in cnt.most_common(): print(f' {label}: {num} ({num/total:.2%})')这一段会输出每个任务的类别数和每个类别的样本占比。如果发现某个类别占比低于 5%,那基本可以判定是长尾分布。这时候不要急着调模型,先决定处理策略。
2.2 多标签不均衡怎么判:不是看一眼标签数量就够了
判断不均衡,光看总样本数不够。我一般会看两个指标:一是类别占比的极差,二是少数类样本的绝对数量。假设 TNEWS 有 30 个类别,最大类有 2000 条,最小类只有 30 条,比例悬殊超过 60 倍,模型很容易把少数类都忽略掉。OCEMOTION 如果情绪类别之间相差 20 倍,同样需要处理。
另一个容易被忽略的是“标签噪声”。从文件命名看,train1128 可能已经是重采样后的结果,但原始原始数据里可能有不一致标签。比如同一句话在不同标注员手里被标成不同类别,这种噪声会被过采样放大。所以在做任何重采样前,先做一遍清洗:去重、看标签是否都在约定集合内、剔除无意义的短文本。
df = pd.read_csv('data/TNEWS_train1128.csv') valid_labels = set(range(20)) # 假设是20分类 df = df[df['label'].isin(valid_labels)].reset_index(drop=True) df = df.drop_duplicates(subset=['text']).reset_index(drop=True) print(df.shape)这里的 valid_labels 需要根据 README 里的标签定义来填。你可能会问,直接去重会不会把不同类别的同文本删掉?在新闻分类里,同一句话几乎不可能属于两个不同主题;在情感分析里也基本相同。所以按 text 去重是安全的。之后再看一次类别分布,如果极差依然很大,就进入重采样阶段。
2.3 重采样与权重调整:先让数据说话,再决定用哪种手段
处理不均衡常见做法有三种:少数类过采样、多数类下采样、类别权重。这个方案里叫“多标签不均衡数据处理”,我理解为对每个任务分别做一次类别层面的重采样。1128 这个数字可能是重采样后每类的目标样本数,也可能是总样本数。如果是总样本数,可能是在 1128 条样本上做均衡。我的做法一般是按类别最少那一类的数量作为参考,或者设定一个阈值。
import pandas as pd from collections import Counter df = pd.read_csv('data/OCEMOTION_train1128.csv') cnt = Counter(df['label']) min_num = min(cnt.values()) target_num = int(min_num * 1.5) # 少数类复制到1.5倍 frames = [] for label, num in cnt.items(): sub = df[df['label'] == label] if num < target_num: reps = target_num // num + 1 sub = pd.concat([sub] * reps).sample(target_num, random_state=42) frames.append(sub) df_resampled = pd.concat(frames).sample(frac=1, random_state=42).reset_index(drop=True) print(df_resampled['label'].value_counts())这段代码把少数类复制到目标数量,然后整体打乱。要注意 random_state 必须固定,否则每次跑出来的训练集顺序不同,对比实验就不公平。复制样本有一个副作用:模型会反复看到同一句话,容易过拟合到少数类的表面特征。所以我一般会配合类别权重一起用,而不是只靠过采样。
类别权重的作用是在计算损失时给少数类更大梯度。如果任务用 CrossEntropyLoss,可以直接传一个 weight 张量。
import torch label_counts = df_resampled['label'].value_counts().sort_index() weights = 1.0 / label_counts weights = weights / weights.sum() * len(label_counts) weight_tensor = torch.FloatTensor(weights.values) criterion = torch.nn.CrossEntropyLoss(weight=weight_tensor)这里把每个类别的权重设为频率倒数并归一化,让模型不会因为某类样本多就偏向它。实际使用中,我建议权重不要拉得太极端,否则噪声样本也会被放大。可以把权重开个平方根,让差距缩小一些。
数据准备完之后,下一步就是选预训练模型。三个任务共用同一个中文预训练模型,只是任务头不同,这是这个方案的核心设计。
3. 预训练模型选型与微调:RoBERTa 中文模型、任务头设计与训练参数
3.1 为什么选 RoBERTa 中文预训练模型而不是直接裸训练
在中文 NLP 任务上,裸训练一个 Transformer 从零做三大任务完全不现实。预训练模型已经在海量中文语料上学会语言表示,你只需要在它的上面接一个分类头做微调。这个方案里比较稳的选择是 RoBERTa 中文预训练模型,它的优势是训练更充分、动态掩码、更大数据量,在很多中文榜单上表现都优于原始 BERT。你可以在 transformers 里直接加载。
from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name = 'hfl/chinese-roberta-wwm-ext' tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=num_labels)hfl/chinese-roberta-wwm-ext 是常用的中文 RoBERTa 变体,“wwm”表示全词掩码,“ext”表示扩展模型。对 TNEWS 这类新闻分类,它能比 BERT-base 多拿 1-2 个百分点的准确率。对 OCNLI 这种推理任务,它也能更好捕捉句子间的语义关系。
你可能会问,三个任务用同一个预训练模型会不会互相打架?我的理解是,方案的做法是每个任务独立微调一份模型,或者共享编码器但各自保留任务头。独立微调最省事,每个任务跑一次,预测时再写个分发逻辑。共享编码器则适合多任务学习,但需要额外处理梯度。对天池这类比赛,独立微调已经够用,也更稳。
3.2 输入格式与 dataset.py 的构建:单句和句对都要能处理
dataset.py 是整个数据管道里最核心的文件。它负责把 csv 里的文本转成模型需要的 token ids、attention mask 和 label。TNEWS 和 OCEMOTION 是单句输入,OCNLI 是句对输入,所以 dataset 必须兼容两种模式。
import torch from transformers import AutoTokenizer class CNLPDataset(torch.utils.data.Dataset): def __init__(self, df, tokenizer, max_len=128, is_pair=False): self.df = df.reset_index(drop=True) self.tokenizer = tokenizer self.max_len = max_len self.is_pair = is_pair def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.loc[idx] text = str(row['text']) if self.is_pair: text_b = str(row['hypothesis']) encoded = self.tokenizer( text, text_b, max_length=self.max_len, padding='max_length', truncation=True, return_tensors='pt' ) else: encoded = self.tokenizer( text, max_length=self.max_len, padding='max_length', truncation=True, return_tensors='pt' ) label = torch.tensor(row['label'], dtype=torch.long) return { 'input_ids': encoded['input_ids'].squeeze(0), 'attention_mask': encoded['attention_mask'].squeeze(0), 'label': label }这段代码里,is_pair 控制是不是句对输入。OCNLI 的数据通常有 premise 和 hypothesis 两列,调用 tokenizer 时把两段文本一起传进去;TNEWS 和 OCEMOTION 只传 text。padding 用 max_length 是为了让每个 batch 维度整齐,truncation 直接截断超长句子。max_len 对新闻文本建议设 128 或 256,太长会拖慢训练,太短会丢失信息。情感分析一般 128 足够。
dataset 写好之后,还需要 collate 函数来组 batch。batch 里每个样本的 input_ids 可能长度一样,因为前面已经 padding 过了,所以直接堆叠即可。
def collate_fn(batch): input_ids = torch.stack([b['input_ids'] for b in batch]) attention_mask = torch.stack([b['attention_mask'] for b in batch]) labels = torch.stack([b['label'] for b in batch]) return input_ids, attention_mask, labels这个 collate_fn 会在 DataLoader 里使用。它不做任何后处理,因为数据已经 padding 成等长。如果未来想改成动态 padding,可以在这里针对每个 batch 单独做,省点算力。但从稳定性出发,max_length 定长 padding 更简单。
3.3 训练参数:学习率、batch size、epoch 与 early stopping
预训练模型微调的核心是两个参数:学习率和 batch size。学习率太大会把预训练权重冲掉,太小则训练慢。我常用的范围是 2e-5 到 5e-5,RoBERTa 类模型用 2e-5 比较稳。batch size 受显存限制,TNEWS 这种短文本可以到 32,OCNLI 句对稍微大一点,建议 16。training_args 里还需要设置 warmup ratio 和 weight decay。
from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir='./checkpoints', num_train_epochs=5, per_device_train_batch_size=16, per_device_eval_batch_size=32, learning_rate=2e-5, warmup_ratio=0.1, weight_decay=0.01, logging_steps=50, save_strategy='epoch', evaluation_strategy='epoch', load_best_model_at_end=True, metric_for_best_model='accuracy', seed=42, )这里的 save_strategy 和 evaluation_strategy 都设成 epoch,每个 epoch 存一次模型并做一次验证。load_best_model_at_end=True 会让训练结束后自动加载验证集上最好的模型,这是防止过拟合的关键。如果你发现训练集损失还在下降但验证集指标不涨,说明开始过拟合了,此时应该提前停止。Trainer 本身没有内置 early stopping,需要自己回调或者简单地在循环里判断。
如果你不想用 Trainer,也可以自己写循环。常见做法是每轮 epoch 结束后计算验证准确率,保留最佳 checkpoint,连续两轮不提升就 break。这个方案的 run_classifier.py 里应该就是类似逻辑。我自己更习惯手动控制,因为三个任务要分别跑,手动循环能更清楚每个任务的收敛情况。
best_acc = 0 patience = 0 for epoch in range(5): train_one_epoch(model, train_loader, optimizer) acc = evaluate(model, val_loader) if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_model.bin') patience = 0 else: patience += 1 if patience >= 2: break这段代码把“验证集不提升就早停”做了简单实现。patience 设 2 意味着连续两个 epoch 没有刷新最佳准确率就停。你需要根据自己的数据调整 patience,数据噪声大时可以放到 3。不要迷信 epoch 越多越好,预训练模型微调通常在 3-5 个 epoch 内就收敛了。
4. 从数据到提交:gen_data.py、run_classifier.py 与 run_all.sh 的完整复现流程
4.1 gen_data.py:先把原始 csv 处理成语料和标签文件
拿到压缩包后,你会发现里面还有 gen_data.py。它负责从原始比赛数据生成后续训练需要的格式。原始 csv 可能字段名不统一,或者 label 是字符串而不是数字,需要经过转换。gen_data.py 的核心就是把字符串标签映射成整数 id,同时做初步清洗。
python gen_data.py \ --data_dir data/ \ --output_dir data/processed \ --task TNEWS \ --label_map_file data/TNEWS_labels.json上面是一个常见用法示例。参数 --data_dir 指定原始数据目录,--output_dir 指定处理后的输出目录,--task 告诉脚本当前处理哪一个任务,--label_map_file 是标签到 id 的映射文件。实际参数名可能略有不同,以 README 为准。但逻辑不会变:读 csv、映射标签、写出新的 csv 或 jsonl。
如果原始数据里有缺失文本,gen_data.py 通常会把它过滤掉。还有一个容易被忽略的点:标签映射必须和提交格式一致。天池比赛通常要求提交文件里的 label 是 id 或原始字符串,你需要先确认。我之前见过有人训练时用 id 0-19,提交时才发现样例提交文件用的是字符串标签,结果整个预测白做。
4.2 run_classifier.py:三个任务的训练入口
run_classifier.py 是主训练脚本。它应该支持通过 --task 参数切换 TNEWS / OCEMOTION / OCNLI,并自动加载对应数据、初始化对应分类头。这意味着你可以用一条命令训练一个任务,也可以写个循环把所有任务都过一遍。
python run_classifier.py \ --task TNEWS \ --model_name hfl/chinese-roberta-wwm-ext \ --do_train \ --do_eval \ --data_dir data/processed/TNEWS \ --output_dir models/TNEWS \ --num_labels 20 \ --max_seq_length 128 \ --train_batch_size 16 \ --eval_batch_size 32 \ --learning_rate 2e-5 \ --num_train_epochs 5在这条命令里,--num_labels 必须和 gen_data.py 里的标签映射数量一致。如果 TNEWS 类别数是 20,就写 20;OCEMOTION 如果是 3 类,就写 3;OCNLI 固定 3 类。--do_train 和 --do_eval 同时出现,表示训练完立即在验证集上评估。output_dir 会保存 checkpoint 和预测结果。
如果你要跑三个任务,不要手工一条条执行。把命令写成 shell 循环更省事。run_classifier.sh 可能就是这么干的。
#!/bin/bash declare -A num_labels=( ["TNEWS"]=20 ["OCEMOTION"]=3 ["OCNLI"]=3 ) for task in TNEWS OCEMOTION OCNLI; do python run_classifier.py \ --task $task \ --model_name hfl/chinese-roberta-wwm-ext \ --do_train --do_eval \ --data_dir data/processed/$task \ --output_dir models/$task \ --num_labels ${num_labels[$task]} done这里用关联数组把每个任务的类别数记录下来。有的任务可能标签要从 csv 里自动统计,那就不需要 num_labels 参数。不管怎样,循环训练能保证三个任务都跑完,而且不会因为某个任务失败就让整个流程中断。建议在循环里加个 set -e,一旦某个任务训练失败就停止,省得后面提交时才发现。
4.3 run_all.sh 与 submit.sh:一键复现和生成可提交文件
run_all.sh 是整条流水线的总控。它把 gen_data、训练、预测全部串起来。执行它,你只需要等待训练完成,然后就能得到预测结果。
#!/bin/bash set -e python gen_data.py --data_dir data --output_dir data/processed python run_classifier.py --task TNEWS --data_dir data/processed/TNEWS --output_dir models/TNEWS python run_classifier.py --task OCEMOTION --data_dir data/processed/OCEMOTION --output_dir models/OCEMOTION python run_classifier.py --task OCNLI --data_dir data/processed/OCNLI --output_dir models/OCNLI python predict.py --data_dir data/processed --model_dir models --output_dir submission最后一步 predict.py 读取三个已训练好的模型,对测试数据分别预测,并把结果按天池提交格式合并。run_all.sh 的好处是你不用记命令顺序,坏处是如果中间某一步失败,你不会立刻意识到是数据问题还是模型参数问题。所以第一次跑时我建议不要直接 run_all.sh,而是分步执行,看到每一步的输出。
submit.sh 一般是把 submission 目录下的文件打包或转换成最终提交格式。常见做法是生成一个不带表头的 txt 或 csv,包含 id 和 label 两列。
#!/bin/bash python predict.py \ --data_dir data \ --model_dir models \ --output_dir submission cd submission python - <<'PY' import pandas as pd sample = pd.read_csv('sample_submission.csv') pred = pd.read_csv('prediction.txt') sample['label'] = pred['label'] sample.to_csv('final_submit.csv', index=False, encoding='utf-8') PY这里用 sample_submission.csv 的 id 顺序,把 prediction.txt 里的 label 填进去,避免因为行顺序不对导致提交失败。天池这类比赛会检查提交文件的行数和列名,多一列少一列都会被判 0 分。所以我每次都会先读样例提交文件的表头,再把自己的预测结果对齐。
整个复现流程就是这样。你可能觉得脚本很多,但真正需要人工参与的只有 gen_data.py 里的标签映射定义和参数调整。其余部分按默认设置跑,就能得到一个不差的基线。
5. 避坑与常见问题:多标签不均衡数据处理的五个翻车点
5.1 重采样之前没切分验证集,导致验证集虚高
现象:训练后验证准确率非常高,但提交到天池线上分数却暴跌几十分。
原因:你先把全部数据重采样,再把重采样后的数据切成训练集和验证集。少数类的复制样本同时出现在训练集和验证集里,模型等于提前看到了验证集答案。
解决:必须先按原始数据切分训练集和验证集,然后只在训练集上做重采样,验证集保持原始分布。这个顺序不能反。如果你用的是 train1128.csv 这种已经重采样过的数据,也要先把它按原始比例切分,再评估分布。
from sklearn.model_selection import train_test_split train_df, val_df = train_test_split( df, test_size=0.2, stratify=df['label'], random_state=42 ) # 只在 train_df 上重采样 train_resampled = resample(train_df)stratify 参数会让每个类别在训练集和验证集中占比一致。如果你的数据是长尾分布,这一点尤其重要。切分之后,验证集永远不参与重采样,线上分数才可参考。
5.2 把单标签多分类当成多标签分类,损失函数用错
现象:用了 BCEWithLogitsLoss,模型输出层接 sigmoid,训练几个 epoch 后损失卡住不下,预测结果全是零。
原因:TNEWS、OCEMOTION、OCNLI 本质都是单标签分类,一个样本只能属于一个类别。用 sigmoid 等于把每个类别看成独立事件,模型无法学到类别间的互斥关系。
解决:确定任务类型。单标签就用 CrossEntropyLoss 或者 Trainer 默认的分类损失。虽然在方案名里看到了“多标签”这个词,但数据集本身是单标签多分类。如果你自己的任务确实需要多标签,那才用 sigmoid 和 BCEWithLogitsLoss。初学者最容易在这里踩坑,看到“多标签”三个字就条件反射换损失函数。
5.3 过采样直接把少数类复制太多,模型对重复样本过拟合
现象:训练集准确率接近 100%,验证集准确率忽高忽低,特别是长尾类别变化剧烈。
原因:把少数类复制了 5 倍甚至 10 倍,模型记住了少数类那几句话的原文,而不是真正的语义特征。
解决:限制过采样的倍数,一般不超过 2-3 倍。或者改用 SMOTE 一类的插值方法,但文本数据做插值比较少见。更稳的做法是配合类别权重,或者用回译、同义词替换做文本增强。从实际比赛经验看,权重调整往往比简单复制更有效。我习惯先用类别权重试试,不行再加少量过采样。
5.4 预训练模型加载时远程下载失败,卡在 tokenizer 初始化
现象:第一次跑 run_classifier.py 时卡在 downloading 状态,或者报 ConnectionError。
原因:transformers 库默认从 HuggingFace Hub 下载模型,网络波动时容易失败。
解决:提前把模型权重下载到本地,然后直接用本地路径替代 model_name。
python - <<'PY' from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name = 'hfl/chinese-roberta-wwm-ext' tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=3) tokenizer.save_pretrained('./pretrained/chinese-roberta-wwm-ext') model.save_pretrained('./pretrained/chinese-roberta-wwm-ext') PY之后把训练脚本里的 model_name 改成 './pretrained/chinese-roberta-wwm-ext'。如果是在离线服务器上跑,还要设置环境变量 TRANSFORMERS_OFFLINE=1,让 transformers 不再尝试网络请求。这个坑在没有公网的环境里非常常见。
5.5 三个任务共享 num_labels,导致 OCNLI 加载模型时参数不匹配
现象:训练完 TNEWS 再训练 OCNLI,加载模型时报维度不匹配,或者分类头输出数和标签类别数对不上。
原因:run_classifier.py 里可能把 num_labels 写成全局变量,没按任务分别传入。OCNLI 是 3 类,TNEWS 是 20 类,如果都用同一个 num_labels,最后一个全连接层维度就会错位。
解决:在训练循环里显式按任务传入 num_labels。或者用我刚才给的关联数组方式,在 shell 里分别指定。如果你发现模型加载报错,第一步就是检查 num_labels 是否和当前任务的标签类别数一致。
python run_classifier.py --task OCNLI --num_labels 3这只是一个很小的坑,但会导致整个训练流程中断,而且错误信息不会直接提示你“num_labels 不对”,而是抛出一堆 shape mismatch。所以每次跑新任务前,我都习惯先打印 smodel.config.num_labels 确认一下。
6. 进阶:分层验证、多任务融合与我的提交习惯
6.1 分层 K 折:让不均衡数据在验证时更可信
单次切分训练集和验证集有偶然性。更稳的做法是分层 K 折,把数据分成 5 份,每份都保持原始类别比例,然后训练 5 个模型,用平均预测结果作为最终提交。这种方法对长尾分布非常有效,等于每个样本都有机会出现在验证集里,不会因为某一次切分不好而误判模型能力。
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) fold = 0 for train_idx, val_idx in skf.split(df, df['label']): fold += 1 df.loc[train_idx].to_csv(f'data/{task}_fold{fold}_train.csv', index=False) df.loc[val_idx].to_csv(f'data/{task}_fold{fold}_val.csv', index=False)跑 5 折会比单折慢 5 倍,但预测稳定性提升明显。天池比赛如果只追求一次提交,可以不用 5 折;但如果时间充足,我会至少跑 3 折。
6.2 多任务输出的 logits 融合:比硬投票更稳
三个任务各自训练后,predict.py 会产生每个测试样本的 logits。融合阶段,不要直接取 argmax,而是把多个模型的 logits 做平均再 argmax。如果同一任务跑了多个折,把折之间的概率平均,最后再找最大项。
import numpy as np logits_list = [] for fold in range(5): logits = np.load(f'preds/fold{fold}_logits.npy') logits_list.append(logits) avg_logits = np.mean(logits_list, axis=0) pred = np.argmax(avg_logits, axis=1)这种方法比多数投票更平滑,因为少数类虽然某些折分类错误,但概率平均后可能被拉回来。对 OCNLI 这种类别不平衡明显的任务,效果尤其明显。
从我自己的天池经验看,最后决定名次的往往不是模型结构,而是数据处理的顺序和验证策略。每次比赛我都会强制走一遍:先统计标签分布,再分层切分,然后只在训练集上做重采样和权重调整,训练结束后回滚到验证集最好的 checkpoint,最后再做折间融合。这个习惯帮我避免了好几次因为数据泄漏导致的线上分数翻车。希望帮到你。
本文还有配套的精品资源,点击获取