멀티모달 채팅 템플릿

멀티모달 채팅 템플릿 (Multimodal chat templates)

멀티모달 채팅 모델은 텍스트 외에 이미지, 오디오, 비디오 같은 입력도 받아들여요. 멀티모달 채팅 기록에서 content 키는 서로 다른 타입의 여러 항목을 담은 리스트예요. 텍스트 전용 채팅 모델의 content 키가 단일 문자열인 것과는 다르지요.

출처: 문서

본문

Tokenizer 클래스가 텍스트 전용 모델의 채팅 템플릿과 토큰화를 처리하는 것과 같은 방식으로, Processor 클래스는 멀티모달 모델의 전처리, 토큰화, 채팅 템플릿을 처리해요. 이들의 apply_chat_template() 메서드는 거의 동일해요.

이 가이드에서는 하이레벨 ImageTextToTextPipeline로, 그리고 더 로우레벨로는 apply_chat_template()과 generate() 메서드를 사용해서 멀티모달 모델과 채팅하는 방법을 보여줄게요.

ImageTextToTextPipeline

ImageTextToTextPipeline은 "챗 모드(chat mode)"를 가진 하이레벨 이미지·텍스트 생성 클래스예요. 대화형 모델이 감지되고 채팅 프롬프트가 제대로 형식화되면 챗 모드가 활성화돼요.

채팅 기록의 content 키에 이미지와 텍스트 블록을 추가해요.

messages = [
    {
        "role": "system",
        "content": [{"type": "text", "text": "You are a friendly chatbot who always responds in the style of a pirate"}],
    },
    {
      "role": "user",
      "content": [
            {"type": "image", "url": "http://images.cocodataset.org/val2017/000000039769.jpg"},
            {"type": "text", "text": "What are these?"},
        ],
    },
]

ImageTextToTextPipeline을 만들고 채팅을 전달해요. 큰 모델의 경우 device_map="auto"를 설정하면 모델을 더 빨리 로드하고 사용 가능한 가장 빠른 디바이스에 자동으로 배치할 수 있어요. 데이터 타입을 auto로 설정하는 것도 메모리를 아끼고 속도를 높이는 데 도움이 돼요.

import torch
from transformers import pipeline

pipe = pipeline("image-text-to-text", model="Qwen/Qwen2.5-VL-3B-Instruct", device_map="auto", dtype="auto")
out = pipe(text=messages, max_new_tokens=128)
print(out[0]['generated_text'][-1]['content'])
Ahoy, me hearty! These be two feline friends, likely some tabby cats, taking a siesta on a cozy pink blanket. They're resting near remote controls, perhaps after watching some TV or just enjoying some quiet time together. Cats sure know how to find comfort and relaxation, don't they?

해적 말투에서 현대 미국식 영어로 점점 떨어지는 것(어쨌든 3B 모델이니까요)만 빼면, 이 응답은 정확해요!

apply_chat_template 사용하기

텍스트 전용 모델처럼 apply_chat_template() 메서드를 사용해서 멀티모달 모델의 채팅 메시지를 준비해요. 이 메서드는 이미지와 다른 미디어 타입을 포함해 채팅 메시지의 토큰화와 형식화를 처리해요. 결과 입력은 생성용으로 모델에 전달돼요.

from transformers import AutoProcessor, AutoModelForImageTextToText

model = AutoModelForImageTextToText.from_pretrained("Qwen/Qwen2.5-VL-3B-Instruct", device_map="auto", torch_dtype="auto")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-3B-Instruct")

messages = [
    {
      "role": "system",
      "content": [{"type": "text", "text": "You are a friendly chatbot who always responds in the style of a pirate"}],
    },
    {
      "role": "user",
      "content": [
            {"type": "image", "url": "http://images.cocodataset.org/val2017/000000039769.jpg"},
            {"type": "text", "text": "What are these?"},
        ],
    },
]

입력 콘텐츠를 토큰화하려면 messages를 apply_chat_template()에 전달해요. 텍스트 모델과 달리 apply_chat_template의 출력에는 토큰화된 텍스트 외에 전처리된 이미지 데이터가 담긴 pixel_values 키가 있다는 점을 기억하세요.

processed_chat = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt")
print(list(processed_chat.keys()))
['input_ids', 'attention_mask', 'pixel_values', 'image_grid_thw']

이 입력들을 generate()에 전달해요.

out = model.generate(**processed_chat.to(model.device), max_new_tokens=128)
print(processor.decode(out[0]))

디코딩된 출력은 지금까지의 전체 대화를 담고 있어요. 사용자 메시지와 이미지 정보를 담은 placeholder 토큰을 포함하지요. 사용자에게 보여주기 전에 이전 대화를 출력에서 잘라내야 할 수 있어요.

비디오 입력 (Video inputs)

일부 비전 모델은 비디오 입력도 지원해요. 메시지 형식은 이미지 입력의 형식과 매우 비슷해요.

  • 콘텐츠 "type"은 콘텐츠가 비디오임을 나타내는 "video"여야 해요.
  • 비디오의 경우 비디오 링크("url")나 파일 경로("path")일 수 있어요. 비디오는 torchcodec으로 디코딩돼요. torchcodec이 없고 더 오래된 torchvision 버전을 쓰고 있다면 디코딩이 torchvision으로 폴백돼요.
  • URL이나 파일 경로에서 비디오를 로드하는 것 외에도, 디코딩된 비디오 데이터를 직접 전달할 수도 있어요. 이미 다른 곳에서 비디오 프레임을 전처리하거나 디코딩했다면 유용해요. 파일로 저장하거나 URL에 저장할 필요가 없어요.

[!TIP] PyAV와 Decord도 사용할 수 있어요. 단, 비디오를 직접 디코딩하고 load_video(backend=...)로 백엔드를 명시적으로 요청해야 해요.

from transformers import AutoProcessor, LlavaOnevisionForConditionalGeneration

model_id = "llava-hf/llava-onevision-qwen2-0.5b-ov-hf"
model = LlavaOnevisionForConditionalGeneration.from_pretrained(model_id)
processor = AutoProcessor.from_pretrained(model_id)

messages = [
    {
      "role": "system",
      "content": [{"type": "text", "text": "You are a friendly chatbot who always responds in the style of a pirate"}],
    },
    {
      "role": "user",
      "content": [
            {"type": "video", "url": "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_10MB.mp4"},
            {"type": "text", "text": "What do you see in this video?"},
        ],
    },
]

예시: 디코딩된 비디오 객체 전달하기

import numpy as np

video_object1 = np.random.randint(0, 255, size=(16, 224, 224, 3), dtype=np.uint8),

messages = [
    {
        "role": "system",
        "content": [{"type": "text", "text": "You are a friendly chatbot who always responds in the style of a pirate"}],
    },
    {
        "role": "user",
        "content": [
            {"type": "video", "video": video_object1},
            {"type": "text", "text": "What do you see in this video?"}
        ],
    },
]

기존의 ("load_video()") 함수를 사용해서 비디오를 로드하고, 메모리 안에서 비디오를 편집한 다음 메시지에 전달할 수도 있어요.


# Make sure a video backend library (torchcodec, pyav, or decord) is available.
from transformers.video_utils import load_video

# load a video file in memory for testing
video_object2, _ = load_video(
    "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_10MB.mp4"
)

messages = [
    {
        "role": "system",
        "content": [{"type": "text", "text": "You are a friendly chatbot who always responds in the style of a pirate"}],
    },
    {
        "role": "user",
        "content": [
            {"type": "video", "video": video_object2},
            {"type": "text", "text": "What do you see in this video?"}
        ],
    },
]

messages를 apply_chat_template()에 전달해서 입력 콘텐츠를 토큰화해요. 샘플링 과정을 제어하는 apply_chat_template()에 포함해야 할 몇 가지 추가 파라미터가 있어요.

num_frames 파라미터는 비디오에서 균등하게 샘플링할 프레임 수를 제어해요. 각 checkpoint는 사전 학습할 때 사용한 최대 프레임 수가 있고, 이를 초과하면 생성 품질이 크게 떨어질 수 있어요. 모델 용량과 하드웨어 리소스에 모두 맞는 프레임 수를 고르는 게 중요해요. num_frames를 지정하지 않으면 프레임 샘플링 없이 전체 비디오가 로드돼요.

processed_chat = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
    num_frames=32,
)
print(processed_chat.keys())

이 입력들은 이제 generate()에서 사용할 준비가 됐어요.

더 긴 비디오의 경우 fps 파라미터로 더 많은 프레임을 샘플링하는 게 더 나은 표현을 위해 좋을 수 있어요. 이는 초당 추출할 프레임 수를 결정해요. 예를 들어 비디오가 10초이고 fps=2라면 모델은 20프레임을 샘플링해요. 즉 10초마다 2프레임이 균등하게 샘플링되는 거지요.

processed_chat = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    fps=16,
)
print(processed_chat.keys())

비디오는 전체 비디오 파일이 아니라 이미지로 저장된 샘플링된 프레임 집합으로 존재할 수도 있어요.

이 경우 이미지 파일 경로 리스트를 전달하면 processor가 자동으로 그것들을 비디오로 연결(concatenate)해요. 모두 같은 비디오에서 온 것으로 가정하므로 모든 이미지가 같은 크기인지 확인하세요.

frames_paths = ["/path/to/frame0.png", "/path/to/frame5.png", "/path/to/frame10.png"]
messages = [
    {
        "role": "system",
        "content": [{"type": "text", "text": "You are a friendly chatbot who always responds in the style of a pirate"}],
    },
    {
      "role": "user",
      "content": [
            {"type": "video", "path": frames_paths},
            {"type": "text", "text": "What do you see in this video?"},
        ],
    },
]

processed_chat = processor.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
)
print(processed_chat.keys())

더 알아보기 (Learn more)