op_test.py 20.6 KB
Newer Older
1
#   Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved.
D
dzhwinter 已提交
2
#
D
dzhwinter 已提交
3 4 5
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
D
dzhwinter 已提交
6
#
D
dzhwinter 已提交
7
#     http://www.apache.org/licenses/LICENSE-2.0
D
dzhwinter 已提交
8
#
D
dzhwinter 已提交
9 10 11 12 13 14
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

15 16
from __future__ import print_function

17 18
import unittest
import numpy as np
19
import random
M
minqiyang 已提交
20
import six
21
import time
22
import itertools
Y
Yu Yang 已提交
23
import collections
24 25 26

import paddle.fluid as fluid
import paddle.fluid.core as core
27 28 29
from paddle.fluid.backward import append_backward
from paddle.fluid.op import Operator
from paddle.fluid.executor import Executor
30
from paddle.fluid.framework import Program, OpProtoHolder, Variable
31
from testsuite import create_op, set_input, append_input_output, append_loss_ops
32 33


34 35 36 37
def randomize_probability(batch_size, class_num, dtype='float32'):
    prob = np.random.uniform(
        0.1, 1.0, size=(batch_size, class_num)).astype(dtype)
    prob_sum = prob.sum(axis=1)
M
minqiyang 已提交
38
    for i in six.moves.xrange(len(prob)):
39 40 41 42
        prob[i] /= prob_sum[i]
    return prob


43 44
def get_numeric_gradient(place,
                         scope,
45 46 47
                         op,
                         inputs,
                         input_to_check,
Y
Yancey 已提交
48
                         output_names,
49
                         delta=0.005,
50 51
                         in_place=False,
                         sum_outputs=None):
Y
Yu Yang 已提交
52
    # FIXME: change this method by compile time concepts
53
    set_input(scope, op, inputs, place)
54 55

    def product(dim):
M
minqiyang 已提交
56
        return six.moves.reduce(lambda a, b: a * b, dim, 1)
57 58

    def get_output():
Y
Yu Yang 已提交
59
        sum = []
C
chengduo 已提交
60
        op.run(scope, place)
Y
Yancey 已提交
61
        for output_name in output_names:
62 63
            if sum_outputs and output_name not in sum_outputs:
                continue
Y
Yu Yang 已提交
64 65
            sum.append(
                np.array(scope.find_var(output_name).get_tensor()).mean())
66
        return np.array(sum).sum() / len(output_names)
67 68

    tensor_to_check = scope.find_var(input_to_check).get_tensor()
Y
yuyang18 已提交
69 70
    tensor_size = product(tensor_to_check.shape())
    tensor_to_check_dtype = tensor_to_check._dtype()
71
    if tensor_to_check_dtype == core.VarDesc.VarType.FP32:
72
        tensor_to_check_dtype = np.float32
73
    elif tensor_to_check_dtype == core.VarDesc.VarType.FP64:
74
        tensor_to_check_dtype = np.float64
D
dzhwinter 已提交
75 76 77 78
    elif tensor_to_check_dtype == core.VarDesc.VarType.FP16:
        tensor_to_check_dtype = np.float16
        # set delta as np.float16, will automatic convert to float32, float64
        delta = np.array(delta).astype(np.float16)
79 80 81 82 83 84 85
    else:
        raise ValueError("Not supported data type " + str(
            tensor_to_check_dtype))

    gradient_flat = np.zeros(shape=(tensor_size, ), dtype=tensor_to_check_dtype)

    def __get_elem__(tensor, i):
D
dzhwinter 已提交
86 87 88 89 90
        if tensor_to_check_dtype == np.float16:
            numpy_tensor = np.array(tensor).astype(np.float16)
            numpy_tensor = numpy_tensor.flatten()
            return numpy_tensor[i]
        elif tensor_to_check_dtype == np.float32:
Y
yuyang18 已提交
91
            return tensor._get_float_element(i)
92
        else:
Y
yuyang18 已提交
93
            return tensor._get_double_element(i)
94 95

    def __set_elem__(tensor, i, e):
D
dzhwinter 已提交
96 97 98 99 100 101 102 103
        if tensor_to_check_dtype == np.float16:
            numpy_tensor = np.array(tensor).astype(np.float16)
            shape = numpy_tensor.shape
            numpy_tensor = numpy_tensor.flatten()
            numpy_tensor[i] = e
            numpy_tensor = numpy_tensor.reshape(shape).view(np.uint16)
            tensor.set(numpy_tensor, place)
        elif tensor_to_check_dtype == np.float32:
Y
yuyang18 已提交
104
            tensor._set_float_element(i, e)
105
        else:
Y
yuyang18 已提交
106
            tensor._set_double_element(i, e)
107

108 109
    # we only compute gradient of one element each time.
    # we use a for loop to compute the gradient of every element.
M
minqiyang 已提交
110
    for i in six.moves.xrange(tensor_size):
111
        if in_place:
112
            set_input(scope, op, inputs, place)
113 114

        # get one input element throw it's index i.
115
        origin = __get_elem__(tensor_to_check, i)
116 117
        # add delta to it, run op and then get the sum of the result tensor.
        x_pos = origin + delta
118
        __set_elem__(tensor_to_check, i, x_pos)
119 120 121
        y_pos = get_output()

        if in_place:
122
            set_input(scope, op, inputs, place)
123 124

        x_neg = origin - delta
125
        __set_elem__(tensor_to_check, i, x_neg)
126 127
        y_neg = get_output()

128
        __set_elem__(tensor_to_check, i, origin)
129 130
        gradient_flat[i] = (y_pos - y_neg) / delta / 2

Y
yuyang18 已提交
131
    return gradient_flat.reshape(tensor_to_check.shape())
132 133 134


class OpTest(unittest.TestCase):
135 136 137 138 139
    @classmethod
    def setUpClass(cls):
        '''Fix random seeds to remove randomness from tests'''
        cls._np_rand_state = np.random.get_state()
        cls._py_rand_state = random.getstate()
140 141 142
        cls.call_once = False
        cls.dtype = "float32"
        cls.outputs = {}
143 144 145 146 147 148

        np.random.seed(123)
        random.seed(124)

    @classmethod
    def tearDownClass(cls):
Y
yuyang18 已提交
149
        """Restore random seeds"""
150 151 152
        np.random.set_state(cls._np_rand_state)
        random.setstate(cls._py_rand_state)

153 154 155 156
    def try_call_once(self, data_type):
        if not self.call_once:
            self.call_once = True
            self.dtype = data_type
D
dzhwinter 已提交
157 158 159 160 161
            # See the comment of np_dtype_to_fluid_dtype
            # If the input type is uint16, we assume use float16
            # for lodtensor dtype.
            if self.dtype == np.uint16:
                self.dtype == np.float16
162 163 164 165 166 167

    def infer_dtype_from_inputs_outputs(self, inputs, outputs):
        def infer_dtype(numpy_dict):
            assert isinstance(
                numpy_dict,
                dict), "self.inputs, self.outputs must be numpy_dict"
M
minqiyang 已提交
168
            for var_name, var_value in six.iteritems(numpy_dict):
169 170 171 172 173 174 175 176 177 178 179 180 181 182
                if isinstance(var_value, (np.ndarray, np.generic)):
                    self.try_call_once(var_value.dtype)
                elif isinstance(var_value, (list, tuple)):
                    # the case of self.inputs = {"X": [("x0", x0), ("x1", x1), ("x2", x2)]}
                    if len(var_value) > 1 and isinstance(var_value[1], (
                            np.ndarray, np.generic)):
                        instance = var_value[1]
                        self.try_call_once(instance[1].dtype)
                else:
                    self.try_call_once("float32")

        infer_dtype(inputs)
        infer_dtype(outputs)

Y
Yang Yang(Tony) 已提交
183 184 185 186 187 188
    def feed_var(self, input_vars, place):
        feed_map = {}
        for var_name in input_vars:
            if isinstance(input_vars[var_name], list):
                for name, np_value in self.inputs[var_name]:
                    tensor = core.LoDTensor()
189
                    if isinstance(np_value, tuple):
D
dzhwinter 已提交
190 191
                        tensor.set(
                            OpTest.np_value_to_fluid_value(np_value[0]), place)
192
                        tensor.set_recursive_sequence_lengths(np_value[1])
193
                    else:
D
dzhwinter 已提交
194 195
                        tensor.set(
                            OpTest.np_value_to_fluid_value(np_value), place)
Y
Yang Yang(Tony) 已提交
196 197 198 199
                    feed_map[name] = tensor
            else:
                tensor = core.LoDTensor()
                if isinstance(self.inputs[var_name], tuple):
D
dzhwinter 已提交
200 201 202
                    tensor.set(
                        OpTest.np_value_to_fluid_value(self.inputs[var_name][
                            0]), place)
203 204
                    tensor.set_recursive_sequence_lengths(self.inputs[var_name][
                        1])
Y
Yang Yang(Tony) 已提交
205
                else:
D
dzhwinter 已提交
206 207 208
                    tensor.set(
                        OpTest.np_value_to_fluid_value(self.inputs[var_name]),
                        place)
Y
Yang Yang(Tony) 已提交
209 210 211 212
                feed_map[var_name] = tensor

        return feed_map

213
    def _append_ops(self, block):
Y
Yang Yang(Tony) 已提交
214
        op_proto = OpProtoHolder.instance().get_op_proto(self.op_type)
215 216 217 218 219 220
        "infer datatype from inputs and outputs for this test case"
        self.infer_dtype_from_inputs_outputs(self.inputs, self.outputs)
        inputs = append_input_output(block, op_proto, self.inputs, True,
                                     self.dtype)
        outputs = append_input_output(block, op_proto, self.outputs, False,
                                      self.dtype)
Y
Yang Yang(Tony) 已提交
221 222 223 224 225
        op = block.append_op(
            type=self.op_type,
            inputs=inputs,
            outputs=outputs,
            attrs=self.attrs if hasattr(self, "attrs") else dict())
Q
QI JUN 已提交
226 227 228
        # infer variable type and infer shape in compile-time
        op.desc.infer_var_type(block.desc)
        op.desc.infer_shape(block.desc)
Y
Yang Yang(Tony) 已提交
229

230 231
    def _get_io_vars(self, block, numpy_inputs):
        inputs = {}
M
minqiyang 已提交
232
        for name, value in six.iteritems(numpy_inputs):
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
            if isinstance(value, list):
                var_list = [
                    block.var(sub_name) for sub_name, sub_value in value
                ]
                inputs[name] = var_list
            else:
                inputs[name] = block.var(name)
        return inputs

    def _get_inputs(self, block):
        return self._get_io_vars(block, self.inputs)

    def _get_outputs(self, block):
        return self._get_io_vars(block, self.outputs)

    def calc_output(self, place):
        outs, _ = self._calc_output(place)
        return outs

252
    def _calc_output(self, place, parallel=False, no_check_set=None):
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274

        program = Program()
        block = program.global_block()
        self._append_ops(block)

        inputs = self._get_inputs(block)
        outputs = self._get_outputs(block)
        feed_map = self.feed_var(inputs, place)

        if parallel:
            use_cuda = False
            if isinstance(place, fluid.CUDAPlace(0)):
                use_cuda = True
            executor = fluid.ParallelExecutor(
                use_cuda=use_cuda, loss_name=loss.name, main_program=program)
        else:
            executor = Executor(place)

        fetch_list = getattr(self, "fetch_list", [])
        # if the fetch_list is customized by user, we use it directly.
        # if not, fill the fetch_list by the user configured outputs in test.
        if len(fetch_list) == 0:
M
minqiyang 已提交
275
            for var_name, var in six.iteritems(outputs):
276 277
                if no_check_set is not None and var_name in no_check_set:
                    continue
Y
Yang Yang(Tony) 已提交
278 279 280 281 282
                if isinstance(var, list):
                    for v in var:
                        fetch_list.append(v)
                else:
                    fetch_list.append(var)
283 284 285 286 287
        # if the fetch_list still empty, fill the fetch_list by the operator output.
        if len(fetch_list) == 0:
            for out_name, out_dup in Operator.get_op_outputs(self.op_type):
                fetch_list.append(str(out_name))
        # fetch_list = map(block.var, fetch_list)
W
Wu Yi 已提交
288
        if not isinstance(fetch_list[0], fluid.framework.Variable):
289
            fetch_list = list(map(block.var, fetch_list))
290 291 292 293
        outs = executor.run(program,
                            feed=feed_map,
                            fetch_list=fetch_list,
                            return_numpy=False)
294
        return outs, fetch_list
Y
Yang Yang(Tony) 已提交
295

296 297 298 299 300 301
    def check_output_with_place(self,
                                place,
                                atol,
                                no_check_set=None,
                                equal_nan=False):
        outs, fetch_list = self._calc_output(place, no_check_set=no_check_set)
Y
Yang Yang(Tony) 已提交
302
        for out_name, out_dup in Operator.get_op_outputs(self.op_type):
303 304
            if out_name not in self.outputs:
                continue
305 306
            if no_check_set is not None and out_name in no_check_set:
                continue
307

Y
Yang Yang(Tony) 已提交
308 309 310 311 312 313 314 315 316 317
            def find_actual(target_name, fetch_list):
                found = [
                    i for i, var in enumerate(fetch_list)
                    if var.name == target_name
                ]
                self.assertTrue(
                    len(found) == 1, "Found {} {}".format(
                        len(found), target_name))
                return found[0]

318 319
            if out_dup:
                sub_out = self.outputs[out_name]
Y
Yancey 已提交
320 321 322
                if not isinstance(sub_out, list):
                    raise AssertionError("sub_out type %s is not list",
                                         type(sub_out))
323 324
                for item in sub_out:
                    sub_out_name, expect = item[0], item[1]
Y
Yang Yang(Tony) 已提交
325
                    idx = find_actual(sub_out_name, fetch_list)
Q
QI JUN 已提交
326 327
                    actual = outs[idx]
                    actual_t = np.array(actual)
328 329
                    expect_t = expect[0] \
                        if isinstance(expect, tuple) else expect
330 331
                    self.assertTrue(
                        np.allclose(
332
                            actual_t, expect_t, atol=atol, equal_nan=equal_nan),
Y
Yang Yang(Tony) 已提交
333 334
                        "Output (" + sub_out_name + ") has diff at " +
                        str(place))
335 336
                    if isinstance(expect, tuple):
                        self.assertListEqual(
337 338
                            actual.recursive_sequence_lengths(), expect[1],
                            "Output (" + sub_out_name +
Q
QI JUN 已提交
339
                            ") has different lod at " + str(place))
340
            else:
Y
Yang Yang(Tony) 已提交
341
                idx = find_actual(out_name, fetch_list)
Q
QI JUN 已提交
342 343
                actual = outs[idx]
                actual_t = np.array(actual)
344
                expect = self.outputs[out_name]
345
                expect_t = expect[0] if isinstance(expect, tuple) else expect
346 347
                self.assertTrue(
                    np.allclose(
348
                        actual_t, expect_t, atol=atol, equal_nan=equal_nan),
E
emailweixu 已提交
349
                    "Output (" + out_name + ") has diff at " + str(place) +
D
dzhwinter 已提交
350 351
                    "\nExpect " + str(expect_t) + "\n" + "But Got" +
                    str(actual_t))
352
                if isinstance(expect, tuple):
353 354
                    self.assertListEqual(actual.recursive_sequence_lengths(),
                                         expect[1], "Output (" + out_name +
355
                                         ") has different lod at " + str(place))
356

357
    def _get_places(self):
D
dzhwinter 已提交
358 359 360 361 362 363 364 365
        if self.dtype == np.float16:
            if core.is_compiled_with_cuda() and core.op_support_gpu(
                    self.op_type):
                place = core.CUDAPlace(0)
                if core.is_float16_supported(place):
                    return [place]
            else:
                return []
366
        places = [fluid.CPUPlace()]
367
        if core.is_compiled_with_cuda() and core.op_support_gpu(self.op_type):
D
dzhwinter 已提交
368
            places.append(core.CUDAPlace(0))
369 370
        return places

371
    def check_output(self, atol=1e-5, no_check_set=None, equal_nan=False):
372
        places = self._get_places()
Q
qijun 已提交
373
        for place in places:
374
            self.check_output_with_place(place, atol, no_check_set, equal_nan)
Q
qijun 已提交
375

376
    def check_output_customized(self, checker):
377
        places = self._get_places()
378 379 380
        for place in places:
            outs = self.calc_output(place)
            outs = [np.array(out) for out in outs]
381
            outs.sort(key=len)
382 383
            checker(outs)

384 385 386
    def __assert_is_close(self, numeric_grads, analytic_grads, names,
                          max_relative_error, msg_prefix):

M
minqiyang 已提交
387
        for a, b, name in six.moves.zip(numeric_grads, analytic_grads, names):
388 389 390 391 392 393 394 395
            abs_a = np.abs(a)
            abs_a[abs_a < 1e-3] = 1

            diff_mat = np.abs(a - b) / abs_a
            max_diff = np.max(diff_mat)

            def err_msg():
                offset = np.argmax(diff_mat > max_relative_error)
396
                return ("%s Variable %s max gradient diff %f over limit %f, "
D
dzhwinter 已提交
397 398 399
                        "the first error element is %d, expected %f, but got %f"
                        ) % (msg_prefix, name, max_diff, max_relative_error,
                             offset, a.flatten()[offset], b.flatten()[offset])
400 401 402 403 404

            self.assertLessEqual(max_diff, max_relative_error, err_msg())

    def check_grad(self,
                   inputs_to_check,
Y
Yancey 已提交
405
                   output_names,
406
                   no_grad_set=None,
407
                   numeric_grad_delta=0.005,
408
                   in_place=False,
Q
Qiao Longfei 已提交
409
                   max_relative_error=0.005,
410 411
                   user_defined_grads=None,
                   sum_outputs=None):
412
        places = self._get_places()
413 414 415 416
        for place in places:
            self.check_grad_with_place(place, inputs_to_check, output_names,
                                       no_grad_set, numeric_grad_delta,
                                       in_place, max_relative_error,
417
                                       user_defined_grads, sum_outputs)
418 419 420 421 422 423 424 425 426

    def check_grad_with_place(self,
                              place,
                              inputs_to_check,
                              output_names,
                              no_grad_set=None,
                              numeric_grad_delta=0.005,
                              in_place=False,
                              max_relative_error=0.005,
427 428
                              user_defined_grads=None,
                              sum_outputs=None):
429
        self.scope = core.Scope()
Q
qijun 已提交
430
        op_inputs = self.inputs if hasattr(self, "inputs") else dict()
431
        op_outputs = self.outputs if hasattr(self, "outputs") else dict()
Q
qijun 已提交
432
        op_attrs = self.attrs if hasattr(self, "attrs") else dict()
433
        self.op = create_op(self.scope, self.op_type, op_inputs, op_outputs,
Q
qijun 已提交
434
                            op_attrs)
Y
Yu Yang 已提交
435

436 437 438
        if no_grad_set is None:
            no_grad_set = set()

Y
Yancey 已提交
439 440 441
        if not type(output_names) is list:
            output_names = [output_names]

Q
Qiao Longfei 已提交
442
        numeric_grads = user_defined_grads or [
443
            get_numeric_gradient(
444
                place,
445 446 447 448
                self.scope,
                self.op,
                self.inputs,
                input_to_check,
Y
Yancey 已提交
449
                output_names,
450
                delta=numeric_grad_delta,
451 452
                in_place=in_place,
                sum_outputs=sum_outputs) for input_to_check in inputs_to_check
453
        ]
454 455 456 457 458 459
        analytic_grads = self._get_gradient(inputs_to_check, place,
                                            output_names, no_grad_set)

        self.__assert_is_close(numeric_grads, analytic_grads, inputs_to_check,
                               max_relative_error,
                               "Gradient Check On %s" % str(place))
Q
qijun 已提交
460

Y
Yu Yang 已提交
461 462 463 464 465
    @staticmethod
    def _numpy_to_lod_tensor(np_value, lod, place):
        tensor = core.LoDTensor()
        tensor.set(np_value, place)
        if lod is not None:
466
            tensor.set_recursive_sequence_lengths(lod)
Y
Yu Yang 已提交
467 468
        return tensor

K
Kexin Zhao 已提交
469
    @staticmethod
K
Kexin Zhao 已提交
470 471
    def np_dtype_to_fluid_dtype(input):
        """Change the dtype of float16 numpy array
K
Kexin Zhao 已提交
472

473
        numpy float16 is binded to paddle::platform::float16
K
Kexin Zhao 已提交
474
        in tensor_py.h via the help of uint16 data type since
475
        the internal memory representation of float16 is
K
Kexin Zhao 已提交
476 477
        uint16_t in paddle and np.uint16 in numpy, which are
        themselves binded together by pybind.
K
Kexin Zhao 已提交
478 479 480 481 482

        Args:
            input: input numpy array

        Returns:
483
            input: The dtype of input will be changed to np.uint16 if
K
Kexin Zhao 已提交
484
                it is originally np.float16, such that the internal memory
485
                of input will be reinterpreted as of dtype np.uint16.
K
Kexin Zhao 已提交
486 487
        """
        if input.dtype == np.float16:
K
Kexin Zhao 已提交
488 489
            input.dtype = np.uint16
        return input
K
Kexin Zhao 已提交
490

D
dzhwinter 已提交
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
    @staticmethod
    def fluid_dtype_to_np_dtype(self, dtype):
        """
        See above, convert the dtype to normal type.
        """
        if dtype == np.uint16:
            dtype = np.float16
        return dtype

    @staticmethod
    def np_value_to_fluid_value(input):
        if input.dtype == np.float16:
            input = input.view(np.uint16)
        return input

506 507 508 509 510 511
    def _get_gradient(self,
                      input_to_check,
                      place,
                      output_names,
                      no_grad_set,
                      parallel=False):
Y
Yu Yang 已提交
512 513
        prog = Program()
        block = prog.global_block()
514 515
        self._append_ops(block)
        loss = append_loss_ops(block, output_names)
F
fengjiayi 已提交
516
        param_grad_list = append_backward(
Y
Yu Yang 已提交
517 518
            loss=loss, parameter_list=input_to_check, no_grad_set=no_grad_set)

519 520
        inputs = self._get_inputs(block)
        feed_dict = self.feed_var(inputs, place)
Y
Yu Yang 已提交
521 522

        fetch_list = [g for p, g in param_grad_list]
523 524 525 526 527
        if parallel:
            use_cuda = False
            if isinstance(place, fluid.CUDAPlace(0)):
                use_cuda = True
            executor = fluid.ParallelExecutor(
D
dzhwinter 已提交
528
                use_cuda=use_cuda, loss_name=loss.name, main_program=prog)
529 530
        else:
            executor = Executor(place)
531 532 533
        return list(
            map(np.array,
                executor.run(prog, feed_dict, fetch_list, return_numpy=False)))