op_test.py 20.4 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

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

    tensor_to_check = scope.find_var(input_to_check).get_tensor()
Y
yuyang18 已提交
66 67
    tensor_size = product(tensor_to_check.shape())
    tensor_to_check_dtype = tensor_to_check._dtype()
68
    if tensor_to_check_dtype == core.VarDesc.VarType.FP32:
69
        tensor_to_check_dtype = np.float32
70
    elif tensor_to_check_dtype == core.VarDesc.VarType.FP64:
71
        tensor_to_check_dtype = np.float64
D
dzhwinter 已提交
72 73 74 75
    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)
76 77 78 79 80 81 82
    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 已提交
83 84 85 86 87
        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 已提交
88
            return tensor._get_float_element(i)
89
        else:
Y
yuyang18 已提交
90
            return tensor._get_double_element(i)
91 92

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

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

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

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

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

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

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


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

J
JiabinYang 已提交
141 142
        np.random.seed(123)
        random.seed(124)
143 144 145

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

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

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

        return feed_map

210
    def _append_ops(self, block):
Y
Yang Yang(Tony) 已提交
211
        op_proto = OpProtoHolder.instance().get_op_proto(self.op_type)
212 213 214 215 216 217
        "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) 已提交
218 219 220 221 222
        op = block.append_op(
            type=self.op_type,
            inputs=inputs,
            outputs=outputs,
            attrs=self.attrs if hasattr(self, "attrs") else dict())
Q
QI JUN 已提交
223 224 225
        # 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) 已提交
226

227 228
    def _get_io_vars(self, block, numpy_inputs):
        inputs = {}
M
minqiyang 已提交
229
        for name, value in six.iteritems(numpy_inputs):
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
            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

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

        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 已提交
272
            for var_name, var in six.iteritems(outputs):
273 274
                if no_check_set is not None and var_name in no_check_set:
                    continue
Y
Yang Yang(Tony) 已提交
275 276 277 278 279
                if isinstance(var, list):
                    for v in var:
                        fetch_list.append(v)
                else:
                    fetch_list.append(var)
280 281 282 283 284
        # 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 已提交
285
        if not isinstance(fetch_list[0], fluid.framework.Variable):
286
            fetch_list = list(map(block.var, fetch_list))
287 288 289 290
        outs = executor.run(program,
                            feed=feed_map,
                            fetch_list=fetch_list,
                            return_numpy=False)
291
        return outs, fetch_list
Y
Yang Yang(Tony) 已提交
292

293 294 295 296 297 298
    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) 已提交
299
        for out_name, out_dup in Operator.get_op_outputs(self.op_type):
300 301
            if out_name not in self.outputs:
                continue
302 303
            if no_check_set is not None and out_name in no_check_set:
                continue
304

Y
Yang Yang(Tony) 已提交
305 306 307 308 309 310 311 312 313 314
            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]

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

354
    def _get_places(self):
D
dzhwinter 已提交
355 356 357 358 359 360 361 362
        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 []
363
        places = [fluid.CPUPlace()]
364
        if core.is_compiled_with_cuda() and core.op_support_gpu(self.op_type):
D
dzhwinter 已提交
365
            places.append(core.CUDAPlace(0))
366 367
        return places

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

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

381 382 383
    def __assert_is_close(self, numeric_grads, analytic_grads, names,
                          max_relative_error, msg_prefix):

M
minqiyang 已提交
384
        for a, b, name in six.moves.zip(numeric_grads, analytic_grads, names):
385 386 387 388 389 390 391 392
            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)
393
                return ("%s Variable %s max gradient diff %f over limit %f, "
D
dzhwinter 已提交
394 395 396
                        "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])
397 398 399 400 401

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

    def check_grad(self,
                   inputs_to_check,
Y
Yancey 已提交
402
                   output_names,
403
                   no_grad_set=None,
404
                   numeric_grad_delta=0.005,
405
                   in_place=False,
Q
Qiao Longfei 已提交
406
                   max_relative_error=0.005,
C
chengduo 已提交
407
                   user_defined_grads=None):
408
        places = self._get_places()
409 410 411 412
        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 已提交
413
                                       user_defined_grads)
414 415 416 417 418 419 420 421 422

    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 已提交
423
                              user_defined_grads=None):
424
        self.scope = core.Scope()
Q
qijun 已提交
425
        op_inputs = self.inputs if hasattr(self, "inputs") else dict()
426
        op_outputs = self.outputs if hasattr(self, "outputs") else dict()
Q
qijun 已提交
427
        op_attrs = self.attrs if hasattr(self, "attrs") else dict()
428
        self.op = create_op(self.scope, self.op_type, op_inputs, op_outputs,
Q
qijun 已提交
429
                            op_attrs)
Y
Yu Yang 已提交
430

431 432 433
        if no_grad_set is None:
            no_grad_set = set()

Y
Yancey 已提交
434 435 436
        if not type(output_names) is list:
            output_names = [output_names]

Q
Qiao Longfei 已提交
437
        numeric_grads = user_defined_grads or [
438
            get_numeric_gradient(
439
                place,
440 441 442 443
                self.scope,
                self.op,
                self.inputs,
                input_to_check,
Y
Yancey 已提交
444
                output_names,
445
                delta=numeric_grad_delta,
C
chengduo 已提交
446
                in_place=in_place) for input_to_check in inputs_to_check
447
        ]
448 449 450 451 452 453
        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 已提交
454

Y
Yu Yang 已提交
455 456 457 458 459
    @staticmethod
    def _numpy_to_lod_tensor(np_value, lod, place):
        tensor = core.LoDTensor()
        tensor.set(np_value, place)
        if lod is not None:
460
            tensor.set_recursive_sequence_lengths(lod)
Y
Yu Yang 已提交
461 462
        return tensor

K
Kexin Zhao 已提交
463
    @staticmethod
K
Kexin Zhao 已提交
464 465
    def np_dtype_to_fluid_dtype(input):
        """Change the dtype of float16 numpy array
K
Kexin Zhao 已提交
466

467
        numpy float16 is binded to paddle::platform::float16
K
Kexin Zhao 已提交
468
        in tensor_py.h via the help of uint16 data type since
469
        the internal memory representation of float16 is
K
Kexin Zhao 已提交
470 471
        uint16_t in paddle and np.uint16 in numpy, which are
        themselves binded together by pybind.
K
Kexin Zhao 已提交
472 473 474 475 476

        Args:
            input: input numpy array

        Returns:
477
            input: The dtype of input will be changed to np.uint16 if
K
Kexin Zhao 已提交
478
                it is originally np.float16, such that the internal memory
479
                of input will be reinterpreted as of dtype np.uint16.
K
Kexin Zhao 已提交
480 481
        """
        if input.dtype == np.float16:
K
Kexin Zhao 已提交
482 483
            input.dtype = np.uint16
        return input
K
Kexin Zhao 已提交
484

D
dzhwinter 已提交
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
    @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

500 501 502 503 504 505
    def _get_gradient(self,
                      input_to_check,
                      place,
                      output_names,
                      no_grad_set,
                      parallel=False):
Y
Yu Yang 已提交
506 507
        prog = Program()
        block = prog.global_block()
508 509
        self._append_ops(block)
        loss = append_loss_ops(block, output_names)
F
fengjiayi 已提交
510
        param_grad_list = append_backward(
Y
Yu Yang 已提交
511 512
            loss=loss, parameter_list=input_to_check, no_grad_set=no_grad_set)

513 514
        inputs = self._get_inputs(block)
        feed_dict = self.feed_var(inputs, place)
Y
Yu Yang 已提交
515 516

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