【问题标题】:Custom transformer for sklearn Pipeline that alters both X and y用于改变 X 和 y 的 sklearn 管道的自定义转换器
【发布时间】:2014-10-21 18:08:14
【问题描述】:

我想创建自己的转换器以用于 sklearn Pipeline

我正在创建一个实现 fit 和 transform 方法的类。转换器的目的是从矩阵中删除具有超过指定数量的 NaN 的行。

我面临的问题是如何更改传递给转换器的 X 和 y 矩阵?

我相信这必须在 fit 方法中完成,因为它可以访问 X 和 y。由于一旦我将 X 重新分配给具有较少行的新矩阵,python 通过赋值传递参数,因此对原始 X 的引用丢失(当然对于 y 也是如此)。是否可以保留此引用?

我正在使用 pandas DataFrame 轻松删除具有太多 NaN 的行,这对于我的用例可能不是正确的方法。当前代码如下所示:

class Dropna():

    # thresh is max number of NaNs allowed in a row
    def __init__(self, thresh=0):
        self.thresh = thresh

    def fit(self, X, y):
        total = X.shape[1]
        # +1 to account for 'y' being added to the dframe                                                                                                                            
        new_thresh = total + 1 - self.thresh
        df = pd.DataFrame(X)
        df['y'] = y
        df.dropna(thresh=new_thresh, inplace=True)
        X = df.drop('y', axis=1).values
        y = df['y'].values
        return self

    def transform(self, X):
        return X

【问题讨论】:

    标签: python pandas numpy machine-learning scikit-learn


    【解决方案1】:

    修改采样轴,例如删除样本,不(还?)符合 scikit-learn 转换器 API。所以如果你需要这样做,你应该在任何对 scikit learn 的调用之外进行,作为预处理。

    现在,转换器 API 用于将给定样本的特征转换为新的东西。这可以隐式包含来自其他样本的信息,但样本永远不会被删除。

    另一种选择是尝试估算缺失值。但同样,如果您需要删除样本,请在使用 scikit learn 之前将其视为预处理。

    【讨论】:

      【解决方案2】:

      sklearn 之上构建的包imblearn 包含一个估计器FunctionSampler,它允许在管道步骤中操作特征数组X 和目标数组y

      请注意,在管道步骤中使用它需要使用imblearn 中的Pipeline 类,该类继承自sklearn 中的类。此外,默认情况下,在Pipeline 的上下文中,如果在fit 之后没有立即调用resample 方法(如fit_resample),它什么也不做。所以,请提前阅读文档。

      【讨论】:

        【解决方案3】:

        您可以使用 sklearn.preprocessing.FunctionTransformer 方法轻松解决此问题 (http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.FunctionTransformer.html)

        你只需要把你对 X 的替换放在一个函数中

        def drop_nans(X, y=None):
            total = X.shape[1]                                           
            new_thresh = total - thresh
            df = pd.DataFrame(X)
            df.dropna(thresh=new_thresh, inplace=True)
            return df.values
        

        然后你通过调用得到你的转换器

        transformer = FunctionTransformer(drop_nans, validate=False)
        

        您可以在管道中使用。阈值可以在 drop_nans 函数之外设置。

        【讨论】:

          【解决方案4】:

          @eickenberg 是正确而干净的答案。不过,我喜欢将所有内容都保存在一个 Pipeline 中,因此如果您有兴趣,我创建了一个库(尚未部署在 pypi 上),允许在 Y 上应用转换:

          https://gitlab.com/thibaultB/transformers/

          用法如下:

          df = pd.DataFrame([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
          df.columns = ["a", "b", "target"]
          
          spliter = SplitXY("target") # Create a new step and give it name of column target
          
          pipe = Pipeline([
                  ("imputer", SklearnPandasWrapper(KNNImputer())),
                  ("spliter", spliter), 
                  ("scaler", StandardScaler()),
                  ("rf",
                      EstimatorWithoutYWrapper(RandomForestRegressor(random_state=45),
                                              spliter)) # EstimatorWithoutYWrapper overwrite RandomForestRegressor to get y from spliter just before calling fit or transform
              ])
          pipe.fit(df)
          
          res = pipe.predict(df)
          

          使用此代码,如果将所有修改行数的转换器放在“SplitXY”转换器之前,则可以更改行数。 SplitXY 转换器之前的转换器应该保留列名,这就是为什么我还添加了一个 SklearnPandasWrapper 来包装 sklearn 转换器(通常返回 numpy 数组)以保留列名。

          【讨论】:

            【解决方案5】:

            你要修改sklearn的内部代码Pipeline

            我们定义了一个转换器,它在拟合期间移除至少特征值或目标值为 NaN 的样本 (fit_transform)。虽然它会在推理过程中删除特征值至少为 NaN 的样本 (transform)。需要注意的是,我们的转换器在 fit_transform 中返回 X 和 y,因此我们需要在 sklearn Pipeline 中处理这种行为。

            class Dropna():
            
                def fit(self, X, y):
                    return self
            
                def fit_transform(self, X, y):
                    
                    mask = (np.isnan(X).any(-1) | np.isnan(y))
                    if hasattr(X, 'loc'):
                        X = X.loc[~mask]
                    else:
                        X = X[~mask]
                    if hasattr(y, 'loc'):
                        y = y.loc[~mask]
                    else:
                        y = y[~mask]
                    
                    return X, y   ###### make fit_transform return X and y
                
                def transform(self, X):
                    
                    mask = np.isnan(X).any(-1)
                    if hasattr(X, 'loc'):
                        X = X.loc[~mask]
                    else:
                        X = X[~mask]
                    
                    return X
            

            我们只需要在fit_fit方法中的两个具体点上修改原来的sklearn Pipeline。其余的保持不变。

            from sklearn import pipeline
            from sklearn.base import clone
            from sklearn.utils import _print_elapsed_time
            from sklearn.utils.validation import check_memory
            
            class Pipeline(pipeline.Pipeline):
            
                def _fit(self, X, y=None, **fit_params_steps):
                    self.steps = list(self.steps)
                    self._validate_steps()
                    memory = check_memory(self.memory)
            
                    fit_transform_one_cached = memory.cache(pipeline._fit_transform_one)
            
                    for (step_idx, name, transformer) in self._iter(
                        with_final=False, filter_passthrough=False
                    ):
                                    
                        if transformer is None or transformer == "passthrough":
                            with _print_elapsed_time("Pipeline", self._log_message(step_idx)):
                                continue
            
                        try:
                            # joblib >= 0.12
                            mem = memory.location
                        except AttributeError:
                            mem = memory.cachedir
                        finally:
                            cloned_transformer = clone(transformer) if mem else transformer
            
                        X, fitted_transformer = fit_transform_one_cached(
                            cloned_transformer,
                            X,
                            y,
                            None,
                            message_clsname="Pipeline",
                            message=self._log_message(step_idx),
                            **fit_params_steps[name],
                        )
                        
                        if isinstance(X, tuple):    ###### unpack X if is tuple X = (X,y)
                            X, y = X
                        
                        self.steps[step_idx] = (name, fitted_transformer)
                    
                    return X, y
                
                def fit(self, X, y=None, **fit_params):
                    fit_params_steps = self._check_fit_params(**fit_params)
                    Xt = self._fit(X, y, **fit_params_steps)
                    
                    if isinstance(Xt, tuple):    ###### unpack X if is tuple X = (X,y)
                        Xt, y = Xt 
                    
                    with _print_elapsed_time("Pipeline", self._log_message(len(self.steps) - 1)):
                        if self._final_estimator != "passthrough":
                            fit_params_last_step = fit_params_steps[self.steps[-1][0]]
                            self._final_estimator.fit(Xt, y, **fit_params_last_step)
            
                    return self
            

            这是在新的Xy 中解压缩Dropna().fit_transform(X, y) 生成的值所必需的。

            这是工作中的完整管道:

            from sklearn.linear_model import Ridge
            
            X = np.random.uniform(0,1, (100,3))
            y = np.random.uniform(0,1, (100,))
            X[np.random.uniform(0,1, (100)) < 0.1] = np.nan
            y[np.random.uniform(0,1, (100)) < 0.1] = np.nan
            
            pipe = Pipeline([('dropna', Dropna()), ('model', Ridge())])
            pipe.fit(X, y)
            
            pipe.predict(X).shape
            

            另一个带有进一步中间预处理步骤的试验:

            from sklearn.preprocessing import StandardScaler
            
            pipe = Pipeline([('dropna', Dropna()), ('scaler', StandardScaler()), ('model', Ridge())])
            pipe.fit(X, y)
            
            pipe.predict(X).shape
            

            可以根据需要通过其他简单的修改来实现更复杂的行为。如果您也对Pipeline().fit_transformPipeline().fit_predict 感兴趣,则需要进行相同的更改。

            【讨论】:

              【解决方案6】:

              继续使用“deep-copies”,继续使用,Xy 仍然受到保护

              .fit() 可以在每次调用时首先将 deep-copy 分配给新的类变量

              self.X_without_NaNs = X.copy()
              self.y_without_NaNs = y.copy()
              

              然后减少/转换这些不超过NaN-s 比self.treshold 排序

              【讨论】:

              • 但是同一管道中的其他转换器如何访问这些副本?
              • 这是一个不完整的解决方案,它没有解决问题并留下额外的疑问。
              猜你喜欢
              • 2016-04-16
              • 2021-12-20
              • 2020-06-17
              • 2018-01-02
              • 1970-01-01
              • 2018-11-04
              • 1970-01-01
              • 2019-09-30
              • 2020-03-08
              相关资源
              最近更新 更多