5.6.BERT

BERT

​ NLP中的迁移学习,使用预训练好的模型来抽取词、句子的特征,例如word2vec或语言模型,不更新预训练好的模型。需要构建新的网络来抓取任务需要的信息,而word2vec忽略了时序信息,语言模型也只看一个方向。

​ BERT的动机:首先是基于微调的NLP模型,预训练的模型也抽取了足够多的信息,新的任务只需要增加一个简单的输出层。

1.BERT 架构

​ 只有编码器的Transformer,有两个版本:

  • Base: #blocks=12,hidden size=768,#heads =12,#parameters =110M
  • Large:#blocks =24,hidden size=1024,#heads = 16,#parameter=340M

​ 在大规模数据上训练>3B词

1.1.对输入的修改

​ 现在只有一个编码器,则不能一个输入进编码器,另一个输入进解码器,就将每个样本是一个句子对,加入额外的片段嵌入,位置编码可学习

1.2 预训练任务1:带掩码的语言模型

​ Transfomer的编码器是双向的,标准语言模型要求单向,带掩码的语言模型每次随机(15%概率)将一些词元换成<mask>,(就类似做完形填空,不用预测未来,所以双向是没问题的)

​ 因为微调中没有<mask>,那么:

  • 80%概率下,将选中的词元变成<mask>
  • 10%概率下换成一个随机词元
  • 10%概率下保持原有的词元

​ 一个可能解释:

​ 作者在论文中谈到了采取上面的mask策略的好处。大致是说采用上面的策略后,Transformer encoder就不知道会让其预测哪个单词,或者说不知道哪个单词会被随机单词给替换掉,那么它就不得不保持每个输入token的一个上下文的表征分布(a distributional contextual representation)。也就是说如果模型学习到了要预测的单词是什么,那么就会丢失对上下文信息的学习,而如果模型训练过程中无法学习到哪个单词会被预测,那么就必须通过学习上下文的信息来判断出需要预测的单词,这样的模型才具有对句子的特征表示能力。另外,由于随机替换相对句子中所有tokens的发生概率只有1.5%(即15%的10%),所以并不会影响到模型的语言理解能力。

1.3 预训练任务2:下一句子预测

​ 预测一个句子对中两个句子是不是相邻。

​ 在训练样本中:

  • 50%概率选择相邻句子对:<cls> this movie is great <sep> i like it <sep>
  • 50%概率选择随机句子对:<cls> this movie is great <sep> hello world<sep>

​ 将<cls>对应的输出放到一个全连接层来预测

2. BERT模型代码实现

2.1 输入表示

​ 有些任务是单个文本作为输入,有些任务以一对文本序列作为输入。

​ BERT输入序列明确地表示单个文本和文本对。当输入为单个文本时,BERT输入序列是特殊类别词元"<cls>"、文本序列的标记、以及特殊分隔词元"<sep>"的连结。当输入为文本对时,BERT输入序列是"<cls>"、第一个文本序列的标记、"<sep>"、第二个文本序列标记、以及"<sep>"的连结。

​ 对于BERT编码器,使用可学习的位置嵌入,Bert输入序列的嵌入是词元嵌入、片段嵌入和位置嵌入的和。

python 复制代码
import torch
from torch import nn
from d2l import torch as d2l

#@save
def get_tokens_and_segments(tokens_a, tokens_b=None):
    """获取输入序列的词元及其片段索引"""
    tokens = ['<cls>'] + tokens_a + ['<sep>']
    # 0和1分别标记片段A和B
    segments = [0] * (len(tokens_a) + 2)
    if tokens_b is not None:
        tokens += tokens_b + ['<sep>']
        segments += [1] * (len(tokens_b) + 1)
    return tokens, segments
  
  
  
#@save
class BERTEncoder(nn.Module):
    """BERT编码器"""
    def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
                 ffn_num_hiddens, num_heads, num_layers, dropout,
                 max_len=1000, key_size=768, query_size=768, value_size=768,
                 **kwargs):
        super(BERTEncoder, self).__init__(**kwargs)
        self.token_embedding = nn.Embedding(vocab_size, num_hiddens)
        self.segment_embedding = nn.Embedding(2, num_hiddens)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module(f"{i}", d2l.EncoderBlock(
                key_size, query_size, value_size, num_hiddens, norm_shape,
                ffn_num_input, ffn_num_hiddens, num_heads, dropout, True))
        # 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数
        self.pos_embedding = nn.Parameter(torch.randn(1, max_len,
                                                      num_hiddens))

    def forward(self, tokens, segments, valid_lens):
        # 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens)
        X = self.token_embedding(tokens) + self.segment_embedding(segments)
        X = X + self.pos_embedding.data[:, :X.shape[1], :]
        for blk in self.blks:
            X = blk(X, valid_lens)
        return X

vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4
norm_shape, ffn_num_input, num_layers, dropout = [768], 768, 2, 0.2
encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, ffn_num_input,
                      ffn_num_hiddens, num_heads, num_layers, dropout)

tokens = torch.randint(0, vocab_size, (2, 8))
segments = torch.tensor([[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 1, 1, 1, 1, 1]])
encoded_X = encoder(tokens, segments, None)
encoded_X.shape


'''torch.Size([2, 8, 768])'''

2.2预训练任务

带掩码的语言模型

python 复制代码
#@save
class MaskLM(nn.Module):
    """BERT的掩蔽语言模型任务"""
    def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs):
        super(MaskLM, self).__init__(**kwargs)
        self.mlp = nn.Sequential(nn.Linear(num_inputs, num_hiddens),
                                 nn.ReLU(),
                                 nn.LayerNorm(num_hiddens),
                                 nn.Linear(num_hiddens, vocab_size))

    def forward(self, X, pred_positions):
       #pred_positions包含所有需要预测的位置,以及总共需要预测的位置个数
        num_pred_positions = pred_positions.shape[1]
        pred_positions = pred_positions.reshape(-1)
        batch_size = X.shape[0]
        batch_idx = torch.arange(0, batch_size)
        # 假设batch_size=2,num_pred_positions=3
        # 那么batch_idx是np.array([0,0,0,1,1,1])
        batch_idx = torch.repeat_interleave(batch_idx, num_pred_positions)
        masked_X = X[batch_idx, pred_positions]
        masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))
        mlm_Y_hat = self.mlp(masked_X)
        return mlm_Y_hat

mlm = MaskLM(vocab_size, num_hiddens)
mlm_positions = torch.tensor([[1, 5, 2], [6, 1, 5]])
mlm_Y_hat = mlm(encoded_X, mlm_positions)
mlm_Y_hat.shape

'''torch.Size([2, 3, 10000]),2个样本,预测3个位置,vocab_size是10000'''

mlm_Y = torch.tensor([[7, 8, 9], [10, 20, 30]])
loss = nn.CrossEntropyLoss(reduction='none')
mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape(-1))
mlm_l.shape

'''torch.Size([6]),损失函数的计算,总共2*3=6个样本'''
 

下一句预测

python 复制代码
#@save
class NextSentencePred(nn.Module):
    """BERT的下一句预测任务"""
    def __init__(self, num_inputs, **kwargs):
        super(NextSentencePred, self).__init__(**kwargs)
        self.output = nn.Linear(num_inputs, 2) #单分类问题

    def forward(self, X):
        # X的形状:(batchsize,num_hiddens)
        return self.output(X)


encoded_X = torch.flatten(encoded_X, start_dim=1)
# NSP的输入形状:(batchsize,num_hiddens)
# 返回每个Bert输入序列的二分类预测
nsp = NextSentencePred(encoded_X.shape[-1])
nsp_Y_hat = nsp(encoded_X)
nsp_Y_hat.shape
'''torch.Size([2, 2])'''

# 计算交叉熵损失
nsp_y = torch.tensor([0, 1])
nsp_l = loss(nsp_Y_hat, nsp_y)
nsp_l.shape

'''torch.Size([2])'''

2.3 整合代码

python 复制代码
#@save
class BERTModel(nn.Module):
    """BERT模型"""
    def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
                 ffn_num_hiddens, num_heads, num_layers, dropout,
                 max_len=1000, key_size=768, query_size=768, value_size=768,
                 hid_in_features=768, mlm_in_features=768,
                 nsp_in_features=768):
        super(BERTModel, self).__init__()
        self.encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape,
                    ffn_num_input, ffn_num_hiddens, num_heads, num_layers,
                    dropout, max_len=max_len, key_size=key_size,
                    query_size=query_size, value_size=value_size)
        self.hidden = nn.Sequential(nn.Linear(hid_in_features, num_hiddens),
                                    nn.Tanh())
        self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features)
        self.nsp = NextSentencePred(nsp_in_features)

    def forward(self, tokens, segments, valid_lens=None,
                pred_positions=None):
        encoded_X = self.encoder(tokens, segments, valid_lens)
        if pred_positions is not None:
            mlm_Y_hat = self.mlm(encoded_X, pred_positions)
        else:
            mlm_Y_hat = None
        # 用于下一句预测的多层感知机分类器的隐藏层,0是"<cls>"标记的索引
        nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
        return encoded_X, mlm_Y_hat, nsp_Y_hat
相关推荐
兰亭妙微37 分钟前
用户体验的真正边界在哪里?对的 “认知负荷” 设计思考
人工智能·ux
13631676419侯42 分钟前
智慧物流与供应链追踪
人工智能·物联网
TomCode先生44 分钟前
MES 离散制造核心流程详解(含关键动作、角色与异常处理)
人工智能·制造·mes
zd2005721 小时前
AI辅助数据分析和学习了没?
人工智能·学习
johnny2331 小时前
强化学习RL
人工智能
乌恩大侠1 小时前
无线网络规划与优化方式的根本性变革
人工智能·usrp
放羊郎1 小时前
基于萤火虫+Gmapping、分层+A*优化的导航方案
人工智能·slam·建图·激光slam
王哈哈^_^1 小时前
【数据集+完整源码】水稻病害数据集,yolov8水稻病害检测数据集 6715 张,目标检测水稻识别算法实战训推教程
人工智能·算法·yolo·目标检测·计算机视觉·视觉检测·毕业设计
SEOETC2 小时前
数字人技术:虚实交融的未来图景正在展开
人工智能
boonya2 小时前
从阿里云大模型服务平台百炼看AI应用集成与实践
人工智能·阿里云·云计算