- 原文地址:RECURRENT NEURAL NETWORKS (RNN) – PART 2: TEXT CLASSIFICATION
- 原文作者:GokuMohandas
- 譯文出自:掘金翻譯計劃
- 本文永久鏈接:github.com/xitu/gold-m…
- 譯者:Changkun Ou
- 校對者:yanqiangmiffy, TobiasLee
本系列文章匯總
- RNN 循環神經網絡系列 1:基本 RNN 與 CHAR-RNN
- RNN 循環神經網絡系列 2:文本分類
- RNN 循環神經網絡系列 3:編碼、解碼器
- RNN 循環神經網絡系列 4:注意力機制
- RNN 循環神經網絡系列 5:自定義單元
RNN 循環神經網絡系列 2:文本分類
在第一篇文章中,我們看到了如何使用 TensorFlow 實現一個簡單的 RNN 架構。現在我們將使用這些組件并將其應用到文本分類中去。主要的區別在于,我們不會像 CHAR-RNN 模型那樣輸入固定長度的序列,而是使用長度不同的序列。
文本分類
這個任務的數據集選用了來自 Cornell 大學的語句情緒極性數據集 v1.0,它包含了 5331 個正面和負面情緒的句子。這是一個非常小的數據集,但足夠用來演示如何使用循環神經網絡進行文本分類了。
我們需要進行一些預處理,主要包括標注輸入、附加標記(填充等)。請參考完整代碼了解更多。
預處理步驟
- 清洗句子并切分成一個個 token;
- 將句子轉換為數值 token;
- 保存每個句子的序列長。
如上圖所示,我們希望在計算完成時立即對句子的情緒做出預測。引入額外的填充符會帶來過多噪聲,這樣的話你模型的性能就會不太好。注意:我們填充序列的唯一原因是因為需要以固定大小的批量輸入進 RNN。下面你會看到,使用動態 RNN 還能避免在序列完成后的不必要計算。
模型
代碼:
class model(object):def __init__(self, FLAGS):# 占位符self.inputs_X = tf.placeholder(tf.int32,shape=[None, None], name='inputs_X')self.targets_y = tf.placeholder(tf.float32,shape=[None, None], name='targets_y')self.dropout = tf.placeholder(tf.float32)# RNN 單元stacked_cell = rnn_cell(FLAGS, self.dropout)# RNN 輸入with tf.variable_scope('rnn_inputs'):W_input = tf.get_variable("W_input",[FLAGS.en_vocab_size, FLAGS.num_hidden_units])inputs = rnn_inputs(FLAGS, self.inputs_X)#initial_state = stacked_cell.zero_state(FLAGS.batch_size, tf.float32)# RNN 輸出seq_lens = length(self.inputs_X)all_outputs, state = tf.nn.dynamic_rnn(cell=stacked_cell, inputs=inputs,sequence_length=seq_lens, dtype=tf.float32)# 由于使用了 seq_len[0],state 自動包含了上一次的對應輸出# 因為 state 是一個帶有張量的元組outputs = state[0]# 處理 RNN 輸出with tf.variable_scope('rnn_softmax'):W_softmax = tf.get_variable("W_softmax",[FLAGS.num_hidden_units, FLAGS.num_classes])b_softmax = tf.get_variable("b_softmax", [FLAGS.num_classes])# Logitslogits = rnn_softmax(FLAGS, outputs)probabilities = tf.nn.softmax(logits)self.accuracy = tf.equal(tf.argmax(self.targets_y,1), tf.argmax(logits,1))# 損失函數self.loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits, self.targets_y))# 優化self.lr = tf.Variable(0.0, trainable=False)trainable_vars = tf.trainable_variables()# 使用梯度截斷來避免梯度消失和梯度爆炸grads, _ = tf.clip_by_global_norm(tf.gradients(self.loss, trainable_vars), FLAGS.max_gradient_norm)optimizer = tf.train.AdamOptimizer(self.lr)self.train_optimizer = optimizer.apply_gradients(zip(grads, trainable_vars))# 下面是用于采樣的值# (在每個單詞后生成情緒)# 取所有輸出作為第一個輸入序列# (由于采樣,只需一個輸入序列)sampling_outputs = all_outputs[0]# Logitssampling_logits = rnn_softmax(FLAGS, sampling_outputs)self.sampling_probabilities = tf.nn.softmax(sampling_logits)# 保存模型的組件self.global_step = tf.Variable(0, trainable=False)self.saver = tf.train.Saver(tf.all_variables())def step(self, sess, batch_X, batch_y=None, dropout=0.0,forward_only=True, sampling=False):input_feed = {self.inputs_X: batch_X,self.targets_y: batch_y,self.dropout: dropout}if forward_only:if not sampling:output_feed = [self.loss,self.accuracy]elif sampling:input_feed = {self.inputs_X: batch_X,self.dropout: dropout}output_feed = [self.sampling_probabilities]else: # 訓練output_feed = [self.train_optimizer,self.loss,self.accuracy]outputs = sess.run(output_feed, input_feed)if forward_only:if not sampling:return outputs[0], outputs[1]elif sampling:return outputs[0]else: # 訓練return outputs[0], outputs[1], outputs[2]復制代碼
上面的代碼就是我們的模型代碼,它在訓練的過程中使用了輸入的文本。注意:為了清楚起見,我們決定將批量數據的大小保存在我們的輸入和目標占位符中,但是我們應該讓它們獨立于一個特定的批量大小之外。由于這個特定的批量大小依賴于 batch_size
,如果我們這么做,那么我們就還得輸入一個 initial_state
。我們通過嵌入他們來為每個數據序列來輸入 token。實踐策略表明,我們在輸入文本上使用 skip-gram 模型預訓練嵌入權重能夠取得更好的性能。
在此模型中,我們再次使用 dynamic_rnn
,但是這次我們提供了sequence_length
參數的值,它是一個包含每個序列長度的列表。這樣,我們就可以避免在輸入序列的最后一個詞之后進行的不必要的計算。length
函數就用來獲取這個列表的長度,如下所示。當然,我們也可以在外面計算seq_len
,再通過占位符進行傳遞。
def length(data):relevant = tf.sign(tf.abs(data))length = tf.reduce_sum(relevant, reduction_indices=1)length = tf.cast(length, tf.int32)return length復制代碼
由于我們填充符 token 為 0,因此可以使用每個 token 的 sign 性質來確定它是否是一個填充符 token。如果輸入大于 0,則 tf.sign
為 1;如果輸入為 0,則為 tf.sign
為 0。這樣,我們可以逐步通過列索引來獲得 sign 值為正的 token 數量。至此,我們可以將這個長度提供給 dynamic_rnn
了。
注意:我們可以很容易地在外部計算 seq_lens
,并將其作為占位符進行傳參。這樣我們就不用依賴于 PAD_ID = 0
這個性質了。
一旦我們從 RNN 拿到了所有的輸出和最終狀態,我們就會希望分離對應輸出。對于每個輸入來說,將具有不同的對應輸出,因為每個輸入長度不一定不相同。由于我們將 seq_len
傳給了 dynamic_rnn
,而 state
又是最后一個對應輸出,我們可以通過查看 state
來找到對應輸出。注意,我們必須取 state[0]
,因為返回的 state
是一個張量的元組。
其他需要注意的事情:我并沒有使用 initial_state
,而是直接給 dynamic_rnn
設置 dtype
。此外,dropout
將根據 forward_only
與否,作為參數傳遞給 step()
。
推斷
總的來說,除了單個句子的預測外,我還想為具有一堆樣本句子整體情緒進行預測。我希望看到的是,每個單詞都被 RNN 讀取后,將之前的單詞分值保存在內存中,從而查看預測分值是怎樣變化的。舉例如下(值越接近 0 表明越靠近負面情緒):
注意:這是一個非常簡單的模型,其數據集非常有限。主要目的只是為了闡明它是如何搭建以及如何運行的。為了獲得更好的性能,請嘗試使用數據量更大的數據集,并考慮具體的網絡架構,比如 Attention 模型、Concept-Aware 詞嵌入以及隱喻(symbolization to name)等等。
損失屏蔽(這里不需要)
最后,我們來計算 cost。你可能會注意到我們沒有做任何損失屏蔽(loss masking)處理,因為我們分離了對應輸出,僅用于計算損失函數。然而,對于其他諸如機器翻譯的任務來說,我們的輸出很有可能還來自填充符 token。我們不想考慮這些輸出,因為傳遞了 seq_lens
參數的 dynamic_rnn
將返回 0。下面這個例子比較簡單,只用來說明這個實現大概是怎么回事;我們這里再一次使用了填充符 token 為 0 的性質:
# 向量化 logits 和目標
targets = tf.reshape(targets, [-1]) # 將張量 targets 轉為向量
losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits, targets)
mask = tf.sign.(tf.to_float(targets)) # targets 為 0 則輸出為 0, target < 0 則輸出為 -1, 否則 為 1
masked_losses = mask*losses # 填充符所在位置的貢獻為 0復制代碼
首先我們要將 logits 和 targets 向量化。為了使 logits 向量化,一個比較好的辦法是將 dynamic_rnn
的輸出向量化為 [-1,num_hidden_units]
的形狀,然后乘以 softmax 權重 [num_hidden_units,num_classes]
。通過損失屏蔽操作,就可以消除填充符所在位置貢獻的損失。
代碼
GitHub 倉庫 (正在更新,敬請期待!)
張量形狀變化的參考
原始未處理過的文本 X
形狀為 [N,]
而 y
的形狀為 [N, C]
,其中 C
是輸出類別的數量(這些是手動完成的,但我們需要使用獨熱編碼來處理多類情況)。
然后 X
被轉化為 token 并進行填充,變成了 [N, <max_len>]
。我們還需要傳遞形狀為 [N,]
的 seq_len
參數,包含每個句子的長度。
現在 X
、seq_len
和 y
通過這個模型首先嵌入為 [NXD]
,其中 D 是嵌入維度。X
便從 [N, <max_len>]
轉換為了 [N, <max_len>, D]
。回想一下,X 在這里有一個中間表示,它被獨熱編碼為了 [N, <max_len>, <num_words>]
。但我們并不需要這么做,因為我們只需要使用對應詞的索引,然后從詞嵌入權重中取值就可以了。
我們需要將這個嵌入后的 X
傳遞給 dynamic_rnn
并返回 all_outputs
([N, <max_len>, D]
)以及 state
([1, N, D]
)。由于我們輸入了 seq_lens
,對于我們而言它就是最后一個對應的狀態。從維度的角度來說,你可以看到, all_outputs
就是來自 RNN 的對于每個句子中的每個詞的全部輸出結果。然而,state
僅僅只是每個句子的最后一個對應輸出。
現在我們要輸入 softmax 權重,但在此之前,我們需要通過取第一個索引(state[0]
)來把狀態從 [1,N,D]
轉換為[N,D]
。如此便可以通過與 softmax 權重 [D,C]
的點積,來得到形狀為 [N,C]
的輸出。其中,我們做指數級 softmax 運算,然后進行正則化,最終結合形狀為 [N,C]
的 target_y
來計算損失函數。
注意:如果你使用了基本的 RNN 或者 GRU,從 dynamic_rnn
返回的 all_outputs
和 state
的形狀是一樣的。但是如果使用 LSTM 的話,all_outputs
的形狀就是 [N, <max_len>, D]
而 state
的形狀為 [1, 2, N, D]
。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、后端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。