Repository: chainer/chainer

Issue:
Optimizer's states are serialized outside user-specified group
When user manually creates `HDF5Serializer` with user-specified group and saves an instance of `optimizer`, group hierarchy is created outside the group. See the code below for details.

```
In [2]: import chainer
In [3]: from chainer.link import Chain
In [4]: from chainer.links import Linear
In [5]: from chainer.optimizers import AdaDelta
In [6]: from chainer.serializers import HDF5Serializer
In [7]: import h5py
In [8]: chain = Chain(linear=Linear(100, 10))
In [9]: opt = AdaDelta()
In [10]: opt.setup(chain)
In [11]: h5 = h5py.File("test.h5")
In [12]: ls
test.h5
In [13]: h5.keys()
Out[13]: []
In [14]: group = h5.create_group("test")
In [15]: s = HDF5Serializer(group)
In [16]: s.save(opt)
In [17]: h5.keys()
Out[17]: [u'linear', u'test']  # 'linear' is supposed to create under 'test'
In [18]: h5['test'].keys()
Out[18]: [u'epoch', u't']
In [19]: h5['linear'].keys()
Out[19]: [u'W', u'b']
```

This is reported by Junya Norimatsu. Thank you!



Discussion:

Resolving patch:
diff --git a/chainer/serializers/hdf5.py b/chainer/serializers/hdf5.py
--- a/chainer/serializers/hdf5.py
+++ b/chainer/serializers/hdf5.py
@@ -30,7 +30,8 @@ def __init__(self, group, compression=4):
         self.compression = compression
 
     def __getitem__(self, key):
-        return HDF5Serializer(self.group.require_group(key), self.compression)
+        name = self.group.name + '/' + key
+        return HDF5Serializer(self.group.require_group(name), self.compression)
 
     def __call__(self, key, value):
         ret = value


Test patch:
diff --git a/tests/chainer_tests/serializers_tests/test_hdf5.py b/tests/chainer_tests/serializers_tests/test_hdf5.py
--- a/tests/chainer_tests/serializers_tests/test_hdf5.py
+++ b/tests/chainer_tests/serializers_tests/test_hdf5.py
@@ -7,6 +7,9 @@
 import numpy
 
 from chainer import cuda
+from chainer import link
+from chainer import links
+from chainer import optimizers
 from chainer.serializers import hdf5
 from chainer import testing
 from chainer.testing import attr
@@ -165,4 +168,59 @@ def test_load(self):
         self.assertIsInstance(serializer, hdf5.HDF5Deserializer)
 
 
+class TestGroupHierachy(unittest.TestCase):
+
+    def setUp(self):
+        fd, path = tempfile.mkstemp()
+        os.close(fd)
+        self.temp_file_path = path
+
+        child = link.Chain(linear=links.Linear(2, 3))
+        child.add_param('Wc', (2, 3))
+        self.parent = link.Chain(child=child)
+        self.parent.add_param('Wp', (2, 3))
+
+        self.optimizer = optimizers.AdaDelta()
+        self.optimizer.setup(self.parent)
+
+    def _save(self, h5, obj, name):
+        group = h5.create_group(name)
+        serializer = hdf5.HDF5Serializer(group)
+        serializer.save(obj)
+
+    def tearDown(self):
+        if hasattr(self, 'temp_file_path'):
+            os.remove(self.temp_file_path)
+
+    def _check_group(self, h5, state):
+        self.assertSetEqual(set(h5.keys()),
+                            set(('child',) + state))
+        self.assertSetEqual(set(h5['child'].keys()),
+                            set(('linear', 'Wc')))
+        self.assertSetEqual(set(h5['child']['linear'].keys()),
+                            set(('W', 'b')))
+
+    def test_save_chain(self):
+        with h5py.File(self.temp_file_path) as h5:
+            self._save(h5, self.parent, 'test')
+            self.assertSetEqual(set(h5.keys()), set(('test',)))
+            self._check_group(h5['test'], ('Wp',))
+
+    def test_save_optimizer(self):
+        with h5py.File(self.temp_file_path) as h5:
+            self._save(h5, self.optimizer, 'test')
+            self.assertSetEqual(set(h5.keys()), set(('test',)))
+            self._check_group(h5['test'], ('Wp', 'epoch', 't'))
+
+    def test_save_chain2(self):
+        hdf5.save_hdf5(self.temp_file_path, self.parent)
+        with h5py.File(self.temp_file_path) as h5:
+            self._check_group(h5, ('Wp', ))
+
+    def test_save_optimizer2(self):
+        hdf5.save_hdf5(self.temp_file_path, self.optimizer)
+        with h5py.File(self.temp_file_path) as h5:
+            self._check_group(h5, ('Wp', 'epoch', 't'))
+
+
 testing.run_module(__name__, __file__)