[译] RNN 循环神经网络系列 2:文本分类
- 原文地址:RECURRENT NEURAL NETWORKS (RNN) – PART 2: TEXT CLASSIFICATION
- 原文作者:GokuMohandas
- 譯文出自:掘金翻譯計(jì)劃
- 本文永久鏈接:github.com/xitu/gold-m…
- 譯者:Changkun Ou
- 校對(duì)者:yanqiangmiffy, TobiasLee
本系列文章匯總
RNN 循環(huán)神經(jīng)網(wǎng)絡(luò)系列 2:文本分類
在第一篇文章中,我們看到了如何使用 TensorFlow 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 RNN 架構(gòu)。現(xiàn)在我們將使用這些組件并將其應(yīng)用到文本分類中去。主要的區(qū)別在于,我們不會(huì)像 CHAR-RNN 模型那樣輸入固定長(zhǎng)度的序列,而是使用長(zhǎng)度不同的序列。
文本分類
這個(gè)任務(wù)的數(shù)據(jù)集選用了來(lái)自 Cornell 大學(xué)的語(yǔ)句情緒極性數(shù)據(jù)集 v1.0,它包含了 5331 個(gè)正面和負(fù)面情緒的句子。這是一個(gè)非常小的數(shù)據(jù)集,但足夠用來(lái)演示如何使用循環(huán)神經(jīng)網(wǎng)絡(luò)進(jìn)行文本分類了。
我們需要進(jìn)行一些預(yù)處理,主要包括標(biāo)注輸入、附加標(biāo)記(填充等)。請(qǐng)參考完整代碼了解更多。
預(yù)處理步驟
如上圖所示,我們希望在計(jì)算完成時(shí)立即對(duì)句子的情緒做出預(yù)測(cè)。引入額外的填充符會(huì)帶來(lái)過(guò)多噪聲,這樣的話你模型的性能就會(huì)不太好。注意:我們填充序列的唯一原因是因?yàn)樾枰怨潭ù笮〉呐枯斎脒M(jìn) RNN。下面你會(huì)看到,使用動(dòng)態(tài) RNN 還能避免在序列完成后的不必要計(jì)算。
模型
代碼:
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 自動(dòng)包含了上一次的對(duì)應(yīng)輸出# 因?yàn)?state 是一個(gè)帶有張量的元組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))# 損失函數(shù)self.loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits, self.targets_y))# 優(yōu)化self.lr = tf.Variable(0.0, trainable=False)trainable_vars = tf.trainable_variables()# 使用梯度截?cái)鄟?lái)避免梯度消失和梯度爆炸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))# 下面是用于采樣的值# (在每個(gè)單詞后生成情緒)# 取所有輸出作為第一個(gè)輸入序列# (由于采樣,只需一個(gè)輸入序列)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: # 訓(xùn)練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: # 訓(xùn)練return outputs[0], outputs[1], outputs[2]復(fù)制代碼上面的代碼就是我們的模型代碼,它在訓(xùn)練的過(guò)程中使用了輸入的文本。注意:為了清楚起見,我們決定將批量數(shù)據(jù)的大小保存在我們的輸入和目標(biāo)占位符中,但是我們應(yīng)該讓它們獨(dú)立于一個(gè)特定的批量大小之外。由于這個(gè)特定的批量大小依賴于 batch_size,如果我們這么做,那么我們就還得輸入一個(gè) initial_state。我們通過(guò)嵌入他們來(lái)為每個(gè)數(shù)據(jù)序列來(lái)輸入 token。實(shí)踐策略表明,我們?cè)谳斎胛谋旧鲜褂?skip-gram 模型預(yù)訓(xùn)練嵌入權(quán)重能夠取得更好的性能。
在此模型中,我們?cè)俅问褂?dynamic_rnn,但是這次我們提供了sequence_length 參數(shù)的值,它是一個(gè)包含每個(gè)序列長(zhǎng)度的列表。這樣,我們就可以避免在輸入序列的最后一個(gè)詞之后進(jìn)行的不必要的計(jì)算。length 函數(shù)就用來(lái)獲取這個(gè)列表的長(zhǎng)度,如下所示。當(dāng)然,我們也可以在外面計(jì)算seq_len,再通過(guò)占位符進(jìn)行傳遞。
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復(fù)制代碼由于我們填充符 token 為 0,因此可以使用每個(gè) token 的 sign 性質(zhì)來(lái)確定它是否是一個(gè)填充符 token。如果輸入大于 0,則 tf.sign 為 1;如果輸入為 0,則為 tf.sign 為 0。這樣,我們可以逐步通過(guò)列索引來(lái)獲得 sign 值為正的 token 數(shù)量。至此,我們可以將這個(gè)長(zhǎng)度提供給 dynamic_rnn 了。
注意:我們可以很容易地在外部計(jì)算 seq_lens,并將其作為占位符進(jìn)行傳參。這樣我們就不用依賴于 PAD_ID = 0 這個(gè)性質(zhì)了。
一旦我們從 RNN 拿到了所有的輸出和最終狀態(tài),我們就會(huì)希望分離對(duì)應(yīng)輸出。對(duì)于每個(gè)輸入來(lái)說(shuō),將具有不同的對(duì)應(yīng)輸出,因?yàn)槊總€(gè)輸入長(zhǎng)度不一定不相同。由于我們將 seq_len 傳給了 dynamic_rnn,而 state 又是最后一個(gè)對(duì)應(yīng)輸出,我們可以通過(guò)查看 state 來(lái)找到對(duì)應(yīng)輸出。注意,我們必須取 state[0],因?yàn)榉祷氐?state 是一個(gè)張量的元組。
其他需要注意的事情:我并沒(méi)有使用 initial_state,而是直接給 dynamic_rnn 設(shè)置 dtype。此外,dropout 將根據(jù) forward_only 與否,作為參數(shù)傳遞給 step()。
推斷
總的來(lái)說(shuō),除了單個(gè)句子的預(yù)測(cè)外,我還想為具有一堆樣本句子整體情緒進(jìn)行預(yù)測(cè)。我希望看到的是,每個(gè)單詞都被 RNN 讀取后,將之前的單詞分值保存在內(nèi)存中,從而查看預(yù)測(cè)分值是怎樣變化的。舉例如下(值越接近 0 表明越靠近負(fù)面情緒):
Screen Shot 2016-10-05 at 8.34.51 PM.png注意:這是一個(gè)非常簡(jiǎn)單的模型,其數(shù)據(jù)集非常有限。主要目的只是為了闡明它是如何搭建以及如何運(yùn)行的。為了獲得更好的性能,請(qǐng)嘗試使用數(shù)據(jù)量更大的數(shù)據(jù)集,并考慮具體的網(wǎng)絡(luò)架構(gòu),比如 Attention 模型、Concept-Aware 詞嵌入以及隱喻(symbolization to name)等等。
損失屏蔽(這里不需要)
最后,我們來(lái)計(jì)算 cost。你可能會(huì)注意到我們沒(méi)有做任何損失屏蔽(loss masking)處理,因?yàn)槲覀兎蛛x了對(duì)應(yīng)輸出,僅用于計(jì)算損失函數(shù)。然而,對(duì)于其他諸如機(jī)器翻譯的任務(wù)來(lái)說(shuō),我們的輸出很有可能還來(lái)自填充符 token。我們不想考慮這些輸出,因?yàn)閭鬟f了 seq_lens 參數(shù)的 dynamic_rnn 將返回 0。下面這個(gè)例子比較簡(jiǎn)單,只用來(lái)說(shuō)明這個(gè)實(shí)現(xiàn)大概是怎么回事;我們這里再一次使用了填充符 token 為 0 的性質(zhì):
# 向量化 logits 和目標(biāo) targets = tf.reshape(targets, [-1]) # 將張量 targets 轉(zhuǎn)為向量 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 # 填充符所在位置的貢獻(xiàn)為 0復(fù)制代碼首先我們要將 logits 和 targets 向量化。為了使 logits 向量化,一個(gè)比較好的辦法是將 dynamic_rnn 的輸出向量化為 [-1,num_hidden_units] 的形狀,然后乘以 softmax 權(quán)重 [num_hidden_units,num_classes]。通過(guò)損失屏蔽操作,就可以消除填充符所在位置貢獻(xiàn)的損失。
代碼
GitHub 倉(cāng)庫(kù) (正在更新,敬請(qǐng)期待!)
張量形狀變化的參考
原始未處理過(guò)的文本 X 形狀為 [N,] 而 y 的形狀為 [N, C],其中 C 是輸出類別的數(shù)量(這些是手動(dòng)完成的,但我們需要使用獨(dú)熱編碼來(lái)處理多類情況)。
然后 X 被轉(zhuǎn)化為 token 并進(jìn)行填充,變成了 [N, <max_len>]。我們還需要傳遞形狀為 [N,] 的 seq_len 參數(shù),包含每個(gè)句子的長(zhǎng)度。
現(xiàn)在 X、seq_len 和 y 通過(guò)這個(gè)模型首先嵌入為 [NXD],其中 D 是嵌入維度。X 便從 [N, <max_len>] 轉(zhuǎn)換為了 [N, <max_len>, D]?;叵胍幌?#xff0c;X 在這里有一個(gè)中間表示,它被獨(dú)熱編碼為了 [N, <max_len>, <num_words>]。但我們并不需要這么做,因?yàn)槲覀冎恍枰褂脤?duì)應(yīng)詞的索引,然后從詞嵌入權(quán)重中取值就可以了。
我們需要將這個(gè)嵌入后的 X 傳遞給 dynamic_rnn 并返回 all_outputs ([N, <max_len>, D])以及 state([1, N, D])。由于我們輸入了 seq_lens,對(duì)于我們而言它就是最后一個(gè)對(duì)應(yīng)的狀態(tài)。從維度的角度來(lái)說(shuō),你可以看到, all_outputs 就是來(lái)自 RNN 的對(duì)于每個(gè)句子中的每個(gè)詞的全部輸出結(jié)果。然而,state 僅僅只是每個(gè)句子的最后一個(gè)對(duì)應(yīng)輸出。
現(xiàn)在我們要輸入 softmax 權(quán)重,但在此之前,我們需要通過(guò)取第一個(gè)索引(state[0])來(lái)把狀態(tài)從 [1,N,D] 轉(zhuǎn)換為[N,D]。如此便可以通過(guò)與 softmax 權(quán)重 [D,C] 的點(diǎn)積,來(lái)得到形狀為 [N,C] 的輸出。其中,我們做指數(shù)級(jí) softmax 運(yùn)算,然后進(jìn)行正則化,最終結(jié)合形狀為 [N,C] 的 target_y 來(lái)計(jì)算損失函數(shù)。
注意:如果你使用了基本的 RNN 或者 GRU,從 dynamic_rnn 返回的 all_outputs 和 state 的形狀是一樣的。但是如果使用 LSTM 的話,all_outputs 的形狀就是 [N, <max_len>, D] 而 state 的形狀為 [1, 2, N, D]。
掘金翻譯計(jì)劃 是一個(gè)翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來(lái)源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android、iOS、React、前端、后端、產(chǎn)品、設(shè)計(jì) 等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請(qǐng)持續(xù)關(guān)注 掘金翻譯計(jì)劃、官方微博、知乎專欄。
總結(jié)
以上是生活随笔為你收集整理的[译] RNN 循环神经网络系列 2:文本分类的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: linux vma,内存管理 – Lin
- 下一篇: matlab人脸追踪,求大神帮助我这个菜