1. transformer 아키텍처
2017년 구글에서 발표한 Attention is All you need 논문에서 처음 등장
머신러닝을 통해 언어를 번역하는 기계 번역 성능을 높이기 위한 방법을 연구하였는데, 당시 널리 사용된 RNN에 비해
성능 면에서 큰 폭으로 앞섰다.
또한 RNN에 비해 모델 학습 속도도 빨랐다.
이렇게 완전히 새로운 형태의 모델이 성능과 속도 면에서 뛰어난 모습을 보여
많은 인공지능 연구자들이 각자 연구에 transformer를 적용하기 시작
현재 transformer은 자연어 처리는 물론 컴퓨터 비전, 추천 시스템 등 모든 AI 분야에서 핵심 아키텍처가 되었다.
기존에 자연어 처리 문제에서 사용하던 RNN은 다음과 같이 텍스트를 순차적으로 하나씩 입력하는 형태다

사람이 글을 읽을 때 왼쪽에서 오른쪽으로 차례대로 읽는 것처럼 딥러닝 모델에 텍스트를 순차적으로 넣어준 것이다.
그림에서 xi는 i번째 텍스트 토큰이다.
토큰은 거의 모든 자연어 처리 연산의 기본 단위이고, 보통 단어보다 짧은 텍스트 단위이다.
h는 입력 토큰을 RNN 모델에 입력했을 때의 출력이다.
그림을 보면 이전 토큰의 출력을 다시 모델에 입력으로 사용하므로, 입력을 병렬적으로 처리하지 못한다.
이렇게 순차적으로 처리해야하기 때문에 학습 속도가 느리고, 입력이 길어지면 먼저 입력한 토큰의 정보가 희석되면서
성능이 떨어진다는 문제도 있었다.
또한 성능을 높이기 위해 층을 깊이 쌓으면 gradient vanishing이나 exploding이 발생하여 학습이 불안정했다.
transformer은 RNN의 이런 문제를 해결하기 위해 하나씩 순차적으로 처리하는 방식을 버리고,
self attention이라는 개념을 도입했다.
이는 입력된 문장 내의 각 단어가 서로 어떤 관련이 있는지 계산해서 각 단어의 표현 representation을 조정하는 역할을 한다.
이로 인해 다음과 같은 장점들이 생겼다.
1) 확장성: 더 깊은 모델을 만들어도 학습이 잘 된다. 동일한 블록을 반복 사용하여 확장이 용이하다.
2) 효율성: 학습할 때 병렬 연산이 가능하므로 학습 시간이 단축된다.
3) 더 긴 입력 처리: 입력이 길어져도 성능이 거의 떨어지지 않는다.

transformer 아키텍처는 위와 같다.
크게 encoder와 decoder로 나뉜다.
왼쪽은 언어를 이해하는 encoder이고 오른쪽은 언어를 생성하는 decoder이다.
공통적으로 입력을 embedding 층을 통해 숫자 집합인 embedding으로 변환한다.
그리고 positional encoding을 통해 문장의 위치 정보를 더한다.
encoder에서는 layer normalization, multi-head attention, feed forward 층을 거쳐 문장을 이해하고,
그 결과를 decoder의 cross attention으로 전달한다.
decoder에서는 encoder에서와 유사하게 layer normalization, multi head attention을 수행하여,
cross attention을 통해 encoder가 전달한 데이터를 출력과 함께 종합해서, feed forward 층을 거쳐 번역한다.
2.텍스트를 임베딩으로 변환하기
컴퓨터는 텍스트를 그대로 계산에 사용할 수 없다.
따라서 텍스트를 숫자 형식의 데이터로 변환해야한다.
텍스트를 모델에 입력할 수 있는 숫자형 데이터인 임베딩으로 변환하는 과정은

먼저 텍스트를 적절한 단위로 잘라 숫자형 아이디를 부여하는 토큰화(tokenization)
다음 토큰 아이디를 토큰 임베딩 층을 통해 여러 숫자의 집합인 토큰 임베딩으로 변환
마지막으로 위치 인코딩 층을 통해 토큰의 위치 정보를 담고 있는 위치 임베딩을 추가
3. 토큰화
텍스트를 적절한 단위로 나누고 숫자 아이디를 부여하는 것
한글은 작게는 자모 단위부터, 크게는 단어 단위로 나눌 수 있다.

토큰화를 할 때는 어떤 토큰이 어떤 숫자 아이디로 연결됐는지 기록해 둔 사전을 만들어야한다.
예를 들어 단어를 단위로 토큰화하는 경우 어떤 단어를 몇번으로 변환했는지 모두 저장한다.
큰 단위를 기준으로 토큰화할 수록 텍스트의 의미가 잘 유지되지만, 사전의 크기가 커진다.
단어로 토큰화를 하는 경우, 텍스트에 등장하는 단어 수만큼 토큰 아이디가 필요하기 때문이다.
또한 이전에 본적이 없는 새로운 단어는 사전에 없으므로, 처리하지 못하는 OOV(Out of Vocabulary) 문제가 발생한다.
반대로 작은 단위로 토큰화하는 경우 사전의 크기가 작고 OOV 문제를 줄일 수 있지만,
텍스트의 의미가 유지되지 않는다는 단점이 있다.
파리를 자모 단위로 나누면 ㅍ ㅏ ㄹ ㅣ로 나뉘는데, 이러면 파리의 의미가 사라진다.
이처럼 작은 단위, 큰단위 모두 장단점이 뚜렷하므로 최근에는 데이터에 등장하는 빈도에 따라 토큰화 단위를 결정하는
subword 토큰화를 사용한다.

이는 자주 나오는 단어는 단어 단위 그대로 유지하고,
가끔 나오는 단어는 더 작은 단위로 나눠 텍스트의 의미를 최대한 유지하면서도, 사전의 크기는 작고 효율적으로 유지한다.
예를 들어 인사말로 자주 사용하는 '안녕'이나 '대한민국'같은 국가 이름, 유명 사람 이름과 같이 자주 나오는 단어는 그대로 형태를 유지하지만,
한글 데이터에서 비교적 드물게 등장하는 외국어나 특수문자, 이모티콘 등은 작게 나눠 사전 크기가 커지지 않도록 한다.
한글의 경우 보통 음절과 단어 사이에서 토큰화 된다.
#띄어쓰기 단위로 분리
input_text = "나는 최근 파리 여행을 다녀왔다"
input_text_list = input_text.split()
print("input_text_list: ", input_text_list)
#토큰 > 아이디 딕셔너리와 아이디 > 토큰 딕셔너리 만들기
str2idx = {word:idx for idx, word in enumerate(input_text_list)}
idx2str = {idx:word for idx, word in enumerate(input_text_list)}
print("str2idx: ", str2idx)
print("idx2str: ", idx2str)
#토큰을 토큰 아이디로 변환
input_ids = [str2idx[word] for word in input_text_list]
print("input_ids: ", input_ids)
#출력 결과
#input_text_list: ['나는', '최근', '파리', '여행을', '다녀왔다']
#str2idx: {'나는': 0, '최근': 1, '파리': 2, '여행을': 3, '다녀왔다': 4}
#idx2str: {0: '나는', 1: '최근', 2: '파리', 3: '여행을', 4: '다녀왔다'}
#input_ids: [0, 1, 2, 3, 4]
최근 토큰화는 subword 토큰화가 기본이지만, 위 코드는 단어 단위로 토큰을 수행하는 코드이다.
"나는 최근 파리 여행을 다녀왔다"라는 문장을 띄어쓰기 단위로 분리하고, 각각 토큰에 0부터 토큰 아이디를 부여했다.
그리고 각각 토큰을 토큰 아이디로 변환해서 input_ids로 저장한다.
4. 토큰 임베딩으로 변환
딥러닝 모델이 텍스트 데이터를 처리하기 위해서 입력으로 들어오는 토큰과 토큰 사이의 관계를 계산할 수 있어야 한다.
토큰과 토큰 사이의 관계를 계산하기 위해 토큰의 의미를 숫자로 나타낼 수 있어야 한다.
토큰화에서 부여한 토큰 아이디는 하나의 숫자일 뿐이므로, 토큰의 의미를 담지는 않는다.
의미를 담기 위해 최소 2개 이상의 숫자 집합인 벡터여야 한다.
데이터를 의미를 담아 숫자 집합으로 변환하는 것을 임베딩(embedding)이라 한다.
import torch
import torch.nn as nn
embedding_dim = 16
embed_layer = nn.Embedding(len(str2idx), embedding_dim)
input_embeddings = embed_layer(torch.tensor(input_ids)) #(5,16)
input_embeddings = input_embeddings.unsqueeze(0) #(1,5,16)
input_embeddings.shape
#출력 결과
#torch.Size([1,5,16])
파이토치가 제공하는 nn.Embedding 을 사용하면 토큰 아이디를 토큰 임베딩으로 변환할 수 있다
nn.Embedding 클래스에 사전 크기가 len(str2idx) = 5이고 embedding_dim=16차원의 임베딩을 생성하는 임베딩 층을
embed_layer로 만들고, 입력 토큰을 임베딩 층을 통해 임베딩으로 변환한다.
torch.Size([1,5,16])으로 보아, 1개의 문장이 5개의 토큰을 가지고 16차원의 임베딩이 된다
embed_layer는 토큰의 의미를 담아 벡터로 변환하는 것인가? 그것은 아니다.
지금의 embed_layer는 input_ids를 16차원의 임의의 숫자 집합으로 바꿔줄 뿐이다.
임베딩 층이 단어의 의미를 담을려면, 딥러닝 모델이 학습 데이터로 훈련되어야 한다.

딥러닝에서는 모델이 특정 작업을 잘 수행하도록 학습하는 과정에서, 데이터의 의미를 잘 담은 임베딩을 만드는 방법도 함께 학습한다.
그림과 같이 입력 텍스트가 속한 카테고리를 맞추는 텍스트 분류 모델이 있을 때,
'나는 최근 파리 여행을 다녀왔다'라는 문장에 맞는 카테고리로 잘 분류하도록 학습된다.
그 과정에서 딥러닝 모델의 첫번째 단계인 임베딩 층도 함께 학습되면서 점차 토큰의 의미를 잘 담은 임베딩을 생성하게 된다.
5. 위치 인코딩
RNN과 transformer의 가장 큰 차이점은 입력을 순차적으로 처리하는지 여부다.
RNN은 입력을 순차적으로 처리하는데, 자연스럽게 입력 데이터의 순서 정보가 고려된다.
transformer은 순차적인 처리 방식을 버리고 모든 입력을 동시에 처리한다.
그 과정에서 순서 정보가 사라지게 된다.
하지만 텍스트에서 순서는 매우 중요한 정보인데 이를 위치 인코딩이 담당한다.
Attention is All you need 논문에서 sine과 cosine을 이용하여 위치에 대한 정보를 입력했다.
하지만 그 이후에는 위치 인코딩도 위치에 따른 임베딩 층을 추가하여, 학습 데이터를 통해 학습하는 방식을 주로 이용하고 있다.
두 방식 모두 결국 모델로 추론을 수행하는 시점에서는 입력 토큰의 위치에 따라 고정된 임베딩을 더해주므로, 절대적 위치 인코딩(absolute position encoding)이라 한다.
이는 간단하게 구현할 수 있지만, 토큰과 토큰 사이 상대적인 위치 정보를 활용하지 못하고,
학습 데이터에서 보기 어려웠던 긴 텍스트를 추론하는 경우에는 성능이 떨어진다.
최근에는 상대적 위치 인코딩(relative position encoding)도 많이 활용되고 있다.
transformer는 모든 입력 토큰을 동등하게 처리하기 때문에 입력으로 위치 정보를 함께 더해준다는 사실을 기억하자.
embedding_dim = 16
max_position = 12
embed_layer = nn.Embedding(len(str2idx), embedding_dim)
position_embed_layer = nn.Embedding(max_position, embedding_dim)
position_ids = torch.arange(len(input_ids), dtype = torch.long).unsqueeze(0)
position_encodings = position_embed_layer(position_ids)
token_embeddings = embed_layer(torch.tensor(input_ids)) #(5,16)
token_mebeddings = token_embeddings.unsqueeze(0) #(1,5,16)
input_embeddings = token_embeddings + position_encodings
print(input_embeddings.shape)
#출력 결과
#torch.Size([1,5,16])
절대적 위치 인코딩 중 위치 임베딩 층을 학습하는 방식은 위와 같이 새로운 위치 임베딩 층 position_embed_layer를 추가하고,
위치 인덱스 position_ids을 임베딩 층에 넣어 얻은 임베딩을 token_embeddings에 더하는 방식으로 구현할 수 있다.
position_ids에는 0부터 len(input_ids)까지 1씩 증가하도록 만들어진다.
위치 임베딩 층은 최대 토큰 수 max_position = 12로 설정하여 만들어졌다.
token_embeddings와 position_embeddings를 더한 input_embeddings가 모델에 들어갈 입력이 된다.

임베딩은 위와 같이 '나는 최근 파리 여행을 다녀왔다'라는 문장을 단어 단위로 토큰화를 수행한 뒤에
각 토큰에 토큰 아이디와 위치 아이디를 부여한다.
모두 [0,1,2,3,4]로 동일하지만, 토큰 아이디는 사전에 저장된 토큰의 고유 아이디이고, 위치 아이디는 토큰의 위치이다.
예시로 만들어서 같지만, 일반적으로 같지는 않다.
하나의 숫자로된 숫자 아이디는 데이터의 의미를 담을 수 없으므로 의미를 담을 수 있도록
토큰 아이디와 위치 아이디를 토큰 임베딩 층과 위치 임베딩 층을 통해 토큰 임베딩과 위치 임베딩으로 변환한다.
첫번째 토큰 임베딩 [0.2,0.3,...,0.9,1.2]는 각각 개별 숫자 0.2,0.3,...,0.9,1.2는 의미를 알기 어렵지만,
이들이 모여 만든 [0.2,0.3,...,0.9,1.2]는 확실하게 '나는'이라는 의미이다.
마찬가지로 첫번째 위치 임베딩 [0.3,0.1,...0.8,0.2]는 전체가 첫번째 토큰 위치라는 것을 알려준다.
'딥러닝 > LLM' 카테고리의 다른 글
LLM 기본3 - LLM 애플리케이션을 개발하기 위해 필요한 핵심 개념 (0) | 2025.03.31 |
---|---|
LLM 기본2 - ChatGPT가 나오기까지 (0) | 2025.03.27 |
LLM 기본1 - 언어 모델링의 역사 (0) | 2025.03.03 |
2024 LLM 최신 트렌드(카카오) (0) | 2025.01.12 |
RAG(Retrieval-Augmented Generation)의 개념 간단하게 (0) | 2024.08.11 |