diff --git a/setup.py b/setup.py index b7c3e4d69f3ce60fa3fbb7c5dbf8d0018807b4c9..bc32e789637bb4502587cb803f01b22a824c351f 100644 --- a/setup.py +++ b/setup.py @@ -63,9 +63,14 @@ setup( ), Extension("xbob.learn.mlp._library", [ + "xbob/learn/mlp/roll.cpp", "xbob/learn/mlp/rprop.cpp", "xbob/learn/mlp/backprop.cpp", "xbob/learn/mlp/trainer.cpp", + "xbob/learn/mlp/shuffler.cpp", + "xbob/learn/mlp/cost.cpp", + "xbob/learn/mlp/machine.cpp", + "xbob/learn/mlp/main.cpp", "xbob/learn/mlp/cxx/roll.cpp", "xbob/learn/mlp/cxx/machine.cpp", "xbob/learn/mlp/cxx/cross_entropy.cpp", @@ -74,10 +79,6 @@ setup( "xbob/learn/mlp/cxx/trainer.cpp", "xbob/learn/mlp/cxx/backprop.cpp", "xbob/learn/mlp/cxx/rprop.cpp", - "xbob/learn/mlp/shuffler.cpp", - "xbob/learn/mlp/cost.cpp", - "xbob/learn/mlp/machine.cpp", - "xbob/learn/mlp/main.cpp", ], packages = packages, include_dirs = include_dirs, diff --git a/xbob/learn/mlp/backprop.cpp b/xbob/learn/mlp/backprop.cpp index 6f77625ceb07fbbc36b9a0f5bc0978128e4275f4..d9462de445b15f9b17151698aba17368c21a4f95 100644 --- a/xbob/learn/mlp/backprop.cpp +++ b/xbob/learn/mlp/backprop.cpp @@ -256,8 +256,8 @@ static int PyBobLearnMLPBackProp_setPreviousDerivatives (PyBobLearnMLPBackPropObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,2>> bzvec; - int retval = convert_tuple<2>((PyObject*)self, s_previous_derivatives_str, - o, bzvec); + int retval = convert_tuple<2>(Py_TYPE(self)->tp_name, + s_previous_derivatives_str, o, bzvec); if (retval < 0) return retval; try { @@ -292,7 +292,7 @@ static int PyBobLearnMLPBackProp_setPreviousBiasDerivatives (PyBobLearnMLPBackPropObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,1>> bzvec; - int retval = convert_tuple<1>((PyObject*)self, + int retval = convert_tuple<1>(Py_TYPE(self)->tp_name, s_previous_bias_derivatives_str, o, bzvec); if (retval < 0) return retval; diff --git a/xbob/learn/mlp/cxx/roll.cpp b/xbob/learn/mlp/cxx/roll.cpp index 7c7b7dec0d1198584c56aaca922e024afdd5f989..d76de0a9a08a731ebe108aca6d09bf3deff3cd83 100644 --- a/xbob/learn/mlp/cxx/roll.cpp +++ b/xbob/learn/mlp/cxx/roll.cpp @@ -5,7 +5,6 @@ * Copyright (C) 2011-2014 Idiap Research Institute, Martigny, Switzerland */ -#include <bob/core/assert.h> #include <xbob.learn.mlp/roll.h> int bob::learn::mlp::detail::getNbParameters(const bob::learn::mlp::Machine& machine) @@ -19,7 +18,6 @@ int bob::learn::mlp::detail::getNbParameters( const std::vector<blitz::Array<double,2> >& w, const std::vector<blitz::Array<double,1> >& b) { - bob::core::array::assertSameDimensionLength(w.size(), b.size()); int N = 0; for (int i=0; i<(int)w.size(); ++i) N += b[i].numElements() + w[i].numElements(); @@ -37,11 +35,6 @@ void bob::learn::mlp::unroll(const bob::learn::mlp::Machine& machine, void bob::learn::mlp::unroll(const std::vector<blitz::Array<double,2> >& w, const std::vector<blitz::Array<double,1> >& b, blitz::Array<double,1>& vec) { - // 1/ Check number of elements - const int N = bob::learn::mlp::detail::getNbParameters(w, b); - bob::core::array::assertSameDimensionLength(vec.extent(0), N); - - // 2/ Roll blitz::Range rall = blitz::Range::all(); int offset=0; for (int i=0; i<(int)w.size(); ++i) @@ -74,11 +67,6 @@ void bob::learn::mlp::roll(bob::learn::mlp::Machine& machine, void bob::learn::mlp::roll(std::vector<blitz::Array<double,2> >& w, std::vector<blitz::Array<double,1> >& b, const blitz::Array<double,1>& vec) { - // 1/ Check number of elements - const int N = bob::learn::mlp::detail::getNbParameters(w, b); - bob::core::array::assertSameDimensionLength(vec.extent(0), N); - - // 2/ Roll blitz::Range rall = blitz::Range::all(); int offset=0; for (int i=0; i<(int)w.size(); ++i) diff --git a/xbob/learn/mlp/machine.cpp b/xbob/learn/mlp/machine.cpp index 5d36bc142ca4c0bfaf9d17b38b349227ae9d9532..abdac46b1e8fd565e33bddd73e0c1d53adda72cd 100644 --- a/xbob/learn/mlp/machine.cpp +++ b/xbob/learn/mlp/machine.cpp @@ -36,7 +36,7 @@ a global activation function. References to fully-connected\n\ feed-forward networks:\n\ \n\ Bishop's Pattern Recognition and Machine Learning, Chapter 5.\n\ - Figure 5.1 shows what we mean.\n\ + Figure 5.1 shows what is programmed.\n\ \n\ MLPs normally are multi-layered systems, with 1 or more hidden\n\ layers. As a special case, this implementation also supports\n\ @@ -737,7 +737,7 @@ PyObject* PyBobLearnMLPMachine_Repr(PyBobLearnMLPMachineObject* self) { PyDoc_STRVAR(s_forward_str, "forward"); PyDoc_STRVAR(s_forward_doc, -"o.forward(input [, output]) -> array\n\ +"o.forward(input, [output]) -> array\n\ \n\ Projects ``input`` through its internal structure. If\n\ ``output`` is provided, place output there instead of allocating\n\ @@ -833,6 +833,7 @@ static PyObject* PyBobLearnMLPMachine_forward osize[1] = self->cxx->outputSize(); } output = (PyBlitzArrayObject*)PyBlitzArray_SimpleNew(NPY_FLOAT64, input->ndim, osize); + if (!output) return 0; output_ = make_safe(output); } @@ -937,7 +938,7 @@ static PyObject* PyBobLearnMLPMachine_Save PyDoc_STRVAR(s_is_similar_to_str, "is_similar_to"); PyDoc_STRVAR(s_is_similar_to_doc, -"o.is_similar_to(other [, r_epsilon=1e-5 [, a_epsilon=1e-8]]) -> bool\n\ +"o.is_similar_to(other, [r_epsilon=1e-5, [a_epsilon=1e-8]]) -> bool\n\ \n\ Compares this MLP with the ``other`` one to be approximately the same.\n\ \n\ diff --git a/xbob/learn/mlp/main.cpp b/xbob/learn/mlp/main.cpp index b330cfc1b72f9f1b7cfadf5d3f30d1ef2e336c5f..20f94b007e343e2bce9e2a261709ac3a62e26d8f 100644 --- a/xbob/learn/mlp/main.cpp +++ b/xbob/learn/mlp/main.cpp @@ -2,7 +2,7 @@ * @author Andre Anjos <andre.anjos@idiap.ch> * @date Fri 13 Dec 2013 12:35:59 CET * - * @brief Bindings to bob::machine + * @brief Bindings to bob::learn::mlp */ #define XBOB_LEARN_MLP_MODULE @@ -17,8 +17,191 @@ #include <xbob.learn.activation/api.h> #include <xbob.core/random.h> +PyDoc_STRVAR(s_unroll_str, "unroll"); +PyDoc_STRVAR(s_unroll_doc, +"unroll(machine, [parameters]) -> parameters\n\ +\n\ +unroll(weights, biases, [parameters]) -> parameters\n\ +\n\ +Unroll the parameters (weights and biases) into a 64-bit float 1D array.\n\ +\n\ +This function will unroll the MLP machine weights and biases into a\n\ +single 1D array of 64-bit floats. This procedure is useful for adapting\n\ +generic optimization procedures for the task of training MLPs.\n\ +\n\ +Keyword parameters:\n\ +\n\ +machine, :py:class:`xbob.learn.mlp.Machine`\n\ + An MLP that will have its weights and biases unrolled into a 1D array\n\ +\n\ +weights, sequence of 2D 64-bit float arrays\n\ + If you choose the second calling strategy, then pass a sequence of\n\ + 2D arrays of 64-bit floats representing the weights for the MLP you\n\ + wish to unroll.\n\ + \n\ + .. note::\n\ + In this case, both this sequence and ``biases`` must have the\n\ + same length. This is the sole requirement.\n\ + \n\ + Other checks are disabled as this is considered an *expert* API.\n\ + If you plan to unroll the weights and biases on a\n\ + :py:class:`xbob.learn.mlp.Machine`, notice that in a given\n\ + ``weights`` sequence, the number of outputs in layer ``k``\n\ + must match the number of inputs on layer ``k+1`` and the\n\ + number of biases on layer ``k``. In practice, you must assert\n\ + that ``weights[k].shape[1] == weights[k+1].shape[0]`` and.\n\ + that ``weights[k].shape[1] == bias[k].shape[0]``.\n\ +\n\ +biases, sequence of 1D 64-bit float arrays\n\ + If you choose the second calling strategy, then pass a sequence of\n\ + 1D arrays of 64-bit floats representing the biases for the MLP you\n\ + wish to unroll.\n\ + \n\ + .. note::\n\ + In this case, both this sequence and ``biases`` must have the\n\ + same length. This is the sole requirement.\n\ +\n\ +parameters, 1D 64-bit float array\n\ + You may decide to pass the array in which the parameters will be\n\ + placed using this variable. In this case, the size of the vector\n\ + must match the total number of parameters available on the input\n\ + machine or discrete weights and biases. If you decided to omit\n\ + this parameter, then a vector with the appropriate size will be\n\ + allocated internally and returned.\n\ + \n\ + You can use py:func:`number_of_parameters` to calculate the total\n\ + length of the required ``parameters`` vector, in case you wish\n\ + to supply it.\n\ +\n\ +"); + +PyObject* unroll(PyObject*, PyObject* args, PyObject* kwds); + +PyDoc_STRVAR(s_roll_str, "roll"); +PyDoc_STRVAR(s_roll_doc, +"roll(machine, parameters) -> parameters\n\ +\n\ +roll(weights, biases, parameters) -> parameters\n\ +\n\ +Roll the parameters (weights and biases) from a 64-bit float 1D array.\n\ +\n\ +This function will roll the MLP machine weights and biases from a\n\ +single 1D array of 64-bit floats. This procedure is useful for adapting\n\ +generic optimization procedures for the task of training MLPs.\n\ +\n\ +Keyword parameters:\n\ +\n\ +machine, :py:class:`xbob.learn.mlp.Machine`\n\ + An MLP that will have its weights and biases rolled from a 1D array\n\ +\n\ +weights, sequence of 2D 64-bit float arrays\n\ + If you choose the second calling strategy, then pass a sequence of\n\ + 2D arrays of 64-bit floats representing the weights for the MLP you\n\ + wish to roll the parameters into using this argument.\n\ + \n\ + .. note::\n\ + In this case, both this sequence and ``biases`` must have the\n\ + same length. This is the sole requirement.\n\ + \n\ + Other checks are disabled as this is considered an *expert* API.\n\ + If you plan to roll the weights and biases on a\n\ + :py:class:`xbob.learn.mlp.Machine`, notice that in a given\n\ + ``weights`` sequence, the number of outputs in layer ``k``\n\ + must match the number of inputs on layer ``k+1`` and the\n\ + number of biases on layer ``k``. In practice, you must assert\n\ + that ``weights[k].shape[1] == weights[k+1].shape[0]`` and.\n\ + that ``weights[k].shape[1] == bias[k].shape[0]``.\n\ +\n\ +biases, sequence of 1D 64-bit float arrays\n\ + If you choose the second calling strategy, then pass a sequence of\n\ + 1D arrays of 64-bit floats representing the biases for the MLP you\n\ + wish to roll the parameters into.\n\ + \n\ + .. note::\n\ + In this case, both this sequence and ``biases`` must have the\n\ + same length. This is the sole requirement.\n\ +\n\ +parameters, 1D 64-bit float array\n\ + You may decide to pass the array in which the parameters will be\n\ + placed using this variable. In this case, the size of the vector\n\ + must match the total number of parameters available on the input\n\ + machine or discrete weights and biases. If you decided to omit\n\ + this parameter, then a vector with the appropriate size will be\n\ + allocated internally and returned.\n\ + \n\ + You can use py:func:`number_of_parameters` to calculate the total\n\ + length of the required ``parameters`` vector, in case you wish\n\ + to supply it.\n\ +\n\ +"); + +PyObject* roll(PyObject*, PyObject* args, PyObject* kwds); + +PyDoc_STRVAR(s_number_of_parameters_str, "number_of_parameters"); +PyDoc_STRVAR(s_number_of_parameters_doc, +"number_of_parameters(machine) -> scalar\n\ +\n\ +number_of_parameters(weights, biases) -> scalar\n\ +\n\ +Returns the total number of parameters in an MLP.\n\ +\n\ +Keyword parameters:\n\ +\n\ +machine, :py:class:`xbob.learn.mlp.Machine`\n\ + Using the first call API, counts the total number of parameters in\n\ + an MLP.\n\ +\n\ +weights, sequence of 2D 64-bit float arrays\n\ + If you choose the second calling strategy, then pass a sequence of\n\ + 2D arrays of 64-bit floats representing the weights for the MLP you\n\ + wish to count the parameters from.\n\ + \n\ + .. note::\n\ + In this case, both this sequence and ``biases`` must have the\n\ + same length. This is the sole requirement.\n\ + \n\ + Other checks are disabled as this is considered an *expert* API.\n\ + If you plan to unroll the weights and biases on a\n\ + :py:class:`xbob.learn.mlp.Machine`, notice that in a given\n\ + ``weights`` sequence the number of outputs in layer ``k``\n\ + must match the number of inputs on layer ``k+1`` and the\n\ + number of bias on layer ``k``. In practice, you must assert\n\ + that ``weights[k].shape[1] == weights[k+1].shape[0]`` and.\n\ + that ``weights[k].shape[1] == bias[k].shape[0]``.\n\ +\n\ +biases, sequence of 1D 64-bit float arrays\n\ + If you choose the second calling strategy, then pass a sequence of\n\ + 1D arrays of 64-bit floats representing the biases for the MLP you\n\ + wish to number_of_parameters the parameters into.\n\ + \n\ + .. note::\n\ + In this case, both this sequence and ``biases`` must have the\n\ + same length. This is the sole requirement.\n\ +\n\ +"); + +PyObject* number_of_parameters(PyObject*, PyObject* args, PyObject* kwds); + static PyMethodDef module_methods[] = { - {0} /* Sentinel */ + { + s_unroll_str, + (PyCFunction)unroll, + METH_VARARGS|METH_KEYWORDS, + s_unroll_doc + }, + { + s_roll_str, + (PyCFunction)roll, + METH_VARARGS|METH_KEYWORDS, + s_roll_doc + }, + { + s_number_of_parameters_str, + (PyCFunction)number_of_parameters, + METH_VARARGS|METH_KEYWORDS, + s_number_of_parameters_doc + }, + {0} /* Sentinel */ }; PyDoc_STRVAR(module_docstr, "bob's multi-layer perceptron machine and trainers"); diff --git a/xbob/learn/mlp/roll.cpp b/xbob/learn/mlp/roll.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7155c5db21da197057b1d6d24a8a5feb6bd28798 --- /dev/null +++ b/xbob/learn/mlp/roll.cpp @@ -0,0 +1,411 @@ +/** + * @author Andre Anjos <andre.anjos@idiap.ch> + * @date Wed 21 May 12:08:40 2014 CEST + * + * @brief Bindings to roll/unroll + */ + + +#define XBOB_LEARN_MLP_MODULE +#include <xbob.learn.mlp/api.h> +#include <xbob.learn.mlp/roll.h> +#include <xbob.blitz/capi.h> +#include <xbob.blitz/cleanup.h> + +#include "utils.h" + +static PyObject* unroll_from_machine(PyObject* args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"machine", "parameters", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyObject* machine = 0; + PyBlitzArrayObject* parameters = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O&", kwlist, + &PyBobLearnMLPMachine_Type, &machine, + &PyBlitzArray_OutputConverter, ¶meters + )) return 0; + + auto machine_ = reinterpret_cast<PyBobLearnMLPMachineObject*>(machine); + auto parameters_ = make_xsafe(parameters); + Py_ssize_t nb_parameters = + bob::learn::mlp::detail::getNbParameters(*machine_->cxx); + + if (parameters) { + + if (parameters->type_num != NPY_FLOAT64 || + parameters->ndim != 1 || + parameters->shape[0] != nb_parameters) { + PyErr_Format(PyExc_TypeError, "function only supports 1D 64-bit float arrays with shape (%" PY_FORMAT_SIZE_T "d,) for output array `parameters', but you passed a %" PY_FORMAT_SIZE_T"dD %s array with shape (%" PY_FORMAT_SIZE_T "d,)", nb_parameters, parameters->ndim, PyBlitzArray_TypenumAsString(parameters->type_num), parameters->shape[0]); + return 0; + } + + } + + else { + + //allocate space for the parameters + parameters = (PyBlitzArrayObject*)PyBlitzArray_SimpleNew(NPY_FLOAT64, 1, &nb_parameters); + if (!parameters) return 0; + parameters_ = make_safe(parameters); + + } + + /** all basic checks are done, can execute the function now **/ + try { + bob::learn::mlp::unroll(*machine_->cxx, + *PyBlitzArrayCxx_AsBlitz<double,1>(parameters)); + } + catch (std::exception& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return 0; + } + catch (...) { + PyErr_SetString(PyExc_RuntimeError, "cannot unroll machine parameters: unknown exception caught"); + return 0; + } + + Py_INCREF(parameters); + return PyBlitzArray_NUMPY_WRAP((PyObject*)parameters); + +} + +static PyObject* unroll_from_values(PyObject* args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"weights", "biases", "parameters", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyObject* weights = 0; + PyObject* biases = 0; + PyBlitzArrayObject* parameters = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O&", kwlist, + &weights, &biases, + &PyBlitzArray_OutputConverter, ¶meters + )) return 0; + + auto parameters_ = make_xsafe(parameters); + + //converts weights and biases + std::vector<blitz::Array<double,2>> weights_; + int status = convert_tuple<2>("unroll", "weights", weights, weights_); + if (status < 0) return 0; + + std::vector<blitz::Array<double,1>> biases_; + status = convert_tuple<1>("unroll", "biases", biases, biases_); + if (status < 0) return 0; + + //checks + if (weights_.size() != biases_.size()) { + PyErr_Format(PyExc_RuntimeError, "unroll, when applied to individual weights and biases, requires these iterables to have the same length but len(weights) = %" PY_FORMAT_SIZE_T"d != len(biases) = %" PY_FORMAT_SIZE_T "d", weights_.size(), biases_.size()); + return 0; + } + + /** we don't check to provide a fast path + for (Py_ssize_t k=0; k<weights_.size(); ++k) { + + if (weights_[k].extent(1) != biases_[k].extent(0)) { + Py_ssize_t cols = weights_[k].extent(1); + Py_ssize_t elems = biases_[k].extent(0); + PyErr_Format(PyExc_RuntimeError, "unroll, when applied to individual weights and biases, requires these iterables to have the same length and that the number of columns in each weight matrix matches the number of biases for the same layer, but in layer %" PY_FORMAT_SIZE_T "d, the weight matrix has %" PY_FORMAT_SIZE_T "d columns and the bias vector has %" PY_FORMAT_SIZE_T "d elements", k, cols, elems); + return 0; + } + + if (k < (weights_.size()-1)) { + if (weights_[k].extent(1) != weights_[k+1].extent(0)) { + Py_ssize_t cols = weights_[k].extent(1); + Py_ssize_t rows = weights_[k+1].extent(0); + PyErr_Format(PyExc_RuntimeError, "unroll, when applied to individual weights and biases, requires that weights of successive layers have matching number of inputs and outputs, but the weight matrix in layer %" PY_FORMAT_SIZE_T "d, has %" PY_FORMAT_SIZE_T "d columns (outputs) and in layer %" PY_FORMAT_SIZE_T "d, %" PY_FORMAT_SIZE_T "d rows (inputs)", k, cols, k+1, rows); + return 0; + } + } + + } + **/ + + Py_ssize_t nb_parameters = + bob::learn::mlp::detail::getNbParameters(weights_, biases_); + + if (parameters) { + + if (parameters->type_num != NPY_FLOAT64 || + parameters->ndim != 1 || + parameters->shape[0] != nb_parameters) { + PyErr_Format(PyExc_TypeError, "function only supports 1D 64-bit float arrays with shape (%" PY_FORMAT_SIZE_T "d,) for output array `parameters', but you passed a %" PY_FORMAT_SIZE_T"dD %s array with shape (%" PY_FORMAT_SIZE_T "d,)", nb_parameters, parameters->ndim, PyBlitzArray_TypenumAsString(parameters->type_num), parameters->shape[0]); + return 0; + } + + } + + else { + + //allocate space for the parameters + parameters = (PyBlitzArrayObject*)PyBlitzArray_SimpleNew(NPY_FLOAT64, 1, &nb_parameters); + if (!parameters) return 0; + parameters_ = make_safe(parameters); + + } + + /** all basic checks are done, can execute the function now **/ + try { + bob::learn::mlp::unroll(weights_, biases_, + *PyBlitzArrayCxx_AsBlitz<double,1>(parameters)); + } + catch (std::exception& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return 0; + } + catch (...) { + PyErr_SetString(PyExc_RuntimeError, "cannot unroll machine parameters: unknown exception caught"); + return 0; + } + + Py_INCREF(parameters); + return PyBlitzArray_NUMPY_WRAP((PyObject*)parameters); + +} + +PyObject* unroll(PyObject*, PyObject* args, PyObject* kwds) { + + PyObject* arg = 0; ///< borrowed (don't delete) + if (PyTuple_Size(args)) arg = PyTuple_GET_ITEM(args, 0); + else { + PyObject* tmp = PyDict_Values(kwds); + auto tmp_ = make_safe(tmp); + arg = PyList_GET_ITEM(tmp, 0); + } + + if (PyBobLearnMLPMachine_Check(arg)) return unroll_from_machine(args, kwds); + return unroll_from_values(args, kwds); + +} + +static PyObject* roll_to_machine(PyObject* args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"machine", "parameters", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyObject* machine = 0; + PyBlitzArrayObject* parameters = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O&", kwlist, + &PyBobLearnMLPMachine_Type, &machine, + &PyBlitzArray_Converter, ¶meters + )) return 0; + + auto machine_ = reinterpret_cast<PyBobLearnMLPMachineObject*>(machine); + auto parameters_ = make_safe(parameters); + Py_ssize_t nb_parameters = + bob::learn::mlp::detail::getNbParameters(*machine_->cxx); + + if (parameters->type_num != NPY_FLOAT64 || + parameters->ndim != 1 || + parameters->shape[0] != nb_parameters) { + PyErr_Format(PyExc_TypeError, "function only supports 1D 64-bit float arrays with shape (%" PY_FORMAT_SIZE_T "d,) for input array `parameters', but you passed a %" PY_FORMAT_SIZE_T"dD %s array with shape (%" PY_FORMAT_SIZE_T "d,)", nb_parameters, parameters->ndim, PyBlitzArray_TypenumAsString(parameters->type_num), parameters->shape[0]); + return 0; + } + + /** all basic checks are done, can execute the function now **/ + try { + bob::learn::mlp::roll(*machine_->cxx, + *PyBlitzArrayCxx_AsBlitz<double,1>(parameters)); + } + catch (std::exception& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return 0; + } + catch (...) { + PyErr_SetString(PyExc_RuntimeError, "cannot roll machine parameters: unknown exception caught"); + return 0; + } + + Py_RETURN_NONE; + +} + +static PyObject* roll_to_values(PyObject* args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"weights", "biases", "parameters", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyObject* weights = 0; + PyObject* biases = 0; + PyBlitzArrayObject* parameters = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO&", kwlist, + &weights, &biases, + &PyBlitzArray_Converter, ¶meters + )) return 0; + + auto parameters_ = make_safe(parameters); + + //converts weights and biases + std::vector<blitz::Array<double,2>> weights_; + int status = convert_tuple<2>("roll", "weights", weights, weights_); + if (status < 0) return 0; + + std::vector<blitz::Array<double,1>> biases_; + status = convert_tuple<1>("roll", "biases", biases, biases_); + if (status < 0) return 0; + + //checks + if (weights_.size() != biases_.size()) { + PyErr_Format(PyExc_RuntimeError, "roll, when applied to individual weights and biases, requires these iterables to have the same length but len(weights) = %" PY_FORMAT_SIZE_T"d != len(biases) = %" PY_FORMAT_SIZE_T "d", weights_.size(), biases_.size()); + return 0; + } + + /** we don't check to provide a fast path + for (Py_ssize_t k=0; k<weights_.size(); ++k) { + + if (weights_[k].extent(1) != biases_[k].extent(0)) { + Py_ssize_t cols = weights_[k].extent(1); + Py_ssize_t elems = biases_[k].extent(0); + PyErr_Format(PyExc_RuntimeError, "roll, when applied to individual weights and biases, requires these iterables to have the same length and that the number of columns in each weight matrix matches the number of biases for the same layer, but in layer %" PY_FORMAT_SIZE_T "d, the weight matrix has %" PY_FORMAT_SIZE_T "d columns and the bias vector has %" PY_FORMAT_SIZE_T "d elements", k, cols, elems); + return 0; + } + + if (k < (weights_.size()-1)) { + if (weights_[k].extent(1) != weights_[k+1].extent(0)) { + Py_ssize_t cols = weights_[k].extent(1); + Py_ssize_t rows = weights_[k+1].extent(0); + PyErr_Format(PyExc_RuntimeError, "roll, when applied to individual weights and biases, requires that weights of successive layers have matching number of inputs and outputs, but the weight matrix in layer %" PY_FORMAT_SIZE_T "d, has %" PY_FORMAT_SIZE_T "d columns (outputs) and in layer %" PY_FORMAT_SIZE_T "d, %" PY_FORMAT_SIZE_T "d rows (inputs)", k, cols, k+1, rows); + return 0; + } + } + + } + **/ + + Py_ssize_t nb_parameters = + bob::learn::mlp::detail::getNbParameters(weights_, biases_); + + if (parameters->type_num != NPY_FLOAT64 || + parameters->ndim != 1 || + parameters->shape[0] != nb_parameters) { + PyErr_Format(PyExc_TypeError, "function only supports 1D 64-bit float arrays with shape (%" PY_FORMAT_SIZE_T "d,) for input array `parameters', but you passed a %" PY_FORMAT_SIZE_T"dD %s array with shape (%" PY_FORMAT_SIZE_T "d,)", nb_parameters, parameters->ndim, PyBlitzArray_TypenumAsString(parameters->type_num), parameters->shape[0]); + return 0; + } + + /** all basic checks are done, can execute the function now **/ + try { + bob::learn::mlp::roll(weights_, biases_, + *PyBlitzArrayCxx_AsBlitz<double,1>(parameters)); + } + catch (std::exception& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return 0; + } + catch (...) { + PyErr_SetString(PyExc_RuntimeError, "cannot roll machine parameters: unknown exception caught"); + return 0; + } + + Py_RETURN_NONE; + +} + +PyObject* roll(PyObject*, PyObject* args, PyObject* kwds) { + + PyObject* arg = 0; ///< borrowed (don't delete) + if (PyTuple_Size(args)) arg = PyTuple_GET_ITEM(args, 0); + else { + PyObject* tmp = PyDict_Values(kwds); + auto tmp_ = make_safe(tmp); + arg = PyList_GET_ITEM(tmp, 0); + } + + if (PyBobLearnMLPMachine_Check(arg)) return roll_to_machine(args, kwds); + return roll_to_values(args, kwds); + +} + +static PyObject* nb_param_from_machine(PyObject* args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"machine", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyObject* machine = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist, + &PyBobLearnMLPMachine_Type, &machine)) return 0; + + auto machine_ = reinterpret_cast<PyBobLearnMLPMachineObject*>(machine); + return Py_BuildValue("n", + bob::learn::mlp::detail::getNbParameters(*machine_->cxx)); + +} + +static PyObject* nb_param_from_values(PyObject* args, PyObject* kwds) { + + /* Parses input arguments in a single shot */ + static const char* const_kwlist[] = {"weights", "biases", 0}; + static char** kwlist = const_cast<char**>(const_kwlist); + + PyObject* weights = 0; + PyObject* biases = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, + &weights, &biases)) return 0; + + //converts weights and biases + std::vector<blitz::Array<double,2>> weights_; + int status = convert_tuple<2>("unroll", "weights", weights, weights_); + if (status < 0) return 0; + + std::vector<blitz::Array<double,1>> biases_; + status = convert_tuple<1>("unroll", "biases", biases, biases_); + if (status < 0) return 0; + + //checks + if (weights_.size() != biases_.size()) { + PyErr_Format(PyExc_RuntimeError, "unroll, when applied to individual weights and biases, requires these iterables to have the same length but len(weights) = %" PY_FORMAT_SIZE_T"d != len(biases) = %" PY_FORMAT_SIZE_T "d", weights_.size(), biases_.size()); + return 0; + } + + /** we don't check to provide a fast path + for (Py_ssize_t k=0; k<weights_.size(); ++k) { + + if (weights_[k].extent(1) != biases_[k].extent(0)) { + Py_ssize_t cols = weights_[k].extent(1); + Py_ssize_t elems = biases_[k].extent(0); + PyErr_Format(PyExc_RuntimeError, "MLP parameter counting, when applied to individual weights and biases, requires these iterables to have the same length and that the number of columns in each weight matrix matches the number of biases for the same layer, but in layer %" PY_FORMAT_SIZE_T "d, the weight matrix has %" PY_FORMAT_SIZE_T "d columns and the bias vector has %" PY_FORMAT_SIZE_T "d elements", k, cols, elems); + return 0; + } + + if (k < (weights_.size()-1)) { + if (weights_[k].extent(1) != weights_[k+1].extent(0)) { + Py_ssize_t cols = weights_[k].extent(1); + Py_ssize_t rows = weights_[k+1].extent(0); + PyErr_Format(PyExc_RuntimeError, "MLP parameter counting, when applied to individual weights and biases, requires that weights of successive layers have matching number of inputs and outputs, but the weight matrix in layer %" PY_FORMAT_SIZE_T "d, has %" PY_FORMAT_SIZE_T "d columns (outputs) and in layer %" PY_FORMAT_SIZE_T "d, %" PY_FORMAT_SIZE_T "d rows (inputs)", k, cols, k+1, rows); + return 0; + } + } + + } + **/ + + return Py_BuildValue("n", + bob::learn::mlp::detail::getNbParameters(weights_, biases_)); + +} + +PyObject* number_of_parameters(PyObject*, PyObject* args, PyObject* kwds) { + + PyObject* arg = 0; ///< borrowed (don't delete) + if (PyTuple_Size(args)) arg = PyTuple_GET_ITEM(args, 0); + else { + PyObject* tmp = PyDict_Values(kwds); + auto tmp_ = make_safe(tmp); + arg = PyList_GET_ITEM(tmp, 0); + } + + if (PyBobLearnMLPMachine_Check(arg)) return nb_param_from_machine(args, kwds); + return nb_param_from_values(args, kwds); + +} diff --git a/xbob/learn/mlp/rprop.cpp b/xbob/learn/mlp/rprop.cpp index 43f58d5bbe2f2755d4c24759456c28801149ed26..4c79cda8fa3c3e97e812a3cfa0ea8edb50817bd7 100644 --- a/xbob/learn/mlp/rprop.cpp +++ b/xbob/learn/mlp/rprop.cpp @@ -319,8 +319,8 @@ static int PyBobLearnMLPRProp_setPreviousDerivatives (PyBobLearnMLPRPropObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,2>> bzvec; - int retval = convert_tuple<2>((PyObject*)self, s_previous_derivatives_str, - o, bzvec); + int retval = convert_tuple<2>(Py_TYPE(self)->tp_name, + s_previous_derivatives_str, o, bzvec); if (retval < 0) return retval; try { @@ -355,7 +355,7 @@ static int PyBobLearnMLPRProp_setPreviousBiasDerivatives (PyBobLearnMLPRPropObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,1>> bzvec; - int retval = convert_tuple<1>((PyObject*)self, + int retval = convert_tuple<1>(Py_TYPE(self)->tp_name, s_previous_bias_derivatives_str, o, bzvec); if (retval < 0) return retval; @@ -388,7 +388,7 @@ static int PyBobLearnMLPRProp_setDeltas (PyBobLearnMLPRPropObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,2>> bzvec; - int retval = convert_tuple<2>((PyObject*)self, s_deltas_str, o, bzvec); + int retval = convert_tuple<2>(Py_TYPE(self)->tp_name, s_deltas_str, o, bzvec); if (retval < 0) return retval; try { @@ -420,7 +420,8 @@ static int PyBobLearnMLPRProp_setBiasDeltas (PyBobLearnMLPRPropObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,1>> bzvec; - int retval = convert_tuple<1>((PyObject*)self, s_bias_deltas_str, o, bzvec); + int retval = convert_tuple<1>(Py_TYPE(self)->tp_name, s_bias_deltas_str, o, + bzvec); if (retval < 0) return retval; try { diff --git a/xbob/learn/mlp/test_roll.py b/xbob/learn/mlp/test_roll.py index 825aaf0cd1502b7049d29d05c11aba7d0bf434e8..b502024913a3a5c33019518e837ce5569de40861 100644 --- a/xbob/learn/mlp/test_roll.py +++ b/xbob/learn/mlp/test_roll.py @@ -9,9 +9,9 @@ """ import numpy -from . import Machine, roll, unroll +from . import Machine, roll, unroll, number_of_parameters -def test_roll_1(self): +def test_roll_1(): m = Machine((10,3,8,5)) m.randomize() @@ -26,15 +26,15 @@ def test_roll_1(self): roll(m3, vec) assert m == m3 -def test_roll_2(self): +def test_roll_2(): w = [numpy.array([[2,3.]]), numpy.array([[2,3,4.],[5,6,7]])] - b = [numpy.array([5.,]), numpy.array([7,8.])] - vec = numpy.ndarray(11, numpy.float64) + b = [numpy.array([5.]), numpy.array([7.,8.])] + vec = numpy.ndarray(number_of_parameters(w, b), numpy.float64) unroll(w, b, vec) w_ = [numpy.ndarray((1,2), numpy.float64), numpy.ndarray((2,3), numpy.float64)] - b_ = [numpy.ndarray(1, numpy.float64), numpy.ndarray(2, numpy.float64)] + b_ = [numpy.ndarray((1,), numpy.float64), numpy.ndarray((2,), numpy.float64)] roll(w_, b_, vec) assert (w_[0] == w[0]).all() diff --git a/xbob/learn/mlp/trainer.cpp b/xbob/learn/mlp/trainer.cpp index 83956cb40c0a4b39c6fbfa836314e3c80c929357..f5c8fe473694314bf647f6141fbd489edd8c1e63 100644 --- a/xbob/learn/mlp/trainer.cpp +++ b/xbob/learn/mlp/trainer.cpp @@ -274,7 +274,7 @@ static int PyBobLearnMLPTrainer_setError (PyBobLearnMLPTrainerObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,2>> bzvec; - int retval = convert_tuple<2>((PyObject*)self, s_error_str, o, bzvec); + int retval = convert_tuple<2>(Py_TYPE(self)->tp_name, s_error_str, o, bzvec); if (retval < 0) return retval; try { @@ -306,7 +306,7 @@ static int PyBobLearnMLPTrainer_setOutput (PyBobLearnMLPTrainerObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,2>> bzvec; - int retval = convert_tuple<2>((PyObject*)self, s_output_str, o, bzvec); + int retval = convert_tuple<2>(Py_TYPE(self)->tp_name, s_output_str, o, bzvec); if (retval < 0) return retval; try { @@ -340,7 +340,8 @@ static int PyBobLearnMLPTrainer_setDerivatives (PyBobLearnMLPTrainerObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,2>> bzvec; - int retval = convert_tuple<2>((PyObject*)self, s_derivatives_str, o, bzvec); + int retval = convert_tuple<2>(Py_TYPE(self)->tp_name, s_derivatives_str, o, + bzvec); if (retval < 0) return retval; try { @@ -374,7 +375,8 @@ static int PyBobLearnMLPTrainer_setBiasDerivatives (PyBobLearnMLPTrainerObject* self, PyObject* o, void* /*closure*/) { std::vector<blitz::Array<double,1>> bzvec; - int retval = convert_tuple<1>((PyObject*)self, s_bias_derivatives_str, o, bzvec); + int retval = convert_tuple<1>(Py_TYPE(self)->tp_name, s_bias_derivatives_str, + o, bzvec); if (retval < 0) return retval; try { diff --git a/xbob/learn/mlp/utils.h b/xbob/learn/mlp/utils.h index e194faa3a0946117dcd42a0cc238375a1434fe84..1497b61dfc5a6556b34167ab4d90c86625da567f 100644 --- a/xbob/learn/mlp/utils.h +++ b/xbob/learn/mlp/utils.h @@ -15,6 +15,13 @@ #include <xbob.blitz/cleanup.h> #include <xbob.learn.mlp/api.h> +/** + * Converts a vector of blitz::Array<double,N> into a python iterable over + * arrays. + * + * Returns NULL if a problem occurs, the PyObject* resulting from the + * conversion if all is good. + */ template <int N> PyObject* convert_vector(const std::vector<blitz::Array<double,N>>& v) { PyObject* retval = PyTuple_New(v.size()); @@ -29,12 +36,18 @@ PyObject* convert_vector(const std::vector<blitz::Array<double,N>>& v) { return retval; } +/** + * Converts an iterable of pythonic arrays into a vector of + * blitz::Array<double,N>, checking for errors. + * + * Returns -1 if a problem occurs, 0 if all is good. + */ template <int N> -int convert_tuple(PyObject* self, const char* attr, +int convert_tuple(const char* name, const char* attr, PyObject* o, std::vector<blitz::Array<double,N>>& seq) { if (!PyIter_Check(o) && !PySequence_Check(o)) { - PyErr_Format(PyExc_TypeError, "setting attribute `%s' of `%s' requires an iterable, but you passed `%s' which does not implement the iterator protocol", Py_TYPE(self)->tp_name, attr, Py_TYPE(o)->tp_name); + PyErr_Format(PyExc_TypeError, "parameter `%s' of `%s' requires an iterable, but you passed `%s' which does not implement the iterator protocol", name, attr, Py_TYPE(o)->tp_name); return -1; } @@ -51,12 +64,12 @@ int convert_tuple(PyObject* self, const char* attr, PyBlitzArrayObject* bz = 0; if (!PyBlitzArray_Converter(item, &bz)) { - PyErr_Format(PyExc_TypeError, "`%s' (while setting `%s') could not convert object of type `%s' at position %" PY_FORMAT_SIZE_T "d of input sequence into an array - check your input", Py_TYPE(self)->tp_name, attr, Py_TYPE(item)->tp_name, seq.size()); + PyErr_Format(PyExc_TypeError, "`%s' (while reading `%s') could not convert object of type `%s' at position %" PY_FORMAT_SIZE_T "d of input sequence into an array - check your input", name, attr, Py_TYPE(item)->tp_name, seq.size()); return -1; } if (bz->ndim != N || bz->type_num != NPY_FLOAT64) { - PyErr_Format(PyExc_TypeError, "`%s' only supports 2D 64-bit float arrays for attribute `%s' (or any other object coercible to that), but at position %" PY_FORMAT_SIZE_T "d I have found an object with %" PY_FORMAT_SIZE_T "d dimensions and with type `%s' which is not compatible - check your input", Py_TYPE(self)->tp_name, attr, seq.size(), bz->ndim, PyBlitzArray_TypenumAsString(bz->type_num)); + PyErr_Format(PyExc_TypeError, "`%s' only supports 2D 64-bit float arrays for parameter `%s' (or any other object coercible to that), but at position %" PY_FORMAT_SIZE_T "d I have found an object with %" PY_FORMAT_SIZE_T "d dimensions and with type `%s' which is not compatible - check your input", name, attr, seq.size(), bz->ndim, PyBlitzArray_TypenumAsString(bz->type_num)); Py_DECREF(bz); return -1; }