A feature selection and optimization framework built around ResNet50 embeddings, with the goal of reducing a high-dimensional feature space while preserving the information required for downstream classification.
The project started as part of a deepfake detection pipeline. After extracting 2048-dimensional feature vectors from ResNet50, the next challenge was figuring out how to reduce that feature space before feeding the representations into computationally expensive temporal models.
Instead of applying a fixed dimensionality-reduction technique or manually selecting features, I built a configurable Genetic Algorithm framework that treats feature selection as an optimization problem.
A pretrained ResNet50 produces a 2048-dimensional feature vector for each input image.
That representation works well for image-level classification, but using all 2048 features for every frame becomes expensive when the same representations are extended to video sequences and recurrent models such as LSTM or Bi-GRU.
The goal was therefore not simply to reduce the dimensionality.
The goal was to find a smaller subset of the original features that provides a strong classification signal while reducing the computational cost of the downstream pipeline.
This led to the following optimization problem:
2048 ResNet50 Features
↓
Feature Selection
↓
Reduced Feature Space
↓
KNN Evaluation
↓
Genetic Algorithm Optimization
A brute-force search over 2048 binary features would require evaluating:
2^2048
possible feature subsets.
That search space is far beyond what can be exhaustively explored.
Instead, the problem was formulated as a binary optimization problem where every candidate solution represents a possible subset of the ResNet50 feature space.
Each solution is represented as a chromosome:
[1, 0, 1, 1, 0, 0, 1, ...]
Where:
1means the feature is selected0means the feature is excluded- The chromosome length is 2048
The Genetic Algorithm then evolves these solutions toward better-performing feature subsets.
Rather than relying on a single predefined configuration, I designed the framework so that different evolutionary strategies could be combined and evaluated independently.
The framework includes multiple strategies for each major component of the evolutionary process.
Five initialization strategies were implemented:
- Random Initialization
- Pearson Correlation-Based Initialization
- Hybrid Initialization
- Sparse / Balanced Initialization
- Opposition-Based Initialization
The idea was to test whether giving the population some prior information about feature relevance could improve convergence compared with purely random initialization.
The sparse initialization strategy was particularly useful for encouraging solutions that activate only a small percentage of the available features.
Eight parent-selection strategies were implemented:
- Tournament Selection
- Rank Selection
- Roulette Wheel Selection
- Stochastic Universal Sampling
- Truncation Selection
- Boltzmann Selection
- Sigma Scaling
- Random Selection
Each strategy provides a different balance between exploration and exploitation.
This makes the framework suitable for comparing how selection pressure affects convergence and feature-space reduction.
Seven crossover mechanisms were implemented:
- Single-Point Crossover
- Two-Point Crossover
- Three-Point Crossover
- Uniform Crossover
- Half Uniform Crossover (HUX)
- Shuffle Crossover
- Consensus Crossover
HUX was implemented to exchange approximately half of the differing genes between two parents.
Shuffle crossover was used to reduce positional bias by randomly rearranging the chromosome before applying crossover and restoring the original feature order afterward.
Consensus crossover preserves genes where both parents agree and introduces variation where they disagree.
Eight mutation mechanisms were implemented, ranging from simple bit-level changes to more adaptive exploration strategies.
These include:
- Bit-Flip Mutation
- Random Mutation
- Heavy-Tailed Mutation
- Adaptive Mutation
- and additional mutation configurations for controlling exploration intensity
The Heavy-Tailed approach is particularly useful when the population becomes trapped around local optima because it can occasionally introduce much larger changes to a chromosome.
Adaptive mutation changes the mutation behavior as evolution progresses, allowing the search to move from broader exploration toward more focused refinement.
The framework also includes multiple strategies for deciding which individuals survive to the next generation.
The implemented approaches include:
- Elitism
- Steady-State Replacement
- Tournament Survival
- Generational Replacement
- Mu + Lambda
- Mu, Lambda
- Roulette-Based Replacement
- Rank-Based Replacement
- Aging
- Deterministic Crowding
Some of these strategies were implemented with multiple configurations, resulting in 13 experimental survival configurations.
Deterministic Crowding was implemented using Hamming distance to compare offspring with their closest parents, allowing similar individuals to compete directly rather than allowing the entire population to converge toward the same region of the search space.
The Aging strategy assigns a lifetime to individuals and removes them after reaching a predefined age, encouraging continuous exploration of new solutions.
The fitness function evaluates each selected feature subset using K-Nearest Neighbors with 5-fold cross-validation.
Multiple values of K were evaluated rather than relying on a single classifier configuration.
This gives the evolutionary process a measurable objective:
Feature Subset
↓
Select ResNet50 Features
↓
KNN
↓
5-Fold Cross-Validation
↓
Fitness Score
The fitness score is then used to determine which feature subsets should influence the next generation.
One of the main reasons for building the framework was the size of the resulting configuration space.
Combining the different hyperparameter dimensions produced:
6,552,000 unique experimental configurations
These configurations were generated from combinations of:
- Population size
- Mutation rate
- Crossover rate
- Initialization strategy
- Selection strategy
- Crossover strategy
- Mutation strategy
- Survival strategy
- Survival configuration
The objective was not to randomly run millions of experiments without structure.
The framework was designed so that this large search space could be systematically explored while keeping the individual components modular and replaceable.
Running these experiments sequentially would make the search impractical.
To address this, the execution layer was built around multiprocessing using joblib, allowing multiple configurations to run concurrently across available CPU cores.
The architecture separates the evolutionary logic from the experiment orchestration layer, allowing configurations to be distributed across workers independently.
Experiment Space
↓
Configuration Generator
↓
Parallel Workers
↓
Genetic Algorithm
↓
Fitness Evaluation
↓
Result Aggregation
Parallel execution introduced another problem.
Multiple workers needed to continuously write their results into a shared CSV file.
Without synchronization, multiple processes could attempt to write at the same time, resulting in corrupted or incomplete output.
This caused a real failure during development and resulted in several hours of computation being lost.
The solution was to introduce a shared Mutex using:
multiprocessing.Manager().Lock()The logging pipeline was changed so that each worker:
Worker
↓
Acquire Lock
↓
Write Result
↓
Release Lock
Only one worker can write to the shared output at a time, while the computationally expensive parts of the experiment remain parallelized.
This allowed the system to maintain data integrity without serializing the entire experiment.
After profiling the system, the main bottleneck was no longer the Genetic Algorithm itself.
The expensive operation was the repeated distance computation required by KNN.
For every candidate feature subset, the system had to perform distance calculations across high-dimensional vectors. Repeating this process across thousands of individuals and generations on the CPU made the full experimental space extremely expensive.
To avoid rebuilding the framework later, the distance-computation layer was deliberately isolated from the evolutionary logic.
This means the architecture can replace the CPU-based KNN implementation with GPU-accelerated alternatives such as NVIDIA RAPIDS cuML without changing the core Genetic Algorithm.
The intended architecture is therefore:
Genetic Algorithm
↓
Feature Selection
↓
Fitness Evaluation Interface
↓
┌───────────────┐
│ │
CPU KNN GPU KNN
scikit-learn cuML
│ │
└───────┬───────┘
↓
Fitness Score
This separation makes the framework easier to scale to GPU-based environments.
Another part of the project involved handling model-loading problems caused by custom preprocessing operations inside the ResNet50 pipeline.
One of the main errors encountered was:
Unknown layer: TrueDivide
The issue appeared when attempting to reload a saved .h5 model containing operations that Keras could not reconstruct automatically.
To make the loading process more robust, several compatibility mechanisms were implemented.
Custom fallback layers such as GenericLayer and CustomInputLayer were introduced to absorb unsupported configuration parameters and behave as pass-through layers when necessary.
A patch_h5_model_config function was implemented to directly inspect and clean problematic JSON configuration data inside the H5 file before passing the model to Keras.
The loading system follows a fallback strategy:
Direct Model Loading
↓
If Failed
↓
Patch H5 Configuration
↓
If Failed
↓
Rebuild ResNet50 Architecture
↓
Load Compatible Weights
This approach makes the pipeline more resilient to serialization differences between environments and library versions.
The project was also developed across different environments, which introduced platform-specific issues.
One example was the use of NVIDIA RAPIDS and cuML, which has different installation constraints depending on the operating system.
Windows multiprocessing also introduced process-spawning issues that caused repeated RuntimeError failures.
These were addressed by restructuring the execution entry point using the standard Python main-process guard:
if __name__ == "__main__":
...This ensured that worker processes could be created correctly without recursively re-executing the main program.
The most important part of this project was not implementing a Genetic Algorithm from a textbook definition.
The challenge was turning the idea into an experimental system that could actually operate at scale.
That required solving several problems independently:
- Representing a 2048-dimensional feature-selection problem as a binary chromosome
- Designing interchangeable evolutionary operators
- Managing millions of experimental configurations
- Parallelizing experiments across CPU workers
- Preventing race conditions during result logging
- Identifying KNN distance calculations as the main computational bottleneck
- Separating fitness evaluation from the evolutionary engine
- Preparing the architecture for GPU acceleration
- Handling model serialization failures
- Making the pipeline more robust across different execution environments
The overall framework can be summarized as:
ResNet50
↓
2048-D Feature Vector
↓
Binary Feature Mask
↓
┌─────────────────────┐
│ Genetic Algorithm │
│ │
│ Initialization │
│ Selection │
│ Crossover │
│ Mutation │
│ Survival │
└──────────┬──────────┘
↓
Feature Subset
↓
KNN Evaluation
↓
Cross-Validation
↓
Fitness
↓
Next Generation
The framework is designed so that the feature-selection engine remains independent from the classifier and distance-computation backend.
This makes it possible to experiment with different evaluation methods without rewriting the evolutionary core.
This project started with a practical problem in a deepfake detection pipeline: a 2048-dimensional ResNet50 representation was too expensive to carry directly into more complex temporal models.
The solution evolved into a general-purpose feature optimization framework.
Instead of manually selecting features or applying a fixed dimensionality-reduction technique, the system searches for useful feature subsets using configurable evolutionary strategies.
The result is a modular framework capable of exploring a configuration space of more than 6.5 million experimental combinations while supporting parallel execution, synchronized result logging, and a future transition from CPU-based fitness evaluation to GPU acceleration.
The main lesson from the project was that optimization at this scale is not only an algorithmic problem.
It becomes a systems-engineering problem involving computation, memory, parallelism, synchronization, serialization, and scalability.