为啥这段代码不用import sklearn就可以使用sklearn函数?

Posted

技术标签:

【中文标题】为啥这段代码不用import sklearn就可以使用sklearn函数?【英文标题】:Why is this code able to use the sklearn function without import sklearn?为什么这段代码不用import sklearn就可以使用sklearn函数? 【发布时间】:2021-11-14 15:29:20 【问题描述】:

所以我只是看了一个教程,作者在anaconda环境(安装了sklearn)中使用predict腌制模型功能时不需要import sklearn

我试图在 Google Colab 中重现它的最小版本。如果你有一个 pickled-sklearn-model,下面的代码可以在 Colab 中运行(安装了 sklearn):

import pickle
model = pickle.load(open("model.pkl", "rb"), encoding="bytes")
out = model.predict([[20, 0, 1, 1, 0]])
print(out)

我意识到我仍然需要安装 sklearn 包。如果我卸载 sklearn,predict 功能现在不起作用:

!pip uninstall scikit-learn
import pickle
model = pickle.load(open("model.pkl", "rb"), encoding="bytes")
out = model.predict([[20, 0, 1, 1, 0]])
print(out)

错误:

WARNING: Skipping scikit-learn as it is not installed.

---------------------------------------------------------------------------

ModuleNotFoundError                       Traceback (most recent call last)

<ipython-input-1-dec96951ae29> in <module>()
      1 get_ipython().system('pip uninstall scikit-learn')
      2 import pickle
----> 3 model = pickle.load(open("model.pkl", "rb"), encoding="bytes")
      4 out = model.predict([[20, 0, 1, 1, 0]])
      5 print(out)

ModuleNotFoundError: No module named 'sklearn'

那么,它是如何工作的?据我了解泡菜不依赖于scikit-learn。序列化模型做import sklearn吗? 为什么在第一个代码中不用import scikit learn就可以使用predict函数?

【问题讨论】:

你确定作者没有导入sklearn? 是的,我上面的第一个代码在 Colab 中工作,你也可以看到我在这里提到的代码:github.com/alfanme/dts-deployment-linreg/blob/main/app.py at line 34 he use predict() 当您尝试在不使用 'scikit-learn` 的情况下运行时,能否包含完整的错误消息? 我已经更新了问题 Pickling 不会序列化/存储模块依赖项。所以你不仅需要在目标机器上安装scikit-learn,而且它的版本必须与源机器上的相同或兼容。 【参考方案1】:

这里有几个问题,让我们一一解决:

那么,它是如何工作的?据我了解,pickle 不依赖于 scikit-learn。

这里的 scikit-learn 没有什么特别之处。 Pickle 对于任何模块都会表现出这种行为。这是一个 Numpy 的例子:

will@will-desktop ~ $ python
Python 3.9.6 (default, Aug 24 2021, 18:12:51) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> 'numpy' in sys.modules
False
>>> import numpy
>>> 'numpy' in sys.modules
True
>>> pickle.dumps(numpy.array([1, 2, 3]))
b'\x80\x04\x95\xa0\x00\x00\x00\x00\x00\x00\x00\x8c\x15numpy.core.multiarray\x94\x8c\x0c_reconstruct\x94\x93\x94\x8c\x05numpy\x94\x8c\x07ndarray\x94\x93\x94K\x00\x85\x94C\x01b\x94\x87\x94R\x94(K\x01K\x03\x85\x94h\x03\x8c\x05dtype\x94\x93\x94\x8c\x02i8\x94\x89\x88\x87\x94R\x94(K\x03\x8c\x01<\x94NNNJ\xff\xff\xff\xffJ\xff\xff\xff\xffK\x00t\x94b\x89C\x18\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x94t\x94b.'
>>> exit()

到目前为止,我所做的是表明在新的 Python 进程中 'numpy' 不在 sys.modules (导入模块的字典)中。然后我们导入 Numpy,然后腌制一个 Numpy 数组。

然后在下图所示的一个新的 Python 进程中,我们看到在 unpickle 之前数组 Numpy 没有被导入,但是在我们已经导入 Numpy 之后。

will@will-desktop ~ $ python
Python 3.9.6 (default, Aug 24 2021, 18:12:51) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> import sys
>>> 'numpy' in sys.modules
False
>>> pickle.loads(b'\x80\x04\x95\xa0\x00\x00\x00\x00\x00\x00\x00\x8c\x15numpy.core.multiarray\x94\x8c\x0c_reconstruct\x94\x93\x94\x8c\x05numpy\x94\x8c\x07ndarray\x94\x93\x94K\x00\x85\x94C\x01b\x94\x87\x94R\x94(K\x01K\x03\x85\x94h\x03\x8c\x05dtype\x94\x93\x94\x8c\x02i8\x94\x89\x88\x87\x94R\x94(K\x03\x8c\x01<\x94NNNJ\xff\xff\xff\xffJ\xff\xff\xff\xffK\x00t\x94b\x89C\x18\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x94t\x94b.')
array([1, 2, 3])
>>> 'numpy' in sys.modules
True
>>> numpy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'numpy' is not defined

尽管已导入,但numpy 仍然不是已定义的变量名。 Python 中的导入是全局的,但导入只会更新实际执行导入的模块的命名空间。如果我们想访问numpy,我们仍然需要编写import numpy,但是由于Numpy 已经在进程的其他地方导入,这不会重新运行Numpy 的模块初始化代码。相反,它将在我们模块的全局字典中创建一个 numpy 变量,并使其成为对预先存在的 Numpy 模块对象的引用,并且可以通过 sys.modules['numpy'] 访问。

那么,Pickle 在这里做什么?它嵌入了关于使用什么模块来定义它在泡菜中腌制的任何内容的信息。然后当它 unp​​ickle 某些东西时,它使用该信息来导入模块,以便它可以使用类的 unpickle 方法。我们可以查看 Pickle 模块的源代码,我们可以看到正在发生的事情:

_Pickler 中我们看到save 方法使用save_global 方法。这又使用whichmodule 函数获取模块名称(在您的情况下为'scikit-learn'),然后将其保存在pickle 中。

_UnPickler 中,我们看到find_class 方法使用__import__ 使用存储的模块名称导入模块。 find_class 方法用于一些load_* 方法,例如load_inst,用于加载类的实例,例如您的模型实例:

def load_inst(self):
    module = self.readline()[:-1].decode("ascii")
    name = self.readline()[:-1].decode("ascii")
    klass = self.find_class(module, name)
    self._instantiate(klass, self.pop_mark())

The documentation for Unpickler.find_class explains:

如有必要,导入模块并从中返回名为 name 的对象,其中模块和 name 参数是 str 对象。

The docs also explain how you can restrict this behaviour:

[你]可能想通过自定义 Unpickler.find_class() 来控制哪些内容被取消。不像它的名字所暗示的那样,Unpickler.find_class() 每当请求全局(即类或函数)时都会被调用。因此,可以完全禁止全局变量或将它们限制为安全子集。

虽然这通常仅在取消不可信数据时才相关,但此处似乎并非如此。


序列化模型是否导入sklearn?

严格来说,序列化模型本身不任何事情。如上所述,这一切都由 Pickle 模块处理。


为什么在第一个代码中不用import scikit learn就可以使用predict函数?

因为 sklearn 是由 Pickle 模块在解包数据时导入的,从而为您提供了一个完全实现的模型对象。就像其他模块导入 sklearn,创建模型对象,然后将其作为参数传递给函数一样。


因此,为了取消对模型的腌制,您需要安装 sklearn - 最好与用于创建腌制的版本相同。一般来说,Pickle 模块存储任何所需模块的完全限定路径,因此腌制对象和取消腌制对象的 Python 进程必须具有所有 [1] 必需模块并具有相同的完全限定名称。


[1] 需要注意的是,Pickle 模块可以自动调整/修复特定模块/类的某些导入,这些模块/类在 Python 2 和 3 之间具有不同的完全限定名称。来自the docs:

如果 fix_imports 为 true,pickle 将尝试将旧的 Python 2 名称映射到 Python 3 中使用的新名称。

【讨论】:

很好的例子,解释,也感谢您的参考!我现在明白了【参考方案2】:

第一次腌制模型时,您已经安装了 sklearn。 pickle 文件的结构依赖于 sklearn,因为它所代表的对象的类是 sklearn 类,pickle 需要知道该类结构的详细信息才能解开对象。

当您尝试在未安装 sklearn 的情况下对文件进行 unpickle 时,pickle 从文件中确定对象是实例的类是 sklearn.x.y.z 或者您有什么,然后 unpickling 失败,因为模块 sklearnpickle 尝试解析该名称时找不到。请注意,异常发生在 unpickling 行上,而不是在调用 predict 的行上。

你不需要在你的代码中导入sklearn,因为一旦对象被解压,它就知道它的类是什么以及它所有的方法名是什么,所以你可以从对象中调用它们。

【讨论】:

以上是关于为啥这段代码不用import sklearn就可以使用sklearn函数?的主要内容,如果未能解决你的问题,请参考以下文章

为啥“import *”不好?

python import sklearn包出错 ImportError: No module named nose.tools

用Python,在 import sklearn 总是报错怎么办

为啥这段代码在 valgrind (helgrind) 下失败?

from sklearn.externals import joblib 失败

为啥这段代码会泄露? (简单的代码片段)