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>
This commit is contained in:
commit
9bbdc8d42b
82 files changed
+8742
No files matched your search
+182
@@ -0,0 +1,182 @@
|
||||
"""Base model interface for all prediction models."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelConfig:
|
||||
"""Base configuration for all models."""
|
||||
|
||||
input_dim: int # Number of input features
|
||||
lookback: int # Sequence length
|
||||
task: Literal['classification', 'regression'] = 'classification'
|
||||
hidden_dim: int = 512
|
||||
dropout: float = 0.1
|
||||
|
||||
|
||||
class BasePredictor(nn.Module, ABC):
|
||||
"""Abstract base class for all prediction models."""
|
||||
|
||||
def __init__(self, config: ModelConfig):
|
||||
"""Initialize model with configuration.
|
||||
|
||||
Args:
|
||||
config: Model configuration
|
||||
"""
|
||||
super().__init__()
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Forward pass.
|
||||
|
||||
Args:
|
||||
x: Input tensor of shape (batch_size, lookback, input_dim)
|
||||
|
||||
Returns:
|
||||
Output tensor. Shape depends on task:
|
||||
- Classification: (batch_size, 1) with sigmoid output
|
||||
- Regression: (batch_size, 1) with raw value
|
||||
"""
|
||||
pass
|
||||
|
||||
def predict(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Make predictions (inference mode).
|
||||
|
||||
Args:
|
||||
x: Input tensor
|
||||
|
||||
Returns:
|
||||
Predictions
|
||||
"""
|
||||
self.eval()
|
||||
with torch.no_grad():
|
||||
return self.forward(x)
|
||||
|
||||
def get_loss_function(self, pos_weight=None, device='cpu') -> nn.Module:
|
||||
"""Get appropriate loss function for the task.
|
||||
|
||||
Args:
|
||||
pos_weight: Weight for positive class (for imbalanced data)
|
||||
device: Device to place tensors on
|
||||
|
||||
Returns:
|
||||
Loss function
|
||||
"""
|
||||
if self.config.task == 'classification':
|
||||
if pos_weight is not None:
|
||||
# 使用float32以兼容MPS (Mac M1)
|
||||
pos_weight_tensor = torch.tensor([pos_weight], dtype=torch.float32).to(device)
|
||||
return nn.BCEWithLogitsLoss(pos_weight=pos_weight_tensor)
|
||||
else:
|
||||
return nn.BCELoss()
|
||||
else: # regression
|
||||
return nn.MSELoss()
|
||||
|
||||
def save(self, path: str):
|
||||
"""Save model checkpoint.
|
||||
|
||||
Args:
|
||||
path: Path to save checkpoint
|
||||
"""
|
||||
torch.save({
|
||||
'model_state_dict': self.state_dict(),
|
||||
'config': self.config
|
||||
}, path)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str, device: Optional[str] = None):
|
||||
"""Load model from checkpoint.
|
||||
|
||||
Args:
|
||||
path: Path to checkpoint
|
||||
device: Device to load model to
|
||||
|
||||
Returns:
|
||||
Loaded model
|
||||
"""
|
||||
checkpoint = torch.load(path, map_location=device)
|
||||
model = cls(checkpoint['config'])
|
||||
model.load_state_dict(checkpoint['model_state_dict'])
|
||||
return model
|
||||
|
||||
def to_onnx(self, path: str, dummy_input: Optional[torch.Tensor] = None):
|
||||
"""Export model to ONNX format.
|
||||
|
||||
Args:
|
||||
path: Path to save ONNX model
|
||||
dummy_input: Example input tensor. If None, will create default.
|
||||
"""
|
||||
# Move to CPU for ONNX export (critical for M1)
|
||||
self.cpu()
|
||||
self.eval()
|
||||
|
||||
# Create dummy input if not provided
|
||||
if dummy_input is None:
|
||||
dummy_input = torch.randn(
|
||||
1,
|
||||
self.config.lookback,
|
||||
self.config.input_dim
|
||||
)
|
||||
|
||||
# Ensure dummy input is on CPU
|
||||
dummy_input = dummy_input.cpu()
|
||||
|
||||
# Export to ONNX
|
||||
torch.onnx.export(
|
||||
self,
|
||||
dummy_input,
|
||||
path,
|
||||
export_params=True,
|
||||
opset_version=14,
|
||||
do_constant_folding=True,
|
||||
input_names=['input'],
|
||||
output_names=['output'],
|
||||
dynamic_axes={
|
||||
'input': {0: 'batch_size'},
|
||||
'output': {0: 'batch_size'}
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Model exported to ONNX: {path}")
|
||||
|
||||
def count_parameters(self) -> int:
|
||||
"""Count trainable parameters.
|
||||
|
||||
Returns:
|
||||
Number of trainable parameters
|
||||
"""
|
||||
return sum(p.numel() for p in self.parameters() if p.requires_grad)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test base class (with a dummy implementation)
|
||||
class DummyModel(BasePredictor):
|
||||
def __init__(self, config: ModelConfig):
|
||||
super().__init__(config)
|
||||
self.fc = nn.Linear(config.input_dim * config.lookback, 1)
|
||||
if config.task == 'classification':
|
||||
self.activation = nn.Sigmoid()
|
||||
else:
|
||||
self.activation = nn.Identity()
|
||||
|
||||
def forward(self, x):
|
||||
batch_size = x.size(0)
|
||||
x = x.view(batch_size, -1)
|
||||
x = self.fc(x)
|
||||
return self.activation(x)
|
||||
|
||||
# Test
|
||||
config = ModelConfig(input_dim=10, lookback=168, task='classification')
|
||||
model = DummyModel(config)
|
||||
|
||||
print(f"Parameters: {model.count_parameters()}")
|
||||
|
||||
# Test forward pass
|
||||
x = torch.randn(32, 168, 10)
|
||||
output = model(x)
|
||||
print(f"Output shape: {output.shape}")
|
||||
Reference in new issue
Block a user