Spaces:
Sleeping
Sleeping
fracapuano
commited on
Commit
•
81a5d0a
1
Parent(s):
7d62cb4
add files via upload
Browse files- .DS_Store +0 -0
- LICENSE +21 -0
- README.md +73 -13
- app.py +250 -0
- data/df_nebuloss.csv +0 -0
- data/nats_arch_index.json +0 -0
- data/nebuloss_1.json +0 -0
- data/nebuloss_2.json +0 -0
- data/nebuloss_3.json +0 -0
- data/nebuloss_4.json +0 -0
- nas.py +79 -0
- requirements.txt +4 -0
- split_in_chunks.ipynb +109 -0
- src/__init__.py +3 -0
- src/__pycache__/__init__.cpython-310.pyc +0 -0
- src/__pycache__/genetics.cpython-310.pyc +0 -0
- src/__pycache__/hw_nats_fast_interface.cpython-310.pyc +0 -0
- src/__pycache__/utils.cpython-310.pyc +0 -0
- src/genetics.py +301 -0
- src/hw_nats_fast_interface.py +245 -0
- src/search/__init__.py +1 -0
- src/search/__pycache__/__init__.cpython-310.pyc +0 -0
- src/search/__pycache__/ga.cpython-310.pyc +0 -0
- src/search/ga.py +228 -0
- src/utils.py +25 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 Francesco Capuano
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,13 +1,73 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div align="center">
|
2 |
+
<a href="https://ibb.co/gTkPrng">
|
3 |
+
<img src="https://i.ibb.co/FXYXZfD/Nebul-OS-logo.png" alt="Nebul-OS-logo" border="0">
|
4 |
+
</a>
|
5 |
+
</div>
|
6 |
+
|
7 |
+
# NebulOS: Fair, green AI. For Real 🌿
|
8 |
+
Welcome to the GitHub repository of the 18th ASP cycle coolest project! 🚀
|
9 |
+
|
10 |
+
With NebulOS, we push the boundaries AI adoption, focusing on how to design architectures tailored for the hardware on which they run.
|
11 |
+
During this wonderful journey, we counted on the support of the amazing people at [Nebuly](https://www.nebuly.com/) (8.3k 🌟 on GitHub), as well the guidance and help by Prof. [Barbara Caputo](linkedin.com/in/barbara-caputo-a610201a7/?originalSubdomain=it) (Politecnico di Torino, Top50 Universities world-wide), and Prof. [Stefana Maja Broadbent](https://www.linkedin.com/in/stefanabroadbent/?originalSubdomain=uk) (Politecnico di Milano, Top20 Universities world-wide).
|
12 |
+
|
13 |
+
Give us a star to show your support for the project ⭐
|
14 |
+
You can find an extended abstract of this project [here](https://sites.google.com/view/nebulos)
|
15 |
+
|
16 |
+
## Foreword 📝
|
17 |
+
### Alta Scuola Politecnica (ASP)
|
18 |
+
Alta Scuola Politecnica (more [here](https://www.asp-poli.it/)) is the **joint honors program** of Italy's best technical universities, Politecnico di Milano ([18th world-wide, QS Rankings](https://www.topuniversities.com/university-rankings/university-subject-rankings/2023/engineering-technology?&page=1)) and Politecnico di Torino ([45th world-wide, QS Rankings](https://www.topuniversities.com/university-rankings/university-subject-rankings/2023/engineering-technology?&page=1)).
|
19 |
+
Each year, 90 students from Politecnico di Milano and 60 from Politecnico di Torino are selected from a highly competitive pool and those who succeed receive free tuition for their MSc in exchange for ~1.5 years working as **student consultants** with a partner company for an industrial project.
|
20 |
+
|
21 |
+
The project we present has been carried out with the invaluable support of folks at [Nebuly](https://www.nebuly.com/), the company behind the very well-known [`nebullvm`](https://github.com/nebuly-ai/nebuly/tree/main/optimization/nebullvm) open-source AI-acceleration library 🚀
|
22 |
+
|
23 |
+
Alongside them, we have developed a stable and reliable AI-acceleration tool that capable of designing just the right network for each specific target device.
|
24 |
+
With this, we propose a new answer to an old Deep Learning question: how to bring large models to tiny devices. **Screw forcing a circle in a square-hole**: we feel like we are the trouble-makers here, *better to change the model from the ground up!*
|
25 |
+
|
26 |
+
## Contributions 🌟
|
27 |
+
NebulOS takes a step further by adopting actual hardware-aware metrics (such as the architectures' energy consumption 🌿) to perform the automated design of Deep Neural Architectures.
|
28 |
+
|
29 |
+
## How to Reproduce the Results 💻
|
30 |
+
1. **Clone the Repository**: `git clone https://github.com/fracapuano/NebulOS.git`
|
31 |
+
2. **Install Dependencies**:
|
32 |
+
After having made sure you have a working version of `conda` on your machine (you can double-check running the command `conda` in your terminal), go ahead:
|
33 |
+
- Creating the environment (this code has been fully tested for Python 3.10)
|
34 |
+
```bash
|
35 |
+
conda create -n nebulosenv python=3.10 -y
|
36 |
+
```
|
37 |
+
- Activating the environment
|
38 |
+
```bash
|
39 |
+
conda activate nebulosenv
|
40 |
+
```
|
41 |
+
- Installing the (very minimal) necessary requirements
|
42 |
+
```bash
|
43 |
+
pip install -r requirements.txt
|
44 |
+
```
|
45 |
+
3. **Run the Code**: Use the provided scripts and guidelines in the repository.
|
46 |
+
To reproduce our results you can simply run the following command:
|
47 |
+
```bash
|
48 |
+
python nas.py
|
49 |
+
```
|
50 |
+
|
51 |
+
To specialize your search, you can select multiple arguments. You may select those of interest to you using Python args. To see all args available you run:
|
52 |
+
|
53 |
+
```bash
|
54 |
+
python nas.py --help
|
55 |
+
```
|
56 |
+
|
57 |
+
For instance, you can specify a search for an NVIDIA Jetson Nano device on ImageNet16-120 by running:
|
58 |
+
```bash
|
59 |
+
python nas.py --device edgegpu --dataset ImageNet16-120
|
60 |
+
```
|
61 |
+
## Live-demo ⚡
|
62 |
+
Our live demo is currently hosted as an Hugging Face space. You can find it at [spaces/fracapuano/NebulOS](https://huggingface.co/spaces/fracapuano/NebulOS)
|
63 |
+
|
64 |
+
## Next modules and roadmap
|
65 |
+
We are actively working on obtaining the next results.
|
66 |
+
|
67 |
+
- [ ] Extending this work to deal with Transformer networks in NLP.
|
68 |
+
- [ ] Bring actual AI Optimization to LLMs.
|
69 |
+
|
70 |
+
## Conclusions 🌍
|
71 |
+
We really hyped up about NebulOS because we feel it is way more than an extension; it's a revolution in the field of Green-AI. This project stays as a testament of our commitment toward truly sustainable AI, and by adopting actual hardware-aware metrics, we are making a tangible difference in the world of Deep Neural Architectures.
|
72 |
+
|
73 |
+
Join us in this journey towards a greener future! Help us keep AI beneficial to all. This time, for real.
|
app.py
ADDED
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from src.search.ga import GeneticSearch
|
2 |
+
from src.hw_nats_fast_interface import HW_NATS_FastInterface
|
3 |
+
from src.utils import DEVICES, DATASETS
|
4 |
+
import streamlit as st
|
5 |
+
import numpy as np
|
6 |
+
import pandas as pd
|
7 |
+
import matplotlib.pyplot as plt
|
8 |
+
import plotly.graph_objects as go
|
9 |
+
from collections import OrderedDict
|
10 |
+
st.set_page_config(layout="wide")
|
11 |
+
|
12 |
+
TIME_TO_SCORE_EACH_ARCHITECTURE=0.15
|
13 |
+
DAYS_7 = 604800
|
14 |
+
NEBULOS_COLOR = '#FF6961'
|
15 |
+
TF_COLOR = '#A7C7E7'
|
16 |
+
|
17 |
+
@st.cache_data(ttl=DAYS_7)
|
18 |
+
def load_lookup_table():
|
19 |
+
"""Load recap table of NebulOS metrics and cache it.
|
20 |
+
"""
|
21 |
+
df_nebuloss = pd.read_csv('data/df_nebuloss.csv').rename(columns = {'test_accuracy' : 'validation_accuracy'})
|
22 |
+
|
23 |
+
return df_nebuloss
|
24 |
+
|
25 |
+
@st.cache_data(ttl=DAYS_7)
|
26 |
+
def subset_dataframe(df_nebuloss, dataset):
|
27 |
+
"""Subset df_nebuloss based on the right dataset.
|
28 |
+
"""
|
29 |
+
return df_nebuloss[df_nebuloss['dataset'] == dataset]
|
30 |
+
|
31 |
+
@st.cache_data(ttl=DAYS_7)
|
32 |
+
def compute_quantiles(df_nebuloss_dataset):
|
33 |
+
"""Turn the values of df_nebuloss (of a certain dataset) into the corresponding quantiles, computed along the columns
|
34 |
+
"""
|
35 |
+
# compute quantiles
|
36 |
+
quantiles = df_nebuloss_dataset.drop(columns = ['idx']).rank(pct = True)
|
37 |
+
# re-attach the original indices
|
38 |
+
quantiles['idx'] = df_nebuloss_dataset['idx']
|
39 |
+
|
40 |
+
return quantiles
|
41 |
+
|
42 |
+
# Streamlit app
|
43 |
+
def main():
|
44 |
+
# mapping the devices pseudo-symbols to actual names
|
45 |
+
device_mapping_dict = {
|
46 |
+
"edgegpu": "NVIDIA Jetson nano",
|
47 |
+
"eyeriss": "Eyeriss",
|
48 |
+
"fpga": "FPGA",
|
49 |
+
}
|
50 |
+
|
51 |
+
inverse_device_mapping_dict = {
|
52 |
+
"NVIDIA Jetson nano": "edgegpu",
|
53 |
+
"Eyeriss": "eyeriss",
|
54 |
+
"FPGA": "fpga"
|
55 |
+
}
|
56 |
+
|
57 |
+
# load the lookup table of NebulOS metrics
|
58 |
+
df_nebuloss = load_lookup_table()
|
59 |
+
# add a title
|
60 |
+
st.sidebar.title("🚀 NebulOS: Fair Green AI🌿")
|
61 |
+
st.sidebar.write(
|
62 |
+
"""
|
63 |
+
Welcome to the live demo of NebulOS! This Streamlit app serves the scope of presenting the results obtained with our
|
64 |
+
Hardware-Aware Training-Free Automated Architecture Design procedure.
|
65 |
+
|
66 |
+
You can check out the source code for the search process at https://www.github.com/fracapuano/NebulOS.
|
67 |
+
You can find an extended abstract of our solution at https://sites.google.com/view/nebulos.
|
68 |
+
|
69 |
+
Drop us a line if you want to know more about the project (and forget to ⭐ our GitHub repo).
|
70 |
+
|
71 |
+
Contact person: Francesco Capuano ({first}.{last}@asp-poli.it)
|
72 |
+
"""
|
73 |
+
)
|
74 |
+
|
75 |
+
# dropdown menu for dataset selection
|
76 |
+
dataset = st.sidebar.selectbox("Select Dataset", DATASETS)
|
77 |
+
|
78 |
+
# dropdown menu for device selection
|
79 |
+
device = st.sidebar.selectbox("Select Device", list(inverse_device_mapping_dict.keys()))
|
80 |
+
|
81 |
+
# mapping selected device to usable one
|
82 |
+
device = inverse_device_mapping_dict[device]
|
83 |
+
|
84 |
+
# slider for performance weight selection
|
85 |
+
performance_weight = st.sidebar.slider(
|
86 |
+
"Select trade-off between PERFORMANCE WEIGHT and HARDWARE WEIGHT.\nHigher values will give larger weight to validation accuracy, with less and less importance to the hardware performance.",
|
87 |
+
min_value=0.0,
|
88 |
+
max_value=1.0,
|
89 |
+
value=0.5,
|
90 |
+
step=0.05
|
91 |
+
)
|
92 |
+
# hardware weight (complementary to performance weight)
|
93 |
+
hardware_weight = 1.0 - performance_weight
|
94 |
+
|
95 |
+
# subset the dataframe for the current daset and device
|
96 |
+
df_nebuloss_dataset = subset_dataframe(df_nebuloss, dataset)
|
97 |
+
|
98 |
+
# best architecture index
|
99 |
+
best_arch_idx = 9930
|
100 |
+
|
101 |
+
# Trigger the search and plot NebulOS Architecture
|
102 |
+
searchspace_interface = HW_NATS_FastInterface(device=device, dataset=dataset)
|
103 |
+
search = GeneticSearch(
|
104 |
+
searchspace=searchspace_interface,
|
105 |
+
fitness_weights=np.array([performance_weight, hardware_weight])
|
106 |
+
)
|
107 |
+
|
108 |
+
results = search.solve(return_trajectory=True)
|
109 |
+
|
110 |
+
arch_idx = searchspace_interface.architecture_to_index["/".join(results[0].genotype)]
|
111 |
+
|
112 |
+
# Create scatter plot
|
113 |
+
scatter_trace1 = go.Scatter(
|
114 |
+
x=df_nebuloss_dataset.loc[df_nebuloss['dataset'] == dataset, f'{device}_energy'],
|
115 |
+
y=df_nebuloss_dataset.loc[df_nebuloss['dataset'] == dataset, 'validation_accuracy'],
|
116 |
+
mode='markers',
|
117 |
+
marker=dict(color='#D3D3D3', size=5),
|
118 |
+
name='Architectures in the search space'
|
119 |
+
)
|
120 |
+
|
121 |
+
# Scatter plot for best architecture
|
122 |
+
scatter_trace2 = go.Scatter(
|
123 |
+
x=df_nebuloss_dataset.loc[df_nebuloss_dataset['idx'] == best_arch_idx, f'{device}_energy'],
|
124 |
+
y=df_nebuloss_dataset.loc[df_nebuloss_dataset['idx'] == best_arch_idx, 'validation_accuracy'],
|
125 |
+
mode='markers',
|
126 |
+
marker=dict(color=TF_COLOR, symbol='circle-dot', size=12),
|
127 |
+
name='Best TF-Architecture'
|
128 |
+
)
|
129 |
+
|
130 |
+
scatter_trace3 = go.Scatter(
|
131 |
+
x=df_nebuloss_dataset.loc[df_nebuloss_dataset['idx'] == arch_idx, f'{device}_energy'],
|
132 |
+
y=df_nebuloss_dataset.loc[df_nebuloss_dataset['idx'] == arch_idx, 'validation_accuracy'],
|
133 |
+
mode='markers',
|
134 |
+
marker=dict(color=NEBULOS_COLOR, symbol='circle-dot', size=12),
|
135 |
+
name='NebulOS Architecture'
|
136 |
+
)
|
137 |
+
scatter_layout = go.Layout(
|
138 |
+
title=f'Validation Accuracy vs. {device_mapping_dict[device]} Energy Consumption',
|
139 |
+
xaxis=dict(title=f'{device.upper()} Energy'),
|
140 |
+
yaxis=dict(title='Validation Accuracy'),
|
141 |
+
showlegend=True
|
142 |
+
)
|
143 |
+
scatter_fig = go.Figure(data=[scatter_trace1, scatter_trace2, scatter_trace3], layout=scatter_layout)
|
144 |
+
|
145 |
+
# Extracting quantile values
|
146 |
+
metrics_considered = OrderedDict()
|
147 |
+
# these are the metrics that we want to plot
|
148 |
+
metrics_considered["flops"] = "FLOPS",
|
149 |
+
metrics_considered["params"] = "Num. Params",
|
150 |
+
metrics_considered["validation_accuracy"] = "Accuracy",
|
151 |
+
metrics_considered[f"{device}_energy"] = f"{device_mapping_dict[device]} - Energy Consumption",
|
152 |
+
metrics_considered[f"{device}_latency"] = f"{device_mapping_dict[device]} - Latency"
|
153 |
+
|
154 |
+
|
155 |
+
# this retrieves the optimal row
|
156 |
+
best_row_to_plot = df_nebuloss_dataset.loc[
|
157 |
+
df_nebuloss_dataset['idx'] == best_arch_idx,
|
158 |
+
list(metrics_considered.keys())
|
159 |
+
].values
|
160 |
+
|
161 |
+
# this retrieves the row that has been found by the NAS search
|
162 |
+
row_to_plot = df_nebuloss_dataset.loc[
|
163 |
+
df_nebuloss_dataset['idx'] == arch_idx,
|
164 |
+
list(metrics_considered.keys())
|
165 |
+
].values
|
166 |
+
|
167 |
+
row_to_plot = row_to_plot/best_row_to_plot
|
168 |
+
best_row_to_plot = best_row_to_plot/best_row_to_plot
|
169 |
+
|
170 |
+
best_row_to_plot = best_row_to_plot.flatten().tolist()
|
171 |
+
row_to_plot = row_to_plot.flatten().tolist()
|
172 |
+
|
173 |
+
# Bar chart for NebulOS Architecture
|
174 |
+
bar_trace1 = go.Bar(
|
175 |
+
x=list(metrics_considered.keys()),
|
176 |
+
y=row_to_plot,
|
177 |
+
name='NebulOS Architecture',
|
178 |
+
marker=dict(color=NEBULOS_COLOR)
|
179 |
+
)
|
180 |
+
# Bar chart for Best TF-Architecture
|
181 |
+
bar_trace2 = go.Bar(
|
182 |
+
x=list(metrics_considered.keys()),
|
183 |
+
y=best_row_to_plot,
|
184 |
+
name='Best TF-Architecture Found',
|
185 |
+
marker=dict(color=TF_COLOR)
|
186 |
+
)
|
187 |
+
# Layout configuration
|
188 |
+
bar_layout = go.Layout(
|
189 |
+
title=f'Hardware-Agnostic Architecture (blue) vs. NebulOS (red)',
|
190 |
+
yaxis=dict(title="(%)Hardware-Agnostic Architecture Value"),
|
191 |
+
barmode='group'
|
192 |
+
)
|
193 |
+
|
194 |
+
# Combining traces with the layout
|
195 |
+
bar_fig = go.Figure(data=[bar_trace2, bar_trace1], layout=bar_layout)
|
196 |
+
|
197 |
+
# Create two columns in Streamlit to show data near each other
|
198 |
+
col1, col2 = st.columns(2)
|
199 |
+
|
200 |
+
# Display scatter plot in the first column
|
201 |
+
with col1:
|
202 |
+
st.plotly_chart(scatter_fig)
|
203 |
+
|
204 |
+
# Display bar chart in the second column
|
205 |
+
with col2:
|
206 |
+
st.plotly_chart(bar_fig)
|
207 |
+
|
208 |
+
best_architecture = df_nebuloss_dataset.loc[
|
209 |
+
df_nebuloss_dataset['idx'] == best_arch_idx,
|
210 |
+
list(metrics_considered.keys())
|
211 |
+
]
|
212 |
+
|
213 |
+
best_architecture_string = searchspace_interface[best_arch_idx]["architecture_string"]
|
214 |
+
|
215 |
+
found_architecture = df_nebuloss_dataset.loc[
|
216 |
+
df_nebuloss_dataset['idx'] == arch_idx,
|
217 |
+
list(metrics_considered.keys())
|
218 |
+
]
|
219 |
+
|
220 |
+
message = \
|
221 |
+
f"""
|
222 |
+
<h4>NebulOS Search Process: Outcome</h4>
|
223 |
+
<p>
|
224 |
+
This search took ~{results[-1]*TIME_TO_SCORE_EACH_ARCHITECTURE} seconds (scoring {results[-1]} architectures using ~{TIME_TO_SCORE_EACH_ARCHITECTURE} seconds each)
|
225 |
+
</p>
|
226 |
+
The architecture found for <b>{device_mapping_dict[device]}</b> is: <b>{searchspace_interface[arch_idx]["architecture_string"]}</b><br>
|
227 |
+
The optimal (hardware-agnostic) architecture in the searchspace is <b>{best_architecture_string}</b>
|
228 |
+
</p>
|
229 |
+
<p>
|
230 |
+
You can find the recap, in terms of the percentage of the Training-Free metric found in the table to your right 👉
|
231 |
+
</p>
|
232 |
+
"""
|
233 |
+
|
234 |
+
# Sample data - replace these with your actual ratio values
|
235 |
+
data = {
|
236 |
+
"Metric": ["FLOPS", "Number of Parameters", "Validation Accuracy", "Energy Consumption", "Latency"],
|
237 |
+
"NebulOS vs. Hardware Agnostic Network": ["{:.2g}%".format(val) for val in row_to_plot]
|
238 |
+
}
|
239 |
+
|
240 |
+
col1, _, col2 = st.columns([2,1,2])
|
241 |
+
recap_df = pd.DataFrame(data).sort_values(by="Metric").set_index("Metric")
|
242 |
+
|
243 |
+
with col1:
|
244 |
+
st.write(message, unsafe_allow_html=True)
|
245 |
+
|
246 |
+
with col2:
|
247 |
+
st.dataframe(recap_df)
|
248 |
+
|
249 |
+
if __name__ == "__main__":
|
250 |
+
main()
|
data/df_nebuloss.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
data/nats_arch_index.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
data/nebuloss_1.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
data/nebuloss_2.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
data/nebuloss_3.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
data/nebuloss_4.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
nas.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from src.search import GeneticSearch
|
2 |
+
from src.hw_nats_fast_interface import HW_NATS_FastInterface
|
3 |
+
from src.utils import DEVICES, union_of_dicts
|
4 |
+
import numpy as np
|
5 |
+
import argparse
|
6 |
+
import json
|
7 |
+
|
8 |
+
def parse_args()->object:
|
9 |
+
"""Args function.
|
10 |
+
Returns:
|
11 |
+
(object): args parser
|
12 |
+
"""
|
13 |
+
parser = argparse.ArgumentParser()
|
14 |
+
# this selects the dataset to be considered for the search
|
15 |
+
parser.add_argument(
|
16 |
+
"--dataset",
|
17 |
+
default="cifar10",
|
18 |
+
type=str,
|
19 |
+
help="Dataset to be considered. One in ['cifar10', 'cifar100', 'ImageNet16-120'].s",
|
20 |
+
choices=["cifar10", "cifar100", "ImageNet16-120"]
|
21 |
+
)
|
22 |
+
# this selects the target device to be considered for the search
|
23 |
+
parser.add_argument(
|
24 |
+
"--device",
|
25 |
+
default="edgegpu",
|
26 |
+
type=str,
|
27 |
+
help="Device to be considered. One in ['edgegpu', 'eyeriss', 'fpga'].",
|
28 |
+
choices=["edgegpu", "eyeriss", "fpga"]
|
29 |
+
)
|
30 |
+
# when this flag is triggered, the search is hardware-agnostic (penalized with FLOPS and params)
|
31 |
+
parser.add_argument("--device-agnostic", action="store_true", help="Flag to trigger hardware-agnostic search.")
|
32 |
+
|
33 |
+
parser.add_argument("--n-generations", default=50, type=int, help="Number of generations to let the genetic algorithm run.")
|
34 |
+
parser.add_argument("--n-runs", default=30, type=int, help="Number of runs used to compute the average test accuracy.")
|
35 |
+
|
36 |
+
parser.add_argument("--performance-weight", default=0.65, type=float, help="Weight of the performance metric in the fitness function.")
|
37 |
+
parser.add_argument("--hardware-weight", default=0.35, type=float, help="Weight of the hardware metric in the fitness function.")
|
38 |
+
|
39 |
+
return parser.parse_args()
|
40 |
+
|
41 |
+
def main():
|
42 |
+
# parse arguments
|
43 |
+
args = parse_args()
|
44 |
+
|
45 |
+
dataset = args.dataset
|
46 |
+
device = args.device if args.device in DEVICES else None
|
47 |
+
n_generations = args.n_generations
|
48 |
+
n_runs = args.n_runs
|
49 |
+
performance_weight, hardware_weight = args.performance_weight, args.hardware_weight
|
50 |
+
|
51 |
+
if performance_weight + hardware_weight > 1.0 + 1e-6:
|
52 |
+
error_msg = f"""
|
53 |
+
Performance weight: {performance_weight}, Hardware weight: {hardware_weight} (they sum up to {performance_weight + hardware_weight}).
|
54 |
+
The sum of the weights must be less than 1.
|
55 |
+
"""
|
56 |
+
raise ValueError(error_msg)
|
57 |
+
|
58 |
+
nebulos_chunks = []
|
59 |
+
for i in range(4): # the number of chunks is 4 in this case
|
60 |
+
with open(f"data/nebuloss_{i+1}.json", "r") as f:
|
61 |
+
nebulos_chunks.append(json.load(f))
|
62 |
+
|
63 |
+
searchspace_dict = union_of_dicts(nebulos_chunks)
|
64 |
+
|
65 |
+
# initialize the search space given dataset and device
|
66 |
+
searchspace_interface = HW_NATS_FastInterface(datapath=searchspace_dict, device=args.device, dataset=args.dataset)
|
67 |
+
search = GeneticSearch(
|
68 |
+
searchspace=searchspace_interface,
|
69 |
+
fitness_weights=np.array([performance_weight, hardware_weight])
|
70 |
+
)
|
71 |
+
# this perform the actual architecture search
|
72 |
+
results = search.solve(max_generations=n_generations)
|
73 |
+
|
74 |
+
print(f'{dataset}-{device.upper() if device is not None else device}')
|
75 |
+
print(results[0].genotype, results[0].genotype_to_idx["/".join(results[0].genotype)], results[1])
|
76 |
+
print()
|
77 |
+
|
78 |
+
if __name__=="__main__":
|
79 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
matplotlib==3.7.2
|
2 |
+
numpy==1.25.2
|
3 |
+
pandas==2.1.0
|
4 |
+
streamlit==1.26.0
|
split_in_chunks.ipynb
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 6,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [],
|
8 |
+
"source": [
|
9 |
+
"import json\n",
|
10 |
+
"with open(\"data/nebuloss.json\", \"r\") as f:\n",
|
11 |
+
" data = json.load(f)"
|
12 |
+
]
|
13 |
+
},
|
14 |
+
{
|
15 |
+
"cell_type": "code",
|
16 |
+
"execution_count": 7,
|
17 |
+
"metadata": {},
|
18 |
+
"outputs": [],
|
19 |
+
"source": [
|
20 |
+
"def split_dict(d, n):\n",
|
21 |
+
" \"\"\"\n",
|
22 |
+
" Splits a dictionary into n dictionaries with almost equal number of items.\n",
|
23 |
+
"\n",
|
24 |
+
" Parameters:\n",
|
25 |
+
" - d (dict): The input dictionary.\n",
|
26 |
+
" - n (int): The number of dictionaries to split into.\n",
|
27 |
+
"\n",
|
28 |
+
" Returns:\n",
|
29 |
+
" - list of dict: A list of n dictionaries.\n",
|
30 |
+
" \"\"\"\n",
|
31 |
+
" items = list(d.items())\n",
|
32 |
+
" length = len(items)\n",
|
33 |
+
" \n",
|
34 |
+
" # Calculate the size of each chunk\n",
|
35 |
+
" chunk_size = length // n\n",
|
36 |
+
" remainder = length % n\n",
|
37 |
+
"\n",
|
38 |
+
" # Split the items into chunks\n",
|
39 |
+
" chunks = []\n",
|
40 |
+
" start = 0\n",
|
41 |
+
"\n",
|
42 |
+
" for i in range(n):\n",
|
43 |
+
" if remainder:\n",
|
44 |
+
" end = start + chunk_size + 1\n",
|
45 |
+
" remainder -= 1\n",
|
46 |
+
" else:\n",
|
47 |
+
" end = start + chunk_size\n",
|
48 |
+
" chunks.append(dict(items[start:end]))\n",
|
49 |
+
" start = end\n",
|
50 |
+
"\n",
|
51 |
+
" return chunks\n"
|
52 |
+
]
|
53 |
+
},
|
54 |
+
{
|
55 |
+
"cell_type": "code",
|
56 |
+
"execution_count": 8,
|
57 |
+
"metadata": {},
|
58 |
+
"outputs": [],
|
59 |
+
"source": [
|
60 |
+
"chunk_1, chunk_2, chunk_3, chunk_4 = split_dict(data, n=4)"
|
61 |
+
]
|
62 |
+
},
|
63 |
+
{
|
64 |
+
"cell_type": "code",
|
65 |
+
"execution_count": 13,
|
66 |
+
"metadata": {},
|
67 |
+
"outputs": [],
|
68 |
+
"source": [
|
69 |
+
"with open(\"data/nebuloss_1.json\", \"w\") as f:\n",
|
70 |
+
" json.dump(chunk_1, f, indent=4)\n",
|
71 |
+
"with open(\"data/nebuloss_2.json\", \"w\") as f:\n",
|
72 |
+
" json.dump(chunk_2, f, indent=4)\n",
|
73 |
+
"with open(\"data/nebuloss_3.json\", \"w\") as f:\n",
|
74 |
+
" json.dump(chunk_3, f, indent=4)\n",
|
75 |
+
"with open(\"data/nebuloss_4.json\", \"w\") as f:\n",
|
76 |
+
" json.dump(chunk_4, f, indent=4)"
|
77 |
+
]
|
78 |
+
},
|
79 |
+
{
|
80 |
+
"cell_type": "code",
|
81 |
+
"execution_count": null,
|
82 |
+
"metadata": {},
|
83 |
+
"outputs": [],
|
84 |
+
"source": []
|
85 |
+
}
|
86 |
+
],
|
87 |
+
"metadata": {
|
88 |
+
"kernelspec": {
|
89 |
+
"display_name": "hackenv",
|
90 |
+
"language": "python",
|
91 |
+
"name": "python3"
|
92 |
+
},
|
93 |
+
"language_info": {
|
94 |
+
"codemirror_mode": {
|
95 |
+
"name": "ipython",
|
96 |
+
"version": 3
|
97 |
+
},
|
98 |
+
"file_extension": ".py",
|
99 |
+
"mimetype": "text/x-python",
|
100 |
+
"name": "python",
|
101 |
+
"nbconvert_exporter": "python",
|
102 |
+
"pygments_lexer": "ipython3",
|
103 |
+
"version": "3.10.12"
|
104 |
+
},
|
105 |
+
"orig_nbformat": 4
|
106 |
+
},
|
107 |
+
"nbformat": 4,
|
108 |
+
"nbformat_minor": 2
|
109 |
+
}
|
src/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from .hw_nats_fast_interface import *
|
2 |
+
from .genetics import *
|
3 |
+
from .utils import *
|
src/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (214 Bytes). View file
|
|
src/__pycache__/genetics.cpython-310.pyc
ADDED
Binary file (14.5 kB). View file
|
|
src/__pycache__/hw_nats_fast_interface.cpython-310.pyc
ADDED
Binary file (10.3 kB). View file
|
|
src/__pycache__/utils.cpython-310.pyc
ADDED
Binary file (870 Bytes). View file
|
|
src/genetics.py
ADDED
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Iterable, Callable, Tuple, List, Union, Dict
|
2 |
+
import numpy as np
|
3 |
+
from copy import deepcopy as copy
|
4 |
+
from .utils import *
|
5 |
+
from itertools import chain
|
6 |
+
from abc import abstractproperty, abstractmethod
|
7 |
+
from .hw_nats_fast_interface import HW_NATS_FastInterface
|
8 |
+
|
9 |
+
|
10 |
+
class Individual:
|
11 |
+
"""
|
12 |
+
Base Class for all individuals in the population.
|
13 |
+
Base class attributes are the genotype identifying the individual (and, therefore, the network) and its
|
14 |
+
index within the search space it is drawn from.
|
15 |
+
"""
|
16 |
+
def __init__(self, genotype:List[str], index:int):
|
17 |
+
self._genotype = genotype
|
18 |
+
self.index=index
|
19 |
+
self._fitness = None
|
20 |
+
|
21 |
+
@abstractproperty
|
22 |
+
def genotype(self):
|
23 |
+
"""This class is used to define the network architecture."""
|
24 |
+
raise NotImplementedError("Implement this property in child classes!")
|
25 |
+
|
26 |
+
@abstractproperty
|
27 |
+
def fitness(self):
|
28 |
+
"""This class is used to define the fitness of the individual."""
|
29 |
+
raise NotImplementedError("Implement this property in child classes!")
|
30 |
+
|
31 |
+
@abstractmethod
|
32 |
+
def update_idx(self):
|
33 |
+
"""Update the index of the individual in the population"""
|
34 |
+
raise NotImplementedError("Implement this method in child classes!")
|
35 |
+
|
36 |
+
@abstractmethod
|
37 |
+
def update_genotype(self, new_genotype:List[str]):
|
38 |
+
"""Update current genotype with new one. When doing so, also the network field is updated"""
|
39 |
+
raise NotImplementedError("Implement this method in child classes!")
|
40 |
+
|
41 |
+
@abstractmethod
|
42 |
+
def update_fitness(self, metric:Callable, attribute:str="net"):
|
43 |
+
"""Update the current value of fitness using provided metric"""
|
44 |
+
raise NotImplementedError("Implement this method in child classes!")
|
45 |
+
|
46 |
+
|
47 |
+
class FastIndividual(Individual):
|
48 |
+
"""
|
49 |
+
Fast individuals are used in the context of age-regularized genetic algorithms and, therefore, are
|
50 |
+
characterized by an additional field, i.e. age.
|
51 |
+
"""
|
52 |
+
def __init__(
|
53 |
+
self,
|
54 |
+
genotype:List[str],
|
55 |
+
index:int,
|
56 |
+
genotype_to_idx:Dict[str, int],
|
57 |
+
age:int=0):
|
58 |
+
|
59 |
+
# init parent class
|
60 |
+
super().__init__(genotype, index)
|
61 |
+
|
62 |
+
self.age = age
|
63 |
+
self.genotype_to_idx = genotype_to_idx
|
64 |
+
|
65 |
+
@property
|
66 |
+
def genotype(self):
|
67 |
+
return self._genotype
|
68 |
+
|
69 |
+
@property
|
70 |
+
def fitness(self):
|
71 |
+
return self._fitness
|
72 |
+
|
73 |
+
def update_idx(self):
|
74 |
+
self.index = self.genotype_to_idx["/".join(self._genotype)]
|
75 |
+
|
76 |
+
def update_genotype(self, new_genotype:List[str]):
|
77 |
+
"""Update current genotype with new one. When doing so, also the network field is updated"""
|
78 |
+
self._genotype = new_genotype
|
79 |
+
self.update_idx()
|
80 |
+
|
81 |
+
def update_fitness(self, metric:Callable, attribute:str="net"):
|
82 |
+
"""Update the current value of fitness using provided metric"""
|
83 |
+
self._fitness = metric(getattr(self, attribute))
|
84 |
+
|
85 |
+
class Genetic:
|
86 |
+
def __init__(
|
87 |
+
self,
|
88 |
+
genome:Iterable[str],
|
89 |
+
searchspace:HW_NATS_FastInterface,
|
90 |
+
strategy:Tuple[str, str]="comma",
|
91 |
+
tournament_size:int=5):
|
92 |
+
|
93 |
+
self.genome = set(genome) if not isinstance(genome, set) else genome
|
94 |
+
self.strategy = strategy
|
95 |
+
self.tournament_size = tournament_size
|
96 |
+
self.searchspace = searchspace
|
97 |
+
|
98 |
+
def tournament(self, population:Iterable[Individual]) -> Iterable[Individual]:
|
99 |
+
"""
|
100 |
+
Return tournament, i.e. a random subset of population of size tournament size.
|
101 |
+
Sampling is done without replacement to ensure diversity inside the actual tournament.
|
102 |
+
"""
|
103 |
+
return np.random.choice(a=population, size=self.tournament_size, replace=False).tolist()
|
104 |
+
|
105 |
+
def obtain_parents(self, population:Iterable[Individual], n_parents:int=2) -> Iterable[Individual]:
|
106 |
+
"""Obtain n_parents from population. Parents are defined as the fittest individuals in n_parents tournaments"""
|
107 |
+
tournament = self.tournament(population = population)
|
108 |
+
# parents are defined as fittest individuals in tournaments
|
109 |
+
parents = sorted(tournament, key = lambda individual: individual.fitness, reverse=True)[:n_parents]
|
110 |
+
return parents
|
111 |
+
|
112 |
+
def mutate(self,
|
113 |
+
individual:Individual,
|
114 |
+
n_loci:int=1,
|
115 |
+
genes_prob:Tuple[None, List[float]]=None) -> Individual:
|
116 |
+
"""Applies mutation to a given individual"""
|
117 |
+
for _ in range(n_loci):
|
118 |
+
mutant_genotype = copy(individual.genotype)
|
119 |
+
# select a locus in the genotype (that is, where mutation will occurr)
|
120 |
+
if genes_prob is None: # uniform probability over all loci
|
121 |
+
mutant_locus = np.random.randint(low=0, high=len(mutant_genotype))
|
122 |
+
else: # custom probability distrubution over which locus to mutate
|
123 |
+
mutant_locus = np.random.choice(mutant_genotype, p=genes_prob)
|
124 |
+
# mapping the locus to the actual gene that will effectively change
|
125 |
+
mutant_gene = mutant_genotype[mutant_locus]
|
126 |
+
operation, level = mutant_gene.split("~") # splits the gene into operation and level
|
127 |
+
# mutation changes gene, so the current one must be removed from the pool of candidate genes
|
128 |
+
mutations = self.genome.difference([operation])
|
129 |
+
|
130 |
+
# overwriting the mutant gene with a new one - probability of chosing how to mutate should be selected as well
|
131 |
+
mutant_genotype[mutant_locus] = np.random.choice(a=list(mutations)) + f"~{level}"
|
132 |
+
|
133 |
+
mutant_individual = FastIndividual(genotype=None, genotype_to_idx=self.searchspace.architecture_to_index, index=None)
|
134 |
+
mutant_individual.update_genotype(mutant_genotype)
|
135 |
+
|
136 |
+
return mutant_individual
|
137 |
+
|
138 |
+
def recombine(self, individuals:Iterable[Individual], P_parent1:float=0.5) -> Individual:
|
139 |
+
"""Performs recombination of two given `individuals`"""
|
140 |
+
if len(individuals) != 2:
|
141 |
+
raise ValueError("Number of individuals cannot be different from 2!")
|
142 |
+
|
143 |
+
individual1, individual2 = individuals
|
144 |
+
recombinant_genotype = [None for _ in range(len(individual1.genotype))]
|
145 |
+
for locus_idx, (gene_1, gene_2) in enumerate(zip(individual1.genotype, individual2.genotype)):
|
146 |
+
# chose genes from parent1 according to P_parent1
|
147 |
+
recombinant_genotype[locus_idx] = gene_1 if np.random.random() <= P_parent1 else gene_2
|
148 |
+
|
149 |
+
recombinant = FastIndividual(genotype=None, genotype_to_idx=self.searchspace.architecture_to_index, index=None)
|
150 |
+
recombinant.update_genotype(list(recombinant_genotype))
|
151 |
+
|
152 |
+
return recombinant
|
153 |
+
|
154 |
+
class Population:
|
155 |
+
def __init__(self,
|
156 |
+
searchspace:object,
|
157 |
+
init_population:Union[bool, Iterable]=True,
|
158 |
+
n_individuals:int=20,
|
159 |
+
normalization:str='dynamic'):
|
160 |
+
self.searchspace = searchspace
|
161 |
+
if init_population is True:
|
162 |
+
self._population = generate_population(searchspace_interface=searchspace, n_individuals=n_individuals)
|
163 |
+
else:
|
164 |
+
self._population = init_population
|
165 |
+
|
166 |
+
self.oldest = None
|
167 |
+
self.worst_n = None
|
168 |
+
self.normalization = normalization.lower()
|
169 |
+
|
170 |
+
def __iter__(self):
|
171 |
+
for i in self._population:
|
172 |
+
yield i
|
173 |
+
|
174 |
+
@property
|
175 |
+
def individuals(self):
|
176 |
+
return self._population
|
177 |
+
|
178 |
+
def update_population(self, new_population:Iterable[Individual]):
|
179 |
+
"""Overwrites current population with new one stored in `new_population`"""
|
180 |
+
if all([isinstance(el, Individual) for el in new_population]):
|
181 |
+
del self._population
|
182 |
+
self._population = new_population
|
183 |
+
else:
|
184 |
+
raise ValueError("new_population is not an Iterable of `Individual` datatype!")
|
185 |
+
|
186 |
+
def fittest_n(self, n:int=1):
|
187 |
+
"""Return first `n` individuals based on fitness value"""
|
188 |
+
return sorted(self._population, key=lambda individual: individual.fitness, reverse=True)[:n]
|
189 |
+
|
190 |
+
def update_ranking(self):
|
191 |
+
"""Updates the ranking in the population in light of fitness value"""
|
192 |
+
sorted_individuals = sorted(self._population, key=lambda individual: individual.fitness, reverse=True)
|
193 |
+
|
194 |
+
# ranking in light of individuals
|
195 |
+
for ranking, individual in enumerate(sorted_individuals):
|
196 |
+
individual.update_ranking(new_rank=ranking)
|
197 |
+
|
198 |
+
def update_fitness(self, fitness_function:Callable):
|
199 |
+
"""Updates the fitness value of individuals in the population"""
|
200 |
+
for individual in self.individuals:
|
201 |
+
fitness_function(individual)
|
202 |
+
|
203 |
+
def apply_on_individuals(self, function:Callable)->Union[Iterable, None]:
|
204 |
+
"""Applies a function on each individual in the population
|
205 |
+
|
206 |
+
Args:
|
207 |
+
function (Callable): function to apply on each individual. Must return an object of class Individual.
|
208 |
+
Returns:
|
209 |
+
Union[Iterable, None]: Iterable when inplace=False represents the individuals with function applied.
|
210 |
+
None represents the output when inplace=True (hence function is applied on the
|
211 |
+
actual population.
|
212 |
+
"""
|
213 |
+
self._population = [function(individual) for individual in self._population]
|
214 |
+
|
215 |
+
def set_extremes(self, score:str):
|
216 |
+
"""Set the maximal&minimal value in the population for the score 'score' (must be a class attribute)"""
|
217 |
+
if self.normalization == 'dynamic':
|
218 |
+
# accessing to the score of each individual
|
219 |
+
scores = [getattr(ind, score) for ind in self.individuals]
|
220 |
+
min_value = min(scores)
|
221 |
+
max_value = max(scores)
|
222 |
+
elif self.normalization == 'minmax':
|
223 |
+
# extreme_scores is a 2x`number_of_scores`
|
224 |
+
min_value, max_value = self.extreme_scores[:, self.scores_dict[score]]
|
225 |
+
elif self.normalization == 'standard':
|
226 |
+
# extreme_scores is a 2x`number_of_scores`
|
227 |
+
mean_value, std_value = self.extreme_scores[:, self.scores_dict[score]]
|
228 |
+
|
229 |
+
if self.normalization in ['minmax', 'dynamic']:
|
230 |
+
setattr(self, f"max_{score}", max_value)
|
231 |
+
setattr(self, f"min_{score}", min_value)
|
232 |
+
else:
|
233 |
+
setattr(self, f"mean_{score}", mean_value)
|
234 |
+
setattr(self, f"std_{score}", std_value)
|
235 |
+
|
236 |
+
def age(self):
|
237 |
+
"""Embeds ageing into the process"""
|
238 |
+
def individuals_ageing(individual):
|
239 |
+
individual.age += 1
|
240 |
+
return individual
|
241 |
+
|
242 |
+
self.apply_on_individuals(function=individuals_ageing)
|
243 |
+
|
244 |
+
def add_to_population(self, new_individuals:Iterable[Individual]):
|
245 |
+
"""Add new_individuals to population"""
|
246 |
+
self._population = list(chain(self.individuals, new_individuals))
|
247 |
+
|
248 |
+
def remove_from_population(self, attribute:str="fitness", n:int=1, ascending:bool=True):
|
249 |
+
"""
|
250 |
+
Remove first/last `n` elements from sorted population population in `ascending/descending`
|
251 |
+
order based on the value of `attribute`.
|
252 |
+
"""
|
253 |
+
# check that input attribute is an attribute of all the individuals
|
254 |
+
if not all([hasattr(el, attribute) for el in self.individuals]):
|
255 |
+
raise ValueError(f"Attribute '{attribute}' is not an attribute of all the individuals!")
|
256 |
+
|
257 |
+
# sort the population based on the value of attribute
|
258 |
+
sorted_population = sorted(self.individuals, key=lambda ind: getattr(ind, attribute), reverse=False if ascending else True)
|
259 |
+
# new population is old population minus the `n` worst individuals with respect to `attribute`
|
260 |
+
self.update_population(sorted_population[n:])
|
261 |
+
|
262 |
+
def update_oldest(self, candidate:Individual):
|
263 |
+
"""Updates oldest individual in the population"""
|
264 |
+
if candidate.age >= self.oldest.age:
|
265 |
+
self.oldest = candidate
|
266 |
+
else:
|
267 |
+
pass
|
268 |
+
|
269 |
+
def update_worst_n(self, candidate:Individual, attribute:str="fitness", n:int=2):
|
270 |
+
"""Updates worst_n elements in the population"""
|
271 |
+
if hasattr(candidate, attribute):
|
272 |
+
if any([getattr(candidate, attribute) < getattr(worst, attribute) for worst in self.worst_n]):
|
273 |
+
# candidate is worse than one of the worst individuals
|
274 |
+
bad_individuals = self.worst_n + candidate
|
275 |
+
# sort in increasing values of fitness
|
276 |
+
bad_sorted = sorted(bad_individuals, lambda ind: getattr(ind, attribute))
|
277 |
+
self.worst_n = bad_sorted[:n] # return new worst individuals
|
278 |
+
|
279 |
+
def set_oldest(self):
|
280 |
+
"""Sets oldest individual in population"""
|
281 |
+
self.oldest = max(self.individuals, key=lambda ind: ind.age)
|
282 |
+
|
283 |
+
def set_worst_n(self, attribute:str="fitness", n:int=2):
|
284 |
+
"""Sets worst n elements based on the value of arbitrary attribute"""
|
285 |
+
self.worst_n = sorted(self.individuals, key=lambda ind: getattr(ind, attribute))[:n]
|
286 |
+
|
287 |
+
|
288 |
+
def generate_population(searchspace_interface:HW_NATS_FastInterface, n_individuals:int=20)->list:
|
289 |
+
"""This function generates a population of FastInviduals based on the searchspace interface"""
|
290 |
+
# at first generate full cell-structure and unique network indices
|
291 |
+
cells, indices = searchspace_interface.generate_random_samples(n_samples=n_individuals)
|
292 |
+
|
293 |
+
# mapping strings to list of genes (~genomes)
|
294 |
+
genotypes = map(lambda cell: searchspace_interface.architecture_to_list(cell), cells)
|
295 |
+
# turn full architecture and cell-structure into genetic population individual
|
296 |
+
population = [
|
297 |
+
FastIndividual(genotype=genotype, index=index, genotype_to_idx=searchspace_interface.architecture_to_index)
|
298 |
+
for genotype, index in zip(genotypes, indices)
|
299 |
+
]
|
300 |
+
|
301 |
+
return population
|
src/hw_nats_fast_interface.py
ADDED
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Set, Text, List, Tuple, Dict
|
2 |
+
from itertools import chain
|
3 |
+
from .utils import get_project_root
|
4 |
+
import numpy as np
|
5 |
+
import json
|
6 |
+
|
7 |
+
class HW_NATS_FastInterface:
|
8 |
+
def __init__(self,
|
9 |
+
datapath:str=str(get_project_root()) + "/data/nebuloss.json",
|
10 |
+
indexpath:str=str(get_project_root()) + "/data/nats_arch_index.json",
|
11 |
+
dataset:str="cifar10",
|
12 |
+
device:Text="edgegpu",
|
13 |
+
scores_sample_size:int=1e3):
|
14 |
+
|
15 |
+
AVAILABLE_DATASETS = ["cifar10", "cifar100", "ImageNet16-120"]
|
16 |
+
AVAILABLE_DEVICES = ["edgegpu", "eyeriss", "fpga"]
|
17 |
+
# catch input errors
|
18 |
+
if dataset not in AVAILABLE_DATASETS:
|
19 |
+
raise ValueError(f"Dataset {dataset} not in {AVAILABLE_DATASETS}!")
|
20 |
+
|
21 |
+
if device not in AVAILABLE_DEVICES and device is not None:
|
22 |
+
raise ValueError(f"Device {device} not in {AVAILABLE_DEVICES}!")
|
23 |
+
|
24 |
+
if isinstance(datapath, str):
|
25 |
+
# parent init
|
26 |
+
with open(datapath, "r") as datafile:
|
27 |
+
self._data = {
|
28 |
+
int(key): value for key, value in json.load(datafile).items()
|
29 |
+
}
|
30 |
+
elif isinstance(datapath, dict):
|
31 |
+
self._data = {
|
32 |
+
int(key): value for key, value in datapath.items()
|
33 |
+
}
|
34 |
+
else:
|
35 |
+
raise ValueError(f"Datapath must be either a string or a dictionary, not {type(datapath)}")
|
36 |
+
|
37 |
+
# importing the "/"-architecture <-> index from a json file
|
38 |
+
with open(indexpath, "r") as indexfile:
|
39 |
+
self._architecture_to_index = json.load(indexfile)
|
40 |
+
|
41 |
+
# store dataset field
|
42 |
+
self._dataset = dataset
|
43 |
+
self.target_device = device
|
44 |
+
# architectures to use to estimate mean and std for scores normalization
|
45 |
+
self.random_indices = np.random.choice(len(self), int(scores_sample_size), replace=False)
|
46 |
+
|
47 |
+
def __len__(self)->int:
|
48 |
+
"""Number of architectures in considered search space."""
|
49 |
+
return len(self._data)
|
50 |
+
|
51 |
+
def __getitem__(self, idx:int) -> Dict:
|
52 |
+
"""Returns (untrained) network corresponding to index `idx`"""
|
53 |
+
return self._data[idx]
|
54 |
+
|
55 |
+
def __iter__(self):
|
56 |
+
"""Iterator method"""
|
57 |
+
self.iteration_index = 0
|
58 |
+
return self
|
59 |
+
|
60 |
+
def __next__(self):
|
61 |
+
if self.iteration_index >= self.__len__():
|
62 |
+
raise StopIteration
|
63 |
+
# access current element
|
64 |
+
net = self[self.iteration_index]
|
65 |
+
# update the iteration index
|
66 |
+
self.iteration_index += 1
|
67 |
+
return net
|
68 |
+
|
69 |
+
@property
|
70 |
+
def data(self):
|
71 |
+
return self._data
|
72 |
+
|
73 |
+
@property
|
74 |
+
def architecture_to_index(self):
|
75 |
+
return self._architecture_to_index
|
76 |
+
|
77 |
+
@property
|
78 |
+
def name(self)->Text:
|
79 |
+
return "nats"
|
80 |
+
|
81 |
+
@property
|
82 |
+
def ordered_all_ops(self)->List[Text]:
|
83 |
+
"""NASTS Bench available operations, ordered (without any precise logic)"""
|
84 |
+
return ['skip_connect', 'nor_conv_1x1', 'nor_conv_3x3', 'none', 'avg_pool_3x3']
|
85 |
+
|
86 |
+
@property
|
87 |
+
def architecture_len(self)->int:
|
88 |
+
"""Returns the number of different operations that uniquevoly define a given architecture"""
|
89 |
+
return 6
|
90 |
+
|
91 |
+
@property
|
92 |
+
def all_ops(self)->Set[Text]:
|
93 |
+
"""NASTS Bench available operations."""
|
94 |
+
return {'skip_connect', 'nor_conv_1x1', 'nor_conv_3x3', 'none', 'avg_pool_3x3'}
|
95 |
+
|
96 |
+
@property
|
97 |
+
def dataset(self)->Text:
|
98 |
+
return self._dataset
|
99 |
+
|
100 |
+
@dataset.setter
|
101 |
+
def change_dataset(self, new_dataset:Text)->None:
|
102 |
+
"""
|
103 |
+
Updates the current dataset with a new one.
|
104 |
+
Raises ValueError when new_dataset is not one of ["cifar10", "cifar100", "imagenet16-120"]
|
105 |
+
"""
|
106 |
+
if new_dataset.lower() in self.NATS_datasets:
|
107 |
+
self._dataset = new_dataset
|
108 |
+
else:
|
109 |
+
raise ValueError(f"New dataset {new_dataset} not in {self.NATS_datasets}")
|
110 |
+
|
111 |
+
def get_score_mean(self, score_name:Text)->float:
|
112 |
+
"""
|
113 |
+
Calculate the mean score value across the dataset for the given score name.
|
114 |
+
|
115 |
+
Args:
|
116 |
+
score_name (Text): The name of the score for which to calculate the mean.
|
117 |
+
|
118 |
+
Returns:
|
119 |
+
float: The mean score value.
|
120 |
+
|
121 |
+
Note:
|
122 |
+
The score values are retrieved from each data point in the dataset and averaged.
|
123 |
+
"""
|
124 |
+
if not hasattr(self, f"mean_{score_name}"):
|
125 |
+
# compute the mean on 1000 instances
|
126 |
+
mean_score = np.mean([self[i][self.dataset][score_name] for i in self.random_indices])
|
127 |
+
|
128 |
+
# set the mean score accordingly
|
129 |
+
setattr(self, f"mean_{score_name}", mean_score)
|
130 |
+
self.get_score_mean(score_name=score_name)
|
131 |
+
|
132 |
+
return getattr(self, f"mean_{score_name}")
|
133 |
+
|
134 |
+
def get_score_std(self, score_name: Text) -> float:
|
135 |
+
"""
|
136 |
+
Calculate the standard deviation of the score values across the dataset for the given score name.
|
137 |
+
|
138 |
+
Args:
|
139 |
+
score_name (Text): The name of the score for which to calculate the standard deviation.
|
140 |
+
|
141 |
+
Returns:
|
142 |
+
float: The standard deviation of the score values.
|
143 |
+
|
144 |
+
Note:
|
145 |
+
The score values are retrieved from each data point in the dataset, and the standard deviation is calculated.
|
146 |
+
"""
|
147 |
+
if not hasattr(self, f"std_{score_name}"):
|
148 |
+
# compute the mean on 1000 instances
|
149 |
+
std_score = np.std([self[i][self.dataset][score_name] for i in self.random_indices])
|
150 |
+
|
151 |
+
# set the mean score accordingly
|
152 |
+
setattr(self, f"std_{score_name}", std_score)
|
153 |
+
self.get_score_std(score_name=score_name)
|
154 |
+
|
155 |
+
return getattr(self, f"std_{score_name}")
|
156 |
+
|
157 |
+
def generate_random_samples(self, n_samples:int=10)->Tuple[List[Text], List[int]]:
|
158 |
+
"""Generate a group of architectures chosen at random"""
|
159 |
+
idxs = np.random.choice(self.__len__(), size=n_samples, replace=False)
|
160 |
+
cell_structures = [self[i]["architecture_string"] for i in idxs]
|
161 |
+
# return tinynets, cell_structures_string and the unique indices of the networks
|
162 |
+
return cell_structures, idxs
|
163 |
+
|
164 |
+
def list_to_architecture(self, input_list:List[str])->str:
|
165 |
+
"""
|
166 |
+
Reformats genotype as architecture string.
|
167 |
+
This function clearly is specific for this very search space.
|
168 |
+
"""
|
169 |
+
return "|{}|+|{}|{}|+|{}|{}|{}|".format(*input_list)
|
170 |
+
|
171 |
+
def architecture_to_list(self, architecture_string:Text)->List[Text]:
|
172 |
+
"""Turn architectures string into genotype list
|
173 |
+
|
174 |
+
Args:
|
175 |
+
architecture_string(str): String characterising the cell structure only.
|
176 |
+
|
177 |
+
Returns:
|
178 |
+
List[str]: List containing the operations in the input cell structure.
|
179 |
+
In a genetic-algorithm setting, this description represents a genotype.
|
180 |
+
"""
|
181 |
+
# divide the input string into different levels
|
182 |
+
subcells = architecture_string.split("+")
|
183 |
+
# divide into different nodes to retrieve ops
|
184 |
+
ops = chain(*[subcell.split("|")[1:-1] for subcell in subcells])
|
185 |
+
|
186 |
+
return list(ops)
|
187 |
+
|
188 |
+
def list_to_accuracy(self, input_list:List[str])->float:
|
189 |
+
"""Returns the test accuracy of an input list representing the architecture.
|
190 |
+
This list contains the operations.
|
191 |
+
|
192 |
+
Args:
|
193 |
+
input_list (List[str]): List of operations inside the architecture.
|
194 |
+
|
195 |
+
Returns:
|
196 |
+
float: Test accuracy (after 200 training epochs).
|
197 |
+
"""
|
198 |
+
# retrieving the index associated to this particular architecture
|
199 |
+
arch_index = self.architecture_to_index["/".join(input_list)]
|
200 |
+
return self[arch_index][self.dataset]["test_accuracy"]
|
201 |
+
|
202 |
+
def architecture_to_accuracy(self, architecture_string:str)->float:
|
203 |
+
"""Returns the test accuracy of an architecture string.
|
204 |
+
The architecture <-> index map is normalized to be as general as possible, hence some (minor)
|
205 |
+
input processing is needed.
|
206 |
+
|
207 |
+
Args:
|
208 |
+
architecture_string (str): Architecture string.
|
209 |
+
|
210 |
+
Returns:
|
211 |
+
float: Test accuracy (after 200 training epochs).
|
212 |
+
"""
|
213 |
+
# retrieving the index associated to this particular architecture
|
214 |
+
arch_index = self.architecture_to_index["/".join(self.architecture_to_list(architecture_string))]
|
215 |
+
return self[arch_index][self.dataset]["test_accuracy"]
|
216 |
+
|
217 |
+
def list_to_score(self, input_list:List[Text], score:Text)->float:
|
218 |
+
"""Returns the value of `score` of an input list representing the architecture.
|
219 |
+
This list contains the operations.
|
220 |
+
|
221 |
+
Args:
|
222 |
+
input_list (List[Text]): List of operations inside the architecture.
|
223 |
+
score (Text): Score of interest.
|
224 |
+
|
225 |
+
Returns:
|
226 |
+
float: Score value for `input_list`.
|
227 |
+
"""
|
228 |
+
arch_index = self.architecture_to_index["/".join(input_list)]
|
229 |
+
return self[arch_index][self.dataset].get(score, None)
|
230 |
+
|
231 |
+
def architecture_to_score(self, architecture_string:Text, score:Text)->float:
|
232 |
+
"""Returns the value of `score` of an architecture string.
|
233 |
+
The architecture <-> index map is normalized to be as general as possible, hence some (minor)
|
234 |
+
input processing is needed.
|
235 |
+
|
236 |
+
Args:
|
237 |
+
architecture_string (Text): Architecture string.
|
238 |
+
score (Text): Score of interest.
|
239 |
+
|
240 |
+
Returns:
|
241 |
+
float: Score value for `architecture_string`.
|
242 |
+
"""
|
243 |
+
# retrieving the index associated to this particular architecture
|
244 |
+
arch_index = self.architecture_to_index["/".join(self.architecture_to_list(architecture_string))]
|
245 |
+
return self[arch_index][self.dataset].get(score, None)
|
src/search/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
from .ga import *
|
src/search/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (164 Bytes). View file
|
|
src/search/__pycache__/ga.cpython-310.pyc
ADDED
Binary file (7.54 kB). View file
|
|
src/search/ga.py
ADDED
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from src.hw_nats_fast_interface import HW_NATS_FastInterface
|
2 |
+
from src.genetics import FastIndividual
|
3 |
+
from src.genetics import Genetic, Population
|
4 |
+
from typing import Iterable, Union, Text
|
5 |
+
import numpy as np
|
6 |
+
from collections import OrderedDict
|
7 |
+
|
8 |
+
FreeREA_dict = {
|
9 |
+
"n": 5, # tournament size
|
10 |
+
"N": 25, # population size
|
11 |
+
"mutation_prob": 1., # always mutates
|
12 |
+
"recombination_prob": 1., # always recombines
|
13 |
+
"P_parent1": 0.5, # fraction of child that comes from parent1 (on average)
|
14 |
+
"n_mutations": 1, # how many loci to mutate at a time
|
15 |
+
"loci_prob": None, # the probability of mutating a given locus (if None, uniform)
|
16 |
+
}
|
17 |
+
|
18 |
+
class GeneticSearch:
|
19 |
+
def __init__(self,
|
20 |
+
searchspace:HW_NATS_FastInterface,
|
21 |
+
genetics_dict:dict=FreeREA_dict,
|
22 |
+
init_population:Union[None, Iterable[FastIndividual]]=None,
|
23 |
+
fitness_weights:Union[None, np.ndarray]=np.array([0.5, 0.5])):
|
24 |
+
|
25 |
+
# instantiating a searchspace instance
|
26 |
+
self.searchspace = searchspace
|
27 |
+
# instatiating the dataset based on searchspace
|
28 |
+
self.dataset = self.searchspace.dataset
|
29 |
+
# instatiating the device based on searchspace
|
30 |
+
self.target_device = self.searchspace.target_device
|
31 |
+
# hardware aware scores changes based on whether or not one uses a given target device
|
32 |
+
if self.target_device is None:
|
33 |
+
self.hw_scores = ["flops", "params"]
|
34 |
+
else:
|
35 |
+
self.hw_scores = [f"{self.target_device}_energy"]
|
36 |
+
|
37 |
+
# scores used to evaluate the architectures on downstream tasks
|
38 |
+
self.classification_scores = ["naswot_score", "logsynflow_score", "skip_score"]
|
39 |
+
self.genetics_dict = genetics_dict
|
40 |
+
# weights used to combine classification performance with hardware performance.
|
41 |
+
self.weights = fitness_weights
|
42 |
+
|
43 |
+
# instantiating a population
|
44 |
+
self.population = Population(
|
45 |
+
searchspace=self.searchspace,
|
46 |
+
init_population=True if init_population is None else init_population,
|
47 |
+
n_individuals=self.genetics_dict["N"],
|
48 |
+
normalization="dynamic"
|
49 |
+
)
|
50 |
+
|
51 |
+
# initialize the object taking care of performing genetic operations
|
52 |
+
self.genetic_operator = Genetic(
|
53 |
+
genome=self.searchspace.all_ops,
|
54 |
+
strategy="comma", # population evolution strategy
|
55 |
+
tournament_size=self.genetics_dict["n"],
|
56 |
+
searchspace=self.searchspace
|
57 |
+
)
|
58 |
+
|
59 |
+
# preprocess population
|
60 |
+
self.preprocess_population()
|
61 |
+
|
62 |
+
def normalize_score(self, score_value:float, score_name:Text, type:Text="std")->float:
|
63 |
+
"""
|
64 |
+
Normalize the given score value using a specified normalization type.
|
65 |
+
|
66 |
+
Args:
|
67 |
+
score_value (float): The score value to be normalized.
|
68 |
+
score_name (Text): The name of the score used for normalization.
|
69 |
+
type (Text, optional): The type of normalization to be applied. Defaults to "std".
|
70 |
+
|
71 |
+
Returns:
|
72 |
+
float: The normalized score value.
|
73 |
+
|
74 |
+
Raises:
|
75 |
+
ValueError: If the specified normalization type is not available.
|
76 |
+
|
77 |
+
Note:
|
78 |
+
The available normalization types are:
|
79 |
+
- "std": Standard score normalization using mean and standard deviation.
|
80 |
+
"""
|
81 |
+
if type == "std":
|
82 |
+
score_mean = self.searchspace.get_score_mean(score_name)
|
83 |
+
score_std = self.searchspace.get_score_std(score_name)
|
84 |
+
|
85 |
+
return (score_value - score_mean) / score_std
|
86 |
+
else:
|
87 |
+
raise ValueError(f"Normalization type {type} not available!")
|
88 |
+
|
89 |
+
def fitness_function(self, individual:FastIndividual)->FastIndividual:
|
90 |
+
"""
|
91 |
+
Directly overwrites the fitness attribute for a given individual.
|
92 |
+
|
93 |
+
Args:
|
94 |
+
individual (FastIndividual): Individual to score.
|
95 |
+
|
96 |
+
# Returns:
|
97 |
+
# FastIndividual: Individual, with fitness field.
|
98 |
+
"""
|
99 |
+
if individual.fitness is None: # None at initialization only
|
100 |
+
scores = np.array([
|
101 |
+
self.normalize_score(
|
102 |
+
score_value=self.searchspace.list_to_score(input_list=individual.genotype,
|
103 |
+
score=score),
|
104 |
+
score_name=score
|
105 |
+
)
|
106 |
+
for score in self.classification_scores
|
107 |
+
])
|
108 |
+
hardware_performance = np.array([
|
109 |
+
self.normalize_score(
|
110 |
+
score_value=self.searchspace.list_to_score(input_list=individual.genotype,
|
111 |
+
score=score),
|
112 |
+
score_name=score
|
113 |
+
)
|
114 |
+
for score in self.hw_scores
|
115 |
+
])
|
116 |
+
# individual fitness is a convex combination of multiple scores
|
117 |
+
network_score = (np.ones_like(scores) / len(scores)) @ scores
|
118 |
+
network_hardware_performance = (np.ones_like(hardware_performance) / len(hardware_performance)) @ hardware_performance
|
119 |
+
|
120 |
+
# in the hardware aware contest performance is in a direct tradeoff with hardware performance
|
121 |
+
individual._fitness = np.array([network_score, -network_hardware_performance]) @ self.weights
|
122 |
+
|
123 |
+
# return individual
|
124 |
+
|
125 |
+
def preprocess_population(self):
|
126 |
+
"""
|
127 |
+
Applies scoring and fitness function to the whole population. This allows each individual to
|
128 |
+
have the appropriate fields.
|
129 |
+
"""
|
130 |
+
# assign the fitness score
|
131 |
+
self.assign_fitness()
|
132 |
+
|
133 |
+
def perform_mutation(
|
134 |
+
self,
|
135 |
+
individual:FastIndividual,
|
136 |
+
)->FastIndividual:
|
137 |
+
"""Performs mutation with respect to genetic ops parameters"""
|
138 |
+
realization = np.random.random()
|
139 |
+
if realization <= self.genetics_dict["mutation_prob"]: # do mutation
|
140 |
+
mutant = self.genetic_operator.mutate(
|
141 |
+
individual=individual,
|
142 |
+
n_loci=self.genetics_dict["n_mutations"],
|
143 |
+
genes_prob=self.genetics_dict["loci_prob"]
|
144 |
+
)
|
145 |
+
return mutant
|
146 |
+
else: # don't do mutation
|
147 |
+
return individual
|
148 |
+
|
149 |
+
def perform_recombination(
|
150 |
+
self,
|
151 |
+
parents:Iterable[FastIndividual],
|
152 |
+
)->FastIndividual:
|
153 |
+
"""Performs recombination with respect to genetic ops parameters"""
|
154 |
+
realization = np.random.random()
|
155 |
+
if realization <= self.genetics_dict["recombination_prob"]: # do recombination
|
156 |
+
child = self.genetic_operator.recombine(
|
157 |
+
individuals=parents,
|
158 |
+
P_parent1=self.genetics_dict["P_parent1"]
|
159 |
+
)
|
160 |
+
return child
|
161 |
+
else: # don't do recombination - simply return 1st parent
|
162 |
+
return parents[0]
|
163 |
+
|
164 |
+
def assign_fitness(self):
|
165 |
+
"""This function assigns to each invidual a given fitness score."""
|
166 |
+
# define a fitness function and compute fitness for each individual
|
167 |
+
fitness_function = lambda individual: self.fitness_function(individual=individual)
|
168 |
+
self.population.update_fitness(fitness_function=fitness_function)
|
169 |
+
|
170 |
+
def obtain_parents(self, n_parents:int=2):
|
171 |
+
# obtain tournament
|
172 |
+
tournament = self.genetic_operator.tournament(population=self.population.individuals)
|
173 |
+
# turn tournament into a local population
|
174 |
+
parents = sorted(tournament, key = lambda individual: individual.fitness, reverse=True)[:n_parents]
|
175 |
+
return parents
|
176 |
+
|
177 |
+
def solve(self, max_generations:int=100, return_trajectory:bool=False)->Union[FastIndividual, float]:
|
178 |
+
"""
|
179 |
+
This function performs Regularized Evolutionary Algorithm (REA) with Training-Free metrics.
|
180 |
+
Details on the whole procedure can be found in FreeREA (https://arxiv.org/pdf/2207.05135.pdf).
|
181 |
+
|
182 |
+
Args:
|
183 |
+
max_generations (int, optional): TODO - ADD DESCRIPTION. Defaults to 100.
|
184 |
+
|
185 |
+
Returns:
|
186 |
+
Union[FastIndividual, float]: Index-0 points to best individual object whereas Index-1 refers to its test
|
187 |
+
accuracy.
|
188 |
+
"""
|
189 |
+
|
190 |
+
MAX_GENERATIONS = max_generations
|
191 |
+
population, individuals = self.population, self.population.individuals
|
192 |
+
bests = []
|
193 |
+
history = OrderedDict()
|
194 |
+
|
195 |
+
for gen in range(MAX_GENERATIONS):
|
196 |
+
# store the population
|
197 |
+
history.update({self.searchspace.list_to_architecture(ind.genotype): ind for ind in population})
|
198 |
+
# save best individual
|
199 |
+
bests.append(max(individuals, key=lambda ind: ind.fitness))
|
200 |
+
# perform ageing
|
201 |
+
population.age()
|
202 |
+
# obtain parents
|
203 |
+
parents = self.obtain_parents()
|
204 |
+
# obtain recombinant child
|
205 |
+
child = self.perform_recombination(parents=parents)
|
206 |
+
# mutate parents
|
207 |
+
mutant1, mutant2 = [self.perform_mutation(parent) for parent in parents]
|
208 |
+
# add mutants and child to population
|
209 |
+
population.add_to_population([child, mutant1, mutant2])
|
210 |
+
# preprocess the new population - TODO: Implement a only-if-extremes-change strategy
|
211 |
+
self.preprocess_population()
|
212 |
+
# remove from population worst (from fitness perspective) individuals
|
213 |
+
population.remove_from_population(attribute="fitness", n=2)
|
214 |
+
# prune from population oldest individual
|
215 |
+
population.remove_from_population(attribute="age", ascending=False)
|
216 |
+
# overwrite population
|
217 |
+
individuals = population.individuals
|
218 |
+
|
219 |
+
best_individual = max(history.values(), key=lambda ind: ind._fitness)
|
220 |
+
# appending in last position the actual best element
|
221 |
+
bests.append(best_individual)
|
222 |
+
|
223 |
+
test_accuracy = self.searchspace.list_to_accuracy(best_individual.genotype)
|
224 |
+
|
225 |
+
if not return_trajectory:
|
226 |
+
return (best_individual, test_accuracy)
|
227 |
+
else:
|
228 |
+
return (best_individual, test_accuracy, bests, len(history))
|
src/utils.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
|
3 |
+
DATASETS = ["cifar10", "cifar100", "ImageNet16-120"]
|
4 |
+
DEVICES = ["edgegpu", "eyeriss", "fpga"]
|
5 |
+
|
6 |
+
def get_project_root():
|
7 |
+
"""
|
8 |
+
Returns project root directory from this script nested in the commons folder.
|
9 |
+
"""
|
10 |
+
return Path(__file__).parent.parent
|
11 |
+
|
12 |
+
def union_of_dicts(dicts):
|
13 |
+
"""
|
14 |
+
Returns a dictionary that represents the union of all input dictionaries.
|
15 |
+
|
16 |
+
Parameters:
|
17 |
+
- dicts (iterable): An iterable of dictionaries.
|
18 |
+
|
19 |
+
Returns:
|
20 |
+
- dict: The union of all dictionaries.
|
21 |
+
"""
|
22 |
+
result = {}
|
23 |
+
for d in dicts:
|
24 |
+
result.update(d)
|
25 |
+
return result
|