eckity-bert-ga 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, EC-KitY
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: eckity-bert-ga
3
+ Version: 0.1.0
4
+ Summary: BERT mutation operator for EC-KitY genetic algorithms
5
+ Author: EC-KitY
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/EC-KitY/BERT-Mutation-for-GA
8
+ Project-URL: Repository, https://github.com/EC-KitY/BERT-Mutation-for-GA
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: eckity~=0.4.1
21
+ Requires-Dist: numpy>=2.0.2
22
+ Requires-Dist: scipy>=1.13.0
23
+ Requires-Dist: torch>=2.7.1
24
+ Requires-Dist: transformers>=4.50.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: gymnasium>=1.0; extra == "dev"
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: twine>=5.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # BERT Mutation for Genetic Algorithms
33
+
34
+ ## Overview
35
+
36
+ This repository implements the paper `BERT Mutation: Deep Transformer Model for Masked Uniform Mutation in Genetic Algorithms`.
37
+
38
+ The paper proposes a domain-independent mutation operator for genetic algorithms that uses a BERT-style masked model to predict beneficial gene replacements from context. To make this work for fixed-length GA representations, it adds an elite-guided data augmentation mechanism that creates additional learning signal from strong historical solutions.
39
+
40
+ In the paper, the method is evaluated on four domains: Frozen Lake, Artificial Ant, Graph Coloring, and Unweighted Set Cover. The reported results show faster convergence and better final fitness than standard mutation baselines and an adaptive operator-selection baseline, while maintaining meaningful population diversity.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install eckity-bert-ga
46
+ ```
47
+
48
+ ## Using the operator
49
+
50
+ Import the public API from `eckity_bert_ga`:
51
+
52
+ ```python
53
+ from eckity_bert_ga import (
54
+ BertMutation,
55
+ EckityCustomMutation,
56
+ GAIntegerStringVectorCreator,
57
+ )
58
+ ```
59
+
60
+ Create the BERT mutation model and wrap it as an EC-KitY operator:
61
+
62
+ ```python
63
+ population_size = 100
64
+
65
+ bert_mutation = BertMutation(
66
+ max_int_val=MAX_GENE_VALUE,
67
+ get_fitness_func=evaluate_vector,
68
+ context_size=INDIVIDUAL_LENGTH + 1,
69
+ mask_probability=0.1,
70
+ )
71
+
72
+ mutation_operator = EckityCustomMutation(
73
+ mutation_operator=bert_mutation,
74
+ population_size=population_size,
75
+ probability=0.4,
76
+ )
77
+ ```
78
+
79
+ Add `mutation_operator` to an EC-KitY subpopulation's `operators_sequence`. The BERT model is initialized locally from configuration and does not download pretrained model weights.
80
+
81
+ ### Compatibility
82
+
83
+ - Python 3.9 or newer
84
+ - EC-KitY 0.4.x
85
+ - NumPy 2.0.2 or newer
86
+ - SciPy 1.13.0 or newer
87
+ - PyTorch 2.7.1 or newer
88
+ - Transformers 4.50.0 or newer
89
+
90
+ ## Results from the paper
91
+
92
+ ### Fitness by generation
93
+
94
+ The following figure presents representative best-individual fitness curves for BERT Mutation and the mutation baselines across the four evaluated domains.
95
+
96
+ ![Representative fitness curves across the evaluated domains](images/bert_ga.png "Convergence plots")
97
+
98
+ *Best-individual fitness by generation, averaged over 10 runs. Black dots mark the runtime cutoffs.*
99
+
100
+ ### Statistical comparisons
101
+
102
+ The following table presents the paired exact permutation-test results comparing BERT Mutation against each baseline in every evaluated domain.
103
+
104
+ ![Paired exact permutation-test results](images/p_values.png "Permutation-test results")
105
+
106
+ *Holm-corrected p-values for comparisons between BERT Mutation and the baseline mutation operators.*
107
+
108
+ ## Benchmark instances
109
+
110
+ *Benchmark instance sizes used in the experiments. The individual length $L$ denotes the genome length optimized by the GA.*
111
+
112
+ | Domain | Instance | Problem size | Individual length $L$ |
113
+ |---|---|---|---:|
114
+ | **Artificial Ant** | | | |
115
+ | Artificial Ant | `aux_map1` | $20 \times 20$, 91 food cells | 283 |
116
+ | Artificial Ant | `aux_map2` | $20 \times 20$, 69 food cells | 286 |
117
+ | Artificial Ant | `john_muir` | $32 \times 32$, 89 food cells | 200 |
118
+ | Artificial Ant | `los_altos` | $100 \times 100$, 157 food cells | 800 |
119
+ | Artificial Ant | `santafe` | $32 \times 32$, 89 food cells | 400 |
120
+ | **Set Covering** | | | |
121
+ | Set Covering | `scp41` | 200 rows, 1000 columns | 1000 |
122
+ | Set Covering | `scp51` | 200 rows, 2000 columns | 2000 |
123
+ | Set Covering | `scp52` | 200 rows, 2000 columns | 2000 |
124
+ | Set Covering | `scp53` | 200 rows, 2000 columns | 2000 |
125
+ | Set Covering | `scp54` | 200 rows, 2000 columns | 2000 |
126
+ | Set Covering | `scp56` | 200 rows, 2000 columns | 2000 |
127
+ | Set Covering | `scp57` | 200 rows, 2000 columns | 2000 |
128
+ | Set Covering | `scp64` | 200 rows, 1000 columns | 1000 |
129
+ | Set Covering | `scp65` | 200 rows, 1000 columns | 1000 |
130
+ | **Frozen Lake** | | | |
131
+ | Frozen Lake | `default` | $8 \times 8$ grid, 64 states | 64 |
132
+ | Frozen Lake | `rand10x10` | $10 \times 10$ grid, 100 states | 100 |
133
+ | Frozen Lake | `rand7x7` | $7 \times 7$ grid, 49 states | 49 |
134
+ | Frozen Lake | `rand8x8` | $8 \times 8$ grid, 64 states | 64 |
135
+ | Frozen Lake | `rand9x9` | $9 \times 9$ grid, 81 states | 81 |
136
+ | **Graph Coloring** | | | |
137
+ | Graph Coloring | `games120` | 120 vertices, 1276 edges | 120 |
138
+ | Graph Coloring | `myciel7` | 191 vertices, 2360 edges | 191 |
139
+ | Graph Coloring | `le450_5a` | 450 vertices, 5714 edges | 450 |
140
+ | Graph Coloring | `mulsol.i.2` | 188 vertices, 3885 edges | 188 |
141
+ | Graph Coloring | `zeroin.i.1` | 211 vertices, 4100 edges | 211 |
142
+ | Graph Coloring | `zeroin.i.2` | 211 vertices, 3541 edges | 211 |
143
+
144
+ ## Getting started
145
+
146
+ ### Requirements
147
+
148
+ - Python 3.9 or newer
149
+ - A working PyTorch installation
150
+
151
+ The code depends on:
152
+
153
+ - `numpy`
154
+ - `gymnasium`
155
+ - `torch`
156
+ - `transformers`
157
+ - `eckity`
158
+
159
+ ### Installing for repository development
160
+
161
+ From the repository root:
162
+
163
+ ```bash
164
+ python -m pip install --upgrade pip
165
+ python -m pip install -e ".[dev]"
166
+ ```
167
+
168
+ ## Running the experiments
169
+
170
+ Artificial Ant:
171
+
172
+ ```bash
173
+ python example_runner.py
174
+ python -m dnm_paper.experiments.artificial_ant
175
+ ```
176
+
177
+ Frozen Lake:
178
+
179
+ ```bash
180
+ python example_runner_frozen_lake.py
181
+ python -m dnm_paper.experiments.frozen_lake
182
+ ```
183
+
184
+ Useful options:
185
+
186
+ ```bash
187
+ python -m dnm_paper.experiments.artificial_ant --generations 100 --runs 3 --population-size 6
188
+ python -m dnm_paper.experiments.artificial_ant --maps-dir artifical_ant_maps --output-dir experiments/artificial_ant/runs
189
+ python -m dnm_paper.experiments.frozen_lake --generations 10 --runs 1 --population-size 100 --total-episodes 2000
190
+ ```
191
+
192
+ By default, results are written under `experiments/artificial_ant/runs/<map_name>/bert_mutation/<run_id>/results.json`.
193
+ Frozen Lake results are written under `experiments/frozen_lake/runs/<instance_name>/bert_mutation/<run_id>/results.json`.
194
+
195
+ The experiment modules, benchmark maps, datasets, and result figures are repository resources and are not included in the `eckity-bert-ga` wheel.
196
+
197
+ Release preparation and manual PyPI upload commands are documented in [`RELEASING.md`](RELEASING.md).
198
+
199
+ ## Project structure
200
+
201
+ ```text
202
+ dnm_paper/
203
+ config.py Experiment configuration and default paths
204
+ individuals.py Custom ECKITY individual creator
205
+ logging_utils.py JSON statistics logger
206
+ experiments/
207
+ common.py Shared experiment helpers and mutation builder
208
+ artificial_ant.py CLI entry point and experiment orchestration
209
+ frozen_lake.py CLI entry point and experiment orchestration
210
+ mutation/
211
+ bert.py BERT-based mutation operator
212
+ eckity_adapter.py Adapter that plugs the mutation operator into ECKITY
213
+ problems/
214
+ artificial_ant.py Artificial ant map loader and evaluator
215
+ frozen_lake.py Frozen Lake evaluator
216
+ frozen_lake_instances.py Named Frozen Lake benchmark instances
217
+ artifical_ant_maps/ Benchmark map files
218
+ pyproject.toml Package metadata and dependencies
219
+ ```
@@ -0,0 +1,188 @@
1
+ # BERT Mutation for Genetic Algorithms
2
+
3
+ ## Overview
4
+
5
+ This repository implements the paper `BERT Mutation: Deep Transformer Model for Masked Uniform Mutation in Genetic Algorithms`.
6
+
7
+ The paper proposes a domain-independent mutation operator for genetic algorithms that uses a BERT-style masked model to predict beneficial gene replacements from context. To make this work for fixed-length GA representations, it adds an elite-guided data augmentation mechanism that creates additional learning signal from strong historical solutions.
8
+
9
+ In the paper, the method is evaluated on four domains: Frozen Lake, Artificial Ant, Graph Coloring, and Unweighted Set Cover. The reported results show faster convergence and better final fitness than standard mutation baselines and an adaptive operator-selection baseline, while maintaining meaningful population diversity.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install eckity-bert-ga
15
+ ```
16
+
17
+ ## Using the operator
18
+
19
+ Import the public API from `eckity_bert_ga`:
20
+
21
+ ```python
22
+ from eckity_bert_ga import (
23
+ BertMutation,
24
+ EckityCustomMutation,
25
+ GAIntegerStringVectorCreator,
26
+ )
27
+ ```
28
+
29
+ Create the BERT mutation model and wrap it as an EC-KitY operator:
30
+
31
+ ```python
32
+ population_size = 100
33
+
34
+ bert_mutation = BertMutation(
35
+ max_int_val=MAX_GENE_VALUE,
36
+ get_fitness_func=evaluate_vector,
37
+ context_size=INDIVIDUAL_LENGTH + 1,
38
+ mask_probability=0.1,
39
+ )
40
+
41
+ mutation_operator = EckityCustomMutation(
42
+ mutation_operator=bert_mutation,
43
+ population_size=population_size,
44
+ probability=0.4,
45
+ )
46
+ ```
47
+
48
+ Add `mutation_operator` to an EC-KitY subpopulation's `operators_sequence`. The BERT model is initialized locally from configuration and does not download pretrained model weights.
49
+
50
+ ### Compatibility
51
+
52
+ - Python 3.9 or newer
53
+ - EC-KitY 0.4.x
54
+ - NumPy 2.0.2 or newer
55
+ - SciPy 1.13.0 or newer
56
+ - PyTorch 2.7.1 or newer
57
+ - Transformers 4.50.0 or newer
58
+
59
+ ## Results from the paper
60
+
61
+ ### Fitness by generation
62
+
63
+ The following figure presents representative best-individual fitness curves for BERT Mutation and the mutation baselines across the four evaluated domains.
64
+
65
+ ![Representative fitness curves across the evaluated domains](images/bert_ga.png "Convergence plots")
66
+
67
+ *Best-individual fitness by generation, averaged over 10 runs. Black dots mark the runtime cutoffs.*
68
+
69
+ ### Statistical comparisons
70
+
71
+ The following table presents the paired exact permutation-test results comparing BERT Mutation against each baseline in every evaluated domain.
72
+
73
+ ![Paired exact permutation-test results](images/p_values.png "Permutation-test results")
74
+
75
+ *Holm-corrected p-values for comparisons between BERT Mutation and the baseline mutation operators.*
76
+
77
+ ## Benchmark instances
78
+
79
+ *Benchmark instance sizes used in the experiments. The individual length $L$ denotes the genome length optimized by the GA.*
80
+
81
+ | Domain | Instance | Problem size | Individual length $L$ |
82
+ |---|---|---|---:|
83
+ | **Artificial Ant** | | | |
84
+ | Artificial Ant | `aux_map1` | $20 \times 20$, 91 food cells | 283 |
85
+ | Artificial Ant | `aux_map2` | $20 \times 20$, 69 food cells | 286 |
86
+ | Artificial Ant | `john_muir` | $32 \times 32$, 89 food cells | 200 |
87
+ | Artificial Ant | `los_altos` | $100 \times 100$, 157 food cells | 800 |
88
+ | Artificial Ant | `santafe` | $32 \times 32$, 89 food cells | 400 |
89
+ | **Set Covering** | | | |
90
+ | Set Covering | `scp41` | 200 rows, 1000 columns | 1000 |
91
+ | Set Covering | `scp51` | 200 rows, 2000 columns | 2000 |
92
+ | Set Covering | `scp52` | 200 rows, 2000 columns | 2000 |
93
+ | Set Covering | `scp53` | 200 rows, 2000 columns | 2000 |
94
+ | Set Covering | `scp54` | 200 rows, 2000 columns | 2000 |
95
+ | Set Covering | `scp56` | 200 rows, 2000 columns | 2000 |
96
+ | Set Covering | `scp57` | 200 rows, 2000 columns | 2000 |
97
+ | Set Covering | `scp64` | 200 rows, 1000 columns | 1000 |
98
+ | Set Covering | `scp65` | 200 rows, 1000 columns | 1000 |
99
+ | **Frozen Lake** | | | |
100
+ | Frozen Lake | `default` | $8 \times 8$ grid, 64 states | 64 |
101
+ | Frozen Lake | `rand10x10` | $10 \times 10$ grid, 100 states | 100 |
102
+ | Frozen Lake | `rand7x7` | $7 \times 7$ grid, 49 states | 49 |
103
+ | Frozen Lake | `rand8x8` | $8 \times 8$ grid, 64 states | 64 |
104
+ | Frozen Lake | `rand9x9` | $9 \times 9$ grid, 81 states | 81 |
105
+ | **Graph Coloring** | | | |
106
+ | Graph Coloring | `games120` | 120 vertices, 1276 edges | 120 |
107
+ | Graph Coloring | `myciel7` | 191 vertices, 2360 edges | 191 |
108
+ | Graph Coloring | `le450_5a` | 450 vertices, 5714 edges | 450 |
109
+ | Graph Coloring | `mulsol.i.2` | 188 vertices, 3885 edges | 188 |
110
+ | Graph Coloring | `zeroin.i.1` | 211 vertices, 4100 edges | 211 |
111
+ | Graph Coloring | `zeroin.i.2` | 211 vertices, 3541 edges | 211 |
112
+
113
+ ## Getting started
114
+
115
+ ### Requirements
116
+
117
+ - Python 3.9 or newer
118
+ - A working PyTorch installation
119
+
120
+ The code depends on:
121
+
122
+ - `numpy`
123
+ - `gymnasium`
124
+ - `torch`
125
+ - `transformers`
126
+ - `eckity`
127
+
128
+ ### Installing for repository development
129
+
130
+ From the repository root:
131
+
132
+ ```bash
133
+ python -m pip install --upgrade pip
134
+ python -m pip install -e ".[dev]"
135
+ ```
136
+
137
+ ## Running the experiments
138
+
139
+ Artificial Ant:
140
+
141
+ ```bash
142
+ python example_runner.py
143
+ python -m dnm_paper.experiments.artificial_ant
144
+ ```
145
+
146
+ Frozen Lake:
147
+
148
+ ```bash
149
+ python example_runner_frozen_lake.py
150
+ python -m dnm_paper.experiments.frozen_lake
151
+ ```
152
+
153
+ Useful options:
154
+
155
+ ```bash
156
+ python -m dnm_paper.experiments.artificial_ant --generations 100 --runs 3 --population-size 6
157
+ python -m dnm_paper.experiments.artificial_ant --maps-dir artifical_ant_maps --output-dir experiments/artificial_ant/runs
158
+ python -m dnm_paper.experiments.frozen_lake --generations 10 --runs 1 --population-size 100 --total-episodes 2000
159
+ ```
160
+
161
+ By default, results are written under `experiments/artificial_ant/runs/<map_name>/bert_mutation/<run_id>/results.json`.
162
+ Frozen Lake results are written under `experiments/frozen_lake/runs/<instance_name>/bert_mutation/<run_id>/results.json`.
163
+
164
+ The experiment modules, benchmark maps, datasets, and result figures are repository resources and are not included in the `eckity-bert-ga` wheel.
165
+
166
+ Release preparation and manual PyPI upload commands are documented in [`RELEASING.md`](RELEASING.md).
167
+
168
+ ## Project structure
169
+
170
+ ```text
171
+ dnm_paper/
172
+ config.py Experiment configuration and default paths
173
+ individuals.py Custom ECKITY individual creator
174
+ logging_utils.py JSON statistics logger
175
+ experiments/
176
+ common.py Shared experiment helpers and mutation builder
177
+ artificial_ant.py CLI entry point and experiment orchestration
178
+ frozen_lake.py CLI entry point and experiment orchestration
179
+ mutation/
180
+ bert.py BERT-based mutation operator
181
+ eckity_adapter.py Adapter that plugs the mutation operator into ECKITY
182
+ problems/
183
+ artificial_ant.py Artificial ant map loader and evaluator
184
+ frozen_lake.py Frozen Lake evaluator
185
+ frozen_lake_instances.py Named Frozen Lake benchmark instances
186
+ artifical_ant_maps/ Benchmark map files
187
+ pyproject.toml Package metadata and dependencies
188
+ ```
@@ -0,0 +1 @@
1
+ """Codebase for the DNM paper experiments."""
@@ -0,0 +1,59 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+
4
+
5
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
6
+ DEFAULT_RESULTS_ROOT = PROJECT_ROOT / "experiments" / "artificial_ant" / "runs"
7
+ DEFAULT_MAPS_DIR = PROJECT_ROOT / "artifical_ant_maps"
8
+ DEFAULT_FROZEN_LAKE_RESULTS_ROOT = PROJECT_ROOT / "experiments" / "frozen_lake" / "runs"
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class MutationModelConfig:
13
+ batch_size: int = 64
14
+ learning_rate: float = 1e-3
15
+ epsilon_greedy: float = 0.05
16
+ mask_probability: float = 0.1
17
+ word_embedding_dim: int = 60
18
+ internal_size: int = 128
19
+ n_layers: int = 3
20
+ n_attention_heads: int = 3
21
+ normalize_batches: bool = False
22
+ best_ind_sample_size: int = 100
23
+ full_trajectory_query: bool = False
24
+ higher_is_better: bool = True
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class EvolutionConfig:
29
+ generations: int = 1000
30
+ population_size: int = 100
31
+ mutation_probability: float = 0.4
32
+ crossover_probability: float = 0.75
33
+ crossover_n_points: int = 2
34
+ tournament_size: int = 4
35
+ runs: int = 1
36
+ output_root: Path = DEFAULT_RESULTS_ROOT
37
+ maps_dir: Path = DEFAULT_MAPS_DIR
38
+ instances_to_run: dict[str, int] = field(
39
+ default_factory=lambda: {
40
+ "aux_map1.txt": 283,
41
+ "aux_map2.txt": 286,
42
+ "john_muir_trail.txt": 200,
43
+ "los_altos.txt": 800,
44
+ "santafe_trail.txt": 400,
45
+ }
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class FrozenLakeConfig:
51
+ generations: int = 100
52
+ population_size: int = 100
53
+ mutation_probability: float = 0.4
54
+ crossover_probability: float = 0.75
55
+ crossover_n_points: int = 2
56
+ tournament_size: int = 4
57
+ runs: int = 1
58
+ total_episodes: int = 2000
59
+ output_root: Path = DEFAULT_FROZEN_LAKE_RESULTS_ROOT
@@ -0,0 +1,22 @@
1
+ from eckity.creators import GAVectorCreator
2
+ from eckity.genetic_encodings.ga import IntVector
3
+
4
+
5
+ class GAIntegerStringVectorCreator(GAVectorCreator):
6
+ def __init__(self, length=1, bounds=(0, 1), gene_creator=None, events=None):
7
+ super().__init__(
8
+ length=length,
9
+ bounds=bounds,
10
+ gene_creator=gene_creator,
11
+ vector_type=IntVector,
12
+ events=events,
13
+ )
14
+
15
+ def individual_from_vector(self, vector):
16
+ individual = self.type(
17
+ length=self.length,
18
+ bounds=self.bounds,
19
+ fitness=self.fitness_type(higher_is_better=True),
20
+ )
21
+ individual.set_vector(vector)
22
+ return individual
@@ -0,0 +1,59 @@
1
+ import json
2
+ import logging
3
+ import time
4
+
5
+ import numpy as np
6
+ from eckity.statistics.statistics import Statistics
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class FileLogger(Statistics):
13
+ """Persist per-generation summary statistics to JSON."""
14
+
15
+ def __init__(self, format_string=None, output_path=None, save_every_n_generations=50):
16
+ if format_string is None:
17
+ format_string = "best fitness {}\nworst fitness {}\naverage fitness {}\n"
18
+ self.output_path = output_path or "./experiment/results.json"
19
+ self.generation_stats = {
20
+ "mean": [],
21
+ "max": [],
22
+ "min": [],
23
+ "std": [],
24
+ "time": [],
25
+ }
26
+ super().__init__(format_string)
27
+ self.time_from_last_generation = time.time()
28
+ self.save_every_n_generations = save_every_n_generations
29
+
30
+ def write_statistics(self, sender, data_dict):
31
+ logger.info("generation #%s", data_dict["generation_num"])
32
+ for index, sub_population in enumerate(data_dict["population"].sub_populations):
33
+ logger.info("subpopulation #%s", index)
34
+ best_individual = sub_population.get_best_individual()
35
+ all_fitness_values = np.array(
36
+ [individual.get_pure_fitness() for individual in sub_population.individuals]
37
+ )
38
+
39
+ self.generation_stats["mean"].append(float(np.mean(all_fitness_values)))
40
+ self.generation_stats["max"].append(float(np.max(all_fitness_values)))
41
+ self.generation_stats["min"].append(float(np.min(all_fitness_values)))
42
+ self.generation_stats["std"].append(float(np.std(all_fitness_values)))
43
+ self.generation_stats["time"].append(time.time() - self.time_from_last_generation)
44
+ self.time_from_last_generation = time.time()
45
+
46
+ logger.info(
47
+ self.format_string.format(
48
+ best_individual.get_pure_fitness(),
49
+ sub_population.get_worst_individual().get_pure_fitness(),
50
+ sub_population.get_average_fitness(),
51
+ )
52
+ )
53
+
54
+ if data_dict["generation_num"] % self.save_every_n_generations == 0:
55
+ self.save_expr()
56
+
57
+ def save_expr(self):
58
+ with open(self.output_path, "w", encoding="utf-8") as output_file:
59
+ json.dump(self.generation_stats, output_file)
@@ -0,0 +1,4 @@
1
+ from dnm_paper.mutation.bert import BertMutation
2
+ from dnm_paper.mutation.eckity_adapter import EckityCustomMutation
3
+
4
+ __all__ = ["BertMutation", "EckityCustomMutation"]
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ import torch
8
+ from numpy.typing import NDArray
9
+ from torch import Tensor
10
+ from torch.optim import Adam
11
+ from transformers import BertConfig, BertForMaskedLM
12
+
13
+
14
+ GenomeArray = NDArray[np.integer[Any]]
15
+ MaskArray = NDArray[np.bool_]
16
+ FitnessDict = dict[tuple[int, ...], float]
17
+
18
+
19
+ class BertMutation:
20
+ def __init__(
21
+ self,
22
+ max_int_val: int,
23
+ get_fitness_func: Callable[[GenomeArray], float],
24
+ batch_size: int = 64,
25
+ learning_rate: float = 1e-3,
26
+ adam_decay: float = 0,
27
+ epsilon_greedy: float = 0.01,
28
+ word_embedding_dim: int = 64,
29
+ context_size: int = 1024,
30
+ n_layers: int = 4,
31
+ n_attention_heads: int = 4,
32
+ internal_size: int = 64,
33
+ clip_grad_norm: float | None = 1.0,
34
+ full_trajectory_query: bool = False,
35
+ higher_is_better: bool = True,
36
+ mask_probability: float = 0.1,
37
+ normalize_batches: bool = False,
38
+ entropy_coefficient: float = 0.0,
39
+ scale_fitness_function: Callable[[Tensor], Tensor] | None = None,
40
+ fitness_dict: FitnessDict | None = None,
41
+ best_ind_scale_factor: float = 2.0,
42
+ best_ind_sample_size: int = 10,
43
+ ) -> None:
44
+ assert 0 <= mask_probability <= 1, "Mask probability must be between 0 and 1"
45
+ assert n_layers > 0, "Number of layers must be greater than 0"
46
+ assert n_attention_heads > 0, "Number of attention heads must be greater than 0"
47
+ assert internal_size > 0, "Internal size must be greater than 0"
48
+ assert word_embedding_dim > 0, "Word embedding dimension must be greater than 0"
49
+ assert context_size > 0, "Context size must be greater than 0"
50
+ assert batch_size > 0, "Batch size must be greater than 0"
51
+ assert learning_rate > 0, "Learning rate must be greater than 0"
52
+ assert max_int_val >= 1, "Max integer value must be at least 1"
53
+
54
+ self.vocab_size = max_int_val + 2
55
+ self.max_int_val = max_int_val
56
+ self.mask_id = max_int_val + 1
57
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
58
+ print(f"Using device: {self.device}")
59
+
60
+ bert_config = BertConfig(
61
+ vocab_size=self.vocab_size,
62
+ hidden_size=word_embedding_dim,
63
+ num_hidden_layers=n_layers,
64
+ num_attention_heads=n_attention_heads,
65
+ intermediate_size=internal_size,
66
+ max_position_embeddings=context_size,
67
+ pad_token_id=self.mask_id,
68
+ )
69
+ self.model = BertForMaskedLM(bert_config).to(self.device)
70
+ self.fitness_dict = fitness_dict
71
+ self.batch_size = batch_size
72
+ self.get_fitness_func = get_fitness_func
73
+ self.epsilon_greedy = epsilon_greedy
74
+ self.clip_grad_norm = clip_grad_norm
75
+ self.full_trajectory_query = full_trajectory_query
76
+ self.higher_is_better = higher_is_better
77
+ self.mask_probability = mask_probability
78
+ self.normalize_batches = normalize_batches
79
+ self.entropy_coefficient = entropy_coefficient
80
+ self.scale_fitness_function = scale_fitness_function
81
+ self.best_ind_scale_factor = best_ind_scale_factor
82
+ self.best_ind_sample_size = best_ind_sample_size
83
+ self.optimizer = Adam(self.model.parameters(), lr=learning_rate, weight_decay=adam_decay)
84
+ self.trajectory_log_probabilities: list[Tensor] = []
85
+ self.rewards: list[Tensor] = []
86
+ self.entropy_list: list[Tensor] = []
87
+
88
+ def mutate(self, individual_to_mutate: GenomeArray, mask: MaskArray) -> GenomeArray:
89
+ unmasked_tokens = np.copy(individual_to_mutate)
90
+ masked_individual = np.copy(unmasked_tokens)
91
+ masked_individual[mask] = self.mask_id
92
+
93
+ token_ids = torch.tensor(np.array([masked_individual]), dtype=torch.long, device=self.device)
94
+ logits = self.model(token_ids, attention_mask=torch.ones_like(token_ids, device=self.device)).logits
95
+ mask_indices = torch.where(token_ids == self.mask_id)[1]
96
+
97
+ suggested_mutation, trajectory_action_probabilities, dist_entropy = self.masked_trajectory_generation(
98
+ logits, mask_indices
99
+ )
100
+
101
+ for genome_index, suggested_value in zip(np.where(mask)[0], suggested_mutation):
102
+ unmasked_tokens[genome_index] = suggested_value
103
+
104
+ reward = self.get_fitness_func(unmasked_tokens)
105
+ self.log_trajectory_to_memory(dist_entropy, reward, trajectory_action_probabilities)
106
+
107
+ if self.fitness_dict is not None and self.fitness_dict:
108
+ self.peek_to_best_individual(logits)
109
+
110
+ self.run_epoch()
111
+ return unmasked_tokens
112
+
113
+ def log_trajectory_to_memory(
114
+ self,
115
+ dist_entropy: Tensor,
116
+ reward: float,
117
+ trajectory_action_probabilities: Tensor,
118
+ ) -> None:
119
+ trajectory_probability = torch.log(trajectory_action_probabilities).sum().unsqueeze(0).unsqueeze(0)
120
+ self.rewards.append(torch.full_like(trajectory_probability, reward))
121
+ self.trajectory_log_probabilities.append(trajectory_probability)
122
+ self.entropy_list.append(dist_entropy.unsqueeze(0).unsqueeze(0))
123
+
124
+ def get_mutation(self, individuals_to_mutate: Sequence[GenomeArray]) -> list[GenomeArray]:
125
+ mutated_individuals: list[GenomeArray] = []
126
+ individual_masks = self._sample_masks(individuals_to_mutate)
127
+ for individual_to_mutate, mask in zip(individuals_to_mutate, individual_masks):
128
+ mutated_individuals.append(self.mutate(individual_to_mutate, mask))
129
+ return mutated_individuals
130
+
131
+ def _sample_masks(self, individuals: Sequence[GenomeArray]) -> MaskArray:
132
+ n_individuals = len(individuals)
133
+ individual_length = len(individuals[0])
134
+ return np.random.choice(
135
+ [True, False],
136
+ size=(n_individuals, individual_length),
137
+ p=[self.mask_probability, 1 - self.mask_probability],
138
+ )
139
+
140
+ def masked_trajectory_generation(
141
+ self,
142
+ logits: Tensor,
143
+ mask_indices: Tensor,
144
+ ) -> tuple[NDArray[np.int64], Tensor, Tensor]:
145
+ n_masks = len(mask_indices)
146
+ operator_probabilities = torch.softmax(logits[0, mask_indices][:, :-1], dim=-1).to(self.device)
147
+ sampled_distribution = torch.distributions.Categorical(operator_probabilities)
148
+ sampled_operator_indices = sampled_distribution.sample().to(self.device)
149
+ dist_entropy = sampled_distribution.entropy().mean()
150
+
151
+ epsilon_greedy_probas = torch.rand(n_masks, device=self.device)
152
+ epsilon_greedy_masks = torch.where(epsilon_greedy_probas < self.epsilon_greedy)[0]
153
+ sampled_operator_indices[epsilon_greedy_masks] = torch.randint(
154
+ 0, self.vocab_size - 1, (epsilon_greedy_masks.shape[0],), device=self.device
155
+ )
156
+ trajectory_action_probabilities = torch.gather(
157
+ operator_probabilities, dim=1, index=sampled_operator_indices.unsqueeze(-1)
158
+ )
159
+ suggested_mutation = sampled_operator_indices.detach().cpu().numpy()
160
+ return suggested_mutation, trajectory_action_probabilities, dist_entropy
161
+
162
+ def peek_to_best_individual(self, logits: Tensor) -> None:
163
+ top_k_individuals = sorted(
164
+ self.fitness_dict.items(),
165
+ key=lambda item: item[1],
166
+ reverse=self.higher_is_better,
167
+ )[: self.best_ind_sample_size]
168
+ best_individual = top_k_individuals[np.random.randint(0, len(top_k_individuals))][0]
169
+ best_individual_fitness = self.fitness_dict[tuple(best_individual)]
170
+
171
+ best_individual = torch.tensor(best_individual, dtype=torch.long, device=self.device).unsqueeze(0)
172
+ operator_probabilities = torch.softmax(logits[0][:, :-1], dim=-1).to(self.device)
173
+ dist_entropy = torch.distributions.Categorical(operator_probabilities).entropy().mean()
174
+ trajectory_action_probabilities = torch.gather(operator_probabilities, dim=1, index=best_individual.T)
175
+ self.log_trajectory_to_memory(
176
+ dist_entropy,
177
+ best_individual_fitness * self.best_ind_scale_factor,
178
+ trajectory_action_probabilities,
179
+ )
180
+
181
+ def run_epoch(self, numerical_stability: float = 1e-10) -> None:
182
+ if self.get_batch_size() < self.batch_size:
183
+ return
184
+
185
+ all_traj_proba = torch.cat(self.trajectory_log_probabilities, dim=0).to(self.device)
186
+ all_rewards = torch.cat(self.rewards, dim=0).to(self.device)
187
+ all_entropy = torch.cat(self.entropy_list, dim=0).to(self.device)
188
+
189
+ if self.scale_fitness_function is not None:
190
+ all_rewards = self.scale_fitness_function(all_rewards)
191
+
192
+ self.trajectory_log_probabilities.clear()
193
+ self.rewards.clear()
194
+ self.entropy_list.clear()
195
+ self.optimizer.zero_grad()
196
+
197
+ if self.normalize_batches:
198
+ advantages = (all_rewards - torch.mean(all_rewards)) / (torch.std(all_rewards) + numerical_stability)
199
+ else:
200
+ advantages = all_rewards
201
+
202
+ loss = -torch.mean(all_traj_proba * advantages) - self.entropy_coefficient * torch.mean(all_entropy)
203
+ loss.backward()
204
+ loss_value = loss.item()
205
+
206
+ if self.clip_grad_norm is not None:
207
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.clip_grad_norm)
208
+
209
+ self.optimizer.step()
210
+ print(f"loss: {loss_value}, reward: {torch.mean(all_rewards)}")
211
+
212
+ def get_batch_size(self) -> int:
213
+ return sum(len(reward) for reward in self.rewards)
@@ -0,0 +1,31 @@
1
+ import numpy as np
2
+ from eckity.genetic_operators import GeneticOperator
3
+
4
+
5
+ class EckityCustomMutation(GeneticOperator):
6
+ """Wrap a mutation model so it can be applied through ECKITY."""
7
+
8
+ def __init__(self, mutation_operator, population_size, probability=1.0, events=None):
9
+ super().__init__(probability=1.0, arity=population_size, events=events)
10
+ self.mutation_operator = mutation_operator
11
+ self.mutation_probability = probability
12
+
13
+ def apply(self, individuals):
14
+ mutation_masks = np.random.rand(len(individuals)) < self.mutation_probability
15
+ individuals_matrix = np.array([individual.vector for individual in individuals])[mutation_masks]
16
+
17
+ if len(individuals_matrix) == 0:
18
+ return individuals
19
+
20
+ mutated_individuals = self.mutation_operator.get_mutation(individuals_matrix)
21
+ eckity_mutated_individuals = [
22
+ individual for index, individual in enumerate(individuals) if mutation_masks[index]
23
+ ]
24
+ assert len(mutated_individuals) == len(
25
+ eckity_mutated_individuals
26
+ ), "Mutated individuals length mismatch"
27
+
28
+ for index, individual in enumerate(eckity_mutated_individuals):
29
+ individual.set_vector(list(mutated_individuals[index]))
30
+
31
+ return individuals
@@ -0,0 +1,10 @@
1
+ """BERT mutation operator for EC-KitY genetic algorithms."""
2
+
3
+ from dnm_paper.individuals import GAIntegerStringVectorCreator
4
+ from dnm_paper.mutation import BertMutation, EckityCustomMutation
5
+
6
+ __all__ = [
7
+ "BertMutation",
8
+ "EckityCustomMutation",
9
+ "GAIntegerStringVectorCreator",
10
+ ]
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: eckity-bert-ga
3
+ Version: 0.1.0
4
+ Summary: BERT mutation operator for EC-KitY genetic algorithms
5
+ Author: EC-KitY
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/EC-KitY/BERT-Mutation-for-GA
8
+ Project-URL: Repository, https://github.com/EC-KitY/BERT-Mutation-for-GA
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: eckity~=0.4.1
21
+ Requires-Dist: numpy>=2.0.2
22
+ Requires-Dist: scipy>=1.13.0
23
+ Requires-Dist: torch>=2.7.1
24
+ Requires-Dist: transformers>=4.50.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: gymnasium>=1.0; extra == "dev"
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: twine>=5.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # BERT Mutation for Genetic Algorithms
33
+
34
+ ## Overview
35
+
36
+ This repository implements the paper `BERT Mutation: Deep Transformer Model for Masked Uniform Mutation in Genetic Algorithms`.
37
+
38
+ The paper proposes a domain-independent mutation operator for genetic algorithms that uses a BERT-style masked model to predict beneficial gene replacements from context. To make this work for fixed-length GA representations, it adds an elite-guided data augmentation mechanism that creates additional learning signal from strong historical solutions.
39
+
40
+ In the paper, the method is evaluated on four domains: Frozen Lake, Artificial Ant, Graph Coloring, and Unweighted Set Cover. The reported results show faster convergence and better final fitness than standard mutation baselines and an adaptive operator-selection baseline, while maintaining meaningful population diversity.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install eckity-bert-ga
46
+ ```
47
+
48
+ ## Using the operator
49
+
50
+ Import the public API from `eckity_bert_ga`:
51
+
52
+ ```python
53
+ from eckity_bert_ga import (
54
+ BertMutation,
55
+ EckityCustomMutation,
56
+ GAIntegerStringVectorCreator,
57
+ )
58
+ ```
59
+
60
+ Create the BERT mutation model and wrap it as an EC-KitY operator:
61
+
62
+ ```python
63
+ population_size = 100
64
+
65
+ bert_mutation = BertMutation(
66
+ max_int_val=MAX_GENE_VALUE,
67
+ get_fitness_func=evaluate_vector,
68
+ context_size=INDIVIDUAL_LENGTH + 1,
69
+ mask_probability=0.1,
70
+ )
71
+
72
+ mutation_operator = EckityCustomMutation(
73
+ mutation_operator=bert_mutation,
74
+ population_size=population_size,
75
+ probability=0.4,
76
+ )
77
+ ```
78
+
79
+ Add `mutation_operator` to an EC-KitY subpopulation's `operators_sequence`. The BERT model is initialized locally from configuration and does not download pretrained model weights.
80
+
81
+ ### Compatibility
82
+
83
+ - Python 3.9 or newer
84
+ - EC-KitY 0.4.x
85
+ - NumPy 2.0.2 or newer
86
+ - SciPy 1.13.0 or newer
87
+ - PyTorch 2.7.1 or newer
88
+ - Transformers 4.50.0 or newer
89
+
90
+ ## Results from the paper
91
+
92
+ ### Fitness by generation
93
+
94
+ The following figure presents representative best-individual fitness curves for BERT Mutation and the mutation baselines across the four evaluated domains.
95
+
96
+ ![Representative fitness curves across the evaluated domains](images/bert_ga.png "Convergence plots")
97
+
98
+ *Best-individual fitness by generation, averaged over 10 runs. Black dots mark the runtime cutoffs.*
99
+
100
+ ### Statistical comparisons
101
+
102
+ The following table presents the paired exact permutation-test results comparing BERT Mutation against each baseline in every evaluated domain.
103
+
104
+ ![Paired exact permutation-test results](images/p_values.png "Permutation-test results")
105
+
106
+ *Holm-corrected p-values for comparisons between BERT Mutation and the baseline mutation operators.*
107
+
108
+ ## Benchmark instances
109
+
110
+ *Benchmark instance sizes used in the experiments. The individual length $L$ denotes the genome length optimized by the GA.*
111
+
112
+ | Domain | Instance | Problem size | Individual length $L$ |
113
+ |---|---|---|---:|
114
+ | **Artificial Ant** | | | |
115
+ | Artificial Ant | `aux_map1` | $20 \times 20$, 91 food cells | 283 |
116
+ | Artificial Ant | `aux_map2` | $20 \times 20$, 69 food cells | 286 |
117
+ | Artificial Ant | `john_muir` | $32 \times 32$, 89 food cells | 200 |
118
+ | Artificial Ant | `los_altos` | $100 \times 100$, 157 food cells | 800 |
119
+ | Artificial Ant | `santafe` | $32 \times 32$, 89 food cells | 400 |
120
+ | **Set Covering** | | | |
121
+ | Set Covering | `scp41` | 200 rows, 1000 columns | 1000 |
122
+ | Set Covering | `scp51` | 200 rows, 2000 columns | 2000 |
123
+ | Set Covering | `scp52` | 200 rows, 2000 columns | 2000 |
124
+ | Set Covering | `scp53` | 200 rows, 2000 columns | 2000 |
125
+ | Set Covering | `scp54` | 200 rows, 2000 columns | 2000 |
126
+ | Set Covering | `scp56` | 200 rows, 2000 columns | 2000 |
127
+ | Set Covering | `scp57` | 200 rows, 2000 columns | 2000 |
128
+ | Set Covering | `scp64` | 200 rows, 1000 columns | 1000 |
129
+ | Set Covering | `scp65` | 200 rows, 1000 columns | 1000 |
130
+ | **Frozen Lake** | | | |
131
+ | Frozen Lake | `default` | $8 \times 8$ grid, 64 states | 64 |
132
+ | Frozen Lake | `rand10x10` | $10 \times 10$ grid, 100 states | 100 |
133
+ | Frozen Lake | `rand7x7` | $7 \times 7$ grid, 49 states | 49 |
134
+ | Frozen Lake | `rand8x8` | $8 \times 8$ grid, 64 states | 64 |
135
+ | Frozen Lake | `rand9x9` | $9 \times 9$ grid, 81 states | 81 |
136
+ | **Graph Coloring** | | | |
137
+ | Graph Coloring | `games120` | 120 vertices, 1276 edges | 120 |
138
+ | Graph Coloring | `myciel7` | 191 vertices, 2360 edges | 191 |
139
+ | Graph Coloring | `le450_5a` | 450 vertices, 5714 edges | 450 |
140
+ | Graph Coloring | `mulsol.i.2` | 188 vertices, 3885 edges | 188 |
141
+ | Graph Coloring | `zeroin.i.1` | 211 vertices, 4100 edges | 211 |
142
+ | Graph Coloring | `zeroin.i.2` | 211 vertices, 3541 edges | 211 |
143
+
144
+ ## Getting started
145
+
146
+ ### Requirements
147
+
148
+ - Python 3.9 or newer
149
+ - A working PyTorch installation
150
+
151
+ The code depends on:
152
+
153
+ - `numpy`
154
+ - `gymnasium`
155
+ - `torch`
156
+ - `transformers`
157
+ - `eckity`
158
+
159
+ ### Installing for repository development
160
+
161
+ From the repository root:
162
+
163
+ ```bash
164
+ python -m pip install --upgrade pip
165
+ python -m pip install -e ".[dev]"
166
+ ```
167
+
168
+ ## Running the experiments
169
+
170
+ Artificial Ant:
171
+
172
+ ```bash
173
+ python example_runner.py
174
+ python -m dnm_paper.experiments.artificial_ant
175
+ ```
176
+
177
+ Frozen Lake:
178
+
179
+ ```bash
180
+ python example_runner_frozen_lake.py
181
+ python -m dnm_paper.experiments.frozen_lake
182
+ ```
183
+
184
+ Useful options:
185
+
186
+ ```bash
187
+ python -m dnm_paper.experiments.artificial_ant --generations 100 --runs 3 --population-size 6
188
+ python -m dnm_paper.experiments.artificial_ant --maps-dir artifical_ant_maps --output-dir experiments/artificial_ant/runs
189
+ python -m dnm_paper.experiments.frozen_lake --generations 10 --runs 1 --population-size 100 --total-episodes 2000
190
+ ```
191
+
192
+ By default, results are written under `experiments/artificial_ant/runs/<map_name>/bert_mutation/<run_id>/results.json`.
193
+ Frozen Lake results are written under `experiments/frozen_lake/runs/<instance_name>/bert_mutation/<run_id>/results.json`.
194
+
195
+ The experiment modules, benchmark maps, datasets, and result figures are repository resources and are not included in the `eckity-bert-ga` wheel.
196
+
197
+ Release preparation and manual PyPI upload commands are documented in [`RELEASING.md`](RELEASING.md).
198
+
199
+ ## Project structure
200
+
201
+ ```text
202
+ dnm_paper/
203
+ config.py Experiment configuration and default paths
204
+ individuals.py Custom ECKITY individual creator
205
+ logging_utils.py JSON statistics logger
206
+ experiments/
207
+ common.py Shared experiment helpers and mutation builder
208
+ artificial_ant.py CLI entry point and experiment orchestration
209
+ frozen_lake.py CLI entry point and experiment orchestration
210
+ mutation/
211
+ bert.py BERT-based mutation operator
212
+ eckity_adapter.py Adapter that plugs the mutation operator into ECKITY
213
+ problems/
214
+ artificial_ant.py Artificial ant map loader and evaluator
215
+ frozen_lake.py Frozen Lake evaluator
216
+ frozen_lake_instances.py Named Frozen Lake benchmark instances
217
+ artifical_ant_maps/ Benchmark map files
218
+ pyproject.toml Package metadata and dependencies
219
+ ```
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dnm_paper/__init__.py
5
+ dnm_paper/config.py
6
+ dnm_paper/individuals.py
7
+ dnm_paper/logging_utils.py
8
+ dnm_paper/mutation/__init__.py
9
+ dnm_paper/mutation/bert.py
10
+ dnm_paper/mutation/eckity_adapter.py
11
+ eckity_bert_ga/__init__.py
12
+ eckity_bert_ga.egg-info/PKG-INFO
13
+ eckity_bert_ga.egg-info/SOURCES.txt
14
+ eckity_bert_ga.egg-info/dependency_links.txt
15
+ eckity_bert_ga.egg-info/requires.txt
16
+ eckity_bert_ga.egg-info/top_level.txt
17
+ tests/test_package.py
@@ -0,0 +1,11 @@
1
+ eckity~=0.4.1
2
+ numpy>=2.0.2
3
+ scipy>=1.13.0
4
+ torch>=2.7.1
5
+ transformers>=4.50.0
6
+
7
+ [dev]
8
+ build>=1.2
9
+ gymnasium>=1.0
10
+ pytest>=8.0
11
+ twine>=5.0
@@ -0,0 +1,2 @@
1
+ dnm_paper
2
+ eckity_bert_ga
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "eckity-bert-ga"
7
+ version = "0.1.0"
8
+ description = "BERT mutation operator for EC-KitY genetic algorithms"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "BSD-3-Clause"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "EC-KitY" }]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "eckity~=0.4.1",
26
+ "numpy>=2.0.2",
27
+ "scipy>=1.13.0",
28
+ "torch>=2.7.1",
29
+ "transformers>=4.50.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "build>=1.2",
35
+ "gymnasium>=1.0",
36
+ "pytest>=8.0",
37
+ "twine>=5.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/EC-KitY/BERT-Mutation-for-GA"
42
+ Repository = "https://github.com/EC-KitY/BERT-Mutation-for-GA"
43
+
44
+ [tool.setuptools]
45
+ packages = ["eckity_bert_ga", "dnm_paper", "dnm_paper.mutation"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,84 @@
1
+ import unittest
2
+
3
+ import numpy as np
4
+ from eckity.algorithms.simple_evolution import SimpleEvolution
5
+ from eckity.breeders.simple_breeder import SimpleBreeder
6
+ from eckity.evaluators.simple_individual_evaluator import SimpleIndividualEvaluator
7
+ from eckity.genetic_operators.selections.tournament_selection import TournamentSelection
8
+ from eckity.subpopulation import Subpopulation
9
+
10
+ from dnm_paper.individuals import GAIntegerStringVectorCreator as OriginalCreator
11
+ from dnm_paper.mutation import BertMutation as OriginalBertMutation
12
+ from dnm_paper.mutation import EckityCustomMutation as OriginalEckityCustomMutation
13
+ from eckity_bert_ga import BertMutation, EckityCustomMutation, GAIntegerStringVectorCreator
14
+
15
+
16
+ class SumEvaluator(SimpleIndividualEvaluator):
17
+ def evaluate_individual(self, individual):
18
+ return float(np.sum(individual.vector))
19
+
20
+
21
+ def create_mutation(evaluator, population_size):
22
+ model = BertMutation(
23
+ max_int_val=3,
24
+ get_fitness_func=lambda vector: float(np.sum(vector)),
25
+ batch_size=1024,
26
+ epsilon_greedy=0.0,
27
+ word_embedding_dim=8,
28
+ context_size=8,
29
+ n_layers=1,
30
+ n_attention_heads=2,
31
+ internal_size=8,
32
+ mask_probability=1.0,
33
+ )
34
+ adapter = EckityCustomMutation(
35
+ mutation_operator=model,
36
+ population_size=population_size,
37
+ probability=1.0,
38
+ )
39
+ return model, adapter
40
+
41
+
42
+ class PackageTests(unittest.TestCase):
43
+ def test_public_api_reexports_existing_classes(self):
44
+ self.assertIs(BertMutation, OriginalBertMutation)
45
+ self.assertIs(EckityCustomMutation, OriginalEckityCustomMutation)
46
+ self.assertIs(GAIntegerStringVectorCreator, OriginalCreator)
47
+
48
+ def test_mutation_output_shape_and_bounds(self):
49
+ model, _ = create_mutation(SumEvaluator(), population_size=2)
50
+ genomes = [np.array([0, 1, 2, 3]), np.array([3, 2, 1, 0])]
51
+
52
+ mutated = model.get_mutation(genomes)
53
+
54
+ self.assertEqual(len(mutated), len(genomes))
55
+ for genome in mutated:
56
+ self.assertEqual(genome.shape, genomes[0].shape)
57
+ self.assertTrue(np.all(genome >= 0))
58
+ self.assertTrue(np.all(genome <= 3))
59
+
60
+ def test_two_generation_eckity_evolution(self):
61
+ population_size = 4
62
+ creator = GAIntegerStringVectorCreator(length=4, bounds=(0, 3))
63
+ evaluator = SumEvaluator()
64
+ model, adapter = create_mutation(evaluator, population_size)
65
+ algorithm = SimpleEvolution(
66
+ Subpopulation(
67
+ creators=creator,
68
+ population_size=population_size,
69
+ evaluator=evaluator,
70
+ higher_is_better=True,
71
+ operators_sequence=[adapter],
72
+ selection_methods=[
73
+ (TournamentSelection(tournament_size=2, higher_is_better=True), 1)
74
+ ],
75
+ ),
76
+ breeder=SimpleBreeder(),
77
+ max_workers=1,
78
+ max_generation=2,
79
+ random_seed=4242,
80
+ )
81
+
82
+ algorithm.evolve()
83
+
84
+ self.assertGreater(len(model.rewards), 0)