尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

领域语料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 整体流程概览整个流程可以拆成四个阶段后面每个章节都对应其中一个阶段数据准备清洗、去重、切分、构建MLM训练样本。模型与分词器准备选择中文预训练模型决定是否扩充词表。训练实现基于Transformers框架跑通MLM训练脚本监控Loss。评估与应用用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%概率替换为随机token10%概率保持不变。但对中文来说“字”不等于“词”。比如“宕机”是两个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”原始词表很可能把这些术语切成一堆单字。模型虽然还是能通过单字组合学习语义但学习效率和效果会打折扣。此时你有两个选择不扩充词表简单省事模型通过字级上下文也能学会领域语义只是路径更长。适合领域词占比不高的场景。扩充词表用分词器在领域语料上统计高频词片段往词表里加新token然后用新词表重新初始化embedding矩阵并继续预训练。收益可能更大但工程复杂度显著更高。我的建议第一阶段先不扩充词表直接用原始词表做继续预训练。如果下游任务效果始终上不去再考虑扩容词表。扩充词表需要熟练掌握tokenizer.add_tokens()和模型resize_token_embeddings()的配合容易踩坑后面常见问题章节我会展开讲。3. 数据准备语料清洗、样本切分与Tokenization实操3.1 准备好你的语料格式与清洗规范MLM预训练不需要标注数据只需要纯文本。这个“纯”字很关键我踩过的坑大多来自清洗不彻底。以工单语料为例原始数据可能是数据库导出的JSON里面除了正文还有工单号、时间戳、操作人、状态字段等。清洗的第一步是只保留正文并且把HTML标签、URL、特殊符号、乱码全部清掉。如果里面还有客服和客户的对话标记比如“用户”“客服”我倾向于保留这些前缀——它们是真实业务场景的特征模型需要学习两者的语言风格差异。一个标准的数据清洗流程去重用哈希或SimHash删除重复文本。重复语料会让模型过拟合到重复片段。非文本内容清理删除HTML标签、Markdown标记、URL、emoji或替换为特殊tokenEMOJI。噪声过滤剔除过短的句子小于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_size32seq_len512则每个batch包含32×51216384个token。5亿/16384≈30517步一个epoch。训练1个epoch在当前消费级显卡上大约要跑几天属于合理范围。如果你只有200MB文本一个epoch大约6000步训练量完全可以接受。3.3 如何把语料“喂”给BERTTokenization与Block切分Text文件不能直接喂给BERT需要先转成token id序列并按固定长度切成块。这里有两个常见的坑BERT的Tokenizer会把每个样本编码成input_ids和attention_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_filescorpus.txt, splittrain) def tokenize_function(examples): return tokenizer(examples[text], truncationFalse, add_special_tokensTrue) tokenized_dataset dataset.map( tokenize_function, batchedTrue, remove_columns[text], num_proc8, ) # 将token序列按block_size512切分 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, batchedTrue, num_proc8)这段代码的核心逻辑是先把语料全部tokenize成id序列然后切块。group_texts函数会在一个batch内部把所有样本的token id“拍扁”再切成固定长度块确保每个样本长度都是512不需要padding训练效率最高。要注意的是add_special_tokensTrue会在每段文本首尾加上[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 环境准备与依赖安装开始之前先准备好环境。我的建议版本组合避免踩兼容性坑transformers4.30.0 datasets2.12.0 tokenizers0.13.0 torch2.0.0 accelerate0.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 构建DataCollatorMLM预训练的心脏接下来是训练前最重要的一步——构建DataCollatorForLanguageModeling。它会在每个batch里动态做mask并生成对应的labels。from transformers import DataCollatorForLanguageModeling data_collator DataCollatorForLanguageModeling( tokenizertokenizer, mlmTrue, mlm_probability0.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会自动执行全词掩码无需额外配置。这里再强调一次确保tokenizer和model来自同一个模型版本别搞混。用RoBERTa-wwm的tokenizer配BERT的模型词表id对不上Loss会直接飞掉。4.3 加载预训练模型与配置训练参数选择底座模型后加载方式如下。这里以rbt3RoBERTa-wwm-ext-small为例适合快速验证如果你的语料量大且算力充足可以换成hfl/chinese-roberta-wwm-extbase版。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(fTotal 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_dirTrue, num_train_epochs2, per_device_train_batch_size16, per_device_eval_batch_size16, gradient_accumulation_steps2, learning_rate1.5e-4, warmup_steps1000, weight_decay0.01, logging_dir./logs, logging_steps100, save_steps2000, eval_steps1000, evaluation_strategysteps, save_total_limit2, fp16True, # 仅在GPU支持时开启 dataloader_num_workers4, ) trainer Trainer( modelmodel, argstraining_args, train_datasetlm_dataset[train], eval_datasetlm_dataset[validation], data_collatordata_collator, tokenizertokenizer, ) trainer.train()参数解释per_device_train_batch_size16如果显存不足12GB以下降到8或4。gradient_accumulation_steps2相当于把实际batch size翻倍到32稳定训练。learning_rate1.5e-4继续预训练经验值不建议一上来就设2e-4。warmup_steps1000前1000步学习率线性增长避免训练初期剧烈震荡。eval_steps1000每1000步跑一次验证集监控Loss。save_total_limit2只保留最新两个checkpoint避免磁盘爆掉。有一点值得说明MLM继续预训练的参数量虽然和微调一样但它的优化目标更“简单”——只需要让模型在掩码位置输出正确的词。所以收敛速度远快于从头预训练。你往往能在几千步内看到验证Loss明显下降。4.5 想用自定义掩码策略看这里虽然DataCollatorForLanguageModeling够用但如果你有特殊需求比如不想掩码某些特殊字符、或者想让某些领域关键词拥有更高掩码概率可以继承类改写。比如我曾在工单语料中希望提高产品名的掩码概率因为产品名是下游分类的关键特征。做法是自定义一个“加权掩码”的collatorimport torch from transformers import DataCollatorForLanguageModeling class WeightedDataCollatorForLanguageModeling(DataCollatorForLanguageModeling): def __init__(self, tokenizer, mlm_probability0.15, high_freq_idsNone): super().__init__(tokenizertokenizer, mlm_probabilitymlm_probability) self.high_freq_ids high_freq_ids or [] def torch_mask_tokens(self, inputs, special_tokens_maskNone): 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_probability0.15上跑通再考虑加权方案避免一开始就引入太多变量。4.6 训练时间估算与checkpoint选择在我常用的单卡V10032GB上训练一个小型rbt3模型batch_size16、seq_len512速度大概是每秒4~5步。如果语料对应6000步一个epoch训练2个epoch需要约3000秒一个小时内搞定非常轻量。如果是bert-base级别参数量约110M同样卡上每秒约2~3步同样的量需要2~3小时。如果你的资源很紧张可以先拿rbt3跑通全流程确认效果方向正确后再用base模型做正式实验。保存checkpoint时我建议保留训练完成后的最终模型即可。Trainer会自动保存最后一个epoch的权重你可以在output_dir里找到pytorch_model.bin和config.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}) ]举个例子Loss2.0时PPL≈7.4意味着模型在候选词中选择正确答案的平均困难度是7.4选1。Loss1.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对比Baseline模型直接用原始预训练权重做微调得到指标A。继续预训练模型用你的MLM checkpoint做微调得到指标B。对比指标A和B如果B显著优于A说明继续预训练有价值如果差距不大说明领域语料与通用语料差异不大或者数据量不够。我做过一次实验在IT工单多分类任务上原始RoBERTa的F1是82.3MLM继续预训练后F1到85.1提升了近3个点。在情感分析任务上提升相对小只有0.8个点因为情感表达与领域差异相关性弱。建议你固定种子例如seed42多次运行取平均避免把随机波动当成真实提升。微调阶段用同样的超参数和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.4PPL约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_norm1.0显存不够(OOM)batch_size过大 / seq_len过大降低batch_size开启gradient accumulation或缩短seq_len到256Tokenizer编码乱码全角半角混用 / 未清洗特殊字符清洗时统一转半角过滤无法解码的bytes下游任务反而变差过度训练导致灾难性遗忘 / 微调数据量太少减少预训练步数采用更小学习率或跳过后直接微调模型输出的[MASK]位置全是unk词表没有覆盖领域词且未扩充考虑扩充词表或用原始tokenizer不额外训练验证集PPL很低但下游没提升模型“死记硬背”了语料常见搭配语义泛化不足增加语料多样性加强清洗去重降低训练epoch6.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_tokensTrue) 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-8iconv -f GBK -t UTF-8 corpus_raw.txt corpus_utf8.txt另外如果语料文件很大超过几个GB建议load_dataset时用num_proc并行读取避免单进程耗时太久。6.6 坑五用fp16训练时Loss出现诡异波动开启fp16True可以在V100/A100上加速训练但半精度训练在某些显卡和数据分布下会出现Loss震荡或NaN。如果你观察到Loss曲线剧烈抖动先关掉fp16跑几百步看是否恢复稳定。如果确认是fp16导致的可以考虑用bf16TrueAmpere架构及以上支持bf16的动态范围比fp16大稳定性更好。6.7 训练完无法加载checkpoint如果你保存的checkpoint目录里只有pytorch_model.bin、config.json和optimizer.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继续预训练流程之后我现在拿到一个新领域项目会先用这套判断框架快速决策通用预训练模型在这个领域语料上的PPL是否明显偏高比如大于20如果是继续预训练大概率有收益。领域语料是否有100MB以上的干净文本如果没有先去做数据积累别急着训练。下游任务是否对领域词汇敏感分类、NER、语义相似度、检索这些任务都高度依赖领域语义继续预训练收益明显纯情感打分类任务收益相对有限。算力是否允许跑至少3000步如果只够跑500步效果可能不明显不如直接微调。这套框架帮我避免了很多“为了训练而训练”的无效工作。希望你读完这篇文章后不仅掌握了MLM继续预训练的代码细节也能建立起一个更清醒的判断标准预训练是手段不是目的领域语料是燃料但也要用之有度。最后再分享一个小技巧训练完成后别急着把模型部署到生产环境。先用一个小型的下游任务评测集几百条标注样例就够对比原始模型和继续预训练模型在真实场景上的差异。这个“最小可行性验证”只要半天时间却能帮你避免把一个大但无效的模型推到线上。毕竟模型参数再多不如在真实数据上“懂行”来得重要。
返回列表