这里的循环我们使用Cython循环就能获得更快的运行速度,而我们只需获取Cython C对象。
设计这种循环的一个直接方法就是定义C结构,它会包含我们计算中所需的全部东西:在我们这里所举的例子中,就是矩形的长和宽。
然后我们将矩形列表保存在所定义的C结构的数组中,我们会将数组传入check_rectangle函数中。该函数现在必需接受C数组作为输入,这样就会被定义为Cython函数,使用cdef关键字而非def(cdef也用于定义Cython C对象)。
这里是我们的Python模块的高速Cython版的样子:
from cymem.cymem cimport Poolfrom random import randomcdef struct Rectangle: float w float hcdef int check_rectangles(Rectangle* rectangles, int n_rectangles, float threshold): cdef int n_out = 0 # C arrays contain no size information => we need to give it explicitly for rectangle in rectangles[:n_rectangles]: if rectangle.w * rectangle.h > threshold: n_out += 1 return n_outdef main(): cdef: int n_rectangles = 10000000 float threshold = 0.25 Pool mem = Pool() Rectangle* rectangles = <Rectangle*>mem.alloc(n_rectangles, sizeof(Rectangle)) for i in range(n_rectangles): rectangles.w = random() rectangles.h = random() n_out = check_rectangles(rectangles, n_rectangles, threshold)print(n_out)复制代码这里我们使用C指针的原生数组,但是你也可以选择其他选项,尤其是C++结构,比如向量、二元组、队列之类。在这里的脚本中,我还使用了cymem的很方面的Pool()内存管理对象,避免了必须手动释放所申请的C数组内存空间。当Python不再需要Pool时,它会自动释放我们用它申请时所占的内存。 我们试试代码! 我们有很多种方法可以测试、编辑和分发Cython代码!Cython甚至还能像Python一样直接在Jupyter Notebook中使用。
用pip install cython安装Cython。
当某个模块需要在某些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份文档的形式。
import urllib.requestimport spacywith 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))复制代码我们想用这个数据集执行一些自然语言处理任务。例如,我们想计算词汇“run”在数据集中用作名词的次数(比如,被 spaCy 标记为「NN」词性标签)。
使用Python 循环实现上述分析的过程非常简单直接:
def slow_loop(doc_list, word, tag): n_out = 0 for doc in doc_list: for tok in doc: if tok.lower_ == word and tok.tag_ == tag: n_out += 1 return n_outdef main_nlp_slow(doc_list): n_out = slow_loop(doc_list, 'run', 'NN')print(n_out)复制代码但是它运行的非常慢!在我的笔记本上,这点代码花了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 -+import numpy # Sometime we have a fail to import numpy compilation error if we don't import numpyfrom cymem.cymem cimport Poolfrom spacy.tokens.doc cimport Docfrom spacy.typedefs cimport hash_tfrom spacy.structs cimport TokenCcdef struct DocElement: TokenC* c int lengthcdef int fast_loop(DocElement* docs, int n_docs, hash_t word, hash_t tag): cdef int n_out = 0 for doc in docs[:n_docs]: for c in doc.c[:doc.length]: if c.lex.lower == word and c.tag == tag: n_out += 1 return n_outdef main_nlp_fast(doc_list): cdef int i, n_out, n_docs = len(doc_list) cdef Pool mem = Pool() cdef DocElement* docs = <DocElement*>mem.alloc(n_docs, sizeof(DocElement)) cdef Doc doc for i, doc in enumerate(doc_list): # Populate our database structure docs.c = doc.c docs.length = (<Doc>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函数