벡터 임베딩

벡터 임베딩 (Vector embeddings)

OpenAI의 텍스트 임베딩은 텍스트 문자열 간의 관련성(relatedness)을 측정해요. 검색, 클러스터링, 추천, 이상 탐지, 다양성 측정, 분류 등 다양한 곳에 쓰입니다.

출처: 문서

본문

임베딩이란 무엇인가요?

텍스트 임베딩은 텍스트 문자열 간의 관련성을 측정해요. 흔히 쓰이는 용도로는:

  • 검색 (쿼리 문자열과의 관련성으로 결과 순위 지정)
  • 클러스터링 (유사성으로 텍스트 문자열 그룹화)
  • 추천 (관련 텍스트 문자열이 있는 항목 추천)
  • 이상 탐지 (관련성이 낮은 이상값 식별)
  • 다양성 측정 (유사성 분포 분석)
  • 분류 (가장 유사한 레이블로 텍스트 문자열 분류)

임베딩은 부동소수점 숫자의 벡터(목록)예요. 두 벡터 간의 거리가 그 관련성을 측정해요. 작은 거리는 높은 관련성을, 큰 거리는 낮은 관련성을 뜻해요.

임베딩 가격은 가격 페이지를 참고하세요. 요청은 입력의 토큰 수를 기준으로 청구돼요.

임베딩 얻는 방법

임베딩을 얻으려면 텍스트 문자열을 임베딩 모델 이름(예: text-embedding-3-small)과 함께 임베딩 API 엔드포인트로 보내면 돼요.

임베딩 얻기 예시:

import OpenAI from "openai";
const openai = new OpenAI();

const embedding = await openai.embeddings.create({
  model: "text-embedding-3-small",
  input: "Your text string goes here",
  encoding_format: "float",
});

console.log(embedding);
from openai import OpenAI

client = OpenAI()

response = client.embeddings.create(
    input="Your text string goes here", model="text-embedding-3-small"
)

print(response.data[0].embedding)
package main

import (
	"context"
	"fmt"

	"github.com/openai/openai-go/v3"
)

func main() {
	client := openai.NewClient()

	embedding, err := client.Embeddings.New(context.Background(), openai.EmbeddingNewParams{
		Model: openai.EmbeddingModelTextEmbedding3Small,
		Input: openai.EmbeddingNewParamsInputUnion{
			OfString: openai.String("Your text string goes here."),
		},
	})
	if err != nil {
		panic(err)
	}

	fmt.Println(len(embedding.Data[0].Embedding))
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.embeddings.EmbeddingCreateParams;

var embedding =
    client
        .embeddings()
        .create(
            EmbeddingCreateParams.builder()
                .model("text-embedding-3-small")
                .input("The food was delicious and the waiter...")
                .build());

System.out.println(embedding.data().get(0).embedding());
using OpenAI.Embeddings;

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
string model = "text-embedding-3-small";
EmbeddingClient client = new(model, key);

OpenAIEmbedding embedding = await client.GenerateEmbeddingAsync(
    "The food was delicious and the waiter was friendly."
);

Console.WriteLine($"Dimensions: {embedding.ToFloats().Length}");
require "openai"

client = OpenAI::Client.new

response = client.embeddings.create(
  model: "text-embedding-3-small",
  input: "The food was delicious and the waiter..."
)

puts(response.data.fetch(0).embedding)
curl https://api.openai.com/v1/embeddings \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ***" \
  -d '{
    "input": "Your text string goes here",
    "model": "text-embedding-3-small"
  }'

응답은 임베딩 벡터(부동소수점 숫자 목록)와 추가 메타데이터를 포함해요. 임베딩 벡터를 추출해 벡터 데이터베이스에 저장하고 다양한 용도로 쓸 수 있어요.

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [
        -0.006929283495992422, -0.005336422007530928, -4.547132266452536e-5,
        -0.024047505110502243
      ]
    }
  ],
  "model": "text-embedding-3-small",
  "usage": {
    "prompt_tokens": 5,
    "total_tokens": 5
  }
}

기본적으로 임베딩 벡터의 길이는 text-embedding-3-small의 경우 1536, text-embedding-3-large의 경우 3072예요. 개념 표현 속성을 잃지 않고 임베딩 차원을 줄이려면 dimensions 파라미터를 전달하세요. 임베딩 차원에 대한 자세한 내용은 사용 사례 섹션을 참고하세요.

임베딩 모델

OpenAI는 두 개의 강력한 3세대 임베딩 모델(모델 ID의 -3으로 표시)을 제공해요. 자세한 내용은 임베딩 v3 발표 블로그 글을 참고하세요.

사용량은 입력 토큰당 요금이 매겨져요. 아래는 달러당 텍스트 페이지 수(페이지당 약 800 토큰 가정)의 가격 예시예요.

모델 달러당 페이지 수 MTEB 평가 성능 최대 입력
text-embedding-3-small 62,500 62.3% 8192
text-embedding-3-large 9,615 64.6% 8192
text-embedding-ada-002 12,500 61.0% 8192

사용 사례

여기서는 Amazon 미식 리뷰 데이터셋을 사용한 대표적인 사용 사례를 보여드릴게요.

임베딩 얻기

데이터셋에는 2012년 10월까지 Amazon 사용자가 남긴 총 568,454개의 음식 리뷰가 있어요. 설명을 위해 가장 최근 리뷰 1000개의 하위 집합을 사용해요. 리뷰는 영어이고 긍정적이거나 부정적인 경향이 있어요. 각 리뷰에는 ProductId, UserId, Score, 리뷰 제목(Summary)과 본문(Text)이 있어요. 예를 들면:

Product Id User Id Score Summary Text
B001E4KFG0 A3SGXH7AUHU8GW 5 Good Quality Dog Food I have bought several of the Vitality canned...
B00813GRG4 A1D87F6ZCVE5NK 1 Not as Advertised Product arrived labeled as Jumbo Salted Peanut...

아래에서는 리뷰 제목과 본문을 단일 결합 텍스트로 합쳐요. 모델이 이 결합 텍스트를 인코딩해 단일 벡터 임베딩을 출력해요.

임베딩 얻기 (JS/Python/Java/Ruby — text-embedding-3-small로 리뷰를 임베딩하고 CSV로 저장)

import { mkdir, writeFile } from "node:fs/promises";
import OpenAI from "openai";

const client = new OpenAI();
const reviews = ["A rich cup of coffee.", "A bright herbal tea."];

const response = await client.embeddings.create({
  model: "text-embedding-3-small",
  input: reviews.map((review) => review.replaceAll("\n", " ")),
});

const csvField = (value) => `"${value.replaceAll('"', '""')}"`;
const rows = response.data.map(({ embedding }, index) =>
  [csvField(reviews[index]), csvField(JSON.stringify(embedding))].join(",")
);

await mkdir("output", { recursive: true });
await writeFile(
  "output/embedded_1k_reviews.csv",
  ["combined,ada_embedding", ...rows].join("\n") + "\n"
);
from openai import OpenAI

client = OpenAI()


def get_embedding(text, model="text-embedding-3-small"):
    text = text.replace("\n", " ")
    return client.embeddings.create(input=[text], model=model).data[0].embedding


df["ada_embedding"] = df.combined.apply(
    lambda x: get_embedding(x, model="text-embedding-3-small")
)
df.to_csv("output/embedded_1k_reviews.csv", index=False)

저장된 파일에서 데이터를 로드하려면:

import pandas as pd

df = pd.read_csv("output/embedded_1k_reviews.csv")
df["ada_embedding"] = df.ada_embedding.apply(eval).apply(np.array)

임베딩 차원 줄이기

더 큰 임베딩을 사용하면(예: 벡터 저장소에 저장해 검색) 일반적으로 더 작은 임베딩보다 비용이 더 들고 컴퓨트·메모리·저장 공간을 더 소비해요.

새 임베딩 모델 두 개는 모두 성능과 비용의 트레이드오프를 허용하는 기법으로 학습됐어요. 구체적으로 개발자는 dimensions API 파라미터를 전달해 개념 표현 속성을 잃지 않고 임베딩을 줄일 수 있어요 (즉 시퀀스 끝에서 일부 숫자 제거). 예를 들어 MTEB 벤치마크에서 text-embedding-3-large 임베딩은 256 크기로 줄여도 크기 1536의 줄이지 않은 text-embedding-ada-002 임베딩을 능가해요. 차원 변경이 성능에 미치는 영향은 embeddings v3 출시 블로그 글에서 더 읽을 수 있어요.

일반적으로 임베딩을 만들 때 dimensions 파라미터를 쓰는 것이 권장돼요. 어떤 경우에는 생성한 뒤 임베딩 차원을 바꿔야 할 수도 있어요. 수동으로 차원을 바꿀 때는 아래처럼 임베딩의 차원을 정규화해야 해요.

import OpenAI from "openai";

const client = new OpenAI();

const response = await client.embeddings.create({
  model: "text-embedding-3-small",
  input: "Testing 123",
  encoding_format: "float",
});

const shortened = response.data[0].embedding.slice(0, 256);
const magnitude = Math.hypot(...shortened);
const normalized = shortened.map((value) =>
  magnitude === 0 ? 0 : value / magnitude
);

console.log(normalized);
from openai import OpenAI
import numpy as np

client = OpenAI()


def normalize_l2(x):
    x = np.array(x)
    if x.ndim == 1:
        norm = np.linalg.norm(x)
        if norm == 0:
            return x
        return x / norm
    else:
        norm = np.linalg.norm(x, 2, axis=1, keepdims=True)
        return np.where(norm == 0, x, x / norm)


response = client.embeddings.create(
    model="text-embedding-3-small", input="Testing 123", encoding_format="float"
)

cut_dim = response.data[0].embedding[:256]
norm_dim = normalize_l2(cut_dim)

print(norm_dim)

(Java, C#, Ruby 예시도 임베딩을 첫 256차원으로 자르고 L2 정규화하는 같은 패턴입니다.)

차원을 동적으로 바꾸면 매우 유연하게 사용할 수 있어요. 예를 들어 최대 1024 차원까지만 지원하는 벡터 데이터 저장소를 쓰는 경우, 개발자는 여전히 최고 임베딩 모델 text-embedding-3-large를 쓰되 dimensions API 파라미터에 1024를 지정해 3072 차원에서 줄일 수 있어요. 작은 벡터 크기와 맞바꿔 약간의 정확도를 트레이드오프하는 거죠.

임베딩 기반 검색을 사용한 질의 응답

모델이 학습하지 않은 핵심 사실·정보가 포함된 데이터를 사용자 질문 응답 생성에 제공해야 하는 흔한 경우가 있어요. 아래처럼 해결하는 한 가지 방법은 모델의 컨텍스트 창에 추가 정보를 넣는 거예요. 많은 사용 사례에서 효과적이지만 토큰 비용이 높아져요. 이 노트북에서는 이 접근과 임베딩 기반 검색 사이의 트레이드오프를 탐구해요.

query = f"""Use the below article on the 2022 Winter Olympics to answer the subsequent question. If the answer cannot be found, write "I don't know."

Article:
\"\"\"
{wikipedia_article_on_curling}
\"\"\"

Question: Which athletes won the gold medal in curling at the 2022 Winter Olympics?"""

response = client.chat.completions.create(
    messages=[
        {
            "role": "system",
            "content": "You answer questions about the 2022 Winter Olympics.",
        },
        {"role": "user", "content": query},
    ],
    model=GPT_MODEL,
    temperature=0,
)

print(response.choices[0].message.content)

임베딩을 사용한 텍스트 검색

가장 관련 있는 문서를 가져오려면 쿼리와 각 문서의 임베딩 벡터 간 코사인 유사성을 사용하고, 점수가 가장 높은 문서를 반환해요.

def search_reviews(df, product_description, n=3, pprint=True):
    embedding = get_embedding(product_description, model="text-embedding-3-small")
    df["similarities"] = df.ada_embedding.apply(
        lambda x: cosine_similarity(x, embedding)
    )
    res = df.sort_values("similarities", ascending=False).head(n)
    return res


res = search_reviews(df, "delicious beans", n=3)

임베딩을 사용한 코드 검색

코드 검색은 임베딩 기반 텍스트 검색과 비슷하게 작동해요. 주어진 저장소의 모든 Python 파일에서 Python 함수를 추출하는 메서드를 제공하고, 각 함수를 text-embedding-3-small 모델로 인덱싱해요.

코드 검색을 수행하려면 같은 모델로 자연어 쿼리를 임베딩해요. 그런 다음 결과 쿼리 임베딩과 각 함수 임베딩 사이의 코사인 유사성을 계산해요. 코사인 유사성이 가장 높은 결과가 가장 관련 있어요.

df["code_embedding"] = df["code"].apply(
    lambda x: get_embedding(x, model="text-embedding-3-small")
)


def search_functions(df, code_query, n=3, pprint=True, n_lines=7):
    embedding = get_embedding(code_query, model="text-embedding-3-small")
    df["similarities"] = df.code_embedding.apply(
        lambda x: cosine_similarity(x, embedding)
    )

    res = df.sort_values("similarities", ascending=False).head(n)
    return res


res = search_functions(df, "Completions API tests", n=3)

임베딩을 사용한 추천

임베딩 벡터 사이의 거리가 짧을수록 유사성이 크므로, 임베딩은 추천에 유용할 수 있어요.

아래는 기본 추천기를 보여줘요. 문자열 목록과 하나의 'source' 문자열을 받아 그들의 임베딩을 계산하고, 유사성이 높은 순에서 낮은 순으로 문자열을 순위 매겨 반환해요. 구체적인 예로, 연결된 노트북은 AG 뉴스 데이터셋(2,000개 뉴스 기사 설명으로 샘플링)에 이 함수 버전을 적용해 주어진 source 기사에 가장 유사한 상위 5개 기사를 반환해요.

def recommendations_from_strings(
    strings: list[str],
    index_of_source_string: int,
    model="text-embedding-3-small",
) -> list[int]:
    """Return nearest neighbors of a given string."""

    # get embeddings for all strings
    embeddings = [embedding_from_string(string, model=model) for string in strings]

    # get the embedding of the source string
    query_embedding = embeddings[index_of_source_string]

    # get distances between the source embedding and other embeddings (function from embeddings_utils.py)
    distances = distances_from_embeddings(
        query_embedding, embeddings, distance_metric="cosine"
    )

    # get indices of nearest neighbors (function from embeddings_utils.py)
    indices_of_nearest_neighbors = indices_of_nearest_neighbors_from_distances(
        distances
    )
    return indices_of_nearest_neighbors

2D 데이터 시각화

임베딩의 크기는 기본 모델의 복잡성에 따라 달라져요. 이 고차원 데이터를 시각화하기 위해 t-SNE 알고리즘으로 데이터를 2차원으로 변환해요.

리뷰어가 준 별점에 따라 개별 리뷰에 색을 칠해요.

  • 1성: 빨강
  • 2성: 진한 주황
  • 3성: 골드
  • 4성: 청록
  • 5성: 진한 초록

시각화는 대략 3개의 클러스터를 만드는 것처럼 보이고, 그중 하나는 대부분 부정 리뷰예요.

import numpy as np
import pandas as pd
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
import matplotlib

df = pd.read_csv("output/embedded_1k_reviews.csv")
matrix = np.array(df.ada_embedding.apply(eval).to_list())

# Create a t-SNE model and transform the data
tsne = TSNE(
    n_components=2, perplexity=15, random_state=42, init="random", learning_rate=200
)
vis_dims = tsne.fit_transform(matrix)

colors = ["red", "darkorange", "gold", "turquoise", "darkgreen"]
x = [x for x, y in vis_dims]
y = [y for x, y in vis_dims]
color_indices = df.Score.values - 1

colormap = matplotlib.colors.ListedColormap(colors)
plt.scatter(x, y, c=color_indices, cmap=colormap, alpha=0.3)
plt.title("Amazon ratings visualized in language using t-SNE")

ML 알고리즘을 위한 텍스트 피처 인코더로서의 임베딩

임베딩은 머신러닝 모델 내에서 일반적인 자유 텍스트 피처 인코더로 쓸 수 있어요. 관련 입력 중 일부가 자유 텍스트라면 임베딩을 포함하면 어떤 ML 모델이든 성능이 개선돼요. 임베딩은 ML 모델 내 범주형 피처 인코더로도 쓸 수 있어요. 직함처럼 범주형 변수의 이름이 의미 있고 많을 때 가장 가치가 높아요. 유사성 임베딩은 일반적으로 이 작업에 검색 임베딩보다 성능이 좋아요.

일반적으로 임베딩 표현은 매우 풍부하고 정보 밀도가 높다는 것을 관찰했어요. 예를 들어 SVD나 PCA로 입력의 차원을 10%만 줄여도 특정 작업의 다운스트림 성능이 일반적으로 나빠져요.

이 코드는 데이터를 학습용·테스트용 집합으로 나누는데, 회귀와 분류 두 사용 사례에서 쓰일 거예요.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    list(df.ada_embedding.values), df.Score, test_size=0.2, random_state=42
)

임베딩 피처를 사용한 회귀

임베딩은 숫자 값을 예측하는 우아한 방법을 제공해요. 이 예시에서는 리뷰 텍스트를 기반으로 리뷰어의 별점을 예측해요. 임베딩에 담긴 의미 정보가 높기 때문에 리뷰가 아주 적어도 예측이 괜찮아요.

점수가 1~5의 연속 변수라고 가정하고, 알고리즘이 임의의 부동소수점 값을 예측하게 해요. ML 알고리즘은 예측값과 실제 점수의 거리를 최소화하며 평균 절대 오차 0.39를 달성해요. 즉 평균적으로 예측이 별 반 개 미만으로 어긋난다는 뜻이에요.

from sklearn.ensemble import RandomForestRegressor

rfr = RandomForestRegressor(n_estimators=100)
rfr.fit(X_train, y_train)
preds = rfr.predict(X_test)

임베딩 피처를 사용한 분류

이번에는 알고리즘이 15 사이의 값을 예측하게 하는 대신, 리뷰의 정확한 별 개수를 15 별의 5개 버킷으로 분류하려 해요.

학습 후 모델은 2~4성 같은 미묘한 리뷰보다 1성·5성 리뷰를 훨씬 잘 예측하는 법을 배우는데, 더 극단적인 감정 표현 때문일 가능성이 커요.

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, accuracy_score

clf = RandomForestClassifier(n_estimators=100)
clf.fit(X_train, y_train)
preds = clf.predict(X_test)

제로샷 분류

임베딩을 레이블된 학습 데이터 없이 제로샷 분류에 쓸 수 있어요. 각 클래스에 대해 클래스 이름이나 짧은 설명을 임베딩해요. 새 텍스트를 제로샷 방식으로 분류하려면 그 임베딩을 모든 클래스 임베딩과 비교하고 유사성이 가장 높은 클래스를 예측해요.

df = df[df.Score != 3]
df["sentiment"] = df.Score.replace(
    {1: "negative", 2: "negative", 4: "positive", 5: "positive"}
)

labels = ["negative", "positive"]
label_embeddings = [get_embedding(label, model=model) for label in labels]


def label_score(review_embedding, label_embeddings):
    return cosine_similarity(review_embedding, label_embeddings[1]) - cosine_similarity(
        review_embedding, label_embeddings[0]
    )


prediction = (
    "positive" if label_score(get_embedding("Sample Review", model=model), label_embeddings) > 0 else "negative"
)

콜드 스타트 추천을 위한 사용자·제품 임베딩 얻기

사용자의 모든 리뷰를 평균내 사용자 임베딩을 얻을 수 있어요. 마찬가지로 그 제품에 대한 모든 리뷰를 평균내 제품 임베딩을 얻을 수 있어요. 이 접근의 유용성을 보여주기 위해 50k 리뷰의 하위 집합을 사용해 사용자·제품별 리뷰를 더 많이 커버해요.

이 임베딩의 유용성을 별도의 테스트 집합에서 평가하는데, 사용자·제품 임베딩의 유사성을 별점의 함수로 그려요. 흥미롭게도 이 접근에 기반하면 사용자가 제품을 받기 전에도 그들이 그 제품을 좋아할지 무작위보다 더 잘 예측할 수 있어요.

user_embeddings = df.groupby("UserId").ada_embedding.apply(np.mean)
prod_embeddings = df.groupby("ProductId").ada_embedding.apply(np.mean)

클러스터링

클러스터링은 대량의 텍스트 데이터를 이해하는 한 가지 방법이에요. 임베딩은 각 텍스트의 의미적으로 유의미한 벡터 표현을 제공하므로 이 작업에 유용해요. 비지도 방식으로 클러스터링이 데이터셋의 숨은 그룹을 드러내요.

이 예시에서는 네 개의 뚜렷한 클러스터를 발견해요. 하나는 강아지 사료, 하나는 부정 리뷰, 둘은 긍정 리뷰예요.

import numpy as np
from sklearn.cluster import KMeans

matrix = np.vstack(df.ada_embedding.values)
n_clusters = 4

kmeans = KMeans(n_clusters=n_clusters, init="k-means++", random_state=42)
kmeans.fit(matrix)
df["Cluster"] = kmeans.labels_

FAQ

임베딩하기 전에 문자열이 몇 토큰인지 어떻게 알 수 있나요?

Python에서 OpenAI의 토크나이저 tiktoken으로 문자열을 토큰으로 쪼갤 수 있어요.

예시 코드:

import tiktoken


def num_tokens_from_string(string: str, encoding_name: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.get_encoding(encoding_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens


num_tokens_from_string("tiktoken is great!", "cl100k_base")

text-embedding-3-small 같은 3세대 임베딩 모델에는 cl100k_base 인코딩을 사용하세요.

자세한 내용과 예시 코드는 OpenAI Cookbook 가이드 tiktoken으로 토큰 세는 방법에 있어요.

K개의 최근접 임베딩 벡터를 빠르게 검색하려면 어떻게 하나요?

많은 벡터를 빠르게 검색하려면 벡터 데이터베이스를 사용하는 것을 권장해요. 벡터 데이터베이스와 OpenAI API를 함께 쓰는 예시는 GitHub의 Cookbook에서 찾을 수 있어요.

어떤 거리 함수를 써야 하나요?

코사인 유사성을 권장해요. 거리 함수의 선택은 보통 크게 중요하지 않아요.

OpenAI 임베딩은 길이 1로 정규화되므로:

  • 코사인 유사성을 내적(dot product)만으로 약간 더 빠르게 계산할 수 있어요.
  • 코사인 유사성과 Euclidean 거리가 동일한 순위를 결과로 줘요.

임베딩을 온라인으로 공유할 수 있나요?

네, 고객은 임베딩을 포함해 모델의 입력과 출력을 소유해요. API에 넣는 콘텐츠가 적용 가능한 법률이나 이용 약관을 위반하지 않는지 확인할 책임은 여러분에게 있어요.

V3 임베딩 모델이 최근 이벤트를 알고 있나요?

아니요. text-embedding-3-large와 text-embedding-3-small 모델은 2021년 9월 이후의 이벤트에 대한 지식이 없어요. 텍스트 생성 모델만큼 큰 제약은 아니지만, 특정 엣지 케이스에서는 성능을 낮출 수 있어요.

더 알아보기 (Learn more)

관련 문서: 프롬프트 캐싱, 모델 선택, 임베딩 API 레퍼런스를 함께 보면 도움이 돼요.