对话式人工智能

使用 NVIDIA NeMo 定制神经机器翻译模型,第 2 部分

上一篇文章 中,我们介绍了使用 NeMo 运行英-中翻译模型的示例,并评估其性能。在这篇文章中,我们将指导您如何定制数据集,并在该数据集上微调模型。

数据收集

数据收集在模型微调中至关重要,因为它使模型适配特定任务或领域要求。

例如,我们翻译任务是将计算机科学相关的技术文章从英文翻译成中文,那么收集以前人工翻译的文章作为微调数据是很有必要的。因为此类文章中包含很多这个领域中常用概念和术语,但在这些语料在预训练数据集中极少出现。

我们建议至少采集几千个高质量的样本。在使用这些量身定制的数据进行微调后,模型可以在技术博客翻译任务中取得更好的表现。

数据预处理流程

为了提高数据质量,您需要对数据进行预处理,以过滤掉无效和冗余的脏数据。NVIDIA NeMo framework 包含了 NVIDIA NeMo Curator,目前可以用于处理 LLM 预训练中使用的语料库。然而,NMT 的双语数据集与单语语料库不同,因为它们有源文本和目标文本,需要特殊的过滤方法。幸运的是,NeMo 也提供了大部分开箱即用的函数和脚本,满足了双语数据集处理的需求。

您可以用一个简单的数据预处理流程来清洗英-中的翻译数据集:

  • 语言筛选
  • 长度过滤
  • 数据去重
  • 分词处理和标准化(仅用于 NeMo 模型微调)
  • 转换为 JSONL 格式(仅用于 ALMA 模型微调)
  • 拆分数据集

除了以上这些方法,您还可以使用其他自定义的方案来过滤掉无效数据。例如,使用现有的 NMT 模型筛选出可能是错误的翻译。如想了解更多数据预处理方法,请参阅 此文档

原始的数据格式

我们收集了英-中的翻译对,并放在了两个文本文件:

  • en_zh.en:存储了按行分隔的英语句子。
  • En_zh.zh:这个文件中每一行是相应 en_zh.en 中每一行的中文翻译。
  en_zh.en en_zh.zh
第 1 行 Accelerate Data Preparation 加快完成数据准备
第 2 行 An end to end, cloud native, suite of AI and data analytics software, optimized, certified and supported by NVIDIA. 一款经 NVIDIA 优化、认证和支持的端到端云原生 AI 和数据分析软件套件。
表 1:中英两个文件之间的对应关系示例

 

在本节中,经过每个处理数据处理步骤后输出的文件都按以上格式记录。

语种过滤

NeMo 通过使用 fastText 的语种识别技术提供了语种过滤的方法。

首先,从 fastText 网站下载语言分类器模型 lid.176.bin:

wget https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin -O lid.176.bin

使用以下代码进行语种筛选:

python /opt/NeMo/scripts/neural_machine_translation/filter_langs_nmt.py \
--input-src en_zh.en \
--input-tgt en_zh.zh \
--output-src en_zh_preprocessed1.en \
--output-tgt en_zh_preprocessed1.zh \
--removed-src en_zh_garbage1.en \
--removed-tgt en_zh_garbage1.zh \
--source-lang en \
--target-lang zh \
--fasttext-model \
lid.176.bin

en_zh_preprocessed1.enen_zh_preprocessed1.zh 是保留下来的有效数据,而 en_zh_garbage1.en 和 en_zh_garbage1.zh 则是废弃数据。

以下是 en_zh_garbage1.zh 文件的一部分,这些都不是中文所以被过滤了:

NVIDIA OVX
Tensor Core
PROP_2.USD
ConnectX-6 Dx、ConnectX-6

长度过滤

长度过滤用于去除双语语料库中过短或过长的文本,因为他们可能是一些脏数据。此外,我们还会根据目标句子和源句子之间的长度比进行过滤。

在运行长度筛选之前,您可以使用以下脚本计算出您收集的数据中的译文和原文的长度比的分布,以便了解要筛选的比率阈值:

def compute_length_ratio(source_txt, target_txt, percentile):
    len_ratios = list()
 
    with open(source_txt, "r") as src, \
        open(target_txt, "r") as tgt:
        for src_line, tgt_line in zip(src, tgt):
            len_ratios.append(len(src_line.strip()) / len(tgt_line.strip()))
 
    len_ratios.sort()
 
    # compute percentile
    ratio = len_ratios[int(len(len_ratios) * percentile)]
    print(f"Length ratio @{percentile} percentile is {ratio}")
    return ratio
 
compute_length_ratio("en_zh_preprocessed1.en", "en_zh_preprocessed1.zh", 0.95)

这个长度比在不同的数据集以及源语言和目标语言上也有所不同。

运行以下脚本以执行长度筛选。其中,--ratio 4.6参数用于指定最大的长度比。

python /opt/NeMo/scripts/neural_machine_translation/length_ratio_filter.py \
    --input-src en_zh_preprocessed1.en \
    --input-tgt en_zh_preprocessed1.zh \
    --output-src en_zh_preprocessed2.en \
    --output-tgt en_zh_preprocessed2.zh \
    --removed-src en_zh_garbage2.en \
    --removed-tgt en_zh_garbage2.zh \
    --min-length 10 \
    --max-length 512 \
    --ratio 4.6

同上,en_zh_preprocessed2.enen_zh_preprocessed2.zh 是可以保留到下一步的数据文件。

数据去重

在这一步中,您可以使用xxhash库。

pip install xxhash

运行以下 Python 脚本进行去重:

import xxhash
 
def dedup_file(input_file_lang_1, input_file_lang_2, output_file_lang_1, output_file_lang_2):
    hashes = set()
    with open(input_file_lang_1, 'r') as f_lang1, \
        open(input_file_lang_2, 'r')  as f_lang2, \
        open(output_file_lang_1, 'w') as f_out_lang1, \
        open(output_file_lang_2, 'w') as f_out_lang2:
        for line_1, line_2 in zip(f_lang1, f_lang2):
            parallel_hash = xxhash.xxh64((line_1.strip()).encode('utf-8')).hexdigest()
            if parallel_hash not in hashes:
                hashes.add(parallel_hash)
                f_out_lang1.write(line_1.strip() + '\n')
                f_out_lang2.write(line_2.strip() + '\n')
 
dedup_file(
    'en_zh_preprocessed2.en',
    'en_zh_preprocessed2.zh',
    'en_zh_preprocessed3.en',
    'en_zh_preprocessed3.zh'
)

在此步骤中,要保留的数据文件为 en_zh_preprocessed3.enen_zh_preprocessed3.zh

分词处理和标准化 (仅用于 NeMo 模型微调)

如果要微调 NeMo 模型,需要进行额外的分词处理和标准化:

  • 标准化:统一规范句子中的标点符号,例如引号的格式。

分词处理:通过在标点符号和相邻单词之间添加空格,以避免单词上附加标点符号,这是 NeMo 训练中的一项建议步骤。

python /opt/NeMo/scripts/neural_machine_translation/preprocess_tokenization_normalization.py \
    --input-src en_zh_preprocessed3.en \
    --input-tgt en_zh_preprocessed3.zh \
    --output-src en_zh_final_nemo.en \
    --output-tgt en_zh_final_nemo.zh \
    --source-lang en \
    --target-lang zh

这脚本底层采用不同了的库来处理不同的语言。例如,英语使用了 sacremoses ,对简体中文,则使用 JiebaOpenCC

这一步输出的 en_zh_final_nemo.enen_zh_final_nemo.zh 是 NeMo 微调所需的数据集文件。

转换 JSONL 格式 (仅用于 ALMA 模型微调)

ALMA 训练需要采用 JSON 行(JSONL)作为原始输入数据,其中文件中的每一行都是一个 JSON 结构:

{"translation": {"en": "Accelerate Data Preparation", "zh": "加快完成数据准备"}}

如要训练 ALMA 模型,您必须将 en_zh_preprocessed3.enen_zh_preprocessed3.zh 这两个中英文件转换为一个 JSONL 格式的文件:

import json
 
def txt2jsonl(source_txt, target_txt, source, target, output_jsonl):
  with open(source_txt, "r") as f:
      source_lines = f.read().splitlines()
  
  with open(target_txt, "r") as f:
      target_lines = f.read().splitlines()
 
  json_list = list()
  for source_text, target_text in zip(source_lines, target_lines):
      json_item = {
          "translation": {source: source_text, target: target_text}
      }
      json_list.append(json_item)
 
  with open(output_jsonl, "w") as f:
      for json_item in json_list:
          f.write(json.dumps(json_item, ensure_ascii=False) + "\n")
 
source_txt = "en_zh_preprocessed3.en"
target_txt = "en_zh_preprocessed3.zh"
source = "en"
target = "zh"
output_jsonl = "en_zh_final_alma.jsonl"
txt2jsonl(source_txt, target_txt, source, target, output_jsonl)

这一步输出的 en_zh_final_alma.jsonl 是 ALMA 微调所需的数据集文件。

拆分数据集

最后一步是将数据集拆分为训练集、验证集和测试集。你可以使用 sklearn.model_selection 库中的 train_test_split 方法来完成此操作。

对于 NeMo NMT 模型微调,上一步输出的 en_zh_final_nemo.en 和 en_zh_final_nemo.zh 会被拆分为:

  • en_zh_final_nemo_train.en 
  • en_zh_final_nemo_train.zh 
  • en_zh_final_nemo_val.en 
  • en_zh_final_nemo_val.zh
  • en_zh_final_nemo_test.en 
  • en_zh_final_nemo_test.zh 

对于 ALMA 模型微调,上一步输出的 en_zh_final_alma.jsonl 会被拆分为:

  • train.zh-en.json
  • valid.zh-en.json
  • test.zh-en.json

在本节中,我们将分别演示如何微调 NeMo 和 ALMA 模型。

模型微调

在本节中,我们将分别演示如何微调 NeMo 和 ALMA 模型。

微调 NeMo NMT 模型

在进行微调之前,请参考上一个博客中 NMT 模型评估章节的介绍,把 NeMo 预训练模型下载到 ./model/pretrained_ckpt/en_zh_24x6.nemo。然后,您可以使用收集的数据集对 NeMo EN-ZH 模型进行微调。请注意,批处理大小将取决于 GPU 显存的大小。

python /opt/NeMo/examples/nlp/machine_translation/enc_dec_nmt_finetune.py \
  model_path=model/pretrained_ckpt/en_zh_24x6.nemo \
  trainer.devices=1 \
  trainer.max_epochs=10 \
  +trainer.val_check_interval=600 \
  model.train_ds.tgt_file_name=data/en_zh_final_nemo_train.zh \
  model.train_ds.src_file_name=data/en_zh_final_nemo_train.en \
  model.train_ds.tokens_in_batch=3000 \
  model.validation_ds.tgt_file_name=data/en_zh_final_nemo_val.zh \
  model.validation_ds.src_file_name=data/en_zh_final_nemo_val.en \
  model.validation_ds.tokens_in_batch=1000 \
  model.test_ds.tgt_file_name=data/en_zh_final_nemo_test.zh \
  model.test_ds.src_file_name=data/en_zh_final_nemo_test.en \
  +exp_manager.exp_dir=output/ \
  +exp_manager.create_checkpoint_callback=True \
  +exp_manager.checkpoint_callback_params.monitor=val_sacreBLEU \
  +exp_manager.checkpoint_callback_params.mode=max \
  +exp_manager.checkpoint_callback_params.save_best_model=true \
  +exp_manager.checkpoint_callback_params.always_save_nemo=true \
  +exp_manager.checkpoint_callback_params.save_top_k=10

训练完成后,结果和权重文件将被保存到 ./output/AAYNBaseFineTune 路径。您可以使用 TensorBoard 来可视化损失曲线或查看日志文件。

微调 ALMA NMT 模型

要微调 ALMA 模型,首先要从 GitHub 上克隆 ALMA 的仓库,并在 NeMo framework 容器中的安装额外的依赖项。

git clone https://github.com/fe1ixxu/ALMA.git
cd ALMA
bash install_alma.sh
pip install --upgrade pytest

数据

您可以将处理后的数据集(train.zh-en.jsonvalid.zh-en.jsontest.zh-en.json)放置在根目录下的 ./human_written_data/zhen 数据目录中,其中 /zhen 子目录专门用于存储中英文的数据集。

配置修改

下一步是修改两个配置文件中的参数:/runs/parallel_ft_lora.sh/configs/deepspeed_train_config.yaml

/runs/parallel_ft_lora.sh 中,需要修改的字段有:

  • per_device_train_batch_size:每个设备训练批大小。
  • gradient_accumulation_steps:在梯度更新前的累积步数。
  • learning_rate:根据批大小和累积步骤数进行动态调整。

/configs/deepspeed_train_config.yaml文件中需要修改的字段有:

  • gradient_accumulation_steps:需要和上面 /runs/parallel_ft_lora.sh 中的gradient_accumulation_steps 值相同
  • num_processes:用于指定 GPU 设备的数量。

微调命令

如要使用 LoRA 微调 ALMA 模型的英-中和中-英双向翻译能力,可在 ALMA 仓库的根目录中运行以下命令:

bash runs/parallel_ft_lora.sh output zh-en,en-zh

结果文件将被存储在 ./output 目录:

  • adapter_config.json:用于存储 LoRA 配置。
  • adapter_model.bin:LoRA 模型权重。
  • trainer_state.json:训练过程中的损失记录。

微调模型评估

在上一博客中,您评估了 NeMo 和 ALMA 预训练模型的初始性能。在这里,您可以通过使用相同的测试数据集对微调后的模型进行评估测试。

微调 NeMo 模型评估

对于 NeMo 模型可使用以下脚本在相同的测试集进行推理:

python /opt/NeMo/examples/nlp/machine_translation/nmt_transformer_infer.py \
  --model $fine_tuned_nemo_path \
  --srctext input_en.txt \
  --tgtout nemo_ft_out_zh.txt \
  --source_lang en \
  --target_lang zh \
  --batch_size 200 \
  --max_delta_length 20
sacrebleu reference.txt -i nemo_ft_out_zh.txt -m bleu -b -w 4
  • $fine_tuned_nemo_path:NeMo 微调后的模型路径。
  • input_en.txt:输入的英文文本。
  • nemo_ft_out_zh.txt:NeMo 模型微调后输出的中文。
  • reference.txt: 参考译文。

最后一行代码计算了 BLEU 分数。

微调 ALMA 模型评估

评估 ALMA 模型时也需要对相同的测试集进行推理。以下脚本是对微调后的 ALMA 模型的推理代码片段,其中模型加载部分与前面讨论的略有不同,因为它需要读取本地的微调 PEFT 模型和配置。

import torch
from peft import PeftModel, PeftConfig
from transformers import AutoModelForCausalLM
from transformers import LlamaTokenizer
 
 
# Load base model and LoRA weights
peft_config = PeftConfig.from_pretrained("./output")
model = AutoModelForCausalLM.from_pretrained(peft_config.base_model_name_or_path, torch_dtype=torch.float16, device_map="auto")
model = PeftModel.from_pretrained(model, "./output")
model = model.eval()
tokenizer = LlamaTokenizer.from_pretrained("haoranxu/ALMA-7B-Pretrain", padding_side='left')
 
# Add the source sentence into the prompt template
prompt_template = "Translate this from English to Chinese:\nEnglish: {}\nChinese:"
prompt = prompt_template.format("AI is powering change in every industry")
 
 
# Tokenize
input_ids = tokenizer(prompt, return_tensors="pt", padding=True, max_length=40, truncation=True).input_ids.cuda()
# Inference
with torch.no_grad():
    generated_ids = model.generate(input_ids=input_ids, num_beams=5, max_new_tokens=256, do_sample=True, temperature=0.6, top_p=0.9)
 
outputs = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)
output = outputs[0].replace(prompt, "").strip()
print(output)

您可以通过修改示例推理的脚本以对英语文本文件进行翻译,并计算其 BLEU 分数。

结论

NeMo 框架容器 提供了一个方便的环境,适用于各种推理和定制任务。

NeMo framework 容器 提供了一个方便的环境,适用于各种推理和定制任务。

在本系列中,我们介绍了 NeMo NMT 模型和 ALMA NMT 模型在容器中从零开始微调的流程,涵盖了预训练模型推理和评估、数据收集和处理、模型微调和最终评估。

如想解更多关于构建和部署多语言 AI 对话系统的方案,请参阅 NVIDIA Riva。其中您还可以了解到更多关于实时双语或多语言的 语音到语音翻译(S2S)语音到文本翻译(S2T) API。

如想在 NeMo 框架中执行更多 LLM 和分布式训练任务,请参考我们的 playbooks开发者文档

欲了解更多关于处理数据和模型评估的方案,请参阅最近发布的 NeMo CuratorNVIDIA NeMo Evaluator 微服务,并申请 NeMo Microservices 早期访问

 

Tags