NLP之预训练语言模型BERT
1引言
通常来说,在NLP领域的很多场景中模型最后所做的基本上都是一个分类任务,虽然表面上看起来不是。例如:文本蕴含任务其实就是将两个序列拼接在一起,然后预测其所属的类别;基于神经网络的序列生成模型(翻译、文本生成等)本质就是预测词表中下一个最有可能出现的词,此时的分类类别就是词表的大小。
对于问答选择这个任务场景来说其本质上依旧可以归结为分类任务,只是关键在于如何构建这个任务以及整个数据集。对于问答选择这个场景来说,其整体原理如下图
原始数据的形式是一个问题和四个选项,模型需要做的就是从四个选项中给出最合理的一个,于是也就变成了一个四分类任务。同时,构建模型输入的方式就是将原始问题和每一个答案都拼接起来构成一个序列中间用符号隔开,然后再分别输入到BERT模型中进行特征提取得到四个特征向量形状为,最后再经过一个分类层进行分类处理得到预测选项。值得一提的是,通常情况下这里的四个特征都是直接取每个序列经BERT编码后的向量。
2BERT刷新的nlp任务
在OpenAI发布GPT之后,谷歌坐不住了,也基于transformer结构开发出了Bert模型,该模型刷新了11个nlp任务
任务中文 | 英文 | 描述 |
---|---|---|
文本蕴含识别 | MultiNLI(multi-genre natural language inference,MNLI) | 文本间的推理关系,又称为文本蕴含关系。样本都是文本对,第一个文本M作为前提,如果能够从文本M推理出第二个文本N,即可说M蕴含N,M->N。两个文本关系一共有三种entailment(蕴含)、contradiction(矛盾)、neutral(中立) |
文本匹配 | QQP(quora question pairs,) | 判断两个问题是不是同一个意思,即是不是等价的。属于分类任务 |
自然语言问题推理 | QNLI(question natural language inference,) | 是一个二分类任务。正样本为(question,sentence),包含正确的answer;负样本为(question,sentence),不包含正确的answer。 |
斯坦福情感分类树 | SST-2(the stanford sentiment treebank,) | 分类任务 |
语言可接受性语料库 | CoLA(the corpus of linguistic acceptability,) | 分类任务,预测一个句子是否是acceptable |
语义文本相似度数据集 | STS-B(the semantic textual similarity benchmark,) | 样本为文本对,分数为1-5,用来评判两个文本语义信息的相似度 |
微软研究释义语料库 | MRPC(microsoft research paraphrase corpus,) | 样本为文本对,判断两个文本对语音信息是否是等价的 |
识别文本蕴含关系 | RTE(recognizing textual entailment,) | 与MNLI相似,只不过数据集更少 |
自然语言推理 | WNLI(winograd NLI,) | Winograd NLI,一个小的自然语言处理推理数据集 |
斯坦福问答数据集 | SQuAD(the standFord question answering dataset,) | question,从phrase中选取answer |
命名实体识别 | NER(named entity recognition,) | CoNLL-2003数据集 |
问答选择 | SWAG(the situations with adversarial generations dataset,) | 给定一个情景(一个问题或一句描述),任务是模型从给定的四个选项中预测最有可能的一个 |
GLUE榜单包含了MNLI;QQP;QNLI;SST-2;CoLA;STS-B;MRPC;RTE;WNLI这几个数据集
3Bert的训练数据预处理解析
以codertimo/BERT-pytorch为例,Bert的训练数据是经过Mask 和拼接的,即masked language model" and "predict next sentence",如下图所示
4以莫烦的教程进行学习
4.1Bert训练代码解析
以莫凡实现的bert为例
4.2前置代码
莫凡的Bert代码是继承自GPT,通过GPT看,也简单,就是先编写transformer,然后gpt使用encoder部分即可,不复杂。
4.3基于莫烦的Bert网络结构部分
莫凡实现的bert结构如下,其中call就是用的gpt代码中的call
莫凡说:“因为BERT的主架构是Transformer的Encoder,而我们之前写的GPT也是用的它的encoder。 还不太清楚我的GPT架构的朋友,请过目一下~ 所以这里我们只需要在GPT的结构上修改一下计算loss的方案和双向mask的方案即可。(我的GPT代码是继承的Transformer的架构,所以他们都是通用的)”
“所以这个BERT和我的GPT还算挺兼容的,只是稍微改动了下step()和mask().通过这个修改,就保留了BERT的双向注意力,而且在算loss的时候,能只计算需要计算的部分”
当然莫凡也说了,他实现的和原文还是有点出入的,但是我咋觉得网络结构上基本没变化,就是训练上多了trick
# [BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://arxiv.org/pdf/1810.04805.pdf)
import numpy as np
import tensorflow as tf
import utils # this refers to utils.py in my [repo](https://github.com/MorvanZhou/NLP-Tutorials/)
import time
from GPT import GPT
import os
import pickle
class BERT(GPT):
def __init__(self, model_dim, max_len, n_layer, n_head, n_vocab, lr, max_seg=3, drop_rate=0.1, padding_idx=0):
super().__init__(model_dim, max_len, n_layer, n_head, n_vocab, lr, max_seg, drop_rate, padding_idx)
# I think task emb is not necessary for pretraining,
# because the aim of all tasks is to train a universal sentence embedding
# the body encoder is the same across all tasks,
# and different output layer defines different task just like transfer learning.
# finetuning replaces output layer and leaves the body encoder unchanged.
# self.task_emb = keras.layers.Embedding(
# input_dim=n_task, output_dim=model_dim, # [n_task, dim]
# embeddings_initializer=tf.initializers.RandomNormal(0., 0.01),
# )
def step(self, seqs, segs, seqs_, loss_mask, nsp_labels):
with tf.GradientTape() as tape:
# 复用GPT类的call
mlm_logits, nsp_logits = self.call(seqs, segs, training=True)
# 准备计算loss
mlm_loss_batch = tf.boolean_mask(self.cross_entropy(seqs_, mlm_logits), loss_mask)
mlm_loss = tf.reduce_mean(mlm_loss_batch)
nsp_loss = tf.reduce_mean(self.cross_entropy(nsp_labels, nsp_logits))
loss = mlm_loss + 0.2 * nsp_loss
grads = tape.gradient(loss, self.trainable_variables)
self.opt.apply_gradients(zip(grads, self.trainable_variables))
return loss, mlm_logits
def mask(self, seqs):
mask = tf.cast(tf.math.equal(seqs, self.padding_idx), tf.float32)
return mask[:, tf.newaxis, tf.newaxis, :] # [n, 1, 1, step]
#==============上面是网络结构======================================
def _get_loss_mask(len_arange, seq, pad_id):
rand_id = np.random.choice(len_arange, size=max(2, int(MASK_RATE * len(len_arange))), replace=False)
loss_mask = np.full_like(seq, pad_id, dtype=np.bool)
loss_mask[rand_id] = True
return loss_mask[None, :], rand_id
def do_mask(seq, len_arange, pad_id, mask_id):
loss_mask, rand_id = _get_loss_mask(len_arange, seq, pad_id)
seq[rand_id] = mask_id
return loss_mask
def do_replace(seq, len_arange, pad_id, word_ids):
loss_mask, rand_id = _get_loss_mask(len_arange, seq, pad_id)
seq[rand_id] = np.random.choice(word_ids, size=len(rand_id))
return loss_mask
def do_nothing(seq, len_arange, pad_id):
loss_mask, _ = _get_loss_mask(len_arange, seq, pad_id)
return loss_mask
def random_mask_or_replace(data, arange, batch_size):
seqs, segs, xlen, nsp_labels = data.sample(batch_size)
seqs_ = seqs.copy()
p = np.random.random()
if p < 0.7:
# mask
loss_mask = np.concatenate(
[do_mask(
seqs[i],
np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])),
data.pad_id,
data.v2i["<MASK>"]) for i in range(len(seqs))], axis=0)
elif p < 0.85:
# do nothing
loss_mask = np.concatenate(
[do_nothing(
seqs[i],
np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])),
data.pad_id) for i in range(len(seqs))], axis=0)
else:
# replace
loss_mask = np.concatenate(
[do_replace(
seqs[i],
np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])),
data.pad_id,
data.word_ids) for i in range(len(seqs))], axis=0)
return seqs, segs, seqs_, loss_mask, xlen, nsp_labels
def train(model, data, step=10000, name="bert"):
t0 = time.time()
arange = np.arange(0, data.max_len)
for t in range(step):
seqs, segs, seqs_, loss_mask, xlen, nsp_labels = random_mask_or_replace(data, arange, 16)
loss, pred = model.step(seqs, segs, seqs_, loss_mask, nsp_labels)
if t % 100 == 0:
pred = pred[0].numpy().argmax(axis=1)
t1 = time.time()
print(
"\n\nstep: ", t,
"| time: %.2f" % (t1 - t0),
"| loss: %.3f" % loss.numpy(),
"\n| tgt: ", " ".join([data.i2v[i] for i in seqs[0][:xlen[0].sum()+1]]),
"\n| prd: ", " ".join([data.i2v[i] for i in pred[:xlen[0].sum()+1]]),
"\n| tgt word: ", [data.i2v[i] for i in seqs_[0]*loss_mask[0] if i != data.v2i["<PAD>"]],
"\n| prd word: ", [data.i2v[i] for i in pred*loss_mask[0] if i != data.v2i["<PAD>"]],
)
t0 = t1
os.makedirs("./visual/models/%s" % name, exist_ok=True)
model.save_weights("./visual/models/%s/model.ckpt" % name)
def export_attention(model, data, name="bert"):
model.load_weights("./visual/models/%s/model.ckpt" % name)
# save attention matrix for visualization
seqs, segs, xlen, nsp_labels = data.sample(32)
model.call(seqs, segs, False)
data = {"src": [[data.i2v[i] for i in seqs[j]] for j in range(len(seqs))], "attentions": model.attentions}
path = "./visual/tmp/%s_attention_matrix.pkl" % name
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
pickle.dump(data, f)
if __name__ == "__main__":
utils.set_soft_gpu(True)
MODEL_DIM = 256
N_LAYER = 4
LEARNING_RATE = 1e-4
MASK_RATE = 0.15
d = utils.MRPCData("./MRPC", 2000)
print("num word: ", d.num_word)
m = BERT(
model_dim=MODEL_DIM, max_len=d.max_len, n_layer=N_LAYER, n_head=4, n_vocab=d.num_word,
lr=LEARNING_RATE, max_seg=d.num_seg, drop_rate=0.2, padding_idx=d.v2i["<PAD>"])
train(m, d, step=10000, name="bert")
export_attention(m, d, "bert")
5以月光客栈掌柜的教程进行学习
bert主要分为三个部分:
- input embedding部分
- bert模型部分
- 下游任务
5.0config的实现
创建一个文件名称为BertConfig.py
import json
import copy
import six
import logging
class BertConfig(object):
"""Configuration for `BertModel`."""
def __init__(self,
vocab_size=21128,
hidden_size=768,
num_hidden_layers=12,
num_attention_heads=12,
intermediate_size=3072,
pad_token_id=0,
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):
"""Constructs BertConfig.
Args:
vocab_size: Vocabulary size of `inputs_ids` in `BertModel`.
hidden_size: Size of the encoder layers and the pooler layer.
num_hidden_layers: Number of hidden layers in the Transformer encoder.
num_attention_heads: Number of attention heads for each attention layer in
the Transformer encoder.
intermediate_size: The size of the "intermediate" (i.e., feed-forward)
layer in the Transformer encoder.
hidden_act: The non-linear activation function (function or string) in the
encoder and pooler.
hidden_dropout_prob: The dropout probability for all fully connected
layers in the embeddings, encoder, and pooler.
attention_probs_dropout_prob: The dropout ratio for the attention
probabilities.
max_position_embeddings: The maximum sequence length that this model might
ever be used with. Typically set this to something large just in case
(e.g., 512 or 1024 or 2048).
type_vocab_size: The vocabulary size of the `token_type_ids` passed into
`BertModel`.
initializer_range: The stdev of the truncated_normal_initializer for
initializing all weight matrices.
"""
self.vocab_size = vocab_size
self.hidden_size = hidden_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.hidden_act = hidden_act
self.intermediate_size = intermediate_size
self.pad_token_id = pad_token_id
self.hidden_dropout_prob = hidden_dropout_prob
self.attention_probs_dropout_prob = attention_probs_dropout_prob
self.max_position_embeddings = max_position_embeddings
self.type_vocab_size = type_vocab_size
self.initializer_range = initializer_range
@classmethod
def from_dict(cls, json_object):
"""Constructs a `BertConfig` from a Python dictionary of parameters."""
config = BertConfig(vocab_size=None)
for (key, value) in six.iteritems(json_object):
config.__dict__[key] = value
return config
@classmethod
def from_json_file(cls, json_file):
"""Constructs a `BertConfig` from a json file of parameters."""
"""从json配置文件读取配置信息"""
with open(json_file, 'r') as reader:
text = reader.read()
logging.info(f"成功导入BERT配置文件 {json_file}")
return cls.from_dict(json.loads(text))
def to_dict(self):
"""Serializes this instance to a Python dictionary."""
output = copy.deepcopy(self.__dict__)
return output
def to_json_string(self):
"""Serializes this instance to a JSON string."""
return json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n"
if __name__ == '__main__':
json_file = '../bert_base_chinese/config.json'
config = BertConfig.from_json_file(json_file)
print(config.hidden_size)
position_embedding_type = getattr(config, "position_embedding_type", "absolute")
print(position_embedding_type)
5.1Transformer的实现
from torch.nn.init import xavier_uniform_
import torch.nn.functional as F
import torch.nn as nn
import copy
import torch
class MyTransformer(nn.Module):
def __init__(self, d_model=512, nhead=8, num_encoder_layers=6,
num_decoder_layers=6, dim_feedforward=2048, dropout=0.1,
):
super(MyTransformer, self).__init__()
"""
:param d_model: d_k = d_v = d_model/nhead = 64, 模型中向量的维度,论文默认值为 512
:param nhead: 多头注意力机制中多头的数量,论文默认为值 8
:param num_encoder_layers: encoder堆叠的数量,也就是论文中的N,论文默认值为6
:param num_decoder_layers: decoder堆叠的数量,也就是论文中的N,论文默认值为6
:param dim_feedforward: 全连接中向量的维度,论文默认值为 2048
:param dropout: 丢弃率,论文中的默认值为 0.1
"""
# ================ 编码部分 =====================
encoder_layer = MyTransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout)
encoder_norm = nn.LayerNorm(d_model)
self.encoder = MyTransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm)
# ================ 解码部分 =====================
decoder_layer = MyTransformerDecoderLayer(d_model, nhead, dim_feedforward, dropout)
decoder_norm = nn.LayerNorm(d_model)
self.decoder = MyTransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm)
self._reset_parameters()
self.d_model = d_model
self.nhead = nhead
def _reset_parameters(self):
r"""Initiate parameters in the transformer model."""
"""
初始化
"""
for p in self.parameters():
if p.dim() > 1:
xavier_uniform_(p)
def forward(self, src, tgt, src_mask=None, tgt_mask=None,
memory_mask=None, src_key_padding_mask=None,
tgt_key_padding_mask=None, memory_key_padding_mask=None):
"""
:param src: [src_len,batch_size,embed_dim]
:param tgt: [tgt_len, batch_size, embed_dim]
:param src_mask: None
:param tgt_mask: [tgt_len, tgt_len]
:param memory_mask: None
:param src_key_padding_mask: [batch_size, src_len]
:param tgt_key_padding_mask: [batch_size, tgt_len]
:param memory_key_padding_mask: [batch_size, src_len]
:return: [tgt_len, batch_size, num_heads * kdim] <==> [tgt_len,batch_size,embed_dim]
"""
memory = self.encoder(src, mask=src_mask, src_key_padding_mask=src_key_padding_mask)
# [src_len, batch_size, num_heads * kdim] <==> [src_len,batch_size,embed_dim]
output = self.decoder(tgt=tgt, memory=memory, tgt_mask=tgt_mask, memory_mask=memory_mask,
tgt_key_padding_mask=tgt_key_padding_mask,
memory_key_padding_mask=memory_key_padding_mask)
return output # [tgt_len, batch_size, num_heads * kdim] <==> [tgt_len,batch_size,embed_dim]
def generate_square_subsequent_mask(self, sz):
r"""Generate a square mask for the sequence. The masked positions are filled with float('-inf').
Unmasked positions are filled with float(0.0).
"""
mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask # [sz,sz]
class MyTransformerEncoderLayer(nn.Module):
def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1):
super(MyTransformerEncoderLayer, self).__init__()
"""
:param d_model: d_k = d_v = d_model/nhead = 64, 模型中向量的维度,论文默认值为 512
:param nhead: 多头注意力机制中多头的数量,论文默认为值 8
:param dim_feedforward: 全连接中向量的维度,论文默认值为 2048
:param dropout: 丢弃率,论文中的默认值为 0.1
"""
self.self_attn = MyMultiheadAttention(d_model, nhead, dropout=dropout)
# Implementation of Feedforward model
self.dropout1 = nn.Dropout(dropout)
self.norm1 = nn.LayerNorm(d_model)
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.activation = F.relu
self.dropout2 = nn.Dropout(dropout)
self.norm2 = nn.LayerNorm(d_model)
def forward(self, src, src_mask=None, src_key_padding_mask=None):
"""
:param src: 编码部分的输入,形状为 [src_len,batch_size, embed_dim]
:param src_mask: 编码部分输入的padding情况,形状为 [batch_size, src_len]
:return:
"""
src2 = self.self_attn(src, src, src, attn_mask=src_mask,
key_padding_mask=src_key_padding_mask, )[0] # 计算多头注意力
# src2: [src_len,batch_size,num_heads*kdim] num_heads*kdim = embed_dim
src = src + self.dropout1(src2) # 残差连接
src = self.norm1(src) # [src_len,batch_size,num_heads*kdim]
src2 = self.activation(self.linear1(src)) # [src_len,batch_size,dim_feedforward]
src2 = self.linear2(self.dropout(src2)) # [src_len,batch_size,num_heads*kdim]
src = src + self.dropout2(src2)
src = self.norm2(src)
return src # [src_len, batch_size, num_heads * kdim] <==> [src_len,batch_size,embed_dim]
class MyTransformerEncoder(nn.Module):
def __init__(self, encoder_layer, num_layers, norm=None):
super(MyTransformerEncoder, self).__init__()
"""
encoder_layer: 就是包含有多头注意力机制的一个编码层
num_layers: 克隆得到多个encoder layers 论文中默认为6
norm: 归一化层
"""
self.layers = _get_clones(encoder_layer, num_layers) # 克隆得到多个encoder layers 论文中默认为6
self.num_layers = num_layers
self.norm = norm
def forward(self, src, mask=None, src_key_padding_mask=None):
"""
:param src: 编码部分的输入,形状为 [src_len,batch_size, embed_dim]
:param mask: 编码部分输入的padding情况,形状为 [batch_size, src_len]
:return:# [src_len, batch_size, num_heads * kdim] <==> [src_len,batch_size,embed_dim]
"""
output = src
for mod in self.layers:
output = mod(output, src_mask=mask,
src_key_padding_mask=src_key_padding_mask) # 多个encoder layers层堆叠后的前向传播过程
if self.norm is not None:
output = self.norm(output)
return output # [src_len, batch_size, num_heads * kdim] <==> [src_len,batch_size,embed_dim]
def _get_clones(module, N):
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
class MyTransformerDecoderLayer(nn.Module):
def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1):
super(MyTransformerDecoderLayer, self).__init__()
"""
:param d_model: d_k = d_v = d_model/nhead = 64, 模型中向量的维度,论文默认值为 512
:param nhead: 多头注意力机制中多头的数量,论文默认为值 8
:param dim_feedforward: 全连接中向量的维度,论文默认值为 2048
:param dropout: 丢弃率,论文中的默认值为 0.1
"""
self.self_attn = MyMultiheadAttention(embed_dim=d_model, num_heads=nhead, dropout=dropout)
# 解码部分输入序列之间的多头注意力(也就是论文结构图中的Masked Multi-head attention)
self.multihead_attn = MyMultiheadAttention(embed_dim=d_model, num_heads=nhead, dropout=dropout)
# 编码部分输出(memory)和解码部分之间的多头注意力机制。
# Implementation of Feedforward model
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, d_model)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
self.activation = F.relu
def forward(self, tgt, memory, tgt_mask=None, memory_mask=None, tgt_key_padding_mask=None,
memory_key_padding_mask=None):
"""
:param tgt: 解码部分的输入,形状为 [tgt_len,batch_size, embed_dim]
:param memory: 编码部分的输出(memory), [src_len,batch_size,embed_dim]
:param tgt_mask: 注意力Mask输入,用于掩盖当前position之后的信息, [tgt_len, tgt_len]
:param memory_mask: 编码器-解码器交互时的注意力掩码,一般为None
:param tgt_key_padding_mask: 解码部分输入的padding情况,形状为 [batch_size, tgt_len]
:param memory_key_padding_mask: 编码部分输入的padding情况,形状为 [batch_size, src_len]
:return:
"""
tgt2 = self.self_attn(tgt, tgt, tgt, # [tgt_len,batch_size, embed_dim]
attn_mask=tgt_mask,
key_padding_mask=tgt_key_padding_mask)[0]
# 解码部分输入序列之间'的多头注意力(也就是论文结构图中的Masked Multi-head attention)
tgt = tgt + self.dropout1(tgt2) # 接着是残差连接
tgt = self.norm1(tgt) # [tgt_len,batch_size, embed_dim]
tgt2 = self.multihead_attn(tgt, memory, memory, # [tgt_len, batch_size, embed_dim]
attn_mask=memory_mask,
key_padding_mask=memory_key_padding_mask)[0]
# 解码部分的输入经过多头注意力后同编码部分的输出(memory)通过多头注意力机制进行交互
tgt = tgt + self.dropout2(tgt2) # 残差连接
tgt = self.norm2(tgt) # [tgt_len, batch_size, embed_dim]
tgt2 = self.activation(self.linear1(tgt)) # [tgt_len, batch_size, dim_feedforward]
tgt2 = self.linear2(self.dropout(tgt2)) # [tgt_len, batch_size, embed_dim]
# 最后的两层全连接
tgt = tgt + self.dropout3(tgt2)
tgt = self.norm3(tgt)
return tgt # [tgt_len, batch_size, num_heads * kdim] <==> [tgt_len,batch_size,embed_dim]
class MyTransformerDecoder(nn.Module):
def __init__(self, decoder_layer, num_layers, norm=None):
super(MyTransformerDecoder, self).__init__()
self.layers = _get_clones(decoder_layer, num_layers)
self.num_layers = num_layers
self.norm = norm
def forward(self, tgt, memory, tgt_mask=None, memory_mask=None, tgt_key_padding_mask=None,
memory_key_padding_mask=None):
"""
:param tgt: 解码部分的输入,形状为 [tgt_len,batch_size, embed_dim]
:param memory: 编码部分最后一层的输出 [src_len,batch_size, embed_dim]
:param tgt_mask: 注意力Mask输入,用于掩盖当前position之后的信息, [tgt_len, tgt_len]
:param memory_mask: 编码器-解码器交互时的注意力掩码,一般为None
:param tgt_key_padding_mask: 解码部分输入的padding情况,形状为 [batch_size, tgt_len]
:param memory_key_padding_mask: 编码部分输入的padding情况,形状为 [batch_size, src_len]
:return:
"""
output = tgt # [tgt_len,batch_size, embed_dim]
for mod in self.layers: # 这里的layers就是N层解码层堆叠起来的
output = mod(output, memory,
tgt_mask=tgt_mask,
memory_mask=memory_mask,
tgt_key_padding_mask=tgt_key_padding_mask,
memory_key_padding_mask=memory_key_padding_mask)
if self.norm is not None:
output = self.norm(output)
return output # [tgt_len, batch_size, num_heads * kdim] <==> [tgt_len,batch_size,embed_dim]
class MyMultiheadAttention(nn.Module):
"""
多头注意力机制的计算公式为(就是论文第5页的公式):
.. math::
\text{MultiHead}(Q, K, V) = \text{Concat}(head_1,\dots,head_h)W^O
\text{where} head_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
"""
def __init__(self, embed_dim, num_heads, dropout=0., bias=True):
super(MyMultiheadAttention, self).__init__()
"""
:param embed_dim: 词嵌入的维度,也就是前面的d_model参数,论文中的默认值为512
:param num_heads: 多头注意力机制中多头的数量,也就是前面的nhead参数, 论文默认值为 8
:param dropout:
:param bias: 最后对多头的注意力(组合)输出进行线性变换时,是否使用偏置
"""
self.embed_dim = embed_dim # 前面的d_model参数
self.head_dim = embed_dim // num_heads # head_dim 指的就是d_k,d_v
self.kdim = self.head_dim
self.vdim = self.head_dim
self.num_heads = num_heads # 多头个数
self.dropout = dropout
assert self.head_dim * num_heads == self.embed_dim, "embed_dim 除以 num_heads必须为整数"
# 上面的限制条件就是论文中的 d_k = d_v = d_model/n_head 条件
self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias) # embed_dim = kdim * num_heads
# 这里第二个维度之所以是embed_dim,实际上这里是同时初始化了num_heads个W_q堆叠起来的, 也就是num_heads个头
self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias) # W_k, embed_dim = kdim * num_heads
self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias) # W_v, embed_dim = vdim * num_heads
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)
# 最后将所有的Z组合起来的时候,也是一次性完成, embed_dim = vdim * num_heads
def forward(self, query, key, value, attn_mask=None, key_padding_mask=None):
"""
在论文中,编码时query, key, value 都是同一个输入, 解码时 输入的部分也都是同一个输入,
解码和编码交互时 key,value指的是 memory, query指的是tgt
:param query: # [tgt_len, batch_size, embed_dim], tgt_len 表示目标序列的长度
:param key: # [src_len, batch_size, embed_dim], src_len 表示源序列的长度
:param value: # [src_len, batch_size, embed_dim], src_len 表示源序列的长度
:param attn_mask: # [tgt_len,src_len] or [num_heads*batch_size,tgt_len, src_len]
一般只在解码时使用,为了并行一次喂入所有解码部分的输入,所以要用mask来进行掩盖当前时刻之后的位置信息
:param key_padding_mask: [batch_size, src_len], src_len 表示源序列的长度
:return:
attn_output: [tgt_len, batch_size, embed_dim]
attn_output_weights: # [batch_size, tgt_len, src_len]
"""
return multi_head_attention_forward(query, key, value, self.num_heads,
self.dropout,
out_proj=self.out_proj,
training=self.training,
key_padding_mask=key_padding_mask,
q_proj=self.q_proj,
k_proj=self.k_proj,
v_proj=self.v_proj,
attn_mask=attn_mask)
def multi_head_attention_forward(query, # [tgt_len,batch_size, embed_dim]
key, # [src_len, batch_size, embed_dim]
value, # [src_len, batch_size, embed_dim]
num_heads,
dropout_p,
out_proj,
training=True,
key_padding_mask=None, # [batch_size,src_len/tgt_len]
q_proj=None, # weight: [embed_dim,kdim * num_heads] , bias: [embed_dim]
k_proj=None, # weight: [embed_dim,kdim * num_heads] , bias: [embed_dim]
v_proj=None, # weight: [embed_dim,kdim * num_heads] , bias: [embed_dim]
attn_mask=None, # [tgt_len,src_len] or [num_heads*batch_size,tgt_len, src_len]
):
q = q_proj(query)
# [tgt_len,batch_size, embed_dim] x [embed_dim,kdim * num_heads] = [tgt_len,batch_size,kdim * num_heads]
k = k_proj(key)
# [src_len, batch_size, embed_dim] x [embed_dim, kdim * num_heads] = [src_len, batch_size, kdim * num_heads]
v = v_proj(value)
# [src_len, batch_size, embed_dim] x [embed_dim, vdim * num_heads] = [src_len, batch_size, vdim * num_heads]
tgt_len, bsz, embed_dim = query.size() # [tgt_len,batch_size, embed_dim]
src_len = key.size(0)
head_dim = embed_dim // num_heads # num_heads * head_dim = embed_dim
scaling = float(head_dim) ** -0.5
q = q * scaling # [query_len,batch_size,kdim * num_heads]
if attn_mask is not None: # [tgt_len,src_len] or [num_heads*batch_size,tgt_len, src_len]
if attn_mask.dim() == 2:
attn_mask = attn_mask.unsqueeze(0) # [1, tgt_len,src_len]
if list(attn_mask.size()) != [1, query.size(0), key.size(0)]:
raise RuntimeError('The size of the 2D attn_mask is not correct.')
elif attn_mask.dim() == 3:
if list(attn_mask.size()) != [bsz * num_heads, query.size(0), key.size(0)]:
raise RuntimeError('The size of the 3D attn_mask is not correct.')
# 现在 atten_mask 的维度就变成了3D
q = q.contiguous().view(tgt_len, bsz * num_heads, head_dim).transpose(0, 1)
# [batch_size * num_heads,tgt_len,kdim]
# 因为前面是num_heads个头一起参与的计算,所以这里要进行一下变形,以便于后面计算。 且同时交换了0,1两个维度
k = k.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1) # [batch_size * num_heads,src_len,kdim]
v = v.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1) # [batch_size * num_heads,src_len,vdim]
attn_output_weights = torch.bmm(q, k.transpose(1, 2))
# [batch_size * num_heads,tgt_len,kdim] x [batch_size * num_heads, kdim, src_len]
# = [batch_size * num_heads, tgt_len, src_len] 这就num_heads个QK相乘后的注意力矩阵
if attn_mask is not None:
attn_output_weights += attn_mask # [batch_size * num_heads, tgt_len, src_len]
if key_padding_mask is not None:
attn_output_weights = attn_output_weights.view(bsz, num_heads, tgt_len, src_len)
# 变成 [batch_size, num_heads, tgt_len, src_len]的形状
attn_output_weights = attn_output_weights.masked_fill(
key_padding_mask.unsqueeze(1).unsqueeze(2),
float('-inf')) #
# 扩展维度,key_padding_mask从[batch_size,src_len]变成[batch_size,1,1,src_len]
# 然后再对attn_output_weights进行填充
attn_output_weights = attn_output_weights.view(bsz * num_heads, tgt_len,
src_len) # [batch_size * num_heads, tgt_len, src_len]
attn_output_weights = F.softmax(attn_output_weights, dim=-1) # [batch_size * num_heads, tgt_len, src_len]
attn_output_weights = F.dropout(attn_output_weights, p=dropout_p, training=training)
attn_output = torch.bmm(attn_output_weights, v)
# Z = [batch_size * num_heads, tgt_len, src_len] x [batch_size * num_heads,src_len,vdim]
# = # [batch_size * num_heads,tgt_len,vdim]
# 这就num_heads个Attention(Q,K,V)结果
attn_output = attn_output.transpose(0, 1).contiguous().view(tgt_len, bsz, embed_dim)
# 先transpose成 [tgt_len, batch_size* num_heads ,kdim]
# 再view成 [tgt_len,batch_size,num_heads*kdim]
attn_output_weights = attn_output_weights.view(bsz, num_heads, tgt_len, src_len)
Z = out_proj(attn_output)
# 这里就是多个z 线性组合成Z [tgt_len,batch_size,embed_dim]
return Z, attn_output_weights.sum(dim=1) / num_heads # average attention weights over heads
if __name__ == '__main__':
src_len = 5
batch_size = 2
dmodel = 32
tgt_len = 6
num_head = 8
src = torch.rand((src_len, batch_size, dmodel)) # shape: [src_len, batch_size, embed_dim]
src_key_padding_mask = torch.tensor([[True, True, True, False, False],
[True, True, True, True, False]]) # shape: [batch_size, src_len]
tgt = torch.rand((tgt_len, batch_size, dmodel)) # shape: [tgt_len, batch_size, embed_dim]
tgt_key_padding_mask = torch.tensor([[True, True, True, False, False, False],
[True, True, True, True, False, False]]) # shape: [batch_size, tgt_len]
# ============ 测试 MyTransformer ============
my_transformer = MyTransformer(d_model=dmodel, nhead=num_head, num_encoder_layers=6,
num_decoder_layers=6, dim_feedforward=500)
src_mask = my_transformer.generate_square_subsequent_mask(src_len)
tgt_mask = my_transformer.generate_square_subsequent_mask(tgt_len)
out = my_transformer(src=src, tgt=tgt, tgt_mask=tgt_mask,
src_key_padding_mask=src_key_padding_mask,
tgt_key_padding_mask=tgt_key_padding_mask,
memory_key_padding_mask=src_key_padding_mask)
print(out.shape)
5.1input embedding实现
创建一个文件名称为BertEmbedding.py
import torch.nn as nn
import torch
from torch.nn.init import normal_
class PositionalEmbedding(nn.Module):
"""
位置编码。
*** 注意: Bert中的位置编码完全不同于Transformer中的位置编码,
前者本质上也是一个普通的Embedding层,而后者是通过公式计算得到,
而这也是为什么Bert只能接受长度为512字符的原因,因为位置编码的最大size为512 ***
# Since the position embedding table is a learned variable, we create it
# using a (long) sequence length `max_position_embeddings`. The actual
# sequence length might be shorter than this, for faster training of
# tasks that do not have long sequences.
———————— GoogleResearch
https://github.com/google-research/bert/blob/eedf5716ce1268e56f0a50264a88cafad334ac61/modeling.py
"""
def __init__(self, hidden_size, max_position_embeddings=512, initializer_range=0.02):
super(PositionalEmbedding, self).__init__()
# 因为BERT预训练模型的长度为512
self.embedding = nn.Embedding(max_position_embeddings, hidden_size)
self._reset_parameters(initializer_range)
def forward(self, position_ids):
"""
:param position_ids: [1,position_ids_len]
:return: [position_ids_len, 1, hidden_size]
"""
return self.embedding(position_ids).transpose(0, 1)
def _reset_parameters(self, initializer_range):
r"""Initiate parameters."""
"""
初始化
"""
for p in self.parameters():
if p.dim() > 1:
normal_(p, mean=0.0, std=initializer_range)
class TokenEmbedding(nn.Module):
def __init__(self, vocab_size, hidden_size, pad_token_id=0, initializer_range=0.02):
super(TokenEmbedding, self).__init__()
self.embedding = nn.Embedding(vocab_size, hidden_size, padding_idx=pad_token_id)
self._reset_parameters(initializer_range)
def forward(self, input_ids):
"""
:param input_ids: shape : [input_ids_len, batch_size]
:return: shape: [input_ids_len, batch_size, hidden_size]
"""
return self.embedding(input_ids)
def _reset_parameters(self, initializer_range):
r"""Initiate parameters."""
"""
初始化
"""
for p in self.parameters():
if p.dim() > 1:
normal_(p, mean=0.0, std=initializer_range)
class SegmentEmbedding(nn.Module):
def __init__(self, type_vocab_size, hidden_size, initializer_range=0.02):
super(SegmentEmbedding, self).__init__()
self.embedding = nn.Embedding(type_vocab_size, hidden_size)
self._reset_parameters(initializer_range)
def forward(self, token_type_ids):
"""
:param token_type_ids: shape: [token_type_ids_len, batch_size]
:return: shape: [token_type_ids_len, batch_size, hidden_size]
"""
return self.embedding(token_type_ids)
def _reset_parameters(self, initializer_range):
r"""Initiate parameters."""
"""
初始化
"""
for p in self.parameters():
if p.dim() > 1:
normal_(p, mean=0.0, std=initializer_range)
class BertEmbeddings(nn.Module):
"""
BERT Embedding which is consisted with under features
1. TokenEmbedding : normal embedding matrix
2. PositionalEmbedding : normal embedding matrix
2. SegmentEmbedding : adding sentence segment info, (sent_A:1, sent_B:2)
sum of all these features are output of BERTEmbedding
"""
def __init__(self, config):
super().__init__()
self.word_embeddings = TokenEmbedding(vocab_size=config.vocab_size,
hidden_size=config.hidden_size,
pad_token_id=config.pad_token_id,
initializer_range=config.initializer_range)
# return shape [src_len,batch_size,hidden_size]
self.position_embeddings = PositionalEmbedding(max_position_embeddings=config.max_position_embeddings,
hidden_size=config.hidden_size,
initializer_range=config.initializer_range)
# return shape [src_len,1,hidden_size]
self.token_type_embeddings = SegmentEmbedding(type_vocab_size=config.type_vocab_size,
hidden_size=config.hidden_size,
initializer_range=config.initializer_range)
# return shape [src_len,batch_size,hidden_size]
self.LayerNorm = nn.LayerNorm(config.hidden_size)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.register_buffer("position_ids",
torch.arange(config.max_position_embeddings).expand((1, -1)))
# shape: [1, max_position_embeddings]
def forward(self,
input_ids=None,
position_ids=None,
token_type_ids=None):
"""
:param input_ids: 输入序列的原始token id, shape: [src_len, batch_size]
:param position_ids: 位置序列,本质就是 [0,1,2,3,...,src_len-1], shape: [1,src_len]
:param token_type_ids: 句子分隔token, 例如[0,0,0,0,1,1,1,1]用于区分两个句子 shape:[src_len,batch_size]
:return: [src_len, batch_size, hidden_size]
"""
src_len = input_ids.size(0)
token_embedding = self.word_embeddings(input_ids)
# shape:[src_len,batch_size,hidden_size]
if position_ids is None: # 在实际建模时这个参数其实可以不用传值
position_ids = self.position_ids[:, :src_len] # [1,src_len]
positional_embedding = self.position_embeddings(position_ids)
# [src_len, 1, hidden_size]
if token_type_ids is None: # 如果输入模型的只有一个序列,那么这个参数也不用传值
token_type_ids = torch.zeros_like(input_ids,
device=self.position_ids.device) # [src_len, batch_size]
segment_embedding = self.token_type_embeddings(token_type_ids)
# [src_len,batch_size,hidden_size]
embeddings = token_embedding + positional_embedding + segment_embedding
# [src_len,batch_size,hidden_size] + [src_len,1,hidden_size] + [src_len,batch_size,hidden_size]
embeddings = self.LayerNorm(embeddings) # [src_len, batch_size, hidden_size]
embeddings = self.dropout(embeddings)
return embeddings
if __name__ == '__main__':
json_file = '../bert_base_chinese/config.json'
config = BertConfig.from_json_file(json_file)
src = torch.tensor([[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]], dtype=torch.long)
src = src.transpose(0, 1) # [src_len, batch_size]
# ***** --------- 测试TokenEmbedding ------------
token_embedding = TokenEmbedding(vocab_size=config.vocab_size, hidden_size=config.hidden_size)
t_embedding = token_embedding(input_ids=src)
print("***** --------- 测试TokenEmbedding ------------")
print("input_token shape [src_len,batch_size]: ", src.shape)
print(f"input_token embedding shape [src_len,batch_size,hidden_size]: {t_embedding.shape}\n")
# ***** --------- 测试PositionalEmbedding ------------
position_ids = torch.arange(src.size()[0]).expand((1, -1))
pos_embedding = PositionalEmbedding(max_position_embeddings=6,
hidden_size=8)
p_embedding = pos_embedding(position_ids=position_ids)
# print(pos_embedding.embedding.weight) # embedding 矩阵
# print(p_embedding) # positional embedding 结果,
print("***** --------- 测试PositionalEmbedding ------------")
print("position_ids shape [1,src_len]: ", position_ids.shape)
print(f"pos embedding shape [src_len, 1, hidden_size]: {p_embedding.shape}\n")
# ***** --------- 测试SegmentEmbedding ------------
token_type_ids = torch.LongTensor([[0, 0, 0, 1, 1], [0, 0, 1, 1, 1]]).transpose(0, 1)
seg_embedding = SegmentEmbedding(hidden_size=config.hidden_size, type_vocab_size=config.type_vocab_size)
token_type_ids_embedding = seg_embedding(token_type_ids)
print("***** --------- 测试SegmentEmbedding ------------")
print(seg_embedding.embedding.weight.requires_grad)
print("token_type_ids shape [src_len,batch_size]: ", token_type_ids.shape)
print(f"seg embedding shape [src_len, batch_size, hidden_size]: {token_type_ids_embedding.shape}\n")
# ***** --------- 测试BertEmbedding ------------
bert_embedding = BertEmbeddings(config)
bert_embedding_result = bert_embedding(src, token_type_ids=token_type_ids)
print("***** --------- 测试BertEmbedding ------------")
print("bert embedding shape [src_len, batch_size, hidden_size]: ", bert_embedding_result.shape)
5.2BertModel实现
创建一个文件名称为Bert.py
import torch
from torch.nn.init import normal_
from .BertEmbedding import BertEmbeddings
from .MyTransformer import MyMultiheadAttention
import torch.nn as nn
import os
import logging
from copy import deepcopy
def get_activation(activation_string):
act = activation_string.lower()
if act == "linear":
return None
elif act == "relu":
return nn.ReLU()
elif act == "gelu":
return nn.GELU()
elif act == "tanh":
return nn.Tanh()
else:
raise ValueError("Unsupported activation: %s" % act)
class BertSelfAttention(nn.Module):
"""
实现多头注意力机制,对应的是GoogleResearch代码中的attention_layer方法
https://github.com/google-research/bert/blob/eedf5716ce1268e56f0a50264a88cafad334ac61/modeling.py#L558
"""
def __init__(self, config):
super(BertSelfAttention, self).__init__()
if 'use_torch_multi_head' in config.__dict__ and config.use_torch_multi_head:
MultiHeadAttention = nn.MultiheadAttention
else:
MultiHeadAttention = MyMultiheadAttention
self.multi_head_attention = MultiHeadAttention(embed_dim=config.hidden_size,
num_heads=config.num_attention_heads,
dropout=config.attention_probs_dropout_prob)
def forward(self, query, key, value, attn_mask=None, key_padding_mask=None):
"""
:param query: # [tgt_len, batch_size, hidden_size], tgt_len 表示目标序列的长度
:param key: # [src_len, batch_size, hidden_size], src_len 表示源序列的长度
:param value: # [src_len, batch_size, hidden_size], src_len 表示源序列的长度
:param attn_mask: # [tgt_len,src_len] or [num_heads*batch_size,tgt_len, src_len]
一般只在解码时使用,为了并行一次喂入所有解码部分的输入,所以要用mask来进行掩盖当前时刻之后的位置信息
在Bert中,attention_mask指代的其实是key_padding_mask,因为Bert主要是基于Transformer Encoder部分构建的,
所有没有Decoder部分,因此也就不需要用mask来进行掩盖当前时刻之后的位置信息
:param key_padding_mask: [batch_size, src_len], src_len 表示源序列的长度
:return:
attn_output: [tgt_len, batch_size, hidden_size]
attn_output_weights: # [batch_size, tgt_len, src_len]
"""
return self.multi_head_attention(query, key, value, attn_mask=attn_mask, key_padding_mask=key_padding_mask)
class BertSelfOutput(nn.Module):
def __init__(self, config):
super().__init__()
# self.dense = nn.Linear(config.hidden_size, config.hidden_size)
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=1e-12)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, hidden_states, input_tensor):
"""
:param hidden_states: [src_len, batch_size, hidden_size]
:param input_tensor: [src_len, batch_size, hidden_size]
:return: [src_len, batch_size, hidden_size]
"""
# hidden_states = self.dense(hidden_states) # [src_len, batch_size, hidden_size]
hidden_states = self.dropout(hidden_states)
hidden_states = self.LayerNorm(hidden_states + input_tensor)
return hidden_states
class BertAttention(nn.Module):
def __init__(self, config):
super().__init__()
self.self = BertSelfAttention(config)
self.output = BertSelfOutput(config)
def forward(self,
hidden_states,
attention_mask=None):
"""
:param hidden_states: [src_len, batch_size, hidden_size]
:param attention_mask: [batch_size, src_len]
:return: [src_len, batch_size, hidden_size]
"""
self_outputs = self.self(hidden_states,
hidden_states,
hidden_states,
attn_mask=None,
key_padding_mask=attention_mask)
# self_outputs[0] shape: [src_len, batch_size, hidden_size]
attention_output = self.output(self_outputs[0], hidden_states)
return attention_output
class BertIntermediate(nn.Module):
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
if isinstance(config.hidden_act, str):
self.intermediate_act_fn = get_activation(config.hidden_act)
else:
self.intermediate_act_fn = config.hidden_act
def forward(self, hidden_states):
"""
:param hidden_states: [src_len, batch_size, hidden_size]
:return: [src_len, batch_size, intermediate_size]
"""
hidden_states = self.dense(hidden_states) # [src_len, batch_size, intermediate_size]
if self.intermediate_act_fn is None:
hidden_states = hidden_states
else:
hidden_states = self.intermediate_act_fn(hidden_states)
return hidden_states
class BertOutput(nn.Module):
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.intermediate_size, config.hidden_size)
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=1e-12)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, hidden_states, input_tensor):
"""
:param hidden_states: [src_len, batch_size, intermediate_size]
:param input_tensor: [src_len, batch_size, hidden_size]
:return: [src_len, batch_size, hidden_size]
"""
hidden_states = self.dense(hidden_states) # [src_len, batch_size, hidden_size]
hidden_states = self.dropout(hidden_states)
hidden_states = self.LayerNorm(hidden_states + input_tensor)
return hidden_states
class BertLayer(nn.Module):
def __init__(self, config):
super().__init__()
self.bert_attention = BertAttention(config)
self.bert_intermediate = BertIntermediate(config)
self.bert_output = BertOutput(config)
def forward(self,
hidden_states,
attention_mask=None):
"""
:param hidden_states: [src_len, batch_size, hidden_size]
:param attention_mask: [batch_size, src_len] mask掉padding部分的内容
:return: [src_len, batch_size, hidden_size]
"""
attention_output = self.bert_attention(hidden_states, attention_mask)
# [src_len, batch_size, hidden_size]
intermediate_output = self.bert_intermediate(attention_output)
# [src_len, batch_size, intermediate_size]
layer_output = self.bert_output(intermediate_output, attention_output)
# [src_len, batch_size, hidden_size]
return layer_output
class BertEncoder(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.bert_layers = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
def forward(
self,
hidden_states,
attention_mask=None):
"""
:param hidden_states: [src_len, batch_size, hidden_size]
:param attention_mask: [batch_size, src_len]
:return:
"""
all_encoder_layers = []
layer_output = hidden_states
'''重复n个编码层 '''
for i, layer_module in enumerate(self.bert_layers):
layer_output = layer_module(layer_output,
attention_mask)
# [src_len, batch_size, hidden_size]
all_encoder_layers.append(layer_output)
return all_encoder_layers
class BertPooler(nn.Module):
''' 在将 BertEncoder 部分的输出结果输入到下游任务前,需要将其进行略微的处理, '''
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.hidden_size)
self.activation = nn.Tanh()
self.config = config
def forward(self, hidden_states):
"""
:param hidden_states: [src_len, batch_size, hidden_size]
:return: [batch_size, hidden_size]
"""
'''来取 BertEncoder输出的第一个位置([cls]位置),例如在进行文本分类时可以取该位置上的结果进行下一步的分类处理 '''
if self.config.pooler_type == "first_token_transform":
token_tensor = hidden_states[0, :].reshape(-1, self.config.hidden_size)
'''是掌柜自己加入的一个选项,表示取所有位置的平均值,当然我们也可以根据自己的需要在添加下面添加其它的方式 '''
elif self.config.pooler_type == "all_token_average":
token_tensor = torch.mean(hidden_states, dim=0)
pooled_output = self.dense(token_tensor) # [batch_size, hidden_size]
pooled_output = self.activation(pooled_output)
return pooled_output # [batch_size, hidden_size]
def format_paras_for_torch(loaded_paras_names, loaded_paras):
"""
该函数的作用是将预训练参数格式化成符合torch(1.5.0)框架中MultiHeadAttention的参数形式
:param loaded_paras_names:
:param loaded_paras:
:return:
"""
qkv_weight_names = ['query.weight', 'key.weight', 'value.weight']
qkv_bias_names = ['query.bias', 'key.bias', 'value.bias']
qkv_weight, qkv_bias = [], []
torch_paras = []
for i in range(len(loaded_paras_names)):
para_name_in_pretrained = loaded_paras_names[i]
para_name = ".".join(para_name_in_pretrained.split('.')[-2:])
if para_name in qkv_weight_names:
qkv_weight.append(loaded_paras[para_name_in_pretrained])
elif para_name in qkv_bias_names:
qkv_bias.append(loaded_paras[para_name_in_pretrained])
else:
torch_paras.append(loaded_paras[para_name_in_pretrained])
if len(qkv_weight) == 3:
torch_paras.append(torch.cat(qkv_weight, dim=0))
qkv_weight = []
if len(qkv_bias) == 3:
torch_paras.append(torch.cat(qkv_bias, dim=0))
qkv_bias = []
return torch_paras
def replace_512_position(init_embedding, loaded_embedding):
"""
本函数的作用是当max_positional_embedding > 512时,用预训练模型中的512个向量来
替换随机初始化的positional embedding中的前512个向量
:param init_embedding: 初始化的positional embedding矩阵,大于512行
:param loaded_embedding: 预训练模型中的positional embedding矩阵,等于512行
:return: 前512行被替换后的初始化的positional embedding矩阵
"""
logging.info(f"模型参数max_positional_embedding > 512,采用替换处理!")
init_embedding[:512, :] = loaded_embedding[:512, :]
return init_embedding
class BertModel(nn.Module):
"""
"""
def __init__(self, config):
super().__init__()
self.bert_embeddings = BertEmbeddings(config)
self.bert_encoder = BertEncoder(config)
self.bert_pooler = BertPooler(config)
self.config = config
self._reset_parameters()
def forward(self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
position_ids=None):
"""
***** 一定要注意,attention_mask中,被mask的Token用1(True)表示,没有mask的用0(false)表示
这一点一定一定要注意
:param input_ids: [src_len, batch_size]
:param attention_mask: [batch_size, src_len] mask掉padding部分的内容
:param token_type_ids: [src_len, batch_size] # 如果输入模型的只有一个序列,那么这个参数也不用传值
:param position_ids: [1,src_len] # 在实际建模时这个参数其实可以不用传值
:return:
"""
''' Input Embedding 后的输出结果,其形状为[src_len, batch_size, hidden_size]; '''
embedding_output = self.bert_embeddings(input_ids=input_ids,
position_ids=position_ids,
token_type_ids=token_type_ids)
'''是整个 BERT 编码部分的输出,其中 all_encoder_outputs 为一个包含有 num_hidden_layers 个层的输出 '''
# embedding_output: [src_len, batch_size, hidden_size]
all_encoder_outputs = self.bert_encoder(embedding_output,
attention_mask=attention_mask)
'''处理得到整个 BERT 网络的输出,这里取了最后一层的输出,形状为[src_len, batch_size, hidden_size] '''
# all_encoder_outputs 为一个包含有num_hidden_layers个层的输出
sequence_output = all_encoder_outputs[-1] # 取最后一层
'''默认是最后一层的第 1 个 token 即[cls]位置经 dense + tanh 后的结果,其形状为[batch_size, hidden_size] '''
# sequence_output: [src_len, batch_size, hidden_size]
pooled_output = self.bert_pooler(sequence_output)
# 默认是最后一层的first token 即[cls]位置经dense + tanh 后的结果
# pooled_output: [batch_size, hidden_size]
return pooled_output, all_encoder_outputs
def _reset_parameters(self):
r"""Initiate parameters in the transformer model."""
"""
初始化
"""
for p in self.parameters():
if p.dim() > 1:
normal_(p, mean=0.0, std=self.config.initializer_range)
@classmethod
def from_pretrained(cls, config, pretrained_model_dir=None):
model = cls(config) # 初始化模型,cls为未实例化的对象,即一个未实例化的BertModel对象
pretrained_model_path = os.path.join(pretrained_model_dir, "pytorch_model.bin")
if not os.path.exists(pretrained_model_path):
raise ValueError(f"<路径:{pretrained_model_path} 中的模型不存在,请仔细检查!>")
loaded_paras = torch.load(pretrained_model_path)
state_dict = deepcopy(model.state_dict())
loaded_paras_names = list(loaded_paras.keys())[:-8]
model_paras_names = list(state_dict.keys())[1:]
if 'use_torch_multi_head' in config.__dict__ and config.use_torch_multi_head:
torch_paras = format_paras_for_torch(loaded_paras_names, loaded_paras)
for i in range(len(model_paras_names)):
logging.debug(f"## 成功赋值参数:{model_paras_names[i]},形状为: {torch_paras[i].size()}")
if "position_embeddings" in model_paras_names[i]:
# 这部分代码用来消除预训练模型只能输入小于512个字符的限制
if config.max_position_embeddings > 512:
new_embedding = replace_512_position(state_dict[model_paras_names[i]],
loaded_paras[loaded_paras_names[i]])
state_dict[model_paras_names[i]] = new_embedding
continue
state_dict[model_paras_names[i]] = torch_paras[i]
logging.info(f"## 注意,正在使用torch框架中的MultiHeadAttention实现")
else:
for i in range(len(loaded_paras_names)):
logging.debug(f"## 成功将参数:{loaded_paras_names[i]}赋值给{model_paras_names[i]},"
f"参数形状为:{state_dict[model_paras_names[i]].size()}")
if "position_embeddings" in model_paras_names[i]:
# 这部分代码用来消除预训练模型只能输入小于512个字符的限制
if config.max_position_embeddings > 512:
new_embedding = replace_512_position(state_dict[model_paras_names[i]],
loaded_paras[loaded_paras_names[i]])
state_dict[model_paras_names[i]] = new_embedding
continue
state_dict[model_paras_names[i]] = loaded_paras[loaded_paras_names[i]]
logging.info(f"## 注意,正在使用本地MyTransformer中的MyMultiHeadAttention实现,"
f"如需使用torch框架中的MultiHeadAttention模块可通过config.__dict__['use_torch_multi_head'] = True实现")
'''加载模型 '''
model.load_state_dict(state_dict)
return model
if __name__ == '__main__':
json_file = '../bert_base_chinese/config.json'
config = BertConfig.from_json_file(json_file)
config.__dict__['use_torch_multi_head'] = True # 表示使用 torch框架中的MultiHeadAttention 注意力实现方法
config.max_position_embeddings = 518 # 测试大于512时的情况
src = torch.tensor([[1, 3, 5, 7, 9, 2, 3], [2, 4, 6, 8, 10, 0, 0]], dtype=torch.long)
src = src.transpose(0, 1) # [src_len, batch_size]
print(f"input shape [src_len,batch_size]: ", src.shape)
token_type_ids = torch.LongTensor([[0, 0, 0, 1, 1, 1, 1], [0, 0, 1, 1, 1, 0, 0]]).transpose(0, 1)
attention_mask = torch.tensor([[True, True, True, True, True, True, True],
[True, True, True, True, True, False, False]])
# attention_mask 实际就是Transformer中指代的key_padding_mask
# ------ BertEmbedding -------
bert_embedding = BertEmbeddings(config)
bert_embedding_result = bert_embedding(src, token_type_ids=token_type_ids)
# [src_len, batch_size, hidden_size]
# 测试类BertAttention
bert_attention = BertAttention(config)
bert_attention_output = bert_attention(bert_embedding_result, attention_mask=attention_mask)
print(f"BertAttention output shape [src_len, batch_size, hidden_size]: ", bert_attention_output.shape)
# 测试类BertLayer
bert_layer = BertLayer(config)
bert_layer_output = bert_layer(bert_embedding_result, attention_mask)
print(f"BertLayer output shape [src_len, batch_size, hidden_size]: ", bert_layer_output.shape)
# 测试类BertEncoder
bert_encoder = BertEncoder(config)
bert_encoder_outputs = bert_encoder(bert_embedding_result, attention_mask)
print(f"num of BertEncoder [config.num_hidden_layers]: ", len(bert_encoder_outputs))
print(f"each output shape in BertEncoder [src_len, batch_size, hidden_size]: ", bert_encoder_outputs[0].shape)
# 测试类BertModel
position_ids = torch.arange(src.size()[0]).expand((1, -1)) # [1,src_len]
bert_model = BertModel(config)
bert_model_output = bert_model(input_ids=src,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids)[0]
print(f"BertModel's pooler output shape [batch_size, hidden_size]: ", bert_model_output.shape)
print("\n ======= BertMolde 参数: ========")
for param_tensor in bert_model.state_dict():
print(param_tensor, "\t", bert_model.state_dict()[param_tensor].size())
print(f"\n ======= 测试BertModel载入预训练模型: ========")
model = BertModel.from_pretrained(config, pretrained_model_dir="../bert_base_chinese")
5.3Bert进行文本分类的代码解析
基于 BERT的文本分类(准确的是单文本,也就是输入只包含一个句子)模型就是在原始的 BERT 模型后再加上一个分类层即可。同时,对于分类层的输入(也就是原始 BERT 的输出),默认情况下取 BERT输出结果中[CLS]位置对于的向量即可,当然也可以修改为其它方式,例如所有位置向量的均值等(将配置文件 config.json 中的 pooler_type 字段设置为"all_token_average"即可)。因此,对于基于 BERT 的文本分类模型来说其输入就是 BERT 的输入,输出则是每个类别对应的 logits 值。
- 由于对于文本分类这个场景来说其输入只有一个序列,所以在构建数据集的时候并不需要构造 Segment Embedding 的输入,直接默认使用全为 0 即可
- 同时,对于 Position Embedding 来说在任何场景下都不需要对其指定输入,因为我们在代码实现时已经做了相应默认时的处理
- 因此,对于文本分类这个场景来说,只需要构造原始文本对应的 Token序列,并在首尾分别再加上一个[CLS]符和[SEP]符作为输入即可
5.3.1 数据集预览
数据集是今日头条开放的一个新闻分类数据集[13],一共包含有 382688 条数据,15 个类别。同时掌柜已近将其进行了格式化处理,以 7:2:1 的比例划分成了训练集、验证集和测试集 3 个部分。假如我们现在有两个样本构成了一个 batch,那么其整个数据的处理过程。
- 第 1步需要将原始的数据样本进行分字(tokenize)处理;
- 第2 步再根据 tokenize 后的结果构造一个字典,不过在使用 BERT 预训练时并不需要我们自己来构造这个字典,直接载入谷歌开源的 vocab.txt 文件构造字典即可,因为只有 vocab.txt 中每个字的索引顺序才与开源模型中每个字的Embedding 向量一一对应的。
- 第 3 步则是根据字典将 tokenize 后的文本序列转换为 Token 序列,同时在 Token 序列的首尾分别加上[CLS]和[SEP]符号,并进行 Padding。
- 第4 步则是根据第 3 步处理后的结果生成对应的 Padding Mask 向量。
- 最后,在模型训练时只需要将第 3 步和第 4 步处理后的结果一起喂给模型即可
5.3.2 数据集构建
1)定义tokenize
因为预训练模型打算用huggingface提供的bert,所以直接用对应的BertTokenizer方法
import torch
from torch.utils.data import DataLoader
from tqdm import tqdm
import pandas as pd
import json
import logging
import os
from sklearn.model_selection import train_test_split
import collections
import six
class Vocab:
"""
根据本地的vocab文件,构造一个词表
vocab = Vocab()
print(vocab.itos) # 得到一个列表,返回词表中的每一个词;
print(vocab.itos[2]) # 通过索引返回得到词表中对应的词;
print(vocab.stoi) # 得到一个字典,返回词表中每个词的索引;
print(vocab.stoi['我']) # 通过单词返回得到词表中对应的索引
print(len(vocab)) # 返回词表长度
"""
UNK = '[UNK]'
def __init__(self, vocab_path):
self.stoi = {}
self.itos = []
with open(vocab_path, 'r', encoding='utf-8') as f:
for i, word in enumerate(f):
w = word.strip('\n')
self.stoi[w] = i
self.itos.append(w)
def __getitem__(self, token):
return self.stoi.get(token, self.stoi.get(Vocab.UNK))
def __len__(self):
return len(self.itos)
def build_vocab(vocab_path):
"""
vocab = Vocab()
print(vocab.itos) # 得到一个列表,返回词表中的每一个词;
print(vocab.itos[2]) # 通过索引返回得到词表中对应的词;
print(vocab.stoi) # 得到一个字典,返回词表中每个词的索引;
print(vocab.stoi['我']) # 通过单词返回得到词表中对应的索引
"""
return Vocab(vocab_path)
def pad_sequence(sequences, batch_first=False, max_len=None, padding_value=0):
"""
对一个List中的元素进行padding
Pad a list of variable length Tensors with ``padding_value``
a = torch.ones(25)
b = torch.ones(22)
c = torch.ones(15)
pad_sequence([a, b, c],max_len=None).size()
torch.Size([25, 3])
sequences:
batch_first: 是否把batch_size放到第一个维度
padding_value:
max_len :
当max_len = 50时,表示以某个固定长度对样本进行padding,多余的截掉;
当max_len=None是,表示以当前batch中最长样本的长度对其它进行padding;
Returns:
"""
if max_len is None:
max_len = max([s.size(0) for s in sequences])
out_tensors = []
for tensor in sequences:
if tensor.size(0) < max_len:
tensor = torch.cat([tensor, torch.tensor([padding_value] * (max_len - tensor.size(0)))], dim=0)
else:
tensor = tensor[:max_len]
out_tensors.append(tensor)
out_tensors = torch.stack(out_tensors, dim=1)
if batch_first:
return out_tensors.transpose(0, 1)
return out_tensors
def cache(func):
"""
本修饰器的作用是将SQuAD数据集中data_process()方法处理后的结果进行缓存,下次使用时可直接载入!
:param func:
:return:
"""
def wrapper(*args, **kwargs):
filepath = kwargs['filepath']
postfix = kwargs['postfix']
data_path = filepath.split('.')[0] + '_' + postfix + '.pt'
if not os.path.exists(data_path):
logging.info(f"缓存文件 {data_path} 不存在,重新处理并缓存!")
data = func(*args, **kwargs)
with open(data_path, 'wb') as f:
torch.save(data, f)
else:
logging.info(f"缓存文件 {data_path} 存在,直接载入缓存文件!")
with open(data_path, 'rb') as f:
data = torch.load(f)
return data
return wrapper
class LoadSingleSentenceClassificationDataset:
def __init__(self,
vocab_path='./vocab.txt', #
tokenizer=None,
batch_size=32,
max_sen_len=None,
split_sep='\n',
max_position_embeddings=512,
pad_index=0,
is_sample_shuffle=True
):
"""
:param vocab_path: 本地词表vocab.txt的路径
:param tokenizer:
:param batch_size:
:param max_sen_len: 在对每个batch进行处理时的配置;
当max_sen_len = None时,即以每个batch中最长样本长度为标准,对其它进行padding
当max_sen_len = 'same'时,以整个数据集中最长样本为标准,对其它进行padding
当max_sen_len = 50, 表示以某个固定长度符样本进行padding,多余的截掉;
:param split_sep: 文本和标签之前的分隔符,默认为'\t'
:param max_position_embeddings: 指定最大样本长度,超过这个长度的部分将本截取掉
:param is_sample_shuffle: 是否打乱训练集样本(只针对训练集)
在后续构造DataLoader时,验证集和测试集均指定为了固定顺序(即不进行打乱),修改程序时请勿进行打乱
因为当shuffle为True时,每次通过for循环遍历data_iter时样本的顺序都不一样,这会导致在模型预测时
返回的标签顺序与原始的顺序不一样,不方便处理。
"""
self.tokenizer = tokenizer
self.vocab = build_vocab(vocab_path)
self.PAD_IDX = pad_index
self.SEP_IDX = self.vocab['[SEP]']
self.CLS_IDX = self.vocab['[CLS]']
# self.UNK_IDX = '[UNK]'
self.batch_size = batch_size
self.split_sep = split_sep
self.max_position_embeddings = max_position_embeddings
if isinstance(max_sen_len, int) and max_sen_len > max_position_embeddings:
max_sen_len = max_position_embeddings
self.max_sen_len = max_sen_len
self.is_sample_shuffle = is_sample_shuffle
@cache
def data_process(self, filepath, postfix='cache'):
"""
将每一句话中的每一个词根据字典转换成索引的形式,同时返回所有样本中最长样本的长度
:param filepath: 数据集路径
:return:
"""
raw_iter = open(filepath, encoding="utf8").readlines()
data = []
max_len = 0
for raw in tqdm(raw_iter, ncols=80):
line = raw.rstrip("\n").split(self.split_sep)
s, l = line[0], line[1]
tmp = [self.CLS_IDX] + [self.vocab[token] for token in self.tokenizer(s)]
if len(tmp) > self.max_position_embeddings - 1:
tmp = tmp[:self.max_position_embeddings - 1] # BERT预训练模型只取前512个字符
tmp += [self.SEP_IDX]
tensor_ = torch.tensor(tmp, dtype=torch.long)
l = torch.tensor(int(l), dtype=torch.long)
max_len = max(max_len, tensor_.size(0))
data.append((tensor_, l))
return data, max_len
def load_train_val_test_data(self, train_file_path=None,
val_file_path=None,
test_file_path=None,
only_test=False):
postfix = str(self.max_sen_len)
test_data, _ = self.data_process(filepath=test_file_path, postfix=postfix)
test_iter = DataLoader(test_data, batch_size=self.batch_size,
shuffle=False, collate_fn=self.generate_batch)
if only_test:
return test_iter
train_data, max_sen_len = self.data_process(filepath=train_file_path,
postfix=postfix) # 得到处理好的所有样本
if self.max_sen_len == 'same':
self.max_sen_len = max_sen_len
val_data, _ = self.data_process(filepath=val_file_path,
postfix=postfix)
train_iter = DataLoader(train_data, batch_size=self.batch_size, # 构造DataLoader
shuffle=self.is_sample_shuffle, collate_fn=self.generate_batch)
val_iter = DataLoader(val_data, batch_size=self.batch_size,
shuffle=False, collate_fn=self.generate_batch)
return train_iter, test_iter, val_iter
def generate_batch(self, data_batch):
batch_sentence, batch_label = [], []
for (sen, label) in data_batch: # 开始对一个batch中的每一个样本进行处理。
batch_sentence.append(sen)
batch_label.append(label)
batch_sentence = pad_sequence(batch_sentence, # [batch_size,max_len]
padding_value=self.PAD_IDX,
batch_first=False,
max_len=self.max_sen_len)
batch_label = torch.tensor(batch_label, dtype=torch.long)
return batch_sentence, batch_label
测试上面的代码
from Tasks.TaskForSingleSentenceClassification import ModelConfig
from utils.data_helpers import LoadSingleSentenceClassificationDataset
from transformers import BertTokenizer
if __name__ == '__main__':
model_config = ModelConfig()
load_dataset = LoadSingleSentenceClassificationDataset(
vocab_path=model_config.vocab_path,
tokenizer=BertTokenizer.from_pretrained(model_config.pretrained_model_dir).tokenize,
batch_size=model_config.batch_size,
max_sen_len=model_config.max_sen_len,
split_sep=model_config.split_sep,
max_position_embeddings=model_config.max_position_embeddings,
pad_index=model_config.pad_token_id,
is_sample_shuffle=model_config.is_sample_shuffle)
train_iter, test_iter, val_iter = \
load_dataset.load_train_val_test_data(model_config.train_file_path,
model_config.val_file_path,
model_config.test_file_path)
for sample, label in train_iter:
print(sample.shape) # [seq_len,batch_size]
print(sample.transpose(0, 1))
padding_mask = (sample == load_dataset.PAD_IDX).transpose(0, 1)
print(padding_mask)
# print(label)
break
5.3.3下游任务
创建一个文件名称为BertForSentenceClassification.py
在bert输出的基础上增加dropout层和一层dnn层用于分类
from ..BasicBert.Bert import BertModel
import torch.nn as nn
class BertForSentenceClassification(nn.Module):
def __init__(self, config, bert_pretrained_model_dir=None):
super(BertForSentenceClassification, self).__init__()
self.num_labels = config.num_labels
if bert_pretrained_model_dir is not None:
self.bert = BertModel.from_pretrained(config, bert_pretrained_model_dir)
else:
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, self.num_labels)
def forward(self, input_ids,
attention_mask=None,
token_type_ids=None,
position_ids=None,
labels=None):
"""
:param input_ids: [src_len, batch_size]
:param attention_mask: [batch_size, src_len]
:param token_type_ids: 句子分类时为None
:param position_ids: [1,src_len]
:param labels: [batch_size,]
:return:
"""
pooled_output, _ = self.bert(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids) # [batch_size,hidden_size]
'''将模型输出输入到dropout层和dnn层,label有则输出loss,无则直接输出logit '''
pooled_output = self.dropout(pooled_output)
logits = self.classifier(pooled_output) # [batch_size, num_label]
if labels is not None:
loss_fct = nn.CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
return loss, logits
else:
return logits
5.3.4train和inference
创建一个文件名称为TaskForSingleSentenceClassification.py
import sys
sys.path.append('../')
from model import BertForSentenceClassification
from model import BertConfig
from utils import LoadSingleSentenceClassificationDataset
from utils import logger_init
from transformers import BertTokenizer
import logging
import torch
import os
import time
class ModelConfig:
def __init__(self):
self.project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.dataset_dir = os.path.join(self.project_dir, 'data', 'SingleSentenceClassification')
self.pretrained_model_dir = os.path.join(self.project_dir, "bert_base_chinese")
self.vocab_path = os.path.join(self.pretrained_model_dir, 'vocab.txt')
self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
self.train_file_path = os.path.join(self.dataset_dir, 'toutiao_train.txt')
self.val_file_path = os.path.join(self.dataset_dir, 'toutiao_val.txt')
self.test_file_path = os.path.join(self.dataset_dir, 'toutiao_test.txt')
self.model_save_dir = os.path.join(self.project_dir, 'cache')
self.logs_save_dir = os.path.join(self.project_dir, 'logs')
self.split_sep = '_!_'
self.is_sample_shuffle = True
self.batch_size = 64
self.max_sen_len = None
self.num_labels = 15
self.epochs = 10
self.model_val_per_epoch = 2
logger_init(log_file_name='single', log_level=logging.INFO,
log_dir=self.logs_save_dir)
if not os.path.exists(self.model_save_dir):
os.makedirs(self.model_save_dir)
# 把原始bert中的配置参数也导入进来
bert_config_path = os.path.join(self.pretrained_model_dir, "config.json")
bert_config = BertConfig.from_json_file(bert_config_path)
for key, value in bert_config.__dict__.items():
self.__dict__[key] = value
# 将当前配置打印到日志文件中
logging.info(" ### 将当前配置打印到日志文件中 ")
for key, value in self.__dict__.items():
logging.info(f"### {key} = {value}")
def train(config):
'''在bert基础上增加dnn层,然后整个网络进行微调 '''
model = BertForSentenceClassification(config,
config.pretrained_model_dir)
model_save_path = os.path.join(config.model_save_dir, 'model.pt')
if os.path.exists(model_save_path):
loaded_paras = torch.load(model_save_path)
model.load_state_dict(loaded_paras)
logging.info("## 成功载入已有模型,进行追加训练......")
model = model.to(config.device)
optimizer = torch.optim.Adam(model.parameters(), lr=5e-5)
model.train()
bert_tokenize = BertTokenizer.from_pretrained(config.pretrained_model_dir).tokenize
data_loader = LoadSingleSentenceClassificationDataset(vocab_path=config.vocab_path,
tokenizer=bert_tokenize,
batch_size=config.batch_size,
max_sen_len=config.max_sen_len,
split_sep=config.split_sep,
max_position_embeddings=config.max_position_embeddings,
pad_index=config.pad_token_id,
is_sample_shuffle=config.is_sample_shuffle)
train_iter, test_iter, val_iter = data_loader.load_train_val_test_data(config.train_file_path,
config.val_file_path,
config.test_file_path)
max_acc = 0
for epoch in range(config.epochs):
losses = 0
start_time = time.time()
for idx, (sample, label) in enumerate(train_iter):
sample = sample.to(config.device) # [src_len, batch_size]
label = label.to(config.device)
padding_mask = (sample == data_loader.PAD_IDX).transpose(0, 1)
loss, logits = model(
input_ids=sample,
attention_mask=padding_mask,
token_type_ids=None,
position_ids=None,
labels=label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
losses += loss.item()
acc = (logits.argmax(1) == label).float().mean()
if idx % 10 == 0:
logging.info(f"Epoch: {epoch}, Batch[{idx}/{len(train_iter)}], "
f"Train loss :{loss.item():.3f}, Train acc: {acc:.3f}")
end_time = time.time()
train_loss = losses / len(train_iter)
logging.info(f"Epoch: {epoch}, Train loss: {train_loss:.3f}, Epoch time = {(end_time - start_time):.3f}s")
if (epoch + 1) % config.model_val_per_epoch == 0:
acc = evaluate(val_iter, model, config.device, data_loader.PAD_IDX)
logging.info(f"Accuracy on val {acc:.3f}")
if acc > max_acc:
max_acc = acc
torch.save(model.state_dict(), model_save_path)
def inference(config):
model = BertForSentenceClassification(config,
config.pretrained_model_dir)
model_save_path = os.path.join(config.model_save_dir, 'model.pt')
if os.path.exists(model_save_path):
loaded_paras = torch.load(model_save_path)
model.load_state_dict(loaded_paras)
logging.info("## 成功载入已有模型,进行预测......")
model = model.to(config.device)
data_loader = LoadSingleSentenceClassificationDataset(vocab_path=config.vocab_path,
tokenizer=BertTokenizer.from_pretrained(
config.pretrained_model_dir).tokenize,
batch_size=config.batch_size,
max_sen_len=config.max_sen_len,
split_sep=config.split_sep,
max_position_embeddings=config.max_position_embeddings,
pad_index=config.pad_token_id,
is_sample_shuffle=config.is_sample_shuffle)
train_iter, test_iter, val_iter = data_loader.load_train_val_test_data(config.train_file_path,
config.val_file_path,
config.test_file_path)
acc = evaluate(test_iter, model, device=config.device, PAD_IDX=data_loader.PAD_IDX)
logging.info(f"Acc on test:{acc:.3f}")
def evaluate(data_iter, model, device, PAD_IDX):
model.eval()
with torch.no_grad():
acc_sum, n = 0.0, 0
for x, y in data_iter:
x, y = x.to(device), y.to(device)
padding_mask = (x == PAD_IDX).transpose(0, 1)
'''输出模型预测的概率值 '''
logits = model(x, attention_mask=padding_mask)
acc_sum += (logits.argmax(1) == y).float().sum().item()
n += len(y)
model.train()
return acc_sum / n
if __name__ == '__main__':
model_config = ModelConfig()
train(model_config)
inference(model_config)
5.4文本蕴含任务
所谓文本对分类指的就是同时给模型输入两句话,然后让模型来判断两句话之间的关系,所以本质上也就变成了一个文本分类任务。总的来说,基于 BERT 的文本蕴含任务同第 4 节中介绍的单文本分类任务本质上没有任何不同,最终都是对一个文本序列进行分类。只是按照 BERT 模型的思想,文本对分类任务在数据集的构建过程中需要通过 Segment Embedding来区分前后两个不同的序列,并且两个句子之间需要通过一个[SEP]符号来进行分割,因此本节内容的核心就在于如何构建数据集。文本对的分类任务除了在模型输入上发生了变换,其它地方均
与单文本分类任务一样,同样也是取最后一层的[CLS]向量进行分类。
5.5Swag选择任务
通常来说,在 NLP 领域的很多场景中模型最后所做的基本上都是一个分类任务,虽然表面上看起来不是。例如:文本蕴含任务其实就是将两个序列拼接在一起,然后预测其所属的类别;基于神经网络的序列生成模型(翻译、文本生成等)本质就是预测词表中下一个最有可能出现的词,此时的分类类别就是词表的大小。因此,从本质上来说本文介绍的问答选择任务以及在后面将要介绍的问题回答任务其实都是一个分类任务,而关键的地方就在于如何构建模型的输入和输出。
从图中可以看出,原始数据的形式是一个问题和四个选项,模型需要做的就是从四个选项中给出最合理的一个,于是也就变成了一个四分类任务。同时,构建模型输入的方式就是将原始问题和每一个答案都拼接起来构成一个序列中间用[SEP]符号隔开,然后再分别输入到 BERT 模型中进行特征提取得到四个特征向量形状为[4,hidden_size],最后再经过一个分类层进行分类处理得到预测选项。值得一提的是,通常情况下这里的四个特征都是直接取每个序列经 BERT编码后的[CLS]向量。
5.6SQuAD问答任务
所谓问题回答指的就是同时给模型输入一个问题和一段描述,最后需要模型从给定的描述中预测出问题答案所在的位置(text span)。
在做这个任务之前首先需要明白的就是:
- ①最终问题的答案一定是在给定的描述中;
- ②问题再描述中的答案一定是一段连续的字符,不能有间隔。例如对于上面的描述内容来说,如果给出的问题是“苏轼生活在什么年代以及他是哪里人?”,那么模型最终并不会给出类似“北宋”和“眉州眉山人”这两个分离的答案,最好的情况下便是给出“北宋著名的文学家与政治家,眉州眉山人”这一个连续的答案。
在有了这两个限制条件后,对于这类问答任务的本质也就变成了需要让模型预测得到答案在描述中的起始位置(start position)以及它的结束位置(end position)。所以,问题最终又变成了如何在 BERT 模型的基础上再构建一个分类器来对 BERT 最后一层输出的每个 Token 进行分类,判断它们是否属于 start position 或者是 end position。
构建模型输入的方式就是将原始问题和上下文描述拼接成一个序列中间用[SEP]符号隔开,然后再分别输入到 BERT 模型中进行特征提取。在BERT 编码完成后,再取最后一层的输出对每个 Token 进行分类即可得到 start position 和 end position 的预测输出。
- 值得注意的是在问答场景中是将问题放在上下文描述前面的,即Sentence A 为问题,Sentence B 为描述。而在上一个问题选择任务场景(Swag选择)中是将答案放在描述之后。 猜测这是因为在 SWAG 这一推理数据集中,每个选项其实都可以看作是问题(描述)的下半句,两者具有强烈的先后顺序算是一种逻辑推理,因此将选项放在了描述了后面。
- 在问题回答这一场景中,论文中将问题放在描述前面猜测是因为:①两者并没有强烈的先后顺序;②问题相对较短放到前面可能好处理一点。所以基于这样的考虑,在问答任务中将问题放在了描述前面。不过后续大家依旧可以尝试交换一下顺序看看效果。
5.6.1数据输入介绍
如果在建模时碰到上下文过长时的情况该怎么办?是直接采取截断处理吗?如果问题答案恰巧是在被截断的那部分里面呢,还能直接截断吗?显然,认真想一想截断这种做法在这里肯定是不行的,因此在论文中作者也采用了另外一种方法来解决这一问题,那就是滑动窗口
第①步需要做的是根据指定最大长度和滑动窗口大小将原始样本进行滑动窗口处理并得到多个子样本。不过这里需要注意的是,sentence A,也就是问题部分不参与滑动处理。同时,图中样本右边的3 列数字分别表示每个子样本的起始、结束索引和原始样本对应的数据集中的ID(如其中的9,13是“[CLS]苏轼哪里人?[SEP],眉州眉山人”这句话从0开始算,即“眉州眉山人”)。紧接着第②步便是将所有原始样本滑动处理后的结果作为训练集来训练模型。
总的来说,在这一场景中训练程并没有太大的问题,因为每个子样本也都有其对应的标签值,因此和普通的训练过程并没有什么本质上的差异。因此,最关键的地方在于如何在推理过程中也使用滑动窗口。
5.6.2 结果筛选
一种最直观的做法就是直接取起始位置预测概率值加结束位置预测概率值最大的子样本对应的结果,作为整个原始样本对应的预测结果。不过下面将来介绍另外一种效果更好的处理方式(这也是论文中所采取的方式),其整个处理流程如图所示。
如图 所示,在推理过程中第①步要做的仍旧是需要根据指定最大长度和滑动窗口大小将原始样本进行滑动窗口处理。接着第②步便是根据 BERT 分类的输出取前 K 个概率值最大的结果。在图中这里的 K 值为 4,因此对于每个子样本来说其 start position 和 end position 分别都有 4 个候选结果。例如,
第②步中第 1行的 7:0.41,10:02,9:0.12,2:01表示函数就是对于第 1个子样本来说,start position 为索引 7 的概率值为 0.41,其它同理。这样对于每一个子样本来说,在分别得到 start position 和 end position的 K 个候选值后便可以通过组合来得到更多的候选预测结果,然后再根据一些规则来选择最终原始样本对应的预测输出。
根据图中样本重构后的结果可以看出:
- (1)最终的索引预测结果肯定是大于 8 的,因为答案只可能在上下文中出现;
- (2)在进行结果组合的过程中,起始索引肯定是小于等于结束索引的。
因此,根据这两个条件在经过步骤③的处理后,便可以得到进一步的筛选结果。例如,对于第 1 个子样本来说,start position 中 7 和 2 是不满足条件(1)的,所以可以直接去掉;同时,为了满足第(2)个条件所以在 end position 中 8,6,7 均需要去掉。
进一步,将第③步处理后的结果在每个子样本内部进行组合,并按照 start position 加 end position 值的大小进行排序,便可以得到如下图所示的结果
如图所示表示根据概率和排序后的结果。例如第 1 列 9,13,0.65 的含义便是最终原始样本预测结果为 9,13 的概率值为 0.65。因此,最终该原始样本对应的预测值便可以取 9 和 13。
5.7命名实体识别任务
所谓命名体指的是给模型输入一句文本,最后需要模型将其中的实体(例如人名、地名、组织等等)标记出来
对于任意一个 NLP 任务来说最后所要完成的基本上都是一个分类任务,尽管表面上看起来可能不太像。根据给出的标签来看,对于原始句子中的每个字符来说其都有一个对应的类别标签,因此对于 NER 任务来说只需对原始句子里每个字符进行分类即可,然后再将预测后的结果进行后处理便能够得到句子中存在的相应实体
5.7.1任务构造原理
从图中可以看出原始数据输入为一个句子,我们只需要在句子的首尾分别加上[CLS]和[SEP],然后输入到模型当中进行特征提取并最终通过一个分类层对输出的每个Token进行分类即可,最后只需要对各个 Token的预测结果进行后处理便能够实现整个 NER 任务。
5.8从零实现NSP和MlM预训练任务
6huggingface例子
参照Huggingface简介及BERT代码浅析中的例子
import torch
from transformers import BertModel, BertTokenizer
# 这里我们调用bert-base模型,同时模型的词典经过小写处理
model_name = 'bert-base-uncased'
# 读取模型对应的tokenizer
tokenizer = BertTokenizer.from_pretrained(model_name)
# 载入模型
model = BertModel.from_pretrained(model_name)
# 输入文本
input_text = "Here is some text to encode"
# 通过tokenizer把文本变成 token_id
input_ids = tokenizer.encode(input_text, add_special_tokens=True)
# input_ids: [101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102]
input_ids = torch.tensor([input_ids])
# 获得BERT模型最后一个隐层结果
with torch.no_grad():
last_hidden_states = model(input_ids)[0] # Models outputs are now tuples
""" tensor([[[-0.0549, 0.1053, -0.1065, ..., -0.3550, 0.0686, 0.6506],
[-0.5759, -0.3650, -0.1383, ..., -0.6782, 0.2092, -0.1639],
[-0.1641, -0.5597, 0.0150, ..., -0.1603, -0.1346, 0.6216],
...,
[ 0.2448, 0.1254, 0.1587, ..., -0.2749, -0.1163, 0.8809],
[ 0.0481, 0.4950, -0.2827, ..., -0.6097, -0.1212, 0.2527],
[ 0.9046, 0.2137, -0.5897, ..., 0.3040, -0.6172, -0.1950]]])
shape: (1, 9, 768)
"""
以tokenization开头的都是跟vocab有关的代码,比如在 tokenization_bert.py 中有函数如whitespace_tokenize,还有不同的tokenizer的类。同时也有各个模型对应的vocab.txt。从第一个链接进去就是bert-base-uncased的词典,这里面有30522个词,对应着config里面的vocab_size。
https://github.com/huggingface/transformers/blob/main/src/transformers/models/bert/tokenization_bert.py
其中,第0个token是[pad],第101个token是[CLS],第102个token是[SEP],所以之前我们encode得到的 [101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102] ,其实tokenize后convert前的token就是 ['[CLS]', 'here', 'is', 'some', 'text', 'to', 'en', '##code', '[SEP]'],这是通过子词化得到的,及encode切分成了更小的子词,中文就是切词后的结果
读取一个预训练过的BERT模型,来encode我们指定的一个文本,对文本的每一个token生成768维的向量。如果是二分类任务,我们接下来就可以把第一个token也就是[CLS]的768维向量,接一个linear层,预测出分类的logits,或者根据标签进行训练。
参考文献:
论文解读:BERT模型及fine-tuning
最强预训练模型BERT的Pytorch实现(非官方)
Huggingface简介及BERT代码浅析
bert可以做哪些nlp任务
月来客栈
codertimo/BERT-pytorch
莫凡-BERT 双向语言模型