2편. Transformer 구조

Gyullbb·3일 전

LLM

목록 보기
2/2

"GPU는 행렬곱을 가장 잘하는 프로세서다.

그렇다면 GPU는 실제로 무엇을 계산하고 있을까?"

1편에서는 CPU와 GPU의 구조를 살펴보며 왜 AI가 GPU 위에서 동작하는지 알아보았다.

CPU는 복잡한 제어를 빠르게 수행하도록 설계되었고,

GPU는 수천 개의 연산 유닛을 이용해 같은 계산을 동시에 수행하도록 설계되었다.

그렇다면 자연스럽게 이런 질문이 생긴다.

GPU는 실제로 무엇을 계산하기에 그렇게 빠를까?

오늘날 ChatGPT와 같은 대부분의 대형 언어 모델(LLM)은 Transformer라는 구조를 사용한다.

Transformer는 GPU를 위해 만들어진 모델은 아니다.

하지만 결과적으로 GPU가 가장 잘하는 계산인 행렬곱(Matrix Multiplication)을 거의 모든 단계에서 사용한다.

그래서 GPU의 성능이 좋아질수록 Transformer 역시 더 빠르게 학습하고 더 빠르게 추론할 수 있다.

이번 글에서는 LLM이 문장 하나를 생성하는 과정을 따라가며 Transformer가 어떻게 동작하는지 살펴본다.


LLM은 어떻게 답을 만들까?

예를 들어 사용자가 ChatGPT에게 다음과 같이 질문했다고 해보자.

The weather

우리는 자연스럽게 다음 단어로

is

또는

today

같은 단어를 떠올릴 것이다.

LLM도 겉보기에는 비슷한 일을 하는 것처럼 보인다. 하지만 내부 동작은 사람과 전혀 다르다.

사람은 문장의 의미를 이해한 뒤 다음 말을 생각하지만, LLM은 입력 문장을 숫자로 변환하고 수많은 행렬 연산을 수행하여 가장 가능성이 높은 다음 Token을 계산한다.

LLM 실제 계산의 대부분은 Transformer Block 안에서 이루어진다.

하지만 Transformer를 이해하기 전에 먼저 컴퓨터가 문장을 어떻게 숫자로 바꾸는지부터 살펴보자.


Tokenizer

컴퓨터는 사람이 사용하는 언어를 그대로 이해하지 못한다.

따라서 가장 먼저 해야 하는 일은 문장을 Token이라는 작은 단위로 분리하는 것이다.

The weather is nice

라는 문장은

"The"
"weather"
"is"
"nice"
처럼 여러 개의 Token으로 나뉜다.

여기서 중요한 점은 Token이 반드시 단어 하나를 의미하는 것은 아니라는 것이다.

예를 들어

playing

이라는 단어는
"play"
"ing"
처럼 두 개의 Token 으로 분리될 수도 있다.

즉, Tokenizer는 언어를 가장 효율적으로 표현할 수 있도록 문장을 분해하는 과정이다.

Tokenizer는 이렇게 분해한 각 Token에 고유한 번호(ID)를 부여한다.

"The" -> id 464

"weather" -> id 6193

이제 컴퓨터는 문장을 숫자로 표현할 수 있게 되었다.

하지만 아직 문제가 하나 남아 있다.

숫자 464를 보고 컴퓨터는 이것이 "The"라는 단어인지, "weather"와 어떤 관계가 있는지 전혀 알지 못한다.

이 문제를 해결하기 위해 사용하는 것이 Embedding이다.


Embedding

Embedding은 Token ID를 의미를 가진 벡터(Vector)로 변환하는 과정이다.

Transformer는 Token ID를 그대로 사용하는 대신, 각각의 Token을 수백 또는 수천 개의 숫자로 이루어진 벡터로 표현한다.

예를 들어 Hidden Size가 768인 모델이라면 하나의 Token은 다음과 같이 표현된다.

"The" -> [0.82, -1.37, 0.19, ..., 0.45]

이 벡터 하나가 바로 "The"라는 단어를 표현하는 새로운 방식이다.

Embedding은 어떻게 만들어질까?

LLM 내부에는 Embedding Matrix라는 거대한 테이블이 하나 존재한다.

Embedding Matrix (Vocabulary Size × Embedding Dimension)

ID Vector

0 → [ 0.11, -0.54, 0.92, ... ]
1 → [-0.38, 1.25, -0.73, ... ]
2 → [ 0.66, 0.17, 0.44, ... ]
...
464 → [ 0.82, -1.37, 0.19, ... ]
...
6193 → [-0.51, 0.84, -0.11, ... ]
...

예를 들어 "The"의 Token ID가 464라면 Embedding Layer는 Embedding Matrix의 464번째 행(Row)을 그대로 가져온다.

Embedding Matrix의 464번째 Row 조회 -> [0.82, -1.37, 0.19, ..., 0.45]

이렇게 얻어진 벡터가 바로 "The"의 Embedding이다.

즉, Embedding Layer는 새로운 벡터를 계산하는 것이 아니라 미리 학습된 벡터를 조회(Lookup) 하는 과정이다.

그렇다면 이 벡터는 누가 만들었을까?

처음에는 모든 벡터가 무작위(Random) 값으로 초기화된다.
하지만 모델이 수십억 개의 문장을 학습하면서 역전파(Backpropagation)를 통해 조금씩 수정된다.

예를 들어

  • weather
  • rain
  • cloud
  • temperature

처럼 자주 함께 등장하는 단어들은 점점 가까운 위치로 이동하고, 관련이 적은 단어들은 멀어지게 된다.

결국 학습이 끝나면 단순한 숫자였던 Token ID는 의미를 담은 좌표가 된다.

중요한 점은 절대적인 좌표가 아니라 벡터들 사이의 거리와 방향이다.

그래서 의미가 비슷한 단어일수록 벡터 공간에서도 가까운 위치에 존재하게 된다.

서로 다른 모델은 학습 데이터와 구조가 다르기 때문에 같은 "The"라도 모델에 따라 서로 다른 Embedding 벡터를 갖는다.

여기까지 오면 컴퓨터는 각 단어의 의미는 표현할 수 있게 되었다.

하지만 아직도 중요한 정보 하나가 부족하다.

바로 단어의 순서다.


Position Encoding

다음 두 문장을 비교해보자.

I love you

You love I

두 문장은 같은 단어를 사용하지만 의미는 완전히 다르다.

Embedding만 사용한다면 Transformer는 두 문장을 거의 같은 단어 집합으로 인식한다.

왜냐하면 각 단어의 의미는 알 수 있지만 어느 단어가 먼저 등장했는지는 알 수 없기 때문이다.

그래서 Transformer는 각 Token에 위치 정보(Position) 를 함께 전달한다.

이를 Positional Encoding이라고 한다.

동작 방식은 생각보다 단순하다. 각 Token은 이미 Embedding을 통해 의미를 표현하는 벡터를 가지고 있다.
여기에 "몇 번째 단어인지" 를 나타내는 위치 벡터를 더한다.

"The" (1번째 단어)

Embedding [0.82, -1.37, 0.19, ...] + Position Encoding [0.00, 1.00, 0.00, ...]

= Transformer에 입력되는 값 [0.82, -0.37, 0.19, ...]

즉, Transformer에 입력되는 값은

단어의 의미 + 단어의 위치

를 함께 표현하는 하나의 벡터이다.
이렇게 하면 같은 "love"라는 단어라도 문장의 첫 번째에 등장하는지, 세 번째에 등장하는지에 따라 서로 다른 표현을 갖게 되고, 모델은 단어의 의미뿐 아니라 순서까지 함께 이해할 수 있다.

그렇다면 위치 벡터는 어떻게 만들까? Transformer 논문에서는 각 위치마다 고유한 패턴을 만들기 위해 사인(Sine) 과 ​코사인(Cosine) 함수를 사용했다.
위치 번호(Position)를 입력하면 항상 같은 위치 벡터가 생성되며, 가까운 위치는 비슷한 값을, 먼 위치는 다른 값을 갖도록 설계되어 모델이 단어들의 상대적인 위치 관계까지 학습할 수 있다.

수식은 다음과 같다.

PE(pos,2i)=sin(pos100002id)PE(pos,2i+1)=cos(pos100002id)\begin{aligned} PE(pos,2i) &=\sin\left(\frac{pos}{10000^{\frac{2i}{d}}}\right) \\[1.2em] PE(pos,2i+1) &=\cos\left(\frac{pos}{10000^{\frac{2i}{d}}}\right) \end{aligned}

pos : 현재 토큰의 위치 (0, 1, 2, ...)
i : 벡터의 차원 인덱스
d : Embedding 차원 (예: 512, 768, 4096 ...)

해당 수식으로 각 위치마다 고유한 벡터를 만들어 Embedding에 더해 줌으로써 이제 Transformer는 단어의 의미뿐 아니라 문장 속 순서까지 함께 표현할 수 있게 된다.

각 Token은 의미를 담은 Embedding 벡터와 순서를 나타내는 Position 정보를 함께 갖게 된다.
하지만 이것만으로는 아직 다음 단어를 예측할 수 없다.

Transformer의 핵심인 Transformer Block 안으로 들어가, 문장 속 단어들이 서로 어떤 관계를 가지는지 계산하기 시작한다.

그 중심에 있는 것이 바로 Self-Attention이다.


Self-Attention

Transformer Block 내부 과정을 이해하기 위해서는 Self-Attention에 대한 이해는 필수적이다.

Self-Attention을 간단하게 표현하면 문장 안의 모든 단어를 동시에 바라보며 어떤 단어가 자신과 가장 관련이 있는지 계산하는 과정이다.

이 과정을 위해 등장하는 개념이 바로 Query(Q), Key(K), Value(V) 이다.

Query, Key, Value

Transformer는 입력으로 받은 Embedding을 그대로 비교하지 않는다.

대신 하나의 Embedding으로부터 서로 다른 역할을 가진 세 개의 벡터를 만든다.

  • Query(Q)
  • Key(K)
  • Value(V)

왜 굳이 하나를 세 개로 나눌까?

그 이유는 하나의 단어가 문장 안에서 동시에 여러 역할을 수행하기 때문이다.

예를 들어

The weather is nice.

라는 문장에서 "weather"는

  • 다른 단어를 찾기 위한 기준이 되기도 하고,
  • 다른 단어가 자신을 참고하는 대상이 되기도 하며,
  • 최종적으로 전달할 의미를 담기도 한다.

이 세 가지 역할을 하나의 벡터가 모두 수행하도록 하면 서로 다른 목적이 뒤섞이게 된다.

그래서 Transformer는 역할을 분리한다.

각각을 다음처럼 생각하면 이해하기 쉽다.

  • Query : 나는 무엇을 찾고 싶은가?
  • Key : 나는 어떤 특징을 가지고 있는가?
  • Value : 나는 어떤 정보를 전달할 것인가?

즉, Query는 질문, Key는 검색 키, Value는 실제 데이터라고 생각하면 된다.

이처럼 역할을 분리하면 비교는 Query와 Key가 담당하고, 실제 정보 전달은 Value가 담당하게 되어 각 역할을 독립적으로 학습할 수 있다.

여기서 한 가지 질문이 생긴다.

Query, Key, Value는 누가 만드는 걸까?

이 질문에 답하려면 먼저 학습(Training) 과 추론(Inference) 을 구분해야 한다.


학습(Training)과 추론(Inference)의 차이

Transformer의 동작을 이해하기 전에 반드시 구분해야 하는 개념이 있다. 바로 학습추론이다.
이 두 개념을 분리하여 이해하여야 Transformer에 대한 이해가 쉬워진다.

ChatGPT와 같은 LLM을 사용할 때, 내가 질문을 하면 ChatGPT가 그 자리에서 계속 학습하는 것이 아닐까? 라는 생각을 할 수 있다.
하지만 Transformer에서 학습과 추론은 목적과 동작 방식이 서로 다른 두 과정이다.

학습(Training)

학습은 모델이 언어를 배우는 과정이다.
문장이 주어지면 모델은 현재까지의 문맥을 바탕으로 다음 토큰을 예측하는 문제를 반복해서 푼다.

예를 들어

나는 오늘 밥을

이라는 문장이 주어졌을 때 모델이

침대

라고 예측했다고 해보자.

실제 정답은

먹었다

이다.

모델은 자신의 예측과 정답을 비교하여 얼마나 틀렸는지를 계산한다.

이 값을 Loss라고 한다.

Loss는 모델의 예측이 실제 정답과 얼마나 다른지를 수치로 나타낸 값이다. 모델의 목표는 Loss를 최대한 작게 만드는 것이다.

Loss가 계산되면 "어떤 가중치 때문에 이런 실수가 발생했는가?" 를 역전파(Backpropagation)를 통해 계산하고, 모델 내부의 모든 Weight를 조금씩 수정한다.

이 과정을 수십억 개의 문장에 대해 반복하면서 Embedding, Q를 만드는 Weight, K를 만드는 Weight, V를 만드는 Weight, 그리고 Transformer의 모든 Weight가 조금씩 최적화된다.

즉, Query, Key, Value를 만드는 방법도 모두 학습을 통해 결정된다.


추론(Inference)

우리가 ChatGPT를 사용할 때는 학습의 과정을 통해 얻은 모델을 사용하게 된다. 이 과정을 추론이라고 한다.

추론에서는 이미 학습이 끝난 Weight를 그대로 사용한다. 새로운 문장이 들어오더라도 Weight는 변하지 않는다.

그런데도 입력 문장에 따라 Self-Attention 결과는 계속 달라진다.

Weight는 고정되어 있지만 입력되는 Embedding은 매번 달라지기 때문이다.

같은 Weight를 사용하더라도 입력되는 Embedding이 달라지면 생성되는 Query, Key, Value 역시 달라진다.

즉, 학습Weight를 최적화하는 과정이고, 추론은 학습된 Weight를 이용해 새로운 입력에 대한 결과를 계산하는 과정이다.

중요한 점은 Transformer는 추론 중에도 Query, Key, Value를 새롭게 계산한다는 것이다.

바뀌지 않는 것은 Weight이고, 매 입력마다 새롭게 계산되는 것은 Embedding과 Query, Key, Value 그리고 Self-Attention 결과이다.

즉, 모델이 새로운 문장을 이해하는 이유는 Weight가 바뀌기 때문이 아니라, 같은 Weight를 이용해 입력에 맞는 새로운 표현을 매번 계산하기 때문이다.


Query, Key, Value 계산

이제 실제 계산을 살펴보자.

Embedding 하나는 각각 다른 Weight를 이용하여 세 번의 선형 변환을 수행한다.

Embedding
     │
     ├────────► Q = XWQ + bQ
     │
     ├────────► K = XWK + bK
     │
     └────────► V = XWV + bV
  • X : Embedding 벡터
  • WQ, WK, WV : Query, Key, Value를 만들기 위한 학습된 Weight
  • bQ, bK, bV : Bias

LLM에서 하나의 Token은 여러 개의 숫자로 이루어진 벡터로 표현된다. 이 벡터의 길이를 Hidden Size라고 하며, 모델을 설계할 때 미리 정해진다.

예를 들어 BERT Base의 Hidden Size는 768이다. 즉, 하나의 Token은 768개의 숫자로 표현된다.

"The weather"라는 문장은 Token이 두 개이므로 Embedding 행렬의 크기는 다음과 같다.

(2, 768)

  • 2 : Token의 개수
  • 768 : Hidden Size

앞에서 본 수식대로라면 Query, Key, Value를 각각 계산해야 하므로 세 번의 행렬곱이 필요해 보인다.

하지만 실제 구현은 조금 다르다.

GPU는 작은 행렬을 여러 번 곱하는 것보다 큰 행렬 하나를 한 번에 계산하는 것이 메모리 접근과 병렬 처리 측면에서 훨씬 효율적이다.
따라서 대부분의 Transformer 구현은 Query, Key, Value를 위한 세 개의 Weight를 하나의 큰 Weight로 합쳐 한 번의 행렬곱으로 계산한다.

Hidden Size가 768이라면 세 개의 Weight를 이어 붙인 하나의 Weight 행렬의 크기는 다음과 같다.

(768, 2304)

실제 계산은 다음과 같이 한 번의 행렬곱으로 수행된다.

(2, 768) × (768, 2304) = (2, 2304)

계산이 끝난 뒤 생성된 (2, 2304) 행렬은 가로 방향으로 세 등분되어 각각 (2, 768) 크기의 Query, Key, Value 행렬로 분리된다.

       (2, 2304)
            │
 ┌──────────┼──────────┐
 ▼          ▼          ▼

Q (2,768) K (2,768) V (2,768)

수학적으로는 Query, Key, Value를 각각 계산하는 것으로 표현하지만, 실제 구현에서는 GPU의 병렬 연산 성능을 최대한 활용하기 위해 한 번의 큰 행렬곱으로 계산한 뒤 다시 세 개의 행렬로 분리하는 방식을 사용한다.

이제 모든 Token에 대한 Query, Key, Value가 준비되었다. 다음 단계에서는 각 Token이 서로 얼마나 관련 있는지를 계산한다.


Self-Attention 계산

앞에서 모든 Token에 대한 Query(Q), Key(K), Value(V)를 준비했다.

이제 Transformer는 이 세 가지 정보를 이용해 각 Token이 문장 속 어떤 단어를 얼마나 참고해야 하는지 계산한다.
논문에서는 이를 다음과 같은 수식으로 정의한다.

Attention(Q,K,V)=Softmax(QKTdk)VAttention(Q,K,V) = Softmax \left( \frac{QK^T}{\sqrt{d_k}} \right) V

이 수식은 크게 네 단계로 나누어 볼 수 있다.

1. Query와 Key를 비교한다.

가장 먼저 Query와 모든 Key를 비교하여 단어들 사이의 관련성을 계산한다.

이때 사용하는 연산이 내적(Dot Product) 이다.

내적은 두 벡터가 얼마나 비슷한 방향을 바라보고 있는지를 나타내는 연산으로, 값이 클수록 두 벡터가 더 비슷하다는 의미이다.

예를 들어 "weather"를 처리하고 있다면 "weather"의 Query는 문장 속 모든 Key와 비교된다.

weather(Q)

├──► The(K)
├──► weather(K)
├──► is(K)
└──► nice(K)

이 과정을 거치면 "weather"와 다른 단어들 사이의 관련성이 숫자로 계산된다.

이 숫자를 Attention Score라고 한다.

중요한 점은 "weather"만 계산하는 것이 아니라 모든 Token이 동시에 같은 계산을 수행한다는 것이다.

Transformer가 문장을 한 번에 처리할 수 있는 이유도 여기에 있다.


2. 왜 √dₖ로 나눌까?

Query와 Key를 내적하면 차원이 커질수록 결과도 함께 커지는 경향이 있다.

예를 들어 Hidden Size가 64일 때보다 4096일 때는 내적 값이 훨씬 큰 숫자가 된다.

이 상태에서 바로 Softmax를 적용하면 문제가 발생한다.

예를 들어 다음과 같은 결과가 나올 수 있다.

0.999999
0.000001
0.000000

모델이 하나의 단어만 지나치게 선택하고 나머지 단어는 거의 무시하게 되는 것이다.

이런 현상을 방지하기 위해 논문에서는 내적 값을

dk\sqrt{d_k}

로 나누어 값의 크기를 적절하게 조절한다.

이 과정을 포함한 Attention을 Scaled Dot-Product Attention이라고 부른다.


3. Softmax로 중요도를 계산한다.

정규화된 Attention Score는 Softmax를 거쳐 확률 형태의 가중치로 변환된다.
예를 들어 "weather"를 처리한 결과가 다음과 같다고 해보자.

단어Attention Weight
The0.05
weather0.65
is0.20
nice0.10

위와 같은 결과가 나왔다면 Transformer는 "weather"를 가장 중요하게 참고하고, "the""is" 그리고 "nice"는 그보다 적게 참고한다는 의미이다.

이 가중치는 단순히 가장 큰 값을 선택하는 것이 아니라, 문맥에 따라 각 단어를 얼마나 반영할지를 결정하는 비율이다.


4. Value를 이용해 새로운 표현을 만든다.

이제 계산된 Attention Weight를 각 Token의 Value에 곱한 뒤 모두 더한다.

Output=Attention Weights×VOutput = Attention\ Weights \times V

즉, 관련성이 높은 단어의 정보는 더 많이 반영하고, 관련성이 낮은 단어의 정보는 적게 반영한다.

결과적으로 "weather"는 처음의 Embedding과는 다른 벡터가 된다.

이 벡터는 이제 단순히 "weather"라는 단어를 표현하는 것이 아니라,

문장 전체의 문맥이 반영된 새로운 표현(Contextual Representation)이 된다.

이것이 바로 Self-Attention의 핵심이다.


Multi-Head Attention

지금까지는 Attention을 하나만 사용하는 경우를 살펴봤다.

하지만 실제 Transformer는 Attention을 하나만 사용하지 않고 여러 개의 Attention을 동시에 계산한다.

이는 문장을 이해하는 방법은 하나가 아니기 때문이다.

하나의 문장에서 어떤 Head는 문법적 관계를, 또 다른 Head는 동사의 의미를, 또 다른 Head는 문장 전체의 흐름을 학습할 수 있다.

각 Head는 서로 다른 Weight를 사용하므로 같은 문장을 보더라도 서로 다른 특징을 학습하게 된다.

모든 Head의 계산이 끝나면 결과를 하나로 합쳐 다음 단계로 전달한다.

이 과정을 Multi-Head Attention이라고 한다.

덕분에 Transformer는 하나의 관점이 아니라 여러 관점을 동시에 고려하여 문맥을 이해할 수 있다.

여러 Head의 결과는 하나로 합쳐져 다음 단계인 Feed Forward Network로 전달된다.


Feed Forward Network

Self-Attention을 거치면 각 Token은 문맥 정보를 반영한 새로운 표현을 갖게 된다.

하지만 이것만으로는 충분하지 않다.

문맥을 이해한 뒤에는 그 정보를 이용해 각 Token 자체를 더 풍부하게 표현해야 한다.

이 역할을 수행하는 것이 Feed Forward Network(FFN) 이다.

쉽게 말하면,

Self-Attention이 어떤 단어를 참고할지 결정한다면,
FFN은 그 정보를 이용해 Token의 표현 자체를 업그레이드하는 과정이다.

FFN의 구조

FFN은 두 개의 Linear Layer와 하나의 활성화 함수(GELU)로 이루어진다.

h=GELU(XW1+b1)h = GELU(XW_1+b_1)
Output=hW2+b2Output = hW_2+b_2
  • W₁ : 차원을 확장하는 Weight
  • W₂ : 다시 원래 크기로 줄이는 Weight

첫 번째 Linear Layer는 Hidden Size를 크게 확장하고, 두 번째 Linear Layer는 다시 원래 크기로 축소한다.

예를 들어 BERT Base의 Hidden Size는 768이지만, FFN 내부에서는 먼저 3072(768 × 4) 차원으로 확장한 뒤 다시 768 차원으로 줄인다.

이는 Token의 개수는 그대로 유지한 채, 각 Token을 표현하는 Feature의 개수를 768개에서 3072개로 확장했다가 다시 압축하는 것을 의미한다.

여기서 차원을 늘린다는 것은 단순히 768개의 숫자를 3072개로 복사하는 것이 아니다.

첫 번째 Linear Layer는 768개의 입력 Feature를 3072개의 새로운 Feature로 변환하며, 이때 생성되는 3072개의 출력 각각을 하나의 뉴런(Neuron)이라고 생각하면 이해하기 쉽다.

각 뉴런은 입력 Feature 전체를 자신만의 Weight로 조합하여 하나의 새로운 값을 계산하는 계산기이다.
따라서 3072개의 뉴런은 모두 서로 다른 Weight를 사용해 동일한 입력을 각자의 방식으로 계산한다.

결과적으로 하나의 Token은 3072개의 새로운 Feature로 표현되며, 이는 하나의 Token을 3072개의 서로 다른 관점에서 분석하는 과정이라고 이해하면 된다.

GELU

여기서 또 하나의 궁금한 점이 생긴다.

Linear Layer 두 번 사용하는데, 왜 그 사이에 GELU를 넣는 걸까?

만약 활성화 함수가 없다면 Linear → Linear는 결국 하나의 큰 Linear Layer와 완전히 동일한 계산이 된다.

즉, 아무리 Layer를 많이 쌓아도 복잡한 패턴을 학습할 수 없다.

이를 해결하기 위해 첫 번째 Linear Layer와 두 번째 Linear Layer 사이에 GELU(Gaussian Error Linear Unit) 라는 활성화 함수를 넣는다.

GELU는 입력값을 단순히 통과시키는 것이 아니라 값의 크기에 따라 출력을 부드럽게 조절하는 비선형 함수이다.

큰 값은 거의 그대로 유지하고, 작은 값이나 중요하지 않은 값은 자연스럽게 줄여 준다.

입력

├── 큰 값 ────────► 거의 그대로 통과

└── 작은 값 ───────► 부드럽게 감소

ReLU처럼 일정 기준 아래의 값을 모두 0으로 만드는 것이 아니라,연속적으로 값을 조절하기 때문에 정보 손실이 적고 학습도 더욱 안정적이다.

FFN의 목적

FFN을 거치면 3072개의 다양한 특징 가운데 중요한 정보는 더욱 강조되고, 덜 중요한 정보는 자연스럽게 약해진다.

마지막 Linear Layer는 이를 다시 원래 Hidden Size로 압축하여 다음 Block으로 전달한다.

처음 Transformer Block에 들어왔을 때의 벡터는 단순히 단어의 의미와 위치만 담고 있었다.

하지만 Self-Attention과 FFN을 거친 후에는 문맥과 다양한 특징이 모두 반영된 새로운 표현으로 변하게 된다.

이 표현은 다음 Transformer Block으로 전달되고,

같은 과정이 반복되면서 문장의 의미는 점점 더 풍부하게 다듬어진다.


Residual Connection

지금까지 살펴본 Self-Attention과 Feed Forward Network는 입력된 Token을 새로운 표현으로 변환하는 역할을 한다.

하지만 Transformer는 이러한 Block을 하나만 사용하는 것이 아니다.

Transformer가 수십 개, 많게는 수백 개의 Block을 거치게 되면 문제가 생긴다.

매 Block마다 입력을 계속 새로운 값으로 바꾸다 보면 원래 가지고 있던 정보가 점점 희미해질 수 있다.

이 문제를 해결하기 위해 사용하는 것이 Residual Connection(잔차 연결) 이다.

Residual Connection은 새로운 계산 결과만 사용하는 것이 아니라, 원래 입력을 함께 더해주는 방식이다.

수식으로 표현하면 다음과 같다.

Output=X+F(X)

  • X : Block의 입력
  • F(X) : Self-Attention 또는 FFN의 출력

즉, Transformer는 새롭게 학습한 정보와 기존 정보를 함께 유지하도록 설계되어 있다.

덕분에 Block이 아무리 깊어져도 중요한 정보가 쉽게 사라지지 않으며, 역전파 과정에서도 Gradient가 안정적으로 전달된다.

이러한 구조 덕분에 수십, 수백 개의 Layer를 가진 매우 깊은 모델을 안정적으로 학습할 수 있다.


Layer Normalization

Residual Connection으로 입력 정보를 유지했다면, 이제는 계산 결과를 안정적으로 만들어야 한다.

Transformer는 Block을 반복해서 거치는 동안 벡터 안의 값이 점점 커지거나 작아질 수 있다.

예를 들어 어떤 Block에서는

[ 0.8, -0.5, 1.2 ]

가 나오고,

다음 Block에서는

[ 130, -85, 240 ]

처럼 값의 크기가 크게 변할 수도 있다.

이러한 변화가 계속 누적되면 학습이 불안정해지고, 모델의 성능도 떨어질 수 있다.

이를 방지하기 위해 사용하는 것이 Layer Normalization(LayerNorm) 이다.

LayerNorm은 벡터의 평균과 분산을 이용해 값의 분포를 일정한 범위로 맞춰준다.

여기서 중요한 점은 값의 크기를 조정하는 것이지, 의미를 없애는 것은 아니라는 것이다.

각 Feature가 가진 상대적인 관계는 그대로 유지하면서, 계산하기 좋은 범위로 정리해 주는 역할을 한다.

Residual Connection이 정보를 유지한다면, LayerNorm은 그 정보를 안정적으로 다음 Layer에 전달하는 역할을 수행한다고 이해하면 된다.


Transformer Block

이제 지금까지 살펴본 내용을 하나로 정리해 보자.

Transformer Block 내부에서는 다음과 같은 순서로 계산이 이루어진다.

Input


Multi-Head Attention


Add (Residual)


Layer Normalization


Feed Forward Network


Add (Residual)


Layer Normalization


Output

먼저 Multi-Head Attention이 문장 속 단어들의 관계를 계산하고,

Feed Forward Network가 각 Token의 표현을 더욱 풍부하게 만든다.

그리고 Residual Connection과 LayerNorm이 학습을 안정적으로 유지하면서 다음 Block으로 정보를 전달한다.

Transformer는 이러한 Block을 하나만 사용하는 것이 아니라 수십 개에서 많게는 수백 개까지 반복해서 쌓는다.

각 Block을 지날 때마다 Token은 새로운 문맥 정보를 계속 반영하게 되고, 처음에는 단순히 단어의 의미를 나타내던 Embedding이 점차 문장 전체를 이해한 표현(Contextual Representation) 으로 발전한다.

우리가 질문을 입력하면, 모델은 Transformer Block을 반복적으로 통과시키며 문장의 의미를 단계적으로 다듬고, 가장 가능성이 높은 다음 Token을 예측하는 것이다.


마무리

이번 글에서는 Transformer가 문장을 처리하는 전체 과정을 순서대로 따라가 보았다.

먼저 Tokenizer가 문장을 Token으로 분리하고, Embedding이 각 Token을 의미를 가진 벡터로 변환했다.

여기에 Positional Encoding을 더해 단어의 순서까지 함께 표현한 뒤, Transformer Block 안에서 Self-Attention을 통해 문장 속 단어들의 관계를 계산했다.

이후 Multi-Head Attention은 여러 관점에서 문맥을 분석하고, Feed Forward Network는 각 Token의 표현을 더욱 풍부하게 만들었다.

마지막으로 Residual Connection과 LayerNorm은 깊은 네트워크에서도 학습이 안정적으로 이루어질 수 있도록 도와주었다.

이처럼 Transformer의 대부분의 연산은 행렬곱(Matrix Multiplication) 을 기반으로 수행된다.

그래서 GPU가 가진 대규모 병렬 연산 능력을 가장 효과적으로 활용할 수 있었고, Transformer는 오늘날 LLM의 표준 구조가 되었다.

다음 편에서는 모델 서빙(Model Serving) 의 관점에서 LLM이 실제 서비스 환경에서 어떻게 실행되는지 살펴본다.

0개의 댓글