Transformers 因果语言建模(CLM)实战:用 Trainer 在 ELI5 上微调 DistilGPT2 并生成文本
因果语言建模(Causal Language Modeling,CLM)是文本生成类模型的核心训练范式。本篇指南基于 Transformers 仓库官方任务文档 Causal language modeling,完整演示:如何用 🤗 Datasets 加载 ELI5 数据集、对嵌套文本做 tokenize 与 block_size 分块、用 DataCollatorForLanguageModeling 动态组批、通过 Trainer 微调 DistilGPT2 并计算困惑度(Perplexity),最后用 pipeline 和 model.generate() 两种方式做推理生成——读完即可掌握从数据处理到模型上线 Hub 的完整 CLM 工作流。
一、什么是因果语言建模
语言建模分为两类:因果(causal) 与 掩码(masked)。本指南讲解前者。因果语言模型通常用于文本生成,可支撑创意类应用(如文字冒险游戏)和智能编码助手等场景。
从机制上看,因果语言建模预测 token 序列中的下一个 token,且模型只能"看到"左侧的 token,无法感知未来的 token。GPT-2 就是典型的因果语言模型。与之相对的掩码语言建模(BERT 式)可在 masked_language_modeling.md 中查阅。
本指南的目标:
- 在 ELI5 数据集的 r/askscience 子集上微调 DistilGPT2;
- 使用训练好的模型进行推理生成。
环境准备
确保已安装必要的库:
pip install transformers datasets evaluate
建议登录 Hugging Face 账号,以便上传和分享模型:
>>> from huggingface_hub import notebook_login
>>> notebook_login()
二、加载 ELI5 数据集
先用 🤗 Datasets 加载 ELI5-Category 数据集的前 5000 条样本。这样做的好处是可以在花更长时间训练完整数据集之前,先验证整条流水线是否跑通:
>>> from datasets import load_dataset
>>> eli5 = load_dataset("eli5_category", split="train[:5000]")
用 train_test_split 把训练集拆分为训练/测试两部分:
>>> eli5 = eli5.train_test_split(test_size=0.2)
查看一条样本:
>>> eli5["train"][0]
{'q_id': '7h191n',
'title': 'What does the tax bill that was passed today mean? How will it affect Americans in each tax bracket?',
'selftext': '',
'category': 'Economics',
'subreddit': 'explainlikeimfive',
'answers': {'a_id': ['dqnds8l', 'dqnd1jl', 'dqng3i1', 'dqnku5x'],
'text': ["The tax bill is 500 pages long and there were a lot of changes still going on right to the end. ...",
'None yet. It has to be reconciled with a vastly different house bill and then passed again.',
'Also: does this apply to 2017 taxes? Or does it start with 2018 taxes?',
'This article explains both the House and senate bills, ... URL_0'],
'score': [21, 19, 5, 3],
'text_urls': [[], [], [], ['https://.../trumps-tax-reform-what-can-be-done/']],
'title_urls': ['url'],
'selftext_urls': ['url']}
虽然字段较多,但真正关心的是 text 字段。语言建模任务的一大优势是不需要人工标注(属于非监督任务),因为"下一个词"本身就充当了标签。
三、数据预处理
3.1 加载分词器并展平嵌套结构
加载 DistilGPT2 的分词器用于处理 text 字段:
>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("distilbert/distilgpt2")
从上例可以看到,text 字段实际嵌套在 answers 内部。因此需要用 🤗 Datasets 的 flatten 方法把嵌套结构展平为独立列:
>>> eli5 = eli5.flatten()
>>> eli5["train"][0]
{'q_id': '7h191n',
'title': 'What does the tax bill that was passed today mean? ...',
'selftext': '',
'category': 'Economics',
'subreddit': 'explainlikeimfive',
'answers.a_id': ['dqnds8l', 'dqnd1jl', 'dqng3i1', 'dqnku5x'],
'answers.text': ["The tax bill is 500 pages long ...",
'None yet. It has to be reconciled ...',
'Also: does this apply to 2017 taxes? ...',
'This article explains both the House and senate bills, ... URL_0'],
'answers.score': [21, 19, 5, 3],
'answers.text_urls': [[], [], [], ['https://.../trumps-tax-reform-what-can-be-done/']],
'title_urls': ['url'],
'selftext_urls': ['url']}
展平后,每个子字段都成了以 answers. 为前缀的独立列,而 answers.text 是一个列表(每条样本对应多个答案)。与其逐句分别分词,不如先把列表拼接成一个字符串再整体分词。
3.2 第一个预处理函数:拼接 + 分词
>>> def preprocess_function(examples):
... return tokenizer([" ".join(x) for x in examples["answers.text"]])
用 🤗 Datasets 的 map 方法把该函数应用到整个数据集。两个提速要点:batched=True 让 map 一次处理多条样本,num_proc 增加并行进程数。同时用 remove_columns 删掉不需要的列:
>>> tokenized_eli5 = eli5.map(
... preprocess_function,
... batched=True,
... num_proc=4,
... remove_columns=eli5["train"].column_names,
... )
此时数据集里是 token 序列,但其中一些序列比模型的最大输入长度更长。
3.3 第二个预处理函数:拼接 + 按 block_size 分块
第二个预处理函数做两件事:
- 把所有序列串联起来;
- 把串联后的长序列切分为固定长度
block_size的短块。block_size应小于模型最大输入长度,且短到 GPU 显存能装得下。
>>> block_size = 128
>>> def group_texts(examples):
... # 串联所有文本。
... concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()}
... total_length = len(concatenated_examples[list(examples.keys())[0]])
... # 丢弃末尾的小零头;如果模型支持 padding,也可以不丢弃。
... if total_length >= block_size:
... total_length = (total_length // block_size) * 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()
... }
... result["labels"] = result["input_ids"].copy()
... return result
应用到整个数据集:
>>> lm_dataset = tokenized_eli5.map(group_texts, batched=True, num_proc=4)
关键细节:labels = input_ids.copy()。 因果语言建模的标签就是输入本身(模型在第 i 个位置预测第 i+1 个 token),损失计算时由模型的移位逻辑对齐。仓库中的训练脚本 run_clm.py 采用完全相同的 group_texts 策略;同时可以看到官方脚本对 block_size 的默认取值逻辑:未指定时取 tokenizer.model_max_length(并受模型最大位置编码限制,兜底 1024),若手动传入的值超过 model_max_length 则自动下调并发出警告(run_clm.py)。
3.4 动态填充:DataCollatorForLanguageModeling
组批时应使用动态填充——在组批时刻把每个句子填充到该 batch 内最长序列的长度,而不是把整个数据集都填充到最大长度(否则浪费大量计算)。
用序列结束符(EOS)作为填充 token,并关闭掩码模式:
>>> from transformers import DataCollatorForLanguageModeling
>>> tokenizer.pad_token = tokenizer.eos_token
>>> data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
从源码看(data_collator.py),DataCollatorForLanguageModeling 的核心参数为:
| 参数 | 默认值 | 说明 |
|---|---|---|
mlm |
True |
是否为掩码语言建模;CLM 场景必须显式设为 False |
mlm_probability |
0.15 |
mlm=True 时随机掩码 token 的概率 |
mask_replace_prob |
0.8 |
被掩码 token 替换为 [MASK] 的概率 |
random_replace_prob |
0.1 |
被掩码 token 替换为随机词的概率 |
whole_word_mask |
False |
是否整词掩码(仅 mlm=True 时可用) |
pad_to_multiple_of |
None |
若设置,序列将被填充到该值的倍数 |
seed |
None |
掩码随机数生成器的种子 |
当 mlm=False 时,torch_call 的走法很简洁:先对 batch 做动态 padding,然后直接 labels = input_ids.clone(),并把 pad 位置(pad_token_id)置为 -100,使损失计算忽略填充位——这正是 CLM 标签构造的底层实现。若误设 mlm=True 而分词器没有 mask token,__post_init__ 会抛出异常并提示"请传 mlm=False 进行因果语言建模"(data_collator.py)。
四、训练
如果不熟悉用 [
Trainer] 训练模型,可先阅读仓库中的 trainer.md 与 training.md 教程。
用 [AutoModelForCausalLM] 加载 DistilGPT2:
>>> from transformers import AutoModelForCausalLM, TrainingArguments, Trainer
>>> model = AutoModelForCausalLM.from_pretrained("distilbert/distilgpt2")
接下来只剩三步:
- 在 [
TrainingArguments] 中定义训练超参数。唯一必填项是output_dir(模型保存位置);设置push_to_hub=True即可在训练完成后把模型推到 Hub(需已登录); - 把训练参数连同模型、数据集、数据组批器一起传给 [
Trainer]; - 调用 [
~Trainer.train] 开始微调。
>>> training_args = TrainingArguments(
... output_dir="my_awesome_eli5_clm-model",
... eval_strategy="epoch",
... learning_rate=2e-5,
... weight_decay=0.01,
... push_to_hub=True,
... )
>>> trainer = Trainer(
... model=model,
... args=training_args,
... train_dataset=lm_dataset["train"],
... eval_dataset=lm_dataset["test"],
... data_collator=data_collator,
... tokenizer=tokenizer,
... )
>>> trainer.train()
4.1 评估:计算困惑度
训练完成后,用 [~Trainer.evaluate] 评估模型并得到困惑度(perplexity):
>>> import math
>>> eval_results = trainer.evaluate()
>>> print(f"Perplexity: {math.exp(eval_results['eval_loss']):.2f}")
Perplexity: 49.61
注意 math.exp(eval_loss) 这一步:CLM 的损失是逐 token 的交叉熵均值,取指数后即为困惑度,数值越低表示模型对文本的"惊讶程度"越小。
4.2 发布模型
用 [~Trainer.push_to_hub] 把模型分享到 Hub,供他人使用:
>>> trainer.push_to_hub()
如果需要更完整的命令行式训练(支持 --block_size、--line_by_line、--overwrite_output_dir 等大量参数),可以直接运行仓库中的官方脚本:
python examples/pytorch/language-modeling/run_clm.py \
--model_name_or_path distilbert/distilgpt2 \
--dataset_name eli5 \
--block_size 128 \
--output_dir my_awesome_eli5_clm-model
脚本说明见 README,无需 Trainer 的纯手动训练版本可参考 run_clm_no_trainer.py。
五、推理
模型训练好了,下面用它生成文本。
先准备一个提示词:
>>> prompt = "Somatic hypermutation allows the immune system to"
5.1 方式一:pipeline(最简)
最简单的体验方式是把它装进 [pipeline]。实例化一个 text-generation 流水线,传入你的模型和提示文本:
>>> from transformers import pipeline
>>> generator = pipeline("text-generation", model="username/my_awesome_eli5_clm-model")
>>> generator(prompt)
[{'generated_text': "Somatic hypermutation allows the immune system to be able to effectively reverse the damage caused by an infection.\n\n\nThe damage caused by an infection is caused by the immune system's ability to perform its own self-correcting tasks."}]
5.2 方式二:model.generate(可控)
先分词并得到 PyTorch 张量形式的 input_ids:
>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("username/my_awesome_eli5_clm-model")
>>> inputs = tokenizer(prompt, return_tensors="pt").input_ids
调用 [~GenerationMixin.generate] 生成文本。更多生成策略(greedy、beam search、各类采样)与参数可调范围,可参考文档 generation_strategies 中的相关章节:
>>> from transformers import AutoModelForCausalLM
>>> model = AutoModelForCausalLM.from_pretrained("username/my_awesome_eli5_clm-model")
>>> outputs = model.generate(inputs, max_new_tokens=100, do_sample=True, top_k=50, top_p=0.95)
这里演示的是核采样组合:do_sample=True 开启采样,top_k=50 只在概率最高的 50 个候选中采样,top_p=0.95 则只从累积概率达到 0.95 的最小候选集内采样,max_new_tokens=100 控制生成长度。
最后把 token 解码回文本:
>>> tokenizer.batch_decode(outputs, skip_special_tokens=True)
["Somatic hypermutation allows the immune system to react to drugs with the ability to adapt to a different environmental situation. In other words, a system of 'hypermutation' can help the immune system to adapt to a different environmental situation or in some cases even a single life. In contrast, researchers at the University of Massachusetts-Boston have found that 'hypermutation' is much stronger in mice than in humans but can be found in humans, and that it's not completely unknown to the immune system. A study on how the immune system"]
六、要点回顾
| 环节 | 关键实现 | 仓库依据 |
|---|---|---|
| 数据分块 | group_texts:串联全部 token 后按 block_size 切块,labels = input_ids.copy() |
本文 3.3 节、run_clm.py |
| 动态组批 | DataCollatorForLanguageModeling(mlm=False),pad 位置标签置 -100 |
data_collator.py |
| 训练 | Trainer + TrainingArguments,output_dir 必填,push_to_hub=True 一键发布 |
training.md |
| 评估 | Perplexity = exp(eval_loss) |
本文 4.1 节 |
| 生成 | pipeline("text-generation") 或 model.generate(do_sample=True, top_k=50, top_p=0.95) |
本文第五节 |
整条流水线的核心思想可以概括为:因果语言建模不需要标签(下一个 token 即标签)、用串联分块最大化利用长文本、用动态填充控制组批开销。掌握这三点后,把 ELI5 换成任意纯文本数据集、把 DistilGPT2 换成任意因果 LM 架构,流程完全一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00