Scikit-learn Custom Pipeline Save & Reload (저장 및 재사용)

2020. 2. 28. 22:08분석 Python/Scikit Learn (싸이킷런)

필자는 scikit-learn으로 preprocessing custom class들을 만든 후 Pipeline을 활용해 한꺼번에 묶어서 사용하고자 했다. Pipeline으로 구축하게 되면 개인적으로 생각하는 점은 자신의 새로운 class와 더불어서 scikit-learn에 많은 패키지들과 호환돼서 확장성을 가질 수 있을 것이라고 생각했다.

기존에 단순히 Class로 된 것에 대한 아쉬움을 느껴서, 확장하기에는 custom 해야하는 것이 많다.
그래서 Scikit-Learn Pipeline과 호환이 된다면, 더 많은 것을 쉽게 할 수 있을 거라 생각해서, 진행해봤다.

기존에 Pipeline으로만 하는 것은 자주 해서 어려운 부분이 아니였지만, 저장해서 사용하려니 이슈가 생겼고, 해결한 방법에 대해서 관련된 자료와 코드를 공유하고자 한다.

저장하고 위해서 sklearn에 있는 joblib , 그냥 joblib , pickle을 사용해봤다.
하지만 Pickling Error라고 해서 Class를 저장할 수 없다고는 에러가 발생하였다. 그래서 Class까지 저장할 수 있는 dill이라는 패키지를 사용했지만, 위와 같은 동일한 이슈가 발생했다.

아래의 글은 그것을 해결한 방법이다.

https://github.com/scikit-learn/scikit-learn/issues/12903

 

FunctionTransformer Pickling Error · Issue #12903 · scikit-learn/scikit-learn

Description When pickling a FunctionTransformer transformer instance, the wrapped function does not get pickled: AttributeError: Can't get attribute 'f' on <module 'main'="" ......<="" p="">

github.com

https://github.com/urigoren/decorators4DS/blob/master/decorators4DS/sklearn_dec.py

 

urigoren/decorators4DS

Useful decorators every Data Scientist should know - urigoren/decorators4DS

github.com

해결 방법에 대해서 내가 이해한 것은 custom class 함수에다가 특정 저장할 수 있게 하는 class를 상속시켜서 해결하는 것 같다. 이런 방법으로 하니 저장이 되고 잘 되지만, 아쉬운 점이 하나 있었다.
아래의 class를 Decorate를 해주면 저장이 된다.  
하지만 아쉽게도 아래에 보면 self.func 으로 고정된 값을 넣어줘야 하기 때문에, 나의 기존 custom class에 self.func을 임의로 사용 유무와 상관없이 넣어줘야 했다.

그래서 구조는 다음과 같다.

 

class SKDecorate:
    def __init__(self, f):
        self.func = f

    def __call__(self, X):
        return self.func(X)

    def __iter__(self):
        return (i for i in [self.func.__name__, self])

    def __getitem__(self, i):
        return [self.func.__name__, self][i]

    def __getstate__(self):
        self.func_pkl = cloudpickle.dumps(self.func)
        del self.func
        return self.__dict__

    def __setstate__(self, d):
        d["func"] = cloudpickle.loads(d["func_pkl"])
        del d["func_pkl"]
        self.__dict__ = d
class NumericHandler(BaseEstimator, TransformerMixin,SKDecorate) :
    def __init__(self, ) :
       ## something
        

    def fit(self, data, y =None  , **fit_params):
        self.func = None
		## something
        return self

    def transform(self,data, y =None,):
        ## something
        return data
        
        
class CategoryHandler(BaseEstimator, TransformerMixin,SKDecorate) :
    def __init__(self, ) :
       ## something
        

    def fit(self, data, y =None  , **fit_params):
        self.func = None
		## something
        return self

    def transform(self,data, y =None,):
        ## something
        return data

위와 같이 특정 custom한 클래스 안에 다가 SKDecorate를 추가하면 된다.
Pipeline은 다음과 같이 Define을 하였다. 여기서는 전처리 후에 차원 축소를 하는 것을 진행해봤다.

trans_info = {}
Steps = [
    ("NumericImpute", MissingHandling(method="mean", 
                               trans_info= trans_info,
                               var= num_var)) , 
    ("FactorImpute", MissingHandling(method="most_frequent", 
                               trans_info= trans_info,
                               var= fac_var)) , 
    ("numeric", NumericHandler(method="normal",
                               trans_info= trans_info,
                               num_var=num_var)),
    ('category', CategoryHandler(trans_info= trans_info, 
                                 method= "onehot",
                                num_var = num_var , 
                                in_var = in_var,
                                 target_var= target_var)),
    ("PCA", PCA(n_components= 10))
]


pipe = Pipeline(Steps)
pipe.fit(train[in_var])

 

그리고 기존에 저장하는 방식을 그대로 사용해서 저장이 된다!

그리고 이걸 다시 불러와서 사용하다면?

 

!!

재사용이 가능한 것을 확인하였다. 이것의 장점은 이미 fitting시켰기 때문에 굳이 다른 Parameter들은 필요하지 않다는 것이 가장 큰 장점인 것 같다. 
물론 여러 데이터 셋에서 다 처리할 수 있는지는 좀 더 테스트를 해봐야겠다.

암튼 SKDecorate를 이용하면 custom class까지 저장해서 사용할 수 있다는 것이 이글의 핵심이다

728x90