liyongsea / parallel_corpus_mnbvc

parallel corpus dataset from the mnbvc project
Apache License 2.0
11 stars 6 forks source link

联合国6国语料的整条管线接入,爬虫重做 #62

Open voidf opened 8 months ago

voidf commented 8 months ago

【不要合并!】这个PR现在只用于论文草稿和一些统计脚本留档

voidf commented 8 months ago

Unified Pipeline for Reproducing Parallel Resources: Corpus from the United Nations (UPRPRC)

0. Abstract

在数字时代,多语言数据集的保真度和可访问性对于推进机器翻译 (MT) 和自然语言处理 (NLP) 至关重要。本文介绍了联合国平行语料库,这是一个复杂的迭代,它不仅扩展了数据的广度,还包含了将原始联合国数据转换为结构化平行语料库所需的整个流程。该语料库由来自联合国数字图书馆 (UNDL) 的文档组成,时间跨度从 2000 年到 2023 年,旨在立即部署到 MT 系统中。它引入了一个完整的端到端解决方案——从数据采集到网络抓取再到文本对齐等等。我们通过提供一种新颖的流程来解决以前的数据访问方法的过时问题,该流程包括一个简约的单机可运行示例和补充的可选分布式计算步骤,以最大限度地降低时间成本。我们的工作建立在该领域的先前努力的基础上并加以扩展,利用了文本对齐工具的最新进展。该语料库分为三个粒度级别,并附带一组强大的平行文本,可根据 MIT 许可证轻松访问。该语料库不仅促进了机器翻译系统的开发,还为希望探索联合国框架内多语言文献细微差别的语言学家和研究人员提供了资源。

1. Introduction

联合国digital library的翻译质量严谨、有保障(?),并且数据完全公开,量也不少,而且前人有这方面的工作(引原文)。老的语料也在不少机器翻译模型的训练中发挥了作用(引OPUS、OpenNMT的东西)。又因为联合国数据库经过重大改版,老的数据获取方式已经失效(diss一下源链接爬不到,晒一下我们找到的doc链接),同时近几年来已经有更多更先进的工具能够进行文本对齐(引几份工作),所以我们决定延续他们的工作,做一份流程完整的,能够即开即用运行的,从爬取数据到处理数据再到最终平行语料格式的整理的管线,管线包含了一个最基础的单机运行的尽可能简单的样例,以及一小部分可选的为了减小时间成本而设计的分布式计算步骤。此外,我们还提供我们获取到的2000年到2023年的已经处理完毕的平行语料。

全球化步伐的加快凸显了多语言交流的关键作用,尤其是在数字时代。联合国数字图书馆 (UNDL) 正是在此大放异彩,以其严格的翻译质量和全面的数据透明度而闻名。其广泛的公共领域存储库包含历史和当代资料,是自然语言处理 (NLP) 和机器翻译 (MT) 模型的宝贵资源,建立在原始论文中引用的先前基础工作之上。值得注意的是,UNDL 数据的纳入提高了著名机器翻译框架(如 OPUS 和 OpenNMT)的性能。然而,随着联合国数据库的重大修订,传统的数据获取方法已经过时——这是一个争论点,因为原始链接不再可检索。再加上近年来更复杂的文本对齐工具的出现,我们发现必须继续并扩展前人奠定的基础。因此,我们创建了一个全面的流程,它不仅涵盖了从数据抓取到数据集整理的整个过程,还包括一个用于独立执行的简单示例和可选的分布式计算步骤,以提高时间效率。此外,我们还提供了从 2000 年到 2023 年的并行文本语料库,这些文本经过精心处理,可立即应用。

2. License and Availability

我们用MIT协议开源。我们收集到的2000-2023年的数据托管在百度网盘(给个链接)以及huggingface上。https://huggingface.co/datasets/bot-yaya/rework_undl_text

3. File Organization and Format

我们做了三个粒度的平行语料【每个粒度放一份样例】:

  1. 文件级别:保存从原始网站上爬下来的doc文件转出txt文本,这些数据信息损失最小,但存在较多制表噪声,我们以filewise_result.jsonl的形式在管线脚本输出中给出,每一行的json对应于原始数据的一个symbol号指定的文件,其中以双字母缩写表示具体的语种文件。由于存在部分symbol号下并不包含全语种,或者我们在爬取数据时某些文件遇到404 Not Found,这些缺失的文件我们直接用空字符串填充。

  2. 双语段落级:我们使用后文将要提到的算法,将原始doc转出txt的文本以双换行符作为分割依据切分成段落,然后将每种非英语语言机翻成英语,然后运行对齐程序,得到了每个非英语语种"尽可能"按单个或若干个段落对齐到英语的平行语料。这些语料粒度为段落级别,比起文件级别粒度更细,我们预期其可以用于上下文token长度没那么大的模型的训练上。同时,为了评判对齐质量,我们顺带给出两列叫做"匹配率"的参考分数,这个值是两种语言的段落之间最长公共子序列的匹配部分的比例分别占其整个段落的百分比,这个值在后文会详细介绍。这些数据已经托管在huggingface上,(此处需要用latex格式给出一个页脚注解用于放链接)例如:https://huggingface.co/datasets/bot-yaya/undl_zh2en_aligned

  3. 全语种段落块级:我们在双语段落级对齐的基础上对所有语种的段落求了一个最大连通块,使得最后得到的所有语种段落在某个语种段落不完整时用相邻段落将其合并,从而得到粒度较大,但是全语种对齐的平行语料。这部分语料以blockwise_result.jsonl在管线脚本的输出中提供。

4. Pipeline Overview

本节将按pipeline的执行顺序逐个介绍必要以及可选步骤。

【这里要画张流程图】

【以下暂时是口语化的不规范描述,日后需要重写】

new_sample_get_list.py

经过探索,我们发现https://search.un.org/search?sort=ascending&collection=ods&currentPageNumber=1&fromYear=2018&q=*&row=100&toYear=2018这个链接可以按年份找到联合国ODS所有文件的列表。【其它的OAJ、REP简单看了一下不太容易拿到多语种文件】

此步做了缓存,中断重新执行会自动跳过已下载文件。

制作该语料库的首步是从联合国的在线网站上收集数据。United Nations Digital Library的在线查询服务经过数次改版,我们在开始收集数据到最后整理管线脚本时正经历一次改版,老的api已经全部废弃,没有办法拿到任何数据,所以我们仅提供至撰稿为止能够从最新api(提供一个页脚注解,链接:https://search.un.org/search&collection=ods)收集数据的代码。我们选择Official Document System(ODS)系统的原因,是它网页上提供同个文件的多个不同语种版本,这是整理成平行语料的必要条件。

new_sample_get_doc.py

根据new_sample_get_list.py下载下来的列表,逐个串行地下载对应的doc文件。

我们通过在此链接https://documents.un.org/api/symbol/access?s={symbol}&l={l}&t=doc中加入t=doc来尝试获取对应文件的doc版本,下载后的文件会首先对文件头进行校验,如果发现是pdf文件,则忽略它。

此步做了缓存,中断重新执行会自动跳过已下载文件。

拿到文件表之后可以很容易地从网站上下载文件。但值得注意的是网站提供的文件有时并不是doc格式而是pdf,对于pdf类型的文件即使我们做过尝试,最终也并不能以一种程序化的方法将pdf里面的段落文本干净地提取出来,所以对于这一小部分的pdf文件我们会用文件头校验来直接将其跳过。

new_sample_get_doc_async_candidate.py

如果你嫌new_sample_get_doc.py太慢,跑这个,但是如果你不想为他们服务器带来太大的并发负担,就不要用这个。

此步做了缓存,中断重新执行会自动跳过已下载文件。

new_sample_doc2txt.py

本脚本将下载下来的doc文件使用WINWORD转成docx,然后用pandoc转为txt。

【可以放附录】在项目的早期阶段,这步由于单台Windows机器多进程地执行WINWORD另存为会互相竞争,所以如果需要有效利用多核实现大批量高效转出还需要开安装有WINWORD的虚拟机或者用其它机器来实现并行转换。参考代码见convert_data/pywinauto_client.py(客户端)和convert_data/task_server.py(服务端)。

网站提供的文件大部分是老版本的doc文件,小部分是较新版本的docx文件,在早一些的年份中还会给出一部分wpf文件。为了将这些文件中的文本按段落取出,我们需要将它们统一转为易于处理的docx文件。然而用脚本实现这一步并不容易:我们测试了更对脚本友好的LibreOffice Writer和直接用Microsoft Word 2019做文件另存为产出的文件对比,结论是LibreOffice Writer另存为得到的docx文件比起Word的乱码率要高得多,而且对于一些错误文件的处理策略比Word更糟糕(如对于一部分缺少图片引用资源的文件会直接导致打不开报文件损坏,而Word能将其以纯文本形式保存)。而Word只能通过com编程的方式来做批量的另存为,由于网站提供的doc文件偶尔会损坏或者加密,或者是文件过大造成打开时闪退,或者某种原因无法正确读取而导致Word占用内存不断膨胀直到内存用尽,这一步需要非常鲁棒且繁琐的重试和超时机制来保证文件另存为程序的持续运行。我们的脚本已经尽可能枚举了我们能够遇到并且解决的异常方案,剩余的不能够自动解决的我们将其分类为错误文件,如果有足够的人力资源,也许能够从这部分文件中人工拯救一部分仍然可以做成语料的信息。注意我们的com脚本是假设用户以中文语言运行Windows并以中文使用Word来执行转换的,我们使用了控件级别的字符串查找,所以如果在其它语言的系统上运行我们的脚本,可能需要修改这些硬编码的字符串。

将所有的doc文件统一转成docx文件之后,我们简单地使用pandoc将其文本提取出来。得到的文件中双换行符已经隔开了所有自然段。

由于pipeline在实际运行中,这步不是瓶颈,所以这里保留了最初的简易单机运行版本。请注意WINWORD另存为这步的异常case非常多,即使我们已经尽可能用自动化脚本完成各种处理,但仍有可能需要人工干预。

在执行完WINWORD对DOC文件另存为DOCX文件后,脚本会调用pandoc将得到的DOCX转为txt文本。所有文件都转完后,脚本会产出一个文件级别的平行语料filewise_result.jsonl

此步做了缓存,中断重新执行会自动跳过已转换文件。

new_sample_txt2translate.py

doc提取出的文本需要进行机器翻译才能够进行对齐,从而产出对齐好的平行语料。我们选择使用argostranslate作为我们的机器翻译工具,将所有的非英语语种文件翻译为英语,然后记下每个段落被翻成了什么,这些信息将在之后的对齐中用到。机器翻译是我们整条管线中耗费算力最大的一步,所以我们在单机演示脚本之外也提供了一套可选的分布式程序,以满足拥有多台机器的使用者的需要。

我们使用argostranslate【给个引用】做机翻。这个脚本把上一步的非英语语种的txt输出以双换行符\n\n切为段落,全部翻成英语,然后记下每段对应的机翻英语,这些机翻段落会在之后的对齐中用到。

此步做了缓存,中断重新执行会自动跳过已翻译文件。

如果使用单机运行整条pipeline,此步是耗时最大的步骤,所以我们同时给出一个简单的,不考虑网络安全的分布式机器翻译脚本。

new_sample_txt2translate_distrib_candidate_server.py

机翻任务的发布服务器,每次启动会枚举本地没翻的任务,然后等client过来请求的时候消费出去。

收到client回复后,会创建对应文件的翻译缓存。需要注意所有任务都翻完之后应该再次执行new_sample_txt2translate.py,该脚本将所有翻译缓存整合为存在本地的huggingface dataset,以便后续步骤使用。

new_sample_txt2translate_distrib_candidate_client.py

机翻任务的实际工作节点,每台不带cuda的机子最好只起一个。如果机子有cuda,则建议起一个不带cuda实例的同时再起一个带cuda实例的,可以同时利用cpu和gpu的算力。配置设备的选项在脚本头两行。

client会在与server通信不正常的时候每隔30秒重试请求,如果任务做完,应该及时手动关闭client。

new_sample_translate2align.py

得到每个段落及其对应的英语翻译后,我们使用了一种基于最长公共子序列(LCS)的方法来做非英语语种到英语的段落级别的对齐。

首先,我们观察到不同语种文件之间的段落数可能不同,即,他们不是exactly一个段落能够对应到另一个语言的一个段落。这就要求我们设计一种办法,能够处理n段对应于m段的实际文本数据,即是将某个语言的n个段落合并在一起对应到另一个语言的m个段落合并到一起的文本。但这种方法可想而知最坏的结果会导致我们压根没有拆段,而是得到整个文件级别的对齐数据,即是所有段落合并到一起对应于另外一个语言的所有段落合并到一起。这与我们预期提供段落级对齐的语料数据以用于训练上下文长度不够长的模型的需求不符。

所以我们定义了一个指标hit_rate用于界定这个段落,它的意义是翻译后的某种源语言(在我们提供的数据中是非英语语言)与目的语言(在我们提供的数据中是英语)之间的单词级别的LCS部分占整个段落单词数量的比例,或者通俗的说就是匹配部分占整段的百分比。

我们使用翻译后的文本进行对齐的步骤如下所述:

  1. 我们将按段落翻译成英语的某个非英语语种(以下称为“源语言”)先用空字符作为分隔依据将“单词”切开,同一文件的英文版本也如此操作。
  2. 我们令单词完全相等为依据,将翻译后的源语言和英语进行一次全文级别的LCS(注,由于动态规划求LCS效率低到不可接受,我们使用了一种叫做Hunt-Szymanski改进算法来求LCS)
  3. 我们根据LCS匹配的结果,对翻译后源语言的每个段落以及英文段落算hit_rate
  4. 根据实际数据,我们取0.2作为阈值(实际上,这个值可以由用户酌情决定,如果设置得太高,如0.6,则可能会丢掉一些潜在的有意义的段落,如果设置得太低,如0.01,则可能会有一些明显是噪声数据的段落被加入到对齐的段落中),将hit_rate低于这个阈值的源语言段落或者英语段落忽略掉
  5. 我们根据先前抛出的LCS的匹配结果,对于剩余的段落建图。源语言的每个段落和英语文本的每个段落视为图中的一个节点,如果翻译后的源语言中的某个段落存在一个和英语文本中某个段落的“单词”的LCS匹配结果,则为两个段落对应的节点建立一条连边
  6. 对建好的图求联通块,将联通块中所对应的源语言段落拼合到一起,再把联通块中英文段落拼合到一起,制成源语言到英文的平行语料

本脚本利用翻译结果,使用本文提出的一种基于LCS的方法进行段落级对齐【引一下wiki,之后补步骤】。

需要安装定制版本的pylcs https://github.com/voidf/pylcs,避免内存和时间超限

new_sample_align2mergedjsonl.py

我们可以在对齐步骤中得到双语级别的对齐语料以及段落图的连边。在制成全语种对齐语料的时候,我们简单地在段落图上再求一次联通块,然后得到全语种对齐的段落级平行语料。