🎯 项目成果: - 准确率: 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>
235 lines
10 KiB
Python
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()
|