11_bert_head_pooler_output
from transformers import BertTokenizer, BertModelimport torch1. load model and tokenize
model_type = 'bert-base-uncased'bert = BertModel.from_pretrained(model_type)tokenizer = BertTokenizer.from_pretrained(model_type)text = 'This is a text sentence.'inputs = tokenizer(text, return_tensors='pt')inputs输出为:
{'input_ids': tensor([[ 101, 2023, 2003, 1037, 3793, 6251, 1012, 102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])输出为:
['[CLS]', 'this', 'is', 'a', 'text', 'sentence', '.', '[SEP]']2. forward and pooler output
bert.eval()输出为:
BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ))with torch.no_grad(): outputs = bert(**inputs)outputs.keys()输出为:
odict_keys(['last_hidden_state', 'pooler_output'])因为我们没有像在 10 节中那样,往 bert = BertModel.from_pretrained(model_type) 里面传入 output_hidden_states=True 的参数,所以这里只返回两个 keys,没有 hidden_states(参见 10 节)。
outputs['last_hidden_state'].shape # torch.Size([1, 8, 768])
outputs['pooler_output'].shape # torch.Size([1, 768])
outputs['pooler_output']输出为:
tensor([[-0.9320, -0.4660, -0.7054, 0.8013, 0.5395, -0.2326, 0.8985, 0.3100, -0.5941, -1.0000, -0.1055, 0.8209, 0.9858, 0.2604, 0.9526, -0.6847, -0.2648, -0.6366, 0.3471, -0.7114, 0.6518, 0.9998, 0.4752, 0.3604, 0.5276, 0.9401, -0.6821, 0.9463, 0.9627, 0.7596, -0.7972, 0.2211, -0.9909, -0.2564, -0.7488, -0.9928, 0.4347, -0.8085, -0.0717, -0.0137, -0.9286, 0.3592, 1.0000, -0.4739, 0.2884, -0.4205, -1.0000, 0.3234, -0.9190, 0.7232, 0.6715, 0.5167, 0.2301, 0.5236, 0.5461, -0.0622, -0.0630, 0.1883, -0.2802, -0.6607, -0.6670, 0.3900, -0.6133, -0.9413, 0.5821, 0.5472, -0.1604, -0.3769, -0.1576, -0.0250, 0.9041, 0.2955, -0.0418, -0.8321, 0.3900, 0.3109, -0.6546, 1.0000, -0.5625, -0.9819, 0.6171, 0.5162, 0.6011, 0.0482, 0.2289, -1.0000, 0.5958, -0.2103, -0.9917, 0.1602, 0.5617, -0.2821, 0.4634, 0.6287, -0.4933, -0.3350, -0.4028, -0.5878, -0.2872, -0.2631, 0.1988, -0.3540, -0.3882, -0.4115, 0.3234, -0.5114, -0.5085, 0.4941, -0.0114, 0.7357, 0.4615, -0.4198, 0.4495, -0.9632, 0.6444, -0.4120, -0.9886, -0.6143, -0.9883, 0.7400, -0.0702, -0.2572, 0.9748, -0.0086, 0.3666, -0.1545, -0.7035, -1.0000, -0.4364, -0.4116, -0.1010, -0.2736, -0.9826, -0.9627, 0.7004, 0.9718, 0.2619, 0.9998, -0.3054, 0.9478, -0.1006, -0.4293, 0.0960, -0.5103, 0.6878, 0.4061, -0.7764, 0.2234, -0.0699, 0.2507, -0.5001, -0.3151, -0.6120, -0.9422, -0.4599, 0.9538, -0.2553, -0.7623, 0.4071, -0.2945, -0.3870, 0.8860, 0.6171, 0.4231, -0.2888, 0.5307, 0.2569, 0.5667, -0.9075, 0.2098, 0.5000, -0.3142, -0.6503, -0.9822, -0.3632, 0.5577, 0.9902, 0.7836, 0.4002, 0.6218, -0.4148, 0.5758, -0.9572, 0.9842, -0.2230, 0.3169, -0.1974, 0.3705, -0.9103, 0.0049, 0.8592, -0.4281, -0.8949, -0.1139, -0.5668, -0.4746, -0.5588, 0.5546, -0.4232, -0.4242,... -0.0918, -0.4266, -0.3062, -0.8243, 0.8984, -0.4192, -0.5862, -0.5356, 0.6628, 0.3908, 0.9998, -0.5771, -0.6544, -0.2762, -0.3847, 0.4352, -0.4021, -1.0000, 0.4762, -0.2384, 0.5032, -0.3292, 0.5334, -0.3008, -0.9849, -0.2677, 0.4999, 0.4842, -0.5571, -0.4787, 0.6036, 0.3051, 0.8739, 0.9051, -0.1019, 0.2896, 0.6685, -0.6048, -0.7155, 0.9298]])outputs 中 last_hidden_state 的形状为 (1, 8, 768),其中:
- 1:对应一个 batch,因为我们只有一个句子;
- 8:对应 8 个 tokens(在我们的测试句子中,每个词恰好为一个 token);
- 768:每个 token 被表示为 768 维的向量。
而 pooler_output 的形状变成了 (1, 768) 就是如同我们在 03 节中所介绍的那样,pooler 层只取出编码器输出中第一个位置 [CLS] 的向量来作为整句话的语义表示
3. from scratch
my_output = bert.pooler.activation(bert.pooler.dense(outputs['last_hidden_state'][0][0, :]))my_output.shape # torch.Size([768])my_output输出为:
tensor([-0.9320, -0.4660, -0.7054, 0.8013, 0.5395, -0.2326, 0.8985, 0.3100, -0.5941, -1.0000, -0.1055, 0.8209, 0.9858, 0.2604, 0.9526, -0.6847, -0.2648, -0.6366, 0.3471, -0.7114, 0.6518, 0.9998, 0.4752, 0.3604, 0.5276, 0.9401, -0.6821, 0.9463, 0.9627, 0.7596, -0.7972, 0.2211, -0.9909, -0.2564, -0.7488, -0.9928, 0.4347, -0.8085, -0.0717, -0.0137, -0.9286, 0.3592, 1.0000, -0.4739, 0.2884, -0.4205, -1.0000, 0.3234, -0.9190, 0.7232, 0.6715, 0.5167, 0.2301, 0.5236, 0.5461, -0.0622, -0.0630, 0.1883, -0.2802, -0.6607, -0.6670, 0.3900, -0.6133, -0.9413, 0.5821, 0.5472, -0.1604, -0.3769, -0.1576, -0.0250, 0.9041, 0.2955, -0.0418, -0.8321, 0.3900, 0.3109, -0.6546, 1.0000, -0.5625, -0.9819, 0.6171, 0.5162, 0.6011, 0.0482, 0.2289, -1.0000, 0.5958, -0.2103, -0.9917, 0.1602, 0.5617, -0.2821, 0.4634, 0.6287, -0.4933, -0.3350, -0.4028, -0.5878, -0.2872, -0.2631, 0.1988, -0.3540, -0.3882, -0.4115, 0.3234, -0.5114, -0.5085, 0.4941, -0.0114, 0.7357, 0.4615, -0.4198, 0.4495, -0.9632, 0.6444, -0.4120, -0.9886, -0.6143, -0.9883, 0.7400, -0.0702, -0.2572, 0.9748, -0.0086, 0.3666, -0.1545, -0.7035, -1.0000, -0.4364, -0.4116, -0.1010, -0.2736, -0.9826, -0.9627, 0.7004, 0.9718, 0.2619, 0.9998, -0.3054, 0.9478, -0.1006, -0.4293, 0.0960, -0.5103, 0.6878, 0.4061, -0.7764, 0.2234, -0.0699, 0.2507, -0.5001, -0.3151, -0.6120, -0.9422, -0.4599, 0.9538, -0.2553, -0.7623, 0.4071, -0.2945, -0.3870, 0.8860, 0.6171, 0.4231, -0.2888, 0.5307, 0.2569, 0.5667, -0.9075, 0.2098, 0.5000, -0.3142, -0.6503, -0.9822, -0.3632, 0.5577, 0.9902, 0.7836, 0.4002, 0.6218, -0.4148, 0.5758, -0.9572, 0.9842, -0.2230, 0.3169, -0.1974, 0.3705, -0.9103, 0.0049, 0.8592, -0.4281, -0.8949, -0.1139, -0.5668, -0.4746, -0.5588, 0.5546, -0.4232, -0.4242,... 0.6628, 0.3908, 0.9998, -0.5771, -0.6544, -0.2762, -0.3847, 0.4352, -0.4021, -1.0000, 0.4762, -0.2384, 0.5032, -0.3292, 0.5334, -0.3008, -0.9849, -0.2677, 0.4999, 0.4842, -0.5571, -0.4787, 0.6036, 0.3051, 0.8739, 0.9051, -0.1019, 0.2896, 0.6685, -0.6048, -0.7155, 0.9298], grad_fn=<TanhBackward0>)可见,我们得到的结果与之前 outputs['pooler_output'] 的结果一模一样,下面也进行了验证。
3.1 代码分析
我们从最内层往外逐层看:
第 1 层:outputs['last_hidden_state']
形状 (1, 8, 768),即 :
token0 token1 ... token7batch0 [768维] [768维] ... [768维] ← 只有 1 个 batch第 2 层:[0] — 取第一个 batch
outputs['last_hidden_state'][0] # shape: (8, 768)去掉 batch 维度,剩下 的矩阵。
第 3 层:[0, :] — 取 [CLS] token 的向量
outputs['last_hidden_state'][0][0, :] # shape: (768,) [CLS] this is ... [SEP] ┌───┐ ┌───┐ ┌───┐ ┌───┐ (8,768) │ ● │ │ │ │ │ ... │ │ ← [0, :] 取第 0 行 └───┘ └───┘ └───┘ └───┘ ↑ [CLS] token 的 768 维向量BERT 的 [CLS] token 设计上就是用来聚合整个句子的语义信息的——经过 12 层 Transformer 后,[CLS] 的向量可以作为整句话的”摘要表示”。
第 4 层:bert.pooler.dense(...) — 线性变换
bert.pooler.dense(outputs['last_hidden_state'][0][0, :])# Linear(768 → 768),再做一次线性映射从模型结构可以看到:
(pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh())第 5 层:bert.pooler.activation(...) — Tanh 激活
bert.pooler.activation(...) # Tanh,把值压缩到 (-1, 1) 区间3.1.6 最终验证
torch.equal(my_output, outputs['pooler_output'][0]) # True证明手动算出来的和模型给的 pooler_output 完全一致。
3.1.7 一句话总结
outputs['last_hidden_state'] 池化器(pooler) 的处理流程┌─────────────────────────┐ ┌──────────────────────────────┐│ [CLS] this is ... │ │ ││ ● │ ───→ │ Linear(768→768) → Tanh │ ───→ 768 维句子向量│ │ │ │└─────────────────────────┘ └──────────────────────────────┘ 取 [CLS] token 的向量 dense + activation (第 0 行, 768 维) = BertPooler💡
pooler_output的本质就是:取[CLS]token 的最后一层隐藏状态,再过一层 Linear + Tanh,得到整句话的 768 维”句子嵌入”(通常用于下游分类任务)。

4. bert head
- default BertModel head
- BertForMaskedLM
4.1 BertModel head vs BertForMaskedLM
两者共享同一个 BERT Encoder(12 层 Transformer),区别在于 Encoder 上面加的”头”不同:
4.1.1 Default BertModel head
就是本笔记一直在用的——pooler(BertPooler):
last_hidden_state (1, 8, 768) │ ▼ 取 [CLS] 位置 (768,) 向量 │ ▼ Linear(768→768) + Tanh pooler_output (1, 768) ← 整个句子的"摘要向量"作为”基础模型”,
BertModel没有内置任何任务头,只提供一个pooler用于获取句子级表示。下游任务(如分类)需要你自己在它上面加分类层。
4.1.2 BertForMaskedLM
在 Encoder 之上加了一个 MLM Head——一个 Linear 层,把每个 token 的 768 维映射到 30522 维(词表大小):
# 伪代码last_hidden_state = bert_encoder(input_ids) # (batch, seq_len, 768)logits = mlm_head(last_hidden_state) # (batch, seq_len, 30522)# 对每个位置的预测 = argmax(logits, dim=-1)这是 BERT 预训练任务之一——随机遮住一些词,让模型预测它们。我们在 02 节中介绍过,并且将在 12 节中深入探索。
4.1.3 与这篇笔记的联系
| 本笔记的内容 | 关联 |
|---|---|
outputs['last_hidden_state'] | 两者都有,是 Encoder 的输出 |
outputs['pooler_output'] | 仅 BertModel 有,BERT 用它做句子分类(如 NSP) |
| MLM Head | BertForMaskedLM 用 last_hidden_state 的每个 token 位置来预测被 Mask 的词,不使用 pooler_output |
所以,我们目前探索的 pooler_output 只是 BERT 默认的一个”头”,BERT 还可以接不同的头来干不同的事——MLM head 只是其中一个例子,还有 BertForSequenceClassification(分类头)、BertForQuestionAnswering(问答头)等。
💡 可以把 BERT Encoder 想象成”发动机”,
pooler、MLM Head、分类头等是不同车型的”车身”——发动机一样,用途不同。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!