第2章 构建自定义语料库

  • 与所有机器学习应用程序一样,最主要的挑战是确定噪声中是否存在信号,以及信号具体隐藏在何处。这是通过特征分析过程完成的,确定文本的含义和潜在结构通过编码体现为哪些特征、属性或维度。前一章中,我们看到,尽管自然语言具有复杂性和灵活性,但如果能提取其结构特征和上下文特征,就可以进行建模。
  • 后续所有章节的大部分工作将围绕着"特征提取"和"知识工程"展开,包括单独词汇单词的识别、同义词集合、实体间相互关系以及语义上下文。正如我们将在本书中看到的那样,我们使用的基础语言结构的表示方法在很大程度上决定了处理是否成功。要确定如何表示,我们首先要定义语言的单位,用来计算、测量、分析和学习。
  • 某种意义上,文本分析是将任务主体分解成小的组成部分的过程(单独的词汇单词、常用短语、句法模式),再运用统计机制进行处理。通过学习这些组件,我们可以构建语言模型,并赋予应用程序语言的预测能力。我们很快会看到,可以再很多层面上应用分析技术,而所有这些围绕着一个核心的文本数据集:语料库(Corpora)。

语料库是什么?

  • 语料库是包含自然语言相关文档的集合。语料库可大可小,通常包含好几GB、甚至上百GB数据。例如一般电子邮箱大小是2GB(作为参考,Enron公司邮件语料库的完整版本大概在十多年前发布,包括118个用户之间的1MB电子邮件,大小为160GB),公司有大概200名员工,整个公司将有大约0.5TB的电子邮件语料。语料库有的是标记过的(annotated),意味着文本或文档已经标记出监督学习算法的正确响应(例如,用于构建检测垃圾邮件的过滤器),有的则是未标记(unannotated)的,可用于主题建模和文档聚类(例如,探索文本随时间推移潜在主题的变化)。
  • 语料库可分解为文档或单个文档。语料库包含的文档大小各不相同,从推文到书籍都有可能,但它们都包含文本(或者元数据)和一组相关的看法。文件可进一步分成段落和语篇(discourse)单元,每个语篇单元往往表达一个单一的思想。段落可以进一步细分为句子,句子也是句法(syntex)的基本单位;完整的句子是在结构上比较合理的表达。句子由词和标点符号组成,词汇(Lexical)单元用来表达基本的意义,组合使用更为有效。最后,单词本身又由音节、因素、词缀和字符组成,这些单元只有在组成单词时才有意义。

特定语料库

  • 一开始用通用语料库测试自然语言模型是很普遍的。例如,有很多例子,科研论文往往会使用现成的数据集,如布朗语料库、维基百科语料库或康奈尔电影对白语料库等。但是,最好的语言模型往往具有一定的限制、只适合特定应用。
  • 为什么特定领域或特定场景中训练的模型比通用语言上训练的模型表现更好?不同领域有不同的用语(词汇、缩略语、常用短语等),因此,相比涵盖若干不同领域的语料库,领域相对纯粹的语料库能更好地进行分析、建模。
  • 比如bank这个词,很可能是在经济、金融或政治领域,表示财政、货币工具相关的机构(银行),而在航空或汽车领域,更可能表示导致车辆或飞机方向改变的一种运动形式(倾斜转弯)。在相对较窄的上下文中拟合模型,预测空间更小、更具体,因此能更好地应对语言的灵活性。
  • 获取特定领域语料库,对打造高性能的语言感知数据产品至关重要。所以,下一个问题是"如何构建用于学习语言模型的数据集?"无论通过抓取、RSS提取,还是API获取,打造能为构建数据产品提供支持的原始文本语料库并不是件容易的事。
  • 数据科学家通常从收集单个的、静态的文档开始,再用常规方法分析。但是,如果没有常规化、程序化的数据摄取,分析也只能是静态的,无法响应最新的反馈,也无法应对语言的动态性。
  • 在本章中,我们重点关注的不是如何获取数据,而是如何以支持机器学习的方式构建和管理数据。在下一节中,我们会简单介绍一种名为Baleen的数据摄取引擎的框架,该框架特别适合构建应用文本分析的特定领域语料库。

Baleen数据摄取引擎

  • Baleen是用于构建自定义语料库的开源工具。它的工作原理是面向专业和业余作家的文件,如博客和新闻,以分类方式摄取自然语言数据。
  • 给定RSS源的OPML文件(聚合新闻阅读器的常见导出格式),Baleen从这些源下载所有文章,并将它们保存到MongoDB,再导出可用于分析的文本语料库。虽然这个任务看似可以通过单个函数轻松完成,但实际的摄取过程可能会很复杂,API和RSS源可能经常会发生变化。如何组合出应用程序达到最佳效果,不仅能自主摄取,还能安全进行数据管理,显然要花不少心思。
  • 通过RSS进行常规文本摄取的复杂性如图2-1所示。指定要摄取的源以及如何对其进行分类,这些信息保存在磁盘读取的OPML文件中。对文章、订阅源和其他信息进行访问并存入MongoDB,需要对象文档映射(ODM),还要用工具定义单个提取任务,改作业同步整个提要,然后提取和整合单个文章。
  • Baleen开放了基于以上机制的使用程序,可以定期(例如,每小时)运行摄取任务,当然还需要一些配置,来指定数据库连接参数和运行频率。由于这将是个长期运行的过程,Baleen还提供了一个控制台来协助调度、记录日志和监视错误。最后,Baleen的导出工具将语料库从数据库导出。
    第2章 构建自定义语料库_第1张图片
  • 无论是常规摄取文档还是部分获取固定集合,都必须考虑如何管理数据,并为分析处理和模型计算做好准备。在下一节,我们将讨论如何监控语料库,因为我们的摄取历程会继续进行,数据会增长变化。

语料库数据管理

  • 假设即将处理的语料库是非常庞大的,可能包含成千上万篇文档,而最后的大小可能有好几个GB。另外还假设,语言数据将来自数据结构需要清洗和处理才能进行分析的数据源。前一个假设需要可扩展的计算方法(在第11章中更全面地探索),后者意味着我们将对数据进行不可逆的转换(正如我们在第3章看到的那样)。
  • 数据产品通常用一次写入、多次读取(WORM)的存储设施,作为摄取和预处理之间的过渡数据管理层,如2-2所示。WORM存储(有时也称为数据湖)以可重复、可扩展的方式提供对原始数据的流式访问,满足高性能计算的要求。通过将数据保存在WORM存储中,预处理数据可以直接重新分析,无需重新摄取,从而允许在原始数据上轻松开始新的探索过程。
    第2章 构建自定义语料库_第2张图片
  • 在数据提取流水线中加入WORM存储,意味着要把数据保存在两个地方(原始语料库和预处理语料库),这就引出了一个问题:数据存在哪?考虑数据管理时,我们通常首先考虑数据库。数据库无疑是构建语言感知数据产品的好工具,许多数据库都提供了全文搜索功能和其他类索引功能。但是,大多数数据库被设计成每个事务只能检索或者更新几行数据。而对文本语料库的访问,主要是对每个完整文档的读取,不会对文档进行本地更新,也不会搜索或查询单个文档。因此,在这里用数据库往往会增加计算开销,实际上并没什么好处。

关系数据库管理系统非常适合一次对少数几行数据进行操作的事务,特别是当这些行经常更新时。文本语料库上的机器学习需求则有所不同;主要是对整个数据集的多次顺序读取。因此,通常优先考虑将语料库存储在磁盘(或文档数据库)中。

  • 要管理文本数据,最佳选择通常是将数据存到NoSQL文档数据库中,这类数据库能以最小开销读取文档,或者直接把每个文档写进磁盘。尽管NoSQL数据库在大型应用程序中很合适,但基于文件的方法也有其优点:文件目录上的压缩技术很适合用于文本数据,还能用文件同步服务完成自动复制。用数据构建语料库超出了本书范围。接下来,我们会介绍在磁盘上有效组织数据的方法,来支持系统对语料库的访问。

语料库磁盘结构

  • 组织和管理基于文本的语料库最简单、最常用的方法,是把单独的文档保存在磁盘的文件系统。通过将语料库组成子目录,可以对语料库进行分类或根据元信息(如日期)进行有意义的划分。将每个文档保存为自己对应的文件,语料库读取器可以快速搜索不同的子文档集,还可以并行处理,每个进程处理不同的子文档集。

我们将在下一节探讨如何利用NLTK CopusReader 对象从目录或Zip文件中读取数据。

  • 文本文件也是最容易压缩的格式,压缩得到的Zip文件可以保留磁盘上的目录结构,是比较理想的数据分发、存储格式。另外,存在磁盘上的语料库通常是静态的,可作为整体进行处理,满足上一节要求的WORD存储的要求。
  • 但是,每个文档单独存成一个文件,可能会带来一些挑战。太小的文档(如电子邮件或推文)作为单个文件存储没什么意义。另外,电子邮件通常以MBox格式存储,这是一种用分隔符来分隔其中的文本、HTML、图像、附件的多部分HIME消息,通常可以按电子邮件服务(收件箱、加星标、存档等)包含的类别进行组织。推文通常是小型JSON数据结构,不仅包括推文的文本,还包括其他元数据,如用户或位置信息,存储多个推文的典型方法,是以换行符分隔的JSON内容,有时也叫做JSON行格式。这种格式一次解析一行,就能得到一条推文,也可以在文件中通过搜索找到不同的推文。包含推文的单个文件可能会很大,因此按用户名、位置或日期来组织推文文件,可以减少单个文件大小,创建多文件的、更有意义的磁盘结构。
  • 将数据存储在某个逻辑结构中的另一种方法,是简单写入具有最大容量限制的文件。例如,我们可以持续写入数据,直到达到某个大小限制(例如,128M),然后打开一个新文件并继续写。

磁盘上的语料库必然包含许多文件,每个文件包含语料库中的一个或多个文档,有时划分为子目录,这些子目录表示有意义的分组,如类别。语料库和文档信息也必须与对应文档保存在一起。磁盘上语料库按标准结构进行组织,对确保Python程序有效读取这些数据至关重要。

  • 无论文档是聚合成多文档文件,还是每篇文档保存成独立的文件,语料库表示需要组织起来的许多文件。如果随着时间推移,语料库摄取了新的数据,一种有意义的组织方式是按年、月、日组织子目录,文档分别放置在对应文件夹中。如果文档按情绪分类,无论是正面还是负面,每种类型的文档都可以组合在一起,放在自己的类别子目录下。如果系统中有多个用户,生成了他们自己的特定写作子集,例如评论或推文,那么每个用户都可以有自己的子目录。所有子目录需要并存在一个语料库根目录下。还有一点很重要,诸如许可证、清单、自述文件(README)或引用之类的语料库元信息,也必须和文档一起保存,这样语料库看起来才是一个独立的整体。

Baleen文件结构

  • 磁盘上组织方式的选择对CorpusReader对象读取文档的方式有很大影响,我们将在下一节介绍。Baleen语料库摄取引擎将HTML语料库写入磁盘,如下所示:
    第2章 构建自定义语料库_第3张图片
  • 这里有几点需要注意。首先,所有文档都存为HTML文件,根据其MD5哈希值进行命名(为了防止重复),可以简单识别哪些文件是文档,哪些文件是元文件。元信息方面,citation.bib文件提供语料库的属性,LICENSE.md文件指定其他人使用此语料库必需的权限。虽然这两条信息通常是为公共语料库保留的,但将它们包含在内,以便明确如何使用语料库是有帮助的,这与此类信息添加到私有软件仓库是一个道理。feeds.json和manifest.json是两个特定于语料库的文件,分别用于标识有关类别和每个特定文件的信息,最后,README.md文件是语料库的自然语言描述。
  • 在这些文件里,citation.bib,LICENSE.md和README.md是特殊文件,可以用citation()方法、license()方法和readme()方法在NLTK CorpusReader对象中自动读取。
  • 语料库管理和存储的结构化方法,意味着应用文本分析遵循科学的可重复性过程,这中方法鼓励分析的可解释性,增强分析结果的可信度。此外,按这种方式构建语料库,我们能用CorpusReader对象方便地进行访问,在下一节我们会详细解释。
  • 修改这些方法用于处理Markdown或读取类似清单的语料库特定文件非常简单:
import json
    # In a custom corpus reader class
    def manifest(self):
        """
        读取并剖析语料库中的mainfest.json文件(如果存在)
        """
        return json.load(self.open('README.md'))
  • 这些方法以源代码方式开放,允许语料库保持压缩的同时,数据仍然可读,从而最大限度减少磁盘上所需的存储量。考虑到README.md文件对于交代语料库的构成必不可少,不仅是对语料库的其他用户或开发者,也对于“未来的自己”,有可能是不记得具体细节、没法确定哪些模型在哪个语料库上进行了训练,以及这些模型还具有哪些信息。

语料库读取器

  • 一旦在磁盘上构建、组织起语料库,就为两件事打好了基础:在编程环境中用系统方法访问语料库,以及对语料库变化的监控和管理。我们将在本章末尾讨论后者,现在我们来讨论如何加载文档用于后续分析。
  • 大多数有用的语料库包含了数千个文档,包含文本数据的总量可能会有几GB。从文档加载的原始文本字符串,需要进行预处理,并将其解析为适合于分析的表示,这是个用来生成或复制数据的附加过程,增加了处理所需内存量。从计算角度来看,这是个重要的考虑因素,因为如果没有从磁盘流式传输和选择文档的合适方法,文本分析很快就会受到单个机器性能的限制,进而限制我们生成有趣模型的能力。幸运的是,NLTK库已经充分考虑了这一需求,并提供了用于从磁盘流式访问语料库的工具,可以通过CorpusReader对象在Python程序中打开语料库。

Hadoop等分布式计算框架的创建,是为了响应搜索引擎网络爬虫产生的文本量(Hadoop受到两篇Google论文的启发,是Nutch搜索引擎的后续项目)。我们将讨论集群计算技术,以便在第11章使用Spark(Hadoop的分布式计算继任者)进行扩展。

  • CorpusReader是一个编程接口,用于读取、定位、流式传输和过滤文档,还可以将数据处理部分,如编程和预处理,开放给需要访问语料库数据的代码。CorpusReader的初始化,需要传入包含语料库文件的目录根路径、用于发现文档名称的签名以及文件编码(默认情况下为UTF-8)。
  • 因为语料库包含超过分析用途的文件(例如,自述文件、引用、许可等),所以必须向读者提供相应的机制,以准确识别哪些文档是语料库的一部分。此机制是一个参数,可以显式地用名称列表指定,也可以隐式地用正则表达式指定,它将匹配根目录下的所有文档(例如,\w +.txt),匹配扩展名.txt前面文件名式是一个或多个字符或数字的文件。例如,在以下目录中,此正则表达式模式将匹配三个语音文件和脚本,但不匹配许可证、README或元数据文件:
    第2章 构建自定义语料库_第4张图片
  • 然后,这三个简单参数使CorpusReader 能够列出语料库中所有文件的绝对路径,使用正确的编码打开每个文档,并允许程序员单独访问元数据。
  • 在默认情况下,NLTK CorpusReader对象甚至可以访问压缩为Zip文件的语料库,简单扩展也允许读取Gzip或Bzip压缩文件。
  • 就其本身而言,CorpusReader的概念可能看起来并不显眼,但在处理大量文档时,该界面允许程序员将一个或多个文档读入内存,向前或向后定位语料库中的特定位置、而无需打开或读取不必要的文档,将数据流式传输到一次只在内存中保存一篇文档的分析过程,一次过滤或仅从语料库中选择特定文档。这些技术使得语料库的内存文本分析成为可能,因为它们每次只能处理载入内存中的少量文档。
  • 因此,为了在特定域中分析你自己的文本语料库,该语料库完全针对你尝试构建的模型,你将需要一个特定于应用程序的语料库阅读器。这对于一个应用文本分析至关重要,所以我们将本章的大部分内容聚焦于此!在本节中,我们将讨论NLTK附带的语料库读取器以及构建语料库的可能性,以便你可以简单地选用其一。最后,我们将讨论定义执行特定于应用程序工作的自定义语料库读取器,即处理在摄取过程中手机的HTML文件。

NLTK流式访问

  • NLTK附带了各种语料库读取器,专门用于访问NLTK能下载到的文本语料库和词汇资源。它还附带了更通用的工具CorpusReader对象,在语料库结构方面要求相当严格,但用得好可以快速创建语料库,并与读取器直接绑定。NLTK还提供了数据应用程序要求定制CorpusReader的技巧。这里列举几个值得关注的工具读取器:
  • PlaintextCorpusReader
    — 纯文本文档语料库读取器,假定语料库文本段落用空行隔开。
  • TaggedCorpusReader
    — 简单词性标记语料库读取器,句子在单独的一行,词条用对应的词性标记分隔。
  • BrackedCorpusReader
    — 用括号格式化的分块(可带标签)语料库读取器
  • TwitterCorpusReader
    — 推文语料库读取器,数据为序列化的行分隔JSON格式
  • WordlistCorpusReader
    — 词列表读取器,每行一个,空行会被忽略
  • XMLCorpusReader
    — XML文档语料库读取器
  • CategorizedCorpusReader
    — 分裂混合文档词料库读取器
  • 带标签的、带括号解析的,以及分块语料库读取器是标记过语料库读取器;如果要在机器学习之前进行特定领域的手工标记,那么向读取器开放的格式对于理解来说还是很重要的。Twitter、XML和纯文本语料库读取都提供了关于如何处理具有不同可解析格式磁盘数据的提示,允许扩展到CSV语料库、JSON甚至数据库等其他数据源。如果你的语料库已经采用其中某一种格式,几乎无需做任何其他额外工作。例如,星球大战和星际迷航电影的纯文本剧本语料库格式如下:
    第2章 构建自定义语料库_第5张图片
    第2章 构建自定义语料库_第6张图片
  • CategorizedPlaintextCorpusReader非常适合从电影脚本访问数据,因为文档是.txt文件,而且有两个分类:“星球大战(Star Wars)”和“星际迷航(Star Trek)”。为了使用CategorizedPlaintextCorpusReader,我们需要指定一个正则表达式让读者自动区分文件名和分类名:
from nltk.corpus.reader.plaintext import CategorizedPlaintextCorpusReader
DOC_PATTERN = r'(?!\.)[\w_\s] + /[\w\s\d\-] + \.txt'
CAT_PATTERN = r'([\w_\s]+)/.*'
corpus = CategorizedPlaintextCorpusReader(
    '/path/to/corpus/root',DOC_PATTERN,cat_pattern=CAT_PATTERN
)
  • 文档模式正则表达式规定了文档是语料库根目录下,且名字里有一个或多个字母、数字、空格或下划线,接着式“/”字符,然后是一个或多个字母、数字、空格或“-”,最后是“.txt”。这将会匹配名字形如“Star Wars/Star Wars Episode 1.txt”的文档,但不会匹配文件名为“episode.txt”的文档。类别模式正则表达式用捕获组截断原始正则表达式,规定类别是所有目录名称(例如,Star War/anything.txt将捕获Star Wars作为类别)。试试看这些名称是如何捕获的,开始对磁盘数据进行访问:
    第2章 构建自定义语料库_第7张图片
  • 正则表达式可能有点难,但它们提供了一种强大的机制,能准确指定语料库读取器应该加载什么,以及如何进行加载。你也可以显式地传递类别和文件组列表,但这会大大降低语料库读取器的灵活性。使用正则表达式,可以通过语料库中创建目录来添加新类别,将新文档移动到正确目录来添加新文档。
  • 我们了解了NLTK附带的CorpusReader对象如何使用,接下来看看摄取的HTML数据如何进行流水式传输。

你可能感兴趣的:(利用Python进行数据分析,python文本分析,机器学习,人工智能,python)