几乎在对文本进行任何自然语言处理之前,都必须对其进行规范化。作为任何规范化的一部分,通常至少有三项任务:
- 分词(Tokenizing (segmenting) words)
- 词格式(word formats)规范化
- 分句
在接下来的章节中,我们将逐一讲解这些任务。
让我们从一个简单、有点幼稚的针对英语的分词和规范化(和频率计算)版本开始,该版本受到 Church(1994)1 启发,可以只使用一个 UNIX 命令完成。我们将使用一些 Unix 命令:tr
,用于改变输入中的特定字符;sort
,按字母顺序对输入行进行排序;uniq
,统计(collapse and count)文本文件中相邻且相同的行。
例如,让我们从一个包含 Shakespeare “完整单词”(complete words)的文本文件 sh.txt
开始(译者注:应该是指 Shakespeare 所有作品文本都放在一行,然后该行放入 sh.txt
中)。我们可以使用 tr
将每个非字母字符序列改为换行来进行分词(“A-Za-z”表示字母,-c
选项表示反选,即非字母,-s
选项将所有重复序列压缩为一个字符):
tr -sc 'A-Za-z' '\n' < sh.txt
该命令的输出如下:
THE
SONNETS
by
William
Shakespeare
From
fairest
creatures
We
...
现在每行只有一个单词,我们可以对这些行进行排序,并将结果传递给 uniq -c
进行统计(译者注:每列旁边显示该行重复出现的次数):
tr -sc 'A-Za-z' '\n' < sh.txt | sort | uniq -c
命令输出如下:
1945 A
72 AARON
19 ABBESS
25 Aaron
6 Abate
1 Abates
5 Abbess
6 Abbey
3 Abbot
...
另外,我们也可以先把所有的大写字母转成小写字母:
tr -sc 'A-Za-z' '\n' < sh.txt | tr A-Z a-z | sort | uniq -c
命令输出为:
14725 a
97 aaron
1 abaissiez
10 abandon
2 abandoned
2 abase
1 abash
14 abate
3 abated
3 abatement
...
现在我们可以再进行排序,找到经常出现的词。sort
的 -n
选项意味着按数字而不是按字母排序,而 -r
选项意味着按相反的顺序排序(从高到低):
tr -sc 'A-Za-z' '\n' < sh.txt | tr A-Z a-z | sort | uniq -c | sort -n -r
结果表明,与其他语料库一样,Shakespeare 中出现频率最高的词是冠词(articles)、代词(pronouns)和介词(prepositions)等短虚词(function words):
27378 the
26084 and
22538 i
19771 to
17481 of
14725 a
13826 you
12489 my
11318 that
11112 in
...
这类 Unix 工具在为任何语料库快速建立字数统计时都非常方便。
以上简单的 UNIX 工具对于获得粗略的单词统计是没有问题的,但是对于分词,也就是将文本分割成一个个词的任务,一般需要更复杂的分词算法。
Unix 命令过滤了所有的数字和标点符号,但对于大多数 NLP 应用来说,我们需要在分词任务中保留这些字符。我们往往想把标点符号视作一个单独词;对于解析器来说,逗号是一个有用的信息。句号有助于指示句子的边界。但我们有时会想保留词内部的标点符号,例如 m.p.h、Ph.D.、AT&T 和 cap'n。价格($45.55)和日期(01/02/06
)中的特殊字符和数字也需要保留;我们不想把这个价格分成“45”和“55”。还有网址(http://www.stanford.edu
)、推特标签(#nlproc
)和电子邮件地址(someone@cs.colorado.edu
)。
数字也会带来其他复杂的问题;逗号通常是单词的边界,但在英语中,逗号也会用在数字内部,每三位数有一个逗号:555,500.50
。不同语言对分词的要求也不一样,许多欧洲大陆的语言,如西班牙语、法语和德语,则用逗号来标记小数点,英语放逗号的地方则用空格(有时也用句号),例如:555 500,50
。
分词器也可以用来扩展以撇号(apostrophes)标记的附着语素(clitic),例如,将 what're
转换为 what are
,we're
转换为 we are
。附着语素是一个词的一部分,它不能独立存在,只有在与另一个词相连时才会出现。一些这样的缩略语也会出现在其他字母语言(alphabetic languages)中,包括法语中的冠词和代词(j'ai
,l'homme
)。
根据应用不同,分词算法还可以将多词表达(如 New York
和 rock 'n' roll
)分为一个单一的词,这需要某种多词表达字典。因此,分词与命名实体识别(named entity recognition)紧密相连,即检测名字、日期和组织的任务(第 8 章)。
一个常用的分词标准是 Penn Treebank 分词(Penn Treebank tokenization)标准,用于语言数据联盟(Linguistic Data Consortium)(LDC)发布的解析语料库(treebanks),它是许多有用数据集的来源。该标准会分离出附着语素(doesn’t 变成 does 加 n’t),保持连字符词在一起,并分离出所有标点符号(为了节省空间,我们在标记之间加入可见空格‘␣’,尽管换行更常见):
输入:"The San Francisco-based restaurant," they said, "doesn't charge $10".
输出:"␣The␣San␣Francisco-based␣restaurant␣,␣"␣they␣said␣,␣"␣does␣n't␣charge␣$␣10␣"␣.
在实践中,由于分词需要在其他自然语言处理任务之前进行,所以它必须非常快。因此,分词的标准方法是使用基于正则表达式的确定性算法,这些正则表达式被编译成非常高效的有限状态自动机(finite state automata)。例如,图 2.12 显示了一个使用基本正则表达式进行分词的例子,使用基于 Python 的自然语言工具包(Natural Language Toolkit)(NLTK)的 ntrk.regexp_tokenize
函数(Bird et al. 20092; http://www.nltk.org)。
精心设计的确定性算法可以处理歧义问题,例如,当把撇号用作属格标记(genitive marker)(如 the book’s cover),引用标记如 ‘The other class’, she said 中的引号,或者像 they’re 这样用在附着语素中,撇号都需要进行不同的分词处理。
在书面汉语、日语和泰语等语言中,分词更为复杂,因为这些语言不使用空格来标记潜在的词的边界。例如,在中文中,词(words)是由汉字(characters)组成的(中文称为“hanzi”)。每个汉字一般代表一个语义单位(称为语素(morpheme)),并可读作一个音节。词的平均长度约为 2.4 个汉字。但在汉语中,决定什么算作一个词是很复杂的。例如,考虑以下句子:
(2.4) 姚明进入总决赛
“Yao Ming reaches the finals”
如 Chen et al. (2017)3 指出,可将其视为 3 个词(‘Chinese Treebank’ 分割):
(2.5) 姚明 进入 总决赛
YaoMing reaches finals
或者视为 5 个词(‘Peking University’ 分割):
(2.6) 姚 明 进入 总 决赛
Yao Ming reaches overall finals
最后,在汉语中可以干脆完全不考虑词,以汉字为基本元素,把句子当作一连串的 7 个字:
(2.7) 姚 明 进 入 总 决 赛
Yao Ming enter enter overall decision game
事实上,对于大多数中文 NLP 任务来说,事实证明采取字而不是词作为输入效果更好,因为对于大多数应用来说,字已经包含了足够的语义,而且相比之下,大多数使用词作为输入的方法会导致巨大的词汇表,该表会包含大量非常罕见的词(Li et al., 20194)。
然而,对于日语和泰语来说,字的单位太小,因此需要使用分词(word segmentation)算法。在极少的情况下,这些算法对中文的分词也很有用,因为在这种情况下,需要的是词的边界而不是字的边界。这些语言的标准分词算法是使用有监督机器学习训练的神经序列模型(sequence models),在人工标注的训练集上进行训练;我们将在第 8 章和第 9 章介绍序列模型。
我们还有第三种分词方法。既不是将 token 定义为词(无论是用空格或更复杂的算法来分隔),也不是定义为字符(例如在中文任务中),我们可以让数据自动告诉我们应该将什么定义为 token。这在处理未登录词(unknown words)时特别有用,这在 NLP 中是一个很重要的问题。正如我们将在下一章看到的那样,NLP 算法通常会从一个语料库(训练语料库)进行学习,从中提取一些关于语言的事实,然后利用这些事实对另外一个测试语料库进行推理。因此,假设我们的训练语料库中包含,比如说 low、new、newer 等词,但不包含 lower,那么如果在测试语料库中出现了 lower 这个词,我们的系统就不知道该怎么处理它了。
为了处理这种未登录词的问题,现代分词器通常会自动推导出包括比词(words)更小的 token 集,称为子词(subwords)。子词可以是任意的子串,也可以是有语义的单位如 -est 或 -er 这种语素。语素是语言中最小的语义单位,例如 unlikeliest 这个词就有语素 un-、likely 和 -est。在现代分词方案中,大多数 token 是词,但有些 token 是经常出现的语素或其他子词,如 -er。因此,每一个未登录词,如 lower,都可以用一些已知的子词来表示,如 low 和 er,必要时甚至可以用单个字母序列来表示。
大多数分词方案有两个部分:一个 token 学习器(token learner)和一个 token 分割器(token segmenter)。Token 学习器使用原始训练语料库(有时会预先进行粗略分词,如使用空格),然后得到一个词汇表(vocabulary),一个 token 集合。Token 分割器将一个原始测试句子分割成词汇表中的 token。有三种广泛使用的算法:字节对编码(byte-pair encoding)(Sennrich et al., 20165)、一元语法语言模型(unigram language modeling)(Kudo, 2018)6 和 WordPiece(Schuster and Nakajima, 20127);还有一个 SentencePiece 库,包含了这三种算法中前两种的实现(Kudo and Richardson, 20188)。
在本节中,我们将介绍三种算法中最简单的一种,即字节对编码或 BPE 算法(Sennrich et al., 20165),见图 2.13。BPE 的 token 学习器从一个初始词汇表开始,这个词汇表只是所有单个字符(characters)的集合。然后它遍历训练语料库,选择两个最常相邻的符号(symbol)(比如说 ‘A’、‘B’),将两者合并成一个新的符号‘AB’添加到词汇表中,并将语料库中每一个相邻的‘A’‘B’替换为新的‘AB’。以此循环往复,得到新的越来越长的字符串,直到完成了
Bostrom and Durrett (2020)9
通常是在词内部运行该算法(不会跨越词边界进行合并),所以先将输入语料库进行空格分割,得到一组字符串,每个字符串对应一个词的字符,再加上一个特殊的词尾符号 _
,以及其频数。我们以下面这个包含 18 个 token 及其频数的微型语料库为例来说明算法时如何运行的(词 low 出现 5 次,词 newer 出现 6 次,以此类推),初始词汇表有 11 个字符:
语料库:
5 l o w _
2 l o w e s t _
6 n e w e r _
3 w i d e r _
2 n e w _
词汇表:
_, d, e, i, l, n, o, r, s, t, w
BPE 算法首先计算所有相邻符号对的频数:最频繁出现的是 e r
这对符号,因为其分别出现在 newer(频数为 6)和 wider(频数为 3)中,共出现 9 次(注意也有可能是先合并 r _
,因为其频数也是 9)。然后我们将这些符号合并,把 er
当作一个符号,再进行统计:
语料库:
5 l o w _
2 l o w e s t _
6 n e w er _
3 w i d er _
2 n e w _
词汇表:
_, d, e, i, l, n, o, r, s, t, w, er
现在最常出现的字符对是 er _
,所以将其合并;此时我们的系统已经学习到存在一个词尾 token 为 er
,表示为 er_
:
语料库:
5 l o w _
2 l o w e s t _
6 n e w er_
3 w i d er_
2 n e w _
词汇表:
_, d, e, i, l, n, o, r, s, t, w, er, er_
接下来是 n e
(频数为 8)合并为 ne
:
语料库:
5 l o w _
2 l o w e s t _
6 ne w er_
3 w i d er_
2 ne w _
词汇表:
_, d, e, i, l, n, o, r, s, t, w, er, er_, ne
如果继续合并,那么就是:
合并 | 当前词汇表 |
---|---|
(ne, w) |
_, d, e, i, l, n, o, r, s, t, w, er, er_, ne, new |
(l, o) |
_, d, e, i, l, n, o, r, s, t, w, er, er_, ne, new, lo |
(lo, w) |
_, d, e, i, l, n, o, r, s, t, w, er, er_, ne, new, lo, low |
(new, er_) |
_, d, e, i, l, n, o, r, s, t, w, er, er_, ne, new, lo, low, newer_ |
(low, _) |
_, d, e, i, l, n, o, r, s, t, w, er, er_, ne, new, lo, low, newer_, low_ |
一旦我们学到了词汇表,token 解析器(token parser)就会对测试句子进行分词。Token 解析器会按照词汇表顺序,贪婪地在测试数据上匹配我们从训练数据中学到的词。(因此,测试数据中的频率并不发挥作用,发挥作用的只有训练数据中的频率)。所以,首先我们把每个测试句子的词分割成字符。然后我们会应用第一条规则:将测试语料库中的 e r
替换为 er
,然后应用第二条规则:将测试语料库中的 er _
替换为 er_
,以此类推。到最后,如果测试语料库中包含 n e w e r _
这个词,它将被分为一个完整的词。但是,一个新的(未知)单词,如 l o w e r _
,将被分成两个 token:low er_
。
当然在实际算法中,BPE 的输入会是一个非常大的语料库,并进行成千上万次合并。其结果是,大多数词将被完整表示,只有非常罕见的词(和未登录词)才会需要用它们的部分(译者注:子词 subword)来表示。
词的规范化是将词或者 token 转成标准格式的任务,对于有多种形式的词,如 USA
和 US
或 uh-huh
和 uhhuh
,选择一个规范形式。尽管在规范化过程中会丢失拼写信息,但这种标准化也是有一定价值的。对于关于 US
的信息检索(information retrieval)或信息抽取(information extraction),无论文档中写的是 US
还是 USA
,我们可能都希望能从中提取到信息。
大小写折叠(Case folding)是另一种规范化方法。将所有字母都变成小写形式,即视 Woodchuck 和 woodchuck 为同一个词,这在许多任务中,如信息检索或语音识别,对泛化(generalization)是非常有帮助的。而对于情感分析、其他文本分类任务、信息抽取和机器翻译,大小写信息可以提供相当大的帮助,所以一般不做大小写折叠。这是因为保持如国家 US
与代词 us
之间的差异,比大小写折叠带来的优势大得多。
对于自然语言处理的许多情况,我们也希望一个词的两种不同形态表现是相似的。例如,在网络搜索中,有人可能会键入字符串 woodchucks,但一个有用的系统可能也会返回提到 woodchuck 的页面,即不带 s。这在形态复杂(morphologically complex)的语言中特别常见,例如俄语,Moscow 这个词在 Moscow、of Moscow 和 to Moscow 等短语中具有不同的结尾。
词形还原(Lemmatization)是确定两个词尽管表面存在差异但具有相同词根(root)的任务。。am、are 和 is 这三个词有共同的词素(lemma)be;dinner 和 dinners 这两个词都有共同的词素 dinner。将这些不同形式的词都还原成其共同的词素,就可以让我们找到俄语中所有提到的单词,比如 Moscow。因此,像 He is reading detective stories 这样的句子,词形还原后就是 He be read detective story。
那么如何进行词形还原?最复杂的词形还原方法涉及到对词进行完整的形态学解析(morphological parsing)。形态学(Morphology)是研究词是如何由较小的语义单位(称为语素)构成的。语素可以分为两大类:词干(stems)—— 词的中心语素,词的主要含义;词缀(affixes)—— 增加各种“附加”(additional)意义。因此,例如,fox 这个词由一个语素(语素 fox)构成,而 cats 这个词由两个语素构成:语素 cat 和语素 -s。形态学解析器会将一个像 cats 这样的词解析成两个语素 cat 和 s,或者将一个西班牙词 amaren(‘if in the future they would love’)解析成语素 amar ‘to love’,以及形态学特征 3PL 和 future subjunctive。
The Porter Stemmer
词形还原算法比较复杂。出于这个原因,我们有时会使用一种更简单但更粗糙的方法,主要是去掉词尾后缀。这种形态学分析的 naive 版本被称为词干提取(stemming)。最广泛使用的词干提取算法之一是 Porter (1980)10。将 Porter stemmer 应用于以下段落:
This was not the map we found in Billy Bones's chest, but an accurate copy, complete in all things-names and heights and soundings-with the single exception of the red crosses and the written notes.
将会得到以下输出:
Thi wa not the map we found in Billi Bone s chest but an accur copi complet in all thing name and height and sound with the singl except of the red cross and the written note
该算法基于一系列串联运行的重写规则,就像一个级联,即每一个步骤的输出都会作为下一个步骤的输入;下面是这些规则的几个例子:
在 Martin Porter 的 主页 上可以找到 Porter stemmer 的 详细规则列表,以及代码(Java、Python 等);也可以参考原始论文 Porter (1980)10。
在我们需要处理同一词素的不同变体时,简单的 stemmer 是有用的。然而,它们确实倾向于犯过度概括和概括不足(over- and under-generalizing)的错误,如下表所示 (Krovetz, 1993)11:
分句(Sentence segmentation)是文本处理的另一个重要步骤。将文本分割成句子最有用的线索是标点符号,如句号、问号和感叹号。问号和感叹号是相对明确的句子边界标记。而句号则比较含糊,既可以作为在句子边界标记,也可以作为缩写标记如 Mr. 和 *Inc.*你刚才读到的上一句话显示了这种歧义的更复杂的情况,其中 Inc. 的最后一个句号既表示缩写,又表示句子边界。因此,分句和分词可以合并一起进行。
一般来说,分句首先需要确定句号是词的一部分还是句子边界标记(基于规则或机器学习)。缩略语词典可以帮助确定句号是否是常用缩略语的一部分;词典可以是手工建立的,也可以是机器学习到的 (Kiss and Strunk, 2006)12,最后的分句器也是如此。例如,在 Stanford CoreNLP 工具包中 (Manning et al., 2014)13,分句是基于规则的,是 tokenization 的一个确定性(deterministic)结果;句子以一个句尾标点(.、! 或 ?)结束,且该句尾标点没有与其他字符组合成一个 token(如缩写或数字),后面可有附加的引号或括号。
Footnotes
-
Church, K. W. (1994). Unix for Poets. Slides from 2nd ELSNET Summer School and unpublished paper ms. ↩
-
Bird, S., Klein, E., and Loper, E. (2009). Natural Language Processing with Python. O’Reilly. ↩
-
Chen, X., Shi, Z., Qiu, X., and Huang, X. (2017). Adversarial multi-criteria learning for Chinese word segmentation. ACL. ↩
-
Li, X., Meng, Y., Sun, X., Han, Q., Yuan, A., and Li, J. (2019). Is word segmentation necessary for deep learning of Chinese representations?. ACL. ↩
-
Sennrich, R., Haddow, B., and Birch, A. (2016). Neural machine translation of rare words with subword units. ACL. ↩ ↩2
-
Kudo, T. (2018). Subword regularization: Improving neural network translation models with multiple subword candidates. ACL. ↩
-
Schuster, M. and Nakajima, K. (2012). Japanese and korean voice search. ICASSP. ↩
-
Kudo, T. and Richardson, J. (2018). SentencePiece: A simple and language independent subword tokenizer and detokenizer for neural text processing. EMNLP. ↩
-
Bostrom, K. and Durrett, G. (2020). Byte pair encoding is suboptimal for language model pretraining. arXiv. ↩
-
Porter, M. F. (1980). An algorithm for suffix stripping. Program 14(3), 130–137. ↩ ↩2
-
Krovetz, R. (1993). Viewing morphology as an inference process. SIGIR-93. ↩
-
Kiss, T. and Strunk, J. (2006). Unsupervised multilingual sentence boundary detection. Computational Linguistics 32(4), 485–525. ↩
-
Manning, C. D., Surdeanu, M., Bauer, J., Finkel, J., Bethard, S., and McClosky, D. (2014). The Stanford CoreNLP natural language processing toolkit. ACL. ↩