07_model_outputs

3281 字
16 分钟
07_model_outputs

本节我们详细地探讨 BERT 模型的 outputs.

import torch
from torch import nn
from transformers import BertModel, BertTokenizer
model_name = 'bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name, output_hidden_states=True)

1. input#

# 表示拼接
text = 'After stealing money from the bank vault, the bank robber was seen '\
'fishing on the Mississippi river bank.'
token_input = tokenizer(text, return_tensors='pt')
print(token_input)

输出为:

{'input_ids': tensor([[ 101, 2044, 11065, 2769, 2013, 1996, 2924, 11632, 1010, 1996,
2924, 27307, 2001, 2464, 5645, 2006, 1996, 5900, 2314, 2924,
1012, 102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 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, 1, 1, 1, 1, 1, 1, 1]])}
token_input['input_ids'], token_input['input_ids'].shape

输出为:

(tensor([[ 101, 2044, 11065, 2769, 2013, 1996, 2924, 11632, 1010, 1996,
2924, 27307, 2001, 2464, 5645, 2006, 1996, 5900, 2314, 2924,
1012, 102]]),
torch.Size([1, 22]))

可见 batch_size = 1(我们只有一个句子),序列长度 seq_length = 22(没有 truncatepadding).

2. model forward#

  • forward
    • embedding -> encoder -> pooler
model.eval()
with torch.no_grad():
outputs = model(**token_input)
outputs

输出为:

BaseModelOutputWithPoolingAndCrossAttentions(last_hidden_state=tensor([[[-0.4964, -0.1831, -0.5231, ..., -0.1902, 0.3738, 0.3964],
[-0.1323, -0.2762, -0.3495, ..., -0.4567, 0.3786, -0.1096],
[-0.3626, -0.4002, 0.0676, ..., -0.3207, -0.2709, -0.3004],
...,
[ 0.2961, -0.2856, -0.0382, ..., -0.6056, -0.5163, 0.2005],
[ 0.4878, -0.0909, -0.2358, ..., -0.0017, -0.5945, -0.2431],
[-0.2517, -0.3519, -0.4688, ..., 0.2500, 0.0336, -0.2627]]]), pooler_output=tensor([[-0.6031, -0.3342, -0.7174, 0.3347, 0.5145, -0.1722, 0.4502, 0.2768,
-0.3769, -0.9998, -0.3657, 0.7535, 0.9817, -0.0192, 0.7959, -0.3459,
-0.1338, -0.3026, 0.1097, 0.5836, 0.5736, 0.9999, 0.1798, 0.1845,
0.2250, 0.9109, -0.5653, 0.8616, 0.8994, 0.7423, -0.2525, 0.0394,
-0.9894, -0.1331, -0.7763, -0.9826, 0.2223, -0.6115, 0.1941, 0.0177,
-0.7634, 0.2312, 0.9999, -0.7000, 0.4623, -0.2202, -1.0000, 0.1908,
-0.8150, 0.6483, 0.5878, 0.8198, 0.1014, 0.3185, 0.3963, -0.3216,
-0.1701, 0.0588, -0.1544, -0.4987, -0.5284, 0.1228, -0.4823, -0.7788,
0.6954, 0.0891, -0.0855, -0.1500, 0.0390, -0.0760, 0.6154, 0.2662,
-0.0129, -0.7253, 0.1352, 0.2921, -0.5613, 1.0000, 0.1536, -0.9681,
0.7166, 0.2600, 0.4519, 0.5470, -0.2798, -1.0000, 0.3419, -0.2645,
-0.9863, 0.1263, 0.5249, -0.2000, 0.5980, 0.4752, -0.2355, -0.4808,
-0.3786, -0.7284, -0.0909, 0.0124, -0.0689, -0.2531, -0.1324, -0.2361,
0.1732, -0.3216, -0.0188, 0.2302, -0.3221, 0.4996, 0.4346, -0.1935,
0.2968, -0.9292, 0.5326, -0.3695, -0.9876, -0.4770, -0.9902, 0.6349,
-0.1863, -0.2612, 0.9123, -0.1930, 0.3110, 0.0803, -0.7598, -1.0000,
0.0292, -0.0628, -0.1086, -0.2135, -0.9671, -0.9521, 0.3334, 0.8675,
0.2254, 0.9995, -0.2908, 0.9420, 0.0336, -0.4542, 0.4123, -0.4455,
0.4558, -0.4442, -0.0685, 0.3043, 0.0575, 0.1843, -0.6577, -0.3121,
...
[-0.3626, -0.4002, 0.0676, ..., -0.3207, -0.2709, -0.3004],
...,
[ 0.2961, -0.2856, -0.0382, ..., -0.6056, -0.5163, 0.2005],
[ 0.4878, -0.0909, -0.2358, ..., -0.0017, -0.5945, -0.2431],
[-0.2517, -0.3519, -0.4688, ..., 0.2500, 0.0336, -0.2627]]])), past_key_values=None, attentions=None, cross_attentions=None)

这里就是我们把 token_input 丢给 BERT 模型进行前向传播。

其中 **token_input 是把字典展开为关键字参数,相当于:

# 不要执行这段代码
# token_input 是一个 dict,包含:
# {
# 'input_ids': tensor([[101, 2044, ..., 102]]), # shape: (1, 22)
# 'token_type_ids': tensor([[0, 0, ..., 0]]), # shape: (1, 22)
# 'attention_mask': tensor([[1, 1, ..., 1]]) # shape: (1, 22)
# }
#
# 所以 model(**token_input) 等价于:
outputs = model(
input_ids=tensor_input_ids,
token_type_ids=tensor_token_type_ids,
attention_mask=tensor_attention_mask
)

3. output#

  • len(outputs) == 3
  • outputs[0]
    • last_hidden_state, shape: batch_size * seq_len * hidden_size(1 * 22 * 768)
  • outputs[1]
    • pooler_output, shape: batch_size * hidden_size(1 * 768)
  • outputs[2](model.config.output_hidden_states == True)
    • type: tuple
    • one for the output of the embeddings(1), if the model has an embedding layer(12), + one for the output of each layer
      • (1 + 12) * (batch_size * seq_len * hidden_size) = 1 * 22 * 768
len(outputs) # 3
type(outputs[2]), len(outputs[2]) # (tuple, 13)
outputs[0]
outputs[2][-1]
outputs[0] == outputs[2][-1] # True

输出为:

tensor([[[-0.4964, -0.1831, -0.5231, ..., -0.1902, 0.3738, 0.3964],
[-0.1323, -0.2762, -0.3495, ..., -0.4567, 0.3786, -0.1096],
[-0.3626, -0.4002, 0.0676, ..., -0.3207, -0.2709, -0.3004],
...,
[ 0.2961, -0.2856, -0.0382, ..., -0.6056, -0.5163, 0.2005],
[ 0.4878, -0.0909, -0.2358, ..., -0.0017, -0.5945, -0.2431],
[-0.2517, -0.3519, -0.4688, ..., 0.2500, 0.0336, -0.2627]]])
tensor([[[-0.4964, -0.1831, -0.5231, ..., -0.1902, 0.3738, 0.3964],
[-0.1323, -0.2762, -0.3495, ..., -0.4567, 0.3786, -0.1096],
[-0.3626, -0.4002, 0.0676, ..., -0.3207, -0.2709, -0.3004],
...,
[ 0.2961, -0.2856, -0.0382, ..., -0.6056, -0.5163, 0.2005],
[ 0.4878, -0.0909, -0.2358, ..., -0.0017, -0.5945, -0.2431],
[-0.2517, -0.3519, -0.4688, ..., 0.2500, 0.0336, -0.2627]]])
tensor([[[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True],
...,
[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True]]])

可以看到二者的输出是相等的。因为 outputs[0] 就是最后一层隐藏层的状态,而 outputs[2][-1] 就是所有隐藏层状态的最后一个,二者当然相等。

outputs[1]
model.pooler(outputs[2][-1])
outputs[1] == model.pooler(outputs[2][-1]) # True

输出为:

tensor([[-0.6031, -0.3342, -0.7174, 0.3347, 0.5145, -0.1722, 0.4502, 0.2768,
-0.3769, -0.9998, -0.3657, 0.7535, 0.9817, -0.0192, 0.7959, -0.3459,
-0.1338, -0.3026, 0.1097, 0.5836, 0.5736, 0.9999, 0.1798, 0.1845,
0.2250, 0.9109, -0.5653, 0.8616, 0.8994, 0.7423, -0.2525, 0.0394,
-0.9894, -0.1331, -0.7763, -0.9826, 0.2223, -0.6115, 0.1941, 0.0177,
-0.7634, 0.2312, 0.9999, -0.7000, 0.4623, -0.2202, -1.0000, 0.1908,
-0.8150, 0.6483, 0.5878, 0.8198, 0.1014, 0.3185, 0.3963, -0.3216,
-0.1701, 0.0588, -0.1544, -0.4987, -0.5284, 0.1228, -0.4823, -0.7788,
0.6954, 0.0891, -0.0855, -0.1500, 0.0390, -0.0760, 0.6154, 0.2662,
-0.0129, -0.7253, 0.1352, 0.2921, -0.5613, 1.0000, 0.1536, -0.9681,
0.7166, 0.2600, 0.4519, 0.5470, -0.2798, -1.0000, 0.3419, -0.2645,
-0.9863, 0.1263, 0.5249, -0.2000, 0.5980, 0.4752, -0.2355, -0.4808,
-0.3786, -0.7284, -0.0909, 0.0124, -0.0689, -0.2531, -0.1324, -0.2361,
0.1732, -0.3216, -0.0188, 0.2302, -0.3221, 0.4996, 0.4346, -0.1935,
0.2968, -0.9292, 0.5326, -0.3695, -0.9876, -0.4770, -0.9902, 0.6349,
-0.1863, -0.2612, 0.9123, -0.1930, 0.3110, 0.0803, -0.7598, -1.0000,
0.0292, -0.0628, -0.1086, -0.2135, -0.9671, -0.9521, 0.3334, 0.8675,
0.2254, 0.9995, -0.2908, 0.9420, 0.0336, -0.4542, 0.4123, -0.4455,
0.4558, -0.4442, -0.0685, 0.3043, 0.0575, 0.1843, -0.6577, -0.3121,
-0.1069, -0.7729, -0.2328, 0.9032, -0.4681, -0.5580, 0.3359, -0.1644,
-0.1572, 0.6441, 0.2810, 0.2651, 0.1532, 0.4485, -0.5299, 0.2616,
-0.7412, -0.0347, 0.2560, -0.2659, -0.6092, -0.9868, -0.2346, 0.4682,
0.9771, 0.5646, 0.2360, 0.4567, -0.2170, 0.1420, -0.9554, 0.9832,
-0.0935, 0.2621, -0.7901, 0.5758, -0.7642, -0.5362, 0.6374, -0.3891,
-0.6368, -0.0045, -0.2658, -0.1721, -0.7466, 0.4832, -0.3128, -0.2640,
...
-0.0989, -0.3853, -0.3173, -0.5348, 0.5307, -0.6883, -0.3786, -0.4665,
0.7207, 0.3239, 0.9999, -0.5526, -0.4825, -0.3439, -0.3580, 0.1069,
-0.2620, -1.0000, 0.2673, -0.3037, 0.4706, -0.6132, 0.8511, -0.4910,
-0.7231, -0.1092, 0.5412, 0.5265, -0.4635, -0.2461, 0.4946, -0.3469,
0.9149, 0.5902, -0.1862, 0.7004, 0.5465, -0.2839, -0.5723, 0.6648]])
tensor([[-0.6031, -0.3342, -0.7174, 0.3347, 0.5145, -0.1722, 0.4502, 0.2768,
-0.3769, -0.9998, -0.3657, 0.7535, 0.9817, -0.0192, 0.7959, -0.3459,
-0.1338, -0.3026, 0.1097, 0.5836, 0.5736, 0.9999, 0.1798, 0.1845,
0.2250, 0.9109, -0.5653, 0.8616, 0.8994, 0.7423, -0.2525, 0.0394,
-0.9894, -0.1331, -0.7763, -0.9826, 0.2223, -0.6115, 0.1941, 0.0177,
-0.7634, 0.2312, 0.9999, -0.7000, 0.4623, -0.2202, -1.0000, 0.1908,
-0.8150, 0.6483, 0.5878, 0.8198, 0.1014, 0.3185, 0.3963, -0.3216,
-0.1701, 0.0588, -0.1544, -0.4987, -0.5284, 0.1228, -0.4823, -0.7788,
0.6954, 0.0891, -0.0855, -0.1500, 0.0390, -0.0760, 0.6154, 0.2662,
-0.0129, -0.7253, 0.1352, 0.2921, -0.5613, 1.0000, 0.1536, -0.9681,
0.7166, 0.2600, 0.4519, 0.5470, -0.2798, -1.0000, 0.3419, -0.2645,
-0.9863, 0.1263, 0.5249, -0.2000, 0.5980, 0.4752, -0.2355, -0.4808,
-0.3786, -0.7284, -0.0909, 0.0124, -0.0689, -0.2531, -0.1324, -0.2361,
0.1732, -0.3216, -0.0188, 0.2302, -0.3221, 0.4996, 0.4346, -0.1935,
0.2968, -0.9292, 0.5326, -0.3695, -0.9876, -0.4770, -0.9902, 0.6349,
-0.1863, -0.2612, 0.9123, -0.1930, 0.3110, 0.0803, -0.7598, -1.0000,
0.0292, -0.0628, -0.1086, -0.2135, -0.9671, -0.9521, 0.3334, 0.8675,
0.2254, 0.9995, -0.2908, 0.9420, 0.0336, -0.4542, 0.4123, -0.4455,
0.4558, -0.4442, -0.0685, 0.3043, 0.0575, 0.1843, -0.6577, -0.3121,
-0.1069, -0.7729, -0.2328, 0.9032, -0.4681, -0.5580, 0.3359, -0.1644,
-0.1572, 0.6441, 0.2810, 0.2651, 0.1532, 0.4485, -0.5299, 0.2616,
-0.7412, -0.0347, 0.2560, -0.2659, -0.6092, -0.9868, -0.2346, 0.4682,
0.9771, 0.5646, 0.2360, 0.4567, -0.2170, 0.1420, -0.9554, 0.9832,
-0.0935, 0.2621, -0.7901, 0.5758, -0.7642, -0.5362, 0.6374, -0.3891,
-0.6368, -0.0045, -0.2658, -0.1721, -0.7466, 0.4832, -0.3128, -0.2640,
...
-0.0989, -0.3853, -0.3173, -0.5348, 0.5307, -0.6883, -0.3786, -0.4665,
0.7207, 0.3239, 0.9999, -0.5526, -0.4825, -0.3439, -0.3580, 0.1069,
-0.2620, -1.0000, 0.2673, -0.3037, 0.4706, -0.6132, 0.8511, -0.4910,
-0.7231, -0.1092, 0.5412, 0.5265, -0.4635, -0.2461, 0.4946, -0.3469,
0.9149, 0.5902, -0.1862, 0.7004, 0.5465, -0.2839, -0.5723, 0.6648]])
tensor([[True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
...
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True, True, True, True]])

可以看到,outputs[1] 实际上就是 outputs[0][CLS] 的那个 1 * 768 维的向量,再经过一个 全连接层 + tanh 激活函数得到的东西。

但对于 model.pooler(outputs[2][-1])outputs[2][-1] 似乎拿出的是最后一个隐藏层的状态,而 outputs[1] 是最后一个隐藏层状态的第一个 token [CLS] 经过激活函数后的输出。那么,model.pooler(outputs[2][-1]) 不应该是对最后一个隐藏层的所有 tokens 都做激活函数处理吗?为什么他会与只取第一个 token 的 outputs[1] 相等?

的确,如果 pooler 真的对全部 tokens 都做激活,那么输出形状应该是 (1, 22, 768),不可能和 (1, 768) 的 outputs[1] 相等

关键的秘密在于:model.pooler 内部其实只取了第一个 token([CLS],而不是对所有 tokens 做处理。

我们来看看 HuggingFace 的源码:

class BertPooler(nn.Module):
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.hidden_size)
self.activation = nn.Tanh()
def forward(self, hidden_states):
# ⭐ 关键在这里:只取第一个 token ([CLS])!
first_token_tensor = hidden_states[:, 0]
pooled_output = self.dense(first_token_tensor)
pooled_output = self.activation(pooled_output)
return pooled_output

看到 hidden_states[:, 0] 了吗?Pooler 在 forward 的第一步就做了切片——从 (1, 22, 768) 中只取出 [CLS] 对应的 (1, 768)剩下的 21 个 token 直接丢弃了

所以整个过程是:

outputs[2][-1] shape: (1, 22, 768)
│ pooler.forward() 内部:
│ hidden_states[:, 0] ← 只取 [CLS]
│ ↓
│ shape: (1, 768)
│ ↓
│ nn.Linear(768→768)
│ ↓
│ nn.Tanh()
│ ↓
pooler_output shape: (1, 768) ✅ 与 outputs[1] 一致

outputs[1] 本质上就是 BERT 模型内部调用了完全相同的这个 model.pooler(outputs[0]) 计算出来的,所以二者必然相等。

总结:Pooler 不是”对所有 tokens 做激活”,而是”只取 [CLS] 的向量,然后做全连接+tanh”。它的设计目的就是为句子级别的分类任务压缩出一个固定长度的句子表示向量。

outputs[2][0]
model.embeddings(token_input['input_ids'], token_input['token_type_ids'])
outputs[2][0] == model.embeddings(token_input['input_ids'], token_input['token_type_ids']) # True

输出为:

tensor([[[ 0.1686, -0.2858, -0.3261, ..., -0.0276, 0.0383, 0.1640],
[ 0.2329, 0.1390, 0.2979, ..., -0.0655, 0.8885, 0.5109],
[ 0.2257, -0.7165, -0.7255, ..., 0.4844, 0.6030, -0.0957],
...,
[-0.0374, -0.6155, -1.4419, ..., 0.0793, -0.0811, -0.3802],
[-0.0228, 0.4207, -0.3288, ..., 0.4464, 0.5178, 0.5501],
[-0.2350, 0.1566, -0.0462, ..., -0.4206, 0.3074, -0.2288]]])
tensor([[[ 0.1686, -0.2858, -0.3261, ..., -0.0276, 0.0383, 0.1640],
[ 0.2329, 0.1390, 0.2979, ..., -0.0655, 0.8885, 0.5109],
[ 0.2257, -0.7165, -0.7255, ..., 0.4844, 0.6030, -0.0957],
...,
[-0.0374, -0.6155, -1.4419, ..., 0.0793, -0.0811, -0.3802],
[-0.0228, 0.4207, -0.3288, ..., 0.4464, 0.5178, 0.5501],
[-0.2350, 0.1566, -0.0462, ..., -0.4206, 0.3074, -0.2288]]],
grad_fn=<NativeLayerNormBackward0>)
tensor([[[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True],
...,
[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True],
[True, True, True, ..., True, True, True]]])

可见,outputs[2][0] 实际上是所有隐藏层状态中的第一个,也就是 embedding 层,它的后面才是 12 个 transformer 层。

自然,embedding 层等于 model.embeddings(token_input['input_ids'], token_input['token_type_ids']). 注意这里的 model.embeddings() 不接受 attention_mask 参数,所以我们只传入 input_idstoken_type_ids.

for i in range(len(outputs[2])):
print(i, outputs[2][i].shape)

输出为:

0 torch.Size([1, 22, 768])
1 torch.Size([1, 22, 768])
2 torch.Size([1, 22, 768])
3 torch.Size([1, 22, 768])
4 torch.Size([1, 22, 768])
5 torch.Size([1, 22, 768])
6 torch.Size([1, 22, 768])
7 torch.Size([1, 22, 768])
8 torch.Size([1, 22, 768])
9 torch.Size([1, 22, 768])
10 torch.Size([1, 22, 768])
11 torch.Size([1, 22, 768])
12 torch.Size([1, 22, 768])

这里我们就能很清晰地看到所有隐藏层的形状了。

最后,详细解释如下。

3.1 为什么 len(outputs) == 3#

outputs 是 HuggFace 的 BaseModelOutputWithPoolingAndCrossAttentions 对象,它的设计类似一个命名元组,支持两种访问方式:

索引属性名内容shape
outputs[0]outputs.last_hidden_state最后一层编码器输出1 × 22 × 768
outputs[1]outputs.pooler_output池化后的 [CLS] 向量1 × 768
outputs[2]outputs.hidden_states所有层的 hidden states(元组)13个 1×22×768

这就像你有一个文件夹

outputs/ ← len(outputs) = 3(顶层文件夹数)
├── last_hidden_state/ ← outputs[0],里面有 1×22×768 个数字
├── pooler_output/ ← outputs[1],里面有 1×768 个数字
└── hidden_states/ ← outputs[2],里面还有 13 个子文件夹
├── layer_0 (embedding)
├── layer_1
├── ...
└── layer_12

len() 只统计顶层有几个”文件夹”,而不是递归数里面所有的”文件”。 所以返回 3 是对的 ✅

3.2 三个输出分别是什么?#

last_hidden_state(最后一层隐藏状态)#

shape: (batch_size=1, seq_len=22, hidden_size=768)

含义: 句子中每个 token 经过 BERT 全部 12 层 transformer 编码后的最终向量表示。

  • 每一行对应一个 token([CLS]afterstealing → … → bank[SEP]
  • 每个 token 被编码为一个 768 维的稠密向量
  • 这 22 个向量各自包含了上下文信息——比如句子中两个 bank 虽然 token 相同,但它们在第 10 行和第 20 行的向量是不一样的,因为上下文不同
例如:token "bank" 出现了 3 次
position 6: "the bank vault" → 银行(金融机构)
position 10: "the bank robber" → 银行(金融机构)
position 19: "river bank" → 河岸(地理位置)
→ 这三个 bank 的 768 维向量是不同的!因为 BERT 编码了上下文

用途: 这是最通用的特征,可以用来做序列标注(命名实体识别、词性标注)或直接取某个位置的向量做下游任务。


pooler_output(池化输出)#

shape: (batch_size=1, hidden_size=768)

含义: 专门为句子级别的分类任务设计的输出。

它的计算方式:

  1. 先取出 last_hidden_state 中第一个 token([CLS])的向量
  2. 再经过一个 全连接层 + tanh 激活函数
last_hidden_state[0, 0, :] → [CLS] 的 768 维向量
全连接层 (768 → 768)
tanh 激活
pooler_output (768 维)

为什么要有 pooler_output

BERT 在预训练时,[CLS] token 被设计用来聚合整个句子的信息(做下一句预测任务)。所以 pooler_output 是对整个句子的压缩表示

用途: 文本分类、情感分析、句子对匹配等句子级别的任务。


hidden_states(所有隐藏层状态)#

type: tuple of 13 tensors
每个 tensor shape: (batch_size=1, seq_len=22, hidden_size=768)

含义: BERT 每一层的输出都被保存下来了,包括第 0 层(embedding 层)到第 12 层(最后一层 transformer)。

hidden_states[0] → embedding 层的输出(还没经过 transformer)
hidden_states[1] → 第 1 层 transformer 的输出
hidden_states[2] → 第 2 层 transformer 的输出
...
hidden_states[12] → 第 12 层 transformer 的输出 = last_hidden_state 🎯

为什么需要看每一层?

不同层的编码器学到了不同层次的语言特征

层次学到了什么类比
底层(1-4)词法、语法特征像学语言时先认单词、学词性
中层(5-8)句法、语义特征理解短语结构、主谓关系
高层(9-12)上下文语义、抽象概念理解整体意思、指代关系

用途: 某些任务中拼接多层的特征比只用最后一层效果更好(比如命名实体识别中拼最后 4 层)。这也是为什么需要设置 output_hidden_states=True


一张图总结#

输入句子: "After stealing money from the bank vault..."
Tokenization
┌──────────────────┐
│ Embedding 层 │ ← hidden_states[0]
└────────┬─────────┘
┌──────────────────┐
│ Transformer 1 │ ← hidden_states[1]
└────────┬─────────┘
...
┌──────────────────┐
│ Transformer 12 │ ← hidden_states[12] = last_hidden_state
└────────┬─────────┘
取 [CLS] 位置
+ 全连接层 + tanh
pooler_output

文章分享

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

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

评论区

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

音乐

暂未播放

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

文章目录