自然语言处理概述

在计算机科学领域,我们将人类语言(如英语或普通话)称为“自然”语言,以区别于为
机器设计的语言(如汇编语言、LISP 或XML)。每一种机器语言都是设计出来的:人类工程师
写下一组正式规则,描述用这种语言可以编写哪些语句以及这些语句的含义。规则在先,只有
这组规则是完备的,人们才会开始使用这种语言。人类语言正好相反:使用在先,规则出现在后。
自然语言是进化形成的,就像生物体一样,这是我们称其为“自然语言”的原因。自然语言的
“规则”(比如英语语法)是在事后确立的,而且经常被使用者忽视或破坏。因此,虽然机器可
读语言是严格、高度结构化的,使用明确的句法规则将固定词表中准确定义的概念组合在一起,
但自然语言是模糊、混乱、不断扩展的,并且还在不断变化。

创建出能够理解自然语言的算法,是一件很重要的事情:语言(特别是文本)是我们大多
数交流与文化的基础。互联网上大多是文本。语言是我们存储几乎所有知识的方式。我们的思
想在很大程度上建立在语言之上。但长期以来,理解自然语言的能力一直是机器所无法实现的。
有人曾天真地认为,可以简单地写出“英语规则集”,就像写出LISP 规则集一样。因此,早期
人们在尝试构建自然语言处理(natural language processing,NLP)系统时,都是从“应用语言学”
的视角进行的。工程师和语言学家手动编写复杂的规则集,以实现初级的机器翻译或创建简单
的聊天机器人,比如20 世纪60 年代著名的ELIZA 程序,可以使用模式匹配来进行非常简单的
对话。但是,语言是一种叛逆的事物,很难被形式化所束缚。经过人们数十年的努力,这些系
统的能力仍然令人失望。

直到20 世纪90 年代,手动编写规则一直都是主流方法。但从20 世纪80 年代末开始,由于
出现了更快的计算机和更多的可用数据,一种更好的替代方法开始变得可行。如果你发现自己构建的系统包含大量特定规则,那么作为聪明的工程师,你可能会问:“我能否使用数据语料库来
自动寻找这些规则?我能否在某个规则空间内搜索规则,而不必自己想出这些规则?”提出这
些问题,你就已经开始做机器学习了。因此,在20 世纪80 年代末,人们开始将机器学习方法应
用于自然语言处理。最早的方法是基于决策树,其目的是自动开发先前系统中的那种if/then/else
规则。随后,从logistic 回归开始,统计学方法开始加速发展。随着时间的推移,参数学习模型
完全占据主导地位,语言学被看作一种障碍而不是有用的工具。早期的语音识别专家Frederick
Jelinek 在20 世纪90 年代开玩笑说:“每次我解雇一名语言学家,语音识别的性能都会提高一些。”

这就是现代自然语言处理:利用机器学习和大型数据集,让计算机可以不对语言进行理解
(这是一个更崇高的目标),而是接收一段语言作为输入,并返回一些有用的内容,比如预测以
下内容:

  • “ 这段文本的主题是什么?”(文本分类)
  • “ 这段文本是否包含脏话?”(内容过滤)
  • “ 这段文本是积极的还是消极的?”(情感分析)
  • “ 这是一个不完整的句子,下一个词应该是什么?”(语言模型)
  • “ 这用德语怎么说?”(翻译)
  • “ 你会如何用一段话来概括这篇文章?”(摘要)

在阅读本章时请记住,你训练的文本处理模型并不会像人类一样理解语言;相反,模型只
是在输入数据中寻找统计规律。事实证明,这足以在许多简单任务上表现很好。计算机视觉是
应用于像素的模式识别,与此类似,自然语言处理是应用于单词、句子和段落的模式识别。

自然语言处理的工具集——决策树、logistic 回归——从20 世纪90 年代到21 世纪10 年代
初只经历了缓慢的发展。当时的研究重点大多在特征工程上。我在2013 年第一次参加Kaggle
自然语言处理竞赛并获胜时,我的模型就是基于决策树和logistic 回归的——你猜对了。然而,
在2014 年~ 2015 年,事情终于开始发生变化。很多研究人员开始研究RNN 的语言理解能力,
特别是LSTM,它是一个源自20 世纪90 年代末的序列处理算法,直到那时才开始受到关注。

在2017 年~ 2018 年,一种新的架构取代了RNN,它就是Transformer,本章后半部分会介
绍它。Transformer 在很短时间内就在整个领域取得了巨大进展,如今大多数自然语言处理系统
是基于Transformer 的。

我们来深入了解细节。本章内容既涵盖基础知识,也包括用Transformer 做机器翻译。

准备文本数据

准备文本数据
深度学习模型是可微函数,只能处理数值张量,不能将原始文本作为输入。文本向量化是指
将文本转换为数值张量的过程。文本向量化有许多种形式,但都遵循相同的流程,如图11-1 所示。

  • 首先,将文本标准化,使其更容易处理,比如转换为小写字母或删除标点符号。
  • 然后,将文本拆分为单元[称为词元(token)],比如字符、单词或词组。这一步叫作
    词元化。
  • 最后,将每个词元转换为一个数值向量。这通常需要首先对数据中的所有词元建立索引。
    我们来详细看一下每个步骤。

从原始文本到向量

文本标准化

我们来看下面这两个句子
-“ sunset came. i was staring at the Mexico sky. Isnt nature splendid??”
-“ Sunset came; I stared at the México sky. Isn’t nature splendid?”
两个句子非常相似——事实上,它们几乎完全相同。然而,如果将它们转换成字节串,会
得到非常不同的表示,因为“i”和“I”是不同的字符,“Mexico”和“México”是不同的单词,
“Isnt”不同于“Isn’t”,等等。机器学习模型不会预先知道“i”和“I”是同一个字母、“é”是
带有重音符的“e”,以及“staring”和“stared”是同一个动词的两种形式。

文本标准化是一种简单的特征工程,旨在消除你不希望模型处理的那些编码差异。它不是
机器学习所特有的,如果你想搭建一个搜索引擎,那么也需要做同样的事情。

最简单也是最广泛使用的一种标准化方法是:将所有字母转换为小写并删除标点符号。这
样前面的两个句子就会变为:

  • “ sunset came i was staring at the mexico sky isnt nature splendid”
  • “ sunset came i stared at the méxico sky isnt nature splendid”
    两个句子更加相似了。另一种常见的变换是将特殊字符转换为标准形式,比如将“é”转换
    为“e”、将“æ”转换为“ae”等。这样一来,词元“méxico”就会转换为“mexico”

最后还有一种更高级的标准化方法, 但在机器学习中很少使用, 它就是词干提取
(stemming):将一个词的变体(比如动词的不同变位)转换为相同的表示,比如将“caught”
和“been catching”转换为“[catch]”,或者将“cats”转换为“[cat]”。使用词干提取之后,“was
staring”和“stared”就都会转换为“[stare]”,这样前面两个相似的句子就会变成相同的编码:

  • “ sunset came i [stare] at the mexico sky isnt nature splendid”

使用这些标准化方法之后,模型将需要更少的训练数据,并且具有更好的泛化效果。模型
不需要很多“Sunset”和“sunset”的示例就可以知道二者含义相同,并且即使在训练集中只见
过“mexico”,也可以理解“México”。当然,标准化也可能会删掉一些信息,所以要始终牢记
任务背景。举个例子,如果你的模型要从采访文章中提取出问题,那么你肯定应该将“?”作
为单独的词元,而不应删掉它,因为它对这项特定任务来说很有用。

文本拆分(词元化)

完成文本标准化之后,你需要将文本拆分成能够向量化的单元(词元),这一步叫作词元化。
词元化有以下3 种方法。

  • 单词级词元化(word-level tokenization):词元是以空格(或标点)分隔的子字符串。这
    种方法的一个变体是将部分单词进一步拆分成子词,比如将“staring”拆分成“star+ing”,
    或者将“called”拆分成“call+ed”。
  • N 元语法词元化(N-gram tokenization):词元是 N 个连续单词,比如“the cat”或“he
    was”都是二元语法词元。
  • 字符级词元化(character-level tokenization):每个字符都是一个词元。我们在实践中很
    少采用这种方法,只有在专门的领域才会用到,比如文本生成或语音识别。

一般来说,你可以一直使用单词级词元化或N 元语法词元化。有两种文本处理模型:一种
是关注词序的模型,叫作序列模型(sequence model);另一种将输入单词作为一个集合,不考
虑其原始顺序,叫作词袋模型(bag-of-words model)。如果要构建序列模型,则应使用单词级词
元化;如果要构建词袋模型,则应使用N 元语法词元化。N 元语法可以手动向模型注入少量局
部词序信息。本章将介绍这两种模型及其使用场景。

建立词表索引

将文本拆分成词元之后,你需要将每个词元编码为数值表示。你可以用无状态的方式来执
行此操作,比如将每个词元哈希编码为一个固定的二进制向量,但在实践中,你需要建立训练
数据中所有单词(“词表”)的索引,并为词表中的每个单词分配唯一整数,如下所示。

image
然后,你可以将这个整数转换为神经网络能够处理的向量编码,比如one-hot 向量。

image

token_index = vocabulary[token]
vector[token_index] = 1
return vector

请注意,这一步通常会将词表限制为训练数据中前20 000 或30 000 个最常出现的单词。任
何文本数据集中往往都包含大量独特的单词,其中大部分只出现一两次。对这些罕见词建立索
引会导致特征空间过大,其中大部分特征几乎没有信息量。

第4 章和第5 章在IMDB 数据集上训练了第一个深度学习模型,还记得吗?你使用的数据
来自keras.datasets.imdb,它已经经过预处理转换为整数序列,其中每个整数代表一个特
定单词。当时我们设置num_words=10000,其目的就是将词表限制为训练数据中前10 000 个
最常出现的单词。

这里有一个不可忽略的重要细节:当我们在词表索引中查找一个新的词元时,它可能不存
在。你的训练数据中可能不包含“cherimoya”一词的任何实例(也可能是你将它从词表中去
除了,因为它太罕见了),所以运行token_index = vocabulary[“cherimoya”] 可能导致
KeyError。要处理这种情况,你应该使用“未登录词”(out of vocabulary,缩写为OOV)索引,
以涵盖所有不在索引中的词元。OOV 的索引通常是1,即设置token_index = vocabulary.
get(token, 1)。将整数序列解码为单词时,你需要将1 替换为“[UNK]”之类的词(叫作
“OOV 词元”)。

你可能会问:“为什么索引是1 而不是0 ?”这是因为0 已经被占用了。有两个特殊词元你
会经常用到:OOV 词元(索引为1)和掩码词元(mask token,索引为0)。OOV 词元表示“这
里有我们不认识的一个单词”,掩码词元的含义则是“别理我,我不是一个单词”。你会用掩码
词元来填充序列数据:因为数据批量需要是连续的,一批序列数据中的所有序列必须具有相同
的长度,所以需要对较短的序列进行填充,使其长度与最长序列相同。如果你想用序列[5, 7,
124, 4, 89] 和[8, 34, 21] 生成一个数据批量,那么它应该是这个样子:

[[5, 7, 124, 4, 89]
[8, 34, 21, 0, 0]]

第4 章和第5 章所使用的IMDB 数据集也使用了这种方法,用0 对整数序列批量进行填充。

使用TextVectorization 层

到目前为止的每一个步骤都很容易用纯Python 实现。你可以写出如下所示的代码。

image

image
以上代码的效果如下。

>>> test_sentence = "I write, rewrite, and still rewrite again"
>>> encoded_sentence = vectorizer.encode(test_sentence)
>>> print(encoded_sentence)
[2, 3, 5, 7, 1, 5, 6]
>>> decoded_sentence = vectorizer.decode(encoded_sentence)
>>> print(decoded_sentence)
"i write rewrite and [UNK] rewrite again"

但是,这种做法不是很高效。在实践中,我们会使用Keras 的TextVectorization 层。
它快速高效,可直接用于tf.data 管道或Keras 模型中。
TextVectorization 层的用法如下所示。

image
默认情况下,TextVectorization 层的文本标准化方法是“转换为小写字母并删除标点符
号”,词元化方法是“利用空格进行拆分”。但重要的是,你也可以提供自定义函数来进行标准化和词元化,这表示该层足够灵活,可以处理任何用例。请注意,这种自定义函数的作用对象应该
是tf.string 张量,而不是普通的Python 字符串。例如,该层的默认效果等同于下列代码。

image
要想对文本语料库的词表建立索引,只需调用该层的adapt() 方法,其参数是一个可以生
成字符串的Dataset 对象或者一个由Python 字符串组成的列表。

image
请注意,你可以利用get_vocabulary() 来获取得到的词表,如代码清单11-1 所示。对
于编码为整数序列的文本,如果你需要将其转换回单词,那么这种方法很有用。词表的前两个
元素是掩码词元(索引为0)和OOV 词元(索引为1)。词表中的元素按频率排列,所以对于来
自现实世界的数据集,“the”或“a”这样非常常见的单词会排在前面。

代码清单11-1 显示词表

image
现在你已经掌握了文本预处理的全部知识,下面我们来构建模型。

更多推荐