← posts/b.log()

blog92@web:~$ cat posts/ai-02-transformer-and-attention.md

AI4 min read

AI 완전 정복 2편 — 트랜스포머와 어텐션

트랜스포머는 모든 토큰이 다른 모든 토큰을 "둘러보고" 서로 얼마나 관련 있는지 계산하는 구조(어텐션)다. 이 구조 때문에 긴 입력은 비싸고, 맥락의 배치가 결과를 바꾼다.

한 줄 요약: 트랜스포머는 모든 토큰이 다른 모든 토큰을 "둘러보고" 서로 얼마나 관련 있는지 계산하는 구조(어텐션)다. 이 구조 때문에 긴 입력은 비싸고, 맥락의 배치가 결과를 바꾼다.

텍스트가 숫자가 되는 과정

신경망은 숫자만 다룹니다. 그래서 모델에 들어간 문장은 다음 순서로 처리됩니다.

  1. 토큰화: 문장을 토큰으로 쪼개고 각각을 정수 ID로 바꾼다. (const → 1234 같은 식)
  2. 임베딩(embedding): 각 ID를 수천 차원의 벡터(숫자 배열)로 바꾼다. 이 벡터가 토큰의 "의미"를 담는다.
  3. 위치 정보 추가: 몇 번째 토큰인지를 벡터에 섞는다. 어텐션 자체는 순서를 모르기 때문이다.
  4. 트랜스포머 블록 반복: "어텐션 → 피드포워드" 블록을 수십~수백 층 통과하며 벡터가 점점 문맥을 머금은 표현으로 바뀐다.
  5. 출력: 마지막 위치의 벡터를 어휘 전체(수만~수십만 개 토큰)에 대한 확률 분포로 바꾼다. 1편의 "다음 토큰 후보 표"가 여기서 나온다.

임베딩: 의미를 좌표로 바꾸기

임베딩은 단어를 거대한 공간의 한 점으로 보내는 것입니다. 학습을 거치면 비슷한 뜻의 토큰은 가까운 자리에 모입니다. fetch, axios, request는 서로 가깝고, fetch와 banana는 멀리 떨어져 있는 식이죠.

이 아이디어는 LLM 밖에서도 자주 씁니다. 문서나 코드를 임베딩 벡터로 바꿔 두면 "의미가 비슷한 코드를 찾아줘" 같은 검색이 가능해지고, 이것이 7편에서 다룰 RAG의 핵심입니다.

셀프 어텐션: 모든 토큰이 모든 토큰을 본다

2017년 논문 "Attention Is All You Need"가 제안한 어텐션은 지금 모든 LLM의 심장입니다. 다음 코드를 보세요.

ts
const user = await getUser(id);
if (!user) throw new NotFoundError();
return user.name;

마지막 줄의 user를 이해하려면 첫 줄의 const user를 참조해야 합니다. 어텐션은 이런 참조를 수학적으로 해냅니다. 각 토큰은 세 가지 벡터를 만듭니다.

벡터역할검색 엔진 비유
Query (Q)"내가 찾는 정보"검색어
Key (K)"내가 가진 정보의 색인"문서 제목·태그
Value (V)"실제로 건네줄 내용"문서 본문

마지막 줄의 user는 자신의 Query를 앞의 모든 토큰의 Key와 비교해 점수를 매깁니다. 첫 줄의 user, getUser 같은 토큰이 높은 점수를 받고, 그 토큰들의 Value가 많이 섞여 들어옵니다. 이런 계산을 여러 개의 "헤드"가 병렬로 하므로(멀티헤드 어텐션), 어떤 헤드는 변수 참조를, 어떤 헤드는 괄호 짝을, 어떤 헤드는 들여쓰기 패턴을 추적하는 식으로 역할이 나뉩니다.

생성 모델은 자기보다 앞에 있는 토큰만 볼 수 있습니다. 아직 생성되지 않은 미래를 볼 수는 없으니까요.

이 구조가 개발자에게 주는 세 가지 실용적 의미

1. 긴 입력은 비싸다. 모든 토큰 쌍을 비교하므로, 입력이 2배가 되면 어텐션 계산량은 원리상 최대 4배까지 늘어납니다(최신 모델들은 다양한 최적화로 이를 완화합니다). 레포지토리 전체를 매번 붙여넣는 것이 느리고 비싼 이유입니다.

2. 맥락이 많을수록 주의가 분산된다. 어텐션 점수는 합이 1인 비율로 나뉩니다. 관련 없는 파일 20개를 함께 넣으면 진짜 중요한 코드에 돌아갈 주의가 엷어질 수 있습니다. "더 많이 넣으면 더 잘하겠지"가 항상 맞지는 않는 이유이고, 5편의 컨텍스트 열화(context rot)로 이어집니다.

3. 앞부분은 재사용할 수 있다. 생성 모델은 앞 토큰만 보므로, 입력의 앞부분이 같다면 그 부분의 계산 결과(KV 캐시)를 저장해 두고 다시 쓸 수 있습니다. 이것이 4편과 10편에서 다루는 프롬프트 캐싱의 원리이고, "자주 안 바뀌는 내용을 앞에, 자주 바뀌는 내용을 뒤에" 두라는 설계 원칙의 근거입니다.

코딩 실습 (20분)

임베딩 유사도를 직접 계산해 봅니다. 임베딩 API(예: Voyage AI, OpenAI embeddings)나 로컬 모델 어느 것이든 됩니다. 아래는 로컬에서 바로 돌리는 Python 예제입니다.

python
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer, util
 
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")  # 한국어를 포함한 다국어 모델
snippets = [
    "fetch('/api/users').then(r => r.json())",
    "axios.get('/api/users')",
    "const total = items.reduce((a, b) => a + b, 0)",
]
query = "HTTP GET 요청으로 사용자 목록 가져오기"
 
emb = model.encode(snippets + [query])
for s, score in zip(snippets, util.cos_sim(emb[-1], emb[:-1])[0]):
    print(f"{score:.3f}  {s}")

첫 두 스니펫의 점수가 세 번째보다 높게 나오면 성공입니다. 문자열이 전혀 겹치지 않는데도 의미로 찾아낸다는 점이 포인트입니다.

정리

  • 텍스트 → 토큰 → 임베딩 벡터 → 수십 층의 어텐션 → 다음 토큰 확률.
  • 어텐션은 Q·K·V로 "어떤 앞 토큰을 참고할지"를 계산한다.
  • 긴 입력 = 비용↑, 주의 분산↑. 같은 앞부분 = 캐시 재사용 가능.

다음 편: 이 구조에 어떤 데이터를 어떻게 학습시켜야 "대화 잘하는 코딩 도우미"가 될까요?

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..