Files
liqiang b119135836
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
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

174 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""DPO 训练脚本(实验 8-17 主线,需要单卡 GPU)。
默认配置面向单卡:bf16、gradient checkpointing、LoRA r=16 alpha=32、
per_device_batch_size=1、gradient_accumulation 2、learning_rate 3e-5、
beta 0.1、4 epochs。小数据集使用较小的累积步数,确保确实有足够的更新步;可用
`--epochs`、`--gradient-accumulation` 和 `--learning-rate` 覆盖。训练产物:output/adapter/(仅 LoRA adapter),
训练回执 validation/<run>/training_receipt.json(配置、数据哈希、时间戳)。
--smoke 模式只做数据加载、tokenizer、模型前向的一次性检查,不训练,
用于无 GPU 环境验证脚本完整性(仍需能下载一个小模型,见 --smoke-model)。
"""
from __future__ import annotations
import argparse
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent
DATA_PATH = ROOT / "output" / "preference_pairs.jsonl"
ADAPTER_DIR = ROOT / "output" / "adapter"
def sha256_file(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_pairs(path: Path):
"""加载 DPO 偏好对为 datasets.Datasetprompt/chosen/rejected 三列)。"""
from datasets import Dataset
rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
if not rows:
raise SystemExit(f"偏好对为空:{path};请先运行 python build_preference_data.py")
return Dataset.from_list([{k: r[k] for k in ("prompt", "chosen", "rejected")} for r in rows])
def smoke(args) -> None:
"""一次性完整性检查:数据加载 + tokenizer + 模型前向,不做训练。"""
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
dataset = load_pairs(Path(args.data))
print(f"数据加载 OK{len(dataset)} 条偏好对,列 = {dataset.column_names}")
tokenizer = AutoTokenizer.from_pretrained(args.smoke_model)
text = dataset[0]["prompt"] + dataset[0]["chosen"]
inputs = tokenizer(text, return_tensors="pt")
print(f"tokenizer OK:首条样本 {inputs['input_ids'].shape[-1]} tokens")
model = AutoModelForCausalLM.from_pretrained(args.smoke_model)
model.eval()
with torch.no_grad():
outputs = model(**inputs)
print(f"前向 OKlogits shape = {tuple(outputs.logits.shape)}")
print("smoke 检查通过(未训练)。")
def train(args) -> None:
import torch
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import DPOConfig, DPOTrainer
data_path = Path(args.data)
dataset = load_pairs(data_path)
started = datetime.now(timezone.utc)
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,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
)
training_args = DPOConfig(
output_dir=str(args.output_dir),
beta=0.1,
learning_rate=args.learning_rate,
num_train_epochs=args.epochs,
per_device_train_batch_size=1,
gradient_accumulation_steps=args.gradient_accumulation,
gradient_checkpointing=True,
bf16=True,
logging_steps=1,
save_strategy="no",
report_to=[],
max_length=1536,
seed=args.seed,
)
trainer = DPOTrainer(
model=model,
args=training_args,
train_dataset=dataset,
processing_class=tokenizer,
peft_config=peft_config,
)
trainer.train()
adapter_dir = Path(args.output_dir)
adapter_dir.mkdir(parents=True, exist_ok=True)
trainer.save_model(str(adapter_dir))
finished = datetime.now(timezone.utc)
run = started.strftime("train_%Y%m%dT%H%M%SZ")
receipt_dir = ROOT / "validation" / run
receipt_dir.mkdir(parents=True, exist_ok=True)
receipt = {
"experiment": "8-17 premature-completion-dpo",
"run": run,
"model": args.model,
"data": str(data_path.relative_to(ROOT)),
"data_sha256": sha256_file(data_path),
"pair_count": len(dataset),
"config": {
"beta": 0.1, "learning_rate": args.learning_rate, "epochs": args.epochs,
"lora_r": 16, "lora_alpha": 32,
"per_device_batch_size": 1, "gradient_accumulation": args.gradient_accumulation,
"bf16": True, "gradient_checkpointing": True, "seed": args.seed,
},
"started_at": started.isoformat(),
"finished_at": finished.isoformat(),
"adapter_dir": str(adapter_dir.relative_to(ROOT)),
"training_loss": next(
(entry.get("loss") for entry in reversed(trainer.state.log_history)
if entry.get("loss") is not None),
None,
),
}
(receipt_dir / "training_receipt.json").write_text(
json.dumps(receipt, ensure_ascii=False, indent=2), encoding="utf-8"
)
(ROOT / "validation" / "latest.json").write_text(
json.dumps({"run": run, "training_receipt": f"validation/{run}/training_receipt.json"},
ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"adapter 已保存到 {adapter_dir};训练回执 -> {receipt_dir / 'training_receipt.json'}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model", default="Qwen/Qwen2.5-7B-Instruct", help="基座模型(可用 --model 覆盖)")
parser.add_argument("--data", default=str(DATA_PATH), help="preference_pairs.jsonl 路径")
parser.add_argument("--output-dir", default=str(ADAPTER_DIR), help="adapter 输出目录")
parser.add_argument("--seed", type=int, default=717)
parser.add_argument("--epochs", type=float, default=4,
help="训练轮数;小数据演示默认 4 轮,避免只有一两个更新步")
parser.add_argument("--gradient-accumulation", type=int, default=2,
help="梯度累积步数;24 条演示数据默认设为 2")
parser.add_argument("--learning-rate", type=float, default=3e-5,
help="LoRA 学习率;小数据演示默认 3e-5")
parser.add_argument("--smoke", action="store_true", help="只做数据/tokenizer/前向检查,不训练")
parser.add_argument("--smoke-model", default="Qwen/Qwen2.5-0.5B-Instruct",
help="smoke 模式用的小模型(CPU 可前向)")
args = parser.parse_args()
if args.smoke:
smoke(args)
else:
train(args)
if __name__ == "__main__":
main()