赞
踩
生成式建模知识回顾:
[1] 生成式建模概述
[2] Transformer I,Transformer 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 是一个二元分类任务。输入两个句子,我们的模型应该能够预测第二个句子是否是第一个句子的真实延续。
比如有一段:
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 window | is next |
I have a cat named Tom. We walk together everyday | is not next |
掩码语言模型是预测句子中隐藏单词的任务。
例如有一句话:
Tom likes to [MASK] with birds [MASK] on the window.
该模型应该预测屏蔽词是 play 和 sitting。
为了构建 BERT,我们需要制定三个步骤:
对于 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: ...
__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]
...
我们定义上面的列来创建self.df. 用should_include_text=True在数据框中包含所创建句子的文本表示。了解我们的预处理算法到底创建了什么是很有用的。
因此,should_include_text=True仅出于调试目的才需要设置。
大部分工作将在该prepare_dataset方法中完成。在该__getitem__方法中,我们准备一个训练项张量。
为了准备数据集,我们接下来要做:
我们来逐步回顾一下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)
请注意,我们按 . 来分割文本。但正如[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']
有趣的部分在于我们如何定义句子长度:
def _find_optimal_sentence_length(self, lengths: typing.List[int]):
arr = np.array(lengths)
return int(np.percentile(arr, self.OPTIMAL_LENGTH_PERCENTILE))
我们不是硬编码最大长度,而是将所有句子长度存储在列表中并计算 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()
tokenization后的句子是其单词列表:
"My cat is Tom" -> ['my', 'cat', 'is', 'tom']
这是打印后您应该看到的输出self.counter:
Counter({'the': 6929,
',': 5753,
'and': 3409,
'a': 3385,
'of': 3073,
'to': 2774,
"'": 2692,
'.': 2184,
'is': 2123,
...
请注意,在本教程中,我们省略了数据集清理的重要步骤。这就是为什么最受欢迎的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)
在本教程中,我们将仅将在数据集中出现 2 次或多次的单词添加到词汇表中。创建词汇表后,我们向词汇表添加特殊标记并将[UNK]标记设置为默认标记。
工作完成了一半
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。