文章目录
- 摘要
- Abstract
- 1 自监督学习(Self-Supervised Learning)
- 1.1 BERT
- 1.1.1 Masking Input
- 1.1.2 Next Sentence Prediction
- 1.1.3 BERT的使用方式
- 1.2 Why does BERT work?
- 1.3 Multi-lingual BERT
- 2 pytorch中tensor相关函数学习使用
- 2.1 张量拼接与拆分
- 2.1.1 tensor.cat张量合并
- 两个二维张量拼接
- 两个三维张量拼接
- 2.1.2 tensor.stack张量合并
- 2.1.3 tensor.split张量分割
- 2.1.4 tensor.chunk张量分割
- 总结
摘要
主要学习了自监督学习的相关知识,了解了 BERT 是如何实现自监督学习以及 BERT 为什么能够有效地完成任务。通过 BERT 的四个例子来理解怎样使用 BERT 。最后了解了 Multi-lingual BERT 的基本知识。补充学习了pytorch中有关tensor的拼接与拆分
Abstract
I mainly learned the relevant knowledge of self-supervised learning, and understood how BERT realized self-supervised learning and why BERT could effectively complete tasks. Take a look at four examples of BERT to understand how to use BERT. Finally, I have understood the basic knowledge of Multi-lingual BERT. I also learned the splicing and splitting of tensor in pytorch
1 自监督学习(Self-Supervised Learning)
对于Supervised Learning(监督式学习),我们需要一组带有 label 的训练资料,当我们把 x 输入模型之后将输出的 y 和对应的 label 作对比来训练模型。
而Self-Supervised Learning(自监督学习)属于非监督式学习的一种,我们把训练资料 x 分为两部分,把其中一部分输入到模型中,再把另一部分作为 label 来训练这个模型。
1.1 BERT
BERT 模型是自监督学习的经典模型,BERT 是一个 Transformer 的编
码器,BERT 的架构与 Transformer 的编码器完全相同,里面有很多自注意力和残差连接、归一化等等. BERT 可以输入一行向量,输出另一行向量. 输出的长度与输入的长度相同
1.1.1 Masking Input
BERT 的输入是一段文字. 接下来需要随机掩码一些输入文字,被掩码的部分是随机决定的,有两种方法来实现掩码:
- 用特殊符号替换句子中的单词,使用“MASK”词元来表示特殊符号,可以将其看成一个新的汉字
- 用另一个字随机替换一个字. 本来是“度”字,可以随机选择另一个汉字来替换它
这两种方法都可以使用,使用哪种方法也是随机确定的
掩码后,向 BERT 输入了一个序列,BERT 的相应输出就是另一个序列,接下来,查看输入序列中掩码部分的对应输出,仍然在掩码部分输入汉字,它可能是“MASK”词元或随机单词,它仍然输出一个向量,对这个向量使用线性变换然后做 softmax 并输出一个分布. 输出是一个很长的向量,包含要处理的每个汉字. 每个字对应一个分数,它是通过 softmax 函数生成的分布.
我们知道被掩码字符是哪个字符,而 BERT 不知道. 因为把句子交给BERT 时,该字符被掩码了,所以 BERT 不知道该字符,但我们知道掩码字“深度”一词中的“度”. 因此,训练的目标是输出一个尽可能接近真实答案的字符,即“度”字符. 独热编码可以用来表示字符,并最小化输出和独热向量之间的交叉熵损失
1.1.2 Next Sentence Prediction
训练 BERT 时,除了掩码之外,还有另一种方法:下一句预测(next sentence prediction)
给定一个很长的序列,其中包括两个句子,中间有个 [SEP] 词元(表示两个句子分割),前面有一个 [CLS]词元
我们只取与 [CLS] 对应的输出,忽略其他输出,并将 [CLS]的输出乘以线性变换. 现在它做一个二元分类问题,它有两个可能的输出:是或否。这种方法称为Next Sentence Prediction
即需要预测第二句是否是第一句的后一句(这两个句子是不是相接的). 如果第二句确实是后续句子(这两个句子是相接的),就要训练 BERT 输出“是”. 当第二句不是后一句时(这两个句子不是相接的),BERT 需要输出“否”作为预测.
1.1.3 BERT的使用方式
在训练时,让BERT学习了两个任务:
- 把一些字符掩盖起来,让它做填空题,补充掩码的字符.
- 预测两个句子是否有顺序关系(两个句子是否应该接在一起)
通过这两个任务,BERT 学会了如何填空。在训练模型填空后,也可以用于其他任务
- downstream task(下游任务)
这些任务不一定与填空有关,它可能是完全不同的东西. 尽管如此,BERT 仍然可以用于这些任务. 这些任务是真正使用 BERT 的任务,称为**下游任务(downstream task)**下游任务就是我们实际关心的任务. 但当 BERT 学习完成这些任务时,仍然需要一些标注的数据
给 BERT 一些有标注的数据,它可以学习各种任务,将 BERT 分化并用于各种任务称为微调(fine-tuning),让它可以做某种任务。
在微调之前产生此 BERT 的过程称为Pre-train(预训练), 所以产生 BERT 的过程就是自监督学习,也可以将其称为预训练。
case1
假设下游任务是输入一个序列并输出一个类别. 这是一个分类问题,输入是一个序列
例如:情感分析,给机器一个句子,并让它判断句子是正面的还是负面的
其中 BERT 需要先经过预训练来初始化参数,而 Linear部分仅需随机初始化。BERT 的部分输入非监督式学习,而在下游任务的环节需要一部分标注的资料,这属于监督式学习。因此,整个环节应该属于半监督式学习。
case2:
对于词性标注问题也一样,BERT 参数不是随机初始化,而是在Pre-train 阶段就找到一组比较好的初始化参数了。
case3:
NLI(Natural Language Inference,自然语言推理)是一项重要的自然语言处理(NLP)任务,它涉及到判断两个句子之间的逻辑关系。NLI任务的核心在于理解两个句子之间的逻辑联系,并根据这种联系做出合理的推断。具体来说,NLI任务通常包含两个句子:前提句(premise)和假设句(hypothesis),任务是确定假设句相对于前提句的逻辑关系。
将两个句子输入 BERT 产生一组新的向量,将 CLS 产生的向量丢到 Linear transform 就可以得到相应的类别了。其中,BERT 的参数也是预先训练好的。
case4:
基于提取的问答(extraction-based question answering):给机器读一篇文章,问它一个问题,它就会回答一个答案(但是这个答案必须出现在文章里面)
首先计算橙色向量和文档对应的输出向量的内积(inner product).由于有 3 个词元代表文章,因此它将输出 3 个向量. 计算这 3 个向量与橙色向量的内积可以得到 3 个值. 然后将它们传递给 softmax 函数,将得到另外 3 个值(这种内积与注意力非常相似,把橙色部分可以视为查询,把黄色部分视为键,这就是一种注意力)
注意:蓝色和橙色向量是随机初始化的,而 BERT 是由其预训练的权重
初始化的
1.2 Why does BERT work?
将文字序列通过 BERT 之后产生的向量蕴含着文字本身的含义。例如“果”和“草”、“鸟”和“鱼”由于意思比较相近,所以在向量空间中的分布相近。但是 BERT 会考虑上下文的因素,比如“吃苹果”中的“果”和“苹果手机”的“果”距离较远。
我们可以验证一下对于一个同样的字在两句不同的语句的向量相似度。
图中颜色越鲜艳代表相似度越高,可以看到前五句话互相之间相似度较高,而前五句话和后五句话相似度就比较低了。
从语言学的观点来看,一个词语的意思和它的上下文有关。
BERT 正是从它的上下文抽取资讯来预测缺失的向量。
1.3 Multi-lingual BERT
Multi-lingual BERT(mBERT)是BERT的一个多语言版本,旨在支持多种语言的自然语言处理任务。mBERT 在设计上与单语言的BERT非常相似,但其预训练数据集包含了104种不同语言的文本,这使得 mBERT 能够理解和生成多种语言的文本表示。
用英文的 QA 资料集训练,然后在中文上测试,正确率居然表现也还不错。将中文和英文一起训练后正确率甚至快接近人类的正确率了
为什么在英文的训练集里训练 BERT 之后在中文测试里依旧有效呢?
原因就是不同语言描述同一事物或者相似事物的 embedding 很接近。
在英文资料上 BERT 可以做英文的填空题,而在中文资料上 BERT 可以做中文的填空题。如果说两种不同的语言本质上没有区别,那 BERT 怎么会知道它应该要做中文还是英文的填空题?
我们将所有英文的平均 embedding 求出来,再将所有中文的平均 embedding 求出来,最后将两者相减得到图上这个蓝色向量。这个向量就代表了中文和英文之间的差距。我们将 “there is a cat” 输入 BERT 后得到的向量加上这个蓝色向量,最后很神奇的就可以得到“那有一猫”。
2 pytorch中tensor相关函数学习使用
- range arange
arange:返回一个长度为: e n d − s t a r t s t e p \frac{end-start}{step} stepend−start的一维张量,start默认为0,step默认为1
range:返回一个长度为: e n d − s t a r t s t e p + 1 \frac{end-start}{step}+1 stepend−start+1的一维张量,start默认为0,step默认为1
# arange
a = torch.arange(end=5)
print(a) # tensor([0, 1, 2, 3, 4])
print(a.dtype) # torch.int64
# range
b = torch.range(start=0, end=5)
print(b) # tensor([0., 1., 2., 3., 4., 5.])
print(b.dtype) # torch.float32
c = torch.range(start=1, end=6)
print(c) # tensor([1., 2., 3., 4., 5., 6.])
- reshape
改变张量的维度,修改的维度的乘积跟修改之前的维度乘积相同即可。a = torch.reshape(a, (-1,))
将张量修改为一行
a = torch.rand([2, 3])
'''
tensor([[0.3855, 0.8812, 0.7528],
[0.9095, 0.4733, 0.1893]])
'''
a = torch.reshape(a, [3, 2])
'''
tensor([[0.3855, 0.8812],
[0.7528, 0.9095],
[0.4733, 0.1893]])
'''
a = torch.reshape(a, (-1,))
# tensor([0.3855, 0.8812, 0.7528, 0.9095, 0.4733, 0.1893])
2.1 张量拼接与拆分
方法 | 作用 | 区别 |
---|---|---|
cat | 合并 | 保持原有维度的数量 |
stack | 合并 | 原有维度数量加1 |
split | 分割 | 按照长度去分割 |
chunk | 分割 | 等分 |
2.1.1 tensor.cat张量合并
两个二维张量拼接
定义两个二维张量:
# 定义两个二维张量
a = torch.tensor([[1, 2, 3],
[4, 5, 6]])
b = torch.tensor([[7, 8, 9],
[10, 11, 12]])
print(a.shape) # torch.Size([2, 3])
print(b.shape) # torch.Size([2, 3])
- dim=0维度(在第一个维度进行合并,二维张量中,第一个维度为行)
cat_0 = torch.cat((a, b), dim=0)
print(cat_0)
print(cat_0.shape) # torch.Size([4, 3])
合并结果:
tensor([[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[10, 11, 12]])
- dim=1维度(在第二个维度进行合并,二维张量中,第二个维度为列)
cat_1 = torch.cat((a, b), dim=1)
print(cat_1)
print(cat_1.shape) # torch.Size([2, 6])
合并结果:
tensor([[ 1, 2, 3, 7, 8, 9],
[ 4, 5, 6, 10, 11, 12]])
注意:dim=-1
是对最后一个维度进行拼接,故二维张量中,dim=-1拼接和dim=1拼接相同
两个三维张量拼接
定义两个三维张量
# 定义两个三维张量
a = torch.tensor([[[1, 2, 3],
[4, 5, 6],
[7, 8, 9]],
[[10, 11, 12],
[13, 14, 15],
[16, 17, 18]]])
b = torch.tensor([[[20, 21, 22],
[23, 24, 25],
[26, 27, 28]],
[[29, 30, 31],
[32, 33, 34],
[35, 36, 37]]])
print(a.shape) # torch.Size([2, 3, 3])
print(b.shape) # torch.Size([2, 3, 3])
- dim=0维度(在第一个维度进行合并)
cat_0 = torch.cat((a, b), dim=0)
print(cat_0)
print(cat_0.shape) # torch.Size([4, 3, 3])
合并结果为:
tensor([[[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9]],
[[10, 11, 12],
[13, 14, 15],
[16, 17, 18]],
[[20, 21, 22],
[23, 24, 25],
[26, 27, 28]],
[[29, 30, 31],
[32, 33, 34],
[35, 36, 37]]])
- dim=1维度(在第二个维度进行合并,三维张量中,第二个维度为行)
cat_1 = torch.cat((a, b), dim=1)
print(cat_1)
print(cat_1.shape) # torch.Size([2, 6, 3])
合并结果:
tensor([[[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9],
[20, 21, 22],
[23, 24, 25],
[26, 27, 28]],
[[10, 11, 12],
[13, 14, 15],
[16, 17, 18],
[29, 30, 31],
[32, 33, 34],
[35, 36, 37]]])
- dim=2维度(在第三个维度进行合并,三维张量中,第三个维度为列)
cat_2 = torch.cat((a, b), dim=2)
print(cat_2)
print(cat_2.shape) # torch.Size([2, 3, 6])
合并结果:
tensor([[[ 1, 2, 3, 20, 21, 22],
[ 4, 5, 6, 23, 24, 25],
[ 7, 8, 9, 26, 27, 28]],
[[10, 11, 12, 29, 30, 31],
[13, 14, 15, 32, 33, 34],
[16, 17, 18, 35, 36, 37]]])
注意:dim=-1
是对最后一个维度进行拼接,故三维张量中,dim=-1拼接和dim=2拼接相同
2.1.2 tensor.stack张量合并
定义两个二维张量
# stack
a = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
b = torch.tensor([[10, 11, 12],
[13, 14, 15],
[16, 17, 18]])
print(a.shape) # torch.Size([3, 3])
print(b.shape) # torch.Size([3, 3])
对于stack和cat关于关键字dim的理解:
- cat方法中可以理解为原tensor的维度,dim=0,就是沿着原来的0轴进行拼接,dim=1,就是沿着原来的1轴进行拼接。
- stack方法中的dim则是指向新增维度的位置,dim=0,就是在新形成的tensor的维度的第0个位置新插入维度
dim=0,在第一个维度上,新增加一个维度
stack_0 = torch.stack((a, b), dim=0)
print(stack_0)
print(stack_0.shape) # torch.Size([2, 3, 3])
tensor([[[ 1, 2, 3],
[ 4, 5, 6],
[ 7, 8, 9]],
[[10, 11, 12],
[13, 14, 15],
[16, 17, 18]]])
dim=1,在第二个维度上,新增加一个维度
stack_1 = torch.stack((a, b), dim=1)
print(stack_1)
print(stack_1.shape) # torch.Size([3, 2, 3])
tensor([[[ 1, 2, 3],
[10, 11, 12]],
[[ 4, 5, 6],
[13, 14, 15]],
[[ 7, 8, 9],
[16, 17, 18]]])
dim=2,在第三个维度上,新增加一个维度
stack_2 = torch.stack((a, b), dim=2)
print(stack_2)
print(stack_2.shape) # torch.Size([3, 3, 2])
tensor([[[ 1, 10],
[ 2, 11],
[ 3, 12]],
[[ 4, 13],
[ 5, 14],
[ 6, 15]],
[[ 7, 16],
[ 8, 17],
[ 9, 18]]])
torch.Size([3, 3, 2])
2.1.3 tensor.split张量分割
split 可以指定将张量分成几份,并且指定每一份张量的长度
同理,dim=0为对第一维度进行分割,dim=1为对第二维度进行分割
a = torch.arange(start=0, end=10).reshape([5, 2])
# print(a)
'''
tensor([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
'''
- split_size_or_sections参数传入一个整数:将张量分割成长度为该整数长度,无法整除,最后一个取余
split_00, split_01 = torch.split(a, 3, dim=0)
print(split_00)
print(split_01)
print(split_00.shape) # torch.Size([3, 2])
print(split_01.shape) # torch.Size([2, 2])
tensor([[0, 1],
[2, 3],
[4, 5]])
tensor([[6, 7],
[8, 9]])
- split_size_or_sections参数传入一个数组:张量分割个数为数组长度,数组中数字代表分割成每个张量的长度
s1, s2, s3 = torch.split(a, [1, 2, 2], dim=0)
print(s1)
print(s2)
print(s3)
print(s1.shape) # torch.Size([1, 2])
print(s2.shape) # torch.Size([2, 2])
print(s3.shape) # torch.Size([2, 2])
tensor([[0, 1]])
tensor([[2, 3],
[4, 5]])
tensor([[6, 7],
[8, 9]])
2.1.4 tensor.chunk张量分割
chunk可以理解为均等分的split,但是当维度长度不能被等分份数整除时,虽然不会报错,但可能结果与预期的不一样,建议只在可以被整除的情况下运用
将张量拆分成指定数量的块
a = torch.arange(start=0, end=10).reshape([5, 2])
print(a)
'''
tensor([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
'''
- dim=0,在第一个维度进行分割
chunk_00, chunk_01 = torch.chunk(a, chunks=2, dim=0)
print(chunk_00)
print(chunk_01)
print(chunk_00.shape) # torch.Size([3, 2])
print(chunk_01.shape) # torch.Size([2, 2])
chunk_00:
tensor([[0, 1],
[2, 3],
[4, 5]])
chunk_01:
tensor([[6, 7],
[8, 9]])
- dim=1,在第二个维度进行分割
chunk_10, chunk_11 = torch.chunk(a, chunks=2, dim=1)
print(chunk_10)
print(chunk_11)
print(chunk_10.shape) # torch.Size([5, 1])
print(chunk_11.shape) # torch.Size([5, 1])
chunk_10:
tensor([[0],
[2],
[4],
[6],
[8]])
chunk_11:
tensor([[1],
[3],
[5],
[7],
[9]])
总结
BERT(Bidirectional Encoder Representations from Transformers)是一种基于Transformer架构的语言理解模型,它通过双向上下文来捕捉句子中的语义关系。BERT的最终目标是提升自然语言处理任务的效果,如问答系统和文本分类。通过在大规模文本上预训练并进行微调,BERT能够有效理解复杂的语言特征,推动了NLP领域的重大进展。