关于 ml-failfast-validation
用于在开始长时间训练前验证 ML 实验的 Skill,提供模型设置、数据模式、梯度、预测和实验日志检查,并说明常见故障的排查方向。
主要功能
- 01训练前 POC 检查清单
- 02数据模式验证
- 03梯度流与健康检查
- 04预测崩塌与相关性检查
- 05NDJSON 事件验证
适用场景
- 01完整训练前检查模型
- 02发现数据管道模式问题
- 03排查梯度缺失或不稳定
- 04检测预测崩塌
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ terrylica/ cc-skills --skill ml-failfast-validation安装后在 Claude Code 中输入/ml-failfast-validation确认已加载。
附带文件references/evolution-log.md
详细介绍
ML 快速失败验证
用于在提交长时间运行的 ML 实验前发现问题的 POC 验证模式。
自我演进的技能:此技能会在使用过程中不断完善。如果指令有误、参数发生漂移,或需要采用变通方案,请立即修复此文件,不要拖延。仅针对真实且可复现的问题进行更新。
何时使用此技能
在以下情况下使用此技能:
- 开始一项将运行数小时的新 ML 实验
- 在完整训练前验证模型架构
- 检查梯度流和数据管道完整性
- 实施 POC 验证清单
- 调试预测坍塌或梯度爆炸问题
1. 为什么要快速失败?
| 不使用快速失败 | 使用快速失败 |
|---|---|
| 4 小时后才发现崩溃 | 30 秒内发现问题 |
| 从晦涩的错误信息中调试 | 获得清晰的错误信息 |
| 浪费 GPU 时间 | 提交前完成验证 |
| 数据问题悄无声息 | 显式检查架构 |
原则:在执行昂贵的计算之前,验证所有可能出错的环节。
2. POC 验证清单
最小可行 POC(5 项检查)
def run_poc_validation():
"""Fast validation before full experiment."""
print("=" * 60)
print("FAIL-FAST POC VALIDATION")
print("=" * 60)
# [1/5] Model instantiation
print("\n[1/5] Model instantiation...")
model = create_model(architecture, input_size=n_features)
x = torch.randn(32, seq_len, n_features).to(device)
out = model(x)
assert out.shape == (32, 1), f"Output shape wrong: {out.shape}"
print(f" Input: (32, {seq_len}, {n_features}) -> Output: {out.shape}")
print(" Status: PASS")
# [2/5] Gradient flow
print("\n[2/5] Gradient flow...")
y = torch.randn(32, 1).to(device)
loss = F.mse_loss(out, y)
loss.backward()
grad_norms = [p.grad.norm().item() for p in model.parameters() if p.grad is not None]
assert len(grad_norms) > 0, "No gradients!"
assert all(np.isfinite(g) for g in grad_norms), "NaN/Inf gradients!"
print(f" Max grad norm: {max(grad_norms):.4f}")
print(" Status: PASS")
# [3/5] NDJSON artifact validation
print("\n[3/5] NDJSON artifact validation...")
log_path = output_dir / "experiment.jsonl"
with open(log_path, "a") as f:
f.write(json.dumps({"phase": "poc_start", "timestamp": datetime.now().isoformat()}) + "\n")
assert log_path.exists(), "Log file not created"
print(f" Log file: {log_path}")
print(" Status: PASS")
# [4/5] Epoch selector variation
print("\n[4/5] Epoch selector variation...")
epochs = []
for seed in [1, 2, 3]:
selector = create_selector()
# Simulate different validation results
for e in range(10, 201, 10):
selector.record(epoch=e, sortino=np.random.randn() * 0.1, sparsity=np.random.rand())
epochs.append(selector.select())
print(f" Selected epochs: {epochs}")
assert len(set(epochs)) > 1 or all(e == epochs[0] for e in epochs), "Selector not varying"
print(" Status: PASS")
# [5/5] Mini training (10 epochs)
print("\n[5/5] Mini training (10 epochs)...")
model = create_model(architecture, input_size=n_features).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0005)
initial_loss = None
for epoch in range(10):
loss = train_one_epoch(model, train_loader, optimizer)
if initial_loss is None:
initial_loss = loss
print(f" Initial loss: {initial_loss:.4f}")
print(f" Final loss: {loss:.4f}")
print(" Status: PASS")
print("\n" + "=" * 60)
print("POC RESULT: ALL 5 CHECKS PASSED")
print("=" * 60)
扩展 POC(10 项检查)
如需全面验证,请添加以下检查:
# [6/10] Data loading
print("\n[6/10] Data loading...")
df = fetch_data(symbol, threshold)
assert len(df) > min_required_bars, f"Insufficient data: {len(df)} bars"
print(f" Loaded: {len(df):,} bars")
print(" Status: PASS")
# [7/10] Schema validation
print("\n[7/10] Schema validation...")
validate_schema(df, required_columns, "raw_data")
print(" Status: PASS")
# [8/10] Feature computation
print("\n[8/10] Feature computation...")
df = compute_features(df)
validate_schema(df, feature_columns, "features")
print(f" Features: {len(feature_columns)}")
print(" Status: PASS")
# [9/10] Prediction sanity
print("\n[9/10] Prediction sanity...")
preds = model(X_test).detach().cpu().numpy()
pred_std = preds.std()
target_std = y_test.std()
pred_ratio = pred_std / target_std
assert pred_ratio > 0.005, f"Predictions collapsed: ratio={pred_ratio:.4f}"
print(f" Pred std ratio: {pred_ratio:.2%}")
print(" Status: PASS")
# [10/10] Checkpoint save/load
print("\n[10/10] Checkpoint save/load...")
torch.save(model.state_dict(), checkpoint_path)
model2 = create_model(architecture, input_size=n_features)
model2.load_state_dict(torch.load(checkpoint_path))
print(" Status: PASS")
3. 架构验证模式
问题
# BAD: Cryptic error 2 hours into experiment
KeyError: 'returns_vs' # Which file? Which function? What columns exist?
解决方案
def validate_schema(df, required: list[str], stage: str) -> None:
"""Fail-fast schema validation with actionable error messages."""
# Handle both DataFrame columns and DatetimeIndex
available = list(df.columns)
if hasattr(df.index, 'name') and df.index.name:
available.append(df.index.name)
missing = [c for c in required if c not in available]
if missing:
raise ValueError(
f"[{stage}] Missing columns: {missing}\n"
f"Available: {sorted(available)}\n"
f"DataFrame shape: {df.shape}"
)
print(f" Schema validation PASSED ({stage}): {len(required)} columns", flush=True)
# Usage at pipeline boundaries
REQUIRED_RAW = ["open", "high", "low", "close", "volume"]
REQUIRED_FEATURES = ["returns_vs", "momentum_z", "atr_pct", "volume_z",
"rsi_14", "bb_pct_b", "vol_regime", "return_accel", "pv_divergence"]
df = fetch_data(symbol)
validate_schema(df, REQUIRED_RAW, "raw_data")
df = compute_features(df)
validate_schema(df, REQUIRED_FEATURES, "features")
4. 梯度健康检查
基本梯度检查
def check_gradient_health(model: nn.Module, sample_input: torch.Tensor) -> dict:
"""Verify gradients flow correctly through model."""
model.train()
out = model(sample_input)
loss = out.sum()
loss.backward()
stats = {"total_params": 0, "params_with_grad": 0, "grad_norms": []}
for name, param in model.named_parameters():
stats["total_params"] += 1
if param.grad is not None:
stats["params_with_grad"] += 1
norm = param.grad.norm().item()
stats["grad_norms"].append(norm)
# Check for issues
if not np.isfinite(norm):
raise ValueError(f"Non-finite gradient in {name}: {norm}")
if norm > 100:
print(f" WARNING: Large gradient in {name}: {norm:.2f}")
stats["max_grad"] = max(stats["grad_norms"]) if stats["grad_norms"] else 0
stats["mean_grad"] = np.mean(stats["grad_norms"]) if stats["grad_norms"] else 0
return stats
特定于架构的检查
def check_lstm_gradients(model: nn.Module) -> dict:
"""Check LSTM-specific gradient patterns."""
stats = {}
for name, param in model.named_parameters():
if param.grad is None:
continue
# Check forget gate bias (should not be too negative)
if "bias_hh" in name or "bias_ih" in name:
# LSTM bias: [i, f, g, o] gates
hidden_size = param.shape[0] // 4
forget_bias = param.grad[hidden_size:2*hidden_size]
stats["forget_bias_grad_mean"] = forget_bias.mean().item()
# Check hidden-to-hidden weights
if "weight_hh" in name:
stats["hh_weight_grad_norm"] = param.grad.norm().item()
return stats
5. 预测合理性检查
坍塌检测
def check_prediction_sanity(preds: np.ndarray, targets: np.ndarray) -> dict:
"""Detect prediction collapse or explosion."""
stats = {
"pred_mean": preds.mean(),
"pred_std": preds.std(),
"pred_min": preds.min(),
"pred_max": preds.max(),
"target_std": targets.std(),
}
# Relative threshold (not absolute!)
stats["pred_std_ratio"] = stats["pred_std"] / stats["target_std"]
# Collapse detection
if stats["pred_std_ratio"] < 0.005: # < 0.5% of target variance
raise ValueError(
f"Predictions collapsed!\n"
f" pred_std: {stats['pred_std']:.6f}\n"
f" target_std: {stats['target_std']:.6f}\n"
f" ratio: {stats['pred_std_ratio']:.4%}"
)
# Explosion detection
if stats["pred_std_ratio"] > 100: # > 100x target variance
raise ValueError(
f"Predictions exploded!\n"
f" pred_std: {stats['pred_std']:.2f}\n"
f" target_std: {stats['target_std']:.6f}\n"
f" ratio: {stats['pred_std_ratio']:.1f}x"
)
# Unique value check
stats["unique_values"] = len(np.unique(np.round(preds, 6)))
if stats["unique_values"] < 10:
print(f" WARNING: Only {stats['unique_values']} unique prediction values")
return stats
相关性检查
def check_prediction_correlation(preds: np.ndarray, targets: np.ndarray) -> float:
"""Check if predictions have any correlation with targets."""
corr = np.corrcoef(preds.flatten(), targets.flatten())[0, 1]
if not np.isfinite(corr):
print(" WARNING: Correlation is NaN (likely collapsed predictions)")
return 0.0
# Note: negative correlation may still be useful (short signal)
print(f" Prediction-target correlation: {corr:.4f}")
return corr
6. NDJSON 日志验证
必需的事件类型
REQUIRED_EVENTS = {
"experiment_start": ["architecture", "features", "config"],
"fold_start": ["fold_id", "train_size", "val_size", "test_size"],
"epoch_complete": ["epoch", "train_loss", "val_loss"],
"fold_complete": ["fold_id", "test_sharpe", "test_sortino"],
"experiment_complete": ["total_folds", "mean_sharpe", "elapsed_seconds"],
}
def validate_ndjson_schema(log_path: Path) -> None:
"""Validate NDJSON log has all required events and fields."""
events = {}
with open(log_path) as f:
for line in f:
event = json.loads(line)
phase = event.get("phase", "unknown")
if phase not in events:
events[phase] = []
events[phase].append(event)
for phase, required_fields in REQUIRED_EVENTS.items():
if phase not in events:
raise ValueError(f"Missing event type: {phase}")
sample = events[phase][0]
missing = [f for f in required_fields if f not in sample]
if missing:
raise ValueError(f"Event '{phase}' missing fields: {missing}")
print(f" NDJSON schema valid: {len(events)} event types")
7. POC 耗时指南
| 检查 | 典型耗时 | 最长耗时 | 超时后的处理 |
|---|---|---|---|
| 模型实例化 | < 1s | 5s | 检查设备,缩小模型规模 |
| 梯度流 | < 2s | 10s | 检查批次大小 |
| 架构验证 | < 0.1s | 1s | 检查数据加载 |
| 小规模训练(10 个周期) | < 30s | 2min | 减小批次,检查数据加载器 |
| 完整 POC(10 项检查) | < 2min | 5min | 存在问题 |
8. 故障响应指南
| 故障 | 可能原因 | 修复方法 |
|---|---|---|
| 形状不匹配 | input_size 或 seq_len 错误 | 检查特征数量 |
| 梯度为 NaN | LR 过高,初始化不佳 | 降低 LR,检查初始化 |
| 梯度为零 | 层失效,缺少参数 | 检查模型架构 |
| 预测坍塌 | 归一化器问题,损失函数不佳 | 检查 sLSTM 归一化器 |
| 预测爆炸 | 梯度爆炸 | 添加或收紧梯度裁剪 |
| 架构缺少列 | 数据源错误 | 检查 fetch 函数 |
| 检查点加载失败 | State dict 键不匹配 | 检查模型架构是否匹配 |
9. 集成示例
def main():
# Parse args, setup output dir...
# PHASE 1: Fail-fast POC
print("=" * 60)
print("FAIL-FAST POC VALIDATION")
print("=" * 60)
try:
run_poc_validation()
except Exception as e:
print(f"\n{'=' * 60}")
print(f"POC FAILED: {type(e).__name__}")
print(f"{'=' * 60}")
print(f"Error: {e}")
print("\nFix the issue before running full experiment.")
sys.exit(1)
# PHASE 2: Full experiment (only if POC passes)
print("\n" + "=" * 60)
print("STARTING FULL EXPERIMENT")
print("=" * 60)
run_full_experiment()
10. 应避免的反模式
不要:为了“节省时间”而跳过验证
# BAD: "I'll just run it and see"
run_full_experiment() # 4 hours later: crash
不要:对相对量使用绝对阈值
# BAD: Absolute threshold
assert pred_std > 1e-4 # Meaningless for returns ~0.001
# GOOD: Relative threshold
assert pred_std / target_std > 0.005 # 0.5% of target variance
不要:静默捕获所有异常
# BAD: Hides real issues
try:
result = risky_operation()
except Exception:
result = default_value # What went wrong?
# GOOD: Catch specific exceptions
try:
result = risky_operation()
except (ValueError, RuntimeError) as e:
logger.error(f"Operation failed: {e}")
raise
不要:打印时不刷新缓冲区
# BAD: Output buffered, can't see progress
print(f"Processing fold {i}...")
# GOOD: See output immediately
print(f"Processing fold {i}...", flush=True)
参考资料
故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| POC 中出现 NaN 梯度 | 学习率过高 | 将 LR 降低 10 倍,检查权重初始化 |
| 梯度为零 | 层失效或缺少参数 | 检查模型架构,确认 requires_grad=True |
| 预测结果收敛到单一值 | 归一化器问题或损失函数不佳 | 检查目标值归一化,确认损失函数 |
| 预测结果发散 | 梯度爆炸 | 添加梯度裁剪,降低学习率 |
| 缺少 Schema 列 | 数据源或转换错误 | 确认 fetch 函数返回了预期的列 |
| 检查点加载失败 | 状态字典键不匹配 | 确保模型架构与已保存的检查点一致 |
| POC 超时(>5 分钟) | 数据加载或模型过大 | 减小批次大小,检查 DataLoader 的 num_workers |
| Mini 训练没有进展 | 学习率过低或参数被冻结 | 提高 LR,确认优化器会更新所有参数 |
| NDJSON 验证失败 | 缺少必需的事件类型 | 检查所有阶段是否输出了预期字段 |
| 形状不匹配错误 | input_size 或 seq_len 错误 | 确认特征数量与模型输入维度一致 |
执行后复盘
此技能执行完毕后,请在结束前检查:
- 命令是否成功? — 如果没有,请修复导致失败的指令或错误表。
- 参数或输出是否发生变化? — 如果底层工具的接口发生变化,请更新用法示例和参数表,使其保持一致。
- 是否需要变通方案? — 如果你不得不临时调整(使用不同的标志、增加步骤),请更新此 SKILL.md,避免下次调用时再采用相同的变通方案。
只有在问题确实存在且可复现时才进行更新,不要针对推测性问题更新。
评论
支持 Markdown