spaCy:Python与Cython中的高效文本处理库

Introducing spaCy

spaCy 是一个用于 Python 和 Cython 文本处理的新库。创建它的原因是,笔者认为小型公司在自然语言处理(NLP)方面做得非常糟糕。或者更准确地说:小型公司正在使用糟糕的 NLP 技术。

更新(2016年10月3日) 这篇文章展示了 spaCy 最初的发布公告,其中包含一些使用示例和基准测试。这些基准测试现在已经相当过时,但令人欣慰的是,其使用方式变化相对较小。在发布此公告之前,笔者在 spaCy 的初始设计上花费了很长时间。其回报是 API 一直保持相当稳定。

要做出优秀的 NLP,你需要懂一点语言学,懂很多机器学习,并且几乎要了解最新的研究进展。符合这种描述的人很少会加入小公司。他们中的大多数刚刚研究生毕业,经济拮据。如果他们不想留在学术界,就会加入某中心、某机构等科技巨头。

最终的结果是,在过去十年中,除了科技巨头之外,商业 NLP 领域变化不大。而在学术界,它已经完全改变了。质量有了惊人的提升,速度提高了数个数量级。但学术界的代码总是 GPL 许可、缺乏文档、无法使用,或者三者兼具。你可以自己实现这些想法,但论文很难读懂,而且训练数据极其昂贵。那么你还有什么选择呢?一个常见的答案是 NLTK,它主要是作为教育资源编写的。除了分词器,其他部分都不适合生产环境使用。

笔者曾认为,NLP 社区只需要更多地与软件工程师沟通其研究成果。因此,笔者写了两篇博客文章,解释了如何编写词性标注器和句法分析器。两篇文章都反响很好,并且笔者的研究软件也引起了一些兴趣——尽管它完全没有文档,除了笔者本人之外,对大多数人来说基本无法使用。

所以,六个月前,笔者辞去了博士后工作,从那以后一直在日以继夜地开发 spaCy。现在,很高兴宣布其 alpha 版本发布。

如果你是一家从事 NLP 的小公司,笔者认为 spaCy 看起来将是一个小小的奇迹。它是迄今为止发布的最快的 NLP 软件。完整的处理流水线每份文档在 20 毫秒内完成,包括准确的标注和句法分析。所有字符串都被映射为整数 ID,词元链接到嵌入式词表示,并且一系列有用的特征被预先计算和缓存。

计算机不理解文本。这很不幸,因为文本几乎是网络的主要构成部分。

如果以上内容对你来说没有任何意义,那么它的要点是:计算机不理解文本。这很不幸,因为文本几乎是网络的完全构成。我们想要根据人们喜欢的其他文本来推荐文本。我们想要缩短文本以在移动屏幕上显示。我们想要聚合、链接、过滤、分类、生成和纠正文本。spaCy 提供了一个实用函数库,帮助程序员构建此类产品。它是商业开源软件:你可以根据 AGPL 使用它,也可以在优惠的条件下购买商业许可证。

功能示例

假设你正在开发一个校对工具,或者可能是一个为写作者设计的 IDE。你被斯蒂芬·金的建议说服了,即副词不是你的朋友,因此你想突出显示所有副词。我们将使用他认为是特别恶劣的例子之一:

>>> import spacy.en
>>> from spacy.parts_of_speech import ADV
>>> # 加载流水线,并用一些文本调用它。
>>> nlp = spacy.en.English()
>>> tokens = nlp(u"‘Give it back,’ he pleaded abjectly, ‘it’s mine.’", tag=True, parse=False)
>>> print u''.join(tok.string.upper() if tok.pos == ADV else tok.string for tok in tokens)
u‘Give it BACK,’ he pleaded ABJECTLY, ‘it’s mine.’

很简单——但问题是“back”也被高亮显示了。虽然“back”无疑是一个副词,但我们可能不想高亮它。如果我们试图标记可疑的文体选择,就需要细化我们的逻辑。事实证明,只有特定类型的副词是我们感兴趣的。

根据我们想要标记的具体词语,有很多方法可以实现。排除像“back”和“not”这样的副词的最简单方法是依据词频:这些词比风格指南所担心的典型方式副词要常见得多。

Lexeme.probToken.prob 属性给出了该词的对数概率估计:

>>> nlp.vocab[u'back'].prob
-7.403977394104004
>>> nlp.vocab[u'not'].prob
-5.407193660736084
>>> nlp.vocab[u'quietly'].prob
-11.07155704498291

(概率估计基于一个 30 亿词语料库的计数,使用 Simple Good-Turing 方法进行平滑处理。)

因此,我们可以轻松地从我们的副词标记器中排除英语中最常见的 N 个词。让我们暂时尝试 N=1000:

>>> import spacy.en
>>> from spacy.parts_of_speech import ADV
>>> nlp = spacy.en.English()
>>> # 找出第 N 个最常见词的对数概率
>>> probs = [lex.prob for lex in nlp.vocab]
>>> probs.sort()
>>> is_adverb = lambda tok: tok.pos == ADV and tok.prob < probs[-1000]
>>> tokens = nlp(u"‘Give it back,’ he pleaded abjectly, ‘it’s mine.’")
>>> print u''.join(tok.string.upper() if is_adverb(tok) else tok.string for tok in tokens)
‘Give it back,’ he pleaded ABJECTLY, ‘it’s mine.’

根据我们想要标记的具体词语,还有很多其他方法可以细化逻辑。假设我们只想标记修饰类似于“pleaded”的词语的副词。这很容易做到,因为 spaCy 为每个词加载了向量空间表示(默认是由 Levy 和 Goldberg (2014) 生成的向量)。自然地,向量以 numpy 数组的形式提供:

>>> pleaded = tokens[7]
>>> pleaded.vector.shape
(300,)
>>> pleaded.vector[:5]
array([ 0.04229792,  0.07459262,  0.00820188, -0.02181299,  0.07519238], dtype=float32)

我们想根据词汇表中词语与“pleaded”的相似度对它们进行排序。有很多方法可以测量两个向量的相似度。我们将使用余弦度量:

>>> from numpy import dot
>>> from numpy.linalg import norm
>>> cosine = lambda v1, v2: dot(v1, v2) / (norm(v1) * norm(v2))
>>> words = [w for w in nlp.vocab if w.has_vector]
>>> words.sort(key=lambda w: cosine(w.vector, pleaded.vector))
>>> words.reverse()
>>> print('1-20', ', '.join(w.orth_ for w in words[0:20]))
1-20 pleaded, pled, plead, confessed, interceded, pleads, testified, conspired, motioned, demurred, countersued, remonstrated, begged, apologised, consented, acquiesced, petitioned, quarreled, appealed, pleading
>>> print('50-60', ', '.join(w.orth_ for w in words[50:60]))
50-60 counselled, bragged, backtracked, caucused, refiled, dueled, mused, dissented, yearned, confesses
>>> print('100-110', ', '.join(w.orth_ for w in words[100:110]))
100-110 cabled, ducked, sentenced, perjured, absconded, bargained, overstayed, clerked, confided, sympathizes
>>> print('1000-1010', ', '.join(w.orth_ for w in words[1000:1010]))
1000-1010 scorned, baled, righted, requested, swindled, posited, firebombed, slimed, deferred, sagged
>>> print('50000-50010', ', '.join(w.orth_ for w in words[50000:50010]))
50000-50010, fb, ford, systems, puck, anglers, ik, tabloid, dirty, rims, artists

如你所见,这些向量提供给我们的相似性模型非常出色——仅凭一个原型,我们在 1000 个词时仍然得到了有意义的结果!唯一的问题是,这个列表实际上包含了两类词:一类与“pleaded”的法律含义相关,另一类与其更一般的含义相关。理清这些类别是当前一个活跃的研究领域。

一个简单的解决方法是取几个词向量的平均值,并将其作为我们的目标:

>>> say_verbs = ['pleaded', 'confessed', 'remonstrated', 'begged', 'bragged', 'confided', 'requested']
>>> say_vector = sum(nlp.vocab[verb].vector for verb in say_verbs) / len(say_verbs)
>>> words.sort(key=lambda w: cosine(w.vector * say_vector))
>>> words.reverse()
>>> print('1-20', ', '.join(w.orth_ for w in words[0:20]))
1-20 bragged, remonstrated, enquired, demurred, sighed, mused, intimated, retorted, entreated, motioned, ranted, confided, countersued, gestured, implored, interceded, muttered, marvelled, bickered, despaired
>>> print('50-60', ', '.join(w.orth_ for w in words[50:60]))
50-60 flaunted, quarrelled, ingratiated, vouched, agonized, apologised, lunched, joked, chafed, schemed
>>> print('1000-1010', ', '.join(w.orth_ for w in words[1000:1010]))
1000-1010 hoarded, waded, ensnared, clamoring, abided, deploring, shriveled, endeared, rethought, berate

这些看起来确实像是金可能会责备作家在其后附加副词的词语。回想一下,我们最初的副词高亮函数是这样的:

>>> import spacy.en
>>> from spacy.parts_of_speech import ADV
>>> # 加载流水线,并用一些文本调用它。
>>> nlp = spacy.en.English()
>>> tokens = nlp("‘Give it back,’ he pleaded abjectly, ‘it’s mine.’",
                          tag=True, parse=False)
>>> print(''.join(tok.string.upper() if tok.pos == ADV else tok.string for tok in tokens))
‘Give it BACK,’ he pleaded ABJECTLY, ‘it’s mine.’

我们想细化逻辑,以便只高亮修饰像“pleaded”这样富有表现力的交流动词的副词。我们现在已经构建了一个代表这类词的向量,所以现在我们可以根据微妙的逻辑来高亮副词,根据我们的初始假设,聚焦于那些看起来在文体上最有问题的副词:

>>> import numpy
>>> from numpy import dot
>>> from numpy.linalg import norm
>>> import spacy.en
>>> from spacy.parts_of_speech import ADV, VERB
>>> cosine = lambda v1, v2: dot(v1, v2) / (norm(v1) * norm(v2))
>>> def is_bad_adverb(token, target_verb, tol):
...   if token.pos != ADV:
...     return False
...   elif token.head.pos != VERB:
...     return False
...   elif cosine(token.head.vector, target_verb) < tol:
...     return False
...   else:
...     return True

这个例子有些刻意——而且,说实话,笔者从未真正相信副词是一种严重的文体罪恶这种观点。但希望它能传达出这样的信息:最先进的 NLP 技术非常强大。spaCy 让你能够轻松高效地使用它们,从而构建各种以前不可能实现的有用产品和功能。

独立评估

某机构和某机构的独立评估,将在 ACL 2015 上发表。数值越高越好。准确率是未标记弧正确的百分比,速度是每秒处理的词元数。

系统 语言 准确率 速度
spaCy v0.86 Cython 91.9 13,963
ClearNLP Java 91.7 10,271
spaCy v0.84 Cython 90.9 13,963
CoreNLP Java 89.6 8,602
MATE Java 92.5 550
Turbo C++ 92.4 349
Yara Java 92.3 340

某机构和某机构的作者对可用的最佳解析器进行了详细比较。除 spaCy v0.86 外,以上所有数字均取自他们慷慨提供给笔者的预印本。笔者特别感谢作者们对结果的讨论,这导致了 v0.84 到 v0.86 之间的准确率提升。来自 ClearNLP 开发者的一个建议尤其有用。

详细速度比较

每文档处理时间。越低越好。

设置:从 SQLite3 数据库流式传输 100,000 份纯文本文档,并使用一个 NLP 库处理到三个详细级别之一——分词、词性标注或句法分析。这些任务是累加的:要对文本进行句法分析,必须先分词和标注。预处理时间未从时间中减去——报告的是流水线完成所需的时间。报告的是每份文档的平均时间,单位为毫秒。

硬件:Intel i7-3770 (2012)

系统 分词 (ms/文档) 标注 (ms/文档) 句法分析 (ms/文档) 分词 (相对 spaCy) 标注 (相对 spaCy) 句法分析 (相对 spaCy)
spaCy 0.2ms 1ms 19ms 1x 1x 1x
CoreNLP 2ms 10ms 49ms 10x 10x 2.6x
ZPar 1ms 8ms 850ms 5x 8x 44.7x
NLTK 4ms 443ms n/a 20x 443x n/a

效率是 NLP 应用的一个主要关注点。经常听到人们说他们负担不起更详细的处理,因为他们的数据集太大。这是一个糟糕的处境。如果你不能应用详细处理,通常不得不拼凑各种启发式方法。这通常需要多次迭代,并且你想出的方法通常会很脆弱且难以推理。

spaCy 的解析器比大多数标注器都快,其分词器对于任何工作负载来说都足够快。而且分词器不仅仅给你一个字符串列表。spaCy 的词元是一个指向 Lexeme 结构体的指针,从中你可以访问一系列预先计算的特征,包括嵌入式词表示。

关于作者

Matthew Honnibal
CTO,创始人

Matthew 是 AI 技术领域的领先专家。他于 2009 年完成博士学位,并在接下来的 5 年里发表了关于最先进 NLP 系统的研究。他于 2014 年离开学术界,编写 spaCy 并创立了 Explosion。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

公众号二维码

公众号二维码

posted @ 2026-01-03 05:59  CodeShare  阅读(11)  评论(0)    收藏  举报