OpenAI 임베딩 모델
OpenAI API를 이용해 문자열을 임베딩 벡터로 변환해보겠습니다. 임베딩 모델에 몇 가지가 있지만 그 중 OpenAI에서 권장하는 text-embedding-ada-002 모델을 사용하면 됩니다. 모델 정보는 아래 표에 정리했습니다.
토크나이저 | cl100k_base |
최대 입력 토큰수 | 8192 |
출력 벡터 차원 | 1536 |
API 사용료 | $0.0001/1K tokens |
파이썬 API
임베딩 계산을 위한 파이썬 코드는 다음과 같습니다.
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
text = "I love Python!"
response = openai.Embedding.create(
input = text,
model = "text-embedding-ada-002"
)
결과로 나오는 response
는 다음과 같습니다.
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [
-0.0078111737966537476,
-0.01599879004061222,
-0.0008188456413336098,
-0.03275046497583389,
-0.01546101551502943,
...
0.027292054146528244,
-0.0034115067683160305,
-0.005542438011616468,
-0.006056685000658035,
-0.03979530930519104
]
}
],
"model": "text-embedding-ada-002-v2",
"usage": {
"prompt_tokens": 4,
"total_tokens": 4
}
}
결과 임베딩 벡터는 1536차원이기 때문에 중간은 생략했습니다. response
에서 임베딩 벡터만 추출하려면 다음과 같이 합니다.
embeddings = response['data'][0]['embedding']
벡터 거리와 의미 검색
텍스트를 임베딩 벡터로 변환하면 벡터들 사이의 거리를 계산할 수 있게 됩니다. 두 임베딩 벡터의 거리가 가까우면 의미가 유사한 텍스트, 벡터의 거리가 멀먼 관련 없는 텍스트라고 할 수 있습니다. 따라서 텍스트 사이에 일치하는 단어가 없더라도 벡터 거리를 이용해 관련된 텍스트를 찾아낼 수 있죠. 의미 검색(Semantic Search)에서 사용하는 방법입니다.
코사인 유사도와 코사인 거리
벡터 사이의 거리를 계산하는 방법에는 여러 가지가 있는데, LLM에서 많이 사용하는 방법은 코사인 유사도(Cosine Similarity)를 이용하는 방법입니다.
코사인 유사도는 두 벡터가 가까우면 1, 멀면 0이 됩니다. 거리를 재려면 가까운게 0, 먼게 1이 되도록 하는게 좋겠죠? 코사인 거리(Cosine Distance)는 다음과 같이 계산합니다.
예제
“I love Python!“에서 한 단어씩만 바꾼 문장을 몇 개 만들어서 임베딩 벡터를 만들고, 원래 문장의 임베딩 벡터와 코사인 거리를 계산해보겠습니다. 사용할 문장들입니다.
- I love Python (느낌표 삭제)
- I like Python!
- I hate Python!
- I love JavaScript!
- I love you!
다음은 테스트에 사용한 함수들입니다. 먼저 임베딩 벡터를 한 번에 얻는 get_embedding 함수를 만들고 코사인 유사도와 코사인 거리를 계산하는 함수를 만들었습니다.
import numpy as np
from numpy.linalg import norm
def get_embedding(text):
response = openai.Embedding.create(
input=text,
model="text-embedding-ada-002"
)
return np.array(response['data'][0]['embedding'])
def cosine_similarity(a,b):
return np.dot(a,b) / (norm(a)*norm(b))
def cosine_dist(a,b):
return 1-cosine_similarity(a,b)
테스트 코드입니다.
text_org = "I love Python!"
texts = ["I love Python",
"I like Python!",
"I hate Python!",
"I love JavaScript!",
"I love you!"]
# 임베딩 벡터 구하기
emb_org = get_embedding(text_org)
emb_comp = [get_embedding(t) for t in texts]
# 코사인 거리 계산
dist = [cosine_dist(emb_org, emb) for emb in emb_comp]
# 결과 출력
for t,d in zip(texts,dist):
print(f"{d:.4f}: {t}")
다음은 출력된 결과입니다.
0.0235: I love Python
0.0164: I like Python!
0.0790: I hate Python!
0.0888: I love JavaScript!
0.1631: I love you!
결과를 관찰해 봅시다. 숫자는 옆에 있는 문장이 기준 문장 “I love Python!“과 얼마나 가까운 문장인지 나타냅니다. 값이 작을수록 의미가 더 가깝죠.
- “I like Python!”이 “I love Python”보다 더 가깝습니다. love/like 차이보다 느낌표 유무의 차이가 더 크네요!
- hate는 당연히 like에 비해 love로부터의 거리가 멀죠.
- 하지만 “I hate Python!”은 “I love JavaScript!” 보다는 가깝습니다. 좋던 싫던(정반대의 의미지만) 파이썬 선호도를 나타낸 문장들끼리는 JavaScript 선호도를 나타낸 문장보다 의미가 가깝네요.
- JavaScript가 Python은 아니지만 둘 다 프로그래밍 언어이기 때문에 “I love JavaScript!”는 “I love you!” 보다 의미가 훨씬 가깝습니다.
이렇게 텍스트를 임베딩 벡터로 만들면 벡터 사이의 거리를 이용해 의미가 가까운 정도를 정량적으로 계산할 수 있습니다. 벡터 데이터베이스를 이용하면 데이터베이스에 저장해놓은 수많은 임베딩 벡터들 중 기준이 되는 문장(query)과 거리가 가까운 벡터들을 빠르게 추출할 수 있습니다.