카테고리 보관물: 데이터엔지니어링 Data Engineering

Faiss – 고속 벡터 검색 엔진으로 유사도 검색하기, Vector Search Engine

Faiss는 Facebook Lab에서 만든 벡터 검색 엔진입니다.

Faiss는 벡터 갬색 엔진이고 유사도 검색을 하거나 추천, 기계학습로 만든 모델을 활용해서 응용 서비스를 만들 때 사용합니다.

별거 아닌거처럼 보이지만 불가능한 것을 가능하게 만들어 주는 매우 유용한 라이브러려입니다.

라이브러리이기 때문에 자체로 서비를 제공하는 것은 아니고 이 라이브러리를 이용해서 Backend, Frontend 서비스를 개발하거나 응용 프로그램에 넣을 수 있습니다.

벡터 검색 엔진

벡터 검색 엔진이 뭔지를 설명해야 하는데요. 보통 그래프 서치라고도합니다. 이것들은 주로 수치를 찾는 것을 말하는데 지도검색 같은데서도 사용하는 것으로 매우 쓸모가 많은 엔진입니ㅏㄷ.

일반적으로 검색 엔진이라고 말하면 흔히 텍스트를 검색하는 것을 생각합니다. 구글의 웹 검색, 네이버 검색, 다음 검색 같은 것은 검색 포털이요. 그게 아니면 Elastic Search나 Lucene갈은 검색 엔진을 생각할 텐데요.

하지만 벡터 검색은 텍스트가 아닌 벡터를 빠른 속도로 찾는 것을 말합니다. 벡터는 수열을 말합니다.

아래와 같이 10개의 숫자가 묶여 있으면 이걸 10차원 벡터라고 합니다. 숫자가 100개 있으면 100차원 벡터, 1000개면 1000차원 벡터입니다.

[-0.00709967 -0.01956441  0.03790117 -0.00150699 -0.02145613 -0.06128123
  0.04965064 -0.05588596  0.08241648 -0.05128423]

이런 것들이 수억개가 있고 수억개 중에 어떤 벡터와 가장 가까운 벡터를 찾아야 한다면 문제가 어려워집니다.

가장 가까운 것을 주어진 입력 벡터와 수억개의 벡터를 모두 하나씩 연결해서 서로의 거리를 계산한 다음 가장 가까운 것을 찾아야 하기 때문입니다.

가장 가까운 것을 찾는데 수십분이 걸릴 수 있습니다. 이러면 실제 서비스에서는 쓸 수 없습니다.

어떤 사용자가 온라인 서적 판매사이트에 접속했을 때 그 사람에게 책을 추천해줘야 하는데 추천할 책 목록을 검색하는데 10분씩 걸린다면 서비스에 적용하지 못합니다. 다른 서비스도 마찬가지구요.

Faiss는 인덱싱 방식을 다르게 해서 데이터가 많아도 짧게는 밀리초 단위 길게는 수초 이내에 결과를 찾아 줍니다. 즉 온라인 추천 서비스에 빠르게 적용하는 추천 시스템 등을 개발하는데 사용할 수 있습니다.

Python Faiss library

Faiss는 Python wrapper를 공식 지원하고 있습니다. c++로 만들어졌으니까 다른 언어로도 연결해서 사용할 수 있습니다. Go lang이나 Node.js, Kotlin 같은 것을 쓰면 Python 보다는 성능이 더 좋을 것입니다.

깃헙 레파지토리: https://github.com/facebookresearch/faiss

레파지토리에 있는 것을 설치해도 되고 그냥 pip를 이용해서 설치해도 됩니다.

pip3 install faiss-cpu

gpu 버전을 설치하고 싶으면 gpu 버전ㅇ로 명시해서 설치하면 됩니다.

pip3 install faiss-gpu

사용법은 매뉴얼을 봐야 하겠지만 기본 사용법은 쉽습니다.

Faiss로 유클리디안 거리로 벡터 검색하기

아래 코드는 유클리디안 거리(Euclidean Distance)로 찾는 예제입니다.

이런 것은 KNN (K-nearest-neighbor) 와 같은 기계학습 모델에 사용하는 것입니다. KNN은 판별 모델에서 사용할때 매우 강력한 알고리즘이지만 검색할 때 너무 느리고 자원을 많이 사용하는 문제로 인해서 실제로는 거의 사용을 못하는 알고리즘이지만 Faiss를 이용하면 이걸 쓸 수 있습니다.

Faiss 색인을 생성할 때 벡터의 차원을 지정해주고, Index의 유형도 결정을 해줘야 하는 것이 중요합니다. 검색은 입력한 k의 갯수만큼 리턴하게 되어 있고 벡터의 색인 번호와 거리를 리턴하게 되어 있늡니다.

색인 번호는 그냥 입력한 입력한 벡터의 순번입니다.

import faiss
import numpy as np
import random

# Euclidean distance 기반으로 가장 가까운 벡터를 찾는다.

# 랜덤으로 10차원 벡터를 10개 생성
vectors = [[random.uniform(0, 1) for _ in range(10)] for _ in range(10)]
# 10차원짜리 벡터를 검색하기 위한 Faiss index 생성
index = faiss.IndexFlatL2(10)
# Vector를 numpy array로 바꾸기
vectors = np.array(vectors).astype(np.float32)
# 아까 만든 10x10 벡터를 Faiss index에 넣기
index.add(vectors)
# query vector를 하나 만들기
query_vector = np.array([[random.uniform(0, 1) for x in range(10)]]).astype(np.float32)
print("query vector: {}".format(query_vector))
# 가장 가까운 것 10개 찾기
distances, indices = index.search(query_vector, 10)
# 결과룰 출력하자
idx = 0
for i in indices:
    print("v{}: {}, distance={}".format(idx+1, vectors[i], distances[idx]))
    idx += 1

Faiss로 코사인 유사도로 검색하기

유클리디안 거리(Euclidean Distance)로 가장 가까운 벡터를 찾으면 특정 차원의 양적 수치에 따라는 거리가 가깝다고 판별되는 편향의 문제가 있습니다. 이게 문제가 될 때가 있고 그렇지 않을 때가 있는데 이것은 문제의 도메인에 따라 다릅니다. 그러니까 문제가 주어진 환경에 따라 그때그때 다르다는 뜻입니다.

이런 문제를 피하는 방법은 유사도를 계산할 때 거리측정 방법을 유클리디안 거리를 사용하지 않고 코사인 유사도를 사용해서 벡터의 방향이 가까운 것을 찾는 것입니다. 보통 검색엔진들도 이 방법을 기본으로 사용합니다.

Faiss도 이걸 지원하는데 예제는 아래 코드를 보시면 되고 앞서 설명했던 유클리디안 거리 기반의 검색과 다른 점은 index를 생성할 때 타입을 다르게 생성해야 하고 벡터를 노말라이즈 해줘야 한다는 것입니다. 벡터가 이미 노말라이즈되어 있다면 안해도 됩니다.

import faiss
import numpy as np
import random

# 코사인 유사도 (Cosine Similarity) 를 이용해서 가장 가까운 벡터를 찾으려면 몇가지를 바꿔줘야 한다.
# 코사인 유사도 (Cosine Similarity) 를 사용하려면 벡터 내적으로 색인하는 index를 만들면 된다.
# 코사인 유사도를 계산하라면 벡터 내적을 필연적으로 계산해야 하기 때문이다.

# 랜덤으로 10차원 벡터를 10개 생성
vectors = [[random.uniform(0, 1) for _ in range(10)] for _ in range(100)]
# 10차원짜리 벡터를 검색하기 위한 Faiss index를 생성
# 생성할 때 Inner Product을 검색할 수 있는 index를 생성한다.
index = faiss.IndexFlatIP(10)
# 아래는 위와 동일하다.
# index = faiss.index_factory(300, "Flat", faiss.METRIC_INNER_PRODUCT)

# Vector를 numpy array로 바꾸기
vectors = np.array(vectors).astype(np.float32)
# vectors를 노말라이즈 해준다.
faiss.normalize_L2(vectors)
# 아까 만든 10x10 벡터를 Faiss index에 넣기
index.add(vectors)
# query vector를 하나 만들기
query_vector = np.array([[random.uniform(0, 1) for x in range(10)]]).astype(np.float32)
print("query vector: {}".format(query_vector))
# 가장 가까운 것 10개 찾기
distances, indices = index.search(query_vector, 50)
# 결과룰 출력하자.
idx = 0
for i in indices:
    print("v{}: {}, distance={}".format(idx+1, vectors[i], distances[idx]))
    idx += 1

노트북 코드

위 코드의 노트북은 깃헙 레파지토리에 올려 두었습니다.

https://github.com/euriion/python-exams/blob/main/faiss/faiss-exam.ipynb

다음 번에는 기회가 되면 Faiss를 이용한 간단하고 빠른 추천 엔진을 만드는 예제를 올려보겠습니다.

Google Cloud Engine IP 대역 알아내기

Google Cloud Engine (줄여서 이하 GCE)로부터 회사의 서비스에 발생시키는 기계적인 트래픽을 알아내기 위해서 GCE의 전체 IP대역을 알아내서 확인해 보려고 했는데 여기저기 검색해 본 결과 DNS lookup을 하면 되는 것을 알아냈습니다.

이렇게 하시면 됩니다.

for LINE in `dig txt _cloud-netblocks.googleusercontent.com +short | tr " " "\n" | grep include | cut -f 2 -d :`
do
	dig txt ${LINE} +short
done | tr " " "\n" | grep ip4  | cut -f 2 -d : | sort -n

명령의 결과는 밑에 있습니다.

8.34.208.0/20
8.35.192.0/21
8.35.200.0/23
23.236.48.0/20
23.251.128.0/19
35.184.0.0/14
35.188.0.0/15
35.190.0.0/17
35.190.128.0/18
35.190.192.0/19
35.190.224.0/20
35.192.0.0/14
35.196.0.0/15
35.198.0.0/16
35.199.0.0/17
35.199.128.0/18
35.200.0.0/15
35.202.0.0/16
35.203.0.0/17
35.203.128.0/18
35.203.240.0/20
35.204.0.0/16
35.206.64.0/18
104.154.0.0/15
104.196.0.0/14
107.167.160.0/19
107.178.192.0/18
108.170.192.0/20
108.170.208.0/21
108.170.216.0/22
108.170.220.0/23
108.170.222.0/24
108.59.80.0/20
130.211.128.0/17
130.211.16.0/20
130.211.32.0/19
130.211.4.0/22
130.211.64.0/18
130.211.8.0/21
146.148.16.0/20
146.148.2.0/23
146.148.32.0/19
146.148.4.0/22
146.148.64.0/18
146.148.8.0/21
162.216.148.0/22
162.222.176.0/21
173.255.112.0/20
192.158.28.0/22
199.192.112.0/22
199.223.232.0/22
199.223.236.0/23
208.68.108.0/23

많지 않은 것 처럼 보이지만 CIDR를 모두 풀어서 IP주소로 바꿔 놓으면 무지하게 많습니다. (구글 스케일. ‘ㅡ’;)

CIDR 형식으로 리턴되기 때문에 start, end 형식으로 바꾸시려면 Python을 사용하던지 해서 CIDR에서 Start IP와 End ip를 추출하면 됩니다. 시간이 되면 간단한 예제를 업데이트 해보겠습니다.

 

numpy windows용 64bit 버전

Windows를 비롯해서 numpy를 설치하는 것이 쉬운일이 아닌데요.
그래서 따로 패키징된 것을 제공하는 곳이 몇군데 있습니다.
그중 대표적인 곳이 scipy.org 입니다.
numpy는 scipy.org에서 패키징된 버전을 받을 수 있도록 링크를 추천하고 있는데
32bit 버전만 제공하고 있습니다.
32bit용 numpy를 64bit python에 설치하게 되면 python이 없다고 에러메세지가 나옵니다.

  • numpy는 python에서 수학/과학 계산을 위해서 사용하는 라이브러리입니다.
  • scipy를 사용하기 위해서는 numpy가 필요합니다.

아래 사이트에서 공유하고 있습니다.
접속해서 OS와 Python 버전에 맞는 것을 선택해서 다운로드해서 실행하면 쉽게 설치할 수 있습니다.

http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy

사이트의 반응 속도 및 다운로드 속도가 조금 느립니다.

Python multi core 구동 코드

Python을 이용해서 ETL의 일부인 파싱이나 전처리 작업을 수행하는 경우가 많습니다.
빅데이터인 경우에도 데이터를 Hadoop이나 Hive 또는 Oracle과 같은 RDBMS에 로딩하기 전에 할 수 있는 것들은 최대한 전처리를 한 후에 사용하는 경우가 많이 있습니다.
물론 데이터량이 아주 많으면 Map/Reduce를 작성하는 것이 더 낫습니다만 그리 크지 않은 데이터는 한 대의 서버에서 자원을 풀가동해서 처리해 버리는 것이 작업속도를 줄일 수 있습니다.
Hadoop이 일반화되기 이전에는 이런 형태의 코드를 더 구체화해서 여러 대의 서버에서 동시에 구동되도록 (마치 맵리듀스처럼) 프로세스를 돌리고 결과를 취합하는 것을 만드는 것이 빈번했었습니다.

https://gist.github.com/euriion/5719443

코드를 수정하면 더 복잡한 것도 할 수 있습니다만 매우 복잡하다면 다른 구조를 생각해 보는 것이 좋습니다.

CSV포맷을 TSV포맷으로 바꾸는 간단한 스크립트

엑셀(Excel)에서 CSV 포맷으로 파일을 저장할 때 텍스트 컬럼을 Escaping처리하는 경우가 있습니다.
주로 쉼표(comma)와 따옴표(double quotation)을 그렇게 변환해 버리는데 Hadoop이나 이 포팻을 Hive에 업로드해서 사용하려면 Escaping을 빼야 합니다.
크기가 크지 않은 CSV는 간단하게 Python으로 변환코드를 작성해서 올려서 사용하는 것이 편한데 그럴때 사용했던 소스코드입니다.
R에서 데이터를 로딩할 때도 이 방법이 편합니다.
이런 간단한 작업도 넓은 의미에서는 데이터 먼징 (Data Munging) 포함됩니다.

https://gist.github.com/euriion/5720809