Embedding 개론
Text Embedding / Sentence Embedding이란?
자연어 처리(NLP)에서 Embedding(임베딩)은 텍스트 데이터를 컴퓨터가 처리할 수 있는 형태로 변환하는 가장 핵심적인 표현 방식입니다.
사람에게는 직관적으로 이해되는 문장이나 단어도, 컴퓨터에게는 단순한 문자열에 불과하기 때문에 이를 수치적인 표현으로 바꾸는 과정이 반드시 필요합니다. 이때 사용되는 것이 임베딩입니다.
임베딩은 단순히 텍스트를 숫자로 바꾸는 기술이 아니라, 텍스트가 가진 의미를 벡터 공간 상의 위치로 표현하는 방법입니다. 의미적으로 유사한 텍스트는 벡터 공간에서도 서로 가까운 위치에 놓이도록 학습되며, 이를 통해 검색, 추천, 분류, RAG(Retrieval-Augmented Generation)와 같은 다양한 시스템이 동작할 수 있습니다.
이 글에서는 임베딩의 기본 개념부터 시작하여, 현대적인 임베딩 시스템에서 반드시 이해해야 할 구조적 요소들까지 단계적으로 설명드리고자 합니다.
1. 임베딩(Embedding)의 개념과 역할
임베딩이란 텍스트, 이미지, 오디오와 같은 비정형 데이터를 고정된 차원의 실수 벡터로 변환하는 과정을 의미합니다. 특히 텍스트 임베딩은 자연어 처리 전반에서 가장 기본적인 표현 방식으로 사용됩니다.
텍스트 임베딩의 핵심 목적은 다음과 같습니다.
첫째, 텍스트의 의미를 수치적으로 표현하는 것입니다. 단어와 문장은 단순한 문자열이 아니라 의미를 가지며, 임베딩은 이 의미를 숫자의 패턴으로 압축하여 표현합니다.
둘째, 의미적 유사성을 계산 가능하게 만드는 것입니다. 두 문장이 의미적으로 비슷하다면, 임베딩 벡터 간의 거리도 가깝게 유지되도록 학습됩니다. 이를 통해 코사인 유사도나 내적과 같은 수학적 연산으로 의미 비교가 가능해집니다.
셋째, 머신러닝 모델이 텍스트를 입력으로 받아 처리할 수 있도록 하는 것입니다. 대부분의 모델은 숫자 입력만을 처리할 수 있기 때문에, 임베딩은 텍스트와 모델 사이의 필수적인 연결 고리 역할을 합니다.
요약하면, 임베딩은 의미를 기하학적 공간으로 옮기는 표현 방식이라고 볼 수 있습니다.
2. Text Embedding과 Sentence Embedding의 두 가지 관점
Text Embedding과 Sentence Embedding이라는 용어는 문헌과 실무에서 혼용되는 경우가 많습니다. 이를 명확히 이해하기 위해서는 두 가지 서로 다른 관점에서 바라볼 필요가 있습니다.
2.1 입력 단위 기준의 구분
첫 번째 관점은 임베딩이 처리하는 입력 단위의 크기를 기준으로 구분하는 방식입니다.
Text Embedding은 주로 단어(word), 토큰(token), 또는 매우 짧은 텍스트 단위를 임베딩하는 것을 의미합니다. 이 방식은 어휘 수준의 의미 표현에 초점을 두며, 각 단어가 가지는 일반적인 의미를 벡터로 표현하는 데 목적이 있습니다. Word2Vec, GloVe, FastText와 같은 모델들이 대표적인 예입니다.
Sentence Embedding은 문장(sentence), 문단(paragraph), 문서(document)처럼 더 큰 단위를 하나의 벡터로 표현하는 방식입니다. 이 경우 단어 간의 관계와 문맥이 함께 고려되며, 문장이 전달하는 전체적인 의미가 벡터에 반영됩니다. Sentence-BERT나 Universal Sentence Encoder가 여기에 해당합니다.
이 관점에서는 Sentence Embedding이 Text Embedding을 포함하는 상위 개념이라고 이해하셔도 무방합니다.
2.2 사용 목적 기준의 구분 (현대 실무 관점)
두 번째 관점은 최근 LLM 기반 시스템에서 더 자주 사용되는 구분 방식으로, 입력 단위보다는 사용 목적에 초점을 둡니다.
Text Embedding이라는 용어는 검색, RAG, 벡터 데이터베이스 저장과 같이 “텍스트를 의미 공간에 매핑하여 비교하는 목적”으로 사용되는 경우가 많습니다. 이때의 텍스트는 문장일 수도 있고, 문서의 일부인 chunk일 수도 있습니다.
Sentence Embedding은 문장 간 의미 비교, 중복 문장 제거, 문장 단위 클러스터링이나 분류처럼 보다 정밀한 문장 의미 판단이 필요한 경우에 사용됩니다.
이 관점에서는 두 용어의 차이가 모델 구조라기보다는 활용 시나리오에 따른 명칭 차이에 가깝습니다.
3. 희소 임베딩(Sparse Embedding)과 밀집 임베딩(Dense Embedding)
임베딩은 벡터의 구조에 따라 크게 희소 임베딩과 밀집 임베딩으로 나눌 수 있습니다. 이 구분은 임베딩의 성질을 이해하는 데 매우 중요하지만, 동시에 오해가 자주 발생하는 지점이기도 합니다.
3.1 희소 임베딩(Sparse Embedding)
희소 임베딩은 차원이 매우 크고, 대부분의 값이 0으로 채워진 벡터 표현 방식입니다. 각 차원은 특정 단어나 토큰에 대응되며, 해당 단어가 등장했는지 또는 얼마나 자주 등장했는지를 나타냅니다.
대표적인 방식으로는 Bag of Words, TF-IDF, BM25 등이 있습니다. 이 방식들은 통계적 기반 위에서 동작하며, 단어의 출현 빈도와 중요도를 수치화합니다.
희소 임베딩의 가장 큰 장점은 해석 가능성입니다. 벡터의 각 차원이 어떤 단어를 의미하는지 명확하기 때문에, 왜 특정 문서가 선택되었는지 설명하기 쉽습니다. 또한 고유명사, 숫자, 코드, 에러 메시지와 같이 정확한 키워드 매칭이 중요한 경우에는 여전히 매우 강력한 성능을 보입니다.
반면, 단어 간의 의미적 유사성이나 문맥을 반영하지 못한다는 한계가 있습니다. 의미는 비슷하지만 표현이 다른 경우에는 전혀 다른 벡터로 취급됩니다.
3.2 밀집 임베딩(Dense Embedding)
밀집 임베딩은 차원이 비교적 작고, 모든 차원이 실수값으로 채워진 벡터 표현 방식입니다. 이 방식은 신경망 모델을 통해 학습되며, 단어 또는 문장의 의미가 여러 차원에 분산되어 표현됩니다.
Word2Vec이나 Sentence-BERT, 최근의 LLM 기반 임베딩 모델들이 여기에 해당합니다. 밀집 임베딩은 문맥을 반영하고, 동의어나 유사한 표현을 효과적으로 포착할 수 있다는 장점이 있습니다.
이러한 특성 덕분에 밀집 임베딩은 의미 기반 검색, 추천 시스템, RAG와 같은 현대적인 NLP 시스템에서 핵심적인 역할을 합니다. 반면, 벡터의 각 차원이 무엇을 의미하는지 직관적으로 해석하기는 어렵습니다.
3.3 희소 vs 밀집 비교의 한계
희소 임베딩과 밀집 임베딩을 단순히 “어느 쪽이 더 좋다”는 관점에서 비교하는 것은 현대 시스템에서는 적절하지 않습니다.
희소 임베딩은 정확한 키워드 매칭과 높은 precision에 강점이 있고, 밀집 임베딩은 의미적 확장을 통한 높은 recall에 강점이 있습니다. 두 방식은 경쟁 관계라기보다는 서로 다른 문제를 해결하는 도구에 가깝습니다.
이러한 인식 위에서 등장한 개념이 Hybrid Embedding입니다.
4. Hybrid Embedding 개념
Hybrid Embedding은 희소 임베딩과 밀집 임베딩을 함께 사용하는 구조를 의미합니다. 실제 검색 시스템이나 RAG 파이프라인에서는 이 방식이 사실상의 표준으로 자리 잡고 있습니다.
일반적인 Hybrid 구조에서는 먼저 희소 임베딩 기반 검색(BM25 등)을 통해 키워드 정확도가 높은 후보 문서들을 추려냅니다. 이후 밀집 임베딩을 활용하여 의미 기반으로 후보를 재정렬하거나 추가 필터링을 수행합니다.
이 구조의 핵심은 역할 분담입니다. 희소 임베딩은 “정확히 무엇을 찾고 있는지”를 반영하고, 밀집 임베딩은 “비슷한 의미를 가진 다른 표현들”을 포착합니다.
5. Bi-Encoder와 Cross-Encoder
임베딩 기반 검색의 성능 한계를 이해하기 위해서는 Bi-Encoder와 Cross-Encoder의 차이를 이해하셔야 합니다.
Bi-Encoder 구조에서는 Query와 Document를 각각 독립적으로 임베딩한 뒤, 두 벡터 간의 유사도를 계산합니다. 이 방식은 매우 빠르고 확장성이 뛰어나 대규모 검색에 적합합니다. 하지만 두 텍스트를 함께 보지 않기 때문에 의미 비교가 근사적으로 이루어질 수밖에 없습니다.
Cross-Encoder는 Query와 Document를 하나의 입력으로 함께 모델에 넣어, 토큰 단위 상호작용을 통해 의미를 판단합니다. 정확도는 매우 높지만 계산 비용이 크기 때문에 대규모 검색에는 적합하지 않습니다.
실무에서는 Bi-Encoder로 후보를 빠르게 추린 뒤, Cross-Encoder로 재정렬하는 구조가 자주 사용됩니다.
6. Query Embedding과 Document Embedding의 차이
Query와 Document를 동일한 방식으로 임베딩하면 된다고 생각하기 쉽지만, 실제로는 두 텍스트의 성격이 상당히 다릅니다.
Query는 일반적으로 짧고, 사용자의 의도가 압축되어 있으며, 모호성을 포함하는 경우가 많습니다. 반면 Document는 길고 정보가 풍부하며, 하나의 벡터에 많은 내용을 담아야 합니다.
이 차이로 인해 최근에는 Query와 Document를 다르게 인식하도록 학습된 embedding 모델, 또는 instruction 기반 embedding 모델들이 등장하고 있습니다.
7. Chunking 전략과 임베딩 품질
임베딩을 실무에 적용할 때 가장 자주 실패하는 지점 중 하나가 Chunking 전략입니다.
문서 전체를 하나의 벡터로 임베딩하면, 벡터에 너무 많은 의미가 섞이게 되어 Query와의 대응력이 떨어집니다. 이를 해결하기 위해 문서를 적절한 크기의 chunk로 나누어 임베딩하는 방식이 사용됩니다.
Chunk size가 너무 작으면 문맥이 끊어지고, 너무 크면 의미가 희석됩니다. 또한 문단 경계를 고려하지 않거나 overlap을 주지 않으면 검색 품질이 급격히 저하될 수 있습니다.
Chunking은 단순한 전처리 단계가 아니라, 임베딩 품질을 결정하는 핵심 설계 요소입니다.
8. Similarity Metric의 중요성
임베딩은 벡터 자체만으로 완성되지 않습니다. 어떤 거리 함수를 사용하느냐에 따라 결과가 크게 달라질 수 있습니다.
코사인 유사도는 벡터의 방향성을 비교하는 방식으로, 크기 차이를 무시하고 의미적 유사성에 집중합니다. 내적(dot product)은 벡터의 크기와 방향을 모두 고려하며, 특정 모델에서는 이 방식이 더 적합할 수 있습니다. L2 distance는 유클리드 거리 기반 비교입니다.
중요한 점은 임베딩 모델이 학습될 때 사용된 metric과, 실제 검색에서 사용하는 metric을 일관되게 맞추는 것입니다.
9. 임베딩 관련 주요 라이브러리
전통적인 NLP 라이브러리로는 scikit-learn과 gensim이 있으며, TF-IDF나 Word2Vec과 같은 기법을 제공합니다.
현대적인 밀집 임베딩을 위해서는 sentence-transformers, Hugging Face Transformers, 그리고 OpenAI, Google Gemini, Cohere 등의 Embedding API가 널리 사용됩니다.
임베딩을 저장하고 검색하기 위한 벡터 데이터베이스로는 FAISS, Milvus, Pinecone, Weaviate, Qdrant 등이 있습니다.
10. 코드 예제
TF-IDF 기반 희소 임베딩 예제
from sklearn.feature_extraction.text import TfidfVectorizer
texts = [
"Text embedding is useful",
"Sentence embedding captures meaning",
"Embedding converts text to vectors"
]
vectorizer = TfidfVectorizer()
embeddings = vectorizer.fit_transform(texts)
print(embeddings.shape)
Sentence-BERT 기반 밀집 임베딩 예제
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
sentences = [
"Text embedding is useful",
"Sentence embedding captures meaning",
"Embedding converts text to vectors"
]
embeddings = model.encode(sentences)
print(embeddings.shape)
코사인 유사도 계산 예제
from sklearn.metrics.pairwise import cosine_similarity
similarity = cosine_similarity(
embeddings[0].reshape(1, -1),
embeddings[1].reshape(1, -1)
)
print(similarity)