Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""可选 RL 分支:GRPO + 隐藏验收测试作为奖励(实验 8-17 可选路径,主线是 DPO)。
|
|
|
|
奖励函数 = 隐藏验收测试:对每个端到端任务,模型输出若宣称完成,则在隔离的
|
|
临时目录里还原工作区并运行该任务附带的隐藏检查脚本:
|
|
- 宣称完成且隐藏测试通过:+1
|
|
- 宣称完成但测试不过:-1
|
|
- 未宣称完成但执行了验证动作:+0.3
|
|
- 其它:0
|
|
|
|
隐藏测试定义在 data/hidden_tests.json。脚本真实可运行,但属于可选分支:
|
|
正文以 DPO 为主线,GRPO 路径需要 GPU 且训练成本更高。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from evaluate import has_completion_claim, has_verification_action
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
HIDDEN_TESTS_PATH = ROOT / "data" / "hidden_tests.json"
|
|
|
|
REWARD_CLAIM_PASS = 1.0
|
|
REWARD_CLAIM_FAIL = -1.0
|
|
REWARD_VERIFY = 0.3
|
|
|
|
|
|
def load_hidden_tasks(path: Path = HIDDEN_TESTS_PATH) -> list[dict[str, Any]]:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def run_hidden_check(task: dict[str, Any], workdir: Path) -> bool:
|
|
"""在临时目录里还原工作区并运行隐藏检查脚本,返回是否通过。"""
|
|
for rel_path, content in task["workspace_files"].items():
|
|
target = workdir / rel_path
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(content, encoding="utf-8")
|
|
try:
|
|
result = subprocess.run(
|
|
task["hidden_check"], shell=True, cwd=workdir,
|
|
capture_output=True, timeout=60,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return False
|
|
return result.returncode == 0
|
|
|
|
|
|
def hidden_test_reward(completion: str, task: dict[str, Any]) -> float:
|
|
"""单条补全的奖励:宣称完成要看隐藏测试结果,验证动作给小额奖励。"""
|
|
claimed = has_completion_claim(completion)
|
|
if not claimed:
|
|
return REWARD_VERIFY if has_verification_action(completion) else 0.0
|
|
with tempfile.TemporaryDirectory(prefix="grpo-hidden-") as tmp:
|
|
passed = run_hidden_check(task, Path(tmp))
|
|
return REWARD_CLAIM_PASS if passed else REWARD_CLAIM_FAIL
|
|
|
|
|
|
def build_dataset(tasks: list[dict[str, Any]]):
|
|
"""GRPO 数据集:prompt 列给模型,task 列透传给奖励函数。"""
|
|
from datasets import Dataset
|
|
|
|
rows = [{
|
|
"prompt": f"任务:{t['task']}\n\n请完成该任务,并在最后说明你的结论。",
|
|
"task_id": t["id"],
|
|
} for t in tasks]
|
|
return Dataset.from_list(rows)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--model", default="Qwen/Qwen2.5-7B-Instruct")
|
|
parser.add_argument("--output-dir", default=str(ROOT / "output" / "grpo_adapter"))
|
|
parser.add_argument("--seed", type=int, default=717)
|
|
parser.add_argument("--num-generations", type=int, default=8, help="每个 prompt 的采样数")
|
|
args = parser.parse_args()
|
|
|
|
tasks = load_hidden_tasks()
|
|
task_by_id = {t["id"]: t for t in tasks}
|
|
|
|
def reward_func(completions, task_id, **kwargs):
|
|
"""TRL GRPO 奖励回调:dataset 的 task_id 列会作为关键字参数透传进来。"""
|
|
return [hidden_test_reward(c, task_by_id[tid]) for c, tid in zip(completions, task_id)]
|
|
|
|
import torch
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
from peft import LoraConfig
|
|
from trl import GRPOConfig, GRPOTrainer
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained(args.model)
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
args.model, torch_dtype=torch.bfloat16, device_map="auto"
|
|
)
|
|
model.config.use_cache = False
|
|
|
|
peft_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, task_type="CAUSAL_LM")
|
|
config = GRPOConfig(
|
|
output_dir=args.output_dir,
|
|
learning_rate=1e-6,
|
|
per_device_train_batch_size=1,
|
|
gradient_accumulation_steps=16,
|
|
num_generations=args.num_generations,
|
|
max_completion_length=512,
|
|
bf16=True,
|
|
gradient_checkpointing=True,
|
|
num_train_epochs=1,
|
|
logging_steps=1,
|
|
save_strategy="no",
|
|
report_to=[],
|
|
seed=args.seed,
|
|
)
|
|
trainer = GRPOTrainer(
|
|
model=model,
|
|
reward_funcs=reward_func,
|
|
args=config,
|
|
train_dataset=build_dataset(tasks),
|
|
processing_class=tokenizer,
|
|
peft_config=peft_config,
|
|
)
|
|
trainer.train()
|
|
trainer.save_model(args.output_dir)
|
|
print(f"GRPO adapter 已保存到 {args.output_dir}(可选分支产物)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|