原标题:老司机都开火箭了!Cython助力PythonNLP实现百倍加速雷锋网AI研习
原标题:老司机都开火箭了!Cython助力PythonNLP实现百倍加速
如何才能够使用Python设计出一个高效率的模块,
如何利用好spaCy的内置数据结构,从而设计出超高效的自然语言处理函数。
我的标题其实有点作弊,因为我实际上要谈论的是Python,同时也要介绍一些Cython的特性。不过你知道吗?Cython属于Python的超集,所以不要让它吓跑了!
小提示:你当前所编写的Python项目已经算是一个Cython项目了。
以下给出了一些可能需要采用这种加速策略的场景:
你正在使用Python给自然语言处理任务开发一个应用级模块
你正在使用Python分析一个自然语言处理任务的大型数据集
你正在为诸如PyTorch/TensoFlow这些深度学习框架预处理大型训练集,或者你的深度学习模型采用了处理逻辑复杂的批量加载器(Batchloader),它严重拖慢了你的训练速度
你需要知道的第一件事情是,你的大部分代码在纯Python环境下可能都运行良好,但是其中存在一些瓶颈函数(Bottlenecksfunctions),一旦你能给予它们更多的「关照」,你的程序将获得几个数量级的提速。
importcProfile
importpstats
importmy_slow_module
cProfile.run('my_slow_module.run()','restats')
p=pstats.Stats('restats')
p.sort_stats('cumulative').print_stats(30)
那么我们该如何来加速循环呢?
让我们通过一个简单的例子来解决这个问题。假设有一堆矩形,我们将它们存储成一个由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解释器在每次迭代中都需要做很多工作(查找类中的area方法、参数的打包和解包、调用PythonAPI等等)。
这时候该有请Cython出场帮助我们加速循环操作了。
Cython语言是Python的一个超集,它包含有两种类型的对象:
Python对象就是我们在常规Python中使用到的那些对象,诸如数值、字符串、列表和类实例等等
CythonC对象就是那些C和C++对象,诸如双精度、整型、浮点数、结构和向量,它们能够由Cython在超级高效的低级语言代码中进行编译
该循环只要采用Cython进行复现就能获得更高的执行速度,不过在Cython中我们只能够操作CythonC对象。
定义这种循环最直接的一种方法就是,定义一个包含有计算过程中我们所需要用到的所有对象的结构体。具体而言,在本例中就是矩形的长度和宽度。
然后我们可以将矩形对象列表存储到C的结构数组中,再将数组传递给check_rectangles函数。这个函数现在将接收一个C数组作为输入,此外我们还使用cdef关键字取代了def(注意:cdef也可以用于定义CythonC对象)将函数定义为一个Cython函数。
这里是Cython版本的模块程序:
fromcymem.cymemcimportPool
cdefstructRectangle:
floatw
floath
cdefintcheck_rectangles(Rectangle*rectangles,intn_rectangles,floatthreshold):
cdefintn_out=0
#Carrayscontainnosizeinformation=>weneedtogiveitexplicitly
forrectangleinrectangles[:n_rectangles]:
ifrectangle[i].w*rectangle[i].h>threshold:
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)
通过pipinstallcython命令安装Cython。
使用%load_extCython指令在Jupyternotebook中加载Cython扩展。
然后通过指令%%cython,我们就可以像Python一样在Jupyternotebook中使用Cython。
如果在执行Cython代码的时候遇到了编译错误,请检查Jupyter终端的完整输出信息。
大多数情况下可能都是因为在%%cython之后遗漏了-+标签(比如当你使用spaCyCython接口时)。如果编译器报出了关于Numpy的错误,那就是遗漏了importnumpy。
Cython代码的文件后缀是.pyx,这些文件将被Cython编译器编译成C或C++文件,再进一步地被C编译器编译成字节码文件。最终Python解释器将能够调用这些字节码文件。
你也可以使用pyximport将一个.pyx文件直接加载到Python程序中:
>>>importpyximport;pyximport.install()
>>>importmy_cython_module
在我们开始优化自然语言处理任务之前,还是先快速介绍一下def、cdef和cpdef这三个关键字。它们是你开始学会使用Cython之前需要掌握的最主要的知识。
你可以在Cython程序中使用三种类型的函数:
Python函数由def关键字定义,它的输入和输出都是Python对象。在函数内可以使用Python和C/C++对象,并且能够调用Cython和Python函数。
Cython函数由cdef关键字进行定义,它可以作为输入对象,在函数内部也可以操作或者输出Python和C/C++对象。这些函数不能从Python环境中访问(即Python解释器和其它可以导入Cython模块的纯Python模块),但是可以由其它Cython模块进行导入。
通过关键字cpdef定义的Cython函数与cdef定义的Cython函数很相似,但是cpdef定义的函数同时还提供了Python装饰器,所以它们能够在Python环境中被直接调用(函数采用Python对象作为输入与输出),此外也支持在Cython模块中被调用(函数采用C/C++或者Python对象作为输入)。
这一切看起来都很好,但是......我们到现在都还没开始涉及优化自然语言处理任务!没有字符串操作,没有unicode编码,也没有我们在自然语言处理中所使用的妙招。
通常来说:除非你明确地知道自己正在做什么,不然就该避免使用C类型字符串,而应该使用Python的字符串对象。
那么当我们在操作字符串时,要如何在Cython中设计一个更加高效的循环呢?
spaCy引起了我们的注意力。
spaCy处理该问题的做法就非常地明智。
它可以从spaCy的任何地方和任意对象进行访问,例如npl.vocab.strings、doc.vocab.strings或者span.doc.vocab.string。
但是spaCy能做的可不仅仅只有这些,它还允许我们访问文档和词汇表完全填充的C语言类型结构,我们可以在Cython循环中使用这些结构,而不必去构建自己的结构。
与spaCy文档有关的主要数据结构是Doc对象,该对象拥有经过处理的字符串的标记序列(“words”)以及C语言类型对象中的所有标注,称为doc.c,它是一个TokenC的结构数组。
TokenC结构包含了我们需要的关于每个标记的所有信息。这种信息被存储成64位哈希码,它可以与我们刚刚所见到的unicode字符串进行重新关联。
接下来看一个简单的自然语言处理的例子。
假设现在有一个文本文档的数据集需要分析。
importurllib.request
importspacy
text=response.read()
nlp=spacy.load('en')
doc_list=list(nlp(text[:800000].decode('utf8'))foriinrange(10))
我写了一个脚本用于创建一个包含有10份文档的列表,每份文档都大概含有17万个单词,采用spaCy进行分析。当然我们也可以对17万份文档(每份文档包含10个单词)进行分析,但是这样做会导致创建的过程非常慢,所以我们还是选择了10份文档。
我们想要在这个数据集上展开某些自然语言处理任务。例如,我们可以统计数据集中单词「run」作为名词出现的次数(例如,被spaCy标记为「NN」词性标签)。
采用Python循环来实现上述分析过程非常简单和直观:
defslow_loop(doc_list,word,tag):
fordocindoc_list:
fortokindoc:
iftok.lower_==wordandtok.tag_==tag:
defmain_nlp_slow(doc_list):
n_out=slow_loop(doc_list,'run','NN')
现在让我们尝试使用spaCy和Cython来加速Python代码。
首先需要考虑好数据结构,我们需要一个C类型的数组来存储数据,需要指针来指向每个文档的TokenC数组。我们还需要将测试字符(「run」和「NN」)转成64位哈希码。
当所有需要处理的数据都变成了C类型对象,我们就可以以纯C语言的速度对数据集进行迭代。
这里展示了这个例子被转换成Cython和spaCy的实现:
%%cython-+
importnumpy#Sometimewehaveafailtoimportnumpycompilationerrorifwedon'timportnumpy
fromspacy.tokens.doccimportDoc
fromspacy.typedefscimporthash_t
fromspacy.structscimportTokenC
cdefstructDocElement:
TokenC*c
intlength
cdefintfast_loop(DocElement*docs,intn_docs,hash_tword,hash_ttag):
fordocindocs[:n_docs]:
forcindoc.c[:doc.length]:
ifc.lex.lower==wordandc.tag==tag:
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)
这串代码虽然变长了,但是运行效率却更高!在我的Jupyternotebook上,这串Cython代码只运行了大概20毫秒,比之前的纯Python循环快了大概80倍。
使用Jupyternotebook单元编写模块的速度很可观,它可以与其它Python模块和函数自然地连接:在20毫秒内扫描大约170万个单词,这意味着我们每秒能够处理高达8千万个单词。
对使用Cython进行自然语言处理加速的介绍到此为止,希望大家能喜欢它。
如果你还想要获得更多类似的内容,请记得给我们点赞哟!
网罗天下
老大爷街头卖大块头甲鱼,男子好奇走近一看,却差点骂出了声
妻子家中洗漱,隔壁邻居突然闯进来!丈夫却阻止她报警
女子深夜出行被陌生男尾随搭讪拒绝后遭强行扑倒
狮航载189人客机坠毁男子因堵车晚点错过航班
一代“标王”的鼎盛时期:27岁赚了27个亿
女孩8个月被遗弃,爷爷奶奶做体力活将其养大,今却不能孝敬二老
学生没钱回校,公交司机下班后开车送回
李咏珍贵私人照曝光:24岁结婚照甜蜜青涩
播放数:145391
金庸去世享年94岁,三版“小龙女”李若彤刘亦菲陈妍希悼念