- 原文地址: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])
# Logits
logits = 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]
# Logits
sampling_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、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。