LLM 동작에 대해서 지난 글에서는 간략한 개요와, Embedding에 대해서 알아봤습니다. 실제 Embedding의 동작에 대해서 깊게 알아보려면 많은 이해가 필요합니다만, 개념적인 이해를 위해서 최대한 간단하고 쉽게 작성하려고 해 봤습니다. 아. 우리가 말하는 AI라는 것이 LLM이라는 것을 쓰는데, 이렇게 동작하는구나? 정도의 이해를 할 수 있을 정도로요. 😀
지난 글은 아래를 참고해주세요.
https://unnamed-underdogs.tistory.com/28
LLM은 어떻게 동작할까 - Embedding
우리가 흔히 말하는 GPT나 Deepseek과 같은 "AI"들은 쉽게 말해서 LLM (Large Language Model)이라는 "모델"을 GPU에서 구동하는 것입니다. 이 LLM이라는 것은 "생성형 AI"의 한 종류로, 인간의 언어(자연어)를
unnamed-underdogs.tistory.com
이어서 2편을 시작해 보겠습니다. 이번에는 Positional Encoding이라는 것에 대해서 알아보려고 합니다. 이름에서 알 수 있듯이 위치에 대한 뭔가 처리를 하는 그런 것입니다.
우리가 AI(LLM)에 뭔가를 요청할 때 텍스트를 주면, LLM은 이를 토큰이라는 것으로 변환한다고 했었죠? (Embedding). 그런데 우리가 주는 문장을 보면, 중요한 부분이 있죠. 문장들의 순서입니다. 이 순서가 잘 못되면 문장을 이해하기 어렵잖아요? 그런데 embedding 과정에서는 단순히 단어를 토큰(+정보)으로 변환을 했을 뿐, 문장을 구성하는 각 토큰(단어)들의 순서에 대한 정보는 없습니다.
그것을 해결하기 위한 것이 바로, Positional Encoding인 것이죠. LLM이라는 것이 들여다 보면 재밌는 부분 중 하나는, 사람이 생각하는 것을 기계가 어떻게 동일하게 할 수 있을지를 연구하는 것이라고 생각합니다. 😁
Positional Encoding이라는 것을 간단히 설명하면, 각 단어가 문장속에서 어느 위치에 있는지를 알려주는 것입니다.
"안녕, 나는 고양이를 좋아해"라는 문장이,
토큰화를 거치면 [409, 12, 25, 7, 99, 101, 103] 처럼 토큰 ID로 변환이 되고요,
Positional 정보는 [0, 1, 2, 3, 4, 5, 6]이 되는 것입니다. (문장 내 각 단어의 위치 정보, 여기서는 그냥 예시입니다.)
그리고 이 두 정보를 "결합" 해서 단어 임베딩에 위치 정보를 반영하게 되고 이것이 모델 (Transformer)의 입력으로 들어가게 되는 것입니다.
만약 position 정보가 없다면, "안녕/나/는/고양이/를/좋아해" 라는 문장과 "안녕/고양이/는/나/를/좋아해"를 LLM에서는 구분할 수 없게 되는 것이죠. 고양이라는 단어(토큰)와 나라는 단어(토큰)의 위치가 어디 인지 알 수가 없거든요. 그래서 이러한 정보들을 가지고 모델은 학습을 하면서, 문장 처음에 "고양이"라는 단어가 올 때 어떤 "확률"로 다음 단어가 되는지, 또는 문장 중간에 "고양이"라는 단어가 오면 어떤 구문이 자연스러운지 등을 "배우게" 되는 것입니다.
이러한 position 정보를 만드는 방법은 크게 2가지 입니다.
- sin/cos 기반 (Sinusoidal Encoding)
- 학습형 벡터 (Learned Positional Encoding)
sin/cos 나오니까 벌써 머리가 아프시다고요? 네 저도 그렇습니다. 그래서 최대한 간단하게 살펴보겠습니다. 한 번 같이 보시죠.
Sinusoidal Encoding
먼저 sin/cos 기반의 인코딩은, 문장 내 위치를 수학적으로 표현해 보자! 에서부터 시작합니다. Attention is all you need라는 논문에서 처음 제안된 기법으로, 각 단어가 문장 내 몇 번째 위치인지(0번, 1번, 2번…)를 사인(sin), 코사인(cos) 함수를 이용해 벡터로 나타내는 것입니다.
머리 꽉 잡으세요 수식 나갑니다.
위치 = pos, 임베딩 차원에서의 인덱스 = i (짝수/홀수), 모델의 임베딩 차원 = d_model 이라고 할 때, 아래와 같습니다.
개념적으로 간단하게 살펴 볼께요.
sin, cos은 "주기적"으로 오르락, 내리락하는 모양을 나타냅니다. sin/cos 그래프는 많이들 보셨으리라 생각합니다.
여기서 sin, cos을 사용하는 이유는 "주기적으로 반복되는 패턴"을 수학적으로 나타낸 것일 뿐입니다. 그리고 10000 {...} 이 부분은 그 주기적으로 반복되는 패턴(파동)을 어떤 주기로 할 것인지를 결정하는 값인 것이죠. 이 값이 작은 값이면 빨리 변하는 파동, 큰 값이면 느리게 변하는 파동이 되는 것입니다.
예를 들어보면, 안녕 -> 토큰 ID: 409번이 되고, 위치는 0일 때, 임베딩 차원이 4096이라고 하면,
임베딩 차원 4096개에 대해서,
임베딩 차원 0 -> sin(),
임베딩 차원 1 -> cos(),
...
임베딩 차원 4094 -> sin(),
임베딩 차원 4095 -> cos() 을 수행하면 4096개의 어떤 값이 나오겠죠? 그 값들을 모두 더한 값이 토큰 409번의 Positional Encoding 값이 되는 것입니다. Positional encoding을 구글 이미지 검색해 보면, 아래처럼 그라디에이션 된 색으로 표시된 그래프를 볼 수 있는데요. 이렇게 이해하시면 쉽습니다. sin/cos으로 생성된 각 파동이 각각의 LED라고 생각해 보세요. 이 LED는 자신만의 "주기"로 색깔이 계속해서 바뀐다고 가정해 보면요,
0번 LED는 1초 주기로 빨->노->파->빨,
1번 LED는 0.5초 주기로 빨->노->파->빨,
2번 LED는 2초 주기로 빨->노->파->빨,
그럼 특정 시간에 이 LED들이 비추는 곳을 보면, 어떤 특정 "색깔"을 갖고 있겠죠? 매 특정 시점마다 이 색깔은 모두 다른 값을 가지게 될 것입니다. 모델은 이 "색깔"을 보고 지금 시점의 시간이 무엇인지 "식별"할 수 있게 되는 것입니다. 즉 이렇게 여러 파동들이 합쳐진 값을 보고 이 토큰의 위치가 어디인지를 식별하게 되는 것이죠.
임베딩 차원이 8이라고 하면 이런 식의 그래프가 됩니다.
그리고 임베딩 차원이 512이라고 하면, 이렇게 복잡한 그래프가 되는거죠.
(예제 코드로 표현한 것이므로, 실제와 다를 수 있습니다. 개념적으로 참고만 하세요 😤)
이렇게 수식으로 표현할 수 있기 때문에, 추가적인 파라미터가 필요 없게 되고, 상대적으로 가볍고 "규칙적"방식이 됩니다. 즉 이런 규칙적인 방식이 되기 때문에 패턴이 고정되니까, 어떨 때에는 더 "유연한" 방식이 필요할 수 있겠죠? 그래서 나온 방식이 위치별로 벡터를 직접 "학습"하는 방식입니다.
Learned Positional Encoding
이 방식은 단어 임베딩과 유사합니다. pos_embedding_maxrix가 하나 있어야 하고요. 문장에서 0번 위치는 행 0, 1번 위치는 행 1... 이런 식으로 인덱싱 해서, 해당 위치에 있는 정보를 가져오는 것입니다. 해당 위치에 있는 정보는 당연히 "학습"시 업데이트 되겠죠? -> 데이터 상, "첫 번째 단어는 이런 특성을 반영하면 예측이 더 좋아진다"와 같은 방식으로 이 정보(위치 벡터)가 계속 업데이트되는 것입니다. 이렇게 학습에 의해서 정보가 업데이트되기 때문에, 어떤 위치에서 어떤 벡터가 좋을지 스스로 결정할 수 있는 유연성이 확보되겠죠. 하지만 각 위치마다 파라미터가 추가로 필요하기 때문에, 추가로 메모리를 사용해야 하는 단점이 있습니다.
지금까지 살펴본 Sinusoidal Encoding 방식과 Learned Positional Encoding 방식 모두 공통적 속성이 있습니다. 두 방식 모두 절대적 위치(예: 0번 토큰, 1번 토큰…)”를 직접 벡터에 담는다는 것입니다. 따라서 문장의 일부를 잘라내거나, 중간에 문장을 삽입하거나 하면 이 절대 위치가 바뀌어버리겠죠? 그럼 모델이 대응하기 힘들어질거구요. 이를 해결하기 위한 상대적 위치 인코딩(Relative PE) 방식으로 등장한 기법이 RoPE (Rotary Position Embedding)입니다.
RoPE (Rotary Position Embedding)
임베딩 차원을 짝수/2개씩 쌍으로 묶어, 2차원씩 “회전 행렬”을 적용하는 것입니다. 즉 쉽게 말하면 "벡터를 회전"시키는 것이죠.
머리 아프지 않게 개념적으로 이해할 수 있게 살짝 살펴 보겠습니다. 😃
벡터를 회전시킨다는 것은 아래 그림과 같습니다.
(x, y)라는 좌표가 있을 때, 어떤 각도 (θ) 만큼 돌려서 새로운 좌표 (x', y')를 구하는 것이죠. RoPE에서는 문장 내 토큰의 위치가 바뀌면 이 회전각도(θ)가 달라집니다. 따라서, 같은 단어라도 문장 앞쪽에 있을 때와 뒤쪽에 있을 때, 벡터가 살짝 다른 방향으로 회전하게 되는 것이죠. 결과적으로, “어느 두 토큰이 얼마나 떨어져 있느냐(상대적 거리)”가 서로 다른 회전 상태로 나타나게 됩니다. 모델은 “다른 각도로 돌려진 벡터들”을 보고, 문장 내 위치 정보를 학습할 수 있습니다.
RoPE가 동작하는 방식은 간단합니다. 임베딩 차원이 D라면, D/2개의 2D 쌍으로 나눕니다. 예를 들어 임베딩 차원(dim)=768 이면, 384개의 2D 쌍을 만다는 것이죠. 임베딩 차원이 768이라는 것은 지난 글에서 살펴 봤듯이, 한 토큰(단어)를 표현하기 위해 "768개의 숫자"를 쓰고 있다는 뜻입니다. RoPE에서는 “2차원(2D)마다 한 쌍”으로 묶어서 (x, y) 형태로 보고, 이를 빙글 돌리는 “회전”을 합니다. 즉, 768차원을 (x₀,y₀), (x₁,y₁), … (x₃₈₃,y₃₈₃)처럼 384개 쌍으로 쪼갠 뒤, 각 쌍을 “회전 행렬”에 따라 돌려주면, 위치(pos)에 따라 다른 벡터가 만들어지는 원리예요. 그리고 문장 내 0번 단어, 1번 단어, … 각 위치(pos)마다, “이 위치에서 (x,y)를 몇 도(또는 몇 라디안)만큼 회전할지”라는 각도를 정해둡니다(또는 미리 배열로 준비). 이렇게요. pos=0 → θ=0 (회전 없음), pos=1 → θ=0.01, pos=2 → θ=0.02... 이렇게 하는 이유는 간단하겠죠? 문장 속에서 앞뒤 토큰의 상대적 위치 차이”를 ‘회전 상태(위상)’로 구분하려는 것입니다. 모델이 이 회전 상태를 보고 이 토큰의 위치가 어디인지나, 2개의 토큰의 거리가 얼마나 되는지 등을 아주 쉽게 알 수 있는 것이죠! 😄
(물론 좀 더 복잡한 공식을 적용할 수도 있습니다.)
그 후 self-attention 계산 시, Query[pos]와 Key[pos] 벡터에 RoPE를 적용해서 "회전"된 Q', K' 를획득하고, 이후 Q' x K' 점곱(dot product)에서, 위치 차이에 따른 위상(phase) 변화를 반영하게 됩니다. 즉 위치 정보가 녹아 들어간 벡터가 된다는 것이죠.
갑자기 self-attention이니 Query니 Key가 나와서 당황스러울 수 있는데,
아직 self-attention에 대해서는 살펴보지 않았으니, 이 부분은 다음 글에서 설명하겠습니다. 다만, 회전된 Q와 K 벡터 간 내적을 구하면, “pos1 vs pos2” 토큰의 상대적 거리/순서를 구할 수 있게 되는데, RoPE는 “절대 위치” 대신 “상대적 각도 차이”를 통해 어텐션에 위치 정보를 주입한다!라고 이해하면 좋습니다.
수식이 아주 조금 약간 있긴 했지만, 많은 수학적 지식 필요 없이 LLM (Transformer) 동작을 이해하실 수 있게 되셨을거라 생각합니다.
이 그림이 바로 transformer model의 아키텍처입니다. 우리가 사용하는 대부분의 AI (LLM)이 기본적으로 이러한 구조를 채택하고 있습니다. 지난번에 이어서 이번 글에서 살펴본 부분이 바로 그림의 맨 아래에 있는 Input Embedding과 Positional Encoding 부분인 것이죠.
다음 글에서는 self-attention을 살펴보겠습니다.
끝.
'Tech' 카테고리의 다른 글
GEMM (General Matrix to Matrix Multiplication)과 GPU 아키텍처의 이해 (4) | 2025.03.01 |
---|---|
[AI 상식] LLM은 어떻게 동작할까 - Attention (5) | 2025.02.08 |
[AI상식] LLM은 어떻게 동작할까 - Embedding (6) | 2025.02.01 |
Deepseek v3 code review - model (0) | 2025.01.30 |
TGI Review - server (0) | 2025.01.29 |