训练自己的HF分词器

这篇博文是系列文章的第一部分,我们想使用transformer模型创建产品名称生成器。

几个星期以来,我一直在研究 Huggingface 中的不同模型和替代方案,以训练文本生成模型。我们有一份产品清单及其描述,我们的目标是获得产品的名称。我用 Tensorflow 中的 Transformer 模型以及 T5 摘要器做了一些实验。最后,为了深化 Huggingface 转换器的使用,我决定用一种稍微复杂一点的方法解决这个问题,即编码器-解码器模型。也许这不是最好的选择,但我想学习有关 Huggingface Transformers 的新知识。在本系列的下一篇文章中,我们将更深入地介绍这个概念。

在这里,在第一部分中,我们将展示如何从头开始训练分词器以及如何使用 Masked Language Modeling 技术创建 RoBERTa 模型。这个个性化模型将成为我们未来编码器-解码器模型的基础模型。

1、我们自己的解决方案

对于我们的实验,我们将从头开始训练 RoBERTa 模型,它将成为未来模型的编码器和解码器。但我们的领域非常具体,关于衣服、形状、颜色等的单词和概念……因此,我们有兴趣定义我们自己的分词器,这些分词器是根据我们特定的词汇创建的,避免包含来自其他领域的更常见的单词或与我们的最终目的无关的用例。

可以用两个主要步骤来描述我们的训练阶段:

  • 使用与 RoBERTa 相同的特殊标记创建和训练字节级、字节对编码分词器
  • 使用掩码语言建模 (MLM) 从头开始​​训练 RoBERTa 模型。

代码可在此 Github 存储库中找到。在这篇文章中,我们将仅展示主要代码部分及其一些说明。

2、数据集

如前所述,我们的数据集包含约 31,000 件商品,涉及来自重要零售商的服装,包括较长的产品描述和较短的产品名称(我们的目标变量)。首先,我们执行探索性数据分析,可以观察到具有异常值的行数很少。单词数看起来像左偏分布,75% 的行在 50-60 个单词范围内,最多约 125 个单词。目标变量包含约 3 到 6 个单词。

2、训练分词器

斯坦福 NLP 小组将分词器(tokenizer)定义为:

“给定一个字符序列和一个定义的文档单元,分词器就是将其切成碎片(称为标记)的任务,同时可能丢弃某些字符,例如标点符号。”

分词器将一串字符(通常是文本句子)分解为标记(标记的整数表示),通常是通过查找空格(制表符、空格、换行符)来实现的。它通常将一个句子拆分成单词,但也有许多选项,例如子单词。

“我们将使用字节级字节对编码标记器,字节对编码 (BPE) 是一种简单的数据压缩形式,其中最常见的连续字节对数据被替换为未出现在该数据中的字节”,     —— 字节对编码,维基百科定义。

这种方法的好处是它将从单个字符的字母表开始构建词汇表,因此所有单词都可以分解为标记。我们可以避免出现未知 (UNK) 标记。

有关标记器的详细说明,请参阅 Huggingface 文档

要训练标记器,我们需要将数据集保存在一堆文本文件中。 我们为每个描述值创建一个纯文本文件。

# Store values in a dataframe column (Series object) to files, one file per record
def column_to_files(column, prefix, txt_files_dir):
    # The prefix is a unique ID to avoid to overwrite a text file
    i=prefix
    #For every value in the df, with just one column
    for row in column.to_list():
      # Create the filename using the prefix ID
      file_name = os.path.join(txt_files_dir, str(i)+'.txt')
      try:
        # Create the file and write the column text to it
        f = open(file_name, 'wb')
        f.write(row.encode('utf-8'))
        f.close()
      except Exception as e:  #catch exceptions(for eg. empty rows)
        print(row, e) 
      i+=1
    # Return the last ID
    return i
# Get the training data
data = train_df["description"]
# Removing the end of line character \n
data = data.replace("\n"," ")
# Set the ID to 0
prefix=0
# Create a file for every description value
prefix = column_to_files(data, prefix, txt_files_dir)
# Print the last ID
print(prefix)
# Get the test data
data = test_df["description"]
# Removing the end of line character \n
data = data.replace("\n"," ")
print(len(data))
# Create a file for every description value
prefix = column_to_files(data, prefix, txt_files_dir)
print(prefix)

现在,我们可以在创建的包含词汇表的文本文件上训练我们的分词器,我们需要指定词汇表大小、要包含的标记的最小频率以及特殊标记。我们选择词汇表大小为 8,192,最小频率为 2(你可以根据最大词汇表大小调整此值)。特殊分词取决于模型,对于 RoBERTa,我们包含一个候选名单:

  • <s>或bos,句子的开始
  • </s>或eos,句子的结尾
  • <pad>填充分词
  • <unk>未知分词
  • <mask>掩码分词

样本数量很少,标记器训练速度非常快。现在我们可以将标记器保存到磁盘,稍后我们将使用它来训练语言模型。

from tokenizers import ByteLevelBPETokenizer
from tokenizers.processors import BertProcessing
from pathlib import Path

%%time 
paths = [str(x) for x in Path(".").glob("text_split/*.txt")]

# Initialize a tokenizer
tokenizer = ByteLevelBPETokenizer(lowercase=True)

# Customize training
tokenizer.train(files=paths, vocab_size=8192, min_frequency=2,
                show_progress=True,
                special_tokens=[
                                "<s>",
                                "<pad>",
                                "</s>",
                                "<unk>",
                                "<mask>",
])
#Save the Tokenizer to disk
tokenizer.save_model(tokenizer_folder)

我们现在有一个 vocab.json,它是按频率排序的最常见标记的列表,用于将标记转换为 ID,还有一个将文本映射到标记的 merges.txt 文件。

# vocab.json
{
    "<s>": 0,
    "<pad>": 1,
    "</s>": 2,
    "<unk>": 3,
    "<mask>": 4,
    "!": 5,
    "\"": 6,
    "#": 7,
    "$": 8,
    "%": 9,
    "&": 10,
    "'": 11,
    "(": 12,
    ")": 13,
    # ...
}
 
# merges.txt
l a
Ġ k
o n
Ġ la
t a
Ġ e
Ġ d
Ġ p
# ...

最棒的是,我们的 tokenizer 针对我们非常具体的词汇进行了优化,而不是针对常见英语进行训练的通用 tokenizer。

我们可以使用这两个文件实例化我们的 tokenizer,并使用来自我们数据集的一些文本对其进行测试。

# Create the tokenizer using vocab.json and mrege.txt files
tokenizer = ByteLevelBPETokenizer(
    os.path.abspath(os.path.join(tokenizer_folder,'vocab.json')),
    os.path.abspath(os.path.join(tokenizer_folder,'merges.txt'))
)
# Prepare the tokenizer
tokenizer._tokenizer.post_processor = BertProcessing(
    ("</s>", tokenizer.token_to_id("</s>")),
    ("<s>", tokenizer.token_to_id("<s>")),
)
tokenizer.enable_truncation(max_length=512)
# Test the tokenizer
tokenizer.encode("knit midi dress with vneckline straps.")
# Show the tokens created
tokenizer.encode("knit midi dress with vneckline straps.").tokens

稍后,在训练模型时,我们将使用 from_pretrained 方法来初始化分词器。

3、从零训练语言模型

我们将训练一个 RoBERTa 模型,该模型与 BERT 类似,但有一些变化(查看文档了解更多详细信息)。总地来说:

“它建立在 BERT 的基础上并修改了关键超参数,删除了下一个句子的预训练目标,并使用更大的小批量和学习率进行训练”  ——HF 的 RoBERTa 文档

由于该模型与 BERT 类似,我们将在 Masked Language Modeling 任务上对其进行训练。它涉及屏蔽部分输入,大约 10-20% 的标记,然后学习一个模型来预测缺失的标记。MLM 通常用于预训练任务中,使模型有机会从未标记的数据中学习文本模式。它可以针对特定的下游任务进行微调。主要的好处是我们不需要标记数据(很难获得),不需要人工标记者标记文本来预测缺失值。

我们将从头开始训练模型,而不是从预训练模型开始。我们为 RoBERTa 模型创建模型配置,设置主要参数:

  • 词汇量
  • 注意头
  • 隐藏层

最后,让我们使用配置文件初始化模型。由于我们从头开始训练,因此我们从定义模型架构的配置进行初始化,但不恢复先前训练的权重。权重将被随机初始化:

from transformers import RobertaConfig
from transformers import RobertaForMaskedLM

# Set a configuration for our RoBERTa model
config = RobertaConfig(
    vocab_size=8192,
    max_position_embeddings=514,
    num_attention_heads=12,
    num_hidden_layers=6,
    type_vocab_size=1,
)
# Initialize the model from a configuration without pretrained weights
model = RobertaForMaskedLM(config=config)
print('Num parameters: ',model.num_parameters())

然后我们使用上一步中训练并保存的标记器重新创建分词器。我们将使用 RoBERTaTokenizerFast 对象和 from_pretrained 方法来初始化分词器。

from transformers import RobertaTokenizerFast
# Create the tokenizer from a trained one
tokenizer = RobertaTokenizerFast.from_pretrained(tokenizer_folder, max_len=MAX_LEN)

4、构建训练数据集

我们将构建一个 Pytorch 数据集,对 Dataset 类进行子类化。 CustomDataset 接收一个包含描述变量值的 Pandas Series 和用于编码这些值的 tokenizer。Dataset 会返回 Series 中每个产品描述的 token 列表。

为了在训练期间评估模型,我们将生成一个训练数据集和一个评估数据集。

class CustomDataset(Dataset):
    def __init__(self, df, tokenizer):
        # or use the RobertaTokenizer from `transformers` directly.
        self.examples = []
        # For every value in the dataframe 
        for example in df.values:
            # 
            x=tokenizer.encode_plus(example, max_length = MAX_LEN, truncation=True, padding=True)
            self.examples += [x.input_ids]

    def __len__(self):
        return len(self.examples)

    def __getitem__(self, i):
        # We’ll pad at the batch level.
        return torch.tensor(self.examples[i])
      
# Create the train and evaluation dataset
train_dataset = CustomDataset(train_df['description'], tokenizer)
eval_dataset = CustomDataset(test_df['description'], tokenizer)

一旦我们有了数据集,数据整理器(Data Collator)将帮助我们屏蔽训练文本。这只是一个小助手,它将帮助我们将数据集的不同样本批量处理成一个对象,PyTorch 知道如何对其进行反向传播。

数据整理器是使用数据集元素列表作为输入形成批处理的对象,并可能应用一些处理,如填充或随机屏蔽。 DataCollat​​orForLanguageModeling 方法允许我们设置随机屏蔽输入中的标记的概率。

from transformers import DataCollatorForLanguageModeling

# Define the Data Collator
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer, mlm=True, mlm_probability=0.15
)

5、初始化并训练我们的 Trainer

当我们想要训练一个 Transformer 模型时,基本方法是创建一个 Trainer 类,该类提供用于功能完整训练的 API 并包含基本的训练循环。

首先,我们定义训练参数,它们有很多,但更相关的是:

  • output_dir,模型工件将保存在哪里
  • evaluation_strategy,验证损失的计算时间
  • num_train_epochs
  • per_device_train_batch_size 是训练的批处理大小
  • per_device_eval_batch_size 是评估的批处理大小
  • learning_rate,初始化为 1e-4
  • weight_decay,0.01

最后,我们使用参数、输入数据集、评估数据集和定义的数据整理器创建一个 Trainer 对象。现在我们准备好训练我们的模型了。

from transformers import Trainer, TrainingArguments
# Define the training arguments
training_args = TrainingArguments(
    output_dir=model_folder,
    overwrite_output_dir=True,
    evaluation_strategy = 'epoch',
    num_train_epochs=TRAIN_EPOCHS,
    learning_rate=LEARNING_RATE,
    weight_decay=WEIGHT_DECAY,
    per_device_train_batch_size=TRAIN_BATCH_SIZE,
    per_device_eval_batch_size=VALID_BATCH_SIZE,
    save_steps=8192,
    #eval_steps=4096,
    save_total_limit=1,
)
# Create the trainer for our model
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    #prediction_loss_only=True,
)
# Train the model
trainer.train()

因此,我们可以观察训练过程中损失是如何减少的。

最后,我们保存模型和标记器,以便它们可以为未来的下游任务(我们的编码器-解码器模型)恢复。

6、使用管道检查训练后的模型

仅查看训练和评估损失的下降是不够的,我们希望应用我们的模型来检查我们的语言模型是否正在学习任何有趣的东西。一种简单的方法是通过 FillMaskPipeline

管道是标记器和模型的简单包装器。我们可以使用“填充掩码”管道,在其中输入包含掩码标记( <mask>)的序列,它会返回最可能填充的序列及其概率的列表。但仅适用于只有一个标记被掩码的输入(请参阅 Huggingface 官方文档)。

from transformers import pipeline
# Create a Fill mask pipeline
fill_mask = pipeline(
    "fill-mask",
    model=model_folder,
    tokenizer=tokenizer_folder
)
# Test some examples
# knit midi dress with vneckline
# =>
fill_mask("midi <mask> with vneckline.")
# The test text: Round neck sweater with long sleeves
fill_mask("Round neck sweater with <mask> sleeves.")

这就是第一部分的全部内容,我希望你能从中找到灵感,为未来的项目提供灵感,希望几天后我们能回来继续第二部分。

代码可在此 Github 存储库中找到。


原文链接:Create a Tokenizer and Train a Huggingface RoBERTa Model from Scratch

BimAnt翻译整理,转载请标明出处