VQA入门之“论文”《Stacked Attention Networks for Image Question Answering》

写在前面

        本节将要介绍一种新的特征融合方式,这篇论文的方法叫做堆叠注意力网络。那么从本节开始,所有的模型我会把原理讲清楚,然后用一个维度较低的例子带各位同学走一遍模型的前向传播。

        堆叠注意力网络(SANs)其思想是用编码后的文本向量去扫描编码后的图像的每个区域,然后得到每个区域的注意力分数,将注意力分数乘到每个区域上,然后求和,得到一个图像表示向量,然后将图像表示向量与编码后的文本向量求和得到融合后的向量。可能这样说有点抽象,没关系,相信你看完下面的内容再回过头来看这句话就会醍醐灌顶!

论文地址

https://arxiv.org/pdf/1511.02274.pdf

模型讲解

0.模型总示意图

(1)论文给的模型图

Fig1:Stacked Attention Network for Image QA 

        这个图现在你看肯定是难以理解,那么可以暂时先看我给画的图,然后学完本篇博客再回过头看一下这张论文原图。 

(3)博主的模型图

        参考:下面这个博主画的图。

《Stacked Attention Networks for Image Question Answering》论文解读与实验_堆叠注意力网络-CSDN博客

Fig2:My Stacked Attention Network

        下面让我们逐步来讲解这张图,总共分为四个部分:提取图像特征、提取文本特征、SANs做特征的融合、线性层做分类输出。

1.提取图像特征

        图像这里我们依然使用一个预训练好的VGG19,不过与之前不同的是,现在我们的输入图像是[3,448,448],因此经过VGG19提取出来特征的大小应该是[512,14,14],然后我们将第二个维度和第三个维度进行View,变成[512,196],也就是这样图现在有196个区域,每个区域现在用512维的向量表示。

        然后原论文说为了和文本向量的特征维度相匹配,这里多加了一个线性层,将[512,196]变成[1024,196]。经过线性层后的矩阵我们记为:V_I = [v_1,v_2,...,v_{196}] , V_I \in R^{(1024,196)} , v_i \in R^{1024},其中v_i代表每个区域的特征向量,共196个区域。

                                                                Fig3:提取图像特征

2.提取文本特征

        文本这里我们首先会按照词表进行一次独热编码,然后将独热编码送入Embedding层,进行词嵌入编码,将原来的文字编码为向量,然后送入LSTM层,取LSTM层最后一个隐藏单元的输出作为文本的特征表示。具体计算如下:

 (1):独热编码

        假设我们有一个问题已经用独热编码编好了,记为:Q = [q_1,q_2,...,q_T] , q_i代表第i个词的独热编码。

(2):Embedding

        Embedding层会有一个词嵌入矩阵,使得:x_t = W_eq_t , x_t就是编码后的词向量表示。编码后的句子用x = [x_1,x_2,...,x_T]表示。

(3)LSTM

        将x_t,t=1,2,...,T 依次送入LSTM:h_t = LSTM(x_t)。将LSTM最后一个隐层状态作为输入问题的特征向量。即:V_q = h_T \in R^{1024}

                                                                Fig4:提取文本特征

3.SANs

        经过上面两步,我们已经拿到了图像的特征:V_I = [v_1,v_2,...,v_{196}]和文本的特征:V_Q。现在我们依次将V_IV_Q送入到两个线性层做一次映射然后再相加。公式如下:

Step1:
        h_A = tanh(W_IV_I⊕ W_QV_Q) , 其中:W_I \in R^{(k,1024)} , W_Q \in R^{(k,1024)} , ⊕是逐元素加法而不是普通矩阵加法。那么h_A \in R^{(k,196)}

Step2:

        然后我们再经过一个线性映射和Softmax将h_A映射为一个注意力特征分布。公式为:P_I = softmax((W_Ph_A + b_p)^T) = [p_1,p_2,...,p_{196}]\in R^{196} , W_P \in R^{(1,k)}。其中p_i \in R是图像第i个区域v_i的注意力分数。

Step3:

      然后将196个区域的v_i与它们对应的注意力分数p_i相乘求和。得到一个特征:\hat{V_I} = p_1v_1 + p_2v_2 + ... + p_{196}v_{196} \in R^{1024}

        然后将\hat{V_I}V_Q相加得到融合后的特征:u = \hat{V_I} + V_Q \in R^{1024}

                                                                     Fig5:SANs过程

        这只是一层的注意力,然后可以堆叠k层,也非常简单,我们上面不是已经拿到了u嘛,这个u我们就记为u_1,代表是第一次拿到的融合特征。然后让u_1V_I重复Setp1-Step3即可。具体迭代公式如下:

        h_A^k = tanh(W_I^kV_IW_Q^ku^{k-1}) \in R^{(k,196)} 其中u_0 := V_Q

        P_I^k = softmax(W_P^kh_A^k + b_p^k) \in R^{196}  

        \hat{V_I^k} = p_1^kv_1 + p_2^kv_2 + ... + p_{196}^kv_{196} \in R^{1024}

        u^k = \hat{V_I^k} + u^{k-1} \in R^{1024}

4.分类输出

        假设上面已经拿到了迭代k次后的融合特征u^k,然后我们的任务是做1000分类。令:p = softmax(W_uu^k + b_u) \in R^{1000} ,其中:W_u \in R^{(1000,1024)}。这个p就是作为最后的概率输出啦~~

代码

1.提取图像特征

#提取图像特征
class ImageEncoder(nn.Module):
    def __init__(self,embed_size):#embed_size = 本文的1024
        super(ImageEncoder, self).__init__()
        vgg19 = models.vgg19(pretrained=True).features #加载预训练模型的features部分 
        self.cnn = vgg19
        self.fc = nn.Linear(512,embed_size)
    def forward(self,image): #image_size = [batch_size , 3 , 448 , 448]
        with torch.no_grad():    #冻结cnn部分的参数
            features = self.cnn(image) #[batch_szie,512,14,14]
        features = features.view(features.size(0),512,-1) #[batch_size,512,196]
        V_I = self.fc(features) #[batch_size,1024,196]
        return V_I
    
    

2.提取文本特征

#提取文本特征代码
class QuestionEncoder(nn.Module):
    def __init__(self,qst_vocab_size, word_embed_size, embed_size, num_layers, hidden_size):
        super(QuestionEncoder, self).__init__()
        """
        参数解释:
        qst_vocab_size : 问题词汇表的大小
        word_embed_size : 问题经过Embedding层,嵌入词的维度。
        embed_size : 和ImageEncoder里面的embed_size一样
        num_layers : LSTM有多少层
        hidden_size : 每个隐藏层有多少个隐藏单元
        """
        self.embedding = nn.Embedding(qst_vocab_size,word_embed_size)
        self.tanh = nn.Tanh()
        self.lstm = nn.LSTM(word_embed_size, hidden_size, num_layers,batch_first=True)
        self.fc = nn.Linear(2*num_layers*hidden_size, embed_size)
    def forward(self,question):
        qst_vec = self.embedding(question) ## [batch_size, max_qst_length, word_embed_size]
        _,(hn,cn) = self.lstm(qst_vec) 
        """
        lstm有三个输出:
        out输出:nn.LSTM 的 forward 方法返回的第一个值 out 是 LSTM 在所有时间步的输出
        out 的形状为 (batch_size,seq_length, num_directions * hidden_size),其中 seq_len 是序列长度,
        batch 是批量大小,num_directions 是方向数量(单向为 1,双向为 2),hidden_size 是隐藏状态的大小。
        hn输出:lstm的最后一个时刻的隐层状态其形状为 (batch_size, num_layers * num_directions, hidden_size)
        cn输出:最后一个时间步的记忆细胞 cn (batch_size, num_layers * num_directions, hidden_size)。
        """
        qst_feature = torch.cat((hidden, cell), 2)   # [batch_size, num_layers=2, 2*hidden_size=1024]
        qst_feature = qst_feature.reshape(qst_feature.size()[0], -1)  # [batch_size, 2*num_layers*hidden_size=2048]
        V_Q = self.tanh(self.fc(qst_feature))                            # [batch_size, embed_size]
        return V_Q

3.SANs块

class SANsBlock(nn.Module):
    def __init__(self, num_channels, embed_size, dropout=True):
        """Stacked attention Module
        """
        super(SANsBlock, self).__init__()
        self.ff_image = nn.Linear(embed_size, num_channels) #在我们原理部分写的是embed_size = num_channels = 1024
        self.ff_questions = nn.Linear(embed_size, num_channels)
        self.dropout = nn.Dropout(p=0.5)
        self.ff_attention = nn.Linear(num_channels, 1)
    def forward(self, vi, vq):
        """Extract feature vector from image vector.

        """
        V_I = self.ff_image(vi) #[batch_size , 196 , num_channels]
        V_Q = self.ff_questions(vq).unsqueeze(dim=1)#[batch_size , 1 , num_channels] 为了能做按元素加法,我们在V_Q的第二个维度上增加了一个维度
        ha = torch.tanhV_I + V_Q)#[batch_size , 196 , num_channels]
        if self.dropout:
            ha = self.dropout(ha)
        ha = self.ff_attention(ha)#[batch_size , 196 , 1]
        # self.ha = ha
        pi = torch.softmax(ha, dim=1)#[batch_size , 196 , 1]
        vi_attended = (pi * vi).sum(dim=1)
        u = vi_attended + vq #[batch_size , 196 , 1]
        return u

4.模型汇总

class SANModel(nn.Module):
    # num_attention_layer and num_mlp_layer not implemented yet
    def __init__(self, embed_size, qst_vocab_size, ans_vocab_size, word_embed_size, num_layers, hidden_size): 
        super(SANModel, self).__init__()
        self.num_attention_layer = 2
        self.num_mlp_layer = 1
        self.img_encoder = ImgAttentionEncoder(embed_size)    # [batch_size, 196, embed_size]
        self.qst_encoder = QstEncoder(qst_vocab_size, word_embed_size, embed_size, num_layers, hidden_size)
        """
        下面这行代码将Attention这个模块重复num_attention_layer次
        """
        self.san = nn.ModuleList([SANsBlock(512, embed_size)]*self.num_attention_layer)
        self.tanh = nn.Tanh()
        self.mlp = nn.Linear(embed_size, ans_vocab_size)
        self.dropout = nn.Dropout(0.5)
        self.attn_features = []  ## attention features

    def forward(self, img, qst):

        img_feature = self.img_encoder(img)                     # [batch_size, 196 , embed_size]
        qst_feature = self.qst_encoder(qst)                     # [batch_size, embed_size]
        vi = img_feature
        qi = qst_feature
        for attn_layer in self.san:
            u = attn_layer(vi, qi)
#             self.attn_features.append(attn_layer.pi)
            
        combined_feature = self.mlp(self.dropout(u))
        return combined_feature

例子

        好了看完了原理部分和代码部分,相信你已经对堆叠注意力网络有了一定的了解。为了加深你的印象,下面我将会用一个简单的小例子带你走一遍前向传播(反向传播可以自己私下试试,无非就是梯度下降去更新那些参数)。

        假设我们现在有一张图像是[1,2,2],代表1通道,长和宽均为2。然后假设有一个文本“自行车上的篮子里有什么”。我们下面依次对图像和文本做嵌入:

对于图像:

        一个[1,2,2]的图像,假设经过VGG19变成了[2,3,3](这里我们用的维度较低方便计算),即两个通道,长和宽均为3。假设这个[2,3,3]的张量如下:

         [[[2,3,4], [4,5,6], [2,3,9]], [[5,6,7],[2,4,6],[7,9,6]]]

        然后我们经过View操作把上面这个张量变成[2,9],即共9个区域,每个区域用一个2维的向量表示。然后再经过一个线性层变为[2,9](这里为了计算方便我们就不去增加每个区域的维度了)。

        V_I = [v_1,...,v_{9}] = \begin{pmatrix} 2&3&4&4&5&6&2&3&9 \\ 5&6&7&2&4&8&7&9&6 \end{pmatrix} , 比如v_1 = [2,5]^T是图像第1个区域的特征向量。

对于文本:

        首先我们对文本进行分词,假设分词后的文本为:“自行车/上/的/篮子/里/有/什么/”,共7个词,然后经过独热编码的映射,记为Q = [q_1,q_2,...,q_7] 。然后我们将Q送入Embedding层,获得词嵌入的向量,然后送入LSTM。假设从LSTM最后一个隐藏层获取到的特征为:V_Q = [2,7]^T,它就代表了文本的特征。

对于SANs块:

        这里为了方便计算我们只计算一层的注意力,且偏置b都不要了,然后先省略一个线性层,让h_A = (V_IV_Q)  = \begin{pmatrix} 4&5&6&6&7&8&4&5&11 \\ 12&11&12&7&9&13&12&14&11 \end{pmatrix}

        假设初始化的W_P = [2,1] \in R^{(1,2)}  

        则P_I = Softmax((W_Ph_A)^T) = Softmax([20,21,24,19,23,29,20,24,33]^T)

                = [p_1,...,p_9]=['0.00000222', '0.00000603', '0.00012115', '0.00000082', '0.00004457', '0.01798085', '0.00000222', '0.00012115', '0.98172092']^T

        然后计算\hat{V_I} = p_1v_1 + ... + p_9v_9 , 再计算u = \hat{V_I} + V_Q即可。最后再将u经过线性层映射到分类的维度即可(这里给各位同学一些施展空间,后面的几步具体计算可以自己私下算一算~)。

写在后面

        本文到此也就结束了,不知道看完的同学有没有收获呢?如果这篇文章帮助到了你,请你点个赞再走吧~~~~

        后续会更新的内容有:双线性池化(Bilinear Pooling)、紧凑双线性池化(Compact Bilinear Pooling)、双线性池化与双线性模型、低秩双线性池化(Low-rank Bilinear Pooling)(这个要敲重点,因为MLBP这篇论文我觉得和SANs很像)。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值