当前位置:   article > 正文

使用Pytorch从零开始实现BERT_bert模型 pytorch 构建

bert模型 pytorch 构建

生成式建模知识回顾:
[1] 生成式建模概述
[2] Transformer ITransformer II
[3] 变分自编码器
[4] 生成对抗网络高级生成对抗网络 I高级生成对抗网络 II
[5] 自回归模型
[6] 归一化流模型
[7] 基于能量的模型
[8] 扩散模型 I, 扩散模型 II

本博文是尝试创建一个关于如何使用 PyTorch 构建 BERT 架构的完整教程。本教程的完整代码可在pytorch_bert获取。

引言

BERT 代表 Transformers 的双向编码器表示。BERT的原始论文:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding,实际上解释了您需要了解的有关 BERT 的所有内容。

老实说,互联网上有很多更好的文章解释 BERT 是什么,例如BERT Expanded: State of the art language model for NLP。读完本文后,你可能对注意力机制有一些疑问;这篇文章: Illustrated: Self-Attention 解释了注意力。

在本段中我只是想回顾一下 BERT 的思想,并更多地关注实际实现。BERT 同时解决两个任务:

  • 下一句预测(NSP);
  • 掩码语言模型(MLM)。

在这里插入图片描述

下一句话预测 (NSP)

NSP 是一个二元分类任务。输入两个句子,我们的模型应该能够预测第二个句子是否是第一个句子的真实延续。

比如有一段:

I have a cat named Tom. Tom likes to play with birds sitting on the window. They like this game not. I also have a dog. We walk together everyday.

潜在的数据集看起来像:

句子NSP类别
I have a cat named Tom. Tom likes to play with birds sitting on the windowis next
I have a cat named Tom. We walk together everydayis not next

掩码语言模型 (MLM)

掩码语言模型是预测句子中隐藏单词的任务。

例如有一句话:

Tom likes to [MASK] with birds [MASK] on the window.

该模型应该预测屏蔽词是 playsitting

构建 BERT

为了构建 BERT,我们需要制定三个步骤:

  1. 准备数据集;
  2. 建立一个模型;
  3. 建立一个训练器 (Trainer)。

在这里插入图片描述

准备数据集

对于 BERT,应该以特定的某种方式来准备数据集。我大概花了 30% 的时间和脑力来构建 BERT 模型的数据集。因此,值得用一个段落来进行讨论。

原始 BERT 使用 BooksCorpus(8 亿字)和英语维基百科(2,500M 字)进行预训练。我们使用大约 72k 字的 IMDB reviews data 数据集。

Kaggle: IMDB Dataset of 50K Movie Reviews下载数据集,并将其放在data/ 项目根目录下。

接下来,对于pytorch 的数据集和数据加载器,我们必须创建继承 torch.utils.data.Dataset 类的数据集。

class IMDBBertDataset(Dataset):
    # Define Special tokens as attributes of class
    CLS = '[CLS]'
    PAD = '[PAD]'
    SEP = '[SEP]'
    MASK = '[MASK]'
    UNK = '[UNK]'

    MASK_PERCENTAGE = 0.15  # How much words to mask

    MASKED_INDICES_COLUMN = 'masked_indices'
    TARGET_COLUMN = 'indices'
    NSP_TARGET_COLUMN = 'is_next'
    TOKEN_MASK_COLUMN = 'token_mask'

    OPTIMAL_LENGTH_PERCENTILE = 70

    def __init__(self, path, ds_from=None, ds_to=None, should_include_text=False):
        self.ds: pd.Series = pd.read_csv(path)['review']

        if ds_from is not None or ds_to is not None:
            self.ds = self.ds[ds_from:ds_to]

        self.tokenizer = get_tokenizer('basic_english')
        self.counter = Counter()
        self.vocab = None

        self.optimal_sentence_length = None
        self.should_include_text = should_include_text

        if should_include_text:
            self.columns = ['masked_sentence', self.MASKED_INDICES_COLUMN, 'sentence', self.TARGET_COLUMN,
                            self.TOKEN_MASK_COLUMN,
                            self.NSP_TARGET_COLUMN]
        else:
            self.columns = [self.MASKED_INDICES_COLUMN, self.TARGET_COLUMN, self.TOKEN_MASK_COLUMN,
                            self.NSP_TARGET_COLUMN]
        self.df = self.prepare_dataset()

    def __len__(self):
        return len(self.df)
    
    def __getitem__(self, idx):
        ...
    
    def prepare_dataset() -> pd.DataFrame:
        ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

__init__中有点奇怪的部分如下:

...
if should_include_text:
    self.columns = ['masked_sentence', self.MASKED_INDICES_COLUMN, 'sentence', self.TARGET_COLUMN,
                    self.TOKEN_MASK_COLUMN,
                    self.NSP_TARGET_COLUMN]
else:
    self.columns = [self.MASKED_INDICES_COLUMN, self.TARGET_COLUMN, self.TOKEN_MASK_COLUMN,
                    self.NSP_TARGET_COLUMN]
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们定义上面的列来创建self.df. 用should_include_text=True在数据框中包含所创建句子的文本表示。了解我们的预处理算法到底创建了什么是很有用的。

因此,should_include_text=True仅出于调试目的才需要设置。

大部分工作将在该prepare_dataset方法中完成。在该__getitem__方法中,我们准备一个训练项张量。

为了准备数据集,我们接下来要做:

  • 按句子分割数据集
  • 为 word-token 对创建词汇表 例如,{‘go’: 45}
  • 创建训练数据集
    • 在句子中添加特殊标记
    • 屏蔽句子中 15% 的单词
    • 将句子填充到预定义的长度
    • 用两个句子创建 NSP 项

我们来逐步回顾一下prepare_dataset方法的代码。

按句子分割数据集并填充词汇表

检索句子是我们在prepare_dataset方法中执行的第一个(也是最简单的)操作。这对于填充词汇表是必要的。

sentences = []  
nsp = []  
sentence_lens = []

# Split dataset on sentences
for review in self.ds:
    review_sentences = review.split('. ')
    sentences += review_sentences
    self._update_length(review_sentences, sentence_lens)
self.optimal_sentence_length = self._find_optimal_sentence_length(sentence_lens)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

请注意,我们按 . 来分割文本。但正如[devlin et al, 2018]中所述,一个句子可以有任意数量的连续文本;您可以根据需要拆分它。

如果打印sentences[:2]你会看到以下结果:

['One of the other reviewers has mentioned that after watching just 1 Oz '
 "episode you'll be hooked",
 'They are right, as this is exactly what happened with me.<br /><br />The '
 'first thing that struck me about Oz was its brutality and unflinching scenes '
 'of violence, which set in right from the word GO']
  • 1
  • 2
  • 3
  • 4
  • 5

有趣的部分在于我们如何定义句子长度:

def _find_optimal_sentence_length(self, lengths: typing.List[int]):  
    arr = np.array(lengths)  
    return int(np.percentile(arr, self.OPTIMAL_LENGTH_PERCENTILE))
  • 1
  • 2
  • 3

我们不是硬编码最大长度,而是将所有句子长度存储在列表中并计算 sentence_lens的70%。对于 50k IMDB,最佳句子长度值为 27。这意味着 70% 的句子长度小于或等于 27。

然后,我们将这些句子输入词汇表。我们对每个句子进行标记(tokenize),并用句子标记(单词)更新计数器 。

print("Create vocabulary")  
for sentence in tqdm(sentences):  
    s = self.tokenizer(sentence)  
    self.counter.update(s)  
  
self._fill_vocab()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

tokenization后的句子是其单词列表:

"My cat is Tom" -> ['my', 'cat', 'is', 'tom']
  • 1

这是打印后您应该看到的输出self.counter:

Counter({'the': 6929,
         ',': 5753,
         'and': 3409,
         'a': 3385,
         'of': 3073,
         'to': 2774,
         "'": 2692,
         '.': 2184,
         'is': 2123,
         ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

请注意,在本教程中,我们省略了数据集清理的重要步骤。这就是为什么最受欢迎的tokens是the、,、and、a等的原因。

最后,我们准备好建立我们的词汇表了。该操作被移至_fill_vocab方法:

def _fill_vocab(self):  
    # specials= argument is only in 0.12.0 version  
    # specials=[self.CLS, self.PAD, self.MASK, self.SEP, self.UNK]
    self.vocab = vocab(self.counter, min_freq=2)  

    # 0.11.0 uses this approach to insert specials  
    self.vocab.insert_token(self.CLS, 0)  
    self.vocab.insert_token(self.PAD, 1)  
    self.vocab.insert_token(self.MASK, 2)  
    self.vocab.insert_token(self.SEP, 3)  
    self.vocab.insert_token(self.UNK, 4)  
    self.vocab.set_default_index(4)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在本教程中,我们将仅将在数据集中出现 2 次或多次的单词添加到词汇表中。创建词汇表后,我们向词汇表添加特殊标记并将[UNK]标记设置为默认标记。

工作完成了一半

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/345008
推荐阅读
相关标签