From 1da8fb2c3ab6e9cf345e9fe34720e7d42d8b106f Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.dos.anjos@gmail.com>
Date: Wed, 21 May 2014 13:18:35 +0200
Subject: [PATCH] Finished implementing MLP support (added roll/unroll)

---
 setup.py                    |   9 +-
 xbob/learn/mlp/backprop.cpp |   6 +-
 xbob/learn/mlp/cxx/roll.cpp |  12 --
 xbob/learn/mlp/machine.cpp  |   7 +-
 xbob/learn/mlp/main.cpp     | 187 +++++++++++++++-
 xbob/learn/mlp/roll.cpp     | 411 ++++++++++++++++++++++++++++++++++++
 xbob/learn/mlp/rprop.cpp    |  11 +-
 xbob/learn/mlp/test_roll.py |  12 +-
 xbob/learn/mlp/trainer.cpp  |  10 +-
 xbob/learn/mlp/utils.h      |  21 +-
 10 files changed, 643 insertions(+), 43 deletions(-)
 create mode 100644 xbob/learn/mlp/roll.cpp

diff --git a/setup.py b/setup.py
index b7c3e4d..bc32e78 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 6f77625..d9462de 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 7c7b7de..d76de0a 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 5d36bc1..abdac46 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 b330cfc..20f94b0 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 0000000..7155c5d
--- /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, &parameters
+        )) 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, &parameters
+        )) 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, &parameters
+        )) 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, &parameters
+        )) 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 43f58d5..4c79cda 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 825aaf0..b502024 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 83956cb..f5c8fe4 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 e194faa..1497b61 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;
     }
-- 
GitLab