feat(actpred): 多头 one-hot 分类输出——18 组独立 softmax 头替代连续回归
分箱(bins.py):舵机 1°/箱 ×10(180 箱,匹配 0.75° 微动作插值粒度)、 spk.F/spk.delay 对数 32 箱 ×2,共 1864 logits;保留位无头。软标签 (onehot_sigma=1.0 高斯扩散)缓解小样本数据稀疏;期望值解码恢复 连续分辨率(箱生活在原始空间,无反变换放大)。 训练损失为逐头加权 CE(列权重沿用),闭环反馈经解码→范围归一化 回写历史;predict 输出仍为 18 维原始量纲,pybot 契约零变化。 结构性消除尺度放大(每箱等距)与物理界问题(箱天然有界), 为 A-E 归一化实验失败后的正解路线。要求 normalize_mode 为范围类, 与 predict_delta 互斥。测试 +10(含 CLI 映射回归)。
This commit is contained in:
1 parent
99c1bc1739
commit
a18a7de1ca
7 files changed
+409
-20
No files matched your search
+71
-17
@@ -8,6 +8,8 @@ from tqdm import tqdm
|
||||
|
||||
from config import (Config, build_argparser, apply_overrides,
|
||||
validate_config_values, warn_init_from_mismatch)
|
||||
from action_norm import normalize_action, is_range_mode
|
||||
from bins import encode as bin_encode, decode as bin_decode, START, HEAD_COLS
|
||||
from model import ActionPredictor
|
||||
from dataset import ActionDataset
|
||||
from action_norm import default_column_weights
|
||||
@@ -48,6 +50,28 @@ def _collate(batch):
|
||||
return default_collate(batch)
|
||||
|
||||
|
||||
def _onehot_ce(logits: torch.Tensor, targets: torch.Tensor,
|
||||
weights: torch.Tensor, action_dim: int) -> torch.Tensor:
|
||||
"""多头加权交叉熵:每组 softmax 与 one-hot(或软标签)求 CE,按列权重加权
|
||||
|
||||
logits/targets: (B, TOTAL_BINS);weights: (action_dim,) 列权重向量。
|
||||
软标签(sigma>0)各组概率和恒 1,CE 定义一致。
|
||||
"""
|
||||
ls = None
|
||||
head_ce = []
|
||||
for c in HEAD_COLS:
|
||||
if c >= action_dim:
|
||||
break
|
||||
s, e = START[c], START[c + 1]
|
||||
# 逐头 log_softmax(不能整行:各头 bin 数不同,且整行 max 会
|
||||
# 把其他头压到 -1e9 量级,CE 爆炸)
|
||||
head_ce.append(-(targets[:, s:e]
|
||||
* torch.log_softmax(logits[:, s:e], dim=-1)).sum(-1))
|
||||
head_ce = torch.stack(head_ce, dim=1) # (B, n_heads)
|
||||
w = weights[:head_ce.size(1)]
|
||||
return (head_ce * w).sum() / (w.sum() * head_ce.size(0))
|
||||
|
||||
|
||||
def _shift_history(history: torch.Tensor, new_action: torch.Tensor) -> torch.Tensor:
|
||||
"""动作历史左移一步、追加 new_action 到末尾(scheduled sampling 回传自预测用)。
|
||||
|
||||
@@ -58,7 +82,9 @@ def _shift_history(history: torch.Tensor, new_action: torch.Tensor) -> torch.Ten
|
||||
|
||||
def train_one_epoch(model, loader, optimizer, device, max_grad_norm: float = None,
|
||||
rollout_len: int = 1, ss_prob: float = 1.0,
|
||||
predict_delta: bool = False, weights: torch.Tensor = None):
|
||||
predict_delta: bool = False, weights: torch.Tensor = None,
|
||||
onehot: bool = False, range_mode: str = None,
|
||||
action_dim: int = 18):
|
||||
model.train()
|
||||
total_loss = 0.0
|
||||
count = 0
|
||||
@@ -71,7 +97,8 @@ def train_one_epoch(model, loader, optimizer, device, max_grad_norm: float = Non
|
||||
target = batch["target"].to(device)
|
||||
imu = batch["imu"].to(device) if "imu" in batch else None
|
||||
pred = model(images, history, imu)
|
||||
loss = _weighted_mse(pred, target, weights)
|
||||
loss = (_onehot_ce(pred, target, weights, action_dim) if onehot
|
||||
else _weighted_mse(pred, target, weights))
|
||||
bs = images.size(0)
|
||||
else:
|
||||
# 多步 scheduled sampling:首步必用真历史;后续步以 ss_prob 用真历史、
|
||||
@@ -89,11 +116,21 @@ def train_one_epoch(model, loader, optimizer, device, max_grad_norm: float = Non
|
||||
history = true_history
|
||||
# 否则沿用上一轮回传的 history(自预测构造)
|
||||
pred = model(images, history, imu)
|
||||
loss = loss + _weighted_mse(pred, target, weights)
|
||||
loss = loss + (_onehot_ce(pred, target, weights, action_dim)
|
||||
if onehot
|
||||
else _weighted_mse(pred, target, weights))
|
||||
bs = images.size(0)
|
||||
# 回传:残差模式先把 Δ̂ 加回 a_{t-1} 得到绝对动作,再移入历史
|
||||
# 回传:onehot 先期望值解码(原始空间)再按范围归一化;
|
||||
# 残差模式把 Δ̂ 加回 a_{t-1} 得到绝对动作;再移入历史
|
||||
with torch.no_grad():
|
||||
abs_action = prev_action + pred if predict_delta else pred
|
||||
if onehot:
|
||||
raw = bin_decode(pred.detach().cpu().numpy(),
|
||||
action_dim)
|
||||
abs_action = torch.from_numpy(normalize_action(
|
||||
raw, action_dim, mode=range_mode)).to(device)
|
||||
else:
|
||||
abs_action = (prev_action + pred
|
||||
if predict_delta else pred)
|
||||
history = _shift_history(true_history, abs_action.detach())
|
||||
loss = loss / len(batch["steps"])
|
||||
loss.backward()
|
||||
@@ -107,7 +144,8 @@ def train_one_epoch(model, loader, optimizer, device, max_grad_norm: float = Non
|
||||
|
||||
@torch.no_grad()
|
||||
def validate(model, loader, device, rollout_len: int = 1, predict_delta: bool = False,
|
||||
weights: torch.Tensor = None):
|
||||
weights: torch.Tensor = None, onehot: bool = False,
|
||||
action_dim: int = 18):
|
||||
model.eval()
|
||||
total_loss = 0.0
|
||||
count = 0
|
||||
@@ -118,7 +156,8 @@ def validate(model, loader, device, rollout_len: int = 1, predict_delta: bool =
|
||||
target = batch["target"].to(device)
|
||||
imu = batch["imu"].to(device) if "imu" in batch else None
|
||||
pred = model(images, history, imu)
|
||||
loss = _weighted_mse(pred, target, weights)
|
||||
loss = (_onehot_ce(pred, target, weights, action_dim) if onehot
|
||||
else _weighted_mse(pred, target, weights))
|
||||
bs = images.size(0)
|
||||
else:
|
||||
# 验证固定全真历史(确定性,不带 scheduled sampling 随机性),
|
||||
@@ -131,7 +170,9 @@ def validate(model, loader, device, rollout_len: int = 1, predict_delta: bool =
|
||||
target = step["target"].to(device)
|
||||
imu = step["imu"].to(device) if "imu" in step else None
|
||||
pred = model(images, history, imu)
|
||||
loss = loss + _weighted_mse(pred, target, weights)
|
||||
loss = loss + (_onehot_ce(pred, target, weights, action_dim)
|
||||
if onehot
|
||||
else _weighted_mse(pred, target, weights))
|
||||
bs = images.size(0)
|
||||
loss = loss / len(batch["steps"])
|
||||
total_loss += loss.item() * bs
|
||||
@@ -169,7 +210,8 @@ def main():
|
||||
cfg.data.action_stats_path, train_indices, cfg.data.frame_len,
|
||||
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)
|
||||
imu_dim=cfg.data.imu_dim, normalize_mode=cfg.data.normalize_mode,
|
||||
onehot=cfg.data.onehot, onehot_sigma=cfg.data.onehot_sigma)
|
||||
auto_stats = False
|
||||
if (cfg.data.normalize_actions and cfg.data.normalize_mode == "zscore"
|
||||
and not cfg.data.action_stats_path):
|
||||
@@ -186,7 +228,8 @@ def main():
|
||||
cfg.data.image_width, cfg.data.image_height, cfg.data.normalize_actions,
|
||||
cfg.data.action_stats_path, val_indices, 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)
|
||||
imu_dim=cfg.data.imu_dim, normalize_mode=cfg.data.normalize_mode,
|
||||
onehot=cfg.data.onehot, onehot_sigma=cfg.data.onehot_sigma)
|
||||
if auto_stats:
|
||||
# checkpoint 配置只存裸文件名:推理端按 checkpoint 目录解析相对路径
|
||||
cfg.data.action_stats_path = "action_stats.npy"
|
||||
@@ -219,6 +262,14 @@ def main():
|
||||
col_w = default_column_weights(cfg.data.action_dim)
|
||||
weights_t = torch.tensor(np.asarray(col_w, dtype=np.float32), device=device)
|
||||
print(f"Column weights: {[round(float(v), 4) for v in weights_t.tolist()]}")
|
||||
range_mode = (cfg.data.normalize_mode
|
||||
if (cfg.data.normalize_actions
|
||||
and is_range_mode(cfg.data.normalize_mode))
|
||||
else None)
|
||||
if cfg.data.onehot:
|
||||
print(f"One-hot 输出: {sum(1 for c in HEAD_COLS if c < cfg.data.action_dim)} 头"
|
||||
f" / {sum(START[c + 1] - START[c] for c in HEAD_COLS if c < cfg.data.action_dim)} bins"
|
||||
f"(sigma={cfg.data.onehot_sigma})")
|
||||
|
||||
if args.init_from:
|
||||
init_ckpt = torch.load(args.init_from, map_location="cpu", weights_only=True)
|
||||
@@ -257,16 +308,19 @@ def main():
|
||||
|
||||
for epoch in range(start_epoch, cfg.train.epochs + 1):
|
||||
print(f"\nEpoch {epoch}/{cfg.train.epochs} lr={optimizer.param_groups[0]['lr']:.6f}")
|
||||
train_loss = train_one_epoch(model, train_loader, optimizer, device,
|
||||
cfg.train.max_grad_norm,
|
||||
rollout_len=cfg.train.rollout_len,
|
||||
ss_prob=cfg.train.ss_prob,
|
||||
predict_delta=cfg.train.predict_delta,
|
||||
weights=weights_t)
|
||||
train_loss = train_one_epoch(
|
||||
model, train_loader, optimizer, device,
|
||||
cfg.train.max_grad_norm,
|
||||
rollout_len=cfg.train.rollout_len,
|
||||
ss_prob=cfg.train.ss_prob,
|
||||
predict_delta=cfg.train.predict_delta,
|
||||
weights=weights_t, onehot=cfg.data.onehot,
|
||||
range_mode=range_mode, action_dim=cfg.data.action_dim)
|
||||
val_loss = validate(model, val_loader, device,
|
||||
rollout_len=cfg.train.rollout_len,
|
||||
predict_delta=cfg.train.predict_delta,
|
||||
weights=weights_t)
|
||||
weights=weights_t, onehot=cfg.data.onehot,
|
||||
action_dim=cfg.data.action_dim)
|
||||
scheduler.step()
|
||||
print(f" train_loss={train_loss:.6f} val_loss={val_loss:.6f}")
|
||||
|
||||
|
||||
Reference in new issue
Block a user