File size: 9,074 Bytes
ec0a9aa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | """
Classify GR1_robot episodes as 'markovian' or 'non_markovian' using
Qwen3-VL-30B-A3B-Instruct-FP8 served via vLLM (text-only, task strings).
Reads episodes.jsonl from GR1_robot dataset, classifies unique task strings,
then maps back to episodes.
Usage:
python scripts/classify_markovian_gr1robot.py
python scripts/classify_markovian_gr1robot.py \
--episodes-jsonl datasets/humanoid/singleview/PhysicalAI-Robotics-GR00T-Teleop-GR1/GR1_robot/meta/episodes.jsonl \
--cache-file scripts/gr1robot_task_labels_cache.jsonl \
--output-csv scripts/gr1robot_task_labels.csv \
--base-url http://localhost:8000/v1 \
--model Qwen/Qwen3-VL-30B-A3B-Instruct-FP8 \
--batch-size 50 \
--max-retries 2
"""
import argparse
import json
import re
import sys
import time
from pathlib import Path
import requests
import pandas as pd
from tqdm import tqdm
# ---------------------------------------------------------------------------
# Prompt (same rules as gr1.py)
# ---------------------------------------------------------------------------
SYSTEM_PROMPT = """You are a robot manipulation task classifier.
Classify each task description as exactly one of:
- "markovian": a SINGLE atomic manipulation action. The robot only needs to observe the current state to act. No memory of previous steps is required.
Typical examples: "pick up red apple to silver pan", "place cup on table", "push ball left", "open drawer", "remove block from stack"
- "non_markovian": involves MULTIPLE sequential steps, OR requires the robot to remember what it has already done, OR involves ongoing/continuous actions.
Typical examples: "pick up X then place it into Y while holding Z", "stir repeatedly", "sort all blocks", "spray water onto plants", "wipe table surface"
Rules:
- Connectors like "then", "while", "after", "followed by", "and" (joining two actions) → non_markovian
- Ongoing / repetitive actions (stir, spray, wipe, fold, rotate, pour) → non_markovian
- Both arms doing different simultaneous things → non_markovian
- Simple single pick-and-place, push, remove, open/close → markovian
- "unpick up and place X to Y" = pick-and-place = markovian
Reply with ONLY a valid JSON array (no markdown fences, no extra text):
[{"task_index": <int>, "label": "markovian"|"non_markovian", "reason": "<≤12 words>"}]"""
USER_TEMPLATE = "Classify these tasks:\n{tasks_json}"
def clean_prompt(text: str) -> str:
text = text.replace("locked waist: ", "").replace("grid_", "")
text = text.replace("_", " ").strip()
return text
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def load_unique_tasks(episodes_jsonl: Path) -> list[dict]:
"""Return unique cleaned tasks with their first-seen episode index as task_index."""
seen: dict[str, int] = {} # task_text -> synthetic task_index
with open(episodes_jsonl) as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
for raw in obj.get("tasks", []):
text = clean_prompt(raw.strip())
if text and text not in seen:
seen[text] = len(seen)
return [{"task_index": idx, "task": text} for text, idx in seen.items()]
def load_cache(cache_file: Path) -> dict[int, dict]:
cache: dict[int, dict] = {}
if not cache_file.exists():
return cache
with open(cache_file) as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
cache[obj["task_index"]] = obj
return cache
def append_to_cache(cache_file: Path, results: list[dict]) -> None:
with open(cache_file, "a") as f:
for r in results:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
def call_llm(base_url: str, model: str, batch: list[dict], max_retries: int) -> list[dict]:
tasks_json = json.dumps(batch, ensure_ascii=False)
user_msg = USER_TEMPLATE.format(tasks_json=tasks_json)
payload = {
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
],
"temperature": 0.0,
"chat_template_kwargs": {"enable_thinking": False},
}
for attempt in range(1, max_retries + 2):
try:
resp = requests.post(
f"{base_url}/chat/completions", json=payload, timeout=120
)
resp.raise_for_status()
raw = resp.json()["choices"][0]["message"]["content"].strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
raw = raw.strip()
parsed: list[dict] = json.loads(raw)
index_map = {t["task_index"]: t["task"] for t in batch}
results = []
for item in parsed:
ti = int(item["task_index"])
label = item.get("label", "").strip().lower()
if label not in ("markovian", "non_markovian"):
label = "parse_error"
results.append({
"task_index": ti,
"task": index_map.get(ti, ""),
"label": label,
"reason": item.get("reason", ""),
})
return results
except (json.JSONDecodeError, KeyError, TypeError) as e:
if attempt <= max_retries:
time.sleep(2)
else:
print(f" [warn] Parse failed: {e}", file=sys.stderr)
return [
{"task_index": t["task_index"], "task": t["task"],
"label": "parse_error", "reason": str(e)}
for t in batch
]
except requests.RequestException as e:
if attempt <= max_retries:
time.sleep(5)
else:
print(f" [error] HTTP error: {e}", file=sys.stderr)
return [
{"task_index": t["task_index"], "task": t["task"],
"label": "api_error", "reason": str(e)}
for t in batch
]
return []
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Classify GR1_robot tasks via Qwen3-VL")
parser.add_argument(
"--episodes-jsonl", type=Path,
default=Path("datasets/humanoid/singleview/PhysicalAI-Robotics-GR00T-Teleop-GR1/GR1_robot/meta/episodes.jsonl"),
)
parser.add_argument("--cache-file", type=Path, default=Path("scripts/gr1robot_task_labels_cache.jsonl"))
parser.add_argument("--output-csv", type=Path, default=Path("scripts/gr1robot_task_labels.csv"))
parser.add_argument("--base-url", type=str, default="http://localhost:8000/v1")
parser.add_argument("--model", type=str, default="Qwen/Qwen3-VL-30B-A3B-Instruct-FP8")
parser.add_argument("--batch-size", type=int, default=50)
parser.add_argument("--max-retries", type=int, default=2)
args = parser.parse_args()
workspace = Path(__file__).parent.parent
def resolve(p: Path) -> Path:
return workspace / p if not p.is_absolute() else p
episodes_jsonl = resolve(args.episodes_jsonl)
cache_file = resolve(args.cache_file)
output_csv = resolve(args.output_csv)
cache_file.parent.mkdir(parents=True, exist_ok=True)
print(f"[init] Loading unique tasks from {episodes_jsonl}")
all_tasks = load_unique_tasks(episodes_jsonl)
print(f"[init] Unique tasks: {len(all_tasks)}")
cache = load_cache(cache_file)
print(f"[init] Already classified: {len(cache)}")
pending = [t for t in all_tasks if t["task_index"] not in cache]
print(f"[init] Remaining to classify: {len(pending)}")
if pending:
batches = [pending[i: i + args.batch_size] for i in range(0, len(pending), args.batch_size)]
print(f"[run] {len(batches)} batches × {args.batch_size}, model={args.model}")
for batch in tqdm(batches, desc="Classifying", unit="batch"):
results = call_llm(args.base_url, args.model, batch, args.max_retries)
append_to_cache(cache_file, results)
for r in results:
cache[r["task_index"]] = r
rows = list(cache.values())
df = pd.DataFrame(rows, columns=["task_index", "task", "label", "reason"])
df = df.sort_values("task_index").reset_index(drop=True)
df.to_csv(output_csv, index=False)
print(f"\n[saved] {output_csv} ({len(df)} rows)")
print(df["label"].value_counts().to_string())
if __name__ == "__main__":
main()
|