1. 程式人生 > >摒棄encoder-decoder結構,Pervasive Attention模型與Keras實現

摒棄encoder-decoder結構,Pervasive Attention模型與Keras實現

1.引言

    現有的主流機器翻譯模型,基本都是基於encoder-decoder的結構,其思想就是對於輸入句子序列,通過RNN先進行編碼(encoder),轉化為一個上下文向量context vector,然後利用另一個RNN對上下文向量context vector進行解碼(decoder)。其結構如下:

    之後,又有學者在該結構的基礎上,做了各種改進,其中主要有兩方面的改進,一種是添加了注意力機制,其思想就是在decoder的每一步輸出時,不再僅僅關注encoder最後的輸出狀態,而是綜合encoder每一個時間步的輸出狀態,並對其進行加權求和,從而使得在decoder的每一個時間步輸出時,可以對輸入句子序列中的個別詞彙有所側重,進而提高模型的準確率,另一種改進是替換encoder和decoder的RNN模型,比如Facebook提出的Fairseq模型,該模型在encoder和decoder都採用卷積神經網路模型,以及Googlet提出的Transformer模型,該模型在encoder和decoder都採用attention,這兩種模型的出發點都是為了解決RNN沒法平行計算的缺點,因此,在訓練速度上得到了很大的提升。但是,這些改進其實都沒有脫離encoder-decoder的結構。

    因此,《Pervasive Attention: 2D Convolutional Neural Networks for Sequence-to-Sequence Prediction》一文作者提出了一種新的結構,不再使用encoder-decoder的結構,而是採用一種基於2D卷積神經網路(2D CNN)的結構,其思想就是將輸入序列和目標序列embedding之後的矩陣進行拼接,轉化為一個3D的矩陣,然後直接使用卷積網路進行卷積操作,其卷積網路的結構採用的是DenseNet的結構,並且在進行卷積時對卷積核進行遮掩,以防止在卷積時後續資訊的流入,拼接後的feature map如下圖所示:

2.相關符號定義

    論文中涉及到的相關符號及其定義分別如下:

  • (s,t)(s,t):輸入序列和目標序列句子對
  • \left | s \right |:輸入序列長度
  • \left | t \right |:目標序列長度
  • d_s:輸入序列embedding的維度
  • d_t:目標序列embedding的維度
  • \left \{ x_1,x_2,...,x_\left | s \right | \right \}:已經經過embedding的輸入序列矩陣
  • \left \{ y_1,y_2,...,y_\left | t \right | \right \}:已經經過embedding的目標序列矩陣
  • g:growth rate,與DenseNet中的growth rate含義相同,是每個dense layer的輸出維度

3. pervasive attention模型介紹

    pervasive attention模型的結構主要還是借鑑DenseNet的結構,在結構方面其實並沒有多新奇,其主要特別的地方是將輸入序列和目標序列的資料進行融合,轉化為一個3D的矩陣,從而可以避開encoder-decoder的結構,下面對該模型具體展開介紹。

3.1 模型的輸入(Input source-target tensor)

   首先是模型的輸入,記\left \{ x_1,x_2,...,x_\left | s \right | \right \}\left \{ y_1,y_2,...,y_\left | t \right | \right \}分別表示輸入序列和目標序列經過embedding後的二維矩陣,其中\left | s \right |表示輸入序列的長度,\left | t \right |表示目標序列的長度,d_s表示輸入序列embedding後的維度,d_t表示目標序列embedding後的維度。接著,將這兩個矩陣進行拼接,得到一個三維的矩陣,記為X\in \mathbb{R}^\left | t \right |\times \left | s \right |\times f_0,其中f_0=d_t+d_sX_i_j=[y_i, x_j]。這裡有一個地方需要注意的是,作者在論文中是將資料轉化為\left | t \right |\times \left | s \right |\times f_0的形式,這時,在後面的卷積操作時,卷積核的mask就應該是對行方向進行mask,而不是上圖顯示的列方向。

    另外,筆者在檢視作者原始碼時,發現其實在將資料進行拼接之前,作者其實還做了一個conv embedding的操作,即對embedding後的輸入序列和輸出序列矩陣進行1維的卷積操作,這樣使得後面每個單詞其實都可以融合前一個單詞的資訊。

3.2 卷積層(Convolutional layers)

   該論文中卷積層的結構主要參考的DenseNet的dense block結構,在每一個卷積block中,都包含以下7層:

  • Batch_normalizes:第一層標準化層,對輸入資料進行batch標準化
  • ReLU:第一層啟用層
  • Conv(1):第一層卷積層,採用(1,1)的卷積核,輸入的通道數是f_0+(l-1)g,其中,l表示當前的層數,l\in \left \{ 1,...,L \right \}L為dense layer的層數,g稱作growth rate。因為採用的是DenseNet的結構,因此,需要將當前層前面的l-1層的輸出作為附加的通道數與Input一起拼接。第一層卷積操作後的輸出通道數設定為4g
  • Batch_normalizes:第二層標準化層
  • ReLU:第二層啟用層
  • Conv(k):第二層卷積層,採用\left ( k,\left \lceil \frac{k}{2} \right \rceil \right )的卷積核,輸出通道數是g
  • dropout:dropout層 

具體的模型結構如下圖所示:

    不過需要注意的是,筆者在檢視作者原始碼時,發現其實在最開始的Input與dense layer之間,其實還有一層DenseNet的transition操作,即對輸入資料進行卷積,使得通道數減半,這樣在後續的卷積操作時,資料量不會太大。

3.3 輸出層(Target sequence prediction)

    卷積層結束後,記模型的輸出為H^l\in \mathbb{R}^\left | t \right |\times \left | s \right |\times f_l,其中f_l為輸出的通道數,由於輸出的是一個3維的結構,因此,需要對第2維進行摺疊,使其轉化為\left | t \right |\times f_l的形式,這裡作者介紹了兩種主要的操作方法,分別是pooling和注意力機制: 

  • pooling:可以選擇max_pooling或average_pooling,其計算方式分別如下:                                                                                                                                                                    這裡主要需要注意的是做average_pooling時,作者不是直接計算平均,而是除以句子長度的開根號,作者通過實驗發現這種做法效果更好,並且作者在實驗時發現用max_pooling效果要比average_pooling好。
  • 注意力機制:與傳統的注意力機制一樣操作,這裡不具體展開細講了

    得到摺疊後的結果後,再將結果傳入一個全連線層,使得輸出的size轉化為 \left | t \right |\times \nu,這裡\nu是目標序列的詞彙總數,並將結果傳入一個softmax層即可得到最終的概率分佈,計算如下:

                                                                p_i=SoftMax(EH_i^p^o^o^l)

4.pervasive attention的keras實現

    筆者用keras框架對pervasive attention進行了復現,下面對主要的程式碼模組按照上面介紹的模型結構進行講解。首先匯入相關的依賴庫和函式。程式碼如下:

from keras.layers import Input, Embedding, \
    Lambda, Concatenate, BatchNormalization, \
    Conv2D, Dropout, Dense, MaxPool2D, ZeroPadding2D, \
    AveragePooling2D, ZeroPadding1D
from keras.layers import Activation, TimeDistributed, Conv1D
from keras.models import Model
import keras.backend as K
from keras import optimizers

       接著是模型的Input中的embedding部分,其中max_enc_len表示輸入序列的最大長度,max_dec_len表示目標序列的最大長度,src_word_num表示輸入序列的詞彙數,tgt_word_num表示目標序列的詞彙數,這裡+2是為了新增<UNK>、<PAD>兩個特殊字元。另外,這裡加了一個conv embedding,作者在論文中沒有提及,但是原始碼裡面其實是含有這一層,筆者發現加了conv embedding後,每個單詞可以融合前一個單詞的資訊,有助於提升模型的效果,這裡conv embedding的思想其實類似Fairseq的思想。

# Inputs
src_input = Input(shape=(max_enc_len,), name='src_input')
tgt_input = Input(shape=(max_dec_len,), name='tgt_input')

# embedding
src_embedding = Embedding(src_word_num + 2,
                          embedding_dim,
                          name='src_embedding')(src_input)
tgt_embedding = Embedding(tgt_word_num + 2,
                          embedding_dim,
                          name='tgt_embedding')(tgt_input)

# implement a convEmbedding
for i in range(conv_emb_layers):
    src_embedding = Conv1D(embedding_dim, 3, padding='same',
                           data_format='channels_last', activation='relu')(src_embedding)
    tgt_embedding = ZeroPadding1D(padding=(2, 0))(tgt_embedding)
    tgt_embedding = Conv1D(embedding_dim, 3, padding='valid',
                           data_format='channels_last', activation='relu')(tgt_embedding)

     然後對embedding之後的資料進行拼接,使其轉化為一個3D的結構,這裡筆者的程式碼與作者有點不一樣的地方是將資料轉化為X\in \mathbb{R}^\left | s \right |\times \left | t \right |\times f_0的形式,這樣方便後面的卷積mask操作,本質上是一樣的。

def src_reshape_func(src_embedding, repeat):
    """
    對embedding之後的source sentence的tensor轉換成pervasive-attention model需要的shape
    arxiv.org/pdf/1808.03867.pdf
    :param src_embedding: source sentence embedding之後的結果[tensor]
    :param repeat: 需要重複的次數, target sentence t的長度[int]
    :return: 2D tensor (?, s, t, embedding_dim)
    """
    input_shape = src_embedding.shape
    src_embedding = K.reshape(src_embedding, [-1, 1, input_shape[-1]])
    src_embedding = K.tile(src_embedding, [1, repeat, 1])
    src_embedding = K.reshape(src_embedding, [-1, input_shape[1], repeat, input_shape[-1]])

    return src_embedding


def tgt_reshape_func(tgt_embedding, repeat):
    """
    對embedding之後的target sentence的tensor轉換成pervasive-attention model需要的shape
    arxiv.org/pdf/1808.03867.pdf
    :param tgt_embedding: target sentence embedding之後的結果[tensor]
    :param repeat: 需要重複的次數, source sentence s的長度[int]
    :return: 2D tensor (?, s, t, embedding_dim)
    """
    input_shape = tgt_embedding.shape
    tgt_embedding = K.reshape(tgt_embedding, [-1, 1, input_shape[-1]])
    tgt_embedding = K.tile(tgt_embedding, [1, repeat, 1])
    tgt_embedding = K.reshape(tgt_embedding, [-1, input_shape[1], repeat, input_shape[-1]])
    tgt_embedding = K.permute_dimensions(tgt_embedding, [0, 2, 1, 3])

    return tgt_embedding

def src_embedding_layer(src_embedding, repeat):
    """
    轉換成Lambda層
    :param src_embedding: source sentence embedding之後的結果[tensor]
    :param repeat: 需要重複的次數, target sentence t的長度[int]
    :return: 2D tensor (?, s, t, embedding_dim)
    """
    return Lambda(src_reshape_func,
                  arguments={'repeat': repeat})(src_embedding)


def tgt_embedding_layer(tgt_embedding, repeat):
    """
    轉換層Lambda層
    :param tgt_embedding: target sentence embedding之後的結果[tensor]
     :param repeat: 需要重複的次數, target sentence t的長度[int]
    :return: 2D tensor (?, s, t, embedding_dim)
    """
    return Lambda(tgt_reshape_func,
                  arguments={'repeat': repeat})(tgt_embedding)

# concatenate
src_embedding = src_embedding_layer(src_embedding, repeat=max_dec_len)
tgt_embedding = tgt_embedding_layer(tgt_embedding, repeat=max_enc_len)
src_tgt_embedding = Concatenate(axis=3)([src_embedding, tgt_embedding])

    拼接操作後, 為了避免後續卷積時資料太大,並且預測過多地依賴模型的初始資訊,先將資料進行一次卷積操作,使得資料的通道數減半,這裡conv2_filters即為卷積後的通道數,筆者設為原資料embedding維度大小。

# densenet conv1 1x1
x = Conv2D(conv1_filters, 1, strides=1)(src_tgt_embedding)
x = BatchNormalization(axis=3, epsilon=1.001e-5)(x)
x = Activation('relu')(x)
x = MaxPool2D((2, 1), strides=(2, 1))(x)

    接下來是模型的卷積層部分,採用的是DenseNet的結構,由於句子比較長,因此,筆者在transition函式裡做了一點修改,即每次transition操作對輸入序列的維度進行降維,採用的是pooling操作,使得每次輸入序列的維度可以不斷下降,而更多的空間給通道數的增加,這裡transition操作是一個可選操作,作者在論文中沒講,但是DenseNet原始的結構是有這一個操作的。另外,在卷積操作時,原作者是對卷積核的權重進行mask,比如卷積核為\left ( 3,3 \right )時,直接對最後一列變為0,從而保證非法資訊不會被傳入,但是這裡筆者直接採用\left ( 3,2 \right )的卷積核,並對資料進行左padding兩列,這樣就不用重寫卷積層了。

# transition layer
def transition_block(x,
                     reduction):
    """A transition block.
    該transition block與densenet的標準操作不一樣,此處不包括pooling層
    pervasive-attention model中的transition layer需要保持輸入tensor
    的shape不變 arxiv.org/pdf/1808.03867.pdf
    # Arguments
        x: input tensor.
        reduction: float, the rate of feature maps need to retain.

    # Returns
        output tensor for the block.
    """
    x = BatchNormalization(axis=3, epsilon=1.001e-5)(x)
    x = Activation('relu')(x)
    x = Conv2D(int(K.int_shape(x)[3] * reduction), 1, use_bias=False)(x)

    x = MaxPool2D((2, 1), strides=(2, 1))(x)

    return x


# building block
def conv_block(x,
               growth_rate,
               dropout):
    """A building block for a dense block.
    該conv block與densenet的標準操作不一樣,此處通過
    增加Zeropadding2D層實現論文中的mask操作,並將
    Conv2D的kernel size設定為(3, 2)
    # Arguments
        x: input tensor.
        growth_rate: float, growth rate at dense layers.
        dropout: float, dropout rate at dense layers.

    # Returns
        Output tensor for the block.
    """
    x1 = BatchNormalization(axis=3,
                            epsilon=1.001e-5)(x)
    x1 = Activation('relu')(x1)
    x1 = Conv2D(4 * growth_rate, 1, use_bias=False)(x1)
    x1 = BatchNormalization(axis=3, epsilon=1.001e-5)(x1)
    x1 = Activation('relu')(x1)
    x1 = ZeroPadding2D(padding=((1, 1), (1, 0)))(x1)  # mask sake
    x1 = Conv2D(growth_rate, (3, 2), padding='valid')(x1)
    x1 = Dropout(rate=dropout)(x1)

    x = Concatenate(axis=3)([x, x1])

    return x


# dense block
def dense_block(x,
                blocks,
                growth_rate,
                dropout):
    """A dense block.

    # Arguments
        x: input tensor.
        blocks: integer, the number of building blocks.
        growth_rate:float, growth rate at dense layers.
        dropout: float, dropout rate at dense layers.

    # Returns
        output tensor for the block.
    """
    for i in range(blocks):
        x = conv_block(x, growth_rate=growth_rate, dropout=dropout)

    return x

# densenet 4 dense block
if len(blocks) == 1:
    x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)
else:
    for i in range(len(blocks) - 1):
        x = dense_block(x, blocks=blocks[i], growth_rate=growth_rate, dropout=dropout)
        x = transition_block(x, reduction)
    x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)

   卷積操作結束後,是模型的pooling操作,對s維度進行摺疊,這裡筆者只寫了pooling操作。

# avg pooling
def h_avg_pooling_layer(h):
    """
    實現論文中提到的最大池化 arxiv.org/pdf/1808.03867.pdf
    :param h: 由densenet結構輸出的shape為(?, s, t, fl)的tensor[tensor]
    :return: (?, t, fl)
    """
    h = Lambda(lambda x: K.permute_dimensions(x, [0, 2, 1, 3]))(h)
    h = AveragePooling2D(data_format='channels_first',
                         pool_size=(h.shape[2], 1))(h)
    h = Lambda(lambda x: K.squeeze(x, axis=2))(h)

    return h


# max pooling
def h_max_pooling_layer(h):
    """
    實現論文中提到的最大池化 arxiv.org/pdf/1808.03867.pdf
    :param h: 由densenet結構輸出的shape為(?, s, t, fl)的tensor[tensor]
    :return: (?, t, fl)
    """
    h = Lambda(lambda x: K.permute_dimensions(x, [0, 2, 1, 3]))(h)
    h = MaxPool2D(data_format='channels_first',
                  pool_size=(h.shape[2], 1))(h)
    h = Lambda(lambda x: K.squeeze(x, axis=2))(h)

    return h

# Max pooling
h = h_max_pooling_layer(x)

    最後是模型的輸出,是一個全連線層+softmax層,這裡沒什麼好講的,程式碼如下:

# Max pooling
h = h_max_pooling_layer(x)

# Target sequence prediction
output = Dense(tgt_word_num + 2, activation='softmax')(h)

    以上對整個模型各個模組程式碼分別進行了講解,最後,將上面的程式碼串聯起來,彙總如下:

# pervasive-attention model
def pervasive_attention(blocks,
                        conv1_filters=64,
                        growth_rate=12,
                        reduction=0.5,
                        dropout=0.2,
                        max_enc_len=200,
                        max_dec_len=200,
                        embedding_dim=128,
                        src_word_num=4000,
                        tgt_word_num=4000,
                        samples=12000,
                        batch_size=8,
                        conv_emb_layers=6
                        ):
    """
    build a pervasive-attention model with a densenet-like cnn structure.

    :param blocks: a list with length 4, indicates different number of
        building blocks in 4 dense blocks, e.g which [6, 12, 48, 32]
        for DenseNet201 and [6, 12, 32, 32] for DenseNet169. [list]
    :param conv1_filters: the filters used in first 1x1 conv to
        reduce the channel size of embedding input. [int]
    :param growth_rate: float, growth rate at dense layers. [int]
    :param reduction: float, the rate of feature maps which
        need to retain after transition layer. [float]
    :param dropout: dropout rate used in each conv block, default 0.2. [float]
    :param max_enc_len: the max len of source sentences. [int]
    :param max_dec_len: the max len of target sentences. [int]
    :param embedding_dim: the hidden units of first two embedding layers. [int]
    :param src_word_num: the vocabulary size of source sentences. [int]
    :param tgt_word_num: the vocabulary size of target sentences. [int]
    :param samples: the size of the training data. [int]
    :param batch_size: batch size. [int]
    :param conv_emb_layers: the layers of the convolution embedding. [int]
    :return:
    """
    # Inputs
    src_input = Input(shape=(max_enc_len,), name='src_input')
    tgt_input = Input(shape=(max_dec_len,), name='tgt_input')

    # embedding
    src_embedding = Embedding(src_word_num + 2,
                              embedding_dim,
                              name='src_embedding')(src_input)
    tgt_embedding = Embedding(tgt_word_num + 2,
                              embedding_dim,
                              name='tgt_embedding')(tgt_input)
    # implement a convEmbedding
    for i in range(conv_emb_layers):
        src_embedding = Conv1D(embedding_dim, 3, padding='same',
                               data_format='channels_last', activation='relu')(src_embedding)
        tgt_embedding = ZeroPadding1D(padding=(2, 0))(tgt_embedding)
        tgt_embedding = Conv1D(embedding_dim, 3, padding='valid',
                               data_format='channels_last', activation='relu')(tgt_embedding)

    # concatenate
    src_embedding = src_embedding_layer(src_embedding, repeat=max_dec_len)
    tgt_embedding = tgt_embedding_layer(tgt_embedding, repeat=max_enc_len)
    src_tgt_embedding = Concatenate(axis=3)([src_embedding, tgt_embedding])

    # densenet conv1 1x1
    x = Conv2D(conv1_filters, 1, strides=1)(src_tgt_embedding)
    x = BatchNormalization(axis=3, epsilon=1.001e-5)(x)
    x = Activation('relu')(x)
    x = MaxPool2D((2, 1), strides=(2, 1))(x)

    # densenet 4 dense block
    if len(blocks) == 1:
        x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)
    else:
        for i in range(len(blocks) - 1):
            x = dense_block(x, blocks=blocks[i], growth_rate=growth_rate, dropout=dropout)
            x = transition_block(x, reduction)
        x = dense_block(x, blocks=blocks[-1], growth_rate=growth_rate, dropout=dropout)

    # Max pooling
    h = h_max_pooling_layer(x)

    # Target sequence prediction
    output = Dense(tgt_word_num + 2, activation='softmax')(h)

    # compile
    model = Model([src_input, tgt_input], [output])
    adam = optimizers.Adam(lr=0.0001,
                           beta_1=0.9,
                           beta_2=0.999,
                           epsilon=1e-08,
                           decay=0.05 * batch_size / samples)
    model.compile(optimizer=adam, loss='categorical_crossentropy')

    return model

5.小結

    以上就是pervasive attention模型的整體結構及其復現,其實整個模型的思路都不算太難,下面談一談筆者自己對這個模型的一個感受吧:

  • 優點:①拋棄了以往的encoder和decoder結構,可以直接採用卷積操作進行計算,從而實現並行化;②引數量整體比seq2seq要少很多;③可以在每一層的結果實現attention,這也是模型為什麼叫pervasive attention的原因。
  • 缺點:該模型由於對輸入序列和目標序列的資料進行拼接,當序列的長度比較長時,對GPU的記憶體要求就很高,特別是當層數和growth rate比較大時,對GPU的效能要求就特別大。

    最後,附上原論文的地址和作者原始碼的地址:

  • 論文地址:arxiv.org/pdf/1808.03867.pdf
  • Pytorch實現:github.com/elbayadm/attn2d