Repository: chainer/chainer

Issue:
Failed to deep copy a chain object
Here is an example

```
import numpy as np
import chainer

class MyChain(chainer.Chain):
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.param = chainer.Parameter(np.array([0, 0], dtype = np.float32)) 

model = MyChain()
copy = model.copy(mode = 'copy')

print(id(model.param.array) == id(copy.param.array))
```

Tried to deep copy `model`, but the id's of parameter arrays of `model` and `copy` turned out to be the same. It looks like `copy(mode = 'copy')` only deep copy a chain's children links but not its parameters.



Discussion:

Resolving patch:
diff --git a/chainer/link.py b/chainer/link.py
--- a/chainer/link.py
+++ b/chainer/link.py
@@ -942,7 +942,7 @@ def add_link(self, name, link):
     def copy(self, mode='share'):
         # type: (str) -> 'Chain'
 
-        ret = super(Chain, self).copy()  # type: ignore # should be Chain
+        ret = super(Chain, self).copy(mode)  # type: ignore # should be Chain
         ret._children = set(ret._children)  # type: ignore
         d = ret.__dict__  # type: tp.Dict[str, Link]
         for name in ret._children:  # type: ignore


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
@@ -1036,6 +1036,7 @@ def setUp(self):
         #   - l1 (x: uninitialized with shape=(2, 3))
         #   - l2 (x: uninitialized with shape=2)
         # - l3   (x: uninitialized without shape)
+        # - (x: unitialized with shape=2)
 
         self.l1 = chainer.Link()
         with self.l1.init_scope():
@@ -1058,6 +1059,8 @@ def setUp(self):
         with self.c2.init_scope():
             self.c2.c1 = self.c1
             self.c2.l3 = self.l3
+            self.x = chainer.Parameter(shape=2)
+            self.c2.x = self.x
 
     def set_count_parameters(self):
         self.l1.x = CountParameter(self.l1.x)
@@ -1120,10 +1123,17 @@ def test_copy_with_share_mode(self):
         self.l2.x.initializer = initializers.Normal(
             dtype=self.l2.x.initializer.dtype)
         self.l2.x.initialize(self.l2.x.shape)
+        self.x.initializer = initializers.Normal(
+            dtype=self.x.initializer.dtype)
+        self.x.initialize(self.x.shape)
 
         c2 = self.c2.copy(mode='share')
         self.assertIs(c2.name, None)
         self.assertIsInstance(c2._children, set)
+        self.assertTrue(hasattr(c2, 'x'))
+        self.assertIsNot(c2.x, self.x)
+        self.assertIs(c2.x.data, self.x.data)
+
         self.assertTrue(hasattr(c2, 'c1'))
         self.assertEqual(c2.c1.name, 'c1')
         self.assertIsInstance(c2.c1._children, set)
@@ -1155,10 +1165,18 @@ def test_copy_with_copy_mode(self):
         self.l2.x.initializer = initializers.Normal(
             dtype=self.l2.x.initializer.dtype)
         self.l2.x.initialize(self.l2.x.shape)
+        self.x.initializer = initializers.Normal(
+            dtype=self.x.initializer.dtype)
+        self.x.initialize(self.x.shape)
 
         c2 = self.c2.copy(mode='copy')
         self.assertIs(c2.name, None)
         self.assertIsInstance(c2._children, set)
+        self.assertTrue(hasattr(c2, 'x'))
+        self.assertIsNot(c2.x, self.x)
+        self.assertIsNot(c2.x.data, self.x.data)
+        self.assertTrue(numpy.array_equal(c2.x.data, self.x.data))
+
         self.assertTrue(hasattr(c2, 'c1'))
         self.assertEqual(c2.c1.name, 'c1')
         self.assertIsInstance(c2.c1._children, set)
@@ -1194,10 +1212,21 @@ def test_copy_with_init_mode(self):
         self.l2.x.initializer = initializers.Normal(
             dtype=self.l2.x.initializer.dtype)
         self.l2.x.initialize(self.l2.x.shape)
+        self.x.initializer = initializers.Normal(
+            dtype=self.x.initializer.dtype)
+        self.c2.x.initialize(self.x.shape)
 
         c2 = self.c2.copy(mode='init')
         self.assertIs(c2.name, None)
         self.assertIsInstance(c2._children, set)
+        self.assertTrue(hasattr(c2, 'x'))
+        self.assertIsNot(c2.x, self.x)
+        self.assertIsNot(c2.x.data, self.x.data)
+        self.assertFalse(numpy.array_equal(c2.x.data, self.x.data))
+        # _grad_initializer attribute in a copied Parameter has constant.NaN
+        # after calling initilize() method
+        self.assertTrue(numpy.isnan(c2.x.grad).all())
+
         self.assertTrue(hasattr(c2, 'c1'))
         self.assertEqual(c2.c1.name, 'c1')
         self.assertIsInstance(c2.c1._children, set)
@@ -1311,24 +1340,27 @@ def test_to_device(self):
     def test_params(self):
         params = list(self.c2.params())
         self.assertEqual([id(p) for p in params],
-                         [id(self.l1.x), id(self.l2.x), id(self.l3.x)])
+                         [id(self.x), id(self.l1.x),
+                          id(self.l2.x), id(self.l3.x)])
 
     def test_params_skip_uninit(self):
         params = list(self.c2.params(include_uninit=False))
         self.assertEqual([id(p) for p in params],
-                         [id(self.l1.x), id(self.l2.x)])
+                         [id(self.x), id(self.l1.x), id(self.l2.x)])
 
     def test_namedparams(self):
         namedparams = list(self.c2.namedparams())
         self.assertEqual([(name, id(p)) for name, p in namedparams],
-                         [('/c1/l1/x', id(self.l1.x)),
+                         [('/x', id(self.x)),
+                          ('/c1/l1/x', id(self.l1.x)),
                           ('/c1/l2/x', id(self.l2.x)),
                           ('/l3/x', id(self.l3.x))])
 
     def test_namedparams_skip_uninit(self):
         namedparams = list(self.c2.namedparams(include_uninit=False))
         self.assertEqual([(name, id(p)) for name, p in namedparams],
-                         [('/c1/l1/x', id(self.l1.x)),
+                         [('/x', id(self.x)),
+                          ('/c1/l1/x', id(self.l1.x)),
                           ('/c1/l2/x', id(self.l2.x))])
 
     def test_links(self):
@@ -1383,15 +1415,18 @@ def test_copyparams(self):
         with c2.init_scope():
             c2.c1 = c1
             c2.l3 = l3
+            c2.x = chainer.Parameter(shape=2)
         l1.x.data.fill(0)
         l2.x.data.fill(1)
         l3.x.data.fill(2)
+        c2.x.data.fill(3)
 
         self.c2.copyparams(c2)
 
         numpy.testing.assert_array_equal(self.l1.x.data, l1.x.data)
         numpy.testing.assert_array_equal(self.l2.x.data, l2.x.data)
         numpy.testing.assert_array_equal(self.l3.x.data, l3.x.data)
+        numpy.testing.assert_array_equal(self.c2.x.data, c2.x.data)
 
     def test_zerograds(self):
         self.set_count_parameters()
@@ -1424,18 +1459,22 @@ def test_addgrads(self):
         with c2.init_scope():
             c2.c1 = c1
             c2.l3 = l3
+            c2.x = chainer.Parameter(shape=2)
         l1.x.grad.fill(1)
         l2.x.grad.fill(2)
         l3.x.grad.fill(3)
+        c2.x.grad.fill(4)
 
         self.l1.x.grad.fill(-1)
         self.l2.x.grad.fill(-2)
+        self.c2.x.grad.fill(-3)
         self.l3.cleargrads()
 
         self.c2.addgrads(c2)
         numpy.testing.assert_array_equal(self.l1.x.grad, numpy.zeros((2, 3)))
         numpy.testing.assert_array_equal(self.l2.x.grad, numpy.zeros(2))
         numpy.testing.assert_array_equal(self.l3.x.grad, numpy.full(3, 3.))
+        numpy.testing.assert_array_equal(self.c2.x.grad, numpy.ones(2))
 
     def test_serialize(self):
         mocks = {'l1': mock.MagicMock(), 'l2': mock.MagicMock()}