编码器-解码器(seq2seq)

文章目录

在自然语言处理的许多应用中,输入和输出都可以是无限长的序列。在机器翻译的情况下,输入可以是可变长度的英文文本序列,输出可以是可变长度的法语文本序列,例如:

[En]

In many applications of natural language processing, both input and output can be indefinite length sequences. In the case of machine translation, the input can be a sequence of English text with variable length, and the output can be a sequence of French text with variable length, for example:

英语输入:”They” “are” “watching” “.”

法语输出:”Ils” “regardent” “.”

当输入和输出都是不定长序列时,可以使用编码器-解码器(encoder-decoder)架构或seq2seq模型。这两个都由两部分组成,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。

对于编码器-解码器(encoder-decoder)架构和seq2seq模型有啥联系和区别呢?
我的理解是,编码器-解码器更像是一个模型的抽象,一种大的架构,而seq2seq模型则是使用这种架构进行机器翻译的一个模型。因为现在比较火的transformer也是使用了编码器-解码器架构,因此我觉得编码器-解码器是一种抽象的结构,而seq2seq和transformer都是使用这种架构搭建用于具体任务的具体模型。像用于图像处理的CNN也可以看成一个编码器-解码器架构。

1、编码器解码器架构

1.1、概念

在CNN中

在卷积神经网络中,图像首先通过卷积层,然后通过线性层,最后输出分类结果。卷积层用于特征提取,线性层用于结果预测。从另一个角度来看,特征提取可以看作是编码器,它将原始图像编码成有利于机器学习的中间表达式,而解码器将中间表示转换为另一种表达式。

[En]

In the convolution neural network, the picture first passes through the convolution layer, then through the linear layer, and finally outputs the classification result. The convolution layer is used for feature extraction and the linear layer is used for result prediction. From another point of view, feature extraction can be regarded as an encoder, which encodes the original image into an intermediate expression that is beneficial to machine learning, while the decoder converts the intermediate representation into another expression.

  • 编码器:将输入编程为中间表达式功能。
    [En]

    Encoder: program the input into intermediate expression features.*

  • Decoder:将中间表示形式解码成输出。
    [En]

    Decoder: decode the intermediate representation into output.*

编码器-解码器(seq2seq)

在RNN中

RNN同样也可以看成一个编码器-解码器结构,编码器将文本编码成向量,而解码器将向量解码成我们想要的输出。

  • 编码器:将文本表示为向量。
    [En]

    Encoder: represents text as a vector.*

  • Decoder:将向量表示为输出。
    [En]

    Decoder: represents the vector as output.*

编码器-解码器(seq2seq)

架构抽象

因此我么就可以把编码器-解码器抽象成一个框架。一个模型就可以分成两块,一部分叫做编码器,另一部分叫做解码器。编码器将输入编码成一个中间状态(或者中间语义表示),解码器将这些中间状态进行处理然后输出,当然解码器也可以获取输入与中间状态一起生成输出。编码器和解码器也可以由好多个编码器解码器组成,即由多层组成。其中编码器可以是各种网络,如线性网络,CNN,RNN,亦或是像transformer编码器那种复杂的结构;解码器与编码器也可以是一样的网络,也可以是不同的网络。

编码器-解码器(seq2seq)

seq2seq中的编码器解码器

后面介绍的seq2seq用到的也是这种架构,seq2seq实现的功能是将一个序列转换成另一个序列。以英文翻译为法语为例,seq2seq中的编码器就是对输入的英语句子进行处理,将输入的英文句子X X X进行编码,将输入句子通过非线性变换转化成中间语义表示C:
C = f ( x 1 , x 2 , ⋯ , x m ) C=f(x_1,x_2,\cdots ,x_m)C =f (x 1 ​,x 2 ​,⋯,x m ​)
而对于解码器是根据中间语义表示C和之前已经生成的历史信息y 1 , y 2 , y 3 , ⋯ , y i − 1 y_1,y_2,y_3,\cdots ,y_{i-1}y 1 ​,y 2 ​,y 3 ​,⋯,y i −1 ​来生成i时刻要生成的法语单词y i y_i y i ​:
y i = g ( C , y 1 , y 2 , y 3 , ⋯ , y i − 1 ) y_i = g(C,y_1,y_2,y_3,\cdots ,y_{i-1})y i ​=g (C ,y 1 ​,y 2 ​,y 3 ​,⋯,y i −1 ​)
每个y i y_i y i ​依次生成,就将英文句子X X X翻译成了法语句子Y Y Y

编码器-解码器模型应用十分的广泛,其应用场景也很多,比如对于机器翻译来说,< X , Y > 就是对应的不同语言的句子;对于文本摘要来说,X X X为一篇文章,Y Y Y就是对应的摘要;对于问答系统来说,X X X为问题,Y Y Y是给出的回答。

; 1.2、代码

因为编码器-解码器是一种抽象的结构,因此它没有具体的实现,只有使用这种结构用于具体的业务中时才有具体实现,因此下面看看编码器-解码器的相应的抽象接口。

1.2.1、编码器(Encoder)

以下是编码器的接口实现,使用指定可变长度的序列作为输入到编码器,然后得到中间语义编码。

[En]

The following is the interface implementation of the encoder, using a sequence of specified variable length as the input to the encoder, and then get the intermediate semantic coding.

from torch import nn

class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()

    def forward(self,X,*args):

        raise NotImplementedError

1.2.2、解码器(Decoder)

在解码器结构中,新增一个 init_state函数,用于将编码器的输出(enc_output)转换解码器输入的状态。解码器中有时可能需要额外的输入。解码器在每个时间步都会将输入(前一个时间步生成的词元)和编码后的中间状态解码成当前时间步的输出词元。


class Decoder(nn.Module):
    def __init__(self,**kwargs):
        super(Decoder, self).__init__()

    def init_state(self,enc_outputs,*args):
        raise NotImplementedError

    def forward(self,X,state):
        raise NotImplementedError

1.2.3、合并编码器和解码器

该编码器和解码器包含一个编码器和一个解码器。在前向传播期间,编码器的输出被用来生成中间编码状态,该中间编码状态被解码器用作其输入的一部分。

[En]

The encoder and decoder contain an encoder and a decoder. During forward propagation, the output of the encoder is used to generate an intermediate coding state, which is used by the decoder as part of its input.


class EncoderDecoder(nn.Module):
    def __init__(self,encoder,decoder,**kwargs):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder

    def forward(self,enc_X,dec_X,*args):
        enc_outputs = self.encoder(enc_X,*args)
        dec_state = self.decoder.init_state(enc_outputs,*args)
        return self.decoder(dec_X,dec_state)

2、seq2seq模型

按照我的理解,seq2seq模型就是编码器-解码器抽象架构的一种具体实现,它主要用来做机器翻译的任务(也可以用于其他任务),包含了一个编码器和一个解码器。上一节对其进行了一个简单的描述,下面详细介绍seq2seq模型,并使用它实现一个机器翻译的任务。

下图为使用seq2seq模型将英文翻译成法语句子的过程图。在训练过程中,为每个句子后附加上特殊符号< e o s > ( e n d o f s e q u e n c e )
(e n d o f s e q u e n c e )表示序列的终止,在解码器的最初时间步用一个表示序列开始的特殊符号< b o s > ( b e g i n i n g o f s e q u e n c e ) (b e g i n i n g o f s e q u e n c e )。

  • 编码器使用RNN处理输入信息,把最终时间步的隐藏状态作为输入信息的表征或编码信息。编码器由于能够看到句子的全部,因此编码器可以是双向的。
  • 解码器也同样是一个RNN,它在各个时间步中使用输入句子的编码信息(编码器的输出)和解码器上个时间步输出合并作为输入,用来预测当前时间步的输出。由于解码器不能看到句子的全部,因此它不能是双向的。

编码器-解码器(seq2seq)

; 2.1、编码器

编码器的作用是将长度可变的输入序列转换成一个定长的中间语义表示c c c,可以用循环神经网络设计编码器。

考虑由一个序列组成的样本(批量大小为1)。假设输入序列是x 1 , ⋯ , x T x_1,\cdots ,x_T x 1 ​,⋯,x T ​,其中x t x_t x t ​是输入文本序列中的第t t t个词元。在时间步t t t,循环神经网络将词元x t x_t x t ​的输入特征向量x t x_t x t ​和上一时间步的隐状态h t − 1 h_{t-1}h t −1 ​,计算当前时间步的隐状态h t h_t h t ​。使用函数f f f表示循环神经网络的循环层所做的变换:
h t = f ( x t , h t − 1 ) h_t=f(x_t,h_{t-1})h t ​=f (x t ​,h t −1 ​)
而编码器所生成的中间语义表示c c c为最后一个时间步T T T的隐藏状态, 这里最后一个时间步隐藏状态的输出为所有时间步隐藏状态的拼接,即:
c = h T c=h_T c =h T ​

2.2、解码器

对于来自训练数据集的输出序列y 1 , y 2 , ⋯ , y T y_1,y_2,\cdots ,y_T y 1 ​,y 2 ​,⋯,y T ​,对于每个时间步t t t,解码器的输出y t y_t y t ​的概率取决于先前的输出子序列y 1 , y 2 , ⋯ , y t − 1 y_1,y_2,\cdots ,y_{t-1}y 1 ​,y 2 ​,⋯,y t −1 ​和中间语义表示c c c,即:
P ( y t ∣ y 1 , ⋯ , y t − 1 , c ) P(y_t|y_1,\cdots ,y_{t-1},c)P (y t ​∣y 1 ​,⋯,y t −1 ​,c )
为此,可以使用另一个循环神经网络作为解码器。在输出序列的时间步t t t,解码器将上一时间步的输出y t − 1 y_{t-1}y t −1 ​以及中间语义表示c c c作为输入,并将它们与上一时间步的隐藏状态s t − 1 s_{t-1}s t −1 ​变换成当前时间步的隐藏状态s t s_t s t ​。用函数g表示解码器隐藏层的变换:
s t = g ( y t − 1 , c , s t − 1 ) s_t = g(y_{t-1},c,s_{t-1})s t ​=g (y t −1 ​,c ,s t −1 ​)
在获得解码器的隐状态之后,就可以使用输出层和softmax操作来计算在时间步t t t的输出y t y_t y t ​和y t y_t y t ​的条件概率分布P ( y t ∣ y 1 , ⋯ , y t − 1 , c ) P(y_t|y_1,\cdots ,y_{t-1},c)P (y t ​∣y 1 ​,⋯,y t −1 ​,c )。

2.3、编码器-解码器细节

编码器是没有输出的RNN,它只需要把最后一层最后一个时间节点的隐藏状态输出给解码器就好了。而解码器RNN的初始隐藏状态就是编码器输出的中间语义表示,而在解码器的每个时间步都使用了编码器输出的中间语义表示,中间语义表示与解码器的输入(上一个时间步的输出)一起组成当前时间步的输入,与上一时间步的隐状态一起计算当前时间步的输出。

当循环神经网络是多层的时候,解码器的初始状态为编码器最后一个时间步所有层的输出。而解码器每一个时间步的输入则是编码器最后一层最后一个时间步的输出与解码器上一时间输出进行的合并。

编码器-解码器(seq2seq)

; 2.4、训练&推理

seqseq模型在训练和推理的过程中是不一样的。编码器部分是一样的,因为都能看到所有的句子,不同的地方是在解码器部分。

  • 我们在训练中知道正确的句子是什么,所以无论解码器前一个时间步的输出是否正确,当前时间步的输入始终是正确的输入。
    [En]

    We know what the correct sentence is during training, so no matter whether the output of the previous time step of the decoder is correct or not, the input of the current time step is always the correct input. *

  • 推理过程的目的是预测整个句子,所以我们不知道正确的句子是什么,所以解码器当前时间步长的输入是前一个时间步长的输出,然后依次预测。
    [En]

    the purpose of the reasoning process is to predict the whole sentence, so we don’t know what the correct sentence is, so the input of the current time step of the decoder is the output of the previous time step, and then predict it in turn.*

编码器-解码器(seq2seq)

2.5 评价指标-BLEU

通过预测序列与真实标签序列进行比较来评估预测序列,BLEU(Bilingual Evaluation Understudy)不仅可以用于评估机器翻译的结果,它还可以用于测量许多应用输出序列的质量。

对于预测序列中的任意n n n元语法(n − g r a m s n-grams n −g r a m s),BLEU的评估是这个n n n元语法是否出现在标签序列中。

用p n p_n p n ​表示n n n元语法的精度,它是两个数量的比值,第一个数是预测序列与标签序列中匹配的n n n元语法的数量,第二个是 预测序列中n n n元语法的数量。通过一个例子,给定标签序列A 、 B 、 C 、 D 、 E 、 F A、B、C、D、E、F A 、B 、C 、D 、E 、F和预测序列A 、 B 、 B 、 C 、 D A、B、B、C、D A 、B 、B 、C 、D。对于一元语法,预测序列中一元语法的数量为5,而预测序列与标签序列中匹配的n元语法的数量为4,则p 1 = 4 5 p_1 = \frac{4}{5}p 1 ​=5 4 ​,依次类推p 2 = 3 4 p_2=\frac{3}{4}p 2 ​=4 3 ​,p 3 = 1 3 p_3=\frac{1}{3}p 3 ​=3 1 ​,p 4 = 0 p_4=0 p 4 ​=0。

l e n l a b e l len_{label}l e n l a b e l ​和l e n p r e d len_{pred}l e n p r e d ​分别为标签序列中的词元数和预测序列中的词元数,下面公式为BLEU的定义:
e x p ( m i n ( 0 , 1 − l e n l a b e l l e n p r e d ) ) ∏ n = 1 k p n 1 / 2 n exp(min(0,1-\frac{len_{label}}{len_{pred}}))\prod_{n=1}^{k}p_n^{1/2^n}e x p (m i n (0 ,1 −l e n p r e d ​l e n l a b e l ​​))n =1 ∏k ​p n 1 /2 n ​
因为在预测过程中,对较短的句子比较容易预测,而对于比较长的句子不容易预测,因此在BLEU中需要对过短的预测添加惩罚项,对长的匹配分配高的权重:

  • 其中如e x p ( m i n ( 0 , 1 − l e n l a b e l l e n p r e d ) ) exp(min(0,1-\frac{len_{label}}{len_{pred}}))e x p (m i n (0 ,1 −l e n p r e d ​l e n l a b e l ​​))为惩罚过短的预测,当标签序列大于预测序列的时候,该项的值会很小。例如当k = 2 k=2 k =2时,给定标签序列A 、 B 、 C 、 D 、 E 、 F A、B、C、D、E、F A 、B 、C 、D 、E 、F和预测序列A 、 B A、B A 、B,尽管p 1 = p 2 = 1 p_1=p_2=1 p 1 ​=p 2 ​=1,乘法因子exp(1-6/2)=0.14会降低BLEU值。
  • n n n元语法的1 / 2 n 1/2^n 1 /2 n方是为了对长匹配有更高权重,当长度n越大时,1 / 2 n 1/2^n 1 /2 n会越小,则p n 1 / 2 n p_n^{1/2^n}p n 1 /2 n ​会越大。

BLEU的值越大,预测的结果就越好(最大值为1),当预测序列与标签序列完全相同的时候,BLUE的值为1。

3、机器翻译代码

下面使用使用Pytorch实现上面介绍的模型,并使用”英-法”数据集对模型进行训练,下面主要介绍以下两部分

  • 加载并处理”英-法”数据集
  • 搭建seq2seq模型并使用数据集进行训练。

3.1、机器翻译数据集

3.1.1、下载和预处理数据集

下载有Tatoeba项目的双语句子对中的”英-法”数据集,数据集的每一行都是制表符分割的文本序列对,序列对由英文文本序列和翻译后的法语文本序列组成。每个文本序列可以是一个句子,也可以是包含多个句子的一个段落。

import os
import torch
import collections
from torch.utils import data

def read_data_nmt():
    with open('fra-eng/fra.txt', 'r',encoding='utf-8') as f:
        return f.read()

raw_text = read_data_nmt()
print(raw_text[:75])
Go. Va !
Hi. Salut !
Run!    Cours&#x202F;!

Run!    Courez&#x202F;!

Who?    Qui ?
Wow!    &#xC7;a alors&#x202F;!

在读取数据集之后,数据将在下面进行预处理。有以下处理步骤:

[En]

After reading the dataset, the data is preprocessed below. There are the following processing steps:

  • 首先使用空格代替不间断(连续)空格。
  • 将大写字符替换为小写字符。
    [En]

    replace uppercase characters with lowercase characters.*

  • 在单词和标点符号之间插入空格。
    [En]

    insert a space between a word and punctuation.*

def preprocess_nmt(text):    def no_space(char,prev_char):        return char in set(',.!?') and prev_char != ' '    text = text.replace('\u202f',' ').replace('\xa0',' ').lower()    out = [' ' + char if i > 0 and no_space(char,text[i - 1]) else char for i,char in enumerate(text)]    return ''.join(out)text = preprocess_nmt(raw_text)print(text[:80])
go .    va !hi .    salut !run !   cours !run !   courez !who ?   qui ?wow !   &#xE7;a alors !

3.1.2、词元化

下面对数据进行词元化,文本序列中每个词元(tokenize)要么是一个词,要么是一个标点符号。此函数返回两个词元列表source和target:source[i]是第i个文本序列的词元列表,target[i]是目标语言的第i个序列的词元列表。


def tokenize_nmt(text,num_examples = None):
    source,target = [],[]

    for i,line in enumerate(text.split('\n')):

        if num_examples and i > num_examples:
            break

        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' '))
            target.append(parts[1].split(' '))
    return source,target

source,target = tokenize_nmt(text)
source[:6],target[:6]
([['go', '.'],
  ['hi', '.'],
  ['run', '!'],
  ['run', '!'],
  ['who', '?'],
  ['wow', '!']],
 [['va', '!'],
  ['salut', '!'],
  ['cours', '!'],
  ['courez', '!'],
  ['qui', '?'],
  ['&#xE7;a', 'alors', '!']])

3.1.3、词表

下面是为源语言和目标语言建立两个词汇表。当使用词级语素化时,词汇量明显大于字符集级别的词汇量。为了缓解这个问题,出现两次以下的低频词被视为未知词。

[En]

The following is to build two vocabularies for the source language and the target language. When word-level morphemization is used, the size of the vocabulary is significantly larger than that at the character set level. To alleviate this problem, low-frequency words that appear less than twice are regarded as unknown (“

词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。下面,构建一个字典,通常也叫作 词表,用来将字符串类型的词元映射到从0开始的数字索引中。 我理解词表就是一个字典,可以通过索引查词元或者词元查索引。

下面的函数统计单词的出现频率,即单词在训练集中出现的次数,最后以列表的形式返回。

[En]

The following function counts the frequency of a word, that is, the number of times a word appears in the training set, and finally returns in the form of a list.


def count_corpus(tokens):

    if len(tokens) == 0 or isinstance(tokens[0], list):

        tokens = [token for line in tokens for token in line]

    return collections.Counter(tokens)
corpus_num=count_corpus(source)
print(corpus_num['the'])
33263

下面定义的Vocab类,用于构建词表,并支持查询词表长度、通过索引查询词元、词元查询索引的功能


class Vocab:
    def __init__(self,tokens = None, min_freq = 0, reserved_tokens = None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []

        counter = count_corpus(tokens)

        self._token_freqs = sorted(counter.items(),key = lambda x:x[1],reverse = True)

        self.idx_to_token = [''] + reserved_tokens

        self.token_to_idx = {token:idx for idx,token in enumerate(self.idx_to_token)}

        for token, freq in self._token_freqs:

            if freq < min_freq:
                break

            if token not in self.token_to_idx:
                self.idx_to_token.append(token)

                self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens,(list,tuple)):

            return self.token_to_idx.get(tokens,self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self,indices):
        if not  isinstance(indices,(list,tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]
    @property
    def unk(self):
        return 0
    @property
    def token_freqs(self):
        return self._token_freqs

测试一下源语言的词表

src_vocab = Vocab(source, min_freq=2,reserved_tokens=['', '', ''])

print(list(src_vocab.token_to_idx.items())[:10])

print(source[0])
print(src_vocab[source[0]])
[('<unk>', 0), ('<pad>', 1), ('<bos>', 2), ('<eos>', 3), ('.', 4), ('i', 5), ('you', 6), ('to', 7), ('the', 8), ('?', 9)]
['go', '.']
[47, 4]
</eos></bos></pad></unk>

3.1.4、截断和填充文本序列

语言模型中的序列样本具有固定的长度,该长度由时间步数(字数)指定。在机器翻译中,源文本序列和目标文本序列的长度不同。为了提高学习效率,对相同长度的序列进行截断和填充处理。

[En]

The sequence samples in the language model have a fixed length, which is specified by the number of time steps (the number of words). In machine translation, the source and target text sequences have different lengths. In order to improve the learning efficiency, truncation and filling are used to process the sequences with the same length.

假设同一个小批量每个序列有应该有相同的长度 num_steps,那么如果文本序列的词元数目小于 num_steps时,将在其末尾添加特定的”

为什么序列长度需要一样?
同一个batch在处理的时候都是处理相同的时间步,因此序列的长度一定得匹配,不然没办法进行训练。

def truncate_pad(line,num_steps,padding_token):
    if len(line) > num_steps:
        return line[:num_steps]
    return line + [padding_token] * (num_steps - len(line))

truncate_pad(src_vocab[source[0]], 10, src_vocab[''])
[47, 4, 1, 1, 1, 1, 1, 1, 1, 1]

下面的函数将词汇序列转换为索引序列,然后在序列的末尾添加结束标志/相应的索引,这样当预测生成时/它表示序列输出的结束,最后返回填充和截断操作后的序列和填充和截断操作后的序列长度。

[En]

The following function converts the lexical sequence into an index sequence, and then adds an end flag / corresponding index at the end of the sequence, so that when the prediction is generated / it represents the end of the sequence output, and finally returns the sequence after the fill and truncation operation and the sequence length after the fill and truncation operation.

def build_array_nmt(lines,vocab,num_steps):

    lines = [vocab[l] for l in lines]

    lines = [l + [vocab['']] for l in lines]
    array = torch.tensor([truncate_pad(l,num_steps,vocab['']) for l in lines])
    valid_len = (array != vocab['']).type(torch.int32).sum(1)

    return array,valid_len
build_array_nmt(source, src_vocab,1000)
(tensor([[  47,    4,    3,  ...,    1,    1,    1],
         [2944,    4,    3,  ...,    1,    1,    1],
         [ 435,  126,    3,  ...,    1,    1,    1],
         ...,
         [ 381,   60,   26,  ...,    1,    1,    1],
         [  66,  295,   90,  ...,    1,    1,    1],
         [  17,  176,   32,  ...,    1,    1,    1]]),
 tensor([ 3,  3,  3,  ..., 47, 49, 52]))

3.1.5、构建数据迭代器

最后,定义一个函数来返回数据迭代器,以及源语言和目标语言的两个词汇表。

[En]

Finally, define a function to return the data iterator, as well as two vocabularies for the source and target languages.


def load_array(data_arrays,batch_size,is_train=True):
    dataset=data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset,batch_size,shuffle=is_train)

def load_data_nmt(batch_size,num_steps,num_examples = 600):

    text = preprocess_nmt(read_data_nmt())

    source,target = tokenize_nmt(text,num_examples)

    src_vocab = Vocab(source,min_freq=2,reserved_tokens=['','',''])
    tgt_vocab = Vocab(target,min_freq=2,reserved_tokens=['','',''])

    src_array,src_valid_len = build_array_nmt(source,src_vocab,num_steps)
    tgt_array,tgt_valid_len = build_array_nmt(target,tgt_vocab,num_steps)

    data_arrays = (src_array,src_valid_len,tgt_array,tgt_valid_len)
    data_iter = load_array(data_arrays,batch_size)
    return data_iter,src_vocab,tgt_vocab

下面读出”英语-法语”数据集中的第一个小批量数据。

train_iter,src_vocab,tgt_vocab = load_data_nmt(batch_size=2,num_steps=8)

for X,X_valid_len,Y,Y_valid_len in train_iter:
    print('X:',X.type(torch.int32))
    print('X有效长度为:',X_valid_len)
    print('Y:', Y.type(torch.int32))
    print('Y有效长度为:', Y_valid_len)
    break
X: tensor([[162,   9,   5,   3,   1,   1,   1,   1],
        [  6, 143,   4,   3,   1,   1,   1,   1]], dtype=torch.int32)
X&#x6709;&#x6548;&#x957F;&#x5EA6;&#x4E3A;: tensor([4, 4])
Y: tensor([[171,   5,   3,   1,   1,   1,   1,   1],
        [ 10,   0,   4,   3,   1,   1,   1,   1]], dtype=torch.int32)
Y&#x6709;&#x6548;&#x957F;&#x5EA6;&#x4E3A;: tensor([3, 4])

现在已经处理了数据,您可以构建模型并使用数据集进行训练。

[En]

Now that the data has been processed, you can build a model and use the dataset for training.

3.2、seq2seq模型搭建与训练

导入使用的相关包

import collections
import math
import torch
from torch import nn
from d2l import torch as d2l

3.2.1、编码器

首先实现编码器部分,使用嵌入层来获得输入序列中每个词元的特征向量。嵌入层的权重时一个矩阵,其行数等于输入词表的大小,其列数等于特定向量的维度。对于任意输入词元的索引i,嵌入层获取权重矩阵的第i行以返回其特征向量。下面先介绍下pytorch中如何使用 torch.nn.Embedding函数实现词嵌入:

它是一个简单的查找表,存储固定大小词典的嵌入向量,也就是说,给定一个数字,嵌入层可以返回相应的嵌入向量。嵌入的向量反映了由每个数字表示的符号之间的语义关系。

[En]

It is a simple look-up table that stores embedded vectors of fixed-size dictionaries, that is, given a number, the embedded layer can return the corresponding embedded vector. The embedded vector reflects the semantic relationship between the symbols represented by each number.

在pytorch中的格式为:

torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None, max_norm=None, norm_type=2.0,
                   scale_grad_by_freq=False, sparse=False, _weight=None, device=None, dtype=None)

参数说明:

  • num_embeddings (int) – 嵌入字典的大小
  • embedding_dim (int) – 每个嵌入向量的大小
  • padding_idx (int, optional) – 如果提供的话,输出遇到此下标时用零填充
  • max_norm (float, optional) – 如果提供的话,会重新归一化词嵌入,使它们的范数小于提供的值
  • norm_type (float, optional) – 对于max_norm选项计算p范数时的p1
  • scale_grad_by_freq (boolean, optional) – 如果提供的话,会根据字典中单词频率缩放梯度

输入输出形状:

  • 输入:(N, W):N = mini-batch, W = 每个mini-batch中提取的下标数
  • 输出:(N, W, embedding_dim):前一部分为输入的形状,后面为嵌入向量的大小

下面看看简单使用:

embeding = nn.Embedding(10,5)
input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
output = embeding(input)
input.shape,output.shape,output
(torch.Size([2, 4]),
 torch.Size([2, 4, 5]),
 tensor([[[-0.4725,  1.3666,  0.2446,  0.5467,  0.2617],
          [ 0.6482,  0.0768, -1.3123, -1.4706,  0.3101],
          [-0.4600,  1.2231, -0.0948, -2.0522,  0.0775],
          [-0.7260,  0.2551, -0.4578,  1.1381, -0.2653]],

         [[-0.4600,  1.2231, -0.0948, -2.0522,  0.0775],
          [ 0.4369,  0.4180, -0.0175,  0.4675,  0.8469],
          [ 0.6482,  0.0768, -1.3123, -1.4706,  0.3101],
          [-0.2412, -0.3602,  1.2952, -0.5347,  0.9056]]],
        grad_fn=<embeddingbackward0>))
</embeddingbackward0>

然后就使用GRU来实现编码器,以获得输入序列的中间语义表示。

class Seq2SeqEncoder(nn.Module):
    def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout = 0,**kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)

        self.embedding = nn.Embedding(vocab_size,embed_size)

        self.rnn = nn.GRU(embed_size,num_hiddens,num_layers,dropout=dropout)

    def forward(self,X,*args):

        X = self.embedding(X)

        X = X.permute(1,0,2)

        output,state = self.rnn(X)

        return output, state

下面测试一下上面定义的编码器,其中GRU为2层,隐层特征数为16。给定一个小批量输入序列X(批量大小为4,时间步为7)。最后一个隐藏层的输入output形状为(时间步数,批量大小,隐藏特征数),而state保存的时当前时间步每一隐藏层的隐藏状态,其形状为(隐藏层数量,批量大小,隐藏特征数)。

encoder = Seq2SeqEncoder(vocab_size = 10,embed_size = 8,num_hiddens = 16,num_layers = 2)

encoder.eval()
X = torch.zeros((4,7),dtype = torch.long)
output,state = encoder(X)
output.shape,state.shape
(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))
output = encoder(X)
output[0].shape,output[1].shape
(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))

output为所有时间步隐藏状态的集合,而state可以看成当前时间步所有隐藏层隐藏状态的集合,那么output最后一个元素和state最后一个元素都代表了当前时间步最后一层隐藏状态,那么它们应该相等,下面测试一下。

output[-1][-1]==state[-1]
tensor([[True, True, True, True, True, True, True, True, True, True, True, True,
         True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True, True, True,
         True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True, True, True,
         True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True, True, True,
         True, True, True, True]])

没错是一样的!

3.2.2、解码器

解码器的隐藏状态是通过使用编码器的最后时间步长(所有层)的隐藏状态来初始化的,这要求编码器和解码器具有相同数量的隐藏特征和隐藏层。编码器最后一个时间步长的最后一层的输出用于将最新的输入与解码器的输入拼接。为了预测单词的概率分布,在解码器的最后一层使用全连接层来输出最终结果。

[En]

The hidden state of the decoder is initialized by using the hidden state of the last time step (all layers) of the encoder, which requires that the encoder and the decoder have the same number of hidden features and hidden layers. The output of the last layer of the last time step of the encoder is used to splice the latest input with the input of the decoder. In order to predict the probability distribution of words, the full connection layer is used to output the final result at the last layer of the decoder.

class Seq2SeqDecoder(nn.Module):
    def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout = 0,**kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size,embed_size)
        self.rnn = nn.GRU(embed_size + num_hiddens,num_hiddens,num_layers,dropout = dropout)
        self.dense = nn.Linear(num_hiddens,vocab_size)

    def init_state(self,enc_outputs,*args):

        return enc_outputs[1]

    def forward(self,X,state):
        X = self.embedding(X).permute(1,0,2)

        context = state[-1].repeat(X.shape[0],1,1)

        X_and_context = torch.cat((X,context),2)

        output,state = self.rnn(X_and_context,state)

        output = self.dense(output).permute(1,0,2)

        return output,state

让我们实例化解码器,检查输入,并使用与解码器中相同的超参数。因为最后一层使用线性层,所以解码器的输出编程(批次大小、时间步长、词汇大小)和张量的最后一维存储预测的词分布。

[En]

Let’s instantiate the decoder, check the input, and use the same hyperparameters as in the decoder. Because the last layer uses a linear layer, the output programming of the decoder (batch size, time steps, vocabulary size), and the last dimension of the tensor stores the predicted word distribution.

decoder = Seq2SeqDecoder(vocab_size = 10,embed_size = 8,num_hiddens = 16,num_layers = 2)
decoder.eval()
state = decoder.init_state(encoder(X))
output,state = decoder(X,state)
output.shape,state.shape
(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))

3.2.3、编码器-解码器

编码器-解码器中包含了一个编码器和一个解码器。在前向传播过程中,编码器的输出用于生成中间编码状态,这个状态又被解码器作为其输入的一部分。下面将编码器解码器整合起来。

class EncoderDecoder(nn.Module):
    def __init__(self,encoder,decoder,**kwargs):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder

    def forward(self,enc_X,dec_X,*args):
        enc_outputs = self.encoder(enc_X,*args)
        dec_state = self.decoder.init_state(enc_outputs,*args)
        return self.decoder(dec_X,dec_state)

3.2.4、损失函数

在每个时间步,解码器预测了输出词元的分布。与语言模型类似,可以使用softmax来获得分布,并计算交叉熵损失函数来进行优化。而在前面处理数据集的时候,我们在序列的末尾添加了特定的填充词元,将不同长度的序列变为了相同的长度。但是,填充的词元的预测应该排除在损失函数的计算之内。

使用以下函数可以用零字来屏蔽不相关的项,因此任何后续不相关的预测都将乘以零,结果为0。是将有效长度之后的所有项目清除为0。

[En]

Use the following function to mask unrelated items with zero words, so that any subsequent irrelevant predictions are multiplied by zero, and the result is 0. Is to clear all items after the valid length to 0.

def sequence_mask(X,valid_len,value = 0):

    maxlen = X.size(1)

    mask = torch.arange((maxlen),dtype = torch.float32,device=X.device)[None:] < valid_len[:,None]
    X[~mask] = value
    return X
X = torch.tensor([[1,2,3],[4,5,6]])
sequence_mask(X,torch.tensor([1,2]))
tensor([[1, 0, 0],
        [4, 5, 0]])

您还可以使用辅助函数来屏蔽最后几个轴上的所有项目,或者可以用非零值替换它们。

[En]

You can also use secondary functions to mask all items on the last few axes, or you can replace them with non-zero values.

X = torch.ones(2,3,4)
sequence_mask(X,torch.tensor([1,2]),value = -1)
tensor([[[ 1.,  1.,  1.,  1.],
         [-1., -1., -1., -1.],
         [-1., -1., -1., -1.]],

        [[ 1.,  1.,  1.,  1.],
         [ 1.,  1.,  1.,  1.],
         [-1., -1., -1., -1.]]])

下面就可以扩展softmax交叉熵损失函数来遮蔽不相关的预测。


class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):

    def forward(self,pred,label,valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights,valid_len)
        self.reduction = 'none'

        unweighted_loss = super(MaskedSoftmaxCELoss,self).forward(pred.permute(0,2,1),label)

        weighted_loss = (unweighted_loss * weights).mean(dim = 1)
        return weighted_loss

下面使用三个全为0的相同的序列来检查自定义交叉熵损失函数的结果,然后分别指定这些有效序列的长度为4,2,0。结果应该是第一个序列是第二个序列的两倍,而第三个序列为0。

loss = MaskedSoftmaxCELoss()
loss(torch.ones(3,4,10),torch.ones((3,4),dtype = torch.long),torch.tensor([4,2,0]))
tensor([2.3026, 1.1513, 0.0000])

3.2.5、训练

在循环训练的过程中,序列以单词元素(“

[En]

In the process of circular training, the sequence begins with the word element (“

模型计算出的值的长度为词表大小(每一个值代表为该词的概率),而标签值则是一个索引(词表中的第一个词)为一个数值(pytorch中不需要转为one-hot),使用自定义的带遮罩的交叉熵损失函数计算损失。


def try_gpu(i=0):
    if torch.cuda.device_count()>=i+1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def grad_clipping(net,theta):
    if isinstance(net,nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

def train_seq2seq(net,data_iter,lr,num_epochs,tgt_vocab,device):

    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])
    net.apply(xavier_init_weights)
    net.to(device)

    optimizer = torch.optim.Adam(net.parameters(),lr = lr)
    loss = MaskedSoftmaxCELoss()

    net.train()
    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.to(device) for x in batch]

            bos = torch.tensor([tgt_vocab['']] * Y.shape[0],device = device).reshape(-1,1)
            dec_input = torch.cat([bos,Y[:,:-1]],1)

            Y_hat,_ = net(X,dec_input,X_valid_len)

            l = loss(Y_hat,Y,Y_valid_len)
            l.sum().backward()
            grad_clipping(net,1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                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} tokens.sec on {str(device)}')

下面定义了相关变量并使用数据集对模型进行训练

[En]

The following defines related variables and uses datasets to train the model


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,try_gpu()

train_iter,src_vocab,tgt_vocab = 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 = EncoderDecoder(encoder,decoder)

train_seq2seq(net,train_iter,lr,num_epochs,tgt_vocab,device)
loss 0.020,7732.0 tokens.sec on cuda:0

编码器-解码器(seq2seq)

3.2.6、预测

解码器的当前时间步长的输入来自前一时间步长和前一时间步长之间的预测字。开头的语素是

[En]

The input of the current time step of the decoder comes from the prediction word between the previous time step and the previous time step. And the beginning morpheme is

def predict_seq2seq(net,src_sentence,src_vocab,tgt_vocab,num_steps,device):

    net.eval()

    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['']]

    enc_valid_len = torch.tensor([len(src_tokens)],device = device)

    src_tokens = truncate_pad(src_tokens,num_steps,src_vocab[''])

    enc_X = torch.unsqueeze(torch.tensor(src_tokens,dtype = torch.long,device = device),dim = 0)

    enc_outputs = net.encoder(enc_X,enc_valid_len)

    dec_state = net.decoder.init_state(enc_outputs,enc_valid_len)

    dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['']],dtype = torch.long,device = device),dim = 0)

    output_seq = []

    for _ in range(num_steps):

        Y,dec_state = net.decoder(dec_X,dec_state)

        dec_X = Y.argmax(dim = 2)

        pred = dec_X.squeeze(dim = 0).type(torch.int32).item()
        if pred == tgt_vocab['']:
            break
        output_seq.append(pred)

    return ' '.join(tgt_vocab.to_tokens(output_seq))

3.2.7、评估预测的词元

根据上面定义的BLEU的定义,通过代码实现BLEU然后对预测的结果进行评估。

def bleu(preq_seq,label_seq,k):

    pred_tokens,label_tokens = preq_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 . => va !, bleu 1.000
i lost . => j'ai perdu pay&#xE9; ., bleu 0.658
he's calm . => il court ., bleu 0.000
i'm home . => je suis chez moi paresseux ., bleu 0.803

3.2.8、代码整合

以下是所有代码的集成

[En]

Here is the integration of all the code

import os
import torch
import collections
from torch.utils import data
import math
from torch import nn
from d2l import torch as d2l

def read_data_nmt():
    with open('fra-eng/fra.txt', 'r',encoding='utf-8') as f:
        return f.read()

def preprocess_nmt(text):
    def no_space(char, prev_char):
        return char in set(',.!?') and prev_char != ' '

    text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()

    out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char for i, char in enumerate(text)]

    return ''.join(out)

def tokenize_nmt(text,num_examples = None):
    source,target = [],[]

    for i,line in enumerate(text.split('\n')):

        if num_examples and i > num_examples:
            break

        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' '))
            target.append(parts[1].split(' '))
    return source,target

def count_corpus(tokens):

    if len(tokens) == 0 or isinstance(tokens[0], list):

        tokens = [token for line in tokens for token in line]

    return collections.Counter(tokens)

class Vocab:
    def __init__(self,tokens = None, min_freq = 0, reserved_tokens = None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []

        counter = count_corpus(tokens)

        self._token_freqs = sorted(counter.items(),key = lambda x:x[1],reverse = True)

        self.idx_to_token = [''] + reserved_tokens

        self.token_to_idx = {token:idx for idx,token in enumerate(self.idx_to_token)}

        for token, freq in self._token_freqs:

            if freq < min_freq:
                break

            if token not in self.token_to_idx:
                self.idx_to_token.append(token)

                self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens,(list,tuple)):

            return self.token_to_idx.get(tokens,self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self,indices):
        if not  isinstance(indices,(list,tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]
    @property
    def unk(self):
        return 0
    @property
    def token_freqs(self):
        return self._token_freqs

def truncate_pad(line,num_steps,padding_token):
    if len(line) > num_steps:
        return line[:num_steps]
    return line + [padding_token] * (num_steps - len(line))

def build_array_nmt(lines, vocab, num_steps):

    lines = [vocab[l] for l in lines]

    lines = [l + [vocab['']] for l in lines]
    array = torch.tensor([truncate_pad(l, num_steps, vocab['']) for l in lines])
    valid_len = (array != vocab['']).type(torch.int32).sum(1)

    return array, valid_len

def load_array(data_arrays,batch_size,is_train=True):
    dataset=data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset,batch_size,shuffle=is_train)

def load_data_nmt(batch_size,num_steps,num_examples = 600):

    text = preprocess_nmt(read_data_nmt())

    source,target = tokenize_nmt(text,num_examples)

    src_vocab = Vocab(source,min_freq=2,reserved_tokens=['','',''])
    tgt_vocab = Vocab(target,min_freq=2,reserved_tokens=['','',''])

    src_array,src_valid_len = build_array_nmt(source,src_vocab,num_steps)
    tgt_array,tgt_valid_len = build_array_nmt(target,tgt_vocab,num_steps)

    data_arrays = (src_array,src_valid_len,tgt_array,tgt_valid_len)
    data_iter = load_array(data_arrays,batch_size)
    return data_iter,src_vocab,tgt_vocab

class Seq2SeqEncoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)

        self.embedding = nn.Embedding(vocab_size, embed_size)

        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers, dropout=dropout)

    def forward(self, X, *args):

        X = self.embedding(X)

        X = X.permute(1, 0, 2)

        output, state = self.rnn(X)

        return output, state

class Seq2SeqDecoder(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, *args):

        return enc_outputs[1]

    def forward(self, X, state):
        X = self.embedding(X).permute(1, 0, 2)

        context = state[-1].repeat(X.shape[0], 1, 1)

        X_and_context = torch.cat((X, context), 2)

        output, state = self.rnn(X_and_context, state)

        output = self.dense(output).permute(1, 0, 2)

        return output, state

class EncoderDecoder(nn.Module):
    def __init__(self,encoder,decoder,**kwargs):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder

    def forward(self,enc_X,dec_X,*args):
        enc_outputs = self.encoder(enc_X,*args)
        dec_state = self.decoder.init_state(enc_outputs,*args)
        return self.decoder(dec_X,dec_state)

def sequence_mask(X,valid_len,value = 0):

    maxlen = X.size(1)

    mask = torch.arange((maxlen),dtype = torch.float32,device=X.device)[None:] < valid_len[:,None]
    X[~mask] = value
    return X

class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):

    def forward(self,pred,label,valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights,valid_len)
        self.reduction = 'none'

        unweighted_loss = super(MaskedSoftmaxCELoss,self).forward(pred.permute(0,2,1),label)

        weighted_loss = (unweighted_loss * weights).mean(dim = 1)
        return weighted_loss

def try_gpu(i=0):
    if torch.cuda.device_count()>=i+1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def grad_clipping(net,theta):
    if isinstance(net,nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

def train_seq2seq(net,data_iter,lr,num_epochs,tgt_vocab,device):

    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])
    net.apply(xavier_init_weights)
    net.to(device)

    optimizer = torch.optim.Adam(net.parameters(),lr = lr)
    loss = MaskedSoftmaxCELoss()

    net.train()
    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.to(device) for x in batch]

            bos = torch.tensor([tgt_vocab['']] * Y.shape[0],device = device).reshape(-1,1)
            dec_input = torch.cat([bos,Y[:,:-1]],1)

            Y_hat,_ = net(X,dec_input,X_valid_len)

            l = loss(Y_hat,Y,Y_valid_len)
            l.sum().backward()
            grad_clipping(net,1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                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} tokens.sec on {str(device)}')

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,try_gpu()

train_iter,src_vocab,tgt_vocab = 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 = EncoderDecoder(encoder,decoder)

train_seq2seq(net,train_iter,lr,num_epochs,tgt_vocab,device)
loss 0.020,11098.6 tokens.sec on cuda:0

编码器-解码器(seq2seq)

def predict_seq2seq(net,src_sentence,src_vocab,tgt_vocab,num_steps,device):

    net.eval()

    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['']]

    enc_valid_len = torch.tensor([len(src_tokens)],device = device)

    src_tokens = truncate_pad(src_tokens,num_steps,src_vocab[''])

    enc_X = torch.unsqueeze(torch.tensor(src_tokens,dtype = torch.long,device = device),dim = 0)

    enc_outputs = net.encoder(enc_X,enc_valid_len)

    dec_state = net.decoder.init_state(enc_outputs,enc_valid_len)

    dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['']],dtype = torch.long,device = device),dim = 0)

    output_seq = []

    for _ in range(num_steps):

        Y,dec_state = net.decoder(dec_X,dec_state)

        dec_X = Y.argmax(dim = 2)

        pred = dec_X.squeeze(dim = 0).type(torch.int32).item()
        if pred == tgt_vocab['']:
            break
        output_seq.append(pred)

    return ' '.join(tgt_vocab.to_tokens(output_seq))

def bleu(preq_seq,label_seq,k):

    pred_tokens,label_tokens = preq_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 . => va au feu !, bleu 0.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est mouill&#xE9; <unk> de question ., bleu 0.418
i'm home . => je suis libre !, bleu 0.418
</unk>

Original: https://blog.csdn.net/tcn760/article/details/124432361
Author: CityD
Title: 编码器-解码器(seq2seq)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/527311/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球