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
進行計算,把分值均轉換爲正數,通過這個公式,把分值最高的值凸顯出來,同時將輸出結果映射到(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_x
和input_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())
以此初始化全局變量
電腦性能限制
因爲實驗室不是專門做深度學習這塊,所以沒有較好的設備去完成實驗,目前實驗設備有:
-
CPU
Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz 基準速度: 3.60 GHz 插槽: 1 內核: 4 邏輯處理器: 8
-
GPU 1塊
NVIDIA GeForce RTX 2060 SUPER
專用 GPU 內存 8.0 GB 共享 GPU 內存 8.0 GB GPU 內存 16.0 GB
-
內存
16.0 GB
-
磁盤 1 (D: F: G: E:)
WDC WD10EZEX-21M2NA0
容量: 932 GB 已格式化: 932 GB 系統磁盤: 否 頁面文件: 是 讀取速度 199 KB/秒 寫入速度 41.0 KB/秒 活動時間 56% 平均響應時間 6.2 毫秒
目前硬件上面臨的有幾個困難:
- GPU利用率過低,不知道如何優化
- 讀取文檔的速度過慢,優化方法下面介紹
其實總結一下就是,硬件設備性能低,沒法加快程序運行速度。
優化運行方案
因爲內存的不足,所以在學長的建議下,我把我需要輸入的數據進行分段處理,這樣一來能快速看到效果,二來被打斷的話,不需要重新加載那麼龐大的數據繼續跑,三來我電腦性能限制,我沒法一次性跑那麼多數據。其實第三點纔是最重要的。
這就要來說說python
中yield
的語法了。
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,2,3)
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曲線: