BERT 개념이해 #8 [CLS] Token Extraction

BERT 개념이해 #8 [CLS] Token Extraction #

#2026-03-04


#1 왜 [CLS]를 뽑아야 하나: 모델 출력은 “토큰별 결과”인데 우리는 “문장 하나의 답”이 필요하다

BERT를 12층 다 통과하고 나면 출력은 (batch, L, 768)이다. 여기서 L은 최대 192이고, 각 토큰마다 768차원 벡터가 하나씩 있다. 즉 모델은 “문장 전체에 대한 한 번의 답”을 바로 주는 게 아니라, “문장 안의 각 위치(토큰)에 대한 표현”을 만들어준다. 그런데 뉴스 분류 같은 문제는 “이 문장은 스포츠냐 비즈니스냐”처럼 문장 전체에 대해 딱 하나의 라벨이 필요하다. 그래서 우리는 이 L개의 벡터 중에서 문장 전체를 대표할 단 하나의 벡터를 뽑아야 한다. 이때 가장 표준적인 선택이 [CLS] 토큰의 벡터를 쓰는 방식이다.

결국 이 단계는 이렇게 요약된다. “토큰별로 만들어진 표현들 중에서, 문장을 대표하도록 설계된 자리 하나를 골라서 (batch, 768)로 만들자.” 그 자리가 바로 position 0, 즉 [CLS]의 자리다.

토크나이저 출력:
  [CLS] wall st . bears claw back [SEP]
    position 0에 삽입된 특수 토큰

BERT 사전학습(NSP: Next Sentence Prediction):
  [CLS] 문장A [SEP] 문장B [SEP]

  → [CLS] 위치의 출력으로 "A와 B가 연속된 문장인가?" 판별
  → 문장 쌍 전체의 의미를 [CLS]가 압축해서 담도록 학습됨

[CLS]는 일반 단어가 아니다. 토크나이저가 문장 맨 앞에 일부러 붙이는 특수 토큰이다. BERT를 설계할 때부터 “[CLS] 위치의 출력 벡터를 문장 단위 작업에 쓰자”라는 의도가 들어 있다. 특히 BERT의 사전학습 과제 중 하나였던 NSP(Next Sentence Prediction)에서는 입력이 [CLS] 문장A [SEP] 문장B [SEP] 형태로 들어가고, 모델은 “문장A 다음에 문장B가 자연스럽게 이어지는가?”를 맞힌다. 이때 분류 결정을 만드는 데 사용되는 대표 벡터가 바로 [CLS]의 출력이다. 다시 말해 사전학습 과정에서 [CLS]는 “두 문장을 통째로 보고 요약해서 판단하라”는 훈련을 받는다.

이 경험 때문에 [CLS]는 자연스럽게 “문장 전체 정보를 담기 좋은 그릇”으로 학습된다. fine-tuning에서 문장 분류를 할 때 [CLS]를 쓰는 이유가 바로 여기에 있다. 이미 사전학습으로 “문장 대표 역할”을 해본 자리이기 때문이다.

#

#2 Self-Attention 관점에서 보면: [CLS]는 모든 토큰을 바라보며 매층 업데이트된다

[CLS]가 문장 정보를 어떻게 모으는지 가장 직관적으로 이해하려면 Self-Attention을 떠올리면 된다. Self-Attention은 모든 위치가 모든 위치를 볼 수 있게 한다. [CLS]도 예외가 아니다. 첫 번째 레이어에서 [CLS]는 문장 안의 “wall”, “bears”, “back” 같은 토큰들을 보면서 자기 벡터를 업데이트한다. 두 번째 레이어에서는 이미 업데이트된 [CLS]가 다시 전체 토큰을 보면서 또 업데이트된다. 이런 일이 12번 반복된다.

중요한 포인트는 “요약이 한 번에 만들어지는 게 아니라, 레이어를 지나며 누적된다”는 점이다. 얕은 레이어의 [CLS]는 아직 토큰 임베딩과 가까운, 비교적 표면적인 정보에 머물 수 있다. 하지만 레이어가 깊어질수록 [CLS]는 중요한 토큰에 더 집중하고, 문장 의미를 더 고도로 압축한 표현으로 변해간다. 그래서 마지막 레이어의 [CLS] 벡터는 “문장 전체를 문맥적으로 이해한 요약”에 가깝게 된다.

# Self-Attention에서 [CLS]의 동작

Self-Attention은 모든 위치 쌍을 계산한다.
[CLS]도 다른 모든 토큰에 attention을 줄 수 있다.

Layer 1:
  [CLS].Q · [wall].K → attention score
  [CLS].Q · [bears].K → attention score
  [CLS].Q · [back].K → attention score
  → [CLS]가 모든 단어를 참조하여 업데이트

Layer 12까지 12번 반복:
  → [CLS] 벡터에 문장 전체 정보가 축적
  → "AG News 기사의 카테고리"에 관련된 정보를 집약

따라서: position 0의 벡터 = 문장 전체의 요약 표현

실제 추출은 그냥 인덱싱이다. [:, 0, :] 하나로 끝난다. BERT 출력 텐서를 last_hidden_state라고 부르면, 그 모양은 (batch, L, 768)이다. 여기서 0번 위치가 [CLS]이므로, last_hidden_state[:, 0, :]를 하면 배치마다 [CLS] 벡터만 골라서 (batch, 768)이 된다.

이 연산은 학습 가능한 파라미터가 전혀 없다. “학습을 해서 만드는 층”이 아니라, “이미 만들어진 결과에서 대표 벡터 하나를 꺼내는 선택”이다. 그래서 이 단계는 수학적으로는 매우 단순하지만, 의미적으로는 모델 설계 의도를 그대로 활용하는 매우 중요한 연결 고리다.

#

#3 다른 pooling이 있는데도 [CLS]를 쓰는 이유: 가장 표준적이고, 사전학습과 가장 일치한다

문장 벡터를 만드는 방법은 [CLS] 말고도 많다. 모든 토큰을 평균내는 mean pooling은 문장 전체를 균등하게 반영한다는 장점이 있고, max pooling은 강한 신호를 잡는 장점이 있다. attention 가중합 같은 weighted pooling은 “중요한 토큰에 더 주목”하도록 만들 수 있다. 하지만 이런 방법들은 BERT가 사전학습에서 직접 “문장 대표 자리”로 훈련했던 구조와는 결이 조금 다르다.

AG News처럼 문장이 짧고 카테고리가 비교적 명확한 분류 문제에서는, 사전학습 의도와 일치하는 [CLS] 방식이 안정적으로 잘 먹히는 경우가 많다. 그래서 이 모델은 표준적인 [CLS] 추출을 선택했고, 실제로 높은 정확도까지 이어진다. 즉 이 선택은 “가장 단순하면서도 가장 BERT다운 방식”이라고 볼 수 있다.

# [CLS] 외에 사용할 수 있는 다른 Pooling 방법들

방법 1: [CLS] Token (이 모델)
  출력[:, 0, :]  → (batch, 768)
  장점: BERT 사전학습 목적과 일치 (NSP)
  단점: [CLS] 토큰의 표현이 항상 최선이 아닐 수 있음

방법 2: Mean Pooling
  출력.mean(dim=1)  → (batch, 768)
  모든 토큰의 평균을 냄
  장점: 모든 토큰 정보 균등하게 활용
  단점: 중요/비중요 토큰 구분 없음

방법 3: Max Pooling
  출력.max(dim=1)  → (batch, 768)
  각 차원에서 최댓값을 선택
  장점: 가장 강한 신호 포착
  단점: 차원별로 다른 위치에서 값을 가져옴

방법 4: Weighted Pooling
  attention 가중치를 이용한 가중합
  장점: 중요한 토큰에 더 집중
  단점: 추가 파라미터 필요

이 모델: [CLS] Token 사용 (표준 BERT 방식)

#

#4 (batch, 768)이 분류기의 입력

[CLS] 추출을 하고 나면, 더 이상 시퀀스 차원 L이 없다. 이제는 각 문장마다 768차원 벡터 하나만 남는다. 이 벡터는 EnhancedClassifier로 들어간다. Dropout으로 과적합을 누르고, 768→256 Linear로 분류에 필요한 특징을 뽑고, GELU로 비선형 조합을 만들고, 다시 Dropout을 거친 후 256→4 Linear로 최종 로짓을 만든다. 즉 [CLS] 추출은 “토큰 단위 표현”을 “문장 단위 결정”으로 바꾸기 위해 반드시 거쳐야 하는 접합부다.

BERT 12번째 Layer 출력:
  (batch, ≤192, 768)  ← 모든 토큰의 벡터

[CLS] 추출:
  (batch, ≤192, 768) → (batch, 768)
   ↑         ↑              ↑
   배치 유지  시퀀스 차원 제거  차원 유지
              (192→1 선택)

→ EnhancedClassifier 입력:
  (batch, 768) → Dropout → Linear(768→256) → GELU → Dropout → Linear(256→4)

#cf 실제로 [CLS]에 어떤 정보가 담기는가

입력: "Oil prices rise sharply on supply concerns"
카테고리: Business (2)

Layer 1 [CLS] 벡터:
  → 주로 Token Embedding 정보 (단어 자체의 의미)
  → 아직 문맥 통합이 부족

Layer 6 [CLS] 벡터:
  → "oil"이 에너지/경제와 관련 있음을 파악
  → "prices", "supply"가 경제 맥락임을 파악

Layer 12 [CLS] 벡터:
  → 전체 문장이 "경제/비즈니스 뉴스"임을 고도로 압축
  → EnhancedClassifier가 이를 받아 Business(2)로 분류

분석:
  실제로 어텐션 패턴을 시각화하면
  [CLS]는 layer가 깊어질수록
  의미적으로 중요한 토큰("prices", "supply")에 더 집중하는 경향

#

#8 정리

BERT는 토큰마다 벡터를 출력하지만, 분류는 문장 하나당 한 벡터가 필요하다. [CLS]는 애초에 문장 대표 자리로 설계되고 NSP 사전학습에서 그 역할을 훈련받았다. Self-Attention을 거치며 [CLS]는 문장 전체 토큰을 참조하면서 정보가 누적되고, 마지막 레이어의 [CLS] 벡터는 문장 의미를 압축한 요약 표현이 된다. 그래서 우리는 단순히 last_hidden_state[:, 0, :]로 [CLS] 벡터를 뽑아 (batch, 768)로 만들고, 그걸 분류기에 넣어 최종 카테고리를 예측한다.

  • [CLS] Token Extraction = last_hidden_state[:, 0, :]
  • (batch, ≤192, 768)(batch, 768): 시퀀스 차원 제거
  • [CLS]가 특별한 이유: Self-Attention 12번을 통해 문장 전체 정보를 집약
  • BERT 사전학습(NSP)에서 [CLS]가 “문장 쌍 판별"을 담당 → 자연스럽게 문장 표현 학습
  • 학습 가능한 파라미터 없음: 단순 인덱싱 [:, 0, :]
  • 이후 단계: (batch, 768) → EnhancedClassifier 입력