안녕하세요, 쿠킴입니다. 트랜스포머를 이해하기 위해 정리한 내용을 공유합니다. (사용된 코드 전체 colab notebook)
목차
1. 들어가며
1.1. 트랜스포머의 전체 아키텍처
1.2. 텍스트를 숫자로 바꾸기
2. Source sequence: 텍스트 정규화와 토큰화
2.1. 텍스트 정규화: 일관성 있는 텍스트 만들기
2.2. 토큰화와 ID 변환
2.3. 특수 토큰과 전처리
2.4. 토큰화 + ID 변환 활용
3. Embeddings Projections: 임베딩
3.0. 기본 개념: 벡터, 임베딩, 유사도
3.1. 전통적 방법 - 희소 벡터(Sparse Vector)
3.2. 딥러닝 기반 임베딩 - 밀집 벡터(Dense Vector)
4. Positional Encoding: 위치 인코딩
4.1. 위치 인코딩의 필요성
4.2. 위치 인코딩의 종류
4.3. 사인-코사인 위치 인코딩 계산 방법 (텍스트 정규화부터 위치인코딩까지)
5. 마치며
1. 들어가며
ChatGPT, Claude와 같은 AI들은 우리의 일상을 크게 바꾸고 있습니다. 그 발전 속도는 두렵기까지 하죠. 이런 AI들은 우리의 질문을 이해하고, 마치 사람처럼 자연스러운 대화를 이어갑니다. 심지어 코드도 작성하고, 시도 쓰고, 논문도 요약하죠.
이런 놀라운 능력의 핵심에는 '트랜스포머(Transformer)'라는 모델이 있습니다. 트랜스포머는 2017년 구글이 발표한 "Attention is All You Need" 논문에서 처음 소개된 모델 구조입니다. 이 구조는 이후 BERT, GPT, T5 등 현대 거의 모든 대규모 언어 모델의 기반이 되었습니다. ChatGPT의 기반이 되는 GPT(Generative Pre-trained Transformer)도 이름에서 볼 수 있듯이 트랜스포머 구조를 사용하고 있죠.
1.1. 트랜스포머의 전체 아키텍처
트랜스포머의 전체 아키텍처는 아래와 같습니다.
상당히 복잡하지만 이 아키텍처의 부분 부분 설명하려고 합니다.
이번 글에서는 빨간색으로 칠한 트랜스포머의 Source Sequence / Embeddings Projections / Positional Encoding , 바로 기본 재료 준비인 '데이터'를 소개합니다.
1.2. 텍스트를 숫자로 바꾸기
컴퓨터는 텍스트를 직접 이해할 수 없습니다. "사과"라는 단어를 보고 빨간 과일을 떠올리는 것은 인간의 능력이죠.
컴퓨터가 텍스트를 처리하기 위해서는 모든 것을 숫자로 변환해야 합니다. 하지만 단순히 각 글자를 ASCII 코드로 바꾸는 것으로는 부족합니다. "사과"와 "apple"이 같은 의미라는 것을 컴퓨터가 알 수 있어야 하고, "먹다"와 "먹었다"가 비슷한 의미라는 것도 알아야 합니다. 이를 위해 다음과 같은 과정이 필요합니다.
트랜스포머 모델은 어떻게 이해하고 있을까요?
1. Source Sequence
- 텍스트 정규화
- 토큰화
2. Embeddings Projections
- 임베딩
3. Positional Encoding
- 위치 인코딩
세 가지 과정을 하나씩 살펴보면서, 텍스트를 숫자로 바꾸어 어떻게 컴퓨터가 우리의 언어를 이해할 수 있는 형태로 변환하는지 알아보도록 하겠습니다.
2. Source sequence: 텍스트 정규화와 토큰화
2.1. 텍스트 정규화: 일관성 있는 텍스트 만들기
먼저 텍스트 정규화는 단어와 문장의 일관성을 유지하기 위한 과정입니다.
예를 들어 "kukim", "KuKim", "KUKIM!"처럼 같은 의미지만 다르게 표현된 텍스트를 통일하는 작업입니다.
2.1.1. 기본 정규화
기본적인 텍스트 정규화 과정에는 다음과 같은 작업들이 포함됩니다.
- 소문자 변환: KUKIM → kukim
- 문장 부호 제거: kukim! → kukim
- 공백 정규화: kukim 입니다 → kukim 입니다
- 반복 문자/단어 정규화: 너무너무 좋아요 → 너무 좋아요
def basic_normalize(text):
# 소문자 변환
text = text.lower()
# 특수문자 제거
text = re.sub(r'[^\\w\\s]', '', text)
# 공백 정규화
text = re.sub(r'\\s+', ' ', text)
# 반복 단어 정규화
text = re.sub(r'(\\w+)(\\s\\1)+', r'\\1', text)
return text.strip()
# 테스트
texts = [
"안녕하세요~! KuKIM입니다!!!",
"너무너무 좋아요 정말정말 좋아요!!!"
]
for text in texts:
print(f"원본: {text}")
print(f"정규화: {basic_normalize(text)}")
print()
# 결과
# 원본: 안녕하세요~! KuKIM입니다!!!
# 정규화: 안녕하세요 kukim입니다
#
# 원본: 너무너무 좋아요 정말정말 좋아요!!!
# 정규화: 너무 좋아요 정말 좋아요
2.1.2. 특수패턴 / 언어 별 처리
텍스트에는 URL, 이메일, 날짜와 같은 특수한 패턴이 포함될 수 있으며, 언어별로 특별한 처리가 필요할 수 있습니다.
- URL 정규화: "https://kukim.tistory.com/" → <URL>
- 이메일: "kukim@email.com" → <EMAIL>
- 날짜: "2024.01.25" → <DATE>
- 숫자: "100개" → <NUM>개
- 어간 추출: walking, walks, walked → walk
- 한글 형태소: 먹었습니다, 먹어요 → 먹다
- 악센트 제거: résumé café naïve → resume cafe naive
- 유니코드 정규화 (NFKD)
def handle_special_patterns(text):
# URL 처리
text = re.sub(r'https?://\\S+', '', text)
# 이메일 처리
text = re.sub(r'\\S+@\\S+\\.\\S+', '', text)
# 날짜 처리
text = re.sub(r'\\d{4}[-/\\.]\\d{1,2}[-/\\.]\\d{1,2}', '', text)
# 숫자 처리
text = re.sub(r'\\d+', '', text)
return text
# 테스트
texts = [
"자세한 내용은 <https://kukim.dev> 참고",
"kukim@email.com으로 연락주세요",
"2024.01.25에 작성된 글입니다",
"이 책은 100쪽입니다"
]
for text in texts:
print(f"원본: {text}")
print(f"정규화: {handle_special_patterns(text)}")
print()
# 결과
# 원본: 자세한 내용은 <https://kukim.dev> 참고
# 정규화: 자세한 내용은 참고
#
# 원본: kukim@email.com으로 연락주세요
# 정규화: 으로 연락주세요
# 한글 형태소 분석
from konlpy.tag import Okt
okt = Okt()
text = "자연어처리를 공부했습니다"
print(okt.morphs(text)) # ['자연어', '처리', '를', '공부', '했습니다']
# 영어 - 어간 추출
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
print(stemmer.stem("running")) # "run"
# 프랑스어 - 악센트 제거
text = "résumé café naïve"
normalized = unicodedata.normalize('NFKD', text).encode('ASCII', 'ignore').decode()
print(normalized) # "resume cafe naive"
# 유니코드 정규화 (NFKD
text = "𝕋𝕙𝕖 ℚ𝕦𝕚𝕔𝕜 𝔹𝕣𝕠𝕨𝕟 🦊"
normalized = unicodedata.normalize('NFKC', text)
print(normalized) # "The Quick Brown 🦊"
2.1.3. 불용어(Stop words) 제거
텍스트에서 실제 의미 분석에 크게 도움이 되지 않는 단어들을 제거합니다.
- 영어: a, an, the, is, at, which, on...
- 한국어: 저, 나, 을, 를, 이, 가, 그, 저, 것…
def remove_stopwords(text, stop_words):
words = text.split()
return ' '.join([word for word in words if word not in stop_words])
# 테스트
ko_stop_words = {'저는', '입니다', '그리고'}
en_stop_words = {'is', 'a', 'in'}
# 예시
text1 = "저는 kukim 입니다"
text2 = "This is a book"
print(f"한국어 원본: {text1}")
print(f"한국어 결과: {remove_stopwords(text1, ko_stop_words)}")
print(f"\\n영어 원본: {text2}")
print(f"영어 결과: {remove_stopwords(text2, en_stop_words)}")
#한국어 원본: 저는 kukim 입니다
#한국어 결과: kukim
#영어 원본: This is a book
#영어 결과: This book
2.1.4. 정규화 수준 결정
정규화의 모든 과정을 해야 하는 것은 아닙니다. “목적에 따라 다르게” 적용해야 합니다.
- 검색 엔진: 핵심 키워드를 정확히 찾는 것이 중요하므로, 형태소 분석이 필수적입니다.
- 감성 분석: 이모티콘이나 느낌표 같은 감정 표현이 중요한 정보가 될 수 있습니다.
- 챗봇: 자연스러운 대화를 위해 말투나 높임말을 보존해야 할 수 있습니다.
잘못된 처리 예시
- 과도한 정규화: "최고!!!" → "최고" (감정 강도 소실)
- 정규화 부족: "kukim"과 "KuKim"을 다른 사용자로 인식
결국 텍스트 정규화는 '텍스트를 깔끔하게 만드는 것'이 아니라, '다음 단계 처리에 적합한 형태로 만드는 것'입니다.
이제 정규화된 텍스트를 토큰화를 하고 ID로 변환해 보겠습니다.
2.2. 토큰화와 ID 변환
2.2.1. 토큰화: 텍스트를 작은 단위로 나누기
토큰화란 텍스트를 의미 있는 최소 단위인 토큰(token)으로 분리하는 과정
1. 문장 토큰화: 텍스트를 문장 단위로 분리
# 영어: 주로 ., ?, ! 기준
from nltk.tokenize import sent_tokenize
text = "Dr. Smith works at Google. He's an AI researcher!"
sentences = sent_tokenize(text)
print(sentences)
# ['Dr. Smith works at Google.', "He's an AI researcher!"]
# 'Dr.'를 문장 끝으로 인식하지 않음
# 한국어: 의미 단위 분리 필요
import kss
text = "밥을 먹었다. 너무 맛있었다! 또 먹고 싶다..."
sentences = kss.split_sentences(text)
print(sentences)
# ['밥을 먹었다.', '너무 맛있었다!', '또 먹고 싶다...']
# 단순 마침표 분리의 문제점
print("단순 분리:", text.split('.'))
# ['밥을 먹었다', ' 너무 맛있었다!', ' 또 먹고 싶다', '', '']
# 불필요한 공백과 빈 문자열 발생
2. 단어 토큰화: 공백이나 구두점을 기준으로 단어 단위로 분리
# 규칙 기반: 공백/구두점 기준
text = "I love NLP!"
tokens = ["I", "love", "NLP", "!"]
# N-gram: 연속된 N개 단어를 하나의 단위로
text = "I love natural language processing"
bigrams = ["I love", "love natural", "natural language", "language processing"]
# NLTK: 언어학적 규칙 기반
text = "I'm going to New York"
tokens = ["I", "'m", "going", "to", "New York"] # 축약형 처리, 복합명사 인식
3. 문자 토큰화: 텍스트를 개별 문자 단위로 분리
text = "Hello"
tokens = ["H", "e", "l", "l", "o"]
2.2.2. 토큰을 ID로 변환하기
토큰화된 텍스트는 보통 두 가지 방식으로 숫자로 변환될 수 있습니다.
1. 룩업 테이블 방식
각 고유 토큰을 ID에 매핑하여 1:1 룩업 테이블을 생성
단어 | ID |
동물 | 18 |
… | … |
예술 | 35 |
… | … |
자동차 | 128 |
# 룩업 테이블 생성
token_to_id = {
"I": 0,
"love": 1,
"NLP": 2,
"!": 3
}
# 토큰을 ID로 변환
tokens = ["I", "love", "NLP", "!"]
ids = [token_to_id[token] for token in tokens] # [0, 1, 2, 3]
# ID에서 토큰으로 역변환 가능
id_to_token = {id: token for token, id in token_to_id.items()}
# 결과: {0: 'I', 1: 'love', 2: 'NLP', 3: '!'}
2. 해싱 방식
피처 해싱(feature hashing) / 해싱 트릭(hashing trick)라고도 하며 룩업 테이블을 유지하지 않고 해시 함수를 사용하여 ID를 가져오는 메모리 효율성이 좋은 방법
from hashlib import md5
def hash_token(token, vocab_size=1000):
return int(md5(token.encode()).hexdigest(), 16) % vocab_size
# 토큰을 ID로 변환
tokens = ["I", "love", "NLP", "!"]
ids = [hash_token(token) for token in tokens]
print(ids)
# 결과: [655, 518, 276, 507]
룩업 테이블 | 해싱 | |
속도 | 빠른 (O(1) 검색) | 느림 토큰을 ID로 변환하기 위해 해시 함수를 계산해야 함 |
ID에서 토큰으로 변환 | 변환 가능 역색인 테이블 사용하여 ID를 토큰으로 쉽게 변환 | 변환 불가 ID를 토큰으로 변환할 수 없음 |
메모리 | 많이 필요(전체 테이블 저장) 테이블은 메모리에 저장, 토큰 수가 많으면 필요한 메모리 증가 | 적게 필요 해시 함수는 모든 토큰을 해당 토큰의 ID로 변환하는 데 충분 |
신규 토큰 | 처리 불가 새 단어 or 미등록 단어 처리 불가 |
처리 가능 모든 단어에 해시 함수 적용 가능 |
충돌 | 충돌 이슈 없음 | 잠재적 해시 충돌 문제 존재 |
2.2.3. ID 변환의 한계
단어 단위 토큰화와 ID 변환 방식에는 몇 가지 중요한 한계가 있습니다
1. OOV(Out-of-Vocabulary) 문제
사전에 없는 단어를 만났을 때 처리할 수 없는 문제입니다.
# 문제 상황
vocab = {
"자연어": 0,
"처리": 1,
"공부": 2,
"하다": 3
}
# 사례 1: 새로운 단어
text = "트랜스포머를 공부했습니다"
# "트랜스포머" → [UNK] (정보 손실)
# 사례 2: 오타
text = "자연어 치리를 공부중" # "처리"의 오타
# "치리" → [UNK] (비슷한 단어인데도 처리 불가)
2. 너무 큰 어휘 사전 문제
모든 단어의 변형을 저장해야 하는 문제가 있습니다.
# 하나의 동사에서 파생되는 여러 형태
word_to_id = {
"먹다": 0,
"먹었다": 1,
"먹습니다": 2,
"먹어요": 3,
"먹으세요": 4,
"먹었습니다": 5,
# ... 수많은 활용형
}
# 합성어도 별도 저장 필요
compound_words = {
"자연어": 100,
"자연어처리": 101,
"자연어번역": 102,
# ... 무수히 많은 합성어
}
3. 의미적 유사성 무시
비슷한 단어들이 완전히 다른 ID를 가지게 되는 문제가 있습니다.
# 유사한 단어들이 전혀 다른 ID를 가짐
token_to_id = {
"자연어": 0,
"자연언어": 1, # 비슷한 의미인데 다른 ID
"먹다": 100,
"먹었다": 853, # 같은 단어의 활용형인데 다른 ID
"먹습니다": 1274
}
이러한 문제들을 해결하기 위해 단어를 더 작은 의미 단위로 나누는 서브워드 토큰화(Subword Tokenization)가 등장했습니다.
# 기존 방식
"자연어처리" → [UNK] (사전에 없는 경우)
# 서브워드 토큰화
"자연어처리" → ["자연", "##어", "##처리"]
"트랜스포머" → ["트랜", "##스", "##포", "##머"]
서브워드 토큰화로 문제점 해결
#### 1. OOV 문제 해결
# 새로운 단어도 부분으로 분해
"인공지능처리" → ["인공", "##지능", "##처리"]
"자연어번역" → ["자연", "##어", "##번역"]
#### 2. 작은 어휘사전
# 기존: 모든 단어를 저장
{"자연어": 0, "처리": 1, "자연어처리": 2, ...}
# 서브워드: 부분 단위 저장
{"자연": 0, "##어": 1, "##처리": 2}
# 조합으로 새로운 단어 표현 가능
#### 3. 의미 유사성 포착
# 공통 부분을 통해 유사성 인식
"자연어" → ["자연", "##어"]
"자연어처리" → ["자연", "##어", "##처리"]
# "자연", "##어" 부분이 공통됨
2.2.4. 서브워드 토큰화 방식(Subword Tokenization)
1. BPE (Byte Pair Encoding)
GPT 사용되는 토큰화 반식으로 가장 빈번하게 등장하는 문자 쌍을 병합해 가는 방식입니다.
# BPE 학습 과정 예시
text = "low lower lowest"
# 1. 초기 분할
# l o w _ l o w e r _ l o w e s t
# 2. 빈도수 카운트
# (l, o): 3
# (o, w): 3
# (e, r): 1
# ...
# 3. 가장 빈도가 높은 쌍을 병합
# 'l o' → 'lo'
# lo w _ lo w e r _ lo w e s t
# 4. 다시 빈도수 카운트 및 병합
# (lo, w): 3
# lo w _ lo w e r _ lo w e s t
# → low _ low e r _ low e s t
# 이런 식으로 원하는 어휘 크기가 될 때까지 반복
from transformers import GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
text = "transformer"
tokens = tokenizer.tokenize(text)
print(tokens) # ['trans', 'former']
2. WordPiece
BERT에서 사용되는 토큰화 방식으로 BPE와 비슷하지만, 병합 기준으로 빈도 대신 **우도(likelihood)**를 사용합니다.
# 1. 말뭉치 예시
corpus = [
"transform",
"transformer",
"transformers",
"transforming"
]
# 2. 초기 문자 분할
# "transform" → ["t", "r", "a", "n", "s", "f", "o", "r", "m"]
# "transformer" → ["t", "r", "a", "n", "s", "f", "o", "r", "m", "e", "r"]
# "transformers" → ["t", "r", "a", "n", "s", "f", "o", "r", "m", "e", "r", "s"]
# "transforming" → ["t", "r", "a", "n", "s", "f", "o", "r", "m", "i", "n", "g"]
# 3. 문자 빈도 계산
char_freq = {
"t": 4, # 모든 단어에 등장
"r": 8, # 모든 단어에 2번씩
"a": 4,
"n": 4,
"s": 4,
...
}
# 4. 인접 쌍 빈도 계산
pair_freq = {
"tr": 4, # 모든 단어에 등장
"ra": 4,
"an": 4,
...
}
# 5. likelihood 계산
# score = pair의 빈도 / (첫 문자 빈도 × 둘째 문자 빈도)
tr_score = 4 / (4 × 8) = 0.125
ra_score = 4 / (4 × 4) = 0.25 # 가장 높은 점수
# 6. 병합
# "ra" 쌍을 병합한 결과
# "transform" → ["t", "ra", "n", "s", "f", "o", "r", "m"]
# "transformer" → ["t", "ra", "n", "s", "f", "o", "r", "m", "e", "r"]
...
## 이런 과정을 반복함으로 transformer → ['transform', '##er'] 와 같은 형태로 토큰화 됩니다.
from transformers import BertTokenizer
# BERT 토크나이저 로드
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
word = "transformer"
tokens = tokenizer.tokenize(word)
print(f"{word} → {tokens}")
# transformer → ['transform', '##er']
3. SentencePiece
구글의 T5 모델에서 사용하는 방식으로, 언어에 구애받지 않는 특징이 있습니다.
- 문자 단위 아닌 바이트 처리하여 유니코드/ASCII 등 인코딩 방식 구애받지 않음
- 공백도 문자로 처리하기 때문에 영어처럼 공백 단어 구분, 한중일처럼 공백 없이 붙여 쓰는 언어 모두 동일 처리 가능
- 사전 토큰화 불필요(형태소 분석, 단어 분리 x)
# 1. 말뭉치 예시
corpus = [
"자연어처리",
"자연어분석",
"자연어번역"
]
# 2. 특징: 공백을 포함한 모든 문자를 하나의 시퀀스로 처리
# '_'는 공백을 나타냄 (실제로는 특수문자 '▁' 사용)
# "자연어처리" → ["_", "자", "연", "어", "처", "리"]
# 3. 빈도 계산
char_seq = {
"_자": 3, # 모든 단어 시작에 등장
"자연": 3, # 모든 단어에 등장
"연어": 3, # 모든 단어에 등장
"어처": 1, # "자연어처리"에만
"처리": 1, # "자연어처리"에만
...
}
# 4. 가장 빈번한 패턴을 하나의 토큰으로 병합
# "자연어처리" → ["_자연", "어처", "리"]
# 5. 최종 토큰화 결과
text = "자연어처리"
tokens = ["▁자연", "어처", "리"]
import sentencepiece as spm
# 1. 학습용 텍스트
corpus = [
"자연어처리",
"자연어분석",
"자연어번역",
"자연어모델"
]
# 2. 모델 학습
spm.SentencePieceTrainer.train(
sentence_iterator=iter(corpus), # iterator로 변환
model_prefix='nmt', # 모델 이름
vocab_size=16, # 어휘 집합 크기
model_type='unigram', # 모델 타입
character_coverage=0.995 # 문자 커버리지
)
# 3. 학습된 모델 로드
sp = spm.SentencePieceProcessor()
sp.load('nmt.model')
# 4. 토큰화 테스트
text = "자연어처리"
# 토큰화
tokens = sp.encode_as_pieces(text)
print(f"토큰화 결과: {tokens}")
# 토큰화 결과: ['▁자연어', '처', '리']
2.3. 특수 토큰과 전처리
특수 토큰의 이해
트랜스포머 모델에서는 다음과 같은 특수 목적의 토큰들을 사용합니다
# BERT의 특수 토큰들
special_tokens = {
'[CLS]': 101, # Classification: 문장 시작, 문장 분류에 사용
'[SEP]': 102, # Separator: 문장 구분/끝 표시
'[MASK]': 103, # Masking: 마스크 언어 모델링에 사용
'[PAD]': 0, # Padding: 시퀀스 길이 맞추기
'[UNK]': 100 # Unknown: 미등록 단어 처리
}
# 실제 사용 예시
text = "I love transformers"
tokens = ['[CLS]', 'i', 'love', 'transform', '##ers', '[SEP]']
패딩과 트런케이션
모델은 고정된 길이의 입력을 필요로 하지만, 실제 문장들의 길이는 다양하기 때문에 패딩과 트런케이션 처리를 합니다.
- 패딩 (Padding)
- 짧은 시퀀스를 특정 길이까지 채우는 작업
- [PAD] 토큰으로 채움
# 패딩 예시 (max_length=5) sequences = [ [1, 2], # 짧은 시퀀스 [1, 2, 3, 4], # 중간 길이 [1, 2, 3, 4, 5] # 긴 시퀀스 ] padded_sequences = [ [1, 2, 0, 0, 0], # 뒤를 0으로 채움 [1, 2, 3, 4, 0], # 한 자리 채움 [1, 2, 3, 4, 5] # 이미 충분히 김 ]
- 트런케이션 (Truncation)
- 긴 시퀀스를 특정 길이로 자르는 작업
- 보통 앞이나 뒤에서부터 자름
# 트런케이션 예시 (max_length=4)
sequences = [
[1, 2, 3, 4, 5, 6], # 너무 긴 시퀀스
[1, 2, 3, 4, 5], # 긴 시퀀스
[1, 2, 3] # 충분히 짧은 시퀀스
]
truncated_sequences = [
[1, 2, 3, 4], # 뒤의 2개 토큰 제거
[1, 2, 3, 4], # 뒤의 1개 토큰 제거
[1, 2, 3, 0] # 패딩 추가
]
패딩 vs 트런케이션
- 패딩: 짧은 시퀀스를 길게 만듦
- 장점: 정보 손실 없음
- 단점: 메모리 낭비, 불필요한 계산
- 트런케이션: 긴 시퀀스를 짧게 만듦
- 장점: 메모리 효율적
- 단점: 정보 손실 가능성
2.4. 토큰화 +ID 변환 활용
지금까지 텍스트를 정규화하고, 토큰화하고, ID로 변환하는 과정을 살펴보았습니다.
특수 토큰 추가하고 패딩/트런케이션 적용하면 아래와 같이 텍스트가 숫자로 변하게 됩니다.
# 예시: 텍스트가 숫자로 변환되는 전체 과정
text = "transformer is awesome"
# 1. 토큰화 (WordPiece) + 특수 토큰 추가
tokens = ['[CLS]', 'transform', '##er', 'is', 'awesome', '[SEP]']
# 2. ID 변환 (룩업 테이블 사용)
token_to_id = {
'[PAD]': 0, # 패딩용 토큰
'[UNK]': 1, # 미등록 단어용 토큰
'[CLS]': 2, # 문장 시작 토큰
'[SEP]': 3, # 문장 구분 토큰
'transform': 7993,
'##er': 2010,
'is': 2003,
'awesome': 4385
}
# 3. ID 시퀀스 생성
ids = [2, 7993, 2010, 2003, 4385, 3]
# 4. 패딩 적용 (max_length=10)
padded_ids = [2, 7993, 2010, 2003, 4385, 3, 0, 0, 0, 0]
하지만 이 ID들은 단순한 식별자일 뿐, 단어의 '의미'를 담고 있지는 않습니다.
예를 들어
- "cat"과 "kitten"은 의미가 비슷하지만 완전히 다른 ID를 가집니다
- "good"과 "bad"는 반대 의미지만 ID만으로는 이를 알 수 없습니다
- "자연어"와 "natural language"는 같은 의미지만 전혀 다른 ID 시퀀스가 됩니다
이러한 한계를 극복하기 위해, ID를 의미 있는 벡터로 변환하는 '임베딩(Embedding)' 과정이 필요합니다.
3. Embeddings Projections: 임베딩
지금까지 텍스트를 정규화, 토큰화하고 ID로 변환했습니다.
하지만 이 ID들은 단순한 식별자일 뿐, 단어의 의미나 관계를 표현하지 못합니다.
3.0. 기본 개념: 벡터, 임베딩, 유사도
3.0.1. 벡터란?
방향과 크기를 가진 숫자 배열
- 예: [0.2, -1.5] → 2차원 벡터, [1, 3, 5] → 3차원 벡터
- 방향: [1,0] → 오른쪽 방향, [0,−1] → 아래 방향
- 크기(Magnitude): 벡터의 길이. ∥v ∥
- 차원(dimension)이 높을수록 복잡한 정보 표현 가능
- 단어를 벡터로 표현하면 수학적 연산 가능
3.0.2. 임베딩(Embedding)이란?
단어를 의미를 담은 벡터로 변환하는 기술
- 예: "강아지" → [0.7, -0.2, 1.3], "고양이" → [0.6, -0.3, 1.2]
- 비슷한 단어 → 벡터 공간에서 가까이 위치
- 반대 의미 단어 → 벡터 반대 방향
# 임베딩 예시
강아지 = [0.7, -0.2]
고양이 = [0.6, -0.3]
악마 = [-0.8, 0.5]
천사 = [-0.7, 0.6]
희소 벡터(Sparse Vector) vs 밀집 벡터(Dense Vector)
희소 벡터 | 밀집 벡터 | |
0의 비율 | 99% 이상 | 거의 없음 |
차원 | 수만 차원 | 수백 차원 |
의미 반영 | 불가 | 가능 |
e.g. | [1,0,0,0,0,0,0,…] | [0.31, 0.21, 0.5,..] |
3.0.3. 유사도(Similarity)란?
벡터 간 거리로 의미적 친밀도 측정
구분 코사인 유사도 유클리드 거리 맨해튼 거리
코사인 유사도 | 유클리드 거리 | 맨해튼 거리 | |
계산 복잡도 | O(n) | O(n) | O(n) |
값 범위 | -1 ~ 1 | 0 ~ 무한대 | 0 ~ 무한대 |
벡터 크기 영향 | 없음 | 있음 | 있음 |
사용처 | 문서/텍스트 유사도 | 이미지/공간 분석 | 그리드 경로 계산 |
3.1. 전통적 방법 - 희소 벡터(Sparse Vector)
3.1.1. One-Hot Encoding (원-핫 인코딩)
개념
- 단어를 사전의 인덱스 위치만 1, 나머지는 0인 벡터로 표현
- 핵심: 단어 간 의미적 관계를 반영하지 못함
# 어휘 사전
vocab = ['자연어', '처리', '인공지능', '모델']
# One-hot 인코딩
one_hot = {
'자연어': [1, 0, 0, 0],
'처리': [0, 1, 0, 0],
'인공지능': [0, 0, 1, 0],
'모델': [0, 0, 0, 1]
}
print(one_hot['자연어']) # [1,0,0,0]
장점 | 단점 |
구현이 간단 | 차원의 저주 (단어 수=차원) |
직관적 이해 용이 | 모든 단어가 독립적 → "자연어≈언어" 관계 표현 불가 |
99.9%가 0 → 메모리/계산 낭비 |
3.1.2. TF-IDF (단어 빈도-역문서 빈도)
개념
- 단어의 빈도(TF)와 문서에서의 희귀성(IDF)을 결합해 중요도를 측정하는 방식.
- 문서 간의 단어 중요도를 상대적으로 계산 가능.
주요 계산 법
- TF (Term Frequency): 특정 단어가 문서에 나타난 횟수 / 문서 내 총 단어 수
- IDF (Inverse Document Frequency): log(전체 문서 수 / 해당 단어가 등장한 문서 수).
- TF-IDF = TF × IDF: 특정 문서에서 단어의 중요도
doc1 = "자연어 처리 모델 학습"
doc2 = "인공지능 모델 개발"
doc3 = "딥러닝 알고리즘 연구"
doc4 = "머신러닝 모델 최적화 연구"
corpus = [doc1, doc2, doc3, doc4]
TF (Term Frequency): 각 문서 내 단어 빈도
문서 | 총 단어 수 | 단어 별 TF |
doc1 | 4 | 자연어:1/4=0.25, 처리:0.25, 모델:0.25, 학습:0.25 |
doc2 | 3 | 인공지능:0.333, 모델:0.333, 개발:0.333 |
doc3 | 3 | 딥러닝:0.333, 알고리즘:0.333, 연구:0.333 |
doc4 | 4 | 머신러닝:0.25, 모델:0.25, 최적화:0.25, 연구:0.25 |
IDF (Inverse Document Frequency): 전체 문서에서의 희귀성
단어 | 등장 문서 수 (df) | IDF 계산 | IDF 값 (소수점 4자리) |
자연어 | 1 | log(4/1) | 1.3863 |
처리 | 1 | log(4/1) | 1.3863 |
모델 | 3 | log(4/3) | 0.2877 |
학습 | 1 | log(4/1) | 1.3863 |
인공지능 | 1 | log(4/1) | 1.3863 |
개발 | 1 | log(4/1) | 1.3863 |
딥러닝 | 1 | log(4/1) | 1.3863 |
알고리즘 | 1 | log(4/1) | 1.3863 |
연구 | 2 | log(4/2) | 0.6931 |
머신러닝 | 1 | log(4/1) | 1.3863 |
최적화 | 1 | log(4/1) | 1.3863 |
TF-IDF 계산: TF-IDF(t,d)=TF(t,d)×IDF(t)
문서별 TF-IDF 행렬
문서 | 자연어 | 처리 | 모델 | 학습 | 인공지능 | 개발 | 딥러닝 | 알고리즘 | 연구 | 머신러닝 | 최적화 |
doc1 | 0.3466 | 0.3466 | 0.0719 | 0.3466 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
doc2 | 0.0 | 0.0 | 0.0959 | 0.0 | 0.4621 | 0.4621 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
doc3 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.4621 | 0.4621 | 0.2310 | 0.0 | 0.0 |
doc4 | 0.0 | 0.0 | 0.0719 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.1733 | 0.3466 | 0.3466 |
- 높은 TF-IDF 단어
- 인공지능(doc2): 0.4621 → doc2에서만 등장 + 높은 IDF
- 알고리즘(doc3): 0.4621 → doc3 고유 단어
- 최적화(doc4): 0.3466 → doc4 고유 단어
- 낮은 TF-IDF 단어
- 모델(doc1): 0.0719 → 3개 문서에 등장 (IDF 낮음)
- 연구(doc3): 0.2310 → 2개 문서에 등장
장단점
장점 | 단점 |
단어 중요도 반영 | 여전히 희소 행렬 문제 |
문서 간 비교 가능 | 단어 의미 관계 무시 |
간단하고 널리 사용됨 | 언어별 전처리 필요 (예: 한글 형태소 분석) |
3.1.3. + BM25
TF-IDF를 확장하여 검색 시스템(ElasticSearch(Lucene))에서 문서와 쿼리의 관련성을 계산하는 순위 알고리즘.
- 단어 빈도와 문서 길이를 고려해 더 정교한 점수를 산정.
- 벡터 표현 자체보다는 점수 산정 도구로 사용됨.
3.1.4. 전통적 방법의 공통점
- 단어/문서를 고차원 희소 벡터로 변환.
- 통계적 패턴은 반영하나, 의미적 맥락은 무시됨.
3.2. 딥러닝 기반 임베딩 - 밀집 벡터(Dense Vector)
단어의 의미와 문맥을 반영하는 임베딩을 위해 딥러닝 기반 방법들이 등장했습니다.
단순한 통계가 아닌 신경망을 통해 단어 간 복잡한 관계를 학습하는 방식입니다.
3.2.1. Word2Vec: 문맥 기반 임베딩
구글이 2013년 발표한 방법으로, 단어의 주변 문맥을 예측하며 벡터를 학습합니다.
→ 단어를 저차원 밀집 벡터 (예: 300차원)로 표현.
단어가 유사한 문맥에서 등장할수록 벡터 공간에서 가깝게 위치시킵니다. (튜토리얼)
# 벡터 연산 가능
king - man + woman ≈ queen
# 유사도 계산 가능
similarity('자연어', '언어') > similarity('자연어', '자동차')
# Word2Vec 학습 원리
sentence = "I study natural language processing"
# 중심 단어: 'natural'
context_window = 2 # 앞뒤 2단어
# 주변 단어 예측
input_word = "natural"
output_words = ["study", "language", "processing"]
# 신경망 구조
input_layer → hidden_layer(임베딩) → output_layer(주변 단어 예측)
Word2Vec는 2가지 방법이 있습니다.
- CBOW (Continuous Bag of Words): 주변 단어들로 중심 단어 예측
# 예시: "자연어 [MASK] 모델을 공부한다"
context = ['자연어', '모델을', '공부한다']
target = '처리'
# 주변 단어들의 벡터를 통해 '처리'를 예측
- Skip-gram: 중심 단어로 주변 단어들 예측
# 예시: "자연어 처리 모델을 공부한다"
center = '처리'
targets = ['자연어', '모델을']
# '처리'를 통해 주변 단어들을 예측
CBOW | Skip-Gram | |
입력 | 주변 단어들의 벡터 평균 | 중심 단어 벡터 |
출력 | 중심 단어 예측 | 주변 단어들 예측 |
학습 속도 | 빠름 | 느림 |
성능 | 빈도 높은 단어 잘 학습 | 희귀 단어 잘 학습 |
적합한 데이터 | 소량 데이터 | 대량 데이터 |
3.2.2. FastText
Word2Vec에서 서브워드(subword)를 추가하여 임베딩을 학습하는 방식입니다. OOV 문제를 해결할 수 있습니다.
# 단어를 n-gram으로 분해
word = "처리"
ngrams = ["<처", "처리", "리>"] # 3-gram 예시
# 각 n-gram의 벡터 합으로 단어 벡터 생성
word_vector = sum(ngram_vectors)
# OOV 단어도 처리 가능
"처리하기" → ["<처", "처리", "리하", "하기", "기>"]
3.2.3. GloVe (Global Vectors)
Word2Vec이 지역적 문맥만 보는 것과 달리, GloVe는 전체 말뭉치의 통계 정보를 활용합니다.
# 단어 동시 등장 행렬
cooccurrence_matrix = {
('자연어', '처리'): 50, # 자주 함께 등장
('자연어', '자동차'): 2, # 거의 함께 등장하지 않음
}
# 이런 전역 통계를 바탕으로 임베딩 학습
3.2.4. 문장/문서 임베딩
- Doc2Vec: 문서 전체를 벡터화.
- BERT, GPT: 문맥을 반영한 동적 임베딩 (단어 의미가 문장에 따라 변화).
(해당 임베딩 기법은 너무 복잡하여 추후 다른 글로 작성하겠습니다.)
3.2.5. 딥러닝 기반 임베딩의 특징
의미 보존: 유사한 단어는 벡터 공간에서 가까이 위치
차원 효율성: 300~500차원으로 고차원 문제 해결
맥락 인식: 최신 모델(BERT 등)은 문맥에 따른 동적 임베딩 생성
3.3. 전통적 vs 딥러닝 기반 임베딩 차이
전통적 방법(원핫, TF-IDF) | 딥러닝 임베딩 (Word2Vec, BERT, ...) | |
차원 | 고차원 (수천~수만 차원) | 저차원 (수백 차원) |
벡터 타입 | 희소(Sparse) | 밀집(Dense) |
의미 반영 | 불가 | 가능 (단어/문장 유사도 계산) |
맥락 이해 | 불가 | 가능 (BERT 등 트랜스포머 기반) |
계산 비용 | 낮음 | 높음 (GPU 가속 필요) |
지금까지 텍스트를 정규화, 토큰화하고 ID로 변환하는 방법을 배웠습니다. 또한 단어를 의미를 담은 벡터로 변환하는 임베딩 기법(원핫, TF-IDF, Word2Vec, BERT)을 살펴보았습니다. 이제 이 임베딩 벡터들은 단어의 의미적 관계를 표현할 수 있게 되었지만, 한 가지 중요한 정보가 누락되어 있습니다. 바로 단어의 순서입니다.
4. Positional Encoding: 위치 인코딩
트랜스포머는 단어의 순서 정보를 명시적으로 주입하기 위해 위치 인코딩을 사용합니다. 이는 임베딩 벡터에 위치 정보를 더하는 방식으로 구현됩니다.
4.1. 위치 인코딩의 필요성
언어는 단어 순서에 따라 의미가 완전히 달라집니다.
sentence1 = ["고양이", "가", "강아지", "를", "쫓는다"]
sentence2 = ["강아지", "가", "고양이", "를", "쫓는다"]
# 두 문장은 단어 순서만 다르지만 의미가 정반대
위 ‘순서’의 정보를 활용해야 더 좋은 모델이 될 수 있습니다.
4.1.1. RNN과 LSTM
트랜스포머 이전에 RNN/LSTM이 있었습니다.
- RNN(순환 신경망)
- 단어를 순차적으로 처리하며, 이전 단계의 hidden state를 다음 단계로 전달
- 문제점
- 장기 의존성 문제 (길이가 긴 시퀀스에서 정보 손실).
- 병렬화 불가 → GPU 활용 어려움.
- LSTM
- 3개의 게이트(입력/망각/출력)와 셀 상태로 장기 메모리를 관리합니다.
- 장점: RNN보다 긴 시퀀스 학습 가능.
- 한계: 여전히 순차적 처리로 인해 학습 속도 느림.
# RNN/LSTM의 의사 코드
hidden_state = initial_state
for word in ["고양이", "가", "강아지", "를", "쫓는다"]:
hidden_state = update(hidden_state, word) # 이전 상태에 의존적
RNN/LSTM은 순서 정보를 활용합니다.
하지만 학습 속도가 드리고 병렬 처리가 어려운 문제가 있었습니다.
4.1.2. 트랜스포머
트랜스포머는 모든 단어를 동시에 입력받아 병렬 처리할 수 있기 때문에 RNN/LSTM의 문제점을 극복할 수 있습니다.
# 트랜스포머의 의사 코드
words = ["고양이", "가", "강아지", "를", "쫓는다"]
embeddings = [단어_임베딩(word) for word in words]
outputs = 한번에_처리(embeddings) # GPU 병렬처리 최적화
4.1.3. 위치 인코딩이 필요한 이유
문제: 병렬 처리로 인해 단어 순서 정보가 사라짐
- 예: 고양이 → 강아지와 강아지 → 고양이를 구분하지 못함.
해결책: 단어 임베딩에 위치 정보를 명시적으로 추가
4.2. 위치 인코딩의 종류
트랜스포머에서 위치 정보 주입하는 방법은 크게 2가지가 있습니다.
- 절대 위치 인코딩 (Absolute Positional Encoding)
- 정의: 단어의 절대적 위치(예: 0번째, 1번째)를 고정된 값 또는 학습 가능한 벡터로 표현합니다.
- 종류
- 사인-코사인 위치 인코딩: 고정된 삼각함수 기반 (트랜스포머 논문 제안).
- 학습 가능한 위치 임베딩: 임베딩 레이어로 위치 벡터를 학습.
- 상대 위치 인코딩 (Relative Positional Encoding)
- 정의: 단어 간 상대적 거리를 인코딩합니다.
- 예: "고양이"와 "쫓는다"가 4칸 떨어졌을 때, 이 거리를 반영.
- 정의: 단어 간 상대적 거리를 인코딩합니다.
방법 | 장점 | 단점 |
사인-코사인 | - 임의 길이의 시퀀스 처리 가능- 일반화 성능 높음 | - 학습되지 않는 고정 패턴 |
학습 임베딩 | - 데이터 분포에 맞춤 최적화 | - 학습 시 최대 길이 제한 |
상대 위치 | - 단어 간 거리 직접 반영 - 유연한 문맥 이해 |
- 구현 복잡 |
위치 인코딩 중 절대 위치 인코딩 중 사인-코사인 계산 방법을 좀 더 자세히 설명하겠습니다.
4.3. 사인-코사인 위치 인코딩 계산 방법 (텍스트 정규화부터 위치인코딩까지)
사인-코사인 위치 인코딩은 고정된 수학적 함수에 기반하여 각 단어의 위치 정보를 벡터로 변환합니다.
이미 텍스트 정규화 및 토큰화된 데이터가 있다고 가정해봅시다.
# 텍스트 정규화 / 토큰화 된 데이터
sentence1 = ["고양이", "가", "강아지", "를", "쫓는다"]
sentence2 = ["강아지", "가", "고양이", "를", "쫓는다"]
# 예시 임베딩 벡터 (임의 값)
embeddings1 = [
[0.1, 0.2, 0.3], # "고양이"
[0.1, 0.1, 0.1], # "가"
[0.3, 0.2, 0.1], # "강아지"
[0.1, 0.0, 0.1], # "를"
[0.2, 0.3, 0.4], # "쫓는다"
]
사인-코사인 위치 인코딩 계산은 짝수/홀수 인덱스 위치에 맞게 별도로 계산합니다.
- pos는 위치 인덱스
- d_model은 임베딩 벡터의 차원 수
- n은 주기성을 조절하는 상수로, 일반적으로 10,000으로 설정합니다. (트랜스포머 모델)
위 식으로 계산한다면
sentence1 = ["고양이", "가", "강아지", "를", "쫓는다"]
embeddings1 = [
[0.1, 0.2, 0.3], # "고양이" 3차원
첫 번째 단어 ”고양이”의 위치 인코딩
- pos=0
- d_model = 3
- n=10000
"고양이" 위치 인코딩 [0, 1, 0]
두 번째 단어 "가"의 위치 인코딩
“가” 위치 인코딩 [0.0001, 0.9999, 0.00001]
이런 식으로 전체를 계산하면
# 텍스트 정규화 / 토큰화 된 데이터
sentence1 = ["고양이", "가", "강아지", "를", "쫓는다"]
sentence2 = ["강아지", "가", "고양이", "를", "쫓는다"]
# 예시 임베딩 벡터 (임의 값)
embeddings1 = [
[0.1, 0.2, 0.3], # "고양이"
[0.1, 0.1, 0.1], # "가"
[0.3, 0.2, 0.1], # "강아지"
[0.1, 0.0, 0.1], # "를"
[0.2, 0.3, 0.4], # "쫓는다"
]
embeddings2 = [
[0.3, 0.2, 0.1], # "강아지"
[0.1, 0.1, 0.1], # "가"
[0.1, 0.2, 0.3], # "고양이"
[0.1, 0.0, 0.1], # "를"
[0.2, 0.3, 0.4], # "쫓는다"
]
# 위치 인코딩 벡터 (임의 계산 값)
positional_encodings1 = [
[0, 1, 0], # "고양이"
[0.0001, 0.9999, 0.00001], # "가"
[0.0002, 0.9998, 0.00002], # "강아지"
[0.0003, 0.9997, 0.00003], # "를"
[0.0004, 0.9996, 0.00004], # "쫓는다"
]
positional_encodings2 = [
[0, 1, 0], # "강아지"
[0.0001, 0.9999, 0.00001], # "가"
[0.0002, 0.9998, 0.00002], # "고양이"
[0.0003, 0.9997, 0.00003], # "를"
[0.0004, 0.9996, 0.00004], # "쫓는다"
]
이 됩니다.
이제 위치 인코딩과 기존 임베딩 백터를 더하면 최종 임베딩 벡터가 나오게 됩니다
# 최종 벡터 (임베딩 + 위치 인코딩)
final_embeddings1 = [
[0.1 + 0, 0.2 + 1, 0.3 + 0], # "고양이"
[0.1 + 0.0001, 0.1 + 0.9999, 0.1 + 0.00001], # "가"
[0.3 + 0.0002, 0.2 + 0.9998, 0.1 + 0.00002], # "강아지"
[0.1 + 0.0003, 0.0 + 0.9997, 0.1 + 0.00003], # "를"
[0.2 + 0.0004, 0.3 + 0.9996, 0.4 + 0.00004], # "쫓는다"
]
final_embeddings2 = [
[0.3 + 0, 0.2 + 1, 0.1 + 0], # "강아지"
[0.1 + 0.0001, 0.1 + 0.9999, 0.1 + 0.00001], # "가"
[0.1 + 0.0002, 0.2 + 0.9998, 0.3 + 0.00002], # "고양이"
[0.1 + 0.0003, 0.0 + 0.9997, 0.1 + 0.00003], # "를"
[0.2 + 0.0004, 0.3 + 0.9996, 0.4 + 0.00004], # "쫓는다"
]
# 텍스트 정규화 / 토큰화 된 데이터
sentence1 = ["고양이", "가", "강아지", "를", "쫓는다"]
sentence2 = ["강아지", "가", "고양이", "를", "쫓는다"]
# 예시 임베딩 벡터 (임의 값)
embeddings1 = [
[0.1, 0.2, 0.3], # "고양이"
[0.1, 0.1, 0.1], # "가"
[0.3, 0.2, 0.1], # "강아지"
[0.1, 0.0, 0.1], # "를"
[0.2, 0.3, 0.4], # "쫓는다"
]
embeddings2 = [
[0.3, 0.2, 0.1], # "강아지"
[0.1, 0.1, 0.1], # "가"
[0.1, 0.2, 0.3], # "고양이"
[0.1, 0.0, 0.1], # "를"
[0.2, 0.3, 0.4], # "쫓는다"
]
# 파이썬 함수로 계산
import numpy as np
def calculate_positional_encoding(pos, d_model):
return [
np.sin(pos / 10000**(2 * i / d_model)) if i % 2 == 0 else np.cos(pos / 10000**(2 * i / d_model))
for i in range(d_model)
]
def add_positional_encoding(embeddings):
d_model = len(embeddings[0])
positional_encodings = [calculate_positional_encoding(i, d_model) for i in range(len(embeddings))]
final_embeddings = [np.add(embeddings[i], positional_encodings[i]) for i in range(len(embeddings))]
return positional_encodings, final_embeddings
def round_embeddings(embeddings):
return [[round(value, 2) for value in vector] for vector in embeddings]
# 적용
positional_encodings1, final_embeddings1_calculated = add_positional_encoding(embeddings1)
positional_encodings2, final_embeddings2_calculated = add_positional_encoding(embeddings2)
# 결과 출력
print("Sentence 1 - Original Embeddings:", round_embeddings(embeddings1))
print("Sentence 1 - Positional Encodings:", round_embeddings(positional_encodings1))
print("Sentence 1 - Final Embeddings:", round_embeddings(final_embeddings1_calculated))
print("\\nSentence 2 - Original Embeddings:", round_embeddings(embeddings2))
print("Sentence 2 - Positional Encodings:", round_embeddings(positional_encodings2))
print("Sentence 2 - Final Embeddings:", round_embeddings(final_embeddings2_calculated))
# 결과
# Sentence 1 - Original Embeddings: [[0.1, 0.2, 0.3], [0.1, 0.1, 0.1], [0.3, 0.2, 0.1], [0.1, 0.0, 0.1], [0.2, 0.3, 0.4]]
# Sentence 1 - Positional Encodings: [[0.0, 1.0, 0.0], [0.84, 1.0, 0.0], [0.91, 1.0, 0.0], [0.14, 1.0, 0.0], [-0.76, 1.0, 0.0]]
# Sentence 1 - Final Embeddings: [[0.1, 1.2, 0.3], [0.94, 1.1, 0.1], [1.21, 1.2, 0.1], [0.24, 1.0, 0.1], [-0.56, 1.3, 0.4]]
# Sentence 2 - Original Embeddings: [[0.3, 0.2, 0.1], [0.1, 0.1, 0.1], [0.1, 0.2, 0.3], [0.1, 0.0, 0.1], [0.2, 0.3, 0.4]]
# Sentence 2 - Positional Encodings: [[0.0, 1.0, 0.0], [0.84, 1.0, 0.0], [0.91, 1.0, 0.0], [0.14, 1.0, 0.0], [-0.76, 1.0, 0.0]]
# Sentence 2 - Final Embeddings: [[0.3, 1.2, 0.1], [0.94, 1.1, 0.1], [1.01, 1.2, 0.3], [0.24, 1.0, 0.1], [-0.56, 1.3, 0.4]]
5. 마치며
지금까지 트랜스포머 모델의 입력 데이터 준비 과정을 살펴보았습니다.
각 단계별로 정리하면 다음과 같습니다.
1. 텍스트 정규화
원시 텍스트를 일관된 형식으로 변환 / 대소문자 통일, 특수문자 제거, 띄어쓰기 정리 등
예: "KuKim!"→ "kukim"
2. 토큰화
텍스트를 작은 단위로 분리 / 단어, 서브워드, 문자 단위 등 다양한 방식 가능
예: "kukim은 학교에 간다" → ["kukim", "은", "학교", "에", "간다"]
3. 토큰 ID 변환
각 토큰을 고유한 정수 ID로 매핑 / 어휘 사전(vocabulary)을 통해 관리
예: "kukim" → 42
4. 임베딩
토큰 ID를 의미를 담은 벡터로 변환 / Word2Vec, BERT 등 다양한 임베딩 방식 활용
예: 42 → [0.1, 0.2, 0.3]
5. 위치 인코딩 추가
임베딩 벡터에 위치 정보를 더함 / 사인-코사인 함수 기반의 위치 벡터 생성
예: [0.1, 0.2, 0.3] + [0, 1, 0] = [0.1, 1.2, 0.3]
트랜스포머의 입력 데이터, 재료가 어떻게 준비되는지 살펴보았습니다.
텍스트 정규화부터 시작하여, 토큰화, 임베딩, 그리고 위치 인코딩까지의 과정을 통해 원시 텍스트가 어떻게 트랜스포머가 이해할 수 있는 형태로 변환되는지 정리했습니다. 다음 글에서는 트랜스포머의 핵심 매커니즘인 어텐션(Attention)에 대해 알아보겠습니다.
긴 글 읽어주셔서 감사합니다.
+) 25/1/31 추가
회사 동료 분께서 글을 보고 트랜스포머 관련 시각화 프로젝트를 공유주셔서 함께 공유합니다.
설명과 시각화가 친절합니다.
Reference
- 트랜스포머 이해하기 1편 colab 연습 (kukim)
- RNN / LSTM https://colah.github.io/posts/2015-08-Understanding-LSTMs/
- https://en.wikipedia.org/wiki/Word2vec
- https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)
'🏭 Data' 카테고리의 다른 글
실시간 데이터 처리 [1/3] - 배치 그리고 스트림 (0) | 2025.01.19 |
---|---|
MAB 알고리즘 [2/2] - MAB와 Thompson Sampling의 아키텍처 및 실전 구현 (w. Kotlin) (0) | 2024.10.28 |
MAB 알고리즘 [1/2] - A/B 테스트의 한계, MAB 알고리즘과 Thompson Sampling 이해하기 (5) | 2024.10.26 |
댓글