首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >spaCy:Python与Cython中的高效文本处理库

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

原创
作者头像
用户11764306
发布2026-02-01 13:24:52
发布2026-02-01 13:24:52
850
举报

Introducing spaCy

spaCy 是一个用于 Python 和 Cython 文本处理的新库。之所以开发它,是因为我认为小公司在自然语言处理方面表现得很糟糕。或者更准确地说:小公司正在使用糟糕的 NLP 技术。

更新(2016年10月3日)

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

要做好 NLP,你需要懂一点语言学,懂很多机器学习,并且几乎要了解最新的所有研究。符合这种描述的人很少加入小公司。他们中的大多数都刚毕业,囊中羞涩。如果他们不想留在学术界,就会加入某中心、某机构等。

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

我曾经认为 NLP 社区只需要做更多工作,将其发现传达给软件工程师。所以我写了两篇博客文章,解释如何编写一个词性标注器和解析器。两篇文章都反响不错,并且我的研究软件也引起了一些兴趣——尽管它完全没有文档,除了我之外对大多数人来说几乎无法使用。

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

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

计算机不理解文本。这很不幸,因为网络主要由文本构成。

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

功能示例

假设你正在开发一个校对工具,或者可能是一个面向作家的 IDE。你被斯蒂芬·金的建议说服了,他认为副词不是你的朋友,所以你想高亮显示所有副词。我们将使用一个他认为特别糟糕的例子:

代码语言:python
复制
>>> 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 属性给出了该词的对数概率估计:

代码语言:python
复制
>>> 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:

代码语言:python
复制
>>> 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 数组的形式提供:

代码语言:python
复制
>>> pleaded = tokens[7]
>>> pleaded.vector.shape
(300,)
>>> pleaded.vector[:5]
array([ 0.04229792,  0.07459262,  0.00820188, -0.02181299,  0.07519238], dtype=float32)

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

代码语言:python
复制
>>> 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”的法律含义相关,另一个则指更一般的意义。理清这些簇是当前的一个活跃研究领域。

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

代码语言:python
复制
>>> 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

这些词看起来确实像是金可能会因为作家为其添加副词而加以训斥的。回想一下,我们最初的副词高亮函数是这样的:

代码语言:python
复制
>>> 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”这样富有表现力的沟通动词的副词。我们现在已经构建了一个代表这类词的向量,因此现在我们可以基于微妙的逻辑来高亮副词,根据我们的初始假设,聚焦于那些看起来在文体上最有问题的副词:

代码语言:python
复制
>>> 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 之间准确度的提升。来自 Jin-ho(ClearNLP 的开发者)的一个建议尤其有用。

详细速度对比

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

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

硬件:Intel i7-3770 (2012)

绝对(每文档毫秒)

系统

分词

标注

解析

spaCy

0.2ms

1ms

19ms

CoreNLP

2ms

10ms

49ms

ZPar

1ms

8ms

850ms

NLTK

4ms

443ms

n/a

相对(相对于 spaCy)

系统

分词

标注

解析

spaCy

1x

1x

1x

CoreNLP

10x

10x

2.6x

ZPar

5x

8x

44.7x

NLTK

20x

443x

n/a

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

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

关于作者

Matthew Honnibal

首席技术官,创始人

Matthew 是 AI 技术领域的领先专家。他于 2009 年获得博士学位,随后又花了 5 年时间发表关于最先进 NLP 系统的研究。他于 2014 年离开学术界,编写 spaCy 并创立了 Explosion。FINISHED

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 [email protected] 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 [email protected] 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Introducing spaCy
    • 功能示例
    • 独立评估
    • 详细速度对比
    • 关于作者
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档