"""诊断模型为何倾向预测上涨""" 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()