직접 API를 사용하여 VLM 실행
| 필드 | 값 |
|---|---|
| 범주 | GenAI |
| 난이도 | 중급 |
| 예상 소요 시간 | 10-15 minutes |
| 레이블 | genai, vlm, image, cache, multimodal |
비전-언어 모델은 텍스트와 이미지 텐서를 모두 입력으로 받을 수 있습니다. 하나의 질문에 대해 이미지를 GenerationRequest.images에 직접 첨부합니다. 반복적인 질문의 경우, 이미지를 한 번 인코딩하고 후속 요청에서 캐시된 이미지 임베딩을 재사용합니다.
둘러보기
VLM 및 이미지 로드
배포된 LLiMa 모델 디렉터리에서 VisionLanguageModel을 로드하고 디스크에서 이미지를 디코딩합니다.
OpenCV를 사용하여 이미지를 읽습니다. Neat은 3채널 cv::Mat 입력을 BGR로 처리하고 내부적으로 RGB로 변환합니다.
genai::VisionLanguageModel model(args.model);
cv::Mat image = cv::imread(args.image.string(), cv::IMREAD_COLOR);
if (image.empty()) {
throw std::runtime_error("failed to read image: " + args.image.string());
}
직접 이미 지를 사용하여 질문
첫 번째 요청에 이미지를 직접 첨부합니다. 이것은 가장 간단한 방법이며, 단일 시각적 질문에 충분한 경우가 많습니다.
genai::GenerationRequest direct;
direct.prompt = "Describe this image in one sentence.";
direct.images = {image};
direct.max_new_tokens = 96;
const genai::GenerationResult first = model.run(direct);
std::cout << "direct image: " << first.text << "\n\n";
이미지 임베딩 캐시
encode(...)를 호출하여 모델에 이미지 임베딩을 캐시합니다. 호출은 이미지가 수락되고 캐시되었을 때 true를 반환합니다.
if (!model.encode(image)) {
throw std::runtime_error("VLM did not accept the image for caching");
}
std::cout << "cached_images=" << model.cached_image_count() << "\n";
후속 질문
캐시된 이미지를 재사용해야 하는 각 요청에서 use_cached_images = true를 설정합니다. 동일한 캐시된 이미지에 대해 여러 질문을 할 수 있습니다. 해당 플래그가 없는 요청은 정상적으로 작동합니다. 텍스트 전용 요청은 이미지를 사용하지 않고, 직접 이미지 요청은 자체 images를 사용하며, 다른 encode(...) 호출은 캐시된 이미지를 대체합니다.
genai::GenerationRequest cached;
cached.prompt = "What details should I inspect more closely?";
cached.use_cached_images = true;
cached.max_new_tokens = 96;
const genai::GenerationResult follow_up = model.run(cached);
std::cout << "cached image: " << follow_up.text << "\n\n";
genai::GenerationRequest second_cached;
second_cached.prompt = "Summarize the image in three keywords.";
second_cached.use_cached_images = true;
second_cached.max_new_tokens = 48;
const genai::GenerationResult second_follow_up = model.run(second_cached);
std::cout << "cached image keywords: " << second_follow_up.text << "\n\n";
채팅 메시지에 이미지 첨부
messages를 사용하는 경우, 필요한 사용자 메시지에 이미지를 첨부합니다. 이렇게 하면 이미지가 해당 텍스트와 함께 유지됩니다.
genai::ChatMessage image_message;
image_message.role = "user";
image_message.content = "What is the main subject of this image?";
image_message.images = {image};
genai::GenerationRequest message_request;
message_request.messages = {image_message};
message_request.max_new_tokens = 96;
const genai::GenerationResult message_result = model.run(message_request);
std::cout << "message image: " << message_result.text << "\n";
실행
Modalix DevKit에서 LLiMa CLI를 사용하여 Hugging Face에서 LFM2-VL 1.6B VLM을 다운로드합니다.
llima pull LFM2-VL-1.6B-a16w4
Modalix에서 DevKit 로컬 모델 디렉터리와 로컬 이미지를 사용하여 튜토리얼을 실행합니다.
C++ (prebuilt):
./lib/sima-neat/tutorials/tutorial_020_run_a_vlm \
--model /media/nvme/llima/models/LFM2-VL-1.6B-a16w4 \
--image tests/images/people.jpg
C++ (build from source):
./build.sh --target tutorial_020_run_a_vlm
./build/tutorials-standalone/tutorial_020_run_a_vlm \
--model /media/nvme/llima/models/LFM2-VL-1.6B-a16w4 \
--image tests/images/people.jpg
예상되는 출력은 직접 이미지 요청에 대한 하나의 답변, 캐시된 이미지를 재 사용하는 여러 개의 후속 답변, 그리고 메시지 수준의 이미지 요청에 대한 하나의 답변입니다.
실전 활용
사용자가 동일한 프레임, 제품 이미지, 다이어그램 또는 문서 페이지에 대해 여러 질문을 할 때 이미지 캐싱을 사용합니다. 각 요청이 다른 이미지를 사용할 때는 캐싱을 피하십시오. 왜냐하면 직접 이미지 경로가 더 간단하고 프롬프트 상태를 명확하게 유지하기 때문입니다.
일부 모델 제품군은 캐시된 재사용을 지원하지 않을 수 있습니다. 이 경우 각 요청에 대해 직접 이미지를 사용하십시오.
대화를 구축하고 단 하나의 메시지만 이미지를 포함해야 할 때 ChatMessage.images를 사용합니다. 더 간단한 단일 프롬프트 형식에는 최상위 GenerationRequest.images를 사용합니다.
전체 소스
전체 소스 프로그램 표시
#include "neat/genai.h"
#include <opencv2/imgcodecs.hpp>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
namespace genai = simaai::neat::genai;
struct Args {
std::filesystem::path model;
std::filesystem::path image;
};
Args parse_args(int argc, char** argv) {
Args args;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--model" && i + 1 < argc) {
args.model = argv[++i];
} else if (arg == "--image" && i + 1 < argc) {
args.image = argv[++i];
} else {
throw std::runtime_error("usage: run_a_vlm --model <vlm_model_dir> --image <image>");
}
}
if (args.model.empty() || args.image.empty()) {
throw std::runtime_error("missing required --model <vlm_model_dir> or --image <image>");
}
return args;
}
int main(int argc, char** argv) {
try {
const Args args = parse_args(argc, argv);
genai::VisionLanguageModel model(args.model);
cv::Mat image = cv::imread(args.image.string(), cv::IMREAD_COLOR);
if (image.empty()) {
throw std::runtime_error("failed to read image: " + args.image.string());
}
genai::GenerationRequest direct;
direct.prompt = "Describe this image in one sentence.";
direct.images = {image};
direct.max_new_tokens = 96;
const genai::GenerationResult first = model.run(direct);
std::cout << "direct image: " << first.text << "\n\n";
if (!model.encode(image)) {
throw std::runtime_error("VLM did not accept the image for caching");
}
std::cout << "cached_images=" << model.cached_image_count() << "\n";
genai::GenerationRequest cached;
cached.prompt = "What details should I inspect more closely?";
cached.use_cached_images = true;
cached.max_new_tokens = 96;
const genai::GenerationResult follow_up = model.run(cached);
std::cout << "cached image: " << follow_up.text << "\n\n";
genai::GenerationRequest second_cached;
second_cached.prompt = "Summarize the image in three keywords.";
second_cached.use_cached_images = true;
second_cached.max_new_tokens = 48;
const genai::GenerationResult second_follow_up = model.run(second_cached);
std::cout << "cached image keywords: " << second_follow_up.text << "\n\n";
genai::ChatMessage image_message;
image_message.role = "user";
image_message.content = "What is the main subject of this image?";
image_message.images = {image};
genai::GenerationRequest message_request;
message_request.messages = {image_message};
message_request.max_new_tokens = 96;
const genai::GenerationResult message_result = model.run(message_request);
std::cout << "message image: " << message_result.text << "\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
return 1;
}
}