🎯 项目成果: - 准确率: 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>
183 lines
5.1 KiB
Python
183 lines
5.1 KiB
Python
"""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}")
|