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.86Cython91.913,963
ClearNLPJava91.710,271
spaCy v0.84Cython90.913,963
CoreNLPJava89.68,602
MATEJava92.5550
TurboC++92.4349
YaraJava92.3340

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

详细速度比较

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

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

硬件:Intel i7-3770 (2012)

系统分词 (ms/文档)标注 (ms/文档)句法分析 (ms/文档)分词 (相对 spaCy)标注 (相对 spaCy)句法分析 (相对 spaCy)
spaCy0.2ms1ms19ms1x1x1x
CoreNLP2ms10ms49ms10x10x2.6x
ZPar1ms8ms850ms5x8x44.7x
NLTK4ms443msn/a20x443xn/a

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

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

关于作者

Matthew Honnibal
CTO,创始人

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值