Files
crpto_predictor/diagnose_bias.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

235 lines
10 KiB
Python

"""诊断模型为何倾向预测上涨"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import torch
import numpy as np
import pandas as pd
from models.dlinear.model import DLinear
from data.fetcher import BinanceDataFetcher
from data.preprocessor import CryptoPreprocessor, split_data
from data.dataset import create_dataloaders
def analyze_bias():
print("="*80)
print("分析模型预测偏差的原因")
print("="*80)
# 1. 加载数据
print("\n[1/5] 加载数据并分析标签分布...")
fetcher = BinanceDataFetcher()
df = fetcher.fetch_ohlcv('BTC', '1h', use_cache=True)
# 分析原始价格趋势
df['price_change'] = df['close'].pct_change()
df['future_1h'] = df['close'].shift(-1)
df['future_return_1h'] = (df['future_1h'] - df['close']) / df['close']
print(f"\n📊 原始数据分析(最近8760小时):")
print(f" 数据范围: {df['timestamp'].min()} 至 {df['timestamp'].max()}")
print(f" BTC起始价格: ${df['close'].iloc[0]:,.2f}")
print(f" BTC结束价格: ${df['close'].iloc[-1]:,.2f}")
total_return = (df['close'].iloc[-1] - df['close'].iloc[0]) / df['close'].iloc[0]
print(f" 总收益率: {total_return*100:.2f}%")
up_hours = (df['future_return_1h'] > 0).sum()
down_hours = (df['future_return_1h'] <= 0).sum()
total_hours = up_hours + down_hours
print(f"\n 小时K线涨跌统计:")
print(f" 上涨小时: {up_hours}/{total_hours} ({up_hours/total_hours*100:.1f}%)")
print(f" 下跌小时: {down_hours}/{total_hours} ({down_hours/total_hours*100:.1f}%)")
# 2. 预处理并查看训练集分布
print("\n[2/5] 分析训练/验证/测试集的标签分布...")
preprocessor = CryptoPreprocessor(
lookback=168,
forecast_horizon=1,
price_change_threshold=0.0
)
X, y, processed_df = preprocessor.preprocess_pipeline(df, task='classification', fit=True)
train_data, val_data, test_data = split_data(X, y, train_split=0.7, val_split=0.15)
# 分析每个数据集的标签分布
train_up = (train_data[1] == 1).sum()
train_down = (train_data[1] == 0).sum()
val_up = (val_data[1] == 1).sum()
val_down = (val_data[1] == 0).sum()
test_up = (test_data[1] == 1).sum()
test_down = (test_data[1] == 0).sum()
print(f"\n 训练集标签分布:")
print(f" 上涨: {train_up}/{len(train_data[1])} ({train_up/len(train_data[1])*100:.1f}%)")
print(f" 下跌: {train_down}/{len(train_data[1])} ({train_down/len(train_data[1])*100:.1f}%)")
print(f"\n 验证集标签分布:")
print(f" 上涨: {val_up}/{len(val_data[1])} ({val_up/len(val_data[1])*100:.1f}%)")
print(f" 下跌: {val_down}/{len(val_data[1])} ({val_down/len(val_data[1])*100:.1f}%)")
print(f"\n 测试集标签分布:")
print(f" 上涨: {test_up}/{len(test_data[1])} ({test_up/len(test_data[1])*100:.1f}%)")
print(f" 下跌: {test_down}/{len(test_data[1])} ({test_down/len(test_data[1])*100:.1f}%)")
# 3. 分析训练集对应的时间范围
print("\n[3/5] 分析各数据集的时间范围和价格趋势...")
# 由于序列创建,需要偏移lookback
lookback = 168
train_end_idx = int(len(processed_df) * 0.7) - lookback
val_end_idx = int(len(processed_df) * 0.85) - lookback
train_df = processed_df.iloc[lookback:train_end_idx+lookback]
val_df = processed_df.iloc[train_end_idx+lookback:val_end_idx+lookback]
test_df = processed_df.iloc[val_end_idx+lookback:]
print(f"\n 训练集:")
print(f" 时间范围: {train_df['timestamp'].min()} 至 {train_df['timestamp'].max()}")
train_return = (train_df['close'].iloc[-1] - train_df['close'].iloc[0]) / train_df['close'].iloc[0]
print(f" 价格变化: ${train_df['close'].iloc[0]:,.0f} → ${train_df['close'].iloc[-1]:,.0f}")
print(f" 收益率: {train_return*100:.2f}%")
print(f"\n 验证集:")
print(f" 时间范围: {val_df['timestamp'].min()} 至 {val_df['timestamp'].max()}")
val_return = (val_df['close'].iloc[-1] - val_df['close'].iloc[0]) / val_df['close'].iloc[0]
print(f" 价格变化: ${val_df['close'].iloc[0]:,.0f} → ${val_df['close'].iloc[-1]:,.0f}")
print(f" 收益率: {val_return*100:.2f}%")
print(f"\n 测试集:")
print(f" 时间范围: {test_df['timestamp'].min()} 至 {test_df['timestamp'].max()}")
test_return = (test_df['close'].iloc[-1] - test_df['close'].iloc[0]) / test_df['close'].iloc[0]
print(f" 价格变化: ${test_df['close'].iloc[0]:,.0f} → ${test_df['close'].iloc[-1]:,.0f}")
print(f" 收益率: {test_return*100:.2f}%")
# 4. 加载模型并分析输出
print("\n[4/5] 加载模型并分析输出分布...")
model_path = './models/checkpoints/dlinear_btc_1h_forecast/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)
model = DLinear(checkpoint['config'])
model.load_state_dict(checkpoint['model_state_dict'])
model = model.to(device)
model.eval()
# 分析测试集输出
_, _, test_loader = create_dataloaders(
test_data, test_data, test_data,
task='classification',
batch_size=64
)
all_logits = []
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_logits.extend(outputs.cpu().numpy().flatten())
all_labels.extend(batch_y.numpy().flatten())
all_logits = np.array(all_logits)
all_labels = np.array(all_labels)
all_probs = 1 / (1 + np.exp(-all_logits)) # sigmoid
print(f"\n 模型输出统计:")
print(f" Logits均值: {all_logits.mean():.6f}")
print(f" Logits标准差: {all_logits.std():.6f}")
print(f" Logits中位数: {np.median(all_logits):.6f}")
print(f" 概率均值: {all_probs.mean():.6f}")
# 按真实标签分组分析
logits_when_up = all_logits[all_labels == 1]
logits_when_down = all_logits[all_labels == 0]
print(f"\n 按真实标签分组:")
print(f" 真实上涨时,平均logit: {logits_when_up.mean():.6f}")
print(f" 真实下跌时,平均logit: {logits_when_down.mean():.6f}")
print(f" 差值: {logits_when_up.mean() - logits_when_down.mean():.6f}")
# 5. 分析为什么偏向上涨
print("\n[5/5] 分析预测偏差的根本原因...")
print("\n" + "="*80)
print("🔍 根本原因分析")
print("="*80)
# 原因1: 数据趋势
if total_return > 0.1:
print(f"\n✅ 原因1: 训练数据处于上涨趋势")
print(f" - 整体收益率: {total_return*100:.2f}%")
print(f" - 训练集收益率: {train_return*100:.2f}%")
print(f" - 模型学习到了'牛市偏好'")
elif total_return < -0.1:
print(f"\n⚠️ 原因1: 训练数据处于下跌趋势")
print(f" - 整体收益率: {total_return*100:.2f}%")
else:
print(f"\n⚪ 原因1: 训练数据趋势中性")
print(f" - 整体收益率: {total_return*100:.2f}%")
# 原因2: 类别不平衡
if train_up > train_down * 1.05:
print(f"\n✅ 原因2: 训练集标签不平衡")
print(f" - 上涨样本: {train_up} ({train_up/len(train_data[1])*100:.1f}%)")
print(f" - 下跌样本: {train_down} ({train_down/len(train_data[1])*100:.1f}%)")
print(f" - 不平衡比例: {train_up/train_down:.2f}:1")
else:
print(f"\n⚪ 原因2: 训练集标签基本平衡")
print(f" - 上涨/下跌比例: {train_up/train_down:.2f}:1")
# 原因3: 模型输出偏差
if all_logits.mean() > 0.01:
print(f"\n✅ 原因3: 模型输出存在正偏差")
print(f" - 平均logit: {all_logits.mean():.6f} > 0")
print(f" - 平均概率: {all_probs.mean():.6f} > 0.5")
print(f" - 这导致更多样本被预测为上涨")
elif all_logits.mean() < -0.01:
print(f"\n⚠️ 原因3: 模型输出存在负偏差")
print(f" - 平均logit: {all_logits.mean():.6f} < 0")
else:
print(f"\n⚪ 原因3: 模型输出基本无偏")
print(f" - 平均logit: {all_logits.mean():.6f} ≈ 0")
# 原因4: 阈值问题
print(f"\n⚪ 原因4: 决策阈值")
print(f" - 当前阈值: 0.5")
print(f" - 平均概率: {all_probs.mean():.6f}")
if all_probs.mean() > 0.5:
print(f" - 因为平均概率>0.5,导致多数样本被预测为上涨")
# 计算最优阈值
from sklearn.metrics import f1_score
best_threshold = 0.5
best_f1 = 0
for threshold in np.arange(0.3, 0.7, 0.01):
preds = (all_probs > threshold).astype(int)
f1 = f1_score(all_labels, preds)
if f1 > best_f1:
best_f1 = f1
best_threshold = threshold
print(f"\n💡 建议调整阈值:")
print(f" - 最优阈值: {best_threshold:.3f} (F1={best_f1:.4f})")
print(f" - 当前阈值: 0.500 (F1={f1_score(all_labels, (all_probs>0.5).astype(int)):.4f})")
print("\n" + "="*80)
print("📌 总结")
print("="*80)
print(f"\n模型预测上涨的比例: {(all_probs > 0.5).sum()}/{len(all_probs)} ({(all_probs>0.5).mean()*100:.1f}%)")
print(f"主要原因:")
if total_return > 0.1:
print(f" 1. ✅ 训练数据处于上涨趋势 ({total_return*100:.1f}%)")
if train_up > train_down * 1.05:
print(f" 2. ✅ 训练集上涨样本更多 ({train_up/train_down:.2f}:1)")
if all_logits.mean() > 0.01:
print(f" 3. ✅ 模型学习到了上涨偏好 (平均logit={all_logits.mean():.4f})")
print(f"\n这是正常现象,因为:")
print(f" - 如果训练期间BTC整体上涨,模型会学习到'上涨更可能'")
print(f" - 这类似于'在牛市训练的模型会更乐观'")
print(f" - 解决方案:使用更长的训练数据(包含牛市和熊市)")
if __name__ == "__main__":
analyze_bias()