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.
- eckity_bert_ga-0.1.0/LICENSE +28 -0
- eckity_bert_ga-0.1.0/PKG-INFO +219 -0
- eckity_bert_ga-0.1.0/README.md +188 -0
- eckity_bert_ga-0.1.0/dnm_paper/__init__.py +1 -0
- eckity_bert_ga-0.1.0/dnm_paper/config.py +59 -0
- eckity_bert_ga-0.1.0/dnm_paper/individuals.py +22 -0
- eckity_bert_ga-0.1.0/dnm_paper/logging_utils.py +59 -0
- eckity_bert_ga-0.1.0/dnm_paper/mutation/__init__.py +4 -0
- eckity_bert_ga-0.1.0/dnm_paper/mutation/bert.py +213 -0
- eckity_bert_ga-0.1.0/dnm_paper/mutation/eckity_adapter.py +31 -0
- eckity_bert_ga-0.1.0/eckity_bert_ga/__init__.py +10 -0
- eckity_bert_ga-0.1.0/eckity_bert_ga.egg-info/PKG-INFO +219 -0
- eckity_bert_ga-0.1.0/eckity_bert_ga.egg-info/SOURCES.txt +17 -0
- eckity_bert_ga-0.1.0/eckity_bert_ga.egg-info/dependency_links.txt +1 -0
- eckity_bert_ga-0.1.0/eckity_bert_ga.egg-info/requires.txt +11 -0
- eckity_bert_ga-0.1.0/eckity_bert_ga.egg-info/top_level.txt +2 -0
- eckity_bert_ga-0.1.0/pyproject.toml +48 -0
- eckity_bert_ga-0.1.0/setup.cfg +4 -0
- eckity_bert_ga-0.1.0/tests/test_package.py +84 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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,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
|
+

|
|
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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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,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)
|