新知一下
海量新知
5 9 7 6 2 3 5

从零开始训练BERT模型

磐创AI | 人工智能领域前沿自媒体。 2021/10/13 09:51

作者 | James Briggs

编译 | VK

来源 | Towards Data Science

新知达人, 从零开始训练BERT模型

一个出现并主宰了自然语言处理(NLP)世界的模型BERT,标志着语言模型的一个新时代。

对于那些以前可能没有使用过transformers模型(例如BERT是什么)的人,过程看起来有点像这样:

  • pip install transformers

  • 初始化预训练过的transformer模型-从预训练开始。

  • 在一些数据上进行测试。

  • 也许可以对模型进行微调(再进行一些训练)。

现在,这是一个很好的方法,但是如果我们只是这样做的话,我们缺乏对创建我们自己的transformer模型的理解。

而且,如果我们不能创建自己的transformer模型,我们必须依靠一个经过预训练的模型来解决我们的问题,但情况并非总是如此:

新知达人, 从零开始训练BERT模型

因此,在本文中,我们将探讨构建我们自己的transformer模型所必须采取的步骤,特别是进一步开发的BERT版本,称为RoBERTa。


概述

这个过程有几个步骤,所以在开始之前,让我们先总结一下我们需要做什么。总共有四个关键部分:

  • 获取数据

  • 构建tokenizer

  • 创建输入管道

  • 训练模型

一旦我们完成了这些部分中的每一部分,我们将采用我们已经构建的tokenizer和模型,并保存它们,以便我们能够以我们通常使用的方式使用它们。


获取数据

与任何机器学习项目一样,我们需要数据。就训练transformer模型的数据而言,我们几乎可以使用任何文本数据。

使用HuggingFace的数据集库下载 OSCAR 数据集的视频演练:https://youtu.be/GhGUZrcB-WM

而且,如果说我们在互联网上有很多东西的话,那就是非结构化文本数据。

OSCAR数据集是从互联网上获取的文本领域中最大的数据集之一。

OSCAR数据集拥有大量不同的语言,我们可以将BERT应用于一些不太常用的语言,如泰卢固语或纳瓦霍语。

我们将请一名意大利人评估我们的意大利语Bert模型。

因此,要下载奥斯卡数据集的意大利部分,我们将使用HuggingFace的数据集库——我们可以使用pip安装数据集安装它。然后,我们使用以下代码下载 OSCAR_IT :

from datasets import load_dataset

加载OSCAR数据集的意大利部分。这是一个巨大的数据集,所以下载可能需要很长时间:

dataset = load_dataset('oscar''unshuffled_deduplicated_it')

Reusing dataset oscar (C:UsersJames.cachehuggingfacedatasetsoscarunshuffled_deduplicated_it1.0.0e4f06cecc7ae02f7adf85640b4019bf476d44453f251a1d84aebae28b0f8d51d)

让我们看看dataset对象。

数据集是一个包含单一列数据集的字典。

dataset

DatasetDict({

    train: Dataset({

        features: ['id''text'],

        num_rows: 28522082

    })

})

我们可以通过train访问数据集本身。从这里我们可以查看更多信息,比如数据集的行数和结构。

dataset['train']

Dataset({

    features: ['id''text'],

    num_rows: 28522082

})

dataset['train'].features

{'id': Value(dtype='int64', id=None), 'text': Value(dtype='string', id=None)}

让我们来看一个例子:

dataset['train'][0]

{'id'0,

 'text'"La estrazione numero 48 del 10 e LOTTO ogni 5 minuti e' avvenuta sabato 15 settembre 2018 alle ore 04:00 a Roma, nel Centro Elaborazione Dati della Lottomatica Italia (ora GTech SpA), con la supervisione della Amministrazione Autonoma dei Monopoli di Stato (AAMS), incaricata di vigilare sulla regolarità delle operazioni di sorteggio.nIl Montepremi della 48ª estrazione viene ripartito tra i vincitori delle singole categorie di premio.nRicorda di controllare il Numero ORO 53. E, se lo hai giocato, anche il DOPPIO ORO 53 e 66. Se indovini puoi vincere premi più ricchi.nIl nostro sito web impiega cookies per migliorare la navigazione del visitatore. L’utente è consapevole che, continuando a visitare il nostro sito web, accetta l’utilizzo dei cookies Accetto Informazionin(C) Copyright 2013-2017 10elotto.biz | Il presente sito è da considerarsi un sito indipendente, NON collegato alla rete ufficiale Gtech SpA."}

很好,现在让我们以一种格式存储数据,以便在构建tokenizer时使用。我们需要创建一组纯文本文件,其中只包含数据集中的文本功能,我们将使用换行符拆分每个示例。

from tqdm.auto import tqdm

text_data = []

file_count = 0

for sample in tqdm(dataset['train']):

    sample = sample['text'].replace('n''')

    text_data.append(sample)

    if len(text_data) == 10_000:

        # 一旦我们得到了10K个token,保存到文件

        with open(f'../../data/text/oscar_it/text_{file_count}.txt''w', encoding='utf-8'as fp:

            fp.write('n'.join(text_data))

        text_data = []

        file_count += 1

# 在保存10K个token后,我们将有~2082个剩余样本,我们现在也保存了这些

with open(f'../../data/text/oscar_it/text_{file_count}.txt''w', encoding='utf-8'as fp:

    fp.write('n'.join(text_data))

100%|██████████| 28522082/28522082 [33:32<00:0014173.48it/s]

在我们的data/text/oscar_it目录中,我们将发现:

一个屏幕截图, 表示纯文本数据:

新知达人, 从零开始训练BERT模型


构建tokenizer

接下来是tokenizer!当使用transformers时,我们通常会加载一个tokenizer,以及相应的transformer模型——tokenizer是流程中的关键组件。

https://youtu.be/JIeAB8vvBQo

在构建tokenizer时,我们将向其提供所有OSCAR数据,指定词汇表大小(tokenizer中的token数)和任何特殊token。

现在,RoBERTa特殊token如下所示:

Token Use

句子开始

句子结束

未知词token

填充token

掩码token

因此,我们确保将它们包含在tokenizer的train方法调用的special_tokens参数中。

获取oscar_it目录中每个文件的路径列表。

from pathlib import Path

paths = [str(x) for x in Path('../../data/text/oscar_it').glob('**/*.txt')]

现在我们继续训练 tokenizer 。我们使用BPE标记器。这使我们能够从单个字节组成的字母表构建词汇表,这意味着所有单词都可以分解为token。

from tokenizers import ByteLevelBPETokenizer

tokenizer = ByteLevelBPETokenizer()

tokenizer.train(files=paths[:5], vocab_size=30_522, min_frequency=2,

                special_tokens=['<s>''<pad>''</s>''<unk>''<mask>'])

我们的tokenizer现在已准备就绪,我们可以将其保存为文件供以后使用:

import os

os.mkdir('./filiberto')

tokenizer.save_model('filiberto')

['./filibertovocab.json''./filibertomerges.txt']

现在,我们有两个文件定义了新的FiliBERTotokenizer:

  • merges.txt-执行文本到token的初始映射

  • vocab.json-将token映射到token ID

有了这些,我们可以继续初始化tokenizer,这样我们就可以像使用任何其他来自预训练tokenizer的tokenizer一样使用它。

初始化tokenizer

我们首先使用之前构建的两个文件初始化tokenizer-使用一个简单的from_pretrained:

from transformers import RobertaTokenizer

# 初始化tokenizer

tokenizer = RobertaTokenizer.from_pretrained('filiberto', max_len=512)

现在我们的tokenizer已经准备好了,我们可以尝试用它来编码一些文本。编码时,我们使用两种通常使用的方法:encode和encode_batch。

# 在一个简单的句子上测试我们的tokenizer

tokens = tokenizer('ciao, come va?')

print(tokens)

{'input_ids': [01683416488611352], 'attention_mask': [1111111]}

tokens.input_ids

[01683416488611352]

从编码对象 tokens 中,我们将提取用于FiliBERTo的输入ID和 attention_mask 张量。


创建输入管道

我们训练过程的输入管道是整个过程中更复杂的部分。它包括我们获取原始的 OSCAR 训练数据,对其进行转换,并将其加载到数据加载器中,以备训练。

https://youtu.be/heTYbpr9mD8

准备数据

我们将从一个样本开始,并完成准备逻辑。

首先,我们需要打开我们的文件-与我们先前保存为.txt文件的文件相同。我们根据换行符n拆分每个示例,因为这表示单个示例。

with open('../../data/text/oscar_it/text_0.txt''r', encoding='utf-8'as fp:

    lines = fp.read().split('n')

然后,我们使用tokenizer对数据进行编码,确保包含诸如 max_length 、 padding 和 truncation 等关键参数。

batch = tokenizer(lines, max_length=512, padding='max_length', truncation=True)

len(batch)

10000

现在我们可以开始创建张量了——我们将通过屏蔽语言建模(MLM)来训练我们的模型。所以,我们需要三个张量:

  • input_id-我们的 token_ids ,使用掩码token

    屏蔽约15%的token。

  • 注意力掩码-1和0的张量,标记“真实”标记/填充标记的位置-用于注意力计算。

  • 标签-我们的token ID。

如果你不熟悉MLM,我已经在这里解释过了:https://towardsdatascience.com/masked-language-modelling-with-bert-7d49793e5d2c。

我们的注意力掩码和标签张量只是从我们的batch中提取出来的。并且,我们屏蔽了约15%的token-为它们分配token ID 3。

import torch

labels = torch.tensor([x.ids for x in batch])

mask = torch.tensor([x.attention_mask for x in batch])

# 复制标签张量,这将是input_ids

input_ids = labels.detach().clone()

# 创建与input_ids相同的随机浮点数数组

rand = torch.rand(input_ids.shape)

# 屏蔽15%的token

mask_arr = (rand < .15) * (input_ids != 0) * (input_ids != 1) * (input_ids != 2)

# 循环input_ids张量中的每一行(不能并行执行)

for i in range(input_ids.shape[0]):

    # 获取掩码位置的索引

    selection = torch.flatten(mask_arr[i].nonzero()).tolist()

    # 屏蔽

    input_ids[i, selection] = 3  # our custom [MASK] token == 3

我们有10000个序列,每个包含512个token。

input_ids.shape

torch.Size([10000512])

我们可以在这里看到特殊的token, 1是我们的[CLS] token, 2是我们的[SEP] token, 3是我们的[MASK] token,最后我们有两个0 -或[PAD] - token。

input_ids[0][:200]

tensor([    1,   69318623,  1358,  7752,     3,  1056,   280,     3,  6321,

          776,     3,  2145,   280,    1110205,  3778,  1266,     3,  1197,

            3,  114210293,    30,   552,     3,  1340,    16,   385,     3,

          458,  9777,  5942,   37625475,  2870,  1201,   391,  2691,   421,

        1792716996,   739,     3,     322814,   376,  795017824,   980,

          43518388,  1475,     3,     3,   391,    3724909,   739,  2689,

        27869,   275,  5803,   625,   77013459,   483,  4779,   27512870,

          532,    18,   680,  386724138,   376,  77521763018623,  1134,

         8882,   269,   431,   28712450,     3,  8041,  6056,   275,  5286,

           1811755,     3,   275,  6161,   31710528,     3,     313181,

           18,   458,     3,   372,   456,  215012054,    16,     3,   317,

         6122,  5324,  3329,   570,  159413181,   28014634,    18,   763,

            3,  6323,  2484,  6544,  5085,   469,  9106,    18,   680,     3,

          842,  151825737,  3653,   303,  3300,   306,  3063,   292,     3,

           18,   381,   330,  2872,   343,  4722,     3,    1616848,   267,

         5216,   317,  1009,   842,  1518,    16,     3,   338,   330,  2757,

          435,  36532708110965,    12,    39,    13,     3,  1865,    17,

         5580,  1056,   992,   363,     3,   360,    94,  1182,   589,  1729,

            3,     3,   35112863,   300,     3,  5240,     3,     310799,

          480,  2261,     3,   42114591,     3,    18,     2,     0,     0])

在最后的输出中,我们可以看到部分编码的输入张量。第一个token ID是1-即[CLS] token。在张量周围,我们有几个3个token ID-这些是我们新添加的[MASK] token。

构建数据加载器

接下来,我们定义Dataset类——我们使用它初始化三个编码的张量,即PyTorch.utils.data.Dataset对象。

encodings = {'input_ids': input_ids, 'attention_mask': mask, 'labels': labels}

class Dataset(torch.utils.data.Dataset):

    def __init__(self, encodings):

        # 存储编码

        self.encodings = encodings

    def __len__(self):

        # 返回样本的数量

        return self.encodings['input_ids'].shape[0]

    def __getitem__(self, i):

        # 返回 input_ids, attention_mask 和 labels的字典

        return {key: tensor[i] for key, tensor in self.encodings.items()}

接下来,我们初始化数据集。

dataset = Dataset(encodings)

初始化数据加载器,它将在训练期间将数据加载到模型中。

loader = torch.utils.data.DataLoader(dataset, batch_size=16, shuffle=True)

最后,我们的数据集被加载到一个PyTorch DataLoader对象中,我们在训练期间使用该对象将数据加载到模型中。


训练模型

我们需要两样东西来训练,我们的数据加载器和模型。我们有数据加载器,但没有模型。

https://youtu.be/35Pdoyi6ZoQ

初始化模型

对于训练,我们需要一个原始的(不是预训练的)BertlmHead模型。为了创建它,我们首先需要创建一个RoBERTa config对象来描述我们想要初始化FiliBERTo的参数。

from transformers import RobertaConfig

config = RobertaConfig(

    vocab_size=30_522,  # 我们将其与标记器vocab_size对齐

    max_position_embeddings=514,

    hidden_size=768,

    num_attention_heads=12,

    num_hidden_layers=6,

    type_vocab_size=1

)

然后,我们导入并初始化带有语言建模(LM)头的RoBERTa模型。

from transformers import RobertaForMaskedLM

model = RobertaForMaskedLM(config)

训练准备

在进入我们的训练循环之前,我们需要设置一些东西。首先,我们设置GPU/CPU使用率。然后激活模型的训练模式,最后初始化优化器。

设置GPU / CPU使用率。

device = torch.device('cuda'if torch.cuda.is_available() else torch.device('cpu')

# 然后将我们的模型移到选定的设备上

model.to(device)

激活我们的模型的训练模式,并初始化我们的优化器(Adam带有加权衰减-减少过拟合的机会)。

from transformers import AdamW

# 激活训练模式

model.train()

# 初始化优化器

optim = AdamW(model.parameters(), lr=1e-4)

训练

终于-训练时间到了!

epochs = 2

for epoch in range(epochs):

    # 使用TQDM和dataloader设置循环

    loop = tqdm(loader, leave=True)

    for batch in loop:

        # 初始化计算的梯度(从prev步骤)

        optim.zero_grad()

        # 训练所需的所有批

        input_ids = batch['input_ids'].to(device)

        attention_mask = batch['attention_mask'].to(device)

        labels = batch['labels'].to(device)

        # 处理

        outputs = model(input_ids, attention_mask=attention_mask,

                        labels=labels)

        # 提取损失

        loss = outputs.loss

        # 计算每个需要更新的参数的损失

        loss.backward()

        # 更新参数

        optim.step()

        # 打印相关信息到进度条

        loop.set_description(f'Epoch {epoch}')

        loop.set_postfix(loss=loss.item())

Epoch 0100%|██████████| 12500/12500 [1:29:47<00:00,  2.32it/s, loss=0.358]

Epoch 1100%|██████████| 12500/12500 [1:22:20<00:00,  2.53it/s, loss=0.31]

model.save_pretrained('./filiberto')  # 别忘了保存filiBERTo!

如果我们前往Tensorboard,我们会发现我们的损失随着时间的推移-看起来很有希望。

新知达人, 从零开始训练BERT模型


测试

现在是测试的时候了。我们建立了LML管道,并请劳拉评估结果。你可以在22:44在此处观看视频回顾:

https://youtu.be/35Pdoyi6ZoQ

我们首先使用'fill mask'参数初始化管道对象。然后开始测试我们的模型,如下所示:

from transformers import pipeline

fill = pipeline('fill-mask', model='filiberto', tokenizer='filiberto')

Some weights of RobertaModel were not initialized from the model checkpoint at filiberto and are newly initialized: ['roberta.pooler.dense.weight''roberta.pooler.dense.bias']

You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.

fill(f'ciao {fill.tokenizer.mask_token} va?')

[{'sequence''<s>ciao come va?</s>',

  'score'0.33601945638656616,

  'token'482,

  'token_str''Ġcome'},

 {'sequence''<s>ciao, va?</s>',

  'score'0.13736604154109955,

  'token'16,

  'token_str'','},

 {'sequence''<s>ciao mi va?</s>',

  'score'0.05658061057329178,

  'token'474,

  'token_str''Ġmi'},

 {'sequence''<s>ciao chi va?</s>',

  'score'0.047595467418432236,

  'token'586,

  'token_str''Ġchi'},

 {'sequence''<s>ciao ci va?</s>',

  'score'0.03684385493397713,

  'token'435,

  'token_str''Ġci'}]

ciao  come va ?”是正确的答案!这是我的意大利语中最高级的了——接下来,让我们把它交给劳拉吧。

我们从“ buongiorno, come va? ”-或“ good day, how are you? ?”-开始:

fill(f'buongiorno, {fill.tokenizer.mask_token} va?')

[{'sequence''<s>buongiorno, chi va?</s>',

  'score'0.299,

  'token'586,

  'token_str''Ġchi'},

 {'sequence''<s>buongiorno, come va?</s>',

  'score'0.245,

  'token'482,

  'token_str''Ġcome'},

 {'sequence''<s>buongiorno, cosa va?</s>',

  'score'0.116,

  'token'1021,

  'token_str''Ġcosa'},

 {'sequence''<s>buongiorno, non va?</s>',

  'score'0.041,

  'token'382,

  'token_str''Ġnon'},

 {'sequence''<s>buongiorno, che va?</s>',

  'score'0.037,

  'token'313,

  'token_str''Ġche'}]

第一个答案,“buongiorno,chi-va?”的意思是“你好,谁在那里?”-答案不对。但是,我们的第二个答案是正确的!

接下来是一个稍微难一点的短语,“ ciao, dove ci incontriamo oggi pomeriggio? ”-或者“ hi, where are we going to meet this afternoon?

fill(f'ciao, dove ci {fill.tokenizer.mask_token} oggi pomeriggio? ')

[{'sequence''<s>ciao, dove ci vediamo oggi pomeriggio? </s>',

  'score'0.400,

  'token'7105,

  'token_str''Ġvediamo'},

 {'sequence''<s>ciao, dove ci incontriamo oggi pomeriggio? </s>',

  'score'0.118,

  'token'27211,

  'token_str''Ġincontriamo'},

 {'sequence''<s>ciao, dove ci siamo oggi pomeriggio? </s>',

  'score'0.087,

  'token'1550,

  'token_str''Ġsiamo'},

 {'sequence''<s>ciao, dove ci troviamo oggi pomeriggio? </s>',

  'score'0.048,

  'token'5748,

  'token_str''Ġtroviamo'},

 {'sequence''<s>ciao, dove ci ritroviamo oggi pomeriggio? </s>',

  'score'0.046,

  'token'22070,

  'token_str''Ġritroviamo'}]

我们得到了一些更积极的结果:

✅ "hi, where do we see each other this afternoon?"

✅ "hi, where do we meet this afternoon?"

❌ "hi, where here we are this afternoon?"

✅ "hi, where are we meeting this afternoon?"

✅ "hi, where do we meet this afternoon?"

最后,还有一句更难的话,“ cosa sarebbe successo se avessimo scelto un altro giorno? ”——或者“ what would have happened if we had chosen another day? ”

fill(f'cosa sarebbe successo se {fill.tokenizer.mask_token} scelto un altro giorno?')

[{'sequence''<s>cosa sarebbe successo se avesse scelto un altro giorno?</s>',

  'score'0.251,

  'token'6691,

  'token_str''Ġavesse'},

 {'sequence''<s>cosa sarebbe successo se avessi scelto un altro giorno?</s>',

  'score'0.241,

  'token'12574,

  'token_str''Ġavessi'},

 {'sequence''<s>cosa sarebbe successo se avessero scelto un altro giorno?</s>',

  'score'0.217,

  'token'14193,

  'token_str''Ġavessero'},

 {'sequence''<s>cosa sarebbe successo se avete scelto un altro giorno?</s>',

  'score'0.081,

  'token'3609,

  'token_str''Ġavete'},

 {'sequence''<s>cosa sarebbe successo se venisse scelto un altro giorno?</s>',

  'score'0.042,

  'token'17216,

  'token_str''Ġvenisse'}]

我们在这里也给出了一些更好的答案:

✅ "what would have happened if we had chosen another day?"

✅ "what would have happened if I had chosen another day?"

✅ "what would have happened if they had chosen another day?"

✅ "what would have happened if you had chosen another day?"

❌ "what would have happened if another day was chosen?"

总的来说,我们的模型似乎通过了劳拉的测试——现在我们有了一个称职的意大利语模型,名为FiliBERTo!

这就是这次从头开始训练Bert模型的演练!

更多“BERT”相关内容

更多“BERT”相关内容

新知精选

更多新知精选