Files
crpto_predictor/diagnose_model.py
T
crypto_predictorandClaude Sonnet 4.5 9bbdc8d42b Initial commit: BTC 1h prediction model (53.10% accuracy)
🎯 项目成果:
- 准确率: 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>
2026-01-29 21:27:15 +08:00

178 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""诊断模型预测偏差问题"""
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
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def diagnose_model():
"""诊断模型输出分布和损失函数问题"""
print("="*80)
print("模型诊断报告")
print("="*80)
# 1. 加载已训练的模型
print("\n[1/5] 加载已训练模型...")
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"✓ 模型已加载到设备: {device}")
print(f"✓ 模型配置: task={config.task}, input_dim={config.input_dim}")
# 2. 准备测试数据
print("\n[2/5] 准备测试数据...")
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)
# 3. 分析模型输出
print("\n[3/5] 分析模型原始输出...")
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(f"\n📊 模型输出统计(经过Sigmoid):")
print(f" 最小值: {all_outputs.min():.6f}")
print(f" 最大值: {all_outputs.max():.6f}")
print(f" 均值: {all_outputs.mean():.6f}")
print(f" 中位数: {np.median(all_outputs):.6f}")
print(f" 标准差: {all_outputs.std():.6f}")
print(f" > 0.5 的比例: {(all_outputs > 0.5).mean()*100:.2f}%")
print(f" > 0.4 的比例: {(all_outputs > 0.4).mean()*100:.2f}%")
print(f" > 0.3 的比例: {(all_outputs > 0.3).mean()*100:.2f}%")
print(f" > 0.2 的比例: {(all_outputs > 0.2).mean()*100:.2f}%")
print(f" > 0.1 的比例: {(all_outputs > 0.1).mean()*100:.2f}%")
print(f"\n📊 真实标签分布:")
print(f" 下跌(0): {(all_labels == 0).sum()} ({(all_labels == 0).mean()*100:.1f}%)")
print(f" 上涨(1): {(all_labels == 1).sum()} ({(all_labels == 1).mean()*100:.1f}%)")
# 4. 检查损失函数问题
print("\n[4/5] 检查损失函数兼容性...")
print("\n⚠️ 发现问题:")
print(" 模型使用: nn.Sigmoid() 作为输出激活函数")
print(" 当前损失函数: BCEWithLogitsLoss (期望logits输入)")
print("\n🔴 这导致了【双重Sigmoid问题】:")
print(" 1. 模型输出层: output = sigmoid(logits)")
print(" 2. 损失函数内部: sigmoid(output) = sigmoid(sigmoid(logits))")
print(" 3. 结果: 输出被过度压缩到接近0,梯度消失")
# 验证双重sigmoid效果
print("\n🧪 验证双重Sigmoid效果:")
test_logits = np.array([-2, -1, 0, 1, 2])
single_sigmoid = 1 / (1 + np.exp(-test_logits))
double_sigmoid = 1 / (1 + np.exp(-single_sigmoid))
print(" 原始logits: ", test_logits)
print(" 单次sigmoid: ", np.round(single_sigmoid, 4))
print(" 双重sigmoid: ", np.round(double_sigmoid, 4))
print(" → 双重sigmoid后,所有值都接近0.5(失去区分度)")
# 5. 建议修复方案
print("\n[5/5] 修复方案:")
print("\n✅ 方案1(推荐):移除模型输出层的Sigmoid")
print(" - 修改 models/dlinear/model.py:128")
print(" - 将 self.output_activation = nn.Sigmoid() 改为 nn.Identity()")
print(" - 保持损失函数为 BCEWithLogitsLoss")
print("\n✅ 方案2:更换损失函数")
print(" - 修改 models/base.py:74")
print(" - 将 BCEWithLogitsLoss 改为 BCELoss")
print(" - 保持模型输出层的 Sigmoid")
print("\n📌 其他发现:")
print(f" - 模型平均输出 {all_outputs.mean():.1%},远低于期望的50%")
print(f" - 几乎没有输出>0.5({(all_outputs>0.5).sum()}个)")
print(f" - 这解释了为什么模型从不预测上涨")
# 6. 生成可视化
print("\n[6/5] 生成输出分布图...")
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 输出直方图
axes[0, 0].hist(all_outputs, bins=50, alpha=0.7, edgecolor='black')
axes[0, 0].axvline(0.5, color='red', linestyle='--', label='决策阈值=0.5')
axes[0, 0].set_xlabel('模型输出(经过Sigmoid)')
axes[0, 0].set_ylabel('频数')
axes[0, 0].set_title('模型输出分布(双重Sigmoid问题)')
axes[0, 0].legend()
axes[0, 0].grid(alpha=0.3)
# 按真实标签分组的输出分布
outputs_down = all_outputs[all_labels == 0]
outputs_up = all_outputs[all_labels == 1]
axes[0, 1].hist([outputs_down, outputs_up], bins=30, alpha=0.6,
label=['真实下跌', '真实上涨'], edgecolor='black')
axes[0, 1].axvline(0.5, color='red', linestyle='--', label='决策阈值')
axes[0, 1].set_xlabel('模型输出')
axes[0, 1].set_ylabel('频数')
axes[0, 1].set_title('按真实标签分组的输出分布')
axes[0, 1].legend()
axes[0, 1].grid(alpha=0.3)
# CDF(累积分布)
sorted_outputs = np.sort(all_outputs)
cdf = np.arange(1, len(sorted_outputs)+1) / len(sorted_outputs)
axes[1, 0].plot(sorted_outputs, cdf, linewidth=2)
axes[1, 0].axvline(0.5, color='red', linestyle='--', label='决策阈值=0.5')
axes[1, 0].axhline(0.5, color='green', linestyle='--', alpha=0.5)
axes[1, 0].set_xlabel('模型输出')
axes[1, 0].set_ylabel('累积概率')
axes[1, 0].set_title('输出累积分布函数(CDF)')
axes[1, 0].legend()
axes[1, 0].grid(alpha=0.3)
# 双重sigmoid效果演示
x = np.linspace(-5, 5, 100)
sigmoid_1 = 1 / (1 + np.exp(-x))
sigmoid_2 = 1 / (1 + np.exp(-sigmoid_1))
axes[1, 1].plot(x, sigmoid_1, label='单次Sigmoid', linewidth=2)
axes[1, 1].plot(x, sigmoid_2, label='双重Sigmoid', linewidth=2, linestyle='--')
axes[1, 1].axhline(0.5, color='red', linestyle=':', alpha=0.5, label='决策阈值')
axes[1, 1].set_xlabel('输入(logits)')
axes[1, 1].set_ylabel('输出')
axes[1, 1].set_title('双重Sigmoid效果对比')
axes[1, 1].legend()
axes[1, 1].grid(alpha=0.3)
plt.tight_layout()
output_path = 'model_diagnosis.png'
plt.savefig(output_path, dpi=150, bbox_inches='tight')
print(f"✓ 可视化已保存至: {output_path}")
print("\n" + "="*80)
print("诊断完成!")
print("="*80)
if __name__ == "__main__":
diagnose_model()