cnn文本分类python实现_CNN文本分类
將神經(jīng)網(wǎng)絡(luò)應(yīng)用于大圖像時(shí),輸入可能有上百萬(wàn)個(gè)維度,如果輸入層和隱含層進(jìn)行“全連接”,需要訓(xùn)練的參數(shù)將會(huì)非常多。如果構(gòu)建一個(gè)“部分聯(lián)通”網(wǎng)絡(luò),每個(gè)隱含單元僅僅只能連接輸入單元的一部分,參數(shù)數(shù)量會(huì)顯著下降。卷積神經(jīng)網(wǎng)絡(luò)就是基于這個(gè)原理而構(gòu)建的。這其中的思想就是,降維或者說(shuō)是特征選擇,通過(guò)前面的卷積層或者池化層將重要的特征選取出來(lái),然后全連接進(jìn)行分類。特征是最重要的。
論文所提出的模型結(jié)構(gòu)如下圖所示:
1,這里的輸入層顯示有兩個(gè)channel,其實(shí)我們可以看作是一個(gè),因?yàn)楹笪闹姓f(shuō)到這兩個(gè)channel分別是static和non-static,即使用的詞向量是否隨著訓(xùn)練發(fā)生變化。non-static就是詞向量隨著模型訓(xùn)練變化(Fine tune),這樣的好處是詞向量可以根據(jù)數(shù)據(jù)集做適當(dāng)調(diào)整,但是CS224d課程里也說(shuō)過(guò)當(dāng)數(shù)據(jù)集較小時(shí)不推薦此操作,否則容易產(chǎn)生過(guò)擬合現(xiàn)象。static就是直接使用word2vec訓(xùn)練好的詞向量即可。此外,由圖可知,輸入層是將一個(gè)句子所有單詞(padding)的詞向量進(jìn)行拼接成一個(gè)矩陣,每一行代表一個(gè)詞。每個(gè)句子固定20個(gè)詞,如果不夠的補(bǔ)padding。
2,卷積層,不做過(guò)多解釋。每個(gè)卷積核的大小為filter_size*embedding_size。filter_size代表卷積核縱向上包含單詞個(gè)數(shù),即認(rèn)為相鄰幾個(gè)詞之間有詞序關(guān)系,代碼里使用的是[3,4,5]。embedding_size就是詞向量的維數(shù)。每個(gè)卷積核計(jì)算完成之后我們就得到了1個(gè)列向量,代表著該卷積核從句子中提取出來(lái)的特征。有多少和卷積核就能提取出多少種特征,即圖中在縱深方向上channel的數(shù)量。
3,池化層。文中提到pooling操作就是將卷積得到的列向量的最大值提取出來(lái)。這樣pooling操作之后我們會(huì)獲得一個(gè)num_filters維的行向量,即將每個(gè)卷積核的最大值連接起來(lái)。這樣做還有一個(gè)好處就是,如果我們之前沒(méi)有對(duì)句子進(jìn)行padding操作,那么句子的長(zhǎng)度是不同的,卷積之后得到的列向量維度也是不同的,可以通過(guò)pooling來(lái)消除句子之間長(zhǎng)度不同的差異。
4,全連接層,為了將pooling層輸出的向量轉(zhuǎn)化為我們想要的預(yù)測(cè)結(jié)果,加上一個(gè)softmax層即可。針對(duì)電影評(píng)價(jià)的分類任務(wù),就是將其轉(zhuǎn)化為正面、負(fù)面兩個(gè)結(jié)果。文中還提到了過(guò)擬合的問(wèn)題,因?yàn)閷?shí)驗(yàn)中所使用的數(shù)據(jù)集相對(duì)較小,很容易就會(huì)發(fā)生過(guò)擬合現(xiàn)象,在實(shí)驗(yàn)過(guò)程中也會(huì)發(fā)現(xiàn)當(dāng)?shù)?000多輪的時(shí)候準(zhǔn)確率就會(huì)接近1。所以這里引如dropout來(lái)減少過(guò)擬合現(xiàn)象。此外還可以考慮L2正則化等方法實(shí)現(xiàn)防止過(guò)擬合的功能。
到這里其實(shí)對(duì)論文模型的
數(shù)據(jù)獲取和準(zhǔn)備
在本博客中,我們使用的數(shù)據(jù)集是 Movie Review data from Rotten Tomatoes ,這也是論文中使用的其中一個(gè)數(shù)據(jù)集。這個(gè)數(shù)據(jù)集包含 10662 個(gè)評(píng)論樣本,其中一半是正向評(píng)論,一半是負(fù)向評(píng)論。這個(gè)數(shù)據(jù)集大約有2萬(wàn)個(gè)詞。注意,因?yàn)檫@個(gè)數(shù)據(jù)集很小,所以如果我們使用很復(fù)雜的模型,那么容易造成過(guò)擬合。并且,這個(gè)數(shù)據(jù)沒(méi)有幫我們分離訓(xùn)練數(shù)據(jù)集和測(cè)試數(shù)據(jù)集。因此,我們需要自己去預(yù)處理。在這里,我們把10%的數(shù)據(jù)作為交叉驗(yàn)證集。在原始的論文中,作者使用十折交叉驗(yàn)證(10-fold cross validation)。
數(shù)據(jù)預(yù)處理從原始數(shù)據(jù)文件中,導(dǎo)入正樣本和負(fù)樣本數(shù)據(jù)。數(shù)據(jù)清理,使用和論文中相同的代碼。
將每個(gè)句子填充到最大句子長(zhǎng)度,也就是數(shù)據(jù)集中最長(zhǎng)的那個(gè)句子的長(zhǎng)度,這里是20。我們填充的特殊標(biāo)記是 ,將句子填充到相同長(zhǎng)度是非常有用的,因?yàn)樗軒椭覀冞M(jìn)行有效的批處理,因?yàn)樵谂幚碇械拿總€(gè)例子都必須有相同的長(zhǎng)度。
構(gòu)建詞匯索引表,將每個(gè)單詞映射到 0 ~ 18765 之間(18765是詞匯量大小),那么每個(gè)句子就變成了一個(gè)整數(shù)的向量。
準(zhǔn)備單詞的embeding向量,這里采用訓(xùn)練好的256的Word2vector向量。
初始化textcnn模型
為了允許各種的超參數(shù)配置,我們把我們的代碼放到一個(gè)TextCNN類中,并且在 init 函數(shù)中生成模型圖。
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 ...
為了實(shí)例化類,我們需要傳遞以下參數(shù)到類中:sequence_length - 句子的長(zhǎng)度。請(qǐng)注意,我們通過(guò)添加特殊標(biāo)記,使得所欲的句子都擁有了相同的長(zhǎng)度(我們的數(shù)據(jù)集是20)。
num_classes - 最后一層分類的數(shù)目,在這里我們是進(jìn)行二分類(正向評(píng)論和負(fù)向評(píng)論)。
vocab_size - 詞匯量的大小。這個(gè)參數(shù)是為了確定我們?cè)~向量嵌入層的大小,最終的總詞向量維度是 [vocabulary_size, embedding_size] 。
embeddign_size - 每個(gè)單詞的詞向量的長(zhǎng)度128或者256。
filter_sizes - 這個(gè)參數(shù)確定我們希望我們的卷積核每次覆蓋幾個(gè)單詞。對(duì)于每個(gè)卷積核,我們都將有 num_filters 個(gè)。比如,filter_sizes = [3, 4, 5] , 這就意味著,卷積核一共有三種類型,分別是每次覆蓋3個(gè)單詞的卷積核,每次覆蓋4個(gè)單詞的卷積核和每次覆蓋5個(gè)單詞的卷積核。卷積核一共的數(shù)量是 3 * num_filters 個(gè)。
num_filters - 每個(gè)卷積核的數(shù)量(參考 filter_sizes 參數(shù)的介紹)。
輸入占位符
我們首先定義需要輸入到模型中的數(shù)據(jù)。
# 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 創(chuàng)建了一個(gè)占位符變量,當(dāng)我們?cè)谟?xùn)練階段或者測(cè)試階段時(shí),都可以使用它向我們的模型輸入數(shù)據(jù)。第二個(gè)參數(shù)是輸入張量的形狀。None 的意思是,該維度的長(zhǎng)度可以是任何值。在我們的模型中,第一個(gè)維度是批處理大小,而使用 None 來(lái)表示這個(gè)值,說(shuō)明網(wǎng)絡(luò)允許處理任意大小的批次。
在 dropout 層中,我們使用 dropout_keep_prob 參數(shù)來(lái)控制神經(jīng)元的激活程度。但這個(gè)參數(shù),我們只在訓(xùn)練的時(shí)候開(kāi)啟,在測(cè)試的時(shí)候禁止它。(后續(xù)文章會(huì)深入介紹)
嵌入層
我們定義的第一個(gè)網(wǎng)絡(luò)層是嵌入層,這一層的作用是將詞匯索引映射到低維度的詞向量進(jìn)行表示。它本質(zhì)是一個(gè)我們從數(shù)據(jù)中學(xué)習(xí)得到的詞匯向量表。
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)
在這里,我們又使用了一些新功能,讓我們來(lái)學(xué)習(xí)一下它們:tf.device("/cpu:0") 強(qiáng)制代碼在CPU上面執(zhí)行操作。因?yàn)槟J(rèn)情況下,TensorFlow會(huì)嘗試將操作放在GPU上面進(jìn)行運(yùn)行(如果存在GPU),但是嵌入層的操作目前還不支持GPU運(yùn)行,所以如果你不指定CPU進(jìn)行運(yùn)行,那么程序會(huì)報(bào)錯(cuò)。
tf.name_scope 創(chuàng)建了一個(gè)稱之為"embedding"的新的名稱范圍,該范圍將所有的操作都添加到這個(gè)"embedding"節(jié)點(diǎn)下面。以便在TensorBoard中獲得良好的層次結(jié)構(gòu),有利于可視化。
W 是我們的嵌入矩陣,這個(gè)矩陣是我們從數(shù)據(jù)訓(xùn)練過(guò)程中得到的。最開(kāi)始,我們使用一個(gè)隨機(jī)均勻分布來(lái)進(jìn)行初始化。tf.nn.embedding_lookup 創(chuàng)建實(shí)際的嵌入讀取操作,這個(gè)嵌入操作返回的數(shù)據(jù)維度是三維張量 [None, sequence_length, embedding_size] 。
TensorFlow 的卷積操作 conv2d 需要一個(gè)四維的輸入數(shù)據(jù),對(duì)應(yīng)的維度分別是批處理大小,寬度,高度和通道數(shù)。在我們嵌入層得到的數(shù)據(jù)中不包含通道數(shù),所以我們需要手動(dòng)添加它,所以最終的數(shù)據(jù)維度是 [None, sequence_length, embedding_size, 1] 。
卷積層和池化層
現(xiàn)在我們可以構(gòu)建我們的卷積層和池化層了。請(qǐng)記住,我們使用的卷積核是不同尺寸的。因?yàn)槊總€(gè)卷積核經(jīng)過(guò)卷積操作之后產(chǎn)生的張量是不同維度的,所有我們需要為每一個(gè)卷積核創(chuàng)建一層網(wǎng)絡(luò),最后再把這些卷積之后的覺(jué)果合并成一個(gè)大的特征向量。
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 表示對(duì)經(jīng)過(guò)卷積得到的輸出結(jié)果進(jìn)行非線性處理之后的結(jié)果。每個(gè)卷積核會(huì)覆蓋整個(gè)詞向量長(zhǎng)度,但是滑動(dòng)覆蓋幾個(gè)單詞就是不同的了。VALID 填充意味著,我們的卷積核只在我們的單詞上面滑動(dòng),而不填充邊緣,是執(zhí)行窄卷積,所有最后輸出的維度是 [1, sequence_length - filter_size + 1, 1, 1] 。對(duì)經(jīng)過(guò)特定卷積的輸出,我們做最大池化操作,使得我們得到的張量維度是 [batch_size, 1, 1, num_filters]。這實(shí)質(zhì)上就是一個(gè)特征向量,其中最后一個(gè)維度就是對(duì)應(yīng)于我們的特征。一旦我們擁有了來(lái)自各個(gè)卷積核的輸出向量,那么我們就可以把它們合并成一個(gè)長(zhǎng)的特征向量,該向量的維度是 [batch_size, num_filters_total] 。在 tf.reshape 中使用 -1,就是告訴 TensorFlow 在可能的情況下,將維度進(jìn)行展平。
上面部分最好花點(diǎn)時(shí)間看明白,去弄明白每個(gè)操作輸出的維度是什么。如果你不是很了解,也可以再去參考這篇博客 Understanding Convolutional Neural Networks for NLP,獲得一些靈感。下圖是TensorBoard可視化的結(jié)果,你可以發(fā)現(xiàn)三個(gè)卷積核組成了三個(gè)不同的網(wǎng)絡(luò)層。
Dropout層
一定要用 dropout:有兩種情況可以不用:數(shù)據(jù)量特別小,或者你用了更好的正則方法,比如bn。實(shí)際中我們嘗試了不同參數(shù)的dropout,最好的還是0.5,所以如果你的計(jì)算資源很有限,默認(rèn)0.5是一個(gè)很好的選擇。
Dropout 也許是最流行的方法來(lái)正則化卷積神經(jīng)網(wǎng)絡(luò)。Dropout 的思想非常簡(jiǎn)單,就是按照一定的概率來(lái)“禁用”一些神經(jīng)元的發(fā)放。這種方法可以防止神經(jīng)元共同適應(yīng)一個(gè)特征,而迫使它們單獨(dú)學(xué)習(xí)有用的特征。神經(jīng)元激活的概率,我們從參數(shù) dropout_keep_prob 中得到。我們?cè)谟?xùn)練階段將其設(shè)置為 0.5,在測(cè)試階段將其設(shè)置為 1.0(即所有神經(jīng)元都被激活)。
# Add dropout with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)
分?jǐn)?shù)和預(yù)測(cè)
我們使用來(lái)自池化層的特征向量(經(jīng)過(guò)Dropout),然后通過(guò)全連接層,得到一個(gè)分?jǐn)?shù)最高的類別。我們還可以應(yīng)用softmax函數(shù)來(lái)將原始分?jǐn)?shù)轉(zhuǎn)換成歸一化概率,但這個(gè)操作是保護(hù)會(huì)改變我們的最終預(yù)測(cè)。
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是一個(gè)很方便的函數(shù),實(shí)現(xiàn) Wx + b 操作。
損失函數(shù)和正確率
使用我們上面求得的分?jǐn)?shù),我們可以定義損失函數(shù)。損失值是對(duì)模型所造成的誤差的度量,我們的目標(biāo)是最小化這個(gè)損失值。分類問(wèn)題的標(biāo)準(zhǔn)損失函數(shù)是交叉熵?fù)p失函數(shù)。
# 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 是一個(gè)方便的函數(shù),用來(lái)計(jì)算每個(gè)類別的交叉損失熵,對(duì)于我們給定的分?jǐn)?shù)和輸入的正確標(biāo)簽。然后,我們計(jì)算損失值的平均值。當(dāng)然,我們也可以對(duì)它們進(jìn)行求和,但是這會(huì)對(duì)不同批大小的損失值衡量非常困難,尤其是在訓(xùn)練階段和測(cè)試階段。
我們還定義了一個(gè)正確率的函數(shù),它的作用就是在訓(xùn)練階段和測(cè)試階段來(lái)跟蹤模型的性能。
# 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")
基于深度學(xué)習(xí)技術(shù)的文本分類技術(shù)比起傳統(tǒng)的文本分類模型,例如 LR,SVM 等,有什么優(yōu)勢(shì)呢?
首先,最明顯的優(yōu)勢(shì),深度學(xué)習(xí)不需要人工手動(dòng)的提取文本的特征,它可以自動(dòng)的獲取基礎(chǔ)特征并組合為高級(jí)的特征,訓(xùn)練模型獲得文本特征與目標(biāo)分類之間的關(guān)系,省去了使用TF-IDF等提取句子的關(guān)鍵詞構(gòu)建特征工程的過(guò)程。
其次,相比傳統(tǒng)的N-gram模型而言,深度學(xué)習(xí)中可以更好的利用詞序的特征,CNN的文本分類模型中的filter的size的大小可以當(dāng)做是一種類似于N-gram的方式,而RNN(LSTM)則可以利用更長(zhǎng)的詞序,配合Attention機(jī)制則可以通過(guò)加權(quán)體矩陣體現(xiàn)句子中的核心詞匯部位,attention最早是用于自動(dòng)翻譯中實(shí)現(xiàn)對(duì)應(yīng)詞匯對(duì)齊及可視化的功能。
作者:李良
總結(jié)
以上是生活随笔為你收集整理的cnn文本分类python实现_CNN文本分类的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: sql语句添加删除外键
- 下一篇: 英文Ubuntu安装中文包(locale