nlp (下)

Transformer

Transformer模型的作用

  • 基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务,如机器翻译,文本生成等。同时又可以构建预训练语言模型,用于不同任务的迁移学习
  • 在接下来的架构分析中,我们将假设使用Transformer模型架构处理从一种语言文本到另一种语 言文本的翻译工作,因此很多命名方式遵循NLP中的规则。比如:Embeddding层将称作文本嵌入 层,Embedding层产生的张量称为词嵌入张量,它的最后一维将称作词向量等

架构

transformer
  • 输入部分

    • 源文本嵌入层及其位置编码器
    • 目标文本嵌入层及其位置编码器
  • 输出部分

    • 线性层
    • softmax层
  • 编码器部分

    • 由N个编码器层堆叠而成

    • 每个编码器层由两个子层连接结构组成

    • 第一个子层连接结构包括一个多头自注意力子层和规范化层(无量纲化)以及一个残差连接

    • 第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

      transformer_encoder
  • 解码器部分

    • 由N个编码器层堆叠而成

    • 每个编码器层由三个子层连接结构组成

    • 第一个子层连接结构包括一个带掩码的多头自注意力子层和规范化层以及一个残差连接

    • 第一个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接

      注:多头注意力层≠多头自注意力层(Q = K = V)

    • 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

    transformer_decoder

输入部分

  • Embedding 层

  • 位置编码器

    ​ 作用:因为在Transformer的编码器结构中,并没有针对词汇位置信息的处理,因此需要Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中,以弥补位置信 息的缺失。

    三角函数位置编码$$ \begin{align} PE_{(pos,2i)}&=\sin(\frac{pos}{10000\frac{2i}{d_{model}}}) \\ PE_{(pos,2i+1)}&=\cos(\frac{pos}{10000\frac{2i}{d_{model}}}) \end{align} $$

    dmodel 为 embedding 中的维度参数

    ​ 正弦和余弦函数的周期性使得位置编码能够捕获序列中相对位置的变化。通过不同频率的组合,模型可以轻松学习到序列中词语的相对顺序。此外,正余弦函数的对称性也使得编码在高维空间中具有良好的分布特性


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import torch
import torch.nn as nn
import math

class Embedding(nn.Module):
def __init__(self, vocab_size, embed_dim):
super().__init__()
self.embed_dim = embed_dim
self.embed = nn.Embedding(vocab_size, embed_dim)

def forward(self, x):
# ! sqrt -> 1. 使其符合正态分布 2. 增强 embedding 的影响
return self.embed(x) * math.sqrt(self.embed_dim)

def test_embedding(vocab_size= 10, embed_dim= 512):
embed = Embedding(vocab_size, embed_dim)
x = torch.tensor([[1, 2, 4, 5], [4, 3, 2, 0]])
result = embed(x)
print(result.shape)

class PositionEncoding(nn.Module):
def __init__(self, embed_dim, dropout= 0.1, max_len= 60):
super().__init__()
# 参数 max_len 用来控制 token 的数量
self.dropout = nn.Dropout(dropout)
# 定义位置编码矩阵
pe = torch.zeros(max_len, embed_dim)
# 定义位置-列矩阵
position = torch.arange(0, max_len).unsqueeze(1)
# 定义变化矩阵
div_term = torch.exp(torch.arange(0, embed_dim, 2) * (-math.log(10000.0) / embed_dim))
matmulrs = position * div_term
pe[:, 0::2] = torch.sin(matmulrs)
pe[:, 1::2] = torch.cos(matmulrs)
pe = pe.unsqueeze(0)

# * 把pe位置编码矩阵 注册成模型的持久缓冲区buffer 模型保存时pe位置编码矩阵会一起保存
# buffer: 对模型效果有帮助,但不是模型结构中的参数或超参数,不参与模型训练
self.register_buffer('pe', pe)

def forward(self, x):
# ! x 为 embedding 矩阵
x = x + self.pe[:, :x.shape[1]]
return self.dropout(x)

def test_position_encoding(vocab_size= 10, embed_dim= 512):
embed = Embedding(vocab_size, embed_dim)
position = PositionEncoding(embed_dim)
x = torch.tensor([[1, 2, 4, 5], [4, 3, 2, 0]])
result = position(embed(x))
print(result.shape)

if __name__ == '__main__':
# test_embedding()
# test_position_encoding()
pass

编码器

掩码张量

掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换,它的表现形式是一个张量

在transformer中,掩码张量的主要作用在应用attention时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用。所以,我们要进行遮掩

多头注意力机制

计算公式: $$ Attention(Q,K,V)=Softmax(\frac{Q\cdot K^T}{\sqrt{d_k}})\cdot V $$ $\sqrt{d_k}$ 的作用:控制点积结果的方差,避免梯度消失问题,从而稳定训练过程

multi_attn

这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果

编码器的具体实现

流程:输入 -> 编码器 { 多个编码器层堆叠 [ 子层连接层1 (MultiHeadAttention -> Add & Norm) -> 子层连接层2 (Feed Foward -> Add & Norm) ] } -> 中间语料c

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import numpy as np
import copy

# * 输入部分
# ? 词嵌入
class Embed(nn.Module):
def __init__(self, vocab_size, d_k):
super().__init__()
self.d_k = d_k
self.embed = nn.Embedding(vocab_size, d_k)
def forward(self, x):
return self.embed(x) * math.sqrt(self.d_k)

# ? 位置编码
class PositionalEncoding(nn.Module):
def __init__(self, d_k, dropout= 0.1,max_len= 60):
super().__init__()
self.dropout = nn.Dropout(dropout)
pe = torch.zeros(max_len, d_k)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_k, 2).float() * (-math.log(10000.0) / d_k))
mulars = position * div_term
pe[:, 0::2] = torch.sin(mulars)
pe[:, 1::2] = torch.cos(mulars)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)

def forward(self, x):
return self.dropout(x + self.pe[:, :x.shape[1]])

# * 模型部分
# ? 注意力计算
def Attention(query, key, value, mask=None, dropout=None):
d_k = query.size(-1)
scores = query @ key.transpose(-2, -1) / np.sqrt(d_k)
if mask is not None:
scores.masked_fill_(mask == 0, -1e9)
p_attn = F.softmax(scores, dim=-1)
if dropout is not None:
p_attn = dropout(p_attn)
return p_attn @ value

# ? 克隆模型
def Clone(model, N):
return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])

# ? 多头注意力
class MultiHeadAttention(nn.Module):
def __init__(self, head, d_k, dropout= 0.1):
super().__init__()
self.head = head
self.d_k = d_k // head
self.dropout = nn.Dropout(dropout)
self.linear = Clone(nn.Linear(d_k, d_k), 4)

def forward(self, query, key, value, mask=None):
if mask is not None:
mask = mask.unsqueeze(1)
batch_size = query.size(0)
query, key, value = [l(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
for l, x in zip(self.linear, (query, key, value))]
result = Attention(query, key, value, mask, self.dropout)
return self.linear[-1](result.transpose(1, 2).contiguous().view(batch_size, -1, self.d_k * self.head))

# ? 前馈全连接
class Feed_forward(nn.Module):
def __init__(self, d_k, d_ff, dropout= 0.1):
super().__init__()
self.linear = nn.Sequential(
nn.Linear(d_k, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_k),
nn.Dropout(dropout)
)

def forward(self, x):
return self.linear(x)

# ? 规范化层
class LayerNorm(nn.Module):
def __init__(self, features, eps= 1e-6):
super().__init__()
self.a2 = nn.Parameter(torch.ones(features))
self.b2 = nn.Parameter(torch.zeros(features))
self.eps = eps
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a2 * (x - mean) / (std + self.eps) + self.b2

# ? 子层连接层
class SublayerConnection(nn.Module):
def __init__(self, d_k, dropout= 0.1):
super().__init__()
self.norm = LayerNorm(d_k)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
return x + self.dropout(sublayer(self.norm(x)))

# ? 编码器层
class EncoderLayer(nn.Module):
def __init__(self, d_k, dropout= 0.1):
super().__init__()
self.d_k = d_k
self.attn = MultiHeadAttention(8, d_k, dropout)
self.norm = LayerNorm(d_k)
self.ff = Feed_forward(d_k, 2048, dropout)
self.sublayer = Clone(SublayerConnection(d_k, dropout), 2)

def forward(self, x, mask):
x = self.sublayer[0](x, lambda x: self.attn(x, x, x, mask))
return self.sublayer[1](x, self.ff)

# ? 编码器
class Encoder(nn.Module):
def __init__(self, layer, N):
super().__init__()
self.layers = Clone(layer, N)
self.norm = LayerNorm(layer.d_k)
def forward(self, x, mask):
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)

def test_encoder():
N, d_k = 6, 512
x = torch.randn(10, 8, d_k)
mask = torch.ones(8, 8)
try:
encoder = Encoder(EncoderLayer(d_k), N)
print(encoder(x, mask).shape)
except Exception as e:
print(e)

if __name__ == '__main__':
test_encoder()

解码器

解码器中的各个部分与编码器相同,可以直接基于此来构建解码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class DecoderLayer(nn.Module):
def __init__(self, d_k, head= 8, dropout= 0.1):
super().__init__()
self.d_k = d_k
self.self_attn = MultiHeadAttention(head, d_k, dropout)
self.norm = LayerNorm(d_k)
self.enc_dec_attn = MultiHeadAttention(head, d_k, dropout)
self.ff = Feed_forward(d_k, 2048, dropout)
self.sublayer = Clone(SublayerConnection(d_k, dropout), 3)

def forward(self, x, c, source_mask, target_mask):
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))
x = self.sublayer[1](x, lambda x: self.enc_dec_attn(x, c, c, source_mask))
return self.sublayer[2](x, self.ff)

# ? 解码器
class Decoder(nn.Module):
def __init__(self, layer, N):
super().__init__()
self.layers = Clone(layer, N)
self.norm = LayerNorm(layer.d_k)
def forward(self, x, c, source_mask, target_mask):
for layer in self.layers:
x = layer(x, c, source_mask, target_mask)
return self.norm(x)

输出部分

可以不使用 softmax,搭配 CrossEntropyLoss 使用,如果使用,请搭配 NLLloss 使用

1
2
3
4
5
6
7
8
class Generator(nn.Module):
def __init__(self, d_k, vocab_size):
super().__init__()
self.linear = nn.Linear(d_k, vocab_size)
self.softmax = nn.LogSoftmax(dim=-1)

def forward(self, x):
return self.softmax(self.linear(x))

封装为transformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Transformer(nn.Module):
def __init__(self, vocab_size, d_k, N, head= 8, ff_dim= 2048, max_len= 60, dropout= 0.1):
super().__init__()
self.encoder = Encoder(EncoderLayer(d_k, head, ff_dim, dropout), N)
self.decoder = Decoder(DecoderLayer(d_k, head, ff_dim, dropout), N)
self.generator = Generator(d_k, vocab_size)
self.predone = nn.Sequential(
Embed(vocab_size, d_k),
PositionalEncoding(d_k, dropout, max_len)
)

def forward(self, source, target, mask, source_mask, target_mask):
x = self.predone(source)
y = self.predone(target)
return self.generator(self.decoder(y, self.encoder(x, mask), source_mask, target_mask))

Fasttext

作用:

  • 进行文本分类
  • 训练词向量

文本分类的种类:

  • 二分类:文本被分类两个类别中,往往这两个类别是对立面,比如:判断一句评论是好评还是差评
  • 单标签多分类:文本被分入到多个类别中,且每条文本只能属于某一个类别(即被打上某一个标签)比如:输入一个人名,判断它是来自哪个国家的人名。
  • 多标签多分类:文本被分人到多个类别中,但每条文本可以属于多个类别(即被打上多个标签)比如:输入一段描述,判断可能是和哪些兴趣爱好有关,一段描述中可能即讨论了美食,又讨论了游戏爱好

模型架构

  • Fasttext 模型架构和 Word2Vec 中的 CBOW 模型很类似,不同在于 Fasttext 预测标签,而 CBOW 预测中间值

  • Fasttext 的模型分为三层架构:

    • 输入层:对文档 Embedding 后的向量,包含 n-gram 特征
    • 隐藏层:对输入数据的求和平均
    • 输出层:是对应文本的 Lable

    ![fasttextfasttext.png)

  • 文本分类的过程:

    • 第一步:获取数据
    • 第二步:训练集和测试集的划分
    • 第三步:训练模型
    • 第四步:使用模型进行预测并评估
    • 第五步:模型调优
    • 第六步:模型保存与重加载

迁移学习

  • 预训练模型

    ​ 一般情况下预训练模型都是大型模型,具备复杂的网络结构,众多的参数量,以及在足够大的数据集下进行训练而产生的模型。在NLP领域,预训练模型往往是语言模型。因为语言模型的训练是无监督的,可以获得大规模语料,同时语言模型又是许多典型NLP任务的基础,如机器翻译,文本生成,阅读理解等,常见的预训l练模型有BERT,GPT,roBERTa,transformer-XL等

  • 微调

    ​ 根据给定的预训练模型,改变它的部分参数或者为其新增部分输出结构后,通过在小部分数据集上训练,来使整个模型更好的适应特定任务

  • 两种迁移方式

    • 直接使用预训练模型进行相同任务的处理,不需要调整参数或模型结构,这些模型开箱即用。但是这种情况一般只适用于普适任务,如:fasttest工具包中预训练的词向量模型。另外,很多预训练模型开发者为了达到开箱即用的效果,将模型结构分各个部分保存为不同的预训练模型,提供对应的加载方法来完成特定目标
    • 更加主流的迁移学习方式是发挥预训练模型特征抽象的能力,然后再通过微调的方式,通过训练更新小部分参数以此来适应不同的任务。这种迁移方式需要提供小部分的标注数据来进行监督学习

基于 bert-base-chinese 的迁移学习案例 (中文分类任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import torch
import torch.nn as nn
from torch.optim import AdamW
from datasets import load_dataset
from transformers import BertTokenizer, BertModel
from torch.utils.data import DataLoader
import time
from tqdm import tqdm

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')
model = model.to(device)

def read_data():
# * 训练集
train_dataset = load_dataset('csv', data_files= './data/train.csv', split= 'train')
# * 测试集
test_dataset = load_dataset('csv', data_files= './data/test.csv', split= 'train')
# * 验证集
valid_dataset = load_dataset('csv', data_files= './data/valid.csv', split= 'train')

return train_dataset, test_dataset, valid_dataset

def collate_fn(data):
sentences = [i['text'] for i in data]
labels = [i['label'] for i in data]

data = tokenizer.batch_encode_plus(
sentences,
max_length= 512,
padding= 'max_length',
truncation= True,
return_tensors= 'pt',
)

input_ids = data['input_ids']
token_type_ids = data['token_type_ids']
attention_mask = data['attention_mask']
labels = torch.tensor(labels, dtype= torch.long)
return input_ids, token_type_ids, attention_mask, labels

def get_dataloader():
train_dataset, test_dataset, valid_dataset = read_data()
train_dataloader = DataLoader(
train_dataset,
batch_size= 8,
collate_fn= collate_fn,
drop_last= True,
shuffle= True,
)
test_dataloader = DataLoader(
test_dataset,
batch_size= 8,
collate_fn= collate_fn,
drop_last= True,
shuffle= True,
)
valid_dataloader = DataLoader(
valid_dataset,
batch_size= 8,
collate_fn= collate_fn,
drop_last= True,
shuffle= True,
)

return train_dataloader, test_dataloader, valid_dataloader

class MyModel(torch.nn.Module):
def __init__(self):
super().__init__()
# ! 二分类
self.fc = torch.nn.Linear(768, 2)

def forward(self, input_ids, token_type_ids, attention_mask):
with torch.no_grad():
outputs = model(
input_ids= input_ids,
token_type_ids= token_type_ids,
attention_mask= attention_mask,
)

return self.fc(outputs['pooler_output'])

def train_model(train_loader):
mymodel = MyModel().to(device)
optimizer = AdamW(mymodel.parameters(), lr= 5e-4)
criterion = nn.CrossEntropyLoss()
epochs = 10
mymodel.train()
for epoch in range(epochs):
start_time = time.time()
total_loss, total_acc = 0, 0
num = 0
for idx, (input_ids, token_type_ids, attention_mask, labels) in enumerate(tqdm(train_loader)):
labels = labels.to(device)
outputs = mymodel(input_ids.to(device), token_type_ids.to(device), attention_mask.to(device))
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
total_acc += (torch.argmax(outputs, dim= -1) == labels).sum().item()
num += len(labels)

print(f"epoch: [{epoch + 1}/{epochs}] loss: {total_loss / num:.5f} acc: {total_acc / num:.5f} time: {time.time() - start_time:.5f}s")

torch.save(mymodel.state_dict(), f"./model/model{epoch}.pth")

def model_test(test_loader):
mymodel = MyModel().to(device)
mymodel.load_state_dict(torch.load('./model/model0.pth'))
mymodel.eval()
total_acc = 0
num = 0
with torch.no_grad():
for idx, (input_ids, token_type_ids, attention_mask, labels) in enumerate(tqdm(test_loader)):
labels = labels.to(device)
outputs = mymodel(input_ids.to(device), token_type_ids.to(device), attention_mask.to(device))
total_acc += (torch.argmax(outputs, dim= -1) == labels).sum().item()
num += len(labels)

print(f"准确率: {total_acc / num:.5f}")




if __name__ == '__main__':
# read_data()
train_loader, test_loader, valid_loader = get_dataloader()
# train_model(train_loader)
# model_test(test_loader)
# model_test(valid_loader)

BERT系列模型

BERT

BERT是2018年10月由GoogleAI研究院提出的一种预训练模型

  • BERT的全称是Bidirectional Encoder Representation from Transformers.
  • BERT在机器阅读理解顶级水平测试SQUAD1.1中表现出惊人的成绩:全部两个衡量指标上全面超越人类,并且在11种不同NLP测试中创出SOTA表现.包括将GLUE基准推高至80.4%(绝对改进7.6%),MultiNLI准确度达到86.7%(绝对改进5.6%)。成为NLP发展史上的里程碑式的模型成就
bert

Bert包含三部分:

  • 黄色标记:Embedding模块

    bert-embedding
    • TokenEmbeddings是词嵌入张量,第一个单词是CLS标志,可以用于之后的分类任务
    • SegmentEmbeddings是句子分段嵌入张量,是为了服务后续的两个句子为输入的预训练任务
    • PositionEmbeddings是位置编码张量,此处注意和传统的Transformer不同,不是三角函数计算的固定位置编码,而是通过学习得出来的
    • 整个Embedding模块的输出张量就是这3个张量的直接加和结果
  • 蓝色标记:双向Transformer模块

    ​ BERT中只使用了经典Transformer架构中的Encoder部分,完全舍弃了Decoder部分,而两大预训练任务也集中体现在训练Transformer模块中.

  • 绿色标记:预微调模块

    ​ 经过中间层Transformer的处理后,BERT的最后一层根据任务的不同需求而做不同的调整即可 ​ 比如对于sequence-level的分类任务,BERT直接取第一个[CLS] token 的final hidden state,再加一层全连接层后进行softmax就可以用来预测最终的标签

    对于不同的任务,微调都集中在预微调模块

    bert-pre

    对于 Bert 模型的超参数建议如下:

    Batch_size: 16 / 32

    Learing_rate: 5e-5, 3e-5, 2e-5

    Epoch: 3, 4

BERT预训练任务:

BERT包含两个预训练任务

  • Masked LM(带mask的语言训练模型)
    • 关于传统的语言模型训l练,都是采用left-to-right,或者left-to-right + right-to-left结合的方式,但这种单向方式或者拼接的方式提取特征的能力有限,为此BERT提出一个深度双向表达模型(deep bidirectional representation). 即采用MASK任务来训练模型.
    • 1:在原始训练文本中,随机的抽取15%的token作为参与MASK任务的对象
    • 2:在这些被选中的token中,数据生成器并不是把它们全部变成[MASK]而是有下列3种情况
      • 2.1:在80%的概率下,用[MASK]标记替换该token, 比如my dog is hairy > my dog is [MASK]
      • 2.2:在10%的概率下,用一个随机的单词替换token, 比如my dog is hairy -> my dog is apple
      • 2.3:在10%的概率下,保持该token不变,比如my dog is hairy > my dog is hairy
    • 3:模型在训练的过程中,并不知道它将要预测哪些单词,哪些单词是原始的样子,哪些单词被遮掩成了[MASK],哪些单词被替换成了其他单词。正是在这样一种高度不确定的情况下,反倒逼着模型快速学习该token的分布式上下文的语义,尽最大努力学习原始语言说话的样子,同时因为原始文本中只有15%的token参与了MASK操作,并不会破坏原语言的表达能力和语言规则.
  • Next Sentence Prediction(NSP任务)
    • 在NLP中有一类重要的问题比如QA(Quention-Answer), NLI(Natural Language Inference), 需要模型能够很好的理解两个句子之间的关系,从而需要在模型的训练中引入对应的任务,在BERT中引入的就是NextSentencePrediction任务。采用的方式是输入句子对(A,B)模型来预测句子B是不是句子A的真实的下一句话。
    • 1:所有参与任务训练的语句都被选中作为句子A
      • 1.1:其中50%的B是原始文本中真实跟随A的下一句话(标记为IsNext,代表正样本)
      • 1.2:其中50%的B是原始文本中随机抽取的一句话(标记为NotNext,代表负样本)
    • 2:在该任务上,BERT模型可以在测试集上取得97%-98%的准确率。

在MLM任务中采用 80% 10% 10% 分布的原因

  • 首先,如果所有参与训练的token被100%的[MASK],那么在fine-tunning的时候所有单词都是已知的,不存在[MASK],那么模型就只能根据其他token的信息和语序结构来预测当前词,而无法利用到这个词本身的信息,因为它们从未出现在训练过程中,等于模型从未接触到它们的信息,等于整个语义空间损失了部分信息,采用80%的概率下应用[MASK]既可以让模型去学着预测这些单词,又以20%的概率保留了语义信息展示给模型
  • 保留下来的信息如果全部使用原始token,那么模型在预训练的时候可能会偷懒,直接照抄当前token信息。采用10%概率下randomtoken来随机替换当前token,会让模型不能去死记硬背当前的token,而去尽力学习单词周边的语义表达和远距离的信息依赖,尝试建模完整的语言信息
  • 最后再以10%的概率保留原始的token,意义就是保留语言本来的面貌,让信息不至于完全被遮掩,使得模型可以“看清”真实的语言面貌

处理长文本的方法

首选要明确一点,BERT预训l练模型所接收的最大sequence长度是512。那么对于长文本(文本长度超过512的句子),就需要特殊的方式来构造训练样本。核心就是如何进行截断

  • head-only方式:这是只保留长文本头部信息的截断方式,具体为保存前510个token(要留两个位置给[CLS]和[SEP]).
  • tail-only方式:这是只保留长文本尾部信息的截断方式,具体为保存最后510个token(要留两个位置给[CLS]和[SEP]).
  • head+only方式:选择前128个token和最后382个token(文本总长度在800以内),或者前256个token和最后254个token(文本总长度大于800).

ALBERT

AIBERT模型发布于ICLR 2020会议,是基于BERT模型的重要改进版本.是谷歌研究院和芝加哥大学共同发布的研究成果。

论文全称<< A Lite BERT For Self-Supervised Learning Of Language Representations >>.

从模型架构上看,AIBERT和BERT基本一致,核心模块都是基于Transformer的强大特征提取能力.

相比较于BERT模型,AIBERT的出发点即是希望降低预训练的难度,同时提升模型关键能力. 主要引入了5大优化:

  • 第一:词嵌入参数的因式分解
  • 第二:隐藏层之间的参数共享
  • 第三:去掉NSP增加SOP预训练任务
  • 第四:去掉dropout操作
  • 第五:MLM任务的优化

nlp (下)
http://example.com/2025/12/16/nlp-2/
作者
Suzuran
发布于
2025年12月16日
许可协议