10억 규모 이미지 검색 최적화를 향한 여정 (2/2)
이 글은 UPYUN의 10억 규모 이미지 검색 최적화를 향한 여정의 두 번째 부분입니다. 첫 번째 글을 놓치셨다면 여기를 클릭하세요.
2세대 이미지 기반 검색 시스템
2세대 이미지 기반 검색 시스템은 기술적으로 CNN + Milvus 솔루션을 선택합니다. 이 시스템은 특징 벡터를 기반으로 하며 더 나은 기술적 지원을 제공합니다.
특징 추출
컴퓨터 비전 분야에서 인공지능의 활용은 주류가 되었습니다. 마찬가지로 2세대 이미지 기반 검색 시스템의 특징 추출은 합성곱 신경망(CNN)을 기반 기술로 사용합니다
CNN이라는 용어는 이해하기 어렵습니다. 여기서는 두 가지 질문에 답하는 데 집중합니다:
- CNN은 무엇을 할 수 있나요?
- 이미지 검색에 CNN을 사용할 수 있는 이유는 무엇인가요?
Photo by memegenerator.net
AI 분야에는 많은 대회가 있으며, 이미지 분류는 그중 가장 중요한 분야 중 하나입니다. 이미지 분류의 역할은 사진의 내용이 고양이, 개, 사과, 배 또는 다른 유형의 객체에 관한 것인지 판단하는 것입니다.
CNN은 무엇을 할 수 있나요? 특징을 추출하고 객체를 인식할 수 있습니다. 여러 차원에서 특징을 추출하고, 이미지의 특징이 고양이나 개의 특징에 얼마나 가까운지 측정합니다. 가장 가까운 것들을 식별 결과로 선택할 수 있으며, 이는 특정 이미지의 내용이 고양이, 개 또는 그 밖의 것인지 나타냅니다.
CNN의 객체 식별 기능과 이미지 기반 검색 사이에는 어떤 연관이 있을까요? 우리가 원하는 것은 최종 식별 결과가 아니라 여러 차원에서 추출된 특징 벡터입니다. 유사한 내용을 가진 두 이미지의 특징 벡터는 반드시 가까워야 합니다.
어떤 CNN 모델을 사용해야 하나요?
답은 VGG16입니다. 왜 이것을 선택할까요? 첫째, VGG16은 일반화 능력이 좋아 매우 범용적입니다. 둘째, VGG16이 추출하는 특징 벡터는 512차원입니다. 차원이 너무 적으면 정확도에 영향을 줄 수 있습니다. 차원이 너무 많으면 이러한 특징 벡터를 저장하고 계산하는 비용이 상대적으로 높습니다.
CNN을 사용해 이미지 특징을 추출하는 것은 주류 솔루션입니다. 모델로는 VGG16을 사용하고, 기술 구현에는 Keras + TensorFlow를 사용할 수 있습니다. 다음은 Keras의 공식 예제입니다:
from keras.applications.vgg16 import VGG16
from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input
import numpy as np
model = VGG16(weights=’imagenet’, include_top=False)
img_path = ‘elephant.jpg’
img = image.load_img(img_path, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
features = model.predict(x)
여기서 추출된 features는 특징 벡터입니다.
1. 정규화
후속 작업을 용이하게 하기 위해, 우리는 종종 특징을 정규화합니다:
이후에 사용되는 것도 정규화된 norm_feat입니다.
2. 이미지 설명
이미지는 keras.preprocessing의 image.load_img 메서드를 사용하여 로드됩니다:
from keras.preprocessing import image
img_path = 'elephant.jpg'
img = image.load_img(img_path, target_size=(224, 224))
사실 이는 Keras가 호출하는 TensorFlow 메서드입니다. 자세한 내용은 TensorFlow 문서를 참조하세요. 최종 이미지 객체는 실제로 PIL Image 인스턴스입니다(TensorFlow에서 사용하는 PIL).
3. 바이트 변환
실무적으로 이미지 콘텐츠는 종종 네트워크를 통해 전송됩니다. 따라서 경로에서 이미지를 로드하는 대신, 바이트 데이터를 직접 이미지 객체, 즉 PIL Images로 변환하는 것을 선호합니다:
import io
from PIL import Image
# img_bytes: 图片内容 bytes
img = Image.open(io.BytesIO(img_bytes))
img = img.convert('RGB')
img = img.resize((224, 224), Image.NEAREST)
위의 img는 image.load_img 메서드로 얻은 결과와 동일합니다. 주의해야 할 점은 두 가지입니다:
- RGB 변환을 반드시 해야 합니다.
- resize를 반드시 해야 합니다(resize는
load_img method의 두 번째 매개변수입니다).
4. 검은색 테두리 처리
스크린샷과 같은 이미지에는 때때로 꽤 많은 검은색 테두리가 있을 수 있습니다. 이러한 검은색 테두리는 실질적인 가치가 없으며 많은 방해를 일으킵니다. 이런 이유로 검은색 테두리를 제거하는 것도 일반적인 관행입니다.
검은색 테두리는 본질적으로 모든 픽셀이 (0, 0, 0)인 픽셀의 행 또는 열입니다(RGB 이미지). 검은색 테두리를 제거한다는 것은 이러한 행 또는 열을 찾아 삭제하는 것입니다. 이는 실제로 NumPy에서의 3차원 행렬 곱셈입니다.
가로 검은색 테두리를 제거하는 예:
# -*- coding: utf-8 -*-
import numpy as np
from keras.preprocessing import image
def RemoveBlackEdge(img):
Args:
img: PIL image instance
Returns:
PIL image instance
"""
width = img.width
img = image.img_to_array(img)
img_without_black = img[~np.all(img == np.zeros((1, width, 3), np.uint8), axis=(1, 2))]
img = image.array_to_img(img_without_black)
return img
이것이 CNN을 사용하여 이미지 특징을 추출하고 다른 이미지 처리를 구현하는 것에 대해 제가 이야기하고 싶은 거의 전부입니다. 이제 벡터 검색 엔진을 살펴보겠습니다.
벡터 검색 엔진
이미지에서 특징 벡터를 추출하는 문제는 해결되었습니다. 그러면 남은 문제는 다음과 같습니다:
- 특징 벡터를 어떻게 저장할 것인가?
- 특징 벡터의 유사도를 어떻게 계산할 것인가, 즉 어떻게 검색할 것인가? 오픈 소스 벡터 검색 엔진 Milvus는 이 두 가지 문제를 해결할 수 있습니다. 지금까지 우리 프로덕션 환경에서 잘 실행되고 있습니다.
Milvus 로고.
벡터 검색 엔진 Milvus
이미지에서 특징 벡터를 추출하는 것만으로는 전혀 충분하지 않습니다. 또한 이러한 특징 벡터를 동적으로 관리(추가, 삭제, 업데이트)하고, 벡터의 유사도를 계산하며, 최근접 이웃 범위의 벡터 데이터를 반환해야 합니다. 오픈 소스 벡터 검색 엔진 Milvus는 이러한 작업을 꽤 잘 수행합니다.
이 글의 나머지 부분에서는 구체적인 실무 방법과 주의해야 할 점을 설명합니다.
1. CPU 요구 사항
Milvus를 사용하려면 CPU가 avx2 명령어 세트를 지원해야 합니다. Linux 시스템에서는 다음 명령을 사용하여 CPU가 지원하는 명령어 세트를 확인합니다:
cat /proc/cpuinfo | grep flags</code?
그러면 다음과 같은 결과를 얻습니다:
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti intel_ppin tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm xsaveopt cqm_llc cqm_occup_llc dtherm ida arat pln pts
flags 뒤에 오는 것은 CPU가 지원하는 명령어 세트입니다. 물론 이것들은 제가 필요한 것보다 훨씬 많습니다. 저는 avx2와 같은 특정 명령어 세트가 지원되는지만 확인하고 싶습니다. grep을 하나 더 추가하여 필터링하면 됩니다:
cat /proc/cpuinfo | grep flags | grep avx2
결과가 반환되지 않으면 이 특정 명령어 세트가 지원되지 않는다는 뜻입니다. 그러면 머신을 교체해야 합니다.
2. 용량 계획
용량 계획은 시스템을 설계할 때 가장 먼저 고려해야 할 사항입니다. 얼마나 많은 데이터를 저장해야 할까요? 데이터에는 얼마나 많은 메모리와 디스크 공간이 필요할까요?
간단히 계산해 보겠습니다. 벡터의 각 차원은 float32입니다. float32 타입은 4 Bytes를 차지합니다. 그러면 512차원 벡터 하나에는 2 KB의 저장 공간이 필요합니다. 같은 원리로:
- 1천 개의 512차원 벡터에는 2 MB의 스토리지가 필요합니다.
- 100만 개의 512차원 벡터에는 2 GB의 스토리지가 필요합니다.
- 1천만 개의 512차원 벡터에는 20 GB의 스토리지가 필요합니다.
- 1억 개의 512차원 벡터에는 200 GB의 스토리지가 필요합니다.
- 10억 개의 512차원 벡터에는 2 TB의 스토리지가 필요합니다.
모든 데이터를 메모리에 저장하려면, 시스템에는 최소한 그에 해당하는 메모리 용량이 필요합니다.
공식 크기 계산 도구인 Milvus sizing tool을 사용하는 것을 권장합니다.
실제로 우리의 메모리는 그렇게 크지 않을 수 있습니다. (메모리가 충분하지 않아도 크게 문제되지는 않습니다. Milvus는 데이터를 자동으로 디스크에 플러시합니다.) 원본 벡터 데이터 외에도, 로그와 같은 다른 데이터의 저장 공간도 고려해야 합니다.
3. 시스템 구성
시스템 구성에 대한 자세한 내용은 Milvus 문서를 참조하세요:
- Milvus 서버 구성: https://milvus.io/docs/v0.10.1/milvus_config.md
4. 데이터베이스 설계
컬렉션 및 파티션
- 컬렉션은 테이블이라고도 합니다.
- 파티션은 컬렉션 내부의 파티션을 의미합니다.
파티션의 기본 구현은 실제로 컬렉션의 구현과 동일하지만, 파티션은 컬렉션 아래에 있습니다. 그러나 파티션을 사용하면 데이터 구성이 더 유연해집니다. 또한 더 나은 쿼리 결과를 얻기 위해 컬렉션에서 특정 파티션을 쿼리할 수도 있습니다.
컬렉션과 파티션은 몇 개까지 가질 수 있을까요? 컬렉션과 파티션에 대한 기본 정보는 Metadata에 있습니다. Milvus는 내부 메타데이터 관리를 위해 SQLite(Milvus 내부 통합) 또는 MySQL(외부 연결 필요)을 사용합니다. Metadata 관리를 위해 기본적으로 SQLite를 사용하면 컬렉션과 파티션의 수가 너무 많을 때 심각한 성능 저하가 발생합니다. 따라서 컬렉션과 파티션의 총 수는 50,000개를 초과해서는 안 됩니다(Milvus 0.8.0은 이 숫자를 4,096개로 제한합니다). 더 큰 숫자를 설정해야 하는 경우, 외부 연결을 통해 MySQL을 사용하는 것이 좋습니다.
Milvus의 컬렉션과 파티션이 지원하는 데이터 구조는 매우 단순하며, 즉 ID + vector입니다. 다시 말해, 테이블에는 ID와 벡터 데이터라는 두 개의 열만 있습니다.
참고:
- ID는 정수여야 합니다.
- ID가 파티션 내에서가 아니라 컬렉션 내에서 고유하도록 보장해야 합니다.
조건부 필터링
기존 데이터베이스를 사용할 때는 필드 값을 필터링 조건으로 지정할 수 있습니다. Milvus가 정확히 같은 방식으로 필터링하지는 않지만, 컬렉션과 파티션을 사용하여 간단한 조건부 필터링을 구현할 수 있습니다. 예를 들어, 대량의 이미지 데이터가 있고 해당 데이터가 특정 사용자에게 속한다고 가정해 보겠습니다. 그러면 사용자별로 데이터를 파티션으로 나눌 수 있습니다. 따라서 사용자를 필터 조건으로 사용하는 것은 실제로 파티션을 지정하는 것입니다.
구조화된 데이터와 벡터 매핑
Milvus는 ID + 벡터 데이터 구조만 지원합니다. 하지만 비즈니스 시나리오에서 우리에게 필요한 것은 비즈니스 의미를 지닌 구조화된 데이터입니다. 다시 말해, 벡터를 통해 구조화된 데이터를 찾아야 합니다. 따라서 ID를 통해 구조화된 데이터와 벡터 간의 매핑 관계를 유지해야 합니다.
구조화된 데이터 ID <--> 매핑 테이블 <--> Milvus ID
인덱스 선택
다음 문서를 참조할 수 있습니다:
- 인덱스 유형: https://www.milvus.io/docs/v0.10.1/index.md
- 인덱스 선택 방법: https://medium.com/@milvusio/how-to-choose-an-index-in-milvus-4f3d15259212
5. 검색 결과 처리
Milvus의 검색 결과는 ID + 거리의 모음입니다:
- ID: 컬렉션 내의 ID입니다.
- 거리: 0 ~ 1의 거리 값은 유사도 수준을 나타내며, 값이 작을수록 두 벡터가 더 유사합니다.
ID가 -1인 데이터 필터링
컬렉션 수가 너무 적으면 검색 결과에 ID가 -1인 데이터가 포함될 수 있습니다. 이를 직접 필터링해야 합니다.
페이지네이션
벡터 검색은 상당히 다릅니다. 쿼리 결과는 유사도 내림차순으로 정렬되며, 결과 중 가장 유사한 것(topK)이 선택됩니다(topK는 쿼리 시 사용자가 지정합니다).
Milvus는 페이지네이션을 지원하지 않습니다. 비즈니스에 필요하다면 페이지네이션 기능을 직접 구현해야 합니다. 예를 들어, 각 페이지에 결과가 10개씩 있고 세 번째 페이지만 표시하고 싶다면, topK = 30으로 지정하고 마지막 10개의 결과만 반환해야 합니다.
비즈니스를 위한 유사도 임계값
두 이미지의 벡터 간 거리는 0과 1 사이입니다. 특정 비즈니스 시나리오에서 두 이미지가 유사한지 여부를 결정하려면, 이 범위 내에서 임계값을 지정해야 합니다. 거리가 임계값보다 작으면 두 이미지는 유사하고, 거리가 임계값보다 크면 서로 상당히 다릅니다. 자체 비즈니스 요구 사항에 맞게 임계값을 조정해야 합니다.
이 글은 Milvus 사용자이자 UPYUN의 소프트웨어 엔지니어인 rifewang이 작성했습니다. 이 글이 마음에 드셨다면, https://github.com/rifewang 에서 인사하러 오세요.
계속 읽기

From Vector Database to Vector Lakebase
Zilliz offers a fully managed Vector Lakebase powered by Milvus, unifying real-time vector search, lake-scale discovery, and Al data operations.

Zilliz Cloud Now Available in Azure North Europe: Bringing AI-Powered Vector Search Closer to European Customers
The addition of the Azure North Europe (Ireland) region further expands our global footprint to better serve our European customers.

The Real Bottlenecks in Autonomous Driving — And How AI Infrastructure Can Solve Them
Autonomous driving faces a data bottleneck. Learn how AI-native vector databases like Zilliz solve scale, cost, and insight challenges across AV pipelines.



