共计 2559 个字符,预计需要花费 7 分钟才能阅读完成。
CLS Token 的起源与设计初衷
CLS(Classification)Token 最早出现在 Google 的 BERT 论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》中。论文中明确指出,CLS Token 是一个在输入序列开头添加的特殊标记,其对应的最终隐藏状态被用作分类任务的聚合序列表示。

- 原始论文设计要点:
- 位置固定:始终位于输入序列的第一个位置
- 无实际语义:与常规单词标记不同,不携带具体文本含义
-
全局信息聚合:通过自注意力机制收集整个序列的上下文信息
-
设计哲学:
- 为下游任务提供统一的接口
- 避免对特定位置特征的依赖(相比传统 Pooling 方式)
- 使模型能够学习任务相关的聚合方式
BERT 中的 CLS Token 工作机制
在 BERT 等 Transformer 架构中,CLS Token 通过以下方式发挥作用:
- 输入处理阶段:
- 在 tokenization 时自动添加 [CLS] 标记
- 与其他 token 一同参与 embedding 计算
-
获得独立的位置编码(position 0)
-
注意力机制中的表现:
- 作为 query 向量参与所有层的注意力计算
- 通过多头注意力收集全局上下文信息
-
各层 Transformer 逐步提炼序列的抽象表示
-
输出特征特性:
- 最后一层 CLS 位置的 hidden state 作为序列表示
- 包含经过深度双向编码的上下文信息
- 特别适合作为分类器的输入特征
不同任务中的使用差异
分类任务(推荐使用 CLS)
- 文本分类标准流程:
- 取最后一层 CLS 位置的 hidden state
- 接全连接层 +softmax 进行分类
-
示例代码片段:
outputs = model(input_ids) cls_embedding = outputs.last_hidden_state[:, 0, :] # 取 CLS 位置 logits = classifier(cls_embedding) -
优势体现:
- 避免手动设计 pooling 策略
- 模型可学习最优的信息聚合方式
- 在 GLUE 基准测试中表现优异
序列标注任务(通常不使用 CLS)
- 典型处理方式:
- 使用每个 token 对应的输出(忽略 CLS)
-
示例 NER 任务代码:
outputs = model(input_ids) token_embeddings = outputs.last_hidden_state[:, 1:, :] # 跳过 CLS -
原因分析:
- 需要 token 级别的细粒度预测
- CLS 的全局特性反而可能引入噪声
- 原始论文未推荐在此类任务中使用
PyTorch 完整实现示例
下面展示一个完整的文本分类实现,包含 CLS Token 的正确处理:
import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer
class BertClassifier(nn.Module):
def __init__(self, num_classes):
super().__init__()
self.bert = BertModel.from_pretrained('bert-base-uncased')
self.dropout = nn.Dropout(0.1)
self.classifier = nn.Linear(768, num_classes) # BERT hidden_size=768
def forward(self, input_ids, attention_mask):
outputs = self.bert(
input_ids=input_ids,
attention_mask=attention_mask
)
# 关键步骤:正确提取 CLS 向量
pooled_output = outputs.last_hidden_state[:, 0, :] # 取第一个位置
pooled_output = self.dropout(pooled_output)
return self.classifier(pooled_output)
# 使用示例
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertClassifier(num_classes=2)
# 模拟输入处理
texts = ["This is a positive example", "Negative sentence here"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# 前向传播
with torch.no_grad():
logits = model(inputs['input_ids'], inputs['attention_mask'])
predictions = torch.argmax(logits, dim=1)
print(f"Predictions: {predictions}")
常见误用场景及解决方案
- 错误 1:错误索引 CLS 位置
- 错误做法:
outputs[0][:, 1, :](错取第二个 token) -
修正方案:始终使用
[:, 0, :]索引 -
错误 2:忽略 attention_mask
- 错误影响:可能将 padding 部分纳入计算
-
正确实践:始终传入正确的 attention_mask
-
错误 3:错误微调策略
- 常见问题:冻结所有 BERT 参数
- 建议方案:至少解冻最后 3 层 + 分类头
性能优化建议
- 注意力调整技巧:
- 使用
BertConfig修改 attention_probs_dropout_prob -
实验表明 0.1-0.2 的 dropout 效果较佳
-
表示增强方法:
- 尝试拼接最后 4 层 CLS 状态
-
公式:
h = [h₁; h₂; h₃; h₄] -
学习率设置:
- 分类头使用较大 lr(如 5e-5)
- BERT 主体使用较小 lr(如 2e-5)
动手实践建议
建议读者在自己的数据集上尝试以下对比实验:
- 基准测试:
- 标准 CLS 用法(如前述代码)
-
记录准确率 /F1 值
-
对比方案:
- 使用 mean-pooling 代替 CLS
- 取中间层 CLS 表示
-
拼接多层 CLS 特征
-
分析方向:
- 不同方案的计算效率
- 在小数据集的过拟合情况
- 长文本处理效果差异
通过实际对比,可以更深入理解 CLS Token 的设计价值。根据我们的经验,在大多数分类任务中,标准 CLS 用法既能保证效果又实现简单,是值得优先尝试的方案。
正文完
发表至: 未分类
近两天内
