- GPT식 디코더 전용 Transformer에서 학습 가능한 셀프 어텐션은 각 토큰이 앞선 입력의 어떤 토큰에 주목해야 할지 계산해 문맥 벡터를 만듦
- 핵심은 스케일드 닷 프로덕트 어텐션으로, 입력 임베딩을 query, key, value 공간으로 보내는 세 학습 행렬
Wq,Wk,Wv를 사용함 - 입력 행렬
X는Q=XWq,K=XWk,V=XWv로 변환되고,Ω=QKᵀ를√c로 나눈 뒤 행 단위 softmax로 어텐션 가중치A를 얻음 - 문맥 벡터는
C=AV라는 한 번의 행렬 곱으로 만들어지며, 전체 계산은 5번의 행렬 곱과 전치 하나로 모든 토큰에 적용 가능함 - 이 단계는 입력 임베딩끼리 직접 dot product를 하던 장난감 예제를 넘어, PyTorch
nn.Module과nn.Linear로 구현할 수 있는 훈련 가능한 어텐션으로 이어짐
LLM 처리 흐름에서 셀프 어텐션의 위치
- GPT식 디코더 전용 Transformer 기반 LLM은 지금까지의 토큰을 보고 다음 토큰을 예측하는 구조임
- 처리 흐름은 문자열을 토큰으로 나누고, 각 토큰을 토큰 임베딩으로 바꾼 뒤, 위치 정보를 나타내는 위치 임베딩을 더해 입력 임베딩을 만드는 순서임
- 셀프 어텐션은 각 입력 임베딩에 대해 다른 토큰들에 얼마나 주목할지를 나타내는 어텐션 점수 목록을 생성함
- 예시 문장
"the fat cat sat on the mat"에서"cat"을 볼 때"fat"은 중요할 수 있음 "mat"을 볼 때는"fat"의 중요도가 상대적으로 낮을 수 있음
- 예시 문장
- 어텐션 점수는 softmax를 거쳐 합이 1인 어텐션 가중치가 되고, 이 가중치로 입력 임베딩들을 가중합해 문맥 벡터를 만듦
- 문맥 벡터는 각 토큰의 의미를 입력 전체의 문맥 속에서 표현하는 벡터로 다뤄짐
학습 가능한 셀프 어텐션의 목표
- 이전 단계까지는 입력 임베딩끼리 직접 dot product를 계산하는 장난감 셀프 어텐션을 사용함
- 이번 단계의 목표는 입력 벡터에서 어텐션 점수를 만들 수 있는 학습 가능한 어텐션 메커니즘을 구성하는 것임
- Sebastian Raschka의 Build a Large Language Model (from Scratch) 3.4절은 이를 scaled dot product attention으로 구현함
- 초점은 왜 이 구조가 효과적인지보다, 어떤 계산으로 동작하는지에 맞춰져 있음
Query, Key, Value 행렬과 공간 투영
- 입력 시퀀스 길이를
n, 입력 임베딩 차원을d, 문맥 벡터 차원을c로 둠 - 입력 임베딩 시퀀스는
x1, x2, x3, ... xn으로 표현되고, 각 입력 임베딩은d차원 벡터임 - 세 개의 학습 가능한 가중치 행렬을 정의함
- query weights matrix:
Wq - key weights matrix:
Wk - value weights matrix:
Wv
- query weights matrix:
- 각 행렬은
d×c크기이며,d차원 입력 벡터를c차원 공간으로 투영함 - 입력 벡터
xm을 query 공간으로 보내는 계산은qm=xmWq임 - key 공간과 value 공간도 같은 방식으로 입력 임베딩을 각각 다른
c차원 공간으로 투영함
행렬을 투영으로 보는 방식
- 행렬은 점을 회전시키는 등 기하학적 변환에 사용할 수 있음
- 정사각 행렬은 같은 차원 안에서 변환을 수행하고, 정사각이 아닌 행렬은 벡터를 다른 차원 공간으로 보낼 수 있음
- 예를 들어
3×2행렬은 3차원 벡터들을 2차원 벡터들로 변환할 수 있음 - 3D 그래픽에서 3D 점을 2D 화면의 점으로 변환하는 frustum 행렬도 이런 투영의 예로 사용됨
- 셀프 어텐션은 입력 임베딩을 query, key, value라는 세 개의 서로 다른 투영 공간으로 보낸 뒤, 투영된 벡터들로 계산을 진행함
- 이 투영 행렬들은 훈련 중 학습되므로, 단순 dot product 어텐션에는 없던 간접성이 생김
어텐션 점수 계산
- 특정 입력
xm을 고려할 때, 다른 입력xp에 대한 어텐션 점수는 query 투영과 key 투영의 dot product로 정의됨 - 계산식은 다음과 같음
qm=xmWqkp=xpWkωm,p=qm·kp
- 모든 입력에 대해 이 계산을 반복문으로 처리할 수도 있지만, 행렬 곱을 쓰면 한 번에 계산할 수 있음
- 입력 임베딩 전체를 행렬
X로 두면,X는n×d크기임 - key 행렬은
K=XWk로 한 번에 계산됨- 결과
K는n×c크기 - 각 행은 해당 입력 임베딩을 key 공간으로 투영한 벡터임
- 결과
- query 행렬도 같은 방식으로
Q=XWq로 계산됨 - 모든 query와 모든 key 사이의 dot product는
QKᵀ로 얻음Q는n×cKᵀ는c×n- 결과
Ω는n×n
Ωm,p는xm의 문맥 벡터를 만들 때xp에 얼마나 주목할지를 나타내는 어텐션 점수임
스케일링과 softmax 정규화
- 어텐션 점수는 이전 예제처럼 softmax를 거쳐 합이 1인 가중치로 바뀜
- softmax는 큰 값은 더 키우고 작은 값은 낮추면서, 목록 전체의 합이 1이 되도록 조정함
- 실제 LLM에서는
d와c가 수천 단위가 될 수 있어, 순수 softmax만 쓰면 작은 gradient가 생길 수 있음 - 이 경우 softmax가 “step function처럼” 동작할 수 있음
- 가장 큰 값이 지배하고 나머지 값들이 매우 작아지는 상황으로 해석됨
- 이를 완화하기 위해 어텐션 점수를 투영 공간 차원
c의 제곱근으로 나눈 뒤 softmax를 적용함 - 행렬 표현은 다음과 같음
A=softmax(Ω/√c, axis=1)
axis=1은 PyTorch식 표기로, softmax를 행 단위로 적용한다는 뜻임- 결과
A는 정규화된 어텐션 점수, 즉 어텐션 가중치 행렬임
문맥 벡터 생성
- value 공간 투영은
V=XWv로 계산함 A는n×n크기의 어텐션 가중치 행렬임Am,p는xm의 문맥 벡터를 만들 때 입력p에 적용할 어텐션 가중치임
V는n×c크기이며, 각 행은 입력 임베딩을 value 공간으로 투영한 벡터임- 문맥 벡터 행렬은
C=AV로 계산됨- 결과
C는n×c C의m번째 행은 입력xm에 대한 문맥 벡터임
- 결과
- 이 계산은 각 토큰에 대해 value 벡터들을 어텐션 가중치로 곱해 더하는 작업을 행렬 곱 하나로 수행함
전체 계산 요약
- 입력 행렬
X는 토큰 시퀀스의 입력 임베딩을 담고 있으며, 크기는n×d임 - 세 개의 학습 가능한 행렬로 입력을 각각 query, key, value 공간으로 투영함
Q=XWqK=XWkV=XWv
- query와 key의 dot product로 어텐션 점수를 계산함
Ω=QKᵀ
- 점수를 스케일링한 뒤 행 단위 softmax를 적용해 어텐션 가중치를 만듦
A=softmax(Ω/√c, axis=1)
- value 투영과 어텐션 가중치를 곱해 문맥 벡터를 생성함
C=AV
- 전체 셀프 어텐션 메커니즘은 5번의 행렬 곱과 하나의 전치로 모든 입력 토큰의 문맥 벡터를 만들 수 있음
PyTorch 구현과 다음 단계
- 책의 3.4절은 위 계산을 PyTorch 코드로 구현하고, 동일한 행렬 연산을 수행하는 간단한
nn.Module서브클래스를 만듦 - 첫 번째 버전은 세 개의 가중치 행렬에 일반
nn.Parameter객체를 사용함 - 두 번째 버전은 더 효과적인 훈련을 위해
nn.Linear를 사용함 - 이후 다룰 주제는 두 가지임
- causal self-attention: 특정 토큰을 볼 때 이후 토큰에는 주목하지 않는 방식
- multi-head attention: 처음 생각했던 것만큼 복잡하지 않은 주제로 예고됨
- 배치 처리는 별도 고민거리로 남아 있음
- 단일 입력 시퀀스에서도 어텐션 점수 행렬을 사용함
- 여러 입력 시퀀스를 병렬 처리하려면 행렬보다 높은 차수의 텐서가 필요할 수 있음
- 다음 글은 Writing an LLM from scratch, part 9 -- causal attention로 이어짐