news 2026/10/20 7:40:04

Tokenizer Padding全解析:训练与推理阶段的正确用法与踩坑经验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Tokenizer Padding全解析:训练与推理阶段的正确用法与踩坑经验

我最初接触 Tokenizer Padding 时,以为它只是“给短句子末尾补一堆没意义的占位符”,直到有一次训练 loss 曲线莫名抖动、单独预测和批量预测结果对不上,才意识到这个环节藏着不少约定。这篇笔记,就是我在某短文本分类项目、某序列标注项目和后续生成任务里,反复踩坑之后整理出来的训练与推理阶段 padding 用法总结。

内容围绕三件事展开:padding 到底在解决什么问题、训练和推理两个阶段各自的正确用法、以及一旦没处理好会出现哪些典型事故和对应的排查思路。最后附上一套可以直接改动使用的代码模板。适合刚接触预训练模型的同学,也适合已经跑了几个项目但总在 batch 环节卡住的工程师。

1. Padding 不是凑数的:先弄清楚三种等待模式在解决什么问题

1.1 矩阵计算要求每个 batch 内长度一致,这是 padding 存在的根本原因

Transformer 模型内部绝大部分计算都是张量运算,attention 的核心操作依赖(batch, seq_len, hidden_dim)这样的形状。一个 batch 里如果同时出现长度为 3、5、9 的句子,这三条样本根本没法叠成一个规整的矩阵参与矩阵乘法。你不是在“给模型补词”,你是在“把一张长短不齐的桌子修成矩形”。

但这里有一个很容易被忽略的矛盾点:形状上补齐了,语义上不能补。padding token 只是为了让长度对齐而存在的空位,它在输入序列里并不代表任何真实信息。所以 Transformer 里才有对应的attention_mask,模型在计算注意力分数时要把这些空位的位置权重遮掉。

打个比方:一桌人合影,为了拍出一张整齐的矩形照片,个子矮的人脚下要垫箱子,但垫箱子不是让照片里多一个人,后期必须把箱子修掉。padding 就是垫箱子,attention mask 就是后期修图。前者解决整齐问题,后者解决“别让人误以为箱子是人”的问题。

1.2 三种常见 padding 模式:longest、max_length、动态分桶

我自己第一次写代码时,只知道padding=True,后来才发现这背后其实有几种不同的取舍,分别对应不同的训练场景。

模式基准长度是否定长显存波动实现难度适用场景
longest当前 batch 内最长样本不定,每个 batch 不同较大低训练阶段长度差异大的任务
max_length固定长度,如 512恒定小低分布式训练、需固定 shape 的部署
动态分桶每个桶内最长样本桶内定长,桶间不定中等中大批量训练、吞吐优先

padding="longest"的好处是几乎没有多余计算,缺点是每个 batch 的序列长度都在变,GPU 利用率会有波动。padding="max_length"的好处是 shape 稳定,坏处是短样本被白白补一大截,长样本又可能被截断。动态分桶是折中方案:先把样本按长度分到若干桶里,每个桶内部再做 longest padding,这样既减少了无意义的 pad 占比,又不会剧烈波动。

从实践来看,训练阶段我会优先使用 longest 或动态分桶,除非是分布式多卡训练中对固定 shape 有硬性要求。推理阶段则要单独考虑,后面专门讲。

1.3 pad token 本质上是一个真实的符号,不是数字 0

padding 并不是简单往input_ids里填 0。pad_token_id是词汇表里一个实际存在的 token id,它有自己的 embedding 向量。你填进去的是一个有向量表示的符号,只是模型通过 mask 知道这个位置不该产生真实信息。

很多人会把input_ids里的 pad 位置和attention_mask里的 0 混为一谈。实际上input_ids里填的是pad_token_id,attention_mask里对应位置才是 0。一个是内容符号,一个是参与计算的开关,两回事。

还要注意,并不是所有 tokenizer 都自带 pad token。我用过的某些开源模型,Tokenzier 默认没有设置pad_token,如果不处理,训练时就会直接报错或者生成奇怪的结果。此时需要手动添加特殊 token,具体做法在后面代码模板里会写。

2. 训练阶段:真正做对了才不会被维度错误教做人

2.1 tokenizer 参数的正确组合:padding、truncation、max_length 缺一不可

训练阶段最常见的写法是这样:

encoded = tokenizer( batch_texts, padding="longest", # 或者直接写成 padding=True truncation=True, max_length=256, return_tensors="pt", )

这里要注意几个容易混淆的细节。

padding=True是padding="longest"的等价写法,它会根据当前这一批数据的最大长度来补。padding="max_length"则不同,它需要配合max_length作为补齐目标长度。max_length这个参数同时也承担 truncation 的长度上限,所以一旦设了它,短文本会被补长,长文本会被截断,整套行为都在一个参数里控制。

训练时我建议至少把truncation=True一直开着。否则当某个 batch 里出现一条超长文本,而你又设置了max_length却没有 truncation,tokenizer 可能不会像你预期的那样截断,而是把超出的内容原样返回,直接打乱后面的形状假设。

另外提一句,tokenizer 的 padding 是针对一次调用里的多条文本而言的。如果你单条文本传入tokenizer(text),就算写了padding=True,也不会有什么效果,因为单条文本不需要对齐,它是“自己和自己对齐”。

2.2 labels 同样要 padding,而且 pad 值必须用 -100

padding 不只是输入侧的事。序列标注、文本生成这类 token 级任务里,labels也是序列,长度必须和input_ids对齐,否则 forward 阶段计算 loss 时维度对不上。

但 labels 里 padding 位置不能随便填,最标准的做法是填-100。PyTorch 的CrossEntropyLoss默认把ignore_index设为-100,也就是说这个位置不参与梯度计算。Transformers Trainer 的大部分任务也都沿用了这个约定。

举个例子,某序列标注样本经过 padding 后:

input_ids: [101, 2023, 2003, 1996, 102, 0, 0] labels: [101, 2023, 2003, 1996, 102, -100, -100]

这里的0是 pad token id,而 labels 里对应位置是-100。如果 labels 里也填 0,相当于告诉模型“padding 这个位置我要预测 id 为 0 的 token”,模型会把大量计算浪费在学 padding 位置的规律上,训练曲线会变得非常不稳。

提示:分类任务的 labels 是标量,不需要 padding。需要 padding 的是那些 labels 本身也是 token 序列的任务,比如序列标注、摘要生成、机器翻译、文本生成。

2.3 DataCollator 是训练侧管理 padding 的核心,但别完全依赖默认行为

在 Transformers 训练流程里,DataLoader每次取出的原始样本长度各不相同,需要一个collate_fn把它们整合成 batch。DataCollatorWithPadding就是为了这个场景设计的,它内部会调用tokenizer.pad,把 input 侧处理得很干净。

from transformers import DataCollatorWithPadding collator = DataCollatorWithPadding(tokenizer=tokenizer)

但我后来发现一个问题:它处理 input 没问题,但 labels 的 pad 值替换成-100这件事,在不同版本和不同任务里并不总是自动发生。为了让自己睡得踏实,我最终选择了自定义 collator,把 labels 处理逻辑写在明面上。

如果你不用 Transformers 这套 Trainer,而是手写 PyTorch 训练循环,通常会用到torch.nn.utils.rnn.pad_sequence:

from torch.nn.utils.rnn import pad_sequence input_ids = pad_sequence( [torch.tensor(item["input_ids"]) for item in batch], batch_first=True, padding_value=tokenizer.pad_token_id, ) attention_mask = pad_sequence( [torch.tensor(item["attention_mask"]) for item in batch], batch_first=True, padding_value=0, ) labels = pad_sequence( [torch.tensor(item["labels"]) for item in batch], batch_first=True, padding_value=-100, )

pad_sequence只会帮你把序列补齐,不会自动选择合理的 pad 值,input 的 pad 值、label 的 pad 值、mask 的 pad 值全靠手动传。这里最容易漏的就是 mask 那一行,我之前就漏过,后面仔细讲。

3. 推理阶段:同样的 padding,为什么坑比训练还多

3.1 attention mask 一旦丢失,批量推理结果会出现系统性的偏差

很多人觉得模型训练完就稳定了,推理时少传点参数无所谓。这个想法在 padding 场景特别危险。

推理阶段如果不传attention_mask,模型仍然会执行完整的 attention 计算,padding 位置会被当作真实 token 参与注意力权重分配。后果不是直接报错,而是结果悄悄变差。具体表现有几种:序列 mean pooling 时被 padding 稀释,注意力权重被吸引到无意义的 pad 位上,分类边界附近的样本预测被系统性拉偏。

我遇到过的情况是:同一个文本,单独预测的结果是对的,放进 batch 之后预测反而错了。后来排查出来,就是因为推理脚本里组装 batch 时只放了input_ids,没放attention_mask。

验证方法也很简单:拿一条长度差异明显的样本,单独跑一次,再塞进一个只有短文本的 batch 里跑一次,对比两次 logits。如果结果不一致,八成就是 mask 或者 padding 相关问题。

3.2 左 padding 与右 padding:生成模型的最后一位必须是真实内容

padding 方向这事,训练阶段不太容易感知,但推理阶段会直接影响生成质量。

对 decoder-only 这类自回归模型来说,生成逻辑是从输入序列的末尾继续往后预测。如果使用的是右 padding,真实内容会被挤到序列开头,最后一个位置是 pad token 的隐状态。当你依靠最后一个位置的隐状态去预测下一个 token 时,拿到的就是一个无意义位置的特征。这也是为什么很多生成任务的推理批次里要求左 padding。

tokenizer.padding_side = "left"

设置了左 padding 以后,真实内容靠右,最后一个非 pad 位置就是输入的真实末尾,生成起点是正确的位置。

不过左 padding 也不是设置完就万事大吉。有些实现里position_ids仍然从 0 开始编号,左侧的 pad 位置也会被分配一个位置编号,这会导致位置信息整体偏移。签名侧的策略是:改padding_side之后,自己打印一次input_ids、attention_mask、position_ids,确认最终喂进模型的三个张量在语义上是对齐的。

3.3 单条推理与批量推理:策略不同,别复用同一段代码

单条推理不需要 padding,必要性为零。批量推理则必须处理长度对齐,但我不建议无脑固定一个max_length。

固定长度的问题有两个:短文本被白白 pad 一截,长文本又被截断导致关键信息丢失。更好的做法还是按当前 batch 内最长样本做动态 padding,把 truncation 上限设高一点,只用来兜底。代码上就是padding="longest",配合truncation=True。

另一个经验是:训练阶段用 longest,推理阶段也尽量保持 longest。虽然理论上模型能适应不同 padding 方式,但完全一致的预处理策略会让训练和推理之间的分布偏移最小。

4. 踩坑实录:三次 padding 事故的完整排查链路

4.1 事故一:attention mask 忘传,线上分类精度掉了一截

当时某个二分类模型在训练评估时指标正常,部署到线上推理服务后,发现中长文本的预测结果总是偏向某一个类别。短文本基本正常,长文本越是后面 pad 多,偏差越明显。

排查链路我如实记在这里:

  1. 先怀疑后处理逻辑,单独构造一条短文本测试,预测正常。
  2. 再把这条短文本和一条长文本放进同一个 batch,发现同样的内容预测结果变了。
  3. 打印 batch 里的input_ids,短文本后面补了一串 pad token。
  4. 检查送入模型的 dict,发现只有input_ids和token_type_ids,没有attention_mask。

修复方案是在推理脚本里显式传入attention_mask。修复后再跑同一条样本,单测和 batch 结果完全一致。

这也是我后来一直坚持的经验:只要看到“单独跑对,batch 跑不对”,第一反应就是去查 padding 和 mask,而不是先去调模型参数。

4.2 事故二:labels 没有 padding,形状对不上导致 loss 计算崩溃

做序列标注任务时,有一次我偷懒没有自定义 collator,直接用了框架默认的数据整理流程。前几个 batch 正常,后来某个 batch 里所有样本长度都比较短,而最长样本和最短样本差异很大,loss 计算报错,提示 logits 和 labels 的形状不一致。

排查链路:

  1. 看报错信息,定位到loss = criterion(logits.view(-1, num_labels), labels.view(-1))这一行。
  2. 打印 logits 形状,发现 seq_len 维度对应的是 batch 内最长 padding 后的长度。
  3. 打印 labels 形状,发现它停留在每条样本的原始长度,没有被补齐。
  4. 根因是训练数据构造时 labels 没有经过 padding 逻辑。

修复方案是在数据处理阶段让 labels 走一遍和 input_ids 完全相同的 padding 流程,并且把新增位置填为-100。

之后我每次写训练数据处理,都会把 input 和 labels 放到同一个函数里统一处理,避免出现“一个补了另一个没补”的状态。

4.3 事故三:固定 max_length 导致显存被 padding 吃满,吞吐上不去

有一个文本生成项目,数据长度长尾分布严重。大部分样本只有几十个 token,少部分样本接近几百甚至上千 token。我一开始图省事,直接设了max_length=512,训练时显存一直贴着上限,batch size 不敢调大,吞吐低得让人着急。

排查链路:

  1. 对数据集的 token 长度做分布统计,发现中位数只有 64,95 分位才是 512 左右。
  2. 计算 padding 占比,发现整个训练过程中超过六成的计算都在处理无意义的 pad 位置。
  3. 把padding="max_length"改成padding="longest",显存占用立刻降下来,batch size 可以往上提。

后续我又进一步做了长度分桶,把相似长度的样本放在一起,同一桶内再做 longest padding。吞吐比固定长度提升了将近一倍,且显存波动在可控范围内。

这个教训让我形成习惯:任何用固定max_length的方案,都得先问一句“我的数据长度分布真的支持这个固定值吗”。如果分布很集中,固定长度没问题;如果长尾严重,动态 padding 几乎一定是更优解。

4.4 三次事故的共同根因

回头看,三个事故其实指向同一个本质:padding 有“形状补齐”和“语义屏蔽”两层含义,任何一层没做好都会出问题。

事故症状根因修复
分类精度掉点单独推理正确,batch 推理偏移attention mask 丢失显式传attention_mask
loss 崩溃logits 与 labels 形状不匹配labels 未同步 paddinglabels 补到同长度并填 -100
显存吃满GPU 占用率高但有效计算少固定长度导致大量 pad动态 padding 或长度分桶

5. 一套可直接抄的训练 / 推理 padding 代码模板

5.1 训练侧:自定义一个可靠的 collator

先确认 tokenizer 有没有 pad token,没有就手动加。这里有一个容易忽略的点和模型 embedding 的关系,如果加载模型之后才新增了 pad token,需要重新调整 embedding 矩阵大小。

from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments model_name = "some-base-model" tokenizer = AutoTokenizer.from_pretrained(model_name) if tokenizer.pad_token is None: tokenizer.add_special_tokens({"pad_token": "[PAD]"}) # 如果模型是在添加 pad token 之前加载的,必须 resize embedding model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) if len(tokenizer) != model.config.vocab_size: model.resize_token_embeddings(len(tokenizer))

然后定义一个自定义 collator,把 input 和 labels 的 padding 逻辑放在同一个地方,避免出现“只补 input 不补 label”的问题。

import torch class UnifiedCollator: def __init__(self, tokenizer): self.tokenizer = tokenizer def __call__(self, features): # 让 tokenizer 按 batch 内最长长度补齐所有序列字段 batch = self.tokenizer.pad( features, padding=True, return_tensors="pt", ) if "labels" in batch: # 把 labels 中 padding 占位统一替换为 -100 batch["labels"] = batch["labels"].masked_fill( batch["labels"] == self.tokenizer.pad_token_id, -100 ) return batch

Trainer 里直接指定:

trainer = Trainer( model=model, args=TrainingArguments(output_dir="./output"), train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=UnifiedCollator(tokenizer), )

在使用 Trainer 时,很多同学会直接用默认 collator,结果就是 labels 的处理规则不透明。写上这个自定义版本之后,整个 padding 链路就在你自己掌控下了。

5.2 推理侧:批量分类与批量生成

分类推理就是按 batch 动态 padding,注意把attention_mask也放进 model 的入参:

def batch_predict(texts, tokenizer, model, device, max_len=256): encoded = tokenizer( texts, padding="longest", truncation=True, max_length=max_len, return_tensors="pt", ) encoded = {k: v.to(device) for k, v in encoded.items()} with torch.no_grad(): outputs = model(**encoded) probs = outputs.logits.softmax(-1) return probs

生成任务则要先检查 padding 方向。对 decoder-only 模型,推理批量生成前把padding_side切到left:

tokenizer.padding_side = "left" encoded = tokenizer( texts, padding=True, truncation=True, max_length=512, return_tensors="pt", ) encoded = {k: v.to(device) for k, v in encoded.items()} generated_ids = model.generate( **encoded, max_new_tokens=64, )

这里最心虚的地方是position_ids。部分模型在 left padding 时会正确生成,部分则不会,建议打印一次序列送入前后的位置索引,确认真实 token 的位置编号正确。

5.3 性能优化:pad_to_multiple_of 与长度分桶

如果动态 padding 已经用上了,还想继续优化,有两个方向值得试。

第一个是pad_to_multiple_of。这个参数可以把 padding 后的长度对齐到某个数字的倍数。大部分 GPU 加速库对对齐后的形状处理更友好,实测在小 batch 场景下有一定加速效果,虽然没有夸张到翻倍,但属于零成本收益。

encoded = tokenizer( texts, padding="longest", pad_to_multiple_of=8, return_tensors="pt", )

第二个是长度分桶。做法是先统计所有训练样本的 token 长度,按长度排序后切成若干桶,每个桶内动态 padding,并且每个 epoch 开始前打乱桶的顺序。这样既能避免浪费,又不破坏训练随机性。

def build_buckets(texts, bucket_size=100): lengths = [len(tokenizer(text)["input_ids"]) for text in texts] order = sorted(range(len(texts)), key=lambda i: lengths[i]) buckets = [order[i : i + bucket_size] for i in range(0, len(order), bucket_size)] return buckets

对大批量训练来说,长度分桶带来的吞吐提升往往比调整学习率还明显。我测试过的某个序列标注任务里,固定长度改成动态分桶后,训练耗时降了大概一半。

6. 我的几条实操体会

写了这么多,最后还是想把自己最直接的几条经验留在这里,比代码更值钱。

第一,把 padding 当成一套完整的协议来对待:input_ids负责形状,attention_mask负责内容参与度,labels里的-100负责梯度屏蔽。三条缺一不可,每次组装 batch 时按这个清单自查。

第二,训练和推理阶段的 padding 策略尽量保持一致。你可以训练时用 longest、推理时用固定长度,但这么做省不了多少事,反而引入不必要的分布偏移。我自己已经改成两套代码全部走动态 padding。

第三,不要盲信默认 collator。训练脚本里如果发现数据处理环节有模糊不清的地方,优先写一个显式的 collator,把 padding 规则打印出来确认,哪怕多花十分钟也值得。

第四,排障时记住一个优先级:先查 data,再查 model,最后才调超参。凡是出现“单条对、batch 错”“训练时好时坏”“显存莫名偏高”这类问题,基本都能在 padding 和 mask 这一步找到答案。

第五,pad token 缺失的问题在任何模型上都有可能遇到。函复的适配方式就两行代码,但少了这两行,后续所有 padding 逻辑都会建立在沙地上。

整理完这份笔记之后,我自己再写数据处理逻辑时,已经把这些规则当成了默认动作。如果你顺着这套流程走一遍,应该也能少踩很多原本看不到的坑。

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

OPC UA服务端快速开发工具包:3天交付可审计嵌入式服务器

简介:这是一套面向工业自动化软件开发者的OPC服务器快速开发工具包,专为缺乏ATL/DCOM及OPC底层协议经验的C或VB初级程序员设计,可显著降低OPC DA 1.0/2.0标准服务器的开发门槛。资源共54个文件,涵盖7个核心C源码(cpp&a…

作者头像 李华
网站建设 2026/10/20 7:39:18

如何合理使用AI编程平台的项目启动信用额度

我无法根据您提供的输入内容生成符合要求的博文。原因如下:输入中缺少项目正文、关键词、摘要描述等必要字段,仅提供了项目标题和空的“相关热搜词”“最新网络热词”及一段空的搜索内容(),无任何实质性信息支撑。标题…

作者头像 李华
网站建设 2026/10/20 7:39:17

Postgres-XL GTM主备启动与故障切换机制深度解析

接触过单机PostgreSQL的人,第一次看Postgres-XL的部署文档,多半会在GTM这个组件上卡一下。明明是数据库,为什么要额外起一个叫“全局事务管理器”的服务?而且它还分Master和Standby,两个都要启动,启动顺序和…

作者头像 李华
网站建设 2026/10/20 7:37:55

HTTP协议核心总结:从报文结构、缓存机制到版本演进

做了这么多年 Web 开发,几乎每个项目都在跟 HTTP 打交道。说实话,真正理解 HTTP 的人比想象中少——能背出状态码的人很多,但能把一次请求从发出到响应、从缓存到重定向、从 HTTP/1.1 到 HTTP/3 完整讲清楚的人很少。这篇 HTTP 总结不打算按教…

作者头像 李华
网站建设 2026/10/20 7:35:31

Vidu Q4预览版:三锚点语义架构重构AI视频生成工作流

1. 这不是“又一个AI视频工具”,而是视频生成工作流的重新定义最近在几个技术社群里,不少做内容创作的朋友都在问:“Vidu Q4预览版到底值不值得现在就上手?”我第一时间下载试用,并拉了三位不同背景的同行——一位专注…

作者头像 李华
网站建设 2026/10/20 7:35:23

GLM-4源码包深度解析:本地可调试推理引擎搭建指南

简介:本资源为智谱AI最新发布的GLM-4大语言模型官方代码仓库完整源码压缩包,面向AI算法工程师、大模型研究者及开源贡献者,用于本地部署、二次开发、推理优化与微调实验。压缩包共78个文件,涵盖28个核心Python脚本(含i…

作者头像 李华