Source code for rep.estimators.neurolab

"""
These classes are wrappers for the `Neurolab library <https://pythonhosted.org/neurolab/lib.html>`_ --- a neural network python library.

.. warning:: To make neurolab reproducible we change global random seed

    ::

        numpy.random.seed(42)
"""
# Copyright 2014-2015 Yandex LLC and contributors <https://yandex.com/>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# <http://www.apache.org/licenses/LICENSE-2.0>
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import division, print_function, absolute_import
from abc import ABCMeta
from copy import deepcopy

import neurolab as nl
import numpy
import scipy

from .interface import Classifier, Regressor
from .utils import check_inputs, check_scaler, one_hot_transform, remove_first_line


__author__ = 'Vlad Sterzhanov, Alex Rogozhnikov, Tatiana Likhomanenko'
__all__ = ['NeurolabClassifier', 'NeurolabRegressor']

NET_TYPES = {'feed-forward': nl.net.newff,
             'competing-layer': nl.net.newc,
             'learning-vector': nl.net.newlvq,
             'elman-recurrent': nl.net.newelm,
             'hemming-recurrent': nl.net.newhem,
             'hopfield-recurrent': nl.net.newhop
             }

NET_PARAMS = ('minmax', 'cn', 'layers', 'transf', 'target',
              'max_init', 'max_iter', 'delta', 'cn0', 'pc')

BASIC_PARAMS = ('layers', 'net_type', 'trainf', 'initf', 'scaler', 'random_state')

# Instead of a single layer use feed-forward.
CANT_CLASSIFY = ('hopfield-recurrent', 'competing-layer', 'hemming-recurrent')
CANT_DO_REGRESSION = ('hopfield-recurrent', )


class NeurolabBase(object):
    """ A base class for estimators from the Neurolab library.

    :param features: features used in training
    :type features: list[str] or None
    :param list[int] layers: sequence, number of units inside each **hidden** layer.
    :param string net_type: type of the network; possible values are:

        * `feed-forward`
        * `competing-layer`
        * `learning-vector`
        * `elman-recurrent`
        * `hemming-recurrent`

    :param initf: layer initializers
    :type initf: anything implementing call(layer), e.g. neurolab.init.* or list[neurolab.init.*] of shape [n_layers]
    :param trainf: net training function; default value depends on the type of a network
    :param scaler: transformer which is applied to the input samples. If it is False, scaling will not be used
    :type scaler: str or sklearn-like transformer or False
    :param random_state: this parameter is ignored and is added for uniformity.
    :param dict kwargs: additional arguments to net `__init__`, varies with different `net_types`

    .. seealso:: `Supported training functions and their parameters <https://pythonhosted.org/neurolab/lib.html>`_
    """

    __metaclass__ = ABCMeta

    def __init__(self,
                 features=None,
                 layers=(10,),
                 net_type='feed-forward',
                 initf=nl.init.init_rand,
                 trainf=None,
                 scaler='standard',
                 random_state=None,
                 **other_params):
        self.features = list(features) if features is not None else features
        self.layers = list(layers)
        self.trainf = trainf
        self.initf = initf
        self.net_type = net_type
        self.scaler = scaler
        self.random_state = random_state

        self.net = None
        self.train_params = {}
        self.net_params = {}
        self.set_params(**other_params)

    def _is_fitted(self):
        """
        Check if the estimator is fitted or not.

        :rtype: bool
        """
        return self.net is not None

    def set_params(self, **params):
        """
        Set the parameters of the estimator.

        :param dict params: parameters to be set in the model
        """
        for name, value in params.items():
            if name.startswith("scaler__"):
                assert hasattr(self.scaler, 'set_params'), \
                    "Trying to set {} without scaler".format(name)
                self.scaler.set_params(**{name[len("scaler__"):]: value})
            elif name.startswith('layers__'):
                index = int(name[len('layers__'):])
                self.layers[index] = value
            elif name.startswith('initf__'):
                index = int(name[len('initf__'):])
                self.initf[index] = value
            elif name in NET_PARAMS:
                self.net_params[name] = value
            elif name in BASIC_PARAMS:
                setattr(self, name, value)
            else:
                self.train_params[name] = value

    def get_params(self, deep=True):
        """
        Get parameters of the estimator.

        :rtype: dict
        """
        parameters = deepcopy(self.net_params)
        parameters.update(deepcopy(self.train_params))
        for name in BASIC_PARAMS:
            parameters[name] = getattr(self, name)
        return parameters

    def _partial_fit(self, X, y_original, y_train):
        """
        Train the estimator by training the existing estimator again.

        :param pandas.DataFrame X: data shape [n_samples, n_features]
        :param y_train: array-like target, which is always 2-dimensional (one-hot for classification)
        :param y_original: array-like target, which originally was passed to `fit`.
        :return: self
        """
        # magic reproducibilizer
        numpy.random.seed(42)

        if self._is_fitted():
            x_train = self._transform_data(X, y_original, fit=False)
        else:
            x_train = self._transform_data(X, y_original, fit=True)

            # Prepare parameters depending on the network purpose (classification / regression)
            net_params = self._prepare_params(self.net_params, x_train, y_train)

            initializer = self._get_initializer(self.net_type)
            net = initializer(**net_params)

            # To allow similar initf function on all layers
            initf_iterable = self.initf if hasattr(self.initf, '__iter__') else [self.initf] * len(net.layers)
            for layer, init_function in zip(net.layers, initf_iterable):
                layer.initf = init_function
                net.init()

            if self.trainf is not None:
                net.trainf = self.trainf

            self.net = net

        self.net.train(x_train, y_train, **self.train_params)
        return self

    def _activate_on_dataset(self, X):
        """
        Predict data.

        :param pandas.DataFrame X: data to be predicted
        :return: array-like predictions [n_samples, n_targets]
        """
        assert self.net is not None, 'Model is not fitted, prediction is denied'
        transformed_x = self._transform_data(X, fit=False)
        return self.net.sim(transformed_x)

    def _transform_data(self, X, y=None, fit=True):
        """
        Transform input samples by the scaler.

        :param pandas.DataFrame X: input data
        :param y: array-like target
        :param bool fit: true if scaler is not trained yet
        :return: array-like transformed data
        """
        X = self._get_features(X)
        # The following line fights the bug in sklearn < 0.16,
        # most of the transformers there modify X if it is pandas.DataFrame.
        X = numpy.copy(X)
        if fit:
            self.scaler = check_scaler(self.scaler)
            self.scaler.fit(X, y)
        X = self.scaler.transform(X)

        # HACK: neurolab requires all features (even those of predicted objects) to be in [min, max]
        # so this dark magic appeared, seems to work ok for the most reasonable use-cases,
        # while allowing arbitrary inputs.
        return scipy.special.expit(X / 3)

    def _prepare_params(self, net_params, x_train, y_train):
        """
        Set parameters for the neurolab net.

        :param dict net_params: parameters
        :param x_train: array-like training data
        :param y_train: array-like training target
        :return: prepared parameters in the neurolab interface
        """
        net_params = deepcopy(net_params)
        # Network expects features to be [0, 1]-scaled
        net_params['minmax'] = [[0, 1]] * (x_train.shape[1])

        # To unify the layer-description argument with other supported networks
        if 'size' not in net_params:
            net_params['size'] = self.layers
        else:
            if self.layers != (10, ):
                raise ValueError('For neurolab please use either `layers` or `sizes`, not both')

        # Set output layer size
        net_params['size'] = list(net_params['size']) + [y_train.shape[1]]

        # Default parameters for the transfer functions in the networks
        if self.net_type != 'learning-vector':
            if 'transf' not in net_params:
                net_params['transf'] = [nl.trans.TanSig()] * len(net_params['size'])
            if not hasattr(net_params['transf'], '__iter__'):
                net_params['transf'] = [net_params['transf']] * len(net_params['size'])
            net_params['transf'] = list(net_params['transf'])

        return net_params

    @staticmethod
    def _get_initializer(net_type):
        """
        Return a neurolab net type object.

        :param str net_type: net type
        :return: a neurolab object corresponding to the net type
        """
        if net_type not in NET_TYPES:
            raise AttributeError("Got unexpected network type: '{}'".format(net_type))
        return NET_TYPES.get(net_type)


[docs]class NeurolabClassifier(NeurolabBase, Classifier): __doc__ = "Implements a classification model from the Neurolab library. \n" + remove_first_line(NeurolabBase.__doc__)
[docs] def fit(self, X, y): """ Train a classification model on the data. :param pandas.DataFrame X: data of shape [n_samples, n_features] :param y: labels of samples --- array-like of shape [n_samples] :return: self """ # erasing results of the previous training self.net = None return self.partial_fit(X, y)
[docs] def partial_fit(self, X , y): """ Additional training of the classifier. :param pandas.DataFrame X: data of shape [n_samples, n_features] :param y: labels of samples, array-like of shape [n_samples] :return: self """ assert self.net_type not in CANT_CLASSIFY, 'Network type does not support classification' X, y, _ = check_inputs(X, y, None) if not self._is_fitted(): self._set_classes(y) y_train = one_hot_transform(y, n_classes=len(self.classes_)) * 0.98 + 0.01 return self._partial_fit(X, y, y_train)
[docs] def predict_proba(self, X): return self._activate_on_dataset(X)
predict_proba.__doc__ = Classifier.predict_proba.__doc__
[docs] def staged_predict_proba(self, X): """ .. warning:: This is not supported in the Neurolab (**AttributeError** will be thrown) """ raise AttributeError("'staged_predict_proba' is not supported by the Neurolab networks")
def _prepare_params(self, params, x_train, y_train): net_params = super(NeurolabClassifier, self)._prepare_params(params, x_train, y_train) # Classification networks should have SoftMax as the transfer function on output layer net_params['transf'][-1] = nl.trans.SoftMax() return net_params _prepare_params.__doc__ = NeurolabBase._prepare_params.__doc__
[docs]class NeurolabRegressor(NeurolabBase, Regressor): __doc__ = "Implements a regression model from the Neurolab library. \n" + remove_first_line(NeurolabBase.__doc__)
[docs] def fit(self, X, y): """ Train a regression model on the data. :param pandas.DataFrame X: data of shape [n_samples, n_features] :param y: values for samples --- array-like of shape [n_samples] :return: self """ # erasing results of previous training self.net = None return self.partial_fit(X, y)
[docs] def partial_fit(self, X , y): """ Additional training of the regressor. :param pandas.DataFrame X: data of shape [n_samples, n_features] :param y: values for samples, array-like of shape [n_samples] :return: self """ if self.net_type in CANT_DO_REGRESSION: raise RuntimeError('Network type does not support regression') X, y, _ = check_inputs(X, y, None, allow_multiple_targets=True) y_train = y.reshape(len(y), 1 if len(y.shape) == 1 else y.shape[1]) return self._partial_fit(X, y, y_train)
[docs] def predict(self, X): modeled = self._activate_on_dataset(X) return modeled if modeled.shape[1] != 1 else numpy.ravel(modeled)
predict.__doc__ = Regressor.predict.__doc__
[docs] def staged_predict(self, X): """ .. warning:: This is not supported in the Neurolab (**AttributeError** will be thrown) """ raise AttributeError("'staged_predict' is not supported by the Neurolab networks")
def _prepare_params(self, params, x_train, y_train): net_params = super(NeurolabRegressor, self)._prepare_params(params, x_train, y_train) net_params['transf'][-1] = nl.trans.PureLin() return net_params _prepare_params.__doc__ = NeurolabBase._prepare_params.__doc__