[NLP] Faiss와 CLIP을 이용한 간단한 Text Retrieval 구현하기

2024. 6. 6. 02:43Developers 공간 [Shorts]/Vision & Audio

728x90
반응형
<분류>
A. 수단
- OS/Platform/Tool : Linux, Kubernetes(k8s), Docker, AWS
- Package Manager : node.js, yarn, brew, 
- Compiler/Transpillar : React, Nvcc, gcc/g++, Babel, Flutter

- Module Bundler  : React, Webpack, Parcel

B. 언어
- C/C++, python, Javacsript, Typescript, Go-Lang, CUDA, Dart, HTML/CSS

C. 라이브러리 및 프레임워크 및 SDK
- OpenCV, OpenCL, FastAPI, PyTorch, Tensorflow, Nsight

 


1. What? (현상)

추천시스템(Recommendation System)은 정보 필터링(Information Filtering)기술의 일종으로, 유저의 행동패턴이나 선호도 등을 이용해 관심을 가질만한 정보를 추천하는 분야를 말합니다.

 

추천시스템은 크게 보면 Information Retrieval의 일종이므로 단계적으로 feature를 얻는 단계와, 이 feature를 score 기반으로 Ranking하는 단계로 구성됩니다.

 

추천시스템의 분류는 크게는 두가지 방법으로 나뉩니다.

 

1. Collaborative Filtering

 

User과 Item의 interaction 혹은 behavior 기반으로 추천을 진행하는 방법입니다. 따라서 user-item matrix가 필요합니다.

 

Collaborative Filtering은 아래와 같이 두가지로 나뉩니다.

  • Memory-based(Neighborhood-based) : 현재까지의 모든 데이터를 활용해 추천하는 방법입니다.
    • Item-based : user”들”이 어떤 item을 좋아할 때 → 비슷한 user”들”이 좋아하는 item을 추천
    • User-based : user가 어떤 item”들”을 좋아할 때 → 비슷한 item”들”을 좋아하는 user의 item을 추천

[Item-based와 User-based]

  • Model-based : 데이터로 모델을 만들어놓고, 이 모델을 통해 추천을 제공하는 방법입니다.

 

Collaborative Filtering는 아래와 같은 문제점 들이 있습니다.

  • cold start에 민감합니다.
  • 사용자가 많아질 수록 계산시간이 증가합니다.
    • 보통 item<<user이므로 user-item matrix를 매번 업데이트 할 수는 없어 batch 방식으로 업데이트 해 해결합니다.
    • 데이터가 너무 많은 경우 clustering을 통해 데이터를 줄여 cluster 레벨에서 연산된 아이템을 추천하기도 합니다.
  • long tail 문제가 있습니다.

    ** long tail : 상위20%와 하위80%로 나눌 때, 상위 20%가 대부분의 핵심이라는 파레토 법칙과 반대로, 하위 80%의 가치가 더 크다는 현상입니다. 여기서는 하위 80%의 콘텐츠가 추천되지 못하는 경우를 설명하기 위해 사용됩니다.

 

2. Content-based Filtering

 

User나 Item 그 자체의 특징을 기반으로 추천하는 방법입니다. 

  • Item-Item : user가 어떤 item을 좋아할 때 → 비슷한 item을 추천
  • User-User : user가 어떤 item을 좋아할 때 → 비슷한 user의 item을 추천

[Item-Item과 User-User]

 

Content-based FilteringCollaborative Filtering 방법에 비해 cold start에 강건한 특징을 가지지만, user의 경험 내의 유형을 기반으로만 추천하기 때문에 다양한 추천이 불가할 수도 있습니다.

 

이때, 결국 Content-based Filtering은 feature를 선택해 score를 구하는 방식이 중요한데, 이에는 아래와 같은 방법들이 있습니다.

  • Sparse Vector : 검색어의 단어와 문서의 관계를 직접 표현하는 방법으로, Lexical Mismatch 문제가 있습니다.
    ** Lexical Mismatch : 검색어와 문서상의 단어가 의미로 비슷하지만 정확히 일치하지 않아 검색이 되지 않는 문제
    • TF-IDF + Cosine Similarity : 가장 많이 사용되는 방법
    • BM25(Best Matching 25) : 검색어와 문서 사이의 겹치는 정도인 lexical match 점수(Score)를 계산해 Ranking합니다. 아래 식을 보면 단순히 TF-IDF에 Smoothing이 추가된 것을 알 수 있습니다.

      **The Probabilistic Relevance Framework: BM25 and Beyond
      $$BM25(D,Q)=\sum ^n_{i=1}IDF(q_i)\cdot \frac{f(q_i,D)\cdot (k_1+1)}{f(q_i,D)+k_1\cdot (1-b+b\cdot \frac{|D|}{avgdl})}$$
  • Dense Vector : 의미를 담은 벡터를 기반으로 표현하는 방법으로, "핵심 단어"에 대해 직접적인 Lexical match가 부족합니다.
    • BERT + Cosine Similarity
    • DPR(Dense Passage Retrieval) : 문서/검색어에 각각 BERT encoder를 사용하며 in-batch negative sampling과 contrastive learning(RANK-IBN)을 통해 encoder를 학습합니다. 

      ** RANK-IBN(In-Batch Negatives) : 학습 batch안에서 negative sampling을 통해 contrastive learning을 하는 것.

      **Dense Passage Retrieval for Open-Domain Question Answering (arxiv'20)
더보기

------------------------------------------------

<기본적인 Represenetation 방법>

 

정보를 표현하기 위한 representation방법은 다양하지만, 그 중 tf-idfword2vec을 간단히 소개하겠습니다.

 

1. tf-idf(sparse representation) : 여러개의 문서가 있을 때 특정 문서내에서 어떤 단어가 얼마나 중요한지를 나타내는 수치입니다.

  • TF(Term-frequency) : 특정 문서에서 특정 단어가 얼마나 많이 등장하는지
  • DF(document frequency) : 전체 문서에서 특정 단어가 얼마나 많이 등장하는지
  • IDF(Inverse-DF) : $IDF(doc,text)=log(\frac{n}{1+DF})$
  • TF-IDF : TF와 IDF의 곱

2. word2vec(dense representation) : CBOW, SkipGram등의 방법으로 “근처에 있는 단어와 비슷한 의미를 가질 것”이라는 가정을 통해 만든 distributional한 표현입니다.

 

------------------------------------------------

 

3. Hybrid Filtering

 

2가지 이상의 다양한 추천 시스템 알고리즘을 조합하는 방법으로, Netflix, Amazon, Spotify, Tik Tok, YouTube와 같은 유명한 기업에서는 Hybrid Filtering을 활용합니다.


 

이번 글에서는 간단하게 Content-based Filtering기법을 Faiss를 활용해 Text Retrieval을 구현하는 방법을 보이겠습니다. 


2. Why? (원인)

  • X

3. How? (해결책)

Faiss란 Facebook에서 제공하는 패키지로, Similarity Search나 Dense vector clustering 등을 효율적으로 구현해놓았기 때문에 Collaborative Filtering과 Content-based 모두에서 활용됩니다.

** https://github.com/facebookresearch/faiss

더보기

-------------------------------------------------

<Similarity 종류>

 

두 대상 간의 Similarity은 아래와 같이 다양한 방법이있습니다.

  • Cosine : 두 벡터 사이의 각도를 구해 사용
  • Pearson : 두 벡터 사이의 선형관계를 구해 사용 [-1~1]
  • Jacard : 집합 간의 교집합의 크기를 이용해 측정
  • KNN, HNSW : 대상과 가까운 k개를 뽑아 그중 가장 많은 비율을 차지한 쪽으로 분류
  • Euclidean Distance : 두 대상 간의 거리를 구해 사용
  • Correlation

-------------------------------------------------

이제 필요한 패키지들을 설치하겠습니다.

pip3 install autofaiss clip-retrieval

 

embedding은 CLIP을 사용하는 경우 image embedding과 text embedding 두가지로 만들어도 text retrieval을 구현할 수 있습니다.

 

따라서 시작하기에 앞서, 각각의 embedding을 추출하는 방법을 먼저 정의하겠습니다.

 

text embdding은 아래와 같이 구할 것입니다. CLIP으로 얻은 embedding을 normalize해 얻었습니다.

import clip
import torch

device ="cpu"
CLIPEmbedder , _= clip.load("ViT-B/32", device=device)

@torch.inference_mode
def get_text_embedding(sentence, device="cpu", wo_padding=True, max_padding=100):
    sentence_list = sentence.strip().split(', ')
    
    # Tokenize
    text_tokens = clip.tokenize(sentence_list, truncate=False)
    
    # Encoding
    text_features = CLIPEmbedder.encode_text(text_tokens.to(device))
    
    # Normalization
    text_features /= text_features.norm(dim=-1, keepdim=True)
    
    # Padding If needed
    if not wo_padding:
        text_features = torch.nn.functional.pad(text_features, (0,0,0,max_padding-text_features.shape[0]), 'constant', 0)
        text_features = text_features.reshape([1,-1])
        
    text_embeddings = text_features.cpu().detach().numpy().astype('float32')
    return text_embeddings
더보기

--------------------------------------------------------

<Padding 하는 방법>

 

위에서 Padding을 하고 싶은 경우는 텍스트가 아래와 같이 여러개의 정보가 들어온 경우에 대해 만들었습니다.

ex) "a picture of dog, on the red carpet, with white background"

 

CLIP의 tokenize의 잇풋으로는 텍스트의 리스트를 넣도록 되어있는데,

**https://github.com/openai/CLIP/blob/main/clip/simple_tokenizer.py

 

각 리스트의 요소인 텍스트는 white space를 기준으로 분리해 token을 만들고, token을 utf-8마다 처리합니다.

ex) 'This is dog' -> [[84, 104, 105, 115], [105, 115], [100, 111, 103]]

 

따라서 ', '를 기준으로 나누어 넣어주었고, 이렇게 구분된 하나 하나를 keyword라 할 때

ex) "a picture of dog, on the red carpet, with white background" -> 3 keywords

 

각 sentence들의 keyword 최대 개수를 max_padding으로 정해야 모든 텍스트를 임베딩할 수 있습니다.

 

이때 아래와 같은 코드로 max_padding 값을 찾아주었습니다.

f = open(csv_path,'r')
reader =csv.reader(f)
data =list(reader)

max_length = 0
for ids, row in enumerate(data[1:]):
    sentence = row[2]
    text_embeddings = get_text_embedding(sentence, device, wo_padding=True)
    length, dim = text_embeddings.shape
    max_length = max(length,max_length)
print("\n\nmax_length is : {}\n\n".format(max_length))

--------------------------------------------------------

 

또한 image embedding은 아래와 같이 구할 것입니다. 역시나 CLIP으로 얻은 embedding을 normalize해 얻었습니다.

import torchvision
import clip
import torch
from PIL import Image

device ="cpu"
CLIPEmbedder , CLIPPreprocess= clip.load("ViT-B/32", device=device)

@torch.inference_mode
def get_image_embedding(image_path, device="cpu", save_image=False):
    img_orig = Image.open(image_path)
    w,h = img_orig.size
    
    # Image Padding 
    max_wh = max(w,h)
    min_wh = min(w,h)
    img_padded = Image.new(img_orig.mode, (max_wh,max_wh), (255,255,255))  # White Background
    if w ==max_wh:
        img_padded.paste(img_orig, (0, int((max_wh-h)/2)))
    else : 
        img_padded.paste(img_orig, (int((max_wh-w)/2), 0))
        
    # Resize
    img_small = img_padded.resize((224,224))
    
    # Preprocess
    image_preprocessed = CLIPPreprocess(img_small.convert('RGB'))
    
    # Save Image
    if save_image :
    	transform = torchvision.transforms.ToPILImage()
    	save_image = transform(image_preprocessed)
    	save_image.save("./IMAGES/{}".format(image_path.split('/')[-1]))
    
    # Encoding
    image_features = CLIPEmbedder.encode_image(image_preprocessed.unsqueeze(0).to(device))
    
    # Normalization
    image_features /= image_features.norm(dim=-1, keepdim=True)
    
    image_embeddings = image_features.cpu().detach().numpy().astype('float32')
    return image_embeddings

 

1. Index 만들기

 

그럼 검색이 가능한 Index를 먼저 만들어 보겠습니다. Indexfaiss에서 사용하는 용어로, 벡터 데이터를 담고 있는 오브젝트로 검색과 전처리가 가능합니다.

 

주어진 텍스트를 위한 csv파일은 아래와 같이 되어있다고 가정하겠습니다.

Index 이름 Description
1 이름1/그룹1 This is for index 1, which is ...
2 이름2/그룹1 This is for index 2, which is ...
3 이름1/그룹2 This is for index 3, which is ...

 

또한 이미지그룹_이름.png 혹은 그룹_이름.jpg로 형태로 존재한다고 가정하겠습니다.

 

1. 주어진 path를 활용해 데이터 얻기

 

텍스트의 경우, 아래와 같이 csv파일을 읽어 data를 얻어줍니다.

import csv

csv_path="/root/workspace/description.csv"

f = open(csv_path,'r')
reader =csv.reader(f)
data =list(reader)
print('data shape :{}'.format(len(data)))

 

이미지의 경우, 아래와 같이 data들을 얻어줍니다.

from unicodedata import normalize

image_folder="/root/workspace/IMAGE_FOLDER"
file_types=("{}/*.png".format(image_folder), "{}/*.jpg".format(image_folder))
data =[]

for file_type in file_types:
    for file in glob.glob(file_type):
        # NFC : Normal Form Composed for Windows
        # NFD : Normal Form Decomposed for Mac
        file_normed = normalize('NFC', file)
        
        group = file_normed.split('/')[-1].split('_')[0]
        name = file_normed.split('/')[-1].split('_')[1].split('.')[0]
        
        data.append([file, group, name])
print('data shape :{}'.format(len(data)))

 

2. 오브젝트 선언

 

이제 두가지 오브젝트를 선언할 것인데 용도는 아래와 같습니다.

  • EmbedIndex : embedding 벡터의 집합으로, 검색에 활용하기 위한 오브젝트입니다.
  • data_dict : index에 해당하는 정보를 가지고 있는 단순 자료구조 입니다.

위 두가지 오브젝트를 아래와 같이 선언해줍니다. 이 때 사용한 CLIP의 embedding의 차원은 512이므로 512로 정해주었습니다.

import faiss

EmbedIndex = faiss.IndexFlatL2(512) 
EmbedIndex = faiss.IndexIDMap2(EmbedIndex)

data_dict={}

 IndexFlatL2는 유클리드 거리를 기반으로 Indexing을 진행하며, 이와 다르게 IndexFlatIP는 코싸인 유사도를 기반으로 Indexing을 진행합니다.

** IP는 Inner Product, 즉 내적을 의미합니다. 

 

또한, IndexIDMap2는 embedding값이 아닌 ID를 결과로 얻기 위해 사용하는 함수입니다.

 

3. 데이터에서 정보 추출

 

이제 위 data에서 정보를 추출해보겠습니다.

 

먼저 텍스트의 경우, csv파일에서 얻은 정보를 펼쳐 inner loop의 값은 아래와 같을 것입니다.

  • row[0] : Index
  • row[1] : 이름/그룹
  • row[2] : Description
for ids, row in enumerate(data[1:]):
    ids2= row[0]
    name = row[1].split('/')[0]
    group = row[1].split('/')[1]
    desc = row[2]
    data_dict[ids]=[name,group]
    
    text_embeddings = get_text_embedding(desc, device)

    data_dict[ids]=[name,group]
    EmbedIndex.add_with_ids(text_embeddings, ids)

 

다음으로 이미지의 경우, 폴더에서 얻은 정보를 펼쳐 inner loop의 값은 아래와 같을 것입니다.

  • row[0] : 이미지의 path
  • row[1] : 그룹
  • row[2] : 이름
for ids, row in enumerate(data):    
    image = row[0]
    group = row[1]
    name = row[2]

    image_embeddings =get_image_embedding(image, device)

    data_dict[ids]=[name,group]
    EmbedIndex.add_with_ids(image_embeddings, ids)

 

4. Index 저장하기

 

index 정보를 확인해준 뒤, 저장해줍니다.

print("Check1 : {}".format(EmbedIndex.ntotal)) # # of docs
print("Check2 : {}".format(EmbedIndex.d)) # dimension of embiddings
faiss.write_index(EmbedIndex, "output.index")

 

5. dictionary를 DataFrame으로 저장하기

 

다음으로 dictionary를 DataFrame으로 저장해줍니다.

 

아래는 parquet파일로 저장했는데 df.to_csv('output.csv', index=False)와 같이 csv파일로 저장해줄 수도 있습니다.

import pandas as pd

df = pd.DataFrame.from_dict([data_dict])
df.to_parquet('output.parquet', compression=None)

 

 

2. 검색하기

 

다음으로 만들어진 Index를 활용해 검색을 진행해보겠습니다.

 

1. Index와 DataFrame 불러오기

 

위에서 만들어준 IndexDataFrame을 불러오겠습니다.

import faiss
import glob
import pandas as pd

file_name = "output.index"
EmbedIndex = faiss.read_index(file_name)

data =[]
for file in glob.glob("./*.parquet"):
    data.append(pd.read_parquet(file))
data_pd=pd.concat(data)
text_list = data_pd.to_dict()

 

2. Input 처리하기

 

이제 검색할 input 텍스트를 임베딩으로 만들어주겠습니다.

test = "my text"
text_embeddings = get_text_embedding(test)

 

3. 검색하기

 

검색을 진행합니다. 5개의 Rank 순으로 가져오겠습니다.

top_k=5
distances, retrieved_ids = EmbedIndex.search(text_embeddings,5)

 

4. 결과 확인

 

위에서 얻은 결과를 프린트 해보겠습니다.

 

retrieved_ids는 검색한 결과의 id를 얻을 수 있으며, (1,topk)의 구조를 가질 것입니다.

 

얻어진 id를 활용해 아래와 같이 text_list에서 정보를 추출합니다.

print("====================================")
for i in range(top_k):
    ids = retrieved_ids[0][i]
    print("Rank {} : {} in {} (distance: {})".format(i+1, text_list[ids][0][0], text_list[ids][0][1], distances[0][i]))

 


이모티콘 : https://emojipedia.org/apple

https://recostream.com/blog/how-does-recommendation-systems-of-netflix-amazon-spotify-tiktok-and-youtube-work

검색 패러다임 : https://ncsoft.github.io/ncresearch/003e686308f3f3db597b5e5b5d9b6a6bd062aad0
Faiss 설명 : https://encord.com/blog/vector-similarity-search/?utm_source=pytorchkr
이미지 활용 : https://anttihavanko.medium.com/building-image-search-with-openai-clip-5a1deaa7a6e2

 

728x90
반응형