Cybersecurity-Projects/PROJECTS/advanced/ai-threat-detection/backend/ml/splitting.py

102 lines
2.6 KiB
Python

"""
©AngelaMos | 2026
splitting.py
Stratified train/val/test splitting with SMOTE
oversampling for imbalanced attack data
prepare_training_data performs a 70/15/15 stratified split
preserving class ratios, extracts the normal-only subset
from training data for the autoencoder and isolation
forest, and conditionally applies SMOTE oversampling to
the training set when the minority class ratio falls below
the target strategy (default 0.3). SMOTE is skipped if the
minority class has fewer than k_neighbors+1 samples.
Returns a TrainingSplit dataclass with X_train, y_train,
X_val, y_val, X_test, y_test, and X_normal_train arrays
Connects to:
ml/orchestrator - called at the start of the training
pipeline
"""
from dataclasses import dataclass
import numpy as np
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split
@dataclass
class TrainingSplit:
"""
Result of stratified splitting with SMOTE oversampling
"""
X_train: np.ndarray
y_train: np.ndarray
X_val: np.ndarray
y_val: np.ndarray
X_test: np.ndarray
y_test: np.ndarray
X_normal_train: np.ndarray
def prepare_training_data(
X: np.ndarray,
y: np.ndarray,
train_ratio: float = 0.70,
val_ratio: float = 0.15,
smote_strategy: float = 0.3,
smote_k: int = 5,
random_state: int = 42,
) -> TrainingSplit:
"""
Split data into train/val/test with SMOTE on training set only
"""
n_classes = len(np.unique(y))
if n_classes < 2:
raise ValueError("y must contain at least 2 classes")
test_size = 1.0 - train_ratio
X_train, X_rem, y_train, y_rem = train_test_split(
X,
y,
test_size=test_size,
stratify=y,
random_state=random_state,
)
X_val, X_test, y_val, y_test = train_test_split(
X_rem,
y_rem,
test_size=0.5,
stratify=y_rem,
random_state=random_state,
)
X_normal_train = X_train[y_train == 0]
class_counts = np.bincount(y_train)
minority_count = class_counts.min()
majority_count = class_counts.max()
current_ratio = minority_count / majority_count
if (minority_count >= smote_k + 1 and current_ratio < smote_strategy):
sampler = SMOTE(
sampling_strategy=smote_strategy,
k_neighbors=smote_k,
random_state=random_state,
)
X_train, y_train = sampler.fit_resample(X_train, y_train)
return TrainingSplit(
X_train=X_train,
y_train=y_train,
X_val=X_val,
y_val=y_val,
X_test=X_test,
y_test=y_test,
X_normal_train=X_normal_train,
)