QANet模型系列(1)

QANet模型系列(1)

学习机器阅读理解模型的时候,在GitHub上见到了一系列很好的NLP教程,大佬的博客地址在这里,有时间我会对这些文章进行翻译(已经申请,但是还没有回复),对代码进行注释,当我本身也是自然语言处理方面的初学者,难免有很多错误,如果有朋友能够帮我指出,将非常感谢 !


这里将解释QANet模型的部分知识,SQuAD数据处理可我之前翻译的文章。

文章目录


前言

一、Depthwise Separable Convolutions

注意! 专业名词翻译的不准确,我对卷积神经网络了解比较少,在filter和kernal的理解还不是很深刻,有些地方的概念没有写清楚,读的时候注意上下文判断。

深度可分离卷积与普通卷积的目的相同,唯一的区别是它们更快,因为它们减少了乘法运算的次数。这是通过将卷积运算分为两部分来实现的:(depthwise convolution, DC)和(pointwise convolution, PC)。

*We use depthwise separable convolutions rather than traditional ones, as we observe that it is memory efficient and has better generalization. *

下面来理解为什么depthwise convolutions为什么比传统卷积快。
传统的卷积可以想象为:

QANet模型系列(1)
首先计算一下在传统卷积运算中乘法的次数,单个卷积操作的乘法次数就是核内元素的数量。 D K D_{K} DK​ X D K D_{K} DK​ X M M M = D K 2 D_{K}^{2} DK2​ X M M M.为了得到输出特征映射(feature map),我们在输入端滑动(slide)或卷积(convolve)这个KERNEL。给定输出维度,我们沿着输入图像的宽度和高度执行 D O D_{O} DO​卷积。因此,每个内核的乘法次数为 D O 2 D_{O}^{2} DO2​ X D K 2 D_{K}^{2} DK2​ X M M M。

这些计算是针对单个内核的。在卷积神经网络中,我们通常使用多个核。期望每个内核从输入中提取一个独特的特征。如果使用 N N N个过滤器(filter),那么计算次数是 N N N X D O 2 D_{O}^{2} DO2​ X D K 2 D_{K}^{2} DK2​ X M M M.

Depthwise convolution

QANet模型系列(1)
在depthwise convolution中,我们执行卷积操作使用 D K D_{K} DK​ X D K D_{K} DK​ X 1维的卷积核。因此,一次卷积运算的乘法次数为 D K 2 D_{K}^{2} DK2​ X 1 1 1。如果输出维度是 D O D_{O} DO​,则每个卷积核的乘法次数为 D K 2 D_{K}^{2} DK2​ X D O 2 D_{O}^{2} DO2​。如果有 M M M个输入通道,我们需要使用 M M M这样的卷积核,一个卷积核对每个输入通道得到所有的特性。对于 M M M个卷积核,我们需要 D K 2 D_{K}^{2} DK2​ X D O 2 D_{O}^{2} DO2​ X M M M次乘法运算。

Pointwise convolution

QANet模型系列(1)
这一部分从Depthwise convolution 中获取输出,并使用大小为1 X 1 X N N N的卷积核进行卷积运算,其中 N N N是期望的输出features/channels数量。
同样在这里,

每1个卷积运行的乘法次数 = 1 X 1 X M M M

每个卷积核的乘法次数 = D O 2 D_{O}^{2} DO2​ X M M M

对于N个输出 features = N N N X D O 2 D_{O}^{2} DO2​ X M M M

把两个相相乘的次数加起来,我们得到,

=   N   .   D O 2   .   M   +   D K 2   .   D O 2   .   M =\ N\ .\ D_{O}^{2} \ .\ M \ +\ D_{K}^{2}\ .\ D_{O}^{2}\ .\ M = N . DO2​ . M + DK2​ . DO2​ . M
=   D O 2   .   M ( N + D K 2 ) =\ D_{O}^{2}\ .\ M (N + D_{K}^{2}) = DO2​ . M(N+DK2​)

相比较传统卷积操作次数

=   D O 2   .   M   ( N + D K 2 ) D O 2   .   M   .   D K 2   .   N =\ \frac {D_{O}^{2}\ .\ M\ (N + D_{K}^{2})} {D_{O}^{2}\ .\ M\ .\ D_{K}^{2}\ .\ N} = DO2​ . M . DK2​ . NDO2​ . M (N+DK2​)​

=   1 D K 2   +   1 N =\ \frac{1}{D_{K}^{2}}\ +\ \frac{1}{N} = DK2​1​ + N1​

这清楚地表明,深度可分卷积(depthwise separable convolutions)的计算次数比传统的要少。代码中,卷积的depthwise阶段是通过将 groups分配为in_channels来完成的,根据文档。

At groups= in_channels, each `nput channel is convolved with its own set of filters, of size: ⌊ o u t _ c h a n n e l s i n _ c h a n n e l s ⌋ \left\lfloor\frac{out\_channels}{in\_channels}\right\rfloor ⌊in_channelsout_channels​⌋

对应的中文文档 1.0版本

当 groups= in_channels, 每个输入通道都会被单独的一组卷积层处理,这个组的大小是 ⌊ C o u t C i n ⌋ \left\lfloor\frac{C_{out}}{C_{in}}\right\rfloor ⌊Cin​Cout​​⌋

CONV2D

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode=‘zeros’)
CONV2D 1.0 中文官方文档

python
class DepthwiseSeparableConvolution(nn.Module):
    
    def __init__(self, in_channels, out_channels, kernel_size, dim=1):
        
        super().__init__()
        self.dim = dim
        if dim == 2:
            
            self.depthwise_conv = nn.Conv2d(in_channels=in_channels, out_channels=in_channels,
                                        kernel_size=kernel_size, , padding=kernel_size//2)
        
            self.pointwise_conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0)
        
    
        else:
        
            self.depthwise_conv = nn.Conv1d(in_channels=in_channels, out_channels=in_channels,
                                            kernel_size=kernel_size, groups=in_channels, padding=kernel_size//2,
                                            bias=False)

            self.pointwise_conv = nn.Conv1d(in_channels, out_channels, kernel_size=1, padding=0, bias=True)
        # groups=in_channels 使用 Depthwise convolution 且kernel_size=1使用pointwise_conv

    
    def forward(self, x):
        # x = [bs, seq_len, emb_dim]
        if self.dim == 1:
            x = x.transpose(1,2)
            x = self.pointwise_conv(self.depthwise_conv(x))
            x = x.transpose(1,2)
        else:
            x = self.pointwise_conv(self.depthwise_conv(x))
        #print("DepthWiseConv output: ", x.shape)
        return x

二、Highway Networks

highway networks最初被引入是为了简化深度神经网络的训练。虽然研究人员已经破解了优化浅层神经网络的代码,但由于存在诸如消失梯度等问题,训练deep神经网络仍然是一项具有挑战性的任务,应用这篇论文,

We present a novel architecture that enables the optimization of networks with virtually arbitrary depth. This is accomplished through the use of a learned gating mechanism for regulating information flow which is inspired by Long Short Term Memory recurrent neural networks. Due to this gating mechanism, a neural network can have paths along which information can flow across several layers without attenuation. We call such paths information highways, and such networks highway networks.

我们提出了一个新的架构,使网络的优化具有几乎任意的深度。这是通过使用一个学习的门控机制来调节信息流来完成的,该机制是受LSTM的启发。由于门控机制,一个神经网络可以有路径,信息可以在没有衰减的情况下跨几个层流动。我们称这些路径为information highways,称这些网络为highway networks。

这篇论文从LSTMs中借鉴了学习门机制的核心思想,LSTMs通过一系列学习门来内部处理信息。这一层的目的是learn从输入中传递的相关信息。highway network是一系列应用门机制的前馈层或者线性层。门是通过使用sigmoid函数实现的,该函数决定应该转换多少信息以及应该传递什么信息。

一个普通的feed-forward层由( W H , b H W_{H}, b_{H} WH​,bH​)为参数的线性转化 H H H,例如,输入 x x x,输出 y y y,那么

y = g ( W H . x + b H ) y = g(W_{H}.x + b_{H}) y=g(WH​.x+bH​)

g g g是一个非线性激活函数

对于highway networks,定义两个附加的线性变换。 T T T ( W T , b T W_{T},b_{T} WT​,bT​) and C C C ( W C W_{C} WC​, b C b_{C} bC​).
那么

y = T ( x ) . H ( x ) + x . ( 1 − T ( x ) ) y = T(x) . H(x) + x . (1 - T(x)) y=T(x).H(x)+x.(1−T(x))

*We refer to T as the transform gate and C as the carry gate, since they express how much of the output is produced by
transforming the input and carrying it, respectively. For simplicity, in this paper we set C = 1 − T. *

我们提出T作为 transform门,C作为carry门,因为它们分别表示,输入的变换产生的输出量和进位量,为简单起见,在这篇论文中我们设置为 C = 1 - T

y = T ( x ) . H ( x ) + x . ( 1 − T ( x ) ) y = T(x) . H(x) + x . (1 - T(x)) y=T(x).H(x)+x.(1−T(x))

y = T ( x ) . g ( W H . x + b H ) + x . ( 1 − T ( x ) ) y = T(x) . g(W_{H}.x + b_{H}) + x . (1 - T(x)) y=T(x).g(WH​.x+bH​)+x.(1−T(x))
where T ( x ) T(x) T(x) = σ \sigma σ ( W T W_{T} WT​ . x x x + b T b_{T} bT​) and g g g is relu activation.

这一层的输入是单词和单词字符的embeddings。为了实现这个功能,我们使用 nn.ModuleList来增加多重线性层,来实现gate layer和linear transform。在代码flow_layer与上述相同的线性变换 H H H,gate_layer是 T T T。在之后,我们循环遍历每一层,并根据上述highway的方程计算输出。

这一层的输出,文本: X X X ϵ \epsilon ϵ R   d   X   T R^{\ d \ X \ T} R d X T,问题: Q Q Q ϵ \epsilon ϵ R   d   X   J R^{\ d \ X \ J} R d X J, d d d 是LSTM的隐藏层大小, T T T 是文本长度, J J J 是问题长度.

到目前为止所讨论的结构是许多自然语言处理系统中反复出现的模式。尽管随着transformer和大型预先训练语言模型的出现,这种模式现在可能已经不受欢迎了,但在transformer出现之前,您会在许多NLP系统中发现这种模式。其背后的想法是,添加 highway layers 可以使网络更有效地使用字符嵌入。如果在预训练的词向量词汇表(vocab, OOV word)中没有找到特定的词,则很可能用零向量初始化它。这样将更有意义,查看这个词的字符嵌入,而不是嵌入这个词。在highway layers的门机制(soft gating mechanism)将有助于模型实现这个目标

class HighwayLayer(nn.Module):
    
    def __init__(self, layer_dim, num_layers=2):
    
        super().__init__()
        self.num_layers = num_layers
        
        self.flow_layers = nn.ModuleList([nn.Linear(layer_dim, layer_dim) for _ in range(num_layers)])
        self.gate_layers = nn.ModuleList([nn.Linear(layer_dim, layer_dim) for _ in range(num_layers)])
    
    def forward(self, x):
        # print("Highway input: ", x.shape)
        for i in range(self.num_layers):
            
            flow = self.flow_layers[i](x)
            gate = torch.sigmoid(self.gate_layers[i](x))
            
            x = gate * flow + (1 - gate) * x
            
        # print("Highway output: ", x.shape)
        return x

三、Embedding Layer

这一层:

  • 将单词级别的tokens转化为 300-dim的glove预训练的向量
  • 使用2-D 卷积创建可训练的字符嵌入
  • 拼接字符嵌入和单词嵌入,并且使其通过highway network

每个字符表示为维度p2 = 200的可训练向量,也就是说每个单词可以看作是其每个字符的嵌入向量的拼接。每个单词的长度被截断或填充为16。我们取这个矩阵中每一行的最大值,以得到每个单词的固定大小的向量表示

Word Embedding

问题和上下文的第一次转换是通过一个由预训练的glove词向量初始化的嵌入层传递。这里使用840B web crawl版本的300维向量。这个版本的GloVe有220万个词汇。Out of vocabulary(OOV)单词由零向量初始化, OOV单词是出现在数据集中,但不在预先训练的手套词汇表中的单词。

这些词向量用于投射/转换一个词到一个浮点向量,该浮点向量将与词相关的各种特征编码到它的维度中。这种转换是必要的,因为计算机不能把字当作字符串来处理,但可以无缝地处理大量的浮点矩阵。

语义相似的单词向量的点积接近于1,反之亦然。

Character Embedding

为每一个context和query的单词计算一个字符嵌入,是通过卷积实现。

It maps each word to a vector space using character-level CNNs.

2014年 Yoon Kim 在他的论文中首次提出使用在NLP中使用CNNs,“Convolutional Neural Networks for Sentence Classification”, 这篇文试图将cnn在视觉上的运用,运用到自然语言处理中。当时,CV的大多数最先进的成果都是通过在ImageNet上预训练的更大模型的迁移学习获得的。在这篇文章中,他们在预先训练的词向量上训练了一个简单的CNN,并在上面进行了一层卷积,假设这些预先训练的词向量可以作为各种分类任务的通用特征提取器。这类似于早期的视觉模型层,如VGG和Inception,作为通用特征提取器。

这是非常直观的,就像卷积滤波器通过操作像素来学习图像的各种特征一样,这里它们也将通过操作单词的字符来做到这一点。下面来看一下这层是怎么做的。

我们首先将每个单词通过嵌入层得到一个固定大小的向量。让嵌入的维度是 d d d。矩阵 C C C表示单词长度为 l l l的矩阵。因此, C C C的矩阵维度为 d d d x l l l.
QANet模型系列(1)
H H H表示一个 d x w d x w dxw维的滤波器,这里 d d d是嵌入的维度, w w w是宽度或者滤波器窗口的大小。
滤波器的权重是随机初始化的,并且通过反向传播并行学习。将这个滤波器H与单词表示C进行卷积,表示如下:
QANet模型系列(1)
卷积运算就是滤波器 H H H和矩阵 C C C的内积。卷积运算如下图所示:
QANet模型系列(1)
上述操作的结果就是一个特征向量。一个单滤波器通常从图像/矩阵中捕获的独特特征相关联。为了获得与该特征相关的最具代表性的值,我们对该向量的维数进行max pooling。
QANet模型系列(1)
上述过程是针对单个滤波器进行的。使用 N N N数目的卷积器重复相同的过程。每个卷积核都捕获单词的不同属性。例如,在一幅图像中,如果一个滤波器捕捉图像的边缘,另一个滤波器捕捉图像的纹理,另一个滤波器捕捉图像的形状,以此类推。 N N N表示想要得到的字符嵌入的尺寸。在这篇论文中 N = 100 N=100 N=100.

这一层的实现相当简单。这一层输入的维度是[batch_size, seq_len, word_len],在这里 seq_len and word_len 是给定批次的最大序列的长度和单词长度。我们首先使用嵌入层将字符标记嵌入到一个固定大小的向量中。给出了一个维度为[batch_size, seq_len, word_len, emb_dim]的向量。

然后我们把这个张量(tensor)转换成一种非常像图像的格式 [ N N N, C i n C_{in} Cin​, H i n H_{in} Hin​, W i n W_{in} Win​]。输入通道的数量是 C i n C_{in} Cin​将会是1,输出通道的数量将会是嵌入尺寸为100。然后通过卷积层,得到形状[ N N N, C o u t C_{out} Cout​, H o u t H_{out} Hout​, W o u t W_{out} Wout​]的输出

QANet模型系列(1)
如果padding = [0,0], kernel_size (or filter_size) = [ H i n H_{in} Hin​, w w w], dilation = [1,1], stride = [1,1] (如上图所示), 那么, H o u t H_{out} Hout​ = 1, 且 W o u t W_{out} Wout​ = W i n W_{in} Win​ - w w w - 1.
QANet模型系列(1)
如果卷积核= L i n L_{in} Lin​,其它值默认得到 L o u t L_{out} Lout​ = 1。这个维度再次被压缩,最终给出一个维度的张量[batch_size, seq_len, output_channels (or 100)].

class EmbeddingLayer(nn.Module):
    
    def __init__(self, char_vocab_dim, char_emb_dim, kernel_size, device):
        
        super().__init__()
        
        self.device = device
        
        self.char_embedding = nn.Embedding(char_vocab_dim, char_emb_dim)
        
        self.word_embedding = self.get_glove_word_embedding()
        
        self.conv2d = DepthwiseSeparableConvolution(char_emb_dim, char_emb_dim, kernel_size,dim=2)
        
        self.highway = HighwayLayer(self.word_emb_dim + char_emb_dim)
    
        
    def get_glove_word_embedding(self):
        
        weights_matrix = np.load('qanetglove.npy')
        num_embeddings, embedding_dim = weights_matrix.shape
        self.word_emb_dim = embedding_dim
        embedding = nn.Embedding.from_pretrained(torch.FloatTensor(weights_matrix).to(self.device),freeze=True)

        return embedding
    
    def forward(self, x, x_char):
        # x = [bs, seq_len]
        # x_char = [bs, seq_len, word_len(=16)]
        
        word_emb = self.word_embedding(x)
        # word_emb = [bs, seq_len, word_emb_dim]
        
        word_emb = F.dropout(word_emb,p=0.1)
        
        char_emb = self.char_embedding(x_char)
        # char_embed = [bs, seq_len, word_len, char_emb_dim]
        
        char_emb = F.dropout(char_emb.permute(0,3,1,2), p=0.05)
        # [bs, char_emb_dim, seq_len, word_len] == [N, Cin, Hin, Win]
        
        conv_out = F.relu(self.conv2d(char_emb))
        # [bs, char_emb_dim, seq_len, word_len] 
        # the depthwise separable conv does not change the shape of the input
        
        char_emb, _ = torch.max(conv_out, dim=3)
        # [bs, char_emb_dim, seq_len]
        
        char_emb = char_emb.permute(0,2,1)
        # [bs, seq_len, char_emb_dim]
        
        concat_emb = torch.cat([char_emb, word_emb], dim=2)
        # [bs, seq_len, char_emb_dim + word_emb_dim]
        
        emb = self.highway(concat_emb)
        # [bs, seq_len, char_emb_dim + word_emb_dim]
        
        #print("Embedding output: ", emb.shape)
        return emb

总结

这是这两天看的,开始去看重头戏 Multiheaded Self Attention

上一篇:Anaconda 换源


下一篇:ffplay SDL_OpenAudio (2 channels, 44100 Hz): WASAPI can‘t initialize audio client“