自然语言处理 02:文本预处理

墨尔本大学 COMP90042 课程笔记

Posted by YEY on March 3, 2020

Lecture 02 文本预处理

1. 引言

现在我们将学习文本预处理。通常情况下,语言数据是带有噪声的,是不干净的,你可能是从网上下载的,它可能有自己的格式,所以,在使用这些数据之前,我们需要对其进行清洗。

1.1 定义

  • 语料库(Corpus):一个 文档(documents)集合(collection)
    • 例如:维基百科中全部的英文文章
  • 文档(Document):一个或者多个 句子(sentence)
    • 通常,这些句子是经过理解性组织过的,可能是谈论某件事情,而不仅仅是一些随机的句子。
    • 例如:维基百科的一篇文章
  • 句子(Sentence)
    • $\textit{“The student is enrolled at the University of Melbourne.”}$
  • 单词(Words):带有意义或者功能的 字符序列(sequence of characters)
  • 单词 token:数据中你所见的每个单词实例。
    • 例如:上面的例句中有 9 个 tokens(单词 $“the”$ 算了 2 次)
  • 单词 type:不同于 token,它是数据中的那些唯一的单词,即不包含重复单词
    • 例如:上面的例句中有 8 个 type(单词 $“the”$ 只算 1 次)
  • 词典(Lexicon 或者 Dictionary)单词 types 的一个集合

1.2 有多少个唯一单词?

这个问题实际上取决于你的数据集有多大,你的数据越多,单词也就越多。当然,像在 Google N-gram 语料库中,并非每一个词都是合法的英文单词,例如:格式标签、URL 等等。所以,我们很难回答在某种语言中有多少个唯一单词,因为人们总是会创造一些新的名字、新的缩写等等,所以单词数量实际上是与数据规模成正比的。

  Token 数量($N$) Type 数量($|V|$)
交换台电话对话 240 万 2 万
莎士比亚 80 万 3 万 1 千
Google N-gram 1 万亿 1300 万

1.3 为什么要进行预处理?

  • 对于大多数 NLP 应用,我们的输入都是文档:
    • $\textit{“This movie is so great!!! U should definitely watch it in the theater! Best sci-fi}$
      $\textit{ eva!”}\to$ 😀

      在这个例子中,我们有一个情感预测任务:我们有一条用户评论,然后我们需要对其进行情感预测以便对电影进行评分。

    • $\textit{“Eu estive em Melbourne no ano passado.”} \to \textit{“I was in Melbourne last year.”}$

      在这个例子中,我们有一个机器翻译任务:我们有一段某种语言的文本,然后我们试图将其转换为一段另一种语言的文本。

  • 关键点:语言是具有 组合性的(compositional)。作为人类,我们并不是直接浏览一段长长的文本,然后随机猜测它的意思。相反,我们可以将这些文档拆解为各个组件或单词,然后我们从左至右地阅读一个个的单词。意义是具有具有组合性的,单词也是具有组合性的,而单词流构成了意义。因此,从直觉上,为了让计算机程序理解语言,我们可以让其完成同样的事情。首先,我们需要将文档拆解成单词,程序对单词进行理解,然后再构成意义。

  • 预处理(Preprocessing) 是第一步,简单来说,它的目的是将文档拆解成单词以便计算机程序能够解释。

1.4 预处理的步骤

  1. 移除不需要的格式(例如:HTML 标签)
  2. 句子分割(Sentence segmentation):将文档拆分成句子
  3. 分词(Word tokenisation):将句子拆分成单词
  4. 词规范化(Word normalisation):将单词转换为规范形式
  5. 去停用词(Stopword removal):删除不需要的词

2. 句子分割

2.1 句子分割

  • 朴素方法:在句子标点处断开(正则:[.?!])
    • 但是,句号 [.] 也常用于缩写词(例如:$U.S. dollar$),感叹号 [!] 有时也因为一些其他原因使用(例如:$Yahoo!$(雅虎) 其实是一个词)
  • 聪明一点的方法:使用正则表达式要求在标点之后有大写字母跟随时断开(正则:[.?!] [A-Z])
    • 但是,缩写词后面也会经常跟着一个名字(例如:$Mr.Brown$)
  • 更好的做法:构建词典(lexicons)
    • 但是,我们很难将所有的名字和缩写词都枚举出来,因为每天都可能会有一些新的名字和缩写词产生
  • 最新技术是采用机器学习而不是依赖规则

2.2 二分类器

  • 查看每一个句号 “.” 并判断它是不是表示一句话的结束。
    • 选择一个 机器学习模型:例如,决策树、Logistic 回归、SVM 等
  • 因此,我们可以搜集一些句号 “.” 附近的相关信息作为模型的 特征
    • 查看句号 “.” 前后的词:
      • 有些词经常出现在句子开头,有些词经常出现在句子结尾,所以,单词本身就包含了一些和句子分割相关的信息。
    • 词的形状(Word shapes):
      • 大写、小写、全大写、数字
      • 字符长度
    • 词性标注(Part-of-speech tags):
      • 单词的词性为我们提供了更多的信息,例如:限定词(Determiners)通常代表了一个句子的开始

3. 分词

3.1 英文分词

  • 朴素方法:分离出字母字符串(正则:\w+)
  • 但这种方法可能无法很好地处理以下问题:
    • 缩写(例如:$U.S.A.$)
    • 连字符(例如:$merry\text{-}go\text{-}round$、$well\text{-}respected$、$yes\text{-}but$ 等)
    • 数字(例如:$1,000,00.01$)
    • 日期(例如:$3/1/2016$)
    • 附着词(例如:$can’t$ 中的 $n’t$)
    • 网络语言(例如:http://www.google.com、#metoo、:-) 等)
    • 多词单位(例如:$New\;Zealand$)

3.2 中文分词

  • 一些亚洲语言的文字之间没有空格
  • 在中文中,一个词通常对应多个字符
  • 例如:

  • 标准方法假设一个词是现有词汇
  • MaxMatch 算法
    • 从左至右,贪婪地匹配词汇表中最长的单词,例如:

    • 但是该算法的难点在于如何构建词汇表,即我们如何能预先知道应该包含哪些词汇。很多时候,构建一个好的词汇表并不是件容易的事,因为人们每天都在创造新的词汇。
    • 而且,即便我们有了一个完美的词汇表,该算法还是有可能会失效。例如:

      对于 “去买新西蓝花” 这个句子来说,如果我们采用 MaxMatch 算法,我们将得到上面第一个句子所示的分词结果,因为 “新西兰” 是一个合法的中文词汇,所以算法会基于贪婪规则去尝试匹配这个词。这将导致句子被解读为:“去(花店)购买一些新西兰 🇳🇿花 🌸”。但实际上,这个句子可能会由于上下文语境的差别而存在歧义,例如上面第二种分词方法所示,我们很可能只是想表达 “去(菜市场)购买一些新鲜的西蓝花 🥦” 而已。

3.3 德语分词

  • 德语中可能存在一些非常长的复合词(compound),例如:
    $Lebensversicherungsgesellschaftsangestellter$
    翻译过来的意思为:
    $\textit{life insurance company employee}$(人寿保险公司员工)
  • 所以,假如我们仅仅采用空格分词的话,将会导致我们的词汇表变得非常大,因此,我们需要一个 复合词分离器(compound splitter)

3.4 Subword 分词

现在,我们将介绍 Subword 分词(Subword Tokenisation),它与我们之前介绍的普通分词技术(Word Tokenisation)所采用的范式略有不同。普通分词技术的目标是将句子拆分成我们所认为的一些合法的单词,而 Subword 分词是一种不同的技巧,我们将句子拆分成 subwords(或者 partial-words)。所以,相比于普通分词技术,我们不再关心拆分后的结果是否是合法的单词,我们只是希望将它们拆分成某种形式的单元。

回顾上节课中的一个例子:

\[\begin{align} & \textit{Colourless green ideas sleep furiously} \\ \to\; & [colour]\;[less]\;[green]\;[idea]\;[s]\;[sleep]\;[furious]\;[ly] \end{align}\]

可以看到,$Colourless$ 这个单词被拆分成了两个单元:$[colour]$ 和 $[less]$,类似的还有 $ideas$ 和 $furiously$ 这两个词也都各自被拆分成了两部分。

那么,我们为什么要这么做呢?

Subword 分词在目前非常流行,原因有很多,其中之一是因为很多深度学习方法已经开始采用这种分词技术,并取得了很好的结果。另外,Subword 分词还具有以下这些优势:

  • 数据信息分词(Data-informed tokenisation)
    Subword 分词是一种数据驱动的分词策略,它不需要使用者具备任何语言学知识,我们只需要一个大的语料库,然后在上面进行训练即可。
  • 适用于不同的语言
    我们将在后面介绍一种用于 Subword 分词的流行算法:Byte-Pair Encoding(BPE),正如它的名字一样,BPE 算法运行在 byte 级别上,与具体的语言种类无关。
  • 能够更好地处理未知词汇
    当遇到未知词汇时,BPE 算法会将它们拆分为单个字符,所以我们不会因为未知词汇而困扰。

3.5 Byte-Pair Encoding(BPE)算法

现在,我们将介绍一种用于 Subword 分词的简单高效的流行算法:Byte-Pair Encoding(BPE)

其核心思想是:迭代地合并频繁的字符对。我们需要一个用于训练的大的语料库(例如:维基百科),然后尝试对其中每个频繁出现的字符对迭代地进行合并,直到得到一个很大的词汇集。

示例: 假设我们有一个小的用于训练的语料库数据集,我们可以创建一个包含该语料库中所有单词的 字典(Dictionary):方括号 “[ ]” 中的数字表示该词在语料库中出现的频率,并且我们将对其末尾的空格符 “_”(它们代表了一个单词的完结)进行一些预处理。然后,我们有一个初始的 词汇表(Vocabulary),它是数据中所有出现过的字符的一个集合,可以看到例子中的词汇表包含了空格符和所有数据中出现过的字母字符。

  • Dictionary
    • [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 _
  • Vocabulary
    • _, d, e, i, l, n, o, r, s, t, w

首先,找出字典中出现频率最高字符对,并且将它们合并后添加到词汇表中。
例如,示例中的 “r_” 字符对一共出现了 3 + 6 = 9 次,所以我们将其合并后加入词汇表中:

  • Dictionary
    • [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 _
  • Vocabulary
    • _, d, e, i, l, n, o, r, s, t, w, r_

重复上面的过程。
例如,示例中的 “er_” 字符对也出现了 9 次,将其合并后加入词汇表中:

  • Dictionary
    • [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 _
  • Vocabulary
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_

继续:

  • Dictionary
    • [5] l o w _
    • [2] l o w e s t _
    • [6] n ew er_
    • [3] w i d er_
    • [2] n ew _
  • Vocabulary
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew

随着上面过程的不断重复,我们可以看到词汇表的变化如下:

  • Vocabulary
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew, new
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew, new, lo
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew, new, lo, low
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew, new, lo, low, newer_
    • _, d, e, i, l, n, o, r, s, t, w, r_, er_, ew, new, lo, low, newer_, low_

    ……

总结:

  • 在实践中,BPE 算法运行中会进行成千上万次合并,并创建一个巨大的词汇表。
  • 大部分情况下,那些最常见的单词将被表示为完整单词。
  • 而少数情况下,一些罕见的词会被拆分成 subwords。
  • 最坏情况下,测试集中的未知词汇将被拆解为一个个单独的字母,所以永远不会有未知词汇。

4. 词规范化

到这里,我们已经完成了分词,接下来我们将进行词规范化,目的是将已经分词的单词转换成正规形式。

4.1 词规范化

词规范化(Word Normalisation) 通常有以下几种方法:

  • 小写字母(例如:$Australia\to australia$)
  • 移除形态(例如:$folks\to folk$)
  • 拼写纠错(例如:$camputer\to computer$)
  • 缩写展开(例如:$U.S.A\to USA$)

目的有两个:

  • 缩小词汇表
  • 将具有相同意义的单词映射为相同的 type

4.2 屈折形态

  • 屈折形态(Inflectional Morphology) 创造了语法上的变体:它们是同一个词的不同形态。
  • 英语中,名词(nouns)、动词(verbs)和形容词(adjectives)会发生形变
    • 名词:名词的 数量($\text{-}s$)
    • 动词:主语的 数量($\text{-}s$)、动作的 方面($\text{-}ing$)和 时态($\text{-}ed$)
    • 形容词:比较级($\text{-}er$)和 最高级($\text{-}est$)
  • 很多其他语言中有着比英语更加丰富的屈折形态
    • 例如法语中,名词和冠词也会受到性别的影响:
      $\textit{un chat}$:一只(公)猫
      $\textit{une chatte}$:一只(母)猫

4.3 词形还原

  • 词形还原(Lemmatisation) 意思是移除一个词的任何屈折形态,将其还原为无屈折变化的形态(uninflected form,也称为 lemma)。
    • 例如:$speaking\to speak$
  • 在英语中,我们可以写下一些规则来实现词形还原,这不算太困难。但是,还存在有很多不合常规的词会让我们的解决方案变得比较繁琐,例如:
    • $poked\to poke$(不是 $pok$)
    • $stopping\to stop$(不是 $stopp$)
    • $watches\to watch$(不是 $watche$)
    • $was\to be$(不是 $wa$)

    因为这些不规则变化的存在,词形还原有时并不如我们想象的那么简单。

  • 为了进行准确的词形还原,我们需要构建一个由所有的合法 lemmas 组成的词典,然后在此基础上进行词形还原。

4.4 派生形态

  • 不同于屈折形态,派生形态(Derivational Morphology) 通常会创造出新的词和词性。
  • 英语中的派生 后缀(suffixes) 通常会改变词的类别,例如:
    • $\text{-}ly$($personal\to personally$)
    • $\text{-}ise$($final\to finalise$)
    • $\text{-}er$($write\to writer$)
  • 英语中的派生 前缀(prefixes) 通常会改变词的意思,而不会改变词的类别,例如:
    • $re\text{-}$($write\to rewrite$)
    • $un\text{-}$($healthy\to unhealthy$)

4.5 词干提取

  • 词干提取(Stemming) 会去除所有的后缀,只保留 stem(主干)
  • stem 和 lemma 很相似,但二者并不相同。stem 通常不是一个词汇项(lexical item),事实上,通常你不会在词典中找到它。
    • 例如:$automate,\;automatic,\;automation\to automat$
      可以看见,上面左边的 3 个词都属于派生形态,词形还原(Lemmatisation)并不会对它们进行处理,但是词干提取(Stemming)会将它们都转换成同一个 stem($automat$,虽然这并不是一个合法的英文单词)。
    • 通常并不是实际的词汇项
  • 词汇稀疏性(lexical sparsity)比 lemmatisation 还要低
    即经过 stemming 处理后,词汇表(Vocabullary)会进一步缩小。
  • 常用于信息检索(Information Retrieval)
    因为在信息检索中,你永远不需要将这些 stem 呈现给用户,搜索引擎只需要返回相关性最高的文档(document)给用户即可。
  • Stem 并不总是可翻译的
    在一些像机器翻译之类的应用中,输出是对用户可见的。因此,我们希望它们是有意义的,但是并不是所有的 stem 都是合法的单词。

4.6 The Porter Stemmer

  • 波特词干器(Porter Stemmer) 是目前最流行的英文词干提取算法。
  • 只适用于英文,实际上,词干提取任务是和语言种类密切相关的。
  • 分阶段应用 重写规则(rewrite rules)
    • 首先,去除 屈折后缀(inflectional suffixes)
      例如:$\text{-}ies \to \text{-}i$
    • 然后,去除 派生后缀(derivational suffixes)
      例如:$\text{-}isation\to \text{-}ies \to \text{-}i$

在展开讨论 Porter Stemmer 的那些规则之前,我们将先介绍一些 定义

  • C = 一个辅音字母(consonants)序列
    • 例如:s, ss, tr, bl
  • V = 一个元音字母(vowels)序列
    • 例如:o, oo, ee, io
  • 一个单词具有下面四种形式之一:
    • CVCV … C
    • CVCV … V
    • VCVC … C
    • VCVC … V
  • 以上几种情况都可以被表示为下面的形式,其中方括号 [ ] 表示可选项,m 我们称为 measure(度量):
    • [C]VCVC … [V]
    • [C] (VC)m [V]
    • m = measure

    通常,越长的词具有越大的 measure,例如:

    • m = 0: TR, EE, TREE, Y, BY
    • m = 1: TROUBLE, OATS, TREES, IVY
    • m = 2: TROUBLES, PRIVATE, OATEN, ORRERY

现在,我们来看一下具体的 规则,了解 Porter Stemmer 算法是如何工作的:

  • 规则格式:(条件)S1 $\to$ S2
    规则很简单,我们有一个前置条件,然后将规则 S1 改写为 S2。
    注意:这里判断前置条件时只考虑原单词中 S1 之前的部分(即不包括 S1 部分)。
  • 例如,我们可以将规则设定为:(m > 1)EMENT $\to$ null
    • 以单词 REPLACEMENT 为例,在此规则下我们计算 RELPAC 部分的 m 值:
      REPLAC = CVCVC = C (VC)2 $\to$ m = 2 > 1
    • 满足规则重写的条件,我们最终得到的 stem 为:
      REPLACEMENT $\to$ REPLAC
  • 总是使用能够匹配到的最长的 S1
    • 例如,制定如下规则:
      SSES $\to$ SS
      IES $\to$ I
      SS $\to$ SS
      S $\to$ null
    • 在此规则下的一些例子:
      CARESSES $\to$ CARESS
      CARESS $\to$ CARESS
      CARES $\to$ CARE

Porter Stemmer 一共分为下面 5 个步骤:
表格中的正例(Positive Example)代表规则中的条件满足的情况;负例(Negative Example)代表规则中的条件不满足的情况。

  • 步骤 1: 处理屈折形态变体(Inflectional Morphology Variants)
    例如:复数(plurals)和过去分词(past participles)

  • 步骤 2, 3, 4: 处理派生形态后缀(Derivational Morphology Suffixes)

  • 步骤 5: 整理(注意:*o 中的小写字母 c 和 v 仅代表单个辅音和元音字母)

更多例子:

  • computational $\to$ comput
    • 步骤 2: ATIONAL $\to$ ATE: computate
    • 步骤 4: ATE $\to$ null: comput
  • computer $\to$ comput
    • 步骤 4: ER $\to$ null: comput

4.7 拼写纠错

  • 为什么需要拼写纠错?
    • 拼写错误会创造新的、罕见的 types
      如果有很多拼写错误,将导致词汇表变得很大,并且里面很多罕见词汇,这种情况是我们不希望看到的。
    • 拼写错误会破坏各种语言分析
      如今,纠正拼写错误正变得越来越重要,因为越来越多的人们将他们的内容或者数据上传到网络上(例如:维基百科),这些由人们书写的内容很可能会产生拼写错误,而一些编辑器并不会处理它们。
    • 在互联网语料库中非常常见
    • 在网络搜索中,对于查询语句(queries)的拼写纠错尤其重要
  • 如何实现拼写纠错?
    • 字符串距离(例如:Levenshtein 距离等等)
      我们可以构建一个字典,计算错误单词与字典中单词的字符串距离,并将其替换为字典中距离最近的单词。
    • 对错误的 types 进行建模(例如:phonetic、typing 等等)
      如果你是一名语言学家,你可以做更多的事情。你可以对一些人们容易犯的拼写错误 types 进行建模从而实现纠错。
    • 采用 n-gram 语言模型
      或者,如果你倾向于数据驱动的解决方案,可以尝试语言模型。通常,语言模型可以用于文本生成任务。此外,如果一个语言模型是在字符级别(character level)上训练的,那么它也可以用于拼写纠错。

4.8 其他的词规范化方法

  • 拼写变体规范化(Normalising spelling variations)
    • $\textit{Normalize } \to \textit{ Normalise}$(反之亦然)
    • $\textit{U r so coool! } \to \textit{ you are so cool!}$
  • 缩写展开(Expanding abbreviations)
    • $\textit{US, U.S. } \to \textit{ United States}$
    • $\textit{imho } \to \textit{ in my humble opinion}$

5. 去停用词

去停用词(Stopword Removal) 是我们的预处理管道(Preprocessing Pipeline)中的最后一步。

5.1 停用词

  • 定义:需要被从文档中移除的词组成的一个列表
    • 在词袋(bag-of-words, BOW)表示的任务中很典型
      例如:搜索引擎
    • 不适用于单词序列很重要的情况
      例如:语言模型
  • 如何选择停用词?
    • 所有 封闭类型的功能性的
      例如:$the,\; a,\; of,\; for,\; he,\;…$
    • 任何高频词
    • NLTK, spaCy NLP toolkits

6. 总结

  • 在文本分析中不可避免地会涉及预处理
  • 预处理会对下游应用产生重大影响
  • 确切的步骤可能会因为语料库和任务的不同而存在一些差异
  • 简单的基于规则的系统运行良好,但很少能完美运行
  • 文本预处理的方法与具体的语言种类相关

7. 思考

思考1: 当我们进行词规范化(word normalization)时,词形还原(Lemmatisation)和词干提取(Stemming)之间是否存在顺序?还是说我们只是根据不同情况选择其中某一种进行预处理?

我们会根据不同的任务选择其中的一种。Stemming 常用于信息检索(Information Retrieval,IR),但是对于大多数 NLP 任务,我们通常选择 lemmatisation。原因是在进行 stemming 后,我们会丢失大量和 tokens 相关的信息(lemmatisation 也会丢失一些信息,但是总体来说要少得多)。而对于大部分 NLP 任务,以丢失更多的单词含义为代价来换取更低的词汇稀疏性是不值得的,因为这将破坏句子原本的意思。


思考2: 在 Byte-Pair Encoding(BPE)算法中,我们通过合并字符得到一个个新的 “词”:1. 我们什么时候应该停止合并?2. 在我们停止合并之后,我们应该将哪些词作为最终的 tokens,是词汇表中所有的词还是其中一部分?

对于第 1 个问题,迭代次数 $k$ 是算法的一个参数,我们需要根据下游任务的表现来对其进行调整;对于第 2 个问题,我们会保留 token list 里面的所有 tokens,包括那些频率为 $0$ 但可能会出现在你的测试数据中的 tokens。


思考3: 词干提取(Stemming)通常用于解决派生后缀(derivational suffixes)的问题,那么对于派生前缀(derivational prefixes)应该如何处理?

Stemming 是将变形后的单词缩减到其对应的词干 stem 的过程,它不一定要移除后缀。但是通常,我们在谈到 stemming 时确实是指移除后缀,这可能是由于大多数词干提取器(stemmer)只会对后缀进行处理。当然,如果你想对前缀进行 stemming,你也可以自己写一个移除前缀的 function,更多细节可以参考 这里

8. 扩展阅读

下节内容:N-gram 语言模型

知识共享许可协议本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 欢迎转载,并请注明来自:YEY 的博客 同时保持文章内容的完整和以上声明信息!