机器翻译——中法

机器翻译:编码器—解码器与注意力机制

机器翻译是一种将文本从一种语言自动翻译到另一种语言的技术。由于不同语言的文本序列长度不一致,编码器—解码器和注意力机制在机器翻译中得到了广泛应用。

1.1.读取和预处理数据

首先,定义一些特殊符号:

  • <pad>:用于填充较短序列,直到每个序列等长。
  • <bos>:序列开始符号。
  • <eos>:序列结束符号。
  • import collections
    import os
    import io
    import torch
    from torch import nn
    import torch.nn.functional as F
    import torchtext.vocab as Vocab
    import torch.utils.data as Data
    
    # 定义特殊符号
    PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'
    # 设置环境变量来设置使用哪个GPU,"0"表示第一个GPU
    os.environ["CUDA_VISIBLE_DEVICES"] = "0"
    # 如果GPU可用,使用GPU,否则使用CPU
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    # 打印当前PyTorch版本和使用的设备(CPU/GPU)
    print(torch.__version__, device)

    定义辅助函数对数据进行预处理:

    # 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
    # 长度变为max_seq_len,然后将序列保存在all_seqs中
    def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
        all_tokens.extend(seq_tokens)
        seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
        all_seqs.append(seq_tokens)
    
    # 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造Tensor
    def build_data(all_tokens, all_seqs):
        vocab = Vocab.Vocab(collections.Counter(all_tokens), specials=[PAD, BOS, EOS])
        indices = [[vocab.stoi[w] for w in seq] for seq in all_seqs]
        return vocab, torch.tensor(indices)

    读取法语—英语数据集,并创建词典:

    # 读取数据并进行预处理
    def read_data(max_seq_len):
        in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
        with io.open('fr-en-small.txt') as f:
            lines = f.readlines()
        for line in lines:
            in_seq, out_seq = line.rstrip().split('\t')
            in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
            if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
                continue  # 如果加上EOS后长于max_seq_len,则忽略掉此样本
            process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
            process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
        in_vocab, in_data = build_data(in_tokens, in_seqs)
        out_vocab, out_data = build_data(out_tokens, out_seqs)
        return in_vocab, out_vocab, Data.TensorDataset(in_data, out_data)
    
    # 设置最大序列长度,读取数据并展示第一个样本
    max_seq_len = 7
    in_vocab, out_vocab, dataset = read_data(max_seq_len)
    print(dataset[0])

    2.2含注意力机制的编码器—解码器

     

    2.2.1 编码器

     

    编码器将输入语言的词索引通过词嵌入层得到词的表征,然后输入到多层门控循环单元(GRU)中。

    # 定义编码器
    class Encoder(nn.Module):
        def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, drop_prob=0, **kwargs):
            super(Encoder, self).__init__(**kwargs)
            # 嵌入层
            self.embedding = nn.Embedding(vocab_size, embed_size)
            # GRU层
            self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=drop_prob)
        
        def forward(self, inputs, state):
            # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
            embedding = self.embedding(inputs.long()).permute(1, 0, 2) # (seq_len, batch, input_size)
            return self.rnn(embedding, state)
        
        def begin_state(self):
            # 初始化隐藏状态
            return None
    
    # 创建编码器实例并进行前向计算
    encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
    output, state = encoder(torch.zeros((4, 7)), encoder.begin_state())
    print(output.shape, state.shape)

    2.2.2注意力机制

     

    实现注意力机制:

    # 定义注意力机制模型
    def attention_model(input_size, attention_size):
        model = nn.Sequential(nn.Linear(input_size, attention_size, bias=False),
                              nn.Tanh(),
                              nn.Linear(attention_size, 1, bias=False))
        return model
    
    # 注意力机制的前向计算
    def attention_forward(model, enc_states, dec_state):
        """
        enc_states: (时间步数, 批量大小, 隐藏单元个数)
        dec_state: (批量大小, 隐藏单元个数)
        """
        # 将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结
        dec_states = dec_state.unsqueeze(dim=0).expand_as(enc_states)
        enc_and_dec_states = torch.cat((enc_states, dec_states), dim=2)
        e = model(enc_and_dec_states)  # 形状为(时间步数, 批量大小, 1)
        alpha = F.softmax(e, dim=0)  # 在时间步维度做softmax运算
        return (alpha * enc_states).sum(dim=0)  # 返回背景变量
    
    # 测试注意力机制
    seq_len, batch_size, num_hiddens = 10, 4, 8
    model = attention_model(2*num_hiddens, 10)
    enc_states = torch.zeros((seq_len, batch_size, num_hiddens))
    dec_state = torch.zeros((batch_size, num_hiddens))
    print(attention_forward(model, enc_states, dec_state).shape)

    2.2.3含注意力机制的解码器

    # 定义解码器
    class Decoder(nn.Module):
        def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, attention_size, drop_prob=0):
            super(Decoder, self).__init__()
            self.embedding = nn.Embedding(vocab_size, embed_size)
            self.attention = attention_model(2*num_hiddens, attention_size)
            # GRU的输入包含attention输出的c和实际输入, 所以尺寸是 num_hiddens+embed_size
            self.rnn = nn.GRU(num_hiddens + embed_size, num_hiddens, num_layers, dropout=drop_prob)
            self.out = nn.Linear(num_hiddens, vocab_size)
        
        def forward(self, cur_input, state, enc_states):
            """
            cur_input shape: (batch, )
            state shape: (num_layers, batch, num_hiddens)
            """
            # 使用注意力机制计算背景向量
            c = attention_forward(self.attention, enc_states, state[-1])
            # 将嵌入后的输入和背景向量在特征维连结, (批量大小, num_hiddens+embed_size)
            input_and_c = torch.cat((self.embedding(cur_input), c), dim=1) 
            # 为输入和背景向量的连结增加时间步维,时间步个数为1
            output, state = self.rnn(input_and_c.unsqueeze(0), state)
            # 移除时间步维,输出形状为(批量大小, 输出词典大小)
            output = self.out(output).squeeze(dim=0)
            return output, state
        
        def begin_state(self, enc_state):
            # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
            return enc_state

    2.2.3 训练模型

     

    定义损失函数和训练函数:

    # 定义计算批量损失的函数
    def batch_loss(encoder, decoder, X, Y, loss):
        batch_size = X.shape[0]
        enc_state = encoder.begin_state()
        enc_outputs, enc_state = encoder(X, enc_state)
        # 初始化解码器的隐藏状态
        dec_state = decoder.begin_state(enc_state)
        # 解码器在最初时间步的输入是BOS
        dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
            # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失, 初始全1
        mask, num_not_pad_tokens = torch.ones(batch_size,), 0
        l = torch.tensor([0.0])
        for y in Y.permute(1, 0):  # Y shape: (batch, seq_len)
            dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
            l = l + (mask * loss(dec_output, y)).sum()
            dec_input = y  # 使用强制教学
            num_not_pad_tokens += mask.sum().item()
            # EOS后面全是PAD. 下面一行保证一旦遇到EOS接下来的循环中mask就一直是0
            mask = mask * (y != out_vocab.stoi[EOS]).float()
        return l / num_not_pad_tokens
    
    # 定义训练函数
    def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
        enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
        dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)
        loss = nn.CrossEntropyLoss(reduction='none')
        data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
        for epoch in range(num_epochs):
            l_sum = 0.0
            for X, Y in data_iter:
                enc_optimizer.zero_grad()
                dec_optimizer.zero_grad()
                l = batch_loss(encoder, decoder, X, Y, loss)
                l.backward()
                enc_optimizer.step()
                dec_optimizer.step()
                l_sum += l.item()
            if (epoch + 1) % 10 == 0:
                print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))
    
    # 创建模型实例并设置超参数,然后训练模型
    embed_size, num_hiddens, num_layers = 64, 64, 2
    attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
    encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers, drop_prob)
    decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers, attention_size, drop_prob)
    train(encoder, decoder, dataset, lr, batch_size, num_epochs)

    2.2.4 预测不定长的序列

     

    实现贪婪搜索:

    # 定义翻译函数,使用贪婪搜索
    def translate(encoder, decoder, input_seq, max_seq_len):
        in_tokens = input_seq.split(' ')
        in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
        enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]])  # batch=1
        enc_state = encoder.begin_state()
        enc_output, enc_state = encoder(enc_input, enc_state)
        dec_input = torch.tensor([out_vocab.stoi[BOS]])
        dec_state = decoder.begin_state(enc_state)
        output_tokens = []
        for _ in range(max_seq_len):
            dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
            pred = dec_output.argmax(dim=1)
            pred_token = out_vocab.itos[int(pred.item())]
            if pred_token == EOS:  # 当任一时间步搜索出EOS时,输出序列即完成
                break
            else:
                output_tokens.append(pred_token)
                dec_input = pred
        return output_tokens
    
    # 测试翻译函数
    input_seq = 'ils regardent .'
    print(translate(encoder, decoder, input_seq, max_seq_len))

    3.评价翻译结果

     

    使用BLEU(Bilingual Evaluation Understudy)来评价机器翻译结果。BLEU考察模型预测序列中任意子序列是否出现在标签序列中。

     

    实现BLEU的计算:

    # 定义BLEU计算函数
    def bleu(pred_tokens, label_tokens, k):
        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
    
    # 定义辅助打印函数
    def score(input_seq, label_seq, k):
        pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
        label_tokens = label_seq.split(' ')
        print('bleu %.3f, predict: %s' % (bleu(pred_tokens, label_tokens, k), ' '.join(pred_tokens)))
    
    # 测试BLEU评分
    score('ils regardent .', 'they are watching .', k=2)
    score('ils sont canadienne .', 'they are canadian .', k=2)

    4.总结

  •  

    通过以上步骤,成功实现了一个简单的机器翻译模型,利用编码器—解码器和注意力机制将法语句子翻译成英语。此外,还使用BLEU评分来评价翻译结果的质量。尽管这个模型已经能够完成基本的翻译任务,但它仍有很大的扩展和优化空间,以便处理更大规模的数据集和更复杂的翻译任务。希望这篇博客能为你提供一些有价值的参考和启发,感谢您的阅读!

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值