🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

텍스트 임베딩 개념 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 17. · 6 Views

텍스트 임베딩 개념 완벽 가이드

텍스트를 벡터로 변환하는 임베딩 기술의 핵심 원리를 학습합니다. 벡터 공간에서 의미를 표현하고, 유사도를 계산하는 방법을 실무 예제와 함께 알아봅니다. 초급 개발자도 쉽게 이해할 수 있도록 비유와 스토리텔링으로 풀어냈습니다.


목차

  1. 임베딩이란_무엇인가
  2. 텍스트를_벡터로_변환
  3. 벡터_공간에서의_의미
  4. 코사인_유사도_이해
  5. 유사도_검색_원리
  6. 임베딩_모델_종류

1. 임베딩이란 무엇인가

어느 날 김개발 씨는 회사에서 검색 엔진을 개선하는 프로젝트에 투입되었습니다. 선배 개발자 박시니어 씨가 말합니다.

"이번에는 텍스트 임베딩을 사용해볼 거예요." 김개발 씨는 고개를 갸우뚱했습니다. 임베딩이란 대체 무엇일까요?

임베딩이란 텍스트나 이미지 같은 데이터를 숫자 벡터로 변환하는 기술입니다. 마치 도서관에서 책에 고유한 분류번호를 부여하는 것과 같습니다.

이렇게 변환하면 컴퓨터가 단어의 의미를 이해하고, 비교할 수 있게 됩니다. 임베딩을 제대로 이해하면 검색, 추천, 번역 등 다양한 AI 서비스의 핵심 원리를 알 수 있습니다.

다음 코드를 살펴봅시다.

from sentence_transformers import SentenceTransformer

# 임베딩 모델 불러오기
model = SentenceTransformer('all-MiniLM-L6-v2')

# 텍스트를 벡터로 변환
text = "안녕하세요, 반갑습니다"
embedding = model.encode(text)

# 결과 확인: 384차원의 벡터가 생성됩니다
print(f"벡터 차원: {len(embedding)}")
print(f"첫 5개 값: {embedding[:5]}")
# 출력: [0.123, -0.456, 0.789, -0.234, 0.567]

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 오늘 새로운 프로젝트에 투입되었습니다.

회사의 고객 문의 시스템을 개선하는 업무인데, 비슷한 질문을 자동으로 찾아주는 기능을 추가해야 한다고 합니다. 박시니어 씨가 김개발 씨의 자리로 다가와 말합니다.

"이번에는 텍스트 임베딩을 활용해 볼 거예요. 아, 임베딩이 뭔지 모르시나요?

괜찮습니다. 제가 차근차근 설명해 드릴게요." 임베딩이란 정확히 무엇일까요?

쉽게 비유하자면, 임베딩은 마치 도서관의 책 분류 시스템과 같습니다. 도서관에서는 모든 책에 고유한 분류번호를 부여합니다.

예를 들어 소설은 800번대, 과학은 500번대처럼 말이죠. 비슷한 주제의 책들은 비슷한 번호를 받게 됩니다.

이처럼 임베딩도 단어나 문장을 숫자의 나열로 변환하여, 컴퓨터가 이해할 수 있는 형태로 만들어줍니다. 왜 이런 변환이 필요할까요?

컴퓨터는 본래 숫자만 이해할 수 있습니다. "사과"라는 단어를 보여줘도, 컴퓨터는 그것이 과일인지, 빨간색인지, 달콤한지 전혀 알 수 없습니다.

그저 "사과"라는 문자의 조합일 뿐입니다. 이런 한계 때문에 과거에는 단순히 문자열을 비교하는 방식을 사용했습니다.

"사과"와 "사과"는 같지만, "사과"와 "apple"은 완전히 다른 것으로 인식했습니다. 더 큰 문제는 "사과"와 "배"가 둘 다 과일이라는 공통점을 컴퓨터가 전혀 파악할 수 없다는 점이었습니다.

바로 이런 문제를 해결하기 위해 임베딩 기술이 등장했습니다. 임베딩을 사용하면 단어의 의미를 숫자로 표현할 수 있습니다.

"사과"와 "배"는 둘 다 과일이므로 비슷한 숫자 패턴을 가지게 됩니다. 반대로 "사과"와 "자동차"는 전혀 다른 의미이므로 서로 다른 숫자 패턴을 가집니다.

이렇게 되면 컴퓨터도 단어 간의 유사성을 계산할 수 있게 됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 SentenceTransformer 라이브러리를 불러옵니다. 이것은 구글과 같은 기업에서 미리 학습시킨 임베딩 모델을 쉽게 사용할 수 있게 해주는 도구입니다.

'all-MiniLM-L6-v2'라는 모델은 가볍고 빠르면서도 성능이 좋아 실무에서 자주 사용됩니다. 다음으로 encode 함수를 호출하여 텍스트를 벡터로 변환합니다.

"안녕하세요, 반갑습니다"라는 문장이 순식간에 384개의 숫자로 이루어진 벡터로 바뀝니다. 이 숫자들이 바로 문장의 의미를 담고 있는 암호와 같습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰에서 상품 추천 시스템을 만든다고 가정해봅시다.

고객이 "편안한 운동화"를 검색하면, 이 문장을 임베딩으로 변환합니다. 그리고 데이터베이스에 저장된 모든 상품 설명도 임베딩으로 변환해 둡니다.

이제 두 벡터를 비교하여 가장 비슷한 상품을 찾아낼 수 있습니다. 심지어 정확히 "편안한 운동화"라는 단어가 없는 상품 설명이라도, 의미가 비슷하면 찾아낼 수 있습니다.

Netflix나 YouTube 같은 서비스도 이런 원리를 사용합니다. 사용자가 본 영상이나 좋아한 콘텐츠를 임베딩으로 변환하여, 비슷한 취향의 다른 콘텐츠를 추천해주는 것입니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 텍스트에 동일한 임베딩 모델을 사용하는 것입니다.

임베딩 모델은 학습된 데이터에 따라 성능이 크게 달라집니다. 영어로 학습된 모델을 한국어에 사용하면 좋은 결과를 얻기 어렵습니다.

따라서 언어와 도메인에 맞는 적절한 모델을 선택해야 합니다. 또한 임베딩은 많은 메모리와 계산 자원을 필요로 합니다.

수백만 개의 문장을 실시간으로 임베딩하려면 서버 비용이 크게 증가할 수 있습니다. 따라서 미리 임베딩을 계산해서 데이터베이스에 저장해두는 방식을 주로 사용합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다.

"아, 그래서 비슷한 문의를 자동으로 찾을 수 있는 거군요!" 임베딩을 제대로 이해하면 검색, 추천, 번역, 챗봇 등 다양한 AI 서비스의 핵심 원리를 알 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 한국어 텍스트는 'KoSimCSE'나 'KoSBERT' 같은 한국어 특화 모델을 사용하세요

  • 임베딩은 한 번만 계산하고 데이터베이스에 저장하여 재사용하세요
  • 긴 문장보다는 짧은 문장이 더 정확한 임베딩을 생성합니다

2. 텍스트를 벡터로 변환

김개발 씨가 임베딩의 개념은 이해했지만, 구체적으로 어떻게 텍스트가 숫자로 바뀌는지 궁금해졌습니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명을 시작합니다.

"자, 이제 실제로 어떻게 변환되는지 보여드릴게요."

텍스트를 벡터로 변환하는 과정은 크게 토큰화벡터 매핑 두 단계로 이루어집니다. 마치 문장을 단어로 쪼개고, 각 단어에 고유한 좌표를 부여하는 것과 같습니다.

이 과정을 거치면 "사과"라는 단어가 [0.8, 0.3, -0.5]와 같은 숫자 배열로 표현됩니다. 이렇게 변환된 벡터는 고차원 공간에서 단어의 위치를 나타내며, 의미상 비슷한 단어들은 가까운 위치에 배치됩니다.

다음 코드를 살펴봅시다.

from transformers import AutoTokenizer, AutoModel
import torch

# 토크나이저와 모델 불러오기
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
model = AutoModel.from_pretrained('bert-base-uncased')

# 텍스트를 토큰으로 변환
text = "Natural language processing"
tokens = tokenizer(text, return_tensors='pt')

# 토큰을 벡터로 변환
with torch.no_grad():
    outputs = model(**tokens)
    # 마지막 은닉층의 평균을 임베딩으로 사용
    embedding = outputs.last_hidden_state.mean(dim=1)

print(f"벡터 형태: {embedding.shape}")  # [1, 768]

김개발 씨는 화이트보드 앞에 섰습니다. 박시니어 씨가 그린 도표를 보니 복잡해 보이지만, 차근차근 따라가면 이해할 수 있을 것 같습니다.

"텍스트를 벡터로 바꾸는 건 마법이 아니에요. 정해진 절차가 있습니다." 박시니어 씨가 설명을 시작합니다.

텍스트를 벡터로 변환하는 과정은 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 주소를 GPS 좌표로 바꾸는 것과 같습니다.

"서울시 강남구 테헤란로"라는 주소를 위도 37.5, 경도 127.0 같은 숫자로 변환하는 것이죠. 주소는 사람이 이해하기 쉽지만, GPS는 숫자 좌표로만 작동합니다.

마찬가지로 텍스트는 사람에게 친숙하지만, 컴퓨터는 벡터로 변환해야 처리할 수 있습니다. 왜 이런 변환 과정이 필요할까요?

과거에는 원-핫 인코딩이라는 단순한 방법을 사용했습니다. 사전에 있는 모든 단어를 나열하고, 해당 단어의 위치에만 1을 표시하는 방식이었습니다.

예를 들어 사전에 10,000개의 단어가 있다면, "사과"는 [0, 0, 1, 0, 0, ...] 처럼 10,000개의 숫자로 표현되었습니다. 하지만 이 방법에는 큰 문제가 있었습니다.

첫째, 벡터의 크기가 너무 컸습니다. 사전이 커질수록 벡터도 무한정 커졌습니다.

둘째, 단어 간의 관계를 전혀 표현할 수 없었습니다. "사과"와 "배"가 둘 다 과일이라는 정보가 벡터에 담기지 않았습니다.

바로 이런 문제를 해결하기 위해 밀집 벡터 표현이 등장했습니다. 밀집 벡터는 원-핫 인코딩과 달리 훨씬 작은 차원으로 단어를 표현합니다.

보통 128차원에서 768차원 정도를 사용합니다. 10,000차원이 아니라 말이죠.

더 중요한 것은 이 벡터의 각 차원이 의미를 담고 있다는 점입니다. 예를 들어 첫 번째 차원은 "과일인지 아닌지"를, 두 번째 차원은 "색깔이 밝은지 어두운지"를 나타낼 수 있습니다.

물론 실제로는 이렇게 명확하지 않고, 수많은 의미가 복잡하게 얽혀 있습니다. 하지만 중요한 것은 비슷한 의미의 단어들이 비슷한 벡터 값을 가진다는 점입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 transformers 라이브러리에서 토크나이저와 모델을 불러옵니다.

BERT는 구글이 개발한 유명한 언어 모델로, 수십억 개의 문장으로 학습되었습니다. 이 모델은 단어의 의미를 매우 잘 이해합니다.

다음으로 tokenizer 함수를 호출하여 텍스트를 토큰으로 변환합니다. "Natural language processing"이라는 문장이 ["natural", "language", "processing"] 같은 단어 조각으로 쪼개지고, 각각에 고유한 숫자 ID가 부여됩니다.

그런 다음 model에 토큰을 입력하면, 신경망을 통과하여 벡터로 변환됩니다. last_hidden_state는 모델의 마지막 층에서 나온 결과로, 각 토큰마다 768차원의 벡터가 생성됩니다.

우리는 이들의 평균을 계산하여 문장 전체를 대표하는 하나의 벡터로 만듭니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 뉴스 기사 분류 시스템을 만든다고 가정해봅시다. 수천 개의 뉴스 기사를 정치, 경제, 스포츠, 연예 등으로 자동 분류해야 합니다.

먼저 각 기사를 벡터로 변환합니다. 그러면 비슷한 주제의 기사들은 벡터 공간에서 가까운 위치에 모이게 됩니다.

정치 뉴스는 한 영역에, 스포츠 뉴스는 다른 영역에 군집을 이룹니다. 이제 새로운 기사가 들어오면, 그 기사의 벡터를 계산하고 어느 군집에 가까운지 판단하여 자동으로 분류할 수 있습니다.

실제로 네이버 뉴스나 구글 뉴스에서 이런 기술을 사용하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 토큰화를 무시하는 것입니다. 단순히 문자열을 그대로 모델에 넣으면 작동하지 않습니다.

반드시 해당 모델의 토크나이저를 사용하여 전처리를 해야 합니다. 각 모델마다 학습할 때 사용한 토크나이저가 다르기 때문입니다.

또한 문맥을 고려해야 합니다. "사과"라는 단어는 과일을 의미할 수도 있고, 잘못을 인정하는 행위를 의미할 수도 있습니다.

BERT 같은 모델은 주변 단어를 보고 문맥에 맞는 벡터를 생성합니다. 따라서 단어 하나만 떼어내서 임베딩하는 것보다 문장 전체를 임베딩하는 것이 더 정확합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 토크나이저와 모델을 함께 사용하는 거군요!" 텍스트를 벡터로 제대로 변환하면 검색의 정확도가 크게 향상되고, 추천 시스템도 훨씬 똑똑해집니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 모델과 토크나이저는 항상 같은 버전을 사용하세요 (예: bert-base-uncased)

  • 긴 문장은 512토큰 이내로 잘라서 처리하세요 (대부분 모델의 최대 길이)
  • GPU가 있다면 model.to('cuda')로 속도를 크게 높일 수 있습니다

3. 벡터 공간에서의 의미

김개발 씨가 벡터가 무엇인지는 알았지만, 이 숫자들이 정확히 어떤 의미를 담고 있는지 궁금했습니다. 박시니어 씨가 3차원 그래프를 그리며 말합니다.

"벡터 공간에서는 위치가 곧 의미입니다. 가까이 있으면 비슷한 뜻이고, 멀리 있으면 다른 뜻이죠."

벡터 공간은 각 단어나 문장이 하나의 점으로 표현되는 고차원 공간입니다. 마치 지도에서 건물의 위치를 좌표로 나타내듯, 단어의 의미를 좌표로 표현합니다.

이 공간에서 "강아지"와 "고양이"는 가까이 위치하고, "강아지"와 "자동차"는 멀리 위치합니다. 더 흥미로운 것은 벡터 연산으로 의미의 관계를 표현할 수 있다는 점입니다.

"왕 - 남자 + 여자 = 여왕"처럼 말이죠.

다음 코드를 살펴봅시다.

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# 예시 벡터들 (실제로는 모델에서 생성)
king = np.array([0.5, 0.8, 0.1])
queen = np.array([0.5, 0.9, -0.1])
man = np.array([0.3, 0.2, 0.5])
woman = np.array([0.3, 0.3, -0.5])

# 벡터 연산: king - man + woman
result = king - man + woman
print(f"결과 벡터: {result}")

# queen과의 유사도 계산
similarity = cosine_similarity([result], [queen])[0][0]
print(f"여왕과의 유사도: {similarity:.3f}")

김개발 씨는 화이트보드에 그려진 3차원 그래프를 바라봅니다. x, y, z 축이 있고, 그 안에 여러 점들이 찍혀 있습니다.

박시니어 씨가 설명을 이어갑니다. "우리가 사는 세상은 3차원이죠.

하지만 벡터 공간은 보통 수백 차원입니다. 상상하기 어렵지만, 수학적으로는 동일하게 작동합니다." 벡터 공간이란 정확히 무엇일까요?

쉽게 비유하자면, 벡터 공간은 마치 거대한 도시의 지도와 같습니다. 도시에는 수많은 건물이 있고, 각 건물은 고유한 주소와 위치를 가집니다.

비슷한 기능을 가진 건물들은 모여 있습니다. 은행들은 금융가에, 음식점들은 맛집 거리에 모이는 것처럼 말이죠.

벡터 공간도 마찬가지입니다. 비슷한 의미의 단어들은 공간 안에서 가까운 위치에 모입니다.

왜 이런 공간 표현이 중요할까요? 과거의 단순한 검색 시스템은 정확히 일치하는 단어만 찾을 수 있었습니다.

"강아지"를 검색하면 "강아지"가 포함된 문서만 찾았습니다. "개"나 "애완동물"이 포함된 문서는 찾지 못했습니다.

사용자가 원하는 정보가 있는데도 말이죠. 더 큰 문제는 동의어나 유사 개념을 처리할 수 없다는 점이었습니다.

사용자는 "저렴한 노트북"을 검색하는데, 실제 상품 설명에는 "가성비 좋은 컴퓨터"라고 적혀 있다면 어떻게 될까요? 전통적인 검색 시스템은 이 둘을 연결하지 못했습니다.

바로 이런 문제를 해결하기 위해 벡터 공간 모델이 등장했습니다. 벡터 공간에서는 "강아지", "개", "애완동물"이 모두 가까운 위치에 배치됩니다.

따라서 "강아지"를 검색하면, 벡터 공간에서 가까운 위치에 있는 "개"나 "애완동물"이 포함된 문서도 함께 찾을 수 있습니다. 사용자 입장에서는 훨씬 더 만족스러운 검색 결과를 얻게 됩니다.

더 놀라운 것은 벡터 연산이 가능하다는 점입니다. 고등학교 수학 시간에 배웠던 벡터 덧셈과 뺄셈을 기억하시나요?

같은 원리를 적용할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 네 개의 단어를 벡터로 표현했습니다. 실제로는 임베딩 모델에서 생성하지만, 여기서는 이해를 돕기 위해 간단한 3차원 벡터를 사용했습니다.

king과 queen은 비슷한 값을 가지지만, 한 차원(성별을 나타내는)에서만 차이가 납니다. 다음으로 벡터 연산을 수행합니다.

"king - man + woman"은 "왕이라는 개념에서 남성성을 빼고 여성성을 더한다"는 의미입니다. 수학적으로 계산하면 놀랍게도 queen 벡터와 매우 비슷한 결과가 나옵니다.

마지막으로 코사인 유사도를 계산하여 얼마나 가까운지 확인합니다. 유사도가 0.9 이상이면 매우 비슷한 의미라고 볼 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 번역 시스템을 만든다고 가정해봅시다.

영어 단어들을 벡터 공간 A에 배치하고, 한국어 단어들을 벡터 공간 B에 배치합니다. 그리고 두 공간을 매핑하는 함수를 학습합니다.

이제 영어 단어 "dog"의 벡터를 공간 B로 옮기면, 가장 가까운 한국어 벡터인 "개"를 찾을 수 있습니다. 구글 번역이나 파파고 같은 서비스가 이런 원리로 작동합니다.

단순히 사전에 없는 단어도 벡터 공간에서 가장 가까운 단어를 찾아 번역할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 저차원으로 시각화하려는 시도입니다. 768차원의 벡터를 2차원이나 3차원으로 줄여서 그래프로 그리면, 많은 정보가 손실됩니다.

시각화는 대략적인 이해를 돕지만, 실제 분석에는 원래의 고차원 벡터를 사용해야 합니다. 또한 편향의 문제도 있습니다.

만약 학습 데이터에 편견이 있다면, 벡터 공간에도 그 편견이 반영됩니다. 예를 들어 "의사"와 "남자"가 가까이 있고, "간호사"와 "여자"가 가까이 있다면, 이것은 사회적 편견을 그대로 학습한 것입니다.

실무에서는 이런 편향을 완화하는 기술도 함께 적용해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 감탄했습니다. "와, 벡터로 의미의 관계까지 표현할 수 있다니 신기하네요!" 벡터 공간을 제대로 이해하면 검색, 번역, 추천 시스템의 원리가 명확해집니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 벡터 차원이 클수록 더 정교한 의미를 표현할 수 있지만, 메모리와 계산 비용도 증가합니다

  • 벡터 연산은 단어 간의 관계를 발견하는 데 유용합니다 (예: 수도 관계, 성별 관계)
  • t-SNE나 UMAP 같은 기법으로 고차원 벡터를 2D로 시각화할 수 있습니다

4. 코사인 유사도 이해

김개발 씨가 벡터 공간까지는 이해했는데, 두 벡터가 얼마나 비슷한지 어떻게 계산하는지 궁금해졌습니다. 박시니어 씨가 웃으며 말합니다.

"좋은 질문이에요. 거리를 재는 방법도 여러 가지가 있는데, 가장 많이 쓰이는 게 코사인 유사도입니다."

코사인 유사도는 두 벡터 사이의 각도를 이용하여 유사도를 측정하는 방법입니다. 마치 두 사람이 바라보는 방향이 얼마나 비슷한지 확인하는 것과 같습니다.

값은 -1에서 1 사이이며, 1에 가까울수록 매우 비슷하고, 0에 가까우면 관련이 없으며, -1에 가까우면 반대 의미입니다. 벡터의 크기가 아닌 방향만 고려하기 때문에, 문장의 길이와 무관하게 의미의 유사성을 비교할 수 있습니다.

다음 코드를 살펴봅시다.

import numpy as np
from numpy.linalg import norm

def cosine_similarity(vec1, vec2):
    # 내적 계산
    dot_product = np.dot(vec1, vec2)
    # 각 벡터의 크기 계산
    norm1 = norm(vec1)
    norm2 = norm(vec2)
    # 코사인 유사도 = 내적 / (크기1 * 크기2)
    return dot_product / (norm1 * norm2)

# 예시 벡터
dog = np.array([1.0, 2.0, 1.5])
cat = np.array([1.1, 2.1, 1.4])
car = np.array([-0.5, 0.1, 3.0])

# 유사도 계산
print(f"dog-cat 유사도: {cosine_similarity(dog, cat):.3f}")
print(f"dog-car 유사도: {cosine_similarity(dog, car):.3f}")

김개발 씨는 노트를 펼쳐 메모를 시작했습니다. 벡터 공간에 점들이 찍혀 있다는 건 알겠는데, 두 점이 얼마나 가까운지 어떻게 숫자로 표현할까요?

박시니어 씨가 삼각형을 그리며 설명합니다. "중학교 때 배웠던 코사인 함수 기억나세요?

바로 그걸 사용합니다." 코사인 유사도란 정확히 무엇일까요? 쉽게 비유하자면, 코사인 유사도는 마치 두 손전등이 비추는 방향을 비교하는 것과 같습니다.

두 손전등이 같은 방향을 가리키면 완전히 비슷한 것이고, 서로 수직으로 비추면 전혀 관련이 없는 것입니다. 반대 방향을 가리키면 정반대의 의미가 됩니다.

중요한 것은 손전등의 밝기(벡터의 크기)가 아니라 방향이라는 점입니다. 왜 거리가 아닌 각도를 사용할까요?

처음에는 유클리드 거리를 사용하려는 시도가 있었습니다. 두 점 사이의 직선 거리를 재는 방법이죠.

"강아지"와 "고양이" 벡터 사이의 거리를 계산하는 식입니다. 하지만 이 방법에는 큰 문제가 있었습니다.

문장의 길이가 다르면 벡터의 크기도 달라집니다. "강아지"라는 한 단어와 "귀여운 강아지가 공원에서 뛰어놀고 있다"라는 긴 문장을 비교한다고 해봅시다.

둘 다 강아지에 관한 내용이지만, 벡터의 크기가 크게 차이 나면 거리도 멀어집니다. 의미는 비슷한데 거리가 멀다고 판단하는 모순이 생기는 것이죠.

바로 이런 문제를 해결하기 위해 코사인 유사도가 등장했습니다. 코사인 유사도는 벡터의 크기를 정규화하여 방향만 비교합니다.

따라서 문장이 길든 짧든, 의미가 비슷하면 높은 유사도 값을 얻습니다. 이것은 실제 검색 시스템에서 매우 중요한 특성입니다.

수학적으로는 어떻게 계산할까요? 코사인 유사도는 두 벡터의 내적을 각 벡터의 크기로 나눈 값입니다.

내적은 두 벡터의 대응하는 원소를 곱해서 모두 더한 것입니다. 예를 들어 [1, 2, 3]과 [4, 5, 6]의 내적은 1×4 + 2×5 + 3×6 = 32입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 cosine_similarity 함수를 정의합니다.

이 함수는 두 벡터를 입력받아 유사도를 계산합니다. np.dot으로 내적을 계산하고, norm으로 각 벡터의 크기를 계산합니다.

벡터의 크기는 각 원소를 제곱해서 더한 뒤 제곱근을 취한 값입니다. 다음으로 세 개의 예시 벡터를 만듭니다.

dog과 cat은 거의 비슷한 값을 가지고, car는 완전히 다른 값을 가집니다. 유사도를 계산해보면 dog과 cat은 0.99처럼 매우 높은 값이 나오고, dog과 car는 0.3처럼 낮은 값이 나옵니다.

결과를 해석해봅시다. 0.99는 거의 동일한 의미라는 뜻입니다.

0.3은 약간의 관련은 있지만 주제가 다르다는 의미입니다. 보통 실무에서는 0.7 이상이면 유사하다고 판단하고, 0.5 이하면 관련이 없다고 판단합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 지원 시스템을 만든다고 가정해봅시다.

고객이 "배송이 너무 느려요"라고 문의하면, 시스템은 이 문장을 벡터로 변환합니다. 그리고 FAQ 데이터베이스에 저장된 모든 질문 벡터와 코사인 유사도를 계산합니다.

"주문한 상품이 언제 도착하나요?"라는 질문과 높은 유사도가 나오면, 해당 답변을 자동으로 제시할 수 있습니다. 정확히 같은 표현이 아니어도 의미가 비슷하면 찾아낼 수 있는 것이죠.

실제로 많은 챗봇과 FAQ 시스템이 이 방식을 사용합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 음수 값을 무시하는 것입니다. 코사인 유사도는 -1에서 1 사이의 값을 가집니다.

-1은 완전히 반대 의미를 나타냅니다. 예를 들어 "좋아요"와 "싫어요"는 음수 유사도를 가질 수 있습니다.

절대값이 크다고 해서 무조건 유사한 것은 아닙니다. 또한 임계값 설정도 중요합니다.

유사도가 얼마 이상일 때 같은 의미로 볼 것인가는 도메인마다 다릅니다. 의료 정보처럼 정확성이 중요한 분야는 0.9 이상으로 높게 설정하고, 일반 추천 시스템은 0.6 정도로 낮게 설정할 수 있습니다.

실험을 통해 최적의 임계값을 찾아야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 문장 길이와 상관없이 비교할 수 있는 거군요!" 코사인 유사도를 제대로 이해하면 검색 시스템과 추천 알고리즘의 핵심을 파악할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 코사인 유사도는 항상 -1과 1 사이의 값을 반환합니다

  • 대량의 벡터를 비교할 때는 numpy의 벡터화 연산을 사용하면 훨씬 빠릅니다
  • scikit-learn의 cosine_similarity 함수는 여러 벡터를 한 번에 비교할 수 있습니다

5. 유사도 검색 원리

김개발 씨가 이제 유사도를 계산할 수 있게 되었습니다. 그런데 실제 서비스에는 수백만 개의 문서가 있는데, 전부 비교하면 너무 오래 걸리지 않을까요?

박시니어 씨가 미소 지으며 말합니다. "좋은 지적이에요.

그래서 특별한 기술이 필요합니다."

유사도 검색은 수백만 개의 벡터 중에서 쿼리와 가장 비슷한 벡터를 빠르게 찾는 기술입니다. 마치 도서관에서 책을 찾을 때 모든 책을 다 뒤지는 것이 아니라, 분류 체계를 활용하는 것과 같습니다.

근사 최근접 이웃(ANN) 알고리즘을 사용하면 정확도를 약간 희생하는 대신 검색 속도를 수천 배 빠르게 만들 수 있습니다. FAISS, Annoy, HNSW 같은 라이브러리가 이를 구현합니다.

다음 코드를 살펴봅시다.

import faiss
import numpy as np

# 100만 개의 512차원 벡터 (실제 서비스 규모)
dimension = 512
num_vectors = 1000000

# 데이터베이스 벡터 생성 (실제로는 임베딩 모델 출력)
database = np.random.random((num_vectors, dimension)).astype('float32')

# FAISS 인덱스 생성 (내적 기반)
index = faiss.IndexFlatIP(dimension)
index.add(database)

# 검색 쿼리
query = np.random.random((1, dimension)).astype('float32')

# 상위 5개 유사 벡터 검색
k = 5
distances, indices = index.search(query, k)
print(f"상위 {k}개 인덱스: {indices[0]}")

김개발 씨는 걱정스러운 표정을 지었습니다. "선배님, 우리 서비스에는 상품 설명이 백만 개나 있는데, 사용자가 검색할 때마다 전부 비교하면 엄청 느리지 않을까요?" 박시니어 씨가 고개를 끄덕입니다.

"정말 좋은 질문이에요. 그게 바로 실무에서 가장 중요한 문제입니다." 유사도 검색의 핵심 문제는 무엇일까요?

쉽게 비유하자면, 이것은 마치 거대한 도서관에서 책 한 권을 찾는 것과 같습니다. 만약 책장을 하나하나 다 뒤진다면, 수십만 권의 책을 검색하는 데 몇 시간이 걸릴 것입니다.

하지만 도서관은 분류 체계가 있습니다. 소설은 800번대, 과학은 500번대처럼 구역이 나뉘어 있어서, 원하는 구역만 찾으면 됩니다.

벡터 검색도 마찬가지입니다. 수백만 개의 벡터와 하나하나 유사도를 계산하면 너무 오래 걸립니다.

따라서 영리한 방법이 필요합니다. 과거에는 어떤 문제가 있었을까요?

초기의 벡터 검색은 선형 탐색이라는 단순한 방법을 사용했습니다. 모든 벡터와 일일이 비교하는 것이죠.

벡터가 1,000개라면 1,000번의 유사도 계산이 필요합니다. 10,000개라면 10,000번입니다.

문제는 현대 서비스는 수백만, 수억 개의 문서를 다룬다는 점입니다. 예를 들어 100만 개의 벡터가 있고, 각 유사도 계산에 1밀리초가 걸린다면, 검색 한 번에 1,000초가 걸립니다.

거의 17분입니다. 사용자는 절대 그렇게 오래 기다려주지 않습니다.

서비스가 성립할 수 없는 것이죠. 바로 이런 문제를 해결하기 위해 근사 최근접 이웃(ANN) 기술이 등장했습니다.

ANN은 100% 정확한 결과 대신 99% 정확한 결과를 훨씬 빠르게 찾는 방법입니다. 생각해보면 검색 결과가 1등과 2등이 바뀐다고 해도 사용자는 거의 느끼지 못합니다.

둘 다 충분히 관련 있는 결과이기 때문입니다. 이런 작은 오차를 허용하면 속도를 수천 배 높일 수 있습니다.

대표적인 ANN 알고리즘에는 여러 가지가 있습니다. FAISS는 페이스북(현 Meta)에서 개발한 라이브러리로, GPU 가속을 지원하여 매우 빠릅니다.

Annoy는 Spotify에서 만든 것으로, 메모리 효율이 좋습니다. HNSW는 그래프 기반 알고리즘으로, 정확도와 속도의 균형이 뛰어납니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 100만 개의 512차원 벡터를 생성합니다.

실제 서비스에서는 임베딩 모델의 출력이 이 역할을 합니다. 여기서는 시연을 위해 랜덤 벡터를 사용했습니다.

다음으로 FAISS 인덱스를 생성합니다. IndexFlatIP는 내적(Inner Product)을 사용하는 가장 기본적인 인덱스입니다.

코사인 유사도와 거의 같은 결과를 줍니다. add 메서드로 모든 벡터를 인덱스에 추가합니다.

검색 쿼리 벡터를 만들고, search 메서드를 호출합니다. k=5는 상위 5개의 유사 벡터를 찾겠다는 의미입니다.

결과는 distances(유사도 점수)와 indices(벡터의 인덱스 번호)로 반환됩니다. 놀라운 점은 이 모든 과정이 0.1초 이내에 완료된다는 것입니다.

100만 개와 비교하는데 말이죠. 선형 탐색이었다면 몇 분이 걸렸을 것입니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 동영상 추천 시스템을 만든다고 가정해봅시다.

YouTube에는 수억 개의 동영상이 있습니다. 사용자가 어떤 동영상을 시청하면, 그 동영상의 임베딩 벡터를 쿼리로 사용하여 유사한 동영상을 찾습니다.

FAISS 같은 라이브러리를 사용하면 0.1초 만에 수억 개 중에서 상위 10개를 찾을 수 있습니다. 사용자는 동영상이 끝나자마자 바로 추천 목록을 볼 수 있습니다.

이것이 바로 실시간 추천이 가능한 이유입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 인덱스 갱신을 무시하는 것입니다. 새로운 상품이나 콘텐츠가 추가되면 인덱스도 업데이트해야 합니다.

하지만 인덱스를 다시 빌드하는 데는 시간이 걸립니다. 따라서 일정 주기마다 배치로 업데이트하거나, 온라인 갱신을 지원하는 인덱스를 사용해야 합니다.

또한 메모리 관리도 중요합니다. 100만 개의 512차원 벡터는 약 2GB의 메모리를 차지합니다.

수억 개가 되면 수백 GB가 필요합니다. 따라서 압축 기법을 사용하거나, 양자화하여 메모리를 줄이는 기술도 함께 적용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다.

"와, 그래서 실시간 검색이 가능한 거군요!" 유사도 검색 기술을 제대로 이해하면 대규모 서비스의 검색과 추천 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - FAISS는 CPU와 GPU 버전이 있으며, GPU는 수십 배 더 빠릅니다

  • 인덱스 타입에 따라 속도와 정확도가 달라지므로, 벤치마크를 통해 최적의 타입을 선택하세요
  • Pinecone, Weaviate 같은 벡터 데이터베이스는 인덱스 관리를 자동화해줍니다

6. 임베딩 모델 종류

김개발 씨가 임베딩의 원리는 이해했지만, 실제로 어떤 모델을 사용해야 할지 고민되었습니다. 박시니어 씨가 여러 모델의 특징을 설명하기 시작합니다.

"프로젝트 성격에 따라 적합한 모델이 다릅니다. 같이 살펴볼까요?"

임베딩 모델에는 Word2Vec, GloVe, BERT, Sentence-BERT 등 다양한 종류가 있습니다. 마치 자동차에 경차, 세단, SUV가 있듯이, 각 모델은 서로 다른 장단점을 가집니다.

Word2Vec은 가볍고 빠르지만 문맥을 고려하지 못하고, BERT는 문맥을 잘 이해하지만 느립니다. 최근에는 OpenAI Embeddings, Cohere 같은 API 기반 모델도 인기입니다.

프로젝트의 요구사항에 맞는 모델을 선택하는 것이 핵심입니다.

다음 코드를 살펴봅시다.

from sentence_transformers import SentenceTransformer

# 여러 임베딩 모델 비교
models = {
    'MiniLM': 'all-MiniLM-L6-v2',  # 빠르고 가벼움
    'MPNet': 'all-mpnet-base-v2',  # 높은 정확도
    'Multilingual': 'paraphrase-multilingual-MiniLM-L12-v2'  # 다국어
}

# 테스트 문장
text = "Machine learning is fascinating"

for name, model_name in models.items():
    model = SentenceTransformer(model_name)
    embedding = model.encode(text)
    print(f"{name}: {embedding.shape}, 처음 3개 값: {embedding[:3]}")

# 출력 예시:
# MiniLM: (384,), 처음 3개 값: [0.123, -0.456, 0.789]
# MPNet: (768,), 처음 3개 값: [0.234, -0.567, 0.891]

김개발 씨는 검색 결과를 보며 혼란스러웠습니다. "임베딩 모델"을 검색하니 수십 가지가 나옵니다.

Word2Vec, GloVe, BERT, GPT, 심지어 OpenAI Embeddings API까지. 어떤 것을 사용해야 할까요?

박시니어 씨가 웃으며 말합니다. "처음엔 다들 헷갈려 해요.

하지만 각각의 특징을 알면 선택이 쉬워집니다." 임베딩 모델의 종류는 어떻게 분류할까요? 쉽게 비유하자면, 임베딩 모델은 마치 교통수단과 같습니다.

가까운 거리는 자전거로, 중거리는 자동차로, 장거리는 비행기로 이동하는 것처럼, 작업의 성격에 따라 적합한 모델이 다릅니다. 속도가 중요하면 가벼운 모델을, 정확도가 중요하면 큰 모델을 선택합니다.

초기 모델들은 어땠을까요? Word2Vec은 2013년에 구글이 발표한 획기적인 모델이었습니다.

단어를 벡터로 변환하는 것이 가능하다는 것을 처음 보여줬습니다. 가볍고 빠르지만, 큰 단점이 있었습니다.

"사과"라는 단어는 항상 같은 벡터를 가집니다. 문맥과 무관하게 말이죠.

"사과를 먹었다"와 "진심으로 사과한다"에서 "사과"가 다른 의미인데도 같은 벡터로 표현되었습니다. GloVe는 스탠포드 대학에서 개발한 모델로, Word2Vec의 개선판이었습니다.

전체 문서의 통계 정보를 활용하여 조금 더 정확한 벡터를 만들었습니다. 하지만 여전히 문맥을 고려하지 못하는 한계가 있었습니다.

바로 이런 문제를 해결하기 위해 문맥 기반 모델이 등장했습니다. BERT는 2018년 구글이 발표한 혁명적인 모델입니다.

Transformer라는 새로운 아키텍처를 사용하여, 같은 단어라도 문맥에 따라 다른 벡터를 생성합니다. "사과를 먹었다"의 사과는 과일 벡터를, "진심으로 사과한다"의 사과는 행위 벡터를 가집니다.

하지만 BERT에도 문제가 있었습니다. 문장 전체를 하나의 벡터로 만드는 것이 어려웠습니다.

BERT는 원래 단어 하나하나를 임베딩하도록 설계되었기 때문입니다. Sentence-BERT는 이 문제를 해결했습니다.

BERT의 구조를 개선하여, 문장 전체를 의미 있는 하나의 벡터로 변환할 수 있게 만들었습니다. 검색 시스템에 딱 맞는 모델이 된 것이죠.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 세 가지 인기 있는 Sentence-BERT 모델을 정의합니다.

MiniLM은 가볍고 빠른 모델로, 실시간 서비스에 적합합니다. 384차원의 벡터를 생성합니다.

MPNet은 더 크고 정확한 모델로, 768차원의 벡터를 생성합니다. 배치 처리나 오프라인 작업에 적합합니다.

Multilingual 모델은 100개 이상의 언어를 지원합니다. 한국어, 영어, 중국어 등을 하나의 벡터 공간에 매핑할 수 있습니다.

다국어 서비스에 필수적입니다. 각 모델로 같은 문장을 임베딩하면, 벡터의 차원과 값이 다르다는 것을 알 수 있습니다.

모델이 다르면 학습 방법과 데이터가 다르기 때문입니다. 중요한 것은 같은 모델로 일관되게 사용해야 한다는 점입니다.

최근에는 API 기반 모델도 인기입니다. OpenAI Embeddings API는 text-embedding-3-small, text-embedding-3-large 같은 모델을 제공합니다.

자체 서버에서 모델을 실행할 필요 없이, API 호출만으로 임베딩을 얻을 수 있습니다. 초기 개발이 매우 빠르고, 성능도 뛰어납니다.

하지만 비용이 발생하고, 네트워크 의존성이 있습니다. Cohere Embed는 또 다른 인기 API로, 다국어 지원이 강력합니다.

Voyage AI는 특정 도메인에 특화된 모델을 제공합니다. 의료, 법률, 금융 같은 전문 분야에서 일반 모델보다 훨씬 좋은 성능을 보입니다.

실제 현업에서는 어떻게 선택할까요? 스타트업처럼 빠른 개발이 필요하면 OpenAI API를 사용합니다.

프로토타입을 며칠 만에 만들 수 있습니다. 서비스가 커지면 비용이 부담되므로, 오픈소스 모델로 전환합니다.

Sentence-BERT의 MiniLM 모델을 자체 서버에서 실행하면 API 비용을 절약할 수 있습니다. 한국어 서비스라면 한국어 특화 모델을 고려해야 합니다.

KoSimCSEKoSBERT는 한국어로 학습된 모델로, 영어 모델보다 훨씬 정확합니다. "배"가 과일인지 배(선박)인지를 제대로 구분할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 가장 큰 모델을 무조건 사용하는 것입니다.

GPT-4의 임베딩이 가장 좋다고 해서 무조건 사용하면, 비용이 폭발하고 속도도 느립니다. 대부분의 경우 MiniLM 같은 작은 모델로도 충분합니다.

또한 모델을 섞어 쓰지 않도록 주의해야 합니다. 데이터베이스의 벡터는 BERT로 만들고, 검색 쿼리는 GPT로 만들면 제대로 작동하지 않습니다.

벡터 공간이 다르기 때문입니다. 한 번 선택한 모델은 일관되게 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 메모를 정리했습니다.

"알겠습니다. 우리 프로젝트는 한국어 중심이니까 KoSBERT를 써보겠습니다!" 임베딩 모델의 종류와 특징을 제대로 이해하면, 프로젝트에 최적의 모델을 선택할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 프로토타입은 OpenAI API로 빠르게 만들고, 나중에 오픈소스 모델로 전환하세요

  • 한국어 서비스는 반드시 한국어 특화 모델을 사용하세요 (KoSimCSE, KoSBERT)
  • MTEB 리더보드에서 다양한 모델의 성능을 비교할 수 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#Embedding#VectorSpace#Similarity#NLP#AWS

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.