手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

機器學習 深度學習 FLOW Google 雷鋒網 2017-04-19
手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

雷鋒網按:本文源自一位數據科學開發者的個人博客,雷鋒網編譯。

許多開發者向新手建議:如果你想要入門機器學習,就必須先了解一些關鍵算法的工作原理,然後再開始動手實踐。但我不這麼認為。

我覺得實踐高於理論,新手首先要做的是瞭解整個模型的工作流程,數據大致是怎樣流動的,經過了哪些關鍵的結點,最後的結果在哪裡獲取,並立即開始動手實踐,構建自己的機器學習模型。至於算法和函數內部的實現機制,可以等了解整個流程之後,在實踐中進行更深入的學習和掌握。

那麼問題來了,既然作為初學者不需要掌握算法細節,但實現模型的過程中又必須用到相關算法,怎麼辦呢?答案是藉助於互聯網上已經實現好的函數庫,例如 TensorFlow。

在本文中,我們將利用 TensorFlow 實現一個基於深度神經網絡(DNN)的文本分類模型,希望對各位初學者有所幫助。文中所涉完整代碼已經在 GitHub 上開源,感興趣的朋友可以在以下鏈接中下載:

http://t.cn/RXiP3Om

下面是正式的教程內容:

關於 TensorFlow

TensorFlow 是谷歌旗下一個開源的機器學習框架。從它的名字就能看出這個框架基本的工作原理:由多維數組構成的張量(tensor)在圖(graph)結點之間定向流動(flow),從輸入走到輸出。

在 TensorFlow 中,每次運算都可以用數據流圖(dataflow graph)的方式表示。每個數據流圖都有以下兩個重要元素:

● 一組 tf.Operation 對象,代表運算本身;

● 一組 tf.Tensor 對象,代表被運算的數據。

如下圖所示,這裡我們以一個簡單的例子說明什麼計算流圖具體是怎樣運行的。

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

假設圖中的 x=[1,3,6],y=[1,1,1]。由於 tf.Tensor 被用來表示運算數據,因此在 TensorFlow 中我們會首先定義兩個 tf.Tensor 常量對象存放數據。然後再用 tf.Operation 對象定義圖中的加法運算,具體代碼如下:

import tensorflow as tf

x = tf.constant([1,3,6])

y = tf.constant([1,1,1])

op = tf.add(x,y)

現在,我們已經定義了數據流圖的兩個重要元素:tf.Operation 和 tf.Tensor,那麼如何構建圖本身呢,具體代碼如下:

import tensorflow as tf

my_graph = tf.Graph

with my_graph.as_default:

x = tf.constant([1,3,6])

y = tf.constant([1,1,1])

op = tf.add(x,y)

至此我們已經完成了數據流圖的定義,在 TensorFlow 中,只有先定義了圖,才能進行後續的計算操作(即驅動數據在圖的結點間定向流動)。這裡 TensorFlow 又規定,要進行後續的計算,必須通過 tf.Session 來統一管理,因此我們還要定義一個 tf.Session 對象,即會話。

在 TensorFlow 中,tf.Session 專門用來封裝 tf.Operation 在 tf.Tensor 基礎上執行的操作環境。因此,在定義 tf.Session 對象時,也需要傳入相應的數據流圖(可以通過 graph 參數傳入),本例中具體的代碼如下:

import tensorflow as tf

my_graph = tf.Graph

with tf.Session(graph=my_graph) as sess:

x = tf.constant([1,3,6])

y = tf.constant([1,1,1])

op = tf.add(x,y)

定義好 tf.Session 之後,我們可以通過 tf.Session.run 方法來執行對應的數據流圖。run 方法可以通過 fetches 參數傳入相應 tf.Operation 對象,並導入與 tf.Operation 相關的所有 tf.Tensor 對象,然後遞歸執行與當前 tf.Operation 有依賴關係的所有操作。本例中具體執行的是求和操作,實現代碼如下:

import tensorflow as tf

my_graph = tf.Graph

with tf.Session(graph=my_graph) as sess:

x = tf.constant([1,3,6])

y = tf.constant([1,1,1])

op = tf.add(x,y)

result = sess.run(fetches=op)

print(result)

>>> [2 4 7]

可以看到運算結果是 [2 4 7]。

關於預測模型

瞭解 TensorFlow 的基本原理之後,下面的任務是如何構建一個預測模型。簡單來說,機器學習算法 + 數據就等於預測模型。構建預測模型的流程如下圖所示:

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

如圖,經過數據訓練的機器學習算法就是模型。訓練好一個模型之後,輸入待預測數據,就能得到相應的預測結果。大體流程如下圖所示:

在本例中,我們將要構建的模型需要根據輸入文本,輸出相應的類別,即完成文本分類的工作。因此這裡的輸入應該是文本(text),輸出是類別(category)。更具體地說,本例中我們已經事先獲取了標記數據(即一些已經標明瞭類別的文本段),然後用這些數據對算法進行訓練,最後再用訓練好的模型對新文本分類。這一過程也就是通常所說的監督學習(supervised learning)。另外,由於我們的任務是對文本數據進行分類,所以也屬於分類問題的範疇。

為了構建該文本分類模型,下面我們需要介紹一些神經網絡的基礎知識。

關於神經網絡

從本質上說,神經網絡是計算模型(computational model)的一種。(注:這裡所謂計算模型是指通過數學語言和數學概念描述系統的方法)並且這種計算模型還能夠自動完成學習和訓練,不需要精確編程。

最原始也是最基礎的一個神經網絡算法模型是感知機模型(Perceptron),關於感知機模型的詳細介紹請參見這篇博客:

http://t.cn/R5MphRp

由於神經網絡模型是模擬人類大腦神經系統的組織結構而提出的,因此它與人類的腦神經網絡具有相似的結構。

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

如上圖所示,一般的神經網絡結構可以分為三層:輸入層、隱蔽層(hidden layer)和輸出層。

為了深入理解神經網絡究竟是如何工作的,我們需要利用 TensorFlow 自己親手構建一個神經網絡模型,下面介紹一個具體的實例。(本例部分內容源自 GitHub 上的一段開源代碼:鏈接)

本例中,我們有兩個隱蔽層(關於隱蔽層層數的選擇是另一個問題,更詳細的內容可以參考:鏈接)。概括地說,隱蔽層的主要作用是將輸入層的數據轉換成一種輸出層更便於利用的形式。

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

如圖所示,本例中輸入層的每個結點都代表了輸入文本中的一個詞,接下來是第一個隱蔽層。這裡需要注意的是,第一層隱蔽層的結點個數選擇也是一項重要的任務,通常被稱為特徵選擇。

圖中的每個結點(也被稱為神經元),都會搭配一個權重。而我們下面所謂訓練過程其實就是不斷調整這些權重值,讓模型的實際輸出和預想輸出更匹配的過程。當然,除了權重之外,整個網絡還要加上一個偏差值。(關於偏差的詳細介紹詳見:鏈接)

對每個結點做加權和並加上一個偏差值之後,還需要經過激活函數(activation function)的處理才能輸出到下一層 。實際上,這裡激活函數確定了每個結點的最終輸出情況,同時為整個模型加入了非線性元素。如果用檯燈來做比喻的話,激活函數的作用就相當於開關。實際研究中根據應用的具體場景和特點,有各種不同的激活函數可供選擇,這裡屏蔽層選擇的是 ReLu 函數。

另外圖中還顯示了第二個隱蔽層,它的功能和第一層並沒有本質區別,唯一的不同就是它的輸入是第一層的輸出,而第一層的輸入則是原始數據。

最後是輸出層,本例中應用了獨熱編碼的方式來對結果進行分類。這裡所謂獨熱編碼是指每個向量中只有一個元素是 1,其他均為 0 的編碼方式。例如我們要將文本數據分為三個類別(體育、航空和電腦繪圖),則編碼結果為:

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

這裡獨熱編碼的好處是:輸出結點的個數恰好等於輸出類別的個數。此外,輸出層和前面的隱蔽層結構類似,我們也要為每個結點搭配一個權重值,加上恰當的偏差,最後通過激活函數的處理。

但本例中輸出層的激活函數與隱蔽層的激活函數不同。由於本例的最終目的是輸出每個文本對應的類別信息,而這裡所有類別之間又是互斥的關係。基於這些特點,我們在輸出層選擇了 Softmax 函數作為激活函數。該函數的特點是可以將輸出值轉換為 0-1 之間的一個小數值,並且這些小數值的和為 1。於是正好可以用這些小數表示每個類別的可能性分佈情況。假如剛才提到的三個類別原本的輸出值為 1.2、0.9 和 0.4,則通過 Softmax 函數的處理後,得到的結果為:

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

可以看到這三個小數的和正好為 1。

到目前為止,我們已經明確了該神經網絡的數據流圖,下面為具體的代碼實現:

# Network Parameters

n_hidden_1 = 10 # 1st layer number of features

n_hidden_2 = 5 # 2nd layer number of features

n_input = total_words # Words in vocab

n_classes = 3 # Categories: graphics, space and baseball

def multilayer_perceptron(input_tensor, weights, biases):

layer_1_multiplication= tf.matmul(input_tensor, weights['h1'])

layer_1_addition = tf.add(layer_1_multiplication, biases['b1'])

layer_1_activation= tf.nn.relu(layer_1_addition)

# Hidden layer with RELU activation

layer_2_multiplication = tf.matmul(layer_1_activation, weights['h2'])

layer_2_addition = tf.add(layer_2_multiplication, biases['b2'])

layer_2_activation= tf.nn.relu(layer_2_addition)

# Output layer with linear activation

out_layer_multiplication = tf.matmul(layer_2_activation, weights['out'])

out_layer_addition = out_layer_multiplication + biases['out']

return out_layer_addition

神經網絡的訓練

如前所述,模型訓練中一項非常重要的任務就是調整結點的權重。本節我們將介紹如何在 TensorFlow 中實現這一過程。

在 TensorFlow 中,結點權重和偏差值以變量的形式存儲,即 tf.Variable 對象。在數據流圖調用 run 函數的時候,這些值將保持不變。在一般的機器學習場景中,權重值和偏差值的初始取值都通過正太分佈確定。具體代碼如下圖所示:

weights = {

'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),

'h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),

'out': tf.Variable(tf.random_normal([n_hidden_2, n_classes]))

}

biases = {

'b1': tf.Variable(tf.random_normal([n_hidden_1])),

'b2': tf.Variable(tf.random_normal([n_hidden_2])),

'out': tf.Variable(tf.random_normal([n_classes]))

}

以初始值運行神經網絡之後,會得到一個實際輸出值 z,而我們的期望輸出值是 expected,這時我們需要做的就是計算兩者之間的誤差,並通過調整權重等參數使之最小化。一般計算誤差的方法有很多,這裡因為我們處理的是分類問題,因此採用交叉熵誤差。(關於為什麼分類問題選用交叉熵,參見:連接)

在 TensorFlow 中,我們可以通過調用 tf.nn.softmax_cross_entropy_with_logits 函數來計算交叉熵誤差,因為這裡我們的激活函數選擇了 Softmax ,因此誤差函數中出現了 softmax_ 前綴。具體代碼如下(代碼中我們同時調用了 tf.reduced_mean() 函數來計算平均誤差):

# Construct model

prediction = multilayer_perceptron(input_tensor, weights, biases)

# Define loss

entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=output_tensor)

loss = tf.reduce_mean(entropy_loss)

得到誤差之後,下面的任務是如何使之最小化。這裡我們選擇的方法是最常用的隨機梯度下降法,其直觀的原理圖如下所示:

手把手教你如何用 TensorFlow 實現基於 DNN 的文本分類

同樣,用來計算梯度下降的方法也有很多,這裡我們採用了 Adaptive Moment Estimation (Adam) 優化法,即自適應矩估計的優化方法,具體在 TensorFlow 中的體現是 tf.train.AdamOptimizer(learning_rate).minimize(loss) 函數。這裡我們需要傳入 learning_rate 參數以決定計算梯度時的步進長度。

非常方便的一點是,AdamOptimizer 函數封裝了兩種功能:一是計算梯度,二是更新梯度。換句話說,調用該函數不但能計算梯度值,還能將計算結果更新到所有 tf.Variables 對象中,這一點大大降低了編程複雜度。

具體模型訓練部分的代碼如下所示:

learning_rate = 0.001

# Construct model

prediction = multilayer_perceptron(input_tensor, weights, biases)

# Define loss

entropy_loss = tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=output_tensor)

loss = tf.reduce_mean(entropy_loss)

optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)

數據處理

本例中,我們得到的原始數據是許多英文的文本片段,為了將這些數據導入模型中,我們需要對原始數據進行必要的預處理過程。這裡具體包括兩個部分:

● 為每個單詞編碼;

● 為每個文本片段創建對應的張量表示,其中以數字 1 代表出現了某個單詞,0 表示沒有該單詞。

具體實現代碼如下:

import numpy as np #numpy is a package for scientific computing

from collections import Counter

vocab = Counter

text = "Hi from Brazil"

#Get all words

for word in text.split(' '):

vocab[word]+=1

#Convert words to indexes

def get_word_2_index(vocab):

word2index = {}

for i,word in enumerate(vocab):

word2index[word] = i

return word2index

#Now we have an index

word2index = get_word_2_index(vocab)

total_words = len(vocab)

#This is how we create a numpy array (our matrix)

matrix = np.zeros((total_words),dtype=float)

#Now we fill the values

for word in text.split:

matrix[word2index[word]] += 1

print(matrix)

>>> [ 1. 1. 1.]

從以上代碼可以看到,當輸入文本是“Hi from Brazil”時,輸出矩陣是 [ 1. 1. 1.]。而當輸入文本只有“Hi”時又會怎麼樣呢,具體代碼和結果如下:

matrix = np.zeros((total_words),dtype=float)

text = "Hi"

for word in text.split:

matrix[word2index[word.lower()]] += 1

print(matrix)

>>> [ 1. 0. 0.]

可以看到,這時的輸出是 [ 1. 0. 0.]。

相應的,我們也可以對類別信息進行編碼,只不過這時使用的是獨熱編碼:

y = np.zeros((3),dtype=float)

if category == 0:

y[0] = 1. # [ 1. 0. 0.]

elif category == 1:

y[1] = 1. # [ 0. 1. 0.]

else:

y[2] = 1. # [ 0. 0. 1.]

運行模型並預測

至此我們已經對 TensorFlow、神經網絡模型、模型訓練和數據預處理等方面有了初步的瞭解,下面我們將演示如何將這些知識應用於實際的數據。

這裡我們的數據來源是 20 Newsgroups,其中包括了 18000 篇新聞稿,覆蓋率 20 個類別,開源免費,下載地址為:

http://t.cn/zY6ssrE

首先,為了導入這些數據集,我們需要藉助 scikit-learn 庫。它也是個開源的函數庫,基於 Python 語言,主要進行機器學習相關的數據處理任務。本例中我們只使用了其中的三個類:comp.graphics,sci.space 和 rec.sport.baseball。

最終數據會被分為兩個子集,一個是數據訓練集,一個是測試集。這裡的建議是最好不要提前查看測試數據集。因為提前查看測試數據會影響我們對模型參數的選擇,從而影響模型對其他未知數據的通用性。

具體的數據導入代碼如下:

from sklearn.datasets import fetch_20newsgroups

categories = ["comp.graphics","sci.space","rec.sport.baseball"]

newsgroups_train= fetch_20newsgroups(subset='train', categories=categories)

newsgroups_test= fetch_20newsgroups(subset='test', categories=categories)

在神經網絡術語中,一個 epoch 過程就是對所有訓練數據的一個前向傳遞(forward pass)加後向傳遞(backward pass)的完整循環。這裡前向是指根據現有權重得到實際輸出值的過程,後向是指根據誤差結果反過來調整權重的過程。下面我們重點介紹一下 tf.Session.run 函數,實際上它的完整調用形式如下:

tf.Session.run(fetches, feed_dict=None, options=None, run_metadata=None)

在文章開頭介紹該函數時,我們只通過 fetches 參數傳入了加法操作,但其實它還支持一次傳入多種操作的用法。在面向實際數據的模型訓練環節,我們就傳入了兩種操作:一個是誤差計算(即隨機梯度下降),另一個是優化函數(即自適應矩估計)。

run 函數中另一個重要的參數是 feed_dict,我們就是通過這個參數傳入模型每次處理的輸入數據。而為了輸入數據,我們又必須先定義 tf.placeholders。

按照官方文檔的解釋,這裡 placeholder 僅僅是一個空客,用於引用即將導入模型的數據,既不需要初始化,也不存放真實的數據。本例中定義 tf.placeholders 的代碼如下:

n_input = total_words # Words in vocab

n_classes = 3 # Categories: graphics, sci.space and baseball

input_tensor = tf.placeholder(tf.float32,[None, n_input],name="input")

output_tensor = tf.placeholder(tf.float32,[None, n_classes],name="output")

在進行實際的模型訓練之前,還需要將數據分成 batch,即一次計算處理數據的量。這時就體現了之前定義 tf.placeholders 的好處,即可以通過 placeholders 定義中的“None”參數指定一個維度可變的 batch。也就是說,batch 的具體大小可以等後面使用時再確定。這裡我們在模型訓練階段傳入的 batch 更大,而測試階段可能會做一些改變,因此需要使用可變 batch。隨後在訓練中,我們通過 get_batches 函數來獲取每次處理的真實文本數據。具體模型訓練部分的代碼如下:

training_epochs = 10

# Launch the graph

with tf.Session as sess:

sess.run(init) #inits the variables (normal distribution, remember?)

# Training cycle

for epoch in range(training_epochs):

avg_cost = 0.

total_batch = int(len(newsgroups_train.data)/batch_size)

# Loop over all batches

for i in range(total_batch):

batch_x,batch_y = get_batch(newsgroups_train,i,batch_size)

# Run optimization op (backprop) and cost op (to get loss value)

c,_ = sess.run([loss,optimizer], feed_dict={input_tensor: batch_x, output_tensor:batch_y})

至此我們已經針對實際數據完成了模型訓練,下面到了應用測試數據對模型進行測試的時候。在測試過程中,和之前的訓練部分類似,我們同樣要定義圖元素,包括操作和數據兩類。這裡為了計算模型的精度,同時還因為我們對結果引入了獨熱編碼,因此需要同時得到正確輸出的索引,以及預測輸出的索引,並檢查它們是否相等,如果不等,要計算相應的平均誤差。具體實現代碼和結果如下:

# Test model

index_prediction = tf.argmax(prediction, 1)

index_correct = tf.argmax(output_tensor, 1)

correct_prediction = tf.equal(index_prediction, index_correct)

# Calculate accuracy

accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

total_test_data = len(newsgroups_test.target)

batch_x_test,batch_y_test = get_batch(newsgroups_test,0,total_test_data)

print("Accuracy:", accuracy.eval({input_tensor: batch_x_test, output_tensor: batch_y_test}))

>>> Epoch: 0001 loss= 1133.908114347

Epoch: 0002 loss= 329.093700409

Epoch: 0003 loss= 111.876660109

Epoch: 0004 loss= 72.552971845

Epoch: 0005 loss= 16.673050320

Epoch: 0006 loss= 16.481995190

Epoch: 0007 loss= 4.848220565

Epoch: 0008 loss= 0.759822878

Epoch: 0009 loss= 0.000000000

Epoch: 0010 loss= 0.079848485

Optimization Finished!

Accuracy: 0.75

最終可以看到,我們的模型預測精度達到了 75%,對於初學者而言,這個成績還是不錯的。至此,我們已經通過 TensorFlow 實現了基於神經網絡模型的文本分類任務。

來源:medium,雷鋒網編譯

相關推薦

推薦中...