Files
Lotto-Tip-Generator/scripts/utils/deep_learning_engine.py
T
cbazzaandClaude Sonnet 4.5 428403557e Add Deep Learning (LSTM) + Quick Wins automation features
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>
2026-01-07 09:20:49 +01:00

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