Python 管道 returns 在交叉验证中使用时的 NaN 分数
Python Pipeline returns NaN Scores When Used in Cross Validation
我想用 sklearn 创建一个管道,包括一些预处理步骤和最后一个模型以适应数据。我使用此管道通过交叉验证获得分数。稍后我想使用GridSearchCV
中的管道进行参数优化。
截至目前,预处理步骤包括:
- 使用我创建的
ColumnsRemoval()
class 删除一些列的一个步骤,
- 每个特征类型(分类或数值)特定的一个步骤。为了在下面的示例中简化,我刚刚为数值特征添加了
StandardScaler()
,为分类特征添加了 OneHotEncoder()
。
问题是我得到的分数都是nan
。它运行得非常快,似乎传递给模型的空数组:
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.model_selection import cross_validate
import numpy as np
# Create random dataframe
num_data = np.random.random_sample((5,4))
cat_data = ['good','bad','fair','excellent','bad']
col_list_stack = ['SalePrice','Id','TotalBsmtSF','GrdLivArea']
data = pd.DataFrame(num_data, columns = col_list_stack)
data['Quality'] = cat_data
X_train = data.drop(labels = ['SalePrice'], axis = 1)
y_train = data['SalePrice']
#------------------------------------------------------------#
# create a custom transformer to remove columns
class ColumnsRemoval(BaseEstimator, TransformerMixin):
def __init__(self, skip = False, remove_cols = ['Id','TotalBsmtSF']):
self._remove_cols = remove_cols
self._skip = skip
def fit(self, X, y = None):
return self
def transform(self, X, y = None):
if not self._skip:
return X.drop(labels = self._remove_cols,axis = 1)
else:
return X
#------------------------------------------------------------#
# PIPELINE and cross-validation
# Preprocessing steps common to numerical and categorical data
preprocessor_common = Pipeline(steps=[
('remove_features', ColumnsRemoval())])
# Separated preprocessing steps
numeric_transformer = Pipeline(steps=[
('scaler', StandardScaler())])
categorical_transformer = Pipeline(steps=[
('onehot', OneHotEncoder(handle_unknown='ignore'))])
preprocessor_by_cat = ColumnTransformer(
transformers=[
('num', numeric_transformer, ['GrdLivArea']),
('cat', categorical_transformer, ['Quality'])], remainder = 'passthrough')
# Full pipeline with model
pipe = Pipeline(steps = [('preprocessor_common', preprocessor_common),
('preprocessor_by_cat', preprocessor_by_cat),
('model', LinearRegression())])
# Use cross validation to obtain scores
scores = cross_validate(pipe, X_train, y_train,
scoring = ["neg_mean_squared_error","r2"], cv = 4)
我试过以下方法:
- 仅使用一个预处理步骤加上管道中的模型。当我在管道中使用
preprocessor_by_cat
+ model
步骤时,我得到了分数值。使用 preprocessor_common
+ model
步骤也得到 nan
分数
- 在管道中执行两个预处理步骤(
preprocessor_common
+preprocessor_by_cat
),.fit_transform()
训练数据,然后将其发送到cross_validate(),大致如下下面:
pipe = Pipeline(steps = [('preprocessor_common', preprocessor_common),
('preprocessor_by_cat', preprocessor_by_cat),
])
X_processed = pipe.fit_transform(X_train)
# Use cross validation to obtain scores
scores = cross_validate(LinearRegression(), X_processed, y_train,
scoring = ["neg_mean_squared_error","r2"], cv = 4)
据我了解,在管道中进行预处理或对管道进行预处理+模型是相同的,这就是为什么我认为获取 NaN
值是一个问题。
我希望问题很清楚,恭喜你做到了这里:)
TL;DR
您需要重新定义自定义 ColumnsRemoval
的 __init()__
函数,因为将 Python 列表作为默认值传递会导致错误。一种可能的解决方案:
class ColumnsRemoval(BaseEstimator, TransformerMixin):
def __init__(self, skip=False, remove_cols=None):
if remove_cols is None:
remove_cols = ['Id', 'TotalBsmtSF']
self._remove_cols = remove_cols
self._skip = skip
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
if not self._skip:
return X.drop(labels=self._remove_cols, axis=1)
else:
return X
有了这个,您的管道应该按预期工作。
背景
我 运行 你的 MWE 出现了以下错误:
FitFailedWarning: Estimator fit failed. The score on this train-test partition for these parameters will be set to nan.
它与您自定义的以下行有关 ColumnsRemoval
:
return X.drop(labels=self._remove_cols, axis=1)
抛出了错误:
ValueError: Need to specify at least one of 'labels', 'index' or 'columns'
将标准 Python 列表传递给 drop()
函数时,这似乎是一个已知问题,在此 中进行了讨论。解决方案是通过例如numpy
数组或 pandas
索引对象。我提出的另一个解决方案是不在函数定义中为 remove_cols
设置默认值,而是在函数体中分配它。这也有效。
看起来没有人真正知道为什么会这样。抱歉,我无法详细说明实际原因(如果有人可以补充,我会很高兴)。不过问题应该解决了。
我找到问题所在了。我一直在做一些进一步的测试,也使用 float
而不是列表作为默认值。
详述 here,在 Instantiation 部分下:
the object's attributes used in __init__()
should have exactly the
name of the argument in the constructor.
所以我所做的是使用与 __init__()
中传递的参数名称相同的对象属性名称,现在一切正常。例如:
class ColumnsRemoval(BaseEstimator, TransformerMixin):
def __init__(self, threshold = 0.9)
self.threshold = threshold
Using self._threshold
(注意 threshold
之前的 _
)有一个奇怪的行为,在某些情况下,对象与提供的值(或默认值)一起使用,但是在其他情况下 self._threshold
被设置为 None
。这也允许使用 list
作为默认值来传递 __init__()
(尽管应避免使用 list
作为默认值,详情请参阅 afsharov 的回答)
我想用 sklearn 创建一个管道,包括一些预处理步骤和最后一个模型以适应数据。我使用此管道通过交叉验证获得分数。稍后我想使用GridSearchCV
中的管道进行参数优化。
截至目前,预处理步骤包括:
- 使用我创建的
ColumnsRemoval()
class 删除一些列的一个步骤, - 每个特征类型(分类或数值)特定的一个步骤。为了在下面的示例中简化,我刚刚为数值特征添加了
StandardScaler()
,为分类特征添加了OneHotEncoder()
。
问题是我得到的分数都是nan
。它运行得非常快,似乎传递给模型的空数组:
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.model_selection import cross_validate
import numpy as np
# Create random dataframe
num_data = np.random.random_sample((5,4))
cat_data = ['good','bad','fair','excellent','bad']
col_list_stack = ['SalePrice','Id','TotalBsmtSF','GrdLivArea']
data = pd.DataFrame(num_data, columns = col_list_stack)
data['Quality'] = cat_data
X_train = data.drop(labels = ['SalePrice'], axis = 1)
y_train = data['SalePrice']
#------------------------------------------------------------#
# create a custom transformer to remove columns
class ColumnsRemoval(BaseEstimator, TransformerMixin):
def __init__(self, skip = False, remove_cols = ['Id','TotalBsmtSF']):
self._remove_cols = remove_cols
self._skip = skip
def fit(self, X, y = None):
return self
def transform(self, X, y = None):
if not self._skip:
return X.drop(labels = self._remove_cols,axis = 1)
else:
return X
#------------------------------------------------------------#
# PIPELINE and cross-validation
# Preprocessing steps common to numerical and categorical data
preprocessor_common = Pipeline(steps=[
('remove_features', ColumnsRemoval())])
# Separated preprocessing steps
numeric_transformer = Pipeline(steps=[
('scaler', StandardScaler())])
categorical_transformer = Pipeline(steps=[
('onehot', OneHotEncoder(handle_unknown='ignore'))])
preprocessor_by_cat = ColumnTransformer(
transformers=[
('num', numeric_transformer, ['GrdLivArea']),
('cat', categorical_transformer, ['Quality'])], remainder = 'passthrough')
# Full pipeline with model
pipe = Pipeline(steps = [('preprocessor_common', preprocessor_common),
('preprocessor_by_cat', preprocessor_by_cat),
('model', LinearRegression())])
# Use cross validation to obtain scores
scores = cross_validate(pipe, X_train, y_train,
scoring = ["neg_mean_squared_error","r2"], cv = 4)
我试过以下方法:
- 仅使用一个预处理步骤加上管道中的模型。当我在管道中使用
preprocessor_by_cat
+model
步骤时,我得到了分数值。使用preprocessor_common
+model
步骤也得到nan
分数 - 在管道中执行两个预处理步骤(
preprocessor_common
+preprocessor_by_cat
),.fit_transform()
训练数据,然后将其发送到cross_validate(),大致如下下面:
pipe = Pipeline(steps = [('preprocessor_common', preprocessor_common),
('preprocessor_by_cat', preprocessor_by_cat),
])
X_processed = pipe.fit_transform(X_train)
# Use cross validation to obtain scores
scores = cross_validate(LinearRegression(), X_processed, y_train,
scoring = ["neg_mean_squared_error","r2"], cv = 4)
据我了解,在管道中进行预处理或对管道进行预处理+模型是相同的,这就是为什么我认为获取 NaN
值是一个问题。
我希望问题很清楚,恭喜你做到了这里:)
TL;DR
您需要重新定义自定义 ColumnsRemoval
的 __init()__
函数,因为将 Python 列表作为默认值传递会导致错误。一种可能的解决方案:
class ColumnsRemoval(BaseEstimator, TransformerMixin):
def __init__(self, skip=False, remove_cols=None):
if remove_cols is None:
remove_cols = ['Id', 'TotalBsmtSF']
self._remove_cols = remove_cols
self._skip = skip
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
if not self._skip:
return X.drop(labels=self._remove_cols, axis=1)
else:
return X
有了这个,您的管道应该按预期工作。
背景
我 运行 你的 MWE 出现了以下错误:
FitFailedWarning: Estimator fit failed. The score on this train-test partition for these parameters will be set to nan.
它与您自定义的以下行有关 ColumnsRemoval
:
return X.drop(labels=self._remove_cols, axis=1)
抛出了错误:
ValueError: Need to specify at least one of 'labels', 'index' or 'columns'
将标准 Python 列表传递给 drop()
函数时,这似乎是一个已知问题,在此 numpy
数组或 pandas
索引对象。我提出的另一个解决方案是不在函数定义中为 remove_cols
设置默认值,而是在函数体中分配它。这也有效。
看起来没有人真正知道为什么会这样。抱歉,我无法详细说明实际原因(如果有人可以补充,我会很高兴)。不过问题应该解决了。
我找到问题所在了。我一直在做一些进一步的测试,也使用 float
而不是列表作为默认值。
详述 here,在 Instantiation 部分下:
the object's attributes used in
__init__()
should have exactly the name of the argument in the constructor.
所以我所做的是使用与 __init__()
中传递的参数名称相同的对象属性名称,现在一切正常。例如:
class ColumnsRemoval(BaseEstimator, TransformerMixin):
def __init__(self, threshold = 0.9)
self.threshold = threshold
Using self._threshold
(注意 threshold
之前的 _
)有一个奇怪的行为,在某些情况下,对象与提供的值(或默认值)一起使用,但是在其他情况下 self._threshold
被设置为 None
。这也允许使用 list
作为默认值来传递 __init__()
(尽管应避免使用 list
作为默认值,详情请参阅 afsharov 的回答)