Skip to content

Commit 6485619

Browse files
qiagurasbt
authored andcommitted
Make StackingCVRegressor capable of replacing regressors in GridSearchCV (#515)
* introduce cross_val_predict to StackingCVRegressor * fix test for stacking_cv_regression * remove duplicate key-values from get_params of StackingCVRegressor * make regressor replaceable in GridSearchCV for StackingCVRegressor * changelog entry * allow droping regressor in gridsearch for StackingCVRegressor * make base parameter handler for estimators composed of a list of base estimators * add a test for stackingcvregressor with gridsearch and update notebook documentation * minor doc updates and changelog entry * Update CHANGELOG.md
1 parent 3f0935a commit 6485619

File tree

5 files changed

+132
-55
lines changed

5 files changed

+132
-55
lines changed

docs/sources/CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ The CHANGELOG for the current development version is available at
1717

1818
##### New Features
1919

20-
- Adds multiprocessing support to `StackingCVClassifier`. ([#512](https://github.com/rasbt/mlxtend/pull/512) via [Qiang Gu](https://github.com/qiaguhttps://github.com/qiagu))
20+
- Adds multiprocessing support to `StackingCVRegressor`. ([#512](https://github.com/rasbt/mlxtend/pull/512) via [Qiang Gu](https://github.com/qiaguhttps://github.com/qiagu))
21+
- Now, the `StackingCVRegressor` also enables grid search over the `regressors` and even a single base regressor. When there are level-mixed parameters, `GridSearchCV` will try to replace hyperparameters in a top-down order (see the [documentation](http://rasbt.github.io/mlxtend/user_guide/regressor/StackingCVRegressor/) for examples details). ([#515](https://github.com/rasbt/mlxtend/pull/512) via [Qiang Gu](https://github.com/qiaguhttps://github.com/qiagu))
2122
- Adds a `verbose` parameter to `apriori` to show the current iteration number as well as the itemset size currently being sampled. ([#519](https://github.com/rasbt/mlxtend/pull/519)
2223
- Adds an optional `class_name` parameter to the confusion matrix function to display class names on the axis as tick marks. ([#487](https://github.com/rasbt/mlxtend/pull/487) via [sandpiturtle](https://github.com/qiaguhttps://github.com/sandpiturtle))
2324

2425
##### Changes
2526

26-
-
27+
- Due to new features, restructuring, and better scikit-learn support (for `GridSearchCV`, etc.) the `StackingCVRegressor`'s meta regressor is now being accessed via `'meta_regressor__*` in the parameter grid. E.g., if a `RandomForestRegressor` as meta- egressor was previously tuned via `'randomforestregressor__n_estimators'`, this has now changed to `'meta_regressor__n_estimators'`. ([#515](https://github.com/rasbt/mlxtend/pull/512) via [Qiang Gu](https://github.com/qiaguhttps://github.com/qiagu))
28+
2729

2830
##### Bug Fixes
2931

docs/sources/user_guide/regressor/StackingCVRegressor.ipynb

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@
8484
"text": [
8585
"5-fold cross validation scores:\n",
8686
"\n",
87-
"R^2 Score: 0.45 (+/- 0.29) [SVM]\n",
87+
"R^2 Score: 0.46 (+/- 0.29) [SVM]\n",
8888
"R^2 Score: 0.43 (+/- 0.14) [Lasso]\n",
89-
"R^2 Score: 0.52 (+/- 0.28) [Random Forest]\n",
90-
"R^2 Score: 0.58 (+/- 0.24) [StackingCVRegressor]\n"
89+
"R^2 Score: 0.53 (+/- 0.28) [Random Forest]\n",
90+
"R^2 Score: 0.58 (+/- 0.23) [StackingCVRegressor]\n"
9191
]
9292
}
9393
],
@@ -138,10 +138,10 @@
138138
"text": [
139139
"5-fold cross validation scores:\n",
140140
"\n",
141-
"Neg. MSE Score: -33.69 (+/- 22.36) [SVM]\n",
141+
"Neg. MSE Score: -33.34 (+/- 22.36) [SVM]\n",
142142
"Neg. MSE Score: -35.53 (+/- 16.99) [Lasso]\n",
143-
"Neg. MSE Score: -27.32 (+/- 16.62) [Random Forest]\n",
144-
"Neg. MSE Score: -25.64 (+/- 18.11) [StackingCVRegressor]\n"
143+
"Neg. MSE Score: -27.25 (+/- 16.76) [Random Forest]\n",
144+
"Neg. MSE Score: -25.56 (+/- 18.22) [StackingCVRegressor]\n"
145145
]
146146
}
147147
],
@@ -177,19 +177,27 @@
177177
"source": [
178178
"In this second example we demonstrate how `StackingCVRegressor` works in combination with `GridSearchCV`. The stack still allows tuning hyper parameters of the base and meta models!\n",
179179
"\n",
180-
"To set up a parameter grid for scikit-learn's `GridSearch`, we simply provide the estimator's names in the parameter grid -- in the special case of the meta-regressor, we append the `'meta-'` prefix.\n"
180+
"For instance, we can use `estimator.get_params().keys()` to get a full list of tunable parameters.\n"
181181
]
182182
},
183183
{
184184
"cell_type": "code",
185185
"execution_count": 3,
186186
"metadata": {},
187187
"outputs": [
188+
{
189+
"name": "stderr",
190+
"output_type": "stream",
191+
"text": [
192+
"/Users/guq/miniconda3/envs/python3/lib/python3.7/site-packages/sklearn/model_selection/_search.py:841: DeprecationWarning: The default of the `iid` parameter will change from True to False in version 0.22 and will be removed in 0.24. This will change numeric results when test-set sizes are unequal.\n",
193+
" DeprecationWarning)\n"
194+
]
195+
},
188196
{
189197
"name": "stdout",
190198
"output_type": "stream",
191199
"text": [
192-
"Best: 0.673590 using {'lasso__alpha': 0.4, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.3}\n"
200+
"Best: 0.674237 using {'lasso__alpha': 1.6, 'meta_regressor__n_estimators': 100, 'ridge__alpha': 0.2}\n"
193201
]
194202
}
195203
],
@@ -203,8 +211,8 @@
203211
"\n",
204212
"X, y = load_boston(return_X_y=True)\n",
205213
"\n",
206-
"ridge = Ridge()\n",
207-
"lasso = Lasso()\n",
214+
"ridge = Ridge(random_state=RANDOM_SEED)\n",
215+
"lasso = Lasso(random_state=RANDOM_SEED)\n",
208216
"rf = RandomForestRegressor(random_state=RANDOM_SEED)\n",
209217
"\n",
210218
"# The StackingCVRegressor uses scikit-learn's check_cv\n",
@@ -224,7 +232,7 @@
224232
" param_grid={\n",
225233
" 'lasso__alpha': [x/5.0 for x in range(1, 10)],\n",
226234
" 'ridge__alpha': [x/20.0 for x in range(1, 10)],\n",
227-
" 'meta-randomforestregressor__n_estimators': [10, 100]\n",
235+
" 'meta_regressor__n_estimators': [10, 100]\n",
228236
" }, \n",
229237
" cv=5,\n",
230238
" refit=True\n",
@@ -244,20 +252,20 @@
244252
"name": "stdout",
245253
"output_type": "stream",
246254
"text": [
247-
"0.622 +/- 0.10 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.05}\n",
248-
"0.649 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.1}\n",
249-
"0.650 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.15}\n",
250-
"0.667 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.2}\n",
251-
"0.629 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.25}\n",
252-
"0.663 +/- 0.08 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.3}\n",
253-
"0.633 +/- 0.08 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.35}\n",
254-
"0.637 +/- 0.08 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.4}\n",
255-
"0.649 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.45}\n",
256-
"0.653 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 100, 'ridge__alpha': 0.05}\n",
257-
"0.648 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 100, 'ridge__alpha': 0.1}\n",
258-
"0.645 +/- 0.09 {'lasso__alpha': 0.2, 'meta-randomforestregressor__n_estimators': 100, 'ridge__alpha': 0.15}\n",
255+
"0.616 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.05}\n",
256+
"0.656 +/- 0.08 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.1}\n",
257+
"0.653 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.15}\n",
258+
"0.669 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.2}\n",
259+
"0.632 +/- 0.08 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.25}\n",
260+
"0.664 +/- 0.08 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.3}\n",
261+
"0.632 +/- 0.08 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.35}\n",
262+
"0.642 +/- 0.08 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.4}\n",
263+
"0.653 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 10, 'ridge__alpha': 0.45}\n",
264+
"0.657 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 100, 'ridge__alpha': 0.05}\n",
265+
"0.650 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 100, 'ridge__alpha': 0.1}\n",
266+
"0.648 +/- 0.09 {'lasso__alpha': 0.2, 'meta_regressor__n_estimators': 100, 'ridge__alpha': 0.15}\n",
259267
"...\n",
260-
"Best parameters: {'lasso__alpha': 0.4, 'meta-randomforestregressor__n_estimators': 10, 'ridge__alpha': 0.3}\n",
268+
"Best parameters: {'lasso__alpha': 1.6, 'meta_regressor__n_estimators': 100, 'ridge__alpha': 0.2}\n",
261269
"Accuracy: 0.67\n"
262270
]
263271
}
@@ -284,12 +292,12 @@
284292
"source": [
285293
"**Note**\n",
286294
"\n",
287-
"The `StackingCVRegressor` also enables grid search over the `regressors` argument. However, due to the current implementation of `GridSearchCV` in scikit-learn, it is not possible to search over both, different regressors and regressor parameters at the same time. For instance, while the following parameter dictionary works\n",
295+
"The `StackingCVRegressor` also enables grid search over the `regressors` and even a single base regressor. When there are level-mixed hyperparameters, `GridSearchCV` will try to replace hyperparameters in a top-down order, i.e., `regressors` -> single base regressor -> regressor hyperparameter. For instance, given a hyperparameter grid such as\n",
288296
"\n",
289297
" params = {'randomforestregressor__n_estimators': [1, 100],\n",
290298
" 'regressors': [(regr1, regr1, regr1), (regr2, regr3)]}\n",
291299
" \n",
292-
"it will use the instance settings of `regr1`, `regr2`, and `regr3` and not overwrite it with the `'n_estimators'` settings from `'randomforestregressor__n_estimators': [1, 100]`."
300+
"it will first use the instance settings of either `(regr1, regr2, regr3)` or `(regr2, regr3)` . Then it will replace the `'n_estimators'` settings for a matching regressor based on `'randomforestregressor__n_estimators': [1, 100]`."
293301
]
294302
},
295303
{
@@ -605,7 +613,7 @@
605613
"name": "python",
606614
"nbconvert_exporter": "python",
607615
"pygments_lexer": "ipython3",
608-
"version": "3.6.6"
616+
"version": "3.7.1"
609617
},
610618
"toc": {
611619
"nav_menu": {},

mlxtend/regressor/stacking_cv_regression.py

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
# License: BSD 3 clause
1515

1616
from ..externals.estimator_checks import check_is_fitted
17-
from ..externals import six
1817
from ..externals.name_estimators import _name_estimators
18+
from ..utils.base_compostion import _BaseXComposition
1919
from scipy import sparse
20-
from sklearn.base import BaseEstimator
2120
from sklearn.base import RegressorMixin
2221
from sklearn.base import TransformerMixin
2322
from sklearn.base import clone
@@ -27,7 +26,7 @@
2726
import numpy as np
2827

2928

30-
class StackingCVRegressor(BaseEstimator, RegressorMixin, TransformerMixin):
29+
class StackingCVRegressor(_BaseXComposition, RegressorMixin, TransformerMixin):
3130
"""A 'Stacking Cross-Validation' regressor for scikit-learn estimators.
3231
3332
New in mlxtend v0.7.0
@@ -123,12 +122,6 @@ def __init__(self, regressors, meta_regressor, cv=5,
123122

124123
self.regressors = regressors
125124
self.meta_regressor = meta_regressor
126-
self.named_regressors = {key: value for
127-
key, value in
128-
_name_estimators(regressors)}
129-
self.named_meta_regressor = {'meta-%s' % key: value for
130-
key, value in
131-
_name_estimators([meta_regressor])}
132125
self.cv = cv
133126
self.shuffle = shuffle
134127
self.n_jobs = n_jobs
@@ -273,25 +266,29 @@ def predict_meta_features(self, X):
273266
check_is_fitted(self, 'regr_')
274267
return np.column_stack([regr.predict(X) for regr in self.regr_])
275268

269+
@property
270+
def named_regressors(self):
271+
"""
272+
Returns
273+
-------
274+
List of named estimator tuples, like [('svc', SVC(...))]
275+
"""
276+
return _name_estimators(self.regressors)
277+
276278
def get_params(self, deep=True):
277279
#
278280
# Return estimator parameter names for GridSearch support.
279281
#
280-
if not deep:
281-
return super(StackingCVRegressor, self).get_params(deep=False)
282-
else:
283-
out = self.named_regressors.copy()
284-
for name, step in six.iteritems(self.named_regressors):
285-
for key, value in six.iteritems(step.get_params(deep=True)):
286-
out['%s__%s' % (name, key)] = value
282+
return self._get_params('named_regressors', deep=deep)
287283

288-
out.update(self.named_meta_regressor.copy())
289-
for name, step in six.iteritems(self.named_meta_regressor):
290-
for key, value in six.iteritems(step.get_params(deep=True)):
291-
out['%s__%s' % (name, key)] = value
284+
def set_params(self, **params):
285+
"""Set the parameters of this estimator.
292286
293-
for key, value in six.iteritems(super(StackingCVRegressor,
294-
self).get_params(deep=False)):
295-
out['%s' % key] = value
287+
Valid parameter keys can be listed with ``get_params()``.
296288
297-
return out
289+
Returns
290+
-------
291+
self
292+
"""
293+
self._set_params('regressors', 'named_regressors', **params)
294+
return self

mlxtend/regressor/tests/test_stacking_cv_regression.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_gridsearch_numerate_regr():
9898
params = {'ridge-1__alpha': [0.01, 1.0],
9999
'ridge-2__alpha': [0.01, 1.0],
100100
'svr__C': [0.01, 1.0],
101-
'meta-svr__C': [0.01, 1.0],
101+
'meta_regressor__C': [0.01, 1.0],
102102
'use_features_in_secondary': [True, False]}
103103

104104
grid = GridSearchCV(estimator=stack,
@@ -122,7 +122,6 @@ def test_get_params():
122122
got = sorted(list({s.split('__')[0] for s in stregr.get_params().keys()}))
123123
expect = ['cv',
124124
'linearregression',
125-
'meta-svr',
126125
'meta_regressor',
127126
'n_jobs',
128127
'pre_dispatch',
@@ -332,3 +331,34 @@ def test_weight_unsupported_with_no_weight():
332331
stack = StackingCVRegressor(regressors=[svr_lin, lr, ridge],
333332
meta_regressor=lasso)
334333
stack.fit(X1, y).predict(X1)
334+
335+
336+
def test_gridsearch_replace_mix():
337+
svr_lin = SVR(kernel='linear', gamma='auto')
338+
ridge = Ridge(random_state=1)
339+
svr_rbf = SVR(kernel='rbf', gamma='auto')
340+
lr = LinearRegression()
341+
lasso = Lasso(random_state=1)
342+
stack = StackingCVRegressor(regressors=[svr_lin, lasso, ridge],
343+
meta_regressor=svr_rbf,
344+
shuffle=False)
345+
346+
params = {'regressors': [[svr_lin, lr]],
347+
'linearregression': [None, lasso, ridge],
348+
'svr__kernel': ['poly']}
349+
350+
grid = GridSearchCV(estimator=stack,
351+
param_grid=params,
352+
cv=KFold(5, shuffle=True, random_state=42),
353+
iid=False,
354+
refit=True,
355+
verbose=0)
356+
grid = grid.fit(X1, y)
357+
358+
got1 = round(grid.best_score_, 2)
359+
got2 = len(grid.best_params_['regressors'])
360+
got3 = grid.best_params_['regressors'][0].kernel
361+
362+
assert got1 == 0.73, got1
363+
assert got2 == 2, got2
364+
assert got3 == 'poly', got3

mlxtend/utils/base_compostion.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Utilties to handle estimator list"""
2+
3+
from ..externals import six
4+
from sklearn.utils.metaestimators import _BaseComposition
5+
6+
7+
class _BaseXComposition(_BaseComposition):
8+
"""
9+
parameter handler for list of estimators
10+
"""
11+
def _set_params(self, attr, named_attr, **params):
12+
# Ordered parameter replacement
13+
# 1. root parameter
14+
if attr in params:
15+
setattr(self, attr, params.pop(attr))
16+
17+
# 2. single estimator replacement
18+
items = getattr(self, named_attr)
19+
names = []
20+
if items:
21+
names, estimators = zip(*items)
22+
estimators = list(estimators)
23+
for name in list(six.iterkeys(params)):
24+
if '__' not in name and name in names:
25+
# replace single estimator and re-build the
26+
# root estimators list
27+
for i, est_name in enumerate(names):
28+
if est_name == name:
29+
new_val = params.pop(name)
30+
if new_val is None:
31+
del estimators[i]
32+
else:
33+
estimators[i] = new_val
34+
break
35+
# replace the root estimators
36+
setattr(self, attr, estimators)
37+
38+
# 3. estimator parameters and other initialisation arguments
39+
super(_BaseXComposition, self).set_params(**params)
40+
return self

0 commit comments

Comments
 (0)