# 11 语言学数据管理 已标注的语言数据的结构化集合在 NLP 的大部分领域都是至关重要的,但是,我们使用它们仍然面临着许多障碍。本章的目的是要回答下列问题: 1. 我们如何设计一种新的语言资源,并确保它的覆盖面、平衡以及支持广泛用途的文档? 2. 现有数据对某些分析工具格式不兼容,我们如何才能将其转换成合适的格式? 3. 有什么好的方法来记录我们已经创建的资源的存在,让其他人可以很容易地找到它? 一路上,我们将研究当前语料库的设计、创建一个语料库的典型工作流程,及语料库的生命周期。与在其他章节中一样,会有语言数据管理实际实验的很多例子,包括在语言学现场教学课程、实验室的工作和网络爬取中收集的数据。 ## 1 语料库结构:一个案例研究 TIMIT 语料库是第一个广泛发布的已标注语音数据库,它有一个特别清晰的组织结构。TIMIT 由一个包括克萨斯仪器公司和麻省理工学院的财团开发,它也由此得名。它被设计用来为声学-语音知识的获取提供数据,并支持自动语音识别系统的开发和评估。 ## 1.1 TIMIT 的结构 与布朗语料库显示文章风格和来源的平衡选集一样,TIMIT 包括方言、说话者和材料的平衡选集。对 8 个方言区中的每一种方言,具有一定年龄范围和教育背景的 50 个男性和女性的说话者每人读 10 个精心挑选的句子。设计中有两句话是所有说话者都读的,带来方言的变化: ```py >>> phonetic = nltk.corpus.timit.phones('dr1-fvmh0/sa1') >>> phonetic ['h#', 'sh', 'iy', 'hv', 'ae', 'dcl', 'y', 'ix', 'dcl', 'd', 'aa', 'kcl', 's', 'ux', 'tcl', 'en', 'gcl', 'g', 'r', 'iy', 's', 'iy', 'w', 'aa', 'sh', 'epi', 'w', 'aa', 'dx', 'ax', 'q', 'ao', 'l', 'y', 'ih', 'ax', 'h#'] >>> nltk.corpus.timit.word_times('dr1-fvmh0/sa1') [('she', 7812, 10610), ('had', 10610, 14496), ('your', 14496, 15791), ('dark', 15791, 20720), ('suit', 20720, 25647), ('in', 25647, 26906), ('greasy', 26906, 32668), ('wash', 32668, 37890), ('water', 38531, 42417), ('all', 43091, 46052), ('year', 46052, 50522)] ``` 除了这种文本数据,TIMIT 还包括一个词典,提供每一个词的可与一个特定的话语比较的规范的发音: ```py >>> timitdict = nltk.corpus.timit.transcription_dict() >>> timitdict['greasy'] + timitdict['wash'] + timitdict['water'] ['g', 'r', 'iy1', 's', 'iy', 'w', 'ao1', 'sh', 'w', 'ao1', 't', 'axr'] >>> phonetic[17:30] ['g', 'r', 'iy', 's', 'iy', 'w', 'aa', 'sh', 'epi', 'w', 'aa', 'dx', 'ax'] ``` 这给了我们一点印象:语音处理系统在处理或识别这种特殊的方言(新英格兰)的语音中必须做什么。最后,TIMIT 包括说话人的人口学统计,允许细粒度的研究声音、社会和性别特征。 ```py >>> nltk.corpus.timit.spkrinfo('dr1-fvmh0') SpeakerInfo(id='VMH0', sex='F', dr='1', use='TRN', recdate='03/11/86', birthdate='01/08/60', ht='5\'05"', race='WHT', edu='BS', comments='BEST NEW ENGLAND ACCENT SO FAR') ``` ## 1.2 主要设计特点 TIMIT 演示了语料库设计中的几个主要特点。首先,语料库包含语音和字形两个标注层。一般情况下,文字或语音语料库可能在多个不同的语言学层次标注,包括形态、句法和段落层次。此外,即使在给定的层次仍然有不同的标注策略,甚至标注者之间也会有分歧,因此我们要表示多个版本。TIMIT 的第二个特点是:它在多个维度的变化与方言地区和二元音覆盖范围之间取得平衡。人口学统计的加入带来了许多更独立的变量,这可能有助于解释数据中的变化,便于以后出于在建立语料库时没有想到的目的使用语料库,例如社会语言学。第三个特点是:将原始语言学事件作为录音来捕捉和作为标注来捕捉之间有明显的区分。两者一致表示文本语料库正确,原始文本通常有被认为是不可改变的作品的外部来源。那个作品的任何包含人的判断的转换——即使如分词一样简单——也是后来的修订版,因此以尽可能接近原始的形式保留源材料是十分重要的。 ![Images/timit-structure.png](Images/953f4a408c97594449de5ca84c294719.jpg) 图 1.2:发布的 TIMIT 语料库的结构:CD-ROM 包含文档、顶层的训练和测试目录;训练和测试目录都有 8 子目录,每个方言区一个;这些目录又包含更多子目录,每个说话者一个;列出的目录是女性说话者`aks0`的目录的内容,显示 10 个`wav`文件配以一个录音文本文件、一个录音文本词对齐文件和一个音标文件。 TIMIT 的第四个特点是语料库的层次结构。每个句子 4 个文件,500 个说话者每人 10 个句子,有 20,000 个文件。这些被组织成一个树状结构,示意图如 1.2 所示。在顶层分成训练集和测试集,用于开发和评估统计模型。 最后,请注意虽然 TIMIT 是语音语料库,它的录音文本和相关数据只是文本,可以使用程序处理了,就像任何其他的文本语料库那样。因此,许多在这本书中所描述的计算方法都适用。此外,注意 TIMIT 语料库包含的所有数据类型分为词汇和文字两个基本类别,我们将在下面讨论。说话者人口学统计数据只不过是词汇数据类型的另一个实例。 当我们考虑到文字和记录结构是计算机科学中关注数据管理的两个子领域首要内容,即全文检索领域和数据库领域,这最后的观察就不太令人惊讶了。语言数据管理的一个显着特点是往往将这两种数据类型放在一起,可以利用这两个领域的成果和技术。 ## 1.3 基本数据类型 ![Images/datatypes.png](Images/13361de430cd983e689417c547330bbc.jpg) 图 1.3:基本语言数据类型——词汇和文本:它们的多样性中,词汇具有记录结构,而已标注文本具有时间组织。 不考虑它的复杂性,TIMIT 语料库只包含两种基本数据类型,词典和文本。正如我们在 2 中所看到的,大多数词典资源都可以使用记录结构表示,即一个关键字加一个或多个字段,如 1.3 所示。词典资源可能是一个传统字典或比较词表,如下所示。它也可以是一个短语词典,其中的关键字是一个短语而不是一个词。词典还包括记录结构化的数据,我们可以通过对应主题的非关键字字段来查找条目。我们也可以构造特殊的表格(称为范例)来进行对比和说明系统性的变化,1.3 显示了三个动词。TIMIT 的说话者表也是一种词典资源。 在最抽象的层面上,文本是一个真实的或虚构的讲话事件的表示,该事件的时间过程也在文本本身存在。一个文本可以是一个小单位,如一个词或句子,也可以是一个完整的叙述或对话。它可能会有标注如词性标记、形态分析、话语结构等。正如我们在 IOB 标注`(7)`中所看到的可以使用单个词的标记表示更高层次的成分。因此,1.3 所示的文本的抽象就足够了。 不考虑单独的语料库的复杂性和特质,最基本的,它们是带有记录结构化数据的文本集合。语料库的内容往往偏重于这些类型中的一种或多种。例如:布朗语料库包含 500 个文本文件,但我们仍然可以使用表将这些文件与 15 种不同风格关联。在事情的另一面,WordNet 包含 117659 个同义词集记录,也包含许多例子句子(小文本)来说明词的用法。TIMIT 处在中间,含有大量的独立的文本和词汇类型的材料。 ## 2 语料库生命周期 语料库并不是从天而降的,需要精心的准备和许多人长时期的输入。原始数据需要进行收集、清理、记录并以系统化的结构存储。标注可分为各种层次,一些需要语言的形态或句法的专门知识。要在这个阶段成功取决于建立一个高效的工作流程,包括适当的工具和格式转换器。质量控制程序可以将寻找标注中的不一致落实到位,确保尽最大可能在标注者之间达成一致。由于任务的规模和复杂性,大型语料库可能需要几年的准备,包括几十或上百人多年的努力。在本节中,我们简要地回顾语料库生命周期的各个阶段。 ## 2.1 语料库创建的三种方案 语料库的一种类型是设计在创作者的探索过程中逐步展现。这是典型的传统“领域语言学”模式,即来自会话的材料在它被收集的时候就被分析,明天的想法往往基于今天的分析中产生的问题。。在随后几年的研究中产生的语料不断被使用,并可能用作不确定的档案资源。计算机化明显有利于这种类型的工作,以广受欢迎的程序 Shoebox 为例,它作为 Toolbox 重新发布,现在已有超过二十年的历史(见 4)。其他的软件工具,甚至是简单的文字处理器和电子表格,通常也可用于采集数据。在下一节,我们将着眼于如何从这些来源提取数据。 另一种语料库创建方案是典型的实验研究,其中一些精心设计的材料被从一定范围的人类受试者中收集,然后进行分析来评估一个假设或开发一种技术。此类数据库在实验室或公司内被共享和重用已很常见,经常被更广泛的发布。这种类型的语料库是“共同任务”的科研管理方法的基础,这在过去的二十年已成为政府资助的语言技术研究项目。在前面的章节中,我们已经遇到很多这样的语料库;我们将看到如何编写 Python 程序实践这些语料库发布前必要的一些任务。 最后,还有努力为一个特定的语言收集“参考语料”,如*美国国家语料库*(ANC)和*英国国家语料库*(BNC)。这里的目标已经成为产生各种形式、风格和语言的使用的一个全面的记录。除了规模庞大的挑战,还严重依赖自动标注工具和后期编辑共同修复错误。然而,我们可以编写程序来查找和修复错误,还可以分析语料库是否平衡。 ## 2.2 质量控制 自动和手动的数据准备的好的工具是必不可少的。然而,一个高质量的语料库的建立很大程度取决于文档、培训和工作流程等平凡的东西。标注指南确定任务并记录标记约定。它们可能会定期更新以覆盖不同的情况,同时制定实现更一致的标注的新规则。在此过程中标注者需要接受训练,包括指南中没有的情况的解决方法。需要建立工作流程,尽可能与支持软件一起,跟踪哪些文件已被初始化、标注、验证、手动检查等等。可能有多层标注,由不同的专家提供。不确定或不一致的情况可能需要裁决。 大的标注任务需要多个标注者,由此产生一致性的问题。一组标注者如何能一致的处理呢?我们可以通过将一部分独立的原始材料由两个人分别标注,很容易地测量标注的一致性。这可以揭示指南中或标注任务的不同功能的不足。在对质量要求较高的情况下,整个语料库可以标注两次,由专家裁决不一致的地方。 报告标注者之间对语料库达成的一致性被认为是最佳实践(如通过两次标注 10% 的语料库)。这个分数作为一个有用的在此语料库上训练的所有自动化系统的期望性能的上限。 小心! 应谨慎解释标注者之间一致性得分,因为标注任务的难度差异巨大。例如,90% 的一致性得分对于词性标注是可怕的得分,但对语义角色标注是可以预期的得分。 Kappa 系数`K`测量两个人判断类别和修正预期的期望一致性的一致性。例如,假设要标注一个项目,四种编码选项可能性相同。这种情况下,两个人随机编码预计有 25% 可能达成一致。因此,25% 一致性将表示为`k = 0`,相应的较好水平的一致性将依比例决定。对于一个 50% 的一致性,我们将得到`k = 0.333`,因为 50 是从 25 到 100 之间距离的三分之一。还有许多其他一致性测量方法;详情请参阅`help(nltk.metrics.agreement)`。 ![Images/windowdiff.png](Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg) 图 2.1:一个序列的三种分割:小矩形代表字、词、句,总之,任何可能被分为语言单位的序列;`S[1]`和`S[2]`是接近一致的,两者都与`S[3]`显著不同。 我们还可以测量语言输入的两个独立分割的一致性,例如分词、句子分割、命名实体识别。在 2.1 中,我们看到三种可能的由标注者(或程序)产生的项目序列的分割。虽然没有一个完全一致,`S[1]`和`S[2]`是接近一致的,我们想要一个合适的测量。Windowdiff 是评估两个分割一致性的一个简单的算法,通过在数据上移动一个滑动窗口计算近似差错的部分得分。如果我们将词符预处理成 0 和 1 的序列,当词符后面跟着边界符号时记录下来,我们就可以用字符串表示分割,应用 windowdiff 打分器。 ```py >>> s1 = "00000010000000001000000" >>> s2 = "00000001000000010000000" >>> s3 = "00010000000000000001000" >>> nltk.windowdiff(s1, s1, 3) 0.0 >>> nltk.windowdiff(s1, s2, 3) 0.190... >>> nltk.windowdiff(s2, s3, 3) 0.571... ``` 上面的例子中,窗口大小为 3。Windowdiff 计算在一对字符串上滑动这个窗口。在每个位置它计算两个字符串在这个窗口内的边界的总数,然后计算差异。最后累加这些差异。我们可以增加或缩小窗口的大小来控制测量的敏感度。 ## 2.3 维护与演变 随着大型语料库的发布,研究人员立足于均衡的从为完全不同的目的而创建的语料库中派生出的子集进行调查的可能性越来越大。例如,Switchboard 数据库,最初是为识别说话人的研究而收集的,已被用作语音识别、单词发音、口吃、句法、语调和段落结构研究的基础。重用语言语料库的动机包括希望节省时间和精力,希望在别人可以复制的材料上工作,有时希望研究语言行为的更加自然的形式。为这样的研究选择子集的过程本身可视为一个不平凡的贡献。 除了选择语料库的适当的子集,这个新的工作可能包括重新格式化文本文件(如转换为 XML),重命名文件,重新为文本分词,选择数据的一个子集来充实等等。多个研究小组可以独立的做这项工作,如 2.2 所示。在以后的日子,应该有人想要组合不同的版本的源数据,这项任务可能会非常繁重。 ![Images/evolution.png](Images/e33fb540f11c5ea9a07441be8a407d43.jpg) 图 2.2:语料库随着时间的推移而演变:语料库发布后,研究小组将独立的使用它,选择和丰富不同的部分;然后研究努力整合单独的标注,面临校准注释的艰巨的挑战。 由于缺乏有关派生的版本如何创建的,哪个版本才是最新的等记录,使用派生的语料库的任务变得更加困难。 这种混乱情况的改进方法是集中维护语料库,专家委员会定期修订和扩充它,考虑第三方的意见,不时发布的新版本。出版字典和国家语料库可能以这种方式集中维护。然而,对于大多数的语料库,这种模式是完全不切实际的。 原始语料库的出版的一个中间过程是要有一个能识别其中任何一部分的规范。每个句子、树、或词条都有一个全局的唯一标识符,每个词符、节点或字段(分别)都有一个相对偏移。标注,包括分割,可以使用规范的标识符(一个被称为对峙注释的方法)引用源材料。这样,新的标注可以与源材料独立分布,同一来源的多个独立标注可以对比和更新而不影响源材料。 如果语料库出版提供了多个版本,版本号或日期可以是识别规范的一部分。整个语料的版本标识符之间的对应表,将使任何对峙的注释更容易被更新。 小心! 有时一个更新的语料包含对一直在外部标注的基本材料的修正。词符可能会被分拆或合并,成分可能已被重新排列。新老标识符之间可能不会一一对应。使对峙标注打破新版本的这些组件比默默允许其标识符指向不正确的位置要好。 ## 3 数据采集 ## 3.1 从网上获取数据 网络是语言分析的一个丰富的数据源。我们已经讨论了访问单个文件,如 RSS 订阅、搜索引擎的结果(见 3.1)的方法。然而,在某些情况下,我们要获得大量的 Web 文本。 最简单的方法是获得出版的网页文本的文集。Web 语料库 ACL 特别兴趣组(SIGWAC)在`http://www.sigwac.org.uk/`维护一个资源列表。使用定义好的 Web 语料库的优点是它们有文档、稳定并允许重复性实验。 如果所需的内容在一个特定的网站,有许多实用程序能捕获网站的所有可访问内容,如 *GNU Wget* `http://www.gnu.org/software/wget/`。为了最大的灵活性和可控制,可以使用网络爬虫如 *Heritrix*,`http://crawler.archive.org/`(Croft, Metzler, & Strohman, 2009)。例如:如果我们要编译双语文本集合,对应两种语言的文档对,爬虫需要检测站点的结构以提取文件之间的对应关系,它需要按照捕获的对应方式组织下载的页面。写你自己的网页爬虫可能使很有诱惑力的,但也有很多陷阱需要克服,如检测 MIME 类型、转换相对地址为绝对 URL、避免被困在循环链接结构、处理网络延迟、避免使站点超载或被禁止访问该网站等。 ## 3.2 从字处理器文件获取数据 文字处理软件通常用来在具有有限的可计算基础设施的项目中手工编制文本和词汇。这些项目往往提供数据录入模板,通过字处理软件并不能保证数据结构正确。例如,每个文本可能需要有一个标题和日期。同样,每个词条可能有一些必须的字段。随着数据规模和复杂性的增长,用于维持其一致性的时间的比重也增大。 我们怎样才能提取这些文件的内容,使我们能够在外部程序中操作?此外,我们如何才能验证这些文件的内容,以帮助作者创造结构良好的数据,在原始的创作过程中最大限度提高数据的质量? 考虑一个字典,其中的每个条目都有一个词性字段,从一个 20 个可能值的集合选取,在发音字段显示,以 11 号黑体字呈现。传统的文字处理器没有能够验证所有的词性字段已正确输入和显示的搜索函数或宏。这个任务需要彻底的手动检查。如果字处理器允许保存文档为一种非专有的格式,如 text、HTML 或 XML,有时我们可以写程序自动做这个检查。 思考下面的一个词条的片段:`sleep [sli:p] v.i.condition of body and mind...`。我们可以在 MSWord 中输入这些词,然后“另存为网页”,然后检查生成的 HTML 文件: ```py
sleep
[sli:p]
v.i.
a condition of body and mind ...
2:
yield entry.split(' ', 3)
```
with gzip.open(fn+".gz","wb") as f_out:
f_out.write(bytes(s, 'UTF-8'))
注意
更多 HTML 复杂的处理可以使用`http://www.crummy.com/software/BeautifulSoup/`上的 *BeautifulSoup* 的包。
## 3.3 从电子表格和数据库中获取数据
电子表格通常用于获取词表或范式。例如,一个比较词表可以用电子表格创建,用一排表示每个同源组,每种语言一列(见`nltk.corpus.swadesh`和`www.rosettaproject.org`)。大多数电子表格软件可以将数据导出为 CSV 格式。正如我们将在下面看到的,使用`csv`模块 Python 程序可以很容易的访问它们。
有时词典存储在一个完全成熟的关系数据库。经过适当的标准化,这些数据库可以确保数据的有效性。例如,我们可以要求所有词性都来自指定的词汇,通过声明词性字段为*枚举类型*或用一个外键引用一个单独的词性表。然而,关系模型需要提前定义好的数据(模式)结构,这与高度探索性的构造语言数据的主导方法相违背。被认为是强制性的和独特的字段往往需要是可选的、可重复。只有当数据类型提前全都知道时关系数据库才是适用的,如果不是,或者几乎所有的属性都是可选的或重复的,关系的做法就行不通了。
然而,当我们的目标只是简单的从数据库中提取内容时,完全可以将表格(或 SQL 查询结果)转换成 CSV 格式,并加载到我们的程序中。我们的程序可能会执行不太容易用 SQL 表示的语言学目的的查询,如`select all words that appear in example sentences for which no dictionary entry is provided`。对于这个任务,我们需要从记录中提取足够的信息,使它连同词条和例句能被唯一的识别。让我们假设现在这个信息是在一个 CSV 文件`dict.csv`中:
```py
"sleep","sli:p","v.i","a condition of body and mind ..."
"walk","wo:k","v.intr","progress by lifting and setting down each foot ..."
"wake","weik","intrans","cease to sleep"
```
然后,这些信息将可以指导正在进行的工作来丰富词汇和更新关系数据库的内容。
## 3.4 转换数据格式
已标注语言数据很少以最方便的格式保存,往往需要进行各种格式转换。字符编码之间的转换已经讨论过(见 3.3)。在这里,我们专注于数据结构。
最简单的情况,输入和输出格式是同构的。例如,我们可能要将词汇数据从 Toolbox 格式转换为 XML,可以直接一次一个的转换词条`(4)`。数据结构反映在所需的程序的结构中:一个`for`循环,每次循环处理一个词条。
另一种常见的情况,输出是输入的摘要形式,如一个倒置的文件索引。有必要在内存中建立索引结构(见 4.8),然后把它以所需的格式写入一个文件。下面的例子构造一个索引,映射字典定义的词汇到相应的每个词条❶的语意❷,已经对定义文本分词❸,并丢弃短词❹。一旦该索引建成,我们打开一个文件,然后遍历索引项,以所需的格式输出行❺。
```py
>>> idx = nltk.Index((defn_word, lexeme) ❶
... for (lexeme, defn) in pairs ❷
... for defn_word in nltk.word_tokenize(defn) ❸
... if len(defn_word) > 3) ❹
>>> with open("dict.idx", "w") as idx_file:
... for word in sorted(idx):
... idx_words = ', '.join(idx[word])
... idx_line = "{}: {}".format(word, idx_words) ❺
... print(idx_line, file=idx_file)
```
由此产生的文件`dict.idx`包含下面的行。(如果有更大的字典,我们希望找到每个索引条目中列出的多个语意)。
```py
body: sleep
cease: wake
condition: sleep
down: walk
each: walk
foot: walk
lifting: walk
mind: sleep
progress: walk
setting: walk
sleep: wake
```
## 3.5 决定要包含的标注层
发布的语料库中所包含的信息的丰富性差别很大。语料库最低限度通常会包含至少一个声音或字形符号的序列。事情的另一面,一个语料库可以包含大量的信息,如句法结构、形态、韵律、每个句子的语义、加上段落关系或对话行为的标注。标注的这些额外的层可能正是有人执行一个特定的数据分析任务所需要的。例如,如果我们可以搜索特定的句法结构,找到一个给定的语言模式就更容易;如果每个词都标注了意义,为语言模式归类就更容易。这里提供一些常用的标注层:
* 分词:文本的书写形式不能明确地识别它的词符。分词和规范化的版本作为常规的正式版本的补充可能是一个非常方便的资源。
* 断句:正如我们在 3 中看到的,断句比它看上去的似乎更加困难。因此,一些语料库使用明确的标注来断句。
* 分段:段和其他结构元素(标题,章节等)可能会明确注明。
* 词性:文档中的每个单词的词类。
* 句法结构:一个树状结构,显示一个句子的组成结构。
* 浅层语义:命名实体和共指标注,语义角色标签。
* 对话与段落:对话行为标记,修辞结构
不幸的是,现有的语料库之间在如何表示标注上并没有多少一致性。然而,两个大类的标注表示应加以区别。内联标注通过插入带有标注信息的特殊符号或控制序列修改原始文档。例如,为文档标注词性时,字符串`"fly"`可能被替换为字符串`"fly/NN"`来表示词`fly`在文中是名词。相比之下,对峙标注不修改原始文档,而是创建一个新的文档,通过使用指针引用原始文档来增加标注信息。例如,这个新的文档可能包含字符串`"<token id=8 pos='NN'/>"`,表示 8 号词符是一个名词。(我们希望可以确保的分词本身不会变化,因为它会导致默默损坏这种引用。)
## 3.6 标准和工具
一个用途广泛的语料库需要支持广泛的格式。然而,NLP 研究的前沿需要各种新定义的没有得到广泛支持的标注。一般情况下,并没有广泛使用的适当的创作、发布和使用语言数据的工具。大多数项目都必须制定它们自己的一套工具,供内部使用,这对缺乏必要的资源的其他人没有任何帮助。此外,我们还没有一个可以胜任的普遍接受的标准来表示语料库的结构和内容。没有这样的标准,就不可能有通用的工具——同时,没有可用的工具,适当的标准也不太可能被开发、使用和接受。
针对这种情况的一个反应就是开拓未来开发一种通用的能充分表现捕获多种标注类型(见 8 的例子)的格式。NLP 的挑战是编写程序处理这种格式的泛化。例如,如果编程任务涉及树数据,文件格式允许任意有向图,那么必须验证输入数据检查树的属性如根、连通性、无环。如果输入文件包含其他层的标注,该程序将需要知道数据加载时如何忽略它们,将树数据保存到文件时不能否定或抹杀这些层。
另一种反应一直是写一个一次性的脚本来操纵语料格式;这样的脚本将许多 NLP 研究人员的文件夹弄得乱七八糟。在语料格式解析工作应该只进行一次(每编程语言)的前提下,NLTK 中的语料库阅读器是更系统的方法。
![Images/three-layer-arch.png](Images/102675fd70e434164536c75bf7f8f043.jpg)
图 3.2:通用格式对比通用接口
不是集中在一种共同的格式,我们认为更有希望开发一种共同的接口(参见`nltk.corpus`)。思考 NLP 中的一个重要的语料类型 treebanks 的情况。将短语结构树存储在一个文件中的方法很多。我们可以使用嵌套的括号、或嵌套的 XML 元素、或每行带有一个`(child-id,parent-id)`对的依赖符号、或一个 XML 版本的依赖符号等。然而,每种情况中的逻辑结构几乎是相同的。很容易设计一种共同的接口,使应用程序员编写代码使用如`children()`、`leaves()`、`depth()`等方法来访问树数据。注意这种做法来自计算机科学中已经接受的做法,即即抽象数据类型、面向对象设计、三层结构`(3.2)`。其中的最后一个——来自关系数据库领域——允许终端用户应用程序使用通用的模型(“关系模型”)和通用的语言(SQL)抽象出文件存储的特质,并允许新的文件系统技术的出现,而不会干扰到终端用户的应用。以同样的方式,一个通用的语料库接口将应用程序从数据格式隔离。
在此背景下,创建和发布一个新的语料库时,尽可能使用现有广泛使用的格式是权宜之计。如果这样不可能,语料库可以带有一些软件——如`nltk.corpus`模块——支持现有的接口方法。
## 3.7 处理濒危语言时特别注意事项
语言对科学和艺术的重要性体现在文化宝库包含在语言中。世界上大约 7000 种人类语言中的每一个都是丰富的,在它独特的方面,在它口述的历史和创造的传说,在它的文法结构和它的变化的词汇和它们含义中的细微差别。受威胁残余文化中的词能够区分具有科学家未知的治疗用途的植物亚种。当人们互相接触,每个人都为之前的语言提供一个独特的窗口,语言随着时间的推移而变化。世界许多地方,小的语言变化从一个镇都另一个镇,累加起来在一个半小时的车程的空间中成为一种完全不同的语言。对于其惊人的复杂性和多样性,人类语言犹如丰富多彩的挂毯随着时间和空间而伸展。
然而,世界上大多数语言面临灭绝。对此,许多语言学家都在努力工作,记录语言,构建这个世界语言遗产的重要方面的丰富记录。在 NLP 的领域能为这方面的努力提供什么帮助吗?开发标注器、分析器、命名实体识别等不是最优先的,通常没有足够的数据来开发这样的工具。相反,最经常提出的是需要更好的工具来收集和维护数据,特别是文本和词汇。
从表面看,开始收集濒危语言的文本应该是一件简单的事情。即使我们忽略了棘手的问题,如谁拥有文本,文本中包含的文化知识有关敏感性,转录仍然有很多明显的实际问题。大多数语言缺乏标准的书写形式。当一种语言没有文学传统时,拼写和标点符号的约定也没有得到很好的建立。因此,通常的做法是与文本收集一道创建一个词典,当在文本中出现新词时不断更新词典。可以使用文字处理器(用于文本)和电子表格(用于词典)来做这项工作。更妙的是,SIL 的自由语言软件 Toolbox 和 Fieldworks 对文本和词汇的创建集成提供了很好的支持。
当濒危语言的说话者学会自己输入文本时,一个共同的障碍就是对正确的拼写的极度关注。有一个词典大大有助于这一进程,但我们需要让查找的方法不要假设有人能确定任意一个词的引文形式。这个问题对具有复杂形态的包括前缀的语言可能是很急迫的。这种情况下,使用语义范畴标注词项,并允许通过语义范畴或注释查找是十分有益的。
允许通过相似的发音查找词项也是很有益的。下面是如何做到这一点的一个简单的演示。第一步是确定易混淆的字母序列,映射复杂的版本到更简单的版本。我们还可以注意到,辅音群中字母的相对顺序是拼写错误的一个来源,所以我们将辅音字母顺序规范化。
```py
>>> mappings = [('ph', 'f'), ('ght', 't'), ('^kn', 'n'), ('qu', 'kw'),
... ('[aeiou]+', 'a'), (r'(.)\1', r'\1')]
>>> def signature(word):
... for patt, repl in mappings:
... word = re.sub(patt, repl, word)
... pieces = re.findall('[^aeiou]+', word)
... return ''.join(char for piece in pieces for char in sorted(piece))[:8]
>>> signature('illefent')
'lfnt'
>>> signature('ebsekwieous')
'bskws'
>>> signature('nuculerr')
'nclr'
```
下一步,我们对词典中的所有词汇创建从特征到词汇的映射。我们可以用这为一个给定的输入词找到候选的修正(但我们必须先计算这个词的特征)。
```py
>>> signatures = nltk.Index((signature(w), w) for w in nltk.corpus.words.words())
>>> signatures[signature('nuculerr')]
['anicular', 'inocular', 'nucellar', 'nuclear', 'unicolor', 'uniocular', 'unocular']
```
最后,我们应该按照与原词相似程度对结果排序。通过函数`rank()`完成。唯一剩下的函数提供给用户一个简单的接口:
```py
>>> def rank(word, wordlist):
... ranked = sorted((nltk.edit_distance(word, w), w) for w in wordlist)
... return [word for (_, word) in ranked]
>>> def fuzzy_spell(word):
... sig = signature(word)
... if sig in signatures:
... return rank(word, signatures[sig])
... else:
... return []
>>> fuzzy_spell('illefent')
['olefiant', 'elephant', 'oliphant', 'elephanta']
>>> fuzzy_spell('ebsekwieous')
['obsequious']
>>> fuzzy_spell('nucular')
['anicular', 'inocular', 'nucellar', 'nuclear', 'unocular', 'uniocular', 'unicolor']
```
这仅仅是一个演示,其中一个简单的程序就可以方便的访问语言书写系统可能不规范或语言的使用者可能拼写的不是很好的上下文中的词汇数据。其他简单的 NLP 在这个领域的应用包括:建立索引以方便对数据的访问,从文本中拾取词汇表,构建词典时定位词语用法的例子,在知之甚少的数据中检测普遍或特殊模式,并在创建的数据上使用各种语言的软件工具执行专门的验证。我们将在 5 返回到其中的最后一个。
## 4 使用 XML
可扩展标记语言(XML)为设计特定领域的标记语言提供了一个框架。它有时被用于表示已标注的文本和词汇资源。不同于 HTML 的标签是预定义的,XML 允许我们组建自己的标签。不同于数据库,XML 允许我们创建的数据而不必事先指定其结构,它允许我们有可选的、可重复的元素。在本节中,我们简要回顾一下 XML 的一些与表示语言数据有关的特征,并说明如何使用 Python 程序访问 XML 文件中存储的数据。
## 4.1 语言结构中使用 XML
由于其灵活性和可扩展性,XML 是表示语言结构的自然选择。下面是一个简单的词汇条目的例子。
```py
`,`
`(表格的行)和` `(表格数据)。
```py
>>> html = " \n"
>>> for entry in lexicon[70:80]:
... lx = entry.findtext('lx')
... ps = entry.findtext('ps')
... ge = entry.findtext('ge')
... html += "
"
>>> print(html)
\n" % (lx, ps, ge)
>>> html += "%s %s %s
```
## 5 使用 Toolbox 数据
鉴于 Toolbox 在语言学家中十分流行,我们将讨论一些使用 Toolbox 数据的进一步的方法。很多在前面的章节讲过的方法,如计数、建立频率分布、为同现制表,这些都可以应用到 Toolbox 条目的内容上。例如,我们可以为每个条目计算字段的平均个数:
```py
>>> from nltk.corpus import toolbox
>>> lexicon = toolbox.xml('rotokas.dic')
>>> sum(len(entry) for entry in lexicon) / len(lexicon)
13.635...
```
在本节中我们将讨论记录语言学的背景下出现的都不被 Toolbox 软件支持的两个任务。
## 5.1 为每个条目添加一个字段
添加一个自动从现有字段派生出的新的字段往往是方便的。这些字段经常使搜索和分析更加便捷。例如,在 5.1 中我们定义了一个函数`cv()`,将辅音和元音的字符串映射到相应的 CV 序列,即`kakapua`将映射到`CVCVCVV`。这种映射有四个步骤。首先,将字符串转换为小写,然后将所有非字母字符`[^a-z]`用下划线代替。下一步,将所有元音替换为`V`。最后,所有不是`V`或下划线的必定是一个辅音,所以我们将它替换为`C`。现在,我们可以扫描词汇,在每个`lx`字段后面添加一个新的`cv`字段。5.1 显示了它对一个特定条目上做的内容;注意输出的最后一行表示新的`cv`字段。
```py
from xml.etree.ElementTree import SubElement
def cv(s):
s = s.lower()
s = re.sub(r'[^a-z]', r'_', s)
s = re.sub(r'[aeiou]', r'V', s)
s = re.sub(r'[^V_]', r'C', s)
return (s)
def add_cv_field(entry):
for field in entry:
if field.tag == 'lx':
cv_field = SubElement(entry, 'cv')
cv_field.text = cv(field.text)
```
注意
如果一个 Toolbox 文件正在不断更新,`code-add-cv-field`中的程序将需要多次运行。可以修改`add_cv_field()`来修改现有条目的内容。使用这样的程序为数据分析创建一个附加的文件比替换手工维护的源文件要安全。
## 5.2 验证 Toolbox 词汇
Toolbox 格式的许多词汇不符合任何特定的模式。有些条目可能包括额外的字段,或以一种新的方式排序现有字段。手动检查成千上万的词汇条目是不可行的。我们可以在`Counter`的帮助下很容易地找出频率异常的字段序列:
```py
>>> from collections import Counter
>>> field_sequences = Counter(':'.join(field.tag for field in entry) for entry in lexicon)
>>> field_sequences.most_common()
[('lx:ps:pt:ge:tkp:dt:ex:xp:xe', 41), ('lx:rt:ps:pt:ge:tkp:dt:ex:xp:xe', 37),
('lx:rt:ps:pt:ge:tkp:dt:ex:xp:xe:ex:xp:xe', 27), ('lx:ps:pt:ge:tkp:nt:dt:ex:xp:xe', 20), ...]
```
检查完高频字段序列后,我们可以设计一个词汇条目的上下文无关语法。在 5.2 中的语法使用我们在 8 看到的 CFG 格式。这样的语法模型隐含 Toolbox 条目的嵌套结构,建立一个树状结构,树的叶子是单独的字段名。最后,我们遍历条目并报告它们与语法的一致性,如 5.2 所示。那些被语法接受的在前面加一个`'+'`❶,那些被语法拒绝的在前面加一个`'-'`❷。在开发这样一个文法的过程中,它可以帮助过滤掉一些标签❸。
```py
grammar = nltk.CFG.fromstring('''
S -> Head PS Glosses Comment Date Sem_Field Examples
Head -> Lexeme Root
Lexeme -> "lx"
Root -> "rt" |
PS -> "ps"
Glosses -> Gloss Glosses |
Gloss -> "ge" | "tkp" | "eng"
Date -> "dt"
Sem_Field -> "sf"
Examples -> Example Ex_Pidgin Ex_English Examples |
Example -> "ex"
Ex_Pidgin -> "xp"
Ex_English -> "xe"
Comment -> "cmt" | "nt" |
''')
def validate_lexicon(grammar, lexicon, ignored_tags):
rd_parser = nltk.RecursiveDescentParser(grammar)
for entry in lexicon:
marker_list = [field.tag for field in entry if field.tag not in ignored_tags]
if list(rd_parser.parse(marker_list)):
print("+", ':'.join(marker_list)) ❶
else:
print("-", ':'.join(marker_list)) ❷
```
另一种方法是用一个词块分析器`(7)`,因为它能识别局部结构并报告已确定的局部结构,会更加有效。在 5.3 中我们为词汇条目建立一个词块语法,然后解析每个条目。这个程序的输出的一个示例如 5.4 所示。
```py
grammar = r"""
lexfunc: {kakae ??? small kakae CLASS child kakaevira ADV small-like kakapikoa ??? small kakapikoto N newborn baby kakapu V place in sling for purpose of carrying kakapua N sling for lifting kakara N arm band Kakarapaia N village name kakarau N frog