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

    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)
Y
Yang Yang(Tony) 已提交
219 220 221 222 223
        op = block.append_op(
            type=self.op_type,
            inputs=inputs,
            outputs=outputs,
            attrs=self.attrs if hasattr(self, "attrs") else dict())
Q
QI JUN 已提交
224 225 226
        # 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) 已提交
227

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Args:
            input: input numpy array

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

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

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

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

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