Vector DB 를 사용한 LLM RAG 실습 리뷰

Vector DB 를 사용한 LLM RAG 실습과정을 리뷰합니다. Vector를 구성하는 기본 embedding의 정의를 다루고 Vector DB에서 유사성에 기반한 검색 알고리즘을 소개합니다. 아울러 이를 통한 RAG과정을 리뷰하고 prompt engineering과의 비교합니다.

Vector DB 기본-어떻게 하면 데이터를 Vector로 표현할 수 있을까


벡터 embedding은 데이터에서 “의미”를 끄집어 내는 일입니다.
아울러 이러한 의미를 컴퓨터에서 처리할 수 있도록 이른바 기계가 읽을 수 있는 형태로 변환하는 일을 말합니다. (Machine Readable format)

Vector DB 의 기본은 embeddings


이른바 Vector라는 것의 수학적인 의미를 이해해야 합니다.
서울에서 부산으로 여행을 간다고 가정해 봅시다. 지도상에 서울에서 부산까지의 화살표를 그릴 수 있습니다.
그러면 동쪽으로 몇 Km, 그리고 남쪽으로 몇 Km 떨어진 부산까지의 화살표를 상상할 수 있습니다.
이 화살표가 하나의 벡터가 되는 셈입니다. 그리고 그 크기는 서로 겹치지 않는 동쪽과 남쪽의 방향으로 얼마정도 떨어진 거리를 나타내면 서울에서 부산까지의 여정을 수학적으로 표현하게 됩니다.
가장 쉬운 예는 우리가 경험하는 3차원에서 공간을 표현하는 (x,y,z) 좌표를 예로 들수 있고, 지리정보인 위도, 경도 그리고 고도를 예를 들 수 있겠습니다. 가장 중요한 점은 이 세가지의 숫자가 대상 하나의 모든 정보를 표현할 수 있다는 점과 각 차원의 변화는 서로 다른 차원에 영향을 끼치지 않는다는 점입니다.
실습 에서는 두 가지 use case를 사용합니다.
하나는 유명한 MNIST dataset의 손글씨로 쓴 숫자들을 2차원 vector로 표현해 봅니다.
다른 하나는 임의의 문장을 transformer 기반의 모델을 사용하여 embedding, 즉 문장을 벡터로 표현해 봅니다.

이미지를 embedding 해보기


MNIST의 데이터는 손으로 쓴 숫자들에 대한 이미지를 가지고 있는 데이터 셋 입니다.
일반적인 머신 러닝 실습에 많이 사용 됩니다.
숫자 하나를 2차원 벡터로 변환하는 일은 머신 러닝에 기반한 fitting 작업으로 수행되며, 이를 위한 네트워크는 아래와 같이 구성합니다.

이미지 임베딩을 위한 인코더 디코더 구조
이미지 임베딩을 위한 인코더 디코더 구조

결과로 나온 파라미터들을 사용하여 Encoder 기능만을 수행하여 모든 image 들을 2차원 벡터로 변환한 후 모든 데이터 셋들을 fitting해 보면 아래와 같은 그림을 얻을 수 있습니다.

embedding plot
embedding plot

실제로 dataset 내부의 해당 image 들이 어떻게 x,y좌표의 두가지 성분으로 분해될 수 있는지 직관적인 설명은 불가합니다. 왜냐하면 image의 특성을 2차원 vector로 축소 하기까지 많은 재귀적이고 비선형적인 수학적 변환과정을 거치기 때문입니다.

일반적으로 우리가 직관적으로 이해할 수 있는 잘 알려진 함수가 아니기 때문에 직관적인 접근이 어렵고 해당 함수를 정의하는 주요한 parameter들도 일반적인 대수학의 법칙에 따라 결정된 것이 아니고 주어진 오차를 최소하하는 방향으로 결정된 숫자들의 모임이기 때문입니다.


문장을 벡터로 표현해 보기


이 실습 에서는
‘The team enjoyed the hike through the meadow’,
‘The national park had great views’,
‘Olive oil drizzled over pizza tastes delicious’
의 문장을 벡터로 표현해 보도록 하겠습니다. Vector 표현을 위해 다음의 code 를 사용합니다.

from sentence_transformers import SentenceTransformer
model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
sentence = ['The team enjoyed the hike through the meadow',
            'The national park had great views',
            'Olive oil drizzled over pizza tastes delicious']
embedding = model.encode(sentence)

이 코드는 sentence_transformers 라이브러리에서 SentenceTransformer클래스를 가져 옵니다.
이 라이브러리는 문장 혹은 단락을 의미를 담은 숫자 벡터로 변환하는 기능을 제공합니다.
아울러 위에 사용된 “paraphrase-MiniLM-L6-v2” 모델은 문장 유사도 및 비슷한 의미를 가진 다른 표현을 가진 문장들의 식별작업에 효과적이라고 알려진 사전 훈련된 모델입니다.

Vector DB 내부 비슷한 vector 검색하기

무식하게 검색하기


현재 대상으로 삼고 있는 데이터 셋은 일단 첫 번째로 embedding 해 본 이미지와 문장 들입니다.
질의로 입력받은 데이터를 벡터화 하고 변환된 벡터와 기존에 데이터로 있는 모든 벡터들의 L2거리를 계산합니다.
여기에 사용 되는 L2 거리는 유클리디안 등 여러가지 방식으로 정의된 거리를 사용할 수 있습니다.
계산된 모든 거리를 크기대로 정렬합니다.
정렬된 거리에서 상위 K개만큼의 벡터를 반환합니다.
이렇게 해서 얻어진 벡터들의 모임은 이른바 “의미” 혹은 “문맥”이라는 관점에서 가장 비슷한 데이터 셋이라고 이야기 할 수 있습니다.
단점은 상위 K 벡터들을 얻기 위해서는 질의로 입력받은 벡터와 비교를 하기 위한 데이터 벡터를 모두 비교해야 하기 때문에 데이터 벡터의 개수가 증가하거나 차원이 증가하는 경우 그만큼 많은 계산시간이 필요하다는 점입니다.

근사적인 Nearest Neighbors

위에서 본 것처럼 직접적이고 연관된 모든 Vector들에 대해 유사성을 계산하는 일은 확장가능하지 않습니다. 따라서 이러한 문제점을 극복하기 위하여 제안된 솔루션이 바로 ANN입니다.
ANN은 이른바 “Small world’ 현상에 기반하고 있습니다. 인간사회의 사회 관계망은 모두가 밀접하게 연결되어 있다라는 특징을 가지고 있습니다.
임의의 두명을 선정 하더라도 서로 연관이 되어 있는 사회관계망의 연결을 통해 6다리정도 따라가면 서로 관련이 있음을 알 수 있습니다.

ANN – Approximate nearest neighbor


각각의 벡터 들을 일정한 수의 가까운 이웃쌍과 미리 연결해 둡니다. 연결을 하는 규칙은 해당 벡터와 가장 가까운 벡터 들을 정해진 수 만큼만 연결을 하는 규칙으로 연결합니다.
벡터간의 거리에 따라서 어떤 벡터는 위의 정해진 수 만큼만 연결이 되어 있을수도 있고, 인기가 많은 벡터는 (연결이 많은) 정해진 수 이상으로 가까운 이웃들을 가지고 있을 수 있습니다.
벡터간의 거리를 계산할 때 무대뽀로 모든 벡터 들을 계산하는 것이 아니라 위의 방식대로 이미 설정되어 있는 벡터들의 공간에서 임의의 벡터를 선정합니다.
선정된 벡터에 연결되어 있는 벡터 들에 대해 거리를 각각 계산합니다. 계산 회수는 해당 벡터에 연결되어 있는 이웃 벡터 들의 개수만큼 계산합니다.
최종적으로 도달한 벡터의 가장 가까운 이웃 벡터 들을 대상으로 거리를 계산하고 계산 값을 근거로 가까운 상위 K 벡터 들을 얻어 냅니다.

HNSW (Hierarchical Navigable Small World)

HNSW는 위에 설명한 ANN을 확장한 개념입니다.
먼저 벡터들이 위치하는 공간을 layer 로 나눕니다. 그리고 layer 들을 구성할 때 그 밀도를 낮은 것에서 높은 것으로 여러 계층으로 구분을 합니다.
가장 상위 레이어에는 몇개의 벡터만 존재하고 다음 층으로 내려갈수록 그 위층에 있던 벡터들에 새로운 벡터들을 조금씩 추가하여 여러개의 레이어를 만듭니다.
맨 아래의 레이어에는 대상이 되는 모든 벡터들을 포함하게 됩니다.
이렇게 계층별로 분류해 놓은 벡터들을 대상으로 위의 ANN방법을 사용하여 가장 가까운 이웃 벡터를 찾게 됩니다. 첫번째 레이어에서 가장 가까운 벡터를 찾으면 바로 다음 레이어에 위치한 동일 벡터에서 다시 이웃 벡터를 찾는 작업을 시작합니다. 상위 레이어보다 더 많은 수의 벡터들이 존재함으로 위의 레이어에서 찾은 결과보다 더 가까운 벡터를 찾을 수 있는 확률이 증가하게 됩니다.
이런식으로 최하위 계층까지 가까운 이웃 벡터를 찾는 작업을 계속하여 가장 가까운 벡터를 찾는 작업을 수행합니다.

HNSW에서 레이어별 벡터 분포
HNSW에서 레이어별 벡터 분포. 반시계 방향으로 하위 레이어의 벡터 분표를 표현한다.

Vector DB 의 운용


이 강의를 제공해 준 Weaviate 사의 Vector DB를 사용하여 Vector DB 를 운용하는 방법에 대해 예제를 보도록 하겠습니다.
스키마 정의
스키마는 아래와 같이 json 형식으로 정의 할 수 있습니다.

schema = {
"class": "MyCollection",
"vectorizer": "none",
"vectorIndexConfig": {
"distance": "cosine" # let's use cosine distance
},
}

임의의 튜플을 생성하였다면 (여기에서도 튜플이라는 말을 사용하는지는 (?) 다음과 같은 기본 query 기능을 수행할 수 있습니다.

client.query.aggregate(“MyCollection”).with_meta_count().do()

그러면 회신으로 아래와 같은 결과를 얻습니다.

{'data': {'Aggregate': {'MyCollection': [{'meta': {'count': 5}}]}}}

아래와 같이 Vector search도 가능합니다.

response = (
client.query
.get("MyCollection", ["title"])
.with_near_vector({
"vector": [-0.012, 0.021, -0.23, -0.42, 0.5, 0.5]
})
.with_limit(2) # limit the output to only 2
.do()
)

또한 아래와 같이 Filter를 적용한 Vector search도 가능합니다.

response = (
client.query
.get("MyCollection", ["title", "foo"])
.with_near_vector({
"vector": [-0.012, 0.021, -0.23, -0.42, 0.5, 0.5]
})
.with_additional(["distance, id"]) # output the distance of the query vector to the objects in the database
.with_where({
"path": ["foo"],
"operator": "GreaterThan",
"valueNumber": 44
})
.with_limit(2) # limit the output to only 2
.do()
)
result = response["data"]["Get"]["MyCollection"]

위와 같은 query문을 실행하면 . with_where에 언급된 ValueNumber>44에 만족하는 vector들을 검색하여 return합니다.
위의 설명에서는 임의의 word embedding을 사용했지만, Open AI의 API key를 사용하여 Open AI의 text2vec word embedding service를 사용할 수도 있습니다. Open AI와의 연동을 위해서는 필수 적인 기능입니다.
이 기능을 생성하기 위해서는 client기능 생성시 아래와 같은 module을 구성해 주어야 합니다.

"moduleConfig": {
"text2vec-openai": {
"model": "ada",
"modelVersion": "002",
"type": "text",
"baseURL": os.environ["OPENAI_API_BASE"]
}
}

Concluding Remarks


기본적으로 Vector database는 우리가 사용을 하고 있는 LLM 을 더욱 효율적인 도구로 사용할 수 있도록 도와주는 보완재 입니다.
일반적인 지식으로 pre-train된 LLM으로 하여금 vector database를 외부 지식에 대한 저장소의 역할로 사용할 수 있게 합니다.
그리고 여기에서 추출된 추가적인 정보를 사용자가 질의한 프롬프트에 추가하여 좀 더 정확한 답변을 만들어 낼 수 있도록 유도합니다.
이 시나리오에서 일반적인 자연어 정보에 이와 비슷한 유사성을 검색할 수 있는 Vector DB는 기존 LLM의 기능을 더욱 향상 시켜 줄수 있는 역할을 수행합니다.

Prompt engineering의 장점

Vector DB라는 별도의 인프라 스트럭쳐를 도입하지 않아도 LLM을 사용하는 사람에 따라 즉각적으로 사용이 가능합니다.
LLM에서 자연어 처리를 통하여 기존의 지식을 활용하여 새로운 토큰 들을 생성할때, 기존의 지식을 어떻게 문장이나 이미지로 표현하는 것에 따라 다른 답을 얻을 수 있습니다.
그리고 이런 프롬프트를 작성할때는 작성자의 지식과 문장 표현에 많은 의존성을 가지므로 기업체에서 쓰는 경우 어느정도의 규격화나 품질을 유지하기 어렵습니다.
One-shot/Multi-shot에 사용 되는 사용자 입력 프롬프트에 대한 관리가 어려우며 확장성을 가지기 어렵습니다.

Vector DB를 사용한 RAG의 장점


기업내부에서 사용하는 경우 foundation LLM을 fine tuning 시키지 않아도 LLM이 실행되는 외부에서 독립적인 기업 자체 지식 DB를 사용할 수 있으므로 보안성이 증가할 수 있습니다.
개인별 prompt 엔지니어링을 적용하는 경우 prompt를 작성하는 개인의 문장 구성 능력과 지식의 폭에 종속이 될 수 있으나 Vector DB를 사용하는 경우 사내의 모든 지식을 체계적으로 구조화하여 DB에 저장할 수 있고 이를 통해 개인별 능력차에서 오는 편차를 줄일 수 있습니다.
아울러 LLM이 잘못된 결과를 내는 경우, 참조된 DB tuple을 직접적으로 확인 할 수 있으므로 LLM의 추론에 영향을 줄 수 있는 knowledge를 조정하여 재사용 할 수 있습니다.

Similar Posts

Leave a Reply