有什么工具能高速运行自然语言的处理项目

更新时间:02-08 教程 由 悦缘 分享

推荐使用spaCy和Cython,运行自然语言处理项目比Python快近100倍!Cython是一个工具包,可以使你在Python中编译C语言,这就是为什么numpy和pandas很快的原因,Cython就是Python的超集。在本文中,作者将为我们介绍他的GitHub项目NeuralCorefv3.0,详解如何利用spaCy和Cython以约100倍于Python的速度实现NLP项目。

相关JupyterNotebook地址:https://github.com/huggingface/100-times-faster-nlp

去年我们发布Python包coreferenceresolutionpackage后,我们收到了来自社区的精彩反馈,并且人们开始在很多应用中使用它,其中一些与我们原来的对话用例迥异。

我们发现,尽管对话信息的处理速度非常好,但对于长的新闻文章来说,处理速度可能会非常慢。

我决定详细研究这一问题,最终成果即NeuralCorefv3.0,它在相同准确率的情况下比老版本快100倍左右(每秒几千字),同时兼顾Python库的易用性和兼容性。

NeuralCorefv3.0:https://github.com/huggingface/neuralcoref/

我想在这篇文章中分享一些关于这个项目的经验,特别是:

如何用Python设计一个高速模块;

如何利用spaCy的内部数据结构来有效地设计超高速NLP函数。

所以我在这里有点作弊,因为我们会谈论Python,但也谈论一些Cython的神奇作用。但是,你知道吗?Cython是Python的超集,所以不要让它吓跑你!

你现在的Python程序已经是Cython程序。

有几种情况下你可能需要加速,例如:

你正在使用Python开发一个NLP的生产模块;

你正在使用Python计算分析大型NLP数据集;

你正在为深度学习框架,如PyTorch/TensorFlow,预处理大型训练集,或者你的深度学习批处理加载器中的处理逻辑过于繁重,这会降低训练速度。

再强调一遍:我同步发布了一个JupyterNotebook,其中包含我在本文中讨论的例子。试试看!

JupyterNotebook:https://github.com/huggingface/100-times-faster-nlp

加速第一步:剖析

首先要知道的是,你的大多数代码在纯Python环境中可能运行的不错,但是如果你多用点心,其中一些瓶颈函数可能让你的代码快上几个数量级。

因此,你首先应该分析你的Python代码并找出瓶颈部分的位置。使用如下的cProfile是一种选择:

importcProfile
importpstats
importmyslowmodule
cProfile.run('myslowmodule.run()','restats')
p=pstats.Stats('restats')
p.sortstats('cumulative').printstats(30)

如果你使用神经网络,你可能会发现瓶颈部分是几个循环,并且涉及Numpy数组操作。

那么,我们如何加速这些循环代码?

在Python中使用一些Cython加速循环

让我们用一个简单的例子来分析这个问题。假设我们有一大堆矩形,并将它们存储进一个Python对象列表,例如Rectangle类的实例。我们的模块的主要工作是迭代这个列表,以便计算有多少矩形的面积大于特定的阈值。

我们的Python模块非常简单,如下所示:

fromrandomimportrandom
classRectangle:
def__init__(self,w,h):
self.w=w
self.h=h
defarea(self):
returnself.w*self.h
defcheck_rectangles(rectangles,threshold):
n_out=0
forrectangleinrectangles:
ifrectangle.area()>threshold:
n_out+=1
returnn_out
defmain():
n_rectangles=10000000
rectangles=list(Rectangle(random(),random())foriinrange(n_rectangles))
n_out=check_rectangles(rectangles,threshold=0.25)
print(n_out)

check_rectangles函数是瓶颈部分!它对大量的Python对象进行循环,这可能会很慢,因为Python解释器在每次迭代时都会做大量工作(寻找类中的求面积方法、打包和解包参数、调用PythonAPI...)。

Cython将帮助我们加速循环。

Cython语言是Python的超集,它包含两种对象:

Python对象是我们在常规Python中操作的对象,如数字、字符串、列表、类实例...

CythonC对象是C或C++对象,比如double、int、float、struct、vectors。这些可以由Cython在超快速的底层代码中编译。

快速循环只是Cython程序(只能访问CythonC对象)中的一个循环。

设计这样一个循环的直接方法是定义C结构,它将包含我们在计算过程中需要的所有要素:在我们的例子中,就是矩形的长度和宽度。

然后,我们可以将矩形列表存储在这种结构的C数组中,并将这个数组传递给我们的check_rectangle函数。此函数现在接受一个C数组作为输入,因此通过cdef关键字而不是def将其定义为Cython函数(请注意,cdef也用于定义CythonC对象)。

下面是我们的Python模块的快速Cython版:

fromcymem.cymemcimportPool
fromrandomimportrandom
cdefstructRectangle:
floatw
floath
cdefintcheck_rectangles(Rectangle*rectangles,intn_rectangles,floatthreshold):
cdefintn_out=0
#Carrayscontainnosizeinformation=>weneedtogiveitexplicitly
forrectangleinrectangles[:n_rectangles]:
ifrectangles[i].w*rectangles[i].h>threshold:
n_out+=1
returnn_out
defmain():
cdef:
intn_rectangles=10000000
floatthreshold=0.25
Poolmem=Pool()
Rectangle*rectangles=mem.alloc(n_rectangles,sizeof(Rectangle))
foriinrange(n_rectangles):
rectangles[i].w=random()
rectangles[i].h=random()
n_out=check_rectangles(rectangles,n_rectangles,threshold)
print(n_out)

我们在这里使用了原生C指针数组,但你也可以选择其他选项,特别是C++结构,如向量、对、队列等。在这个片段中,我还使用了cymem的便利的Pool()内存管理对象,以避免必须手动释放分配的C数组。当Pool由Python当做垃圾回收时,它会自动释放我们使用它分配的内存。

spaCyAPI的CythonConventions是Cython在NLP中的实际运用的一个很好的参考。

spaCy:https://spacy.io

CythonConventions:https://spacy.io/api/cython#conventions

让我们试试这个代码吧!

有很多方法可以测试、编译和发布Cython代码!Cython甚至可以直接用在Python这样的JupyterNotebook中。

JupyterNotebook:http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiling-notebook

首先使用pipinstallcython安装Cython

在Jupyter的第一次测试

使用%load_extCython将Cython插件加载到Jupyternotebook中。

现在,你可以使用黑魔术命令%%cython编写像Python代码一样的Cython代码。

如果在执行Cython单元时遇到编译错误,请务必检查Jupyter终端输出以查看完整的信息。

大多数情况下,在%%cython编译为C++(例如,如果你使用spaCyCythonAPI)或者importnumpy(如果编译器不支持NumPy)之后,你会丢失-+标记。

正如我在开始时提到的,查看这篇文章的同步JupyterNotebook,该Notebook包含本文讨论的所有示例。

编写、使用和发布Cython代码

Cython代码写在.pyx文件中。这些文件由Cython编译器编译为C或C++文件,然后通过系统的C编译器编译为字节码文件。Python解释器可以使用字节码文件。

你可以使用pyximport直接在Python中加载.pyx文件:

>>>importpyximport;pyximport.install()
>>>importmy_cython_module

你还可以将你的Cython代码构建为Python包,并将其作为常规Python包导入/发布,详见下方地址。这可能需要一些时间才能开始工作,尤其在全平台上。如果你需要一个有效示例,spaCy』sinstallscript是一个相当全面的例子。

导入教程:http://cython.readthedocs.io/en/latest/src/tutorial/cython_tutorial.html#

BeforewemovetosomeNLP,let』squicklytalkaboutthedef,cdefandcpdefkeywords,becausetheyarethemainthingsyouneedtograbtostartusingCython.

在我们转向NLP之前,让我们先快速讨论一下def、cdef和cpdef关键字,因为它们是你开始使用Cython需要掌握的主要内容。

你可以在Cython程序中使用三种类型的函数:

Python函数,用常用的关键字def定义。它们可作为输入和输出的Python对象。也可以在内部同时使用Python和C/C++对象,并可以调用Cython和Python函数。

用cdef关键字定义的Cython函数。它们可以作为输入,在内部使用并输出Python和C/C++对象。这些函数不能从Python空间访问(即Python解释器和其他可导入Cython模块的纯Python模块),但可以由其他Cython模块导入。

用cpdef关键字定义的Cython函数就像cdef定义的Cython函数一样,但它们也提供了一个Python封装器,因此可以从Python空间(以Python对象作为输入和输出)以及其他Cython模块(以C/C++或Python对象作为输入)中调用它们。

cdef关键字有另一种用途,即在代码中定义CythonC/C++对象。除非用这个关键字定义对象,否则它们将被视为Python对象(因此访问速度很慢)。

使用Cython与spaCy来加速NLP

这些东西又好又快,但是......我们现在还没有融入NLP!没有字符串操作、没有unicode编码,也没有我们在自然语言处理中幸运拥有的微妙联系。

官方的Cython文档甚至建议不要使用C字符串:

一般来说:除非你知道自己在做什么,否则应尽可能避免使用C字符串,而应使用Python字符串对象。

那么我们如何在使用字符串时在Cython中设计快速循环?

spaCy会帮我们的。

spaCy解决这个问题的方式非常聪明。

将所有字符串转换为64位哈希码

spaCy中的所有unicode字符串(token的文本、其小写文本、引理形式、POS键标签、解析树依赖关系标签、命名实体标签...)都存储在叫StringStore的单数据结构中,它们在里面由64位散列索引,即Cuint64_t。

StringStore对象实现了Pythonunicode字符串和64位哈希码之间的查找表。

它可以通过spaCy任意处及任意对象访问(请参阅上图),例如nlp.vocab.strings、doc.vocab.strings或span.doc.vocab.string。

当某个模块需要对某些token执行快速处理时,仅使用C级别的64位哈希码而不是字符串。调用StringStore查找表将返回与哈希码相关联的Pythonunicode字符串。

但是,spaCy做的远不止这些,它使我们能够访问文档和词汇表的完全覆盖的C结构,我们可以在Cython循环中使用这些结构,而不必自定义结构。

spaCy的内部数据结构

与spaCyDoc对象关联的主要数据结构是Doc对象,该对象拥有已处理字符串的token序列(「单词」)以及C对象中的所有称为doc.c的标注,它是一个TokenC结构数组。

TokenC结构包含我们需要的关于每个token的所有信息。这些信息以64位哈希码的形式存储,可以重新关联到unicode字符串,就像我们刚刚看到的那样。

要深入了解这些C结构中的内容,只需查看刚创建的SpaCy的CythonAPIdoc。

我们来看看一个简单的NLP处理示例。

使用spaCy和Cython进行快速NLP处理

假设我们有一个需要分析的文本数据集

importurllib.request
importspacy
withurllib.request.urlopen('https://raw.githubusercontent.com/pytorch/examples/master/word_language_model/data/wikitext-2/valid.txt')asresponse:
text=response.read()
nlp=spacy.load('en')
doc_list=list(nlp(text[:800000].decode('utf8'))foriinrange(10))

我在左边写了一个脚本,它生成用于spaCy解析的10份文档的列表,每个文档大约170k字。我们也可以生成每个文档10个单词的170k份文档(比如对话数据集),但创建速度较慢,因此我们坚持使用10份文档。

我们想要在这个数据集上执行一些NLP任务。例如,我们想要统计数据集中单词「run」作为名词的次数(即用spaCy标记为「NN」词性)。

一个简单明了的Python循环就可以做到:

defslow_loop(doc_list,word,tag):
n_out=0
fordocindoc_list:
fortokindoc:
iftok.lower_==wordandtok.tag_==tag:
n_out+=1
returnn_out
defmain_nlp_slow(doc_list):
n_out=slow_loop(doc_list,'run','NN')
print(n_out)

但它也很慢!在我的笔记本电脑上,这段代码需要大约1.4秒才能得到结果。如果我们有一百万份文件,则需要一天以上才能给出结果。

我们可以使用多线程,但在Python中通常不是很好的解决方案,因为你必须处理GIL。另外,请注意,Cython也可以使用多线程!而且这实际上可能是Cython最棒的部分,因为GIL被释放,我们可以全速运行。Cython基本上直接调用OpenMP。

现在我们尝试使用spaCy和部分Cython加速我们的Python代码。

首先,我们必须考虑数据结构。我们将需要一个C数组用于数据集,指针指向每个文档的TokenC数组。我们还需要将我们使用的测试字符串(「run」和「NN」)转换为64位哈希码。

当我们所需的数据都在C对象中时,我们可以在数据集上以C的速度进行迭代。

下面是如何使用spaCy在Cython中编写的示例:

%%cython-+
importnumpy#Sometimewehaveafailtoimportnumpycompilationerrorifwedon'timportnumpy
fromcymem.cymemcimportPool
fromspacy.tokens.doccimportDoc
fromspacy.typedefscimporthash_t
fromspacy.structscimportTokenC
cdefstructDocElement:
TokenC*c
intlength
cdefintfast_loop(DocElement*docs,intn_docs,hash_tword,hash_ttag):
cdefintn_out=0
fordocindocs[:n_docs]:
forcindoc.c[:doc.length]:
ifc.lex.lower==wordandc.tag==tag:
n_out+=1
returnn_out
defmain_nlp_fast(doc_list):
cdefinti,n_out,n_docs=len(doc_list)
cdefPoolmem=Pool()
cdefDocElement*docs=mem.alloc(n_docs,sizeof(DocElement))
cdefDocdoc
fori,docinenumerate(doc_list):#Populateourdatabasestructure
docs[i].c=doc.c
docs[i].length=(doc).length
word_hash=doc.vocab.strings.add('run')
tag_hash=doc.vocab.strings.add('NN')
n_out=fast_loop(docs,n_docs,word_hash,tag_hash)
print(n_out)

代码有点长,因为我们必须在调用Cython函数之前在main_nlp_fast中声明并填充C结构。(如果你在代码中多次使用低级结构,使用C结构包装的Cython扩展类型来设计我们的Python代码是比每次填充C结构更优雅的选择。这就是大多数spaCy的结构,它是一种结合了快速,低内存以及与外部Python库和函数接口的简便性的非常优雅的方法。)

但它也快很多!在我的JupyterNotebook中,这个Cython代码的运行时间大约为20毫秒,比我们的纯Python循环快大约80倍。

JupyterNotebookcell中编写的模块的绝对速度同样令人印象深刻,并且可以为其他Python模块和函数提供本地接口:在30ms内扫描约1,700万字意味着我们每秒处理高达8000万字。

我们这就结束了使用Cython进行NLP的快速介绍。我希望你喜欢它。

Cython还有很多其他的东西可讲,但这会让我们远离主题。从现在开始,最好的地方可能就是Cythontutorials的概述和适用于NLP的spaCy’sCythonpage。

声明:关于《有什么工具能高速运行自然语言的处理项目》以上内容仅供参考,若您的权利被侵害,请联系13825271@qq.com
本文网址:http://www.25820.com/tutorial/14_2105914.html