在当今的全球化世界中,AI 系统理解和沟通不同语言的能力变得越来越重要。大型语言模型 (LLMs) 彻底改变了自然语言处理领域,使 AI 能够生成类似人类的文本、回答问题和执行各种语言任务。然而,大多数主流 LLM 都在主要由英语组成的数据语料库上进行训练,从而限制了它们对其他语言和文化语境的适用性。
这就是 多语种 LLM 的价值所在:缩小语言差距,并释放 AI 的潜力,使其惠及更广泛的受众。
特别是,由于训练数据有限以及东南亚 (SEA) 语言的独特语言特性,当前最先进的 LLM 经常难以与这些语言进行交流。这导致与英语等高资源语言相比,性能较低。虽然一些 LLM 在一定程度上可以处理某些 SEA 语言,但仍然存在不一致、幻觉和安全问题。
与此同时,人们对在东南亚开发本地化的多语种 LLM 有着浓厚的兴趣和决心。一个值得注意的例子是,新加坡启动了一项 7000 万新元的计划,以开发国家多模态大型语言模型计划 (NMLP)。
这项为期两年的国家层面计划旨在打造东南亚首个区域 LLM,专注于了解该地区独特的语言和文化细微差别。随着东南亚地区对 AI 解决方案的需求不断增长,开发本地化的多语种 LLM 成为战略需要。
在其他地区也可以看到类似的趋势,目前先进的 LLM 不足以支持复杂的区域语言。这些模型可以帮助企业和组织更好地服务客户、实现流程自动化,并创建更具吸引力的内容,与该地区的多元化人口产生共鸣。
NVIDIA NeMo 是一个端到端平台,旨在随时随地开发自定义生成式 AI。它包括用于训练的工具、检索增强生成(RAG)、护栏和工具包、数据管护工具以及预训练模型,为企业提供了一种简单、经济高效且快速的方法来采用生成式 AI。
在本系列中,我们将探索使用 Omniverse Create 向基础语言模型(LLM)添加新语言支持的最佳实践。本教程将指导您完成 NeMo 的关键步骤,包括分词器训练和合并、模型架构修改和模型持续预训练等。
在本文中,我们使用泰文维基百科数据对 GPT-1.3 B 模型进行持续预训练。我们在第 1 部分中着重介绍了训练和合并多语种分词器,然后讨论了在 NeMo 模型中采用自定义分词器的方法,并将在 第 2 部分 中继续探讨。
通过遵循这些指南,您可以为多语种 AI 的发展做出贡献,并让更广泛的全球受众受益于 LLM。
本地化多语种 LLM 训练概述
多语种 LLM 面临的一个重大挑战是,理解目标语言的预训练基础 LLM 不足。要构建多语种 LLM,您有以下几种选择:
- 使用多语种数据集从头开始预训练 LLM。
- 使用目标语言的数据集对英语基础模型进行持续预训练。
在低资源语言的情况下,后一种选择更可行。根据定义,低资源语言的可用训练数据有限。通过利用从最初训练模型所基于的高资源语言进行迁移学习,持续预训练可以有效地使模型适应新语言,即使数据量相对较少。
从头开始预训练需要使用低资源语言处理更大量的数据,才能达到相同的性能水平。
在尝试使用低资源数据进行持续预训练时,我们面临的一个挑战是次优分词器。大多数基础模型都采用字节对编码 (BPE) 分词器。原始分词器无法充分涵盖低资源语言的独特字符、子词和形态。
如果没有足够富有表现力的分词器,模型将难以高效地表示低资源语言,从而导致性能欠佳。有必要构建自定义分词器,使模型能够在持续预训练期间更有效地处理和学习低资源语言数据。
为解决这些问题,我们建议通过以下工作流程为 LLM 添加新的语言支持。
此工作流程将泰文维基百科数据用作以下步骤中的输入示例:
- 下载并解压缩 GPT 模型以获取模型权重和模型分词器。
- 自定义分词器训练并合并以输出双语分词器。
- 修改 GPT 模型架构以适应双语言分词器。
- 使用泰文维基百科数据执行持续预训练。
该工作流程具有通用性,可以应用于不同的语言数据集。本博文详细介绍了步骤 1 和 2。欲了解步骤 3 和 4 的详细信息,请参阅 第 2 部分。
教程预备知识
如需持续预训练 GPT-1.3 B 模型,我们建议使用以下硬件设置:
- 配备至少 30 GB GPU 显存的 NVIDIA GPU
- CUDA 和 NVIDIA 驱动:带驱动 535.154.05 的 CUDA 12.2
- Ubuntu 22.04
- NVIDIA Container Toolkit 版本 1.14.6:安装指南
- 我们使用了 NeMo 框架容器 24.01.01 版本。
我们通过 NGC 目录 提供对 GPU 加速软件的访问,旨在通过性能优化的容器、预训练的 AI 模型以及可在本地、云端或边缘部署的行业特定 SDK,以加速端到端工作流程。
第一步,从 NGC 目录中下载 NeMo 框架容器,并在容器镜像中运行 JupyterLab:
docker pull nvcr.io/nvidia/nemo:24.01.01.framework
docker run -it --gpus all -v : --workdir -p 8888:8888/ nvcr.io/nvidia/nemo:24.01.01.framework bash -c "jupyter lab"
数据收集和清理
在本教程中,我们使用 NVIDIA NeMo Curator 的 GitHub 存储库,用于下载和整理高质量的泰文维基百科数据。NVIDIA NeMo Curator 由一系列可扩展的数据挖掘模块组成,旨在整理用于训练 LLM 的 NLP 数据。NeMo Curator 中的模块使 NLP 研究人员能够从大量未整理的网络语料库中大规模挖掘高质量文本。
对于管线,请按照以下步骤操作:
- 使用不同的语言筛选非泰语内容。
- 重新格式化文档以校正任何 Unicode。
- 执行文档级精确重复数据删除和模糊重复数据删除,以删除重复的数据点。
- 执行文档级启发式过滤,以删除低质量文档。
通过使用 NVIDIA NeMo Curator 中演示的相同流程,可以复制其他语言的策展过程。
模型下载和提取
在这篇博文中,我们使用 nemo-megatron-gpt-1.3B 模型,该模型基于英语单语数据集 Pile 数据集。您可以直接从 HuggingFace 下载模型,或者运行以下命令下载模型:
!wget -P './model/nemo_gpt_megatron_1pt3b_fb16/' https://huggingface.co/nvidia/nemo-megatron-gpt-1.3B/resolve/main/nemo_gpt1.3B_fp16.nemo
验证下载文件的 MD5 校验和,以确保其完整性:
!md5sum nemo_gpt1.3B_fp16.nemo
您应获得以下内容作为输出:
38f7afe7af0551c9c5838dcea4224f8a nemo_gpt1.3B_fp16.nemo
下载模型后,解压模型中的文件:vocab.json
和 merge.txt
:
!tar -xvf ./model/nemo_gpt_megatron_1pt3b_fb16/nemo_gpt1.3B_fp16.nemo -C ./model/nemo_gpt_megatron_1pt3b_fb16/
执行该命令后,将生成以下输出。现在,您可以访问 vocab.json
和 merge.txt
文件,这些文件稍后将用于 tokenizer 的合并。
./
./50284f68eefe440e850c4fb42c4d13e7_merges.txt
./c4aec99015da48ba8cbcba41b48feb2c_vocab.json
./model_config.yaml
./model_weights.ckpt
分词器训练
要训练能够将其他语言和英语标记化的分词器,您可以采用以下两种方法之一:
- 多语种数据集:使用包含英语的多语言数据集,从头开始训练分词器,其优点在于您可以获得多语言数据集的真实分布。
- 单语言数据集:首先训练单语分词器,然后将其与原始英语分词器合并。这样做的优点是,可以保留英语分词的原始分词映射,并重复使用基础模型的嵌入层,从而缩短分词器训练的时间。
本教程使用单语言数据集方法来保留预训练 GPT Megatron 模型的嵌入层。
此方法的详细步骤包括:
- 收集分词器训练数据:从预训练数据集中进行子采样,以获取 tokenizer 训练所需的数据。在本教程中,我们随机采样了 30% 的训练数据用于该目的。
- 训练自定义 GPT2 分词器:使用预训练的 HuggingFace 模型作为起点,我们可以使用您自己的数据语料库来训练自定义的 TH GPT2 分词器。
- 合并两个分词器:手动合并
merges.txt
和vocab.json
两个文件,以创建一个单一的分词器。
在本教程中,请使用泰语作为目标语言。
导入必要的库
开始之前,请先导入以下库:
import os
from transformers import GPT2Tokenizer, AutoTokenizer
import random import json
准备训练语料库
定义一个名为 convert_jsonl_to_txt
的函数,以从训练文档数据中采样,并将其写入 .txt 格式的输出文件中。在本教程中,我们使用 'text'
作为访问训练文档数据的 JSON 密钥,根据需要可以更改该密钥。
def convert_jsonl_to_txt(input_file, output_file, percentage, key='text'):
with open(input_file, 'r', encoding='utf-8') as in_file, open(output_file, 'a', encoding='utf-8') as out_file:
for line in in_file:
if random.random() < percentage:
data = json.loads(line)
out_file.write(f"{data[key].strip()}\n")
现在,您可以读取输入文件并形成分词器训练语料库:
for file in os.listdir('./training_data'):
if 'jsonl' not in file:
continue
input_file = os.path.join('./training_data',file)
convert_jsonl_to_txt(input_file,'training_corpus.txt', 0.3)
with open('training_corpus.txt', 'r') as file:
training_corpus = file.readlines()
当训练语料库过大,无法加载到一个目标中时,可以采用迭代器方法加载训练语料库。另外,如果您想了解更多相关信息,请参阅 根据旧的分词器训练新的分词器。
训练单语分词器
首先加载预训练的 GPT2 分词器,然后调用 tokenizer.train_new_from_iterator 方法,以训练新分词器。
Vocab_size
是 tokenizer.train_new_from_iterator
的一个参数,它决定词汇表中唯一令牌的最大数量。值越大,可实现更精细的令牌化,但会增加模型的复杂性;而值越小,则可实现更粗粒度的令牌化,且唯一令牌越少,模型越简单。
old_tokenizer = AutoTokenizer.from_pretrained("gpt2")
new_tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, vocab_size=8000)
new_tokenizer.save_pretrained('./new_monolingual_tokenizer/')
现在,您已完成训练新的单语分词器,请检查新分词器在目标语言上的有效性。使用预训练的 GPT2 分词器和 TH 分词器分别将泰语句子和英语句子分词:
- 泰语句子:“เมืองหลวงของประเทศไทยคือกรุงเทพฯ” 是指“泰国的首都是曼谷”。
- 英语句子:“The capital of Thailand is Bangkok.”
Thai_text='เมืองหลวงของประเทศไทยคือกรุงเทพฯ'
print(f"Sentence:{Thai_text}")
print("Output of TH tokenizer: ",new_tokenizer.tokenize(Thai_text,return_tensors='pt'))
print("Output of pretrained tokenizer: ", old_tokenizer.tokenize(Thai_text,return_tensors='pt'))
Eng_text="The capital of Thailand is Bangkok."
print(f"Sentence:{Eng_text}")
print("Output of TH tokenizer: ",new_tokenizer.tokenize(Eng_text,return_tensors='pt'))
print("Output of pretrained tokenizer: ", old_tokenizer.tokenize(Eng_text,return_tensors='pt'))
您应该会得到以下几行作为输出:
Sentence:เมืองหลวงของประเทศไทยคือกรุงเทพฯ
Output of TH tokenizer: ['à¹Ģม', 'ื', 'à¸Ńà¸ĩหลวà¸ĩ', 'à¸Ĥà¸Ńà¸ĩà¸Ľà¸£à¸°à¹Ģà¸Ĺศà¹Ħà¸Ĺย', 'à¸Ħ', 'ื', 'à¸Ńà¸ģร', 'ุ', 'à¸ĩà¹Ģà¸Ĺà¸ŀฯ']
Output of pretrained tokenizer: ['à¹', 'Ģ', 'à¸', '¡', 'à¸', '·', 'à¸', 'Ń', 'à¸', 'ĩ', 'à¸', '«', 'à¸', '¥', 'à¸', '§', 'à¸', 'ĩ', 'à¸', 'Ĥ', 'à¸', 'Ń', 'à¸', 'ĩ', 'à¸', 'Ľ', 'à¸', '£', 'à¸', '°', 'à¹', 'Ģ', 'à¸', 'Ĺ', 'à¸', '¨', 'à¹', 'Ħ', 'à¸', 'Ĺ', 'à¸', '¢', 'à¸', 'Ħ', 'à¸', '·', 'à¸', 'Ń', 'à¸', 'ģ', 'à¸', '£', 'à¸', '¸', 'à¸', 'ĩ', 'à¹', 'Ģ', 'à¸', 'Ĺ', 'à¸', 'ŀ', 'à¸', '¯']
Sentence:The capital of Thailand is Bangkok.
Output of TH tokenizer: ['The', 'Ġc', 'ap', 'ital', 'Ġof', 'ĠThailand', 'Ġis', 'ĠB', 'ang', 'k', 'ok', '.']
Output of pretrained tokenizer: ['The', 'Ġcapital', 'Ġof', 'ĠThailand', 'Ġis', 'ĠBangkok', '.']
从输出中可以看到,与泰语句子的英语标记器和英语句子的英语标记器相比,TH 标记器生成的标记列表更短。
原因是,许多泰文字符,尤其是那些表示元音和色调标记的字符,在英语分词器中很可能被视为词外音(OOV)。分词器可以将这些字符拆分为单个字节,或将其替换为UNK
token,从而增加 token 数量。
分词器合并
要合并两个分词器,您必须处理vocab.json
和merges.txt
文件。以下是合并这两个文件的方法。
对于 vocab.json
文件中:
- 维护预训练的分词器的
vocab.json
文件中的 ID 令牌映射。 - 通过自定义的单语言分词器迭代处理
vocab.json
文件,以便在遇到新令牌时进行更新。 - 将其添加到原始文件
vocab.json
中,该文件带有累加令牌 ID。
关于merges.txt
文件:
- 预训练的分词器的
merges.txt
文件保持不变。 - 通过使用自定义的单语言分词器来迭代
merges.txt
文件。 - 当遇到新的合并规则时,请将其添加到原始规则文件
merges.txt
中。
规则是,在合并时,不允许重新排列或修改vocab.json
文件或merges.txt
文件的原始顺序。
对于vocab.json
,您必须保持原始 ID 令牌映射相同,才能重用预训练嵌入层。如果在将新合并的标记器加载到预训练模型并尝试获取令牌的嵌入时,映射受到干扰,模型可能会输出其他令牌的预训练嵌入,例如将‘dog’
的 token ID 更改为‘cat’
,从而导致‘dog’
在合并过程中发生更改。
例如,在merges.txt
文件中,合并规则的顺序对于让 BPE 标记器在标记化新文本时以最佳状态运行至关重要。标记化器将按顺序应用这些规则,从第一个规则开始,一直向下列表,直到无法应用进一步的规则。因此,合并规则的顺序更改将严重影响标记化器的性能,并导致次优标记化。
这是一个示例。假设您有一个令牌列表['N', 'VI', 'D', 'IA']
,以及两套不同的合并规则:
Set A:
N VI
D IA
NVI DIA
Set B:
D IA
NVI DIA
N VI
在对令牌列表应用集 A 时,分词器按给定顺序遵循合并规则:
['N', 'VI', 'D', 'IA'] -> ['NVI', 'D', 'IA']
(应用规则 1。)['NVI', 'D', 'IA'] -> ['NVI', 'DIA']
(应用规则 2。)['NVI', 'DIA'] -> ['NVIDIA']
(应用规则 3)
标记化的最终输出结果为['NVIDIA']
,这是我们所期望的结果。
但是,将集合 B 应用于同一令牌列表时,分词器会遇到以下问题:
['N', 'VI', 'D', 'IA'] -> ['N', 'VI', 'DIA']
(应用规则 1)。['N', 'VI', 'DIA']
(从下一个合并规则中的第一个令牌开始,无法应用更多规则,因为'NVI'
未找到。)
在这种情况下,分词器无法合并 'N'
和 'VI'
,因为合并规则 'N VI'
出现在 'NVI DIA'
中。因此,分词器会生成次优输出: ['N', 'VI', 'DIA']
,而不是['NVIDIA']
。
更改规则的顺序会改变分词器的行为,并可能降低其性能。
运行以下代码以进行分词器合并:
output_dir = './path_to_merged_tokenizer'
# Make the directory if necessary
if not os.path.exists(output_dir ):
os.makedirs(output_dir)
#Read vocab files
old_vocab = json.load(open(os.path.join('./path_to_pretrained_tokenizer', 'vocab.json')))
new_vocab = json.load(open(os.path.join('./path_to_cusotmized_tokenizer', 'vocab.json')))
next_id = old_vocab[max(old_vocab, key=lambda x: int(old_vocab[x]))] + 1
# Add words from new tokenizer
for word in new_vocab.keys():
if word not in old_vocab.keys():
old_vocab[word] = next_id
next_id += 1
# Save vocab
with open(os.path.join(output_dir , 'vocab.json'), 'w') as fp:
json.dump(old_vocab, fp, ensure_ascii=False)
old_merge_path = os.path.join('./path_to_pretrained_tokenizer', 'merges.txt')
new_merge_path = os.path.join('./path_to_cusotmized_tokenizer', 'merges.txt')
#Read merge files
with open(old_merge_path, 'r') as file:
old_merge = file.readlines()
with open(new_merge_path, 'r') as file:
new_merge = file.readlines()[1:]
#Add new merge rules, the order of merge rule has to be maintained
old_merge_set = set(old_merge)
combined_merge = old_merge + [merge_rule for merge_rule in new_merge if merge_rule not in old_merge_set]
# Save merge.txt
with open(os.path.join(output_dir , 'merges.txt'), 'w') as file:
for line in combined_merge:
file.write(line)
现在,您可以加载并测试组合的 tokenizer,并将其 tokenization 输出与预训练的 tokenizer 和自定义的单语言 tokenizer 进行比较。您应该能够观察到组合的 tokenizer 在将目标语言和英语标记化方面效果良好。
结束语
此时,您已成功自定义能够将英语和目标语言标记化的 BPE 标记器。
在 下一篇文章 中,我们将修改预训练模型的嵌入层,以便采用自定义分词器,并开始将修改后的模型与自定义分词器一起使用,以便在 NeMo 中进行持续预训练。
要开始训练多语种分词器,请首先下载并设置开源语言数据集,以整理用于训练的低资源语言数据集。NeMo 策展人 已在 GitHub 上发布。另外,您还可以通过 NeMo 微服务抢先体验 请求访问 NVIDIA NeMo Curator,以加速和简化数据管护流程。