379 lines
13 KiB
Python
379 lines
13 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 unittest
|
|
|
|
import numpy as np
|
|
from op_test import OpTest, get_device_place, is_custom_device
|
|
from utils import dygraph_guard, static_guard
|
|
|
|
import paddle
|
|
|
|
|
|
def valid_eigh_result(A, eigh_value, eigh_vector, uplo):
|
|
assert A.ndim == 2 or A.ndim == 3
|
|
|
|
if A.ndim == 2:
|
|
valid_single_eigh_result(A, eigh_value, eigh_vector, uplo)
|
|
return
|
|
|
|
for batch_A, batch_w, batch_v in zip(A, eigh_value, eigh_vector):
|
|
valid_single_eigh_result(batch_A, batch_w, batch_v, uplo)
|
|
|
|
|
|
def valid_single_eigh_result(A, eigh_value, eigh_vector, uplo):
|
|
FP32_MAX_RELATIVE_ERR = 5e-5
|
|
FP64_MAX_RELATIVE_ERR = 1e-14
|
|
|
|
if A.dtype == np.single or A.dtype == np.csingle:
|
|
rtol = FP32_MAX_RELATIVE_ERR
|
|
else:
|
|
rtol = FP64_MAX_RELATIVE_ERR
|
|
|
|
M, N = A.shape
|
|
|
|
triangular_func = np.tril if uplo == 'L' else np.triu
|
|
|
|
if not np.iscomplexobj(A):
|
|
# Reconstruct A by filling triangular part
|
|
A = triangular_func(A) + triangular_func(A, -1).T
|
|
else:
|
|
# Reconstruct A to Hermitian matrix
|
|
A = triangular_func(A) + np.matrix(triangular_func(A, -1)).H
|
|
|
|
# Diagonal matrix of eigen value
|
|
T = np.diag(eigh_value)
|
|
|
|
# A = Q*T*Q'
|
|
residual = A - (eigh_vector @ T @ np.linalg.inv(eigh_vector))
|
|
|
|
# ||A - Q*T*Q'|| / (N*||A||) < rtol
|
|
np.testing.assert_array_less(
|
|
np.linalg.norm(residual, np.inf) / (N * np.linalg.norm(A, np.inf)), rtol
|
|
)
|
|
|
|
# ||I - Q*Q'|| / M < rtol
|
|
residual = np.eye(M) - eigh_vector @ np.linalg.inv(eigh_vector)
|
|
np.testing.assert_array_less(np.linalg.norm(residual, np.inf) / M, rtol)
|
|
|
|
|
|
def valid_eigh_shape_result(A, eigh_value, eigh_vector):
|
|
assert A.ndim == 2 or A.ndim == 3
|
|
|
|
if A.ndim == 2:
|
|
valid_single_eigh_shape_result(A, eigh_value, eigh_vector)
|
|
return
|
|
|
|
for batch_A, batch_w, batch_v in zip(A, eigh_value, eigh_vector):
|
|
valid_single_eigh_shape_result(batch_A, batch_w, batch_v)
|
|
|
|
|
|
def valid_single_eigh_shape_result(A, eigh_value, eigh_vector):
|
|
N = A.shape[0]
|
|
if eigh_value.shape != (N,):
|
|
raise ValueError(
|
|
f"Eigenvalues array must have shape ({N},), but got {eigh_value.shape}."
|
|
)
|
|
|
|
if eigh_vector.shape != (N, N):
|
|
raise ValueError(
|
|
f"Eigenvectors matrix must have shape ({N}, {N}), but got {eigh_vector.shape}."
|
|
)
|
|
|
|
|
|
class TestEighOp(OpTest):
|
|
def setUp(self):
|
|
paddle.enable_static()
|
|
self.op_type = "eigh"
|
|
self.python_api = paddle.linalg.eigh
|
|
self.init_input()
|
|
self.init_config()
|
|
np.random.seed(123)
|
|
out_w, out_v = np.linalg.eigh(self.x_np, self.UPLO)
|
|
self.inputs = {"X": self.x_np}
|
|
self.attrs = {"UPLO": self.UPLO}
|
|
self.outputs = {'Eigenvalues': out_w, "Eigenvectors": out_v}
|
|
|
|
def init_config(self):
|
|
self.UPLO = 'L'
|
|
|
|
def init_input(self):
|
|
self.x_shape = (10, 10)
|
|
self.x_type = np.float64
|
|
self.x_np = np.random.random(self.x_shape).astype(self.x_type)
|
|
|
|
def test_check_output(self):
|
|
self.check_output(
|
|
no_check_set=['Eigenvectors'],
|
|
check_symbol_infer=True,
|
|
check_pir=True,
|
|
)
|
|
|
|
def test_grad(self):
|
|
self.check_grad(["X"], ["Eigenvalues"], check_pir=True)
|
|
|
|
|
|
class TestEighUPLOCase(TestEighOp):
|
|
def init_config(self):
|
|
self.UPLO = 'U'
|
|
|
|
def init_input(self):
|
|
self.x_shape = (20, 10, 10)
|
|
self.x_type = np.float64
|
|
self.x_np = np.random.random(self.x_shape).astype(self.x_type)
|
|
|
|
|
|
class TestEighGPUCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.x_shape = [32, 32]
|
|
self.dtype = "float32"
|
|
self.UPLO = "L"
|
|
np.random.seed(123)
|
|
self.x_np = np.random.random(self.x_shape).astype(self.dtype)
|
|
|
|
def test_check_output_gpu(self):
|
|
if paddle.is_compiled_with_cuda() or is_custom_device():
|
|
paddle.disable_static(place=get_device_place())
|
|
input_real_data = paddle.to_tensor(self.x_np)
|
|
actual_w, actual_v = paddle.linalg.eigh(input_real_data, self.UPLO)
|
|
valid_eigh_result(
|
|
self.x_np, actual_w.numpy(), actual_v.numpy(), self.UPLO
|
|
)
|
|
|
|
|
|
class TestEighAPI(unittest.TestCase):
|
|
def setUp(self):
|
|
self.init_input_data()
|
|
self.UPLO = 'L'
|
|
self.rtol = 1e-5 # for test_eigh_grad
|
|
self.atol = 1e-5 # for test_eigh_grad
|
|
self.place = get_device_place()
|
|
np.random.seed(123)
|
|
|
|
def init_input_shape(self):
|
|
self.x_shape = [5, 5]
|
|
|
|
def init_input_data(self):
|
|
self.init_input_shape()
|
|
self.dtype = "float32"
|
|
self.real_data = np.random.random(self.x_shape).astype(self.dtype)
|
|
complex_data = np.random.random(self.x_shape).astype(
|
|
self.dtype
|
|
) + 1j * np.random.random(self.x_shape).astype(self.dtype)
|
|
self.trans_dims = [
|
|
*range(len(self.x_shape) - 2),
|
|
len(self.x_shape) - 1,
|
|
len(self.x_shape) - 2,
|
|
]
|
|
# build a random conjugate matrix
|
|
self.complex_symm = np.divide(
|
|
complex_data + np.conj(complex_data.transpose(self.trans_dims)), 2
|
|
)
|
|
|
|
def check_static_float_result(self):
|
|
main_prog = paddle.static.Program()
|
|
startup_prog = paddle.static.Program()
|
|
with paddle.static.program_guard(main_prog, startup_prog):
|
|
input_x = paddle.static.data(
|
|
'input_x', shape=self.x_shape, dtype=self.dtype
|
|
)
|
|
output_w, output_v = paddle.linalg.eigh(input_x)
|
|
exe = paddle.static.Executor(self.place)
|
|
actual_w, actual_v = exe.run(
|
|
main_prog,
|
|
feed={"input_x": self.real_data},
|
|
fetch_list=[output_w, output_v],
|
|
)
|
|
valid_eigh_result(self.real_data, actual_w, actual_v, self.UPLO)
|
|
|
|
def check_static_complex_result(self):
|
|
main_prog = paddle.static.Program()
|
|
startup_prog = paddle.static.Program()
|
|
with paddle.static.program_guard(main_prog, startup_prog):
|
|
x_dtype = np.complex64 if self.dtype == "float32" else np.complex128
|
|
input_x = paddle.static.data(
|
|
'input_x', shape=self.x_shape, dtype=x_dtype
|
|
)
|
|
output_w, output_v = paddle.linalg.eigh(input_x)
|
|
exe = paddle.static.Executor(self.place)
|
|
actual_w, actual_v = exe.run(
|
|
main_prog,
|
|
feed={"input_x": self.complex_symm},
|
|
fetch_list=[output_w, output_v],
|
|
)
|
|
valid_eigh_result(self.complex_symm, actual_w, actual_v, self.UPLO)
|
|
|
|
def test_in_static_mode(self):
|
|
paddle.enable_static()
|
|
self.check_static_float_result()
|
|
self.check_static_complex_result()
|
|
|
|
def test_in_dynamic_mode(self):
|
|
paddle.disable_static()
|
|
input_real_data = paddle.to_tensor(self.real_data)
|
|
actual_w, actual_v = paddle.linalg.eigh(input_real_data)
|
|
valid_eigh_result(
|
|
self.real_data, actual_w.numpy(), actual_v.numpy(), self.UPLO
|
|
)
|
|
|
|
input_complex_data = paddle.to_tensor(self.complex_symm)
|
|
actual_w, actual_v = paddle.linalg.eigh(input_complex_data)
|
|
valid_eigh_result(
|
|
self.complex_symm, actual_w.numpy(), actual_v.numpy(), self.UPLO
|
|
)
|
|
|
|
def test_eigh_grad(self):
|
|
paddle.disable_static()
|
|
x = paddle.to_tensor(self.complex_symm, stop_gradient=False)
|
|
w, v = paddle.linalg.eigh(x)
|
|
(w.sum() + paddle.abs(v).sum()).backward()
|
|
np.testing.assert_allclose(
|
|
abs(x.grad.numpy()),
|
|
abs(x.grad.numpy().conj().transpose(self.trans_dims)),
|
|
rtol=self.rtol,
|
|
atol=self.atol,
|
|
)
|
|
|
|
|
|
class TestEighBatchAPI(TestEighAPI):
|
|
def init_input_shape(self):
|
|
self.x_shape = [2, 5, 5]
|
|
|
|
|
|
class TestEighAPIError(unittest.TestCase):
|
|
def test_error(self):
|
|
main_prog = paddle.static.Program()
|
|
startup_prog = paddle.static.Program()
|
|
with paddle.static.program_guard(main_prog, startup_prog):
|
|
# input maxtrix must greater than 2 dimensions
|
|
input_x = paddle.static.data(
|
|
name='x_1', shape=[12], dtype='float32'
|
|
)
|
|
self.assertRaises(ValueError, paddle.linalg.eigh, input_x)
|
|
|
|
# input matrix must be square matrix
|
|
input_x = paddle.static.data(
|
|
name='x_2', shape=[12, 32], dtype='float32'
|
|
)
|
|
self.assertRaises(ValueError, paddle.linalg.eigh, input_x)
|
|
|
|
# uplo must be in 'L' or 'U'
|
|
input_x = paddle.static.data(
|
|
name='x_3', shape=[4, 4], dtype="float32"
|
|
)
|
|
uplo = 'R'
|
|
self.assertRaises(ValueError, paddle.linalg.eigh, input_x, uplo)
|
|
|
|
# x_data cannot be integer
|
|
input_x = paddle.static.data(
|
|
name='x_4', shape=[4, 4], dtype="int32"
|
|
)
|
|
self.assertRaises(TypeError, paddle.linalg.eigh, input_x)
|
|
|
|
|
|
class TestEighAPIZeroSize(unittest.TestCase):
|
|
def setUp(self):
|
|
self.init_input_data()
|
|
self.place = get_device_place()
|
|
self.rtol = 1e-5 # for test_eigh_grad
|
|
self.atol = 1e-5 # for test_eigh_grad
|
|
np.random.seed(123)
|
|
|
|
def init_input_shape(self):
|
|
self.x_shape = [0, 0]
|
|
|
|
def init_input_data(self):
|
|
self.init_input_shape()
|
|
self.dtype = "float32"
|
|
self.real_data = np.random.random(self.x_shape).astype(self.dtype)
|
|
|
|
def test_in_static_mode(self):
|
|
with static_guard():
|
|
main_prog = paddle.static.Program()
|
|
startup_prog = paddle.static.Program()
|
|
with paddle.static.program_guard(main_prog, startup_prog):
|
|
input_x = paddle.static.data(
|
|
'input_x', shape=self.x_shape, dtype=self.dtype
|
|
)
|
|
output_w, output_v = paddle.linalg.eigh(input_x)
|
|
exe = paddle.static.Executor(self.place)
|
|
actual_w, actual_v = exe.run(
|
|
main_prog,
|
|
feed={"input_x": self.real_data},
|
|
fetch_list=[output_w, output_v],
|
|
)
|
|
valid_eigh_shape_result(self.real_data, actual_w, actual_v)
|
|
|
|
main_prog = paddle.static.Program()
|
|
startup_prog = paddle.static.Program()
|
|
with paddle.static.program_guard(main_prog, startup_prog):
|
|
input_x = paddle.static.data(
|
|
'input_x', shape=self.x_shape, dtype=self.dtype
|
|
)
|
|
output_w, output_v = paddle.linalg.eigh(input_x)
|
|
exe = paddle.static.Executor(paddle.CPUPlace())
|
|
actual_w, actual_v = exe.run(
|
|
main_prog,
|
|
feed={"input_x": self.real_data},
|
|
fetch_list=[output_w, output_v],
|
|
)
|
|
valid_eigh_shape_result(self.real_data, actual_w, actual_v)
|
|
|
|
def test_in_dynamic_mode(self):
|
|
with dygraph_guard():
|
|
input_real_data = paddle.to_tensor(self.real_data)
|
|
actual_w, actual_v = paddle.linalg.eigh(input_real_data)
|
|
valid_eigh_shape_result(
|
|
self.real_data, actual_w.numpy(), actual_v.numpy()
|
|
)
|
|
|
|
def test_eigh_grad(self):
|
|
paddle.disable_static()
|
|
x = paddle.to_tensor(self.real_data, stop_gradient=False)
|
|
w, v = paddle.linalg.eigh(x)
|
|
self.trans_dims = [
|
|
*range(len(self.x_shape) - 2),
|
|
len(self.x_shape) - 1,
|
|
len(self.x_shape) - 2,
|
|
]
|
|
(w.sum() + paddle.abs(v).sum()).backward()
|
|
np.testing.assert_allclose(
|
|
abs(x.grad.numpy()),
|
|
abs(x.grad.numpy().conj().transpose(self.trans_dims)),
|
|
rtol=self.rtol,
|
|
atol=self.atol,
|
|
)
|
|
|
|
|
|
class TestEighBatchAPIZeroSize(TestEighAPIZeroSize):
|
|
def init_input_shape(self):
|
|
self.x_shape = [0, 5, 5]
|
|
|
|
|
|
class TestEighBatchAPIZeroSize1(TestEighAPIZeroSize):
|
|
def init_input_shape(self):
|
|
self.x_shape = [5, 0, 0]
|
|
|
|
|
|
class TestEighAPIError_ZeroSize(unittest.TestCase):
|
|
def _test_case(self):
|
|
paddle.linalg.eigh(paddle.randn([0, 5]))
|
|
|
|
def test_error(self):
|
|
self.assertRaises(ValueError, self._test_case)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|