24

我正在尝试腌制一个 sklearn 机器学习模型,并将其加载到另一个项目中。该模型被包裹在管道中,该管道执行特征编码、缩放等。当我想在管道中使用自写转换器来执行更高级的任务时,问题就开始了。

假设我有两个项目:

  • train_project:它在 src.feature_extraction.transformers.py 中有自定义转换器
  • use_project:它在src中有其他东西,或者根本没有src目录

如果在“train_project”中我使用 joblib.dump() 保存管道,然后在“use_project”中使用 joblib.load() 加载它,它将找不到诸如“src.feature_extraction.transformers”之类的内容并抛出异常:

ModuleNotFoundError:没有名为“src.feature_extraction”的模块

我还应该补充一点,我从一开始的意图是简化模型的使用,因此程序师可以像加载任何其他模型一样加载模型,传递非常简单的、人类可读的特征,以及对实际模型的所有“神奇”特征预处理(例如梯度提升)正在内部发生。

我想在两个项目的根目录下创建 /dependencies/xxx_model/ 目录,并在其中存储所有需要的类和函数(将代码从“train_project”复制到“use_project”),这样项目的结构是相同的,并且可以加载转换器。我发现这个解决方案非常不优雅,因为它会强制使用该模型的任何项目的结构。

我想只是在“use_project”中重新创建管道和所有变压器,并以某种方式从“train_project”加载变压器的拟合值。

最好的解决方案是,如果转储文件包含所有需要的信息并且不需要依赖项,我真的很震惊 sklearn.Pipelines 似乎没有这种可能性 - 如果我以后不能加载合适的对象,那么安装管道有什么意义?是的,如果我只使用 sklearn 类而不创建自定义类,它会起作用,但非自定义类没有所有需要的功能。

示例代码:

火车项目

src.feature_extraction.transformers.py

from sklearn.pipeline import TransformerMixin
class FilterOutBigValuesTransformer(TransformerMixin):
    def __init__(self):
        pass

    def fit(self, X, y=None):
        self.biggest_value = X.c1.max()
        return self

    def transform(self, X):
        return X.loc[X.c1 <= self.biggest_value]

火车项目

主文件

from sklearn.externals import joblib
from sklearn.preprocessing import MinMaxScaler
from src.feature_extraction.transformers import FilterOutBigValuesTransformer

pipeline = Pipeline([
    ('filter', FilterOutBigValuesTransformer()),
    ('encode', MinMaxScaler()),
])
X=load_some_pandas_dataframe()
pipeline.fit(X)
joblib.dump(pipeline, 'path.x')

测试项目

主文件

from sklearn.externals import joblib

pipeline = joblib.load('path.x')

预期的结果是使用可以使用的转换方法正确加载管道。

加载文件时实际结果是异常。

4

4 回答 4

7

我找到了一个非常简单的解决方案。假设您使用 Jupyter 笔记本进行培训:

  1. 创建一个.py定义自定义转换器的文件并将其导入 Jupyter 笔记本。

这是文件custom_transformer.py

from sklearn.pipeline import TransformerMixin

class FilterOutBigValuesTransformer(TransformerMixin):
    def __init__(self):
        pass

    def fit(self, X, y=None):
        self.biggest_value = X.c1.max()
        return self

    def transform(self, X):
        return X.loc[X.c1 <= self.biggest_value]
  1. 训练您的模型,从文件中导入此类.py并使用joblib.
import joblib
from custom_transformer import FilterOutBigValuesTransformer
from sklearn.externals import joblib
from sklearn.preprocessing import MinMaxScaler

pipeline = Pipeline([
    ('filter', FilterOutBigValuesTransformer()),
    ('encode', MinMaxScaler()),
])

X=load_some_pandas_dataframe()
pipeline.fit(X)

joblib.dump(pipeline, 'pipeline.pkl')
  1. 在不同的 python 脚本中加载.pkl文件时,您必须导入.py文件才能使其工作:
import joblib
from utils import custom_transformer # decided to save it in a utils directory

pipeline = joblib.load('pipeline.pkl')

于 2020-05-20T18:37:13.890 回答
1

我创建了一个解决方案。我不认为它是我问题的完整答案,但它让我从我的问题中继续前进。

变通办法起作用的条件:

一、管道只需要2种变压器:

  1. sklearn 变形金刚
  2. 自定义转换器,但只有类型的属性:
    • 数字
    • 细绳
    • 列表
    • 听写

或任何组合,例如带有字符串和数字的字典列表。通常重要的是属性是 json 可序列化的。

二、管道步骤的名称必须是唯一的(即使存在管道嵌套)


简而言之,模型将存储为包含 joblib 转储文件的目录、用于自定义转换器的 json 文件以及包含有关模型的其他信息的 json 文件。

我创建了一个函数,它通过管道的步骤并检查变压器的 __module__ 属性。

如果它在其中找到 sklearn,那么它将以步骤(步骤元组的第一个元素)中指定的名称运行 joblib.dump 函数到某些选定的模型目录。

否则(在 __module__ 中没有 sklearn)它会将转换器的 __dict__ 添加到 result_dict 中,其键与步骤中指定的名称相同。最后,我将 result_dict json.dump 到名为 result_dict.json 的模型目录中。

如果需要进入某个转换器,因为例如管道中有一个管道,您可以通过在函数的开头添加一些规则来递归地运行这个函数,但是始终拥有唯一的步骤/转换器变得很重要甚至在主管道和子管道之间命名。

如果创建模型管道需要其他信息,请将它们保存在 model_info.json 中。


然后,如果您想加载模型以供使用:您需要在目标项目中创建(不拟合)相同的管道。如果管道创建有点动态,并且您需要来自源项目的信息,则从 model_info.json 加载它。

您可以复制用于序列化的函数,并且:

  • 用 joblib.load 语句替换所有 joblib.dump,将 __dict__ 从加载的对象分配给已经在管道中的对象的 __dict__
  • 将添加 __dict__ 到 result_dict 的所有位置替换为将 result_dict 中的适当值分配给对象 __dict__ (记得事先从文件中加载 result_dict)

运行此修改后的功能后,先前未拟合的管道应该具有加载拟合效果的所有转换器属性,并且整个管道准备好进行预测。

我不喜欢这个解决方案的主要事情是它需要目标项目中的管道代码,并且需要自定义转换器的所有属性都是 json 可序列化的,但是我把它留在这里给遇到类似问题的其他人,也许有人会来有了更好的东西。

于 2019-10-03T08:43:34.120 回答
1

根据我的研究,似乎最好的解决方案是创建一个 Python 包,其中包含经过训练的管道和所有文件。

然后你可以将它 pip 安装在你想使用它的项目中,并使用from <package name> import <pipeline name>.

于 2020-08-04T13:10:25.507 回答
0

你试过用云泡菜吗? https://github.com/cloudpipe/cloudpickle

于 2020-01-28T11:46:23.320 回答