[핸즈온 LLM] 트랜스포머 모델 개요 (3.1)
🧠 트랜스포머 LLM은 어떻게 텍스트를 생성할까?
— 입력에서 출력까지, 그리고 어텐션 메커니즘의 정밀 해부
1. 트랜스포머 LLM이란?
트랜스포머 기반의 대형 언어 모델(LLM)은 우리가 흔히 사용하는 챗봇, 텍스트 요약기, 코드 생성기 등에 쓰이는 핵심 기술입니다. 이 모델은 텍스트를 입력받아 응답을 생성하는 텍스트-입력 → 텍스트-출력 시스템으로 동작합니다.
하지만 중요한 점은:
✨ 트랜스포머 LLM은 텍스트 전체를 한꺼번에 생성하지 않습니다.
🔁 한 번에 한 토큰씩, 반복적으로 예측하며 문장을 완성해 갑니다.
이렇게 이전의 예측을 사용해 다음 예측을 만드는 모델을 지칭하는 머신러닝 용어가 자기회귀(Autoregressive) 모델임. 텍스트 생성 LLM은 이 같은 특성이 있어 자기회귀 모델이라 함.
정방향 계산(Forward Pass)의 구성 요소
2. 입력 → 정방향 계산 → 출력
✅ 기본 처리 흐름
- 텍스트 입력 → 토크나이저가 정수 시퀀스로 변환
- 트랜스포머 블록 스택 → 입력 시퀀스를 처리
- LM 헤드 → 다음 토큰에 대한 확률 분포 출력
- 디코딩 → 확률 분포에서 다음 토큰을 선택
- 선택된 토큰을 프롬프트 끝에 추가 → 다음 루프 실행
이러한 루프는 모델이 </s> 또는 최대 길이(context length)에 도달할 때까지 반복됩니다.
모델 객체를 출력하면 모델에 포함된 층을 순서대로 표시할 수 있음. 우리가 사용하는 모델의 경우 다음과 같은 결과를 얻음.
print(model)
# output
Phi3ForCausalLM(
(model): Phi3Model(
(embed_tokens): Embedding(32064, 3072, padding_idx=32000)
(embed_dropout): Dropout(p=0.0, inplace=False)
(layers): ModuleList(
(0-31): 32 x Phi3DecoderLayer(
(self_attn): Phi3Attention(
(o_proj): Linear(in_features=3072, out_features=3072, bias=False)
(qkv_proj): Linear(in_features=3072, out_features=9216, bias=False)
(rotary_emb): Phi3RotaryEmbedding()
)
(mlp): Phi3MLP(
(gate_up_proj): Linear(in_features=3072, out_features=16384, bias=False)
(down_proj): Linear(in_features=8192, out_features=3072, bias=False)
(activation_fn): SiLU()
)
(input_layernorm): Phi3RMSNorm()
(resid_attn_dropout): Dropout(p=0.0, inplace=False)
(resid_mlp_dropout): Dropout(p=0.0, inplace=False)
(post_attention_layernorm): Phi3RMSNorm()
)
)
(norm): Phi3RMSNorm()
)
(lm_head): Linear(in_features=3072, out_features=32064, bias=False)
)
3. 출력은 어떻게 선택될까? (Sampling)
prompt = 'The capital of France is'
# 입력 프롬프트를 토큰화 합니다.
input_ids = tokenizer(prompt, return_tensors = 'pt').input_ids
# 입력 토큰을 GPU에 배치합니다.
input_ids = input_ids.to('cuda')
# lm_head 앞에 있는 model의 출력을 얻습니다.
model_output = model.model(input_ids)
# lm_head의 출력을 얻습니다.
lm_head_output = model.lm_head(model_output[0])
# lm_head_output의 shape는 [1,5,32064]임. [텍스트 수, 토큰 수, 어휘사전 크기]
# lm_head_output[0, -1]로 마지막에 생성된 토큰에 대한 확률 점수를 얻을 수 있음.
# 텍스트 샘플이 하나이므로 배치 차원의 인덱스에는 0 지정, 시퀀스에 있는 마지막 토큰을 얻기 위해 인덱스 -1을 사용.
token_id = lm_head_output[0,-1].argmax(-1)
tokenizer.decode(token_id)
트랜스포머 모델의 마지막 출력은 다음과 같은 확률 분포입니다:
logits = lm_head(hidden_state) # shape: (1, vocab_size)
이때 사용되는 디코딩 전략은 다음과 같습니다:
Greedy | 확률이 가장 높은 토큰 선택 (argmax) |
Sampling | 확률 기반 랜덤 샘플링 |
Top-k / Top-p | 일정 확률 이상 토큰만 샘플링 대상 |
Temperature | 확률 분포의 날카로움을 조절 (온도=0이면 Greedy) |
4. 문맥 길이 (Context Length)
트랜스포머는 병렬 처리가 뛰어나지만 동시에 처리할 수 있는 최대 토큰 수에 제한이 있습니다. 이를 문맥 길이라고 하며, 예를 들어 GPT-3의 경우 최대 2,048 ~ 4,096개 토큰까지 입력 가능했습니다.
📌 입력 길이가 길어지면 기존 토큰의 계산 결과도 같이 전달되어야 하기 때문에, 이전 토큰을 무시할 수 없습니다.
마지막 토큰을 제외한 모든 토큰의 결과를 버리는 데 왜 모든 토큰 스트림을 계산하기 위해 애쓰는가? 이전 스트림의 계산이 최종 스트림의 계산에 필요하기 때문! 마지막을 제외한 스트림의 최종 출력 벡터를 사용하지 않지만 트랜스포머 블록의 어텐션 메커니즘에서는 이전 스트림의 출력을 사용합니다.
5. 키-밸류 캐시로 속도 높이기 (Key-Value Cache)
모델이 토큰을 하나씩 생성할 때마다 전체 시퀀스를 다시 계산하면 매우 비효율적입니다. 그래서 많은 LLM 구현체는 이전 계산 중 Key/Value 벡터를 캐싱하여, 다음 토큰 계산 시 재활용합니다.
이 캐싱이 바로 use_cache=True로 설정되는 부분이며, 생성 속도를 크게 향상시킵니다.
허깅 페이스의 transformers는 기본적으로 캐싱을 사용함. use_cache 매개변수를 False로 지정하여 캐싱을 끌 수 있음. 코드 예제를 통해 긴 문장을 생성 요청하고 캐싱을 할 때와 안 할 때의 생성 속도 차이를 살펴보자. (100개의 토큰을 생성하는데 걸리는 시간)
%%timeit -n 1
generation_output = model.generate(
input_ids = input_ids,
max_new_tokens = 100,
use_cache = False
)
# output: 9.93 s ± 605 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit -n 1
generation_output = model.generate(
input_ids = input_ids,
max_new_tokens = 100,
use_cache = False
)
# output :33.5 s ± 337 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
약 3.3배 차이가 난다 ㄷㄷ
6. 트랜스포머 블록 내부 구조
트랜스포머는 수십~수백 개의 블록으로 이루어진 신경망이며, 각 블록은 다음 두 가지 구성 요소로 이루어져 있습니다:
- 어텐션 층 (Self-Attention Layer)
→ 문맥 정보를 통합하여 토큰 간 관계를 모델링 - 피드포워드 층 (MLP Layer)
→ 정보 변환 및 보간(interpolation) 기능 수행
이 블록들이 쌓이면서 모델은 복잡한 언어 패턴을 학습할 수 있게 됩니다.
7. 어텐션 메커니즘
이제 많은 분들이 헷갈리는 어텐션 메커니즘의 작동 방식을 벡터 중심으로 차근차근 설명해볼게요.
💡 개념 요약
셀프 어텐션은 하나의 토큰이, 다른 모든 토큰을 얼마나 참고할지를 결정하고
그 참고 정도에 따라 가중 평균을 통해 새로운 벡터를 만드는 과정입니다.
예시 조건
- 입력 문장: "나는 사과를 먹었다" → 총 4개의 토큰
- 시퀀스 길이: L = 4
- 전체 임베딩 차원: d_model = 512
- 어텐션 헤드 개수: h = 8
→ 각 헤드당 차원: d_k = d_v = 64
🧱 Step 1. 임베딩 및 Q/K/V 만들기
입력 X의 shape:
X: (L, d_model) = (4, 512)
Q, K, V를 만들기 위해 각각의 Projection Matrix를 곱합니다:
- 어텐션 메커니즘
Q = X @ W_Q # shape: (4, 64)
K = X @ W_K # shape: (4, 64)
V = X @ W_V # shape: (4, 64)
🔎 Step 2. 관련성 점수 계산 (Attention Score)
지금 "먹었다"라는 토큰을 처리 중이라고 합시다.
그 벡터는 Q[3]에 해당합니다.
관련성 점수는 다음과 같이 계산됩니다:
scores = Q[3] @ K.T # shape: (1, 4)
→ softmax(scores) # attention_weights: (1, 4)
예를 들어 softmax 결과가 다음과 같다고 합시다:
attention_weights = [0.1, 0.6, 0.2, 0.1]
🔁 Step 3. 정보 통합 (Weighted Sum of V)
이제 V 행렬에서 해당 가중치만큼 각 벡터를 곱해 더합니다:
attention_output = attention_weights @ V
# shape: (1, 64)
즉, "먹었다"의 최종 표현은:
- "나는", "사과를", "먹었다", "[PAD]" 각각의 의미 정보가
- 가중치 0.1, 0.6, 0.2, 0.1만큼 반영된 합계입니다
🧠 Step 4. Multi-Head Attention → Concat → Output
8개의 헤드가 위 과정을 병렬로 수행한 결과:
- 각 헤드 출력: (4, 64)
- 8개 concat: (4, 512)
- W_O로 선형 변환 후: (4, 512)
마지막 토큰(예: X[3])의 출력 벡터만 LM 헤드로 전달됩니다.
📌 전체 정리 (각 단계의 Shape)
입력 | X | (L, d_model) = (4, 512) |
프로젝션 | Q, K, V | (L, d_k) = (4, 64) |
점수 계산 | Q @ Kᵀ | (L, L) = (4, 4) |
소프트맥스 결과 | attention weights | (L, L) = (4, 4) |
값 통합 | attention weights @ V | (L, d_v) = (4, 64) |
멀티헤드 concat | 여러 head의 출력 연결 | (L, 512) |
최종 선형변환 | W_O를 곱함 | (L, 512) |
8. LM 헤드: 다음 토큰 확률 계산
마지막 토큰의 최종 출력 벡터는 LM 헤드에 들어가 다음과 같이 처리됩니다:
logits = lm_head(last_hidden_state) # shape: (1, vocab_size)
- 모델이 알고 있는 단어 수가 32,064개라면
→ logits: shape (1, 32064) - softmax(logits)를 통해 각 단어에 대한 확률을 얻습니다.
🔚 마무리: 이 모든 것이 하나의 토큰을 생성하기 위한 과정
이러한 과정을 하나의 토큰을 예측할 때마다 반복합니다.
트랜스포머 LLM은 단순히 거대한 DB처럼 과거를 암기하는 것이 아니라,
문맥을 정교하게 통합하고 패턴을 보간하며 일반화할 수 있는 강력한 모델입니다.
이 글은 『핸즈온 LLM』 (박해선 옮김, 한빛미디어, p99-122)을 바탕으로 정리하였습니다.
또한, 생성형 AI의 도움을 받아 작성되었습니다.