LLM 엔지니어링12장 · 멀티모달 RAG 와 데이터셋 자동 생성17 / 22← 이전목차다음 →MetaContents

12장. 멀티모달 RAG 와 데이터셋 자동 생성

실습 4 — 그림과 표가 섞인 안내서로 답하기

상품 안내서(product_guide.md)를 상품마다 조각으로 만들고, "빨간 머그컵의 가격은?"처럼 색으로 묻는 질문 12개(product_guide_qa.jsonl, 지어낸 것)에 답합니다. 표만 넣은 조각, 실습 3의 그림 설명을 붙인 조각, CLIP 그림 검색의 세 가지로 정답 상품을 찾는지 견주고, 그림 설명을 붙인 조각으로 LLM 이 답하게 합니다. 실습 3을 먼저 실행해야 합니다.

1단계. 안내서를 상품마다 조각으로

Python
import json, re, urllib.request
import numpy as np
import pandas as pd

guide = open("product_guide.md", encoding="utf-8").read()
captions = pd.read_csv("out_captions.csv").set_index("코드")["설명"]       # 실습 3의 그림 설명

chunks = []
for section in guide.split("\n## ")[1:]:                 # 절 = 상품 종류 하나
    kind, body = section.split("\n", 1)
    note = body.strip().split("\n")[0]                    # 절의 첫 문단(관리 안내)
    header = [c.strip() for c in re.search(r"^\| 이름 .*$", body, re.M).group(0).strip("|").split("|")]
    for row in re.findall(r"^\| (?!이름|---).*$", body, re.M):
        rec = dict(zip(header, [c.strip() for c in row.strip("|").split("|")]))
        table_text = f"[{kind}] {rec['이름']}({rec['코드']}): " + ", ".join(f"{h} {rec[h]}" for h in header[2:]) + f". {note}"
        chunks.append({"코드": rec["코드"], "표만": table_text, "그림 설명 붙임": table_text + f" 그림: {captions[rec['코드']]}"})
print(len(chunks), "조각")
print(chunks[0]["그림 설명 붙임"])
출력
24 조각
[머그컵] 데일리 머그 1호(P01): 가격 9,900원, 용량 350ml, 무게 190g. 전자레인지에 써도 됩니다. 오래 쓰려면 식기세척기보다 손으로 씻는 편이 좋습니다. 그림: 그림 속 물건은 컵으로, 노란색입니다.

2단계. 세 가지로 정답 상품 찾기

Python
from PIL import Image
from sentence_transformers import SentenceTransformer

qa = [json.loads(line) for line in open("product_guide_qa.jsonl", encoding="utf-8")]
codes = [c["코드"] for c in chunks]
emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")      # 7차시의 글 임베딩
Q = emb.encode(["query: " + x["질문"] for x in qa], normalize_embeddings=True)

hits = {}
for kind in ["표만", "그림 설명 붙임"]:
    E = emb.encode(["passage: " + c[kind] for c in chunks], normalize_embeddings=True)
    found = [codes[i] for i in (Q @ E.T).argmax(axis=1)]
    hits[kind] = [f == x["코드"] for f, x in zip(found, qa)]

# CLIP: 한국어 질문을 그림 임베딩과 견줘 가장 가까운 그림의 상품을 고른다(실습 1의 방법)
labels = pd.read_csv("product_labels.csv")
img_model = SentenceTransformer("clip-ViT-B-32", device="cpu")
ko_model = SentenceTransformer("sentence-transformers/clip-ViT-B-32-multilingual-v1", device="cpu")
I = img_model.encode([Image.open(f).convert("RGB") for f in labels["파일"]], normalize_embeddings=True)
T = ko_model.encode([x["질문"] for x in qa], normalize_embeddings=True)
hits["CLIP 그림 검색"] = [labels.loc[i, "코드"] == x["코드"] for i, x in zip((T @ I.T).argmax(axis=1), qa)]
print({k: f"{sum(v)}/{len(v)}" for k, v in hits.items()})
출력
{'표만': '5/12', '그림 설명 붙임': '12/12', 'CLIP 그림 검색': '7/12'}

3단계. 그림 설명을 붙인 조각으로 답하기

Python
from openai import OpenAI

# 실습 3의 비전 모델을 내려 GPU 를 비운다. 이어서 쓸 글 모델(qwen2.5:3b)이 GPU 에 올라가게
urllib.request.urlopen(urllib.request.Request("http://localhost:11434/api/generate", headers={"Content-Type": "application/json"},
                                              data=json.dumps({"model": "qwen2.5vl:3b", "keep_alive": 0}).encode())).read()
llm = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
SYSTEM = "당신은 솔빛생활(가상 쇼핑몰)의 상담원입니다. [자료]에 있는 내용만으로 한 문장으로 답하고, 끝에 상품 코드를 (P00) 꼴로 적으세요."
E = emb.encode(["passage: " + c["그림 설명 붙임"] for c in chunks], normalize_embeddings=True)

right = 0
for x, q in zip(qa, Q):
    top = np.argsort(-(E @ q))[:3]                        # 그림 설명을 붙인 조각 3개
    context = "\n".join(chunks[i]["그림 설명 붙임"] for i in top)
    reply = llm.chat.completions.create(model="qwen2.5:3b", temperature=0, seed=42, messages=[
        {"role": "system", "content": SYSTEM}, {"role": "user", "content": f"[자료]\n{context}\n\n[질문]\n{x['질문']}"}])
    a = reply.choices[0].message.content.strip()
    ok = x["열쇠"].replace(",", "") in a.replace(",", "")
    right += ok
    print("O" if ok else "X", x["질문"], "→", a)
print(f"맞힌 수: {right} / {len(qa)}")
출력
X 노란 머그컵의 가격은 얼마인가요? → P01
O 빨간 우산의 지름은 얼마인가요? → 빨간 우산의 지름은 112cm입니다.(P04)
O 노란 우산의 가격은 얼마인가요? → P06 (15,900원)
O 빨간 운동화의 사이즈는 얼마인가요? → 러너 운동화 1호의 사이즈는 230-280mm입니다 (P07).
O 노란 운동화의 가격은 얼마인가요? → 러너 운동화 3호(P09)의 가격은 59,000원입니다. (P09)
O 파란 티셔츠의 사이즈는 얼마인가요? → 파란 티셔츠의 사이즈는 S-XL입니다. (P15)
O 노란 모자의 가격은 얼마인가요? → P16 (13,900원)
O 빨간 모자의 둘레는 얼마인가요? → P18호 빨간 모자의 둘레는 60cm입니다(P18).
O 빨간 스탠드 조명의 가격은 얼마인가요? → P19 (31,000원)
O 파란 스탠드 조명의 높이는 얼마인가요? → 파란 스탠드조명의 높이는 38cm입니다.(P20)
X 노란 화분의 가격은 얼마인가요? → P22
O 파란 화분의 지름은 얼마인가요? → P24 화분의 지름은 15cm입니다(P24).
맞힌 수: 10 / 12

이 실습이 뜻하는 것

이 실습은 사진 없이 글로만 된 상품 목록에서 "빨간 머그컵의 가격"을 찾는 일과 같습니다. 안내서를 상품마다 카드 한 장으로 자르고 카드 끝에 해설사의 그림 설명을 붙입니다(1단계). 질문에 맞는 카드를 세 가지 방법(표만 적힌 카드, 그림 설명을 붙인 카드, 사진과 직접 견주기)으로 찾아보고(2단계), 그림 설명을 붙인 카드 세 장을 상담원에게 건네 답하게 합니다(3단계). 여기서 카드가 조각, 카드 찾기가 검색, 사진과 직접 견주기가 CLIP 그림 검색, 상담원이 LLM(Large Language Model, 거대 언어 모델) qwen2.5:3b 입니다.

단계출력뜻
1. 조각24 조각상품 24개가 조각 24개가 되었습니다. 첫 조각 끝에 "그림: 그림 속 물건은 컵으로, 노란색입니다."가 붙었습니다
2. 표만'표만': '5/12'표에는 색이 없어 같은 종류 셋 가운데 짐작한 셈입니다. 우연히 맞을 몫(12개의 3분의 1, 4개)과 비슷합니다
2. 그림 설명 붙임'그림 설명 붙임': '12/12'조각에 색이 들어가자 12개 모두 정답 상품을 1위로 찾았습니다
2. CLIP'CLIP 그림 검색': '7/12'한국어 질문으로 그림을 바로 찾으면 12개 가운데 7개입니다. 실습 1의 종류 약점이 다시 나왔습니다
3. 답맞힌 수: 10 / 12답은 10개가 맞았습니다. 틀린 둘은 검색은 맞았는데 값 대신 "P01", "P22"처럼 상품 코드만 적었습니다

한 줄로 줄이면, 그림을 글로 바꿔 표 옆에 붙여 두면 색으로 묻는 질문도 보통의 글 검색으로 찾을 수 있다는 실습입니다. 검색이 맞아도 작은 모델이 답의 꼴을 어길 수 있어 답은 따로 확인해야 합니다. 질문 12개와 지어낸 안내서 하나로 낸 결과라, 다른 문서에서도 세 방법의 순서가 같다고 볼 수는 없습니다. 실습 5부터는 문서로 미세조정 데이터를 만드는 쪽으로 넘어갑니다.

코드 설명

  • 1단계는 안내서를 ## 으로 절(종류)마다 자르고, 표의 머리글과 행을 정규식으로 읽어 상품마다 한 조각을 만듭니다. 그림 파일 이름과 표의 코드 열이 같아서, 실습 3의 설명을 코드로 찾아 붙일 수 있습니다.
  • 정답 상품을 1위로 찾은 수는 표만 넣은 조각이 5/12, 그림 설명을 붙인 조각이 12/12 였습니다. 표만 있는 조각에는 색이 없어서 같은 종류 셋 가운데 하나를 짐작한 셈입니다(우연히 맞을 몫은 3분의 1).
  • 한국어 질문으로 CLIP 그림을 바로 찾으면 7/12 였습니다. 실습 1처럼 색은 맞히고 종류를 틀린 것이 대부분입니다. 이 데이터에서는 그림을 한국어 설명 글로 바꿔 두는 길이 더 나았습니다.
  • 3단계에서 LLM 은 12개 가운데 10개를 맞혔습니다. 틀린 둘(노란 머그컵, 노란 화분)은 검색은 맞았는데 답으로 값 대신 상품 코드("P01")만 적었습니다. 지시의 "끝에 상품 코드를 적으세요"를 작은 모델이 답 자체로 받아들인 것으로, 검색이 맞아도 답의 꼴은 따로 확인해야 합니다.
← 이전 절12장 · 실습 3 — 비전 모델로 그림 설명 만들기다음 절 →12장 · 실습 5 — 안내서로 질문-답 데이터 만들기