Repository: chainer/chainer

Issue:
About the unordered behavior of `params`, `namedparams` method.
For `Link` class, the implementation of `params` and `namedparams` methods depends on the `self._params` attribute. Similarly, For `Chain` class, the implementation of `params` and `namedparams` also depends on the `self._children` attribute. The problem is that both `_params` and `_children` is type `set`, which causes the unordered behavior when calling `params` or `namedparams` methods.

This problem can be inconvenient when we want to use an exact copy (deepcopy) of a model, expecially when we expect the `params` or `namedparams` are well aligned between the original model and the duplicate version (using copy.deepcopy).

Suppose we have a model composed of three convolutional layers: conv1, conv2, conv3.
```
for name, param in original_model.namedparams():
    print(name)
```
Above code gives us the results:
```
/conv1/W
/conv1/b
/conv2/W
/conv2/b
/conv3/W
/conv3/b
```

However, if we use `copied_model = copy.deepcopy(original_model)`, the results can happen to be:
```
/conv1/W
/conv1/b
/conv3/W
/conv3/b
/conv2/W
/conv2/b
```

For this problem, first, I'm wondering why a `set` is used for storing `_params` and `_children` attribute (maybe for avoiding the duplicate params/child links?). Second, is it possible to change the current code to make the `_params` and `_children` have a definite order? Finally, even if such change is not proper, I think it is necessary to add some note in the documentation to warn users about this kind of behavior.

At least for myself, I was expecting the `params` and `namedparams` to return a fixed order of parameters before reading details in the source code...





Discussion:
`self._params` etc. are defined as `set` just to make implementation simple  (https://github.com/chainer/chainer/pull/2657). As the performance does not matter here, I think it can be replaced it with some ordered data structure.

I want to understand the context of this feature request. Could you tell me why you need params to be ordered in your usecase? (e.g. `zip`-ing and comparing params from two models?)
Hi @kmaehashi, thanks for the reply. 

I use it to add constraints between the parameters of two models (I use `zip` here).
ChainerRL iterates over zipped namedvalues by https://github.com/chainer/chainerrl/blob/c5483009ccf5875e02d2c73592c975ad97680274/chainerrl/misc/copy_param.py#L12-L30

`zip(sorted(link1.namedparams()), sorted(link2.namedparams()))` also works fine if you know the sets of the names are equal.
After the discussion in the team, we decided to fix `namedparams` and `param` in sorted (by name) order.

Resolving patch:
diff --git a/chainer/link.py b/chainer/link.py
--- a/chainer/link.py
+++ b/chainer/link.py
@@ -442,7 +442,7 @@ def params(self, include_uninit=True):
 
         """
         d = self.__dict__
-        for name in self._params:
+        for name in sorted(self._params):
             if include_uninit or d[name].data is not None:
                 yield d[name]
 
@@ -459,7 +459,7 @@ def namedparams(self, include_uninit=True):
 
         """
         d = self.__dict__
-        for name in self._params:
+        for name in sorted(self._params):
             if include_uninit or d[name].data is not None:
                 yield '/' + name, d[name]
 
@@ -906,7 +906,7 @@ def params(self, include_uninit=True):
         for param in super(Chain, self).params(include_uninit):
             yield param
         d = self.__dict__
-        for name in self._children:
+        for name in sorted(self._children):
             for param in d[name].params(include_uninit):
                 yield param
 
@@ -914,7 +914,7 @@ def namedparams(self, include_uninit=True):
         for ret in super(Chain, self).namedparams(include_uninit):
             yield ret
         d = self.__dict__
-        for name in self._children:
+        for name in sorted(self._children):
             prefix = '/' + name
             for path, param in d[name].namedparams(include_uninit):
                 yield prefix + path, param
@@ -923,7 +923,7 @@ def links(self, skipself=False):
         if not skipself:
             yield self
         d = self.__dict__
-        for name in self._children:
+        for name in sorted(self._children):
             for link in d[name].links():
                 yield link
 
@@ -931,7 +931,7 @@ def namedlinks(self, skipself=False):
         if not skipself:
             yield '/', self
         d = self.__dict__
-        for name in self._children:
+        for name in sorted(self._children):
             child = d[name]
             prefix = '/' + name
             yield prefix, child
@@ -940,7 +940,7 @@ def namedlinks(self, skipself=False):
 
     def children(self):
         d = self.__dict__
-        for name in self._children:
+        for name in sorted(self._children):
             yield d[name]
 
     def copyparams(self, link, copy_persistent=True):


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
@@ -327,25 +327,25 @@ def test_to_gpu_current_device(self):
 
     def test_params(self):
         params = list(self.link.params())
-        self.assertEqual({id(p) for p in params},
-                         {id(self.link.x), id(self.link.y),
-                          id(self.link.u), id(self.link.v)})
+        self.assertEqual([id(p) for p in params],
+                         [id(self.link.u), id(self.link.v),
+                          id(self.link.x), id(self.link.y)])
 
     def test_params_skip_uninit(self):
         params = list(self.link.params(include_uninit=False))
-        self.assertEqual({id(p) for p in params},
-                         {id(self.link.x), id(self.link.y)})
+        self.assertEqual([id(p) for p in params],
+                         [id(self.link.x), id(self.link.y)])
 
     def test_namedparams(self):
         namedparams = list(self.link.namedparams())
-        self.assertEqual({(name, id(p)) for name, p in namedparams},
-                         {('/x', id(self.link.x)), ('/y', id(self.link.y)),
-                          ('/u', id(self.link.u)), ('/v', id(self.link.v))})
+        self.assertEqual([(name, id(p)) for name, p in namedparams],
+                         [('/u', id(self.link.u)), ('/v', id(self.link.v)),
+                          ('/x', id(self.link.x)), ('/y', id(self.link.y))])
 
     def test_namedparams_skip_uninit(self):
         namedparams = list(self.link.namedparams(include_uninit=False))
-        self.assertEqual({(name, id(p)) for name, p in namedparams},
-                         {('/x', id(self.link.x)), ('/y', id(self.link.y))})
+        self.assertEqual([(name, id(p)) for name, p in namedparams],
+                         [('/x', id(self.link.x)), ('/y', id(self.link.y))])
 
     def test_links(self):
         links = list(self.link.links())
@@ -878,58 +878,60 @@ def test_to_gpu(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)})
+        self.assertEqual([id(p) for p in params],
+                         [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)})
+        self.assertEqual([id(p) for p in params],
+                         [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)),
+        self.assertEqual([(name, id(p)) for name, p in namedparams],
+                         [('/c1/l1/x', id(self.l1.x)),
                           ('/c1/l2/x', id(self.l2.x)),
-                          ('/l3/x', id(self.l3.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)),
-                          ('/c1/l2/x', id(self.l2.x))})
+        self.assertEqual([(name, id(p)) for name, p in namedparams],
+                         [('/c1/l1/x', id(self.l1.x)),
+                          ('/c1/l2/x', id(self.l2.x))])
 
     def test_links(self):
         links = list(self.c2.links())
-        self.assertEqual({id(l) for l in links},
-                         {id(l) for l in [self.l1, self.l2, self.l3,
-                                          self.c1, self.c2]})
+        self.assertEqual([id(l) for l in links],
+                         [id(l) for l in [self.c2,
+                                          self.c1, self.l1, self.l2,
+                                          self.l3]])
 
     def test_links_skipself(self):
         links = list(self.c2.links(skipself=True))
-        self.assertEqual({id(l) for l in links},
-                         {id(l) for l in [self.l1, self.l2, self.l3, self.c1]})
+        self.assertEqual([id(l) for l in links],
+                         [id(l) for l in [self.c1, self.l1, self.l2,
+                                          self.l3]])
 
     def test_namedlinks(self):
         namedlinks = list(self.c2.namedlinks())
-        self.assertEqual({(name, id(l)) for name, l in namedlinks},
-                         {('/', id(self.c2)),
+        self.assertEqual([(name, id(l)) for name, l in namedlinks],
+                         [('/', id(self.c2)),
                           ('/c1', id(self.c1)),
                           ('/c1/l1', id(self.l1)),
                           ('/c1/l2', id(self.l2)),
-                          ('/l3', id(self.l3))})
+                          ('/l3', id(self.l3))])
 
     def test_namedlinks_skipself(self):
         namedlinks = list(self.c2.namedlinks(skipself=True))
-        self.assertEqual({(name, id(l)) for name, l in namedlinks},
-                         {('/c1', id(self.c1)),
+        self.assertEqual([(name, id(l)) for name, l in namedlinks],
+                         [('/c1', id(self.c1)),
                           ('/c1/l1', id(self.l1)),
                           ('/c1/l2', id(self.l2)),
-                          ('/l3', id(self.l3))})
+                          ('/l3', id(self.l3))])
 
     def test_children(self):
         children = list(self.c2.children())
-        self.assertEqual({id(c) for c in children}, {id(self.c1), id(self.l3)})
+        self.assertEqual([id(c) for c in children], [id(self.c1), id(self.l3)])
 
     def test_copyparams(self):
         l1 = chainer.Link()
@@ -1400,57 +1402,59 @@ def test_to_gpu(self):
 
     def test_params(self):
         params = list(self.c2.params())
-        self.assertEqual({id(p) for p in params},
-                         {id(self.l1.x), id(self.l1.y),
-                          id(self.l2.x), id(self.l3.x)})
+        self.assertEqual([id(p) for p in params],
+                         [id(self.l1.x), id(self.l1.y),
+                          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.l3.x)})
+        self.assertEqual([id(p) for p in params],
+                         [id(self.l1.x), id(self.l2.x), id(self.l3.x)])
 
     def test_namedparams(self):
         namedparams = list(self.c2.namedparams())
-        self.assertEqual({(name, id(p)) for name, p in namedparams},
-                         {('/0/0/x', id(self.l1.x)),
+        self.assertEqual([(name, id(p)) for name, p in namedparams],
+                         [('/0/0/x', id(self.l1.x)),
                           ('/0/0/y', id(self.l1.y)),
                           ('/0/1/x', id(self.l2.x)),
-                          ('/1/x', id(self.l3.x))})
+                          ('/1/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},
-                         {('/0/0/x', id(self.l1.x)),
+        self.assertEqual([(name, id(p)) for name, p in namedparams],
+                         [('/0/0/x', id(self.l1.x)),
                           ('/0/1/x', id(self.l2.x)),
-                          ('/1/x', id(self.l3.x))})
+                          ('/1/x', id(self.l3.x))])
 
     def test_links(self):
         links = list(self.c2.links())
-        self.assertEqual({id(l) for l in links},
-                         {id(l) for l in [self.l1, self.l2, self.l3,
-                                          self.c1, self.c2]})
+        self.assertEqual([id(l) for l in links],
+                         [id(l) for l in [self.c2,
+                                          self.c1, self.l1, self.l2,
+                                          self.l3]])
 
     def test_links_skipself(self):
         links = list(self.c2.links(skipself=True))
-        self.assertEqual({id(l) for l in links},
-                         {id(l) for l in [self.l1, self.l2, self.l3, self.c1]})
+        self.assertEqual([id(l) for l in links],
+                         [id(l) for l in [self.c1, self.l1, self.l2,
+                                          self.l3]])
 
     def test_namedlinks(self):
         namedlinks = list(self.c2.namedlinks())
-        self.assertEqual({(name, id(l)) for name, l in namedlinks},
-                         {('/', id(self.c2)),
+        self.assertEqual([(name, id(l)) for name, l in namedlinks],
+                         [('/', id(self.c2)),
                           ('/0', id(self.c1)),
                           ('/0/0', id(self.l1)),
                           ('/0/1', id(self.l2)),
-                          ('/1', id(self.l3))})
+                          ('/1', id(self.l3))])
 
     def test_namedlinks_skipself(self):
         namedlinks = list(self.c2.namedlinks(skipself=True))
-        self.assertEqual({(name, id(l)) for name, l in namedlinks},
-                         {('/0', id(self.c1)),
+        self.assertEqual([(name, id(l)) for name, l in namedlinks],
+                         [('/0', id(self.c1)),
                           ('/0/0', id(self.l1)),
                           ('/0/1', id(self.l2)),
-                          ('/1', id(self.l3))})
+                          ('/1', id(self.l3))])
 
     def test_children(self):
         self.assertEqual(tuple(id(c) for c in self.c2.children()),