NLP 太难怎么办?教你 8 步实现代码编写!

NLP 太难怎么办?教你 8 步实现代码编写!

计算机很擅长处理结构化的数据,比如工作表、数据库的表等。但我们人类的沟通通常使用词汇,而不是表。对于计算机来说,这是件很不幸的事情。

NLP 太难怎么办?教你 8 步实现代码编写!

不幸的是,我们并没有生活在这个所有数据都是结构化的平行历史中。

这个世界中的许多信息都是非结构化的,比如英文文本,或任何其他人类语言。我们怎样才能让计算机理解非结构化的文字,并从中提取出数据呢?

NLP 太难怎么办?教你 8 步实现代码编写!

自然语言处理(Natural Language Processing,简称NLP)是人工智能的一个子领域,它专注于让计算机理解并处理人类的语言。我们来看看NLP的工作原理,学学如何写一段Python程序从人类语言的文本中提取信息吧。

注意:如果你不关心NLP的工作原理,只希望复制粘贴代码,可以直接跳到“用Python编写NPL的流水线代码”一节。

NLP 太难怎么办?教你 8 步实现代码编写!

计算机能理解人类语言吗?

自从计算机诞生以来,程序员们就在试图编写能理解英语等语言的程序。理由很明显,人类几千年来一直在用这些语言写东西,如果计算机能阅读并理解这些东西,那对人类非常有帮助。

计算机还不能真正地像人类那样理解英语,但它们已经能做得很好了!在某些特定的领域,NLP的能力已经像魔法一样了。在项目中使用NLP也许可以节省不少时间。

而且更好的是,NLP最新的进展可以很容易地通过开源的Python库访问到,如spaCy、textacy和neuralcoref等。你只需要写几行Python就可以。

NLP 太难怎么办?教你 8 步实现代码编写!

从文本提取含义很困难

阅读并理解英语的过程非常复杂,这还没有考虑英语其实根本没有合乎逻辑并且一致的规则。比如,这个新闻标题是什么意思?

“Environmental regulators grill business owner over illegal coal fires.”

规制者是在质问企业家关于非法烧煤的问题,还是规制者真的把企业家放在火上烧?可见,让计算机分析英语是非常复杂的。

让机器学习做任何复杂的事情通常意味着要建立一个流水线。其思想就是将问题分解成许多非常小的问题,然后用机器学习分别解决每个小问题。然后将几个机器学习模型连接起来让它们互相提供数据,就可以做非常复杂的事情。

这正是我们要在NLP上应用的策略。我们将把理解英语的整个过程分解成几个小步骤,看看每个步骤是怎样工作的。

NLP 太难怎么办?教你 8 步实现代码编写!

一步步建立NLP流水线

我们来看一段来自维基百科的文本:

London is the capital and most populous city of England and the United Kingdom. Standing on the River Thames in the south east of the island of Great Britain, London has been a major settlement for two millennia. It was founded by the Romans, who named it Londinium.(来源:维基百科条目“London”)

这段文字包含几个有用的内容。我们希望计算机阅读这段文本后能理解,伦敦是一个城市,伦敦位于英格兰,伦敦由罗马人建立等等。但为了达到这个目标,首先我们需要让计算机理解书面语言的最基本的概念。

第1步:句子分片

流水线的第一步就是将文本分解成独立的句子。我们可以得到:

1.“London is the capital and most populous city of England and the United Kingdom.”

2.“Standing on the River Thames in the south east of the island of Great Britain, London has been a major settlement for two millennia.”

3.“It was founded by the Romans, who named it Londinium.”

我们可以假设每个英文句子都是独立的含义,写程序理解单个句子要比理解整个段落容易得多。

编写分句的模型非常容易,只需要在看到标点符号时进行分句即可。但现代的NLP流水线同时会用更复杂的技术,即使文档的格式不那么清晰也能正常分解。

第2步:将词汇变成标记(token)

现在我们已经把文档变成了句子,我们可以每次处理一个句子了。首先从文档的第一个句子开始:

“London is the capital and most populous city of England and the United Kingdom.”

流水线中的下一步是将这个句子分解成独立的单词,或者叫标记(token)。这一步叫做标记化(tokenization)。下面是结果:

“London”, “is”, “ the”, “capital”, “and”, “most”, “populous”, “city”, “of”, “England”, “and”, “the”, “United”, “Kingdom”, “.”

对于英语来说标记化很容易,只需要利用空格将单词分隔开即可。我们还要把标点符号作为标记来处理,因为标点符号也是有意义的。

第3步:预测每个标记的词性

下一步,我们将查看每个标记,并猜测它的词性——是名词、动词、还是形容词?等等。弄清楚每个单词的角色能帮我们分析句子的含义。

我们可以把每个单词(以及它上下文环境中的一些单词)提供给已经训练好的词性分类模型:

NLP 太难怎么办?教你 8 步实现代码编写!

词性模型是使用几百万个已经标注了每个单词词性的英语句子训练过的,它被训练成能够重复这种行为。

要注意这个模型完全是依据统计学的,它并不像人类那样理解每个单词的实际意义。它只知道怎样根据以前见过的类似句子来猜测词性。

在处理完整个句子之后,我们得到类似下面的结果:

NLP 太难怎么办?教你 8 步实现代码编写!

有了这些信息,我们就可以提取一些最基本的含义了。例如,我们看到句子中的名词包含“London”和“capital”,所以这个句子很可能就是在讨论伦敦。

第4步:文本词形还原(lemmatization)

在英语(及大多数语言)中,单词会有不同的词形。例如下面两个句子:

I had a pony.

I had two ponies.

两个句子都在谈论名词pony,但它们使用的是不同的变形。在使用计算机处理文本时,知道每个词的基本形很有帮助,这样可以知道两个句子讨论的是同一个概念。否则“pony”和“ponies”两个字符串会被计算机当作两个完全不同的单词。

在NLP中,这个过程叫做词形还原(lemmatization),即找出句子中每个单词的最基本词形(lemma)。

动词也有同样的问题。动词的词形还原可以找出它们的词根,即没有发生变化的形式。因此“I had two ponies”将会变成“I [have] two [pony]”。

词性还原通常通过查找表的方式进行,根据它们的词性,在词形表中找出原形,可能还要加上一些特殊规则来处理一些没见过的词。

下面是经过词形还原,添加了词根形式之后的句子:

NLP 太难怎么办?教你 8 步实现代码编写!

唯一的变化就是把“is”变成了“be”。

第5步:确定停止词

接下来,我们要考虑句子中每个单词的重要性。英语有许多出现频率非常高的填充词,如“and”、“the”和“a”等。在进行词频统计时,这些单词会引入许多噪声,因为它们出现的频率比其他单词高得多。一些NLP流水线会将这些词标记为停止词(stop words),意思是在进行统计分析之前要过滤掉这些词。

将停止词标记成灰色后,我们的句子如下所示:

NLP 太难怎么办?教你 8 步实现代码编写!

识别停止词只需要检查一个固定的已知停止词的词汇表即可。不过,没有能适用于所有应用的标准停止词列表。不同应用程序需要忽略的单词可能并不相同。

例如,如果你要建立一个摇滚乐队搜索引擎,你就不能忽略单词“The”。因为单词“The”不仅出现在许多乐队名称里,八十年代还有个著名的摇滚乐队就叫The The!

第6步:依赖解析

下一步是要找出句子中所有单词之间的关系。这一步叫做依赖解析(dependency parsing)。

目标是建立一棵树,给句子中的每个单词找出一个唯一的父单词。树的根节点就是句子的主要动词。这里是句子的解析树最初的样子:

NLP 太难怎么办?教你 8 步实现代码编写!

但我们还可以进一步。除了找出每个词的父单词之外,我们还能预测两个单词之间的关系类型:

NLP 太难怎么办?教你 8 步实现代码编写!

这个解析树显示,句子的主语是名词“London”,它和“capital”是“be”关系。我们终于知道了些有用的东西——伦敦是首都!如果我们遍历句子的整个解析树(图中没有画出),我们甚至还能发现伦敦是英国的首都。

就像我们之前利用机器学习模型预测词性一样,依赖解析的原理也是将单词提供给一个机器学习模型,并计算出结果。但解析单词之间的依赖通常是个很复杂的任务,想要解释清楚其原理,那又是一整篇文章了。如果想知道它的工作原理,可以从Matthew Honnibal的这篇文章《Parsing English in 500 Lines of Python》(https://explosion.ai/blog/parsing-english-in-python)开始阅读。

但是,尽管作者在2015年说这种方式是标准方法,但实际上现在已经过时,即使作者自己都不再使用这种方法了。在2016年,Google发布了一个新的依赖解析器,叫做Parsey McParseface,它使用深度学习方法,其能力在各种测试中表现优异,于是迅速在业界流传开来。一年之后,他们发布了一个更新的模型,叫做ParseySaurus,有了进一步提高。换句话说,解析的技术现在依然是个活跃的研究领域,依然在不断改变和进步。

还要记住一点很重要的事情:许多英语句子是模糊的,本身就很难解析。这种情况下,模型会根据句子的不同分析结果进行猜测,但有时模型并不完美,也会弄出笑话。但我们的NLP模型还在不断进步,越来越能够以合理的方式解析文本。

想要在自己的句子上试试依赖解析吗?spaCy团队给出了一个很不错的交互式演示(https://explosion.ai/demos/displacy)。

第6b步:找出名词词组

到现在为止,我们是把句子中的每个单词当作独立的实体进行处理的。但有时候对表示同一个意思的单词进行分组可能更有道理。我们可以使用依赖解析树提供的信息,自动地将单词分成表示同一含义的组。

例如,下面的句子:

NLP 太难怎么办?教你 8 步实现代码编写!

可以将名词分组,得到下面的句子:

NLP 太难怎么办?教你 8 步实现代码编写!

是否需要做这一步,取决于我们的最终目标。但如果我们不需要知道究竟哪个词是形容词,而更关心整句的含义,那么这一步通常能很容易地将句子简化。

第7步:命名实体识别(NER)

现在我们完成了所有的力气活儿,终于可以从语法中解脱出来,开始提取语义的实际工作了。

我们句子中的名词如下所示:

NLP 太难怎么办?教你 8 步实现代码编写!

一些名词表示真实存在的东西。例如,“London”、“England”和“United Kingdom”都表示地图上的物理位置。最好能够检测出它们!有了这些信息,我们就能利用NLP自动找出文档中提到的物理位置。

命名实体识别(Named Entity Recognition,简称NER)的目的就是检测并标记这些表示真实世界中存在的概念的名词。把句子中的每个标记通过NER模型之后得到的结果如下:

NLP 太难怎么办?教你 8 步实现代码编写!

但NER系统并不只是简单的字典查找而已。它还会通过单词在句子中出现位置的上下文,和一个统计模型来猜测单词表示的名词的类别。好的NER系统能通过上下文提示区分出人名“Brooklyn Decker”和地名“Brooklyn”。

下面是通常的NER系统能标记的一些对象:

  • 人名;
  • 公司名;
  • 地理位置(包括物理位置和政治位置);
  • 产品名;
  • 日期和时间;
  • 金钱数量;
  • 事件名。

NER可以简化从文本中提取结构化数据的过程,因此得到了非常广泛的应用。这是从NLP流水线中快速获得价值的最简单的方法之一。

想要试试命名实体识别吗?spaCy也提供了另一个交互式的演示(https://explosion.ai/demos/displacy-ent)。

第8步:相互引用解析

此时,我们已经拥有了该句子的有意义的表示。我们知道了每个词的词性,与其他单词的关系,以及哪些词是关于命名实体的。

但是,我们还有个重要的问题。英语里有很多代词,如he、she和it等。我们经常使用这些代称,以避免在句子中反复写出具体的名字。人类能根据上下文跟踪这些单词的意思。但NLP模型还不知道代词的意思,因为它每次只会检查一个句子。

我们来看看文档中的第三个句子:

“It was founded by the Romans, who named it Londinium.”

用我们的NLP流水线分析这句话,我们知道“it”是由罗马人建立的。但如果能知道“London”是由罗马人建立的就会好得多。

人类阅读这句话时很容易就能发现“it”指代的是“London”。相互引用解析(coreference resolution)的作用就是通过在句子间跟踪代词,找出这些同义的映射。我们希望能找出所有指代同一个实体的单词。

在文档上对“London”进行相互引用解析,得到以下结果:

NLP 太难怎么办?教你 8 步实现代码编写!

把相互引用解析的结果跟解析树和命名实体的信息联合起来,我们就能从文档中提取出更多的信息!

相互引用解析是我们要实现的流水线中最难的步骤之一。它甚至比句子分析还要困难。最近深度学习方面的进展给出了更精确的实现,但仍然不够完美。想了解具体情况可以参考这里(https://medium.com/huggingface/state-of-the-art-neural-coreference-resolution-for-chatbots-3302365dcf30)。

希望尝试下相互引用解析?可以看看Hugging Face的这个很不错的相互引用解析演示(https://huggingface.co/coref/)。

NLP 太难怎么办?教你 8 步实现代码编写!

用Python编写NLP流水线

下面是NLP流水线的概貌:

NLP 太难怎么办?教你 8 步实现代码编写!

相互引用解析是个可选的步骤,不一定每次都会做。

注意:在继续之前,应当提一下,这些只是常见的NLP流水线中的步骤,但你可以根据你的目的以及NLP库的实现情况决定跳过某些步骤,或者重新排列它们的顺序。例如,像spaCy等库会利用依赖解析的结果,在流水线中更晚的地方进行句子分片。

我们该如何编写这个流水线呢?感谢spaCy等伟大的Python库,这些工作已经由他们完成了!这些步骤都已经编码好,可以拿来就用。

首先,假设你安装了Python 3,你可以这样安装spaCy:

# Install spaCy

pip3 install -U spacy

# Download the large English model for spaCy

python3 -m spacy download en_core_web_lg

# Install textacy which will also be useful

pip3 install -U textacy

然后在一段文本上运行NLP,代码如下所示:

import spacy

# Load the large English NLP model

nlp = spacy.load('en_core_web_lg')

# The text we want to examine

text = """London is the capital and most populous city of England and

the United Kingdom. Standing on the River Thames in the south east

of the island of Great Britain, London has been a major settlement

for two millennia. It was founded by the Romans, who named it Londinium.

"""

# Parse the text with spaCy. This runs the entire pipeline.

doc = nlp(text)

# 'doc' now contains a parsed version of text. We can use it to do anything we want!

# For example, this will print out all the named entities that were detected:

for entity in doc.ents:

print(f"{entity.text} ({entity.label_})")

运行后就能得到一个从文档中检测出命名实体及类型列表:

London (GPE)

England (GPE)

the United Kingdom (GPE)

the River Thames (FAC)

Great Britain (GPE)

London (GPE)

two millennia (DATE)

Romans (NORP)

Londinium (PERSON)

从这里(https://spacy.io/usage/linguistic-features#entity-types)可以看到每个实体代码的含义。

注意它犯了一个错误,把“Londinium”当做了人名,而不是地名。很可能是因为它的训练数据集中不包括这个词,所以它做了最好的猜测。如果你要分析ID文本包含独有或专用的术语,那么命名实体检测通常需要一些模型调优(https://spacy.io/usage/training#section-ner)。

我们用实体检测的思路做一个数据清洗程序。假设你要遵守新的

GDPR隐私规制

,而且你在几千份文档中发现了能用于识别个人的信息,如人名等。你的任务是删除文档中的所有名字。手工浏览几千份文档并删除所有名字可能会花掉几年的时间。但用NLP就像呼吸一样容易。下面这个简单的清洗程序能删除所有检测到的名字:

import spacy

# Load the large English NLP model

nlp = spacy.load('en_core_web_lg')

# Replace a token with "REDACTED" if it is a name

def replace_name_with_placeholder(token):

if token.ent_iob != 0 and token.ent_type_ == "PERSON":

return "[REDACTED] "

else:

return token.string

# Loop through all the entities in a document and check if they are names

def scrub(text):

doc = nlp(text)

for ent in doc.ents:

ent.merge()

tokens = map(replace_name_with_placeholder, doc)

return "".join(tokens)

s = """

In 1950, Alan Turing published his famous article "Computing Machinery and Intelligence". In 1957, Noam Chomsky’s

Syntactic Structures revolutionized Linguistics with 'universal grammar', a rule based system of syntactic structures.

"""

print(scrub(s))

运行这段代码可以看到它工作得很好:

In 1950, [REDACTED] published his famous article "Computing Machinery and Intelligence". In 1957, [REDACTED]

Syntactic Structures revolutionized Linguistics with 'universal grammar', a rule based system of syntactic structures.

NLP 太难怎么办?教你 8 步实现代码编写!

提取事实

能用spaCy直接实现的功能非常棒。但你还可以把解析过的spaCy输出作为输入,提供给更复杂的数据提取算法。有一个Python库叫做textacy(http://textacy.readthedocs.io/en/stable/),在spaCy的基础上实现了集中常见的数据提取算法。这是个很好的七点。

它实现的算法之一叫做半结构化声明提取(https://textacy.readthedocs.io/en/stable/api_reference.html#textacy.extract.semistructured_statements)。我们可以用它在解析树中搜索简单的声明,声明的主语为“London”,动词为任意形式的“be”,这样可以找出许多关于伦敦的事实。

下面是代码:

import spacy

import textacy.extract

# Load the large English NLP model

nlp = spacy.load('en_core_web_lg')

# The text we want to examine

text = """London is the capital and most populous city of England and the United Kingdom.

Standing on the River Thames in the south east of the island of Great Britain,

London has been a major settlement for two millennia. It was founded by the Romans,

who named it Londinium.

"""

# Parse the document with spaCy

doc = nlp(text)

# Extract semi-structured statements

statements = textacy.extract.semistructured_statements(doc, "London")

# Print the results

print("Here are the things I know about London:")

for statement in statements:

subject, verb, fact = statement

print(f" - {fact}")

下面是输出结果:

Here are the things I know about London:

- the capital and most populous city of England and the United Kingdom.

- a major settlement for two millennia.

看起来也不并不那么惊艳,但如果在整个伦敦的维基百科条目上运行同样的代码,就能得到令人惊讶的结果:

Here are the things I know about London:

- the capital and most populous city of England and the United Kingdom

- a major settlement for two millennia

- the world's most populous city from around 1831 to 1925

- beyond all comparison the largest town in England

- still very compact

- the world's largest city from about 1831 to 1925

- the seat of the Government of the United Kingdom

- vulnerable to flooding

- "one of the World's Greenest Cities" with more than 40 percent green space or open water

- the most populous city and metropolitan area of the European Union and the second most populous in Europe

- the 19th largest city and the 18th largest metropolitan region in the world

- Christian, and has a large number of churches, particularly in the City of London

- also home to sizeable Muslim, Hindu, Sikh, and Jewish communities

- also home to 42 Hindu temples

- the world's most expensive office market for the last three years according to world property journal (2015) report

- one of the pre-eminent financial centres of the world as the most important location for international finance

- the world top city destination as ranked by TripAdvisor users

- a major international air transport hub with the busiest city airspace in the world

- the centre of the National Rail network, with 70 percent of rail journeys starting or ending in London

- a major global centre of higher education teaching and research and has the largest concentration of higher education institutes in Europe

- home to designers Vivienne Westwood, Galliano, Stella McCartney, Manolo Blahnik, and Jimmy Choo, among others

- the setting for many works of literature

- a major centre for television production, with studios including BBC Television Centre, The Fountain Studios and The London Studios

- also a centre for urban music

- the "greenest city" in Europe with 35,000 acres of public parks, woodlands and gardens

- not the capital of England, as England does not have its own government

现在事情开始有意思了!这些自动收集的信息数量很庞大。

还可以安装neuralcoref库(https://github.com/huggingface/neuralcoref),向流水线中加入互相引用解析。这样能得到更多的事实,因为它能抓住那些主语是“it”而不是“London”的句子。

还能做什么?

阅读spaCy的文档(https://spacy.io/api/doc)和textacy的文档(http://textacy.readthedocs.io/en/latest/),你会发现许多使用各种方法解析文本的例子。我们现在介绍的只是沧海一粟。

这里还有另一个实际的例子:想像你在建一个网站,利用我们从上面的例子中提取的信息,允许用户查看每个城市的信息。

如果网站上有个搜索功能,可以像Google那样提供常用的搜索自动完成:

NLP 太难怎么办?教你 8 步实现代码编写!

Google自动完成了对“London”给出的建议

要实现这一点,我们需要一个可能的自动完成列表才能给用户提供建议。我们可以用NLP迅速地生成这些数据。

下面的方法可以从文档中提取出经常被提到的名词:

import spacy

import textacy.extract

# Load the large English NLP model

nlp = spacy.load('en_core_web_lg')

# The text we want to examine

text = """London is [.. shortened for space ..]"""

# Parse the document with spaCy

doc = nlp(text)

# Extract noun chunks that appear

noun_chunks = textacy.extract.noun_chunks(doc, min_freq=3)

# Convert noun chunks to lowercase strings

noun_chunks = map(str, noun_chunks)

noun_chunks = map(str.lower, noun_chunks)

# Print out any nouns that are at least 2 words long

for noun_chunk in set(noun_chunks):

if len(noun_chunk.split(" ")) > 1:

print(noun_chunk)

在伦敦的维基百科条目上运行这段代码,可以得到以下的输出:

westminster abbey

natural history museum

west end

east end

st paul's cathedral

royal albert hall

london underground

great fire

british museum

london eye

.... etc ....

NLP 太难怎么办?教你 8 步实现代码编写!

继续深入

这只是个NLP最简单的介绍。以后的文章里我们会讨论其他NLP的应用,如文本分类,以及像Amazon Alexa那种系统怎样解析问题等。

但现在,你可以安装spaCy(https://spacy.io/)尽情尝试了!如果你不用Python,也可以选其他的NLP库,概念应该都是相同的。

原文:https://medium.com/@ageitgey/natural-language-processing-is-fun-9a0bff37854e

作者:Adam Geitgey,对计算机和机器学习感兴趣,喜欢写有关机器学习的文章。

译者:弯月,责编:郭芮

相关推荐