Jasontreks Blog

DM 보내기

메세지는 텔레그램 챗봇에 의해 익명으로 전달됩니다. 답장을 받으려면 이메일을 입력하세요.
Send

VLM 이미지 캡셔닝

RAG는 기본적으로 이미지 데이터를 다루지 않는다. 하지만 RAG 구축을 위한 문서 데이터에서 이미지는 꽤 중요한 역할을 할 수 있다.

인공지능에서 텍스트와 이미지를 함께 다루는것을 '멀티모달' 이라고 부르는데, 일부 멀티모달 임베딩 모델은 이미지를 텍스트처럼 벡터로 만들어 한 공간에 매핑될 수 있게 한다. 그래서 텍스트로 검색하면 그 내용과 유사한 이미지를 찾을 수 있다.

하지만 이 방식은 이미지 분류에 가깝다. 예를 들어 PDF에 도표나 차트 이미지가 있다면, 멀티모달 임베딩 모델은 단순히 '도표 또는 차트'로 분류할 뿐이지 어떤 시리즈들이 있으며, 어느 곡선이 얼마나 가파르게 증가하는지와 같은 '그래프 분석'은 이루어지지 않는다.

그래서 이미지에 대한 해석을 RAG 지식에 포함시키는 방법으로는 VLM을 사용해 캡션을 만들어내야 한다. 이를 위해 비전 기능이 있는 LLM(VLM)을 사용해 이미지를 입력으로 주고 해석을 만들어내도록 프롬프팅하여 파이프라인을 구축하였다.

VLM 품질 테스트

본격적으로 파이프라인을 구축하기 전, Ollama에서 바로 설치할 수 있는 여러 무료 VLM 중 어느 모델이 가장 정확하게 이미지를 분석할 수 있는지 확인하는 과정을 거쳤다. 간단한 대답조차 못하는 llama 계열과 같은 모델들을 제외하고, 써볼만 하다고 생각되는 4개 모델로 추린 다음, 하나의 그래프 이미지로 VLM 분석 결과를 비교해보았다.

프롬프트로 요구한것은 다른 두 곡선의 증가 추이가 어떻게 다른지에 대한 분석 내용이였다. 초록색 선과 빨간색 선이 각각 다른 시리즈임을 인지하고, 초록색 선이 빨간색 선에 비해 상대적으로 급격히 증가한다는 분석 결과만 나와주기를 기대하였다.

gemma3, qwen-2.5와 3.5, ministral-3 총 4가지를 비교해본 결과 ministral-3이 압도적으로 우수한 분석 결과를 내놓았다. 다른 모델들과 비교가 어려울만큼 상세하고 정확하게 분석했는데, 여러번 테스트해도 거의 같은 답변을 내놓을만큼 품질 역시 매우 일관적이라 서비스하기에도 적절해보였다.

이후 알아본 바로는 ministral-3는 메타에서 분리된 스타트업 미스트랄AI(Mistral AI)가 개발한 오픈 소스 모델이라고 한다.

자세한 비교 내용을 기술한 파일은 아래 링크에서 다운로드할 수 있다.

VLMtest.xlsx

Docling 이미지 추출 및 컨텍스트 윈도잉

VLM이 이미지를 분석하고 나면 그 내용은 전부 벡터로 임베딩하여 RAG에 반영해야 한다. 하지만 이미지 분석 내용만 딱 잘라 임베딩한다면 문서에서 이미지 원래 위치에서의 문맥과 단절될 수 있다.

그렇게 되면 VLM 만든 이미지 캡션은 문서 내용와 별개인 이미지 분석 내용에 불과하게 되고, 그것이 문서와 어떤 연관이 있는지는 검색시 반영되지 않을 수 있다.

정석대로라면 VLM이 생성한 캡션 내용을 원래 이미지 위치에 되돌려놓아야 문맥이 적절하게 형성된다. 그러나 VLM 캡션 생성이 상당히 오래걸리는 작업이라 PDF 추출 로직과 비동기적으로 따로 처리해야 하기에 문서를 재구성하는것은 어려움이 있고, 되돌려놓는다 하더라도 문서 전체 내용을 다시 임베딩해야 하므로 비효율적일 수 있다.

그래서 비동기적인 파이프라인 내에서 본문의 문맥을 VLM 캡션에 반영할 방법을 고안했는데, VLM에 전달할 이미지를 추출할 때 앞, 뒤로 내용 몇 문장을 발췌한 다음 VLM 캡션이 완성되고 나면 그 내용의 양쪽에 본문 일부를 덧붙여 함께 임베딩하는 것이다. 제미나이는 이것을 '컨텍스트 윙도잉' 기법이라고 했다.

바로 이 부분이다.

캡션 생성 파이프라인

아래는 Docling을 구현한 클래스에서 이미지들을 추출해 저장하고 컨텍스트(앞, 뒤 본문 일부 내용)들을 반환하는 메소드이다.

    def save_images_and_context_window(self, WINDOW_SIZE=5) -> list[tuple[str, str, str]]:
        if not self.parsed_doc_items:
            logging.error("처리할 문서 데이터가 없음.")
            return []
        
        result = []

        for i, (item, level) in enumerate(self.parsed_doc_items):
            if (isinstance(item, PictureItem) or isinstance(item, TableItem)) and item.image:
                # 이미지 파일명 규칙: img + 아이템 번호
                img_filename = f"img{i}.png"
                save_path = f"{self.metadata['path']}/{img_filename}"

                # 메타데이터 수정
                metadata = self.metadata
                metadata['name'] = img_filename
                metadata['is_image'] = True

                
                # 컨텍스트 윈도잉: 본문 중 앞 내용 
                prev_context = []
                for j in range(1, WINDOW_SIZE + 1):
                    if i - j >= 0:
                        prev_item, _ = self.parsed_doc_items[i - j]
                        if hasattr(prev_item, "text") and prev_item.text.strip():
                            prev_context.insert(0, prev_item.text)

                # 컨텍스트 윈도잉: 본문 중 뒷 내용 
                next_context = []
                for j in range(1, WINDOW_SIZE + 1):
                    if i + j < len(self.parsed_doc_items):
                        next_item, _ = self.parsed_doc_items[i + j]
                        if hasattr(next_item, "text") and next_item.text.strip():
                            next_context.append(next_item.text)

                # 이미지 저장
                try:
                    item.image.pil_image.save(save_path)
                    result.append((save_path, '\n'.join(prev_context), '\n'.join(next_context)))
                except Exception as e:
                    logging.error(f"이미지 저장 실패: {e}")
                    continue
        
        return result

이미지 저장과 함께 컨텍스트들 역시 각 이미지에 대한 DB에 저장된다. VLM 캡션이 완성되면 DB에서 문맥을 다시 조회하여 캡션과 결합되고 임베딩되어 RAG를 업데이트한다.

프롬프팅

이미지의 형태는 다양하다. 그래프, 표, 구조도, 해부도, 플로우차트 등 논문을 뒷받침하는 다양한 형태의 시각자료를 VLM이 해석하고 일관된 형식의 분석 결과를 내놓아야 지속적인 RAG를 운영할 수 있다. 따라서 가장 일반화되고 명확한 프롬프트가 필요했다.

아래는 이미지 캡션 생성 시 VLM에 전달하는 프롬프트 전문이다.

You are an objective image analysis engine optimized for data extraction and technical documentation. 
Your goal is to convert visual information into structured text without using external knowledge or hallucinations. 
Analyze the image strictly based on visual evidence.

### INSTRUCTIONS:

1. **DETECT IMAGE TYPE**: Determine if the image is a (A) Chart/Graph, (B) Diagram/Flowchart, or (C) General Image.

2. **FOR CHARTS & GRAPHS**:
- **Coordinates**: Define the X-axis and Y-axis labels, units, and ranges.
- **Series**: Identify all distinct data series (legends, colors).
- **Data Sampling**: Create a Markdown table representing key data points. Estimate values as precisely as possible based on the visual grid.
- **Trend Analysis**: Analyze the overall trend (increasing, decreasing, fluctuating). Identify distinct slopes, sudden spikes/drops, and significant gaps between series.

3. **FOR DIAGRAMS (Flowcharts, Architectures, UML)**:
- **Components**: List all nodes, blocks, or entities containing text.
- **Flow**: Describe the relationships and direction of arrows.
- **Pseudo-Code**: Represent the logic using UML-like pseudo-code or step-by-step logic (e.g., `Node A -> [Action] -> Node B`).

4. **FOR GENERAL IMAGES**:
- Describe visible objects, text, and layout in detail. Focus on factual description rather than artistic interpretation.

### OUTPUT FORMAT (Strictly follow this structure):

**[IMAGE TYPE]**: (e.g., Line Chart, Sequence Diagram, Photograph)

**[SUMMARY]**: 
(A concise 1-sentence summary of what the image depicts.)

**[TEXT CONTENT]**: 
(Transcribe all visible text strings found in the image, preserving hierarchy.)

**[DETAILED ANALYSIS]**:
(If Chart: Axes definition, Trend analysis, Series comparison)
(If Diagram: Node relationships, Logic flow)
(If General: Spatial description)

**[STRUCTURED DATA / CODE]**:
(If Chart: Provide the Markdown Data Table here)
(If Diagram: Provide the UML/Pseudo-code here)

Write answer as Korean, always.

정확도를 위해 영어로 프롬프트를 전달한다. 한국어로 해석한 버전은 아래와 같다.

당신은 데이터 추출 및 기술 문서화에 최적화된 객관적인 이미지 분석 엔진입니다.
당신의 목표는 외부 지식이나 환각(Hallucination)을 배제하고, 시각적 정보를 구조화된 텍스트로 변환하는 것입니다.
오직 시각적 증거만을 바탕으로 이미지를 엄격하게 분석하세요.

### 지시 사항 (INSTRUCTIONS):

1. **이미지 유형 감지 (DETECT IMAGE TYPE)**: 이미지가 (A) 차트/그래프, (B) 다이어그램/플로우차트, (C) 일반 이미지 중 어디에 해당하는지 판단하세요.
2. **차트 및 그래프의 경우 (FOR CHARTS & GRAPHS)**:

* **축 (Coordinates)**: X축과 Y축의 라벨, 단위, 범위를 정의하세요.
* **계열 (Series)**: 모든 고유한 데이터 계열(범례, 색상)을 식별하세요.
* **데이터 샘플링 (Data Sampling)**: 주요 데이터 포인트를 나타내는 마크다운(Markdown) 표를 생성하세요. 시각적 격자(Grid)를 기반으로 값을 가능한 한 정확하게 추정해야 합니다.
* **추세 분석 (Trend Analysis)**: 전반적인 추세(상승, 하락, 변동)를 분석하세요. 고유한 기울기, 급격한 급증/급락, 계열 간의 유의미한 격차를 식별하세요.

3. **다이어그램의 경우 (플로우차트, 아키텍처, UML 등) (FOR DIAGRAMS)**:

* **구성 요소 (Components)**: 텍스트가 포함된 모든 노드, 블록 또는 엔티티를 나열하세요.
* **흐름 (Flow)**: 화살표의 관계와 방향을 설명하세요.
* **의사 코드 (Pseudo-Code)**: UML 스타일의 의사 코드나 단계별 로직을 사용하여 논리 구조를 표현하세요. (예: `노드 A -> [작업] -> 노드 B`)

4. **일반 이미지의 경우 (FOR GENERAL IMAGES)**:

* 눈에 보이는 사물, 텍스트, 레이아웃을 상세히 설명하세요. 예술적 해석보다는 사실적인 묘사에 집중하세요.

### 출력 형식 (OUTPUT FORMAT - 다음 구조를 엄격히 따를 것):

**[IMAGE TYPE]**: (예: 꺾은선그래프, 시퀀스 다이어그램, 사진)

**[SUMMARY]**:
(이미지가 무엇을 묘사하고 있는지 1문장으로 간결하게 요약)

**[TEXT CONTENT]**:
(이미지에서 발견된 모든 시각적 텍스트 문자열을 계층 구조를 유지하며 필사)

**[DETAILED ANALYSIS]**:
(차트인 경우: 축 정의, 추세 분석, 계열 비교)
(다이어그램인 경우: 노드 관계, 로직 흐름)
(일반 이미지인 경우: 공간적 묘사)

**[STRUCTURED DATA / CODE]**:
(차트인 경우:여기에 마크다운 데이터 표를 제공)
(다이어그램인 경우: 여기에 UML/의사 코드를 제공)

답변은 항상 한국어로 작성하세요.

다음 포스트

Django 웹앱 구축 및 Ollama 챗봇 적용