target_space.py 7.74 KB
Newer Older
liuzhe-lz's avatar
liuzhe-lz committed
1
2
3
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

4
5
6
7
"""
Tool class to hold the param-space coordinates (X) and target values (Y).
"""

Guoxin's avatar
Guoxin committed
8
9
10
11
12
import numpy as np
import nni.parameter_expressions as parameter_expressions


def _hashable(params):
13
14
15
16
17
18
19
20
21
22
23
24
25
    """
    Transform list params to tuple format. Ensure that an point is hashable by a python dict.

    Parameters
    ----------
    params : numpy array
        array format of parameters

    Returns
    -------
    tuple
        tuple format of parameters
    """
Guoxin's avatar
Guoxin committed
26
27
28
29
30
31
    return tuple(map(float, params))


class TargetSpace():
    """
    Holds the param-space coordinates (X) and target values (Y)
32
33
34
35
36
37
38
39

    Parameters
    ----------
    pbounds : dict
        Dictionary with parameters names and legal values.

    random_state : int, RandomState, or None
        optionally specify a seed for a random number generator, by default None.
Guoxin's avatar
Guoxin committed
40
41
42
    """

    def __init__(self, pbounds, random_state=None):
43
        self._random_state = random_state
Guoxin's avatar
Guoxin committed
44
45
46

        # Get the name of the parameters
        self._keys = sorted(pbounds)
47

Guoxin's avatar
Guoxin committed
48
49
50
51
52
        # Create an array with parameters bounds
        self._bounds = np.array(
            [item[1] for item in sorted(pbounds.items(), key=lambda x: x[0])]
        )

53
54
55
56
57
58
59
60
        # check values type
        for _bound in self._bounds:
            if _bound['_type'] == 'choice':
                try:
                    [float(val) for val in _bound['_value']]
                except ValueError:
                    raise ValueError("GP Tuner supports only numerical values")

Guoxin's avatar
Guoxin committed
61
62
63
64
65
66
67
68
        # preallocated memory for X and Y points
        self._params = np.empty(shape=(0, self.dim))
        self._target = np.empty(shape=(0))

        # keep track of unique points we have seen so far
        self._cache = {}

    def __contains__(self, params):
69
        """
Guoxin's avatar
Guoxin committed
70
        check if a parameter is already registered
71
72
73
74
75
76
77
78
79
80

        Parameters
        ----------
        params : numpy array

        Returns
        -------
        bool
            True if the parameter is already registered, else false
        """
Guoxin's avatar
Guoxin committed
81
82
83
        return _hashable(params) in self._cache

    def len(self):
84
        """
Guoxin's avatar
Guoxin committed
85
        length of registered params and targets
86
87
88
89
90

        Returns
        -------
        int
        """
Guoxin's avatar
Guoxin committed
91
92
93
94
95
        assert len(self._params) == len(self._target)
        return len(self._target)

    @property
    def params(self):
96
97
98
99
100
101
102
        """
        registered parameters

        Returns
        -------
        numpy array
        """
Guoxin's avatar
Guoxin committed
103
104
105
106
        return self._params

    @property
    def target(self):
107
108
109
110
111
112
113
        """
        registered target values

        Returns
        -------
        numpy array
        """
Guoxin's avatar
Guoxin committed
114
115
116
117
        return self._target

    @property
    def dim(self):
118
119
120
121
122
123
124
        """
        dimension of parameters

        Returns
        -------
        int
        """
Guoxin's avatar
Guoxin committed
125
126
127
128
        return len(self._keys)

    @property
    def keys(self):
129
130
131
132
133
134
135
        """
        keys of parameters

        Returns
        -------
        numpy array
        """
Guoxin's avatar
Guoxin committed
136
137
138
139
        return self._keys

    @property
    def bounds(self):
140
141
142
143
144
145
146
        """
        bounds of parameters

        Returns
        -------
        numpy array
        """
Guoxin's avatar
Guoxin committed
147
148
149
        return self._bounds

    def params_to_array(self, params):
150
151
152
153
154
155
156
157
158
159
160
161
162
        """
        dict to array

        Parameters
        ----------
        params : dict
            dict format of parameters

        Returns
        -------
        numpy array
            array format of parameters
        """
Guoxin's avatar
Guoxin committed
163
164
165
166
167
168
169
170
171
172
        try:
            assert set(params) == set(self.keys)
        except AssertionError:
            raise ValueError(
                "Parameters' keys ({}) do ".format(sorted(params)) +
                "not match the expected set of keys ({}).".format(self.keys)
            )
        return np.asarray([params[key] for key in self.keys])

    def array_to_params(self, x):
173
        """
Guoxin's avatar
Guoxin committed
174
175
176
        array to dict

        maintain int type if the paramters is defined as int in search_space.json
177
178
179
180
181
182
183
184
185
186
        Parameters
        ----------
        x : numpy array
            array format of parameters

        Returns
        -------
        dict
            dict format of parameters
        """
Guoxin's avatar
Guoxin committed
187
188
189
190
191
        try:
            assert len(x) == len(self.keys)
        except AssertionError:
            raise ValueError(
                "Size of array ({}) is different than the ".format(len(x)) +
liuzhe-lz's avatar
liuzhe-lz committed
192
                "expected number of parameters ({}).".format(self.dim)
Guoxin's avatar
Guoxin committed
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
            )

        params = {}
        for i, _bound in enumerate(self._bounds):
            if _bound['_type'] == 'choice' and all(isinstance(val, int) for val in _bound['_value']):
                params.update({self.keys[i]: int(x[i])})
            elif _bound['_type'] in ['randint']:
                params.update({self.keys[i]: int(x[i])})
            else:
                params.update({self.keys[i]:  x[i]})

        return params

    def register(self, params, target):
        """
        Append a point and its target value to the known data.

        Parameters
        ----------
212
213
        params : dict
            parameters
Guoxin's avatar
Guoxin committed
214

215
        target : float
Guoxin's avatar
Guoxin committed
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
            target function value
        """

        x = self.params_to_array(params)
        if x in self:
            print('Data point {} is not unique'.format(x))

        # Insert data into unique dictionary
        self._cache[_hashable(x.ravel())] = target

        self._params = np.concatenate([self._params, x.reshape(1, -1)])
        self._target = np.concatenate([self._target, [target]])

    def random_sample(self):
        """
        Creates a random point within the bounds of the space.

233
234
235
236
        Returns
        -------
        numpy array
            one groupe of parameter
Guoxin's avatar
Guoxin committed
237
238
239
240
241
        """
        params = np.empty(self.dim)
        for col, _bound in enumerate(self._bounds):
            if _bound['_type'] == 'choice':
                params[col] = parameter_expressions.choice(
242
                    _bound['_value'], self._random_state)
Guoxin's avatar
Guoxin committed
243
            elif _bound['_type'] == 'randint':
244
                params[col] = self._random_state.randint(
Guoxin's avatar
Guoxin committed
245
246
247
                    _bound['_value'][0], _bound['_value'][1], size=1)
            elif _bound['_type'] == 'uniform':
                params[col] = parameter_expressions.uniform(
248
                    _bound['_value'][0], _bound['_value'][1], self._random_state)
Guoxin's avatar
Guoxin committed
249
250
            elif _bound['_type'] == 'quniform':
                params[col] = parameter_expressions.quniform(
251
                    _bound['_value'][0], _bound['_value'][1], _bound['_value'][2], self._random_state)
Guoxin's avatar
Guoxin committed
252
253
            elif _bound['_type'] == 'loguniform':
                params[col] = parameter_expressions.loguniform(
254
                    _bound['_value'][0], _bound['_value'][1], self._random_state)
Guoxin's avatar
Guoxin committed
255
256
            elif _bound['_type'] == 'qloguniform':
                params[col] = parameter_expressions.qloguniform(
257
                    _bound['_value'][0], _bound['_value'][1], _bound['_value'][2], self._random_state)
liuzhe-lz's avatar
liuzhe-lz committed
258

Guoxin's avatar
Guoxin committed
259
260
261
        return params

    def max(self):
262
263
264
265
266
267
268
269
        """
        Get maximum target value found and its corresponding parameters.

        Returns
        -------
        dict
            target value and parameters, empty dict if nothing registered
        """
Guoxin's avatar
Guoxin committed
270
271
272
273
274
275
276
277
278
279
280
281
        try:
            res = {
                'target': self.target.max(),
                'params': dict(
                    zip(self.keys, self.params[self.target.argmax()])
                )
            }
        except ValueError:
            res = {}
        return res

    def res(self):
282
283
284
285
286
287
288
289
        """
        Get all target values found and corresponding parameters.

        Returns
        -------
        list
            a list of target values and their corresponding parameters
        """
Guoxin's avatar
Guoxin committed
290
291
292
293
294
295
        params = [dict(zip(self.keys, p)) for p in self.params]

        return [
            {"target": target, "params": param}
            for target, param in zip(self.target, params)
        ]