AI 기초

GUIDE / 중급

CLIP: 이미지와 텍스트를 같은 공간에 정렬하는 대조 학습

OpenAI CLIP의 대조 학습 원리, InfoNCE 손실, 제로샷 이미지 분류 메커니즘, 그리고 Stable Diffusion·VLM에서의 활용까지 완전 해설합니다.

PALDYN Team7 MIN READ

지난 글에서 Vision Transformer가 이미지를 패치 시퀀스로 처리하는 방법을 다뤘다. 이번 글에서는 ViT를 비전 인코더로 사용하면서, 이미지와 텍스트를 동일한 임베딩 공간에 정렬하는 CLIP(Contrastive Language-Image Pre-training)을 완전 해설한다. CLIP은 오늘날 Stable Diffusion, LLaVA, GPT-4V 등 거의 모든 멀티모달 AI의 비전 인코더로 쓰이는 핵심 기반 기술이다.

CLIP의 핵심 아이디어

CLIP은 인터넷에서 수집한 4억 개의 이미지-텍스트 쌍을 사용해, 이미지와 그것을 설명하는 텍스트가 임베딩 공간에서 가깝도록 훈련한다. 새로운 라벨이나 수작업 어노테이션 없이, 웹의 alt-text와 캡션만으로 강력한 비전 표현을 학습하는 자기지도 학습의 일종이다.

CLIP 대조 학습 구조

대조 학습과 InfoNCE 손실

배치 크기를 N이라 하면, N개의 이미지와 N개의 텍스트로 N×N 유사도 행렬을 만든다. 대각선의 N개 쌍은 매칭 쌍(양성)이고, 나머지 N²-N개는 비매칭 쌍(음성)이다. InfoNCE 손실은 각 이미지에 대해 올바른 텍스트 쌍의 유사도를 최대화하는 다중 클래스 분류 손실로 해석할 수 있다.

import torch
import torch.nn.functional as F

def clip_loss(
    image_features: torch.Tensor,  # (N, D) 이미지 임베딩
    text_features: torch.Tensor,   # (N, D) 텍스트 임베딩
    temperature: float = 0.07,
) -> torch.Tensor:
    # L2 정규화
    image_features = F.normalize(image_features, dim=-1)
    text_features = F.normalize(text_features, dim=-1)

    # 코사인 유사도 행렬 (N × N)
    logits = image_features @ text_features.T / temperature

    # 대각선이 정답 (i번째 이미지 ↔ i번째 텍스트)
    labels = torch.arange(len(logits), device=logits.device)

    # 이미지→텍스트 / 텍스트→이미지 양방향 손실
    loss_i2t = F.cross_entropy(logits, labels)
    loss_t2i = F.cross_entropy(logits.T, labels)

    return (loss_i2t + loss_t2i) / 2

temperature τ=0.07은 학습 초기의 기본값이며, 실제 CLIP 학습에서는 이 값도 학습 가능한 파라미터로 설정한다.

CLIP 아키텍처

import torch
import torch.nn as nn
from torchvision.models import vit_b_16
from transformers import BertModel

class CLIP(nn.Module):
    def __init__(self, embed_dim: int = 512):
        super().__init__()
        # 이미지 인코더: ViT 또는 ResNet
        self.image_encoder = vit_b_16(weights=None)
        self.image_proj = nn.Linear(768, embed_dim)

        # 텍스트 인코더: Transformer
        self.text_encoder = BertModel.from_pretrained(
            'bert-base-uncased'
        )
        self.text_proj = nn.Linear(768, embed_dim)

        # 학습 가능한 temperature
        self.log_scale = nn.Parameter(torch.ones([]) * 0.07)

    def encode_image(self, images: torch.Tensor):
        feats = self.image_encoder(images)
        return self.image_proj(feats)

    def encode_text(self, input_ids, attention_mask):
        out = self.text_encoder(
            input_ids=input_ids,
            attention_mask=attention_mask
        )
        # [CLS] 토큰 특징 사용
        return self.text_proj(out.last_hidden_state[:, 0])

    def forward(self, images, input_ids, attention_mask):
        img_feat = self.encode_image(images)
        txt_feat = self.encode_text(input_ids, attention_mask)
        loss = clip_loss(img_feat, txt_feat, self.log_scale.exp())
        return loss

제로샷 이미지 분류

CLIP의 가장 강력한 능력은 파인튜닝 없이 임의의 클래스를 분류하는 것이다. ImageNet 분류라면 "a photo of a {classname}" 형태의 템플릿 텍스트 1000개를 만들어 임베딩하고, 이미지 임베딩과 가장 유사한 텍스트를 찾으면 된다.

from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import torch

model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained(
    "openai/clip-vit-base-patch32"
)

# 분류할 이미지
image = Image.open("dog.jpg")

# 후보 클래스 텍스트 생성
candidate_labels = ["고양이", "강아지", "새", "물고기"]
texts = [f"a photo of a {label}" for label in candidate_labels]

# 전처리 및 추론
inputs = processor(
    text=texts,
    images=image,
    return_tensors="pt",
    padding=True
)

with torch.no_grad():
    outputs = model(**inputs)
    logits_per_image = outputs.logits_per_image  # (1, 4)
    probs = logits_per_image.softmax(dim=-1)

for label, prob in zip(candidate_labels, probs[0]):
    print(f"{label}: {prob.item():.3f}")
# 강아지: 0.923, 고양이: 0.041, 새: 0.022, 물고기: 0.014

이미지-텍스트 검색

CLIP 임베딩으로 대규모 이미지 데이터베이스를 구축하면, 텍스트 쿼리로 의미 기반 이미지 검색이 가능하다.

import numpy as np
from typing import List

def build_image_index(
    image_paths: List[str], model, processor
) -> np.ndarray:
    """이미지 임베딩 인덱스 구축"""
    embeddings = []
    for path in image_paths:
        image = Image.open(path)
        inputs = processor(images=image, return_tensors="pt")
        with torch.no_grad():
            feat = model.get_image_features(**inputs)
            feat = feat / feat.norm(dim=-1, keepdim=True)
        embeddings.append(feat.cpu().numpy())
    return np.vstack(embeddings)


def search_by_text(
    query: str,
    image_embeddings: np.ndarray,
    model, processor,
    top_k: int = 5
) -> List[int]:
    """텍스트 쿼리로 유사 이미지 검색"""
    inputs = processor(text=[query], return_tensors="pt", padding=True)
    with torch.no_grad():
        txt_feat = model.get_text_features(**inputs)
        txt_feat = txt_feat / txt_feat.norm(dim=-1, keepdim=True)

    similarities = (image_embeddings @ txt_feat.T.numpy()).squeeze()
    return similarities.argsort()[::-1][:top_k].tolist()

CLIP 파생 모델 생태계

CLIP 파생 응용 모델

DALL-E 2는 CLIP 이미지 임베딩을 prior 모델로 변환한 뒤 확산 모델로 이미지를 생성한다. Stable Diffusion은 CLIP 텍스트 인코더의 출력을 UNet의 크로스 어텐션에 주입해 텍스트-이미지 정렬을 달성한다. LLaVA·InstructBLIP 등 VLM들은 CLIP 비전 인코더로 추출한 이미지 피처를 LLM에 프로젝션해 멀티모달 이해를 가능하게 한다.

CLIP의 한계와 개선 방향

강점: 제로샷 일반화, 멀티모달 표현, 노이즈 데이터에 강건.

약점: 미세한 시각 추론 약함(숫자 세기, 공간 관계), 도메인 특화 데이터에서 SOTA에 못 미칠 수 있음, 4억 쌍이라는 대규모 학습 데이터 요구.

SigLIP(Google)은 Softmax 대신 Sigmoid 손실을 사용해 배치 크기에 독립적인 학습을 달성했고, OpenCLIP은 LAION-5B 데이터셋으로 CLIP을 오픈소스로 재현해 접근성을 높였다.

다음 글에서는 CLIP의 비전 인코더인 ViT가 확산 모델과 만나는 Diffusion 모델의 기초를 다룬다.


지난 글: Vision Transformer(ViT): 이미지를 문장처럼 처리하는 Transformer

다음 글: 확산 모델(Diffusion Model) 기초: 노이즈에서 이미지로


읽어주셔서 감사합니다. 😊

KEEP READING

이어 읽기

AI 기초2026.07.29

검증자 모델: 답을 고르는 눈을 따로 기른다

규칙 검증기, 결과 보상 모델(ORM), 과정 보상 모델(PRM), 자기 검증의 특성과 한계를 비교하고, 여러 후보 중 정답을 골라내는 검증 계층을 실무에서 어떻게 구성할지 코드와 함께 정리합니다.

중급16 MIN
AI 기초2026.07.29

사고 예산 제어: 얼마나 생각하게 할 것인가

사고 예산과 출력 상한의 관계, 난도에 따른 적응형 예산 배분, 예산 소진으로 답변이 잘리는 실패 모드와 그 방어책을 실무 코드와 운영 지표 중심으로 정리합니다.

중급14 MIN