요즘들어 챈에 정보공헌을 별달리 못한거 같아 오랫만에 스터디 글을 써보려고 합니다. 오늘의 주제는 트랜스포머의 의미입니다


약 1년동안 생성형 AI 에 관련되어 직접 뭔가를 만들거나 응용해보거나 하면서 새삼 깨닫게 된 것은 트랜스포머의 범용성이었습니다. 전자, 전기에서는 트랜지스터의 발명이 컴퓨터 시대를 개막한 것과도 같이 AI 에서는 트랜스포머가 생성형 AI 의 시대를 열었다고 해도 과언이 아닌데요,


문제는 트랜스포머가 너무 범용적으로 쓰이고 있다보니 정작 그것을 쓰는 사람의 입장에서는 이것의 의미가 무엇인지 직관적으로 파악하기가 어려운 것 같습니다. 그래서 좀 더 자세히 알아보려고 관련 자료를 보게 되면 대부분 원 논문 attention is all you need 를 해설하는 내용을 만나게 되는데, 원 논문은 수학과 딥러닝에 익숙하지 않은 사람에게는 너무나 높은 벽입니다.


보통 사람들이나 입문자에게 필요한 것은 코드나 수학, 딥러닝에 관련된 기본지식이 부족해도 트랜스포머의 개념을 이해할 수 있는 해설인 것 같아서, 제가 1년동안 공부한 내용을 바탕으로 자료를 만들어보았습니다. 언제나 그렇듯이 이해를 돕기 위해 왜곡하거나 생략한 부분이 있을 수 있습니다. 이런 부분 외에 제가 실수하거나 놓쳤다라고 생각하는 부분을 발견하신다면 언제든 댓글로 지적, 정정 부탁드립니다.


이 게시물은 한번에 완성하지 않고, 생각나는대로 내용이나 그림같은 참고자료를 보강하는 식으로 업데이트할 것입니다. 이해가 잘 안되는 부분이 있는 분은 질문을 남겨주시면 제가 이해를 돕기 위한 참고 이미지등을 찾아다가 붙이는 식으로 업데이트하겠습니다.

 

비유하자면 초심자에게 bohr의 원자모형을 설명하면서 전자가 원자핵을 공전하는 것처럼 묘사하는 수준입니다. 실제로는 전자가 원자핵 주변의 껍질 어딘가에서 랜덤하게 튀어나왔다 사라진다를 반복하고 설명하는게 더 현실에 가깝겠지만, 너무 많은 정보, 비직관적인 정보를 초심자에게 몰아주는 것은 아무래도 부담스럽습니다. 양자역학적인 관점에서 접근하는 것이 아니라 화학이나 기타 응용분야에서 접근한다면 전자가 원자핵 주변을 공전한다라는 설명으로도 도움이 될 수 있습니다.




트랜스포머가 하는 일을 간단히 표현하자면 '정보에 의미를 주입' 하는 것입니다.


함수로 생각하자면, 

  - 입력값은 일정 길이의 sequence length 로 정의된 d 차원 성분을 가진 tensor이며, 

  - 출력값도 똑같이 sequence length 와 d 차원 성분을 가진 tensor 입니다. 

  - 입력값과 출력값의 모양이 동일하기 때문에 여러개의 transformer 를 쌓아 올릴 수 있습니다. 

  - 이때 트랜스포머 한 층을 보통 transformer block이라고 부릅니다.


즉 , 

  입력 -> Transformer Block 1 -> 출력 ->Transformer Block 2 -> 출력 -> Transformer Block 3 -> ... -> 최종 출력

같은 형태로 구성할 수 있습니다. 여러 층을 쌓아서 돌리게 되면 그만큼 의미를 확실하게 주입할 수 있습니다.


몇가지 트랜스포머 응용 아키텍처의 사례를 갖고 살펴보자면

llama2 (https://huggingface.co/docs/transformers/model_doc/llama2

 - d 차원 성분 = 4096 입니다. 토큰이 변환된 엠베딩은 4096차원 공간 내의 존재하는 벡터입니다.

 - num_hidden_layers = 32 개의 트랜스포머 블럭을 쌓아올렸다라고 되어 있습니다.

 - num_attention_heads = 32 개의 어텐션 헤드를 갖고 있다라고 되어 있습니다. 레이어의 갯수와 어텐션헤드의 갯수의 조합에 의해 모델의 전체 크기가 좌우됩니다. 7B, 13B, 70B 등의 용량

 - max_position_embeddings = 4096 까지의 길이를 가진 엠베딩을 입력으로 받게 되어 있습니다. 하지만 고정된 것은 아니고 늘리는 방법들이 존재합니다.


CLIP (https://huggingface.co/docs/transformers/v4.37.2/en/model_doc/clip#overview)

 - 멀티모달에 많이 쓰이는 모델입니다. LLama 모델은 디코더 모델이기 때문에 텍스트를 생성해낼 수 있는 반면, CLIP 은 인코더 모델이기 때문에 텍스트나 이미지로부터 엠베딩을 만들어내는 일을 수행합니다. 스테이블 디퓨전에서 사용되며, 프롬프트를 알아듣는 기능을 담당합니다. 

 - d 차원 성분 = 512 입니다

 - num_hidden_layers = 12

 - num_attention_heads = 8

 - max_position_embeddings = 77 까지의 길이를 가진 엠베딩을 입력으로 받게 되어 있습니다. 역시 늘리는 방법들이 존재합니다.



다시 정리하자면 트랜스포머는 기능적으로 엠베딩의 시퀀스를 받아서 같은 크기의 엠베딩을 만들어주는 함수입니다. 보통은 시퀀스를 입력할 때 여백부분을 pad_token 같은 것으로 채워넣어서 일정한 크기를 맞춰서 넣게 됩니다. (왜? = flash attention 같은 최적화를 돕기 위해서?)




트랜스포머를 쓰는 이유는 의미(semantic) 를 주입하게 위해서라고 했습니다. 이제 이 부분을 설명하겠습니다.


인간은 의미라는 것을 어떻게 이해할까요? 우리는 의미를 모르겠으면 보통 사전을 찾아보거나 검색을 합니다. 사전을 찾아보는 것은 복잡한 의미를 더 간단한 의미로 분해하는 역할을 해줍니다만, 너무 간단한 개념에 도달하게 되면 대답하기가 쉽지 않습니다. 


예를 들면 '빨갛다' '빠르다' '집합' 같은 단어는 사전을 찾아봐도 도움이 되지 않습니다. 하지만 우리는 태어난 이후 여러가지 감각적인 정보를 습득하면서 따로 누가 가르쳐주지 않아도 (unsupervised) 저러한 개념들을 터득합니다.


기본적인 레벨로 내려가게 되면 의미란 것은 관련된 기억의 연관성에서 파악하게 됩니다. 우리가 빨갛다라는 것을 정의하게 되는 것은 빨간 것과 빨갛지 않은 것을 구분하게 되면서 파악하게 됩니다. 만약 적록색맹으로 태어나서 빨간색과 녹색이 완전히 똑같이 보이는 사람이라면 그 두개는 그냥 같은 개념으로 인식될 수 밖에 없습니다. 

마찬가지로 빠른 것, 많은 것 등의 개념들도 그 개념 단독으로는 우리의 인식체계에서 자리를 잡을 수 없습니다. 빠르지 않은 것, 많지 않은 것과의 구분에 의해서 인식체계가 구성됩니다. 


이러한 점은 트랜스포머가 의미를 인식하는 것에서도 동일하게 작용합니다. 각각의 엠베딩은 hidden_size 로 정의된 벡터공간상의 한 지점을 차지하게 되는데, 트랜스포머에 학습이 이루어질 수록, 각 엠베딩은 의미상으로 가까운 것에서는 벡터 공간상에서도 가까운 쪽으로, 의미상으로 무관한 것은 벡터공간에서 멀어지는 방향으로 유도하게 됩니다. 



예전에 엠베딩에 대해 설명하면서 워드벡터 엠베딩 공간의 유명한 사례 man-woman+queen = king 같은 수식이 성립하는 것을 예로 든 적이 있습니다. 저런 수식은 각각의 개념 단어들이 이상적으로 엠베딩 공간의 위치를 찾아갔을 때 결과적으로 나타나는 현상입니다. 



저렇게 자리를 찾아가게 만드는 학습방법으로는 몇가지가 있습니다.

1. Masked Language Modeling

  BERT 같은 인코더 모델을 학습할 때 쓰는 방법입니다. 데이타셋 문장의 중간에 일정 비율 (15%정도) 로 구멍을 뚫어놓고, 그 부분이 무엇이었는지 맞히게 하는 식입니다.

  보통 인간이 사용하는 언어의 단어들은 의미에 중복이 많이 존재합니다. 따라서 어느정도 이상 길이의 문장은 일부 글자나 단어를 빼놓아도 전체 의미를 이해하는데 지장이 없습니다.

  BERT 같은 전체 구조를 만들어놓고 많은 문장을 대상으로 학습시키면 결과적으로 입력문장의 토큰 엠비딩시퀀스가 의미가 주입된 엠베딩의 시퀀스로 변화하게 됩니다.


2. Causal Language Modeling

 BERT 같은 인코더 모델은 문장 시퀀스를 받아서 의미가 주입된 엠베딩을 만드는 역할을 합니다. 하지만 엠베딩은 그 자체로는 사람이 읽을 수 있는 문장이 아닙니다. 사람이 읽을 수 있는 형태의 문장을 만들려면 엠베딩으로부터 토큰을 뽑아내야 합니다. 사람이 사용하는 문법에서는 문장을 단어 순서대로 읽게 되어 있으므로 차례대로 토큰을 만들어내게 됩니다.

GPT 같은 디코더 구조에서는 문장의 첫 부분을 주고 다음부분을 가려놓은 다음, 한 토큰씩 가려놓은 부분을 차례로 열어주면서 모델이 예측한 부분과 실제 문장의 내용을 비교해서 학습하게 만듭니다.


3. Contrastive Pretraining

 CLIP 같은 모델은 이미지와 텍스트 두가지를 동시에 같은 형태의 엠베딩으로 인코딩할 수 있는 모델입니다. 서로 다른 형태 (modality) 의 정보이기 때문에 위의 MLM 이나 CLM 같은 방법으로는 학습하는 것이 불가능합니다.

 CLIP 은 이 부분을 해결하기 위해서 순수하게 연관성이라는 정보만 가지고 같은 개념끼리는 가깝게, 다른 개념끼리는 멀어지게 하는 대조(contrast) 학습만 진행합니다. 


 - 예를 들면 개 그림, 고양이 그림, 사자 그림, 같은 이미지 n 장과 'a photo of a dog', 'a photo of a cat', 'a photo of a lion' 같은 문장 n 개를 입력해서 

 - 개 그림을 인코딩한 결과 (512 차원 벡터) 와 'a photo of a dog' 라는 텍스트를 인코딩한 결과 (역시 512 차원 벡터) 간의 의미상 거리 (cosine similarity) 는 가까와지고, 

 - 개 그림의 인코딩과 'a photo of a cat' 이라는 문장 인코딩의 거리는 멀어지는 쪽으로 학습을 시킨 것입니다

 

  각 임베딩의 벡터값의 내용물이 무엇인지는 중요하지 않습니다. 중요한 것은 다른 개념을 나타내는 임베딩과는 거리가 멀고,. 비슷한 개념과는 거리가 가깝기만 하면 되는 것이기 때문입니다.


상당히 비효율적인 방법일 수도 있지만, 인간의 인식체계를 닮아 있고, 그렇기 때문에 전통적으로는 하기 힘들던 일을 해내는 것. 그것이 트랜스포머의 매력입니다.





'의미의 주입' 에 대해서 대강의 개념 (의미 공간 내에서의 거리의 멀고 가까움) 을 설명했으니, 실제 사례를 갖고 설명해보겠습니다.


[텍스트의 경우]

apple 이라는 단어를 보면 여러분은 무엇이 연상되시나요? 먹는 사과? 아니면 컴퓨터 기업 애플?

모든 단어는 그 맥락 안에서만 의미가 정해집니다. 앞뒤에 무슨 문장이 놓여있었는지에 대한 정보가 없는 상태에서 apple 이란 단어의 의미는 애매합니다. 

  의미가 애매하다 = 엠베딩 공간 상에서 위치가 애매한 곳에 놓여져 있다.

  -> 위치가 없는 것은 아니고 어느정도의 초기 위치는 정해져 있습니다. GPT 같은 디코더 구조에서라면 apple 이라는 토큰을 받아서 엠베딩으로 변환해주는 레이어를 초반부에 거치게 되어있습니다. 다만 초반 부분의 엠베딩은 아직 다른 주변 맥락에 녹아들지 않았기 때문에 아직 '의미가 주입'되지 않은 상태라고 할 수 있습니다.


트랜스포머는 그렇다면 어떻게 의미를 주입할 까요? 그 비결은 바로 '검색' 입니다.


네. 그렇습니다. 트랜스포머는 사실상 미니 검색 엔진입니다. 그리고 검색하는 메커니즘의 이름은 바로 '어텐션' 입니다.


애플이라는 단어의 의미를 알기 위해서는 주변에 무슨 단어들이 있는지 둘러봐야 합니다. 애플이라는 단어가 속한 문장 (=입력받은 문장) 내부를 둘러보는 경우, 셀프 어텐션이라고 부릅니다. 입력받은 텐서 바깥에 있는 정보를 둘러보는 경우 크로스 어텐션이라고 부릅니다.


주변을 둘러보는 방법은 어텐션의 query, key, value 텐서들에 의해 구현됩니다. 대강 'apple' 이라는 단어를 갖고서 query 텐서를 구성하고 나서, 내 주변 문장의 단어들로부터 유도된 key 텐서로부터 검색합니다. 그리고 검색 결과가 된 key 로부터 value 벡터를 끄집어냅니다. 


뉴럴네트워크가 아닌 일반 python 코드로 key 들로부터 query 를 검색해서 value 를 끄집어내는 부분을 코드로 설명한다면

for k in keys:

    if query is similar to k:

        return values[k]

같은 식으로 생각해볼 수 있습니다. 트랜스포머에서는 텐서들을 이용하기 때문에 저렇게 for 루프를 돈다던가 하는 대신에 행렬곱 몇번으로 수천개의 항목 내에서 query 에 가장 가까운 key 를 찾아내게 되어있습니다. 


어쨌든 apple 이라는 단어의 주변 단어들을 검색해봤더니 macbook, tim cook 같은 단어들이 걸려나왔습니다. 아무래도 이번 예제에서의 apple 은 먹는 사과보다는 컴퓨터 기업의 이름이 확실해보입니다.

apple 이라는 엠베딩은 macbook, tim cook 같은 key 에 관련된 value 값의 영향을 받아서 변환됩니다. 아까의 apple 엠베딩은 의미공간상에서 뭔가 컴퓨터스러운 영역쪽으로 이동합니다. 


위에서 설명한 내용은 어텐션의 여러가지 헤드들 중 하나에서 일어나는 것 뿐입니다. 문장이 조리있게 서술되려면 여러가지 조건들을 만족해야 합니다. apple 이라는 단어에 영향을 줄 수 있는 관점에는 여러가지가 있습니다. 예를 들어 '문법에 맞는 문장'이라는 관점에서는 apple 이라는 단어의 주변에 'verb, noun, subjective' 등등의 어떤 것들이 있는지에 따라 영향을 주고 받습니다. 

어떤 관점으로 문장의 각 단어들의 의미를 주입할 것인가는 사람이 정한 것이 아닙니다. 그저 어텐션 헤드를 여러개로 분리하고 최대한 MLM 이나 CLM 같은 학습 방식에서 최종 loss 를 낮추는 방향으로 지속적으로 역전파를 시키다보면 각각의 어텐션 헤드들은 역할을 분리하는 쪽으로 저절로 움직입니다. 이것이 비지도 학습을 통해 범용적으로 패턴을 포착해내는 트랜스포머의 비결입니다




단어가 아니라 이미지의 경우로 생각해봅시다. 이미지의 한 부분에 '파란색' 영역이 있다고 합시다. 이 파란색은 무엇을 의미하는 것일까요? 하늘? 바다? 아니면 파란색 눈동자?


역시 주변 정보, 맥락으로부터 동떨어진 요소는 의미를 잃게됩니다. 반대로 주변에 무엇이 있는지를 알게되면 점점 의미가 주입됩니다.


학습에 의거하여 어떤 파란색 영역 (스테이블 디퓨전의 VAE 를 통해서 엠베딩으로 인코딩된 영역은 8*8 픽셀에 해당합니다) 의 주변에 하얀색 덩어리들이 보인다고 합시다. 주변을 볼 때는 전체적인 구도를 볼 수 있도록 일부러 해상도를 낮춘 버전으로도 보고, 중간해상도로도 보고, 원 해상도로도 보면 도움이 됩니다. 여러 해상도에서 여러가지 어텐션 헤드들을 통해서 파악한 결과 주변의 하얀색 덩어리들은 구름이라고 알고 있는 것과 유사도가 높아보입니다. 그렇다면 저 파란색 임베딩의 의미는 '하늘' 인 것 같습니다. 이번 트랜스포머 블럭의 결과로 임베딩의 위치는 의미없는 파란색 덩어리에서 하늘이라는 개념이 위치한 방향으로 옮겨가게 됩니다.


트랜스포머 구조의 특징중 하나는 각 임베딩에 위치정보를 넣을 수 있다는 점입니다. 이미지 모델의 경우 전체 이미지를 조각내서 각 조각마다 번호를 붙이는 식으로 구분을 합니다. 그 번호 정보는 임베딩에 그냥 붙일 수는 없고 텐서 모양을 맞춰지도록 positional encoding 으로 변환되어 붙게됩니다. 


전통적으로 이미지 정보, 시각정보를 이해하는 데에는 CNN 이라는 방법을 사용했었습니다. CNN 의 한계는 자기 주변정보를 파악하는데에 제약이 있다는 것입니다. 어떤 픽셀은 convolution kernel 로 정해놓은 자기 주변의 영역만 볼 수 있습니다. 해상도를 낮춘 다음에 또 컨벌루션을 통해서 조금 더 넓은 영역을 볼 수 있게 되긴 하지만, 높은 해상도 상태에서는 볼 수 있는 영역이 딱 정해져있는 것은 CNN의 큰 제약입니다. 그에 비해 비전 트랜스포머 구조에서는 한 픽셀이 다른 모든 픽셀에 대해 어텐션을 통해 맥락을 검색하기 때문에 훨씬 효과적으로 의미를 포착할 수 있습니다.


비전 트랜스포머모델도 마찬가지로 여러개의 어텐션 헤드들을 갖고 있습니다. 추론과정에서 각 헤드들의 활성도 정보를 뜯어서 시각화해보면, 어떤 헤드는 대상 픽셀의 가까운 주변 중심으로, 어떤 헤드는 대상 픽셀과 같은 가로축에, 어떤 헤드는 대상 픽셀의 세로축에, 어떤 헤드는 무작위하게 화면의 여기저기에 어텐션하고 있는 것을 볼 수 있습니다.



응용 정보

1. 

스테이블 디퓨전을 쓰다보면 CLIP_SKIP 이라는 개념이 등장합니다. 이것은 문장임베딩을 StableDiffusion의 CLIP 모델에서 넣고 트랜스포머 블럭을 통과시킬 때 끝까지 완전히 통과시키지 않고, 마지막 부분은 생략(skip) 하게 만드는 것입니다. 의미를 최대한 주입하지 않고 약간 설익은 상태로 놔두는 것인데, novelai 팀이 sd1.5 구조를 갖고 여러가지 실험을 하다가 경험적으로 발견한 부분, 저렇게 약간 설익은 상태로 두니까 novelai 식의 태그 (1girl, solo, spread legs, masterpiece, ...) 구조에 더 잘 맞더라는 것입니다. 


직관적으로 생각해보면 novelai tag 는 문장이 아니라 단어의 나열 형태로 되어 있기 때문에 일반적인 문장의 형식( a photo of a girl who is spreading legs ) 으로 학습된 CLIP 임베딩보다 마지막 CLIP의 트랜스포머 블럭 처리를 생략한 것이 좀 더 매칭이 잘 되는 형태인 것으로 이해할 수 있습니다.


2. 

SDXL 이라는 모델을 사용해보면 알 수 있는 차이점 중 하나가 SD15 보다 확실히 프롬프트를 잘 먹는다는 것입니다. 어떤 구조적 차이가 있기에 SDXL 은 프롬프트를 더 잘 이해하는 것일까요?

그 비결은 SDXL 은 CLIP 모델을 2개 사용한다는 점입니다.


SDXL 은 2개의 서로 다른 CLIP 모델을 사용합니다 첫번째는 CLIP ViT-L 이고 , 두번째는 OpenCLIP ViT-bigG 입니다. CLIP ViT-L 은 SD1.5 까지 사용하던 것과 동일하고 OpenCLIP ViT-bigG 가 SDXL 에서 추가된 것입니다.


첫번째 CLIP 모델은 기존과 동일하게 프롬프트의 각 토큰을 주고 Unet 이 디퓨전을 하는 과정에서 참고정보로 삼게 시킵니다. 이부분은 각 토큰의 엠베딩 단위에서 주로 일어나기 때문에, 결과 이미지의 부분 부분에 각 토큰이 영향을 주게 됩니다. 하지만 토큰단위로 전체 씬에 대한 설명 정보로의 통일성은 부족할 수 있습니다. 

이부분을 극복하기 위해서 전체 씬에 대한 설명은 두번째 더 큰 CLIP 모델을 통해서 만들어냅니다. 즉 전체 씬을 설명하는 문장을 1024 차원 벡터 하나로 만들어내고 이것을 추가적인 조건 정보로 활용합니다. 이것이 SDXL 의 마이크로 컨디셔닝이라는 개념입니다. 


생성형 AI 는 거칠게 말하자면 그냥 검색이 가능한 DB 의 일종입니다. 다만 정확히 입력한 내용만 찾아주는 DB가 아니고, 적당하게 근사값을 조합해주는 DB 라는 점에서 전통적인 DB 와 다른 점입니다.


DB 이기 때문에 입력한 내용을 찾으려면 인덱스가 필요합니다. 프롬프트나 마이크로 컨디셔닝은 그런 추가적인 인덱스라고 할 수 있습니다. IP-Adapter 나 control-net 등도 결국 하는 일은 인덱스를 추가한 것 뿐입니다.





positional encoding 은 전체 시퀀스 안에서 각 임베딩의 의미를 구분하는 데에 중요한 역할을 합니다. 그런데 검색이 잘 되게 하기 위해서는 몇가지 조건이 있습니다. 


위치정보를 넣을 때에는 절대적인 위치도 중요하고 상대적인 위치도 중요합니다. 순서도 중요하고요. 예를 들어서 cat eat dog 라는 문장과 dog eat cat 이라는 문장은 단어의 위치 때문에 각각 전혀 다른 의미가 되어야 합니다. 


그렇다면 cat eat dog 라는 문장과 ......... cat eat dog 같은 문장의 경우는 어떨까요? (...... 부분은 cat eat dog 와 별로 관계없는 토큰이라고 가정합니다) 어텐션에 있어서 cat 이 주변의 엠베딩을 검색할 때 절대적인 위치가 영향을 줄 수도 있고 안줄 수도 있어야 할 것 같습니다. (이부분은 여러개의 어텐션 헤드를 분리함으로써 해결합니다). 임베딩 공간에서 1 번위치에 해당하는 PE(Positional Encoding) 이 2번을 참조할때의 벡터와 뒷부분에 자리한 N 번째 PE (예: 37 번 PE) 가 N+1번 PE (예: 38번 PE) 를 볼 때 상대거리가 일정하게 나올 수 있을까요?


우리나라 딥러닝 벤처로 유명한 남세동님이 페이스북 텐서플로우 코리아에 올렸던 글에 유용한 정보가 있어서 발췌해 소개합니다


Positional Encoding 관련해서 PE(1)과 PE(2)의 거리와 PE(37)과 PE(38)의 거리는 다른가? 비슷한가? 아니면 완전히 똑같은가?

개인적으로 오랫동안 이해가 안 되던 부분인데, 오늘 (거의) 말끔하게 해결 되었기에 공유합니다. 저와 같이 이 부분 궁금하셨던 분들에게 도움이 되길 바랍니다.

우선 (저처럼) 코드가 더 편한 분들을 위해서 다음 코드를 실행해 봅시다.

---

import numpy as np

def position_encoding(pos, d_model=512):
pe = np.zeros(d_model)
for i in range(0, d_model, 2):
pe[i] = np.math.sin(pos / (10000 ** ((2 * i) / d_model)))
pe[i + 1] = np.math.cos(pos / (10000 ** ((2 * i) / d_model)))
return pe

pos1 = position_encoding(1)
pos2 = position_encoding(2)
pos3 = position_encoding(3)
pos4 = position_encoding(4)

print(np.linalg.norm(pos1 - pos2))
print(np.linalg.norm(pos2 - pos3))
print(np.linalg.norm(pos3 - pos4))

---

출력되는 값 3개가 동일합니다. pos들을 1,2,3,4가 아닌 다른 값들록 해도 결과는 동일합니다. 즉, 답부터 얘기하자면 PE(pos)와 PE(pos+k)의 거리(유클리디안 거리)는 동일합니다.

어떻게 이런 일이 가능한가? d_model=2, 즉 PE를 2차원으로 한다고 놓고 생각하면 이해하기 쉽습니다.

pe(pos)[0] = sin(pos / C) (C=상수)
pe(pos)[1] = cos(pos / C)

즉, pe(pos) = (sin(pos / C), cos(pos / C)) 입니다. 이 pe 값(점)을 xy 좌표계에 올려 놓으면, 그냥 pos는 각도고, pos를 아무리 변경해도 pe 점은 중심이 (0, 0)이고 반지름이 1인 원 위에 위치하게 되겠습니다.

따라서 pe(pos)와 pe(pos+k)는 각도만 바꿔준 것으로 생각할 수 있습니다. 이에 pos와 상관 없이 k에 의해서만 pe(pos)와 pe(pos+k) 간의 거리가 정해지는 것입니다.

그래서 PE 함수가 sin만 쓰지 않고 sin과 cos 두개를 같이 쓰는 것으로 이해할 수 있습니다.

이렇게 이해하고 보니 Transformer 논문의 PE에 대한 아래 문장, 그리고 다른 PE 설명글들이 훨씬 쉽게 이해 되네요.

“We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset k, PEpos+k can be represented as a linear function of PEpos.”

삼각함수와 선형대수를 잘 이해하고 있으면 이렇게 굳이 고딩 수학 수준으로 내려와서 이해할 필요는 없었을텐데요. 저는 겨우 이해했네요. (저와 같은) 어떤 분들에게는 도움이 되길 바랍니다. ㅠㅠ




응용정보 - 양자화


llama2 같은 언어모델은 상당히 용량이 큽니다. 13B 모델의 경우만 해도 그대로는 4090 의 24GB VRAM 에 로딩이 불가능합니다. 대신 8비트로 바꿔서 로딩하거나 4비트 같은 양자화 모델을 쓰면 됩니다. 양자화가 무엇이길래 이게 가능한걸까요?


잘 모르는 사람은 양자화가 무슨 양자컴퓨팅의 일종으로 생각하는 경우도 보았습니다만, 사실 그런 것과는 전혀 관계가 없고 그저 정보를 압축하는 방법의 일종입니다.


보통의 24bit 비트맵 이미지를 압축하는 방법은 여러가지 접근방법이 있는데, 그중 하나가 색상의 가짓수를 줄이는 것입니다. 24bit 로 색상 한개를 표현하게 되면 약 1600 만 가지 컬러를 표현하게 되지만, 보통의 이미지의 경우 그 색상을 다 쓰지 않아도 비슷하게 표현이 가능합니다. 원본 이미지에서 대표적인 색상을 256 가지만 뽑아놓고, 원본 이미지의 각 픽셀을 대응하게 해도 체감적으로는 크게 차이나지 않게 할 수 있습니다. 문제는 256 개를 어떻게 뽑냐는 것인데, 그 방법중 하나가 양자화 (quantization) 입니다.


언어모델의 경우 수십억개의 파라메터를 사용해서 문장을 처리하지만, 사실 여러개의 트랜스포머 블럭들과 기타 레이어들을 통과시켜서 최종적으로 얻고자 하는 결론은 '그래서 다음 단어는 뭐냐?' 입니다. 그리고 단어의 갯수는 언어모델의 경우 3만개 내외의 vocab 리스트로 이미 정해져 있습니다.

즉, 수십억개의 파라메터를 통해 원본 텍스트를 입력받아서 3 만개 토큰 중의 하나만 잘 찍으면 되는 것입니다. 

또한 트랜스포머를 통과하면서 처리되는 값들은 항상 일정한 범위 안에 있도록 정규화 단계를 거칩니다. 블럭을 통과할 때마다 값이 0 근처로 쪼그라든다던가, 점점 값의 스케일이 커진다던가, 점점 + 방향이나 - 방향으로 쏠림이 일어난다던가 하는 현상이 일어나면 안되기 때문입니다. 


1) 어느정도 값의 범위가 정해져 있기 때문에 

2) 여러단계를 거치면서 임베딩의 위치를 변화하는 역할을 각 파라메터들이 분담하기 때문에


파라메터의 숫자 하나를 표현하기 위해 온전히 모든 실수의 영역을 표현하지 않고도 충분히 의미가 전달됩니다.

여러가지 양자화 기법에서 실험한 결과에 의하면 6bit 정도면 거의 perplexity 에 영향을 주지 않을 수준이 된다고 알려져 있는데, 6bit 면 2^6 = 32 가지 경우의 수라고 할 수 있습니다. 숫자 하나를 블럭 하나에서 32개의 칸 중 하나로 넣고, 다음 블럭에서 또 다시 32개의 칸 중 하나로 옮기고 하는 과정을 각 블럭 갯수만큼 거치게 해서 최종적으로는 3만개 (vocab_size)의 칸 중 하나를 선택하는 것이니까 충분히 가능한 일입니다. 학습이 완벽하게 최적화된다면 6비트가 아니라 1bit 만 써도 가능할 수도 있을겁니다. 각 토큰마다 플러스 아니면 마이너스만 선택하게 해서 여러단계를 거쳐서 결론을 내는 것인데, 둘중 하나를 선택하는 간단한 스무고개 퀴즈도 나타낼 수 있는 정보는 2^20 에 달하니까, 블럭의 갯수가 충분하다면 1bit 양자화도 가능할 지도 모르는 것이지요.







오늘 준비한 내용은 여기까지입니다. 몇달동안 생각했던 것을 일단 두서없이 적다보니 장황한 글이 되었을 것 같지만, 혼자서만 정리하기보다는 일단 풀어놓고 다른 분들의 피드백을 받으면서 단계별로 정리하는 것이 좋을 것 같아서 일단 글을 올립니다.


최근에 초심자분들이 챈에 찾아오셔서 이것저것 많이 물어보시는 모습을 보게 되는데, 저는 좋은 현상이라고 생각합니다. 검색을 하면 우리 챈에 있는 내용들이 나오기 때문에 자연스럽게 찾아오시게 되는 것이라고 생각하는데요,


최대한 있는 정보들을 잘 정리하고, 최신으로 업데이트하고, 간단한 질문에 대해서 쉽게 답을 찾을 수 있는 환경 (챈 뉴비 대응용 챗봇을 개발한다던가?) 을 만들어서 모든 사람이 즐겁고 유익하고 효과적으로 정보를 공유할 수 있는 환경이 되었으면 합니다. 


일반인들을 위해서 생성형 AI에 관련된 양질의 정보들이 충분한 환경이라면, 본 채널이 마이너, 매니악, 음습한 채널로 활동하는 것도 좋겠지만, 현 시점에서 AI 공부하려는 사람들이 정보 검색하다보면 나오는 곳이 이 채널이라면 더 많은 사람들에게 유용한 정보를 제공하는 밝은 곳이 되어야 하지 않을까라는 것이 개인적인 생각입니다.


긴 글 읽어주셔서 감사합니다