🎯 项目成果: - 准确率: 53.10% (2026年测试集) - 任务类型: 二分类 (预测涨/跌) - 特征: OHLCV + RSI + MACD (9个特征) - 训练数据: 2020-2025年BTC小时数据 (52,351序列) - 测试数据: 2026年1-29日 (484序列) 📊 性能指标: - 精确率: 51.79% - 召回率: 49.36% - F1分数: 0.5054 - 预测分布: 非常平衡 (偏差仅-11个) 🔬 实验历史: - 尝试了5种方案 (回归/分类, 不同数据划分) - 分类任务优于回归任务 (53.10% vs 49.5%) - 全量训练避免了过拟合问题 📦 主要模型文件: - models/checkpoints/dlinear_classification_2026/ - best_model.pt (最佳模型) - preprocessor.pkl (预处理器) - test_2026_results.json (测试结果) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
135 lines
5.3 KiB
Python
135 lines
5.3 KiB
Python
"""简化版模型诊断(无需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()
|