cnn文本分类python实现_CNN文本分类
將神經網絡應用于大圖像時,輸入可能有上百萬個維度,如果輸入層和隱含層進行“全連接”,需要訓練的參數將會非常多。如果構建一個“部分聯通”網絡,每個隱含單元僅僅只能連接輸入單元的一部分,參數數量會顯著下降。卷積神經網絡就是基于這個原理而構建的。這其中的思想就是,降維或者說是特征選擇,通過前面的卷積層或者池化層將重要的特征選取出來,然后全連接進行分類。特征是最重要的。
論文所提出的模型結構如下圖所示:
1,這里的輸入層顯示有兩個channel,其實我們可以看作是一個,因為后文中說到這兩個channel分別是static和non-static,即使用的詞向量是否隨著訓練發生變化。non-static就是詞向量隨著模型訓練變化(Fine tune),這樣的好處是詞向量可以根據數據集做適當調整,但是CS224d課程里也說過當數據集較小時不推薦此操作,否則容易產生過擬合現象。static就是直接使用word2vec訓練好的詞向量即可。此外,由圖可知,輸入層是將一個句子所有單詞(padding)的詞向量進行拼接成一個矩陣,每一行代表一個詞。每個句子固定20個詞,如果不夠的補padding。
2,卷積層,不做過多解釋。每個卷積核的大小為filter_size*embedding_size。filter_size代表卷積核縱向上包含單詞個數,即認為相鄰幾個詞之間有詞序關系,代碼里使用的是[3,4,5]。embedding_size就是詞向量的維數。每個卷積核計算完成之后我們就得到了1個列向量,代表著該卷積核從句子中提取出來的特征。有多少和卷積核就能提取出多少種特征,即圖中在縱深方向上channel的數量。
3,池化層。文中提到pooling操作就是將卷積得到的列向量的最大值提取出來。這樣pooling操作之后我們會獲得一個num_filters維的行向量,即將每個卷積核的最大值連接起來。這樣做還有一個好處就是,如果我們之前沒有對句子進行padding操作,那么句子的長度是不同的,卷積之后得到的列向量維度也是不同的,可以通過pooling來消除句子之間長度不同的差異。
4,全連接層,為了將pooling層輸出的向量轉化為我們想要的預測結果,加上一個softmax層即可。針對電影評價的分類任務,就是將其轉化為正面、負面兩個結果。文中還提到了過擬合的問題,因為實驗中所使用的數據集相對較小,很容易就會發生過擬合現象,在實驗過程中也會發現當迭代3000多輪的時候準確率就會接近1。所以這里引如dropout來減少過擬合現象。此外還可以考慮L2正則化等方法實現防止過擬合的功能。
到這里其實對論文模型的
數據獲取和準備
在本博客中,我們使用的數據集是 Movie Review data from Rotten Tomatoes ,這也是論文中使用的其中一個數據集。這個數據集包含 10662 個評論樣本,其中一半是正向評論,一半是負向評論。這個數據集大約有2萬個詞。注意,因為這個數據集很小,所以如果我們使用很復雜的模型,那么容易造成過擬合。并且,這個數據沒有幫我們分離訓練數據集和測試數據集。因此,我們需要自己去預處理。在這里,我們把10%的數據作為交叉驗證集。在原始的論文中,作者使用十折交叉驗證(10-fold cross validation)。
數據預處理從原始數據文件中,導入正樣本和負樣本數據。數據清理,使用和論文中相同的代碼。
將每個句子填充到最大句子長度,也就是數據集中最長的那個句子的長度,這里是20。我們填充的特殊標記是 ,將句子填充到相同長度是非常有用的,因為它能幫助我們進行有效的批處理,因為在批處理中的每個例子都必須有相同的長度。
構建詞匯索引表,將每個單詞映射到 0 ~ 18765 之間(18765是詞匯量大小),那么每個句子就變成了一個整數的向量。
準備單詞的embeding向量,這里采用訓練好的256的Word2vector向量。
初始化textcnn模型
為了允許各種的超參數配置,我們把我們的代碼放到一個TextCNN類中,并且在 init 函數中生成模型圖。
import tensorflow as tf
import numpy as np
class TextCNN(object):
"""A CNN for text classification.Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer."""
def __init__(
self, sequence_length, num_classes, vocab_size,
embedding_size, filter_sizes, num_filters, l2_reg_lambda=0.0):
# Implementation ...
為了實例化類,我們需要傳遞以下參數到類中:sequence_length - 句子的長度。請注意,我們通過添加特殊標記,使得所欲的句子都擁有了相同的長度(我們的數據集是20)。
num_classes - 最后一層分類的數目,在這里我們是進行二分類(正向評論和負向評論)。
vocab_size - 詞匯量的大小。這個參數是為了確定我們詞向量嵌入層的大小,最終的總詞向量維度是 [vocabulary_size, embedding_size] 。
embeddign_size - 每個單詞的詞向量的長度128或者256。
filter_sizes - 這個參數確定我們希望我們的卷積核每次覆蓋幾個單詞。對于每個卷積核,我們都將有 num_filters 個。比如,filter_sizes = [3, 4, 5] , 這就意味著,卷積核一共有三種類型,分別是每次覆蓋3個單詞的卷積核,每次覆蓋4個單詞的卷積核和每次覆蓋5個單詞的卷積核。卷積核一共的數量是 3 * num_filters 個。
num_filters - 每個卷積核的數量(參考 filter_sizes 參數的介紹)。
輸入占位符
我們首先定義需要輸入到模型中的數據。
# Placeholders for input, output and dropout
self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name="input_x")
self.input_y = tf.placeholder(tf.float32, [None, num_classes], name="input_y")
self.dropout_keep_prob = tf.placeholder(tf.float32, name="dropout_keep_prob")
tf.placeholder 創建了一個占位符變量,當我們在訓練階段或者測試階段時,都可以使用它向我們的模型輸入數據。第二個參數是輸入張量的形狀。None 的意思是,該維度的長度可以是任何值。在我們的模型中,第一個維度是批處理大小,而使用 None 來表示這個值,說明網絡允許處理任意大小的批次。
在 dropout 層中,我們使用 dropout_keep_prob 參數來控制神經元的激活程度。但這個參數,我們只在訓練的時候開啟,在測試的時候禁止它。(后續文章會深入介紹)
嵌入層
我們定義的第一個網絡層是嵌入層,這一層的作用是將詞匯索引映射到低維度的詞向量進行表示。它本質是一個我們從數據中學習得到的詞匯向量表。
with tf.device('/cpu:0'), tf.name_scope("embedding"):
W = tf.Variable(
tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
name="W")
self.embedded_chars = tf.nn.embedding_lookup(W, self.input_x)
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)
在這里,我們又使用了一些新功能,讓我們來學習一下它們:tf.device("/cpu:0") 強制代碼在CPU上面執行操作。因為默認情況下,TensorFlow會嘗試將操作放在GPU上面進行運行(如果存在GPU),但是嵌入層的操作目前還不支持GPU運行,所以如果你不指定CPU進行運行,那么程序會報錯。
tf.name_scope 創建了一個稱之為"embedding"的新的名稱范圍,該范圍將所有的操作都添加到這個"embedding"節點下面。以便在TensorBoard中獲得良好的層次結構,有利于可視化。
W 是我們的嵌入矩陣,這個矩陣是我們從數據訓練過程中得到的。最開始,我們使用一個隨機均勻分布來進行初始化。tf.nn.embedding_lookup 創建實際的嵌入讀取操作,這個嵌入操作返回的數據維度是三維張量 [None, sequence_length, embedding_size] 。
TensorFlow 的卷積操作 conv2d 需要一個四維的輸入數據,對應的維度分別是批處理大小,寬度,高度和通道數。在我們嵌入層得到的數據中不包含通道數,所以我們需要手動添加它,所以最終的數據維度是 [None, sequence_length, embedding_size, 1] 。
卷積層和池化層
現在我們可以構建我們的卷積層和池化層了。請記住,我們使用的卷積核是不同尺寸的。因為每個卷積核經過卷積操作之后產生的張量是不同維度的,所有我們需要為每一個卷積核創建一層網絡,最后再把這些卷積之后的覺果合并成一個大的特征向量。
pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope("conv-maxpool-%s" % filter_size):
# Convolution Layer filter_shape = [filter_size, embedding_size, 1, num_filters]
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W") b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b") conv = tf.nn.conv2d(
self.embedded_chars_expanded,
W,
strides=[1, 1, 1, 1],
padding="VALID", name="conv") # Apply nonlinearity h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu") # Max-pooling over the outputs pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID', name="pool")
pooled_outputs.append(pooled)
# Combine all the pooled features num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(3, pooled_outputs)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])
代碼中,W 表示不同的卷積核,h 表示對經過卷積得到的輸出結果進行非線性處理之后的結果。每個卷積核會覆蓋整個詞向量長度,但是滑動覆蓋幾個單詞就是不同的了。VALID 填充意味著,我們的卷積核只在我們的單詞上面滑動,而不填充邊緣,是執行窄卷積,所有最后輸出的維度是 [1, sequence_length - filter_size + 1, 1, 1] 。對經過特定卷積的輸出,我們做最大池化操作,使得我們得到的張量維度是 [batch_size, 1, 1, num_filters]。這實質上就是一個特征向量,其中最后一個維度就是對應于我們的特征。一旦我們擁有了來自各個卷積核的輸出向量,那么我們就可以把它們合并成一個長的特征向量,該向量的維度是 [batch_size, num_filters_total] 。在 tf.reshape 中使用 -1,就是告訴 TensorFlow 在可能的情況下,將維度進行展平。
上面部分最好花點時間看明白,去弄明白每個操作輸出的維度是什么。如果你不是很了解,也可以再去參考這篇博客 Understanding Convolutional Neural Networks for NLP,獲得一些靈感。下圖是TensorBoard可視化的結果,你可以發現三個卷積核組成了三個不同的網絡層。
Dropout層
一定要用 dropout:有兩種情況可以不用:數據量特別小,或者你用了更好的正則方法,比如bn。實際中我們嘗試了不同參數的dropout,最好的還是0.5,所以如果你的計算資源很有限,默認0.5是一個很好的選擇。
Dropout 也許是最流行的方法來正則化卷積神經網絡。Dropout 的思想非常簡單,就是按照一定的概率來“禁用”一些神經元的發放。這種方法可以防止神經元共同適應一個特征,而迫使它們單獨學習有用的特征。神經元激活的概率,我們從參數 dropout_keep_prob 中得到。我們在訓練階段將其設置為 0.5,在測試階段將其設置為 1.0(即所有神經元都被激活)。
# Add dropout with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)
分數和預測
我們使用來自池化層的特征向量(經過Dropout),然后通過全連接層,得到一個分數最高的類別。我們還可以應用softmax函數來將原始分數轉換成歸一化概率,但這個操作是保護會改變我們的最終預測。
with tf.name_scope("output"):
W = tf.Variable(tf.truncated_normal([num_filters_total, num_classes], stddev=0.1), name="W")
b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")
self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")
self.predictions = tf.argmax(self.scores, 1, name="predictions")
上面代碼中,tf.nn.xw_plus_b是一個很方便的函數,實現 Wx + b 操作。
損失函數和正確率
使用我們上面求得的分數,我們可以定義損失函數。損失值是對模型所造成的誤差的度量,我們的目標是最小化這個損失值。分類問題的標準損失函數是交叉熵損失函數。
# Calculate mean cross-entropy loss with tf.name_scope("loss"):
losses = tf.nn.softmax_cross_entropy_with_logits(self.scores, self.input_y)
self.loss = tf.reduce_mean(losses)
這里,tf.nn.softmax_cross_entropy_with_logits 是一個方便的函數,用來計算每個類別的交叉損失熵,對于我們給定的分數和輸入的正確標簽。然后,我們計算損失值的平均值。當然,我們也可以對它們進行求和,但是這會對不同批大小的損失值衡量非常困難,尤其是在訓練階段和測試階段。
我們還定義了一個正確率的函數,它的作用就是在訓練階段和測試階段來跟蹤模型的性能。
# Calculate Accuracy
with tf.name_scope("accuracy"):
correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))
self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"), name="accuracy")
基于深度學習技術的文本分類技術比起傳統的文本分類模型,例如 LR,SVM 等,有什么優勢呢?
首先,最明顯的優勢,深度學習不需要人工手動的提取文本的特征,它可以自動的獲取基礎特征并組合為高級的特征,訓練模型獲得文本特征與目標分類之間的關系,省去了使用TF-IDF等提取句子的關鍵詞構建特征工程的過程。
其次,相比傳統的N-gram模型而言,深度學習中可以更好的利用詞序的特征,CNN的文本分類模型中的filter的size的大小可以當做是一種類似于N-gram的方式,而RNN(LSTM)則可以利用更長的詞序,配合Attention機制則可以通過加權體矩陣體現句子中的核心詞匯部位,attention最早是用于自動翻譯中實現對應詞匯對齊及可視化的功能。
作者:李良
總結
以上是生活随笔為你收集整理的cnn文本分类python实现_CNN文本分类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql语句添加删除外键
- 下一篇: websocket python爬虫_p