TensorFlow實現cnn踩坑點


title: TensorFlow實現cnn踩坑點
copyright: true
date: 2019-12-25 20:20:00
categories:

  • nlp自然語言處理
  • CNN語言模型
    tags:
  • nlp
    mathjax: true

因爲是新手,所以跟着網上的教程用tensorflow框架實現了CNN模型,並且開始了訓練。在實現過程中經歷了很多的坑,今天記錄一下在實現過程中遇到過的坑。

教程博客

數據預處理

我一直覺得數據處理只是簡單的對數據進行數據加載,然後清洗(去掉停用詞、標點符號、轉義字符–比如\n以及空格等冗餘數據)

其實,要做的處理並不止這些。

標籤處理

在預處理中需要把不同文本對應的標籤轉換爲one-hot類型:

def dense_to_one_hot(self, labels_dense, num_classes):    
    """Convert class labels from scalars to one-hot vectors.""" 
    num_labels = labels_dense.shape[0]    
    index_offset = np.arange(num_labels) * num_classes    
    labels_one_hot = np.zeros((num_labels, num_classes))    
    temp = index_offset + labels_dense.ravel()    
    labels_one_hot.flat[temp] = 1    
    return labels_one_hot

在後面隱藏層輸出結果與對應正確標籤計算時候是需要的。

因爲後面:

with tf.name_scope("output"):    
    W = tf.Variable(tf.truncated_normal([self.num_filters_total, self.num_classes]), name="W")    
    b = tf.Variable(tf.constant(0.1, shape=[self.num_classes]), name="b")       if l2_reg_lambda:        
        W_l2_loss = tf.contrib.layers.l2_regularizer(l2_reg_lambda)(W)    
        tf.add_to_collection("losses", W_l2_loss)    
        self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")             # (self.h_drop * W + b)    
        self.predictions = tf.argmax(self.scores, 1, name="predictions")           # 找出分數中的最大值,就是預測值

全連接輸出時候,裏面會通過tf.nn.xw_plus_b()計算得到的不同種類對應得到的score和以one-hot表示的標籤下標值進行softmax計算誤差,也就是loss值。

losses = tf.nn.softmax_cross_entropy_with_logits_v2(self.input_y, self.scores)
# softmax_cross_entropy_with_logits_v2第一個是對應標籤的labels值,第二個logits值是預測分數,會自動轉爲對應的標籤值進行計算

文本處理

文本處理除掉上面說過的去停用詞去標點符號去轉義字符等,還需要把文本處理成機器可讀取的模式,就是把文本變成數字。

我剛開始對這個的理解就是把文本轉換爲對應的詞向量,再喂進模型,結果發現,並不是這樣。

如果直接把詞向量做詞嵌入處理,這樣會導致工程浩大(因爲會有很多重複的詞出現在文檔裏,在喂進模型前要把對應的詞向量矩陣搗鼓出來,這樣的話,就會消耗計算機大量的內存,降低速率。)所以我們會先把清洗後的文本轉換成對應的文本下標的ID

vocab_processor =learn.preprocessing.VocabularyProcessor(normal_param.max_document_length)
x = np.array(list(vocab_processor.fit_transform(x_texts)))

【注:x_texts是已經經過清洗的數據,x就是x_texts對應的下標矩陣】

爲了防止之後加載模型時候,測試文件不能轉換爲和訓練集一樣對應的下標ID,所以在訓練集轉換完對應的ID後,用

然後傳入cnn模型中word embeding部分,找到對應的詞向量,進行後續操作。

with tf.device('/cpu:0'), tf.name_scope("embedding"):    
    self.W = tf.Variable(tf.random_uniform([self.vocab_size, self.embedding_size], -1.0, 1.0))   
    # 隨機化W,得到維度是self.vocab_size個self.embedding_size大小的矩陣,隨機值在-1.0-1.0之間 
    self.embedding_chars = tf.nn.embedding_lookup(self.W, self.input_x)    
    # 從id(索引)找到對應的One-hot encoding    
    self.embedding_chars_expanded = tf.expand_dims(self.embedding_chars, -1)    # 增加維度

W相當於詞袋模型,每個詞都是個獨立的個體,所以通過tf.variable設置隨機變量,然後再根據前面處理好的下標過來找到不同的詞對應的embeding。

因爲是詞袋模型,所以不好的地方在於,會丟失上下文的關係,造成準確度偏低的情況。

所以會有詞預處理模型,比如Word2vec、最近特別熱門的Bert這些模型,都會想辦法獲取詞的上下文關係,得到對應的詞向量,增加預測結果的準確性。

模型搭建

在搭建CNN模型的過程中也碰到過一些問題,因爲是新手,看着論文,也不知道如何下手去搭建這個模型,所以跟着教程才寫了出來,在討論碰到的問題之前,先總結一下經驗。

先不提python語法問題,python是個很好的東西,對新手來說,有很多庫,上手很快,是個很優美的語言。特別是在你瞭解到它的面向對象的特性的時候,它的優美實現會讓你瞬間愛上它,反正我還沒學會python。它是面向對象,我是面向百度谷歌。

接下來要說的是tensorflow框架了,TensorFlow框架是個坑,它升級到2.0版本,和1.0版本有很多不兼容的地方,但因爲是新出的2.0,普及性還不是很廣,出了問題翻遍它官方的issue都不一定能找到答案,所以我配合我的cuda版本裝了TensorFlow1.13.1。其實這個因果關係不是特別緊密,我只是想吐槽它升級版本順便改了改語法,讓我初學者翻了一下午的資料,才發現爲啥教程上是這樣寫沒問題,我這樣寫就有問題了。

回到正文。

在用代碼搭建CNN模型中,作者用了tf.name_scope("")把這個實現分成了詞嵌入、卷積-池化、dropout、輸出、loss值計算、準確值計算這六個部分

tf.name_scope(""):它給我感覺就是把複雜的網絡分開來,變成幾個小的模塊,不會因爲網絡複雜了,而數據混亂沒有條理,導致出錯。實際上和我想的差不多吧。

loss絕對值增大

當時loss值的絕對值越來越大,並沒有收斂的趨勢,我找了很久的問題,在學長的幫助下才發現出在這裏:

losses = tf.nn.softmax_cross_entropy_with_logits_v2(self.input_y, self.scores)

這是第一個坑, softmax_cross_entropy_with_logits_v2第一個是對應標籤的labels值,第二個logits值是預測分數,會自動轉爲對應的標籤值進行計算出相應的loss值。

講到這裏,不得不提及一下softmax是什麼了,參考博客博客裏面給出了相關公式,是將每個類別給出的分數通過softmax進行計算,把分值均轉換爲正數,通過Si=eijejS*{i} = \frac{e^{i}}{\sum*{j}e^{j}}這個公式,把分值最高的值凸顯出來,同時將輸出結果映射到(0,1)之間,轉換成概率。

loss值是由損失函數算出來的,這裏使用交叉熵函數作爲我們的損失函數。這裏有交叉熵函數的公式。參考博客

對tf運行的理解

        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")

tf和平常代碼不一樣,日常代碼都是先賦值,再進行運算,而tf是先定義運算,再進行初始化賦值,就像上面這兩行代碼一樣,這兩行代碼是預定義了input_xinput_y的值,然後再在後面

step, summaries, loss, accuracy = sess.run([global_step, dev_summary_op, cnn_init.loss, cnn_init.accuracy], feed_dic)

把定義好feed_dic的值,喂進初始化後的cnn模型中,再返回[global_step, dev_summary_op, cnn_init.loss, cnn_init.accuracy]裏面的值(cnn_init就是初始化後的CNN模型)

global_step, dev_summary_op和後面的值不一樣,不是從模型裏面輸出的值,

模型存取

在程序的運行中,會因爲各種問題導致程序突然中止,所以我想要運行到一定時間就自動保存一個正在學習的數據模型,再次運行時候,如果已經有保存了的模型,就加載出來。

checkpoint_dir = os.path.abspath(os.path.join(out_dir, "checkpoint"))
checkpoint_prefix = os.path.join(checkpoint_dir, "model")
if not os.path.exists(checkpoint_dir):    
    os.makedirs(checkpoint_dir)
saver = tf.train.Saver(tf.global_variables())ckpt = tf.train.latest_checkpoint(checkpoint_dir)
if ckpt:    
    saver.restore(sess, ckpt)    
    print("CNN restore from the checkpoint {0}".format(ckpt))

繼續訓練後,運行了好幾遍,發現並沒有辦法加載已有模型繼續訓練,究其原因,發現,問題出在session初始化上面。

sess.run(tf.global_variables_initializer())

參考博文

以此初始化全局變量

電腦性能限制

因爲實驗室不是專門做深度學習這塊,所以沒有較好的設備去完成實驗,目前實驗設備有:

  1. CPU

     Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
     
     基準速度:	3.60 GHz
     插槽:	1
     內核:	4
     邏輯處理器:	8
    
  2. GPU 1塊

    NVIDIA GeForce RTX 2060 SUPER

     專用 GPU 內存	8.0 GB
     共享 GPU 內存	8.0 GB
     GPU 內存	16.0 GB
    
  3. 內存

    16.0 GB

  4. 磁盤 1 (D: F: G: E:)

    WDC WD10EZEX-21M2NA0

     容量:	932 GB
     已格式化:	932 GB
     系統磁盤:	否
     頁面文件:	是
     
     讀取速度	199 KB/秒
     寫入速度	41.0 KB/秒
     活動時間	56%
     平均響應時間	6.2 毫秒
    

目前硬件上面臨的有幾個困難:

  1. GPU利用率過低,不知道如何優化
  2. 讀取文檔的速度過慢,優化方法下面介紹

其實總結一下就是,硬件設備性能低,沒法加快程序運行速度。

優化運行方案

因爲內存的不足,所以在學長的建議下,我把我需要輸入的數據進行分段處理,這樣一來能快速看到效果,二來被打斷的話,不需要重新加載那麼龐大的數據繼續跑,三來我電腦性能限制,我沒法一次性跑那麼多數據。其實第三點纔是最重要的。

這就要來說說pythonyield的語法了。

yield相當於return,不同的是,它不會結束程序的運行,而是讓程序在經過yield的處理後,回到開始的循環,繼續向下執行。可能這樣幹說不太能很好的表達,所以我寫一段。

def batch_iter(a, b, num_foreach):
    for i in range(num_foreach):
        c = a + b
        yield c

if __name__ == "__main__":
    batch = batch_iter(1,23)
    for b in batch:
        t = b
        print(t)

輸出:

3
3
3

就是迭代器,裏面batch_iter這個方法就是迭代時候會使用的方法,然後迭代完c就是輸出的值。

根據這個語法的特性,我改進了一下代碼:

batches = process_data_init.batch_iter(normal_param.batch_size, normal_param.num_epochs)
for batch, dev_data, dev_label, is_save in batches:    
    x_batch, y_batch = zip(*batch)    # print("y_batch", y_batch) 
    train_step(x_batch, y_batch, train_summary_write)    
    current_step = tf.train.global_step(sess, global_step)    
    if current_step % normal_param.evaluate_every == 0:        
        print("\nEvaluation:")        
        dev_step(dev_data, dev_label, writer=dev_summary_writer)

把數據集的讀取改到迭代時候讀取。

def batch_iter(self, batch_size, num_epochs, shuffle=True):    
    '''迭代器'''    
    # num = 1    # data = np.array(data)    # data_size = len(data)    # num_batches_per_epoch = int((data_size - 1) / batch_size) + 1    
    echo_part_num = len(self.all_text_path) // normal_param.num_database    
    for epoch in range(num_epochs):        
        print("epoch:", epoch, "/", num_epochs)        
        for part_n in range(normal_param.num_database):            
            is_save = False            
            train_data, train_label, dev_data, dev_label, vocal_size_train = self.deal_data(part=echo_part_num,n_part=part_n)            
            data = list(zip(train_data, train_label))            
            data = np.array(data)            
            data_size = len(data)            
            num_batches_per_epoch = int((data_size - 1) / batch_size) + 1            
            if shuffle:                
                shuffle_indices = np.random.permutation(np.arange(data_size))         
                shuffle_data = data[shuffle_indices]            
             else:                
                shuffle_data = data            
             for batch_num in range(num_batches_per_epoch):                
                 start_idx = batch_num * batch_size                
                 end_idx = min((batch_num + 1) * batch_size, data_size)                
                 if batch_num + 1 == num_batches_per_epoch:                    
                    is_save = True                
                    yield shuffle_data[start_idx:end_idx], dev_data,dev_label,is_save

調參

根據數據集大小以及過擬合情況,適當調整了batch_size的大小和學習率的大小、dropout的大小以及迭代的次數:

batch_size = 64
num_epochs = 50
#(視情況定,驗證集loss值上升結束後開始下降說明學習已經結束)
dropout_keep_prob = 0.5
#學習率設置:1e-3
optimizer = tf.train.AdamOptimizer(1e-3)

結果

數據集我用的是THUCNew1,裏面有14個class,將前三個class抽取出來,並且每個類別中的數據以10:1的比例抽取,所以實驗數據集有6,472個文件數據,實驗中使用TensorBoard可視化數據,結果如下:

神經網絡流程圖可視化

在這裏插入圖片描述

驗證集的loss曲線圖:

在這裏插入圖片描述

驗證集的acc的曲線:

在這裏插入圖片描述

訓練集的acc曲線:

在這裏插入圖片描述
訓練集的loss曲線:

在這裏插入圖片描述
實現代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章