Skip to content

Commit 34ebd2d

Browse files
authored
[API Compatiblity] Support paddle.unflatten and add paddle.nn.functional.unfold (PaddlePaddle#76493)
* add unfold * remove * fix UT * fix doc
1 parent 0d9b235 commit 34ebd2d

File tree

5 files changed

+274
-53
lines changed

5 files changed

+274
-53
lines changed

python/paddle/compat/nn/__init__.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121

2222
import paddle
2323
from paddle import nn
24-
from paddle.framework import (
25-
in_dynamic_mode,
26-
)
2724
from paddle.utils.decorator_utils import ForbidKeywordsDecorator
2825

2926
from . import functional
@@ -458,28 +455,16 @@ def __init__(
458455
super().__init__(kernel_size, dilation, padding, stride)
459456

460457
def forward(self, input: Tensor) -> Tensor:
461-
def to_list_if_necessary(x, size_check=False):
462-
res = x
463-
if in_dynamic_mode() and isinstance(
464-
x, (paddle.pir.Value, paddle.Tensor)
465-
):
466-
res = x.tolist()
467-
else:
468-
if not isinstance(x, (list, tuple, int)):
469-
raise TypeError(
470-
"paddle.compat.nn.Unfold does not allow paddle.Tensor or pir.Value as inputs in static graph mode."
471-
)
472-
if size_check and isinstance(res, (list, tuple)) and len(res) > 2:
473-
raise ValueError(
474-
f"The `padding` field of paddle.compat.nn.Unfold can only have size 1 or 2, now len={len(res)}. \nDid you mean to use paddle.nn.Unfold() instead?"
475-
)
476-
return res
458+
def to_list_if_necessary(x):
459+
if isinstance(x, (paddle.pir.Value, paddle.Tensor)):
460+
x = x.tolist()
461+
return x
477462

478463
return nn.functional.unfold(
479464
input,
480465
kernel_sizes=to_list_if_necessary(self.kernel_sizes),
481466
strides=to_list_if_necessary(self.strides),
482-
paddings=to_list_if_necessary(self.paddings, size_check=True),
467+
paddings=to_list_if_necessary(self.paddings),
483468
dilations=to_list_if_necessary(self.dilations),
484469
name=self.name,
485470
)

python/paddle/compat/nn/functional/__init__.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@
3131
from paddle import Tensor
3232
from paddle._typing import (
3333
ShapeLike,
34+
Size2,
3435
)
3536

3637
_PaddingTensorMode: TypeAlias = Literal[
3738
"zeros", "constant", "reflect", "replicate", "circular"
3839
]
3940

4041

41-
__all__ = ['pad', 'softmax', 'linear']
42+
__all__ = ['pad', 'softmax', 'linear', 'unfold']
4243

4344

4445
def _check_valid_pad_len(pad_len, x_dim, is_constant):
@@ -266,3 +267,95 @@ def linear(input: Tensor, weight: Tensor, bias: Tensor | None = None) -> Tensor:
266267
if bias is not None:
267268
out = _C_ops.add(out, bias)
268269
return out
270+
271+
272+
@ForbidKeywordsDecorator(
273+
illegal_keys={
274+
"x",
275+
"kernel_sizes",
276+
"dilations",
277+
"paddings",
278+
"strides",
279+
"name",
280+
},
281+
func_name="paddle.compat.nn.functional.unfold",
282+
correct_name="paddle.nn.functional.unfold",
283+
)
284+
def unfold(
285+
input: Tensor,
286+
kernel_size: Size2,
287+
dilation: Size2 = 1,
288+
padding: Size2 = 0,
289+
stride: Size2 = 1,
290+
) -> Tensor:
291+
r"""
292+
293+
Return a col buffer of sliding local blocks of input, also known
294+
as im2col for batched 2D image tensors. For each block under the convolution filter,
295+
all element will be rearranged as a column. While the convolution filter sliding over
296+
the input feature map, a series of such columns will be formed.
297+
298+
For each input :math:`input` with shape [N, C, H, W], the output shape [N, Cout, Lout]
299+
can be calculated as following.
300+
301+
.. math::
302+
303+
dkernel[0] &= dilation[0] \times (kernel\_sizes[0] - 1) + 1
304+
305+
dkernel[1] &= dilation[1] \times (kernel\_sizes[1] - 1) + 1
306+
307+
hout &= \frac{H + padding[0] + padding[2] - dkernel[0]}{stride[0]} + 1
308+
309+
wout &= \frac{W + padding[1] + padding[3] - dkernel[1]}{stride[1]} + 1
310+
311+
Cout &= C \times kernel\_sizes[0] \times kernel\_sizes[1]
312+
313+
Lout &= hout \times wout
314+
315+
316+
Parameters:
317+
input(Tensor): 4-D Tensor, input tensor of format [N, C, H, W],
318+
data type can be float32 or float64
319+
kernel_size(int|list|tuple): The size of convolution kernel, should be [k_h, k_w]
320+
or an integer k treated as [k, k].
321+
dilation(int|list|tuple, optional): the dilation of convolution kernel, should be
322+
[dilation_h, dilation_w], or an integer dilation treated as
323+
[dilation, dilation]. For default, it will be [1, 1].
324+
padding(int|list|tuple, optional): The paddings of each dimension, should be
325+
a single integer or [padding_h, padding_w]. If [padding_h, padding_w] was given, it will expanded to
326+
[padding_h, padding_w, padding_h, padding_w]. If an integer padding was given,
327+
[padding, padding, padding, padding] will be used. By default, paddings will be 0.
328+
strides(int|list|tuple, optional): The strides, should be [stride_h, stride_w]
329+
or an integer stride treated as [stride, stride].
330+
For default, strides will be [1, 1].
331+
332+
Returns:
333+
Tensor, The tensor corresponding to the sliding local blocks.
334+
The output shape is [N, Cout, Lout] as described above.
335+
Cout is the total number of values within each block,
336+
and Lout is the total number of such blocks.
337+
The data type of output is the same as the input :math:`input`
338+
339+
Examples:
340+
341+
.. code-block:: python
342+
343+
>>> import paddle
344+
>>> import paddle.compat.nn.functional as F
345+
346+
>>> x = paddle.randn((100,3,224,224))
347+
>>> y = F.unfold(x, [3, 3], 1, 1, 1)
348+
"""
349+
350+
def to_list_if_necessary(x):
351+
if isinstance(x, (paddle.pir.Value, paddle.Tensor)):
352+
x = x.tolist()
353+
return x
354+
355+
return paddle.nn.functional.unfold(
356+
x=input,
357+
kernel_sizes=to_list_if_necessary(kernel_size),
358+
strides=to_list_if_necessary(stride),
359+
paddings=to_list_if_necessary(padding),
360+
dilations=to_list_if_necessary(dilation),
361+
)

python/paddle/tensor/manipulation.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7705,6 +7705,7 @@ def index_add_(
77057705
return _C_ops.index_add_(x, index, scaled_value, axis)
77067706

77077707

7708+
@ParamAliasDecorator({"x": ["input"], "axis": ["dim"], "shape": ["sizes"]})
77087709
def unflatten(
77097710
x: Tensor, axis: int, shape: ShapeLike, name: str | None = None
77107711
) -> Tensor:
@@ -7718,13 +7719,21 @@ def unflatten(
77187719
:alt: Illustration of unflatten
77197720
:align: center
77207721
7722+
.. note::
7723+
Alias Support: The parameter name ``input`` can be used as an alias for ``x``.
7724+
Alias Support: The parameter name ``dim`` can be used as an alias for ``axis``.
7725+
Alias Support: The parameter name ``sizes`` can be used as an alias for ``shape``.
7726+
77217727
Args:
77227728
x (Tensor) : An N-D Tensor. The data type is float16, float32, float64, int16, int32, int64, bool, uint16.
7729+
Alias: ``input``.
77237730
axis (int): :attr:`axis` to be unflattened, specified as an index into `x.shape`.
7731+
Alias: ``dim``.
77247732
shape (list|tuple|Tensor): Unflatten :attr:`shape` on the specified :attr:`axis`. At most one dimension of the target :attr:`shape` can be -1.
77257733
If the input :attr:`shape` does not contain -1 , the product of all elements in ``shape`` should be equal to ``x.shape[axis]``.
77267734
The data type is `int` . If :attr:`shape` is a list or tuple, the elements of it should be integers or Tensors with shape [].
77277735
If :attr:`shape` is an Tensor, it should be an 1-D Tensor.
7736+
Alias: ``sizes``.
77287737
name(str|None, optional): For details, please refer to :ref:`api_guide_Name`. Generally, no setting is required. Default: None.
77297738
77307739
Returns:
@@ -7740,21 +7749,21 @@ def unflatten(
77407749
>>> axis = 1
77417750
>>> res = paddle.unflatten(x, axis, shape)
77427751
>>> print(res.shape)
7743-
[4, 2, 3, 8]
7752+
paddle.Size([4, 2, 3, 8])
77447753
77457754
>>> x = paddle.randn(shape=[4, 6, 8])
77467755
>>> shape = (-1, 2)
77477756
>>> axis = -1
77487757
>>> res = paddle.unflatten(x, axis, shape)
77497758
>>> print(res.shape)
7750-
[4, 6, 4, 2]
7759+
paddle.Size([4, 6, 4, 2])
77517760
77527761
>>> x = paddle.randn(shape=[4, 6, 8])
77537762
>>> shape = paddle.to_tensor([2, 2])
77547763
>>> axis = 0
77557764
>>> res = paddle.unflatten(x, axis, shape)
77567765
>>> print(res.shape)
7757-
[2, 2, 6, 8]
7766+
paddle.Size([2, 2, 6, 8])
77587767
"""
77597768

77607769
# determine whether the input axis is valid.

test/legacy_test/test_compat_unfold.py

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import unittest
1515

1616
import numpy as np
17-
from op_test import get_device_place, is_custom_device
1817

1918
import paddle
19+
import paddle.compat.nn.functional as F_compat
2020

2121

2222
class TestCompatUnfold(unittest.TestCase):
@@ -77,8 +77,6 @@ def test_error_handling(self):
7777

7878
msg_gt_1 = "paddle.nn.Unfold() received unexpected keyword arguments 'dilation', 'stride'. \nDid you mean to use paddle.compat.nn.Unfold() instead?"
7979
msg_gt_2 = "paddle.compat.nn.Unfold() received unexpected keyword argument 'paddings'. \nDid you mean to use paddle.nn.Unfold() instead?"
80-
msg_gt_3 = "The `padding` field of paddle.compat.nn.Unfold can only have size 1 or 2, now len=4. \nDid you mean to use paddle.nn.Unfold() instead?"
81-
msg_gt_4 = "paddle.compat.nn.Unfold does not allow paddle.Tensor or pir.Value as inputs in static graph mode."
8280

8381
with self.assertRaises(TypeError) as cm:
8482
unfold = paddle.nn.Unfold([3, 3], dilation=[2, 2], stride=[1, 1])
@@ -88,33 +86,95 @@ def test_error_handling(self):
8886
unfold = paddle.compat.nn.Unfold([3, 3], paddings=[2, 1])
8987
self.assertEqual(str(cm.exception), msg_gt_2)
9088

91-
with self.assertRaises(ValueError) as cm:
92-
unfold = paddle.compat.nn.Unfold([3, 3], padding=[2, 1, 2, 2])
93-
res = unfold(paddle.ones([2, 2, 5, 5]))
94-
self.assertEqual(str(cm.exception), msg_gt_3)
89+
90+
class TestCompatFunctionalUnfold(unittest.TestCase):
91+
def _compare_with_origin(
92+
self, input_tensor, kernel_size, dilation, padding, stride
93+
):
94+
out_compat = F_compat.unfold(
95+
input=input_tensor,
96+
kernel_size=kernel_size,
97+
dilation=dilation,
98+
padding=padding,
99+
stride=stride,
100+
)
101+
102+
out_origin = paddle.nn.functional.unfold(
103+
x=input_tensor,
104+
kernel_sizes=kernel_size
105+
if not isinstance(kernel_size, paddle.Tensor)
106+
else kernel_size.tolist(),
107+
dilations=dilation
108+
if not isinstance(dilation, paddle.Tensor)
109+
else dilation.tolist(),
110+
paddings=padding
111+
if not isinstance(padding, paddle.Tensor)
112+
else padding.tolist(),
113+
strides=stride
114+
if not isinstance(stride, paddle.Tensor)
115+
else stride.tolist(),
116+
)
117+
118+
expected_res = out_origin.numpy()
119+
np.testing.assert_allclose(out_compat.numpy(), expected_res)
120+
121+
to_tensor = lambda x: x if isinstance(x, int) else paddle.to_tensor(x)
122+
k_t = (
123+
to_tensor(kernel_size)
124+
if not isinstance(kernel_size, paddle.Tensor)
125+
else kernel_size
126+
)
127+
d_t = (
128+
to_tensor(dilation)
129+
if not isinstance(dilation, paddle.Tensor)
130+
else dilation
131+
)
132+
p_t = (
133+
to_tensor(padding)
134+
if not isinstance(padding, paddle.Tensor)
135+
else padding
136+
)
137+
s_t = (
138+
to_tensor(stride)
139+
if not isinstance(stride, paddle.Tensor)
140+
else stride
141+
)
142+
143+
out_compat_tensor = F_compat.unfold(
144+
input=input_tensor,
145+
kernel_size=k_t,
146+
dilation=d_t,
147+
padding=p_t,
148+
stride=s_t,
149+
)
150+
np.testing.assert_allclose(out_compat_tensor.numpy(), expected_res)
151+
152+
def test_compare_with_origin(self):
153+
input_shape = (3, 4, 5, 6)
154+
input_tensor = paddle.arange(360, dtype=paddle.float32).reshape(
155+
input_shape
156+
)
157+
self._compare_with_origin(input_tensor, [3, 3], [1, 1], (1, 2), [1, 1])
158+
159+
input_shape = (5, 10, 13, 13)
160+
input_tensor = paddle.ones(input_shape, dtype=paddle.float64)
161+
self._compare_with_origin(input_tensor, [4, 4], [2, 2], 1, (1, 2))
162+
163+
input_shape = (12, 4, 10, 10)
164+
input_tensor = paddle.ones(input_shape, dtype=paddle.float64)
165+
self._compare_with_origin(input_tensor, 3, 2, 1, (1, 1))
166+
167+
def test_error_handling(self):
168+
"""Test whether there will be correct exception when users pass incorrect kwargs."""
169+
x = paddle.randn([3, 9, 5, 5])
170+
171+
msg_gt_wrong_key = "paddle.compat.nn.functional.unfold() received unexpected keyword argument 'paddings'. \nDid you mean to use paddle.nn.functional.unfold() instead?"
95172

96173
with self.assertRaises(TypeError) as cm:
97-
paddle.enable_static()
98-
input_data = np.random.randn(2, 4, 8, 8).astype(np.float32)
99-
with paddle.static.program_guard(paddle.static.Program()):
100-
x = paddle.static.data(
101-
name='x', shape=[None, None, 8, 8], dtype='float32'
102-
)
103-
place = (
104-
get_device_place()
105-
if (paddle.is_compiled_with_cuda() or is_custom_device())
106-
else paddle.CPUPlace()
107-
)
108-
unfold_pass = paddle.compat.nn.Unfold(
109-
kernel_size=paddle.to_tensor([3, 3]),
110-
padding=paddle.to_tensor([1, 2]),
111-
)
112-
result = unfold_pass(x)
113-
exe = paddle.static.Executor(place)
114-
feed = {'x': input_data}
115-
exe_res = exe.run(feed=feed)
116-
paddle.disable_static()
117-
self.assertEqual(str(cm.exception), msg_gt_4)
174+
F_compat.unfold(x, [3, 3], paddings=[2, 1])
175+
self.assertEqual(str(cm.exception), msg_gt_wrong_key)
176+
177+
paddle.disable_static()
118178

119179

120180
if __name__ == '__main__':

0 commit comments

Comments
 (0)