5P by GN⁺ | ★ favorite | 댓글 1개
  • 트랜스포머 추론 과정을 Hello World → Hola Mundo 번역 예제로 줄여, 토큰화부터 인코더·디코더·다음 토큰 확률 계산까지 손으로 따라갈 수 있게 함
  • 원 논문의 큰 설정 대신 4차원 임베딩, 2개 어텐션 헤드, 8차원 피드포워드 계층을 사용해 행렬 곱셈과 softmax 흐름을 작게 만듦
  • 인코더는 토큰 임베딩에 위치 인코딩을 더한 뒤, 멀티헤드 self-attention과 피드포워드 계층을 거쳐 입력 시퀀스의 문맥 표현을 만듦
  • 디코더는 SOS에서 시작해 이전 생성 토큰과 인코더 출력을 함께 사용하며, encoder-decoder attention에서 query는 디코더, key/value는 인코더 출력에서 계산함
  • 마지막 디코더 임베딩은 선형 계층과 softmax를 거쳐 다음 토큰 확률이 되지만, 예제는 무작위 가중치라 실제 번역 품질을 기대하지 않음

목표와 전제

  • 트랜스포머 모델 내부에서 추론 시 수학이 어떻게 이어지는지 end-to-end 예제로 확인함
  • 계산을 손으로 따라가기 쉽도록 모델 크기를 크게 줄임
    • 원 논문의 임베딩 차원 512 대신 예제는 4차원 사용
    • 어텐션 헤드는 원 논문 8개 대신 2개 사용
    • 피드포워드 차원은 원 논문 2048 대신 8차원 사용
  • 필요한 전제는 기본적인 선형대수이며, 대부분의 계산은 행렬 곱셈으로 진행됨
  • 트랜스포머가 “무엇인지”보다 실제 계산이 어떻게 진행되는지에 초점을 둠
  • 직관적 설명은 The Illustrated Transformer와 함께 읽기 좋고, 원 논문은 Attention is all you need

인코더 입력 만들기

  • 토큰화

    • 머신러닝 모델은 텍스트가 아니라 숫자를 처리하므로, 입력 텍스트를 토큰 ID로 바꿈
    • 예제는 단순화를 위해 "Hello World""Hello""World" 두 단어 토큰으로 나눔
    • 실제 토큰화 방식은 단어 기반, 문자 기반, subword 기반으로 나뉠 수 있음
    • 단어 기반은 큰 vocabulary가 필요하고 "dog""dogs"를 서로 다른 토큰으로 다룸
    • 문자 기반은 vocabulary가 작지만 의미 정보가 적을 수 있음
    • subword 토큰화는 단어와 문자 방식의 중간 지점이며, 통계적 과정으로 토크나이저를 학습함
  • 토큰 임베딩

    • 토큰 ID 자체에는 의미가 없으므로, 각 토큰을 고정 크기 벡터인 임베딩으로 변환함
    • 예제 임베딩은 임의 값을 사용함
      • Hello -> [1, 2, 3, 4]
      • World -> [2, 3, 4, 5]
    • 실제 트랜스포머에서는 임베딩 매핑도 학습되며, 모델이 작업에 맞는 토큰 표현을 학습함
    • 두 임베딩은 하나의 행렬로 묶여 이후 행렬 곱셈에 사용됨
  • 위치 인코딩

    • 임베딩만으로는 단어의 문장 내 위치를 알 수 없어 위치 인코딩을 더함
    • 원 논문은 고정된 sine/cosine 위치 인코딩을 사용하며, 예제도 같은 방식을 따름
    • 예제의 위치 인코딩은 다음과 같이 계산됨
      • Hello -> [0, 1, 0, 1]
      • World -> [0.84, 0.99, 0, 1]
    • 토큰 임베딩과 위치 인코딩을 더해 인코더 입력 행렬을 만듦
      • Hello -> [1, 3, 3, 5]
      • World -> [2.84, 3.99, 4, 6]

Self-attention 계산

  • Q, K, V 만들기

    • self-attention은 입력 임베딩에서 query(Q), key(K), value(V) 를 계산함
    • 예제는 2개의 어텐션 헤드를 사용하며, 각 헤드는 별도의 WQ, WK, WV 행렬을 가짐
    • 각 가중치 행렬은 4차원 임베딩을 3차원 query/key/value로 변환함
    • 첫 번째 헤드에서는 입력 행렬과 WK1, WV1, WQ1를 곱해 K1, V1, Q1을 얻음
  • Attention 공식

    • 어텐션 점수는 네 단계로 계산됨
      • query와 각 key의 내적을 계산함
      • key 차원의 제곱근으로 나눔
      • softmax로 양수이며 합이 1인 가중치로 바꿈
      • 가중치로 value 벡터를 가중합함
    • 이 과정은 원 논문의 공식으로 압축됨
    • [
    • Attention(Q,K,V) = \text{softmax}\left(\frac{QK^\top}{\sqrt{d}}\right)V
    • ]
    • 예제에서는 작은 차원과 임의 초기값 때문에 softmax 결과가 거의 0과 1로 치우침
    • 큰 dot product 값은 softmax에서 더 강하게 증폭될 수 있어, key 차원의 제곱근으로 나누는 스케일링이 필요함
    • 설명을 위해 일시적으로 sqrt(3) 대신 30으로 나누는 변형도 사용하지만, 장기적인 해결책은 아님
  • 멀티헤드 어텐션 출력

    • 각 헤드의 어텐션 결과를 concatenate한 뒤, 학습되는 가중치 행렬을 곱해 다시 임베딩 차원으로 되돌림
    • 예제에서는 두 헤드 결과를 합쳐 6차원 행렬을 만들고, 이를 4차원 출력으로 변환함
    • 이 출력은 인코더 블록의 다음 단계인 피드포워드 계층으로 전달됨

피드포워드 계층과 인코더 블록

  • 피드포워드 계층

    • self-attention 뒤에는 피드포워드 신경망(FFN) 이 위치함
    • FFN은 두 개의 선형 변환과 그 사이의 ReLU 활성화로 구성됨
    • 첫 번째 선형 계층은 차원을 확장하고, 두 번째 선형 계층은 차원을 원래 크기로 줄임
    • ReLU는 음수를 0으로 만들고 양수는 그대로 유지해 비선형성을 추가함
    • 예제에서는 4차원 입력을 8차원으로 확장한 뒤 다시 4차원으로 줄임
    • [
    • \text{FFN}(x) = \text{ReLU}(xW_1 + b_1)W_2 + b_2
    • ]
  • 인코더 블록

    • 하나의 인코더 블록은 멀티헤드 어텐션과 FFN으로 구성됨
    • 원 논문은 6개의 인코더를 쌓으며, 예제 코드도 n=6으로 인코더를 반복함
    • 여러 인코더 블록을 단순히 통과시키면 값이 너무 커져 softmax 계산에서 overflow가 발생하고 nan이 나올 수 있음

Residual connection과 layer normalization

  • 값 폭주 문제

    • 예제에서 6개 인코더를 통과시키자 overflow encountered in expinvalid value encountered in divide 경고가 발생하고 출력이 nan이 됨
    • 값이 너무 커지고 다음 계층에서 더 커지는 현상은 깊은 신경망에서 흔한 문제임
    • backpropagation 중 gradient가 너무 커지는 경우는 gradient explosion이라고 부름
  • Residual connection

    • residual connection은 계층 입력을 계층 출력에 더하는 방식임
    • [
    • \text{Residual}(x) = x + \text{Layer}(x)
    • ]
    • 예제에서는 어텐션 출력과 FFN 출력에 각각 residual connection을 적용함
    • residual connection은 vanishing gradient 문제를 완화하는 데 사용됨
  • Layer normalization

    • layer normalization은 각 임베딩 차원에 대해 평균 0, 표준편차 1이 되도록 정규화함
    • 수식은 다음과 같음
    • [
    • \text{LayerNorm}(x) = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \times \gamma + \beta
    • ]
    • (\epsilon)은 표준편차가 0일 때 0으로 나누는 문제를 피하기 위한 작은 값임
    • (\gamma)와 (\beta)는 scaling과 shifting을 제어하는 학습 파라미터임
    • residual connection과 layer normalization을 추가한 뒤에는 6개 인코더를 통과해도 nan 없이 정상적인 값이 나옴

디코더 구조

  • 디코더 입력과 생성 방식

    • 디코더는 인코더 출력과 지금까지 생성된 출력 시퀀스를 입력으로 받음
    • 추론 중에는 SOS(start-of-sequence) 토큰에서 시작함
    • 디코더는 autoregressive 방식으로 한 번에 하나의 토큰을 생성함
      • 1회차: SOS를 입력으로 받아 "hola" 생성
      • 2회차: SOS + hola를 입력으로 받아 "mundo" 생성
      • 3회차: SOS + hola + mundo를 입력으로 받아 EOS 생성
    • EOS(end-of-sequence) 토큰이 생성되면 디코딩을 멈춤
    • 인코더는 한 번의 forward pass로 표현을 만들 수 있지만, 디코더는 여러 번 forward pass를 해야 하므로 느림
  • 디코더 블록 구성

    • 디코더 블록은 인코더 블록보다 복잡하며 다음 순서로 구성됨
      • masked self-attention
      • residual connection과 layer normalization
      • encoder-decoder attention
      • residual connection과 layer normalization
      • 피드포워드 계층
      • residual connection과 layer normalization
    • 추론 예제에서는 SOS 임베딩에 위치 인코딩을 더해 [1, 1, 0, 1]을 사용함
    • 학습 중에는 미래 토큰을 볼 수 없도록 attention score를 -inf로 마스킹하는 masked self-attention을 사용함

Encoder-decoder attention

  • encoder-decoder attention은 디코더가 입력 문장의 관련 부분에 집중하도록 만드는 단계임
  • self-attention과 계산 방식은 같지만, Q/K/V를 만드는 입력이 다름
    • query는 이전 디코더 계층 출력에서 계산함
    • key와 value는 인코더 출력에서 계산함
  • 이 구조 덕분에 디코더의 각 위치가 입력 시퀀스의 모든 위치를 참고할 수 있음
  • 번역처럼 출력 토큰이 입력 문장의 관련 위치에 의존해야 하는 작업에 유용함

출력 토큰 생성

  • Linear layer와 softmax

    • 디코더 출력은 바로 단어가 아니므로, 마지막 임베딩을 선형 계층에 통과시켜 vocabulary 크기의 logits 벡터로 바꿈
    • 예제 vocabulary 크기는 10이며, 다음 토큰 후보는 아래와 같음
      • hello, mundo, world, how, ?, EOS, SOS, a, hola, c
    • logits는 softmax를 거쳐 각 토큰의 확률 분포가 됨
    • 예제 확률에서는 "hola"가 가장 높은 확률을 가져 다음 토큰으로 선택됨
    • 항상 가장 높은 확률의 토큰을 고르는 방식은 greedy decoding이며, 항상 최선은 아님
    • 생성 기법은 Hugging Face 글에서 더 자세히 볼 수 있음
  • 전체 생성 루프

    • 전체 생성 절차는 다음 흐름을 따름
      • 입력 시퀀스를 임베딩으로 변환함
      • 인코더가 입력 전체의 문맥 표현을 생성함
      • 디코더는 SOS에서 시작해 이전 생성 토큰과 인코더 출력을 함께 사용함
      • 마지막 디코더 임베딩에 linear layer와 softmax를 적용함
      • 가장 가능성 높은 다음 토큰을 선택해 시퀀스에 추가함
      • EOS가 나오거나 최대 길이에 도달할 때까지 반복함
    • 예제 실행은 hello world 입력에 대해 SOS hola mundo world를 생성함
    • 모든 가중치와 임베딩을 무작위로 사용했기 때문에 결과는 좋은 번역이 아니며, 이는 기대된 동작임

결론과 범위

  • 예제는 트랜스포머 핵심 구성요소인 임베딩, 위치 인코딩, self-attention, 멀티헤드 어텐션, FFN, residual connection, layer normalization, encoder-decoder attention, softmax 출력을 한 흐름으로 연결함
  • 최신 트랜스포머 아키텍처는 여러 기법을 추가하지만, 핵심 수학은 이 예제에서 다룬 구조에 기반함
  • 작업 유형에 따라 사용하는 스택이 달라질 수 있음
    • 분류처럼 이해 중심 작업은 인코더 스택 위에 linear layer를 둘 수 있음
    • 번역처럼 생성 중심 작업은 인코더와 디코더 스택을 함께 사용할 수 있음
    • ChatGPT나 Mistral 같은 자유 생성 작업은 디코더 스택만 사용할 수 있음
  • 학습 과정은 다루지 않고, 기존 모델을 사용할 때의 추론 수학을 이해하는 데 초점을 둠
  • 더 형식적인 수학 자료는 이 PDF를 참고할 수 있음

댓글과 토론

Hacker News 의견들
  • Transformer의 “미스터리”는 각 층에서 정적인 가중치와 값을 선형 순서로 곱하는 대신, 같은 입력에 학습된 가중치를 곱해 얻은 3개의 행렬을 만들고 그 행렬들을 서로 곱한다는 데 있음
    병렬성이 늘어나 잘 작동하지만, 어텐션 공식 자체가 고정되어 있어서 매우 제한적임
    더 발전하려면 계산 그래프 자체를 학습 가능한 매개변수로 일반화할 방법이 필요해 보임. 작은 변화가 성능의 큰 변화로 이어지는 혼돈 효과 때문에 전통적인 기울기 방식으로 가능한지는 모르겠고, 내부적으로 유전 알고리즘이나 입자 군집 최적화 같은 형태가 필요할 수도 있음

    • 그 설명은 전혀 맞지 않음. Transformer의 특별한 점은 시퀀스의 각 요소가 다른 모든 요소에서 자기에게 중요한 부분을 고르고, 그것을 뽑아 계산할 수 있게 한다는 데 있음
      RNN에 비해 큰 이론적 장점은 이것을 손실 없이 지원한다는 점임. 각 요소가 시퀀스의 다른 모든 요소, 또는 시간 순서에서는 앞선 요소들의 전체 정보에 접근할 수 있기 때문임
      반면 RNN과 “선형 Transformer”는 과거 값을 압축하므로, 긴 시퀀스의 마지막 요소가 첫 요소의 모든 정보에 접근하기는 보통 어렵고, 내부 상태가 매우 커서 아무 정보도 버리지 않는 경우가 아니면 불가능함
    • “계산 그래프를 학습 가능한 매개변수로 일반화해야 더 발전한다”는 건 꽤 과감한 말임. 계산 그래프 학습 없이도 이미 엄청난 진전이 있었기 때문임
    • 이 방식은 기본적으로 일부 경로를 무시하고 더 중요한 것을 증폭하도록 배울 수 있으며, 그다음 품질 손실이 크지 않은 경로를 잘라낼 수 있음
      문제는 여기서 얻는 게 별로 없다는 것임. 행렬 곱셈이 아닌 연산은 더 느리거나 비슷한 속도일 가능성이 큼
    • 계산 그래프를 학습 가능한 매개변수로 일반화한다는 점에는 동의함. LLM이 풀었으면 하는 문제, 즉 Transformer가 잘하는 “언어 처리”를 넘어 실제 추론에 가까운 문제를 인간의 정신 과정이 푸는 방식과 비슷해 보임
      다만 흐름 제어를 넣으면 사실상 Turing machine이 될 위험이 있고, 그렇게 되면 말한 대로 학습이 문제가 됨. 그래도 완전히 다루기 어려운 문제는 아닐 수도 있음
    • 하이퍼파라미터 튜닝도 계산 그래프를 학습하는 방향으로 어느 정도 나아가고 있음. 다만 매우 제한적이고 훨씬 더 많은 학습이 필요함
  • 더 건조하고 형식적이며 간결한 설명을 원하면 John Thickstun의 “The Transformer Model in Equations” [0]이 있음
    표준 수학 표기법으로 전체가 한 페이지에 들어감
    [0] https://johnthickstun.com/docs/transformers.pdf

    • 드디어 이런 설명이 나와서 좋음. 수학 표기 7줄이 정성적인 잡담 여러 페이지보다 훨씬 낫다고 봄
      머신러닝 연구자들이 수학을 전혀 공부하지 않은 것처럼 보일 때가 많음
    • 논문을 읽다가 이런 내용을 개인 노트에 몇 번씩 짜깁기해야 했고, 뭔가 빠뜨린 게 아닌지 늘 확신이 없었음
  • “NaN이 나온다, 값이 너무 커서 다음 인코더로 넘어가며 폭발한다, 이것이 기울기 폭발이다”라는 설명은 이해한 바로는 틀림
    여기서는 어느 지점에서도 기울기를 계산하지 않으므로 기울기 폭발이 아님
    문제는 softmax 구현 쪽으로 보이고, 수치적으로 안정적인 softmax 구현 방법은 여기 [0]에 설명되어 있음
    [0]: https://jaykmody.com/blog/stable-softmax/

    • 맞음. 흔한 학습 문제인 기울기 폭발과 기울기 소실을 softmax가 큰 값에 민감한 문제와 연결하려 했는데, 오해를 부르거나 부정확하다는 데 동의해서 그 부분은 다시 쓸 예정임
      다만 신경망 전체가 큰 값에 민감하므로, 수치적으로 안정적인 softmax만으로 해결되지는 않음. 네트워크가 작동하려면 정규화가 핵심임
  • Transformer 튜토리얼은 새로운 Monad 튜토리얼이 될지도 모름. 이해하기 어려운 개념이지만, 예제를 연습하며 씨름해야 이해되는 종류임
    컴퓨터과학의 많은 부분이 그렇듯이

    • Transformer를 이해하는 순간, 더 이상 그것을 설명할 수 없게 됨
    • Monad가 어려운 개념이라고? Monad는 그냥 endofunctor 범주에서의 monoid일 뿐인데 뭐가 문제임?
    • “You could have invented transformers”라는 제목의 블로그 글을 기다리는 중임
  • 여섯 문단만 읽었는데 벌써 질문이 생김
    Hello -> [1,2,3,4] World -> [2,3,4,5]에서 벡터가 무작위라지만 패턴이 있어 보임. 두 벡터에 모두 있는 2가 뭔가 의미하는 건지, 아니면 전체 집합이 고유성을 만드는 건지 궁금함

    • 숫자가 재사용된 건 그냥 저자가 조금 게을렀던 것에 가까움. 두 벡터가 비슷한 방향을 가리키는지 보거나 각도를 계산해서 유사도를 추정할 수 있음
      여기서는 약 60도 정도 떨어져 있고 어느 정도 같은 방향인데, 예시에 음수를 넣지 않으려 해서 실제보다 벡터들이 더 비슷해진 영향이 큼
      숫자가 재사용됐다는 사실 자체는 의미가 없음. 첫 번째 위치의 1은 두 번째 위치의 1과 거의 관련이 없음. 이 벡터 위에서 합성곱을 하는 것도 아니기 때문임
    • 좋은 예시는 아님. 각 토큰의 벡터는 각 원소가 정규분포에서 뽑혀 무작위 초기화
      학습 후에는 비슷한 단어들이 어느 정도 코사인 유사도를 갖겠지만, [1,2,3,4][2,3,4,5]만큼 코사인 유사도가 높아지는 경우는 거의 없음
  • 완전히 관련된 질문은 아니지만, Transformer가 단순히 “다음 토큰 예측기”처럼 동작하면서도 다음 같은 질문을 처리할 수 있는 이유를 다룬 글이나 논문을 찾고 있음

    1. 학습 데이터셋에서 보지 못한 알 수 없는 단어, 하위 단어, 토큰을 처리하는 경우. 예: pandas에서 "sdsfs_ff", "fsdf_value"를 열로 가진 표를 만들기
    2. 학습 데이터에 없던 예시를 만들고 LLM에게 비슷한 출력을 요구하는 경우
      흔한 질문일 것 같은데 검색할 키워드를 못 찾겠음. 위치 임베딩에 대해 깊게 다룬 링크도 있으면 좋겠고, 사인/코사인을 쓰는 이유와 곱셈 대 덧셈에 대해서도 만족스러운 답을 아직 못 얻었음
    • 추측하자면 단일 문자도 토큰으로 인코딩될 수 있지만, 모델 안에서 이를 처리하는 데 더 많은 대역폭이 쓰이고, 구체적인 단어 토큰에 비해 기본적으로 담긴 의미는 적음
      모델이 필요하다고 판단하면 알 수 없는 시퀀스를 단일 문자 토큰을 복사해 재현하거나, 맥락상 말이 되면 새로 만들어낼 수 있음
    • P(X_1=x_1, X_2=x_2, X_3=x_3) = P(X_3=x_3 | X_1=X_1, X_2=x_2) • P(X_1=x_1, X_2=x_2)
      = P(X_3=x_3 | X_1=X_1, X_2=x_2) • P(X_2=x_2 | X_1=x_1) • P(X_1=x_1)
      즉 이전 토큰들이 주어졌을 때 다음 토큰에 대한 올바른 조건부 확률분포가 있으면, 토큰 시퀀스 전체에 대한 올바른 확률분포도 만들어짐
      “토큰 시퀀스에 대한 올바른 확률분포”, 또는 어떤 조건이 주어진 토큰 시퀀스의 올바른 조건부 확률분포는 사실상 거의 모든 종류의 입력/출력 동작을 그런 말로 설명할 수 있음
      그래서 “다음 토큰을 예측해서 작동한다”는 것은 원칙적으로 어떤 입력/출력 행동을 할 수 있는지에 대한 큰 제약이 아님
      어떤 인상적인 일을 하더라도, 그 출력이 P(X_{n+1}=x_{n+1} | X_1=x_1, ..., X_n=x_n)에서 나온다는 것, 즉 “다음 토큰 예측”이라는 사실과 충돌하지 않음
    • 학습 데이터의 정확한 문자열을 재생산하는 게 아니라, 패턴과 패턴의 패턴을 재생산하는 것임
      다음 토큰 예측은 들리는 것보다 더 지능적인 작업임
  • “복잡도는 단계 수와 매개변수 수에서 온다”는 말에 동의함
    우리가 이해할 만큼 단순한 Transformer 모델은 흥미로운 일을 못 하고, 흥미로운 일을 할 만큼 복잡한 Transformer는 우리가 이해하기엔 너무 복잡해 보임
    이해할 만큼 단순하면서도 흥미로운 일을 할 만큼 복잡한 중간 규모 모델을 연구해 보고 싶음

    • 이미 익숙하지 않다면 기계적 해석 가능성 분야의 연구가 흥미로울 수 있음. Neel Nanda가 이 주제에 대해 접근하기 쉬운 자료를 많이 올려두었음: https://www.neelnanda.io/mechanistic-interpretability
    • 그 양쪽 경계가 실제로는 이렇게 생겼을 것 같음. 중간 지대는 이미 인간이 제대로 이해하기엔 너무 복잡하지만, 동시에 흥미로운 일을 하기엔 아직 너무 작은 영역일 가능성이 큼
  • 개념을 정의하거나 소개하지 않고 쓰면 이해하기 어려움. Encoder 섹션이 그것이 무엇인지, 전체 과정의 어디에 놓이는지 설명 없이 바로 시작됨
    저자가 무엇을 하려는지는 알겠지만, 아이디어를 먼저 소개하고 설명한 뒤 사용하는 기본적인 글 구조가 빠져 있음
    이미 이 주제를 공부 중이고 절반쯤 이해한 사람이 아니라면 글 전체가 혼란스럽게 느껴짐

  • ANN을 처음부터 작성해 본 적이 있고 TensorFlow는 쓰지 않았는데도 이 설명은 여전히 혼란스러움
    ChatGPT에게 MatrixVector라는 말을 쓰지 않고 기본 ANN을 self-attention을 구현하도록 바꾸는 방법을 설명해 달라고 했더니 꽤 단순한 설명을 줬음. 아직 구현해 보지는 않았음
    모든 것을 노드, 가중치, 층의 관점에서 생각하는 편이 더 좋음. 행렬과 벡터는 ANN 내부에서 실제로 벌어지는 일과 연결하기 더 어렵게 만듦
    익숙한 ANN 작성 방식에서는 각 입력 노드가 스칼라이지만, 순전파 알고리즘은 모든 입력 노드에 가중치를 곱하고 더하므로 벡터-행렬 곱셈처럼 보임. 이런 설명들에 잘못된 마음가짐으로 접근하고 있는 느낌이고, 필요한 배경지식이 부족한 것일 수도 있음