"""简化版模型诊断(无需matplotlib)""" import torch import numpy as np from models.dlinear.model import DLinear, DLinearConfig from data.fetcher import BinanceDataFetcher from data.preprocessor import CryptoPreprocessor, split_data from data.dataset import create_dataloaders def diagnose(): print("="*80) print("🔍 模型预测偏差诊断报告") print("="*80) # 加载模型 print("\n[1/4] 加载已训练模型...") model_path = './models/checkpoints/dlinear_btc_1h_balanced/best_model.pt' device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') checkpoint = torch.load(model_path, map_location=device, weights_only=False) config = checkpoint['config'] model = DLinear(config) model.load_state_dict(checkpoint['model_state_dict']) model = model.to(device) model.eval() print(f"✓ 模型配置: task={config.task}, hidden_dim={config.hidden_dim}") # 准备数据 print("\n[2/4] 加载测试数据...") fetcher = BinanceDataFetcher() df = fetcher.fetch_ohlcv('BTC', '1h', use_cache=True) preprocessor = CryptoPreprocessor(lookback=168, forecast_horizon=24, price_change_threshold=0.0) X, y, _ = preprocessor.preprocess_pipeline(df, task='classification', fit=True) _, _, test_data = split_data(X, y, train_split=0.7, val_split=0.15) _, _, test_loader = create_dataloaders(test_data, test_data, test_data, batch_size=64) # 收集输出 print("\n[3/4] 分析模型输出...") all_outputs = [] all_labels = [] with torch.no_grad(): for batch_X, batch_y in test_loader: batch_X = batch_X.to(device) outputs = model(batch_X).squeeze() all_outputs.extend(outputs.cpu().numpy().flatten()) all_labels.extend(batch_y.numpy().flatten()) all_outputs = np.array(all_outputs) all_labels = np.array(all_labels) # 统计分析 print("\n" + "="*80) print("📊 关键发现") print("="*80) print(f"\n1️⃣ 模型输出统计(已经过Sigmoid):") print(f" 最小值: {all_outputs.min():.6f}") print(f" 最大值: {all_outputs.max():.6f}") print(f" 均值: {all_outputs.mean():.6f} ← 应该接近0.5") print(f" 中位数: {np.median(all_outputs):.6f}") print(f" 标准差: {all_outputs.std():.6f}") print(f"\n2️⃣ 预测阈值分析:") for threshold in [0.5, 0.4, 0.3, 0.2, 0.1]: count = (all_outputs > threshold).sum() pct = (all_outputs > threshold).mean() * 100 print(f" 输出 > {threshold}: {count:4d} 个 ({pct:5.2f}%)") print(f"\n3️⃣ 真实标签分布:") n_down = (all_labels == 0).sum() n_up = (all_labels == 1).sum() print(f" 下跌(0): {n_down} ({n_down/len(all_labels)*100:.1f}%)") print(f" 上涨(1): {n_up} ({n_up/len(all_labels)*100:.1f}%)") # 验证双重sigmoid问题 print("\n" + "="*80) print("🔴 问题诊断:双重Sigmoid Bug") print("="*80) print("\n📋 当前架构:") print(" 模型输出层: self.output_activation = nn.Sigmoid() (models/dlinear/model.py:128)") print(" 损失函数: BCEWithLogitsLoss() (models/base.py:74)") print("\n⚠️ 问题:") print(" BCEWithLogitsLoss 内部包含 Sigmoid,期望输入是 logits(未激活)") print(" 但模型已经输出了 sigmoid(logits)") print(" → 损失函数再次应用: sigmoid(sigmoid(logits)) ← 双重激活!") print("\n🧪 双重Sigmoid效果演示:") test_logits = np.array([-3, -1, 0, 1, 3]) single = 1 / (1 + np.exp(-test_logits)) double = 1 / (1 + np.exp(-single)) print(f" 原始logits: {test_logits}") print(f" 单次sigmoid: {np.round(single, 4)}") print(f" 双重sigmoid: {np.round(double, 4)} ← 全部压缩到~0.5") print("\n💥 后果:") print(f" - 模型输出均值仅 {all_outputs.mean():.1%}(期望~50%)") print(f" - {(all_outputs>0.5).sum()} 个样本输出>0.5(共{len(all_outputs)}个)") print(f" - 模型永远预测类别0(下跌),因为没有输出超过阈值0.5") print(f" - 梯度消失,模型无法有效学习") # 解决方案 print("\n" + "="*80) print("✅ 修复方案") print("="*80) print("\n方案1(推荐):移除模型的Sigmoid输出") print(" 文件: models/dlinear/model.py") print(" 位置: 第127-130行") print(" 修改前: if config.task == 'classification':") print(" self.output_activation = nn.Sigmoid()") print(" 修改后: if config.task == 'classification':") print(" self.output_activation = nn.Identity() # 输出logits") print(" 说明: 保持损失函数为BCEWithLogitsLoss") print("\n方案2:更换损失函数(不推荐)") print(" 文件: models/base.py") print(" 位置: 第70-76行") print(" 修改: 将 BCEWithLogitsLoss 改为 BCELoss") print(" 说明: 保持模型输出Sigmoid,但BCELoss数值稳定性较差") print("\n" + "="*80) print("📌 结论") print("="*80) print("✓ 预测偏差的根本原因是【双重Sigmoid Bug】") print("✓ 与数据处理、类别平衡、特征工程无关") print("✓ 修复后需要重新训练模型") print("="*80) if __name__ == "__main__": diagnose()