Embedding
ont-hot和word embedding的联系与区别
- One-Hot Encoding(One-Hot 编码)和 Word Embedding (词嵌入)和就是把单词变成向量的两类方法
-
深度学习应用在自然语言处理当中的时候,基本都会通过词向量的方式将one-hot编码的向量,转换为词向量。至于为什么这么做,
一个原因是因为深度学习对稀疏输入效果不好
第二个最主要原因是,那种one-hot方式的编码,对于每一个不同的单词或者中文的词语,之间关系没有办法表达出来,也即,对于不同的单词,两个单词的one-hot编码的向量的相似度永远为0, 也即cos(Vi, Vj) = 0。那么问题来了,怎样表示出单词之间的内在联系呢?embedding来了 -
embeding的出现时因为ont-hot有两个明显的缺点:
-
对于具有非常多类型的类别变量,变换后的向量维数过于巨大,且过于稀疏。
-
映射之间完全独立,并不能表示出不同类别之间的关系。
我们是否可以通过较少的维度表示出每个类别,并且还可以一定的表现出不同类别变量之间的关系,这也就是 embedding 出现的目的
总结:
nn.Embedding 与 nn.Linear 的区别
nn.Embedding
实际上就是输入为one-hot向量,且不带bias的nn.Linear
- 回顾最简单的多层感知机,其中的
nn.Linear.weight
会随着反向传播自动更新。当我们把nn.Embedding
视为一个特殊的nn.Linear
后,其更新机制就不难理解了,无非就是按照梯度进行更新罢了。
BERT的动机
- 基于微调的NLP
- 预训练的模型抽取了足够多的信息
- 新的任务只需要增加一个输出层
BERT原理
相对于Transformer,在输入和loss上有创新
我们输入一个句子,Transformer的编码器会输出句子中每个单词的编码表示
由于Transformer编码器天然就是双向的,因为它的输入是完整的句子,也就是说指定某个单词,BERT已经读入了它两个方向上的所有单词。
Transformer的编码器通过多头注意力机制理解每个单词的上下文,然后输出每个单词的嵌入向量。最后一个 transformer 块的输出,表示 这个词源 token 的 BERT 的表示。在后面再添加额外的输出层,来得到想要的结果。
假设编码器层大小为768,那么单词的向量表示大小也就是768。
BERT使用的激活函数叫作GELU
在基于深度神经网络的NLP方法中,文本中的字/词通常都用一维向量来表示(一般称之为“词向量”);在此基础上,神经网络会将文本中各个字或词的一维词向量作为输入,经过一系列复杂的转换后,输出一个一维词向量作为文本的语义表示。特别地,我们通常希望语义相近的字/词在特征向量空间上的距离也比较接近,如此一来,由字/词向量转换而来的文本向量也能够包含更为准确的语义信息。因此,BERT模型的主要输入是文本中各个字/词的原始词向量,该向量既可以随机初始化,也可以利用Word2Vector等算法进行预训练以作为初始值;输出是文本中各个字/词融合了全文语义信息后的向量表示,
预训练BERT模型
再看如何进行预训练之前,先看一下如何表示输入数据
输入数据表示
- 标记嵌入(Token embedding)
- 片段嵌入(Segment embedding)
- 位置嵌入(Position embedding)
标记嵌入
标记嵌入指在第一个句子前面加入[CLS]
标记,在每个句子末尾加[SEP]
标记。
例如下面的例子
Sentence A: Paris is a beautiful city.
Sentence B: I love Paris.
加入标记后:
tokens = [ [CLS], Paris, is, a, beautiful, city, [SEP], I, love, Paris, [SEP]]
这些所有的标记使用一个叫作标记嵌入的嵌入层转换这些标记为嵌入向量
片段嵌入
片段嵌入层只返回两种嵌入,Ea或Eb
如下图所示:
位置嵌入
位置嵌入层是为了得到单词的位置信息,与transformer不同在于,bert的位置嵌入的权重是可学习的
最终表示
如下图所示,首先我们将给定的输入序列分词为标记列表,然后喂给标记嵌入层,片段嵌入层和位置嵌入层,得到对应的嵌入表示。然后,累加所有的嵌入表示作为BERT的输入表示。
WordPiece分词器
BERT的词表有30K个标记,如果某个单词属于这30K个标记中一个,那我们将该单词视为一个标记;否则,我们拆分单词为子词,然后检查子词是否属于这30K个标记之一。
例如,单词pretraining
不在BERT的词表中。一次你,我们将它拆分为子词pre
,##train
和##ing
。前面的#
表示这个单词为一个子词,并且它前面有其他单词。现在我们检查子词##train
和##ing
是否出现在词表中。因为它们正好在词表中,所以我们不需要继续拆分。
BERT预训练两个任务
1.MLM
Masked Language Model(MLM,遮盖语言模型):这种任务的目的是预测句子中部分单词的原始形式。在训练过程中,BERT模型会随机选择一些单词并用“【MASK】”标记替换它们。模型的任务是预测被替换的单词的原始形式。这种方法可以使模型在理解句子语义的同时学习到词语之间的关系。
80%的时间是采用[mask]
tokens = [ [CLS], Paris, is, a beautiful, [MASK], [SEP], I, love, Paris, [SEP] ]
10%的时间是随机取一个词来代替mask的词
tokens = [ [CLS], Paris, is, a beautiful, love, [SEP], I, love, Paris, [SEP] ]
10%的时间保持不变,
tokens = [ [CLS], Paris, is, a beautiful, city, [SEP], I, love, Paris, [SEP] ]
原因:随机词是为了防止Transformer很可能会记住这个[MASK]就是"city"
以这种方式屏蔽标记会在预训练和微调之间产生差异。即,我们训练BERT通过预测[MASK]标记。训练完之后,我们可以为下游任务微调预训练的BERT模型,比如情感分析任务。但在微调期间,我们的输入不会有任何的[MASK]标记。因此,它会导致 BERT 的预训练方式与微调方式不匹配。
在分词和屏蔽之后,我们分别将这些输入标记喂给标记嵌入、片段嵌入和位置嵌入层,然后得到输入嵌入。然后,我们将输入嵌入喂给BERT。如图(使用BERT-base)
为了预测屏蔽的标记,我们将BERT返回的屏蔽的单词表示R [MASK] ,喂给一个带有softmax激活函数的前馈神经网络。然后该网络输出词表中每个单词属于该屏蔽的单词的概率
不过,在初始的迭代中,我们的模型不会输出正确的概率,因为前馈网络和BERT编码器层的参数还没有被优化。然而,通过一系列的迭代之后,我们更新了前馈网络和BERT编码器层的参数,然后学到了优化的参数。
2.NSP
Next Sentence Prediction(NSP,下一句预测):这种任务的目的是预测一个句子是否是另一个句子的下一句。在训练过程中,BERT模型会从两个句子中选择一个随机的句子对,并根据是否是下一句来训练模型。这种方法可以使模型更好地理解上下文之间的关系。
NSP是二分类任务,NSP任务中,我们模型的目标是预测句子对属于isNext
还是notNext
例如,选定句子She cooked pasta It was delicious
tokens = [[CLS], She, cooked, pasta, [SEP], It, was, delicious, [SEP]]
输入如图:
为了进行分类,我们简单地将[CLS]
标记的嵌入表示喂给一个带有softmax函数的全连接网络,该网络会返回我们输入的句子对属于isNext
和notNext
的概率。
因为[CLS]
标记保存了所有标记的聚合表示。也就得到了整个输入的信息。所以我们可以直接拿该标记对应的嵌入表示来进行预测。如下图所示:
通过运行NSP任务,我们的模型可以理解两个句子之间的关系,这会有利于很多下游任务,像问答和文本生成
MLM与NSP同时进行训练,这样用next_sentence来辅助模型对噪声/非噪声的辨识,用MLM来完成语义的大部分的学习。
BERT适用场景
第一,如果NLP任务偏向在语言本身中就包含答案,而不特别依赖文本外的其它特征,往往应用Bert能够极大提升应用效果。典型的任务比如QA和阅读理解,正确答案更偏向对语言的理解程度,理解能力越强,解决得越好,不太依赖语言之外的一些判断因素,所以效果提升就特别明显。反过来说,对于某些任务,除了文本类特征外,其它特征也很关键,比如搜索的用户行为/链接分析/内容质量等也非常重要,所以Bert的优势可能就不太容易发挥出来。再比如,推荐系统也是类似的道理,Bert可能只能对于文本内容编码有帮助,其它的用户行为类特征,不太容易融入Bert中。
第二,Bert特别适合解决句子或者段落的匹配类任务。就是说,Bert特别适合用来解决判断句子关系类问题,这是相对单文本分类任务和序列标注等其它典型NLP任务来说的,很多实验结果表明了这一点。而其中的原因,我觉得很可能主要有两个,一个原因是:很可能是因为Bert在预训练阶段增加了Next Sentence Prediction任务,所以能够在预训练阶段学会一些句间关系的知识,而如果下游任务正好涉及到句间关系判断,就特别吻合Bert本身的长处,于是效果就特别明显。第二个可能的原因是:因为Self Attention机制自带句子A中单词和句子B中任意单词的Attention效果,而这种细粒度的匹配对于句子匹配类的任务尤其重要,所以Transformer的本质特性也决定了它特别适合解决这类任务。
从上面这个Bert的擅长处理句间关系类任务的特性,我们可以继续推理出以下观点:
既然预训练阶段增加了Next Sentence Prediction任务,就能对下游类似性质任务有较好促进作用,那么是否可以继续在预训练阶段加入其它的新的辅助任务?而这个辅助任务如果具备一定通用性,可能会对一类的下游任务效果有直接促进作用。这也是一个很有意思的探索方向,当然,这种方向因为要动Bert的第一个预训练阶段,所以属于NLP届土豪们的工作范畴,穷人们还是散退、旁观、鼓掌、叫好为妙。
第三,Bert的适用场景,与NLP任务对深层语义特征的需求程度有关。感觉越是需要深层语义特征的任务,越适合利用Bert来解决;而对有些NLP任务来说,浅层的特征即可解决问题,典型的浅层特征性任务比如分词,POS词性标注,NER,文本分类等任务,这种类型的任务,只需要较短的上下文,以及浅层的非语义的特征,貌似就可以较好地解决问题,所以Bert能够发挥作用的余地就不太大,有点杀鸡用牛刀,有力使不出来的感觉。
这很可能是因为Transformer层深比较深,所以可以逐层捕获不同层级不同深度的特征。于是,对于需要语义特征的问题和任务,Bert这种深度捕获各种特征的能力越容易发挥出来,而浅层的任务,比如分词/文本分类这种任务,也许传统方法就能解决得比较好,因为任务特性决定了,要解决好它,不太需要深层特征。
第四,Bert比较适合解决输入长度不太长的NLP任务,而输入比较长的任务,典型的比如文档级别的任务,Bert解决起来可能就不太好。主要原因在于:Transformer的self attention机制因为要对任意两个单词做attention计算,所以时间复杂度是n平方,n是输入的长度。如果输入长度比较长,Transformer的训练和推理速度掉得比较厉害,于是,这点约束了Bert的输入长度不能太长。所以对于输入长一些的文档级别的任务,Bert就不容易解决好。结论是:Bert更适合解决句子级别或者段落级别的NLP任务。
贡献
- 引入了Masked LM,使用双向LM做模型预训练。
- 为预训练引入了新目标NSP,它可以学习句子与句子间的关系。
- 进一步验证了更大的模型效果更好: 12 --> 24 层。
- 为下游任务引入了很通用的求解框架,不再为任务做模型定制。
BERT下游任务
举例一个语言生成模型的例子:
生成下一句话的过程通常是在上下文的基础上,通过模型预测下一个词或下一个子序列。在使用 last_hidden_states
进行下一句话生成时,你需要使用一个解码器(decoder)来输出新的文本
以下是通过循环神经网络(LSTM)解码器生成下一句话的例子
import torch
from transformers import BertModel, BertTokenizer
import torch.nn as nn
import torch.nn.functional as F
# 加载预训练的BERT模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
bert_model = BertModel.from_pretrained('bert-base-uncased')
# 定义一个简单的LSTM解码器
class LSTMDecoder(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(LSTMDecoder, self).__init__()
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden=None):
lstm_out, _ = self.lstm(x, hidden)
output = self.fc(lstm_out)
return output
# 输入文本
input_text = "I love natural language processing!"
# 使用BERT的分词器对输入进行编码
input_ids = tokenizer.encode(input_text, return_tensors='pt')
# 获取BERT的输出
with torch.no_grad():
outputs = bert_model(input_ids)
# 获取最后一层的隐藏状态
last_hidden_states = outputs.last_hidden_state
# 定义LSTM解码器
decoder = LSTMDecoder(input_size=last_hidden_states.shape[-1], hidden_size=256, output_size=tokenizer.vocab_size)
# 将BERT的输出传递给解码器,并生成下一句话
decoder_output = decoder(last_hidden_states)
# 在这里,decoder_output 可以用作生成模型的输出,你可以定义适当的损失函数,并进行训练。
# 获取下一句话的概率分布
next_word_probs = F.softmax(decoder_output, dim=-1)
# 从概率分布中采样生成下一个词
sampled_next_word = torch.multinomial(next_word_probs[:, -1, :], 1)
# 将生成的词加入到输入序列中
generated_sequence = torch.cat([input_ids, sampled_next_word], dim=-1)
# 将生成的词转化为文本
generated_text = tokenizer.decode(generated_sequence[0].tolist())
print("Generated Text:", generated_text)
这个例子中,LSTMDecoder
接收 last_hidden_states
作为输入,然后通过LSTM解码器生成下一个词的概率分布。我们使用softmax函数获取概率分布,并从中采样得到下一个词。生成的词被添加到输入序列中,然后迭代该过程,直到生成所需长度的文本。