[pycaffe] split _caffe into source and header files
authorJonathan L Long <jonlong@cs.berkeley.edu>
Sun, 31 Aug 2014 07:26:12 +0000 (00:26 -0700)
committerJonathan L Long <jonlong@cs.berkeley.edu>
Sun, 31 Aug 2014 08:13:39 +0000 (01:13 -0700)
This brings pycaffe more in line with usual C++ style, and will allow
the wrapper classes to be used within caffe when embedding Python.

Makefile
python/caffe/_caffe.cpp
python/caffe/_caffe.hpp [new file with mode: 0644]

index b6e0444..dd9ba5c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -70,6 +70,7 @@ EMPTY_LINT_REPORT := $(BUILD_DIR)/.$(LINT_EXT)
 NONEMPTY_LINT_REPORT := $(BUILD_DIR)/$(LINT_EXT)
 # PY$(PROJECT)_SRC is the python wrapper for $(PROJECT)
 PY$(PROJECT)_SRC := python/$(PROJECT)/_$(PROJECT).cpp
+PY$(PROJECT)_HXX_SRC := python/$(PROJECT)/_$(PROJECT).hpp
 PY$(PROJECT)_SO := python/$(PROJECT)/_$(PROJECT).so
 # MAT$(PROJECT)_SRC is the matlab wrapper for $(PROJECT)
 MAT$(PROJECT)_SRC := matlab/$(PROJECT)/mat$(PROJECT).cpp
@@ -345,7 +346,7 @@ py$(PROJECT): py
 
 py: $(PY$(PROJECT)_SO) $(PROTO_GEN_PY)
 
-$(PY$(PROJECT)_SO): $(STATIC_NAME) $(PY$(PROJECT)_SRC)
+$(PY$(PROJECT)_SO): $(STATIC_NAME) $(PY$(PROJECT)_SRC) $(PY$(PROJECT)_HXX_SRC)
        $(CXX) -shared -o $@ $(PY$(PROJECT)_SRC) \
                $(STATIC_NAME) $(LINKFLAGS) $(PYTHON_LDFLAGS)
        @ echo
index 49e2862..7c26c7f 100644 (file)
@@ -2,17 +2,14 @@
 // caffe::Caffe functions so that one could easily call it from Python.
 // Note that for Python, we will simply use float as the data type.
 
-#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
-
-#include "boost/python.hpp"
-#include "boost/python/suite/indexing/vector_indexing_suite.hpp"
-#include "numpy/arrayobject.h"
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
 
 // these need to be included after boost on OS X
 #include <string>  // NOLINT(build/include_order)
 #include <vector>  // NOLINT(build/include_order)
 #include <fstream>  // NOLINT
 
+#include "_caffe.hpp"
 #include "caffe/caffe.hpp"
 
 // Temporary solution for numpy < 1.7 versions: old macro, no promises.
@@ -22,8 +19,6 @@
 #define PyArray_SetBaseObject(arr, x) (PyArray_BASE(arr) = (x))
 #endif
 
-namespace bp = boost::python;
-
 namespace caffe {
 
 // for convenience, check that input files can be opened, and raise an
@@ -39,276 +34,113 @@ static void CheckFile(const string& filename) {
     f.close();
 }
 
-// wrap shared_ptr<Blob> in a class that we construct in C++ and pass
-// to Python
-template <typename Dtype>
-class PyBlob {
- public:
-  PyBlob(const shared_ptr<Blob<Dtype> > &blob, const string& name)
-      : blob_(blob), name_(name) {}
-
-  string name() const { return name_; }
-  int num() const { return blob_->num(); }
-  int channels() const { return blob_->channels(); }
-  int height() const { return blob_->height(); }
-  int width() const { return blob_->width(); }
-  int count() const { return blob_->count(); }
-
-  // this is here only to satisfy boost's vector_indexing_suite
-  bool operator == (const PyBlob &other) {
-      return this->blob_ == other.blob_;
-  }
-
- protected:
-  shared_ptr<Blob<Dtype> > blob_;
-  string name_;
-};
-
-
-// We need another wrapper (used as boost::python's HeldType) that receives a
-// self PyObject * which we can use as ndarray.base, so that data/diff memory
-// is not freed while still being used in Python.
-class PyBlobWrap : public PyBlob<float> {
- public:
-  PyBlobWrap(PyObject *p, const PyBlob<float> &blob)
-      : PyBlob<float>(blob), self_(p) {}
-
-  bp::object get_data() {
-      npy_intp dims[] = {num(), channels(), height(), width()};
-
-      PyObject *obj = PyArray_SimpleNewFromData(4, dims, NPY_FLOAT32,
-                                                blob_->mutable_cpu_data());
-      PyArray_SetBaseObject(reinterpret_cast<PyArrayObject *>(obj), self_);
-      Py_INCREF(self_);
-      bp::handle<> h(obj);
-
-      return bp::object(h);
-  }
-
-  bp::object get_diff() {
-      npy_intp dims[] = {num(), channels(), height(), width()};
-
-      PyObject *obj = PyArray_SimpleNewFromData(4, dims, NPY_FLOAT32,
-                                                blob_->mutable_cpu_diff());
-      PyArray_SetBaseObject(reinterpret_cast<PyArrayObject *>(obj), self_);
-      Py_INCREF(self_);
-      bp::handle<> h(obj);
-
-      return bp::object(h);
-  }
-
- private:
-  PyObject *self_;
-};
-
+bp::object PyBlobWrap::get_data() {
+  npy_intp dims[] = {num(), channels(), height(), width()};
 
-class PyLayer {
- public:
-  PyLayer(const shared_ptr<Layer<float> > &layer, const string &name)
-    : layer_(layer), name_(name) {}
+  PyObject *obj = PyArray_SimpleNewFromData(4, dims, NPY_FLOAT32,
+                                            blob_->mutable_cpu_data());
+  PyArray_SetBaseObject(reinterpret_cast<PyArrayObject *>(obj), self_);
+  Py_INCREF(self_);
+  bp::handle<> h(obj);
 
-  string name() const { return name_; }
-  vector<PyBlob<float> > blobs() {
-    vector<PyBlob<float> > result;
-    for (int i = 0; i < layer_->blobs().size(); ++i) {
-      result.push_back(PyBlob<float>(layer_->blobs()[i], name_));
-    }
-    return result;
-  }
-
-  // this is here only to satisfy boost's vector_indexing_suite
-  bool operator == (const PyLayer &other) {
-      return this->layer_ == other.layer_;
-  }
-
- protected:
-  shared_ptr<Layer<float> > layer_;
-  string name_;
-};
-
-
-// A simple wrapper over PyNet that runs the forward process.
-class PyNet {
- public:
-  // For cases where parameters will be determined later by the Python user,
-  // create a Net with unallocated parameters (which will not be zero-filled
-  // when accessed).
-  explicit PyNet(string param_file) {
-    Init(param_file);
-  }
-
-  PyNet(string param_file, string pretrained_param_file) {
-    Init(param_file);
-    CheckFile(pretrained_param_file);
-    net_->CopyTrainedLayersFrom(pretrained_param_file);
-  }
-
-  explicit PyNet(shared_ptr<Net<float> > net)
-      : net_(net) {}
-
-  void Init(string param_file) {
-    CheckFile(param_file);
-    net_.reset(new Net<float>(param_file));
-  }
-
-
-  virtual ~PyNet() {}
-
-  // Generate Python exceptions for badly shaped or discontiguous arrays.
-  inline void check_contiguous_array(PyArrayObject* arr, string name,
-      int channels, int height, int width) {
-    if (!(PyArray_FLAGS(arr) & NPY_ARRAY_C_CONTIGUOUS)) {
-      throw std::runtime_error(name + " must be C contiguous");
-    }
-    if (PyArray_NDIM(arr) != 4) {
-      throw std::runtime_error(name + " must be 4-d");
-    }
-    if (PyArray_TYPE(arr) != NPY_FLOAT32) {
-      throw std::runtime_error(name + " must be float32");
-    }
-    if (PyArray_DIMS(arr)[1] != channels) {
-      throw std::runtime_error(name + " has wrong number of channels");
-    }
-    if (PyArray_DIMS(arr)[2] != height) {
-      throw std::runtime_error(name + " has wrong height");
-    }
-    if (PyArray_DIMS(arr)[3] != width) {
-      throw std::runtime_error(name + " has wrong width");
-    }
-  }
+  return bp::object(h);
+}
 
-  void Forward(int start, int end) {
-    net_->ForwardFromTo(start, end);
-  }
+bp::object PyBlobWrap::get_diff() {
+  npy_intp dims[] = {num(), channels(), height(), width()};
 
-  void Backward(int start, int end) {
-    net_->BackwardFromTo(start, end);
-  }
+  PyObject *obj = PyArray_SimpleNewFromData(4, dims, NPY_FLOAT32,
+                                            blob_->mutable_cpu_diff());
+  PyArray_SetBaseObject(reinterpret_cast<PyArrayObject *>(obj), self_);
+  Py_INCREF(self_);
+  bp::handle<> h(obj);
 
-  void set_input_arrays(bp::object data_obj, bp::object labels_obj) {
-    // check that this network has an input MemoryDataLayer
-    shared_ptr<MemoryDataLayer<float> > md_layer =
-      boost::dynamic_pointer_cast<MemoryDataLayer<float> >(net_->layers()[0]);
-    if (!md_layer) {
-      throw std::runtime_error("set_input_arrays may only be called if the"
-          " first layer is a MemoryDataLayer");
-    }
-
-    // check that we were passed appropriately-sized contiguous memory
-    PyArrayObject* data_arr =
-        reinterpret_cast<PyArrayObject*>(data_obj.ptr());
-    PyArrayObject* labels_arr =
-        reinterpret_cast<PyArrayObject*>(labels_obj.ptr());
-    check_contiguous_array(data_arr, "data array", md_layer->datum_channels(),
-        md_layer->datum_height(), md_layer->datum_width());
-    check_contiguous_array(labels_arr, "labels array", 1, 1, 1);
-    if (PyArray_DIMS(data_arr)[0] != PyArray_DIMS(labels_arr)[0]) {
-      throw std::runtime_error("data and labels must have the same first"
-          " dimension");
-    }
-    if (PyArray_DIMS(data_arr)[0] % md_layer->batch_size() != 0) {
-      throw std::runtime_error("first dimensions of input arrays must be a"
-          " multiple of batch size");
-    }
+  return bp::object(h);
+}
 
-    // hold references
-    input_data_ = data_obj;
-    input_labels_ = labels_obj;
+PyNet::PyNet(string param_file, string pretrained_param_file) {
+  Init(param_file);
+  CheckFile(pretrained_param_file);
+  net_->CopyTrainedLayersFrom(pretrained_param_file);
+}
 
-    md_layer->Reset(static_cast<float*>(PyArray_DATA(data_arr)),
-        static_cast<float*>(PyArray_DATA(labels_arr)),
-        PyArray_DIMS(data_arr)[0]);
-  }
+void PyNet::Init(string param_file) {
+  CheckFile(param_file);
+  net_.reset(new Net<float>(param_file));
+}
 
-  // save the network weights to binary proto for net surgeries.
-  void save(string filename) {
-    NetParameter net_param;
-    net_->ToProto(&net_param, false);
-    WriteProtoToBinaryFile(net_param, filename.c_str());
+void PyNet::check_contiguous_array(PyArrayObject* arr, string name,
+    int channels, int height, int width) {
+  if (!(PyArray_FLAGS(arr) & NPY_ARRAY_C_CONTIGUOUS)) {
+    throw std::runtime_error(name + " must be C contiguous");
   }
-
-  // The caffe::Caffe utility functions.
-  void set_mode_cpu() { Caffe::set_mode(Caffe::CPU); }
-  void set_mode_gpu() { Caffe::set_mode(Caffe::GPU); }
-  void set_phase_train() { Caffe::set_phase(Caffe::TRAIN); }
-  void set_phase_test() { Caffe::set_phase(Caffe::TEST); }
-  void set_device(int device_id) { Caffe::SetDevice(device_id); }
-
-  vector<PyBlob<float> > blobs() {
-    vector<PyBlob<float> > result;
-    for (int i = 0; i < net_->blobs().size(); ++i) {
-      result.push_back(PyBlob<float>(net_->blobs()[i], net_->blob_names()[i]));
-    }
-    return result;
+  if (PyArray_NDIM(arr) != 4) {
+    throw std::runtime_error(name + " must be 4-d");
   }
-
-  vector<PyLayer> layers() {
-    vector<PyLayer> result;
-    for (int i = 0; i < net_->layers().size(); ++i) {
-      result.push_back(PyLayer(net_->layers()[i], net_->layer_names()[i]));
-    }
-    return result;
+  if (PyArray_TYPE(arr) != NPY_FLOAT32) {
+    throw std::runtime_error(name + " must be float32");
   }
-
-  bp::list inputs() {
-    bp::list input_blob_names;
-    for (int i = 0; i < net_->input_blob_indices().size(); ++i) {
-      input_blob_names.append(
-          net_->blob_names()[net_->input_blob_indices()[i]]);
-    }
-    return input_blob_names;
+  if (PyArray_DIMS(arr)[1] != channels) {
+    throw std::runtime_error(name + " has wrong number of channels");
   }
-
-  bp::list outputs() {
-    bp::list output_blob_names;
-    for (int i = 0; i < net_->output_blob_indices().size(); ++i) {
-      output_blob_names.append(
-          net_->blob_names()[net_->output_blob_indices()[i]]);
-    }
-    return output_blob_names;
+  if (PyArray_DIMS(arr)[2] != height) {
+    throw std::runtime_error(name + " has wrong height");
   }
-
-  // Input preprocessing configuration attributes. These are public for
-  // direct access from Python.
-  bp::dict mean_;
-  bp::dict input_scale_;
-  bp::dict raw_scale_;
-  bp::dict channel_swap_;
-
- protected:
-  // The pointer to the internal caffe::Net instant.
-  shared_ptr<Net<float> > net_;
-  // if taking input from an ndarray, we need to hold references
-  bp::object input_data_;
-  bp::object input_labels_;
-};
-
-class PySGDSolver {
- public:
-  explicit PySGDSolver(const string& param_file) {
-    // as in PyNet, (as a convenience, not a guarantee), create a Python
-    // exception if param_file can't be opened
-    CheckFile(param_file);
-    solver_.reset(new SGDSolver<float>(param_file));
-    // we need to explicitly store the net wrapper, rather than constructing
-    // it on the fly, so that it can hold references to Python objects
-    net_.reset(new PyNet(solver_->net()));
+  if (PyArray_DIMS(arr)[3] != width) {
+    throw std::runtime_error(name + " has wrong width");
   }
+}
 
-  shared_ptr<PyNet> net() { return net_; }
-  void Solve() { return solver_->Solve(); }
-  void SolveResume(const string& resume_file) {
-    CheckFile(resume_file);
-    return solver_->Solve(resume_file);
-  }
+void PyNet::set_input_arrays(bp::object data_obj, bp::object labels_obj) {
+  // check that this network has an input MemoryDataLayer
+  shared_ptr<MemoryDataLayer<float> > md_layer =
+    boost::dynamic_pointer_cast<MemoryDataLayer<float> >(net_->layers()[0]);
+  if (!md_layer) {
+    throw std::runtime_error("set_input_arrays may only be called if the"
+        " first layer is a MemoryDataLayer");
+  }
+
+  // check that we were passed appropriately-sized contiguous memory
+  PyArrayObject* data_arr =
+      reinterpret_cast<PyArrayObject*>(data_obj.ptr());
+  PyArrayObject* labels_arr =
+      reinterpret_cast<PyArrayObject*>(labels_obj.ptr());
+  check_contiguous_array(data_arr, "data array", md_layer->datum_channels(),
+      md_layer->datum_height(), md_layer->datum_width());
+  check_contiguous_array(labels_arr, "labels array", 1, 1, 1);
+  if (PyArray_DIMS(data_arr)[0] != PyArray_DIMS(labels_arr)[0]) {
+    throw std::runtime_error("data and labels must have the same first"
+        " dimension");
+  }
+  if (PyArray_DIMS(data_arr)[0] % md_layer->batch_size() != 0) {
+    throw std::runtime_error("first dimensions of input arrays must be a"
+        " multiple of batch size");
+  }
+
+  // hold references
+  input_data_ = data_obj;
+  input_labels_ = labels_obj;
+
+  md_layer->Reset(static_cast<float*>(PyArray_DATA(data_arr)),
+      static_cast<float*>(PyArray_DATA(labels_arr)),
+      PyArray_DIMS(data_arr)[0]);
+}
 
- protected:
-  shared_ptr<PyNet> net_;
-  shared_ptr<SGDSolver<float> > solver_;
-};
+PySGDSolver::PySGDSolver(const string& param_file) {
+  // as in PyNet, (as a convenience, not a guarantee), create a Python
+  // exception if param_file can't be opened
+  CheckFile(param_file);
+  solver_.reset(new SGDSolver<float>(param_file));
+  // we need to explicitly store the net wrapper, rather than constructing
+  // it on the fly, so that it can hold references to Python objects
+  net_.reset(new PyNet(solver_->net()));
+}
 
+void PySGDSolver::SolveResume(const string& resume_file) {
+  CheckFile(resume_file);
+  return solver_->Solve(resume_file);
+}
 
-// The boost_python module definition.
 BOOST_PYTHON_MODULE(_caffe) {
   // below, we prepend an underscore to methods that will be replaced
   // in Python
diff --git a/python/caffe/_caffe.hpp b/python/caffe/_caffe.hpp
new file mode 100644 (file)
index 0000000..20f9908
--- /dev/null
@@ -0,0 +1,186 @@
+#ifndef PYTHON_CAFFE__CAFFE_HPP_
+#define PYTHON_CAFFE__CAFFE_HPP_
+
+#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
+
+#include <boost/python.hpp>
+#include <boost/shared_ptr.hpp>
+#include <numpy/arrayobject.h>
+
+// these need to be included after boost on OS X
+#include <string>  // NOLINT(build/include_order)
+#include <vector>  // NOLINT(build/include_order)
+
+#include "caffe/caffe.hpp"
+
+namespace bp = boost::python;
+using boost::shared_ptr;
+
+namespace caffe {
+
+// wrap shared_ptr<Blob> in a class that we construct in C++ and pass
+// to Python
+template <typename Dtype>
+class PyBlob {
+ public:
+  PyBlob(const shared_ptr<Blob<Dtype> > &blob, const string& name)
+      : blob_(blob), name_(name) {}
+
+  string name() const { return name_; }
+  int num() const { return blob_->num(); }
+  int channels() const { return blob_->channels(); }
+  int height() const { return blob_->height(); }
+  int width() const { return blob_->width(); }
+  int count() const { return blob_->count(); }
+
+  // this is here only to satisfy boost's vector_indexing_suite
+  bool operator == (const PyBlob &other) {
+      return this->blob_ == other.blob_;
+  }
+
+ protected:
+  shared_ptr<Blob<Dtype> > blob_;
+  string name_;
+};
+
+// We need another wrapper (used as boost::python's HeldType) that receives a
+// self PyObject * which we can use as ndarray.base, so that data/diff memory
+// is not freed while still being used in Python.
+class PyBlobWrap : public PyBlob<float> {
+ public:
+  PyBlobWrap(PyObject *p, const PyBlob<float> &blob)
+      : PyBlob<float>(blob), self_(p) {}
+
+  bp::object get_data();
+  bp::object get_diff();
+
+ private:
+  PyObject *self_;
+};
+
+class PyLayer {
+ public:
+  PyLayer(const shared_ptr<Layer<float> > &layer, const string &name)
+    : layer_(layer), name_(name) {}
+
+  string name() const { return name_; }
+  vector<PyBlob<float> > blobs() {
+    vector<PyBlob<float> > result;
+    for (int i = 0; i < layer_->blobs().size(); ++i) {
+      result.push_back(PyBlob<float>(layer_->blobs()[i], name_));
+    }
+    return result;
+  }
+
+  // this is here only to satisfy boost's vector_indexing_suite
+  bool operator == (const PyLayer &other) {
+      return this->layer_ == other.layer_;
+  }
+
+ protected:
+  shared_ptr<Layer<float> > layer_;
+  string name_;
+};
+
+class PyNet {
+ public:
+  // For cases where parameters will be determined later by the Python user,
+  // create a Net with unallocated parameters (which will not be zero-filled
+  // when accessed).
+  explicit PyNet(string param_file) { Init(param_file); }
+  PyNet(string param_file, string pretrained_param_file);
+  explicit PyNet(shared_ptr<Net<float> > net)
+      : net_(net) {}
+  virtual ~PyNet() {}
+
+  void Init(string param_file);
+
+
+  // Generate Python exceptions for badly shaped or discontiguous arrays.
+  inline void check_contiguous_array(PyArrayObject* arr, string name,
+      int channels, int height, int width);
+
+  void Forward(int start, int end) { net_->ForwardFromTo(start, end); }
+  void Backward(int start, int end) { net_->BackwardFromTo(start, end); }
+
+  void set_input_arrays(bp::object data_obj, bp::object labels_obj);
+
+  // Save the network weights to binary proto for net surgeries.
+  void save(string filename) {
+    NetParameter net_param;
+    net_->ToProto(&net_param, false);
+    WriteProtoToBinaryFile(net_param, filename.c_str());
+  }
+
+  // The caffe::Caffe utility functions.
+  void set_mode_cpu() { Caffe::set_mode(Caffe::CPU); }
+  void set_mode_gpu() { Caffe::set_mode(Caffe::GPU); }
+  void set_phase_train() { Caffe::set_phase(Caffe::TRAIN); }
+  void set_phase_test() { Caffe::set_phase(Caffe::TEST); }
+  void set_device(int device_id) { Caffe::SetDevice(device_id); }
+
+  vector<PyBlob<float> > blobs() {
+    vector<PyBlob<float> > result;
+    for (int i = 0; i < net_->blobs().size(); ++i) {
+      result.push_back(PyBlob<float>(net_->blobs()[i], net_->blob_names()[i]));
+    }
+    return result;
+  }
+
+  vector<PyLayer> layers() {
+    vector<PyLayer> result;
+    for (int i = 0; i < net_->layers().size(); ++i) {
+      result.push_back(PyLayer(net_->layers()[i], net_->layer_names()[i]));
+    }
+    return result;
+  }
+
+  bp::list inputs() {
+    bp::list input_blob_names;
+    for (int i = 0; i < net_->input_blob_indices().size(); ++i) {
+      input_blob_names.append(
+          net_->blob_names()[net_->input_blob_indices()[i]]);
+    }
+    return input_blob_names;
+  }
+
+  bp::list outputs() {
+    bp::list output_blob_names;
+    for (int i = 0; i < net_->output_blob_indices().size(); ++i) {
+      output_blob_names.append(
+          net_->blob_names()[net_->output_blob_indices()[i]]);
+    }
+    return output_blob_names;
+  }
+
+  // Input preprocessing configuration attributes. These are public for
+  // direct access from Python.
+  bp::dict mean_;
+  bp::dict input_scale_;
+  bp::dict raw_scale_;
+  bp::dict channel_swap_;
+
+ protected:
+  // The pointer to the internal caffe::Net instant.
+  shared_ptr<Net<float> > net_;
+  // if taking input from an ndarray, we need to hold references
+  bp::object input_data_;
+  bp::object input_labels_;
+};
+
+class PySGDSolver {
+ public:
+  explicit PySGDSolver(const string& param_file);
+
+  shared_ptr<PyNet> net() { return net_; }
+  void Solve() { return solver_->Solve(); }
+  void SolveResume(const string& resume_file);
+
+ protected:
+  shared_ptr<PyNet> net_;
+  shared_ptr<SGDSolver<float> > solver_;
+};
+
+}  // namespace caffe
+
+#endif