在本笔记本中,我们将实现论文Convolutional Sequence to Sequence Learning模型。

Introduction
这个模型与之前笔记中使用的先前模型有很大的不同。根本没有使用任何循环的组件。相反,它使用通常用于图像处理的卷积层。
简而言之,卷积层使用了过滤器。这些过滤器有一个宽度(在图像中也有一个高度,但通常不是文本)。如果一个过滤器的宽度为3,那么它可以看到3个连续的标记。每个卷积层都有许多这样的过滤器(本教程中是1024个)。每个过滤器将从开始到结束滑过序列,一次查看所有3个连续的标记。其思想是,这1024个过滤器中的每一个都将学习从文本中提取不同的特征。这个特征提取的结果将被模型使用——可能作为另一个卷积层的输入。然后,这些都可以用来从源句子中提取特征,将其翻译成目标语言。
数据预处理
首先,让我们导入所有必需的模块,并为可重复性设置随机种子。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchtext.datasets import Multi30k
from torchtext.data import Field, BucketIterator
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import spacy
import numpy as np
import random
import math
import time
SEED = 1234
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
接下来,我们将加载spaCy模块,并为源语言和目标语言定义标记器。
spacy_de = spacy.load('de_core_news_sm')
spacy_en = spacy.load('en_core_web_sm')
def tokenize_de(text):
"""
Tokenizes German text from a string into a list of strings
"""
return [tok.text for tok in spacy_de.tokenizer(text)]
def tokenize_en(text):
"""
Tokenizes English text from a string into a list of strings
"""
return [tok.text for tok in spacy_en.tokenizer(text)]
接下来,我们将设置决定如何处理数据的字段。默认情况下,PyTorch中的RNN模型要求序列是一个[src_len,批batch_size]形状的张量,因此TorchText将默认返回一批相同形状的张量。然而,在本笔记中,我们使用的CNN期望batch_size是第一个。通过设置batch_first = True,我们告诉TorchText将batch设置为[batch_size,src_len]。
我们还附加了序列标记的开始和结束,并对所有文本进行小写。
SRC = Field(tokenize = tokenize_de,
init_token = '<sos>',
eos_token = '<eos>',
lower = True,
batch_first = True)
TRG = Field(tokenize = tokenize_en,
init_token = '<sos>',
eos_token = '<eos>',
lower = True,
batch_first = True)
#然后,我们加载数据集。
train_data, valid_data, test_data = Multi30k.splits(exts=('.de', '.en'),
fields=(SRC, TRG))
#我们像以前一样构建词汇表,将出现次数少于2次的任何标记转换为《unk》标记。
SRC.build_vocab(train_data, min_freq = 2)
TRG.build_vocab(train_data, min_freq = 2)
#最后一点数据准备是定义device,然后构建迭代器。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
BATCH_SIZE = 128
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size = BATCH_SIZE,
device = device)
搭建模型
接下来是构建模型。与之前一样,该模型由编码器和解码器组成。编码器用源语言将输入句子编码成上下文向量。解码器对上下文向量进行解码,以生成目标语言的输出句子。
Encoder
这些教程中以前的模型有一个编码器,它可以将整个输入句子压缩到单个上下文向量z zz中。卷积序列到序列模型有一点不同——它为输入句子中的每个标记获得两个上下文向量。因此,如果我们的输入句子有6个标记,我们将得到12个上下文向量,每个标记两个有两个上下文向量。
每个标记的两个上下文向量是一个卷积向量(conved vector)和一个组合向量(combined vector)。conved向量是每个标记通过几个层传递的结果——我们稍后将对此进行解释。combined向量来自于卷积向量和该标记的embedding的和。这两个都由编码器返回,由解码器使用。下图显示了输入句子zwei menschen fechten的结果。-通过编码器传递。

首先,token通过标记嵌入层传递——这是自然语言处理中的神经网络的标准。然而,由于该模型中没有循环的连接,因此不知道序列中标记的顺序。为了纠正这一点,我们有第二个嵌入层,位置嵌入层。这是一个标准的嵌入层,其中的输入不是标记本身,而是标记在序列中的位置——从第一个标记《sos》(序列开始)标记开始,位置为0。
接下来,将标记和位置嵌入元素相加得到一个向量,该向量包含关于标记及其在序列中的位置的信息——我们简单地称之为嵌入向量。随后是一个线性层,它将嵌入向量转换成具有所需隐藏维度大小的向量。
下一步是将这个隐藏向量传递到N NN卷积块中。这就是这个模型中发生“魔法”的地方,我们稍后将详细介绍卷积块的内容。经过卷积块后,向量传入另一个线性层,将其从隐藏维数大小转换回嵌入维数大小。这是我们的卷积向量(conved vector)------在输入序列中每个标记卷积后都会有一个。
最后,通过残差连接将卷积向量(conved vector)与嵌入向量(embedding vector)进行元素相加,得到每个标记的组合向量(combined vector)。同样,输入序列中的每个标记都有一个组合向量(combined vector)。
Convolutional Blocks
那么,这些卷积块是如何工作的呢?下图显示了两个卷积块,其中一个过滤器(蓝色)在序列中的标记上滑动。在实际的实现中,我们将有10个卷积块,每个块中有1024个过滤器。
首先,填充输入句子。这是因为卷积层将减少输入句子的长度,我们希望进入卷积块的句子的长度等于从卷积块中出来的句子的长度。如果没有填充,从卷积层出来的序列的长度将比进入卷积层的序列短filter_size - 1。例如,如果我们的过滤器大小为3,那么序列将短2个元素。因此,我们在句子的每一侧都填充一个padding元素。对于奇数大小的过滤器,我们可以通过简单的操作(filter_size - 1)/2来计算两边的填充量,在本教程中我们将不涉及偶数大小的过滤器。
这些过滤器的设计使其输出隐藏维数是输入隐藏维数的两倍。在计算机视觉术语中,这些隐藏的维度被称为通道——但我们将坚持称它们为隐藏的维度。为什么我们要把隐藏维度的大小增加一倍来离开卷积滤波器?这是因为我们使用了一种特殊的激活函数,叫做门控线性单元(GLU)。GLUs有门控机制(类似于LSTMs和GRUs),包含在激活函数中,实际上是隐藏维度大小的一半——而激活函数通常保持隐藏维度的大小相同。
经过GLU激活后,每个标记的隐藏维度大小与进入卷积块时相同。在经过卷积层之前,它现在与自己的向量进行元素级求和。
这就得到了一个单独的卷积块。后续块获取前一个块的输出并执行相同的步骤。每个块都有自己的参数,它们不会在块之间共享。最后一个块的输出返回到主编码器——在那里它通过线性层被馈入以得到卷积(conved)输出,然后与标记的嵌入(embedding)元素累加以得到组合(combined)输出。
Encoder的实现
为了使实现简单,我们只允许奇数大小的卷积核。这允许将填充相等地添加到源序列的两边。
研究人员使用这个尺度(scale)变量来“确保整个网络的方差不会发生显著变化”。如果不使用不同的种子,模型的性能似乎会有很大的不同。
位置嵌入被初始化为100的“词汇表”。这意味着它可以处理长度为100个元素的序列,索引范围从0到99。如果在具有更长的序列的数据集上使用,这个值可以增加。
class Encoder(nn.Module):
def __init__(self,
input_dim,
emb_dim,
hid_dim,
n_layers,
kernel_size,
dropout,
device,
max_length = 100):
super().__init__()
assert kernel_size % 2 == 1, "Kernel size must be odd!"
self.device = device
self.scale = torch.sqrt(torch.FloatTensor([0.5])).to(device)
self.tok_embedding = nn.Embedding(input_dim, emb_dim)
self.pos_embedding = nn.Embedding(max_length, emb_dim)
self.emb2hid = nn.Linear(emb_dim, hid_dim)
self.hid2emb = nn.Linear(hid_dim, emb_dim)
self.convs = nn.ModuleList([nn.Conv1d(in_channels = hid_dim,
out_channels = 2 * hid_dim,
kernel_size = kernel_size,
padding = (kernel_size - 1) // 2)
for _ in range(n_layers)])
self.dropout = nn.Dropout(dropout)
def forward(self, src):
#src = [batch size, src len]
batch_size = src.shape[0]
src_len = src.shape[1]
#create position tensor
pos = torch.arange(0, src_len).unsqueeze(0).repeat(batch_size, 1).to(self.device)
#pos = [0, 1, 2, 3, ..., src len - 1]
#pos = [batch size, src len]
#embed tokens and positions
tok_embedded = self.tok_embedding(src)
pos_embedded = self.pos_embedding(pos)
#tok_embedded = pos_embedded = [batch size, src len, emb dim]
#combine embeddings by elementwise summing
embedded = self.dropout(tok_embedded + pos_embedded)
#embedded = [batch size, src len, emb dim]
#pass embedded through linear layer to convert from emb dim to hid dim
conv_input = self.emb2hid(embedded)
#conv_input = [batch size, src len, hid dim]
#permute for convolutional layer
conv_input = conv_input.permute(0, 2, 1)
#conv_input = [batch size, hid dim, src len]
#begin convolutional blocks...
for i, conv in enumerate(self.convs):
#pass through convolutional layer
conved = conv(self.dropout(conv_input))
#conved = [batch size, 2 * hid dim, src len]
#pass through GLU activation function
conved = F.glu(conved, dim = 1)
#conved = [batch size, hid dim, src len]
#apply residual connection
conved = (conved + conv_input) * self.scale
#conved = [batch size, hid dim, src len]
#set conv_input to conved for next loop iteration
conv_input = conved
#...end convolutional blocks
#permute and convert back to emb dim
conved = self.hid2emb(conved.permute(0, 2, 1))
#conved = [batch size, src len, emb dim]
#elementwise sum output (conved) and input (embedded) to be used for attention
combined = (conved + embedded) * self.scale
#combined = [batch size, src len, emb dim]
return conved, combined
Decoder
解码器接收实际的目标句子并试图预测它。这个模型不同于前面在这些教程中详细介绍的循环神经网络模型,因为它可以并行地预测目标句子中的所有标记。没有顺序处理,也就是说没有解码循环。这将在后面的教程中进一步详细说明。
解码器与编码器类似&

最低0.47元/天 解锁文章
805

被折叠的 条评论
为什么被折叠?



