Transformers documentation

이미지-텍스트-투-텍스트

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

이미지-텍스트-투-텍스트

이미지-텍스트-투-텍스트 모델, 즉 비전 언어 모델(VLM)은 이미지 입력을 받는 언어 모델입니다. 이러한 모델은 시각적 질의응답부터 이미지 분할까지 다양한 작업을 수행할 수 있습니다. 이 작업은 이미지-투-텍스트와 많은 유사점을 공유하지만, 이미지 캡셔닝과 같은 일부 중복되는 사용 사례가 있습니다. 이미지-투-텍스트 모델은 이미지 입력만 받아 특정 작업을 수행하는 경우가 많지만, VLM은 개방형 텍스트와 이미지 입력을 모두 받아 더 범용적인 모델로 동작합니다.

이 가이드에서는 VLM에 대한 간단한 개요를 제공하고, Transformers를 사용하여 추론을 수행하는 방법을 보여 줍니다.

시작하기에 앞서, VLM에는 여러 유형이 있습니다:

  • fine-tuning에 사용되는 base models
  • 대화용 fine-tuned 모델
  • 지시어 파인튜닝된 모델

이 가이드는 지시 튜닝된 모델로 추론하는 데 초점을 맞춥니다.

의존성을 설치하는 것으로 시작해 보겠습니다.

pip install -q transformers accelerate 
pip install flash-attn --no-build-isolation

모델과 프로세서를 초기화해 보겠습니다.

from transformers import AutoProcessor, AutoModelForImageTextToText
from accelerate import Accelerator
import torch

device = Accelerator().device
model = AutoModelForImageTextToText.from_pretrained(
    "Qwen/Qwen3-VL-4B-Instruct",
    dtype=torch.bfloat16,
    attn_implementation="flash_attention_2",
).to(device)

Qwen3-VL, Qwen3.5와 같은 동적 해상도 모델은 기본적으로 약 16MP의 longest_edge를 사용하며, 이로 인해 이미지당 10,000개 이상의 시각 토큰이 생성되어 추론이 중단되거나 소비자용 GPU에서 OOM이 발생할 수 있습니다. 이를 제한하려면 프로세서를 로드할 때 min_pixels와 max_pixels를 설정하세요.

processor = AutoProcessor.from_pretrained(
    "Qwen/Qwen3-VL-4B-Instruct",
    min_pixels=224 * 224,
    max_pixels=1024 * 1024,
)

이 모델에는 chat template가 포함되어 있어 사용자가 채팅 출력을 파싱하는 데 도움이 됩니다. 또한, 이 모델은 단일 대화나 메시지 내에서 여러 이미지를 입력으로 받을 수도 있습니다. 이제 입력을 준비하겠습니다.

이미지 입력은 다음과 같은 형태입니다.

Two cats sitting on a net
A bee on a pink flower

이미지와 텍스트 입력이 포함된 단일 프롬프트에 대해 아래에 표시된 대로 대화를 구성하세요.

messages = [
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png"},
            {"type": "text", "text": "What do we see in this image?"},
        ]
    }
]

user와 assistant 역할을 번갈아 사용하여 이전 컨텍스트를 기반으로 모델을 학습시켜 더 나은 응답을 생성합니다.

messages = [
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png"},
            {"type": "text", "text": "What do we see in this image?"},
        ]
    },
    {
        "role": "assistant",
        "content": [
            {"type": "text", "text": "In this image we can see two cats on the nets."},
        ]
    },
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg"},
            {"type": "text", "text": "And how about this image?"},
        ]
    },
]

이제 프로세서의 apply_chat_template() 메서드를 호출하여 출력과 이미지 입력을 전처리합니다.

inputs = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt").to(device)

이제 전처리된 입력을 모델에 전달할 수 있습니다.

input_len = len(inputs.input_ids[0])

with torch.no_grad():
    generated_ids = model.generate(**inputs, max_new_tokens=200)
generated_texts = processor.batch_decode(generated_ids[:, input_len:], skip_special_tokens=True)

print(generated_texts)
## ['In this image we can see flowers, plants and insect.']

Pipeline

가장 빠른 시작 방법은 Pipeline API를 사용하는 것입니다. "image-text-to-text" 작업과 사용하려는 모델을 지정하세요.

from transformers import pipeline
pipe = pipeline("image-text-to-text", model="llava-hf/llava-interleave-qwen-0.5b-hf")

아래 예시는 텍스트 입력을 포맷팅하기 위해 chat templates를 사용합니다.

messages = [
     {
         "role": "user",
         "content": [
             {
                 "type": "image",
                 "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg",
             },
             {"type": "text", "text": "Describe this image."},
         ],
     },
     {
         "role": "assistant",
         "content": [
             {"type": "text", "text": "There's a pink flower"},
         ],
     },
 ]

채팅 템플릿으로 포맷된 텍스트와 이미지를 Pipeline에 전달하고, return_full_text=False를 설정하여 생성된 출력에서 입력을 제거합니다.

outputs = pipe(text=messages, max_new_tokens=20, return_full_text=False)
outputs[0]["generated_text"]
#  with a yellow center in the foreground. The flower is surrounded by red and white flowers with green stems

원하시는 경우, 이미지를 별도로 로드한 후 다음과 같이 pipeline에 전달할 수도 있습니다:

import requests
from PIL import Image
from transformers import pipeline

pipe = pipeline("image-text-to-text", model="HuggingFaceTB/SmolVLM-256M-Instruct")

img_urls = [
    "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png",
    "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg",
]
images = [
    Image.open(requests.get(img_urls[0], stream=True).raw),
    Image.open(requests.get(img_urls[1], stream=True).raw),
]

messages = [
    {
        "role": "user",
        "content": [
            {"type": "image"},
            {"type": "image"},
            {"type": "text", "text": "What do you see in these images?"},
        ],
    }
]
outputs = pipe(text=messages, images=images, max_new_tokens=50, return_full_text=False)
outputs[0]["generated_text"]
" In the first image, there are two cats sitting on a plant. In the second image, there are flowers with a pinkish hue."

이미지는 출력의 "input_text" 필드에 여전히 포함됩니다:

outputs[0]['input_text']
"""
[{'role': 'user',
  'content': [{'type': 'image',
    'image': <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=622x412>},
   {'type': 'image',
    'image': <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=5184x3456>},
   {'type': 'text', 'text': 'What do you see in these images?'}]}]
"""

스트리밍

더 나은 생성 경험을 위해 텍스트 스트리밍을 사용할 수 있습니다. Transformers는 TextStreamer 또는 TextIteratorStreamer 클래스를 통해 스트리밍을 지원합니다. IDEFICS-8B에는 TextIteratorStreamer를 사용합니다.

채팅 히스토리를 유지하고 새로운 사용자 입력을 받는 애플리케이션이 있다고 가정해 보겠습니다. 입력을 통상적으로 전처리한 후, 별도 스레드에서 생성을 처리하기 위해 TextIteratorStreamer를 초기화합니다. 이를 통해 생성된 텍스트 토큰을 실시간으로 스트리밍할 수 있습니다. 생성 인수는 TextIteratorStreamer에 전달할 수 있습니다.

import time
from transformers import TextIteratorStreamer
from threading import Thread

def model_inference(
    user_prompt,
    chat_history,
    max_new_tokens,
    images
):
    user_prompt = {
        "role": "user",
        "content": [
            {"type": "image"},
            {"type": "text", "text": user_prompt},
        ]
    }
    chat_history.append(user_prompt)
    streamer = TextIteratorStreamer(
        processor.tokenizer,
        skip_prompt=True,
        timeout=5.0,
    )

    generation_args = {
        "max_new_tokens": max_new_tokens,
        "streamer": streamer,
        "do_sample": False
    }

    # add_generation_prompt=True makes model generate bot response
    prompt = processor.apply_chat_template(chat_history, add_generation_prompt=True)
    inputs = processor(
        text=prompt,
        images=images,
        return_tensors="pt",
    ).to(device)
    generation_args.update(inputs)

    thread = Thread(
        target=model.generate,
        kwargs=generation_args,
    )
    thread.start()

    acc_text = ""
    for text_token in streamer:
        time.sleep(0.04)
        acc_text += text_token
        if acc_text.endswith("<end_of_utterance>"):
            acc_text = acc_text[:-18]
        yield acc_text

    thread.join()

이제 만든 model_inference 함수를 호출하여 값을 스트리밍해 보겠습니다.

generator = model_inference(
    user_prompt="And what is in this image?",
    chat_history=messages[:2],
    max_new_tokens=100,
    images=images
)

for value in generator:
  print(value)

# In
# In this
# In this image ...

smaller hardware에서 모델을 학습

VLM은 일반적으로 크며, 더 작은 하드웨어에 맞게 최적화가 필요합니다. Transformers는 다양한 모델 양자화 라이브러리를 지원하며, 여기서는 Quanto를 사용한 int8 양자화만 소개합니다. int8 양자화는 (모든 가중치가 양자화된 경우) 최대 75%까지 메모리 사용량을 줄여줍니다. 다만, 8-bit는 CUDA 네이티브 정밀도가 아니므로 가중치가 실시간으로 양자화 및 역양자화되며, 이로 인해 지연 시간이 증가합니다.

먼저, 종속성을 설치합니다.

pip install -U optimum-quanto bitsandbytes

모델을 로드하는 동안 양자화를 수행하려면 먼저 QuantoConfig를 생성해야 합니다. 그런 다음 모델을 평소처럼 로드하되, 모델 초기화 시 quantization_config를 전달합니다.

from transformers import AutoModelForImageTextToText, QuantoConfig

model_id = "Qwen/Qwen3-VL-4B-Instruct"
quantization_config = QuantoConfig(weights="int8")
quantized_model = AutoModelForImageTextToText.from_pretrained(
    model_id, device_map="auto", quantization_config=quantization_config
)

messages = [
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png"},
            {"type": "text", "text": "What do we see in this image?"},
        ]
    },
]
inputs = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt").to(quantized_model.device)
input_len = len(inputs.input_ids[0])

with torch.no_grad():
    generated_ids = quantized_model.generate(**inputs, cache_implementation="static", max_new_tokens=100)
generated_texts = processor.batch_decode(generated_ids[:, input_len:], skip_special_tokens=True)

print(generated_texts[0])
## ['In this image, we see two tabby cats resting on a large, tangled pile of fishing nets. The nets are a mix of brown, orange, and red colors, with some blue and green ropes visible in the background. The cats appear relaxed and comfortable, nestled into the fibers of the nets. One cat is in the foreground, looking slightly to the side, while the other is positioned further back, looking directly at the camera. The scene suggests a coastal or fishing-related setting, possibly near']

그리고 끝입니다. 모델은 변경 사항 없이 동일한 방식으로 사용할 수 있습니다.

캐시를 활용한 반복적 채팅

멀티모달 챗 에이전트를 구축하고 있다면, 동일한 이미지나 오디오를 여러 턴에 걸쳐 전달하는 경우가 많을 것입니다. 캐싱을 사용하면 매번 다시 처리하는 대신 인코딩된 표현을 재사용할 수 있어 중복 계산을 줄일 수 있습니다.

텍스트 전용 모델과 마찬가지로, 멀티모달 모델은 채팅 템플릿을 통해 대화 이력을 추적합니다. 핵심적인 차이점은 채팅 템플릿을 "text" 형식으로만 적용하고, 새로운 멀티모달 콘텐츠는 별도로 처리한다는 것입니다. 템플릿을 처리와 함께 적용하면, Jinja가 이전에 인코딩된 입력과 새로운 입력을 구별할 수 없으므로 매 턴마다 전체 대화가 다시 처리됩니다.

아래 예제는 이 패턴을 보여줍니다. 텍스트 전용 모델의 경우 반복 생성 가이드를 참고하세요.

이 패턴을 보여주는 간단한 Python 예제입니다. 텍스트 전용 모델에 대한 예제는 이 가이드를 참고하세요.

import torch
from transformers import AutoProcessor, Gemma4ForConditionalGeneration, TextStreamer
from transformers.cache_utils import DynamicCache
from transformers.image_utils import load_image
from transformers.audio_utils import load_audio

model_id = 'google/gemma-4-E2B-it'
processor = AutoProcessor.from_pretrained(model_id)
model = Gemma4ForConditionalGeneration.from_pretrained(model_id, dtype=torch.float32, device_map='cpu')

past_key_values = DynamicCache()
streamer = TextStreamer(processor.tokenizer, skip_prompt=True, skip_special_tokens=True)

## Turn 1: multimodal input (text + image + audio)
audio = load_audio("https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/jfk.wav")
image = load_image("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/bee.jpg")
messages = [
    {
        "role": "user",
        "content": [
            {"type": "text", "text": "In detail, describe the following audio and image."},
            {"type": "audio"},
            {"type": "image"},
        ],
    },
]
input_string = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=input_string, images=[image], audio=[audio], return_tensors='pt')
output = model.generate(**inputs, past_key_values=past_key_values, max_new_tokens=1024, do_sample=False, streamer=streamer)
first_response = processor.decode(output[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)

## Turn 2: text-only follow-up (reuses KV cache, no new media)
messages.append({"role": "assistant", "content": [{"type": "text", "text": first_response}]})
cached_string = processor.apply_chat_template(messages, tokenize=False)
messages.append({"role": "user", "content": [{"type": "text", "text": "Summarize the previous descriptions in one sentence."}]})
full_string = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
new_string = full_string[len(cached_string):]

new_input_ids = processor.tokenizer(new_string, add_special_tokens=False, return_tensors='pt')['input_ids']
# Build and pass an attention mask only if batched input or there is padding
# attention_mask = torch.ones(1, past_key_values.get_seq_length() + new_input_ids.shape[1], dtype=torch.long)

output2 = model.generate(input_ids=new_input_ids, past_key_values=past_key_values, max_new_tokens=1024, do_sample=False, streamer=streamer)
second_response = processor.decode(output2[0][new_input_ids.shape[1]:], skip_special_tokens=True)

## Turn 3: new image (must go through processor for image token expansion + encoding)
image2 = load_image("https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/artemis.jpeg")
messages.append({"role": "assistant", "content": [{"type": "text", "text": second_response}]})
cached_string2 = processor.apply_chat_template(messages, tokenize=False)
messages.append({"role": "user", "content": [{"type": "text", "text": "Describe this image."}, {"type": "image"}]})
full_string2 = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
new_string2 = full_string2[len(cached_string2):]

new_inputs2 = processor(text=new_string2, images=[image2], add_special_tokens=False, return_tensors='pt')
# new_inputs2['attention_mask'] = torch.ones(1, past_key_values.get_seq_length() + new_inputs2['input_ids'].shape[1], dtype=torch.long)
new_inputs2['past_key_values'] = past_key_values

output3 = model.generate(**new_inputs2, max_new_tokens=1024, do_sample=False, streamer=streamer)

추가 자료

이미지-텍스트-대-텍스트 작업에 대한 추가 리소스입니다.

Update on GitHub