Repository: chainer/chainer

Issue:
Partial deserialization of links
We sometimes want to deserialize a link only partially, i.e. we want to load a smaller set of parameters to a larger link. It happens when we do some surgery to neural nets (e.g. fine-tuning), in which case the set of parameters in the source network does not match to the set of parameters in the target network.

One possible design is to introduce a `strict` option to the deserializer, and if that flag is False, any mismatch of the sets of parameters is ignored. I think it is safer to turn on this flag by default (I mean, we should not change the default behavior).


Discussion:
There are two types of mismatch.
- Some parameters in a model are not found in serialized data
- Some redundant parameters in serialized data are not required for a model

Is it OK to ignore both situation with `strict=False` option?
And I think we can implement this option before v2 because we can keep compatibility.

Resolving patch:
diff --git a/chainer/serializers/hdf5.py b/chainer/serializers/hdf5.py
--- a/chainer/serializers/hdf5.py
+++ b/chainer/serializers/hdf5.py
@@ -82,18 +82,26 @@ class HDF5Deserializer(serializer.Deserializer):
 
     Args:
         group (h5py.Group): The group that the deserialization starts from.
+        strict (bool): If ``True``, the deserializer raises an error when an
+            expected value is not found in the given HDF5 file. Otherwise,
+            it ignores the value and skip deserialization.
 
     """
 
-    def __init__(self, group):
+    def __init__(self, group, strict=True):
         _check_available()
         self.group = group
+        self.strict = strict
 
     def __getitem__(self, key):
         name = self.group.name + '/' + key
         return HDF5Deserializer(self.group.require_group(name))
 
     def __call__(self, key, value):
+        if not self.strict and key not in self.group:
+            return value
+
+        self.group.keys
         dataset = self.group[key]
         if value is None:
             return numpy.asarray(dataset)
diff --git a/chainer/serializers/npz.py b/chainer/serializers/npz.py
--- a/chainer/serializers/npz.py
+++ b/chainer/serializers/npz.py
@@ -82,20 +82,27 @@ class NpzDeserializer(serializer.Deserializer):
     Args:
         npz: `npz` file object.
         path: The base path that the deserialization starts from.
+        strict (bool): If ``True``, the deserializer raises an error when an
+            expected value is not found in the given NPZ file. Otherwise,
+            it ignores the value and skip deserialization.
 
     """
 
-    def __init__(self, npz, path=''):
+    def __init__(self, npz, path='', strict=True):
         self.npz = npz
         self.path = path
+        self.strict = strict
 
     def __getitem__(self, key):
         key = key.strip('/')
         return NpzDeserializer(self.npz, self.path + key + '/')
 
     def __call__(self, key, value):
-        key = key.lstrip('/')
-        dataset = self.npz[self.path + key]
+        key = self.path + key.lstrip('/')
+        if not self.strict and key not in self.npz:
+            return value
+
+        dataset = self.npz[key]
         if value is None:
             return dataset
         elif isinstance(value, numpy.ndarray):


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
@@ -144,6 +144,31 @@ def test_deserialize_scalar(self):
         self.assertEqual(ret, 10)
 
 
+@unittest.skipUnless(hdf5._available, 'h5py is not available')
+class TestHDF5DeserializerNonStrict(unittest.TestCase):
+
+    def setUp(self):
+        fd, path = tempfile.mkstemp()
+        os.close(fd)
+        self.temp_file_path = path
+        with h5py.File(path, 'w') as f:
+            f.require_group('x')
+
+        self.hdf5file = h5py.File(path, 'r')
+        self.deserializer = hdf5.HDF5Deserializer(self.hdf5file, strict=False)
+
+    def tearDown(self):
+        if hasattr(self, 'hdf5file'):
+            self.hdf5file.close()
+        if hasattr(self, 'temp_file_path'):
+            os.remove(self.temp_file_path)
+
+    def test_deserialize_partial(self):
+        y = numpy.empty((2, 3), dtype=numpy.float32)
+        ret = self.deserializer('y', y)
+        self.assertIs(ret, y)
+
+
 @unittest.skipUnless(hdf5._available, 'h5py is not available')
 class TestSaveHDF5(unittest.TestCase):
 
diff --git a/tests/chainer_tests/serializers_tests/test_npz.py b/tests/chainer_tests/serializers_tests/test_npz.py
--- a/tests/chainer_tests/serializers_tests/test_npz.py
+++ b/tests/chainer_tests/serializers_tests/test_npz.py
@@ -143,6 +143,31 @@ def test_deserialize_scalar(self):
         self.assertEqual(ret, 10)
 
 
+class TestNpzDeserializerNonStrict(unittest.TestCase):
+
+    def setUp(self):
+        fd, path = tempfile.mkstemp()
+        os.close(fd)
+        self.temp_file_path = path
+        with open(path, 'wb') as f:
+            numpy.savez(
+                f, **{'x': numpy.asarray(10)})
+
+        self.npzfile = numpy.load(path)
+        self.deserializer = npz.NpzDeserializer(self.npzfile, strict=False)
+
+    def tearDown(self):
+        if hasattr(self, 'npzfile'):
+            self.npzfile.close()
+        if hasattr(self, 'temp_file_path'):
+            os.remove(self.temp_file_path)
+
+    def test_deserialize_partial(self):
+        y = numpy.empty((2, 3), dtype=numpy.float32)
+        ret = self.deserializer('y', y)
+        self.assertIs(ret, y)
+
+
 @testing.parameterize(*testing.product({'compress': [False, True]}))
 class TestSaveNpz(unittest.TestCase):