ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

领域语料MLM预训练实战:从词表构建到动态掩码的完整指南

领域语料MLM预训练实战:从词表构建到动态掩码的完整指南 先说一个很多刚接触NLP的同学容易踩的误区一说到预训练就想着拿别人的开源模型微调。但在真实项目里领域语料和通用语料差距大的时候比如医疗、法律、金融合同带大量专业缩写的场景直接用开源的RoBERTa或者BERT下游任务效果往往卡在瓶颈上。这时候就需要用自己的语料去做Mask Language Model预训练让模型先把领域知识“读”进去再做下游任务。这篇博文我系统性讲一遍整套流程从语料处理、词表构建、模型配置到真正跑起来的每一步以及我实际踩过的那些坑。这套方案适合三种人一是手里有一批垂直领域语料、但开源模型在业务上效果不理想的人二是想从零开始把MLM原理彻底搞懂的学生三是在研究预训练和后训练关系、想自己验证一些想法的工程师。内容会偏实操我会把能直接抄的代码、参数和命令放到对应位置直接复制改路径就能用。1. 整体设计思路与方案选型1.1 为什么要在自己的语料上做预训练很多人会问同一个问题“BERT都出来这么多年了预训练模型到处都是为什么要自己折腾直接微调不行吗”这个问题的答案取决于你的语料和通用语料之间存在多大的分布偏移。举个例子如果你要做的是电商评论情感分析通用BERT基本够用因为这类文本在预训练语料里大量存在。但如果你面对的是法律判决书、病历文本、汽车维修工单、服务器日志这些专业内容情况就完全不同了。首先词汇分布差异巨大。通用预训练词表里可能根本没有“股骨颈骨折”“执行异议之诉”“CAN总线故障码”这类领域强相关词一个词被硬切成多个subword模型要额外花很大代价才能建立语义联系。其次句式和表达习惯也不一样。病历文本大量使用省略主语、高度浓缩的短语而通用语料更像“正常写文章”。最后相同词汇的语义在领域内往往已经偏移了“转移”在通用语料里是“位置变化”在医学影像报告里是“癌细胞扩散”。你自己预训练过之后这些信息就直接烙在模型的权重里了。但我不建议所有人都去做全套预训练这是成本问题。纯从零训练一个大模型你需要的数据量是数十GB级的需要的时间按周算。更合理的方式是二选一领域语料量大10GB以上可以继续预训练一个开源基座语料量中等1GB到10GB在你的业务场景下用领域语料继续预训练同样能显著缓解上面说的三个问题而且成本低得多。1.2 三种主流预训练方案的取舍从零预训练、领域继续预训练、动态掩码增量训练这三条路线我都走过各自的定位是这样的从零预训练Train from scratch语料极其充足或者你想把网络结构换掉比如从BERT换成ELECTRA式的生成器判别器结构。这个过程烧算力最短也要两周起需要多卡并行否则时间根本扛不住。领域继续预训练Continue Pretraining加载开源基座权重用领域语料继续跑MLM任务。这是我个人最推荐的首选方案有开源模型的通用基础又有领域知识的沉淀成本和效果之间的平衡最好。增量预训练Dynamic Masking Continue在继续预训练的基础上每次epoch重新生成掩码减轻模型对固定掩码模式的过拟合。适合语料不算特别大、想尽可能压榨数据价值的情况。在模型结构上如果你不是做研究不建议自己写Transformer。Hugging Face的BertForMaskedLM已经封装好了加载配置就能用。你真正要费心的是三个地方语料质量、词表设计和训练过程的loss曲线。1.3 MLM任务训练机制的本质掩码语言模型的训练目标用一句话概括就是把输入文本中一部分token遮住让模型根据上下文预测被遮住的内容。这个过程强制模型学会利用双向上下文信息来理解每个词在特定语境下的含义。Transformer的注意力机制天然适合这个任务因为self-attention会让每个token同时“看到”左右两侧的信息。实际训练里BERT原始的掩码策略是15%的token参与预测其中80%直接用[MASK]替换10%用一个随机词替换10%保持不变。为什么要掺随机词和原词而不是一律用[MASK]原因很实际如果模型只在看到[MASK]时才做预测那下游任务里没有[MASK]标记的句子就会让模型不适应。[MASK]替换80%是希望模型学到语义恢复能力随机词替换是强迫模型不要过度依赖词本身而是要依赖上下文判断这个词是否合适原词保持则是让模型学会对正常句子也做hidden表示层面的精细建模。自己训练时有一个细节要注意RoBERTa后来验证了动态掩码每个epoch重新生成掩码效果更好。后续实现里我会直接采用动态掩码这也是当前实践的标准做法。2. 环境准备与语料处理2.1 工具选型与环境依赖预训练MLM基本绕不开Hugging Face生态核心依赖如下transformers负责加载模型、分词器、训练器tokenizers独立的分词器训练工具速度比transformers自带的分词器训练快很多datasets数据集的加载和预处理torch / tensorflowPyTorch在NLP实践中更常用建议torch版本不低于1.13accelerate / deepspeed多卡训练和混合精度支持建议的安装命令pip install transformers tokenizers datasets accelerate pip install torch --index-url https://download.pytorch.org/whl/cu118关于硬件我有一个比较现实的经验纯CPU训练MLM是极端低效的10万条中文数据在CPU上可能要跑一周。最好有一块24GB显存的GPU比如3090、4090、A5000至少也要16GB。如果显存不足把batch size调小配合梯度累积一样能跑只是会慢一些。2.2 语料采集与清洗规范语料质量决定了预训练效果的上限。这里的核心原则是宁缺毋滥。文本去重这一步特别容易被忽视。若语料里重复内容过多模型会对高频重复片段过拟合很多领域语料本身来源单一重复率相当高。可以用datasets自带的shuffle后近似查重也可以用SimHash做近似去重。我实操时发现一个简单有效的方法按整个样本做MD5再对正文按10-gram切分做Jaccard相似度过滤能把重复率从15%直接压到2%以下。清洗时要处理的噪音包括HTML标签、markdown符号、非正文内容如导航栏文本全半角混乱的符号超短文本少于20个字符的内容通常信息量太低大段连续重复字符如“哈哈哈哈哈哈”被截断成多个样本的情况语言混杂的句子除非你的任务本身就是多语言。清洗示例代码import re def clean_text(text): # 去除HTML标签 text re.sub(r[^], , text) # 统一全半角 text text.replace(\u3000, ).replace(\xa0, ) # 去除多余空白 text re.sub(r\s, , text).strip() # 过滤掉无意义符号行 if len(text) 20: return None return text2.3 语料格式与对齐处理语料建议整理成一行一条的纯文本文件每条可以是一个段落、一个对话轮次或一篇文档。不用带标签因为MLM是自监督任务文本本身就是标签。文件格式最好是UTF-8编码。如果一个样本太长比如超过512 token建议直接截断或者做滑窗切片。BERT类模型的最大位置编码通常是512虽然可以改但改长之后训练成本和收敛难度都会增加。最省事的方法是控制在512以内。数据规模上虽然理论上越大越好但对于继续预训练我实测下来5万到20万条高质量领域文本每条100到300字就能看到明显的效果提升不需要一上来就追求几千万条。关键在“领域浓度”这才是让模型学到领域知识的核心。语料准备好后目录结构参考data/ train.txt valid.txt test.txttrain用来训练valid用于训练中评估losstest留到最后做验证。哪怕语料不多也要划出验证集后面判断过拟合全靠它。3. 词表构建与分词器训练3.1 用现有词表还是训练新词表这一步是整个实践里最容易被低估的环节。很多人直接用开源模型的词表做继续预训练发现领域词总被切得稀碎。中文还好一点因为它本身是单字为基础但英文领域词如“metastasis”如果不在词表里就会被切成“meta”、“##sta”、“##sis”语义信息损失很大。判断要不要训练新词表有一个直接指标用你的领域语料在现有分词器上跑一遍统计平均每个词的subword碎片数。如果碎片数明显偏高或者大量关键词被切开就该考虑用领域语料训练新词表了。我个人的处理方式是这样的如果语料规模大10GB以上直接训练一个全新词表模型也从零训练如果语料规模中等1-10GB用领域语料训练一个小增量词表然后和原词表合并如果语料规模小1GB以下不训练新词表只做继续预训练。3.2 WordPiece与BPE的选择BERT原始用的是WordPieceGPT系列用的是BPE两者本质都是子词切分算法但在合并规则上有区别。WordPiece每次合并能最大化语言模型似然提升的pairBPE则是最频繁出现的pair优先合并。对中文来说这两种方案差别并没有想象中那么大因为中文天然以字为基本单位。对英文来说如果考虑词缀信息BPE表现略优。Hugging Face的BertTokenizer默认支持WordPiece如果想要BPE可以用RobertaTokenizer。用tokenizers库训练词表是最高效的方式from tokenizers import Tokenizer from tokenizers.models import WordPiece from tokenizers.trainers import WordPieceTrainer from tokenizers.pre_tokenizers import Whitespace tokenizer Tokenizer(WordPiece(unk_token[UNK])) tokenizer.pre_tokenizer Whitespace() trainer WordPieceTrainer( vocab_size30000, special_tokens[[PAD], [UNK], [CLS], [SEP], [MASK]], min_frequency2, ) files [data/train.txt] tokenizer.train(files, trainer) tokenizer.save(tokenizer.json)min_frequency设2可以去掉低频噪音tokenvocab_size这里取30000可以根据语料规模上下调整。特殊token列表放在最前面顺序不要乱改因为模型embedding初始化时要按id对齐。3.3 词表合并技巧如果要走“新词表继续预训练”路线会面临词表对齐问题。方法不复杂给新词表中的每个token在原词表中查id能查到的直接映射查不到的分配给新id。模型的embedding矩阵要扩大新增行可以用随机初始化也可以用原词表中类似词的embedding做初始化。实测下来用已有词嵌入的平均值初始化新增词向量比纯随机初始化收敛快很多。代码层面就是先加载原模型权重再把embedding矩阵resize后填入初始值。from transformers import BertConfig, BertForMaskedLM config BertConfig.from_pretrained(bert-base-chinese) model BertForMaskedLM.from_pretrained(bert-base-chinese, configconfig) # 调整embedding大小 model.resize_token_embeddings(len(new_tokenizer))resize之后新加入的embedding行是随机初始化的。更优的做法是先获取原模型的embedding权重把已有的部分拷贝到新矩阵对应位置新增行用均值初始化再放回模型。4. 预训练核心实现与代码解析4.1 数据加载与动态掩码实现数据加载用datasets库的load_dataset来做读取文本文件from datasets import load_dataset dataset load_dataset(text, data_files{train: data/train.txt, valid: data/valid.txt})接下来是掩码逻辑。PyTorch里最直接的方法是继承DataCollatorForLanguageModeling在其基础上实现动态掩码。transformers自带的DataCollatorForLanguageModeling本身就支持每次调用时重新生成掩码所以直接用它就是动态掩码效果。from transformers import DataCollatorForLanguageModeling data_collator DataCollatorForLanguageModeling( tokenizertokenizer, mlmTrue, mlm_probability0.15 )注意mlm_probability。BERT原论文是15%我试过10%和20%。语料充足时20%会让任务更难、收敛更慢语料少时15%仍是安全牌。如果你做的是类似法律这种长依赖强的文本可以试试20%但要做好loss不降的心理准备。4.2 配置与训练参数详解训练超参是预训练成败的关键之一。我最常用的配置如下from transformers import TrainingArguments training_args TrainingArguments( output_dir./checkpoints, overwrite_output_dirTrue, num_train_epochs5, per_device_train_batch_size16, per_device_eval_batch_size16, gradient_accumulation_steps4, learning_rate5e-5, weight_decay0.01, warmup_steps1000, logging_dir./logs, logging_steps100, evaluation_strategysteps, eval_steps1000, save_steps1000, load_best_model_at_endTrue, fp16True, )从零训练时学习率建议调到3e-4到1e-3之间因为所有参数都是随机初始化需要更大的更新步长。继续预训练时学习率要保守5e-5是个安全起点。我见过有人继续预训练时还按1e-4跑结果下游任务掉点的案例。warmup_steps建议占总训练步数的5%-10%。比如总步数20000步warmup设1000到2000步。warmup的意义是让模型在刚开始时不至于因为大步长导致loss震荡甚至发散。fp16能不能开取决于你的GPU是否支持半精度加速。实测下来V100及以上显卡开fp16能节省约40%的显存速度提升约30%。开之前确保你的语料里没有极端数值问题导致溢出如果loss突然变成nan可以先关掉fp16排查。4.3 完整训练代码框架组装好数据管道和参数后训练代码其实非常简洁。from transformers import Trainer trainer Trainer( modelmodel, argstraining_args, train_datasetdataset[train], eval_datasetdataset[valid], data_collatordata_collator, tokenizertokenizer, ) trainer.train()到这里一套可以跑的MLM预训练流程就已经成型了。但要注意这里用的是Hugging Face Trainer它能帮你处理断点续训、日志记录、模型保存等杂事省心很多。用裸PyTorch自己写训练循环也是可选的但需要自己处理的东西会翻好几倍。4.4 继续预训练模型时的两个隐藏设置如果你是在开源模型基础上继续预训练除了学习率调低还有两个容易忽略的地方。第一个是attention mask。如果你的语料是变长文本padding用[PAD]做填充一定要确保模型在attention时不看pad位置。这个DataCollatorForLanguageModeling会处理但如果你自己写collator别忘了加上attention_mask。第二个是tokenizer的padding和truncation设置。在线tokenize时建议这样def tokenize_function(examples): return tokenizer( examples[text], truncationTrue, max_length512, paddingmax_length, )padding策略选“max_length”会在每个batch内都padding到512这会浪费一些算力。更高效的方式是不padding让collator在组batch时动态pad到本batch的最大长度。Hugging Face的数据集map可以做延迟padding运行时只有当前batch内的pad效率差很大。4.5 多卡分布式训练与断点续训单卡训练慢多卡是常态。用accelerate launch是最简单的多卡启动方式accelerate launch --num_processes4 run_mlm.py配合Hugging Face Trainer只需要准备好这样一个脚本accelerate会自动处理梯度同步和模型分发。不开accelerate也可以直接用Python的torch.distributed但代码量会增加不少。断点续训同样由Trainer支持训练中断后在指定output_dir里能找到checkpoint-xxxx重新执行trainer.train(resume_from_checkpointTrue)即可。这几乎是训练超过48小时后的保命功能跑过长时间训练的人都懂。5. 常见问题与排查技巧实录5.1 训练loss不下降的排查顺序loss不降是预训练最常遇到的问题。我的排查顺序是数据问题样本是否大量为空或重复清洗是否充分特殊字符是否导致tokenize后几乎都是[UNK]学习率问题太大会在初期就发散太小则收敛速度极慢。从零训练保持1e-4到1e-3继续预训练保持5e-5到1e-4。掩码比例问题mlm_probability设为0.15但实际掩码位置太少试试调高到0.2看loss变化。模型结构问题只改embedding没resize对应的lm_head维度导致输出层和embedding维数不匹配模型虽然能跑但loss不降。一个非常隐蔽的坑是resize_token_embeddings之后语言模型输出头的权重不会自动resize必须同时调整lm_head。Hugging Face的BertForMaskedLM内部通过tie_weights使embedding和lm_head共享权重resize时一般会处理但如果你自定义了模型结构一定要自己处理。5.2 验证集loss不降但训练loss在降这种情况基本可以断定为过拟合。尤其是语料只有几万条、训练多轮之后模型已经开始记忆训练语料了。对策有三个增加训练数据或做数据增强对文本做回译、同义词替换虽然会引入噪声但能缓解过拟合减少训练轮数继续预训练通常2到3个epoch就够不是越多越好加dropout正则hidden_dropout_prob和attention_probs_dropout_prob适当调高比如从0.1调到0.15。我自己做过一个对比实验10万条医疗语料第2个epoch时验证loss已经在上升但训练loss还在下降。把训练轮数从5减到3后下游命名实体识别任务的F1从82.3提到85.1。训练轮数真不是越多越好。5.3 显存不足时的内存优化策略24GB显存跑BERT-basebatch_size16max_length512batch_size只能勉强放下。显存不够时的调整顺序先把per_device_train_batch_size降到8或4调高gradient_accumulation_steps做补偿比如batch_size4、gradient_accumulation_steps8等效batch size为32开fp16混合精度用gradient_checkpointingTrue这是最有效但也最影响训练速度的方案显存占用可以减半但时间会多出30%到50%。training_args TrainingArguments( per_device_train_batch_size4, gradient_accumulation_steps8, gradient_checkpointingTrue, fp16True, )对了如果你的数据长度均值只有100到200个字可以把max_length直接设成256省下的显存非常可观速度也能提升不少。5.4 MLM预训练效果验证的常用方法预训练跑完别急着直接上线下游任务。先做两个快速验证第一直接用掩码预测看看效果。用你自己领域的句子把某个关键词替换成[MASK]让模型预测。比如病历文本把“患者诊断为[MASK]”里的[MASK]换成模型预测结果看一下是不是领域内疾病名。第二做同义词召回。选几个高频领域词取模型倒数第二层输出的token embedding计算其他词的余弦相似度看召回的词是否语义相关。这一步能直观检验领域预训练是否真的把知识注入进去了。第三个更严谨的方法是下游任务对比同一个模型结构一组走“通用预训练直接用”一组走“领域继续预训练后微调”控制相同的数据和训练参数对比最终F1/准确率。我自己的经验是领域数据质量越高、与通用语料差异越大提升越明显。from transformers import pipeline fill_mask pipeline(fill-mask, model./checkpoints/checkpoint-5000, tokenizertokenizer) result fill_mask(患者被诊断为[MASK]) print(result)看到预测结果里出现“肺炎”“心肌梗死”“脑梗死”这类领域词汇就说明预训练学到东西了。要看到“苹果”“香蕉”这种通用答案那就说明你的领域语料浓度还不够。5.5 多语言与中英混合语料的使用中英混合语料在训练时有一个典型问题中文以字为粒度英文以subword为粒度两者的频率分布差异很大。如果混在一起直接训练词表中文部分往往被切得非常碎英文部分对词表的占用又过高。处理方式是分开构建词表中文部分用字粒度英文部分用BPE子词粒度再把两类词拼在一起加上必要的特殊token。这样得到的词表大小虽会膨胀但切分质量和下游效果都好于硬混。另外语料里如果包含大量URL、邮箱地址、时间戳这类模板化内容建议先做标准化替换。否则模型会花不少容量去记忆“http://”这类token的共现模式属于浪费。6. 实操心得与扩展方向做MLM预训练这件事技术难度没有想象中高真正的壁垒在于对领域语料的理解和对训练细节的控制。数据清洗花的时间往往比训练本身还多但这一步省不得。我自己的经验是先把1000条数据清洗到满意状态再批量跑全量清洗脚本不然全量跑完发现规则不对返工成本极高。在一个更实际的层面上继续预训练的收益与领域语料的独特性正相关。通用新闻语料继续预训练提升空间有限病历、裁判文书、工单日志这些高度垂直的文本提升非常明显。如果你的领域语料只有几万条不要放弃继续预训练加小学习率依然能在下游任务上看得到涨点只是幅度小一点。另外两个扩展方向也值得一提。一是把MLM预训练作为多任务学习的一部分联合句子序预测、对比学习这些辅助目标一起训练这在数据量有限时尤其好用。二是只对领域特定词表做bias训练冻结大部分参数只训练embedding层和最后的MLM head这种轻量方案在算力受限时很实用。关于模型保存建议每个checkpoint都保留分词器文件否则加载模型时会遇到tokenizer不匹配的问题。看起来是小事真到加载模型推理时match不上debug起来也很恼人。这个项目做完之后我对预训练和后训练阶段的关系有了重新认识。预训练负责让模型理解语言结构后训练负责让模型对齐到特定任务偏好两者侧重点不同但对业务效果都至关重要。如果你后续有精力可以沿着这个方向继续往下挖训练完的MLM模型去做文本分类、命名实体识别、检索排序一条链路走通了整个NLP项目管理能力会提升一个档次。
返回列表