# Layer Mechanics¶

ML-Ensemble is designed to provide an easy user interface. But it is also designed to be extremely flexible, all the wile providing maximum concurrency at minimal memory consumption. The lower-level API that builds the ensemble and manages the computations is constructed in as modular a fashion as possible.

The low-level API introduces a computational graph-like environment that you can directly exploit to gain further control over your ensemble. In fact, building your ensemble through the low-level API is almost as straight forward as using the high-level API. In this tutorial, we will walk through how to use the Group and Layer classes to fit several learners.

Suppose we want to fit several learners. The :ref: learner tutorial <learner_tutorial showed us how to fit a single learner, and so one approach would be to simple iterate over our learners and fit them one at a time. This however is a very slow approach since we don’t exploit the fact that learners can be trained in parallel. Moreover, any type of aggregation, like putting all predictions into an array, would have to be done manually.

## The Layer API¶

To parallelize the implementation, we can use the Layer class. A layer is a handle that will run any number of Group instances attached to it in parallel. Each group in turn is a wrapper around a indexer-transformers-estimators triplet.

### Basics¶

So, to fit our two learners in parallel, we first need a Group object to handle them.

from mlens.parallel import Layer, Group, make_group, run
from mlens.utils.dummy import OLS, Scale
from mlens.index import FoldIndex

indexer = FoldIndex(folds=2)
group = make_group(indexer, [OLS(1), OLS(2)], None)


This group object is now a complete description of how to fit our two learners using the prescribed indexing method.

To train the estimators, we need feed the group to a Layer instance:

import numpy as np

np.random.seed(2)

X = np.arange(20).reshape(10, 2)
y = np.random.rand(10)

layer = Layer(stack=group)

print(
run(layer, 'fit', X, y, return_preds=True)
)


Out:

[[ 1.3665537  2.3665535]
[ 5.363353  10.363353 ]
[ 9.360152  18.360151 ]
[13.356951  26.35695  ]
[17.35375   34.35375  ]
[21.486897  42.486897 ]
[25.524712  50.52471  ]
[29.562525  58.562527 ]
[33.60034   66.60034  ]
[37.638153  74.63815  ]]


To use some preprocessing before fitting the estimators, we can use the transformers argument when creating our group:

group = make_group(indexer, [OLS(1), OLS(2)], [Scale()])

layer = Layer(stack=group)

print(
run(layer, 'fit', X, y, return_preds=True)
)


Out:

[[-27.977594 -55.977592]
[-23.980795 -47.980793]
[-19.983995 -39.983997]
[-15.987196 -31.987196]
[-11.990397 -23.990396]
[ 12.113442  24.113443]
[ 16.151257  32.151257]
[ 20.189072  40.18907 ]
[ 24.226885  48.226887]
[ 28.2647    56.264698]]


If we want our estimators two have different preprocessing, we can easily achieve this either by specifying different cases when making the group, or by making two separate groups. In the first case:

group = make_group(
indexer,
{'case-1': [OLS(1)], 'case-2': [OLS(2)]},
{'case-1': [Scale()], 'case-2': []}
)

layer = Layer(stack=group)

print(
run(layer, 'fit', X, y, return_preds=True)
)


Out:

[[-27.977594    2.3665535]
[-23.980795   10.363353 ]
[-19.983995   18.360151 ]
[-15.987196   26.35695  ]
[-11.990397   34.35375  ]
[ 12.113442   42.486897 ]
[ 16.151257   50.52471  ]
[ 20.189072   58.562527 ]
[ 24.226885   66.60034  ]
[ 28.2647     74.63815  ]]


In the latter case:

groups = [
make_group(indexer, OLS(1), Scale()), make_group(indexer, OLS(2), None)
]

layer = Layer(stack=groups)

print(
run(layer, 'fit', X, y, return_preds=True)
)


Out:

[[-27.977594    2.3665535]
[-23.980795   10.363353 ]
[-19.983995   18.360151 ]
[-15.987196   26.35695  ]
[-11.990397   34.35375  ]
[ 12.113442   42.486897 ]
[ 16.151257   50.52471  ]
[ 20.189072   58.562527 ]
[ 24.226885   66.60034  ]
[ 28.2647     74.63815  ]]


Which method to prefer depends on the application, but generally, it is preferable to put all transformers and all estimators belonging to a given indexing strategy into one group instance as it is easier to separate groups based on indexer and using cases to distinguish between different preprocessing pipelines.

Now, suppose we want to do something more exotic, like using different indexing strategies for different estimators. This can easily be achieved by creating groups for each indexing strategy we want:

groups = [
make_group(FoldIndex(2), OLS(1), Scale()),
make_group(FoldIndex(4), OLS(2), None)
]

layer = Layer(stack=groups)

print(
run(layer, 'fit', X, y, return_preds=True)
)


Out:

[[-27.977594    2.4661984]
[-23.980795   10.449842 ]
[-19.983995   18.433487 ]
[-15.987196   26.33962  ]
[-11.990397   34.341675 ]
[ 12.113442   42.343727 ]
[ 16.151257   50.33181  ]
[ 20.189072   58.324955 ]
[ 24.226885   66.458244 ]
[ 28.2647     74.47614  ]]


Some care needs to be taken here: if indexing strategies do not return the same number of rows, the output array will be zero-padded.

from mlens.index import BlendIndex

groups = [
make_group(FoldIndex(2), OLS(1), None),
make_group(BlendIndex(0.5), OLS(1), None)
]

layer = Layer(stack=groups)
print(
run(layer, 'fit', X, y, return_preds=True)
)


Out:

[[ 1.3665537  0.       ]
[ 5.363353   0.       ]
[ 9.360152   0.       ]
[13.356951   0.       ]
[17.35375    0.       ]
[21.486897  21.486897 ]
[25.524712  25.524712 ]
[29.562525  29.562525 ]
[33.60034   33.60034  ]
[37.638153  37.638153 ]]


Note that even if mlens indexer output different shapes, they preserve row indexing to ensure predictions are consistently mapped to their respective input. If you build a custom indexer, make sure that it uses a strictly sequential (with respect to row indexing) partitioning strategy.

### Layer features¶

A layer does not have to be specified all in one go; you can instantiate a layer and push and pop to its stack.

layer = Layer()
group = make_group(FoldIndex(4), OLS(), None)
layer.push(group)


If you push or pop to the stack, you must call fit before you can use the layer for prediction.

run(layer, 'fit', X, y)

group = make_group(FoldIndex(2), OLS(1), None)
layer.push(group)

try:
run(layer, 'predict', X, y)
except Exception as exc:
print("Error: %s" % str(exc))


Out:

Error: Layer instance (layer-6) not fitted.


The Layer class can print the progress of a job, as well as inspect data collected during the job. Note that the printouts of the layer does not take group membership into account.

from mlens.metrics import rmse

layer = Layer()
group1 = make_group(
indexer,
{'case-1': [OLS(1)], 'case-2': [OLS(2)]},
{'case-1': [Scale()], 'case-2': []},
learner_kwargs={'scorer': rmse}
)

layer.push(group1)

run(layer, 'fit', X, y, return_preds=True)
print()
print("Collected data:")
print(layer.data)


Out:

Collected data:
score-m  score-s  ft-m  ft-s  pt-m  pt-s
case-1  ols-1      20.88     0.23  0.00  0.00  0.00  0.00
ols-2      40.27    19.05  0.00  0.00  0.00  0.00


Total running time of the script: ( 0 minutes 2.157 seconds)

Generated by Sphinx-Gallery