tensorflow進階——(1)讀取數據的背景知識

  • tensorflow基本知識學完了,但是看源碼時,很多代碼都用比較高封裝的用法…所以準備開個系列,整個貫通一遍。

  • TensorFlow程序讀取數據一共有3種方法:

  1. 供給數據(Feeding): 在TensorFlow程序運行的每一步, 讓Python代碼來供給數據。
  2. 從文件讀取數據: 在TensorFlow圖的起始, 讓一個輸入管線從文件中讀取數據。
  3. 預加載數據: 在TensorFlow圖中定義常量或變量來保存所有數據(僅適用於數據量比較小的情況)。

預加載數據

  • 個人目前唯一見過是對應NLP任務中的:embedding層,可能會將pre-train的embedding向量靜態加載進來,其他情況還真沒怎麼用過。
  • 一句話形容就是:將數據直接內嵌到圖中
  • 最最簡單的方式類似與下面:
import tensorflow as tf
x1 = tf.constant([2,3,4])
x2 = tf.constant([4,0,1])

y = tf.add(x1,x2)

with tf.Session() as sess:
    print(sess.run(y))

Feeding

  • 這個數據的讀入方式應該是像我這樣的小白最常用,或者最習慣用的。但是卻是大佬們不常用的。
  • 就是讀取數據和處理數據全部用python程序寫好,然後feed進對應的節點。
  • 簡單用代碼舉一個例子:
import tensorflow as tf
x1 = tf.placeholder(tf.int32)
x2 = tf.placeholder(tf.int32)
#用python產生數據
v1 = [2,3,4]
v2 = [4,0,1]

y = tf.add(x1,x2)

with tf.Session() as sess:
    print(sess.run(y,feed_dict={x1:v1,x2:v2}))

以上兩種方法都很方便,但是遇到大型數據的時候就會很喫力,即使是Feed_dict,中間環節的增加也是不小的開銷,因爲數據量大的時候,TensorFlow程序運行的每一步,我們都需要使用python代碼去從文件中讀取數據,並對讀取到的文件數據進行解碼。最優的方案就是在圖中定義好文件讀取的方法,讓TF自己從文件中讀取數據,並解碼成可用的樣本集。

從流水線(pipeline)說起

最早用到的TensorFlow讀數據的方法就是把數據集中所有的數據都讀到內存中。但是有些數據集很大,沒法一次讀入內存。我們可以按batch讀數據,一次讀入一個batch。如果是純串行的操作,即“讀數據->計算->讀數據”,這樣的計算效率就相當低。

  • 一個可行的改進就是“流水線”(pipeline)。流水線的一個簡明的解釋

  • 流水線(pipeline)是將組合邏輯進行分割,能讓任務以類似並行方式處理,提高系統頻率,提高吞吐量(throughput),使各模塊利用率達到最高。

  • 放到tensorflow裏就是:TensorFlow中,讀取數據的線程負責把數據從硬盤讀到內存中的隊列裏面,計算的線程從內存中的隊列得到數據進行計算。也就是說,讀數據的線程只管把數據讀到內存中,計算的線程只管從內存中取數據。兩者都不會空閒下來等對方。

TensorFlow中的隊列機制

TensorFlow提供了一個隊列機制,通過多線程將讀取數據與計算數據分開。因爲在處理海量數據集的訓練時,無法把數據集一次全部載入到內存中,需要一邊從硬盤中讀取,一邊進行訓練,爲了加快訓練速度,我們可以採用多個線程讀取數據,一個線程消耗數據。

  1. Queue,隊列,本身也是圖中的一個節點。入隊和出隊的操作(enqueue, dequeue)也是圖中的節點,可以修改Queue節點中的內容。類似Variable,用來存放數據。

  2. 如果Queue中的數據滿了,那麼enqueue操作將會阻塞,如果Queue是空的,那麼dequeue操作就會阻塞。如果操作不當,可能會出現程序卡住的問題,我就遇到過這個情況。

  3. 在常用環境中,一般是有多個enqueue線程同時像Queue中放數據,有一個dequeue操作從Queue中取數據。一般來說enqueue線程就是準備數據的線程,dequeue線程就是訓練數據的線程.

明確概念

  • 其實概念只有三個:
  1. Queue是TF隊列和緩存機制的實現
  2. QueueRunner是TF中對操作Queue的線程的封裝
  3. Coordinator是TF中用來協調線程運行的工具
  • 雖然它們經常同時出現,但這三樣東西在TensorFlow裏面是可以單獨使用的,不妨先分開來看待。

Queue

據實現的方式不同(出隊的方式不同),分成具體的幾種類型,例如:

  • tf.FIFOQueue :按入列順序出列的隊列
  • tf.RandomShuffleQueue :隨機順序出列的隊列
  • tf.PaddingFIFOQueue :以固定長度批量出列的隊列
  • tf.PriorityQueue :帶優先級出列的隊列
  • … …
  • 這些類型的Queue除了自身的性質不太一樣外,創建、使用的方法基本是相同的。
  • Queue主要包含入列(enqueue)和出列(dequeue)兩個操作。隊列本身也是圖中的一個節點。其他節點(enqueue, dequeue)可以修改隊列節點中的內容。enqueue操作返回計算圖中的一個Operation節點,dequeue操作返回一個Tensor值。Tensor在創建時同樣只是一個定義(或稱爲“聲明”),需要放在Session中運行才能獲得真正的數值。
  • Enqueue、 EnqueueMany和Dequeue都是特殊的節點。他們需要獲取隊列指針,而非普通的值,如此才能修改隊列內容。我們建議您將它們看作隊列的方法。

下面是一個單獨使用Queue的例子:參考:tensorflow中關於隊列使用的實驗

#創建的圖:一個先入先出隊列,以及初始化,出隊,+1,入隊操作  
q = tf.FIFOQueue(3, "float")  
init = q.enqueue_many(([0.1, 0.2, 0.3],))  

x = q.dequeue()  
y = x + 1  
q_inc = q.enqueue([y])  
  
#開啓一個session,session是會話,會話的潛在含義是狀態保持,各種tensor的狀態保持  
with tf.Session() as sess: 
    
    # 初始化節點
    sess.run(init)  
    
    # 出隊改變
    for i in range(2):  
        sess.run(q_inc) 
    
    # 隊列長度
    quelen =  sess.run(q.size())  
    
    for i in range(quelen):  
            print (sess.run(q.dequeue())) 

0.3
1.1
1.2

QueueRunner

  • 之前的例子中,入隊操作都在主線程中進行,Session中可以多個線程一起運行。 在數據輸入的應用場景中,入隊操作從硬盤上讀取,入隊操作是從硬盤中讀取輸入,放到內存當中,速度較慢。 使用QueueRunner可以創建一系列新的線程進行入隊操作,讓主線程繼續使用數據。如果在訓練神經網絡的場景中,就是訓練網絡和讀取數據是異步的,主線程在訓練網絡,另一個線程在將數據從硬盤讀入內存。

代碼參考:tensorflow中關於隊列使用的實驗

'''
'''
QueueRunner()的使用
'''

q = tf.FIFOQueue(10, "float")  

counter = tf.Variable(0.0)  #計數器

# 給計數器加一
# 返回的是一個op
increment_op = tf.assign_add(counter, 1.0)

# 將計數器加入隊列
enqueue_op = q.enqueue(counter)

# 創建QueueRunner
# 用多個線程向隊列添加數據
# 這裏實際創建了4個線程,兩個增加計數,兩個執行入隊

qr = tf.train.QueueRunner(q, enqueue_ops=[increment_op, enqueue_op] * 2)


#主線程  

sess =  tf.Session()  
    
# 初始化變量
sess.run(tf.initialize_all_variables())  

#啓動入隊線程  
enqueue_threads = qr.create_threads(sess, start=True)  

#主線程  
for i in range(10):              
    print (sess.run(q.dequeue())) 

2.0
2.0
2.0
5.0
4.0
7.0
7.0
10.0
10.0
12.0

  • 並不是我們設想的1,2,3,4,本質原因是增加計數的進程會不停的後臺運行,執行入隊的進程會先執行10次(因爲隊列長度只有10),然後主線程開始消費數據,當一部分數據消費被後,入隊的進程又會開始執行。最終主線程消費完10個數據後停止,但其他線程繼續運行,程序不會結束。
  • 經驗:因爲tensorflow是在圖上進行計算,要驅動一張圖進行計算,必須要送入數據,如果說數據沒有送進去,那麼sess.run(),就無法執行,tf也不會主動報錯,提示沒有數據送進去,其實tf也不能主動報錯,因爲tf的訓練過程和讀取數據的過程其實是異步的。tf會一直掛起,等待數據準備好。現象就是tf的程序不報錯,但是一直不動,跟掛起類似。

Coordinator

  • 最後一個因素是Coordinator。這是負責在收到任何關閉信號的時候,讓所有的線程都知道。最常用的是在發生異常時這種情況就會呈現出來,比如說其中一個線程在運行某些操作時出現錯誤(或一個普通的Python異常)。
  • 個人嘗試代碼後發現代碼有些問題,所以此處暫時保留意見。

從文件中讀取數據(只是讀取方式,這裏沒有加上面說的多線程)

  • 注意下面讀取文件的函數都是tf的內置函數,而不是我們平常python IO文件Open那些函數。

字典

  • 1.從字典結構的數據文件讀取(python數據格式)
    其實就是pickle的使用

詳情看:此博文

從bin文件讀取

  • tf.FixedLengthRecordReader 類來每次讀取固定長度的字節,正好對應一個樣本存儲的字節(包括label)。
  • 用tf.decode_raw進行解析。

從CSV(TXT)文件讀取

  • tf.TextLineReader 類來每次讀取一行,並使用tf.decode_csv來對每一行進行解析

從TFRecord文件讀取

  • TFrecord是Tensorflow推薦的數據集格式,與Tensorflow框架緊密結合。在TensorFlow中提供了一系列接口可以訪問TFRecord格式。

多線程讀取文件

QueueRunner和Coordinator經典結合方式一

  • 顯式的創建QueueRunner,然後調用它的create_threads方法啓動線程
  • 但我們一般都是IO文件
import numpy as np
# 1000個4維輸入向量,每個數取值爲1-10之間的隨機數
data = 10 * np.random.randn(1000, 4) + 1
# 1000個隨機的目標值,值爲0或1
target = np.random.randint(0, 2, size=1000)

# 創建Queue,隊列中每一項包含一個輸入數據和相應的目標值
queue = tf.FIFOQueue(capacity=50, dtypes=[tf.float32, tf.int32], shapes=[[4], []])

# 批量入列數據(這是一個Operation)
enqueue_op = queue.enqueue_many([data, target])
# 出列數據(這是一個Tensor定義)
data_sample, label_sample = queue.dequeue()

# 創建包含4個線程的QueueRunner
qr = tf.train.QueueRunner(queue, [enqueue_op] * 4)

with tf.Session() as sess:
    # 創建Coordinator
    coord = tf.train.Coordinator()
    # 啓動QueueRunner管理的線程
    enqueue_threads = qr.create_threads(sess, coord=coord, start=True)
    # 主線程,消費100個數據
    for step in range(100):
        if coord.should_stop():
            break
        data_batch, label_batch = sess.run([data_sample, label_sample])
        print(data_batch)
        print(label_batch)
        
    # 主線程計算完成,停止所有采集數據的進程
    coord.request_stop()
    coord.join(enqueue_threads)

QueueRunner和Coordinator經典結合方式二

  • 隱式創建線程的方法。

  • 在講解具體代碼之前,我們需要先來講解關於TensorFlow中的文件隊列機制和內存隊列機制。

  • 具體可以參考:

  • 十圖講解tensorflow內存讀取機制

  • 針對不同的文件,其實就是解析方式不同,代碼可以參考

  • 上面那篇論可以結合這篇文章

例如我們要讀取CSV:單個reader,多個樣本

import tensorflow as tf
filenames = ['A.csv', 'B.csv', 'C.csv']
filename_queue = tf.train.string_input_producer(filenames, shuffle=False)
reader = tf.TextLineReader()
key, value = reader.read(filename_queue)
example, label = tf.decode_csv(value, record_defaults=[['null'], ['null']])

# 使用tf.train.batch()會多加了一個樣本隊列和一個QueueRunner。Decoder解後數據會進入這個隊列,再批量出隊。
# 雖然這裏只有一個Reader,但可以設置多線程,相應增加線程數會提高讀取速度,但並不是線程越多越好。

example_batch, label_batch = tf.train.batch( [example, label], batch_size=5)

with tf.Session() as sess:
	coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)
    
    for i in range(10):
    	print(example_batch.eval())
    coord.request_stop()
    coord.join(threads)
# output
# ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2']
# ['Bee3' 'Sea1' 'Sea2' 'Sea3' 'Alpha1']
# ['Alpha2' 'Alpha3' 'Bee1' 'Bee2' 'Bee3']
# ['Sea1' 'Sea2' 'Sea3' 'Alpha1' 'Alpha2']
# ['Alpha3' 'Bee1' 'Bee2' 'Bee3' 'Sea1']
# ['Sea2' 'Sea3' 'Alpha1' 'Alpha2' 'Alpha3']
# ['Bee1' 'Bee2' 'Bee3' 'Sea1' 'Sea2']
# ['Sea3' 'Alpha1' 'Alpha2' 'Alpha3' 'Bee1']
 # ['Bee2' 'Bee3' 'Sea1' 'Sea2' 'Sea3']
 # ['Alpha1' 'Alpha2' 'Alpha3' 'Bee1' 'Bee2']

tf.train.batch(tensors, batch_size, num_threads=1, capacity=32, enqueue_many=False, shapes=None, dynamic_pad=False, allow_smaller_final_batch=False, shared_name=None, name=None)

對比,多reader
在這裏插入圖片描述

tf.train.batch與tf.train.shuffle_batch函數是單個Reader讀取,但是可以多線程。tf.train.batch_join與tf.train.shuffle_batch_join可設置多Reader讀取,每個Reader使用一個線程。至於兩種方法的效率,單Reader時,2個線程就達到了速度的極限。多Reader時,2個Reader就達到了極限。所以並不是線程越多越快,甚至更多的線程反而會使效率下降。

參考

[ 1 ]TensorFlow讀取數據的幾種方法以及隊列的使用

[ 2 ]tensorflow 手冊

[ 3 ]線程和隊列 極客時間

[ 4 ]TensorFlow中的Queue和QueueRunner

[ 5 ]tensorflow中關於隊列使用的實驗

[ 6 ]Pipleline 流水線

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