用Tensorflow實現CNN文字分類(詳細解釋及TextCNN程式碼解釋)
阿新 • • 發佈:2019-01-09
Ox00: Motivation最近在研究Yoon
Kim的一篇經典之作Convolutional
Neural Networks for Sentence Classification,這篇文章可以說是cnn模型用於文字分類的開山之作(其實第一個用的不是他,但是Kim提出了幾個variants,並有詳細的調參)
wildml對這篇paper有一個tensorflow的實現,具體參見here。其實blog已經寫的很詳細了,但是對於剛入手tensorflow的新人來說程式碼可能仍存在一些細節不太容易理解,我也是初學,就簡單總結下自己的理解,如果對讀者有幫助那將是極好的。
Ox01: Start!我主要對TextCNN這個類進行解讀,具體程式碼在
研究別人程式碼時,時常問自己幾個問題,由問題切入,在讀的過程中找答案,這種方式我個人認為是最efficient的1 這個class的主要作用是什麼?TextCNN類搭建了一個最basic的CNN模型,有input layer,convolutional layer,max-pooling layer和最後輸出的softmax layer。
但是又因為整個模型是用於文字的(而非CNN的傳統處理物件:影象),因此在cnn的操作上相對應地做了一些小調整:
- 對於文字任務,輸入層自然使用了word embedding來做input data representation。
- 接下來是卷積層,大家在影象處理中經常看到的卷積核都是正方形的,比如4*4,然後在整張image上沿寬和高逐步移動進行卷積操作。但是nlp中輸入的“image”是一個詞矩陣,比如n個words,每個word用200維的vector表示的話,這個”image”就是n*200的矩陣,卷積核只在高度上已經滑動,在寬度上和word vector的維度一致(=200),也就是說每次視窗滑動過的位置都是完整的單詞,不會將幾個單詞的一部分“vector”進行卷積,這也保證了word作為語言中最小粒度的合理性。(當然,如果研究的粒度是character-level而不是word-level,需要另外的方式處理)
- 由於卷積核和word embedding的寬度一致,一個卷積核對於一個sentence,卷積後得到的結果是一個vector, shape=(sentence_len - filter_window + 1, 1),那麼,在max-pooling後得到的就是一個Scalar。所以,這點也是和影象卷積的不同之處,需要注意一下。
- 正是由於max-pooling後只是得到一個scalar,在nlp中,會實施多個filter_window_size(比如3,4,5個words的寬度分別作為卷積的視窗大小),每個window_size又有num_filters個(比如64個)卷積核。一個卷積核得到的只是一個scalar太孤單了,智慧的人們就將相同window_size卷積出來的num_filter個scalar組合在一起,組成這個window_size下的feature_vector。
-
最後再將所有window_size下的feature_vector也組合成一個single vector,作為最後一層softmax的輸入。
重要的事情說三遍:一個卷積核對於一個句子,convolution後得到的是一個vector;max-pooling後,得到的是一個scalar。如果對上述講解還有什麼不理解的地方,請移步wildml的另一篇blog,包教包會。
說了這麼多,總結一下這個類的作用就是:搭建一個用於文字資料的CNN模型!
2 一些引數既然TextCNN類是基於YoonKim的思路搭建的,那麼我們接下來一個很重要的步驟就是將paper中提到的各種引數設定都整理出來,有一些引數是關於模型的,有一些引數是關於training的,比如epoch等,這類引數就和模型本身無關,以此來確定我們的TextCNN類需要傳遞哪些引數來初始化。
趕緊把paper開啟,來仔細找找引數吧。
3.1節Hyperparameters and Training部分講到一些,還有一部分在Table1中:
關於model
- filter windows: [3,4,5]
- filter maps: 100 for each filter window
- dropout rate: 0.5
- l2 constraint: 3
- randomly select 10% of training data as dev set(early stopping)
- word2vec(google news) as initial input, dim = 300
- sentence of length: n, padding where necessary
- number of target classes
- dataset size
-
vocabulary size
- mini batch size: 50
- shuffuled mini batch
- Adadelta update rule: similar results to Adagrad but required fewer epochs
-
Test method: standard train/test split ot CV
策略就是在:
在訓練階段,對max-pooling layer的輸出實行一些dropout,以概率p啟用,啟用的部分傳遞給softmax層。
在測試階段,w已經學好了,但是不能直接用於unseen sentences,要乘以p之後再用,這個階段沒有dropout了全部輸出給softmax層。
4 Embedding Layer
訓練過程中並不是每次都會使用全部的vocabulary,而只是產生一個batch(batch中都是sentence,每個sentence標記了出現哪些word(最大長度為sequence_length),因此batch相當於一個二維列表),這個batch就是input_x。
tf.nn.embedding_lookup:查詢input_x中所有的ids,獲取它們的word vector。batch中的每個sentence的每個word都要查詢。所以得到的embedded_chars的shape應該是[None, sequence_length, embedding_size](1)
但是,輸入的word vectors得到之後,下一步就是輸入到卷積層,用到tf.nn.conv2d函式,
再看看conv2d的引數列表:
input: [batch, in_height, in_width, in_channels](2)
filter: [filter_height, filter_width, in_channels, out_channels](3)
對比(1)(2)可以發現,就差一個in_channels了,而最simple的版本也就只有1通道(Yoon的第四個模型用到了multichannel)
因此需要expand dim來適應conv2d的input要求,萬能的tensorflow已經提供了這樣的功能:
This operation is useful if you want to add a batch dimension to a single element. For example, if you have a single image of shape [height, width, channels], you can make it a batch of 1 image with expand_dims(image, 0), which will make the shape [1, height, width, channels].因此只需要
Example:
# ‘t’ is a tensor of shape [2]
shape(expand_dims(t, -1)) ==> [2, 1]
就能在embedded_chars後面加一個in_channels=1
5 Conv and Max-pooling
繼續,看到了一個比較陌生的函式tf.name_scope('xxx')
這個函式的作用參見官方文件
由於在for迴圈內部,filter_size是固定了的,因此可以結合(3):[filter_height, filter_width, in_channels, out_channels]得到,filter_shape = [filter_size, embedding_size, 1, num_filters]
之所以要弄清楚filter shape是因為要對filter的權重矩陣w進行初始化:
這裡為什麼要用tf.truncated_normal()函式呢?
答:tensorflow中提供了兩個normal函式:
- tf.random_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
-
tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
Outputs random values from a truncated normal distribution.也就是說random出來的值的範圍都在[mean - 2 standard_deviations, mean + 2 standard_deviations]內。
The generated values follow a normal distribution with specified mean and standard deviation, except that values whose magnitude is more than 2 standard deviations from the mean are dropped and re-picked.
下圖可以告訴你這個範圍在哪,
conv2d得到的其實是下圖中的<span tabindex="0" class="MathJax" id="MathJax-Element-2-Frame" role="presentation" style="display: inline-block; position: relative;" data-mathml='w⋅x'>w⋅x w⋅x的部分,
還要加上bias項tf.nn.bias_add(conv, b),並且通過relu:tf.nn.relu才最終得到卷積層的輸出<span tabindex="0" class="MathJax" id="MathJax-Element-3-Frame" role="presentation" style="display: inline-block; position: relative;" data-mathml='h'>h h。
那究竟卷積層的輸出的shape是什麼樣呢?
官方文件中有一段話解釋了卷積後得到的輸出結果:
第三部進行了right-multiply之後得到的結果就是[batch, out_height, out_width, output_channels],但是還是不清楚這裡的out_height和out_width到底是什麼。
那就看看wildml中怎麼說的吧
“VALID” padding means that we slide the filter over our sentence without padding the edges, performing a narrow convolution that gives us an output of shape [1, sequence_length - filter_size + 1, 1, 1].哦,這句話的意思是說out_height和out_width其實和padding的方式有關係,這裡選擇了”VALID”的方式,也就是不在邊緣加padding,得到的out_height=sequence_length - filter_size + 1,out_width=1
因此,綜合上面的兩個解釋,我們知道conv2d-加bias-relu之後得到的<span tabindex="0" class="MathJax" id="MathJax-Element-4-Frame" role="presentation" style="display: inline-block; position: relative;" data-mathml='h'>h h的shape=[batch, sequence_length - filter_size + 1, 1, num_filters] 接下來的工作就是max-pooling了,來看一下tensorflow中給出的函式:
tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)
其中最重要的兩個引數是value和ksize。
value相當於是max pooling層的輸入,在整個網路中就是剛才我們得到的<span tabindex="0" class="MathJax" id="MathJax-Element-5-Frame" role="presentation" style="display: inline-block; position: relative;" data-mathml='h'>h h,check了一下它倆的shape是一致的,說明可以直接傳遞到下一層。
另一個引數是ksize,官方解釋說是input tensor每一維度上的window size。仔細想一下,其實就是想定義多大的範圍來進行max-pooling,比如在影象中常見的2*2的小正方形區域對整個h得到feature map進行pooling,但是在nlp中,剛才說到了每一個feature map現在是[batch, sequence_length - filter_size + 1, 1, num_filters]維度的,我們想知道每個output_channels(每個channel是一個vector)的最大值,也就是最重要的feature是哪一個,那麼就是在第二個維度上設定window=sequence_length - filter_size + 1【這裡感覺沒解釋通,待後續探索】
根據ksize的設定,和value的shape,可以得到pooled的shape=[batch, 1, 1, num_filters],
這是一個filter_size的結果(比如filter_size = 3),pooled儲存的是當前filter_size下每個sentence最重要的num_filters個features,結果append到pooled_outputs列表中存起來,再對下一個filter_size進行相同的操作。 等到for迴圈結束時,也就是所有的filter_size全部進行了卷積和max-pooling之後,首先需要把相同filter_size的所有pooled結果concat起來,再將不同的filter_size之間的結果concat起來,最後的到的應該類似於二維陣列,[batch, all_pooled_result]
all_pooled_result一共有num_filters\(100)*len(filter_sizes)(3)個,比如300個
連線的過程需要使用tf.concat,官方給出的例子很容易理解。
最後得到的h_pool_flat也就是[batch, 300]維的tensor。
6 Dropout
7 Output
但是我還有一個疑問是為什麼對b也要進行正則約束?
另外,tf.nn.xw_plus_b()在open api中並沒有提供,參考github上的某個issue
因此可以改為tf.matmul(self.h_drop, W) + b但是不好的地方是無法設定name了。。(用xw_plus_b也不會報錯不改也可以)
還有一個奇怪的地方是,這一層按道理說應該是一個softmax layer,但是並沒有使用到softmax函式,在Yoon的文章中也是直接得到輸出的,
因此,我們也按照這種方式寫程式碼,得到所有類別的score,並且選出最大值的那個類別(argmax)
y的shape為[batch, num_classes],因此argmax的時候是選取每行的max,dimention=1
因此,最後scores的shape為[batch, 1]
8 Loss function得到了整個網路的輸出之後,也就是我們得到了y_prediction,但還需要和真實的y label進行比較,以此來確定預測好壞。
還是使用常規的cross_entropy作為loss function。最後一層是全連線層,為了防止過擬合,最後還要在loss func中加入l2正則項,即l2_loss。l2_reg_lambda來確定懲罰的力度。
9 Accuracy
tf.cast(x, dtype)將bool tensor轉化成float型別的tensor,方便計算
tf.reduce_mean()本身輸入的就是一個float型別的vector(元素要麼是0.0,要麼是1.0),直接對這樣的vector計算mean得到的就是accuracy,不需要指定reduction_indices
0x02: Conclusion後續可能還會對其他部分進行解讀,敬請期待。