op_test.py 21.1 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,
C
chengduo 已提交
50
                         in_place=False):
Y
Yu Yang 已提交
51
    # FIXME: change this method by compile time concepts
52
    set_input(scope, op, inputs, place)
53 54

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

    tensor_to_check = scope.find_var(input_to_check).get_tensor()
Y
yuyang18 已提交
58 59
    tensor_size = product(tensor_to_check.shape())
    tensor_to_check_dtype = tensor_to_check._dtype()
60
    if tensor_to_check_dtype == core.VarDesc.VarType.FP32:
61
        tensor_to_check_dtype = np.float32
62
    elif tensor_to_check_dtype == core.VarDesc.VarType.FP64:
63
        tensor_to_check_dtype = np.float64
D
dzhwinter 已提交
64 65 66 67
    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)
68 69 70 71
    else:
        raise ValueError("Not supported data type " + str(
            tensor_to_check_dtype))

C
chengduo 已提交
72 73 74 75 76 77 78 79 80
    def get_output():
        sum = []
        op.run(scope, place)
        for output_name in output_names:
            sum.append(
                np.array(scope.find_var(output_name).get_tensor()).astype(
                    tensor_to_check_dtype).mean())
        return tensor_to_check_dtype(np.array(sum).sum() / len(output_names))

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

    def __get_elem__(tensor, i):
D
dzhwinter 已提交
84 85 86 87 88
        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 已提交
89
            return tensor._get_float_element(i)
90
        else:
Y
yuyang18 已提交
91
            return tensor._get_double_element(i)
92 93

    def __set_elem__(tensor, i, e):
D
dzhwinter 已提交
94 95 96 97 98 99 100 101
        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 已提交
102
            tensor._set_float_element(i, e)
103
        else:
Y
yuyang18 已提交
104
            tensor._set_double_element(i, e)
105

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

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

        if in_place:
120
            set_input(scope, op, inputs, place)
121 122

        x_neg = origin - delta
123
        __set_elem__(tensor_to_check, i, x_neg)
124 125
        y_neg = get_output()

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

Y
yuyang18 已提交
129
    return gradient_flat.reshape(tensor_to_check.shape())
130 131 132


class OpTest(unittest.TestCase):
133 134 135 136 137
    @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()
138 139 140
        cls.call_once = False
        cls.dtype = "float32"
        cls.outputs = {}
141 142 143 144 145 146

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

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

151 152 153 154
    def try_call_once(self, data_type):
        if not self.call_once:
            self.call_once = True
            self.dtype = data_type
D
dzhwinter 已提交
155 156 157 158 159
            # 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
160 161 162 163 164 165

    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 已提交
166
            for var_name, var_value in six.iteritems(numpy_dict):
167 168 169 170 171 172 173 174 175 176 177 178 179 180
                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) 已提交
181 182 183 184 185 186
    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()
187
                    if isinstance(np_value, tuple):
D
dzhwinter 已提交
188 189
                        tensor.set(
                            OpTest.np_value_to_fluid_value(np_value[0]), place)
190
                        tensor.set_recursive_sequence_lengths(np_value[1])
191
                    else:
D
dzhwinter 已提交
192 193
                        tensor.set(
                            OpTest.np_value_to_fluid_value(np_value), place)
Y
Yang Yang(Tony) 已提交
194 195 196 197
                    feed_map[name] = tensor
            else:
                tensor = core.LoDTensor()
                if isinstance(self.inputs[var_name], tuple):
D
dzhwinter 已提交
198 199 200
                    tensor.set(
                        OpTest.np_value_to_fluid_value(self.inputs[var_name][
                            0]), place)
201 202
                    tensor.set_recursive_sequence_lengths(self.inputs[var_name][
                        1])
Y
Yang Yang(Tony) 已提交
203
                else:
D
dzhwinter 已提交
204 205 206
                    tensor.set(
                        OpTest.np_value_to_fluid_value(self.inputs[var_name]),
                        place)
Y
Yang Yang(Tony) 已提交
207 208 209 210
                feed_map[var_name] = tensor

        return feed_map

211
    def _append_ops(self, block):
Y
Yang Yang(Tony) 已提交
212
        op_proto = OpProtoHolder.instance().get_op_proto(self.op_type)
213 214 215 216 217 218
        "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)
P
phlrain 已提交
219 220 221 222 223 224 225 226 227

        if hasattr(self, "cache_name_list"):
            for name in self.cache_name_list:
                inputs[name] = block.create_var(
                    name=name,
                    persistable=True,
                    type=core.VarDesc.VarType.RAW,
                    stop_gradient=True)

Y
Yang Yang(Tony) 已提交
228 229 230 231 232
        op = block.append_op(
            type=self.op_type,
            inputs=inputs,
            outputs=outputs,
            attrs=self.attrs if hasattr(self, "attrs") else dict())
Q
QI JUN 已提交
233 234 235
        # 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) 已提交
236

237 238
    def _get_io_vars(self, block, numpy_inputs):
        inputs = {}
M
minqiyang 已提交
239
        for name, value in six.iteritems(numpy_inputs):
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
            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

259
    def _calc_output(self, place, parallel=False, no_check_set=None):
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281

        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 已提交
282
            for var_name, var in six.iteritems(outputs):
283 284
                if no_check_set is not None and var_name in no_check_set:
                    continue
Y
Yang Yang(Tony) 已提交
285 286 287 288 289
                if isinstance(var, list):
                    for v in var:
                        fetch_list.append(v)
                else:
                    fetch_list.append(var)
290 291 292 293 294
        # 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 已提交
295
        if not isinstance(fetch_list[0], fluid.framework.Variable):
296
            fetch_list = list(map(block.var, fetch_list))
297 298 299 300
        outs = executor.run(program,
                            feed=feed_map,
                            fetch_list=fetch_list,
                            return_numpy=False)
301
        return outs, fetch_list
Y
Yang Yang(Tony) 已提交
302

303 304 305 306 307 308
    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) 已提交
309
        for out_name, out_dup in Operator.get_op_outputs(self.op_type):
310 311
            if out_name not in self.outputs:
                continue
312 313
            if no_check_set is not None and out_name in no_check_set:
                continue
314

Y
Yang Yang(Tony) 已提交
315 316 317 318 319 320 321 322 323 324
            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]

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

364
    def _get_places(self):
D
dzhwinter 已提交
365 366 367 368 369 370
        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]
371 372
                else:
                    return []
D
dzhwinter 已提交
373 374
            else:
                return []
375
        places = [fluid.CPUPlace()]
376 377 378
        cpu_only = self._cpu_only if hasattr(self, '_cpu_only') else False
        if core.is_compiled_with_cuda() and core.op_support_gpu(self.op_type)\
           and not cpu_only:
D
dzhwinter 已提交
379
            places.append(core.CUDAPlace(0))
380 381
        return places

382
    def check_output(self, atol=1e-5, no_check_set=None, equal_nan=False):
383
        places = self._get_places()
Q
qijun 已提交
384
        for place in places:
385
            self.check_output_with_place(place, atol, no_check_set, equal_nan)
Q
qijun 已提交
386

387
    def check_output_customized(self, checker):
388
        places = self._get_places()
389 390 391
        for place in places:
            outs = self.calc_output(place)
            outs = [np.array(out) for out in outs]
392
            outs.sort(key=len)
393 394
            checker(outs)

D
Dun 已提交
395 396
    def _assert_is_close(self, numeric_grads, analytic_grads, names,
                         max_relative_error, msg_prefix):
397

M
minqiyang 已提交
398
        for a, b, name in six.moves.zip(numeric_grads, analytic_grads, names):
399 400 401 402 403 404 405 406
            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)
407
                return ("%s Variable %s max gradient diff %f over limit %f, "
D
dzhwinter 已提交
408 409 410
                        "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])
411 412 413 414 415

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

    def check_grad(self,
                   inputs_to_check,
Y
Yancey 已提交
416
                   output_names,
417
                   no_grad_set=None,
418
                   numeric_grad_delta=0.005,
419
                   in_place=False,
Q
Qiao Longfei 已提交
420
                   max_relative_error=0.005,
C
chengduo 已提交
421
                   user_defined_grads=None):
422
        places = self._get_places()
423 424 425 426
        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,
C
chengduo 已提交
427
                                       user_defined_grads)
428 429 430 431 432 433 434 435 436

    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,
C
chengduo 已提交
437
                              user_defined_grads=None):
438
        self.scope = core.Scope()
Q
qijun 已提交
439
        op_inputs = self.inputs if hasattr(self, "inputs") else dict()
440
        op_outputs = self.outputs if hasattr(self, "outputs") else dict()
Q
qijun 已提交
441
        op_attrs = self.attrs if hasattr(self, "attrs") else dict()
P
phlrain 已提交
442 443 444 445 446 447 448 449 450 451 452

        cache_list = None
        if hasattr(self, "cache_name_list"):
            cache_list = self.cache_name_list
        self.op = create_op(
            self.scope,
            self.op_type,
            op_inputs,
            op_outputs,
            op_attrs,
            cache_list=cache_list)
Y
Yu Yang 已提交
453

454 455 456
        if no_grad_set is None:
            no_grad_set = set()

Y
Yancey 已提交
457 458 459
        if not type(output_names) is list:
            output_names = [output_names]

Q
Qiao Longfei 已提交
460
        numeric_grads = user_defined_grads or [
461
            get_numeric_gradient(
462
                place,
463 464 465 466
                self.scope,
                self.op,
                self.inputs,
                input_to_check,
Y
Yancey 已提交
467
                output_names,
468
                delta=numeric_grad_delta,
C
chengduo 已提交
469
                in_place=in_place) for input_to_check in inputs_to_check
470
        ]
471 472 473
        analytic_grads = self._get_gradient(inputs_to_check, place,
                                            output_names, no_grad_set)

D
Dun 已提交
474 475 476
        self._assert_is_close(numeric_grads, analytic_grads, inputs_to_check,
                              max_relative_error,
                              "Gradient Check On %s" % str(place))
Q
qijun 已提交
477

Y
Yu Yang 已提交
478 479 480 481 482
    @staticmethod
    def _numpy_to_lod_tensor(np_value, lod, place):
        tensor = core.LoDTensor()
        tensor.set(np_value, place)
        if lod is not None:
483
            tensor.set_recursive_sequence_lengths(lod)
Y
Yu Yang 已提交
484 485
        return tensor

K
Kexin Zhao 已提交
486
    @staticmethod
K
Kexin Zhao 已提交
487 488
    def np_dtype_to_fluid_dtype(input):
        """Change the dtype of float16 numpy array
K
Kexin Zhao 已提交
489

490
        numpy float16 is binded to paddle::platform::float16
K
Kexin Zhao 已提交
491
        in tensor_py.h via the help of uint16 data type since
492
        the internal memory representation of float16 is
K
Kexin Zhao 已提交
493 494
        uint16_t in paddle and np.uint16 in numpy, which are
        themselves binded together by pybind.
K
Kexin Zhao 已提交
495 496 497 498 499

        Args:
            input: input numpy array

        Returns:
500
            input: The dtype of input will be changed to np.uint16 if
K
Kexin Zhao 已提交
501
                it is originally np.float16, such that the internal memory
502
                of input will be reinterpreted as of dtype np.uint16.
K
Kexin Zhao 已提交
503 504
        """
        if input.dtype == np.float16:
K
Kexin Zhao 已提交
505 506
            input.dtype = np.uint16
        return input
K
Kexin Zhao 已提交
507

D
dzhwinter 已提交
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
    @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

523 524 525 526 527 528
    def _get_gradient(self,
                      input_to_check,
                      place,
                      output_names,
                      no_grad_set,
                      parallel=False):
Y
Yu Yang 已提交
529 530
        prog = Program()
        block = prog.global_block()
531 532
        self._append_ops(block)
        loss = append_loss_ops(block, output_names)
F
fengjiayi 已提交
533
        param_grad_list = append_backward(
Y
Yu Yang 已提交
534 535
            loss=loss, parameter_list=input_to_check, no_grad_set=no_grad_set)

536 537
        inputs = self._get_inputs(block)
        feed_dict = self.feed_var(inputs, place)
Y
Yu Yang 已提交
538 539

        fetch_list = [g for p, g in param_grad_list]
540 541 542 543 544
        if parallel:
            use_cuda = False
            if isinstance(place, fluid.CUDAPlace(0)):
                use_cuda = True
            executor = fluid.ParallelExecutor(
D
dzhwinter 已提交
545
                use_cuda=use_cuda, loss_name=loss.name, main_program=prog)
546 547
        else:
            executor = Executor(place)
548 549 550
        return list(
            map(np.array,
                executor.run(prog, feed_dict, fetch_list, return_numpy=False)))