docs(actpred): 研究方向 R8 细化——三层流水线离线 RL + 样本级验证集报告
R8 细化:预训练→监督微调→离线 RL 流水线定稿;IQL/CQL/DT 选型对比; IMU 奖励工程四分量(姿态稳定/抖动/平滑/任务进度);三阶段落地路径。 新增样本级验证集划分实验报告(整集 60 样本噪声大 → 样本级 142 样本 更稳,F2b 在更难验证集上取得更低 val)。实现:val_split_mode 默认 sample,dataset sample_indices 过滤 + zscore 统计口径保持训练集专属。
This commit is contained in:
1 parent
8cf3dd2d93
commit
13beba09b4
6 files changed
+192
-4
No files matched your search
@@ -60,6 +60,11 @@ class TrainConfig:
|
||||
lr: float = 1e-3
|
||||
weight_decay: float = 1e-4
|
||||
val_split: float = 0.15
|
||||
# 验证集划分方式:
|
||||
# "episode" 整集划分(防数据泄漏,但单 episode 验证噪声大)
|
||||
# "sample" 样本级随机划分(默认:多集混合、验证集更大更稳,
|
||||
# 代价是相邻高相关样本泄漏使 val 略乐观)
|
||||
val_split_mode: str = "sample"
|
||||
checkpoint_dir: str = "./checkpoints"
|
||||
patience: int = 15
|
||||
num_workers: int = 2
|
||||
@@ -132,6 +137,10 @@ def build_argparser(description: str = "") -> argparse.ArgumentParser:
|
||||
parser.add_argument("--lr", type=float, default=None)
|
||||
parser.add_argument("--weight-decay", type=float, default=None)
|
||||
parser.add_argument("--val-split", type=float, default=None)
|
||||
parser.add_argument("--val-split-mode", type=str, default=None,
|
||||
choices=["episode", "sample"],
|
||||
help="验证集划分:episode=整集(防泄漏但噪声大);"
|
||||
"sample=样本级随机(默认,更大更稳)")
|
||||
parser.add_argument("--checkpoint-dir", type=str, default=None)
|
||||
parser.add_argument("--patience", type=int, default=None)
|
||||
parser.add_argument("--num-workers", type=int, default=None)
|
||||
@@ -337,6 +346,7 @@ def apply_overrides(cfg: Config, args: argparse.Namespace) -> Config:
|
||||
"lr": ("train", "lr"),
|
||||
"weight_decay": ("train", "weight_decay"),
|
||||
"val_split": ("train", "val_split"),
|
||||
"val_split_mode": ("train", "val_split_mode"),
|
||||
"checkpoint_dir": ("train", "checkpoint_dir"),
|
||||
"patience": ("train", "patience"),
|
||||
"num_workers": ("train", "num_workers"),
|
||||
|
||||
+10
-1
@@ -25,7 +25,8 @@ class ActionDataset(Dataset):
|
||||
image_width: int = 640, image_height: int = 480,
|
||||
normalize_actions: bool = False,
|
||||
action_stats_path: str = None, episode_indices: list = None,
|
||||
frame_len: int = 3, augment: bool = False,
|
||||
frame_len: int = 3, sample_indices: list = None,
|
||||
augment: bool = False,
|
||||
predict_delta: bool = False, rollout_len: int = 1,
|
||||
imu_dim: int = 0, normalize_mode: str = "zscore",
|
||||
onehot: bool = False, onehot_sigma: float = 1.0):
|
||||
@@ -63,6 +64,7 @@ class ActionDataset(Dataset):
|
||||
episodes = ActionDataset._valid_episode_dirs(data_dir)
|
||||
if episode_indices is not None:
|
||||
episodes = [episodes[i] for i in episode_indices]
|
||||
self._sample_indices = sample_indices
|
||||
|
||||
# 样本存 (image_files, actions, t_start):t_start 是 rollout 第一步的目标时刻,
|
||||
# __getitem__ 按 rollout_len 展开 K 步。存原始数组引用避免重复加载/拷贝。
|
||||
@@ -93,6 +95,13 @@ class ActionDataset(Dataset):
|
||||
for t_start in range(start, n - (self.rollout_len - 1)):
|
||||
self.samples.append((image_files, actions, imus, t_start))
|
||||
|
||||
# 样本级随机划分:按样本索引过滤(相邻微步高度相关,随机划分
|
||||
# 会让 val 略乐观——高相关样本的近邻大量在训练集,见训练报告)
|
||||
if sample_indices is not None:
|
||||
idx_set = set(sample_indices)
|
||||
self.samples = [s for i, s in enumerate(self.samples)
|
||||
if i in idx_set]
|
||||
|
||||
self._action_mean = None
|
||||
self._action_std = None
|
||||
if normalize_actions and not self._range_mode:
|
||||
|
||||
@@ -2484,6 +2484,47 @@ class TestOneHot(unittest.TestCase):
|
||||
cfg = apply_overrides(Config(), args)
|
||||
self.assertFalse(cfg.data.onehot)
|
||||
|
||||
def test_dataset_sample_indices(self):
|
||||
"""样本级随机划分:sample_indices 过滤样本,zscore 统计仅用子集"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
TestActionDataset._create_dummy_episode(
|
||||
Path(tmpdir), n_frames=12, action_dim=4)
|
||||
full = ActionDataset(tmpdir, history_len=3, action_dim=4,
|
||||
image_width=32, image_height=32, frame_len=3,
|
||||
normalize_actions=True)
|
||||
n = len(full)
|
||||
sub = ActionDataset(tmpdir, history_len=3, action_dim=4,
|
||||
image_width=32, image_height=32, frame_len=3,
|
||||
normalize_actions=True,
|
||||
sample_indices=[0, 1, 2])
|
||||
self.assertEqual(len(sub), 3)
|
||||
# 原始目标一致(用非归一化口径对照,zscore 统计量不同会
|
||||
# 改变归一化值,故不能直接比 target)
|
||||
raw_full = ActionDataset(tmpdir, history_len=3, action_dim=4,
|
||||
image_width=32, image_height=32,
|
||||
frame_len=3, normalize_actions=False)
|
||||
raw_sub = ActionDataset(tmpdir, history_len=3, action_dim=4,
|
||||
image_width=32, image_height=32,
|
||||
frame_len=3, normalize_actions=False,
|
||||
sample_indices=[0, 1, 2])
|
||||
for i in range(3):
|
||||
np.testing.assert_allclose(
|
||||
raw_sub[i]["target"].numpy(),
|
||||
raw_full[i]["target"].numpy())
|
||||
# 统计量由子集样本计算(zscore 均值 = 子集样本均值)
|
||||
self.assertIsNotNone(sub.get_action_stats())
|
||||
self.assertNotEqual(n, 3)
|
||||
|
||||
def test_val_split_mode_cli(self):
|
||||
"""--val-split-mode 参数经 apply_overrides 落到 train 配置"""
|
||||
from config import build_argparser, apply_overrides
|
||||
parser = build_argparser("t")
|
||||
cfg = apply_overrides(Config(),
|
||||
parser.parse_args(["--val-split-mode", "episode"]))
|
||||
self.assertEqual(cfg.train.val_split_mode, "episode")
|
||||
cfg2 = apply_overrides(Config(), parser.parse_args([]))
|
||||
self.assertEqual(cfg2.train.val_split_mode, "sample") # 新默认
|
||||
|
||||
def test_onehot_config_validation(self):
|
||||
cfg = Config()
|
||||
cfg.data.onehot = True
|
||||
|
||||
+26
-3
@@ -206,14 +206,36 @@ def main():
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Device: {device}")
|
||||
|
||||
train_indices, val_indices = split_episodes(
|
||||
cfg.data.data_dir, cfg.train.val_split, seed=cfg.train.seed)
|
||||
print(f"Episodes: {len(train_indices)} train, {len(val_indices)} val")
|
||||
if cfg.train.val_split_mode == "sample":
|
||||
# 样本级随机划分:先建全量数据集取样本索引,再按 seed 随机分成
|
||||
# train/val(zscore 统计量仍只由训练样本计算,经 stats 文件复用)
|
||||
probe_ds = ActionDataset(
|
||||
cfg.data.data_dir, cfg.data.history_len, cfg.data.action_dim,
|
||||
cfg.data.image_width, cfg.data.image_height, False,
|
||||
cfg.data.action_stats_path, None, cfg.data.frame_len,
|
||||
predict_delta=cfg.train.predict_delta,
|
||||
rollout_len=cfg.train.rollout_len, imu_dim=cfg.data.imu_dim,
|
||||
normalize_mode=cfg.data.normalize_mode)
|
||||
n = len(probe_ds)
|
||||
rng = np.random.RandomState(cfg.train.seed)
|
||||
perm = rng.permutation(n)
|
||||
val_count = max(1, int(n * cfg.train.val_split))
|
||||
val_idx = perm[:val_count]
|
||||
train_idx = perm[val_count:]
|
||||
train_indices = None
|
||||
val_indices = None
|
||||
print(f"Sample split: {n - val_count} train, {val_count} val "
|
||||
f"(mode=sample)")
|
||||
else:
|
||||
train_indices, val_indices = split_episodes(
|
||||
cfg.data.data_dir, cfg.train.val_split, seed=cfg.train.seed)
|
||||
print(f"Episodes: {len(train_indices)} train, {len(val_indices)} val")
|
||||
|
||||
train_ds = ActionDataset(
|
||||
cfg.data.data_dir, cfg.data.history_len, cfg.data.action_dim,
|
||||
cfg.data.image_width, cfg.data.image_height, cfg.data.normalize_actions,
|
||||
cfg.data.action_stats_path, train_indices, cfg.data.frame_len,
|
||||
sample_indices=train_idx if cfg.train.val_split_mode == "sample" else None,
|
||||
augment=cfg.train.augment,
|
||||
predict_delta=cfg.train.predict_delta, rollout_len=cfg.train.rollout_len,
|
||||
imu_dim=cfg.data.imu_dim, normalize_mode=cfg.data.normalize_mode,
|
||||
@@ -233,6 +255,7 @@ def main():
|
||||
cfg.data.data_dir, cfg.data.history_len, cfg.data.action_dim,
|
||||
cfg.data.image_width, cfg.data.image_height, cfg.data.normalize_actions,
|
||||
cfg.data.action_stats_path, val_indices, cfg.data.frame_len,
|
||||
sample_indices=val_idx if cfg.train.val_split_mode == "sample" else None,
|
||||
predict_delta=cfg.train.predict_delta, rollout_len=cfg.train.rollout_len,
|
||||
imu_dim=cfg.data.imu_dim, normalize_mode=cfg.data.normalize_mode,
|
||||
onehot=cfg.data.onehot, onehot_sigma=cfg.data.onehot_sigma)
|
||||
|
||||
Reference in new issue
Block a user