들어가며
지난 글에서는 Token, Embedding, Context Window를 살펴봤습니다.
임베딩은 토큰이나 문서를 학습된 숫자 벡터로 표현하는 방법입니다. RAG는 질문과 문서를 검색용 임베딩 벡터로 바꾸고, 관련성이 높은 문서를 찾아 답변 생성에 활용합니다.
그렇다면 벡터끼리 얼마나 비슷한지는 어떻게 판단할까요?
이번 글에서는 대표적인 비교 방법인 코사인 유사도(Cosine Similarity)를 간단한 숫자 예제로 이해해 보겠습니다.
이 글의 벡터는 계산 원리를 설명하기 위해 직접 지정한 값입니다. 실제 임베딩 모델의 출력이나 검색 성능 측정 결과가 아닙니다. Python 코드 역시 실행 전 예제이며, 출력은 예상 결과로 표시합니다.
목차
- 벡터의 크기와 방향
- 코사인 유사도가 비교하는 것
- 숫자로 계산해 보기
- Python 예제 코드
- 예상 실행 결과
- RAG에서 문서를 선택하는 과정
- 흔한 오해와 문제 해결
- 결론
1. 벡터의 크기와 방향
숫자 벡터는 순서가 있는 숫자 배열입니다. Java에서는 다음과 같이 표현할 수 있습니다.
double[] vector = {3.0, 4.0};
숫자가 두 개이므로 2차원 벡터입니다. 이를 좌표로 생각하면 원점 (0, 0)에서 (3, 4)로 향하는 화살표로 볼 수 있습니다.
| 구분 | 의미 |
|---|---|
| 크기 | 화살표의 길이 |
| 방향 | 화살표가 향하는 쪽 |
다음 벡터를 비교해 보겠습니다.
A = [3, 4]
B = [6, 8]
B는 A의 각 값에 2를 곱한 벡터입니다. 따라서 B의 길이는 A의 두 배지만, 두 화살표가 향하는 방향은 같습니다.
코사인 유사도는 이러한 벡터의 방향 차이를 비교하는 방법입니다. 실제 임베딩은 수백·수천 차원일 수 있어 눈으로 그리기 어렵지만, 방향을 비교하는 계산 원리는 같습니다.
2. 코사인 유사도가 비교하는 것
코사인 유사도는 두 벡터 사이 각도의 코사인 값입니다. 0이 아닌 두 벡터에 대해 다음 식으로 계산합니다.
코사인 유사도 = 두 벡터의 내적 ÷ (A의 크기 × B의 크기)
- 내적: 같은 자리에 있는 숫자끼리 곱하고 모두 더한 값
- 벡터의 크기: 각 숫자를 제곱해서 더한 뒤 제곱근을 구한 값
계산 결과는 수학적으로 -1에서 1 사이입니다.
| 값 | 기하학적 의미 |
|---|---|
| 1 | 같은 방향 |
| 0 | 서로 직각 |
| -1 | 정반대 방향 |
다만 검색용 임베딩에서 0이 무조건 무관함을, -1이 문장의 반대 의미를 뜻하는 것은 아닙니다. 방향이 의미적 관련성과 연결되도록 학습된 모델에서 이 수치를 검색 점수로 활용하는 것입니다. 점수의 해석은 모델과 데이터에 따라 달라집니다.
3. 숫자로 계산해 보기
3-1. 같은 방향인 경우
A = [3, 4]
B = [6, 8]
내적 = (3 × 6) + (4 × 8) = 50
A의 크기 = √(3² + 4²) = 5
B의 크기 = √(6² + 8²) = 10
코사인 유사도 = 50 ÷ (5 × 10) = 1
숫자 자체는 다르지만 방향이 같으므로 유사도는 1입니다.
3-2. 방향이 조금 다른 경우
A = [3, 4]
C = [4, 3]
내적 = (3 × 4) + (4 × 3) = 24
두 벡터의 크기 = 각각 5
코사인 유사도 = 24 ÷ (5 × 5) = 0.96
방향이 완전히 같지는 않지만 가까워서 1에 가까운 값이 나옵니다.
3-3. 서로 직각인 경우
A = [3, 4]
D = [-4, 3]
내적 = (3 × -4) + (4 × 3) = 0
코사인 유사도 = 0
| 비교 | 코사인 유사도 |
|---|---|
| A와 B | 1.00 |
| A와 C | 0.96 |
| A와 D | 0.00 |
핵심은 각 숫자의 차이를 단순히 더하는 것이 아니라, 벡터 전체의 방향을 비교한다는 점입니다.
4. Python 예제 코드
외부 패키지 없이 Python 표준 라이브러리만 사용하는 예제입니다.
아래 코드에서 질문과 문서에 연결한 벡터는 계산용 가상 값입니다. 실제 문장을 임베딩 모델로 변환한 결과가 아닙니다.
파일명은 cosine_demo.py로 지정합니다.
import math
def cosine_similarity(a, b):
if len(a) != len(b):
raise ValueError("벡터의 차원 수가 같아야 합니다.")
dot_product = sum(x * y for x, y in zip(a, b))
magnitude_a = math.sqrt(sum(x * x for x in a))
magnitude_b = math.sqrt(sum(y * y for y in b))
if magnitude_a == 0 or magnitude_b == 0:
raise ValueError("영벡터의 코사인 유사도는 정의되지 않습니다.")
return dot_product / (magnitude_a * magnitude_b)
# 실제 임베딩 결과가 아닌 설명용 벡터입니다.
query_vector = [3, 4]
documents = [
{
"id": "FAQ-A",
"text": "단순 변심으로 반품하면 배송비는 고객이 부담합니다.",
"vector": [6, 8],
},
{
"id": "FAQ-B",
"text": "상품 불량으로 반품하면 배송비는 판매자가 부담합니다.",
"vector": [4, 3],
},
{
"id": "FAQ-C",
"text": "비밀번호는 계정 설정에서 변경할 수 있습니다.",
"vector": [-4, 3],
},
]
results = []
for document in documents:
score = cosine_similarity(query_vector, document["vector"])
results.append({
"id": document["id"],
"text": document["text"],
"score": score,
})
# 점수가 높은 문서부터 정렬합니다.
results.sort(key=lambda result: result["score"], reverse=True)
top_k = 2
for result in results[:top_k]:
print(f'{result["id"]}: {result["score"]:.4f}')
print(result["text"])
Java와 연결하면 다음과 같습니다.
| Python 코드 | Java에서의 개념 |
|---|---|
| zip(a, b) | 같은 인덱스의 원소를 짝지어 순회 |
| sum(...) | 반복문으로 누적 합산 |
| math.sqrt(...) | Math.sqrt(...) |
| results.sort(...) | Comparator를 이용한 정렬 |
| results[:top_k] | 상위 K개 선택 |
Windows에서 Python이 설치되어 있다면 다음 명령으로 실행할 수 있습니다.
py cosine_demo.py
5. 예상 실행 결과
직접 지정한 벡터에 대한 예상 출력은 다음과 같습니다. 이 글에서는 코드를 실행하지 않았습니다.
FAQ-A: 1.0000
단순 변심으로 반품하면 배송비는 고객이 부담합니다.
FAQ-B: 0.9600
상품 불량으로 반품하면 배송비는 판매자가 부담합니다.
이 결과는 지정한 벡터를 계산하고 정렬한 결과입니다. 프로그램이 문장의 의미를 분석해서 해당 점수를 알아낸 것이 아닙니다. 실제 의미 검색을 구현하려면 질문과 문서를 검색용 임베딩 모델로 변환하는 단계가 추가되어야 합니다.
또한 위 점수만으로 FAQ-A가 FAQ-B보다 실제 검색에 더 적합하다는 결론을 내릴 수는 없습니다.
6. RAG에서 문서를 선택하는 과정
문서 준비
- 문서를 적절한 청크로 나눕니다.
- 검색용 임베딩 모델로 청크를 벡터화합니다.
- 벡터를 원문·출처와 연결해서 저장합니다.
사용자 질문 처리
- 질문을 호환되는 검색용 임베딩 모델로 벡터화합니다.
- 질문 벡터와 저장된 문서 벡터를 비교합니다.
- 점수가 높은 문서 후보를 선택합니다.
- 필요한 조건과 출처를 확인합니다.
- 선택한 문서의 원문을 질문과 함께 생성 LLM에 전달합니다.
- 생성 LLM이 자료를 참고해 답변을 만듭니다.
Java 백엔드 관점에서는 검색 Service가 관련 문서 목록을 반환하고, 답변 생성 Service가 그 원문을 활용하는 구조로 이해할 수 있습니다.
작은 예제에서는 모든 문서의 점수를 직접 계산할 수 있습니다. 문서가 많아지면 벡터 인덱스를 활용해 후보를 효율적으로 찾기도 하며, 근사 검색을 사용하면 전체 문서를 비교한 정확한 순위와 차이가 날 수 있습니다.
7. 흔한 오해와 문제 해결
유사도 0.96이면 정답 확률이 96%인가요?
아닙니다. 코사인 유사도는 벡터 방향을 비교한 점수이지, 답변이 맞을 확률이 아닙니다. 질문과 비슷한 표현을 가진 문서라도 오래된 정책이거나 다른 고객에게 적용되는 자료일 수 있습니다.
점수가 높은 문서를 무조건 사용하면 되나요?
관련 없는 문서들만 있어도 그중 1등은 존재합니다.
따라서 Top-K만으로 충분하지 않을 수 있습니다. 모델과 데이터에 맞게 검색 품질을 평가하고, 근거가 부족할 때는 답변을 보류하거나 추가 질문을 하는 동작도 필요합니다. 모든 모델에 공통으로 적용되는 “0.8 이상이면 정답” 같은 기준은 없습니다.
차원 수가 같으면 다른 모델의 벡터도 비교할 수 있나요?
숫자 계산은 가능해도 검색 점수로 의미 있게 해석할 수 있다는 보장은 없습니다. 벡터의 각 자리가 표현하는 학습된 공간이 다를 수 있기 때문입니다. 질문과 문서는 호환되는 임베딩 모델과 설정으로 처리해야 합니다.
영벡터가 들어오면 어떻게 하나요?
모든 원소가 0인 벡터는 크기가 0입니다. 코사인 유사도 계산에서 분모가 0이 되므로 정의되지 않습니다. 예제에서는 이를 오류로 처리합니다. 실제 서비스에서도 해당 데이터를 확인하고 제외하거나 재처리하는 정책이 필요합니다.
8. 결론
코사인 유사도는 두 벡터의 방향이 얼마나 비슷한지 계산하는 방법입니다.
임베딩 벡터의 각 숫자가 무엇을 뜻하는지 직접 해석하지 못하더라도, 같은 벡터 공간에서 전체 방향을 비교해 검색에 활용할 수 있습니다.
RAG에서는 이 점수를 관련 문서 후보를 찾는 데 사용합니다. 이후에는 문서의 출처와 적용 조건을 확인하고, 선택한 원문을 생성 LLM에 전달합니다.
- 벡터의 크기와 방향은 서로 다른 개념입니다.
- 코사인 유사도는 방향의 유사도를 계산합니다.
- 검색 점수가 높다는 사실만으로 문서의 정확성이나 답변의 정답 여부가 보장되지는 않습니다.
다음 실습에서는 직접 지정한 숫자 대신 실제 검색용 임베딩 모델로 질문과 문서를 변환하고, 검색 결과가 적절한지 확인할 수 있습니다.
이전 글
'AI·LLM' 카테고리의 다른 글
| [LLM 기초] Token·Embedding·Context Window — Java 개발자가 이해하는 LLM의 입력 처리 (0) | 2026.10.14 |
|---|---|
| [LLM·RAG 기초] FAQ 예제로 따라가는 RAG의 답변 생성 과정 (0) | 2026.10.14 |