自动向非英语文档添加 Diacritic/Accent 标记

Automatically Add Diacritic/Accent Marks to a Non-English Document

在业余时间,我正在抄写一本用罗马尼亚语写的非常古老、稀有的书(事实上,据我所知,这是唯一剩下的副本)。它写于一百多年前,远早于任何计算机的出现。因此,不存在数字副本,我正在手动转录和数字化它。

这本书有数千页,在我键入的每个单词上添加变音符号和重音符号 (ă/â/î/ş/ţ) 非常耗时(至少对我而言)。如果我省略标记并只键入裸字母(即 a 而不是 ă/â),我的键入速度可以提高两倍以上,这是一个巨大的好处。目前,我将所有内容直接输入到 .tex 文件中,以便为页面和插图应用特殊格式。

但是,我知道最终我将不得不将所有这些标记重新添加到文本中,而且似乎 tedious/unecessary 需要手动完成所有这些操作,因为我已经有了所有的字母。我正在寻找一些方法来 automatically/semi-automatically ADD diacritic/accent 标记到大量文本(not 删除 -我看到很多问题询问如何删除 SO 上的标记)。

我尝试搜索大量罗马尼亚语词(this and this 是最有希望的两个),但我发现的所有内容都不尽如人意,在我提供给它的任何随机文本样本中至少遗漏了几个词(我使用了一个简短的 python 脚本)。这本书使用了许多 archaic/uncommon 个单词或不常见的单词拼写,这无济于事。

有没有人对我如何处理这件事有任何想法?这里没有愚蠢的想法 - 任何您能想到的可能有帮助的文档格式、机器学习技术、编码语言、专业工具等,我们将不胜感激。

我还应该指出,我有丰富的编码经验,不会认为自己构建一些东西是浪费时间。老实说,我认为它可能对社区有益,因为我在 任何 西方语言(法语、捷克语、塞尔维亚语等)中找不到这样的工具。只需要一些关于如何开始的指导。

我想到的是一个简单的替换。大约 10% 的单词只能通过变音符号来区分,例如abandonaabandonă,这些不会被修复。但剩下的90%会被修复。

const dictUrl = 'https://raw.githubusercontent.com/ManiacDC/TypingAid/master/Wordlists/Wordlist%20Romanian.txt';
async function init(){
  console.log('init')
  const response = await fetch(dictUrl);
  const text = await response.text();
  console.log(`${text.length} characters`)
  const words = text.split(/\s+/mg);
  console.log(`${words.length} words`)
  const denormalize = {}
  let unique_count = 0;
  for(const w of words){
    const nw = w.normalize('NFD').replace(/[^a-z]/ig, '')
    if(!Object.hasOwnProperty.call(denormalize, nw)){
      denormalize[nw] = [];
      unique_count += 1;
    }
    denormalize[nw].push(w);
  }
  console.log(`${unique_count} unique normalized words`)
  
  for(const el of document.querySelectorAll('textarea')){
    handleSpellings(el, denormalize);
  }
}

function handleSpellings(el, dict){
    el.addEventListener("keypress", function (e) {
      if(e.key == ' ')
      setTimeout(function () {
         const restored = el.value.replace(
           /\b\S+(?=[\x20-\x7f])/g, 
            (s) => {
              const s2 = dict[s] ? dict[s][0] : s;
              console.log([s, dict[s], s2]);
              return s2;
            }
         );
         el.value = restored;
      }, 0)
    })
}

window.addEventListener('load', init);
<body>

<textarea width=40 height=10 style="width: 40em; height:10em;">

</textarea>
</body>

是一种静态方法,它的工作取决于 word-list 的好坏。 因此,如果此列表中缺少一个词,它将永远不会被处理。

此外,与许多其他语言一样,存在两个(或更多)单词具有相同字符但变音符号不同的情况。 对于罗马尼亚语,我找到了以下示例:peste = over vs. pesţe = fish。 这些情况也不能直接处理。 这尤其是一个问题,如果您转换的文本包含当今语言中不再使用的词,尤其是变音符号。

在这个回答中,我将介绍一种使用机器学习的替代方法。 唯一需要注意的是,我找不到一个公开可用的训练有素的模型来为罗马尼亚语进行变音符号恢复。 您可能会幸运地联系到我将在此处提到的论文的作者,看看他们是否愿意将经过训练的模型发送给您使用。 否则,你将不得不训练自己,我会给出一些建议。 我将尝试提供全面的概述以帮助您入门,但我们鼓励您进一步阅读。

虽然这个过程可能很费力,但它可以给你 99% accuracy 使用正确的工具。

语言模型

语言模型是一种可以被认为具有对语言的high-level“理解”的模型。 在原始文本语料库中通常是 pre-trained。 尽管您可以自己训练,但要注意这些模型相当 expensive to pre-train.

虽然可以使用 multilingual models,但如果使用足够的数据进行训练,language-specific 模型通常表现更好。 幸运的是,罗马尼亚语有可用的公共语言模型,例如 RoBERT。 该语言模型基于 BERT,一种在自然语言处理中广泛使用的体系结构,由于它在英语和其他语言中取得了 state-of-the-art 结果,因此或多或少是该领域的标准。

实际上有三种变体:base, large, & small。 由于更大的表示能力,模型越大,结果越好。 但更大的模型在内存方面也会有更高的占用空间。

使用 transformers 库加载这些模型非常容易。 例如,base model:

from transformers import AutoModel, AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("readerbench/RoBERT-base")
model = AutoModel.from_pretrained("readerbench/RoBERT-base")
inputs = tokenizer("exemplu de propoziție", return_tensors="pt")
outputs = model(**inputs)

上面的 outputs 将包含输入文本的矢量表示,通常称为“词嵌入”。 然后语言模型 fine-tuned 到下游任务——在你的例子中,变音符号恢复——并将这些嵌入作为输入。

Fine-tuning

我找不到任何公开可用的 fine-tuned 模型,所以你必须 fine-tune 自己的模型,除非你自己找到模型。

为了 fine-tune 语言模型,我们需要构建一个 task-specific 架构,该架构将在某些数据集上进行训练。 数据集用于告诉模型输入如何以及我们希望输出如何。

数据集

来自 Diacritics Restoration using BERT with Analysis on Czech language, there's a publicly available dataset 的多种语言,包括罗马尼亚语。 数据集注释还将取决于您使用的 fine-tuning 架构(更多内容见下文)。

一般来说,您会选择一个您认为具有 high-quality 个变音符号的数据集。 从此文本中,您可以通过生成单词的未变体以及相应的标签来自动构建注释。

请记住,这个或您将使用的任何其他数据集都会包含偏差,尤其是在注释文本的来源领域方面。 根据您已经转录的数据量,您可能还想使用您的文本构建数据集。

建筑

您选择的架构将影响您使用的下游性能以及您必须执行的自定义代码量。

Word-level

上述工作 Diacritics Restoration using BERT with Analysis on Czech language 使用 token-level 分类机制,其中每个单词都标有一组变音符号类型的指令,以插入字符索引。 例如,带有指令集 1:CARON;3:ACUTE 的未变音词“dite”表示在索引 1 和索引 3 处添加适当的变音符号以生成“dítě”。

由于这是一个 token-level 分类任务,您无需编写太多自定义代码,因为您可以直接使用 BertForTokenClassification。 有关更完整的示例,请参阅 authors' code

旁注是作者使用了多语言语言模型。 这可以很容易地替换为另一种语言模型,例如上面提到的RoBERT

Character-level

或者,the RoBERT paper 使用 character-level 模型。 从论文中,每个字符都被注释为以下之一:

make no modification to the current character (e.g., a → a), add circumflex mark (e.g., a → â and i → î), add breve mark (e.g., a → ̆ă), and two more classes for adding comma below (e.g., s → ş and t → ţ)

在这里您必须构建自己的自定义模型(而不是上面的 BertForTokenClassification)。 但是,其余的训练代码将基本相同。 这是模型 class 的模板,可以使用 transformers 库:

from transformers import BertModel, BertPreTrainedModel

class BertForDiacriticRestoration(BertPreTrainedModel):

    def __init__(self, config):
        super().__init__(config)
        self.bert = BertModel(config)
        ...

    def forward(
        self,
        input_ids=None,
        attention_mask=None,
        token_type_ids=None
    ):
        ...

评价

每个部分都有很多选择供您选择。 如果您想进一步改进,我会提供一些务实的建议,那就是从简单和复杂的事情开始。 保留一个测试集来衡量您所做的更改是否会导致您之前设置的改进或退化。

最重要的是,我建议你的测试集至少有一小部分是来自你自己转录的文本的文本,你使用的越多越好。 主要是,这是您自己注释的数据,因此与任何其他公开可用的来源相比,您对这些数据的质量更有把握。 其次,当您测试来自目标域的数据时,由于其他域可能存在的某些偏差,您更有可能更准确地评估您的系统以完成您的目标任务。