关键时刻,第一时间送达!
作者:景略集智
https://weibo.com/ttarticle/p/show?id=2309404260639311816588
Python开发整理发布,转载请联系作者获得授权
科创公司Hugging Face机器学习专家Thomas Wolf去年曾带领团队推出一款Python共指解析工具包NeuralCoref,用神经网络解析句子中的共同指代词。
工具包发布以后,Thomas收到了来自技术社区的积极反馈,但也发现了一个大问题:工具包在处理对话信息是反应迅速,但处理文本较长的新闻文章时速度就变得非常缓慢。
最终,Thomas经过种种努力解决了这个问题,推出的NeuralCoref新版在保证准确率的同时,将处理速度提升了100倍!而且,工具包依然易于使用,也符合Python库的生态环境。
Thomas 随后将他解决这个问题的心得总结了出来,把如何将Python自然语言处理速度提高100倍的经验分享给大家,其中涉及:
怎样才能用Python设计出一个高效率模块
怎样利用好 spaCy 的内置数据结构,从而设计出超高效的自然语言处理函数
在本文,Thomas将讲解如何利用 Cython 和 spaCy 让 Python 在自然语言处理任务中的速度提高百倍。
开始前,我(作者Thomas Wolf——译者注)得承认文章略微有些标题党,因为虽然我们会讨论Python,但也会包含一些Cython技巧。不过,你知道吗?Cython就是Python的超集啊,所以不要被它吓跑!你当前所写的Python项目已经算是一种Cython项目了。
下面是一些你可能需要本文所说Python加速策略的情况:
你在用Python开发一款用于NLP任务的产品模块。
你在用Python计算一个大型NLP数据集的分析数据。
你在为PyTorch/TensorFlow这样的深度学习框架预处理大型训练数据集,或你的深度学习模型的批次加载器(batch loader)采用了非常复杂的处理逻辑,严重减缓了你的训练时间。
实现百倍加速第一步:分析代码
第一件你需要知道的事情就是,你的大部分代码在纯Python环境都能运行良好,但其中的一些性能瓶颈问题,只要你略表“关切”,就能让程序的速度加速几个量级。
因此,你应该着手分析你的Python代码,找到那些运行很慢的部分。解决这个问题的一种方法就是使用cProfile:
你会发现运行缓慢的部分基本就是一些循环,或者你用的神经网络里有太多的Numpy数组操作(这里就不再详细讨论Numpy的问题了,因为已经有很多这方面的分析资料)。
那么,我们该怎么加速这些循环?
借助一点Cython技巧,为Python中的循环提速
我们以一个简单的例子讲解一下。比方说我们有很多矩形,将它们保存为一列Python对象,比如Rectangle类的实例。我们模块的主要工作就是迭代该列表,计算有多少矩形的面积大于所设阙值。 我们的Python模块会非常简单,就像这样:
这里的Check_rectangles函数就是我们要解决的瓶颈!它循环了大量的Python对象,这会变得非常慢,因为Python迭代器每次迭代时都要在背后做大量工作(查询类中的area方法,打包和解包参数,调取Python API···)。
这里我们可以借助Cython帮我们加快循环速度。
Cython语言是Python的超集,Python包含两种对象:
Python对象就是我们在常规Python中操作的对象,比如数字、字符串、列表、类实例···
Cython C对象是C或C++对象,比如双精度、整型、浮点数、结构和向量,Cython能以运行超快的低级代码编译它们。
这里的循环我们使用Cython循环就能获得更快的运行速度,而我们只需获取Cython C对象。
设计这种循环的一个直接方法就是定义C结构,它会包含我们计算中所需的全部东西:在我们这里所举的例子中,就是矩形的长和宽。
然后我们将矩形列表保存在所定义的C结构的数组中,我们会将数组传入check_rectangle函数中。该函数现在必需接受C数组作为输入,这样就会被定义为Cython函数,使用cdef关键字而非def(cdef也用于定义Cython C对象)。
这里是我们的Python模块的高速Cython版的样子:
这里我们使用C指针的原生数组,但是你也可以选择其他选项,尤其是C++结构,比如向量、二元组、队列之类。在这里的脚本中,我还使用了cymem的很方面的Pool()内存管理对象,避免了必须手动释放所申请的C数组内存空间。当Python不再需要Pool时,它会自动释放我们用它申请时所占的内存。
我们试试代码!我们有很多种方法可以测试、编辑和分发Cython代码!Cython甚至还能像Python一样直接在Jupyter Notebook中使用。
首先用pip install cython安装Cython。
首先在Jupyter中测试
在Jupyter notebook中用%load_ext Cython加载Cython扩展项。
现在我们就可以用神奇的命令%%cython像写Python代码一样编写Cython代码。
如果你在执行Cython代码块时出现了编译错误,一定要检查一下Jupyter终端输出,看看信息是否完整。
大多数时候你可能会编译成C++时,在 %%cython后面漏掉了 a-+ 标签(例如在你使用spaCy Cython API时),或者如果编译器出现关于Numpy的报错,你可能是遗漏了import Numpy。
编写、使用和分发Cython代码
Cython代码编写为.pyx文件。这些文件被Cython编译器编译为C或C++文件,然后进一步由系统的C编译器编译为字节码文件。接着,字节码文件就能被Python解释器使用了。
你可以在Python里直接用pyximport加载.pyx文件:
importpyximport; pyximport.install()
import my_cython_module
你也可以将自己的Cython代码创建为Python包,将其作为正常Python包导入或分发。这部分工作或花费一点时间。如果你需要一个工作示例,spaCy的安装脚本是比较详细的例子。
在我们讲NLP之前,先快速说说def,cdef和cpdef关键字,因为它们是你着手使用Cython需要理解的主要知识点。
你可以在Cython中使用3种类型的函数:
Python函数是用常见关键字def定义的。它的输入和输出均为Python对象。在函数内部既可以使用Python对象,也能使用C/C++对象,同样能调用Python和Cython函数。
Cython函数是以关键字cdef定义的。可以将Python和C/C++对象作为输入和输出,也能在内部操作它们。Cython函数不能从Python环境中直接访问(Python解释器和其它纯Python模块会导入你的Cython模块),但能被其它Cython模块导入。
Cython 函数用cpdef关键字定义时和cdef定义的函数一样,但它们带有Python包装器,因此从Python环境(Python对象为输入和输出)和其它Cython模块(C/C ++或Python对象为输入)中都能调用它们。
Cdef关键字还有另一个用途,即在代码中输入Cython C/C ++。如果你没有用该关键字输入你的对象,它们会被当成Python对象(这样就会延缓访问速度)。
使用Cython和spaCy加快解决NLP问题的速度
现在一切进行的很好也很快,但是···我们还没涉及自然语言处理任务呢!没有字符串操作,没有Unicode编码,也没有我们在自然语言处理中能够使用的妙计。
总的来说,除非你很清楚自己所做的任务,不然就不要使用C类型字符串,而是使用Python字符串对象。
所以,我们操作字符串时,该怎样设计Cython中的快速循环呢?
spaCy是我们的“护身符”。spaCy解决这个问题的方式非常智能。
将所有字符串转换为64位哈希码
在spaCy中,所有的Unicode字符串(token的文本,它的小写形式文本,POS 标记标签、解析树依赖标签、命名实体标签等等)都被存储在一个叫StringStore的单数据结构中,可以被64位哈希码索引,也就是C类型unit64_t 。
StringStore对象实现了Python unicode 字符串与 64 位哈希码之间的查找映射。
当某个模块需要在某些tokens上获得更快的处理速度时,就可以使用 C 语言类型的 64 位哈希码代替字符串来实现。调用 StringStore 查找表将返回与该哈希码相关联的 Python unicode 字符串。
但是spaCy的作用不止如此,它还能让我们获取文档和词汇表的完全填充的C语言类型结构,我们可以在Cython循环中用到这一点,而不必创建我们自己的结构。
spaCy的内部数据结构和spaCy相关的主要数据结构是Doc对象,它有被处理的字符串的token序列,它在C语言类型对象中的所有注释都被称为doc.c,是为TokenC结构的数组。
TokenC结构包含了我们关于每个token所需的全部信息。该信息以64位哈希码的形式保存,能够与我们刚刚看到的Unicode字符串重新关联。
如果想看看这些C类型结构中到底有什么,只需查看新建的spaCy的Cython API doc即可。
我们接下来看一个简单的自然语言处理的例子。
使用spaCy和Cython快速执行自然语言处理任务假设我们有一个文本文档数据集需要分析。
下面是我写的一段脚本,创建一个列表,包含10个由spaCy解析的文档,每个文档包含大约17万个词汇。我们也可以解析17万份文档,每份文档包含10个词汇(就像对话框数据集),但这种创建方式要慢的多,所以我们还是采取10份文档的形式。
我们想用这个数据集执行一些自然语言处理任务。例如,我们想计算词汇“run”在数据集中用作名词的次数(比如,被 spaCy 标记为「NN」词性标签)。
使用Python 循环实现上述分析的过程非常简单直接:
但是它运行的非常慢!在我的笔记本上,这点代码花了1.4秒才得到结果。如果我们有数百万份文档,就需要花费一天多的时间才能得到答案。
我们可以使用多线程处理,但在Python中这通常也不是个很好的解决方法,因为你必须处理GIL问题(GIL即global interpreter lock,全局解释器锁)。而且,Cython也能使用多线程!实际上,这可能是Cython中最棒的部分,因为Cython基本上能在后台直接调用OpenMP。这里不再详细讨论并行性的问题,可以点击这里查看更多信息。
接下来,我们用spaCy和Cython加快我们的Python代码的运行速度。
首先,我们必须考虑好数据结构。我们需要为数据集获取一个C类型数组,并有指针指向每个文档的TokenC数组。我们还需要将所用的测试字符串(“run”和“NN”)转换为64位哈希码。
如果我们处理过程中所需的全部数据都是C类型对象,然后我们可以以纯C语言的速度迭代整个数据集。
下面是可以用Cython和spaCy实现的示例:
代码有点长,因为我们必须在调用Cython函数[*]之前在main_nlp_fast之中声明和填充C结构。
但是代码的运行速度快了很多!在我的Jupyter notebook中,这段Cython代码运行速度大概只有20微秒,相比我们此前的完全由Python编写的循环,运行速度快了80倍。
使用Jupyter Notebook编写模块的速度同样令人瞩目,它可以和其它Python模块和函数自然地连接:20微秒内可处理多达170万个词汇,也就是说我们每秒处理的词汇数量高达8000万!
以上就是我们团队如何用Cython处理NLP任务的快速介绍,希望你能喜欢。
结语
关于Cython,还有很多需要学习的知识,如果你在你的代码中数次使用低级结构,相比每次填充C类型结构,更好的选择是围绕低级结构设计我们的Python代码,使用Cython扩展类型包装C类型结构。这也是大部分spaCy的构建方式,不仅运行速度快,内存消耗小,而且还能让我们很容易的连接外部Python库和函数。
领取专属 10元无门槛券
私享最新 技术干货