Daft 多模态结构化输出实战:用消融实验与 VLM-as-a-Judge 规模化评估 VLM 图像理解

Daft 多模态结构化输出实战:用消融实验与 VLM-as-a-Judge 规模化评估 VLM 图像理解 Daft 多模态结构化输出实战用消融实验与 VLM-as-a-Judge 规模化评估 VLM 图像理解【免费下载链接】DaftHigh-performance data engine for AI and multimodal workloads. Process images, audio, video, and structured data at any scale项目地址: https://gitcode.com/GitHub_Trending/da/Daft一个基于 Daft 与 Qwen3-VL-8B 的端到端多模态结构化输出Multimodal Structured Outputs示例从数据加载、图像解码、Pydantic 结构化推理到带图/不带图消融实验、四象限诊断分类与 VLM-as-a-Judge 失败归因完整覆盖可扩展的图像理解评估流水线。导读本文以 Daft 为核心引擎演示如何对视觉语言模型VLM的图像理解能力做一次可审计、可归因、可扩展的量化评估。我们将使用 HuggingFace 上 The Cauldron 数据集中的 AI2D 子集科学图表配多选题让 Qwen3-VL-8B 分别在带图与不带图两种条件下输出结构化答案通过消融实验区分真图像理解与文本捷径再借助VLM-as-a-Judge对失败样本做结构化归因题目问题 vs 图像理解问题。读完本文你将掌握prompt函数配合 Pydantic 模型实现结构化输出、format表达式动态拼提示词、when条件表达式完成四象限分类以及一套可平滑扩展到百万行规模的评估方法论。本教程在 50 行小样本上运行方便逐条检查示例与理解方法官方在其博客中评估了 Qwen3-VL-4B 在 3 个数据集上的 2 万行数据生产级脚本eval_image_understanding.py可运行于 Daft Cloud支撑百万行级评估。评估流水线总览整个评估管线由四步构成每一步都产出一个可独立检查的 DataFrame 列结构化输出推理对图像 文本的多选题进行结构化输出推理强制模型只返回答案字母消融实验在同一批问题上去掉图像再跑一遍暴露图像理解中的文本偏置textual bias四象限分类把每条样本按带图对错 × 不带图对错归入四个诊断象限VLM-as-a-Judge对信息量最大的失败子集Image Hurt 与 Both Incorrect用第二个结构化输出让裁判模型解释失败原因。下面按原教程的 8 个步骤逐步展开并在每个环节结合 Daft 源码解释底层原理。1. 环境准备Setup安装依赖prompt功能基于 OpenAI 兼容协议实现因此需要daft[openai].env文件的读取依赖python-dotenv。pip install daft[openai] python-dotenv在项目目录创建.env文件并填入 HuggingFace Token用于鉴权访问托管推理端点可在 HuggingFace 账号设置页生成# .env HF_TOKENyour_huggingface_token_here然后在 Python 中加载环境变量并配置模型与推理端点import os from dotenv import load_dotenv load_dotenv() # Configuration MODEL_ID Qwen/Qwen3-VL-8B-Instruct LIMIT 50 # Keep low for interactive demo # HuggingFace Inference Provider (hosted Qwen3-VL endpoints) OPENAI_API_KEY os.getenv(HF_TOKEN) OPENAI_BASE_URL https://router.huggingface.co/v1将 Daft 的默认模型 Provider 指向 OpenAI 兼容端点。这里base_url是 HuggingFace Inference Provider 的 OpenAI 兼容路由api_key复用 HF Tokenimport daft # Set the OpenAI-compatible provider daft.set_provider(openai, api_keyOPENAI_API_KEY, base_urlOPENAI_BASE_URL)源码佐证daft.set_provider定义在 daft/session.py会转发到会话层。根据 daft/session.py 的实现传入已知 provider 标识符如openai时会自动创建并挂载该 provider——这正是本教程只写一行配置即可完成接入的原因。底层请求由 daft/ai/openai/protocols/prompter.py 中的OpenAIPrompterDescriptor负责其默认prompt_options为use_chat_completionsFalse、max_retries3、on_errorraise即默认走 OpenAI Responses API最多自动重试 3 次。2. 数据加载Data LoadingThe Cauldron 是一个包含 50 个视觉-语言数据集的巨型集合覆盖视觉问答VQAOCR 与文档理解图表/图形理解推理与数学以及更多……本教程选用其中的AI2D子集——科学图表配多选题df_raw daft.read_huggingface(HuggingFaceM4/the_cauldron/ai2d).limit(LIMIT).collect() df_raw.show(3)数据集为嵌套结构包含以下列images图像字节列表List of image bytestexts对话轮次列表含user问题与assistant答案字段其他元数据字段每一行代表一道科学图表 多选题。源码佐证read_huggingface定义在 daft/io/huggingface/init.py优先走快路径直接把hf://datasets/{repo}下的 Parquet 文件交给read_parquet以列式格式并行读取若该数据集没有可用的 Parquet 文件如 400 错误或 glob 无匹配则回退到datasets库加载全部 split 再转为 Arrow 表_fallback_to_datasets_library。这意味着在 AI2D 这种带 Parquet 快照的数据集上加载是纯列式、免下载大文件的。3. 预处理Preprocessing预处理完成两件事把图像字节解码为 Daft 的 Image 类型从对话文本中抽取问题、选项与正确答案。from daft import col from daft.functions import unnest df_img df_raw.explode(col(images)) df_img df_img.with_column(image, col(images)[bytes].decode_image()) df_text df_img.explode(col(texts)).select(unnest(col(texts)), image) df_prep df_text.with_column( answer, col(assistant).regexp_replace(Answer: , ).lstrip().rstrip() ).collect() df_prep.show(3)这里用到的四个关键操作explode(col(images))把每行的图像列表展开成多行每行一张图。根据 daft/functions/list.py 的文档explode 会为列表中每个元素生成一行其他列广播复制一次投影只允许一次 explode所以本教程的两次 explode 分步执行。col(images)[bytes].decode_image()从嵌套结构里取出bytes字段并解码为 Daft 图像类型prompt函数才能把图像作为多模态输入传给模型。explode(col(texts))把对话轮次列表逐轮展开。unnest(col(texts))把对话轮次这一struct 列的字段user、assistant等平铺成独立列。该函数定义在 daft/functions/struct.py。regexp_replace(Answer: , )配合lstrip()/rstrip()把assistant回答里的Answer:前缀去掉并裁剪空白得到纯净的答案字母。4. 用prompt实现结构化输出Structured Outputs withpromptDaft 的prompt函数能把 OpenAI 兼容调用向量化地铺满整个 DataFrame——每一行自动成为一个推理请求框架负责批处理与并行调度。配合Pydantic 模型即可强制结构化输出。from daft.functions import prompt from pydantic import BaseModel, Field import time PARAMS {temperature: 0.0, max_tokens: 2} class ChoiceResponse(BaseModel): Structured output for multiple choice answers. choice: str Field(..., descriptionThe letter of the correct choice (e.g., A, B, C, D)) start time.time() df_results df_prep.with_column( result, prompt( messages[col(image), col(user)], modelMODEL_ID, use_chat_completionsTrue, return_formatChoiceResponse, **PARAMS, ) ).limit(LIMIT).collect() elapsed time.time() - start print(fProcessed {df_results.count_rows()} rows in {elapsed:.1f} seconds)要点拆解messages[col(image), col(user)]多模态消息。prompt的messages参数接受表达式列表每个元素可以是纯文本、图像数据numpy 数组、字节或 File 对象按 MIME 类型识别或文件。详见 daft/functions/ai/init.py 的参数文档。return_formatChoiceResponse传入 Pydantic 模型即启用结构化输出模型必须返回符合该 schema 的 JSONDaft 会把结果物化为嵌套 struct 列Field(description...)里的描述会作为 schema 提示注入请求。use_chat_completionsTrue切换到 Chat Completions API。根据 daft/ai/openai/protocols/prompter.pyOpenAI Provider 默认走 Responses APIuse_chat_completionsFalse而 Chat Completions 的消息格式为{type: text, text: ...}两种模式的消息构建逻辑不同。对于 HuggingFace Inference Provider 这类兼容端点通常需要显式指定use_chat_completionsTrue。temperature0.0, max_tokens2评测场景要确定性温度归零多选题答案只有一个字母max_tokens2既省成本又防模型话痨。**kwargs会原样透传给推理请求merge_provider_and_api_options合并后交给AsyncOpenAI客户端。df_prep.with_column(...)推理结果是延迟执行的表达式直到.collect()才真正触发所有行的并发请求。.limit(LIMIT)在推理列之后再次截断确保演示规模可控。紧接着计算带图准确率并查看预测样例df_eval df_results.with_column( is_correct, col(result)[choice].lstrip().rstrip() col(answer).lstrip().rstrip() ) accuracy df_eval.where(col(is_correct)).count_rows() / df_eval.count_rows() print(fAccuracy (with image): {accuracy:.1%}) df_eval.select(user, image, answer, col(result)[choice].alias(predicted), is_correct).show(5)col(result)[choice]从结构化输出的 struct 列中取出choice字段where(col(is_correct))过滤正确行后计数即得正确数。更完整的prompt用法提示词模板、vLLM 在线服务、OpenRouter/Transformers 等 Provider可参考仓库内的 docs/ai-functions/prompt.md 与 docs/ai-functions/providers.md。5. 消融实验与四象限分析Ablation Study单纯的准确率只告诉我们对多少次不告诉我们为什么对。官方在完整评估中发现图像理解基准上约70% 的正确答案其实并不需要图像。为分离图像理解的真实贡献我们做消融实验——同一批问题、同样的结构化输出只是不传图像并追加一条 system prompt 强调只输出答案字母。SYSTEM_PROMPT_NO_IMAGE Respond to the multiple choice question with just the letter corresponding to the correct answer. start time.time() df_ablation df_eval.with_column( result_no_image, prompt( messagescol(user), system_messageSYSTEM_PROMPT_NO_IMAGE, modelMODEL_ID, use_chat_completionsTrue, return_formatChoiceResponse, **PARAMS, ) ).with_column( is_correct_no_image, col(result_no_image)[choice].lstrip().rstrip() col(answer).lstrip().rstrip() ).collect() elapsed time.time() - start print(fProcessed {df_ablation.count_rows()} rows in {elapsed:.1f} seconds) accuracy_no_image df_ablation.where(col(is_correct_no_image)).count_rows() / df_ablation.count_rows() print(fAccuracy with image: {accuracy:.1%}) print(fAccuracy without image: {accuracy_no_image:.1%}) print(fDelta: {accuracy - accuracy_no_image:.1%})system_message会被自动前置为{role: system, content: ...}消息见 daft/ai/openai/protocols/prompter.py 的拼装逻辑。注意这次messages只传了col(user)——没有图像从而隔离出纯文本推理的表现。有了两组对错标签就能把每条样本归入四个诊断象限QuadrantWith ImageWithout ImageInterpretationBoth Correct✓✓Question may be solvable from text aloneImage Helped✓✗True image understandingImage Hurt✗✓Visual confusionBoth Incorrect✗✗Hard question or model limitation用 Daft 的when条件表达式实现分类from daft.functions import when, monotonically_increasing_id df_classified df_ablation.with_column( id, monotonically_increasing_id() ).with_column( quadrant, when((col(is_correct) True) (col(is_correct_no_image) True), Both Correct) .when((col(is_correct) True) (col(is_correct_no_image) False), Image Helped) .when((col(is_correct) False) (col(is_correct_no_image) True), Image Hurt) .otherwise(Both Incorrect) ) df_classified.groupby(quadrant).count().select(quadrant, col(id).alias(count)).show()when(cond, then)语义等价于 SQL 的CASE WHENdaft/functions/misc.py链式调用.when(...).otherwise(...)。monotonically_increasing_id()生成唯一递增 IDdaft/functions/misc.py此处借作行号/计数列。检查图像真正帮助的样本——这是衡量模型视觉能力的关键证据df_classified.where(col(quadrant) Image Helped).select( user, image, answer, col(result)[choice].alias(with_image), col(result_no_image)[choice].alias(without_image) ).show(3)检查图像反而有害的样本——视觉混淆visual confusion的典型场景df_classified.where(col(quadrant) Image Hurt).select( user, image, answer, col(result)[choice].alias(with_image), col(result_no_image)[choice].alias(without_image) ).show(3)最后汇总各象限占比total_count df_classified.count_rows() df_results df_classified.groupby(quadrant).count().select( quadrant, col(id).alias(count) ).with_column( percentage, (col(count) / daft.lit(total_count) * 100) ).collect() df_results.show()6. VLM-as-a-Judge结构化失败归因通过/不通过这类二元指标还不足以指导改进。对信息量最大的两个失败子集我们用VLM-as-a-Judge解释模型为什么答错Image Hurt不带图对、带图错Both Incorrect带图不带图都错。裁判也使用结构化输出 schema保证返回字段可直接落表分析from daft.functions import format JUDGE_SYSTEM_PROMPT You are an impartial judge reviewing the results of a textbook academic questions multiple choice benchmark. Inspect the attached image and provide high-signal feedback on why the model chose its answer. First, reason about the models answer with the image and the models answer without the image. Second, develop a hypothesis for why the model made the choice it did. Third, attribute the failure to a question issue or an image understanding issue. Finally, assign whether the models answer with the image is correct and whether the models answer without the image is correct. class JudgeResponse(BaseModel): Structured diagnostic feedback from the VLM judge. reasoning: str Field(..., descriptionWhy did the model choose the answer it did?) hypothesis: str Field(..., descriptionWhat caused the divergence from the correct answer?) attribution: str Field( ..., descriptionWas this a question issue or an image understanding issue or other?, )用format表达式把每行的四类信息问题、带图答案、不带图答案、正确答案动态拼成裁判提示词。format与 Python 的str.format类似但作用于 Daft 表达式daft/functions/str.py行内question{}/question这类占位符随行替换judge_template format( Given the image attached and the multiple choice question of question{}/question, The model chose the following prediction model_answer{}/model_answer and without the image, the model chose the following prediction no_image_model_answer{}/no_image_model_answer, but the correct answer is correct_answer{}/correct_answer. Provide diagnostic feedback. , col(user), col(result)[choice], col(result_no_image)[choice], col(answer), ) df_failures df_classified.where( (col(quadrant) Image Hurt) | (col(quadrant) Both Incorrect) ) JUDGE_PARAMS {temperature: 0.0, max_tokens: 512} df_judged df_failures.with_column( judge_response, prompt( messages[col(image), judge_template], system_messageJUDGE_SYSTEM_PROMPT, modelMODEL_ID, use_chat_completionsTrue, return_formatJudgeResponse, **JUDGE_PARAMS, ), ).collect() print(fJudged {df_judged.count_rows()} failure rows)裁判返回的attribution字段把失败原因区分为题目问题提示词歧义与图像理解问题漏读标签、视觉歧义比单一准确率多出一个可量化、可聚合的归因维度df_judged.select( quadrant, user, image, answer, col(result)[choice].alias(with_image), col(result_no_image)[choice].alias(without_image), unnest(col(judge_response)), ).show(3)unnest(col(judge_response))再次把裁判的 struct 字段reasoning、hypothesis、attribution平铺成列便于后续筛选、聚合或导出。验证整条流水线的运行结果print(fAccuracy (with image): {accuracy:.1%}) print(fAccuracy (without image): {accuracy_no_image:.1%}) print(fDelta: {accuracy - accuracy_no_image:.1%}) df_classified.groupby(quadrant).count().show() print(fJudge rows: {df_judged.count_rows()})从实现层面看prompt的底层是一个类 UDF 的表达式_PrompterExpression在 daft/ai/_expressions.py 中把一次请求委托给Prompter协议实现OpenAI/Google/Transformers/vLLM 各有一个实现并自动完成并行批处理、重试与 token 用量指标记录见_record_usage_metrics这就是一条with_column铺满整个 DataFrame的并行化来源。对应测试可参考 tests/ai/openai/test_openai_prompter.py 等文件。7. 扩展到大规模Daft Cloud本教程在本地跑 50 行。而 The Cauldron 包含50 个子集、数百万行。要跑完整评估可使用 Daft Cloud 托管执行数据加载、预处理、结构化推理、消融、裁判归因这些阶段在 Daft 中全部是惰性表达式 列式并行的同一套代码从 50 行换到百万行只需调整集群规模与LIMIT无需重写逻辑。官方仓库中的生产级脚本eval_image_understanding.py额外包含多数据集评估覆盖所有 Cauldron 子集而非单一 AI2D可配置批处理批量大小、并发、重试等参数化结果聚合与导出把象限分布、裁判归因聚合成可下发的报表。8. 结论与下一步至此我们构建了一套完整的小规模 VLM 图像理解评估流水线四个关键能力结构化输出用 Pydantic 模型强制模型输出一致、可落表的响应消融实验把图像理解从通用推理中隔离出来暴露文本捷径与视觉混淆四象限分析把结果归入 Both Correct / Image Helped / Image Hurt / Both Incorrect 四类可行动类别VLM-as-a-Judge只在信息量最大的失败子集Image Hurt Both Incorrect上做结构化失败诊断。下一步建议多数据集评估参考官方 daft-examples 仓库中的完整脚本把评估扩展到全部 50 个 Cauldron 子集获得跨任务类型的图像理解全景。实验追踪把is_correct信号与裁判的attribution归因接入 MLflow 或 WB 等实验追踪系统随模型版本迭代跟踪改进趋势。RLVR 训练利用is_correct信号与裁判归因作为可验证奖励verifiable rewards为强化学习RLVR训练提供标注信号——这正是结构化输出带来的附加价值机器可直接消费的信号。仓库内延伸阅读docs/ai-functions/prompt.mdprompt函数完整指南模板、参数、结构化输出、多模态输入、工具调用docs/ai-functions/providers.mdOpenAI 兼容 Provider、vLLM 在线服务、Transformers 本地推理等配置docs/ai-functions/overview.mdAI 函数体系总览daft/functions/ai/init.pyprompt函数源码与参数文档daft/ai/openai/protocols/prompter.pyOpenAI Prompter 的请求构建与消息处理实现tests/ai/openai/test_openai_prompter.pyprompt行为测试示例补充参考结构化 LLM 输出方法与 LLM-as-a-Judge 方法论NeurIPS 2023是本文方法论的经典出处可在学术检索中进一步查阅。【免费下载链接】DaftHigh-performance data engine for AI and multimodal workloads. Process images, audio, video, and structured data at any scale项目地址: https://gitcode.com/GitHub_Trending/da/Daft创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考