Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
nni
Commits
2f8524ba
Unverified
Commit
2f8524ba
authored
Mar 24, 2022
by
liuzhe-lz
Committed by
GitHub
Mar 24, 2022
Browse files
Deduplicate generated parameters for TPE and Random (#4679)
parent
5136a86d
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
186 additions
and
10 deletions
+186
-10
nni/__init__.py
nni/__init__.py
+1
-1
nni/algorithms/hpo/gridsearch_tuner.py
nni/algorithms/hpo/gridsearch_tuner.py
+6
-1
nni/algorithms/hpo/random_tuner.py
nni/algorithms/hpo/random_tuner.py
+4
-1
nni/algorithms/hpo/tpe_tuner.py
nni/algorithms/hpo/tpe_tuner.py
+4
-1
nni/common/hpo_utils/__init__.py
nni/common/hpo_utils/__init__.py
+1
-0
nni/common/hpo_utils/dedup.py
nni/common/hpo_utils/dedup.py
+85
-0
nni/common/hpo_utils/formatting.py
nni/common/hpo_utils/formatting.py
+9
-6
test/ut/sdk/test_builtin_tuners.py
test/ut/sdk/test_builtin_tuners.py
+2
-0
test/ut/sdk/test_hpo_dedup.py
test/ut/sdk/test_hpo_dedup.py
+74
-0
No files found.
nni/__init__.py
View file @
2f8524ba
...
...
@@ -20,7 +20,7 @@ if dispatcher_env_vars.SDK_PROCESS != 'dispatcher':
from
.common.nas_utils
import
training_update
class
NoMoreTrialError
(
Exception
):
def
__init__
(
self
,
ErrorInfo
):
def
__init__
(
self
,
ErrorInfo
=
'Search space fully explored'
):
super
().
__init__
(
self
)
self
.
errorinfo
=
ErrorInfo
...
...
nni/algorithms/hpo/gridsearch_tuner.py
View file @
2f8524ba
...
...
@@ -200,13 +200,18 @@ class GridSearchTuner(Tuner):
mid
=
(
l
+
r
)
/
2
diff_l
=
_less
(
l
,
mid
,
spec
)
diff_r
=
_less
(
mid
,
r
,
spec
)
if
diff_l
and
diff_r
:
# we can skip these for non-q, but it will complicate the code
# if l != 0 and r != 1, then they are already in the grid, else they are not
# the special case is needed because for normal distribution 0 and 1 will generate infinity
if
(
diff_l
or
l
==
0.0
)
and
(
diff_r
or
r
==
1.0
):
# we can skip these for non-q, but it will complicate the code
new_vals
.
append
(
mid
)
updated
=
True
if
diff_l
:
new_divs
.
append
((
l
,
mid
))
updated
=
(
updated
or
l
==
0.0
)
if
diff_r
:
new_divs
.
append
((
mid
,
r
))
updated
=
(
updated
or
r
==
1.0
)
self
.
grid
[
i
]
+=
new_vals
self
.
divisions
[
i
]
=
new_divs
...
...
nni/algorithms/hpo/random_tuner.py
View file @
2f8524ba
...
...
@@ -17,7 +17,7 @@ import numpy as np
import
schema
from
nni
import
ClassArgsValidator
from
nni.common.hpo_utils
import
format_search_space
,
deformat_parameters
from
nni.common.hpo_utils
import
Deduplicator
,
format_search_space
,
deformat_parameters
from
nni.tuner
import
Tuner
_logger
=
logging
.
getLogger
(
'nni.tuner.random'
)
...
...
@@ -47,13 +47,16 @@ class RandomTuner(Tuner):
if
seed
is
None
:
# explicitly generate a seed to make the experiment reproducible
seed
=
np
.
random
.
default_rng
().
integers
(
2
**
31
)
self
.
rng
=
np
.
random
.
default_rng
(
seed
)
self
.
dedup
=
None
_logger
.
info
(
f
'Using random seed
{
seed
}
'
)
def
update_search_space
(
self
,
space
):
self
.
space
=
format_search_space
(
space
)
self
.
dedup
=
Deduplicator
(
self
.
space
)
def
generate_parameters
(
self
,
*
args
,
**
kwargs
):
params
=
suggest
(
self
.
rng
,
self
.
space
)
params
=
self
.
dedup
(
params
)
return
deformat_parameters
(
params
,
self
.
space
)
def
receive_trial_result
(
self
,
*
args
,
**
kwargs
):
...
...
nni/algorithms/hpo/tpe_tuner.py
View file @
2f8524ba
...
...
@@ -23,7 +23,7 @@ from typing import Any, NamedTuple
import
numpy
as
np
from
scipy.special
import
erf
# pylint: disable=no-name-in-module
from
nni.common.hpo_utils
import
OptimizeMode
,
format_search_space
,
deformat_parameters
,
format_parameters
from
nni.common.hpo_utils
import
Deduplicator
,
OptimizeMode
,
format_search_space
,
deformat_parameters
,
format_parameters
from
nni.tuner
import
Tuner
from
nni.typehint
import
Literal
from
nni.utils
import
extract_scalar_reward
...
...
@@ -153,6 +153,7 @@ class TpeTuner(Tuner):
# concurrent generate_parameters() calls are likely to yield similar result, because they use same history
# the liar solves this problem by adding fake results to history
self
.
liar
=
create_liar
(
self
.
args
.
constant_liar_type
)
self
.
dedup
=
None
if
seed
is
None
:
# explicitly generate a seed to make the experiment reproducible
seed
=
np
.
random
.
default_rng
().
integers
(
2
**
31
)
...
...
@@ -165,6 +166,7 @@ class TpeTuner(Tuner):
def
update_search_space
(
self
,
space
):
self
.
space
=
format_search_space
(
space
)
self
.
dedup
=
Deduplicator
(
self
.
space
)
def
generate_parameters
(
self
,
parameter_id
,
**
kwargs
):
if
self
.
liar
and
self
.
_running_params
:
...
...
@@ -178,6 +180,7 @@ class TpeTuner(Tuner):
history
=
self
.
_history
params
=
suggest
(
self
.
args
,
self
.
rng
,
self
.
space
,
history
)
params
=
self
.
dedup
(
params
)
self
.
_params
[
parameter_id
]
=
params
self
.
_running_params
[
parameter_id
]
=
params
...
...
nni/common/hpo_utils/__init__.py
View file @
2f8524ba
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
from
.dedup
import
Deduplicator
from
.validation
import
validate_search_space
from
.formatting
import
*
from
.optimize_mode
import
OptimizeMode
nni/common/hpo_utils/dedup.py
0 → 100644
View file @
2f8524ba
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
"""
Deduplicate repeated parameters.
No guarantee for forward-compatibility.
"""
import
logging
import
nni
from
.formatting
import
deformat_parameters
_logger
=
logging
.
getLogger
(
__name__
)
# TODO:
# Move main logic of basic tuners (random and grid search) into SDK,
# so we can get rid of private methods and circular dependency.
class
Deduplicator
:
"""
A helper for tuners to deduplicate generated parameters.
When the tuner generates an already existing parameter,
calling this will return a new parameter generated with grid search.
Otherwise it returns the orignial parameter object.
If all parameters have been generated, raise ``NoMoreTrialError``.
All search space types, including nested choice, are supported.
Resuming and updating search space are not supported for now.
It will not raise error, but may return duplicate parameters.
See random tuner's source code for example usage.
"""
def
__init__
(
self
,
formatted_search_space
):
self
.
_space
=
formatted_search_space
self
.
_never_dup
=
any
(
_spec_never_dup
(
spec
)
for
spec
in
self
.
_space
.
values
())
self
.
_history
=
set
()
self
.
_grid_search
=
None
def
__call__
(
self
,
formatted_parameters
):
if
self
.
_never_dup
or
self
.
_not_dup
(
formatted_parameters
):
return
formatted_parameters
if
self
.
_grid_search
is
None
:
_logger
.
info
(
f
'Tuning algorithm generated duplicate parameter:
{
formatted_parameters
}
'
)
_logger
.
info
(
f
'Use grid search for deduplication.'
)
self
.
_init_grid_search
()
while
True
:
new
=
self
.
_grid_search
.
_suggest
()
if
new
is
None
:
raise
nni
.
NoMoreTrialError
()
if
self
.
_not_dup
(
new
):
return
new
def
_init_grid_search
(
self
):
from
nni.algorithms.hpo.gridsearch_tuner
import
GridSearchTuner
self
.
_grid_search
=
GridSearchTuner
()
self
.
_grid_search
.
history
=
self
.
_history
self
.
_grid_search
.
space
=
self
.
_space
self
.
_grid_search
.
_init_grid
()
def
_not_dup
(
self
,
formatted_parameters
):
params
=
deformat_parameters
(
formatted_parameters
,
self
.
_space
)
params_str
=
nni
.
dump
(
params
,
sort_keys
=
True
)
if
params_str
in
self
.
_history
:
return
False
else
:
self
.
_history
.
add
(
params_str
)
return
True
def
_spec_never_dup
(
spec
):
if
spec
.
is_nested
():
return
False
# "not chosen" duplicates with "not chosen"
if
spec
.
categorical
or
spec
.
q
is
not
None
:
return
False
if
spec
.
normal_distributed
:
return
spec
.
sigma
>
0
else
:
return
spec
.
low
<
spec
.
high
nni/common/hpo_utils/formatting.py
View file @
2f8524ba
...
...
@@ -78,9 +78,16 @@ class ParameterSpec(NamedTuple):
For nested search space, check whether this parameter should be skipped for current set of paremters.
This function must be used in a pattern similar to random tuner. Otherwise it will misbehave.
"""
if
len
(
self
.
key
)
<
2
or
isinstance
(
self
.
key
[
-
2
],
str
):
return
True
if
self
.
is_nested
():
return
partial_parameters
[
self
.
key
[:
-
2
]]
==
self
.
key
[
-
2
]
else
:
return
True
def
is_nested
(
self
):
"""
Check whether this parameter is inside a nested choice.
"""
return
len
(
self
.
key
)
>=
2
and
isinstance
(
self
.
key
[
-
2
],
int
)
def
format_search_space
(
search_space
:
SearchSpace
)
->
FormattedSearchSpace
:
"""
...
...
@@ -88,10 +95,6 @@ def format_search_space(search_space: SearchSpace) -> FormattedSearchSpace:
The dict key is dict value's `ParameterSpec.key`.
"""
formatted
=
_format_search_space
(
tuple
(),
search_space
)
# In CPython 3.6, dicts preserve order by internal implementation.
# In Python 3.7+, dicts preserve order by language spec.
# Python 3.6 is crappy enough. Don't bother to do extra work for it.
# Remove these comments when we drop 3.6 support.
return
{
spec
.
key
:
spec
for
spec
in
formatted
}
def
deformat_parameters
(
...
...
test/ut/sdk/test_builtin_tuners.py
View file @
2f8524ba
...
...
@@ -324,11 +324,13 @@ class BuiltinTunersTestCase(TestCase):
self
.
import_data_test
(
tuner_fn
,
support_middle
=
False
)
def
test_tpe
(
self
):
self
.
exhaustive
=
True
tuner_fn
=
TpeTuner
self
.
search_space_test_all
(
TpeTuner
)
self
.
import_data_test
(
tuner_fn
)
def
test_random_search
(
self
):
self
.
exhaustive
=
True
tuner_fn
=
RandomTuner
self
.
search_space_test_all
(
tuner_fn
)
self
.
import_data_test
(
tuner_fn
)
...
...
test/ut/sdk/test_hpo_dedup.py
0 → 100644
View file @
2f8524ba
import
numpy
as
np
import
nni
from
nni.algorithms.hpo.random_tuner
import
suggest
from
nni.common.hpo_utils
import
Deduplicator
,
deformat_parameters
,
format_search_space
seed
=
np
.
random
.
default_rng
().
integers
(
2
**
31
)
print
(
seed
)
rng
=
np
.
random
.
default_rng
(
seed
)
finite_space
=
{
'x'
:
{
'_type'
:
'choice'
,
'_value'
:
[
'a'
,
'b'
]},
'y'
:
{
'_type'
:
'quniform'
,
'_value'
:
[
0
,
1
,
0.6
]},
'z'
:
{
'_type'
:
'normal'
,
'_value'
:
[
1
,
0
]},
}
infinite_space
=
{
'x'
:
{
'_type'
:
'choice'
,
'_value'
:
[
'a'
,
'b'
]},
'y'
:
{
'_type'
:
'uniform'
,
'_value'
:
[
0
,
1
]},
}
nested_space
=
{
'outer'
:
{
'_type'
:
'choice'
,
'_value'
:
[
{
'_name'
:
'A'
,
'x'
:
{
'_type'
:
'choice'
,
'_value'
:
[
'a'
,
'b'
]}},
{
'_name'
:
'B'
,
'y'
:
{
'_type'
:
'uniform'
,
'_value'
:
[
0
,
1
]}},
]
}
}
def
test_dedup_finite
():
space
=
format_search_space
(
finite_space
)
dedup
=
Deduplicator
(
space
)
params
=
[]
exhausted
=
False
try
:
for
i
in
range
(
7
):
p
=
dedup
(
suggest
(
rng
,
space
))
params
.
append
(
deformat_parameters
(
p
,
space
))
except
nni
.
NoMoreTrialError
:
exhausted
=
True
params
=
sorted
(
params
,
key
=
(
lambda
p
:
(
p
[
'x'
],
p
[
'y'
],
p
[
'z'
])))
assert
exhausted
assert
params
==
[
{
'x'
:
'a'
,
'y'
:
0.0
,
'z'
:
1.0
},
{
'x'
:
'a'
,
'y'
:
0.6
,
'z'
:
1.0
},
{
'x'
:
'a'
,
'y'
:
1.0
,
'z'
:
1.0
},
{
'x'
:
'b'
,
'y'
:
0.0
,
'z'
:
1.0
},
{
'x'
:
'b'
,
'y'
:
0.6
,
'z'
:
1.0
},
{
'x'
:
'b'
,
'y'
:
1.0
,
'z'
:
1.0
},
]
def
test_dedup_infinite
():
space
=
format_search_space
(
infinite_space
)
dedup
=
Deduplicator
(
space
)
for
i
in
range
(
10
):
p
=
suggest
(
rng
,
space
)
assert
dedup
(
p
)
is
p
def
test_dedup_nested
():
space
=
format_search_space
(
nested_space
)
dedup
=
Deduplicator
(
space
)
params
=
set
()
for
i
in
range
(
10
):
p
=
dedup
(
suggest
(
rng
,
space
))
s
=
nni
.
dump
(
deformat_parameters
(
p
,
space
),
sort_keys
=
True
)
assert
s
not
in
params
params
.
add
(
s
)
if
__name__
==
'__main__'
:
test_dedup_finite
()
test_dedup_infinite
()
test_dedup_nested
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment