引言
自然语言处理中的词嵌入模型,在传统的Embedding方法中,我们可以将模型得到的输出,即词嵌入之后的输出理解为是一哥矩阵,每一行代表一个字或者一个词的的词向量表示,而这些表示放在现在来说其实并不能包含较多句子序列之间的相互关系和较多的语义信息,因此词嵌入模型经历了一系列 的变化和更新
word2vec&GloVe
根据目前的知识我们都知道word2vec以及GloVe都是不考虑词的上下文的语义信息和相互关系,相同的词会映射成相同的词向量,因此这种语言模型又被叫做上下文无关模型。然而自然语言中有丰富且复杂的语义信息,因此这种语言模型就存在着较为严重的局限性,我们就必须通过句子的上下文来表示词。
上下文敏感
上下文敏感顾名思义,词的表示由上下文决定,包含哪几种常见的语言模型:
- TagLM(language-model-augmented sequence tagger,语言模型增强的序列标记器)
- CoVe(Context Vectors,上下文向量)
- ELMo(Embeddings from Language Models,来自语言模型的嵌入)
重点提一下ELMo吧,他是将输入序列的每个单词分配一个表示函数,即将来自预训练的双向长短期记忆网络的中间层表示组合为输出表示,然后再将表示作为附加特征添加到下游任务的有监督模型中,经常用于情感分析、自然语言推断、语义角色标注、共指消解、命名实体识别、问答。
任务泛化
ELMo其实也存在一定的弊端,那就是不同的任务依赖属于特定任务的框架,GPT模型就是解决这个问题的一种通用的任务无关模型。 GPT是在Transformer解码器的基础上预训练一个用于文本序列的模型。Finetune阶段就是直接加一个线性层来预测标签,在下游任务中将参数微调,GPT的缺点也很明显,他是从左到右的单向语言模型,因此对于i went to the bank to sit down和i went to the bank to deposit cash两句话中的bank表示相同,尽管他们的实际意思不同。
取长补短
我们发现ELMo能够对上下文进行双向编码,但需要应用于特定任务,GPT任务无关但是单向编码,我们考虑的Bert可以双向编码,通过预训练Transformer编码器来双向表示上下文,下游任务只需要加一个全连接层或其他网络进行预测。如下图:
重头戏
BERT实战操作
- 导包
import torch
from torch import nn
from d2l import torch as d2l
- 输入表示
def get_tokens_and_segments(tokens_a, tokens_b=None):
"""获取输入序列的词元及其片段索引"""
tokens = ['<cls>'] + tokens_a + ['<sep>']
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
3.BertEncoder
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))
self.pos_embedding = nn.Parameter(torch.randn(1, max_len,
num_hiddens))
def forward(self, tokens, segments, valid_lens):
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
4.初始化参数进行实例化
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)
5.举个例子
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
6.预训练任务 1)MaskLM
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):
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_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
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
2)NextSentencePred
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):
return self.output(X)
例:
encoded_X = torch.flatten(encoded_X, start_dim=1)
nsp = NextSentencePred(encoded_X.shape[-1])
nsp_Y_hat = nsp(encoded_X)
nsp_Y_hat.shape
nsp_y = torch.tensor([0, 1])
nsp_l = loss(nsp_Y_hat, nsp_y)
nsp_l.shape
7.代码整合
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
nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
return encoded_X, mlm_Y_hat, nsp_Y_hat
小结
word2vec和GloVe等词嵌入模型与上下文无关。它们将相同的预训练向量赋给同一个词,而不考虑词的上下文(如果有的话)。它们很难处理好自然语言中的一词多义或复杂语义。
对于上下文敏感的词表示,如ELMo和GPT,词的表示依赖于它们的上下文。
ELMo对上下文进行双向编码,但使用特定于任务的架构(然而,为每个自然语言处理任务设计一个特定的体系架构实际上并不容易);而GPT是任务无关的,但是从左到右编码上下文。
BERT结合了这两个方面的优点:它对上下文进行双向编码,并且需要对大量自然语言处理任务进行最小的架构更改。
BERT输入序列的嵌入是词元嵌入、片段嵌入和位置嵌入的和。
预训练包括两个任务:掩蔽语言模型和下一句预测。前者能够编码双向上下文来表示单词,而后者则显式地建模文本对之间的逻辑关系。
|