Howwise RSS
AI & Automation6 min read

BERT CLS Token Purpose: Why Position 0 Summarises Text

Short answer

The BERT CLS token purpose is to give the model a neutral slot at position 0 that attends to every token and ends up holding a sequence-level summary, which a classification head then reads. Its position does not limit what it sees.

Key takeaways
  • [CLS] is prepended to every input (id 101 in bert-base-uncased), but bidirectional self-attention lets it see all tokens at every layer.
  • Because [CLS] has no word of its own, its final vector is free to aggregate sentence-level information instead of encoding one token.
  • Classification uses last_hidden_state[:, 0, :] (shape [batch, 768]); BertForSequenceClassification feeds pooler_output into dropout and a linear layer.
  • Raw [CLS] from a non-fine-tuned model is a weak sentence embedding; mean pooling with the attention mask is usually better for frozen similarity work.
  • Decoder-only models classify from the last token, so never assume index 0 for every model or padding side.

What is the CLS token in BERT?

[CLS] (short for classification) is a special token that the BERT tokenizer prepends to every input. In bert-base-uncased it has id 101 and always sits at position 0. [SEP] (id 102) marks the end of a sentence, or separates sentence A from sentence B in a pair.

A single sentence becomes `[CLS] tokens [SEP]`. A pair becomes `[CLS] A tokens [SEP] B tokens [SEP]`, with token type ids 0 for the first segment and 1 for the second.

The token exists purely as a placeholder. Its embedding is a learned vector that is identical for every input before any attention happens, so it carries no information about your text at the input layer.

from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained('bert-base-uncased')
enc = tok('I loved this film', 'It was great')
print(enc['input_ids'])
# [101, 1045, 3866, 2023, 2143, 102, 2009, 2001, 2307, 102]
print(tok.convert_ids_to_tokens(enc['input_ids']))
print(enc['token_type_ids'])

If CLS is at the start, how does it know about the rest of the sentence?

Position is irrelevant to what a token can see in BERT. The encoder uses full bidirectional self-attention with no causal mask, so every token attends to every other token (except padding, which the attention mask blocks).

The walkthrough looks like this:

  1. Layer 0: [CLS] is just the same learned embedding plus a position embedding for slot 0. It knows nothing about the text.
  2. Layer 1: attention lets [CLS] take a weighted mix of the value vectors of all tokens, and all other tokens also take a weighted mix that includes [CLS].
  3. Layers 2 to 12: each layer re-mixes already-contextualised vectors, so information propagates further and is combined non-linearly through the feed-forward blocks.
  4. Final layer: the 768-dim hidden state of [CLS] (bert-base) is a context-dependent blend of the whole sequence.
Tip: Contrast this with a causal decoder like GPT, where token 0 can only see itself. There, a first-position summary token would be useless, which is why decoder models use the last token.

Why does BERT use the CLS token for classification?

A normal token's output vector mostly encodes that word in its context, since its residual stream started from that word's embedding. Pooling from one real word would bias the summary toward it.

[CLS] has no lexical identity, so it is a neutral slot. Nothing pulls it toward a particular word, so training can shape it into an aggregator of sequence-level information.

Pretraining gave it that job. In Next Sentence Prediction (NSP), a classifier on the final [CLS] vector predicted whether sentence B actually followed sentence A, forcing [CLS] to hold sequence-level signals.

NSP is not essential, though. RoBERTa dropped NSP and still classifies well using its `<s>` token (the equivalent of [CLS]), because fine-tuning teaches the slot what it needs.

How does fine-tuning use the CLS vector, and what tensor shapes are involved?

The encoder output is a tensor of shape [batch, seq_len, hidden], for bert-base [batch, seq_len, 768]. Think of it as a cube. Selecting [CLS] is a slice at index 0 along the sequence axis: `[:, 0, :]`, giving [batch, 768].

For classification, a dropout plus a linear layer (768 to num_labels) is applied to that slice. The cross-entropy loss backpropagates through the head and all 12 layers, so [CLS] learns task-specific summarisation. The other token vectors are discarded for classification, but token-level tasks such as NER and QA use them directly.

This is why raw [CLS] from a model that was never fine-tuned is a mediocre sentence embedding: nothing ever trained it to be comparable under cosine similarity.

How do I get the CLS embedding from BERT in Hugging Face?

The snippet below shows the three common representations side by side. The mean pooling uses the attention mask so padding tokens do not dilute the average.

import torch
from transformers import AutoTokenizer, AutoModel

name = 'bert-base-uncased'
tok = AutoTokenizer.from_pretrained(name)
model = AutoModel.from_pretrained(name).eval()

texts = ['A short sentence.', 'A noticeably longer sentence with more tokens in it.']
enc = tok(texts, padding=True, truncation=True, return_tensors='pt')  # add_special_tokens=True by default

with torch.no_grad():
    out = model(**enc)

h = out.last_hidden_state            # [batch, seq_len, 768]
cls = h[:, 0, :]                     # [batch, 768] raw [CLS]
pooled = out.pooler_output           # [batch, 768] dense + tanh over [CLS]

mask = enc['attention_mask'].unsqueeze(-1).float()   # [batch, seq_len, 1]
mean = (h * mask).sum(1) / mask.sum(1).clamp(min=1e-9)  # [batch, 768]

print(cls.shape, pooled.shape, mean.shape)

BERT pooler_output vs last_hidden_state CLS: what is the difference?

`last_hidden_state[:, 0, :]` is the raw final-layer vector. `pooler_output` passes that vector through an extra dense layer (768 to 768) and tanh. That pooler was trained during NSP.

`BertForSequenceClassification` uses `pooler_output`, then dropout, then a linear classifier. Models such as DistilBERT keep [CLS] but have no pooler and no NSP, so their classification heads work from the raw hidden state with their own added layers.

Note that if you load a checkpoint that lacks pooler weights, the pooler is randomly initialised, so its output is not meaningful until fine-tuned.

BERT CLS token vs mean pooling: which should I use?

Mean pooling averages all non-padding token vectors, so every token contributes. For frozen embeddings and semantic similarity it usually beats raw [CLS]. Sentence-BERT uses mean pooling by default, and sometimes max pooling. When you fine-tune end to end, the gap is usually small.

Choose using the table below.

SituationUseWhy
Fine-tuning for classification[CLS] (or pooler_output) plus linear headLoss shapes [CLS] into a task summary
Frozen BERT, similarity or clusteringMean pooling with attention maskRaw [CLS] was never trained for cosine geometry
Semantic search or retrievalPurpose-trained embedding modelTrained contrastively for this exact use
NER, QA, token taggingPer-token last_hidden_stateEach token needs its own vector
Decoder-only model classificationLast non-pad tokenCausal attention: only the last token sees everything

What mistakes do people make with the CLS token?

These are the pitfalls most tutorials skip:

MistakeEffectFix
Cosine similarity on raw [CLS] without fine-tuningPoor, noisy similarity scoresUse mean pooling or a sentence-embedding model
add_special_tokens=FalseNo [CLS] or [SEP]; index 0 is a real wordKeep the default True
Averaging over paddingEmbeddings depend on batch padding lengthMultiply by attention_mask and divide by its sum
Assuming index 0 for every modelWrong vector with left padding or decodersUse the model's convention; for GPT-style, the last non-pad token
Using pooler_output from a checkpoint without trained poolerRandom-layer outputUse last_hidden_state[:, 0] or fine-tune
Warning: Other models differ: RoBERTa uses <s>, DistilBERT keeps [CLS] without pooler or NSP, and ViT also prepends a learnable [CLS] token for image classification. Check which token and index a given model's classification head reads.

Common questions

Is the CLS token at the beginning of the sentence in BERT?

Yes, the tokenizer always places it at position 0, before the first word. For a sentence pair it appears only once, at the very start, while [SEP] appears after each segment.

Does the CLS token have its own meaning?

No. Its input embedding is the same learned vector for every input, so it encodes no word. Any meaning in its output comes from attention over the actual tokens plus what pretraining and fine-tuning taught it.

Do I need the CLS token if I use mean pooling?

The tokenizer still adds it and the model still processes it, and it takes part in attention. When mean pooling, you can include or exclude it; both are common, and the difference is typically small.

Why did RoBERTa drop NSP if CLS was trained with it?

Experiments in the RoBERTa work found that removing NSP did not hurt, and could help, downstream results. The <s> token still becomes a good summary because fine-tuning supplies the supervised signal.

Can I fine-tune BERT with mean pooling instead of CLS?

Yes. Compute the masked mean of last_hidden_state and put your dropout and linear head on it. Results are usually close to the [CLS] approach, so pick whichever matches your downstream embedding needs.

Related topics:berttransformersnlphugging faceembeddings