Skip to content

Commit 7fd222d

Browse files
authored
【Hackathon 5th No.115】为 paddle.linalg.norm 进行功能对齐与功能增强-增加nuclear_norm (#60766)
1 parent de37c94 commit 7fd222d

File tree

2 files changed

+187
-2
lines changed

2 files changed

+187
-2
lines changed

python/paddle/tensor/linalg.py

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,8 @@ def norm(x, p='fro', axis=None, keepdim=False, name=None):
295295
Args:
296296
x (Tensor): The input tensor could be N-D tensor, and the input data
297297
type could be float32 or float64.
298-
p (float|string, optional): Order of the norm. Supported values are `fro`, `0`, `1`, `2`,
299-
`inf`, `-inf` and any positive real number yielding the corresponding p-norm. Not supported: ord < 0 and nuclear norm.
298+
p (float|string, optional): Order of the norm. Supported values are `fro`, `nuc`, `0`, `1`, `2`,
299+
`inf`, `-inf` and any positive real number yielding the corresponding p-norm. Not supported: ord < 0.
300300
Default value is `fro`.
301301
axis (int|list|tuple, optional): The axis on which to apply norm operation. If axis is int
302302
or list(int)/tuple(int) with only one element, the vector norm is computed over the axis.
@@ -374,6 +374,21 @@ def norm(x, p='fro', axis=None, keepdim=False, name=None):
374374
[4., 3., 2., 1.]])
375375
"""
376376

377+
def _backshift_permutation(dim0, dim1, dimn):
378+
"""
379+
Auxiliary function for matrix_norm
380+
Computes the permutation that moves the two given dimensions to the back
381+
"""
382+
ret = [i for i in range(dimn) if i != dim0 and i != dim1]
383+
ret.extend((dim0, dim1))
384+
return ret
385+
386+
def _inverse_permutation(perm):
387+
"""
388+
Given a permutation, returns its inverse. It's equivalent to argsort on an array
389+
"""
390+
return [i for i, j in sorted(enumerate(perm), key=lambda ij: ij[1])]
391+
377392
def frobenius_norm(input, dim=None, keepdim=False, name=None):
378393
"""
379394
The frobenius norm OP is to calculate the frobenius norm of certain two dimensions of Tensor `input`.
@@ -414,6 +429,98 @@ def frobenius_norm(input, dim=None, keepdim=False, name=None):
414429
)
415430
return out
416431

432+
def nuclear_norm(input, axis=axis, keepdim=False, name=None):
433+
"""
434+
The nuclear norm OP is to calculate the nuclear norm of certain two dimensions of Tensor `input`.
435+
Args:
436+
input (Variable): Tensor, data type float32, float64.
437+
dim (list): Two dimensions.
438+
keepdim (bool, optional): Whether keep the dimensions as the `input`, Default False.
439+
name (str, optional): The default value is None. Normally there is no need for
440+
user to set this property. For more information, please refer to :ref:`api_guide_Name`.
441+
"""
442+
443+
perm = _backshift_permutation(axis[0], axis[1], len(input.shape))
444+
inv_perm = _inverse_permutation(perm)
445+
446+
if in_dynamic_mode():
447+
transposed = _C_ops.transpose(input, perm)
448+
u, s, vh = _C_ops.svd(transposed, False)
449+
result = _C_ops.sum(s, -1, None, keepdim)
450+
if keepdim:
451+
result = _C_ops.transpose(
452+
_C_ops.unsqueeze(result, -1), inv_perm
453+
)
454+
return result
455+
456+
attrs = {'axis': axis, 'keepdim': keepdim}
457+
458+
check_variable_and_dtype(
459+
input, 'input', ['float32', 'float64'], 'nuclear_norm'
460+
)
461+
462+
block = LayerHelper('nuclear_nrom', **locals())
463+
out = block.create_variable_for_type_inference(
464+
dtype=block.input_dtype()
465+
)
466+
467+
transpose_out = block.create_variable_for_type_inference(
468+
dtype=block.input_dtype()
469+
)
470+
input_shape = block.create_variable_for_type_inference(
471+
dtype=block.input_dtype()
472+
)
473+
474+
block.append_op(
475+
type='transpose2',
476+
inputs={'X': [input]},
477+
outputs={'Out': [transpose_out], 'XShape': [input_shape]},
478+
attrs={'axis': perm},
479+
)
480+
481+
u = block.create_variable_for_type_inference(dtype=block.input_dtype())
482+
s = block.create_variable_for_type_inference(dtype=block.input_dtype())
483+
vt = block.create_variable_for_type_inference(dtype=block.input_dtype())
484+
block.append_op(
485+
type='svd',
486+
inputs={'X': [transpose_out]},
487+
outputs={'U': u, 'VH': vt, 'S': s},
488+
attrs={'full_matrices': False},
489+
)
490+
491+
reduce_all, sum_axis = _get_reduce_axis(-1, s)
492+
block.append_op(
493+
type='reduce_sum',
494+
inputs={'X': s},
495+
outputs={'Out': out},
496+
attrs={
497+
'dim': sum_axis,
498+
'keep_dim': keepdim,
499+
'reduce_all': reduce_all,
500+
},
501+
)
502+
503+
if keepdim:
504+
unsqueeze_out = block.create_variable_for_type_inference(
505+
dtype=block.input_dtype()
506+
)
507+
508+
block.append_op(
509+
type='unsqueeze2',
510+
inputs={'X': [out]},
511+
outputs={'Out': [unsqueeze_out], 'XShape': [input_shape]},
512+
attrs={'axes': [-1]},
513+
)
514+
515+
block.append_op(
516+
type='transpose2',
517+
inputs={'X': [unsqueeze_out]},
518+
outputs={'Out': [out], 'XShape': [input_shape]},
519+
attrs={'axis': inv_perm},
520+
)
521+
522+
return out
523+
417524
def vector_norm(
418525
input, porder=None, axis=None, keepdim=False, asvector=False, name=None
419526
):
@@ -616,6 +723,8 @@ def p_matrix_norm(input, porder=1.0, axis=axis, keepdim=False, name=None):
616723
elif isinstance(axis, list) and len(axis) == 2:
617724
if p == "fro":
618725
return frobenius_norm(x, dim=axis, keepdim=keepdim, name=name)
726+
elif p == "nuc":
727+
return nuclear_norm(x, axis=axis, keepdim=keepdim, name=name)
619728
elif p == np.inf or p == -np.inf:
620729
return inf_norm(x, porder=p, axis=axis, keepdim=keepdim, name=name)
621730
elif p == 0:

test/legacy_test/test_norm_all.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,23 @@ def numpy_frobenius_norm(x, axis=None, keepdims=False):
8080
return r
8181

8282

83+
def numpy_nuclear_norm(x, axis=None, keepdims=False):
84+
if isinstance(axis, list):
85+
axis = tuple(axis)
86+
r = np.linalg.norm(x, ord='nuc', axis=axis, keepdims=keepdims).astype(
87+
x.dtype
88+
)
89+
return r
90+
91+
8392
def frobenius_norm(x, dim, keep_dim, reduce_all):
8493
return paddle.linalg.norm(x, p='fro', axis=dim, keepdim=keep_dim)
8594

8695

96+
def nuclear_norm(x, dim, keep_dim, reduce_all):
97+
return paddle.linalg.norm(x, p='nuc', axis=dim, keepdim=keep_dim)
98+
99+
87100
class TestFrobeniusNormOp(OpTest):
88101
def setUp(self):
89102
self.python_api = frobenius_norm
@@ -433,6 +446,33 @@ def run_fro(self, p, axis, shape_x, dtype, keep_dim, check_dim=False):
433446
)
434447

435448

449+
def check_nuc_static(self, p, axis, shape_x, dtype, keep_dim, check_dim=False):
450+
with base.program_guard(base.Program()):
451+
data = paddle.static.data(name="X", shape=shape_x, dtype=dtype)
452+
out = paddle.norm(x=data, p=p, axis=axis, keepdim=keep_dim)
453+
place = base.CPUPlace()
454+
exe = base.Executor(place)
455+
np_input = (np.random.rand(*shape_x) + 1.0).astype(dtype)
456+
expected_result = numpy_nuclear_norm(
457+
np_input, axis=axis, keepdims=keep_dim
458+
)
459+
(result,) = exe.run(feed={"X": np_input}, fetch_list=[out])
460+
np.testing.assert_allclose(result, expected_result, rtol=1e-6, atol=1e-8)
461+
if keep_dim and check_dim:
462+
np.testing.assert_equal(result.shape, expected_result.shape)
463+
464+
465+
def check_nuc_dygraph(self, p, axis, shape_x, dtype, keep_dim, check_dim=False):
466+
x_numpy = (np.random.random(shape_x) + 1.0).astype(dtype)
467+
expected_result = numpy_nuclear_norm(x_numpy, axis, keep_dim)
468+
x_paddle = paddle.to_tensor(x_numpy)
469+
result = paddle.norm(x=x_paddle, p=p, axis=axis, keepdim=keep_dim)
470+
result = result.numpy()
471+
np.testing.assert_allclose(result, expected_result, rtol=1e-6, atol=1e-8)
472+
if keep_dim and check_dim:
473+
np.testing.assert_equal(result.shape, expected_result.shape)
474+
475+
436476
def run_pnorm(self, p, axis, shape_x, dtype, keep_dim, check_dim=False):
437477
with base.program_guard(base.Program()):
438478
data = paddle.static.data(name="X", shape=shape_x, dtype=dtype)
@@ -469,6 +509,8 @@ def run_graph(self, p, axis, shape_x, dtype):
469509
out_fro = paddle.norm(x, p='fro')
470510
out_fro = paddle.norm(x, p='fro', axis=0)
471511
out_fro = paddle.norm(x, p='fro', axis=[0, 1])
512+
# compute nuclear norm.
513+
out_nuc = paddle.norm(x, p='nuc', axis=[0, 1])
472514
# compute 2-order norm along [0,1] dimension.
473515
out_pnorm = paddle.norm(x, p=2, axis=[0, 1])
474516
out_pnorm = paddle.norm(x, p=2)
@@ -508,6 +550,15 @@ def test_basic(self):
508550
keep_dim=keep,
509551
check_dim=True,
510552
)
553+
check_nuc_static(
554+
self,
555+
p='nuc',
556+
axis=[0, 1],
557+
shape_x=[2, 3, 4],
558+
dtype='float64',
559+
keep_dim=keep,
560+
check_dim=True,
561+
)
511562
run_pnorm(
512563
self,
513564
p=2,
@@ -636,13 +687,38 @@ def test_basic(self):
636687
def test_dygraph(self):
637688
run_graph(self, p='fro', axis=None, shape_x=[2, 3, 4], dtype="float32")
638689

690+
paddle.disable_static()
691+
keep_dims = {False, True}
692+
for keep in keep_dims:
693+
check_nuc_dygraph(
694+
self,
695+
p='nuc',
696+
axis=[0, 1],
697+
shape_x=[2, 3, 4],
698+
dtype='float64',
699+
keep_dim=keep,
700+
check_dim=True,
701+
)
702+
check_nuc_dygraph(
703+
self,
704+
p='nuc',
705+
axis=[1, 2],
706+
shape_x=[2, 3, 4, 5],
707+
dtype='float64',
708+
keep_dim=keep,
709+
check_dim=True,
710+
)
711+
paddle.enable_static()
712+
639713
def test_name(self):
640714
with base.program_guard(base.Program()):
641715
x = paddle.static.data(name="x", shape=[10, 10], dtype="float32")
642716
y_1 = paddle.norm(x, p='fro', name='frobenius_name')
643717
y_2 = paddle.norm(x, p=2, name='pnorm_name')
718+
y_3 = paddle.norm(x, p='nuc', axis=[0, 1], name='nuclear_name')
644719
self.assertEqual(('frobenius_name' in y_1.name), True)
645720
self.assertEqual(('pnorm_name' in y_2.name), True)
721+
self.assertEqual(('nuclear_name' in y_3.name), True)
646722

647723
def test_errors(self):
648724
with base.program_guard(base.Program(), base.Program()):

0 commit comments

Comments
 (0)