fracapuano commited on
Commit
81a5d0a
1 Parent(s): 7d62cb4

add files via upload

Browse files
.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
- title: NebulOS
3
- emoji: 📉
4
- colorFrom: gray
5
- colorTo: green
6
- sdk: streamlit
7
- sdk_version: 1.26.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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