Repository: chainer/chainer

Issue:
ConvolutionND can't  in_channels = None
`L.Convolution2D` can `input_channels = None` or omit input_channels.
But, `L.ConvolutionND` can't. 
What about making this feature be able to use in `L.ConvolutionND`.


Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs. Thank you for your contributions.

This issue is closed as announced. Feel free to re-open it if needed.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs. Thank you for your contributions.


Resolving patch:
diff --git a/chainer/links/connection/convolution_nd.py b/chainer/links/connection/convolution_nd.py
--- a/chainer/links/connection/convolution_nd.py
+++ b/chainer/links/connection/convolution_nd.py
@@ -19,6 +19,8 @@ class ConvolutionND(link.Link):
     Args:
         ndim (int): Number of spatial dimensions.
         in_channels (int): Number of channels of input arrays.
+            If ``None``, parameter initialization will be deferred until the
+            first forward data pass at which time the size will be determined.
         out_channels (int): Number of channels of output arrays.
         ksize (int or tuple of ints): Size of filters (a.k.a. kernels).
             ``ksize=k`` and ``ksize=(k, k, ..., k)`` are equivalent.
@@ -55,14 +57,62 @@ class ConvolutionND(link.Link):
         b (~chainer.Variable): Bias parameter. If ``initial_bias`` is ``None``,
             set to ``None``.
 
-    """
+    .. admonition:: Example
 
-    def __init__(self, ndim, in_channels, out_channels, ksize, stride=1, pad=0,
-                 nobias=False, initialW=None, initial_bias=None,
+        There are several ways to make a ConvolutionND link.
+
+        Let an input vector ``x`` be:
+
+        >>> x = np.arange(2 * 5 * 5 * 5, dtype='f').reshape(1, 2, 5, 5, 5)
+
+        1. Give the first four arguments explicitly:
+
+            >>> l = L.ConvolutionND(3, 2, 7, 4)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 2, 2, 2)
+
+        2. Omit ``in_channels`` or fill it with ``None``:
+
+            The below two cases are the same.
+
+            >>> l = L.ConvolutionND(3, 7, 4)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 2, 2, 2)
+
+            >>> l = L.ConvolutionND(3, None, 7, 4)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 2, 2, 2)
+
+            When you omit the second argument, you need to specify the other
+            subsequent arguments from ``stride`` as keyword auguments. So the
+            below two cases are the same.
+
+            >>> l = L.ConvolutionND(3, 7, 4, stride=1, pad=0)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 2, 2, 2)
+
+            >>> l = L.ConvolutionND(3, None, 7, 4, 1, 0)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 2, 2, 2)
+
+    """  # NOQA
+
+    def __init__(self, ndim, in_channels, out_channels, ksize=None, stride=1,
+                 pad=0, nobias=False, initialW=None, initial_bias=None,
                  cover_all=False, dilate=1, groups=1):
         super(ConvolutionND, self).__init__()
 
-        ksize = conv_nd.as_tuple(ksize, ndim)
+        if ksize is None:
+            out_channels, ksize, in_channels = \
+                in_channels, out_channels, None
+
+        self.out_channels = out_channels
+        self.ksize = conv_nd.as_tuple(ksize, ndim)
         self.stride = stride
         self.pad = pad
         self.cover_all = cover_all
@@ -70,9 +120,10 @@ def __init__(self, ndim, in_channels, out_channels, ksize, stride=1, pad=0,
         self.groups = int(groups)
 
         with self.init_scope():
-            W_shape = (out_channels, in_channels) + ksize
-            self.W = variable.Parameter(
-                initializers._get_initializer(initialW), W_shape)
+            W_initializer = initializers._get_initializer(initialW)
+            self.W = variable.Parameter(W_initializer)
+            if in_channels is not None:
+                self._initialize_params(in_channels)
 
             if nobias:
                 self.b = None
@@ -82,6 +133,17 @@ def __init__(self, ndim, in_channels, out_channels, ksize, stride=1, pad=0,
                 initial_bias = initializers._get_initializer(initial_bias)
                 self.b = variable.Parameter(initial_bias, out_channels)
 
+    def _initialize_params(self, in_channels):
+        if self.out_channels % self.groups != 0:
+            raise ValueError('the number of output channels must be'
+                             ' divisible by the number of groups')
+        if in_channels % self.groups != 0:
+            raise ValueError('the number of input channels must be'
+                             ' divisible by the number of groups')
+        W_shape = (
+            int(self.out_channels / self.groups), in_channels) + self.ksize
+        self.W.initialize(W_shape)
+
     def forward(self, x):
         """Applies N-dimensional convolution layer.
 
@@ -92,6 +154,8 @@ def forward(self, x):
             ~chainer.Variable: Output of convolution.
 
         """
+        if self.W.data is None:
+            self._initialize_params(x.shape[1])
         return convolution_nd.convolution_nd(
             x, self.W, self.b, self.stride, self.pad, cover_all=self.cover_all,
             dilate=self.dilate, groups=self.groups)
diff --git a/chainer/links/connection/deconvolution_nd.py b/chainer/links/connection/deconvolution_nd.py
--- a/chainer/links/connection/deconvolution_nd.py
+++ b/chainer/links/connection/deconvolution_nd.py
@@ -19,6 +19,8 @@ class DeconvolutionND(link.Link):
     Args:
         ndim (int): Number of spatial dimensions.
         in_channels (int): Number of channels of input arrays.
+            If ``None``, parameter initialization will be deferred until the
+            first forward data pass at which time the size will be determined.
         out_channels (int): Number of channels of output arrays.
         ksize (int or tuple of ints): Size of filters (a.k.a. kernels).
             ``ksize=k`` and ``ksize=(k, k, ..., k)`` are equivalent.
@@ -53,14 +55,62 @@ class DeconvolutionND(link.Link):
         b (~chainer.Variable): Bias parameter. If ``initial_bias`` is ``None``,
             set to ``None``.
 
-    """
+    .. admonition:: Example
 
-    def __init__(self, ndim, in_channels, out_channels, ksize, stride=1, pad=0,
-                 nobias=False, outsize=None, initialW=None, initial_bias=None,
-                 dilate=1, groups=1):
+        There are several ways to make a DeconvolutionND link.
+
+        Let an input vector ``x`` be:
+
+        >>> x = np.arange(2 * 5 * 5 * 5, dtype='f').reshape(1, 2, 5, 5, 5)
+
+        1. Give the first four arguments explicitly:
+
+            >>> l = L.DeconvolutionND(3, 2, 7, 4)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 8, 8, 8)
+
+        2. Omit ``in_channels`` or fill it with ``None``:
+
+            The below two cases are the same.
+
+            >>> l = L.DeconvolutionND(3, 7, 4)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 8, 8, 8)
+
+            >>> l = L.DeconvolutionND(3, None, 7, 4)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 8, 8, 8)
+
+            When you omit the second argument, you need to specify the other
+            subsequent arguments from ``stride`` as keyword auguments. So the
+            below two cases are the same.
+
+            >>> l = L.DeconvolutionND(3, 7, 4, stride=2, pad=1)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 10, 10, 10)
+
+            >>> l = L.DeconvolutionND(3, None, 7, 4, 2, 1)
+            >>> y = l(x)
+            >>> y.shape
+            (1, 7, 10, 10, 10)
+
+    """  # NOQA
+
+    def __init__(self, ndim, in_channels, out_channels, ksize=None, stride=1,
+                 pad=0, nobias=False, outsize=None, initialW=None,
+                 initial_bias=None, dilate=1, groups=1):
         super(DeconvolutionND, self).__init__()
 
-        ksize = conv_nd.as_tuple(ksize, ndim)
+        if ksize is None:
+            out_channels, ksize, in_channels = \
+                in_channels, out_channels, None
+
+        self.out_channels = out_channels
+        self.ksize = conv_nd.as_tuple(ksize, ndim)
         self.stride = stride
         self.pad = pad
         self.outsize = outsize
@@ -69,8 +119,10 @@ def __init__(self, ndim, in_channels, out_channels, ksize, stride=1, pad=0,
 
         with self.init_scope():
             W_initializer = initializers._get_initializer(initialW)
-            self.W = variable.Parameter(W_initializer,
-                                        (in_channels, out_channels) + ksize)
+            self.W = variable.Parameter(W_initializer)
+            if in_channels is not None:
+                self._initialize_params(in_channels)
+
             if nobias:
                 self.b = None
             else:
@@ -79,7 +131,20 @@ def __init__(self, ndim, in_channels, out_channels, ksize, stride=1, pad=0,
                 initial_bias = initializers._get_initializer(initial_bias)
                 self.b = variable.Parameter(initial_bias, out_channels)
 
+    def _initialize_params(self, in_channels):
+        if self.out_channels % self.groups != 0:
+            raise ValueError('the number of output channels must be'
+                             'divisible by the number of groups')
+        if in_channels % self.groups != 0:
+            raise ValueError('the number of input channels must be'
+                             'divisible by the number of groups')
+        W_shape = (
+            in_channels, int(self.out_channels / self.groups)) + self.ksize
+        self.W.initialize(W_shape)
+
     def forward(self, x):
+        if self.W.data is None:
+            self._initialize_params(x.shape[1])
         return deconvolution_nd.deconvolution_nd(
             x, self.W, b=self.b, stride=self.stride, pad=self.pad,
             outsize=self.outsize, dilate=self.dilate, groups=self.groups)


Test patch:
diff --git a/tests/chainer_tests/links_tests/connection_tests/test_convolution_nd.py b/tests/chainer_tests/links_tests/connection_tests/test_convolution_nd.py
--- a/tests/chainer_tests/links_tests/connection_tests/test_convolution_nd.py
+++ b/tests/chainer_tests/links_tests/connection_tests/test_convolution_nd.py
@@ -17,10 +17,12 @@
 
 @testing.parameterize(*(testing.product({
     'dims': [(3, 4), (3, 4, 3)],
-    'dtype': [numpy.float32]
+    'dtype': [numpy.float32],
+    'in_channels': [3, None, 'omit'],
 }) + testing.product({
     'dims': [(5,)],
-    'dtype': [numpy.float16, numpy.float32, numpy.float64]
+    'dtype': [numpy.float16, numpy.float32, numpy.float64],
+    'in_channels': [3, None, 'omit'],
 })))
 class TestConvolutionND(unittest.TestCase):
 
@@ -30,9 +32,16 @@ def setUp(self):
         self.stride = (2,) * ndim
         self.pad = (1,) * ndim
 
-        self.link = convolution_nd.ConvolutionND(
-            ndim, 3, 2, self.ksize, stride=self.stride, pad=self.pad,
-            initial_bias=initializers.Uniform(scale=1., dtype=self.dtype))
+        if self.in_channels == 'omit':
+            self.link = convolution_nd.ConvolutionND(
+                ndim, 2, self.ksize, stride=self.stride,
+                pad=self.pad, initial_bias=initializers.Uniform(
+                    scale=1., dtype=self.dtype))
+        else:
+            self.link = convolution_nd.ConvolutionND(
+                ndim, self.in_channels, 2, self.ksize, stride=self.stride,
+                pad=self.pad, initial_bias=initializers.Uniform(
+                    scale=1., dtype=self.dtype))
         self.link.cleargrads()
 
         x_shape = (2, 3) + self.dims
diff --git a/tests/chainer_tests/links_tests/connection_tests/test_deconvolution_nd.py b/tests/chainer_tests/links_tests/connection_tests/test_deconvolution_nd.py
--- a/tests/chainer_tests/links_tests/connection_tests/test_deconvolution_nd.py
+++ b/tests/chainer_tests/links_tests/connection_tests/test_deconvolution_nd.py
@@ -20,18 +20,19 @@
     'dtype': [numpy.float32],
     'use_cudnn': ['always', 'auto', 'never'],
     'used_outsize': ['case1', 'case2', 'None'],
+    'in_channels': [3, None, 'omit'],
 }) + testing.product({
     'dims': [(4, 3, 2)],
     'nobias': [False],
     'dtype': [numpy.float16, numpy.float32, numpy.float64],
     'use_cudnn': ['always'],
     'used_outsize': ['None'],
+    'in_channels': [3, None, 'omit'],
 }))
 class TestDeconvolutionND(unittest.TestCase):
 
     def setUp(self):
         N = 2
-        in_channels = 3
         out_channels = 2
         ndim = len(self.dims)
         ksize = (3,) * ndim
@@ -60,12 +61,18 @@ def setUp(self):
         else:
             initial_bias = None
 
-        self.link = deconvolution_nd.DeconvolutionND(
-            ndim, in_channels, out_channels, ksize, stride=stride, pad=pad,
-            outsize=outsize, initial_bias=initial_bias, nobias=self.nobias)
+        if self.in_channels == 'omit':
+            self.link = deconvolution_nd.DeconvolutionND(
+                ndim, out_channels, ksize, stride=stride, pad=pad,
+                outsize=outsize, initial_bias=initial_bias, nobias=self.nobias)
+        else:
+            self.link = deconvolution_nd.DeconvolutionND(
+                ndim, self.in_channels, out_channels, ksize, stride=stride,
+                pad=pad, outsize=outsize, initial_bias=initial_bias,
+                nobias=self.nobias)
         self.link.cleargrads()
 
-        x_shape = (N, in_channels) + self.dims
+        x_shape = (N, 3) + self.dims
         self.x = numpy.random.uniform(-1, 1, x_shape).astype(self.dtype)
         gy_shape = (N, out_channels) + outs
         self.gy = numpy.random.uniform(-1, 1, gy_shape).astype(self.dtype)