Files
paddlepaddle--paddle/test/legacy_test/test_svd_op.py
T
2026-07-13 12:40:42 +08:00

642 lines
21 KiB
Python

# 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 itertools
import unittest
import numpy as np
from op_test import (
OpTest,
get_device_place,
is_custom_device,
skip_check_grad_ci,
)
from utils import dygraph_guard, static_guard
import paddle
from paddle import base
from paddle.base import core
class TestSvdOp(OpTest):
def setUp(self):
with static_guard():
self.python_api = paddle.linalg.svd
self.generate_input()
self.generate_output()
self.op_type = "svd"
assert hasattr(self, "_output_data")
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],
"VH": self._output_data[2],
}
def _get_places(self):
places = [base.CPUPlace()]
if core.is_compiled_with_cuda() or is_custom_device():
places.append(get_device_place())
return places
def generate_input(self):
"""return a input_data and input_shape"""
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):
assert hasattr(self, "_input_data")
self._output_data = np.linalg.svd(self._input_data)
def test_check_output(self):
self.check_output(no_check_set=['U', 'VH'], check_pir=True)
def test_svd_forward(self):
"""u matmul diag(s) matmul vt must become X"""
single_input = self._input_data.reshape(
[-1, self._input_shape[-2], self._input_shape[-1]]
)[0]
with dygraph_guard():
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-5).all():
...
else:
raise RuntimeError("Check SVD Failed")
def check_S_grad(self):
self.check_grad(['X'], ['S'], numeric_grad_delta=0.001, check_pir=True)
def check_U_grad(self):
self.check_grad(['X'], ['U'], numeric_grad_delta=0.001, check_pir=True)
def check_V_grad(self):
self.check_grad(['X'], ['VH'], numeric_grad_delta=0.001, check_pir=True)
def test_check_grad(self):
"""
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()
@unittest.skipIf(
core.is_compiled_with_xpu(),
"Skip XPU for complex dtype is not fully supported",
)
class TestSvdOpComplexCase1(TestSvdOp):
def generate_input(self):
"""return a input_data and input_shape"""
self._input_shape = (5, 3)
real_part = np.random.rand(*self._input_shape).astype("float32")
imag_part = np.random.rand(*self._input_shape).astype("float32")
self._input_data = real_part + 1j * imag_part
def test_check_grad(self):
places = self._get_places()
with dygraph_guard():
for place in places:
x = paddle.to_tensor(
self._input_data, place=place, stop_gradient=False
)
U, s, Vh = paddle.linalg.svd(x, self.get_full_matrices_option())
loss = (
paddle.sum(paddle.abs(U))
+ paddle.sum(paddle.abs(s))
+ paddle.sum(paddle.abs(Vh))
)
x_grad = paddle.grad(outputs=[loss], inputs=[x])
@unittest.skipIf(
core.is_compiled_with_xpu(),
"Skip XPU for complex dtype is not fully supported",
)
class TestSvdOpComplexCase2(TestSvdOpComplexCase1):
def generate_input(self):
"""return a input_data and input_shape"""
self._input_shape = (3, 30)
real_part = np.random.rand(*self._input_shape).astype("float32")
imag_part = np.random.rand(*self._input_shape).astype("float32")
self._input_data = real_part + 1j * imag_part
@unittest.skipIf(
core.is_compiled_with_xpu(),
"Skip XPU for complex dtype is not fully supported",
)
class TestSvdOpComplexCase3(TestSvdOpComplexCase1):
def generate_input(self):
"""return a input_data and input_shape"""
self._input_shape = (100, 40)
real_part = np.random.rand(*self._input_shape).astype("float64")
imag_part = np.random.rand(*self._input_shape).astype("float64")
self._input_data = real_part + 1j * imag_part
@unittest.skipIf(
core.is_compiled_with_xpu(),
"Skip XPU for complex dtype is not fully supported",
)
class TestSvdOpComplexCase4(TestSvdOpComplexCase1):
def generate_input(self):
"""return a input_data and input_shape"""
self._input_shape = (100, 200)
real_part = np.random.rand(*self._input_shape).astype("float64")
imag_part = np.random.rand(*self._input_shape).astype("float64")
self._input_data = real_part + 1j * imag_part
def get_full_matrices_option(self):
return True
class TestSvdCheckGrad2(TestSvdOp):
# NOTE(xiongkun03): because we want to construct some full rank matrices,
# so we can't specifize matrices which numel() > 100
no_need_check_grad = True
def generate_input(self):
"""return a deterministic matrix, the range matrix;
vander matrix must be a full rank matrix.
"""
self._input_shape = (5, 5)
self._input_data = (
np.vander([2, 3, 4, 5, 6])
.astype("float64")
.reshape(self._input_shape)
)
class TestSvdNormalMatrixSmall(TestSvdCheckGrad2):
def generate_input(self):
"""small matrix SVD."""
self._input_shape = (1, 1)
self._input_data = np.random.random(self._input_shape).astype("float64")
class TestSvdNormalMatrix6x3(TestSvdCheckGrad2):
def generate_input(self):
"""return a deterministic matrix, the range matrix;
vander matrix must be a full rank matrix.
"""
self._input_shape = (6, 3)
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")
class TestSvdNormalMatrix3x6(TestSvdCheckGrad2):
def generate_input(self):
"""return a deterministic matrix, the range matrix;
vander matrix must be a full rank matrix.
"""
self._input_shape = (3, 6)
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")
self._input_data = self._input_data.transpose((-1, -2))
class TestSvdNormalMatrix6x3Batched(TestSvdOp):
def generate_input(self):
self._input_shape = (10, 6, 3)
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")
self._input_data = np.stack([self._input_data] * 10, axis=0)
def test_svd_forward(self):
"""test_svd_forward not support batched input, so disable this test."""
pass
class TestSvdNormalMatrix3x6Batched(TestSvdOp):
def generate_input(self):
"""return a deterministic matrix, the range matrix;
vander matrix must be a full rank matrix.
"""
self._input_shape = (10, 3, 6)
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")
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):
"""test_svd_forward not support batched input, so disable this test."""
pass
class TestSvdNormalMatrix3x3x3x6Batched(TestSvdOp):
def generate_input(self):
"""return a deterministic matrix, the range matrix;
vander matrix must be a full rank matrix.
"""
self._input_shape = (3, 3, 3, 6)
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")
self._input_data = self._input_data.transpose((-1, -2))
self._input_data = np.stack(
[self._input_data, self._input_data, self._input_data], axis=0
)
self._input_data = np.stack(
[self._input_data, self._input_data, self._input_data], axis=0
)
def test_svd_forward(self):
"""test_svd_forward not support batched input, so disable this test."""
pass
@skip_check_grad_ci(
reason="'check_grad' on large inputs is too slow, "
+ "however it is desirable to cover the forward pass"
)
class TestSvdNormalMatrixBig(TestSvdOp):
def generate_input(self):
"""big matrix SVD."""
self._input_shape = (2, 200, 300)
self._input_data = np.random.random(self._input_shape).astype("float64")
def test_svd_forward(self):
"""test_svd_forward not support batched input, so disable this test."""
pass
def test_check_grad(self):
pass
class TestSvdNormalMatrixBig2(TestSvdOp):
def generate_input(self):
"""big matrix SVD."""
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)
assert u.shape == [2, 2]
assert vh.shape == [3, 3]
x_recover = u.matmul(paddle.diag(s)).matmul(vh[0:2])
if (paddle.abs(x_recover - x) > 1e-4).any():
raise RuntimeError("mat can't be recovered\n")
class TestSvdFullMatriceGrad(TestSvdNormalMatrix6x3):
def get_full_matrices_option(self):
return True
def test_svd_forward(self):
"""test_svd_forward not support full matrices, so disable this test."""
pass
def test_check_grad(self):
"""
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() // don't check U grad, because U have freedom vector
self.check_V_grad()
class TestSvdAPI(unittest.TestCase):
def test_dygraph(self):
def run_svd_dygraph(shape, dtype):
if dtype == "float32":
np_dtype = np.float32
elif dtype == "float64":
np_dtype = np.float64
elif dtype == "complex64":
np_dtype = np.complex64
elif dtype == "complex128":
np_dtype = np.complex128
if np.issubdtype(np_dtype, np.complexfloating):
a_dtype = np.float32 if np_dtype == np.complex64 else np.float64
a_real = np.random.rand(*shape).astype(a_dtype)
a_imag = np.random.rand(*shape).astype(a_dtype)
a = a_real + 1j * a_imag
else:
a = np.random.rand(*shape).astype(np_dtype)
places = []
places.append(base.CPUPlace())
if core.is_compiled_with_cuda() or is_custom_device():
places.append(get_device_place())
for place in places:
x = paddle.to_tensor(a, place=place)
u, s, vh = paddle.linalg.svd(x)
gt_u, gt_s, gt_vh = np.linalg.svd(a, full_matrices=False)
np.testing.assert_allclose(s, gt_s, rtol=1e-05)
with dygraph_guard():
np.random.seed(7)
tensor_shapes = [
(0, 3),
(3, 5),
(5, 5),
(5, 3), # 2-dim Tensors
(0, 3, 5),
(4, 0, 5),
(5, 4, 0),
(4, 5, 3), # 3-dim Tensors
(0, 5, 3, 5),
(2, 5, 3, 5),
(3, 5, 5, 5),
(4, 5, 5, 3), # 4-dim Tensors
]
dtypes = ["float32", "float64", 'complex64', 'complex128']
for tensor_shape, dtype in itertools.product(tensor_shapes, dtypes):
run_svd_dygraph(tensor_shape, dtype)
def test_static(self):
def run_svd_static(shape, dtype):
if dtype == "float32":
np_dtype = np.float32
elif dtype == "float64":
np_dtype = np.float64
elif dtype == "complex64":
np_dtype = np.complex64
elif dtype == "complex128":
np_dtype = np.complex128
if np.issubdtype(np_dtype, np.complexfloating):
a_dtype = np.float32 if np_dtype == np.complex64 else np.float64
a_real = np.random.rand(*shape).astype(a_dtype)
a_imag = np.random.rand(*shape).astype(a_dtype)
a = a_real + 1j * a_imag
else:
a = np.random.rand(*shape).astype(np_dtype)
places = []
places.append(base.CPUPlace())
if core.is_compiled_with_cuda() or is_custom_device():
places.append(get_device_place())
for place in places:
with paddle.static.program_guard(
paddle.static.Program(), paddle.static.Program()
):
x = paddle.static.data(
name="input", shape=shape, dtype=dtype
)
u, s, vh = paddle.linalg.svd(x)
exe = paddle.static.Executor(place)
gt_u, gt_s, gt_vh = np.linalg.svd(a, full_matrices=False)
fetches = exe.run(
feed={"input": a},
fetch_list=[s],
)
np.testing.assert_allclose(fetches[0], gt_s, rtol=1e-05)
with static_guard():
np.random.seed(7)
tensor_shapes = [
(0, 3),
(3, 5),
(5, 5),
(5, 3), # 2-dim Tensors
(0, 3, 5),
(4, 0, 5),
(5, 4, 0),
(4, 5, 3), # 3-dim Tensors
(0, 5, 3, 5),
(2, 5, 3, 5),
(3, 5, 5, 5),
(4, 5, 5, 3), # 4-dim Tensors
]
dtypes = ["float32", "float64", 'complex64', 'complex128']
for tensor_shape, dtype in itertools.product(
tensor_shapes, dtypes
):
run_svd_static(tensor_shape, dtype)
class SvdOutTest(unittest.TestCase):
def setUp(self):
paddle.disable_static()
if core.is_compiled_with_cuda():
self.place = core.CUDAPlace(0)
else:
self.place = core.CPUPlace()
def test_svd_api(self):
def run_svd(test_type):
x = paddle.to_tensor(
[[1.0, 2.0], [1.0, 3.0], [4.0, 6.0]], dtype='float64'
)
a = paddle.ones([3, 2], dtype="float64")
b = paddle.ones([2], dtype="float64")
c = paddle.ones([2, 2], dtype="float64")
x.stop_gradient = False
a.stop_gradient = False
b.stop_gradient = False
c.stop_gradient = False
input = x + x
u = a + a
s = b + b
vh = c + c
out = (u, s, vh)
if test_type == "return":
out = paddle.linalg.svd(input, False)
elif test_type == "input_out":
paddle.linalg.svd(input, False, out=out)
elif test_type == "both_return":
out = paddle.linalg.svd(input, False, out=out)
elif test_type == "both_input_out":
tmp = paddle.linalg.svd(input, False, out=out)
ref_out = paddle._C_ops.svd(input, False)
np.testing.assert_allclose(
ref_out[0].numpy(),
out[0].numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
ref_out[1].numpy(),
out[1].numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
ref_out[2].numpy(),
out[2].numpy(),
1e-20,
1e-20,
)
out_0 = out[0] + out[0]
out_1 = out[1] + out[1]
out_2 = out[2] + out[2]
(
paddle.sum(paddle.abs(out_0))
+ paddle.sum(paddle.abs(out_1))
+ paddle.sum(paddle.abs(out_2))
).backward()
return out[0], out[1], out[2], x.grad, a.grad, b.grad, c.grad
paddle.disable_static()
u1, s1, vh1, gx1, ga1, gb1, gc1 = run_svd("return")
u2, s2, vh2, gx2, ga2, gb2, gc2 = run_svd("input_out")
u3, s3, vh3, gx3, ga3, gb3, gc3 = run_svd("both_return")
u4, s4, vh4, gx4, ga4, gb4, gc4 = run_svd("both_input_out")
np.testing.assert_allclose(
u1.numpy(),
u2.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
u1.numpy(),
u3.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
u1.numpy(),
u4.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
s1.numpy(),
s2.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
s1.numpy(),
s3.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
s1.numpy(),
s4.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
vh1.numpy(),
vh2.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
vh1.numpy(),
vh3.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
vh1.numpy(),
vh4.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
gx1.numpy(),
gx2.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
gx1.numpy(),
gx3.numpy(),
1e-20,
1e-20,
)
np.testing.assert_allclose(
gx1.numpy(),
gx4.numpy(),
1e-20,
1e-20,
)
np.testing.assert_equal(ga1, None)
np.testing.assert_equal(ga2, None)
np.testing.assert_equal(ga3, None)
np.testing.assert_equal(ga4, None)
np.testing.assert_equal(gb1, None)
np.testing.assert_equal(gb2, None)
np.testing.assert_equal(gb3, None)
np.testing.assert_equal(gb4, None)
np.testing.assert_equal(gc1, None)
np.testing.assert_equal(gc2, None)
np.testing.assert_equal(gc3, None)
np.testing.assert_equal(gc4, None)
if __name__ == "__main__":
unittest.main()