그림이 라벨과 일치하지 않을 때 — 파트 2: OCR 및 VQA를 활용한 특징 기반 탐지
요약
제품 이미지와 라벨 정보가 불일치하는 문제를 해결하기 위해 OCR과 VQA를 결합한 특징 기반 탐지 파이프라인을 소개합니다. CLIP/BLIP의 한계를 극복하고자 제품의 속성을 구조화된 특징 사전으로 정의하고 개별 검증하는 방식을 제안합니다.
핵심 포인트
- CLIP/BLIP은 세밀한 제품 특징 불일치 탐지에 한계가 있음
- 특징 사전(Feature Dictionary)을 통한 구조화된 속성 검증 방식 도입
- EasyOCR을 활용하여 브랜드, 무게 등 텍스트 정보 추출
- OCR 결과와 설명 간의 공백 차이 등 데이터 정제 필요성 강조
이 글은 2부작 시리즈의 두 번째 파트입니다. 파트 1에서는 문제 상황, CLIP 베이스라인(baseline), 그리고 BLIP에 대해 다루었습니다. 이번 파트에서는 세밀한 제품 특징 매칭을 위해 이들을 대체한 OCR + VQA 파이프라인(pipeline)을 다룹니다.
점수(Scores)의 문제점
파트 1의 마지막에서 결론은 명확했습니다. CLIP과 BLIP은 명백한 불일치는 잘 잡아내지만, 마켓플레이스에서 가장 중요한 세밀한(fine-grained) 불일치에는 실패한다는 것입니다. 잘못된 브랜드는 BLIP ITM에서 0.79점을 받는데, 이는
- Prairie Farms Whole Milk (1 quart carton) — 스타일화된 브랜드 로고, 뒷면의 영양 정보, 그리고 명확한 전면 패널이 있는 텍스트가 많은 라벨입니다. 브랜드 및 변형 탐지 테스트에 유용합니다.
- Amazon Fresh Boneless Skinless Chicken Breast (30 oz 플라스틱 용기) — 최소한의 라벨 텍스트이며, 스티커에 브랜드가 인쇄되어 있습니다. 모델이 브랜드와 특정 부위를 모두 식별할 수 있는지 테스트합니다.
- Land O Lakes Salted Butter (8 oz 종이 포장재) — 라벨에 여러 크기 표현(
8 oz,226.8 g,4 half sticks)이 인쇄된 컴팩트한 패키지입니다. 크기 일치 엣지 케이스를 테스트합니다.
특징 사전 접근법 (The Feature Dictionary Approach)
이미지를 설명과 전체적으로 점수화하는 대신, 특징 사전(feature dictionary) — 모든 제품 목록이 가져야 하는 구조화된 속성 집합 — 을 정의하고 각 속성을 독립적으로 검증하는 아이디어입니다.
description_features = {
"product_type": "milk",
"brand": "prairie farms",
...
각 특징에 대해 시스템은 제품 이미지에서 해당 값을 추출하고 설명과 비교합니다. 단 하나의 특징이라도 실패하면 — 목록이 플래그 지정됩니다.
두 가지 도구가 추출을 수행하며, 각각 다른 것이 처리할 수 있는 것을 담당합니다:
Tool 1: 텍스트 특징을 위한 OCR (Optical Character Recognition)
**OCR(광학 문자 인식)**은 이미지의 픽셀에서 직접 인쇄된 텍스트를 읽어 문자열로 변환합니다. 제품 라벨의 경우, 가장 중요한 정보 — 브랜드 이름, 무게, 변형 — 는 인쇄된 텍스트입니다. 이를 직접 읽는 것이 비전 모델에게 해석하도록 요청하는 것보다 빠르고 정확합니다.
저희는 API 호출이 필요 없고 로컬에서 실행되는 Python 라이브러리인 EasyOCR (pip install easyocr)을 사용합니다.
import easyocr
import numpy as np
from src.utils import fetch_image_from_url
...
Land O Lakes 버터 라벨에 대한 OCR 결과는 다음과 같습니다:
jarmer-owned landolakes since 1921 tbsp 4 half sticks butter half net wt 8oz (226.8 g) salted
여기에 `
공백 제거 트릭 (The space stripping trick)
OCR은 흔히 "8oz"를 하나의 토큰으로 읽는 반면, 설명(description)에는 공백이 포함된 "8 oz"라고 되어 있는 경우가 많습니다. 단순한 부분 문자열 일치(substring match) 방식으로는 실패하게 됩니다. 해결책은 비교하기 전에 모든 공백을 제거하는 것입니다:
ocr_found = any(
d.replace(" ", "") in ocr_text.replace(" ", "")
for d in desc_norms
...
다중 동등 표현 (Multiple equivalent representations)
크기(Size) 값은 리스팅(listing)마다 일관되지 않습니다. 버터 리스팅에는 "8 oz", "226.8 g", 또는 "4 half sticks"라고 적혀 있을 수 있으며, 이들은 모두 동일한 제품을 가리킵니다. 정확히 일치(exact match)할 것을 요구하면 거짓 음성(false negatives)이 발생합니다. 해결책은 모든 특징(feature)에 대해 동등한 표현들의 리스트를 허용하는 것입니다:
"size": ["8 oz", "226.8 g", "4 half sticks"]
OCR이 이 중 하나라도 찾아내면, 해당 특징은 통과됩니다.
도구 2: 폴백(Fallback)으로서의 Moondream2 VQA
OCR에는 두 가지 명확한 한계가 있습니다. 첫째, 스타일화된 로고나 맞춤형 브랜드 폰트를 읽을 수 없습니다. 장식적인 스크립트로 표현된 "Prairie Farms"는 깨진 문자(garbled characters)를 반환하거나 아무것도 반환하지 않을 수 있습니다. 둘째, 어떤 특징들은 일반 텍스트로 아예 인쇄되어 있지 않습니다. 예를 들어 "용기 유형 (container type)"은 라벨에 적혀 있는 것이 아니라, 모델이 추론해야 하는 시각적 속성입니다.
이러한 경우를 위한 폴백(fallback)은 Moondream2 (1.86B 파라미터, vikhyatk/moondream2)를 사용한 **VQA (Visual Question Answering, 시각적 질의응답)**입니다.
VQA 모델은 이미지와 자연어 질문을 입력받아 짧은 답변을 반환합니다. 모든 것을 설명하느라 정작 중요한 세부 사항을 놓칠 수 있는 캡셔닝(captioning)과 달리, VQA는 목표 지향적이며 제어가 가능합니다.
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
...
왜 Moondream2인가?
| 모델 | 크기 | 선택 이유 |
|---|---|---|
blip-vqa-base | ~400MB | 지시 이행(instruction-following) 능력이 약하며, 제품 라벨 인식 성능이 낮음 |
| ... |
Moondream2는 Apple M2 MPS에서 float16으로 실행되며 약 4GB의 통합 메모리(unified memory)를 사용합니다. 이는 16GB 사양의 기기에서 여유롭게 구동 가능한 수준입니다.
제약된 프롬프트 (Constrained prompts)
개방형 질문 (Open-ended questions)은 예측 불가능한 답변을 유도합니다. 제약된 프롬프트 (Constrained prompts)는 모델이 알려진 어휘 중에서 선택하도록 강제하여 환각 (Hallucination)을 줄입니다:
FEATURE_QUESTIONS = {
"product_type": "이것은 우유, 커피, 버터, 치킨, 고기, 빵, 주스, 음료, 스낵, 또는 세정제입니까?",
"container": "이 제품은 병, 캔, 카톤, 용기, 캐니스터, 봉지, 또는 상자에 들어있습니까?",
...
종합하기: OCR 우선, VQA 보조 (OCR First, VQA Fallback)
라우팅 로직은 간단합니다:
텍스트 특징 (브랜드, 크기, 변형, 제품 유형)의 경우:
1. OCR 시도 → 발견되면 완료
2. 발견되지 않으면 → Moondream2에게 질문
...
def compare_features(image_features: dict, description_features: dict) -> dict:
ocr_text = image_features.get("_ocr_text", "")
...
세 가지 제품에 대한 결과
| 제품 | 브랜드 | 제품 유형 | 크기 | 변형 | 용기 | 종합 |
|---|---|---|---|---|---|---|
| Prairie Farms Whole Milk | ✓ VQA | ✓ OCR | ✓ OCR | ✓ OCR | ✓ VQA | ✅ 일치 |
| ... |
모든 설명이 정확하게 일치했습니다. 잘못된 브랜드, 잘못된 크기, 그리고 잘못된 변형은 세 가지 제품 모두에서 정확하게 불일치 (Mismatch)를 트리거했습니다.
모든 특징은 결정적입니다
라벨링 로직의 초기 버전은 비율을 사용했습니다: 특징의 75%가 일치하면 리스팅을 통과시키는 방식이었습니다. 이는 마켓플레이스(Marketplace)에서는 잘못된 방식입니다.
8 oz의 버터를 주문한 고객이 16 oz를 받았다면, 잘못된 제품을 받은 것입니다. 뼈가 없는 닭가슴살이 뼈가 포함된 것으로 설명되어 있다면 이는 실제 배송 실패 (Fulfilment failure)입니다. 잘못된 크기가 허용될 수 있는 임계값 (Threshold)이란 존재하지 않습니다.
최종 라벨링 로직은 강력한 거부권 (Hard veto) 방식입니다:
CRITICAL_FEATURES = {"brand", "product_type", "size", "variant", "container"}
def overall_label(feature_results: dict) -> str:
...
다른 항목들이 무엇과 일치하든 상관없이, 단 하나의 특징이라도 실패하면 "Likely mismatch"를 반환합니다.
한계점
이 접근 방식은 세밀한 제품 매칭 (Fine-grained product matching)에 있어 CLIP 또는 BLIP보다 유의미하게 뛰어나지만, 몇 가지 실제적인 과제들이 남아 있습니다:
이미지 각도(Image angle): OCR은 보이는 것만 읽습니다. 만약 상품 목록 이미지가 전면 라벨 대신 영양 성분표를 보여준다면, 브랜드 및 변형 텍스트는 완전히 누락됩니다. 이 문제에 대한 해결책—상품당 여러 이미지 URL을 허용하고 OCR 결과를 병합하는 방식—은 향후 작업으로 남겨두었습니다.
이미지 품질(Image quality): 흐릿하거나 압축된 이미지는 OCR 정확도를 떨어뜨리고, Moondream2가 명확하게 볼 수 없는 특징에 대해 환각적인 답변을 생성할 수 있습니다.
프롬프트 커버리지(Prompt coverage): 현재의 VQA 프롬프트는 소수의 제품 카테고리를 다룹니다. 수천 개의 카테고리가 있는 실제 마켓플레이스에서는, 카테고리별로 프롬프트를 신중하게 설계해야 합니다. 여기에는 더 광범위한 옵션 목록, 상황 인지적 문구(context-aware phrasing), 잠재적으로 사고 사슬 추론(chain-of-thought reasoning) 등이 필요합니다. 현재의 프롬프트는 개념 증명(proof of concept)이며, 프로덕션 준비 상태가 아닙니다.
의미론적 일치(Semantic matching): 비교 로직은 공백 정규화 후 부분 문자열 매칭을 사용합니다. `
모든 코드, 노트북 및 실험 결과는 GitHub에서 확인할 수 있습니다:
github.com/ebiarian/product-image-description-alignment
_← Part 1: CLIP & BLIP으로 돌아가기
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기