机器学习笔记:seq2seq训练

写在前面

这节难度非常大,笔者刚刚学习完这一节,最后在”思考“处提及了一个疑惑点,如有了解seq2seq的大佬,希望能给予指点。

涉及许多预备知识,为了方便大家学习,我将预备知识直接写在这里。其中3-6和8,可以看我之前发的博客。

  1. 全连接层
  2. Softmax回归&交叉熵损失函数
  3. 循环神经网络
  4. 深层循环神经网络
  5. 门控循环单元
  6. 机器翻译数据集处理
  7. one-hot编码
  8. 编码器-解码器结构

目录

写在前面

损失函数

训练

简化代码

预测 

预测序列的评估

 训练结果输出


损失函数

使用掩蔽softmax函数作为损失函数,从而避免计算填充词元<pad>的损失值。下面构建掩蔽Softmax类作为损失函数使用。

pred为需要进行计算的序列,形状为(batch_size,num_steps,vocab_size),需要对pred进行掩蔽操作。label为经过计算后产生的值,形状为(batch_size,num_steps)。valid_len为序列实际有效长度,形状为(batch_size,)。

class MaskedSoftmaxCELoss(gluon.loss.SoftmaxCELoss):
    def forward(self, pred, label, valid_len):
        # weights的形状:(batch_size,num_steps,1)
        weights = np.expand_dims(np.ones_like(label), axis=-1)
        weights = npx.sequence_mask(weights, valid_len, True, axis=1)
        return super(MaskedSoftmaxCELoss, self).forward(pred, label, weights)

通过这个损失函数,我们重新分配计算权重,使得交叉熵损失函数计算时将有效长度之后的内容的权重值设置为0(不参与损失计算)。

训练

定义损失函数后,我们终于可以开始对模型进行训练了。这里还有一个问题需要注意。特定的序列开始词元(“<bos>”)和 原始的输出序列(不包括序列结束词元“<eos>”) 拼接在一起作为解码器的输入。 这被称为强制教学(teacher forcing)。

在下面所展示的代码中,bos即为开始词元,将它扩展为与批量大小一致,然后添加到Y的序列中。另外使用了梯度裁剪进行辅助,其余的内容与之前的训练并没有太大区别。

#@save
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    
    net.initialize(init.Xavier(), force_reinit=True, ctx=device)
    trainer = gluon.Trainer(net.collect_params(), 'adam',
                            {'learning_rate': lr})
    loss = MaskedSoftmaxCELoss()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # 训练损失求和,词元数量
        for batch in data_iter:
            X, X_valid_len, Y, Y_valid_len = [
                x.as_in_ctx(device) for x in batch]
            bos = np.array([tgt_vocab['<bos>']] * Y.shape[0],
                       ctx=device).reshape(-1, 1)
            dec_input = np.concatenate([bos, Y[:, :-1]], 1)  # 强制教学
            with autograd.record():
                Y_hat, _ = net(X, dec_input, X_valid_len)
                l = loss(Y_hat, Y, Y_valid_len)
            l.backward()
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            trainer.step(num_tokens)
            metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
        f'tokens/sec on {str(device)}')

简化代码

上面的代码过多,十分复杂,但有些是为展示方便的,比如Animator、Timer和Accumulator,我将非训练必要代码去除,再放上一个简化代码,方便阅读:

def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    net.initialize(init.Xavier(), force_reinit=True, ctx=device)
    trainer = gluon.Trainer(net.collect_params(), 'adam',{'learning_rate': lr})
    loss = MaskedSoftmaxCELoss()
    for epoch in range(num_epochs):
        for batch in data_iter:
            X, X_valid_len, Y, Y_valid_len = [x.as_in_ctx(device) for x in batch]
            bos = np.array([tgt_vocab['<bos>']] * Y.shape[0],ctx=device).reshape(-1, 1)
            dec_input = np.concatenate([bos, Y[:, :-1]], 1)
            with autograd.record():
                Y_hat, _ = net(X, dec_input, X_valid_len)
                l = loss(Y_hat, Y, Y_valid_len)
            l.backward()
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            trainer.step(num_tokens)

现在,在机器翻译数据集上,我们可以 创建和训练一个循环神经网络“编码器-解码器”模型用于序列到序列的学习。

下面分别定义了嵌入层大小(将词表大小的输入转换为一定数量的特征向量),隐藏层大小(关系到每个GRU的精度),循环神经网络层数(使得RNN成为一个深层RNN),裁剪梯度(优化梯度消失和梯度爆炸),批量大小时间步大小(规定序列的实际长度,超出或不足进行截断-填充操作),学习率学习轮次(在数据集较小时可以提高训练精度),计算设备(GPU运算)。

embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,
                        dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
                        dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
loss 0.024, 5031.7 tokens/sec on gpu(0)

预测 

为了采用一个接着一个词元的方式预测输出序列, 每个解码器当前时间步的输入都将来自于前一时间步的预测词元。 与训练类似,序列开始词元(“<bos>”) 在初始时间步被输入到解码器中。 该预测过程如图所示, 当输出序列的预测遇到序列结束词元(“<eos>”)时,预测就结束了。

def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device):
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['<eos>']]
    enc_valid_len = np.array([len(src_tokens)], ctx=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    enc_X = np.expand_dims(np.array(src_tokens, ctx=device), axis=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    dec_X = np.expand_dims(np.array([tgt_vocab['<bos>']], ctx=device),axis=0)
    output_seq= []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        dec_X = Y.argmax(axis=2)
        pred = dec_X.squeeze(axis=0).astype('int32').item()
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq))

预测序列的评估

在评估预测序列的质量时,我们使用BLEU进行评估(本片篇幅已经很长了,所以BLEU的介绍后面的文章再单独写),BLEU定义为:

e^{Min(0,1-\frac{len_{label}}{len_{pred}})}\prod_{n=1}^{k}p_n^{\frac{1}{2^n}}

def bleu(pred_seq, label_seq, k):
    pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[' '.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score

 训练结果输出

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation= predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')
go . => entre ., bleu 0.000
i lost . => j'ai gagné ., bleu 0.000
he's calm . => j'ai gagné ., bleu 0.000
i'm home . => je suis chez moi <unk> !, bleu 0.719s

思考 

在predict_seq2seq()中,有效长度这一变量被输入到encoder和decoder的前向计算当中,但是在encoder和decoder的前向计算定义中,只需要一个必须的输入X,而另一个参数则是*args,在这里,是否这个参数是多余的呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值