This repository has been archived by the owner on Sep 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[tuner] Regularized Evolution (#2802)
- Loading branch information
1 parent
9ed545c
commit 8d3f444
Showing
10 changed files
with
309 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .regularized_evolution_tuner import RegularizedEvolutionTuner |
172 changes: 172 additions & 0 deletions
172
src/sdk/pynni/nni/regularized_evolution_tuner/regularized_evolution_tuner.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import copy | ||
import logging | ||
import random | ||
from collections import deque | ||
|
||
import nni | ||
from schema import Schema, Optional | ||
from nni.tuner import Tuner | ||
from nni import ClassArgsValidator | ||
from nni.utils import OptimizeMode, extract_scalar_reward | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class FinishedIndividual: | ||
def __init__(self, parameter_id, parameters, result): | ||
""" | ||
Parameters | ||
---------- | ||
parameter_id: int | ||
the index of the parameter | ||
parameters : dict | ||
chosen architecture and parameters | ||
result : float | ||
final metric of the chosen one | ||
""" | ||
self.parameter_id = parameter_id | ||
self.parameters = parameters | ||
self.result = result | ||
|
||
|
||
class EvolutionClassArgsValidator(ClassArgsValidator): | ||
def validate_class_args(self, **kwargs): | ||
Schema({ | ||
'optimize_mode': self.choices('optimize_mode', 'maximize', 'minimize'), | ||
Optional('population_size'): self.range('population_size', int, 0, 99999), | ||
Optional('sample_size'): self.range('sample_size', int, 0, 9999), | ||
}).validate(kwargs) | ||
|
||
|
||
class RegularizedEvolutionTuner(Tuner): | ||
""" | ||
RegularizedEvolutionTuner is tuner using Evolution NAS Tuner. | ||
See ``Regularized Evolution for Image Classifier Architecture Search`` for details. | ||
Parameters | ||
--- | ||
optimize_mode: str | ||
whether to maximize metric or not. default: 'maximize' | ||
population_size: int | ||
the maximum number of kept models | ||
sample_size: int | ||
the number of models chosen from population each time when evolution | ||
""" | ||
def __init__(self, optimize_mode="maximize", population_size=100, sample_size=25): | ||
super(RegularizedEvolutionTuner, self).__init__() | ||
self.optimize_mode = OptimizeMode(optimize_mode) | ||
self.population_size = population_size | ||
self.sample_size = sample_size | ||
self.initial_population = deque() | ||
self.population = deque() | ||
self.history = {} | ||
self.search_space = None | ||
self._from_initial = {} # whether the parameter is from initial population | ||
|
||
def generate_parameters(self, parameter_id, **kwargs): | ||
""" | ||
This function will returns a dict of trial (hyper-)parameters, as a serializable object. | ||
Parameters | ||
--- | ||
parameter_id: int | ||
the index of current set of parameters | ||
""" | ||
if self.initial_population: | ||
arch = self.initial_population.popleft() | ||
self.history[parameter_id] = arch | ||
self._from_initial[parameter_id] = True | ||
return arch | ||
elif self.population: | ||
sample = [] | ||
while len(sample) < self.sample_size: | ||
sample.append(random.choice(list(self.population))) | ||
|
||
candidate = max(sample, key=lambda x: x.result) | ||
arch = self._mutate_model(candidate) | ||
self.history[parameter_id] = arch | ||
self._from_initial[parameter_id] = False | ||
return arch | ||
else: | ||
raise nni.NoMoreTrialError | ||
|
||
def receive_trial_result(self, parameter_id, parameters, value, **kwargs): | ||
""" | ||
Record the result from a trial | ||
Parameters | ||
---------- | ||
parameter_id : int | ||
parameters : dict | ||
value : dict/float | ||
if value is dict, it should have "default" key. | ||
value is final metrics of the trial. | ||
""" | ||
reward = extract_scalar_reward(value) | ||
if parameter_id not in self.history: | ||
raise RuntimeError('Received parameter_id not in total_data.') | ||
params = self.history[parameter_id] | ||
|
||
if self.optimize_mode == OptimizeMode.Minimize: | ||
reward = -reward | ||
|
||
self.population.append(FinishedIndividual(parameter_id, params, reward)) | ||
if len(self.population) > self.population_size: | ||
self.population.popleft() | ||
|
||
def update_search_space(self, search_space): | ||
""" | ||
Update search space. | ||
Search_space contains the information that user pre-defined. | ||
Parameters | ||
---------- | ||
search_space : dict | ||
""" | ||
logger.info('update search space %s', search_space) | ||
assert self.search_space is None | ||
self.search_space = search_space | ||
|
||
for _, val in search_space.items(): | ||
if val['_type'] != 'layer_choice' and val['_type'] != 'input_choice': | ||
raise ValueError('Unsupported search space type: %s' % (val['_type'])) | ||
|
||
self._generate_initial_population() | ||
|
||
def trial_end(self, parameter_id, success, **kwargs): | ||
if not success: | ||
del self.history[parameter_id] | ||
if self._from_initial[parameter_id]: | ||
self.initial_population.append(self._random_model()) | ||
del self._from_initial[parameter_id] | ||
|
||
def _mutate(self, key, individual): | ||
mutate_val = self.search_space[key] | ||
if mutate_val['_type'] == 'layer_choice': | ||
idx = random.randint(0, len(mutate_val['_value']) - 1) | ||
individual[key] = {'_value': mutate_val['_value'][idx], '_idx': idx} | ||
elif mutate_val['_type'] == 'input_choice': | ||
candidates = mutate_val['_value']['candidates'] | ||
n_chosen = mutate_val['_value']['n_chosen'] | ||
idxs = [random.randint(0, len(candidates) - 1) for _ in range(n_chosen)] | ||
vals = [candidates[k] for k in idxs] | ||
individual[key] = {'_value': vals, '_idx': idxs} | ||
else: | ||
raise KeyError | ||
|
||
def _random_model(self): | ||
individual = {} | ||
for key in self.search_space.keys(): | ||
self._mutate(key, individual) | ||
return individual | ||
|
||
def _mutate_model(self, model): | ||
new_individual = copy.deepcopy(model.parameters) | ||
mutate_key = random.choice(list(new_individual.keys())) | ||
self._mutate(mutate_key, new_individual) | ||
return new_individual | ||
|
||
def _generate_initial_population(self): | ||
while len(self.initial_population) < self.population_size: | ||
self.initial_population.append(self._random_model()) | ||
logger.info('init population done.') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"first_conv": { | ||
"_type": "layer_choice", | ||
"_value": [ | ||
"conv5x5", | ||
"conv3x3" | ||
] | ||
}, | ||
"mid_conv": { | ||
"_type": "layer_choice", | ||
"_value": [ | ||
"0", | ||
"1" | ||
] | ||
}, | ||
"skip": { | ||
"_type": "input_choice", | ||
"_value": { | ||
"candidates": [ | ||
"", | ||
"" | ||
], | ||
"n_chosen": 1 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
authorName: nni | ||
experimentName: default_test | ||
maxExecDuration: 10m | ||
maxTrialNum: 1 | ||
trialConcurrency: 1 | ||
searchSpacePath: seach_space_classic_nas.json | ||
tuner: | ||
builtinTunerName: RegularizedEvolutionTuner | ||
classArgs: | ||
optimize_mode: maximize | ||
trial: | ||
codeDir: ../../../examples/nas/classic_nas | ||
command: python3 mnist.py --epochs 1 | ||
gpuNum: 0 | ||
|
||
useAnnotation: false | ||
multiPhase: false | ||
multiThread: false | ||
|
||
trainingServicePlatform: local |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"first_conv": { | ||
"_type": "layer_choice", | ||
"_value": [ | ||
"conv5x5", | ||
"conv3x3" | ||
] | ||
}, | ||
"mid_conv": { | ||
"_type": "layer_choice", | ||
"_value": [ | ||
"0", | ||
"1" | ||
] | ||
}, | ||
"skip": { | ||
"_type": "input_choice", | ||
"_value": { | ||
"candidates": [ | ||
"", | ||
"" | ||
], | ||
"n_chosen": 1 | ||
} | ||
} | ||
} |