【Transformers 实战——特征提取】从 Tokenizer 编码到 BERT 四类返回值详解与 NER 选型避坑

【Transformers 实战——特征提取】从 Tokenizer 编码到 BERT 四类返回值详解与 NER 选型避坑 文章目录1、my_tokenizer.encode_plus(...)2、代码3、my_model(...) 返回值last_hidden_state 和 pooler_output4、my_model(...) 返回值hidden_states 和 attentions5、my_model(...) 返回值总结两种模型的加载及其细节可以看这篇文章一文吃透 Transformers 模型加载from_pretrained 参数大全与推理全流程解析分词器的加载及其细节可以看这篇文章一文吃透 HuggingFace TokenizerAPI 参数详解、encode 避坑、模型推理全流程实战1、my_tokenizer.encode_plus(...)简单来说encode_plus是transformers库中一个功能强大的底层方法。它主要用于对单条文本或一个文本对进行完整编码并返回一个包含input_ids、attention_mask等丰富信息的BatchEncoding对象。尽管它曾经是主要接口但现在官方更推荐使用高层封装的__call__方法。encode_plus和my_tokenizer(...)以及my_tokenizer.encode(...)的核心区别在于调用层级和信息完整度。方法调用层级输入支持返回值推荐程度encode()便捷方法单条 / 批量仅input_ids列表或张量仅用于快速测试encode_plus()底层方法单条或文本对信息完整的BatchEncoding对象阅读旧代码时需了解batch_encode_plus()底层方法批量文本信息完整的BatchEncoding对象批量编码的底层实现__call__()官方推荐单条 / 批量信息完整的BatchEncoding对象日常开发首选 三者关系详解encode()一个“轻量级”便捷方法只返回纯净的input_ids序列。它不提供attention_mask或token_type_ids等关键辅助信息因此极易触发模型关于缺失attention_mask的警告。通常只适合在快速查看 token ID 或调试时使用。encode_plus()专注于单条文本编码的“基本工作单元”返回一个包含完整编码信息的BatchEncoding对象。它是__call__方法处理单条输入时的底层实现。batch_encode_plus()与encode_plus对应专门用于批量文本的高效编码同样是底层方法。__call__()这是当前官方推荐的统一入口。它内部会根据输入类型智能分发传入单条文本→ 调用encode_plus()。传入文本列表→ 调用batch_encode_plus()。因此使用__call__()方法可以同时获得代码的简洁性和批量处理的高效性并始终返回可直接传入模型的BatchEncoding对象。 深入理解encode_plus函数签名与关键参数encode_plus提供了对编码过程的精细控制其参数与你熟悉的__call__方法高度一致因为后者正是对其的封装。defencode_plus(text,# 核心要编码的文本或文本对 (text, text_pair)text_pairNone,# 可选第二个文本用于句对任务add_special_tokensTrue,# 是否添加 [CLS], [SEP] 等特殊标记paddingFalse,# 填充策略truncationFalse,# 截断策略max_lengthNone,# 最大长度限制return_tensorsNone,# 返回张量类型 (pt, tf, np, None)return_attention_maskNone,# 是否返回注意力掩码默认遵循模型设置return_token_type_idsTrue,# 是否返回分段IDBERT系模型需要return_offsets_mappingFalse,# 是否返回 token 到原字符的偏移映射return_special_tokens_maskFalse,# 是否返回特殊 token 掩码return_lengthFalse,# 是否返回去除填充后的真实长度**kwargs# 其他参数)返回值BatchEncoding对象与encode()只返回简单列表不同encode_plus返回的是一个功能丰富的BatchEncoding对象行为类似字典你可以直接将其解包**后传递给模型。input_ids模型能直接“消化”的数字 ID 序列是最核心的输出。attention_mask强烈建议使用它告诉模型哪些位置是真实的词1哪些是填充的0。token_type_ids用于区分句子对如问答、相似度任务单句任务中全为0。可选信息如offsetstoken 到原字符的映射NER 任务必备、special_tokens_mask和length等为更复杂的任务提供额外元数据。 总结日常开发如何选择场景需求推荐方法理由模型推理或训练my_tokenizer(texts, ...)官方推荐代码简洁高效自动处理单条/批量返回完整BatchEncoding。快速查看 token ID无需掩码my_tokenizer.encode(text)简单快捷直接返回 ID 列表适合临时调试。理解底层机制或阅读旧版代码my_tokenizer.encode_plus(...)有助于深入理解 Tokenizer 内部原理但新项目开发中不推荐直接使用。因此在日常使用中熟练掌握my_tokenizer(...)即__call__方法就完全足够了它体现了当前 Transformers 库的最佳实践。2、代码importtorchfromtransformersimportAutoTokenizer,AutoModel# 1. 加载分词器my_tokenizerAutoTokenizer.from_pretrained(r./model/bert-base-chinese)# print(my_tokenizer)# BertTokenizerFast(name_or_path./model/bert-base-chinese, vocab_size21128, model_max_length1000000000000000019884624838656, is_fastTrue, padding_sideright, truncation_sideright, special_tokens{unk_token: [UNK], sep_token: [SEP], pad_token: [PAD], cls_token: [CLS], mask_token: [MASK]}, clean_up_tokenization_spacesFalse, added_tokens_decoder{# 0: AddedToken([PAD], rstripFalse, lstripFalse, single_wordFalse, normalizedFalse, specialTrue),# 100: AddedToken([UNK], rstripFalse, lstripFalse, single_wordFalse, normalizedFalse, specialTrue),# 101: AddedToken([CLS], rstripFalse, lstripFalse, single_wordFalse, normalizedFalse, specialTrue),# 102: AddedToken([SEP], rstripFalse, lstripFalse, single_wordFalse, normalizedFalse, specialTrue),# 103: AddedToken([MASK], rstripFalse, lstripFalse, single_wordFalse, normalizedFalse, specialTrue),# }# )# 2. 加载模型my_modelAutoModel.from_pretrained(r./model/bert-base-chinese)# 3. 准备样本message[我们各自攀登,然后山顶见]# 4. 把句子转成张量message_idmy_tokenizer(textmessage,return_tensorspt,paddingTrue,# True (补齐到 batch 中最长)truncationTrue,max_length15)# print(message_id)# 101-CLS(开头), 102-SEP(结尾)# {# input_ids: tensor([[101, 2769, 812, 1392, 5632, 3102, 4633, 102],# [101, 4197, 1400, 2255, 7553, 6224, 102, 0 ]]), # 自动处理了batch, my_tokenizer.encode_plus(..)只能处理单样本或文本对# token_type_ids: tensor([[0, 0, 0, 0, 0, 0, 0, 0],# [0, 0, 0, 0, 0, 0, 0, 0]]),# attention_mask: tensor([[1, 1, 1, 1, 1, 1, 1, 1],# [1, 1, 1, 1, 1, 1, 1, 0]])# }# 5. 开启模型推理模型 不计算梯度, 这两个可以同时开启, 也可以只开启 my_model.eval(), 推荐同时开启my_model.eval()withtorch.no_grad():# 6. 得到模型输出结果outputmy_model(**message_id)print(output.last_hidden_state.shape)# torch.Size([2, 8, 768])print(output.pooler_output.shape)# torch.Size([2, 768])print(output)# BaseModelOutputWithPoolingAndCrossAttentions(# last_hidden_statetensor([# [[ 0.5498, 0.7501, 0.0460, ..., 0.6016, -0.1392, -0.2972],# [ 0.1664, -0.0419, -0.0726, ..., -0.5484, -0.5869, -0.2282],# ...,# [ 1.0726, 0.3592, -0.3137, ..., 0.8689, -0.3638, -0.4375],# [ 0.2997, 0.3714, 0.1938, ..., 0.2208, -0.0583, -0.2907]],## [[-0.8766, 0.8066, -0.2332, ..., 0.4074, 0.5240, -0.3291],# [-0.4696, -0.0328, 0.9041, ..., -0.9132, -0.2767, -0.1639],# ...,# [-0.7512, 0.6317, 0.0726, ..., 0.3382, 0.0803, -0.0023],# [-0.4700, 0.5480, -0.0080, ..., -0.1451, -0.0982, -0.5543]]# ]),# pooler_outputtensor([# [ 0.9995, 1.0000, 0.7246, ..., -0.9914, -0.9976, 0.8039],# [ 0.9994, 1.0000, 0.9479, ..., -0.9971, -0.9897, 0.8022]# ]),# hidden_statesNone,# past_key_valuesNone,# attentionsNone,# cross_attentionsNone# )3、my_model(...)返回值last_hidden_state和pooler_output BERT 输出详解# BaseModelOutputWithPoolingAndCrossAttentions(# last_hidden_statetensor([# [[ 0.5498, 0.7501, 0.0460, ..., 0.6016, -0.1392, -0.2972],# [ 0.1664, -0.0419, -0.0726, ..., -0.5484, -0.5869, -0.2282],# ...,# [ 1.0726, 0.3592, -0.3137, ..., 0.8689, -0.3638, -0.4375],# [ 0.2997, 0.3714, 0.1938, ..., 0.2208, -0.0583, -0.2907]],## [[-0.8766, 0.8066, -0.2332, ..., 0.4074, 0.5240, -0.3291],# [-0.4696, -0.0328, 0.9041, ..., -0.9132, -0.2767, -0.1639],# ...,# [-0.7512, 0.6317, 0.0726, ..., 0.3382, 0.0803, -0.0023],# [-0.4700, 0.5480, -0.0080, ..., -0.1451, -0.0982, -0.5543]]# ]),# pooler_outputtensor([# [ 0.9995, 1.0000, 0.7246, ..., -0.9914, -0.9976, 0.8039],# [ 0.9994, 1.0000, 0.9479, ..., -0.9971, -0.9897, 0.8022]# ]),# hidden_statesNone,# past_key_valuesNone,# attentionsNone,# cross_attentionsNone# )简单来说output对象就像是模型处理完文本后给你的“体检报告”。对于AutoModel基础模型来说这份报告主要包含两个核心部分详细的逐词报告(last_hidden_state) 和整句的总结报告(pooler_output)。 1.last_hidden_state逐词的特征表示这是模型最后一层 Transformer 输出的原始结果它保留了文本的序列结构。含义它代表了输入序列中每一个 token包括[CLS]、汉字、标点、[SEP]以及填充的[PAD]在经过 BERT 模型深层处理后得到的上下文向量表示。在这个向量中每个词的含义都融合了整句话的语境因为 BERT 是双向的。形状解读torch.Size([2, 8, 768])这三个数字分别代表(Batch_Size, Sequence_Length, Hidden_Size)2(Batch Size)你输入了两句话[我们各自攀登, 然后山顶见]所以模型一次性处理了 2 个样本。8(Sequence Length)这是分词并填充后的长度。第一句我们各自攀登(6个字) [CLS][SEP] 8个token。第二句然后山顶见(5个字) [CLS][SEP] 7个token。因为开启了paddingTrue第二句被补齐到了 8 的长度最后一个是[PAD]。所以长度是 8。768(Hidden Size)这是bert-base模型的隐藏层维度。每一个 token 都被映射成了一个 768 维的向量用来描述这个词在当前语境下的特征。形象理解这就像是一个 Excel 表格有 2 行2句话每行有 8 列8个词每个格子里装着 768 个数字这个词的详细特征。 2.pooler_output整句的聚合表示这是专门为了句子级别的任务如文本分类、语义相似度准备的输出。含义它是对整个句子语义的总结。具体来说它的计算过程分为两步提取取出last_hidden_state中第一个 token即[CLS]的向量。变换将这个向量通过一个**全连接层Linear**和一个Tanh 激活函数进行非线性变换。这个全连接层的权重是在 BERT 预训练阶段Next Sentence Prediction 任务学到的。形状解读torch.Size([2, 768])这两个数字代表(Batch_Size, Hidden_Size)2(Batch Size)对应 2 个句子每个句子得到一个总结向量。768(Hidden Size)维度保持不变依然是 768 维。形象理解如果说last_hidden_state是整篇文章的逐字详解那么pooler_output就是这篇文章经过“提炼”后的“中心思想”。 总结对比表特性last_hidden_statepooler_output核心含义逐词表示每个词在语境中的具体含义整句表示整个句子的语义总结基于[CLS] 全连接层形状[2, 8, 768](3维)[2, 768](2维)适用任务序列标注如命名实体识别、分词、机器翻译句子分类如情感分析、文本匹配对应位置对应输入序列的每一个位置仅对应[CLS]位置经过额外处理 进阶补充避坑指南pooler_output真的最好吗虽然pooler_output设计初衷是用于分类但在很多现代微调任务中大家发现直接使用last_hidden_state中的[CLS]向量即last_hidden_state[:, 0, :]或者对整个序列进行平均池化Mean Pooling效果往往比pooler_output更好。因为pooler_output经过了 Tanh 激活可能会丢失一些信息。我们接着上面关于pooler_output的话题来具体说说“平均池化Mean Pooling”在这里是什么意思。 一句话概括平均池化就是把 BERT 输出的所有 Token 的向量在序列长度这个维度上求平均值得到一个单一的、固定长度的向量来表示整个句子。 为什么要做平均池化先回顾一下 BERT 的输出last_hidden_state形状是(batch_size, seq_len, hidden_size)。它是一个矩阵每一行对应输入文本中一个 Token比如[CLS]、我、爱、你、[SEP]的 768 维向量。但我们做分类或句子相似度任务时通常需要一个向量来表示整句话而不是一堆向量。常见的提取句子向量的方法有四种方法操作优缺点1. 直接取[CLS]last_hidden_state[:, 0, :]简单但[CLS]在预训练时主要用于 NSP 任务未必能完美总结整句信息。2. 用pooler_output取[CLS]后过全连接层 Tanh专为分类设计但 Tanh 可能导致信息压缩或饱和。3. 平均池化last_hidden_state在seq_len维度求平均让每个词都贡献意见能捕捉更全面的语义实践中常优于前两者。4. 最大池化last_hidden_state在seq_len维度求最大值只保留每一维最显著的特征但容易忽略整体语境较少单独使用。 平均池化的具体操作画图式讲解假设一句话有 4 个 Token包含[CLS]和[SEP]每个 Token 是一个 768 维向量。可视化矩阵数字随便写的仅示意维度 1 维度 2 ... 维度 768 [CLS] → [ 0.1, 0.2, ..., 0.5 ] 我 → [ 0.8, 0.1, ..., 0.3 ] 爱 → [ 0.6, 0.9, ..., 0.2 ] 你 → [ 0.3, 0.4, ..., 0.7 ] [SEP] → [ 0.0, 0.0, ..., 0.1 ]平均池化就是沿着“列”的方向即跨过所有 Token对每一列的所有数值求平均。结果是一个新的 768 维向量这个向量就是整句话的表示。代码实现PyTorch# last_hidden_state.shape (batch_size, seq_len, hidden_size)sentence_embeddinglast_hidden_state.mean(dim1)# 在序列长度维度 dim1 上求平均# sentence_embedding.shape (batch_size, hidden_size)就这么简单.mean(dim1)就是平均池化。⚠️ 实际编码避坑指南别忘了排除[PAD]上面直接.mean(dim1)有一个隐患如果句子长短不一我们通常会用[PAD]把短句子补长。[PAD]的向量是全 0如果不加处理直接平均会稀释掉真实语义句子越长有效信息占比越低。正确的平均池化写法利用attention_mask# attention_mask.shape (batch_size, seq_len)有效位置为 1填充位置为 0maskattention_mask.unsqueeze(-1).float()# 扩展维度以广播sum_embeddings(last_hidden_state*mask).sum(dim1)sum_maskmask.sum(dim1).clamp(min1e-9)# 防止除以 0sentence_embeddingsum_embeddings/sum_mask这样只有真正的词参与了平均[PAD]被完美忽略。 举个例子感受一下文本今天天气真好分词后[CLS] 今 天 天 气 真 好 [SEP]取[CLS]向量只听了第一个词[CLS]的“总结发言”。平均池化让[CLS]、今、天、天、气、真、好、[SEP]每个词都发言然后取大家的平均意见。这样好和天气等关键信息都直接参与了最终向量的构建不会被[CLS]一个词“代表”了。✅ 总结平均池化就是把 BERT 输出的所有 Token 向量加起来取平均得到一个融合了整句信息的句子向量。因为不引入额外参数、不经过 Tanh 压缩在很多微调任务中表现比pooler_output更稳定、效果更好。实际代码中务必结合attention_mask只对真实 Token 求平均。预训练模型的pooler_output是经过全连接层Linear处理得到的这是一种普遍的标准实现。不过想知道特定模型的确切情况最可靠的方法是“查看配置”或“检查模型”。 总结默认情况对于大多数标准的BERT模型pooler_output就是[CLS]向量经过一个全连接层 Tanh激活函数后的输出。如何确认最可靠检查config.json文件中的use_pooler或pooler_type参数。最直接打印模型对象查看pooler属性是否存在及其内部结构。最规范查阅模型官方文档或Hugging Face Hub上的模型卡片。通过这三种方法你就能准确判断任意预训练模型pooler_output的具体实现了。4、my_model(...)返回值hidden_states和attentions 深入 BERT 内部机制这部分确实是 BERT 的 “黑盒” 内部机制。为了让你彻底搞懂hidden_states和attentions我们需要把 BERT 想象成一个由 12 层 Transformer 堆叠而成的“千层饼”。默认情况下模型只给你看最后的结果last_hidden_state而把中间的过程都扔掉了。如果你开启了这两个选项就像是给模型做了一次“全身体检”能看到每一层的内部变化。以下是详细的解释 1.hidden_states模型的“思考过程”BERTBase版有12 层Transformer 编码器。默认情况模型只返回最后一层第 12 层的输出也就是我们常说的last_hidden_state。开启后你会得到一个包含13 个元素的元组Tuple。为什么是 13 个第 0 个元素Embedding 层的输出即词向量刚查表出来还没进 Transformer 之前的状态。第 1~12 个元素分别是第 1 层到第 12 层 Transformer 的输出。形状是什么每一个元素的形状都是[Batch_Size, Sequence_Length, 768]。比如你输入 2 句话每句 8 个词那么hidden_states就是一个包含 13 个[2, 8, 768]张量的元组。有什么用研究模型学到了什么浅层通常关注语法如词性深层关注语义如指代关系。提取更强特征有些任务如语义相似度会将最后 4 层的hidden_states加权求和效果比单用最后一层更好。️ 2.attentions模型的“注意力焦点”这是 Transformer 最核心的机制——自注意力Self-Attention。默认情况不返回。开启后你会得到一个包含12 个元素的元组对应 12 层。它代表什么意思它展示了在每一层中每一个词“关注”了句子中的哪些其他词。比如句子是“我把苹果吃了”当模型处理“吃”这个词时注意力机制会告诉我们要关注“我”主语和“苹果”宾语。形状是什么每一个元素的形状是[Batch_Size, Num_Heads, Sequence_Length, Sequence_Length]。Batch_Size2。Num_Heads12BERT Base 有 12 个注意力头意味着每一层有 12 种不同的关注视角。Sequence_Length, Sequence_Length比如 8x8。这是一个矩阵表示第i个词对第j个词的注意力权重0到1之间的概率值。有什么用可解释性分析你可以把注意力矩阵画成热力图。比如你想看模型是怎么做“指代消解”的比如把“它”和“苹果”联系起来看注意力图一目了然。️ 3. 如何开启代码实操开启这两个功能通常有三种方法方法二在实际写脚本时最常用。方法一在加载模型时配置最正式通过BertConfig在初始化时就告诉模型“我要保留中间状态”。fromtransformersimportBertModel,BertConfig configBertConfig.from_pretrained(./model/bert-base-chinese)config.output_hidden_statesTrueconfig.output_attentionsTruemodelBertModel.from_pretrained(./model/bert-base-chinese,configconfig)方法二直接传参最常用在from_pretrained加载模型时直接覆盖默认配置。# 推荐这种写法简单直接modelBertModel.from_pretrained(./model/bert-base-chinese,output_hidden_statesTrue,output_attentionsTrue)# 下面这个也是可以的my_modelAutoModel.from_pretrained(r./model/bert-base-chinese,output_hidden_statesTrue,output_attentionsTrue)方法三在推理时指定仅当前有效# 这种方式适合只想偶尔调试一次的情况outputsmodel(**message_id,output_hidden_statesTrue,output_attentionsTrue)⚠️ 4. 注意事项内存爆炸开启这两个选项会显著增加内存占用。因为原本模型只传一个张量现在要传 13 个隐藏状态和 12 个注意力矩阵。如果是大批量训练Batch Size 大很容易显存溢出OOM。速度变慢返回这些数据需要时间推理速度会略微下降。总结last_hidden_state是最终答案。hidden_states是草稿纸上的演算过程每一层的变化。attentions是解题时的视线轨迹关注点在哪里。5、my_model(...)返回值总结两种 Hugging Face 模型的两种返回类型与 NER 选型这两个返回值之所以长得完全不一样根本原因在于你加载的**模型类型Model Class**不同。简单来说就是你向模型“要”的东西不一样它给你的“包裹”自然也不同。在 Hugging Face 的 Transformers 库中模型主要分为两大类正好对应你看到的这两种输出基础模型Base Model代表模型BertModel、RobertaModel以及万能类AutoModel名字里没有For...的。对应你看到的输出BaseModelOutputWithPoolingAndCrossAttentions这类模型只负责把输入的文本转换成高质量的向量特征提取器它不包含针对具体任务如分类、NER的“任务头Task Head”。last_hidden_state这是最核心的输出。形状通常是[batch_size, seq_len, hidden_size]比如 BERT 就是 768 维。它包含了句子中每一个 Token经过 Transformer 编码后的上下文语义表示。做 NER命名实体识别或序列标注时我们就是拿这个输出接一个 Linear 层和 CRF。pooler_output形状通常是[batch_size, hidden_size]。它是把last_hidden_state中第一个 Token也就是[CLS]的向量拿出来再经过一个额外的全连接层Linear和 Tanh 激活函数处理后的结果。它代表的是整个句子的语义特征通常用于文本分类、句子相似度匹配等句子级别的任务。下游任务模型Task-Specific Models代表模型BertForSequenceClassification、AutoModelForSequenceClassification等名字里带For...的。对应你看到的输出SequenceClassifierOutput这类模型在基础模型Base Model的上面已经帮你搭好了一个专门用于特定任务的“头Head”。比如ForSequenceClassification就是在 BERT 后面接了一个分类器。logits这是最核心的输出。它是经过模型顶部的分类层计算后Softmax 激活函数之前的原始预测分数。比如你的输出是tensor([[-1.34, ..., 3.65], ...])这代表当前 batch 里的两句话在 5 个分类标签上的预测得分得分最高的那个就是模型预测的类别。loss如果你在调用模型时传入了labels真实标签它会自动帮你计算交叉熵损失并返回如果没传labels这里就是None。 核心区别与 NER 任务的选择为了让你更直观地理解我为你整理了一个对比表格特性基础模型 (BertModel/AutoModel)任务模型 (BertForSequenceClassification)返回对象BaseModelOutputWithPooling...SequenceClassifierOutput核心输出last_hidden_state(每个字的特征)logits(最终的分类预测分)包含任务头❌ 无 (只有 Transformer 底座)✅ 有 (底座 顶部的分类层)适用场景需要自定义下游结构 (如 NER CRF)标准的文本分类、情感分析如果你要做的是 NER命名实体识别并且要自己接一个 CRF 层。这意味着你需要拿到每一个字Token的深层语义特征然后自己传给 CRF 去计算发射分数。因此你应该使用的是第一种返回值的模型也就是基础模型如BertModel或AutoModel。你需要提取它的last_hidden_state形状[batch_size, seq_len, 768]。然后通过一个你自己定义的nn.Linear(768, num_tags)线性层把它转换成 CRF 需要的发射分数Emissions。避坑指南在做 NER 时千万不要用pooler_output因为它只是[CLS]这个特殊标记的向量丢失了句子中其他所有字的信息。如果你用了第二种模型ForSequenceClassification它直接给你返回了分类的logits不仅丢失了每个 Token 的细粒度信息而且它的输出维度是分类标签数完全无法满足 NER 任务的需求。