공부

SigLIP으로 Zero-shot 이미지 분류하기: Python과 Hugging Face 실습

whidam 2026. 10. 30. 09:05

안녕하세요!
고양이·개·자동차 사진을 구분하려면,
반드시 분류 모델을 새로 학습해야 할까요?

SigLIP을 쓰면 후보 문장만 준비해도 됩니다.
이 글에서는 로컬 사진으로 직접 이미지분류를 실행합니다.

학습 없이 분류한다는 말은 무슨 뜻일까?

Zero-shot Classification은 새 분류기를 따로 훈련하지 않는 방식입니다.
이미지와 문장의 어울림을 비교해 가장 맞는 후보를 찾습니다.

예를 들어 사진 한 장과 세 문장을 함께 넣습니다.
고양이, 개, 자동차 중 어느 문장이 사진과 가까운지 계산합니다.

여기서 문장은 단순한 클래스 이름보다 중요합니다.
모델은 이미지와 자연어 설명을 연결하도록 사전학습됐기 때문입니다.

SigLIP은 이미지와 문장을 어떻게 비교할까?

SigLIP은 이미지 인코더와 텍스트 인코더를 함께 사용합니다.
인코더는 입력을 비교 가능한 숫자 표현으로 바꾸는 모델입니다.

두 표현의 관련성을 계산하면 이미지-문장별 로짓이 나옵니다.
로짓은 아직 확률이 아닌 원시 점수입니다.

사진: Pexels 의 Brett Sayles

CLIP 계열 모델은 후보 간 Softmax를 자주 사용합니다.
그러나 SigLIP은 이미지와 문장 쌍을 독립적으로 다루는 특징이 있습니다.

그래서 로짓에 Sigmoid를 적용한 점수의 합은 1일 필요가 없습니다.
여러 후보가 동시에 그럴듯하게 나올 수도 있습니다.

이 점수는 후보 문장 집합 안에서의 절대 정답 보증이 아닙니다.
같은 조건에서 후보들의 관련성을 비교하는 용도로 보는 편이 안전합니다.

실습 전 Python 가상환경과 패키지 준비

프로젝트마다 라이브러리 버전을 분리하려면 가상환경이 편합니다.
아래 명령은 터미널에서 실행합니다.

mkdir siglip-zero-shot
cd siglip-zero-shot

python -m venv .venv

# macOS 또는 Linux
source .venv/bin/activate

# Windows PowerShell
# .venv\Scripts\Activate.ps1

python -m pip install --upgrade pip
pip install torch torchvision transformers pillow

PyTorch는 텐서 연산과 모델 실행을 담당합니다.
Transformers는 HuggingFace 모델을 불러오는 라이브러리입니다.

Pillow는 JPG, PNG 같은 이미지 파일을 여는 도구입니다.
GPU 사용자는 환경에 맞는 PyTorch 설치 명령을 공식 페이지에서 확인하세요.

체크포인트를 고르고 모델을 불러오기

이번 예제는 공개 체크포인트인 google/siglip-base-patch16-224를 사용합니다.
처음 실행할 때는 모델 파일이 내려받아져 시간이 걸릴 수 있습니다.

체크포인트마다 입력 해상도와 메모리 사용량이 다릅니다.
처음에는 base 크기로 흐름을 확인하는 방법이 무난합니다.

from pathlib import Path

import torch
from PIL import Image
from transformers import AutoModel, AutoProcessor

MODEL_ID = "google/siglip-base-patch16-224"
IMAGE_PATH = Path("sample.jpg")

# CUDA GPU가 있으면 GPU를 사용하고, 없으면 CPU를 사용합니다.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

processor = AutoProcessor.from_pretrained(MODEL_ID)
model = AutoModel.from_pretrained(MODEL_ID).to(device)
model.eval()

AutoProcessor는 이미지 크기 조절과 정규화를 처리합니다.
텍스트 토큰화도 같은 객체가 처리해 입력 형식을 맞춥니다.

model.eval()은 평가 모드로 전환하는 호출입니다.
학습 때와 다르게 동작할 수 있는 층을 추론용 상태로 둡니다.

로컬 사진과 후보 문장을 입력하는 코드

현재 폴더에 sample.jpg 파일을 둔다고 가정하겠습니다.
다른 위치라면 IMAGE_PATH에 파일 경로를 지정하면 됩니다.

candidate_texts = [
    "This is a photo of a cat.",
    "This is a photo of a dog.",
    "This is a photo of a car.",
]

if not IMAGE_PATH.is_file():
    raise FileNotFoundError(
        f"이미지 파일을 찾을 수 없습니다: {IMAGE_PATH.resolve()}"
    )

# convert("RGB")는 PNG의 알파 채널, 흑백 이미지 등의 차이를 줄입니다.
with Image.open(IMAGE_PATH) as opened_image:
    image = opened_image.convert("RGB")

inputs = processor(
    text=candidate_texts,
    images=image,
    padding="max_length",
    return_tensors="pt",
)

inputs = {name: value.to(device) for name, value in inputs.items()}

candidate_texts의 각 원소가 하나의 분류 후보입니다.
문장 수를 늘리면 그 후보들까지 함께 비교합니다.

processor의 출력은 PyTorch 텐서입니다.
to(device)는 CPU 텐서를 GPU 또는 선택한 장치로 옮깁니다.

로짓을 Sigmoid 점수로 바꾸고 정렬하기

이제 모델에 입력을 전달하고 이미지-텍스트 로짓을 받습니다.
추론에서는 gradient 계산이 필요하지 않습니다.

with torch.inference_mode():
    outputs = model(**inputs)
    logits = outputs.logits_per_image[0]
    sigmoid_scores = torch.sigmoid(logits)

results = sorted(
    zip(candidate_texts, logits.cpu().tolist(), sigmoid_scores.cpu().tolist()),
    key=lambda item: item[2],
    reverse=True,
)

print("--- Zero-shot classification result ---")
for text, logit, score in results:
    print(f"{text:35s} logit={logit:8.4f}, sigmoid={score:.4f}")

best_text, best_logit, best_score = results[0]
print(f"\nTop candidate: {best_text}")

outputs.logits_per_image의 첫 행은 입력 이미지 한 장의 결과입니다.
후보 문장이 세 개라면 보통 세 개의 로짓을 얻습니다.

torch.inference_mode()는 추론용 문맥입니다.
미분 계산용 정보를 저장하지 않아 메모리와 연산 부담을 줄입니다.

torch.no_grad()도 비슷한 목적에 쓸 수 있습니다.
이 예제처럼 순수 추론만 할 때는 inference_mode가 적합한 선택입니다.

예를 들어 고양이 사진이라면 결과가 아래처럼 보일 수 있습니다.
수치는 이미지와 환경에 따라 달라지는 형식 예시일 뿐입니다.

--- Zero-shot classification result ---
This is a photo of a cat.         logit=  5.1200, sigmoid=0.9941
This is a photo of a dog.         logit=  1.0400, sigmoid=0.7388
This is a photo of a car.         logit= -4.2300, sigmoid=0.0143

Top candidate: This is a photo of a cat.

Sigmoid 점수를 Softmax 확률로 오해하면 안 되는 이유

Softmax는 여러 값의 합을 1로 맞추는 변환입니다.
서로 하나만 고르는 분류 확률에서 자주 쓰입니다.

반면 Sigmoid는 각 로짓을 독립적으로 0과 1 사이로 바꿉니다.
따라서 고양이 점수와 개 점수가 함께 높을 수도 있습니다.

후보 중 하나만 반드시 뽑아야 한다면 정렬 1위를 사용할 수 있습니다.
다만 Sigmoid 값을 정규화된 다중 클래스 확률로 읽으면 안 됩니다.

참고로 비교 목적의 Softmax를 계산할 수는 있습니다.
하지만 그 값은 후보 목록을 바꾸면 함께 바뀌는 상대적 값입니다.

softmax_scores = torch.softmax(logits, dim=0)

for text, score in zip(candidate_texts, softmax_scores.cpu().tolist()):
    print(f"{text}: softmax={score:.4f}")

후보 문장을 바꾸면 왜 결과도 달라질까?

ZeroShot 분류에서 문장은 라벨 이름 이상의 역할을 합니다.
사진을 어떤 관점으로 설명할지 모델에 알려 주는 입력입니다.

예를 들어 반려동물 사진에는 아래처럼 조금 더 구체적인 문장이 유용할 수 있습니다.
다만 모든 이미지에서 같은 표현이 항상 최고라는 뜻은 아닙니다.

candidate_texts = [
    "A close-up photo of a domestic cat.",
    "A close-up photo of a domestic dog.",
    "A photo of a passenger car on a road.",
]

배경이 복잡하거나 대상이 작으면 점수가 흔들릴 수 있습니다.
비슷한 뜻의 템플릿을 여러 개 시험해 비교하는 방법도 있습니다.

중요한 업무라면 몇 장의 검증 이미지로 문장을 먼저 점검하세요.
사전학습 모델의 결과만으로 고위험 판단을 자동화해서는 안 됩니다.

실행이 막힐 때 먼저 확인할 항목

이미지 파일을 열 수 없을 때

FileNotFoundError가 나오면 현재 작업 폴더와 파일명을 확인합니다.
Windows에서는 경로 문자열에 raw string을 쓰는 방법도 있습니다.

IMAGE_PATH = Path(r"C:\images\sample.jpg")

PIL.UnidentifiedImageError는 손상된 파일이나 지원하지 않는 형식일 수 있습니다.
이미지를 다시 저장하거나 다른 파일로 열리는지 확인해 보세요.

모델 다운로드가 실패할 때

처음에는 인터넷 연결과 디스크 여유 공간이 필요합니다.
회사망이나 프록시 환경에서는 Hugging Face 접근이 제한될 수 있습니다.

Transformers 버전이 오래된 경우도 문제가 될 수 있습니다.
가상환경 안에서 pip install -U transformers를 실행해 갱신합니다.

CUDA 메모리가 부족할 때

CUDA out of memory가 나오면 다른 GPU 작업을 먼저 종료합니다.
한 번에 여러 이미지나 너무 많은 후보를 넣었다면 수를 줄입니다.

가장 단순한 해결책은 CPU 실행입니다.
device = torch.device("cpu")로 지정하면 느릴 수 있지만 동작을 확인할 수 있습니다.

GPU에서는 상황에 따라 반정밀도 사용도 고려할 수 있습니다.
다만 장치와 라이브러리 호환성을 확인한 뒤 적용해야 합니다.

짧게 정리하면

이번 SigLIP실습의 핵심은 이미지와 후보 문장을 함께 넣는 것입니다.
별도 재학습 없이도 후보별 관련성을 비교할 수 있습니다.

결과를 읽을 때는 두 가지만 기억하면 됩니다.
문장 표현이 결과에 영향을 주며, Sigmoid 합계는 1일 필요가 없습니다.

먼저 고양이·개·자동차처럼 차이가 큰 후보로 실행해 보세요.
그다음 실제 업무의 후보 문장으로 조금씩 바꾸어 검증하면 됩니다.


일부 이미지는 Pexels에서 제공되었습니다.