""" Training script for Anomaly Detection using Autoencoder. Note: Ensure TensorFlow version matches production environment for compatibility. Recommended: tensorflow>=2.13.0,<3.0.0 """ import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler from tensorflow import keras from tensorflow.keras import Model, Input from tensorflow.keras.layers import Dense import joblib import os import warnings from preprocessing import engineer_features, get_feature_columns # Suppress TensorFlow warnings warnings.filterwarnings('ignore', category=UserWarning) os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' def train_anomaly_detector( train_csv_paths: list, output_dir: str = 'models', encoding_dim: int = 32, epochs: int = 50, batch_size: int = 256, random_state: int = 42 ): """ Train autoencoder for anomaly detection. Args: train_csv_paths: List of CSV file paths to load output_dir: Directory to save model artifacts encoding_dim: Dimension of encoding layer epochs: Training epochs batch_size: Batch size random_state: Random seed """ os.makedirs(output_dir, exist_ok=True) np.random.seed(random_state) # Load and combine data print("Loading data...") dfs = [] for csv_path in train_csv_paths: if os.path.exists(csv_path): df = pd.read_csv(csv_path) dfs.append(df) print(f" Loaded {len(df)} rows from {csv_path}") else: print(f" Warning: {csv_path} not found, skipping") if not dfs: raise ValueError("No data files found!") df = pd.concat(dfs, ignore_index=True) print(f"Total rows: {len(df)}") # Engineer features print("Engineering features...") df = engineer_features(df) # Get numeric features for autoencoder feature_cols = get_feature_columns() num_cols = feature_cols['anomaly_numeric'] # Ensure all columns exist for col in num_cols: if col not in df.columns: df[col] = 0.0 X = df[num_cols].copy() X = X.fillna(0.0) print(f"Anomaly features: {num_cols}") print(f"Feature matrix shape: {X.shape}") # Scale features print("Scaling features...") scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Build autoencoder print("Building autoencoder...") input_dim = X_scaled.shape[1] input_layer = Input(shape=(input_dim,)) encoded = Dense(64, activation='relu')(input_layer) encoded = Dense(encoding_dim, activation='relu')(encoded) decoded = Dense(64, activation='relu')(encoded) decoded = Dense(input_dim, activation='linear')(decoded) autoencoder = Model(input_layer, decoded) autoencoder.compile(optimizer='adam', loss='mse') print(f"Autoencoder architecture:") autoencoder.summary() # Train print("Training autoencoder...") history = autoencoder.fit( X_scaled, X_scaled, epochs=epochs, batch_size=batch_size, validation_split=0.1, verbose=1 ) # Compute reconstruction errors print("Computing reconstruction errors...") X_pred = autoencoder.predict(X_scaled, verbose=0) recon_errors = np.mean((X_scaled - X_pred) ** 2, axis=1) df['recon_error'] = recon_errors # Compute per-job thresholds (97th percentile) df['anom_threshold_job'] = df.groupby('job_nm')['recon_error'].transform( lambda s: np.percentile(s, 97) if len(s) > 0 else np.percentile(recon_errors, 97) ) df['is_anomaly_job'] = (df['recon_error'] > df['anom_threshold_job']).astype(int) print(f"\nReconstruction error stats:") print(f" Mean: {recon_errors.mean():.6f}") print(f" Std: {recon_errors.std():.6f}") print(f" Min: {recon_errors.min():.6f}") print(f" Max: {recon_errors.max():.6f}") print(f" 97th percentile: {np.percentile(recon_errors, 97):.6f}") print(f" Anomalies detected: {df['is_anomaly_job'].sum()} ({df['is_anomaly_job'].mean()*100:.2f}%)") # Save model and artifacts # Save in Keras format with explicit save_format for compatibility model_path = os.path.join(output_dir, 'anomaly_autoencoder_cpu.keras') print(f"\nSaving model...") print(f"TensorFlow version: {keras.__version__ if hasattr(keras, '__version__') else 'unknown'}") try: # Save model without compiling to avoid version-specific config issues # This ensures better compatibility across TensorFlow versions autoencoder.save( model_path, save_format='keras', include_optimizer=False # Don't save optimizer for inference-only models ) print(f"✓ Autoencoder saved to {model_path} (Keras format, no optimizer)") except TypeError: # If include_optimizer is not supported, try without it try: autoencoder.save(model_path, save_format='keras') print(f"✓ Autoencoder saved to {model_path} (Keras format)") except Exception as e: # Fallback: save without explicit format try: autoencoder.save(model_path) print(f"✓ Autoencoder saved to {model_path} (default format)") except Exception as e2: print(f"✗ Error saving model: {e2}") raise except Exception as e: print(f"✗ Error saving model: {e}") raise scaler_path = os.path.join(output_dir, 'anomaly_scaler.joblib') joblib.dump(scaler, scaler_path) print(f"Scaler saved to {scaler_path}") feature_list_path = os.path.join(output_dir, 'anomaly_features.joblib') joblib.dump(num_cols, feature_list_path) print(f"Feature list saved to {feature_list_path}") # Save global threshold (97th percentile) global_threshold = np.percentile(recon_errors, 97) threshold_path = os.path.join(output_dir, 'anomaly_threshold.joblib') joblib.dump(global_threshold, threshold_path) print(f"Global threshold saved to {threshold_path} (value: {global_threshold:.6f})") return autoencoder, scaler, X_scaled, recon_errors if __name__ == '__main__': import sys import glob # Default CSV paths (can be overridden via command line) if len(sys.argv) > 1: # Handle glob patterns (works in both PowerShell and bash) csv_paths = [] for arg in sys.argv[1:]: # Expand glob patterns expanded = glob.glob(arg) if expanded: csv_paths.extend(expanded) else: # If no match, use as-is (might be a specific file) csv_paths.append(arg) else: # Default: all CSV files in data directory csv_paths = glob.glob('data/*.csv') if not csv_paths: csv_paths = [ 'data/true_export_report_20260120.csv', 'data/true_export_report_20260121.csv' ] if not csv_paths: print("Error: No CSV files found!") print("Usage: python train_anomaly.py [file1.csv] [file2.csv] ...") print(" or: python train_anomaly.py data/*.csv") sys.exit(1) print(f"Training with {len(csv_paths)} file(s):") for path in csv_paths: print(f" - {path}") print() train_anomaly_detector(csv_paths)