解决自定义管道类中OneHotEncoder问题,从Jupyter Notebook转换为.py文件
总结:我在一个 ipynb
文件中定义了一个数据处理流程,运行得很好,但当我尝试把它放进一个 Class
里时,结果却不如预期。我可能在 OneHotEncode
这部分犯了错误。问题的描述有点长,因为涉及到代码,但其实可能很简单。
补充说明:如果我运行 housing = housing[housing['ocean_proximity'] != "ISLAND"]
,代码就能正常运行。所以,问题确实出在这个类别上,这可能让我的问题变得简单多了。我又检查了一下 OneHotEncode
,但不知道哪里出错了。
补充说明2:运行 grid_search.fit(housing.iloc[:100], housing_labels.iloc[:100])
没有错误。同时, ...housing.iloc[:1000]
也没有问题。但是运行 [:5000]
时出现了 ValueError: The feature names should match those that were passed during fit. Feature names seen at fit time, yet now missing: cat__ocean_proximity_ISLAND
。这个类别在数据集中样本很少。问题确实出在我如何进行 OneHotEncoding 上。
问题:
我有一个在 Jupyter Notebook 中写的转换流程,运行没有错误。作为自我练习,我尝试把函数、流程、模型等写在不同的 Python .py
文件中,以更专业的方式进行。但是,当我把这个流程写成一个 Python Class
并在主 .py
文件中导入时,有些地方没有按预期工作。
在笔记本中写的流程如下:
class ClusterSimilarity(BaseEstimator, TransformerMixin):
def __init__(self, n_clusters=10, gamma=1.0, random_state=None):
self.n_clusters = n_clusters
self.gamma = gamma
self.random_state = random_state
def fit(self, X, y=None, sample_weight=None):
self.kmeans_ = KMeans(self.n_clusters, n_init=10,
random_state=self.random_state)
self.kmeans_.fit(X, sample_weight=sample_weight)
return self # always return self!
def transform(self, X):
return rbf_kernel(X, self.kmeans_.cluster_centers_, gamma=self.gamma)
def get_feature_names_out(self, names=None):
return [f"Cluster {i} similarity" for i in range(self.n_clusters)]
def column_ratio(X):
return X[:, [0]] / X[:, [1]]
def ratio_name(function_transformer, feature_names_in):
return ["ratio"] # feature names out
def ratio_pipeline():
return make_pipeline(
SimpleImputer(strategy="median"),
FunctionTransformer(column_ratio, feature_names_out=ratio_name),
StandardScaler())
log_pipeline = make_pipeline(
SimpleImputer(strategy="median"),
FunctionTransformer(np.log, feature_names_out="one-to-one"),
StandardScaler())
cat_pipeline = make_pipeline(
SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore"))
cluster_simil = ClusterSimilarity(n_clusters=10, gamma=1., random_state=42)
default_num_pipeline = make_pipeline(SimpleImputer(strategy="median"),
StandardScaler())
preprocessing = ColumnTransformer([
("bedrooms", ratio_pipeline(), ["total_bedrooms", "total_rooms"]),
("rooms_per_house", ratio_pipeline(), ["total_rooms", "households"]),
("people_per_house", ratio_pipeline(), ["population", "households"]),
("log", log_pipeline, ["total_bedrooms", "total_rooms", "population",
"households", "median_income"]),
("geo", cluster_simil, ["latitude", "longitude"]),
("cat", cat_pipeline, make_column_selector(dtype_include=object)),
],
remainder=default_num_pipeline) # one column remaining: housing_median_age
我把它重写成 Python 文件中的一个 Class 如下:
from sklearn import set_config
set_config(transform_output='pandas')
class Preprocessor(TransformerMixin):
def __init__(self):
pass
def fit(self, X, y=None):
self._cluster_simil = ClusterSimilarity(n_clusters=10, gamma=1., random_state=42)
return self
def transform(self, X):
preprocessing = self._preprocessing()
return preprocessing.fit_transform(X)
def _column_ratio(self, X):
ratio = X.iloc[:, 0] / X.iloc[:, 1]
return np.reshape(ratio.to_numpy(), (-1, 1))
def _ratio_name(self, function_transformer, feature_names_in):
return ["ratio"] # feature names out
def _ratio_pipeline(self):
return make_pipeline(
SimpleImputer(strategy="median"),
FunctionTransformer(self._column_ratio, feature_names_out=self._ratio_name),
StandardScaler()
)
def _log_pipeline(self):
return make_pipeline(
SimpleImputer(strategy="median"),
FunctionTransformer(np.log, feature_names_out="one-to-one"),
StandardScaler()
)
def _cat_pipeline(self):
return make_pipeline(
SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore", sparse_output=False)
)
def _default_num_pipeline(self):
return make_pipeline(SimpleImputer(strategy="median"),
StandardScaler()
)
def _preprocessing(self):
return ColumnTransformer([
("bedrooms", self._ratio_pipeline(), ["total_bedrooms", "total_rooms"]),
("rooms_per_house", self._ratio_pipeline(), ["total_rooms", "households"]),
("people_per_house", self._ratio_pipeline(), ["population", "households"]),
("log", self._log_pipeline(), ["total_bedrooms", "total_rooms", "population",
"households", "median_income"]),
("geo", self._cluster_simil, ["latitude", "longitude"]),
("cat", self._cat_pipeline(), make_column_selector(dtype_include=object)),
],
remainder=self._default_num_pipeline()) # one column remaining: housing_median_age
class ClusterSimilarity(BaseEstimator, TransformerMixin):
def __init__(self, n_clusters=10, gamma=1.0, random_state=None):
self.n_clusters = n_clusters
self.gamma = gamma
self.random_state = random_state
def fit(self, X, y=None, sample_weight=None):
self.kmeans_ = KMeans(self.n_clusters, n_init=10,
random_state=self.random_state)
self.kmeans_.fit(X, sample_weight=sample_weight)
return self # always return self!
def transform(self, X):
return rbf_kernel(X, self.kmeans_.cluster_centers_, gamma=self.gamma)
def get_feature_names_out(self, names=None):
return [f"Cluster {i} similarity" for i in range(self.n_clusters)]
但它运行得不太好。当我只是测试时:
preprocessor = Preprocessor()
X_train = preprocessor.fit_transform(housing)
对于 X_train.info()
的输出是完全正确的。但是当我尝试用 gridSearch
进行测试时:
svr_pipeline = Pipeline([("preprocessing", preprocessor), ("svr", SVR())])
grid_search = GridSearchCV(svr_pipeline, param_grid, cv=3,
scoring='neg_root_mean_squared_error')
grid_search.fit(housing.iloc[:5000], housing_labels.iloc[:5000])
它输出了一个警告:
ValueError: The feature names should match those that were passed during fit.
Feature names seen at fit time, yet now missing:
- cat__ocean_proximity_ISLAND
UserWarning: One or more of the test scores are non-finite:
[nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan
nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan
nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan
nan nan nan nan nan nan nan nan]
问题可能出在这里:
ValueError: X has 23 features, but SVR is expecting 24 features as input.
经过处理后的正确 X
的形状是 X.shape
: (16521, 24)
。
也就是说,经过转换流程后我有 24 个特征。但不知为何,当调用 Preprocessor
类时,SVR 只看到了 23 个特征,缺少的那个特征是 ocean_proximity_ISLAND
,在数据集中只有少量值。这就是为什么在处理前 100 或 1000 行数据时没有问题,但在处理足够多的行时,ocean_proximity_ISLAND
被识别出来时,就会出现这个问题。
这个警告在 GridSearch
的每一步都会重复出现,指向的列总是同一个 cat__ocean_proximity_ISLAND
,它来自于 def _cat_pipeline(self):
这部分的流程,是使用 OneHotEncode
的结果。
注意:上面的代码在我的笔记本上运行正常。实际上,这是一本书的练习解决方案,书名是 "Hands-on Machine Learning"。问题出在我尝试把作者给的代码重写到另一个 .py
文件中作为一个 Class,以便我可以导入这个流程并使用它。因此,问题可能出在 class Preprocessor(TransformerMixin):
的某一行,但我真的不知道在哪里。由于错误来自 OneHotEncode
,我觉得我可能使用错了,但我在那部分并没有更改原始代码。我不知道该如何修复这个问题,或者为什么它在拟合时能看到列,但在预测时却看不到。
如何修复这个问题,并避免将来出现类似错误?
2 个回答
首先,我建议你更新一下你在 Preprocessor 类里的 fit 方法。在这个方法里,你只对 k_means 估算器进行了拟合,但没有对 ColumnTransformer 及其相关对象进行拟合,而这些也是需要拟合的。
当你调用 CVGridSearch 时,管道中的每个部分都会调用 fit/fit_transform 方法,但当在 Preprocessing 类的对象上调用 fit 方法时,实际上并没有对底层对象进行拟合(特别是 ColumnTransformer 实例)。你可以尝试像下面这样,利用 ColumnTransformer 的 fit 和 transform 方法,这样做会更正确。
更新:其次,_preprocessing 在被调用时会返回一个新的 ColumnTransformer 实例,而拟合后的版本并没有被保存。一个简单的解决办法是在构造方法里创建一个实例,然后把这个实例传给 fit 方法,这样就能保存拟合后的实例,以便后续可以应用 transform。
def __init__(self):
self._column_transformer = ColumnTransformer(...)
def fit(self, X, y=None):
self._cluster_simil = ClusterSimilarity(n_clusters=10, gamma=1., random_state=42)
self._preprocessing().fit(X)
return self
def transform(self, X):
return self._preprocessing().transform(X)
def _preprocessing(self):
return self._column_transformer
如果这样做仍然不能解决问题,我需要你提供你想要优化的参数字典(param_grid),因为“cat__ocean_proximity_ISLAND”这个表示法说明“ocean_proximity_ISLAND”在你的管道中某个地方被用作参数。
最后,我想给你一个建议,来自一个曾经多次尝试扩展 sklearn 和 tensorflow 类的人(基于无数小时与这些库的斗争)。管道存在的原因是为了组合估算器(在 tensorflow 中,类似的事情发生在顺序模型类中)。通过继承/重写来扩展这些类真的很难。这是因为你会继承这些复杂类中的很多内容,而这些内容并不明显,也没有考虑到。我的看法是,使用管道,如果你想要更复杂的结构,可以使用依赖注入,在一个容器对象里创建和处理你需要的对象实例。
一般来说,我会说,如果你去查看 sklearn 文档中像 ColumnTransformer 这样的类,然后点击蓝色的 [source] 超链接,进入 github 上的任何方法实现,如果你觉得自己能实现类似的东西,那你就可以尝试创建一个新的估算器类,使其与库中的其他类协同工作,而不会破坏任何东西。
我不是在评判你的技能,但要小心,当你从 BaseEstimator 继承并重写 fit 和 transform 时,可能还有很多其他的东西你也需要实现,以使其与其他类(例如 CVSearchGrid)兼容,而这些你可能并不知道。这就是为什么我总是建议尽量坚持使用管道,并使用依赖注入,而不是在 sklearn 中进行继承。