sktime.classification.sklearn._rotation_forest 源代码

"""A rotation forest (RotF) vector classifier.

A rotation Forest sktime implementation for continuous values only. Fits sklearn
conventions.
"""

__author__ = ["MatthewMiddlehurst"]
__all__ = ["RotationForest"]

import time

import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator
from sklearn.decomposition import PCA
from sklearn.tree import DecisionTreeClassifier
from sklearn.utils import check_random_state

from sktime.base._base import _clone_estimator
from sktime.exceptions import NotFittedError
from sktime.utils.validation import check_n_jobs


[文档]class RotationForest(BaseEstimator): """A rotation forest (RotF) vector classifier. Implementation of the Rotation Forest classifier described in Rodriguez et al (2013) [1]. Builds a forest of trees build on random portions of the data transformed using PCA. Intended as a benchmark for time series data and a base classifier for transformation based approaches such as ShapeletTransformClassifier, this sktime implementation only works with continuous attributes. Parameters ---------- n_estimators : int, default=200 Number of estimators to build for the ensemble. min_group : int, default=3 The minimum size of an attribute subsample group. max_group : int, default=3 The maximum size of an attribute subsample group. remove_proportion : float, default=0.5 The proportion of cases to be removed per group. base_estimator : BaseEstimator or None, default="None" Base estimator for the ensemble. By default, uses the sklearn ``DecisionTreeClassifier`` using entropy as a splitting measure. time_limit_in_minutes : int, default=0 Time contract to limit build time in minutes, overriding ``n_estimators``. Default of ``0`` means ``n_estimators`` is used. contract_max_n_estimators : int, default=500 Max number of estimators to build when ``time_limit_in_minutes`` is set. save_transformed_data : bool, default=False Save the data transformed in fit in ``transformed_data_`` for use in ``_get_train_probs``. n_jobs : int, default=1 The number of jobs to run in parallel for both ``fit`` and ``predict``. ``-1`` means using all processors. random_state : int, RandomState instance or None, default=None If ``int``, random_state is the seed used by the random number generator; If ``RandomState`` instance, random_state is the random number generator; If ``None``, the random number generator is the ``RandomState`` instance used by ``np.random``. Attributes ---------- classes_ : list The unique class labels in the training set. n_classes_ : int The number of unique classes in the training set. n_instances_ : int The number of train cases in the training set. n_atts_ : int The number of attributes in the training set. transformed_data_ : list of shape (n_estimators) of ndarray The transformed training dataset for all classifiers. Only saved when ``save_transformed_data`` is ``True``. estimators_ : list of shape (n_estimators) of BaseEstimator The collections of estimators trained in fit. See Also -------- ShapeletTransformClassifier: A shapelet-based classifier using Rotation Forest. Notes ----- For the Java version, see `tsml <https://github.com/uea-machine-learning/tsml/blob/master/src/main/java /weka/classifiers/meta/RotationForest.java>`_. References ---------- .. [1] Rodriguez, Juan José, Ludmila I. Kuncheva, and Carlos J. Alonso. "Rotation forest: A new classifier ensemble method." IEEE transactions on pattern analysis and machine intelligence 28.10 (2006). .. [2] Bagnall, A., et al. "Is rotation forest the best classifier for problems with continuous features?." arXiv preprint arXiv:1809.06705 (2018). Examples -------- >>> from sktime.classification.sklearn import RotationForest >>> from sktime.datasets import load_unit_test >>> from sktime.datatypes._panel._convert import from_nested_to_3d_numpy >>> X_train, y_train = load_unit_test(split="train", return_X_y=True) >>> X_test, y_test = load_unit_test(split="test", return_X_y=True) >>> X_train = from_nested_to_3d_numpy(X_train) >>> X_test = from_nested_to_3d_numpy(X_test) >>> clf = RotationForest(n_estimators=10) >>> clf.fit(X_train, y_train) RotationForest(...) >>> y_pred = clf.predict(X_test) """ def __init__( self, n_estimators=200, min_group=3, max_group=3, remove_proportion=0.5, base_estimator=None, time_limit_in_minutes=0.0, contract_max_n_estimators=500, save_transformed_data=False, n_jobs=1, random_state=None, ): self.n_estimators = n_estimators self.min_group = min_group self.max_group = max_group self.remove_proportion = remove_proportion self.base_estimator = base_estimator self.time_limit_in_minutes = time_limit_in_minutes self.contract_max_n_estimators = contract_max_n_estimators self.save_transformed_data = save_transformed_data self.n_jobs = n_jobs self.random_state = random_state super().__init__()
[文档] def fit(self, X, y): """Fit a forest of trees on cases (X,y), where y is the target variable. Parameters ---------- X : 2d ndarray or DataFrame of shape = [n_instances, n_attributes] The training data. y : array-like, shape = [n_instances] The class labels. Returns ------- self : Reference to self. Notes ----- Changes state by creating a fitted model that updates attributes ending in "_". """ from joblib import Parallel, delayed if isinstance(X, np.ndarray) and len(X.shape) == 3 and X.shape[1] == 1: X = np.reshape(X, (X.shape[0], -1)) elif isinstance(X, pd.DataFrame) and len(X.shape) == 2: X = X.to_numpy() elif not isinstance(X, np.ndarray) or len(X.shape) > 2: raise ValueError( "RotationForest is not a time series classifier. " "A valid sklearn input such as a 2d numpy array is required." "Sparse input formats are currently not supported." ) X, y = self._validate_data(X=X, y=y, ensure_min_samples=2) self._n_jobs = check_n_jobs(self.n_jobs) self.n_instances_, self.n_atts_ = X.shape self.classes_ = np.unique(y) self.n_classes_ = self.classes_.shape[0] self._class_dictionary = {} for index, classVal in enumerate(self.classes_): self._class_dictionary[classVal] = index # escape if only one class seen if self.n_classes_ == 1: self._is_fitted = True return self time_limit = self.time_limit_in_minutes * 60 start_time = time.time() train_time = 0 if self.base_estimator is None: self._base_estimator = DecisionTreeClassifier(criterion="entropy") # remove useless attributes self._useful_atts = ~np.all(X[1:] == X[:-1], axis=0) X = X[:, self._useful_atts] self._n_atts = X.shape[1] # normalise attributes self._min = X.min(axis=0) self._ptp = X.max(axis=0) - self._min X = (X - self._min) / self._ptp X_cls_split = [X[np.where(y == i)] for i in self.classes_] if time_limit > 0: self._n_estimators = 0 self.estimators_ = [] self._pcas = [] self._groups = [] self.transformed_data_ = [] while ( train_time < time_limit and self._n_estimators < self.contract_max_n_estimators ): fit = Parallel(n_jobs=self._n_jobs)( delayed(self._fit_estimator)( X, X_cls_split, y, i, ) for i in range(self._n_jobs) ) estimators, pcas, groups, transformed_data = zip(*fit) self.estimators_ += estimators self._pcas += pcas self._groups += groups self.transformed_data_ += transformed_data self._n_estimators += self._n_jobs train_time = time.time() - start_time else: self._n_estimators = self.n_estimators fit = Parallel(n_jobs=self._n_jobs)( delayed(self._fit_estimator)( X, X_cls_split, y, i, ) for i in range(self._n_estimators) ) self.estimators_, self._pcas, self._groups, self.transformed_data_ = zip( *fit ) self._is_fitted = True return self
[文档] def predict(self, X): """Predict for all cases in X. Built on top of predict_proba. Parameters ---------- X : 2d ndarray or DataFrame of shape = [n_instances, n_attributes] The data to make predictions for. Returns ------- y : array-like, shape = [n_instances] Predicted class labels. """ rng = check_random_state(self.random_state) return np.array( [ self.classes_[int(rng.choice(np.flatnonzero(prob == prob.max())))] for prob in self.predict_proba(X) ] )
[文档] def predict_proba(self, X): """Probability estimates for each class for all cases in X. Parameters ---------- X : 2d ndarray or DataFrame of shape = [n_instances, n_attributes] The data to make predictions for. Returns ------- y : array-like, shape = [n_instances, n_classes_] Predicted probabilities using the ordering in classes_. """ from joblib import Parallel, delayed if not self._is_fitted: raise NotFittedError( f"This instance of {self.__class__.__name__} has not " f"been fitted yet; please call `fit` first." ) # treat case of single class seen in fit if self.n_classes_ == 1: return np.repeat([[1]], X.shape[0], axis=0) if isinstance(X, np.ndarray) and len(X.shape) == 3 and X.shape[1] == 1: X = np.reshape(X, (X.shape[0], -1)) elif isinstance(X, pd.DataFrame) and len(X.shape) == 2: X = X.to_numpy() elif not isinstance(X, np.ndarray) or len(X.shape) > 2: raise ValueError( "RotationForest is not a time series classifier. " "A valid sklearn input such as a 2d numpy array is required." "Sparse input formats are currently not supported." ) X = self._validate_data(X=X, reset=False) # replace missing values with 0 and remove useless attributes X = X[:, self._useful_atts] # normalise the data. X = (X - self._min) / self._ptp y_probas = Parallel(n_jobs=self._n_jobs)( delayed(self._predict_proba_for_estimator)( X, self.estimators_[i], self._pcas[i], self._groups[i], ) for i in range(self._n_estimators) ) output = np.sum(y_probas, axis=0) / ( np.ones(self.n_classes_) * self._n_estimators ) return output
def _get_train_probs(self, X, y): from joblib import Parallel, delayed if not self._is_fitted: raise NotFittedError( f"This instance of {self.__class__.__name__} has not " f"been fitted yet; please call `fit` first." ) if isinstance(X, np.ndarray) and len(X.shape) == 3 and X.shape[1] == 1: X = np.reshape(X, (X.shape[0], -1)) elif isinstance(X, pd.DataFrame) and len(X.shape) == 2: X = X.to_numpy() elif not isinstance(X, np.ndarray) or len(X.shape) > 2: raise ValueError( "RotationForest is not a time series classifier. " "A valid sklearn input such as a 2d numpy array is required." "Sparse input formats are currently not supported." ) X = self._validate_data(X=X, reset=False) # handle the single-class-label case if len(self._class_dictionary) == 1: return np.repeat([[1]], len(X), axis=0) n_instances, n_atts = X.shape if n_instances != self.n_instances_ or n_atts != self.n_atts_: raise ValueError( "n_instances, n_atts mismatch. X should be the same as the training " "data used in fit for generating train probabilities." ) if not self.save_transformed_data: raise ValueError("Currently only works with saved transform data from fit.") p = Parallel(n_jobs=self._n_jobs)( delayed(self._train_probas_for_estimator)( y, i, ) for i in range(self._n_estimators) ) y_probas, oobs = zip(*p) results = np.sum(y_probas, axis=0) divisors = np.zeros(n_instances) for oob in oobs: for inst in oob: divisors[inst] += 1 for i in range(n_instances): results[i] = ( np.ones(self.n_classes_) * (1 / self.n_classes_) if divisors[i] == 0 else results[i] / (np.ones(self.n_classes_) * divisors[i]) ) return results def _fit_estimator(self, X, X_cls_split, y, idx): rs = 255 if self.random_state == 0 else self.random_state rs = ( None if self.random_state is None else (rs * 37 * (idx + 1)) % np.iinfo(np.int32).max ) rng = check_random_state(rs) groups = self._generate_groups(rng) pcas = [] # construct the slices to fit the PCAs too. for group in groups: classes = rng.choice( range(self.n_classes_), size=rng.randint(1, self.n_classes_ + 1), replace=False, ) # randomly add the classes with the randomly selected attributes. X_t = np.zeros((0, len(group))) for cls_idx in classes: c = X_cls_split[cls_idx] X_t = np.concatenate((X_t, c[:, group]), axis=0) sample_ind = rng.choice( X_t.shape[0], max(1, int(X_t.shape[0] * self.remove_proportion)), replace=False, ) X_t = X_t[sample_ind] # try to fit the PCA if it fails, remake it, and add 10 random data # instances. while True: # ignore err state on PCA because we account if it fails. with np.errstate(divide="ignore", invalid="ignore"): # differences between os occasionally. seems to happen when there # are low amounts of cases in the fit pca = PCA(random_state=rs).fit(X_t) if not np.isnan(pca.explained_variance_ratio_).all(): break X_t = np.concatenate( (X_t, rng.random_sample((10, X_t.shape[1]))), axis=0 ) pcas.append(pca) # merge all the pca_transformed data into one instance and build a classifier # on it. X_t = np.concatenate( [pcas[i].transform(X[:, group]) for i, group in enumerate(groups)], axis=1 ) X_t = np.nan_to_num(X_t, False, 0, 0, 0) tree = _clone_estimator(self._base_estimator, random_state=rs) tree.fit(X_t, y) return tree, pcas, groups, X_t if self.save_transformed_data else None def _predict_proba_for_estimator(self, X, clf, pcas, groups): X_t = np.concatenate( [pcas[i].transform(X[:, group]) for i, group in enumerate(groups)], axis=1 ) X_t = np.nan_to_num(X_t, False, 0, 0, 0) probas = clf.predict_proba(X_t) if probas.shape[1] != self.n_classes_: new_probas = np.zeros((probas.shape[0], self.n_classes_)) for i, cls in enumerate(clf.classes_): cls_idx = self._class_dictionary[cls] new_probas[:, cls_idx] = probas[:, i] probas = new_probas return probas def _train_probas_for_estimator(self, y, idx): rs = 255 if self.random_state == 0 else self.random_state rs = ( None if self.random_state is None else (rs * 37 * (idx + 1)) % np.iinfo(np.int32).max ) rng = check_random_state(rs) indices = range(self.n_instances_) subsample = rng.choice(self.n_instances_, size=self.n_instances_) oob = [n for n in indices if n not in subsample] results = np.zeros((self.n_instances_, self.n_classes_)) if len(oob) == 0: return [results, oob] clf = _clone_estimator(self._base_estimator, rs) clf.fit(self.transformed_data_[idx][subsample], y[subsample]) probas = clf.predict_proba(self.transformed_data_[idx][oob]) if probas.shape[1] != self.n_classes_: new_probas = np.zeros((probas.shape[0], self.n_classes_)) for i, cls in enumerate(clf.classes_): cls_idx = self._class_dictionary[cls] new_probas[:, cls_idx] = probas[:, i] probas = new_probas for n, proba in enumerate(probas): results[oob[n]] += proba return [results, oob] def _generate_groups(self, rng): permutation = rng.permutation(np.arange(0, self._n_atts)) # select the size of each group. group_size_count = np.zeros(self.max_group - self.min_group + 1) n_attributes = 0 n_groups = 0 while n_attributes < self._n_atts: n = rng.randint(group_size_count.shape[0]) group_size_count[n] += 1 n_attributes += self.min_group + n n_groups += 1 groups = [] current_attribute = 0 current_size = 0 for i in range(0, n_groups): while group_size_count[current_size] == 0: current_size += 1 group_size_count[current_size] -= 1 n = self.min_group + current_size groups.append(np.zeros(n, dtype=int)) for k in range(0, n): if current_attribute < permutation.shape[0]: groups[i][k] = permutation[current_attribute] else: groups[i][k] = permutation[rng.randint(permutation.shape[0])] current_attribute += 1 return groups