©PaperWeekly 原創 · 作者|海晨威

學校|同濟大學碩士生

研究方向|自然語言處理

在 NLP 中,文本數據大都是變長的,爲了能夠做 batch 的訓練,需要 padding 到相同的長度,並在實際訓練中忽略 padding 部分的影響。

在不同的深度學習框架中,對變長序列的處理,本質思想都是一致的,但具體的實現方式有較大差異,下面 針對 Pytorch、Keras 和 TensorFlow 三大框架,以 LSTM 模型爲例,說明各框架對 NLP 中變長序列的處理方式和注意事項。

PyTorch

在 pytorch 中,是用的 torch.nn.utils.rnn 中的 pack_padded_sequence 和 pad_packed_sequence 來處理變長序列,前者可以理解爲對 padded 後的 sequence 做 pack(打包/壓緊),也就是去掉 padding 位,但會記錄每個樣本的有效長度信息;後者是逆操作,對 packed 後的 sequence 做 pad,恢復到相同的長度。

def pack_padded_sequence(input, lengths, batch_first=False, enforce_sorted=True):
    ...
    if enforce_sorted:
        sorted_indices = None
    else:
        lengths, sorted_indices = torch.sort(lengths, descending=True)
        sorted_indices = sorted_indices.to(input.device)
        batch_dim = 0 if batch_first else 1
        input = input.index_select(batch_dim, sorted_indices)
    ...

不過在使用過程中,要格外注意 pack_padded_sequence 的 enforce_sorted 參數和 pad_packed_sequence 的 total_length 參數。

1.1 pack_padded_sequence

下面是 pack_padded_sequence 函數的部分 Pytorch 源碼,input 就是輸入的一個 batch 的 tensor,lengths 是這個 batch 中每個樣本的有效長度。

在 pack_padded_sequence 處理之後,會得到一個 PackedSequence 的數據,其除了記錄 Tensor data 之外,還會記錄 batch_sizes, sorted_indices 和 unsorted_indices,其中 batch_sizes 是將輸入按照有效長度排序之後,每個時間步對應的 batch 大小,後面會有例子;sorted_indices 就是對輸入 lengths 排序後的索引,unsorted_indices 是用來將排序數據恢復到原始順序的索引。

在 pack_padded_sequence 中,enforce_sorted 默認設置爲 True,也就是說輸入的 batch 數據要事先按照長度排序,才能輸入,實際上,更簡單的方式是,將其設置爲 False,從上面的代碼中也可以看出,Pytorch 會自動給我們做排序。

注:torch1.1 及之後纔有 enforce_sorted 參數,因此 torch1.1 之後纔有自動排序功能。

一個簡單的例子:

# input_tensor shape:batch_size=2,time_step=3,dim=1
input_tensor = torch.FloatTensor([[4, 0, 0], [5, 6, 0]]).resize_(2, 3, 1)
seq_lens = torch.IntTensor([1, 2])
x_packed = nn_utils.rnn.pack_padded_sequence(input_tensor, seq_lens, batch_first=True, enforce_sorted=False)

輸出的 x_packed 爲:

PackedSequence(data=tensor([[5.],
        [4.],
        [6.]]), batch_sizes=tensor([2, 1]), sorted_indices=tensor([1, 0]), unsorted_indices=tensor([1, 0]))

在上面的例子中,首先,經過 pack_padded_sequence 內部按有效長度逆序排列之後,輸入數據會變成:

[[5, 6, 0],
[4, 0, 0]]

PackedSequence 中的 data 是按照 time_step 這個維度,也就是按列來記錄數據的,但是不包括 padding 位

該圖僅作爲理解參考,圖片來自:

https://www.cnblogs.com/lindaxin/p/8052043.html

batch_sizes 記錄的每列有幾個數據是有效的,也就是每列有效的 batch_size 長度,但是不包括爲 0 的長度,因此上面例子中,x_packed 的 batch_sizes=tensor([2, 1]),因此,每個 time_step 只需要傳入對應 batch_size 個數據即可,可以減少計算量。

要注意的是,batch_sizes 這個 tensor 的長度是 2,而 input_tensor 的 time_step 是 3,因爲 batch_sizes 不包含都是 padding 的時間步,也就是上面的第三列,因此後面的 pad_packed_sequence 要注意設置 total_length 參數。

1.2 pad_packed_sequence

下面是 pad_packed_sequence 函數的部分 Pytorch 源碼,輸入 sequence 是  PackedSequence 型數據。pad_packed_sequence 實際上就是做一個 padding 操作和根據索引恢復數據順序操作。

def pad_packed_sequence(sequence, batch_first=False, padding_value=0.0, total_length=None):
  max_seq_length = sequence.batch_sizes.size(0)
    if total_length is not None:
        max_seq_length = total_length
    ...

這裏要注意的一個參數是 total_length,它是 sequence 需要去被 padding 的長度,我們期望的一般都是 padding 到和輸入序列一樣的 time_step 長度 ,但是PackedSequence 型數據並沒有記錄這個數據,因此它用的是 sequence.batch_sizes.size(0),也就是 batch_sizes 這個 tensor 的長度。

上面已經提到,batch_sizes 不包含都是 padding 的時間步,這樣,如果整個 batch 中的每條記錄有都做padding,那 batch_sizes 這個 tensor 的長度就會小於 time_step ,就像上面代碼中的例子。

這時如果沒有設置 total_length,pad_packed_sequence 就不會 padding 到我們想要的長度。

可能你在實際使用時,不設置 total_length 參數也沒有出現問題,那大概率是因爲你的每個 batch 中,都有至少一條記錄沒有 padding 位,也就是它的每一步都是有效位,那 sequence.batch_sizes.size(0) 就等於 time_step。

1.3 使用方式

爲了方便使用,這裏將 pack_padded_sequence,LSTM 和 pad_packed_sequence 做了一個封裝,參數和原始 LSTM 一樣,唯一的區別是使用中要輸入 seq_lens 數據。

class MaskedLSTM(Module):
    def __init__(self, input_size, hidden_size, num_layers=1, bias=True, batch_first=False, dropout=0., bidirectional=False):
        super(MaskedLSTM, self).__init__()
        self.batch_first = batch_first
        self.lstm = LSTM(input_size, hidden_size, num_layers=num_layers, bias=bias,
             batch_first=batch_first, dropout=dropout, bidirectional=bidirectional)

    def forward(self, input_tensor, seq_lens):
        # input_tensor shape: batch_size*time_step*dim , seq_lens: (batch_size,)  when batch_first = True
        total_length = input_tensor.size(1) if self.batch_first else input_tensor.size(0)
        x_packed = pack_padded_sequence(input_tensor, seq_lens, batch_first=self.batch_first, enforce_sorted=False)
        y_lstm, hidden = self.lstm(x_packed)
        y_padded, length = pad_packed_sequence(y_lstm, batch_first=self.batch_first, total_length=total_length)
        return y_padded, hidden

小總結:

使用 pack_padded_sequence 和 pad_packed_sequence 之後,LSTM 輸出對應的 padding 位是全 0 的,隱藏層輸出 (h_n,c_n) 都是不受 padding 影響的,都是 padding 前最後一個有效位的輸出,而且對單向/雙向 LSTM 都是沒有影響的,因爲 padding 位不參與運算,即減少了不必要的計算,又避免了 padding 位對輸出的影響。

Keras

在 keras 中,自帶有 Masking 層,簡單方便,使用了一個 mask 操作則可以貫穿後面的整個模型,實際的過程是把一個布爾型的 mask 矩陣一直往下游傳遞下去,當然這個矩陣的維度會根據當前層的維度情況重新調整,以使其能在下游層中被使用。

確實方便,但也因此丟失了靈活性,如果使用了 mask,則後面層都要支持mask,否則會報異常,這對於一些不支持 mask 的層,例如 Flatten、AveragePooling1D 等等,並不是很友好。

keras 中對於變長序列的處理,一般使用 Masking 層,如果需要用到 Embedding 層,那可以直接在 Embedding 中設置 mask_zero=True,就不需要再加 Masking 層了,但本質上都是建了布爾型的 mask 矩陣並往下游傳遞下去。

下面是 Masking 和 Embedding 層的定義:

Masking(mask_value=0.,input_shape=(time_step,feature_size))
Embedding(input_dim, output_dim, mask_zero=False, input_length=None)

下面是 Embedding 層中的 mask 計算函數,如果 mask_zero 設置爲 True,那這裏會計算 mask 矩陣並往後傳遞,如果要繼續深入其傳遞的機制,建議看 keras 源碼,也可以參考一下這個:keras 源碼分析之 Layer [1]

# Embedding 層中的mask計算函數
def compute_mask(self, inputs, mask=None):
    if not self.mask_zero:
        return None
    output_mask = K.not_equal(inputs, 0)
    return output_mask

不過要注意的一點是,mask_zero 設置爲 True,輸入通過 Embedding 後,padding 位所對應的向量並不是全 0,仍然是一個隨機的向量,和 mask_zero 的值沒有關係,mask_zero 只是影響是否計算 mask 矩陣。但是有了 mask 矩陣之後,padding 位都不會被計算,因此,其對應向量的值並不重要。

2.1 使用方式

input = keras.layers.Input((time_step,feature_size))
mask = keras.layers.Masking(mask_value=0, input_shape=(time_step,feature_size))(input)
lstm_output = keras.layers.LSTM(hidden_size, return_sequences=True)(mask)
model = Model(input, lstm_output)

或:

input = keras.layers.Input((time_step,))
embed = keras.layers.Embedding(vocab_size, embedding_size, mask_zero=True)(input)
lstm_output = keras.layers.LSTM(hidden_size, return_sequences=True)(emd)
model = Model(input, lstm_output)

keras 模型中,Masking 之後的層,只要支持 mask,都不用再手動創建 mask 了,當然,如果是自己定義的層,要支持 mask,需要設置 supports_masking=True,並實現自己的 compute_mask 函數。

要注意的是,和 pytorch、TF 有些不一樣的地方,對於有了 Masking 層之後的 LSTM,padding 位的輸出不會是全 0,而是最後一位有效位的輸出,也就是 padding 位輸出都複製了最後有效位的輸出。

Embedding 層和 Masking 層都有 mask 功能,但與 Masking 層不同的是,Embedding 它只能過濾 0,不能指定其他字符。

TensorFlow

在 TF (tf 1.x) 中是通過 dynamic_rnn 來實現變長序列的處理,它和 pytorch 的 pack_padded_sequence 一樣,也有 sequence_length 參數,但它相對比 pytorch 更方便,不用手動去 pack 和 pad,只要傳遞 sequence_length 參數,其他都由 dynamic_rnn 來完成。

但是 TF 中 dynamic_rnn 計算的循環次數仍然是 time_steps 次,並沒有帶來計算效率上的提升。sequence length 的作用只是在每個序列達到它的實際長度後,把後面時間步的輸出全部置成零、狀態全部置成實際長度那個時刻的狀態。

這一點可以參考:

https://www.zhihu.com/question/52200883

3.1 使用方式

# 靜態圖定義部分
basic_cell = tf.nn.rnn_cell.LSTMCell(hidden_size)
X = tf.placeholder(tf.float32, shape=[None, time_step, dim])
seq_length = tf.placeholder(tf.int32, [None])
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32, sequence_length=seq_length)

以上是從應用和代碼的角度,介紹了 Pytorch、Keras 和 TensorFlow 三大框架對變長數據的處理和使用方式,但這不僅僅適用於 NLP 領域,只是在 NLP 中變長數據更爲常見,希望能幫助你在工程實踐中更好地去處理變長的數據。

參考文獻

[1]https://blog.csdn.net/u012526436/article/details/98206560

更多閱讀

# 投 稿 通 道 #

讓你的論文被更多人看到 

如何才能讓更多的優質內容以更短路徑到達讀者羣體,縮短讀者尋找優質內容的成本呢? 答案就是:你不認識的人。

總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成爲一座橋樑,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。 

PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是 最新論文解讀 ,也可以是 學習心得技術乾貨 。我們的目的只有一個,讓知識真正流動起來。

:memo:  來稿標準:

• 稿件確係個人 原創作品 ,來稿需註明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向) 

• 如果文章並非首發,請在投稿時提醒並附上所有已發佈鏈接 

• PaperWeekly 默認每篇文章都是首發,均會添加“原創”標誌

:mailbox_with_mail:  投稿郵箱:

• 投稿郵箱: [email protected] 

• 所有文章配圖,請單獨在附件中發送 

• 請留下即時聯繫方式(微信或手機),以便我們在編輯發佈時和作者溝通

:mag:

現在,在 「知乎」 也能找到我們了

進入知乎首頁搜索 「PaperWeekly」

點擊 「關注」 訂閱我們的專欄吧

關於PaperWeekly

PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公衆號後臺點擊 「交流羣」 ,小助手將把你帶入 PaperWeekly 的交流羣裏。

相關文章