RAG와 임베딩 · 글 11편RAG와 임베딩
  1. RAG란 무엇인가 - 넣기, 쪼개기, 찾기, 답하기
  2. 문서를 붙여 넣을까, RAG를 만들까
  3. 임베딩이란 무엇인가
  4. 로컬 임베딩 모델 고르기
  5. 문서를 청크로 나누기
  6. 키워드 검색, 의미 검색, 하이브리드 검색
  7. 로컬 의미 검색 만들기 - 젬마 임베딩 실습 따라 읽기
  8. RAG 답변 평가와 자주 생기는 실패
  9. RAG, 긴 컨텍스트, 파인튜닝, LLM Wiki 비교
  10. RAG가 잘 되도록 사무 문서 준비하기
  11. 내부 문서를 색인할 때의 개인정보와 보안

이 글에서 다루는 것임베딩은 글이나 그림을 비교할 수 있는 숫자 목록으로 바꾸는 일입니다. 숫자 세 개짜리 작은 예제로 유사도 계산을 직접 따라가 봅니다.

한 문장 정의

임베딩은 텍스트를 비교할 수 있는 숫자 목록으로 바꾸는 과정입니다. 이 숫자 목록을 벡터라고 부릅니다. 뜻이 비슷한 글은 비슷한 숫자 목록이 되도록 모델이 학습되어 있습니다.

예를 들어 “강아지”와 “개”는 뜻이 가까우므로 두 벡터도 가깝습니다. “컴퓨터”는 “강아지”와 관련이 적으므로 벡터가 멀리 떨어집니다.

키워드 검색은 같은 글자가 있어야 찾습니다. 임베딩 검색은 글자가 달라도 뜻이 가까우면 찾습니다. “연차 며칠 쓸 수 있어?“라는 질문으로 “휴가 사용 기준”이라는 조항을 찾을 수 있는 이유입니다.

대화 모델과 다른 모델이다

임베딩을 만드는 모델은 대화하는 모델과 따로 있습니다.

구분 넣는 것 나오는 것 용도
대화 모델 질문 문장 답변 작성
임베딩 모델 문장 또는 이미지 숫자 수백 개 검색, 분류, 비슷한 것 찾기

임베딩 모델은 대화 모델보다 훨씬 작습니다. 그래서 그래픽카드가 없는 PC에서도 돌릴 수 있습니다. 어떤 모델을 고를지는 로컬 임베딩 모델 고르기에서 다룹니다.

숫자 세 개로 해 보는 예제

실제 임베딩은 숫자가 수백 개입니다. 원리를 보기 위해 세 개짜리로 줄여 봅니다. 아래 숫자는 설명을 위해 손으로 정한 것입니다.

글 벡터
질문: 연차 며칠 쓸 수 있어? (0.8, 0.2, 0.1)
조각 A: 휴가 사용 기준 (0.9, 0.1, 0.2)
조각 B: 주차장 이용 안내 (0.1, 0.9, 0.3)

두 벡터가 얼마나 같은 방향을 가리키는지를 코사인 유사도로 잽니다. 계산은 세 단계입니다.

  1. 같은 자리 숫자끼리 곱해서 더합니다(내적).
  2. 각 벡터의 길이를 구합니다. 숫자를 제곱해서 더한 뒤 제곱근을 구합니다.
  3. 1번을 두 길이의 곱으로 나눕니다.

질문과 조각 A를 계산합니다.

  • 내적: 0.8×0.9 + 0.2×0.1 + 0.1×0.2 = 0.76
  • 질문의 길이: 약 0.831, 조각 A의 길이: 약 0.927
  • 코사인 유사도: 0.76 ÷ (0.831 × 0.927) = 약 0.987

질문과 조각 B를 계산합니다.

  • 내적: 0.8×0.1 + 0.2×0.9 + 0.1×0.3 = 0.29
  • 조각 B의 길이: 약 0.954
  • 코사인 유사도: 0.29 ÷ (0.831 × 0.954) = 약 0.366

조각 A가 0.987, 조각 B가 0.366입니다. 검색 프로그램은 점수가 높은 순서로 정렬해 조각 A를 먼저 내놓습니다. 임베딩 검색이 하는 일은 이것이 전부입니다.

같은 계산을 코드로 확인합니다.

import numpy as np

q = np.array([0.8, 0.2, 0.1])
a = np.array([0.9, 0.1, 0.2])
b = np.array([0.1, 0.9, 0.3])

def cos(x, y):
    return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y)))

print(round(cos(q, a), 3))   # 0.987
print(round(cos(q, b), 3))   # 0.366

코사인 유사도는 -1에서 1 사이 값입니다. 1에 가까울수록 같은 방향입니다.

정규화하면 곱셈 한 번으로 끝난다

벡터의 길이를 미리 1로 맞춰 두는 것을 정규화라고 합니다. 길이가 1이면 3번 단계의 나눗셈이 필요 없어서, 내적만 구하면 그 값이 곧 코사인 유사도입니다.

로컬 검색 프로그램은 대개 이렇게 합니다. 색인할 때 벡터를 정규화해 저장하고, 검색할 때는 저장된 벡터 전체와 질문 벡터를 행렬 곱셈 한 번으로 계산합니다. 조각이 수천 개여도 이 계산은 일반 PC에서 금방 끝납니다. 시간이 걸리는 부분은 계산이 아니라 임베딩을 만드는 쪽입니다.

텍스트만 되는 것은 아니다

이미지를 넣을 수 있는 임베딩 모델도 있습니다. 이런 모델은 글과 그림을 같은 숫자 공간에 놓습니다. 그래서 “붉은 노을이 물드는 저녁 바다”라는 글로 노을 사진을 찾을 수 있습니다.

신성진 강사의 실습 프로젝트는 이 방식으로 만든 사진 검색입니다. 테스트 코드는 글 세 개와 단색 이미지 두 장(파란색, 초록색)을 임베딩한 뒤, 글과 이미지 사이의 유사도 표를 출력합니다.

queries = ["푸른 하늘과 바다 풍경", "컴퓨터 프로그래밍 코드", "귀여운 고양이와 강아지"]
query_embeddings = model.encode(queries, prompt_name="SearchQuery")

blue_img = Image.new("RGB", (224, 224), color=(30, 100, 220))
green_img = Image.new("RGB", (224, 224), color=(30, 180, 50))
img_embeddings = model.encode([{"image": blue_img}, {"image": green_img}])

similarity = model.similarity(query_embeddings, img_embeddings)

전체 구조는 로컬 의미 검색 만들기 - 젬마 임베딩 실습 따라 읽기에서 단계별로 봅니다.

지켜야 할 규칙 세 가지

문서와 질문을 같은 모델로 임베딩합니다. 모델이 다르면 숫자의 의미가 달라서 비교할 수 없습니다. 임베딩 모델을 바꾸면 문서 전체를 다시 임베딩해야 합니다.

모델 이름, 버전, 벡터 차원을 기록해 둡니다. 차원은 벡터에 들어 있는 숫자의 개수입니다. 이 기록이 있어야 나중에 같은 색인을 다시 만들 수 있고, 저장된 벡터와 새 벡터가 섞이는 사고를 막습니다.

유사도가 높다고 정답은 아닙니다. 유사도는 질문과 관련될 가능성을 뜻합니다. 주제가 비슷한 다른 조항이 높은 점수를 받는 일이 흔합니다. 상위 조각을 원문과 대조해 보는 단계를 빼지 않습니다.

임베딩이 약한 경우

임베딩은 정확한 문자열을 찾는 데는 약합니다.

  • 문서 번호, 계약 번호, 조문 번호
  • 사람 이름, 기관명, 제품명
  • 뜻보다 철자가 중요한 전문 용어

“제37조”를 물었는데 내용이 비슷한 제35조가 먼저 나올 수 있습니다. 이런 질문은 키워드 검색이 더 잘 찾습니다. 그래서 실무에서는 두 검색을 함께 씁니다. 방법은 키워드 검색, 의미 검색, 하이브리드 검색에 있습니다.

직접 눌러 보려면

실습실에 브라우저에서 임베딩을 체험하는 카드가 있습니다. 사진이나 문서를 넣고 비슷한 순서로 정렬되는 것을 확인할 수 있습니다. 용어가 낯설면 용어집을 함께 봅니다.

다음에 읽을 글