1. CLIP 모델 개요 – 내가 구현한 것은 무엇인가?

이번 글에서는 OpenAI의 CLIP(Contrastive Language–Image Pretraining) 모델을 이용해
이미지를 입력하면, 미리 정의한 텍스트 라벨 중 어떤 개념과 가장 유사한지 추론하는 모델을 구현해보았다.

특징은 다음과 같다.

  • 사전 학습된 CLIP 모델 사용
  • 추가 학습 없이 (Zero-shot) 이미지 분류 수행
  • 이미지와 텍스트를 같은 임베딩 공간으로 변환
  • 여러 개의 텍스트 프롬프트를 사용하는 Prompt Ensemble 적용 여부 비교

즉,

“이 이미지는 고양이입니다” 같은 정답 라벨을 학습시키는 방식이 아니라
이미지와 문장 간의 의미적 유사도를 계산해 분류하는 방식이다.

 

초기 구현 단계에서는 CLIP의 구조와 동작 원리를 명확히 이해하지 못했기 때문에
이미지 URL, 텍스트 라벨, 프롬프트 템플릿을 직접 하드코딩하여 실험 형태로 구현했다.


2. 라이브러리 및 모델 로딩

from PIL import Image import requests 
import torch from transformers 
import CLIPProcessor, CLIPModel from IPython.display 
import display
  • transformers : Hugging Face에서 제공하는 CLIP 모델 로딩
  • CLIPModel : 이미지 인코더 + 텍스트 인코더를 포함한 모델
  • CLIPProcessor : 이미지 전처리 + 텍스트 토크나이징을 동시에 담당
  • PIL, requests : 이미지 URL을 불러오기 위한 용도
device = "cuda" if torch.cuda.is_available() else "cpu" 
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device) 
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
  • ViT-B/32 기반 CLIP 모델 사용
  • GPU가 있다면 CUDA 사용
  • 학습은 하지 않고, pretrained weight 그대로 사용

3. 이미지 데이터 준비 (직접 입력)

urls = [ "https://images.pexels.com/photos/103123/pexels-photo-103123.jpeg", ... ]

초기 구현 단계에서는 데이터셋을 쓰지 않고,
이미지 URL을 직접 입력해서 CLIP이 어떤 결과를 내는지 확인했다.

images = [] for url in urls: img = Image.open(requests.get(url, stream=True).raw) images.append(img)
  • URL → PIL Image 객체 변환
  • 이후 CLIP Processor를 통해 모델 입력 형태로 변환됨

4. 텍스트 라벨 & 프롬프트 템플릿 구성

labels = ["animal", "object", "banana", "bird", "person","flower","food","scenery"]
  • CLIP은 정해진 클래스가 없음
  • 내가 직접 분류하고 싶은 개념(라벨)을 정의해야 함
templates = [ "a photo of a {}", "a close-up photo of a {}", "a blurry photo of a {}", ... ]

여기서 중요한 개념이 Prompt Engineering이다.

같은 라벨이라도

  • “a photo of a bird”
  • “a close-up photo of a bird”
  • “a photo of a bird in the wild”

처럼 표현이 다르면 임베딩 결과도 달라진다.

texts = [t.format(l) for l in labels for t in templates]

그래서 모든 라벨 × 모든 템플릿 조합을 만들어 텍스트 후보 집합을 구성했다.


5. 텍스트 임베딩 사전 계산

inputs_text = processor(text=texts, return_tensors="pt", padding=True).to(device)
  • 텍스트는 한 번에 배치 처리 가능
  • 이미지와 달리 길이만 다를 뿐 구조가 동일하기 때문
with torch.no_grad(): 
	text_embeds = model.get_text_features(**inputs_text) 
	text_embeds = text_embeds / text_embeds.norm(dim=-1, keepdim=True)
  • get_text_features() → 텍스트 임베딩 추출
  • L2 정규화
    • CLIP은 코사인 유사도 기반 비교를 하기 때문

6. Prompt Ensemble (프롬프트 앙상블)

text_embeds_ensemble = text_embeds.view(len(labels), num_templates, -1).mean(dim=1)

여기서 프롬프트 앙상블을 적용했다.

  • 같은 라벨에 대해 여러 문장 프롬프트 사용
  • 각각의 임베딩을 평균내어 라벨 대표 임베딩 생성

📌 효과

  • 특정 문장 표현에 과도하게 의존하지 않음
  • Zero-shot 분류 성능이 더 안정적

7. 이미지 임베딩 및 유사도 계산

inputs_image = processor(images=image, return_tensors="pt").to(device)
  • 이미지는 1장씩 처리
  • 이미지마다 해상도/비율이 다르기 때문
image_embeds = model.get_image_features(**inputs_image) 
image_embeds = image_embeds / image_embeds.norm(dim=-1, keepdim=True)

8. 프롬프트 앙상블 적용 전 vs 후 비교

(1) 프롬프트 앙상블 미적용

logits_templates = image_embeds @ text_embeds.T 
probs_templates = logits_templates.softmax(dim=1)
  • 이미지 ↔ 모든 텍스트 문장 비교
  • 가장 유사한 문장 단위 결과 출력

(2) 프롬프트 앙상블 적용

logits_labels = image_embeds @ text_embeds_ensemble.T 
probs_labels = logits_labels.softmax(dim=1)
  • 이미지 ↔ 라벨 단위 비교
  • 실제 분류에 가까운 결과

9. 최종 코드 및 결과

from PIL import Image
import requests
import torch
from transformers import CLIPProcessor, CLIPModel
from IPython.display import display

# 모델과 프로세서
device = "cuda" if torch.cuda.is_available() else "cpu"
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device)
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# 이미지 불러오기
urls = [
    "https://images.pexels.com/photos/103123/pexels-photo-103123.jpeg",
    "https://images.pexels.com/photos/414712/pexels-photo-414712.jpeg",
    "https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg",
    "https://images.pexels.com/photos/17474740/pexels-photo-17474740.jpeg",
    "https://images.pexels.com/photos/10875195/pexels-photo-10875195.jpeg",
    "https://images.pexels.com/photos/34026276/pexels-photo-34026276.jpeg"
]

images = []
for url in urls:
    img = Image.open(requests.get(url, stream=True).raw)
    images.append(img)

# 라벨과 템플릿 준비
templates = [
    "a photo of a {}",
    "a close-up photo of a {}",
    "a blurry photo of a {}",
    "a cropped photo of a {}",
    "a photo of a {} in the wild",
    "a photo of a {} outdoors",
]
labels = ["animal", "object", "banana", "bird", "person","flower","food","scenery"]

# 모든 텍스트 후보 만들기
texts = [t.format(l) for l in labels for t in templates]

# 텍스트 임베딩 미리 계산
# 배치처리 가능으로 인해 texts들을 한꺼번에 임베딩 할 수 있음
inputs_text = processor(text=texts, return_tensors="pt", padding=True).to(device)

with torch.no_grad():
    text_embeds = model.get_text_features(**inputs_text)
    text_embeds = text_embeds / text_embeds.norm(dim=-1, keepdim=True)

num_templates = len(templates)

# 프롬프트 앙상블
text_embeds_ensemble = text_embeds.view(len(labels), num_templates, -1).mean(dim=1)  # [num_labels, dim]


# 이미지별 처리
topk = 3
thumb_size = (400, 500)

for idx, image in enumerate(images):
    # 이미지 임베딩
    # text와 다르게 이미지는 사이즈 등이 다르기 때문에 processer에서는 1:1로 다루려고 하므로 각각 하였다.
    inputs_image = processor(images=image, return_tensors="pt").to(device)
    with torch.no_grad():
        image_embeds = model.get_image_features(**inputs_image)
        image_embeds = image_embeds / image_embeds.norm(dim=-1, keepdim=True)

    # 프롬프트 앙상블 미적용
    logits_templates = image_embeds @ text_embeds.T
    probs_templates = logits_templates.softmax(dim=1)
    
    # top-3
    values, indices = probs_templates.topk(topk, dim=1)
    print("\n프롬프트 앙상블 미적용: Top-3 템플릿")
    for rank, (v, i) in enumerate(zip(values[0], indices[0]), 1):
        print(f"{rank}. '{texts[i]}' - 확률: {v.item():.3f}")

    # 프롬프트 앙상블 적용
    logits_labels = image_embeds @ text_embeds_ensemble.T  # [1, num_labels]
    probs_labels = logits_labels.softmax(dim=1)

    # top-k 라벨 출력
    values, indices = probs_labels.topk(topk, dim=1)
    print(f"\n프롬프트 앙상블 적용: Top-{topk} 라벨")
    for rank, (v, i) in enumerate(zip(values[0], indices[0]), 1):
        print(f"{rank}. '{labels[i]}' - 확률: {v.item():.3f}")

    # 이미지 출력
    image_copy = image.copy()
    image_copy.thumbnail(thumb_size)
    display(image_copy)

9. 구현 회고 (초기 구현의 한계)

이 코드는 CLIP을 처음 다루면서 작성한 코드라서:

  • 이미지, 라벨, 프롬프트를 모두 직접 정의
  • 실제 데이터셋 기반 학습/평가는 없음
  • “CLIP이 이런 식으로 동작하는구나”를 확인하는 실험용 구현

하지만 이 과정을 통해 다음을 명확히 이해할 수 있었다.

  • CLIP은 분류 모델이 아니라 임베딩 모델
  • Zero-shot 분류의 핵심은 텍스트 설계
  • Prompt Ensemble이 성능에 미치는 영향

겨울방학 때 현장실습을 지원하여 인공지능 데이터셋을 다루는 기업에 합격을 하게 되어 겨울방학 기간 동안 다니게 되었다.
먼저 실습기간 동안 해야할 업무와 기업의 교육 목표를 참고한 후 출근을 하였다.


직무: 인공지능 딥러닝 모델 및 데이터 셋 검증
교육 목표 :인공지능 딥러닝 모델을 학습시키는 과정을 이해하고 직접 품질 검증을 실습 주 실습 내용 본 현장실습에서는 인공지능 딥러닝 모델의 학습 구조를 이해하고 실제 산업 현장에서 활용되는 데이터를 기반으로 데이터 셋 품질 검증 및 모델 성능 평가 과정을 실습합니다. 학생들은 AI 모델의 학습 데이터 구축 절차를 직접 경험하며 데이터 신뢰성 확보와 AI 성능 향상에 필요한 실무 역량을 습득하게 됩니다.

3학년 1학기에 데이터 사이언스 과목을 수강하였기에 머신러닝과 딥러닝에 대해 어느정도 개념과 코드들이 익숙하였다.
실습하기에 앞서 어느정도 기본 개념은 있어야 하기에 이사님이 텐서플로우, 파이토치에 대해서 공부하고 그것에 대한 결과물을 만들어 오라는 과제를 내주셨다. 제약조건도 CNN과 YOLO 같은 옛날 모델은 지양하라는 점을 추가 하였다. 그 결과물에 따라서 각자가 맡을 테스크가 달라질 것이라는 말씀을 해주셨다.
그래서 1주일 동안 계속 공부만 하였다. 유튜브, LLM, 논문 영상, github page 등을 찾아 보면서 공부했던 내용들을 적어보려 한다.

 

2026.01.11 - [현장실습 일기] - 회고 1주차 - PyTorch로 로지스틱 회귀 구현

 

회고 1주차 - PyTorch로 로지스틱 회귀 구현

PyTorch로 로지스틱 회귀(Logistic Regression) 직접 구현하기1. 데이터셋 다운로드 및 NumPy로 데이터 로드import kagglehubimport osimport numpy as npfrom kagglehub import KaggleDatasetAdapterpath = kagglehub.dataset_download("uciml/p

younglook.tistory.com

2026.01.11 - [현장실습 일기] - 회고 1주차 – CLIP 모델 첫 구현과 Zero-shot 분류 실험

 

회고 1주차 – CLIP 모델 첫 구현과 Zero-shot 분류 실험

1. CLIP 모델 개요 – 내가 구현한 것은 무엇인가?이번 글에서는 OpenAI의 CLIP(Contrastive Language–Image Pretraining) 모델을 이용해이미지를 입력하면, 미리 정의한 텍스트 라벨 중 어떤 개념과 가장 유사

younglook.tistory.com

2026.01.11 - [현장실습 일기] - 회고 1주차 – Oxford-IIIT Pet Dataset으로 CLIP 적용하기

 

회고 1주차 – Oxford-IIIT Pet Dataset으로 CLIP 적용하기

CLIP 모델의 동작 방식을 이해하기 위해 이미지와 텍스트를 직접 입력하는 방식으로 Zero-shot 분류를 실험해보았다.이번 글에서는 한 단계 더 나아가 실제 데이터셋(Oxford-IIIT Pet Dataset)을 사용해 CLI

younglook.tistory.com

 

단순히 라이브러리를 사용하는 수준을 넘어, 로지스틱 회귀가 실제로 어떻게 동작하는지를 코드 단위로 이해할 수 있었다.
특히 nn.Linear 하나만으로도 로지스틱 회귀 모델이 구성된다는 점이 인상 깊었고 수식으로만 보던 개념이 실제 코드로 구현되면서 이해가 훨씬 명확해졌다. 처음에는 loss 값이 매우 크고 정확도도 낮아 제대로 학습이 되는 게 맞는지 의문이 들었지만 epoch이 증가할수록 loss가 안정적으로 감소하고 accuracy가 점진적으로 올라가는 과정을 직접 확인하면서 모델이 데이터를 통해 학습해 나간다는 감각을 확실히 느낄 수 있었다.

이후 CLIP 모델을 구현하면서는 기존의 지도학습 방식과는 전혀 다른 접근을 경험할 수 있었다. CLIP은 명시적인 학습 과정 없이도 이미지와 텍스트를 동일한 임베딩 공간에 매핑하여 Zero-shot 분류가 가능하다는 점이 매우 인상적이었다. 특히 이미지 자체를 분류하는 것이 아니라 이미지와 텍스트 간의 의미적 유사도를 계산해 결과를 도출한다는 구조를 코드로 직접 확인하면서 모델의 관점이 어떻게 달라지는지를 이해할 수 있었다.

또한 Oxford-IIIT Pet Dataset을 활용한 실험을 통해, 단순한 단일 프롬프트보다 여러 텍스트 템플릿을 활용한 Prompt(Template) Ensemble 방식이 성능과 예측 신뢰도를 모두 개선한다는 점을 정량적으로 확인할 수 있었다. 이는 모델 구조를 바꾸지 않더라도 입력 텍스트 설계만으로 결과가 크게 달라질 수 있다는 것을 보여주었고 딥러닝 모델에서 입력 설계가 얼마나 중요한 요소인지를 다시 한 번 느끼게 해주었다.

PyTorch로 로지스틱 회귀(Logistic Regression) 직접 구현하기

1. 데이터셋 다운로드 및 NumPy로 데이터 로드

import kagglehub
import os
import numpy as np
from kagglehub import KaggleDatasetAdapter
path = kagglehub.dataset_download("uciml/pima-indians-diabetes-database")

print("Path to dataset files:", path)

file_path = os.path.join(path, "diabetes.csv")
# Load the latest version
loaded_data=np.loadtxt(file_path,delimiter=',',skiprows=1)

x_train_np=loaded_data[:,0:-1]
y_train_np=loaded_data[:,[-1]]

print('loaded_data.shape=',loaded_data.shape)
print('x_train_np.shape=',x_train_np.shape)
print('y_train_np.shape=',y_train_np.shape)
  • KaggleHub를 사용해 Pima Indians Diabetes 데이터셋을 다운로드
  • np.loadtxt로 CSV 파일을 NumPy 배열로 로드
  • skiprows=1
    → CSV 첫 줄은 컬럼명이므로 학습 데이터에서 제외
  • 입력 데이터(x_train_np)
    • 마지막 열을 제외한 8개의 특성(feature)
  • 정답 데이터(y_train_np)
    • 마지막 열(당뇨병 여부, 0 또는 1)

이렇게 분리하는 이유는 머신러닝에서는 입력 X와 정답 y를 명확히 분리해야 모델 학습이 가능하다.


2. NumPy → PyTorch Tensor 변환

import torch
from torch import nn

x_train = torch.Tensor(x_train_np)
y_train = torch.Tensor(y_train_np)

 

  • PyTorch 모델은 Tensor 타입만 연산 가능
  • NumPy 배열을 그대로 사용할 수 없기 때문에 torch.Tensor()로 변환
  • 이후 연산(Forward, Backward, Optimizer)이 모두 PyTorch 기준으로 수행됨

 


3. 로지스틱 회귀 모델 정의

class MyLogisticRegressionModel(nn.Module):

    def __init__(self):
        super().__init__()
        self.logistic_stack = nn.Sequential(
            nn.Linear(8, 1)
        )

    def forward(self, data):
        pred = self.logistic_stack(data)
        return pred

 

  • nn.Module을 상속받아 사용자 정의 모델 생성
  • nn.Linear(8, 1)
    • 입력 특성: 8개
    • 출력: 1개 (이진 분류)
  • Sigmoid를 사용하지 않은 이유는 뒤에서 BCEWithLogitsLoss를 사용하기 때문

로지스틱 회귀 수식

y = wx + b

 

PyTorch에서는 이 선형 연산을 nn.Linear 하나로 표현 가능


4. 모델 파라미터 확인

model = MyLogisticRegressionModel()

for param in model.parameters():
    print(param)

  • 모델이 실제로 어떤 파라미터를 학습하는지 확인
  • 출력값:
    • 가중치(weight)
    • 편향(bias)

학습이 진행되면 이 값들이 계속 업데이트됨


5. 손실 함수와 옵티마이저 설정

loss_function = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

손실 함수

  • BCEWithLogitsLoss
    • Sigmoid + Binary Cross Entropy를 내부적으로 포함
    • 수치적으로 더 안정적
  • 이진 분류 문제에 가장 적합

옵티마이저

  • Adam
    • 학습이 빠르고 안정적
    • 실무에서도 가장 많이 사용
  • lr=0.001은 기본적으로 무난한 학습률

6. 학습 루프 (Training Loop)

train_loss_list = []
train_accuracy_list = []

nums_epoch = 10000

for epoch in range(nums_epoch + 1):
    outputs = model(x_train)
    loss = loss_function(outputs, y_train)

    train_loss_list.append(loss.item())

    pred = outputs > 0.5
    correct = (pred.float() == y_train)
    accuracy = correct.sum().item() / len(correct)

    train_accuracy_list.append(accuracy)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if epoch % 100 == 0:
        print('epoch=', epoch, 'current loss=', loss.item(), 'accuracy=', accuracy)

 

  1. Forward
    • 입력 데이터를 모델에 통과시켜 예측값 생성
  2. Loss 계산
    • 예측값과 실제값 비교
  3. Accuracy 계산
    • 0.5 기준으로 True / False 분류
  4. Backward
    • loss.backward()로 기울기 계산
  5. Optimizer step
    • 가중치 업데이트

optimizer.zero_grad()
→ 이전 epoch의 gradient 누적 방지


7. 학습 결과 출력 로그

  • 초기에는 loss가 매우 크고 정확도도 낮음
  • 학습이 진행되면서
    • Loss 감소
    • Accuracy 증가
  • 최종 정확도 약 76% 수준

로지스틱 회귀 + 단순 학습만으로도 꽤 의미 있는 결과


8. 학습 후 파라미터 확인

for param in model.parameters():
    print(param)

  • 학습 전과 비교하면 가중치와 bias가 확연히 달라짐
  • 모델이 데이터의 패턴을 학습했다는 증거

9. Loss 변화 시각화

import matplotlib.pyplot as plt

plt.title("Loss Trend")
plt.xlabel("epochs")
plt.ylabel("loss")
plt.grid()

plt.plot(train_loss_list, label="train loss")
plt.legend(loc='best')

plt.show()

  • Epoch이 증가할수록 loss가 안정적으로 감소
  • 학습이 정상적으로 수렴하고 있음을 확인 가능

10/ Accuracy 변화 시각화

plt.title("Loss Trend")
plt.xlabel("epochs")
plt.ylabel("loss")
plt.grid()

plt.plot(train_accuracy_list, label="train accuracy")
plt.legend(loc='best')

plt.show()

  • 초반 급격한 정확도 상승
  • 이후 완만하게 증가하며 수렴

마무리 정리

  • PyTorch로 로지스틱 회귀를 처음부터 직접 구현
  • 데이터 로드 → 모델 정의 → 학습 → 시각화 전체 흐름 이해

 

+ Recent posts