对长句做分句处理,避免单条输入太长影响 attention 效果
一次真实模型训练调优实践:从掉链子到稳如老狗

去年我参与了一个 NLP 相关的项目,是为某家保险公司开发一个基于文本的风险评估系统。说白了就是根据用户填写的申请表和附加描述,判断其风险等级,并输出评分建议。整个项目前后历时三个多月,踩坑无数,尤其在模型训练调优阶段折腾了很久,今天就借这个机会把一些关键经验整理出来,供同行朋友们参考。
项目的起点:数据不干净、结果不稳定
我们拿到的数据集大约有 20 多万条客户提交的信息,每条信息包括用户的文字描述、历史风险等级(1 到 5 级)以及人工标注的标签。看起来数据不少,但实际用起来问题很多:
- 文本中夹杂大量口语化内容,比如“可能吧”、“差不多吧”,导致语义模糊;
- 标签存在严重的类别不平衡,5 级别的样本数量比 2 级少了一个数量级;
- 模型训练时 loss 下降缓慢,val_loss 波动大得像坐过山车。
当时团队试用了 RoBERTa + MLP 的结构,跑了两轮都没见起色,准确率一直在 68% 左右打转。那段时间压力特别大,老板天天问效果,业务方也催得急。后来我们决定沉下心来一步步排查和优化。
第一招:数据清洗 + 增强,先让模型看得更清楚
我们首先对数据做了几个小动作:
1. 清理噪声词 + 分句处理
import re
def clean_text(text):
text = re.sub(r'[^\u4e00-\u9fa5\w\s]', '', text) # 只保留汉字+字母+空格
text = text.lower()
return text.strip()
from nltk import sent_tokenize
def split_sentences(text):
return sent_tokenize(text)
我们发现很多样本句子太长,严重影响模型捕捉关键信息的能力。通过拆分成多个短句,再分别建模后聚合得分,准确率立马提升了 3%。
2. 过采样与伪标签生成
由于类别分布严重不均衡,我们用了 SMOTE 和 Easy Data Augmentation (EDA) 方法进行增强。
pip install textattack
from textattack.augmentation import EasyDataAugmenter
eda = EasyDataAugmenter(pct_words_to_swap=0.1, transformations_per_example=3)
augmented_text_list = eda.augment("这辆车撞过两次了,不过修得很好")
配合过采样策略之后,5 类样本数量达到了 1:1.5 的比例,最终验证集上的 recall 提升明显。
第二步:模型结构上“微创新”
原方案是一个简单的 RoBERTa 加 MLP,虽然能跑,但没发挥出潜力。
我们做了一些调整:
1. 使用 Pooling 层之前加 LSTM 或 Attention
import torch.nn as nn
class BiLSTMAttn(nn.Module):
def __init__(self, input_dim, hidden_dim):
super().__init__()
self.lstm = nn.LSTM(input_dim, hidden_dim, bidirectional=True)
self.attn = nn.Linear(hidden_dim * 2, 1)
def forward(self, x):
outputs, _ = self.lstm(x)
weights = torch.softmax(self.attn(outputs), dim=1)
weighted = torch.sum(weights * outputs, dim=1)
return weighted
这部分设计让我们更好地捕捉文本中的长期依赖关系,尤其是在表达复杂场景的时候提升明显。
2. 尝试使用 Deberta + 多任务学习
我们尝试将风险等级预测与关键词提取一起训练,作为一个辅助任务。这样做的好处是可以帮助模型学出更多上下文相关的特征表示。
最终选择的是 HuggingFace 上的 microsoft/deberta-base 预训练模型,在加上一个多头注意力层之后,指标进一步提升。
调参是个技术活:不止是 lr 和 epoch
模型结构定了以后,调参成了最花时间的部分。这里总结几个比较关键的经验点:
1. 学习率调度器的选择
最开始我们用的是固定 lr,但后来换成 CosineAnnealingWithWarmup 后,效果明显更好。特别是在训练后期,loss 波动小了很多。
from transformers import get_cosine_with_hard_restarts_schedule_with_warmup
scheduler = get_cosine_with_hard_restarts_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=num_train_steps,
num_cycles=2
)
2. Batch Size 影响太大
初期 batch_size 设置成 16,显卡吃不满。后来增加到 32 再配上 gradient accumulation,训练速度反而更快,收敛也更稳定。
3. Label Smoothing 解决过拟合问题
我们在交叉熵损失中加入了 label smoothing,缓解类别不平衡带来的过拟合风险。
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
实战中的几个大坑和教训
⛔️ 坑一:Tokenizer 不统一导致 embedding 错位
有个同事在处理数据时用了不同的 tokenizer 版本,导致 embedding 错位,训练出来的模型完全不 work。这个问题花了半天才发现,教训很惨痛。
建议:一定要用同一个 tokenizer 初始化方式加载,并且记录好版本号或 hash。
⛔️ 坑二:本地训练和集群部署参数不一致
本地调试设置 num_workers=0,结果上线时候开到了 4,因为 dataloader 中用了自定义 transform,多线程环境下报错频频。
建议:所有数据处理逻辑必须是线程安全的,否则很容易在分布式训练中翻车。
⛔️ 坑三:早停机制设计不合理
一开始用了简单的 early stop,但如果 val loss 在某个阈值附近震荡,就容易提前终止。后来改成了动态窗口判断,比如连续 5 个 epoch 平均 loss 不下降才停下来。
最终成果:稳了!
经过两个月的反复打磨,我们的模型在测试集上达到了 83.7% 的准确率,F1 得分 0.81,线上 A/B 测试显示相比旧规则系统减少了 22% 的人工审核成本。
更重要的是,训练过程稳定了,每次 run 出来的结果都能保持一致性,不再像最初那样忽高忽低。
经验总结:我的 AI 调优心得
如果让我总结几条给想做好模型训练的朋友:
- 数据质量 > 模型结构。哪怕你用 BERT++,垃圾进照样垃圾出。
- 不要迷信默认参数。lr、bs、dropout 这些都要手动调节。
- 多试组合,别怕折腾。有时候一个小小的结构改动就能带来大幅提升。
- 早点写好监控工具。log、tensorboard、可视化工具要早早配好。
- 留足 baseline 和 AB 对比。没有对比就没有伤害。
最后送大家一句话:调模型不是玄学,也不是拼运气,而是科学 + 工程 + 经验的结合体。
如果你也在调模型的路上,别灰心,坚持下去,总会看到曙光。
附:文章提到的相关库推荐
- Transformers:HuggingFace 官方预训练模型库
- TextAttack:文本增强的好帮手
- Optuna:自动超参搜索利器
- Weights & Biases:非常棒的训练跟踪平台
希望这篇实战笔记对你有所帮助,有什么问题欢迎留言交流!

评论 0