GPT 모델 만들기(2)
셀프 어텐션 추가하기
어텐션 메커니즘은 간단히 말해 문자나 단어 사이의 관계를 파악하고, 특정 정보의 중요성을 인식하는 메커니즘이다. 이 메커니즘을 이해하려면 두 가지 핵심 질문을 고민해야 한다.
- 어떻게 단어 사이의 관계를 파악할 수 있을까?
- 어떻게 특정 정보의 중요성을 모델에 전달할 수 있을까?
이러한 고민을 실제 코드로 구현해 보면 어텐션 메커니즘의 작동 원리를 더 깊이 이해할 수 있다. 코드를 통해 이론적 개념을 실제로 적용해 보면서 어텐션 메커니즘이 어떻게 작동하는지 더 명확하게 파악할 수 있다.
먼저 문자들 간의 정보를 주고받는 방법을 생각해보겠다.
문자들 간에 정보를 주고받는 방식(평균 방식)
간단한 숫자 데이터를 가지고, 문자들 간에 정보를 주고받는 방법을 살펴보겠다.
배치(batch) 크기가 2이고, 시퀀스(sequence) 길이가 4, 그리고 임베딩(embedding) 차원이 6인 데이터를 생성한다. 데이터의 내용은 중요하지 않기 떄문에 torch.randn
함수를 사용해 랜덤한 값으로 데이터를 만듭니다. 이렇게 생성된 4개의 시퀀스는 서로 연관성이 없는 랜덤한 숫자들로 구성된다.
import torch
torch.manual_seed(1441)
num_batches, seqence_length, embedding_dim = 2, 4, 6
embeddings_tensor = torch.randn(num_batches,
seqence_length,
embedding_dim)
embeddings_tensor.shape
torch.Size([2, 4, 6])
이 코드의 목표는 더 나은 예측을 위해 시퀀스들이 서로 어떻게 정보를 주고받을 수 있는지를 알아보는 것이다. 여기서 주목할 점은 4개의 시퀀스가 순차적으로 입력된다는 것이다. 시퀀스들끼리 정보를 주고받는 방법은 코사인 유사도 등 다양하지만, 여기서는 가장 쉬운 방법인 평균을 구하는 방식으로 설명하겠다.
다음으로, embeddings_tensor 를 활용해 averaged_embeddings 라는 변수를 생성한다. 이 변수는 다음 시퀀스로 넘어갈 때마다 평균값을 사용하도록 설계된다.
# 이전 임베딩의 평균을 저장할 텐서 초기화
averaged_embeddings = torch.zeros((num_batches, sequence_length, embedding_dim))
# 각 배치에 대해 반복
for batch_index in range(num_batches):
# 각 시퀀스 위치에 대해 반복
for sequence_position in range(sequence_length):
# 현재 시퀀스 위치까지의 이전 임베딩을 슬라이스
previous_embeddings = embeddings_tensor[batch_index, :sequence_position + 1]
# 현재 위치까지의 임베딩의 평균을 계산
averaged_embeddings[batch_index, sequence_position] = torch.mean(
previous_embeddings,
dim=0
)
이 과정에서 각 시퀀스의 정보를 압축해 다음 시퀀스로 전달할 수 있다. 임베딩의 중요성이 여기서 드러난다. 임베딩은 단어나 문자를 숫자 벡터로 표현하는 방법이다. 왕을 나타내는 입베딩과 여자를 나타내는 임베딩을 더하면 여왕의 임베딩이 나오는 것을 의미한다. 이러한 임베딩 벡터들의 평균을 사용하면 이전 정보의 특성을 효과적으로 요약할 수 있다. 시퀀스 내 각 시점에서 이전의 모든 문자의 정보를 모아 평균을 계산함으로써 정보를 집계하고 문맥을 반영한다.
예를 들어, "나는 학교에 간다"라는 문장에서 '간다'를 해석할 때 이전 단어들의 임베딩 벡터의 평균은 '나', '는', '학교', '에'의 의미를 포함한 새로운 벡터가 된다. 이 평균 벡터는 문장의 전반적인 문맥을 나타내며, 모델이 '간다'와 같은 다음 단어를 더 정확하게 예측하거나 이해하는 데 도움을 준다.
이 방법은 간단하지만 시퀀스 내에서 이전 정보를 현재에 효과적으로 전달하는 방법으로 사용된다.
averaged_embeddings[0]
의 값은 시퀀스가 증가함에 따라 변화한다. 하지만 for 문을 사용하는 방식은 시간 복잡도 문제로 인해 효율적이지 않다. 대신 행렬곱을 활용해 이 과정을 더욱 간단하고 효율적으로 수행할 수 있다.
셀프 어텐션이란?
시퀀스가 증가하면서 의미 정보를 어떻게 전달할 수 있는지 알아봤고 마스크를 사용해 모델이 중요한 정보에 집중할 수 있게 하는 과정을 살펴봤다.
이제 본격적으로 셀프 어텐션에 대해 알아보겠다. 셀프 어텐션은 입력 시퀀스(문장)내 모든 단어 간의 관계를 직접 분석하고 처리한다.
이 과정에서 각 단어가 다른 모든 단어와 어떻게 상호작용하는지 계산하며, 단어들 간의 유사도를 측정해 연관성 높은 단어 쌍을 파악한다. 이를 통해 문장 내 단어들 사이의 복잡한 연관성을 포착하고 이해한다.
입력 시퀀스(문장)를 쿼리(Query, Q), 키(Key, K), 밸류(Value, V) 세 개로 복사한다. Query는 질문하는 역할을 하는 문장으로 생각하면 된다.
- Key는 Query가 한 질문에 답변하는 역할을 하는 문장이다. Value는 실제 전달되는 정보를 나타낸다.
- Query의 질문이 행렬로 들어오므로 Key는 행렬 연산을 위해 전치(Transpose)되고 행렬 연산이 진행된다. 이 과정에서 Query와 Key의 관련성을 계산한다. 행렬 연산으로 tensor의 크기가 변경되는데, 이를 다시 복원하기 위해 Value와 연산을 진행한다.
- 또한, Value는 Query와 Key의 관련성에 따라 가중치가 부여되어 최종 출력을 생성하는 데 사용된다.
결과적으로 각