Mistral Embeddings API

Mistral Embeddings API

Mistral Embeddings API의 기초를 다루는 가이드예요. 텍스트 임베딩을 생성하고, 임베딩 간 거리를 측정하며, 주요 사용 사례인 군집화(clustering)와 분류(classification)를 살펴봐요.

출처: 문서

본문

**임베딩(Embeddings)**은 텍스트의 벡터 표현으로, 단락이 고차원 벡터 공간에서의 위치를 통해 의미를 포착해요. Mistral Embeddings API는 텍스트에 대해 최첨단의 임베딩을 제공하며, 많은 NLP 작업에 쓸 수 있어요. 이 가이드에서는 Mistral embeddings API의 기본, 텍스트 임베딩 간 거리 측정 방법, 주요 사용 사례(군집화·분류)를 다룰게요.

Mistral Embeddings API

Mistral의 embeddings API로 텍스트 임베딩을 생성하려면 API 엔드포인트에 요청을 보내고 임베딩 모델 mistral-embed를 지정한 뒤, 입력 텍스트 리스트를 제공하면 돼요. API는 대응하는 임베딩을 숫자 벡터로 반환하고, 이는 NLP 애플리케이션에서 추가 분석·처리에 쓸 수 있어요.

!pip install mistralai seaborn numpy scikit-learn
import os
from mistralai.client import Mistral

api_key = os.environ["MISTRAL_API_KEY"]
model = "mistral-embed"

client = Mistral(api_key=api_key)

embeddings_batch_response = client.embeddings.create(
    model=model,
    inputs=["Embed this sentence.", "As well as this one."],
)

출력은 임베딩과 토큰 사용량 정보를 담은 EmbeddingResponse 객체예요.

Data(id='eb4c2c739780415bb3af4e47580318cc', object='list', data=[EmbeddingObject(object='embedding', embedding=[-0.0165863037109375,...], index=0), Data(object='embedding', embedding=[-0.0234222412109375,...], index=1)], model='mistral-embed', usage=EmbeddingResponseUsage(prompt_tokens=15, total_tokens=15, completion_tokens=0))

첫 임베딩의 길이를 확인해 볼게요:

len(embeddings_batch_response.data[0].embedding)

1024가 반환돼요 — 우리 임베딩의 차원이 1024라는 뜻이에요. mistral-embed 모델은 텍스트 길이와 무관하게 각 텍스트 문자열에 대해 차원 1024의 임베딩 벡터를 생성해요. 차원이 높을수록 텍스트 정보를 더 잘 포착하고 NLP 작업 성능을 높일 수 있지만, 호스팅·추론에 더 많은 계산 리소스가 필요하고, 임베딩 저장·처리에 지연 시간과 메모리 사용이 늘 수 있어요. 이 성능-리소스 트레이드오프는 텍스트 임베딩에 의존하는 NLP 시스템을 설계할 때 고려해야 해요.

거리 측정 (Distance measures)

텍스트 임베딩의 세계에서는 의미나 맥락이 비슷한 텍스트들이 벡터 간 거리로 측정했을 때 이 공간에서 서로 더 가까이 위치하는 경향이 있어요. 모델이 훈련 과정에서 의미적으로 관련된 텍스트들을 함께 묶도록 학습했기 때문이에요.

간단한 예제를 살펴볼게요. 텍스트 임베딩 작업을 단순화하기 위해 임베딩 API를 이 함수로 감쌀 수 있어요:

from sklearn.metrics.pairwise import euclidean_distances

def get_text_embedding(inputs):
    embeddings_batch_response = client.embeddings.create(
        model=model,
        inputs=inputs
    )
    return embeddings_batch_response.data[0].embedding

고양이에 대한 문장과 책에 대한 문장 두 개가 있다고 가정해 봐요. 각 문장이 참조 문장 "Books are mirrors: You only see in them what you already have inside you"과 얼마나 비슷한지 찾고 싶어요. 참조 문장 임베딩과 책 문장 임베딩 간의 거리는, 참조 문장 임베딩과 고양이 문장 임베딩 간의 거리보다 작다는 것을 볼 수 있어요.

sentences = [
    "A home without a cat — and a well-fed, well-petted and properly revered cat — may be a perfect home, perhaps, but how can it prove title?",
    "I think books are like people, in the sense that they'll turn up in your life when you most need them"
]
embeddings = [get_text_embedding([t]) for t in sentences]
reference_sentence = "Books are mirrors: You only see in them what you already have inside you"
reference_embedding = get_text_embedding([reference_sentence])
for t, e in zip(sentences, embeddings):
    distance = euclidean_distances([e], [reference_embedding])
    print(t, distance)

위 예제에서는 임베딩 벡터 간 거리로 유클리드 거리(Euclidean distance)를 사용했어요(Mistral 임베딩은 norm이 1이므로 코사인 유사도, 내적, 유클리드 거리가 모두 동등해요).

패러프레이즈 탐지 (Paraphrase detection)

또 다른 잠재적 사용 사례는 패러프레이즈 탐지예요. 이 간단한 예제에서는 세 문장의 리스트가 있고, 두 문장이 서로의 패러프레이즈인지 찾고 싶어요. 두 문장 임베딩 간 거리가 작으면 두 문장이 의미적으로 비슷하고 잠재적 패러프레이즈가 될 수 있음을 시사해요.

결과는 처음 두 문장이 의미적으로 비슷하고 잠재적 패러프레이즈일 수 있다는 것을 시사해요. 반면 세 번째 문장은 더 다르고요. 이것은 아주 간단한 예제지만, 이 접근법은 소셜 미디어 게시물, 뉴스 기사, 고객 리뷰에서 패러프레이즈를 탐지하는 것 같은 실세계 애플리케이션의 더 복잡한 상황으로 확장할 수 있어요.

sentences = [
    'Have a safe happy Memorial Day weekend everyone',
    'To all our friends at Whatsit Productions Films enjoy a safe happy Memorial Day weekend',
    'Where can I find the best cheese?'
]
sentence_embeddings = [get_text_embedding([t]) for t in sentences]
import itertools

sentence_embeddings_pairs = list(itertools.combinations(sentence_embeddings, 2))
sentence_pairs = list(itertools.combinations(sentences, 2))
for s, e in zip(sentence_pairs, sentence_embeddings_pairs):
    print(s, euclidean_distances([e[0]], [e[1]]))

배치 처리 (Batch processing)

Mistral Embeddings API는 효율과 속도를 위해 텍스트를 배치로 처리하도록 설계됐어요. 이 예제에서는 Kaggle의 Symptom2Disease 데이터셋을 로드해서 보여줄게요. 이 데이터셋은 1200행에 "label"(질병 범주)과 "text"(질병과 연관된 증상 설명) 두 열이 있어요.

데이터를 청크로 나누고 각 청크를 Mistral 임베딩 API에 보내 임베딩을 얻는 get_embeddings_by_chunks 함수를 작성했어요. 그런 다음 임베딩을 데이터프레임의 새 열로 저장했어요. 참고로 Mistral Embeddings API는 앞으로 **자동 청킹(auto-chunking)**을 제공할 예정이라, 사용자가 API에 보내기 전에 데이터를 수동으로 나눌 필요가 없어질 거예요.

import pandas as pd
df = pd.read_csv("https://raw.githubusercontent.com/mistralai/cookbook/main/data/Symptom2Disease.csv", index_col=0)
def get_embeddings_by_chunks(data, chunk_size):
    chunks = [data[x : x + chunk_size] for x in range(0, len(data), chunk_size)]
    embeddings_response = [
        client.embeddings.create(model=model, inputs=c) for c in chunks
    ]
    return [d.embedding for e in embeddings_response for d in e.data]
df['embeddings'] = get_embeddings_by_chunks(df['text'].tolist(), 50)
df.head()

t-SNE 임베딩 시각화

앞서 우리 임베딩은 1024차원이라 직접 시각화할 수 없다고 말했어요. 그래서 임베딩을 시각화하려면 t-SNE 같은 차원 축소 기법을 사용해 더 쉽게 시각화할 수 있는 저차원 공간으로 투영해야 해요.

이 예제에서는 임베딩을 2차원으로 변환하고, 서로 다른 질병들의 임베딩 간 관계를 보여주는 2D 산점도를 만들어요.

import seaborn as sns
from sklearn.manifold import TSNE
import numpy as np


tsne = TSNE(n_components=2, random_state=0).fit_transform(np.array(df['embeddings'].to_list()))
ax = sns.scatterplot(x=tsne[:, 0], y=tsne[:, 1], hue=np.array(df['label'].to_list()))
sns.move_legend(ax, 'upper left', bbox_to_anchor=(1, 1))

fasttext와 비교

인기 있는 오픈소스 임베딩 모델인 fastText와 비교할 수 있어요. 그러나 t-SNE 임베딩 플롯을 살펴보면 fastText 임베딩은 일치하는 라벨의 데이터 포인트 사이에 명확한 분리를 만들지 못한다는 것을 알 수 있어요.

!pip install fasttext
import fasttext.util
fasttext.util.download_model('en', if_exists='ignore')  # English
ft = fasttext.load_model('cc.en.300.bin')

df['fasttext_embeddings'] = df['text'].apply(lambda x: ft.get_word_vector(x).tolist())

tsne = TSNE(n_components=2, random_state=0).fit_transform(np.array(df['fasttext_embeddings'].to_list()))
ax = sns.scatterplot(x=tsne[:, 0], y=tsne[:, 1], hue=np.array(df['label'].to_list()))
sns.move_legend(ax, 'upper left', bbox_to_anchor=(1, 1))

분류 (Classification)

텍스트 임베딩은 분류·군집화 같은 머신러닝 모델의 입력 특징으로 쓸 수 있어요. 이 예제에서는 분류 모델로 질병 설명 텍스트의 임베딩에서 질병 라벨을 예측해요.

# Create a train / test split

from sklearn.model_selection import train_test_split

train_x, test_x, train_y, test_y = train_test_split(df['embeddings'], df["label"],test_size=0.2)
# Normalize features
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
train_x = scaler.fit_transform(train_x.to_list())
test_x = scaler.transform(test_x.to_list())
# Train a classifier and compute the test accuracy

from sklearn.linear_model import LogisticRegression

# For a real problem, C should be properly cross validated and the confusion matrix analyzed
clf = LogisticRegression(random_state=0, C=1.0, max_iter=500).fit(train_x, train_y.to_list()) 

# you can also try the sag algorithm:
# clf = LogisticRegression(random_state=0, C=1.0, max_iter=1000, solver='sag').fit(train_x, train_y)

print(f"Precision: {100*np.mean(clf.predict(test_x) == test_y.to_list()):.2f}%")
# Classify a single example
text = "I've been experiencing frequent headaches and vision problems."
clf.predict([get_text_embedding([text])])

fasttext와 비교

추가로 이 분류 작업에서 fastText 임베딩을 사용한 성능을 살펴볼게요. fastText 임베딩보다 Mistral 임베딩 모델을 사용할 때 분류 모델이 더 나은 성능을 달성하는 것으로 보여요.

# Create a train / test split
from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y = train_test_split(df['fasttext_embeddings'], df["label"],test_size=0.2)
# Normalize features
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
train_x = scaler.fit_transform(train_x.to_list())
test_x = scaler.transform(test_x.to_list())
# Train a classifier and compute the test accuracy
from sklearn.linear_model import LogisticRegression
# For a real problem, C should be properly cross validated and the confusion matrix analyzed
clf = LogisticRegression(random_state=0, C=1.0, max_iter=500).fit(train_x, train_y.to_list()) 
# you can also try the sag algorithm:
# clf = LogisticRegression(random_state=0, C=1.0, max_iter=1000, solver='sag').fit(train_x, train_y)
print(f"Precision: {100*np.mean(clf.predict(test_x) == test_y.to_list()):.2f}%")

군집화 (Clustering)

질병 라벨이 없다면 어떨까요? 데이터에서 인사이트를 얻는 한 가지 접근법은 군집화예요. 군집화는 특정 특징에 대한 유사성에 따라 비슷한 데이터 포인트를 함께 묶는 비지도 머신러닝 기법이에요. 텍스트 임베딩 맥락에서는 각 임베딩 간 거리를 유사성의 척도로 사용해, 고차원 공간에서 서로 가까운 임베딩을 가진 데이터 포인트를 함께 묶을 수 있어요.

이미 24개의 군집이 있다는 걸 알고 있으니, 24개 군집의 K-means 군집화를 사용해 볼게요. 그런 다음 몇 가지 예제를 검사해 단일 군집 안의 예제들이 서로 비슷한지 확인할 수 있어요. 예를 들어 군집 23의 처음 세 행을 살펴보면 증상 측면에서 매우 비슷해 보여요.

from sklearn.cluster import KMeans
model = KMeans(n_clusters=24, max_iter=1000)
model.fit(df['embeddings'].to_list())
df["cluster"] = model.labels_
print(*df[df.cluster==23].text.head(3), sep='\n')

검색 (Retrieval)

우리 임베딩 모델은 검색을 염두에 두고 훈련됐기 때문에 검색 작업에 탁월해요. 임베딩은 또한 응답을 생성하기 위해 지식 베이스에서 검색한 관련 정보를 사용하는 RAG(Retrieval-Augmented Generation) 시스템을 구현하는 데도 매우 유용해요. 높은 수준에서 보면, 로컬 디렉터리·텍스트 파일·내부 위키든 지식 베이스를 텍스트 임베딩으로 임베딩해 벡터 데이터베이스에 저장해요. 그런 다음 사용자 쿼리를 바탕으로, 지식 베이스의 관련 정보를 나타내는 가장 유사한 임베딩을 검색해요. 마지막으로 이 관련 임베딩을 대형 언어 모델에 공급해 사용자 쿼리와 맥락에 맞는 응답을 생성해요. RAG 시스템이 어떻게 동작하는지와 기본 RAG를 구현하는 방법에 관심이 있다면 이전 가이드를 참고하세요.

더 알아보기 (Learn more)