수달이네 기술 블로그

2. CLIP모델 구현 본문

AI공부/멀티모달

2. CLIP모델 구현

슬픈 수달이 2026. 3. 14. 13:01

구현

import torch
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox

import seaborn as sns
from PIL import Image
from datasets import load_dataset
from transformers import CLIPProcessor, CLIPModel
from sklearn.metrics.pairwise import cosine_similarity

openai/clip-vit-base-patch32 · Hugging Face

모델은 HuggingFace에 올려져 있는 CLIP-ViTbase모델을 사용한다.

MODEL_NAME = "openai/clip-vit-base-patch32"
DATASET_NAME = "clip-benchmark/wds_imagenetv2"
SPLIT = "test"
SAMPLE_SIZE = 10
THUMB_SIZE = (80, 80)
OUT_PATH = "heatmap.png"
  • CLIP모델과 그 성능을 평가하기 위한 ImageNetV2데이터셋을 이용할 것이다.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
  • 디바이스 설정(일단cpu로 진행할것임.)
model = CLIPModel.from_pretrained(MODEL_NAME).to(device)
processor = CLIPProcessor.from_pretrained(MODEL_NAME)
model.eval()
  • 모델 설정: 사전에 정한 MODEL-NAME을 통해 huggingface에서 모델을 가져온다.
  • processor: 이미지와 텍스트를 모델 입력 형식에 맞게 변환해주는 전처리기.
CLIPModel(
  (text_model): CLIPTextTransformer(
    (embeddings): CLIPTextEmbeddings(
      (token_embedding): Embedding(49408, 512)
      (position_embedding): Embedding(77, 512)
    )
    (encoder): CLIPEncoder(
      (layers): ModuleList(
        (0-11): 12 x CLIPEncoderLayer(
          (self_attn): CLIPAttention(
            (k_proj): Linear(in_features=512, out_features=512, bias=True)
            (v_proj): Linear(in_features=512, out_features=512, bias=True)
            (q_proj): Linear(in_features=512, out_features=512, bias=True)
            (out_proj): Linear(in_features=512, out_features=512, bias=True)
          )
          (layer_norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
          (mlp): CLIPMLP(
            (activation_fn): QuickGELUActivation()
            (fc1): Linear(in_features=512, out_features=2048, bias=True)
            (fc2): Linear(in_features=2048, out_features=512, bias=True)
          )
          (layer_norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
        )
      )
    )
    (final_layer_norm): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
  )
  (vision_model): CLIPVisionTransformer(
    (embeddings): CLIPVisionEmbeddings(
      (patch_embedding): Conv2d(3, 768, kernel_size=(32, 32), stride=(32, 32), bias=False)
      (position_embedding): Embedding(50, 768)
    )
    (pre_layrnorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
    (encoder): CLIPEncoder(
      (layers): ModuleList(
        (0-11): 12 x CLIPEncoderLayer(
          (self_attn): CLIPAttention(
            (k_proj): Linear(in_features=768, out_features=768, bias=True)
            (v_proj): Linear(in_features=768, out_features=768, bias=True)
            (q_proj): Linear(in_features=768, out_features=768, bias=True)
            (out_proj): Linear(in_features=768, out_features=768, bias=True)
          )
          (layer_norm1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
          (mlp): CLIPMLP(
            (activation_fn): QuickGELUActivation()
            (fc1): Linear(in_features=768, out_features=3072, bias=True)
            (fc2): Linear(in_features=3072, out_features=768, bias=True)
          )
          (layer_norm2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
        )
      )
    )
    (post_layernorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  )
  (visual_projection): Linear(in_features=768, out_features=512, bias=False)
  (text_projection): Linear(in_features=512, out_features=512, bias=False)
)
  • 위는 모델의 구조를 출력한 내용이다.

Text Encoder(CLIPTextTransformer)

 (text_model): CLIPTextTransformer(
    (embeddings): CLIPTextEmbeddings(
      (token_embedding): Embedding(49408, 512)
      (position_embedding): Embedding(77, 512)
    )
    (encoder): CLIPEncoder(
      (layers): ModuleList(
        (0-11): 12 x CLIPEncoderLayer(
          (self_attn): CLIPAttention(
            (k_proj): Linear(in_features=512, out_features=512, bias=True)
            (v_proj): Linear(in_features=512, out_features=512, bias=True)
            (q_proj): Linear(in_features=512, out_features=512, bias=True)
            (out_proj): Linear(in_features=512, out_features=512, bias=True)
          )
          (layer_norm1): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
          (mlp): CLIPMLP(
            (activation_fn): QuickGELUActivation()
            (fc1): Linear(in_features=512, out_features=2048, bias=True)
            (fc2): Linear(in_features=2048, out_features=512, bias=True)
          )
          (layer_norm2): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
        )
      )
    )
    (final_layer_norm): LayerNorm((512,), eps=1e-05, elementwise_affine=True)
  )
  • Embedding(임베딩)
    • token_embedding: 텍스트 토큰(어휘 49408개)을 512차원의 벡터로 변환
    • position_embedding: 77개의 토큰으로 위치정보 부여(Positional encoding)
  • Encoder(Transformer기반) → 12개의 EncoderLayer 존재(아래의 층이 12번 반복)
    • self-attention → (토큰 간 관계(문맥, 상호작용)을 학습)
      • q, k, v_proj: 입력을 각각 query, key, value로 변환하는 linear층
      • out_proj: 결과를 원래 차원으로 다시 매핑
    • Layer_norm1,2: 레이어 정규화 → (안정적인 학습)
      • 한개의 입력샘플 내부의 모든 입력값의 평균, 분산을 계산해 정규화
    • MLP(Feed-Forward Network) → (복잡한 패턴 학습)
      • fc1: 512차원 → 2058차원
      • fc2: 2048차원 → 512차원
      • GELU로 비선형 활성화
    • final_layer_norm: 마지막 출력에 정규화 → 안정성

Vision Encoder(CLIPVisionTransformer)

  (vision_model): CLIPVisionTransformer(
    (embeddings): CLIPVisionEmbeddings(
      (patch_embedding): Conv2d(3, 768, kernel_size=(32, 32), stride=(32, 32), bias=False)
      (position_embedding): Embedding(50, 768)
    )
    (pre_layrnorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
    (encoder): CLIPEncoder(
      (layers): ModuleList(
        (0-11): 12 x CLIPEncoderLayer(
          (self_attn): CLIPAttention(
            (k_proj): Linear(in_features=768, out_features=768, bias=True)
            (v_proj): Linear(in_features=768, out_features=768, bias=True)
            (q_proj): Linear(in_features=768, out_features=768, bias=True)
            (out_proj): Linear(in_features=768, out_features=768, bias=True)
          )
          (layer_norm1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
          (mlp): CLIPMLP(
            (activation_fn): QuickGELUActivation()
            (fc1): Linear(in_features=768, out_features=3072, bias=True)
            (fc2): Linear(in_features=3072, out_features=768, bias=True)
          )
          (layer_norm2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
        )
      )
    )
    (post_layernorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  )
  • Embedding
    • ImageNetV2를 224x224로 resize후 이용할 것임.
    • patch_embedding: Conv를 이용해 이미지를 분할(kernel 크기 = stride크기 = 32px)
      • 왜? → 분할과 동시에 각각 임베딩까지 한번에 처리
      • 동시에 (32x32x3픽셀)차원에서 768차원 벡터로 압축(충분한 차원 학습) → 차후 projection 단계에서 768→512차원으로 바꾸어 텍스트와 연결함.
    • patch_embedding 이후 224 ÷ 32 = 7 즉, 7×7 = 49개의 패치 + CLS토큰 50개
    • position_embedding: 위에서 출력된 패치 별로 위치 인코딩 진행.(50토큰)
  • pre_layernorm: 연산 전 정규화를 해줌 (텍스트보다 이미지가 차원이 크고 변동성이 있으므로)
  • Encoder(ViT) → 12개의 EncoderLayer 존재(아래의 층이 12번 반복)
    • 텍스트의 transformer와 층 구조 동일
  (visual_projection): Linear(in_features=768, out_features=512, bias=False)
  (text_projection): Linear(in_features=512, out_features=512, bias=False)
  • 이후 projection으로 두 출력의 차원을 동일시 해준다.
# 데이터셋을 불러오고, test split에서 섞어서 10개만 고름
dataset = load_dataset(DATASET_NAME)
ds = dataset[SPLIT]
subset = ds.shuffle(seed=2026).select(range(SAMPLE_SIZE))
  • 데이터셋(ImageNetV2)을 불러옴
cls2label = [line.strip() for line in open("./dataset/classnames.txt", "r", encoding="utf-8").readlines()]
cls2label

# ['tench',
#  'goldfish',
#  'great white shark',
#  'tiger shark',
#  ...
# ]
  • 준비된 라벨셋을 불러온 후
def to_pil(x):
    if isinstance(x, Image.Image):
        return x.convert("RGB")
        # subset에서 webp 컬럼을 꺼내 PIL 이미지 리스트를 만듦
        # images = [to_pil(x) for x in list(subset("webp"))]
    if isinstance(x, dict) and "bytes" in x and x["bytes"] is not None:
        return Image.open(io.BytesIO(x["bytes"])).convert("RGB")
    if isinstance(x, str):
        return Image.open(x).convert("RGB")
    raise TypeError(f'Unsupported image type: {type(x)}')
label_names = [cls2label[int(c)] for c in list(subset['cls'])]
label_texts = [f'a photo of a {name}' for name in label_names]

images = [to_pil(x) for x in list(subset['webp'])]
  • 이미지를 파이썬에서 사용하는 이미지 처리 모듈 Pillow에 호환되는 객체로 변환하는 함수
with torch.no_grad():
    # 이미지 임베딩
    # processor가 이미지를 CLIP이 받는 파이토치 텐서 형태로 변환 (batch, 3, H, W)
    inputs_image = processor(images=images, return_tensors='pt', padding=True).to(device)
    # vision_model이 비전 트랜스포머를 통과시켜 출력
    vision_out = model.vision_model(pixel_values=inputs_image['pixel_values'])
    # pooler_output = "대표 임베딩" 같은 역할을 하는 벡터(보통 CLS 토큰 기반)
    # visual_projection으로 CLIP 공통 임베딩 공간에 차원에 맞춰 projection 함
    image_features = model.visual_projection(vision_out.pooler_output)

    # 텍스트 임베딩
    # 텍스트도 토크나이징/패딩 후 모델 통과
    inputs_text = processor(text=label_texts, return_tensors='pt', padding=True).to(device)
    text_out = model.text_model(
        input_ids = inputs_text['input_ids'],
        attention_mask = inputs_text['attention_mask']
    )
    text_features = model.text_projection(text_out.pooler_output)
  • 이미지와 텍스트를 임베딩
# 정규화 + 유사도 계산(코사인 유사도)
# 각 벡터를 L2 정규화하면 길이가 1이 됨
# 내적(@)이 코사인 유사도
# 결과: (10, D) @ (D, 10) = (10, 10)
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
similarity_matrix = (image_features @ text_features.T).cpu().numpy()
  • 코사인 유사도 계산(대비 학습)
fig, ax = plt.subplots(figsize=(20, 12))
im = ax.imshow(similarity_matrix, aspect='auto')
plt.colorbar(im, ax=ax, fraction=0.02, pad=0.02)

ax.set_xticks(np.arange(len(label_texts)))
ax.set_xticklabels(label_texts, rotation=45, ha='right')

ax.set_yticks(np.arange(len(images)))
ax.set_yticklabels([""] * len(images))

ax.set_title('CLIP Image vs. Label Similarity (Cosine Similarity)')
ax.set_xlabel('Label Text')
ax.set_ylabel('Image')

for i in range(similarity_matrix.shape[0]):
    for j in range(similarity_matrix.shape[1]):
        ax.text(j, i, f'{similarity_matrix[i, j]:.2f}', ha='center', va='center', fontsize=8)

#
for i, img in enumerate(thumbnails):
    imagebox = OffsetImage(img, zoom=1.0)
    # AnnotationBbox를 사용해서 좌표 (-0.6, i) 위치에 썸네일 이미지를 붙임
    # (-0.6, i)는 히트맵의 첫 열(0)보다 왼쪽에 배치하기 위해서
    ab = AnnotationBbox(imagebox, (-0.6, i), frameon=False, xycoords='data', boxcoords='data', pad=0)
    ax.add_artist(ab)

ax.set_xlim(-1.2, similarity_matrix.shape[1] - 0.5)
ax.set_ylim(similarity_matrix.shape[0] - 0.5, -0.5)

plt.tight_layout()
plt.savefig(OUT_PATH, dpi=200)
plt.show()
print(f'saved: {OUT_PATH}')
  • 학습한 이미지의 유사도를 출력