Loading... <div class="tip inlineBlock share"> 译文链接: https://neptune.ai/blog/how-to-code-bert-using-pytorch-tutorial * 代码链接: 论文链接: https://arxiv.org/abs/1810.04805 * 代码链接: https://github.com/google-research/bert * 其它实现: https://github.com/iioSnail/chaotic-transformer-tutorials * 代码解读: https://blog.51cto.com/u_11466419/5782638 视频讲解: </div> ## 前言 如果你是一个自然语言处理的爱好者,那么你可能听说过 BERT。在这篇文章中,我们将探索 BERT:它是什么?它是如何工作的?并学习如何使用 PyTorch 实现它。 2018 年,谷歌发表了一篇名为“[Pre-training of deep bidirectional transformers for language understanding](https://arxiv.org/pdf/1810.04805.pdf)”的论文。在这篇论文中,他们介绍了一种名为 BERT(Bidirectional Encoder Representation with Transformers)的语言模型,该模型在一些列任务中取得了最先进的性能。 * Question-Answering, Natural Language Inference, Classification, and General language understanding evaluation (GLUE) * 问答、自然语言推理、分类和通用语言理解评估(GLUE) BERT 发布之前,还有三种架构也取得了SOTA性能: * ULM-Fit (January, 1月) * ELMo (February, 2月) * OpenAI GPT (June, 6月) * BERT (October, 10月) OpenAI GPT 和 BERT 均使用的是 [Transformer ]([**Transformer**](https://papers.nips.cc/paper/7181-attention-is-all-you-need.pdf))架构,而不是循环神经网络;这使得架构能够通过自注意机制建模长期依赖关系,从而根本上改变了我们对顺序数据建模的方式。它引入了*编码器-解码器*架构,这在计算机视觉应用中被看作是图像生成的一种方式,通过变分自动编码器实现编码。 那么BERT与2018年发布的所有其它模型有何不同?为了回答这个问题,我们需要了解 BERT 是什么以及它是如何工作的。 所以,让我们开始吧。 --- ## What is BERT? BERT 表示“Bidirectional Encoder Representation with Transformers”。简单来说,BERT 通过将*数据或词嵌入*传递给编码器来提取模式或表示。Encoder本身是堆叠在一起的transformer架构(一组EncoderLayer堆叠)。它是一个双向transformer,这意味着在训练过程中,它考虑了词汇表左右两侧的上下文(即计算过程中可以看到整个输入全部token),以提取模式或表示。 ![BERT encoder](https://zoe.red/usr/uploads/2024/04/3475359534.png) BERT 使用两种训练范式:*Pre-training*和 *Fine-tuning*。 在*预训练*阶段,模型会在大型数据集上进行训练以提取模式。这通常是一个*无监督学习*任务,模型会在一个无标签的数据集上进行训练,比如来自维基百科等大型语料库的数据。 在*微调*阶段,模型会针对下游任务进行训练,如分类、文本生成、语言翻译、问答等。基本上,你可以下载一个预训练模型,然后在你的数据上进行*迁移学习*。 --- ### Core components of BERT BERT借鉴了之前发布的SOTA模型的思想, 下面我们对此进行详细介绍。 #### The Transformers BERT 的主要组件是 Transformer 架构。Transformer 由两个组件组成:`encoder`和`decoder`。decoder本身包含两个组件:`self-attention layer`和`feed-forward neural network`。 `self-attention layer`接受输入并将每个单词编码为中间编码表示,然后通过前馈神经网络传递。前馈网络将这些表示传递给解码器,解码器本身由三个组件组成:`self-attention layer`, `Encoder-Decoder Attention`, 以及 `feed-forward neural network`。 ![BERT components](https://zoe.red/usr/uploads/2024/04/4217935996.png) Transformer 架构的好处是它能够帮助模型保留传统的 RNN、LSTM 和 GRU 所不能实现的无限长序列。但即使它能够实现长期依赖,它仍然缺乏上下文理解(lacks contextual understanding)。 Jay Alammar 在他的文章[*The Illustrated Transformer*](https://jalammar.github.io/illustrated-transformer/)中深入解释了 transformers,值得一读。 --- #### ELMo BERT 从 ELMo 借鉴了另一个想法,ELMo 代表来自语言模型的Embeddings。ELMo 由 Peters 等人于 2017 年引入,处理了上下文理解的思想。ELMo 的工作方式是使用*双向 LSTM* 来理解上下文。由于它考虑了来自两个方向的单词,它可以为拼写相似但含义不同的单词分配不同的word embedding。 例如,“You kids should **stick** together in the dark”与“Hand me that **stick**”完全不同。尽管两个句子中使用了相同的词,但根据上下文,意义是不同的。 所以,ELMo通过考虑来自右方和左方的words来分配embeddings,与之前只考虑左方单词的模型相比。这些模型是单向的,如RNNs、LSTMs等。 这使得 ELMo 能够从序列中捕捉上下文信息,但由于 ELMo 使用 LTSM,与 transformers 相比,它没有长期依赖性。 到目前为止,我们已经看到 BERT 可以访问文档中的序列,即使它在序列中比当前单词落后 'n' 个单词,这是因为 transformers 中存在的注意机制,即它可以保持长期依赖性,并且由于 ELMo 中存在的双向机制,它还可以实现对句子的上下文理解。 --- #### ULM-FiT 2018 年,Jeremy Howard 和 Sebastian Ruder 发表了一篇名为[Universal Language Model Fine-tuning or ULM-FiT](https://arxiv.org/pdf/1801.06146.pdf)的论文,他们认为transfer learning可以像在CV中一样应用于NLP。 以前我们使用的是pre-trained models来进行word-embeddings,只针对整个模型的第一层,即embedding layers进行训练,整个模型是从头开始训练的,这是非常耗时的,并且在这个领域并没有取得很大的成功。然而,Howard和Ruder提出了3种文本分类的方法: * 第一步包括在更大的数据集上训练模型,以便模型学习表示。 * 第二步是使用特定任务的数据集对模型进行fine-tuning,期间引入了两种方法:判别式微调(Discriminative fine-tuning)和倾斜三角形学习率(STLR)。前一种方法尝试在网络的传输层中微调或优化每个参数,而后一种方法控制每个优化步骤中的学习率。 * 第三步是在特定任务的数据集上对分类器进行微调。 ![BERT ULM-FiT](https://zoe.red/usr/uploads/2024/04/2469357829.png) 随着 ULM-FiT 的发布,NLP 从业者现在可以在他们的 NLP 问题中使用迁移学习方法。但是 ULM-FiT 迁移学习方法的唯一问题是需要对网络中的*所有层*进行微调,这是一项繁重的工作。 --- #### OpenAI GPT OpenAI 的团队 Radford、Narasimhan、Salimans 和 Sutskever 提出了生成式预训练 Transformer(Generative Pre-trained Transformer, GPT)。他们提出了一种*只使用 Transformer 的解码器而不是编码器的单向方法*。结果,在各种任务中都优于以前的模型,如: * Classification, 分类 * Natural Language Inference, 自然语言推理 * Semantic similarity, 语义相似度 * Question answering, 问答 * Multiple Choice, 多项选择 尽管 GPT 仅使用了解码器,但它仍然能够保持长期依赖关系。此外,与 ULM-FiT 中看到的相比,它将微调降至最低。 下面是一个比较不同模型基于预训练、下游任务和最重要的微调的表格。 ![Language models comparison](https://zoe.red/usr/uploads/2024/04/53039384.png) GPT论文中的一段摘录是这样写的:“这种模型选择为我们提供了一个更加结构化的记忆,以处理文本中的长期依赖关系,与RNN等替代方案相比,能够在不同任务之间实现稳健的迁移性能。在迁移过程中,我们利用来自遍历式方法派生出的*特定于任务的输入适配器*,将结构化文本输入作为一个连续的令牌序列进行处理。正如我们在实验中所展示的那样,这些适配器使我们能够通过对预训练模型架构进行最小改动来有效地进行微调。” 让我们比较一下所有模型与 BERT 在它们可以执行的任务上的表现: | | Transformer | ELMo | ULM-FiT | OpenAI GPT | BERT | | ------------------------------------------------ | ----------- | ----------- | ----------- | --------------- | ------------- | | **Contextual understanding** | No | Yesrn(Weak) | Yesrn(Weak) | Yesrn(Moderate) | Yesrn(Strong) | | **Long-term dependencies** | Infinite | Finite | Finite | Infinite | Infinite | | **Machine translation** | Yes | No | No | Yes | Yes | | **Natural language inference** | No | Yes | No | Yes | Yes | | **Question answering** | No | Yes | No | Yes | Yes | | **Classification or**<br/>**sentiment analysis** | No | Yes | Yes | Yes | Yes | | **Text generation** | No | No | No | Yesrn(Poor) | No | | **Fill-Mask** | No | No | No | No | Yes | 你可以检查[Huggingface models ](https://huggingface.co/models),以了解模型在每个任务上的性能。 --- ## Why BERT? BERT 是[self-supervised](https://neptune.ai/blog/self-supervised-learning) 模型。这意味着它可以从原始语料库中生成输入和标签,而无需人类明确编程(进行标注?)。请记住,它训练的数据是非结构化的。 BERT是通过两个特定任务进行预训练的:*Masked Language Model(MLM)* and *Next sentence prediction(NSP)*。前者使用掩码输入,例如“the man [MASK] to the store”而不是“the man went to the store”。这限制了BERT只能看到其旁边的单词,使其能够尽可能地学习双向表示,从而使其在多个下游任务中更加灵活可靠。后者预测两个句子是否在语境上相互关联。 ![BERT masking](https://zoe.red/usr/uploads/2024/04/2460203938.png) (MLM任务示意图) 例如,如果句子 A 是“[CLS] the man [MASK] to the store”,句子 B 是“penguin [MASK] are flightless birds [SEP]”,那么 BERT 将能够判断这两个句子是否连续。 ![BERT feature extraction](https://zoe.red/usr/uploads/2024/04/1042665421.png) (NSP示意图) 在训练过程中,BERT 使用特殊类型的tokens,如`[CLS]`、`[MASK]`、`[SEP]`等,这些标记使 BERT 能够区分句子的开始位置、哪个词被掩盖以及两个句子之间的分隔。我已经在`preprocessing section`以表格形式解释了这些标记。 由于之前讨论的特性,*BERT 还可以用于特征提取*,并将这些提取结果馈送给您现有的模型。 在原始的BERT论文中,它与GPT在通用语言理解评估基准(GLUE, 包含多个类型的基本数据集, [链接](https://zhuanlan.zhihu.com/p/151818251))上进行了比较,以下是结果。 ![BERT GPT comparison](https://zoe.red/usr/uploads/2024/04/2097462309.png) 正如您所看到的,BERT在所有任务中表现优于GPT1,并且平均比GPT1好7%。 补充: GLUE包含的数据集简介 ![](https://zoe.red/usr/uploads/2024/04/1989819826.jpg) ![BERT tasks](https://zoe.red/usr/uploads/2024/04/689404007.png) 上面的图像显示了 BERT 可以用于的不同任务: (a)句子对之间的关系分类; (b) 单句分类; (c)问答, 生成; (d) 单句标注任务 --- ## Coding BERT with Pytorch 让我们通过代码来了解如何基于PyTorch一步步构建 BERT。 我们将整个程序分为 4 个部分: 1. Preprocessing 2. Building model 3. Loss and Optimization 4. Training --- ### Preprocessing 在预处理阶段,我们将对数据进行结构化处理,以便神经网络能够处理它。我们首先为训练分配原始文本。 ```python text = ( 'Hello, how are you? I am Romeo.n' 'Hello, Romeo My name is Juliet. Nice to meet you.n' 'Nice meet you too. How are you today?n' 'Great. My baseball team won the competition.n' 'Oh Congratulations, Julietn' 'Thanks you Romeo' ) ``` 然后我们将通过以下方式清理数据: * 将句子转换为小写。 * 创建vocabulary/词汇表。词汇表是文档中唯一单词的列表(对应输入的token)。 ``` # 将多行text输入, 排除掉标点符号 # 之后根据空格/分隔符切分成一个个单词/token, 结果送入set去重; 得到了vocab sentences = re.sub("[.,!?-]", '', text.lower()).split('n') # filter '.', ',', '?', '!' word_list = list(set(" ".join(sentences).split())) ``` 现在,在接下来的步骤中,重要的是要记住BERT在训练过程中使用特殊的标记。下面是一张表格,解释了各种标记的用途: | Token | Purpose | | ------ | ------------------------------------------------------------------------------------ | | [CLS] | The first token is always classification第一个标记始终是分类 | | [SEP] | Separates two sentences 分隔两个句子 | | [END] | End the sentence. 结束句子。 | | [PAD] | Use to truncate the sentence with equal length.用于将句子截断为相等长度。 | | [MASK] | Use to create a mask by replacing the original word.用于通过替换原始词语来创建掩码。 | 这些tokens应包含在单词字典中,其中vocabulary中的每个token和word都被分配一个索引号 ``` # token to index word_dict = {'[PAD]': 0, '[CLS]': 1, '[SEP]': 2, '[MASK]': 3} for i, w in enumerate(word_list): word_dict[w] = i + 4 # index to token number_dict = {i: w for i, w in enumerate(word_dict)} vocab_size = len(word_dict) ``` 一旦这个问题解决了,我们需要创建一个函数来格式化输入序列,包括三种类型的embeddings:*token embedding*, *segment embedding*,以及 *position embedding*。 *什么是token embedding?* 例如,如果句子是“The cat is walking. The dog is barking”,那么函数应该按照以下方式创建一个序列:“[CLS] the cat is walking [SEP] the dog is barking”。 然后,我们将所有内容转换为来自单词字典的索引。所以上面的句子会变成类似于“[1, 5, 7, 9, 10, 2, 5, 6, 9, 11]”。请记住,1 和 2 分别代表[CLS]和[SEP]。 *什么是segment embedding?* 一个segment embedding/段落嵌入用于将两个句子分开,通常被定义为0和1。 *什么是position embedding?* position embedding为序列中的每个embedding提供位置信息。 我们将在后面创建一个用于位置嵌入的函数。 ![BERT embeddings](https://zoe.red/usr/uploads/2024/04/4271996881.png) 补充内容: * Token Embeddings与Position Embeddings 可以直接对照标准Transformer论文实现 * Segment Embeddings: 句子分段嵌入, 是一个仅包含3个Token映射的嵌入, 其中0是填充,1代表第一句话,2代表第二句话 *现在的下一步将是创建masking。* 如原始论文中所述,BERT 将随机分配掩码给序列的 15%。但请记住,*不要给特殊标记分配掩码*。为此,我们将使用条件语句。 一旦我们用[MASK]标记替换 15%的words,我们将添加padding。padding通常用于确保所有句子的长度相等。例如,如果我们取句子: “The cat is walking. The dog is barking at the tree” 然后加上padding,长度对齐后它会变成这样: “[CLS] The cat is walking [PAD] [PAD] [PAD]. [CLS] The dog is barking at the tree.” 第一句话的长度等于第二句话的长度。 ``` def make_batch(): batch = [] positive = negative = 0 while positive != batch_size/2 or negative != batch_size/2: tokens_a_index, tokens_b_index= randrange(len(sentences)), randrange(len(sentences)) tokens_a, tokens_b= token_list[tokens_a_index], token_list[tokens_b_index] input_ids = [word_dict['[CLS]']] + tokens_a + [word_dict['[SEP]']] + tokens_b + [word_dict['[SEP]']] segment_ids = [0] * (1 + len(tokens_a) + 1) + [1] * (len(tokens_b) + 1) # MASK LM n_pred = min(max_pred, max(1, int(round(len(input_ids) * 0.15)))) # 15 % of tokens in one sentence cand_maked_pos = [i for i, token in enumerate(input_ids) if token != word_dict['[CLS]'] and token != word_dict['[SEP]']] shuffle(cand_maked_pos) masked_tokens, masked_pos = [], [] for pos in cand_maked_pos[:n_pred]: masked_pos.append(pos) masked_tokens.append(input_ids[pos]) if random() < 0.8: # 80% input_ids[pos] = word_dict['[MASK]'] # make mask elif random() < 0.5: # 10% index = randint(0, vocab_size - 1) # random index in vocabulary input_ids[pos] = word_dict[number_dict[index]] # replace # Zero Paddings n_pad = maxlen - len(input_ids) input_ids.extend([0] * n_pad) segment_ids.extend([0] * n_pad) # Zero Padding (100% - 15%) tokens if max_pred > n_pred: n_pad = max_pred - n_pred masked_tokens.extend([0] * n_pad) masked_pos.extend([0] * n_pad) if tokens_a_index + 1 == tokens_b_index and positive < batch_size/2: batch.append([input_ids, segment_ids, masked_tokens, masked_pos, True]) # IsNext positive += 1 elif tokens_a_index + 1 != tokens_b_index and negative < batch_size/2: batch.append([input_ids, segment_ids, masked_tokens, masked_pos, False]) # NotNext negative += 1 return batch ``` 由于我们正在处理next-word prediction,我们必须创建一个label来预测句子是否有连续的句子,即 IsNext 或 NotNext。因此,我们为每个在下一个句子之前的句子分配 True,并使用条件语句来实现。 例如,如果两个句子在上下文中,它们通常会相互跟随。所以假设第一个句子是 A,那么下一个句子应该是 A+1。直观地,我们编写代码,如果第一个句子的位置即`tokens_a_index + 1 == tokens_b_index`,即在同一上下文中的第二个句子,那么我们可以将此输入的标签设置为 True。 如果不满足上述条件,即如果`tokens_a_index + 1 != tokens_b_index`,则将此输入的标签设置为False。 --- ### Building model BERT 是一个复杂的模型,只有逐个解释其组件及其功能让您充分理解才有意义。 BERT 具有以下组件: 1. Embedding layers, 嵌入层 2. Attention Mask, 注意力掩码 3. Encoder layer , 编码器层 1. Multi-head attention, 多头注意力 1. Scaled dot product attention, 缩放点积注意力 2. Position-wise feed-forward network, 位置编码前馈网络 4. BERT (assembling all the components), BERT(组装所有组件) 为了使学习更容易,您可以随时参考此图表。 ![BERT building model](https://zoe.red/usr/uploads/2024/04/3986710689.png) #### Embedding layer 嵌入层是 BERT 中的第一层,它接收输入并创建一个*查找表*。嵌入层的参数是可学习的,这意味着在学习过程结束时,嵌入将把相似的单词聚集在一起。 嵌入层还保留了词之间的不同关系,如:语义、句法、线性关系,由于 BERT 是双向的,它还会保留上下文关系。 对于 BERT 来说,它为每个词创建了三个embedding。 * Token, 令牌, * Segments and 段落和 * Position. 位置。 如果你回忆起来,我们还没有创建一个函数来接受输入并对其进行位置嵌入的格式化,但是对于标记和段落的格式化已经完成。因此,我们将接受输入并为序列中的每个单词创建一个位置。它看起来像这样: ``` print(torch.arange(30, dtype=torch.long).expand_as(input_ids)) ``` ``` Output: tensor([[ 0, 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], [ 0, 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], [ 0, 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], [ 0, 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], [ 0, 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], [ 0, 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]]) ``` 在前向函数中,我们将所有的嵌入相加并进行归一化处理。 ![BERT embeddings](https://zoe.red/usr/uploads/2024/04/1666266245.png) ``` class Embedding(nn.Module): def __init__(self): super(Embedding, self).__init__() self.tok_embed = nn.Embedding(vocab_size, d_model) # token embedding self.pos_embed = nn.Embedding(maxlen, d_model) # position embedding self.seg_embed = nn.Embedding(n_segments, d_model) # segment(token type) embedding self.norm = nn.LayerNorm(d_model) def forward(self, x, seg): seq_len = x.size(1) pos = torch.arange(seq_len, dtype=torch.long) pos = pos.unsqueeze(0).expand_as(x) # (seq_len,) -> (batch_size, seq_len) embedding = self.tok_embed(x) + self.pos_embed(pos) + self.seg_embed(seg) return self.norm(embedding) ``` #### Creating attention mask [BERT needs attention masks](https://neptune.ai/blog/unmasking-bert-transformer-model-performance). 而且这些mask应该是以正确的格式存在。以下代码将帮助您创建掩码。 它将把[PAD]转换为1,其他地方转换为0。 ``` def get_attn_pad_mask(seq_q, seq_k): batch_size, len_q = seq_q.size() batch_size, len_k = seq_k.size() # eq(zero) is PAD token pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # batch_size x 1 x len_k(=len_q), one is masking return pad_attn_mask.expand(batch_size, len_q, len_k) # batch_size x len_q x len_k ``` ``` print(get_attn_pad_mask(input_ids, input_ids)[0][0], input_ids[0]) ``` ``` Output: (tensor([False, False, False, False, False, False, False, False, False, False, False, False, False, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True]), tensor([ 1, 3, 26, 21, 14, 16, 12, 4, 2, 27, 3, 22, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])) ``` #### Encoder 编码器有两个主要组件: * Multi-head Attention, 多头注意力 * Position-wise feed-forward network, 位置编码前馈网络。 编码器的目的是从输入和注意力掩码中找到表示和模式。 ``` class EncoderLayer(nn.Module): def __init__(self): super(EncoderLayer, self).__init__() self.enc_self_attn = MultiHeadAttention() self.pos_ffn = PoswiseFeedForwardNet() def forward(self, enc_inputs, enc_self_attn_mask): enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask) # enc_inputs to same Q,K,V enc_outputs = self.pos_ffn(enc_outputs) # enc_outputs: [batch_size x len_q x d_model] return enc_outputs, attn ``` ##### Multi-head attention 这是编码器的主要组件之一。 注意力模型接受三个输入:查询(Query)、键(Key)和值(Value)。 我强烈推荐您阅读 Jay Alammar 的《图解 Transformer》([*The Illustrated Transformer*](https://jalammar.github.io/illustrated-transformer/)),该书深入解释了注意力模型。 多头注意力接受四个输入:查询(Query)、键(Key)、值(Value)和注意力掩码(Attention mask)。嵌入被作为输入传递给查询(Query)、键(Key)和值(Value)参数,而注意力掩码(Attention mask)被作为输入传递给注意力掩码(Attention mask)参数。 这三个输入和注意力掩码(Attention mask)通过点积操作进行运算,得到两个输出:上下文向量和注意力。然后,上下文向量通过线性层传递,并最终得到输出。 ``` class MultiHeadAttention(nn.Module): def __init__(self): super(MultiHeadAttention, self).__init__() self.W_Q = nn.Linear(d_model, d_k * n_heads) self.W_K = nn.Linear(d_model, d_k * n_heads) self.W_V = nn.Linear(d_model, d_v * n_heads) def forward(self, Q, K, V, attn_mask): # q: [batch_size x len_q x d_model], k: [batch_size x len_k x d_model], v: [batch_size x len_k x d_model] residual, batch_size = Q, Q.size(0) # (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W) q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2) # q_s: [batch_size x n_heads x len_q x d_k] k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2) # k_s: [batch_size x n_heads x len_k x d_k] v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2) # v_s: [batch_size x n_heads x len_k x d_v] attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) # attn_mask : [batch_size x n_heads x len_q x len_k] # context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)] context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask) context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v] output = nn.Linear(n_heads * d_v, d_model)(context) return nn.LayerNorm(d_model)(output + residual), attn # output: [batch_size x len_q x d_model] ``` 现在,让我们来探索一下缩放点积注意力: * The scaled dot product attention class, 缩放点积注意力类接受四个参数:查询(Query)、键(Key)、值(Value)和注意力掩码(Attention mask)。基本上,前三个参数用于输入词嵌入,而注意力掩码参数用于输入注意力掩码嵌入。 * 然后,它对查询Query和键Key进行矩阵乘法运算,得到注意力分数。 接下来,我们使用 `scores.masked_fill_(attn_mask, -1e9)`。这个属性将 scores 中的元素用 `-1e9` 填充,其中注意力掩码为 True,而其他元素则获得一个经过 softmax 函数处理的注意力分数,该函数给出一个介于 0 和 1 之间的分数。最后,我们对注意力和值进行矩阵乘法运算,得到上下文向量。 ``` class ScaledDotProductAttention(nn.Module): def __init__(self): super(ScaledDotProductAttention, self).__init__() def forward(self, Q, K, V, attn_mask): scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)] scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is one. attn = nn.Softmax(dim=-1)(scores) context = torch.matmul(attn, V) return score, context, attn ``` ``` emb = Embedding() embeds = emb(input_ids, segment_ids) attenM = get_attn_pad_mask(input_ids, input_ids) SDPA= ScaledDotProductAttention()(embeds, embeds, embeds, attenM) S, C, A = SDPA print('Masks',masks[0][0]) print() print('Scores: ', S[0][0],'nnAttention Scores after softmax: ', A[0][0]) ``` ``` Output: Masks tensor([False, False, False, False, False, False, False, False, False, False, False, False, False, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True]) Scores: tensor([ 9.6000e+01, 3.1570e+01, 2.9415e+01, 3.3990e+01, 3.7752e+01, 3.7363e+01, 3.1683e+01, 3.2156e+01, 3.5942e+01, -2.4670e+00, -2.2461e+00, -8.1908e+00, -2.1571e+00, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09], grad_fn=<SelectBackward>) Attention Scores after softmax:: tensor([1.0000e+00, 1.0440e-28, 1.2090e-29, 1.1732e-27, 5.0495e-26, 3.4218e-26, 1.1689e-28, 1.8746e-28, 8.2677e-27, 1.7236e-43, 2.1440e-43, 0.0000e+00, 2.3542e-43, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00], grad_fn=<SelectBackward>) ``` ##### Position-Wise Feed Forward Network 多头注意力机制的输出进入前馈网络,这就完成了编码器部分。 让我们稍作休息,回顾一下我们到目前为止学到的内容: * 输入进入embedding和注意力函数。两者都被馈送到具有多头功能和前馈网络的编码器中。 * 多头功能本身具有使用点积操作操作embedding和注意力掩码的功能。 ![BERT building model](https://zoe.red/usr/uploads/2024/04/3218429710.png) #### Assembling all the components 让我们从上次离开的地方继续,即来自编码器的输出。 编码器产生两个输出: * 一个来自前馈层的 * 一个来自注意力掩码 需要记住的关键是 * BERT 并没有明确使用解码器 *。相反,它使用输出和注意力掩码来获得所需的结果。 尽管transformers中的解码器部分被一个浅层网络替换,该网络可以用于分类,如下面的代码所示。此外,BERT 输出两个结果:一个用于分类器,另一个用于掩码。 ``` class BERT(nn.Module): def __init__(self): super(BERT, self).__init__() self.embedding = Embedding() self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)]) self.fc = nn.Linear(d_model, d_model) self.activ1 = nn.Tanh() self.linear = nn.Linear(d_model, d_model) self.activ2 = gelu self.norm = nn.LayerNorm(d_model) self.classifier = nn.Linear(d_model, 2) # decoder is shared with embedding layer embed_weight = self.embedding.tok_embed.weight n_vocab, n_dim = embed_weight.size() self.decoder = nn.Linear(n_dim, n_vocab, bias=False) self.decoder.weight = embed_weight self.decoder_bias = nn.Parameter(torch.zeros(n_vocab)) def forward(self, input_ids, segment_ids, masked_pos): output = self.embedding(input_ids, segment_ids) enc_self_attn_mask = get_attn_pad_mask(input_ids, input_ids) for layer in self.layers: output, enc_self_attn = layer(output, enc_self_attn_mask) # output : [batch_size, len, d_model], attn : [batch_size, n_heads, d_mode, d_model] # it will be decided by first token(CLS) h_pooled = self.activ1(self.fc(output[:, 0])) # [batch_size, d_model] logits_clsf = self.classifier(h_pooled) # [batch_size, 2] masked_pos = masked_pos[:, :, None].expand(-1, -1, output.size(-1)) # [batch_size, max_pred, d_model] # get masked position from final output of transformer. h_masked = torch.gather(output, 1, masked_pos) # masking position [batch_size, max_pred, d_model] h_masked = self.norm(self.activ2(self.linear(h_masked))) logits_lm = self.decoder(h_masked) + self.decoder_bias # [batch_size, max_pred, n_vocab] return logits_lm, logits_clsf ``` 需要记住的几个要点: 1. 您可以分配编码器的数量。在原始论文中,基础模型有 12 个。 2. 有两个激活函数:`Tanh` 和 `GELU`(G**aussian **E**rror **L**inear **U**nit, 高斯误差线性单元)。 ``` def gelu(x): return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0))) ``` --- ### Loss and optimization 尽管原始论文计算了整个词汇表上的概率分布,但我们可以使用 softmax 近似。但一个简洁的方法是使用cross-entropy loss。它是 softmax 和负对数似然的组合。 因此,在构建模型时,您不必包含 softmax,而是从前馈神经网络中获得一个无需 softmax 归一化的干净输出。 . 当涉及优化时,我们将使用 Adam 优化器。 ``` criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) ``` --- ### Training 最后,我们将开始训练。 ``` model = BERT() batch = make_batch() input_ids, segment_ids, masked_tokens, masked_pos, isNext = map(torch.LongTensor, zip(*batch)) for epoch in range(100): optimizer.zero_grad() logits_lm, logits_clsf = model(input_ids, segment_ids, masked_pos) loss_lm = criterion(logits_lm.transpose(1, 2), masked_tokens) # for masked LM loss_lm = (loss_lm.float()).mean() loss_clsf = criterion(logits_clsf, isNext) # for sentence classification loss = loss_lm + loss_clsf if (epoch + 1) % 10 == 0: print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss)) loss.backward() optimizer.step() # Predict mask tokens input_ids, segment_ids, masked_tokens, masked_pos, isNext = map(torch.LongTensor, zip(batch[0])) print(text) print([number_dict[w.item()] for w in input_ids[0] if number_dict[w.item()] != '[PAD]']) logits_lm, logits_clsf = model(input_ids, segment_ids, masked_pos) logits_lm = logits_lm.data.max(2)[1][0].data.numpy() print('masked tokens list : ',[pos.item() for pos in masked_tokens[0] if pos.item() != 0]) print('predict masked tokens list : ',[pos for pos in logits_lm if pos != 0]) logits_clsf = logits_clsf.data.max(1)[1].data.numpy()[0] print('isNext : ', True if isNext else False) print('predict isNext : ',True if logits_clsf else False) ``` ``` Output: Hello, how are you? I am Romeo. Hello, Romeo My name is Juliet. Nice to meet you. Nice meet you too. How are you today? Great. My baseball team won the competition. Oh Congratulations, Juliet Thanks you Romeo ['[CLS]', 'nice', 'meet', 'you', 'too', 'how', 'are', 'you', 'today', '[SEP]', '[MASK]', 'congratulations', '[MASK]', '[SEP]'] masked tokens list : [27, 22] predict masked tokens list : [] isNext : False predict isNext : True ``` 这就是从零开始的BERT编码。如果你在一个大语料库上训练它,那么你可以使用同样的模型来: 1. Pretraining, 预训练:使用任何语料库,但输入表示的格式必须与之前提到的完全相同。 2. Fine-tuning, 微调:确保使用有监督学习数据进行微调。 3. 不同任务的特征提取器,甚至主题建模。 您可以在[这里](https://colab.research.google.com/drive/13FjI_uXaw8JJGjzjVX3qKSLyW9p3b6OV?usp=sharing)找到完整的笔记本。 --- ## Is there a way to get a pre-trained model? 在原始论文中,发布了两个模型:BERT-base 和 BERT-large。在本文中,我展示了如何从头开始编码 BERT。 通常,您可以下载预训练模型,这样您就不必经历这些步骤。[Huggingface]([Huggingface](https://huggingface.co/))库提供了这个功能,您可以使用 Huggingface 的 transformer 库来进行 PyTorch。流程保持不变。 我有一个jupyter-notebook脚本,在其中使用了 Huggingface 的预训练 BERT,您可以在[这里](https://github.com/Nielspace/BERT/blob/master/BERT%20Text%20Classification%20fine-tuning.ipynb)查看。 当您使用预训练模型时,您只需要下载模型,然后在类中调用它,并使用前向方法来提供输入和掩码。 例如: ``` import transformers class BERTClassification(nn.Module): def __init__ (self): super(BERTClassification, self).__init__() self.bert = transformers.BertModel.from_pretrained('bert-base-cased') self.bert_drop = nn.Dropout(0.4) self.out = nn.Linear(768, 1) def forward(self, ids, mask, token_type_ids): _, pooledOut = self.bert(ids, attention_mask = mask, token_type_ids=token_type_ids) bertOut = self.bert_drop(pooledOut) output = self.out(bertOut) return output ``` ## Final thoughts BERT 是一个非常强大的最先进的自然语言处理模型。预训练模型是在大型语料库上训练的,您可以根据自己的需求和基于较小的数据集上的任务对其进行微调。微调的最好之处在于您不需要进行 1000 个epoch的训练,它可以在 3 到 10 个epoch内微调出最先进的性能,具体这取决于参数和数据集的处理情况。 我希望这个教程有趣且有益。我希望你能从中获得一些收获。 --- ## Resources * [BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://arxiv.org/pdf/1810.04805.pdf) * [The Illustrated BERT, ELMo, and co. (How NLP Cracked Transfer Learning)](http://jalammar.github.io/illustrated-bert/) * [Deep contextualized word representations : ELMo](https://arxiv.org/abs/1802.05365) * [Universal Language Model Fine-tuning for Text Classification](https://arxiv.org/abs/1801.06146) * [Attention is All you Need: Transformer](https://papers.nips.cc/paper/7181-attention-is-all-you-need.pdf) * [The Illustrated Transformer – Jay Alammar – Visualizing …](http://jalammar.github.io/illustrated-transformer/) * [Improving Language Understanding by … – Amazon S3](https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf) THE END 本文作者:将夜 本文链接:https://zoe.red/2024/483.html 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。 最后修改:2024 年 04 月 09 日 © 转载自他站 赞 如果觉得我的文章对你有用,请随意赞赏