Major improvements: - Deep Learning integration with PyTorch LSTM (Bidirectional, 128→64 units) - Hybrid predictor: 40% RandomForest + 60% Deep Learning - LaunchAgent for automatic weekly tip generation (Tue/Fri 21:00) - Health-Check system with auto-recovery and Telegram alerts - Model caching and intelligent retraining logic - Updated CSV data and generated tips - Performance reports for recent draws Technical details: - PyTorch used instead of TensorFlow (Python 3.14 compatibility) - Apple Silicon MPS acceleration support - Sequence learning with 20-draw history - Early stopping and learning rate scheduling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
474 lines
15 KiB
Python
474 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Deep Learning Engine mit LSTM für Lotto-Vorhersagen
|
|
====================================================
|
|
|
|
Implementiert LSTM-basierte Modelle zur Vorhersage von Lotto-Zahlen
|
|
basierend auf historischen Sequenzen und Features.
|
|
|
|
Features:
|
|
- LSTM-Netzwerk für zeitliche Sequenzen
|
|
- Sequence-to-Probability Mapping
|
|
- Feature Engineering Integration
|
|
- Model Persistence & Caching
|
|
- Hybrid mit RandomForest
|
|
"""
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import os
|
|
import json
|
|
import pickle
|
|
from datetime import datetime
|
|
from typing import Dict, List, Tuple, Optional
|
|
import warnings
|
|
warnings.filterwarnings('ignore')
|
|
|
|
# TensorFlow/Keras Imports
|
|
try:
|
|
import tensorflow as tf
|
|
from tensorflow import keras
|
|
from tensorflow.keras.models import Sequential, load_model
|
|
from tensorflow.keras.layers import LSTM, Dense, Dropout, Bidirectional, BatchNormalization
|
|
from tensorflow.keras.optimizers import Adam
|
|
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
|
|
from tensorflow.keras.regularizers import l2
|
|
TENSORFLOW_AVAILABLE = True
|
|
except ImportError:
|
|
TENSORFLOW_AVAILABLE = False
|
|
print("⚠️ TensorFlow not available. Install with: pip install tensorflow")
|
|
|
|
|
|
class DeepLearningEngine:
|
|
"""
|
|
LSTM-basierter Deep Learning Engine für Lotto-Vorhersagen.
|
|
|
|
Architecture:
|
|
- Input: Sequence of historical draws + features
|
|
- LSTM layers: Learn temporal patterns
|
|
- Dense layers: Map to probability distribution
|
|
- Output: Probability for each number
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
num_numbers: int = 49,
|
|
sequence_length: int = 20,
|
|
cache_dir: str = None,
|
|
fast_mode: bool = True
|
|
):
|
|
"""
|
|
Args:
|
|
num_numbers: Maximum number (49 for Lotto, 50 for Eurojackpot)
|
|
sequence_length: How many past draws to consider
|
|
cache_dir: Directory for model persistence
|
|
fast_mode: Use faster training (fewer epochs)
|
|
"""
|
|
self.num_numbers = num_numbers
|
|
self.sequence_length = sequence_length
|
|
self.cache_dir = cache_dir
|
|
self.fast_mode = fast_mode
|
|
|
|
# Model components
|
|
self.model = None
|
|
self.is_trained = False
|
|
self.training_history = {}
|
|
|
|
# Configuration
|
|
self.config = {
|
|
'lstm_units_1': 128,
|
|
'lstm_units_2': 64,
|
|
'dense_units': 128,
|
|
'dropout_rate': 0.3,
|
|
'learning_rate': 0.001,
|
|
'batch_size': 32,
|
|
'epochs': 30 if fast_mode else 100,
|
|
'validation_split': 0.2
|
|
}
|
|
|
|
if cache_dir:
|
|
os.makedirs(cache_dir, exist_ok=True)
|
|
self.model_path = os.path.join(cache_dir, f'lstm_model_{num_numbers}.h5')
|
|
self.config_path = os.path.join(cache_dir, f'lstm_config_{num_numbers}.json')
|
|
else:
|
|
self.model_path = None
|
|
self.config_path = None
|
|
|
|
print(f"🧠 Deep Learning Engine initialized")
|
|
print(f" Numbers: 1-{num_numbers}")
|
|
print(f" Sequence Length: {sequence_length}")
|
|
print(f" Fast Mode: {fast_mode}")
|
|
|
|
def _build_model(self, num_features: int) -> Sequential:
|
|
"""
|
|
Builds LSTM architecture.
|
|
|
|
Architecture:
|
|
Input (sequence_length, num_features)
|
|
↓
|
|
Bidirectional LSTM(128) + Dropout(0.3)
|
|
↓
|
|
Bidirectional LSTM(64) + Dropout(0.3)
|
|
↓
|
|
Dense(128, relu) + BatchNorm + Dropout(0.3)
|
|
↓
|
|
Dense(num_numbers, sigmoid)
|
|
"""
|
|
model = Sequential([
|
|
# First Bidirectional LSTM layer
|
|
Bidirectional(
|
|
LSTM(
|
|
self.config['lstm_units_1'],
|
|
return_sequences=True,
|
|
kernel_regularizer=l2(0.01)
|
|
),
|
|
input_shape=(self.sequence_length, num_features)
|
|
),
|
|
Dropout(self.config['dropout_rate']),
|
|
BatchNormalization(),
|
|
|
|
# Second Bidirectional LSTM layer
|
|
Bidirectional(
|
|
LSTM(
|
|
self.config['lstm_units_2'],
|
|
return_sequences=False,
|
|
kernel_regularizer=l2(0.01)
|
|
)
|
|
),
|
|
Dropout(self.config['dropout_rate']),
|
|
BatchNormalization(),
|
|
|
|
# Dense layers
|
|
Dense(
|
|
self.config['dense_units'],
|
|
activation='relu',
|
|
kernel_regularizer=l2(0.01)
|
|
),
|
|
BatchNormalization(),
|
|
Dropout(self.config['dropout_rate']),
|
|
|
|
# Output layer - probability for each number
|
|
Dense(self.num_numbers, activation='sigmoid')
|
|
])
|
|
|
|
# Compile
|
|
model.compile(
|
|
optimizer=Adam(learning_rate=self.config['learning_rate']),
|
|
loss='binary_crossentropy',
|
|
metrics=['accuracy', 'AUC']
|
|
)
|
|
|
|
return model
|
|
|
|
def _prepare_sequences(
|
|
self,
|
|
df: pd.DataFrame,
|
|
features_df: pd.DataFrame
|
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
"""
|
|
Prepares sequences for LSTM training.
|
|
|
|
Args:
|
|
df: Historical draws (with columns Z1-Z6)
|
|
features_df: Engineered features
|
|
|
|
Returns:
|
|
X: (num_samples, sequence_length, num_features)
|
|
y: (num_samples, num_numbers) - binary matrix
|
|
"""
|
|
print(f" Preparing sequences (length={self.sequence_length})...")
|
|
|
|
# Ensure data is sorted by date
|
|
if 'datum' in df.columns:
|
|
df = df.sort_values('datum').reset_index(drop=True)
|
|
|
|
# Extract number columns
|
|
num_cols = [col for col in df.columns if col.startswith('Z')]
|
|
|
|
# Normalize features to [0, 1]
|
|
features_normalized = features_df.copy()
|
|
for col in features_normalized.columns:
|
|
min_val = features_normalized[col].min()
|
|
max_val = features_normalized[col].max()
|
|
if max_val > min_val:
|
|
features_normalized[col] = (features_normalized[col] - min_val) / (max_val - min_val)
|
|
else:
|
|
features_normalized[col] = 0.5
|
|
|
|
X_sequences = []
|
|
y_targets = []
|
|
|
|
# Create sequences
|
|
for i in range(self.sequence_length, len(df)):
|
|
# Get sequence of features
|
|
sequence = features_normalized.iloc[i - self.sequence_length:i].values
|
|
X_sequences.append(sequence)
|
|
|
|
# Target: next draw as binary vector
|
|
target = np.zeros(self.num_numbers)
|
|
next_draw = df.iloc[i][num_cols].values
|
|
for num in next_draw:
|
|
if 1 <= num <= self.num_numbers:
|
|
target[int(num) - 1] = 1
|
|
y_targets.append(target)
|
|
|
|
X = np.array(X_sequences)
|
|
y = np.array(y_targets)
|
|
|
|
print(f" ✅ Created {len(X)} sequences")
|
|
print(f" Shape: X={X.shape}, y={y.shape}")
|
|
|
|
return X, y
|
|
|
|
def train(
|
|
self,
|
|
df: pd.DataFrame,
|
|
features_df: pd.DataFrame,
|
|
force_retrain: bool = False
|
|
) -> bool:
|
|
"""
|
|
Trains LSTM model on historical data.
|
|
|
|
Args:
|
|
df: Historical draws
|
|
features_df: Engineered features
|
|
force_retrain: Retrain even if cached model exists
|
|
|
|
Returns:
|
|
Success status
|
|
"""
|
|
if not TENSORFLOW_AVAILABLE:
|
|
print("❌ TensorFlow not available")
|
|
return False
|
|
|
|
# Check for cached model
|
|
if not force_retrain and self.model_path and os.path.exists(self.model_path):
|
|
print(" 📦 Loading cached LSTM model...")
|
|
try:
|
|
self.model = load_model(self.model_path)
|
|
self.is_trained = True
|
|
|
|
# Load config
|
|
if os.path.exists(self.config_path):
|
|
with open(self.config_path, 'r') as f:
|
|
self.training_history = json.load(f)
|
|
|
|
print(f" ✅ Loaded cached model")
|
|
return True
|
|
except Exception as e:
|
|
print(f" ⚠️ Failed to load cached model: {e}")
|
|
print(" 🔄 Training new model...")
|
|
|
|
print(f"\n🧠 TRAINING DEEP LEARNING MODEL (LSTM)")
|
|
print("=" * 70)
|
|
|
|
# Prepare data
|
|
X, y = self._prepare_sequences(df, features_df)
|
|
|
|
if len(X) < 100:
|
|
print(" ⚠️ Not enough data for training (need >100 sequences)")
|
|
return False
|
|
|
|
# Build model
|
|
print(f" Building LSTM architecture...")
|
|
num_features = X.shape[2]
|
|
self.model = self._build_model(num_features)
|
|
|
|
# Show summary
|
|
print(f"\n 📊 Model Summary:")
|
|
total_params = self.model.count_params()
|
|
print(f" Total parameters: {total_params:,}")
|
|
|
|
# Callbacks
|
|
callbacks = [
|
|
EarlyStopping(
|
|
monitor='val_loss',
|
|
patience=10,
|
|
restore_best_weights=True,
|
|
verbose=0
|
|
),
|
|
ReduceLROnPlateau(
|
|
monitor='val_loss',
|
|
factor=0.5,
|
|
patience=5,
|
|
verbose=0
|
|
)
|
|
]
|
|
|
|
# Train
|
|
print(f"\n 🚀 Training for {self.config['epochs']} epochs...")
|
|
print(f" Batch size: {self.config['batch_size']}")
|
|
print(f" Validation split: {self.config['validation_split']:.1%}")
|
|
|
|
try:
|
|
history = self.model.fit(
|
|
X, y,
|
|
batch_size=self.config['batch_size'],
|
|
epochs=self.config['epochs'],
|
|
validation_split=self.config['validation_split'],
|
|
callbacks=callbacks,
|
|
verbose=1
|
|
)
|
|
|
|
# Store training history
|
|
self.training_history = {
|
|
'trained_at': datetime.now().isoformat(),
|
|
'num_samples': len(X),
|
|
'num_features': num_features,
|
|
'final_loss': float(history.history['loss'][-1]),
|
|
'final_val_loss': float(history.history['val_loss'][-1]),
|
|
'final_accuracy': float(history.history['accuracy'][-1]),
|
|
'final_val_accuracy': float(history.history['val_accuracy'][-1]),
|
|
'epochs_trained': len(history.history['loss'])
|
|
}
|
|
|
|
self.is_trained = True
|
|
|
|
# Save model
|
|
if self.model_path:
|
|
print(f"\n 💾 Saving model to cache...")
|
|
self.model.save(self.model_path)
|
|
|
|
with open(self.config_path, 'w') as f:
|
|
json.dump(self.training_history, f, indent=2)
|
|
|
|
print(f" ✅ Model saved")
|
|
|
|
# Print results
|
|
print(f"\n ✅ TRAINING COMPLETED")
|
|
print(f" Final Loss: {self.training_history['final_loss']:.4f}")
|
|
print(f" Final Val Loss: {self.training_history['final_val_loss']:.4f}")
|
|
print(f" Final Accuracy: {self.training_history['final_accuracy']:.4f}")
|
|
print(f" Epochs: {self.training_history['epochs_trained']}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"\n ❌ Training failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def predict(
|
|
self,
|
|
recent_df: pd.DataFrame,
|
|
recent_features: pd.DataFrame
|
|
) -> Dict[int, float]:
|
|
"""
|
|
Predicts probabilities for each number.
|
|
|
|
Args:
|
|
recent_df: Recent draws (at least sequence_length)
|
|
recent_features: Recent features
|
|
|
|
Returns:
|
|
{number: probability} for numbers 1-num_numbers
|
|
"""
|
|
if not self.is_trained or self.model is None:
|
|
print("⚠️ Model not trained, returning uniform distribution")
|
|
return {i: 0.5 for i in range(1, self.num_numbers + 1)}
|
|
|
|
# Prepare last sequence
|
|
if len(recent_df) < self.sequence_length:
|
|
print(f"⚠️ Not enough recent data (need {self.sequence_length}, got {len(recent_df)})")
|
|
return {i: 0.5 for i in range(1, self.num_numbers + 1)}
|
|
|
|
# Get last sequence
|
|
recent_features_normalized = recent_features.copy()
|
|
for col in recent_features_normalized.columns:
|
|
min_val = recent_features_normalized[col].min()
|
|
max_val = recent_features_normalized[col].max()
|
|
if max_val > min_val:
|
|
recent_features_normalized[col] = (recent_features_normalized[col] - min_val) / (max_val - min_val)
|
|
else:
|
|
recent_features_normalized[col] = 0.5
|
|
|
|
sequence = recent_features_normalized.iloc[-self.sequence_length:].values
|
|
X = np.array([sequence]) # Shape: (1, sequence_length, num_features)
|
|
|
|
# Predict
|
|
predictions = self.model.predict(X, verbose=0)[0] # Shape: (num_numbers,)
|
|
|
|
# Convert to dictionary
|
|
result = {i + 1: float(predictions[i]) for i in range(self.num_numbers)}
|
|
|
|
return result
|
|
|
|
def get_model_info(self) -> Dict:
|
|
"""Returns model information."""
|
|
return {
|
|
'is_trained': self.is_trained,
|
|
'tensorflow_available': TENSORFLOW_AVAILABLE,
|
|
'num_numbers': self.num_numbers,
|
|
'sequence_length': self.sequence_length,
|
|
'config': self.config,
|
|
'training_history': self.training_history,
|
|
'model_exists': self.model is not None
|
|
}
|
|
|
|
|
|
class HybridDeepLearningPredictor:
|
|
"""
|
|
Kombiniert RandomForest + LSTM für robustere Vorhersagen.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
dl_engine: DeepLearningEngine,
|
|
rf_weight: float = 0.4,
|
|
dl_weight: float = 0.6
|
|
):
|
|
"""
|
|
Args:
|
|
dl_engine: Deep Learning Engine
|
|
rf_weight: Weight for RandomForest predictions
|
|
dl_weight: Weight for Deep Learning predictions
|
|
"""
|
|
self.dl_engine = dl_engine
|
|
self.rf_weight = rf_weight
|
|
self.dl_weight = dl_weight
|
|
|
|
print(f"🔀 Hybrid Predictor: RF={rf_weight:.1%} + DL={dl_weight:.1%}")
|
|
|
|
def predict(
|
|
self,
|
|
rf_predictions: Dict[int, float],
|
|
recent_df: pd.DataFrame,
|
|
recent_features: pd.DataFrame
|
|
) -> Dict[int, float]:
|
|
"""
|
|
Combines RandomForest and Deep Learning predictions.
|
|
|
|
Args:
|
|
rf_predictions: Predictions from RandomForest
|
|
recent_df: Recent draws for DL
|
|
recent_features: Recent features for DL
|
|
|
|
Returns:
|
|
Combined predictions
|
|
"""
|
|
# Get DL predictions
|
|
dl_predictions = self.dl_engine.predict(recent_df, recent_features)
|
|
|
|
# Combine
|
|
combined = {}
|
|
for num in range(1, self.dl_engine.num_numbers + 1):
|
|
rf_score = rf_predictions.get(num, 0.5)
|
|
dl_score = dl_predictions.get(num, 0.5)
|
|
|
|
combined[num] = (
|
|
self.rf_weight * rf_score +
|
|
self.dl_weight * dl_score
|
|
)
|
|
|
|
# Normalize to [0, 1]
|
|
min_score = min(combined.values())
|
|
max_score = max(combined.values())
|
|
if max_score > min_score:
|
|
combined = {
|
|
num: (score - min_score) / (max_score - min_score)
|
|
for num, score in combined.items()
|
|
}
|
|
|
|
return combined
|