목차
1. [멀티모달 AI 개요](#1-멀티모달-ai-개요)
2. [CLIP 심층 분석](#2-clip-심층-분석)
3. [BLIP 계열](#3-blip-계열)
4. [LLaVA: 대규모 언어-비전 어시스턴트](#4-llava-대규모-언어-비전-어시스턴트)
5. [InstructBLIP](#5-instructblip)
6. [GPT-4 Vision](#6-gpt-4-vision)
7. [Gemini Vision](#7-gemini-vision)
8. [Claude Vision](#8-claude-vision)
9. [멀티모달 RAG](#9-멀티모달-rag)
10. [오픈소스 멀티모달 모델](#10-오픈소스-멀티모달-모델)
11. [비디오 이해 AI](#11-비디오-이해-ai)
1. 멀티모달 AI 개요
단일 모달리티의 한계
기존 AI 시스템은 텍스트, 이미지, 오디오 중 하나의 데이터 형태(모달리티)만 처리할 수 있었습니다. 이러한 단일 모달리티 접근 방식은 실세계의 복잡한 문제를 해결하는 데 근본적인 한계를 가지고 있습니다.
**텍스트 전용 모델의 한계:**
- 이미지 설명 요청 시 이미지 자체를 분석 불가
- 도표, 차트, 스크린샷의 내용 이해 불가
- 시각적 컨텍스트 없는 의사결정
**이미지 전용 모델의 한계:**
- 이미지 내 텍스트와 시각적 요소의 복합적 이해 불가
- 언어 기반 질의응답 불가
- 자연어 설명을 통한 검색 불가
멀티모달 AI의 가능성
멀티모달 AI는 여러 형태의 데이터를 동시에 처리하고 이해할 수 있는 시스템입니다. 인간의 자연스러운 인지 방식인 "보고, 듣고, 읽으면서 동시에 이해하는" 능력을 AI가 갖추게 됩니다.
**주요 활용 분야:**
- **의료 진단**: 의료 영상 + 환자 기록 텍스트 통합 분석
- **자율주행**: 카메라 + 라이더 + 지도 데이터 통합
- **교육**: 교재 이미지 + 설명 텍스트 자동 생성
- **이커머스**: 제품 사진 + 설명 + 리뷰 통합 처리
- **문서 이해**: 스캔 문서 OCR + 내용 분석
- **창작**: 텍스트 설명으로 이미지 생성 (DALL-E, Stable Diffusion)
비전-언어 모델의 발전사
2021: CLIP (OpenAI) - 대조 학습으로 이미지-텍스트 연결
2022: BLIP - 이미지 캡셔닝과 VQA 통합
2023: BLIP-2 - Q-Former로 효율적 멀티모달 학습
2023: LLaVA - 오픈소스 비전-언어 어시스턴트
2023: GPT-4V - 상업용 멀티모달 LLM
2023: Gemini - 구글의 멀티모달 파운데이션 모델
2024: Claude 3 Vision - Anthropic의 멀티모달 모델
2024: LLaVA-1.6, InternVL2, Qwen-VL2 - 오픈소스 개선
2025: 비디오 이해, 3D 이해로 확장
2. CLIP 심층 분석
CLIP의 핵심 아이디어
CLIP(Contrastive Language-Image Pre-training)은 2021년 OpenAI가 발표한 모델로, 4억 개의 이미지-텍스트 쌍으로 대조 학습(Contrastive Learning)을 수행하여 이미지와 텍스트를 동일한 임베딩 공간에 매핑합니다.
**핵심 혁신**: 별도의 레이블 없이 인터넷에서 수집한 이미지-캡션 쌍만으로 학습하여 강력한 제로샷 분류 능력을 획득했습니다.
CLIP 아키텍처
이미지 → [이미지 인코더 (ViT/ResNet)] → 이미지 임베딩 (512차원)
↕ 유사도 측정
텍스트 → [텍스트 인코더 (Transformer)]→ 텍스트 임베딩 (512차원)
**대조 학습 메커니즘:**
배치 내 N개의 이미지-텍스트 쌍에서:
- 올바른 쌍(diagonal)의 유사도는 최대화
- 잘못된 쌍(off-diagonal)의 유사도는 최소화
from PIL import Image
from transformers import CLIPProcessor, CLIPModel
CLIP 모델 로드
model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
def clip_zero_shot_classification(
image: Image.Image,
candidate_labels: list[str]
) -> dict[str, float]:
"""CLIP을 사용한 제로샷 이미지 분류."""
텍스트 프롬프트 생성 (CLIP의 권장 형식)
text_prompts = [f"a photo of a {label}" for label in candidate_labels]
전처리
inputs = processor(
text=text_prompts,
images=image,
return_tensors="pt",
padding=True
)
추론
with torch.no_grad():
outputs = model(**inputs)
logits_per_image = outputs.logits_per_image
probs = logits_per_image.softmax(dim=1)
결과 반환
return {
label: prob.item()
for label, prob in zip(candidate_labels, probs[0])
}
사용 예시
image_url = "https://example.com/sample_image.jpg"
image = Image.open(requests.get(image_url, stream=True).raw)
labels = ["cat", "dog", "bird", "fish", "rabbit"]
results = clip_zero_shot_classification(image, labels)
확률 기준 정렬
sorted_results = sorted(results.items(), key=lambda x: x[1], reverse=True)
for label, prob in sorted_results:
print(f"{label}: {prob:.4f} ({prob*100:.1f}%)")
이미지-텍스트 검색
from typing import Union
class CLIPSearchEngine:
"""CLIP 기반 이미지-텍스트 검색 엔진."""
def __init__(self, model_name: str = "openai/clip-vit-large-patch14"):
self.model = CLIPModel.from_pretrained(model_name)
self.processor = CLIPProcessor.from_pretrained(model_name)
self.image_embeddings = []
self.text_embeddings = []
self.image_metadata = []
self.text_metadata = []
def encode_images(self, images: list[Image.Image]) -> torch.Tensor:
"""이미지 배치를 임베딩으로 변환."""
inputs = self.processor(
images=images,
return_tensors="pt",
padding=True
)
with torch.no_grad():
image_features = self.model.get_image_features(**inputs)
L2 정규화
image_features = F.normalize(image_features, p=2, dim=-1)
return image_features
def encode_texts(self, texts: list[str]) -> torch.Tensor:
"""텍스트 배치를 임베딩으로 변환."""
inputs = self.processor(
text=texts,
return_tensors="pt",
padding=True,
truncation=True
)
with torch.no_grad():
text_features = self.model.get_text_features(**inputs)
text_features = F.normalize(text_features, p=2, dim=-1)
return text_features
def index_images(
self,
images: list[Image.Image],
metadata: list[dict] = None
):
"""이미지를 인덱싱합니다."""
embeddings = self.encode_images(images)
self.image_embeddings.append(embeddings)
if metadata:
self.image_metadata.extend(metadata)
def text_to_image_search(
self,
query: str,
top_k: int = 5
) -> list[dict]:
"""텍스트 쿼리로 이미지를 검색합니다."""
if not self.image_embeddings:
return []
모든 이미지 임베딩 결합
all_embeddings = torch.cat(self.image_embeddings, dim=0)
쿼리 인코딩
query_embedding = self.encode_texts([query])
코사인 유사도 계산 (이미 L2 정규화됨)
similarities = (all_embeddings @ query_embedding.T).squeeze(-1)
Top-K 결과 선택
top_indices = similarities.argsort(descending=True)[:top_k]
results = []
for idx in top_indices:
idx = idx.item()
result = {
"index": idx,
"similarity": similarities[idx].item()
}
if self.image_metadata:
result.update(self.image_metadata[idx])
results.append(result)
return results
OpenCLIP (오픈소스 CLIP)
OpenCLIP: 다양한 아키텍처와 학습 데이터 지원
pip install open_clip_torch
from PIL import Image
사용 가능한 모델 목록 확인
available_models = open_clip.list_pretrained()
print("사용 가능한 모델:", available_models[:5])
LAION-2B로 학습된 대형 모델 로드
model, preprocess_train, preprocess_val = open_clip.create_model_and_transforms(
'ViT-H-14',
pretrained='laion2b_s32b_b79k'
)
tokenizer = open_clip.get_tokenizer('ViT-H-14')
def compute_clip_similarity(
image: Image.Image,
texts: list[str],
model=model,
preprocess=preprocess_val,
tokenizer=tokenizer
) -> list[float]:
"""이미지와 텍스트 목록 간의 CLIP 유사도를 계산합니다."""
model.eval()
이미지 전처리
image_input = preprocess(image).unsqueeze(0)
텍스트 토큰화
text_input = tokenizer(texts)
with torch.no_grad(), torch.cuda.amp.autocast():
image_features = model.encode_image(image_input)
text_features = model.encode_text(text_input)
정규화
image_features /= image_features.norm(dim=-1, keepdim=True)
text_features /= text_features.norm(dim=-1, keepdim=True)
유사도 계산
similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)
return similarity[0].tolist()
CLIP 파인튜닝
from torch.utils.data import DataLoader, Dataset
from transformers import CLIPModel, CLIPProcessor
class ImageTextDataset(Dataset):
"""이미지-텍스트 쌍 데이터셋."""
def __init__(
self,
image_paths: list[str],
texts: list[str],
processor: CLIPProcessor
):
self.image_paths = image_paths
self.texts = texts
self.processor = processor
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx):
image = Image.open(self.image_paths[idx]).convert("RGB")
text = self.texts[idx]
inputs = self.processor(
images=image,
text=text,
return_tensors="pt",
padding="max_length",
max_length=77,
truncation=True
)
return {
"pixel_values": inputs["pixel_values"].squeeze(0),
"input_ids": inputs["input_ids"].squeeze(0),
"attention_mask": inputs["attention_mask"].squeeze(0)
}
class CLIPFineTuner:
"""CLIP 모델 파인튜닝 클래스."""
def __init__(self, model_name: str = "openai/clip-vit-base-patch32"):
self.model = CLIPModel.from_pretrained(model_name)
self.processor = CLIPProcessor.from_pretrained(model_name)
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model.to(self.device)
def contrastive_loss(
self,
image_features: torch.Tensor,
text_features: torch.Tensor,
temperature: float = 0.07
) -> torch.Tensor:
"""대조 학습 손실 함수."""
정규화
image_features = F.normalize(image_features, dim=-1)
text_features = F.normalize(text_features, dim=-1)
유사도 행렬
logits = torch.matmul(image_features, text_features.T) / temperature
대각선이 정답 (i번째 이미지는 i번째 텍스트와 쌍)
labels = torch.arange(len(logits)).to(self.device)
양방향 크로스 엔트로피
loss_i = F.cross_entropy(logits, labels)
loss_t = F.cross_entropy(logits.T, labels)
return (loss_i + loss_t) / 2
def train(
self,
train_dataset: ImageTextDataset,
num_epochs: int = 10,
batch_size: int = 32,
learning_rate: float = 1e-5
):
"""CLIP 파인튜닝 학습."""
dataloader = DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=4
)
optimizer = optim.AdamW(
self.model.parameters(),
lr=learning_rate,
weight_decay=0.01
)
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=num_epochs
)
for epoch in range(num_epochs):
total_loss = 0
self.model.train()
for batch in dataloader:
pixel_values = batch["pixel_values"].to(self.device)
input_ids = batch["input_ids"].to(self.device)
attention_mask = batch["attention_mask"].to(self.device)
순전파
outputs = self.model(
pixel_values=pixel_values,
input_ids=input_ids,
attention_mask=attention_mask
)
image_features = outputs.image_embeds
text_features = outputs.text_embeds
손실 계산
loss = self.contrastive_loss(image_features, text_features)
역전파
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
scheduler.step()
avg_loss = total_loss / len(dataloader)
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}")
3. BLIP 계열
BLIP (Bootstrapping Language-Image Pre-training)
BLIP은 2022년 Salesforce Research에서 발표한 모델로, 이미지 캡셔닝, 이미지-텍스트 검색, 시각적 질의응답(VQA) 등 다양한 비전-언어 작업에서 뛰어난 성능을 보입니다.
**핵심 혁신**: Captioner와 Filter를 활용한 데이터 부트스트래핑으로 웹에서 수집한 노이즈 많은 이미지-텍스트 쌍을 정제합니다.
from transformers import BlipProcessor, BlipForConditionalGeneration
from transformers import BlipForQuestionAnswering
from PIL import Image
BLIP 이미지 캡셔닝
class BLIPCaptioner:
def __init__(self):
self.processor = BlipProcessor.from_pretrained(
"Salesforce/blip-image-captioning-large"
)
self.model = BlipForConditionalGeneration.from_pretrained(
"Salesforce/blip-image-captioning-large",
torch_dtype=torch.float16
)
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model.to(self.device)
def caption(
self,
image: Image.Image,
conditional_text: str = None,
max_new_tokens: int = 50
) -> str:
"""이미지에 대한 캡션을 생성합니다."""
if conditional_text:
조건부 캡셔닝
inputs = self.processor(
image,
conditional_text,
return_tensors="pt"
).to(self.device, torch.float16)
else:
무조건부 캡셔닝
inputs = self.processor(
image,
return_tensors="pt"
).to(self.device, torch.float16)
with torch.no_grad():
output = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
num_beams=4,
early_stopping=True
)
return self.processor.decode(output[0], skip_special_tokens=True)
BLIP VQA (시각적 질의응답)
class BLIPVisualQA:
def __init__(self):
self.processor = BlipProcessor.from_pretrained(
"Salesforce/blip-vqa-base"
)
self.model = BlipForQuestionAnswering.from_pretrained(
"Salesforce/blip-vqa-base"
)
def answer(self, image: Image.Image, question: str) -> str:
"""이미지에 대한 질문에 답변합니다."""
inputs = self.processor(image, question, return_tensors="pt")
with torch.no_grad():
output = self.model.generate(**inputs, max_new_tokens=50)
return self.processor.decode(output[0], skip_special_tokens=True)
사용 예시
captioner = BLIPCaptioner()
vqa = BLIPVisualQA()
image = Image.open("sample.jpg")
캡션 생성
caption = captioner.caption(image)
print(f"캡션: {caption}")
조건부 캡션
cond_caption = captioner.caption(image, "a photo of")
print(f"조건부 캡션: {cond_caption}")
VQA
answer = vqa.answer(image, "What color is the sky?")
print(f"답변: {answer}")
BLIP-2: Querying Transformer
BLIP-2는 2023년 발표된 BLIP의 후속 모델로, Q-Former(Querying Transformer)를 도입하여 동결된(frozen) 이미지 인코더와 동결된 LLM을 효율적으로 연결합니다.
**Q-Former의 역할:**
- 이미지 인코더의 출력에서 가장 중요한 시각적 특징을 추출
- 학습 가능한 쿼리 토큰 32개가 이미지 특징과 교류하여 압축된 표현 생성
- 이 압축된 표현이 LLM의 입력으로 전달
from transformers import Blip2Processor, Blip2ForConditionalGeneration
class BLIP2Assistant:
"""BLIP-2 기반 시각 질의응답 어시스턴트."""
def __init__(
self,
model_name: str = "Salesforce/blip2-opt-2.7b"
):
self.processor = Blip2Processor.from_pretrained(model_name)
self.model = Blip2ForConditionalGeneration.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
def generate_response(
self,
image: Image.Image,
prompt: str = None,
max_new_tokens: int = 200,
temperature: float = 1.0
) -> str:
"""이미지와 (선택적) 프롬프트에 대한 응답을 생성합니다."""
if prompt:
inputs = self.processor(
images=image,
text=prompt,
return_tensors="pt"
).to("cuda", torch.float16)
else:
inputs = self.processor(
images=image,
return_tensors="pt"
).to("cuda", torch.float16)
with torch.no_grad():
generated_ids = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
do_sample=temperature > 0
)
generated_text = self.processor.batch_decode(
generated_ids,
skip_special_tokens=True
)[0].strip()
return generated_text
def batch_caption(
self,
images: list[Image.Image],
batch_size: int = 8
) -> list[str]:
"""이미지 배치에 대한 캡션을 일괄 생성합니다."""
all_captions = []
for i in range(0, len(images), batch_size):
batch = images[i:i + batch_size]
inputs = self.processor(
images=batch,
return_tensors="pt",
padding=True
).to("cuda", torch.float16)
with torch.no_grad():
generated_ids = self.model.generate(
**inputs,
max_new_tokens=50
)
captions = self.processor.batch_decode(
generated_ids,
skip_special_tokens=True
)
all_captions.extend([c.strip() for c in captions])
return all_captions
사용 예시
assistant = BLIP2Assistant("Salesforce/blip2-flan-t5-xxl")
image = Image.open("document.png")
자유 형식 질문
response = assistant.generate_response(
image,
"Question: What is the main topic of this document? Answer:"
)
print(response)
대화형 세션
conversation_history = []
questions = [
"이미지에 무엇이 보이나요?",
"색상은 어떻게 되나요?",
"배경은 어떤가요?"
]
for q in questions:
history_text = "\n".join(conversation_history)
prompt = f"{history_text}\nQuestion: {q} Answer:"
answer = assistant.generate_response(image, prompt)
print(f"Q: {q}")
print(f"A: {answer}")
conversation_history.append(f"Q: {q} A: {answer}")
4. LLaVA: 대규모 언어-비전 어시스턴트
LLaVA 아키텍처
LLaVA(Large Language and Vision Assistant)는 2023년 발표된 오픈소스 시각-언어 모델로, 강력한 LLM (LLaMA, Vicuna)과 CLIP 비전 인코더를 연결하여 인스트럭션 팔로잉 능력을 갖춘 멀티모달 챗봇을 구현합니다.
**아키텍처 구성:**
이미지 → [CLIP ViT-L/14] → 이미지 특징 (1024차원)
↓
[선형 프로젝션 레이어]
↓
[비주얼 토큰들]
↓
[LLM (LLaMA/Vicuna)] ← [텍스트 토큰들]
↓
최종 응답
**LLaVA-1.5 개선사항:**
- MLP 프로젝션 레이어 (선형 → 2-레이어 MLP)
- 고해상도 이미지 지원
- 더 많은 학습 데이터
**LLaVA-1.6 (LLaVA-NeXT) 개선사항:**
- Dynamic High Resolution: 최대 672x672 → 4배 더 많은 시각 토큰
- 개선된 추론 및 OCR 능력
- 다양한 종횡비 지원
HuggingFace로 LLaVA 사용
from transformers import LlavaNextProcessor, LlavaNextForConditionalGeneration
from PIL import Image
class LLaVAAssistant:
"""LLaVA-1.6 기반 시각 어시스턴트."""
def __init__(
self,
model_name: str = "llava-hf/llava-v1.6-mistral-7b-hf"
):
self.processor = LlavaNextProcessor.from_pretrained(model_name)
self.model = LlavaNextForConditionalGeneration.from_pretrained(
model_name,
torch_dtype=torch.float16,
low_cpu_mem_usage=True,
device_map="auto"
)
def chat(
self,
image: Image.Image,
message: str,
max_new_tokens: int = 500,
temperature: float = 0.7
) -> str:
"""이미지와 함께 대화합니다."""
LLaVA-1.6의 대화 형식
conversation = [
{
"role": "user",
"content": [
{"type": "image"},
{"type": "text", "text": message}
]
}
]
prompt = self.processor.apply_chat_template(
conversation,
add_generation_prompt=True
)
inputs = self.processor(
prompt,
image,
return_tensors="pt"
).to("cuda")
with torch.no_grad():
output = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
do_sample=temperature > 0,
pad_token_id=self.processor.tokenizer.eos_token_id
)
입력 제외하고 생성된 텍스트만 추출
generated = output[0][inputs["input_ids"].shape[1]:]
return self.processor.decode(generated, skip_special_tokens=True)
def analyze_chart(self, chart_image: Image.Image) -> dict:
"""차트 이미지를 분석합니다."""
analysis_prompts = [
"이 차트의 제목은 무엇인가요?",
"x축과 y축은 무엇을 나타내나요?",
"가장 높은 값과 낮은 값은 무엇인가요?",
"전체적인 트렌드를 설명해주세요.",
"이 데이터에서 가장 중요한 인사이트는 무엇인가요?"
]
results = {}
for prompt in analysis_prompts:
response = self.chat(chart_image, prompt)
results[prompt] = response
return results
def extract_text_from_image(self, image: Image.Image) -> str:
"""이미지에서 텍스트를 추출합니다 (OCR)."""
return self.chat(
image,
"이 이미지에 있는 모든 텍스트를 정확히 추출해주세요. "
"텍스트만 반환하고 다른 설명은 추가하지 마세요."
)
실전 활용: 문서 분석 파이프라인
class DocumentAnalysisPipeline:
"""LLaVA를 사용한 문서 분석 파이프라인."""
def __init__(self):
self.llava = LLaVAAssistant()
def analyze_document(self, document_image: Image.Image) -> dict:
"""문서 이미지를 종합 분석합니다."""
1. 문서 타입 식별
doc_type = self.llava.chat(
document_image,
"이 문서의 유형은 무엇인가요? (청구서, 계약서, 보고서, 양식 등)"
)
2. 텍스트 추출
extracted_text = self.llava.extract_text_from_image(document_image)
3. 핵심 정보 추출
key_info = self.llava.chat(
document_image,
f"이 {doc_type}에서 다음 정보를 JSON 형식으로 추출해주세요: "
"날짜, 발신인, 수신인, 금액(있는 경우), 주요 내용 요약"
)
4. 액션 아이템 식별
action_items = self.llava.chat(
document_image,
"이 문서에서 필요한 조치사항이 있다면 목록으로 나열해주세요."
)
return {
"document_type": doc_type,
"extracted_text": extracted_text,
"key_information": key_info,
"action_items": action_items
}
5. InstructBLIP
InstructBLIP의 핵심
InstructBLIP은 BLIP-2를 기반으로 하되, 다양한 인스트럭션을 따를 수 있도록 인스트럭션 튜닝을 적용한 모델입니다. Q-Former가 인스트럭션을 인식하여 관련 시각 특징을 추출하는 것이 핵심입니다.
from transformers import InstructBlipProcessor, InstructBlipForConditionalGeneration
from PIL import Image
class InstructBLIPAssistant:
"""InstructBLIP 기반 지시 따르기 어시스턴트."""
def __init__(self, model_name: str = "Salesforce/instructblip-vicuna-7b"):
self.processor = InstructBlipProcessor.from_pretrained(model_name)
self.model = InstructBlipForConditionalGeneration.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
def instruct(
self,
image: Image.Image,
instruction: str,
max_new_tokens: int = 300
) -> str:
"""이미지에 대한 구체적인 지시를 수행합니다."""
inputs = self.processor(
images=image,
text=instruction,
return_tensors="pt"
).to("cuda", torch.float16)
with torch.no_grad():
outputs = self.model.generate(
**inputs,
do_sample=False,
num_beams=5,
max_new_tokens=max_new_tokens,
min_length=1,
top_p=0.9,
repetition_penalty=1.5,
length_penalty=1.0,
temperature=1.0
)
generated_text = self.processor.batch_decode(
outputs,
skip_special_tokens=True
)[0].strip()
return generated_text
다양한 활용 예시
assistant = InstructBLIPAssistant()
image = Image.open("complex_diagram.png")
복잡한 다이어그램 설명
description = assistant.instruct(
image,
"이 다이어그램을 상세히 설명해주세요. 각 컴포넌트의 역할과 연결 관계를 포함하세요."
)
특정 객체 감지
objects = assistant.instruct(
image,
"이미지에서 발견되는 모든 객체를 목록으로 나열하고, 각 객체의 위치를 설명해주세요."
)
감정 분석
emotion = assistant.instruct(
image,
"이미지에 있는 사람들의 감정 상태를 분석하고, 그 근거를 설명해주세요."
)
비교 분석
if len([image]) > 1: # 여러 이미지의 경우
comparison = assistant.instruct(
image,
"이미지의 특징을 자세히 설명하고, 비슷한 이미지와 비교했을 때의 차이점을 설명해주세요."
)
6. GPT-4 Vision
GPT-4V API 사용법
GPT-4 Vision은 OpenAI의 GPT-4 모델에 시각 능력을 추가한 것으로, 현재 가장 강력한 상업용 멀티모달 LLM 중 하나입니다.
from pathlib import Path
client = openai.OpenAI()
def encode_image_to_base64(image_path: str) -> str:
"""이미지 파일을 Base64로 인코딩합니다."""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def image_url_to_base64(url: str) -> str:
"""URL에서 이미지를 다운로드하여 Base64로 인코딩합니다."""
response = httpx.get(url)
return base64.b64encode(response.content).decode('utf-8')
class GPT4VisionAnalyzer:
"""GPT-4 Vision 기반 이미지 분석기."""
def __init__(self, model: str = "gpt-4o"):
self.client = openai.OpenAI()
self.model = model
def analyze_image(
self,
image_source: str, # 파일 경로 또는 URL
prompt: str,
is_url: bool = True,
detail: str = "high", # "low", "high", "auto"
max_tokens: int = 1000
) -> str:
"""단일 이미지를 분석합니다."""
if is_url:
image_content = {
"type": "image_url",
"image_url": {
"url": image_source,
"detail": detail
}
}
else:
로컬 파일
base64_image = encode_image_to_base64(image_source)
ext = Path(image_source).suffix.lower()
media_type_map = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp"
}
media_type = media_type_map.get(ext, "image/jpeg")
image_content = {
"type": "image_url",
"image_url": {
"url": f"data:{media_type};base64,{base64_image}",
"detail": detail
}
}
response = self.client.chat.completions.create(
model=self.model,
messages=[
{
"role": "user",
"content": [
image_content,
{"type": "text", "text": prompt}
]
}
],
max_tokens=max_tokens
)
return response.choices[0].message.content
def analyze_multiple_images(
self,
image_sources: list[dict], # [{"source": "...", "is_url": True}]
prompt: str,
max_tokens: int = 2000
) -> str:
"""여러 이미지를 동시에 분석합니다."""
content = []
for img_info in image_sources:
source = img_info["source"]
is_url = img_info.get("is_url", True)
if is_url:
content.append({
"type": "image_url",
"image_url": {"url": source, "detail": "high"}
})
else:
base64_image = encode_image_to_base64(source)
content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
})
content.append({"type": "text", "text": prompt})
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": content}],
max_tokens=max_tokens
)
return response.choices[0].message.content
def analyze_chart_or_graph(self, image_source: str) -> dict:
"""차트나 그래프를 구조화된 형식으로 분석합니다."""
prompt = """이 차트/그래프를 분석하여 다음 JSON 형식으로 반환해주세요:
{
"chart_type": "막대/선/원/산점도 등",
"title": "차트 제목",
"x_axis": {"label": "X축 레이블", "unit": "단위"},
"y_axis": {"label": "Y축 레이블", "unit": "단위"},
"data_series": [{"name": "시리즈명", "trend": "상승/하락/유지"}],
"key_findings": ["발견사항1", "발견사항2"],
"data_range": {"min": 0, "max": 0},
"anomalies": ["이상치 설명"]
}"""
response = self.analyze_image(
image_source,
prompt,
detail="high",
max_tokens=1500
)
try:
JSON 파싱 시도
start = response.find('{')
end = response.rfind('}') + 1
if start >= 0 and end > start:
return json.loads(response[start:end])
except json.JSONDecodeError:
pass
return {"raw_response": response}
def extract_structured_data_from_document(
self,
document_image_path: str
) -> dict:
"""문서 이미지에서 구조화된 데이터를 추출합니다."""
prompt = """이 문서에서 다음 정보를 JSON 형식으로 추출해주세요:
1. 문서 유형
2. 날짜 (있는 경우)
3. 발신인/작성자
4. 수신인 (있는 경우)
5. 주요 내용 요약 (3-5문장)
6. 주요 수치 데이터 (표, 금액 등)
7. 서명/승인 여부
JSON 형식:
{
"document_type": "",
"date": "",
"author": "",
"recipient": "",
"summary": "",
"numerical_data": [],
"signature_present": false
}"""
return self.analyze_chart_or_graph.__func__(self, document_image_path)
실전 활용: 이커머스 제품 분석
def analyze_product_images(image_urls: list[str]) -> dict:
"""여러 제품 이미지를 분석합니다."""
analyzer = GPT4VisionAnalyzer()
image_sources = [{"source": url, "is_url": True} for url in image_urls]
result = analyzer.analyze_multiple_images(
image_sources,
prompt="""이 제품 이미지들을 분석하여 다음을 JSON으로 반환해주세요:
{
"product_name": "추정 제품명",
"category": "제품 카테고리",
"color_options": ["색상 목록"],
"key_features": ["주요 특징"],
"condition": "새 제품/중고 등",
"quality_score": 0-10,
"marketing_description": "마케팅용 설명 (100자)",
"seo_keywords": ["SEO 키워드"]
}"""
)
return result
7. Gemini Vision
Gemini의 멀티모달 능력
Google의 Gemini는 처음부터 멀티모달을 고려하여 설계된 파운데이션 모델입니다. 특히 Gemini 1.5 Pro는 100만 토큰 컨텍스트 윈도우로 장시간 비디오, 긴 문서, 다수의 이미지를 처리할 수 있습니다.
from pathlib import Path
API 키 설정
genai.configure(api_key="YOUR_GEMINI_API_KEY")
class GeminiVisionAnalyzer:
"""Gemini Vision 기반 분석기."""
def __init__(self, model_name: str = "gemini-1.5-pro"):
self.model = genai.GenerativeModel(model_name)
self.vision_model = genai.GenerativeModel("gemini-1.5-flash")
def analyze_image(
self,
image_path: str,
prompt: str
) -> str:
"""이미지를 분석합니다."""
image = PIL.Image.open(image_path)
response = self.model.generate_content([prompt, image])
return response.text
def analyze_with_url(self, image_url: str, prompt: str) -> str:
"""URL의 이미지를 분석합니다."""
image_data = httpx.get(image_url).content
image_part = {
"mime_type": "image/jpeg",
"data": base64.b64encode(image_data).decode('utf-8')
}
response = self.model.generate_content([
{"text": prompt},
image_part
])
return response.text
def analyze_video(
self,
video_path: str,
questions: list[str]
) -> dict:
"""비디오를 분석합니다. (Gemini 1.5 Pro의 강점)"""
비디오 파일 업로드
print(f"비디오 업로드 중: {video_path}")
video_file = genai.upload_file(
path=video_path,
display_name="analysis_video"
)
업로드 완료 대기
while video_file.state.name == "PROCESSING":
print("처리 중...")
time.sleep(10)
video_file = genai.get_file(video_file.name)
if video_file.state.name == "FAILED":
raise ValueError("비디오 처리 실패")
print(f"비디오 업로드 완료: {video_file.uri}")
질문별 분석
results = {}
for question in questions:
response = self.model.generate_content(
[video_file, question],
request_options={"timeout": 600}
)
results[question] = response.text
파일 삭제 (선택적)
genai.delete_file(video_file.name)
return results
def analyze_multiple_images_interleaved(
self,
image_text_pairs: list[dict] # [{"image": PIL.Image, "text": str}]
) -> str:
"""이미지와 텍스트가 교차 배치된 복합 쿼리를 처리합니다."""
content = []
for pair in image_text_pairs:
if "text" in pair:
content.append(pair["text"])
if "image" in pair:
content.append(pair["image"])
response = self.model.generate_content(content)
return response.text
def process_document_batch(
self,
document_images: list[PIL.Image.Image],
extraction_schema: str
) -> list[dict]:
"""여러 문서를 일괄 처리합니다 (Gemini의 긴 컨텍스트 활용)."""
모든 이미지를 하나의 요청으로 처리
content = [f"다음 {len(document_images)}개의 문서를 분석해주세요:\n"]
for i, img in enumerate(document_images, 1):
content.append(f"\n--- 문서 {i} ---")
content.append(img)
content.append(f"\n각 문서에 대해 다음 JSON 스키마로 데이터를 추출하세요:\n{extraction_schema}")
response = self.model.generate_content(content)
JSON 파싱
try:
text = response.text
JSON 배열 추출
start = text.find('[')
end = text.rfind(']') + 1
if start >= 0 and end > start:
return json.loads(text[start:end])
except json.JSONDecodeError:
return [{"raw_response": response.text}]
활용 예시: 비디오 분석
analyzer = GeminiVisionAnalyzer()
video_questions = [
"비디오의 전체 내용을 요약해주세요.",
"주요 장면들을 타임스탬프와 함께 나열해주세요.",
"비디오에서 언급된 주요 키워드나 개념은 무엇인가요?",
"비디오의 주제와 목적은 무엇인가요?"
]
results = analyzer.analyze_video("lecture_video.mp4", video_questions)
for question, answer in results.items():
print(f"\n질문: {question}")
print(f"답변: {answer}")
8. Claude Vision
Claude Vision API
Anthropic의 Claude 3.5 Sonnet은 강력한 시각 능력을 제공하며, 특히 문서 이해, 코드 스크린샷 분석, 세밀한 이미지 해석에서 뛰어난 성능을 보입니다.
from pathlib import Path
client = anthropic.Anthropic()
class ClaudeVisionAnalyzer:
"""Claude Vision 기반 이미지 분석기."""
def __init__(self, model: str = "claude-3-5-sonnet-20241022"):
self.client = anthropic.Anthropic()
self.model = model
def _prepare_image_content(
self,
image_source: str,
is_url: bool = True
) -> dict:
"""이미지 콘텐츠를 Claude API 형식으로 준비합니다."""
if is_url:
return {
"type": "image",
"source": {
"type": "url",
"url": image_source
}
}
else:
로컬 파일을 Base64로 인코딩
with open(image_source, "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
ext = Path(image_source).suffix.lower()
media_type_map = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp"
}
media_type = media_type_map.get(ext, "image/jpeg")
return {
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": image_data
}
}
def analyze(
self,
image_source: str,
prompt: str,
is_url: bool = True,
system_prompt: str = None,
max_tokens: int = 1000
) -> str:
"""이미지를 분석합니다."""
image_content = self._prepare_image_content(image_source, is_url)
messages = [
{
"role": "user",
"content": [
image_content,
{"type": "text", "text": prompt}
]
}
]
kwargs = {
"model": self.model,
"max_tokens": max_tokens,
"messages": messages
}
if system_prompt:
kwargs["system"] = system_prompt
response = self.client.messages.create(**kwargs)
return response.content[0].text
def analyze_code_screenshot(
self,
screenshot_path: str
) -> dict:
"""코드 스크린샷을 분석하고 코드를 추출합니다."""
system_prompt = """당신은 코드 분석 전문가입니다.
스크린샷에서 코드를 정확히 추출하고 분석하세요."""
extraction_prompt = """이 코드 스크린샷에서:
1. 코드를 정확히 추출하세요 (들여쓰기 포함)
2. 프로그래밍 언어를 식별하세요
3. 코드의 주요 기능을 설명하세요
4. 잠재적인 버그나 개선사항을 제안하세요
다음 JSON 형식으로 응답하세요:
{
"language": "프로그래밍 언어",
"code": "추출된 코드",
"description": "코드 설명",
"potential_issues": ["이슈1", "이슈2"],
"improvements": ["개선사항1", "개선사항2"]
}"""
response = self.analyze(
screenshot_path,
extraction_prompt,
is_url=False,
system_prompt=system_prompt,
max_tokens=2000
)
try:
start = response.find('{')
end = response.rfind('}') + 1
return json.loads(response[start:end])
except json.JSONDecodeError:
return {"raw_response": response}
def compare_images(
self,
image_sources: list[tuple[str, bool]], # (source, is_url) 쌍
comparison_prompt: str
) -> str:
"""여러 이미지를 비교 분석합니다."""
content = []
for source, is_url in image_sources:
content.append(self._prepare_image_content(source, is_url))
content.append({"type": "text", "text": comparison_prompt})
response = self.client.messages.create(
model=self.model,
max_tokens=2000,
messages=[{"role": "user", "content": content}]
)
return response.content[0].text
def analyze_ui_design(self, ui_screenshot_path: str) -> dict:
"""UI 디자인 스크린샷을 분석합니다."""
prompt = """이 UI 스크린샷을 UX/UI 전문가 관점에서 분석해주세요:
분석 항목:
1. 레이아웃 구조
2. 색상 팔레트
3. 타이포그래피
4. 사용성 (Usability) 평가
5. 접근성 (Accessibility) 이슈
6. 개선 제안사항
JSON 형식으로 반환해주세요:
{
"layout": "레이아웃 설명",
"color_palette": ["주요 색상들"],
"typography": "타이포그래피 평가",
"usability_score": 0-10,
"usability_issues": ["이슈들"],
"accessibility_issues": ["접근성 문제"],
"improvements": ["개선 제안"]
}"""
return self.analyze(
ui_screenshot_path,
prompt,
is_url=False,
max_tokens=1500
)
사용 예시
analyzer = ClaudeVisionAnalyzer()
이미지 분석
result = analyzer.analyze(
"https://example.com/product.jpg",
"이 제품의 특징을 자세히 설명하고, 잠재적 고객층을 추천해주세요.",
is_url=True
)
print(result)
9. 멀티모달 RAG
멀티모달 RAG 개요
멀티모달 RAG는 텍스트뿐만 아니라 이미지, 표, 차트 등 다양한 형태의 콘텐츠를 인덱싱하고 검색하는 시스템입니다.
이미지 인덱싱 전략
from PIL import Image
from transformers import CLIPModel, CLIPProcessor
from chromadb.utils.embedding_functions import OpenCLIPEmbeddingFunction
class MultimodalRAGSystem:
"""멀티모달 RAG 시스템."""
def __init__(self):
CLIP 모델 초기화
self.clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
self.clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
ChromaDB 초기화
self.chroma_client = chromadb.Client()
self.image_collection = self.chroma_client.get_or_create_collection(
name="images",
metadata={"hnsw:space": "cosine"}
)
self.text_collection = self.chroma_client.get_or_create_collection(
name="texts"
)
def get_image_embedding(self, image: Image.Image) -> np.ndarray:
"""이미지를 CLIP 임베딩으로 변환합니다."""
inputs = self.clip_processor(
images=image,
return_tensors="pt"
)
with torch.no_grad():
features = self.clip_model.get_image_features(**inputs)
features = torch.nn.functional.normalize(features, p=2, dim=-1)
return features.numpy()[0]
def get_text_embedding(self, text: str) -> np.ndarray:
"""텍스트를 CLIP 임베딩으로 변환합니다."""
inputs = self.clip_processor(
text=[text],
return_tensors="pt",
padding=True,
truncation=True
)
with torch.no_grad():
features = self.clip_model.get_text_features(**inputs)
features = torch.nn.functional.normalize(features, p=2, dim=-1)
return features.numpy()[0]
def index_image(
self,
image: Image.Image,
image_id: str,
metadata: dict = None
):
"""이미지를 인덱싱합니다."""
embedding = self.get_image_embedding(image)
이미지를 Base64로 저장
buffer = io.BytesIO()
image.save(buffer, format="PNG")
image_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
doc_metadata = {"image_b64": image_b64}
if metadata:
doc_metadata.update(metadata)
self.image_collection.add(
embeddings=[embedding.tolist()],
ids=[image_id],
metadatas=[doc_metadata]
)
def search_images_by_text(
self,
query: str,
n_results: int = 5
) -> list[dict]:
"""텍스트로 이미지를 검색합니다."""
query_embedding = self.get_text_embedding(query)
results = self.image_collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=n_results,
include=["metadatas", "distances", "ids"]
)
retrieved = []
for i in range(len(results['ids'][0])):
metadata = results['metadatas'][0][i]
image_b64 = metadata.pop('image_b64', None)
image = None
if image_b64:
image_bytes = base64.b64decode(image_b64)
image = Image.open(io.BytesIO(image_bytes))
retrieved.append({
"id": results['ids'][0][i],
"distance": results['distances'][0][i],
"metadata": metadata,
"image": image
})
return retrieved
def multimodal_rag_query(
self,
question: str,
vision_model_fn, # GPT-4V, Claude Vision 등
n_image_results: int = 3
) -> str:
"""멀티모달 RAG 쿼리를 수행합니다."""
관련 이미지 검색
relevant_images = self.search_images_by_text(question, n_image_results)
if not relevant_images:
return vision_model_fn(question=question, images=[])
검색된 이미지로 응답 생성
retrieved_images = [r["image"] for r in relevant_images if r["image"]]
metadata_info = [
f"이미지 {i+1}: {r['metadata']}"
for i, r in enumerate(relevant_images)
]
enhanced_prompt = f"""
질문: {question}
관련 이미지 정보:
{chr(10).join(metadata_info)}
위의 이미지들을 참고하여 질문에 답변해주세요.
각 이미지의 관련 내용을 구체적으로 인용하세요.
"""
return vision_model_fn(question=enhanced_prompt, images=retrieved_images)
ColPali: PDF 페이지 검색
ColPali: 비전 언어 모델로 PDF 페이지 직접 검색
pip install colpali-engine
from colpali_engine.models import ColPali, ColPaliProcessor
class ColPaliPDFSearch:
"""ColPali를 사용한 PDF 페이지 검색."""
def __init__(self, model_name: str = "vidore/colpali-v1.2"):
self.model = ColPali.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="cuda"
)
self.processor = ColPaliProcessor.from_pretrained(model_name)
def index_pdf_pages(
self,
page_images: list[Image.Image]
) -> torch.Tensor:
"""PDF 페이지 이미지들을 인덱싱합니다."""
all_embeddings = []
batch_size = 4
for i in range(0, len(page_images), batch_size):
batch = page_images[i:i + batch_size]
inputs = self.processor.process_images(batch)
inputs = {k: v.to("cuda") for k, v in inputs.items()}
with torch.no_grad():
embeddings = self.model(**inputs)
all_embeddings.append(embeddings)
return torch.cat(all_embeddings, dim=0)
def search(
self,
query: str,
page_embeddings: torch.Tensor,
top_k: int = 3
) -> list[int]:
"""쿼리로 관련 PDF 페이지를 검색합니다."""
쿼리 임베딩
query_inputs = self.processor.process_queries([query])
query_inputs = {k: v.to("cuda") for k, v in query_inputs.items()}
with torch.no_grad():
query_embedding = self.model(**query_inputs)
MaxSim 스코어 계산 (ColPali의 핵심)
scores = self.processor.score_multi_vector(
query_embedding,
page_embeddings
)
Top-K 페이지 인덱스 반환
top_indices = scores[0].argsort(descending=True)[:top_k]
return top_indices.tolist()
10. 오픈소스 멀티모달 모델
Phi-3 Vision (Microsoft)
from transformers import AutoModelForCausalLM, AutoProcessor
from PIL import Image
class Phi3VisionModel:
"""Microsoft Phi-3 Vision 모델."""
def __init__(self):
model_id = "microsoft/Phi-3-vision-128k-instruct"
self.model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="cuda",
trust_remote_code=True,
torch_dtype=torch.bfloat16,
_attn_implementation='flash_attention_2' # CUDA 필요
)
self.processor = AutoProcessor.from_pretrained(
model_id,
trust_remote_code=True
)
def analyze(self, image: Image.Image, prompt: str) -> str:
"""이미지를 분석합니다."""
messages = [
{"role": "user", "content": f"<|image_1|>\n{prompt}"}
]
prompt_text = self.processor.tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
inputs = self.processor(
prompt_text,
[image],
return_tensors="pt"
).to("cuda")
with torch.no_grad():
output = self.model.generate(
**inputs,
max_new_tokens=500,
eos_token_id=self.processor.tokenizer.eos_token_id
)
generated = output[0][inputs['input_ids'].shape[1]:]
return self.processor.decode(generated, skip_special_tokens=True)
Qwen-VL (Alibaba)
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
class QwenVLModel:
"""Qwen2-VL 멀티모달 모델."""
def __init__(self, model_name: str = "Qwen/Qwen2-VL-7B-Instruct"):
self.model = Qwen2VLForConditionalGeneration.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
device_map="auto"
)
self.processor = AutoProcessor.from_pretrained(
model_name,
min_pixels=256*28*28,
max_pixels=1280*28*28
)
def analyze_image(
self,
image_path: str,
question: str
) -> str:
"""이미지를 분석합니다."""
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"image": image_path
},
{
"type": "text",
"text": question
}
]
}
]
text = self.processor.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
image_inputs, video_inputs = process_vision_info(messages)
inputs = self.processor(
text=[text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt"
).to("cuda")
with torch.no_grad():
output_ids = self.model.generate(**inputs, max_new_tokens=512)
generated_ids = [
output_ids[len(input_ids):]
for input_ids, output_ids in zip(inputs.input_ids, output_ids)
]
return self.processor.batch_decode(
generated_ids,
skip_special_tokens=True,
clean_up_tokenization_spaces=False
)[0]
로컬 실행 가이드 (Ollama)
Ollama로 멀티모달 모델 로컬 실행
ollama.ai에서 Ollama 설치
LLaVA 모델 다운로드 및 실행
ollama pull llava:13b
이미지와 함께 모델 실행
ollama run llava:13b
from pathlib import Path
class OllamaVisionModel:
"""Ollama를 사용한 로컬 비전 모델."""
def __init__(self, model: str = "llava:13b"):
self.model = model
def analyze(
self,
image_path: str,
prompt: str
) -> str:
"""로컬 모델로 이미지를 분석합니다."""
response = ollama.chat(
model=self.model,
messages=[
{
"role": "user",
"content": prompt,
"images": [image_path]
}
]
)
return response["message"]["content"]
def batch_analyze(
self,
image_paths: list[str],
prompt: str
) -> list[str]:
"""여러 이미지를 순차 분석합니다."""
results = []
for path in image_paths:
result = self.analyze(path, prompt)
results.append(result)
return results
사용 예시
model = OllamaVisionModel("llava:13b")
result = model.analyze(
"/path/to/image.jpg",
"이 이미지에 무엇이 있나요? 자세히 설명해주세요."
)
print(result)
11. 비디오 이해 AI
비디오 이해의 과제
비디오 이해는 시간적 정보를 포함하는 멀티모달 태스크로, 정적 이미지 이해보다 훨씬 복잡합니다.
**주요 과제:**
- 시간적 의존성: 프레임 간의 시간적 관계 이해
- 대용량 데이터: 1분 비디오 = 약 1800 프레임 (30fps)
- 동작 인식: 움직임 패턴 파악
- 다중 스케일: 짧은 동작과 긴 이벤트 동시 이해
VideoMAE를 활용한 비디오 특징 추출
from transformers import VideoMAEImageProcessor, VideoMAEModel
class VideoFeatureExtractor:
"""VideoMAE를 사용한 비디오 특징 추출기."""
def __init__(self, model_name: str = "MCG-NJU/videomae-base"):
self.processor = VideoMAEImageProcessor.from_pretrained(model_name)
self.model = VideoMAEModel.from_pretrained(model_name)
def extract_video_features(
self,
video_frames: list, # PIL 이미지 또는 numpy 배열 목록
num_frames: int = 16 # VideoMAE는 보통 16프레임 사용
) -> torch.Tensor:
"""비디오 프레임에서 특징을 추출합니다."""
균일하게 프레임 샘플링
total_frames = len(video_frames)
indices = np.linspace(0, total_frames - 1, num_frames, dtype=int)
sampled_frames = [video_frames[i] for i in indices]
전처리
inputs = self.processor(sampled_frames, return_tensors="pt")
with torch.no_grad():
outputs = self.model(**inputs)
[batch, num_patches, hidden_size] 형태
return outputs.last_hidden_state
OpenCV로 비디오 프레임 추출
def extract_frames_from_video(
video_path: str,
target_fps: int = 1
) -> list:
"""비디오에서 프레임을 추출합니다."""
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = int(fps / target_fps)
frames = []
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_interval == 0:
BGR을 RGB로 변환
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
from PIL import Image
pil_frame = Image.fromarray(frame_rgb)
frames.append(pil_frame)
frame_count += 1
cap.release()
return frames
Gemini로 긴 비디오 이해
class LongVideoUnderstanding:
"""Gemini 1.5 Pro를 사용한 긴 비디오 이해 시스템."""
def __init__(self):
self.model = genai.GenerativeModel("gemini-1.5-pro")
def analyze_long_video(
self,
video_path: str,
analysis_tasks: list[str]
) -> dict:
"""최대 1시간 길이의 비디오를 분석합니다."""
print("비디오 업로드 중...")
video_file = genai.upload_file(
path=video_path,
display_name="long_video_analysis"
)
처리 완료 대기
while video_file.state.name == "PROCESSING":
print(f"처리 중... (상태: {video_file.state.name})")
time.sleep(15)
video_file = genai.get_file(video_file.name)
if video_file.state.name != "ACTIVE":
raise RuntimeError(f"비디오 처리 실패: {video_file.state.name}")
print(f"업로드 완료 (URI: {video_file.uri})")
results = {}
for task in analysis_tasks:
print(f"분석 중: {task}")
response = self.model.generate_content(
[video_file, task],
request_options={"timeout": 900}
)
results[task] = response.text
업로드된 파일 정리
genai.delete_file(video_file.name)
print("파일 정리 완료")
return results
def create_video_summary(self, video_path: str) -> dict:
"""비디오의 종합 요약을 생성합니다."""
tasks = [
"비디오의 전체 내용을 3-5문장으로 요약해주세요.",
"주요 장면을 타임스탬프와 함께 나열해주세요. 형식: MM:SS - 설명",
"비디오에 등장하는 주요 인물, 사물, 장소를 목록으로 나열해주세요.",
"비디오에서 강조된 핵심 메시지나 결론은 무엇인가요?",
"이 비디오의 대상 시청자와 목적은 무엇인가요?"
]
return self.analyze_long_video(video_path, tasks)
비디오 이해 시스템 활용
video_analyzer = LongVideoUnderstanding()
summary = video_analyzer.create_video_summary("lecture.mp4")
for task, result in summary.items():
print(f"\n{'='*50}")
print(f"질문: {task}")
print(f"답변: {result}")
마치며
멀티모달 AI는 빠르게 발전하고 있으며, 텍스트, 이미지, 비디오를 통합적으로 이해하는 능력이 점점 더 강력해지고 있습니다.
이 가이드에서 다룬 핵심 내용:
- **CLIP**: 대조 학습으로 이미지-텍스트를 동일 공간에 매핑, 제로샷 분류의 기반
- **BLIP/BLIP-2**: 부트스트래핑과 Q-Former로 효율적인 멀티모달 학습
- **LLaVA**: 오픈소스 비전-언어 어시스턴트의 표준
- **GPT-4V / Claude Vision**: 상업용 최고 성능 멀티모달 LLM
- **Gemini 1.5**: 100만 토큰 컨텍스트로 긴 비디오와 문서 처리
- **멀티모달 RAG**: CLIP 임베딩으로 이미지를 검색 가능한 지식베이스로 구축
- **오픈소스 생태계**: Phi-3 Vision, Qwen-VL 등 로컬 실행 가능한 강력한 모델들
앞으로의 방향은 더 긴 비디오 이해, 3D 공간 이해, 실시간 멀티모달 처리로 나아가고 있습니다. 이 분야는 매우 빠르게 발전하므로 지속적인 학습이 필요합니다.
참고 자료
- Radford, A. et al. (2021). Learning Transferable Visual Models From Natural Language Supervision (CLIP). [arXiv:2103.00020](https://arxiv.org/abs/2103.00020)
- Li, J. et al. (2023). BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models. [arXiv:2301.12597](https://arxiv.org/abs/2301.12597)
- Liu, H. et al. (2023). Visual Instruction Tuning (LLaVA). [arXiv:2304.08485](https://arxiv.org/abs/2304.08485)
- OpenAI GPT-4 Technical Report: [openai.com/research/gpt-4](https://openai.com/research/gpt-4)
- Google Gemini API: [ai.google.dev/gemini-api](https://ai.google.dev/gemini-api)
- Anthropic Claude API: [docs.anthropic.com](https://docs.anthropic.com)
- HuggingFace LLaVA: [huggingface.co/llava-hf](https://huggingface.co/llava-hf)
- ColPali: Efficient Document Retrieval with Vision Language Models. [arxiv.org/abs/2407.01449](https://arxiv.org/abs/2407.01449)
현재 단락 (1/1426)
1. [멀티모달 AI 개요](#1-멀티모달-ai-개요)