如何在scikit-learn管道中对变换参数进行网格搜索

10 投票
3 回答
5990 浏览
提问于 2025-04-18 03:25

我的目标是用一个模型来选择最重要的变量,然后用另一个模型来利用这些变量进行预测。在下面的例子中,我使用了两个RandomForestClassifier的实例,但第二个模型可以是任何其他分类器。

这个随机森林模型有一个叫做 transform 的方法,里面有一个阈值参数。我想对不同的阈值参数进行网格搜索,也就是尝试不同的值来找出最合适的。

下面是一个简化的代码片段:

# Transform object and classifier
rf_filter = RandomForestClassifier(n_estimators=200, n_jobs=-1, random_state=42, oob_score=False)
clf = RandomForestClassifier(n_jobs=-1, random_state=42, oob_score=False)

pipe = Pipeline([("RFF", rf_filter), ("RF", clf)])

# Grid search parameters
rf_n_estimators = [10, 20]
rff_transform = ["median", "mean"] # Search the threshold parameters

estimator = GridSearchCV(pipe,
                            cv = 3, 
                            param_grid = dict(RF__n_estimators = rf_n_estimators,
                                            RFF__threshold = rff_transform))

estimator.fit(X_train, y_train)

出现的错误是 ValueError: Invalid parameter threshold for estimator RandomForestClassifier

我以为这样可以工作,因为文档上说:

如果是 None,并且可用的话,就会使用对象属性 threshold。

我尝试在进行网格搜索之前设置阈值属性(rf_filter.threshold = "median"),这样是可以的;不过,我搞不清楚接下来怎么对这个阈值进行网格搜索。

有没有办法遍历不同的参数,这些参数通常是在分类器的 transform 方法中提供的?

3 个回答

1

你可以通过下面这个小技巧来减少很多额外的编码工作。

首先,获取估计器的变量引用。在这个例子中就是“estimator”。你可以在调试的时候查看实际引用的超参数名称。

针对上面的问题

pipe = Pipeline([("RFF", rf_filter), ("RF", clf)])
...

param_grid = {"clf__estimator__n_estimators": [10, 20],

}

estimator = GridSearchCV(pipe,
                         cv = 3, 
                         param_grid )

所以只需要把超参数 max_features 改成 clf__estimator__max_features 就可以了

5
class my_rf_filter(BaseEstimator, TransformerMixin):
def __init__(self,threshold):
    self.threshold = threshold

def fit(self,X,y):
    model = RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=42, oob_score=False)
    model.fit(X,y)
    self.model = model
    return self

def transform(self,X):
    return self.model.transform(X,self.threshold)

通过把 RandomForestClassifier 包装在一个新类里,它就能正常工作了。

rf_filter = my_rf_filter(threshold='mean')
clf = RandomForestClassifier(n_jobs=-1, random_state=42, oob_score=False)

pipe = Pipeline([("RFF", rf_filter), ("RF", clf)])

# Grid search parameters
rf_n_estimators = [10, 20]
rff_transform = ["median", "mean"] # Search the threshold parameters

estimator = GridSearchCV(pipe,
                         cv = 3, 
                         param_grid = dict(RF__n_estimators = rf_n_estimators,
                                           RFF__threshold = rff_transform))

这是一个测试示例:

from sklearn import datasets
digits = datasets.load_digits()
X_digits = digits.data
y_digits = digits.target

estimator.fit(X_digits, y_digits)


Out[143]:
GridSearchCV(cv=3,
       estimator=Pipeline(steps=[('RFF', my_rf_filter(threshold='mean')), ('RF', RandomForestClassifier(bootstrap=True, compute_importances=None,
            criterion='gini', max_depth=None, max_features='auto',
            max_leaf_nodes=None, min_density=None, min_samples_leaf=1,
            min_samples_split=2, n_estimators=10, n_jobs=-1,
            oob_score=False, random_state=42, verbose=0))]),
       fit_params={}, iid=True, loss_func=None, n_jobs=1,
       param_grid={'RF__n_estimators': [10, 20], 'RFF__threshold': ['median', 'mean']},
       pre_dispatch='2*n_jobs', refit=True, score_func=None, scoring=None,
       verbose=0)


estimator.grid_scores_

Out[144]:
[mean: 0.89705, std: 0.00912, params: {'RF__n_estimators': 10, 'RFF__threshold': 'median'},
 mean: 0.91597, std: 0.00871, params: {'RF__n_estimators': 20, 'RFF__threshold': 'median'},
 mean: 0.89705, std: 0.00912, params: {'RF__n_estimators': 10, 'RFF__threshold': 'mean'},
 mean: 0.91597, std: 0.00871, params: {'RF__n_estimators': 20, 'RFF__threshold': 'mean'}]

如果你需要修改 my_rf_filter 类里面的 RandomForestClassifier 的参数,我觉得你需要明确地添加这些参数,也就是说,不要在 __init__() 里使用 **kwargs,也不要用 model.set_paras(**kwargs),因为我尝试过这样做但失败了。我认为可以在 __init__() 里添加 n_estimators=200,然后再用 model.n_estimators = self.n_estimators 来设置,这样就可以了。

10

按照你描述的方法,也就是用两个不同的随机森林分类器进行特征选择和分类,并把它们放在一个管道中,我也遇到了同样的问题。

RandomForestClassifier这个类没有一个叫做threshold的属性。你确实可以手动添加一个,方法可以是你说的那样,或者用

setattr(object, 'threshold', 'mean')

但是主要的问题似乎在于get_params这个方法是如何检查BaseEstimator类的有效属性的:

class BaseEstimator(object):
"""Base class for all estimators in scikit-learn

Notes
-----
All estimators should specify all the parameters that can be set
at the class level in their __init__ as explicit keyword
arguments (no *args, **kwargs).
"""

@classmethod
def _get_param_names(cls):
    """Get parameter names for the estimator"""
    try:
        # fetch the constructor or the original constructor before
        # deprecation wrapping if any
        init = getattr(cls.__init__, 'deprecated_original', cls.__init__)

        # introspect the constructor arguments to find the model parameters
        # to represent
        args, varargs, kw, default = inspect.getargspec(init)
        if not varargs is None:
            raise RuntimeError("scikit-learn estimators should always "
                               "specify their parameters in the signature"
                               " of their __init__ (no varargs)."
                               " %s doesn't follow this convention."
                               % (cls, ))
        # Remove 'self'
        # XXX: This is going to fail if the init is a staticmethod, but
        # who would do this?
        args.pop(0)
    except TypeError:
        # No explicit __init__
        args = []
    args.sort()
    return args

实际上,正如明确说明的那样,所有的估计器都应该在它们的__init__函数中将所有可以设置的参数作为明确的关键字参数列出。

所以我尝试在__init__函数中把threshold作为一个参数,默认值设为'mean'(这也是当前实现中的默认值)

    def __init__(self,
             n_estimators=10,
             criterion="gini",
             max_depth=None,
             min_samples_split=2,
             min_samples_leaf=1,
             max_features="auto",
             bootstrap=True,
             oob_score=False,
             n_jobs=1,
             random_state=None,
             verbose=0,
             min_density=None,
             compute_importances=None,
             threshold="mean"): # ADD THIS!

然后把这个参数的值赋给类的一个属性。

    self.threshold = threshold # ADD THIS LINE SOMEWHERE IN THE FUNCTION __INIT__

当然,这意味着要修改RandomForestClassifier类(在/python2.7/site-packages/sklearn/ensemble/forest.py里),这可能不是最好的方法……但对我来说有效!我现在可以在不同的threshold参数上进行网格搜索(和交叉验证),从而选择不同数量的特征。

撰写回答