自然语言基础2 BERT源码 & 大模型技术(attention/transformer/LLM) 如何处理各类NLP任务

自然语言基础2 BERT源码 & 大模型技术(attention/transformer/LLM) 如何处理各类NLP任务

1.简介

本文以huggingface BERT模型为例,概述常见Transformer的结构,以及数据处理的过程和相关概念,这里一定要牢记attention论文中的那张图,bert主要是其中encoder部分的实现(其实也可以通过参数变为decoder部分)

现在的transformer模型 (大模型LLM) 有很多了,包括GPTs, LLaMa, Moss等等,但作为初学者,4-5年前的GPT2和BERT相对会好上手一些,一方面可借此了解常见NLP任务,数据处理和模型基本结构等。

总的来说,这类模型都是基于attention, 至于是self-attention,cross-attention还是multi-head attention, 大体的layer结构是相似的,核心就是QKV的计算。

区别更多是nlp数据的数据,包括训练技巧,和具体任务的不同。

这里有几个重要概念:

  • tokenizer

tokenizer可以理解为对文本的编码,将文本编码为模型可以处理的数据。

NLP文本是“字符串”的形式保存句子(sentence),比如"A Titan RTX has 24GB of VRAM", 但模型计算需要的是浮点数, 因此需要将字符串中的sentence分词,分词是将句子划分为更小的单位,如单词或者特殊符号。

分词对照的是一个涵盖所有单词和符号的字典,也就是词汇表/语料库(vocab),每个单词对应不同的数字以区分。

在Bert中,其大小为vocab_size = 30522, 即有30522个tokens, vocab中的特殊标识符(special token)包括:

<[UNK]> unkown  

未知的令牌, 不在词汇表中的标记无法转换为 ID,而是设置为此标记。

<[SEP]> separator 

分隔符标记,在从多个序列构建序列时使用,例如 用于序列分类或 和 问答 (QA) 任务 。 它还用作用特殊标记构建的序列的最后一个标记。

<[PAD]> padding

用于填充的标记,例如在批处理不同长度的序列时。

<[CLS]> classifier

进行序列分类时使用的分类器标记(对整个序列进行分类而不是对每个标记进行分类)。它通常是序列的第一个标记。

<[MASK]> masking values

用于屏蔽值的标记。 这是使用掩码语言建模训练该模型时使用的标记。 这是模型将尝试预测的标记。

  • input_ids

分词后,各个词对应的id, id值为 [0, 30522]

  • attention_mask

mask是在将序列批处理在一起时使用的可选参数。该参数向模型指示哪些标记应该被关注。

mask是一个二进制张量,指示填充索引的位置,以便模型不会关注它们。 对于 BertTokenizer,1 表示应注意的值,而 0 表示填充值。 该注意力掩码位于分词器(tokenizer)返回的字典中,键为“attention_mask”:

  • token_type_ids

某些模型的目的是进行序列分类或问题回答。 这些需要在相同的输入 ID 中编码两个不同的序列。 它们通常由特殊标记分隔,例如分类器[CLS]和分隔符标记[SEP]。 例如,BERT 模型构建其两个序列输入:

" [CLS] SEQUENCE_A [SEP] SEQUENCE_B [SEP] "

  • position_ids

模型使用 position_ids 来识别哪个令牌位于哪个位置。 与将每个标记的位置嵌入其中的 RNN 相反,Transformer 不知道每个标记的位置。 职位 ID 就是为此目的而创建的。

它是可选参数。 如果没有将 position_ids 传递给模型,将自动创建为绝对位置嵌入。绝对位置嵌入在 [0, config.max_position_embeddings - 1] 范围内选择。 某些模型使用其他类型的位置嵌入,例如正弦位置嵌入或相对位置嵌入。

  • embedding

这个是transformer模型输入attention(encoder)前的操作,需要将原有的尺寸映射到潜空间的尺寸(hidden_states),我们来看一下BertEmbeddings:


class BertEmbeddings(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
        self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
        self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)

        self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

    def forward(self, input_ids=None, token_type_ids=None, position_ids=None, inputs_embeds=None):
        if input_ids is not None:
            input_shape = input_ids.size()
        else:
            input_shape = inputs_embeds.size()[:-1]

        seq_length = input_shape[1]
        device = input_ids.device if input_ids is not None else inputs_embeds.device
        if position_ids is None:
            position_ids = torch.arange(seq_length, dtype=torch.long, device=device)
            position_ids = position_ids.unsqueeze(0).expand(input_shape)
        if token_type_ids is None:
            token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)

        if inputs_embeds is None:
            inputs_embeds = self.word_embeddings(input_ids)
        position_embeddings = self.position_embeddings(position_ids)
        token_type_embeddings = self.token_type_embeddings(token_type_ids)

        embeddings = inputs_embeds + position_embeddings + token_type_embeddings
        embeddings = self.LayerNorm(embeddings)
        embeddings = self.dropout(embeddings)
        return embeddings

可以看到,BERT 将 word, position, type_token的size映射为hidden_size, 并将三者加到一起,成为输入transformer的embedding

2.模型结构

2.1 设置Config

BERT-Base, Uncased: 12-layer, 768-hidden, 12-heads, 110M parameters
BERT-Large, Uncased: 24-layer, 1024-hidden, 16-heads, 340M parameters

class transformers.BertConfig(vocab_size=30522, hidden_size=768, num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072, hidden_act='gelu', hidden_dropout_prob=0.1, attention_probs_dropout_prob=0.1, max_position_embeddings=512, type_vocab_size=2, initializer_range=0.02, layer_norm_eps=1e-12, pad_token_id=0, gradient_checkpointing=False, **kwargs)
  • max_position_embeddings

位置编码, 该模型可能使用的最大序列长度。 通常将其设置为较大的值以防万一(例如 512 / 1024 / 2048)。

  • num_hidden_layers

bert默认是12层

2.2 一个 Layer

  • BertEmbeddings

# 输入:
#      input_ids (or inputs_embeds=), token_type_ids, position_ids

# 输出:

#     embeddings = inputs_embeds + position_embeddings + token_type_embeddings
#     embeddings = LayerNorm(embeddings)
#     embeddings = dropout(embeddings) 
  • BertSelfAttention

# 输入:
#      hidden_state # QKV都通过 hidden_state 输入 FC 得到

# 输出:
#     context_layer, attention_score
    # context_layer 是 attentionQKV的计算输出
    # attention_score是QK的矩阵乘积,是可选的 (output_attentions)

# Tips:
# 如果用在decoder里面,那么 Q 来自 hidden_state
# K,V来自可选参数 : encoder_hidden_states=None, encoder_attention_mask=None,
  • BertSelfOutput

attention之后的全连接层

# 输入:
#      hidden_states, input_tensor

# 输出:
#     hidden_states

# 代码很简单:
class BertSelfOutput(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

    def forward(self, hidden_states, input_tensor):
        hidden_states = self.dense(hidden_states)
        hidden_states = self.dropout(hidden_states)
        hidden_states = self.LayerNorm(hidden_states + input_tensor)
        return hidden_states
  • BertAttention

BertSelfAttention + BertSelfOutput

  • BertIntermediate

它位于BERT模型的编码器部分,在每个编码器层的中间位置, 区别于BertSelfOutput是没用layerNorm

  • BertOutput

这个和 BertSelfOutput 完全一样

  • BertLayer

这个实现的是 attention论文中 encoder 或者 decoder完整的一层, encoder就是 (Multi-head attention + (Add & Norm)) + (FC + (Add & Norm))

  1. 这里的 add 是 残差的意思 即 y = f(x) + x

  2. 如果把Bert当作decoder的话,其需要在中间加一层, 输入encoder的输出 ( 即K , V 的 hidden_state ),同时decoder上一层的attention 输出的 hidder_state 作为Q, 该层一般叫做 cross_attention

  3. 这里面主要用到了分快运算技术,具体为 chunk_size_feed_forward()函数, 来自reformer

沿着指定的维度(chunk_dim)分割成大小为chunk_size的小块输入张量。然后,它独立地对每个块应用一个层的前向函数(forward fn),以节省内存。如果前向函数在分割维度(chunk_dim)上是独立的,那么这个函数将产生与不应用函数相同的结果。

具体为为 [batchsize , sequence_length, hidden_size] 的 tensor 分为sequence_length个[batchsize, hidden_size]来计算以提升效率节省内存

  • BertEncoder

融合了多个attention层的transofomer, 标准的attention论文中encoder的复现

训练技巧是 Gradient checkpointing:

技术在训练时节省内存,它将计算图中的某些节点标记为检查点,
并在前向传播期间保存这些节点的部分计算结果。
在反向传播过程中,只需要重新计算与检查点相关的节点,从而减少了内存消耗。 
使用梯度检查点时,内存消耗和计算成本之间存在一个权衡。
尽管需要更多的计算来重新计算检查点之后的节点,但它显著减少了内存需求,
使得可以在有限的内存资源下训练更大的模型,
在计算资源有限的情况下实现更大规模的训练。

3.训练 及 各类任务

除了第一个是bert encoder 预训练模型外,其他都是基于预训练模型加的head,这个head一般就是个fc映射。

针对不同任务,不同的head类主要是数据格式的处理以适配输入和损失函数的计算

  • BertForPreTraining

这个是最基本的bert预训练模型,即训练大量数据得到PreTraining bert, 将其输出接上不不同的 head 用于各项下游nlp任务

  1. 损失用的是交叉熵 CrossEntropyLoss, 即bert的输出作为 prediction_scores 和 labels 比较
  • BertLMHeadModel

这是一个标准头的实现,封装了Bertencoder, encoder输出后 接额外的FC: BertLMPredictionHead 作为特定任务输出

BertLMHeadModel的任务是基于输入文本的上下文,预测下一个词或一系列词的概率分布。它通过线性层和softmax激活函数对Bert模型的输出进行转换,以生成语言模型的预测结果。 接收经过Bert模型编码器处理后的特征表示作为输入。它可以被视为Bert模型的一部分,负责生成语言模型的预测结果。

  • BertForMaskedLM

BertForMaskedLM用于处理遮蔽语言建模(Masked Language Modeling)任务。

在遮蔽语言建模任务中,输入文本中的某些词会被遮蔽(即用特殊的"[MASK]"符号替代),
然后模型需要预测这些被遮蔽的词。BertForMaskedLM针对这种任务进行了优化。

具体而言,BertForMaskedLM是在BERT模型的基础上添加了一个用于遮蔽语言建模的头部模型。
该头部模型包含一个线性层和softmax激活函数,用于根据Bert模型的输出进行预测。

在训练过程中,BertForMaskedLM会接收一段包含遮蔽词的输入文本,并根据Bert模型的输出预测这些遮蔽词的概率分布。
训练过程中,通过最小化预测结果与真实标签的差异来优化模型参数。

在 inference 阶段,BertForMaskedLM可以用于预测遮蔽词的概率分布,
从而得到对输入文本中遮蔽位置的推测。

BertForMaskedLM在BERT模型的输出基础上进行预测,
从而对输入文本中遮蔽词的位置进行推测。

  • BertForNextSentencePrediction

  • BertForSequenceClassification

  • BertForMultipleChoice

  • BertForTokenClassification

  • BertForQuestionAnswering


BertForQuestionAnswering是基于BERT模型的特定任务模型,用于处理问答(Question Answering)任务。

在问答任务中,给定一个问题和一个上下文文本(通常是一篇文章或段落),
模型需要从上下文中找到与问题相关的答案。
BertForQuestionAnswering针对这种任务进行了优化。

具体而言,BertForQuestionAnswering是在BERT模型的基础上添加了一个用于问答任务的头部模型。
该头部模型包含一个线性层和softmax激活函数,用于对BERT模型的输出进行答案的开始位置和结束位置的预测。

在训练过程中,BertForQuestionAnswering接收一个问题和上下文文本作为输入,
并根据Bert模型的输出进行答案位置的预测。
训练过程中,通过最小化预测结果与真实答案位置的差异来优化模型参数。

在应用阶段,BertForQuestionAnswering可以接收一个问题和上下文文本,
然后预测最可能的答案的开始位置和结束位置。

总之,BertForQuestionAnswering是基于BERT模型的特定任务模型,
用于处理问答任务。它通过添加一个头部模型,
在BERT模型的输出基础上进行预测,从而找到问题在上下文中最可能的答案位置。

4. Reference

  • https://huggingface.co/docs/transformers/model_doc/bert

  • https://huggingface.co/transformers/v3.0.2/main_classes/model.html

  • https://huggingface.co/transformers/v3.2.0/model_doc/bert.html

  • https://zhuanlan.zhihu.com/p/359682043

  • https://blog.ceshine.net/post/bert-gradient-checkpoint/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Huggingface自然语言处理详解&mdash;&mdash;基于BERT中文模型任务实战》码是一份用于展示如何使用Huggingface库进行自然语言处理任务的示例代码。该代码基于BERT中文模型,旨在帮助读者理解如何使用该模型进行文本分类、情感分析等任务。 该码由以下几个部分组成: 1. 导入必要的库:代码开始部分导入了需要使用的Huggingface库,包括`transformers`和`tokenizers`。这些库提供了BERT模型以及相关的处理函数和工具。 2. 数据预处理:接下来,码介绍了如何进行数据预处理。这包括读取数据集、分词、编码等步骤。具体来说,使用`BertTokenizer`类对文本进行分词,并使用`BertTokenizer.from_pretrained`加载预训练的BERT中文模型。 3. 构建模型码构建了一个简单的BERT分类模型,使用`BertForSequenceClassification`类来实现。这个模型有一个BERT编码器和一个用于分类的线性层。 4. 训练模型:通过`Trainer`类,码进行了模型的训练。在训练过程中,使用了一个训练集和一个验证集,并定义了相应的训练参数,如学习率、批大小等。训练过程中,模型参数逐渐更新,直到达到设定的最大训练轮数。 5. 模型评估:训练完成后,码进行模型评估。通过计算准确率、查全率、查准率等指标,评估模型在验证集上的性能。 总的来说,《Huggingface自然语言处理详解&mdash;&mdash;基于BERT中文模型任务实战》码提供了一个完整的BERT模型应用示例,通过该码,读者能够了解如何使用Huggingface库中的BERT模型进行中文文本分类任务,并学会了如何进行数据预处理、构建模型、训练和评估模型等操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值