从今天开始,我会再看一遍Transformer(这是第3遍了吧……)。
这次是依据Transformer 模型的 PyTorch 实现进行学习,再梳理一下Transformer模型的重点,最后用Pytorch实现。
本来想用AllenNLP一步到位,但是前天敲了一天发现不行,我对Pytorch不懂,同时还是不了AllenNLP,干脆从头再来。
在这里参考The Annotated Transformer进行实现。
第一节完成各个模块的编写第二节完成模型的组成- 第三节运行训练和预测
在前面,已经将模型完整构建好了,但是如何才能用起来?只有一个模型是不够的,接下来就是如何训练模型,并进行简单的测试。
9. 训练模型
这一节主要是处理模型训练的细节
在这里教程快速介绍训练标准Encoder和Decoder模型所需的一些工具。
构造批处理对象Batches和Masking
这一步包含用于训练的src和目标句子,以及构造掩码。
class Batch:
def __init__(self, src, trg=None, pad=0):
self.src = src
self.src_mask = (src != pad).unsqueeze(-2)
if trg is not None:
self.trg = trg[:, :-1]
self.trg_y = trg[:, 1:]
self.trg_mask = self.make_std_mask(self.trg, pad)
self.ntokens = (self.trg_y != pad).data.sum()
@staticmethod
def make_std_mask(tgt, pad):
"构建一个mask用于隐藏padding和未来的词"
tgt_mask = (tgt != pad).unsqueeze(-2)
tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))
return tgt_mask
构造一个训练循环
接下来,我们创建一个通用的训练和评分功能来跟踪损失。我们传入一个通用的损失计算函数,它也处理参数更新。
def run_epoch(data_iter, model, loss_compute):
start = time.time()
total_tokens = 0
total_loss = 0
tokens = 0
for i, batch in enumerate(data_iter):
out = model.forward(batch.src, batch.trg, batch.src_mask, batch.trg_mask)
loss = loss_compute(out, batch.trg_y, batch.ntokens)
total_loss += loss
total_tokens += batch.ntokens
tokens += batch.ntokens
if i % 50 == 1:
elapsed = time.time() - start
print("Epoch Step: %d Loss: %f Tokens per Sec: %f" % (i, loss / batch.ntokens, tokens / elapsed))
start = time.time()
tokens = 0
return total_loss / total_tokens
训练数据和批处理
基于WMT2014英语-德语数据集进行训练,其中包括了450w对句子。句子已经对齐,由37000个tokens。
在英语-法语方面,使用更大的WMT2014英语-发育数据集,包含有36M个句子和32000个tokens
句子对按照近似的序列长度进行批处理。每个训练批包含一组句子对,其包含大约25000个源tokens和25000个目标tokens。
global max_src_in_batch, max_tgt_in_batch
def batch_size_fn(new, count, sofar):
"Keep augmenting batch and calculate total number of tokens + padding."
global max_src_in_batch, max_tgt_in_batch
if count == 1:
max_src_in_batch = 0
max_tgt_in_batch = 0
max_src_in_batch = max(max_src_in_batch, len(new.src))
max_tgt_in_batch = max(max_tgt_in_batch, len(new.trg) + 2)
src_elements = count * max_src_in_batch
tgt_elements = count * max_tgt_in_batch
return max(src_elements, tgt_elements)
硬件和计划
教程是在8张英伟达P100的GPU上训练的。
对于使用本文所述超参数的基本模型,每个训练步骤大约需要0.4秒。我们对基础模型进行了总共100,000步即12小时的训练。
对于我们的大型模型,步进时间为1.0秒。大型模型经过300,000步(3.5天)的训练。
Optimizer调优
我们使用Adam优化器,参数β1= 0.9,β2= 0.98,ε= 10-9。
我们根据以下公式改变训练过程中的学习率:
这对应于第一次升级训练步骤线性增加学习率,然后,与步数的倒数平方根成比例地减小它。我们使用了。
class NoamOpt:
"Optim wrapper that implements rate."
def __init__(self, model_size, factor, warmup, optimizer):
self.optimizer = optimizer
self._step = 0
self.warmup = warmup
self.factor = factor
self.model_size = model_size
self._rate = 0
def step(self):
"Update parameters and rate"
self._step += 1
rate = self.rate()
for p in self.optimizer.param_groups:
p['lr'] = rate
self._rate = rate
self.optimizer.step()
def rate(self, step=None):
"Implement `lrate` above"
if step is None:
step = self._step
return self.factor * \
(self.model_size ** (-0.5) *
min(step ** (-0.5), step * self.warmup ** (-1.5)))
def get_std_opt(model):
return NoamOpt(model.src_embed[0].d_model, 2, 4000,
torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))
10. 模型的正则化Regularization
机器学习的样本中通常会存在少量错误标签,这些错误标签会影响到预测的效果。标签平滑采用如下思路解决这个问题:在训练时即假设标签可能存在错误,避免“过分”相信训练样本的标签。当目标函数为交叉熵时,这一思想有非常简单的实现,称为标签平滑(Label Smoothing)
作者:cxsmarkchan
链接:https://juejin.im/post/5a29fd4051882534af25dc92
来源:掘金
在训练中,引入标签平滑值。这会伤害困惑,因为模型的学习更加不确定,但提高了准确性和BLEU分数。
以1-ε的概率将(xi,yi)代入训练,以ε的概率将(xi,1-yi)代入训练。 这样,模型在训练时,既有正确标签输入,又有错误标签输入,可以想象,如此训练出来的模型不会“全力匹配”每一个标签,而只是在一定程度上匹配。这样,如果真的出现错误标签,模型受到的影响就会更小。
作者:cxsmarkchan
链接:https://juejin.im/post/5a29fd4051882534af25dc92
来源:掘金
在这里教程使用KL div loss实现标签平滑。我们不使用独热目标分布,而是创建一个分布,该分布对正确的单词和整个词汇表中分布的平滑质量的其余部分具有信心。
class LabelSmoothing(nn.Module):
"Implement label smoothing."
def __init__(self, size, padding_idx, smoothing=0.0):
super(LabelSmoothing, self).__init__()
self.criterion = nn.KLDivLoss(size_average=False)
self.padding_idx = padding_idx
self.confidence = 1.0 - smoothing
self.smoothing = smoothing
self.size = size
self.true_dist = None
def forward(self, x, target):
assert x.size(1) == self.size
true_dist = x.data.clone()
true_dist.fill_(self.smoothing / (self.size - 2))
true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
true_dist[:, self.padding_idx] = 0
mask = torch.nonzero(target.data == self.padding_idx)
if mask.dim() > 0:
true_dist.index_fill_(0, mask.squeeze(), 0.0)
self.true_dist = true_dist
return self.criterion(x, Variable(true_dist, requires_grad=False))
11. 记忆优化Memory Optimization
在原文中是没有这部分,教程里直接跳到测试阶段,但是我在它提供的colab中看到这部分。还是按照教程里的走。
12. 第一个例子
在这个例子中,实验一个简单的复制任务。
给定一个来自小词汇表的随机输入符号集,目标是生成那些相同的符号。
合成数据
def data_gen(V, batch, nbatches):
'''
为src-tgt复制任务生成随机数据
:param V:
:param batch:
:param nbatches:
:return:
'''
for i in range(nbatches):
data = torch.from_numpy(np.random.randint(1, V, size=(batch, 10)))
data[:, 0] = 1
src = Variable(data, requires_grad=False)
tgt = Variable(data, requires_grad=False)
yield Batch(src, tgt, 0)
损失计算
计算训练方法的损失
class SimpleLossCompute:
def __init__(self, generator, criterion, opt=None):
self.generator = generator
self.criterion = criterion
self.opt = opt
def __call__(self, x, y, norm):
x = self.generator(x)
loss = self.criterion(x.contiguous().view(-1, x.size(-1)),
y.contiguous().view(-1)) / norm.float()
loss.backward()
if self.opt is not None:
self.opt.step()
self.opt.optimizer.zero_grad()
return loss.item() * norm.float().item()
在最后的验证中,因为一些原因,我的环境里报错了,在今天的更新里有写。先把整个流程跑完。
if __name__ == '__main__':
V = 10
criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
print(criterion)
model = make_model(V, V, N=2)
model_opt = NoamOpt(model.src_embed[0].d_model, 1, 400,
torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))
print(model)
print(model_opt)
for epoch in range(10):
model.train()
run_epoch(data_gen(V, 30, 20), model, SimpleLossCompute(model.generator, criterion, model_opt))
model.eval()
print(run_epoch(data_gen(V, 30, 5), model, SimpleLossCompute(model.generator, criterion, None)))