输入一段文本,Transformer把它变成一串向量——这个过程看似简单,背后却隐藏着一个精妙的信息加工流水线。每个Token在每个Transformer层都有一个对应的Hidden State(隐藏状态),这个向量不是静态的词嵌入,而是在网络的层层传递中不断被重塑、被丰富、被"理解"的动态表示。
理解Hidden State是什么、如何演变、不同层分别编码了什么信息,不仅能帮助开发者更好地使用预训练模型,也是理解大模型"涌现能力"的关键切口。
从静态词向量到动态Hidden State
在Transformer之前,词向量是静态的。Word2Vec和GloVe为每个词分配一个固定的向量,“bank"无论出现在"river bank"还是"bank account"中,其向量表示完全相同。这种静态表示无法处理一词多义,也无法捕捉词语在特定上下文中的细微语义变化。
graph LR
subgraph Static["静态词向量时代"]
A["bank"] --> B["固定向量 [0.1, 0.3, ...]"]
C["bank account"] --> B
D["river bank"] --> B
end
subgraph Dynamic["动态Hidden State时代"]
E["bank"] --> F["上下文: bank account"]
F --> G["向量A [0.2, 0.1, ...]"]
E --> H["上下文: river bank"]
H --> I["向量B [0.8, 0.4, ...]"]
end
Static -->|演进| Dynamic
ELMo在2018年首次提出了上下文化词表示的概念,但真正将这一范式发扬光大的是BERT和后续的Transformer模型。关键的变化在于:同一个Token,在不同的句子中,甚至在同一句话的不同位置,其Hidden State都是不同的。
从数学上看,给定一个包含 $n$ 个Token的输入序列,Transformer的第 $l$ 层会产生 $n$ 个Hidden State向量,每个向量的维度为 $d_{model}$:
$$H^{(l)} = \{h_1^{(l)}, h_2^{(l)}, \ldots, h_n^{(l)}\}$$其中 $h_i^{(l)} \in \mathbb{R}^{d_{model}}$ 表示第 $l$ 层第 $i$ 个Token的表示。第0层的Hidden State就是Token Embedding和Position Embedding的加和,而更高层的Hidden State则通过自注意力机制不断融入上下文信息。
Hidden State的层间演变:信息瓶颈视角
Elena Voita等人在EMNLP 2019发表的研究从一个独特的视角分析了Hidden State的演变——信息瓶颈(Information Bottleneck)。核心观点是:网络的每一层都在"挤压"与任务无关的信息,同时保留与任务相关的信息。
不同训练目标下,Hidden State的演变模式截然不同:
graph TB
subgraph LM["语言模型 LM"]
L1["Layer 0: Token身份完整"] --> L2["Layer 6: 开始遗忘"]
L2 --> L3["Layer 12: 聚焦预测"]
end
subgraph MLM["掩码语言模型 BERT"]
M1["Layer 0-4: 上下文编码"] --> M2["Layer 4-8: 信息融合"]
M2 --> M3["Layer 8-12: Token重建"]
end
subgraph MT["机器翻译 Encoder"]
T1["Layer 0: 源语言表示"] --> T2["Layer 6: 结构编码"]
T2 --> T3["Layer 12: 语义表示"]
T3 -.-> T4["Decoder使用"]
end
语言模型(LM) 采用自回归训练,每个位置只能看到之前的Token。研究发现,随着层数增加,LM会逐渐"忘记"当前Token的身份信息,同时积累关于下一个Token的信息。这是因为模型需要从当前的表示中预测未来,当前Token的身份变得不那么重要。
掩码语言模型(MLM) 采用BERT式的双向训练。Hidden State的演变呈现明显的两个阶段:前几层进行上下文编码,融入周围Token的信息;最后几层进行Token重建,恢复被掩码Token的身份。这种两阶段模式解释了为什么BERT的中间层表示往往在下游任务上表现更好——它们既获得了丰富的上下文信息,又没有被任务特定的预测目标"污染”。
机器翻译(MT) 模型的Hidden State变化相对平缓。因为编码器的表示不直接用于预测,而是作为解码器的输入,所以Token的身份信息被更好地保留。
graph LR
subgraph Layer0["第0层:Token Embedding"]
A1["Token ID"] --> B1["Position"]
B1 --> C1["静态表示"]
end
subgraph LayerLow["低层:局部信息"]
D1["词身份"] --> E1["词性"]
E1 --> F1["位置信息"]
end
subgraph LayerMid["中层:句法信息"]
G1["依存关系"] --> H1["短语结构"]
H1 --> I1["句法角色"]
end
subgraph LayerHigh["高层:语义信息"]
J1["语义角色"] --> K1["实体类型"]
K1 --> L1["共指消解"]
end
Layer0 --> LayerLow --> LayerMid --> LayerHigh
BERT重新发现NLP管道
Tenney等人在2019年发表的研究揭示了一个令人惊讶的现象:BERT的不同层以传统NLP管道的顺序编码不同类型的语言信息。
研究者使用Edge Probing方法,在BERT的每一层训练轻量级分类器来预测各种语言属性。结果发现:
- 第1-4层:词性标注(POS)信息最丰富。模型首先学会识别名词、动词、形容词等基本词性。
- 第4-8层:句法结构信息。依存关系、短语成分等句法信息在中层达到峰值。
- 第8-12层:语义信息。语义角色标注、共指消解等需要深层理解的任务在高层表现最好。
graph LR
subgraph Pipeline["传统NLP管道"]
P1["词性标注"] --> P2["句法分析"]
P2 --> P3["语义角色标注"]
P3 --> P4["共指消解"]
end
subgraph BERT["BERT层级编码"]
B1["Layer 1-4: POS"]
B2["Layer 4-8: Syntax"]
B3["Layer 8-12: Semantics"]
end
P1 -.->|对应| B1
P2 -.->|对应| B2
P3 -.->|对应| B3
P4 -.->|对应| B3
这不是巧合。从计算角度看,识别词性只需要局部上下文,而理解"谁对谁做了什么"则需要整合整个句子的信息。Transformer的层级结构天然支持这种从局部到全局的信息整合。
更精彩的是,研究者发现BERT可以动态调整这个"管道"。当高层语义信息有助于消歧时,模型会回过头修正低层的判断。例如,在没有上下文时,“Toronto"被标记为地名(GPE);但在"He smoked Toronto"这句话中,当模型理解了语义角色——Toronto是被"smoked"的对象——它会将实体类型修正为组织(ORG),指的是体育队伍。
sequenceDiagram
participant Token as 输入: He smoked Toronto
participant L1 as Layer 1-4
participant L2 as Layer 4-8
participant L3 as Layer 8-12
participant Output as 最终输出
Token->>L1: 局部处理
L1->>L2: 词性信息: He/PRP, smoked/VBD, Toronto/NNP
L2->>L3: 句法信息: smoked是谓语, Toronto是宾语
L3->>L3: 语义推理: 宾语应是组织而非地点
L3-->>L1: 反馈修正
L1->>Output: 实体类型: ORG 体育队伍
为什么中间层往往更有用?
很多实践者发现,使用BERT中间层的Hidden State而非最后一层,在特定任务上效果更好。这背后有几个原因:
信息保留与损失的权衡。最后一层的Hidden State已经被调整为适合预训练任务(掩码语言建模),一些对特定下游任务有用的信息可能被"挤压"掉了。中间层保留了大量语言学结构信息,这些信息对分类、序列标注等任务可能更有价值。
任务相关性。不同任务需要不同抽象级别的信息。命名实体识别可能更多依赖局部特征和词法信息,而文本蕴含则需要深层语义理解。选择合适的层级本质上是选择合适的信息抽象级别。
表示空间的几何特性。Zhou和Srikumar在ACL 2022的研究发现,Fine-tuning主要通过调整不同类别在表示空间中的距离来提升性能。对于已经线性可分的表示,Fine-tuning会推远不同类别的聚类中心;对于不可分的表示,Fine-tuning会尝试将同类样本聚拢。预训练模型的中间层往往已经形成了相对清晰的类别边界,为下游任务提供了良好的起点。
一个实用的技巧是层级混合(Scalar Mixing),ELMo论文中提出的方法:
$$\mathbf{h} = \gamma \sum_{l=0}^{L} s_l \mathbf{h}^{(l)}$$其中 $s_l$ 是可学习的权重,$\gamma$ 是缩放因子。这种方法允许下游任务自动学习应该从哪些层提取多少信息。
graph TB
subgraph Layers["BERT各层输出"]
H0["h⁰: Embedding层"]
H1["h¹: Layer 1"]
H2["h²: Layer 2"]
Hn["h¹²: Layer 12"]
end
subgraph Mixing["层级混合"]
S0["权重 s₀"]
S1["权重 s₁"]
S2["权重 s₂"]
Sn["权重 s₁₂"]
H0 --> S0
H1 --> S1
H2 --> S2
Hn --> Sn
S0 --> SUM["加权求和"]
S1 --> SUM
S2 --> SUM
Sn --> SUM
end
SUM --> GAMMA["缩放因子 γ"]
GAMMA --> OUTPUT["最终表示 h"]
如何提取和使用Hidden State
在实践中,有几种主流的Hidden State提取和使用策略:
CLS Token策略。BERT在句首添加特殊的[CLS]Token,其最终Hidden State被设计为整个序列的聚合表示。这种方法简单直接,适合句子级别的分类任务。但研究显示,[CLS]Token的表示质量受预训练目标影响——BERT的Next Sentence Prediction任务使其更适合句子对任务,而单句任务上可能不是最优选择。
平均池化策略。对所有Token的Hidden State取平均:
$$\mathbf{h}_{sentence} = \frac{1}{n}\sum_{i=1}^{n} \mathbf{h}_i$$这种方法可以捕捉更全面的序列信息,但也会混入padding Token的噪声。实践中常用掩码平均:
$$\mathbf{h}_{sentence} = \frac{\sum_{i=1}^{n} m_i \mathbf{h}_i}{\sum_{i=1}^{n} m_i}$$其中 $m_i$ 是掩码,区分真实Token和padding Token。
最大池化策略。对每个维度取所有Token中的最大值,适合需要捕捉最显著特征的任务,如关键词提取、情感分析等。
graph TB
subgraph Input["输入序列"]
T1["[CLS]"]
T2["The"]
T3["cat"]
T4["sat"]
T5["[PAD]"]
end
subgraph Hidden["Hidden States"]
H1["h₁"]
H2["h₂"]
H3["h₃"]
H4["h₄"]
H5["h₅"]
end
T1 --> H1
T2 --> H2
T3 --> H3
T4 --> H4
T5 --> H5
subgraph Pooling["池化策略"]
CLS["CLS: h₁"]
MEAN["Mean: h₁+h₂+h₃+h₄+h₅ / 5"]
MASK["Masked Mean: h₂+h₃+h₄ / 3"]
MAX["Max: max h₁,h₂,h₃,h₄,h₅"]
end
H1 --> CLS
H2 --> MEAN
H3 --> MEAN
H4 --> MEAN
H5 --> MEAN
H2 --> MASK
H3 --> MASK
H4 --> MASK
H1 --> MAX
H2 --> MAX
H3 --> MAX
H4 --> MAX
H5 --> MAX
层级选择策略。根据任务特性选择合适的层。Stanford的CS224N课程建议:词性标注等浅层任务用第4-8层,语义角色标注等深层任务用第8-12层。更精细的方法是验证集搜索或层级混合。
# 一个实用的Hidden State提取示例
import torch
from transformers import AutoModel, AutoTokenizer
def extract_hidden_states(model, tokenizer, text, layer_indices=None, pooling='cls'):
"""
提取指定层的Hidden State
Args:
layer_indices: 要提取的层,None表示最后一层,-1表示所有层
pooling: 'cls', 'mean', 'max' 或 'none'(返回所有token)
"""
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
# output_hidden_states=True 返回所有层的Hidden State
outputs = model(**inputs, output_hidden_states=True)
# hidden_states是一个tuple,包含embedding层和所有transformer层
all_hidden_states = outputs.hidden_states # (layer_count+1, batch, seq_len, hidden_size)
if layer_indices is None:
hidden_states = all_hidden_states[-1] # 最后一层
elif isinstance(layer_indices, int):
hidden_states = all_hidden_states[layer_indices]
else:
# 多层加权平均
hidden_states = torch.stack([all_hidden_states[i] for i in layer_indices]).mean(dim=0)
if pooling == 'cls':
return hidden_states[:, 0, :] # CLS token
elif pooling == 'mean':
# 排除padding token
attention_mask = inputs['attention_mask'].unsqueeze(-1).expand(hidden_states.size())
sum_hidden = torch.sum(hidden_states * attention_mask, dim=1)
return sum_hidden / attention_mask.sum(dim=1)
elif pooling == 'max':
return hidden_states.max(dim=1)[0]
else:
return hidden_states # 返回所有token的表示
Fine-tuning如何改变Hidden State
Fine-tuning是目前使用预训练模型的主流方式,但这个过程如何改变Hidden State的几何结构?
Zhou和Srikumar的研究提供了清晰的答案。他们使用DIRECT PROBE方法分析BERT在Fine-tuning前后的表示空间,发现:
Fine-tuning不会随意改变表示。即使在变化最剧烈的高层,Fine-tuned表示与原始表示的空间相似度(用Pearson相关系数衡量)仍然超过0.5。这意味着Fine-tuning在很大程度上保留了预训练学到的空间结构。
Fine-tuning通过调整类别距离来提升性能。具体而言:
- 如果原始表示已经线性可分,Fine-tuning会推远不同类别的聚类中心,扩大决策边界的安全边际
- 如果原始表示不可分,Fine-tuning会尝试将同类样本聚集到更少的聚类中
graph LR
subgraph Before["Fine-tuning前"]
A1["类别A"]
A2["类别A"]
B1["类别B"]
B2["类别B"]
end
subgraph After["Fine-tuning后"]
C1["类别A 聚类"]
D1["类别B 聚类"]
E["决策边界扩大"]
end
Before -->|"推远不同类别"| After
高层变化更大,但方向更有序。虽然高层参数的变化幅度更大,但标签聚类中心的移动方向比低层更加一致。低层的移动方向分散,而高层往往朝特定的方向调整。
graph TB
subgraph LowLayer["低层:变化小,方向分散"]
L1["标签A"]
L2["标签B"]
L3["标签C"]
L1 -.->|"随机方向"| L1a["新位置"]
L2 -.->|"随机方向"| L2a["新位置"]
L3 -.->|"随机方向"| L3a["新位置"]
end
subgraph HighLayer["高层:变化大,方向一致"]
H1["标签A"]
H2["标签B"]
H3["标签C"]
H1 ==>|"远离中心"| H1b["新位置"]
H2 ==>|"远离中心"| H2b["新位置"]
H3 ==>|"远离中心"| H3b["新位置"]
end
存在Fine-tuning损害性能的案例。研究发现在至少一个配置(BERT-small在PS-fxn任务上),Fine-tuning后性能反而下降。这个案例的特殊之处在于,训练集和测试集在Fine-tuning后的空间相似度最低(0.44),暗示过度的表示变化可能导致泛化能力下降。
Probing:解码Hidden State的秘密
如何知道Hidden State中编码了什么信息?Probing(探针)是回答这个问题的核心技术。
分类器探针是最常用的方法。在冻结模型参数的情况下,训练一个轻量级分类器从Hidden State预测某个语言属性。如果分类器表现好,说明该属性被编码在表示中。
但这种方法有缺陷:分类器可能"作弊”,利用表示中的随机噪声来拟合任务。Hewitt和Liang提出了控制任务来检测这个问题:设计一个与主任务复杂度相同但不应被表示支持的任务,如果探针在控制任务上也表现良好,说明探针本身太强而非表示真的包含该信息。
DIRECT PROBE提供了一种不需要训练分类器的几何分析方法。它通过聚类识别表示空间中的类别边界,可以测量:
- 聚类数量:反映表示的线性可分性
- 聚类间距离:反映类别间的区分度
- 空间相似度:反映两个表示空间的结构相似性
graph TB
subgraph ProbeTypes["Probing方法"]
Classifier["分类器探针"]
Control["控制任务"]
Direct["DIRECT PROBE"]
end
subgraph ClassifierFlow["分类器探针流程"]
C1["Hidden State h"] --> C2["线性/非线性分类器"]
C2 --> C3["预测语言属性"]
C3 --> C4["准确率高 → 编码了该信息"]
end
subgraph ControlFlow["控制任务检测"]
CT1["主任务准确率"] --> CT3["比较"]
CT2["控制任务准确率"] --> CT3
CT3 --> CT4["主任务>>控制任务 → 探针有效"]
end
subgraph DirectFlow["DIRECT PROBE流程"]
D1["表示空间"] --> D2["聚类分析"]
D2 --> D3["计算类别边界"]
D3 --> D4["测量聚类间距离"]
end
Classifier --> ClassifierFlow
Control --> ControlFlow
Direct --> DirectFlow
这些方法共同揭示了一个核心洞察:Hidden State不是随机的向量,而是结构化地编码了语言信息的压缩表示。理解这种结构,才能更好地利用它们。
面试与实践中的关键问题
Q:为什么BERT用CLS Token做句子表示?
这源于BERT的预训练设计。Next Sentence Prediction任务要求模型判断两个句子是否连续,[CLS]Token被训练为聚合整个输入序列的信息。但需要注意,[CLS]表示的质量受预训练目标影响——对于单句分类任务,平均池化往往更稳定。
Q:应该用哪一层的Hidden State?
没有标准答案,取决于任务特性:
- 词性标注、命名实体识别等局部任务:第4-8层
- 语义角色标注、文本分类等全局任务:第8-12层
- 不确定时,使用层级混合让模型自己学习
Q:Hidden State和Embedding有什么区别?
Embedding(词嵌入)是静态的查找表,每个Token ID对应一个固定的向量。Hidden State是动态的上下文化表示,同一Token在不同上下文中有不同的Hidden State。Embedding是Hidden State的起点(第0层),而真正的"理解"发生在后续的Transformer层中。
Q:Fine-tuning会"遗忘"预训练知识吗?
部分会,但比想象中少。研究表明,即使Fine-tuning后,高层表示仍与原始表示保持较高相似度。关键是在下游任务性能和预训练知识保留之间找到平衡——过度的Fine-tuning可能导致"灾难性遗忘"。
从Hidden State到理解大模型
Hidden State的研究不仅是学术兴趣,它为我们理解大模型的能力边界提供了窗口。当我们说模型"理解"语言时,实际上是在说它的Hidden State有效地编码了语言的各个层次——从词法到句法,从句法到语义。
这种理解是涌现的,而非被显式编程的。Transformer的自注意力机制让每个Token都能"看到"整个序列,而层级结构则让信息从局部逐步整合到全局。最终,一个768维(BERT-base)或更高维度的向量,承载了词语在特定上下文中的身份、角色、关系和意义。
对于开发者而言,理解Hidden State意味着更好地掌控模型:知道从哪一层提取特征、如何设计池化策略、Fine-tuning何时会损害性能。对于研究者而言,Hidden State是打开模型"黑盒"的钥匙,是可解释性研究的核心对象。
从静态词向量到动态Hidden State,从单向LSTM到双向Transformer,表示学习的历史就是一部让机器更好"理解"语言的历史。而Hidden State,正是这一历史进程中最核心的技术载体。
参考文献
- Voita, E., et al. (2019). The Bottom-up Evolution of Representations in the Transformer. EMNLP 2019.
- Tenney, I., et al. (2019). BERT Rediscovers the Classical NLP Pipeline. ACL 2019.
- Zhou, Y., & Srikumar, V. (2022). A Closer Look at How Fine-tuning Changes BERT. ACL 2022.
- Peters, M., et al. (2018). Deep Contextualized Word Representations. NAACL 2018.
- Devlin, J., et al. (2019). BERT: Pre-training of Deep Bidirectional Transformers. NAACL 2019.
- Hewitt, J., & Liang, P. (2019). Designing and Interpreting Probes with Control Tasks. EMNLP 2019.
- Jawahar, G., et al. (2019). What Does BERT Learn about the Structure of Language? ACL 2019.