scikit-learn模型持久化:pickle vs pmml vs ...?

12

我建立了一个scikit-learn模型,并想在每日的Python计划任务中重用它(NB:没有涉及其他平台-没有R,Java等)。

对其进行了序列化(实际上,我序列化了自己的对象,其中一个字段是GradientBoostingClassifier),并在计划任务中解除序列化。 到目前为止还算顺利(已在Save classifier to disk in scikit-learnModel persistence in Scikit-Learn?中讨论过)。

但是,我升级了sklearn,现在我得到了这些警告:

.../.local/lib/python2.7/site-packages/sklearn/base.py:315: 
UserWarning: Trying to unpickle estimator DecisionTreeRegressor from version 0.18.1 when using version 0.18.2. This might lead to breaking code or invalid results. Use at your own risk.
UserWarning)
.../.local/lib/python2.7/site-packages/sklearn/base.py:315: 
UserWarning: Trying to unpickle estimator PriorProbabilityEstimator from version 0.18.1 when using version 0.18.2. This might lead to breaking code or invalid results. Use at your own risk.
UserWarning)
.../.local/lib/python2.7/site-packages/sklearn/base.py:315: 
UserWarning: Trying to unpickle estimator GradientBoostingClassifier from version 0.18.1 when using version 0.18.2. This might lead to breaking code or invalid results. Use at your own risk.
UserWarning)

现在该怎么办?

  • 我可以降级到0.18.1并一直使用它,直到我准备重建模型。出于各种原因,我认为这是不可接受的。

  • 我可以取消pickle文件的封存并重新pickle它。这适用于0.18.2,但在0.19中会出现问题。NFG。joblib看起来也不错。

  • 我希望能将数据保存在版本无关的ASCII格式(例如JSON或XML)中。显然,这是最优的解决方案,但似乎没有方法可以做到这一点(请参见Sklearn - model persistence without pkl file)。

  • 我可以将模型保存到PMML,但它的支持至多只是温和的: 我可以使用sklearn2pmml(虽然不容易)保存模型,并使用augustus/lightpmmlpredictor应用(但不能加载)模型。但是,这些项目都不直接可用于pip,这使得部署成为一场噩梦。此外,augustus&lightpmmlpredictor项目似乎已死亡。Importing PMML models into Python (Scikit-learn) - 不行。

  • 以上的变体:使用sklearn2pmml保存PMML,并使用openscoring进行评分。需要与外部进程交互。糟糕。

有什么建议吗?

1个回答

5
跨不同版本的scikit-learn进行模型持久化通常是不可能的。原因很明显:您用一个定义pickle Class1,而希望将其解pickle到另一个定义的Class2中。
您可以:
  • 仍然尝试坚持使用一个版本的sklearn。
  • 忽略警告,并希望对于Class1有效的内容也适用于Class2。
  • 编写自己的类,可以序列化您的GradientBoostingClassifier并从该序列化形式还原它,并希望它比pickle更好用。
我制作了一个示例,演示了如何将单个DecisionTreeRegressor转换为纯列表和字典格式,完全兼容JSON,并将其恢复回来。
import numpy as np
from sklearn.tree import DecisionTreeRegressor
from sklearn.datasets import make_classification

### Code to serialize and deserialize trees

LEAF_ATTRIBUTES = ['children_left', 'children_right', 'threshold', 'value', 'feature', 'impurity', 'weighted_n_node_samples']
TREE_ATTRIBUTES = ['n_classes_', 'n_features_', 'n_outputs_']

def serialize_tree(tree):
    """ Convert a sklearn.tree.DecisionTreeRegressor into a json-compatible format """
    encoded = {
        'nodes': {},
        'tree': {},
        'n_leaves': len(tree.tree_.threshold),
        'params': tree.get_params()
    }
    for attr in LEAF_ATTRIBUTES:
        encoded['nodes'][attr] = getattr(tree.tree_, attr).tolist()
    for attr in TREE_ATTRIBUTES:
        encoded['tree'][attr] = getattr(tree, attr)
    return encoded

def deserialize_tree(encoded):
    """ Restore a sklearn.tree.DecisionTreeRegressor from a json-compatible format """
    x = np.arange(encoded['n_leaves'])
    tree = DecisionTreeRegressor().fit(x.reshape((-1,1)), x)
    tree.set_params(**encoded['params'])
    for attr in LEAF_ATTRIBUTES:
        for i in range(encoded['n_leaves']):
            getattr(tree.tree_, attr)[i] = encoded['nodes'][attr][i]
    for attr in TREE_ATTRIBUTES:
        setattr(tree, attr, encoded['tree'][attr])
    return tree

## test the code

X, y = make_classification(n_classes=3, n_informative=10)
tree = DecisionTreeRegressor().fit(X, y)
encoded = serialize_tree(tree)
decoded = deserialize_tree(encoded)
assert (decoded.predict(X)==tree.predict(X)).all()

有了这个,您就可以继续对整个 GradientBoostingClassifier 进行序列化和反序列化:

from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble.gradient_boosting import PriorProbabilityEstimator

def serialize_gbc(clf):
    encoded = {
        'classes_': clf.classes_.tolist(),
        'max_features_': clf.max_features_, 
        'n_classes_': clf.n_classes_,
        'n_features_': clf.n_features_,
        'train_score_': clf.train_score_.tolist(),
        'params': clf.get_params(),
        'estimators_shape': list(clf.estimators_.shape),
        'estimators': [],
        'priors':clf.init_.priors.tolist()
    }
    for tree in clf.estimators_.reshape((-1,)):
        encoded['estimators'].append(serialize_tree(tree))
    return encoded

def deserialize_gbc(encoded):
    x = np.array(encoded['classes_'])
    clf = GradientBoostingClassifier(**encoded['params']).fit(x.reshape(-1, 1), x)
    trees = [deserialize_tree(tree) for tree in encoded['estimators']]
    clf.estimators_ = np.array(trees).reshape(encoded['estimators_shape'])
    clf.init_ = PriorProbabilityEstimator()
    clf.init_.priors = np.array(encoded['priors'])
    clf.classes_ = np.array(encoded['classes_'])
    clf.train_score_ = np.array(encoded['train_score_'])
    clf.max_features_ = encoded['max_features_']
    clf.n_classes_ = encoded['n_classes_']
    clf.n_features_ = encoded['n_features_']
    return clf

# test on the same problem
clf = GradientBoostingClassifier()
clf.fit(X, y);
encoded = serialize_gbc(clf)
decoded = deserialize_gbc(encoded)
assert (decoded.predict(X) == clf.predict(X)).all()

这适用于scikit-learn v0.19,但是我不知道下一个版本会有什么来破坏这段代码。我既不是预言家,也不是sklearn的开发人员。
如果你想完全独立于sklearn的新版本,最安全的方法是编写一个遍历序列化树并作出预测的函数,而不是重新创建一个sklearn树。

这种方法比pickle更可靠吗?pickle的问题在于,如果sklearn更改了类定义(例如删除或重命名插槽),我将不得不重写serialize_*deserialize_*函数,并且更重要的是编写反序列化器,将旧版本的序列化转换为新版本。我同意这可能比pickle噩梦好,但并不是很多。 - sds
这并不能保证你与sklearn的20或200版本兼容。但至少它让你对情况有更多的掌控。例如,如果sklearn完全重写了它的ClassificationLossFunction,你也不会受到影响。 - David Dale

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接