2024년 6월 2일 일요일

대형언어모델 검색증강생성의 핵심기술, 벡터 데이터베이스 Chroma 분석하기

이 글은 대형언어모델(LLM. Large Language Model)의 검색증강생성(RAG. Retrieval-Augmented Generation) 구현 시 핵심기술인 임베딩 벡터 데이터베이스(Embedding vector database)로 유명한 Chroma 핵심 구조를 간략히 분석한다. RAG는 생성AI의 환각현상을 줄여주어, 전문가적인 정보를 생성하는 데 도움을 준다. 

Chroma의 동작 방식을 이해하여, LLM 기술 개발 시 이해도와 응용력를 높일 수 있다. 

이 글에 관련된 용어와 상세 개념은 다음 링크를 참고한다. 
개요
Chroma(크로마)는 AI 지원 오픈소스 벡터 베이터베이스이다. RAG 처리할 때 필수적으로 사용되는 데이터베이스 중 하나이다. 크로마를 이용해 LLM 기반 다양한 앱(지식 서비스 등)을 개발할 수 있다. 
크로마는 임베딩 벡터를 메타데이터와 함께 저장하고, 질의를 통해 해당 임베딩 도큐먼트를 검색할 수 있다. 크로마는 서버로써 동작될 수 있다(데모). 

설치 및 사용
크로마 설치는 다음과 같다. 
pip install chromadb

벡터 데이터베이스에 저장되는 단위는 다음과 같다. 
collection = client.create_collectoin(name='test', embedding_function=emb_fn)

collection.add(
    embeddings=[
        [1.1, 2.3, 3.2],
        [4.5, 6.9, 4.4],
        [1.1, 2.3, 3.2]
    ],
    metadatas=[
        {"uri": "img1.png", "style": "style1"},
        {"uri": "img2.png", "style": "style2"},
        {"uri": "img3.png", "style": "style1"}
    ],
    documents=["doc1", "doc2", "doc3"],
    ids=["id1", "id2", "id3"],
)

보는 것 같이, 벡터 좌표계에 위치할 임베딩 벡터, 벡터에 매달아 놓을 메타데이터와 도큐먼트, ID를 하나의 컬랙션 단위로 저장한다. 이를 통해, 벡터 간 유사도, 거리 등을 계산해, 원하는 도큐먼트, 메타데이터 등을 얻을 수 있다. 이때 임베딩 벡터는 미리 학습된 임베딩 모델을 사용할 수 있다. 

질의해 원하는 벡터를 얻으려면, 벡터 공간에서 거리계산이 필수적이다. 이때 사용하는 함수는 다음과 같다. 

컬렉션에 벡터 추가와 질의는 다음과 같다. 
collection.add(
    documents=["doc1", "doc2", "doc3", ...],
    embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2], ...],
    metadatas=[{"chapter": "3", "verse": "16"}, {"chapter": "3", "verse": "5"}, {"chapter": "29", "verse": "11"}, ...],
    ids=["id1", "id2", "id3", ...]
)

collection.query(
    query_texts=["doc10", "thus spake zarathustra", ...],
    n_results=10,
    where={"metadata_field": "is_equal_to_this"},
    where_document={"$contains":"search_string"}
)

여기서, where의 metadata_field를 이용해 다음과 같은 조건 비교 연산이 가능하다.
  $eq, $ne, $gt, $gte, $lt, $lte
 
그리고, 논리 연산자인 $and, $or를 지원한다.

크로마는 향후, 워크플로우, 가시화, 질의 계획, 분석 기능을 준비하고 있다. 

구조 분석
크로마 구조에서 핵심은 임베딩 벡터 모델과 RAG에서 사용되는 query이다. 벡터 데이터를 저장하고 관리하는 방법은 일반적으로 많이 알려져 있는 방식이나, query는 RAG에 특화되도록 개발된 방법을 사용한다.  
Chroma 구조

위에서 SegmentAPI의 구조를 살펴보면, 베이스 클래스가 Collection인 것을 알 수 있다. 이 클래스의 query가 호출되면, 입력된 데이터와 가장 유사한 집합이 리턴된다. 구현은 VectorReader를 통해 이뤄진다. 

다음은 쿼리의 핵심 코드이다. 
coll = _get_collection(collection_id)                   // 컬랙션 획득
query = t.VectorQuery(vectors, k, allowed_ids)  
results = vector_reader.query_vectors(query)       // 저장된 데이터셋에 쿼리 질의
return QueryResult(ids, distance, metadatas, embeddings, documents)   // 결과 리턴

두 벡터 간 유사도 거리 계산을 통해 가까운 벡터 데이터를 리턴하는 알고리즘이 query_vectors 에 구현되어 있다. 

예를 들어 LocalHnswSegment를 확인해보면, 다음 코드를 발견할 수 있다. 
    def query_vectors(
        self, query: VectorQuery
    ) 
        query_vectors = query["vectors"]
        with ReadRWLock(self._lock):
            result_labels, distances = self._index.knn_query(
                query_vectors, k=k, filter=filter_function if ids else None
            )

            all_results: List[List[VectorQueryResult]] = []
            for result_i in range(len(result_labels)):
                results: List[VectorQueryResult] = []
                for label, distance in zip(
                    result_labels[result_i], distances[result_i]
      
이 부분은 query에 담긴 vectors 값을 얻어 KNN 기반 거리 비교로 벡터를 계산하는 방법을 보여준다. 이는 이미 임베딩 벡터 처리할 때, 개념적으로 가까운 데이터는 벡터 값이 가까워지도록 학습된 임베딩 모델을 사용했기 때문에 가능한 것이다. 

다음은 Documents에 포함된 데이터의 임베딩 공간에서 거리를 보여준다. 
마무리
앞서 분석한 바와 같이, 크로마의 핵심은 동일한 임베딩 모델로 계산된 임베딩 벡터들을 저장하고, 텍스트(혹은 이미지) 질의 시 유사도 거리함수를 통해 관련된 임베딩 벡터를 빠르게 검색하는 기능에 있다. 검색된 임베딩 벡터 데이터는 LLM 모델에 사전 프롬프트로 주입됨으로써 환각현상을 막고, 전문적인 답변을 생성한다.

크로마는 해커 기질과 철학이 섞여 있는 Jeff HuberAnton Troynikov가 공동 개발하였다. 이들은 이전에 3D computer vision, 로보틱스 분야에 일했던 경험이 있다. 현재, 크로마는 1,800만달러를 펀딩받았고, 다음 라운드를 준비하고 있다. 
공동 개발자 Jeff Huber, Anton Troynikov

레퍼런스

댓글 없음:

댓글 쓰기