Repository: chainer/chainer

Issue:
Unpooling2D function?
Do you have plan to implement unpooling2d function?



Discussion:
I'd like to use it to construct convolutional autoencoder like [here](https://github.com/mikesj-public/convolutional_autoencoder).

simple demo: https://gist.github.com/wkentaro/bb8300867a5ed6d53200

Duplicate of https://github.com/pfnet/chainer/pull/301

I didn't notice that, but that is PR not issue. So maybe we can keep this issue open.


Resolving patch:
diff --git a/chainer/functions/__init__.py b/chainer/functions/__init__.py
--- a/chainer/functions/__init__.py
+++ b/chainer/functions/__init__.py
@@ -53,6 +53,7 @@
 from chainer.functions.pooling import average_pooling_2d
 from chainer.functions.pooling import max_pooling_2d
 from chainer.functions.pooling import spatial_pyramid_pooling_2d
+from chainer.functions.pooling import unpooling_2d
 from chainer.links.activation import prelu as links_prelu
 from chainer.links.connection import bilinear as links_bilinear
 from chainer.links.connection import convolution_2d as links_convolution_2d
@@ -190,6 +191,9 @@
 spatial_pyramid_pooling_2d = \
     spatial_pyramid_pooling_2d.spatial_pyramid_pooling_2d
 
+Unpooling2D = unpooling_2d.Unpooling2D
+unpooling_2d = unpooling_2d.unpooling_2d
+
 # Import for backward compatibility
 PReLU = links_prelu.PReLU
 
diff --git a/chainer/functions/pooling/unpooling_2d.py b/chainer/functions/pooling/unpooling_2d.py
new file mode 100644
--- /dev/null
+++ b/chainer/functions/pooling/unpooling_2d.py
@@ -0,0 +1,101 @@
+import numpy
+
+from chainer import cuda
+from chainer.functions.pooling import pooling_2d
+from chainer.utils import conv
+from chainer.utils import type_check
+
+
+class Unpooling2D(pooling_2d.Pooling2D):
+
+    """Unpooling over a set of 2d planes."""
+
+    def __init__(self, ksize, stride=None, pad=0,
+                 outsize=None, cover_all=True):
+        super(Unpooling2D, self).__init__(ksize, stride, pad, cover_all)
+        self.outh, self.outw = (None, None) if outsize is None else outsize
+
+    def check_type_forward(self, in_types):
+        n_in = in_types.size()
+        type_check.expect(n_in == 1)
+        x_type = in_types[0]
+
+        type_check.expect(
+            x_type.dtype == numpy.float32,
+            x_type.ndim == 4,
+        )
+
+        if self.outh is not None:
+            expected_h = conv.get_conv_outsize(
+                self.outh, self.kh, self.sy, self.ph, cover_all=self.cover_all)
+            type_check.expect(x_type.shape[2] == expected_h)
+        if self.outw is not None:
+            expected_w = conv.get_conv_outsize(
+                self.outw, self.kw, self.sx, self.pw, cover_all=self.cover_all)
+            type_check.expect(x_type.shape[3] == expected_w)
+
+    def forward(self, x):
+        h, w = x[0].shape[2:]
+        if self.outh is None:
+            self.outh = conv.get_deconv_outsize(
+                h, self.kh, self.sy, self.ph, cover_all=self.cover_all)
+        if self.outw is None:
+            self.outw = conv.get_deconv_outsize(
+                w, self.kw, self.sx, self.pw, cover_all=self.cover_all)
+        xp = cuda.get_array_module(*x)
+        col = xp.tile(x[0][:, :, xp.newaxis, xp.newaxis],
+                      (1, 1, self.kh, self.kw, 1, 1))
+        if isinstance(x[0], cuda.ndarray):
+            y = conv.col2im_gpu(col, self.sy, self.sx, self.ph, self.pw,
+                                self.outh, self.outw)
+        else:
+            y = conv.col2im_cpu(col, self.sy, self.sx, self.ph, self.pw,
+                                self.outh, self.outw)
+        return y,
+
+    def backward(self, x, gy):
+        if isinstance(gy[0], cuda.ndarray):
+            gcol = conv.im2col_gpu(
+                gy[0], self.kh, self.kw, self.sy, self.sx, self.ph, self.pw,
+                cover_all=self.cover_all)
+        else:
+            gcol = conv.im2col_cpu(
+                gy[0], self.kh, self.kw, self.sy, self.sx, self.ph, self.pw,
+                cover_all=self.cover_all)
+        gx = gcol.sum(axis=(2, 3))
+        return gx,
+
+
+def unpooling_2d(x, ksize, stride=None, pad=0, outsize=None, cover_all=True):
+    """Inverse operation of pooling for 2d array.
+
+    This function acts similarly to :class:`~functions.Deconvolution2D`, but
+    it spreads input 2d array's value without any parameter instead of
+    computing the inner products.
+
+    Args:
+        x (~chainer.Variable): Input variable.
+        ksize (int or pair of ints): Size of pooling window. ``ksize=k`` and
+            ``ksize=(k, k)`` are equivalent.
+        stride (int, pair of ints or None): Stride of pooling applications.
+            ``stride=s`` and ``stride=(s, s)`` are equivalent. If ``None`` is
+            specified, then it uses same stride as the pooling window size.
+        pad (int or pair of ints): Spatial padding width for the input array.
+            ``pad=p`` and ``pad=(p, p)`` are equivalent.
+        outsize (None or pair of ints): Expected output size (height, width)
+            of array after the operation.  If ``None``, the size
+            (height or width) is estimated from the size of input array
+            in first batch with
+            :func:`~chainer.utils.conv.get_deconv_outsize`.
+            If outsize is not ``None``, the result of outsize applied to
+            :func:`~chainer.utils.conv.get_conv_outsize` must be equal to
+            the shape of the 2d array in the input batch ``x``.
+        cover_all (bool): If ``True``, all spatial locations are pooled
+            into some output pixels, and the output size is larger than that
+            when cover_all is ``False``.
+
+    Returns:
+        ~chainer.Variable: Ouptut variable.
+
+    """
+    return Unpooling2D(ksize, stride, pad, outsize, cover_all)(x)
diff --git a/chainer/utils/conv.py b/chainer/utils/conv.py
--- a/chainer/utils/conv.py
+++ b/chainer/utils/conv.py
@@ -11,8 +11,11 @@ def get_conv_outsize(size, k, s, p, cover_all=False):
         return (size + p * 2 - k) // s + 1
 
 
-def get_deconv_outsize(size, k, s, p):
-    return s * (size - 1) + k - 2 * p
+def get_deconv_outsize(size, k, s, p, cover_all=False):
+    if cover_all:
+        return s * (size - 1) + k - s + 1 - 2 * p
+    else:
+        return s * (size - 1) + k - 2 * p
 
 
 def im2col_cpu(img, kh, kw, sy, sx, ph, pw, pval=0, cover_all=False):


Test patch:
diff --git a/tests/chainer_tests/functions_tests/pooling_tests/test_unpooling_2d.py b/tests/chainer_tests/functions_tests/pooling_tests/test_unpooling_2d.py
new file mode 100644
--- /dev/null
+++ b/tests/chainer_tests/functions_tests/pooling_tests/test_unpooling_2d.py
@@ -0,0 +1,101 @@
+import unittest
+
+import numpy
+import six
+
+import chainer
+from chainer import cuda
+from chainer import functions
+from chainer import gradient_check
+from chainer import testing
+from chainer.testing import attr
+from chainer.testing import condition
+
+
+@testing.parameterize(
+    # we assume insize as (2, 1)
+    # standard output size which is estimated with get_deconv_outsize function
+    {'cover_all': False, 'outsize': (4, 2)},
+    {'cover_all': True, 'outsize': (3, 1)},
+    {'cover_all': False, 'outsize': None, 'expected_outsize': (4, 2)},
+    {'cover_all': True, 'outsize': None, 'expected_outsize': (3, 1)},
+    # another sizes which can be outsize of insize (2, 1)
+    {'cover_all': False, 'outsize': (5, 2)},
+    {'cover_all': True, 'outsize': (4, 2)},
+)
+class TestUnpooling2D(unittest.TestCase):
+
+    def setUp(self):
+        self.N = 2
+        self.n_channels = 3
+        inh, inw = 2, 1
+        self.x = numpy.arange(
+            self.N * self.n_channels * inh * inw, dtype=numpy.float32)\
+            .reshape(self.N, self.n_channels, inh, inw)
+        numpy.random.shuffle(self.x)
+        self.x = 2 * self.x / self.x.size - 1
+
+        self.ksize = 2
+        outh, outw = self.outsize or self.expected_outsize
+        self.gy = numpy.random.uniform(
+            -1, 1, (self.N, self.n_channels, outh, outw)).astype(numpy.float32)
+
+    def check_forward(self, x_data):
+        x = chainer.Variable(x_data)
+        y = functions.unpooling_2d(x, self.ksize, outsize=self.outsize,
+                                   cover_all=self.cover_all)
+        self.assertEqual(y.data.dtype, numpy.float32)
+        y_data = cuda.to_cpu(y.data)
+
+        self.assertEqual(self.gy.shape, y_data.shape)
+        for i in six.moves.range(self.N):
+            for c in six.moves.range(self.n_channels):
+                outsize = self.outsize or self.expected_outsize
+                assert y_data.shape[2:] == outsize
+                if outsize == (5, 2):
+                    expect = numpy.zeros(outsize, dtype=numpy.float32)
+                    expect[:2, :] = self.x[i, c, 0, 0]
+                    expect[2:4, :] = self.x[i, c, 1, 0]
+                elif outsize == (4, 2):
+                    expect = numpy.array([
+                        [self.x[i, c, 0, 0], self.x[i, c, 0, 0]],
+                        [self.x[i, c, 0, 0], self.x[i, c, 0, 0]],
+                        [self.x[i, c, 1, 0], self.x[i, c, 1, 0]],
+                        [self.x[i, c, 1, 0], self.x[i, c, 1, 0]],
+                    ])
+                elif outsize == (3, 1):
+                    expect = numpy.array([
+                        [self.x[i, c, 0, 0]],
+                        [self.x[i, c, 0, 0]],
+                        [self.x[i, c, 1, 0]],
+                    ])
+                else:
+                    raise ValueError('Unsupported outsize: {}'.format(outsize))
+                gradient_check.assert_allclose(expect, y_data[i, c])
+
+    @condition.retry(3)
+    def test_forward_cpu(self):
+        self.check_forward(self.x)
+
+    @attr.gpu
+    @condition.retry(3)
+    def test_forward_gpu(self):
+        self.check_forward(cuda.to_gpu(self.x))
+
+    def check_backward(self, x_data, y_grad):
+        gradient_check.check_backward(
+            functions.Unpooling2D(self.ksize, outsize=self.outsize,
+                                  cover_all=self.cover_all),
+            x_data, y_grad)
+
+    @condition.retry(3)
+    def test_backward_cpu(self):
+        self.check_backward(self.x, self.gy)
+
+    @attr.gpu
+    @condition.retry(3)
+    def test_backward_gpu(self):
+        self.check_backward(cuda.to_gpu(self.x), cuda.to_gpu(self.gy))
+
+
+testing.run_module(__name__, __file__)