Repository: chainer/chainer

Issue:
Adding parameter via Link's constructor does not accept its dtype.
Relating to #1295 .

While Link's `add_param` method has `dtype` keyword parameter, adding parameter via its constructor's keyword parameter dictionary does not support `dtype` option, ending up with making parameter with `dtype` of FP32.

http://docs.chainer.org/en/stable/reference/core/link.html#chainer.Link

This prevents us from adding parameters via Link's constructor when their expected `dtype` are not FP32, the following weight initialization does not work:

``` Python
W_shape = (in_channels, out_channels) + ksize
super(DeconvolutionND, self).__init__(W=W_shape)
initializers.init_weight(self.W.data, initialW)    # initialW.dtype is other than numpy.float32, raising an exception
```

To solve this, we would be able to extend the protocol of Link constructor's keyword parameter as following:

```
<parameter> := <name>=<shape>
             | <name>=(<shape>, <dtype>)
```

With this, we can write the previous code part as:

``` Python
W_shape = (in_channels, out_channels) + ksize
super(DeconvolutionND, self).__init__(W=(W_shape, W_dtype))    # W_dtype is other than numpy.float32
initializers.init_weight(self.W.data, initialW)                # actually initialW.dtype is not needed
```

This solution is based on that initializers are not responsible of converting parameter data types, so data types need to be determined when they are added by `add_param`.



Discussion:

Resolving patch:
diff --git a/chainer/link.py b/chainer/link.py
--- a/chainer/link.py
+++ b/chainer/link.py
@@ -1,3 +1,4 @@
+import collections
 import copy
 import warnings
 
@@ -8,6 +9,26 @@
 from chainer import variable
 
 
+def _is_shape(value):
+    if value is None:
+        return True
+    elif isinstance(value, six.integer_types):
+        return True
+    elif isinstance(value, collections.Sequence):
+        return all(isinstance(x, six.integer_types) for x in value)
+    else:
+        return False
+
+
+def _ensure_shape_dtype(value):
+    # Return value paired with dtype FP32 if it is a shape.
+    if _is_shape(value):
+        return (value, 'f')
+    # Otherwise, returns it with assuming a shape-dtype pair.
+    else:
+        return value
+
+
 class Link(object):
 
     """Building block of model definitions.
@@ -82,8 +103,11 @@ def __call__(self, x):
        operator.
 
     Args:
-        params: Shapes of initial parameters. The keywords are used as their
-            names. The names are also set to the parameter variables.
+        params: Names, shapes, and optional dtypes of initial parameters. The
+            keywords are used as the parameter names and the corresponding
+            values consist either of the shape or a tuple of shape and a dtype
+            `(shape, dtype)`. If only the shape is supplied, the default dtype
+            will be used.
 
     Attributes:
         name (str): Name of this link, given by the parent chain (if exists).
@@ -97,8 +121,9 @@ def __init__(self, **params):
         self._cpu = True
         self.name = None
 
-        for name, shape in six.iteritems(params):
-            self.add_param(name, shape)
+        for name, value in six.iteritems(params):
+            shape, dtype = _ensure_shape_dtype(value)
+            self.add_param(name, shape, dtype=dtype)
 
     @property
     def xp(self):


Test patch:
diff --git a/tests/chainer_tests/test_link.py b/tests/chainer_tests/test_link.py
--- a/tests/chainer_tests/test_link.py
+++ b/tests/chainer_tests/test_link.py
@@ -12,7 +12,7 @@
 class TestLink(unittest.TestCase):
 
     def setUp(self):
-        self.link = chainer.Link(x=(2, 3), y=2)
+        self.link = chainer.Link(x=((2, 3), 'd'), y=2)
         self.p = numpy.array([1, 2, 3], dtype='f')
         self.link.add_persistent('p', self.p)
         self.link.name = 'a'
@@ -30,7 +30,7 @@ def check_param_init(self, name, shape, dtype):
         self.assertTrue(numpy.all(numpy.isnan(var.grad)))
 
     def test_init(self):
-        self.check_param_init('x', (2, 3), 'f')
+        self.check_param_init('x', (2, 3), 'd')
         self.check_param_init('y', (2,), 'f')
 
     def test_add_param(self):