标星★公众号 爱你们♥ 作者:George Seif、Thomas Wolf、Lukas Frei 编译:1+1=6 | 公众号海外部 近期原创文章: ♥♥♥2万字干货: ♥♥♥♥♥♥♥♥♥♥♥♥♥♥ 前言 您可能经常听到有关Python运行速度慢的抱怨。与其他编程语言相比,Python确实较慢。有几种方法可以加速代码:如果您的代码是纯Python,如果您有一个大型的for循环,您只能使用它,而不能使用矩阵,因为数据必须按顺序处理。那么有没有办法加快Python本身的速度呢?让我们来看看Cython吧!
x = 0.5
文末下载Cython相关书籍 什么是Cython? Cython是Python和C / C++之间的一个中间步骤。它允许您编写纯Python代码,并进行一些小的修改,然后将其直接翻译成C代码。Cython语言是Python的一个超集,它包含两种类型的对象:您只需向每个变量添加类型信息。通常,我们可以在Python中声明一个变量,例如:
cdef float x = 0.5
pip install cython
使用Cython,我们为该变量添加了一个类型声明:
这告诉Cython,该变量是一个浮点数,就像我们在C中所做的一样。对于纯Python,变量的类型是动态确定的。Cython中的显式类型声明使其转换为C代码成为可能,因为显式类型声明需要+。有许多方法可以测试、编译和发布Cython代码。Cython甚至可以像Python一样直接在Jupyter Notebook中使用。安装Cython只需要一行pip:
%load_ext Cython
使用Cython需要安装C语言编译器,因此,安装过程因您当前的操作系统而异。对于Linux,通常使用GNU C编译器(gncc)。对于Mac OS,您可以下载Xcode以获取gncc。而在Windows桌面系统下安装C编译器会更复杂。使用%load_ext Cython指令在Jupyter notebook中加载Cython扩展。然后通过指令%%cython,我们就可以像Python一样在Jupyter notebook中使用Cython。如果在执行Cython代码时遇到编译错误,请检查Jupyter终端的完整输出信息。大多数情况下可能是因为在%%cython之后遗漏了-+标签(比如当您使用spaCy Cython接口时)。如果编译器报告有关NumPy的错误,则可能是遗漏了import numpy。如果您要在IPython中使用Cython,请首先介绍一下IPython Magic命令。Magic命令以百分号开头,通常有两种类型:首先运行以下语句引入Cython:
%%cython
def test(x):
y = 1
for i in range(x+1):
y *= i
return y
然后,在运行Cython代码时,我们需要添加以下Cython代码:
然后,您就可以愉快地使用Cython了。Cython中的类型 在Cython中,变量和函数有两组不同的类型。对于变量,我们有:请注意,所有这些类型都来自C / C++!我们可以方便地将结果传递给C代码,并返回结果,Cython会自动进行类型转换。了解了Cython类型之后,我们就可以直接实现加速了!如何使用Cython加速代码 我们首先需要设置Python代码的基准:一个用于计算阶乘的for循环。原始的Python代码如下:
import pyximport; pyximport.install()
import my_cython_module
Cython的实现过程看起来非常相似。首先,确保Cython代码文件具有.pyx扩展名。这些文件将由Cython编译器编译为C或C++文件,然后由C编译器进一步编译为字节码文件。您还可以使用pyximport将.pyx文件直接加载到Python程序中:
您还可以将自己的Cython代码构建为Python包,然后像正常的Python包一样导入或发布。但是,这种方法需要更多的时间,特别是如果您希望Cython包能够在所有平台上运行。如果您需要一个参考示例,可以查看spaCy的安装脚本:
cpdef int test(int x):
cdef int y = 1
cdef int i
for i in range(x+1):
y *= i
return y
from distutils.core import setup
from Cython.Build import cythonize
setup(ext_modules = cythonize('run_cython.pyx'))
最终,Python解释器将能够调用这些字节码文件。对代码本身的唯一更改是我们已经为每个变量和函数声明了类型。请注意,函数具有cpdef以确保我们可以从Python调用它。另外,请注意我们的循环变量i如何具有类型。您需要为函数中的所有变量设置类型,以便C编译器知道使用哪种类型!接下来,创建一个setup.py文件,将Cython代码编译为C代码:
python setup.py build_ext --inplace
import run_python
import run_cython
import time
number = 10
start = time.time()
run_python.test(number)
end = time.time()
py_time = end - start
print("Python time = {}".format(py_time))
start = time.time()
run_cython.test(number)
end = time.time()
cy_time = end - start
print("Cython time = {}".format(cy_time))
print("Speedup = {}".format(py_time / cy_time))
然后执行编译:Boom!我们的C代码已经编译好,可以使用了!您将看到,在Cython代码所在的文件夹中,有所有运行C代码所需的文件,包括run_cython.c文件。如果您感兴趣,可以查看Cython生成的C代码!现在我们准备测试新的C代码!查看下面的代码,它将执行一个速度测试,将原始Python代码与Cython代码进行比较。现在我们准备测试我们新的超快速C代码了!查看下面的代码,它执行速度测试以将原始Python代码与Cython代码进行比较。
Cython可以让您在几乎所有原始Python代码上获得良好的加速,而不需要太多额外的工作。需要注意的关键是,循环次数越多,处理的数据越多,Cython可以提供的帮助就越多。查看下表,该表显示了Cython为不同阶乘值提供的速度,我们使用Cython获得了超过36倍的加速!
Cython在NLP中的加速应用 当我们处理字符串时,如何在Cython中设计更高效的循环呢?spaCy是一个不错的选择!spaCy中的所有Unicode字符串(标记的文本、其小写文本、其词形形式、POS标签、解析树依赖标签、命名实体标签等)都存储在一个称为StringStore的数据结构中,它通过一个64位哈希码进行索引,例如C类型的uint64_t。
StringStore对象实现了Python Unicode字符串与64位哈希码之间的查找映射。它可以在spaCy的任何地方和任意对象中访问,例如npl.vocab.strings、doc.vocab.strings或span.doc.vocab.string。当某个模块需要在某些标记上获得更快的处理速度时,可以使用C语言类型的64位哈希码代替字符串来实现。调用StringStore查找表将返回与该哈希码相关联的Python Unicode字符串。但是,spaCy能做的不仅仅是这些,它还允许我们访问完全填充的C语言类型结构的文档和词汇表,我们可以在Cython循环中使用这些结构,而不必构建自己的结构。spaCy拓展:
import urllib.request
import spacy
with urllib.request.urlopen('https://raw.githubusercontent.com/pytorch/examples/master/word_language_model/data/wikitext-2/valid.txt') as response:
text = response.read()
nlp = spacy.load('en')
doc_list = list(nlp(text[:800000].decode('utf8')) for i in range(10))
建立一个脚本用于创建一个包含10份文档的列表,每份文档大约包含17万个单词,使用spaCy进行分析。当然,我们也可以对17万份文档(每份文档包含10个单词)进行分析,但这样做会导致创建过程非常慢,因此我们选择了10份文档。我们想要在这个数据集上执行某些自然语言处理任务。例如,我们可以统计数据集中作为名词出现的单词”run”的次数(例如,被spaCy标记为”NN”词性标签)。使用Python循环来实现上述分析过程非常简单和直观:
这段代码至少需要运行1.4秒才能获得答案。如果我们的数据集中包含数百万个文档,为了获得答案,我们可能需要花费超过一天的时间。我们也许可以使用多线程来加速,但在Python中这种做法并不明智,因为您还需要处理全局解释器锁(GIL)。在Cython中,可以无视GIL的存在而自由使用线程加速。但是不能再使用Python中的字典和列表,因为Python中的变量都自动带有锁(GIL)。幸运的是,Cython已经封装了C++标准库中的容器:deque、list、map、pair、queue、set、stack、vector。完全可以替代Python的dict、list、set等。我们使用Cython就可以解决这个问题,但不能再使用Python中的字典和列表,因为Python中的变量都自动带有锁(GIL)。幸运的是,Cython已经封装了C++标准库中的容器:deque、list、map、pair、queue、set、stack、vector。完全可以替代Python的dict、list、set等。另外,请注意,Cython也可以使用多线程!Cython在后台可以直接调用OpenMP。现在让我们尝试使用spaCy和Cython来加速Python代码。首先需要考虑好数据结构,我们需要一个C类型的数组来存储数据,需要指针来指向每个文档的TokenC数组。我们还需要将测试字符(”run”和”NN”)转换为64位哈希码。当所有需要处理的数据都变成了C类型对象,我们就可以以纯C语言的速度对数据集进行迭代。以下是转换为Cython和spaCy的实现:
%%cython
cdef extern from "math.h":
cpdef double sin(double x)
在Jupyter notebook上,这段Cython代码运行了大约20毫秒,比之前的纯Python循环快了大约80倍。使用Jupyter notebook单元编写模块的速度很可观,它可以与其他Python模块和函数自然地连接:在20毫秒内扫描大约170万个单词,这意味着我们每秒能够处理高达8千万个单词。如果您已经了解C语言,Cython还允许访问C代码,而Cython的创建者还没有为这些代码添加现成的声明。例如,使用以下代码,可以为C函数生成Python包装器并将其添加到模块dict中。
Cython注意的陷阱 1、.pyx中用CDEF定义的内容,除类以外对的.py都是不可见的。 2、.c中不能操作C类型,如果想在.py中操作C类型,就要在.pyx中从Python对象转换为C类型,或者用含有set/get方法的C类型包装类。 3、虽然Cython可以自动进行Python的str和C的”char *”之间的类型转换,但对于固定长度的字符串”char a[n]”是无法自动转换的。需要使用Cython的libc.string.strcpy进行显式拷贝。 4、回调函数需要用函数包装,并通过C的”void *”强制转换后才能传入C函数。 Cython相关资料(下载) 0、其他:
1、官方文档: 2、参考书籍(文末下载): 书籍下载 在后台输入(严格大小写) Cython资料 —End— 量化投资与机器学习微信公众号,是业内垂直于Quant、MFE、CST、AI等专业的主流量化自媒体。公众号拥有来自公募、私募、券商、银行、海外等众多圈内10W+关注者。每日发布行业前沿研究成果和最新量化资讯。