11_bert_head_pooler_output

1741 字
9 分钟
11_bert_head_pooler_output
from transformers import BertTokenizer, BertModel
import torch

1. 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]])

outputslast_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),即 batch×seq_len×hidden_sizebatch \times seq\_len \times hidden\_size

token0 token1 ... token7
batch0 [768维] [768维] ... [768维] ← 只有 1 个 batch

第 2 层:[0] — 取第一个 batch#

outputs['last_hidden_state'][0] # shape: (8, 768)

去掉 batch 维度,剩下 8×7688 \times 768 的矩阵。


第 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 上面加的”头”不同:

graph TB subgraph BertModel E1[BERT Encoder<br/>12层 Transformer] --> LS[last_hidden_state<br/>每个 token 的 768 维向量] E1 --> PL[Pooler<br/>取 CLS → Linear → Tanh] PL --> PO[pooler_output<br/>1个 768 维句子向量] end subgraph BertForMaskedLM E2[BERT Encoder<br/>12层 Transformer<br/>与上面完全相同的权重] --> LS2[last_hidden_state] LS2 --> MLM[MLM Head<br/>Linear 768→30522 + Softmax] MLM --> PRED[每个位置的词表概率分布<br/>预测被 Mask 的词] end

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 HeadBertForMaskedLMlast_hidden_state每个 token 位置来预测被 Mask 的词,不使用 pooler_output

所以,我们目前探索的 pooler_output 只是 BERT 默认的一个”头”,BERT 还可以接不同的头来干不同的事——MLM head 只是其中一个例子,还有 BertForSequenceClassification(分类头)、BertForQuestionAnswering(问答头)等。

💡 可以把 BERT Encoder 想象成”发动机”,poolerMLM Head、分类头等是不同车型的”车身”——发动机一样,用途不同。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

11_bert_head_pooler_output
https://github.com/chunhuizhang/bilibili_vlogs/
作者
HAC
发布于
2026-06-25
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
HAC
观之非易,行且克难
Greetings
欢迎来到我的博客!这里主要分享我的学习笔记与兴趣爱好。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
5
标签
13
总字数
79,889
运行时长
0
最后活动
0 天前

文章目录