안녕하세요. 8기 랭체인방 정정민입니다.
아직 버전이 0.0.3xxx 임에도 공부할 부분이 참 많은 Langchain이네요.
이번에는 VectorDB에 대한 공부를 해봤습니다. 코드
Faiss 뜯어보기
VectorDB와 Faiss
LLM 모델이 이전의 대화를 기억해 질문에 알맞은 문맥을 고려한 대답을 주고, 여러 문서에서 적절한 답변을 만들어내는 기능을 수행할 수 있는 이유는 DB에 필요한 정보를 잘 저장해두기 때문입니다.
VectorDB란 Langchain에서 사용하는 DB로 데이터의 특징값을 이용해 위 업무를 수행할 수 있는 기능을 제공합니다.
제공하는 VectorDB의 종류는 매우 많습니다. 공식 페이지에서 사용 가능한 VectorDB의 종류만해도 65개 정도 되네요. (아마 더 많아지겠죠?? ㄷㄷ!)
저는 이 VectorDB 중 많은 사람들이 사용하는 Faiss vectorDB를 뜯어보았습니다.
Faiss는 Facebook AI Similarity Search의 약자로 Facebook에서 개발했습니다.
이 DB는 고차원 데이터의 빠른 검색을 위한 목적으로, 입력 특징 vector와 유사한 vector를 효율적으로 검색할 수 있도록 설계되었다고 합니다.
입력 문장에 대응하는 이전 대화 내용 혹은 문서를 찾는데 유용하죠.
결론부터 말씀드리면 Faiss 는 index라는 객체 변수와 docstore라는 객체 변수를 활용해 데이터를 저장하고 비슷한 문서(document)를 뽑아줍니다.
Faiss 기본 코드
공식 문서에 있는 예시 코드를 바탕으로 매우 간단한 코드를 작성하고 내부 구조를 확인해 보았습니다. 아래는 사용한 예시 코드입니다.
embedding = HuggingFaceHubEmbeddings(huggingfacehub_api_token=HUGGINGFACE_API_KEY,
repo_id="sentence-transformers/all-MiniLM-L12-v2")
text1 = 'simple text for FAISS testing'
text2 = 'another simple text for vectorDB testing'
vectorstore = FAISS.from_texts(
[text1, text2], embedding=embedding
)
docs = vectorstore.similarity_search("hello")
print(docs) # [Document(page_content='simple text for FAISS testing'), Document(page_content='another simple text for vectorDB testing')]
docs = vectorstore.similarity_search("vectorDB")
print(docs) # [Document(page_content='another simple text for vectorDB testing'), Document(page_content='simple text for FAISS testing')]
docs = vectorstore.similarity_search("FAISS")
print(docs) # [Document(page_content='simple text for FAISS testing'), Document(page_content='another simple text for vectorDB testing')]
Faiss에 데이터를 밀어 넣는 방법은 다양(from_texts, from_embedding 등)합니다.
위에서는 from_texts라는 클래스 메소드를 활용합니다.
Faiss 객체 생성
위 코드에서는 명시적으로 Faiss 객체가 보이지는 않습니다. 살펴보면 내부 메소드에서 FAISS class 객체를 생성합니다(링크). 여기서 중요한 점은 아래 객체들이 필요하다는 점입니다.
embedding
index
docstore
embedding 객체
입력한 text가 특징화 되어 만들어진 vector 입니다.
저는 huggingface의 transformer 계열 feature 추출기를 활용했고, 총 384 크기를 갖는 vector가 생성됩니다.
index 객체
index는 Faiss에서 데이터베이스를 의미합니다. 데이터를 어떤 방식으로 저장하고 계산해서 유사한 데이터를 찾아줄지를 결정하는 중요한 요소라고 하네요. (faiss 공식 링크)
Langchain 내부 코드로는 23년 12월 현재 vector간 내적 연산이 가능한 Flat 형태의 index만 사용 가능해보입니다.
이 index 안에 특징 vector가 저장됩니다.
if distance_strategy == DistanceStrategy.MAX_INNER_PRODUCT:
index = faiss.IndexFlatIP(len(embeddings[0]))
else:
# Default to L2, currently other metric types not initialized.
index = faiss.IndexFlatL2(len(embeddings[0]))
docstore 객체
Langchain에서 제공하는 내장 memory라고 해요. 사실은 간단한 dict형 객체 변수입니다. 휘발적 특성이 있을테니 코드가 커진다면 저장은 필수로 보입니다.
원본 데이터 소스인 document 정보를 저장하기 위해 사용됩니다.
제가 사용한 예에서는 text1과 text2의 원본 값이 될거고, 만약 pdf와 같은 외부 데이터를 썼다면 그 안의 내용이 되겠죠.
Faiss에 데이터 밀어 넣기
__add 메소드에서 embedding vector를 index에 저장(코드)하고, document 정보를 docstore에 저장(코드)하는 과정이 진행됩니다.
이때, index와 docstore는 아래와 같은 관계를 갖고 있습니다.
이 관계도를 이용해 추후에 vector 연산 결과로 document를 찾아내는 과정에서 사용합니다.
그 외의 코드를 살펴보면 Faiss 객체는 text에 특화된 DB class 입니다. Text 기반 Embedding을 가정하고, 내부 코드로는 사용하는 쿼리 혹은 text 입력을 필수로 요구합니다.
이로인해 text만을 사용하는 시스템에서는 코드가 한결 간단해질 수 있지만, 다양한 활용성은 제한이될 수 있겠죠!
similarity search
Faiss를 쓰는 목적이죠.
입력 쿼리를 기반으로 갖고 있던 document 중 유사도가 높은 doc을 찾아줍니다. 필요에 따라서는 얼마나 유사한지 score도 가져올 수 있습니다만 여러 예제에서는 잘 안쓰네요 ㅎㅎ
index에서 vector 연산을 진행(링크)한 뒤
document를 꺼내와서(링크) return 하는 형식입니다.
여기까지 결과와 인사이트
Faiss를 사용하는 과정에서 내부적으로 index와 docstore가 생성되고 그것으로부터 search & return을 받습니다.
이는 Faiss의 특징으로 다른 VectorDB에서는 좀 다르게 코드가 짜여있었습 니다.
특히 Faiss 처럼 서로 다른 두 개의 데이터 저장소 (index, docstore)를 사용하지 않는 경우가 많았습니다.
그래도 큰 틀로 보면 비슷한 과정이 있어보입니다.
Langchain에서 제공하는 Faiss는 꼭 글의 형태로 입력을 받아 객체를 만들어야 합니다. (내장 함수가 그렇게 구현되어 있음)
Faiss는 index와 docstore의 간단한 사용만으로도 강력한 연산 과정을 지원해주는데, text만 쓸 수 있는게 좀 아쉽단말이죠..
그래서! 텍스트가 아닌 다른 데이터(이미지)를 이용해 특징값을 비교하는 search를 제공하는 Custom Faiss VectorDB를 구성해보았습니다.
이미지 기반 Faiss VectorDB 만들고 테스트하기
텍스트가 아닌 이미지 search가 가능하도록 코드를 작성하고 결과를 살펴봤습니다. 아래는 대략적인 코드의 흐름입니다.
치와와, 요크셔테리아, 시바견 각 1장씩 이미지를 수집
각 이미지를 AI 모델에 통과시켜 특징값을 생성
해당 feature를 기반으로 faiss db에 저장
같은 종 다른 이미지를 query로 주고 search 결과 확인
이미지 수집
DB 저장용(이름.jpg 혹은 jpeg) & search query용(이름_test.jpg 혹은 jpeg) 데이터 수집
이미지의 특징 생성
간단한 AI 모델을 가져와 저장용 이미지의 각 특징값을 추출
class feat_extractor(nn.Module):
def __init__(self, model):
super().__init__()
self.model = model
def forward(self, x) :
x = self.model.conv1(x)
x = self.model.bn1(x)
x = self.model.relu(x)
x = self.model.maxpool(x)
x = self.model.layer1(x)
x = self.model.layer2(x)
x = self.model.layer3(x)
x = self.model.layer4(x)
x = self.model.avgpool(x)
x = torch.flatten(x, 1)
return x
model = feat_extractor(resnet50(weights=weights))
preprocess = weights.transforms()
batch = load_images(image_paths, preprocess)
feats = model(batch)
feats = feats.detach().numpy()
결과로 생성된 이미지의 특징값은 총 2048만큼의 길이를 갖습니다. (이는 AI 모델마다 상이할 수 있음) 실제 값은 이렇게 생겼습니다.
Faiss에 저장
Custom VectorDB를 위해 간다한 class를 구현합니다.
다양한 기능은 배제하고 정말 필요한 기능(추가, 서치)만 구현합니다.
class CustomFAISS():
def __init__(self, index, docstore):
self.index = index
self.docstore = docstore
def add(self, feats, image_paths):
self.index.add(feats)
ids = [str(uuid.uuid4()) for _ in image_paths]
self.docstore.add({id_: path for id_, path in zip(ids, image_paths)})
self.index_to_docstore_id = {i: id_ for i, id_ in enumerate(ids)}
def _similiarity_search(self, feat, k=1):
scores, indices = self.index.search(feat, k)
docs = []
for j, idx in enumerate(indices[0]):
if idx == -1 : continue
_id = self.index_to_docstore_id[idx]
doc = self.docstore.search(_id)
docs.append((doc, scores[0][j]))
return docs[:k]
결과 확인
결론적으로 잘 나옵니다.
좀 다른 요크셔테리아 이미지를 넣어주면 DB에 저장된 요크셔테리아를 찝어줍니다. 치와와도 마찬가지고, 시바견도 그렇네요!
print('Chihuahua Test Image.. ', end='')
test_image_path = 'dogs_image/chihuahua_test.jpg'
batch = load_images([test_image_path], preprocess)
feat = model(batch).detach().numpy()
docs = custom_faiss._similiarity_search(feat, k=1)
print(f'most similiar images: {docs[0][0]} with score : {docs[0][1]:.3f}')
# Chihuahua Test Image.. most similiar images: dogs_image/chihuahua.jpg with score : 59.027
print('Shiba Test Image.. ', end='')
test_image_path = 'dogs_image/shiba_test.jpeg'
batch = load_images([test_image_path], preprocess)
feat = model(batch).detach().numpy()
docs = custom_faiss._similiarity_search(feat, k=1)
print(f'most similiar images: {docs[0][0]} with score : {docs[0][1]:.3f}')
# Shiba Test Image.. most similiar images: dogs_image/shiba.jpeg with score : 47.540
print('York Test Image.. ', end='')
test_image_path = 'dogs_image/york_test.jpg'
batch = load_images([test_image_path], preprocess)
feat = model(batch).detach().numpy()
docs = custom_faiss._similiarity_search(feat, k=1)
print(f'most similiar images: {docs[0][0]} with score : {docs[0][1]:.3f}')
# York Test Image.. most similiar images: dogs_image/york.jpg with score : 49.255
진짜 결론
VectorDB 중 Faiss는 vector 연산에 특화된 DB 입니다.
Langchain을 관통하는 기본 데이터는 text입니다. 그래서 Faiss는 text 데이터 저장 및 서칭에 손쉽게 사용될 수 있습니다. 반면, 그렇기 때문에 이미지와 같은 데이터를 저장하거나 서칭하는 과정은 내장 코드만으로는 힘듭니다.
하지만 서비스 종류에 따라 이종간 데이터 저장 및 서칭이 필요할 수 있겠죠. 그를 위해 Custom Faiss VectorDB를 구현해봤고 필수 요소(index와 docstore)의 역할을 공부해봤습니다.
제 삽질이 비슷한 문제를 고민하는 분들에게 혹은 Langchain을 공부하시는 분들에게 도움이 되었길 희망합니다 ^^
작성자
정정민
블로그 : 링크