news 2026/9/15 16:54:06

领域语料MLM继续预训练:从数据清洗到RoBERTa实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
领域语料MLM继续预训练:从数据清洗到RoBERTa实战指南

如果你突然接到一个任务,要用公司内部积累了多年的工单语料训练一个NLP模型来做智能客服、舆情分析或者知识检索,你大概率会遇到一个尴尬的局面:开源的中文RoBERTa预训练模型在通用语料上表现不错,但一旦面对满屏的行业黑话、产品缩写、特殊话术,效果就明显拉胯。这时候,真正管用的思路往往不是直接微调(fine-tuning),而是先在自有语料上做一轮Mask Language Model(MLM)预训练,让模型先“读懂”你这摊语料,再做下游任务。这篇文章我会完整记录我在这类项目里的实操路径,包括数据准备、模型选型、训练参数、代码实现和踩坑记录,给同样需要折腾领域预训练的同学一份可以直接上手的参考。

我默认你已经有基本的PyTorch和Transformers使用经验,能跑通一个简单的文本分类任务,但还没到能徒手改BERT源码的地步。这个定位下,下面的方案会更看重稳定、快速和容易复现,而不是把性能压榨到极致。

1. 整体思路与关键决策:为什么需要一份“领域专属”的MLM预训练

1.1 通用预训练模型和行业语料之间的巨大鸿沟

我先用一个实际例子说清楚问题。之前我在处理一批IT运维工单数据时,里面充满了“宕机”“限流”“CDN回源”“pod重启”“SLB健康检查”这类词。通用中文BERT模型见过“宕机”,也见过“CDN”,但在它的语义空间里,“pod”大概率被切分成了“po”和“d”这种没意义的片段,而“SLB”甚至可能被当成一个生僻词丢掉。

这就导致一个很常见的后果:你拿RoBERTa去微调工单分类任务,模型在训练集上能到95%的准确率,到了验证集就掉到82%,而且bad case分析下来几乎都集中在包含大量专有名词的样本上。原因很简单——模型没有在预训练阶段充分学习这些词的上下文分布,你硬让它在下游微调时“临时抱佛脚”,它当然学不稳。

自己做MLM预训练,本质上就是让模型在“你的语言环境”里重新学会“说话”。这不是锦上添花,而是很多垂直领域NLP项目的必经步骤。

1.2 两种常见路线:全量预训练 vs 继续预训练

很多人一听到“预训练”就以为是要从头训练一个BERT,甚至要复现RoBERTa的百万步训练流程。这是最容易被带偏的地方。对于绝大多数垂直领域场景,你根本不需要从头训练,需要的只是继续预训练(continued pretraining)

  • 全量预训练(Pre-training from scratch):从随机初始化参数开始,在超大语料上训练数百万步,需要几十张GPU跑几周。除非你要研究新模型结构,否则完全不推荐。
  • 继续预训练(Continued Pre-training):在开源通用模型(如BERT-base、RoBERTa-base、中文MacBERT)的基础上,用你的领域语料继续做MLM任务。训练几万步,单卡或双卡就能完成。这是目前垂直领域NLP实践中最通用的方案。

继续预训练之所以好用,是因为它保留了通用语言知识,同时把领域知识“注入”到模型参数里。打个比方,一个读过很多书的通才,去医院实习三个月,他就懂医学术语和诊疗逻辑了,但没必要从零开始重新学识字和语法。同理,领域语料存量有限(一般几GB已经算很多),从零训练反而会因为数据不足而学崩。

1.3 什么情况下才值得做这一步

我见过很多团队盲目照搬“领域预训练”流程,结果训完模型,下游任务效果反而变差了。这里有一个前置判断标准:

场景是否建议做MLM继续预训练理由
语料<100MB,领域词汇与通用语料差异不大不建议数据量太少,MLM学不到稳定规律,反而可能遗忘通用知识
语料100MB~10GB,领域有较多专有词汇/句式强烈建议收益最明显的区间,能有效提升下游任务上限
语料>10GB,算力充足建议,但考虑增量训练数据量大时收益趋于饱和,可以结合增量训练方案
下游任务本身数据极少(小于1万条)谨慎即使做了预训练,下游任务样本不足时,效果提升也比较有限

我个人的经验是,如果你手里的领域语料能达到“让一个人类读三个月才能读完”的量级,那做MLM继续预训练大概率是值得的。如果只有几万条短文本,那不如把精力花在数据增强和微调策略上。

1.4 整体流程概览

整个流程可以拆成四个阶段,后面每个章节都对应其中一个阶段:

  1. 数据准备:清洗、去重、切分、构建MLM训练样本。
  2. 模型与分词器准备:选择中文预训练模型,决定是否扩充词表。
  3. 训练实现:基于Transformers框架跑通MLM训练脚本,监控Loss。
  4. 评估与应用:用Perplexity和下游任务验证预训练效果,决定最终模型。

这个流程里,最容易翻车的其实不是训练代码,而是数据准备。Mask Language Model的训练目标很简单,难的是喂给它的数据质量。所以我先花大篇幅把数据这块讲透。

2. 技术底座:模型、分词器与MLM损失函数的底层逻辑

2.1 模型选型:BERT、RoBERTa还是Electra

继续预训练的第一步,是选一个“底子”模型。中文NLP里主流选择无非这几个:

  • BERT-base-chinese:经典选择,分词方式是字粒度(Character-level),词表大小21128。优势是稳定、通用,大量中文模型都从它演进。缺点是静态Mask,训练效率略低。
  • RoBERTa-wwm-ext / MacBERT:哈工大讯飞联合发布的模型,用了全词掩码(Whole Word Masking),在中文上效果明显优于原版BERT。我做中文MLM预训练时最常用这个作为底座。
  • ELECTRA:用生成器-判别器结构,训练效率高,但由于它的预训练任务不是纯MLM,直接拿来做继续预训练需要额外适配,不太适合入门复现。
  • 中文LLM(如ChatGLM、Baichuan):如果你做的是大规模生成任务,可能要考虑这些。但它们做MLM继续预训练的结构适配成本高,且显存消耗大,不在本次分享范围。

我的建议很简单:中文任务无脑选RoBERTa-wwm-ext,英文任务选RoBERTa-base。MacBERT也行,但MacBERT的预训练里引入了纠错任务,继续训练时如果你不改任务头,等价于只做了MLM部分,不够优雅。

从模型结构上看,你不需要修改任何代码——BERT为MLM设计的架构天然支持继续预训练。你只负责给模型喂语料,让它在原有的权重上继续更新。

2.2 中文里必须懂的“全词掩码(Whole Word Masking)”

继续预训练之前,我一直推荐检查分词器是否支持Whole Word Masking(WWM)。这个概念在中文里尤其重要。

标准的MLM做法是:以15%的概率选中一个token(中文里通常是一个字),然后80%概率替换为[MASK],10%概率替换为随机token,10%概率保持不变。但对中文来说,“字”不等于“词”。比如“宕机”是两个token(字),如果只把“宕”掩码住,模型可以通过旁边的“机”字轻易猜出来,因为“宕机”作为一个词共现频率极高。这就让预训练任务变得简单,模型学到的东西就打折扣。

全词掩码的做法是:如果某个字被选中,那它所在整个词的所有字都被掩码。比如“CDN回源”中选中了“回”,则“回源”两个字一起被掩码。这样模型必须真正根据上下文推断,而不是偷看同一词的相邻字。

好在HuggingFace的BertTokenizerFast配合DataCollatorForLanguageModeling,在wwm类模型下默认可以执行全词掩码逻辑。你不需要自己实现,但要在选模型时确认它带wwm标记,并且训练时不要误用了不匹配的Tokenizer。

2.3 MLM的损失函数:为什么只算被Mask位置的Loss

经常有人问我:继续预训练时,模型的损失函数是怎么定义的?是不是跟文本生成一样,每个位置都要算交叉熵?

不是。MLM只对被掩码(Masked)的位置计算交叉熵损失。每一条样本里,被掩码的token大概是15%,也就是说,80%的token在计算梯度时是完全不参与的。这是一个巨大的区别。

用公式来理解,对一个样本序列 ( x = [x_1, x_2, ..., x_n] ),掩码后的序列是 ( x_{masked} )。模型前向得到每个位置的预测分布 ( P(x_i | x_{masked}) ),但损失函数只考虑被掩码位置的集合 ( M ):

[ \mathcal{L}{MLM} = - \sum{i \in M} \log P(x_i | x_{masked}) ]

第一次跑MLM训练的同学,看到Loss很小不用惊讶——因为它只衡量被Mask的15%的位置的预测能力。这也解释了一个现象:MLM Loss一般在1.0~4.0之间波动,很难降到0.5以下。如果一个位置的预测难度低,整体Loss自然就小。比如高频词“的”被掩码时,模型轻松猜对,Loss贡献就很低;而掩码一个冷门机构名“中科寒武纪”时,Loss就会剧烈上升。

2.4 关键超参数:掩码概率与Mask/Replace/Keep的比例

训练脚本里最重要的超参数是mlm_probability,默认0.15。这个值不是拍脑袋定的,而是BERT论文里的遗产。0.15的含义是:每个token有15%的概率成为“候选掩码位置”,在候选位置上再按80/10/10的比例决定具体操作。

操作概率目的
[MASK]替换80%让模型学会根据上下文填空
随机token替换10%让模型学会纠错,缓解[MASK]只在预训练出现、微调阶段消失的gap
保持不变10%让模型把注意力放在上下文编码上

你可能会问:为什么不让[ MASK ]的比例更高?如果100%都替换成[MASK],模型会过度依赖[MASK]标记,下游微调阶段没有[MASK]时它就“不习惯”。10%随机替换和10%保持不变是对抗这种分布偏移的trick。我们在数据处理里经常说“别把训练和推理的分布搞不一样”,这里就是典型例子。

我自己在垂直领域语料上做测试时,mlm_probability设成0.15是没问题的。只有当语料极短(比如平均长度小于20个词)时,我会升到0.2,因为短句里15%的选择样本太稀疏,模型更新信号不足。

2.5 学习率与优化器:继续预训练不是微调

继续预训练和下游微调有一个重要区别:学习率不能太大。

微调阶段我们常用2e-5到5e-5的学习率,因为此时模型已经收敛,只希望在小范围内调整参数。而继续预训练如果也用5e-5,很容易破坏通用语义表征,导致灾难性遗忘——模型把领域知识学进去了,但把通用能力丢了。

我的经验值:

  • 继续预训练:峰值学习率1e-4到2e-4,配合Warmup。用AdamW优化器。
  • 如果训练数据领域差异十分大(比如通用中文→古文),可以把学习率降到1e-4以下,减少对原有知识的冲击。
  • 如果数据规模较大(>1GB),可以用2e-4,加速领域知识吸收。

这只是经验参考,更科学的方法是跑一个小规模实验,对比不同学习率在验证集Perplexity上的表现,选那个验证Loss最低的。

2.6 词表扩充:什么时候需要,什么时候不要

继续预训练里还有一个高频话题:要不要扩充词表?

开源的中文RoBERTa词表包含21128个token(字、常见词、特殊符号)。如果你的语料里全是专业术语,例如生物信息学里的“碱基对”“逆转录”“CRISPR”,原始词表很可能把这些术语切成一堆单字。模型虽然还是能通过单字组合学习语义,但学习效率和效果会打折扣。

此时你有两个选择:

  1. 不扩充词表:简单省事,模型通过字级上下文也能学会领域语义,只是路径更长。适合领域词占比不高的场景。
  2. 扩充词表:用分词器在领域语料上统计高频词片段,往词表里加新token,然后用新词表重新初始化embedding矩阵并继续预训练。收益可能更大,但工程复杂度显著更高。

我的建议:第一阶段先不扩充词表,直接用原始词表做继续预训练。如果下游任务效果始终上不去,再考虑扩容词表。扩充词表需要熟练掌握tokenizer.add_tokens()和模型resize_token_embeddings()的配合,容易踩坑,后面常见问题章节我会展开讲。

3. 数据准备:语料清洗、样本切分与Tokenization实操

3.1 准备好你的语料:格式与清洗规范

MLM预训练不需要标注数据,只需要纯文本。这个“纯”字很关键,我踩过的坑大多来自清洗不彻底。

以工单语料为例,原始数据可能是数据库导出的JSON,里面除了正文,还有工单号、时间戳、操作人、状态字段等。清洗的第一步是只保留正文,并且把HTML标签、URL、特殊符号、乱码全部清掉。如果里面还有客服和客户的对话标记,比如“用户:”“客服:”,我倾向于保留这些前缀——它们是真实业务场景的特征,模型需要学习两者的语言风格差异。

一个标准的数据清洗流程:

  1. 去重:用哈希或SimHash删除重复文本。重复语料会让模型过拟合到重复片段。
  2. 非文本内容清理:删除HTML标签、Markdown标记、URL、emoji(或替换为特殊token<EMOJI>)。
  3. 噪声过滤:剔除过短的句子(小于5个字)、全数字或全符号的文本。
  4. 统一格式:全角转半角(中文标点保留)、连续空格压缩、规范化换行。
  5. 文档切分:如果语料本身就是长文档,建议按段落切割。BERT的最大序列长度通常是512,过长的段落需要截断,过短的段落可以拼接。

清洗规范做得越细,后面训练时模型学到的噪声就越少。我甚至会把“重复字符”也规范化,比如“哈哈哈”压缩成“哈”,因为重复字符的预测难度高,会拉偏Loss。

3.2 数据规模估算:多少语料才够“预训练”

很多第一次做继续预训练的同学会问:我只有500MB文本,够吗?

从经验来看,继续预训练对数据量的要求远低于从零预训练。一张通俗的对照表:

数据量可支撑的训练步数预期效果
50MB~200MB500~2000步小幅提升,适合快速实验验证
200MB~1GB2000~10000步中等提升,领域词汇有明显改善
1GB~10GB10000~50000步显著提升,模型领域语义基本成型
10GB以上更多收益递减,需考虑算力成本

这里的“步数”还取决于batch size。一个通用公式:总token数 / (batch_size × seq_len) = 每个epoch的step数。通常训练2~3个epoch就够了,再多容易过拟合。

举个例子,你有1GB中文文本,约5亿个token。batch_size=32,seq_len=512,则每个batch包含32×512=16384个token。5亿/16384≈30517步一个epoch。训练1个epoch在当前消费级显卡上大约要跑几天,属于合理范围。如果你只有200MB文本,一个epoch大约6000步,训练量完全可以接受。

3.3 如何把语料“喂”给BERT:Tokenization与Block切分

Text文件不能直接喂给BERT,需要先转成token id序列,并按固定长度切成块。这里有两个常见的坑:

  • BERT的Tokenizer会把每个样本编码成input_idsattention_mask,但如果你直接把每条文本编码后再单独padding到512,会产生大量无效计算。最优做法是先拼接所有文本,然后按512长度连续切块,不padding。
  • 多文档拼接时,需要在两个文档之间加[SEP]标记,避免模型认为所有文本来自同一篇文章。

使用HuggingFace的datasets库,可以用map函数批量tokenize。下面给一段核心代码:

from datasets import load_dataset from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("hfl/rbt3") dataset = load_dataset("text", data_files="corpus.txt", split="train") def tokenize_function(examples): return tokenizer(examples["text"], truncation=False, add_special_tokens=True) tokenized_dataset = dataset.map( tokenize_function, batched=True, remove_columns=["text"], num_proc=8, ) # 将token序列按block_size=512切分 block_size = 512 def group_texts(examples): concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()} total_length = len(concatenated_examples["input_ids"]) total_length = (total_length // block_size) * block_size result = { k: [t[i : i + block_size] for i in range(0, total_length, block_size)] for k, t in concatenated_examples.items() } return result lm_dataset = tokenized_dataset.map(group_texts, batched=True, num_proc=8)

这段代码的核心逻辑是:先把语料全部tokenize成id序列,然后切块。group_texts函数会在一个batch内部把所有样本的token id“拍扁”再切成固定长度块,确保每个样本长度都是512,不需要padding,训练效率最高。

要注意的是,add_special_tokens=True会在每段文本首尾加上[CLS]和[SEP],多个文档拼接时模型能感知边界。如果你用的是group_texts的方式,不同文档的边界信息会被块切分模糊化——这其实是BERT预训练的标准做法,模型会逐渐学会[SEP]位置的语义,不必过度担心。

3.4 动态掩码(Dynamic Masking)为什么对继续预训练更重要

原版BERT在数据预处理阶段就固定了掩码位置,训练时每个epoch看到的掩码方式一模一样,这被称为静态掩码(Static Masking)。静态掩码的缺点是模型容易“背题”——同一个位置的掩码反复出现,模型记住了答案而没学到上下文。

RoBERTa引入的改进是动态掩码(Dynamic Masking):每次喂给模型数据时,重新随机生成掩码位置。这样同一个句子在多个epoch中会以不同的掩码形式出现,相当于数据量变多了。

在HuggingFace里,动态掩码不是通过数据集预处理实现的,而是通过DataCollatorForLanguageModeling在每batch生成时动态执行。所以后续训练脚本里,data_collator是MLM预训练的“灵魂组件”。如果你遗漏了它,模型训练时会因为没有掩码标签而直接报错。

4. 实操过程:用Transformers在自己的语料上跑通MLM预训练

4.1 环境准备与依赖安装

开始之前,先准备好环境。我的建议版本组合(避免踩兼容性坑):

transformers>=4.30.0 datasets>=2.12.0 tokenizers>=0.13.0 torch>=2.0.0 accelerate>=0.20.0

安装命令很简单:

pip install transformers datasets tokenizers accelerate

如果你有GPU,请确保CUDA可用:

import torch print(torch.cuda.is_available()) # True print(torch.cuda.get_device_name(0))

没有GPU也可以跑通,但速度会非常慢。MLM继续预训练哪怕只跑几千步,也强烈建议用GPU。如果你只有CPU,可以先把数据量缩小到50MB做流程验证,再提交到GPU服务器上跑正式版。

4.2 构建DataCollator:MLM预训练的心脏

接下来是训练前最重要的一步——构建DataCollatorForLanguageModeling。它会在每个batch里动态做mask,并生成对应的labels

from transformers import DataCollatorForLanguageModeling data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15, )

这段代码会让collator对每个batch的input_ids做以下操作:

  • 随机选15%的token作为候选掩码位置。
  • 其中80%替换为tokenizer.mask_token_id
  • 10%替换为词表中的随机token id。
  • 10%保持原id不变。
  • 生成labels张量,被掩码位置记录原始token id,其余位置为-100(交叉熵损失自动忽略)。

labels里用-100填充非掩码位置,是因为PyTorch的CrossEntropyLoss默认忽略ignore_index=-100。这一机制让你不需要手动写mask loss函数。

如果你用的是BertTokenizerFast且模型是wwm类型,DataCollatorForLanguageModeling会自动执行全词掩码,无需额外配置。这里再强调一次:确保tokenizermodel来自同一个模型版本,别搞混。用RoBERTa-wwm的tokenizer配BERT的模型,词表id对不上,Loss会直接飞掉。

4.3 加载预训练模型与配置训练参数

选择底座模型后,加载方式如下。这里以rbt3(RoBERTa-wwm-ext-small)为例,适合快速验证;如果你的语料量大且算力充足,可以换成hfl/chinese-roberta-wwm-ext(base版)。

from transformers import AutoModelForMaskedLM, AutoTokenizer model_name = "hfl/rbt3" # 小模型,适合验证流程 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForMaskedLM.from_pretrained(model_name) # 查看参数量 total_params = sum(p.numel() for p in model.parameters()) print(f"Total parameters: {total_params / 1e6:.2f}M")

AutoModelForMaskedLM会自动加载BERT的MLM head(一个带权重的分类层),这个head在预训练时是必须的。如果你加载的是AutoModel而不是AutoModelForMaskedLM,你会发现模型没有lm_head,没办法计算MLM损失——这是新手最常见的错误。

4.4 训练脚本:使用Trainer实现完整的MLM预训练

HuggingFace的Trainer把训练循环封装得很干净,适合大多数场景。下面给一份完整可跑的脚本:

from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir="./mlm_output", overwrite_output_dir=True, num_train_epochs=2, per_device_train_batch_size=16, per_device_eval_batch_size=16, gradient_accumulation_steps=2, learning_rate=1.5e-4, warmup_steps=1000, weight_decay=0.01, logging_dir="./logs", logging_steps=100, save_steps=2000, eval_steps=1000, evaluation_strategy="steps", save_total_limit=2, fp16=True, # 仅在GPU支持时开启 dataloader_num_workers=4, ) trainer = Trainer( model=model, args=training_args, train_dataset=lm_dataset["train"], eval_dataset=lm_dataset["validation"], data_collator=data_collator, tokenizer=tokenizer, ) trainer.train()

参数解释:

  • per_device_train_batch_size=16:如果显存不足(12GB以下),降到8或4。
  • gradient_accumulation_steps=2:相当于把实际batch size翻倍到32,稳定训练。
  • learning_rate=1.5e-4:继续预训练经验值,不建议一上来就设2e-4。
  • warmup_steps=1000:前1000步学习率线性增长,避免训练初期剧烈震荡。
  • eval_steps=1000:每1000步跑一次验证集,监控Loss。
  • save_total_limit=2:只保留最新两个checkpoint,避免磁盘爆掉。

有一点值得说明:MLM继续预训练的参数量虽然和微调一样,但它的优化目标更“简单”——只需要让模型在掩码位置输出正确的词。所以收敛速度远快于从头预训练。你往往能在几千步内看到验证Loss明显下降。

4.5 想用自定义掩码策略?看这里

虽然DataCollatorForLanguageModeling够用,但如果你有特殊需求(比如不想掩码某些特殊字符、或者想让某些领域关键词拥有更高掩码概率),可以继承类改写。

比如,我曾在工单语料中希望提高产品名的掩码概率,因为产品名是下游分类的关键特征。做法是自定义一个“加权掩码”的collator:

import torch from transformers import DataCollatorForLanguageModeling class WeightedDataCollatorForLanguageModeling(DataCollatorForLanguageModeling): def __init__(self, tokenizer, mlm_probability=0.15, high_freq_ids=None): super().__init__(tokenizer=tokenizer, mlm_probability=mlm_probability) self.high_freq_ids = high_freq_ids or [] def torch_mask_tokens(self, inputs, special_tokens_mask=None): labels = inputs.clone() probability_matrix = torch.full(labels.shape, self.mlm_probability) # 提高关键token的掩码概率 for token_id in self.high_freq_ids: probability_matrix[inputs == token_id] = min(self.mlm_probability * 3, 0.6) # 其余逻辑与父类一致 masked_indices = torch.bernoulli(probability_matrix).bool() ...

这种做法适合领域特征极为集中的语料。实操中我建议先在标准mlm_probability=0.15上跑通,再考虑加权方案,避免一开始就引入太多变量。

4.6 训练时间估算与checkpoint选择

在我常用的单卡V100(32GB)上,训练一个小型rbt3模型,batch_size=16、seq_len=512,速度大概是每秒4~5步。如果语料对应6000步一个epoch,训练2个epoch需要约3000秒,一个小时内搞定,非常轻量。

如果是bert-base级别(参数量约110M),同样卡上每秒约2~3步,同样的量需要2~3小时。如果你的资源很紧张,可以先拿rbt3跑通全流程,确认效果方向正确后再用base模型做正式实验。

保存checkpoint时,我建议保留训练完成后的最终模型即可。Trainer会自动保存最后一个epoch的权重,你可以在output_dir里找到pytorch_model.binconfig.json

4.7 生成训练集时的Poisoning风险:别让模型看到Future Labels

这里有一个容易被忽略的细节:如果你是在有大字段的数据库里导出的语料,比如“订单标题+商品描述+售后结论”,你在清洗时如果不小心把“售后结论”也当成正文包含进去,模型预训练时就会“看到”未来信息(结论),这不仅不影响MLM训练本身,反而会制造一种假象——下游任务(比如预测退款风险)场景下,模型在测试时拿不到结论字段,效果会崩得很难看。

所以清洗时一定要保留字段边界:只保留模型下游使用时“已知”的信息。比如做工单分类,正文和用户描述可以喂给MLM,但“最终处理结果”绝不能喂进去。这个原则叫作“不要让你的预训练语料泄露未来标签”。

5. 评估与调优:从Loss到下游任务效果的全链路验证

5.1 如何判断预训练是否有效:验证集Loss与Perplexity

训练过程中的Loss曲线是最直接的反馈。MLM Loss在继续预训练场景下,通常会在前几百步快速下降,然后进入缓慢下降的平台期。如果你发现Loss不降反升,那说明学习率可能过大,或者数据清洗有问题。

有了验证集Loss,就可以算Perplexity(PPL),直觉理解是“模型预测答案时的困惑程度”:PPL越低,说明模型对语料的掌握越好。

[ PPL = \exp(\text{Loss}) ]

举个例子,Loss=2.0时,PPL≈7.4,意味着模型在候选词中选择正确答案的平均困难度是7.4选1。Loss=1.5时,PPL≈4.5,显著优于随机猜测(中文词表是21128,随机猜的PPL约等于词表大小)。继续预训练的目标是把PPL从初始值(比如30~50)压到5以下,理想情况接近领域内语言模型的正常水平。

不过别盲目追求极低Loss。如果你的Loss降到0.8以下,PPL≈2.2,那可能是模型“背”下了语料中的常见搭配,对下游任务反而不一定有利。预训练的目标是学到可泛化的语义,不是记住语料。

下面是一份我在工单语料上跑出来的典型曲线参考:

训练步数Loss(验证集)PPL备注
04.266.7初始通用模型在领域语料上的表现
5002.816.4快速学习阶段
15002.18.2领域词开始被“理解”
30001.86.0语言规律趋于稳定
60001.65.0平台期,继续训练收益变小

这类曲线只能在“领域语料”上计算。如果在通用语料上做验证,Loss通常比这低很多,因为通用模型本来就在那类数据上训练过。

5.2 下游任务验证:预训练效果到底有没有用

Loss下降还不够,你最终要回答的问题是:训练出的模型在真实任务上有没有变强。

最稳妥的验证方式是选一两个下游任务做A/B对比:

  1. Baseline模型:直接用原始预训练权重做微调,得到指标A。
  2. 继续预训练模型:用你的MLM checkpoint做微调,得到指标B。
  3. 对比指标A和B:如果B显著优于A,说明继续预训练有价值;如果差距不大,说明领域语料与通用语料差异不大,或者数据量不够。

我做过一次实验:在IT工单多分类任务上,原始RoBERTa的F1是82.3,MLM继续预训练后F1到85.1,提升了近3个点。在情感分析任务上,提升相对小,只有0.8个点,因为情感表达与领域差异相关性弱。

建议你固定种子(例如seed=42),多次运行取平均,避免把随机波动当成真实提升。微调阶段用同样的超参数和epoch数,否则无法归因。

5.3 检查模型真的学到了什么:用Fill-Mask做定性验证

定量指标之外,还有一个非常直观的定性验证方法——直接用模型的fill-mask能力测试领域知识。

from transformers import pipeline fill_mask = pipeline("fill-mask", model="./mlm_output", tokenizer="./mlm_output") # 工单场景 result = fill_mask("服务器 [MASK] 导致服务不可用,需要立即处理。") for item in result: print(f"{item['token_str']}: {item['score']:.4f}")

如果模型在[MASK]位置预测出“宕机”“死机”“重启”等高相关词,说明领域语义确实被吸收了。如果还是预测出“故障”“问题”这种通用词,说明领域词汇还没充分学习,可以继续训练或增加数据。

5.4 训练日志分析:一个案例拆解

我在训练一次电力行业语料时,把训练日志拉出来看,发现一段有意思的过程:

  • 第200步时,验证Loss是3.4,PPL约30。
  • 第600步时,Loss降到2.4,模型明显在“拼命学”行业词。
  • 第1200步后,Loss降速变慢,进入平台期。

这提示我:文本里大量电力专业词汇(“继电保护”“变电站”“负荷预测”)正在被模型消化。之后我用fill-mask测试,输入“线路[MASK]导致跳闸”,模型预测“故障”概率0.72,“过载”概率0.11,“短路”概率0.08,已经能给出行业相关性很高的结果。

从这类日志里,你能很直观感觉到“模型正在进入你的领域”,这种反馈比任何指标都安慰人。

6. 常见问题与排查技巧实录

6.1 问题速查表

我把实操中遇到的典型问题整理成一个速查表,方便你对照排错。

现象可能原因处理方式
Loss不下降或升高学习率过大 / 数据清洗不干净 / 模型与tokenizer不匹配降低学习率至5e-5,检查语料是否有大量重复噪声
训练时出现NaN学习率过高 / 梯度爆炸 / fp16精度问题调低学习率,关闭fp16,或增加梯度裁剪(max_grad_norm=1.0
显存不够(OOM)batch_size过大 / seq_len过大降低batch_size,开启gradient accumulation,或缩短seq_len到256
Tokenizer编码乱码全角半角混用 / 未清洗特殊字符清洗时统一转半角,过滤无法解码的bytes
下游任务反而变差过度训练导致灾难性遗忘 / 微调数据量太少减少预训练步数,采用更小学习率,或跳过后直接微调
模型输出的[MASK]位置全是unk词表没有覆盖领域词,且未扩充考虑扩充词表,或用原始tokenizer不额外训练
验证集PPL很低但下游没提升模型“死记硬背”了语料常见搭配,语义泛化不足增加语料多样性,加强清洗去重,降低训练epoch

6.2 坑一:忘记给[PAD]之外的token做掩码保护

某些token不应该参与掩码,比如[CLS][SEP][PAD]。如果你在自定义collator时没有拒绝这些特殊token,模型可能会把[CLS]掩码掉,导致预训练目标包含了无意义的内容。

DataCollatorForLanguageModeling内部已经通过special_tokens_mask处理了这个问题,但如果你自己写collator,务必记得:

special_tokens_mask = [ tokenizer.get_special_tokens_mask(val, already_has_special_tokens=True) for val in labels.tolist() ]

然后把special_tokens_mask对应的位置排除在掩码候选之外。

6.3 坑二:block_size设置太大导致长文本语义割裂

如果语料以短文本为主(比如工单日志、对话片段),把block_size设为512会导致多个文本被拼接进同一个块,模型很难区分不同样本的边界,也可能学到“跨样本”的错误依赖。

解决思路:根据语料平均长度动态调整block_size。如果平均长度只有128,那就用128作为块大小,保证每个块大致对应一个完整样本。短文本拼接时,多利用[SEP]做边界标记,效果会更好。

6.4 坑三:扩充词表后忘记resize_token_embeddings

如果你按2.6节的思路扩充了词表,调用了tokenizer.add_tokens(new_tokens)之后,必须同步调整模型embedding层:

model.resize_token_embeddings(len(tokenizer))

如果不做这一步,模型输出层的维度(vocab_size)与词表长度不一致,前向传播时直接报维度不匹配错误。而且新加的token embedding是随机初始化的,需要额外训练一段时间才能“适应”模型,所以扩充词表后训练步数要适当增加。

6.5 坑四:数据文件名或格式不一致

datasets读取语料时,如果文本文件编码是GBK,而load_dataset("text")默认按UTF-8读取,会直接报错或读出乱码。正确做法是提前统一编码为UTF-8:

iconv -f GBK -t UTF-8 corpus_raw.txt > corpus_utf8.txt

另外,如果语料文件很大(超过几个GB),建议load_dataset时用num_proc并行读取,避免单进程耗时太久。

6.6 坑五:用fp16训练时Loss出现诡异波动

开启fp16=True可以在V100/A100上加速训练,但半精度训练在某些显卡和数据分布下会出现Loss震荡或NaN。如果你观察到Loss曲线剧烈抖动,先关掉fp16跑几百步看是否恢复稳定。如果确认是fp16导致的,可以考虑用bf16=True(Ampere架构及以上支持),bf16的动态范围比fp16大,稳定性更好。

6.7 训练完无法加载checkpoint

如果你保存的checkpoint目录里只有pytorch_model.binconfig.jsonoptimizer.pt,但缺少tokenizer文件,当你用AutoModelForMaskedLM.from_pretrained(checkpoint_dir)加载时,模型能加载,但tokenizer必须单独加载。

解决办法:保存时最好把tokenizer也存到同一目录:

model.save_pretrained("./mlm_output") tokenizer.save_pretrained("./mlm_output")

之后加载:

model = AutoModelForMaskedLM.from_pretrained("./mlm_output") tokenizer = AutoTokenizer.from_pretrained("./mlm_output")

这条经验很基础,但我在多个项目里看到同事栽在这上面。

7. 进阶扩展:从MLM到更多自监督信号的迁移路径

跑通MLM继续预训练后,你可能会发现一个自然的追问:除了MLM,还有没有其他自监督任务可以在领域语料上继续训练?

有。最常见的两个方向是:

  • Sentence Order Prediction(SOP):ALBERT使用的任务,交换句子顺序让模型判断是否打乱。对长文档语义理解更有帮助。
  • Token-level Contrastive Learning:类似SimCSE的思路,让模型在一个batch内区分相似与不相似的token表示。这需要额外构造正负样本,实现成本更高。

但从实践性价比来看,MLM仍然是垂直领域继续预训练的第一选择,因为它实现简单,效果稳定,且和下游微调的适配度最高。如果你把MLM跑到平台期,效果依然不够,再考虑叠加其他损失。

另外,如果你最终要做的是生成任务,也可以考虑在通用LLM上做“增量预训练”,方式与MLM类似,只是损失函数换成了自回归交叉熵。两者的数据准备流程高度重合——清洗、切块、去重这些经验完全能复用。

我想强调的是:自监督预训练本质上是在“压缩语料中的信息”。你提供的语料质量越高、领域特性越清晰,MLM能压缩出的有效知识就越多。这也是为什么我一直强调数据清洗比模型调参更重要的原因。

8. 写在最后:关于一套可复用的判断框架

跑过完整的MLM继续预训练流程之后,我现在拿到一个新领域项目,会先用这套判断框架快速决策:

  1. 通用预训练模型在这个领域语料上的PPL是否明显偏高(比如大于20)?如果是,继续预训练大概率有收益。
  2. 领域语料是否有100MB以上的干净文本?如果没有,先去做数据积累,别急着训练。
  3. 下游任务是否对领域词汇敏感?分类、NER、语义相似度、检索这些任务都高度依赖领域语义,继续预训练收益明显;纯情感打分类任务收益相对有限。
  4. 算力是否允许跑至少3000步?如果只够跑500步,效果可能不明显,不如直接微调。

这套框架帮我避免了很多“为了训练而训练”的无效工作。希望你读完这篇文章后,不仅掌握了MLM继续预训练的代码细节,也能建立起一个更清醒的判断标准:预训练是手段,不是目的;领域语料是燃料,但也要用之有度。

最后再分享一个小技巧:训练完成后,别急着把模型部署到生产环境。先用一个小型的下游任务评测集(几百条标注样例就够)对比原始模型和继续预训练模型在真实场景上的差异。这个“最小可行性验证”只要半天时间,却能帮你避免把一个大但无效的模型推到线上。毕竟,模型参数再多,不如在真实数据上“懂行”来得重要。

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

Unity DOTS万人同屏实战:ECS与Job System优化萌宠战场

做类萌宠宠之战这种游戏&#xff0c;最让我上头的画面不是什么高清大世界&#xff0c;而是战斗刚开的那个瞬间&#xff1a;上万只宠物从四面八方涌向同一个目标&#xff0c;屏幕上密密麻麻全是单位&#xff0c;压迫感直接拉满。但这份压迫感首先压垮的是程序员自己。我第一版原…

作者头像 李华
网站建设 2026/9/15 16:50:49

搞定备案不踩坑,专注微商推广的网站源码下载避坑指南

搞定备案不踩坑,专注微商推广的网站源码下载避坑指南 备案流程一头雾水?别急,我懂你。很多做微商的朋友,手里拿着精心挑选的【专注微商推广的网站】源码,准备大干一场,结果卡在ICP备案这一步,直接懵圈。…

作者头像 李华
网站建设 2026/9/15 16:48:39

Archify社区指南:如何提交你的架构图到Showcase画廊

Archify社区指南&#xff1a;如何提交你的架构图到Showcase画廊 【免费下载链接】archify Agent skill for beautiful, verifiable architecture, workflow, sequence, data-flow, and lifecycle diagrams—self-contained HTML with motion and crisp export. 项目地址: htt…

作者头像 李华