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 입력