这里有几个问题,让我们一一解决:
那么,它是如何工作的?据我了解,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 在这里做什么?它嵌入了关于使用什么模块来定义它在泡菜中腌制的任何内容的信息。然后当它 unpickle 某些东西时,它使用该信息来导入模块,以便它可以使用类的 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 中使用的新名称。