Skip to content

DeepEyes 示例

DeepEyes 示例演示了使用 Relax 进行 Agent 式多轮视觉语言强化学习。模型学习通过工具调用(缩放、旋转)与图像交互,在回答视觉问题前先观察感兴趣的区域,使用 GRPO 算法训练。

概述

DeepEyes 是一个多轮交互式视觉问答环境,模型可以调用图像操作工具(image_zoom_in_toolimage_rotate_tool)来检查感兴趣的区域,然后产生最终的 <answer>...</answer>。这是 Relax Agentic RL 能力的典型示例:

  • 多轮采样与 loss mask — 模型输出(mask=1)与环境观测(mask=0)分离,只有模型动作参与训练。
  • 工具使用环境DeepeyesEnv 解析 <tool_call> 块,执行图像变换,返回更新后的图像作为 <tool_response>
  • VLM 多模态上下文累积 — 每轮工具交互产生的图像被逐步合并到跨轮的视觉上下文中。
  • Judge 奖励 — LLM judge 对答案准确性打分,结合格式和工具使用奖励。

数据准备

DeepEyes 使用 HuggingFace 上的 Visual-Agent/DeepEyes 数据集。数据集包含视觉问答样本,图像以 HF Image 格式内嵌存储。

下载数据集

bash
# 使用 huggingface-cli
hf download --repo-type dataset ChenShawn/DeepEyes-Datasets-47k \
  --local-dir /root/deepeyes-v1

# 或使用 Python
from datasets import load_dataset
ds = load_dataset("ChenShawn/DeepEyes-Datasets-47k")
ds.save_to_disk("/root/deepeyes-v1")

下载后会产生 parquet 文件。训练使用 data_0.1.2_visual_toolbox_v2.parquet(22,362 个样本,来自 V*-Bench 数据源)。

数据集格式

每个样本包含以下字段:

列名类型描述
promptlist[dict]聊天消息(system + user),user 内容中包含 <image> 占位符
imageslist[dict]HF Image 格式:{"bytes": b"...", "path": "..."}
reward_modeldict{"ground_truth": "...", "style": "model"}
extra_infodict{"answer": "...", "question": "...", "index": "...", "split": "train"}
data_sourcestr数据来源标识(如 "vstar"
abilitystr能力分类
env_namestr环境名称

HF Image dict 格式({"bytes": ...})被 Relax 的图像加载管线原生支持,无需转换。

下载模型

bash
hf download Qwen/Qwen3-VL-30B-A3B-Thinking \
  --local-dir /root/Qwen3-VL-30B-A3B-Thinking

快速开始

30B-A3B 模型(8 GPU,MoE)

完整配置需要 judge 模型进行奖励评分:

bash
export MODEL_DIR=/root
export DATA_DIR=/root
export SAVE_DIR=/root/save

# 设置 judge 模型 API(完整奖励评分所需)
# 或执行 sglang_judge_service.sh 实现本地部署
export DEEPEYES_JUDGE_API_KEY=your-api-key
export DEEPEYES_JUDGE_BASE_URL=http://your-judge-endpoint/v1

cd /root/Relax
bash examples/deepeyes/run_deepeyes.sh

远程集群(通过 Ray Job)

bash
WORKING_DIR="./" RAY_ADDRESS=<RAY_HEAD_IP>:6379 \
  MODEL_DIR=/root DATA_DIR=/root SAVE_DIR=/root/save \
  bash -x scripts/entrypoint/ray-job.sh examples/deepeyes/run_deepeyes.sh

架构

文件结构

examples/deepeyes/
├── run_deepeyes.sh            # 启动脚本(Qwen3-VL-30B-A3B,完整配置)
├── deepeyes_config.yaml       # 任务配置(max_turns、环境路径)
├── rollout.py                 # 多轮 rollout 逻辑
├── env_deepeyes.py            # DeepEyes 工具使用环境
├── base_env.py                # BaseInteractionEnv 接口
├── reward_deepeyes.py         # 基于 Judge 的奖励函数
└── sglang_judge_service.sh    # Judge 模型的 SGLang 服务

多轮 Rollout

自定义 rollout 函数(rollout.py:generate)实现多轮交互循环:

  1. 初始化:加载环境,编码初始图像,准备采样参数
  2. 推理步骤:将 token + 图像发送到 SGLang 引擎,获取模型响应
  3. 环境步骤:从响应中解析工具调用,执行图像变换,返回观测
  4. 追加与掩码:模型输出 token 设置 loss_mask=1,观测 token 设置 loss_mask=0
  5. 重复直到出现 <answer> 标签、达到最大轮数或 token 预算耗尽

每轮的状态(图像数据、多模态输入)被累积以实现正确的 VLM 上下文传递。

环境

DeepEyes 环境定义在 env_deepeyes.py 中:

python
class DeepeyesEnv(BaseInteractionEnv):
    """Environment for Deepeyes with zoom in and rotate tools."""

    MIN_DIMENSION = 28

    def __init__(self, *, max_turns: int | None = None, image=None):
        self.max_turns = max_turns
        self.turn = 0
        self.tool_calls: list[dict[str, Any]] = []
        self.current_image = image
        self.origin_image = image

    def reset(self):
        self.turn = 0
        self.tool_calls.clear()
        observation: dict[str, Any] = {}
        reset_info = {"has_image": self.current_image is not None}
        return observation, reset_info

    def step(self, response_text: str):
        """Process agent response and return observation, done flag, and info."""
        self.turn += 1
        # Check if answer is provided
        if ANSWER_RE.search(response_text):
            return self._build_obs_text(text="Answer received."), True, {"final_answer": True}

        # Extract and execute tool call
        tool_call = self._extract_tool_call(response_text)
        if not tool_call:
            obs = self._build_obs_text(text="No tool call detected; ending the episode.")
            return obs, True, {"tool_executed": False}

        obs, done, info = self._apply_tool(tool_call)
        # Check if max turns reached
        if self.max_turns is not None and self.turn >= self.max_turns:
            done = True
        return obs, done, info

核心行为:

  • reset() — 初始化轮次计数器和工具调用历史
  • step(response_text) — 从模型输出中解析 <tool_call> / <answer>
    • 如果找到 <answer> → 回合结束
    • 如果 <tool_call> 包含 image_zoom_in_tool → 裁剪图像到边界框
    • 如果 <tool_call> 包含 image_rotate_tool → 按角度旋转图像
    • 如果没有识别到标签 → 回合结束(未检测到工具)
  • _build_obs_text() — 构建包含文本和可选图像的观测字典,用于 <tool_response>

环境通过 build_env(sample, args) 实例化,从 sample.multimodal_inputs 中提取初始图像。

奖励函数

奖励函数定义在 reward_deepeyes.py 中:

python
def compute_score(predict_str: str, ground_truth: str, extra_info: dict | None = None) -> dict:
    # 1. 格式检查:验证 <think>、</think>、<answer>、</answer> 标签
    # 2. 如果格式错误 → acc_reward=0,跳过 judge
    # 3. 如果格式正确 → 通过 OpenAI 兼容 API 调用 LLM judge
    # 4. 综合各信号:
    tool_reward = 1.0 if count_vision_1 > 0 and acc_reward > 0.5 else 0.0
    format_reward = -1.0 if is_format_error else 0.0
    final_score = 0.8 * acc_reward + 0.2 * format_reward + 1.2 * tool_reward
    return {"score": final_score, "acc": acc_reward, "format": format_reward, "tool": tool_reward, ...}


async def reward_func(args, sample: Sample, **kwargs):
    question = sample.metadata.get("question")
    ground_truth = sample.metadata.get("answer")
    return await asyncio.to_thread(compute_score, sample.response, ground_truth, {"question": question})

奖励综合三个信号:

final_score = 0.8 × acc_reward + 0.2 × format_reward + 1.2 × tool_reward
组成部分取值条件
acc_reward0.0 或 1.0LLM judge 将提取的 <answer> 与 ground truth 比较
format_reward-1.0 或 0.0如果 <think>/</think>/<answer>/</answer> 标签格式错误则为 -1.0
tool_reward0.0 或 1.0如果模型使用了工具且答案正确则为 1.0

没有 judge 模型时:如果 DEEPEYES_JUDGE_API_KEY 未配置(或指向不可达的端点),所有格式错误的样本获得 acc_reward=0,格式正确的样本会尝试调用 judge(调用失败后默认 acc_reward=0)。训练仍会继续,但奖励信号退化——仅格式正确性和工具使用会被奖励。

配置

任务配置(deepeyes_config.yaml

yaml
max_turns: 10                                          # 每个回合的最大交互轮数
rollout_interaction_env_path: examples.deepeyes.env_deepeyes  # 环境模块路径

关键启动脚本参数

参数4B 脚本30B 脚本描述
模型qwen3-vl-4B.shqwen3-vl-30B-A3B.sh模型架构配置
--tensor-model-parallel-size44张量并行度
--expert-model-parallel-size18专家并行度(MoE)
--rollout-batch-size1632Rollout 批大小
--n-samples-per-prompt48GRPO 每个 prompt 的采样数
--global-batch-size128256训练批大小
--rollout-max-response-len10242048每轮最大响应 token 数
--max-tokens-per-gpu81928192动态 batching 每 GPU 最大 token 数
--colocateActor 和 Rollout 共享 GPU
--max-staleness00严格 on-policy 训练

Judge 模型配置

奖励函数使用 OpenAI 兼容的 API 进行 LLM-as-judge 评分:

环境变量默认值描述
DEEPEYES_JUDGE_API_KEY(必需)Judge 端点的 API 密钥
DEEPEYES_JUDGE_BASE_URL(可选)Judge API 的 Base URL
DEEPEYES_JUDGE_MODELSgpt-4o逗号分隔的 judge 模型名称列表
DEEPEYES_JUDGE_TIMEOUT120Judge API 调用超时(秒)

可以使用本地 SGLang 服务器作为 judge:

bash
# 启动 judge 模型服务
bash examples/deepeyes/sglang_judge_service.sh

故障排除

内存不足

减小启动脚本中的批大小:

bash
--global-batch-size 64       # 从 128 减小
--rollout-batch-size 8       # 从 16 减小
--max-tokens-per-gpu 4096    # 从 8192 减小

Judge API 错误

如果 judge 模型不可达,奖励函数会重试 3 次然后将 acc_reward 设为 0。训练会继续,但只有格式奖励。请检查:

  1. DEEPEYES_JUDGE_API_KEYDEEPEYES_JUDGE_BASE_URL 设置正确
  2. Judge 端点可从所有 Ray worker 节点访问
  3. DEEPEYES_JUDGE_MODELS 中的模型名称与部署的模型匹配

响应中没有工具调用

如果模型始终不产生 <tool_call> 标签,回合会立即终止。这在训练早期是正常的——模型通过 RL 学习工具使用。请确保:

  1. 数据中的 system prompt 包含工具使用说明
  2. deepeyes_config.yamlmax_turns > 1
  3. Rollout 温度足够高以支持探索(默认:1.0)

基于 Apache 2.0 许可发布