Add Deep Learning (LSTM) + Quick Wins automation features
Major improvements: - Deep Learning integration with PyTorch LSTM (dual models: main 1-50 + euro 1-12) - Hybrid predictor: 40% RandomForest + 60% Deep Learning - LaunchAgent for automatic weekly tip generation (Mon/Thu 21:00) - Health-Check system with auto-recovery and Telegram alerts - Fixed health checks for Eurojackpot-specific paths and file names - 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) - Separate LSTM models for main numbers (1-50) and euro numbers (1-12) - Apple Silicon MPS acceleration support - Sequence learning with 20-draw history - Health-check adapted for eurojackpot_ml_models/ and learning_log.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user