01. 단어 임베딩(Word Embedding)
1. 텍스트를 숫자로 바꿔야 하는 이유
머신러닝과 딥러닝 모델은 숫자 형태의 데이터만 처리할 수 있습니다. 따라서 자연어 처리(NLP)는 텍스트를 숫자로 변환하는 단계에서 출발합니다.
2. 단어를 숫자로 변환하는 대표 방식
단어를 숫자로 변환하는 세 가지 대표적인 방식이 있습니다.
- 정수 인덱스: 단어를 단순한 정수 값으로 매핑합니다. (예: "나는" -> 0, "밥을" -> 1)
- 원-핫 인코딩 (One-Hot Encoding):
- 임베딩 (Embedding):

3. 단어 임베딩의 아이디어 (Word Embedding)
단어 임베딩은 원-핫 인코딩의 단점, 특히 의미적 정보의 부재와 고차원 문제를 해결하기 위해 고안된 방법입니다.
임베딩의 정의 및 목표
- 정의: 각 단어를 의미를 반영하는 작은 차원(저차원)의 실수 벡터로 표현하는 방법입니다.
- 목표: 벡터 간의 거리와 방향이 해당 단어 간의 유사성과 관계를 반영하도록 학습하는 것입니다.
- 요약: 단어의 의미를 벡터 공간에 매핑해 두는 것입니다.
주요 특징
- 의미적 유사성 반영: 벡터 공간에서 **의미가 비슷한 단어(예: "밥"과 "국수")**끼리는 가깝게 배치되고, **전혀 다른 개념(예: "밥" vs "학교")**은 멀리 배치되도록 학습됩니다.
- 활용: 텍스트 분류, 감정 분석, 기계 번역 등 NLP 작업에 필수적인 요소입니다.
- 예시 (5차원 임베딩): | 단어 | 임베딩 벡터 (예시) | | :--- | :--- |
| 나는 |$[0.13, 1.20, -0.70, 0.05, 0.30]$ | | 밥 | $[-0.50, 0.95, -0.20, 0.10, 0.80]$ |
| 국수 |$[-0.45, 0.90, -0.25, 0.12, 0.75]$ | | 학교 | $[1.30, -0.20, 0.50, -0.10, -0.60]$ |
4. 딥러닝 내부의 임베딩 레이어 구조
딥러닝 모델에서 단어 임베딩은 **임베딩 레이어(Embedding Layer)**를 통해 구현됩니다.
- 임베딩 레이어의 역할: 정수 인덱스를 실수 벡터로 매핑하는 테이블 역할을 합니다.
- 구조: 이 테이블은 $W$라는 가중치 행렬로 구성됩니다.
- 행 (Row): 단어의 개수 ($vocab\_size$)
- 열 (Column): 임베딩 차원 수 ($embedding\_dim$)

- 입력/출력:
- 입력: 단어 인덱스 $i$
- 출력: 행렬 $W$의 $i$번째 행 (해당 단어의 임베딩 벡터)
- 학습: 이 **가중치 행렬 $W$**는 역전파(gradient descent)를 통해 모델의 다른 부분과 함께 학습됩니다. 즉, 감성 분류, 토픽 분류 등의 작업을 학습하면서 단어 벡터도 같이 업데이트됩니다.
- 특징,설명
저차원 (Dense Vector)
수만 차원이 아니라 수십~수백 차원의 작은 크기입니다. 대부분의 값이 0이 아니라 실수로 빽빽하게 채워진 벡터입니다.
의미적 유사성 반영
비슷한 맥락에서 많이 등장하는 단어들끼리 벡터가 가깝게 학습됩니다. 코사인 유사도 등을 사용하여 단어 유사도를 계산할 수 있습니다.
사전학습 후 재사용 가능,
"Word2Vec, fastText 등이 대표적인 사전학습 임베딩입니다. 위키/뉴스 전체를 가지고 임베딩만 미리 학습해 놓고, 나중에 개별 프로젝트(리뷰 분석 등)에 가져와서 사용할 수 있습니다."
from collections import (
Counter,
) # 단어의 등장 횟수(빈도수)를 쉽게 계산하기 위해 Counter 모듈을 임포트합니다.
import torch # 텐서 연산과 딥러닝 모델 구축을 위해 PyTorch 라이브러리를 임포트합니다.
import torch.nn as nn # 신경망 모듈(레이어)을 정의하기 위해 임포트합니다.
import torch.nn.functional as F # 코사인 유사도 계산 함수(F.cosine_similarity)를 사용하기 위해 임포트합니다.
# 1) 간단한 말뭉치 (Corpus): 분석 대상이 되는 문장 데이터입니다.
sentences = [
"이 영화 정말 최고였어요",
"이 배우 연기가 최고입니다",
"내용이 지루하고 별로였어요",
"스토리가 지루하지만 배우는 좋았어요",
]
# ... (방법 1 주석 처리) ...
#! 방법2 이게 좋음 이유: 빈도수로 오름차순이 좋음----------------------------------------
# 2) 단어집(Vocab) 및 임베딩 레이어 생성 (방법 2: 빈도수 기반)
# ----------------------------------------
# 각 문장을 공백 기준으로 토큰(단어) 리스트로 분리하여 2차원 리스트를 생성합니다. (토큰화)
tokenized = [sentence.split() for sentence in sentences]
counter = Counter() # 단어 빈도를 셀 Counter 객체를 초기화합니다.
for sent in tokenized:
counter.update(sent) # 각 문장의 단어 리스트를 Counter에 넣어 빈도를 누적합니다.
vocab = {} # 단어집 {단어: 인덱스}를 저장할 딕셔너리를 초기화합니다.
# most_common()을 사용해 빈도수가 높은 순서대로 단어를 가져와 인덱스를 부여합니다.
# (실제 NLP에서 가장 흔하게 사용되는 방식입니다.)
for word, _ in counter.most_common():
vocab[word] = len(
vocab
) # 현재 vocab의 크기를 인덱스로 사용하여 0부터 순차적으로 부여합니다.
vocab_size = len(vocab) # 단어집의 크기(고유 단어의 총 개수)를 저장합니다.
embed_dim = 8 # 임베딩 벡터의 차원(크기)을 8로 설정합니다.
# nn.Embedding 레이어를 정의합니다.
# num_embeddings: 단어집 크기 (vocab_size)
# embedding_dim: 임베딩 벡터 차원 (embed_dim)
embed = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embed_dim)
# 임베딩 레이어의 전체 정보(하이퍼파라미터)를 출력합니다.
print("\nembed\n", embed)
# 임베딩 가중치 표(행렬 W)의 초기 랜덤 값을 출력합니다.
print("\nembed.weight\n", embed.weight)
# 임베딩 가중치 표(행렬 W)의 shape을 출력합니다. (vocab_size, embed_dim) 형태입니다.
print("\nembed.weight.shape\n", embed.weight.shape)
# 3) 문장을 단어 인덱스 시퀀스로 변환
# ----------------------------------------
# 토큰화된 문장을 단어집(vocab)의 인덱스 리스트로 변환하는 함수를 정의합니다.
def sentence_to_indices(sentence_tokens, vocab):
# 리스트 컴프리헨션을 사용하여 각 단어를 해당 인덱스로 변환합니다.
return [vocab[w] for w in sentence_tokens]
# 최종적으로 생성된 단어집의 내용을 확인합니다.
print("\nvocab\n", vocab)
# 토큰화된 문장의 형태를 확인합니다.
print("\ntokenized\n", tokenized)
# 전체 토큰화된 문장들을 인덱스 시퀀스(정수 리스트)로 변환합니다.
indexed_sentences = [sentence_to_indices(s, vocab) for s in tokenized]
print("indexed_sentences :", indexed_sentences)
# 4) 문장 임베딩(단어 벡터)을 얻는 함수 정의
# ----------------------------------------
# 인덱스 리스트를 입력받아 문장 임베딩 벡터(평균)를 추출하는 함수를 정의합니다.
def get_sentence_embedding(idx_list):
# 파이썬 리스트 형태의 인덱스 리스트를 PyTorch 텐서로 변환합니다.
idx_tensor = torch.tensor(idx_list)
# nn.Embedding 레이어에 인덱스 텐서를 전달하여 해당하는 단어 임베딩 벡터들을 조회합니다.
# 결과 word_embeds의 shape은 (문장 길이, embed_dim)이 됩니다.
word_embeds = embed(idx_tensor)
# 단어 벡터들의 평균을 계산하여 문장 전체를 대표하는 하나의 벡터를 만듭니다. (Mean Pooling)
# dim=0 (단어 차원)을 기준으로 평균을 구하여 (embed_dim) shape의 벡터를 반환합니다.
doc_embed = word_embeds.mean(dim=0)
return doc_embed
# 모든 문장에 대해 임베딩 벡터를 생성하고, torch.stack을 사용하여 하나의 텐서로 합칩니다.
# (문장 임베딩의 shape이 모두 (embed_dim,)로 동일하므로 stack 사용 가능)
sentence_embeddings = torch.stack(
[get_sentence_embedding(idex_list) for idex_list in indexed_sentences]
)
# 최종 문장 임베딩 텐서의 내용을 출력합니다. shape은 (문장 개수, embed_dim)이 됩니다.
print("\nsentence_embeddings\n", sentence_embeddings)
# 5) 문장 유사도 측정 (코사인 유사도)
# ----------------------------------------
# 두 벡터(a, b)의 코사인 유사도를 계산하고 Python float 값으로 반환하는 함수를 정의합니다.
def consine_sim(a, b):
# F.cosine_similarity(벡터1, 벡터2, 계산할 차원).item()으로 유사도 스칼라 값을 얻습니다.
return F.cosine_similarity(a, b, dim=0).item()
print("\n문장유사도\n")
# 모든 가능한 문장 쌍(Pair)에 대해 반복합니다.
for i in range(len(sentences)):
# j는 i보다 큰 인덱스부터 시작하여 중복 계산을 방지합니다.
for j in range(i + 1, len(sentences)):
# 두 문장 임베딩 벡터의 유사도를 계산합니다.
sim = consine_sim(sentence_embeddings[i], sentence_embeddings[j])
# 결과를 문장과 함께 소수점 셋째 자리까지 출력합니다.
print(f"({i}) {sentences[i]} vs ({j}) {sentences[j]} -> 유사도 : {sim:.3f}")
Word2Vec
Word2Vec의 핵심 개념
Word2Vec은 텍스트 말뭉치(Corpus)에서 단어의 의미를 벡터 공간에 표현하는 방법론입니다. 그 핵심 아이디어는 다음과 같습니다.
- "비슷한 문맥에서 쓰이는 단어들은 의미가 비슷하다."
Word2Vec의 학습 과정
Word2Vec은 거대한 말뭉치(뉴스, 위키 등)를 사용하여 단어의 임베딩 벡터를 학습하며, 주로 두 가지 모델 구조를 사용합니다.
- 주변 단어들을 보고 중심 단어를 맞추기 (CBOW)
- 중심 단어를 보고 주변 단어를 맞추기 (Skip-gram)
- 1, 2의 과정을 반복 학습하면서, 각 단어의 임베딩 벡터를 학습합니다.
주요 모델 구조 비교
1. CBOW (Continuous Bag of Words)
CBOW는 주변 단어들의 정보를 기반으로 가운데 중심 단어를 예측하는 방식입니다.
- 입력: 주변 단어들
- 출력: 중심 단어 예측
- 특징:
- 학습 속도가 빠릅니다.
- 자주 나오는 단어 (빈번한 단어)에 대해 강한 성능을 보입니다.
- 주변 단어의 'Bag of Words' (순서를 고려하지 않고 단어의 존재 여부만 고려) 형태로 정보를 사용합니다.
2. Skip-gram
Skip-gram은 가운데 중심 단어를 기반으로 주변 단어들을 예측하는 방식입니다.
- 입력: 중심 단어
- 출력: 주변 단어들 예측
- 특징:
- CBOW보다 느리지만, 더 섬세한 표현 (미묘한 의미 차이)을 얻는 경우가 많습니다.
- 드문 단어 (rare word)를 처리하는 데 더 유리합니다.
word2vec
from gensim.models import Word2Vec
import torch
import torch.nn as nn
sentences = [
["이", "영화", "정말", "최고", "였다"],
["배우", "연기", "가", "최고", "이다"],
["스토리", "가", "지루하다"],
["내용", "이", "지루하고", "별로", "였다"],
["이", "브랜드", "디자인", "이", "세련되다"],
["이", "브랜드", "가격", "은", "비싸다"],
]
model = Word2Vec(
sentences,
vector_size=50,
window=3,
min_count=1,
sg=1,
workers=1, # 병렬 처리에 사용할 CPU 코어 개수
epochs=100, # 전체 말뭉치를 몇 번 반복해서 학습할지
)
# print("단어 '최고' 벡터 크기 : ", model.wv["최고"].shape)
# print("벡터 앞 5개 : ", model.wv["최고"][:5])
# print("\n[단어 '최고'와 비슷한 단어 Top-5]")
# for w, score in model.wv.most_similar("최고", topn=5):
# print(f"{w:10s} 유사도:{score:.3f}")
# print("\n단어간 유사도")
# print("최고 vs 별로 : ", model.wv.similarity("최고", "별로"))
# print("지루하다 vs 별로 : ", model.wv.similarity("지루하다", "별로"))
word_index = model.wv.key_to_index
index_word = model.wv.index_to_key
# print(word_index)
# print(index_word)
vocab_size = len(word_index)
embed_dim = model.vector_size
print("vocab_size : ", vocab_size)
print("embed_dim : ", embed_dim)
pretrained_weights = model.wv.vectors
print("pretrained_weights.shape : ", pretrained_weights.shape)
# 임베딩 레이어 생성
embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embed_dim)
embedding.weight.data.copy_(torch.from_numpy(pretrained_weights))
word1 = "최고"
word2 = "지루하다"
idx1 = word_index[word1]
idx2 = word_index[word2]
idx_tensor = torch.tensor([idx1, idx2])
emb_vec = embedding(idx_tensor)
print("\n Pytorch Embedding으로 가져온 벡터 ")
print(f"{word1} 임베딩 벡터 : ", emb_vec[0][:5])
print(f"{word2} 임베딩 벡터 : ", emb_vec[1][:5])
----------
from gensim.models import Word2Vec
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
# 가변 길이 텐서를 배치로 만들 때 길이를 맞추기 위해 패딩하는 함수
from torch.nn.utils.rnn import pad_sequence
# -----------------------------------------------------------
# 1) 데이터 및 Word2Vec 학습 준비
# -----------------------------------------------------------
# 예제용 문장 (토큰화된 리스트)과 감성 라벨 (1=긍정, 0=부정)
sentences = [
["이", "영화", "정말", "최고", "였다"], # 1
["배우", "연기", "가", "너무", "좋다"], # 1
["스토리", "가", "지루하고", "별로", "였다"], # 0
["내용", "이", "지루하다"], # 0
["음악", "이", "감동적이고", "최고", "였다"], # 1
["연출", "이", "허술하고", "지루하다"], # 0
]
labels = [1, 1, 0, 0, 1, 0]
# Word2Vec 모델 학습 설정 및 실행
model = Word2Vec(
sentences,
vector_size=50, # 임베딩 벡터의 차원 (embed_dim)
window=3, # 컨텍스트 윈도우 크기
min_count=1, # 최소 단어 출현 빈도
sg=1, # Skip-gram 방식 사용
workers=1,
epochs=200,
)
# 학습된 Word2Vec 단어장 정보 추출
word_index = model.wv.key_to_index
print("\nword_index\n", word_index)
index_word = model.wv.index_to_key
print("\nindex_word\n", index_word)
vocab_size = len(word_index) # 단어장의 크기 (패딩 제외)
print("\nvocab_size\n", vocab_size)
embed_dim = model.vector_size # 임베딩 차원 (50)
print("\nembed_dim\n", embed_dim)
# 학습된 Word2Vec 벡터 (가중치) 추출
pretrained_weights = model.wv.vectors
# 패딩 인덱스 정의: 기존 단어장 크기 다음의 인덱스를 사용
PAD_IDX = vocab_size
vocab_size_with_pad = vocab_size + 1
# 패딩 인덱스를 포함하도록 가중치 행렬을 확장 (Word2Vec 벡터 + 패딩 벡터)
extended_weights = np.zeros((vocab_size_with_pad, embed_dim), dtype=np.float32)
# 기존 Word2Vec 가중치를 복사
extended_weights[:vocab_size, :] = pretrained_weights
# 패딩 인덱스에 해당하는 벡터는 0.0으로 초기화
extended_weights[PAD_IDX, :] = 0.0
# -----------------------------------------------------------
# 2) PyTorch Dataset 및 DataLoader 정의
# -----------------------------------------------------------
class PaddedTextDataset(Dataset):
"""
토큰화된 문장과 라벨을 받아 Word2Vec 인덱스 텐서와 라벨 텐서를 반환하는 Dataset
"""
def __init__(self, tokenized_sentences, labels, word_index):
super().__init__()
self.sentences = tokenized_sentences
self.labels = labels
self.word_index = word_index
def __len__(self):
return len(self.labels)
def __getitem__(self, index):
tokens = self.sentences[index]
y = self.labels[index]
# 단어 리스트를 Word2Vec 인덱스 리스트로 변환
idxs = [self.word_index[w] for w in tokens]
# 인덱스 리스트를 PyTorch 텐서로 변환
idx_tensor = torch.tensor(idxs, dtype=torch.long)
label_tensor = torch.tensor(y, dtype=torch.float32)
return idx_tensor, label_tensor
def collate_fn_with_pad(batch):
"""
DataLoader에서 사용될 콜레이트 함수: 배치 내 가변 길이 시퀀스에 패딩을 추가
"""
# zip(*batch)를 사용하여 문장 인덱스 텐서와 라벨 텐서를 분리
sequences, labels = zip(*batch)
# pad_sequence를 사용하여 배치 내 텐서들을 패딩하여 크기를 통일
# batch_first=True: [배치 크기, 시퀀스 길이] 형태로 만듦
# padding_value=PAD_IDX: 패딩에 사용할 토큰 인덱스를 지정
padded_seqs = pad_sequence(sequences, batch_first=True, padding_value=PAD_IDX)
# 라벨들을 하나의 텐서로 쌓아올림
labels = torch.stack(labels)
# [패딩된 시퀀스 텐서, 라벨 텐서] 반환
return padded_seqs, labels
# -----------------------------------------------------------
# 3) PyTorch 모델 정의 (PaddedSentClassifier)
# -----------------------------------------------------------
class PaddedSentClassifier(nn.Module):
"""
패딩된 문장 배치를 처리하고 평균 풀링으로 분류하는 신경망 모델
"""
def __init__(self, embedding: nn.Embedding, pad_idx=int):
super().__init__()
self.embedding = embedding
self.pad_idx = pad_idx
# 임베딩 차원을 입력으로, 1차원 출력을 내는 완전 연결 레이어
self.fc = nn.Linear(embedding.embedding_dim, 1)
def forward(self, idx_batch):
# idx_batch: [배치 크기, 시퀀스 길이] (토큰 인덱스)
# 1. 임베딩 벡터 생성: [배치 크기, 시퀀스 길이, 임베딩 차원]
emb = self.embedding(idx_batch)
# 2. 마스크 생성: 패딩이 아닌 위치는 1.0, 패딩 위치는 0.0인 텐서 [배치 크기, 시퀀스 길이]
mask = (idx_batch != self.pad_idx).float()
# 3. 실제 길이 계산: 마스크의 합 = 실제 토큰 개수 [배치 크기, 1]
lenghs = mask.sum(dim=1, keepdim=True)
# 4. 임베딩 차원으로 마스크 확장: [배치 크기, 시퀀스 길이, 1]
extended_mask = mask.unsqueeze(dim=2)
# 5. 마스킹된 임베딩 생성: 패딩 위치의 임베딩을 0으로 만듦 [배치 크기, 시퀀스 길이, 임베딩 차원]
masked_emb = emb * extended_mask
# 6. 합산 풀링 (Sum Pooling): 모든 시퀀스 차원(dim=1)을 따라 합산 [배치 크기, 임베딩 차원]
sum_emb = masked_emb.sum(dim=1)
# 7. 평균 풀링 (Average Pooling): 합을 실제 길이로 나누어 문장 벡터 생성
# lenghs.clamp(min=1.0)는 길이가 0인 경우(일반적으로 발생하지 않음) 0으로 나누는 것을 방지
sent_vec = sum_emb / lenghs.clamp(min=1.0) # [배치 크기, 임베딩 차원]
# 8. 분류 (Fully Connected Layer): 문장 벡터를 입력으로 로짓 계산
# squeeze(1)로 불필요한 차원 제거 (출력: [배치 크기])
logits = self.fc(sent_vec).squeeze(1)
return logits
# -----------------------------------------------------------
# 4) 모델 학습 함수 정의
# -----------------------------------------------------------
def train_model(model, loader, num_epochs=50, lr=0.01):
"""
모델 학습을 수행하는 함수
"""
# 손실 함수 정의: Sigmoid와 이진 교차 엔트로피를 결합 (안정적)
criterion = nn.BCEWithLogitsLoss()
# 옵티마이저 정의: 모델의 모든 학습 가능한 파라미터를 대상으로 Adam 사용
optimizer = optim.Adam(model.parameters(), lr=lr)
for epoch in range(1, num_epochs + 1):
total_loss = 0.0
model.train() # 모델을 학습 모드로 설정
for x, y in loader:
optimizer.zero_grad() # 이전 스텝의 그래디언트 초기화
logits = model(x) # Forward Pass (예측 로짓)
loss = criterion(logits, y) # 손실 계산
loss.backward() # Backward Pass (그래디언트 계산)
optimizer.step() # 옵티마이저 스텝 (파라미터 업데이트)
total_loss += loss.item()
avg_loss = total_loss / len(loader)
if epoch % 10 == 0 or epoch == 1:
print(f"[Epoch {epoch:02d}] loss = {avg_loss:.4f}")
# -----------------------------------------------------------
# 5) 모델 인스턴스화 및 학습 실행
# -----------------------------------------------------------
# Dataset 및 DataLoader 인스턴스 생성
padded_dataset = PaddedTextDataset(sentences, labels, word_index)
padded_loader = DataLoader(
padded_dataset, batch_size=2, shuffle=True, collate_fn=collate_fn_with_pad
)
# nn.Embedding 레이어 생성 (패딩 포함된 단어장 크기)
padded_embedding = nn.Embedding(vocab_size_with_pad, embed_dim)
# **사전 학습된 가중치(Word2Vec) 로드**
# torch.no_grad() 블록 안에서 수행하여 그래디언트 계산 없이 가중치 복사
with torch.no_grad():
# numpy 배열을 torch 텐서로 변환 후, Embedding 레이어의 weight에 복사
padded_embedding.weight.copy_(torch.from_numpy(extended_weights))
# 가중치 학습 가능 설정 (Word2Vec 임베딩을 Fine-tuning)
padded_embedding.weight.requires_grad = True
# 최종 분류 모델 인스턴스 생성 및 학습 시작
padded_model = PaddedSentClassifier(padded_embedding, pad_idx=PAD_IDX)
print("\n모델 학습 시작:")
train_model(padded_model, padded_loader, num_epochs=30, lr=0.01)
# 학습 로그 출력 (예시)
# [Epoch 01] loss = 0.6931
# [Epoch 10] loss = 0.6000
# [Epoch 20] loss = 0.5000
# [Epoch 30] loss = 0.4000
FastText
Word2Vec의 한계
FastText는 기존 Word2Vec의 다음과 같은 한계를 극복하기 위해 제안되었습니다.
- 단어 전체를 하나의 토큰으로만 취급하여 단어 내부의 구조를 고려하지 않습니다.
- 형태가 비슷한 단어 (예: 복수형, 조사 변화 등) 간의 관계를 잘 포착하지 못할 수 있습니다.
- 학습 시점에 등장하지 않은 완전히 새로운 단어(OOV: Out-Of-Vocabulary)에 대해서는 단어 벡터를 생성할 수 없습니다.
FastText의 핵심 아이디어
FastText는 Word2Vec의 한계를 해결하기 위해 단어를 다음과 같이 처리합니다.
- "단어를 문자 n-gram의 모음으로 본다."
- 예를 들어, "사랑스러운"이라는 단어는 *사랑스러운* 으로 간주하고, 이를 일정한 길이의 문자 조각, 즉 subword 단위(n-gram)로 나눕니다.
- 예시: $n=3$이라고 가정하면, *사, 사랑, 사랑스, 랑스러, 스러운, 러운*, 운** 등과 같은 subword 단위(n-gram)로 쪼개게 됩니다. (실제 구현에서는 단어의 시작과 끝을 나타내는 $<$와 $>$ 문자도 함께 사용합니다.)
FastText의 단어 벡터 생성 과정
- 각 subword(n-gram)에 대해 임베딩(벡터)을 학습합니다.
- 특정 단어의 벡터는 그 단어를 구성하는 모든 subword들의 임베딩을 합쳐서(평균 내어) 생성합니다.
FastText의 장점
이러한 접근 방식은 다음과 같은 이점을 제공합니다.
- 철자가 비슷한 단어 처리: 철자가 비슷한 단어들은 공통된 subword를 공유하게 되므로, 자연스럽게 벡터 공간에서 비슷한 위치에 배치됩니다. 이를 통해 복수형, 동사 변화형 등 형태소적 관계를 더 잘 포착할 수 있습니다.
- OOV(새로운 단어) 처리: 학습 시점에 보지 못했던 새로운 단어(OOV)일지라도, 그 단어 안에 있는 subword들이 이미 학습되어 있다면, 학습된 subword 임베딩을 합쳐서 해당 단어의 벡터를 생성할 수 있습니다. 이로 인해 OOV 문제에 대한 강점을 가집니다.
from gensim.models import FastText
import numpy as np # 벡터 출력을 보기 좋게 하기 위해 numpy import
# 학습에 사용할 샘플 문장 (토큰화된 상태)
sentences = [
["이", "영화", "정말", "최고", "였다"],
["배우", "연기", "가", "최고", "이다"],
["스토리", "가", "지루하다"],
["내용", "이", "지루하고", "별로", "였다"],
["이", "브랜드", "디자인", "이", "세련되다"],
["이", "브랜드", "가격", "은", "비싸다"],
]
# FastText 모델 학습
ft_model = FastText(
sentences,
vector_size=100, # 생성할 단어 벡터의 차원 수
window=5, # 주변 단어를 고려할 윈도우 크기
min_count=1, # 최소 출현 빈도를 1로 설정하여 모든 단어를 단어 집합(Vocabulary)에 포함 (이전 KeyError 방지)
min_n=2, # subword의 최소 길이 (2-gram)
max_n=4, # subword의 최대 길이 (4-gram)
sg=1, # Skip-gram 방식 사용 (sg=0이면 CBOW)
workers=4, # 학습에 사용할 CPU 코어 수
)
# 학습된 단어 "브랜드"의 벡터 크기 출력
# min_count=1 덕분에 "브랜드"는 정식 단어 벡터로 학습됨
print('단어 "브랜드" 벡터 크기 : ', ft_model.wv["브랜드"].shape)
# OOV(Out-Of-Vocabulary) 단어 정의
oov_word = "브랜드맛집"
# OOV 단어의 벡터 생성 및 출력
# FastText는 단어 집합에 없어도, 단어를 구성하는 subword들의 합으로 벡터를 생성할 수 있음
oov_vec = ft_model.wv[oov_word]
print("\n# OOV 단어 '브랜드맛집'의 벡터 (subword 합)\n", oov_vec)
# 정식 단어 "브랜드"의 벡터 출력
print('\n# 정식 단어 "브랜드"의 벡터\n', ft_model.wv["브랜드"])
# OOV 단어 "브랜드맛집"과 정식 단어 "브랜드" 사이의 유사도 계산 및 출력
# FastText의 강점: OOV 단어라도 유사한 벡터를 가질 수 있음
print("브랜드 vs 브랜드맛집 유사도 : ", ft_model.wv.similarity("브랜드", "브랜드맛집"))
# FastText가 학습한 subword(n-gram) 벡터들의 배열 크기 출력
# (gensim 기본값: 2,000,000개의 해시 버킷 크기, 100차원)
print(
"\n# Subword 벡터 저장 공간 크기 (hash bucket, vector_size)\n",
ft_model.wv.vectors_ngrams.shape,
)
# "지루하고" 단어의 인덱스 확인 (min_count=1 덕분에 이제 KeyError 없이 접근 가능)
word = "지루하고"
idx = ft_model.wv.key_to_index[word]
print("\n# 단어 '지루하고'의 단어 집합 인덱스 : ", idx)
# "지루하고" 단어를 구성하는 subword들이 저장된 해시 버킷 인덱스(list of hash indices)를 가져옴
bucket_indices = ft_model.wv.buckets_word[idx]
print("# '지루하고'를 구성하는 subword들이 매핑된 버킷 인덱스 : \n", bucket_indices)
# 위의 버킷 인덱스를 사용하여 실제 subword 벡터들을 가져옴
subword_vectors = ft_model.wv.vectors_ngrams[bucket_indices]
# "지루하고" 단어가 몇 개의 subword로 구성되었는지 확인
print("\n# '지루하고' 단어를 구성하는 subword의 개수 : ", subword_vectors.shape[0])
# 첫 번째 subword 벡터의 처음 5차원만 출력
print("# 첫 번째 subword 벡터 (처음 5차원) : ")
print(subword_vectors[0][:5])'데이터 분석 > 머신러닝, 딥러닝' 카테고리의 다른 글
| Attention (0) | 2025.12.09 |
|---|---|
| 시퀀스 모델 (0) | 2025.12.05 |
| 딥러닝 텍스트 전처리 (0) | 2025.12.01 |
| K-means Document Clustering (0) | 2025.11.28 |
| Cosine 유사도 (0) | 2025.11.28 |