- train/val 归一化统计量改为仅用训练集计算并落盘复用,修复 val loss 不可信 - predict.py 统计文件路径改用 os.path 处理,修复跨平台与无分隔符路径问题 - dataset.py jpg/png 混合时统一按文件名排序,保证时间顺序 - 新增 --seed 全局随机种子(含 DataLoader generator),训练可复现 - 新增 --max-grad-norm 梯度裁剪(默认 1.0),防 GRU 梯度爆炸 - get_episode_count 与数据集目录过滤条件对齐,避免无效目录导致索引错位 - 空验证集返回 inf 并保底保存最终权重,避免伪造 best 模型 - 清理 export_onnx.py 重复分支死代码
112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
import os
|
|
import numpy as np
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
import torch
|
|
from torch.utils.data import Dataset
|
|
from torchvision import transforms
|
|
|
|
|
|
class ActionDataset(Dataset):
|
|
def __init__(self, data_dir: str, history_len: int, action_dim: int,
|
|
image_size: int = 64, normalize_actions: bool = False,
|
|
action_stats_path: str = None, episode_indices: list = None):
|
|
self.history_len = history_len
|
|
self.action_dim = action_dim
|
|
self.image_size = image_size
|
|
|
|
self.transform = transforms.Compose([
|
|
transforms.Resize((image_size, image_size)),
|
|
transforms.ToTensor(),
|
|
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
|
|
])
|
|
|
|
data_path = Path(data_dir)
|
|
episodes = sorted([d for d in data_path.iterdir() if d.is_dir()])
|
|
if episode_indices is not None:
|
|
episodes = [episodes[i] for i in episode_indices]
|
|
|
|
self.samples = []
|
|
for ep_dir in episodes:
|
|
actions_path = ep_dir / "actions.npy"
|
|
images_dir = ep_dir / "images"
|
|
if not actions_path.exists() or not images_dir.is_dir():
|
|
continue
|
|
actions = np.load(actions_path).astype(np.float32)
|
|
n_frames = len(actions)
|
|
image_files = sorted(list(images_dir.glob("*.jpg")) + list(images_dir.glob("*.png")),
|
|
key=lambda p: p.name)
|
|
n_images = len(image_files)
|
|
n = min(n_frames, n_images)
|
|
for t in range(history_len, n):
|
|
self.samples.append((image_files[t], actions[t - history_len:t], actions[t]))
|
|
|
|
self._action_mean = None
|
|
self._action_std = None
|
|
if normalize_actions:
|
|
if action_stats_path and os.path.exists(action_stats_path):
|
|
stats = np.load(action_stats_path)
|
|
self._action_mean = stats[0]
|
|
self._action_std = stats[1]
|
|
else:
|
|
all_actions = np.stack([s[2] for s in self.samples])
|
|
self._action_mean = all_actions.mean(axis=0)
|
|
self._action_std = all_actions.std(axis=0) + 1e-8
|
|
|
|
def __len__(self):
|
|
return len(self.samples)
|
|
|
|
def __getitem__(self, idx):
|
|
image_path, action_history, target = self.samples[idx]
|
|
image = Image.open(image_path).convert("RGB")
|
|
image = self.transform(image)
|
|
action_history = torch.tensor(action_history, dtype=torch.float32)
|
|
target = torch.tensor(target, dtype=torch.float32)
|
|
if self._action_mean is not None:
|
|
action_history = (action_history - self._action_mean) / self._action_std
|
|
target = (target - self._action_mean) / self._action_std
|
|
return {"image": image, "action_history": action_history, "target": target}
|
|
|
|
def get_action_stats(self):
|
|
if self._action_mean is not None:
|
|
return np.stack([self._action_mean, self._action_std])
|
|
return None
|
|
|
|
@staticmethod
|
|
def _valid_episode_dirs(data_dir: str) -> list:
|
|
data_path = Path(data_dir)
|
|
return sorted([d for d in data_path.iterdir()
|
|
if d.is_dir() and (d / "actions.npy").exists() and (d / "images").is_dir()])
|
|
|
|
@staticmethod
|
|
def get_episode_count(data_dir: str) -> int:
|
|
# 与 __init__ 的过滤条件保持一致,避免 split_episodes 索引错位
|
|
return len(ActionDataset._valid_episode_dirs(data_dir))
|
|
|
|
|
|
class DataRecorder:
|
|
def __init__(self, output_dir: str, action_dim: int):
|
|
self.output_dir = Path(output_dir)
|
|
self.action_dim = action_dim
|
|
self._frame_idx = 0
|
|
self._actions = []
|
|
ep_idx = 0
|
|
while (self.output_dir / f"episode_{ep_idx:04d}").exists():
|
|
ep_idx += 1
|
|
self.episode_dir = self.output_dir / f"episode_{ep_idx:04d}"
|
|
self.images_dir = self.episode_dir / "images"
|
|
self.images_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def record(self, image: np.ndarray, action: np.ndarray):
|
|
img = Image.fromarray(image)
|
|
img.save(self.images_dir / f"{self._frame_idx:04d}.jpg", quality=90)
|
|
self._actions.append(action.astype(np.float32))
|
|
self._frame_idx += 1
|
|
|
|
def flush(self):
|
|
if self._actions:
|
|
actions = np.array(self._actions, dtype=np.float32)
|
|
np.save(self.episode_dir / "actions.npy", actions)
|
|
return str(self.episode_dir)
|