|
|
BERT (인코더 전용 모델) 구조 정리
- 추론 과정과 학습 과정을 블록 단위로 나눠서
지난번에 GPT 디코더 구조를 끝냈으니, 이번엔 인코더 전용 모델인 BERT를 본다.
디코더는 "다음 토큰을 생성하는" 모델이었는데, 인코더는 목적 자체가 다르다.
생성이 아니라 이해다. 문장을 통째로 받아서, 각 단어가 그 문장 안에서
어떤 의미를 갖는지를 벡터로 뽑아내는 게 목적이다.
이 글은 크게 두 부분으로 나뉜다.
먼저 추론 과정(이미 학습이 끝난 모델이 문장을 어떻게 처리하는가),
그다음 학습 과정(그 모델의 가중치가 애초에 어떻게 만들어지는가).
구조가 겹치는 부분이 많아서, 추론 쪽에서 인코더 내부를 다 뜯어보고 학습 쪽에서는 거기에 추가되는 것들만 본다.
═══════════════════════════════════════
제1부. 추론 과정
═══════════════════════════════════════
[구조도 ① — 추론 시 전체 파이프라인 (큰 스케일)]
■ 전체 흐름 먼저
입력 문장 → 토큰화 및 특수 토큰 삽입 → 세 가지 임베딩 합산 → 인코더 블록 N층 통과 → 토큰별 문맥화된 출력 벡터
이게 전부다. 여기서 나온 출력 벡터를 분류나 검색 같은 다운스트림 작업에 갖다 쓴다.
■ 블록 1. 입력 구성과 특수 토큰
입력: 원문 문장 (한 개 또는 두 개)
내부 연산: 토큰화, 특수 토큰 삽입
출력: 특수 토큰이 포함된 토큰 시퀀스
[전문 설명]
BERT는 문장 한 개도 받고 문장 두 개도 받을 수 있는데, 어느 쪽이든 시퀀스 맨 앞에는 [CLS]를 붙이고 각 문장이 끝나는 지점마다 [SEP]를 넣어서 형태를 맞춘다.
• 문장 1개: [CLS] 문장A [SEP]
• 문장 2개: [CLS] 문장A [SEP] 문장B [SEP]
[CLS]는 Classification의 약자이고 시퀀스 전체에 딱 하나만 붙는데, 이 자리에서 나오는 최종 출력 벡터가 문장 또는 문장쌍 전체를 대표하는 요약 벡터로 쓰이기 때문에 나중에 분류 작업을 할 때 이 벡터 하나만 뽑아서 쓰게 된다.
[SEP]는 문장과 문장의 경계를 표시하는 구분자인데, 이게 있다고 해서 attention 계산이 그 경계를 넘지 못하는 건 아니고 어디까지나 표시 역할만 한다.
애초에 문장 쌍을 받을 수 있게 설계된 이유는, 학습 과제 중 하나인 NSP가 "문장 A 다음에 문장 B가 오는가"를 맞추는 과제였기 때문에 입력 포맷 자체가 두 문장을 담을 수 있어야 했던 것이다.
다만 문장 쌍이 항상 필수라는 뜻은 아니고, 실제로는 풀려는 작업의 성격에 따라 하나만 넣어도 아무 문제가 없다.
• 문장 1개를 쓰는 경우: 감성 분석, 개체명 인식
• 문장 2개를 쓰는 경우: 질의응답(질문+지문), 문장 함의 관계 판단
한 가지 짚어둘 게, 여기서 말하는 토큰의 단위는 완전한 단어가 아니라 WordPiece라고 부르는 서브워드 단위여서, "은행"이 한 토큰일 수도 있고 "은" + "##행"처럼 더 잘게 쪼개질 수도 있다.
이 글에서는 이해를 돕기 위해 계속 완전한 단어 단위로 예시를 들겠지만, 실제 어휘사전은 이런 서브워드 조각들로 구성되어 있다는 점은 기억해두는 게 좋다.
[쉽게 다시 말하면]
[CLS]는 문서 맨 앞에 붙이는 "요약란"이다. 처음엔 빈칸인데, 모델이 문장 전체를 읽어나가면서 이 칸을 채워나간다.
다 읽고 나면 이 요약란만 봐도 문장 전체가 어떤 내용인지 알 수 있게 된다.
[SEP]는 그냥 "여기서 문장이 갈립니다"라는 표지판이다.
■ 블록 2. 세 가지 임베딩 합산
입력: 토큰 시퀀스 (각 토큰의 어휘사전 인덱스)
내부 연산: 세 개의 룩업 테이블 조회 후 요소별 덧셈
출력: 토큰마다 하나씩, 768차원 벡터
[구조도 ② — 임베딩 합산 블록 (작은 스케일)]
[전문 설명]
각 토큰은 단순히 "단어 벡터" 하나로 변환되는 게 아니라, 서로 다른 질문에 답하는 세 가지 임베딩을 각각 만든 뒤에 그걸 전부 더한 값으로 표현된다.
• Token embedding — "이 토큰이 무슨 단어인가"
크기: 어휘 수 × 768 (예: 30000 × 768)
가장 기본적인 임베딩으로, "은행"이 어휘사전에서 8532번이라면 그 테이블의 8532번째 행을 그대로 꺼내오는 방식이라 계산이라기보다는 표에서 찾아오는 룩업(조회)에 가깝다.
• Segment embedding — "이 토큰이 문장 A 소속인가 B 소속인가"
크기: 2 × 768 (A용 한 줄, B용 한 줄)
self-attention은 [SEP]로 문장이 갈려 있어도 그 경계를 스스로 이해하는 게 아니라 그냥 하나의 긴 토큰 나열로 보기 때문에, "이 토큰은 A 소속"이라는 표시를 따로 붙여주지 않으면 모델이 두 문장을 구분해서 처리할 방법이 없다.
• Position embedding — "이 토큰이 몇 번째 자리인가"
크기: 최대 시퀀스 길이 × 768 (예: 512 × 768)
self-attention은 모든 토큰을 병렬로 동시에 보기 때문에 그 자체로는 "몇 번째"라는 순서 개념이 아예 없어서, "나는 은행에 갔다"와 "갔다 은행에 나는"을 attention만으로는 구별하지 못한다. 그래서 각 위치마다 고유한 벡터를 더해줘서 순서 정보를 따로 주입해줘야 한다.
세 임베딩의 테이블 크기와 조회 기준을 나란히 놓고 보면 차이가 분명해진다.
[표 ① — 세 가지 임베딩 비교]
이렇게 꺼낸 세 벡터를 요소별로, 즉 768개 숫자를 자리마다 각각 더한다.
E_final = E_token + E_segment + E_position
결과는 여전히 768차원 벡터 하나이고, 이게 인코더로 들어갈 최종 입력이 된다.
참고로 BERT는 원조 Transformer가 썼던 고정된 sin/cos 함수 방식이 아니라
학습되는 Position embedding 테이블을 쓴다는 점이 다르다.
───【핵심】───
왜 굳이 셋으로 나눠서 만들고 더하는가?
결국 더해서 하나가 될 거면 처음부터 하나로 만들면 안 되나?
각 정보를 독립적으로 학습 가능한 파라미터로 분리하기 위해서다.
만약 분리하지 않고 "3번째 자리의 은행"을 통째로 하나의 새 벡터로 학습시킨다면, 모델은 "은행"이 1번째 자리에 있을 때와 3번째 자리에 있을 때가 같은 단어라는 사실조차 스스로 재발견해야 하기 때문에 데이터도 훨씬 많이 필요하고 학습 효율도 크게 떨어진다.
분리해서 더하면
• Token embedding은 "은행이 뭔가"만 담당 → 위치와 무관하게 재사용
• Position embedding은 "3번째 자리"만 담당 → 어떤 단어든 재사용
만드는 과정에서 학습 부담을 나눠준 것이고, 다 만들고 나서 더하는 순간 결국 하나의 벡터가 되는 건 맞다.
[쉽게 다시 말하면]
우편물에 붙이는 라벨 세 장이라고 보면 된다.
"내용물이 뭔지"(Token), "몇 번 상자인지"(Position), "A동 소속인지 B동 소속인지"(Segment).
세 장을 따로 만들어서 한 상자에 다 붙이면, 나중에 라벨 하나만 바꿔도 나머지는 그대로 쓸 수 있다. 처음부터 셋을 합친 한 장짜리 라벨을 만들면, 조합마다 새 라벨을 다 찍어내야 한다.
■ 블록 3. 인코더 레이어 (N층 반복)
입력: 768차원 벡터들 (토큰 개수만큼)
내부 연산: self-attention → Add&LN → FFN → Add&LN
출력: 같은 개수, 같은 차원의 벡터들
[구조도 ③ — 인코더 레이어 1개 내부 (작은 스케일)]
여기가 모델의 본체다. 하나씩 뜯어본다.
3-1. Q, K, V 생성
입력 벡터 x에 서로 다른 세 개의 학습 가능한 가중치 행렬을 각각 곱해서 역할이 다른 세 가지 벡터를 만들어낸다.
Q = x · Wq
K = x · Wk
V = x · Wv
Wq, Wk, Wv는 전부 학습되는 가중치 행렬이고, 여기서 나온 Q, K, V가 바로 다음 단계인 attention 계산의 재료가 된다.
3-2. 양방향 멀티헤드 Self-Attention
[전문 설명]
각 헤드가 서로 독립적으로 다음 계산을 수행한다.
score = Q Kᵀ / √dₖ
output = softmax(score) · V
여기서 √dₖ로 나누는 이유는 차원이 커질수록 내적 결과값이 같이 커져서 softmax가 한쪽으로 극단적으로 쏠려버리는(포화되는) 걸 막기 위한 스케일 조정이다.
BERT의 핵심은 이 attention에 causal mask가 없다는 점인데, GPT 디코더에서는 미래 토큰의 score를 -무한대로 만들어 softmax를 거치면 0이 되게 함으로써 왼쪽만 보게 만들었지만 BERT에는 그런 장치가 아예 없어서 모든 토큰이 자기 왼쪽이든 오른쪽이든 상관없이 다른 모든 토큰과의 score를 전부 계산하게 되고, 이것이 바로 "양방향"이라고 부르는 이유다.
다만 완전히 마스크가 없는 건 아니고, 문장 길이를 맞추려고 채워 넣은 패딩 토큰에는 attention을 못 하게 막는 padding mask는 쓰는데, 이건 의미 없는 빈 토큰을 무시하는 용도일 뿐이라 "미래를 가리는" causal mask와는 성격이 완전히 다르다.
헤드를 여러 개로 나누는 이유는, 언어가 문법적으로 정해진 구조를 갖고 있다고 해도 한 토큰이 동시에 여러 종류의 서로 다른 관계를 맺기 때문이다. 예를 들어 "갔다"라는 동사 하나만 봐도 다음 질문들에 동시에 답해야 한다.
• 누가 갔는가 → "나는"과의 주어 관계
• 어디로 갔는가 → "은행에"와의 목적지 관계
• 문맥상 이 "은행"이 금융기관인지 강둑인지 → 의미 중의성 해소
이 세 가지는 계산 방식 자체가 다를 수밖에 없는 별개의 관계인데, 헤드가 하나뿐이라면 이 모든 관계를 단 하나의 attention 가중치 분포로 뭉뚱그려 표현해야 해서 서로 충돌하거나 희석되는 문제가 생긴다.
실제로 학습이 끝난 BERT를 분석한 연구들을 보면 어떤 헤드는 주로 구문 관계에, 어떤 헤드는 대명사가 가리키는 대상을 찾는 데, 또 어떤 헤드는 바로 인접한 단어에만 집중하는 식으로 역할이 자연스럽게 나뉘는 현상이 관찰된다.
3-3. Concat 후 선형 변환
헤드가 8개라면 각 헤드가 낸 출력을 옆으로 그대로 이어 붙인 다음(concat), 학습되는 가중치 행렬 Wo를 한 번 곱해준다.
output = Concat(head₁, ..., head₈) · Wo
이 선형 변환이 필요한 이유는 두 가지다.
첫째, 헤드들의 정보를 서로 섞어주기 위해서인데, concat만 하고 끝내면 8개 헤드의 결과가 그냥 나란히 붙어있을 뿐 서로 전혀 상호작용하지 않은 파편 상태로 남기 때문에, Wo를 곱하는 과정에서 1번 헤드의 어떤 차원과 3번 헤드의 어떤 차원을 조합해 새로운 의미를 만들어내는 혼합이 일어나야 비로소 하나의 통합된 표현이 된다.
둘째, 바로 다음 단계인 residual 연결에서 원본 입력 x와 더해야 하는데 더하려면 두 벡터의 차원이 정확히 같아야 하고, 설령 우연히 차원이 맞더라도 학습 가능한 변환을 한 번 거쳐야 모델이 이 출력을 다음 단계로 어떻게 넘길지 스스로 조정할 자유도가 생긴다.
3-4. Add & LayerNorm
입력: 벡터 두 개 (attention 출력, residual로 넘어온 원본 x)
내부 연산: 요소별 덧셈 후 정규화
출력: 벡터 하나
[전문 설명]
이름은 하나로 붙어 있지만 실제로는 순차적인 두 연산이고, 병렬로 각각 다른 결과를 내는 게 아니라 앞 연산의 출력이 뒤 연산의 유일한 입력이 된다.
1단계 Add (residual 연결)
z = Attention(x) + x
attention이 아무리 잘 학습된다고 해도 층을 12개나 쌓다 보면 매 층마다 정보가 조금씩 손실되거나 왜곡될 위험이 있는데, 원본 x를 그대로 한 번 더 더해주면 최소한 원본 정보로 돌아갈 수 있는 지름길이 항상 열려 있게 되고, 극단적으로 attention이 아무 쓸모없는 값을 내놓더라도 x가 그대로 더해지기 때문에 정보가 완전히 사라지지는 않는다.
이게 층을 아주 깊게 쌓아도 학습이 안정적으로 되게 해주는 핵심 장치다.
2단계 LayerNorm
output = LayerNorm(z)
더한 결과 z에 대해, 토큰 하나의 벡터 z = [z₁, z₂, ..., z₇₆₈] 안에 있는 768개 숫자 전체를 대상으로 다음을 계산한다.
μ = (1/768) Σ zᵢ
σ² = (1/768) Σ (zᵢ - μ)²
ẑᵢ = (zᵢ - μ) / √(σ² + ε)
outputᵢ = γᵢ · ẑᵢ + βᵢ
여기서 γ와 β는 학습되는 파라미터인데, 앞서 나온 Wo 같은 행렬이 아니라 768차원 벡터 두 개일 뿐이라서 파라미터 수가 768 × 2 = 1536개밖에 안 되고, 약 59만 개인 Wo와 비교하면 정말 작은 연산이다.
γ와 β를 굳이 두는 이유는, 정규화만 하고 끝내면 무조건 평균 0 분산 1로 강제 고정되는데 그게 모델 입장에서 항상 최선은 아닐 수 있기 때문이다.
그래서 정규화는 하되 그 결과를 다시 얼마나 늘리고 어디로 옮길지는 모델이 학습으로 스스로 결정하게 여지를 남겨둔 것이고, 극단적으로는 γ와 β가 원래 분산과 평균 값으로 학습되면 정규화를 사실상 취소하는 것까지 가능하다.
LayerNorm이 필요한 근본적인 이유는, Add를 거치면 값의 크기가 들쭉날쭉 커지거나 작아질 수 있고 이게 층을 거듭할수록 누적되면 gradient가 폭주하거나 사라져서 학습이 불안정해지기 때문에, 매 블록마다 값의 범위를 표준화해서 다음 레이어가 항상 비슷한 규격의 입력을 받도록 만들어주는 것이다.
───【핵심】───
평균 0 분산 1인 분포에서
• 약 68%가 -1~1 사이
• 약 95%가 -2~2 사이
• 나머지는 -3, +4 같은 값도 나올 수 있다
분산 1은 "평균적인 퍼짐 정도"를 나타내는 통계적 기준값일 뿐, 개별 숫자를 강제로 잘라내는 하드 제약(clipping)이 아니다.
768개 중에 유독 큰 값 하나가 있으면 정규화 후에도 여전히 크게 남는다.
[쉽게 다시 말하면]
Add는 "보험"이다. 아무리 복잡한 가공을 거쳐도 원본을 한 부 더 얹어두니 중간에 뭘 망쳐도 원본이 완전히 사라지지는 않는다.
LayerNorm은 "규격 통일"이다. 사람마다 cm로 재고 inch로 재던 걸 전부 표준 단위로 바꿔서 비교 가능하게 만드는 것. 다만 모든 사람의 키를 똑같이 맞추는 게 아니라, 재는 자를 통일하는 것뿐이다.
3-5. Feed-Forward Network
입력: 768차원 벡터
내부 연산: Linear(768→3072) → GELU → Linear(3072→768)
출력: 768차원 벡터
[전문 설명]
앞의 Wo가 차원을 그대로 유지했던 것과 달리, 여기서는 진짜로 차원이 커졌다가 다시 작아지는 과정을 거친다.
첫 번째 Linear에서 768을 3072로 네 배 확장하고 그 사이에 GELU라는 비선형 활성화 함수를 통과시키는데, 이 지점이 중요한 이유는 attention이 그 자체로는 가중합이라 선형적인 연산에 가깝기 때문에 FFN의 비선형 함수가 모델 전체에 복잡한 패턴을 표현할 수 있는 능력을 부여하는 핵심 역할을 맡고 있어서다.
차원을 굳이 넓히는 건 더 넓은 공간에서 다양한 특징들을 각각 감지하고 조합할 여유를 주기 위해서인데, 768개의 질문만 던질 수 있던 것을 3072개의 세부 질문으로 쪼개서 던지는 것에 가깝다.
두 번째 Linear에서 3072를 다시 768로 압축하는데, 이건 넓게 펼쳐서 뽑아낸 특징들을 다음 레이어가 처리할 수 있는 표준 크기로 되돌려주는 과정이다.
멀티헤드 뒤에 나왔던 Wo와 헷갈리기 쉬운데, 둘 다 학습되는 행렬 곱이라는 점은 같지만 목적이 완전히 다르다.
• Wo: 768 → 768, 차원 유지, 여러 헤드의 정보를 섞는 역할
• FFN: 768 → 3072 → 768, 차원 확장 후 압축, 비선형 재가공
역할 차이를 크게 보면, attention은 토큰들끼리 서로 정보를 주고받는 단계이고 FFN은 각 토큰이 받은 정보를 혼자 조용히 곱씹어서 더 깊게 가공하는 단계라고 볼 수 있는데, 실제로도 attention은 토큰 간 상호작용을 계산하는 반면 FFN은 토큰 하나하나에 독립적으로 똑같이 적용되는 연산이다.
[쉽게 다시 말하면]
768개의 질문만 던질 수 있던 걸 3072개의 세부 질문으로 쪼개서 던지고, 그 답들을 다시 768개로 요약해서 정리하는 과정이다.
3-6. 반복
FFN을 거친 뒤 Add&LN이 한 번 더 들어가고, 여기까지가 인코더 레이어 하나이며 이 블록 전체가 그대로 N번 반복된다. 반복 횟수와 각종 크기는 모델 규모에 따라 달라진다.
[표 ② — BERT-base와 BERT-large 사양 비교]
여기서 중요한 건 각 층의 파라미터(Wq, Wk, Wv, Wo, FFN 가중치, γ, β)가 전부 따로따로 학습된다는 점이라서, 층 1과 층 12가 같은 가중치를 공유하는 게 아니라 완전히 독립적인 12벌의 파라미터 세트가 존재하는 셈이다.
그리고 층이 깊어질수록 낮은 층은 인접 단어 관계 위주로, 높은 층은 문장 전체의 의미적 관계 위주로 포착하는 경향이 있다는 연구 결과들도 있다.
■ 블록 4. 최종 출력 벡터
입력: 마지막 인코더 층의 출력
내부 연산: 없음 (그대로)
출력: 입력 토큰 개수와 동일한 개수의 768차원 벡터
[구조도 ④ — 출력 벡터의 형태와 활용 (작은 스케일)]
[전문 설명]
입력이 6개 토큰이었으면 출력도 정확히 6개 벡터가 나오는데, 층을 몇 번을 거치든 토큰 개수는 절대 바뀌지 않고 각 자리에 하나씩 대응된다.
각 벡터는 이제 그 단어 하나만의 뜻을 담은 게 아니라 N개 층을 거치면서 문장 안의 다른 모든 토큰과 attention으로 상호작용한 결과가 전부 녹아든 값이 되었기 때문에, 이걸 문맥화된 표현(contextualized representation)이라고 부른다. 예를 들어 "은행" 벡터는 주변의 "나는", "갔다" 같은 단어들의 정보를 다 반영해서 문맥에 따라 금융기관인지 강둑인지에 맞는 다른 값을 갖게 된다.
───【핵심】───
입력 임베딩과 출력 벡터는 뭐가 다른가?
입력 임베딩의 "나는" 벡터
• Token + Segment + Position을 더한 값
• 문맥과 무관하게 고정
• "나는 은행에 갔다"든 "나는 슬프다"든, 1번째 자리 "나는"의 입력 임베딩은 항상 똑같다
출력 벡터의 "나는"
• self-attention을 N번 거치며 같은 문장의 다른 단어들과 상호작용한 결과가 반영됨
• 문맥에 따라 달라진다
• "이 특정 문장에서 나는이 어떤 역할로 쓰였는가"를 담은 값
한마디로
• 입력 임베딩 = 사전에서 찾은 단어 뜻 (문맥 무시, 고정)
• 출력 벡터 = 문장 속에서 실제로 읽고 난 뒤의 이해 (문맥 반영)
단, 완전히 다른 단어로 바뀌는 게 아니다. "나는"이라는 정체성은 유지하면서 그 위에 문맥 정보가 덧입혀진 상태다.
───────────────
이 출력 벡터들 자체가 최종 답은 아니고, 어떤 작업을 하느냐에 따라 필요한 자리의 벡터를 골라서 쓰게 된다.
• [CLS] 벡터만 사용 → 문장 전체를 대표 → 감성 분석, 문장 관계 판단
• 개별 토큰 벡터 사용 → 각 단어의 문맥적 의미 → 개체명 인식, 품사 태깅
• 전체 벡터를 평균/풀링 → 문장 임베딩 → 유사도 검색
[쉽게 다시 말하면]
들어갈 때는 사전에서 뜻만 찾아온 상태였다면, 나올 때는 그 문장을 실제로 다 읽고 "아 여기서는 이런 의미로 쓰인 거구나"까지 파악한 상태가 된다. 단어는 같은데 이해도가 달라진 것이다.
═══════════════════════════════════════
제2부. 학습 과정
═══════════════════════════════════════
[구조도 ⑤ — 학습 시 전체 파이프라인 (큰 스케일)]
지금까지 본 건 이미 학습이 끝난 모델이 문장을 처리하는 순전파 과정이었고, 이제부터는 그 계산에 쓰이는 가중치들(Wq, Wk, Wv, Wo, FFN 가중치, γ, β, 그리고 세 임베딩 테이블까지 전부)이 애초에 어떻게 만들어지는지를 본다.
중요한 전제가 하나 있는데, 학습 시작 시점에는 이 모든 파라미터가 랜덤 초기화 상태이지 미리 학습되어 있는 게 아니라는 점이다. 세 임베딩 테이블도 마찬가지로 랜덤에서 출발해서, 다른 가중치들과 정확히 같은 타이밍에 같은 역전파 과정에서 전부 동시에 학습된다. 즉 임베딩이 먼저 완성되고 그다음 인코더가 학습되는 순차적 구조가 아니라 처음부터 끝까지 한꺼번에 간다.
그리고 인코더 블록 내부의 계산 방식 자체는 학습 시나 추론 시나 완전히 동일하기 때문에 제1부에서 본 구조를 그대로 다시 쓰면 되고, 달라지는 건 그 계산 뒤에 loss를 구하고 역전파를 한다는 것뿐이다.
■ 학습 과제가 두 개인 이유
BERT는 두 가지 과제를 동시에 학습하는데, 둘이 다루는 범위가 다르다.
• MLM (Masked Language Modeling) — 단어 레벨, 문장 내부
한 문장 안에서 어떤 단어가 주변 단어들과 어떻게 관계 맺는지
• NSP (Next Sentence Prediction) — 문장 레벨, 문장과 문장 사이
두 문장 덩어리가 논리적으로 이어지는지
둘 다 사람이 일일이 라벨링한 데이터가 필요한 게 아니라 원문 텍스트에서 정답을 자동으로 만들어낼 수 있는 self-supervised 과제이기 때문에, 인터넷에 널린 방대한 텍스트를 그대로 학습에 쓸 수 있다는 게 큰 장점이다.
구조적으로 보면 MLM과 NSP를 위한 통합된 새 구조가 따로 있는 게 아니라, 완전히 동일한 하나의 인코더 몸통을 함께 통과시킨 뒤 그 출력에서 서로 다른 자리의 벡터를 뽑아 서로 다른 분류기(head)에 넣는 방식으로 갈라진다.
• 공유되는 몸통: 인코더 스택 (한 번만 통과)
• MLM head: 예측 대상 자리들의 출력 벡터를 받음
• NSP head: [CLS] 자리의 출력 벡터 하나를 받음
여기서 헷갈리기 쉬운 게 두 head의 관계인데, 이 둘은 병렬이지 직렬이 아니어서 MLM이 먼저 단어를 예측하고 그 결과가 NSP로 흘러가는 구조가 전혀 아니고, 인코더 출력에서 각자 자기가 필요한 자리의 벡터를 따로 가져간다.
■ 블록 5. 문장쌍 구성 (NSP 정답 라벨 생성)
입력: 원문 코퍼스(모델이 학습할 원본 글 창고)
내부 연산: 50:50 확률로 실제 다음 문장 또는 무작위 문장 선택
출력: 문장쌍 + 정답 라벨 (IsNext / NotNext)
[전문 설명]
원문 코퍼스에서 문장 A를 하나 고른 뒤, 그 뒤에 붙일 문장 B를 정하는데 이때 동전 던지듯 절반의 확률로 갈린다.
• 50% — 같은 문서의 실제 연속된 두 문장 → 정답 라벨 IsNext
• 50% — 다른 문서에서 무작위로 뽑은 문장 → 정답 라벨 NotNext
여기서 어느 쪽을 골랐는지가 그대로 NSP의 정답 라벨이 되기 때문에, 정답을 만들기 위해 사람이 따로 라벨링 작업을 할 필요가 전혀 없다.
[쉽게 다시 말하면]
문제를 내는 사람이 답을 이미 알고 있는 상태로 문제를 만드는 것이다. 동전을 던져서 앞면이면 진짜 이어지는 문장을, 뒷면이면 아무 상관없는 문장을 갖다 붙인다.
어느 쪽을 붙였는지는 문제 낸 쪽이 아니까 그게 그대로 정답이 된다.
■ 블록 6. 15% 선택과 80-10-10 손상
입력: 문장쌍의 토큰 시퀀스
내부 연산: 15% 무작위 선택 후 그 안에서 세 갈래 손상
출력: 손상된 입력 시퀀스 + 원래 정답 토큰들
[구조도 ⑥ — 15% 선택과 80-10-10 분기 (작은 스케일)]
[전문 설명]
먼저 문장쌍 전체 토큰의 15%를 무작위로 골라서 예측 대상으로 표시해두는데, 이 시점에는 아직 아무것도 변형되지 않았고 "이 자리들을 나중에 손댈 것"이라고 찜해두는 단계일 뿐이다.
그 다음 찜해둔 15% 안에서만 다시 세 갈래로 나눠서 실제 변형을 가한다.
• 80% — [MASK] 토큰으로 교체 (은행 → [MASK])
• 10% — 무작위 다른 단어로 교체 (은행 → 사과)
• 10% — 원래 단어 그대로 유지 (은행 → 은행)
───【핵심】───
80-10-10은 전체 토큰이 아니라 선택된 15% 안에서의 비율이다.
토큰이 100개인 문장이면
• 15개가 선택됨 (예측 대상)
• 그중 12개 → [MASK]로 교체
• 그중 1.5개 → 무작위 단어로 교체
• 그중 1.5개 → 원래 단어 그대로
• 나머지 85개 → 손대지도 않고, 예측 대상도 아님
세 갈래 모두 정답은 원래 단어인 "은행"으로 동일하고 세 경우 전부 loss 계산에 포함되는데, 특히 10%짜리 "원래 단어 그대로" 케이스는 겉보기엔 원문과 아무 차이가 없어 보이지만 모델은 그 자리에서도 원래 단어를 맞춰야 하기 때문에 손대지 않은 나머지 85%와는 성격이 완전히 다르다. 85%는 애초에 예측 대상 자체가 아니다.
이렇게 복잡하게 나누는 이유는 학습과 추론 사이의 불일치(mismatch) 때문이다.
추론 시점에는 [MASK] 토큰이 아예 존재하지 않는데(실제 문장엔 마스크가 없으니까) 학습 때 100% [MASK]만 보고 배우면 모델이 "[MASK] 표시가 있는 자리만 문맥으로 추론하면 된다"는 식으로 요령을 익혀버릴 수 있어서, 일부러 마스크가 아닌 경우도 섞어서 이 자리가 마스크든 아니든 모든 토큰을 항상 문맥 기반으로 검증하도록 강제하는 것이다.
특히 10% 무작위 교체가 까다로운데, "은행"이 "사과"로 바뀌어 있는데도 모델은 "은행"이라고 답해야 하기 때문에 눈앞에 보이는 토큰을 그대로 믿으면 안 되고 주변 문맥과 대조해서 "이 자리 단어가 이상하다"는 것까지 판단할 수 있어야 한다.
참고로 이 80/10/10이라는 비율 자체는 논문 저자들도 시행착오 과정에서 정해진 다소 임의적인 값이라고 언급하고 있고, 후속 연구에서는 이 전략을 아예 쓰지 않는 편이 성능이 더 나은 경우도 있다는 결과가 나왔다.
[쉽게 다시 말하면]
시험 문제를 만드는데 빈칸만 뚫으면 학생이 "빈칸만 신경 쓰면 되네" 하고 요령을 피운다. 그래서 빈칸도 만들고, 일부러 오답을 적어놓은
칸도 만들고, 정답이 그대로 적혀 있는 칸도 만든다.
어느 칸이 어떤 경우인지 알려주지 않고 전부 채점 대상이라고 하면, 학생은 모든 칸을 다 검토할 수밖에 없다.
■ 블록 7. 임베딩과 인코더 통과
제1부의 블록 2, 3과 완전히 동일한 과정이라 손상된 입력 시퀀스가 세 임베딩을 거쳐 인코더 N층을 그대로 통과하는데, 여기서 [MASK] 자리에 일어나는 변화만 따로 짚고 넘어갈 필요가 있다.
입력 단계의 [MASK]는 원래 단어 정보가 완전히 지워진 빈 자리로, "은행"이라는 정보 자체가 사라지고 [MASK]라는 고정된 특수 토큰 임베딩으로 대체된 상태다.
그런데 인코더를 통과하는 동안 이 빈 자리는 self-attention을 통해 좌우 주변 토큰들의 정보를 끌어와서 자기 벡터를 채워나가기 때문에, 출력 단계의 [MASK] 자리는 입력의 그 빈 벡터가 단순히 통과된 게 아니라 주변 문맥으로 새롭게 채워진 완전히 다른 벡터가 된다.
다만 이 시점에도 아직 "정답이 은행이다"라고 확정된 건 아니어서, 여기 담긴 것은 어디까지나 "이 자리에 뭐가 와야 할지에 대한 힌트가 잔뜩 담긴 벡터"일 뿐이고 실제 예측은 다음 단계인 MLM head에서 일어난다.
[CLS] 자리도 같은 원리로 채워지는데, self-attention이 양방향인 데다 [SEP]가 attention의 계산 범위를 막지 않기 때문에 [CLS]는 문장 A와 B의 모든 토큰을 전부 참고할 수 있고, 그래서 N층을 거치는 동안 양쪽 문장의 정보를 계속 누적해 흡수한 끝에 문장쌍 전체를 요약한 벡터가 된다.
이게 바로 NSP head가 두 문장의 관계를 판단할 수 있는 근거가 된다.
[쉽게 다시 말하면]
[MASK] 자리는 처음엔 텅 빈 칸인데, 주변 단어들을 조금씩 참고해서 채워진다. 단, 똑같은 비율로 섞이는 게 아니라 단어마다 다른 비중
(attention weight)으로 섞인다. 많이 참고되는 단어도 있고 거의 무시되는 단어도 있다.
■ 블록 8. 출력 벡터에서 두 갈래 분기
입력: 인코더 출력 (토큰 개수만큼의 768차원 벡터)
내부 연산: 특정 위치의 벡터만 추출
출력: [CLS] 벡터 1개 / 예측 대상 15% 자리 벡터들
[전문 설명]
인코더 출력에서 두 head가 각자 필요한 자리의 벡터만 뽑아 가는데, 이 두 경로는 중간에 서로 섞이지 않는 완전히 독립된 갈래다.
• [CLS] 자리 벡터 1개 → NSP head로
• 예측 대상 15% 자리 벡터들 → MLM head로
그러면 나머지 85% 토큰의 출력 벡터는 왜 쓰지 않는가 하는 의문이 생긴다.
MLM 관점에서 보면 그 자리들은 애초에 정답이 필요 없는 자리인데, 이미 원본 그대로 모델이 볼 수 있었으니 "이게 뭐였는지 맞춰봐"라고 물을 이유가 없고 정답이 없으니 loss를 계산할 방법도 없기 때문이다.
NSP 관점에서 보면 과제 자체가 단어 하나하나에 대한 판단이 아니라 문장 전체에 대한 하나의 질문이라서, 문장 전체를 하나로 압축한 판단이 필요한데 이미 그 역할을 하도록 학습되고 있는 [CLS] 벡터 하나만 있으면 충분하고 오히려 모든 토큰 벡터를 다 넣으면 뭘 어떻게 조합해야 할지 모호해지고 비효율적이 된다.
단, 나머지 85%가 쓸모없다는 뜻은 전혀 아니다. 그 벡터들은 [CLS]와 [MASK] 자리의 벡터를 만드는 재료로 이미 충분히 쓰였는데, self-attention 계산 과정에서 [CLS]나 [MASK]가 그 85% 토큰들을 참고했기 때문이다. 다만 그 85% 토큰 자신의 출력 벡터를 직접 뽑아서 loss에 넣지 않을 뿐이다.
[쉽게 다시 말하면]
채점자는 답을 써야 할 빈칸만 채점하지, 이미 인쇄되어 있던 문제 지문까지 채점하지 않는다. 지문은 답을 쓰는 데 필요한 재료였을 뿐이다.
■ 블록 9. MLM head
입력: 예측 대상 자리의 출력 벡터 (768차원)
내부 연산: Linear+GELU → LayerNorm → 어휘 전체와 내적 → Softmax
출력: 어휘 크기만큼의 확률분포
[구조도 ⑦ — MLM head 내부 (작은 스케일)]
[전문 설명]
MLM head는 네 단계로 이루어져 있고, 마지막 단계에서 비로소 실제 예측이 나온다.
1단계. Linear + GELU (768 → 768)
차원은 유지하고 비선형 변환만 추가
2단계. LayerNorm
값 범위 정규화
3단계. 어휘사전 전체와 내적 (768 → 어휘 크기)
이 768차원 벡터를 어휘사전의 모든 단어 벡터와 각각 내적해서 어휘 크기만큼(예: 30000개)의 점수를 만든다
4단계. Softmax
점수들을 확률분포로 변환 (전체 합 = 1)
3단계의 내적이 무슨 의미인지가 핵심인데, 내적은 두 벡터가 얼마나 비슷한 방향을 가리키는지를 재는 연산이라 값이 클수록 유사하다는 신호가 된다.
지금 가지고 있는 건 MASK 자리에서 나온, 주변 문맥이 잔뜩 녹아든 벡터인데 이걸 "이 문맥에 어울리는 단어가 뭘까"라는 질문이 담긴 벡터라고 보면, 어휘사전 테이블의 각 행은 각 단어의 정체성 벡터라고 볼 수 있다.
그러니까 내적을 한다는 건 이 질문 벡터가 어느 단어의 정체성 방향과 가장 가까운지를 3만 개 단어 전부에 대해 한 번에 계산하는 것이고, 이 단계가 없으면 768차원 벡터 하나만 가지고는 후보 중에서 답을 고를 방법 자체가 없다.
3단계는 수학적으로 768 × 30000 크기의 Linear 층과 정확히 같은 연산이라서, 결국 이 층의 출력 노드가 어휘 크기만큼 존재한다는 뜻이 되고 파라미터로 치면 약 2300만 개나 되어 BERT 전체 파라미터 중에서도 결코 작지 않은 비중을 차지한다.
그래서 weight tying이라는 기법을 쓰게 되는데, 생각해보면 "이 벡터가 은행이라는 단어에 얼마나 가까운가"(출력용)와 "은행이라는 단어의 벡터가 뭔가"(입력용 Token embedding)는 사실상 같은 정보이기 때문에, 입력 임베딩 테이블을 전치해서 출력용 가중치로 그대로 재사용하면 파라미터를 크게 아낄 수 있다.
한편 중간층인 1, 2단계까지 어휘 크기로 부풀릴 필요는 전혀 없는데, 이 두 단계의 역할은 문맥 정보를 단어 매칭에 적합한 형태로 다듬는 것이지 어떤 단어인지를 결정하는 게 아니기 때문이다. 압축된 768차원 공간에서 계속 다듬다가 최종 판단이 필요한 순간에만 후보 개수만큼 펼치는 게 훨씬 효율적이고, 중간층까지 다 키우면 파라미터만 폭증하고 얻는 이득은 없다.
───【핵심】───
Softmax로 확률을 만든 다음, 그 확률대로 뽑기(sampling)를 하는 게 아니다.
정답("은행")을 이미 알고 있으니, 정답 자리에 모델이 몇 %를 부여했는지만 보고 loss를 계산한다.
Loss = -log(정답 단어에 부여된 확률)
• 정답에 80%를 줬다면 → -log(0.8) ≈ 0.22 (loss 작음, 잘 맞춤)
• 정답에 1%를 줬다면 → -log(0.01) ≈ 4.6 (loss 큼, 많이 틀림)
뽑기는 생성(generation) 상황에서 쓰는 개념이다. GPT가 다음 단어를 실제로 만들어낼 때는 확률분포에서 하나를 골라야 하니까. 하지만 MLM은 이미 존재하는 정답을 얼마나 잘 맞추는가를 학습하는 것이라 뽑는 행위 자체가 필요 없다.
마스킹된 자리가 여러 개일 때는 어떻게 되는가 하면, MLM head는 자리 하나당 한 번씩 독립적으로 적용되는 구조라서 예측 대상 자리가 3개면 그 3개 벡터가 각각 똑같은 head(같은 가중치)를 통과해 각자 자기 자리의 확률분포를 만들고 각자 자기 정답과 비교해 loss를 낸 뒤, 그 loss들을 다 더하거나 평균 내서 최종 MLM Loss로 쓴다.
head가 자리 개수만큼 여러 개 있는 게 아니라 같은 head를 여러 벡터에 반복 적용하는 것이다.
[쉽게 다시 말하면]
3단계는 "이 문맥에 어울리는 단어가 뭘까?"라는 질문 벡터를 들고 사전 전체를 훑으면서 "이거랑 얼마나 비슷해?"를 3만 번 물어보는 것이다. 가장 비슷하다고 나온 게 답 후보가 된다. 그리고 채점은 뽑기가 아니라 배점 확인이다. 정답 칸에 몇 점을 줬는지만 보고, 점수가 낮으면 "더 높게 주도록" 모델을 밀어준다.
■ 블록 10. NSP head
입력: [CLS] 자리의 출력 벡터 (768차원, 딱 1개)
내부 연산: Linear+Tanh → Linear(768→2) → Softmax
출력: 2개짜리 확률분포
[구조도 ⑧ — NSP head 내부 (작은 스케일)]
[전문 설명]
NSP head는 MLM head와 골격이 거의 같은데 후보가 두 가지뿐이라 훨씬 단순하다.
1단계. Linear + Tanh (768 → 768)
pooler라고 부르는 층. 문장쌍 요약 벡터를 이 과제에 맞게 한 번 더 다듬는다. MLM head의 1단계와 역할은 비슷하고 활성화 함수만 GELU 대신 Tanh를 쓴다.
2단계. Linear (768 → 2)
출력 노드가 딱 2개. IsNext 점수와 NotNext 점수.
3단계. Softmax
두 점수를 확률로 변환
수식으로 쓰면 다음과 같은데, 여기서 h_[CLS]는 [CLS]의 은닉 벡터이고 W_nsp는 2 × H 크기의 학습되는 행렬이다.
s = W_nsp · tanh(W · h_[CLS] + b)
여기서 Linear가 정확히 뭘 하는 연산인지 짚고 가면, 행렬 곱셈에 덧셈을
더한 것 그 이상도 이하도 아니다.
y = x · W + b
W는 학습되는 가중치 행렬이고 b는 학습되는 편향 벡터인데, 차원이 768에서 768로 그대로 유지되더라도 그 안의 숫자 값 자체는 완전히 새로 계산된다는 점이 중요하다.
W를 곱하는 과정에서 입력의 768개 숫자가 서로 뒤섞이고 재조합되어 새로운 768개 숫자가 나오기 때문에, 개수만 같을 뿐 내용물은 전혀 다른 값이다. 편향 b가 필요한 이유는 x · W만 하면 원점을 지나는 변환만 가능한데 b를 더하면 결과를 원하는 방향으로 밀어서 이동시킬 자유도가 하나 더 생기기 때문이다.
Tanh는 활성화 함수로, 입력값이 뭐든 -1과 1 사이 값으로 눌러 짜내는 역할을 한다.
• 입력이 아주 큰 양수 → 출력은 1에 가까워짐
• 입력이 아주 큰 음수 → 출력은 -1에 가까워짐
• 입력이 0 → 출력도 0
여기서 주의할 게 Tanh는 스케일링이 아니라는 점인데, 스케일링은 모든 값에 일정한 비율을 곱하는 선형 연산인 반면 Tanh는 입력값 크기에 따라 눌리는 정도가 달라서 0 근처에서는 거의 그대로 통과하지만 값이 클수록 훨씬 세게 눌린다.
이렇게 입력 크기에 따라 다르게 반응하는 성질이 바로 비선형성이고, 이게 필요한 이유는 Linear만 계속 쌓으면 선형 변환끼리는 합쳐지는 성질이 있어서 아무리 여러 번 해도 결국 하나의 큰 행렬곱으로 환원되기 때문에 층을 깊게 쌓는 의미 자체가 사라지기 때문이다.
그리고 학습되는 건 Linear 안의 W와 b뿐이고, Tanh나 GELU, Softmax 같은 활성화 함수는 고정된 수학 공식이라 학습되는 파라미터가 전혀 없다. 전체를 신경망 구조로 정리하면 이렇게 된다.
입력층: 768개 노드 ([CLS] 벡터)
은닉층: 768개 노드 (Tanh 통과)
출력층: 2개 노드 (IsNext 점수, NotNext 점수)
즉 흔히 말하는 2층짜리 이진 분류기(binary classifier)이고, MLM head와 비교하면 골격은 완전히 같고 마지막 출력 노드 개수만 다르다. 두 head를 나란히 놓고 보면 후보 개수가 곧 출력 노드 개수가 된다는 원리가 양쪽에 똑같이 적용되었다는 걸 확인할 수 있다.
[표 ③ — MLM head와 NSP head 구조 비교]
[쉽게 다시 말하면]
Linear는 재료(768개 숫자)에 요리법(W)을 적용하는 것이다. 재료 개수는 그대로여도 나오는 음식은 완전히 다르다.
Tanh는 고무줄 같은 것이다. 작은 힘에는 잘 늘어나지만 세게 당길수록 점점 덜 늘어나서, 아무리 당겨도 어느 한계 이상은 안 늘어난다. 값마다 다르게 반응한다는 게 핵심이다.
■ 블록 11. Loss 합산과 역전파
입력: MLM Loss, NSP Loss
내부 연산: 두 loss를 더한 뒤 역전파
출력: 갱신된 모든 파라미터
[전문 설명]
두 head가 각각 cross-entropy loss를 내고, 이 둘을 더해서 하나의 loss로 만든다.
Total Loss = MLM Loss + NSP Loss
그리고 이 하나로 합쳐진 loss로 역전파를 수행해서 인코더 몸통과 두 head는 물론 세 임베딩 테이블까지 전부 한 번에 업데이트하기 때문에, MLM 학습 라운드와 NSP 학습 라운드가 따로 있는 게 아니라 매 스텝마다 두 과제를 동시에 풀면서 그 오차를 합쳐서 학습하는 구조가 된다.
인코더를 두 번 돌리지 않고 한 번만 돌린 뒤 두 head가 그 결과를 나눠 쓰는 이유도 결국 효율성 때문인데, 인코더를 두 번 통과시키면 계산 비용이 두 배가 되기 때문이다. 그리고 이렇게 입력을 공유하다 보니 결과적으로 NSP도 마스킹으로 일부 훼손된 문장을 받게 된다.
───【핵심】───
마스킹으로 정보가 훼손된 문장인데 NSP가 제대로 판단할 수 있나?
• 실제로 [MASK]로 완전히 가려지는 건 전체의 12%(15%의 80%)뿐이다.
10%는 원래 단어 그대로 남고, 10%는 다른 단어로 바뀐다. 즉 대략 86% 정도는 온전하다.
• NSP 과제 자체가 쉽다. 무작위로 뽑은 문장은 대개 주제 자체가 완전히 딴판이라, 단어 몇 개 가려진 정도로는 구별에 지장이 없다.
• 부수 효과로, 불완전한 정보로 판단해야 하니 몇몇 키워드에만 의존하지 않고 문장 전체 흐름을 보게 되는 정규화 효과도 생긴다.
다만 마스킹의 주인은 MLM이다. NSP를 어렵게 하려고 일부러 가린 게 아니라, MLM 때문에 가려진 입력을 NSP가 같이 쓰게 된 것이다.
[쉽게 다시 말하면]
두 과목 시험을 같은 시험지 한 장으로 동시에 보는 것이다. 채점도 따로 하지만 최종 점수는 합산해서 하나로 매기고, 그 하나의 점수로 공부 방향을 조정한다.
마무리. NSP는 필요한가
NSP는 BERT 원 논문(2018)에 처음부터 함께 설계된 것이지 나중에 덧붙인 게 아니다. 저자들은 질의응답이나 문장 함의 판단처럼 두 문장의 관계를 봐야 하는 downstream 작업을 염두에 두고, 그런 능력을 사전학습 단계에서 미리 심어주려는 의도로 NSP를 처음부터 설계에 포함시켰다.
그런데 후속 연구에서 검증해보니 얘기가 달라졌는데, RoBERTa와 XLNet의 실험에서 NSP가 downstream 성능에 긍정적 효과가 없는 경우가 많다는 것이 밝혀졌고 두 논문 모두 MLM만으로 사전학습할 것을 권고했다. 실제로 이후에 나온 모델들은 NSP를 대부분 빼거나 다른 과제로 대체했다.
즉 NSP는 설계 시점엔 핵심 의도였지만 검증 결과 필수적이지 않은 것으로 밝혀진 요소라고 정리할 수 있다.
그러면 NSP가 빠지면 나머지 구조는 어떻게 되느냐 하면, 대부분은 그대로 필요하다.
• Token / Position embedding — 여전히 필수
• 인코더 스택 전체 — 여전히 필수
• [MASK] 토큰 — MLM의 핵심이니 필수
• [SEP] 토큰 — 문장 쌍 입력 작업에서는 여전히 필요
• [CLS] 토큰 — fine-tuning에서 문장 분류할 때 여전히 유용. 다만 NSP라는 사전학습 신호가 없으면 [CLS] 벡터가 잘 훈련되지 않아서 평균 풀링 등으로 대체하기도 한다
• Segment embedding — 가장 애매한 부분. 필요성은 줄지만 문장 쌍 입력 작업이 있으니 완전히 없애지는 않는다. RoBERTa는 NSP를 빼면서 입력 구성 방식 자체를 바꿨다
다음은 인코더-디코더 구조(T5류)다. 지금까지 본 인코더와 지난번에 본 디코더가 Cross-Attention으로 어떻게 결합되는지를 볼 차례다.
추가
1. 텍스트를 임베딩 벡터로 변환하는 과정과 영상을 임베딩 벡터로 변환하는 과정을 비교설명하라
텍스트는 단어나 서브워드같은 나누기 쉬운 단위로 자연스럽게 나뉘어 있어서 필요한 만큼 쪼개 어휘사전을 구성하고, 쪼개진 각 토큰을 어휘사전 상의 인덱스로 매핑한 뒤, 학습된 임베딩 테이블에서 해당 인덱스의 행을 조회(lookup)하는 방식으로 벡터를 얻는다.
그러나 이미지는 픽셀 단위로 연속적으로 이어져 있어 자연스러운 경계가 존재하지 않기 때문에 이미지를 임의의 크기인 16×16 픽셀과 같은 고정 크기의 패치로 분할한 뒤, 각 패치를 평탄화하(펼치)고 학습 가능한 선형 변환을 적용하여 벡터로 만든다.
즉 텍스트는 사전 조회 방식을, 이미지는 패치 분할 후 선형 투영 방식을 사용한다는 점이 핵심적인 차이이다.
두 경우 모두 이 변환 과정 자체에는 순서 정보가 반영되지 않으므로, 이후 Position embedding을 더하여 순서 또는 위치 정보를 별도로 주입해야 하고, 이 과정을 거치고 나면 텍스트와 이미지 모두 동일하게 "벡터의 시퀀스"라는 형태를 가지게 되며, 이후에 두 모달리티에 대해 구조적으로 동일한 self-attention 연산(과정)이 적용된다.
2. 어텐션에서 Query, Key, Value 의 의미를 설명하라
Query, Key, Value는 같은 입력 벡터 x에 서로 다른 학습 가중치 Wq, Wk, Wv를 곱해 만들어지는 세 벡터로, 하나의 토큰이 동시에 세 가지 역할을 수행하도록 변환한 것이다. Query는 "지금 이 토큰이 무엇을 찾고자 하는가"를 나타내는 역할이고, Key는 "각 토큰이 자신이 어떤 정보를 가지고 있는지 표시하는" 역할이다. Query와 Key를 내적해 얻은 값은 이 질의가 어느 토큰의 색인과 얼마나 잘 맞는지를 나타내는 유사도 점수이며, 이를 softmax로 정규화해 가중치로 바뀐다. Value는 다음 층에 실제로 전달되는 벡터 자체이며, 앞서 구한 가중치로 모든 토큰의 Value를 가중합해 최종 출력을 얻는다. 또한 Query와 Key를 서로 다른 가중치로 분리하는 이유는 관계의 방향성을 표현하기 위함이다. 만약 Wq와 Wk가 같다면, 토큰 A가 B를 참고하는 점수(Q_A·K_B)와 토큰 B가 A를 참고하는 점수(Q_B·K_A)가 수학적으로 항상 같아진다. 같은 가중치로 만든 벡터끼리의 내적은 대칭이기 때문이다. 이렇게 되면 "그것(it)"이 앞의 명사를 강하게 참고해야 하는데 그 명사는 "그것"을 그만큼 참고할 필요가 없는 것과 같은 비대칭적 관계를 표현할 수 없다. Wq와 Wk를 별도로 두어야 A→B 점수와 B→A 점수가 서로 다르게 나올 수 있고, 이를 통해 방향성 있는 관계가 가능해진다.
Key와 Value를 굳이 분리하는 이유는 이와는 다른 문제로, "어디를 참고할지 결정하는 기준"과 "실제로 참고해서 가져올 내용"을 서로 다른 역할로 나눠 모델이 이 둘을 독립적으로 최적화할 수 있게 하기 위함이다.
3. 싱글헤드 어텐션(single head attention)과 멀티헤드 어텐션(multi head attention)의 차이를 설명하라
싱글헤드 어텐션은 입력 벡터로부터 Query, Key, Value를 각각 하나씩만 만들어 attention을 한 번만 계산하는 방식이다. 반면 멀티헤드 어텐션은 이 계산을 여러 개의 헤드로 나눠 병렬로 수행하는 방식으로, 각 헤드가 서로 다른 Wq, Wk, Wv를 가지고 독립적으로 attention을 계산한 뒤 그 결과를 이어붙이고 다시 한 번 선형 변환한다. 이때 전체 차원을 헤드 수만큼 나눠 쓰므로(예: 768차원을 12헤드로 나누면 헤드당 64차원) 전체 연산량은 싱글헤드와 비슷한 수준으로 유지된다. 헤드를 나누는 이유는 한 토큰이 동시에 여러 종류의 서로 다른 관계(주어 관계, 목적어 관계, 의미 중의성 등)를 맺기 때문인데, 헤드가 하나뿐이면 이 관계들이 하나의 가중치 분포로 뭉뚱그려져 서로 충돌하거나 희석될 수 있다. 실제로 학습된 BERT를 분석한 연구에서는 헤드마다 구문 관계, 앞 문장에 나온 인물을 다시 가리키는 대명사가 실제로 어느 명사를 가리키는지 찾아내는 관계, 인접 토큰 관계 등으로 역할이 자연스럽게 나뉘는 현상이 관찰되었다(Clark et al., 2019).
4. 셀프 어텐션(self attention)과 크로스 어텐션(cross attention)의 차이를 설명하라
두 방식 모두 Q, K, V를 만들어 유사도를 구하고 가중합하는 연산 절차는 동일하며, 차이는 Q, K, V를 어느 시퀀스에서 만드는지에서 생긴다.
셀프 어텐션은 하나의 시퀀스 x에 Wq, Wk, Wv를 각각 곱해 Q, K, V를 전부 x에서 만들어 시퀀스 안의 각 토큰이 자기 자신을 포함한 같은 시퀀스의 모든 토큰을 향해 질의를 던지고 답을 받는다. 예를 들어 "나는 은행에 갔다"에서 "은행"의 Query는 같은 문장의 다른 토큰들의 Key와 비교되고, 그 가중치만큼 각 토큰의 Value가 섞여 "은행"의 새 벡터가 만들어진다. 입력과 출력이 같은 하나의 시퀀스이며, 토큰마다 새 벡터가 하나씩 나와 개수와 순서가 그대로 유지된다.
크로스 어텐션은 시퀀스가 두 개일 때, 한쪽(예: 디코더)에는 Wq만 곱해 Q를 만들고, 다른 쪽(예: 인코더 출력)에는 Wk, Wv를 곱해 K, V를 만든다. 그래서 한쪽 시퀀스의 각 토큰이 자신이 속한 시퀀스가 아니라 다른 쪽 시퀀스를 향해 질의를 던진다. 디코더가 다음 단어를 생성할 때 디코더 벡터로 Q를, 인코더 출력으로 K, V를 만들어 "지금 이 자리를 채우려면 원문의 어느 부분을 봐야 하는가"를 찾아오는 것이 대표적인 예다. 또한 출력 길이는 Q를 만든 쪽을 따르고 참고 대상 개수는 K, V를 만든 쪽을 따르므로, 두 시퀀스의 길이가 달라도 계산에 문제가 없다.
정리하면 셀프 어텐션은 Q, K, V가 모두 한 시퀀스에서 나와 자기 자신을 참고하는 구조이고, 크로스 어텐션은 Q는 시퀀스 A에서, K와 V는 시퀀스 B에서 나와 A가 B를 참고하는 구조이다. 이러한 구조의 차이는, 셀프 어텐션은 한 시퀀스 내부의 문맥을 스스로 파악하려 하고 크로스 어텐션은 서로 다른 두 시퀀스를 연결해 A가 필요로 하는 정보를 B에서 가져오려 한다는 의도 차이에서 생긴 것으로, 연산식 자체는 동일하며 Q, K, V의 출처가 다르다는 점만이 두 구조를 가르는 차이다.
|
|

첫댓글 1. 텍스트를 임베딩 벡터로 변환하는 과정과 ViT에서 영상을 임베딩 벡터로 변환하는 과정을 비교설명하라
2. 어텐션에서 Query, Key, Value 의 의미를 설명하라
3. 싱글헤드 어텐션(single head attention)과 멀티헤드 어텐션(multi head attention)의 차이를 설명하라
4. 셀프 어텐션(self attention)과 크로스 어텐션(cross attention)의 차이를 설명하라
본문에 추가했습니다!