🤖 AI/ML

Sentence Transformer

SBERT (Sentence-BERT)

문장 전체를 고정 크기의 밀집 벡터(Dense Vector)로 변환하는 임베딩 모델입니다. 시맨틱 검색, 문장 유사도 계산, 클러스터링, RAG 파이프라인 구축에 널리 활용됩니다.

📖 상세 설명

Sentence Transformer는 2019년 Reimers와 Gurevych가 발표한 "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks" 논문에서 제안된 문장 임베딩 프레임워크입니다. 기존 BERT 모델은 문장 유사도를 계산하려면 두 문장을 함께 입력해야 해서 N개 문장의 모든 쌍을 비교하는 데 O(N^2) 시간이 걸렸습니다. Sentence Transformer는 각 문장을 독립적으로 임베딩하여 코사인 유사도로 빠르게 비교할 수 있게 만들어, 대규모 시맨틱 검색을 실용적으로 가능하게 했습니다.

Sentence Transformer의 핵심 아키텍처는 Siamese/Triplet Network 구조입니다. 동일한 BERT(또는 다른 Transformer) 모델을 공유하면서, 두 문장이 유사하면 임베딩이 가까워지고 다르면 멀어지도록 학습합니다. 학습에는 NLI(Natural Language Inference) 데이터셋과 STS(Semantic Textual Similarity) 벤치마크가 주로 사용됩니다. 출력 임베딩은 [CLS] 토큰 또는 Mean Pooling을 통해 생성되며, 일반적으로 384~1024 차원의 밀집 벡터입니다.

현재 가장 널리 사용되는 모델은 all-MiniLM-L6-v2all-mpnet-base-v2입니다. MiniLM 모델은 6개 레이어, 384차원 임베딩으로 매우 빠르며(CPU에서 초당 5,000~14,000 문장), 품질도 실용적인 수준입니다. MPNet 모델은 12개 레이어, 768차원으로 MTEB(Massive Text Embedding Benchmark) 리더보드에서 최고 수준의 품질을 제공하지만 4~5배 느립니다. 공식 문서에 따르면 "mpnet이 최고 품질, MiniLM이 5배 빠르면서도 좋은 품질"이라고 설명합니다.

Sentence Transformer의 주요 활용 사례는 시맨틱 검색(키워드가 아닌 의미 기반 검색), RAG(Retrieval-Augmented Generation) 파이프라인의 Retriever, 중복 문서 탐지, 클러스터링(뉴스 기사 그룹화 등), 추천 시스템입니다. LangChain, LlamaIndex 등 LLM 프레임워크에서 기본 임베딩 모델로 자주 사용되며, FAISS, Pinecone, Weaviate 등 벡터 데이터베이스와 결합하여 수억 건의 문서에서도 밀리초 단위 검색이 가능합니다.

💻 코드 예제

Sentence Transformer를 사용한 문장 임베딩 및 유사도 계산 예제입니다.

# pip install sentence-transformers
from sentence_transformers import SentenceTransformer, util
import numpy as np

# 모델 로드 (첫 실행 시 자동 다운로드)
# 빠른 모델: all-MiniLM-L6-v2 (384차원, 빠름)
# 고품질 모델: all-mpnet-base-v2 (768차원, 느리지만 정확)
model = SentenceTransformer('all-MiniLM-L6-v2')

# 문장 리스트
sentences = [
    "인공지능이 세상을 바꾸고 있습니다.",
    "AI 기술이 우리의 삶을 혁신하고 있습니다.",
    "오늘 날씨가 매우 좋습니다.",
    "머신러닝은 데이터에서 패턴을 학습합니다.",
]

# 문장 임베딩 생성
embeddings = model.encode(sentences, convert_to_tensor=True)
print(f"임베딩 shape: {embeddings.shape}")  # (4, 384)

# 코사인 유사도 계산
cosine_scores = util.cos_sim(embeddings, embeddings)
print("\n유사도 행렬:")
print(cosine_scores.numpy().round(3))

# 쿼리와 가장 유사한 문장 찾기
query = "딥러닝 기술의 발전"
query_embedding = model.encode(query, convert_to_tensor=True)

# 모든 문장과의 유사도 계산
similarities = util.cos_sim(query_embedding, embeddings)[0]

# 결과 정렬 및 출력
print(f"\n쿼리: '{query}'")
print("유사한 문장 순위:")
for idx in similarities.argsort(descending=True):
    print(f"  {similarities[idx]:.3f}: {sentences[idx]}")

대규모 시맨틱 검색 (FAISS 연동)

# pip install sentence-transformers faiss-cpu
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

# 대규모 문서 코퍼스 (예시)
corpus = [
    "Python은 인기 있는 프로그래밍 언어입니다.",
    "JavaScript는 웹 개발에 필수적입니다.",
    "딥러닝은 신경망을 사용한 머신러닝 기법입니다.",
    "자연어 처리는 텍스트 데이터를 분석합니다.",
    "컴퓨터 비전은 이미지를 이해하는 AI 분야입니다.",
    # ... 수백만 개의 문서
]

# 코퍼스 임베딩 (한 번만 실행, 저장해두고 재사용)
corpus_embeddings = model.encode(corpus, convert_to_numpy=True)
corpus_embeddings = corpus_embeddings.astype('float32')

# FAISS 인덱스 생성 (L2 거리 기반)
dimension = corpus_embeddings.shape[1]  # 384
index = faiss.IndexFlatIP(dimension)  # Inner Product (코사인 유사도용 정규화 필요)

# 정규화 후 인덱스에 추가
faiss.normalize_L2(corpus_embeddings)
index.add(corpus_embeddings)

# 검색 함수
def search(query: str, top_k: int = 3):
    query_embedding = model.encode([query], convert_to_numpy=True).astype('float32')
    faiss.normalize_L2(query_embedding)

    scores, indices = index.search(query_embedding, top_k)

    results = []
    for score, idx in zip(scores[0], indices[0]):
        results.append({
            'document': corpus[idx],
            'score': float(score)
        })
    return results

# 검색 테스트
query = "AI가 이미지를 분석하는 방법"
results = search(query, top_k=3)

print(f"검색 쿼리: '{query}'")
for i, result in enumerate(results, 1):
    print(f"{i}. [{result['score']:.3f}] {result['document']}")

커스텀 모델 파인튜닝

# pip install sentence-transformers datasets
from sentence_transformers import SentenceTransformer, InputExample, losses
from sentence_transformers.evaluation import EmbeddingSimilarityEvaluator
from torch.utils.data import DataLoader

# 기본 모델 로드
model = SentenceTransformer('all-MiniLM-L6-v2')

# 학습 데이터 준비 (문장 쌍 + 유사도 점수)
train_examples = [
    InputExample(texts=["문장 A1", "문장 B1"], label=0.9),  # 매우 유사
    InputExample(texts=["문장 A2", "문장 B2"], label=0.1),  # 다름
    # ... 더 많은 데이터
]

train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)

# 손실 함수: CosineSimilarityLoss (연속 점수용)
train_loss = losses.CosineSimilarityLoss(model)

# 평가 데이터 (선택적)
evaluator = EmbeddingSimilarityEvaluator.from_input_examples(
    train_examples[:100],
    name='dev'
)

# 파인튜닝
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=3,
    warmup_steps=100,
    evaluator=evaluator,
    evaluation_steps=500,
    output_path='./my-finetuned-model'
)

# 파인튜닝된 모델 로드
model = SentenceTransformer('./my-finetuned-model')

📊 성능 & 비용

Sentence Transformers는 오픈소스 무료입니다. Apache 2.0 라이선스로 상업적 사용이 가능합니다.

모델 차원 속도 (CPU) MTEB 점수 특징
all-MiniLM-L6-v2 384 ~14,000 sent/s ~56 5x 빠름
all-MiniLM-L12-v2 384 ~7,500 sent/s ~57 L6보다 약간 정확
all-mpnet-base-v2 768 ~2,800 sent/s ~59 최고 품질
paraphrase-MiniLM-L6-v2 384 ~14,000 sent/s ~55 패러프레이즈 특화
multi-qa-MiniLM-L6-cos-v1 384 ~14,000 sent/s ~53 QA 검색 특화
distiluse-base-multilingual-cased-v2 512 ~4,000 sent/s ~51 다국어 (50+ 언어)

* 속도는 CPU(i7) 기준, GPU 사용 시 10배+ 빨라짐

용도별 추천 모델

용도 추천 모델 이유
프로덕션 시맨틱 검색 all-MiniLM-L6-v2 속도/품질 균형, 낮은 메모리
최고 정확도 필요 all-mpnet-base-v2 MTEB 최고 점수
한국어 지원 distiluse-base-multilingual 다국어 지원
RAG 파이프라인 multi-qa-MiniLM-L6 질문-문서 매칭 특화
중복 탐지 paraphrase-MiniLM-L6 패러프레이즈 인식 특화

🗣️ 실무에서 이렇게 말하세요

올바른 표현

"검색 품질보다 속도가 중요하면 MiniLM, 정확도가 더 중요하면 mpnet 모델을 쓰세요. MiniLM이 5배 빠르지만 품질 차이는 크지 않아요."

올바른 표현

"RAG 시스템에서 chunk 임베딩할 때 Sentence Transformer 써서 FAISS에 넣으면 밀리초 단위로 검색됩니다."

부정확한 표현

"Sentence Transformer로 GPT처럼 텍스트 생성할 수 있어요." → Sentence Transformer는 임베딩 전용 모델입니다. 텍스트 생성은 못 합니다.

⚠️ 흔한 실수 & 주의사항

  • Max Sequence Length 확인: 대부분의 모델은 256~512 토큰이 최대입니다. 긴 문서는 chunking 후 임베딩하세요.
  • 벤치마크 ≠ 실제 성능: MTEB 점수가 높아도 내 도메인에서 성능이 다를 수 있습니다. 반드시 자체 데이터로 평가하세요.
  • 임베딩 정규화: 코사인 유사도를 쓰려면 벡터를 L2 정규화해야 합니다. FAISS IndexFlatIP 사용 시 필수입니다.
  • 다국어 모델 선택: 한국어가 포함된 경우 multilingual 모델을 사용하세요. 영어 전용 모델은 한국어 성능이 매우 낮습니다.
  • 배치 처리: encode() 호출 시 문장 리스트를 한 번에 전달하세요. 개별 호출은 매우 비효율적입니다.
  • GPU 메모리: 대량 임베딩 시 batch_size를 조절하세요. OOM 에러가 나면 batch_size를 줄이세요.
  • 모델 캐싱: 모델 로드는 한 번만 하고 재사용하세요. 매번 로드하면 수 초의 오버헤드가 발생합니다.

🔗 관련 용어

📚 더 배우기