이미지-텍스트-텍스트
이미지-텍스트-텍스트 (Image-text-to-text)
Image-text-to-text 모델은 비전 언어 모델(VLM)이라고도 하며, 이미지 입력을 받는 언어 모델입니다. 시각 질의응답부터 이미지 분할까지 다양한 작업을 처리할 수 있습니다.
출처: 문서
본문
이 작업은 image-to-text와 많은 공통점이 있지만, 이미지 캡셔닝 같은 일부 중복 사용 사례도 있습니다. Image-to-text 모델은 이미지 입력만 받고 종종 특정 작업을 수행하는 반면, VLM은 열린 형식의 텍스트와 이미지 입력을 받고 더 범용적인(generalist) 모델입니다.
이 가이드에서는 VLM에 대한 간략한 개요를 제공하고 Transformers로 추론(inference)에 사용하는 방법을 보여드립니다.
먼저, VLM에는 여러 유형이 있습니다.
- 파인튜닝에 사용되는 기본(base) 모델
- 대화용 채팅 파인튜닝 모델
- 지시 파인튜닝 모델
이 가이드는 지시 튜닝된 모델로 추론하는 데 초점을 맞춥니다.
의존성 설치를 시작하겠습니다.
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)
processor = AutoProcessor.from_pretrained("Qwen/Qwen3-VL-4B-Instruct")
이 모델에는 사용자가 채팅 출력을 파싱하는 데 도움이 되는 chat template이 있습니다. 또한 이 모델은 단일 대화나 메시지에서 여러 이미지를 입력으로 받을 수 있습니다. 이제 입력을 준비하겠습니다.
이미지 입력은 다음과 같습니다.
단일 프롬프트에 이미지와 텍스트 입력을 사용하는 경우 아래와 같이 대화를 구성하세요.
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")
아래 예시는 채팅 템플릿으로 텍스트 입력을 형식화합니다.
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?'}]}]
"""
스트리밍 (Streaming)
더 나은 생성 경험을 위해 text streaming을 사용할 수 있습니다. 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 ...
더 작은 하드웨어에 모델 맞추기
VLM은 종종 크기가 커서 더 작은 하드웨어에 맞추기 위해 최적화가 필요합니다. Transformers는 많은 모델 양자화 라이브러리를 지원하며, 여기서는 Quanto로 int8 양자화만 보여드리겠습니다. int8 양자화는 (모든 가중치가 양자화된다면) 메모리를 최대 75% 절약할 수 있습니다. 하지만 8비트는 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']
이것으로 끝입니다. 모델을 변경 없이 동일하게 사용할 수 있습니다.
캐시로 반복 채팅하기
멀티모달 채팅 에이전트를 구축하고 있다면 여러 턴에 걸쳐 동일한 이미지나 오디오를 전달할 가능성이 높습니다. 캐싱을 사용하면 매번 다시 처리하는 대신 인코딩된 표현을 재사용해 중복 계산을 줄일 수 있습니다.
텍스트 전용 모델처럼 멀티모달 모델도 chat template으로 대화 기록을 추적합니다. 핵심 차이는 채팅 템플릿을 "text" 형식으로만 적용하고 새로운 멀티모달 콘텐츠는 별도로 처리한다는 것입니다. 템플릿을 처리와 함께 적용하면 Jinja가 이전에 인코딩된 입력과 새 입력을 구분할 수 없기 때문에 매 턴마다 전체 대화가 다시 처리됩니다.
아래 예시는 이 패턴을 보여줍니다. 텍스트 전용 모델의 경우 iterative generation 가이드를 참고하세요.
이 패턴을 보여주는 간단한 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)
추가 자료 (Further Reading)
다음은 image-text-to-text 작업을 위한 추가 리소스입니다.
- Image-text-to-text task page는 모델 유형, 사용 사례, 데이터셋 등을 다룹니다.
- Vision Language Models Explained는 시각 언어 모델과 TRL을 사용한 지도 파인튜닝에 관한 모든 것을 다루는 블로그 게시물입니다.
- Learn how to fine-tune vision language models using TRL