From 80c4a9c7e500a941b8554bcd043d71abaa390e7e Mon Sep 17 00:00:00 2001 From: Takuya Narihira Date: Fri, 27 Feb 2015 19:01:54 -0800 Subject: [PATCH 1/5] Error in python can be shown Now we can keep the exception information on Python interpreter. This is usefull if you are working on Python interface. You can catch the right Exception class when error occurs in your `PythonLayer`. Now we don't use `PyErr_Print` that clears exceptions. We use `PyErr_Fetch` to get the information and `PyErr_Restore` to restore the exceptions back. And I find that `bp::import` or `bp::object::attr` breaks Python interpreter if a module or an attribute doesn't exist. We now use `bp::exec` and `bp::eval` to keep Python interpreter working even after errors occur. I got the hint from the following page. http://boost.2283326.n4.nabble.com/embedded-python-td2702335.html And I also modified to use `bp::object` for `self_` member. --- include/caffe/python_layer.hpp | 41 +++++++++++++++++++++------------- src/caffe/layer_factory.cpp | 12 ++++++---- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/include/caffe/python_layer.hpp b/include/caffe/python_layer.hpp index 816ef453720..9fae4668138 100644 --- a/include/caffe/python_layer.hpp +++ b/include/caffe/python_layer.hpp @@ -2,6 +2,8 @@ #define CAFFE_PYTHON_LAYER_HPP_ #include + +#include #include #include "caffe/layer.hpp" @@ -10,29 +12,41 @@ namespace bp = boost::python; namespace caffe { +#define PYTHON_LAYER_ERROR() { \ + PyObject *petype, *pevalue, *petrace; \ + PyErr_Fetch(&petype, &pevalue, &petrace); \ + bp::object etype(bp::handle<>(bp::borrowed(petype))); \ + bp::object evalue(bp::handle<>(bp::borrowed(bp::allow_null(pevalue)))); \ + bp::object etrace(bp::handle<>(bp::borrowed(bp::allow_null(petrace)))); \ + bp::object sio(bp::import("StringIO").attr("StringIO")()); \ + bp::import("traceback").attr("print_exception")( \ + etype, evalue, etrace, bp::object(), sio); \ + LOG(INFO) << bp::extract(sio.attr("getvalue")())(); \ + PyErr_Restore(petype, pevalue, petrace); \ + throw; \ +} + template class PythonLayer : public Layer { public: PythonLayer(PyObject* self, const LayerParameter& param) - : Layer(param), self_(self) { } + : Layer(param), self_(bp::handle<>(bp::borrowed(self))) { } virtual void LayerSetUp(const vector*>& bottom, const vector*>& top) { try { - bp::call_method(self_, "setup", bottom, top); + self_.attr("setup")(bottom, top); } catch (bp::error_already_set) { - PyErr_Print(); - throw; + PYTHON_LAYER_ERROR(); } } virtual void Reshape(const vector*>& bottom, const vector*>& top) { try { - bp::call_method(self_, "reshape", bottom, top); + self_.attr("reshape")(bottom, top); } catch (bp::error_already_set) { - PyErr_Print(); - throw; + PYTHON_LAYER_ERROR(); } } @@ -42,25 +56,22 @@ class PythonLayer : public Layer { virtual void Forward_cpu(const vector*>& bottom, const vector*>& top) { try { - bp::call_method(self_, "forward", bottom, top); + self_.attr("forward")(bottom, top); } catch (bp::error_already_set) { - PyErr_Print(); - throw; + PYTHON_LAYER_ERROR(); } } virtual void Backward_cpu(const vector*>& top, const vector& propagate_down, const vector*>& bottom) { try { - bp::call_method(self_, "backward", top, propagate_down, - bottom); + self_.attr("backward")(top, propagate_down, bottom); } catch (bp::error_already_set) { - PyErr_Print(); - throw; + PYTHON_LAYER_ERROR(); } } private: - PyObject* self_; + bp::object self_; }; } // namespace caffe diff --git a/src/caffe/layer_factory.cpp b/src/caffe/layer_factory.cpp index d6a1cac5090..1a0cdf658df 100644 --- a/src/caffe/layer_factory.cpp +++ b/src/caffe/layer_factory.cpp @@ -160,14 +160,18 @@ REGISTER_LAYER_CREATOR(TanH, GetTanHLayer); #ifdef WITH_PYTHON_LAYER template shared_ptr > GetPythonLayer(const LayerParameter& param) { + string module_name = param.python_param().module(); + string layer_name = param.python_param().layer(); Py_Initialize(); try { - bp::object module = bp::import(param.python_param().module().c_str()); - bp::object layer = module.attr(param.python_param().layer().c_str())(param); + bp::object globals = bp::import("__main__").attr("__dict__"); + bp::exec(("import " + module_name).c_str(), globals, globals); + bp::object layer_class = bp::eval((module_name + "." + layer_name).c_str(), + globals, globals); + bp::object layer = layer_class(param); return bp::extract > >(layer)(); } catch (bp::error_already_set) { - PyErr_Print(); - throw; + PYTHON_LAYER_ERROR(); } } From 38ddc14c04f4914bea6f0779203293c0742c45bc Mon Sep 17 00:00:00 2001 From: Takuya Narihira Date: Fri, 27 Feb 2015 19:34:51 -0800 Subject: [PATCH 2/5] PythonLayer takes parameters by string --- include/caffe/python_layer.hpp | 2 + .../test/test_python_layer_with_param_str.py | 85 +++++++++++++++++++ src/caffe/proto/caffe.proto | 6 ++ 3 files changed, 93 insertions(+) create mode 100644 python/caffe/test/test_python_layer_with_param_str.py diff --git a/include/caffe/python_layer.hpp b/include/caffe/python_layer.hpp index 9fae4668138..b891ad780c0 100644 --- a/include/caffe/python_layer.hpp +++ b/include/caffe/python_layer.hpp @@ -35,6 +35,8 @@ class PythonLayer : public Layer { virtual void LayerSetUp(const vector*>& bottom, const vector*>& top) { try { + self_.attr("param_str_") = bp::str( + this->layer_param_.python_param().param_str()); self_.attr("setup")(bottom, top); } catch (bp::error_already_set) { PYTHON_LAYER_ERROR(); diff --git a/python/caffe/test/test_python_layer_with_param_str.py b/python/caffe/test/test_python_layer_with_param_str.py new file mode 100644 index 00000000000..709483b5eb8 --- /dev/null +++ b/python/caffe/test/test_python_layer_with_param_str.py @@ -0,0 +1,85 @@ +import unittest +import tempfile + +import os + +import caffe + +class ElemwiseScalarLayer(caffe.Layer): + """A layer that just multiplies by ten""" + + def setup(self, bottom, top): + self.layer_params_ = eval(self.param_str_) + self.value_ = self.layer_params_['value'] + if self.layer_params_['op'].lower() == 'add': + self._forward = self._forward_add + self._backward = self._backward_add + elif self.layer_params_['op'].lower() == 'mul': + self._forward = self._forward_mul + self._backward = self._backward_mul + else: + raise ValueError("Unknown operation type: '%s'" + % self.layer_params_['op'].lower()) + def _forward_add(self, bottom, top): + top[0].data[...] = bottom[0].data + self.value_ + def _backward_add(self, bottom, propagate_down, top): + bottom[0].diff[...] = top[0].diff + def _forward_mul(self, bottom, top): + top[0].data[...] = bottom[0].data * self.value_ + def _backward_mul(self, bottom, propagate_down, top): + bottom[0].diff[...] = top[0].diff * self.value_ + def reshape(self, bottom, top): + top[0].reshape(bottom[0].num, bottom[0].channels, bottom[0].height, + bottom[0].width) + def forward(self, bottom, top): + self._forward(bottom, top) + def backward(self, top, propagate_down, bottom): + self._backward(bottom, propagate_down, top) + + +def python_net_file(): + f = tempfile.NamedTemporaryFile(delete=False) + f.write(r"""name: 'pythonnet' force_backward: true + input: 'data' input_dim: 10 input_dim: 9 input_dim: 8 input_dim: 7 + layer { type: 'Python' name: 'one' bottom: 'data' top: 'one' + python_param { + module: 'test_python_layer_with_param_str' layer: 'ElemwiseScalarLayer' + param_str: "{'op': 'add', 'value': 2}" } } + layer { type: 'Python' name: 'two' bottom: 'one' top: 'two' + python_param { + module: 'test_python_layer_with_param_str' layer: 'ElemwiseScalarLayer' + param_str: "{'op': 'mul', 'value': 3}" } } + layer { type: 'Python' name: 'three' bottom: 'two' top: 'three' + python_param { + module: 'test_python_layer_with_param_str' layer: 'ElemwiseScalarLayer' + param_str: "{'op': 'add', 'value': 10}" } }""") + f.close() + return f.name + +class TestLayerWithParam(unittest.TestCase): + def setUp(self): + net_file = python_net_file() + self.net = caffe.Net(net_file, caffe.TRAIN) + os.remove(net_file) + + def test_forward(self): + x = 8 + self.net.blobs['data'].data[...] = x + self.net.forward() + for y in self.net.blobs['three'].data.flat: + self.assertEqual(y, (x + 2) * 3 + 10) + + def test_backward(self): + x = 7 + self.net.blobs['three'].diff[...] = x + self.net.backward() + for y in self.net.blobs['data'].diff.flat: + self.assertEqual(y, 3 * x) + + def test_reshape(self): + s = 4 + self.net.blobs['data'].reshape(s, s, s, s) + self.net.forward() + for blob in self.net.blobs.itervalues(): + for d in blob.data.shape: + self.assertEqual(s, d) diff --git a/src/caffe/proto/caffe.proto b/src/caffe/proto/caffe.proto index 5b21cf20028..4a897221e72 100644 --- a/src/caffe/proto/caffe.proto +++ b/src/caffe/proto/caffe.proto @@ -664,6 +664,12 @@ message PowerParameter { message PythonParameter { optional string module = 1; optional string layer = 2; + // This value is set to the attribute `param_str_` of your custom + // `PythonLayer` object in Python before calling `setup()` method. This could + // be a number, a string, a dictionary in Python dict format or JSON etc. You + // may parse this string in `setup` method and use them in `forward` and + // `backward`. + optional string param_str = 3 [default = '']; } // Message that stores parameters used by ReLULayer From bf5de7640904d07dcab5934e6aa85822d68eb31c Mon Sep 17 00:00:00 2001 From: Takuya Narihira Date: Mon, 2 Mar 2015 10:54:57 -0800 Subject: [PATCH 3/5] Check injections in PythonLayer * Modified Makefile to link boost_regex for PythonLayer. * Travis installs boost_regex * Add a note of a new dependency boost_regex on Makefile.config.example --- Makefile | 2 +- Makefile.config.example | 1 + scripts/travis/travis_install.sh | 1 + src/caffe/layer_factory.cpp | 7 +++++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 706959ad0ca..fbcf038cfc9 100644 --- a/Makefile +++ b/Makefile @@ -164,7 +164,7 @@ endif LIBRARIES += glog gflags protobuf leveldb snappy \ lmdb boost_system hdf5_hl hdf5 m \ opencv_core opencv_highgui opencv_imgproc -PYTHON_LIBRARIES := boost_python python2.7 +PYTHON_LIBRARIES := boost_python python2.7 boost_regex WARNINGS := -Wall -Wno-sign-compare ############################## diff --git a/Makefile.config.example b/Makefile.config.example index 7a8aafd7c9f..897f9574991 100644 --- a/Makefile.config.example +++ b/Makefile.config.example @@ -58,6 +58,7 @@ PYTHON_LIB := /usr/lib # PYTHON_LIB := $(ANACONDA_HOME)/lib # Uncomment to support layers written in Python (will link against Python libs) +# This will require an additional dependency boost_regex provided by boost. # WITH_PYTHON_LAYER := 1 # Whatever else you find you need goes here. diff --git a/scripts/travis/travis_install.sh b/scripts/travis/travis_install.sh index 0e8c37861b0..277ecbb11bc 100755 --- a/scripts/travis/travis_install.sh +++ b/scripts/travis/travis_install.sh @@ -15,6 +15,7 @@ apt-get install \ python-dev python-numpy \ libleveldb-dev libsnappy-dev libopencv-dev \ libboost-dev libboost-system-dev libboost-python-dev libboost-thread-dev \ + libboost-regex-dev \ libprotobuf-dev protobuf-compiler \ libatlas-dev libatlas-base-dev \ libhdf5-serial-dev libgflags-dev libgoogle-glog-dev \ diff --git a/src/caffe/layer_factory.cpp b/src/caffe/layer_factory.cpp index 1a0cdf658df..6dd25b3cc36 100644 --- a/src/caffe/layer_factory.cpp +++ b/src/caffe/layer_factory.cpp @@ -6,6 +6,7 @@ #include "caffe/vision_layers.hpp" #ifdef WITH_PYTHON_LAYER +#include #include "caffe/python_layer.hpp" #endif @@ -162,6 +163,12 @@ template shared_ptr > GetPythonLayer(const LayerParameter& param) { string module_name = param.python_param().module(); string layer_name = param.python_param().layer(); + // Check injection. This doesn't allow nested importing. + boost::regex expression("[a-zA-Z_][a-zA-Z0-9_]*"); + CHECK(boost::regex_match(module_name, expression)) + << "Module name is invalid: " << module_name; + CHECK(boost::regex_match(layer_name, expression)) + << "Layer name is invalid: " << layer_name; Py_Initialize(); try { bp::object globals = bp::import("__main__").attr("__dict__"); From 471e71dcf7fb3495762f15555b381a58278e9d45 Mon Sep 17 00:00:00 2001 From: Takuya Narihira Date: Tue, 3 Mar 2015 09:12:02 -0800 Subject: [PATCH 4/5] PythonLayer allows to import nested modules --- src/caffe/layer_factory.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/caffe/layer_factory.cpp b/src/caffe/layer_factory.cpp index 6dd25b3cc36..aba8df342f2 100644 --- a/src/caffe/layer_factory.cpp +++ b/src/caffe/layer_factory.cpp @@ -163,8 +163,9 @@ template shared_ptr > GetPythonLayer(const LayerParameter& param) { string module_name = param.python_param().module(); string layer_name = param.python_param().layer(); - // Check injection. This doesn't allow nested importing. - boost::regex expression("[a-zA-Z_][a-zA-Z0-9_]*"); + // Check injection. This allows nested import. + boost::regex expression("[a-zA-Z_][a-zA-Z0-9_]*" + "(\\.[a-zA-Z_][a-zA-Z0-9_]*)*"); CHECK(boost::regex_match(module_name, expression)) << "Module name is invalid: " << module_name; CHECK(boost::regex_match(layer_name, expression)) From 7b965270253b6b089ab48ed99d477644c6a84ac3 Mon Sep 17 00:00:00 2001 From: Takuya Narihira Date: Tue, 31 Mar 2015 07:30:32 -0700 Subject: [PATCH 5/5] CMake dependency to boost regex --- cmake/Dependencies.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index f328e8246ab..5e9cb5dc7c5 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -131,6 +131,11 @@ if(BUILD_python) add_definitions(-DWITH_PYTHON_LAYER) include_directories(SYSTEM ${PYTHON_INCLUDE_DIRS} ${NUMPY_INCLUDE_DIR} ${Boost_INCLUDE_DIRS}) list(APPEND Caffe_LINKER_LIBS ${PYTHON_LIBRARIES} ${Boost_LIBRARIES}) + find_package(Boost 1.46 COMPONENTS regex) + if (Boost_REGEX_FOUND) + include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) + list(APPEND Caffe_LINKER_LIBS ${Boost_LIBRARIES}) + endif() endif() endif() endif()