共计 2728 个字符,预计需要花费 7 分钟才能阅读完成。
1. CLS Token 的起源与设计初衷
CLS Token(Classification Token)最早出现在 Google 的 BERT 论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》中。论文中明确提到:

“The first token of every sequence is always a special classification token ([CLS]). The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks.”
原始设计意图可以总结为三点:
- 作为整个序列的聚合表示(特别是当序列没有明显边界时)
- 为下游分类任务提供统一的特征抽取入口
- 避免直接使用所有 token 的均值 / 最大值池化可能带来的信息损失
2. BERT 中的工作机制详解
在 BERT 的输入层,CLS Token 有以下关键特性:
- 位置固定:始终作为序列的第一个 token
- 特殊嵌入:拥有独立的 token embedding(与常规单词不同的 ID)
- 全局感知:通过自注意力机制与所有其他 token 交互
实际前向传播时,CLS Token 的隐藏层状态形成过程:
# 简化版 Transformer 计算流程
hidden_states = input_embeddings + position_embeddings
for layer in transformer_layers:
# 自注意力阶段 - CLS 与所有 token 交互
attention_output = self_attention(hidden_states)
# 前馈网络
hidden_states = feed_forward(attention_output)
# 最终取第一个位置的输出作为 CLS 表示
cls_representation = hidden_states[:, 0, :]
3. 不同任务中的使用差异
分类任务(文本分类、情感分析等)
- 直接使用最后一层 CLS Token 的 768 维向量
- 添加简单的全连接层即可获得分类结果
- 典型结构示例:
class BertForClassification(nn.Module):
def __init__(self, bert_model, num_labels):
super().__init__()
self.bert = bert_model
self.classifier = nn.Linear(768, num_labels)
def forward(self, input_ids, attention_mask):
outputs = self.bert(input_ids, attention_mask)
cls_output = outputs.last_hidden_state[:, 0, :] # 取 CLS 位置
return self.classifier(cls_output)
序列标注任务(NER、词性标注等)
- 通常忽略 CLS Token 的输出
- 使用其他 token 的隐藏状态作为词级别特征
- 需要特别注意处理 subword 带来的对齐问题
4. PyTorch 完整实现示例
以下代码展示如何正确提取和使用 CLS Token:
from transformers import BertModel, BertTokenizer
import torch
# 初始化模型和分词器
model = BertModel.from_pretrained('bert-base-uncased')
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
# 示例文本处理
text = "This is a sample sentence for CLS demonstration."
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
# 前向传播
with torch.no_grad():
outputs = model(**inputs)
# 获取 CLS 表示的三种等效方式
cls_rep1 = outputs.last_hidden_state[:, 0, :] # 直接索引
cls_rep2 = outputs.pooler_output # 官方提供的池化输出
cls_rep3 = outputs[0][:, 0, :] # 元组访问方式
# 验证一致性
print(torch.allclose(cls_rep1, cls_rep2)) # 可能为 False,因 pooler 有额外线性层
print(torch.allclose(cls_rep1, cls_rep3)) # 应为 True
5. 常见误用及解决方案
误用场景 1:错误的位置索引
- 错误做法:直接取
outputs[0]认为就是 CLS 向量 - 正确做法:明确指定
[:, 0, :]维度
误用场景 2:忽略注意力掩码
# 错误示范(未考虑 padding 的影响)outputs = model(input_ids)
# 正确做法
outputs = model(input_ids, attention_mask=attention_mask)
误用场景 3:不必要的复杂处理
- 不需要手动对 CLS 输出做 LayerNorm 或 Dropout
- BERT 输出层已经包含必要的规范化处理
6. 性能优化建议
- 注意力头调整:
- 通过
config.num_attention_heads减少头数可提升速度 -
但需保持 hidden_size 能被 num_heads 整除
-
表示增强技巧:
- 尝试拼接最后 4 层的 CLS 输出(类似 ELMo 思路)
- 代码示例:
# 获取所有层的 CLS 表示
all_layer_cls = [layer[:, 0, :] for layer in outputs.hidden_states[-4:]]
enhanced_cls = torch.cat(all_layer_cls, dim=-1)
- Fine-tuning 策略:
- 分类任务:优先调大学习率(比基础学习率大 3 - 5 倍)
- 小样本场景:冻结 BERT 底层参数
动手实践建议
推荐进行以下对比实验:
- 基础实验:
-
在相同数据集上比较
pooler_outputvslast_hidden_state[:,0,:]的效果差异 -
进阶实验:
- 尝试用 CLS 输出作为特征训练 SVM,与端到端微调对比
-
分析不同层 CLS 表示的分类效果差异
-
可视化分析:
- 使用 t -SNE 绘制不同类别样本的 CLS 向量分布
- 观察模型是否学习到有区分度的表示
通过以上实践,可以更直观地理解 CLS Token 的实际效果。建议记录不同处理方式在验证集上的准确率变化,这将帮助您深入掌握这一重要设计。
正文完
发表至: 自然语言处理
近一天内
