목차
- 문제와 접근
- 전날 CLIP 인코더 복습
- 이미지 폴더 인덱싱
- 텍스트 쿼리로 이미지 검색
- 이미지 쿼리로 이미지 검색
- build_image_index / search 함수
- Qdrant 인메모리 컬렉션
- image/caption named vector
- filename_to_caption
- 핵심 개념 총정리
- 자주 나오는 실수/주의사항
- 인덱스를 다시 만들 수 있는 정보 남기기
1. 문제와 접근
멀티모달 검색기를 구성할 때는 이미지 파일과 벡터, 설명 문장의 연결을 유지해야 한다. 작은 배열 기반 검색에서 시작해 Qdrant의 컬렉션과 named vector로 옮기면서 저장 방식이 해결하는 문제를 살펴본다. 검색 인터페이스보다 데이터 갱신과 점수 비교 기준을 먼저 맞춘다.
1
2
3
4
5
6
| image files
↓ encode_image_clip
image embedding matrix
↓
text query -> encode_text_clip -> top-k images
image query -> encode_image_clip -> top-k images
|
2. 전날 CLIP 인코더 복습
1
2
3
4
5
6
7
8
| def encode_image_clip(image) -> np.ndarray:
inputs = processor(images=image.convert("RGB"), return_tensors="pt").to(DEVICE)
with torch.no_grad():
vision_out = model.vision_model(pixel_values=inputs["pixel_values"])
pooled = vision_out.pooler_output
projected = model.visual_projection(pooled)
feat = projected[0].cpu().numpy()
return feat / np.linalg.norm(feat)
|
1
2
3
4
5
6
7
8
9
10
11
| def encode_text_clip(text: str) -> np.ndarray:
inputs = processor(text=[text], return_tensors="pt", padding=True).to(DEVICE)
with torch.no_grad():
text_out = model.text_model(
input_ids=inputs["input_ids"],
attention_mask=inputs.get("attention_mask"),
)
pooled = text_out.pooler_output
projected = model.text_projection(pooled)
feat = projected[0].cpu().numpy()
return feat / np.linalg.norm(feat)
|
3. 이미지 폴더 인덱싱
1
2
3
4
5
| image_paths = sorted(IMAGE_DIR.glob("*.png"))
embeddings = np.stack([encode_image_clip(Image.open(p)) for p in image_paths])
embeddings.shape
# -> (이미지 개수, 512)
|
4. 텍스트 쿼리로 이미지 검색
텍스트를 CLIP 텍스트 임베딩으로 바꾸고 이미지 임베딩과 내적한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| queries = [
"a bar chart of revenue",
"an invoice document",
"a dashboard with KPIs",
"korean text",
]
for q in queries:
q_emb = encode_text_clip(q)
sims = embeddings @ q_emb
top3 = np.argsort(-sims)[:3]
print(q)
for i in top3:
print(f" {sims[i]} ---- {image_paths[i].name}")
|
5. 이미지 쿼리로 이미지 검색
이미지 자체를 쿼리로 써서 비슷한 이미지를 찾는다.
1
2
3
4
5
| def search_by_image(query_image, embeddings, paths, k=3):
q = encode_image_clip(query_image)
sims = embeddings @ q
top = np.argsort(-sims)[:k]
return [(paths[i], float(sims[i])) for i in top]
|
6. build_image_index / search 함수
반복해서 쓸 수 있게 인덱스 생성과 검색을 함수로 나눈다.
1
2
3
4
5
6
7
8
9
10
| def build_image_index(paths) -> tuple:
embs = np.stack([encode_image_clip(Image.open(p)) for p in paths])
return embs, list(paths)
def search_by_text(text, embeddings, paths, k=3) -> list:
q = encode_text_clip(text)
sims = embeddings @ q
top = np.argsort(-sims)[:k]
return [(paths[i], float(sims[i])) for i in top]
|
1
2
3
4
5
6
| embs, paths = build_image_index(image_paths)
for q in ["bar chart", "math equation"]:
for p, s in search_by_text(q, embs, paths, k=2):
print(q)
print(f"{s} {p.name}")
|
7. Qdrant 인메모리 컬렉션
벡터 DB 도입은 데이터 양뿐 아니라 필터링, 갱신, 여러 요청의 접근 같은 요구를 보고 결정한다. 이 절의 인메모리 구성은 컬렉션 동작을 확인하는 예제이며, 지속 저장과 재시작 이후 복구는 별도의 확인 항목이다.
1
2
3
4
5
| from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
client = QdrantClient(":memory:")
COLLECTION = "multimodal_demo"
|
8. image/caption named vector
이미지 벡터와 caption 벡터는 같은 항목에 연결되어 있어도 입력 정보가 다르다. 어느 벡터로 검색한 결과인지 구분하고, 결과를 합칠 때는 각 경로의 점수가 같은 의미라고 가정하지 않는다.
Qdrant는 한 point에 여러 이름의 벡터를 둘 수 있다.
1
2
3
4
5
6
7
| client.create_collection(
collection_name=COLLECTION,
vectors_config={
"image": VectorParams(size=512, distance=Distance.COSINE),
"caption": VectorParams(size=512, distance=Distance.COSINE),
},
)
|
1
2
3
4
5
| image vector:
이미지 자체의 CLIP 이미지 임베딩
caption vector:
파일명/캡션/설명 텍스트의 CLIP 텍스트 임베딩
|
9. filename_to_caption
파일명을 간단한 캡션으로 바꿔 caption vector를 만들 수 있다.
1
2
| def filename_to_caption(path):
return path.stem.replace("_", " ")
|
예:
1
2
| chart_quarterly.png -> chart quarterly
dashboard_v1.png -> dashboard v1
|
10. 핵심 개념 총정리
| 개념 | 역할 |
|---|
| image embedding matrix | 이미지 검색용 벡터 인덱스 |
| text query embedding | 텍스트로 이미지 검색 |
| image query embedding | 이미지로 이미지 검색 |
embeddings @ q | 정규화 벡터의 cosine similarity |
| Qdrant | 벡터 DB |
| named vector | 한 point에 여러 벡터 저장 |
| image/caption vector | 이미지 기반/텍스트 기반 검색을 분리 |
11. 자주 나오는 실수/주의사항
실수 1: 파일명 캡션에서 공백 제거
1
2
3
4
5
| # 비추천
path.stem.replace("_", "")
# 권장
path.stem.replace("_", " ")
|
실수 2: Qdrant vector size 불일치
1
| VectorParams(size=512, distance=Distance.COSINE)
|
CLIP projection 결과가 512차원이므로 Qdrant 설정도 512여야 한다.
실수 3: 이미지 검색과 캡션 검색을 섞어 해석
1
2
| image vector는 시각적 유사도,
caption vector는 텍스트 설명 유사도에 가깝다.
|
실수 4: 모든 벡터를 매번 재계산
1
| 이미지 임베딩은 인덱싱 시 한 번 계산하고 저장해두는 것이 좋다.
|
12. 인덱스를 다시 만들 수 있는 정보 남기기
이미지 경로만으로 항목을 식별하면 파일 이름 변경이나 교체를 추적하기 어려워진다. 항목 ID, 모델과 전처리 설정, 원본 위치를 함께 관리하면 벡터를 다시 생성하거나 갱신할 범위를 정하기 쉽다.
검색 품질은 동일한 질문에서 배열 기반 결과와 저장소 기반 결과를 먼저 대조한다. 저장 방식을 바꾸는 작업과 모델·caption을 바꾸는 작업을 나누어야 결과 차이의 원인을 구분할 수 있다.