实战指南:用 DistilRoBERTa 微调与推理)
Transformers 掩码语言建模Masked Language Modeling实战指南用 DistilRoBERTa 微调与推理【免费下载链接】transformers Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers掩码语言建模MLM是预训练与微调双向 Transformer 语言模型的核心任务模型需要根据序列中未被遮蔽的上下文预测被mask掩码替换的 token。由于模型在训练中能同时看到掩码位置的左右两侧内容这类模型特别适合需要完整理解整个序列语境的下游任务BERT 就是最经典的掩码语言模型代表。本文将基于本仓库Transformers提供的工具链从加载数据集、预处理、构建数据整理器、微调 DistilRoBERTa到 pipeline 与底层手动推理完整走通填词任务的实战全流程。任务背景什么是掩码语言建模掩码语言建模的目标是预测序列中被掩码的 token。与因果语言建模Causal Language Modeling只能看到左侧 token不同MLM 允许模型双向处理输入模型对每个位置的 token 都拥有左右两侧上下文的完全访问权。这种双向特性让 MLM 预训练出的模型如 BERT、RoBERTa、DistilRoBERTa非常擅长需要全局语境理解的任务例如问答、命名实体识别、情感分类与句子关系判断。在 Transformers 库中MLM 属于 fill-mask 任务族可以通过pipeline(fill-mask)直接调用。仓库的自动映射表 modeling_auto.py 列出了所有支持掩码语言建模的架构包括bert、roberta、distilbert、deberta、electra、albert、camembert、longformer、modernbert、xlm-roberta等 40 余种AutoModelForMaskedLM会根据 checkpoint 的architectures字段自动实例化对应的*ForMaskedLM模型类。以本文使用的 DistilRoBERTa 为例其对应类为RobertaForMaskedLMdistilroberta-base复用 RoBERTa 的模型结构。在动手之前请先确认环境依赖已安装齐全pip install transformers datasets evaluate如果计划将模型上传到 Hugging Face Hub 与社区共享建议先登录账户按提示输入 token 即可 from huggingface_hub import notebook_login notebook_login()加载 ELI5 数据集先使用 Datasets 库加载 ELI5 数据集中r/askscience子集的较小切片这样可以在投入整份数据集的训练时间之前先快速实验验证整套流程是否跑通 from datasets import load_dataset eli5 load_dataset(eli5, splittrain_asks[:5000])train_asks[:5000]表示只取前 5000 条问答数据。随后用 [~datasets.Dataset.train_test_split] 方法将train_asks划分为训练集与测试集 eli5 eli5.train_test_split(test_size0.2)先看一条样本了解数据结构 eli5[train][0] {answers: {a_id: [c3d1aib, c3d4lya], score: [6, 3], text: [The velocity needed to remain in orbit is equal to the square root of Newtons constant times the mass of earth divided by the distance from the center of the earth. ..., Hope you dont mind me asking another question, but why arent there any stars visible in this photo?]}, answers_urls: {url: []}, document: , q_id: nyxfp, selftext: _URL_0_\n\nThis was on the front page earlier and I have a few questions about it. ..., selftext_urls: {url: [http://apod.nasa.gov/apod/image/1201/freeflyer_nasa_3000.jpg]}, subreddit: askscience, title: Few questions about this space walk photograph., title_urls: {url: []}}虽然样本字段较多但语言建模任务真正关心的是text字段。语言建模的一大好处是不需要标注即所谓的无监督任务因为下一个 token或此处被掩码的 token本身就是标签。预处理tokenize 与文本分块加载 tokenizer 并展平嵌套字段掩码语言建模的预处理第一步是加载与模型配套的 DistilRoBERTa tokenizer from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(distilbert/distilroberta-base)从上面的样本可以看到text字段嵌套在answers内部。需要用 Datasets 的flatten方法把嵌套结构中的text子字段提取出来使其成为独立的列 eli5 eli5.flatten() eli5[train][0] {answers.a_id: [c3d1aib, c3d4lya], answers.score: [6, 3], answers.text: [The velocity needed to remain in orbit ..., Hope you dont mind me asking another question, ...], answers_urls.url: [], document: , q_id: nyxfp, selftext: _URL_0_\n\nThis was on the front page earlier and I have a few questions about it. ..., selftext_urls.url: [http://apod.nasa.gov/apod/image/1201/freeflyer_nasa_3000.jpg], subreddit: askscience, title: Few questions about this space walk photograph., title_urls.url: []}展平后每个子字段以answers前缀命名成为独立列text字段变成了列表。与其逐句单独 tokenize不如先把每条样本中的字符串列表拼接成一个字符串再统一 tokenize def preprocess_function(examples): ... return tokenizer([ .join(x) for x in examples[answers.text]])将预处理函数应用到整个数据集时使用 Datasets 的 [~datasets.Dataset.map] 方法。为加速map设置batchedTrue一次处理多个元素并用num_proc增加进程数同时用remove_columns移除不再需要的原始列 tokenized_eli5 eli5.map( ... preprocess_function, ... batchedTrue, ... num_proc4, ... remove_columnseli5[train].column_names, ... )拼接并切分为固定长度块此时数据集包含的是 token 序列其中一部分可能超过模型的最大输入长度。第二个预处理函数group_texts负责两件事将所有序列首尾拼接concatenate把拼接后的长序列按block_size切成短块。block_size应小于模型最大输入长度同时也要足够短以适配 GPU 显存。 block_size 128 def group_texts(examples): ... # Concatenate all texts. ... concatenated_examples {k: sum(examples[k], []) for k in examples.keys()} ... total_length len(concatenated_examples[list(examples.keys())[0]]) ... # We drop the small remainder, we could add padding if the model supported it instead of this drop, you can ... # customize this part to your needs. ... if total_length block_size: ... total_length (total_length // block_size) * block_size ... # Split by chunks of 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将group_texts应用到整个数据集 lm_dataset tokenized_eli5.map(group_texts, batchedTrue, num_proc4)这种拼接后切块的预处理方式不仅解决了序列长度问题还能让跨样本的上下文保持连续性是语言模型训练的标准做法。用 DataCollatorForLanguageModeling 动态掩码接下来使用 [DataCollatorForLanguageModeling] 构建训练批次。与把整个数据集 pad 到最大长度不同数据整理器在组 batch 时动态填充到当前批次内的最长长度更加高效。在使用 MLM 模式时需要指定mlm_probability即每次迭代数据时随机掩码 token 的概率。DistilRoBERTa 没有专门的 pad token因此这里将序列结束 tokeneos用作 pad token from transformers import DataCollatorForLanguageModeling tokenizer.pad_token tokenizer.eos_token data_collator DataCollatorForLanguageModeling(tokenizertokenizer, mlm_probability0.15)源码视角掩码究竟如何发生从源码看DataCollatorForLanguageModeling位于 data_collator.py它继承DataCollatorMixin其核心逻辑在torch_call与torch_mask_tokens中numpy_call/numpy_mask_tokens提供 NumPy 后端动态填充torch_call调用pad_without_fast_tokenizer_warning将 batch 内的样本填充到一致长度并支持pad_to_multiple_of参数构造概率矩阵torch_mask_tokens用mlm_probability填充与输入同形状的概率矩阵然后通过torch.bernoulli采样出masked_indices特殊 token通过special_tokens_mask标记被强制置为不掩码概率填 0避免把[CLS]、[SEP]等特殊 token 掩掉构造标签未被掩码的位置标签设为-100这样在计算交叉熵损失时这些位置会被自动忽略只有被掩码的 token 参与损失计算按概率替换输入对被选中的掩码位置80%mask_replace_prob0.8替换为mask10%random_replace_prob0.1替换为词表中的随机 token剩余 10% 保持原样。源码中会将random_replace_prob按剩余概率比例缩放后再次伯努利采样这与 BERT 原始的掩码策略完全一致。该整理器的关键参数与默认值如下参数默认值说明mlmTrue是否使用掩码语言建模设为False则退化为因果语言建模标签与输入相同pad 位置置-100mlm_probability0.15随机掩码 token 的概率需在 01 之间whole_word_maskFalse是否按整词掩码依赖 fast tokenizer 的 offset mapping开启后不支持随机 token 替换mask_replace_prob0.8被掩码 token 中替换为mask的比例random_replace_prob0.1被掩码 token 中替换为随机 token 的比例与mask_replace_prob之和不能超过 1pad_to_multiple_ofNone若设置将序列填充到该值的整数倍return_tensorspt返回张量类型可取np或ptseedNone掩码随机数种子配合 PyTorch DataLoader 多进程使用时可为每个 worker 派生独立种子需要注意的是__post_init__会校验 tokenizer 必须含有mask_token否则会抛出异常并提示改用mlmFalse训练因果语言模型。DistilRoBERTa 的 mask token 是mask满足条件。微调 DistilRoBERTa如果不熟悉用 [Trainer] 微调模型可先阅读基础教程 Trainer 训练入门对应英文版 training.md。使用 [AutoModelForMaskedLM] 加载 DistilRoBERTa from transformers import AutoModelForMaskedLM model AutoModelForMaskedLM.from_pretrained(distilbert/distilroberta-base)AutoModelForMaskedLM是一个懒加载的自动映射类定义见 modeling_auto.py它会读取 checkpoint 的 config 中声明的架构从MODEL_FOR_MASKED_LM_MAPPING_NAMES映射表选出对应的*ForMaskedLM类实例化模型。它会在预训练权重之上自动添加/匹配语言模型头LM head因此加载后即可直接用于掩码预测与继续微调。剩余步骤只有三步用 [TrainingArguments] 定义训练超参数。唯一必填参数是output_dir用于指定模型保存位置设置push_to_hubTrue可将模型推送到 Hub需要已登录 Hugging Face将训练参数、模型、数据集与数据整理器一起传给 [Trainer]调用 [~Trainer.train] 开始微调。 training_args TrainingArguments( ... output_dirmy_awesome_eli5_mlm_model, ... eval_strategyepoch, ... learning_rate2e-5, ... num_train_epochs3, ... weight_decay0.01, ... push_to_hubTrue, ... ) trainer Trainer( ... modelmodel, ... argstraining_args, ... train_datasetlm_dataset[train], ... eval_datasetlm_dataset[test], ... data_collatordata_collator, ... ) trainer.train()训练参数说明output_dir模型与检查点的输出目录必填eval_strategyepoch每个 epoch 结束时执行一次评估这样训练过程中就能持续看到验证损失的变化learning_rate2e-5微调预训练模型惯用的较小学习率避免破坏已学到的表征num_train_epochs3训练轮数可按数据量与显存调整weight_decay0.01L2 权重衰减帮助抑制过拟合push_to_hubTrue训练结束后自动把模型上传到 Hub需要登录。训练完成后用 [~transformers.Trainer.evaluate] 评估模型并计算困惑度PerplexityPPL。困惑度是语言模型的经典指标等于交叉熵损失的指数值越小说明模型对文本的预测能力越强 import math eval_results trainer.evaluate() print(fPerplexity: {math.exp(eval_results[eval_loss]):.2f}) Perplexity: 8.76最后用 [~transformers.Trainer.push_to_hub] 把模型分享到 Hub方便社区直接使用 trainer.push_to_hub()更完整的掩码语言建模微调示例可参考 PyTorch 语言建模 notebook 与 TensorFlow 语言建模 notebook 中的完整实现。推理让模型填空微调完成后模型就可以投入推理。构造一段带空白的文本用特殊 tokenmask标记要预测的位置 text The Milky Way is a mask galaxy.方式一使用 fill-mask pipeline推理最便捷的方式是通过 [pipeline] 使用微调后的模型。实例化fill-maskpipeline 并把文本传入可以用top_k参数指定返回的候选数量 from transformers import pipeline mask_filler pipeline(fill-mask, stevhliu/my_awesome_eli5_mlm_model) mask_filler(text, top_k3) [{score: 0.5150994658470154, token: 21300, token_str: spiral, sequence: The Milky Way is a spiral galaxy.}, {score: 0.07087188959121704, token: 2232, token_str: massive, sequence: The Milky Way is a massive galaxy.}, {score: 0.06434620916843414, token: 650, token_str: small, sequence: The Milky Way is a small galaxy.}]输出中每个候选包含四个字段score为归一化后的预测概率token为预测 token 的 IDtoken_str为 token 的字符串形式sequence为将预测 token 回填到原句后的完整文本。pipeline 底层做了什么FillMaskPipeline的实现位于 fill_mask.py调用链清晰可查preprocess对输入文本调用 tokenizer并调用ensure_exactly_one_mask_token校验输入中必须且只能包含一个masktoken否则抛出PipelineException_forward将input_ids送入模型取回logits并把input_ids一并放入输出以便后处理postprocess定位mask在input_ids中的位置取出对应行的logits经过softmax得到概率分布再用topk(top_k)取概率最高的若干候选回填 token 后通过decode生成完整序列并过滤掉 pad token。该 pipeline 还支持targets参数限定候选词表与tokenizer_kwargs透传给 tokenizer例如{truncation: True}可参考源码中的 docstring 示例。注意 pipeline 对多个mask的支持目前属于实验特性返回的是各位置独立disjoint的概率而非联合概率。方式二手动 tokenize 前向传播如果想对推理过程有完全的控制可以手动完成。先将文本 tokenize 并返回 PyTorch 张量同时定位masktoken 的位置 from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(stevhliu/my_awesome_eli5_mlm_model) inputs tokenizer(text, return_tensorspt) mask_token_index torch.where(inputs[input_ids] tokenizer.mask_token_id)[1]将输入送入模型取出掩码位置的logits from transformers import AutoModelForMaskedLM model AutoModelForMaskedLM.from_pretrained(stevhliu/my_awesome_eli5_mlm_model) logits model(**inputs).logits mask_token_logits logits[0, mask_token_index, :]logits的形状为(batch_size, seq_len, vocab_size)mask_token_logits则只保留掩码位置在词表维度上的分数。用torch.topk取出概率最高的 3 个 token逐个回填到原文中并打印 top_3_tokens torch.topk(mask_token_logits, 3, dim1).indices[0].tolist() for token in top_3_tokens: ... print(text.replace(tokenizer.mask_token, tokenizer.decode([token]))) The Milky Way is a spiral galaxy. The Milky Way is a massive galaxy. The Milky Way is a small galaxy.这与 pipeline 的结果完全一致——pipeline 本质上就是对上述步骤的封装。验证与扩展本仓库的测试套件为上述流程提供了验证依据。在 test_modeling_distilbert.py 中DistilBertModelTest继承ModelTesterMixin与PipelineTesterMixin并将fill-mask: DistilBertForMaskedLM注册进 pipeline 支持的任务映射同时DistilBertModelTester会直接实例化DistilBertForMaskedLM(configconfig)L136来验证前向与损失计算。这意味着任何注册进MODEL_FOR_MASKED_LM_MAPPING_NAMES的模型都可以用本文的两套推理流程直接替换distilroberta-base使用。如果想验证数据整理器的工作方式可以用trainer.get_train_dataloader()或直接调用data_collator对一批 tokenize 后的样本进行查看你会看到input_ids中部分 token 被替换为maskID 50264而labels中只有这些位置保留原 token ID、其余位置为-100。小结本文完整演示了基于 Transformers 的掩码语言建模实战路径ELI5 数据集加载与flatten展平、map批量预处理与group_texts拼接切块、DataCollatorForLanguageModeling动态填充与 BERT 式掩码策略80%mask/ 10% 随机 / 10% 保留、Trainer微调与困惑度评估以及 pipeline 与手动前向两种推理方式。这套方法论不仅适用于 DistilRoBERTa也适用于 modeling_auto.py 中列出的所有双向架构。掌握了掩码语言建模也就掌握了绝大多数双向预训练模型继续训练与领域适配的通用钥匙。【免费下载链接】transformers Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考