Repository: chainer/chainer

Issue:
[chainer.exporters.caffe] Export eps param of BatchNormalization 
I met a problem for exporting caffe model.
exporters.caffe don't export eps param of BatchNormalization. 
Chainer sets eps param to 2e-5 by default. however, Caffe sets eps param to 1e-5 by default.
The output value becomes very different in the regression task. 


Discussion:

Resolving patch:
diff --git a/chainer/exporters/caffe.py b/chainer/exporters/caffe.py
--- a/chainer/exporters/caffe.py
+++ b/chainer/exporters/caffe.py
@@ -239,7 +239,7 @@ def dump_function_object(self, func, prototxt, net):
 
         elif func.label == 'FixedBatchNormalization':
             _, gamma, beta, mean, var = func.inputs
-            batch_norm_param = {'use_global_stats': True}
+            batch_norm_param = {'use_global_stats': True, 'eps': func.eps}
             params['type'] = 'BatchNorm'
             params['bottom'] = params['bottom'][:1]
             params['batch_norm_param'] = batch_norm_param


Test patch:
diff --git a/tests/chainer_tests/exporters_tests/test_caffe.py b/tests/chainer_tests/exporters_tests/test_caffe.py
--- a/tests/chainer_tests/exporters_tests/test_caffe.py
+++ b/tests/chainer_tests/exporters_tests/test_caffe.py
@@ -1,3 +1,4 @@
+import os
 import unittest
 
 import numpy
@@ -23,17 +24,17 @@
 class TestCaffeExport(unittest.TestCase):
 
     def setUp(self):
-        x = numpy.ones((1, 3, 7, 7)).astype(numpy.float32)
+        x = numpy.random.uniform(-1, 1, (1, 3, 7, 7)).astype(numpy.float32)
         self.x = chainer.Variable(x)
 
-    def test_caffe_export_no_save(self):
+    def test_caffe_export_model(self):
         class Model(chainer.Chain):
 
             def __init__(self):
                 super(Model, self).__init__()
                 with self.init_scope():
                     self.l1 = L.Convolution2D(None, 1, 1, 1, 0)
-                    self.b2 = L.BatchNormalization(1)
+                    self.b2 = L.BatchNormalization(1, eps=1e-2)
                     self.l3 = L.Linear(None, 1)
 
             def __call__(self, x):
@@ -41,43 +42,82 @@ def __call__(self, x):
                 h = self.b2(h)
                 return self.l3(h)
 
-        caffe.export(Model(), [self.x], None, True, 'test')
+        assert_export_import_match(Model(), self.x)
 
     def test_Reshape(self):
         class Link(chainer.Chain):
-
             def __call__(self, x):
                 return F.reshape(x, (-1,))
 
-        caffe.export(Link(), [self.x], None, True, 'test')
+        assert_export_import_match(Link(), self.x)
+
+    def test_LinearFunction(self):
+        W = numpy.random.uniform(-1, 1, (1, numpy.prod(self.x.shape[1:])))
+
+        class Link(chainer.Chain):
+            def __call__(self, x):
+                return F.linear(x, W)
+
+        assert_export_import_match(Link(), self.x)
 
     def test_AveragePooling2D(self):
         class Link(chainer.Chain):
             def __call__(self, x):
                 return F.average_pooling_2d(x, 1, 1, 0)
 
-        caffe.export(Link(), [self.x], None, True, 'test')
+        assert_export_import_match(Link(), self.x)
 
     def test_MaxPooling2D(self):
         class Link(chainer.Chain):
             def __call__(self, x):
                 return F.max_pooling_2d(x, 1, 1, 0)
 
-        caffe.export(Link(), [self.x], None, True, 'test')
+        assert_export_import_match(Link(), self.x)
 
     def test_Softmax(self):
         class Link(chainer.Chain):
             def __call__(self, x):
                 return F.softmax(x)
 
-        caffe.export(Link(), [self.x], None, True, 'test')
+        assert_export_import_match(Link(), self.x)
 
     def test_Add(self):
         class Link(chainer.Chain):
             def __call__(self, x):
                 return x + x
 
-        caffe.export(Link(), [self.x], None, True, 'test')
+        assert_export_import_match(Link(), self.x)
+
+
+def assert_export_import_match(l1, x):
+    """Asserts that results from original Link and re-imported Link are close.
+
+    """
+
+    l2 = export_and_import(l1, (x,), True)
+    inputs = {'data': x}
+    outputs = [l2.layers[-1][0]]
+    with chainer.using_config('train', False):
+        for v1, v2 in zip(l1(x), l2(inputs=inputs, outputs=outputs)[0]):
+            testing.assert_allclose(v1.data, v2.data)
+
+
+def export_and_import(l, args, export_params=True, graph_name='test'):
+    """Exports the given Link as Caffe model and returns the re-imported Link.
+
+    """
+
+    with chainer.utils.tempdir() as tempdir:
+        caffe.export(l, args, tempdir, export_params, graph_name)
+
+        prototxt = os.path.join(tempdir, 'chainer_model.prototxt')
+        caffemodel = os.path.join(tempdir, 'chainer_model.caffemodel')
+
+        assert os.path.exists(prototxt)
+        assert os.path.exists(caffemodel)
+
+        # with open(prototxt) as f: print(f.read())
+        return L.caffe.CaffeFunction(caffemodel)
 
 
 testing.run_module(__name__, __file__)