當前“人工智能”是繼“大數據”後又一個即將被毀的詞,每家公司都宣稱要發力人工智能,就跟4-5年前大數據一樣,業界叫的都非常響亮,不禁想到之前一個老外說過的話:
Big Data is like teenage sex: Everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims.
現在看來,上面的”Big Data”可以換成”AI”了,在大家還沒搞明白大數據的時候,人工智能就開始引領下一個潮流了。本着跟風的態度,我也嘗試去窺探個究竟。
引言
當前無論是學術界還是工業界,深度學習都受到極大的追捧,尤其是在Google開源深度學習平臺TensorFlow之後,更是給深度學習火上澆油。目前在開源社區Github上所有開源項目中,TensorFlow最爲活躍,從推出到現在,經歷了幾個版本的演進,可以說能夠靈活高效地解決大量實際問題。本文主要嘗試闡述TensorFlow在自然語言處理(NLP)領域的簡單應用,讓大家夥兒更加感性地認識TensorFlow。
說到NLP,其實我對它並不是很熟悉,之前也未曾有過NLP的相關經驗,本文是我最近學習TensorFlow的一些積累,就當拋磚引玉了。當前互聯網每天都在產生大量的文本和音頻數據,通過挖掘這些數據,我們可以做一些更加便捷的應用,例如機器翻譯、語音識別、詞性標註以及信息檢索等,這些都屬於NLP範疇。而在NLP領域中,語言模型是最基本的一個環節,本文主要圍繞語言模型展開,首先介紹其基本原理,進而引出詞向量(word2vec)、循環神經網絡(RNN)、長短時記憶網絡(LSTM)等深度學習相關模型,並詳細介紹如何利用 TensorFlow 實現上述模型。
語言模型
語言模型是一種概率模型,它是基於一個語料庫創建,得到每個句子出現的概率,通俗一點講就是看一句話是不是正常人說出來的,數學上表示爲:
上述公式的意義是:一個句子出現的概率等於給定前面的詞情況下,緊接着後面的詞出現的概率。它是通過條件概率公式展開得到。其中條件概率
P(w2|w1),P(w3|w1w2),⋯,P(wt|w1w2⋯wt−1)
根據大數定理上述公式又可以近似爲:
假如語料庫裏有
這意思就是說一個詞出現的概率只與它前面
近年也流行起神經網絡語言模型,從機器學習的角度來看,一開始不全部計算這些詞串的概率值,而是通過一個模型對詞串的概率進行建模,然後構造一個目標函數,不斷優化這個目標,得到一組優化的參數,當需要哪個詞串概率時,利用這組優化的參數直接計算得到對應的詞串概率。將詞串概率
目標函數採用對數似然函數,表示如下(其中
通過優化算法不斷最小化目標函數得到一組優化的參數
上述神經網絡包括輸入層、投影層、隱藏層以及輸出層,其中投影層只是對輸入層做了一個預處理,將輸入的所有詞進行一個連接操作,假如一個詞表示爲
其中
詞向量(word2vec)
詞向量要做的事就是將語言數學化表示,以往的做法是採用 One-hot Representation 表示一個詞,即語料庫詞典中有
詞被映射到3維空間,每個詞表示爲一個3維向量,相近的詞離的較近,可以看到兩組差不多關係的詞,他們之間的詞向量距離也差不多。
要想得到詞向量,需要藉助語言模型訓練得到,本質上來說,詞向量是在訓練語言模型過程中得到的副產品。解決word2vec問題有兩種模型,即 CBOW 和 Skip-Gram 模型[3],如下圖所示:
CBOW 模型是根據詞的上下文預測當前詞,這裏的上下文是由待預測詞的前後
Skip-Gram 模型
前面也提到, Skip-Gram 模型是根據當前詞去預測上下文,例如有如下語句:
“php 是 世界上 最好的 語言”
假定上下文是由待預測詞的前後2個詞組成,那麼由以上句子可以得到如下正樣本:
(世界上, 是), (世界上, php), (世界上, 最好的), (世界上, 語言), (最好的, 世界上), …
訓練目標爲最大化以下對數似然函數:
其中
其中
以上表達式稱之爲 NCE(Noise-contrastive estimation)[4]目標函數,其中等號右邊第二項表示通過一個服從
負採樣算法
詞典中的每個詞在語料庫中出現的頻次有高有低,理論上來說,對於那些高頻詞,被選爲負樣本的概率較大,對於那些低頻詞,被選爲負樣本的概率較小。基於這個基本事實,可以通過帶權採樣方法來實現,假設每個詞的詞頻表示爲單位線段上的一小分段,對於詞典大小爲
文[2]中在實際負採樣計算詞頻時,做了一點修正,不是簡單的統計詞的出現次數,而是對詞的出現次數做了
高頻詞二次採樣
在一個大語料庫中,很多常見的詞大量出現,如“的”、“是”等。這些詞雖然詞頻較高,但是能提供的有用信息卻很少。一般來說,這些高頻詞的詞向量在訓練幾百萬樣本後基本不會有太大的變化,爲了提高訓練速度,平衡低頻詞和高頻詞,文[2]中提出一種針對高頻詞二次採樣的技巧,對於每個詞,按如下概率丟棄而不做訓練。
其中
TensorFlow實現
根據以上實現原理,下面結合代碼闡述利用TensorFlow實現一個簡易的word2vec模型[5],藉助TensorFlow豐富的api以及強大的計算引擎,我們可以非常方便地表達模型。給定語料庫作爲訓練數據,首先掃描語料庫建立字典,爲每個詞編號,同時將那些詞頻低於min_count的詞過濾掉,即不對那些陌生詞生成詞向量。對於一個樣本(“世界上”, “php”),利用負採樣得到若干負實例,分別計算輸入詞爲“世界上”到“php”以及若干負樣本的logit值,最後通過交叉熵公式得到目標函數(3-3)。
構建計算流圖
首先定義詞向量矩陣,也稱爲 embedding matrix,這個是我們需要通過訓練得到的詞向量,其中vocabulary_size
表示詞典大小,embedding_size
表示詞向量的維度,那麼詞向量矩陣爲
vocabulary_size
1 2 |
embeddings = tf.Variable( tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0)) |
定義權值矩陣和偏置向量(對應於3-3式中的
1 2 3 4 |
weights = tf.Variable( tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0 / math.sqrt(embedding_size))) biases = tf.Variable(tf.zeros([vocabulary_size])) |
給定一個batch的輸入,從詞向量矩陣中找到對應的向量表示,以及從權值矩陣和偏置向量中找到對應正確輸出的參數,其中examples
是輸入詞,labels
爲對應的正確輸出,一維向量表示,每個元素爲詞在字典中編號:
1 2 3 4 5 6 |
# Embeddings for examples: [batch_size, embedding_size] example_emb = tf.nn.embedding_lookup(embeddings, examples) # Weights for labels: [batch_size, embedding_size] true_w = tf.nn.embedding_lookup(weights, labels) # Biases for labels: [batch_size, 1] true_b = tf.nn.embedding_lookup(biases, labels) |
負採樣得到若干非正確的輸出,其中labels_matrix
爲正確的輸出詞,採樣的時候會跳過這些詞,num_sampled
爲採樣個數,distortion
即爲公式(3-4)中的冪指數:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
labels_matrix = tf.reshape( tf.cast(labels, dtype=tf.int64), [batch_size, 1]) # Negative sampling. sampled_ids, _, _ = tf.nn.fixed_unigram_candidate_sampler( true_classes=labels_matrix, num_true=1, num_sampled=num_samples, unique=True, range_max=vocab_size, distortion=0.75, unigrams=vocab_counts.tolist()) |
找到採樣樣本對應的權值和偏置參數:
1 2 3 4 |
# Weights for sampled ids: [num_sampled, embedding_size] sampled_w = tf.nn.embedding_lookup(weights, sampled_ids) # Biases for sampled ids: [num_sampled, 1] sampled_b = tf.nn.embedding_lookup(biases, sampled_ids) |
分別計算正確輸出和非正確輸出的logit值,即計算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# True logits: [batch_size, 1] true_logits = tf.reduce_sum(tf.mul(example_emb, true_w), 1) + true_b # Sampled logits: [batch_size, num_sampled] # We replicate sampled noise lables for all examples in the batch # using the matmul. sampled_b_vec = tf.reshape(sampled_b, [num_samples]) sampled_logits = tf.matmul(example_emb, sampled_w, transpose_b=True) + sampled_b_vec # cross-entropy(logits, labels) true_xent = tf.nn.sigmoid_cross_entropy_with_logits( true_logits, tf.ones_like(true_logits)) sampled_xent = tf.nn.sigmoid_cross_entropy_with_logits( sampled_logits, tf.zeros_like(sampled_logits)) # NCE-loss is the sum of the true and noise (sampled words) # contributions, averaged over the batch. loss = (tf.reduce_sum(true_xent) + tf.reduce_sum(sampled_xent)) / batch_size |
訓練模型
計算流圖構建完畢後,我們需要去優化目標函數。採用梯度下降逐步更新參數,首先需要確定學習步長,隨着迭代進行,逐步減少學習步長,其中trained_words
爲已訓練的詞數量,words_to_train
爲所有待訓練的詞數量:
1 2 |
lr = init_learning_rate * tf.maximum( 0.0001, 1.0 - tf.cast(trained_words, tf.float32) / words_to_train) |
定義優化算子,使用梯度下降訓練模型:
1 2 3 4 5 |
optimizer = tf.train.GradientDescentOptimizer(lr) train = optimizer.minimize(loss, global_step=global_step, gate_gradients=optimizer.GATE_NONE) session.run(train) |
驗證詞向量
經過以上步驟後,即可得到詞向量矩陣,即上述代碼中的變量embeddings
,那麼如何驗證得到的詞向量矩陣的好壞呢,Mikolov等人發現[2],如果一對關係差不多的詞,其詞向量在空間中的連線近乎平行,如下圖所示。
爲此,給定基準測試集,其每行包含4個詞組成一個四元組
循環神經網絡(RNN)
人類不是從腦子一片空白開始思考,當你讀一篇文章的時候,你會根據前文去理解下文,而不是每次看到一個詞後就忘掉它,理解下一個詞的時候又從頭開始。傳統的神經網絡模型是從輸入層到隱藏層再到輸出層,每層之間的節點是無連接的,這種普通的神經網絡不具備記憶功能,而循環神經網絡(Recurrent Neural Network,RNN)就是來解決這類問題,它具備記憶性,通常用於處理時間序列問題,在衆多NLP問題中,RNN取得了巨大成功以及廣泛應用。
在RNN網絡中,一個序列當前的輸出除了與當前輸入有關以外,還與前面的輸出也有關,下圖爲RNN中一個單元的結構示意圖,圖片來源於文[7]。
上圖理解起來可能還不是很形象,根據時間序列將上圖平鋪展開得到如下圖,其鏈式的特徵揭示了 RNN 本質上是與序列相關的,所以 RNN 對於這類數據來說是最自然的神經網絡架構。
然而 RNN 有一個缺點,雖然它可以將之前的信息連接到當前的輸入上,但是如果當前輸入與之前的信息時間跨度很大,由於梯度衰減等原因,RNN 學習如此遠的信息的能力會下降,這個問題稱之爲長時間依賴(Long-Term Dependencies)問題。例如預測一句話“飛機在天上”下一個詞,可能不需要太多的上下文就可以預測到下一個詞爲“飛”,這種情況下,相關信息與要預測的詞之間的時間跨度很小,RNN 可以很容易學到之前的信息。再比如預測“他來自法國,…,他會講”的下一個詞,從當前的信息來看,下一個詞可能是一種語言,但是要想準確預測哪種語言,就需要再去前文找信息了,由於前文的“法國”離當前位置的時間跨度較大,RNN很難學到如此遠的信息。更多長時間依賴細節參考文[8]。幸運的是,有一種 RNN 變種,叫做長短時記憶網絡(Long Short Term Memory networks, LSTM),可以解決這個問題。
長短時記憶網絡(LSTM)
LSTM 是一種帶有選擇性記憶功能的 RNN,它可以有效的解決長時間依賴問題,並能學習到之前的關鍵信息。如下圖所示爲 LSTM 展開後的示意圖。
相對於 RNN , LSTM 只是在每個單元結構上做了改進,在 RNN 中,每個單元結構只有單個激活函數,而 LSTM 中每個單元結構更爲複雜,它增加了一條狀態線(圖中最上面的水平線),以記住從之前的輸入學到的信息,另外增加三個門(gate)來控制其該狀態,分別爲忘記門、輸入門和輸出門。忘記門的作用是選擇性地將之前不重要的信息丟掉,以便存儲新信息;輸入門是根據當前輸入學習到新信息然後更新當前狀態;輸出門則是結合當前輸入和當前狀態得到一個輸出,該輸出除了作爲基本的輸出外,還會作爲下一個時刻的輸入。下面用數學的方式表達每個門的意思。
忘記門,要丟掉的信息如下:
輸入門,要增加的信息如下:
那麼根據忘記門和輸入門,狀態更新如下:
輸出門,得到輸出信息如下:
LSTM 單元輸入都是上一個時刻的輸出與當前時刻的輸入通過向量concat連接而得到,基於這個輸入,利用sigmoid函數作爲三個門的篩選器,分別得到
深層LSTM網絡
深度學習,其特點在於深,前面已經講述單層 LSTM 網絡結構,深層 LSTM 網絡其實就是將多層 LSTM 疊加,形成多個隱藏層,如下圖所示。
上圖中每個 LSTM 單元內部結構如下圖所示,對於
根據上面的結構,可以得到
其中
正則化
然而,實踐證明大規模的 LSTM 網絡很容易過擬合,實際應用中,需要採取正則化方法來避免過擬合,神經網絡中常見的正則化方法是Dropout方法[11],文[12]提出一種簡單高效的Dropout方法運用於 RNN/LTSM 網絡。如下圖所示,Dropout僅應用於虛線方向的輸入,即僅針對於上一層的輸出做Dropout。
根據上圖的Dropout策略,公式(5-5)可以改寫成如下形式:
其中
TensorFlow實現
根據前面所述的 LSTM 模型原理,實現之前提到的語言模型,即根據前文預測下一個詞,例如輸入“飛機在天上”預測下一個詞“飛”,使用 TensorFlow 來實現 LSTM 非常的方便,因爲 TensorFlow 已經提供了基本的 LSTM 單元結構的Operation,其實現原理就是基於文[12]提出的帶Dropout的 LSTM 模型。完整代碼請參考ptb_word_lm.py
構建LSTM模型
利用TensorFlow提供的Operation,實現 LSTM 網絡很簡單,首先定義一個基本的 LSTM 單元,其中size
爲 LSTM 單元的輸出維度,再對其添加Dropout,根據
LSTM 的層數num_layers
得到多層的 RNN 結構單元。
1 2 3 4 |
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0) lstm_cell = tf.nn.rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=keep_prob) cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * num_layers) |
每次給定一個batch的輸入,將 LSTM 網絡的狀態初始化爲0。詞的輸入由詞向量表示,所以先定義一個embedding矩陣,這裏可以不要關心它一開始有沒有,它會在訓練過程中的慢慢得到的,僅作爲訓練的副產品。假設LSTM網絡展開num_steps
步,每一步給定一個batch的詞作爲輸入,經過
LSTM 單元處理後,狀態更新並得到輸出,並通過softmax歸一化後計算損失函數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
initial_state = cell.zero_state(batch_size, tf.float32) embedding = tf.get_variable("embedding", [vocab_size, size]) # input_data: [batch_size, num_steps] # targets: [batch_size, num_steps] input_data = tf.placeholder(tf.int32, [batch_size, num_steps]) targets = tf.placeholder(tf.int32, [batch_size, num_steps]) inputs = tf.nn.embedding_lookup(embedding, input_data) outputs = [] for time_step in range(num_steps): (cell_output, state) = cell(inputs[:, time_step, :], state) outputs.append(cell_output) output = tf.reshape(tf.concat(1, outputs), [-1, size]) softmax_w = tf.get_variable("softmax_w", [size, vocab_size]) softmax_b = tf.get_variable("softmax_b", [vocab_size]) logits = tf.matmul(output, softmax_w) + softmax_b loss = tf.nn.seq2seq.sequence_loss_by_example( [logits], [tf.reshape(targets, [-1])], [tf.ones([batch_size * num_steps])]) |
訓練模型
簡單採用梯度下降優化上述損失函數,逐步迭代,直至最大迭代次數,得到final_state
,即爲LSTM所要學習的參數。
1 2 3 4 5 6 |
optimizer = tf.train.GradientDescentOptimizer(lr) train_op = optimizer.minimize(loss) for i in range(max_epoch): _, final_state = session.run([train_op, state], {input_data: x, targets: y}) |
驗證測試模型
模型訓練完畢後,我們已經得到LSTM網絡的狀態,給定輸入,經過LSTM網絡後即可得到輸出了。
1 2 |
(cell_output, _) = cell(inputs, state) session.run(cell_output) |
小結
在使用TensorFlow處理深度學習相關問題時,我們不需要太關注其內部實現細節,只需把精力放到模型的構建上,利用TensorFlow已經提供的抽象單元結構就可以構建靈活的模型。也恰恰正是因爲TensorFlow的高度抽象化,有時讓人理解起來頗費勁。所以在我們使用TensorFlow的過程中,不要把問題細化的太深,一切數據看成Tensor即可,利用Tensor的操作符對其進行運算,不要在腦海裏想如何如何的運算細節等等,不然就會身陷囹圄。
參考文獻
[1]. Bengio Y, Schwenk H, Senécal J S, et al. Neural probabilistic language models[M]//Innovations in Machine Learning. Springer Berlin Heidelberg, 2006: 137-186.MLA.
[2]. Mikolov T, Sutskever I, Chen K, et al. Distributed representations of words and phrases and their compositionality[C]//Advances in neural information processing systems. 2013: 3111-3119.
[3]. Mikolov T, Le Q V, Sutskever I. Exploiting similarities among languages for machine translation[J]. arXiv preprint arXiv:1309.4168, 2013.
[4]. Gutmann M U, Hyvärinen A. Noise-contrastive estimation of unnormalized statistical models, with applications to natural image statistics[J]. The Journal of Machine Learning Research, 2012, 13(1): 307-361.
[5]. Vector Representations of Words. https://www.tensorflow.org/versions/r0.8/tutorials/word2vec/index.html#vector-representations-of-words
[6]. word2vec 中的數學原理詳解. http://www.cnblogs.com/peghoty/p/3857839.html
[7]. Understanding LSTM Networks. http://colah.github.io/posts/2015-08-Understanding-LSTMs/
[8]. Bengio Y, Simard P, Frasconi P. Learning long-term dependencies with gradient descent is difficult[J]. Neural Networks, IEEE Transactions on, 1994, 5(2): 157-166.
[9]. Graves A. Generating sequences with recurrent neural networks[J]. arXiv preprint arXiv:1308.0850, 2013.
[10]. Recurrent Neural Networks. https://www.tensorflow.org/versions/r0.8/tutorials/recurrent/index.html#recurrent-neural-networks
[11]. Srivastava N. Improving neural networks with dropout[D]. University of Toronto, 2013.
[12]. Zaremba W, Sutskever I, Vinyals O. Recurrent neural network regularization[J]. arXiv preprint arXiv:1409.2329, 2014.
轉載請註明出處,本文永久鏈接:http://sharkdtu.com/posts/nn-nlp.html