Skip to content

Commit 018eeec

Browse files
[ADD] first push of coalescer
1 parent f9fe056 commit 018eeec

7 files changed

Lines changed: 493 additions & 1 deletion

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import Any, Dict, Optional, Union
2+
3+
from ConfigSpace.configuration_space import ConfigurationSpace
4+
from ConfigSpace.hyperparameters import (
5+
UniformFloatHyperparameter,
6+
)
7+
8+
import numpy as np
9+
10+
from autoPyTorch.pipeline.components.preprocessing.tabular_preprocessing.coalescer.base_coalescer import BaseCoalescer
11+
from autoPyTorch.utils.common import HyperparameterSearchSpace, add_hyperparameter
12+
from autoPyTorch.utils.implementations import MinorityCoalescing
13+
14+
15+
class MinorityCoalescer(BaseCoalescer):
16+
"""
17+
Groups together classes in a categorical feature if the frequency
18+
of occurrence is less than minimum_fraction
19+
"""
20+
def __init__(self, minimum_fraction: float, random_state: Optional[Union[np.random.RandomState, int]] = None):
21+
super().__init__()
22+
self.minimum_fraction = minimum_fraction
23+
self.random_state = random_state
24+
25+
def fit(self, X: Dict[str, Any], y: Any = None) -> BaseCoalescer:
26+
27+
self.check_requirements(X, y)
28+
29+
self.preprocessor['categorical'] = MinorityCoalescing(minimum_fraction=self.minimum_fraction)
30+
return self
31+
32+
@staticmethod
33+
def get_properties(dataset_properties: Optional[Dict[str, Any]] = None) -> Dict[str, Union[str, bool]]:
34+
return {
35+
'shortname': 'MinorityCoalescer',
36+
'name': 'Minority Feature-class coalescer',
37+
'handles_sparse': False
38+
}
39+
40+
@staticmethod
41+
def get_hyperparameter_search_space(
42+
dataset_properties: Optional[Dict] = None,
43+
minimum_fraction: HyperparameterSearchSpace = HyperparameterSearchSpace(hyperparameter="minimum_fraction",
44+
value_range=(0.0001, 0.5),
45+
default_value=0.01,
46+
log=True),
47+
) -> ConfigurationSpace:
48+
cs = ConfigurationSpace()
49+
50+
add_hyperparameter(cs, minimum_fraction, UniformFloatHyperparameter)
51+
52+
return cs
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Any, Dict, Optional, Union
2+
3+
import numpy as np
4+
5+
from autoPyTorch.pipeline.components.preprocessing.tabular_preprocessing.coalescer.base_coalescer import BaseCoalescer
6+
7+
8+
class NoCoalescer(BaseCoalescer):
9+
"""
10+
Don't perform NoCoalescer on categorical features
11+
"""
12+
def __init__(self,
13+
random_state: Optional[Union[np.random.RandomState, int]] = None
14+
):
15+
super().__init__()
16+
self.random_state = random_state
17+
18+
def fit(self, X: Dict[str, Any], y: Any = None) -> BaseCoalescer:
19+
"""
20+
The fit function calls the fit function of the underlying model
21+
and returns the transformed array.
22+
Args:
23+
X (np.ndarray): input features
24+
y (Optional[np.ndarray]): input labels
25+
26+
Returns:
27+
instance of self
28+
"""
29+
self.check_requirements(X, y)
30+
31+
return self
32+
33+
def transform(self, X: Dict[str, Any]) -> Dict[str, Any]:
34+
"""
35+
Adds the self into the 'X' dictionary and returns it.
36+
Args:
37+
X (Dict[str, Any]): 'X' dictionary
38+
39+
Returns:
40+
(Dict[str, Any]): the updated 'X' dictionary
41+
"""
42+
X.update({'coalescer': self.preprocessor})
43+
return X
44+
45+
@staticmethod
46+
def get_properties(dataset_properties: Optional[Dict[str, Any]] = None) -> Dict[str, Union[str, bool]]:
47+
return {
48+
'shortname': 'NoCoalescer',
49+
'name': 'No Coalescer',
50+
'handles_sparse': True
51+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import os
2+
from collections import OrderedDict
3+
from typing import Any, Dict, List, Optional
4+
5+
import ConfigSpace.hyperparameters as CSH
6+
from ConfigSpace.configuration_space import ConfigurationSpace
7+
8+
from autoPyTorch.pipeline.components.base_choice import autoPyTorchChoice
9+
from autoPyTorch.pipeline.components.base_component import (
10+
ThirdPartyComponents,
11+
autoPyTorchComponent,
12+
find_components,
13+
)
14+
from autoPyTorch.pipeline.components.preprocessing.tabular_preprocessing.coalescer.base_coalescer import BaseCoalescer
15+
16+
17+
coalescer_directory = os.path.split(__file__)[0]
18+
_coalescer = find_components(__package__,
19+
coalescer_directory,
20+
BaseCoalescer)
21+
_addons = ThirdPartyComponents(BaseCoalescer)
22+
23+
24+
def add_coalescer(coalescer: BaseCoalescer) -> None:
25+
_addons.add_component(coalescer)
26+
27+
28+
class CoalescerChoice(autoPyTorchChoice):
29+
"""
30+
Allows for dynamically choosing coalescer component at runtime
31+
"""
32+
33+
def get_components(self) -> Dict[str, autoPyTorchComponent]:
34+
"""Returns the available coalescer components
35+
36+
Args:
37+
None
38+
39+
Returns:
40+
Dict[str, autoPyTorchComponent]: all BaseCoalescer components available
41+
as choices for coalescer the categorical columns
42+
"""
43+
components = OrderedDict()
44+
components.update(_coalescer)
45+
components.update(_addons.components)
46+
return components
47+
48+
def get_hyperparameter_search_space(self,
49+
dataset_properties: Optional[Dict[str, Any]] = None,
50+
default: Optional[str] = None,
51+
include: Optional[List[str]] = None,
52+
exclude: Optional[List[str]] = None) -> ConfigurationSpace:
53+
cs = ConfigurationSpace()
54+
55+
if dataset_properties is None:
56+
dataset_properties = dict()
57+
58+
dataset_properties = {**self.dataset_properties, **dataset_properties}
59+
60+
available_preprocessors = self.get_available_components(dataset_properties=dataset_properties,
61+
include=include,
62+
exclude=exclude)
63+
64+
if len(available_preprocessors) == 0:
65+
raise ValueError("no coalescer found, please add a coalescer")
66+
67+
if default is None:
68+
defaults = ['MinorityCoalescer', 'NoCoalescer']
69+
for default_ in defaults:
70+
if default_ in available_preprocessors:
71+
if include is not None and default_ not in include:
72+
continue
73+
if exclude is not None and default_ in exclude:
74+
continue
75+
default = default_
76+
break
77+
78+
updates = self._get_search_space_updates()
79+
if '__choice__' in updates.keys():
80+
choice_hyperparameter = updates['__choice__']
81+
if not set(choice_hyperparameter.value_range).issubset(available_preprocessors):
82+
raise ValueError("Expected given update for {} to have "
83+
"choices in {} got {}".format(self.__class__.__name__,
84+
available_preprocessors,
85+
choice_hyperparameter.value_range))
86+
if len(dataset_properties['categorical_columns']) == 0:
87+
assert len(choice_hyperparameter.value_range) == 1
88+
assert 'MinorityCoalescer' in choice_hyperparameter.value_range, \
89+
"Provided {} in choices, however, the dataset " \
90+
"is incompatible with it".format(choice_hyperparameter.value_range)
91+
92+
preprocessor = CSH.CategoricalHyperparameter('__choice__',
93+
choice_hyperparameter.value_range,
94+
default_value=choice_hyperparameter.default_value)
95+
else:
96+
# add only no coalescer to choice hyperparameters in case the dataset is only numerical
97+
if len(dataset_properties['categorical_columns']) == 0:
98+
default = 'NoCoalescer'
99+
if include is not None and default not in include:
100+
raise ValueError("Provided {} in include, however, the dataset "
101+
"is incompatible with it".format(include))
102+
preprocessor = CSH.CategoricalHyperparameter('__choice__',
103+
['NoCoalescer'],
104+
default_value=default)
105+
else:
106+
preprocessor = CSH.CategoricalHyperparameter('__choice__',
107+
list(available_preprocessors.keys()),
108+
default_value=default)
109+
110+
cs.add_hyperparameter(preprocessor)
111+
112+
# add only child hyperparameters of early_preprocessor choices
113+
for name in preprocessor.choices:
114+
preprocessor_configuration_space = available_preprocessors[name].\
115+
get_hyperparameter_search_space(dataset_properties)
116+
parent_hyperparameter = {'parent': preprocessor, 'value': name}
117+
cs.add_configuration_space(name, preprocessor_configuration_space,
118+
parent_hyperparameter=parent_hyperparameter)
119+
120+
self.configuration_space = cs
121+
self.dataset_properties = dataset_properties
122+
return cs
123+
124+
def _check_dataset_properties(self, dataset_properties: Dict[str, Any]) -> None:
125+
"""
126+
A mechanism in code to ensure the correctness of the fit dictionary
127+
It recursively makes sure that the children and parent level requirements
128+
are honored before fit.
129+
Args:
130+
dataset_properties:
131+
132+
"""
133+
super()._check_dataset_properties(dataset_properties)
134+
assert 'numerical_columns' in dataset_properties.keys(), \
135+
"Dataset properties must contain information about numerical columns"
136+
assert 'categorical_columns' in dataset_properties.keys(), \
137+
"Dataset properties must contain information about categorical columns"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Any, Dict, List
2+
3+
from autoPyTorch.pipeline.components.preprocessing.tabular_preprocessing.base_tabular_preprocessing import (
4+
autoPyTorchTabularPreprocessingComponent
5+
)
6+
from autoPyTorch.utils.common import FitRequirement
7+
8+
9+
class BaseCoalescer(autoPyTorchTabularPreprocessingComponent):
10+
"""
11+
Base class for coalescing
12+
"""
13+
def __init__(self) -> None:
14+
super().__init__()
15+
self.add_fit_requirements([
16+
FitRequirement('categorical_columns', (List,), user_defined=True, dataset_property=True),
17+
FitRequirement('categories', (List,), user_defined=True, dataset_property=True)])
18+
19+
def transform(self, X: Dict[str, Any]) -> Dict[str, Any]:
20+
"""
21+
Adds the self into the 'X' dictionary and returns it.
22+
Args:
23+
X (Dict[str, Any]): 'X' dictionary
24+
25+
Returns:
26+
(Dict[str, Any]): the updated 'X' dictionary
27+
"""
28+
if self.preprocessor['numerical'] is None and self.preprocessor['categorical'] is None:
29+
raise ValueError("cant call transform on {} without fitting first."
30+
.format(self.__class__.__name__))
31+
X.update({'coalescer': self.preprocessor})
32+
return X

autoPyTorch/utils/implementations.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
from typing import Any, Callable, Dict, Type, Union
1+
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
22

33
import numpy as np
44

5+
from scipy import sparse
6+
7+
from sklearn.base import BaseEstimator, TransformerMixin
8+
59
import torch
610

711

@@ -61,3 +65,76 @@ def __call__(self, y: Union[np.ndarray, torch.Tensor]) -> np.ndarray:
6165
@staticmethod
6266
def get_properties() -> Dict[str, Any]:
6367
return {'supported_losses': ['BCEWithLogitsLoss']}
68+
69+
70+
class MinorityCoalescing(BaseEstimator, TransformerMixin):
71+
""" Group together categories which occurence is less than a specified
72+
minimum fraction. Coalesced categories get index of one.
73+
"""
74+
75+
def __init__(self, minimum_fraction: Optional[float] = None):
76+
self.minimum_fraction = minimum_fraction
77+
78+
def check_X(self, X: np.array) -> None:
79+
X_data = X.data if sparse.issparse(X) else X
80+
if np.nanmin(X_data) <= -2:
81+
raise ValueError("X needs to contain only integers greater than -2.")
82+
83+
def fit(self, X: np.array, y: Optional[np.ndarray] = None) -> 'MinorityCoalescing':
84+
self.check_X(X)
85+
86+
if self.minimum_fraction is None:
87+
return self
88+
89+
# Remember which values should not be coalesced
90+
do_not_coalesce: List[Set[int]] = list()
91+
for column in range(X.shape[1]):
92+
do_not_coalesce.append(set())
93+
94+
if sparse.issparse(X):
95+
indptr_start = X.indptr[column]
96+
indptr_end = X.indptr[column + 1]
97+
unique, counts = np.unique(
98+
X.data[indptr_start:indptr_end], return_counts=True)
99+
colsize = indptr_end - indptr_start
100+
else:
101+
unique, counts = np.unique(X[:, column], return_counts=True)
102+
colsize = X.shape[0]
103+
104+
for unique_value, count in zip(unique, counts):
105+
fraction = float(count) / colsize
106+
if fraction >= self.minimum_fraction:
107+
do_not_coalesce[-1].add(unique_value)
108+
109+
self.do_not_coalesce_ = do_not_coalesce
110+
return self
111+
112+
def transform(self, X: np.ndarray) -> np.ndarray:
113+
self.check_X(X)
114+
115+
if self.minimum_fraction is None:
116+
return X
117+
118+
for column in range(X.shape[1]):
119+
if sparse.issparse(X):
120+
indptr_start = X.indptr[column]
121+
indptr_end = X.indptr[column + 1]
122+
unique = np.unique(X.data[indptr_start:indptr_end])
123+
for unique_value in unique:
124+
if unique_value not in self.do_not_coalesce_[column]:
125+
indptr_start = X.indptr[column]
126+
indptr_end = X.indptr[column + 1]
127+
X.data[indptr_start:indptr_end][
128+
X.data[indptr_start:indptr_end] == unique_value] = -2
129+
else:
130+
unique = np.unique(X[:, column])
131+
unique_values = [unique_value for unique_value in unique
132+
if unique_value not in self.do_not_coalesce_[column]]
133+
mask = np.isin(X[:, column], unique_values)
134+
# The imputer uses -1 for unknown categories
135+
# Then -2 means coalesced categories
136+
X[mask, column] = -2
137+
return X
138+
139+
def fit_transform(self, X: np.ndarray, y: Optional[np.ndarray] = None) -> np.ndarray:
140+
return self.fit(X, y).transform(X)

0 commit comments

Comments
 (0)