test_svd_op.py 10.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#   Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
#
# 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
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# 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.

import unittest
16

17
import numpy as np
18 19
from op_test import OpTest, skip_check_grad_ci

20 21 22 23 24 25 26 27
import paddle
import paddle.fluid as fluid
import paddle.fluid.core as core


class TestSvdOp(OpTest):
    def setUp(self):
        paddle.enable_static()
28
        self.python_api = paddle.linalg.svd
29 30 31
        self.generate_input()
        self.generate_output()
        self.op_type = "svd"
32
        assert hasattr(self, "_output_data")
33 34 35 36 37
        self.inputs = {"X": self._input_data}
        self.attrs = {'full_matrices': self.get_full_matrices_option()}
        self.outputs = {
            "U": self._output_data[0],
            "S": self._output_data[1],
38
            "VH": self._output_data[2],
39 40 41
        }

    def generate_input(self):
42
        """return a input_data and input_shape"""
43 44 45 46 47 48 49
        self._input_shape = (100, 1)
        self._input_data = np.random.random(self._input_shape).astype("float64")

    def get_full_matrices_option(self):
        return False

    def generate_output(self):
50
        assert hasattr(self, "_input_data")
51 52 53
        self._output_data = np.linalg.svd(self._input_data)

    def test_check_output(self):
54
        self.check_output(no_check_set=['U', 'VH'], check_eager=True)
55 56

    def test_svd_forward(self):
57
        """u matmul diag(s) matmul vt must become X"""
58
        single_input = self._input_data.reshape(
59 60
            [-1, self._input_shape[-2], self._input_shape[-1]]
        )[0]
61 62 63 64 65 66 67 68 69 70 71 72 73
        paddle.disable_static()
        dy_x = paddle.to_tensor(single_input)
        dy_u, dy_s, dy_vt = paddle.linalg.svd(dy_x)
        dy_out_x = dy_u.matmul(paddle.diag(dy_s)).matmul(dy_vt)
        if (paddle.abs(dy_out_x - dy_x) < 1e-7).all():
            ...
        else:
            print("EXPECTED:\n", dy_x)
            print("GOT     :\n", dy_out_x)
            raise RuntimeError("Check SVD Failed")
        paddle.enable_static()

    def check_S_grad(self):
74 75 76
        self.check_grad(
            ['X'], ['S'], numeric_grad_delta=0.001, check_eager=True
        )
77 78

    def check_U_grad(self):
79 80 81
        self.check_grad(
            ['X'], ['U'], numeric_grad_delta=0.001, check_eager=True
        )
82 83

    def check_V_grad(self):
84 85 86
        self.check_grad(
            ['X'], ['VH'], numeric_grad_delta=0.001, check_eager=True
        )
87 88

    def test_check_grad(self):
89
        """
90 91 92 93 94 95 96 97 98 99 100 101 102 103
        remember the input matrix must be the full rank matrix, otherwise the gradient will stochatic because the u / v 's  (n-k) freedom  vectors
        """
        self.check_S_grad()
        self.check_U_grad()
        self.check_V_grad()


class TestSvdCheckGrad2(TestSvdOp):
    # NOTE(xiongkun03): because we want to construct some full rank matrics,
    #                   so we can't specifize matrices which numel() > 100

    no_need_check_grad = True

    def generate_input(self):
104 105
        """return a deterministic  matrix, the range matrix;
        vander matrix must be a full rank matrix.
106 107
        """
        self._input_shape = (5, 5)
108 109 110 111 112
        self._input_data = (
            np.vander([2, 3, 4, 5, 6])
            .astype("float64")
            .reshape(self._input_shape)
        )
113 114 115 116


class TestSvdNormalMatrixSmall(TestSvdCheckGrad2):
    def generate_input(self):
117
        """small matrix SVD."""
118 119 120 121 122 123
        self._input_shape = (1, 1)
        self._input_data = np.random.random(self._input_shape).astype("float64")


class TestSvdNormalMatrix6x3(TestSvdCheckGrad2):
    def generate_input(self):
124 125
        """return a deterministic  matrix, the range matrix;
        vander matrix must be a full rank matrix.
126 127
        """
        self._input_shape = (6, 3)
128 129 130 131 132 133 134 135 136 137
        self._input_data = np.array(
            [
                [1.0, 2.0, 3.0],
                [0.0, 1.0, 5.0],
                [0.0, 0.0, 6.0],
                [2.0, 4.0, 9.0],
                [3.0, 6.0, 8.0],
                [3.0, 1.0, 0.0],
            ]
        ).astype("float64")
138 139 140 141


class TestSvdNormalMatrix3x6(TestSvdCheckGrad2):
    def generate_input(self):
142 143
        """return a deterministic  matrix, the range matrix;
        vander matrix must be a full rank matrix.
144 145
        """
        self._input_shape = (3, 6)
146 147 148 149 150 151 152 153 154 155
        self._input_data = np.array(
            [
                [1.0, 2.0, 3.0],
                [0.0, 1.0, 5.0],
                [0.0, 0.0, 6.0],
                [2.0, 4.0, 9.0],
                [3.0, 6.0, 8.0],
                [3.0, 1.0, 0.0],
            ]
        ).astype("float64")
156 157 158 159 160 161
        self._input_data = self._input_data.transpose((-1, -2))


class TestSvdNormalMatrix6x3Batched(TestSvdOp):
    def generate_input(self):
        self._input_shape = (10, 6, 3)
162 163 164 165 166 167 168 169 170 171
        self._input_data = np.array(
            [
                [1.0, 2.0, 3.0],
                [0.0, 1.0, 5.0],
                [0.0, 0.0, 6.0],
                [2.0, 4.0, 9.0],
                [3.0, 6.0, 8.0],
                [3.0, 1.0, 0.0],
            ]
        ).astype("float64")
172 173 174
        self._input_data = np.stack([self._input_data] * 10, axis=0)

    def test_svd_forward(self):
175
        """test_svd_forward not support batched input, so disable this test."""
176 177 178 179 180
        pass


class TestSvdNormalMatrix3x6Batched(TestSvdOp):
    def generate_input(self):
181 182
        """return a deterministic  matrix, the range matrix;
        vander matrix must be a full rank matrix.
183 184
        """
        self._input_shape = (10, 3, 6)
185 186 187 188 189 190 191 192 193 194
        self._input_data = np.array(
            [
                [1.0, 2.0, 3.0],
                [0.0, 1.0, 5.0],
                [0.0, 0.0, 6.0],
                [2.0, 4.0, 9.0],
                [3.0, 6.0, 8.0],
                [3.0, 1.0, 0.0],
            ]
        ).astype("float64")
195 196 197 198
        self._input_data = self._input_data.transpose((-1, -2))
        self._input_data = np.stack([self._input_data] * 10, axis=0)

    def test_svd_forward(self):
199
        """test_svd_forward not support batched input, so disable this test."""
200 201 202 203 204
        pass


class TestSvdNormalMatrix3x3x3x6Batched(TestSvdOp):
    def generate_input(self):
205 206
        """return a deterministic  matrix, the range matrix;
        vander matrix must be a full rank matrix.
207 208
        """
        self._input_shape = (3, 3, 3, 6)
209 210 211 212 213 214 215 216 217 218
        self._input_data = np.array(
            [
                [1.0, 2.0, 3.0],
                [0.0, 1.0, 5.0],
                [0.0, 0.0, 6.0],
                [2.0, 4.0, 9.0],
                [3.0, 6.0, 8.0],
                [3.0, 1.0, 0.0],
            ]
        ).astype("float64")
219 220
        self._input_data = self._input_data.transpose((-1, -2))
        self._input_data = np.stack(
221 222
            [self._input_data, self._input_data, self._input_data], axis=0
        )
223
        self._input_data = np.stack(
224 225
            [self._input_data, self._input_data, self._input_data], axis=0
        )
226 227

    def test_svd_forward(self):
228
        """test_svd_forward not support batched input, so disable this test."""
229 230 231
        pass


232 233 234 235
@skip_check_grad_ci(
    reason="'check_grad' on large inputs is too slow, "
    + "however it is desirable to cover the forward pass"
)
236 237
class TestSvdNormalMatrixBig(TestSvdOp):
    def generate_input(self):
238
        """big matrix SVD."""
239 240 241 242
        self._input_shape = (2, 200, 300)
        self._input_data = np.random.random(self._input_shape).astype("float64")

    def test_svd_forward(self):
243
        """test_svd_forward not support batched input, so disable this test."""
244 245 246 247 248 249 250 251
        pass

    def test_check_grad(self):
        pass


class TestSvdNormalMatrixBig2(TestSvdOp):
    def generate_input(self):
252
        """big matrix SVD."""
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
        self._input_shape = (1, 100)
        self._input_data = np.random.random(self._input_shape).astype("float64")


class TestSvdNormalMatrixFullMatrices(unittest.TestCase):
    def setUp(self):
        paddle.disable_static()

    def tearDown(self):
        paddle.enable_static()

    def test_full_matrices(self):
        mat_shape = (2, 3)
        mat = np.random.random(mat_shape).astype("float64")
        x = paddle.to_tensor(mat)
        u, s, vh = paddle.linalg.svd(x, full_matrices=True)
269 270
        assert u.shape == [2, 2]
        assert vh.shape == [3, 3]
271
        x_recover = u.matmul(paddle.diag(s)).matmul(vh[0:2])
272
        if (paddle.abs(x_recover - x) > 1e-4).any():
273 274 275 276 277 278 279 280
            raise RuntimeError("mat can't be recovered\n")


class TestSvdFullMatriceGrad(TestSvdNormalMatrix6x3):
    def get_full_matrices_option(self):
        return True

    def test_svd_forward(self):
281
        """test_svd_forward not support full matrices, so disable this test."""
282 283 284
        pass

    def test_check_grad(self):
285
        """
286 287 288
        remember the input matrix must be the full rank matrix, otherwise the gradient will stochatic because the u / v 's  (n-k) freedom  vectors
        """
        self.check_S_grad()
289
        # self.check_U_grad() // don't check U grad, because U have freedom vector
290 291 292 293 294 295 296 297 298 299
        self.check_V_grad()


class TestSvdAPI(unittest.TestCase):
    def test_dygraph(self):
        paddle.disable_static()
        a = np.random.rand(5, 5)
        x = paddle.to_tensor(a)
        u, s, vh = paddle.linalg.svd(x)
        gt_u, gt_s, gt_vh = np.linalg.svd(a, full_matrices=False)
300
        np.testing.assert_allclose(s, gt_s, rtol=1e-05)
301 302 303 304 305 306 307 308 309

    def test_static(self):
        paddle.enable_static()
        places = [fluid.CPUPlace()]
        if core.is_compiled_with_cuda():
            places.append(fluid.CUDAPlace(0))
        for place in places:
            with fluid.program_guard(fluid.Program(), fluid.Program()):
                a = np.random.rand(5, 5)
310 311 312
                x = paddle.fluid.data(
                    name="input", shape=[5, 5], dtype='float64'
                )
313 314 315
                u, s, vh = paddle.linalg.svd(x)
                exe = fluid.Executor(place)
                gt_u, gt_s, gt_vh = np.linalg.svd(a, full_matrices=False)
316 317 318 319 320
                fetches = exe.run(
                    fluid.default_main_program(),
                    feed={"input": a},
                    fetch_list=[s],
                )
321
                np.testing.assert_allclose(fetches[0], gt_s, rtol=1e-05)
322 323 324 325 326


if __name__ == "__main__":
    paddle.enable_static()
    unittest.main()