使用Transformer Encoder进行NER任务

使用Transformer Encoder进行NER任务

我们都知道命名实体识别任务最常用的网络结构是BiLSTM + CRF的结构,在transformer被提出之后,transformer也被用于命名实体识别任务,但是一般是使用Transformer的Encoder模块作为特征提取器,后面还是使用softmax、线性层或CRF层,也就是说Transformer的Decoder模块是不使用的(例如预训练模型BERT也是仅堆叠transformer encoder)。

那么为什么在命名实体识别任务中不使用Decoder模型呢?
其实答案是比较容易理解的,我们都知道Decoder是利用Encoder得到的memory和当前的输出,自回归的不断给出下一个概率最高的输出,所以Decoder是一个主要用于生成的模型,也是目前最火热的大模型如GPT最主要的核心结构,即主要用于生成大段的文本,而命名实体识别任务本质上是一个序列标注任务,其并不需要根据输入文本去生成其他文本,它只需要判断给定文本每个字是否属于实体以及实体的类型,所以并不需要Decoder模块,同样的序列的分类任务也不需要Decoder模块。
当然不需要不等于不能用,你也可以把序列标注、序列分类任务转变成生成任务,但是这样会使得你的模型结构变得更加复杂,效率会下降,效果也并不一定能得到提升。

Transformer一步步实现一文中我们已经理解了transformer的实现过程,在这一篇中我们要将transformer的encoder部分应用于命名实体识别任务。

代码示例

第一部分是transformer的实现相关代码,但是剔除了Decoder部分,因为命名实体识别任务无需Decoder。

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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""
最简单版本的transformer encoder实现(非BERT)
"""
import copy
import torch
import torch.nn as nn
import numpy as np
import math
import torch.nn.functional as F
from torch.autograd import Variable
from torchcrf import CRF


def clones(module, n):
""" 将给定的网络clone n次 """
return nn.ModuleList([copy.deepcopy(module) for _ in range(n)])


# LayerNorm可以自行实现,也可以使用Pytorch内置的
class LayerNorm(nn.Module):
""" Construct a layer norm module (See citation for details)"""
def __init__(self, features, eps=1e-6):
super(LayerNorm, self).__init__()
self.a_2 = nn.Parameter(torch.ones(features)) # features等于dmodel词向量维度
self.b_2 = 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.a_2 * (x - mean) / (std + self.eps) + self.b_2


class SublayerConnection(nn.Module):
""" 残差连接 """
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size) # size等于dmodel词向量维度
self.dropout = nn.Dropout(dropout)

def forward(self, x, sublayer):
return x + self.dropout(sublayer(self.norm(x)))


class TransformerEncoder(nn.Module):
"""Encoder 堆叠了N层 Encoder Layer"""
def __init__(self, layer, n):
super(TransformerEncoder, self).__init__()
self.layers = clones(layer, n) # 这个layer实际就是Encoder Layer
self.norm = LayerNorm(layer.size)

def forward(self, x, mask):
# layers里有N个EncoderLayer,EncoderLayer中包括multi-head、feedforward、sublayer
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)


class EncoderLayer(nn.Module):
"""
EncoderLayer层里包括一个Multi-head attention + 残差连接 + feedforward + 残差链接
Encoder 由N个 EncoderLayer组成
"""
def __init__(self, size, self_attn, feed_forward, dropout):
"""
:param size: 词向量维度(d_model)
:param self_attn: Multi-Head Attention
:param feed_forward:
:param dropout:
"""
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
# 初始化得到两个残差连接层
self.sub_layer = clones(SublayerConnection(size, dropout), 2)
self.size = size

def forward(self, x, mask):
"""
Follow Figure 1 (left) for connections
:param x: size(batch, seq_length, d_model)
:param mask: size(batch, 1, seq_length),mask在encoder中没啥用,主要是在decoder中实现遮蔽的多头注意力
"""
# 先使用第一个残差连接层,连接x和多头注意力后的值
# (lambda y: self.self_attn(y, y, y, mask),这只是一个参数,在这里并不执行)
x = self.sub_layer[0](x, lambda y: self.self_attn(y, y, y, mask))
return self.sub_layer[1](x, self.feed_forward)


def subsequent_mask(size):
""" Masked attention 在训练期间,当前解码位置的词不能Attend到后续位置的词 """
attn_shape = (1, size, size)
# np.triu(a, k)是取矩阵a的上三角数据,k=0 的时候就是包含对角线和上(下)方的元素,其他值为 0
# k = 1, 2, ...的时候,对角线向上(下)移动
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
return torch.from_numpy(subsequent_mask) == 0


def attention(query, key, value, mask=None, dropout=None):
""" Compute 'Scaled Dot Produce Attention' """
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None: # masked_fill(tensor, value),在tensor中为True的位置,填充value
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim=-1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn


class MultiHeadAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
"""
Take in model size and number of heads
:param h: 注意力头个数
:param d_model: 词向量维度
"""
super(MultiHeadAttention, self).__init__()
assert d_model % h == 0
# We assume d_v always equals d_k
self.d_k = d_model // h # 每个注意力头要计算的词向量维度
self.h = h
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)

def forward(self, query, key, value, mask=None):
"""
Implements Figure 2
query、key、value都是输入x的副本
"""
if mask is not None:
# Same mask applied to all h heads
mask = mask.unsqueeze(1)
n_batches = query.size(0)
# 1) Do all the linear projections in batch from d_model => h x dk
# 先经过一个线性层,然后在第1维增加一维(自适应大小),将d_model拆分成 h x dk,最后再将第1维和第2维交换
query, key, value = [
l(x).view(n_batches, -1, self.h, self.d_k,).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]

# 2) Apply attention on all the projected vectors in batch
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

# 2) "Concat" using a view and apply a final linear
# 这里多头注意力计算完成,再将x恢复成最初的形状
x = x.transpose(1, 2).contiguous().view(n_batches, -1, self.h*self.d_k)
return self.linears[-1](x) # 怎么还要过一层线性层?


class PositionWiseFeedForward(nn.Module):
"""Implements FFN equation."""
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionWiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)

def forward(self, x):
return self.w_2(self.dropout(F.relu(self.w_1(x))))


class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
"""
:param d_model: 词向量维度大小
:param vocab: 词表大小
"""
super(Embeddings, self).__init__()
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model

def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model) # 为啥要乘以sqrt(d_model)


class PositionEncoding(nn.Module):
"""Implement the PE function"""
def __init__(self, d_model, dropout, max_len=5000):
super(PositionEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)

# Compute the positional encoding once in log space
pe = torch.zeros(max_len, d_model) # pe用来存储,每个位置-每个维度的位置信息
position = torch.arange(0, max_len).unsqueeze(1) # 位置张量,size为(句子长度, 1)
# torch.arange(0, d_model, 2) ==> 2i,size为d_model/2
div_term = torch.exp(torch.arange(0, d_model, 2)*-(math.log(10000.0)/d_model))
# position * div_term 的 size为(句子长度, d_model/2)
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度(2i),注意不是位置
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度(2i+1)
pe = pe.unsqueeze(0) # 增加一维为了保持和x的size一致,可以直接加和
# 定义pe参数在模型训练时不可更新,即optimizer.step()后不会被更新
self.register_buffer('pe', pe) # 并且同时还有self.pe = pe的功效

def forward(self, x):
# x的size为(batch, seq_length, d_model),x.size(1)为句子长度
# 取出给定x的句子长度的位置向量,前提是x<5000,pe的size为(1, 5000, d_model)
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=True)
return self.dropout(x)

第2部分是将Transformer Encoder作为命名实体识别的编码器,并增加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
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
class NerTransformerModel(nn.Module):
"""
使用最原始的transformer encoder作为编码器的NER模型
"""
def __init__(self, configs): # 因为要传的参数较多,所以这里将所有的参数都存储在字典中
super(NerTransformerModel, self).__init__()
self.embedding_dim = configs['embedding_dim'] # 词向量维度(长度),即用多少维度向量表示一个词
self.hidden_size = configs['hidden_size']
self.vocab_size = configs['vocab_size'] # 词表大小
self.label_to_id = configs['label_to_id'] # 标签序号的映射字典
self.label_nums = len(configs['label_to_id']) # 标签数量/类别数
self.layer_nums = configs['layer_nums'] # encoder layer层数
self.use_crf = configs['use_crf'] # 是否使用CRF层(先不考虑crf)
self.seq_length = configs['seq_length'] # 句子最大长度,超过截断,小于填充,会将所有输入神经网络的句子向量统一为该长度
self.dropout_rate = configs['dropout_rate']
self.heads_num = configs['heads_num'] # 注意力头个数
self.feed_forward_size = configs['feed_forward_size'] # feedforward层内部隐层大小

# 词嵌入层
self.embedding_layer = Embeddings(self.embedding_dim, self.vocab_size)
# 位置嵌入层
self.position_layer = PositionEncoding(self.embedding_dim, self.dropout_rate, self.seq_length)
# LayerNorm层
self.layer_norm = LayerNorm(self.embedding_dim)
# dropout层
self.dropout = nn.Dropout(self.dropout_rate)
# 注意力层
self.attention_layer = MultiHeadAttention(self.heads_num, self.embedding_dim)
self.feed_forward_layer = PositionWiseFeedForward(self.embedding_dim, self.feed_forward_size)
# 编码器
self.encoder = TransformerEncoder(
EncoderLayer(self.embedding_dim, copy.deepcopy(self.attention_layer),
copy.deepcopy(self.feed_forward_layer), self.dropout_rate),
self.layer_nums)
# 将Transformer提取的特征向量映射到特征空间,即经过全连接得到发射分数
self.hidden2tag = nn.Linear(self.hidden_size, self.label_nums)
if self.use_crf:
# crf层,可以使用pytorch的crf包
self.crf = CRF(self.label_nums, batch_first=True)

def forward(self, sentence, targets_mask, targets=None):
"""
NER model的前向传播过程
:param sentence: 传入句子,size(batch_size, seq_length)
:param targets_mask: 标签是否有效的标志,=1的部分是句子的有效部分,size(batch_size, seq_length)
也可以理解为bert的segment 因为只有一个句子所以属于这个句子的部分为1,其他的为0
:param targets: 标签,计算损失时需要, size(batch_size, seq_length)
:return:
"""
# 得到词向量
embeddings = self.embedding_layer(sentence)
embeddings = self.position_layer(embeddings)
output = self.encoder(embeddings, targets_mask)
# 经过全连接层得到发射分数,size:(batch_size, seq_length, label_nums)
features = self.hidden2tag(output)
if self.use_crf:
targets_mask = targets_mask.type(torch.uint8)
targets_mask = targets_mask.squeeze(1) # transformer encoder在开始处理时mask增加了一维,这里需要复原
pred = self.crf.decode(features, mask=targets_mask)
for j in range(len(pred)):
# 以STOP_TAG的id填充至seq_length长度
while len(pred[j]) < self.seq_length:
pred[j].append(self.label_nums-1)
# 从(batch_size, seq_length)转成(batch_size*seq_length)一维
pred = torch.tensor(pred).contiguous().view(-1)
# 传入targets时,计算损失
if targets is not None:
# targets_mask=1的部分计算损失
loss = -self.crf(F.log_softmax(features, 2), targets, mask=targets_mask, reduction='mean')
return loss, pred
else:
return None, pred
else:
if targets is not None:
total_loss = 0.0
batch_size = features.size()[0]
for i in range(batch_size):
length = torch.sum(targets_mask[i]) # 求出来的值就是1的数量,也就表示这个句子的长度
single_feature = features[i][:length] # 只计算句子有效部分的损失
single_target = targets[i][:length]
# 使用mean和sum所需的学习率可能有不同的要求
loss_f = nn.CrossEntropyLoss(reduction='sum')
loss = loss_f(single_feature, single_target)
total_loss += loss
batch_loss = total_loss / batch_size
# 为了统一返回的pred的size
features = features.contiguous().view(-1, self.label_nums)
pred = features.argmax(dim=-1)
return batch_loss, pred
else:
features = features.contiguous().view(-1, self.label_nums)
pred = features.argmax(dim=-1)
return None, pred

第3部分是一个简单的代码有效性测试。

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
def prepare_data(data, to_ix, seq_length, label_to_id):
"""
获取传入文本在词典中的序号
:param data: 训练数据,tuple形式,第一个元素是分割好的文本,以列表形式;第二个元素是标签,以列表形式
:param to_ix: 单词/词语 到 序号的映射字典
:param seq_length: 需要统一的句子长度
:param label_to_id: 标签序号的映射字典
:return:
"""
seq = data[0]
labels = data[1]
text_ids = [to_ix[w] for w in seq]
targets = [label_to_id[t] for t in labels]
targets_mask = [1] * len(text_ids)
if len(text_ids) > seq_length:
text_ids = text_ids[:seq_length]
targets = targets[:seq_length]
targets_mask = targets_mask[:seq_length]
while len(text_ids) < seq_length:
text_ids.append(to_ix["[PAD]"])
targets.append(label_to_id[STOP_TAG])
targets_mask.append(0)
return text_ids, targets_mask, targets


def get_transformer_encoder_params():
params = {
"embedding_dim": 384,
"hidden_size": 384,
"layer_nums": 3,
"use_crf": True,
"seq_length": 512, # 根据数据集情况调整,将句子全部处理成一致的长度(一般按照最长的句子设定)
"epoch_nums": 100,
"batch_size": 16,
"learning_rate": 0.001,
"weight_decay": 1e-4,
"momentum": 0.9,
"heads_num": 6,
"dropout_rate": 0.1,
"feed_forward_size": 1536,
"hidden_activation": "gelu", # 目前暂未使用
"attention_head_size": 64, # 一般情况下这个参数用不到,head_size = d_model / heads_num
"optimizer": "SGD", # SGD/Adam/AdamW
"scheduler": "linear", # 学习率调整策略
"warmup": 0.1,
# word2vec训练的词向量
# "vocab_path": "word_embedding/word2vec-384/vocab.txt",
# "pretrained_embedding": "word_embedding/word2vec-384/embedding_matrix.npy",
}
return params


if __name__ == "__main__":
configs = CONFIG
transformer_params = get_transformer_encoder_params()
configs.update(transformer_params)
# configs.update(get_small_bert_params())
STOP_TAG = "<STOP>"
START_TAG = "<START>"

# Make up some training data
training_data = [(
"the wall street journal reported today that apple corporation made money".split(),
"B I I I O O O B I O O".split()
), (
"georgia tech is a university in georgia".split(),
"B I O O O O B".split()
)]

word_to_id = {} # 词典
for sentence, tags in training_data:
for word in sentence:
if word not in word_to_id:
word_to_id[word] = len(word_to_id)
word_to_id["[PAD]"] = len(word_to_id)
configs['word_to_id'] = word_to_id
configs['vocab_size'] = len(word_to_id)

label_to_id = {"B": 0, "I": 1, "O": 2, START_TAG: 3, STOP_TAG: 4}
configs['label_to_id'] = label_to_id

# model = NerModel(configs)
model = NerTransformerModel(configs)
optimizer = optim.SGD(model.parameters(), lr=0.001, weight_decay=1e-4)

# 训练前检查模型预测结果
with torch.no_grad():
sentence0_length = len(training_data[0][0])
sentence1_length = len(training_data[1][0])
seq_length = configs['seq_length']
sentence0, targets_mask_0, targets_0 = prepare_data(training_data[0], word_to_id, seq_length, label_to_id)
sentence1, targets_mask_1, targets_1 = prepare_data(training_data[1], word_to_id, seq_length, label_to_id)
batch_sentence_0 = torch.tensor([sentence0, sentence1])
# batch_targets_0 = torch.tensor([targets_0])
batch_targets_mask_0 = torch.tensor([targets_mask_0, targets_mask_1])
batch_targets_mask_0 = batch_targets_mask_0.unsqueeze(-2) # transformer的mask需要多一步处理
_, batch_pred0 = model(batch_sentence_0, batch_targets_mask_0) # 返回损失和预测
print(f"句子1训练前网络得到的标签为:{batch_pred0[:sentence0_length]}")
print(f"句子2训练前网络得到的标签为:{batch_pred0[seq_length:seq_length+sentence1_length]}")
print(f"句子1正确的标签为:{targets_0[:sentence0_length]}")
print(f"句子2正确的标签为:{targets_1[:sentence1_length]}")

for epoch in range(100):
# 第一步,pytorch梯度积累,需要先清零梯度
model.zero_grad()

sentence0, targets_mask_0, targets_0 = prepare_data(training_data[0], word_to_id, seq_length, label_to_id)
sentence1, targets_mask_1, targets_1 = prepare_data(training_data[1], word_to_id, seq_length, label_to_id)
batch_sentence = torch.tensor([sentence0, sentence1])
batch_targets = torch.tensor([targets_0, targets_1])
batch_targets_mask = torch.tensor([targets_mask_0, targets_mask_1])
batch_targets_mask = batch_targets_mask.unsqueeze(-2) # transformer的mask需要多一步处理
# 第三步,进行前向计算,得到crf loss,当传入target时会自动计算loss
loss, _ = model(batch_sentence, batch_targets_mask, batch_targets)

# 第四步,计算loss,梯度,通过optimizer更新参数
loss.backward()
optimizer.step()

# 训练结束查看模型预测结果,对比观察模型是否学到东西
with torch.no_grad():
_, batch_pred0 = model(batch_sentence_0, batch_targets_mask_0) # 返回损失和预测
print(f"句子1训练后网络得到的标签为:{batch_pred0[:sentence0_length]}")
print(f"句子2训练后网络得到的标签为:{batch_pred0[seq_length:seq_length+sentence1_length]}")
# We got it!

以上只是测试模型能够进行有效的学习,还未涉及正式的项目训练,在实际训练时与前面的BiLSTM模型的输入格式是一致的,也是UER-py框架中命名实体识别任务的数据格式,具体可以参考这篇:https://forchenxi.github.io/2022/10/06/nlp-UER-cluener/

另外,因为我使用的同一套训练框架,使用自行实现的transformer进行训练时还要注意一点就是mask的size会和lstm略有不同,在data_loader里需要增加一行代码进行size调整

1
targets_mask_batch = targets_mask_batch.unsqueeze(-2)  # transformer的mask需要多一步处理

同时,在使用CRF的情况下,计算损失时需要将mask的size复原

1
targets_mask = targets_mask.squeeze(1)  # transformer encoder在开始处理时mask增加了一维,这里需要复原

增加CRF层的情况下,损失依然可以使用mean。
最后,在预测的predict.py中,保存预测结果部分,针对mask的size也需要复原

1
targets_mask_batch = targets_mask_batch.squeeze(1)  # transformer encoder在开始处理时mask增加了一维,这里需要复原

最后还有一点值得记录的是,在最初的训练过程中,尝试不使用CRF层,损失使用mean会导致一开始损失值就比较低,然后难以下降的问题,改成sum之后损失可以下降,召回率可以学习到90%多,但是准确率较低,所以增加CRF层后开始记录实验数据。

实验数据(自行实现的transformer作为编码器)

目前在训练第6行(第2、4行效果很差)

模型配置 precision recall f1 f2 weight test_special f1 f2 参数量
tiny 98.5% 98.5% 98.5% 89.9% 58.9MB 92.9% 76.3% 15,253,938
tiny(word2vec) 9,046,962
small 98.9% 98.9% 98.9% 92.0% 126MB 94.6% 79.7% 32,938,674
small(word2vec) 25,393,842
base 98.8% 98.8% 98.8% 91.2% 414MB 94.2% 79.7% 108,446,130
base(bert-pre) 79.1% 95.4% 86.5% 80.3% 414MB 68.4% 69.5% 108,446,130

以上表格记录的数据均是包含CRF层的结果(precision、recall和f1是根据标签的,f2是根据单条数据统计的(evaluate方法评估,只看accuracy值即可))

学习率为0.001,因为学习过程中学习率会先增大到设定的学习率,再逐渐减小到0,故设置的学习率为最大学习率,如果设置为bert类似的学习率2e-5,则无法有效学习。
上表第2行、第4行使用word2vec训练好的embedding作为整个模型的embedding层初始化参数,但是最终学习效果不佳,无论是尝试增大学习率到0.01还是减小学习率至2e-5均无法有效学习,所以证明word2vec的静态词向量在transformer上不起作用?

tiny使用的word2vec是60W条文本训练的,small使用的word2vec是200W条文本训练的
第6行,虽然使用了bert的预训练权重,但是实际上因为自定义的transformer架构,层的名称与bert并不一致,故只初始化了embedding层。

与BERT的区别

为了进一步对比上面自行实现的Transformer相比BERT的性能差异,所以在此又针对三种Bert大小进行了训练测试,先观察实验对比结果,然后再分析BERT中改进的点。

BERT各版本对比
| version | emb_size | feedforward_size |hidden_size|heads_num|layers_num|
| ——–| ——– | —————-| ———–| ——–| ———|
| tiny | 384 | 1536 | 384 | 6 | 3 |
| small | 512 | 2048 | 512 | 8 | 6 |
| base | 768 | 3072 | 768 | 12 | 12 |
| large | 1024 | 4096 | 1024 | 16 | 24 |

tiny、small BERT是无法使用预训练的权重的

我在的训练框架中同样集成了原始的BERT代码,之所以强调原始代码是因为BERT的预训练权重是跟模型每一层的名称相匹配的,如果层名称不一致会导致加载预训练权重时实际参数初始化失败,还是用的随机初始化的情况,我在将BERT集成到我的框架内部时也遇到了一些问题,主要包括以下方面:

  1. 关于attention_head_size参数设置的错误;
  2. 缩放点积部分的bug;(很重要)
  3. crf.decode未传入mask参数;(很重要)
  4. 优化器配置错误,过去一直用的SGD优化器(很重要)
  5. 优化器的correct_bias=FALSE,这个参数需要复制bert自行实现的AdamW优化器代码,pytorch内置的代码不包含此参数;
  6. LayerNorm的实现,使用transformer的自主实现版本,不使用pytorch内置版本;
  7. 从relu激活函数切换为gelu激活函数(这个与原始的transformer有区别)。
  8. 整个模型结构的self变量命名(相当于模型各个的层名称)需要与bert预训练权重的源代码保持完全一致,否则加载预训练权重时命名不一致的层的参数是没有初始化成功的。(很重要)

使用pytorch-nlp框架训练BERT,记录结果:
|模型配置 |learning_rate| precision |recall| F1 |F2 |test_special f1| test_special f2 |修正后的special F2|
| – | – | – | – | – | – | – | – | – |
|tiny |1.00E-04 |0.995 |0.995 |0.995 |0.961 |0.977 |0.847 |0.881|
|small |2.00E-05 |0.993| 0.992 |0.992 |0.942 |0.964| 0.814| 0.847|
|base |2.00E-05 |0.994 |0.993| 0.994| 0.955 |0.969 |0.831| 0.864|
|base(pre_train)| 2.00E-05| 0.999| 0.998| 0.999| 0.99| 0.99| 0.915| 0.949|

基本上与使用UER-py框架训练得到的结果一致,如果直接将UER-py中训练得到的模型放到pytorch-nlp中调用进行预测,准确率与在UER中预测的一致,也就是说在pytorch-nlp框架中实现的模型已经与UER中完全一致。

如果seq_length=512,batch_size=8的条件下,11G的显存跑不起来BERT-Base,所以设置seq_length=300(数据长度基本不会超过300)

目前针对真实标注集的最好F2值就是base bert基于预训练权重得到的,93.2%(其实说不定还有提升空间,因为还没有finetune)。

这篇文章主要是为了学习从头实现transformer然后将其用于NER任务的方法,只能算作是一个baseline,其实还有很多改进的方法可供学习和使用。