scikit-learn/sklearn/ensemble/voting_classifier.py

346 lines
12 KiB
Python
Raw Normal View History

2015-04-21 07:27:30 +08:00
"""
Soft Voting/Majority Rule classifier.
This module contains a Soft Voting/Majority Rule classifier for
classification estimators.
"""
2015-05-08 16:10:47 +08:00
# Authors: Sebastian Raschka <se.raschka@gmail.com>,
# Gilles Louppe <g.louppe@gmail.com>
2015-04-21 07:27:30 +08:00
#
2016-04-01 08:25:31 +08:00
# License: BSD 3 clause
2015-04-21 07:27:30 +08:00
2015-05-08 16:10:47 +08:00
import numpy as np
2015-04-21 07:27:30 +08:00
from ..base import ClassifierMixin
from ..base import TransformerMixin
from ..base import clone
from ..preprocessing import LabelEncoder
from ..utils._joblib import Parallel, delayed
from ..utils.validation import has_fit_parameter, check_is_fitted
from ..utils.metaestimators import _BaseComposition
from ..utils import Bunch
def _parallel_fit_estimator(estimator, X, y, sample_weight=None):
"""Private function used to fit an estimator within a job."""
if sample_weight is not None:
estimator.fit(X, y, sample_weight=sample_weight)
else:
estimator.fit(X, y)
return estimator
2015-04-21 07:27:30 +08:00
class VotingClassifier(_BaseComposition, ClassifierMixin, TransformerMixin):
2015-05-08 16:10:47 +08:00
"""Soft Voting/Majority Rule classifier for unfitted estimators.
2015-04-21 07:27:30 +08:00
2015-10-22 03:51:04 +08:00
.. versionadded:: 0.17
2015-11-05 08:21:50 +08:00
2015-06-03 12:24:04 +08:00
Read more in the :ref:`User Guide <voting_classifier>`.
2015-04-21 07:27:30 +08:00
Parameters
----------
estimators : list of (string, estimator) tuples
2015-11-05 08:21:50 +08:00
Invoking the ``fit`` method on the ``VotingClassifier`` will fit clones
2015-05-08 16:10:47 +08:00
of those original estimators that will be stored in the class attribute
``self.estimators_``. An estimator can be set to `None` using
``set_params``.
2015-04-21 07:27:30 +08:00
voting : str, {'hard', 'soft'} (default='hard')
2015-05-08 16:10:47 +08:00
If 'hard', uses predicted class labels for majority rule voting.
Else if 'soft', predicts the class label based on the argmax of
2015-12-08 02:13:40 +08:00
the sums of the predicted probabilities, which is recommended for
2015-05-08 16:10:47 +08:00
an ensemble of well-calibrated classifiers.
2015-04-21 07:27:30 +08:00
weights : array-like, shape = [n_classifiers], optional (default=`None`)
2015-12-08 02:13:40 +08:00
Sequence of weights (`float` or `int`) to weight the occurrences of
2015-05-08 16:10:47 +08:00
predicted class labels (`hard` voting) or class probabilities
before averaging (`soft` voting). Uses uniform weights if `None`.
2015-04-21 07:27:30 +08:00
n_jobs : int or None, optional (default=None)
The number of jobs to run in parallel for ``fit``.
``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
``-1`` means using all processors. See :term:`Glossary <n_jobs>`
for more details.
flatten_transform : bool, optional (default=True)
Affects shape of transform output only when voting='soft'
If voting='soft' and flatten_transform=True, transform method returns
matrix with shape (n_samples, n_classifiers * n_classes). If
flatten_transform=False, it returns
(n_classifiers, n_samples, n_classes).
2015-04-21 07:27:30 +08:00
Attributes
----------
estimators_ : list of classifiers
The collection of fitted sub-estimators as defined in ``estimators``
that are not `None`.
named_estimators_ : Bunch object, a dictionary with attribute access
Attribute to access any fitted sub-estimators by name.
.. versionadded:: 0.20
2015-04-21 07:27:30 +08:00
classes_ : array-like, shape = [n_predictions]
The classes labels.
2015-04-21 07:27:30 +08:00
Examples
--------
>>> import numpy as np
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.naive_bayes import GaussianNB
>>> from sklearn.ensemble import RandomForestClassifier, VotingClassifier
>>> clf1 = LogisticRegression(solver='lbfgs', multi_class='multinomial',
... random_state=1)
>>> clf2 = RandomForestClassifier(n_estimators=50, random_state=1)
2015-04-21 07:27:30 +08:00
>>> clf3 = GaussianNB()
>>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
>>> y = np.array([1, 1, 1, 2, 2, 2])
>>> eclf1 = VotingClassifier(estimators=[
... ('lr', clf1), ('rf', clf2), ('gnb', clf3)], voting='hard')
>>> eclf1 = eclf1.fit(X, y)
>>> print(eclf1.predict(X))
[1 1 1 2 2 2]
>>> np.array_equal(eclf1.named_estimators_.lr.predict(X),
... eclf1.named_estimators_['lr'].predict(X))
True
2015-04-21 07:27:30 +08:00
>>> eclf2 = VotingClassifier(estimators=[
... ('lr', clf1), ('rf', clf2), ('gnb', clf3)],
... voting='soft')
>>> eclf2 = eclf2.fit(X, y)
>>> print(eclf2.predict(X))
[1 1 1 2 2 2]
>>> eclf3 = VotingClassifier(estimators=[
... ('lr', clf1), ('rf', clf2), ('gnb', clf3)],
... voting='soft', weights=[2,1,1],
... flatten_transform=True)
2015-04-21 07:27:30 +08:00
>>> eclf3 = eclf3.fit(X, y)
>>> print(eclf3.predict(X))
[1 1 1 2 2 2]
>>> print(eclf3.transform(X).shape)
(6, 6)
2015-04-21 07:27:30 +08:00
"""
def __init__(self, estimators, voting='hard', weights=None, n_jobs=None,
flatten_transform=True):
2015-04-21 07:27:30 +08:00
self.estimators = estimators
self.voting = voting
self.weights = weights
self.n_jobs = n_jobs
self.flatten_transform = flatten_transform
2015-04-21 07:27:30 +08:00
@property
def named_estimators(self):
return Bunch(**dict(self.estimators))
def fit(self, X, y, sample_weight=None):
2015-04-21 07:27:30 +08:00
""" Fit the estimators.
Parameters
----------
X : {array-like, sparse matrix}, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of samples and
n_features is the number of features.
y : array-like, shape = [n_samples]
Target values.
sample_weight : array-like, shape = [n_samples] or None
Sample weights. If None, then samples are equally weighted.
Note that this is supported only if all underlying estimators
support sample weights.
2015-04-21 07:27:30 +08:00
Returns
-------
self : object
"""
if isinstance(y, np.ndarray) and len(y.shape) > 1 and y.shape[1] > 1:
2015-05-08 16:10:47 +08:00
raise NotImplementedError('Multilabel and multi-output'
2015-04-21 07:27:30 +08:00
' classification is not supported.')
if self.voting not in ('soft', 'hard'):
raise ValueError("Voting must be 'soft' or 'hard'; got (voting=%r)"
2015-05-08 16:10:47 +08:00
% self.voting)
2015-04-21 07:27:30 +08:00
if self.estimators is None or len(self.estimators) == 0:
raise AttributeError('Invalid `estimators` attribute, `estimators`'
' should be a list of (string, estimator)'
' tuples')
if (self.weights is not None and
len(self.weights) != len(self.estimators)):
2015-04-21 07:27:30 +08:00
raise ValueError('Number of classifiers and weights must be equal'
'; got %d weights, %d estimators'
% (len(self.weights), len(self.estimators)))
if sample_weight is not None:
for name, step in self.estimators:
if not has_fit_parameter(step, 'sample_weight'):
raise ValueError('Underlying estimator \'%s\' does not'
' support sample weights.' % name)
names, clfs = zip(*self.estimators)
self._validate_names(names)
n_isnone = np.sum([clf is None for _, clf in self.estimators])
if n_isnone == len(self.estimators):
raise ValueError('All estimators are None. At least one is '
'required to be a classifier!')
self.le_ = LabelEncoder().fit(y)
2015-04-21 07:27:30 +08:00
self.classes_ = self.le_.classes_
self.estimators_ = []
2015-05-08 16:10:47 +08:00
transformed_y = self.le_.transform(y)
self.estimators_ = Parallel(n_jobs=self.n_jobs)(
delayed(_parallel_fit_estimator)(clone(clf), X, transformed_y,
sample_weight=sample_weight)
for clf in clfs if clf is not None)
2015-05-08 16:10:47 +08:00
self.named_estimators_ = Bunch(**dict())
for k, e in zip(self.estimators, self.estimators_):
self.named_estimators_[k[0]] = e
2015-04-21 07:27:30 +08:00
return self
@property
def _weights_not_none(self):
"""Get the weights of not `None` estimators"""
if self.weights is None:
return None
return [w for est, w in zip(self.estimators,
self.weights) if est[1] is not None]
2015-04-21 07:27:30 +08:00
def predict(self, X):
""" Predict class labels for X.
Parameters
----------
X : {array-like, sparse matrix}, shape = [n_samples, n_features]
The input samples.
2015-04-21 07:27:30 +08:00
Returns
----------
maj : array-like, shape = [n_samples]
Predicted class labels.
"""
check_is_fitted(self, 'estimators_')
2015-04-21 07:27:30 +08:00
if self.voting == 'soft':
maj = np.argmax(self.predict_proba(X), axis=1)
2015-05-08 16:10:47 +08:00
2015-04-21 07:27:30 +08:00
else: # 'hard' voting
predictions = self._predict(X)
maj = np.apply_along_axis(
lambda x: np.argmax(
np.bincount(x, weights=self._weights_not_none)),
axis=1, arr=predictions)
2015-04-21 07:27:30 +08:00
maj = self.le_.inverse_transform(maj)
2015-05-08 16:10:47 +08:00
2015-04-21 07:27:30 +08:00
return maj
2015-05-08 16:10:47 +08:00
def _collect_probas(self, X):
2015-05-08 16:10:47 +08:00
"""Collect results from clf.predict calls. """
return np.asarray([clf.predict_proba(X) for clf in self.estimators_])
def _predict_proba(self, X):
2015-05-08 16:10:47 +08:00
"""Predict class probabilities for X in 'soft' voting """
if self.voting == 'hard':
raise AttributeError("predict_proba is not available when"
" voting=%r" % self.voting)
check_is_fitted(self, 'estimators_')
avg = np.average(self._collect_probas(X), axis=0,
weights=self._weights_not_none)
return avg
2015-04-21 07:27:30 +08:00
@property
def predict_proba(self):
2015-05-08 16:10:47 +08:00
"""Compute probabilities of possible outcomes for samples in X.
2015-04-21 07:27:30 +08:00
Parameters
----------
X : {array-like, sparse matrix}, shape = [n_samples, n_features]
The input samples.
2015-04-21 07:27:30 +08:00
Returns
----------
avg : array-like, shape = [n_samples, n_classes]
Weighted average probability for each class per sample.
"""
return self._predict_proba
2015-05-08 16:10:47 +08:00
2015-04-21 07:27:30 +08:00
def transform(self, X):
2015-05-08 16:10:47 +08:00
"""Return class labels or probabilities for X for each estimator.
2015-04-21 07:27:30 +08:00
Parameters
----------
X : {array-like, sparse matrix}, shape = [n_samples, n_features]
Training vectors, where n_samples is the number of samples and
n_features is the number of features.
Returns
-------
probabilities_or_labels
If `voting='soft'` and `flatten_transform=True`:
returns array-like of shape (n_classifiers, n_samples *
n_classes), being class probabilities calculated by each
classifier.
If `voting='soft' and `flatten_transform=False`:
array-like of shape (n_classifiers, n_samples, n_classes)
If `voting='hard'`:
array-like of shape (n_samples, n_classifiers), being
class labels predicted by each classifier.
2015-04-21 07:27:30 +08:00
"""
check_is_fitted(self, 'estimators_')
2015-04-21 07:27:30 +08:00
if self.voting == 'soft':
probas = self._collect_probas(X)
if not self.flatten_transform:
return probas
return np.hstack(probas)
2015-04-21 07:27:30 +08:00
else:
return self._predict(X)
def set_params(self, **params):
""" Setting the parameters for the voting classifier
Valid parameter keys can be listed with get_params().
Parameters
----------
**params : keyword arguments
Specific parameters using e.g. set_params(parameter_name=new_value)
In addition, to setting the parameters of the ``VotingClassifier``,
the individual classifiers of the ``VotingClassifier`` can also be
set or replaced by setting them to None.
Examples
--------
# In this example, the RandomForestClassifier is removed
clf1 = LogisticRegression()
clf2 = RandomForestClassifier()
eclf = VotingClassifier(estimators=[('lr', clf1), ('rf', clf2)]
eclf.set_params(rf=None)
"""
2019-01-11 05:27:06 +08:00
super()._set_params('estimators', **params)
return self
2015-04-21 07:27:30 +08:00
def get_params(self, deep=True):
""" Get the parameters of the VotingClassifier
Parameters
----------
deep : bool
Setting it to True gets the various classifiers and the parameters
of the classifiers as well
"""
return super(VotingClassifier,
self)._get_params('estimators', deep=deep)
2015-04-21 07:27:30 +08:00
def _predict(self, X):
2015-05-08 16:10:47 +08:00
"""Collect results from clf.predict calls. """
2015-04-21 07:27:30 +08:00
return np.asarray([clf.predict(X) for clf in self.estimators_]).T