12장. 멀티모달 RAG 와 데이터셋 자동 생성
그림을 글로 바꾸기
라디오에서 야구 중계를 들으면 경기장을 보지 않아도 무슨 일이 일어나는지 압니다. 중계하는 사람이 그림을 말로 바꿔 주기 때문입니다. 비전 언어 모델(VLM)은 그림을 보고 "노란색 모자입니다"처럼 글로 설명합니다. 설명 글을 만들어 두면 그 뒤는 지금까지의 글 RAG 를 그대로 쓸 수 있습니다.
왼쪽 길은 CLIP 으로 그림을 임베딩해 바로 찾습니다. 빠르지만 그림 임베딩을 다룰 검색이 따로 있어야 합니다. 오른쪽 길은 비전 모델로 그림마다 설명 글을 만들어 두고, 그 글을 다른 문서와 함께 글 임베딩으로 찾습니다. 설명을 만드는 데 시간이 들지만, 검색과 답하기는 지금까지의 RAG 를 그대로 씁니다. 여기서 중계하는 사람이 비전 모델, 중계 말이 캡션입니다.
두 길 견주기
| 견줄 것 | 그림 임베딩(CLIP) | 설명 글(비전 모델) |
|---|---|---|
| 미리 하는 일 | 그림마다 임베딩(빠르다) | 그림마다 설명 생성(느리다) |
| 질문할 때 | 글 → 그림 임베딩과 견준다 | 지금까지의 글 검색 그대로 |
| 잘하는 것 | 생김새, 색처럼 말로 하기 어려운 것 | 다른 글, 표와 섞어 찾고 LLM 에 보여 주기 |
| 조심할 것 | 한국어 질문에 약할 수 있다 | 설명이 틀리면 그대로 틀린 근거가 된다 |
그림을 보내는 꼴
Ollama 의 비전 모델에는 그림을 base64 글자로 바꿔 메시지의 images 에 넣어 보냅니다.
import base64
from PIL import Image
from pathlib import Path
with Image.open("products/P01.png") as im:
size = im.size
raw = Path("products/P01.png").read_bytes()
b64 = base64.b64encode(raw).decode() # 바이트를 글자로. JSON 에 넣을 수 있다
message = {"role": "user", "content": "이 그림 속 물건과 색을 한 문장으로 설명하세요.", "images": [b64]}
print(size, len(raw), "바이트 →", len(b64), "글자")코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
from PIL import Image | PIL(Python Imaging Library, 파이썬 그림 도구)의 Image 모듈을 불러온다. 지금은 Pillow 라는 이름으로 설치한다 |
with Image.open(...) as im: | 그림 파일을 열고, 블록이 끝나면 파일을 닫는다 |
im.size | 그림의 (가로, 세로) 크기다. 단위는 픽셀(pixel, 그림의 점)이다 |
Path(...).read_bytes() | 파일을 글이 아니라 바이트 그대로 읽는다 |
base64.b64encode(raw) | 바이트를 base64 글자 64가지로 바꾼다. 결과는 아직 bytes 다 |
.decode() | bytes 를 파이썬 글(str)로 바꾼다. 그래야 JSON 에 넣을 수 있다 |
"images": [b64] | Ollama 메시지에서 그림을 담는 칸이다. 그림 여러 장을 목록으로 넣을 수 있다 |
이 코드가 뜻하는 것
글자만 실을 수 있는 편지에 그림을 담으려면, 그림을 약속한 글자들로 바꿔 적어 보내야 합니다. 받는 쪽은 같은 약속으로 글자를 다시 그림으로 되돌립니다. 여기서 글자만 싣는 편지가 JSON 메시지이고, 약속한 글자로 바꿔 적는 일이 base64 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
with Image.open(...) as im: | 그림을 열어 크기만 읽는다 | 그림 크기를 잰다 |
raw = ...read_bytes() | 그림 파일을 바이트로 읽는다 | 그림 원본 |
b64 = base64.b64encode(raw).decode() | 바이트를 글자로 바꾼다 | 편지에 적을 수 있는 글자로 옮겨 적는다 |
message = {...} | 질문 글과 그림 글자를 한 메시지에 담는다 | 편지 봉투에 질문과 그림을 함께 넣는다 |
print(...) | 크기와 바이트 수, 글자 수를 견준다 | 옮겨 적으니 얼마나 길어졌나 |
실행하면 (256, 256) 1556 바이트 → 2076 글자 가 나옵니다. base64 로 바꾸면 길이가 원래의 4/3 배쯤으로 늘어납니다. 이 코드는 메시지를 만들기만 하고 Ollama 에 보내지는 않습니다.
작게 줄여서 눈으로 보기
세 글자 cat 의 바이트로 base64 가 어떻게 바뀌고 되돌아오는지 봅니다.
import base64
raw = b"cat"
b64 = base64.b64encode(raw)
print(b64, b64.decode())
print(base64.b64decode(b64.decode()))
for n in (3, 30, 300):
print(n, "바이트 →", len(base64.b64encode(b"x" * n)), "글자")b'Y2F0' Y2F0
b'cat'
3 바이트 → 4 글자
30 바이트 → 40 글자
300 바이트 → 400 글자| 결과 | 읽는 법 |
|---|---|
b'Y2F0' Y2F0 | 3 바이트가 4 글자가 됐다. 앞의 b'...' 는 bytes, .decode() 뒤는 보통 글이다 |
b'cat' | base64.b64decode 로 글자를 원래 바이트로 되돌렸다 |
3 → 4, 30 → 40, 300 → 400 | 3 바이트마다 4 글자다. 그래서 길이가 4/3 배가 된다 |
위 그림 파일의 1556 바이트는 3 으로 나누어떨어지지 않아, 끝을 채우는 글자가 붙어 2076 글자가 됐습니다.
주의할 점
- 이 장의 비전 모델
qwen2.5vl:3b는 3.2GB 로, 4GB GPU 에 다 올라가지 않아 일부를 CPU 에서 돌립니다. 이 PC 에서는 그림 한 장에 20초쯤 걸렸습니다. 그래서 설명은 미리 한 번 만들어 파일로 저장해 두고 씁니다. - 비전 모델의 설명도 틀릴 수 있고, 한자나 영어가 섞이기도 합니다. 설명을 사람이 표본으로 확인합니다.