自去年发布 Python 的指代消解包(coreference resolution package)之后,很多用户开始用它来构建许多应用程序,而这些应用与我们最初的对话应用完全不同。我们发现,尽管在处理对话时这个包的速度完全没问题,但在处理较大的问题时却非常慢。
笔者决定调查一下这个问题,于是就产生了 NeuralCoref v3.0(https://github.com/huggingface
/neuralcoref/)这一项目,它比上一个版本快 100 倍(每秒能分析几千个单词),同时保持准确度、易用性,并且依然在 Python 库的生态系统中。
在本文中笔者想分享一些在这个项目中学习到的经验,具体来说包括:
1.怎样用 Python 设计高速的模块;
2.怎样利用 spaCy 的内部数据结构来有效地设计高速的 NLP 函数。
虽然我们是在讨论 Python,但还要用一些 Cython的魔法。但别忘了,Cython 是 Python 的超集(http://cython.org/),所以别被它吓住了!
为 pyTorch 或 TensorFlow 等深度学习框架预处理一个大型数据集,或者在深度学习的批次加载器中有个很复杂的处理逻辑使得训练变慢。
加速的第一步:性能分析
首先要明确一点,绝大部分纯 Python 的代码是没有问题的,但有几个瓶颈函数如果能够解决,就能给速度带来数量级上的提升。
因此首先应该用分析工具分析 Python 代码,找出哪里慢。一个办法是使用cProfile(https://docs.python.org/3/library/profile.html):
import cProfile
import pstats
import my_slow_module
cProfile.run(‘my_slow_module.run()’, ‘restats’)
p = pstats.Stats(‘restats’)
p.sort_stats(‘cumulative’).print_stats(30)
也许你会发现有几个循环比较慢,如果用神经网络的话,可能有几个 NumPy 数组操作会很慢(但这里我不会讨论如何加速 NumPy,那么,应该如何加快循环的速度?
利用 Cython 实现更快的循环
用个简单的例子来说明。假设我们一个巨大的集合里包含许多长方形,保存为 Python 对象(即 Rectangle 类的实例)的列表。模块的主要功能就是遍历该列表,数出有多少个长方形超过了某个阈值。
我们的 Python 模块非常简单,如下所示:
from random import random
class Rectangle:
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
def check_rectangles(rectangles, threshold):
n_out = 0
for rectangle in rectangles:
if rectangle.area() > threshold:
n_out += 1
return n_out
def main():
n_rectangles = 10000000
rectangles = list(Rectangle(random(), random()) for i in range(n_rectangles))
n_out = check_rectangles(rectangles, threshold=0.25)
print(n_out)
這里 check_rectangles 函数就是瓶颈!它要遍历大量 Python 对象,而由于每次循环中 Python 解释器都要在背后进行许多工作(如在类中查找 area 方法、打包解包参数、调用 Python API 等),这段代码就会非常慢。
Cython 能帮我们加快循环
Cython 语言是 Python 的一个超集,它包含两类对象:
1.Python 对象是在正常的 Python 中操作的对象,如数字、字符串、列表、类实例等。
2.Cython C 对象是 C 或 C++ 对象,如 dobule、int、float、struct、vectors,这些可以被 Cython 编译成超级快的底层代码。
高速循环就是 Cython 程序中只访问 Cython C 对象的循环。
设计这种高速循环最直接的办法就是,定义一个 C 结构,它包含计算过程需要的一切。在这个例子中,该结构需要包含长方形的长和宽。
然后我们就可以将长方形列表保存在一个 C 数组中,传递给 check_rectangles 函数。现在该函数就需要接收一个 C 数组作为输入,因此它应该用 cdef 关键字(而不是 def)定义为 Cython 函数。(注意 cdef 也被用来定义 Cython C 对象。)
试一下这段代码
有许多方法可以测试、编译并发布 Cython 代码!Cython 甚至可以像 Python 一样直接用在 Jupyter Notebook 中,首先用 pip install cython 安装 Cython:
编写、使用并发布 Cython 代码
Cython 代码保存在 .pyx 文件中。这些文件会被 Cython 編译器编译成 C 或 C++ 文件,然后再被系统的 C 编译器编译成字节码。这些字节码可以直接被 Python 解释器使用。
可以在 Python 中使用 pyximport 直接加载 .pyx 文件:
>>> import pyximport; pyximport.install()
>>> import my_cython_module
也可以将Cython代码构建成Python包,并作为正常的Python包导入或发布。这项工作比较花费时间,主要是要处理所有平台上的兼容性问题。在进入 NLP 之前,我们先快速讨论下 def、cdef 和 cpdef 关键字,这些是学习 Cython 时最关键的概念。
通过 spaCy 使用 Cython 加速 NLP
前面说的这些都很好……但这跟 NLP 还没关系呢!没有字符串操作,没有 Unicode 编码,自然语言处理中的难点都没有支持啊!而且 Cython 的官方文档甚至还反对使用 C 语言级别的字符串。一般来说,除非你知道你在做什么,否则尽量不要使用 C 字符串,而应该使用 Python 字符串对象,这就轮到 spaCy 出场了,spaCy 解决这个问题的办法特别聪明。
将所有字符串转换成 64 比特 hash
在 spaCy 中,所有 Unicode 字符串(token 的文本,token 的小写形式,lemma 形式,词性标注,依存关系树的标签,命名实体标签……)都保存在名为 StringStore 的单一数据结构中,字符串的索引是 64 比特 hash,也就是 C 语言层次上的 unit64_t。
StringStore 对象实现了在 Python unicode 字符串和 64 比特 hash 之间的查找操作。StringStore 可以从 spaCy 中的任何地方、任何对象中访问,例如可以通过 nlp.vocab.string、doc.vocab.strings 或 span.doc.vocab.string 等。当模块需要在某些 token 上进行快速处理时,它只会使用 C 语言层次上的 64 比特 hash,而不是使用原始字符串。调用 StringStore 的查找表就会返回与该 hash 关联的 Python unicode 字符串。但是 spaCy 还做了更多的事情,我们可以通过它访问完整的 C 语言层次上的文档和词汇表结构,因此可以使用 Cython 循环,不需要再自己构建数据结构。